Skip to content

Dashboard

The dashboard is a single-page web app served by fleetd.js, a Node daemon that runs on the same machine as your runners. Open http://localhost:7878 after installing it.

For installation, see Get started → Install the dashboard.

The screenshots on this page are a fixture fleet

Every repository, host and runner name below is invented, and the numbers come with them. They are the real interface rendering fixture data, so that a screenshot of the Analytics tab cannot be mistaken for a measurement. docs/tools/shoot-dash.mjs builds the fleet and captures them; the two images in the README that are from a live fleet say so where they appear.

Tabs

Fleet

The Fleet tab: seven summary tiles across the top, three drift rows naming a dead LaunchAgent, a label mismatch and a repo at capacity, then every runner as a card grouped by project

The main view. Shows every registered runner grouped by project, with live status badges.

Runner status badges: | Badge | Meaning | |---|---| | busy | A job is running (Runner.Worker process detected) | | online | Registered, service running, no active job | | offline | Registered on GitHub but process not detected | | draining | Scheduled to stop after its current job | | drained | Stopped intentionally; will not take new jobs | | dead | LaunchAgent loaded but process not running | | not-loaded | LaunchAgent not loaded (or runner not registered) | | unknown | GitHub API could not be reached for this runner |

Click any runner to open the runner drawer: recent events, job history, version info, diagnostic log summary, and action buttons.

The runner drawer for a runner in launchd-dead drift: last exit code, the errors its own log recorded, a runner version that GitHub auto-updated, seven-day utilisation, and the actions available on it

Drift alerts appear under a runner when the local and GitHub states disagree — for example, the service is running but GitHub says the runner is offline, which usually means a registration that needs re-running.

Runs

The Runs tab: what is building now with elapsed time and the runner that claimed it, the queued jobs with their diagnosed cause, and the recent runs below

Live and recent runs across all repos. Job status, duration, and runner assignment. Click a run to open it on GitHub.

Queue diagnosis: if a job has been queued for more than a moment, the dashboard classifies why. Hover the queued indicator to see the verdict (label mismatch, no runner, runner down, host saturated, etc.) and whether the autoscaler could help.

Analytics

The Analytics tab: runs, success rate, CI time, allowance saved, peak concurrency and never-scheduled runs, over a table classifying why jobs failed and whose problem each cause is

30-day job history: count, duration percentiles (p50/p95), failure rate, and runner utilisation per repo. Updated when you open the tab. Not live-streamed.

Lint

The Lint tab: workflow findings grouped by rule, each naming the file, the branches it applies to, and what the rule catches

Workflow file analysis across all repos with registered runners. Flags: - Unbounded matrix jobs (n×m runners needed) - Missing concurrency: groups (parallel-on-push risk) - Static concurrency group collisions (two workflows blocking each other) - Matrix parallelism that exceeds the per-repo runner cap

Concurrency advisor findings appear here with YAML snippets showing the suggested fix.

Alerts

The Alerts tab: one open alert with how long it has been open, above the history of rules that fired and closed

Configurable alert rules for runner offline time, queue depth, error rates, and more. Configure in dashboard/alerts.config.json — see the example in examples/alerts.config.json.

Capacity

The Capacity tab: the per-repo sizing verdict, the headroom gate with the reason it would refuse, and the admission panel

Sizing panel: shows the recommended runner count for each repo based on observed peak concurrent jobs. Toggle autoscaling on/off here.

Admission panel: shows the current FLEET_ADMIT_MODE and a history of held/admitted decisions if observe or enforce mode is active.

Scenario replay: run "what if we had N runners for repo X" against actual historical job data. Shows queue wait p95, max queue depth, and SLO compliance.

Burst forecast: next-hour demand estimate based on weekday/time-of-day patterns and workflow schedules. Shadow-only — verifies accuracy against reality before any action is taken.

Control

The Control tab: the action catalogue, the live settings table showing which layer each value came from, and the audit log

Manual actions: duplicate a runner, deregister one, drain/resume, run a health check. Every action requires the bearer token (paste once; stored in localStorage). Dangerous actions show a confirmation dialog.

Action log at the bottom of the page shows the stdout of the last executed action.

Hosts

The Hosts tab: this machine live, and a second Mac marked stale after six minutes without a heartbeat, its runners greyed rather than removed

Federated host view (when agents are configured). Shows each remote Mac's runners, load, disk, and drain state. A host whose heartbeat is stale (>2 minutes old) is flagged in orange.

Queue diagnosis causes

Cause Meaning Autoscale eligible?
telemetry-unavailable GitHub or the local probes failed, so nothing here is trustworthy No
unserved No runner registered for this repo No
label-mismatch No runner has all required labels No
runner-down All runners for this repo are offline/drained No
concurrency-block Another job from this run is using the only runner No
host-saturation No runner is free and the headroom gate is refusing additions No
repo-capacity More jobs than runners for this repo Yes
github-delay Runner is ready but GitHub has not dispatched No

Only repo-capacity with high confidence triggers autoscale. A label mismatch would only be made worse by cloning the existing runner.

Why a job is queued explains what each cause rules out, and Honest analytics explains how they are recorded.

Actions

All actions require the control token in an Authorization: Bearer header. The browser stores this token in localStorage after you paste it once.

Action Effect Requires confirmation?
Duplicate runner register.sh with same labels + RUNNER_INSTANCE=N+1 Yes
Drain runner scripts/drain-runner.sh --drain Yes
Resume runner scripts/drain-runner.sh --resume Yes
Deregister runner scripts/deregister.sh --apply Yes
Health check ./health.sh No
Download diagnostics Redacted bundle (token-gated) No

Autoscaling

The autoscaler adds runners when the queue classifier returns repo-capacity with high confidence, and removes idle duplicates that have been idle longer than the configured TTL.

Toggle and configure from the Capacity tab. dry-run mode logs decisions without executing them — run there first.

The autoscaler never touches the instance-1 runner for any repo (it would remove the last runner, which is never the right outcome).

See Admission and Scaling for the full policy.

Diagnostic bundles

The runner drawer's Diagnostics button downloads a redacted plaintext bundle containing the runner's configuration, recent events, job history, and log summary. The bundle uses an allowlist — only named files are included, and every line is passed through the secret redactor. Safe to paste into an issue.

The download requires the control token.

Read-only mode

FLEET_READ_ONLY=1 disables the control plane entirely. The dashboard is still live-updating, all read routes still work, and no bearer token exists. Use this for a LAN-accessible read-only status board where you want to bind to 0.0.0.0 without granting any write access.

Security note

Read routes are unauthenticated by design on loopback. This is appropriate for the default deployment (dashboard on localhost, accessed via SSH tunnel) but means that anything on the same machine with localhost access can read runner status, job history, and diagnostic log tails. See Security Hardening for LAN deployment guidance.