Commit graph

3612 commits

Author SHA1 Message Date
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
Till JS
4d77934bd5 test(augur): unit tests for all deterministic engines
Locks in the contracts of the pure-math modules:

  - reminders: 30-day fallback, isDue ≤ today, signed daysUntilDue
  - calibration: weighted hit-rate (partly = 0.5), Brier squared error,
    per-source ranking, vibe directional hit (good = fulfilled,
    bad = not-fulfilled, mysterious = no direction)
  - living-oracle: stop-word filtering, 5-component matchScore, find
    against resolved history only, both cold-start gates (≥50 history
    AND ≥3 matches), reflection text shape
  - year-recap: year filter, distribution counts, best/worst-source
    n>=3 eligibility, mostSurprising = good→not-fulfilled OR
    bad→fulfilled, mostFulfilled ordered by resolvedAt desc, capped
  - correlation-engine: zero-σ refusal, 0.3σ delta threshold, n>=5
    minimum, mood + sleep-quality + sleep-duration handled
    independently, sort by |Δσ| desc

65 tests across 5 files, all pure — no Dexie + no runes. Synthetic
mood/sleep maps for the cross-module engine.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:18:35 +02:00
Till JS
1cb137c4ff feat(visibility): pull augur onto the embed + privacy-overview rails
Augur landed (faa16fa89) with the visibility Picker + setVisibility
already in place — but no embed-resolver and no entry in the
/settings privacy registry. So flipping an omen to 'public' did
nothing visible, and the kill-switch couldn't see augur records
either. Closes both gaps.

- New EmbedSource `augur.entries` + resolveAugurEntries. Whitelist:
  claim + "{kind} · {vibe} · {outcome}" line. Personal fields
  (feltMeaning, expectedOutcome, source name, outcomeNote, related
  dream/decision links, livingOracleSnapshot) all stay private.
  Optional `status` filter maps to AugurOutcome so the user can
  build "predictions I got right" widgets.
- Sort: resolved-first, then encounteredAt desc — fulfilled
  predictions outrank still-open ones (more interesting public
  signal).
- Inspector dropdown gains "Augur (Omen / Wahrsagungen)".
- exposed-records.ts gains the augur entry — augur records now
  show up in /settings → Privatsphäre and the kill-switch.

Note: augur's `unlistedToken` field (set by its store on
'unlisted' flips) is currently dead code — the mana-api unlisted
backend doesn't know about `augurEntries` and there's no shared
view component. Half-state predates this commit; full unlisted-
share wiring is a separate, larger task that would touch the
backend's ALLOWED_COLLECTIONS, the resolvers blob, and a new
SharedAugurEntryView. Leaving as-is until there's clear demand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:18:14 +02:00
Till JS
21c64e2616 docs(workbench-seeding-cleanup): record shipped status, sequence Schicht A
Plan now reflects what's actually merged: D-soft, B+C, and Schicht A
Etappe 1 are in. Etappe 2 (creating-hook flip to throw) is queued
post-soak; D-hard (deterministic-id rename) follows after that.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:12:25 +02:00
Till JS
43bef2b24b refactor(scope): explicit spaceId stamping at every space-scoped write
Schicht A Etappe 1 of the workbench-seeding-cleanup plan: every module
store that writes to a space-scoped Dexie table now sets `spaceId`
explicitly via `getEffectiveSpaceId()` instead of relying on the
creating-hook's silent auto-stamp. Etappe 2 (flip the hook to throw on
missing spaceId) follows after a soak day.

New helper:

- `data/scope/scoped-db.ts` — `getEffectiveSpaceId()` returns the
  active Space's id when one is loaded, falling back to the personal
  sentinel `_personal:<userId>` for guests / pre-bootstrap windows.
  Symmetric with `getEffectiveUserId()`. Re-exported from
  `data/scope/index.ts`.

Migrated call sites (16 writes across 10 modules):

  picture: boards, boardItems
  events:  eventGuests, eventItems
  companion: companionConversations, companionMessages
  calc:    savedFormulas, calculations
  quotes:  quotesFavorites, customQuotes
  skilltree: skills, achievements
  moodlit: moods
  plants:  wateringSchedules
  questions: answers (manual + research-driven paths)
  data/ai: agents, agentKontextDocs

The audit also flagged `_serverIterationExecutions`, but it's an
internal infra table (leading underscore, not in SYNC_APP_MAP, never
synced) — the creating-hook doesn't run on it, so no change needed.

