Skip to content

Commit 32ce0ca

Browse files
committed
fix(review): give every scope a description, derive the done-code unions, correct the sweep docs
Second CodeRabbit pass. Most of the batch was already fixed in fd506c1/09a5e90 and is skipped; what remained: /login rendered `<dt>` with no `<dd>` for a scope `describeScope` does not know. A `dt` alone is invalid in a `<dl>`, and AT groups a term with the next definition it finds — so an undescribed scope was read as meaning whatever the scope below it means, on the one page whose job is to say what is being granted. The default case now returns an honest "no description here, ask the deployment" line and the `<dd>` is unconditional. The e2e comment claiming the dd/dt count catches a fall-through is corrected: the per-row text assertions are what catch it now. `DONE_CODES` and its union are derived from one `as const` tuple in both `admin/accounts/view.ts` and `account/view.ts`, so the runtime guard and the exhaustive switch cannot drift. Docs: "365d ago" is 8 chars (four-digit days are the 9); the login srcset proposal needs the 132px mobile slot in `sizes`; setNameAction belongs in the useActionState list; the appraise confirmation cannot name a pool index or total that `AppraiseActionState` does not carry; five of the six notices need unconditional mounting, not four; `date_future` cannot hold a `<date>` placeholder in a static map, and `max` goes stale across UTC midnight; the th-anchor padding has to be 0.35rem to reach the 28px floor; COMPARISON's "three" was four, its chain is 13 steps, and its test counts now match the verified 1134/218. PREAMBLE's `.st` claim is marked corrected in place rather than deleted — SYNTHESIS and COMPARISON both cite what it said. typecheck, lint, format:check clean; npm test 77 files/1134 passed; npx playwright test 218 passed.
1 parent 8327194 commit 32ce0ca

11 files changed

Lines changed: 112 additions & 74 deletions

File tree

docs/design-sweep/COMPARISON.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,12 @@ Aug-4 ended at "§10. Recommended next commands." Nothing ran. The user's own
6868
account of that sweep was that they "ran the recommended commands" — manually,
6969
one at a time, after the fact.
7070

71-
Aug-5 ran a 14-step chain from a single gate. Four blocking items (1–4) are
72-
closed. 1127 unit tests and 204 e2e tests pass, up from 201 — the three new ones
73-
cover `/login`, which had no spec at all before this.
71+
Aug-5 ran the 13-step chain in SYNTHESIS.md's "Proposed command chain" from a
72+
single gate. Four blocking items (1–4) are closed. On the branch's final state —
73+
which includes the review fixes made after the chain finished, so these are not
74+
the numbers any single step ended on — 1134 unit tests and 218 Playwright test
75+
cases pass, up from 201 e2e cases. Four of the new ones cover `/login`, which
76+
had no spec at all before this.
7477

7578
Two things the chain taught that no amount of reviewing would have:
7679

@@ -96,7 +99,7 @@ spec after every step, not just at the end.
9699

97100
## Where Aug-5 was not better
98101

99-
Three of the 24 items did not survive contact with the code. The backlog should
102+
Four of the 24 items did not survive contact with the code. The backlog should
100103
not be read as 24-for-24.
101104

102105
- **Item 18, second half.** "The queued marker misattributes its own age" is

docs/design-sweep/PREAMBLE.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,14 @@ product's own nouns. Do not propose generic replacements.
100100
— capping the column moved the whole page 144px sideways on a nav click.
101101
- Body prose caps at 68ch.
102102

103-
**Known open defect, already logged.** `.st` (the Status token) declares no
104-
`font-weight` and renders at 400 where the label register calls for 600.
105-
DESIGN.md names this "a defect, not a variant". You may cite it; you do not need
106-
to discover it.
103+
**Corrected after dispatch — do not reuse.** This brief told all eighteen
104+
reviewers that `.st` (the Status token) declares no `font-weight` and renders at
105+
400, as a known open defect they could cite without discovering. That is wrong,
106+
and was wrong when they were dispatched: `.st` declares `font-weight: 600`, and
107+
`DESIGN.md` was the stale half. Five reviewers pushed back; thirteen took it as
108+
given. The paragraph is kept here rather than deleted because SYNTHESIS.md's
109+
"Correction to the brief" and COMPARISON.md both refer to what this document
110+
said — but anyone reusing this preamble for another sweep must drop it.
107111

