//! Health Snapshot — on-demand query for all subsystem health status //! //! Provides a single Tauri command that aggregates health data from: //! - Intelligence Heartbeat engine (running state, config, alerts) //! - Memory pipeline (entries count, storage size) //! //! Connection and SaaS status are managed by frontend stores and not included here. use serde::Serialize; use super::heartbeat::{HeartbeatConfig, HeartbeatEngineState, HeartbeatResult}; /// Aggregated health snapshot from Rust backend #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct HealthSnapshot { pub timestamp: String, pub intelligence: IntelligenceHealth, pub memory: MemoryHealth, } /// Intelligence heartbeat engine status #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct IntelligenceHealth { pub engine_running: bool, pub config: HeartbeatConfig, pub last_tick: Option, pub alert_count_24h: usize, pub total_checks: usize, } /// Memory pipeline status #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct MemoryHealth { pub total_entries: usize, pub storage_size_bytes: u64, pub last_extraction: Option, } /// Query a unified health snapshot for an agent // @connected #[tauri::command] pub async fn health_snapshot( agent_id: String, heartbeat_state: tauri::State<'_, HeartbeatEngineState>, ) -> Result { let engines = heartbeat_state.lock().await; let engine = engines .get(&agent_id) .ok_or_else(|| format!("Heartbeat engine not initialized for agent: {}", agent_id))?; let engine_running = engine.is_running().await; let config = engine.get_config().await; let history: Vec = engine.get_history(100).await; // Calculate alert count in the last 24 hours let now = chrono::Utc::now(); let twenty_four_hours_ago = now - chrono::Duration::hours(24); let alert_count_24h = history .iter() .filter(|r| { r.timestamp.parse::>() .map(|t| t > twenty_four_hours_ago) .unwrap_or(false) }) .flat_map(|r| r.alerts.iter()) .count(); let last_tick = history.first().map(|r| r.timestamp.clone()); // Memory health from cached stats (fallback to zeros) // Read cache in a separate scope to ensure RwLockReadGuard is dropped before any .await let cached_stats: Option = { let cache = super::heartbeat::get_memory_stats_cache(); match cache.read() { Ok(c) => c.get(&agent_id).cloned(), Err(_) => None, } }; // RwLockReadGuard dropped here let memory = match cached_stats { Some(s) => MemoryHealth { total_entries: s.total_entries, storage_size_bytes: s.storage_size_bytes as u64, last_extraction: s.last_updated, }, None => { // Fallback: try to query VikingStorage directly match crate::viking_commands::get_storage().await { Ok(storage) => { match zclaw_growth::VikingStorage::find_by_prefix(&*storage, &format!("mem:{}", agent_id)).await { Ok(entries) => MemoryHealth { total_entries: entries.len(), storage_size_bytes: 0, last_extraction: None, }, Err(_) => MemoryHealth { total_entries: 0, storage_size_bytes: 0, last_extraction: None, }, } } Err(_) => MemoryHealth { total_entries: 0, storage_size_bytes: 0, last_extraction: None, }, } } }; Ok(HealthSnapshot { timestamp: chrono::Utc::now().to_rfc3339(), intelligence: IntelligenceHealth { engine_running, config, last_tick, alert_count_24h, total_checks: 5, // Fixed: 5 built-in checks }, memory, }) }