Layouts keep every tile they are given, and show what is on offer #12

Merged
michael merged 1 commits from claude/layout-audit into main 2026-09-21 13:30:57 -04:00
Owner

The bug, root-caused

For seventeen days (2026-08-31 → 09-17) the server's sanitizeLayout() rebuilt each saved tile from six fields and dropped the rest — including variant — on both save and load. The client had carried variant since multi-tile modules arrived; the server was the one that lagged. Every named layout saved to a booth in that window came back with its solo timers as "All timers", its Now tile as the full Now/Next card, its transcripts as the plain module. Fixed for new saves in 1.6.0 (#6); this PR makes the class of bug impossible and fixes what loading does with what it finds.

Reproduced both halves before changing anything: on current main a four-tile round trip (All timers, Service, Current item, Now) came back exact; a file written the old way came back as three "All timers" — the reported symptom verbatim.

What changes

The server stops deciding which tile fields exist. It checks the shape of the fields it knows, keeps any other small plain-JSON field a tile carries, drops only retired ones (headHidden), stamps files v: 2, and refuses a layout over 100 tiles instead of quietly saving the first hundred. A booth's server can now never again erase a newer client's data.

A layout keeps every tile; the board shows what is offered. A tile whose module is absent/disabled, or whose picker entry (or alias) isn't in the module's list right now, keeps its place and draws nothing — never the module's default in its place — and appears the moment the registry offers it again. Because an entry list can change with no event to mark it, the registry is re-read every 6 s while anything is folded away, and not at all otherwise. Timers' placeholder card for a missing solo timer goes; the dot says why.

