Commit graph

767 commits

Author SHA1 Message Date
Till JS
58c057f6c5 feat(cards-web): Phase δ.2 — Subscribe + initial pull
End-to-end subscribe flow on cards.mana.how. From a public deck page
the user can now pull the deck into their own Cards instance with
one click; subscribed decks live alongside own decks but carry a
`subscribedFromSlug` marker so the editor knows to hide mutate
controls (UI gating in δ.3).

  - cards-core types: LocalDeck gains subscribedFromSlug +
    subscribedAtVersion. LocalCard gains serverContentHash. Both
    optional — own decks/cards are unaffected.
  - data/database.ts: Dexie v2 adds index on cardDecks.subscribedFromSlug
    so the lookup-by-slug path is O(1).
  - lib/api/cards-api.ts: subscriptions.{list,subscribe,unsubscribe,
    version,diff} + the SubscriptionInfo / ServerCard / DeckVersionPayload
    / DiffPayload types.
  - lib/services/subscribe.ts: subscribeAndPull() sequences server
    POST /subscribe → GET /decks/:slug → GET /versions/:semver →
    create LocalDeck + LocalCards + ensure FSRS reviews. Re-pull
    refreshes in place (Phase δ.3 will swap to real diff-apply that
    keeps FSRS state). unsubscribe() soft-deletes the local mirror.
    isSubscribedLocally() backs the deck-page state check.
  - routes/d/[slug]/+page.svelte: full subscribe UI — Abonnieren →
    Abonniert + Lernen-Button (deep-links into the existing learn
    session route).

Validated: svelte-check 0/0, vite build green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 19:56:48 +02:00
Till JS
daa1ef0513 feat(cards): image / audio attachments on cards via mana-media
Cards can now carry image, audio, and video attachments uploaded to
mana-media (the existing CAS service that already powers picture,
photos, wardrobe, etc.).

Pipeline:
  • lib/media/upload.ts wraps POST /api/v1/media/upload (multipart,
    app=cards). Returns { id, url, kind } with the right variant URL
    per kind (medium for images, full file for audio/video). 25 MB
    cap matches the website-upload pattern.
  • mediaToFieldSnippet(): drops Markdown ![]() for images; raw
    <audio>/<video controls> for the others — the user can later
    tweak attributes by hand.
  • Deck-detail card editor gains a "📎 Anhang" button next to every
    text field (front/back/cloze). Pick → upload → snippet appended
    to the field's content. Loading + error states surfaced inline.

Render:
  • @mana/cards-core/render.ts whitelists `audio`, `source`, `video`
    plus the `controls`/`preload`/`src`/`type` attrs in DOMPurify so
    inline media survives sanitization. Markdown's <img> already
    passed through the default policy.

Wiring:
  • hooks.server.ts injects __PUBLIC_MANA_MEDIA_URL__.
  • compose adds PUBLIC_MANA_MEDIA_URL_CLIENT=https://media.mana.how
    to cards-web.

Phase 2 ideas: drag-drop directly into the textarea, paste-from-
clipboard for screenshots, mana-media auth scoping per user, Anki
import bringing media files along.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 13:52:53 +02:00
Till JS
f422fd6779 fix(shared-error-tracking): point main at src/, strip dashes from Glitchtip DSN
Two real-world fixes from wiring mana-auth to Glitchtip:

1. The compiled dist/ folder was excluded from Docker builds via
   .dockerignore's '**/dist' rule, so any container that pnpm-installed
   the package found node_modules/@mana/shared-error-tracking but no
   loadable entry point ('Cannot find module' at startup). Match the
   pattern shared-hono uses — point main + types + exports straight at
   src/*.ts. Bun runs TS natively and the type-only consumers don't
   care.

2. Glitchtip projects expose UUID-format public keys (`556fbd2e-a720-…`)
   in their generated DSNs. @sentry/node v9 tightened its DSN regex to
   alphanumeric-only, so it silently rejects the DSN with "Invalid
   Sentry Dsn" and never sends events. Strip the dashes from the
   user/key portion before handing it to Sentry — the Glitchtip ingest
   endpoint accepts both forms over the wire, so no server change.

Plus the missing Dockerfile COPY lines for shared-error-tracking and
eslint-config (root package.json devDeps reference the latter, which
breaks pnpm-filter installs that don't include it in the build context).

Verified end-to-end: 4 issues now in Glitchtip from mana-auth
(2 manual probes + 1 captureException + 1 401 from a
real /api/v1/me/data request without auth).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 02:34:54 +02:00
Till JS
f94c047daa chore: silence pre-existing svelte-check a11y warnings
Pre-push hook runs svelte-check with --fail-on-warnings; nine
long-standing warnings in unrelated files (forms / website-blocks)
were blocking otherwise-clean pushes.

Each <label> here is a visual label whose control follows on the next
line — accessible to a screen reader through proximity but not through
a `for=`/`id` association. The state_referenced_locally cases capture
a prop on first render by design (re-running the hook on prop change
would be a different feature). The <nav role=tablist> is the existing
tab-strip semantic.

All seven sites get scoped svelte-ignore comments rather than functional
rewrites — the goal is to unblock CI, not redesign these components.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 01:34:36 +02:00
Till JS
0a544ac410 feat(cards): Phase-1 Spinoff — standalone cards.mana.how + cards-core extraction
Builds out the Cards spinoff end-to-end so the standalone app at
cards.mana.how shares its data layer with the in-mana cards module
through a single pure-utility package.

Why a spinoff and not just a deeper module: per the GUIDELINES, Cards
gets its own brand + URL while reusing mana-auth, mana-sync, and the
mana-credits/billing stack. The in-mana module under mana.how/cards
stays untouched as the integrated experience.

Phase 0 — mana-modul foundation
  • New tables cardReviews + cardStudyBlocks (Dexie v61) + plaintext
    classification in the crypto registry.
  • LocalCard learns a {type, fields} shape; legacy front/back columns
    kept as a back-compat mirror so older builds keep rendering.
  • FSRS v6 scheduler + Cloze parser + Markdown render pipeline.
  • UI in apps/mana/.../routes/(app)/cards/ gets a learn session
    (learn/[deckId]), 4-type card editor, due-counter, markdown lists.

Phase 1 — standalone (apps/cards/apps/web)
  • SvelteKit 2 + Svelte 5 + Tailwind 4, port 5180.
  • Own Dexie 'cards' DB with a slim 5-table schema.
  • Own sync engine: pending-changes hooks, 1 s push / 5 s pull against
    POST /sync/cards, server-apply with suppression to avoid ping-pong.
  • Auth-Gate via @mana/shared-auth-ui (LoginPage / RegisterPage).
  • Encryption hooks at every write/read/apply path, currently no-op
    stubs — flipping to real vault-backed AES-GCM is a single-file
    change in src/lib/data/crypto.ts.

Shared package — @mana/cards-core
  • Pulls types, cloze, card-reviews, FSRS wrapper, and Markdown
    renderer out of the mana module so both frontends import from one
    source. mana-modul keeps thin re-export shims so consumers don't
    need to change imports.
  • 19 vitest tests carried over from the mana module.

Server-side wiring
  • cards.mana.how added to mana-auth PRODUCTION_TRUSTED_ORIGINS and
    its CORS_ORIGINS env (sso-config.spec.ts stays green).
  • New cards-web container in docker-compose.macmini.yml (mirrors
    manavoxel-web pattern, 128m, depends on mana-auth healthy).
  • cloudflared-config.yml repoints cards.mana.how from :5000 (the
    unified mana-web container) to :5180. mana.how/cards is unchanged.

Cleanup
  • Removed an unrelated 2026-03/04 NestJS+Supabase+Expo experiment
    that was lingering under apps/cards/ (apps/landing, supabase/,
    .github/workflows, MANA_CORE_*.md, etc.). It predated this plan
    and would have confused future readers.

Validation
  • svelte-check on mana-web: 0 errors over 7697 files
  • svelte-check on cards-web: 0 errors over 3481 files
  • vitest on cards-core: 19/19 pass
  • pnpm check:crypto: 214 tables classified
  • bun test sso-config.spec.ts: 8/8 pass
  • vite build on cards-web: green

Not done in this commit (deliberate)
  • Real encryption (vault roundtrip) — Phase 2.
  • WebSocket-driven pull (5 s polling for now).
  • Mobile/landing standalone surfaces — Phase 2/3.
  • The actual production cutover on the Mac mini (build, deploy,
    cloudflared sync) — config is staged, deploy is a user action.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 01:20:43 +02:00
Till JS
795b39e065 feat(forms): M10d headless wave-cron — server-worker + private internal_meta
Echter Server-Cron für recurring forms — wave-send läuft jetzt
unabhängig von Owner-Tab-State. Bisheriger M10c webapp-side scheduler
bleibt als Belt-and-suspenders aktiv (idempotent).

Architektur:
1. **Owner-private internal_meta auf unlisted snapshots**
   - Drizzle: neue jsonb-column `internal_meta` (Drizzle migration
     0001_internal_meta.sql).
   - public-routes.ts strippt sie strukturell — die explicit select()-
     projection enthält sie nicht (recipients + sender würden sonst
     via share-link leaken).
   - publish-route akzeptiert sie im Body, persistiert auf insert +
     update.
   - ALLOWED_COLLECTIONS um 'lasts' und 'forms' erweitert (war ein
     latenter Bug — formsStore.setVisibility('unlisted') hätte ohne
     diese Ergänzung 400 zurückbekommen; M4b lief vermutlich nie
     end-to-end durch).

2. **shared-privacy publishUnlistedSnapshot**
   - PublishUnlistedOptions erweitert um optionales `internalMeta`.
     Forwarded an /api/v1/unlisted/:collection/:recordId body.

3. **Webapp formsStore**
   - lib/wave-mail.ts: buildFormInternalMeta(form, broadcastSettings)
     baut den Owner-Private-Blob: { kind, recurrence: {frequency,
     recipientEmails, lastSentAt}, sender: {fromEmail, fromName,
     replyTo, legalAddress}, formMeta: {title, description} }.
     Returns null wenn Voraussetzungen fehlen (kein recurrence, keine
     recipients, fehlende broadcast-settings).
   - stores/forms.svelte.ts: setVisibility / regenerateUnlistedToken /
     setUnlistedExpiry laden broadcastSettings via Dexie + decrypt,
     bauen internalMeta, übergeben an publishUnlistedSnapshot. Form
     wird vor dem buildFormInternalMeta-Call dekrypted.

4. **mana-mail internal bulk-send route**
   - createInternalRoutes(accountService, broadcastOrchestrator,
     maxRecipients) — Signature erweitert.
   - Neue POST /api/v1/internal/mail/bulk-send: gleicher Payload-shape
     wie user-facing /v1/mail/bulk-send aber userId aus Body statt
     JWT. X-Service-Key-gate sitzt bei /api/v1/internal/* prefix.
     Audit-trail trägt principalId aus Body. Cap = 5000 (gleicher
     Wert wie user-facing).

5. **apps/api forms wave-worker**
   - 5-min setInterval, advisory-lock-gated (key 0x464f5257 'FORW').
   - Tick: select snapshots WHERE collection='forms' AND
     internal_meta IS NOT NULL AND revoked_at IS NULL. Filter auf
     kind='forms-recurrence' + isWaveDue (lastSentAt + period <= now,
     never-sent fires sofort). Pro fälligem snapshot: build HTML/text
     mailbody (mirror webapp wave-mail-render), POST an mana-mail
     internal-bulk-send mit X-Service-Key + userId, dann jsonb_set
     auf internal_meta.recurrence.lastSentAt. Per-snapshot errors
     werden als console.warn geloggt, Tick läuft weiter.
   - Disable via FORMS_WAVE_WORKER_DISABLED=true (tests / multi-
     replica deployments).
   - Wired in apps/api/src/index.ts neben startArticleImportWorker().

Trade-offs:
- internal_meta wird beim setVisibility/regenerate/setExpiry frisch
  aus broadcast-settings gebaut — wenn der User später broadcast-
  settings ändert (zB neuer fromEmail) muss er das Form re-publishen
  damit die snapshot-internal_meta aktualisiert wird. Doc-it: zukünftiger
  Patch könnte ein "settings drift"-Warning ins UI surfacen.
- Worker-Update von lastSentAt geht NICHT zurück in den webapp-form
  (settings.recurrence.lastSentAt ist verschlüsselt, server kann
  nicht schreiben). Owner-UI zeigt ältere lastSentAt von manuellen
  Sends; auto-cron-sends sind in den Server-Logs sichtbar. Future
  patch: GET /api/v1/forms/:id/recurrence-status (auth) gibt das
  snapshot.internal_meta zurück, UI rendert Auto-Cron-State.
- Webapp-side wave-scheduler (M10c) läuft parallel weiter — wenn
  Owner-Tab offen ist, kann beides feuern. Idempotent durch
  lastSentAt-check (weekly/monthly buckets), aber theoretisch könnte
  double-fire passieren wenn die Calls innerhalb 1ms versetzt sind.
  Real-world ignorierbar; future patch: scheduler liest jetzt
  internal_meta.lastSentAt vom server-side state.

apps/api buildet (1776 modules). mana-mail buildet (523 modules).
svelte-check 0 errors in forms/. Forms-Tests 70/70 unverändert.

DB-Migration 0001_internal_meta.sql muss manuell appliziert werden
(siehe feedback memory: hand-authored SQL migrations sind nicht in
pnpm setup:db).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:18:05 +02:00
Till JS
ace1b706e6 feat(forms): M8 website-block — formEmbed bindet Mana-Formulare ein
Neuer Block-Type `formEmbed` im Website-Builder
(docs/plans/forms-module.md M8):

- @mana/website-blocks/src/formEmbed/:
  - schema.ts: FormEmbedSchema mit token (32-char base64url) +
    titleOverride + optional resolved-Block (formTitle, fields,
    branching, settings.{submitButtonLabel, successMessage}).
    FormFieldEmbedSchema duplicated leichtgewichtig statt cross-
    package import — website-blocks bleibt self-contained.
  - FormEmbed.svelte: edit/preview rendert Placeholder-Card mit
    Token-Snippet und resolved-Status; public rendert die kompletten
    11 Field-Types inkl. Live-Branching-aware-Render. Submitter-
    Block (Name+Email optional). Submit POSTet an
    /api/v1/forms/public/:token/submit. Lazy-Fallback fetcht
    /api/v1/unlisted/public/:token wenn die publish-resolver-blob
    fehlt. Bot-Honeypot bleibt M8-Polish.
  - FormEmbedInspector.svelte: Token-Input mit base64url-Validierung
    bei blur, optional titleOverride, resolved-Card mit
    Field-Count + Logik-Regel-Count.
- BLOCK_SPECS + BLOCK_SCHEMAS + BLOCK_DEFAULTS um formEmbed
  erweitert. schemas.test.ts erwartet jetzt 12 Block-Types.
- apps/mana/apps/web/src/lib/modules/website/forms-embeds.ts:
  resolveFormEmbed scant formTable nach unlistedToken (linear scan
  ist günstig bei <100 forms pro user, kein Index nötig), dekrypted,
  validiert published-status, gibt resolved-Block zurück.
- publish.ts.resolveEmbedsInTree erweitert um formEmbed-Branch — ruft
  resolveFormEmbed parallel zu resolveEmbed (moduleEmbed) im selben
  Walk.

Trade-offs:
- Token statt formId: bei Token-Rotation (M4b) muss der User den Block
  neu konfigurieren. Der formEmbed-Block-Resolver erkennt das + setzt
  resolved.error; public-Renderer fällt auf lazy-fetch zurück.
- Plaintext stored: das resolved-Blob landet als plaintext im
  public-snapshot, gleiches Trust-Modell wie moduleEmbed (öffentliche
  Website per Definition).

Tests: website-blocks 50/50 grün (12 schema-block-types + per-type
defaults validation). svelte-check 0 errors. forms 26/26 unverändert.

Use-Case: Vereins-Sommerfest. User legt /forms/anmeldung an,
publisht, setzt unlisted, kopiert Token. Im Website-Builder fügt er
einen formEmbed-Block auf der Event-Seite ein, paste Token → bei
Publish wird der Form-Schema inlined → Besucher submitten direkt
auf der Vereins-Website.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 02:38:28 +02:00
Till JS
59373c0d57 chore(articles): hygiene pass — shared-ai actor + lib/sync-db + metrics (#5,#7,#11)
#5 — SYSTEM_ARTICLES_IMPORT_WORKER hoisted into @mana/shared-ai
   The worker built its actor inline, bypassing the SystemSource union
   that's the blessed list for system-write principals. Now uses
   makeSystemActor(SYSTEM_ARTICLES_IMPORT_WORKER) like every other
   server-side system writer (mission-runner, projection, …).

#7 — sync-db helper hoisted out of mcp/ into lib/
   Implementation moved to apps/api/src/lib/sync-db.ts; mcp/sync-db.ts
   is a re-export shim so existing MCP imports keep working. Articles
   bulk-import + future modules import from lib/ directly — no more
   "articles depending on mcp" layering smell.

#11 — Prometheus metrics for the worker
   New counters + histogram in lib/metrics.ts under
   mana_api_articles_import_*:
     - ticks_total{result=processed|skipped|error}
     - items_total{result=extracted|error|consent_wall|cancelled}
     - extract_duration_seconds (histogram, 0.25–30s buckets)
     - jobs_completed_total{result=done}
     - pickup_gc_rows_total
   Worker tick + extractor instrumented at the right transition points.
   Steady-state pickup_gc_rows_total > 0 over time signals a stuck
   consumer somewhere — useful operator alert.

Plan: docs/plans/articles-bulk-import.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 01:06:15 +02:00
Till JS
93545f8516 chore: drop who + kontext MANA_APPS entries to match earlier extractions
Two cleanup follow-ups that the parallel sessions which extracted these
modules left behind, surfaced by the route-drift test added in 6d193a9fa:

who — `chore: extract who module into standalone repo` (a3eedfc87) +
follow-up cleanup (f076d9345) removed `lib/modules/who/` and the
workbench `registerApp({ id: 'who' })` block, but the broken `/who/+page`
and `/who/play/[gameId]/+page` routes still imported the deleted module
and the MANA_APPS entry, APP_ICONS icon, categories.ts mapping and
help-content block were still in place. Drop all five.

kontext — `feat(notes): isSpaceContext flag replaces kontext module
(Option B)` (8fbdc6db7) replaced the kontext module with a per-note
`isSpaceContext` flag in the notes module. The MANA_APPS entry I added
in 6d193a9fa and the matching APP_ICONS entry are now both stale —
there is no `kontext` route, no module, no registerApp. Drop them.

Verification: `registry.spec.ts` 4/4 green, `svelte-check src/lib`
0 errors / 5 warnings (pre-existing in other files).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 01:04:34 +02:00
Till JS
0d85d7c36b feat(forms): M5 AI tools — 7 tools im AI_TOOL_CATALOG
AI-Zugriff aufs Forms-Modul (docs/plans/forms-module.md M5):

Propose (User-Approval erforderlich):
- forms_create — neues Formular im Draft-Status, optional mit Feldern.
  Field-Shape im params-Array: { type, label, required?, helpText?,
  options?: [{label}] }. Type-Enum aus dem 11-Typ-Katalog. Planner
  kann z.B. "Vereins-Anmeldung" mit Name+Email+Position+Trikotgröße
  in einem Aufruf bauen.
- forms_add_field — Feld ans Ende anhängen, Reorder bleibt User
  vorbehalten (Drag im Builder).
- forms_publish — draft → published. Wirft, wenn Form keine Antwort-
  felder hat (nur section/consent würde Public-Submit sinnlos machen).
- forms_close — published → closed, Antworten + Share-Link bleiben.

Auto (silent execution während Planner-Reasoning):
- forms_list — Metadaten (id, title, status, fieldCount, responseCount,
  visibility), Status-Filter optional, Default-Limit 50. VaultLocked-
  aware → klare Fehlermeldung statt Crash.
- forms_get_responses — Aggregat-Stats: per Form ein
  ResponseAggregate {totalCount, statusCounts, choiceHistograms,
  textSamples, numericStats}. Choice-Felder mit Option-Label-Mapping
  (nicht Option-IDs), Text-Felder als Sample-Array (cap 50, default).
- forms_summarize_responses — gleicher Aggregator mit window-filter
  (sinceDays) und höherem Sample-Cap (200), als Daten-Vorlage für
  LLM-Clustering im nächsten Planner-Schritt. Augur-style: keine
  eigene LLM-Roundtrip, der Planner formuliert Themes selbst.

Verdrahtung:
- AI_TOOL_CATALOG in @mana/shared-ai mit 7 ToolSchema-Einträgen +
  defaultPolicy.
- ModuleTool-Implementierungen in modules/forms/tools.ts mit
  scopedForModule für Space-Awareness, decryptRecords für encrypted-
  table-Reads, VaultLocked-Handling.
- Registriert in data/tools/init.ts.

Validierung:
- mana-ai planner-drift test: 4/4 grün — alle 4 propose-Tools
  (forms_create/add_field/publish/close) im SERVER_TOOLS-Subset.
- svelte-check 0 errors in forms/.
- Forms unit tests: 16/16 (csv + branching) unverändert grün.

Tools-executor.test.ts ist pre-existing rot wegen
$lib/modules/context-Drift in module-registry.ts (Parallel-Session-
WIP, nicht durch mich).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:33:55 +02:00
Till JS
a295894ca6 chore: drop legacy context module files (companion to acb737e25)
Companion deletion sweep — acb737e25 removed all the *registry refs*
to the legacy `context` module, but its source files were still on
disk on main (because the original deletion in d3e2e73ca on the
articles-bulk-import branch was bundled with unrelated photon /
broadcast-rename work and never landed on main). Dropping them now
so the consolidation is self-contained:

- apps/mana/apps/web/src/lib/modules/context/ — entire module dir
- apps/mana/apps/web/src/routes/(app)/context/ — page routes
- apps/mana/apps/web/src/lib/components/dashboard/widgets/ContextDocsWidget.svelte
- apps/mana/apps/web/src/lib/i18n/locales/context/{de,en,es,fr,it}.json
- packages/shared-branding/src/logos/ContextLogo.svelte

Verified: svelte-check + tsc --noEmit both clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:23:10 +02:00
Till JS
1815139dc1 chore: drop context module — registry refs, schema, AI route, AppId
The context module's UI + Dexie tables + i18n bundle were already
removed in d3e2e73ca. This follow-up cleans up everything else that
still referenced it:

- API: rename POST /api/v1/context/import-url → /api/v1/kontext/import-url
  (the kontext singleton was the only consumer); drop the unused
  /ai/generate + /ai/estimate endpoints; rename the credit-op label
  AI_CONTEXT_IMPORT_URL → KONTEXT_IMPORT_URL; drop AI_CONTEXT_GENERATION
  from packages/credits.
- Web: drop registerApp + File icon import from app-registry/apps.ts;
  drop contextModuleConfig from data/module-registry.ts (+ snapshot test);
  drop useRecentDocuments + useSpaces from cross-app-queries.ts; drop
  ContextDocsWidget from widget-registry + dashboard.svelte.ts +
  types/dashboard{,.test}.ts; drop dashboard.widgets.context from all 5
  dashboard locales; drop context entries from hooks.server allowlist,
  splitscreen registry, observatory mockData, spiral collect, crypto
  registry + plaintext-allowlist.
- Dexie: remove documents/contextSpaces/documentTags from v1, v31, v53
  stores blocks; add v57 dropping the three tables on local dev DBs
  that already ran an earlier schema.
- Shared-branding: drop 'context' from AppId union, APP_BRANDING,
  MANA_APPS, APP_ICONS (+ contextSvg), ContextLogo.svelte (+ logos
  barrel re-export).
- Spiral-DB: drop context: 10 from MANA_APP_INDEX (slot now free).
- i18n hardcoded-string baseline: drop 5 context routes/files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:20:04 +02:00
Till JS
8fbdc6db77 feat(notes): isSpaceContext flag replaces kontext module (Option B)
Retire the kontext module entirely; the per-Space standing-context
document is now a regular Note flagged with `isSpaceContext: true`.
Daily use ("URL → Notiz") moves to the notes module as a first-class
action; the same primitive is reused by the (planned) Brand/Firma-Space
onboarding wizard to seed a Space-context Note from a URL.

Why: kontext was inconsistent — its UI was a URL-crawler that wrote
to userContext.freeform (profile module), while its kontextDoc table
+ AI-Mission-Runner auto-injection was a write-only shell with no
real editor. One concept (Notes) now carries both ad-hoc noting and
Space-context, with mutex (max 1 flagged Note per Space).

Notes module:
- types: add `isSpaceContext?: boolean` to LocalNote + Note
- queries: add `useSpaceContextNote()` (the active Space's flagged note)
- store: `markAsSpaceContext(id | null)` with mutex sweep across Space
- ListView: "Aus URL importieren" inline form (URL + crawl-mode +
  KI-Zusammenfassung toggle); "Als Space-Kontext markieren" /
  "Space-Kontext lösen" context-menu item; ★-Badge on flagged notes
- new api.ts: `crawlUrl()` client for POST /api/v1/notes/import-url

Notes API (apps/api):
- new modules/notes/routes.ts with /import-url (ported from kontext;
  same crawler + LLM summary pipeline, NOTES_IMPORT_URL credit op)
- mount at /api/v1/notes; add 'notes' to RESOURCE_MODULES (beta+ tier)
- delete modules/context (UI-less /ai/generate + /ai/estimate had no
  consumers; /import-url moved to notes)
- packages/credits: rename AI_CONTEXT_GENERATION → NOTES_IMPORT_URL

AI Mission Runner:
- default-resolvers: drop kontextResolver + kontextIndexer; the
  notesIndexer flags `isSpaceContext` notes with "★ " prefix and
  bubbles them to the top of the picker
- writing reference-resolver: `kind: 'kontext'` now reads the flagged
  Note via scope-scan instead of the kontextDoc table; tests updated
- writing ReferencePicker: useSpaceContextNote replaces useKontextDoc
- AiDebugBlock + MissionGrantDialog + ai-missions ListView: drop
  'kontextDoc' from ENCRYPTED_SERVER_TABLES set
- ai-agents ListView: drop 'kontext' from POLICY_MODULES

Profile module:
- ContextFreeform.svelte: switch import from kontext/api to notes/api
  (the URL-crawl is the same primitive; it still writes to
  userContext.freeform — only the import path changed)

Dexie:
- v58: notes index gains `isSpaceContext`; kontextDoc table dropped

Kontext module deletion:
- delete apps/mana/apps/web/src/lib/modules/kontext/ entirely
- delete (app)/kontext/ route
- drop registerApp + Scroll icon from app-registry/apps.ts
- drop kontext entry from help-content
- drop kontextModuleConfig from data/module-registry.ts
- drop kontextDoc from crypto registry

mana-auth:
- bootstrap-singletons: drop bootstrapSpaceSingletons function entirely
  (kontextDoc was the only per-Space singleton); userContext bootstrap
  unchanged
- better-auth.config: drop kontextDoc bootstrap call from personal-space
  hook + organizationHooks.afterCreateOrganization
- me-bootstrap: drop per-space bootstrap loop; response shape kept
  (always-empty `spaces: {}`) for backwards-compat with older clients

Note: the still-existing legacy `context` module (CMS-style docs/spaces,
unrelated to kontext) is left in place; its cleanup landed on the
articles-bulk-import branch and is out of scope for this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:14:32 +02:00
Till JS
6d193a9fa7 chore(app-registry): polish 4 small wins — TOC + AppId-derive + route-drift test + 3 MANA_APPS
§1 AppId derivation (shared-branding):
- `AppId` is now `keyof typeof APP_BRANDING` (config.ts) instead of a
  hand-maintained union in types.ts. Adding/removing an entry in
  `APP_BRANDING` automatically updates the union — eliminates the
  drift class that produced the ContextLogo type-error.
- `AppBranding.id` relaxed to `string` to break the circular type
  reference (key in `APP_BRANDING` is the authoritative id).

§2 Route-drift smoke test (registry.spec.ts):
- New 4th test: parses every `routes/(app)/*+page.svelte`, extracts
  the `<RoutePage appId="…">` literal, asserts the id is registered
  in the workbench app-registry. Catches drift like the earlier
  `appId="broadcasts"` vs id `'broadcast'` bug structurally.
- ROUTE_ONLY_APP_IDS allowlist for routes that intentionally don't
  back a workbench module (gifts, llm-test, milestones, organizations,
  teams, tags).
- Caught two real drifts in the process and fixed them:
    /agents/+page.svelte → appId="ai-agents" → "agents"
    /agents/templates/+page.svelte → same

§3 MANA_APPS hochgezogen (kontext, wishes):
- kontext (Web-Context URL crawler) + wishes (Wunschliste) had module
  + workbench card but no MANA_APPS branding entry. Both got proper
  description, longDescription and a fresh APP_ICONS entry (globe-
  with-text-lines for kontext, shooting-star for wishes).
- Removed both from WORKBENCH_ONLY in spec — they're full apps now.
- Note: `myday` was already in MANA_APPS, the WORKBENCH_ONLY entry
  was redundant and had been silently double-counting.

§4 apps.ts — top-level INDEX comment:
- 80 registerApp() calls were chronological-by-when-added — basically
  unsearchable. Added an §1–§4 navigation comment near the top
  grouping apps by role (entity / module surface / AI Workbench /
  System) so devs can jump to a section. Physical reordering of
  the 80 blocks deferred to avoid disturbing the active multi-
  terminal session — the TOC delivers ~80% of the navigation win.

Bonus: register `forms` module that the parallel session added but
hadn't wired into the workbench yet — the new route-drift test caught
this immediately on first run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:59:26 +02:00
Till JS
230dfd5dad chore: extract arcade into standalone repo
Arcade lives as its own pnpm workspace at ~/Documents/Code/arcade
now, with no @mana/* coupling. This drops every reference and the
games/ directory from the monorepo.

Removes:
- games/ directory (89 files: web + server + 22 HTML games + screenshots)
- @arcade/web, @arcade/server pnpm workspace entries (games/* globs)
- arcade scripts in root package.json (4 scripts)
- arcade.mana.how from mana-auth trusted origins + CORS_ORIGINS
- arcade entries in mana-apps registry, app-icons, URL overrides
- arcade.mana.how from cloudflared tunnel + prometheus blackbox probes
- arcade-web service block in docker-compose.macmini.yml
- generate-env.mjs entries for arcade server + web
- BRANDING_ONLY 'arcade' entry in registry consistency spec
- dead arcade translation keys in GuestWelcomeModal (DE+EN)
- arcade mention in CLAUDE.md, authentication guideline, MODULE_REGISTRY

Verified:
- services/mana-auth/src/auth/sso-config.spec.ts: 8/8 pass
- pnpm install regenerates lockfile cleanly (-536 lines)
- no remaining 'arcade' refs outside historical snapshot docs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:40:01 +02:00
Till JS
0fc16d1bfd feat(articles): bulk-import AI tool wiring (Phase 6)
Adds import_articles_from_urls tool to the articles module so the AI
Workbench can kick off a bulk-import job in one call. Auto-policy: the
job itself is the unit of approval, no per-article propose card.

- shared-ai schemas: declare the tool name + propose/auto policy
- articles/tools.ts: implement parseUrls + articleImportsStore.createJob
- consume-pickup.ts: handle the new event type
- events/catalog.ts: register article-import lifecycle events
- imports.svelte.ts: minor polish

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:33:31 +02:00
Till JS
fa299e3bf9 feat(app-registry): wire up 4 modules + 7 routes + tier-patch validator
Resolves the cross-cutting drift that the app-registry sanity-test was
silently catching but BRANDING_ONLY exceptions papered over.

App-registry wiring:
- Register augur, broadcasts, invoices, timeline as workbench cards.
- Resolve agents↔ai-agents naming drift: workbench id is now `agents`
  (matches MANA_APPS + the /agents route URL); folder stays `ai-agents`
  for grouping with other ai-* modules.

Broadcast→broadcasts unification:
- module.config appId, MANA_APPS id, APP_ICONS key, all route appIds,
  and the redundant APP_URL_OVERRIDES entry — all aligned with the
  earlier folder rename so nothing diverges anymore.

Top-level routes for workbench-only modules:
- /goals, /myday, /kontext, /rituals, /automations, /activity — thin
  RoutePage wrappers around the existing module ListViews.
- /timeline becomes a real module (ListView extracted from the route),
  route shrinks to a 12-line wrapper.

Food unarchive:
- packages/shared-branding/src/mana-apps.ts: remove `archived: true`
  from food entry. The module is fully wired (registered, synced,
  routed, with AI tools); the flag was outdated.

i18n cleanup:
- Rename ai-agents → agents key in all 5 apps locales.
- Drop dead "observatory" key from all 5 nav locales (route folder was
  removed in 7bca16dfa).

New CI guard — scripts/validate-tier-patches.mjs:
- Scans for `LOCAL TIER PATCH — revert before release` markers.
- Default: informational list (does not fail).
- Strict mode (MANA_TIER_PATCH_STRICT=1) for release/RC pipeline.
- Wired into validate:all.

Spec update:
- registry.spec.ts WORKBENCH_ONLY/BRANDING_ONLY: documented Settings
  family + AI Studio surfaces + intentionally-internal modules so the
  drift guard fires only on real drift.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:21:41 +02:00
Till JS
c7094207da fix(feedback): ReactionBar stoppt Click-Bubbling
Wer in Feed/Workbench eine Reaction setzt, landete bisher direkt im
Detail-View — der Button-Click ist zur Card-onclick durchgesickert.

Fix in der Quelle: ReactionBar.handleClick ruft jetzt e.stopPropagation()
bevor onToggle feuert. Damit funktioniert es überall, wo Reactions in
einer klickbaren Hülle sitzen (Feed-Cards, MyReactedView, Detail-Page,
zukünftige Surfaces).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:25:22 +02:00
Till JS
15ab24bda8 feat(feedback): heart-half als globales Feedback-Icon + inline-Form in der Workbench
Drei Probleme adressiert:

1. **Icon-Vereinheitlichung**: alle Feedback-Affordances tragen jetzt
   das phosphor `heart-half`-Icon (statt vorher Lightbulb/Mix). Geändert
   in PillNav-Usermenü, ModuleShell-Header (FeedbackHook), Phosphor-Icon-
   Map. Eine Stelle, ein Icon — Wiedererkennung steigt.

2. **Inline statt Modal in Workbench-Cards**: AppPage.svelte rendert
   das Feedback-Formular jetzt im selben Slot wie die Hilfe-Seite —
   Klick auf das Heart-Half-Icon togglet den Inline-Panel statt einen
   Modal-Backdrop über die ganze Workbench zu legen. Hilfe und Feedback
   sind mutually-exclusive (eines geht zu, sobald das andere aufgeht).

3. **Form-Body extrahiert**: FeedbackForm.svelte enthält jetzt das
   Formular ohne jegliches Chrome. FeedbackQuickModal nutzt es im Modal-
   Mode (Standalone-Routen, PillNav), AppPage im Inline-Mode. Eine
   Quelle, beide Surfaces bleiben in sync.

ModuleShell schluckt zusätzlich `onFeedback`/`feedbackOpen`-Props: wenn
gesetzt, ruft die FeedbackHook-Komponente onClick statt das eigene Modal
zu öffnen — der Host (AppPage) übernimmt das Rendering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:36:52 +02:00
Till JS
ff823bff60 fix(feedback): POST /api/v1/feedback liest appId aus X-App-Id-Header
Der Submit-Handler hat den Body 1:1 an feedbackService.createFeedback
weitergereicht. Da CreateFeedbackInput appId nicht enthält (Client
schickt es als X-App-Id-Header), schlug jeder INSERT mit "null value
in column app_id violates not-null constraint" fehl.

Außerdem: lightbulb-Icon im phosphor-icon-map nachgezogen, sonst
zeigt der "Idee teilen"-Eintrag in der barMode-Variante des Usermenüs
kein Icon (nur Label).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:16:11 +02:00
Till JS
94d3277e2e feat(feedback): "Idee teilen" lebt jetzt im PillNav-Usermenü
Ersetzt den schwebenden "Idee?"-Pill durch einen Eintrag im rechten
Usermenü (Profil / Credits / Idee teilen / Logout). Ein Affordance an
einer Stelle statt zwei nebeneinander.

- PillNavigation: neuer onFeedback-Prop + Lightbulb-Icon. Wenn gesetzt,
  ersetzt der Eintrag den Legacy-/feedback-Link in accountLinks und
  taucht zusätzlich oben in den userMenuBarItems (barMode) auf.
- UserMenuPanel: AccountLink kennt jetzt onClick? als Alternative zu
  href? — Action-Chips schließen das Panel direkt nach dem Klick.
- (app)/+layout: GlobalFeedbackPill-Mount entfernt, FeedbackQuickModal
  wird state-gebunden gerendert (moduleContext aus Pfad/?app= abgeleitet
  wie bisher in der alten Pill).
- GlobalFeedbackPill.svelte gelöscht — niemand referenziert sie mehr.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:12:27 +02:00
Till JS
0c30a16eb5 fix: 4 boot-time noise + correctness bugs surfaced by post-deploy smoke
All four were pre-existing; the audit smoke-test made them visible. Fixed
together because they share a "boot console-warn cleanup" theme.

1. streaks ensureSeeded race (DexieError2 ×2)
   - Two boot-time liveQuery callers passed the `count > 0` check before
     either had written, then the second's `.add()` hit a ConstraintError.
   - Fix: cache the seed promise per module, run the existence check +
     bulkAdd inside one Dexie RW transaction, and only insert MISSING
     defs (preserves existing currentStreak/longestStreak counts).

2. encryptRecord('agents', …) "wrong table name?" warning
   - The DEV-only check fired whenever a record carried none of the
     registered encrypted fields, regardless of whether anything could
     actually leak. `ensureDefaultAgent` writes a fresh agent row before
     `systemPrompt` / `memory` exist — pure noise.
   - Fix: drop the "no fields at all" branch. Keep the case-mismatch
     branch (the branch that actually catches silent plaintext leaks).

3. Passkey signInWithPasskey "Cannot read properties of undefined
   (reading 'allowCredentials')"
   - Client destructured `{ options, challengeId }` from the server's
     options response, but Better-Auth's `@better-auth/passkey` plugin
     returns the raw PublicKeyCredentialRequestOptionsJSON (no
     envelope) and tracks the challenge in a signed cookie. Both
     `options` and `challengeId` came back undefined; SimpleWebAuthn
     blew up the moment it tried to read the request shape. Verify body
     `{ challengeId, credential }` was likewise wrong — Better-Auth
     wants `{ response }`.
   - Fix: align both register and authenticate flows with Better-Auth's
     native shape on options + verify, and add `credentials: 'include'`
     on every fetch so the challenge cookie actually round-trips.
     Server's verify proxy now reads `parsed?.response?.id` for
     credentialID rate-limiting.

4. /api/v1/me/onboarding/ → 404
   - Hono's nested router (`app.route(prefix, sub)` + inner
     `app.get('/')`) matches the prefix-without-slash form only. The
     onboarding-status store sent the request with a trailing slash, so
     every login produced a 404 + a console warn.
   - Fix: client sends the path without trailing slash; mana-auth picks
     up `hono/trailing-slash` middleware as defense-in-depth so a future
     accidental trailing slash on any /me/* route 301-redirects instead
     of 404-ing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 14:56:24 +02:00
Till JS
246c94374f test(feedback): pixel-avatar + redact privacy-boundary; mark plan SHIPPED
Tests:
- packages/feedback/src/avatar.test.ts — 10 unit tests (determinism,
  mirror-symmetry, color contrast, padding-resilience, pseudonym-
  integration, density-sanity).
- services/mana-analytics/src/services/feedback-redact.test.ts —
  9 privacy-boundary tests verifying:
    * anonymous path NEVER includes realName, even when author opted in
    * auth path NEVER includes realName when author opted OUT
    * realName only when (opted-in AND auth-path) — both gates required
    * userId / deviceInfo / voteCount stripped from output

Plan-Doc:
- docs/plans/feedback-rewards-and-identity.md status → shipped (3.A,
  3.B, 3.C, 3.F live; 3.D, 3.E open) mit Commit-Hashes.

Service-Layer minor: REWARD-const + redact als __TEST__-Export
publik gemacht (nur fürs Testen, kein Verhaltensänderung).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 18:11:17 +02:00
Till JS
ee5bb2871c feat(community): Phase 3.C — Identität (Avatar + Klarname-Toggle + Karma + Eulen-Profil)
Macht aus den Pseudonymen echte Charaktere ohne Klarnamen-Zwang.

Pixel-Identicon-Avatar (3.C.2):
- generateAvatarSvg(displayHash) — pure-function, deterministisch.
  5×5 left-mirrored Identicon mit HSL-Foreground/Background aus dem
  Hash. Inline-SVG, kein Storage, kein img-load-Flicker.
- <EulenAvatar> Component im Package, in ItemCard neben dem Pseudonym.

Klarname-Toggle (3.C.1):
- auth.users + community_show_real_name boolean (default off, opt-in).
- PATCH /api/v1/me/profile akzeptiert communityShowRealName.
- mana-analytics LEFT JOINs auth.users → bei opt-in liefert auth-
  required /public + /me/reacted Endpoints zusätzlich realName.
- Anonymous /api/v1/public/feedback/* zeigt realName NIE — auch nicht
  wenn opted-in. Public-Mirror bleibt für SEO + Privacy safe.
- Migration 008_community_identity.sql lokal + prod eingespielt.

Karma-System (3.C.3):
- auth.users + community_karma int. toggleReaction increment/decrement
  am Author-User (Self-Reactions zählen nicht — kein Self-Farming).
- KARMA_THRESHOLDS + tierFromKarma() im Package: Bronze (0-9) /
  Silver (10-49) / Gold (50-199) / Platin (200+).
- ItemCard zeigt Tier-Dot neben dem Pseudonym, Title-Tooltip mit
  Karma-Zahl. Floor-clamped at 0.

Eulen-Profil (3.C.4):
- GET /api/v1/public/feedback/eule/{hash} — alle public-Posts dieser
  Eule + aggregiertes Karma. SHA256-Format-Validation.
- /community/eule/[hash] Public-SSR-Route mit Avatar-Hero, Tier-Badge,
  Karma-Counter, Post-Liste. Author-Klick im ItemCard navigiert hin.
- publicFeedbackService.getEulenProfile() im Package.

PublicFeedbackItem erweitert um displayHash (public Pseudonym-ID,
SHA256 ist one-way → safe to expose) + karma + optional realName.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:15:16 +02:00
Till JS
3a18a5e50d feat(community): Phase 3.B — loop closure (notifications + my-wishes page)
Schließt den Loop zwischen Submit und Ship. User kriegt jetzt:
- Toast beim nächsten App-Start, wenn ein eigener oder unterstützter
  Wisch ›planned/in_progress/completed/declined‹ wurde
- /profile/my-wishes als persönliche Roadmap mit drei Tabs:
  Eigene · Unterstützt · Inbox

Server (mana-analytics):
- Neue Tabelle feedback_notifications mit ON DELETE CASCADE auf
  user_feedback. Migration 0004 lokal + prod eingespielt.
- adminUpdate enqueued bei jeder Status-Transition Author-
  Notifications. AdminResponse-Edits feuern eine eigene
  'admin_response'-Notify. tryGrantShipBonus hängt zusätzlich
  Reactioner-Notifications dran (›Dein Like ist gelandet, +25 Mana‹).
- Endpoints:
    GET  /api/v1/feedback/me/notifications?unread_only=true&limit=N
    POST /api/v1/feedback/me/notifications/:id/read
    POST /api/v1/feedback/me/notifications/read-all
    GET  /api/v1/feedback/me/reacted    (für die My-Wishes-Page)

Package (@mana/feedback):
- FeedbackNotification + NotificationKind types exportiert
- service.getNotifications/markNotificationRead/markAllNotificationsRead
- service.getMyReactedItems

Web:
- lib/notifications/feedback-toaster.svelte.ts: Boot-Pull + 60s-Poll,
  rendert unread-notifications via toast-store, markiert sofort read.
  In (app)/+layout.svelte's authReady-Hook gestartet/gestoppt.
- /profile/my-wishes: Tab-View über getMyFeedback + getMyReactedItems
  + getNotifications. Tabs zeigen Counter-Badges, unread-Badge in der
  Inbox-Sektion. ›Alle als gelesen markieren‹-Action vorhanden.

Pre-launch saubere Lösung — kein Polling-Spam (60s), Mark-Read direkt
nach Toast-Display, fail-soft an mehreren Stellen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:55:01 +02:00
Till JS
eecf64c1c6 feat(community,feedback): +5 reward chip + Phase 3.F legacy-cleanup
UI:
- FeedbackQuickModal Success-State + Onboarding-Wish Confirm zeigen
  +5-Mana-Reward-Chip mit reward-in-Animation. Sofortiger Sichtbarer
  Reziprozitäts-Loop.

Legacy-Cleanup (Phase 3.F):
- @mana/feedback dropped:
  - FeedbackPage.svelte, FeedbackCard.svelte, FeedbackList.svelte,
    FeedbackForm.svelte, VoteButton.svelte, StatusBadge.svelte
    (alles Pre-Reactions-Markup, durch Community-Modul ersetzt)
  - vote/unvote/toggleVote/getPublicFeedback service-shims
  - VoteResponse, voteCount, userHasVoted Types
- mana-web dropped:
  - lib/modules/feedback/ListView.svelte
  - routes/(app)/feedback/+page.svelte
  - app-registry-Eintrag 'feedback' (nur Bug-Reports — Community macht
    das ohnehin besser via /community)

Pre-launch saubere Lösung: keine Backward-Compat-Shims, keine alten
Markup-Reste. ReactionBar bleibt der einzige Voting-Surface, /community
ist die einzige Feedback-Surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:14:08 +02:00
Till JS
ae6a14fb76 feat(shared-ai): SYSTEM_BOOTSTRAP system source — fallback inserts now stamp origin='system'
The race-window `getOrCreateLocalDoc()` fallback in userContextStore +
kontextStore stays (without it, a write that lands between "endpoint
provisioned the singleton in mana_sync" and "first pull landed it in
IndexedDB" would hit `update(missing-id, diff)` — a Dexie no-op that
silently swallows the user's edit). But it was semantically lying: the
insert stamped `origin='user'` even though the row is logically a
client-side replica of the server-side bootstrap.

This commit adds `SYSTEM_BOOTSTRAP = 'system:bootstrap'` to
`@mana/shared-ai` and wraps the two fallback inserts in
`runAsAsync(makeSystemActor(SYSTEM_BOOTSTRAP), ...)`. The Dexie hook
now stamps `origin: 'system'` on the empty-row insert — structurally
identical to the row mana-auth's bootstrap-singletons.ts writes. When
the server's pull arrives later both sides carry the same origin and
the conflict-gate stays quiet. The user's subsequent writes still
stamp `origin: 'user'` on the changed fields.

Plan: docs/plans/sync-field-meta-overhaul.md (F4-fu Fallback-Origin row).

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 00:01:06 +02:00
Till JS
6bb9d77be9 feat(sync): F3 — drop updatedAt as a synced data field
Removes `updatedAt` from the wire protocol and from every Local-prefixed
record type. Replaced by two orthogonal mechanisms — deriveUpdatedAt()
for read-side public-facing values, _updatedAtIndex shadow for indexed
sorts.

Local-side:
- New `_updatedAtIndex` shadow column. Stamped by the Dexie creating /
  updating hook on every write. Stripped from the pending-change payload
  so it never travels to mana-sync. Indexed in Dexie v53 on the 22 tables
  that previously indexed `updatedAt`.
- `deriveUpdatedAt(record)` in sync.ts returns max(__fieldMeta[*].at) so
  the public-facing Task / Note / etc. shape keeps an `updatedAt: string`
  property without holding it as data.
- Type-converters across ~60 module/queries.ts and types.ts files now
  call `deriveUpdatedAt(local)` instead of reading `local.updatedAt`.

Module-store sweep:
- Regex codemod removed `updatedAt: new Date().toISOString()` /
  `: now` / `: now()` / `: nowIso()` stamping from 121 store files
  (~382 call sites total). Single-property update calls
  (`{ updatedAt: now }`) collapsed to `{}`; touch-only patterns
  (writing/drafts, writing/generations) kept the call as a no-op
  because the hook now stamps `_updatedAtIndex` automatically on
  any Dexie modification.
- Local* interfaces stripped of `updatedAt: string` (43 types.ts files).
  Public-facing types (Task, Note, Mission, Agent, …) keep
  `updatedAt: string` as a computed read-side property.
- Companion's chat conversation now sorts on a real
  `lastMessageAt` data field instead of touching `updatedAt`.
- Session-only stores (times/session-alarms, session-countdown-timers)
  stamp `updatedAt: now` directly because they're not in Dexie and
  have no field-meta layer to derive from.

Sync engine:
- applyServerChanges sets `_updatedAtIndex` itself when applying
  server changes (max of server-field times for updates, recordTime
  for inserts) so server-replays land orderable.
- Dropped the legacy `localUpdatedAt` fallback — every record now has
  `__fieldMeta`, the per-field at is the canonical source.
- Soft-delete tombstone path stops stamping `updatedAt: serverTime`,
  uses `_updatedAtIndex` instead.

Server-side:
- mana-ai iteration-writer no longer emits `updatedAt` in
  sync_changes.data; receivers derive it from the field-meta map.
- mana-sync types: no change (the wire format already uses
  `field_meta` / `at` from F1).

Out of scope: backend Drizzle schemas (mana-credits, mana-events, …)
keep their `updated_at` columns. Those are pure server-internal — not
part of the sync_changes / __fieldMeta mechanism F3 cleans up.

Tests + checks:
- 0 svelte-check errors over 7652 files.
- 29/29 sync.test.ts (vitest).
- 61 mana-ai bun tests.
- mana-sync go test ./... cached green.

Plan: docs/plans/sync-field-meta-overhaul.md F3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 23:12:22 +02:00
Till JS
1398d76b41 refactor(lasts,firsts): German display names — "Letzte Male" / "Erste Male"
"Lasts" auf Deutsch ist ein Homophon zu "die Last" (Bürde/Belastung).
Ein deutscher Muttersprachler las "Last nicht gefunden" als "Bürde
nicht gefunden". Falsches Gefühl für ein kontemplatives Modul.

Renames:
- mana-apps.ts: name "Lasts" → "Letzte Male", "Firsts" → "Erste Male"
- lasts/de.json: app.title + Singular-Bezüge weg von "Last" auf
  "Letztes Mal" (detail.routeTitle, banner.recognition) bzw.
  "Eintrag" (detail.notFound, settings.testSampleTitle, …)
- milestones/de.json: tabs.first/last + recap.topFirstsLabel/topLastsLabel
  switchen auf "Erste Male" / "Letzte Male"
- store error: "Aufgehobene Lasts ..." → "Aufgehobene Einträge ..."

Andere Locales (en/es/fr/it) bleiben unangetastet — dort ist "Lasts"
und "Firsts" linguistisch unproblematisch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 21:58:31 +02:00
Till JS
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
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
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
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
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
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
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
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
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
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
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
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
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
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
6f37e00bf4 feat(comic): AI_TOOL_CATALOG bridge — webapp-runner kann jetzt Comics
Macht den Comic-Autor-Template (M6) auch im Web-App-Mission-Runner
nutzbar. Bisher war der Template nur über persona-runner/Claude
Desktop sinnvoll, weil die comic.*-Tools nur im mana-tool-registry
(MCP) lagen. Jetzt kennt die AI Workbench drei neue Tools und der
Template-Policy-Map trägt beide Naming-Konventionen.

AI_TOOL_CATALOG-Einträge (packages/shared-ai/src/tools/schemas.ts):
- list_comic_stories (auto) — filter style?/favoriteOnly?/limit?
- create_comic_story (propose) — title + style + optional
  description/storyContext/tags. Character-Refs werden vom Executor
  automatisch aus meImages primary face-ref + body-ref gezogen,
  also muss der Planner keine mediaIds kennen.
- generate_comic_panel (propose) — storyId + panelPrompt + optional
  caption/dialogue + quality. Kostet Credits.

Executors (apps/mana/apps/web/src/lib/modules/comic/tools.ts):
- list: scopedForModule pull + decrypt + filter + sort newest.
- create: resolveCharacterMediaIds() scannt meImagesTable für das
  aktive Space, nimmt face-ref+body-ref. Fehler wenn kein Face
  hinterlegt ("Lade eines in /profile/me-images hoch"). Delegiert
  an comicStoriesStore.createStory — gleiche encryption/event-
  pipeline wie StoryForm.
- generate: lädt Story decrypted, delegiert an runPanelGenerate
  (identischer Pfad wie PanelEditor in der UI), liefert
  panelIndex + imageUrl zurück.

Registrierung in data/tools/init.ts (registerTools(comicTools)).

Template-Policy (comic-author.ts) jetzt bi-lingual: snake_case
(AI_TOOL_CATALOG) UND dot-case (MCP) nebeneinander in tools-Map.
So gilt die Intent-Policy konsistent egal welche Runner-Oberfläche
das Tool nennt — auto für list_comic_stories / comic.listStories,
propose für create_comic_story / comic.createStory /
generate_comic_panel / comic.generatePanel / comic.reorderPanels.

apps/mana/CLAUDE.md Tool-Coverage-Tabelle bekommt eine Comic-Zeile.

Tool-Count jetzt 75→78, Module 22→23. 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 16:49:24 +02:00
Till JS
6545498dc2 feat(writing): agent.defaultWritingStyleId — M8 persona-linkage follow-up
Agents can now pin a default writing style. When an AI-actor runs
`create_draft` without an explicit styleId, the tool resolves to the
agent's `defaultWritingStyleId` so e.g. a "Marketing-Agent" always
drafts in the Corporate-Tone style and a "Memoir-Agent" in Memoir.

- @mana/shared-ai: optional `defaultWritingStyleId?: string` added to
  the Agent interface (plaintext FK, format `preset:<id>` or a custom
  WritingStyle uuid). No migration — existing rows stay undefined and
  the fallback path no-ops for them.
- ai-agents store: field threaded through CreateAgentInput + AgentPatch
  + the create function's copy-list. `updateAgent` already deep-clones
  the patch so nothing else to change there.
- ai-agents ListView: new "Writing" section in the agent detail panel
  with a StylePicker (reuses the writing module's component — Vorlagen
  + Meine Stile optgroups). Empty = kein Default.
- writing/tools.ts: `resolveAgentDefaultStyle()` reads the current
  actor, guards `isAiActor`, loads the agent row, and returns its
  defaultWritingStyleId. Wired into `create_draft` as a fallback when
  `params.styleId` is missing. User-invoked calls skip the lookup — a
  human omitting styleId means "ad-hoc, no style", not "my default".
  `generate_draft_content` needs no change because the draft's styleId
  is already set at create time.

107 shared-ai tests still pass.

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