Commit Graph
117 Commits
Author SHA1 Message Date
christianandClaude Opus 5.5 4c1c7f565f Vista Cues 1.0.1: its own heading in the + picker instead of sitting under Slack
The picker gives a heading only to modules that offer a list of tiles; a
module without one is a bare line under whichever module comes before it,
and alphabetically that is Slack. Vista Cues now offers one entry, "Live
cue lists", so it reads as its own module. Tiles added before this have no
variant and keep using the module's schema, so they still draw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:39:53 -04:00
christianandClaude Opus 5.5 2893316a1a Vista Cues 1.0.0: which Vista 3 cue lists are live, from the network MIDI that fires them
Vista 3 reports nothing about what it is playing — its OSC only takes
commands — so a booth with no lighting operator can't see the lighting
computer. ProPresenter, the tracks rig and Companion fire Vista's cue lists
with notes over macOS network MIDI sessions, and a session sends every note
to every participant, so the module joins those sessions as one more
listener (RTP-MIDI in plain Node, no helper) and hears what Vista hears.

The admin's cue sheet turns notes into Go / release / pause / back / flash
on named lists. Sheets with no release notes use [groups]: a Go replaces its
group's live list, and the first group is the headline — the main scene —
which takes over the look and clears the FX below it. The tile shows the
headline list large with how long ago and who fired it, the other live
lists as tags, recent notes, and which senders are connected.

The module invites each configured sender and keeps re-inviting, accepts
invitations from anyone, and advertises itself over Bonjour on macOS. A
busy port (a config save re-initing before the old sockets close) is
retried rather than failing. Default session port is 5040/5041, clear of
the 5009 session on NA Ingest.

tools/vista-midi-mock.js is a stand-in sender for development;
modules/vista-cues/vista.test.js covers the parser, packet decoding and
real loopback sessions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:15:45 -04:00
christian 60f82690f3 Merge pull request 'Resi Cues 1.0.3: the next cue's name keeps moving, and Resi logins stop hammering the account' (#43) from claude/resi-cues-startup-pause into main 2026-10-07 17:24:56 -04:00
christianandClaude Opus 5.5 79d3237328 Resi Cues 1.0.3: a login without access to the venue stops signing in every poll
Every 401/403 was answered by signing in again on the spot. For a login that
works but may not list the account's venues (media@waters.church at North
Attleboro got 403 on /venues), that was a successful sign-in on every 1s poll
-- the pattern that had just got another Resi account locked.

- A refusal straight after signing in is a permission, not an expired
  session: it is held off like a failed login, with the message "This Resi
  login isn't allowed to see its venues (403) — ask a Resi admin for access".
  Only a session over a minute old is renewed.
- 429 on any request holds off too, not only on the login.
- The players cache-bust fallback no longer drops the parameter for good on a
  401/403/429, which are about the login rather than the parameter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 16:27:38 -04:00
christianandClaude Opus 5.5 67f612e371 Resi Cues 1.0.2: a failed Resi login holds off instead of retrying every second
A failed login left no session, so the next 1s poll signed in again, and the
next, for ever. Resi answers that with 429s, and since the retries never let
up the account never got out of them: at North Attleboro the decoder picker
stayed empty with "Resi login failed (429)". The picker made it worse by
building a fresh client -- and signing in -- on every admin page load.

