Skip to content

Live web dashboard (optional, alongside the GitHub-issue dashboard) #59

Description

@kristofferR

What this is

crq has one dashboard today: a Markdown GitHub issue that gets re-rendered after every state write. It works, and it should keep working exactly as it does. But it is static, read-only, and can only say things that survive being re-rendered into Markdown — no live countdowns, no editing, no drill-down.

This issue plans an optional live web dashboard that runs alongside it. Nothing about the issue dashboard changes; it stays as the legacy option for anyone who wants it, and for the case where the web server isn't running.

The web dashboard adds four things the issue can't do:

  1. Live state. Countdowns that tick, a queue that reorders itself, an activity feed — without reloading anything.
  2. Editing. Per-repo settings: which bots run, which ones a round must wait for, whether crq may run fix sessions, which model and effort level to fix with.
  3. Onboarding. A repo adder, and a guide to the review bots themselves — what they are, which fit this repo, and how to sign up.
  4. Depth. A PR view with the actual findings, the running fix session, and the round's history.

Design mockups exist in design/mockups/ (open index.html). They're static HTML with realistic fake data — design artifacts, not the frontend.


Decisions already made

Name. The project is now Code Review Queue. The crq initialism survives — it just stands for the new name. The dashboard uses the new name with a short descriptor on first sight ("fleet-wide orchestration for automated PR reviewers"). No rabbit imagery: crq orchestrates several reviewers and CodeRabbit is only one of them. Renaming the repo/module/binary is separate work, and the legacy issue dashboard keeps its current wording.

How it's served. A new crq serve subcommand. The Go binary serves the API and a React SPA embedded with go:embed, so a single binary is still the whole product. It runs on the always-on host and is reached over Tailscale — one user, no auth, no multi-tenancy.

What's editable. Both per-repo settings and the fleet-wide defaults they inherit from.

Enrollment moves into state. Today "which repos does the fleet review" lives in CRQ_REPOS in a per-host env file. The dashboard needs to change it from the UI, so enrollment becomes a record in the shared state ref, like RepoReviewers already is. The env vars stay honored as a per-host legacy override, and the UI is explicit about which one is winning (see Enrollment precedence below). The same reasoning applies to the editable fleet defaults — they live in state, because an env file on one host can't be the source of truth for a fleet.


The mental model everything hangs off

