Commit graph

3644 commits

Author SHA1 Message Date
Till JS
c9a091e696 fix(docker): COPY packages/shared-privacy into sveltekit-base
mana-web depends on @mana/shared-privacy (visibility-system) but it was
missing from the base-image COPY list, causing pnpm install to fail
inside the build container with ERR_PNPM_WORKSPACE_PKG_NOT_FOUND.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 01:02:55 +02:00
Till JS
119cd2cf83 chore(boot): sweep orphan migration flags from localStorage
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>
2026-04-27 01:01:35 +02:00
Till JS
cabfd1004d feat(infra): community.mana.how (instead of analytics.*) for the public-feedback hub
analytics.mana.how DNS already existed as a non-CNAME record — picking
the user-facing 'community.mana.how' subdomain instead. Added the
tunnel ingress + matched the CORS origin + client-side env var.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 01:00:22 +02:00
Till JS
515de79c8b refactor(stores): replace Record<string,unknown> declarations with Partial<LocalX>
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>
2026-04-27 00:58:01 +02:00
Till JS
b4589a7249 feat(infra): route analytics.mana.how to mana-analytics:3064
Phase 2 feedback hub needs a public hostname so the browser-side
FeedbackHook + /community page can talk to mana-analytics. Internal
docker URL stays for SSR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 00:57:05 +02:00
Till JS
812f3f7fa0 i18n(uload): extend namespace JSONs for routes/views
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>
2026-04-27 00:55:48 +02:00
Till JS
3673542f82 feat(mana-web): inject PUBLIC_MANA_ANALYTICS_URL for client-side feedback calls
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>
2026-04-27 00:55:40 +02:00
Till JS
1334bd3626 refactor(stores): drop remaining \as never\ + \as Record<string,unknown>\ casts
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>
2026-04-27 00:53:41 +02:00
Till JS
5387681936 fix(mana-analytics): copy shared-logger into installer (transitive workspace dep)
shared-hono depends on @mana/shared-logger; without it, the bun runtime
crashes on first import with ENOENT for the workspace symlink target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 00:52:33 +02:00
Till JS
db01ddccc7 fix(mana-analytics): multi-stage Dockerfile that resolves workspace deps
Switches the build context to repo-root so the pnpm-workspace install
can pull in @mana/shared-hono. Mirrors the mana-auth/mana-ai pattern
(node+pnpm installer stage → bun runtime stage).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 00:50:25 +02:00
Till JS
722fe74ced i18n(stretch): wire components to namespace — 34 strings cleared
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>
2026-04-27 00:49:23 +02:00
Till JS
421663ba3d i18n(stretch): add namespace JSONs for de/en/es/fr/it
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>
2026-04-27 00:30:17 +02:00
Till JS
a68933bff3 refactor(stores): drop \as never\ Dexie-update casts
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>
2026-04-27 00:29:51 +02:00
Till JS
576fe79269 fix(community): clear svelte-check warnings for fail-on-warnings push gate
Reactive-state-locally + a11y-noninteractive-tabindex/element warnings.
No behavior changes — props stay reactive via destructure-from-$props
pattern, ignore-comments document intentional a11y exceptions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 00:25:35 +02:00
Till JS
39ef7ca9d5 chore(compose): wire FEEDBACK_PSEUDONYM_SECRET into mana-analytics
Required by the public-community-hub stamping. Compose enforces the
var via :? syntax — startup fails fast if .env.macmini is missing it,
which beats silently using the dev default in production.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 00:18:16 +02:00
Till JS
5959f66387 i18n(wardrobe): translate all 5 locales — 36 strings
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>
2026-04-27 00:12:18 +02:00
Till JS
8804a20a7f feat(community): public anon hub — module + inline + admin + onboarding
Macht @mana/feedback omnipräsent + öffentlich. Phase 2 vom
Public-Community-Hub-Plan (docs/plans/feedback-hub-public.md).

Inline-Touchpoints:
- FeedbackHook: Lightbulb-Button, opens FeedbackQuickModal vorausgefüllt
  mit module-context. Auto-injected in jeder ModuleShell-Header
  (window-actions row), opt-out via hideFeedback prop.
- GlobalFeedbackPill: Floating "Idee?"-Pill bottom-right, self-hides
  auf /onboarding, /feedback, /community, und für Gäste. Auto-detected
  module-context aus URL bzw. ?app=-Param.
