Commit graph

1913 commits

Author SHA1 Message Date
Till JS
513e3c7496 fix(types): enable allowImportingTsExtensions, restore .ts on shared-types
The prior dance (93bb94a12 drop .ts, bb278fb3c switch to .js) kept
breaking one consumer or the other:

- bare specifiers (no extension) satisfied svelte-check but broke the
  Node ESM loader invoked via @tailwindcss/node during SSR — SSR of
  every (app) route 500'd with ERR_MODULE_NOT_FOUND on 'theme'.

- .js extensions satisfied svelte-check and Vite but still broke the
  Tailwind loader, because the files on disk are .ts — Node ESM walks
  the actual filesystem and can't rewrite .js → .ts the way tsc does
  at type-check time.

Flip the web app's tsconfig to "allowImportingTsExtensions": true and
put the .ts extensions back. tsc now accepts the imports, and Node's
loader finds the real file on disk. No build step, no emit, and the
shared-types package stays a pure source-only TS workspace.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:54:35 +02:00
Till JS
e38257b93d feat(ai): policy-gated tool executor with pendingProposals lifecycle
Adds the staging layer that turns AI-attributed tool calls into user-reviewed
proposals instead of direct writes.

- `data/ai/policy.ts` — per-tool AiPolicy (`auto` | `propose` | `deny`) with
  module-level defaults and a global fallback. `user` and `system` actors
  always bypass (they ARE the decision / are trusted subsystems).
- `data/ai/proposals/` — Proposal + Intent types, store with
  create/list/approve/reject/expire. Proposals are local-only (do NOT
  sync); the approved write syncs through the normal module path.
- `tools/executor.ts` routes by actor+policy: `auto` runs directly under
  `runAsAsync(actor, ...)`, `propose` stages a Proposal carrying rationale
  + mission metadata, `deny` refuses. `executeToolRaw` bypasses the policy
  gate — used only on the approval path where consent already exists.

Default policy is conservative: read-only and append-only self-state
(log_drink, log_meal) auto-execute; everything that mutates user-visible
records defaults to propose.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:48:53 +02:00
Till JS
d1a0d09692 feat(data): stamp actor on records and pending changes via Dexie hooks
Extends the creating/updating hooks to capture the ambient actor
synchronously and freeze it onto every write:

- `__lastActor` on each record (whole-record attribution for Workbench badges)
- `__fieldActors` parallel to `__fieldTimestamps` (field-level attribution
  for inline diff rendering — e.g. "AI changed due date, user changed title")
- `actor` on `_pendingChanges` rows so mana-sync + cross-device views can
  distinguish AI- vs user-initiated writes

Also adds `kontextDoc` to v17 (missing from schema while module was live)
alongside the new `pendingProposals` table for staged AI intents.

Actor is captured in-hook rather than at emit time because
`trackPendingChange` is deferred via setTimeout and would otherwise lose
ambient context.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:48:30 +02:00
Till JS
a18506caf6 feat(events): Actor attribution on every DomainEvent
Introduces a discriminated Actor union (user | ai | system) threaded through
the event pipeline so downstream consumers can distinguish human writes from
AI-initiated ones and derived subsystem writes.

- `EventMeta.actor: Actor` is required (no legacy fallback — pre-launch)
- `emitDomainEvent` takes an options bag `{ actor?, causedBy? }`; falls back
  to the ambient actor set by `runAs` / `runAsAsync`
- `runAs` / `runAsAsync` pin the actor at defined boundaries (tool executor,
  mission runner, projection dispatcher) — primitives capture synchronously
  so ambient context is never SoT past the write moment

Foundation for the AI Workbench. Follow-up: mana-sync server must accept
and persist `actor` in pending-change payloads.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:48:03 +02:00
Till JS
2fb2bb60fb feat(kontext): singleton markdown doc with inline editor
New Mana module "Kontext" — one editable markdown document per user,
displayed as a workbench page. Toggle between rendered preview and a
raw textarea editor (Cmd/Ctrl+E); debounced autosave 500 ms.

Content is encrypted at rest (new `kontextDoc` table, singleton row).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:46:04 +02:00
Till JS
66f8e86d59 fix(web): add missing pako dep for backup import
The .mana backup parser in src/lib/data/backup/format.ts imports
inflateRaw from pako but the package was never declared. The
production Vite build fails to resolve it — pnpm let it through
locally only because some other workspace dep hoists pako into
node_modules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:29:31 +02:00
Till JS
9a6ccf5076 fix(a11y): clear pre-push svelte-check warnings
- mail/ListView: add a11y_click_events_have_key_events ignore to match
  the existing a11y_no_static_element_interactions suppression
- sleep/MorningLog + companion/CompanionChat: mark intentional
  initial-value state reads with state_referenced_locally ignore
- goals/GoalEditor: add tabindex="-1" to the dialog role element

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:12:14 +02:00
Till JS
93bb94a121 fix(types): drop .ts extensions + narrow Uint8Array buffer type
- shared-types/index.ts: drop .ts import extensions (web-app tsconfig
  has no allowImportingTsExtensions; bundler resolution handles it)
