//! Journal of routine firings — one row per execution, powering the dashboard //! routines panel's run history + live ("now running") loops. use serde_json::Value; use sqlx::PgPool; use time::OffsetDateTime; use uuid::Uuid; use crate::DbError; /// One routine execution. #[derive(Debug, Clone, serde::Serialize)] pub struct RoutineRun { pub id: Uuid, pub routine_id: Uuid, pub status: String, // running | ok | error #[serde(with = "time::serde::rfc3339")] pub started_at: OffsetDateTime, #[serde(with = "time::serde::rfc3339::option")] pub completed_at: Option, pub error: Option, } /// Record the start of a routine firing; returns the run id to finish later. pub async fn start(pool: &PgPool, routine_id: Uuid) -> Result { let id = Uuid::now_v7(); sqlx::query!( "INSERT INTO routine_runs (id, routine_id) VALUES ($1, $2)", id, routine_id, ) .execute(pool) .await?; Ok(id) } /// Mark a routine run terminal (`ok`/`error`). pub async fn finish( pool: &PgPool, id: Uuid, status: &str, error: Option<&str>, ) -> Result<(), DbError> { sqlx::query!( "UPDATE routine_runs SET status = $2, completed_at = now(), error = $3 WHERE id = $1", id, status, error, ) .execute(pool) .await?; Ok(()) } /// Recent runs for one routine, newest first. pub async fn list_by_routine( pool: &PgPool, routine_id: Uuid, limit: i64, ) -> Result, DbError> { let rows = sqlx::query_as!( RoutineRun, r#"SELECT id, routine_id, status, started_at, completed_at, error FROM routine_runs WHERE routine_id = $1 ORDER BY started_at DESC LIMIT $2"#, routine_id, limit, ) .fetch_all(pool) .await?; Ok(rows) } /// Recent runs across all of a workspace's routines (the panel's run history). /// Joined through `routines` → `agents` to scope to the workspace. Each row also /// carries its routine name for display. pub async fn recent_for_workspace( pool: &PgPool, workspace_id: Uuid, limit: i64, ) -> Result, DbError> { let rows = sqlx::query!( r#"SELECT rr.id, rr.routine_id, r.name AS routine_name, rr.status, rr.started_at, rr.completed_at, rr.error FROM routine_runs rr JOIN routines r ON r.id = rr.routine_id JOIN agents a ON a.id = r.agent_id WHERE a.workspace_id = $1 ORDER BY rr.started_at DESC LIMIT $2"#, workspace_id, limit, ) .fetch_all(pool) .await?; Ok(rows .into_iter() .map(|r| { serde_json::json!({ "id": r.id, "routine_id": r.routine_id, "routine_name": r.routine_name, "status": r.status, "started_at": r.started_at.format(&time::format_description::well_known::Rfc3339).unwrap_or_default(), "completed_at": r.completed_at.and_then(|t| t.format(&time::format_description::well_known::Rfc3339).ok()), "error": r.error, }) }) .collect()) }