Work in crq happens in three lanes, and the dashboard should never blur them:

  • Metered review — the primary reviewer (CodeRabbit by default, but CRQ_BOT is configurable). It spends the shared account quota and is serialized by the single global fire slot, min-interval pacing, and per-round cooldowns.
  • Co-review — Codex, Cursor Bugbot, Macroscope. Free, no quota, no fire slot; each round claims its own trigger.
  • Fix sessions — autofix: dispatch agents running on hosts, working through findings. (The feature is autofix, per the rename in Rename the drain to autofix #60; an individual run is still a fix session.)

This matters most when the quota is blocked. The issue dashboard says "account blocked", which reads like the whole fleet has stopped. It hasn't: only the metered lane is paused. The web dashboard says "CodeRabbit quota blocked" and shows, next to it, that co-review and fix sessions are still running.


Where the data comes from

Almost everything on the main screen is already in the persisted state — the same state.json in the git ref the issue dashboard renders from. State.Queue(now, minInterval) already computes, per waiting round, when it can fire and what's holding it (cooling down, account blocked, slot busy, pacing, behind an earlier round). The rounds carry phase, head, attempts, timestamps, co-bot trigger marks, dispatch claims and the note explaining the last transition. Holds, per-host autofix health, the fire slot, the leader lease and the archive ring are all there too.

So the overview costs zero GitHub API calls. The server watches the state ref and pushes the whole snapshot to the browser whenever Rev changes; the browser ticks countdowns between pushes. The state blob is small, so pushing a full snapshot beats computing diffs, and reconnecting after a dropped connection is trivially correct.

The expensive part is per-PR depth — findings, convergence, per-co-reviewer verdicts — which needs a live GitHub observation (several API calls). That is fetched on demand only, when a PR view is opened: the cached-state layer renders instantly, the findings fill in behind a small "observing…" chip, the result is cached per (PR, head), and it refreshes slowly only while that PR is open and its round is in flight. It never blocks the page, and a failed observation leaves the state layer intact with a Retry.


The pages

Overview

The one screen that answers "is anything stuck?".

  • Status banner — the headline follows the same precedence ladder the issue dashboard already uses (stranded > quota-blocked > reviewing > awaiting feedback > queued > held > idle), plus the live countdown, the quota chip, the fire slot and the leader. The browser tab title mirrors it, so it's glanceable while unfocused. The quota shows a remaining count and a reopen time onlyAccountQuota has no capacity field, so the dashboard must not invent an "N of 8".
  • Needs attention — a ranked list of things that will not fix themselves: a stranded reservation, an unhealthy host, no leader lease, hosts running an older binary that will ignore new settings, env-vs-state enrollment conflicts, failed actions, stale state. Each names its subject and links to the fix. It stacks under the headline rather than competing with it.
  • In flight — the round(s) being reviewed: PR, head, phase, when it fired, the deadline counting down, per-bot state, host, and a badge if a fix session is running.
  • Queue — each waiting round with its reason. Only the front row gets a time; everything behind it shows its reason without a made-up ETA, because when it starts depends on the round ahead finishing. That's State.Queue()'s own epistemics and the UI must not overpromise past it. A countdown that reaches zero flips to "due — waiting for the next pump pass" rather than going negative, because the daemon acts on its own cadence.
  • Held — PRs excluded from review, with reason, who held them and since when, plus an inline Unhold.
  • Fix sessions — what's running now, and per-host health as Healthy / Unhealthy / No recent signal. The third state matters: a host that hasn't reported anything is not healthy, and calling it healthy is how you fail to notice a dead watcher.
  • Recently finished — completed rounds plus the archive endings (superseded, closed, cancelled).
  • Activity feed — labeled honestly as live, since this dashboard started. See below.
  • Plain-language line per row. Every waiting or in-flight row carries one sentence combining status, blocker and next transition — "Waits for the CodeRabbit quota window at 14:32, then its turn in the queue." The raw phase vocabulary stays visible, but it isn't the only explanation.

PR detail

Two layers, as described above. The instant layer shows the round: phase, head, attempts, timestamps, deadlines, note, hold status, per-co-bot bookkeeping, the dispatch claim, dismissed findings, and this PR's earlier rounds. The fetched layer adds convergence, who has reviewed, per-co-reviewer status, and the findings themselves grouped by severity with expandable bodies.

Repos & settings

A rail on the left lists the fleet and is how you choose a repo; the panel on the right is that repo's full settings. Every repo reference elsewhere in the app (queue rows, feed entries, PR pages) carries a small gear that opens a quick-settings slide-over scoped to that repo, with the repo named in a switcher and a link through to the full page. That answers the obvious question about any settings panel: how did it open, and which repo is it about?

Add repo

The enrollment flow. See Repo adder below.

Review bots

The comparison and signup guide. See Review bot guide below.

Fleet settings

The defaults every repo inherits: bot set, Runs/Required defaults, per-bot trigger mode and grace, solver defaults, pacing and timeouts. Plus the account quota card with a "Refresh quota" button, a Hosts & capabilities panel, and the plumbing (leader lease, writers seen, calibration issue, state ref).

The hosts panel exists because fleet policy and host capability are different things and mixing them causes bad UI. "This repo may run fix sessions with claude/sonnet/high" is policy. "atlas doesn't have claude on its PATH" is capability. The settings page states the policy and shows the capability next to it.

Changing a fleet default gets a stronger confirmation than a per-repo edit, because it can move many repos at once: "affects 12 repos; 3 open PRs will be reconsidered".


Per-repo settings in detail

Runs vs Required

Two separate ideas, shown as two separate toggles per reviewer:

  • Runs — this bot reviews this repo.
  • Required to finish — convergence waits for it.

The primary's Runs is effectively fixed (it's the primary), but its Required is a real toggle: a repo can legitimately converge on co-reviewers alone. Requiring a bot visibly turns on its Runs. An empty required set is prevented while editing, with an explanation, instead of failing at save time.

Provenance and inheritance

Every control shows whether its value is a fleet default or an override, inline — not hidden behind a hover — along with who set it and when. Editing an inherited value creates the override in place; every override has a "reset to fleet default"; the panel header has "clear all overrides". This mirrors the state's own nil-vs-empty semantics: "following the fleet" and "explicitly none here" are different things and must not look alike.

Impact preview

