Skip to content

fix(ui): audit mutation-state fixes UI-01/02/03/05/07/08/09/22/25 - #181

Merged
lnieuwenhuis merged 16 commits into
stagingfrom
fix/audit-ui-state-staging
Sep 17, 2026
Merged

lnieuwenhuis merged 16 commits into
stagingfrom
fix/audit-ui-state-staging

Conversation

@lnieuwenhuis

Copy link
Copy Markdown
Owner

Scope

Release branch for the UI mutation-state audit seat: the 8 owned commits of fix/audit-ui-mutation-state cherry-picked onto fresh staging (no dev docs drift), plus 4 independent-review commits. Findings covered: UI-01, UI-02, UI-03, UI-05, UI-07, UI-08, UI-09, UI-22, UI-25.

No production dependency added, no paid provider calls, no actions.ts/proxy.ts/Rust changes. Bug fixes keep app version 3.5.0.

What ships (cherry-picked audit fixes)

  • UI-05 — use-action-runner / use-template-mutations route rejected transport promises through fallbackError + onError (gym rollback preserved); NEXT_REDIRECT / NEXT_NOT_FOUND rethrow so framework control flow survives.
  • UI-01 — photo/barcode dialogs await a Promise<boolean> template save, show Saving… / a local error, and show Saved! only after real success; resolved failure, transport rejection and manual product save stay retryable.
  • UI-02 — analysisSequenceRef invalidates in-flight photo analysis on image change/unmount; a held A estimate or clarification never lands on photo B.
  • UI-07 — barcode lookup distinguishes auth (401/403, followed login redirect, HTML login body) from outages; scanner adds sign-in recovery and retries with the kept barcode.
  • UI-09 — decimal-comma normalization applies to numeric fields only; Yoghurt, Grieks keeps its comma.
  • UI-08 — shared shouldSuppressGlobalShortcut stops global day arrows while dialogs/menus own keyboard interaction.
  • UI-22 — the search modal captures the local day per operation; a retry of a pre-midnight attempt keeps its original date and idempotency key.
  • UI-03 — status changes merge the server row under pre-status edits; same-day refreshes reconcile against the pre-refresh baseline so dirty fields stay local and untouched fields adopt server values.
  • UI-25 — per-entry pending set with a synchronous admission guard; completions/failures clear only their own card, and saves preserve edits admitted mid-flight.

Independent review commits on top

  • fix(ui): keep framework control flow rethrowing through mutation catches — the new UI-05 catches swallowed NEXT_REDIRECT/NEXT_NOT_FOUND; the predicate moves to lib/framework-control-flow.ts and every new catch site (capture dialogs, food search, dashboard card/group/copy) rethrows it.
  • fix(ui): reset scanner mount marker on StrictMode effect replay — with React StrictMode (default in the app router), the cleanup-only mountedRef left Retry permanently short-circuiting in development.
  • fix(ui): keep HTML error responses as barcode outages — a 5xx HTML outage page was classified as auth recovery; non-OK now stays unavailable, while 2xx login HTML/redirects stay auth.
  • test(e2e): prove an unsaved draft label survives a status change — browser-level UI-03 regression.

Verification

  • pnpm typecheck (db + web): PASS.
  • pnpm lint (web eslint): PASS.
  • Database-free web unit subset: 69 files / 405 tests PASS (incl. 7 new files from this branch).
  • Playwright E2E against a disposable PostgreSQL + local Rust backend (next dev, i.e. StrictMode): 28 passed, including the new UI-03 dirty-draft test.
  • gitleaks git on the 12-commit range: no leaks found.
  • Reviewed with fresh eyes: stale-photo guard, rejection rollback/control-flow, honest saved feedback, dirty merge + per-card pending, auth errors, midnight retry identity, modal shortcut suppression.

Merge hold — do not merge yet