Loading tells you what happened, and offers the way back. "Loaded 'FOH': 9 tiles, 2 not shown — Service, #ops aren't offered right now", with Undo in the toast. Saving over a name asks first (naming the earlier save's time and tile count); the server keeps the copy it replaced as <name>.previous.json; Admin → Saved layouts → Restore previous swaps the two, so a restore is one restore from undone. Delete takes the previous copy with it. The browser keeps one good copy of its own layout behind the live one and falls back to it — then writes it back — when the live one won't parse.

tools/layout-roundtrip.test.js starts the server on a spare port with a temp data dir and round-trips a fully-populated tile plus fields the server has never heard of; it fails if anything is lost, and covers previous/restore/cap/delete.

Shell 1.8.0, timers 1.4.1. Module guide and README updated; a plain tile (variant: '') is still always shown — that's the module's own tile, and hiding it would take out every legitimately-plain tile ever added.

Verified in the browser

  • Save → save again over the same name → confirm modal "Replace 'Booth'? It was saved 9/20/2026, 11:57 AM with 4 tiles…" → toast "Replaced… the earlier copy is kept"; listing shows hasPrevious=true.
  • Reset → load → all four tiles back with variant, title and position; toast "Loaded 'Booth': 4 tiles" with Undo; Undo returned to the empty board.
  • PCO Plan switched off: Service and Current item folded away, All timers and Now stayed, layout still held 4 tiles. Switched on: both back in 6.0 s, same tile ids, same grid positions.
  • Legacy variant-less layout: plain timers shown as the module tile (by design), #ops [transcript:C999] hidden, toast "…1 not shown — #ops isn't offered right now".
  • Corrupt proddash:layout → reload → 4 tiles restored with the error toast; key parses again afterwards.
  • Admin: Restore previous appears only on the layout that has one; restore returns 200 and keeps hasPrevious.

What this does not do

The four layouts already on the booth machines are still wrong — AB's "FOH" and "Studio" (17 multi-tile tiles) and NA's "Studio A Ingest" and "BOH Band Lineup" (5) were stripped before 1.6.0 and the data is gone from the files. Nothing here recovers what a file no longer contains. Each machine's booth browser still holds the right tiles locally; re-saving under the same name repairs them. Held until after 2:00 pm Eastern today — live services.

🤖 Generated with Claude Code

## The bug, root-caused For seventeen days (2026-08-31 → 09-17) the server's `sanitizeLayout()` rebuilt each saved tile from six fields and dropped the rest — including **`variant`** — on both save and load. The client had carried `variant` since multi-tile modules arrived; the *server* was the one that lagged. Every named layout saved to a booth in that window came back with its solo timers as "All timers", its `Now` tile as the full Now/Next card, its transcripts as the plain module. Fixed for new saves in 1.6.0 (#6); **this PR makes the class of bug impossible and fixes what loading does with what it finds.** Reproduced both halves before changing anything: on current main a four-tile round trip (All timers, Service, Current item, Now) came back exact; a file written the old way came back as three "All timers" — the reported symptom verbatim. ## What changes **The server stops deciding which tile fields exist.** It checks the shape of the fields it knows, **keeps any other small plain-JSON field** a tile carries, drops only retired ones (`headHidden`), stamps files `v: 2`, and **refuses** a layout over 100 tiles instead of quietly saving the first hundred. A booth's server can now never again erase a newer client's data. **A layout keeps every tile; the board shows what is offered.** A tile whose module is absent/disabled, or whose picker entry (or alias) isn't in the module's list right now, keeps its place and draws nothing — never the module's default in its place — and appears the moment the registry offers it again. Because an entry list can change with no event to mark it, the registry is re-read every 6 s **while anything is folded away**, and not at all otherwise. Timers' placeholder card for a missing solo timer goes; the dot says why. **Loading tells you what happened, and offers the way back.** *"Loaded 'FOH': 9 tiles, 2 not shown — Service, #ops aren't offered right now"*, with **Undo** in the toast. Saving over a name asks first (naming the earlier save's time and tile count); the server keeps the copy it replaced as `<name>.previous.json`; **Admin → Saved layouts → Restore previous** swaps the two, so a restore is one restore from undone. Delete takes the previous copy with it. The browser keeps one good copy of its own layout behind the live one and falls back to it — then writes it back — when the live one won't parse. **`tools/layout-roundtrip.test.js`** starts the server on a spare port with a temp data dir and round-trips a fully-populated tile *plus fields the server has never heard of*; it fails if anything is lost, and covers previous/restore/cap/delete. Shell **1.8.0**, timers **1.4.1**. Module guide and README updated; a plain tile (`variant: ''`) is still always shown — that's the module's own tile, and hiding it would take out every legitimately-plain tile ever added. ## Verified in the browser - Save → save again over the same name → confirm modal *"Replace 'Booth'? It was saved 9/20/2026, 11:57 AM with 4 tiles…"* → toast *"Replaced… the earlier copy is kept"*; listing shows `hasPrevious=true`. - Reset → load → all four tiles back with variant, title and position; toast *"Loaded 'Booth': 4 tiles"* with **Undo**; Undo returned to the empty board. - **PCO Plan switched off**: Service and Current item folded away, All timers and Now stayed, layout still held 4 tiles. **Switched on**: both back in **6.0 s**, same tile ids, same grid positions. - Legacy variant-less layout: plain timers shown as the module tile (by design), `#ops [transcript:C999]` hidden, toast *"…1 not shown — #ops isn't offered right now"*. - Corrupt `proddash:layout` → reload → 4 tiles restored with the error toast; key parses again afterwards. - Admin: **Restore previous** appears only on the layout that has one; restore returns 200 and keeps `hasPrevious`. ## What this does not do **The four layouts already on the booth machines are still wrong** — AB's "FOH" and "Studio" (17 multi-tile tiles) and NA's "Studio A Ingest" and "BOH Band Lineup" (5) were stripped before 1.6.0 and the data is gone from the files. Nothing here recovers what a file no longer contains. Each machine's booth browser still holds the right tiles locally; re-saving under the same name repairs them. **Held until after 2:00 pm Eastern today — live services.** 🤖 Generated with [Claude Code](https://claude.com/claude-code)
michael added 1 commit 2026-09-20 12:03:27 -04:00
The bug behind this: for seventeen days the server rebuilt each saved tile
from the fields it knew and dropped the rest — including `variant`, so a
layout of five different timers came back from the server as five "All
timers". The client had carried `variant` since multi-tile modules arrived;
the server was the one that lagged, and it is the part of ProdDash that
always will (a booth updates weeks after the dev machine). So the server
stops deciding which tile fields exist: it checks the shape of the ones it
knows, keeps any other small plain-JSON field a tile carries, drops only
retired ones, stamps the file `v: 2`, and refuses a layout over a hundred
tiles instead of quietly saving the first hundred. A test round-trips a
fully-populated tile plus one the server has never heard of, and fails if
anything is lost.

Then the rule the arrangement is loaded by. A layout keeps every tile it is
given; the board shows the ones whose module is installed and enabled and —
for a tile added from a picker entry — whose entry is offered right now. The
rest keep their place and draw nothing, and appear the moment the registry
offers them again: a Service timer waits for PCO Plan, a transcript for its
channel. Never the module's default in their place. Because a module's entry
list can change with no event to mark it (PCO Plan comes back and, seconds
later, the timers module is offering its timers again), the registry is
re-read every few seconds while anything is folded away, and not at all
otherwise. The timers module's placeholder card for a missing solo timer
goes with this: nothing is drawn, the dot says why.

Loading says what it did — "Loaded 'FOH': 9 tiles, 2 not shown — Service,
#ops aren't offered right now" — with Undo in the message. Saving over a
name asks first, and the server keeps the copy it replaced one back; Admin →
Saved layouts gets Restore previous, which swaps the two so a restore is one
restore from undone. The browser keeps one good copy of its own layout
behind the live one and falls back to it when the live one will not parse.

Shell 1.8.0, timers 1.4.1. The layouts already saved on the booth machines
had their variants stripped before this and have to be saved again from a
browser that still holds the right tiles — nothing here recovers what the
files no longer contain.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
michael merged commit 3435a0625e into main 2026-09-21 13:30:57 -04:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: michael/ProdDash#12