Commit graph

2497 commits

Author SHA1 Message Date
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
054b9e5beb fix(articles): import-projection accepts F3 + legacy field_meta shapes
Live-test caught it: the worker projects sync_changes via field-level
LWW, comparing `field_meta[k]` directly. But field_meta is two-shaped
on the wire:

  - Legacy plaintext writes:   { state: '2026-04-28T…' }
  - Field-meta-overhaul writes: { state: { at, actor, origin } }

The naive `rowFM[k] >= localTime` worked for the all-legacy case, but
once a client write (legacy string) followed a worker write (F3
object), the comparison evaluated `'2026-04-28T…' >= '[object …]'`
and the projection silently kept the older value. Live symptom: an
item that was correctly flipped to 'saved' on the client was reported
back as 'extracted' by the projection.

Fix: `fieldMetaTime()` helper that pulls the ISO string out of either
shape; both write paths now compare apples-to-apples.

Verified end-to-end:
  - Synthetic job + item written into sync_changes
  - runTickOnce() → claim → extractFromUrl(example.com) → pickup row
    with title='Example Domain', wordCount=16, actor=
    system:articles-import-worker
  - Item transitions pending → extracting → extracted
  - Simulated client write 'saved'
  - Next tick rolls counters: savedCount 0→1, status running→done,
    finishedAt stamped

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 23:43:14 +02:00
Till JS
18f13e19b2 feat(forms): M4b visibility + unlisted-sharing + public render
Form-Sharing-Pipeline (docs/plans/forms-module.md M4 — Teil 2):

- formsStore.setVisibility(id, level): private/space/unlisted-Wechsel.
  Bei `unlisted` wird publishUnlistedSnapshot mit dem Form-Schema-Blob
  aufgerufen, Token + Expiry landen am LocalForm. Setze Status auf
  'published' Voraussetzung — sonst klare Fehlermeldung mit Hinweis.
- regenerateUnlistedToken: Token-Rotation für leak-Verdacht
  (revoke + neu publish, expiry beibehalten).
- setUnlistedExpiry: TTL-Update mit re-publish.
- buildFormBlob in data/unlisted/resolvers.ts mit Whitelist
  title/description/fields/branching + nur submitButtonLabel +
  successMessage aus settings. Hard-blocks: nicht-published Forms +
  deletedAt → RecordNotFoundError. Server-side Settings (requireEmail,
  anonymous, zkMode, autoSync, responseLimit, closedAt, responsesPublic)
  bleiben strukturell aussen vor — Public-Endpoint validiert
  authoritativ ohne Discovery-Surface.
- VisibilityPicker + SharedLinkControls in BuilderView, eigene Section
  mit Status-Hint wenn Form noch nicht published ist.
- SharedFormView (498 Zeilen): public-render mit allen 11 Field-Types
  (short/long_text, single/multi_choice, number, date, email, yes_no,
  rating, section, consent), Live-Branching via resolveVisibleFields
  bei jedem Keystroke, Required-Field-Validierung blockt Submit-Button.
  Submit zeigt successMessage + DEV-Hinweis (Public-Submit-Endpoint
  landet in M3.b). Mana-Branding-Footer.
- Share-Dispatcher /share/[token] kennt `forms` collection.
- 10 neue i18n-Keys × 5 Locales (forms.builder.visibility.*).

Public-Submit-Pipeline (mana-api POST → mana-sync → owner client) ist
M3.b. Bis dahin zeigt SharedFormView.handleSubmit nur die success-
Message ohne Server-Roundtrip.

svelte-check: 0 errors in forms/. Pre-existing context-removal-Drift
in cross-app-queries + widget-registry (4 errors) ist Parallel-Session-
WIP.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 23:39:49 +02:00
Till JS
afeb32f922 feat(forms): M4a conditional branching — pure resolver + UI editor
Wenn-Dann-Logik für Form-Felder (docs/plans/forms-module.md M4 — Teil 1):

- lib/branching.ts: pure resolveVisibleFields(fields, branching, answers)
  — gibt sichtbare Subset-Liste zurück, Reihenfolge wie Original.
  Operatoren equals/not_equals/contains/is_empty mit Array-aware
  Matching (multi_choice + scalar in beide Richtungen). Aktionen
  show/hide/skip_to. show überschreibt hide bei doppelten Treffern
  (last-write-wins Layering, in Deklarations-Reihenfolge). skip_to
  versteckt alle Felder strikt zwischen Anchor und Target.
  Section/consent-Felder bleiben unbeeinflusst (kein answer-state).
- lib/branching.spec.ts: 11/11 Vitest-Cases — keine Regeln, hide+show
  Kombinationen, skip_to, contains-on-multi-choice, not_equals,
  is_empty (null/undefined/''/[]/false), Layering, fehlerhafte Refs,
  Order-Erhalt.
- components/BranchingEditor.svelte: top-level Builder-Sektion zum
  Anlegen/Editieren/Löschen von Regeln. Pro Regel: IF-Feld + Operator
  + Wert-Input (außer is_empty), THEN-Action + Target-Chips
  (multi-select für show/hide) bzw. einzelnes Feld (skip_to).
  Empty-State warnt wenn weniger als 2 Antwortfelder existieren.
- formsStore.updateBranching(id, rules) — encrypted-aware update.
- Wired in BuilderView als Section zwischen Fields und Settings.
- 18 neue i18n-Keys × 5 Locales (forms.branching.* + .op.* + .action.*).

