research: errored-state card + one-click rerun (no wizard re-entry)
When a topic ends up parked in 'processing' with all runs failed and nothing in flight, the sidebar card was still spinning as if progress were happening. Now: Backend - topology_runs::run_counts_by_research_topic — batch query that returns (in_flight, failed-since-last-success) per topic. Used by the list endpoint; dynamic sqlx::query() so no prepare needed. - TopicListItem DTO gains runs_in_flight + runs_failed. - start_topic status guard relaxed: allow (standby) OR (processing AND runs_in_flight == 0). Blocks accidental double-fires on a live pipeline; permits rerun on a failed one. Same request body, same behavior once accepted, so the frontend just POSTs /research/:id/start on the RotateCw click. Frontend - ResearchList detects errored: status===processing && !in_flight && failed>0. Swaps the MiniSpinner for a red AlertTriangle and changes the status text to 'error · N failed'. - New RotateCw icon button next to the delete Trash — same button cluster, one click, no wizard re-entry required. Disables while a request is in flight; error surfaces in the sidebar's shared error banner.
This commit is contained in:
@@ -406,6 +406,13 @@ pub struct TopicListItem {
|
||||
pub outcome_kind: String,
|
||||
pub status: String,
|
||||
pub updated_at: String,
|
||||
/// queued + running topology_runs bound to this topic.
|
||||
pub runs_in_flight: i64,
|
||||
/// failed runs since the last successful run (or all-time if none).
|
||||
/// > 0 with `runs_in_flight == 0` and `status == "processing"` is
|
||||
/// the errored-but-not-terminal state — frontend swaps the spinner
|
||||
/// for an error icon and offers a rerun.
|
||||
pub runs_failed: i64,
|
||||
}
|
||||
|
||||
pub async fn list_topics(
|
||||
@@ -413,17 +420,30 @@ pub async fn list_topics(
|
||||
Authed(user): Authed,
|
||||
) -> Result<Json<Vec<TopicListItem>>, ApiError> {
|
||||
let rows = cm_db::repo::research_topics::list(&state.pool, user.workspace_id.as_uuid()).await?;
|
||||
let ids: Vec<Uuid> = rows.iter().map(|t| t.id).collect();
|
||||
let counts = cm_db::repo::topology_runs::run_counts_by_research_topic(&state.pool, &ids)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let mut count_by: std::collections::HashMap<Uuid, (i64, i64)> = counts
|
||||
.into_iter()
|
||||
.map(|(id, in_flight, failed)| (id, (in_flight, failed)))
|
||||
.collect();
|
||||
Ok(Json(
|
||||
rows.into_iter()
|
||||
.map(|t| TopicListItem {
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
outcome_kind: t.outcome_kind,
|
||||
status: t.status,
|
||||
updated_at: t
|
||||
.updated_at
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.unwrap_or_default(),
|
||||
.map(|t| {
|
||||
let (runs_in_flight, runs_failed) = count_by.remove(&t.id).unwrap_or((0, 0));
|
||||
TopicListItem {
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
outcome_kind: t.outcome_kind,
|
||||
status: t.status,
|
||||
updated_at: t
|
||||
.updated_at
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.unwrap_or_default(),
|
||||
runs_in_flight,
|
||||
runs_failed,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
@@ -657,7 +677,15 @@ pub async fn start_topic(
|
||||
let topic = cm_db::repo::research_topics::get(&state.pool, id, user.workspace_id.as_uuid())
|
||||
.await?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
if topic.status != "standby" {
|
||||
// Allow the fresh-start path (standby) AND the rerun path (topic
|
||||
// parked in `processing` after all runs failed / no runs remain in
|
||||
// flight). Blocks accidental double-fires on a live pipeline
|
||||
// (in_flight > 0) and terminal states (reviewing / publishing /
|
||||
// published).
|
||||
let in_flight =
|
||||
cm_db::repo::topology_runs::active_runs_for_research_topic(&state.pool, id).await?;
|
||||
let can_start = topic.status == "standby" || (topic.status == "processing" && in_flight == 0);
|
||||
if !can_start {
|
||||
return Err(ApiError::Conflict);
|
||||
}
|
||||
// D1 fold — refuse to double-fire when a scheduled research loop
|
||||
|
||||
Reference in New Issue
Block a user