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:
@@ -6,7 +6,7 @@
|
||||
use std::convert::Infallible;
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::extract::{Path, State};
|
||||
use axum::extract::{Path, Query, State};
|
||||
use axum::http::{HeaderMap, StatusCode};
|
||||
use axum::response::sse::{Event, KeepAlive, Sse};
|
||||
use axum::response::IntoResponse;
|
||||
@@ -216,6 +216,8 @@ pub async fn run_swarm(
|
||||
}
|
||||
|
||||
/// A saved/queued run, summarized (now includes lifecycle status + kind).
|
||||
/// `iteration` + `finished_at` populate for loop iterations / terminal runs;
|
||||
/// they're skipped from the JSON when null to keep the compares path compact.
|
||||
#[derive(Serialize)]
|
||||
pub struct RunSummary {
|
||||
pub id: String,
|
||||
@@ -223,15 +225,40 @@ pub struct RunSummary {
|
||||
pub status: String,
|
||||
pub kind: String,
|
||||
pub created_at: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub iteration: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub finished_at: Option<String>,
|
||||
}
|
||||
|
||||
/// Query params for `GET /api/topology-runs`. `loop_id` filters to a single
|
||||
/// loop's iterations, ordered newest-iteration-first.
|
||||
#[derive(Deserialize)]
|
||||
pub struct ListRunsQuery {
|
||||
#[serde(default)]
|
||||
pub loop_id: Option<Uuid>,
|
||||
#[serde(default)]
|
||||
pub limit: Option<i64>,
|
||||
}
|
||||
|
||||
/// `GET /api/topology-runs` — recent runs for the workspace (compares + durable
|
||||
/// run jobs), newest first.
|
||||
/// run jobs), newest first. `?loop_id=X` filters to iterations of one loop,
|
||||
/// ordered by iteration DESC (uses `topology_runs_loop_idx`).
|
||||
pub async fn list_runs(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Query(q): Query<ListRunsQuery>,
|
||||
) -> Result<Json<Vec<RunSummary>>, ApiError> {
|
||||
let rows = cm_db::repo::topology_runs::list_recent(&state.pool, user.workspace_id, 20).await?;
|
||||
let limit = q.limit.filter(|n| *n > 0 && *n <= 200).unwrap_or(20);
|
||||
let rows = match q.loop_id {
|
||||
Some(loop_id) => {
|
||||
cm_db::repo::topology_runs::list_by_loop(&state.pool, user.workspace_id, loop_id, limit)
|
||||
.await?
|
||||
}
|
||||
None => {
|
||||
cm_db::repo::topology_runs::list_recent(&state.pool, user.workspace_id, limit).await?
|
||||
}
|
||||
};
|
||||
let out = rows
|
||||
.into_iter()
|
||||
.map(|r| RunSummary {
|
||||
@@ -240,6 +267,8 @@ pub async fn list_runs(
|
||||
status: r.status,
|
||||
kind: r.kind,
|
||||
created_at: r.created_at.format(&Rfc3339).unwrap_or_default(),
|
||||
iteration: r.iteration,
|
||||
finished_at: r.finished_at.and_then(|t| t.format(&Rfc3339).ok()),
|
||||
})
|
||||
.collect();
|
||||
Ok(Json(out))
|
||||
|
||||
Reference in New Issue
Block a user