Add calendar commands; replace todo!() stubs with typed errors to avoid panics

This commit is contained in:
iamdoubz
2026-07-01 15:07:25 -05:00
parent 35c2de5da0
commit 6229b58645
+149 -25
View File
@@ -2,10 +2,13 @@
//! Contract: `docs/04-api-contracts.md`. Commands return promptly; long work
//! is spawned and reported via events ("recording://*", "transcript://*", …).
//!
//! Recording/settings commands (Phase 1) are real implementations; everything
//! else is still a typed `todo!()` stub mapped to its roadmap task.
//! Phases 1-5 commands are real implementations; everything for a
//! not-yet-reached phase returns `Err(not_implemented(...))` rather than
//! panicking — `panic = "abort"` in the release profile means any panic
//! reachable from a command handler kills the whole app, not just that call.
use crate::audio::{AudioCapture, WasapiCapture};
use crate::calendar::{CalImport, CalendarSource, PstSource};
use crate::diarization::{Diarizer, SherpaDiarizer};
use crate::error::WaResult;
use crate::hardware::{HardwareDetector, WinHardwareDetector};
@@ -110,6 +113,16 @@ fn now_unix() -> i64 {
.unwrap_or(0)
}
/// A command whose feature isn't built yet (roadmap phase not reached). This
/// is deliberately a returned `Err`, never a `todo!()`/`unimplemented!()`:
/// the release profile sets `panic = "abort"`, so a panic anywhere a command
/// handler can reach would kill the whole app the instant any frontend code
/// path calls it — not just fail that one call (found the hard way: an
/// unconditional `listSyncTargets()` on startup was crashing every launch).
fn not_implemented(feature: &str) -> WaError {
WaError::new("not_implemented", format!("{feature} isn't built yet"))
}
/// Builds a `Diarizer` if both diarization models are installed — a fixed
/// pair of well-known filenames, downloadable/removable via
/// `diarization::models` and `list_diarization_models`/`download_model`/
@@ -661,6 +674,39 @@ pub async fn merge_speakers(
Ok(())
}
/// Name a speaker AND link them to a known `Participant` (T6.5/T6.6,
/// FR-SPK-4) — e.g. picked from the linked calendar event's attendee
/// dropdown rather than typed free-text. The shared `participant_id` is what
/// gives naming continuity across meetings the same person attends. Post-
/// meeting only, same reasoning as `merge_speakers`: the live provisional
/// diarization pass re-clusters from scratch every tick, so a label mapped
/// now could mean someone else by the next tick.
#[tauri::command]
pub async fn map_speaker_to_participant(
app: AppHandle,
state: State<'_, AppState>,
meeting_id: MeetingId,
label: String,
participant_id: String,
) -> WaResult<()> {
let guard = state.session.lock().await;
if guard.as_ref().is_some_and(|s| s.meeting_id == meeting_id) {
return Err(WaError::new(
"recording",
"cannot map a speaker to a participant while this meeting is still recording — wait until it's stopped",
));
}
drop(guard);
state
.store
.map_speaker_to_participant(&meeting_id, &label, &participant_id)
.await
.map_err(|e| WaError::new("storage", e.to_string()))?;
refresh_notes_and_notify(&app, &state, &meeting_id).await?;
Ok(())
}
// ---- Hardware + models (Phase 3) ----
#[tauri::command]
@@ -1253,9 +1299,86 @@ pub async fn confirm_action_items(
// ---- Calendar / .pst (Phase 6) ----
/// Import events + attendees from a `.pst` (T6.1/T6.2, FR-CAL-1). The
/// `readpst` subprocess + file parsing is CPU/IO-bound and blocking, so it
/// runs off the async runtime thread. `readpst` doesn't expose granular
/// progress, so `pst://progress` is a start/done signal rather than
/// per-item — real per-item progress would mean reimplementing readpst's
/// internals, not worth it for a one-shot import.
#[tauri::command]
pub async fn import_pst(_path: String, _password: Option<String>) -> WaResult<u32> {
todo!("Phase 6 — import_pst")
pub async fn import_pst(
app: AppHandle,
state: State<'_, AppState>,
path: String,
password: Option<String>,
) -> WaResult<u32> {
let events = tauri::async_runtime::spawn_blocking(move || {
PstSource.import(CalImport { path, password })
})
.await
.map_err(|e| WaError::new("calendar", e.to_string()))?
.map_err(|e| WaError::new("calendar", e.to_string()))?;
let total = events.len() as u32;
let _ = app.emit(
"pst://progress",
serde_json::json!({ "processed": 0, "total": total }),
);
let imported = state
.store
.import_calendar_events(events)
.await
.map_err(|e| WaError::new("storage", e.to_string()))?;
let _ = app.emit(
"pst://progress",
serde_json::json!({ "processed": imported, "total": total }),
);
Ok(imported)
}
/// Browse imported calendar events (T6.3, FR-CAL-2).
#[tauri::command]
pub async fn list_calendar_events(
state: State<'_, AppState>,
from: Option<i64>,
to: Option<i64>,
) -> WaResult<Vec<CalendarEvent>> {
state
.store
.list_calendar_events(from, to)
.await
.map_err(|e| WaError::new("storage", e.to_string()))
}
/// A single event with its attendees (T6.3/T6.4, FR-CAL-1/3) — backs the
/// pre-meeting context panel and the speaker-naming attendee dropdown (T6.5).
#[tauri::command]
pub async fn get_calendar_event(
state: State<'_, AppState>,
event_id: String,
) -> WaResult<crate::storage::CalendarEventDetail> {
state
.store
.get_calendar_event(&event_id)
.await
.map_err(|e| WaError::new("storage", e.to_string()))
}
/// Link a meeting (current or historical) to a calendar event (T6.3/T6.6,
/// FR-CAL-2/4).
#[tauri::command]
pub async fn attach_meeting_to_event(
state: State<'_, AppState>,
meeting_id: MeetingId,
event_id: String,
) -> WaResult<()> {
state
.store
.attach_meeting_to_event(&meeting_id, &event_id)
.await
.map_err(|e| WaError::new("storage", e.to_string()))
}
// ---- Sync / upload (Phase 9, ADR-0010) ----
@@ -1263,56 +1386,56 @@ pub async fn import_pst(_path: String, _password: Option<String>) -> WaResult<u3
#[tauri::command]
pub async fn list_sync_targets() -> WaResult<Vec<SyncTargetInfo>> {
// Never returns secrets (FR-SYNC-6).
todo!("Phase 9 — list_sync_targets")
Err(not_implemented("list_sync_targets"))
}
#[tauri::command]
pub async fn add_sync_target(_config: serde_json::Value) -> WaResult<SyncTargetInfo> {
// `config` includes a `secret` stored to the OS credential store, not the DB.
todo!("Phase 9 — add_sync_target")
Err(not_implemented("add_sync_target"))
}
#[tauri::command]
pub async fn update_sync_target(_config: serde_json::Value) -> WaResult<SyncTargetInfo> {
// `config` carries an `id` and optional fields; an optional `secret` updates the credential store.
todo!("Phase 9 — update_sync_target")
Err(not_implemented("update_sync_target"))
}
#[tauri::command]
pub async fn remove_sync_target(_id: String) -> WaResult<()> {
todo!("Phase 9 — remove_sync_target")
Err(not_implemented("remove_sync_target"))
}
/// Begin OAuth 2.0 PKCE linking for a secondary target (loopback redirect). FR-SYNC-9.
#[tauri::command]
pub async fn begin_oauth_link(_kind: String) -> WaResult<serde_json::Value> {
todo!("Phase 9b — begin_oauth_link (onedrive|dropbox|box)")
Err(not_implemented("begin_oauth_link"))
}
#[tauri::command]
pub async fn retry_sync_job(_job_id: String) -> WaResult<()> {
todo!("Phase 9 — retry_sync_job")
Err(not_implemented("retry_sync_job"))
}
#[tauri::command]
pub async fn test_sync_target(_config_or_id: serde_json::Value) -> WaResult<serde_json::Value> {
todo!("Phase 9 — test_sync_target (reachability + auth)")
Err(not_implemented("test_sync_target"))
}
#[tauri::command]
pub async fn set_sync_enabled(_enabled: bool) -> WaResult<()> {
todo!("Phase 9 — set_sync_enabled (master switch)")
Err(not_implemented("set_sync_enabled"))
}
#[tauri::command]
pub async fn sync_meeting(_meeting_id: MeetingId, _target_id: Option<String>) -> WaResult<()> {
// Manual "Upload now"; streams progress via "sync://job" events.
todo!("Phase 9 — sync_meeting")
Err(not_implemented("sync_meeting"))
}
#[tauri::command]
pub async fn sync_status(_meeting_id: Option<MeetingId>) -> WaResult<Vec<SyncJobInfo>> {
todo!("Phase 9 — sync_status")
Err(not_implemented("sync_status"))
}
// ---- Feature briefs + MCP server (Phase 10b, ADR-0011) ----
@@ -1323,30 +1446,30 @@ pub async fn create_feature_brief(
_target_repo: Option<String>,
) -> WaResult<FeatureBrief> {
// T10.6: distill transcript → structured brief via the configured LlmProvider.
todo!("Phase 10b — create_feature_brief")
Err(not_implemented("create_feature_brief"))
}
#[tauri::command]
pub async fn list_feature_briefs(
_meeting_id: Option<MeetingId>,
) -> WaResult<Vec<FeatureBriefInfo>> {
todo!("Phase 10b — list_feature_briefs")
Err(not_implemented("list_feature_briefs"))
}
#[tauri::command]
pub async fn get_feature_brief(_id: String) -> WaResult<FeatureBrief> {
todo!("Phase 10b — get_feature_brief")
Err(not_implemented("get_feature_brief"))
}
/// Scope control: include/exclude a brief from the MCP server (FR-MCP-3).
#[tauri::command]
pub async fn set_brief_exposed(_id: String, _exposed: bool) -> WaResult<()> {
todo!("Phase 10b — set_brief_exposed")
Err(not_implemented("set_brief_exposed"))
}
#[tauri::command]
pub async fn mcp_status() -> WaResult<serde_json::Value> {
todo!("Phase 10b — mcp_status")
Err(not_implemented("mcp_status"))
}
/// Enable/disable the loopback MCP server; returns endpoint + token on enable (FR-MCP-1/6).
@@ -1356,18 +1479,18 @@ pub async fn set_mcp_enabled(
_transport: Option<String>,
_port: Option<u16>,
) -> WaResult<serde_json::Value> {
todo!("Phase 10b — set_mcp_enabled (loopback only, token)")
Err(not_implemented("set_mcp_enabled"))
}
/// Set exposure scope (none|selected|all) and whether recordings may be served (FR-MCP-3).
#[tauri::command]
pub async fn set_mcp_scope(_expose: String, _expose_recordings: Option<bool>) -> WaResult<()> {
todo!("Phase 10b — set_mcp_scope")
Err(not_implemented("set_mcp_scope"))
}
#[tauri::command]
pub async fn mcp_access_log(_limit: Option<u32>) -> WaResult<Vec<McpAccessEntry>> {
todo!("Phase 10b — mcp_access_log (audit)")
Err(not_implemented("mcp_access_log"))
}
// ---- Agent push / task-tracker handoff (Phase 10c, later) ----
@@ -1378,7 +1501,7 @@ pub async fn run_agent(
_tool: String,
_repo_path: String,
) -> WaResult<serde_json::Value> {
todo!("Phase 10c — run_agent (claude|codex|opencode|copilot)")
Err(not_implemented("run_agent"))
}
#[tauri::command]
@@ -1387,7 +1510,7 @@ pub async fn create_issue_from_brief(
_tracker: String,
_assign_copilot: Option<bool>,
) -> WaResult<serde_json::Value> {
todo!("Phase 10c — create_issue_from_brief")
Err(not_implemented("create_issue_from_brief"))
}
// ---- Settings + privacy (Phase 2/7) ----
@@ -1415,7 +1538,7 @@ pub async fn update_settings(patch: serde_json::Value) -> WaResult<Settings> {
/// Reports current egress + LLM endpoint so the UI can prove local-only handling (FR-SEC-2).
#[tauri::command]
pub async fn privacy_self_check() -> WaResult<serde_json::Value> {
todo!("Phase 7 — privacy_self_check")
Err(not_implemented("privacy_self_check"))
}
#[cfg(test)]
@@ -1495,6 +1618,7 @@ mod tests {
],
notes_markdown: "**Alice:** Let's plan the sprint.".to_string(),
summary: None,
calendar_event_id: None,
}
}