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.
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.
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)
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]>
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]>
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]>
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]>
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.
Stage 0 (Signage production readiness). Depends on Zayin/Ekklesia#92, which
ci/platform.envpins.Defects fixed
mainloadingblackout.npmrc/lockfile still named the old registry hostgit.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
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-stateroute 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.tests/browser/stage0-management.test.tsx(9; 8 fail onmain's sources, and the native-dialog guard passes on both), plus 6 display/contract tests indisplay.test.tsanddisplay-contract.test.tsthat fail onmain.pair,restart,two-pages,session-blip,stream-failure,stream-recycle, and now alsoconnectivity-blip,caching-probe, andrevoke(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).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
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.