No behaviour change today: the hook still falls through to sentinel-
stamping when spaceId is missing, so existing rows + any unmigrated
caller keep working. Etappe 2 flips that fallback to a hard error,
turning silent omissions into write-time failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:11:25 +02:00
Till JS
42828434a4 fix(picture): lightbox sits above PillNav + tighter meta column
Two follow-ups on the borderless redesign:

- z-index bumped from `z-50` to `z-[100]`. The `(app)/+layout`'s
  `.bottom-stack` (PillNav, QuickInputBar, sync-status row) sits at
  z-index 90, so Tailwind's `z-50` was leaving the bottom chrome
  poking through the dark backdrop. 100 is the next round number
  above the layout's stack and keeps the lightbox unambiguously on
  top of everything in the (app) tree.

- Meta column tightens from `max-w-md` (~28rem) to `max-w-[14rem]`
  (~224px) so even long prompts hug the right edge instead of
  stretching halfway across the image. The detail row gains
  `flex-wrap: wrap` so model + dimensions + date wrap cleanly when
  they don't fit on one line — `justify-content: flex-end` keeps
  every line right-aligned to the corner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:03:05 +02:00
Till JS
faa16fa898 feat(augur): new module — signs collected, patterns read
Introduces the Augur module: capture omens, fortunes, and hunches in
a poetic Witness mode and read them back empirically in Oracle mode.
Same data, two lenses; the killer mechanic is the Living Oracle that
materialises empirical reflections from the user's own resolved
history at capture time.

Why now: docs/future/MODULE_IDEAS.md captured the brainstorm, then
the spec landed at docs/plans/augur-module.md as a Witness+Oracle
hybrid. Built end-to-end through M6 in one go.

Highlights:
- Witness gallery + DueBanner + DetailView + Resolve flow
- Oracle stats: calibration-per-source, vibe-hit-rate, cross-module
  correlation engine (mood/sleep/duration after-windows)
- Living Oracle: deterministic fingerprint+match against user's own
  resolved history; cold-start-gated at 50 resolved entries
- Year-Recap view at /augur/recap/[year]
- 5 MCP tools: capture_sign, resolve_sign, list_open_signs,
  consult_oracle, augur_year_recap (in AI_TOOL_CATALOG)
- Visibility integration: default 'private', VisibilityPicker in
  DetailView. Server-side unlisted-snapshot-publish stays follow-up
- v47 Dexie schema; encrypted: source/claim/feltMeaning/
  expectedOutcome/outcomeNote/tags/livingOracleSnapshot
- LOCAL TIER PATCH: requiredTier 'guest' for testing

Strings interpolated through `T` constants so the i18n-hardcoded
baseline stays at 0 for augur — real $_('augur.*') keys land later.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:02:15 +02:00
Till JS
568d79dc16 test(workbench): seeder defers to legacy Home + end-to-end wiring test
Two follow-ups to the per-space-seeds refactor:

1. Transitional check in `seedWorkbenchHomeOn`: if a Space already
   carries an uncustomised "Home" row under a legacy random uuid (a
   D-soft dedup survivor from before the deterministic-id contract
   landed), defer to it instead of inserting a parallel
   `seed-home-${spaceId}` row. Avoids an unnecessary
   create-then-soft-delete roundtrip via the +layout dedup pass and
   the sync churn that would follow. Schicht D-hard will rename
   surviving rows to the deterministic id and this branch can go away.

2. `wiring.test.ts` — integration test for the full chain
   (registry → workbench-home seeder → Dexie). Drives the same
   `runSpaceSeeds` entry point that `setActiveSpace` calls in
   production, so the test fails if any seam in the wiring breaks.
   Includes the rapid back-and-forth Space-switch scenario that the
   original bug ran into; with the deterministic id + get-then-add
   guard, 5×2 activations produce exactly 2 rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:34:05 +02:00
Till JS
f71a9377c0 feat(visibility): embed resolvers for memoro/cards/presi (M6 follow-on)
Closes the M6 loop — flipping a memo, card-deck, or presi-deck to
'public' now actually surfaces it on the owner's website embed.
Previously M6 wired the Picker but the embed pipeline didn't know
about these sources, so the flip had no visible effect.

Three new sources in EmbedSourceSchema:
- memoro.memos — voice-memo teaser. Title + intro (140 chars) +
  audio duration. Transcript, source-audio paths, and per-utterance
  speaker data stay private — those are the user's words verbatim
  with much stronger privacy weight than a curated headline.
- cards.decks — flashcard-collection teaser. Name + "N Karten".
  Card fronts/backs, difficulty, review history all private — the
  deck is a unit; the cards belong to the play experience.
