diff --git a/apps/mana/apps/web/src/lib/data/database.ts b/apps/mana/apps/web/src/lib/data/database.ts index 158cbd7b5..f153c9e5b 100644 --- a/apps/mana/apps/web/src/lib/data/database.ts +++ b/apps/mana/apps/web/src/lib/data/database.ts @@ -44,10 +44,15 @@ export { export const db = new Dexie('mana'); -// Single canonical schema. The pre-launch cleanup collapsed historical -// versions 1–10 into this one block — see docs/PRE_LAUNCH_CLEANUP.md for -// rationale. After the system goes live, any further schema change MUST -// be added as a new `db.version(N)` block; never edit this one. +// Schema version 1 — the pre-launch canonical schema. Collapsed from +// historical v1–v10 during cleanup (see docs/PRE_LAUNCH_CLEANUP.md). +// +// IMPORTANT: this block is FROZEN. Any new tables MUST go into a new +// `db.version(N)` block below (currently v2=body, v3=who, v4=news). +// Adding tables here instead of in a new version causes silent schema +// drift: Dexie only runs the upgrade if the version number bumps, so +// existing IndexedDB instances would never see the new tables until +// the user clears storage. db.version(1).stores({ // ─── Sync Infrastructure (local-only, NOT in SYNC_APP_MAP) ─── _pendingChanges: '++id, appId, collection, recordId, createdAt', @@ -216,7 +221,8 @@ db.version(1).stores({ guideTags: 'id, guideId, tagId, [guideId+tagId]', // ─── Playground (appId: 'playground') ─── - // No persistent data — stateless LLM playground + playgroundConversations: 'id, model, isPinned, updatedAt', + playgroundMessages: 'id, conversationId, role, order, [conversationId+order]', // ─── Habits (appId: 'habits') ─── habits: 'id, order, isArchived, color', @@ -350,6 +356,11 @@ db.version(4).stores({ newsCachedFeed: 'id, topic, sourceSlug, language, publishedAt, [topic+publishedAt]', }); +// v5: Zitare custom quotes — user-created quotes stored locally. +db.version(5).stores({ + zitareCustomQuotes: 'id, author, category', +}); + // ─── Sync Routing ────────────────────────────────────────── // SYNC_APP_MAP, TABLE_TO_SYNC_NAME, TABLE_TO_APP, SYNC_NAME_TO_TABLE, // toSyncName() and fromSyncName() are now derived from per-module diff --git a/apps/mana/apps/web/src/lib/data/local-store.ts b/apps/mana/apps/web/src/lib/data/local-store.ts index 5224ed84b..2cee8f121 100644 --- a/apps/mana/apps/web/src/lib/data/local-store.ts +++ b/apps/mana/apps/web/src/lib/data/local-store.ts @@ -13,6 +13,7 @@ import type { WidgetConfig } from '$lib/types/dashboard'; import type { TileNode } from '$lib/types/tiling'; import { db } from './database'; import { guestSettings, guestDashboardConfigs } from './guest-seed.js'; +import { seedAllGuestData } from './seed-registry'; // ─── Types ────────────────────────────────────────────────── @@ -123,6 +124,10 @@ export const manaStore = { if (dashboardCount === 0 && guestDashboardConfigs.length > 0) { await db.table('dashboardConfigs').bulkPut(guestDashboardConfigs); } + + // Seed per-module guest data (habits presets, body exercises, dream + // examples, etc.). Idempotent: only inserts into empty tables. + await seedAllGuestData(); }, // No-ops — sync is handled by the unified sync engine diff --git a/apps/mana/apps/web/src/lib/data/seed-registry.ts b/apps/mana/apps/web/src/lib/data/seed-registry.ts new file mode 100644 index 000000000..bc23fe76b --- /dev/null +++ b/apps/mana/apps/web/src/lib/data/seed-registry.ts @@ -0,0 +1,81 @@ +/** + * Guest Seed Registry — central aggregation point for all module seed data. + * + * Each module defines a `*_GUEST_SEED` constant in its `collections.ts` + * file. This registry imports them all and provides a single + * `seedAllGuestData()` function that the local-store initialization + * path calls on first visit (when IndexedDB is empty). + * + * Adding a new module's seed: import its GUEST_SEED constant and add + * an entry to `MODULE_SEEDS` below. The table names must match the + * Dexie schema in database.ts. + */ + +import { db } from './database'; + +// ─── Module Seed Imports ───────────────────────────────────── +import { HABITS_GUEST_SEED } from '$lib/modules/habits/collections'; +import { BODY_GUEST_SEED } from '$lib/modules/body/collections'; +import { DREAMS_GUEST_SEED } from '$lib/modules/dreams/collections'; +import { MOODLIT_GUEST_SEED } from '$lib/modules/moodlit/collections'; +import { CONTACTS_GUEST_SEED } from '$lib/modules/contacts/collections'; +import { CALENDAR_GUEST_SEED } from '$lib/modules/calendar/collections'; +import { CHAT_GUEST_SEED } from '$lib/modules/chat/collections'; +import { CARDS_GUEST_SEED } from '$lib/modules/cards/collections'; +import { SKILLTREE_GUEST_SEED } from '$lib/modules/skilltree/collections'; +import { TODO_GUEST_SEED } from '$lib/modules/todo/collections'; +import { NOTES_GUEST_SEED } from '$lib/modules/notes/collections'; +import { TIMES_GUEST_SEED } from '$lib/modules/times/collections'; +import { PLANTA_GUEST_SEED } from '$lib/modules/planta/collections'; + +/** + * Flat list of { tableName, rows } entries. Only modules with non-empty + * seed arrays are listed — modules whose GUEST_SEED has only empty + * arrays (e.g. calc, storage, finance) are omitted because there's + * nothing to insert. + */ +const MODULE_SEEDS: { table: string; rows: Record[] }[] = []; + +function register(seed: Record[]>) { + for (const [table, rows] of Object.entries(seed)) { + if (rows.length > 0) { + MODULE_SEEDS.push({ table, rows }); + } + } +} + +// Register all module seeds +register(HABITS_GUEST_SEED); +register(BODY_GUEST_SEED); +register(DREAMS_GUEST_SEED); +register(MOODLIT_GUEST_SEED); +register(CONTACTS_GUEST_SEED); +register(CALENDAR_GUEST_SEED); +register(CHAT_GUEST_SEED); +register(CARDS_GUEST_SEED); +register(SKILLTREE_GUEST_SEED); +register(TODO_GUEST_SEED); +register(NOTES_GUEST_SEED); +register(TIMES_GUEST_SEED); +register(PLANTA_GUEST_SEED); + +/** + * Seed all module guest data into empty tables. Idempotent: tables + * that already have rows are skipped. Called once during + * `manaStore.initialize()`. + */ +export async function seedAllGuestData(): Promise { + for (const { table, rows } of MODULE_SEEDS) { + try { + const count = await db.table(table).count(); + if (count === 0) { + await db.table(table).bulkPut(rows); + } + } catch (err) { + // Non-fatal: seed failure shouldn't block app startup. + // The table might not exist yet (schema drift) or the DB + // might be in a read-only state (quota exceeded). + console.debug(`[seed-registry] failed to seed ${table}:`, err); + } + } +} diff --git a/apps/mana/apps/web/src/lib/modules/calc/queries.ts b/apps/mana/apps/web/src/lib/modules/calc/queries.ts index b8d8111ad..83f2bd89d 100644 --- a/apps/mana/apps/web/src/lib/modules/calc/queries.ts +++ b/apps/mana/apps/web/src/lib/modules/calc/queries.ts @@ -2,7 +2,7 @@ * Reactive queries for Calc — uses Dexie liveQuery on the unified DB. */ -import { liveQuery } from 'dexie'; +import { useLiveQueryWithDefault } from '@mana/local-store/svelte'; import { db } from '$lib/data/database'; import type { LocalCalculation, LocalSavedFormula } from './types'; import type { Calculation, SavedFormula } from '@calc/shared'; @@ -38,19 +38,19 @@ export function toSavedFormula(local: LocalSavedFormula): SavedFormula { /** All calculations (history), newest first. */ export function useAllCalculations() { - return liveQuery(async () => { + return useLiveQueryWithDefault(async () => { const locals = await db.table('calculations').toArray(); return locals .filter((c) => !c.deletedAt) .map(toCalculation) .reverse(); - }); + }, []); } /** All saved formulas. */ export function useAllSavedFormulas() { - return liveQuery(async () => { + return useLiveQueryWithDefault(async () => { const locals = await db.table('savedFormulas').toArray(); return locals.filter((f) => !f.deletedAt).map(toSavedFormula); - }); + }, []); } diff --git a/apps/mana/apps/web/src/lib/modules/contacts/queries.ts b/apps/mana/apps/web/src/lib/modules/contacts/queries.ts index 91f82ad4f..e1763d96a 100644 --- a/apps/mana/apps/web/src/lib/modules/contacts/queries.ts +++ b/apps/mana/apps/web/src/lib/modules/contacts/queries.ts @@ -2,7 +2,7 @@ * Reactive queries & pure helpers for Contacts — uses Dexie liveQuery on the unified DB. */ -import { liveQuery } from 'dexie'; +import { useLiveQueryWithDefault } from '@mana/local-store/svelte'; import { db } from '$lib/data/database'; import { decryptRecords } from '$lib/data/crypto'; import type { LocalContact, Contact, SortField, ContactFilter } from './types'; @@ -48,13 +48,13 @@ export function toContact(local: LocalContact): Contact { // ─── Live Queries ────────────────────────────────────────── export function useAllContacts() { - return liveQuery(async () => { + return useLiveQueryWithDefault(async () => { const visible = (await db.table('contacts').toArray()).filter( (c) => !c.deletedAt ); const decrypted = await decryptRecords('contacts', visible); return decrypted.map(toContact); - }); + }, []); } // ─── Display Helpers ────────────────────────────────────── diff --git a/apps/mana/apps/web/src/lib/modules/inventory/queries.ts b/apps/mana/apps/web/src/lib/modules/inventory/queries.ts index c6d869d8b..0974cbca6 100644 --- a/apps/mana/apps/web/src/lib/modules/inventory/queries.ts +++ b/apps/mana/apps/web/src/lib/modules/inventory/queries.ts @@ -4,7 +4,7 @@ * Uses prefixed table names: invCollections, invItems, invLocations, invCategories. */ -import { liveQuery } from 'dexie'; +import { useLiveQueryWithDefault } from '@mana/local-store/svelte'; import { db } from '$lib/data/database'; import { decryptRecords } from '$lib/data/crypto'; import type { LocalCollection, LocalItem, LocalLocation, LocalCategory } from './types'; @@ -160,32 +160,32 @@ export function toCategory(local: LocalCategory): Category { // ─── Live Queries ────────────────────────────────────────── export function useAllCollections() { - return liveQuery(async () => { + return useLiveQueryWithDefault(async () => { const locals = await db.table('invCollections').toArray(); return locals.filter((c) => !c.deletedAt).map(toCollection); - }); + }, []); } export function useAllItems() { - return liveQuery(async () => { + return useLiveQueryWithDefault(async () => { const visible = (await db.table('invItems').toArray()).filter((i) => !i.deletedAt); const decrypted = await decryptRecords('invItems', visible); return decrypted.map(toItem); - }); + }, []); } export function useAllLocations() { - return liveQuery(async () => { + return useLiveQueryWithDefault(async () => { const locals = await db.table('invLocations').toArray(); return locals.filter((l) => !l.deletedAt).map(toLocation); - }); + }, []); } export function useAllCategories() { - return liveQuery(async () => { + return useLiveQueryWithDefault(async () => { const locals = await db.table('invCategories').toArray(); return locals.filter((c) => !c.deletedAt).map(toCategory); - }); + }, []); } // ─── Pure Collection Helpers ────────────────────────────── diff --git a/apps/mana/apps/web/src/lib/modules/inventory/views/DetailView.svelte b/apps/mana/apps/web/src/lib/modules/inventory/views/DetailView.svelte index 7e1330384..481a56460 100644 --- a/apps/mana/apps/web/src/lib/modules/inventory/views/DetailView.svelte +++ b/apps/mana/apps/web/src/lib/modules/inventory/views/DetailView.svelte @@ -3,7 +3,7 @@ Collection details, always editable, auto-save on blur. -->