108112
**Out of scope for this sweep.** Screenshots and starting a dev server. The
109113
source answers the questions, and a dev server rewrites `tsconfig.json` here.

docs/design-sweep/audit-admin-audit.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ figures below are computed from the OKLCH tokens, not eyeballed.
8080
- **Where:** `src/app/globals.css:3253-3301`, `src/app/_components/format-ago.ts:14-15`
8181
- **Cost:** Three years from now the oldest pages of a log that by design deletes nothing render "1095d ago" in a `white-space: nowrap` cell that is 72px of content box, and the overflow paints out of the pinned column onto the row beneath it — the exact failure the pin exists to prevent.
8282
- **Principle:** none.
83-
- **Fix:** The docblock computes the bound as "`365d ago` caps at 8ch"; the string is 9 characters, and `elapsedShort` has no cap on the day count at all. Either bound the formatter (`>= 999d``999d+`) or add `overflow: hidden; text-overflow: ellipsis` to `.log--audit td:first-child` so an over-long value truncates inside the pin instead of escaping it. The second is one declaration and makes the whole class of future overflow safe.
83+
- **Fix:** The docblock computes the bound as "`365d ago` caps at 8ch", which is right for three-digit days — `365d ago` is 8 characters — but `elapsedShort` has no cap on the day count at all, so a four-digit day (`1095d ago`) is 9 and nothing stops the next digit after that. Either bound the formatter (`>= 999d``999d+`) or add `overflow: hidden; text-overflow: ellipsis` to `.log--audit td:first-child` so an over-long value truncates inside the pin instead of escaping it. The second is one declaration and makes the whole class of future overflow safe.
8484

8585
## What is good and must survive
8686

docs/design-sweep/audit-login.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@
1616
- **Where:** `src/app/login/page.tsx:92-104`, `src/app/globals.css:2279-2308`
1717
- **Cost:** A member deciding whether to hand authGD their character reads `esi-characters.read_contacts.v1`, `esi-characters.write_contacts.v1`, `esi-ui.open_window.v1` and learns nothing they could act on, three lines below a paragraph that explains the same grant in plain English.
1818
- **Principle:** PRODUCT.md principle 2, "State before action" — the screen has to answer what is true before it offers something to press
19-
- **Fix:** Invert the `<dl>` so it carries the mapping instead of a heading. `dt` becomes the scope identifier, `dd` becomes the one sentence it buys ("read the contacts on your characters", "add, update and remove contacts under the `{label}` label", "open the in-game window when authGD sends you somewhere"), from a lookup keyed on the identifier with a fallback of no `dd` for a scope the map does not know. Move "Scopes requested" to a `<p>` above the list carrying `.launch__scopes-head` with the register's type, so the label register keeps its member. This preserves everything the docblock at `page.tsx:95-99` argues for (one row per scope, real boundaries, no space-joined blob) and it makes the `<dl>` semantically honest at the same time: today a screen reader announces each identifier as a *definition of* "Scopes requested" four times over, which is the wrong relationship. Keep `overflow-wrap: anywhere` on the identifier.
19+
- **Fix:** Invert the `<dl>` so it carries the mapping instead of a heading. `dt` becomes the scope identifier, `dd` becomes the one sentence it buys ("read the contacts on your characters", "add, update and remove contacts under the `{label}` label", "open the in-game window when authGD sends you somewhere"), from a lookup keyed on the identifier. An identifier the map does not know still gets a `dd` — a short, honest "no description here, ask the deployment" line rather than no `dd` at all: a `dt` with nothing under it is invalid in a `<dl>`, and AT groups a term with the next definition it finds, so an undescribed scope would be read as meaning whatever the scope below it means. On a consent screen that is the worst available failure. Move "Scopes requested" to a `<p>` above the list carrying `.launch__scopes-head` with the register's type, so the label register keeps its member. This preserves everything the docblock at `page.tsx:95-99` argues for (one row per scope, real boundaries, no space-joined blob) and it makes the `<dl>` semantically honest at the same time: today a screen reader announces each identifier as a *definition of* "Scopes requested" four times over, which is the wrong relationship. Keep `overflow-wrap: anywhere` on the identifier.
2020