- presi.decks — "talks I've given" teaser. Title + "N Folien"
  (counted by joining the slides table). Slide content stays
  private — the public deck is a pointer, the slides belong to
  the talk experience.

Each resolver tolerates the M6 soft-migration window: visibility
falls back to legacy isPublic for rows that haven't been re-saved
since the M6 commit.

Inspector dropdown updated to expose all 15 sources.

Note: 3 unrelated svelte-check errors in
data/seeds/wiring.test.ts (spaceId on LocalWorkbenchScene) from a
parallel session. Not introduced here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:33:43 +02:00
Till JS
e0ec7fe33f feat(visibility): M7 — /settings privacy overview + kill-switch
Adds a "Privatsphäre" tab to /settings (next to Sicherheit) that lists
every record currently flipped to 'public' or 'unlisted' across all
15 visibility-aware tables, with one-click downgrade per record and a
global "Alle auf privat zurücksetzen" kill-switch.

Architecture:
- `lib/data/privacy/exposed-records.ts` is the single registry of
  visibility-aware tables. For each: the collection name, encryption
  flag, title-extraction strategy, deep-link, and a dynamic-import
  fixer that calls the module's setVisibility (so unlisted records
  properly revoke their server snapshots, not just flip the field).
- `PrivacySection.svelte` subscribes via Dexie liveQuery so flips
  from any module DetailView surface here immediately.
- Kill-switch iterates the same fixers — best-effort sweep that
  logs failures but doesn't abort. Toast reports flipped/failed.

Two error/empty states wired:
- "Aktuell ist nichts öffentlich" reassures the privacy-conscious
  user that their footprint is zero.
- Per-record "Privat" button is disabled while busy so multi-clicks
  don't race.

Adding a new visibility-aware module: append one entry to TABLES in
exposed-records.ts. The Section auto-renders it.

Note: 2 unrelated svelte-check errors in
stores/workbench-scenes.svelte.ts (ensureSeedScene) from a parallel
session. Not introduced here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:24:59 +02:00
Till JS
c73f93ff12 refactor(workbench): central per-space-seeds registry + deterministic Home id
Replaces three race-prone seeding paths in `workbench-scenes.svelte.ts`
(count==0 init seed, replay-on-register seed, per-Space-change seed)
with a single registry pattern:

- `data/scope/per-space-seeds.ts` — registry of `Seeder` callbacks,
  fired by `setActiveSpace` whenever the active Space id changes.
  Errors are isolated per seeder so one module's bug can't block the
  others.
- `data/seeds/workbench-home.ts` — registers the workbench Home seeder.
  Uses the deterministic id `seed-home-${spaceId}` and a get-then-add
  guard, making the seed structurally idempotent: re-running for the
  same Space is a no-op regardless of timing. Replaces the old
  random-UUID + check-by-presence pattern that the seeding race could
  defeat.
- `data/seeds/index.ts` — side-effect barrel imported once at the top
  of `(app)/+layout.svelte` so every module's seeder is in the
  registry before the first `loadActiveSpace` fires.
- `active-space.svelte.ts` — both `setActiveSpace` and `loadActiveSpace`
  funnel through a private `applyActiveSpace` helper that calls
  `notifyHandlers` AND `runSpaceSeeds` in one place. No more divergent
  state-update paths.
- `workbench-scenes.svelte.ts` — `ensureSeedScene` and the two
  ad-hoc seed calls deleted. The store now only owns rendering and
  user-driven CRUD; seeding lives in the registry.

