Removes `updatedAt` from the wire protocol and from every Local-prefixed
record type. Replaced by two orthogonal mechanisms — deriveUpdatedAt()
for read-side public-facing values, _updatedAtIndex shadow for indexed
sorts.
Local-side:
- New `_updatedAtIndex` shadow column. Stamped by the Dexie creating /
updating hook on every write. Stripped from the pending-change payload
so it never travels to mana-sync. Indexed in Dexie v53 on the 22 tables
that previously indexed `updatedAt`.
- `deriveUpdatedAt(record)` in sync.ts returns max(__fieldMeta[*].at) so
the public-facing Task / Note / etc. shape keeps an `updatedAt: string`
property without holding it as data.
- Type-converters across ~60 module/queries.ts and types.ts files now
call `deriveUpdatedAt(local)` instead of reading `local.updatedAt`.
Module-store sweep:
- Regex codemod removed `updatedAt: new Date().toISOString()` /
`: now` / `: now()` / `: nowIso()` stamping from 121 store files
(~382 call sites total). Single-property update calls
(`{ updatedAt: now }`) collapsed to `{}`; touch-only patterns
(writing/drafts, writing/generations) kept the call as a no-op
because the hook now stamps `_updatedAtIndex` automatically on
any Dexie modification.
- Local* interfaces stripped of `updatedAt: string` (43 types.ts files).
Public-facing types (Task, Note, Mission, Agent, …) keep
`updatedAt: string` as a computed read-side property.
- Companion's chat conversation now sorts on a real
`lastMessageAt` data field instead of touching `updatedAt`.
- Session-only stores (times/session-alarms, session-countdown-timers)
stamp `updatedAt: now` directly because they're not in Dexie and
have no field-meta layer to derive from.
Sync engine:
- applyServerChanges sets `_updatedAtIndex` itself when applying
server changes (max of server-field times for updates, recordTime
for inserts) so server-replays land orderable.
- Dropped the legacy `localUpdatedAt` fallback — every record now has
`__fieldMeta`, the per-field at is the canonical source.
- Soft-delete tombstone path stops stamping `updatedAt: serverTime`,
uses `_updatedAtIndex` instead.
Server-side:
- mana-ai iteration-writer no longer emits `updatedAt` in
sync_changes.data; receivers derive it from the field-meta map.
- mana-sync types: no change (the wire format already uses
`field_meta` / `at` from F1).
Out of scope: backend Drizzle schemas (mana-credits, mana-events, …)
keep their `updated_at` columns. Those are pure server-internal — not
part of the sync_changes / __fieldMeta mechanism F3 cleans up.
Tests + checks:
- 0 svelte-check errors over 7652 files.
- 29/29 sync.test.ts (vitest).
- 61 mana-ai bun tests.
- mana-sync go test ./... cached green.
Plan: docs/plans/sync-field-meta-overhaul.md F3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous fix wired Dexie's `_scopeCursor` infra-table as a side-
channel between Svelte $state (active-space + current-user) and Dexie
liveQuery: every scoped query touched the table on read so liveQuery
subscribed to it, every setActiveSpace bumped the table so liveQuery
re-ran. Worked, but smelled — hidden side-effect inside `scopedTable`,
scope state pretending to be a Dexie row, +1 roundtrip per query, and
`current-user.ts` had to dynamic-import Dexie just to pump the bridge.
Replacement: a Svelte 5 `$effect`-based hook that owns scope-tracking
explicitly. The dependency now lives in the reactive layer (which is
where it belongs), not as a side-effect in the data layer.
What changes:
- New `data/scope/use-scoped-live-query.svelte.ts`. The hook reads a
module-level `scopeTick` `$state` counter inside its `$effect`.
Both `onActiveSpaceChanged` (existing) and `onCurrentUserChanged`
(new, added to `current-user.ts`) bump the tick on real changes.
Effect re-fires → previous Dexie subscription unsubscribes → fresh
one created with up-to-date `getInScopeSpaceIds()`. Same return
shape as `useLiveQueryWithDefault` for drop-in migration.
- `current-user.ts` gains an `onCurrentUserChanged` event bus,
symmetric to `active-space.svelte.ts#onActiveSpaceChanged`. Stays
a plain `.ts` (no runes) so it remains a leaf and works in the
test runner without the Svelte preprocessor — the rename to
`.svelte.ts` was tried earlier and reverted because of test
fallout (commit `01e6b9f04`).
- 53 module `queries.ts` files migrated from
`useLiveQueryWithDefault` → `useScopedLiveQuery`. The choice of
hook now documents at the call-site whether the query is
scope-aware. Pure mechanical find-replace — no logic changes.
What goes away:
- `data/scope/cursor.ts` deleted.
- `touchScopeCursor()` calls removed from `scopedTable` /
`scopedAnd` / `scopedGet`. Functions are pure data-layer again,
no implicit reactive subscriptions.
- `bumpScopeCursor()` calls removed from `setActiveSpace` and both
`loadActiveSpace` branches. Setter is pure state-update, no
Dexie write side-effect.
- `current-user.ts` no longer dynamic-imports `scope/cursor` —
`setCurrentUserId` is a clean three-line setter again.
- Dexie v46 drops the `_scopeCursor` table (`stores: { _scopeCursor:
null }`). v45 stays declared so existing browsers' version chain
remains contiguous; the v46 deletion runs once on next open. No
user data lost — the table only ever held a transient bumpedAt
row.
Existing 14 scope regression tests still pass. Type-check + theme-
token validators are clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace raw white-alpha Tailwind utilities across the last 12 module
ListViews that were flagged by validate-theme-tokens: citycorners,
guides, inventory, memoro, picture, plants, playground, presi,
questions, times, uload, who. Also replace semantic color hex/names
(bg-yellow-500/20, bg-green-400, text-blue-400, bg-teal-600, etc.)
with success/warning/error/primary tokens.
Per-deck brand colors in who/ListView (#a855f7 purple/historical,
#ec4899 pink/women, #f59e0b amber/antiquity, #0ea5e9 blue/inventors)
stay as hex — those are domain semantics, not theme intent.
Wire validate:theme-tokens into validate:all so future regressions
fail the local pre-push gate. All 76 module ListViews now pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mass rollout of the scope wrapper to every module that had a simple
db.table('X').toArray() or .orderBy('k').toArray() pattern. The
calendar/todo/notes/contacts pilots stay as the original templates;
this commit adds the rest in one pass so the scope layer is the
universal read path.
Modules migrated (43):
Batch A (health/tracking): body, mood, sleep, period, habits,
dreams, journal, meditate, drink, food
Batch B (content/media): recipes, plants, places, firsts, who,
library, quotes, music, photos, picture,
presi, cards, wishes
Batch C (productivity): events, finance, invoices, times, storage,
uload, inventory, skilltree, citycorners,
guides, questions, quiz
Batch D (AI/tools): chat, context, kontext, memoro, mail,
companion, moodlit, wetter, playground,
calc, stretch
Pattern:
- db.table<T>('n').toArray() → scopedForModule<T,string>('mod','n').toArray()
- db.table<T>('n').orderBy('k').toArray() → same, replacing .toArray()
with .sortBy('k') so the sort
runs in-memory on the scope-
filtered result
Also adds scopedAnd() to the scope barrel — wraps an existing indexed
Collection (e.g. `.where('date').aboveOrEqual(x)`) with the scope filter
via Collection.and(). Lets indexed queries keep their index hit while
still honouring scope. ~27 remaining db.table<>.where() calls will move
to scopedAnd() in a follow-up once the active-space-indexed compound
indexes land.
Visibility filtering (applyVisibility) is opt-in: the calendar/todo/
notes/contacts pilots call it; the mass-migrated modules skip it until
private records actually show up in a shared space. The default
visibility='space' makes it a no-op anyway — worth adding later when
records with visibility='private' exist in practice.
Type-check: 0 errors across 7143 files.
Plan: docs/plans/spaces-foundation.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The credit system was overengineered for the local-first architecture:
- Productivity micro-credits (task/event/contact creation at 0.02 credits) made no sense
since these operations happen locally in IndexedDB with zero server cost and were never enforced
- Guild pool system (6 DB tables, spending limits, membership checks) had no active users
- Gift system had 5 types (simple/personalized/split/first_come/riddle) when 2 suffice
Now credits are only charged for operations that actually cost money: AI API calls and
premium features (sync, exports). This makes the value proposition clear to users.
Changes:
- Remove 8 productivity operations + CreditCategory.PRODUCTIVITY from @mana/credits
- Delete guild pool service, routes, schema (3 files); remove guild refs from 8 backend files
- Simplify gifts to simple + personalized only; remove bcrypt/riddle/portions logic
- Update all frontend pages (credits dashboard, gift create/redeem, public gift page)
- Update shared-hono consumeCredits() to remove creditSource parameter
- Update mana-credits CLAUDE.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Svelte 5 requires {@const} to be a direct child of block elements
({#snippet}, {:else}, {#each}, etc.), not inside plain HTML elements
like <div>. The guides DetailView had it inside <div class="meta">,
which broke the production build.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds list (and detail where available) views for four modules that existed
in MANA_APPS but were missing from the workbench app registry. Creates a
static ListView for guides backed by the existing GUIDES catalog.
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>