- backup/format.test.ts: narrow buildZip return to Uint8Array<ArrayBuffer>
  so the Blob() constructor accepts it under strict lib.dom

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:07:22 +02:00
Till JS
73e3fdbbed feat(meditate): add meditation module with presets, sessions, breathing UI
Local-first module with meditatePresets/Sessions/Settings tables, hub
ListView with stats + recent sessions, and SessionPlayer with
BreathingCircle + MoodPicker. Route at /meditate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:50:13 +02:00
Till JS
d11f6aebf7 chore: ignore vite-plugin-pwa dev-dist output
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:50:03 +02:00
Till JS
7c1c6cd54c chore(audit): module complexity reports + workbench map
Adds four audit scripts (module health, inter-module coupling, per-function
cognitive complexity, D3 treemap) with generated reports under docs/ and
an iframe-embedded workbench app at /admin/complexity. Reports regenerate
weekly via the module-health GitHub Action.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:47:42 +02:00
Till JS
b857063120 refactor: rename eventstream -> activity, cycles -> period
eventstream was confusingly branded "Events" in the app registry,
colliding with the real events calendar module. Renamed to activity
(DE: Aktivität) since it's a live activity feed across all modules.

cycles -> period (DE: Periode) makes the menstrual-tracking module
self-describing. Tables cycles/cycleDayLogs/cycleSymptoms renamed to
periods/periodDayLogs/periodSymptoms; field cycleId -> periodId;
TimeBlockType 'cycle' -> 'period'; domain event CycleDayLogged ->
PeriodDayLogged. Generic "cycle" usages (billing, lifecycle, breath,
bicycle, import cycles) left untouched.

Constant disambiguation: prior DEFAULT_PERIOD_LENGTH (bleeding days)
renamed to DEFAULT_BLEEDING_DAYS; prior DEFAULT_CYCLE_LENGTH (28d full
cycle) is now DEFAULT_PERIOD_LENGTH.

Pre-launch, no data migration needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:45:43 +02:00
Till JS
66cda80620 feat(settings): inline BYOK key manager under AI tier card
Moves the BYOK key CRUD from the standalone /settings/ai-keys subpage
directly under the new BYOK tier card in the main AI settings section.
Users now manage keys in-context where they toggle the tier.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:42:40 +02:00
Till JS
4f33435607 docs(sync): document backup/restore pipeline + stability contract
- DATA_LAYER_AUDIT.md: new section 8 covering the export/import flow
  end-to-end — architecture diagram, .mana format, protocol-stability
  commitments we locked in pre-launch (eventId + schemaVersion + op
  vocab + tombstones-forever), encryption-boundary argument, file
  map, and the remaining backup backlog (M4b, M5, signature,
  resumable download, dedup table).
