This branch refactors the pika-news forge UI to separate CI details into a dedicated page while embedding a compact CI summary on the branch detail view. It introduces a new /news/branch/:id/ci route and full-page template (branch_ci.html) with its own SSE live-update stream, adds a lightweight branch_ci_summary.html partial that replaces the inline CI panel on the detail page, normalizes status styling through a unified ci_status_tone helper, resolves CLI path handling so relative config/db paths work correctly under cargo watch, adds a scripts/news wrapper for local hosted-mode development, and updates documentation and example config accordingly.
Tutorial Steps
Add path resolution helpers to CLI args
Intent: When running under `cargo watch` or from a wrapper script, the working directory at invocation may differ from the directory at execution time. This step adds `resolved_config_path` and `resolved_db_path` methods that absolutize relative paths against the captured `cwd`, plus a `current_dir()` helper and unit tests.
Two new public methods on ServeArgs delegate to a private absolutize_path function that joins relative paths against a supplied cwd. A standalone current_dir() wraps env::current_dir with an anyhow error conversion.
The unit tests at cli.rs:82-129 cover both the relative-join and absolute-passthrough cases:
let args = ServeArgs {
config: PathBuf::from("pika-news.toml"),
db: PathBuf::from(".tmp/pika-news.db"),
...
};
let cwd = PathBuf::from("/tmp/pika-news-dev");
assert_eq!(args.resolved_config_path(&cwd),
PathBuf::from("/tmp/pika-news-dev/pika-news.toml"));
Use resolved paths and auto-create DB parent directory in main
Intent: Wire up the new CLI resolution so `serve` always operates on absolute paths and creates any missing parent directory for the SQLite database before opening it.
Affected files: crates/pika-news/src/main.rs
Evidence
@@ -20,14 +20,24 @@
+use std::fs;
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Serve(args) => {
- let config = config::load(&args.config).context("load config file")?;
- let store = storage::Store::open(&args.db).context("initialize sqlite storage")?;
+ let cwd = cli::current_dir().context("resolve current working directory")?;
+ let config_path = args.resolved_config_path(&cwd);
+ let db_path = args.resolved_db_path(&cwd);
+ if let Some(parent) = db_path.parent() {
+ fs::create_dir_all(parent)
Before this change, relative paths like .tmp/pika-news.db would silently resolve against whatever the process's current directory happened to be at runtime—a problem when cargo watch re-invokes the binary. Now main captures cwd once, absolutizes both paths, and ensures the DB parent directory exists via fs::create_dir_all. This is critical for the scripts/news local-dev wrapper that stores state in .tmp/.
Introduce the `ci_status_tone` unified status-styling helper
Intent: Replace scattered per-lane `lane_status_badge_class` and `is_failed` booleans with a single `ci_status_tone` function that maps any CI status string to one of four semantic tones: `success`, `danger`, `warning`, or `neutral`.
@@ -1855,8 +2274,6 @@ fn map_ci_lane_view
- let status_badge_class = lane_status_badge_class(&lane.status).to_string();
- let is_failed = lane.status == "failed";
+ let status_tone = ci_status_tone(&lane.status).to_string();
The new ci_status_tone function centralizes the status → visual-tone mapping that was previously duplicated across badge classes and boolean flags. View structs like CiRunView, CiLaneView, CiSummaryRunView, and CiSummaryLaneView all carry a status_tone: String field that templates consume via CSS classes like .tone-success, .tone-danger, etc.
The old status_badge_class and is_failed fields are removed from CiLaneView, and the ci_status_tone value is also propagated into BranchCiLiveTemplate and BranchCiSummaryTemplate for overall run status styling.
Add ReviewModeQuery with boolish deserializer
Intent: Allow the `?review=` query parameter to accept `true`, `1`, `false`, `0`, or be omitted entirely, so links from the inbox review flow correctly propagate context through the CI detail page.
The custom deserialize_boolish visitor handles bool, i64/u64 (0/1), and string variants ("true", "1", etc.). This matters because query strings are inherently untyped—different clients may send ?review=1 vs ?review=true. The ReviewModeQuery struct defaults review to false when the parameter is absent.
A dedicated test at the bottom of the file verifies all three cases:
let numeric = Query::<ReviewModeQuery>::try_from_uri(
&"/news/branch/7/ci?review=1".parse().unwrap()
).unwrap();
assert!(numeric.0.review);
Create the dedicated CI detail page route and template
Intent: Move the full CI run history, lane details, log output, and rerun/recovery controls out of the branch detail page into a standalone `/news/branch/:id/ci` page with its own handler, template, and SSE stream.
branch_ci_page_handler loads the branch detail record and CI runs, then renders BranchCiTemplate. The template (branch_ci.html) is a full page that:
Shows a hero section with branch name, title, head SHA, merge base, and state badges.
Embeds the branch_ci_live_html partial (the existing full CI run list) inside a #branch-ci-live div.
Connects to /news/branch/:id/ci/stream/full for live SSE updates of the full CI panel.
Includes all rerun/recovery button wiring and review-mode keyboard navigation ([, ], d).
Provides a back link that respects review mode: either /news/inbox/review/:id or /news/branch/:id.
The render_branch_ci_template_with_notices function constructs the template struct, reusing the existing render_branch_ci_live_html for the embedded partial.
Replace inline CI panel with compact CI summary on branch detail
Intent: The branch detail page should show only a compact CI summary with a link to the full CI page, rather than embedding the entire run history inline.
The DetailTemplate struct swaps branch_ci_live_html/branch_ci_live_enabled for branch_ci_summary_html/branch_ci_summary_enabled. The new render_branch_ci_summary_html function builds a BranchCiSummaryTemplate that shows:
The overall CI status badge with tone styling.
A compact view of the latest run: lane count, success/active/failed counts, and per-lane status dots.
An "Open CI Details" link pointing to the new /news/branch/:id/ci page (preserving ?review=true when applicable).
The detail page's SSE stream endpoint (/news/branch/:id/ci/stream) is also updated: it now serves the summary partial via load_branch_ci_summary_snapshot rather than the full CI panel. A new /news/branch/:id/ci/stream/full endpoint serves the full panel for the dedicated CI page.
Add summary and full SSE stream endpoints
Intent: Provide two distinct Server-Sent Events streams: a lightweight summary stream for the detail page and a full stream for the dedicated CI page.
The existing branch_ci_stream_handler is repurposed to serve summary snapshots by calling load_branch_ci_summary_snapshot and next_branch_ci_summary_snapshot. It now also accepts ReviewModeQuery so the summary partial can include the correct CI details link.
The new branch_ci_full_stream_handler keeps the original behavior: it serves full CI live snapshots via load_branch_ci_live_snapshot and next_branch_ci_live_snapshot. Both streams use the same CiLiveUpdate broadcast channel and follow the same unfold pattern—emit the initial snapshot, then loop on broadcast updates, closing the stream when the CI runs become inactive.
Update branch_ci_live.html template with tone-based styling
Intent: Modernize the full CI run list partial to use the new `ci_status_tone` classes instead of the old badge-class approach, and restructure the layout with overview and history sections.
Status badges use .tone-success, .tone-warning, .tone-danger, .tone-neutral classes derived from ci_status_tone.
The status row is moved into the overview card header.
Lane details use <details> elements with status_tone-based styling.
Recovery and rerun buttons remain present but are wired via the parent page's JavaScript.
The ci: status label is capitalized to CI: status (reflected in updated test assertions).
Add URL path helper functions for navigation consistency
Intent: Centralize the construction of branch detail and CI page paths so that review-mode query parameters are consistently applied across templates, SSE payloads, and back-links.
Two helper functions ensure consistent URL generation:
branch_ci_page_path produces the CI detail page link, appending ?review=true when in review mode.
branch_detail_path produces either the inbox review URL or the standard branch URL.
These are used by BranchCiTemplate.back_href, BranchCiSummaryTemplate.ci_details_path, and the test that verifies review-mode links are correctly preserved (review_mode_ci_links_preserve_inbox_context).
Add CI lane counting helper and summary run view
Intent: Provide a compact summary representation of CI runs that includes success/active/failed lane counts for the summary partial.
Affected files: crates/pika-news/src/web.rs
Evidence
@@ +690,0 @@
+fn ci_lane_counts(run: &BranchCiRunRecord) -> (usize, usize, usize) {
+ let mut success_count = 0;
+ let mut active_count = 0;
+ let mut failed_count = 0;
+ for lane in &run.lanes {
+ match ci_status_tone(&lane.status) {
ci_lane_counts iterates over a run's lanes and buckets them by tone into success, active (warning), and failed (danger) counts. map_ci_summary_run uses this to build a CiSummaryRunView with aggregate counts plus a vec of CiSummaryLaneView items (title, status, tone). The summary template uses these counts to show e.g. "3 passed · 1 running · 0 failed" in a compact format.
Propagate additional fields to DetailTemplate
Intent: Expose branch title, head SHA, merge base SHA, tutorial status, and CI status on the detail page so the hero section and summary partial have the data they need.
The DetailTemplate struct gains title, tutorial_status, ci_status, head_sha, and merge_base_sha fields. These are populated from the BranchDetailRecord in render_detail_template_with_notices. Previously these were only available inside the embedded CI live partial; now the detail page template can display them directly in its own hero/metadata section.
Restructure config documentation and example TOML
Intent: Move the `[forge_repo]` table after the flat keys in both the README and example config to match TOML best practices (inline keys before tables) and fix the webhook port to match the default `bind_port`.
The [forge_repo] TOML table is moved below all flat keys (repos, poll_interval_secs, bind_address, bind_port, etc.) in both the README code block and pika-news.example.toml. This follows the TOML specification recommendation that inline key/value pairs appear before table headers.
The hook_url port is also corrected from 8788 to 8787 in the README to match the default bind_port.
Add local development wrapper script and documentation
Intent: Provide a one-command local dev experience that mirrors the hosted forge UI for contributors who want to iterate on templates and CI views without deploying.
@@ -70,3 +71,21 @@
+## Local hosted dev
+
+If you want the forge-style web UI locally, use:
+
+```bash
+./scripts/news
+```
+
+That wrapper:
+
+- creates a local hosted-mode config under `.tmp/`
+- points the forge at this repo's shared git dir
+- stores sqlite state at `.tmp/pika-news.db`
+- exports `GITHUB_TOKEN` from `gh auth token`
+- runs `pika-news serve` under `cargo watch`
A new scripts/news shell script automates local hosted-mode development. The README documents its behavior: it generates a temporary config under .tmp/, sets canonical_git_dir to the repo's own .git directory, exports GITHUB_TOKEN via gh auth token, and launches cargo watch to rebuild and restart on changes.
The default bind address is 127.0.0.1:8787, overridable via PIKA_NEWS_PORT.
Update tests for the detail/CI page split
Intent: Adjust existing tests to reflect that CI run details (lane logs, rerun links, pikaci metadata) now render on the CI page rather than the detail page, and add new tests for review-mode link propagation and boolish query parsing.
Affected files: crates/pika-news/src/web.rs
Evidence
@@ -5865,12 +6311,22 @@
- let template =
- render_detail_template(detail, ci_runs, false).expect("render detail template");
- let rendered = template.render().expect("render html");
- assert!(rendered.contains("branch-ci-ok"));
+ let detail_rendered = render_detail_template(detail.clone(), ci_runs.clone(), false)
+ assert!(detail_rendered.contains("Open CI Details"));
+ assert!(!detail_rendered.contains("branch-ci-ok"));
branch-ci-ok (a CSS class from the full CI panel) no longer appears on the detail page but does appear on the CI page.
The detail page now shows "Open CI Details" linking to the CI page.
Assertions for "manual rerun of lane #", "pikaci run", and lane status badges are moved to render_branch_ci_template_with_notices assertions.
"ci: success" assertions are updated to "CI: success" matching the new capitalization.
New test review_mode_ci_links_preserve_inbox_context verifies that detail → CI links include ?review=true and CI → detail back-links use /news/inbox/review/:id.
New test review_mode_query_accepts_numeric_and_text_bools exercises the deserialize_boolish visitor with 1, true, and missing values.