Continuation of docs/plans/mana-mcp-and-personas.md. Personas are the
auto-test users the M3 runner will drive — they're real Mana users
(kind='persona', tier='founder'), registered through the same Better
Auth pipeline as humans, just stamped differently and metadata-tracked
so the persona-runner knows how to role-play them.
Schemas (auth namespace — personas are 1:1 with users, no reason for a
separate platform.* schema that the plan originally sketched)
- userKindEnum ('human' | 'persona' | 'system') + users.kind column,
wired into better-auth additionalFields so the JWT/user object carry
the flag. Default 'human' keeps every existing user untouched.
- auth.personas — 1:1 descriptor (archetype, systemPrompt, moduleMix
jsonb, tickCadence, lastActiveAt). CASCADE from users.id.
- auth.persona_actions — tick-grouped audit of every tool call the
runner makes (toolName, inputHash for dedup, result, latency).
- auth.persona_feedback — structured 1-5 ratings per module per tick,
plus free-text notes. This is where the runner writes the
self-reflection step at end of each tick.
Admin endpoints (/api/v1/admin/personas, admin-tier-gated)
- POST / create-or-update by email. Uses auth.api.signUpEmail
if the user's new, then stamps kind+tier+verified
and upserts the personas row. Idempotent — safe to
re-run after catalog edits.
- GET / list with 7-day action count per persona.
- GET /:id detail + recent 20 actions + per-module feedback
aggregate.
- DELETE /:id hard delete. Refuses non-persona users as
defense-in-depth: an admin typo here would cascade
through the full user-delete chain.
Catalog + seed pipeline (scripts/personas/)
- catalog.json 10 handwritten personas spanning 7 archetypes
(adhd-student, ceo-busy, creative-parent, solo-dev,
researcher, freelancer, overwhelmed-newbie).
Five pairs of personas that will later share
family/team spaces (cross-space setup is deferred
to M2.d per the plan).
- catalog.ts zod-validated loader. Refines email to require
@mana.test TLD — non-existent, no bounce risk.
- password.ts deterministic HMAC-SHA256(PERSONA_SEED_SECRET,
email). No stored per-persona credentials; the
runner re-derives on every login. Refuses the
dev-fallback secret in production.
- seed.ts POST /admin/personas per catalog entry. Flags:
--auth=, --jwt=, --dry-run.
- cleanup.ts Hard-delete every live persona. Warns when the
live set drifts from the catalog.
Root package.json:
pnpm seed:personas
pnpm seed:personas:cleanup
Extends the ESLint root-ignore list with `scripts/**` so Bun-typed
utility scripts don't fail the typed-parser check they weren't opted
into. Consistent with the rest of scripts/ being .mjs+.sh.
To go live (user action):
pnpm docker:up
cd services/mana-auth && bun run db:push
export MANA_ADMIN_JWT=...
pnpm seed:personas
M2.d deferred: cross-space (family/team/practice) memberships between
persona pairs. Better Auth's org-invite flow is multi-step and would
roughly double the M2 scope; the persona-runner (M3) can operate in
personal spaces first, shared-space tests land as their own milestone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two subdomains the webapp references in its SSR-injected config but
that had no tunnel entry:
- events.mana.how → mana-events on :3065. The container itself was
also missing (defined in compose but never started); started
today so the route now terminates somewhere real.
- research.mana.how → mana-research on :3068. The webapp was built
with PUBLIC_MANA_RESEARCH_URL empty, which made research fetches
fall back to mana.how and 404. The env-var side is still pending
a rebuild, but the tunnel side is live now.
Cloudflare CNAMEs already created via `tunnel route dns`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
M1 of docs/plans/me-images-and-reference-generation.md — a user-owned
pool of reference images (face, fullbody, hands, …) that will back
image generation where the user appears as themselves (outfit try-on,
glasses, portraits) via OpenAI /v1/images/edits. Data layer only in
this commit; UI lands in M2, the edits endpoint in M3.
- Dexie v38: meImages table with id/kind/primaryFor/createdAt indices.
Added to USER_LEVEL_TABLES so the hook stamps userId and skips the
spaceId/authorId/visibility trio (one human = one face across every
Space, not per-Space).
- Encryption registry: label + tags encrypted; kind/primaryFor/usage
stay plaintext because they drive the indexed queries and the
Reference picker's filtering. mediaId/URLs/dimensions are structural.
- Profile module store: createMeImage, updateMeImage,
setAiReferenceEnabled (per-image KI opt-in — plan decision #5),
setPrimary (transactional slot swap — only one row per primary slot),
deleteMeImage. Emits MeImage* domain events.
- Queries: useAllMeImages, useMeImagesByKind, useReferenceImages
(only the rows the user opted in for KI), useImageByPrimary.
- POST /api/v1/profile/me-images/upload: thin wrapper over mana-media
with app='me' as the reference tag. No new MinIO bucket — plan
decision #1 revised after verifying mana-media uses one bucket and
only tags references by app.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two Playwright-based diagnostic scripts for investigating
production-only browser issues that curl can't reproduce:
- scripts/smoke-prod.mjs: loads mana.how like a fresh incognito
tab, waits a configurable budget, reports every console error,
request failure, still-pending request, and slow resource.
- scripts/smoke-prod-load.mjs: measures DOMContentLoaded + load
event timing explicitly. Distinguishes "app interactive" from
"browser tab spinner stops".
Run: `node apps/mana/apps/web/scripts/smoke-prod.mjs`
MANA_URL=https://mana.how/login MANA_WAIT_MS=45000 node ...
Used today to rule out server-side issues in a loader-hang report
that reproduced only in one specific browser profile.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Foundation for autonomous Claude-driven testing. Plan:
docs/plans/mana-mcp-and-personas.md.
New packages
- @mana/tool-registry — schema-first ToolSpec<InputSchema, OutputSchema>
with zod generics, scope ('user-space' | 'admin') and policyHint
('read' | 'write' | 'destructive'). sync-client helpers speak the
mana-sync push/pull protocol directly so RLS and field-level LWW are
preserved. MasterKeyClient fetches per-user MKs via the existing
mana-auth GET /api/v1/me/encryption-vault/key endpoint (JWT-gated,
ZK-aware, already audited) — no new service-key endpoint built.
ZeroKnowledgeUserError surfaced as a typed throw.
- @mana/shared-crypto — AES-GCM-256 primitives extracted from the web
app's $lib/data/crypto/aes.ts so the server-side tool handlers and the
browser produce byte-for-byte identical wire format
(enc:1:{b64(iv)}.{b64(ct)}). Web app aes.ts now re-exports from
shared-crypto — 5 existing importers unchanged, svelte-check stays
green.
New service
- services/mana-mcp (:3069, Bun/Hono) — MCP Streamable HTTP gateway.
JWKS auth against mana-auth, per-user session isolation (session-id
belongs to the user who opened it — cross-user access returns 403),
admin-scoped tools filtered out before registration. MasterKeyClient
cached per process with a 5-minute TTL.
11 tools registered
- habits.{create,list,update,archive}, spaces.list (plaintext, M1)
- todo.{create,list,complete}, notes.{create,search}, journal.add
(encrypted — field lists match
apps/mana/apps/web/src/lib/data/crypto/registry.ts verbatim)
Infra
- Port 3069 added to docs/PORT_SCHEMA.md
- services/mana-mcp/CLAUDE.md with architecture, auth model,
tool-authoring recipe, local smoke-test steps
- Root CLAUDE.md services list updated
Type-check green across shared-crypto, mana-tool-registry, mana-mcp.
svelte-check on apps/mana/apps/web stays at 0 errors / 0 warnings.
Boot smoke verified: /health returns registry.loaded=true, unauthed
/mcp → 401, invalid-JWT /mcp → 401 with descriptive message.
Decisions locked in for later milestones (per plan D1–D10):
- Personas will be real mana-auth users (users.kind='persona'), no
service-key bypass (D1, D2)
- Tool-registry is the SSOT; mana-ai and the legacy
apps/api/src/mcp/server.ts get merged into it in M4 (three current
parallel tool catalogs collapse to one)
- Persona-runner (:3070) will be a separate service using the Claude
Agent SDK + MCP client (D5)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two pieces of the same cleanup:
1. build-app.sh now passes `--env-file .env.macmini` explicitly via a
shared COMPOSE_ARGS array. Without it, docker compose silently fell
back to `.env` in the project root — a separate file that happened
to hold MANA_AUTH_KEK and other secrets that `.env.macmini` lacked.
deploy.sh, restart.sh, and the CD workflow already used the flag;
this aligns build-app.sh with the rest. Server-side .env.macmini
was reconciled 2026-04-23 with the union of both files, so the
duplicate `.env` is no longer needed.
2. .env.macmini.example now documents 7 keys the prod stack actually
depends on but that had never been listed: GOOGLE_GEMINI_API_KEY /
GOOGLE_GENAI_API_KEY (SDK aliases for Deep-Research + mana-ai),
MANA_AI_PRIVATE_KEY_PEM / MANA_AI_PUBLIC_KEY_PEM (Mission-Grant
keypair), MANA_AI_DEEP_RESEARCH_ENABLED + PUBLIC_AI_MISSION_GRANTS
(feature flags), MANA_CORE_SERVICE_KEY (legacy alias), and the STT/
TTS internal shared secrets.
Matrix-bot tokens deliberately left undocumented — no Matrix homeserver
in the current running stack.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
apps/api/package.json lists @mana/shared-ai and @mana/shared-rss as
workspace deps, but the Dockerfile's builder stage never copied their
source. pnpm silently skipped the symlinks, and bun hit ENOENT on every
articles / ai import at runtime. Same class as 70c62e758 (shared-logger
in mana-auth) and the shared-types fix one commit earlier.
Without this, any push that triggered a mana-api rebuild failed
health-check and cascaded mana-web offline via depends_on.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mana-auth's package.json declares @mana/shared-types as a workspace
dependency, but the Dockerfile's install stage never copied its source
into the build context. pnpm then silently failed to create the
workspace symlink under node_modules, and bun hit ENOENT on every
import at runtime: "reading /app/services/mana-auth/node_modules/
@mana/shared-types".
The broken image sat undetected as long as the long-running container
didn't restart. Tonight's deploy recreated it and every mana-auth
container immediately crash-looped — taking mana-api and mana-web
down with it via depends_on.
Same class of bug as 70c62e758 (shared-logger).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs made the Mac Mini auto-deploy silently miss everything on a
multi-commit push:
1. Diff range was HEAD~1..HEAD, so a push with N commits only checked
the tip. Now uses github.event.before..sha, with a safe fallback to
HEAD~1 when the before SHA is absent (first push, force reset).
2. Service list was still the legacy per-product web/backend apps
(todo-web, chat-web, calendar-web, …) that were consolidated into
`mana-web` + `mana-api` months ago. The unified services didn't
exist in the workflow, so a push touching apps/mana/apps/web or
apps/api never rebuilt them.
Rewrite:
- Collapse per-service outputs into one `services` output driven by a
SERVICE_SOURCES array (add a new service by adding one line).
- Expanded service surface: mana-ai, mana-research, mana-events,
mana-user, mana-subscriptions, mana-analytics, mana-llm, mana-api,
mana-web, mana-credits, mana-geocoding, manavoxel-web — alongside
the Go services + memoro + landing-builder.
- Removed dead entries: todo/chat/calendar/clock/contacts/music/
storage/memoro-web variants.
- Expanded sveltekit-base trigger (any commit to shared-pwa /
shared-vite-config / root Dockerfile / pnpm-lock forces a base
rebuild — those were invisible before).
- Updated health-check URLs from the running containers' actual host
ports (PORT_SCHEMA.md prose + table disagreed; docker ps wins).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Merges the feature-rich gallery (search, tag filters, favorites toggle,
view-mode toggles, detail modal) that previously lived in
routes/(app)/picture/+page.svelte INTO modules/picture/ListView.svelte,
and keeps the upload affordances (drag-and-drop, upload button, progress
chips) from the old ListView.
Route shrinks to a 3-liner: <RoutePage appId="picture"><ListView /></RoutePage>.
Responsive behaviour uses CSS container queries (@container inline-size)
on the ListView root. Below ~560px (carousel card width) the search bar,
tag chips and view-mode toggles hide; action-strip buttons drop to
icon-only. Above that breakpoint (route context, ≥~720px up to the
layout's max-w-7xl) everything is visible.
Drag-over handler distinguishes file drags from cross-module drag data
via dataTransfer.types.includes('Files'), so the upload overlay only
appears for real file drops — workbench card-to-card drags pass through
to the wrapping AppPage's dropTarget.
Data source changes from context-based (getContext('allImages')) to
direct Dexie live-queries via ./queries, so the component works in both
the carousel (no layout context) and the route (layout still provides
context for /picture/archive and /picture/board).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every +page.svelte under routes/(app) now renders inside workbench-card
chrome. Before, sub-routes floated directly on the app-shell background
— card-style paper/border/shadow only existed on the homepage carousel,
leaving /library, /notes, /picture, /finance etc. visually disconnected
from the rest of the app.
Coverage:
- 28 SIMPLE routes (single <ListView /> wraps): <RoutePage appId="...">
- 43 top-level main routes: <RoutePage> with preserved internal markup
- 122 sub-routes (/X/[id], /X/new, /X/settings, …): <RoutePage> with
backHref pointing at the parent listing. Title overrides for detail
pages (e.g. "Rechnung", "Deck", "Eintrag").
- Articles tab children (/articles/list, /favorites, /highlights, /stats)
get explicit title overrides ("Artikel · Leseliste", etc.).
A handful of special cases:
- calc/standard: <svelte:window> hoisted outside RoutePage (Svelte forbids
window bindings inside component children).
- agents/templates: {#snippet templateCard} hoisted outside so both {#each}
blocks inside RoutePage can @render it via page-scope lookup.
- citycorners redirect-stubs (add/, locations/[id]/, map/): left unwrapped
— they onMount → goto() with no body to wrap.
- 3 carousel routes (/, /todo, /contacts) keep their PageCarousel wrapping
untouched — they already provide card chrome.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a third provider path to /api/v1/picture/generate that calls OpenAI
gpt-image-2 when model starts with "openai/". Supports n=1..4 batch
generation with character continuity, base64 response decoded server-side
and uploaded to mana-media for dedup + thumbnails. Credit cost scales
by quality (low=3, medium=10, high=25) × n.
Env plumbing:
- scripts/generate-env.mjs: new apps/api/.env stanza propagates
OPENAI_API_KEY + REPLICATE_API_TOKEN from .env.secrets
- .env.macmini.example: documents OPENAI_API_KEY for prod
Frontend /picture/generate: model + quality + aspect-ratio + batch-count
selectors, real fetch with auth, persists each image via imagesStore.insert
(encrypted + synced). Wrapped in ModuleShell variant=fill with back-arrow
to /picture and a live credit badge in the header actions slot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the old PageShell (workbench-only) with a single ModuleShell that
serves both carousel cards (variant=card, width-sized, window actions) and
sub-routes (variant=fill, fills main area, optional back button). RoutePage
wraps ModuleShell with auto-metadata lookup from the app-registry so every
(app)/*/+page.svelte can stay a three-liner.
Drops the dead onMinimize prop-drilling that was declared on PageShell but
never rendered — TodoPage/ContactPage callers cleaned up too.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
c413ab7dd was reverted by c31dcdd66; the re-apply (3a7bc7f1c) only
brought back the mana-research tests, not my sweep. Restored in
af4fd2776. Update the shipping-log row + the attribution note so
future readers find the actual payload.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 2e-followup originally shipped inside c413ab7dd (misattributed to
a test(mana-research) commit via lint-staged race). A later revert
(c31dcdd66) undid that commit, and the re-apply (3a7bc7f1c) only
restored the mana-research test files — dropping this at-rest-sweep
payload. This commit puts it back cleanly with the correct message.
- lib/data/crypto/at-rest-sweep.ts: post-vault-unlock one-shot sweep
that iterates every ENCRYPTION_REGISTRY table with enabled:true
and re-saves every row through encryptRecord(). Per-table
localStorage sentinel for idempotency; change-tracking suppressed
via beginApplyingTables so sync isn't flooded with re-encryption
writes. Fire-and-forget from the caller; idempotent inside each
row (isEncrypted gate in encryptRecord skips already-wrapped
fields).
- routes/+layout.svelte: after vaultClient.unlock() returns
'unlocked', dynamically import the sweep module and fire it. Same
lazy-load pattern the rest of the post-unlock wiring uses.
Plan doc's shipping-log entry stays pointed at c413ab7dd (the
original commit) since that's where the history trail starts, but
this commit is the one currently on main. Both are logged in the
attribution notes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Adds the 2c-followup #1 (f4c66241c, Dexie v35 data-table userId
drop) and #2 (ce5d1f1a2, Dexie v36 user-level table space-field
strip) to the shipping table.
- Adds a "Backend coherence" section documenting the 2026-04-22
post-migration audit of mana-sync. Key finding: mana-sync is
single-table event-sourcing (one sync_changes table with a
table_name discriminator, already space_id-indexed + RLS-scoped),
so the 7 newly-migrated client tables need zero server-side DDL.
Flag is removed from the open-follow-ups list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completes the "no table has both userId AND spaceId" invariant from
the space-scoped plan. Phase 2c-followup v35 cleaned the userId
column off data-record tables; this follow-up cleans the inverse off
user-level singleton tables (userSettings, invoiceSettings, …).
Hook change: user-level tables no longer receive spaceId / authorId /
visibility stamps on new writes. Those three fields are only
meaningful for tenant-scoped data; stamping them on user-level rows
was v28 collateral damage from the blanket migration.
Dexie v36 upgrade: deletes spaceId + authorId + visibility from
every row in the 11 user-level tables. No schema change — these
fields were never indexed on user-level tables, so .stores() stays
untouched.
Safety check before shipping: grep showed zero callers use
scopedTable(<user-level-table>) or .where('spaceId') against these
tables. They're queried directly by userId (via shared-stores or
singleton lookups), so dropping the space columns is a pure cleanup.
After this ships, user-level tables have {userId, …fields} and data
tables have {spaceId, __lastActor, …fields} — the invariant is
truly met app-wide.
Type-check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single file tracking code-complete features still awaiting a human
click-through. Distinct from test-health.md (auto-test coverage) and
TESTING_DEPLOYMENT_CHECKLIST.md (CI system).
Seeded with 4 open entries: data-export-v2 roundtrip, shared-space
two-user smoke (links to existing walkthrough), articles bookmarklet
on consent-walled sites, PWA share-target. Entries get deleted once
verified in a real browser.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The new roundtrip.test.ts uncovered that the importer only stripped
`userId` — after Phase 2c, data tables are scoped by `spaceId`
(sentinel `_personal:<userId>`), so cross-account restores left rows
bound to the source user's personal space and invisible under RLS.
Fix: strip `userId`, `spaceId`, AND `authorId` before bulkPut, so the
Dexie creating-hook re-stamps all three from the current session.
6 new orchestration tests: plain round-trip, scope-filter, cross-
account spaceId adoption, unknown-table skip, sealed round-trip,
wrong-passphrase rejection.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the mana-sync event-stream export (GET /backup/export) with a
fully client-driven `.mana` v2 archive: webapp reads Dexie, decrypts
per-field, packages JSONL + manifest, optionally PBKDF2+AES-GCM seals
with a passphrase.
- New: backup/v2/{format,passphrase,export,import}.ts + format.test.ts
(10 tests: round-trip, sealed path, 3 failure modes incl. wrong-
passphrase vs. tamper distinction).
- UI: ExportImportPanel with module multi-select, optional passphrase,
progress + sealed-file detection — replaces the old backup flow in
Settings → MyData.
- Removes services/mana-sync/internal/backup/ and the corresponding
client helpers + v1 tests. No parallel paths, no legacy shim.
- Why client-driven: zero-knowledge users hold their vault key only
client-side, so a server exporter cannot produce plaintext archives;
GDPR Art. 20 portability is better served by plaintext-by-default.
- Cross-account restore works via re-encryption under the target
vault key (no MK transfer needed).
DATA_LAYER_AUDIT.md §8 rewritten to reflect the new architecture.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-commit of c413ab7dd (reverted in c31dcdd66) without the unrelated
files that accidentally got swept into the original stage. Parser
content is identical.
The real Gemini /v1beta/interactions/:id completed shape bit us once
already during the initial smoke-test (we had OpenAI-style nested
`output.message.content[]` coded; reality is a flat `outputs` array
of thought|text|image items, with url_citations that carry no title
and usage fields named `total_input_tokens` rather than `input_tokens`).
This test pins the parser against a synthetic fixture covering the
cases we saw in the wild plus the failure modes that are hard to
provoke from a live API call:
- status dispatch (queued, in_progress, failed, cancelled, incomplete)
- completed body concatenated across text items, skipping thought/image
- empty/missing `outputs` without crashing
- missing usage
- citations deduped by url, hostname extracted as title
- wrong-type annotations and those without url skipped
- real vertexaisearch redirect URLs Gemini emits
- fallback to url as title when the URL is unparseable
- trimming of leading/trailing whitespace
To make this testable I pulled the completed-branch of
pollGeminiDeepResearch into a standalone parseInteractionResponse
helper — same behaviour, now reachable without mocking global fetch.
Also adds the `test` script to package.json so `pnpm --filter
@mana/research-service test` works.
17 pass / 0 fail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completes the userId cleanup Phase 2c left half-done. The creating-hook
(commit e9b9544ea) stopped stamping userId on new writes, but existing
rows still carried the column from v28 onwards — mixed state. This
migration removes the column from every data-record row and drops the
articles module's userId indexes that are now dead.
v35.stores():
Re-declares articles / articleHighlights / articleTags without
the `userId` index. Other indexes (status, savedAt, isFavorite,
siteName, originalUrl, [articleId+startOffset], [articleId+tagId])
stay identical.
v35.upgrade():
Iterates every SYNC_APP_MAP table that isn't on the USER_LEVEL list,
calls `.modify()` to `delete record.userId` on every row. User-level
tables (userSettings, userContext, newsPreferences, meditate/sleep/
mood/time/invoice/broadcast/wetterSettings, userTagPresets) keep
their userId — their ownership model is user-scoped by design.
The USER_LEVEL set is duplicated inside the upgrade closure because
the hook-registration loop (where the runtime USER_LEVEL_TABLES const
lives) hasn't run yet when the upgrade fires — Dexie applies upgrades
before we call `db.table(...).hook()`.
Public-type converters (tags-local's toTag/toTagGroup, calc's
toCalculation/toSavedFormula) already fall back to 'guest' / '' when
userId is absent, so the field's disappearance doesn't break
downstream reads.
After this ships, the "no table has both userId AND spaceId"
invariant from the plan is truly met on data records. User-level
tables still have both (v28 stamped spaceId onto them) but that's a
separate, lower-priority cleanup.
Type-check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings the shipping log up to date with everything shipped since the
last doc commit, plus flags the c413ab7dd attribution race (same
lint-staged rollback pattern that caught 3b85d7d3d) so future
searches find the at-rest-sweep payload under a misleading
test(mana-research) title.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The real Gemini /v1beta/interactions/:id completed shape bit us once
already during the initial smoke-test (we had OpenAI-style nested
`output.message.content[]` coded; reality is a flat `outputs` array
of thought|text|image items, with url_citations that carry no title
and usage fields named `total_input_tokens` rather than `input_tokens`).
This test pins the parser against a synthetic fixture covering the
cases we saw in the wild plus the failure modes that are hard to
provoke from a live API call:
- status dispatch (queued, in_progress, failed, cancelled, incomplete)
- completed body concatenated across text items, skipping thought/image
- empty/missing `outputs` without crashing
- missing usage
- citations deduped by url, hostname extracted as title
- wrong-type annotations and those without url skipped
- real vertexaisearch redirect URLs Gemini emits
- fallback to url as title when the URL is unparseable
- trimming of leading/trailing whitespace
To make this testable I pulled the completed-branch of
pollGeminiDeepResearch into a standalone parseInteractionResponse
helper — same behaviour, now reachable without mocking global fetch.
Also adds the `test` script to package.json so `pnpm --filter
@mana/research-service test` works.
17 pass / 0 fail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The creating-hook now splits its user-stamping behaviour by table:
- USER_LEVEL_TABLES (userSettings, userContext, newsPreferences,
meditateSettings, sleepSettings, moodSettings, timeSettings,
invoiceSettings, broadcastSettings, wetterSettings, userTagPresets)
still get userId stamped — these rows are primarily scoped to the
signed-in user rather than a Space.
- All other sync tables (the ~53 data-record tables) no longer
receive userId on new writes. Attribution is the Actor system's
job (__lastActor + __fieldActors are already stamped on every
write); tenancy is the spaceId column's job (stamped below in the
same hook). Keeping both userId and spaceId on data records was
redundant.
Migration approach — lenient, no Dexie bump: existing rows keep the
userId they were stamped with in v28. New writes don't have it. The
three public type converters that exposed userId (tags-local's
toTag/toTagGroup, calc's toCalculation/toSavedFormula) use a
`?? 'guest'` / `?? ''` fallback, so rows without userId stay
readable. The 16-site codebase audit in phase 2c found no load-
bearing reader: the few sites that reference record.userId are
either one-time migration code (v28/v31/guest-migration), manifest
metadata (backup format — different userId field), or the hook's own
immutability guard.
authorId stamping now derives from effectiveUserId directly instead
of reading objRecord.userId — the previous chain relied on the
userId stamp having just happened, which no longer holds for data
tables.
The "no table has both userId AND spaceId" invariant from the plan
is now partially met: data tables will converge on it as old rows
cycle out. User-level tables still have both but that's by design
(userId = ownership, spaceId = v28 Personal-sentinel carried through
the hook; a future cleanup could drop the spaceId on user-level
tables but it's harmless today).
Tests: 20/20 agents + workbench-scenes pass. Type-check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Capture the surprises from the first deploy so the next rollout
(or rollback) has the full picture without spelunking logs:
- mana-research had never been started on the Mac-Mini, even though
it was defined in compose. First-boot via `docker compose up -d`.
- research.* schema is not auto-migrated on service boot — drizzle
push must be triggered explicitly: `docker exec mana-research
bun run db:push`. 5 tables created.
- GOOGLE_GENAI_API_KEY was missing in /Users/mana/.../mana-monorepo/.env.
Copied the local key over, with `.env.bak.pre-gemini-deep-research`
as rollback anchor.
- Redis NOAUTH fix (commit 4867300d0) referenced here.
- Smoke-test outcome documented: the 500 was mana-credits 404 on a
test user without a wallet row — expected, and it proves the whole
auth/dispatch chain up to the credits hop works.
- Also noted: mana-llm has the same bare REDIS_URL in compose
(out-of-scope for this deploy), and /providers/health does not list
async providers (known design gap).
Status header updated to reflect deploy completion. Flag stays off
(MANA_AI_DEEP_RESEARCH_ENABLED=false) pending explicit enablement.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Redis runs with --requirepass, but mana-research was pointing at
redis://redis:6379 without credentials. Cache misses are not fatal
(the executor falls back to the upstream provider on every request)
but the NOAUTH error spam drowns real errors in logs/glitchtip.
Match the pattern other services use:
redis://:${REDIS_PASSWORD:-redis123}@redis:6379
Caught during the deep-research deploy smoke-test on 2026-04-22.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Turns on at-rest encryption for the four tables staged in Phase 2a.
New writes now encrypt the user-typed fields; future code paths read
via decryptRecords as normal (the modules already call decrypt on
read, no changes needed).
Flipped:
- globalTags.name — tag names can leak categorization intent
- tagGroups.name — same
- workbenchScenes.name/description — scene labels often encode
Space-specific context
- aiMissions.title/conceptMarkdown/objective — mission configuration
is user-authored
Deliberately unchanged:
- color / icon / groupId / sortOrder / openApps / wallpaper /
scopeTagIds / cadence / state / agentId — all structural, indexed,
or FK data needed for query paths
- agents.name stays plaintext per the prior design note (Actor
displayName cache key)
Migration approach — pre-live lenient: decryptRecords skips values
that aren't encrypted (isEncrypted gate in record-helpers.ts:256), so
existing plaintext rows stay readable after the flip. New writes
encrypt; existing rows get encrypted organically as the user edits
them. No Dexie migration needed. A post-login "encrypt-at-rest
sweep" over pre-existing rows is a follow-up if hard at-rest coverage
is required before launch.
Crypto audit: 196 Dexie tables (95 encrypted, +4 vs 91 before),
101 allowlisted plaintext. Type-check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the userTagPresets loop: users can now create, set-default,
and delete presets from Settings → Tag-Presets, making the dropdown
in SpaceCreateDialog actually useful (before this, it only showed
"empty" / "copy-current" because no presets existed).
New settings category "Tag-Presets":
- searchIndex.ts: adds the category entry + anchor; sidebar picks it
up automatically since it iterates `categories`.
- TagPresetsSection.svelte: list + create + delete + set-default.
- settings/ListView.svelte: conditional render wiring.
The create flow is deliberately one-click: name the preset, hit
"Aus <activeSpace.name> erstellen", and we snapshot every non-deleted
tag + tagGroup in the active Space into the new preset (with
groupName denormalized so the preset is space-independent). The first
preset automatically becomes the user's default — subsequent ones can
be promoted via the star button.
No full per-entry editor in this commit. If the user wants to tweak a
preset's contents, they create a sibling Space with the preset,
modify tags there, and promote THAT Space's tags to a new preset.
Scope-creep avoidance for a feature whose main value is snapshotting,
not authoring.
Type-check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The frontend-consistency-improvements.md was stale — it still listed
the 13 Tailwind-based ListViews as "to migrate" even though all 21
flagged modules shipped today (a2a43b1d5, 86c205ffc, 7d6a340b1, 52af8c0ce,
3e09ff66d). Rewrite to reflect the SHIPPED state and list the next
layer of open consistency work (i18n, Phosphor icons, cross-surface
theme parity).
New docs/optimizable/README.md is the master index. Consolidates:
- 🔴 Release blocker: tier-patch revert (links memory entry)
- 🟠 Tracked trackers: per-topic links with status
- 🟡 Small open items without a dedicated file: module-structure
audit, plan-inventory hygiene, memory-hygiene post-release,
cross-surface theme parity
- How-to: list of `pnpm run audit:*` commands for live metrics
Doesn't introduce new work items beyond what's already been discussed
this session — just gives them a home so future sessions can pick up
any one without re-discovery.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the Phase 2d.5a helpers (applyPresetToSpace,
copyTagsBetweenSpaces) into the new-Space UX so users get a familiar
tag taxonomy in every Space they create, without manual re-entry.
The dialog gains a "Tag-Set" dropdown:
- "Leer starten" — new Space starts without any tags
- "Aus <current> kopieren" — clones the user's active Space's
globalTags + tagGroups as a one-shot snapshot (fresh ids, no live
link back to the source)
- <named-preset> — applies a userTagPreset snapshot, creating tagGroups
for each distinct groupName so the user's familiar grouping carries
over
Default pick (when the dropdown first renders):
- If the user has a default preset → that preset
- Else if currently in Personal → "copy-current"
- Else "empty" (safer inside shared Spaces — don't leak Team/Family
taxonomy into a new one by default)
Seeding runs BEFORE the Space activation switches context, so
copyTagsBetweenSpaces still sees the source-Space's tags as
read-scope. Seeding failures are caught and logged but deliberately
non-fatal — the Space is already created, the user can seed later
from inside it.
`<select>` styling piggy-backs on the existing .field input/textarea
rules (extends the shared selector list instead of duplicating).
Type-check + Svelte a11y check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
/(app)/invoices/[id] route bundle drops from **534 KB → 18.6 KB** by
moving PDF rendering behind dynamic imports.
Changes:
- views/DetailView.svelte: `await import('../pdf/renderer')` inside
renderPdf() + downloadPdf(), cached in a module-local ref.
- components/SendModal.svelte: same for openAndDownload().
- pdf/scor.ts (new): generateSCORReference extracted so the
invoices store can derive a reference string without pulling
swissqrbill/svg + pdf-lib into the list-view bundle.
- pdf/qr-bill.ts: re-exports generateSCORReference from scor.ts
for backward compatibility.
- stores/invoices.svelte.ts: imports from ../pdf/scor (light) instead
of ../pdf/qr-bill (heavy).
- index.ts: drop re-export of the PDF renderer from the module
barrel so `import ... from '$lib/modules/invoices'` never drags
pdf-lib in.
The heavy chunk (pdf-lib + swissqrbill, ~576 KB) now only loads when
a user actually opens an invoice detail — list views, create flow, and
all other routes stay lean.
20/20 qr-bill tests pass; svelte-check clean.
Bonus: scripts/audit-icon-usage.mjs (+ pnpm run audit:icon-usage)
audits @mana/shared-icons imports. Reveals 204 distinct icons across
the codebase, 199 of them at default weight but paying for all 6
Phosphor weights. Biggest offender: app-registry/apps.ts with 69
static icon imports accounting for ~290 KB of the shared 466 KB icon
chunk. Migration path for that is documented in
docs/optimizable/bundle-analysis.md §2 — next session's work.
docs/optimizable/bundle-analysis.md also updated with the root (app)
layout (260 KB) investigation notes (start/stop lifecycle hooks to
defer via idleCallback).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the two seeding helpers the Space-creation flow needs:
- applyPresetToSpace(presetId, targetSpaceId): one-shot-copies a
preset's frozen snapshot as fresh globalTags rows in the target
Space. Creates tagGroups for each distinct groupName so the user's
familiar grouping carries over. Not a live link — renaming the
preset afterwards doesn't rename applied tags.
- copyTagsBetweenSpaces(sourceSpaceId, targetSpaceId): duplicates
every non-deleted tag + tagGroup from one Space into another with
fresh ids. Powers the "copy tags from my current Space" option in
SpaceCreateDialog so solo-Space users don't have to build a named
preset before they inherit their existing taxonomy.
Both helpers explicitly stamp spaceId on every written row so the
write lands in the TARGET Space even while the caller's active-space
context is still the SOURCE Space (SpaceCreateDialog: create Space
→ apply preset → activate → reload). The Dexie creating-hook
normally stamps spaceId from getActiveSpaceId(); pre-populating it
makes the hook's `if undefined/null` guard skip.
Both run inside a single Dexie transaction so a mid-batch failure
doesn't leave a half-seeded Space.
Duck-typed LocalTagShape / LocalTagGroupShape local to this file —
the authoritative types live in @mana/shared-stores but importing
them here would create an awkward data-layer → shared-stores
dependency direction.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two purposes:
1. Make the phase-by-phase progress discoverable — future readers can
see at a glance what's shipped, which commit hash lands each
phase, and what's still open.
2. Flag the 2d.4 attribution oddity: the active-space handler API +
per-Space workbench-scenes localStorage + scene spaceId filter +
runAgentsBootstrap-on-space-change wiring landed inside commit
3b85d7d3d ("chore(bundle): add bundle-size audit") by accident,
when a parallel terminal session's git add -A scooped up those
staged files during a lint-staged rollback race. The commit
message understates the contents; code is correct and tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Opt-in path for missions that want Gemini Deep Research Max (up to 60 min
per task) instead of the shallow RSS pre-research. Because Max runs well
past a single 60-second tick, the state is carried across ticks:
tick N: submit → INSERT mission_research_jobs row → skip planner
tick N+k: poll → still running → skip planner (metric pending_skips)
tick N+m: poll → completed → inject as ResolvedInput, DELETE row, plan
- ManaResearchClient talks to mana-research's new internal
/v1/internal/research/async endpoints with X-Service-Key +
X-User-Id. Graceful-null on transport errors so a flaky
mana-research never crashes the tick loop.
- New table mana_ai.mission_research_jobs with PK (user_id, mission_id)
— presence is the "pending" flag; delete-on-terminal keeps queries
trivial.
- handleDeepResearch() encapsulates the state machine; planOneMission
now returns a discriminated union (planned | skipped | failed) so
"research pending" isn't miscounted as a parse failure.
- Opt-in at TWO gates to keep cost in check ($3–7/task, 1500 credits
per run):
1. MANA_AI_DEEP_RESEARCH_ENABLED=true server-side (default off)
2. DEEP_RESEARCH_TRIGGER regex matches the mission objective
(strict: "deep research", "tiefe recherche", "umfassende
recherche", "hintergrundrecherche", "deep dive")
Falls back to shallow RSS when either gate fails or the submit
errors upstream.
- Prom metrics: mana_ai_research_jobs_{submitted,completed,failed}_total
labelled by provider, plus _pending_skips_total.
- docker-compose wires MANA_RESEARCH_URL + the opt-in flag and adds
mana-research to depends_on.
- Full write-up with real API response shape (outputs plural, not
OpenAI-style), step-3 MCP-server plan (security-gated, not built),
ops + kill-switch: docs/reports/gemini-deep-research.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New providers gemini-deep-research + gemini-deep-research-max on the
Interactions API (preview-04-2026). Submit/poll split, tier parameter
selects between standard (~minutes, $1–3) and max (up to 60 min, $3–7).
- Parser matches the real response shape: flat `outputs` array of
thought|text|image items, url_citation annotations without title,
`usage.total_input_tokens` / `total_output_tokens`.
- Route generalisation: /v1/research/async accepts `provider` with
default 'openai-deep-research' (backward compatible) and dispatches
to the right submit/poll pair.
- New internal service-to-service endpoint /v1/internal/research/async
gated by X-Service-Key + X-User-Id for credit accounting. Enables
mana-ai to drive deep-research jobs on the mission owner's wallet
without requiring a user JWT.
- Pricing: 300 credits (standard) / 1500 credits (max). Conservative
markup over the ~$3/$7 ceiling so the first runs can't surprise us.
- Docs: AGENT_PROVIDER_IDS + pricing + env map + auto-router stay in
sync; CLAUDE.md Phase 3b now current; API_KEYS.md references the
new providers under GOOGLE_GENAI_API_KEY.
Verified with a real smoke test against the Gemini API: submit + poll
both succeed, completed response parsed cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Articles ist jetzt als Workbench-App in apps.ts registriert
(icon BookOpen, collection 'articles', paramKey 'articleId') und
landet damit im Scene-App-Picker. HomeView/ListView/HighlightsView/
StatsView teilen sich eine neue ArticlesTabShell, die sowohl als
SvelteKit-Route als auch als Workbench-Karte rendert.
Shell (ArticlesTabShell.svelte):
- Top-Bar mit QuickAddInput (URL einfügen + Enter = Save + goto
Reader; kein Preview-Schritt) und Settings-Gear.
- Tab-Leiste darunter: Leseliste | Highlights | Favoriten | Stats.
Leseliste ist Default (initialTab='list').
- Tab-Wechsel läuft intern via $state + Svelte-Context — kritisch
für die Workbench-Karte, wo goto() den User aus der Karte kicken
würde. getArticlesTabContext() aus tab-context.ts gibt tief
verschachtelten Sektionen eine switchTo(tab)-API.
- Padding 1rem 1.25rem auf der Shell selbst — PageShell.page-body
hat null padding, sonst klebt QuickAdd am Card-Rand. Im Route-
Kontext addiert's sich zum (app)-Layout-Padding ohne zu viel.
Tabs:
- Leseliste (list): bestehende ListView mit optionalem
initialFilter-Prop. Continue-Reading-Strip (HomeSectionWeiterlesen
horizontal carousel) erscheint über den Filter-Chips wenn
status='reading'-Artikel existieren und filter ∈ {all, reading}.
Filter-Chips sind einzeilig + horizontal scrollbar mit
scroll-snap-Einrast; inaktive Chips haben jetzt sichtbare
Background-Füllung + Border via color-mix(currentColor) — adaptiv
fürs Theme.
- Highlights (highlights): HighlightsView unverändert (nur der
eigene Header + Zurück-Button raus, liegt jetzt in der Shell).
- Favoriten (favorites): ListView mit initialFilter='favorites' —
Shell-Shortcut auf den Filter.
- Stats (stats): neue StatsView mit Stats-Strip (savedThisWeek,
finishedThisWeek, avg reading time), Highlight-Counter, Top-
Sources und Archiv-Link.
Routes (unter (tabs)-Gruppe):
- /articles → initialTab="list" (Default)
- /articles/list → initialTab="list" (alias)
- /articles/highlights → initialTab="highlights"
- /articles/favorites → initialTab="favorites"
- /articles/stats → initialTab="stats"
Detail/Add/Settings bleiben bewusst ausserhalb — die haben ihren
eigenen Reader/Form-Chrome und sollen die Tab-Leiste nicht zeigen.
Neue Files:
- ArticlesTabShell.svelte (Tab-Host)
- tab-context.ts (Cross-Tab-Switch-Context)
- components/ArticleCard.svelte (shared Card aus ListView extrahiert,
row + compact Varianten)
- components/QuickAddInput.svelte (URL-Input aus HomeView extrahiert)
- components/HomeSectionSources.svelte
- components/HomeSectionStats.svelte
- components/HomeSectionWeiterlesen.svelte
- views/StatsView.svelte
- routes/(app)/articles/(tabs)/{+page,list,highlights,favorites,stats}
Gelöscht:
- HomeView.svelte (Overview-Tab wurde rausgenommen auf User-Feedback)
- HomeSectionFrisch/Highlights/Favorites (durch eigene Tabs ersetzt)
docs/plans/articles-homepage.md dokumentiert den Architektur-Plan,
inklusive der Entscheidung für "eine Card pro Domain, interne Tabs"
statt zwei separater App-Registrierungen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before this commit, the bootstrap created one "Mana" agent per user.
After per-Space migration, every Space needs its own default agent so
Actor attribution shows the right identity and missions land in the
right Space. Three users with Personal + Family + Brand Spaces would
have ended up with three "Mana" agents in the picker — ugly and
confusing.
Now each Space type gets a name that reads naturally:
- personal → "Mana" (keeps legacy name + DEFAULT_AGENT_ID
so historic Actor.displayName on
pre-migration records still renders)
- family → "Familien-Helfer"
- team → "Team-Assistent"
- brand → "Brand-Assistent"
- club → "Verein-Helfer"
- practice → "Praxis-Assistent"
Stable id scheme:
- Personal: DEFAULT_AGENT_ID (legacy coupling with LEGACY_AI_PRINCIPAL)
- Others: `default:<spaceId>` (deterministic, collision-free)
Bootstrap bypasses the regular createAgent path (which enforces
global name-uniqueness) because the same name is legitimately repeated
across multiple Spaces of the same type. Deduplication happens via
getAgent(id) + Dexie's add-or-skip for cross-tab races instead.
ensureDefaultAgent() reads the active Space via getActiveSpace(); when
no Space is loaded yet (pre-bootstrap first boot) it falls back to the
Personal default. The per-Space re-run on onActiveSpaceChanged (Phase
2d.4) picks up the correct agent once loadActiveSpace resolves.
Type-check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#6 test coverage (pivot to reporting): 34/653 tests currently fail
(in-flight spaces-foundation migrations). Hard coverage thresholds
aren't enforceable until the suite is green, so this session ships a
file-presence audit instead of line-coverage gates.
- scripts/audit-test-coverage.mjs — counts .svelte + .ts source files
vs .test.ts + .spec.ts per module. Reports total ratio, lists
modules with 0 tests + ≥3 source files (prioritised by size).
- pnpm run audit:test-coverage wires it into audit:*.
- docs/optimizable/test-health.md — state + prevention path + top
untested modules ranked by impact.
Current baseline: 2.6% file-level coverage. 66/78 modules have zero
tests. Biggest untested: times (32 src), articles (29), events (27),
inventory + skilltree (20 each).
#8 audit:all: single entry point for the reporting audits. Runs
port-drift + i18n-coverage + test-coverage in --summary mode. Distinct
from validate:all (which is gates, not reports).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Since Phase 2d.2 of the space-scoped rollout, each Space can have its
own kontextDoc. Before this commit, the module was a user-level
singleton keyed by id='singleton' — which meant Shared/Brand/Family
Spaces saw the user's Personal-Space bio as their AI planner context.
Changes:
- types.ts: relax LocalKontextDoc.id to plain string (was the literal
'singleton'). KONTEXT_SINGLETON_ID stays as an exported const so
legacy Personal-Space rows (stamped before the refactor) are
documented; no longer used at write sites.
- stores/kontext.svelte.ts: ensureDoc() finds the active-Space row via
scopedTable(), creates a fresh UUID row if absent. setContent /
appendContent operate on the found-or-created row's id. Personal-
Space's legacy 'singleton' row keeps rendering because the
`_personal:<userId>` sentinel is inside getInScopeSpaceIds()'s
returned set.
- queries.ts: useKontextDoc() mirrors the same scopedTable filter.
- ai/missions/default-resolvers.ts: kontextIndexer surfaces the active
Space's kontextDoc (not hardcoded 'singleton'). Shared-Spaces without
a doc yet return an empty candidate list, which is the correct
empty-state for the mission-input picker.
Type-check clean. No schema change; relies on v28's existing spaceId
stamping + the creating-hook's ongoing stamp (kontextDoc is in the
kontext module.config).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
svelte-check emits a11y + dead-CSS + Svelte-5 $state warnings that were
previously non-blocking — pre-push only caught hard type errors. The
a11y-30 cleanup commit (3e09ff66d) brought the warning count to 0, so
flipping `--fail-on-warnings` on now makes the checker hold the line:
any new warning fails the pre-push hook that runs `pnpm check`.
Covers: a11y_click_events_have_key_events, a11y_consider_explicit_label,
css_unused_selector, state_referenced_locally, and the other svelte-check
diagnostic categories.
No behaviour change with current codebase (0 warnings); prevents drift
going forward.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First wire-up of the userTagPresets surface from Phase 2b's v34 schema.
This is the store layer only — Space-create UI integration + the
apply-preset-to-space flow land in a follow-up commit alongside the
SpaceCreateDialog changes.
- lib/data/tag-presets/types.ts: LocalUserTagPreset shape + inline
TagPresetEntry + toUserTagPreset converter.
- lib/data/tag-presets/store.svelte.ts: createPreset / updatePreset /
deletePreset / setDefault / appendEntry. Stamps userId explicitly
because userTagPresets is kept out of SYNC_APP_MAP (the Dexie
creating-hook only fires for sync tables). At-most-one-default-per-
user invariant enforced by clearDefaultFlag() before writes that set
isDefault=true.
- lib/data/tag-presets/queries.ts: useUserTagPresets + useDefaultTagPreset
live queries. User-scoped, no active-space filter (presets show from
any Space context).
- crypto: move userTagPresets from plaintext-allowlist to
ENCRYPTION_REGISTRY with fields ['name', 'tags']. AES wrapping handles
the tags array via JSON-stringify, same pattern as food.foods.
Crypto audit: 196 tables (95 encrypted, +1 userTagPresets). Type-check
clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each services/*/CLAUDE.md declares `## Port: NNNN` — the authoritative
per-service port spec (docs/PORT_SCHEMA.md is explicitly partially
aspirational). This audit verifies:
1. Declared port appears as a literal in the service's own source
(catches: moved port in code but forgot to update CLAUDE.md).
2. No two services claim the same port (catches: accidental
collision when scaffolding new services).
Current state: ✓ 15 services, all declared ports found in code, zero
collisions (mana-auth/geocoding/stt/tts/image-gen/voice-bot/mail/
credits/user/subscriptions/analytics/events/news-ingester/ai/research).
Report-only; not a CI gate. Run with `pnpm run audit:port-drift`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PlayView used Tailwind palette classes for game-status feedback:
bg-emerald-500/10 + text-emerald-300 (won) → bg-success/10 + text-success
bg-amber-500/10 + text-amber-300 (lost) → bg-warning/10 + text-warning
border-red-500/20 + bg-red-500/10 +
text-red-300 (error) → border-error/20 + bg-error/10 + text-error
placeholder-white/30 focus:border-purple-400/50 → placeholder:text-muted-foreground/60 focus:border-primary/50
Semantic status now tracks the theme (errors are red in dark, darker red
in light, etc.) instead of being fixed hex ramps.
The `bg-purple-500` / `bg-purple-500/30` / `hover:bg-purple-600` classes
on the user's chat bubble and submit buttons STAY — purple is the who
module's primary identity colour (historical-deck accent `#a855f7` is
semantically the same hue). Documented in brand-literals.md §who.
Also harden two validators against mid-rename states where git ls-files
returns paths that aren't on disk yet — both now skip unreadable files
instead of crashing the pre-commit hook (caught while migrating who).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Translation infrastructure (@mana/shared-i18n + svelte-i18n + 35
per-module locale files with ~3500 lines across de/en/it/fr/es) is fully
wired, but 65/78 modules still hardcode German in .svelte templates
rather than calling {$_('module.key')}.
Adds:
- scripts/audit-i18n-coverage.mjs — scans lib/modules/**/*.svelte for
hardcoded German keywords (Abbrechen, Speichern, Löschen, etc.) in
files that don't import $_(). Reports per-module hit counts,
bucket (FULL/PARTIAL/NONE), and whether the locale file exists.
Supports --summary and --top N flags.
- pnpm run audit:i18n-coverage wires it into the audit:* family
(reporting only, not a CI gate — existing debt would fail
validate:all otherwise).
- docs/optimizable/i18n-migration-inventory.md — priority list,
per-module workflow, and prevention plan.
Top offenders: broadcast (26 hits), articles (24), events (23),
invoices (22), quiz (20), stretch (20), library (19), profile (17),
skilltree (15, PARTIAL), calendar (14, PARTIAL). Modules without a
locale file (broadcast/articles/events/invoices/…) need the locale
stubs scaffolded first.
Real string migration is per-site careful work (key naming, 5-language
parity, UI visual QA) and is left for per-module follow-up sessions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the two small schema changes needed for the space-scoped rollout.
The heavy lifting (userId drop, kontextDoc reshape, store-API pivots)
lives in 2c / 2d — this commit keeps scope surgical to isolate the
Dexie version bump.
1. userTagPresets table — user-level templates for seeding tags into
newly-created Spaces. Deliberately NOT space-scoped: the preset
picker runs from ANY Space during new-Space creation, so active-
space filtering would hide the user's other presets. Indexed on
userId + isDefault. NOT yet in SYNC_APP_MAP — cross-device sync
wires up in 2d alongside the CRUD store API.
2. Compound indexes on globalTags + tagGroups:
- globalTags: [spaceId+sortOrder] (per-Space sorted list) +
[spaceId+name] (in-Space dedup-by-name)
- tagGroups: [spaceId+sortOrder]
Tags/tagGroups already carry spaceId on every row (v28 migration +
the creating-hook's ongoing stamping), these indexes simply let
per-Space queries skip the client-side JS filter that
scopedForModule does today.
Audit script pass-through: userTagPresets sits on the plaintext
allowlist with a comment flagging that it moves to ENCRYPTION_REGISTRY
in 2d once the store API wraps writes. 196 Dexie tables classified,
type-check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The plan-doc commits 129971ffc + 9db044178 dropped the
audit-theme-tokens → validate-theme-variables rename, the
validate-theme-tokens → validate-theme-utilities rename, the new
validate-theme-parity script, brand-literals.md, and the corresponding
package.json + lint-staged.config.js + themes.css wiring. The files
still existed on disk (git mv changes survived) but were untracked.
Restore the validator suite so `pnpm run validate:all` works again:
- validate:theme-variables (CSS var names: --muted → --color-muted)
- validate:theme-utilities (Tailwind: no white/N, no neutral palette)
- validate:theme-parity (every --color-* in :root ⇔ .dark + each
[data-theme="..."])
All three wired into validate:all and lint-staged. `pnpm run validate:all`
is clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Preparation step for the space-scoped data model migration (Phase 2b).
Moves globalTags, tagGroups, workbenchScenes, and aiMissions from the
plaintext allowlist into the encryption registry with enabled:false —
so the audit script documents which fields WILL be encrypted without
changing any runtime behaviour.
Fields chosen per design-doc:
- globalTags.name — personal categorization (Therapie, Finanzen-privat)
- tagGroups.name — same
- workbenchScenes.name + description — scene labels often encode
Space-specific context (Q2-Launch, Urlaub 2026)
- aiMissions.title + conceptMarkdown + objective — all user-typed
mission config; state/cadence/inputs stay plaintext for the Runner
Deliberately kept plaintext (against my initial suggestion):
- aiAgents.name — registry comment explains: name is the Actor
displayName cache key for historic attribution. Encrypting would
show "🤖 [encrypted]" on every past task the agent ever touched.
- globalTags.icon / tagGroups.icon / color — not personal content;
icon is a visual cue, color is theme metadata
The 2c migration (Dexie v35, flip enabled:true) runs after 2b lands
the schema changes so existing rows get encrypted in one controlled
pass instead of mixing schema + encryption in the same upgrade.
Crypto audit: 195 Dexie tables classified (94 encrypted, 101
plaintext-allowlisted). Type-check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>