fix(mana/web): commit module-registry + module.config.ts files (build-critical)

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>
This commit is contained in:
Till JS 2026-04-07 19:49:58 +02:00
parent 42bd2a3a04
commit 5d4123d2b0
33 changed files with 526 additions and 1 deletions

View file

@ -25,7 +25,9 @@ RUN pnpm build
WORKDIR /app/apps/mana/apps/web
RUN pnpm exec svelte-kit sync
RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm build
# Build heap was bumped 4096→8192 after the unified app grew past the
# 4 GB ceiling — Vite OOMs while bundling all 32 module chunks otherwise.
RUN NODE_OPTIONS="--max-old-space-size=8192" pnpm build
FROM node:20-alpine AS production
WORKDIR /app/apps/mana/apps/web

View file

@ -0,0 +1,202 @@
/**
* Module Registry Single source of truth for app/table/sync mappings.
*
* Each module declares its sync surface in a `module.config.ts` file. This
* registry aggregates all configs and exposes derived maps that the rest of
* the data layer (database.ts, sync.ts) consumes.
*
* Adding a new module = create one config file. The legacy hardcoded
* SYNC_APP_MAP / TABLE_TO_SYNC_NAME blocks in database.ts are now generated
* from this registry, so a single edit per module replaces edits in three
* different places.
*
* Schema migrations (db.version(N).stores()) intentionally remain in
* database.ts because they are versioned snapshots that must never change
* after release they are not derived from this registry.
*/
// ─── Types ─────────────────────────────────────────────────
/**
* Declares one Dexie table that participates in sync.
*
* `name` is the unified Dexie table name (e.g., `todoProjects`). If the
* backend (mana-sync) expects a different collection name, set `syncName`
* (e.g., `projects`). Tables without a rename omit `syncName`.
*/
export interface SyncTableConfig {
name: string;
syncName?: string;
}
export interface ModuleConfig {
/** Stable app identifier used for sync routing (POST /sync/{appId}). */
appId: string;
/** All Dexie tables owned by this module that should be tracked + synced. */
tables: SyncTableConfig[];
}
// ─── Module Configs ────────────────────────────────────────
//
// Each entry is imported from a module's own `module.config.ts`. Modules
// without a config (purely UI / stateless, e.g. playground) are simply
// absent from this list.
//
// `core` is the home for cross-cutting tables that don't belong to any
// product module (mana settings, global tags, manaLinks, timeBlocks).
import {
manaCoreConfig,
tagsCoreConfig,
linksCoreConfig,
timeblocksCoreConfig,
} from '$lib/modules/core/module.config';
import { todoModuleConfig } from '$lib/modules/todo/module.config';
import { calendarModuleConfig } from '$lib/modules/calendar/module.config';
import { contactsModuleConfig } from '$lib/modules/contacts/module.config';
import { chatModuleConfig } from '$lib/modules/chat/module.config';
import { pictureModuleConfig } from '$lib/modules/picture/module.config';
import { cardsModuleConfig } from '$lib/modules/cards/module.config';
import { zitareModuleConfig } from '$lib/modules/zitare/module.config';
import { musicModuleConfig } from '$lib/modules/music/module.config';
import { storageModuleConfig } from '$lib/modules/storage/module.config';
import { presiModuleConfig } from '$lib/modules/presi/module.config';
import { inventarModuleConfig } from '$lib/modules/inventar/module.config';
import { photosModuleConfig } from '$lib/modules/photos/module.config';
import { skilltreeModuleConfig } from '$lib/modules/skilltree/module.config';
import { citycornersModuleConfig } from '$lib/modules/citycorners/module.config';
import { timesModuleConfig } from '$lib/modules/times/module.config';
import { contextModuleConfig } from '$lib/modules/context/module.config';
import { questionsModuleConfig } from '$lib/modules/questions/module.config';
import { nutriphiModuleConfig } from '$lib/modules/nutriphi/module.config';
import { plantaModuleConfig } from '$lib/modules/planta/module.config';
import { uloadModuleConfig } from '$lib/modules/uload/module.config';
import { calcModuleConfig } from '$lib/modules/calc/module.config';
import { moodlitModuleConfig } from '$lib/modules/moodlit/module.config';
import { memoroModuleConfig } from '$lib/modules/memoro/module.config';
import { guidesModuleConfig } from '$lib/modules/guides/module.config';
import { habitsModuleConfig } from '$lib/modules/habits/module.config';
import { notesModuleConfig } from '$lib/modules/notes/module.config';
import { dreamsModuleConfig } from '$lib/modules/dreams/module.config';
import { cyclesModuleConfig } from '$lib/modules/cycles/module.config';
import { eventsModuleConfig } from '$lib/modules/events/module.config';
import { financeModuleConfig } from '$lib/modules/finance/module.config';
import { placesModuleConfig } from '$lib/modules/places/module.config';
export const MODULE_CONFIGS: readonly ModuleConfig[] = [
manaCoreConfig,
tagsCoreConfig,
linksCoreConfig,
timeblocksCoreConfig,
todoModuleConfig,
calendarModuleConfig,
contactsModuleConfig,
chatModuleConfig,
pictureModuleConfig,
cardsModuleConfig,
zitareModuleConfig,
musicModuleConfig,
storageModuleConfig,
presiModuleConfig,
inventarModuleConfig,
photosModuleConfig,
skilltreeModuleConfig,
citycornersModuleConfig,
timesModuleConfig,
contextModuleConfig,
questionsModuleConfig,
nutriphiModuleConfig,
plantaModuleConfig,
uloadModuleConfig,
calcModuleConfig,
moodlitModuleConfig,
memoroModuleConfig,
guidesModuleConfig,
habitsModuleConfig,
notesModuleConfig,
dreamsModuleConfig,
cyclesModuleConfig,
eventsModuleConfig,
financeModuleConfig,
placesModuleConfig,
];
// ─── Derived Maps ──────────────────────────────────────────
/**
* appId list of unified Dexie table names. Mirrors the legacy
* `SYNC_APP_MAP` shape so existing consumers (sync.ts) need no changes.
*/
export const SYNC_APP_MAP: Record<string, string[]> = (() => {
const map: Record<string, string[]> = {};
for (const mod of MODULE_CONFIGS) {
if (map[mod.appId]) {
throw new Error(
`[module-registry] duplicate appId "${mod.appId}" — two module configs share the same id`
);
}
map[mod.appId] = mod.tables.map((t) => t.name);
}
return map;
})();
/**
* Unified Dexie table name backend sync collection name.
* Only tables that need a rename are present.
*/
export const TABLE_TO_SYNC_NAME: Record<string, string> = (() => {
const map: Record<string, string> = {};
for (const mod of MODULE_CONFIGS) {
for (const t of mod.tables) {
if (t.syncName && t.syncName !== t.name) {
map[t.name] = t.syncName;
}
}
}
return map;
})();
/**
* Reverse map: unified table name owning appId. Built once at startup;
* used by the Dexie hooks to tag pending changes with the correct appId.
*/
export const TABLE_TO_APP: Record<string, string> = (() => {
const map: Record<string, string> = {};
for (const mod of MODULE_CONFIGS) {
for (const t of mod.tables) {
if (map[t.name]) {
throw new Error(
`[module-registry] table "${t.name}" is registered by both "${map[t.name]}" and "${mod.appId}"`
);
}
map[t.name] = mod.appId;
}
}
return map;
})();
/** Get the backend collection name for a unified table. */
export function toSyncName(tableName: string): string {
return TABLE_TO_SYNC_NAME[tableName] ?? tableName;
}
/**
* Per-app reverse map: backend collection name unified table name.
* Used when applying server changes pulled per appId.
*/
export const SYNC_NAME_TO_TABLE: Record<string, Record<string, string>> = (() => {
const out: Record<string, Record<string, string>> = {};
for (const [appId, tables] of Object.entries(SYNC_APP_MAP)) {
const map: Record<string, string> = {};
for (const tableName of tables) {
map[toSyncName(tableName)] = tableName;
}
out[appId] = map;
}
return out;
})();
/** Get the unified table name for a backend collection + appId. */
export function fromSyncName(appId: string, syncCollection: string): string {
return SYNC_NAME_TO_TABLE[appId]?.[syncCollection] ?? syncCollection;
}

