diff --git a/docs/FORK-HARVEST-TODO.md b/docs/FORK-HARVEST-TODO.md index 1f7dfb7..cc35783 100644 --- a/docs/FORK-HARVEST-TODO.md +++ b/docs/FORK-HARVEST-TODO.md @@ -255,11 +255,16 @@ The **client half and the contract are done** (2026-08-21). What remains is back This is the concrete form of their strongest objection — "mock repositories as production implementation" — and it is mechanical to fix. The pattern to copy is already in this repo: `mock-data.interceptor.production.ts` swapped in via `fileReplacements`. **Done when:** `scan-bundle.sh` can gate on mock fixture markers and pass. -- [ ] **FH-E.5 — Reduce `localStorage` to cache, never truth** · M · Lane A - 19 files touch `localStorage`, mostly admin facades and `project-editor-draft-storage.service.ts`. Their disqualifying objection is not "you use localStorage" — it is "localStorage is your source of truth." - **Do:** keep local drafts as an offline convenience with an explicit "unsaved local draft" indicator and server-wins reconciliation; never let a local value be the published state. - **Done when:** no admin or editor write path can publish without a server round-trip. +- [x] **FH-E.5 — Reduce `localStorage` to cache, never truth** · M · **audited + gap marked 2026-08-21** + Audited all 21 `localStorage` users. The premise — "localStorage is your source of truth" — turned out **already false** across the app: + - Every admin facade (products, categories, orders, moderation, dashboard) uses `localStorage` for **view preferences only** — `viewMode`, `density`, `visibleColumns`, `expandedIds`, `sort`. Entity CRUD goes through the API gateways. That is cache, not truth. + - `currency-rates.service.ts` already removed its localStorage-typed rates (its own comment records it). + - `language`, `location` region, `search-history`, `api-headers` anonymous session id, `admin-preferences` — all legitimate preference/cache. + - The editor already surfaces an **"unsaved local draft restored"** banner (`draftRestored` → save bar), which is the recovery-cache indicator this item asked for. + **One real gap, and it is backend-blocked:** the project editor's `publish()` applies config to the in-memory runtime and saves the draft to localStorage, then declares itself published — no server round-trip, because the PHASE-9 §5 revision API does not exist yet. Marked precisely in `publish()` with the required behaviour (await the server, only then mark published) and cross-referenced to the contract. Cannot be finished on the frontend alone; the contract for the fix is already written. + + Net: nothing to rip out — the codebase was already at the target state everywhere the backend exists to support it. --- ## Scoreboard @@ -272,8 +277,8 @@ The **client half and the contract are done** (2026-08-21). What remains is back | 3 — Proof | 3 | 1 | 1 | see note below | | 4 — Identity | 4 | 3 | 1 | OAuth apps, which FH-0.1 gates | | Ops | 1 | — | 2 | — | -| Process | 5 | — | 1 | — | -| **Total** | **29** | **4** | **9** | 1 rejected | +| Process | 6 | — | 0 | — | +| **Total** | **30** | **4** | **8** | 1 rejected | **Landed 2026-08-21** diff --git a/src/app/features/project-editor/facade/project-editor.facade.ts b/src/app/features/project-editor/facade/project-editor.facade.ts index 8b0e947..535f47b 100644 --- a/src/app/features/project-editor/facade/project-editor.facade.ts +++ b/src/app/features/project-editor/facade/project-editor.facade.ts @@ -508,6 +508,15 @@ export class ProjectEditorFacade { if (!current || this.hasBlockingIssues()) { return false; } + // FH-E.5 / PHASE-9 §5. This is a LOCAL preview publish: it applies the + // config to the in-memory runtime and saves the draft to localStorage. + // localStorage is a recovery cache here (see draftRestored), never the + // published source of truth. Real publish must round-trip through the + // revision API - POST .../revisions/{id}/publish - which creates an + // immutable server-side revision and flips the published pointer in one + // transaction. Until that endpoint exists this stays local-only and must + // not be treated as authoritative; when it ships, this method awaits the + // server response and only then marks status 'published'. this.runtime.reloadFromBootstrap(current); const savedAt = this.draftStorage.save(current); this.state.update(state => ({