4 Commits
Author SHA1 Message Date
ekklesiaandClaude Opus 5.5 51d9085cdb fix(display): drop the previous assignment's frame when the new one has nothing
CI / typecheck (pull_request) Successful in 26s
CI / lint (pull_request) Successful in 26s
CI / build (pull_request) Successful in 26s
CI / test (pull_request) Successful in 28s
CI / package (pull_request) Successful in 22s
CI / integration (pull_request) Successful in 8m49s
Moved from a group that was playing to one with nothing to show, a
display adopted the new assignment but kept the old group's last image
on screen indefinitely. With nothing renderable, the controller keeps
whatever frame is up, which is right for a gap in the same assignment
and wrong for a frame from an assignment the display no longer has. The
same early return also skipped persisting, so an outage or a restart
resumed the old group's content from local state.

The controller now records which assignment the frame on screen came
from. When the server says the current assignment has nothing, a frame
from another assignment is taken down (a new stage clear(), which also
abandons any transition still preparing) and the slate shows; and the
new assignment's state is persisted if the stored one belongs to a
different assignment. A gap within the same assignment still keeps its
last good frame, and every path with content is unchanged.

Regression tests: populated-group to empty-group reassignment drops the
old image and shows the slate; and a restart with no server after that
move does not resume the old group's content.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 20:38:42 -05:00
ekklesiaandClaude Opus 5.5 67d89f1020 fix(display): keep blackout across reloads and follow the display's current assignment
A kiosk that reloaded during a blackout restored its last frame from local
state without the blackout, so the hidden slide flashed on the wall until the
server answered.

A display resolved its channel once, at registration. Moving it to another
group or audience, or deleting the channel it was on, never reached a running
screen: it kept showing the old channel, or fell into offline continuation
against a 404 forever. The display now fetches its state by viewer, so the
server resolves the current assignment on every fetch, and the heartbeat reply
names the assigned channel so a reassignment lands within one heartbeat. A
display removed from the roster registers again once, on backoff; only
Signage's own not-found means removal, so a foreign 404 can never cause a
registration loop. The channel route stays for the preview and for display
code a kiosk cached earlier. A replaced realtime subscription can no longer
schedule a stray resubscription when it reports closing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 21:32:04 -05:00
ekklesiaandClaude Opus 5.5 f1b7d67aa5 fix(display): define the display-server messages once, in src/shared
CI / lint (pull_request) Successful in 13s
CI / typecheck (pull_request) Successful in 15s
CI / test (pull_request) Successful in 21s
CI / build (pull_request) Successful in 18s
CI / integration (pull_request) Successful in 3m32s
The display controller and the viewer routes each described the
heartbeat their own way and disagreed about one thing: the display sent
null for "nothing to report", and the route accepted only a missing
field. Every heartbeat from a screen showing a native image was refused
with a 400. The roster therefore marked working screens Offline 90 s
after they opened, and never recorded offlineReady or cachedAssetCount.

src/shared/display-protocol.ts now defines every message between a
display and the server:
- viewer session request and response;
- heartbeat request and response;
- channel state;
- the realtime display payload.

Both halves import it. Its rules: a nullable field accepts null
explicitly, an absent field means "not reported", and an unknown field
is ignored, so a newer display is never refused by an older server.

- The routes parse with the shared parsers, and a refusal names the
  field with Signage's sanitized code.
- The server's channel state extends the wire type, so a field the
  display relies on cannot drift without a compile error.
- The display builds its heartbeat as the shared type, and reads its
  registration with the shared parser instead of casting it.
- Test stubs that described server responses more loosely than the
  server sends them (no viewerName, no stale, partial media) now match
  the real responses and are typed as the protocol.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 21:25:21 -05:00
ekklesia 92bef4c7aa feat: initial Signage 0.1.0 2026-08-28 16:41:54 -05:00