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:
@@ -171,6 +171,58 @@ pub async fn active_runs_for_research_topic(
|
||||
/// the actual run ids (queued + running) so the UI can subscribe to
|
||||
/// their SSE event streams. Ordered newest first — the freshest run is
|
||||
/// the one the user just kicked off.
|
||||
/// Batch run-count feeder for the research topic list. Returns a
|
||||
/// (topic_id, in_flight, failed) tuple per topic in `topic_ids`,
|
||||
/// omitting topics with zero runs. Used to render the errored-state
|
||||
/// icon + "rerun" affordance on cards in the left sidebar.
|
||||
///
|
||||
/// `failed` counts runs that terminated in `failed` since the topic's
|
||||
/// most recent successful run (or all-time if none have succeeded).
|
||||
/// That way an old failure on a topic that later succeeded doesn't
|
||||
/// keep the card flagged as broken.
|
||||
pub async fn run_counts_by_research_topic(
|
||||
pool: &PgPool,
|
||||
topic_ids: &[Uuid],
|
||||
) -> Result<Vec<(Uuid, i64, i64)>, DbError> {
|
||||
use sqlx::Row;
|
||||
if topic_ids.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let rows: Vec<sqlx::postgres::PgRow> = sqlx::query(
|
||||
"WITH last_success AS (
|
||||
SELECT research_topic_id, max(created_at) AS ts
|
||||
FROM topology_runs
|
||||
WHERE research_topic_id = ANY($1)
|
||||
AND status = 'completed'
|
||||
GROUP BY research_topic_id
|
||||
)
|
||||
SELECT r.research_topic_id AS topic_id,
|
||||
count(*) FILTER (WHERE r.status IN ('queued','running')) AS in_flight,
|
||||
count(*) FILTER (
|
||||
WHERE r.status = 'failed'
|
||||
AND r.created_at > coalesce(ls.ts, 'epoch'::timestamptz)
|
||||
) AS failed
|
||||
FROM topology_runs r
|
||||
LEFT JOIN last_success ls
|
||||
ON ls.research_topic_id = r.research_topic_id
|
||||
WHERE r.research_topic_id = ANY($1)
|
||||
GROUP BY r.research_topic_id",
|
||||
)
|
||||
.bind(topic_ids)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|r| {
|
||||
(
|
||||
r.get::<Uuid, _>("topic_id"),
|
||||
r.get::<i64, _>("in_flight"),
|
||||
r.get::<i64, _>("failed"),
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn active_run_ids_for_research_topic(
|
||||
pool: &PgPool,
|
||||
research_topic_id: Uuid,
|
||||
|
||||
Reference in New Issue
Block a user