Back to feed

codex/skipped-lane-badge

sledtools/pika · branch #23 · target master · updated 2026-03-20 18:38:19

branch: merged tutorial: ready ci: success

CI

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.

merge commit df287ad8bd003261ecb5983fa8acd59af471ebad

Summary

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.

Tutorial Steps

Extend the CiLaneView struct with badge metadata

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

Evidence
@@ -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.

Introduce the lane_status_badge_class helper function

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

Evidence
@@ -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.

Wire the new fields into map_ci_lane_view

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

Evidence
@@ -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.

Update the live CI template to use badge spans

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

Evidence
@@ -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:

  1. <details> conditionals now reference lane.is_failed instead of comparing lane.status == "failed" inline, matching the Rust-side logic.
  2. Summary line wraps the status text in a <span class="lane-status-badge {{ lane.status_badge_class }}"> so it renders as a colored pill.
  3. Meta row is restructured from a <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).

Add CSS badge styles to the detail template

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

Evidence
@@ -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:

ClassColor scheme
.lane-status-badge (base)Pill shape, uppercase, 600 weight
.status-successGreen tint
.status-failedRed/maroon tint
.status-runningBlue tint
.status-queuedNeutral, uses CSS vars
.status-skippedGold/amber tint
.status-neutralNeutral fallback

The .lane-meta-row rule uses flexbox with wrapping so the badge and metadata text stack gracefully on narrow viewports.

Add integration test for skipped lane badge rendering

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

Evidence
@@ -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:

  1. Creates a temporary store and inserts a branch.
  2. Queues a CI run with a single lane, claims it, then finishes it with status "skipped".
  3. Renders the detail template via render_detail_template.
  4. Asserts that the rendered HTML contains exactly two occurrences of 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.

Diff