mission progress UI: auto-refresh + Team tab + Live events tab
Fills the biggest UX gap surfaced during the deploy walk: hosted
missions had no live-progress surface at all. Now they do.
Auto-refresh:
- MissionCanvas grows a second useEffect that polls getMission
every 3s while mission.status === 'running'. Stops immediately
on terminal state (completed / failed / cancelled). Phases,
Tasks, Artifacts, Benchmarks all update without a manual click.
Team tab (new):
- MissionTeamTab.tsx — fetches /api/teams/{id} + /api/team/claws,
shows a card per member with role slot + an "Open" pill that
calls onOpenClaw(clawId) → Dashboard flips to AGENT tier with
that claw selected, dropping the operator into the existing
ClawCommandCenter surface (WorkingOnNow, ReasoningStream, etc).
Live events tab (new):
- MissionLiveEvents.tsx — polls /api/missions/{id}/runs every 5s
for the topology_runs bound to this mission, opens one
EventSource per active run against /api/topology-runs/{id}/events,
renders as a chronological scrolling feed with per-event kind
pills + per-run short-id badges. Auto-scrolls unless the
operator scrolled up. New runs auto-attach; terminal runs
close cleanly.
Backend:
- cm-db::repo::topology_runs::list_by_mission — SELECT ... FROM
topology_runs WHERE mission_id = $1 ORDER BY created_at DESC.
Uses runtime sqlx::query (not the macro) to avoid a sqlx cache
regen just for this route.
- TopologyRunSummary gains #[derive(Serialize)] + rfc3339 codecs.
- GET /api/missions/{id}/runs — workspace-scoped, returns
{ runs: [...] }.
Dashboard wires onOpenClaw on MissionCanvas → setAgentId + setTier("claw").
Verified: cargo check --workspace + tsc --noEmit + eslint --quiet
all green.
This commit is contained in:
@@ -12,12 +12,15 @@ use crate::DbError;
|
||||
|
||||
/// A row summary for the recent-runs list. `finished_at` is populated for
|
||||
/// terminal runs; `None` for compares or still-in-flight runs.
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct TopologyRunSummary {
|
||||
pub id: Uuid,
|
||||
pub task: String,
|
||||
pub status: String,
|
||||
pub kind: String,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub created_at: OffsetDateTime,
|
||||
#[serde(with = "time::serde::rfc3339::option")]
|
||||
pub finished_at: Option<OffsetDateTime>,
|
||||
}
|
||||
|
||||
@@ -366,6 +369,37 @@ pub async fn list_recent(
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// All topology_runs bound to a mission (via topology_runs.mission_id
|
||||
/// added in migration 0051). Newest first — the mission canvas Live
|
||||
/// tab uses this to subscribe to each active run's SSE.
|
||||
pub async fn list_by_mission(
|
||||
pool: &PgPool,
|
||||
mission_id: Uuid,
|
||||
limit: i64,
|
||||
) -> Result<Vec<TopologyRunSummary>, DbError> {
|
||||
use sqlx::Row;
|
||||
let rows = sqlx::query(
|
||||
"SELECT id, task, status, kind, created_at, finished_at
|
||||
FROM topology_runs
|
||||
WHERE mission_id = $1 ORDER BY created_at DESC LIMIT $2",
|
||||
)
|
||||
.bind(mission_id)
|
||||
.bind(limit)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|r| TopologyRunSummary {
|
||||
id: r.get("id"),
|
||||
task: r.get("task"),
|
||||
status: r.get("status"),
|
||||
kind: r.get("kind"),
|
||||
created_at: r.get("created_at"),
|
||||
finished_at: r.try_get("finished_at").ok(),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// A single saved comparison/run result, scoped to its workspace.
|
||||
pub async fn get(
|
||||
pool: &PgPool,
|
||||
|
||||
Reference in New Issue
Block a user