2121
### 3. The LCP element is the one image on the page with no priority, while the 2.2 KB one is boosted
2222

2323
- **Severity:** moderate
2424
- **Where:** `src/app/login/page.tsx:48-54` and `122-128`, `src/app/globals.css:2223-2246`
2525
- **Cost:** Every visitor watches an empty 180px box above the corp name while an 82 KB seal downloads behind an explicitly-prioritised 2.2 KB button mark, and the 620ms `seal-settle` entrance has usually finished playing on that empty box before the artwork arrives, so the seal pops in with no settle at all.
2626
- **Principle:** PRODUCT.md principle 5, "earn the artwork" — an entrance the artwork misses is worse than no entrance
27-
- **Fix:** Three parts, all small. (a) `fetchPriority="high"` belongs on the seal: at 180x180 (132x132 under 40rem) it is the largest paint candidate on this page, larger than the display-size `h1` at every viewport, and `/brand/emblem.webp` is 81,694 bytes against the button's 2,248. Move it, do not duplicate it, or neither is prioritised. (b) Emit a real preload so the fetch starts from the head rather than after body parse: `preload(brand.sealUrl, { as: "image", fetchPriority: "high" })` from `react-dom` at the top of the component. `BRAND_SEAL_URL` can point off-origin in a fork, which makes this more valuable, not less. (c) The docblock at `page.tsx:44-47` names the missing 1x srcset entry as a real cost paid deliberately to keep the encode untouched. That trade is not forced: `srcset="/brand/emblem-256.webp 256w, /brand/emblem.webp 512w" sizes="180px"` adds the 1x entry without re-encoding the 512 source at all, only cutting one new derivative. A 1x phone currently pulls 82 KB to draw 132 CSS px, a 3.9x oversample.
27+
- **Fix:** Three parts, all small. (a) `fetchPriority="high"` belongs on the seal: at 180x180 (132x132 under 40rem) it is the largest paint candidate on this page, larger than the display-size `h1` at every viewport, and `/brand/emblem.webp` is 81,694 bytes against the button's 2,248. Move it, do not duplicate it, or neither is prioritised. (b) Emit a real preload so the fetch starts from the head rather than after body parse: `preload(brand.sealUrl, { as: "image", fetchPriority: "high" })` from `react-dom` at the top of the component. `BRAND_SEAL_URL` can point off-origin in a fork, which makes this more valuable, not less. (c) The docblock at `page.tsx:44-47` names the missing 1x srcset entry as a real cost paid deliberately to keep the encode untouched. That trade is not forced: `srcset="/brand/emblem-256.webp 256w, /brand/emblem.webp 512w" sizes="(max-width: 40rem) 132px, 180px"` adds the 1x entry without re-encoding the 512 source at all, only cutting one new derivative. `sizes` has to carry the mobile slot as well as the desktop one — the seal draws at 132 CSS px under 40rem, and a flat `sizes="180px"` would tell the browser the slot is always 180, which on a 2x phone selects the 512 file for a 132px box and gives back none of the saving the new derivative exists for. A 1x phone currently pulls 82 KB to draw 132 CSS px, a 3.9x oversample.
2828

2929
### 4. A long corp name is clipped by `overflow: hidden` with no way to scroll to the rest
3030

