Commit graph

3014 commits

Author SHA1 Message Date
Till JS
d7bf8a2fd4 feat(ai): Mission datamodel — long-lived autonomous work items
Foundational entity for the AI Workbench Runner. A Mission carries the
user's standing instruction (concept + objective), references to the
modules it should draw context from, a cadence, and an append-only
history of iterations. Each iteration records the plan the AI generated
for that run plus the resulting proposal statuses and user feedback.

- `data/ai/missions/types.ts` — Mission, PlanStep, MissionIteration,
  MissionCadence union (manual / interval / daily / weekly / cron)
- `data/ai/missions/cadence.ts` — pure `nextRunForCadence(cadence, from)`
  used by the store on create / update / finishIteration
- `data/ai/missions/store.ts` — CRUD + lifecycle
  (pause / resume / complete / archive / delete) + iteration helpers
  (start / finish / addFeedback)
- `data/ai/module.config.ts` — new `ai` sync app; Missions sync
  cross-device (unlike the local-only `pendingProposals`)
- `db.version(18)` adds the `aiMissions` Dexie store with indexes on
  state, createdAt, nextRunAt, and [state+nextRunAt] for the Runner's
  "due now" query

Iterations live inline on the Mission record — append-only, small N,
always loaded together by the Planner. No separate child table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 21:10:19 +02:00
Till JS
0f3fd4eebd docs(ai): document Actor attribution + AI Workbench pilot
- COMPANION_BRAIN_ARCHITECTURE §20: Actor model, policy layer,
  pendingProposals lifecycle, ghost-UI pilot, roadmap, open follow-ups,
  manual test snippet
- DATA_LAYER_AUDIT §9: new Actor columns on records
  (`__lastActor`, `__fieldActors`), `pendingProposals` table, write-path
  diagrams for user / AI / approval, open mana-sync Go + Postgres work
- apps/mana/CLAUDE.md: short AI Workbench section with pointers + Dexie
  hook now lists actor stamping

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 21:05:30 +02:00
Till JS
851a281e5a refactor: rename zitare -> quotes (Zitate)
Zitare was opaque Latin/Italian-flavored branding. Renamed to clear
English "quotes" (DE: Zitate) matching short-concrete-noun cluster.

- Module, routes, API, i18n, standalone landing app, plans dirs
- Dexie tables: quotesFavorites, quotesLists, quotesListTags,
  customQuotes (dropped redundant "quotes" prefix on the last)
- Logo QuotesLogo, theme quotes.css, search provider, dashboard
  widget QuoteWidget
- German user-facing label "Zitate" (English brand stays Quotes)

Pre-launch, no data migration needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:59:16 +02:00
Till JS
7a1f11c971 feat(ai): inline proposal inbox in the todo module
First pilot of the AI Workbench ghost-state pattern. A reusable
`<AiProposalInbox module="todo" />` component renders pending proposals
for a given module as dashed-outline ghost cards above the real content —
zero UI when the AI is idle, approve / reject inline when it's not.

- `data/ai/proposals/queries.ts` — reactive `useAiProposals` live query
  with module / status / missionId filters. Module filter resolves via
  the tool registry so each proposal auto-routes to the right page.
- `components/ai/AiProposalInbox.svelte` — the drop-in inbox component.
  Shows tool description + params + AI rationale; approve runs the
  original intent under the AI actor context (preserving attribution),
  reject stores the row with status=rejected for the next planner pass.
- Wired into /todo for the pilot. Other modules opt in by adding one
  line once their tools land in DEFAULT_AI_POLICY.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:58:46 +02:00
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
bb278fb3cf fix(types): use .js extensions for shared-types re-exports
Commit 93bb94a12 dropped the extensions on shared-types re-exports
to make the web app's svelte-check pass (its tsconfig has no
allowImportingTsExtensions). That satisfied tsc but broke SSR: the
dev server tripped with ERR_MODULE_NOT_FOUND on every (app) route
because Node's native ESM loader (used by downstream tooling like
@tailwindcss/node) cannot resolve bare relative specifiers without
an extension, and only Vite-owned paths got the bundler-style
resolution the fix relied on.

