Commit graph

3281 commits

Author SHA1 Message Date
Till JS
a44e3df1d0 refactor(mana): remove post-signup onboarding wizard
Drop the 8-step wizard (Welcome, Profile, Context, Apps, AI-Tier, Sync,
Credits, Complete) in favor of contextual, per-module intros — todo and
news already own their first-run flows, and the workbench empty state
handles the initial surface for new users.

Removes components/onboarding/, stores/onboarding.svelte.ts, the
ONBOARDING storage key, and all trigger/render wiring in (app)/+layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:46:21 +02:00
Till JS
8d00ee0697 feat(invoices): M2 CRUD — draft lifecycle, totals, list + detail
Full draft → sent → paid flow with the supporting surface:

Data layer
- totals.ts: pure computeLineTotal / computeInvoiceTotals (testable
  without Dexie); per-line rounding then per-rate summing so the
  breakdown equals the sum-of-lines exactly
- queries.ts: useAllInvoices (live, overdue derived on read),
  useInvoiceClients, computeStats per currency, formatAmount
- stores/settings.svelte.ts: singleton with stable sentinel id,
  ensureSettings() lazy-creates, takeNextNumber() atomic via Dexie
  rw-transaction (plaintext-only fields → no crypto breaks the tx)
- stores/invoices.svelte.ts: create / update / updateLines (recomputes
  totals) / markSent / markPaid / void / duplicate / soft-delete; drafts
  only editable, paid can't be voided, sent/paid can't be deleted

Components
- StatusBadge, LinesEditor (add/remove/reorder, live per-line totals,
  minor-unit conversion on type), ClientPicker (contacts + invoice
  client book, manual entry fallback, snapshot binding),
  SenderProfileForm (sender + sequence + defaults), InvoiceForm
  (create + edit with live totals + VAT breakdown)

Views & routes
- DetailView with full action bar (edit/duplicate/mark sent/mark paid/
  void/delete), lines table, per-rate totals block
- ListView rewritten: status chips with counts, stats cards
  (open/overdue/YTD fakturiert/YTD bezahlt) scoped to primary currency,
  search, row-click to detail
- routes: /invoices/new, /invoices/[id], /invoices/[id]/edit (with
  edit-lock for non-drafts), /invoices/settings

Design notes addressed
- Number sequence atomicity: rw-transaction around plaintext fields
  guards same-device race; cross-device offline collision documented
  in settings store header as a known gap, acceptable for solo-MVP
- Edit-after-send: drafts only are editable; to revise a sent invoice,
  void and duplicate (keeps bookkeeping evidence intact)

Plan: docs/plans/invoices-module.md §M2.
Next: M3 pdf-lib renderer + M5 swissqrbill.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:40:11 +02:00
Till JS
248137ec43 chore(mobile): remove 6 of 7 mobile apps — keep only memoro
Six Expo mobile apps lagged behind their web counterparts and haven't
shipped updates. Keeping them in the repo kept CI noisy (the context/
mobile type errors were only unmasked after yesterday's postinstall
fix), and they blocked other cleanup (parallel lockfile entries, dead
scripts). Removing them since the web surface under mana.how is the
active product.

Deleted (~175 MB, ~700 files):
- apps/cards/apps/mobile
- apps/chat/apps/mobile
- apps/context/apps/mobile      (the one still failing type-check)
- apps/mana/apps/mobile
- apps/picture/apps/mobile
- apps/traces/apps/mobile

Kept: apps/memoro/apps/mobile (the only actively-developed mobile app,
tied to the audio-recording native module).

Cleanup:
- Dropped 6 `dev:*:mobile` scripts from root package.json that pointed
  at the deleted apps. Other `dev:*:mobile` entries (quotes, contacts,
  calendar, mail, moodlit, finance, figgos) already pointed at
  non-existent apps before this change — out of scope, a separate
  dead-script sweep.
- Root CLAUDE.md: updated the "per-product mobile apps exist" prose
  and the repo-layout diagram to reflect the memoro-only reality.
- apps/mana/CLAUDE.md: removed the `mobile/` entry from the apps/
  layout box, noted the deletion date, and updated the tech-stack
  table to point at the memoro mobile app as the sole Expo surface.

No CI workflow or turbo.json references touched — none existed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:31:47 +02:00
Till JS
4daca8970b feat(shared-ai): runPlannerLoop + compact system prompt for function calling
Introduces the new planner pipeline both the webapp runner and the
mana-ai tick will swap onto in the next commits. Additive for now —
the legacy buildPlannerPrompt + parsePlannerResponse stay exported so
callers can migrate one at a time; they get removed once the last
consumer is gone.

- planner/loop.ts — runPlannerLoop orchestrates a multi-turn chat
  against a caller-supplied LlmClient. Tool-calls from the LLM are
  handed to an onToolCall callback and their results fed back as
  tool-messages. Parallel tool-calls in one turn execute sequentially
  to keep the message log linear for debugging. Stops on assistant
  stop, empty tool_calls, or a hard max-rounds ceiling (default 5).
- planner/system-prompt.ts — new buildSystemPrompt. ~40-line German
  system frame, no tool listing (the SDK-level tools field carries
  the schemas now), no JSON format example, no "please return JSON"
  plea. User frame renders mission + linked inputs + last 3
  iteration summaries, same as before.
- Five test cases covering the loop: immediate stop, single tool
  call with result feedback, parallel calls execute in order, tool
  failures propagate as tool-messages the LLM can react to, and
  maxRounds ceiling fires with the right stopReason.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:31:01 +02:00
