From 53fecbf4a73ff542fa58c488abe8d40ce0abaeb3 Mon Sep 17 00:00:00 2001 From: Till JS Date: Mon, 27 Apr 2026 01:26:21 +0200 Subject: [PATCH] =?UTF-8?q?chore(dexie):=20v55=20=E2=80=94=20sweep=20orpha?= =?UTF-8?q?n=20updatedAt=20field=20from=20existing=20rows=20(F3=20cleanup)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After F3 of the sync field-meta overhaul, every read of "last modified" goes through `deriveUpdatedAt(record)` over `__fieldMeta`. The legacy `updatedAt` field on existing IndexedDB rows was deliberately left in place by v53 (its comment explicitly defers the row-rewrite to a later cleanup) so the cut-over could proceed without a full DB rewrite. This v55 upgrade walks every sync-relevant table (`Object.keys(TABLE_TO_APP)`) and `delete row.updatedAt`. Idempotent (rows without the field are a no-op), best-effort (try/catch per table guards against a registry entry that doesn't yet have a Dexie store row). Local-only tables (_pendingChanges, _activity, _clientIdentity, _aiDebugLog) never carried `updatedAt`, so they stay out of the sweep. Plan: docs/plans/sync-field-meta-overhaul.md (F3-fu row in Shipping Log). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../apps/web/src/lib/data/DATA_LAYER_AUDIT.md | 3 +- apps/mana/apps/web/src/lib/data/database.ts | 35 +++++++++++++++++++ docs/plans/sync-field-meta-overhaul.md | 3 +- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/apps/mana/apps/web/src/lib/data/DATA_LAYER_AUDIT.md b/apps/mana/apps/web/src/lib/data/DATA_LAYER_AUDIT.md index b28ff584c..7273c3f8e 100644 --- a/apps/mana/apps/web/src/lib/data/DATA_LAYER_AUDIT.md +++ b/apps/mana/apps/web/src/lib/data/DATA_LAYER_AUDIT.md @@ -185,7 +185,7 @@ Logout / Tab-Close → MemoryKeyProvider.setKey(null) → Cyphertext bleibt ### Eckdaten - **120+ Collections** in einer einzigen IndexedDB -- **Schema-Versionen** 1–54 (v53 ersetzte `updatedAt`-Indizes durch `_updatedAtIndex`, v54 fügte `_clientIdentity` für stabile Client-IDs hinzu) +- **Schema-Versionen** 1–55 (v53 ersetzte `updatedAt`-Indizes durch `_updatedAtIndex`, v54 fügte `_clientIdentity` für stabile Client-IDs hinzu, v55 löscht den Orphan-`updatedAt`-Wert aus existing rows nach F3-Cutover) - **Eager Apps**: mana, todo, calendar, contacts, tags, links — syncen beim Start - **Lazy Apps**: starten Sync erst beim ersten Modul-Besuch via `ensureAppSynced()` - **Conflict Resolution**: ✅ Origin-gated Field-Level LWW via `__fieldMeta` (siehe Sync Field-Meta Overhaul unten) @@ -205,6 +205,7 @@ Sieben Phasen, die vier strukturelle Bugs in der Conflict-Detection abgeräumt h - **F5** (`d78f57c04`) `userContextStore.ensureDoc()` Public-API entfernt. Internal `getOrCreateLocalDoc()` bleibt als Fallback für brand-new clients deren First-Pull noch nicht durch ist. UI mountet ohne ensureDoc-Race. - **F6** (`a031493fe`) Stable `client_id` in Dexie-Tabelle `_clientIdentity`. `restoreClientIdFromDexie()` läuft im (app)-Layout vor `createUnifiedSync` und reconciliated Dexie ↔ localStorage. Dexie ist canonical, localStorage ist fast-read-cache. Survives clear-site-data und incognito flush. - **F7** (`2a8e8ff98`) `repair-silent-twin.ts` + `legacy-avatar.ts` Migrationen ersatzlos gelöscht — pre-live, keine Live-Daten brauchen sie. Orphan-localStorage-Flags-Sweep im Boot (`migrations-cleanup.ts`, `119cd2cf8`) räumt die zugehörigen Flags auf. +- **F3-fu (v55 cleanup)** (_pending_) Dexie v55 row-rewrite: löscht den Orphan-`updatedAt`-Wert aus jedem Row in `Object.keys(TABLE_TO_APP)`. v53 hatte ihn bewusst stehengelassen (Comment "next-version upgrade can drop it"); nach F3+F5 liest niemand mehr `row.updatedAt`, also pure waste. Idempotent — rows ohne das Feld sind ein no-op. Die vier Bug-Wurzeln (siehe ursprüngliche Diagnose 2026-04-26): diff --git a/apps/mana/apps/web/src/lib/data/database.ts b/apps/mana/apps/web/src/lib/data/database.ts index 908c56a0e..30980ac19 100644 --- a/apps/mana/apps/web/src/lib/data/database.ts +++ b/apps/mana/apps/web/src/lib/data/database.ts @@ -1369,6 +1369,41 @@ db.version(54).stores({ _clientIdentity: 'id', }); +// v55 — Sync Field-Meta Overhaul F3 cleanup. +// The v53 upgrade kept the legacy `updatedAt` field on existing rows so +// nothing read it during the cut-over (the F3 sweep migrated 121 store +// files + 43 Local-* types in one pass; v53's comment explicitly +// deferred the row-rewrite). All reads now go through +// `deriveUpdatedAt(record)` from `__fieldMeta`, so the orphan field is +// pure waste — bytes per row, encrypted-blob noise on the encrypted +// tables, and a confusing artifact in the IndexedDB inspector. +// +// Walk every sync-relevant table (the `TABLE_TO_APP` registry) and +// delete `updatedAt` from each row. Idempotent: rows without the field +// are a no-op. Local-only tables (_pendingChanges, _activity, +// _clientIdentity, _aiDebugLog, …) never carried `updatedAt` so they +// stay out of the sweep. +db.version(55).upgrade(async (tx) => { + const tables = Object.keys(TABLE_TO_APP); + for (const tableName of tables) { + try { + await tx + .table(tableName) + .toCollection() + .modify((row: Record) => { + if ('updatedAt' in row) { + delete row.updatedAt; + } + }); + } catch { + // A table may exist in the registry but not in this Dexie + // version (e.g. a future addition with no upgrade row yet). + // The sweep is best-effort cleanup, not load-bearing — skip + // missing tables silently. + } + } +}); + // ─── Sync Routing ────────────────────────────────────────── // SYNC_APP_MAP, TABLE_TO_SYNC_NAME, TABLE_TO_APP, SYNC_NAME_TO_TABLE, // toSyncName() and fromSyncName() are now derived from per-module diff --git a/docs/plans/sync-field-meta-overhaul.md b/docs/plans/sync-field-meta-overhaul.md index dc7ee6a3c..2d74a71d2 100644 --- a/docs/plans/sync-field-meta-overhaul.md +++ b/docs/plans/sync-field-meta-overhaul.md @@ -152,7 +152,8 @@ _Wird befüllt während der Ausführung._ | F5 | `d78f57c04` | `userContextStore.ensureDoc()` aus der Public-API entfernt; die drei `void userContextStore.ensureDoc()` calls in ContextOverview/ContextInterview/ContextFreeform sind weg. Internal `getOrCreateLocalDoc()` bleibt als Fallback für brand-new clients deren Pull noch nicht durch ist. `kontextStore.ensureDoc()` bleibt — der ist per-Space, kein server-bootstrap. | | F6 | `a031493fe` | Stable `client_id` in Dexie. Neue Tabelle `_clientIdentity` (single row keyed by `id='self'`). `restoreClientIdFromDexie()` läuft einmal beim Boot in `+layout.svelte` vor `createUnifiedSync` und reconciliated Dexie ↔ localStorage: Dexie ist canonical, localStorage ist fast-read cache. Ein localStorage-Wipe wird beim nächsten Boot aus Dexie restored. Dexie v54 mit `_clientIdentity: 'id'`. Survives clear-site-data, incognito flush. | | F7 | `2a8e8ff98` | `repair-silent-twin.ts` + `legacy-avatar.ts` Migrationen ersatzlos gelöscht. Beide existierten nur um die Symptome eines fixed-in-M2.5 Bugs zu cleanen, der pre-live keine echten Daten produziert hat. Mit F2's `origin='migration'` wrapper + F3's drop von synced `updatedAt` würden ihre writes auch nicht mehr als Conflicts auftauchen — sie waren strukturell überflüssig. Caller in MeImagesView + wardrobe/ListView entfernt; leere `migration/` Directory gelöscht. | -| F4-fu (kontextDoc) | _pending_ | F4-Symmetrie: `bootstrapSpaceSingletons(spaceId, ownerUserId, syncSql)` in `bootstrap-singletons.ts`, schreibt einen leeren `kontext/kontextDoc` row pro Space-Erstellung in `mana_sync.sync_changes` mit `client_id='system:bootstrap'`, `origin='system'`. Zwei Aufruf-Sites: `databaseHooks.user.create.after` (Personal-Space; nur wenn `createPersonalSpaceFor` `created: true` zurückgibt) + `organizationHooks.afterCreateOrganization` (alle non-personal Spaces). `createBetterAuth` kriegt `syncDatabaseUrl` als zweites Argument; lazy module-scoped postgres-pool. Webapp `kontextStore.ensureDoc()` zu privat `getOrCreateLocalDoc()` umbenannt — Public-API ist nur noch `setContent` + `appendContent`. Kontext content bleibt encrypted at rest auf dem Client (`kontextDoc.content`); der Server-Bootstrap schreibt `''` plaintext, was im Client-`decryptRecord` toleriert wird (non-envelope strings werden durchgereicht). | +| F4-fu (kontextDoc) | `3df739190` | F4-Symmetrie: `bootstrapSpaceSingletons(spaceId, ownerUserId, syncSql)` in `bootstrap-singletons.ts`, schreibt einen leeren `kontext/kontextDoc` row pro Space-Erstellung in `mana_sync.sync_changes` mit `client_id='system:bootstrap'`, `origin='system'`. Zwei Aufruf-Sites: `databaseHooks.user.create.after` (Personal-Space; nur wenn `createPersonalSpaceFor` `created: true` zurückgibt) + `organizationHooks.afterCreateOrganization` (alle non-personal Spaces). `createBetterAuth` kriegt `syncDatabaseUrl` als zweites Argument; lazy module-scoped postgres-pool. Webapp `kontextStore.ensureDoc()` zu privat `getOrCreateLocalDoc()` umbenannt — Public-API ist nur noch `setContent` + `appendContent`. Kontext content bleibt encrypted at rest auf dem Client (`kontextDoc.content`); der Server-Bootstrap schreibt `''` plaintext, was im Client-`decryptRecord` toleriert wird (non-envelope strings werden durchgereicht). | +| F3-fu (v55 cleanup) | _pending_ | Dexie v55 löscht den Orphan-`updatedAt`-Wert aus jedem Row in `Object.keys(TABLE_TO_APP)`. v53 hatte ihn bewusst stehengelassen (Comment "next-version upgrade can drop it"); nach F3 liest niemand mehr `row.updatedAt` — `deriveUpdatedAt(local)` aus `__fieldMeta` ist die SSOT. Idempotent (delete missing field = no-op), best-effort (try/catch pro Tabelle für unbekannte/missing stores). | ## F1 — Implementation Notes