The F4 server-side singleton bootstrap was fire-and-forget at signup
time — a transient mana_sync outage during registration would leave the
user with no singleton and only the in-store `getOrCreateLocalDoc()`
fallback to race on the first write. The signup-hook is still the
happy-path zero-latency bootstrap; this commit adds a deliberate
reconciliation path that converges on every boot.
- Idempotent `bootstrapUserSingletons` / `bootstrapSpaceSingletons`:
both functions now existence-check sync_changes before INSERT and
return boolean (true=inserted, false=skipped).
- New endpoint `POST /api/v1/me/bootstrap-singletons` — JWT-gated under
the existing `/api/v1/me/*` prefix. Provisions the caller's
userContext and the kontextDoc for every Space they're a member of.
Returns `{ ok, bootstrapped: { userContext, spaces: { id: bool } } }`.
- Webapp `(app)/+layout.svelte` calls the endpoint once per
authenticated boot, after `restoreClientIdFromDexie()` and before
`createUnifiedSync.startAll()`. Best-effort; failures swallow into a
console warning and the in-store fallback still covers the rare
race window.
Plan: docs/plans/sync-field-meta-overhaul.md (F4-robust row).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Views import '../queries.svelte' (not '../queries') so module
resolution finds the renamed file.
- DetailView's filter callbacks need an explicit string param-type
under the stricter implicit-any check — myReactions is string[].
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Svelte 5 runes only work in .svelte / .svelte.ts files; the .ts
extension caused a server-side ReferenceError on /community SSR
because the runtime ships no $state symbol there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
Symmetrically extends the F4 server-side singleton bootstrap to the
per-Space `kontextDoc`. Every Space-creation — Personal at signup and
brand/club/family/team/practice via the org plugin — now writes an empty
kontextDoc row straight into mana_sync.sync_changes with origin='system',
client_id='system:bootstrap'. Fresh clients pull the row instead of
racing on a local insert that the next pull would clobber.
- New `bootstrapSpaceSingletons(spaceId, ownerUserId, syncSql)` in
services/mana-auth/src/services/bootstrap-singletons.ts; shared
`buildFieldMeta` helper extracted.
- `createBetterAuth(databaseUrl, syncDatabaseUrl, webauthn)` now takes
the sync-DB URL and lazy-creates a module-scoped postgres pool for
the bootstrap inserts.
- Hook into `databaseHooks.user.create.after` (only on `created: true`
from createPersonalSpaceFor) and `organizationHooks.afterCreateOrganization`.
- Webapp `kontextStore.ensureDoc()` made private as `getOrCreateLocalDoc()` —
same fallback role as userContextStore's after F5. Public API is now just
setContent + appendContent.
Plan: docs/plans/sync-field-meta-overhaul.md (F4-fu row in Shipping Log).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
apps/mana/CLAUDE.md:
- Data-flow diagram updated: __fieldMeta + _updatedAtIndex + origin
replace the older __fieldTimestamps / __fieldActors / __lastActor trio.
- New "Conflict-Detection" sub-section in §Data Layer summarizes the
four moving parts (origin-gating, derived updatedAt, server-side
singleton bootstrap, stable client_id) with a "use this" cheatsheet
for the patterns you'll reach for when writing new module code.
DATA_LAYER_AUDIT.md:
- Eckdaten line points at v53/v54 instead of "v9 added updatedAt
indexes". Conflict-Resolution bullet says "Origin-gated Field-Level
LWW via __fieldMeta" (was: __fieldTimestamps).
- New "Sync Field-Meta Overhaul (2026-04-26, F1-F7 SHIPPED)" sub-section
with one paragraph per phase + commit hash + the four bug-roots that
were closed.
- Punkt 15 (Conflict-Visualisierung) flipped from "🟢 Backlog" to "✅
Sprint 4+ Backlog C shipped, F2 origin-gated the trigger so only
real user edits surface".
Future sessions reading the repo cold get the post-overhaul architecture
from these two files instead of having to chase the plan + commit log.
Closes Punkt 10 of the F1-F7 follow-up audit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two one-shot bootstraps left a per-user flag in localStorage so they
wouldn't run twice — and after F7 deleted the helpers themselves
(2a8e8ff98), the flags pointed at code that no longer existed:
mana.profile.silentTwinRepair.<userId>
mana.profile.avatarMigration.<userId>
New \`cleanupOrphanMigrationFlags()\` runs once per page load from the
(app) layout's onMount, right after \`restoreClientIdFromDexie()\`.
Cheap (single localStorage scan), idempotent (no-op once swept),
silent on private-mode / quota errors. The known-orphan prefix list
lives in the helper file with deletion-commit refs so it's clear
when each entry can be retired.
Future migration deletions: append the prefix to ORPHAN_KEY_PREFIXES
in the same commit that drops the helper, and the next page load
on every device cleans up.
Closes Punkt 8 of the F1-F7 follow-up audit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final follow-up to drop the type-bypass patterns from F3's codemod.
Mit \`Partial<LocalX>\` als Deklaration akzeptiert Dexie's UpdateSpec
ohne weiteren Cast — die kombinierte \`as Record<string,unknown>\` +
\`as never\` Konstruktion wird durch eine einzige saubere
Typ-Annotation ersetzt.
Touched stores (12 Files):
wardrobe/stores/{garments,outfits}, invoices/stores/invoices,
sleep/stores/sleep, library/stores/entries,
comic/stores/{characters,stories},
profile/stores/me-images, recipes/stores/recipes,
broadcast/stores/campaigns, writing/stores/{styles,drafts}
Plus inline literal-object patterns (\`{ lines, totals } as Record\`,
\`{ content } as Record\`, \`{ audience } as Record\`,
\`{ ...spread } as Record\` im comic appendPanel).
Verbleibende \`as Record<string, unknown>\` Vorkommen sind legitime
Reads von typed-data und nicht das F3-Pattern.
7670 svelte-check Files, 0 Errors, 0 Warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds list_view, detail_view, page, links_route, analytics_route,
settings_route, tags_route sub-namespaces across all 5 locales.
Component patches in follow-up commit (split to land safely with
parallel sessions in this repo committing).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mana-web SSR + browser need the analytics URL so the inline
FeedbackHook + /community page can talk to the new public-feedback
endpoints. SSR uses the internal docker hostname; browser uses the
public subdomain.
Note: analytics.mana.how DNS + Caddy reverse-proxy block must be
provisioned separately on the Mac Mini before browser-side calls
work — TODO in deploy-followup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to a68933bff. Multi-Terminal commits hatten meinen ersten
Cleanup teilweise verschluckt — dieser Commit räumt die übrig
gebliebenen 22 \`update(id, X as never)\` Casts in den 13 Stores
zusammen mit den letzten \`as Record<string, unknown>\` Argumenten
für \`encryptRecord\` weg. Public API der Stores unverändert,
\`Partial<LocalX>\` reicht für Dexie's UpdateSpec ohne Cast.
7670 svelte-check Files, 0 Errors, 0 Warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Patches ListView, AssessmentWizard, ReminderManager, RoutineCreator,
SessionHistory, SessionPlayer, plus the /stretch route page title.
Locale JSONs landed in 421663ba3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Locale files only — component patches land in a follow-up commit.
Splitting the work to land translations safely while parallel sessions
in this repo are committing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cleanup-Schuld aus F3 (sync-field-meta-overhaul). Der Codemod hatte
\`Record<string, unknown>\`-Deklarationen via \`as never\` durch
Dexie's strikten \`UpdateSpec<LocalX>\` durchgemogelt. Jetzt sauber:
jeder Store deklariert \`const wrapped: Partial<LocalX> = { ...patch }\`
und Dexie akzeptiert das ohne Cast.
Touched stores (13 Files, ~24 update-sites):
comic/stores/{stories,characters}, comic/views/DetailView
wardrobe/stores/{garments,outfits}
invoices/stores/invoices
sleep/stores/sleep
library/stores/entries
profile/stores/me-images
recipes/stores/recipes
broadcast/stores/campaigns
writing/stores/{styles,drafts}
\`encryptRecord\` ist generic (\`<T extends object>\`) und akzeptiert
Partial<LocalX> direkt — der äußere \`as Record<string, unknown>\`
Cast ist auch weg.
Übrig bleibende \`as Record<string, unknown>\`-Vorkommen in
{invoices,broadcast}/stores/settings + profile/user-context sind
legitime Reads von nested-data, nicht das F3-Pattern.
7670 svelte-check Files, 0 Errors, 0 Warnings. 29/29 sync.test.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds wardrobe namespace (de/en/es/fr/it) covering ListView,
GridView, OutfitsView, DetailGarmentView, DetailOutfitView,
GarmentForm, OutfitComposer, GarmentTryOnButton, TryOnButton,
TryOnModelPicker, CategoryTabs, GarmentCard, OutfitCard, plus
the /wardrobe/compose route. Categories/occasions/seasons routed
through dynamic `wardrobe.categories.{key}` lookups so constants.ts
keeps the order-tuples without leaking DE labels into UI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The two one-shot bootstraps that were the structural source of three
of the four pre-F1 conflict-toasts have been obsolete since F2 +
shipped:
- F2 now stamps `origin: 'migration'` on Repair-Migration writes via
the system actor wrapper, so even if these helpers ran they would
not surface as conflict toasts on other devices anymore.
- F3 took `updatedAt` out of the wire entirely, removing the field
the helpers used to bump explicitly (the only reason their writes
showed up in someone else's pull as a conflict).
Files removed:
- apps/mana/apps/web/src/lib/modules/profile/migration/repair-silent-twin.ts
- apps/mana/apps/web/src/lib/modules/profile/migration/legacy-avatar.ts
- (empty) migration/ directory
Callers cleaned up:
- profile/MeImagesView.svelte — onMount block + imports gone.
- wardrobe/ListView.svelte — same; `onMount` import dropped (unused).
The original silent-twin bug was already fixed in M2.5 via
`setPrimary` no longer creating a "silent twin" — the repair helper
existed only to clean up rows produced by the buggy code before the
fix shipped. Pre-live, with no production data, no users hold rows
in that broken state, so the cleanup is safe.
Plan: docs/plans/sync-field-meta-overhaul.md F7.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the client_id-inflation bug where every localStorage wipe
spun up a fresh sync identity. Five distinct client_ids accumulated
in mana_sync.sync_changes for a single physical browser over five
days — every wipe made the device's own historical writes look like
"another session" on replay.
Architecture:
- New Dexie v54 table `_clientIdentity` (single row keyed by
`id='self'`) is the canonical source of the client id.
- `restoreClientIdFromDexie()` runs once at app boot, before
`createUnifiedSync`. Reconciles Dexie ↔ localStorage in three
scenarios: Dexie has it (restore localStorage), only localStorage
has it (canonicalise into Dexie), neither has it (mint + write
both). Dexie wins on disagreement.
- `getOrCreateClientId()` keeps reading from localStorage
synchronously — that's the hot path inside push/pull. The async
reconciliation just makes sure localStorage has the right value
by the time sync starts.
Survives: clear-site-data, incognito flush, Settings → "delete
browser cache". Does not survive: full IndexedDB reset (intentional
— that's a real device reset).
Plan: docs/plans/sync-field-meta-overhaul.md F6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes the on-mount `void userContextStore.ensureDoc()` race from
ContextOverview / ContextInterview / ContextFreeform. After F4 the
server creates the singleton at /register time; the first sync pull
lands it before the UI can race.
The internal logic survives as `getOrCreateLocalDoc()` — a private
fallback for the brand-new client whose pull hasn't caught up yet.
First user mutation (setField, setFreeform, …) inserts an empty
local doc with origin='user' on the field-meta map. The F2
conflict-gate then makes sure the server's origin='system' bootstrap
row never silently overwrites the user's local edits — they land in
the conflict toast like a real edit-race would.
`kontextStore.ensureDoc()` is intentionally kept (per-Space, not
per-user; F4 didn't bootstrap it). Its removal will follow once
Space-creation gains its own bootstrap hook.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes `updatedAt` from the wire protocol and from every Local-prefixed
record type. Replaced by two orthogonal mechanisms — deriveUpdatedAt()
for read-side public-facing values, _updatedAtIndex shadow for indexed
sorts.
Local-side:
- New `_updatedAtIndex` shadow column. Stamped by the Dexie creating /
updating hook on every write. Stripped from the pending-change payload
so it never travels to mana-sync. Indexed in Dexie v53 on the 22 tables
that previously indexed `updatedAt`.
- `deriveUpdatedAt(record)` in sync.ts returns max(__fieldMeta[*].at) so
the public-facing Task / Note / etc. shape keeps an `updatedAt: string`
property without holding it as data.
- Type-converters across ~60 module/queries.ts and types.ts files now
call `deriveUpdatedAt(local)` instead of reading `local.updatedAt`.
Module-store sweep:
- Regex codemod removed `updatedAt: new Date().toISOString()` /
`: now` / `: now()` / `: nowIso()` stamping from 121 store files
(~382 call sites total). Single-property update calls
(`{ updatedAt: now }`) collapsed to `{}`; touch-only patterns
(writing/drafts, writing/generations) kept the call as a no-op
because the hook now stamps `_updatedAtIndex` automatically on
any Dexie modification.
- Local* interfaces stripped of `updatedAt: string` (43 types.ts files).
Public-facing types (Task, Note, Mission, Agent, …) keep
`updatedAt: string` as a computed read-side property.
- Companion's chat conversation now sorts on a real
`lastMessageAt` data field instead of touching `updatedAt`.
- Session-only stores (times/session-alarms, session-countdown-timers)
stamp `updatedAt: now` directly because they're not in Dexie and
have no field-meta layer to derive from.
Sync engine:
- applyServerChanges sets `_updatedAtIndex` itself when applying
server changes (max of server-field times for updates, recordTime
for inserts) so server-replays land orderable.
- Dropped the legacy `localUpdatedAt` fallback — every record now has
`__fieldMeta`, the per-field at is the canonical source.
- Soft-delete tombstone path stops stamping `updatedAt: serverTime`,
uses `_updatedAtIndex` instead.
Server-side:
- mana-ai iteration-writer no longer emits `updatedAt` in
sync_changes.data; receivers derive it from the field-meta map.
- mana-sync types: no change (the wire format already uses
`field_meta` / `at` from F1).
Out of scope: backend Drizzle schemas (mana-credits, mana-events, …)
keep their `updated_at` columns. Those are pure server-internal — not
part of the sync_changes / __fieldMeta mechanism F3 cleans up.
Tests + checks:
- 0 svelte-check errors over 7652 files.
- 29/29 sync.test.ts (vitest).
- 61 mana-ai bun tests.
- mana-sync go test ./... cached green.
Plan: docs/plans/sync-field-meta-overhaul.md F3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lasts war im Workbench-Add-Page-Picker nicht findbar — mein M1-Commit
setzte nur den MANA_APPS-Eintrag in shared-branding (für AppSlider/
Launcher), aber NICHT den parallelen registerApp-Eintrag im web-
internen \$lib/app-registry/apps.ts (für Workbench-Scenes, DnD,
Detail-Routes).
- firsts: name "Firsts" → "Erste Male"
- lasts: NEUER registerApp-Block mit name "Letzte Male", Hourglass
icon, color #6366f1, contextMenuActions "Neues letztes Mal",
collection 'lasts', paramKey 'lastId', dragType 'last',
createItem ruft lastsStore.createSuspected.
Workbench-Picker filtert nach name — die DE-Namen tauchen jetzt
direkt in der Suche auf.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
"Lasts" auf Deutsch ist ein Homophon zu "die Last" (Bürde/Belastung).
Ein deutscher Muttersprachler las "Last nicht gefunden" als "Bürde
nicht gefunden". Falsches Gefühl für ein kontemplatives Modul.
Renames:
- mana-apps.ts: name "Lasts" → "Letzte Male", "Firsts" → "Erste Male"
- lasts/de.json: app.title + Singular-Bezüge weg von "Last" auf
"Letztes Mal" (detail.routeTitle, banner.recognition) bzw.
"Eintrag" (detail.notFound, settings.testSampleTitle, …)
- milestones/de.json: tabs.first/last + recap.topFirstsLabel/topLastsLabel
switchen auf "Erste Male" / "Letzte Male"
- store error: "Aufgehobene Lasts ..." → "Aufgehobene Einträge ..."
Andere Locales (en/es/fr/it) bleiben unangetastet — dort ist "Lasts"
und "Firsts" linguistisch unproblematisch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Onboarding wird zur 4-Step-Card im Workbench-Look und schließt mit einer
Freitext-Frage, die als @mana/feedback-Record landet.
UI-Redesign:
- Wraps die Screens in einer zentrierten Card mit ModuleShell-Chrome
(paper texture, soft border, 1.25rem radius, dual shadow). Liest sich
wie eine Workbench-Page statt eines flat Takeover-Screens.
- Header weg. Globaler Skip-Button sitzt unten links, Step-Dots zentriert
unten — drei-Spalten-Grid hält Dots perfekt zentriert egal wie breit
der Skip-Button ist.
- Per-Screen-Skip-Buttons aus name/ und templates/ entfernt — eine
einzige Skip-Affordance reicht.
Wish-Step (neu, Step 4):
- /onboarding/wish: Freitext-Textarea (max 2000), Aktivierungstext
("Eine letzte Sache — was wünschst du dir von Mana?"). Submit postet
fail-soft an feedbackService.createFeedback({ category:
'onboarding-wish', isPublic: false }) — Server-Down blockiert das
Onboarding nicht.
- onboarding-flow Store um pendingWish erweitert (Back-Nav-Preserve).
- Layout: 3 → 4 Step-Dots, Path-Mapping erweitert.
- markComplete + reset wandert von templates' Fertig-Handler in den
wish-Screen; templates' Button heißt jetzt "Weiter" und routet zu
/onboarding/wish.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the false-positive conflict-toast loop on history-replay. Conflict
notifications now fire only when the local field meta records origin='user'
AND the pull is not an initial hydration round.
Origin source-of-truth:
- shared-ai/field-meta.ts → originFromActor(actor) maps actor.kind onto
the FieldOrigin enum: user→'user', ai→'agent', system+SYSTEM_MIGRATION
→'migration', any other system source→'system'.
- Dexie creating/updating hooks call it once per write so every persisted
field carries the right pipeline tag.
- repair-silent-twin + legacy-avatar wrap their writes in
runAsAsync(makeSystemActor(SYSTEM_MIGRATION, ...)) so the hook stamps
origin='migration'. Future replays of those rows from another device
will not surface as conflicts.
applyServerChanges options:
- New ApplyServerChangesOptions { isInitialHydration?: boolean }.
- Push-response and pull-paged-loop callers compute it from the cursor
state (`!oldestCursor` / `!cursor`). Pagination resets the flag after
the first page.
- Conflict-trigger gates on `!options.isInitialHydration && localMeta[k]
?.origin === 'user'` in addition to the prior tests.
Tests (sync.test.ts):
- New: replay-burst (10 sequential server updates → 0 conflicts)
- New: agent-origin local write + server overwrite → 0 conflicts
- New: isInitialHydration suppresses everything → 0 conflicts
- New: real user edit + server overwrite → 1 conflict
- All 25 prior tests still pass.
29/29 vitest sync.test.ts cases green; svelte-check 0 errors over 7647
files.
Plan: docs/plans/sync-field-meta-overhaul.md F2 done-criteria met.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ContextOverview ("Freundebuch" profile cards) was the single biggest
hardcoded-string hot-spot at 35 strings — every user sees this on their
profile. Extended `profile.context.*` namespace with section titles,
field labels (routine/social/leisure), placeholders, weekday short
names, and empty-state hints across DE/EN/ES/FR/IT.
Bonus: ratchet i18n-hardcoded baseline from 1879 → 1817 (settings
namespace + ContextOverview together cleared 62 violations).
- validate:i18n-parity: 39 namespaces × 5 locales — 3381 keys aligned
- svelte-check: 7647 files, 0 errors
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All 5 milestones landed today in one continuous session: registry,
health cache, fallback router, observability, and consumer migration.
115 service-side tests, validator covers 2538 files.
Final milestone of docs/plans/llm-fallback-aliases.md. Every backend
caller now requests models via the `mana/<class>` alias system instead
of hardcoded `ollama/...` strings. mana-llm resolves aliases through
`services/mana-llm/aliases.yaml` with health-aware fallback (M3) and
emits resolved-model + fallback metrics (M4).
SSOT moved to `packages/shared-ai/src/llm-aliases.ts` so apps/api,
apps/mana/apps/web, and services/mana-ai all import the same
`MANA_LLM` constant via the existing `@mana/shared-ai` workspace
dependency. Three additional sites (memoro-server, mana-events,
mana-research) inline the alias string with a SSOT comment because
they don't pull @mana/shared-ai today.
Migrated 14 sites across 10 files:
- apps/api: writing(LONG_FORM), comic(STRUCTURED), context(FAST_TEXT),
food(VISION), plants(VISION), research orchestrator (3 tiers
collapsed to STRUCTURED+FAST_TEXT/LONG_FORM)
- apps/mana/apps/web: voice/parse-task + parse-habit (STRUCTURED)
- services/mana-ai: planner llm-client + tick.ts (REASONING)
- services/mana-events: website-extractor (STRUCTURED, inlined)
- services/mana-research: mana-llm client (FAST_TEXT, inlined)
- apps/memoro/apps/server: ai.ts (FAST_TEXT, inlined)
Legacy env-vars removed: WRITING_MODEL, COMIC_STORYBOARD_MODEL,
VISION_MODEL, MANA_LLM_DEFAULT_MODEL. The chain in aliases.yaml is
now the single tuning surface; SIGHUP reloads it without redeploys.
New `scripts/validate-llm-strings.mjs` regex-scans 2538 files for
hardcoded `<provider>/<model>` strings and fails the build if any
land outside the SSOT or the explicitly-allowed paths (image-gen
modules, model-inspector code, this validator itself, the registry).
Wired into `validate:all` next to the i18n + theme validators.
Verified: `pnpm validate:llm-strings` clean, `pnpm --filter @mana/api
type-check` clean, `pnpm --filter @mana/ai-service type-check`
clean. Web type-check has 2 pre-existing errors in
SettingsSidebar.svelte (i18n MessageFormatter type drift, last
touched in 988c17a67 — unrelated to this work).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der me-image Upload-Endpunkt wird von Wardrobe (face-banner),
Picture (reference-picker), Comic (face-banner) und der profile-
Detail-View geteilt. Bisher: wenn `authStore.getValidToken()` null
zurückgab, ging die Anfrage trotzdem ohne `Authorization`-Header
raus und der Server antwortete mit dem rohen Auth-Middleware-String
"Missing authorization header" — keine Hinweis darauf was der
Nutzer tun soll. Symptom war auch über Module hinweg verschieden:
Wardrobe-Nutzer sah's nie weil sein Token frisch war, Comic-Nutzer
mit ablaufendem Token sah's beim ersten Upload.
Zwei Härtungen in `uploadMeImageFile`:
1. Pre-flight Check — wenn `getValidToken()` null liefert, throw
sofort mit Klartext-Anweisung "Du bist nicht eingeloggt — bitte
aktualisiere die Seite und logge dich neu ein". Spart einen
Server-Roundtrip und gibt actionable feedback.
2. 401 nach getToken-Erfolg — Token war zwar lokal "valid" aber
serverseitig abgelaufen/invalidiert. Statt den Server-String
durchzureichen, eigene "Session abgelaufen — bitte
aktualisieren"-Meldung.
Alle Banner-UIs (Wardrobe + Comic) catchen den Fehler bereits in
`handleFaceUpload` und zeigen ihn im Banner-Error-Bereich, also
fließt die neue Meldung 1:1 durch ohne UI-Änderung.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-Feedback: in Wardrobe konnte man das Gesichtsbild direkt aus
der Workbench-Card hochladen, in Comic verwies der Hint nur auf
/profile/me-images. Asymmetrie geheilt — beide Module nutzen jetzt
das gleiche Banner-Pattern.
Comic-ListView (Modul-Root, oberhalb der Tabs):
- Wardrobe-Banner verbatim übernommen (MeImageUploadZone +
3-Phasen-State-Machine idle/uploading/success + 2.5s
success-card mit fade-out + dismissable + spinner-overlay
während upload + error-card auf Fehler).
- Sitzt oberhalb der Tabs, damit es für BEIDE Sub-Views
(Stories | Characters) sichtbar ist — Comic-Panel UND
Charakter-Generierung brauchen das Face-Ref. Banner blendet
sich automatisch aus sobald face$ via liveQuery flippt + die
2.5s success-Window vorbei ist.
- Copy angepasst: "Wir brauchen dich auf Bild, damit Comic-Panels
und Charakter-Varianten von dir gerendert werden können"
statt Wardrobe's "Try-On Kleidung an dir visualisieren".
Success-CTA: "als nächstes baust du deinen ersten Comic-
Character oder legst direkt eine Story an".
Sub-Views aufgeräumt:
- views/ListView.svelte (StoriesView): hat den redundanten
"Lade erst dein Gesichtsbild"-Hint inkl. UserCircle-Import +
useImageByPrimary-Hook gehabt → entfernt. Modul-Root liefert
das jetzt.
- views/CharactersView.svelte: gleicher Cleanup. Imports von
UserCircle und useImageByPrimary raus.
Repair-Hook (`repairSilentTwinAvatarRows`) bewusst NICHT
kopiert — das war eine Wardrobe-spezifische Migration für die
M2.5-silent-twin-Bug; Comic ist nach v40 entstanden, hat das
Problem nie gehabt.
Comic-Files type-checken sauber.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mehrere Store-Methoden lasen die verschlüsselten Felder eines frisch
aus Dexie geholten Records direkt — references/title/briefing/content
landen aber als Ciphertext-String und nicht als Array/Objekt im Speicher.
Auswirkungen die jetzt behoben sind:
- startDraftGeneration: 'refs.map is not a function' beim ersten Klick
auf "Generieren" (draft.references war Ciphertext)
- refineSelection: Crash beim Lesen von draft.briefing.language
- applyRefinement: Slice-Konkatenation auf Ciphertext (korrumpiert die
Version still beim ersten Selection-Refinement)
- updateBriefing: Spread-merge eines Ciphertext-Strings in den Patch
- createCheckpointVersion: kopiert die Ciphertext-Bytes als neue
Version-Content statt des Plaintexts
Fix: decryptRecord() direkt nach jedem .get() der relevante encrypted
Felder liest. queries.ts war schon korrekt (decryptRecords im liveQuery-
Pfad), aber die Mutation-Pfade haben das übersprungen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the two remaining gaps after the seeding-cleanup landed:
- `data/space-stamping.test.ts` exercises the smart-hook contract
end-to-end against fake-indexeddb. Four scenarios: active Brand
Space → row carries Brand UUID; no active Space → personal sentinel;
explicit spaceId on the record is preserved verbatim; flipping the
active Space between writes flips the stamp. The Brand-Space case
is the regression guard for the original bug (writes silently
routing to Personal after `reconcileSentinels`).
- `apps/mana/CLAUDE.md` gets a Per-Space Seeds section so the next
module dev who needs to pre-populate something on Space activation
finds the `registerSpaceSeed` pattern + `data/seeds/index.ts` barrel
+ the deterministic-id discipline without grepping the codebase.
Reference impl link points at workbench-home.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `dedupHomeScenesOn` helper in `data/scope/dedup-workbench-scenes.ts`
existed only to be called once from the v48 Dexie upgrade — outside of
that single usage it was dead code. Inlining the logic directly into
the upgrade callback eliminates a 120-line module + a 220-line test
file (343 lines net) without changing behaviour: the v48 upgrade still
collapses uncustomised "Home" duplicates per (spaceId, name='Home'),
merges openApps, and soft-deletes losers.
Drive-by tightening:
- `seedWorkbenchHomeOn` returns `Promise<void>` instead of
`Promise<boolean>`. The boolean was only consumed by the
post-`reconcileSentinels` dedup pass that already got removed; the
current callers (registry seeder + tests) don't read it. Less
signature surface, fewer assertions in tests.
- `data/scope/per-space-seeds.ts` comment header drops the
plan-internal "Schicht B + C" reference for a plain link to the
cleanup plan. Code-level vocabulary now reads cleanly without the
rollout-sequencing context.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>