This is Schicht B + C of the broader cleanup plan
(docs/plans/workbench-seeding-cleanup.md). Schicht D-soft already
collapsed existing duplicates; this PR prevents new ones from forming.
The tests cover the registry contract (register/run/idempotency/error
isolation), the deterministic-id helper, and the seeder against an
isolated Dexie fixture (race-safety, no-overwrite of customised rows,
per-Space isolation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:21:25 +02:00
Till JS
ad5987f1dd feat(visibility): M6 soft-migrate isPublic→visibility on memoro/cards/presi/uload
Brings four legacy isPublic-only modules onto the unified visibility
system. The Picker (private/space/public) replaces the ad-hoc
boolean toggle in memoro + cards + presi DetailViews. Each store
now mirrors `visibility ↔ isPublic` on every flip so older readers
(search index, snapshot pipelines, sync server) keep working
through the soak window — M6.1 will hard-drop the legacy field
once the field has propagated to all rows.

Per-module:
- memoro: setVisibility on memosStore + Picker in DetailView
  properties row. Picker reads the unified `visibility` with a
  fallback to legacy isPublic.
- cards (decks): replaces the "Öffentlich Ja/Nein" toggle button
  with the Picker. createDeck initializes both fields; updateDeck
  mirrors when callers pass legacy isPublic.
- presi (decks): same pattern as cards. setVisibility added to the
  store factory's exported surface.
- uload (tags): no active CRUD UI, so this is type-only soft-migrate
  + seed-data update. Future tag-management view writes visibility
  directly. No store mutation method needed yet.

Out of scope (intentional):
- picture isPublic hard-drop: deferred. Picture has been on
  visibility since M3 (commit 0e9f574df), the soak window is mature
  enough to consider, but pre-launch there's no urgency. Defer to
  M6.1 with the rest of the legacy-field cleanup.
- events isPublished/publicToken: STAYS. This isn't legacy — it's
  the orthogonal mana-events RSVP-snapshot system. visibility
  controls website-embed eligibility; isPublished controls RSVP
  page existence. Different concerns; both stay.

Picker hides 'unlisted' for all four (no server-publish-snapshot
flow wired for these collections — same pattern as habits/quiz/events).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:11:07 +02:00
Till JS
bd559e739e polish(picture): clean borderless lightbox — image-first, meta in the corner
Feedback on the previous lightbox: the image was capped at 60vh
inside a bordered card, with metadata + action buttons stacked
underneath in their own panel. Result: ~40% of the modal surface
was chrome around a small picture, and the eye landed on the card
edge before the actual generation. The user asked for image-first:
fill the height, no module border, gray text in the bottom-right
corner.

New layout:

- Borderless backdrop. The card frame is gone; the image sits
  directly on a near-black overlay (rgba(0,0,0,0.92)) so nothing
  competes for the eye.
- Image fills the available area via `max-h-full max-w-full
  object-contain` inside a flex-center wrapper, with 6/10-rem
  outer padding so the picture doesn't kiss the viewport edges
  on small screens.
- Metadata moves to a small grey overlay in the bottom-right
  corner — prompt on top, then a single inline detail line
  (model · dimensions · date) separated by middle dots. Right-
  aligned so longer prompts wrap toward the image edge instead
  of the centre.
- Close becomes a circular icon-button in the top-right (X), no
  longer a footer button. ESC + backdrop-click still close.
- Caller-supplied actions (the `actions` snippet) move to the
  bottom-left so they don't fight the meta block visually.

Colour treatment uses literal white-alpha + black-alpha values
in a scoped `<style>` block instead of theme tokens. The lightbox
always sits on a literal near-black backdrop regardless of which
theme is active, so theme-aware muted tokens would render too
dark in light themes. The validator's brand-literal escape hatch
(see scripts/validate-theme-utilities.mjs comment) covers this
exact case.

Empty-state (publicUrl missing) gets a small SquaresFour icon in
a soft white-alpha tile so the modal still has a visible centre
when an image fails to load.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:10:11 +02:00
Till JS
d62ae8f1e3 fix(workbench): dedup duplicate Home scenes accumulated by seeding race
The Home-seeder in workbench-scenes.svelte.ts writes new scenes without
spaceId, so the creating-hook stamps them with the _personal:<userId>
sentinel. The per-space dedup check filters by the real space UUID and
never finds them — every login adds another Home row, and every visit
to a non-personal Space (Brand/Family/Team) drops yet another seed
into the personal Space.

This is Schicht D-soft of the broader cleanup plan
(docs/plans/workbench-seeding-cleanup.md): a one-shot dedup pass that
collapses duplicate "Home" rows per spaceId, merging openApps from the
losers into the survivor (most apps wins, ties by most-recent
updatedAt) and soft-deleting the rest so mana-sync propagates the
cleanup to other devices. Touches only rows that look like fresh
default seeds — anything customized (description, wallpaper, agent
binding, scope tags, non-Home name) is left alone.

Wired in two places: a Dexie v48 upgrade so it runs once per device on
schema bump, and a belt-and-suspenders pass in (app)/+layout.svelte
right after reconcileSentinels() to catch the edge case where
sentinel-stamped rows just collapsed into the same UUID group as
already-reconciled rows.

The structural fix that prevents new duplicates from ever forming
(per-space-seeds registry + deterministic seed ids +
creating-hook hardening) ships in follow-up commits per the plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:08:32 +02:00
Till JS
ac12b61de2 fix(writing): replace emojis with Phosphor icons in workbench empty-state
Visual cleanup for the workbench-card hero. Emojis were dropping the
design fidelity vs the rest of the Mana UI, which uses Phosphor icons
exclusively in chrome surfaces.

- Action bar 🎨 → <Palette /> in a 2x2-rem ghost button (square, no
  text label) so the visual weight matches the primary "+ Neuer Draft"
  button instead of competing with it.
- Hero icon 📝 → <NotePencil weight="duotone" /> in a 3rem rounded
  tinted-sky pill — gives the empty-state a consistent on-brand
  centerpiece without the multi-color emoji look.
- Hero meta line restructured: was a single -prefixed sentence that
  wrapped awkwardly on narrow workbench cards. Now a centered <ul> of
  short uppercase chips ("12 TEXTARTEN · 9 STILE · 7 QUELLEN · E2E-
  VERSCHLÜSSELT") with a Sparkle icon only on the first item.
- Quick-start tiles 📝/📄/✉️/💬/💌/🎤 → kind-specific Phosphor icons
  via a QUICK_ICON map (blog→Newspaper, essay→Notebook, email→Envelope,
  social→ChatCircle, letter→Envelope, speech→Microphone). Icons inherit
  the muted text colour, switch to sky on hover.
- Hero spacing tightened: padding/margins reduced so the layout breathes
  on a 280–320px-wide workbench card instead of cramming. Quick-grid
  caps at 360px max-width and falls back to 2 cols below 360px.

KIND_LABELS still carries `.emoji` for the populated-state KindTabs and
DraftCard — those stay emoji-flavoured for now since they're inline
text. Icon migration there is a separate sweep when the time comes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:05:26 +02:00
Till JS
59b147f5ee feat(visibility): embed resolvers for habits/quiz/social-events + inspector refresh
Closes the loop on the M5-Rest visibility rollout — flipping a habit,
quiz, or social-event to 'public' now actually surfaces it on the
owner's website embed.

EmbedSourceSchema gains three new sources:
- habits.habits — build-in-public widget. Title + "🔥 N Tage Streak ·
  gesamt M ×". Per-log timestamps + notes stay private (sleep/intake
  patterns are not for public consumption).
- quiz.quizzes — shareable-quiz teaser. Title + "N Fragen · {category}".
  Questions, options, explanations, attempts/scores all stay private —
  the actual play-experience is reserved for a future unlisted-share
  flow.
- events.socialEvents — RSVP-event teaser. Title + formatted start
  date + location + cover image. Hard-gated on the unified `visibility`
  only; the legacy `isPublished` flag is intentionally bypassed so the
  new Picker is the single source of truth (M6 will drop isPublished).

ModuleEmbedInspectorFallback now lists all 12 sources — was only
exposing 2 of the 9 already-wired ones (latent debt unblocking the
new sources from being addable in the editor).

Note: 7 unrelated svelte-check errors exist in
data/scope/dedup-workbench-scenes.test.ts from a parallel session
(spaceId not on LocalWorkbenchScene). Not introduced here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 13:58:43 +02:00
Till JS
21dbce6631 feat(writing): smarter empty-state + help-content + de-emphasized Stile link
Three workbench-card UX fixes for the Schreiben module.

Smart empty-state (replaces the small "Noch keine Drafts" line):
- Hero block with icon + 1-line pitch + meta-line (" 12 Textarten · 9
  Stile · 7 Quellen-Typen · End-to-End-verschlüsselt"). Sells the
  module's surface area before the user has anything saved.
- Six quick-start tiles (Blog · Essay · E-Mail · Social · Brief · Rede)
  in a 3-col grid. Tile click → BriefingForm opens with that kind
  pre-selected, skipping the Textart-dropdown for the most common cases.
  Full 12-kind picker remains one click away inside the form.
- Search + KindTabs + status-filter + favorites-toggle now hide when
  drafts.length === 0 — filtering nothing was visual noise. They reappear
  the moment the first draft lands.

De-emphasized Stile link:
- Was a labeled "🎨 Stile" pill competing with "+ Neuer Draft" for
  attention. Now a compact icon-only ghost button (just 🎨) with an
  aria-label so screen readers still get it. Frees the action bar so
  the primary CTA stands alone.

Module help (renders behind the ?-icon in the Workbench shell):
- New `writing` entry in MODULE_HELP with description + 9 features
  (kinds / styles / references / refinement / versioning / visibility /
  export / persona-linkage / token cost) + 5 tips covering keyboard
  shortcuts, -button, the Quellen-flow, drag-source, and custom
  styles. Uses the existing auto-attach via app-registry's onMissing-
  help fallback — no AppPage changes needed.

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