feat: add display kiosk host
CI / verify (pull_request) Successful in 38s
CI / infrastructure-smoke (pull_request) Successful in 20s
CI / migration-smoke (pull_request) Successful in 59s

This commit is contained in:
ekklesia
2026-08-23 20:50:56 -05:00
parent be5ed7853d
commit 2e0fe059c3
80 changed files with 4868 additions and 397 deletions
+39 -6
View File
@@ -70,7 +70,8 @@ Every module-facing contract in this platform is written so that **full Ekklesia
thing that can satisfy it**. The intended second implementation is a reusable single-module
standalone host — conceptually `@ekklesia/standalone-host` — which loads exactly one module and
supplies the same host contracts Ekklesia does: configuration, secrets, object storage, HTTP, module
lifecycle, and realtime, with display/kiosk hosting still later.
lifecycle, and realtime. Ekklesia now also has a dedicated display surface, but the reusable
single-module standalone host remains later.
**No such package exists yet, and this pass does not create one.** It is recorded here because it is
what several contract decisions are *for*, and because a change that quietly makes it unimplementable
@@ -370,9 +371,10 @@ and nothing more; there is no runtime health-probe contract. Modules remain trus
disabling one is an operational control, never a sandbox. No installer exists.
Phase 7A pass 2 adds the module configuration and secrets contract
([ADR 0029](decisions/0029-module-configuration-and-secrets.md)). A module declares what it needs
configured in its manifest — `manifestVersion: 5`, a hard cutover — using six types (`string`,
`integer`, `boolean`, `enum`, `url`, `secret`) with bounded constraints and no nesting. Static
([ADR 0029](decisions/0029-module-configuration-and-secrets.md)). That pass introduced static
configuration with the `manifestVersion: 5` hard cutover; the current strict manifest is version 6
after ADR 0033. A module uses six declaration types (`string`, `integer`, `boolean`, `enum`, `url`,
`secret`) with bounded constraints and no nesting. Static
declaration is what lets the platform validate configuration **before** deciding whether a module may
start, so an unconfigured module is reported `blocked · configuration_missing` rather than taking
startup down from inside its own `start` hook; discovery, `pnpm modules:validate`, and Doctor all read
@@ -469,5 +471,36 @@ for requests already in flight. Reverse-order stop then deactivates each module'
immediately before that module's own stop hook, so dependencies remain operational while dependents
clean up and captured publishers cannot outlive their module. Direct object-delivery URLs remain
opaque payload strings and are never re-signed per publication or subscriber. MQTT remains an
external-control concern for a future Companion boundary, not an internal module bus. Display/kiosk
hosting and its public/device principal are explicitly not part of this pass.
external-control concern for a future Companion boundary, not an internal module bus.
Phase 7A pass 6 adds a dedicated display role and Ekklesia host
([ADR 0033](decisions/0033-display-kiosk-host-contract.md)). Manifest version 6 gives a module one
optional `entrypoints.display` object containing a compiled artifact and one module-owned permission.
The permission must have an `authenticated` or `restricted` baseline: public/device display identity
is still intentionally unsolved. The display artifact must be distinct from every other configured
entrypoint. Primary UI, display UI, both, and neither are all valid module shapes.
The two browser roles share the existing `defineModuleUi({ id, version, mount })` export and exactly
the same `ModuleUiHostContext`; role selection comes from separate artifacts, never a `mode`,
`presentation.kind`, or host-identity branch. The canonical builder reads only
`src/ui/index.tsx` and `src/display/index.tsx`, bundles React into each declared role, rejects CSS
sidecars, and publishes no output until every requested bundle has compiled. Core validates both
paths inertly and builds both publication records only from the dependency-resolved eligible/running
set; it never imports browser code.
Ekklesia keeps its primary Dashboard host unchanged and adds a separate full-viewport application.
For each running display-capable module, the API registers the stable page `/display/<module-id>`, a
protected descriptor at `/api/v1/module-displays/<module-id>`, and the exact display artifact at
`/api/v1/module-ui/<module-id>/<version>/display`. The page and descriptor use normal cookie identity
and the manifest's display permission; the artifact remains anonymous trusted code because bytes
confer no authority. There is no display inventory in this pass. The descriptor supplies the same
module API root, realtime root, principal-derived permission hints, and exact artifact identity the
generic loader already consumes. The surface adds no Dashboard chrome and does not request the
browser Fullscreen API; failures remain bounded behind a fixed state and manual retry.
Production reads the fixed display HTML and script once before binding the listener and serves that
frozen snapshot, never a directory. Enablement, dependency blocking, and runtime startup determine
whether routes exist, and installed-manifest changes remain restart-based. Realtime and direct signed
object URLs are unchanged and flow through the same portable contracts. Named display variants,
targeting, display inventory/management, device/public identity, a standalone host, shared CSS,
Announcements migration, and Companion integration remain later work.