diff --git a/apps/mana/apps/web/src/lib/data/ai/agents/bootstrap.ts b/apps/mana/apps/web/src/lib/data/ai/agents/bootstrap.ts index 8d8d6ccf0..002da78cb 100644 --- a/apps/mana/apps/web/src/lib/data/ai/agents/bootstrap.ts +++ b/apps/mana/apps/web/src/lib/data/ai/agents/bootstrap.ts @@ -27,6 +27,7 @@ import { encryptRecord } from '../../crypto'; import type { Mission } from '../missions/types'; import { MISSIONS_TABLE } from '../missions/types'; import { getActiveSpace } from '../../scope/active-space.svelte'; +import { getEffectiveSpaceId } from '../../scope/scoped-db'; import { DEFAULT_AI_POLICY } from '../policy'; import { getAgent } from './store'; import type { Agent } from './types'; @@ -105,7 +106,7 @@ export async function ensureDefaultAgent(): Promise { const toWrite: Agent = { ...agent }; await encryptRecord(AGENTS_TABLE, toWrite); try { - await db.table(AGENTS_TABLE).add(toWrite); + await db.table(AGENTS_TABLE).add({ ...toWrite, spaceId: getEffectiveSpaceId() }); } catch (err) { // Race: another tab just wrote the same id. Re-fetch and return // that tab's record. diff --git a/apps/mana/apps/web/src/lib/data/ai/agents/kontext.ts b/apps/mana/apps/web/src/lib/data/ai/agents/kontext.ts index 9d6eaf545..5300f95e8 100644 --- a/apps/mana/apps/web/src/lib/data/ai/agents/kontext.ts +++ b/apps/mana/apps/web/src/lib/data/ai/agents/kontext.ts @@ -9,6 +9,7 @@ import { db } from '../../database'; import { encryptRecord, decryptRecords } from '../../crypto'; +import { getEffectiveSpaceId } from '../../scope/scoped-db'; const TABLE = 'agentKontextDocs'; @@ -65,6 +66,6 @@ export async function saveAgentKontext(agentId: string, content: string): Promis content, }; await encryptRecord(TABLE, doc); - await db.table(TABLE).add(doc); + await db.table(TABLE).add({ ...doc, spaceId: getEffectiveSpaceId() }); } } diff --git a/apps/mana/apps/web/src/lib/data/scope/index.ts b/apps/mana/apps/web/src/lib/data/scope/index.ts index 54ed56afb..5634722e6 100644 --- a/apps/mana/apps/web/src/lib/data/scope/index.ts +++ b/apps/mana/apps/web/src/lib/data/scope/index.ts @@ -29,6 +29,7 @@ export { scopedGet, assertModuleAllowed, getInScopeSpaceIds, + getEffectiveSpaceId, ScopeNotReadyError, ModuleNotInSpaceError, } from './scoped-db'; diff --git a/apps/mana/apps/web/src/lib/data/scope/scoped-db.ts b/apps/mana/apps/web/src/lib/data/scope/scoped-db.ts index 5ced28151..f9d6b0b3e 100644 --- a/apps/mana/apps/web/src/lib/data/scope/scoped-db.ts +++ b/apps/mana/apps/web/src/lib/data/scope/scoped-db.ts @@ -66,6 +66,25 @@ export function getInScopeSpaceIds(): string[] { return [sentinel]; } +/** + * The spaceId every new write to a space-scoped table should carry. + * Returns the active Space's id when one is loaded, falling back to the + * personal sentinel `_personal:` for guests / pre-bootstrap + * windows. The sentinel value matches what `reconcileSentinels` rewrites + * to the real personal-space id once `loadActiveSpace` resolves, so no + * row gets stranded. + * + * Module stores call this and stamp `spaceId` on the record explicitly + * before `.add()` / `.put()`. Once Schicht A flips the creating-hook to + * throw on missing spaceId (see docs/plans/workbench-seeding-cleanup.md), + * forgetting this call is a hard error instead of silent corruption. + */ +export function getEffectiveSpaceId(): string { + const active = getActiveSpaceId(); + if (active) return active; + return personalSpaceSentinel(getEffectiveUserId()); +} + /** * Return a Collection that applies the space filter — chainable with any * further `.where()`, `.filter()`, `.toArray()`, `.modify()`. diff --git a/apps/mana/apps/web/src/lib/modules/calc/stores/calculations.svelte.ts b/apps/mana/apps/web/src/lib/modules/calc/stores/calculations.svelte.ts index 60da43def..1643030b4 100644 --- a/apps/mana/apps/web/src/lib/modules/calc/stores/calculations.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/calc/stores/calculations.svelte.ts @@ -4,17 +4,19 @@ import { db } from '$lib/data/database'; import { CalcEvents } from '@mana/shared-utils/analytics'; +import { getEffectiveSpaceId } from '$lib/data/scope'; import type { LocalCalculation } from '../types'; import type { CreateCalculationInput } from '@calc/shared'; export const calculationsStore = { async addCalculation(input: CreateCalculationInput) { - await db.table('calculations').add({ + await db.table('calculations').add({ id: crypto.randomUUID(), mode: input.mode, expression: input.expression, result: input.result, skin: input.skin, + spaceId: getEffectiveSpaceId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }); diff --git a/apps/mana/apps/web/src/lib/modules/calc/stores/saved-formulas.svelte.ts b/apps/mana/apps/web/src/lib/modules/calc/stores/saved-formulas.svelte.ts index 79bae1ed8..940e0e81d 100644 --- a/apps/mana/apps/web/src/lib/modules/calc/stores/saved-formulas.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/calc/stores/saved-formulas.svelte.ts @@ -4,17 +4,19 @@ import { db } from '$lib/data/database'; import { CalcEvents } from '@mana/shared-utils/analytics'; +import { getEffectiveSpaceId } from '$lib/data/scope'; import type { LocalSavedFormula } from '../types'; import type { CreateFormulaInput, UpdateFormulaInput } from '@calc/shared'; export const savedFormulasStore = { async saveFormula(input: CreateFormulaInput) { - await db.table('savedFormulas').add({ + await db.table('savedFormulas').add({ id: crypto.randomUUID(), name: input.name, expression: input.expression, description: input.description ?? null, mode: input.mode, + spaceId: getEffectiveSpaceId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }); diff --git a/apps/mana/apps/web/src/lib/modules/companion/stores/chat.svelte.ts b/apps/mana/apps/web/src/lib/modules/companion/stores/chat.svelte.ts index a1a9f9850..e499a6bfc 100644 --- a/apps/mana/apps/web/src/lib/modules/companion/stores/chat.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/companion/stores/chat.svelte.ts @@ -8,6 +8,7 @@ import { db } from '$lib/data/database'; import { emitDomainEvent } from '$lib/data/events'; +import { getEffectiveSpaceId } from '$lib/data/scope'; import type { LocalConversation, LocalMessage } from '../types'; const CONV_TABLE = 'companionConversations'; @@ -24,7 +25,7 @@ export const chatStore = { createdAt: now, updatedAt: now, }; - await db.table(CONV_TABLE).add(conv); + await db.table(CONV_TABLE).add({ ...conv, spaceId: getEffectiveSpaceId() }); emitDomainEvent('CompanionConversationStarted', 'companion', CONV_TABLE, conv.id, { conversationId: conv.id, title: conv.title, @@ -66,7 +67,7 @@ export const chatStore = { toolResult: extra?.toolResult, createdAt: new Date().toISOString(), }; - await db.table(MSG_TABLE).add(msg); + await db.table(MSG_TABLE).add({ ...msg, spaceId: getEffectiveSpaceId() }); // Touch conversation updatedAt await db.table(CONV_TABLE).update(conversationId, { diff --git a/apps/mana/apps/web/src/lib/modules/events/stores/guests.svelte.ts b/apps/mana/apps/web/src/lib/modules/events/stores/guests.svelte.ts index fce43f15d..f982dbe84 100644 --- a/apps/mana/apps/web/src/lib/modules/events/stores/guests.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/events/stores/guests.svelte.ts @@ -4,6 +4,7 @@ import { db } from '$lib/data/database'; import { encryptRecord } from '$lib/data/crypto'; +import { getEffectiveSpaceId } from '$lib/data/scope'; import type { LocalEventGuest, RsvpStatus } from '../types'; let error = $state(null); @@ -44,7 +45,7 @@ export const eventGuestsStore = { // records stay local-only — they're never pushed to the // public RSVP snapshot, so no decrypt-before-publish here. await encryptRecord('eventGuests', newGuest); - await db.table('eventGuests').add(newGuest); + await db.table('eventGuests').add({ ...newGuest, spaceId: getEffectiveSpaceId() }); return { success: true as const, id }; } catch (e) { error = e instanceof Error ? e.message : 'Failed to add guest'; diff --git a/apps/mana/apps/web/src/lib/modules/events/stores/items.svelte.ts b/apps/mana/apps/web/src/lib/modules/events/stores/items.svelte.ts index 2ce6146b0..2f565f5e9 100644 --- a/apps/mana/apps/web/src/lib/modules/events/stores/items.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/events/stores/items.svelte.ts @@ -4,6 +4,7 @@ */ import { db } from '$lib/data/database'; +import { getEffectiveSpaceId } from '$lib/data/scope'; import type { LocalEventItem } from '../types'; import { eventsStore } from './events.svelte'; @@ -47,7 +48,7 @@ export const eventItemsStore = { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; - await db.table('eventItems').add(newItem); + await db.table('eventItems').add({ ...newItem, spaceId: getEffectiveSpaceId() }); void eventsStore.syncItems(input.eventId); return { success: true as const, id }; } catch (e) { diff --git a/apps/mana/apps/web/src/lib/modules/moodlit/stores/moods.svelte.ts b/apps/mana/apps/web/src/lib/modules/moodlit/stores/moods.svelte.ts index 02458f5c3..d5a9d4d80 100644 --- a/apps/mana/apps/web/src/lib/modules/moodlit/stores/moods.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/moodlit/stores/moods.svelte.ts @@ -5,6 +5,7 @@ import { db } from '$lib/data/database'; import { MoodlitEvents } from '@mana/shared-utils/analytics'; import { createBlock, updateBlock } from '$lib/data/time-blocks/service'; +import { getEffectiveSpaceId } from '$lib/data/scope'; import type { LocalMood } from '../types'; import type { Mood, MoodSettings } from '../types'; @@ -128,12 +129,13 @@ function createMoodsStore() { // IndexedDB mutation methods async createMood(data: { name: string; colors: string[]; animation: string }) { - await db.table('moods').add({ + await db.table('moods').add({ id: crypto.randomUUID(), name: data.name, colors: data.colors, animation: data.animation, isDefault: false, + spaceId: getEffectiveSpaceId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }); diff --git a/apps/mana/apps/web/src/lib/modules/picture/stores/boards.svelte.ts b/apps/mana/apps/web/src/lib/modules/picture/stores/boards.svelte.ts index 365ff25bb..80b7bc779 100644 --- a/apps/mana/apps/web/src/lib/modules/picture/stores/boards.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/picture/stores/boards.svelte.ts @@ -9,7 +9,7 @@ import { db } from '$lib/data/database'; import { encryptRecord, decryptRecord } from '$lib/data/crypto'; import { emitDomainEvent } from '$lib/data/events'; -import { getActiveSpace } from '$lib/data/scope'; +import { getActiveSpace, getEffectiveSpaceId } from '$lib/data/scope'; import { getEffectiveUserId } from '$lib/data/current-user'; import { defaultVisibilityFor, @@ -56,7 +56,7 @@ export const boardsStore = { // mutates `newLocal` in place — UI consumers expect plaintext. const plaintextSnapshot = toBoard({ ...newLocal }); await encryptRecord('boards', newLocal); - await db.table('boards').add(newLocal); + await db.table('boards').add({ ...newLocal, spaceId: getEffectiveSpaceId() }); return { success: true, data: plaintextSnapshot }; } catch (e) { error = e instanceof Error ? e.message : 'Failed to create board'; @@ -170,7 +170,7 @@ export const boardsStore = { updatedAt: now, }; await encryptRecord('boardItems', newItem); - await db.table('boardItems').add(newItem); + await db.table('boardItems').add({ ...newItem, spaceId: getEffectiveSpaceId() }); } return { success: true, data: plaintextSnapshot }; diff --git a/apps/mana/apps/web/src/lib/modules/plants/mutations.ts b/apps/mana/apps/web/src/lib/modules/plants/mutations.ts index 685c5b792..e92311f79 100644 --- a/apps/mana/apps/web/src/lib/modules/plants/mutations.ts +++ b/apps/mana/apps/web/src/lib/modules/plants/mutations.ts @@ -11,6 +11,7 @@ import { PlantsEvents } from '@mana/shared-utils/analytics'; import { encryptRecord, decryptRecord } from '$lib/data/crypto'; import { emitDomainEvent } from '$lib/data/events'; import { createBlock } from '$lib/data/time-blocks/service'; +import { getEffectiveSpaceId } from '$lib/data/scope'; import { uploadPlantPhoto, identifyPlant, type IdentifyResult } from './api'; import type { LocalPlant, @@ -194,6 +195,7 @@ export const wateringMutations = { nextWateringAt: nextDate.toISOString(), reminderEnabled: false, reminderHoursBefore: 0, + spaceId: getEffectiveSpaceId(), createdAt: now, updatedAt: now, }); diff --git a/apps/mana/apps/web/src/lib/modules/questions/stores/answers.svelte.ts b/apps/mana/apps/web/src/lib/modules/questions/stores/answers.svelte.ts index e037eb216..84a21428f 100644 --- a/apps/mana/apps/web/src/lib/modules/questions/stores/answers.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/questions/stores/answers.svelte.ts @@ -22,6 +22,7 @@ import { db } from '$lib/data/database'; import { encryptRecord, decryptRecord } from '$lib/data/crypto'; import { emitDomainEvent } from '$lib/data/events'; +import { getEffectiveSpaceId } from '$lib/data/scope'; import { researchApi, type ResearchEvent, type ResearchSource } from '$lib/api/research'; import type { LocalAnswer, LocalQuestion } from '../types'; @@ -43,6 +44,7 @@ async function createManual(input: CreateManualAnswerInput): Promise { citations: [], rating: null, isAccepted: false, + spaceId: getEffectiveSpaceId(), createdAt: now, updatedAt: now, }; @@ -108,6 +110,7 @@ async function startResearch(opts: StartResearchOptions): Promise { const now = new Date().toISOString(); const id = `custom-${crypto.randomUUID()}`; - await db.table('customQuotes').add({ + await db.table('customQuotes').add({ id, text: input.text, author: input.author, category: input.category ?? null, source: input.source ?? null, year: input.year ?? null, + spaceId: getEffectiveSpaceId(), createdAt: now, updatedAt: now, }); diff --git a/apps/mana/apps/web/src/lib/modules/quotes/stores/favorites.svelte.ts b/apps/mana/apps/web/src/lib/modules/quotes/stores/favorites.svelte.ts index 5f9813e2c..796b55781 100644 --- a/apps/mana/apps/web/src/lib/modules/quotes/stores/favorites.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/quotes/stores/favorites.svelte.ts @@ -5,15 +5,17 @@ */ import { db } from '$lib/data/database'; +import { getEffectiveSpaceId } from '$lib/data/scope'; import type { LocalFavorite } from '../types'; import type { Favorite } from '../queries'; export const favoritesStore = { async add(quoteId: string) { const now = new Date().toISOString(); - await db.table('quotesFavorites').add({ + await db.table('quotesFavorites').add({ id: crypto.randomUUID(), quoteId, + spaceId: getEffectiveSpaceId(), createdAt: now, updatedAt: now, }); diff --git a/apps/mana/apps/web/src/lib/modules/skilltree/stores/achievements.svelte.ts b/apps/mana/apps/web/src/lib/modules/skilltree/stores/achievements.svelte.ts index 21159e78a..94991b27c 100644 --- a/apps/mana/apps/web/src/lib/modules/skilltree/stores/achievements.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/skilltree/stores/achievements.svelte.ts @@ -6,6 +6,7 @@ */ import { db } from '$lib/data/database'; +import { getEffectiveSpaceId } from '$lib/data/scope'; import type { AchievementWithStatus, AchievementUnlockResult, @@ -95,13 +96,14 @@ async function seedIfEmpty() { const active = stored.filter((a) => !a.deletedAt); if (active.length === 0) { for (const def of ACHIEVEMENT_DEFINITIONS) { - await db.table('achievements').add({ + await db.table('achievements').add({ id: def.id, key: def.id, name: def.name, description: def.description, icon: def.icon, unlockedAt: '', + spaceId: getEffectiveSpaceId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }); diff --git a/apps/mana/apps/web/src/lib/modules/skilltree/stores/skills.svelte.ts b/apps/mana/apps/web/src/lib/modules/skilltree/stores/skills.svelte.ts index 8c633aff2..53415d45a 100644 --- a/apps/mana/apps/web/src/lib/modules/skilltree/stores/skills.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/skilltree/stores/skills.svelte.ts @@ -7,6 +7,7 @@ import { db } from '$lib/data/database'; import { emitDomainEvent } from '$lib/data/events'; +import { getEffectiveSpaceId } from '$lib/data/scope'; import type { Skill } from '../types'; import { calculateLevel, createDefaultSkill, createActivity } from '../types'; import type { LocalSkill, LocalActivity } from '../types'; @@ -29,8 +30,9 @@ async function addSkill(data: Partial): Promise { totalXp: skill.totalXp, level: skill.level, }; - await db.table('skills').add({ + await db.table('skills').add({ ...localSkill, + spaceId: getEffectiveSpaceId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), });