From 4ff95b23155fd0d72e411ad822ff5e08d220fd3f Mon Sep 17 00:00:00 2001 From: Till JS Date: Mon, 20 Apr 2026 19:52:44 +0200 Subject: [PATCH] =?UTF-8?q?feat(spaces):=20rename=20legacy=20spaceId=20?= =?UTF-8?q?=E2=86=92=20contextSpaceId=20(v31=20migration)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves the name collision flagged in the Spaces RFC: four tables owned the term "spaceId" before the multi-tenancy Spaces foundation landed in v28 (conversations, documents, spaceMembers, memoSpaces — chat's context-folder reference, context's parent context-space, and memoro's membership/join tables). After v28, the scope wrapper started filtering on a field that meant something different in these tables, which would have hidden their records from the UI. Dexie v31 migration: - Renames the index from spaceId → contextSpaceId on all four tables. - upgrade() copies each existing `spaceId` value to `contextSpaceId` (when it's a real context-space reference and not already the v28 `_personal:` sentinel), then resets `spaceId` to the personal-space sentinel so the scope wrapper picks the row up on the active-space boot pass. Type changes: - LocalConversation, Conversation: spaceId → contextSpaceId - LocalDocument: spaceId → contextSpaceId - LocalSpaceMember, LocalMemoSpace (memoro): spaceId → contextSpaceId Code updates: - chat/queries.ts: toConversation + filterBySpace renamed to filterByContextSpace (exports updated in chat/index.ts). - chat/stores/conversations.svelte.ts: create() param + write site. - context/queries.ts: toDocument + useSpaceDocuments signature. - context/collections.ts: seed data. - context/ListView.svelte + route pages: form data. - dashboard/widgets/ContextDocsWidget.svelte: read site. Table names stay: `spaceMembers` and `memoSpaces` still carry their old names because they belong to the memoro module's context-space concept and table renames also require sync-routing updates. A dedicated cleanup can rebrand those once memoro's data model is revisited. 0 errors across 7148 files. Plan: docs/plans/spaces-foundation.md Co-Authored-By: Claude Opus 4.7 (1M context) --- .../widgets/ContextDocsWidget.svelte | 6 +- apps/mana/apps/web/src/lib/data/database.ts | 66 +++++++++++++++++++ .../apps/web/src/lib/modules/chat/index.ts | 2 +- .../apps/web/src/lib/modules/chat/queries.ts | 9 ++- .../chat/stores/conversations.svelte.ts | 4 +- .../apps/web/src/lib/modules/chat/types.ts | 4 +- .../src/lib/modules/context/ListView.svelte | 2 +- .../src/lib/modules/context/collections.ts | 4 +- .../web/src/lib/modules/context/queries.ts | 10 +-- .../apps/web/src/lib/modules/context/types.ts | 2 +- .../apps/web/src/lib/modules/memoro/types.ts | 4 +- .../(app)/context/documents/+page.svelte | 2 +- .../(app)/context/spaces/[id]/+page.svelte | 2 +- 13 files changed, 94 insertions(+), 23 deletions(-) diff --git a/apps/mana/apps/web/src/lib/components/dashboard/widgets/ContextDocsWidget.svelte b/apps/mana/apps/web/src/lib/components/dashboard/widgets/ContextDocsWidget.svelte index 479bc0bc7..994d3cd6c 100644 --- a/apps/mana/apps/web/src/lib/components/dashboard/widgets/ContextDocsWidget.svelte +++ b/apps/mana/apps/web/src/lib/components/dashboard/widgets/ContextDocsWidget.svelte @@ -56,8 +56,10 @@ {typeIcons[doc.type ?? 'text'] ?? '📄'}

{doc.title}