Total Forms-Tests: 16/16 grün (5 csv + 11 branching). svelte-check: 0
errors in forms/. Pre-existing drift in context-removal-Spuren auf
main ist Parallel-Session-WIP, nicht durch mich.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 23:31:36 +02:00
Till JS
0ef71de008 feat(forms): M3a responses view + CSV export + detail modal
In-App-Antwort-Inspektion (docs/plans/forms-module.md M3, ohne
Server-Public-Submit — der landet zusammen mit M4 Visibility):

- ResponsesView mit Status-Tabs (Alle | Neu | Gesichtet | Archiviert |
  Spam) + Live-Counts pro Tab. Tabelle: Eingegangen / Status-Chip /
  Submitter (Name → Email → Anonym-Fallback) / Antwort-Snippet (max
  60 Zeichen vom ersten non-section/consent-Feld). Klick auf Zeile
  öffnet Detail-Modal.
- ResponseDetailModal als Overlay mit ESC-Close + Backdrop-Click,
  Status-Pills oben, Submitter-Block, Antworten pro Feld in Form-
  Reihenfolge, Section-Trenner, Required-Marker, Delete-Button.
- lib/csv.ts: pure buildResponsesCsv(form, responses) — Header
  submittedAt/status/submitter/email + Form-Felder (section + consent
  ausgenommen), Werte mit RFC-4180-Escape, UTF-8 BOM beim Download.
  5/5 Vitest-Cases grün.
- BuilderView "Antworten ({n})"-Link mit responseCount-Counter.
- Route /forms/[id]/responses.
- 22 neue i18n-Keys × 5 Locales (forms.responses.* + .detail.*).

Public-Submit-Pfad (mana-api Endpoint + PublicFormView) bleibt offen
bis nach M4 Visibility.

Re-applied from notes-space-context branch (cherry 6e7972181,
forms-only subset).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 23:21:51 +02:00
Till JS
f104a2bc35 feat(forms): M2 builder + CRUD — drag-reorder + 11 field-types
Lieferung M2 (docs/plans/forms-module.md M2):

- BuilderView mit Title/Description-Inputs (autosave-on-blur),
  Status-Pills (draft/published/closed), Delete-Button mit confirm,
  Drag-reorder über svelte-dnd-action + flip-Animation.
- FieldEditor pro Feld: Label-Input, Pflichtfeld-Toggle, Typ-Switcher
  über alle 11 Field-Types, kollabierbare "Erweitert"-Section mit
  helpText + type-spezifischer Konfig (Optionen für choice-Felder mit
  Add/Remove, ratingScale-Toggle 5/10, min/max für number, maxLength
  für text). Type-switch räumt stale Konfig auf.
- FieldPalette mit 11 Buttons (Glyph + Label) — Klick erzeugt frisches
  Feld via makeDefaultField + dispatch an Builder.
- SettingsPanel: submitButtonLabel, successMessage, requireEmail,
  allowMultipleSubmissions, anonymous.
- field-defaults.ts: makeDefaultField(type) generiert je Typ sinnvolle
  Defaults — choice mit 2 Optionen, rating mit 5er-Skala, consent
  required + Standard-Text.
- Route /forms/[id] mit RoutePage-Wrapper.
- 38 neue i18n-Keys × 5 Locales (forms.builder.*).

Optimistic-UI: items-State wird lokal gepatcht vor store.update um
Type-Switches sofort zu rendern; field-array re-syncet bei
upstream-id-Änderung (multi-tab).

Re-applied from notes-space-context branch (cherry 7767e6761,
forms-only subset, ohne Parallel-Session context-removal).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 23:21:27 +02:00
Till JS
75d9207ff2 feat(forms): M1 skeleton — module + Dexie v57 + welcome-seed
Erste Lieferung des Forms-Moduls (docs/plans/forms-module.md M1):

- Modul-Struktur unter src/lib/modules/forms/: types (LocalForm,
  LocalFormResponse, 11 Field-Types, BranchingRule, FormSettings),
  module.config, collections, queries (live + type-converters),
  stores (forms-CRUD inkl. add/update/remove/reorderFields,
  responses-submit mit denormalized responseCount-bump),
  ListView mit Quick-Create + Search + 3-fach Empty-State.
- Dexie v57: forms (id, status, _updatedAtIndex) + formResponses
  (id, formId, status, submittedAt, _updatedAtIndex, [formId+status]).
- Encryption-Registry typed entries: title/description/fields/branching/
  settings auf forms; answers/submitterEmail/submitterName/submitterMeta
  auf formResponses. Status, formId, submittedAt, responseCount,
  visibility, unlistedToken bleiben plaintext (Routing- + Sort-Felder).
- Per-Space-Welcome-Seed mit Beispiel-Formular (3 Felder), wired in
  data/seeds/index.ts.
- Route /forms via RoutePage (appId='forms').
- i18n-Namespace forms/ × 5 Locales (de/en/es/fr/it).

App-Registry-Eintrag (APP_ICONS.forms + MANA_APPS) ist bereits in
6d193a9fa gelandet (paralleler app-registry-polish-Commit).