Switch to the TypeScript-ESM idiomatic `.js` extension. tsc with
moduleResolution: "bundler" still type-checks against the actual
.ts source, and at runtime both Vite and Node resolve `.js` the
same way — no tsconfig flag flip required.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:44:18 +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
f36da0526a docs: add Context Intelligence research report
Survey of cross-domain AI context systems (Gemini Personal Intelligence,
Apple Intelligence, Qira) and architectural options for a Mana-wide
reasoning layer over the 40+ modules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:50:25 +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
f0233b8794 perf(shared-pkgs): declare sideEffects for aggressive tree-shaking
Following the shared-icons fix (d5cabed14), audit every workspace
package's src/index.ts for top-level side effects and flag the
ones that are safe to tree-shake:

- Pure TS re-export barrels (types, theme, utils, llm, storage):
  "sideEffects": false — lets Vite prune entire submodules when a
  consumer only imports a subset of named exports. Matters most for
  shared-llm where the orchestrator/BYOK branch isn't needed on
  every route.

- Packages that ship .svelte components (branding, ui, links):
  "sideEffects": ["**/*.svelte", "**/*.css"] — same tree-shaking
  benefit for TS modules, but keeps Svelte component CSS injection
  intact.

The state-holding submodules (shared-ui drag-state/toast,
shared-llm store, shared-links mutations) are still evaluated
whenever their exports are referenced, so behaviour is unchanged —
the flag only lets the bundler skip modules that aren't in the
dependency graph at all.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 18:12:22 +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
cf3d93fac1 test(sync): extract WriteBackup + 4 Go integration tests
Refactor: HTTP handler becomes a thin shim over a pure WriteBackup(w,
userID, createdAt, iter) function. RowIterator abstracts the store, so
tests feed synthetic ChangeRow slices and production feeds
StreamAllUserChanges. Zero behavior change in production — same bytes
on the wire.

Tests (all pass):

- TestWriteBackup_Roundtrip: three rows across two apps, assert zip has
  2 entries, events.jsonl has 3 JSON lines in order, insert omits
  fieldTimestamps, update surfaces them, manifest apps are sorted,
  eventsSha256 equals a recomputed sha of the decompressed body.
- TestWriteBackup_EmptyUser: empty userID refused up-front.
- TestWriteBackup_NoRows: zero-row export still produces a valid zip
  with an empty events.jsonl and a manifest with eventCount=0 and a
  non-empty sha (sha of empty input).
- TestWriteBackup_DefaultsSchemaVersionZeroRowsToOne: legacy rows with
  schema_version=0 clamp to 1 so the manifest never claims a protocol
  version that never existed.

Paired with the vitest zip parser suite on the TS side, this closes
the Go-writes / JS-reads round-trip without needing live mana-sync.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 17:44:37 +02:00
Till JS
d5cabed14d perf(shared-icons): mark package as side-effect-free for tree-shaking
phosphor-svelte ships a 28 MB lib of per-icon Svelte components and
does not declare "sideEffects" in its own package.json. When
@mana/shared-icons re-exports that package without its own
"sideEffects" hint, Vite/Rollup conservatively assume every
transitive module evaluation might matter and cannot aggressively
prune unused icons across chunk boundaries.

Our re-exports (index.ts: `export * from 'phosphor-svelte'` + a small
name→component registry) are pure ESM barrels with no top-level
runtime code, so flagging the package as side-effect-free is safe
and lets the bundler drop unused icons and skip evaluating the
icon-registry module from chunks that only want a named icon.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 17:16:19 +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
aa29ad860f chore: refresh pnpm-lock after nutriphi -> food rename
Workspace package names changed (apps/nutriphi -> apps/food,
@nutriphi/shared -> @food/shared, @nutriphi/landing -> @food/landing).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:51:51 +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
180e07d59e feat(credits): admin-gifted sync subscriptions
Admins can now grant Cloud Sync to users without charging credits. Gifted
rows carry is_gifted=true plus gifted_by/gifted_at audit columns; the
billing cron skips them, and /activate and /deactivate refuse to touch
them. New endpoints POST/DELETE /api/v1/admin/sync/:userId/gift.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 14:11:07 +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