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