Skip to content

feat: show the input name and downtime on the split-flap board (#154) - #270

Merged
BernardJen merged 1 commit into
mainfrom
feat/board-live-state
Aug 21, 2026
Merged

feat: show the input name and downtime on the split-flap board (#154)#270
BernardJen merged 1 commit into
mainfrom
feat/board-live-state

Conversation

@BernardJen

Copy link
Copy Markdown
Contributor

Closes #154.

The board cycled five hardcoded word triples. It now opens on what the operator actually
wants to know:

HDMI 01
NO SIGNAL
DOWN 04:31

The contract decision, which #154 said was the real work

Option 3 of the three it weighed: the renderer pushes rows in via a new setMessages(),
and the board stays a dumb renderer of whatever it is handed. All knowledge of inputs, sides
and downtime stays in renderer.js.

Chosen over passing app state into create() because this board is not a registry entry
renderer.js imports it directly — so nothing had to change about the saver contract to do it.
The weather saver (#101) answered the same question the other way for a different reason: it
needs state the renderer does not own, so it reads a cached value from its own module. Here the
renderer owns everything, so pushing is strictly less coupling.

Rows apply on the board's next message change, not immediately, so a push never interrupts
a flap in progress. With a single-entry list the board re-flaps the same rows each hold period —
which is what makes a ticking clock read as a mechanism rather than a redraw.

Truncation, not a wider grid

MIN_COLS stays measured over the built-in messages only. Growing cols at runtime would mean
reallocating the tile arrays and the state texture mid-flight; #154 explicitly allows
truncation, so the cheap option is also the sanctioned one.

Computed from the board's own formula:

View Per-board Columns
Dual (one side) 3000×1200 16
Single (full wall) 6000×1200 22

An input named longer than that truncates — BLACKMAGIC ULTR. — and renaming it in settings is
the fix. The cut ends in a period rather than clipping silently, because silent clipping is the
exact failure #92 fixed: NO SIGNAL rendered as NO SIGNA reads as a broken renderer, not as
an abbreviation.

The clock has three forms so it reads correctly at both ends — DOWN 04:31, DOWN 2H 14M,
DOWN 3D 04H. The colon form only ever means minutes and seconds, so it is never ambiguous
against the hour form. A test asserts every character it can emit is in FLAP_CHARS.

Two things that would have failed quietly

A repeated showNoSignal must not restart the clock. It is idempotent and gets called again
for a synced same-device pair and on every re-entry, so restarting would peg the downtime line
near zero forever — looking like it works while never advancing.

A malformed push must not blank the board. setMessages validates and falls back to the
built-in list, so a renderer with nothing to say leaves the static messages rather than a grid
of spaces.

Tests

23 new, 595 → 618. Each invariant confirmed by breaking it:

Mutation Caught
restart the clock on every showNoSignal
clip without a marker
drop the push validation

Verified, and not

Driven in a browser with both sides down, one backdated four minutes and the other two hours.
Both boards started, both took live rows, and the clock advanced across the run. Fitted output
at the real column count: HDMI 01 / NO SIGNAL / DOWN 06:05 and
BLACKMAGIC ULTR. / NO SIGNAL / DOWN 2H 15M.

Not verified: the flaps settling on that text. The Browser pane throttles
requestAnimationFrame, so the board crawls mid-flip and never lands — the screenshots show it
animating toward the new rows rather than displaying them. Rendering arbitrary strings through
this path has been shipping since #147, and only the strings changed here.

One correction worth recording: my first in-page column derivation used TILE_RATIO = 0.72
dividing, when it is 1.45 multiplying. Both produced 16 for that canvas because MIN_COLS
bound either way, so the fitted strings above are right — but the table above comes from the
real constant, not that one.

The board cycled five hardcoded word triples. It now opens on what the operator
actually wants to know -- which input this is, that it has no signal, and how long
it has been gone:

  HDMI 01 / NO SIGNAL / DOWN 04:31

## The contract decision

Option 3 of the three #154 weighed: the renderer pushes rows in via a new
setMessages(), and the board stays a dumb renderer of whatever it is handed. All
knowledge of inputs, sides and downtime stays in renderer.js.

Chosen over passing app state into create() because this board is not a registry
entry -- renderer.js imports it directly -- so nothing had to change about the
saver contract to do it. The weather saver (#101) answered the same question the
other way for a different reason: it needs state the renderer does not own, so it
reads a cached value from its own module. Here the renderer owns everything, so
pushing is strictly less coupling.

Rows are applied on the board's NEXT message change rather than immediately, so a
push never interrupts a flap in progress. With a single-entry list that means the
board re-flaps the same rows each hold period, which is what makes a ticking clock
read as a mechanism rather than a redraw.

## Not widening the grid

MIN_COLS stays measured over the built-in messages only, and live text is truncated
to fit rather than growing the grid. Growing `cols` at runtime would mean
reallocating the tile arrays and the state texture mid-flight; #154 explicitly
allows truncation, so the cheap option is also the sanctioned one.

The budget, computed from the board's own formula:

  dual view, one side    3000x1200    16 columns
  single view, full wall 6000x1200    22 columns

So an input named longer than that truncates -- "BLACKMAGIC ULTR." -- and renaming
it in settings is the fix. Truncation ends in a period rather than clipping
silently, because silent clipping is the exact failure #92 fixed: "NO SIGNAL"
rendered as "NO SIGNA" reads as a broken renderer, not as an abbreviation.

## Two things that would have failed quietly

**A repeated showNoSignal must not restart the clock.** It is idempotent and is
called again for a synced same-device pair and on every re-entry, so restarting
would peg the downtime line near zero forever -- looking like it works while never
advancing. The clock is set only on the transition in, and cleared on the way out.

**A malformed push must not blank the board.** setMessages validates and falls back
to the built-in list, so a renderer with nothing to say leaves the static messages
rather than a grid of spaces.

## Verification

23 new tests, 595 -> 618, each invariant confirmed by breaking it: restarting the
clock on every call fails 1, clipping without a marker fails 1, dropping the
validation fails 1.

Driven in a browser with two sides down, one backdated four minutes and the other
two hours: both boards started, both received live rows, and the clock advanced
across the run. The fitted output at the real column count was
"HDMI 01 / NO SIGNAL / DOWN 06:05" and
"BLACKMAGIC ULTR. / NO SIGNAL / DOWN 2H 15M".

Not verified: the flaps settling on that text. The Browser pane throttles
requestAnimationFrame, so the board crawls mid-flip and never lands -- the
screenshots show it animating toward the new rows rather than displaying them.
Rendering arbitrary strings through this path has been shipping since #147.
@BernardJen
BernardJen merged commit ed01660 into main Aug 21, 2026
1 check passed
@BernardJen
BernardJen deleted the feat/board-live-state branch August 21, 2026 13:10
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.

feat: let the split-flap board show live app state

1 participant