The last open item from the plan. Missions can now draft invoices from
chat context, mark customer payments, and read status for autonomous
follow-up cadences.
Tool catalog (packages/shared-ai/src/tools/schemas.ts)
- create_invoice (propose) — clientName + lines[] + currency + due
- mark_invoice_paid (propose) — by id, optional back-dated paidAt
- list_invoices (auto) — with status + limit filter
- get_invoice_stats (auto) — open/overdue/YTD per currency
Had to widen the tool-parameter type vocabulary so create_invoice can
declare lines as a typed array. Touched three places:
- ToolSchema-side: the catalog's `type` string is already free-form so
'array' / 'object' just pass through
- ModuleTool-side (apps/mana/apps/web/src/lib/data/tools/types.ts): added
'array' | 'object' to the union so TS doesn't narrow the executor's
param signatures
- function-schema translator (packages/shared-ai): mapParamType +
JsonSchemaProperty both gained the two new types; the catalog-typo
guard test now uses 'fruit' as its sentinel (array no longer unknown)
Executor (apps/mana/apps/web/src/lib/modules/invoices/tools.ts)
- coerceLines accepts either a real array or a JSON-stringified array
(planners vary), skips malformed entries, converts major→minor units
- create_invoice pulls the generated number back from Dexie so the
success message shows "Entwurf 2026-0042 …" — the user recognises it
- mark_invoice_paid normalises YYYY-MM-DD → ISO so the store's timestamp
invariant (ISO throughout) stays intact
- list_invoices derives overdue on read (consistent with useAllInvoices),
returns major-unit amounts so the LLM reasons in user-facing numbers
- get_invoice_stats returns counts + open/overdue/YTD per currency
Registration: invoicesTools added to tools/init.ts. mana-ai drift guard
is happy (41/41 green); webapp + shared-ai type-check 0 errors; full
invoice test suite 59/59 green.
Closes: docs/plans/invoices-module.md §M8. All plan milestones now DONE.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Carries per-round token counts from the mana-llm response body
(prompt_tokens + completion_tokens) back through LlmCompletionResponse
→ PlannerLoopResult. The loop sums across rounds and exposes a single
aggregate on result.usage.
Lets mana-ai's tick re-activate per-agent daily-token budget tracking
— tokensUsed was stubbed to 0 in the migration commit (6) because the
loop didn't surface usage yet. Now recordTokenUsage + agentTokenUsage24h
get real numbers again, and the mana_ai_tokens_used_total Prometheus
counter is accurate.
Additive only: consumers without usage needs ignore the new field,
and providers that don't return usage produce zeros (not undefined —
the loop still exposes the object so downstream branches stay trivial).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First user-visible surface for the Spaces foundation. Two components:
SpaceSwitcher (header dropdown)
- Shows the active space name + type badge
- Opens a dropdown listing all user's spaces with per-type color chips
(brand / club / family / team / practice / personal)
- Click on a space → /organization/set-active + full page reload so
every liveQuery re-evaluates against the new active space
- "+ Neuer Space" entry at the bottom opens the Create dialog
SpaceCreateDialog (modal)
- Type picker with description per type (excluding personal — that one
is auto-created at signup and never chosen manually)
- Name input + live slug preview (same slugifier as the server)
- Conditional fields: voiceDoc for brand/club, uid + legalEntity for
brand/club/practice
- POSTs to /api/auth/organization/create with metadata.type, then
/set-active and reload. beforeCreateOrganization hook rejects
malformed metadata server-side.
Placement: compact bar at the top of the (app) max-w-7xl wrapper, only
rendered when authenticated. Zero changes to PillNavigation so the rest
of the nav surface stays untouched.
Reactivity note: the switcher full-reloads on set-active because the
scoped-db wrapper doesn't yet invalidate liveQueries on active-space
change. A reactive-invalidation path can replace the reload once the
wrapper is used across enough modules to make the UX friction matter.
Plan: docs/plans/spaces-foundation.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Calls loadActiveSpace() + reconcileSentinels() in the Phase-B critical
boot block, right after the user identity is bound to the ambient actor
and before sync starts. This means:
- Pending-change rows pushed to mana-sync carry the real organization
id, not the `_personal:<userId>` sentinel the v28 migration uses
as a placeholder.
- Sentinel records (written pre-boot or by the v28 upgrade on an
existing db) get rewritten to the real personal-space id in a single
pass once Better Auth responds.
- The scope wrapper in module queries now partitions by the active
space instead of degrading to sentinel-only filtering.
Failure is non-fatal — an offline boot or a Better Auth hiccup just
means the sentinel path stays live and the next boot retries. A count
log surfaces the reconciliation count so migrations are visible in
devtools.
Plan: docs/plans/spaces-foundation.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completes the Settings polish item left open after M2.
pdf/logo.ts
- loadLogo(mediaId): fetches the large variant from mana-media, sniffs
content-type to pick 'png' vs 'jpg', returns null on any failure so
the PDF still renders without a logo
- uploadLogo(file): multipart POST to /api/v1/media/upload with
app=invoices, returns the new mediaId (or throws a user-facing msg)
- logoPreviewUrl(mediaId): thin helper so the settings form doesn't
have to know the media-URL lookup pattern
Renderer wiring
- loadLogo runs in the same Promise.all as font embedding so it doesn't
add a serial wait
- embedPng / embedJpg based on the sniffed kind; errors degrade silently
- renderHeader takes a PDFImage|null and, when present, draws it top-
left above the sender name, max 25mm × 45% content-width, aspect
preserved, 3mm breathing room below
Settings UI (SenderProfileForm)
- Logo slot at the top of the Absender section: preview when set,
"Ersetzen" / "Entfernen" actions; "+ Logo hochladen" drop-style
button when empty
- Upload persists immediately (no separate "Speichern" click for logo
changes) — keeps the interaction one-handed
- Accepts PNG / JPEG; invalid types rejected client-side before the
network round-trip
Closes one of the open items from docs/plans/invoices-module.md §M3.
Next open: M8 AI-tools (create_invoice / mark_paid / list / stats).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The runPlannerLoop test file and the webapp's mission-runner test each
had their own inline scripted LLM mock — same interface, diverged
slightly. Consolidates into packages/shared-ai/src/planner/mock-llm.ts
and re-exports from the package root so any consumer can drive the
loop deterministically.
Both existing test files now use the shared client. 5 + 3 tests pass,
44 total in shared-ai still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Server-side:
- sync_changes gains a nullable space_id TEXT column + partial index
on (user_id, space_id, app_id, created_at) WHERE space_id IS NOT NULL.
- RecordChange takes spaceID as a first-class parameter; *string so
empty strings land as real SQL NULL and the partial index skips them.
- ChangeRow + all three SELECTs (GetChangesSince, GetAllChangesSince,
StreamAllUserChanges) propagate space_id through to clients.
- changeFromRow surfaces SpaceID on the wire Change shape.
- New extractSpaceID helper reads the incoming payload — prefers top-
level spaceId, falls back to data.spaceId (inserts) or
fields.spaceId.value (updates). Tolerates pre-v28 clients.
- 6 Go tests cover the helper + round-trip.
Client-side:
- PendingChange gains an optional spaceId.
- Dexie creating hook stamps spaceId from the active record onto the
pending-change row (already set by the v28 scope hook).
- Dexie updating hook reads spaceId from the pre-update record and
stamps it on the pending-change so updates carry space context even
though spaceId itself is immutable and never in `fields`.
- buildChangeset forwards spaceId to the server.
Explicitly NOT in scope this pass:
- RLS remains user_id-scoped; multi-member shared-space reads need a
second policy that joins against auth.members. Follow-up once shared
spaces are actually used — today everything is personal.
- Subscription fan-out is still per-user; fan-out to all members of a
shared space is part of the same follow-up.
Go tests: 6/6 pass. Web type-check clean (0 errors across 7139 files).
Plan: docs/plans/spaces-foundation.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The companion chat had its own ad-hoc 3-round tool-calling pipeline:
build a system prompt with tool descriptions, ask the LLM to emit
```tool JSON blocks, regex-extract, execute, feed back the result as
a synthetic user message. Same fragility class as the old text-JSON
planner — and now unnecessary since mana-llm speaks native function
calling.
Migrates companion/engine.ts to the shared runPlannerLoop, same as
the mission runner (commit 5a) and the server tick (commit 6). Tools
go to the LLM as proper function-schemas; tool_calls come back
structured; the executor runs them directly under USER_ACTOR.
Extends shared-ai/planner/loop.ts with an optional priorMessages[]
input field so the chat can preserve multi-turn history between
turns (missions don't need this and leave it empty).
Deletes the old llm-tasks/companion-chat.ts LlmTask wrapper. Nothing
else imported it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First module to consume the scope layer — proves the model end-to-end
on a real query path.
Changes in calendar/queries.ts:
- db.table('calendars') → scopedForModule<LocalCalendar>('calendar', 'calendars')
- db.table('timeBlocks') → scopedForModule<LocalTimeBlock>('calendar', 'timeBlocks')
- db.table('events') → scopedForModule<LocalEvent>('calendar', 'events')
- applyVisibility() wrapper runs on each read to drop private records
authored by other members of a shared space.
Scope wrapper tweaks:
- getInScopeSpaceIds is now lenient during boot: if no active space has
loaded yet, falls back to the user's personal sentinel so sentinel-
stamped records from the v28 migration still render. Returns [] only
when fully unauthenticated, which yields an empty-match filter.
- applyVisibility is no longer generic-constrained — T is inferred
exactly as the input type; visibility/authorId are read via runtime
duck-typing so arbitrary record shapes pass through cleanly.
Known follow-ups:
- Root-layout bootstrap (load active space + reconcile sentinels on
login) is intentionally not wired up yet — needs a separate pass on
the already-crowded (app) layout to avoid collateral damage.
- Four legacy tables (conversations, documents, spaceMembers,
memoSpaces) carry a pre-existing `spaceId` field that points to the
older context-space concept, not our multi-tenancy space. Renaming
those to contextSpaceId is a tracked follow-up in the RFC — calendar
is unaffected.
Plan: docs/plans/spaces-foundation.md (updated with the legacy-spaceId
note + lenient-scope rationale).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the missing bits that turn M1–M6 into a coherent shippable
product rather than a pile of commits.
Dashboard widget (M7)
- InvoicesOpenWidget.svelte: open + overdue totals in the primary
currency, top-3 oldest overdue with "X Tage überfällig" under each,
empty-state CTA for first-time users
- Registered as `invoices-open` in WIDGET_REGISTRY and the component
map. Default size medium, no requiredBackend (local-first, no API)
- Fixed pre-existing test gap: validBackends list was missing 'body'
(body-stats widget has been failing silently) — added so the check
protects against drift for real
Tests (45 total, all green)
- totals.test.ts (9): computeLineTotal with discount+vat, grouping
invariant (breakdown sums == invoice totals), rounding edges
- pdf/qr-bill.test.ts (17): generateSCORReference stability +
spec-validity via swissqrbill's own isSCORReferenceValid, buildQRBillData
eligibility gates (currency, IBAN, address, amount), CH + DE address
parser paths, referenceNumber-preferred-over-regen invariant
- mail-template.test.ts (12): subject/body composition (with/without
subject, CHF vs EUR QR-hint, empty recipient fallback), mailto
spaces-as-%20 patch, looksLikeEmail edge cases
Plan (docs/plans/invoices-module.md)
- Updated with commit SHAs per milestone, testing status, and the
explicit list of open items (Logo-Upload, AI-Tools, sync collision,
structured addresses, finance cross-link, camt bankabgleich) so the
next coder knows exactly what's parked where
Unresolved: browser smoke test couldn't run — SSR is broken for all
module routes in the current tree (pre-existing, likely from the
parallel Spaces refactor; /library, /todo, /contacts all return 500
the same way). Unit tests + clean bundle build (M4) + type-check are
the coverage we have.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Runner no longer creates proposals (commit 5a) and no module renders
the inbox (commit 5b), so the supporting code is dead. This commit
deletes it.
Removed:
- data/ai/proposals/ (types, store, queries + tests) — the entire
Proposal model + createProposal/listProposals/approveProposal API.
- components/ai/AiProposalInbox.svelte — orphaned after commit 5b.
- data/ai/missions/server-iteration-staging.ts + its test — the bridge
that turned server-produced iterations into local proposals. Server
iterations will land with executed steps directly once commit 6
migrates the server runner.
- data/ai/missions/planner/ — all webapp re-exports of the old
buildPlannerPrompt / parsePlannerResponse / AiPlanInput types. The
new runner imports its types directly from @mana/shared-ai.
- llm-tasks/ai-plan.ts — the old LlmTask that wrapped the text-JSON
request/parse cycle for the LlmOrchestrator. Replaced by the direct
mana-llm client in missions/llm-client.ts.
Updated:
- data/database.ts — v29 drops the `pendingProposals` table (passing
null to .stores() deletes it on next open). Safe because nothing is
live.
- routes/(app)/+layout.svelte — no more startServerIterationStaging /
stopServerIterationStaging in the bootstrap/teardown pair.
- data/ai/missions/types.ts — strips the planStepStatusFromProposal
bridge helper (proposals don't exist any more).
- data/ai/missions/input-resolvers.ts — imports ResolvedInput from
@mana/shared-ai directly.
- data/tools/executor.test.ts — the proposal-staging test block is
rewritten to match the new semantics: auto and propose both execute
inline, only deny refuses.
Net: ~1100 LoC removed, 0 added. Type-check green, 15 tests pass
across executor + runner.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the primary workflow: the user can now send an invoice from Mana
in three clicks.
mana-mail's JSON send API can't carry attachments yet, so we hand off to
the user's default mail client via mailto: and download the PDF
separately. The user attaches the PDF manually; a two-step modal keeps
the draft → sent transition honest.
mail-template.ts
- buildInvoiceMailDraft(invoice, settings): German template with
recipient name, amount, due date, QR-bill hint for CHF, sender sign-
off from settings.senderName
- mailDraftToMailto(): URLSearchParams + patch `+` → %20 so macOS Mail /
Outlook / Thunderbird / Apple Mail iOS all preserve spaces correctly
- looksLikeEmail(): permissive inline validator for the recipient field
SendModal.svelte
- Compose step: editable to / subject / body, warning if recipient isn't
a well-formed email (non-blocking — user can fix in their mail client)
- "Öffnen & herunterladen" triggers the PDF download, then navigates
window.location.href to the mailto: URL (window.open gets blocked by
popup blockers; location navigations survive)
- Handoff step: two ✓ rows + explicit instruction to attach the PDF,
"Rechnung wurde versendet" button that calls markSent()
- Backdrop click + Escape both close; role="dialog" on the modal itself
with tabindex so screen readers land correctly
DetailView wires "Per Mail versenden" as the new primary action for
drafts, keeping "Als versendet markieren" as a secondary path for users
who send outside Mana (post, fax, in-person).
Plan: docs/plans/invoices-module.md §M6.
Next: M3 logo-upload / M7 dashboard widget / M8 AI-tools — solo-MVP is
now usable end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the client-side scope primitives that sit between module code and
Dexie so every query is filtered by the user's active Space:
lib/data/scope/
├── active-space.svelte.ts reactive active-space state; loads via
│ Better Auth's organization/get-active-member
│ and auto-activates personal on first boot
├── bootstrap.ts reconcileSentinels() — rewrites every
│ `_personal:<userId>` placeholder from the
│ v28 migration to the real space id once
│ Better Auth responds
├── scoped-db.ts scopedTable / scopedForModule — filter-
│ based scope enforcement. assertModuleAllowed
│ blocks disallowed modules per space-type
│ (e.g. mood in a brand space)
├── visibility.ts applyVisibility / isVisibleToCurrentUser —
│ hides private records not authored by the
│ current user, even inside a shared space
└── index.ts barrel export for consumers
Wrap accepts sentinel spaceId alongside the real id during the bootstrap
window so records written between v28 landing and the first reconcile
don't vanish from the UI.
No module uses this yet — the calendar pilot migration in the next
commit is the first consumer and validates the whole model.
10/10 unit tests pass. The fetch- and Dexie-backed functions
(loadActiveSpace, reconcileSentinels, scopedTable) are integration-only
and covered as the pilot migration lands.
Plan: docs/plans/spaces-foundation.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All 9 module pages that rendered the proposal inbox lose that block.
Since the runner now executes tool calls directly (commit 5a), no
proposals are ever staged — the inbox would just render an empty list
forever.
Removed from: /todo, /calendar, /places, /drink, /food, /news, /notes
module routes plus the goals and ai-missions ListViews. The mission
detail view no longer embeds a "Vorschläge zur Review" section; the
iteration cards with their executed tool_calls are the record now.
The AiProposalInbox component itself survives this commit so the
proposals store and staging code that still imports it keep compiling.
Next commit deletes the whole proposal infrastructure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The runner now drives runPlannerLoop from @mana/shared-ai: the LLM
emits native tool_calls via mana-llm's tools passthrough, we execute
each call immediately under the AI actor, and feed the result back as
a tool-message for the next turn. The reasoning loop still runs up to
5 rounds (same budget as before) but needs no hand-rolled re-prompting
because the SDK-level tool-message exchange does that for us.
Tool execution is direct — no Proposal staging. The executor's propose
branch collapses into auto (proposal store calls stay in place for
legacy consumers this commit doesn't touch; those go next). Agent-
level deny still refuses and surfaces the refusal as a tool-message
the LLM can react to.
New surface:
- missions/llm-client.ts — mana-llm HTTP adapter conforming to shared-
ai's LlmClient. Posts /v1/chat/completions with tools + tool_choice,
converts OpenAI-shape tool_calls back to our ToolCallRequest shape.
- runner.ts shrinks from ~770 to ~410 lines — pre-step research,
guardrails, agent scope, timeout, cancel, debug capture all kept.
- debug.ts stores rawMessages[] (shared-ai ChatMessage) instead of
plannerCalls[]/loopSteps. AiDebugBlock renders the chat transcript.
- available-tools.ts returns ToolSchema[] directly so the runner can
hand the array to runPlannerLoop unchanged.
- setup.ts wires createManaLlmClient() instead of aiPlanTask +
llmOrchestrator. The old aiPlanTask + planner/ re-export files
remain orphaned for the next commit to delete.
Test shape: MockLlmClient scriptable via enqueue-style turns. Three
cases cover happy path, empty-plan stop, and tool-failure propagation.
Dead-but-still-compiling afterwards: the proposals folder, the
AiProposalInbox component + its 9 call-sites, server-iteration-
staging.ts, ai-plan.ts, the legacy planner/ wrappers, and the old
buildPlannerPrompt/parsePlannerResponse exports in shared-ai. These
go in commits 5b/5c/5d.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds swissqrbill integration so CHF/EUR invoices get the Zahlteil (payment
part) rendered in the bottom 105mm of the last page.
Integration path (pdf/qr-bill.ts)
- swissqrbill/pdf targets PDFKit, not pdf-lib; so we use swissqrbill/svg,
rasterise the SVG to PNG in a browser canvas at ~300 DPI target, then
embed the PNG via pdf-lib's embedPng
- Eligibility gate via QRBillError: validates currency (CHF/EUR), IBAN
(swissqrbill's isIBANValid), parseable sender address, positive amount
- Address parser: heuristic for two-line Swiss/DE addresses
(street + number on line 1, "{zip} {city}" on line 2). Fails loud —
the renderer silently omits the Zahlteil and the UI surfaces a warning
- SCOR reference (ISO 11649) generated from invoice.number as payload,
truncated to 21 chars, checksum via swissqrbill/utils. Persisted on
invoice.referenceNumber at create time so it stays stable across edits
and re-renders
Renderer wiring
- renderInvoicePdf(..., { includeQRBill?: boolean }) — defaults true
- QRBillError is caught and absorbed; other errors propagate
- qrBillStatus(invoice, settings) — cheap pure check, returns
{ ok: true } or { ok: false, message, reason } for UI hints
DetailView
- Warning banner above PDF preview when QR-Bill is not eligible, with
a "Einstellungen öffnen →" deep link
- Preview iframe now shows the PNG-embedded Zahlteil on CHF/EUR
invoices
Addressed §"Offene Fragen" from the plan
- QR-Bill-Scope: CHF + EUR per swissqrbill spec, not USD
- Address parsing: heuristic now, structured fields to be added in M7
(tracked in renderer warning path — user sees exactly what's missing)
Plan: docs/plans/invoices-module.md §M5.
Next: M6 send flow (open mail compose with PDF attached).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Narrow pages (e.g. AI Workbench at 320px) wrapped the title onto two
lines because .header-left lacked min-width: 0 and .page-title had no
truncation rules. Add flex shrink + nowrap + text-overflow: ellipsis.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds client-side PDF generation via pdf-lib (Helvetica standard fonts,
~7KB output, no font bytes shipped).
Renderer (pdf/renderer.ts)
- renderInvoicePdf(invoice, settings) → Uint8Array
- renderInvoicePdfBlob(...) → Blob for iframe / download / email attach
- Layout sections: header (sender + meta), recipient, subject, lines
table with wrapping + description row, totals with per-rate VAT
breakdown, notes, terms, footer
- Pagination: lines table opens a continuation page if content would
overflow into the QR-Bill reserved area; continuation pages redraw
the table header
Template (pdf/templates/default.ts)
- A4, margins in mm, emerald accent matching app icon
- Reserves 105mm at page bottom for the Swiss QR-Bill (M5) so the
body never collides with that region
DetailView integration
- Live PDF preview in an iframe — re-renders when invoice.updatedAt
changes (mutations bump the timestamp)
- Blob URLs revoked on render / unmount to avoid memory leaks
- "PDF herunterladen" button produces a Rechnung-{number}.pdf download
- Structured-data view moved behind <details> so the PDF is the primary
surface; raw data still accessible for debugging
pdf-lib dep added to @mana/web.
Plan: docs/plans/invoices-module.md §M4.
Next: M5 swissqrbill (Zahlteil in the reserved region).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the mobile-app deletion unblocked \`@context/mobile\`, five more
pre-existing failures surfaced across shared packages and two services.
All were silent-masked by the postinstall \`|| true\` for months.
- **shared-ai**: \`planner/loop.ts\` imported \`ToolSchema\` from
\`../tools/function-schema\`, which only imports (not re-exports) the
type. Fixed to import from the source (\`../tools/schemas\`).
- **shared-logger**: \`typeof window !== 'undefined'\` blows up under
tsconfigs that don't include the DOM lib (e.g. uload-server's
\`bun-types\`-only config), because shared-logger is consumed via
source import. Replaced with a \`globalThis\`-indirected check that
compiles under any lib configuration.
- **shared-hono**: \`credits.ts\` returned \`res.json()\` directly as
\`Promise<T | null>\`. Modern \`@types/node\` / undici types return
\`unknown\` strictly — cast to \`T\` at the boundary so the generic
contract is explicit.
- **uload-server**: \`routes/analytics.ts\` + \`routes/email.ts\` still
imported \`AuthUser\` from a \`middleware/jwt-auth\` module that was
deleted during the migration to \`@mana/shared-hono\`. Replaced with
\`AuthVariables\` from shared-hono, which matches the actual context
shape set by \`authMiddleware()\`.
- **manavoxel/web**: \`guestSeed\` collection entries were wrapped in
arrow functions, but \`local-store\` expects \`T[]\` directly and
iterates \`seed.length\` — which on a function is 0. The "guest
seed" was silently dead; eager-evaluating \`generateGuestWorld()\`
once and sharing the result fixes both the type and the runtime.
Verified: \`pnpm run type-check\` from the repo root now exits 0 —
76/76 tasks successful, no failures. First fully green state since
well before the postinstall \`|| true\` was introduced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- `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>
- 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>
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>
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>
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>
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>
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>
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>
- 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>
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>
- 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>
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>
- 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>
- 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>