# Master-detail state and bounded review lists

Use this pattern for sensitive, read-only review workspaces such as document histories, reconciliation queues, and staged mapping reviews.

## Durable route state

- Treat URL/history as the durable owner of workspace, filters, sort, and selected opaque item.
- A visible mobile “back to list” action must remove the selection parameter and create the matching history state; do not only toggle CSS/DOM visibility.
- Test selection → reload → browser back → browser forward, plus mobile back → URL has no selection → reload remains on the list.
- Do not restore a selected detail only by searching the currently loaded list page. A valid deep link may point outside page 1. Resolve the opaque item through an existing bounded direct-detail endpoint; for chunked content, follow opaque cursors under an explicit page cap.

## Exact action receipts

- Bind a queued-action receipt to the exact submitted opaque queue key.
- “Queued/vorgemerkt” is still open work, not completed work. An `open` filter must retain that exact row after redirect.
- Never use one global receipt flag to mark every list row queued. Compare the receipt route’s selected key to each item.
- Acceptance-test that submit + CSRF redirect preserves the same selected key, heading, editor, and queued status.

## Render one editor, not hidden editors

- Build the selected detail/editor independently.
- Do not create a form for every row inside closed `<details>` cards and then detach/move one form into the detail pane. This leaves correctness dependent on hidden legacy DOM and can silently produce a detail without an editor.
- Keep technical provenance in a collapsed detail section, but do not make the primary editor itself depend on that disclosure element.

## Fail-closed classification and browser-bundle privacy

- Do not infer “simple food”, “composite product”, medical status, or trust state from unrestricted free-text reason/notes fields.
- Use a structured allowlisted enum. If none exists, show “not determined” and let simple/composite filters return only explicitly classified rows.
- Do not prefill a user-editable note with unrestricted worker/database reason text unless it has a dedicated safe-public contract.
- Removing a field from visible DOM is insufficient if the field is still serialized into generated HTML, bootstrap JSON, JavaScript state, URLs, logs, or storage. Browser delivery itself is disclosure.
- Remove unrestricted fields at the provider/query boundary and from the bundle allowlist/contract, not merely from rendering code.
- Add a synthetic sentinel probe: place a unique private-looking value in the source field, generate the complete browser artifact, and assert the sentinel is absent from the HTML and loaded client state. Also assert the projected row no longer contains the key.

## Filtering after a bounded page

- A client-side filter applied after `LIMIT` is page-local, not global.
- Prefer a pre-limit server filter only when the sprint explicitly permits an API semantic change and that change is reported before implementation.
- If API semantics are frozen, preserve cursor pagination and label page-local technical filters truthfully: state the page size, keep “load more” available, and use an empty state such as “no matches on this loaded page.” Do not pair a page-local list with coverage that appears filtered when coverage is global.

## Focused acceptance

Minimum browser checks:

1. desktop master-detail with exactly one live editor;
2. 390px list-first flow, 44px controls, no page overflow;
3. filters/sort/selection survive reload and history;
4. task/deep-link routing restores the exact item;
5. mobile back clears durable selection state;
6. out-of-page direct detail/preview still resolves;
7. action submit preserves exact identity and CSRF contract;
8. zero console errors, failed requests, external requests, raw IDs, paths, or credentials.

When adding a new focused browser group, register it both in its direct group selector and in the eventual default matrix, even when the current sprint intentionally does not run that full matrix.