Skip to content

fix(payouts): tell the operator what goes in the loot paste field - #136

Merged
guarzo merged 8 commits into
design-sweep/2026-08-06-3from
design-sweep/2026-08-06-4
Aug 6, 2026
Merged

fix(payouts): tell the operator what goes in the loot paste field#136
guarzo merged 8 commits into
design-sweep/2026-08-06-3from
design-sweep/2026-08-06-4

Conversation

@guarzo

@guarzo guarzo commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Round 4 of the autonomous design-sweep-loop run. One theme: the two loot paste fields.

What this round resolved

Both loot textareas were labelled Loot paste with no format. On the composer that field sits ten lines above its own sibling:

Loot paste (optional)
Roster paste (optional: one per line, or separated by /)

Two pastes on one form; one answers "what do I put here" and the other does not.

Cost to an operator: creating an operation means facing a ten-row empty box with no indication of the expected shape. An EVE inventory paste works, but nothing on screen says so, and a hand-typed list is a guess. parseLootPaste also drops a bare quantity on its own line rather than absorbing it (core/loot-paste.ts:142-148) — correct behaviour, added precisely because absorbing it produced a silent zero-priced row — but the operator only meets that rule after submitting.

The hint is written from the parser rather than from an ideal format. parseLootPaste accepts a quantity before the name, after the name, or as column two of a tab-separated line (loot-paste.ts:153-170), so "one item per line, quantity before or after the name" is true of every accepted shape and the inventory-window paste is covered without spelling it out.

Applied to both payouts/new/new-operation-form.tsx and payouts/[id]/appraise-form.tsx, because both feed the same parser and an operator who learns the format on one should not rediscover it on the other. The detail-page field is required, so its hint drops the "optional:".

No test breakage. e2e/payouts.spec.ts:1718,1725 use getByLabel("Loot paste") without exact: true, which is a substring match, so the longer label still resolves. (Flagging it because test:e2e is not run per round — see gates.)

What was skipped, and why

/payouts lede saying "Every fight operation" — dropped as a false positive. I had this queued as a typo for "fleet"/"flight". It is deliberate domain language: payouts/new/page.tsx:42 reads "One row per fight," and the feature commit that introduced the surface is #65, "track fight payouts." Recording it because a future sweep will find it again and should stop at the same place.

Refresh on /admin/sync looking identical to the two mutating controls beside it — left alone. The finding is real: <a className="btn">Refresh</a> sits in the same .btn-row as Recheck invalid affiliations, at the same grade, and only one of them enqueues jobs. But DESIGN.md:184-187 defines the three button grades by prominence (primary / default / quiet), not by whether a control mutates. Demoting Refresh to quiet would be inventing a convention the design system does not have, on the strength of one instance. That is a decision, not a consistency fix.

What needs a human

Unchanged and accumulating. From this round's surface: the payout forms still have no per-field error association (aria-invalid / aria-describedby) — messages name their field in prose, which satisfies WCAG 3.3.1's letter, but a screen-reader user who tabs into a ten-row textarea loses the announcement. That is the fix this round's label hint sits next to and does not address.

Also open: /admin/accounts approve buttons not saying that Associate locks the tier (#134); focus dropping to <body> after /account self-service actions (#132); no way to find a past operation on /payouts; no confirm-arm on tier-set.

Gate results

Gate Result
scripts/check-node-version.sh pass
npm run typecheck pass
npm run lint pass
npm run format:check All matched files use Prettier code style!
npm run build exit 0
npm test 76 files, 1085 tests passed — identical to baseline
docker build . not run (slow, environment-bound)
npm run test:e2e not run (needs a migrated DB; known tree-dirtier)

Stack position

PR 4 of the stack, based on design-sweep/2026-08-06-3 (#134).

Merge #131#132#134 → this, and prefer a merge commit or rebase over squash.


Correction: this round broke 22 e2e specs, and CI caught it

The label shipped above originally read "Loot paste (optional: one item per line, quantity before or after the name)". getByLabel matches on a substring, so that label was also a match for getByLabel("Name") — the Name input is ten lines above it on the same form — and the two resolving together is a Playwright strict-mode violation:

Error: locator.fill: Error: strict mode violation: getByLabel('Name') resolved to 2 elements:
    1) <input value="" required="" name="name" class="field"/> aka getByRole('textbox', { name: 'Name (required)' })
    2) <textarea rows="10" class="field" name="lootPaste"></textarea> aka getByRole('textbox', { name: 'Loot paste (optional: one' })

Every payouts spec starts from that getByLabel("Name").fill(...) line, so 22 of them failed at setup. The suite also printed a relation "payout_operation" does not exist stack trace, which is unrelated server noise and much louder than the real cause — worth saying, because it is what a first read of the log lands on.

