## Outcome - Reviewed the exact clean tree at `9e08547043fbe667cb102c3d3fdae8cdef05245f`. - Scope covered: - `frontend/src/pages/HouseholdImportsPage.vue` - `frontend/src/pages/HouseholdUx.test.ts` - `frontend/src/api/household.ts` - `frontend/src/api/household.test.ts` - `frontend/src/api/client.ts` - No files created or modified. `git diff --check` and `git status --porcelain` were clean. ## Key findings and risks ### Important — current workflow previews every individual edit Each category, cluster, exclusion, or unmatched-transfer action immediately calls `refreshBoundPreview()`: - row category: `HouseholdImportsPage.vue:326-330` - cluster category: `342-349` - cluster exclusion: `351-357` - unmatched transfer: `359-362` This causes one POST and full preview rerender per click, resets pagination on each response, disables controls while loading, and prevents users from reviewing several decisions as one coherent batch. ### Important — a failed decision refresh destroys the visible preview `refreshBoundPreview()` sets `previewResult = null` on failure (`281-293`). The user loses the current review surface even though the last server-bound preview remains conceptually valid. For batched decisions, failed Apply must instead: - keep the last applied preview visible, - retain all draft decisions, - remain dirty/stale, - disable confirmation, - allow retry. ### Important — dirty drafts must never share the confirm request `confirmImport()` currently reconstructs the request from mutable decision refs (`311-317`). That is safe only because every normal UI mutation currently triggers another preview. Once edits become local, confirmation must use an immutable snapshot saved by the **last successful preview/Apply**, not current draft state. Otherwise an old fingerprint could be submitted with newer, unpreviewed decisions. ### Moderate — exclusions can be accidentally discarded Changing a cluster category preserves exclusions (`342-348`), but clearing the category deletes the complete cluster decision (`343`), including exclusions. A local draft model should retain exclusions while the category is temporarily empty, then omit that incomplete decision when serializing an Apply request. ### Moderate — tests encode the old chatty behavior The following tests require immediate re-preview and must be replaced: - `HouseholdUx.test.ts:217-247` - `HouseholdUx.test.ts:249-308` - pagination refresh expectation at `460-475` Existing file/mapping invalidation tests at `310-353`, `377-434`, and `477-490` remain useful and should be extended for drafts and in-flight Apply responses. ### Privacy posture is currently good, but untested - No `localStorage`, `sessionStorage`, or `console.*` use was found in the reviewed frontend source. - Raw CSV remains in Vue refs and POST request bodies. - `apiPost()` does not cache or log request bodies. - The API session cache is GET-only and therefore does not retain CSV payloads. - Filename and fingerprint values are not rendered. Add regression coverage because draft-state work makes accidental persistence tempting. ## Exact implementation plan ### `HouseholdImportsPage.vue` 1. **Separate draft state from server-bound state** - Replace the single mutable decision set with: - `appliedDecisions`: the decisions represented by the visible preview. - `draftDecisions`: locally edited decisions. - `boundPreviewRequest`: immutable normalized request used to produce the current preview. - Keep `previewFiles` as the file binding, but snapshot/clone it when previewing. - Use a local cluster-draft type whose `category_id` may be empty while retaining `excluded_row_tokens`. 2. **Normalize and compare decisions** - Add helpers such as: - `cloneDecisionState()` - `normalizeDecisionState()` - `requestFrom(files, decisions)` - Sort cluster decisions and excluded row tokens before comparison/request creation. - Compute `decisionsDirty` by normalized applied-versus-draft equality, so reverting all edits automatically returns to clean state. 3. **Render controls from draft values** - Row select: draft category override, falling back to the visible preview item category. - Cluster select: draft cluster category, falling back to the applied/preview category. - Cluster checkboxes: draft exclusion set. - Transfer button: draft user decision, falling back to the applied preview state. - Keep unedited categories, transfer choices, cluster choices, and exclusions in every Apply request. 4. **Convert action handlers to synchronous draft edits** - `changePreviewCategory`, `changeClusterCategory`, `toggleClusterRow`, and `toggleUnmatchedTransfer` must only update draft state. - They must not call `previewHouseholdImport()`. - They must not reset pagination; users should remain where they are while editing. 5. **Add one visible Apply action** - Add `data-testid="import-apply-decisions"` near the readiness/confirmation actions. - Suggested label: **“Änderungen anwenden”**. - Enable only when `decisionsDirty && !controlsBusy`. - On Apply: - snapshot files and the complete normalized draft state; - make exactly one preview POST; - atomically replace `previewResult`, `appliedDecisions`, `draftDecisions`, and `boundPreviewRequest` only on success; - reset pagination only after successful Apply; - discard a late response if files/mappings changed or a newer request won. 6. **Represent stale state explicitly** - While dirty, keep the prior preview visible but show: - “Änderungen noch nicht angewendet.” - “Kennzahlen und Importbereitschaft beziehen sich auf die letzte angewendete Vorschau.” - Disable `import-confirm` whenever dirty. - On Apply failure, retain preview and drafts, leave dirty state true, and show retryable error copy. 7. **Bind confirmation to the successful Apply** - `confirmImport()` must submit `boundPreviewRequest` plus the fingerprints from that exact `previewResult`. - Never rebuild confirmation from `draftDecisions`. - Require all of: - bound request exists, - not dirty, - business-ready, - not loading. 8. **Invalidate all state on input binding changes** - The existing synchronous watcher (`181-184`) and replacement-start invalidation (`211-256`) should clear: - visible preview, - bound request/files, - applied decisions, - draft decisions, - dirty state, - request generations. - Preserve the current behavior where replacement reading invalidates confirmation immediately, before `file.text()` resolves. - Mapping/profile changes and slot removal must do the same. 9. **Simplify visible copy** - Replace “fingerprintgebunden” with “Alle Dateien werden gemeinsam geprüft.” - Replace “Technisch bestätigbar/Fachlich bereit” with user-facing terms such as: - “Vorschau gültig” - “Import bereit” / “Noch Entscheidungen offen” - Rename confirmation to **“Import bestätigen”**. - Technical details should say: “Die Bestätigung gilt nur für genau diese angewendete Vorschau,” without exposing fingerprint terminology or values. 10. **Privacy** - Keep drafts in component memory only. - Do not introduce persistence, analytics, debug logging, or request-body logging. - Continue clearing CSV and decisions after confirm/discard. ## Exact test plan Update `HouseholdUx.test.ts` with these cases: 1. **Several edits produce no preview calls** - Create initial preview. - Change a row category, cluster category, exclusion, and transfer decision. - Assert preview API call count remains one. - Assert dirty message and Apply button are visible. - Assert confirm is disabled. 2. **One Apply submits the complete batch** - Click Apply. - Assert exactly one additional preview POST containing all category overrides, user decisions, cluster decisions, and exclusions. - Assert successful response clears dirty state and updates displayed readiness. 3. **Unedited decisions survive later edits** - Apply cluster category plus exclusion. - Edit only an unrelated row. - Apply again. - Assert the second request still contains the prior cluster category and exclusion. 4. **Changing cluster category preserves exclusions** - Exclude a member, change the category twice, and assert the exclusion remains. - Temporarily clear/reselect the category and verify the draft exclusion survives. 5. **Confirm uses only the applied snapshot** - After successful Apply, assert confirm sends that full request and its latest fingerprints. - Make another unsaved edit and assert confirm becomes disabled and cannot call the confirm API. 6. **Apply failure is recoverable** - Reject Apply. - Assert preview remains rendered, drafts remain selected, dirty remains true, confirm remains disabled. - Retry and assert the identical complete draft is submitted. 7. **Late Apply response is discarded** - Start Apply, then replace a file/change mapping. - Resolve Apply. - Assert preview/bound request stay null and confirmation does not reappear. 8. **File/mapping invalidation clears both layers** - Extend existing invalidation tests to verify draft and applied decision state are cleared immediately. 9. **Pagination** - Local edits must preserve current decision/cluster page. - Only successful Apply may reset pages. 10. **Privacy regression** - Spy on `Storage.prototype.setItem`, `console.log`, `console.info`, `console.warn`, and `console.error`. - Complete draft → Apply → Confirm and assert no CSV, filename, row token, fingerprint, or decision payload is persisted/logged. - Keep DOM assertions that raw CSV, filenames, and fingerprint values never render. `household.test.ts` needs no contract change; retain the existing full-decision serialization test and optionally add a confirm request containing all three decision collections plus fingerprints. ## Real-workflow UAT release gate After implementation, run the actual frontend/backend flow with authorized local source files: 1. Select 2–4 supported files and mappings; create one joint preview. 2. Make at least five mixed decisions across pages, including a cluster exclusion and unmatched transfer. 3. Verify no network preview request occurs until **Änderungen anwenden**. 4. Apply once; verify one preview request and updated readiness/counts. 5. Change one unrelated item; verify prior decisions/exclusions remain selected and are included in the next Apply. 6. Simulate failed Apply; verify preview/drafts remain and confirm stays blocked. 7. Replace a file while dirty; verify preview and drafts disappear immediately. 8. Apply successfully and confirm; verify the import history updates exactly once. 9. Reload before confirmation and verify CSV/drafts are gone—no browser persistence. 10. Do not save/share DevTools payloads, screenshots containing transaction details, or raw files. ## Verification issue Focused tests could not execute because dependencies are absent: `vitest: not found`. I did not run `npm ci` because this assignment was explicitly read-only.