Consequential saves say what they'll do before they do it, in product terms: "Enabling Macroscope will trigger it on 2 open PRs at their current heads. No completed rounds are reopened; no metered reviews are spent." If a host in the fleet runs an older binary that won't honor the setting, the same dialog says so.

The controls

Already backed by state today:

Control Backing
Which bots run RepoReviewers.CoBots (crq reviewers set)
Which are required RepoReviewers.Required
Autofix for this repo (Default / On / Off + reason) RepoAutofixSwitch (crq autofix on|off|default)
Held PRs, with unhold State.Holds (crq hold / unhold)

New per-repo fields this introduces (all needing the same By/UpdatedAt provenance the existing records have, plus a writer-capability story like CapsRepoOverrides):

Control Today
Enrollment ("reviewed by crq") env only
Per-bot trigger mode (never / self-heal / always) global env
Per-bot self-heal grace global env (10m)
Fix agent, model, reasoning effort one global agent + one global --agent-args string
Extra prompt instructions appended for this repo one global prompt
Max fix attempts per head global (3)
Fix fork PRs global
Skip PRs by author global

Note on the solver settings: crq deliberately knows nothing today about which model an agent should use — that's in --agent-args. Making it per-repo is a real change in that stance, and worth doing, because "fix this Go queue with high effort" and "fix this small script with low effort" are genuinely different jobs. The dashboard shows the model/effort as policy and the per-host agent availability right next to it.

Setup status never blocks

A bot whose setup can't be verified is not disabled in the UI. Verification is best-effort — it's inferred from bot activity crq has already classified — and a freshly installed bot has no activity yet. Disabling the toggle would be circular: you couldn't enable the bot that needs enabling to produce the evidence. So an unverified bot shows a warning and a link to its setup checklist, and the toggle still works.


Enrollment precedence

Because enrollment can come from state or from a host's env file, the UI shows exactly which, as one of:

  • Managed — a record in shared state; the toggle works here.
  • Forced on by env — a host's CRQ_REPOS includes it. No fake toggle; the UI names the host and the line to edit.
  • Excluded by envCRQ_EXCLUDE.
  • Conflict — state and env disagree; this also shows up in Needs attention.
  • Unsupported — an active host on an older binary won't honor state enrollment.

Repo adder

  1. Pick a repo. Discovery lists repos under the configured scope, minus the enrolled ones, with public/private, open PR count and last push. There's a free-text owner/name fallback. Repos matched by env are annotated with their precedence state.
  2. Choose review bots. Condensed cards from the bot guide, pre-checked to fleet defaults, with suggestions.
  3. Initial settings. The settings panel in miniature, pre-filled with fleet defaults.
  4. Start policy. The important one, and the thing that's easy to get wrong: what happens to the PRs that are already open? Three choices — new PRs and future pushes only (the default), selected existing PRs, or all eligible open PRs — and a plain summary of the consequence before saving: "would enqueue 7 open PRs; about 7 metered reviews against the shared quota; 2 are dependabot PRs and will be skipped, so 5 rounds actually enqueue."
  5. Save. One CAS write: the enrollment record plus only the settings actually changed. Then honest expectations — "picked up on the daemon's next pass" — escalating to "no daemon is running, so nothing will be enqueued" if there's no leader lease.

Unenrolling has an equally precise contract: it stops new enqueues and ignores new heads, active rounds finish by default (with "…and cancel its N active rounds now" as a secondary option), and settings/history are retained. An env-forced repo can't be unenrolled from the UI; it says which host's env to edit.


Review bot guide

A page comparing the reviewers crq can orchestrate: the configured primary plus the co-reviewer registry (Codex, Cursor Bugbot, Macroscope). Each card carries a short description of what the bot is good at, what it requires and costs, its setup steps, its current status, and a call to action.

Status is one of Seen recently / Previously seen (with a date) / Setup not verified, always labeled as best-effort.

Suggestions state their reason inline — "Suggested for this repo: it's public, and CodeRabbit's Pro tier is free for open source", or "the codex CLI is on mbp, so you appear to already have a Codex setup". Plus one standing line: suggestions are based on compatibility, not referral compensation.

Calls to action match the state: "Visit vendor ↗" for signup, "View setup" when unverified, "Configure in Code Review Queue" when the bot is around but not enabled, "Manage repositories" when it's active.

Affiliate links (marked loosely, not designed here). Where a vendor has a referral program, the signup link is a referral link, shipped as a product default so installs credit the project. Each one carries a short disclosure next to it ("referral link — supports Code Review Queue development"). Prices are only shown with a "last checked" date and a link to the vendor, because they drift.


