Skip to content

feat(serve): a colour per node, carried across the graph, the feed and the scrubber - #176

Merged
skakri merged 1 commit into
fix/dashboard-run-list-refreshfrom
feat/node-tint
Aug 7, 2026
Merged

feat(serve): a colour per node, carried across the graph, the feed and the scrubber#176
skakri merged 1 commit into
fix/dashboard-run-list-refreshfrom
feat/node-tint

Conversation

@skakri

@skakri skakri commented Aug 7, 2026

Copy link
Copy Markdown
Member

Stacked on #175 — it touches the same Scrubber component, so it is based on that branch rather than main. Merge #175 first and this retargets cleanly.

Eight node boxes, a feed interleaving all of them, and a scrubber that said nothing about what happened where. Connecting a row in the feed to a box in the graph meant reading both.

Each node now has a hue fixed by its name, carried across four surfaces: the wash along the bottom of its box, its name in the graph, its prefix in the feed, and its stretch of the scrubber's track.

Derived rather than assigned, because the node set is not fixed — a workflow can name nodes this build has never heard of, and a lookup table would give those all one colour or none.

Two quantisations, both load-bearing

FNV-1a over the name, then 18 hues × 3 lightnesses.

Taken continuously off the hash, two of this pipeline's nodes landed seven degrees apart. Different values, and nowhere near different enough to look it — which reads as one node drawn inconsistently rather than as two nodes. An earlier attempt at stepping by the golden angle made it worse: that spreads sequential indices, and a hash is not sequential, so it just reintroduced correlation and put six of eight nodes inside a 40-degree band of cyan.

The lightness axis then separates the pairs that share a hue bucket. Across the twelve node names this repository uses, all twelve come out distinct.

Red is left alone

The palette starts at 40 degrees. --hazard is red and means a failure, so a node washed red reads as broken whatever its state — the first version gave the overseer hue 0 and its idle box looked like an error.

Selected and working now draw in the node's own tint as well, so which node this is stays legible while its state changes, and red keeps its weight by meaning one thing. .node--failed is ordered last, so a node that is both failed and selected reads as failed.

The state colours come from one inline --tint custom property rather than a colour per property: the hue is the only part CSS cannot derive, and everything downstream of it stays in the stylesheet. The .st--working override is scoped to .node, because the same class carries run status in the runs list, where the live green is about the run and not about any node.

The scrubber

The band is the track now, replacing a flat accent that only said "this is a slider" — which the slider already said.

Bucketed rather than one stop per event: a long run has hundreds of events, and two nodes running in parallel alternate between them, which at one stop each renders as shimmer. A bucket that caught several nodes is split evenly between them with hard stops, which is what "both were running here" looks like at this width. Hard rather than interpolated, because a blend between two nodes' colours would invent a third that means nothing.

The handle stays round — the one thing you drag should not look like the hard-edged panels around it — and is filled with a blur of what is beneath, so the band reads through it rather than being hidden by the control pointing at it.

Verified

In a browser, not asserted about the source: every node box carries a wash and a tinted name, the feed prefix matches its node's colour in the graph, and the track resolves to 224 colour stops. Screenshots checked by eye for the red-reads-as-failure problem, which is how it was found.

bun run typecheck and bun run build pass. No Rust changed. Note web/ has no test runner — a test file there only gates typecheck and never executes — so the browser check is the verification.

…d the scrubber

Eight node boxes, a feed interleaving all of them, and a scrubber that says
nothing about what happened where. Connecting a row in the feed to a box in the
graph meant reading both.

Each node now has a hue fixed by its name: the wash along the bottom of its box,
its name in the graph, its prefix in the feed, and its stretch of the scrubber's
track. One glance connects them.

Derived rather than assigned, because the node set is not fixed — a workflow can
name nodes this build has never heard of, and a lookup table would give those
all one colour or none. FNV-1a over the name, quantised into 18 hues and three
lightnesses.

Both of those quantisations are load-bearing. Taken continuously off the hash,
two of this pipeline's nodes landed seven degrees apart — different values, and
nowhere near different enough to look it, which reads as one node drawn
inconsistently. The lightness axis then separates the pairs that share a hue
bucket. Across the twelve node names this repository uses, all twelve are
distinct.

The palette starts at 40 degrees rather than 0. Red is `--hazard` and means a
failure, so a node washed red reads as a broken one whatever its state; the
first version gave the overseer hue 0 and its idle box looked like an error.

Selected and working now draw in the node's own tint too, so which node this is
stays legible while its state changes. Red is left meaning one thing, and
`.node--failed` is ordered last so a node that is both failed and selected reads
as failed.

The scrubber's track is the band, replacing a flat accent colour that only said
"this is a slider" — which the slider already said. Bucketed rather than one
stop per event: a long run has hundreds, and two nodes running in parallel
alternate between them, which at one stop each renders as shimmer. A bucket that
caught several nodes is split evenly between them, which is what "both were
running here" looks like at this width.

The handle stays round — the one thing you drag should not look like the panels
it sits among — and is filled with a blur of what is under it, so the band reads
through it rather than being covered by the control that points at it.
@skakri
skakri merged commit 51ad38b into main Aug 7, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant