//! Stats + loose-ends + todos read endpoints. use axum::extract::State; use axum::response::{IntoResponse, Response}; use serde::Deserialize; use super::AppState; #[derive(Deserialize)] pub(super) struct StatsQuery { window: Option, } pub(super) async fn api_stats( State(state): State, axum::extract::Query(q): axum::extract::Query, ) -> axum::Json { let window = crate::stats::Window::parse(q.window.as_deref().unwrap_or("24h")); let mut snapshot = crate::stats::snapshot_default(window); // Pass the window span to the reminder-stats RPC so the broker // filters its counts to the same time range as the chart data. let window_secs = window.span_secs(); let window_secs_u = u64::try_from(window_secs).unwrap_or(0); snapshot.reminder_stats = fetch_reminder_stats(&state.socket, window_secs_u).await; axum::Json(snapshot) } /// Fetch reminder activity stats from the broker via the per-agent / manager /// socket. Returns None on any transport / decode failure — the stats are /// decorative, not authoritative. async fn fetch_reminder_stats( socket: &std::path::Path, window_secs: u64, ) -> Option { match super::broker_request( socket, &hive_core_agent_sock::Request::ReminderRollup { since_secs: window_secs, agent: None, }, ) .await { Ok(hive_core_agent_sock::Response::ReminderRollup(stats)) => Some(stats), _ => None, } } /// `GET /api/todos` — snapshot of this agent's local todos (loose-ends v2). /// /// Connects to the in-agent harness socket (`HIVE_AGENT_SOCKET`) and calls /// `ListTodos`. Returns `{ "todos": [...] }` where each entry is a /// `LooseEnd::Todo` (subsystem, key, summary, source, `age_seconds`). Returns /// an empty array when the socket is unavailable — best-effort, silent failure. pub(super) async fn api_todos() -> Response { use tokio::io::{AsyncBufReadExt as _, AsyncWriteExt as _, BufReader}; use tokio::net::UnixStream; let socket_path = match std::env::var_os("HIVE_AGENT_SOCKET") { Some(p) => std::path::PathBuf::from(p), None => return axum::Json(serde_json::json!({ "todos": [] })).into_response(), }; if !socket_path.exists() { return axum::Json(serde_json::json!({ "todos": [] })).into_response(); } let todos = tokio::time::timeout(std::time::Duration::from_secs(3), async move { let mut stream = UnixStream::connect(&socket_path).await?; let req = hive_agent_sock::Request::ListTodos { subsystem: None }; let mut line = serde_json::to_string(&req)?; line.push('\n'); stream.write_all(line.as_bytes()).await?; stream.flush().await?; let mut lines = BufReader::new(stream).lines(); let resp_line = lines .next_line() .await? .ok_or_else(|| anyhow::anyhow!("agent socket closed without response"))?; let resp: hive_agent_sock::Response = serde_json::from_str(&resp_line)?; anyhow::Ok(match resp { hive_agent_sock::Response::LooseEnds { loose_ends } => loose_ends, _ => Vec::new(), }) }) .await .unwrap_or_else(|_| Err(anyhow::anyhow!("timeout"))) .unwrap_or_default(); axum::Json(serde_json::json!({ "todos": todos })).into_response() }