Client-side dirty-field merging and the DATA persistence prerequisite must ship coherently. Hold merge until the DATA persistence PR lands; then rebase onto fresh staging, resolve adjacent worker changes (e.g. recipe-builder-shell.tsx from UI-controls), re-run the checks above, and only merge on green CI.

UI-05: use-action-runner catches transport rejection into fallbackError/onError
without refresh; NEXT redirect/notFound still rethrow. Template save/delete/update
catch rejection (delete restores optimistic row) and return false for honest
retry. Regression: runner rejection + recovery, template save/delete/retry.
UI-02: analysis sequence guard commits estimate/question/error only for the
request/image that produced them; replacement clears analyzing state so the new
photo is actionable. Regression: held A estimate and clarification released
after B selection never appear.
UI-07: 401/403, redirected and HTML-login barcode responses return auth, with
sign-in recovery and kept-code retry in the scanner instead of a connectivity
error. UI-09: decimal-comma normalization applies to numeric fields only; the
barcode label keeps commas verbatim.
UI-08: shared shouldSuppressGlobalShortcut covers defaultPrevented events,
focus inside dialog/menu/listbox, and any open dialog/menu in the document, so
arrows on modal buttons no longer navigate the day and remount the page.
UI-22: history copy and product add use a fresh local date for each new
operation instead of the mount-memoized today; retries keep the original
date/key until settled. Search transport rejection now surfaces a local error
instead of an unhandled rejection.
UI-01: photo and barcode dialogs await the template save, show Saving and a
local error, and mark Saved only on success; resolved failures and transport
rejections keep retry. Manual barcode product save handles transport rejection
without hanging. Dashboard and recipe consumers return the save promise.
UI-03: status changes merge the server row under unsaved edits instead of
replacing the draft; same-day refreshes reconcile against the pre-refresh
baseline so unrelated cards keep dirty fields while untouched fields adopt
fresh server values. UI-25: single saving/active markers become a per-entry
pending set with a synchronous admission guard; completions and failures
release only their own card, and saves preserve edits admitted mid-flight.
UI-05: dashboard save/status/delete/group-copy/apply/group-manager paths route
transport rejection to per-card local errors with rollback. Add-to-today from
history stays intentional.
The UI-05 catches added in this branch swallow Next's NEXT_REDIRECT/NEXT_NOT_FOUND rejects from server actions, so a session expiry or framework navigation that arrives as a rejected action promise is shown as a local save error instead of being handled by Next. Move the control-flow predicate into lib/framework-control-flow.ts, use it from the runner and template hooks, and rethrow it in the new catch sites (capture dialogs, food search, dashboard card/group/copy handlers).
React StrictMode (default in the app router) runs effect setup, cleanup, setup on mount. The cleanup-only mountedRef left retryBarcodeLookup short-circuiting permanently in development, so Retry after an auth failure did nothing. Set the marker in setup as well as clearing it in cleanup.
UI-07 classified any redirected or text/html response as auth before checking the status, so a 5xx outage page served as HTML told the user to sign in again. Check the non-OK status first; 2xx HTML responses and followed login redirects stay auth recovery.
Browser-level regression for UI-03: edit a planned card label, mark it eaten, and assert the unsaved label survives the status response and the same-day refresh while the status is what actually persisted.
…-staging

Integrate DATA (#184) after controls (#183), migrations (#180), security (#178), build (#175) and benchmark (#179). Semantic resolution: keep DATA's BrowserFoodProduct types together with the UI-22 per-operation date/idempotency-key logic in food-search-modal.tsx (drop the mount-memoized todayStr/useMemo); all other overlaps auto-merged, including the promise-returning onSaveAsPreset against the controls recipe-builder changes. Preserves controls, security, benchmark and DATA/API-05 patch semantics alongside UI-03 client dirty-draft merging.
…-staging

Integrate contracts (#177) after DATA (#184); no file overlap with this seat (api-v1/openapi/docs/packages-db types only).
@lnieuwenhuis
lnieuwenhuis merged commit b2c7959 into staging Sep 17, 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