docs/design-sweep/audit-payout-detail.md

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@
2828
sitting in" (`appraise-form.tsx:9-20`). The `?error=` redirect is the right
2929
channel for a failure that arrives on page load, and the wrong one for a
3030
rejection of a control that is 2,000px down a page full of open panels. Convert
31-
the four field-level editors that reject on format — `setItemPriceAction`,
32-
`setParticipantSharesAction`, `addFlatPoolAction`'s `total_invalid`, and
33-
`addParticipantAction` — to return state through `useActionState` from a small
34-
client leaf, the way `AppraiseForm` and `NoteForm` already do, and render the
35-
message beside the field that produced it. `openInfoAction`'s five failure codes
31+
the field-level editors that reject on format — `setNameAction`,
32+
`setItemPriceAction`, `setParticipantSharesAction`, `addFlatPoolAction`'s
33+
`total_invalid`, and `addParticipantAction` — to return state through
34+
`useActionState` from a small client leaf, the way `AppraiseForm` and `NoteForm`
35+
already do, and render the message beside the field that produced it. `openInfoAction`'s five failure codes
3636
are the harder case (they can't be predicted client-side); at minimum they should
3737
not navigate, since the action persists nothing by its own docblock's argument.
3838
Keep the redirect only for failures that genuinely have no form still on screen.
@@ -132,13 +132,16 @@
132132
text." The primitive grew an empty-slot mode specifically so call sites could
133133
mount unconditionally, and this page — the one with six of them — uses `&&` at
134134
all six.
135-
- **Fix:** Mount all six unconditionally and pass the empty value:
135+
- **Fix:** Mount them unconditionally and pass the empty value:
136136
`<Notice tone="warn">{poolsWithUnresolvedItems.length > 0 ? <>…</> : ""}</Notice>`.
137-
The `errorMessage` one at line 251 is the exception worth arguing — it arrives by
138-
navigation, so the document is new either way — but the four derived warnings
139-
(unresolved items, the two roster clashes, and the `dropped` report after the
140-
`?dropped=` remount) all appear as a result of an action the operator just took
141-
on a page that did not reload, and those are the ones the region exists for.
137+
Five of the six need it. The `errorMessage` one at line 251 is the single
138+
exception worth arguing — it arrives by navigation, so the document is new
139+
either way — but the other five all appear as a result of an action the
140+
operator just took on a page that did not reload, and those are the ones the
141+
region exists for: the unresolved-items warning, the two roster clashes
142+
(duplicate unresolved names, and names that are both linked and unlinked), the
143+
`dropped` report after the `?dropped=` remount, and the info notice saying
144+
repricing is closed while the operation is finalized.
142145

143146
### 6. The pool-items table is the one table on the page that runs to hundreds of rows, and it is the one that gets no sticky header
144147

@@ -210,10 +213,16 @@
210213
specifically so "a client leaf can tell that apart from either of those"
211214
(actions.ts:160-163), and nothing consumes it.
212215
- **Fix:** Consume the `ok: true` the action already returns: render a
213-
`role="status"` confirmation naming what was added ("pool 3 added, 4.82b ISK"),
214-
and clear the textarea on success — the paste is persisted as `rawPaste` on the
215-
pool by then, so nothing is lost by dropping it from the form, and an empty box
216-
is the strongest available signal that the submit landed.
216+
`role="status"` confirmation saying the paste was appraised and added as a new
217+
pool, and clear the textarea on success — the paste is persisted as `rawPaste`
218+
on the pool by then, so nothing is lost by dropping it from the form, and an
219+
empty box is the strongest available signal that the submit landed. Naming the
220+
numbers ("pool 3 added, 4.82b ISK") would read better still, but
221+
`AppraiseActionState` is `{ ok: true; dropped }` and carries neither the pool
222+
index nor the total; either widen it to carry both, or keep the message to
223+
what the existing result can actually back. Do not write the numbers from
224+
anything the client already had — the whole point is to report what the server
225+
did.
217226

218227
### 10. The notices name the affected items in the faintest, smallest type on the page
219228

docs/design-sweep/audit-payouts-list.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,22 @@ and `DESIGN.md` / `PRODUCT.md`.
2424
`bypassClientGuard` strips `max` by name. Every other client guard on this flow has a
2525
server twin (`name``name_required`, date parse → `date_invalid`, shares → four
2626
checks, price → `price_invalid`). `max` is the one that does not.
27-
- **Fix:** Add `date_future` to `NEW_OPERATION_ERRORS` ("An operation cannot be dated in
28-
the future. EVE time is UTC, so today is <date>.") and the matching check in
27+
- **Fix:** Add a `date_future` code to `NEW_OPERATION_ERRORS` and the matching check in
2928
`createOperationAction` after the NaN test; add the same code and check to
3029
`setOccurredAtAction` / `OPERATION_ERRORS`, since the detail-page editor has the same
31-
gap. Keep `max` as the fast path.
30+
gap. The map entries are static strings that `lookupErrorMessage` reads verbatim, so
31+
the date cannot live in them — an entry reading "…today is `<date>`" ships that
32+
placeholder to the operator literally. Keep the map to the static half ("An operation
33+
cannot be dated in the future. EVE time is UTC.") and let the surface that renders the
34+
rejection append the day, from the same server-computed UTC date it already uses for
35+
`max` — the client's own clock is the wrong source here, since the whole point of the
36+
message is which day the *server* thinks it is.
37+
- **And the `max` guard itself goes stale.** `today` is computed once, when the page
38+
renders, so a form left open across UTC midnight carries yesterday's `max` and the
39+
browser now blocks a submit dated today — the valid case. Either recompute it at
40+
validation time, or drop the native `max` and let the server check be the only guard.
41+
Either way the server-side `date_future` rejection is what has to hold, rendered as a
42+
persistent field error rather than a bubble that vanishes on the next keystroke.
3243

3344
Two sub-cases the same fix covers, both currently unaddressed:
3445

docs/design-sweep/audit-shell.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -165,14 +165,17 @@ findings rather than padded into them. Six findings, worst-first.
165165
```css
166166
.log th a {
167167
display: inline-block;
168-
padding-block: 0.22rem;
169-
margin-block: -0.22rem;
168+
padding-block: 0.35rem;
169+
margin-block: -0.35rem;
170170
}
171171
```
172172

173-
That is 24.09px of target at zero net layout cost. Making the anchor fill the
174-
cell would be better still but is unsafe under `.log--audit`'s
175-
`table-layout: fixed` with `nowrap` cells.
173+
The anchor's own line box is ~17px, so 0.35rem (5.6px) of padding each side
174+
lands at ~28.2px — the floor, rather than the ~24px a smaller value gives.
175+
The equal negative margin is what keeps the net layout change at zero, so the
176+
header height and the `scroll-margin-top: 3rem` figure both stay valid.
177+
Making the anchor fill the cell would be better still but is unsafe under
178+
`.log--audit`'s `table-layout: fixed` with `nowrap` cells.
176179

177180
### 6. The reduced-motion comment states a mechanism the stylesheet does not implement
178181

e2e/login.spec.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ test("each requested scope shows a plain-English description, not just the ident
2121
await expect(rows.nth(0)).toHaveText("esi-characters.read_contacts.v1");
2222
await expect(rows.nth(1)).toHaveText("esi-characters.write_contacts.v1");
2323

24-
// Every configured scope resolves to a description in this deployment: the
25-
// regression this guards is a scope silently falling through describeScope's
26-
// default case (a raw identifier with nothing under it) rather than a
27-
// deliberate omission.
24+
// Every configured scope resolves to a real description in this deployment.
25+
// The count no longer proves that on its own — `describeScope`'s default now
26+
// returns a fallback sentence, so a scope falling through still renders a
27+
// `<dd>` (a `<dt>` without one is invalid in a `<dl>`, and AT would read the
28+
// undescribed scope as meaning whatever the next definition says). The two
29+
// text assertions below are what catches a fall-through now.
2830
const descriptions = page.locator(".launch__scopes dd");
2931
await expect(descriptions).toHaveCount(2);
3032
await expect(descriptions.nth(0)).toContainText("Reads the contacts");

src/app/account/view.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,15 @@
88

99
/** The four outcomes `/account`'s server actions redirect back with. A code
1010
* outside this set (hand-typed, or from a build that has since dropped one)
11-
* renders no confirmation at all — see `accountConfirmation`'s default. */
12-
export type AccountDoneCode = "main" | "unlink" | "wake" | "discord";
11+
* renders no confirmation at all — see `accountConfirmation`'s default.
12+
*
13+
* Tuple first, type derived from it: the guard below and the exhaustive
14+
* switch have to agree about the same four strings, and a separately-written
15+
* union is one edit away from disagreeing. Same shape in
16+
* `admin/accounts/view.ts`. */
17+
const DONE_CODES = ["main", "unlink", "wake", "discord"] as const;
1318

14-
const DONE_CODES: readonly AccountDoneCode[] = ["main", "unlink", "wake", "discord"];
19+
export type AccountDoneCode = (typeof DONE_CODES)[number];
1520

1621
function isDoneCode(value: string | undefined): value is AccountDoneCode {
1722
return value !== undefined && (DONE_CODES as readonly string[]).includes(value);

0 commit comments

Comments
 (0)