managarten/packages/shared-stores/src/reminder-scheduler.ts
Till JS 9a6e51b7a3
Some checks are pending
CD Mac Mini / Detect Changes (push) Waiting to run
CD Mac Mini / Deploy (push) Blocked by required conditions
CI / Detect Changes (push) Waiting to run
CI / Validate (push) Waiting to run
CI / Build mana-search (push) Blocked by required conditions
CI / Build mana-sync (push) Blocked by required conditions
CI / Build mana-api-gateway (push) Blocked by required conditions
CI / Build mana-crawler (push) Blocked by required conditions
Docker Validate / Validate Dockerfiles (push) Waiting to run
Docker Validate / Build calendar-web (push) Blocked by required conditions
Docker Validate / Build quotes-web (push) Blocked by required conditions
Docker Validate / Build todo-backend (push) Blocked by required conditions
Docker Validate / Build todo-web (push) Blocked by required conditions
Docker Validate / Build mana-auth (push) Blocked by required conditions
Docker Validate / Build mana-sync (push) Blocked by required conditions
Docker Validate / Build mana-media (push) Blocked by required conditions
Mirror to Forgejo / Push to Forgejo (push) Waiting to run
chore(mana): plants + who aus unified-App entfernen
Plants → Herbatrium (herbatrium.mana.how, LIVE seit 2026-05-17),
Who → eigenständiger Bun-Stack auf who.mana.how (außerhalb des
managarten-Repos, deployt nativ unter PM2 auf dem Mac Mini).

Gelöscht / abgebaut:
- Module: apps/mana/.../modules/{plants,who} + Routen + Locales +
  routes/api/v1/who Proxy
- Top-Level: apps/plants/
- Backend: apps/api/src/modules/{plants,who} + scripts/generate-who-
  dossiers.ts + RESOURCE_MODULES + app.route()-Mounts
- shared-branding: APP_BRANDING, APP_ICONS, MANA_APPS, PlantsLogo
- credits AI_PLANT_ANALYSIS, shared-utils analytics PlantsEvents,
  spiral-db MANA_APP_INDEX plants
- Cross-Module: PlantWateringWidget, time-blocks/types,
  seed-registry PLANTS_GUEST_SEED, crypto-registry plants +
  plaintext-allowlist plantPhotos/plantTags/wateringLogs/wateringSchedules
- Dashboard: 'plant-watering' Widget, requiredBackend 'plants',
  WIDGET_REGISTRY-Eintrag
- Registries: app-registry/apps.ts + categories + help-content +
  module-registry + splitscreen + hooks.server APP_SUBDOMAINS +
  quick-input registry + data/tools/init
- Infrastruktur: cloudflared plants.mana.how, docker-compose
  CORS_ORIGINS + MinIO plants-storage Bucket, prometheus probe,
  package.json plants:dev Scripts, i18n locales plants+who Strings
- who.mana.how / who-api.mana.how Standalone-Routes BLEIBEN
  (PM2-Container auf Mac Mini, eigenständige Auth/SQLite/LLM-Keys)

Dexie v62 (Nachholung) + v63 Migrations:
- v62: dropped meals, goals, foodFavorites, mealTags, wardrobeGarments,
  wardrobeOutfits + images-Schema-Index ohne wardrobe-FKs + Upgrade-
  Callback strippt wardrobeOutfitId/wardrobeGarmentId aus Image-Rows.
  (Migration war im vorherigen Commit nicht im File gelandet, jetzt
  nachgeholt.)
- v63: dropped plants, plantPhotos, wateringSchedules, wateringLogs,
  plantTags, whoGames, whoMessages.

Test/Doku:
- module-registry.test.ts Snapshot: plants-Eintrag entfernt,
  whoGames/whoMessages aus LEGACY_TABLES (werden jetzt gedroppt)

mana-web svelte-check 0/0, snapshot test 10/10, streaks 5/5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 14:25:45 +02:00

117 lines
2.8 KiB
TypeScript

/**
* Reminder Scheduler
*
* Central polling service that checks all registered reminder sources
* and fires browser notifications for due reminders.
*
* @example
* ```typescript
* import { createReminderScheduler } from '@mana/shared-stores';
* import { todoReminderSource } from '$lib/modules/todo/reminder-source';
*
* const scheduler = createReminderScheduler({
* sources: [todoReminderSource],
* });
*
* // Start polling (typically in +layout.svelte onMount)
* scheduler.start();
*
* // Stop on destroy
* scheduler.stop();
* ```
*/
import { notificationService } from './notifications';
export interface DueReminder {
/** Unique reminder ID */
id: string;
/** Notification title */
title: string;
/** Notification body */
body?: string;
/** Tag for deduplication (same tag replaces previous) */
tag: string;
}
export interface ReminderSource {
/** Source identifier (e.g. 'todo', 'calendar') */
id: string;
/** Returns reminders that are currently due */
checkDue: () => Promise<DueReminder[]>;
/** Mark a reminder as sent (so it won't fire again) */
markSent: (reminderId: string) => Promise<void>;
}
export interface ReminderSchedulerConfig {
/** Check interval in ms (default: 30000 = 30s) */
intervalMs?: number;
/** Registered reminder sources */
sources: ReminderSource[];
/** Override notification service (for testing) */
notifier?: {
hasPermission(): boolean;
send(title: string, options?: { body?: string; tag?: string }): void;
};
}
export interface ReminderScheduler {
/** Start the polling loop */
start(): void;
/** Stop the polling loop */
stop(): void;
/** Manually check all sources now */
checkNow(): Promise<void>;
/** Add a source at runtime */
addSource(source: ReminderSource): void;
}
export function createReminderScheduler(config: ReminderSchedulerConfig): ReminderScheduler {
const intervalMs = config.intervalMs ?? 30_000;
const sources = [...config.sources];
const notifier = config.notifier ?? notificationService;
let timer: ReturnType<typeof setInterval> | null = null;
async function check() {
if (!notifier.hasPermission()) return;
for (const source of sources) {
try {
const due = await source.checkDue();
for (const reminder of due) {
notifier.send(reminder.title, {
body: reminder.body,
tag: reminder.tag,
});
await source.markSent(reminder.id);
}
} catch (e) {
console.error(`[ReminderScheduler] Error checking source "${source.id}":`, e);
}
}
}
return {
start() {
if (timer) return;
// Initial check after short delay (let app settle)
setTimeout(() => check(), 2000);
timer = setInterval(check, intervalMs);
},
stop() {
if (timer) {
clearInterval(timer);
timer = null;
}
},
async checkNow() {
await check();
},
addSource(source: ReminderSource) {
sources.push(source);
},
};
}