View file

@ -0,0 +1,6 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const calcModuleConfig: ModuleConfig = {
appId: 'calc',
tables: [{ name: 'calculations' }, { name: 'savedFormulas' }],
};

View file

@ -0,0 +1,6 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const calendarModuleConfig: ModuleConfig = {
appId: 'calendar',
tables: [{ name: 'calendars' }, { name: 'events' }, { name: 'eventTags' }],
};

View file

@ -0,0 +1,6 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const cardsModuleConfig: ModuleConfig = {
appId: 'cards',
tables: [{ name: 'cardDecks', syncName: 'decks' }, { name: 'cards' }, { name: 'deckTags' }],
};

View file

@ -0,0 +1,11 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const chatModuleConfig: ModuleConfig = {
appId: 'chat',
tables: [
{ name: 'conversations' },
{ name: 'messages' },
{ name: 'chatTemplates', syncName: 'templates' },
{ name: 'conversationTags' },
],
};

View file

@ -0,0 +1,11 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const citycornersModuleConfig: ModuleConfig = {
appId: 'citycorners',
tables: [
{ name: 'cities' },
{ name: 'ccLocations', syncName: 'locations' },
{ name: 'ccFavorites', syncName: 'favorites' },
{ name: 'ccLocationTags' },
],
};

View file