Fixed in c44517f, on this branch, and merged forward into -5 and -6 so #137 and #138 carry it too. Both labels now read "one line per item, quantity before or after", which says the same thing without using a word another label on the page owns. The constraint is written into the comment above each so the wording is not "improved" back later.

How it got through: npm run test:e2e was one of the two gates this run declared it was skipping per round (see the gate table above). That declaration is why the failure was attributable to this round in one step — the PRs below it were green — but skipping it is what let the commit land in the first place. This is the sweep's characteristic failure mode: copy is a selector, so a purely cosmetic diff can break a browser suite while typecheck, lint, build and unit tests all stay green.

Verified by running the gate that caught it, locally, before pushing:

Gate Result
npm run test:e2e -- payouts.spec.ts 57 passed (4.0m)
npm run typecheck pass
npm run lint pass
npm run format:check All matched files use Prettier code style!

Latent fragility left alone, deliberately: getByLabel("Name") is non-exact and will collide with any future label containing the word "name". Tightening it to { exact: true } is a change to e2e/payouts.spec.ts — a settled decision this sweep is not the place to overturn — so it is flagged here rather than fixed.

guarzo added 4 commits August 5, 2026 23:54
…unced

`Notice`'s own docblock names the `&&` form as "the one shape that defeats
the live region it just asked for": it inserts a role="alert"/"status" node
with its text already inside it, and AT announces a *change* to a region far
more reliably than a region born holding text. The docblock adds that keeping
the behaviour in the primitive is the point, "so the next caller cannot omit
it by writing `&&`" — but six call sites across four files did exactly that.
Only admin/sync had adopted the sanctioned form.

Cost: an admin on NVDA or JAWS who freezes an account, approves a pending
one, queues a sync, hits a mutation error on a payout, or narrows the audit
filter to an ambiguous actor is not reliably told what happened. Every one of
those paths ends in a server-action redirect that re-renders without a
document load, which is precisely the case the region was built for and
precisely the case `&&` breaks.

Converting is free of layout cost: an empty `Notice` renders `.notice-slot`,
which globals.css deliberately takes out of flow (position: absolute) after an
earlier regression put ~64px of dead space on /payouts/new.

Deliberately not converted, and left for a later round:
- payouts/[id] `droppedReport` and account `firstSyncPending` are conditional
  content blocks rather than single-expression message slots. The first is a
  genuine mutation outcome and probably should follow; it is a larger diff and
  a separate judgement, so it is reported rather than bundled here.

Gates run: typecheck, lint, format:check, build, npm test (76 files, 1085
tests). Not run this round: docker build, test:e2e.
…t excludes

Two findings on /account, both cases of the code contradicting a comment
sitting directly above it.

The "Add character" link took `.btn--primary` whenever `health.attention === 0`,
under a comment reading "Gold only in the nominal state." The verdict ladder
(core/account-health.ts:163-177) makes `stalled`, `discord-stale` and
`first-sync-pending` all satisfy `attention === 0` — so on three of the four
non-nominal states the page rendered its loudest control directly beneath a
`.verdict` line telling the member something was wrong. A member whose sync has
stopped opens the page and the brightest thing on it invites them to add another
character to the pile. Keyed off `health.verdict === "nominal"` instead, which
is the test the comment already describes. A zero-character account still
computes to `nominal`, so the case that comment calls out explicitly is
unchanged.

The standalone Discord unlink used `.btn--micro` — the 28px grade DESIGN.md:228
reserves for "the in-row controls of the admin tables... and nowhere else."
It sits in the facts grid, not a table row. inline-edit.tsx:75-83 already made
this exact call for this exact grid, and `.inline-edit--standalone`
(globals.css:1695) exists only to restore the floor where a class had to keep
its colouring; nothing had to be kept here, so dropping `--micro` is the whole
fix. A member on a phone gets a 36px target for the one control on the page
that strips every managed role in the guild. The heavier-at-rest argument in the
comment above it is a colour decision and is untouched.

Gates: node-version, typecheck, lint, format:check, build, npm test
(76 files / 1085 tests) all pass. docker build and test:e2e not run.
…ders tiers

The pending-approval pair rendered Alumni before Associate. Every other tier
list on the page runs member, associate, alumni: the filter chips (TIER_FILTERS,
line 195) and the set-tier group in the same drawer, in the same `.btn-group`
slot, two rows apart (TIERS, line 710).

