fix(routines): normalize status display across web and CLI (#1469)

* fix(routines): normalize status display across web and CLI surfaces (#1319)

- Use Display (lowercase) instead of Debug (PascalCase) for RunStatus serialization in web handler
- Update JavaScript status class mapping to match lowercase values from the API
- Enrich CLI `routines list` to show running/attention states by querying last run status

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix(routines): address review -- batch last-run query, consistent status, simplify ternary (#1319)

- Parallelize last-run lookups with join_all to avoid N+1 sequential queries
- Normalize status in /api/routines/{id}/runs handler to match lowercase convention
- Remove redundant 'running' check in app.js runStatusClass logic

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix(db): replace N+1 last-run-status queries with batch method

The CLI routines list was firing a separate list_routine_runs query per
routine to determine each one's last run status. For large routine sets
this overwhelms the connection pool.

Add batch_get_last_run_status to the Database trait with implementations
for both PostgreSQL (DISTINCT ON + ORDER BY) and libSQL (correlated
subquery + in-memory filter). Update the CLI to call the batch method
once instead of N times.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* style: cargo fmt

https://claude.ai/code/session_01Va9wwvATNWFAx35GG7Zek7

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
Zaki Manian
2026-03-24 10:41:33 +01:00
committed by GitHub
co-authored by Claude Opus 4.6
parent fb3548956b
commit 01678be61d
9 changed files with 317 additions and 14 deletions
+50
View File
@@ -462,6 +462,56 @@ impl RoutineStore for LibSqlBackend {
Ok(counts)
}
async fn batch_get_last_run_status(
&self,
routine_ids: &[Uuid],
) -> Result<HashMap<Uuid, RunStatus>, DatabaseError> {
if routine_ids.is_empty() {
return Ok(HashMap::new());
}
let conn = self.connect().await?;
// SQLite doesn't support ANY($1), so we query all latest runs and filter in memory.
// Uses a subquery to pick only the most recent run per routine.
let mut rows = conn
.query(
"SELECT routine_id, status FROM routine_runs r1
WHERE started_at = (
SELECT MAX(started_at) FROM routine_runs r2
WHERE r2.routine_id = r1.routine_id
)
GROUP BY routine_id",
params![],
)
.await
.map_err(|e| {
DatabaseError::Query(format!("Failed to batch get last run status: {}", e))
})?;
let routine_id_set: HashSet<Uuid> = routine_ids.iter().copied().collect();
let mut statuses = HashMap::new();
while let Some(row) = rows
.next()
.await
.map_err(|e| DatabaseError::Query(e.to_string()))?
{
let id_str: String = get_text(&row, 0);
let id = Uuid::parse_str(&id_str)
.map_err(|e| DatabaseError::Query(format!("Invalid routine UUID: {}", e)))?;
if routine_id_set.contains(&id) {
let status_str: String = get_text(&row, 1);
if let std::result::Result::Ok(status) = status_str.parse::<RunStatus>() {
statuses.insert(id, status);
}
}
}
Ok(statuses)
}
async fn link_routine_run_to_job(
&self,
run_id: Uuid,
+9
View File
@@ -528,6 +528,15 @@ pub trait RoutineStore: Send + Sync {
&self,
routine_ids: &[Uuid],
) -> Result<HashMap<Uuid, i64>, DatabaseError>;
/// Fetch the last run status for multiple routines in a single query.
/// Returns a map from routine_id to its most recent RunStatus.
/// Routines with no runs are omitted from the result.
async fn batch_get_last_run_status(
&self,
routine_ids: &[Uuid],
) -> Result<HashMap<Uuid, RunStatus>, DatabaseError>;
async fn link_routine_run_to_job(
&self,
run_id: Uuid,
+8
View File
@@ -510,6 +510,14 @@ impl RoutineStore for PgBackend {
.await
}
async fn batch_get_last_run_status(
&self,
routine_ids: &[Uuid],
) -> Result<std::collections::HashMap<Uuid, crate::agent::routine::RunStatus>, DatabaseError>
{
self.store.batch_get_last_run_status(routine_ids).await
}
async fn link_routine_run_to_job(
&self,
run_id: Uuid,