@ -0,0 +1,6 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const contactsModuleConfig: ModuleConfig = {
appId: 'contacts',
tables: [{ name: 'contacts' }, { name: 'contactTags' }],
};

View file

@ -0,0 +1,10 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const contextModuleConfig: ModuleConfig = {
appId: 'context',
tables: [
{ name: 'contextSpaces', syncName: 'spaces' },
{ name: 'documents' },
{ name: 'documentTags' },
],
};

View file

@ -0,0 +1,37 @@
/**
* Core sync configs cross-cutting tables that don't belong to any product
* module. These are split into four sync apps because they each push/pull on
* their own /sync/{appId} channel:
*
* - mana user settings, dashboard config, automations
* - tags globalTags, tagGroups (shared across all modules)
* - links manaLinks (cross-app record links)
* - timeblocks unified time model (events/timeEntries/habits/tasks all
* project into timeBlocks for cross-module scheduling)
*/
import type { ModuleConfig } from '$lib/data/module-registry';
export const manaCoreConfig: ModuleConfig = {
appId: 'mana',
tables: [{ name: 'userSettings' }, { name: 'dashboardConfigs' }, { name: 'automations' }],
};
export const tagsCoreConfig: ModuleConfig = {
appId: 'tags',
tables: [
{ name: 'globalTags', syncName: 'tags' },
// `tagGroups` is the same on both sides — no rename needed.
{ name: 'tagGroups' },
],
};
export const linksCoreConfig: ModuleConfig = {
appId: 'links',
tables: [{ name: 'manaLinks', syncName: 'links' }],
};
export const timeblocksCoreConfig: ModuleConfig = {
appId: 'timeblocks',
tables: [{ name: 'timeBlocks' }, { name: 'timeBlockTags' }],
};

View file

@ -0,0 +1,6 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const cyclesModuleConfig: ModuleConfig = {
appId: 'cycles',
tables: [{ name: 'cycles' }, { name: 'cycleDayLogs' }, { name: 'cycleSymptoms' }],
};

View file

@ -0,0 +1,6 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const dreamsModuleConfig: ModuleConfig = {
appId: 'dreams',
tables: [{ name: 'dreams' }, { name: 'dreamSymbols' }, { name: 'dreamTags' }],
};

View file

@ -0,0 +1,10 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const financeModuleConfig: ModuleConfig = {
appId: 'finance',
tables: [
{ name: 'transactions' },
{ name: 'financeCategories', syncName: 'categories' },
{ name: 'budgets' },
],
};

View file

@ -0,0 +1,13 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const guidesModuleConfig: ModuleConfig = {
appId: 'guides',
tables: [
{ name: 'guides' },
{ name: 'sections' },
{ name: 'steps' },
{ name: 'guideCollections', syncName: 'collections' },
{ name: 'runs' },
{ name: 'guideTags' },
],
};

View file

@ -0,0 +1,6 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const habitsModuleConfig: ModuleConfig = {
appId: 'habits',
tables: [{ name: 'habits' }, { name: 'habitLogs' }],
};

View file

@ -0,0 +1,12 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const inventarModuleConfig: ModuleConfig = {
appId: 'inventar',
tables: [
{ name: 'invCollections', syncName: 'collections' },
{ name: 'invItems', syncName: 'items' },
{ name: 'invLocations', syncName: 'locations' },
{ name: 'invCategories', syncName: 'categories' },
{ name: 'invItemTags' },
],
};

View file

@ -0,0 +1,13 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const memoroModuleConfig: ModuleConfig = {
appId: 'memoro',
tables: [
{ name: 'memos' },
{ name: 'memories' },
{ name: 'memoTags' },
{ name: 'memoroSpaces', syncName: 'spaces' },
{ name: 'spaceMembers' },
{ name: 'memoSpaces' },
],
};

View file

@ -0,0 +1,6 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const moodlitModuleConfig: ModuleConfig = {
appId: 'moodlit',
tables: [{ name: 'moods' }, { name: 'sequences' }, { name: 'moodTags' }],
};

View file