Cost to an admin: approving the queue is a scanning task done several times a
week, and the one control group that reverses the order is the one whose press
cannot be taken back with the same control. `approveAccount` refuses once the
tier is no longer pending (services/admin-accounts.ts:132), so a mis-click has
to be corrected from the set-tier group instead — after it has already written
a `tier.approved` audit row naming the wrong tier and enqueued an account sync.
The two buttons are also asymmetric in a way neither label shows: associate
sets `tierLocked: true`, alumni leaves the tier under automatic management
(line 133). Muscle memory landing one position off does not land on a
near-miss.

Derived from TIERS via APPROVE_TIERS rather than reordered in place, so the two
groups cannot drift apart again. `member` is excluded because approveAccount's
signature accepts only the other two, which is what makes the derivation
type-checked rather than a convention.

No behaviour change beyond render order: both e2e specs that drive these
buttons select by accessible name, not position.

Gates: node-version, typecheck, lint, format:check, build, npm test
(76 files / 1085 tests) all pass. docker build and test:e2e not run.
Both loot textareas were labelled "Loot paste" with no format. On the composer
that field sits ten lines above "Roster paste (optional: one per line, or
separated by /)" — two sibling pastes on one form, one of which answers "what
do I put here" and one of which does not.

Cost to an operator: creating an operation means facing a ten-row empty box
with no indication of the expected shape. An EVE inventory paste works, but
nothing says so, and a hand-typed list is a guess. A bare quantity on its own
line is dropped rather than absorbed (core/loot-paste.ts:145) — correct
behaviour, added precisely because absorbing it produced a silent wrong answer,
but the operator only meets that rule after submitting.

Both fields get the same hint because both feed parseLootPaste; an operator who
learns the format on the composer should not rediscover it on the detail page.
The wording follows the parser rather than describing an ideal: quantity is
accepted on either side of the name, and the tab-separated inventory layout is
read from column two, so "one item per line" covers the paste case without
spelling it out.

No test breakage: e2e/payouts.spec.ts:1718 selects with getByLabel("Loot
paste"), which is a substring match, so the longer label still resolves.

Gates: node-version, typecheck, lint, format:check, build, npm test
(76 files / 1085 tests) all pass. docker build and test:e2e not run.
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 11 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1b00df65-b1e4-47a2-9413-fcb00fd9a513

📥 Commits

Reviewing files that changed from the base of the PR and between a477e11 and 1e63627.

📒 Files selected for processing (2)
  • src/app/payouts/[id]/appraise-form.tsx
  • src/app/payouts/new/new-operation-form.tsx

Comment @coderabbitai help to get the list of available commands.

The hint added in this round ended "…quantity before or after the name".
`getByLabel` matches on a substring, so that label was also a match for
`getByLabel("Name")` — the Name input sits ten lines above it on the same
form — and the two resolving together is a Playwright strict-mode violation.
It took out 22 specs in e2e/payouts.spec.ts, every one of them at the
`getByLabel("Name").fill(...)` line that every payouts test starts from.

Cost to a user: none directly — the hint reads the same either way. The cost
was to the next person to touch this file, who would have inherited a red
payouts suite with a failure message pointing at the Name field rather than
at the label that actually changed.

Reworded to "one line per item, quantity before or after" on both the
composer and the appraisal form, which says the same thing without using a
word that another label on the page owns. The constraint is written into the
comment above each so the wording is not "corrected" back later.

Found by CI, not by the sweep: `npm run test:e2e` was one of the two gates
this run declared it was skipping per round. That declaration is why the
failure was attributable in one step, but skipping it is what let the commit
land. Verified here by running the suite locally — 57 passed in
e2e/payouts.spec.ts — plus typecheck, lint and format:check.
@guarzo

guarzo commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

# Conflicts:
#	src/app/admin/accounts/page.tsx
guarzo added a commit that referenced this pull request Aug 6, 2026
… one (#147)

`reviews.auto_review.base_branches` defaults to empty, and empty means
automatic review fires only on PRs targeting the default branch. The
design-sweep-loop skill opens one PR per round based on the previous round's
branch, so on the 2026-08-06 run every stacked PR but the first was out of
scope — #134 and #136 both came back "Review skipped: reviews are disabled for
this base branch."

The failure is quiet in the way that matters: `gh pr checks` prints
`CodeRabbit  pass` for a skipped review exactly as it does for one that ran and
found nothing, so an unattended run reports a clean second opinion it never
got. #134's finding only surfaced because the review was requested by hand.

Scoped to `design-sweep/.*` rather than `.*` so this does not also switch on
review for the ad-hoc `worktree-*` branches that occasionally serve as a base.
The account-wide rate limit that hit the same run is a separate matter and is
not addressed here.
@guarzo
guarzo merged commit a5294d9 into main Aug 6, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant