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',