- After a failed login, further attempts wait 15s, doubling to 5min (or as
  long as Resi's Retry-After asks); calls in between fail at once with the
  reason and how long until the next try.
- The status says what happened: rate-limited, password rejected, or Resi
  unreachable.
- The decoder picker reuses the running module's client when the login
  matches, otherwise one client per login, so it shares the hold-off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 15:44:14 -04:00
christianandClaude Opus 5.5 0f0adb4c60 Resi Cues 1.0.1: the next cue's name keeps moving on a 30-second Resi
On a Resi that publishes every ~30s, the board could start a few seconds
before a publish and learn a 2s cadence from that partial first gap. Every
honest 30s gap after that was thrown out as a pause, so playback read as
paused 15s into every update: the countdown froze, and the next cue -- and
the name on the Timers card -- stopped moving on.

- The partial gap right after startup is no longer learned, and one outlier
  gap can no longer lock out the real cadence.
- Until a cadence is measured, a pause is only called after 45s, not 15s.
- The Timers card is titled with the cue's name; its status line now says
  "Next cue" / "Held" instead of repeating the name.
- server.test.js awaited nothing, so an async test raced the next one and
  failed on main; tests now run one at a time.

Same fix as the Resi -> ProPresenter bridge and the Companion module 0.1.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:59:12 -04:00
Waters ChurchandClaude Opus 5 7bb1131378 PCO Plan 1.10.0: follow several service types in priority order, and read any of them from any dashboard
An account runs more than one service type — a Sunday morning, a Wednesday
night, a youth service, a second campus — and until now that meant one
ProdDash per type. The module now follows as many as are configured.

Admin picks them in priority order. Shell 1.12.0 gives `multiselect` an
`orderable: true` that stops being a ticklist: the picks sit in a numbered
list with a nudge either way and a remove, over a dropdown holding what is
left. Forty switches to read past is not a picker, and a ticklist cannot
carry an order anyway. The order is the setting — when two types have a plan
at the same date and time, the one higher up wins.

The server gathers candidate plans from every ticked type and tiers them:
today's plan first, then the next upcoming, then the most recent past, with
the configured order breaking a tie. A type that fails is skipped rather
than failing the read, unless it is an auth or rate-limit answer, which
belongs to the whole connection. The types not being followed are read on a
slower lane (45 s) into `state.others`, so they are there to look at without
a second connection.

Every tile's title bar carries a service-type control. Picking one moves
every PCO Plan tile on that browser's dashboard — plan, teams and song key
together — onto its plan, over a small page bus in `viewed-type.js`; the
choice is per browser and survives a reload. The control hides itself when
only one type is configured.

Live machinery stays with the type being followed: another type's plan shows
its running order with no now marker, no AbleSet and no service clock — it
shows that plan's own scheduled time instead, since borrowing the running
clock of a different service is exactly the misreading the amber top-bar
stripe exists to prevent.

Old configs carry `serviceTypeId`; it is read once into the new array and
saved, and plan pins still resolve bare, so nothing has to be re-entered.

`tools/pco-mock.js` takes `--types N` to pad the list out, for trying the
picker against an account with dozens of them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 14:32:19 -04:00
Waters ChurchandClaude Fable 5.1 1274360ee6 Send Message 1.0.0: one PIN, one place to send from; Slack 1.3.0 and ProdCom 1.4.0 become messaging providers
A new send-message module owns everything a dashboard sends: the PIN
(switchable off in admin), the unlock tokens, the Send message and Quick
replies tiles, the quick replies and quick emojis. It opens no upstream
connection of its own — it finds every module declaring
provides: ["messaging"] through the shell's module list and hands each
message to that module's POST /messaging/send over loopback, so Slack keeps
its one Slack connection and ProdCom its one ProdCom connection. Providers
check the browser's unlock token with GET /verify over loopback before
touching their upstream, so no browser on the network can post as the
location without having entered the PIN here.

Slack 1.3.0 is view-only plus reactions: the Sending group, PIN, send tiles
and quick replies are gone; it serves the messaging descriptor (channels as
recipients) and its reactions ride on the Send Message unlock. A layout's
old Send message / Quick replies tiles draw nothing until removed.

ProdCom 1.4.0 serves the descriptor as identities — only channels with
typingEnabled, since typed text goes out *as* a channel — and posts with
POST /api/v1/channels/{id}/send. Its tile is a click-to-target like Slack's
(accent stripe along the bottom), and typed entries carry a keyboard mark.

The tiles: click any Slack or ProdCom transcript to point sends there; a
Send as menu (only for platforms with identities, hidden otherwise) picks
the ProdCom channel to speak as; the choice is saved with the tile so named
layouts remember it. Wording follows the role — "Send message to #ops…" /
"Send message to ProdCom as FOH…". The PIN pad's dots ride on a real
numeric input so a phone's keyboard appears. Outcome is an icon beside
Send (check / blocked, five seconds, sentence as tooltip). A pill on the
box's bottom edge trades box height for rows of quick-emoji keys, a row
at a time; the single row scrolls sideways on a narrow tile with Send
pinned in place.

The module guide documents the provider contract and the page bus
(window.proddashMessaging + proddash:messaging:* events) the tiles share;
the ProdCom mock gained the send route.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-05 13:55:55 -04:00
michael 7f6e5bd151 Merge pull request 'Shell 1.10.1: header menus open where their button is, folded bar or not; ProdCom 1.3.3 keeps its filter on a narrow tile' (#39) from claude/prodcom-filter-button-visibility-8a1d2d into main 2026-09-30 14:21:52 -04:00
michael 2307770ce9 Merge pull request 'SPL Meter 0.5.0: the capture tool ships prebuilt, for Apple silicon and Intel' (#38) from claude/spl-prebuilt-helper into main 2026-09-30 14:20:46 -04:00
Waters ChurchandClaude Fable 5.1 41811ead1c Shell 1.10.1: header menus open where their button is, folded bar or not; ProdCom 1.3.3 keeps its filter on a narrow tile
A tile's dropdown menus (ProdCom's Channels, the Timers menu) opened but
never showed under a folded-away title bar — in fullscreen, or with
auto-hide on. The bar carries a backdrop-filter and a transform while
folded, and either one makes a fixed-positioned descendant position
itself against the bar instead of the viewport: the menu landed a
screen-height below its button, inside the tile's overflow. A tile near
the top of the screen showed a misplaced menu; anything lower showed
nothing at all.

The shell now hosts every header menu on the body, under an element
carrying the tile's data-module so a module's scoped CSS still reaches
its items; places it whole inside the viewport; holds a folded bar open
while one of its menus is open (the pointer has to travel down into the
menu); and marks the open menu's button the way the gear is marked.

ProdCom: seven controls need ~175 px of bar, and the shell clips what a
narrow tile cannot fit — the Channels button went with the rest. Below
380 px the six secondary controls fold into a ⋯ menu offering the same
actions, so the filter is the last thing to go.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-30 14:08:02 -04:00
Waters ChurchandClaude Fable 5.1 0929af46ca SPL Meter 0.5.0: the capture tool ships prebuilt, for Apple silicon and Intel
Same move as Timers 1.7.0. The capture tool existed only where the macOS
app build had compiled it, with the module trying swiftc on the booth as a
fallback — NA has been logging "spawn spl-capture ENOENT" since its
catalog install. Under the module guide's rule it now ships built:
bin/spl-capture-macos-arm64 and bin/spl-capture-macos-x86_64, signed with
the Developer ID (hardened runtime, audio-input entitlement), from
native/spl-capture.swift via native/build.sh. captureTool() picks the
binary for this platform; where there is none — Windows, until a Windows
implementation exists — measurement reports itself off in the health line
and the device picker while the module stays mounted. The runtime build
and its "building…" state are gone, and the old top-level binary is no
longer gitignored.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-30 10:12:42 -04:00
Waters ChurchandClaude Fable 5.1 2817a444c3 Timers 1.7.0: the LTC capture tool ships prebuilt, for Apple silicon and Intel
The tool used to be compiled by the macOS app build, or by the module
itself on the booth with swiftc — and a catalog install got source only,
which the unbundled 1.10.0 shell made every fresh install. Under the module
guide's rule it now ships built: bin/ltc-capture-macos-arm64 and
bin/ltc-capture-macos-x86_64, signed with the Developer ID (hardened
runtime, audio-input entitlement), from native/ltc-capture.swift via
native/build.sh. Nothing is built or looked for on the machine any more:
captureTool() picks the binary for this platform, and where there is none
— Windows, until a Windows implementation exists — timecode reports itself
off in the tile and the health line while the rest of the module runs.
The runtime build, its "building…" states and the device picker's build
kick are gone with it. The old top-level binary is no longer gitignored
(it is no longer written).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-30 10:11:08 -04:00
Waters ChurchandClaude Opus 5 ece2c0a2c3 Centre the NOW tag; the type badge was already fine
Re-measuring the badges properly changed the answer. The earlier reading
that said the type badge sat 0.52px high was measuring the wrong glyphs: it
fed the canvas the element's raw text ("Song", "Media") while the page
renders text-transform: uppercase ("SONG", "MEDIA"), and a lowercase 'd' or
'g' has an ascender and a descender the capitals never draw.

Measured against the text as actually rendered, the type badge is within
0.10px of centred and wants nothing. The NOW tag was the one off, by 0.59px,
the same way the section chips were. So: the badge padding added in the last
commit is reverted, and the NOW tag gets the correction instead.

That line now reads, in px off centre: NOW 0.01, type badge 0.10, key badge
0.16, section chip -0.01.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 14:57:43 -04:00
Waters ChurchandClaude Opus 5 32180bf299 Centre the text in the song-section chips
The chips looked top-heavy, and measuring said so: the capitals sat 5.12px
below the top of the chip and 4.22px above the bottom. Uppercase text has no
descenders, so the font's descent allowance below the baseline is empty
space the letters never use, while the gap between the ascender line and the
cap height sits above them.

The padding is now a touch shallower on top and deeper below, which puts the
letters themselves in the middle: 4.67px above, 4.68px below. In em, so it
holds wherever the tile's text size is set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 14:57:43 -04:00
Waters ChurchandClaude Opus 5 06066529bc Keys read as notes on the Plan tile's badge too
The item badge still showed Planning Center's raw "Bb" and "F#m" while the
Song Key tile beside it showed B♭ and F♯m. One module should not spell the
same thing two ways.

The notation moves into key-notation.js and both tiles import it, rather
than each keeping its own copy of the parse: the day one of them changes,
they all should.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 14:57:43 -04:00
Waters ChurchandClaude Opus 5 4e5b60e82b PCO Plan 1.9.0: a Song Key tile, and AbleSet-shaped section chips
**Song Key**, a third entry in the + picker: the key of the item
ProPresenter has live, as large as the tile allows. It is set as a note —
B♭, F♯m rather than Bb and F#m — with the song's name and the next keyed
song under it. All three are tile settings: the colour, the name and the
next key. There is no new connection; it reads the same streamed state the
Plan tile does.

The accidental and the quality sit on the same line as the letter. Raising
the accidental read as two stacked characters at tile sizes, which is worse
than plain at a glance from the back of a room.

When the ProPresenter feed that picks the item goes away the key stays up —
the band still needs it — but faded, and the tile's dot says it is the last
item rather than the current one.

**The section chips** on the Plan tile's current item are rounded squares
now, with the tinted ground and transparent border the AbleSet module gives
its own section pills, so the two read as one family. The chip playing keeps
its own ground so its progress fills against the chip rather than against
the row behind it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 14:57:43 -04:00
michael 92b98b6579 Merge pull request 'SPL Meter 0.4.3: a failed capture-tool build must not park the listener forever' (#31) from claude/spl-build-retry into main 2026-09-28 11:31:49 -04:00
michael 58f3e5010a Merge pull request 'SPL Meter 0.4.2: the device picker can build the tool it needs' (#18) from claude/spl-devices-without-binary into main 2026-09-28 11:30:36 -04:00
Waters ChurchandClaude Fable 5 86a3270f1c Timers 1.6.0: the ProPresenter video countdown, with looping videos hidden
Time left on the video ProPresenter is playing — the one number a booth
needs during a bumper or sermon video — was missing: the module only read
the configured timers, and the video countdown lives in the transport
routes. Verified live on ProPresenter 21.4:
  GET /v1/transport/presentation/current → { is_playing, uuid, name,
    artist, audio_only, duration }
  GET /v1/transport/presentation/time    → bare seconds as a sub-second
    float — better than /v1/timer/video_countdown's whole-second string,
    so no quantization phase lock is needed, only an anchor hold (a frame
    is broadcast only when the anchor drifts, not per poll).

The Video card rides the ProPresenter source: transport current on the
slow lane, position on the hot path (skipped while no media is loaded),
running/paused/idle from is_playing and position, the media name (minus
its file extension) as the detail line. It persists as an idle '—'
between videos — a wall tile must not appear and vanish with the media —
and is absent only on a ProPresenter whose transport routes 404.

No looping flag exists anywhere in the API, so looping is inferred: the
same media's position jumping backwards is a wrap, and the flag sticks
until the media changes. Each tile gets a 'Looping videos' switch
(default off): off, a looping background's meaningless time is hidden —
the card sits out of a multi-card tile, and a solo Video tile shows a
quiet 'Looping' card instead of sitting empty. Shown loops never take
the warn/overrun tones: a 15 s background flashing red every wrap would
cry wolf all service.

The picker offers 'Video' as its own tile (solo gear carries the loop
switch); pp-timers-mock gains the transport routes with a 4-minute cycle
(bumper playing → paused → resumed → nothing → looping background).
Verified in the browser across the whole cycle, both tile kinds, and the
per-tile toggle. Selftests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-27 11:32:36 -04:00
Waters ChurchandClaude Fable 5 e9d6edc2c8 SPL Meter 0.4.3: a failed capture-tool build must not park the listener forever
Same deadlock the Timers module hit live on AB (fixed there in 1.5.1):
start() skipped scheduling its retry while a build was in flight, and
ensureCaptureBinary's onReady only fires on build SUCCESS. A failed
build therefore left the listener waiting forever on a callback that
never comes — "Building the capture tool…" indefinitely — and a binary
scp'd in from outside (the required path on AB, whose CLT cannot
compile) was never picked up without a module remount.

onReady stays as the fast path; the 30-second retry is now armed in
every not-ready state, so a failed build keeps re-checking and an
externally supplied binary is found within half a minute.

Reproduced pre-fix (build fails at 0s, binary copied in at 3s, still
untouched at 38s) and verified post-fix (retry at 30s spawns it).
spl-capture --selftest unchanged against the IEC 61672 table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-27 11:03:57 -04:00
Waters ChurchandClaude Fable 5 62c34fe80c Timers 1.5.1: a failed capture-tool build must not park the listener forever
Found live on AB: the build failed (its CLT is broken), the compiled
binary was then copied in by hand — and the listener never noticed. It
had seen 'build in flight' and stopped scheduling retries, waiting for
ensureCaptureBinary's onReady, which only fires on SUCCESS. The tile said
'Capture tool building…' indefinitely with a working binary on disk; only
a module remount recovered it.

onReady stays as the fast path; the 30 s retry now stays armed in every
not-ready state, so a failed build keeps re-checking and a binary that
arrives from outside (scp'd to a machine that can't compile) is picked up
within half a minute.

Reproduced the exact shape in a swiftc-less environment: build fails,
binary copied in at 3 s, retry at 30 s spawns it (the capture's own
unknown-device answer proves the real binary ran) — previously a
permanent deadlock.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-27 10:55:44 -04:00
michael 21519dd3da Merge pull request 'Timers 1.5.0: the LTC capture tool installs itself; tiles warn in one line' (#27) from claude/timers-capture-install into main 2026-09-27 10:08:25 -04:00
Waters ChurchandClaude Fable 5 8843339f11 Timers 1.5.0: the LTC capture tool installs itself; tiles warn in one line
A catalog install ships source only, and a catalog update deletes the
module folder — a previously built ltc-capture binary with it — so every
install or update left LTC dead until someone compiled the tool by hand.
The build also still used the blocking spawnSync that once froze the whole
server (the bug spl-meter fixed in 0.4.0), and only ran when a listener
started: no tool meant no device list, and no device meant no listener.

ensureCaptureBinary() (module scope in ltc-listener.js, the spl-meter
0.4.2 pattern) now builds the tool in the background as part of coming
up: kicked when the LTC source starts and by the admin device picker,
single-flight, built to a temp name and renamed so a killed build can't
leave a truncated binary, and rebuilt behind a still-serving binary when
a module update leaves the source newer. The waiting listener starts
itself the moment the build lands.

Tile-side warnings are now one line — 'Not configured — see Admin',
'Unreachable', 'Capture tool building…' — with the client's source-label
prefix no longer repeated. The full diagnostics (host:port, errno, the
capture tool's own words, build instructions) move to the admin health
line and the log, where the person who can act on them is looking.

Verified: build from the exact catalog-install state (source only) lands
and lists 12 real devices; touching the source triggers an atomic rebuild
while the old binary keeps serving; /ltc/devices kicks the build and
answers with the device list; live server shows terse source messages on
the tile and the detailed story in /api/admin/state health. ltc and phase
selftests still pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-27 09:47:05 -04:00
Waters ChurchandClaude Opus 5 aec9c47ed4 PCO Plan 1.8.0: the plan follows AbleSet, and auto-scroll stays in its tile
Three things, the first on its own.

**Auto-scroll moves the tile, not the board.** `scrollIntoView` walks up and
scrolls every ancestor that can scroll, so bringing the current item into
view dragged the whole dashboard with it whenever the board was taller than
the window. The list's own `scrollTop` is moved instead, measured from
rectangles because the rows' offset parent is the tile rather than the list.

**The current item fills as its song plays.** When an AbleSet module is
installed, configured and playing a song that IS the item on screen, that
item's background fills left to right with the song. The song is tied to a
plan item by the same name matching the live ProPresenter document already
uses, so a bar can never quietly fill the sermon while the band rehearses
something else.

**Its sections slide past, bottom right of that item.** Every section of the
song as a strip, the played ones dimmed, the one playing filling in turn and
kept centred — by moving that strip's own `scrollLeft`, for the same reason
as above. The chips are rebuilt only when the song changes; moving to the
next section just moves the classes, so the strip keeps its place.

Both are switches in the tile's gear, **Song progress** and **Song
sections**, and both draw nothing at all unless there is an AbleSet module
to follow: the server reports `available: false` for no module, a disabled
one, or one with no host, and re-checks every 15 s so installing AbleSet
later lights it up without a restart.

The server follows the AbleSet module over loopback exactly as it follows
ProPresenter's, and republishes a small slice — song, sections, playhead in
beats with a stamp — as its own `ableset` SSE event. Its own event because
it moves five times a second and must never drag the plan (items, teams)
onto the wire with it; the tile carries the playhead forward from the tempo
between packets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 09:14:19 -04:00
michael 5f60cd563c Merge pull request 'Timers 1.4.2: ProPresenter digits no longer flick back a second' (#26) from claude/timers-second-phase into main 2026-09-27 09:05:46 -04:00
Waters ChurchandClaude Opus 5.5 c1d9c59325 Merge origin/main into claude/proddash-ableset-module-6c2556
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:01:35 -04:00
Waters ChurchandClaude Fable 5 2f4c357bf5 Timers 1.4.2: ProPresenter digits no longer flick back a second
ProPresenter reports running timers as truncated whole seconds, and the
poller re-anchored the tiles' local count at the bottom of that second on
every 500 ms poll — so the displayed digit ran up to a second ahead and
got yanked back a step whenever a fresh poll sampled the next flip later
than the last one did. Tiles flickered ...12:33 → 12:34 → 12:33... about
once a second.

phase.js turns the quantized reports into one stable anchor per timer:
the only information in a poll is a FLIP (the report stepping one second
in the direction of travel), which dates the true boundary to the
midpoint of the two polls that straddle it. While reports stay consistent
with the anchor (±450 ms) the anchor holds, so tiles count along one
steady clock; a report the anchor can't explain (reset, seek, resume,
accumulated drift) re-anchors. Timers carry the anchor as a per-timer
asOf the client extrapolates from (optional in the provider contract,
documented in the guide); the client additionally refuses to tick a
running card backwards over sub-1.5 s jitter.

phase.selftest.js (66 checks) drives a simulated ProPresenter — truncated
reports, jittered polls — through the tracker and the client's render
math: digits never step backwards, dwells hold 1 s (the ~2 s zero-crossing
dwell is trunc's honest behavior), anchors stay within tolerance of the
true value, resets snap, pause/resume re-locks, 0.2% clock drift is
absorbed. Verified live against tools/pp-timers-mock.js: 78 s of 100 ms
DOM sampling across four running cards — every step ±1 in the card's own
direction, dwells 998–1002 ms, zero regressions; upstream kill/restart
still recovers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-27 08:46:50 -04:00
Waters ChurchandClaude Opus 5 6c0aee5abf Teams tile: one Colors group, no Positions switches
Colors and Status colors were two groups asking the same question, so they
are one: team header, the tints and the three status colours together.

The Positions switches go entirely — the team switches already decide what
a tile shows, and "Group by position" still arranges what is left. With
them go GET /positions and positionOptions().

Two rules the tile was breaking on the way past: a gear group may carry no
`help` (rule 5), and only hide-toggles belong in Show (rule 8) — so the
group blurbs are gone and the Columns picker sits loose at the top beside
the team switches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 15:15:34 -04:00
Waters ChurchandClaude Opus 5 6d0f8828c1 PCO Plan 1.7.0: a Teams tile — who is scheduled, with photos
The module now offers two tiles in the + picker, Plan and Teams, sharing
the one Planning Center connection and the one poll.

Teams draws the current plan's team and position assignments: each team as
a band of its people, with the person's photo, their position and a dot for
confirmed / unconfirmed / declined. Photos are proxied and cached by the
module (GET /team-avatar/<planPersonId>) so no browser ever talks to
Planning Center; a person with no photo gets their initials.

Everything shown is the tile's own choice, so two tiles can differ:
- Teams and Positions — a switch per team and per position, all on until
  someone flips one (GET /teams, GET /positions feed the lists)
- Colours — a picker for the team headers, or Tint teams to give each its
  own, optionally carried into the rows, plus the header fade and pickers
  for the three statuses
- Show — twelve switches (top bar, team headers and counts, avatars,
  positions, group by position, status, notes, declined and unconfirmed
  people, teams nobody is on, columns)

Server: team_members?include=team,person rides a slow lane (re-read on a
plan change, else at most once a minute) and can never fail the plan read —
a lost roster keeps the last good one and says so. The service type's own
team list is read too, cached ten minutes: Planning Center only returns a
team once somebody is scheduled on it, so that list is the only way to show
a team nobody has signed up for.

No shell changes: picker entries carrying their own instanceSchema, the
colour field and multiselects all already exist.

tools/pco-mock.js grows team_members, the service type's teams and stand-in
avatar images, including a declined person, people with no photo and a team
nobody is on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 12:42:57 -04:00
christian 8ca88ba10b Merge pull request 'Resi Cues: the countdown to the next cue, on the board and as a timer' (#22) from claude/resi-stage-cues into main 2026-09-24 12:19:49 -04:00
christianandClaude Opus 5 a8d1b8e159 Resi Cues becomes a real module, and Companion keeps the stage timer
Yesterday's version of this was a Python script under `modules/` with a
LaunchAgent to keep it alive — which the guide rules out in as many words:
everything a module does runs inside its server entry, no companion programs
or daemons an operator starts by hand. It also could not put anything on a
dashboard, which is half of what it was wanted for. Both halves are now in
the right place.

`modules/resi-cues/` is an ordinary module. Its tile is one number as large as
the tile allows — the countdown to the next Resi cue, the cue's name above it,
the playhead and the cue just passed underneath, and the whole cue list behind
a tile switch. It also declares `"provides": ["timers"]` and serves `/timers`
and `/timers/stream`, so the countdown and the playhead turn up among the
ProPresenter timers in the Timers module without either side knowing about
the other.

The countdown ticks in the browser. The server sends the playhead with the
instant it was true and whether playback is running, and the tile adds the
elapsed time since; the Timers module extrapolates from `startedAt` the same
way. So a snapshot only has to be sent when something structural changes — a
new cue, a pause, a seek — rather than at 1 Hz to every open dashboard.

`resi.js` holds the part worth reading twice. Resi republishes a decoder's
position only every few seconds, on an irregular cadence, and each reading is
already stale when it appears; a countdown built straight from it sits seconds
behind and lurches. So every reading is kept as media time minus wall time and
the largest recent value is trusted — the reading that arrived with the least
delay — which a slow round trip cannot drag backwards. A reading outside the
link's measured noise floor counts as a seek: drop the lock, snap, and re-read
the cue list at once instead of on its timer. Playback is only called paused
once the playhead has been still for longer than Resi's own cadence, because a
fixed timeout mistakes a slow update for a stop and freezes the countdown
mid-service.

Driving the ProPresenter timer on the stage screen is Companion's job now. It
is already a service on that machine, so nothing new has to be kept alive, and
this module no longer has to reach ProPresenter at all — it only has to be
right about Resi, which is the hard part.

`resi.test.js` drives the estimator on a fake clock against nine simulated
Resi profiles — cadences of one to twelve seconds, staleness to three,
irregular timing, variable round trips — covering lock-on, a fired cue, a seek
backwards, a pause, and a pause longer than the offset window. A punctual
fixture proves nothing here: against one, three separate breakages of the
estimator go unnoticed. `server.test.js` runs the real server entry against a
canned Resi account and checks what the two consumers actually read, including
the `/timers` shape field by field. Every mutation tried against either turns
them red.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 12:08:55 -04:00
christianandClaude Opus 5 82c552955d Resi cue countdowns reach the stage screen without Companion
The ask was a Stream Deck button showing the time until the next Resi cue.
What it turned into is simpler: ProPresenter already renders countdowns, and
it already has an HTTP API, so nothing has to sit in between except something
that knows when the next cue is.

`resi_to_propresenter.py` watches a decoder's playhead and keeps a
ProPresenter countdown timer armed with the seconds until its next cue, with
the cue's name as a stage message beside it. Both halves of that come from
Resi's cloud rather than the decoder — the decoder reports its position
upstream and `central.resi.io` hands it back — so there is nothing to install
at the decoder end and nothing polling it every second.

The playhead is estimated rather than repeated. Resi republishes `position`
only every few seconds, irregularly, and each reading is already stale when
it appears; sent straight through, the countdown sat three seconds behind and
lurched. So each reading is kept as media time minus wall time and the
largest recent value is trusted — the reading that arrived with the least
delay — which a slow round trip can no longer drag backwards. A reading
outside the link's measured noise floor counts as a seek: drop the lock, snap,
and re-read the cue list immediately instead of on its timer. Playback is
only called paused once the playhead has been still for longer than Resi's own
cadence, because a fixed timeout mistakes a slow update for a stop and freezes
the countdown mid-service.

This is not a tile. There is no `module.json`, so `readManifests` skips the
folder and it never reaches the + picker; it lives under `modules/` because
that is where the production tooling lives. That is also why it carries a
README when no real module does — there is no *Included modules* entry it
could have instead — and the repo README says so where the module list ends.

`install-agent.command` sets up a LaunchAgent so it runs from login and
restarts if it stops. It deliberately runs a copy in Application Support: a
login agent cannot read `~/Documents` without Full Disk Access, and macOS
answers "Operation not permitted" rather than asking. Settings live there too,
outside the checkout, the same way module config does. The Resi password is
read from the Keychain and never written to disk; with none set the agent
says so once and waits rather than relaunching in a loop.

`test_playhead.py` drives the estimator on a fake clock against nine
simulated Resi profiles — update cadences of one to twelve seconds, staleness
to three, irregular timing, variable round trips — and six of seven deliberate
breakages of the estimator turn it red. `test_end_to_end.py` runs the real
loop against a real ProPresenter with a simulated decoder, and has: it armed
for the first cue, counted down in step, rolled over to the second and stopped
when the cues ran out. Neither covers the live Resi calls, which need real
credentials.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 11:35:40 -04:00
christian f542b34b6c Merge remote-tracking branch 'origin/main' into claude/service-times-module
# Conflicts:
#	README.md
2026-09-24 11:33:48 -04:00
christianandClaude Opus 5 098562c088 Band Lineup's docs move to where every other module's live
Two things were out of place. `modules/band-lineup/README.md` was the only
README inside any of the eight modules — the guide's folder shape is manifest,
client, server, assets, and every other module is described in this README's
*Included modules* instead. It was also describing a module that no longer
exists: photos and birthdays "come from Planning Center", guests "pinned per
slot", and a refresh interval the manifest has never had.

The entry here had drifted the same way, calling the Drive folder somewhere
to put "manual photos" and the board something Planning Center fills. It is
the other way round now, and has been since 1.10: the board is shared server
state and the source of truth, the photo library fills it, and Sync is
auto-fill that only touches slots nobody placed by hand. The entry says that,
and picks up the Choir spot and the title-bar columns while it is there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 11:30:17 -04:00
christianandClaude Opus 5 3ca960baf6 Service Times 1.0.0: every campus's services, and what production can't use
Four campuses run different Sunday schedules and take their resi feed from one
of two broadcast origins. When a service goes wrong — a machine crashes, a feed
is unusable — the booth has to know which service at which campus can be
altered, and which campuses are riding on the one that broke. That lived in
people's heads and in messages.

The times come from the church website rather than from configuration, because
the site is what stays current when services change through the year. Each
campus page carries a "Times" block; the module reads it and re-reads it on a
timer. Two things on those pages are not services and are excluded on purpose:
the main-office hours in every page's footer, and the "Office Hours" block
Apollo Beach carries inside its own Times section. The parse is bounded to the
Times heading and stops at either.

A mark on a service — issue, replay, do not use, with a note, and for a resi
campus which of the origin's services it is taking — is the booth's shared
answer, so it lives in the module's server state and reaches every tile over
SSE. Per-tile settings would let two campuses disagree about whether the 9:30 is
going out. Marks are written to the data directory so a restart mid-morning
does not lose them, and they expire after 36 hours so last week's never greet a
new Sunday.

Campuses added by hand in admin are a stopgap for a site that has not caught up
yet: as soon as the website publishes that campus, the site's times take over
and the admin health line says the manual line can be removed.

Verified: the parser against all five live campus pages, and 31 unit tests plus
16 client checks, each proven to fail under mutation — both parse guards, the
merge rule, dependent recording, and three client behaviours. The suite is run
here under JavaScriptCore, since this machine has no Node.

Not verified here: the module guide's resilience checklist needs a running
ProdDash — upstream loss and recovery, two tiles side by side, and tile removal
leaving no timers behind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 16:57:48 -04:00
Waters ChurchandClaude Opus 5.5 9192eed318 AbleSet 1.0.1: a remounted tile asks for the state instead of waiting
Every gear change remounts the tile, and the remounted tile joins the
page's shared stream mid-flow, so it heard nothing until AbleSet next
changed — with the transport stopped, never. Tiles went blank until a
reload. The tile now fetches /state when it starts and whenever the
stream (re)opens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 16:07:13 -04:00
Waters ChurchandClaude Opus 5 9313c9e42a SPL Meter 0.4.2: the device picker can build the tool it needs
NA could not list a single audio interface. The module was installed from
the catalog, which ships source only because the capture binary is
gitignored, and the picker shells out to that binary with --list. So it
returned nothing, forever:

  [spl-meter] device list failed: spawn .../spl-capture ENOENT

The missing-binary build existed, but only inside the listener, and the
listener only starts once a device has been picked. No tool meant no device
list, and no device meant no listener to build the tool. Nothing was going
to break that on its own.

The build now lives at module scope and both callers use it: init() asks for
it on mount whether or not a device is configured, and /devices asks again
each time it is called, so opening the admin page is enough to start it. It
is still a plain spawn that never touches the event loop, and health() now
names the situation instead of leaving an empty dropdown unexplained.

Also removed two per-listener build flags left shadowing the module-scope
ones, which would have made the "is a build already running" test always
false and let a second build start under the first.

Tested the way it actually failed: a data directory holding only the six
source files, no binary. The build starts on mount and /devices answers with
the real device list moments later.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 15:25:43 -04:00
Waters ChurchandClaude Opus 5.5 41fd76817c AbleSet 1.0.0: follow AbleSet's setlist over OSC
A new module that subscribes to AbleSet's OSC API (UDP 39051) and streams
what it is playing to ten tiles: Now Playing (song, tempo, meter, duration,
time left, section, progress bar, next song, songs left), Song Sections
(a sliding strip, or a lottery wheel from the title bar), Setlist,
Transport, Tempo, Song Time, Set Time, Section, Next Song and Status, each
coloured by its state. The song and set countdowns are offered to the
Timers module.

A heartbeat watchdog resubscribes when AbleSet goes quiet (a restarted
AbleSet forgets its subscribers). Time left falls back to the song length
and playhead when AbleSet stops sending it (Live disconnected).

tools/ableset-mock.js plays a setlist over the same API for testing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 14:24:19 -04:00
Waters ChurchandClaude Opus 5 c8b51c6c2f Spectrogram: stop dragging the labels along with the picture
The spectrogram scrolled by copying the visible canvas onto itself one step
along. That canvas already held the frequency labels and gridlines, because
they were drawn straight onto it, so every frame smeared them sideways and
left a trail. Arrived with the labels in 0.3.0; the gridlines did it too.

Only measured pixels belong on a surface that scrolls. The history now lives
on its own offscreen canvas that holds nothing but spectrum, and the visible
canvas is repainted from scratch each frame — history first, annotations
over the top. Nothing drawn as an annotation can reach the scroll buffer by
construction, rather than by remembering to clear the right strip.

The layout was muddled in the same place and is fixed with it: the gutter
used to be reserved along the bottom while the labels were drawn down the
left, overlapping the plot. Labels now sit beside the axis they describe —
down the left when frequency runs vertically, with room for "12.5k", and
along the bottom when it runs horizontally.

Verified both orientations by fingerprinting the gutter and the plot two and
a half seconds apart: gutter identical, plot changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 09:58:32 -04:00
Waters ChurchandClaude Opus 5 c464e5b4ca SPL Meter 0.4.0: a dead input must not look like a quiet room
AB's meter appeared to halt for a day and a half. Nothing had halted: the
capture child had run continuously since the machine was restarted, never
stalled, never exited, and was delivering twenty frames a second. Every one
of them was digital silence, because Dante Via stopped passing audio after
the reboot — the Timers module's LTC listener, on channel 16 of the same
device, had been reporting no signal for exactly as long.

The module's part in that was reporting "ok" through all of it and drawing
a steady -83, which is the -200 floor plus the +117 calibration offset. A
plausible number on a wall is believed, and a green dot beside it says the
number is worth believing. That is worse than showing nothing.

So silence is now a state the module names. Below -150 dBFS unweighted the
input is not quiet, it is absent: a microphone in a silent room still reads
about -83, and even a muted converter dithers. Five seconds of that on every
channel and health() goes to error naming the device and how long it has
been dead, the tiles stop drawing a level and say "No audio" with how long,
and the status dot goes red.

Verified both directions, which is the half that matters: BlackHole with
nothing routed into it is caught in five seconds, and a real microphone in a
quiet room at -49 dBFS is never flagged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 09:54:27 -04:00
Waters ChurchandClaude Fable 5.1 c73d0a3aac PCO Plan 1.6.0: the message document is matched by the plan's own name
The current item followed the live ProPresenter document by title alone,
and a sermon's document is named after the sermon — Waters names them
"YYMMDD Series Title - Part #" — never after its plan item. So the last
song stayed current through the whole message: North Attleboro on 20 Sept
spent 65 minutes on one song in two of its three services.

A live document that reads like the plan itself — its date code, series
title and part — now lands on the plan's message item: an item titled
Message, Sermon or Teaching, else the first item under such a header. A
date code naming another day is a firm no, so last week's sermon stays
unplaced. Position in the ProPresenter playlist breaks a tie between
equal names, only while the playlist mirrors the plan item for item, and
never places a document on its own.

The log now says what it never said: every feed drop and reconnect, which
item the message document will land on, what placed each item and how,
and a live document that is not in the plan.

matching.test.js: 27 checks built from both campuses' real 27 Sept plans
and ProPresenter playlists.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 13:06:53 -04:00
Waters ChurchandClaude Fable 5.1 5fb90190ae Band Lineup 1.11.0: into the catalog, and to the guide's rules
The module arrived in the previous commit exactly as it runs in North
Attleboro. This brings it to the interface rules every module follows and
puts it where the admin page can offer it.

The three hide-toggles — names, mic labels, parallax — sat in a folded
"Show / hide" group. Rule 8 puts four or fewer at the top of the gear with
no group at all, so they are ungrouped and first; the Birthday group keeps
its switch. The test-mode select loses its help line (rule 5). Everything
else in the gear was already within the rules, and every title-bar control
was already an icon with a tooltip.

The Planning Center mock moves out of the module folder to tools/, beside
the other upstream mocks, so an install does not download a development
tool with the module; README and CLAUDE.md say how to run it. The version
goes to 1.11.0 so the copy already installed on NA is offered the update
from the repo, and the module joins the README's list.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 09:19:34 -04:00
christian 684f06abed Band Lineup module, as it runs in North Attleboro
The backstage stage board as a ProdDash tile: everyone scheduled on the
band, vocal and production teams for the upcoming Planning Center plan,
placed into their mic slot with photo, name and mic/pack label. Birthdays
in the service week get confetti. The display half of the standalone Band
Lineup Electron app, ported to a module — imported here exactly as
installed on NA-Ingest (v1.10.1), so the repo is where it lives from now on.
2026-09-22 09:15:22 -04:00
Waters ChurchandClaude Fable 5.1 a7e40ee1a8 Layouts keep every tile they are given, and show what is on offer
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>
2026-09-20 12:02:34 -04:00
Waters ChurchandClaude Opus 5 87929bfd0b Don't take the server down when the module unmounts
Deploying 0.3.0 to AB crashed the ProdDash server on restart:

  TypeError: Cannot read properties of null (reading 'streams')
      at server.js:273  req.on('close', () => state.streams.delete(res))

teardown() nulls `state` and then ends every open stream, which fires each
request's close handler — and the handler reached back through the `state`
it had just discarded. Any re-init with a browser connected would do it, so
saving admin config while a dashboard was open was enough to stop the whole
server, every module with it.

The stream handler now closes over this mount's Set rather than the module's
state: after teardown it deletes from a set nobody reads, which is harmless.
The same hazard existed anywhere a callback could outlive the mount — the
keepalive ping, a frame arriving from the listener mid-teardown, and the
/measure interval — and each is closed over or guarded now.

Reproduced first (open an SSE client, save config, watch it fall over) and
confirmed fixed against the same sequence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 16:06:47 -04:00
Waters ChurchandClaude Opus 5 46d44d494b Keep the banding caveat where the band plan is built
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 16:00:34 -04:00
Waters ChurchandClaude Opus 5 44fd0b10ba SPL Meter 0.3.0: bring the tiles to the interface rules
Rebasing onto main landed rules 5 to 11, which this module predates and
broke in three places.

Rule 5 — no explanatory text in a tile's gear: the Banding and Smoothing
fields each carried a paragraph. The Banding one said something true and
worth knowing, that below roughly 200 Hz the window length limits real
resolution however fine the setting, so it moves to the source where the
band plan is built rather than being lost. Option labels shortened with it.

Rule 11 — one control per action: the spectrogram offered its axis both as
a gear field and as a title-bar button. The button stays, since that is
what was asked for, and the field is gone. The setting still persists with
the tile; saveInstanceSettings keeps keys that no schema declares.

Rule 9 — title-bar controls are icons: the axis button was drawing a "⇆"
as a label. It is an inline SVG now, with the tooltip carrying the words
and naming the current direction rather than the action.

Rule 8 also asks that a tile can switch off what it draws, so the readout
gained a Caption toggle and both graphs gained Labels.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 16:00:17 -04:00
Waters ChurchandClaude Opus 5 543795f9fd Let the SPL readout fill its tile
The number was capped at 6rem, so a readout given half a wall stopped
growing at the size it reached in a small tile.

Removing the cap was not enough on its own. Sizing text off container units
means choosing a coefficient for some assumed number of characters, and the
value here is not a fixed width: the coefficient that fills a tile nicely
for "-45.9" pushes "-100.5" nine percent out through the side. Measured it
rather than guessed — the tile now sets a trial size, reads the width the
actual text wants, and scales to fit. That runs only when the tile size or
the length of the reading changes, not on every frame.

It now fills about 98% of the width wherever width is the limit and 92% of
the height wherever height is, at every tile size from 180x70 up to
1400x700, with no overflow on either axis. The CSS keeps a conservative
size for the first paint before the measurement has run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 15:56:27 -04:00
Waters ChurchandClaude Opus 5 d2d4207fe4 SPL Meter 0.2.0: one measurement, many views
Every control asked for here is a view choice, so none of them may reach
the capture child — two dashboards watching the same room must be able to
disagree about how to draw it without disturbing the measurement or each
other. That set the shape of the change: the child now sends everything
anyone might ask for, once, and each tile folds it down.

Averaging (was "Reading") is Fast, Slow, Inf and None, and the child
computes and sends all four every frame. Fast and Slow are the IEC time
constants, Inf is Leq since reset, None is the block with no time constant
at all. The admin-wide time constant is gone — it was never a property of
the room.

Banding is 1/3 through 1/48 octave. The wire always carries 1/48, and
because every banding offered divides 48 a coarser band is an exact sum of
finer ones in the power domain. Nothing is resampled and no tile's choice
is visible to another. The transform grew to 16384 points to have bins fine
enough to mean it; below roughly 200 Hz a 1/48 band is still narrower than
one bin, which the Banding help says out loud rather than pretending.

Each tile kind now carries its own settings, which is the fix for the
complaint that started this: the RTA was offering an alarm threshold and a
metric picker that did nothing to it. An entry's instanceSchema replaces the
module's, so a gear popover only holds controls that change that tile.

Also: frequency labels in three tiers that thin out as a tile narrows
instead of smearing, gridlines off by default on both graphs, RTA smoothing
as a damping factor on the drawn value only, a title-bar toggle to swap the
spectrogram's axes, and the threshold blurb removed.

Settings saved by 0.1.0 are mapped forward — a layout on a wall should not
start reading dashes because a field was renamed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 15:56:27 -04:00