Commit graph

3587 commits

Author SHA1 Message Date
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
Till JS
95bedf4625 fix(comic): proper input focus + sichtbare Entfern-Affordance
Drei Tweaks an der Create-Seite, ausgelöst von User-Feedback nach
dem ersten Story-Anlegen:

1. **Inputs/Textarea ohne Focus-Ring.** Title-Input und
   Story-Kontext-Textarea hatten nur `focus:border-primary`
   und sahen im aktiven Zustand fast nicht anders aus als
   inaktiv. Wardrobe's GarmentForm nutzt `focus:ring-1
   focus:ring-primary` plus `disabled:opacity-50` — übernommen.
   Textarea zusätzlich `resize-none` (vertikales Resize von Hand
   ist nett, aber kollidiert mit Padding bei kleiner Karten-Breite).

2. **Face-Tile war ein nackter `<img>`.** Kein border-2, kein
   "Pflicht"-Hinweis — User dachte er könnte's entfernen und
   suchte nach dem X. Jetzt: border-2 border-primary/40 wie
   Body-Tile, plus ein "PFLICHT"-Badge mit Gradient-Overlay am
   unteren Rand. Title="Face-Ref ist Pflicht — kann nicht
   entfernt werden". Damit ist klar: Face = locked.

3. **Body und Garments waren entfernbar, aber das war unsichtbar.**
   - Body-Toggle: bisher gar kein Hover-Feedback im aktiven
     Zustand (Plus-Overlay nur bei inactive). Jetzt im aktiven
     Zustand auf Hover ein rotes X-Overlay über dem Bild
     (group-hover-Pattern, error/60-bg, opacity 0→100).
     Title-Tooltips nochmal verschärft: "Klick zum Entfernen"
     vs. "Klick zum Hinzufügen".
   - Garment-Tiles: das X-Button war 5x5 (20px), abgerundet,
     bg-background/80 — verschwand visuell auf manchen
     Garment-Fotos. Jetzt:
     • Die ganze Tile ist klickbar (Touch-friendly), mit
       hover:border-error/60 (visueller "achtung, klicken
       entfernt das")
     • X-Badge h-6 w-6 (24px) mit Border-Ring, weißer bg-Pille,
       wechselt bei group-hover auf error-bg + weiß. Immer
       sichtbar, nicht erst auf Hover.
   - Heading-Subline ergänzt um "klicke ein Bild oder das ✕,
     um es wieder zu entfernen" — explizite Anleitung.

Comic-Files type-checken sauber.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 13:51:05 +02:00
Till JS
e0c0791bb5 feat(visibility): pilot extended to habits + quiz + events (M5 finish)
Extends the unified visibility system (@mana/shared-privacy) to three
of the four remaining open modules from the M5 rollout list. Each
module now exposes a private/space/public picker — `unlisted` is
hidden via `disabledLevels={['unlisted']}` because none of the three
have a server-publish-snapshot path yet (M8 territory).

Per-module:
- habits: visibility on LocalHabit + Habit; defaultVisibilityFor on
  createHabit; setVisibility emits VisibilityChanged. Picker in
  HabitDetail right under the header.
- quiz: same pattern on LocalQuiz + Quiz; Picker in EditView meta
  section so quiz-authors flip visibility while editing metadata.
- events (socialEvents): visibility coexists with the legacy
  `isPublished` + `publicToken` flags until M6 consolidation. The
  Picker writes the unified field; publish/unpublish still drives the
  RSVP snapshot. Picker as its own section above RSVPs.

Invoices skipped — `invoiceClients` has no write path yet (the
ClientPicker only reads), and the Invoice document itself is too
sensitive to ever go public. Will land alongside the future
client-portal feature.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 13:46:50 +02:00
Till JS
dff02d24a9 fix(mana-media): HEIC uploads from Chrome — sniff + transcode at the edge
iPhone HEIC photos uploaded through Chrome on macOS landed as
`mimeType: application/octet-stream` because Chrome doesn't recognise
the HEIC MIME and `file.type` was empty. The transform endpoint then
refused with `Transform only supported for images` (HTTP 400) and
the wardrobe Try-On flow surfaced this as `mana-media transform
failed for <id>: HTTP 400`. Even fixing the MIME wouldn't have been
enough — sharp's prebuilt binary ships the heif container format
without a HEVC decoder plugin (libde265 is omitted for patent
reasons), so the actual decode would still throw.

Three-part fix at the upload edge:

1. New `services/sniff.ts` — magic-byte sniffer for image MIMEs.
   Reads the first ~16 bytes and recognises JPEG, PNG, GIF, WebP,
   BMP, TIFF, HEIC, HEIF, AVIF. Returns `null` for everything else
   so the caller can fall back to whatever the browser claimed.

2. Upload route — sniffs every upload before passing the buffer to
   `uploadService.upload`. Trusts magic bytes over `file.type` so
   Chrome's empty-type HEIC still lands with `image/heic`. Removes
   the entire class of `application/octet-stream` rows for files
   that are obviously images.

3. HEIC/HEIF transcoded to JPEG at upload via the new
   `heic-convert` dependency (pure-JS WASM, no system libs needed).
   The original buffer is replaced with the JPEG bytes, the MIME
   becomes `image/jpeg`, and the filename's `.HEIC` extension is
   rewritten to `.jpg`. Downstream code (process pipeline, transform
   endpoint, sharp) then deals exclusively with formats sharp can
   actually decode. Failure path returns HTTP 500 with a clear
   `HEIC conversion failed` error so the client knows it wasn't a
   generic crash.

Bonus, transform endpoint hardening: `mimeType.startsWith('image/')`
gate now also accepts a row whose stored MIME is wrong (legacy
`application/octet-stream` from before this fix) when the actual
bytes sniff as an image. Lets old broken rows still serve where
the format itself is decodable; the upload-side fix prevents new
ones from existing.

Sharp 0.33 on this machine reports `heif: 1.18.2` for the container
but rejects the actual HEVC compressed bitstream — confirmed by the
exact error string `No decoding plugin installed for this
compression format (11.6003)`. Going through `heic-convert` first
sidesteps that entirely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 13:46:13 +02:00
Till JS
d880e89204 feat(writing): rename module display name "Writing" → "Schreiben"
User-facing label only — keeps the route /writing, the module id
'writing', the appId 'writing', and the table prefix writingDrafts/
writingDraftVersions/etc. Just renames the display name in:
- shared-branding/mana-apps.ts (AppSlider label)
- app-registry/apps.ts (Workbench card label)
- the three writing route <title> tags (page tab in browser)

The English code identity stays; the German UI label gets a German
name consistent with Bibliothek / Kontakte / Kalender / Notizen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 13:45:02 +02:00
Till JS
32147194fd fix(comic): DataCloneError beim Anlegen + stärkere Hover-States
Zwei Bugs beim ersten Anlegen einer Comic-Story:

1. **DataCloneError "[object Array] could not be cloned"** beim
   `comicStoriesTable.add(newLocal)`. Ursache: StoryForm deklariert
   `characterMediaIds`/`tags` als `$state<string[]>([])` und reicht
   die Proxies direkt an comicStoriesStore.createStory() durch.
   Dexie/IndexedDB's structured-clone refuseiert Svelte-5-State-
   Proxies — `tags`+`panelMeta` werden vorher von encryptRecord zu
   Ciphertext-Strings gewandelt, aber `characterMediaIds` (und
   `panelImageIds`/`tags` falls plaintext) bleiben Arrays und die
   schmieren als Proxy in den IDB-Write rein.

   Fix: Arrays beim Store-Eintritt mit `[...arr]` snapshotten.
   Greift jetzt auch in updateStory() für die gleichen Felder
   (zukünftiges StoryForm-Edit-Mode wäre sonst denselben Bug
   wert). Wardrobe hat das Problem latent auch, fix dort folgt
   wenn jemand auf gleiche Stelle stößt — die Comics-Lösung
   isoliert.

2. **Kein Button-Feel auf der Create-Seite.** StylePicker und
   CharacterPicker hatten zu schwache Tailwind-Hover-Klassen
   (`hover:bg-muted` only) — User las die Tiles nicht klar als
   klickbar.

   StylePicker komplett auf scoped CSS umgezogen (Pattern wie
   PanelModelPicker / wardrobe TryOnModelPicker): hover ändert
   border-color (border-primary/50) UND background (primary/5)
   UND fügt einen schwachen Schatten hinzu, plus active:translate-y-px
   für Touch-Feedback. Active-Tile bekommt klaren primary-Border +
   primary/8-bg + Schatten. Focus-visible mit Outline für
   Keyboard-Nav. role="radiogroup" + aria-checked statt aria-pressed
   für korrekte Semantik.

   CharacterPicker: Body-Ref-Toggle bekommt jetzt hover:border-primary/50
   + hover:shadow-sm und beim aktiven State einen leichten primary-
   Schatten. Add-Garment-Button kriegt hover:border-primary/50 +
   hover:bg-primary/5 + hover:shadow-sm.

5 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 13:39:47 +02:00
Till JS
75c366bff4 test(writing): unit tests for prompt-builder + reference-resolver
64 new tests across two pure-logic surfaces — no Dexie / network /
component setup, runs in <150ms. Plus the LOCAL TIER PATCH revert
that's been waiting for the release window.

prompt-builder.test.ts (39 tests):
- buildDraftPrompt: ghostwriter system + topic/length/kind plumbing,
  optional audience/tone/extra-instructions, preset style injection,
  resolved-references rendering with singular/plural Quelle wording
  and proper bookend markers.
- All five selection prompts (shorten 50–60% / expand 150–180% / tone
  with target / rewrite with instruction / translate with target lang).
- buildTitleSuggestionPrompt: 4–8-word ask, no quotes, no period, no
  prefix; with/without excerpt block.
- cleanSuggestedTitle: now iterative-until-stable so combined artefacts
  ("Titel: \"Hello World\".") collapse in one call. Quote variants
  (straight, curly, German „, French «, single ‚) all stripped via
  asymmetric open/close sets.
- estimateMaxTokens: clamping to [256, 8000], words/chars/minutes
  conversions, fallback when targetLength is null.

reference-resolver.test.ts (25 tests):
- Per-kind shaping for article (siteName-prefix, content/excerpt
  fallback, truncation marker), note (untitled fallback), library
  (book metadata in the label), url (no fetch), kontext (singleton
  via scopedForModule, deletedAt skip), goal (plaintext, no decrypt
  call asserted), me-image (label + tags descriptor, kind fallback).
- Aggregate-budget enforcement in resolveReferences: drops nulls,
  stops adding once MAX_TOTAL_REFERENCE_CHARS is exceeded, but always
  keeps the first ref even if it alone busts the cap (so a single
  large reference doesn't silently produce zero output).

Side-fix: resolver uses `||` for the article content/excerpt fallback
so empty-string content (extraction failures) falls through to the
excerpt — `??` was passing empty strings as valid.

LOCAL TIER PATCH revert: requiredTier flips from 'guest' to 'beta'
in shared-branding/mana-apps.ts. Writing now gates correctly on
release; the comment marker is removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:57:24 +02:00
Till JS
d924895de0 docs(unlisted-sharing): park M8.6-readiness check as 2026-05-09 plan-TODO
The CronCreate scheduler ignored the durable flag so the original
in-session reminder won't survive a Claude restart. Captures the
exact `psql` query to run and the decision criteria (expired-not-
cleaned > 0 → start M8.6; total = 0 → keep waiting) directly in the
plan doc instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:54:51 +02:00
Till JS
2e9ec76d60 feat(writing): token-usage in version history + draft drag-source
Two small UX wins.

Version-history shows generation cost
- VersionHistory takes a generations[] prop (DetailView already pulls
  one via useGenerationsForDraft) and looks up each AI version's linked
  Generation by id. When found, renders a monospace cost line under the
  version's wordcount: "1234 → 567 Tokens · 1.4s · ollama/gemma3:4b".
- Skips silently when the generation row isn't there (e.g. older drafts
  before the field was tracked, or a generation that was reverted).
- Lets the user see what each draft cost without digging into the
  Workbench audit timeline.

Drafts as drag source
- DraftCard wires `use:dragSource` with type='draft' + a payload
  carrying id / title / kind / content / wordCount / topic. Cards in
  the Writing list view are now drag origins for any drop target that
  declares acceptsDropFrom: ['draft'].
- App-registry entry for 'writing' gets the matching collection /
  paramKey / dragType / getDisplayData fields so the workbench layer
  treats drafts as full first-class drag-citizens (sibling navigation,
  display fallbacks).
- @mana/shared-ui DragType union extended with 'draft'.

No drop-target wiring yet — articles' acceptsDropFrom can pick up
'draft' as a follow-up, but the M10 ExportMenu's "Als Artikel
speichern" already covers that flow from the editor side.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:31:29 +02:00
Till JS
b7a54ccd10 feat(unlisted-sharing): QR code + per-link expiry picker (M8.5)
SharedLinkControls now renders a lazy QR code (qrcode npm) and a
datetime-local "Läuft ab" picker. Both stay in sync with the active
URL — regenerating the link rebuilds the QR; clearing the expiry
re-publishes with no `expiresAt`.

Wired across all three unlisted collections:
- Calendar: LocalEvent.unlistedExpiresAt + setUnlistedExpiry +
  preserve-on-refresh + clear-on-flip; both Workbench DetailView and
  EventDetailModal pass expiresAt+onExpiryChange to SharedLinkControls.
- Library: same pattern in libraryEntriesStore + DetailView.
- Places: same pattern in placesStore + DetailView.

setVisibility clears any prior expiry so a flip-away-flip-back gets
a fresh "never expires" link. refreshUnlistedSnapshot and
regenerateUnlistedToken preserve the existing expiry so a content
edit or token rotation never silently extends a link's lifetime.

The qrcode dep ships as a regular `dependencies` entry on
@mana/shared-privacy so any consuming app picks it up via the
workspace.

Note: an unrelated svelte-check error in writing/components/DraftCard
("draft" not assignable to DragType) exists from a parallel session
and is not introduced by this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:29:53 +02:00
Till JS
85fca7ccdc feat(comic): workbench app-registry — Comic als Card verfügbar
Comic war bisher nur als Standalone-Route /comic, /comic/new,
/comic/[id] erreichbar. Damit konnte man die App nicht via
AppPagePicker zur Workbench-Szene hinzufügen, das Comic-Autor-
Persona-Template (das `comic` in scene.openApps führt) hatte
keinen funktionierenden Card-Loader, und Cross-Module-Drops auf
das Comic-Modul gingen ins Leere.

Registrierung in app-registry/apps.ts:
- id: 'comic', name: 'Comic', color: '#f97316' (Orange-Rot
  Gradient analog mana-apps.ts)
- icon: FilmStrip — sequentielle Panels = Filmstreifen, passt zur
  Comic-Strip-Metapher und unterscheidet sich von Library (Stack)
  und Picture (Image)
- views.list: dynamic import von $lib/modules/comic/ListView.svelte
  (das Modul-Root mit Story-Grid + "+ Neue Story"-CTA)
- contextMenuActions: "Neue Story" → /comic/new (gleiche Pattern
  wie website's "Neue Website" — Hard-Navigation, weil der Create-
  Flow eine eigene Route ist)
- Detail-View nicht im Workbench-Slot wired — wie library/writing/
  wardrobe nutzt comic SvelteKit-Routes für Story-Detail, der
  Workbench-Card hostet nur die Liste

Help-Content-Eintrag (help-content.ts) für das ?-Icon im Card-
Header: Beschreibung der drei Generate-Modi, fünf Stil-Presets,
Modell-Auswahl (gpt-image-2 / Nano Banana), Cross-Modul-
Storyboard, MCP-Tools, Tipps zu Face-Ref + englischer Sprache +
Style-Fix-after-Create + 8-Panel-Konsistenz-Limit.

Comic-spezifische Files type-checken sauber. Die 240 Workspace-
weiten Errors sind pre-existing (laufender useLiveQueryWithDefault
→ useScopedLiveQuery Refactor).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:25:48 +02:00
Till JS
a80e8f57a0 feat(writing): auto-title suggestion in the briefing
The plan's open question on title-handling — "Topic = initialer Titel,
beim ersten Generate Auto-Title-Vorschlag anbieten" — answered with a
small -button next to the title input.

- prompt-builder: buildTitleSuggestionPrompt(input) returns a system+
  user pair that asks for a single 4–8-word title in the briefing's
  language. System prompt is strict: no quotes, no period, no "Titel:"
  prefix, no Markdown — so the result drops cleanly into the input
  field. cleanSuggestedTitle() strips wrapping quotes (straight + curly,
  single + double + German „"), a "Titel:" prefix, and a trailing
  period as a defense-in-depth pass.
- BriefingForm: -button next to the title input, disabled until the
  topic field has content (the suggestion needs context). On click it
  calls callWritingGeneration with the title prompt + temperature 0.6
  + maxTokens 60. In edit mode it pulls an excerpt of the current
  version (up to 800 chars) so the title hugs the actual prose, not
  just the briefing.
- The button shows a "…" while running and an error inline if the
  call fails — non-blocking, the user can still type their own title
  and save.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:16:45 +02:00
Till JS
636138b2d4 refactor(scope): replace _scopeCursor bridge with reactive useScopedLiveQuery hook
The previous fix wired Dexie's `_scopeCursor` infra-table as a side-
channel between Svelte $state (active-space + current-user) and Dexie
liveQuery: every scoped query touched the table on read so liveQuery
subscribed to it, every setActiveSpace bumped the table so liveQuery
re-ran. Worked, but smelled — hidden side-effect inside `scopedTable`,
scope state pretending to be a Dexie row, +1 roundtrip per query, and
`current-user.ts` had to dynamic-import Dexie just to pump the bridge.

Replacement: a Svelte 5 `$effect`-based hook that owns scope-tracking
explicitly. The dependency now lives in the reactive layer (which is
where it belongs), not as a side-effect in the data layer.

What changes:

- New `data/scope/use-scoped-live-query.svelte.ts`. The hook reads a
  module-level `scopeTick` `$state` counter inside its `$effect`.
  Both `onActiveSpaceChanged` (existing) and `onCurrentUserChanged`
  (new, added to `current-user.ts`) bump the tick on real changes.
  Effect re-fires → previous Dexie subscription unsubscribes → fresh
  one created with up-to-date `getInScopeSpaceIds()`. Same return
  shape as `useLiveQueryWithDefault` for drop-in migration.

- `current-user.ts` gains an `onCurrentUserChanged` event bus,
  symmetric to `active-space.svelte.ts#onActiveSpaceChanged`. Stays
  a plain `.ts` (no runes) so it remains a leaf and works in the
  test runner without the Svelte preprocessor — the rename to
  `.svelte.ts` was tried earlier and reverted because of test
  fallout (commit `01e6b9f04`).

- 53 module `queries.ts` files migrated from
  `useLiveQueryWithDefault` → `useScopedLiveQuery`. The choice of
  hook now documents at the call-site whether the query is
  scope-aware. Pure mechanical find-replace — no logic changes.

What goes away:

- `data/scope/cursor.ts` deleted.
- `touchScopeCursor()` calls removed from `scopedTable` /
  `scopedAnd` / `scopedGet`. Functions are pure data-layer again,
  no implicit reactive subscriptions.
- `bumpScopeCursor()` calls removed from `setActiveSpace` and both
  `loadActiveSpace` branches. Setter is pure state-update, no
  Dexie write side-effect.
- `current-user.ts` no longer dynamic-imports `scope/cursor` —
  `setCurrentUserId` is a clean three-line setter again.
- Dexie v46 drops the `_scopeCursor` table (`stores: { _scopeCursor:
  null }`). v45 stays declared so existing browsers' version chain
  remains contiguous; the v46 deletion runs once on next open. No
  user data lost — the table only ever held a transient bumpedAt
  row.

Existing 14 scope regression tests still pass. Type-check + theme-
token validators are clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:15:22 +02:00
Till JS
167d616cf7 feat(library,places): M8.4 — extend unlisted-share to two more modules
Calendar pilot proved the pattern in M8.3; this rolls it out to
Library entries and Places using the same backbone (resolvers
dispatcher, share-route SSR, SharedLinkControls UI).