Validatoren grün: validate:turbo, validate:pg-schema,
validate:i18n-parity (77 namespaces × 5), validate:theme-{parity,
utilities,variables}, audit:encrypted-tools (23 tools, M1 hat keine),
svelte-check 0/0/0 über 7680 Files. check:crypto: 213 Tables (+2
sind meine), 3 Violations sind pre-existing dead entries.

LOCAL TIER PATCH: requiredTier='guest' mit revert-Marker vor Release.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 23:01:05 +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
33b3f656fd test(articles): parseUrls unit tests + extract pure module (Phase 7)
Move `parseUrls` out of stores/imports.svelte.ts (which transitively
imports Dexie via collections.ts) into a standalone parse-urls.ts so
the test file can exercise it without booting Dexie. The store re-
exports parseUrls so existing call sites (BulkImportForm, tools.ts)
keep working unchanged.

11 unit tests covering:
  - empty + whitespace-only inputs
  - newline / whitespace / comma / tab separator handling
  - http + https accepted, ftp / mailto / javascript / file rejected
  - bare domains rejected (URL accepts them as opaque, our parser
    requires explicit scheme)
  - duplicate detection preserves first-occurrence order
  - canonicalisation (trailing slash on root, query+fragment kept)
  - mixed valid / invalid / duplicate token ordering
  - title-prefixed-paste behaviour (strict — surfaces non-URL words
    as invalid for the user to see)
  - 50-URL stress check

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:39:17 +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
5f0a1b5053 feat(articles): bulk-import UI (Phase 5)
apps/mana/apps/web/src/lib/modules/articles/components/:
  - BulkImportForm.svelte: <textarea> + live-validating $derived parser,
    counter chips for valid/duplicate/invalid, expandable invalid-list,
    submit creates a job + navigates to /articles/import/[jobId].
  - JobsList.svelte: index of past + active jobs (newest first), status
    pill + progress + per-counter chips. Click row → detail.
  - JobDetailView.svelte: live header (status, progress bar, counters),
    action bar (pause/resume/cancel/retry-failed/delete), per-item rows
    with state pill + URL + open-link or error tooltip.

apps/mana/apps/web/src/routes/(app)/articles/import/:
  - +page.svelte: hosts BulkImportForm + JobsList.
  - [jobId]/+page.svelte: hosts JobDetailView.

AddUrlForm.svelte: small "Mehrere URLs auf einmal? → Bulk-Import" link
under the single-URL input so the existing flow surfaces the new path.

The whole UI is a pure liveQuery view — JobDetailView re-renders as
the server-worker writes counter updates and item-state transitions
through sync_changes. Worker tick + pickup-consumer (already shipped
in 5535f2da4 + a9bcd4183) close the loop end-to-end.

Phase 6 (Domain-Events + AI-Tool) and Phase 7 (Tests) follow.

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:30:36 +02:00
Till JS
29cbaf30f5 feat(articles): bulk-import store + queries (Phase 4)
apps/mana/apps/web/src/lib/modules/articles/:
  - stores/imports.svelte.ts: new file. articleImportsStore with
    createJob (bulkAdd N items + 1 job), pauseJob, resumeJob,
    cancelJob, retryFailed, deleteJob. parseUrls exported as a pure
    function — splits on whitespace+comma, validates http(s) scheme,
    deduplicates while preserving input order; used by both the store
    and the UI's $derived live-validation in Phase 5.
  - queries.ts: toImportJob/toImportItem converters + useImportJobs
    (index list), useImportJob (detail header), useImportItems (per-
    job item list). All scope-aware via scopedForModule / scopedGet.

Job creation: createJob(urls) → jobId. Items written first so a worker
tick that races the job-write doesn't see a job with totalUrls=N but
fewer items reachable. Server-worker picks up state='pending' items
on its 2s tick.

retryFailed re-arms the job to status='running' if it was 'done',
because all-terminal-items had triggered the auto-completion in the
worker's counter-rollup pass.

deleteJob is soft (deletedAt stamp) on both job + items; already-
landed Article rows are NOT touched.

Phase 5 (UI) follows.

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:23:45 +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
a9bcd4183a feat(articles): client-side pickup consumer (Phase 3)
Watches `articleExtractPickup` via liveQuery. For each row the server-
worker drops:

  1. Look up the matching `articleImportItems` row. Stale → just clean
     the inbox.
  2. Dedupe race: if the URL has been single-saved meanwhile, point
     the import item at the existing article (state='duplicate'),
     don't create a second row.
  3. Happy path: call existing articlesStore.saveFromExtracted (which
     runs encryptRecord + articleTable.add and emits ArticleSaved)
     → flip item to 'saved' (or 'consent-wall' on warning).
  4. Delete the pickup row so the inbox stays empty in steady state.

Multi-tab coordination via `navigator.locks.request('mana:articles:pickup')`
with `ifAvailable: true` — only the lock-holder consumes; other tabs
just observe the liveQuery and exit. Falls back to per-row in-memory
dedupe when the Locks API isn't available; the field-LWW server merge
forgives the rare double-process.

Wired from data-layer-listeners.ts so it boots once with the rest of
the data layer and disposes on layout unmount.

End-to-end pipeline now live:
  Client write items(state='pending')
    → sync_changes
    → server-worker tick (Phase 2)
    → Pickup row + state='extracted'
    → sync pull → liveQuery
    → saveFromExtracted (encrypt) → flip 'saved' / 'duplicate' / 'consent-wall'
    → delete pickup row

