Skip to content

feat(web): redesign the setup lists as a table with a bottom inspector - #939

Merged
mforce merged 20 commits into
mainfrom
feat/908-crud-redesign
Sep 23, 2026
Merged

mforce merged 20 commits into
mainfrom
feat/908-crud-redesign

Conversation

@mforce

@mforce mforce commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner

Closes #908

822 component-plan row

This PR doesn't add a new #822 pair — it's a later-phase UX redesign layered on top of pair 9 (table.data → TableContainer/Table size="small"/TableHead/TableRow/TableCell), which #897 already landed on these five screens. "Reused, never rebuilt": the new shared pieces extend web/src/components/FieldConsole.tsx (the #899 Field Console layout family) rather than a new file — LedgerTableContainer gained a scrollHint variant for the combined "Swipe columns ↔ · Scroll rows ↕" cue, and three new exports (RecordInspector, ListInspectorPane, selectableRowProps, STICKY_TABLE_HEAD_SX) implement the bottom inspector and row-selection wiring shared by all five screens.

What changed, per screen

All five screens (Customers, Grades, Flocks, Users, Products) keep their existing table columns, dialogs, business logic and RBAC gates untouched. Each row is now click/keyboard-selectable (aria-selected, Enter/Space, a visible accent bar); the table is wrapped in a bounded, independently-scrolling region (ListInspectorPane) with a docked RecordInspector panel showing the selected row's facts and the SAME action handlers the row's own Actions cell already calls (extracted into one renderActions/renderProductActions-style function per screen, not duplicated). Per-row actions stay in the table too — the approval comment names the inspector as an addition, not a replacement, and removing a working affordance without an explicit ask was not this slice's call to make.

  • Customers: table + inspector (email/address/note/outstanding). Simplest screen, no dialogs beyond create/edit.
  • Grades: + audit-history link and activate/deactivate in the inspector's actions.
  • Flocks: + status/age/birds facts; the bird-movement-ledger drill-down (pair 15's ruled region) is untouched — it is a different, existing UI concept, not part of this redesign's scope.
  • Users: the biggest screen (7 dialogs); inspector reuses all five row actions (edit/reset password/change role/change email/flocks/disable-enable), self-target gating (myId !== u.id) preserved.
  • Products: converted from two stacked sections into two explicit MUI Tabs ("Products" first, "Packed units" second), each with its own table+inspector and its own, independent selection state (switching tabs does not clear or carry over the other tab's selection — confirmed by a dedicated test). Individual's fixed-1 packed-unit behavior is unchanged.

Phone: all five tables carry the combined Swipe columns ↔ · Scroll rows ↕ cue (a new LedgerTableContainer variant + common:swipeColumnsScrollRows i18n key, since the table now also scrolls vertically in its own bounded region, not just horizontally). Verified at 390×844 that the inspector's own in-flow layout never overlaps the fixed bottom nav (screenshots below).

Docs sync

Per #688 (help prose must use each locale's own control label): added a Selected-record inspector entry to specs/product/GLOSSARY.md's "Getting around" section, a matching helpGlossary.ts row, and glossarySelectedRecordInspectorTerm/Def + swipeColumnsScrollRows/inspectorEmptyPrompt/inspectorLabel/entitySingular catalog keys in en/es/tl (es/tl machine-drafted, matching the existing "PENDING NATIVE-SPEAKER REVIEW" convention for those files).

Tests (measured before/after, not remembered)

File Before After Rewrites
CustomersPage.test.tsx 53 59 0
GradesPage.test.tsx 28 31 0
FlocksPage.test.tsx 51 54 0
UsersPage.test.tsx 158 162 0
ProductsPage.test.tsx 43 49 9 (existing packed-unit-conversion tests needed a switchToPackedUnits() tab click first — the row they targeted moved to a second, initially-hidden tab)

Every new test asserts a literal expected value (row aria-selected, inspector heading/field text, dialog reopened, tab-independent selection) — not just "doesn't crash".

Full suite: npm run typecheck clean; npm test -- --run 3387/3387 (zero pre-existing failures on a clean run — one DailyEntryPage.test.tsx test flakes under full-suite parallel load, confirmed pre-existing and unrelated: passes standalone, and this PR never touches that file); npm run test:coverage 92.16/88.96/88.24/94.83 (statements/branches/functions/lines) against the 89/80/85/92 floors; npm run build and npm run verify:sw clean (precache 1843.52 KiB, 56.48 KiB under the 1900 KiB ceiling).

Guards

  • styles.harness-selectors.test.ts, styles.declared-tokens.test.ts, farmTheme.policy.test.ts, styles.elevation.test.ts: green throughout, by construction — every new style is sx on existing MUI components using already-declared var(--…) tokens (--tint-accent, --brand, --rule, --surface), so styles.css itself was never touched.
  • styles.conversion.test.ts (web: the postcss style guards go blind as screens convert to MUI #824 MUI source policy): the inspector's eyebrow label added a second textTransform: "uppercase" site in FieldConsole.tsx (the first is the existing table-head style). Ran RED first (confirmed the guard's pinned census failed with the real diff before I touched the test), then registered the new site as a second, deliberate entry — not an allow-list, the guard's own documented mechanism for tracking new reviewed uppercase sites.
  • catalogParity.test.ts + helpGlossary.test.ts: green after the glossary/help additions (206 tests, includes the cross-check between specs/product/GLOSSARY.md's bold terms and the in-app catalog).
  • dotnet test tests/Cluckwork.Application.Tests --filter "FullyQualifiedName~ImagePin|FullyQualifiedName~RealTree": 14/14, run before both markdown-touching commits (the GLOSSARY.md entry and the copied design artifact).
  • No CSS class or shared component was deleted this slice (only added/extended), so the "grep the whole repo before deleting" step doesn't apply — nothing to report there.

Verification

  • Isolated stacks, never cluckwork-sim. cw908b (before, built from origin/main at 190dcd6, the commit this branch forked from) and cw908a (after, this PR's head, 1b13ad7), both via a standalone copy of docker-compose.sim.yml (project name + published port 8108 overridden, torn down and images removed after use). Both farms provisioned on each (default-farm simulation fixture, readme-farm demo).
  • Full quick Playwright suite run against cw908a (this head): 92 passed, 1 intentionally skipped (the slow session-refresh spec, same as every other PR here), 0 failures — including phone.spec.ts's "no walked screen overflows the viewport horizontally" (the historical class of regression the web: convert the CRUD list screens to MUI — Customers, Products, Grades, Flocks, Users #832 slice hit) and the pagination specs that exercise Customers' and Flocks' paged lists directly.
  • Screenshots, readme-farm, 1:1, same scenario both sides: all five screens (Products both tabs) at 1280×800/390×844, light/dark — before frames from cw908b, after from cw908a. Plus after-only (net-new UI, no before to diff against): a selected row with its inspector open at both widths for all five screens, an open create dialog at both widths for all five screens, the Products "Packed units" tab at both widths, and the Flocks phone table scrolled to its end showing clearance above the bottom nav. Compared every after frame against docs/designs/674-crud-redesign/crud-direction-lab.html's Concept B myself before attaching: table-above/inspector-below layout, dark inspector header, two-column fact list, phone swipe+scroll cue, and independent Products-tab table+inspector pairs all match the approved direction.

Left out / open items

  • Per-row actions were kept alongside the inspector's own action buttons rather than consolidated into the inspector only — see the "What changed" section above for the reasoning; flagging it explicitly as a judgment call, not a hidden decision.
  • No product defect found outside this slice's own scope. The one flaky test noted above (DailyEntryPage.test.tsx) is pre-existing and untouched by this branch.

Also fixes: flush-left first column on every MUI table (theme-level)

Owner-reported: the first column's text sat almost flush against the bordered
pane's left border on all five of this PR's screens. Root cause was the shared
MuiTableCell padding in theme/FarmThemeProvider.tsx (0.6rem 1rem 0.6rem 0,
zero left inset), written for a borderless ledger before every table sat inside
a bordered panel. Fixed at the theme root with "&:where(:first-of-type)": { paddingLeft: "1rem" } — :where() holds it at the SAME specificity as the
base rule, so any route with its own higher-specificity padding override keeps
that override unchanged.

Walked every route rendering a MUI <Table> (Customers, Dashboard,
Expenses, Feed, Flocks, Grades, History, Inventory, Products,
Reports, Sales, Stock, Users, Water; Audit/Export render no MUI
table) at 1280 and 390, light. Visibly affected: only this PR's five screens
(Customers, Grades, Flocks, Products, Users) — the routes with no local
MuiTableCell override. FieldConsole-wrapped ledgers (Expenses, Feed,
History, Inventory, Reports, Sales, Stock, Water) and Dashboard's own inline
table already carry their own equal-or-higher-specificity padding override
(8px and 6px scales respectively) and render byte-identical before and after
this fix — confirmed via computed-style measurement, not inference, so no
before/after pair is posted for those routes.

No new overflow at 1280 or 390 on any of the 14 routes (document.scrollWidth
== clientWidth everywhere). Flocks' own bounded table region already
scrolled horizontally before this fix (its widest row exceeded its container);
the fix adds 16px to that pre-existing, by-design scroll distance (the same
LedgerTableContainer swipe-hint pattern already used by History), not a new
break. Separately found and not fixed (pre-existing, out of scope): Users'
Actions column (5 buttons) already overflowed its table's bounded region by
about 48px at 1280 before this fix; confirmed via DOM-simulated removal of the
fix that the overflow predates it.

Covered by a red-first farmTheme.policy.test.ts assertion (the theme
contract) and a real-browser Playwright geometry check
(table-cell-first-column-inset.spec.ts) on both Flocks (this fix's own
16px scale) and History (an existing #899 ledger's untouched 8px scale),
each asserting the first column's left gap is close to the last column's right
gap.

Acceptance follow-up: row identity/status/actions distinct without colour alone, and dialog before/after

Two #908 acceptance criteria were unmet at e6246b6 and are fixed here:

  • Users' Status was blank for active users and its Actions column carried no header. Both now show explicitly: a green Active badge (matching Grades'/Products' own status chip, table and inspector) alongside the existing Disabled badge, and an Actions header matching Products' precedent. activeBadge added to en/es/tl, reusing enums:status.Active's existing es/tl wording rather than drafting new copy.
  • Destructive row/inspector actions read identically to the primary edit link, colour being their only distinguishing signal. FieldConsole.tsx gained CONSOLE_DESTRUCTIVE_LINK_SX (one shared token, not per-screen sx); every deactivate/archive/deplete/disable call site across Grades, Flocks, Products and Users now pairs it with a leading TriangleAlert/Ban icon. Customers has no destructive row action, so it is unaffected. Corrective actions (activate/reactivate/enable) are untouched. Accessible names are unchanged (icons are aria-hidden).

Verified red-first per screen: reverted each screen's own change via git stash, confirmed the new test failed on color: var(--link) instead of var(--danger), then restored. Full web suite green (3398/3398), typecheck clean, full quick Playwright green on a rebuilt cw908a.

Screenshots: recaptured every after-frame whose appearance changed (Grades, Flocks, Products, Users — Customers excluded, unaffected) across the five main comments, the row-selected comment, and the create-dialog comment. Separately, the create-dialog comment previously had after-only frames; it now also carries before frames from an isolated stack built at origin/main's 190dcd6 (the same base commit the original before-frames used), for all five screens including Customers.

Screenshots below, laid out as before/after pairs per screen, followed by the after-only inspector/dialog/tabs/phone-scroll evidence.

…stomers

#908 Concept B: the entity table keeps its scroll and comparison columns;
selecting a row updates a separate inspector docked at the bottom of the
list region instead of overlaying the table. New shared pieces in
FieldConsole.tsx (RecordInspector, ListInspectorPane, selectableRowProps,
STICKY_TABLE_HEAD_SX) plus a combined swipe+scroll phone cue on
LedgerTableContainer. CustomersPage is the first of five screens to adopt it.
… Help (#908)

Adds a specs/product/GLOSSARY.md entry, an in-app helpGlossary.ts row under
Getting around, and en/es/tl catalog terms/definitions, per #688's rule that
help prose names each control by its own locale's label.
The owner-approved table+bottom-inspector direction lab (crud-direction-lab.html),
copied from the shared checkout's untracked docs/designs/ so this branch carries
the reference the implementation follows.
…t prop

Self-review pass (pstack:deslop) over the #908 diff before opening the PR.
No caller ever passed it; ListInspectorPane's own bounded height already
does the job. No other slop found (no any/eslint-disable/defensive
try-catch/narrative comments in the branch's diff).
@mforce

mforce commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner Author

Customers — before / after (1280×800 and 390×844, light/dark)

After frames captured at 6576010.

Before is origin/main at 190dcd6; after is this PR's head.

Before, 1280 light

After, 1280 light

Before, 1280 dark

After, 1280 dark

Before, 390 light

After, 390 light

Before, 390 dark

After, 390 dark

@mforce

mforce commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner Author

Grades — before / after (1280×800 and 390×844, light/dark)

After frames captured at 6576010.

Before, 1280 light

After, 1280 light

Before, 1280 dark

After, 1280 dark

Before, 390 light

After, 390 light

Before, 390 dark

After, 390 dark

@mforce

mforce commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner Author

Flocks — before / after (1280×800 and 390×844, light/dark)

After frames captured at 6576010.

Before, 1280 light

After, 1280 light

Before, 1280 dark

After, 1280 dark

Before, 390 light

After, 390 light

Before, 390 dark

After, 390 dark

@mforce

mforce commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner Author

Users — before / after (1280×800 and 390×844, light/dark)

After frames captured at 6576010.

Before, 1280 light

After, 1280 light

Before, 1280 dark

After, 1280 dark

Before, 390 light

After, 390 light

Before, 390 dark

After, 390 dark

@mforce

mforce commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner Author

Products — before / after (1280×800 and 390×844, light/dark)

After frames captured at 6576010.

Products tab (the default view; the before stack has no Packed units tab to diff against).

Before, 1280 light

After, 1280 light

Before, 1280 dark

After, 1280 dark

Before, 390 light

After, 390 light

Before, 390 dark

After, 390 dark

@mforce

mforce commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner Author

After-only: a row selected with the inspector showing (net-new UI, no before to diff)

After frames captured at 6576010.

One screen per pair, 1280 then 390, light.

Customers, row selected, 1280 light

Customers, row selected, 390 light

Grades, row selected, 1280 light

Grades, row selected, 390 light

Flocks, row selected, 1280 light

Flocks, row selected, 390 light

Products, row selected, 1280 light

Products, row selected, 390 light

Users, row selected, 1280 light

Users, row selected, 390 light

@mforce

mforce commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner Author

Create dialogs — before / after (1280×800 and 390×844, light)

After frames captured at 6576010.

Before is origin/main at 190dcd6; after is this PR's head. Previously after-only (no before existed yet); before frames added this round.

Customers, New customer dialog, Before 1280 light

Customers, New customer dialog, After 1280 light

Customers, New customer dialog, Before 390 light

Customers, New customer dialog, After 390 light

Grades, New grade dialog, Before 1280 light

Grades, New grade dialog, After 1280 light

Grades, New grade dialog, Before 390 light

Grades, New grade dialog, After 390 light

Flocks, New flock dialog, Before 1280 light

Flocks, New flock dialog, After 1280 light

Flocks, New flock dialog, Before 390 light

Flocks, New flock dialog, After 390 light

Products, New product dialog, Before 1280 light

Products, New product dialog, After 1280 light

Products, New product dialog, Before 390 light

Products, New product dialog, After 390 light

Users, New user dialog, Before 1280 light

Users, New user dialog, After 1280 light

Users, New user dialog, Before 390 light

Users, New user dialog, After 390 light

@mforce

mforce commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner Author

After-only: Products' Packed units tab, and the Flocks phone table at max scroll

After frames captured at 6576010.

The last frame proves the inspector's own in-flow layout never overlaps the fixed bottom nav.

Products, Packed units tab, 1280 light

Products, Packed units tab, 1280 dark

Products, Packed units tab, 390 light

Products, Packed units tab, 390 dark

Flocks, phone table at max scroll, clear of the bottom nav

… mode

MUI's Tabs default (textColor="primary") paints the selected label and
indicator with palette.primary.main (raw --brand), which styles.css keeps
mode-independent on purpose — so it clears roughly 1:1 against a dark
surface. Products' tabs were the first Tabs in the app, surfacing a defect
MuiBottomNavigationAction already had to fix the same way (#829).

Theme-level fix: MuiTabs/MuiTab now use --stat-accent (the same
already-computed, per-brand "active" accent the bottom nav and sidebar's
active-item rule use), which clears 4.5:1 against both --surface and
--canvas in light and dark mode for all four brand palettes.

farmTheme.policy.test.ts pins the computed contrast (not just "uses the
token") across every brand x mode combination; confirmed red against the
pre-fix theme (MuiTab/MuiTabs overrides absent) before restoring the fix.
@mforce

mforce commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner Author

Fix: selected Tabs label/indicator invisible in dark mode (owner-reported)

New head: a592569.

Cause: MUI's Tabs default (textColor="primary") paints the selected label and indicator with palette.primary.main (raw --brand), and --brand is deliberately mode-independent in styles.css — so it clears roughly 1:1 against a dark surface. Products' tabs were the first Tabs in the app, so nothing had exercised this default before. MuiBottomNavigationAction hit and fixed the identical MUI default years earlier (#829) — same root cause, different component.

Fix, at the theme level: MuiTabs/MuiTab now use --stat-accent, the same already-computed, per-brand "active" accent MuiBottomNavigationAction and the sidebar's active-item rule already use. It clears 4.5:1 against both --surface and --canvas, in light and dark mode, for all four brand palettes — confirmed by computing the actual contrast ratio in a new farmTheme.policy.test.ts assertion, not just checking the token is wired. Ran that test red first (against the pre-fix theme, MuiTab/MuiTabs overrides absent) before restoring the fix, confirmed green after. No new derivation invented; light mode is unaffected (--stat-accent already equals raw --brand there, which was already correct).

Current frames: see the Products before/after comment above (captured at 6576010): #939 (comment)

@mforce

mforce commented Sep 22, 2026

Copy link
Copy Markdown
Owner Author

Scan of every other dark after-frame for brand-coloured text on a dark surface

Re-viewed all 10 previously-posted dark frames (Customers, Grades, Flocks, Users, Products — 1280 and 390 each) against this defect class specifically (brand-hued text sitting directly on --surface/--canvas, as opposed to brand-filled buttons/sidebar which pair the brand fill with --on-brand/--on-brand-mute text and were never at risk).

Found only on Products — the one screen with a Tabs component in this PR. No other instance in Customers, Grades, Flocks, or Users: their links (--ink + underline), StatusBadge chips (tint tokens), "New X" buttons (brand fill + --on-brand text) and sidebar active-item highlight all use pairings that were already contrast-correct in both modes.

The theme-level fix above covers every current and future Tabs in the app, not just this one screen, since it changes what MuiTabs/MuiTab resolve to for every consumer.

…ctually sticks

Codex review of 1b13ad7, finding 1 (P2). LedgerTableContainer's TableContainer
had overflow-x: auto (which computes overflow-y: auto too), making it
position: sticky's containing block — but ListInspectorPane's outer Box was
the one that actually scrolled. The header stuck to a scroller that never
moved, and slid away with the content the moment the real scroller (the
outer Box) was scrolled.

Nests LedgerTableContainer and its TableContainer into the same flex-column
chain ListInspectorPane already uses, so TableContainer is now the ONE
element that both scrolls and anchors the sticky header. Outside a flex
parent (this component's seven other, unbounded ledger callers) the added
flex/minHeight properties are inert.

New permanent Playwright spec (tools/simulation/ui/specs/) scrolls Flocks'
real bounded table region in a live browser and asserts the header's
bounding-box top does not move — a screenshot at rest cannot catch this,
only scrolling and re-measuring can. Confirmed genuinely red against the
pre-fix code first (header moved from y=181 to y=-19 on a 200px scroll),
then green after the fix.
Codex review of 1b13ad7, finding 2 (P2). FlocksPage's selectedId was never
cleared when the selected flock left `visible` (the show-archived toggle,
or an archive/reactivate write): selectedFlock derives from visible and so
the inspector correctly went blank, but the stale id survived in state and
could resurface as "selected" — with no new choice from the user — the
moment that row became visible again.

Fixed with useClampSelection (FieldConsole.tsx, added alongside the sticky-
header fix), wired into Flocks and, defensively and uniformly per the
review's explicit request, into the other four selection points (Customers,
Grades, Users, Products' two tabs) — none of which can currently drop a row
out of its own list (no client-side filter, no hard delete anywhere in this
app), confirmed by reading each screen's fetch/filter logic, but the same
shape is now wired the same way everywhere rather than left correct by
accident.

Two new FlocksPage tests, confirmed genuinely red first (toggling the
archived filter back on, or reshowing after an archive write, both left the
row marked aria-selected="true" with no new click) then green after the fix.
… def

Codex review of 1b13ad7, finding 3 (P3). The new tl.ts glossary prose named
the five destinations in English (Customers, Products, Grades, Flocks,
Users) instead of the labels the Tagalog sidebar and More menu actually
show (Mga Customer, Mga Produkto, Mga Grado, Mga Kawan, Mga User),
violating #688 — a user reading Help in Tagalog would be told to find
screen names that don't match the nav.

Re-checked the es prose the same way (read nav.customers/products/grades/
flocks/users in es.ts and compared): it already names the exact es nav
labels (Clientes, Productos, Grados, Lotes, Usuarios) — no change needed
there. en's prose already matches en's own nav labels too.
…-proof identity

Codex review of 1b13ad7, finding 4 (P3). The MUI source policy guard
identified a site only as file:literal, so FieldConsole.tsx's two approved
uppercase sites (the table-head style and the inspector eyebrow, two
different top-level components) both read as the identical
components/FieldConsole.tsx:"uppercase" — the duplicated expected string
checked only the COUNT, not which locations carried it. Removing uppercase
from one and adding it to an unrelated style in the same file would leave
the guard green.

Tags each site with its nearest top-level export/component name
(walkTopLevel + containerName), so the two FieldConsole.tsx sites are now
distinguishable (#FieldConsole vs #RecordInspector) while sites that
genuinely share one enclosing declaration (createFarmTheme's two shadow
sites) are still allowed to collide, honestly.

Mutation-checked the exact substitution the finding describes (removed
uppercase from RecordInspector's eyebrow, added an unrelated uppercase
inside ConsoleSubhead): confirmed the guard now fails on it before
reverting the mutation and confirming green again.
…t any dialog

Codex review of 1b13ad7, finding 5 (P3). "opens the same edit dialog from
the inspector's own edit action" asserted only that some dialog existed, so
a miswiring of the inspector's Edit button to a different dialog (e.g.
openPassword) would still pass a bare findByRole("dialog").

Now identifies the dialog by its own accessible name ("Edit user —
worker@farm.test") and checks the seeded Name value ("Wendy"), matching the
stronger customer and product inspector tests. Mutation-checked by wiring
the inspector's Edit button to openPassword: confirmed the strengthened
assertion goes red (the edit dialog by that name never opens) before
reverting the mutation and confirming green.
@mforce

mforce commented Sep 22, 2026

Copy link
Copy Markdown
Owner Author

Codex (gpt-5.6-sol) review round 1 of 1b13ad7 — all 5 findings fixed

New head: 5f4f753306209ef90453a4649aa4d4900a210320.

  1. P2, FieldConsole.tsx:142,220 — sticky header scrolled away. Fix commit 2dae546. LedgerTableContainer's TableContainer had implicit overflow-y: auto (from MUI's own overflow-x: auto), making it position: sticky's containing block, while ListInspectorPane's outer Box was the element that actually scrolled. Nested both into one flex-column chain so TableContainer is now the single element that both scrolls and anchors the sticky header. Verification: a new permanent Playwright spec (tools/simulation/ui/specs/setup-lists-sticky-header.spec.ts) scrolls Flocks' real table region in a live browser and asserts the header's bounding-box top does not move. Ran genuinely red first against the pre-fix code on an isolated stack (header moved from y=181 to y=−19 on a 200px scroll — the exact shape the finding described), then green after the fix. It's now part of the permanent quick suite (93 passed, 1 intentional skip, 0 failures on the full run).

  2. P2, FlocksPage.tsx:273,425 — stale selection could resurface. Fix commit 5a916e8. selectedId was never cleared when the selected flock left visible; the inspector correctly went blank (it derives from visible), but the stale id survived and could reselect itself the moment the row became visible again (toggling "show archived" back on, or after an archive/reactivate refresh). Added a shared useClampSelection hook (in the same FieldConsole.tsx commit as finding 1, 2dae546, since both touch that file) and wired it into Flocks plus, per your explicit request, the other four selection points (Customers, Grades, Users, Products' two tabs). Checked each of those four/five by reading their actual fetch/filter logic: none can currently drop a row out of its own list (no client-side filter, no hard delete anywhere in this app), so they're defensively uniform rather than fixing an observed bug there. Two new FlocksPage tests confirmed genuinely red first (disabled the useClampSelection call, watched both fail on aria-selected staying "true" after the row was hidden-then-reshown or archived-then-reshown with no new click), then green after restoring the fix.

  3. P3, tl.ts:3024 — English destination names in Tagalog prose. Fix commit 9fe40f5. Replaced with the actual Tagalog nav labels (Mga Customer, Mga Produkto, Mga Grado, Mga Kawan, Mga User). Re-checked es.ts the same way (read nav.customers/products/grades/flocks/users and compared against the glossary prose): it already names the exact es labels (Clientes, Productos, Grados, Lotes, Usuarios) — no change needed. en.ts was already correct too.

  4. P3, styles.conversion.test.ts:234-248 — identical identities let one approval substitute for another. Fix commit 85495f3. Added a walkTopLevel/containerName pass that tags each site's identity with its nearest top-level export/component name, so FieldConsole.tsx's two sites are now #FieldConsole vs #RecordInspector instead of two identical strings; sites that genuinely share one enclosing declaration (createFarmTheme's two shadow sites) are still allowed to collide. Mutation-checked the EXACT substitution you described — removed uppercase from the inspector eyebrow, added an unrelated uppercase inside ConsoleSubhead — confirmed the guard now fails on it, then reverted and confirmed green.

  5. P3, UsersPage.test.tsx:246-251 — weak dialog assertion. Fix commit 5f4f753. Now asserts the dialog's own accessible name (Edit user — worker@farm.test) and the seeded Name value (Wendy), matching the stronger customer/product inspector tests. Mutation-checked by wiring the inspector's Edit button to openPassword: confirmed the strengthened assertion goes red (the named edit dialog never opens), then reverted and confirmed green.

Verification after all five fixes

  • npm run typecheck: clean.
  • npm test -- --run: 3390/3390 (two new FlocksPage tests over the prior round's 3388).
  • Full quick Playwright suite (both projects) on an isolated cw908a stack rebuilt at this head: 93 passed, 1 intentional skip, 0 failures, including the new sticky-header spec.
  • Checked every previously-posted static frame for a visual change: none of the five fixes alter layout at rest (the sticky-header fix only changes behavior during a scroll interaction; recaptured Flocks' base and row-selected frames on the fixed stack and they're pixel-equivalent to the originals already on this PR — nothing new to attach).
  • Deslop pass over the diff: no any/eslint-disable/defensive try/catch/narrative comments found.

Isolated stack torn down (down -v, image removed) after verification; shared cluckwork-sim was never touched.

…w site's identity

Codex review round 2 of 5f4f753, finding 1 (P3, a partial fix of round 1's
finding 4). The container alone still let a same-component substitution
through: moving textTransform: "uppercase" from FieldConsole's
"& .MuiTableCell-head" rule to its "&& h2" rule kept the same container
and the same collected literal, so the guard stayed green with the actual
approved site gone.

walkTopLevel now threads the chain of enclosing object-property keys
between the container and the property itself, so the two FieldConsole
sites are components/FieldConsole.tsx#FieldConsole > & .MuiTableCell-head
> textTransform vs components/FieldConsole.tsx#RecordInspector >
textTransform — genuinely different identities. As a side effect this also
separates FarmThemeProvider's two boxShadow sites, which used to be an
honest collision (MuiAutocomplete/paper vs MuiTooltip/tooltip, now both
distinguishable too).

Mutation-checked the exact scenario from the finding (moved the real
uppercase from .MuiTableCell-head to && h2): confirmed the guard fails on
the substitution (identity read "&& h2" instead of "& .MuiTableCell-head"),
then reverted and confirmed green.
@mforce

mforce commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner Author

Codex (gpt-5.6-sol) review round 2 of 5f4f753 — the one finding fixed; loop stopped here

New head: dfe49d30d52226fb11067044f229f0497994733b.

No P1/P2. Round-1 findings 1, 2, 3, 5 confirmed fixed as-is. Round 2's one finding was a partial fix of round 1's finding 4, not a new product defect:

P3, styles.conversion.test.ts:267-269 — the uppercase-site identity had the enclosing declaration but not the property path. Fix commit dfe49d3. Round 1's fix distinguished sites by their nearest top-level container (#FieldConsole vs #RecordInspector), but a substitution WITHIN one component still passed: moving textTransform: "uppercase" from FieldConsole's "& .MuiTableCell-head" rule to its "&& h2" rule kept the same container and the same collected literal, so the guard stayed green with the actual approved site gone.

walkTopLevel now threads the chain of enclosing object-property keys between the container and the property itself, giving identities like components/FieldConsole.tsx#FieldConsole > & .MuiTableCell-head > textTransform:"uppercase" — exactly the shape you suggested. As a side effect, this also separates FarmThemeProvider's two boxShadow sites (previously an honest collision under one container; now MuiAutocomplete > styleOverrides > paper vs MuiTooltip > styleOverrides > tooltip, both distinguishable too).

Mutation-checked the exact scenario from the finding — moved the real uppercase from .MuiTableCell-head to && h2 — confirmed the guard failed on it (identity read && h2 instead of & .MuiTableCell-head), then reverted and confirmed green.

Verification

npm run typecheck: clean. npm test -- --run: 3390/3390 (no test count change, this round only touched one guard's internals).

Stopping the review loop here

Round 1 confirmed two real product defects (the sticky header, the stale selection); round 2's only finding was against the GUARD ITSELF, not the shipped product code — the first round in this loop whose entire yield was test-scaffolding. Stopping deliberately at this count rather than waiting for a second consecutive no-defect round: another pass over an already-narrow guard-identity question is unlikely to buy anything further, and the cost of another round is real. Not triggering a round 3. Happy to keep going if you want a deeper pass — this is a stop, not a claim that nothing more could ever be found.

Ran the deslop skill over the full origin/main...HEAD diff — the seven
commits after 1b13ad7 had never been passed through it. All findings were
comment-only:

- Comments narrating "codex review", "codex/owner review", or a specific
  round number, added while fixing PR review findings, rewritten to cite
  the actual issue (#908 for the feature, #824 for the guard the comments
  live in) and state the technical why without narrating the review
  process that produced it.
- Several of those comments ran to 6-9 lines; trimmed to the 4-line cap,
  keeping only the non-obvious constraint.

No `any`/`as any`/`eslint-disable`, no defensive try/catch on a trusted
path, and no nesting an early return would flatten were found anywhere in
the branch's diff. Behavior unchanged: typecheck clean, full web suite
3390/3390.
… last

The shared MuiTableCell padding gave every cell 1rem on the right and none
on the left, correct for the borderless ledger it was written against. Every
table now sits inside a bordered FieldConsole panel, so the first cell's text
sat flush against that border on every screen with no local override
(Customers, Grades, Flocks, Products, Users). Add the inset at the base
rule's own specificity via :where(:first-of-type), so routes that already
carry their own higher-specificity override (FieldConsole's ledgers,
Dashboard's inline table) keep their existing, already-symmetric padding.
…908)

Users' Status column and header were blank for active users, leaving row
status implicit. Show an explicit Active/Disabled badge in both the table and
inspector, and give the Actions column the header the other screens already
have (tc("actions"), Products' own precedent).

Destructive row/inspector actions (deactivate, archive, deplete, disable)
read identically to the primary edit link across all five screens, colour
being their only distinguishing signal once colour vision is set aside. Add
CONSOLE_DESTRUCTIVE_LINK_SX (FieldConsole's shared token, not per-screen sx)
plus a leading TriangleAlert/Ban icon at each destructive call site, so shape
carries the signal alongside colour.
)

CONSOLE_DESTRUCTIVE_LINK_SX used --danger, a token designed as a filled-
button background. Dark mode's --danger measures ~3:1 against --surface and
its own hover fill, under AA's 4.5:1 floor for text. Switch to --error, the
token already tuned for small-text contrast; add a red-first styles.test.ts
assertion that reads the actual style value (not an assumed token name) and
checks contrast against both rest and hover fill, every brand and mode.

The destructive/status marker tests asserted only colour and text, so
removing the icon or swapping StatusBadge for plain text kept them green.
Assert the actual aria-hidden icon and StatusBadge class now, mutation-
checked (icon removed, badge swapped for text) to confirm each goes red.

Users' Active badge read a bespoke `activeBadge` catalog entry, duplicating
enums:status.Active in three locales. Render statusLabel("Active") instead,
i18n/enums.ts's one sanctioned renderer for this vocabulary, and delete the
three now-redundant catalog entries.
@mforce

mforce commented Sep 23, 2026

Copy link
Copy Markdown
Owner Author

Codex (gpt-5.6-sol) round 3 at 5418f87

No P1 findings. Fixed both flagged product defects and the noted test gap; new head 6576010.

  1. P2 FieldConsole.tsx:32 — CONSOLE_DESTRUCTIVE_LINK_SX used --danger as normal-size text; measured myself: dark mode's --danger is 2.98:1 against --surface and 2.71:1 against the hover fill --tint-danger, both under AA's 4.5:1. Fixed by switching color/textDecorationColor to --error, the token already tuned for small text (measured 5.47:1/4.58:1 light, 6.95:1/6.32:1 dark). Fix: 6576010. Verification: a new styles.test.ts assertion reads CONSOLE_DESTRUCTIVE_LINK_SX's actual color/hover-fill token names (not an assumed token) and checks contrast against both --surface and --tint-danger, every brand and mode — ran red first against the pre-fix --danger value (4 dark-mode failures at ~2.98:1, all four light modes passing as expected), green after the fix (8/8).

  2. P3 marker/badge tests only checked colour and text — GradesPage.test.tsx, FlocksPage.test.tsx, ProductsPage.test.tsx, UsersPage.test.tsx (both the inspector-status test and the row-status test) now assert the actual aria-hidden Lucide SVG inside each destructive button (.lucide-triangle-alert / .lucide-ban) and that the Active label carries StatusBadge's badge badge-ok classes, alongside the existing negative colour assertions for Edit/Activate/Reactivate/Enable. Fix: 6576010. Mutation-checked myself, each confirmed red then restored: removing the TriangleAlert icon from Grades' deactivate button failed the new icon assertion; removing the Ban icon from Users' inspector disable button failed the same way; swapping the Users row's Active StatusBadge for plain statusLabel("Active") text failed the toHaveClass("badge", "badge-ok") assertion.

  3. P3 UsersPage.tsx:876 — duplicate Active translation — removed the three activeBadge catalog entries (en/es/tl) and render statusLabel("Active") from i18n/enums.ts in both the row and inspector badges, the same sanctioned renderer Grades/Products already use for this vocabulary. Fix: 6576010.

Verification: npm run typecheck clean, full web suite 3403/3403, full quick Playwright suite on a rebuilt isolated stack at 6576010: 95 passed, 1 dispatch-only skip.

@mforce

mforce commented Sep 23, 2026

Copy link
Copy Markdown
Owner Author

Codex (gpt-5.6-sol) review round 4 at 6576010: no P1, P2 or P3 findings.

It checked the round-3 fixes: --error contrast in light and dark against --surface, --tint-danger and the inspector background; that the icon and badge tests fail when the icon or badge is removed; and the statusLabel("Active") switch. It also rescanned the full diff. The review loop stops here, per the owner's rule: continue only while P1/P2 findings appear.

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.

web: redesign the CRUD list screens after the MUI conversion

1 participant