- services/mana-sync/CLAUDE.md: /backup/export row in API table with
  explicit note that it sits outside the billing gate, new Backup /
  Restore section with format sketch + split between writer.go (pure)
  and handler.go (shim), test-coverage line mentions the backup cases,
  project-structure tree lists backup/*.go, Security section mentions
  RLS still applies to the export path.

No code changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 17:48:47 +02:00
Till JS
e219d38436 test(sync): 8 vitest cases for .mana zip parser
Builds synthetic PKZIP archives in-memory (same deflateRaw the runtime
uses on the inflate side) and asserts:

- round-trip through parseBackup surfaces manifest + events + matching
  sha256
- events.jsonl iteration yields both records with fieldTimestamps intact
- wrong formatVersion is rejected with a clear error
- missing manifest.json or events.jsonl is rejected by name
- non-zip input is rejected at EOCD scan
- sha mismatch surfaces as differing manifest vs computed hash fields
- iterateEvents skips blanks + throws on malformed JSON

This is the only untrusted-input frontier in the backup flow, so it
earns a real test harness rather than relying on integration smoke.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:51:47 +02:00
Till JS
7aee552ab4 feat(sync): .mana backup import — zip parser + replay (M4a)
Client-side restore for the same-account case:

- lib/data/backup/format.ts: hand-rolled .mana (zip) parser. Walks the
  central directory, inflates DEFLATE entries via pako (already in the
  repo), exposes manifest + events.jsonl + recomputed sha256. No new
  dependency; the archive shape is narrow enough that 200 lines cover it.
- lib/data/backup/import.ts: validates manifest (userId match is hard-
  refused, eventsSha256 must match, schemaVersionMax ≤ client support),
  streams events through iterateEvents(), batches 300 per appId and
  replays via the existing applyServerChanges() path. LWW makes the
  operation idempotent.
- settings/my-data: file picker, progress bar, per-phase labels, success
  summary with event count + source timestamp.

Scope is intentionally same-account only: events originate from the
server for this user, so re-pushing them is unnecessary. Cross-account
migration needs the MK transfer path in M5.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:49:08 +02:00
Till JS
cf9f4ecd52 fix(llm): per-task tier override bypasses global allowedTiers gate
Bug: setting taskOverrides['companion.chat'] = 'byok' didn't work
when the user's allowedTiers was empty/['none']. The tier-too-low
check in run() compared task.minTier ('browser') against userMaxTier
('none') and threw TierTooLowError before the override was even read.

Same issue in canRun() and candidateTiers().

Fix: when a per-task override exists, treat it as opt-in to that tier
even if not in the global allowedTiers. The override is the user's
explicit per-task signal — overriding the global default is exactly
what an override is for.

- run(): effectiveMaxTier = max(override, userMaxTier)
- candidateTiers(task, override): adds override to baseTiers
- canRun(): now passes the override to candidateTiers

The Companion chat now correctly uses BYOK when selected from the
toolbar, even if the user hasn't enabled BYOK in their global LLM
settings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:19:50 +02:00
Till JS
e95d0487b9 perf(workbench): split SceneAppBar registration from prop updates
The single $effect that wired SceneAppBar into bottomBarStore was
re-writing barComponent on every reactive tick — every change to
openApps, locale or activeSceneId redirected through .set() and
re-assigned the component reference identically.

Add a setProps() method to bottomBarStore that mutates only barProps,
and split the workbench effect in two: a registration effect that
fires on the scenes-empty/non-empty transition, and a props effect
that pushes fresh data without touching barComponent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:13:32 +02:00
Till JS
1249cc49e5 feat(sync): backup as .mana zip with manifest + sha256 (M3)
Replace the raw JSONL response with a zip container:

  events.jsonl  — one SyncChange per line, as before
  manifest.json — formatVersion, schemaVersion, userId, eventCount,
                  eventsSha256, apps, timestamps, schemaVersionMin/Max

Single DB pass: events.jsonl is written while a sha256 hasher tees
every byte of the uncompressed JSONL. The manifest lands as a second
zip entry after the stream closes, so eventsSha256 is filled without
rescanning.

Integrity-check on the restore side becomes trivial (re-hash the
decompressed events.jsonl and compare). Signature over manifest.json
is deferred to a later phase; sha256 already catches corruption.

Client-side: default filename + UI label updated to .mana. Fetch flow
is unchanged — browser gets a zip blob and triggers a download.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:49:47 +02:00
Till JS
53b3746b98 refactor: rename nutriphi module to food (Essen)
Complete rename across the entire monorepo pre-launch:
- Module, routes, API, i18n, standalone landing app directories
- All code identifiers, display names, logo component
- German user-facing label: "Essen" (English brand stays "Food")
- Dexie table nutriFavorites -> foodFavorites
- Infra configs (docker-compose, cloudflared, nginx, wrangler)

Zero residue of nutriphi remains. No data migration needed (pre-launch).

Follow-up: run pnpm install, update Cloudflare DNS
(food.mana.how), rename Cloudflare Pages project.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:30:07 +02:00
Till JS
f5cb833b04 perf(workbench): lazy-mount carousel cards via IntersectionObserver
Each open workbench app card previously mounted its full ListView with
its own Dexie liveQuery on initial render — so 5+ open apps meant 5+
parallel IndexedDB reads and 5+ async chunk fetches before first paint,
even though only the 1-2 cards in the horizontal viewport are visible
at a time.

PageCarousel now wraps each card in an IntersectionObserver-driven
gate. The first card mounts eagerly so paint isn't gated on observer
callback timing; the rest swap in a fixed-size placeholder until they
enter the viewport (with 50% horizontal overshoot so the next card on
either side is ready before the user scrolls to it). Mount is sticky
— once a card has been instantiated we keep it resident, since
re-running a liveQuery and re-fetching its chunk costs more than
keeping the DOM tree around.

Affects all three carousel users: workbench /, /todo, /contacts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:27:32 +02:00
Till JS
ceb5f72f12 feat(sync): wire /backup/export route + client + settings UI (M1 tail)
Recovering three files dropped when a parallel terminal reset past the
original M1 commit:

- cmd/server/main.go: register GET /backup/export outside billingMiddleware
- lib/api/services/backup.ts: browser-side downloadBackup() helper
- settings/my-data/+page.svelte: "Backup & Wiederherstellung" section

Pairs with the earlier backup handler + schema_version work already on
main (79996f946). With this commit the endpoint is actually reachable
end-to-end and the download button works.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:26:30 +02:00
Till JS
79996f946a feat(sync): schemaVersion + eventId on wire (M2 protocol hardening)
- sync_changes gains schema_version column (default 1, idempotent ADD)
- Change/Changeset carry schemaVersion; server refuses > MaxSupported
- server->client changes now carry eventId + schemaVersion so the
  restore path can dedup via eventId and route through a migration
  chain keyed on schemaVersion
- backup JSONL gains schemaVersion per line

Pre-M2 clients (omit the field) are treated as v1 for compatibility.
This is the stability contract we commit to before launch: once v1
events are in the wild, all future builds must replay them forward.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:25:32 +02:00
Till JS
e4f0a410d1 test(byok): add 35 unit tests + update docs to as-built status
Three new test suites covering the critical BYOK paths:

Pricing (14 tests): estimateCost for known/unknown models, scaling,
formatCost edge cases, coverage check for all model IDs.

ByokBackend (10 tests): tier identification, resolver behavior,
provider dispatch, parameter passthrough, onUsage callback, error
paths (no key, unregistered provider), invalidateAvailability.

ByokVault (11 tests): encryption at rest verification, decryption
round-trip, auto-default for first key, promoting default demotes
previous, getForProvider logic, listMeta excludes apiKey, soft
delete, recordUsage accumulation, cross-provider isolation.

Updates docs/architecture/BYOK_PLAN.md with as-built status —
phase table with commit references, deviations from original plan
(no server-proxy fallback, no sensitive opt-in UI, no per-task
provider override yet), test coverage matrix, troubleshooting
guide, v2 follow-ups.

Provider adapters remain unit-untested (need fetch mocking + SSE
parsing) — smoke tests only.

Total: 35/35 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:23:03 +02:00
Till JS
7c6567a815 perf(web): re-apply (app) layout idle-defer + lazy modals after BYOK merge
The BYOK vault commit (db8c2574d) reverted the layout to its pre-perf
shape while integrating initByok(). Restore the perf optimizations from
b196e7782 and slot initByok() into the Phase A-idle block alongside the
other side-effect initializers.

Verified bundle impact (perf vs pre-perf parent):
- (app)/+layout chunk: 42 KB → 26 KB gzip (-38%)
- 7 lazy chunks split out (modals on demand, toasts after idle)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:22:31 +02:00
Till JS
db8c2574d6 feat(byok): IndexedDB vault + settings UI for user API keys
Phase 4-5 of BYOK. Wires the BYOK backend into the running app and
gives users a UI to manage their keys.

Data layer (_byokKeys table, v16 schema):
- Encrypted at rest via user master key (wrapValue/unwrapValue)
- NOT in SYNC_APP_MAP — keys stay device-local on purpose
- Tracks per-key usage: count, total tokens, cumulative USD cost

byokVault API (lib/byok/):
- listAll() / listMeta() — decrypt on read
- create() — encrypts + auto-handles isDefault collisions
- update() — label/model/isDefault edits (not the key itself)
- delete() — soft-delete
- recordUsage() — called from backend's onUsage callback
- getForProvider() — resolver helper

initByok() wires the ByokBackend into the shared orchestrator at
app startup. The resolver picks the most-recently-used provider's
key by default. Usage callback updates the key's counters + cost
via estimateCost().

Settings page (/settings/ai-keys):
- List existing keys with provider, label, model, usage stats
- Add form: provider picker, label, API key (password input),
  model selector with provider defaults, isDefault toggle
- Inline edit for label + model
- Set-as-default action
- Soft-delete with confirmation
- Gracefully handles locked vault state

Companion chat dropdown already picks up the new 'byok' tier
from ALL_TIERS — user can select BYOK directly as KI-Modus.

Total BYOK implementation: ~1500 LOC across 12 files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:14:00 +02:00
Till JS
a33857fa39 feat(llm): add BYOK tier + 4 provider adapters (OpenAI, Anthropic, Gemini, Mistral)
Phase 1-3 of BYOK support. Introduces a 5th LLM tier 'byok' that
routes to user-provided API keys via direct browser fetches.

shared-llm additions:
- LlmTier extended with 'byok' (rank 3, between mana-server and cloud)
- ByokBackend: LlmBackend implementation that delegates key lookup
  to an app-provided resolver callback, then dispatches to the right
  provider adapter
- 4 provider adapters:
  - OpenAI (gpt-5, gpt-4o, o1 family)
  - Anthropic (Claude Opus/Sonnet/Haiku 4.6) with CORS header
  - Gemini (2.5 Pro/Flash) — REST API with different message format
  - Mistral — OpenAI-compatible, reuses shared openai-compat adapter
- Pricing table for 20+ models with USD per 1M tokens
- estimateCost() + formatCost() helpers

Keys stay device-local (IndexedDB in next phase). Browser-direct
fetches mean keys never touch Mana's server.

Updates two existing tier maps (memoro DetailView, SourceBadge) to
include the new tier.

Planning doc at docs/architecture/BYOK_PLAN.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:06:48 +02:00
Till JS
3817111f80 feat(themes): redesign theme picker with gradient cards + beefy mode selector
Replace the shared ThemePage component inside the Themes workbench
panel with a custom compact layout better suited to the narrow
(~300px) panel context. ThemePage was designed for a full-width
desktop route and reads as noisy/overloaded in a panel.

Mode selector (Hell/Dunkel/System) — primary-fill active state with
white icon+text (was subtle shadow-sm that barely registered in dark
mode), fill-weight icons when active, equal-width pill buttons in a
shared muted container.

Theme cards (Option D — "Farbton-Karte") — swap the 2×5 overlapping
color-dot preview for a large 16:10 gradient (primary → secondary in
the effective mode), theme name overlaid bottom-left with text-shadow,
subtle dark-overlay at the bottom for readability, white check badge
in the corner when active, 2px primary border + glow ring for the
active state. Hover lifts the card 1px. Renders all 8 variants
(default + extended) in a uniform 2-column grid.

Wallpaper tabs (Farben/Bilder/Upload + scope toggle) — restyle via
scoped :global() overrides to match the mode selector: muted pill
container, primary-fill active state, muted-foreground inactive.
Previously these used .bg-surface + .shadow-sm which was nearly
invisible against the panel background.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 14:58:10 +02:00
Till JS
b196e7782e perf(web): idle-defer non-critical (app) init + lazy-load modals
Homepage/workbench TTI was dominated by the layout synchronously booting
event-bridge, streak tracker, LLM queue, memoro watcher, dashboard store,
shared-uload and reminder scheduler before first paint, plus statically
importing 7 modals/toasts/banners that are rarely-to-never visible on
initial render.

- Keep critical path inline: local-store init (manaStore/tag/link),
  unified sync engine + billing, guest-mode setup.
- Move side-effect streams, projection workers and telemetry to
  requestIdleCallback (with setTimeout fallback).
- Dynamically import KeyboardShortcutsModal, OnboardingWizard,
  GuestWelcomeModal, SessionWarning, EncryptionIntroBanner,
  SuggestionToast, NudgeToast. Modals fetch on first demand; toasts
  mount after idle so their transitive deps (automationsStore,
  day-snapshot projection, streaks, crypto gate) don't land in the
  initial chunk.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 14:41:27 +02:00
Till JS
c357a1cd1d feat(brain): AI tier selector in Companion chat toolbar
Dropdown at the top of the chat with options: Auto, Keine, Browser,
Mana-Server, Cloud. Selecting a tier sets
settings.taskOverrides['companion.chat'] so the choice only affects
the Companion, not other LLM tasks. "Auto" clears the override and
lets the orchestrator pick the user's preferred tier.

Also shows the current auto-selected tier inline so the user knows
what Auto resolves to, e.g. "Auto (Browser)".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 14:15:30 +02:00
Till JS
e885713fd0 refactor(brain): migrate Companion engine to LlmOrchestrator (4-tier system)
Replaces the ad-hoc local-first + server-fallback pattern with the
shared LlmOrchestrator, giving the Companion Chat full access to
the 4-tier system (none/browser/mana-server/cloud) and its privacy
+ user-preference enforcement.

New companionChatTask (lib/llm-tasks/companion-chat.ts):
- name: 'companion.chat'
- minTier: 'browser' (no rules fallback — needs an LLM)
- contentClass: 'personal' (allows server/cloud if user opted in;
  NOT 'sensitive' because the chat isn't restricted to browser-only,
  but the user can set it per-task via taskOverrides)
- requires: { streaming: true }

Engine changes:
- callLlm() now delegates to llmOrchestrator.run(companionChatTask, ...)
- Still preloads the local model when browser tier is available so
  the UI can show download progress
- isCompanionAvailable() now asks llmOrchestrator.canRun() which
  considers user settings + backend readiness + consent gates

User benefits:
- Tier-selector in the PillNav now applies to Companion Chat
- Users can force cloud/server/browser per-task via settings overrides
- Cloud tier only runs when cloudConsentGiven is set
- Privacy: content marked 'sensitive' in other tasks (Journal etc.)
  is still restricted to browser/rules — Companion respects the
  same orchestrator so privacy invariants hold consistently

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 14:09:57 +02:00
Till JS
4b2007e97c fix(pwa): wire up manifest link + SW registration so install prompt works
The PWA was configured end-to-end in vite.config.ts and built the
manifest + service worker correctly, but neither was ever loaded by
the browser — no <link rel="manifest"> in the HTML and no script
registering the generated registerSW.js. Chrome therefore never fired
beforeinstallprompt, no install icon appeared in the URL bar, and the
hand-rolled PwaUpdatePrompt hung on navigator.serviceWorker.ready
because no SW had been registered.

Changes:
- Render pwaInfo.webManifest.linkTag into <svelte:head> in the root
  layout so Chrome finds the hashed manifest.
- Replace the hand-rolled SW-update logic in PwaUpdatePrompt with
  useRegisterSW() from virtual:pwa-register/svelte — it registers the
  worker (immediate: true) and exposes reactive needRefresh +
  updateServiceWorker stores that match registerType: 'prompt'.
- Add triple-slash refs to vite-plugin-pwa/info and /svelte in
  app.d.ts for the virtual module types.
- Set manifest.id = startUrl in @mana/shared-pwa so Chrome doesn't
  warn and keeps the install identity stable across start_url edits.
- Keep devEnabled: false and expand the comment: the 2026-04-08
  dreams mic-button bug and the /offline navigateFallback both
  misbehave when Workbox precache doesn't run under vite dev. Test
  the install flow via `pnpm build && pnpm preview` instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 14:05:49 +02:00
Till JS
4192a4bd9b feat(brain): emit Companion chat + tool events for observability
Closes the gap where the Companion module wrote messages directly to
IndexedDB without participating in the Domain Event stream — chat
activity, tool invocations and conversation creation were invisible
to the Event Stream page, Goals, Streaks, and Memory layer.

New events (3 types):
- CompanionConversationStarted: emitted on chatStore.createConversation
- CompanionMessageSent: emitted on user/assistant messages (skips
  empty tool plumbing messages)
- CompanionToolCalled: emitted in engine.runCompanionChat after every
  tool execution, with tool name, source module, success/failure,
  latency in ms, and error message on failure

Event Stream page updated with icons (ChatCircle, Robot) and German
labels for the three new event types so they appear inline with all
other domain activity.

Now possible (future iterations):
- Goals like "5x Companion-Chat pro Woche"
- Streaks for daily Companion usage
- Tool performance analytics ("create_task hat 3% Fehlerrate")
- Memory facts about which tools the user uses most

Total: 70 event types, 51 tools across 31 modules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 13:54:43 +02:00
Till JS
9ff2cfcdac feat(workbench): unify system pages as workbench apps + categorize picker
Add 8 system pages as first-class workbench apps (Settings, Themes,
Profile, Admin, API Keys, Help, Feedback, Subscription) so they can be
opened as side-by-side panels next to other apps instead of requiring
a full-page route switch. Existing routes remain as fullscreen
fallback/deep-link targets.

Group the AppPagePicker by 5 categories (Companion, Leben, Arbeit,
Kreativ, System) with collapsible sections; System is collapsed by
default. Search still works as a flat fuzzy match across all apps.
Category assignment lives in a central map so registerApp() calls stay
unchanged — unmapped apps fall back to System, which surfaces
miscategorization at a glance.

Remove profile-data and theme-picker duplication from Settings (both
are separate workbench apps now): Settings defaults to 'Allgemein' and
passes showTheme={false} to GlobalSettingsSection; SettingsSidebar
accepts a categories override so the workbench version hides Profile.

Fix Cannot-read-'subscribe'-of-undefined crash in mood/sleep/body/
stretch ListViews when opened in the workbench: replace getContext
(which is only set by the route +layout.svelte) with direct query-hook
calls, matching the goals/companion pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 13:48:44 +02:00
Till JS
51c8a52811 fix(brain): companion can now act on previous tool results across turns
Five fixes from observed chat where user asked to complete two
tasks by title but the LLM had no way to find their IDs:

1. Tool result history: messagesToLlm() now includes previous
   tool_result messages as "[Previous tool result]" entries so
   the LLM can reference IDs/data from earlier turns.

2. Bare JSON tool call fallback: extractToolCall() now also
   matches bare {"name":..., "params":...} JSON without the
   ```tool fence — the LLM kept dropping the fence.

3. IDs in list message: list_tasks now formats each entry as
   "• [abc123] Title" so the LLM has the ID alongside the title.

4. New complete_tasks_by_title tool: case-insensitive substring
   match, completes all matches at once. Handles "erledige beide
   sicher sicher tasks" without needing IDs.

5. System prompt updates: explains the [id] bracket convention,
   warns the LLM to NEVER show raw IDs to users, and references
   the new tool for title-based completion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 13:41:43 +02:00
Till JS
77d455a18d fix(brain): companion can now actually list tasks instead of hallucinating
Two fixes for the chat where the user asked "show my tasks" and the
LLM responded with "I don't have direct access to a dynamic task list":

1. New list_tasks tool with title/dueDate/priority for each task,
   filterable by open/completed/overdue/today/all (default: open).
   The message field returns a markdown bullet list so the LLM can
   pass it through directly.

2. System prompt rewritten to explicitly tell the LLM:
   - WHEN to call which tool (concrete examples for common questions)
   - "Erfinde keine Daten" — don't make up info, call a tool
   - Tool format must be JUST the JSON block, no preamble

Common questions now have explicit mappings:
- "Welche Tasks?" → list_tasks
- "Wie viel Wasser?" → get_drink_progress
- "Welche Termine heute?" → get_todays_events

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 13:31:13 +02:00
Till JS
daa5aaf0a1 fix(brain): companion chat — markdown rendering, loading status, streaming feedback
Three UX fixes for the Companion Chat:

1. Markdown rendering: assistant messages now render as HTML via
   marked (gfm + breaks). **bold**, _italic_, lists, code blocks,
   links all work. Custom CSS for Mana theme integration.

2. Loading status: while the local Gemma model downloads/loads
   (first use is ~500MB), the placeholder bubble shows "Modell
   wird geladen... 42%" with a spinning icon instead of just a
   blank "thinking" state.

3. Streaming feedback: the placeholder bubble appears immediately
   when sending and shows tokens as they stream from the LLM,
   rendered as markdown in real-time. Auto-scrolls on each chunk.

The pulse animation on the streaming markdown gives a subtle
"this is generating" hint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 13:28:16 +02:00
Till JS
5ae78998d7 test(brain): add 29 unit tests for Event Bus, Tools, Goals, Streaks, Correlations
5 test suites covering the critical Companion Brain systems:

Event Bus (7 tests): typed delivery, onAny, unsubscribe, off(),
error isolation between handlers, multiple handlers

Tool Executor (8 tests): valid execution, unknown tool, missing
required params, string→number coercion, invalid coercion, enum
validation, error catching, optional params

Goal Tracker (6 tests): create from template, pause/resume, soft
delete, event-driven increment, filter matching, paused goals
not tracked

Streak Tracker (5 tests): event-driven seeding, no double-count
same day, filter correctness (water only), water event tracking,
independent multi-streak tracking

Correlation Engine (3 tests): insufficient data returns empty,
positive correlation detection, cross-module only filtering

Also fixes race condition in streaks.ts markActive() — concurrent
events could both try to seed the same streak, causing ConstraintError.
Now caught with try/catch.

All 29 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 00:14:55 +02:00
Till JS
399e927c00 feat(brain): add Goal Editor UI and event-driven incremental streaks
Final two TODOs resolved — the Companion Brain backlog is now empty.

Goal Editor (GoalEditor.svelte):
- Modal with event type picker (13 options), count/sum mode,
  optional filter field/value, target value/period/comparison
- Integrated into Goals ListView with "Eigenes" button alongside
  the existing "Vorlage" template picker
- Creates custom goals via goalStore.create()

Incremental Streaks (rewritten streaks.ts):
- Persistent _streakState table replaces the 90-day lookback scan
- 6 streak definitions: water goal, tasks, meals, workout, journal,
  meditation — each triggered by specific domain events
- Event bus subscription marks streaks active on matching events
- markActive() is O(1): read state → check if today already active
  → increment or reset based on consecutive day check
- useStreaks() reads from _streakState (single table scan, no
  per-day queries) instead of 270+ queries worst case
- startStreakTracker/stopStreakTracker wired into layout lifecycle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 23:52:45 +02:00
Till JS
677f6b799d feat(brain): add NudgeToast, server LLM fallback, trigger-event bridge
Three remaining TODOs resolved:

1. NudgeToast (in-app nudge display):
   - New NudgeToast.svelte in bottom-stack alongside SuggestionToast
   - Evaluates Pulse Rules every 60s, shows nudges as toasts
   - Action button navigates to module route, dismiss records outcome
   - Badge shows count when multiple nudges are queued

2. Server LLM fallback:
   - Companion engine now tries local LLM (Gemma/WebGPU) first
   - Falls back to mana-api /api/v1/chat/completions if no WebGPU
   - isCompanionAvailable() returns true if either path works
   - Graceful error messages when neither is available

3. Trigger-Event bridge (legacy automation migration):
   - event-bridge.ts maps 13 domain event types to legacy
     (appId, collection, op) format
   - Existing user automations now fire on domain events too
   - Domain events carry decrypted data → condition matching on
     encrypted fields (title, etc.) works correctly
   - Bridge wired into layout startup/cleanup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 23:45:37 +02:00
Till JS
d6d50e4d94 feat(brain): add meditate+sleep, parallelize DaySnapshot, deprecate _activity
Final optimization pass for the Companion Brain.

New modules (31 total):
- Meditate: MeditationCompleted event + log_meditation tool
- Sleep: SleepLogged event + log_sleep tool

Performance: DaySnapshot buildSnapshot() now runs all 6 Dexie
queries + 4 decryption passes in parallel via Promise.all instead
of sequentially. Estimated 3-5x speedup on first render.

Cleanup: trackActivity() in database.ts is now a no-op — the
_activity table is no longer written to. getRecentActivity() in
activity.ts delegates to queryEvents() from the Domain Event Store,
converting domain events to the legacy ActivityEntry shape.

Totals: 69 event types, 49 tools across 31 modules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 23:37:33 +02:00
Till JS
c95aaa4d48 feat(brain): add domain events + tools for remaining 9 modules
Batches 5+6: extends to 29 modules. Adds events and tools for cycles,
firsts, guides, inventory, photos, plants, news, recipes, questions.

New domain events (12 types):
- Cycles: CycleDayLogged
- Firsts: FirstCreated
- Guides: GuideCreated
- Inventory: InventoryItemCreated
- Photos: PhotoDeleted
- Plants: PlantCreated, PlantDeleted
- News: ArticleSaved
- Recipes: RecipeCreated, RecipeDeleted
- Questions: QuestionAsked

New tools (7 tools):
- Cycles: log_cycle_day
- Firsts: create_first
- Guides: create_guide
- Inventory: create_inventory_item
- Plants: create_plant
- Recipes: create_recipe

Skipped (no simple create API): context (read-only), news (complex
LocalCachedArticle input), questions (requires questionId).

Totals: 67 event types, 47 tools across 29 modules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 23:26:57 +02:00
Till JS
c7de86282b feat(brain): add domain events + tools for music, storage, chat, memoro, skilltree
Batch 4: extends to 20 modules. Adds events and tools for music
(song added, playlist created), storage (folder created), chat
(message sent, conversation created), memoro (memo created), and
skilltree (skill created, XP added).

New domain events (10 types):
- Music: SongAdded, PlaylistCreated
- Storage: FolderCreated, FileDeleted
- Chat: ChatMessageSent, ChatConversationCreated
- Memoro: MemoCreated, MemoDeleted
- Skilltree: SkillXpAdded, SkillCreated

New tools (8 tools):
- Music: create_playlist
- Storage: create_folder
- Chat: create_chat_conversation
- Memoro: create_memo
- Skilltree: add_skill_xp, create_skill

Totals: 55 event types, 40 tools across 20 modules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 23:07:34 +02:00
Till JS
7752ba9ff9 feat(brain): add domain events + tools for finance, dreams, cards, times, events
Extends the Companion Brain to 15 modules. Adds semantic domain events
and LLM tools for finance, dreams, cards, times, and social events.

New domain events (10 types):
- Finance: TransactionCreated, TransactionDeleted
- Dreams: DreamCreated, DreamDeleted
- Cards: CardCreated, CardStudied
- Times: TimerStarted, TimerStopped
- Social Events: SocialEventCreated, SocialEventDeleted

New tools (7 tools):
- Finance: add_transaction
- Dreams: create_dream
- Cards: create_card
- Times: start_timer, stop_timer
- Events: create_social_event

Totals: 45 event types, 32 tools across 15 modules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 23:00:01 +02:00
Till JS
c51382a76e feat(brain): add domain events + tools for habits, journal, notes, contacts, body
Extends the Companion Brain to 10 modules (from 5). Adds semantic
domain events and LLM tools for the next 5 most valuable modules.

New domain events (15 types):
- Habits: HabitLogged, HabitCreated, HabitDeleted
- Journal: JournalEntryCreated, JournalMoodSet, JournalEntryDeleted
- Notes: NoteCreated, NoteDeleted
- Contacts: ContactCreated, ContactDeleted
- Body: WorkoutStarted, WorkoutFinished, SetLogged,
  MeasurementLogged, EnergyCheckLogged

New tools (12 tools):
- Habits: log_habit, get_habits, create_habit
- Journal: create_journal_entry, set_mood
- Notes: create_note
- Contacts: create_contact, get_contacts
- Body: start_workout, finish_workout, log_measurement

Totals: 35 event types, 25 tools across 10 modules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:48:15 +02:00
Till JS
4211ce68da feat(brain): add 4 Companion Brain workbench pages
Registers Mein Tag, Event Stream, Companion Chat, and Ziele as
workbench apps so they can be added to scenes alongside existing
modules like Todo, Calendar, etc.

New workbench pages:
- Mein Tag (myday): DaySnapshot overview — tasks, events, water
  progress, nutrition, streaks at a glance
- Events (eventstream): live domain event feed with icons, labels,
  and timestamps — shows the system "pulse" in real-time
- Companion (companion): embedded chat interface that auto-creates
  a conversation on first use
- Ziele (goals): goal cards with progress bars, template picker
  for quick goal creation, pause/resume/delete

Each page registered in both app-registry (workbench views) and
shared-branding (app metadata, icons, descriptions, tier=guest).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:33:53 +02:00
Till JS
e1884cfbd1 fix(brain): fix companion page not rendering — schema version + query robustness
- Bump companion tables to db.version(14) so the schema upgrade
  runs even on browsers that already saw v10 with only _events
- Add try/catch to useConversations() and useMessages() queries
  to prevent silent crashes if tables don't exist yet
- Use db.table() directly in chat store instead of collections.ts
  module-level references (avoids eager table resolution issues)
- Add min-height to empty state and companion page container

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:19:59 +02:00
Till JS
87a1dd6829 feat(brain): add Semantic Memory, Pattern Extractors, and Correlation Engine
Phase 7 (final) of the Companion Brain architecture.

Semantic Memory (companion/memory/):
- MemoryFact model with confidence lifecycle (0.3 initial, +0.15 confirm,
  -0.15 contradict, weekly decay after 30 days, delete below 0.1)
- Store with recordFact (upsert by factKey), contradictFact, applyDecay
- 3 pattern extractors: day-of-week (recurring days), time-of-day
  (peak 4h window), frequency (daily average) — all rule-based, no LLM
- Runs across all 5 pilot modules (11 extraction rules total)

Correlation Engine (data/projections/correlations.ts):
- Pearson correlation between 7 daily metrics across 4 modules
- Metrics: tasks completed, water ml, coffee count, calories, meals,
  calendar events, places visited
- Only returns cross-module correlations with |r| >= 0.3 and >= 14 days
- Natural language sentence generation for each correlation

Context Document updated:
- Now accepts optional memory facts + correlations
- Appends "Bekannte Muster" section (top 6 high-confidence facts)
- Appends "Zusammenhaenge" section (top 3 correlations with r-value)

This completes all 7 phases of the Companion Brain architecture.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:07:46 +02:00
Till JS
41357b2541 feat(brain): add Ritual system with guided routines and step execution
Phase 6 of the Companion Brain. Introduces guided routines ("rituals")
that walk users through multi-step sequences, executing tools and
displaying projection data at each step.

Data layer (companion/rituals/):
- LocalRitual + LocalRitualStep + LocalRitualLog types
- 6 step types: tool_call, number_input, text_input, mood_picker,
  info_display, checklist
- 3 templates: Morning routine (water + events + tasks + streaks),
  Evening routine (progress + reflection), Hydration check
- Store with createFromTemplate, CRUD, step management, completion logs
- Reactive queries for active/all rituals

UI:
- RitualRunner.svelte: step-by-step card UI with progress bar,
  tool execution, number/text input, projection data display,
  skip/next navigation
- /companion/rituals route: ritual list, template picker, play/pause

Adds rituals + ritualSteps + ritualLogs tables (v10 schema).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:01:37 +02:00
Till JS
46db527f8c feat(brain): add Companion Chat module with LLM tool calling
Phase 5 of the Companion Brain. Introduces the Companion Chat that
ties together all previous phases into a conversational interface.

Module (modules/companion/):
- types.ts: LocalConversation + LocalMessage with tool call/result fields
- collections.ts: companionConversations + companionMessages tables
- stores/chat.svelte.ts: conversation + message CRUD
- queries.ts: reactive useConversations() + useMessages()
- engine.ts: chat orchestration — builds system prompt from Context
  Document, sends to local LLM (Gemma via @mana/local-llm), handles
  tool calls via JSON extraction + executeTool(), supports multi-round
  tool calling (max 3 rounds)

UI:
- CompanionChat.svelte: message list, streaming output, tool result
  display, keyboard submit (Enter)
- /companion route: sidebar with conversation list + chat area

Also updates the architecture plan with Phase 1-4 completion status.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 21:23:38 +02:00