missions: surface per-phase run errors on the phase card
Adds inline failure debugging to the Phases tab. When you see a
phase card marked FAILED, click the collapsed error summary and the
full topology_run.error text expands under it — the exact stack
trace / provider error / whatever the worker recorded.
Backend:
- TopologyRunSummary gains mission_phase_id + team_id + error
fields. list_by_mission SELECT extended; other constructor
(list_recent) explicitly passes None for the new fields.
- GET /api/missions/{id}/runs response now carries all of the
above so the frontend can attribute failures per phase.
Frontend:
- MissionRunSummary type mirrors backend additions.
- MissionCanvas fetches runs alongside mission on load +
auto-refresh; indexes by mission_phase_id in a memoized Map.
- Each phase card renders a per-run row: colored status pill
(running / completed / failed), short run id, finished_at
timestamp. For failed runs, a <details> collapses the error
text — first line as summary, full 4kB in a monospace <pre> on
expand.
Directly unblocks the "phase says Failed but there's no info to
debug" report. Both research and coding phases get this — the code
path is phase-kind-agnostic.
This commit is contained in:
@@ -22,6 +22,14 @@ pub struct TopologyRunSummary {
|
||||
pub created_at: OffsetDateTime,
|
||||
#[serde(with = "time::serde::rfc3339::option")]
|
||||
pub finished_at: Option<OffsetDateTime>,
|
||||
/// FK to mission_phases when the run was enqueued by phase_runner.
|
||||
/// Frontend uses this to attribute failures to the right phase card.
|
||||
pub mission_phase_id: Option<Uuid>,
|
||||
pub team_id: Option<Uuid>,
|
||||
/// The last error message the worker recorded; only populated when
|
||||
/// status = 'failed'. Trimmed here to first 4kB to keep the API
|
||||
/// response small; the full text lives in topology_runs.error.
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// A full saved comparison run.
|
||||
@@ -365,6 +373,9 @@ pub async fn list_recent(
|
||||
kind: r.kind,
|
||||
created_at: r.created_at,
|
||||
finished_at: r.finished_at,
|
||||
mission_phase_id: None,
|
||||
team_id: None,
|
||||
error: None,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
@@ -379,7 +390,9 @@ pub async fn list_by_mission(
|
||||
) -> Result<Vec<TopologyRunSummary>, DbError> {
|
||||
use sqlx::Row;
|
||||
let rows = sqlx::query(
|
||||
"SELECT id, task, status, kind, created_at, finished_at
|
||||
"SELECT id, task, status, kind, created_at, finished_at,
|
||||
mission_phase_id, team_id,
|
||||
LEFT(coalesce(error, ''), 4096) AS error
|
||||
FROM topology_runs
|
||||
WHERE mission_id = $1 ORDER BY created_at DESC LIMIT $2",
|
||||
)
|
||||
@@ -396,6 +409,16 @@ pub async fn list_by_mission(
|
||||
kind: r.get("kind"),
|
||||
created_at: r.get("created_at"),
|
||||
finished_at: r.try_get("finished_at").ok(),
|
||||
mission_phase_id: r.try_get("mission_phase_id").ok().flatten(),
|
||||
team_id: r.try_get("team_id").ok().flatten(),
|
||||
error: {
|
||||
let s: String = r.try_get("error").unwrap_or_default();
|
||||
if s.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(s)
|
||||
}
|
||||
},
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user