research-pipeline-diag: state-aware statuses (waiting != failing)
The diagnostic was calling any 0-outcome + 0-completed-runs state a
FAIL — red dot + 'No outcome produced yet — check the runs stage for
the failure reason'. That fires the moment a wizard-materialized
topic lands, before its very first turn even completes, and stays
red for the whole 2-3 min a legitimate coordinator turn runs. Result:
users see 'FAIL' on every fresh topic and can't tell a real failure
from a normal in-flight state.
Backend fix — introduce a `waiting` status (blue/pulsing in UI):
Runs stage:
0 runs -> skip ('No runs yet — pipeline hasn't fired')
any running/queued -> waiting ('N in flight, M completed')
all failed -> fail (with error text)
some failed -> warn
all completed no fails -> ok
Outcomes stage:
outcome_count > 0 -> ok
0 outcomes + 0 runs -> skip ('No outcome yet (pipeline hasn't fired)')
0 outcomes + any running -> waiting ('Waiting for the current run to finish…')
0 outcomes + any failed -> fail (the actual silent-bug case)
0 outcomes + all done ok -> warn (weird — completed but wrote nothing)
Also suppresses the run stage's `latest_error` detail when the run
status is `waiting` or `skip` — reporting a stale error next to an
actively-running job is what made users think the current run had
failed.
Frontend:
- PipelineStage['status'] union grows a 'waiting' arm.
- Pill color: cyan (#5ec8d8) with a pulsing scale/opacity animation
(new cm-pulse keyframe in motion.css).
- Strip summary line: 'Pipeline in flight — waiting for run to finish…'
when there's any waiting stage and no failures.
- Border tint: cyan border when waiting, coral when failing, neutral
otherwise.
Zero backend semantic changes to the outcome-write path — this is
purely UI truth-telling.
This commit is contained in:
@@ -122,12 +122,35 @@ pub async fn pipeline_state(
|
||||
.iter()
|
||||
.filter(|r| r.try_get::<String, _>("status").ok().as_deref() == Some("failed"))
|
||||
.count();
|
||||
let n_running = run_rows
|
||||
.iter()
|
||||
.filter(|r| {
|
||||
matches!(
|
||||
r.try_get::<String, _>("status").ok().as_deref(),
|
||||
Some("running") | Some("queued")
|
||||
)
|
||||
})
|
||||
.count();
|
||||
let n_completed = run_rows
|
||||
.iter()
|
||||
.filter(|r| r.try_get::<String, _>("status").ok().as_deref() == Some("completed"))
|
||||
.count();
|
||||
let latest_error = run_rows
|
||||
.iter()
|
||||
.find_map(|r| r.try_get::<Option<String>, _>("error").ok().flatten())
|
||||
.filter(|s| !s.is_empty());
|
||||
// Status rules:
|
||||
// - 0 runs → skip (nothing to see yet — natural pre-fire state,
|
||||
// NOT a failure)
|
||||
// - any running → waiting (blue/spinner in UI — legitimate in-flight
|
||||
// state)
|
||||
// - all failed → fail (nothing succeeded)
|
||||
// - some failed → warn (mixed history)
|
||||
// - all completed → ok
|
||||
let run_status = if n_runs == 0 {
|
||||
"warn"
|
||||
"skip"
|
||||
} else if n_running > 0 {
|
||||
"waiting"
|
||||
} else if n_failed == n_runs {
|
||||
"fail"
|
||||
} else if n_failed > 0 {
|
||||
@@ -135,29 +158,73 @@ pub async fn pipeline_state(
|
||||
} else {
|
||||
"ok"
|
||||
};
|
||||
let run_label = if n_runs == 0 {
|
||||
"No runs yet — pipeline hasn't fired".to_string()
|
||||
} else if n_running > 0 && n_failed == 0 {
|
||||
format!("{n_running} in flight, {n_completed} completed")
|
||||
} else if n_running > 0 {
|
||||
format!("{n_running} in flight, {n_completed} completed, {n_failed} failed")
|
||||
} else {
|
||||
format!("{n_runs} run(s), {n_failed} failed, {n_completed} completed")
|
||||
};
|
||||
stages.push(PipelineStage {
|
||||
key: "runs".into(),
|
||||
label: format!("{n_runs} run(s), {n_failed} failed"),
|
||||
label: run_label,
|
||||
// Suppress the "failure" detail line while runs are still in flight —
|
||||
// reporting a prior turn's stale error text next to an actively-running
|
||||
// job reads like the current run failed, which is what triggered the
|
||||
// "everything looks broken" impression.
|
||||
status: run_status,
|
||||
detail: latest_error,
|
||||
detail: if run_status == "waiting" || run_status == "skip" {
|
||||
None
|
||||
} else {
|
||||
latest_error
|
||||
},
|
||||
});
|
||||
|
||||
// 5. outcomes — the artifact rows get_artifact reads.
|
||||
// 5. outcomes — the artifact rows get_artifact reads. Status is
|
||||
// state-aware: an outcome-less topic with an in-flight run is a
|
||||
// NORMAL waiting state, not a failure. Only flag `fail` when all
|
||||
// runs have terminated AND none produced an outcome — the actual
|
||||
// silent-bug case this diagnostic was designed to catch.
|
||||
let outcome_count: i64 =
|
||||
sqlx::query_scalar("SELECT count(*) FROM research_outcomes WHERE topic_id = $1")
|
||||
.bind(id)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
let outcome_status = if outcome_count > 0 {
|
||||
"ok"
|
||||
} else if n_runs == 0 {
|
||||
"skip"
|
||||
} else if n_running > 0 {
|
||||
"waiting"
|
||||
} else if n_failed > 0 {
|
||||
"fail"
|
||||
} else {
|
||||
"warn"
|
||||
};
|
||||
let outcome_label = if outcome_count > 0 {
|
||||
format!("{outcome_count} outcome(s) written")
|
||||
} else if n_running > 0 {
|
||||
"Waiting for the current run to finish…".to_string()
|
||||
} else if n_runs == 0 {
|
||||
"No outcome yet (pipeline hasn't fired)".to_string()
|
||||
} else if n_failed > 0 {
|
||||
"No outcome — all runs failed".to_string()
|
||||
} else {
|
||||
"No outcome yet".to_string()
|
||||
};
|
||||
let outcome_detail = if outcome_status == "fail" {
|
||||
Some("No outcome produced — check the runs stage for the failure reason.".into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
stages.push(PipelineStage {
|
||||
key: "outcomes".into(),
|
||||
label: format!("{outcome_count} outcome(s) written"),
|
||||
status: if outcome_count > 0 { "ok" } else { "fail" },
|
||||
detail: if outcome_count == 0 {
|
||||
Some("No outcome produced yet — check the runs stage for the failure reason.".into())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
label: outcome_label,
|
||||
status: outcome_status,
|
||||
detail: outcome_detail,
|
||||
});
|
||||
|
||||
// 6. approval — pending publish-approval, if any.
|
||||
|
||||
Reference in New Issue
Block a user