Seven module query files were calling raw `liveQuery(async () => ...)`
from dexie and returning the resulting Observable<T>. Consumer code in
the route .svelte files then read `.value` (or `.current`) on those
observables, which doesn't exist on the Dexie type — TypeScript flagged
38 errors and the call sites were silently relying on a runtime
property that only happens to work because the Svelte reactivity layer
re-evaluates the access.
Migration: switch each `useXxx()` hook to wrap with the existing
`useLiveQueryWithDefault` from `@mana/local-store/svelte`. The wrapper
returns `{ value, loading, error }` (with `value` synced to a `$state`
under the hood), so call sites can read `.value` reactively without
casts. Each hook now provides a typed default array so the wrapper
infers the right shape on first render.
Modules migrated:
- chat — useAllConversations, useArchivedConversations,
useAllTemplates, useConversationMessages
- citycorners — useAllCities, useAllLocations, useAllFavorites
- memoro — useAllMemos, useArchivedMemos, useMemoriesByMemo,
useAllMemoTags, useAllSpaces
- nutriphi — useAllMeals, useAllGoals, useAllFavorites
- presi — useAllDecks, useDeckSlides, useDeck
- questions — useAllCollections, useAllQuestions,
useAnswersByQuestion
- skilltree — useAllSkills, useAllActivities, useAllAchievements
Call sites cleaned up:
- chat/[id], memoro/[id]: removed inline `as { value: T[] }` casts
that were the workaround for the broken type
- nutriphi/{,add,goals,history}/+page.svelte: `.current ?? []` →
`.value` (the wrapper guarantees the default array, so the
nullish coalesce was always dead)
- questions/{,[id],new,collections}/+page.svelte: same `.current` →
`.value` migration
Net: -38 type errors, no behavior change. The wrappers continue to
subscribe to the same Dexie liveQuery under the hood; only the
ergonomic surface changed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Every workbench-style module ListView reimplemented the same
liveQuery + filter + scroll-area + empty-state shell. Extract a
shared <BaseListView> in @mana/shared-ui (with toolbar/header/
listHeader/item/empty snippets) and migrate the 17 modules whose
list templates fit the workbench tailwind track.
While here:
- migrate DeckCard onto the existing (previously unused) shared
Card atom from shared-ui/atoms.
- fix a latent type bug in times/ListView: it was reading .date /
.startTime / .isRunning off LocalTimeEntry, which doesn't define
them. Now uses the proper joined TimeEntry via toTimeEntry() like
the rest of the times module.
Modules with their own scoped-CSS layout track (calendar, finance,
contacts, notes, places, todo, photos, habits, automations, dreams,
cycles) and outliers (calc, events, playground, zitare) are left
alone — migrating them would be a visual rewrite, not a structural
shell swap.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
These files have been sitting untracked in working trees on multiple
machines since the unified module-registry refactor. database.ts
imports from $lib/data/module-registry but the file itself was never
git-add'd, so the production build crashes on any clean clone with:
Could not resolve "./module-registry" from "src/lib/data/database.ts"
Discovered today during the first deploy of the Memoro recording
pipeline: pulling onto the Mac Mini (which had its own untracked copies
of these files in a stash) revealed that origin/main has been silently
broken for clean builds. Fixed by committing the canonical versions:
- apps/mana/apps/web/src/lib/data/module-registry.ts
- apps/mana/apps/web/src/lib/data/module-registry.test.ts
- apps/mana/apps/web/src/lib/modules/{31 modules}/module.config.ts
The events module already had its module.config.ts committed in
6a60e22a3 (events Phase 2), so it isn't included here.
Also bumps apps/mana/apps/web/Dockerfile build heap from 4096 → 8192:
the unified app outgrew the 4 GB ceiling somewhere between Sprint 2
and Sprint 3 of the data layer rewrite, and Vite OOMs while bundling
all 32 module chunks. The bump existed locally on multiple boxes but
was never committed; today's deploy hit the OOM and required restoring
the bump from a stash to make the image rebuild succeed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Six modules join the notes pilot (Phase 4) on the encrypted-at-rest path.
Every user-typed text and PII field listed below is now wrapped via
AES-GCM-256 with the per-user master key before any write hits Dexie,
and decrypted on every liveQuery read coming back through the public
queries module.
Tables flipped to enabled:true in the registry
- chat.messages messageText
- chat.conversations title
- chat.chatTemplates name + description + systemPrompt + initialQuestion
- dreams.dreams title + content + transcript + interpretation
+ aiInterpretation + location
- dreams.dreamSymbols meaning (name stays plaintext — used as
indexed lookup key in touchSymbols /
updateSymbol via where('name'))
- memoro.memos title + intro + transcript
- memoro.memories title + content
- contacts.contacts firstName + lastName + email + phone + mobile
+ birthday + street + city + postalCode
+ country + notes + website + linkedin
+ twitter + instagram + github
- cycles.cycles notes
- cycles.cycleDayLogs notes + mood (symptoms stays plaintext —
standardised label array
consumed by symptomsStore.touchSymptoms
via Set diffs in dayLogsStore.logDay)
- finance.transactions description + note (the schema uses
`note` singular,
not `notes` or `merchant`
as my earlier draft had it)
Tables intentionally left disabled
- questions / answers — direct db.table().update() call sites in
DetailView.svelte instead of going through a store. Need a store
extraction first; registry entry stays in place so the flip is a
one-line change once the store exists.
- tasks, events, calendar.events, plants, meals, slides, presiDecks,
cards, links, etc. — fall through to a future Phase 6 once the
chat/dreams/memoro/contacts pilots are validated in real use.
Per-module changes
Each store now follows the same pattern the notes pilot established:
1. Build the LocalRecord with plaintext fields
2. Snapshot it via toX() for the optimistic UI return value
3. await encryptRecord(tableName, record) // mutates in place
4. await table.add(record) // ciphertext lands on disk
For updates the diff is encrypted in place before the update() call
so partial updates only encrypt the modified fields.
The transcribeBlob flows in dreams + memoro decrypt the existing
record first (to read the user-typed `content`), then build a
diff and re-encrypt it. Same for contactsStore.ensureSelfContact
which compares against decrypted-existing values to decide whether
the profile-sync needs an update.
Per-module query changes
Each public liveQuery now filters on plaintext metadata (deletedAt,
isArchived, etc.) FIRST, then runs decryptRecords on the visible
set, then maps to the public type. Cost stays bounded by what the
view actually renders, not the total table size.
cross-app-queries.ts useFavoriteContacts decrypts firstName before
the localeCompare sort.
Test fixes
- aes.test.ts: the "registry returns null for disabled tables"
assertion now picks tasks + events as the disabled examples
(messages + contacts both flipped on in this commit).
- cycles.integration.test.ts:
1. beforeEach installs a fresh MemoryKeyProvider with a real
Web Crypto key so dayLogsStore.logDay can encrypt mood/notes
2. The "no duplicate" upsert test decrypts the raw rows it reads
directly from the table before asserting on the mood field
- module-registry.test.ts (drive-by, unrelated): adds eventItems
to the events appId snapshot to match the parallel module-registry
refactor.
Verified: 20 test files, 262/262 tests passing.
Phase 6 will roll out to the remaining tables (tasks, events, plants,
meals, slides, etc.) and finally light up the settings/security UI
(lock state, manual rotate, recovery code opt-in).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Single source of truth for the active user via data/current-user.ts;
layout pushes authStore.user.id into it on every auth state change.
- Dexie creating-hook auto-stamps userId from getEffectiveUserId(); the
updating-hook strips userId from modifications so records are
effectively user-immutable after creation.
- BaseRecord gains an optional userId so module types inherit it without
per-module declarations. All hardcoded 'guest'/'local' fallbacks in
module type-converters and session timer stores are deleted; the dead
userId field is removed from the public view types where it was
unused (Task, Conversation, Template, Deck, Plant, Contact, etc.).
- New guest-migration.ts: on first authenticated session, walks every
sync-tracked table, deletes guest-owned records and re-adds them so
the creating-hook re-stamps with the real user id and produces fresh
insert pending-changes with the full payload. Stale guest pending-
changes are cleared up-front.
- Drive-by: root onMount now returns its cleanup synchronously; the
previous async form silently dropped the cleanup callback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>