feat(manacore): cross-app IndexedDB readers for dashboard widgets

Replace REST API polling with direct IndexedDB reads for 4 dashboard
widgets: TasksToday, TasksUpcoming, CalendarEvents, ContactsFavorites.

Data is now reactive via Dexie liveQuery — updates instantly when any
app writes to its IndexedDB (sync, other tabs, local edits). No more
30-60s polling intervals or retry logic needed.

New files:
- cross-app-stores.ts: Opens todo/calendar/contacts IndexedDB databases
- cross-app-queries.ts: Reactive queries (useOpenTasks, useUpcomingEvents, etc.)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-30 01:21:32 +02:00
parent 31faa5b994
commit 80ea301ac1
7 changed files with 351 additions and 264 deletions

View file

@ -1,56 +1,25 @@
<script lang="ts">
/**
* CalendarEventsWidget - Upcoming calendar events
* CalendarEventsWidget - Upcoming calendar events (local-first)
*
* Reads directly from Calendar's IndexedDB via cross-app reader.
* Reactive: auto-updates when events change (sync, other tabs).
*/
import { _ } from 'svelte-i18n';
import { calendarService, type CalendarEvent } from '$lib/api/services';
import { useAutoRefresh } from '$lib/utils/autoRefresh';
import WidgetSkeleton from '../WidgetSkeleton.svelte';
import WidgetError from '../WidgetError.svelte';
import { useUpcomingEvents } from '$lib/data/cross-app-queries';
import type { CrossAppEvent } from '$lib/data/cross-app-stores';
import { APP_URLS } from '@manacore/shared-branding';
const isDev = typeof window !== 'undefined' && window.location.hostname === 'localhost';
const calendarUrl = isDev ? APP_URLS.calendar.dev : APP_URLS.calendar.prod;
let state = $state<'loading' | 'success' | 'error'>('loading');
let data = $state<CalendarEvent[]>([]);
let error = $state<string | null>(null);
let retrying = $state(false);
let retryCount = $state(0);
const events = useUpcomingEvents(7);
const MAX_DISPLAY = 5;
async function load() {
if (data.length === 0) state = 'loading';
retrying = true;
const result = await calendarService.getUpcomingEvents(7);
if (result.data) {
data = result.data;
state = 'success';
retryCount = 0;
} else {
if (data.length === 0) {
error = result.error;
state = 'error';
}
const isServiceUnavailable = error?.includes('nicht erreichbar');
if (!isServiceUnavailable && retryCount < 3) {
retryCount++;
setTimeout(load, 5000 * retryCount);
}
}
retrying = false;
}
useAutoRefresh(load, 60000);
function formatEventTime(event: CalendarEvent): string {
const start = new Date(event.startTime);
function formatEventTime(event: CrossAppEvent): string {
const start = new Date(event.startDate);
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
@ -68,7 +37,7 @@
});
}
if (event.isAllDay) {
if (event.allDay) {
return dateStr;
}
@ -76,8 +45,8 @@
return `${dateStr}, ${timeStr}`;
}
const displayedEvents = $derived((data || []).slice(0, MAX_DISPLAY));
const remainingCount = $derived(Math.max(0, (data || []).length - MAX_DISPLAY));
const displayedEvents = $derived((events.value ?? []).slice(0, MAX_DISPLAY));
const remainingCount = $derived(Math.max(0, (events.value ?? []).length - MAX_DISPLAY));
</script>
<div>
@ -86,18 +55,20 @@
<span>🗓️</span>
{$_('dashboard.widgets.calendar.title')}
</h3>
{#if (data || []).length > 0}
{#if (events.value ?? []).length > 0}
<span class="rounded-full bg-primary/10 px-2 py-0.5 text-sm font-medium text-primary">
{(data || []).length}
{(events.value ?? []).length}
</span>
{/if}
</div>
{#if state === 'loading'}
<WidgetSkeleton lines={4} />
{:else if state === 'error'}
<WidgetError {error} onRetry={load} {retrying} />
{:else if (data || []).length === 0}
{#if events.loading}
<div class="space-y-2">
{#each Array(4) as _}
<div class="h-10 animate-pulse rounded bg-surface-hover"></div>
{/each}
</div>
{:else if (events.value ?? []).length === 0}
<div class="py-6 text-center">
<div class="mb-2 text-3xl">📅</div>
<p class="text-sm text-muted-foreground">
@ -106,7 +77,7 @@
</div>
{:else}
<div class="space-y-2">
{#each displayedEvents as event}
{#each displayedEvents as event (event.id)}
<div class="flex items-start gap-3 rounded-lg p-2 transition-colors hover:bg-surface-hover">
<div
class="mt-1 h-3 w-3 flex-shrink-0 rounded-full"

View file

@ -1,59 +1,28 @@
<script lang="ts">
/**
* ContactsFavoritesWidget - Favorite contacts
* ContactsFavoritesWidget - Favorite contacts (local-first)
*
* Reads directly from Contacts' IndexedDB via cross-app reader.
* Reactive: auto-updates when contacts change (sync, other tabs).
*/
import { onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import { contactsService, type Contact } from '$lib/api/services';
import { useFavoriteContacts } from '$lib/data/cross-app-queries';
import type { CrossAppContact } from '$lib/data/cross-app-stores';
import { APP_URLS } from '@manacore/shared-branding';
import WidgetSkeleton from '../WidgetSkeleton.svelte';
import WidgetError from '../WidgetError.svelte';
let state = $state<'loading' | 'success' | 'error'>('loading');
let data = $state<Contact[]>([]);
let error = $state<string | null>(null);
let retrying = $state(false);
let retryCount = $state(0);
const MAX_DISPLAY = 5;
const contacts = useFavoriteContacts(MAX_DISPLAY);
// Determine app URL based on environment
const isDev = typeof window !== 'undefined' && window.location.hostname === 'localhost';
const contactsUrl = isDev ? APP_URLS.contacts.dev : APP_URLS.contacts.prod;
async function load() {
state = 'loading';
retrying = true;
const result = await contactsService.getFavoriteContacts(MAX_DISPLAY);
if (result.data) {
data = result.data;
state = 'success';
retryCount = 0;
} else {
error = result.error;
state = 'error';
// Don't retry if service is unavailable (network error)
const isServiceUnavailable = error?.includes('nicht erreichbar');
if (!isServiceUnavailable && retryCount < 3) {
retryCount++;
setTimeout(load, 5000 * retryCount);
}
}
retrying = false;
function getDisplayName(contact: CrossAppContact): string {
const parts = [contact.firstName, contact.lastName].filter(Boolean);
return parts.length > 0 ? parts.join(' ') : contact.email || 'Unbekannt';
}
onMount(load);
function getDisplayName(contact: Contact): string {
return contactsService.getDisplayName(contact);
}
function getInitials(contact: Contact): string {
function getInitials(contact: CrossAppContact): string {
const name = getDisplayName(contact);
const parts = name.split(' ');
if (parts.length >= 2) {
@ -66,18 +35,20 @@
<div>
<div class="mb-3 flex items-center justify-between">
<h3 class="flex items-center gap-2 text-lg font-semibold">
<span>=e</span>
<span>👥</span>
{$_('dashboard.widgets.contacts.title')}
</h3>
</div>
{#if state === 'loading'}
<WidgetSkeleton lines={4} />
{:else if state === 'error'}
<WidgetError {error} onRetry={load} {retrying} />
{:else if data.length === 0}
{#if contacts.loading}
<div class="space-y-2">
{#each Array(4) as _}
<div class="h-10 animate-pulse rounded bg-surface-hover"></div>
{/each}
</div>
{:else if (contacts.value ?? []).length === 0}
<div class="py-6 text-center">
<div class="mb-2 text-3xl">=<3D></div>
<div class="mb-2 text-3xl">👤</div>
<p class="text-sm text-muted-foreground">
{$_('dashboard.widgets.contacts.empty')}
</p>
@ -92,7 +63,7 @@
</div>
{:else}
<div class="space-y-2">
{#each data as contact}
{#each contacts.value ?? [] as contact (contact.id)}
<a
href="{contactsUrl}/contacts/{contact.id}"
target="_blank"
@ -118,7 +89,7 @@
</p>
{/if}
</div>
<span class="text-amber-500">P</span>
<span class="text-amber-500"></span>
</a>
{/each}
</div>
@ -129,7 +100,7 @@
rel="noopener"
class="mt-3 block text-center text-sm text-primary hover:underline"
>
{$_('dashboard.widgets.contacts.view_all')} <EFBFBD>
{$_('dashboard.widgets.contacts.view_all')}
</a>
{/if}
</div>

View file

@ -1,18 +1,19 @@
<script lang="ts">
/**
* TasksTodayWidget - Today's tasks from Todo app
* TasksTodayWidget - Today's tasks from Todo app (local-first)
*
* Reads directly from Todo's IndexedDB via cross-app reader.
* Reactive: auto-updates when tasks change (sync, other tabs).
*/
import { _ } from 'svelte-i18n';
import { todoService, type Task } from '$lib/api/services';
import { useAutoRefresh } from '$lib/utils/autoRefresh';
import { useOpenTasks } from '$lib/data/cross-app-queries';
import { crossTaskCollection, type CrossAppTask } from '$lib/data/cross-app-stores';
import { APP_URLS } from '@manacore/shared-branding';
import { format, isToday, isTomorrow, isPast } from 'date-fns';
import { de } from 'date-fns/locale';
import WidgetSkeleton from '../WidgetSkeleton.svelte';
import WidgetError from '../WidgetError.svelte';
function formatDueDate(dueDate?: string): string | null {
function formatDueDate(dueDate?: string | null): string | null {
if (!dueDate) return null;
const date = new Date(dueDate);
if (isToday(date)) return 'Heute';
@ -20,17 +21,13 @@
return format(date, 'dd. MMM', { locale: de });
}
function isOverdue(dueDate?: string): boolean {
function isOverdue(dueDate?: string | null): boolean {
if (!dueDate) return false;
const date = new Date(dueDate);
return isPast(date) && !isToday(date);
}
let state = $state<'loading' | 'success' | 'error'>('loading');
let data = $state<Task[]>([]);
let error = $state<string | null>(null);
let retrying = $state(false);
let retryCount = $state(0);
const tasks = useOpenTasks();
const MAX_DISPLAY = 5;
@ -44,71 +41,31 @@
low: '#22c55e',
};
async function load() {
if (data.length === 0) state = 'loading';
retrying = true;
const result = await todoService.getAllOpenTasks();
if (result.data) {
data = result.data;
state = 'success';
retryCount = 0;
} else {
if (data.length === 0) {
error = result.error;
state = 'error';
}
const isServiceUnavailable = error?.includes('nicht erreichbar');
if (!isServiceUnavailable && retryCount < 3) {
retryCount++;
setTimeout(load, 5000 * retryCount);
}
}
retrying = false;
}
useAutoRefresh(load, 30000);
const displayedTasks = $derived((data || []).slice(0, MAX_DISPLAY));
const remainingCount = $derived(Math.max(0, (data || []).length - MAX_DISPLAY));
const completedCount = $derived((data || []).filter((t) => t.isCompleted).length);
const totalCount = $derived((data || []).length);
const displayedTasks = $derived((tasks.value ?? []).slice(0, MAX_DISPLAY));
const remainingCount = $derived(Math.max(0, (tasks.value ?? []).length - MAX_DISPLAY));
const totalCount = $derived((tasks.value ?? []).length);
// Track tasks being toggled (for optimistic UI)
let togglingIds: Set<string> = $state(new Set());
async function handleToggleComplete(e: MouseEvent, task: Task) {
async function handleToggleComplete(e: MouseEvent, task: CrossAppTask) {
e.preventDefault();
e.stopPropagation();
if (togglingIds.has(task.id)) return;
// Optimistic update
togglingIds = new Set([...togglingIds, task.id]);
const wasCompleted = task.isCompleted;
task.isCompleted = !wasCompleted;
const result = wasCompleted
? await todoService.uncompleteTask(task.id)
: await todoService.completeTask(task.id);
if (result.error) {
// Revert on error
task.isCompleted = wasCompleted;
} else if (!wasCompleted) {
// Task completed: remove from list after brief delay
setTimeout(() => {
data = data.filter((t) => t.id !== task.id);
}, 600);
}
// Write directly to IndexedDB — sync engine will push to server
await crossTaskCollection.update(task.id, {
isCompleted: !task.isCompleted,
completedAt: task.isCompleted ? null : new Date().toISOString(),
} as Partial<CrossAppTask>);
togglingIds = new Set([...togglingIds].filter((id) => id !== task.id));
}
function getSubtaskProgress(task: Task): string | null {
function getSubtaskProgress(task: CrossAppTask): string | null {
if (!task.subtasks || task.subtasks.length === 0) return null;
const done = task.subtasks.filter((s) => s.isCompleted).length;
return `${done}/${task.subtasks.length}`;
@ -123,15 +80,17 @@
</h3>
{#if totalCount > 0}
<span class="rounded-full bg-primary/10 px-2.5 py-0.5 text-sm font-medium text-primary">
{completedCount}/{totalCount}
{totalCount}
</span>
{/if}
</div>
{#if state === 'loading'}
<WidgetSkeleton lines={4} />
{:else if state === 'error'}
<WidgetError {error} onRetry={load} {retrying} />
{#if tasks.loading}
<div class="space-y-2">
{#each Array(4) as _}
<div class="h-8 animate-pulse rounded bg-surface-hover"></div>
{/each}
</div>
{:else if totalCount === 0}
<div class="py-6 text-center">
<div class="mb-2 text-3xl">🎉</div>
@ -141,7 +100,7 @@
</div>
{:else}
<div class="space-y-1">
{#each displayedTasks as task}
{#each displayedTasks as task (task.id)}
<a
href={todoUrl}
target="_blank"
@ -198,41 +157,13 @@
</span>
{/if}
</div>
<!-- Meta row: time, subtasks, labels -->
{#if task.dueTime || getSubtaskProgress(task) || (task.labels && task.labels.length > 0)}
{#if task.dueTime || getSubtaskProgress(task)}
<div class="mt-0.5 flex items-center gap-2 text-xs text-muted-foreground">
{#if task.dueTime}
<span>{task.dueTime}</span>
{/if}
{#if getSubtaskProgress(task)}
<span class="flex items-center gap-0.5">
<svg
class="h-3 w-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"
/>
</svg>
{getSubtaskProgress(task)}
</span>
{/if}
{#if task.labels && task.labels.length > 0}
{#each task.labels.slice(0, 2) as label}
<span class="flex items-center gap-1">
<span
class="inline-block h-2 w-2 rounded-full"
style="background-color: {label.color}"
></span>
{label.name}
</span>
{/each}
{#if task.labels.length > 2}
<span>+{task.labels.length - 2}</span>
{/if}
<span>{getSubtaskProgress(task)}</span>
{/if}
</div>
{/if}

View file

@ -1,20 +1,16 @@
<script lang="ts">
/**
* TasksUpcomingWidget - Upcoming tasks for the next 7 days
* TasksUpcomingWidget - Upcoming tasks for the next 7 days (local-first)
*
* Reads directly from Todo's IndexedDB via cross-app reader.
* Reactive: auto-updates when tasks change (sync, other tabs).
*/
import { onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import { todoService, type Task } from '$lib/api/services';
import { useUpcomingTasks } from '$lib/data/cross-app-queries';
import { APP_URLS } from '@manacore/shared-branding';
import WidgetSkeleton from '../WidgetSkeleton.svelte';
import WidgetError from '../WidgetError.svelte';
let state = $state<'loading' | 'success' | 'error'>('loading');
let data = $state<Task[]>([]);
let error = $state<string | null>(null);
let retrying = $state(false);
let retryCount = $state(0);
const tasks = useUpcomingTasks(7);
const MAX_DISPLAY = 5;
@ -28,32 +24,6 @@
low: '#22c55e',
};
async function load() {
state = 'loading';
retrying = true;
const result = await todoService.getUpcomingTasks(7);
if (result.data) {
data = result.data;
state = 'success';
retryCount = 0;
} else {
error = result.error;
state = 'error';
const isServiceUnavailable = error?.includes('nicht erreichbar');
if (!isServiceUnavailable && retryCount < 3) {
retryCount++;
setTimeout(load, 5000 * retryCount);
}
}
retrying = false;
}
onMount(load);
function formatDate(dateStr: string): string {
const date = new Date(dateStr);
const today = new Date();
@ -72,12 +42,12 @@
return date < today;
}
function isToday(dateStr: string): boolean {
function isTodayDate(dateStr: string): boolean {
return new Date(dateStr).toDateString() === new Date().toDateString();
}
const displayedTasks = $derived(data.slice(0, MAX_DISPLAY));
const remainingCount = $derived(Math.max(0, data.length - MAX_DISPLAY));
const displayedTasks = $derived((tasks.value ?? []).slice(0, MAX_DISPLAY));
const remainingCount = $derived(Math.max(0, (tasks.value ?? []).length - MAX_DISPLAY));
</script>
<div>
@ -86,18 +56,20 @@
<span>📅</span>
{$_('dashboard.widgets.tasks_upcoming.title')}
</h3>
{#if data.length > 0}
{#if (tasks.value ?? []).length > 0}
<span class="rounded-full bg-primary/10 px-2.5 py-0.5 text-sm font-medium text-primary">
{data.length}
{(tasks.value ?? []).length}
</span>
{/if}
</div>
{#if state === 'loading'}
<WidgetSkeleton lines={4} />
{:else if state === 'error'}
<WidgetError {error} onRetry={load} {retrying} />
{:else if data.length === 0}
{#if tasks.loading}
<div class="space-y-2">
{#each Array(4) as _}
<div class="h-8 animate-pulse rounded bg-surface-hover"></div>
{/each}
</div>
{:else if (tasks.value ?? []).length === 0}
<div class="py-6 text-center">
<div class="mb-2 text-3xl">📭</div>
<p class="text-sm text-muted-foreground">
@ -106,7 +78,7 @@
</div>
{:else}
<div class="space-y-1">
{#each displayedTasks as task}
{#each displayedTasks as task (task.id)}
<a
href={todoUrl}
target="_blank"
@ -122,20 +94,6 @@
<!-- Content -->
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium">{task.title}</p>
<!-- Meta row: labels -->
{#if task.labels && task.labels.length > 0}
<div class="mt-0.5 flex items-center gap-2 text-xs text-muted-foreground">
{#each task.labels.slice(0, 2) as label}
<span class="flex items-center gap-1">
<span
class="inline-block h-2 w-2 rounded-full"
style="background-color: {label.color}"
></span>
{label.name}
</span>
{/each}
</div>
{/if}
</div>
<!-- Date badge -->
@ -145,7 +103,7 @@
task.dueDate
)
? 'bg-red-500/10 text-red-500'
: isToday(task.dueDate)
: isTodayDate(task.dueDate)
? 'bg-orange-500/10 text-orange-500'
: 'bg-muted text-muted-foreground'}"
>

View file

@ -0,0 +1,114 @@
/**
* Cross-App Reactive Queries
*
* Live queries that read directly from other apps' IndexedDB databases.
* Auto-update when data changes (local writes, sync, other tabs).
* Replaces REST API polling with instant reactive reads.
*/
import { useLiveQueryWithDefault } from '@manacore/local-store/svelte';
import {
crossTaskCollection,
crossEventCollection,
crossContactCollection,
type CrossAppTask,
type CrossAppEvent,
type CrossAppContact,
} from './cross-app-stores';
// ─── Todo Queries ───────────────────────────────────────────
/** All open (incomplete) tasks, sorted by order. */
export function useOpenTasks() {
return useLiveQueryWithDefault(async () => {
const all = await crossTaskCollection.getAll(undefined, {
sortBy: 'order',
sortDirection: 'asc',
});
return all.filter((t) => !t.isCompleted && !t.deletedAt);
}, [] as CrossAppTask[]);
}
/** Tasks due today or overdue. */
export function useTodayTasks() {
return useLiveQueryWithDefault(async () => {
const today = new Date();
today.setHours(0, 0, 0, 0);
const todayStr = today.toISOString().slice(0, 10);
const all = await crossTaskCollection.getAll(undefined, {
sortBy: 'order',
sortDirection: 'asc',
});
return all.filter((t) => {
if (t.isCompleted || t.deletedAt) return false;
if (!t.dueDate) return false;
const due = t.dueDate.slice(0, 10);
return due <= todayStr;
});
}, [] as CrossAppTask[]);
}
/** Tasks upcoming in the next N days. */
export function useUpcomingTasks(days = 7) {
return useLiveQueryWithDefault(async () => {
const today = new Date();
today.setHours(0, 0, 0, 0);
const todayStr = today.toISOString().slice(0, 10);
const future = new Date(today);
future.setDate(future.getDate() + days);
const futureStr = future.toISOString().slice(0, 10);
const all = await crossTaskCollection.getAll(undefined, {
sortBy: 'dueDate',
sortDirection: 'asc',
});
return all.filter((t) => {
if (t.isCompleted || t.deletedAt) return false;
if (!t.dueDate) return false;
const due = t.dueDate.slice(0, 10);
return due > todayStr && due <= futureStr;
});
}, [] as CrossAppTask[]);
}
// ─── Calendar Queries ───────────────────────────────────────
/** Events in the next N days. */
export function useUpcomingEvents(days = 7) {
return useLiveQueryWithDefault(async () => {
const now = new Date();
const future = new Date(now);
future.setDate(future.getDate() + days);
const nowStr = now.toISOString();
const futureStr = future.toISOString();
const all = await crossEventCollection.getAll(undefined, {
sortBy: 'startDate',
sortDirection: 'asc',
});
return all.filter((e) => {
if (e.deletedAt) return false;
return e.startDate >= nowStr && e.startDate <= futureStr;
});
}, [] as CrossAppEvent[]);
}
// ─── Contacts Queries ───────────────────────────────────────
/** Favorite contacts. */
export function useFavoriteContacts(limit = 5) {
return useLiveQueryWithDefault(async () => {
const all = await crossContactCollection.getAll(undefined, {
sortBy: 'firstName',
sortDirection: 'asc',
});
return all.filter((c) => c.isFavorite && !c.isArchived && !c.deletedAt).slice(0, limit);
}, [] as CrossAppContact[]);
}

View file

@ -0,0 +1,134 @@
/**
* Cross-App IndexedDB Readers
*
* Opens other apps' IndexedDB databases for direct read access.
* All apps on the same origin share IndexedDB, so ManaCore can
* read from manacore-todo, manacore-calendar, etc. directly.
*
* Data is reactive via Dexie's liveQuery updates when any app
* writes to the same database (including via sync).
*
* NOTE: These stores are read-only from ManaCore's perspective.
* Writes that need sync should go through the owning app's collections.
*/
import { createLocalStore, type BaseRecord } from '@manacore/local-store';
// ─── Todo Types ─────────────────────────────────────────────
export interface CrossAppTask extends BaseRecord {
title: string;
description?: string;
projectId?: string | null;
priority: 'low' | 'medium' | 'high' | 'urgent';
isCompleted: boolean;
completedAt?: string | null;
dueDate?: string | null;
dueTime?: string | null;
scheduledDate?: string | null;
estimatedDuration?: number | null;
order: number;
subtasks?: { id: string; title: string; isCompleted: boolean; order: number }[] | null;
labels?: { id: string; name: string; color: string }[];
}
export interface CrossAppProject extends BaseRecord {
name: string;
color: string;
icon?: string | null;
order: number;
isArchived: boolean;
isDefault: boolean;
}
// ─── Calendar Types ─────────────────────────────────────────
export interface CrossAppEvent extends BaseRecord {
calendarId: string;
title: string;
description?: string | null;
startDate: string;
endDate: string;
allDay: boolean;
location?: string | null;
recurrenceRule?: string | null;
color?: string | null;
}
export interface CrossAppCalendar extends BaseRecord {
name: string;
color: string;
isDefault: boolean;
isVisible: boolean;
}
// ─── Contacts Types ─────────────────────────────────────────
export interface CrossAppContact extends BaseRecord {
firstName?: string;
lastName?: string;
email?: string;
phone?: string;
company?: string;
jobTitle?: string;
photoUrl?: string;
isFavorite?: boolean;
isArchived?: boolean;
}
// ─── Store Instances ────────────────────────────────────────
// These open existing IndexedDB databases created by other apps.
// No sync config — ManaCore only reads, the owning app handles sync.
export const todoReader = createLocalStore({
appId: 'todo',
collections: [
{
name: 'tasks',
indexes: [
'projectId',
'dueDate',
'isCompleted',
'priority',
'order',
'[isCompleted+order]',
'[projectId+order]',
],
},
{
name: 'projects',
indexes: ['order', 'isArchived'],
},
],
});
export const calendarReader = createLocalStore({
appId: 'calendar',
collections: [
{
name: 'events',
indexes: ['calendarId', 'startDate', 'endDate', 'allDay', '[calendarId+startDate]'],
},
{
name: 'calendars',
indexes: ['isDefault', 'isVisible'],
},
],
});
export const contactsReader = createLocalStore({
appId: 'contacts',
collections: [
{
name: 'contacts',
indexes: ['firstName', 'lastName', 'email', 'company', 'isFavorite', 'isArchived'],
},
],
});
// Typed collection accessors
export const crossTaskCollection = todoReader.collection<CrossAppTask>('tasks');
export const crossProjectCollection = todoReader.collection<CrossAppProject>('projects');
export const crossEventCollection = calendarReader.collection<CrossAppEvent>('events');
export const crossCalendarCollection = calendarReader.collection<CrossAppCalendar>('calendars');
export const crossContactCollection = contactsReader.collection<CrossAppContact>('contacts');

View file

@ -10,6 +10,7 @@
import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui';
import { tagLocalStore, tagMutations, useAllTags } from '$lib/stores/tags.svelte';
import { manacoreStore } from '$lib/data/local-store';
import { todoReader, calendarReader, contactsReader } from '$lib/data/cross-app-stores';
import { dashboardStore } from '$lib/stores/dashboard.svelte';
import {
THEME_DEFINITIONS,
@ -203,7 +204,14 @@
}
// Initialize local-first databases (opens IndexedDB, seeds guest data)
await Promise.all([manacoreStore.initialize(), tagLocalStore.initialize()]);
await Promise.all([
manacoreStore.initialize(),
tagLocalStore.initialize(),
// Cross-app readers (read-only, no sync — owning apps handle sync)
todoReader.initialize(),
calendarReader.initialize(),
contactsReader.initialize(),
]);
// Start syncing to server
const getToken = () => authStore.getValidToken();