Three changes, the first unrelated to the other two.
Auto-scroll moves the tile, not the whole board
scrollIntoView walks up and scrolls every ancestor that can scroll, so bringing the current item into view dragged the 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.
Proved both ways on the real elements, with the board scrolled to its limit (108) and a row far down the list:
board
list
scrollIntoView (before)
108 → 0
scrolled
this change
108 → 108
0 → 573
And end to end: across a real live-item change from the sermon to the closing song, the board held at 108 for every sample while the list scrolled 436 → 510 to centre the new item.
The current item fills as its song plays
While 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 (matching.js), so a bar can never quietly fill the sermon while the band rehearses something else. In testing, AbleSet's 2 - GOODNESS OF GOD - A landed on the plan's "Goodness of God" through the track number and key suffix; when the mocks drifted so that AbleSet's song was not the current item, the tile correctly drew nothing.
Its sections slide past, bottom right of that item
Every section of the song as a strip: 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; advancing a section just moves the classes, so the strip keeps its place rather than resetting thirteen times a song.
Both hide themselves completely
Switches in the tile's gear (Song progress, Song sections), and neither draws anything 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.
Situation
What the tile draws
No AbleSet module, or disabled
nothing
Installed, no host configured
nothing
Configured, AbleSet not answering
nothing
Playing a song that is not the current item
nothing
Playing the current item
fill + sections
How the data gets there
The server follows the AbleSet module over loopback exactly as it already 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, so the fill glides rather than stepping.
A client-side subscription to /api/modules/ableset/stream was the other option and was rejected: the shell multiplexes every module stream into one page connection precisely to stay under the browser's six-connection limit, and a raw EventSource would have bypassed it.
Verified
Against the real AbleSet module (from PR #17) driven by its own tools/ableset-mock.js, plus the Planning Center and ProPresenter mocks:
The whole chain: OSC mock → AbleSet module → pco-plan over loopback → the tile. Fill advanced 40% → 63% over a song while the sections strip scrolled 156 → 399 keeping the active section centred, with the page and list scroll untouched throughout.
Each switch off individually, and both off.
AbleSet's upstream killed mid-song (reachable: false) and the module disabled entirely (available: false) — the tile drops both features and the rest of the plan is unaffected.
The existing 43 tests pass and the server log is clean.
Note on ordering: the AbleSet module itself is still open as PR #17 and is not on main, though both booths run it as a data-dir copy. Nothing here depends on that merging — with no AbleSet module this is simply invisible — so the two can land in either order.
Three changes, the first unrelated to the other two.
## Auto-scroll moves the tile, not the whole board
`scrollIntoView` walks up and scrolls **every** ancestor that can scroll, so bringing the current item into view dragged the 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.
Proved both ways on the real elements, with the board scrolled to its limit (108) and a row far down the list:
| | board | list |
| --- | --- | --- |
| `scrollIntoView` (before) | 108 → **0** | scrolled |
| this change | 108 → **108** | 0 → 573 |
And end to end: across a real live-item change from the sermon to the closing song, the board held at 108 for every sample while the list scrolled 436 → 510 to centre the new item.
## The current item fills as its song plays
While 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 (`matching.js`), so a bar can never quietly fill the sermon while the band rehearses something else. In testing, AbleSet's `2 - GOODNESS OF GOD - A` landed on the plan's "Goodness of God" through the track number and key suffix; when the mocks drifted so that AbleSet's song was not the current item, the tile correctly drew nothing.
## Its sections slide past, bottom right of that item
Every section of the song as a strip: 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; advancing a section just moves the classes, so the strip keeps its place rather than resetting thirteen times a song.
## Both hide themselves completely
Switches in the tile's gear (**Song progress**, **Song sections**), and neither draws anything 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.
| Situation | What the tile draws |
| --- | --- |
| No AbleSet module, or disabled | nothing |
| Installed, no host configured | nothing |
| Configured, AbleSet not answering | nothing |
| Playing a song that is not the current item | nothing |
| Playing the current item | fill + sections |
## How the data gets there
The server follows the AbleSet module over loopback exactly as it already 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, so the fill glides rather than stepping.
A client-side subscription to `/api/modules/ableset/stream` was the other option and was rejected: the shell multiplexes every module stream into one page connection precisely to stay under the browser's six-connection limit, and a raw `EventSource` would have bypassed it.
## Verified
Against the real AbleSet module (from PR #17) driven by its own `tools/ableset-mock.js`, plus the Planning Center and ProPresenter mocks:
- The whole chain: OSC mock → AbleSet module → pco-plan over loopback → the tile. Fill advanced 40% → 63% over a song while the sections strip scrolled 156 → 399 keeping the active section centred, with the page and list scroll untouched throughout.
- Each switch off individually, and both off.
- AbleSet's upstream killed mid-song (`reachable: false`) and the module disabled entirely (`available: false`) — the tile drops both features and the rest of the plan is unaffected.
- The existing 43 tests pass and the server log is clean.
**Note on ordering:** the AbleSet module itself is still open as PR #17 and is not on `main`, though both booths run it as a data-dir copy. Nothing here depends on that merging — with no AbleSet module this is simply invisible — so the two can land in either order.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Three changes, the first unrelated to the other two.
Auto-scroll moves the tile, not the whole board
scrollIntoViewwalks up and scrolls every ancestor that can scroll, so bringing the current item into view dragged the dashboard with it whenever the board was taller than the window. The list's ownscrollTopis moved instead, measured from rectangles because the rows' offset parent is the tile rather than the list.Proved both ways on the real elements, with the board scrolled to its limit (108) and a row far down the list:
scrollIntoView(before)And end to end: across a real live-item change from the sermon to the closing song, the board held at 108 for every sample while the list scrolled 436 → 510 to centre the new item.
The current item fills as its song plays
While 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 (
matching.js), so a bar can never quietly fill the sermon while the band rehearses something else. In testing, AbleSet's2 - GOODNESS OF GOD - Alanded on the plan's "Goodness of God" through the track number and key suffix; when the mocks drifted so that AbleSet's song was not the current item, the tile correctly drew nothing.Its sections slide past, bottom right of that item
Every section of the song as a strip: 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; advancing a section just moves the classes, so the strip keeps its place rather than resetting thirteen times a song.Both hide themselves completely
Switches in the tile's gear (Song progress, Song sections), and neither draws anything unless there is an AbleSet module to follow. The server reports
available: falsefor 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.How the data gets there
The server follows the AbleSet module over loopback exactly as it already follows ProPresenter's, and republishes a small slice — song, sections, playhead in beats with a stamp — as its own
ablesetSSE 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, so the fill glides rather than stepping.A client-side subscription to
/api/modules/ableset/streamwas the other option and was rejected: the shell multiplexes every module stream into one page connection precisely to stay under the browser's six-connection limit, and a rawEventSourcewould have bypassed it.Verified
Against the real AbleSet module (from PR #17) driven by its own
tools/ableset-mock.js, plus the Planning Center and ProPresenter mocks:reachable: false) and the module disabled entirely (available: false) — the tile drops both features and the rest of the plan is unaffected.Note on ordering: the AbleSet module itself is still open as PR #17 and is not on
main, though both booths run it as a data-dir copy. Nothing here depends on that merging — with no AbleSet module this is simply invisible — so the two can land in either order.🤖 Generated with Claude Code
705e40d42atoaec9c47ed4