@ -0,0 +1,13 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const musicModuleConfig: ModuleConfig = {
appId: 'music',
tables: [
{ name: 'songs' },
{ name: 'mukkePlaylists', syncName: 'playlists' },
{ name: 'playlistSongs' },
{ name: 'mukkeProjects', syncName: 'projects' },
{ name: 'markers' },
{ name: 'songTags' },
],
};

View file

@ -0,0 +1,6 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const notesModuleConfig: ModuleConfig = {
appId: 'notes',
tables: [{ name: 'notes' }, { name: 'noteTags' }],
};

View file

@ -0,0 +1,11 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const nutriphiModuleConfig: ModuleConfig = {
appId: 'nutriphi',
tables: [
{ name: 'meals' },
{ name: 'goals' },
{ name: 'nutriFavorites', syncName: 'favorites' },
{ name: 'mealTags' },
],
};

View file

@ -0,0 +1,11 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const photosModuleConfig: ModuleConfig = {
appId: 'photos',
tables: [
{ name: 'albums' },
{ name: 'albumItems' },
{ name: 'photoFavorites', syncName: 'favorites' },
{ name: 'photoMediaTags', syncName: 'photoTags' },
],
};

View file

@ -0,0 +1,6 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const pictureModuleConfig: ModuleConfig = {
appId: 'picture',
tables: [{ name: 'images' }, { name: 'boards' }, { name: 'boardItems' }, { name: 'imageTags' }],
};

View file

@ -0,0 +1,6 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const placesModuleConfig: ModuleConfig = {
appId: 'places',
tables: [{ name: 'places' }, { name: 'locationLogs' }, { name: 'placeTags' }],
};

View file

@ -0,0 +1,12 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const plantaModuleConfig: ModuleConfig = {
appId: 'planta',
tables: [
{ name: 'plants' },
{ name: 'plantPhotos' },
{ name: 'wateringSchedules' },
{ name: 'wateringLogs' },
{ name: 'plantTags' },
],
};

View file

@ -0,0 +1,10 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const presiModuleConfig: ModuleConfig = {
appId: 'presi',
tables: [
{ name: 'presiDecks', syncName: 'decks' },
{ name: 'slides' },
{ name: 'presiDeckTags' },
],
};

View file

@ -0,0 +1,11 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const questionsModuleConfig: ModuleConfig = {
appId: 'questions',
tables: [
{ name: 'qCollections', syncName: 'collections' },
{ name: 'questions' },
{ name: 'answers' },
{ name: 'questionTags' },
],
};

View file

@ -0,0 +1,11 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const skilltreeModuleConfig: ModuleConfig = {
appId: 'skilltree',
tables: [
{ name: 'skills' },
{ name: 'activities' },
{ name: 'achievements' },
{ name: 'skillTags' },
],
};

View file

@ -0,0 +1,10 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const storageModuleConfig: ModuleConfig = {
appId: 'storage',
tables: [
{ name: 'files' },
{ name: 'storageFolders', syncName: 'folders' },
{ name: 'fileTags' },
],
};

View file

@ -0,0 +1,16 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const timesModuleConfig: ModuleConfig = {
appId: 'times',
tables: [
{ name: 'timeClients', syncName: 'clients' },
{ name: 'timeProjects', syncName: 'projects' },
{ name: 'timeEntries' },
{ name: 'timeTemplates', syncName: 'templates' },
{ name: 'timeSettings', syncName: 'settings' },
{ name: 'timeAlarms', syncName: 'alarms' },
{ name: 'timeCountdownTimers', syncName: 'countdownTimers' },
{ name: 'timeWorldClocks', syncName: 'worldClocks' },
{ name: 'entryTags' },
],
};

View file

@ -0,0 +1,12 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const todoModuleConfig: ModuleConfig = {
appId: 'todo',
tables: [
{ name: 'tasks' },
{ name: 'todoProjects', syncName: 'projects' },
{ name: 'taskLabels' },
{ name: 'reminders' },
{ name: 'boardViews' },
],
};

View file

@ -0,0 +1,11 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const uloadModuleConfig: ModuleConfig = {
appId: 'uload',
tables: [
{ name: 'links' },
{ name: 'uloadTags', syncName: 'tags' },
{ name: 'uloadFolders', syncName: 'folders' },
{ name: 'linkTags' },
],
};

View file

@ -0,0 +1,10 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const zitareModuleConfig: ModuleConfig = {
appId: 'zitare',
tables: [
{ name: 'zitareFavorites', syncName: 'favorites' },
{ name: 'zitareLists', syncName: 'lists' },
{ name: 'zitareListTags' },
],
};