Skip to content

Restyle: redundant encoding, density, drill-down IA, and an honest website - #525

Merged
rainmanjam merged 7 commits into
mainfrom
feat/restyle
Aug 27, 2026
Merged

Restyle: redundant encoding, density, drill-down IA, and an honest website#525
rainmanjam merged 7 commits into
mainfrom
feat/restyle

Conversation

@rainmanjam

Copy link
Copy Markdown
Owner

All nine style directions, plus the demo-seed harness they needed and a full screenshot recapture.

App

Nothing that matters is signalled by colour alone. toneMark gives every state a silhouette that survives greyscale, and the guard asserts the real property rather than a literal: strip every colour-only class and no two tones may render the same mark. The meters gained three non-colour channels — one of which was a plain bug, the zone-break ticks were drawn under the bar, so a hot signal covered the very mark saying where hot starts.

Density is one line of mechanism: Tailwind v4 compiles spacing to calc(var(--spacing)*n), so rescaling that variable rescales everything. Scoped to main so chrome and Radix portals stay comfortable; controls floored in literal rem so a 21px Stop button can't happen.

An OnAirBar answers "what is on air, and what is wrong" with a fault list that is empty when nothing is wrong, rather than a row of reassurances. Across-the-room mode reuses the same silhouettes at 32px.

Website

The hero's docker run had drifted from README — no --name, no :latest, and no -p 1935:1935, so a visitor pasting it got a container with no RTMP ingest and nothing saying why. Now byte-identical, keeping the /udp note that explains the classic silent-ingest failure. make build appeared nowhere on the site at all.

The primary CTA is now "Read the quickstart", not "Install it" — for self-hosted infrastructure the documentation is the conversion path. The comparison page gains the loss tables verbatim, including that Restreamer is more mature and you should probably use it if you don't need per-destination audio. That candour is the asset and is not softened.

No new colour on either surface; the token spine is untouched.

Tokens

Twelve values were numerically identical across the two surfaces. Six are one brand fact spelled twice and are marked SPINE on both sides. One pair was a coincidence — the app's --warn and the website's --color-cross — and folding them would move an operator's degraded-stream colour because a designer retuned a crosspoint. Marked NOT SPINE.

What the screenshots found

Retaking them surfaced three defects, none caught by any test, all needing more than one programme to be visible: destinations compiled against the wrong programme's layout (#515), a dashboard reading OFFLINE over three live streams (#514), and a destination list flipping between programmes every two seconds (#524). The captures are why the product is now right, not just the record of it.

Depends on #524 for the last of those.

Verified: tsc -b clean, vitest 625/625, npm run build on both.

https://claude.ai/code/session_01A8N3W5ct9SZtHK9sCDD9cL

PHASE 1 of the restyle. Two pieces of groundwork the seven remaining
directions depend on.

TOKENS. Twelve values are numerically identical across the app and the website.
Six are one brand fact spelled twice and are now marked SPINE on both sides,
with the reason no build-time single source is reachable: the Vite-Tailwind and
Astro-Tailwind pipelines share no build step. 60-30-10 is written out as
explicit section labels so a new saturated colour is a visible decision rather
than a quiet addition.

One pair was a COINCIDENCE, not a fact: the app's --warn ("reconnecting,
degraded, near clip") and the website's --color-cross ("active crosspoint
ONLY") had landed on the same literal. Unrelated meanings, and folding them is
the mistake -- a designer retuning a crosspoint would move an operator's
degraded-stream colour. Marked NOT SPINE on the website side. An earlier pass
also nudged the hue by 1 degree to make them numerically distinct; that is
reverted, because changing a visual value to encode a non-visual fact does not
survive a rounding pass and spends a real colour on a comment's job.

DEMO SEED. scripts/demo-seed.sh brings up three live programmes on one SRT
port, a 1080p/720p/480p ladder with destinations on different rungs, thirteen
destinations across six kinds, live audio with moving envelopes, and five real
multitrack recordings. Screenshots of an empty install cannot show what this
product is for, and the two bugs in #514 and #515 are invisible without it.

Every URL is .invalid and every key is obviously fake; the script runs gitleaks
on its own fixture and refuses if it fires. Platform destinations are created
DISABLED on purpose -- enabled they sit in failed/reconnecting and photograph
as a broken product.

Claude-Session: https://claude.ai/code/session_01A8N3W5ct9SZtHK9sCDD9cL
PHASE 2 of the restyle. Seven directions across the two surfaces.

APP. Nothing that matters is signalled by colour alone any more. toneMark in
lib/signal.ts gives every state a silhouette that survives greyscale -- live,
warn, down, armed and idle each get their own -- and the guard asserts the real
property rather than a literal: strip every colour-only class and no two tones
may render the same mark. The meters gain three channels, one of which was a
plain bug: the zone-break ticks were drawn UNDER the bar, so a hot signal
covered the very mark saying where hot starts.

Density is one line of mechanism. Tailwind v4 compiles spacing to
calc(var(--spacing)*n), so rescaling that variable rescales everything; scoped
to main so chrome and Radix portals stay comfortable, type left alone, and
controls floored in literal rem so a 21px Stop button cannot happen.

