Run #28 · success · 0 lane(s)
head e9843058b09ae0335af37c3f291eaeac70599510 · queued 2026-03-20 18:37:28
finished 2026-03-20 18:37:28
No lanes were selected for this branch head.
Run #28 · success · 0 lane(s)
head e9843058b09ae0335af37c3f291eaeac70599510 · queued 2026-03-20 18:37:28
finished 2026-03-20 18:37:28
No lanes were selected for this branch head.
This branch adds visual status badges to CI lane displays in the Pika News branch detail and live CI views. Previously, lane status was rendered as plain text; now each lane receives a color-coded pill badge (success, failed, running, queued, skipped, or neutral) in both the summary line and an expanded metadata row. The implementation spans three layers: a new Rust helper function maps status strings to CSS class names, the view-model struct gains precomputed badge fields, the Askama HTML templates consume those fields to render styled <span> badges, and new CSS rules define the six badge color variants. A comprehensive integration test verifies that the skipped badge renders correctly in both locations.
Intent: Give the template layer precomputed values for the CSS class and the failed-state flag so that status logic stays in Rust rather than in template conditionals.
Affected files: crates/pika-news/src/web.rs
@@ -395,6 +395,8 @@ struct CiLaneView {
title: String,
entrypoint: String,
status: String,
+ status_badge_class: String,
+ is_failed: bool,
Two new fields are added to the CiLaneView struct at crates/pika-news/src/web.rs:398-399:
status_badge_class: String – holds the CSS class name (e.g. status-skipped) that the template will inject into badge <span> elements.is_failed: bool – replaces the previous inline lane.status == "failed" comparisons in the templates, centralising the check in Rust.By computing these values once during mapping, the templates stay declarative and free of string-comparison logic.
Intent: Provide a single source of truth that translates a lane status string into the corresponding CSS class name used by the badge styles.
Affected files: crates/pika-news/src/web.rs
@@ -1831,6 +1837,17 @@ fn map_nightly_lane_view(lane: NightlyLaneRecord) -> NightlyLaneView {
+fn lane_status_badge_class(status: &str) -> &'static str {
+ match status {
+ "failed" => "status-failed",
+ "success" => "status-success",
+ "running" => "status-running",
+ "queued" => "status-queued",
+ "skipped" => "status-skipped",
+ _ => "status-neutral",
+ }
+}
A new private function lane_status_badge_class is added at crates/pika-news/src/web.rs:1837. It pattern-matches against the five known lane statuses and returns a &'static str CSS class. Any unrecognised status falls through to status-neutral, keeping the UI safe against future status values.
Returning &'static str avoids allocation; the caller (in map_ci_lane_view) converts to an owned String only when populating the view struct.
Intent: Populate the two new CiLaneView fields during the mapping step so they are available when the template renders.
Affected files: crates/pika-news/src/web.rs
@@ -1796,12 +1798,16 @@ fn map_ci_run_view(run: BranchCiRunRecord) -> CiRunView {
fn map_ci_lane_view(lane: BranchCiLaneRecord) -> CiLaneView {
+ let status_badge_class = lane_status_badge_class(&lane.status).to_string();
+ let is_failed = lane.status == "failed";
CiLaneView {
...
+ status_badge_class,
+ is_failed,
Inside map_ci_lane_view at crates/pika-news/src/web.rs:1800-1801, the badge class is resolved via lane_status_badge_class and the failed flag is set by a simple equality check. Both values are then passed into the CiLaneView constructor alongside the existing fields.
This keeps mapping logic co-located: every field the template needs is computed in one place.
Intent: Replace plain-text status rendering with styled badge spans and use the precomputed is_failed flag for conditional styling.
Affected files: crates/pika-news/templates/branch_ci_live.html
@@ -40,10 +40,17 @@
- <details class="lane-details {% if lane.status == "failed" %}lane-failed{% endif %}" {% if lane.status == "failed" %}open{% endif %}>
- <summary>Lane #{{ lane.id }} · {{ lane.title }} · {{ lane.status }} · <code>{{ lane.entrypoint }}</code></summary>
- <p class="muted">lane id <code>{{ lane.lane_id }}</code> · retries {{ lane.retry_count }} · queued {{ lane.created_at }}</p>
- {% if lane.status == "failed" %}
+ <details class="lane-details {% if lane.is_failed %}lane-failed{% endif %}" {% if lane.is_failed %}open{% endif %}>
+ <summary>
+ Lane #{{ lane.id }} · {{ lane.title }} ·
+ <span class="lane-status-badge {{ lane.status_badge_class }}">{{ lane.status }}</span>
+ · <code>{{ lane.entrypoint }}</code>
+ </summary>
+ <div class="lane-meta-row">
+ <span class="lane-status-badge {{ lane.status_badge_class }}">{{ lane.status }}</span>
+ <span class="muted">lane id <code>{{ lane.lane_id }}</code> · retries {{ lane.retry_count }} · queued {{ lane.created_at }}</span>
+ </div>
+ {% if lane.is_failed %}
Three changes in crates/pika-news/templates/branch_ci_live.html:
<details> conditionals now reference lane.is_failed instead of comparing lane.status == "failed" inline, matching the Rust-side logic.<span class="lane-status-badge {{ lane.status_badge_class }}"> so it renders as a colored pill.<p> to a flex <div class="lane-meta-row"> containing a second badge span and the existing metadata, giving a cleaner two-badge layout (one collapsed, one expanded).Intent: Define the visual appearance of the six badge variants so that each lane status is immediately distinguishable by color.
Affected files: crates/pika-news/templates/detail.html
@@ -47,6 +47,14 @@
+ .lane-meta-row { display: flex; gap: 0.55rem; align-items: center; flex-wrap: wrap; margin-top: 0.55rem; }
+ .lane-status-badge { display: inline-flex; align-items: center; border-radius: 999px; border: 1px solid transparent; padding: 0.12rem 0.55rem; font-size: 0.76rem; font-weight: 600; line-height: 1.2; text-transform: uppercase; letter-spacing: 0.03em; }
+ .lane-status-badge.status-success { background: rgba(40, 167, 69, 0.12); border-color: rgba(40, 167, 69, 0.35); color: #1f7a3d; }
+ .lane-status-badge.status-failed { background: rgba(184, 71, 91, 0.14); border-color: rgba(184, 71, 91, 0.38); color: #8b1e3f; }
+ .lane-status-badge.status-running { background: rgba(15, 76, 129, 0.12); border-color: rgba(15, 76, 129, 0.34); color: #0f4c81; }
+ .lane-status-badge.status-queued { background: var(--badge-bg); border-color: var(--border-light); color: var(--text-secondary); }
+ .lane-status-badge.status-skipped { background: rgba(217, 164, 65, 0.14); border-color: rgba(217, 164, 65, 0.36); color: #8c6112; }
+ .lane-status-badge.status-neutral { background: var(--badge-bg); border-color: var(--border-light); color: var(--text-secondary); }
Eight new CSS rules are added to crates/pika-news/templates/detail.html:50-57:
| Class | Color scheme |
|---|---|
.lane-status-badge (base) | Pill shape, uppercase, 600 weight |
.status-success | Green tint |
.status-failed | Red/maroon tint |
.status-running | Blue tint |
.status-queued | Neutral, uses CSS vars |
.status-skipped | Gold/amber tint |
.status-neutral | Neutral fallback |
The .lane-meta-row rule uses flexbox with wrapping so the badge and metadata text stack gracefully on narrow viewports.
Intent: Verify end-to-end that a lane finished with status 'skipped' produces the expected badge markup in both the summary and expanded body.
Affected files: crates/pika-news/src/web.rs
@@ -5292,6 +5309,72 @@
+ #[test]
+ fn branch_detail_renders_skipped_lane_badges_in_summary_and_body() {
+ ...
+ assert_eq!(
+ rendered
+ .matches("lane-status-badge status-skipped\">skipped</span>")
+ .count(),
+ 2
+ );
+ }
A new test branch_detail_renders_skipped_lane_badges_in_summary_and_body at crates/pika-news/src/web.rs:5312 exercises the full pipeline:
"skipped".render_detail_template.lane-status-badge status-skipped">skipped</span> — one inside the <summary> and one inside the .lane-meta-row.This count-based assertion catches regressions where either badge location is accidentally removed or duplicated.