Brand¶
One palette, one mark, one type pairing. Written down so the next asset matches the last one, and so that somebody who is not the author can regenerate any image in this repository.
Colour¶
The palette is not a decision made for the documentation. It is
dashboard/public/style.css,
dark scheme, copied exactly — a docs site in a different blue from the product
is a docs site for a different product. When the dashboard's colours change,
these change with them.
| Hex | Where | |
|---|---|---|
| Plane | #0b0f17 |
The ground of every figure, and the dashboard's page behind everything |
| Surface | #121925 |
Panels and cards |
| Raised | #18212f |
Chips and inset elements on a surface |
| Ink | #f6f8fb |
Text |
| Muted | #7f8a9b |
Secondary text, structural lines, arrowheads |
| Hairline | #253142 |
Panel borders |
| Accent | #60a5fa |
The one highlight, on dark |
| Accent, dark-on-light | #2563d9 |
Links and accents on a light ground, where #60a5fa fails contrast |
Four more colours exist, and each one may only be used with its meaning.
They are states, not decoration, and using --good because a box needed to be
green is how a reader stops believing that green means anything.
| Hex (dark) | Hex (light) | Means | |
|---|---|---|---|
| Good | #4ade80 |
#15803d |
Online, idle, healthy |
| Busy | #60a5fa |
#2563d9 |
A job is running |
| Warning | #f5b942 |
#c97900 |
Draining, degraded, needs attention eventually |
| Serious | #fb8155 |
#d65a2f |
Drifted, offline |
| Critical | #f36a6a |
#c93636 |
Dead, failed |
The mark¶
Two nodes feeding one node that carries a tick: work arriving at a runner, and the runner reporting a verdict. It reads at 16 px, which is the size it was drawn for — the favicon is the same geometry, not a simplified redraw.
| File | What it is | Use for |
|---|---|---|
dashboard/public/assets/fleet-mark.svg |
The mark on a rounded #111827 tile |
The dashboard header |
docs/img/mark.svg |
The same file, copied | The documentation site favicon, and anywhere it sits on an unknown ground |
docs/img/mark-mono.svg |
Strokes only, currentColor, no tile |
The documentation site header, which inherits its foreground colour |
dashboard/public/assets/app-icon.svg |
Full-bleed square, no rounded corners | The installed-web-app icon |
dashboard/public/assets/favicon.svg |
The mark at tab size | The dashboard tab |
The monochrome mark drops the tick's green and cuts it out of the node instead. A monochrome mark with a second colour in it is two marks.
Type¶
| Role | Face | Notes |
|---|---|---|
| Display and body | Inter | Headings and prose in generated figures |
| Mono | JetBrains Mono | Runner names, repo names, commands, environment variables, metrics |
These are the two faces Material for MkDocs is configured with, so the site and
the figures are one pairing rather than two. The dashboard itself uses the
system stack (system-ui and ui-monospace) and deliberately still does: it is
a local operations page that must render instantly with no network, and Inter
at 14 px is not distinguishable enough from San Francisco to be worth a font
load on a machine that is busy compiling.
Figures load the copies in
docs/figures/fonts/.
Both families are SIL OFL 1.1, both licences travel with them, and both are
recorded in
THIRD_PARTY_NOTICES.md.
Any number that sits in a column gets font-variant-numeric: tabular-nums. A
table of durations whose digits do not line up is harder to read than one with
fewer digits in it.
The figures, and how they are made¶
Everything is rendered from a source in this repository rather than drawn by hand in a tool, so it can be regenerated when the mark or the palette changes.
A figure is an HTML file in
docs/figures/ —
one per figure, each an inline SVG on the tokens in figures/brand.css, with
the shared shapes in figures/symbols.js. npm run assets in docs/tools/
opens each one in headless Chromium and screenshots the .figure element at
2x into docs/img/; npm run assets -- --only architecture does one.
| Asset | Size | Source |
|---|---|---|
img/architecture.png |
2560x1600 | figures/architecture.html |
img/drift.png |
2560x1360 | figures/drift.html |
img/queue-causes.png |
2560x1400 | figures/queue-causes.html |
img/loops.png |
2560x900 | figures/loops.html |
img/federation.png |
2560x1200 | figures/federation.html |
img/banner.png |
2560x1280 | figures/banner.html |
img/social-preview.png |
2560x1280 (2x of GitHub's 1280x640) | figures/social-preview.html |
img/fleet-tab.png, runs-tab.png, analytics-tab.png, lint-tab.png, alerts-tab.png, capacity-tab.png, hosts-tab.png, control-tab.png, runner-drawer.png |
2880x1800 (1440x900 at 2x) | npm run shoot, against the fixture fleet |
img/fleet-live.png, img/analytics-live.png |
2880x1800 | A real fleet, names replaced. See below |
img/drift-repair.gif, img/drift-repair.mp4 |
1280x800 | npm run motion, against a real fleet. See below |
Why PNG, and why each figure carries its own ground¶
GitHub serves an SVG in a README through a proxy, as an <img>. Web fonts
never load through that proxy, so the text falls back to whatever the reader
happens to have installed and the line breaks the figure was laid out around
turn into overlapping labels. A PNG rendered by headless Chromium with the
self-hosted faces is the same image on every machine.
Each figure carries its own #0b0f17 ground rather than a transparent one,
because it gets dropped onto surfaces this repository does not control: the
README in GitHub's light theme, the documentation site in either scheme, a
portfolio page, a slide.
Nothing in a figure is smaller than 13 px at 1x. GitHub renders the README column at about 830 px, so a 1280 px figure is shown at roughly two thirds of its size, and 13 px is the floor for reading it there. A figure that needs more words than fit at that size is two figures.
Why the diagrams are placed by hand¶
The architecture page used to carry two Mermaid blocks. Mermaid renders inside
GitHub's own theme — pale boxes, a default font, edges routed differently in
light and dark — so it was the only element on the page that could not be made
to match anything else, and it sat in the section a reader uses to decide
whether to keep reading. No amount of %%{init}%% theming fixes that, because
the layout engine is the problem rather than the colours.
Mermaid is a layout engine for people who do not want to place boxes. A diagram somebody looks at for ten seconds is placed boxes.
The screenshots¶
npm run shoot in docs/tools/ serves the real dashboard/public/ and
answers every /api/ route from fixture-fleet.mjs. The page is unmodified
product code; only its data is fixture. The snapshot is built by calling the
daemon's own buildRunners, deriveDrift and deriveGroups rather than by
hand-writing what they return, so a change to any of them shows up here instead
of quietly drifting out of date.
It needs no fleet, no GitHub token and no network — which is the point, because
fleetd builds its runner list from launchctl, ps and the GitHub API, and
none of those exist on a machine that is only writing documentation.
Every name in it is visibly invented — testowner/app-ios, testhost,
teststudio — so nothing in a screenshot can be mistaken for a measurement.
The fleet root is a real temporary directory, because discoverRunnerDirs,
runnerVersions, diagSummary and diagTail have to read actual files; it is
presented as /Users/testowner/actions-runners on the way out, so no capture
carries this machine's own temp path.
The fixture is deliberately not healthy. It holds a runner in launchd-dead
drift, a drained one, a label mismatch, a repo at capacity, an open alert, a
stale second host and a lint finding — because a screenshot of a fleet with
nothing wrong shows none of what the dashboard is for.
Three assets are not from fixtures, on purpose
fleet-live.png, analytics-live.png and drift-repair.gif come from a
real fleet, with repository and host names replaced.
The Analytics tab is the reason. A thirty-day success rate, a p90 queue wait and an hosted-macOS allowance saved are numbers a reader would believe, and generating them from seeded runs would mean publishing invented measurements under a heading that says measured. The Fleet tab is shown live alongside it so the two agree about the same fleet.
The recording is the third because a fixture fleet cannot die; the section below says how it was made.
Everything else in docs/img/ is fixture data and says so in its alt text.
The convention for both, taken from the project this documentation borrows its method from: a number that gets believed and turns out to be wrong costs more than one that was never reported.
The recording¶
npm run motion in docs/tools/ records img/drift-repair.gif: a runner
dying and being repaired, on a real fleet. It is the one asset here that cannot
come from fixtures, and shoot-motion.mjs is the one tool here that needs a
fleet to run at all.
deriveDrift opens launchd-dead when launchd holds a job with no process
behind it, and health.sh --repair closes it by restarting the service. A
fixture fleet can hold a dead runner, but it cannot die — and a recording of a
state machine being stepped by a script would be a recording of the script. So
the rig drives a real fleet over SSH and films the real page:
- the front-end is served from
dashboard/public/in the working tree, so the recording matches the screenshots captured beside it rather than whatever build the fleet host happens to be running; - every
/api/request is proxied to thefleetdalready running on that host, so every number on screen was measured; - the service is killed with
launchctl kill 9, which leaves the launchd job loaded. Notlaunchctl unload— an unloaded job reads aslaunchd-missing, a different finding with a different fix.deadis a loaded job with no process, which is the failure no runner plist'sKeepAlivewill undo, because none of them sets it; - the auto-remediation bridge is stopped for the take and started again
afterwards.
dashboard/autofix/bridge.jsanswers the alert by runninghealth.sh --repairwithin seconds of it opening, which is the right behaviour and the wrong recording.
Frames, not a video stream, for the same reason every figure here is rendered rather than drawn: a frame is a screenshot of the real page with animations disabled, so each one is a state the dashboard actually held.
Fifteen seconds is the length of the GIF, not of the take. fleetd polls
launchd every 15s while the fleet is busy and every 45s while it is idle, and a
restarted listener needs another 10-20s to reach GitHub — so the sequence runs
to a hundred-odd seconds and no setting makes it fifteen. Frames are captured
throughout every 400 ms and sampled onto the output grid uniformly at the end.
The take behind the committed GIF ran 98 seconds, so the clock runs at 6.5x and
nothing else does.
The terminal strip is composited into the page rather than over the frames
afterwards, so a captured frame is a screenshot of the whole composition and
there is no second pipeline to fall out of step with the first. It shows the
tail of the real output, with one deliberate exception: health.sh sweeps the
whole fleet, so the dead runner's row and the -> restarting line under it
arrive early and would scroll out of a seven-line strip long before the command
returns — one frame at playback speed, which is no frames. Those lines are held
at the bottom of the strip instead of scrolling away. Nothing is invented and
nothing is reordered.
Names are replaced on the way through, the same substitution fleet-live.png
and analytics-live.png use: the daemon's own project order becomes
project-a, project-b, …, each repository keeps the role word that makes its
tile readable, the owner becomes owner, and the host becomes runner-host.
The mapping is derived from the snapshot rather than written down, so the
recording and fleet-live.png name the same fleet the same way rather than
agreeing by hand. A project name that is also a word this product says about
itself — this fleet has a group called actions and one called fleet — is
replaced only where it is a whole value, because replacing those blind rewrites
actions.runner.<label> and the fleet root along with them.
There is no repository name in shoot-motion.mjs. The runner to kill is named
on the command line, and should be one whose repository has a second runner
still serving it — the rig says so at startup when it is not.