diff --git a/apps/mana/apps/web/src/lib/app-registry/apps.ts b/apps/mana/apps/web/src/lib/app-registry/apps.ts index 7c37dac1c..e822a3f6f 100644 --- a/apps/mana/apps/web/src/lib/app-registry/apps.ts +++ b/apps/mana/apps/web/src/lib/app-registry/apps.ts @@ -66,6 +66,7 @@ import { ChatCircleDots, CreditCard, SquaresFour, + Scroll, } from '@mana/shared-icons'; // ── Apps with entity capabilities ─────────────────────────── @@ -519,6 +520,16 @@ registerApp({ paramKey: 'conversationId', }); +registerApp({ + id: 'kontext', + name: 'Kontext', + color: '#A78B6F', + icon: Scroll, + views: { + list: { load: () => import('$lib/modules/kontext/KontextView.svelte') }, + }, +}); + registerApp({ id: 'context', name: 'Context', diff --git a/apps/mana/apps/web/src/lib/data/crypto/registry.ts b/apps/mana/apps/web/src/lib/data/crypto/registry.ts index 8b33fb596..1b8927b7f 100644 --- a/apps/mana/apps/web/src/lib/data/crypto/registry.ts +++ b/apps/mana/apps/web/src/lib/data/crypto/registry.ts @@ -466,6 +466,11 @@ export const ENCRYPTION_REGISTRY: Record = { // and pattern detection. Settings are structural only. moodEntries: { enabled: true, fields: ['withWhom', 'notes'] }, moodSettings: { enabled: false, fields: [] }, + + // ─── Kontext ───────────────────────────────────────────── + // Singleton markdown document ("Was soll Mana über dich wissen?"). + // Free-form user text — encrypt the content, leave the fixed id plaintext. + kontextDoc: { enabled: true, fields: ['content'] }, }; /** diff --git a/apps/mana/apps/web/src/lib/data/database.ts b/apps/mana/apps/web/src/lib/data/database.ts index 61102540b..9b8a204d5 100644 --- a/apps/mana/apps/web/src/lib/data/database.ts +++ b/apps/mana/apps/web/src/lib/data/database.ts @@ -494,6 +494,12 @@ db.version(16).stores({ _byokKeys: 'id, provider, isDefault, [provider+isDefault]', }); +// v17 — Kontext module: a single user-authored markdown document keyed by +// the fixed id 'singleton'. No indexes beyond the primary key. +db.version(17).stores({ + kontextDoc: 'id', +}); + // ─── 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/module-registry.ts b/apps/mana/apps/web/src/lib/data/module-registry.ts index 054d5266b..17b57d712 100644 --- a/apps/mana/apps/web/src/lib/data/module-registry.ts +++ b/apps/mana/apps/web/src/lib/data/module-registry.ts @@ -95,6 +95,7 @@ import { mailModuleConfig } from '$lib/modules/mail/module.config'; import { meditateModuleConfig } from '$lib/modules/meditate/module.config'; import { sleepModuleConfig } from '$lib/modules/sleep/module.config'; import { moodModuleConfig } from '$lib/modules/mood/module.config'; +import { kontextModuleConfig } from '$lib/modules/kontext/module.config'; export const MODULE_CONFIGS: readonly ModuleConfig[] = [ manaCoreConfig, @@ -145,6 +146,7 @@ export const MODULE_CONFIGS: readonly ModuleConfig[] = [ meditateModuleConfig, sleepModuleConfig, moodModuleConfig, + kontextModuleConfig, ]; // ─── Derived Maps ────────────────────────────────────────── diff --git a/apps/mana/apps/web/src/lib/modules/kontext/KontextView.svelte b/apps/mana/apps/web/src/lib/modules/kontext/KontextView.svelte new file mode 100644 index 000000000..4a7a5e134 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/kontext/KontextView.svelte @@ -0,0 +1,302 @@ + + + + + +
+
+
+ {#if saveState === 'pending'} + Speichert… + {:else if saveState === 'saved'} + Gespeichert + {/if} +
+ +
+ + {#if mode === 'edit'} + + {:else if renderedHtml} + +
{@html renderedHtml}
+ {:else} + + {/if} +
+ + diff --git a/apps/mana/apps/web/src/lib/modules/kontext/collections.ts b/apps/mana/apps/web/src/lib/modules/kontext/collections.ts new file mode 100644 index 000000000..eaa79853b --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/kontext/collections.ts @@ -0,0 +1,8 @@ +/** + * Kontext module — Dexie table accessor for the singleton document. + */ + +import { db } from '$lib/data/database'; +import type { LocalKontextDoc } from './types'; + +export const kontextDocTable = db.table('kontextDoc'); diff --git a/apps/mana/apps/web/src/lib/modules/kontext/index.ts b/apps/mana/apps/web/src/lib/modules/kontext/index.ts new file mode 100644 index 000000000..ab7012155 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/kontext/index.ts @@ -0,0 +1,9 @@ +/** + * Kontext module — barrel exports. + */ + +export { kontextStore } from './stores/kontext.svelte'; +export { useKontextDoc, toKontextDoc } from './queries'; +export { kontextDocTable } from './collections'; +export { KONTEXT_SINGLETON_ID } from './types'; +export type { LocalKontextDoc, KontextDoc } from './types'; diff --git a/apps/mana/apps/web/src/lib/modules/kontext/module.config.ts b/apps/mana/apps/web/src/lib/modules/kontext/module.config.ts new file mode 100644 index 000000000..fd0f5bfa2 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/kontext/module.config.ts @@ -0,0 +1,6 @@ +import type { ModuleConfig } from '$lib/data/module-registry'; + +export const kontextModuleConfig: ModuleConfig = { + appId: 'kontext', + tables: [{ name: 'kontextDoc' }], +}; diff --git a/apps/mana/apps/web/src/lib/modules/kontext/queries.ts b/apps/mana/apps/web/src/lib/modules/kontext/queries.ts new file mode 100644 index 000000000..7fea241dd --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/kontext/queries.ts @@ -0,0 +1,32 @@ +/** + * Kontext module — reactive query for the singleton document. + * + * Content is encrypted at rest. Returns null until first write; the + * view calls kontextStore.ensureDoc() on mount to materialise the row. + */ + +import { useLiveQueryWithDefault } from '@mana/local-store/svelte'; +import { db } from '$lib/data/database'; +import { decryptRecords } from '$lib/data/crypto'; +import { KONTEXT_SINGLETON_ID, type KontextDoc, type LocalKontextDoc } from './types'; + +export function toKontextDoc(local: LocalKontextDoc): KontextDoc { + return { + id: local.id, + content: local.content ?? '', + createdAt: local.createdAt ?? new Date().toISOString(), + updatedAt: local.updatedAt ?? new Date().toISOString(), + }; +} + +export function useKontextDoc() { + return useLiveQueryWithDefault( + async () => { + const local = await db.table('kontextDoc').get(KONTEXT_SINGLETON_ID); + if (!local || local.deletedAt) return null; + const [decrypted] = await decryptRecords('kontextDoc', [local]); + return decrypted ? toKontextDoc(decrypted) : null; + }, + null as KontextDoc | null + ); +} diff --git a/apps/mana/apps/web/src/lib/modules/kontext/stores/kontext.svelte.ts b/apps/mana/apps/web/src/lib/modules/kontext/stores/kontext.svelte.ts new file mode 100644 index 000000000..791506e56 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/kontext/stores/kontext.svelte.ts @@ -0,0 +1,33 @@ +/** + * Kontext Store — singleton markdown document. + * + * `content` is encrypted at rest. The whole module is one row keyed by + * a fixed id so there's no list/detail — just read/write. + */ + +import { kontextDocTable } from '../collections'; +import { encryptRecord } from '$lib/data/crypto'; +import { KONTEXT_SINGLETON_ID, type LocalKontextDoc } from '../types'; + +export const kontextStore = { + async ensureDoc(): Promise { + const existing = await kontextDocTable.get(KONTEXT_SINGLETON_ID); + if (existing) return; + const newLocal: LocalKontextDoc = { + id: KONTEXT_SINGLETON_ID, + content: '', + }; + await encryptRecord('kontextDoc', newLocal); + await kontextDocTable.add(newLocal); + }, + + async setContent(content: string): Promise { + await this.ensureDoc(); + const diff: Partial = { + content, + updatedAt: new Date().toISOString(), + }; + await encryptRecord('kontextDoc', diff); + await kontextDocTable.update(KONTEXT_SINGLETON_ID, diff); + }, +}; diff --git a/apps/mana/apps/web/src/lib/modules/kontext/types.ts b/apps/mana/apps/web/src/lib/modules/kontext/types.ts new file mode 100644 index 000000000..0178ce0cf --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/kontext/types.ts @@ -0,0 +1,19 @@ +/** + * Kontext module types — Singleton markdown document. + */ + +import type { BaseRecord } from '@mana/local-store'; + +export const KONTEXT_SINGLETON_ID = 'singleton' as const; + +export interface LocalKontextDoc extends BaseRecord { + id: typeof KONTEXT_SINGLETON_ID; + content: string; +} + +export interface KontextDoc { + id: string; + content: string; + createdAt: string; + updatedAt: string; +}