What's still needed for first user-visible test: Phase 4 (store
methods to create a job) + Phase 5 (UI). Without those there's no
way yet to inject items.

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:16:10 +02:00
Till JS
2bbcf14aba chore(geocoding): remove Pelias + close 3 bypass paths to public Nominatim
Pelias was retired from the Mac mini on 2026-04-28; photon-self
(self-hosted Photon on mana-gpu) has been the live primary since then.
This removes the now-dead Pelias adapter, config, tests, and the
services/mana-geocoding/pelias/ stack — the entire compose file, the
geojsonify_place_details.js patch, the setup.sh import script.

Provider chain is now `photon-self → photon → nominatim`. The chain
keeps its `privacy: 'local' | 'public'` split, sensitive-query
blocking, coord quantization, and aggressive caching unchanged.

Three direct calls to nominatim.openstreetmap.org that bypassed
mana-geocoding now route through the wrapper:

- citycorners/add-city + citycorners/cities/[slug]/add use the shared
  searchAddress() client (browser → same-origin proxy → mana-geocoding
  → photon-self).
- memoro mobile drops its OSM reverse-geocoding fallback entirely;
  Expo's on-device reverse-geocoding stays as the sole path. Routing
  through the wrapper would require a memoro-server proxy endpoint —
  a follow-up if Expo's quality proves insufficient.

Other behavioral changes:

- CACHE_PUBLIC_TTL_MS dropped from 7d → 1h. The long TTL was a
  privacy-amplification trick from the Pelias era; with photon-self
  serving the bulk of traffic, a transient cross-LAN blip was pinning
  cached fallback answers for days. 1h gives quick recovery.
- /health/pelias renamed to /health/photon-self; prometheus blackbox
  config + status-page generator updated.
- mana-geocoding container no longer needs `extra_hosts:
  host.docker.internal:host-gateway` (was only there for the
  Pelias-on-host-network era).

113 tests passing. CLAUDE.md rewritten to reflect the post-Pelias
architecture.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:12:26 +02:00
Till JS
7bca16dfa7 feat(articles): bulk-import schema + plan (Phase 1)
Three new sync-tracked Dexie tables under the articles appId:

  articleImportJobs     — job header (counters, status, lease metadata).
  articleImportItems    — one row per URL in a job, state-machine driven.
  articleExtractPickup  — short-lived server→client handoff inbox.

URL stays plaintext on items by necessity — the server-worker reads it
without master-key access, same rationale as articles.originalUrl. The
extracted article eventually lands encrypted in the existing `articles`
table; bulk-import rows hold only pointers.

Plan: docs/plans/articles-bulk-import.md (full architecture, 7 phases,
test matrix, edge-cases). Phase 2 already shipped in 5535f2da4 (worker);
this commit lays the schema underneath it.

Originally committed as b2f4e8314, lost during a parallel reset, here
restored via cherry-pick.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:11:51 +02:00
Till JS
5535f2da48 feat(articles): server-side bulk-import worker (Phase 2)
apps/api/src/modules/articles/:
  - import-projection.ts: sync_changes → live LWW projection of jobs
    + items. Cross-user scan for claimable jobs, per-job item scan.
  - import-extractor.ts: per-item state-machine. Claim → fetch → write
    pickup + flip extracted, OR retry up to 3x then 'error'. All writes
    attributed to system:articles-import-worker actor (built inline so
    no shared-ai SystemSource extension needed for now).
  - import-worker.ts: 2s tick, pg_try_advisory_xact_lock keyed on 'ARTI'
    so multi-instance apps/api never double-processes. Concurrency 3
    pending items per job per tick. Job-counter rollups + status flips
    derived from current item states.
  - apps/api/src/index.ts: start the worker at boot.

Pipeline (server side):
  Client write articleImportItems(state='pending')
    → sync push → mana_sync.sync_changes
    → server-worker tick projects 'pending' items
    → extractFromUrl (shared-rss / Readability)
    → write articleExtractPickup row + flip item → 'extracted'

Phase 3 (client-side pickup consumer) and Phase 4+ (store + UI) follow.

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 21:33:49 +02:00
Till JS
248549b15a fix(feedback): keine doppelte Anzeige von Title + Body
Bei kurzen Posts (oder wenn mana-llm fehlschlug) hat der Auto-Title-
Fallback `feedbackText.slice(0, 80)` den Body 1:1 als Title gespeichert
— Card zeigte dann zwei Mal denselben Text.

Zwei Schichten Schutz:

1. **Server (mana-analytics)**: catch-Branch wirft den Prefix-Fallback
   raus (title bleibt null). Zusätzlich neue isRedundantTitle()-Heuristik
   verwirft auch Auto-Titles, die nur ein truncierter Prefix des Bodies
   sind (Whitespace-collapse + Ellipsis-strip).

2. **Frontend (ItemCard)**: defensive showTitle-Computed — ältere DB-
   Items mit redundantem Title rendern automatisch nur den Body, ohne
   dass eine Datenbank-Cleanup nötig ist.

Title-Slot bleibt für echte Auto-Summaries und manuelle Titel sichtbar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:37:51 +02:00
Till JS
f851f15a47 feat(lasts): tidy ListView header — single-row quick-add + scrollable icon-tabs
Two layout fixes for the Lasts ListView:

1. Tab bar: status filters (Alle/Vermutet/Bestätigt/Aufgehoben) get inline
   Phosphor icons + parenthesized counters. Inbox/Meilensteine/Einstellungen
   now render as full icon+label tabs in a `border-left`-separated cluster
   instead of icon-only links. The whole bar is `overflow-x: auto` with
   hidden scrollbars (matches calendar/DateStrip pattern), so narrow
   workbench cards scroll horizontally instead of wrapping.

2. Quick-add: collapses two rows (input + Vermutet/Bestätigt pill toggle)
   into one. Mode is a `<select>` styled like the category select, sitting
   to the right of the title input. Removes the visual duplication where
   the toggle pills mimicked the status tabs above.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:22:40 +02:00
Till JS
941df57f77 feat(feedback): rename community-identity columns + settings-section
Letzter "community"-Rest aus dem Feedback-Hub räumt sich auf — DB-Spalten,
Settings-Search-Index, Section-Name und i18n-Keys einheitlich auf
"feedback":

- DB: auth.users.community_show_real_name → feedback_show_real_name,
  community_karma → feedback_karma. Migration unter
  services/mana-auth/sql/009_rename_community_to_feedback.sql (manuell
  via psql, in Drizzle-Schema beider Services nachgezogen).
- mana-auth/me.ts: PATCH /api/v1/me/profile akzeptiert jetzt
  feedbackShowRealName und gibt es im Response zurück.
- mana-analytics: feedback.ts liest authUsers.feedbackShowRealName /
  feedbackKarma, redact() + Karma-Increment + Tests entsprechend.
- Frontend: CommunitySection.svelte → FeedbackIdentitySection.svelte
  (Datei umbenannt, Property-Namen + Toast-Texte aktualisiert,
  HeartHalf-Icon, "Feedback-Identität" als Title).
- searchIndex.ts: CategoryId 'community' → 'feedback', anchor
  'community-identity' → 'feedback-identity'.
- i18n (5 locales): settings.categories.community → .feedback,
  settings.search.community_* → feedback_*. Labels DE/EN/FR/IT/ES
  jeweils auf "Feedback" + "im Feedback-Feed" angepasst.

38/38 Integration-Tests grün, validate:i18n-parity sauber, svelte-check 0.

BREAKING (intern, nicht live): Frontend, das gegen die alten Spalten- /
Property-Namen aus dem PATCH-Response geht, fällt jetzt um. Kein
Production-Risiko da Hub noch nicht öffentlich.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:09:58 +02:00
Till JS
6f83fba66a docs(reports): geocoding self-hosting decision — recommend Photon on mana-gpu
Compares Pelias / Nominatim / Photon for self-hosting on the GPU
server, with current (2026-04-28) numbers from upstream docs +
GraphHopper's Photon-data downloads:

  Photon Europe pre-built dump: 30.6 GB, weekly refresh
  Photon Germany pre-built dump: 5.8 GB, weekly refresh
  Nominatim Germany import:     ~100 GB disk, 8–12 h, 12 GB RAM
  Pelias DACH (current):         3 GB RAM, 4 services, JS patch hack

Recommendation: Photon Europe-wide on mana-gpu. Single Java process,
embedded OpenSearch, no PBF import (download a tarball, restart),
weekly auto-updates from GraphHopper, integrates with the wrapper's
existing PhotonProvider via just an env-var change.

Once self-hosted, Photon registers as `privacy: 'local'` — the
sensitive-query block (Hausarzt, Klinikum, …) gets a real local
backend and no longer has to return empty results when Pelias is
down. Public Photon stays in the chain as a `privacy: 'public'`
last-resort fallback.

Migration plan included (~3–4 h total, ~1 h waiting), with
phase-by-phase risk assessment.

Pelias does not return — the 3 GB RAM + multi-container + patched
JS combination has no operational case once we have a self-hosted
Photon that already matches our wrapper's wire format.
2026-04-28 17:04:30 +02:00
Till JS
b1fa55dbca feat(places): surface geocoding privacy notices in autocomplete UI
The mana-geocoding wrapper now returns `notice: 'fallback_used' |
'sensitive_local_unavailable'` alongside results so the UI can show
the user *why* a query had unusual behavior. This commit wires that
all the way through the Places module's address-autocomplete inputs.

Geocoding client (lib/geocoding/index.ts):
- Add `GeocodingNotice` and `SearchOutcome` types
- Add `searchAddressDetailed` and `reverseGeocodeDetailed` — same
  semantics as the existing functions but return the wrapper's
  provider/notice metadata. Existing `searchAddress`/`reverseGeocode`
  stay backward-compatible (they call the detailed variants under
  the hood and discard the metadata).
- Extend GeocodingResult with optional `provider` field.

Places ListView (the only current consumer that exposes typed
addresses to users):
- Both autocomplete inputs (tracking-edit + main address-search)
  now use searchAddressDetailed and surface notices inline.
- 'sensitive_local_unavailable' renders an amber explainer block in
  the dropdown — title + body — so the user knows why their medical
  query returned 0 hits without leaking the search to a public API.
- 'fallback_used' renders a small "≈ ungefähr" footer badge so users
  understand the result came from public OSM (less precise but
  still valid).
- The dropdown opens when EITHER results exist OR a notice is
  present — sensitive blocked queries with empty results still
  surface their explainer.

i18n: new `places.geocoding_notice.*` sub-namespace in all 5 locales
(de/en/es/fr/it) — 4 strings each. All validators green.

