Make ci breadcrumb link back to the run list
Replaced the flat Vec<String> crumbs with a Vec<Crumb> where each entry
carries an optional href. The ci crumb on the run detail page now links
to /<repo>/ci; the sha crumb and other leaf crumbs stay as plain text.

Assisted-by: GLM-5.1 via pi
change lyvmuztuwmwmwrnqknqxzmovmpqooqmn
commit 2049368b88b698d7e845559fa39e02d8b1edf041
author Alpha Chen <alpha@kejadlen.dev>
date
parent mrvzwlnw
diff --git a/src/quire/web/handlers.rs b/src/quire/web/handlers.rs
index 69f0323..3672049 100644
--- a/src/quire/web/handlers.rs
+++ b/src/quire/web/handlers.rs
@@ -43,7 +43,7 @@ async fn read_log(path: &std::path::Path) -> String {
 fn render_error(repo: String, status: StatusCode, title: &str, detail: String) -> Response {
     let tmpl = ErrorTemplate {
         repo,
-        crumbs: vec!["error".to_string()],
+        crumbs: vec![Crumb::new("error")],
         title: title.to_string(),
         detail: detail.clone(),
     };
@@ -91,7 +91,7 @@ pub async fn run_list(State(quire): State<Quire>, AxumPath(repo): AxumPath<Strin
 
     let tmpl = RunListTemplate {
         repo: repo_display,
-        crumbs: vec!["ci".to_string()],
+        crumbs: vec![Crumb::new("ci")],
         runs: template_runs,
     };
     render(&tmpl)
@@ -178,7 +178,10 @@ pub async fn run_detail(
         });
     }
 
-    let crumbs = vec!["ci".to_string(), detail_run.sha_short().to_string()];
+    let crumbs = vec![
+        Crumb::with_href("ci", format!("/{}/ci", repo_display)),
+        Crumb::new(detail_run.sha_short()),
+    ];
     let tmpl = RunDetailTemplate {
         repo: repo_display,
         crumbs,
diff --git a/src/quire/web/templates.rs b/src/quire/web/templates.rs
index 84300e4..6aa337e 100644
--- a/src/quire/web/templates.rs
+++ b/src/quire/web/templates.rs
@@ -9,13 +9,37 @@ fn pkg_version() -> &'static str {
     env!("CARGO_PKG_VERSION")
 }
 
+/// A navigation breadcrumb entry.
+///
+/// When `href` is `Some`, the crumb renders as a clickable link.
+pub struct Crumb {
+    pub label: String,
+    pub href: Option<String>,
+}
+
+impl Crumb {
+    pub fn new(label: impl Into<String>) -> Self {
+        Self {
+            label: label.into(),
+            href: None,
+        }
+    }
+
+    pub fn with_href(label: impl Into<String>, href: impl Into<String>) -> Self {
+        Self {
+            label: label.into(),
+            href: Some(href.into()),
+        }
+    }
+}
+
 // ── Run list ───────────────────────────────────────────────────────
 
 #[derive(Template)]
 #[template(path = "ci/run_list.html")]
 pub struct RunListTemplate {
     pub repo: String,
-    pub crumbs: Vec<String>,
+    pub crumbs: Vec<Crumb>,
     pub runs: Vec<RunListRow>,
 }
 
@@ -67,7 +91,7 @@ impl RunListRow {
 #[template(path = "ci/run_detail.html")]
 pub struct RunDetailTemplate {
     pub repo: String,
-    pub crumbs: Vec<String>,
+    pub crumbs: Vec<Crumb>,
     pub run: DetailRun,
     pub jobs: Vec<DetailJob>,
 }
@@ -194,7 +218,7 @@ impl DetailShEvent {
 #[template(path = "error.html")]
 pub struct ErrorTemplate {
     pub repo: String,
-    pub crumbs: Vec<String>,
+    pub crumbs: Vec<Crumb>,
     pub title: String,
     pub detail: String,
 }
diff --git a/templates/_css.html b/templates/_css.html
index 1274e6a..e155e8f 100644
--- a/templates/_css.html
+++ b/templates/_css.html
@@ -44,6 +44,8 @@ pre { white-space: pre-wrap; word-break: break-word; }
 .page-nav .nav-repo { color: var(--ink); text-decoration: none; display: inline-flex; align-items: center; gap: 6px; }
 .page-nav .q-mark { color: var(--ink); display: block; }
 .page-nav .nav-crumb { color: var(--muted); }
+.page-nav a.nav-crumb { text-decoration: none; }
+.page-nav a.nav-crumb:hover { color: var(--accent); }
 .page-nav .sep { color: var(--rule2); }
 
 .page-main { padding: 22px 56px 32px; }
diff --git a/templates/_nav.html b/templates/_nav.html
index 0e65aad..3cc725a 100644
--- a/templates/_nav.html
+++ b/templates/_nav.html
@@ -14,7 +14,11 @@
     <a class="nav-repo" href="/{{ repo }}/ci">{{ repo }}</a>
     {% for crumb in crumbs %}
     <span class="sep">/</span>
-    <span class="nav-crumb">{{ crumb }}</span>
+    {% if let Some(href) = crumb.href %}
+    <a class="nav-crumb" href="{{ href }}">{{ crumb.label }}</a>
+    {% else %}
+    <span class="nav-crumb">{{ crumb.label }}</span>
+    {% endif %}
     {% endfor %}
   </div>
 </nav>