PR actions

All of them mirror an existing CLI verb — the dashboard adds no new semantics:

Action Mirrors Notes
Hold / Unhold crq hold / unhold reason required; unhold is one click
Cancel round crq cancel confirm; warns when the round holds the fire slot
Dismiss finding crq dismiss reason required; thread-less findings only; scoped to this head
Resolve / Decline thread crq resolve / decline decline requires a reason, since it posts a reply

Confirmations pull their consequence text from live state rather than saying "are you sure". They're single-step — this is a single-user tool, and typed confirmations would be theater.

No "re-review" button in v1. The obvious implementation (cancel the round, enqueue a new one) doesn't do what the button promises: the fire logic sees a primary review already on the current head and dedupes. The button would lie. A genuine forced same-head re-review needs new engine semantics, and that's a separate decision.


Activity feed, honestly scoped

The feed is built by diffing consecutive state snapshots — phase transitions, enqueues, supersedes, holds, quota blocks, autofix failures, settings edits — and backfilled from the archive. Where the serve host also runs the watcher, live WatchEvents are merged in as richer entries, but they can't be the backbone: only the process running crq watch emits them, so a feed built on them alone would be blind to everything the daemon and other hosts do.

What it is not is an audit log. It's bounded, it starts when the server starts, and it says so. A dedicated /activity route with real retention is deferred until we decide whether durable history is worth storing; until then it's a panel on the overview labeled "live — since this dashboard started".


Edge cases worth designing on purpose

  • First run — a readiness checklist before the "add repo" button: is a daemon holding the lease, is GitHub reachable, is the quota calibrated, is at least one bot verified, is a fix agent available on some host, is any overlapping native auto-review turned off.
  • Quota blocked — the headline, with a countdown and the scoped wording, plus the reminder that other lanes continue.
  • Stranded reservation — outranks everything, because it never clears on its own; links to the remediation.
  • No leader lease — "enqueued work will not fire on its own". Otherwise "queued forever" is a mystery.
  • Host health — three states, never two.
  • State unreachable — a stale banner with the age and rev, page stays readable, every mutating control disabled, countdowns keep ticking but are marked stale. Old data is fine; silently-wrong data is not.
  • Observation failure — keep what we have, show the error, offer Retry.
  • Connection dropped — a freshness dot goes to "reconnecting…", and one fresh snapshot reconciles everything on return.

What this needs from the rest of crq

  • crq serve: a state-snapshot push channel, the per-PR observation endpoint, and the settings read/write endpoints, all going through the existing Service methods so the dashboard never becomes a second way to mutate state.
  • New state fields: enrollment records, a fleet-defaults record, per-repo trigger/grace, per-repo solver settings. Each with provenance and a writer-capability gate, so an older binary on another host can't silently ignore them without the UI saying so.
  • A decision on durable activity history.
  • A decision on forced same-head re-review.

Also worth noting for later: the JSON endpoints behind these views should reuse the already-frozen CLI shapes (FeedbackReport, NextReport, WatchEvent, ReviewerView) rather than inventing view models, so the same surface can serve scripts and other clients later. That's a loose marker, not a design.


Mockups

In design/mockups/ — open index.html. Five cross-linked pages: overview, repos & settings, add repo, bot guide, PR detail. They share one frozen moment (Mon 2026-07-27 13:51:26 CEST) chosen to show every interesting state at once: quota blocked with a countdown, one round reviewing, four queued PRs with four different wait reasons, a held PR, a fix session running, one host broken and one with no recent signal.

Three notes on them:

  • The scenario was checked against State.Queue() so it depicts a state the code can actually produce. With the fire slot held, every queued row would read "slot busy" — so in the mockup the slot has been released and the round is in reviewing, which is what lets four distinct wait reasons coexist.
  • Bot logos are the real GitHub avatars of the accounts crq matches on; repo icons are the real favicons from those repositories, with a letter-tile fallback for repos that don't have one.
  • An earlier round compared three visual directions (a dense monospace panel, an editorial one, and this one). Operations Desk won and the other two have been removed.

Vocabulary note: this uses the post-rename names throughout — the feature is autofix (crq autofix on|off|default, repo_autofix, autofix_by_host), while an individual run is still a fix session and the agent is still the fix agent. Ref #60.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions