Extract derive_run_state into format.rs
The identical state-derivation logic (outcome + dispatched_at → display
string) was duplicated in RunRow::derived_state(), RunListRow::state(),
and DetailRun::state(). Pull it into a single free function in format.rs
and have both template structs delegate to it. Remove the now-unused
RunRow::derived_state().

https://claude.ai/code/session_0171wuJy2GRJZuj2oVYZ2iKe
change
commit 924014ccaec634620d5e5b1db9dde5cde06ab419
author Claude <noreply@anthropic.com>
date
parent fffeb8bc
diff --git a/quire-server/src/quire/web/db.rs b/quire-server/src/quire/web/db.rs
index c59e154..10e3251 100644
--- a/quire-server/src/quire/web/db.rs
+++ b/quire-server/src/quire/web/db.rs
@@ -13,16 +13,6 @@ pub struct RunRow {
     pub resolved_at: Option<i64>,
 }
 
-impl RunRow {
-    pub fn derived_state(&self) -> &str {
-        match &self.outcome {
-            Some(o) if o.starts_with("failed") => "failed",
-            Some(o) => o.as_str(),
-            None if self.dispatched_at.is_some() => "active",
-            None => "queued",
-        }
-    }
-}
 
 /// Raw job row from the database.
 pub struct JobRow {
diff --git a/quire-server/src/quire/web/format.rs b/quire-server/src/quire/web/format.rs
index e2ee112..c136dc2 100644
--- a/quire-server/src/quire/web/format.rs
+++ b/quire-server/src/quire/web/format.rs
@@ -77,10 +77,18 @@ fn format_ms_duration(ms: i64) -> String {
     format!("{secs}s")
 }
 
+/// Derive a display state string from outcome and dispatched_at.
+pub fn derive_run_state(outcome: Option<&str>, dispatched_at: Option<i64>) -> &'static str {
+    match outcome {
+        Some("succeeded") => "succeeded",
+        Some("superseded") => "superseded",
+        Some(_) => "failed",
+        None if dispatched_at.is_some() => "active",
+        None => "queued",
+    }
+}
+
 /// Map a CI run/job state string to a CSS colour class.
-///
-/// Centralised here so `RunListRow`, `DetailRun`, and `DetailJob`
-/// don't each carry their own identical match.
 pub fn state_class(state: &str) -> &'static str {
     match state {
         "succeeded" => "c-ok",
@@ -186,4 +194,15 @@ mod tests {
         assert_eq!(state_class("active"), "c-muted");
         assert_eq!(state_class(""), "c-muted");
     }
+
+    #[test]
+    fn derive_run_state_covers_all_outcomes() {
+        assert_eq!(derive_run_state(Some("succeeded"), Some(1)), "succeeded");
+        assert_eq!(derive_run_state(Some("superseded"), Some(1)), "superseded");
+        assert_eq!(derive_run_state(Some("failed-pipeline"), Some(1)), "failed");
+        assert_eq!(derive_run_state(Some("failed-orphaned"), Some(1)), "failed");
+        assert_eq!(derive_run_state(Some("failed-internal"), Some(1)), "failed");
+        assert_eq!(derive_run_state(None, Some(1)), "active");
+        assert_eq!(derive_run_state(None, None), "queued");
+    }
 }
diff --git a/quire-server/src/quire/web/templates.rs b/quire-server/src/quire/web/templates.rs
index 499667c..93e7f50 100644
--- a/quire-server/src/quire/web/templates.rs
+++ b/quire-server/src/quire/web/templates.rs
@@ -61,12 +61,7 @@ pub struct RunListRow {
 
 impl RunListRow {
     pub fn state(&self) -> &str {
-        match &self.outcome {
-            Some(o) if o.starts_with("failed") => "failed",
-            Some(o) => o.as_str(),
-            None if self.dispatched_at.is_some() => "active",
-            None => "queued",
-        }
+        format::derive_run_state(self.outcome.as_deref(), self.dispatched_at)
     }
 
     pub fn state_class(&self) -> &'static str {
@@ -123,12 +118,7 @@ pub struct DetailRun {
 
 impl DetailRun {
     pub fn state(&self) -> &str {
-        match &self.outcome {
-            Some(o) if o.starts_with("failed") => "failed",
-            Some(o) => o.as_str(),
-            None if self.dispatched_at.is_some() => "active",
-            None => "queued",
-        }
+        format::derive_run_state(self.outcome.as_deref(), self.dispatched_at)
     }
 
     pub fn state_class(&self) -> &'static str {