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>
svelte-check now completes clean (0 errors, 0 warnings, 0 files with
problems).
- profile/ContextOverview: 11 click-on-div sites made keyboard-
accessible with role="button", tabindex="0", and an onActivate helper
that fires the same handler on Enter/Space. Two <p> wrappers became
<div> since <p> cannot carry role="button" per ARIA.
- profile/ContextInterview: paginate dots got aria-label + aria-current.
- settings/GeneralSection: toggle button got aria-label +
aria-pressed.
- events/RegionPicker: radius label associated with range input via
for/id.
- events/SourceManager: drop unused .source-item.inactive + .inactive-
badge CSS selectors (dead code).
- research-lab/CompareColumn: local `rating` seed from entry.userRating
now uses svelte-ignore comment + $effect sync (intentional seed-only
read, plus prop-update mirror).
- admin/ListView: initialTab prop is deliberately read only at mount;
svelte-ignore comment documents the intent.
- gifts/redeem: drop unused .animate-fade-in CSS selector.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit of every Dexie table in apps/mana/apps/web/src/lib/data/database.ts
+ crypto registry finds no blockers for Phase 2, with two scope
adjustments to fold in:
1. Add agentKontextDocs (v22) to the to-migrate list. Per-agent
context docs reference the aiAgents table; migration order must
be agents first, then backfill agentKontextDocs.spaceId via
parent-agent lookup.
2. The 46 already-space-scoped tables from the Spaces-Foundation
sprint all still carry userId alongside spaceId. To hit the
"no table has both userId and spaceId" invariant, Phase 2
extends from just the 7 newly-migrated tables to a ~53-table
sweep dropping userId everywhere. Mechanically identical per
table, so the extra scope is cheap.
Also confirmed:
- All 19 junction tables have space-scoped parents — no dangling
refs. Safe to migrate parents.
- Actor columns (__lastActor / __fieldActors) stamped everywhere by
the Dexie creating hook — userId can be dropped confidently.
- userContext (v23 profile hub) is distinct from kontextDoc (AI
planner injection). userContext stays user-level; kontextDoc
moves per-Space. No collision.
- 10 user-level singleton tables correctly identified to stay
user-level (userSettings, newsPreferences, meditateSettings, …).
- 10 internal/infra tables (_pendingChanges, _events, _aiDebugLog,
…) get per-table treatment; mostly no spaceId needed.
Phase 2 can proceed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Self-audit of the previous draft surfaced 7 legacy residues that would
have left the rebuild short of the "optimal architecture" bar. Rewrite
the plan with those addressed:
1. Drop userId from data records entirely. Attribution lives in the
Actor system (__lastActor / __fieldActors). userId stays only on
explicitly user-scoped tables.
2. Active scene localStorage key becomes per-Space:
`mana:workbench:activeSceneId:${spaceId}` — switch Space A → scene
X, to B → scene Y, back to A → X restored.
3. New user-level userTagPresets table replaces the "copy from
Personal" checkbox hack. First-class templates for seeding new
Spaces with a named tag set; CRUD in Settings.
4. Encryption decision made in-line: globalTags + tagGroups names
encrypted during migration, not deferred (tag names like
"Therapie" or "Finanzen-privat" can leak personal categorization).
5. kontextDoc moves from user-level singleton to per-Space. AI runner
pulls the active Space's kontextDoc; Shared-Spaces start without
one until the user writes one.
6. Default-agent bootstrap uses SpaceType-aware names (Mana for
personal, Familien-Helfer for family, Team-Assistent for team,
etc.) so users don't end up with "three Mana" in their agent list.
7. Phase 1 explicitly audits every junction table to verify parent
records carry spaceId — no silent user-global references.
Also: an explicit "No legacy residues" section anchors these as
intentional anti-patterns to prevent drift. Success criteria now
includes "no table has both userId AND spaceId" as a testable
invariant.
Timeline grows from 3–4 to 4–5 days; the delta is encryption wiring
+ userTagPresets CRUD + the userId→Actor cleanup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sweep 98 `transition-all` occurrences across 62 files and replace with
targeted Tailwind transition utilities. Motivation:
1. `transition-all` animates every property, including CSS custom-
property-backed colours. On first paint the vars may not have
resolved yet, producing the P5 "white-on-white until first
interaction" rendering bug. The same bug hit food/moodlit ListViews
in the earlier theme migration.
2. Specific transitions also perform better — no layout-property
interpolation overhead.
Codemod scripts/migrate-transition-all.mjs classifies each class
attribute by its sibling classes and picks one of:
- `transition-opacity` — icon fade on group-hover
- `transition-[width]` — progress-bar width anim
- `transition-[transform,colors,box-shadow]` — scaled buttons/cards
- `transition-[border-color,box-shadow]` — card hover:border+shadow
- `transition-colors` — default (card/row hover)
91 / 98 auto-classified, 7 hand-migrated:
- EntryItem → transition-[box-shadow] (ring fade)
- NutritionProgressWidget → transition-[stroke-dashoffset,stroke]
- OnboardingModal → transition-[width,background-color]
- times/reports (3×) → transition-[width] / -[height] (bar anims)
- presi/present → transition-[width,background-color] (dots)
svelte-check clean with 0 errors; validate:all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reader page is now a proper distraction-free reading surface instead
of a padded card inside the (app) layout.
Layout:
- .detail-shell breaks out of the (app) layout's padded + max-width
container via the 100vw + negative-margin-X trick, and additionally
cancels the vertical padding (<main pt-2> + inner py-2) plus the
bottom-chrome reservation. The reader theme therefore paints
edge-to-edge including behind the PillNav. No more island-in-a-sea
look.
- Initial theme (light/sepia/dark) mirrors the global Mana theme at
mount time by checking document.documentElement.classList.dark — so
opening an article from a dark-mode app no longer flashes a white
reader. User can still override per-article via the swatches.
Toolbar unification:
- Old two-bar layout (top: back + typography, bottom: actions) fused
into one floating pill-bar at the bottom. Three groups divided by
vertical rules: nav | typography | actions. flex-wrap handles narrow
screens gracefully.
- position: fixed + bottom: calc(--bottom-chrome-height + 1rem) so the
bar floats above Mana's PillNav without overlap. The CSS var comes
from <main>'s style attribute and cascades even into fixed
descendants.
- backdrop-filter: blur(10px) + theme-specific semi-transparent
background so the bar feels aerial, not docked.
- Custom CSS tooltips on every button (data-tip attribute + ::after
pseudo). Replaces the native `title` attribute which has a ~1s delay
and inherits OS chrome. Tooltip bubble colors adapt to the active
reader theme. aria-label stays for screen-readers.
- Active-state swatches get an outline-ring instead of a background-
swap so the chip color stays visible as a theme-preview.
Spacing:
- meta-bar margin-top: 1.5rem → 4rem — clearer separation between the
viewport edge and the article title.
- ReaderView padding-bottom: 4rem → 14rem — last paragraph no longer
visually attaches to the floating bar when scrolled to the end;
there's a proper "you've reached the end" gap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Supersedes per-space-vs-user-global-tags.md (which recommended defer
under "ship fast" assumptions). Pre-live + unlimited resources changes
the calculus: build the clean architecture now.
Decision: tags, tag-groups, workbench scenes, AI agents, and AI
missions all become Space-scoped. Only identity (user, session,
profile, MK key) and per-device UI prefs stay user-level.
Plan covers 8 phases across ~3–4 days:
1. Audit + schema design
2. Dexie migration (with backfill to user's Personal-Space)
3. Store APIs (implicit via scopedForModule wrapper)
4. Space-switch side-effects (reset active scene, bootstrap defaults)
5. Space-creation seeding (one-shot copy tags from Personal)
6. Backend (mana-sync + Postgres + RLS)
7. Docs + memory updates
8. Delete the old deferred plan
Includes edge cases, success criteria, and reasoning for why β over γ
(two clear levels beat one recursive primitive for user clarity).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Strategic decision doc covering whether the central tag system
(@mana/shared-stores → globalTags) should move from user-global to
per-Space, prompted by integration debt between Spaces (hard tenancy)
and Scene-Scope (tag-based view filter).
Surveyed current state: no spaceId column on globalTags or any of the
19 junction tables, 68 consumer imports, plaintext sync, guest-mode
seed.
Evaluated three options:
- A — status quo (user-global, no migration)
- B — fully per-Space (clean, but loses follow-me-everywhere)
- C — hybrid (nullable spaceId, recommended target if migration)
Recommendation: defer. Stay on A until one of five trigger signals
fires (first shared-Space tagging, user-reported clutter, scope
mismatch bug, >50 tags, or encryption/compliance need). Phase-by-phase
work breakdown included for when we revisit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>