- FeedbackQuickModal: 3-Klick-Submit mit Category-Dropdown, Public-
  Toggle, "Sichtbar als {Pseudonym}"-Confirm-State.

Community-Modul (eigenes Modul, in Workbench drop-bar):
- module.config.ts (server-only, keine Sync-Tabellen)
- queries.ts: useCommunityFeed + useCommunityItem mit auth-aware Switch
  zwischen public + auth-enriched Endpoints
- ListView/DetailView/RoadmapView mit ItemCard-Component
- App-Registry-Eintrag (Megaphone-Icon, #F59E0B)

Public-Mirror-Routes (kein AuthGate):
- /community            — Feed mit SSR-Pre-Render via Public-Endpoint
- /community/[id]       — Single item + replies, SSR
- /community/roadmap    — Kanban Submitted/Planned/InProgress/Completed
- /community/admin      — Founder-only Triage (Status, AdminResponse,
                         visibility-Toggle); Client-side role-gate
                         redirect → /community.
SEO: <svelte:head> mit title/description, <noscript>-Fallback,
Cache-Headers stale-while-revalidate.

API:
- web's lib/api/feedback.ts pointed an die echte mana-analytics-URL
  (3064 dev) statt mana-auth. Neuer publicFeedbackService für
  unauthenticated SSR.
- getManaAnalyticsUrl() in lib/api/config.ts.

Onboarding-Wish public-by-default:
- Disclosure-Text: "Erscheint in Community-Page als Tier-Pseudonym".
- Toggle "Öffentlich teilen" / "Nur für Admins" mit Default on.
- Submitted-Confirm zeigt das generierte Display-Name.

Plan-Doc-Updates:
- feedback-hub.md Phase 2 abgespeckt → Verweis auf feedback-hub-public.md
- feedback-hub-public.md komplett: Architektur-Optionen A-E, Phase 2.x,
  Phase 3 Roadmap (16 Future-Features), Risiken.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 00:02:25 +02:00
Till JS
c9b122076a feat(feedback): public feed types + ReactionBar + service split
@mana/feedback wird zur Pflege-SSOT für Public-Community-Hub.

- PublicFeedbackItem-Typ: anonymisiertes Item, das nur display_name +
  reactions + status führt — kein userId, displayHash, deviceInfo.
- ReactionEmoji ('👍' '❤️' '🚀' '🤔' '🎉') + REACTION_LABELS mit DE-Labels.
- CreateFeedbackInput erweitert um moduleContext + parentId. Reactions
  + score auf Feedback-Type optional gemacht.
- Service-Split:
  createFeedbackService    — auth-required Submit/React/Manage,
                            getPublicFeed (auth-enriched mit myReactions)
  createPublicFeedbackService — anonymous, SSR-only, getFeed/getItem.
  toggleReaction(emoji) statt vote/unvote (legacy-Shims bleiben für
  back-compat zu vote → '👍'-Toggle).
- ReactionBar.svelte: Slack-Style emoji-row mit Active-Highlighting für
  myReactions, ReadOnly-Mode für Public-SSR. Auto-disabled-Tooltip.
- index.ts re-exportiert die neuen Typen + ReactionBar; FeedbackVote
  rausgeschmissen (durch FeedbackReactions im Server-Schema ersetzt).

FeedbackCard + FeedbackPage minimal angepasst, damit svelte-check
clean bleibt — die Legacy-Komponenten bleiben funktional, werden aber
in Phase 3 zu @mana/feedback's neuen Modul-Views ausgemistet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 00:01:06 +02:00
Till JS
8b0a943e71 feat(mana-analytics): pseudonym + reactions + public feed + admin
Macht mana-analytics zur Backend-Heimat des Public-Community-Hubs.

- Pseudonym: createDisplayHash(userId, secret) + generateDisplayName()
  produzieren deterministisch "Wachsame Eule #4528" pro User. 100
  Adjektive × 80 Tieren × 10000 Suffixe → ~80M Kombinationen. 7 unit
  tests, 0 PII im Output.
- Schema-Erweiterung user_feedback: display_hash, display_name,
  module_context, parent_id (1-level Threading), reactions jsonb
  (cached emoji→count), score (cached weighted sort).
- feedback_votes ersetzt durch feedback_reactions (Slack-Pattern:
  unique(feedback_id, user_id, emoji), pro User mehrere Emojis möglich).
- Service: createFeedback stempelt display_hash + display_name. Neue
  Methoden getPublicFeed (redacted), getReplies, toggleReaction
  (rebuilt reactions+score). Admin-Methoden adminListAll/adminUpdate
  founder-tier-gated im Route-Layer.
- Routes:
  /api/v1/public/feedback/*  — anonymous reads (kein Auth, kein
                              userId/displayHash/deviceInfo im Output)
  /api/v1/feedback/*         — auth-required Submit/React/Manage,
                              plus :id/replies, :id/react, /admin
- Config: neuer FEEDBACK_PSEUDONYM_SECRET-Env-Var seedet die Hashes;
  Rotation re-keyt nur künftige Pseudonyme, alte Records bleiben stabil.

Migration 0002_public-community-foundation.sql idempotent, lokal +
prod (mana-server) eingespielt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 00:00:35 +02:00
Till JS
fd11481d94 docs(plans): mark sync-field-meta-overhaul F1-F7 SHIPPED
All seven phases of docs/plans/sync-field-meta-overhaul.md landed.
Final shipping log:

  F1 7766ea502  __fieldMeta replaces __fieldTimestamps trio
  F2 ad5e04a55  origin-gated conflict detection
  F3 6bb9d77be  drop updatedAt as a synced data field
  F4 c07db300b  server-side singleton bootstrap (mana-auth)
  F5 d78f57c04  drop public userContextStore.ensureDoc()
  F6 a031493fe  stable client_id in Dexie
  F7 2a8e8ff98  drop repair-silent-twin + legacy-avatar migrations

Structural outcome: the four conflict-toast root-causes diagnosed
on 2026-04-26 (updatedAt as synced field, history-replay false-
positives, ensureDoc race, localStorage-bound client_id) are all
closed. The conflict surface fires only when a real user edit
genuinely loses to a server overwrite — anything else is silent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 23:54:09 +02:00
Till JS
2a8e8ff98f feat(sync): F7 — drop repair-silent-twin + legacy-avatar migrations
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>
2026-04-26 23:52:58 +02:00
Till JS
a031493fec feat(sync): F6 — stable client_id in Dexie, localStorage as cache
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>
2026-04-26 23:50:55 +02:00
Till JS
d78f57c041 feat(sync): F5 — drop public userContextStore.ensureDoc()
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>
2026-04-26 23:47:53 +02:00
Till JS
c7d80e3423 feat(events): full i18n coverage across 12 files — DE/EN/ES/FR/IT
Events (party/RSVP) module had ~38 hardcoded German strings across
DetailView (13 incl. share row + map labels), SourceManager (5),
DiscoveredEventCard (3), DiscoveryTab (3), EventCard (3), RsvpSummary
(3), ListView (3), BringListEditor (2), GuestListEditor (2),
PublicRsvpList (2), DiscoverySetup (2), RegionPicker (1).

New `events` namespace with 119 keys × 5 locales:
- `list_view.*`, `detail_view.*` (incl. share/publish/map sections),
  `event_card.*` (status badges + summary), `discovered_card.*`,
  `discovery_tab.*`, `discovery_setup.*`, `region_picker.*`,
  `source_manager.*` (incl. errors_count + last_scan), `bring_list_editor.*`,
  `guest_list_editor.*` (RSVP options), `public_rsvp_list.*` (status
  labels + meta), `rsvp_summary.*` (yes/maybe/no/pending labels).
- SourceManager.formatDate now uses get(locale) instead of hardcoded
  'de-DE' for last-scan timestamps.

- Baseline ratchet: 1640 → 1602 (38 strings cleared)
- validate:i18n-parity: 42 namespaces × 5 locales — 4100 keys aligned
- svelte-check: no new errors

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 23:20:09 +02:00
Till JS
c07db300b0 feat(sync): F4 — server-side singleton bootstrap
Closes the userContext race-on-first-mount that surfaced as a
"10 fields overwritten" conflict toast pre-F2. Adds a fire-and-forget
hook in the /register flow that writes the per-user `userContext`
singleton straight into `mana_sync.sync_changes` with
`client_id='system:bootstrap'` and `origin='system'`.

Behavior:
- On successful `signUpEmail`, `bootstrapUserSingletons(userId, syncSql)`
  inserts a `profile/userContext` row with the empty-default shape that
  mirrors the webapp's `emptyUserContext()` factory in
  `apps/mana/apps/web/src/lib/modules/profile/types.ts`.
- The receiving client treats the change as origin='server-replay'
  on apply (per F2 conflict-gate), so no toasts on first pull.
- Failure is logged but does not abort registration — the webapp's
  existing `ensureDoc()` fallback still works during the F4→F5
  transition.

Module-scoped postgres pool (max=2 connections) lazy-initialized on
first signUp; reused for the lifetime of the process. Same pattern as
`UserDataService.getSyncSql`.

Out of scope for F4:
- `kontextDoc` is per-Space (not per-user) — bootstrap there will be
  hooked into the Space-creation flow, not /register. The webapp's
  `ensureDoc()` for kontextDoc stays as-is for now.
- Webapp `ensureDoc()` removal is F5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 23:18:54 +02:00
Till JS
6bb9d77be9 feat(sync): F3 — drop updatedAt as a synced data field
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>
2026-04-26 23:12:22 +02:00
Till JS
e2676252d3 feat(writing): full i18n coverage across 10 files — DE/EN/ES/FR/IT
Writing (Ghostwriter) module had ~47 hardcoded German strings across
BriefingForm (11), RefinementPanel (12), DetailView (7), StylesView (4),
ReferencePicker (4), ListView (3), StyleForm (2), VersionHistory (2),
ExportMenu (1), VersionEditor (1).

New `writing` namespace with 213 keys × 5 locales:
- `kinds.*` (12 draft kinds), `statuses.*`, `generation_statuses.*`,
  `tones.*` (10 presets), `style_sources.*` — Svelte components now use
  these instead of `KIND_LABELS[k].de` / `STATUS_LABELS[s].de` /
  `STYLE_SOURCE_LABELS[s].de` / `TONE_PRESETS[i].de`. Constants stay as
  static maps for non-Svelte callers (prompt builders, AI tools).
- `briefing_form.*`, `refinement_panel.*`, `selection_tools.*`,
  `detail_view.*` (incl. published-target chips, share row, version
  label, generate/checkpoint buttons, undo), `list_view.*` (hero +
  quick-start template), `styles_view.*`, `style_form.*`,
  `version_history.*` (token-usage line), `version_editor.*`,
  `export_menu.*`, `reference_picker.*` (7 source kinds).

- Baseline ratchet: 1687 → 1640 (47 strings cleared, 10 files fully clean)
- validate:i18n-parity: 41 namespaces × 5 locales — 3981 keys aligned
- svelte-check: no new errors

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:34:49 +02:00
Till JS
4e31c8d736 feat(calendar): full i18n coverage across 17 files — DE/EN/ES/FR/IT
Calendar already had a `calendar` namespace, but ~70 strings were
hardcoded across EventForm, EventDetailModal, CustomRecurrenceBuilder,
CalendarHeader (15 block-type filter chips), QuickEventPopover, AgendaView,
EventCard, SlotSuggestions, MiniCalendar, DateStrip, ListView,
SharedEventView, the inline DetailView, and 3 routes.

- Extended namespace with `event_form.*`, `event_card.*`, `event_modal.*`,
  `agenda.*`, `recurrence.*` (custom builder + preview format),
  `weekday_short.*` / `weekday_long.*`, `header.*` (15 block-type labels +
  4 ARIA), `date_strip.*`, `mini_cal.*`, `slots.*`, `quick_event.*`,
  `list_view.*`, `detail_route.*`, `detail_view.*`, `calendars_route.*`,
  `shared_view.*` — ~172 new keys × 5 locales = ~860 translations.
- Recurrence preview formatters in EventForm + EventDetailModal +
  CustomRecurrenceBuilder all rebuilt around `recurrence.every_n_unit` /
  `weekly_with_days` / weekday-short maps.
- Locale-aware Intl.DateTimeFormat in SharedEventView (was hardcoded
  'de-DE').
- Baseline ratchet: 1753 → 1687 (66 calendar strings cleared, 16 files
  fully clean).

- validate:i18n-parity: 40 namespaces × 5 locales — 3768 keys aligned
- svelte-check: 0 new errors from i18n changes (pre-existing drift
  in unrelated modules unchanged)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:17:34 +02:00
Till JS
723a64808c feat(lasts,firsts): register apps in web-internal registry mit DE-Namen
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>
2026-04-26 22:11:22 +02:00
Till JS
1398d76b41 refactor(lasts,firsts): German display names — "Letzte Male" / "Erste Male"
"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>
2026-04-26 21:58:31 +02:00
Till JS
679fb160c2 feat(invoices): full i18n coverage across 12 files — DE/EN/ES/FR/IT
Invoices module had 81 hardcoded German strings across ListView,
DetailView, InvoiceForm, SenderProfileForm, ClientPicker, LinesEditor,
SendModal, StatusBadge, the open-invoices widget, and 4 routes. New
`invoices` namespace (~215 keys × 5 locales = ~1075 translations) covers
list/detail/form/picker/sender-form/send-modal + Swiss + German VAT-rate
labels.

- constants.ts: STATUS_LABELS still kept as a literal map for non-Svelte
  callers (mail-template, PDF renderer); Svelte components now use
  `$_('invoices.status.<status>')`. VAT_RATES_CH/DE switched from
  literal `label` to `i18nKey`, resolved per-component via $_.
- Locale-aware Date.toLocaleString in DetailView meta + SenderProfileForm
  saved-at timestamp (was hardcoded 'de-DE'/default).
- Baseline ratchet: 1817 → 1753 (64 invoices strings + a handful of
  SettingsSidebar follow-ons cleared).

- validate:i18n-parity: 40 namespaces × 5 locales — 3596 keys aligned
- svelte-check: 7647 files, 0 errors

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 21:56:48 +02:00
Till JS
f10439e369 fix(mana-analytics): point migration at public.feedback_status, not feedback.*
Drizzle's pgEnum() ohne pgSchema-Wrap landet immer in public — der
schemaFilter versteckt das nur im Diff (siehe Repo-Memory:
"Drizzle enums with schemaFilter must use pgSchema().enum()"). Die
Tabelle feedback.user_feedback referenziert die Enums quer aus public,
das funktioniert; aber die ALTER-TYPE-Statements in der ursprünglichen
Migration zielten auf feedback.feedback_status / feedback.feedback_category
und hätten damit nichts gefunden.

Lokal verifiziert (mana_platform.public.feedback_status,
mana_platform.public.feedback_category):
- 6 Status-Werte umbenannt → submitted/under_review/planned/in_progress/completed/declined
- Default-Status auf 'submitted'
- Category 'onboarding-wish' hinzugefügt
- Re-Run idempotent (DO-Blöcke + ADD VALUE IF NOT EXISTS)

Mittelfristig sollte feedbackSchema.enum(...) verwendet werden, damit
Enums tatsächlich im feedback-Namespace landen — eigener Refactor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 21:55:45 +02:00
Till JS
e5cd98936f feat(onboarding): card redesign + add wish step routing to feedback hub
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>
2026-04-26 21:52:52 +02:00
Till JS
ba6274edbe refactor(feedback): align package + DB enums, plan central hub
Macht @mana/feedback zur SSOT für alle Nutzer-Feedback-Categories und
-Status — Voraussetzung dafür, dass Onboarding-Wishes, NPS, Churn-Feedback
etc. künftig dort landen.

- Status-Enum: DB-Werte umbenannt new/reviewed/done/rejected →
  submitted/under_review/completed/declined (Package gewinnt). PG≥10
  ALTER TYPE … RENAME VALUE ist non-destructive.
- Category 'praise' ins Package aufgenommen (war nur in DB).
- Category 'onboarding-wish' neu in Package + DB für den Wish-Step.
- Default status in DB: 'new' → 'submitted'.
- CreateFeedbackInput.isPublic optional → Service reicht durch, default
  bleibt true; private Categories wie onboarding-wish setzen false.
- Schema-Datei mit SSOT-Kommentar versehen, der Drift in Zukunft verhindert.

Hand-authored Migration unter services/mana-analytics/drizzle/0001_*.sql
weil drizzle-kit push Enum-Werte nicht zuverlässig umbenennt. Manuell
einspielen vor nächstem db:push:

  psql "\$DATABASE_URL" -f services/mana-analytics/drizzle/0001_align-feedback-enums.sql

Plan in docs/plans/feedback-hub.md (Phase 0–4); Phase 0 + 1 jetzt, 2-4
deferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 21:52:25 +02:00
Till JS
bf3bca268a feat(lasts): M1-M7 — module ship + Meilensteine-Aggregator
Mirror sibling to firsts: das *letzte* Mal, das du etwas getan hast —
markiert oder rückwirkend erkannt. Plan: docs/plans/lasts-module.md.

M1 Skelett — Dexie v51 lasts-Tabelle, Encryption-Registry, Per-Space-
Welcome-Seed, Empty-State ListView. Kategorien aus firsts/types.ts
nach \$lib/data/milestones/categories.ts extrahiert (Re-Exports halten
firsts-API stabil).

M2 CRUD + DetailView — StatusTabs (Vermutet/Bestätigt/Aufgehoben),
Quick-Add mit Mode-Toggle, always-editable DetailView mit Lifecycle-
Buttons (Bestätigen, Aufheben mit Inline-Note), 44 i18n-Keys × 5 Locales.

M3 Inbox + Inferenz — Dexie v52 lastsCooldown (12-Monate-Cooldown,
deterministische ID), Source-Registry-Pattern in inference/, places-
Source mit Heuristik visitCount>=5 Span>=180d Silence>=365d. InboxView
mit Akzeptieren/Verwerfen + manueller Scan. contacts/habits → M3.b
sobald jeweilige Frequenz-Felder existieren.

M4 AI-Tools — 5 Tools im AI_TOOL_CATALOG (create_last, confirm_last,
reclaim_last, list_lasts, suggest_lasts), Webapp-Executor mit Vault-
Locked-Handling. Server-Drift-Test 4/4, Schema-Test 6/6.

M5 Reminders + Settings — Pivot zu In-App-DueBanner statt OS-Push (kein
PWA-Push-System im Repo). Pure date-math (12 Vitest cases), Settings-
Store mit 4 Toggles, DueBanner mit max-N rendering, Test-Banner-Knopf.

M6 Visibility + Unlisted-Sharing — VisibilityPicker + SharedLinkControls
in DetailView, buildLastBlob mit reflective-core whitelist (reclaimed
Lasts gehärtet ausgeblockt), SharedLastView public-render, Share-
Dispatcher kennt 'lasts'.

M7 Meilensteine-Aggregator — Cross-modul firsts vereinigt mit lasts
Timeline + Year-Recap. Pure aggregator (mergeMilestones,
buildMilestonesRecap), 12 Vitest cases. /milestones und
/milestones/recap/[year] Routes, Cross-Link in lasts/ListView.

Validation: 0 errors / 0 warnings (svelte-check 7645 files), 24/24
tests, i18n-parity 39x5 aligned (+2 namespaces), i18n-keys baseline-
equal, crypto 211 tables.

LOCAL TIER PATCH: lasts ist 'guest' für Testing — vor Release auf
'beta' setzen (packages/shared-branding/src/mana-apps.ts).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 21:40:29 +02:00
Till JS
ad5e04a554 feat(sync): F2 — origin-gated conflict-detection
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>
2026-04-26 21:38:56 +02:00
Till JS
6c942e3ab2 feat(profile): translate ContextOverview into all 5 locales
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>
2026-04-26 21:37:57 +02:00
Till JS
7766ea5021 docs(plans): mark llm-fallback-aliases SHIPPED, add M-by-M commit table
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.
2026-04-26 21:27:57 +02:00
Till JS
30eb7ef72d feat(settings): full i18n coverage — DE/EN/ES/FR/IT
Settings page on the workbench was 100% hardcoded German across 14
files / ~5200 LOC. Added a `settings` namespace (~280 keys × 5 locales)
and wired every component through `$_()`.

- New apps/mana/apps/web/src/lib/i18n/locales/settings/{de,en,es,fr,it}.json
- searchIndex.ts now exports `getCategories(t)` + `searchSettings(t, q)`;
  hash-anchor lookup goes through the locale-free `findCategoryByAnchor`
- VaultSection (recovery-code wizard, ZK opt-in, key rotate) + AiSettings
  (4 tier cards), MyDataSection (DSGVO retention/danger-zone),
  SyncSection, PrivacySection translated end-to-end
- Locale-aware Date/Number formatting (toLocaleString uses get(locale))
  in SyncSection + MyDataSection
- validate:i18n-parity: 38 namespaces × 5 locales — 3323 keys aligned
- svelte-check: 7639 files, 0 errors

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 21:27:24 +02:00
Till JS
fea3adf5fe feat(llm-aliases): M5 — migrate consumers to MANA_LLM aliases
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>
2026-04-26 21:26:03 +02:00
Till JS
8a49e3ffd5 feat(mana-llm): M4 — observability, debug endpoints, SIGHUP reload
- `X-Mana-LLM-Resolved: <provider>/<model>` header on non-streaming
  responses. Streaming clients read the same info from each chunk's
  `model` field (SSE headers go out before the chain is walked).
- Three new Prometheus metrics: `mana_llm_alias_resolved_total{alias,
  target}` (which concrete model an alias resolved to per request),
  `mana_llm_fallback_total{from_model, to_model, reason}` (each
  fallback transition), `mana_llm_provider_healthy{provider}` (gauge,
  mirrors the circuit-breaker).
- New debug endpoints: `GET /v1/aliases` (registry inspection — chain
  + description per alias, useful for confirming SIGHUP reloads),
  `GET /v1/health` (full per-provider liveness snapshot — failure
  counter, last error, unhealthy-until backoff).
- `kill -HUP <pid>` reloads `aliases.yaml`. Parse errors leave the
  previous good state in memory and log the rejection.
- `ProviderHealthCache.add_listener()` for cache→metrics decoupling:
  the gauge is updated via a transition-only listener wired in main.py
  rather than the cache importing prometheus_client itself.
- Request-side metrics now use the requested model string, success-side
  uses the resolved one. So `mana_llm_llm_requests_total{provider="ollama",
  model="gemma3:12b"}` reflects actual upstream load even when callers
  used `mana/long-form` aliases.

16 new observability tests (test_m4_observability.py): listener
fire-on-transition semantics, exception-isolation, multi-listener,
counter increments, gauge writes, end-to-end alias→metric flow,
v1/aliases + v1/health endpoint shape, response.model carries the
resolved target after fallback. Total suite: 115/115 in 1.6s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:52:28 +02:00
Till JS
3046da3b19 feat(mana-llm): M3 — health-aware router with alias + chain fallback
Replaces the old Ollama→Google special-case auto-fallback with the
unified pipeline: caller passes either a direct provider/model or an
alias from the `mana/` namespace; the router resolves to a chain and
walks it skipping unhealthy providers (per ProviderHealthCache from M2),
trying each entry, marking provider unhealthy on retryable errors and
falling through to the next.

Retryable: ConnectError, ReadTimeout, RemoteProtocolError, 5xx,
ProviderRateLimitError. Propagated (don't fall back, don't poison the
cache): ProviderCapabilityError, ProviderAuthError, ProviderBlockedError,
4xx, unknown exception types. The cache stays "what the network told us
about this provider's liveness" — caller errors don't muddy that signal.

Streaming: pre-first-byte fallback only. Once a chunk has been yielded
the provider is committed; mid-stream errors propagate as-is so we
don't splice two voices into one output.

`NoHealthyProviderError` (HTTP 503) carries a structured attempt log —
each chain entry shows up as `(model, reason)` so the cause of a 503
is visible in the response and metrics, not only in service logs.

main.py wires the lifespan: aliases.yaml is loaded, ProviderHealthCache
created, ProviderRouter takes both as constructor deps, HealthProbe
spawned with cheap HTTP probes per configured provider (Ollama
/api/tags, OpenAI-compat /v1/models with Bearer header). Google is
skipped — google-genai SDK has no obvious cheap probe; the call-site
fallback handles real errors.

22 new router tests (test_router_fallback.py): chain walking, capability
& auth propagation, 5xx vs 4xx differentiation, rate-limit retry,
all-fail → NoHealthyProviderError, direct provider strings bypass
aliases, streaming pre-first-byte fallback, mid-stream-failure does
NOT fall back, empty stream commits without retry, cache feedback on
success/failure/non-retryable. Existing test_providers.py updated for
the new constructor signature; all 99 service tests green via the dev
container (Python 3.12).

Legacy purged: `_ollama_concurrent`, `_ollama_health_cache`,
`_can_fallback_to_google`, `_should_use_ollama`, `_fallback_to_google`,
`_get_ollama_health_cached` all gone. The `auto_fallback_enabled` /
`ollama_max_concurrent` settings remain in config.py for now (M5 will
remove them along with the per-feature env-var overrides).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:44:16 +02:00
Till JS
59557e62d7 feat(mana-llm): M2 — ProviderHealthCache + background probe loop
Per-provider liveness with circuit-breaker semantics. The router (M3)
will read `is_healthy()` to skip dead providers in a chain; the probe
loop and the call-site fallback handler write state via
`mark_healthy` / `mark_unhealthy`.

State machine: 1st failure stays healthy (transient blips happen);
2nd consecutive failure trips the breaker and sets a 60s backoff
window during which `is_healthy → False`. After the window the
provider is half-open again — next call exercises it, success
resets, failure re-arms.

HealthProbe is the background asyncio.Task that pings every
registered provider every 30s with a 3s timeout. Probes run
concurrently per tick and one bad probe can't sink the loop. Probe
functions are injected (`{name: async-fn}`) so this module stays
decoupled from the provider classes — the wiring lives in main.py
where we already know which providers are configured.

32 new tests (FakeClock for deterministic backoff timing, slow-probe
helpers for parallelism + timeout, lifecycle tests for start/stop
idempotency and tick-after-error survival). 64/64 alias+health tests
green.

Not yet wired into the request path — that's M3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:29:57 +02:00
Till JS
dff8629e1d feat(mana-llm): M1 — AliasRegistry + aliases.yaml SSOT
First milestone of the LLM-fallback plan (docs/plans/llm-fallback-aliases.md).
Introduces the `mana/<class>` namespace; the registry parses + validates
aliases.yaml at startup and reloads on demand. Schema-rejects empty
chains, missing provider prefixes, alias names outside the reserved
namespace, default→unknown references, etc.

Reload semantics: parse error keeps the previous good state in memory
so a typo + SIGHUP doesn't take the service down.

5 aliases ship with the initial config: fast-text, long-form, structured,
reasoning, vision. Each chain ends with a cloud provider so the system
keeps working when the GPU server is offline.

32 unit tests covering happy path, schema validation, namespace check,
reload safety, and a guard that the shipped aliases.yaml itself parses.
M2 (health-cache + probe-loop) and M3 (router fallback execution) build
on this; aliases are not yet wired into the request path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:23:51 +02:00
Till JS
e1860234d6 docs(plans): LLM-fallback via model-aliases — spec
Centralized resilience-layer in mana-llm: callers send semantic aliases
(`mana/long-form`, `mana/structured`, …), the router resolves to a
provider chain and falls back through unhealthy providers via a 30s
health-probe loop. Triggered by today's GPU-server outage that hung
the writing-generation endpoint for 75s before 500.

5 milestones, ~3 dev-days, big-bang migration (no live yet → no legacy).
All hardcoded `ollama/...` strings move into a single aliases.yaml SSOT,
new validate-llm-strings.mjs gate prevents regression.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:19:34 +02:00
Till JS
aac1e3d55c fix(profile): klarere Fehlermeldung bei nicht-authentifiziertem me-image-Upload
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>
2026-04-26 20:01:53 +02:00
Till JS
1c4486ceba fix(comic): inline face-upload banner — Parität mit Wardrobe-UX
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>
2026-04-26 19:48:20 +02:00
Till JS
450372e545 fix(writing): decrypt drafts/versions before reading encrypted fields
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>
2026-04-26 19:46:56 +02:00
Till JS
449837354d chore(branding): tier-patch remaining 8 modules to 'guest'
Schreiben + research-lab + broadcast + invoices + agents + timeline +
website + spaces stehen jetzt auf 'guest' damit alle Beta-Tester ohne
Tier-Upgrade reinkönnen. LOCAL-TIER-PATCH-Marker dokumentieren den
Original-Tier für den Release-Revert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 19:40:21 +02:00
Till JS
507532c367 docs(workbench-seeding-cleanup): record polish-pass commits
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 19:34:25 +02:00