feat(scene-scope): wire filterBySceneScope into notes/todo/contacts/calendar queries

The four modules with tag junctions now filter their main useAll*
queries through filterBySceneScope: when a scene has scopeTagIds set,
only records tagged with at least one matching tag (+ untagged records)
appear in the UI. Modules without tag junctions (drink, food, habits,
journal, dreams, places) are unaffected — their records are always
globally visible.

- useAllNotes → noteTagOps.getTagIds
- useAllTasks → taskTagTable junction lookup
- useAllContacts → contactTagOps.getTagIds
- useAllCalendarItems → eventTagOps.getTagIds (calendar-sourced blocks
  only; task/habit blocks pass through unfiltered)

When no scene scope is active (scopeTagIds undefined), filterBySceneScope
is a no-op identity pass — zero overhead for unscoped scenes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-16 15:22:47 +02:00
parent 62fc566693
commit 26e1c4774f
4 changed files with 23 additions and 4 deletions

View file

@ -13,6 +13,8 @@
import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
import { db } from '$lib/data/database';
import { decryptRecords } from '$lib/data/crypto';
import { filterBySceneScope } from '$lib/stores/scene-scope.svelte';
import { eventTagOps } from './stores/tags.svelte';
import type { LocalCalendar, LocalEvent, Calendar, CalendarEvent } from './types';
import { timeBlockToCalendarEvent } from './types';
import type { LocalTimeBlock } from '$lib/data/time-blocks/types';
@ -65,8 +67,13 @@ export function useAllCalendarItems() {
eventsById.set(e.id, e);
}
// Scene scope filter: only calendar-sourced blocks are tag-filterable.
const scopedBlocks = await filterBySceneScope(decryptedBlocks, async (b) =>
b.sourceModule === 'calendar' && b.sourceId ? eventTagOps.getTagIds(b.sourceId) : []
);
// Convert to CalendarEvent, joining event data for calendar blocks
return decryptedBlocks.map((block) => {
return scopedBlocks.map((block) => {
const tb = toTimeBlock(block);
const eventData =
block.sourceModule === 'calendar' ? (eventsById.get(block.sourceId) ?? null) : null;

View file

@ -5,6 +5,8 @@
import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
import { db } from '$lib/data/database';
import { decryptRecords } from '$lib/data/crypto';
import { filterBySceneScope } from '$lib/stores/scene-scope.svelte';
import { contactTagOps } from './stores/tags.svelte';
import type { LocalContact, Contact, SortField, ContactFilter } from './types';
// ─── Type Converter ───────────────────────────────────────
@ -55,7 +57,8 @@ export function useAllContacts() {
(c) => !c.deletedAt
);
const decrypted = await decryptRecords('contacts', visible);
return decrypted.map(toContact);
const scoped = await filterBySceneScope(decrypted, (c) => contactTagOps.getTagIds(c.id));
return scoped.map(toContact);
}, []);
}

View file

@ -17,6 +17,8 @@
import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
import { db } from '$lib/data/database';
import { decryptRecords } from '$lib/data/crypto';
import { filterBySceneScope } from '$lib/stores/scene-scope.svelte';
import { noteTagOps } from './stores/tags.svelte';
import type { LocalNote, Note } from './types';
// ─── Type Converters ───────────────────────────────────────
@ -49,7 +51,8 @@ export function useAllNotes() {
// Locked vault returns the blobs untouched so the UI can render
// a "🔒" placeholder where title/content would be.
const decrypted = await decryptRecords('notes', visible);
return decrypted.map(toNote).sort((a, b) => {
const scoped = await filterBySceneScope(decrypted, (n) => noteTagOps.getTagIds(n.id));
return scoped.map(toNote).sort((a, b) => {
if (a.isPinned !== b.isPinned) return a.isPinned ? -1 : 1;
return b.updatedAt.localeCompare(a.updatedAt);
});

View file

@ -5,6 +5,8 @@
import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
import { db } from '$lib/data/database';
import { decryptRecords } from '$lib/data/crypto';
import { filterBySceneScope } from '$lib/stores/scene-scope.svelte';
import { taskTagTable } from './collections';
import type {
LocalTask,
LocalBoardView,
@ -46,7 +48,11 @@ export function useAllTasks() {
const locals = await db.table<LocalTask>('tasks').orderBy('order').toArray();
const visible = locals.filter((t) => !t.deletedAt);
const decrypted = await decryptRecords('tasks', visible);
return decrypted.map(toTask);
const scoped = await filterBySceneScope(decrypted, async (t) => {
const links = await taskTagTable.where('taskId').equals(t.id).toArray();
return links.filter((l) => !l.deletedAt).map((l) => l.tagId);
});
return scoped.map(toTask);
}, [] as Task[]);
}