Back to feed

sledtools/pika branch #124

pika-git-web-split-5

Split pika-git web module

Target branch: master

Merge Commit: c9b8943993a7d03824bc8ff8313be3b69c3dd437

branch: merged tutorial: ready ci: failed
Open CI Details

Continuous Integration

CI: failed

Compact status on the review page, with full logs on the CI page.

Open CI Details

Latest run #151 failed

2 failed

head 072f102c95020f8021d50d2f546be65a8f3783f4 · queued 2026-03-26 23:08:19 · 2 lane(s)

queued 12s · ran 28s

check-notifications · failed check-agent-contracts · failed

Summary

This branch decomposes a monolithic ~6,800-line web.rs file in the pika-git crate into eight focused submodules (views, pages, live, api, auth, chat, webhook, admin) plus a parallel split of the test suite into five submodules (api, live, render, unit, webhook_inbox). The refactor uses Rust's include!() macro so every extracted function is still compiled inside the original web module's namespace, meaning no visibility changes, no import rewiring, and zero functional differences at runtime. The result is a codebase where each concern—page rendering, SSE live updates, JSON API endpoints, authentication, chat, webhooks, and admin operations—lives in its own file, making navigation, review, and future modification substantially easier.

Tutorial Steps

Gut the monolith: remove ~6,050 lines from web.rs

Intent: Strip all handler functions, view-mapping logic, live-streaming infrastructure, API endpoints, auth helpers, chat handlers, webhook processing, and admin handlers out of the single `web.rs` file, replacing them with `include!()` directives that textually re-insert the code from new submodule files at compile time.

Affected files: crates/pika-git/src/web.rs

