diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 13bdb4e..2f0ed78 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -16,6 +16,7 @@ pub mod mcp; pub mod models; pub mod notes; pub mod paths; +pub mod reminders; pub mod storage; pub mod sync; pub mod transcription; @@ -94,9 +95,14 @@ pub fn run() { .build(app)?; app.manage(TrayHandle(tray)); - // Startup recovery + retention pass (FR-REL-1, FR-STORE-2). Spawned so - // it never blocks the window from showing (NFR-PERF-4); nothing here - // repeats on a timer (NFR-RES-1). + // Reminders (Phase 8, T8.6, FR-CAL-5) are Windows-scheduled toasts, not + // an app-side timer — Windows itself is what's "polling", so this stays + // within NFR-RES-1. init() just registers the AppUserModelID. + reminders::init(); + + // Startup recovery + retention + reminder-reconcile pass (FR-REL-1, + // FR-STORE-2, FR-CAL-5). Spawned so it never blocks the window from + // showing (NFR-PERF-4); nothing here repeats on a timer (NFR-RES-1). let store = store_for_setup; tauri::async_runtime::spawn(async move { match store.recover_scan().await { @@ -114,6 +120,20 @@ pub fn run() { if let Err(e) = store.enforce_retention(policy).await { tracing::error!("startup retention enforcement failed: {e}"); } + // Re-schedules anything Windows' own toast schedule lost track of + // (uninstall/reinstall, a changed AppUserModelID, …) — schedule() + // itself is idempotent (cancels any existing entry for the same + // action item id first), so this is safe to run every startup. + match store.list_pending_reminders().await { + Ok(pending) => { + for item in &pending { + if let (Some(id), Some(due_at)) = (&item.id, item.due_at) { + reminders::schedule(id, &item.text, item.owner.as_deref(), due_at); + } + } + } + Err(e) => tracing::error!("startup reminder reconcile failed: {e}"), + } }); Ok(()) })