Add calendar commands; replace todo!() stubs with typed errors to avoid panics
This commit is contained in:
+149
-25
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user