An OnAirBar answers "what is on air, and what is wrong" above the dashboard,
with a fault list that is EMPTY when nothing is wrong rather than a row of
reassurances. Across-the-room mode reuses the same silhouettes at 32px.

WEBSITE. The hero showed an uncopyable grey strip whose docker run had drifted
from README: no --name, no :latest, and no 1935, so a visitor pasting it got a
container with no RTMP ingest and nothing saying why. It is now the real
command, byte-for-byte, keeping the /udp note that explains the classic silent
ingest failure. make build appeared nowhere on the site at all.

The primary call to action is now "Read the quickstart" rather than "Install
it": for self-hosted infrastructure the documentation IS the conversion path,
because the visitor is deciding whether they can operate this thing. The
comparison page gains the loss tables verbatim, including that Restreamer is
more mature and you should probably use it if you do not need per-destination
audio. That candour is the asset and is not softened.

No new colour on either side; the token spine is untouched.

tsc -b clean, vitest 614/614.

Claude-Session: https://claude.ai/code/session_01A8N3W5ct9SZtHK9sCDD9cL
…etween programmes

Both found by photographing a seeded three-programme install, and neither is
visible on the single-source box every developer runs.

BITRATE WAS BLANK ON EVERY HEALTHY SRT INSTALL. The card read
ingest.progress.bitrateKbps -- the ingest PROCESS's number -- and for SRT there
is no ingest process: reconcileIngest returns early on purpose, because
srtserver delivers datagrams straight into the hub and a second thing on that
socket would crash-loop. So the most prominent number on the dashboard printed
"—" forever on the primary operated ingest path. Same root as the "Offline"
badge in #514, fixed there and left behind one line below in the same card.

The fallback is not an estimate. It is the same relay byte series useIngestLive
already trusts to decide whether a broadcast is going out at all, so the card
now shows the bytes that made it say "Live" one line above. The process is still
preferred where it exists, because on RTMP or a pull ingest that number is
measured at the ingest and the relay series is one hop later. It returns nothing
rather than 0 when nothing is arriving: a printed 0 claims a running, empty
stream.

THE DESTINATION LIST SHOWED ONE PROGRAMME AT A TIME. The status socket is
install-wide and every engine publishes onto it, so the app kept the last
snapshot and the list -- which is the whole install on one page, grouped by
programme -- rendered a heading for each and the destinations of whichever
engine spoke most recently: 9, then 2, then 2, every two seconds. Hidden until
Engine.Status was scoped to its own source, because the default engine's
snapshot used to carry every row on the machine.

Fixed at the consumer. publishStatus fires per process transition and is
deliberately synchronous: coalescing it handed a destination a backwards decode
timestamp at a failover switch in 3 runs out of 10, and a platform drops the
connection on a backwards DTS. Asking every engine for its status on that path
would multiply a database read per transition to buy what the fold does free.

The fold keys on BOTH the programme a snapshot names and the rows it carries.
Naming alone was the first attempt and it was wrong: two shapes arrive on this
socket -- statusPayload sends the whole install, publishStatus sends one
programme -- and both label themselves with a single source.id, so the
install-wide one kept the other programmes' rows and re-added them. 13
destinations rendered as 17, which is how it was caught.

UPTIME is left as "—" for SRT. It has the same cause and no fix available here:
relay.Stats carries byte and loss counters and no notion of when bytes first
arrived, so it needs a server-side field and a decision about whether a
reconnect gap resets it.

Claude-Session: https://claude.ai/code/session_01A8N3W5ct9SZtHK9sCDD9cL
…mme install

The committed set photographed an install with one source, no destinations and
no recordings, because that is what every box available to point the capture at
looked like. capture.spec.ts drives a real browser and waits for real state
before each shot, which is the right design and is worth nothing aimed at an
empty product.

These are taken against scripts/demo-seed.sh: three programmes on one SRT port,
a 1080p/720p/480p ladder with destinations on different rungs, thirteen
destinations across six kinds, live multitrack audio and a recording library.
Every URL is under .invalid and every key is obviously fake.

The install is left streaming for several minutes before the shutter rather than
the 25 seconds the seed needs to settle, so destination uptimes and speeds read
as a broadcast in progress instead of one that started moments ago.

18-destinations.png is new: the whole destination area, grouped by programme,
which is the only shot that shows what this product is for -- six platforms,
each on its own rendition rung, each taking a different subset of the audio
tracks.

Retaking these found three defects, none of which any test caught and all of
which needed more than one programme to be visible at all: destinations
compiled against the wrong programme's track layout, a dashboard reporting
OFFLINE over three live streams, and a destination list that flipped between
programmes every two seconds. The captures are the reason the product is now
right, not just the record of it.

Claude-Session: https://claude.ai/code/session_01A8N3W5ct9SZtHK9sCDD9cL
Copilot AI lite review requested due to automatic review settings August 26, 2026 06:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
35.0% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@rainmanjam
rainmanjam merged commit 8e06a66 into main Aug 27, 2026
24 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.

2 participants