Changes:
- lib/data/unlisted/resolvers:
    buildLibraryEntryBlob (whitelist: title, kind, creators, year,
      coverUrl, rating). Review, status, tags, progress, externalIds,
      reading-habit fields all stay private.
    buildPlaceBlob (whitelist: name, address, category). Lat/lng
      explicitly NOT inlined — 10m precision identifies homes /
      workplaces; the v1 share page renders no map. v1.1 may add an
      opt-in toggle.
    Dispatcher gains 'libraryEntries' + 'places' cases.
- modules/library: LibraryEntry gains unlistedToken; converter +
  ListView mock-stub forward it; entries store gets the same publish/
  revoke/refresh/regenerate quartet from M8.3:
    - setVisibility coordinates with mana-api server-side; failure
      aborts the local flip so Dexie + server stay aligned
    - deleteEntry revokes the active snapshot before tombstoning
    - updateEntry fire-and-forgets refreshUnlistedSnapshot so the
      shared link tracks edits to the whitelisted fields
    - regenerateUnlistedToken: revoke + republish, returns new token
- modules/library/views/DetailView: SharedLinkControls dropped into
  the existing dl as a labeled dt/dd row, only when visibility ===
  'unlisted' AND unlistedToken AND shareUrl.
- modules/library/SharedLibraryEntryView: standalone public render —
  big cover image, title, creators · year, optional rating-stars,
  OG/Twitter meta tags with cover as og:image (link-preview shows
  the cover on WhatsApp/Slack/iMessage).
