loops: iteration timeline — GET /api/topology-runs?loop_id=X

Extend the topology-runs list route with an optional loop_id filter that
returns iterations for a single loop, newest-iteration-first. Adds the
iteration and finished_at columns to the summary (skip-null on the JSON
so compares stay compact). Backed by list_by_loop in the repo, which uses
the existing topology_runs_loop_idx partial index.

LoopsCanvas fetches the runs in parallel with the loop detail and renders
an iteration timeline card (iteration #, status pill, start time, duration,
run id prefix) between the graph section and the actions row.
This commit is contained in:
Omar Sobh
2026-07-06 12:28:34 -07:00
parent af98a79071
commit 6d1dda6197
7 changed files with 414 additions and 14 deletions
+43 -2
View File
@@ -10,13 +10,17 @@ use uuid::Uuid;
use crate::DbError;
/// A row summary for the recent-runs list.
/// A row summary for the recent-runs list. `iteration` and `finished_at`
/// are populated for loop iterations and for terminal runs respectively;
/// `None` for compares or still-in-flight runs.
pub struct TopologyRunSummary {
pub id: Uuid,
pub task: String,
pub status: String,
pub kind: String,
pub created_at: OffsetDateTime,
pub iteration: Option<i32>,
pub finished_at: Option<OffsetDateTime>,
}
/// A full saved comparison run.
@@ -271,7 +275,8 @@ pub async fn list_recent(
limit: i64,
) -> Result<Vec<TopologyRunSummary>, DbError> {
let rows = sqlx::query!(
"SELECT id, task, status, kind, created_at FROM topology_runs
"SELECT id, task, status, kind, created_at, iteration, finished_at
FROM topology_runs
WHERE workspace_id = $1 ORDER BY created_at DESC LIMIT $2",
workspace_id.as_uuid(),
limit,
@@ -286,6 +291,42 @@ pub async fn list_recent(
status: r.status,
kind: r.kind,
created_at: r.created_at,
iteration: r.iteration,
finished_at: r.finished_at,
})
.collect())
}
/// Iterations of a loop, newest first. Uses the partial index
/// `topology_runs_loop_idx` on `(loop_id, iteration DESC)`.
pub async fn list_by_loop(
pool: &PgPool,
workspace_id: WorkspaceId,
loop_id: Uuid,
limit: i64,
) -> Result<Vec<TopologyRunSummary>, DbError> {
let rows = sqlx::query!(
"SELECT id, task, status, kind, created_at, iteration, finished_at
FROM topology_runs
WHERE workspace_id = $1 AND loop_id = $2
ORDER BY iteration DESC NULLS LAST, created_at DESC
LIMIT $3",
workspace_id.as_uuid(),
loop_id,
limit,
)
.fetch_all(pool)
.await?;
Ok(rows
.into_iter()
.map(|r| TopologyRunSummary {
id: r.id,
task: r.task,
status: r.status,
kind: r.kind,
created_at: r.created_at,
iteration: r.iteration,
finished_at: r.finished_at,
})
.collect())
}