Other consumers (places DetailView, events, photos, contacts) keep
the existing searchAddress/reverseGeocode calls — they don't need
the privacy notices today and would just add noise. They can adopt
the detailed variant if/when the use case warrants it.
2026-04-28 16:24:15 +02:00
Till JS
112e2cc1b4 feat(feedback): rename community → feedback (module + routes + domain)
Modul, Routen und Public-Domain heißen jetzt einheitlich "feedback":

- App-Registry: id 'community' → 'feedback', name 'Community' → 'Feedback',
  Icon Megaphone → HeartHalf (passt zum bereits-globalen heart-half-Icon
  am Module-Header und im PillNav-Usermenü)
- Modul-Config: communityModuleConfig → feedbackModuleConfig
- Routen-Refs: alle href/goto-Aufrufe in Modul-Views, MyWishesView,
  Onboarding-Wish, Profile-MyWishes auf /feedback umgestellt
- /feedback/+layout: Brand "Mana Community" → "Mana Feedback", Megaphone
  → HeartHalf, "In Mana öffnen"-CTA zeigt jetzt auf /?app=feedback
- Public-Mirror Domain: community.mana.how → feedback.mana.how
  (cloudflared-config.yml + docker-compose.macmini.yml CORS_ORIGINS +
  PUBLIC_MANA_ANALYTICS_URL_CLIENT). DNS muss separat angelegt werden.
- Settings-Section: Hilfe-Text nennt jetzt feedback.mana.how

Internal: community_show_real_name + community_karma DB-Spalten bleiben
(Migration nicht im Scope dieses Renames). Settings-Search-Index-Kategorie
'community' bleibt ebenfalls — sie spiegelt das DB-Schema, nicht den
User-Begriff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 16:18:45 +02:00
Till JS
bcc21ca785 feat(geocoding): privacy hardening — sensitive-query block + coord
quantization + extended cache TTL for public answers

Three independent defenses limit what public geocoding APIs (Photon,
Nominatim) can learn from our outbound traffic:

1. **Sensitive-query block** (`lib/sensitive-query.ts`)
   Queries matching the medical/mental-health/crisis-service keyword
   list (Hausarzt, Psychiater, Klinikum, HIV, Frauenhaus, …) are
   never forwarded to public APIs. The chain detects sensitivity at
   the route layer and runs the search in localOnly mode — providers
   with `privacy: 'public'` are filtered out before iteration begins.
   When no local provider is available (Pelias stopped), a sensitive
   query returns ok:true with results:[] and notice:
   'sensitive_local_unavailable' so the UI can show a sensible
   message instead of "no results".

   The keyword list is documented inline. False negatives are the
   risk; false positives just produce a 0-result UX hit (better
   trade-off).

2. **Coordinate quantization** (`lib/privacy.ts`)
   Forward-search focus.lat/lon: rounded to 2 decimals (~1.1km).
     Enough for the bias to work, hides exact GPS.
   Reverse-geocoding lat/lon: rounded to 3 decimals (~110m).
     City-block resolution — sufficient for "what's near me?",
     avoids reverse-geocoding the user's exact front door.
   Pelias always gets full precision; quantization only on the way
   out to public APIs. New `privacy: 'local' | 'public'` field on
   the GeocodingProvider interface drives this.

3. **Extended cache TTL for public answers**
   New `cache.publicTtlMs` config option, default 7 days (vs. 24h
   for local-provider answers). LRU cache extended with optional
   `ttlOverrideMs` per entry. Same query from N users → 1 outbound
   request to Photon/Nominatim. Strongest privacy lever we have
   over public providers (we can't change their logging, only the
   rate at which we feed them queries).

Threat coverage:
   ✓ User IP / identity hidden (already true — wrapper is the proxy)
   ✓ Exact GPS hidden (quantization)
   ✓ Sensitive query content protected (block)
   ~ Non-sensitive query content visible (acceptable trade-off)
   ~ Aggregate profiling reduced ~10–100× (cache)
   ✗ TLS-level traffic analysis, compelled disclosure (out of scope)

Tests: 141 (was 115). New coverage:
- privacy.test.ts: quantization rules (locks the privacy claim)
- sensitive-query.test.ts: positive matches across categories +
  documented false positives we accept
- chain.test.ts: localOnly mode end-to-end including the load-
  bearing assertion that public providers' search() must NEVER be
  called when the chain is in localOnly mode (no race window)
- cache.test.ts: per-entry ttlOverride longer + shorter than default

Live smoke verified end-to-end:
- "Hausarzt Konstanz" with Pelias down → no public API call,
  notice: 'sensitive_local_unavailable'
- "Konstanz" → falls through to Photon, notice: 'fallback_used'
- Reverse with high-precision GPS → Photon receives quantized
  coords, returns city-block-level result