- modules/places: same pattern. Place gains unlistedToken; converter
  + store get publish/revoke/refresh/regenerate; DetailView field-row
  for the SharedLinkControls.
- modules/places/SharedPlaceView: standalone public render — name,
  address, category badge, "Auf OpenStreetMap suchen"-Link (no map
  iframe in v1 because lat/lng aren't in the blob).
- routes/share/[token]/+page.svelte: dispatcher gains two more cases.

Verified:
- pnpm check (web): 7543 files, 0 errors, 0 warnings (svelte-check
  passes on all the new components, hooks, types)

Tests: vitest currently fails to load due to an unrelated parallel
edit on $lib/data/current-user.ts that uses $state in a non-.svelte.ts
file. The break predates this commit and isn't surfaced by
svelte-check; the visibility-system + unlisted code itself
type-checks clean. Will be fixed by whoever's currently iterating
on current-user (separate session).

Next: M8.5 — QR codes via the qrcode npm package, expiry-datepicker
wiring, regenerate confirm-dialog polish, and end-to-end
incognito-tab smoke test of all three modules' share links.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:13:36 +02:00
Till JS
bad935c258 feat(writing): print CSS + keyboard shortcuts
Two DetailView polish items.

Print / PDF (fixes M10's "Drucken / PDF" action):
- New <article class="print-target"> at the top of the route renders
  just the title + current version content. Hidden on screen, only
  visible under @media print so window.print() produces a clean
  manuscript instead of dumping the whole workbench chrome.
- :global(body > *) toggle suppresses the surrounding SvelteKit /
  workbench frame; the .shell + the per-card chrome are explicitly
  display:none in print. @page margin: 2cm gives a readable page
  with no further user setup.
- Body uses ui-serif so the printed prose looks like manuscript.

Keyboard shortcuts (DetailView document-level listener):
- ⌘G / Ctrl+G       → generate / re-generate (was: only the button)
- ⌘⇧S / Ctrl+Shift+S → save checkpoint
- ⌘Z / Ctrl+Z        → undo last refinement (only fires when refineUndo
                       is set; otherwise falls through to the textarea's
                       native undo as the user expects)

Buttons + the undo row carry the shortcut in their title attribute so
mouse-users discover them via tooltip.

i18n baseline +1 for DetailView (the new "(⌘Z)" tooltip suffix counts
as one additional German fragment per the validator's heuristic).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:10:32 +02:00
Till JS
7e6fb5b6d1 docs: surface i18n validator stack + format helper convention
- CLAUDE.md: validate:all section now lists every individual check
  (turbo / pg-schema / theme-{variables,utilities,parity} /
  i18n-{parity,hardcoded,keys} / crypto / encrypted-tools) instead of
  the stale "three invariant checks" line.
- .claude/guidelines/sveltekit-web.md: new "i18n" section with the
  hardcoded-strings rule (use $_), the parity/missing-key gates, and
  the formatDate/formatNumber/getDateFnsLocale convention vs.
  toLocaleDateString('de-DE').
- .claude/GUIDELINES.md: index gains "i18n" tag for the SvelteKit page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:07:35 +02:00
Till JS
01e6b9f044 fix(scope): undo accidental current-user.svelte.ts rename
A stray rename of current-user.ts → current-user.svelte.ts got swept
into the previous writing M5-expansion commit by lint-staged's stash/
restore step. The migration to a runes-bearing .svelte.ts variant
hadn't yet been completed (34 importers still use the old path), so the
result was a broken build until those importers were updated.

Renames the file back to current-user.ts and switches the one import
that had been pre-emptively rewritten (use-scoped-live-query.svelte.ts)
back to the bare path. The runes migration can land cleanly later.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 11:46:50 +02:00
Till JS
26e25b7694 feat(writing): M5 expansion — kontext, goal, me-image references
Three more reference kinds the resolver previously stubbed out are now
fully wired through the briefing form into the LLM prompt.

- reference-resolver: three new resolveX functions.
  * Kontext is a singleton per space (the picker uses a sentinel
    targetId; the resolver ignores it and reads via scopedForModule
    + first non-deleted row). Decrypts content and trims to budget.
  * Goal reads from companionGoals (plaintext today) and surfaces
    title + description + status + current/target so the model can
    tie the draft into the user's actual progress.
  * MeImage reads from the space-scoped meImages table; encrypts
    label + tags. Hands the model a textual descriptor (kind / label /
    tags) since the binary blob can't help prose generation.

- ReferencePicker: three new kind-tabs (🗂 Kontext, 🎯 Ziel, 🖼 Bild).
  Kontext renders as a single-click "Kontext-Dokument verknüpfen" entry
  if the space has one (with /kontext deep-link otherwise). Goals
  active-first, then archived/done. Me-images render with thumbnail +
  label + tags. Live-resolved chips via labelFor() for all three.

- i18n baseline bumped by one for ReferencePicker (the new
  "Kontext-Dokument verknüpfen" string is intentional, in line with
  the rest of the picker's existing German labels).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 11:43:12 +02:00
Till JS
fbbadc91f0 feat(calendar): M8.3 — calendar pilot for unlisted-share end-to-end
Wires Calendar through the M8.1+M8.2 backbone: flipping an event to
'unlisted' now publishes a server-side snapshot, the visible link in
the DetailView/EventDetailModal opens a real /share/[token] page, and
recipients can download an .ics file for their own calendar.

Changes:
- lib/data/unlisted/resolvers.ts (new):
    buildUnlistedBlob(collection, recordId) dispatcher.
    buildEventBlob: load LocalEvent + linked TimeBlock, decrypt
    client-side, return { title, location, startTime, endTime,
    isAllDay, timezone }. Description, reminders, tagIds, calendarId,
    color stay out of the blob — sensitive context the user didn't
    consent to share by flipping a single flag.
- modules/calendar/types: CalendarEvent gains `unlistedToken: string`
  (empty string when no active token). timeBlockToCalendarEvent
  forwards from LocalEvent. Draft-event scaffold initializes empty.
- modules/calendar/stores/events:
    setVisibility now coordinates with mana-api. Flip-to-unlisted:
      build blob -> publishUnlistedSnapshot -> store server-issued
      token in patch.unlistedToken -> commit local update. If the
      server call fails, no local change happens (no drift).
    Flip-from-unlisted: revoke server snapshot first, then clear
      local token + commit visibility change.
    deleteEvent: revoke active unlisted snapshot before tombstoning,
      so the share-link dies in lock-step with the local delete.
    updateEvent + updateSingleInstance fire-and-forget
      refreshUnlistedSnapshot(id) so the published blob tracks any
      whitelist-field edits. Failures log; the next successful
      refresh heals.
    New regenerateUnlistedToken(id): revoke + republish in one call,
      returns the fresh token. Powers the "Neu erzeugen" UI.
- routes/share/[token]/+layout.svelte: minimal anonymous chrome —
  no app nav, no auth, no Dexie. Light/dark via prefers-color-scheme.
  Footer carries "Geteilt via Mana" + signup CTA.
- routes/share/[token]/+page.server.ts: SSR loader. Fetches
  /api/v1/unlisted/public/:token, dispatches 404/410 cleanly,
  sets Cache-Control: private, max-age=60 + X-Robots-Tag: noindex.
- routes/share/[token]/+page.svelte: dispatcher; renders
  SharedEventView for collection='events', stub message otherwise.
- modules/calendar/SharedEventView.svelte: standalone public render —
  big date, location, "Zum eigenen Kalender hinzufügen" .ics link,
  optional expiry note. OG/Twitter meta tags for WhatsApp/Slack
  preview embedding. Uses $derived everywhere so prop updates
  propagate through reactive recompute.
- routes/share/[token]/ical/+server.ts: RFC 5545 builder. No npm
  library — small enough to inline. Escapes per spec, CRLF endings,
  DTSTART/DTEND swap between VALUE=DATE and UTC depending on isAllDay.
  Wrong-collection requests get 400.
- modules/calendar/views/DetailView (Workbench) + components/
  EventDetailModal (/calendar route): SharedLinkControls dropped in
  below the visibility row when event.visibility === 'unlisted'
  AND event.unlistedToken AND shareUrl computed. The URL is built
  client-side via buildShareUrl(window.location.origin, token) so it
  stays in sync with whichever host the editor is open on.

Verified:
- pnpm check (web): 7541 files, 0 errors, 0 warnings
- pnpm test calendar + website: 26/26
- typecheck of new resolver, store hooks, SSR loader, iCal builder

Manual test path:
1. Open /calendar event in Detail view, flip Sichtbarkeit -> "Per Link"
2. Server publishes snapshot, Dexie record gets the server token
3. SharedLinkControls appear with copy + regenerate + revoke buttons
4. Open the URL in incognito → SSR fetches snapshot, renders
   SharedEventView with date / location / .ics download
5. Edit the event title back in the main app → snapshot auto-refreshes
   (refreshUnlistedSnapshot fires after updateEvent succeeds)
6. Flip back to "Bereich" → snapshot revoked server-side; subsequent
   incognito reloads return 410 Gone

Next: M8.4 — same wiring for Library + Places. Uses the same
infra (resolvers dispatcher, share dispatcher) — just adds two new
buildXBlob functions, two SharedXView components, and the store
hooks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 11:40:53 +02:00
Till JS
8b9fbd2e1c feat(scripts): validate:i18n-hardcoded — ratcheting baseline check
Stoppt das Wachsen des 1877-String-Backlogs hardgecodeter deutscher
User-facing Strings in .svelte Files. Per-file Count vs. committed
Baseline; Datei darf NIE über ihrer Baseline liegen, neue Files müssen
mit 0 Verstößen starten.

- Erkennt: placeholder/title/aria-label/label/alt mit Umlauten,
  Text-Content `>Großbuchstabe…<` (ohne Interpolation).
- Aktuelle Baseline: 1877 Verstöße in 428 Files; jeder Fix ratchet't
  den erlaubten Wert nach unten.
- Lokales Update nach gewolltem Wachstum: `pnpm run validate:i18n-hardcoded -- --update`.
- In validate:all + CI verdrahtet.
- Drift-Test bestätigt: ein zusätzlicher umlaut-Placeholder lässt die
  Datei "2 (was 1, +1)" failen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 11:33:24 +02:00
Till JS
d8feef1149 fix(scope): bridge active-space / user changes to Dexie liveQuery
Modules mounted before the active-space bootstrap finished rendered
empty on first paint and stayed empty until some unrelated Dexie
write happened to wake the querier up. Reproducible in wardrobe:
open the module, the face-ref banner stays visible (even though
face-ref is set), the garment grid is empty; create a new garment
and suddenly the existing ones appear alongside it.

Root cause: Dexie's `liveQuery` only re-runs when a Dexie table it
read during evaluation is written to. `getInScopeSpaceIds()` reads
from plain Svelte `$state` (active-space.svelte.ts `active` +
current-user.ts `currentUserId`), which is invisible to Dexie's
change tracker. So:

1. User opens /wardrobe.
2. First querier runs. getActiveSpaceId() is still null (bootstrap
   hasn't resolved yet). getInScopeSpaceIds() returns
   [`_personal:guest`].
3. Existing rows are stamped `_personal:<userId>` or a real
   space-id — no match. Filter emits [].
4. Bootstrap resolves, setActiveSpace fires, `active = realSpace`.
   Svelte $state assignment: invisible to Dexie.
5. liveQuery never re-runs; user sees empty until a subsequent
   Dexie write forces a re-evaluation.

Fix: add `_scopeCursor`, a single-row infra table, as a Dexie
proxy for the scope-state signal.

- `data/scope/cursor.ts`: new module with two exports.
  `bumpScopeCursor()` writes `{id:'active', bumpedAt}` to the
  cursor. `touchScopeCursor()` reads from it, fire-and-forget —
  the Dexie read registers the liveQuery subscription during
  querier execution.
- Dexie v45 registers `_scopeCursor: 'id'`. NOT in SYNC_APP_MAP:
  it's a client-side liveness signal, not user data, so the
  creating-hook loop ignores it and no pending-change rows are
  generated.
- `scopedTable` / `scopedGet` / `scopedAnd` in `scoped-db.ts` call
  `touchScopeCursor()` on every invocation. Since those run inside
  each querier's body, liveQuery picks up the subscription for free.
- `setActiveSpace` (direct + both loadActiveSpace branches) calls
  `bumpScopeCursor()` after the `$state` update.
- `setCurrentUserId` in current-user.ts calls `bumpScopeCursor()`
  via dynamic import so the module stays leaf-level (no eager
  Dexie dep in test envs that mock fake-indexeddb differently).

Net effect: every scope change triggers one Dexie write to an
infra table, every liveQuery that went through `scopedForModule`
sees the write and re-evaluates with the fresh
`getInScopeSpaceIds()`. The first-mount race that left wardrobe
stuck on an empty list is gone.

Existing 14 scope regression tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 11:32:32 +02:00
Till JS
3f438cf882 refactor(i18n): replace hand-maintained locale list with import.meta.glob
Bisher war für jeden Namespace ein eigener `import(\`./locales/X/\${lang}.json\`)`
Eintrag in index.ts + ein spiegelndes Destructuring — 35 Namespaces,
zwei hand-synchronisierte Listen, jeder neue Namespace erforderte Edits.
Leicht zu vergessen, lautlos fehlerhaft.

Vite's import.meta.glob übernimmt das jetzt: alle ./locales/*/*.json
werden automatisch für den gewünschten Lang registriert, Namespace aus
dem Pfad abgeleitet. Neuer Namespace = JSON-Folder droppen, Ende.

- index.ts: 118 Zeilen → 15 Zeilen.
- svelte-check 0/0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 17:21:43 +02:00
Till JS
e794e0ca57 feat(i18n): locale-aware formatters, migrate hardcoded de-DE call-sites
Bisher pinnten 185+ call-sites die UI-Zahlen/Datumsausgabe hart auf
Deutsch ("de" / "de-DE" an toLocaleDate{,Time}String, Intl.*Format,
date-fns locale), unabhängig von der aktiven Sprache. EN-User sahen
dadurch deutsche Datums-/Zahlenformate mitten im englischen UI.

- $lib/i18n/format.ts (neu): formatDate / formatTime / formatDateTime
  / formatNumber / formatCurrency / getDateFnsLocale. Alle lesen die
  aktive Locale aus svelte-i18n's locale-store und mappen de→de-DE
  etc. für Intl.
- Codemod: 119 Direktaufrufe in 79 Files migriert (.toLocaleDateString
  / .toLocaleTimeString / new Intl.{Number,DateTime}Format). Erkennt
  new Date() / Number()-Receiver zum Disambiguieren von
  .toLocaleString('de-DE').
- date-fns: 19 Files auf getDateFnsLocale() umgestellt; hardcoded
  `import { de } from 'date-fns/locale'` entfernt.
- Skipped (Collision): 14 Files hatten lokale format*-Wrapper; diese
  bleiben vorerst als gesonderte Folge-Refactorings stehen. Ca. 58
  deep-gekapselte Aufrufe im .toLocaleString/.toLocaleDateString-Idiom
  sind über diese Wrapper noch zu migrieren.
- svelte-check: 0 Errors / 0 Warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 17:20:11 +02:00
Till JS
364522db87 feat(comic): image-model picker — OpenAI + Nano Banana wählbar
Comic nutzte bisher 'openai/gpt-image-2' hartcodiert auf drei Ebenen
(generate-panel.ts, comic.generatePanel MCP-Tool, generate_comic_panel
AI-Tool). Wardrobe hat seit dem Nano-Banana-Commit einen
TryOnModelPicker mit drei Optionen — Comic spiegelt das jetzt 1:1.

Wählbar in allen drei Editoren (PanelEditor, BatchPanelEditor,
StoryboardSuggester):
- openai/gpt-image-2 (Default) — OpenAI GPT-image Standard
- google/gemini-3-pro-image-preview — Nano Banana Pro, hohe
  Konsistenz, teurer
- google/gemini-3.1-flash-image-preview — Nano Banana 2, neuestes,
  schnell, günstig

Implementierung:
- api/generate-panel.ts: PanelModel Union + DEFAULT_PANEL_MODEL +
  model? Param auf RunPanelGenerateParams + im HTTP-Body
  weitergereicht (vorher hart 'openai/gpt-image-2').
- components/PanelModelPicker.svelte: neue Komponente, Stil/Markup
  identisch zu TryOnModelPicker für Muskel-Memory über beide Flows.
- components/PanelEditor.svelte: `let model = $state(DEFAULT_PANEL_MODEL)`
  + Picker oberhalb der Qualität-/Format-Leiste + model im
  runPanelGenerate-Call.
- components/BatchPanelEditor.svelte: gleiche Änderung — ein Model
  pro Batch (nicht pro Row) damit der Batch konsistent rendert.
- components/StoryboardSuggester.svelte: gleiches Pattern; der
  Picker landet zwischen "Panel manuell"-Button und dem
  Qualität/Format-Block.
- packages/mana-tool-registry/src/modules/comic.ts: generatePanel
  Input-Schema bekommt model mit zod.enum() + default; im Body
  wird input.model durchgereicht.
- packages/shared-ai/src/tools/schemas.ts: generate_comic_panel
  bekommt Parameter 'model' optional mit gleicher Enum-Liste.
- apps/mana/apps/web/src/lib/modules/comic/tools.ts: isValidModel
  Guard + Parameter-Validierung; model an runPanelGenerate.

Keine Story-Level-Persistierung — model bleibt lokaler State pro
Editor-Mount. Eine model-Spalte auf comicStories würde Migration
brauchen und die Wahl ist eh ad-hoc pro Panel/Batch.

Plan-Doc (§2.1) dokumentiert die Entscheidung + die drei Optionen.

107 shared-ai tests weiter grün. check + validate:all clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 17:19:40 +02:00
Till JS
5501f472ae feat(shared-privacy): M8.2 — unlisted-client + SharedLinkControls
Second milestone of the unlisted-share rollout. Backend endpoints
from M8.1 are now callable from the client, and a reusable
SharedLinkControls component is available for the detail views that
wire up in M8.3/M8.4.

Scope: shared primitives only. No module store integrates them yet —
that's the next step per module.

Changes:
- @mana/shared-privacy/unlisted-client.ts:
    publishUnlistedSnapshot(opts) → { token, url }
      Idempotent per (collection, recordId) — server reuses token on
      re-publish, so store code can call on every edit without caring
      whether it's first publish or refresh.
    revokeUnlistedSnapshot(opts)
      Idempotent — resolves silently even on { revoked: 0 }.
    buildShareUrl(origin, token)
      Convenience for UIs that already know the token.
    UnlistedApiError
      Thrown on non-2xx. Carries { status, code } so callers can
      distinguish 400 COLLECTION_NOT_ALLOWED vs 410 REVOKED vs
      500 UNKNOWN.
- @mana/shared-privacy/SharedLinkControls.svelte:
    Dumb presentational component. Props: token, url, expiresAt,
    onRegenerate, onRevoke, onExpiryChange (optional), disabled.
    Renders URL + copy, regenerate with confirm dialog, revoke,
    optional datetime-local expiry picker, debug token fingerprint.
    Clipboard-API fallback to prompt() for unsecure origins.
    QR-code button deferred to M8.5 polish.
- Exports added to index.ts: functions, error class, both types,
  SharedLinkControls component.
- 10 new unit tests (25 total): publish URL shape, headers, body,
  expiresAt serialisation, 4xx/5xx handling, trailing-slash
  trimming on apiUrl, revoke idempotence, buildShareUrl join.

Verified:
- pnpm --filter @mana/shared-privacy test: 25/25 green
- pnpm --filter @mana/shared-privacy check: 0 errors
- pnpm --filter @mana/web check: 7531 files, 0 errors

Next: M8.3 — wire Calendar through the new client.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 17:18:56 +02:00
Till JS
c1d643ffb5 feat(writing): register Writing as a Workbench card
Writing is now pickable from the Workbench AppPagePicker and can run
side-by-side with the other module cards. Previously only reachable via
direct navigation to /writing.

- app-registry: registerApp({ id: 'writing', icon: NotePencil, color:
  '#0ea5e9', list: ListView }) with a "Neuer Draft" context-menu action
  that dispatches mana:quick-action (same convention as library/notes/
  quiz — the event channel is the standard cross-card hook).
- writing ListView: onMount listener for the mana:quick-action event
  flips showCreate=true when { app:'writing', action:'new' } fires, so
  the kebab menu → "Neuer Draft" opens the inline BriefingForm instead
  of being a no-op.

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