//! Scan-pipeline health-check storage. //! //! Unlike the other checks, the scan pipeline was originally only alerted on and //! never persisted, so `/status.json` had nothing to read. This stores the //! latest result per target the same way `backup_checks` does, so the release //! viewer can render it from the ledger rather than re-running a live probe on //! every poll. use super::{Result, ScanPipelineCheckResult, SqlitePool}; use tracing::instrument; #[instrument(skip_all)] pub async fn insert_scan_pipeline_check( pool: &SqlitePool, result: &ScanPipelineCheckResult, ) -> Result { let issues = serde_json::to_string(&result.issues).unwrap_or_default(); let row = sqlx::query( "INSERT INTO scan_pipeline_checks (target, status, issues, queue_pending, queue_running, queue_stuck, held_total, checked_at, error) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", ) .bind(&result.target) .bind(&result.status) .bind(&issues) .bind(result.queue_pending) .bind(result.queue_running) .bind(result.queue_stuck) .bind(result.held_total) .bind(&result.checked_at) .bind(&result.error) .execute(pool) .await?; Ok(row.last_insert_rowid()) } /// The latest scan-pipeline check for a target, if one has been recorded. #[instrument(skip_all)] pub async fn get_latest_scan_pipeline_check( pool: &SqlitePool, target: &str, ) -> Result> { Ok(sqlx::query_as::<_, ScanPipelineCheckRow>( "SELECT id, target, status, issues, queue_pending, queue_running, queue_stuck, held_total, checked_at, error FROM scan_pipeline_checks WHERE target = ? ORDER BY id DESC LIMIT 1", ) .bind(target) .fetch_optional(pool) .await?) } #[derive(Debug, Clone, sqlx::FromRow, serde::Serialize)] pub struct ScanPipelineCheckRow { pub id: i64, pub target: String, pub status: String, /// JSON array of the fired-threshold issue lines. Use [`Self::issue_list`]. pub issues: String, pub queue_pending: i64, pub queue_running: i64, pub queue_stuck: i64, pub held_total: i64, pub checked_at: String, pub error: Option, } impl ScanPipelineCheckRow { /// Decode the stored issue lines, tolerating a malformed blob as empty. pub fn issue_list(&self) -> Vec { serde_json::from_str(&self.issues).unwrap_or_default() } }