missions: retry failed phases + auto-purge on re-launch
Every re-attempted phase now starts with a clean slate:
- phase_runner::launch_phase DELETEs prior status IN ('failed',
'cancelled') topology_runs for the phase before enqueuing the
new ones. Completed runs are kept for audit; only the failure
noise from earlier attempts goes.
- POST /api/missions/{id}/phases/{phase_id}/retry — resets a
failed/cancelled phase to 'pending' (auth-scoped to the calling
workspace + guarded on mission.status='running'). phase_runner
picks it up on the next 10s tick.
- MissionCanvas phase card grows a coral 'Retry' button, visible
only when phase.status='failed' and mission.status='running'.
Click → resets + refreshes; the prior failed run rows disappear
from the card as soon as phase_runner enqueues the new attempt.
Design: auto-purge in phase_runner rather than a separate 'clear
failed runs' endpoint. Users don't have to manually clean up before
retrying; the runner does it as part of the natural work of firing
a fresh attempt.
Verified: cargo check + tsc + eslint --quiet all green.
This commit is contained in:
@@ -447,6 +447,39 @@ pub async fn list_teams(
|
||||
Ok(Json(serde_json::json!({ "teams": teams })))
|
||||
}
|
||||
|
||||
/// POST /api/missions/{id}/phases/{phase_id}/retry — reset a
|
||||
/// failed / cancelled phase back to 'pending' so the phase_runner
|
||||
/// picks it up on the next tick. The runner purges old failed
|
||||
/// topology_runs for the phase before re-enqueuing, so the phase
|
||||
/// card starts fresh on the retry.
|
||||
pub async fn retry_phase(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Path((id, phase_id)): Path<(Uuid, Uuid)>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
// Scope check on the mission.
|
||||
let mission = cm_db::repo::missions::get(&state.pool, id, user.workspace_id.as_uuid())
|
||||
.await?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
if mission.status != "running" {
|
||||
return Err(ApiError::BadRequest);
|
||||
}
|
||||
let r = sqlx::query(
|
||||
"UPDATE mission_phases
|
||||
SET status = 'pending', started_at = NULL, completed_at = NULL
|
||||
WHERE id = $1 AND mission_id = $2
|
||||
AND status IN ('failed', 'cancelled')",
|
||||
)
|
||||
.bind(phase_id)
|
||||
.bind(id)
|
||||
.execute(&state.pool)
|
||||
.await?;
|
||||
if r.rows_affected() == 0 {
|
||||
return Err(ApiError::NotFound);
|
||||
}
|
||||
Ok(Json(serde_json::json!({ "reset": true })))
|
||||
}
|
||||
|
||||
/// GET /api/missions/{id}/runs — topology_runs bound to this mission,
|
||||
/// newest first. Used by the Live tab to subscribe to per-run SSE.
|
||||
pub async fn list_runs(
|
||||
|
||||
Reference in New Issue
Block a user