fix: Stage 0 Signage production readiness #18

Open
ekklesia-bot wants to merge 8 commits from stage0/signage-production-readiness into main
Member

Stage 0 (Signage production readiness). Depends on Zayin/Ekklesia#92, which ci/platform.env pins.

Defects fixed

Defect Reproduced on main Root cause Fix
Timeline "would not stay activated" Activate on Playback, then Timelines shows it inactive Each tab read its own stale copy of the channel groups Shared stale-while-revalidate resource cache; Playback follows the management realtime channel
Dashboard jumps when switching tabs Each switch flashed a spinner and collapsed the page Tabs remount panels, and every panel started from loading The same cache: a remounted panel renders its last data immediately
Reload during blackout flashes the hidden slide Restored snapshot rendered without blackout Offline/restore path never applied persisted blackout Restore blackout first
Reassigning a display (or deleting its channel) never reaches a running screen Display kept fetching its registration-time channel; a deleted channel meant a 404 loop into offline continuation Channel resolved once at registration State fetched by viewer (server resolves the current assignment); heartbeat reply names the assigned channel; removed viewers re-register once, on backoff (only Signage's own not-found counts)
Destructive actions on one click Delete image, timeline, slot, source, audience No confirmation Two-step in-place confirmation (DESIGN §21.3); group delete keeps its existing in-place confirmation
Audience key presented as a required field Two near-identical free-text fields Internal provider key exposed Plain-language "Audience", key derived from the name, Advanced override
Test tabs and retired screens pile up in the roster Admin opening display setup while signed in registers a "New display" Nothing could remove an entry Roster labels Signed-in browser; offline entries can be removed after confirmation; setup copy says to open the address on the screen itself
Preview reused the admin browser's IndexedDB/Cache Storage Preview could restore another channel's frame Preview used the persistent browser store In-memory store for the preview
Narrow-screen notice named the platform — Hard-coded copy Neutral wording
CI could not install Run 271: every job failed at install (502) .npmrc/lockfile still named the old registry host Point at git.zayin.net/api/packages/Zayin/npm/; a frozen install from an empty store verifies every pinned integrity hash (identical bytes)

The revocation and reconnect/remount defects were in the platform display host and are fixed in Zayin/Ekklesia#92.

Findings that are not code changes

  • Audience channels change nothing today. The audience key only selects among variants a content provider returns; native images return one, and no other provider is installed. The UI now says so ("Images look the same for every audience").
  • Channel groups are part of the current model (ADR 0004: a group is one clock), and deletion already had an in-product confirmation on main; it is verified in a real browser below.

Compatibility

No migration, schema, or credential change. Existing viewer identities, device pairings, cached media, and assignments are untouched. The old channels/:id/display-state route stays for the preview and for display code a kiosk cached before this release; the heartbeat response only gains an optional field.

Verification

  • pnpm lint, pnpm typecheck, pnpm test (277 passed; 23 files), pnpm build, pnpm validate (20 checks), bash ci/check-cleanup.sh, release-asset tests.
  • New tests: tests/browser/stage0-management.test.tsx (9; 8 fail on main's sources, and the native-dialog guard passes on both), plus 6 display/contract tests in display.test.ts and display-contract.test.ts that fail on main.
  • Runner CI run 274: all 6 jobs green. Integration ran pair, restart, two-pages, session-blip, stream-failure, stream-recycle, and now also connectivity-blip, caching-probe, and revoke (the revoked screen returns to pairing, is re-paired, and keeps its roster entry: 2 before, 2 after). It then passed API restart, container recreation, disable/enable, and remove/reinstall. The job's stack sweep removed 0 resources (nothing leaked).
  • The same CI-equivalent run locally: 163 checks, 0 failures; Docker containers, networks, and volumes identical to the pre-run baseline afterwards.
  • A local visual driver (not committed, as the README explains) on the same stack, 30 checks:
    • Tabs: one module width (1617 px) and position across 10 tab switches; no horizontal overflow; no loading state when revisiting a tab.
    • Activation: shown as active on Timelines and after a full Dashboard reload.
    • Confirmations: in-product only, and no native dialog was ever opened.
    • Playback: slides in timeline order, and in the new order after reordering. About 250 mid-crossfade frames with 0 frames where the outgoing slide was not fully opaque.
    • Blackout: reached the wall in about 45 ms; every sampled pixel was 0,0,0; a reload mid-blackout leaked 0 frames; ending it brings content back.
    • Offline: 22 s fully offline kept advancing slides from cache, and recovery needed 0 remounts.
    • Reassignment: reached the screen in about 5 s with no remount.
    • Leaks: 3 live object URLs (one per cached slide); event listeners 294 → 294; heap 2.2 → 2.4 MB.

Deferred to Stage 1

Settings → Displays and global display names, responsive/phone layout (the desktop-only notice remains), dark mode across the platform, theme editor, moving shared confirm/cache helpers into @ekklesia/ui.

🤖 Generated with Claude Code

Stage 0 (Signage production readiness). Depends on Zayin/Ekklesia#92, which `ci/platform.env` pins. ## Defects fixed | Defect | Reproduced on `main` | Root cause | Fix | |---|---|---|---| | Timeline "would not stay activated" | Activate on Playback, then Timelines shows it inactive | Each tab read its own stale copy of the channel groups | Shared stale-while-revalidate resource cache; Playback follows the management realtime channel | | Dashboard jumps when switching tabs | Each switch flashed a spinner and collapsed the page | Tabs remount panels, and every panel started from `loading` | The same cache: a remounted panel renders its last data immediately | | Reload during blackout flashes the hidden slide | Restored snapshot rendered without blackout | Offline/restore path never applied persisted `blackout` | Restore blackout first | | Reassigning a display (or deleting its channel) never reaches a running screen | Display kept fetching its registration-time channel; a deleted channel meant a 404 loop into offline continuation | Channel resolved once at registration | State fetched by viewer (server resolves the current assignment); heartbeat reply names the assigned channel; removed viewers re-register once, on backoff (only Signage's own not-found counts) | | Destructive actions on one click | Delete image, timeline, slot, source, audience | No confirmation | Two-step in-place confirmation (DESIGN §21.3); group delete keeps its existing in-place confirmation | | Audience key presented as a required field | Two near-identical free-text fields | Internal provider key exposed | Plain-language "Audience", key derived from the name, *Advanced* override | | Test tabs and retired screens pile up in the roster | Admin opening display setup while signed in registers a "New display" | Nothing could remove an entry | Roster labels *Signed-in browser*; offline entries can be removed after confirmation; setup copy says to open the address on the screen itself | | Preview reused the admin browser's IndexedDB/Cache Storage | Preview could restore another channel's frame | Preview used the persistent browser store | In-memory store for the preview | | Narrow-screen notice named the platform | — | Hard-coded copy | Neutral wording | | CI could not install | Run 271: every job failed at install (502) | `.npmrc`/lockfile still named the old registry host | Point at `git.zayin.net/api/packages/Zayin/npm/`; a frozen install from an empty store verifies every pinned integrity hash (identical bytes) | The revocation and reconnect/remount defects were in the platform display host and are fixed in Zayin/Ekklesia#92. ## Findings that are not code changes - **Audience channels change nothing today.** The audience key only selects among variants a content provider returns; native images return one, and no other provider is installed. The UI now says so ("Images look the same for every audience"). - **Channel groups** are part of the current model (ADR 0004: a group is one clock), and deletion already had an in-product confirmation on `main`; it is verified in a real browser below. ## Compatibility No migration, schema, or credential change. Existing viewer identities, device pairings, cached media, and assignments are untouched. The old `channels/:id/display-state` route stays for the preview and for display code a kiosk cached before this release; the heartbeat response only gains an optional field. ## Verification - `pnpm lint`, `pnpm typecheck`, `pnpm test` (277 passed; 23 files), `pnpm build`, `pnpm validate` (20 checks), `bash ci/check-cleanup.sh`, release-asset tests. - New tests: `tests/browser/stage0-management.test.tsx` (9; 8 fail on `main`'s sources, and the native-dialog guard passes on both), plus 6 display/contract tests in `display.test.ts` and `display-contract.test.ts` that fail on `main`. - Runner CI run 274: all 6 jobs green. Integration ran `pair`, `restart`, `two-pages`, `session-blip`, `stream-failure`, `stream-recycle`, and now also `connectivity-blip`, `caching-probe`, and `revoke` (the revoked screen returns to pairing, is re-paired, and keeps its roster entry: 2 before, 2 after). It then passed API restart, container recreation, disable/enable, and remove/reinstall. The job's stack sweep removed 0 resources (nothing leaked). - The same CI-equivalent run locally: 163 checks, 0 failures; Docker containers, networks, and volumes identical to the pre-run baseline afterwards. - A local visual driver (not committed, as the README explains) on the same stack, 30 checks: - **Tabs:** one module width (1617 px) and position across 10 tab switches; no horizontal overflow; no loading state when revisiting a tab. - **Activation:** shown as active on Timelines and after a full Dashboard reload. - **Confirmations:** in-product only, and no native dialog was ever opened. - **Playback:** slides in timeline order, and in the new order after reordering. About 250 mid-crossfade frames with 0 frames where the outgoing slide was not fully opaque. - **Blackout:** reached the wall in about 45 ms; every sampled pixel was 0,0,0; a reload mid-blackout leaked 0 frames; ending it brings content back. - **Offline:** 22 s fully offline kept advancing slides from cache, and recovery needed 0 remounts. - **Reassignment:** reached the screen in about 5 s with no remount. - **Leaks:** 3 live object URLs (one per cached slide); event listeners 294 → 294; heap 2.2 → 2.4 MB. ## Deferred to Stage 1 Settings → Displays and global display names, responsive/phone layout (the desktop-only notice remains), dark mode across the platform, theme editor, moving shared confirm/cache helpers into `@ekklesia/ui`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
ekklesia-bot added 4 commits 2026-10-13 02:48:36 +00:00
Signage had no agent guidance at all. Add a CLAUDE.md that records the
standalone-module architecture, that the parent ROADMAP.md and DESIGN.md are
authoritative, the ownership, branding, token, dark-mode, no-native-dialog and
plain-language rules, that display pairing is transitional until Stage 1
Settings -> Displays, and the branch-per-PR, no-push-to-main, no-release-tag
workflow.

The README listed a mobile management UI as out of scope, which contradicts
the design contract's responsive requirement; it is a Stage 1 deferral, and
now says so alongside how displays work today.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
Activating a timeline from Playback left Timelines showing it as inactive,
because each tab read its own stale copy of the channel groups; operators
read that as activation not sticking. Tabs also mounted every panel afresh, so
each switch flashed a spinner and collapsed the page before regrowing. Panels
now share one stale-while-revalidate resource cache, and Playback follows the
management realtime channel so Now showing tracks the wall.

Deleting an image, a timeline, a slot, a source, or an audience happened on a
single click; each is now a two-step in-place confirmation (DESIGN 21.3).

The audience key was presented as a field to fill in. Audiences are now named
in plain words, explained (images look the same for every audience), and the
content key is derived from the name with an Advanced override. The roster
labels signed-in browsers and lets an offline entry be removed after
confirmation, so test tabs and retired screens no longer accumulate. The
preview keeps nothing in the Dashboard origin's storage, and the narrow-screen
notice no longer names the platform.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
ci: pin the Stage 0 platform harness and run every display scenario
CI / lint (pull_request) Failing after 1m21s
CI / typecheck (pull_request) Failing after 1m21s
CI / test (pull_request) Failing after 1m24s
CI / build (pull_request) Failing after 1m23s
CI / package (pull_request) Failing after 1m21s
CI / integration (pull_request) Skipped
97de441b5f
Pin the integration harness to Zayin/Ekklesia PR 92, whose display host keeps
a live display across reconnects and returns a revoked one to pairing, and
fetch it from the repository's canonical Zayin location. With those fixes the
connectivity-blip, caching-probe, and revoke scenarios pass, so CI now runs
them too; previously they were left out because they failed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Zayin_Admin added 1 commit 2026-10-13 02:57:39 +00:00
build: fetch Ekklesia packages from the registry's current Zayin location
CI / typecheck (pull_request) Successful in 23s
CI / lint (pull_request) Successful in 23s
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) Canceled after 4m3s
073fbc5855
The @ekklesia package registry moved with the repositories to
git.zayin.net/api/packages/Zayin/npm/. The old River_of_Glory_Church address
now answers 502, so every CI job failed at install (Signage run 271). Only the
address changes: a frozen install from an empty store against the new location
verifies every pinned integrity hash, so the bytes are identical.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Zayin_Admin added 1 commit 2026-10-13 03:02:50 +00:00
ci: pin the platform harness that re-pairs the kiosk after revocation
CI / lint (pull_request) Successful in 23s
CI / typecheck (pull_request) Successful in 22s
CI / build (pull_request) Successful in 27s
CI / test (pull_request) Successful in 50s
CI / package (pull_request) Successful in 23s
CI / integration (pull_request) Successful in 8m38s
649114ba1d
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Zayin_Admin added 1 commit 2026-10-13 03:23:07 +00:00
docs(adr): point platform cross-references at the repository's current location
CI / typecheck (pull_request) Successful in 24s
CI / lint (pull_request) Successful in 24s
CI / build (pull_request) Successful in 36s
CI / test (pull_request) Successful in 38s
CI / package (pull_request) Successful in 29s
CI / integration (pull_request) Failing after 1m6s
f8b131f4ed
Link targets only: the Ekklesia repository moved from
git.riverofglorymn.org/River_of_Glory_Church to git.zayin.net/Zayin, and the
old host no longer answers. No decision text changes, which is the
cross-reference correction the ADR rules allow.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Zayin_Admin added 1 commit 2026-10-13 03:30:28 +00:00
ci: follow the platform harness pin to Zayin/Ekklesia#92's current head
CI / lint (pull_request) Successful in 25s
CI / typecheck (pull_request) Successful in 17s
CI / test (pull_request) Successful in 30s
CI / build (pull_request) Successful in 25s
CI / package (pull_request) Successful in 20s
CI / integration (pull_request) Successful in 8m33s
b479ae9296
Co-Authored-By: Claude Opus 5.5 <[email protected]>
All checks were successful
CI / lint (pull_request) Successful in 25s
Required
Details
CI / typecheck (pull_request) Successful in 17s
Required
Details
CI / test (pull_request) Successful in 30s
Required
Details
CI / build (pull_request) Successful in 25s
Required
Details
CI / package (pull_request) Successful in 20s
CI / integration (pull_request) Successful in 8m33s
Required
Details
This pull request doesn't have enough required approvals yet. 0 of 1 official approvals granted.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin stage0/signage-production-readiness:stage0/signage-production-readiness
git checkout stage0/signage-production-readiness
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Zayin/Signage#18