Timers: steadier menu, ellipsised labels, saner warning window

- The Timers ▾ menu's rows are rebuilt only when the row set or a switch
  changes, not on every snapshot — rows no longer vanish from under a
  click. Their on-state fill uses the shell's accent: the menu hangs off the
  title bar, outside the tile root, where the tile's colour properties
  don't reach.
- A long timer label ends in an ellipsis (the label is its own box inside
  the flex name row).
- The warning window is capped at half a timer's length, so a 20 s
  countdown isn't amber for its whole run (it warns under 10 s; a 25 min
  one under 2:30).
- Server: the change key that gates SSE frames ignores startedAt (derived
  from a whole-second value, it jittered ±500 ms per poll and doubled the
  frame rate); ProPresenter errors name the cause (ECONNREFUSED) rather
  than fetch's "fetch failed".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Waters Church
2026-09-13 13:24:46 -04:00
co-authored by Claude Fable 5.1
parent 1e92454e76
commit 555bc482ce
3 changed files with 46 additions and 25 deletions
+5 -4
View File
@@ -402,7 +402,7 @@ function createProPresenterSource({ config, log }) {
lastError = '';
} catch (err) {
reachable = false;
lastError = err instanceof Error ? err.message : String(err);
lastError = errorText(err); // "ECONNREFUSED" beats fetch's "fetch failed"
}
publish();
if (stopped) return;
@@ -949,11 +949,12 @@ module.exports = {
function rebuild() {
if (stopped) return;
latest = buildState();
// updatedAt / serverNow move on every poll; the tiles extrapolate from
// them but don't need a frame for that alone.
// updatedAt / serverNow move on every poll, and a startedAt derived
// from a whole-second value jitters with them; the tiles extrapolate
// from the values but don't need a frame for that alone.
let key;
try {
key = JSON.stringify(latest.sources.map((v) => [v.id, v.label, v.status, v.message, v.timers]));
key = JSON.stringify(latest.sources.map((v) => [v.id, v.label, v.status, v.message, v.timers.map(({ startedAt, ...rest }) => rest)]));
} catch {
return;
}