Till JS
2cf89ce26a feat(invoices): M1 skeleton — module registration + empty ListView
New outbound-finance module that issues invoices to clients. M1 scope:
- types, constants, collections with demo seed (not auto-loaded)
- module.config registered in module-registry
- Dexie v27 with invoices / invoiceClients / invoiceSettings tables
- encryption registry entries for all three tables (type-safe via entry<T>)
- app entry (requiredTier: alpha) + gradient icon (emerald→teal, QR corner)
- route /invoices mounts ListView with empty state

Money stored as integers in minor units (Rappen/cents) to avoid float
drift. Totals kept plaintext for liveQuery aggregation; lines encrypted
as a whole array so titles ride alongside. Settings is a singleton with
stable sentinel id so sync dedupes on it.

Plan: docs/plans/invoices-module.md. Next: M2 CRUD + number generator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:28:09 +02:00
Till JS
11f768b8e5 docs(invoices): ClubDesk vs. Mana comparison + invoices module plan
Competitive analysis of ClubDesk (reeweb ag, ~20'000 DACH clubs) with a
dual-use roadmap identifying features that benefit both clubs and general
users (freelancers/creators). First chosen step: invoices module with
Swiss QR-Bill as the CH-differentiator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:27:57 +02:00
Till JS
4523ab24e3 feat(shared-ai): toolToFunctionSchema — catalog → OpenAI function-spec
Single bridge between the AI_TOOL_CATALOG shape and the wire format every
provider (Gemini, OpenAI-compat, Ollama ≥ 0.3) speaks for native tool
calling. Keeps the catalog as the source of truth — the runner never
reads catalog entries directly; it asks this converter for function-spec
shapes to hand the LLM.

- No _rationale or wrapper-tool injection: the runner doesn't need it
  and the added schema noise would hurt planner quality.
- Throws on unknown parameter types so catalog typos (e.g. "array"
  instead of "string") fail loudly instead of coercing silently.
- Preserves enum constraints; drops the enum key entirely when absent
  so Gemini doesn't reject empty-enum function-declarations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:24:36 +02:00
Till JS
e757470cb0 feat(mana-llm): add OpenAI-style tools + tool_calls passthrough
Extends the chat-completions surface so callers can ask any provider
to call named functions and get structured tool_calls back. Wired
through all three provider adapters so the planner and companion can
switch off the fragile JSON-parsing pathway.

- Request: tools[], tool_choice, assistant tool_calls, tool-role
  messages with tool_call_id.
- Response: MessageResponse.tool_calls, Choice.finish_reason adds
  "tool_calls", DeltaContent streams tool_calls.
- Google provider: Tool(function_declarations=...) build, result
  normalised (args dict → JSON string), function_response parts on
  a user turn for tool-role messages.
- OpenAI-compat: 1:1 passthrough of the OpenAI spec.
- Ollama: /api/chat passthrough; model-level capability check via a
  TOOL_CAPABLE_OLLAMA_PATTERNS whitelist (llama3.1+, qwen2.5+,
  mistral, command-r, …) — unsupported models rejected rather than
  silently falling back to prose.
- Router: model_supports_tools() check upfront for both streaming
  and non-streaming paths; ProviderCapabilityError bubbles as 400.

No silent downgrade. Missing tool support = explicit error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:22:48 +02:00
Till JS
c612a22371 fix(type-check): unblock two more pre-existing failures
After yesterday's type-check cascade repair (c34175afa), the root
\`pnpm run type-check\` progressed through 5 more packages but still
stopped on two pre-existing failures:

- \`services/mana-media\` delivery route: \`c.body(transformedBuffer)\`
  passed a Node \`Buffer<ArrayBufferLike>\`, but Hono 4.7 types the
  body argument as \`Uint8Array<ArrayBuffer>\` (strict — no
  ArrayBufferLike). \`Uint8Array.from(buf)\` gives a clean copy with a
  fresh \`ArrayBuffer\` backing that the strict type accepts. Runtime
  cost for a handful of KB per image transform is negligible next to
  the Sharp pipeline that produced the buffer.
- \`packages/shared-llm\`: same rune issue as local-stt + local-llm —
  \`store.svelte.ts\` uses \`$state\` and transitively pulls in
  \`local-llm/src/svelte.svelte.ts\`. Plain tsc can't resolve Svelte 5
  runes. Same treatment: \`type-check\` script explicitly skips with a
  message pointing at svelte-check.

Root \`pnpm run type-check\` now reaches \`@context/mobile\`, which has
real code-level type errors (adapter shape mismatches, an RN event-
handler typing drift, and a deleted Supabase module still imported by
\`utils/supabaseTest.ts\`). Those need domain changes, not config
tweaks — out of scope for this repair pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:20:08 +02:00
Till JS
4b8fede7fc fix(mana-llm): surface Gemini finish_reason errors instead of returning ""
The google provider called response.text after a chat completion and
passed the resulting string downstream unchanged. When Gemini's content
filter, recitation guard, or max_tokens ceiling fired, response.text
quietly returned "" — which the planner then reported as "no JSON block
found", masking the real cause. Empirically this failed in 45 ms on a
simple Quiz mission.

Introduces providers/errors.py with a small ProviderError hierarchy
(Blocked / Truncated / Auth / RateLimit / Capability). google.py now
inspects response.candidates[0].finish_reason and raises the matching
structured error; the non-streaming path maps it to 422/502/429 via a
new except-branch in main.py, and the streaming path surfaces the kind
as the SSE error type. Capability is wired but not yet used — it lands
with the tool-schema passthrough in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:15:37 +02:00
Till JS
c34175afab fix(type-check): repair silently broken per-package type-check scripts
Yesterday's postinstall fix (\`d1d37749f\`) removed the \`|| true\`
guards, which in turn exposed that \`pnpm run type-check\` at the
root had been red for a long time but nobody noticed. Several per-
package scripts were genuinely broken:

- \`@mana/test-config\`: \`vitest.config.base.ts\` and \`.svelte.ts\`
  pass \`all: true\` to the coverage block. Vitest 4 removed that flag
  (including uncovered files is now the default), so tsc reports
  \`'all' does not exist in type 'CoverageOptions'\`. Removed both.
- \`@mana/credits\`: \`tsconfig.json\` include glob had
  \`"src/**/*.svelte"\`, which makes tsc try to parse .svelte files
  as TS source. It can't. Removed .svelte from include; added
  \`"exclude": ["src/web/**"]\` — the web consumer layer is checked by
  svelte-check in the apps that import it, not here.
- \`@mana/local-stt\` + \`@mana/local-llm\`: ship \`svelte.svelte.ts\`
  files that use Svelte 5 runes (\`$state\` etc.). Plain tsc has no
  rune support — \`$state\` is not a name it knows about. Both
  packages' \`type-check\` scripts now explicitly skip with a message
  pointing at svelte-check as the right tool. The rune code is still
  type-checked by svelte-check when a consumer app runs \`pnpm check\`.
- \`@manavoxel/shared\`: was missing its \`tsconfig.json\` entirely,
  so the \`type-check\` script ran tsc with no config, which dumped
  the CLI help and exited non-zero. Added a minimal bundler-mode
  tsconfig matching the pattern used by sibling packages.

\`pnpm run type-check\` now goes further than it has in months —
next failure is a real pre-existing Hono type mismatch in
\`services/mana-media/apps/api/src/routes/delivery.ts\` (Buffer vs
c.body signature), which is out of scope here and needs a proper
code fix, not a config fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:13:54 +02:00
Till JS
da47f534bc docs(plans): function-calling migration + removal of propose/approve gate
Plan for ripping out the fragile text-JSON parser and the propose-approve
flow in one atomic PR. Key shifts:

- LLM uses native function calling — SDK-guaranteed structure, no parser
- Tool policy becomes auto | deny (no propose, no confirm for now)
- Timeline + per-iteration revert replace the proposal inbox as the
  review surface; missions run end-to-end without human approval
- Safety via mission-budget, manual-cadence, agent-policy, revert
- No _rationale meta-param (tool name + params are self-explanatory)

Applies to webapp runner, mana-ai server runner, and companion chat —
all three share one runPlannerLoop from @mana/shared-ai after migration.
Net: ~1000 LoC deleted, ~600 added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:10:23 +02:00
Till JS
d1d37749f7 fix(install): remove silent || true from postinstall + narrow filter
The root postinstall was `node scripts/generate-env.mjs || true &&
pnpm run build:packages || true`. Two failures were being swallowed:

1. shared-auth's build has been broken for a while. shared-types
   re-exports its submodules with explicit `.ts` extensions
   (`export * from './theme.ts'`), which only works for downstream
   consumers that set `allowImportingTsExtensions: true`. shared-auth
   didn't — tsc emitted TS5097 on every re-export, the build failed,
   `|| true` hid it, every `pnpm install` appeared clean.

2. The filter `@mana/*` matches everything in the workspace, including
   `@mana/web` — the full 27-module SvelteKit build. On postinstall
   this kicked off vite, which OOM-aborted during SW generation.
   That's the original reason `|| true` was added, judging by shape.

Fixes:
- Dropped the `.ts` suffix from shared-types/src/index.ts re-exports.
  shared-types is consumed in bundler-mode tsconfigs everywhere, so no
  extension is the portable form. shared-types' own `tsc --noEmit`
  still passes.
- Narrowed the filter from `@mana/*` (name-glob, matches apps) to
  `./packages/*` (path-glob, only workspace packages). Scope drops
  from 133 → 39 projects; build:packages now runs cleanly in ~15s.
- Removed both `|| true` guards. A broken postinstall now fails
  loudly instead of producing a half-built state nobody notices.

Verified: `pnpm install` completes exit 0 in 13s; all 39 packages
build green.

Closes audit item #37 (postinstall swallows errors).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:58:59 +02:00
Till JS
5ec1dfc747 chore(db): enforce pgSchema isolation with a lint script
The "every Drizzle table uses pgSchema" rule was documented in
.claude/guidelines/database.md (added yesterday as part of Concern 5)
but enforced only by convention. A new service could slip a raw
\`pgTable()\` past review and collide in the default \`public\` schema
of \`mana_platform\`, and nothing would surface the mistake until a
production migration failed.

- \`scripts/validate-pg-schema-isolation.mjs\` scans every tracked
  TypeScript file under services/, apps/api/, packages/ for call sites
  of \`pgTable(\` (not imports — imports can still be useful for types).
  Strips comments before matching so doc-examples like "use \`pgTable()\`"
  don't trigger false positives.
- Wired as \`pnpm run validate:pg-schema\` and a new CI step in the
  validate job (right after the turbo-recursion check). 721 files
  scan clean today.
- Removed an unused \`pgTable\` import in mana-subscriptions that would
  have been the only import of the symbol remaining after this change.
- Updated .claude/guidelines/database.md — the old verification blurb
  said "no automated lint rule yet", now points at the enforcer.

Drift verified: injecting a synthetic \`pgTable('bad', {})\` into
subscriptions.ts failed with a clear file:line violation pointing at
the database guideline.

Closes the "no automated lint rule" gap noted in the database guideline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:45:59 +02:00
Till JS
1eda3f5395 chore(turbo): lint against recursive \turbo run\ calls in child packages
CLAUDE.md flagged this as "CRITICAL" — a child package.json defining
e.g. \`"build": "turbo run build"\` causes a 10+ minute CI hang with
thousands of duplicate task spawns. The rule was documented but never
enforced, so it re-emerged every couple of months as someone copied a
parent script pattern.

- \`scripts/validate-no-recursive-turbo.mjs\` walks every tracked
  package.json (via \`git ls-files\`, so node_modules is auto-skipped)
  and fails if any non-root package has build/type-check/lint/test/
  test:coverage/check scripts containing \`turbo run\`. \`dev\` stays
  allowed — delegating it from a parent is the intended ergonomic.
- Wired as \`pnpm run validate:turbo\` + a new CI step in the validate
  job (before type-check — fails fast).
- CLAUDE.md §Turborepo updated to point at the enforcer and call out
  the full task list (test/test:coverage/check were missing from the
  original prose).

Verified: 138 non-root package.json files scan clean. Drift simulation
(injecting \`"build": "turbo run build"\` into apps/mana/apps/web) fails
with a clear message pointing at the offending file + script + fix.

This closes audit item #32 from the architecture review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:39:32 +02:00
Till JS
c7af693c6d feat(crypto): Phase C — build-time registry ↔ Dexie audit
Before: adding a new Dexie table left the encryption decision implicit.
If you forgot to register it, the table silently shipped in plaintext
forever — no error, no warning, no footprint anywhere. The architecture
audit flagged this as the root of Concern 1.

- `scripts/audit-crypto-registry.mjs` parses database.ts's `.stores()`
  blocks and registry.ts's entries, then enforces three invariants:
    1. Every Dexie table is either in the encryption registry OR in the
       new `plaintext-allowlist.ts` — one conscious classification per
       table.
    2. No dead registry entries (referring to tables that no longer
       exist in Dexie).
    3. No table appears in both — single authoritative source.
- `plaintext-allowlist.ts` auto-seeded from current state. 105 entries,
  each tagged `// TODO: audit` as an invitation to review whether the
  table truly holds nothing sensitive. The allowlist is intentionally
  a separate file so additions are reviewable on their own (not buried
  inside database.ts schema bumps).
- Wired into `pnpm run check:crypto` + CI validate job — a new table
  now fails the PR check instead of slipping past review.
- `check:crypto:seed` regenerates the allowlist if ever needed.

Verified: drift simulation (removing aiMissions from the allowlist)
fails the audit with a clear message pointing at the missing
classification. Current state passes: 187 Dexie tables, 82 encrypted,
105 explicit plaintext.

Concern 1 is now fully closed (A: typed registry entries, B: dev-mode
runtime drift check, C: build-time audit enforcing coverage).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:36:32 +02:00
Till JS
a2598b9c57 feat(crypto): type-safe registry entries + dev-mode drift check
The encryption registry was a plain Record<string, EncryptionConfig>
with bare string[] fields — a typo in a field name (e.g. 'messagetext'
instead of 'messageText') silently shipped that field in plaintext
forever. No compile error, no runtime error, just quietly-leaked data.
This was flagged as the #1 silent-failure mode in the architecture
audit (Concern 1).

Two additive layers:

1. `entry<T>(fields, opts?)` helper
   - Takes the Local* row type as a type parameter
   - `fields` is `keyof T & string` — TypeScript rejects any name that
     isn't actually on the row type
   - Migrated the 6 highest-value entries as examples: messages,
     conversations, chatTemplates, notes, journalEntries, dreams,
     dreamSymbols, memos. Remaining entries keep the old object-literal
     shape and compile as before — migration is opportunistic, not a
     big-bang rewrite.

2. Dev-only runtime shape check in `encryptRecord`
   - Gated on `import.meta.env.DEV` so production builds pay zero cost
     (Vite strips the call at build time)
   - Case-insensitive near-miss detection: warns when a registered field
     isn't on the record but its lowercased form matches an existing key
     — catches typos for untyped legacy entries too
   - "no registered field present at all" warning catches wrong-tableName
     call sites
   - Throttled per (table, field) so liveQuery loops don't spam

Verification:
  svelte-check: 0 errors, 29 pre-existing warnings (unrelated)
  vitest crypto suite: 77/78 pass (1 pre-existing failure on
    meditateSettings empty-fields assertion, not touched here)

Phase C (build-time audit script enforcing every Dexie table is either
registered or explicitly allowlisted as plaintext) is the bigger win
but requires seeding the allowlist from current state — deferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:26:37 +02:00
Till JS
52d008dd34 fix(goals): start GoalTracker on boot + surface AI proposals inline
startGoalTracker was only ever called from tests, so DrinkLogged /
TaskCompleted / MealLogged events never incremented currentValue and
GoalReached never fired — the progress bars were cosmetic. Wire it into
the (app)/+layout idle boot next to startStreakTracker, with matching
teardown in onDestroy.

Also drop <AiProposalInbox module="goals"/> into the module ListView so
create_goal / pause_goal / resume_goal / complete_goal proposals are
reviewable inline (previously only visible in the mission-detail view).

Refresh the tool-coverage tables while we're at it: apps/mana/CLAUDE.md
now reflects the real catalog state (59 tools, 19 modules — was 37/12),
and services/mana-ai/CLAUDE.md shows the correct server-side propose
subset (31 tools, 16 modules). Also fixes a stale 'location_log' →
'get_current_location' typo in the places row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:24:39 +02:00
Till JS
c119fd7a62 docs(quiz): refresh tools.ts header to list all 8 tools
The header still showed the original three-tool surface after the
update/delete/stats additions landed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 19:50:52 +02:00
Till JS
7fb31e41b5 feat(ai): expand Quiz tools — edit/delete questions, edit meta, stats
Completes the Quiz CRUD surface for the AI agent. Five new tools:

- update_quiz (propose) — rename/archive/pin + description/category
- update_quiz_question (propose) — text, type+options, explanation;
  rejects a type swap without a matching optionsJson
- delete_quiz_question (propose) — symmetric to add_quiz_question
- get_quiz_questions (auto) — lets the planner see existing questions
  before appending more (avoids duplicates)
- get_quiz_stats (auto) — attemptCount / avgScore / bestScore /
  lastAttemptAt; enables adaptive missions like "analyze my weak spots
  and generate harder questions"

delete_quiz deliberately left out — too destructive to leave in the
AI's hands when the user can delete manually in two clicks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 19:50:24 +02:00
Till JS
dd756c4664 docs(database): document the pgSchema isolation rule
pgSchema was enforced in practice across every service (auth, credits,
usr, events, mail, research, subscriptions, analytics) but nowhere
documented as a rule. New services had to reverse-engineer the pattern
from existing code, and the example in the guideline itself still used
raw pgTable() — actively steering readers in the wrong direction.

- New "Schema Isolation" section: the rule, the why, the naming table
  (service → schema), the mana_sync exception, a grep-based verification
  command.
- Updated the "Table Definition Pattern" example to use pgSchema so
  readers copy the right thing.

The root CLAUDE.md already links here from the "Database (PostgreSQL)"
section — no change needed there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 19:47:37 +02:00
Till JS
a7fe828d32 refactor(auth): extract sso-origins SSOT + harden drift test
TRUSTED_ORIGINS was defined inside better-auth.config.ts, which pulls
in the whole Better Auth stack just to read a list of hostnames. Anyone
who wants to consume the list (infra tooling, compose-env generators,
monitoring) had to either duplicate it or pay the import cost.

- New `sso-origins.ts` — zero-dep module exposing
  `PRODUCTION_TRUSTED_ORIGINS` + `LOCAL_TRUSTED_ORIGINS` + the combined
  `TRUSTED_ORIGINS` list. This is now the canonical place to add a new
  top-level SSO origin.
- `better-auth.config.ts` imports + re-exports so existing consumers
  keep working without a touch.
- `sso-config.spec.ts` imports directly from `./sso-origins` (cleaner
  coupling) and now HARD-FAILS when mana-auth CORS_ORIGINS contains a
  production origin that isn't in trustedOrigins. Previously this was
  a `console.warn` only, meaning dead-drift could silently accumulate
  and then surface as a confusing runtime auth rejection.
- Root CLAUDE.md "Adding an app to SSO" updated to point at the SSOT
  and mention the new hard-fail direction.

No current drift — the mana-auth CORS_ORIGINS already match. The
hardened assertion is defensive for future changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 19:45:42 +02:00
Till JS
2bcc3954ea feat(ai): add Quiz tools (create_quiz, add_quiz_question, list_quizzes)
Quiz is now an AI-accessible module. The agent can mint empty quizzes
and append questions across all four types (single / multi / truefalse
/ text) via a single add_quiz_question tool whose optionsJson payload
shape is documented in the catalog description. list_quizzes (auto)
returns decrypted metadata so the planner can reference existing
quizzes when extending them. Enables missions like "baue ein Quiz aus
meinen Notizen zu Thema X" — planner reads via list_notes, proposes
create_quiz, then N × add_quiz_question.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 19:29:35 +02:00
Till JS
4b8defcc4a chore(ci): add v8 test coverage tracking (non-blocking baseline)
CI previously ran `pnpm run test || true` — test failures were silently
swallowed with no artifact, so we had no visibility into what was actually
passing across 1,296 test files.

- New `test:coverage` turbo pipeline task + root script; packages that opt
  in by declaring their own `test:coverage` get picked up automatically.
- Wired up three high-value Vitest targets: apps/mana/apps/web (main
  frontend, ~590 tests), shared-ui (Svelte component library), and
  shared-storage (S3 client). Each emits lcov.info + coverage-summary.json
  + browsable HTML.
- apps/mana/apps/web `"test"` was running in watch mode (just `vitest`),
  which hangs under turbo orchestration — changed to `vitest run` and
  added `test:watch` for the interactive case.
- CI uploads coverage artifacts (14-day retention) regardless of whether
  tests passed. `continue-on-error: true` replaces `|| true` so a failed
  suite shows up as a warning annotation on the PR rather than being
  invisible. Flip to a hard gate once main is green for a full week.
- Testing guideline documents the pattern + the template vitest config
  + the planned 80% threshold.
- ESLint flat-config `vitest.config.ts` ignore only matched at the root;
  widened to `**/vitest.config.{ts,js,mjs}` so nested configs don't trip
  the project-service parser.

Coverage baseline produced locally:
  shared-storage:  91.37% lines (6 files, 123 tests)
  shared-ui:        2.87% lines (mostly Svelte components, untested)
  apps/mana/web:    9/59 test files fail — pre-existing, not regression

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 19:21:14 +02:00
Till JS
1f589c474c docs(ai-tools): add how-to guide for wiring module tools to the AI agent
The flow was only documented in code comments scattered across the
catalog, executor, and runner. This guide collects the three-file
contract (catalog / executor / init.ts), the auto-vs-propose policy
matrix, and the drift-guard semantics into one place so future
sessions adding a new module's tools have a single entry point.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 19:15:57 +02:00
Till JS
76d11a84ee feat(auth): server-side tier gating via requireTier middleware
The JWT already carried a `tier` claim but nothing on the server read it
— AuthGate enforcement was client-only, so a valid JWT could hit paid
LLM/research endpoints regardless of the user's access tier.

- shared-hono authMiddleware now extracts `tier` into `c.userTier`,
  defaulting unknown/missing claims to `public` (never silently grants
  higher access).
- New `requireTier(minTier)` middleware + `hasTier`/`getTierLevel`
  helpers. Tier hierarchy (guest < public < beta < alpha < founder) is
  mirrored locally to avoid pulling the Svelte-facing shared-branding
  package into Bun services.
- Applied `requireTier('beta')` as defense-in-depth on resource-heavy
  apps/api modules (chat, context, food, guides, news-research, picture,
  plants, research, traces, who) and the MCP endpoint. Pure CRUD modules
  stay auth-only — access there is gated by ownership, not tier.
- DEV_BYPASS_AUTH now injects `userTier` (defaults to founder, override
  via DEV_USER_TIER).
- Authentication guideline documents the pattern + test suite covers
  hierarchy, passes-at-minimum, and rejection paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 17:38:06 +02:00
Till JS
4efdcfffdb feat(workbench): register Quiz as first-class app (Kreativ)
Quiz module code was complete but not wired into the app-registry, so
it never appeared in AppPagePicker. Adds an AppDescriptor with the
Phosphor Exam icon, collection/paramKey/createItem for future DnD &
linking, plus a "Neues Quiz" context-menu action. Categorised under
'creative' next to cards, skilltree and library. Edit/Play stay on
route-based navigation (same pattern as library).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 18:51:34 +02:00
Till JS
cce296a527 style(settings): rework "Daten & Sync" tab to match other sections
Sync and MyData panels now follow the GeneralSection/SecuritySection
pattern — scoped CSS with theme tokens, Phosphor icons, .rows/.row
layout, action-snippet headers. MyData splits into seven focused
SettingsPanels (Profil, Auth, Credits, Projektdaten, Aufbewahrung,
Backup, Gefahrenzone). Projektdaten renders as an edge-to-edge compact
table that pulls app icons from the workbench app-registry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 18:51:19 +02:00
Till JS
8dd3dbc9e5 docs(mana-research): step-by-step API_KEYS.md setup guide
Complete walkthrough per provider — signup URL, free-tier details,
pay-per-use pricing, env-var name, key format — plus sections on where
to paste keys (.env.secrets), BYO-keys vs server-keys, verification
curl commands and troubleshooting (including the cross-service
MANA_SERVICE_KEY mismatch encountered during live testing).

Linked from services/mana-research/CLAUDE.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 18:50:06 +02:00
Till JS
b19f5a75c1 docs(help-content): add MODULE_HELP entries for 17 missing modules
Populated help text for every real module that was showing no content
when the user clicked the PageShell ? icon:

  activity, admin, ai-health, ai-insights, ai-policy, api-keys,
  companion, complexity, credits, feedback, help, news, profile,
  rituals, settings, spiral, themes

Each entry follows the established pattern: one-line description,
3–7 concrete features, optional tips. Internal / admin-only tools
(admin, complexity, ai-health) still get help so admins see the
same ?-icon behaviour as users.

The new-* quick-action "apps" (new-task, new-note, new-dream, …) and
log-day / open-feed were intentionally skipped — they don't have a
ListView and fire a CustomEvent instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:31:30 +02:00
Till JS
cd594509b2 chore: clear svelte-check errors + document scene-scope pattern
- `app-registry/types.ts` now includes `tips` in the inline help shape,
  matching `ModuleHelp` and what `AppPage.svelte` actually renders.
  Drops 3 recurring type errors.
- `event-scout` template's `{ kind: 'daily' }` cadence now carries the
  required `atHour` / `atMinute` fields (daily 08:00). Drops the 4th
  type error — svelte-check is clean.
- `apps/mana/CLAUDE.md` gains a "Scene Scope" section documenting the
  pattern: wire `filterBySceneScopeBatch` in the query AND render
  `<ScopeEmptyState>` from the empty branch, so users always see why
  the list is empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:24:52 +02:00
Till JS
e813401dc0 style(research-lab): move help into MODULE_HELP, drop subtitle, keys-button to footer
- The PageShell's ? icon already renders in-view help from the central
  MODULE_HELP map, so the inline subtitle was duplicative. Added a
  rich help entry (description + 7 features + 4 tips) matching the
  calendar/contacts pattern.
- ListView header now just carries the mode-toggle (Suche/Extrakt/
  Agent). Clean single-row layout.
- Moved "🔑 Eigene API-Keys verwalten" to a footer section at the
  bottom of the page, separated by a border. Less busy header, and
  BYO-key management is a rare action — belongs at the end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:16:51 +02:00
Till JS
20ec81e3dd style(research-lab): drop duplicate h2 title (PageShell already renders it)
The workbench PageShell renders the app name in the page header, so the
in-view h2 "Research Lab" was duplicated. Dropped the heading and
realigned the subtitle alongside the API-Keys button and mode-toggle on
the same row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:12:27 +02:00
Till JS
0f4535c48c feat(workbench): interactive scope badge — click to clear, tooltip lists tags
The Funnel badge on a scoped scene pill is now a real button:
- Click clears the scene's scopeTagIds in one interaction instead of
  sending the user through the TagSelector in SceneHeader.
- Tooltip shows the active scope tag names ("Bereich: Deep Work,
  Urlaub — klicken zum Aufheben") so users see which filter is on
  without opening the scene header.
- Keyboard-accessible via Enter/Space; stopPropagation prevents the
  surrounding scene-pill button from also firing scene-select.

Tag names are resolved via the existing useAllTags liveQuery — no
extra Dexie round-trip per render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:05:05 +02:00
Till JS
85537cb92a fix(research): default Gemini to 2.5-flash (2.0-flash deprecated for new users)
Google deprecated `gemini-2.0-flash` for new API users — existing
accounts still work, but a freshly-billed key returns 404
"models/gemini-2.0-flash is no longer available to new users". The
working replacement is `gemini-2.5-flash` (same price tier, better
quality, groundingMetadata shape unchanged).

Verified live: the fix produced a real answer with 6 grounding
citations in 2.6s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:01:24 +02:00
Till JS
dcec0d31ee feat(workbench): scope-active badge on scene pills
Phase 3 follow-up from docs/plans/scene-scope-empty-state.md — a small
Funnel icon now sits next to the scene count whenever the scene has an
explicit `scopeTagIds` set. Users can see at a glance that a scope
filter is active even when the module lists aren't empty, instead of
only noticing the filter when a list turns up zero results.

Only marks explicit scene-level scope; the agent-derived scope is
already signalled by the bound-agent avatar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:59:53 +02:00
Till JS
f09a84ac8e feat(workbench): scope-aware empty state for scoped modules
Phase 1 of the scene-scope empty state plan (docs/plans/scene-scope-
empty-state.md). When the active scene's scope tags filter a module
down to zero results, the ListView now shows a dedicated empty state
with a one-click "Bereich zurücksetzen" button instead of the generic
"Keine Aufgaben"/"Keine Treffer" message. Previously the user couldn't
tell whether the list was empty because of missing data or because of
the scope filter.

- New `ScopeEmptyState.svelte` shared component.
- New `hasActiveSceneScope()` reactive helper on the scene-scope store.
- Wired into todo, notes, calendar, contacts ListViews — the four
  modules that currently use `filterBySceneScopeBatch`.
- 4 unit tests for the scope primitives.

Phase 2 (per-module hidden count) and Phase 3 (persistent scope badge)
remain optional follow-ups.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:52:14 +02:00
Till JS
97abd251e3 fix(events): Eventbrite provider — switch from dead API to web scraping
Eventbrite shut down their public Event Search API (/v3/events/search)
in 2023. The provider now uses the website extractor pipeline
(mana-research + LLM) to scrape Eventbrite's public search pages.
No API key needed — same pipeline as any website source.

Also adds mana-events to generate-env.mjs for automatic .env generation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 16:51:58 +02:00
Till JS
536fc89050 fix(research): Claude Opus 4.7 rejects temperature param + log executor errors
- claude-web-search.ts: only send `temperature` when caller explicitly
  sets one. Opus 4.7 deprecated the param and returns
  400 invalid_request_error "`temperature` is deprecated for this
  model." Sonnet/Haiku still accept it, so keep the opt-in path.
- execute-research.ts: log provider errors via console.warn so future
  integration failures are visible in stdout. Previously the executor
  swallowed the underlying error and only returned a generic errorCode,
  which made diagnosing vendor-specific API changes impossible.

Discovered via smoke-testing with a real Anthropic key — the direct
curl worked, but our provider 400'd because Opus 4.7 tightened the
accepted param set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:36:22 +02:00
Till JS
5d67179842 docs(workbench): plan for scene-scope empty state
Captures the UX gap — a scoped scene that filters out everything shows
the generic "Keine Treffer" with no hint that the scope is the reason
or how to clear it. Plan lays out a minimal Phase 1 (shared
ScopeEmptyState component + clearSceneScope helper, ~10 LOC per
ListView) with optional Phase 2/3 extensions. No code yet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:23:13 +02:00
Till JS
120a191bb0 test(workbench): pure-helper coverage for toScene + pickActiveId
Adds 10 unit tests for the two helpers we hardened this session:

- toScene round-trips the core presentation fields and the two
  previously-dropped extras (viewingAsAgentId, scopeTagIds). Guards
  against the silent field-loss regression fixed in a1baf1053.
- pickActiveId covers empty lists, surviving current, MRU fallback,
  skipping deleted MRU entries, corrupted-JSON MRU payload, and
  non-string entries. Locks down the fallback ladder introduced in
  4e5c3179f so scenes[0] stays a last resort.

Both helpers are now exported from the .svelte.ts store. The test
file mocks `$app/environment.browser=true` and polyfills localStorage
so it runs without jsdom (the web app doesn't bundle jsdom as a test
dep).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:19:14 +02:00
Till JS
afdbc436e4 refactor(page-carousel): name the IntersectionObserver magic numbers
Hoisted the rootMargin and threshold literals into PREMOUNT_MARGIN and
INTERSECTION_THRESHOLD constants next to MAX_MOUNTED. Same behavior —
the intent of the three tuning knobs is now visible at a glance and
easy to adjust from one spot if the lazy-mount envelope needs tweaking.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:08:27 +02:00
Till JS
4e5c3179fc feat(workbench): MRU fallback for active scene + atomic reorderScenes
- pickActiveId now consults a per-device MRU list (top 5 recent
  scenes, stored in localStorage) when the current scene disappears
  (delete, sync pull, tier filter). Previously the fallback was
  always scenes[0], which could strand the user on whatever sorted
  first after a delete rather than the scene they were just on.
- reorderScenes runs all per-scene order patches inside one Dexie
  rw-transaction. A partial failure previously left the scene list
  with gapped or duplicated `order` values visible to subscribers;
  the transaction makes the reorder all-or-nothing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:02:23 +02:00
Till JS
0a928c1a56 fix(workbench): replace brittle setTimeout with tick() in scene rename
handleRequestRename focused the scene-header h1 after a hard-coded
120 ms setTimeout. On slower hardware the query fired before the
SceneHeader had re-rendered with the new active scene, focusing the
previous scene's h1. Replacing the timeout with `await tick()` flushes
Svelte's pending DOM updates before the query and removes the magic
number.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 15:50:57 +02:00
Till JS
ed801cf725 feat(events): Phase 4 — provider adapters for Eventbrite + Meetup
- Add EventProvider interface (base.ts) with fetchEvents(url, name, ctx, config)
- Refactor iCal parser and website extractor as provider adapters
- Add Eventbrite provider: API v3 search by location, category mapping,
  price info extraction. Requires EVENTBRITE_API_KEY env var.
- Add Meetup provider: GraphQL API search by location, topic→category
  mapping, HTML stripping. Requires MEETUP_API_KEY env var.
- Provider registry (getProvider, PROVIDER_TYPES) replaces hardcoded
  switch in crawl-scheduler
- Crawl scheduler now joins sources with regions for ProviderContext
  (lat/lon/radius/label) — platform providers need this for geo-search
- Source creation accepts 'eventbrite' and 'meetup' types (url optional)
- Both providers gracefully return empty when API keys unconfigured

116 tests (all passing), no regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 15:43:40 +02:00
Till JS
4d82381737 perf(workbench): LRU-evict PageCarousel's mounted-cards cache
Previously the intersection-observer cache grew monotonically: once a
card mounted its ListView + Dexie liveQuery, it stayed mounted for the
lifetime of the workbench page. A user who scrolled through 20 apps
kept 20 parallel liveQueries alive.

Now the cache is capped at MAX_MOUNTED=8 with insertion-order LRU
semantics: re-intersecting a mounted card bumps it to MRU, and the
oldest gets evicted when a new mount pushes the set over cap. Set
insertion-order is used for the LRU list so the template's has()
check stays O(1).

The cap is well above typical working-set sizes (3–6 apps) so regular
workbench use never hits the eviction path. Users with large scenes
pay at most one extra liveQuery + chunk re-request when scrolling back
to an evicted card.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 15:38:08 +02:00
Till JS
2c0d866287 feat(events): Phase 3 — AI tools, Event-Scout template, feedback loop
- Add discover_events (auto) and suggest_event (propose) to shared-ai
  tool catalog. discover_events reads the discovery feed, suggest_event
  creates a proposal to save a discovered event to the user's calendar.
- Add Event-Scout agent template with daily "Events der Woche" mission.
  Policy: discover_events=auto, suggest_event=propose, all else denied.
- Add frontend tool implementations in events/tools.ts — discover_events
  calls the feed API, suggest_event delegates to discoveryStore.saveEvent.
- Add feedback.ts — computes implicit user profile from save/dismiss
  history (category affinity + source quality as 0–2x weight multipliers).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 15:37:28 +02:00
Till JS
2f226a93aa feat(workbench): user-visible error toasts + stale-SW safety net
- Workbench CRUD handlers now emit a localized toast on failure instead
  of only logging to the console. Quota, structured-clone or Dexie
  transaction failures are now user-visible, so an add/remove/resize
  that silently rejected can no longer leave the user guessing at a
  frozen UI.
- Added a dev-only onMount that checks for stale Service Workers on
  the homepage. vite-plugin-pwa is disabled in dev (see vite.config.ts
  `devEnabled: false`), but a surviving SW from a previous `pnpm build
  && pnpm preview` session keeps serving cached HTML — e.g. showing
  `/email-verified` at `/`. We detect, warn via toast, and unregister
  automatically. Prod builds are unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 15:34:08 +02:00
Till JS
b5d55fdb21 feat(events): add Event Discovery — Phase 1 + 2
Phase 1: Manual iCal feeds + Discovery tab
- 5 new DB tables in event_discovery schema (regions, interests,
  sources, discovered_events, user_actions)
- iCal parser (node-ical) with deduplication (SHA-256 hash)
- Crawl scheduler (15-min interval, auto-deactivate after 5 errors)
- CRUD routes for regions, interests, sources + paginated feed endpoint
- Frontend: "Meine Events" / "Entdecken" tab navigation in ListView
- Discovery setup wizard (regions via mana-geocoding + interests)
- DiscoveredEventCard with save/dismiss, SourceManager for iCal feeds
- "Merken" creates a local socialEvent from discovered event

Phase 2: Auto source discovery + LLM extraction + relevance scoring
- Source discoverer: web search via mana-research to auto-find iCal
  feeds and venue websites for a region
- Website extractor: crawl via mana-research /extract, then LLM-based
  event extraction via mana-llm with structured JSON output
- Flexible date parsing (ISO, DD.MM.YYYY), markdown fence stripping
- Relevance scorer: category match, freetext match, haversine distance,
  time proximity, weekend bonus (0-100 clamped)
- Routes: POST regions/:id/discover-sources, PUT/DELETE sources/:id/activate|reject
- Frontend: "Automatisch finden" button, suggested vs active sources UI

107 tests (all passing), no regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 15:30:46 +02:00
Till JS
677123091a refactor(settings): unify section styling, remove Credits tab
- AiSettings: Tailwind → scoped CSS with theme tokens; tier cards as
  buttons with subtle primary-tinted active state; Tier 0 in matching
  card; pill toggles for behavior settings; improved body text
  readability
- VaultSection: theme-token colors, SettingsSectionHeader with status
  badge action, unified button system, flat subsection structure with
  border dividers, collapsible encrypted-fields list (5 by default),
  threat-model disclosure moved above destructive actions, wizard step
  numbering; dropped stale dark-mode @media block
- Settings ListView: overflow-x: hidden so chip-row scroll stays scoped;
  hashchange + workbench:navigate-anchor effect for live deep-links
- Remove Credits tab (stub redirecting to /?app=credits) — full Credits
  module + dashboard widget already cover balance/purchases/transactions
2026-04-17 15:46:51 +02:00