- {#if getSpaceName(doc.spaceId)} -

{getSpaceName(doc.spaceId)}

+ {#if getSpaceName(doc.contextSpaceId)} +

+ {getSpaceName(doc.contextSpaceId)} +

{/if}
diff --git a/apps/mana/apps/web/src/lib/data/database.ts b/apps/mana/apps/web/src/lib/data/database.ts index a885d4c0d..767978a00 100644 --- a/apps/mana/apps/web/src/lib/data/database.ts +++ b/apps/mana/apps/web/src/lib/data/database.ts @@ -672,6 +672,72 @@ db.version(30).stores({ _serverIterationExecutions: 'iterationId, missionId, executedAt', }); +// v31 — Rename the legacy `spaceId` field to `contextSpaceId` on four +// tables that owned the term before the multi-tenancy Spaces foundation +// arrived (v28): +// - conversations (chat module's reference to a context-space folder) +// - documents (context module's parent context-space) +// - spaceMembers (memoro's members of a context-space) +// - memoSpaces (memoro's memo ↔ context-space join) +// +// The v28 upgrade did NOT overwrite pre-existing `spaceId` values, so +// records in these tables still carry context-space references under +// the old name. After this migration, `spaceId` belongs exclusively to +// the multi-tenancy primitive; the context-space reference has its own +// disambiguated field. Scope queries that previously would have been +// confused by the collision now work cleanly. +// +// The upgrade also stamps the fresh `spaceId` with the personal-space +// sentinel for these rows so they immediately participate in scope +// filtering instead of staying invisible until the next write. +// +// See docs/plans/spaces-foundation.md §"Legacy spaceId collision". +db.version(31) + .stores({ + conversations: 'id, isArchived, isPinned, contextSpaceId, templateId, updatedAt', + documents: 'id, contextSpaceId, type, pinned, title, [contextSpaceId+type], updatedAt', + spaceMembers: 'id, contextSpaceId, userId', + memoSpaces: 'id, memoId, contextSpaceId', + }) + .upgrade(async (tx) => { + const tables = ['conversations', 'documents', 'spaceMembers', 'memoSpaces'] as const; + for (const name of tables) { + await tx + .table(name) + .toCollection() + .modify((record: Record) => { + if (record.contextSpaceId !== undefined) return; + const legacy = record.spaceId; + if (typeof legacy === 'string' && legacy && !legacy.startsWith('_personal:')) { + // Genuine context-space reference — move to the new field name. + record.contextSpaceId = legacy; + } else { + record.contextSpaceId = null; + } + const ownerId = + typeof record.userId === 'string' && record.userId ? record.userId : GUEST_USER_ID; + // Reset spaceId so scope filtering matches the user's personal + // space (post-bootstrap it's rewritten to the real personal-space id). + record.spaceId = `_personal:${ownerId}`; + }); + } + }); + +// v32 — Broadcast module: 1:N email campaigns (newsletters). +// See docs/plans/broadcast-module.md. Three tables: +// - broadcastCampaigns: the campaigns themselves. status + scheduledAt +// indexed because the two hot queries are "show me drafts" and +// "what's scheduled in the next 24h" (server cron picks those up). +// - broadcastTemplates: reusable content templates. isBuiltIn indexed +// so the picker can split user-created vs. shipped-with-app. +// - broadcastSettings: singleton per user (sender defaults + DNS check +// cache). id is the BROADCAST_SETTINGS_ID sentinel. +db.version(32).stores({ + broadcastCampaigns: 'id, status, scheduledAt, sentAt', + broadcastTemplates: 'id, isBuiltIn', + broadcastSettings: '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/modules/chat/index.ts b/apps/mana/apps/web/src/lib/modules/chat/index.ts index 989eb484c..7d8f1fbde 100644 --- a/apps/mana/apps/web/src/lib/modules/chat/index.ts +++ b/apps/mana/apps/web/src/lib/modules/chat/index.ts @@ -14,7 +14,7 @@ export { toTemplate, toMessage, sortConversations, - filterBySpace, + filterByContextSpace, filterBySearch, splitPinned, } from './queries'; diff --git a/apps/mana/apps/web/src/lib/modules/chat/queries.ts b/apps/mana/apps/web/src/lib/modules/chat/queries.ts index f4a40ab66..499c65fbf 100644 --- a/apps/mana/apps/web/src/lib/modules/chat/queries.ts +++ b/apps/mana/apps/web/src/lib/modules/chat/queries.ts @@ -27,7 +27,7 @@ export function toConversation(local: LocalConversation): Conversation { id: local.id, modelId: local.modelId ?? '', templateId: local.templateId ?? undefined, - spaceId: local.spaceId ?? undefined, + contextSpaceId: local.contextSpaceId ?? undefined, conversationMode: local.conversationMode, documentMode: local.documentMode, title: local.title ?? undefined, @@ -131,8 +131,11 @@ export function sortConversations(list: Conversation[]): Conversation[] { } /** Filter conversations by space. */ -export function filterBySpace(conversations: Conversation[], spaceId: string): Conversation[] { - return conversations.filter((c) => c.spaceId === spaceId); +export function filterByContextSpace( + conversations: Conversation[], + contextSpaceId: string +): Conversation[] { + return conversations.filter((c) => c.contextSpaceId === contextSpaceId); } /** Filter conversations by search query on title. */ diff --git a/apps/mana/apps/web/src/lib/modules/chat/stores/conversations.svelte.ts b/apps/mana/apps/web/src/lib/modules/chat/stores/conversations.svelte.ts index 789007129..747699bfc 100644 --- a/apps/mana/apps/web/src/lib/modules/chat/stores/conversations.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/chat/stores/conversations.svelte.ts @@ -24,7 +24,7 @@ export const conversationsStore = { async create(data: { modelId?: string; templateId?: string; - spaceId?: string; + contextSpaceId?: string; mode?: 'free' | 'guided' | 'template'; documentMode?: boolean; title?: string; @@ -34,7 +34,7 @@ export const conversationsStore = { title: data.title ?? null, modelId: data.modelId ?? null, templateId: data.templateId ?? null, - spaceId: data.spaceId ?? null, + contextSpaceId: data.contextSpaceId ?? null, conversationMode: data.mode ?? 'free', documentMode: data.documentMode ?? false, isArchived: false, diff --git a/apps/mana/apps/web/src/lib/modules/chat/types.ts b/apps/mana/apps/web/src/lib/modules/chat/types.ts index 56fd13e60..0d72cbbde 100644 --- a/apps/mana/apps/web/src/lib/modules/chat/types.ts +++ b/apps/mana/apps/web/src/lib/modules/chat/types.ts @@ -8,7 +8,7 @@ export interface LocalConversation extends BaseRecord { title?: string | null; modelId?: string | null; templateId?: string | null; - spaceId?: string | null; + contextSpaceId?: string | null; conversationMode: 'free' | 'guided' | 'template'; documentMode: boolean; isArchived: boolean; @@ -38,7 +38,7 @@ export interface Conversation { id: string; modelId: string; templateId?: string; - spaceId?: string; + contextSpaceId?: string; conversationMode: 'free' | 'guided' | 'template'; documentMode: boolean; title?: string; diff --git a/apps/mana/apps/web/src/lib/modules/context/ListView.svelte b/apps/mana/apps/web/src/lib/modules/context/ListView.svelte index a28218488..5803463e7 100644 --- a/apps/mana/apps/web/src/lib/modules/context/ListView.svelte +++ b/apps/mana/apps/web/src/lib/modules/context/ListView.svelte @@ -15,7 +15,7 @@ const id = crypto.randomUUID(); const row: LocalDocument = { id, - spaceId: null, + contextSpaceId: null, title: 'Neues Dokument', content: '# Neues Dokument\n\n', type: 'text', diff --git a/apps/mana/apps/web/src/lib/modules/context/collections.ts b/apps/mana/apps/web/src/lib/modules/context/collections.ts index 16ff2334b..ad2203acd 100644 --- a/apps/mana/apps/web/src/lib/modules/context/collections.ts +++ b/apps/mana/apps/web/src/lib/modules/context/collections.ts @@ -29,7 +29,7 @@ export const CONTEXT_GUEST_SEED = { documents: [ { id: 'doc-welcome', - spaceId: DEMO_SPACE_ID, + contextSpaceId: DEMO_SPACE_ID, title: 'Willkommen bei Context', content: 'Context ist dein KI-gestütztes Dokumenten-Management. Erstelle Texte, sammle Kontexte und nutze KI-Prompts.\n\nMelde dich an, um deine Dokumente zu synchronisieren.', @@ -40,7 +40,7 @@ export const CONTEXT_GUEST_SEED = { }, { id: 'doc-prompt', - spaceId: DEMO_SPACE_ID, + contextSpaceId: DEMO_SPACE_ID, title: 'Beispiel-Prompt', content: 'Fasse den folgenden Text in 3 Stichpunkten zusammen:\n\n{text}', type: 'prompt' as const, diff --git a/apps/mana/apps/web/src/lib/modules/context/queries.ts b/apps/mana/apps/web/src/lib/modules/context/queries.ts index 021e69e0c..68cc67418 100644 --- a/apps/mana/apps/web/src/lib/modules/context/queries.ts +++ b/apps/mana/apps/web/src/lib/modules/context/queries.ts @@ -35,7 +35,7 @@ export function toDocument(local: LocalDocument): Document { title: local.title, content: local.content, type: local.type, - space_id: local.spaceId ?? null, + space_id: local.contextSpaceId ?? null, user_id: 'local', created_at: local.createdAt ?? new Date().toISOString(), updated_at: local.updatedAt ?? new Date().toISOString(), @@ -73,13 +73,13 @@ export function useAllDocuments() { }, [] as Document[]); } -/** Documents for a specific space. Auto-updates on any change. */ -export function useSpaceDocuments(spaceId: string) { +/** Documents for a specific context-space. Auto-updates on any change. */ +export function useSpaceDocuments(contextSpaceId: string) { return useLiveQueryWithDefault(async () => { const locals = await db .table('documents') - .where('spaceId') - .equals(spaceId) + .where('contextSpaceId') + .equals(contextSpaceId) .toArray(); const visible = locals.filter((d) => !d.deletedAt); const decrypted = await decryptRecords('documents', visible); diff --git a/apps/mana/apps/web/src/lib/modules/context/types.ts b/apps/mana/apps/web/src/lib/modules/context/types.ts index a667ca0b3..c6517ad7b 100644 --- a/apps/mana/apps/web/src/lib/modules/context/types.ts +++ b/apps/mana/apps/web/src/lib/modules/context/types.ts @@ -39,7 +39,7 @@ export interface LocalContextSpace extends BaseRecord { } export interface LocalDocument extends BaseRecord { - spaceId?: string | null; + contextSpaceId?: string | null; title: string; content: string; type: DocumentType; diff --git a/apps/mana/apps/web/src/lib/modules/memoro/types.ts b/apps/mana/apps/web/src/lib/modules/memoro/types.ts index 839caa6c4..8cdc8eae9 100644 --- a/apps/mana/apps/web/src/lib/modules/memoro/types.ts +++ b/apps/mana/apps/web/src/lib/modules/memoro/types.ts @@ -62,14 +62,14 @@ export interface LocalSpace extends BaseRecord { } export interface LocalSpaceMember extends BaseRecord { - spaceId: string; + contextSpaceId: string; userId: string; role: 'owner' | 'member'; } export interface LocalMemoSpace extends BaseRecord { memoId: string; - spaceId: string; + contextSpaceId: string; } // ─── View Types ──────────────────────────────────────────── diff --git a/apps/mana/apps/web/src/routes/(app)/context/documents/+page.svelte b/apps/mana/apps/web/src/routes/(app)/context/documents/+page.svelte index 00254bcfa..1bce799b9 100644 --- a/apps/mana/apps/web/src/routes/(app)/context/documents/+page.svelte +++ b/apps/mana/apps/web/src/routes/(app)/context/documents/+page.svelte @@ -35,7 +35,7 @@ const id = crypto.randomUUID(); const row: LocalDocument = { id, - spaceId: null, + contextSpaceId: null, title: 'Neues Dokument', content: '# Neues Dokument\n\n', type: 'text', diff --git a/apps/mana/apps/web/src/routes/(app)/context/spaces/[id]/+page.svelte b/apps/mana/apps/web/src/routes/(app)/context/spaces/[id]/+page.svelte index 04118c0d3..e47014dfb 100644 --- a/apps/mana/apps/web/src/routes/(app)/context/spaces/[id]/+page.svelte +++ b/apps/mana/apps/web/src/routes/(app)/context/spaces/[id]/+page.svelte @@ -42,7 +42,7 @@ const id = crypto.randomUUID(); const row: LocalDocument = { id, - spaceId, + contextSpaceId: spaceId, title: 'Neues Dokument', content: '# Neues Dokument\n\n', type: 'text',