mission progress UI: auto-refresh + Team tab + Live events tab
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 36s
ci / rust (push) Successful in 2m59s
ci / e2e (push) Skipped
ci / publish (push) Successful in 3m9s

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:
Omar Sobh
2026-07-20 15:31:41 -07:00
parent cf735312f8
commit 3ba0485e7d
8 changed files with 534 additions and 1 deletions
+34
View File
@@ -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,