ekklesiaandClaude Opus 5.5 7483d7cbbe test(display): run the real display controller against the real viewer routes
Every display test answered the controller with a hand-written stub, and
every viewer test called the service with a hand-written object, so
neither side was ever tested against the other. In production every
heartbeat was refused with a 400.

This suite answers the real controller's requests with the real viewer
route handlers over the in-memory store. The routes gain an optional
store factory for that; production still uses the module database.

Fails today: POST /viewers/:viewerId/heartbeat -> 400
SIGNAGE_INVALID_REQUEST, because the display sends
reportedContributionId: null and the route accepts only a missing field.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 18:28:59 -05:00
2026-08-28 16:41:54 -05:00
2026-08-28 16:41:54 -05:00
2026-08-28 16:41:54 -05:00
2026-08-28 16:41:54 -05:00
2026-09-29 19:27:03 -05:00
2026-10-01 21:32:21 +00:00
2026-08-28 16:41:54 -05:00
2026-08-28 16:41:54 -05:00
2026-08-28 16:41:54 -05:00

Signage

Ekklesia's visual programming, timeline, synchronization, and display-delivery module.

Signage decides what is on a screen, when, and on which screens together. It natively supports static images; every other kind of content reaches a screen because another Ekklesia module registered a content provider and contributed a live React renderer for it. Signage never learns what an announcement, a Bible passage, or a trivia question is.

This repository is a standalone, independently versioned Ekklesia module package. It is not a fork of Ekklesia and vendors none of its SDKs.

  • Module id: signage
  • Package: @ekklesia/signage
  • Manifest version: 7

Status

Version 0.1.1 is feature-complete for its declared scope and unreleased. What exists today:

  • The module package, manifest (v7), four capabilities, six permissions, ten operator settings, and one logical storage bucket.
  • Eleven tables and a single initial migration, declared non-destructive.
  • Native image ingest: one image per streamed request, 25 MiB, content-addressed, with display and thumbnail renditions and temporary object keys removed after any unsuccessful upload.
  • The content provider contract and registry, a bounded and sanitized call path, and Signage's own built-in image provider registered through that same capability.
  • Timelines, provider source slots, pinned slots, channel groups, synchronized audience channels, authoritative playback with absolute instants, activation and scheduling, and viewer registration, assignment, heartbeat, and health.
  • A management-UI artifact and a full-viewport display artifact, both built through the canonical Ekklesia module UI pipeline and mounted through the platform's browser ABI.
  • Two permanent scene layers with a crossfade that never fades the outgoing slide, provider renderers mounted as browser contributions, the built-in "Content unavailable" slate, deliberate blackout, and offline continuation from this display's own persisted state.

Nothing here is deployed, and no cutover from the legacy Announcements container has happened or is planned by this repository. Video, audio, MQTT, Home Assistant, Companion, and a notification outbox are out of scope; so is any content module — Signage ships with no announcements, scripture, or trivia, and cannot render any until one is installed.

Offline cold start is not yet possible

A display that is already mounted keeps playing with no network: its schedule, playback state, provider payloads, and media are Signage's own, persisted by this module in IndexedDB and Cache Storage. Core's service worker caches module code, not Signage domain data.

A display that is restarted while Ekklesia is completely unreachable cannot mount Signage at all. Mounting requires an authenticated module descriptor, and Core does not yet cache one. True offline cold start depends on a Core-provided authorized offline display descriptor or lease; until that exists, this module does not claim it. See ADR 0006.

Developing against Ekklesia

Signage is developed inside the authoritative Ekklesia workspace, checked out at the path it will eventually be installed at:

git clone <ekklesia>            ekklesia
git clone <this repository>     ekklesia/modules/signage
cd ekklesia
pnpm install

modules/* is an Ekklesia workspace glob, so the checkout is picked up with no registry edit, no second copy of Ekklesia, and no vendored SDK. @ekklesia/module-sdk, @ekklesia/module-ui-sdk, @ekklesia/design-tokens, @ekklesia/ui, and @ekklesia/module-ui-build all resolve through the workspace as workspace:* dependencies.

Two consequences worth knowing:

  • Ekklesia uses a shared workspace lockfile, so installing here writes an importer entry for modules/signage into Ekklesia's pnpm-lock.yaml. That entry belongs to whoever maintains the Ekklesia checkout; it is not committed from this repository.
  • Signage requires the Ekklesia platform prerequisites for shared design tokens, module stylesheets, browser contributions, bounded streaming request bodies, and display offline code (ADRs 0039–0043). An Ekklesia checkout without them cannot build or validate this module.

Commands

Run from the Ekklesia repository root:

pnpm --filter @ekklesia/signage build       # tsc + the canonical module UI build
pnpm --filter @ekklesia/signage test        # unit and browser-artifact tests
pnpm --filter @ekklesia/signage typecheck   # node, browser, and browser-test projects
pnpm --filter @ekklesia/signage lint

pnpm boundaries                             # four-layer boundary enforcement
pnpm modules:validate                       # static manifest, entrypoint, and schema validation
pnpm migrations:validate                    # migration journal, policy, ordering, and checksums
pnpm verify                                 # the full Docker-free gate

Signage's tests are its own: pnpm test here runs vitest against this package's vitest.config.ts, which includes .test.tsx as well as .test.ts. The workspace-root config includes only .test.ts, so a browser test added here without that config would be silently skipped.

Never run pnpm migrations:apply against a persistent database while developing this module. Signage reads no environment variable, constructs no database client, configures no object storage, hardcodes no Ekklesia URL, opens no socket of its own, and applies no migration: every one of those reaches it through the host.

Changing the schema

pnpm --filter @ekklesia/signage migration:generate   # drizzle-kit, into drizzle/

tests/schema.test.ts pins the checksum of the generated SQL, so a regenerated migration fails the suite until the pin is updated deliberately. drizzle/meta/_ekklesia.json declares the migration non-destructive; pnpm migrations:validate enforces that.

Verifying what only a browser can show

Automated DOM assertions cannot prove a crossfade, a letterboxed image, or a slate. Those are verified by driving the built artifacts in a real Chromium against a stub host and reading the frames: native fill, both letterboxed fits, a contributed renderer, a frozen mid-crossfade composite, the slate, a blackout, the offline indicator, every management tab, and the narrow-screen gate. The harness is deliberately not committed — it stubs a host and would rot into a second, wrong description of the platform ABI.

What Signage owns, and what providers own

Signage owns A content provider owns
Native image assets and their renditions Its own domain content and tables
Saved timelines, repeating patterns, and slots Which of its content is available now
Channel groups and synchronized audience channels Selecting the next item from one of its sources
Slot timing and authoritative playback Repeat and no-repeat policy
Timeline activation and scheduling Audience-specific variants and fallback
Viewer registration, assignment, heartbeat, health Its own live React presentation
Transitions, offline continuation, playback controls Dynamic change within one slide
The management and display UI Its own internal multi-stage behaviour

Licence and scope

Deployment, the module store, a theme editor, a mobile management UI, video and audio, MQTT, Home Assistant, Companion, and any migration of legacy Announcements data are explicitly out of scope for this repository.

S
Description
No description provided
Readme AGPL-3.0
859 KiB
Languages
TypeScript 92.7%
CSS 2.7%
Python 2.6%
Shell 1.3%
JavaScript 0.7%