5 Commits
Author SHA1 Message Date
ekklesiaandClaude Opus 5.5 ab5b02960a fix(display): continue a source slot's rotation while offline
CI / typecheck (pull_request) Successful in 24s
CI / lint (pull_request) Successful in 26s
CI / build (pull_request) Successful in 28s
CI / test (pull_request) Successful in 29s
CI / package (pull_request) Successful in 22s
CI / integration (pull_request) Successful in 8m49s
Offline continuation replayed each slot's newest cached answer. A
provider-source slot is answered with the provider's next item on every
play, so its newest answer is the image already on screen: on a timeline
of one source slot, every offline boundary "advanced" to the same
picture. Stage 0's real timeline is exactly that, which is why the wall
froze whether the network or only the API went away, and resumed the
moment the server answered.

Each slot's cached answers are now its observed rotation (one per item,
oldest-played first), and the k-th play of the slot since the anchor
shows the k-th entry. The play count is arithmetic, skipping whole
passes wherever the eligible set is stable, so a week offline is still
one evaluation. Where a play starts is unchanged and still decided by
advanceCursor alone; a pinned slot is a rotation of one and behaves as
before. Nothing is invented: only answers the server already resolved
are replayed, and an item whose newest answer is unusable is skipped.

Regression tests: the resolver on a one-slot rotation of two and three
images, across a week, and with an unusable image; and the controller
on the real shape for six slide intervals with the network down and
with only the API down.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 20:14:35 -05:00
ekklesiaandClaude Opus 5.5 5f52334664 fix(display): keep cached image bytes when the browser has no Cache Storage
CI / lint (pull_request) Successful in 24s
CI / typecheck (pull_request) Successful in 25s
CI / build (pull_request) Successful in 32s
CI / test (pull_request) Successful in 33s
CI / package (pull_request) Successful in 24s
CI / integration (pull_request) Successful in 8m43s
A display opened over plain http:// on a LAN address is not a secure
context, so the browser gives it no Cache Storage. The media cache then
fell back to the in-memory test double, which remembered only each
slide's signed delivery URL and still reported it as cached. Online that
looked identical. Offline, the next slide's URL could not be loaded, the
slide was marked unusable, and local continuation kept landing on the
image already on screen. The wall froze until the network returned.

Without Cache Storage, the browser media cache now fetches and holds the
bytes in page memory as object URLs (one per key, revoked on prune,
replace and stop), so a running display keeps cycling through what it
already has. Nothing persists across a reload; on such an origin the
platform cannot cache the display page either.

Regression tests: the media cache on an origin without Cache Storage,
and the whole controller on an ordered fake clock, cycling for six slide
intervals with the network gone and then handing back to the server.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 19:42:00 -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