Evidence
@@ -786,6076 +786,25 @@ pub async fn serve(
-async fn feed_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse {
+include!("web/views.rs");
+include!("web/pages.rs");
+include!("web/live.rs");
+include!("web/api.rs");
+include!("web/auth.rs");
+include!("web/chat.rs");
+include!("web/webhook.rs");
+include!("web/admin.rs");
+async fn shutdown_signal() {
+#[cfg(test)]
+mod tests;

The original web.rs contained everything: page handlers, view mappers, SSE streams, REST endpoints, auth, chat, webhooks, and admin routes—all in a single file exceeding 6,800 lines.

After this change, web.rs retains only:

  • Imports and type/struct definitions (~785 lines)
  • The serve() function that builds the Axum router
  • Eight include!() calls that textually embed the extracted submodules
  • The shutdown_signal() helper
  • The #[cfg(test)] mod tests; declaration

Using include!() rather than proper mod declarations is a deliberate choice: it keeps every extracted function inside the web module's flat namespace, so no item needs pub visibility changes and no call-site needs updating. This makes the split a pure file-organization refactor with zero semantic impact.

Extract view-mapping and rendering helpers into views.rs

Intent: Isolate the ~530 lines of code responsible for transforming domain records (`BranchDetailRecord`, `BranchCiRunRecord`, `NightlyRunRecord`, etc.) into template view structs and rendered HTML strings.

Affected files: crates/pika-git/src/web/views.rs

Evidence
+fn map_feed_item(item: BranchFeedItem) -> FeedItemView {
+fn map_nightly_feed_item(item: NightlyFeedItem) -> NightlyFeedItemView {
+fn render_detail_template_with_notices(
+fn render_branch_ci_template_with_notices(
+fn render_branch_ci_live_html(
+fn render_branch_ci_summary_html(
+fn render_nightly_live_html(
+fn map_ci_run_view(run: BranchCiRunRecord, now: DateTime<Utc>) -> CiRunView {
+fn map_ci_lane_view(lane: BranchCiLaneRecord, now: DateTime<Utc>) -> CiLaneView {
+fn map_ci_summary_run(run: &BranchCiRunRecord, now: DateTime<Utc>) -> CiSummaryRunView {
+fn map_nightly_lane_view(lane: NightlyLaneRecord) -> NightlyLaneView {
+fn map_api_ci_run(run: BranchCiRunRecord, now: DateTime<Utc>) -> CiRun {
+fn map_api_ci_lane(lane: BranchCiLaneRecord, now: DateTime<Utc>) -> CiLane {
+fn map_api_nightly_lane(lane: NightlyLaneRecord) -> CiLane {
+fn lane_operator_hint(context: &LaneHintContext<'_>) -> Option<String> {
+fn lane_target_health_summary(
+fn ci_timing_summary(
+fn format_compact_duration(duration: TimeDelta) -> String {
+fn select_branch_log_lane(

This file becomes the single source of truth for how raw data becomes displayable content. It contains:

  • Feed mappers: map_feed_item, map_nightly_feed_item — convert store records into feed-page view structs.
  • Detail/CI template renderers: render_detail_template_with_notices, render_branch_ci_template_with_notices, render_branch_ci_live_html, render_branch_ci_summary_html, render_nightly_live_html — build Askama template structs and render them to HTML.
  • CI run/lane view mappers: map_ci_run_view, map_ci_lane_view, map_ci_summary_run, map_nightly_lane_view — used by both HTML templates and JSON API responses.
  • API-specific adapters: map_api_ci_run, map_api_ci_lane, map_api_nightly_lane, map_forge_branch_summary — wrap view structs into API response types.
  • Utility functions: ci_timing_summary, format_compact_duration, lane_operator_hint, lane_target_health_summary, lane_status_badge_class, parse_ci_timestamp, select_branch_log_lane.
  • Active-state predicates: branch_ci_runs_are_active, nightly_run_is_active.

Grouping these together means any change to how data is presented—whether a new CI status tone, a different timing format, or an extra operator hint—touches only this file.

Extract HTML page handlers into pages.rs

Intent: Move the Axum handlers that serve full HTML pages (feed, branch detail, nightly detail, branch CI page) into a dedicated file.

Affected files: crates/pika-git/src/web/pages.rs

Evidence
+async fn feed_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse {
+async fn nightly_handler(
+async fn detail_handler(
+async fn branch_ci_page_handler(
+async fn inbox_review_handler(
+async fn detail_page(

This ~155-line file contains every handler that returns a full HTML page:

HandlerRoute purpose
feed_handlerMain feed listing open branches and nightly runs
detail_handlerBranch detail page (tutorial, diff, CI summary)
inbox_review_handlerSame detail page in review mode
branch_ci_page_handlerDedicated CI-runs page for a branch
nightly_handlerNightly run detail page
detail_pageShared helper used by detail_handler and inbox_review_handler

Each handler follows the same pattern: fetch data via forge_service, call a render_* function from views.rs, and return Html(rendered) or an error response.

Extract SSE live-update infrastructure into live.rs

Intent: Consolidate the Server-Sent Events streaming logic—snapshot loading, broadcast-receiver loops, and SSE handler endpoints—into one file.

Affected files: crates/pika-git/src/web/live.rs

Evidence
+struct BranchCiLiveSnapshot {
+struct BranchCiSummarySnapshot {
+struct NightlyLiveSnapshot {
+async fn load_branch_ci_summary_snapshot(
+async fn load_branch_ci_live_snapshot(
+async fn load_nightly_live_snapshot(
+fn live_html_event(html: String) -> Result<Event, Infallible> {
+fn branch_live_update_error_html(
+fn nightly_live_update_error_html(
+async fn next_branch_ci_summary_snapshot(
+async fn next_branch_ci_live_snapshot(
+async fn next_nightly_live_snapshot(
+async fn branch_ci_stream_handler(
+async fn branch_ci_full_stream_handler(
+async fn nightly_stream_handler(

At ~270 lines, live.rs encapsulates the real-time update system:

  1. Snapshot structs (BranchCiLiveSnapshot, BranchCiSummarySnapshot, NightlyLiveSnapshot) hold pre-rendered HTML plus an active flag that controls whether the SSE stream stays open.
  2. Loaders (load_branch_ci_summary_snapshot, load_branch_ci_live_snapshot, load_nightly_live_snapshot) fetch data and render a snapshot, returning Option for not-found cases.
  3. Broadcast listeners (next_branch_ci_summary_snapshot, next_branch_ci_live_snapshot, next_nightly_live_snapshot) loop on a tokio::sync::broadcast::Receiver<CiLiveUpdate>, filtering for the relevant branch/nightly ID and reloading a snapshot on match or lag.
  4. SSE handlers (branch_ci_stream_handler, branch_ci_full_stream_handler, nightly_stream_handler) compose the above into Sse::new(stream) responses with 15-second keep-alive.
  5. Error formatters (branch_live_update_error_html, nightly_live_update_error_html) produce graceful degradation HTML when a live reload fails.

Extract JSON API endpoints into api.rs

Intent: Move all REST/JSON API handlers—branch CRUD, CI operations, nightly operations, and query endpoints—into their own file.

Affected files: crates/pika-git/src/web/api.rs

Evidence
+async fn merge_handler(
+async fn close_handler(
+async fn rerun_branch_ci_lane_handler(
+async fn fail_branch_ci_lane_handler(
+async fn requeue_branch_ci_lane_handler(
+async fn recover_branch_ci_run_handler(
+async fn rerun_nightly_lane_handler(
+async fn fail_nightly_lane_handler(
+async fn requeue_nightly_lane_handler(
+async fn recover_nightly_run_handler(
+async fn api_branch_detail_handler(
+async fn api_branch_resolve_handler(
+async fn api_branch_logs_handler(
+async fn api_branch_ci_runs_handler(
+async fn api_nightly_detail_handler(
+async fn api_nightly_lanes_handler(
+async fn api_trigger_nightly_handler(
+struct ForgeBranchResolveQuery {
+struct ForgeBranchLogsQuery {

The largest extracted file at ~540 lines, api.rs groups every JSON-returning endpoint:

Mutation endpoints (all require require_trusted_auth):

  • merge_handler, close_handler — branch lifecycle
  • rerun_branch_ci_lane_handler, fail_branch_ci_lane_handler, requeue_branch_ci_lane_handler, recover_branch_ci_run_handler — branch CI operations
  • rerun_nightly_lane_handler, fail_nightly_lane_handler, requeue_nightly_lane_handler, recover_nightly_run_handler — nightly CI operations
  • api_trigger_nightly_handler — scheduling trigger

Query endpoints:

  • api_branch_detail_handler, api_branch_resolve_handler — branch lookup
  • api_branch_logs_handler — lane log retrieval
  • api_branch_ci_runs_handler — CI run listing
  • api_nightly_detail_handler, api_nightly_lanes_handler — nightly data

Also includes the request query structs ForgeBranchResolveQuery and ForgeBranchLogsQuery.

Extract authentication helpers into auth.rs

Intent: Separate the cross-cutting authentication and error-mapping concerns that are used by both API and page handlers.

Affected files: crates/pika-git/src/web/auth.rs

Evidence
+fn require_trusted_auth(
+fn require_admin_auth(
+fn forge_service_json_error(
+fn map_forge_service_error(
+struct ReviewModeQuery {

At ~85 lines, this is the smallest extracted module. It contains:

  • require_trusted_auth — extracts an auth token from request headers and validates it against the configured AuthConfig, returning the caller's npub or an error response.
  • require_admin_auth — stricter variant requiring admin-level privileges.
  • forge_service_json_error — converts a ForgeServiceError into a JSON error response with appropriate HTTP status code.
  • map_forge_service_error — lower-level variant returning (StatusCode, String) tuples, used by page handlers.
  • ReviewModeQuery — a small deserializable struct for the ?review=true query parameter.

Keeping these in one place ensures consistent auth behavior across all endpoints.

Extract chat handlers into chat.rs

Intent: Isolate the branch chat SSE streaming and message submission logic, which is a self-contained feature with its own interaction pattern.

Affected files: crates/pika-git/src/web/chat.rs

Evidence
+async fn branch_chat_handler(
+async fn branch_chat_send_handler(

The ~165-line chat.rs contains two handlers:

  • branch_chat_handler — opens an SSE stream for a branch's chat session, streaming Claude AI responses in real time.
  • branch_chat_send_handler — accepts a POST with a user message, forwards it to the Claude session, and returns a JSON acknowledgment.

This is a natural extraction boundary because chat is a distinct feature with its own SSE lifecycle, separate from the CI live-update streams in live.rs.

Extract webhook processing into webhook.rs

Intent: Move the inbound webhook handler—signature validation, payload parsing, and event dispatch—into its own file.

Affected files: crates/pika-git/src/web/webhook.rs

Evidence
+async fn webhook_handler(

The ~145-line webhook.rs contains the webhook_handler function, which:

  1. Reads the raw request body.
  2. Validates the HMAC-SHA256 signature from the X-Hub-Signature-256 header against the configured webhook secret.
  3. Parses the JSON payload and inspects the X-GitHub-Event header to determine the event type.
  4. Dispatches push events and CI status updates to the forge_service for processing.

This is security-sensitive code (signature validation) and benefits from being reviewable in isolation.

Extract admin-only endpoints into admin.rs

Intent: Group privileged administrative operations that require `require_admin_auth` into a single file.

Affected files: crates/pika-git/src/web/admin.rs

Evidence
+async fn admin_ci_targets_handler(
+async fn admin_reset_ci_target_handler(
+async fn admin_trigger_branch_ci_handler(
+async fn admin_update_ci_target_handler(

The ~115-line admin.rs contains four handlers, all gated behind require_admin_auth:

HandlerPurpose
admin_ci_targets_handlerLists all CI targets with their current health state
admin_reset_ci_target_handlerResets a CI target's health counters
admin_trigger_branch_ci_handlerManually triggers a CI run for a branch
admin_update_ci_target_handlerUpdates CI target configuration

Separating admin endpoints makes it easy to audit privileged access patterns.

Split the test suite into five submodule files

Intent: Mirror the source-code split in the test suite, with each test file focused on the corresponding concern: API integration tests, SSE live-stream tests, HTML render tests, unit tests for helpers, and webhook tests.

Affected files: crates/pika-git/src/web/tests.rs, crates/pika-git/src/web/tests/api.rs, crates/pika-git/src/web/tests/live.rs, crates/pika-git/src/web/tests/render.rs, crates/pika-git/src/web/tests/unit.rs, crates/pika-git/src/web/tests/webhook_inbox.rs

Evidence
+use super::*;
+mod api;
+mod live;
+mod render;
+mod unit;
+mod webhook_inbox;

The test module tests.rs becomes a thin hub that declares five child modules via standard mod (not include!):

Test fileLinesCoverage area
tests/api.rs~490JSON API endpoint integration tests
tests/live.rs~275SSE stream behavior and termination
tests/render.rs~590HTML template rendering correctness
tests/unit.rs~465Pure-function unit tests (ci_timing_summary, format_compact_duration, lane_operator_hint, etc.)
tests/webhook_inbox.rs~175Webhook signature validation and event dispatch

Each test file uses use super::*; to access everything in the web module, which works seamlessly because the include!() approach keeps all items in a single namespace. The test split mirrors the source split, so when a developer modifies live.rs they know exactly which test file to check.

Diff