Show the repo section nav on CI pages
Assisted-by: Claude Sonnet 4.6 via Claude Code
change pnxyzzyzrtxtroomsskynwvrmqwpvpqt
commit 8235c79a546eaabe78ba971e6946cfd13e5c5b89
author Alpha Chen <alpha@kejadlen.dev>
date
parent wrlxlkny
diff --git a/quire-server/src/quire/web/handlers.rs b/quire-server/src/quire/web/handlers.rs
index 7fada0e..1d946b7 100644
--- a/quire-server/src/quire/web/handlers.rs
+++ b/quire-server/src/quire/web/handlers.rs
@@ -61,6 +61,7 @@ pub async fn repo_home(State(quire): State<Quire>, AxumPath(repo): AxumPath<Stri
         tags,
         recent_runs,
         recent_changes,
+        active_section: "readme".to_string(),
     };
     render(&tmpl)
 }
@@ -262,13 +263,18 @@ fn render_error(repo: String, status: StatusCode, title: &str, detail: String) -
 pub async fn run_list(State(quire): State<Quire>, AxumPath(repo): AxumPath<String>) -> Response {
     let repo_display = repo.trim_end_matches(".git").to_string();
     let repo_name = db::resolve_repo_name(&repo);
-    match quire.repo(&repo_name) {
-        Ok(r) if r.exists() => {}
+    let git_repo = match quire.repo(&repo_name) {
+        Ok(r) if r.exists() => r,
         _ => return StatusCode::NOT_FOUND.into_response(),
     };
 
     let q = quire.clone();
-    let runs = match tokio::task::spawn_blocking(move || db::load_runs(&q, &repo_name)).await {
+    let rn = repo_name.clone();
+    let runs_handle = tokio::task::spawn_blocking(move || db::load_runs(&q, &rn));
+    let refs_handle =
+        tokio::task::spawn_blocking(move || (read_bookmarks(&git_repo), read_tags(&git_repo)));
+
+    let runs = match runs_handle.await {
         Ok(Ok(r)) => r,
         Ok(Err(e)) => {
             tracing::error!(repo = %repo, error = &e as &(dyn std::error::Error + 'static), "failed to load runs");
@@ -284,6 +290,7 @@ pub async fn run_list(State(quire): State<Quire>, AxumPath(repo): AxumPath<Strin
             return StatusCode::INTERNAL_SERVER_ERROR.into_response();
         }
     };
+    let (bookmarks, tags) = refs_handle.await.unwrap_or_default();
 
     let template_runs: Vec<RunListRow> = runs
         .into_iter()
@@ -300,8 +307,11 @@ pub async fn run_list(State(quire): State<Quire>, AxumPath(repo): AxumPath<Strin
 
     let tmpl = RunListTemplate {
         repo: repo_display,
-        crumbs: vec![Crumb::new("ci")],
+        crumbs: vec![],
         runs: template_runs,
+        bookmarks,
+        tags,
+        active_section: "ci".to_string(),
     };
     render(&tmpl)
 }
@@ -312,14 +322,17 @@ pub async fn run_detail(
 ) -> Response {
     let repo_display = repo.trim_end_matches(".git").to_string();
     let repo_name = db::resolve_repo_name(&repo);
-    match quire.repo(&repo_name) {
-        Ok(r) if r.exists() => {}
+    let git_repo = match quire.repo(&repo_name) {
+        Ok(r) if r.exists() => r,
         _ => return StatusCode::NOT_FOUND.into_response(),
     };
     if !db::is_valid_run_id(&run_id) {
         return StatusCode::NOT_FOUND.into_response();
     }
 
+    let refs_handle =
+        tokio::task::spawn_blocking(move || (read_bookmarks(&git_repo), read_tags(&git_repo)));
+
     let q = quire.clone();
     let rn = repo_name.clone();
     let ri = run_id.clone();
@@ -438,6 +451,7 @@ pub async fn run_detail(
     }
 
     let quire_ci_log = quire_ci_log_handle.await.unwrap_or_default();
+    let (bookmarks, tags) = refs_handle.await.unwrap_or_default();
 
     let crumbs = vec![
         Crumb::with_href("ci", format!("/{}/ci", repo_display)),
@@ -449,6 +463,9 @@ pub async fn run_detail(
         run: detail_run,
         jobs: detail_jobs,
         quire_ci_log,
+        bookmarks,
+        tags,
+        active_section: "ci".to_string(),
     };
     render(&tmpl)
 }
diff --git a/quire-server/src/quire/web/templates.rs b/quire-server/src/quire/web/templates.rs
index f50651f..32b140b 100644
--- a/quire-server/src/quire/web/templates.rs
+++ b/quire-server/src/quire/web/templates.rs
@@ -41,6 +41,9 @@ pub struct RunListTemplate {
     pub repo: String,
     pub crumbs: Vec<Crumb>,
     pub runs: Vec<RunListRow>,
+    pub bookmarks: Vec<BookmarkRow>,
+    pub tags: Vec<TagRow>,
+    pub active_section: String,
 }
 
 impl RunListTemplate {
@@ -99,6 +102,9 @@ pub struct RunDetailTemplate {
     pub run: DetailRun,
     pub jobs: Vec<DetailJob>,
     pub quire_ci_log: String,
+    pub bookmarks: Vec<BookmarkRow>,
+    pub tags: Vec<TagRow>,
+    pub active_section: String,
 }
 
 impl RunDetailTemplate {
@@ -240,6 +246,7 @@ pub struct RepoHomeTemplate {
     pub tags: Vec<TagRow>,
     pub recent_runs: Vec<RunListRow>,
     pub recent_changes: Vec<ChangeRow>,
+    pub active_section: String,
 }
 
 impl RepoHomeTemplate {
diff --git a/quire-server/templates/_repo_section_nav.html b/quire-server/templates/_repo_section_nav.html
new file mode 100644
index 0000000..ba760aa
--- /dev/null
+++ b/quire-server/templates/_repo_section_nav.html
@@ -0,0 +1,8 @@
+<nav class="repo-section-nav">
+  <a class="section-link {% if active_section == "readme" %}section-link--active{% endif %}" href="/{{ repo }}">readme</a>
+  <a class="section-link {% if active_section == "tree" %}section-link--active{% endif %}" href="/{{ repo }}/tree">tree</a>
+  <a class="section-link {% if active_section == "log" %}section-link--active{% endif %}" href="/{{ repo }}/log">log</a>
+  <a class="section-link {% if active_section == "bookmarks" %}section-link--active{% endif %}" href="/{{ repo }}/bookmarks">bookmarks{% if !bookmarks.is_empty() %} <span class="section-count">{{ bookmarks.len() }}</span>{% endif %}</a>
+  <a class="section-link {% if active_section == "tags" %}section-link--active{% endif %}" href="/{{ repo }}/tags">tags{% if !tags.is_empty() %} <span class="section-count">{{ tags.len() }}</span>{% endif %}</a>
+  <a class="section-link {% if active_section == "ci" %}section-link--active{% endif %}" href="/{{ repo }}/ci">ci</a>
+</nav>
diff --git a/quire-server/templates/ci/run_detail.html b/quire-server/templates/ci/run_detail.html
index e5f1d79..ef736f6 100644
--- a/quire-server/templates/ci/run_detail.html
+++ b/quire-server/templates/ci/run_detail.html
@@ -6,7 +6,9 @@
 {% include "_nav.html" %}
 {% endblock %}
 
-{% block content %}
+{% block fullpage %}
+{% include "_repo_section_nav.html" %}
+<main class="page-main">
 <div class="ci-meta">
   <div class="ci-meta-primary">
     <span class="c-accent">{{ run.sha_short() }}</span>
@@ -52,4 +54,5 @@
   <pre class="ci-sh-log">{{ quire_ci_log }}</pre>
 </div>
 {% endif %}
+</main>
 {% endblock %}
diff --git a/quire-server/templates/ci/run_list.html b/quire-server/templates/ci/run_list.html
index 2908486..c6741ac 100644
--- a/quire-server/templates/ci/run_list.html
+++ b/quire-server/templates/ci/run_list.html
@@ -6,7 +6,9 @@
 {% include "_nav.html" %}
 {% endblock %}
 
-{% block content %}
+{% block fullpage %}
+{% include "_repo_section_nav.html" %}
+<main class="page-main">
 <h2 class="ci-heading">ci runs</h2>
 <table class="ci-table">
   <thead>
@@ -32,4 +34,5 @@
   {% endfor %}
   </tbody>
 </table>
+</main>
 {% endblock %}
diff --git a/quire-server/templates/repo_home.html b/quire-server/templates/repo_home.html
index ffa38f0..70ae532 100644
--- a/quire-server/templates/repo_home.html
+++ b/quire-server/templates/repo_home.html
@@ -54,14 +54,7 @@
 </header>
 
 {# ── Section nav ────────────────────────────────────────────── #}
-<nav class="repo-section-nav">
-  <a class="section-link section-link--active" href="/{{ repo }}">readme</a>
-  <a class="section-link" href="/{{ repo }}/tree">tree</a>
-  <a class="section-link" href="/{{ repo }}/log">log</a>
-  <a class="section-link" href="/{{ repo }}/bookmarks">bookmarks{% if !bookmarks.is_empty() %} <span class="section-count">{{ bookmarks.len() }}</span>{% endif %}</a>
-  <a class="section-link" href="/{{ repo }}/tags">tags{% if !tags.is_empty() %} <span class="section-count">{{ tags.len() }}</span>{% endif %}</a>
-  <a class="section-link" href="/{{ repo }}/ci">ci</a>
-</nav>
+{% include "_repo_section_nav.html" %}
 
 {# ── Two-column body ─────────────────────────────────────────── #}
 <div class="repo-body">