diff --git a/src-tauri/src/storage/mod.rs b/src-tauri/src/storage/mod.rs index 4410d88..6923169 100644 --- a/src-tauri/src/storage/mod.rs +++ b/src-tauri/src/storage/mod.rs @@ -167,12 +167,14 @@ pub trait Store: Send + Sync { /// Persist reviewed/edited action items as confirmed tasks (T5.6, /// FR-LLM-3): items with an `id` update that row, items without one /// insert a new row. Drafted (unconfirmed) items from `generate_summary` - /// only ever live in `summary.json` until this is called. + /// only ever live in `summary.json` until this is called. Returns the + /// saved items with `id` populated (Phase 8, T8.6) — new rows don't have + /// one yet at call time, but the caller needs it to key reminder scheduling. async fn save_action_items( &self, id: &MeetingId, items: &[ActionItem], - ) -> Result<(), StoreError>; + ) -> Result, StoreError>; /// Persist imported calendar events + their attendees (T6.1/T6.2, /// FR-CAL-1). Re-importing the same event (matched by `source` + /// `raw_uid`) updates it in place rather than duplicating it. Returns @@ -213,6 +215,12 @@ pub trait Store: Send + Sync { async fn set_tags(&self, meeting_id: &MeetingId, tags: &[String]) -> Result<(), StoreError>; /// All known tag names, sorted, for filter/autocomplete UI. async fn list_tags(&self) -> Result, StoreError>; + /// Action items with an unfired reminder due in the future (Phase 8, + /// T8.6, FR-CAL-5) — a startup reconcile against Windows' scheduled-toast + /// list, since that's the actual source of truth for "already scheduled" + /// (see `reminders` module) and it can be cleared out from under the app + /// (uninstall/reinstall, a different AppUserModelID, etc). + async fn list_pending_reminders(&self) -> Result, StoreError>; /// Startup reconcile: meetings with audio but no finalized transcript (FR-REL-1). async fn recover_scan(&self) -> Result, StoreError>; /// Enforce retention; returns count removed. Skips in-progress meetings @@ -913,42 +921,52 @@ impl Store for SqliteStore { &self, id: &MeetingId, items: &[ActionItem], - ) -> Result<(), StoreError> { + ) -> Result, StoreError> { let now = now_unix(); + let mut saved = Vec::with_capacity(items.len()); for item in items { - match &item.id { + let item_id = match &item.id { Some(existing_id) => { sqlx::query( - "UPDATE action_items SET text = ?, owner = ?, due_at = ?, confirmed = ? + "UPDATE action_items SET text = ?, owner = ?, due_at = ?, confirmed = ?, reminder_set = ? WHERE id = ? AND meeting_id = ?", ) .bind(&item.text) .bind(&item.owner) .bind(item.due_at) .bind(item.confirmed as i64) + .bind(item.reminder_set as i64) .bind(existing_id) .bind(id) .execute(&self.pool) .await?; + existing_id.clone() } None => { + let new_id = uuid::Uuid::new_v4().to_string(); sqlx::query( "INSERT INTO action_items (id, meeting_id, text, owner, due_at, confirmed, reminder_set, created_at) - VALUES (?, ?, ?, ?, ?, ?, 0, ?)", + VALUES (?, ?, ?, ?, ?, ?, ?, ?)", ) - .bind(uuid::Uuid::new_v4().to_string()) + .bind(&new_id) .bind(id) .bind(&item.text) .bind(&item.owner) .bind(item.due_at) .bind(item.confirmed as i64) + .bind(item.reminder_set as i64) .bind(now) .execute(&self.pool) .await?; + new_id } - } + }; + saved.push(ActionItem { + id: Some(item_id), + ..item.clone() + }); } - Ok(()) + Ok(saved) } async fn import_calendar_events(&self, events: Vec) -> Result { @@ -1070,6 +1088,27 @@ impl Store for SqliteStore { .await?) } + async fn list_pending_reminders(&self) -> Result, StoreError> { + let rows = sqlx::query( + "SELECT id, text, owner, due_at, confirmed, reminder_set FROM action_items + WHERE reminder_set = 1 AND due_at IS NOT NULL AND due_at > ?", + ) + .bind(now_unix()) + .fetch_all(&self.pool) + .await?; + Ok(rows + .iter() + .map(|r| ActionItem { + id: Some(r.get("id")), + text: r.get("text"), + owner: r.get("owner"), + due_at: r.get("due_at"), + confirmed: r.get::("confirmed") != 0, + reminder_set: r.get::("reminder_set") != 0, + }) + .collect()) + } + async fn recover_scan(&self) -> Result, StoreError> { let rows = sqlx::query( "SELECT id, audio_path FROM meetings WHERE status IN ('recording', 'transcribing')",