Commit graph

3628 commits

Author SHA1 Message Date
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
Till JS
8c5f064b03 test+docs(workbench-seeding): hook stamping test + per-space-seeds guide
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>
2026-04-26 19:33:59 +02:00
Till JS
e930a66ff3 refactor(workbench-seeding): inline v48 dedup, drop dead helper module
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>
2026-04-26 19:33:46 +02:00
Till JS
3d30e39ae7 feat(comic): Mc5 — Wardrobe-Hook "Als Comic-Character"
Brücke von Wardrobe nach Comic: User klickt auf einem Outfit oder
einem einzelnen Kleidungsstück „Als Comic-Character", landet im
Character-Builder mit pre-filltem Add-Prompt ("wearing the
Bühnenoutfit"), picked Stil und rendert die ersten 4 Varianten.

Wardrobe-Buttons:
- DetailOutfitView: unterhalb des TryOnButton ein outline-Link
  navigiert zu `/comic/character/new?title=…&prompt=wearing+the+
  OUTFITNAME+outfit`.
- DetailGarmentView: analog mit `prompt=wearing+GARMENTNAME` für
  ein einzelnes Kleidungsstück. Beide nur sichtbar wenn das
  Outfit/Garment nicht archiviert ist.
- Sparkle-Icon + dezent neutraler Border-Style (nicht primary —
  das ist die TryOn-CTA), hover schaltet auf primary/40.

Comic CharacterBuilder bekommt drei optionale Props:
`initialName?`, `initialAddPrompt?`, `initialStyle?`. Im
extend-Modus ignoriert (Source ist dann der existing-Character),
im create-Modus dienen sie als $state-Initialwerte. Routine read
ist intentional — Mounting passiert frisch pro Route-Visit, also
einmaliges Capture passt.

`/comic/character/new/+page.svelte` parsed jetzt
`page.url.searchParams` für `title`, `prompt`, `style` und reicht
sie als Props durch. style wird gegen die VALID_STYLES-Liste
validiert — defekte URL-Params fallen ohne Crash auf
"unset/default" zurück.

Bewusst NICHT gemacht: Try-On-Output direkt als sourceBodyMediaId
verwenden. Das Try-On-Bild ist im mana-media mit `app='picture'`
getaggt; `verifyMediaOwnership` auf
`/picture/generate-with-reference` akzeptiert nur
`['me','wardrobe','comic']` — der Comic-Generate würde mit
HTTP 404 abbrechen. Lösung wäre eine Server-Route die Picture-
Output als Comic-Asset re-tagged, das ist aber eigene Spec.
Aktueller Pfad ist sauberer: rohe meImages-Refs bleiben Source,
der Add-Prompt steuert den Outfit-Look.

Plan-Doc §11 Mc5 dokumentiert den Pfad + warum kein
Try-On-Reuse.

Comic-Files type-checken sauber.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 19:32:29 +02:00
Till JS
ef96948ea0 feat(comic): Mc4 — MCP + AI-Catalog für Character-System
Persona-Runner / Claude Desktop / Web-App-Mission-Runner können jetzt
Comic-Characters bauen, iterieren und pinnen — same Auto/Propose-
Pattern wie die Story-Tools.

MCP (packages/mana-tool-registry/src/modules/comic.ts):
- comic.listCharacters (read/auto): Pull, decrypt, filter (style?,
  favoriteOnly?), liefert {id, name, style, addPrompt, source-Refs,
  variantMediaIds, pinnedVariantId, variantCount, tags, isFavorite}.
- comic.createCharacter (write/propose): legt nur die Row an —
  trennt Anlegen von Generierung damit der Agent reviewen kann
  bevor Credits fließen. Liefert characterId zurück.
- comic.generateVariant (write/propose, kostet Credits): pullt
  Character-Row, dekodiert, ruft /picture/generate-with-reference
  mit n=count (default 4) + Stil-Prefix + Identity-Anchor-Prompt,
  schreibt N picture.images mit comicCharacterId-Back-Ref, pusht
  field-level Update auf variantMediaIds + pinnedVariantId
  (auto-pin auf erste neue Variant wenn vorher null).
- comic.pinVariant (write/propose): Set-Equality-Check (variantMediaId
  muss in variantMediaIds sein), field-level Update auf
  pinnedVariantId. Snapshot-Pattern: bestehende Stories bleiben
  unverändert, nur neue Stories nutzen den neuen Pin.

AI_TOOL_CATALOG (packages/shared-ai/src/tools/schemas.ts):
- list_comic_characters (auto)
- create_comic_character (propose) — auto-resolvt face/body-refs aus
  meImages-primaries, Agent muss keine mediaIds kennen
- generate_character_variant (propose, count 1-4)
- pin_character_variant (propose)

Web-App-Executors (apps/mana/apps/web/src/lib/modules/comic/tools.ts):
- 4 ModuleTool-Einträge, die an comicCharactersStore +
  runCharacterGenerate delegieren — gleicher Code-Pfad wie die UI,
  also keine Divergenz zwischen Klick und Agent-Call.

Comic-Autor-Template (packages/shared-ai/src/agents/templates/
comic-author.ts):
- Policy bi-lingual erweitert: snake_case + dot-case Namen für
  alle 4 neuen Character-Tools.
- System-Prompt Schritt 3 ergänzt: "Wenn der User noch keinen
  passenden Comic-Character hat → list_comic_characters →
  create_comic_character → generate_character_variant → pin.
  Das ist EINMALIG — der gepinnte Character bleibt für viele
  Stories der stabile Identity-Anchor."
- Tool-Liste am Ende vom System-Prompt um den Character-Pfad
  ergänzt.

apps/mana/CLAUDE.md Tool-Coverage-Zeile für comic erweitert:
+ create_comic_character / generate_character_variant /
+ pin_character_variant (propose)
+ list_comic_characters (auto)

Tool-Count: comic 3→7. Module 23 unverändert.

107 shared-ai-Tests weiter grün. check für comic-Files clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 19:27:15 +02:00
Till JS
c5ff7e1d33 feat(augur): real i18n keys — replace T constants with $_('augur.*')
The M1–M6 build shipped strings in `T = {} as const` constants — clean
enough to keep the i18n-hardcoded baseline at zero, but never the
real plan. This commit closes the loop:

  - Add per-locale namespace `augur` with five bundles
    (de / en / fr / it / es). DE + EN are translated; FR/IT/ES mirror
    DE for parity until proper translation lands. Same staging that
    other modules use.
  - Replace every `T.x` with `$_('augur.section.x')` across 8 svelte
    files + 2 routes. Drop the const blocks where they were the only
    reason a script tag had untyped state.
  - Existing `KIND_LABELS[k].de` / `VIBE_LABELS` / etc. stay — they
    serve dual-mode (web + tools.ts), and the module convention is
    nested-locale records rather than svelte-i18n keys for those.

Validators:
  - i18n-parity:    36 namespaces × 5 locales — 2852 keys aligned
  - i18n-keys:      315 missing (== baseline) — augur adds zero
  - i18n-hardcoded: augur not flagged

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 18:17:07 +02:00
Till JS
303058d406 refactor(visibility): M6.1 — drop legacy isPublic everywhere
Removes the deprecated `isPublic` field from picture, memoro, cards,
presi, and uload. The unified `visibility` enum has been the source
of truth since M3 (picture) / M6 (others) and the soft-fallback in
queries was the last consumer of the legacy field. Killing it
cleanly:

- types: drops isPublic from LocalX + X interfaces, and from
  CreateDeckInput/UpdateDeckInput/UpdateDeckDto.
- queries.ts: type converters now read `visibility ?? 'space'` (or
  'private' for picture) without the isPublic fallback. Cards's
  `getPublicDecks` helper now filters on `visibility === 'public'`.
- stores: createX no longer initializes isPublic; updateX no longer
  accepts/mirrors it; setVisibility no longer writes the mirror.
- UI: cards CreateDeckModal drops the public-toggle (use the
  Picker in DetailView post-create); DeckCard + presi ListView +
  /cards/decks/[id] page badges read `visibility === 'public'`.
- collections.ts: drops isPublic: false from seed rows.
- embeds.ts: picture/memoro/cards/presi resolvers drop the
  isPublic fallback. Top comment updated to reference
  canEmbedOnWebsite as the canonical gate.

Existing IndexedDB rows still carry the stale isPublic value but
nothing reads it. No Dexie schema bump needed (field was never
indexed). No data loss — visibility was mirrored on every flip
during the soft-migrate window so all "public" intent has already
propagated to the unified field.

Closes M6.1 — picture/memoro/cards/presi/uload now have no
legacy visibility flags. Events' isPublished/publicToken stays
(orthogonal RSVP-snapshot system, not legacy).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 18:16:57 +02:00
Till JS
0ff5030ad2 feat(comic): Mc3 — Story-Create nutzt Character-Mode + Quick-Fallback
Story-Anlegen ist jetzt zweigleisig: Default ist Character-Mode
(picke einen iterierten Comic-Character mit gepinntem Look),
Fallback ist Quick-Mode (rohes face/body/garments wie bisher) als
opt-in-Toggle für Spontan-Stories ohne Setup.

Datenmodell-Erweiterung (soft, kein Breaking-Change):
- LocalComicStory + ComicStory bekommen ein optionales
  `characterId?: string | null` Feld, plaintext, FK auf
  comicCharacters.id. Im Quick-Modus null, im Character-Modus die
  gewählte Character-id.
- `characterMediaIds` bleibt das einzige Feld, das runPanelGenerate
  liest — im Character-Modus enthält es genau die
  `pinnedVariantMediaId` als single-element-Array (Snapshot zum
  Story-Create-Zeitpunkt). Re-Pinning eines Characters ändert
  bestehende Stories also NICHT, weil sie das mediaId fix
  gespeichert haben. Im Quick-Modus enthält's face + body? +
  garments[] wie vorher. Beide Modi gehen durch denselben
  /picture/generate-with-reference-Pfad.
- Soft-Migration: bestehende Stories ohne `characterId` zeigen
  weiterhin keine Character-Linkage und rendern wie vorher (die
  `characterMediaIds` waren vorher ja schon die Quelle).

Neue Komponente:
- `CharacterRefPicker.svelte` ersetzt den alten `CharacterPicker`
  in StoryForm. Mode-Toggle (Character | Quick) erscheint nur wenn
  Characters existieren — sonst startet's direkt im Quick-Modus.
  Character-Mode zeigt Grid der usableCharacters (nicht-archived
  + pinnedVariantId gesetzt) mit Cover, Style-Badge, Active-Border.
  "+ Neuer Character"-Tile öffnet die Builder-Route. Quick-Modus
  rendert intern den alten CharacterPicker (face/body/garments) —
  reuse statt parallel zu pflegen.

StoryForm:
- 2 neue $state-Felder: `characterId` und (umbenannt-) der bestehende
  `characterMediaIds`. CharacterRefPicker emittiert beide via
  onChange-Callback.
- createStory bekommt `characterId` mit, das landet auf der Story-
  Row. canSubmit greift weiterhin auf `characterMediaIds.length > 0`
  — beide Modi liefern mindestens 1 ref.

CharacterBuilder Bugfix: prettier hatte den Add-Prompt-Placeholder
mit nested double-quotes zerstört (z.B. "freundlicher Ausdruck"
wurde zu invalidem HTML). Auf einfache Liste umgestellt.

8/8 Encryption-Tests weiter grün. check für comic-files clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 18:16:24 +02:00
Till JS
882aa60976 feat(comic): Mc2 — Character-Builder UI + Variant-Grid + Routes
Datenschicht aus Mc1 wird jetzt durch UI benutzbar. End-to-end-Flow:
Tab-Switch zu Characters → "+ Neuer Character" → Stil + Add-Prompt
+ Source-Confirm (face Pflicht, body Toggle) → 4 Varianten parallel
gerendert → User pinnt eine als Identity → Character ist fertig,
nutzbar als Story-Anchor (Mc3 wired das in den StoryForm-Flow).

UI-Komponenten:
- `api/generate-character.ts`: runCharacterGenerate({character, n=4,
  quality, model}) ruft /picture/generate-with-reference mit
  [face, body?]-Refs + Stil-Prefix + Add-Prompt + Identity-Anchor-
  Hint, schreibt N picture.images mit comicCharacterId-Back-Ref,
  appended an den Character via comicCharactersStore.appendVariant
  (auto-pin auf erste Variant). Ein Server-Call mit n=4 statt 4
  parallele — gpt-image-2 Multi-Image-Response in einem Batch.
- `components/CharacterCard.svelte`: Grid-Tile mit Cover (pinned
  Variant > erste Variant > Placeholder), Style-Badge, Favorit-
  Heart, Amber "Pin offen"-Badge wenn Varianten existieren aber
  keine gepinned ist.
- `components/VariantTile.svelte`: einzelne Variant im Grid mit
  Pin-Star wenn aktiv, Bottom-Action-Bar auf Hover (Pinnen / Entf.).
  Pinned hat primary-Border + Schatten, Unpinned dezent.
- `components/CharacterBuilder.svelte`: Zwei Modi via `existing`-
  Prop. Create-Modus: Name + StylePicker + AddPrompt + Source-
  Preview (face Pflicht, Body-Toggle). Extend-Modus: Style + Source
  fix vom existierenden Character, nur AddPrompt editierbar pro
  Generierung. Beide feuern die gleiche runCharacterGenerate-Pipeline.
- `views/CharactersView.svelte`: Grid + "+ Neuer Character"-CTA +
  Face-Ref-Empty-State + leeres Empty-Board. Gleicher Aufbau wie
  StoriesView für visuelle Konsistenz.
- `views/DetailCharacterView.svelte`: Meta-Card (Titel + Style-
  Badge + Variant-Count + Pin-offen-Hinweis), Variant-Grid mit
  Pin/Remove, "+ Mehr Varianten"-Button öffnet Builder im
  extend-Modus inline (Builder bleibt offen für Iterations-Flow).
  Plus Archive/Delete.
- `ListView.svelte` (Modul-Root) bekommt 2-Tab-UI:
  **Stories | Characters** mit Count-Badge auf dem Characters-Tab.
  Standardpattern wie Wardrobe's Garments|Outfits.

Routes:
- `/comic/character` (Liste, eigenständige Route — Back-Nav aus
  Detail/New zeigt darauf)
- `/comic/character/new` (CharacterBuilder im Create-Modus)
- `/comic/character/[id]` (DetailCharacterView mit {#key id}
  Re-Mount wie Story-Detail).

check passes 0/0 für comic-files.

Mc3 (Story-Create wechselt auf den neuen Picker, Soft-Migration
für bestehende Stories) folgt im nächsten Commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:42:31 +02:00
Till JS
2b359f9e1a refactor(writing): swap hardcoded sky-cyan for theme tokens
Schreiben-Modul rendert jetzt korrekt unter allen Theme-Varianten
(Lume Gold, Nature Green, Stone, Ocean) statt überall sky-cyan zu
forcieren. 17 Files: alle #0ea5e9/#0284c7 → hsl(var(--color-primary)),
alle var(--color-text-muted, ...) → hsl(var(--color-muted-foreground)),
alle var(--color-border, ...)/var(--color-surface, ...) auf saubere
hsl()-Wraps umgestellt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:53:18 +02:00
Till JS
313809bc95 feat(comic): Mc1 — Character-Datenschicht (Iteration + Pinning)
Comic-Modul nutzte bisher rohe meImages direkt als Story-Refs:
gpt-image-2 / Nano Banana variieren zwischen Calls, Panel 1 sah
anders aus als Panel 4, User hatte keine Iteration vor der Story.
Lösung: Comic-Character als eigene Entität, einmal aufgebaut +
iteriert + gepinnt, danach Story-Anchor.

Datenschicht:
- Dexie v49 `comicCharacters` (space-scoped, indices createdAt /
  style / isFavorite / isArchived).
- types.ts: LocalComicCharacter mit name + style + addPrompt +
  sourceFaceMediaId + sourceBodyMediaId? + variantMediaIds[] +
  pinnedVariantId?, plus toCharacter + characterCoverVariantId
  helper (pinned > erste Variant > null).
- crypto/registry.ts: comicCharacters entry — name + description
  + addPrompt + tags encrypted; style + IDs + Variant-Liste +
  Booleans plaintext.
- collections.ts: comicCharactersTable.
- queries.ts: useAllCharacters, useCharactersByStyle, useCharacter
  via scopedForModule (alle space-scoped).
- stores/characters.svelte.ts: createCharacter (auto-pin first
  variant fallback), appendVariant (auto-pin if none yet),
  pinVariant, removeVariant (mit pin-fallback auf erste
  remaining), updateCharacter, toggleFavorite, archiveCharacter,
  deleteCharacter. Arrays werden via [...arr] entproxiet (Svelte
  5 $state defense).
- module.config.ts: comicCharacters in tables-Liste.
- picture/types.ts + queries.ts: comicCharacterId Back-Ref auf
  LocalImage + Image, mutually exclusive mit comicStoryId.
- 3 neue Encryption-Roundtrip-Tests (insgesamt 8 grün) für
  charakter-Row, Build-in-progress (no variants), Roundtrip.

Architektur-Entscheidungen (Plan-Doc §11 dokumentiert):
- **space-scoped**, nicht user-global: Source-meImages sind ja
  selbst space-scoped post-v40, sonst orphan-Refs nach
  Space-Wechsel.
- **Snapshot at story-create**, kein Live-Lookup: Stories
  speichern die mediaId der gepinnten Variant zum Erstellungs-
  zeitpunkt → re-pinning eines Characters lässt bestehende
  Stories unverändert.
- **n=4 fixes Variant-Count**: in einem gpt-image-2-Call
  parallel; sweet-spot für Auswahl ohne Decision-Fatigue.
- **Mutually-exclusive Back-Refs** auf picture.images:
  comicStoryId XOR comicCharacterId — Image ist Panel ODER
  Variant, nie beides.

Mc2 (UI: Builder + Variant-Grid + Routes), Mc3 (Story-Create-
Update + Soft-Migration), Mc4 (MCP/Catalog), Mc5 (Wardrobe-Hook)
folgen separat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:52:58 +02:00
Till JS
b385839204 feat(augur): SharedLinkControls + setUnlistedExpiry/regenerate
Augur's unlisted-share backend was already wired (mana-api
ALLOWED_COLLECTIONS, blob resolver, /share/[token] dispatcher,
SharedAugurEntryView), but the DetailView didn't show the
share controls — flipping an entry to 'unlisted' generated a
token the user couldn't see, copy, regenerate, or expire.

Closes the loop:
- augurStore gains setUnlistedExpiry + regenerateUnlistedToken
  (same pattern as calendar/library/places M8.5).
- DetailView's visibility section now renders SharedLinkControls
  when the entry is 'unlisted' — URL + copy + QR + regenerate +
  revoke + expiry picker.

This makes augur the 4th collection with full unlisted-share
support (events / library / places / augur). My previous commit's
"deferred until clear demand" note was wrong — the heavy lift
(backend + view component) was already done by the augur module
PR; only the DetailView wiring + 2 store methods were missing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:52:37 +02:00
Till JS
547f643a6f docs(workbench-seeding-cleanup): record final architecture, all shipped
The plan ended up simpler than the four-layer sequence I originally
sketched: making the hook smart (use `getEffectiveSpaceId()` instead of
the literal sentinel) replaced both Schicht-A Etappe-2 (throw on
missing) and the per-call-site stamp migration. With that, the
transitional legacy-Home check + post-reconcile dedup pass also
became dead code and got removed in the same cleanup commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:51:23 +02:00
Till JS
fa71269fc8 refactor(workbench-seeding): drop transitional code paths, finalise via v50
With the smart hook always stamping the active-Space id and the seeder
running the deterministic-id contract, the transitional concessions
introduced for the soft-cleanup window are no longer load-bearing:

- `seedWorkbenchHomeOn` no longer scans for a legacy random-uuid Home
  in the same Space and defers to it. It just no-ops on a present
  deterministic-id row, otherwise inserts. The corresponding three
  transitional unit tests are dropped.
- `(app)/+layout.svelte` no longer runs a post-`reconcileSentinels`
  dedup sweep. The sweep was belt-and-suspenders for an edge case
  that the smart hook + deterministic id structurally prevent.

D-hard via Dexie v50: soft-deletes every uncustomised "Home" row whose
id is NOT `seed-home-<spaceId>`. The per-space-seeds registry
recreates a fresh deterministic-id row on the next `setActiveSpace`
for any Space that lost its uncustomised Home, so the system self-
heals. Customised Homes (description / wallpaper / agent / scope tags)
are preserved.

Combined with v48 this leaves zero legacy duplicates and zero
random-UUID seeds in `workbenchScenes` once the upgrade runs. New
devices coming up against an empty IndexedDB walk through both
upgrades as no-ops and land on the clean state directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:50:53 +02:00
Till JS
a6c5397d10 refactor(scope): smart hook stamps active-Space id, revert explicit stamps
Replaces the silent `_personal:<userId>` literal in the creating-hook
with `getEffectiveSpaceId()`, which returns the currently-active Space
when one is loaded and falls back to the personal sentinel for
guests / pre-bootstrap windows. Side-effect: writes during a Brand /
Family / Team session now land under that Space's UUID instead of
silently routing to Personal once `reconcileSentinels` runs — the
underlying tenancy bug Schicht A was supposed to catch.

With the hook doing the right thing automatically, the 16 explicit
`spaceId: getEffectiveSpaceId()` stamps from Etappe 1 are redundant
boilerplate. Reverted across:

  picture/stores/boards (boards + boardItems)
  events/stores/{guests,items}
  companion/stores/chat (conversations + messages)
  calc/stores/{calculations,saved-formulas}
  quotes/stores/{favorites,custom-quotes}
  skilltree/stores/{skills,achievements}
  moodlit/stores/moods
  plants/mutations
  questions/stores/answers (manual + research draft)
  data/ai/agents/{bootstrap,kontext}

Helper plumbing:

- `getEffectiveSpaceId()` lives in `scope/active-space.svelte.ts` (no
  db dependency) so the creating-hook in `database.ts` can import it
  without an ESM cycle. Inlined the `_personal:<userId>` literal there
  instead of pulling `personalSpaceSentinel` from `bootstrap.ts`,
  which would otherwise tangle the import graph.
- Re-exported via `scope/index.ts` for callers outside the hook.
- `setActiveSpace` and `loadActiveSpace` already funnel through the
  shared `applyActiveSpace` helper, so the hook's view of the active
  Space stays in sync with the rest of the scope layer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:49:42 +02:00
Till JS
9e04385930 feat(augur): unlisted-snapshot publish pipeline
augur.setVisibility now coordinates with the server-side unlisted-
snapshots table — same pattern as library/calendar/places. The local
token-allocation placeholder from M6 is replaced with real publish/
revoke calls; deletion revokes any active link before tombstoning.

  - resolvers.ts: buildAugurEntryBlob with strict whitelist
    (source, claim, kind, vibe, encounteredAt, outcome,
    outcomeNote when resolved). NEVER inlines feltMeaning,
    expectedOutcome, probability, tags, livingOracleSnapshot,
    sourceCategory or related FK references — divinatory captures
    stay sensitive even when shared.
  - SharedAugurEntryView: SSR card with vibe-colored border, kind +
    date meta, outcome badge, "Wie es kam" section only when the
    sign was actually resolved.
  - Dispatcher in /share/[token]/+page.svelte gains the
    augurEntries branch.
  - mana-api ALLOWED_COLLECTIONS extended to four items so the
    publish endpoint accepts augurEntries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:38:09 +02:00
Till JS
a1f2dccb68 feat(tool-registry): augur module — 5 server-side tools
Mirrors apps/mana/apps/web/src/lib/modules/augur/tools.ts for the
shared mana-tool-registry. Lets persona-runner / mana-mcp / mana-ai
invoke augur over stdio and HTTP without going through the web app.

Tools:
  - augur.captureSign     (write) — log a new omen / fortune / hunch
  - augur.resolveSign     (write) — fulfilled / partly / not-fulfilled
  - augur.listOpenSigns   (read)  — what's still waiting on resolution
  - augur.consultOracle   (read)  — Living Oracle reflection from history
  - augur.yearRecap       (read)  — structured year-in-review snapshot

The pure-math engines (fingerprint, matchScore, makeReflection,
yearRecap aggregation) are mirrored from the web-app lib/. Both
sides have unit tests covering the same contract — keep them in
sync. A future shared package would dedupe.

Encrypted fields declared on each spec (audit:encrypted-tools went
from 15 to 20). ModuleId extended in types.ts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:32:04 +02:00