2026-04-28 16:04:56 +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
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
eaa1d7432b fix: silence two cosmetic boot-time devtools warnings
1. /api/auth/organization/get-active-member 400
   The Better-Auth org plugin returns 400 ("active organization not
   found") whenever the session has no activeOrganizationId yet — i.e.
   on every fresh inkognito login. The fetch was already tolerated
   (fetchActiveMember returns null on 400), but the network panel
   logged it as a noisy red row.

   Fix: gate the call on the localStorage hint. The hint is set by
   writeActiveSpaceHint() after every successful set-active, so its
   presence is a reliable proxy for "session has activeOrganizationId
   set". Without a hint we go straight to list + auto-activate
   Personal — same effective outcome, no 400.

2. Chrome "Autofocus processing was blocked" on /onboarding/name and
   /onboarding/wish
   The static `autofocus` attribute races the previous route's focus
   owner across the SvelteKit transition. Chrome refuses to honour
   autofocus when a document already has a focused element and warns.

   Fix: replace the attribute with `bind:this={el}` + a $effect that
   imperatively `el.focus()`s after `tick()` — by then the outgoing
   page has unmounted and there's no competing focus claim. The
   svelte-ignore directives are no longer needed and have been removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:10:15 +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
4237d84c18 i18n(drink+habits+picture): translate 3 list views via $_()
- drink/ListView: route through drink.list_view.* (today/log/empty
  + create form + ctx-menu); drops unused PencilSimple equivalent
- habits/ListView: route through habits.list_view.* (voice
  capture + tally grid + create form + ctx-menu); drops unused
  PencilSimple icon import
- picture/ListView: route through picture.list_view.* (drop overlay,
  action strip, view-mode titles, search placeholder, empty states,
  lightbox actions)

Baseline 833 → 818 (-15).
2026-04-27 22:36:57 +02:00
Till JS
0f1dbe9d4c i18n(locales): add drink+habits, extend picture for list-view sub 2026-04-27 22:32:59 +02:00
Till JS
7dfa1c74be i18n(body+mood+questions): translate picker/quick-log/question-detail via $_()
- body/components/ExercisePicker: route remaining German strings
  (dialog/close/filter_all/create-form) through body.exercisePicker.*;
  drops { default: '...' } fallbacks now that all keys resolve
- mood/components/QuickLog: route through mood.quick_log.*
- questions/views/DetailView: route through questions.detail.*
  + dynamic questions.status.<id> / questions.priority.<id> /
  questions.detail.depth_<id>; drops local statusLabels/priorityLabels/
  depthLabels const blocks (re-uses existing status+priority keys
  extended with `normal`/`urgent`)

Baseline 851 → 833 (-18).
2026-04-27 19:13:18 +02:00
Till JS
136d3fbf87 i18n(locales): extend body+mood+questions for picker/quicklog/question-detail 2026-04-27 19:11:02 +02:00
Till JS
a842537191 i18n(comic+quiz): translate picker/character-detail/play-view via $_()
- comic/components/CharacterPicker: route through comic.picker.* with
  HTML interpolation for the no-face/empty-garment alerts
- comic/views/DetailCharacterView: route through comic.character_detail.*
  + dynamic comic.styles.<id>; drops unused STYLE_LABELS import
- quiz/PlayView: route through quiz.play_view.* (back/empty/result/play
  all consolidated)

Baseline 869 → 851 (-18).
2026-04-27 18:47:37 +02:00
Till JS
5d9dc80662 i18n(locales): extend comic with picker+character_detail, quiz with play_view 2026-04-27 18:44:23 +02:00
Till JS
a5cef980ae i18n(music+profile): translate detail/hub views via $_()
- music/views/DetailView: route through music.detail.* (new namespace)
- profile/ListView: route through profile.hub.* (new sub-namespace)
  + reuse existing profile.* keys for account-section actions; TABS
  refactored from literal label → labelKey routing through $_()

Baseline 881 → 869 (-12).
2026-04-27 18:41:43 +02:00
Till JS
fa401cfeec i18n(locales): add music namespace + extend profile with hub sub 2026-04-27 18:39:50 +02:00
Till JS
b99dd60ad0 i18n(cards+finance+mood): translate 3 list/detail views via $_()
- cards/views/DetailView: route through cards.detail.* (deck-name
  fallback, prop labels, meta, confirm/toast strings)
- finance/ListView: route through finance.page.* (re-uses existing
  page namespace) + finance.list_view.empty_no_tx; drops unused
  Transaction + FinanceCategory type imports
- mood/ListView: route through mood.list_view.* (new namespace)

Baseline 899 → 881 (-18).
2026-04-27 18:38:06 +02:00
Till JS
70a06d1d9f i18n(locales): extend cards/finance + add mood namespace 2026-04-27 18:35:34 +02:00
Till JS
7339fba3aa i18n(inventory+questions+invitations): translate 3 routes via $_()
- inventory/items/[id]/+page: route through inventory.detail.* +
  dynamic inventory.status.<id>; drops local statusLabels constant
  (re-uses existing inventory.status.* keys). Fixes pre-existing
  typos endgultig/loschen/Zuruck/hinzufugen via proper translations.
- questions/+page: route through questions.home.* + dynamic
  questions.home.depth_<id>; locale-aware date formatting via
  get(locale) instead of hardcoded de-DE; drops unused
  ResearchDepth import + depthLabels const.
- accept-invitation/+page: route through invitations.accept.*
  (new namespace); space-type label still uses SPACE_TYPE_LABELS
  from shared-branding (only de/en available — locale-prefix gate).

Baseline 920 → 899 (-21).
2026-04-27 18:29:17 +02:00
Till JS
ef3243a68a i18n(locales): extend inventory + questions, add invitations namespace 2026-04-27 18:26:15 +02:00
Till JS
3abcbd4f4d i18n(wetter+profile+contacts): translate 3 detail/freeform/comparison views via $_()
- wetter/components/SourceComparison: route through wetter.comparison.*
  (also fixes pre-existing typos verfuegbar/Gefuehlt → verfügbar/gefühlt
  via proper translations across all 5 locales). Renamed unused #each
  param `_` → `_ignored` to avoid shadowing svelte-i18n's $_.
- profile/ContextFreeform: route through profile.freeform.*; injected
  markdown source label uses i18n key too
- contacts/[id]/+page: route through contacts.detail.*; replaces typoed
  "endgueltig loeschen"/"geloescht"/"Loeschen"/"Zurueck"/"E-Mail-Mobil"
  fallbacks with proper umlauted translations. Drop unused Observable
  import.

Baseline 940 → 920 (-20).
2026-04-27 18:23:29 +02:00
Till JS
c2660dd6b2 i18n(locales): add wetter + extend profile/contacts for next 3 detail/freeform/comparison views 2026-04-27 18:19:51 +02:00
Till JS
63b9ff4684 i18n(comic+guides+cards): translate 3 detail/progress views via $_()
- comic/views/DetailView: route through comic.detail.* + dynamic
  comic.styles.<id>; drop unused STYLE_LABELS import
- guides/views/DetailView: route through guides.detail.* + dynamic
  guides.categories.<id> / guides.difficulties.<id>; drop unused
  DIFFICULTY_LABELS + Section type
- cards/progress/+page: route through cards.progress.* (also fixes
  pre-existing typos "Fallig"/"Ubersicht"/"Lernsitzungen" via
  proper translations across all 5 locales)

Baseline 961 → 940 (-21).
2026-04-27 18:17:08 +02:00
Till JS
e3c2b26510 i18n(locales): add comic + extend cards/guides for next 3 detail/progress views 2026-04-27 18:14:14 +02:00
Till JS
98a9bc4dc5 i18n(agents/templates): translate /agents/templates +page.svelte via $_()
Adds agents.templates namespace covering page title + back link, header
sub copy, 2 section headings + descriptions, 4 chip variants (Agent/
Scene/Mission/Seeds with one/other plurals), detail no-agent role,
3 preview section headings (Scene-Layout/Starter-Missionen/Seeds), seed
hint + count plurals + unnamed fallback, 5 cadence variants (manual/
daily/weekly/interval/cron) with interpolation, options section + 4
checkbox labels, result strings (existing/new agent + scene + mission
active/paused + seed summary with/without failed), error fallback,
apply-button states (applying / "Template „{label}" anwenden").

Baselines: hardcoded 968 → 961 (7 cleared); missing-keys baseline unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 17:58:15 +02:00
Till JS
aa96cae8a0 i18n(wallpaper): translate WallpaperPicker via $_() — scope toggle, tabs, sections, upload, overlay
Adds wallpaper.picker namespace covering:
- Scope toggle (Alle Szenen / Nur diese Szene), Reset action
- 3 tabs (Farben/Bilder/Upload) routed via labelKey on the tab data
- Section labels (Empfohlen + Weitere)
- "Hintergrundbilder kommen bald" placeholder for empty images tab
- Upload zone (in-progress, drop, prompt, hint with formats)
- Upload error templates (failed with {status}, generic fallback)
- Loading gallery + "Eigene Bilder" section + delete-image title
- Overlay section + Weichzeichner/Abdunklung labels
- "Bild" alt fallback for media originalName

Baselines: hardcoded 975 → 968 (7 cleared); missing-keys baseline unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 16:08:37 +02:00
Till JS
4357433e7b i18n(contacts): translate /contacts +page.svelte via $_() — header, page picker, modal form
Adds contacts.page sub-namespace covering page title, header (title +
"{n} Kontakte" stats + Suchen placeholder + Neu action), 9 PAGE_META
labels (Mein Profil/Alle Kontakte/Favoriten/etc) — refactored to titleKey
routing through $_(), Modal title (edit vs new), 7 form section labels,
21 input placeholders (firstName/lastName/email/phone/company/etc),
Cancel/Save actions, "Seite hinzufügen" page-picker label.

Baselines: hardcoded 982 → 975 (7 cleared); missing-keys baseline unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:44:38 +02:00
Till JS
a5d4554c11 i18n(myday): translate ListView via $_() — 5 sections + alerts
Adds myday namespace covering 5 section headers (Tasks/Termine/Wasser/
Ernährung/Streaks), overdue alert with {n}, empty states (Keine Tasks
heute, Keine Termine), coffee+meals counters with {n} interpolation.
Misspelled "ueberfaellig"/"Ernaehrung" inputs corrected via Unicode
fallback in JSON.

Baselines: hardcoded 989 → 982 (7 cleared); missing-keys baseline unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:36:15 +02:00
Till JS
f92d6475f7 i18n(food/home): translate /food +page.svelte via $_() — header, progress cards, today's meals, links
Adds food.home sub-namespace covering page title, "Heute" heading + locale-
aware date subtitle, Verlauf/Mahlzeit actions, today's meals section, "{n}
Einträge" counter, empty state (no-meals + hint + add action), inline macro
labels ({n}g Protein/Carbs/Fett), Ziele/Verlauf footer links. Reuses
food.nutrition.* for the 4 progress-card labels.

Baselines: hardcoded 994 → 985 (7 cleared from food + 2 added by parallel
CommunitySection commit = net 9); missing-keys baseline unchanged.

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