|
|
|
@@ -0,0 +1,224 @@
|
|
|
|
|
# 0033: Display UI is a separate, permission-gated module entrypoint
|
|
|
|
|
|
|
|
|
|
- Status: Accepted
|
|
|
|
|
- Date: 2026-08-23
|
|
|
|
|
|
|
|
|
|
## Context
|
|
|
|
|
|
|
|
|
|
ADR 0021 through ADR 0027 established one optional module-owned browser artifact,
|
|
|
|
|
`entrypoints.ui`, and an Ekklesia Dashboard shell that discovers, publishes, and mounts it. That
|
|
|
|
|
contract is appropriate for a module's ordinary product interface: Announcements management,
|
|
|
|
|
calendar editing, settings, or operational controls.
|
|
|
|
|
|
|
|
|
|
A long-lived screen is a different presentation context. An announcement TV, lobby sign,
|
|
|
|
|
confidence monitor, or kiosk needs a clean viewport, no Dashboard navigation, and a stable bookmark.
|
|
|
|
|
Forcing that experience through the Dashboard would either expose management chrome on the screen or
|
|
|
|
|
make one large artifact branch on an Ekklesia-specific mode. Neither shape transfers cleanly to the
|
|
|
|
|
future one-module standalone host.
|
|
|
|
|
|
|
|
|
|
The real Announcements client pressure-tests the boundary. Its slideshow wants an authoritative
|
|
|
|
|
snapshot, subsequent state updates, direct image URLs, a black mode-off state, and local fade
|
|
|
|
|
transitions. It does not need Dashboard navigation or a second transport. Its current fixed LAN
|
|
|
|
|
addresses, anonymous Socket.IO connection, global socket, and incomplete teardown are migration
|
|
|
|
|
concerns, not contracts Core should copy.
|
|
|
|
|
|
|
|
|
|
## Decision
|
|
|
|
|
|
|
|
|
|
### A module may declare one separate display artifact
|
|
|
|
|
|
|
|
|
|
Manifest version 6 adds this optional declaration while retaining `entrypoints.ui` unchanged:
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"manifestVersion": 6,
|
|
|
|
|
"permissions": [
|
|
|
|
|
{
|
|
|
|
|
"id": "announcements.display.view",
|
|
|
|
|
"baseline": "restricted",
|
|
|
|
|
"description": "View the announcement display."
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
"entrypoints": {
|
|
|
|
|
"server": "./dist/server/index.js",
|
|
|
|
|
"ui": "./dist/ui/index.js",
|
|
|
|
|
"display": {
|
|
|
|
|
"artifact": "./dist/display/index.js",
|
|
|
|
|
"permission": "announcements.display.view"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`ui` remains the module's primary/management artifact and is the only role eligible for Dashboard
|
|
|
|
|
navigation. `display` is a full-viewport artifact and never implies Dashboard placement. Either
|
|
|
|
|
field may be absent, so a module may provide primary UI only, display only, both, or neither.
|
|
|
|
|
|
|
|
|
|
The display declaration carries its permission beside its artifact. The permission must be declared
|
|
|
|
|
and owned by that module, and its baseline must be `authenticated` or `restricted`, never `public`.
|
|
|
|
|
This makes it impossible to declare a display without an explicit access policy, silently reuse a
|
|
|
|
|
management permission chosen by Core, or introduce anonymous display access before a deliberate
|
|
|
|
|
device/public principal exists. The display artifact must differ from every other configured
|
|
|
|
|
entrypoint before and after canonical path resolution.
|
|
|
|
|
|
|
|
|
|
One display artifact per module is deliberate. Named variants such as lobby, stage, or confidence
|
|
|
|
|
display are deferred until a real module requires them; a generic display registry now would create
|
|
|
|
|
identity, routing, permission, and management contracts without a consumer.
|
|
|
|
|
|
|
|
|
|
Version 6 is a hard cutover. Version 5's strict `entrypoints` object did not admit `display`, so
|
|
|
|
|
silently accepting the new field under the old version would allow one version number to mean two
|
|
|
|
|
schemas. A module with no display upgrades by changing only the version literal.
|
|
|
|
|
|
|
|
|
|
### Discovery and build remain static
|
|
|
|
|
|
|
|
|
|
Discovery reads the manifest without importing browser code. The declared display artifact obeys
|
|
|
|
|
the same package containment, symlink, real-file, and compiled `.js`/`.mjs` rules as primary UI.
|
|
|
|
|
Complete discovery still succeeds or fails atomically before any runtime entrypoint executes.
|
|
|
|
|
|
|
|
|
|
The canonical module UI builder reads both declarations and builds exactly what is declared in one
|
|
|
|
|
invocation:
|
|
|
|
|
|
|
|
|
|
- `entrypoints.ui`, when present, from `src/ui/index.tsx`;
|
|
|
|
|
- `entrypoints.display.artifact`, when present, from `src/display/index.tsx`.
|
|
|
|
|
|
|
|
|
|
It validates every source and output before starting either build. The artifacts use the same
|
|
|
|
|
browser boundary and bundling policy from ADR 0023. No CSS sidecar, router, or alternate React
|
|
|
|
|
runtime contract is introduced.
|
|
|
|
|
|
|
|
|
|
### Both artifacts use the existing module UI ABI and context
|
|
|
|
|
|
|
|
|
|
A display artifact default-exports the same `defineModuleUi({ id, version, mount })` definition and
|
|
|
|
|
returns the same mount cleanup. The generic loader still validates identity and creates one
|
|
|
|
|
independent mounted instance per call. No display-specific ABI exists.
|
|
|
|
|
|
|
|
|
|
`ModuleUiHostContext` also remains unchanged: `moduleId`, the module-relative API client, principal,
|
|
|
|
|
permission hints, and realtime are sufficient. A `presentation.kind` flag is intentionally omitted.
|
|
|
|
|
The distinct static artifact already tells module code which product experience it implements; a
|
|
|
|
|
mode flag would encourage the single branching artifact this decision rejects. A module may share
|
|
|
|
|
ordinary internal components between its two sources and parameterize those components itself.
|
|
|
|
|
|
|
|
|
|
Nothing exposes a database, storage provider, S3 credential, bucket name, session token, server
|
|
|
|
|
path, or Ekklesia route. A standalone host can provide the exact same ABI and context at a different
|
|
|
|
|
outer URL.
|
|
|
|
|
|
|
|
|
|
### Publication distinguishes the role exactly
|
|
|
|
|
|
|
|
|
|
The existing eligible/running publication registry gains a display view. Primary UI keeps its
|
|
|
|
|
existing URL unchanged. The display artifact has one exact role-specific URL:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
/api/v1/module-ui/<module-id>/<version>/display
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
There is no request-controlled role, filename, or package path. The publisher reads only the exact
|
|
|
|
|
canonical path discovery accepted. A source map, sibling chunk, management artifact, and every
|
|
|
|
|
other package file remain unreachable. Artifact bytes keep ADR 0025's anonymous-code policy:
|
|
|
|
|
downloading trusted browser code grants no authority, while failures remain indistinguishable to
|
|
|
|
|
avoid enumeration. Authorization occurs before the host discloses a descriptor or mounts that code.
|
|
|
|
|
|
|
|
|
|
### Ekklesia owns a stable, permission-gated outer route
|
|
|
|
|
|
|
|
|
|
Ekklesia registers these exact routes only for eligible, running display publications:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
GET /display/<module-id>
|
|
|
|
|
GET /api/v1/module-displays/<module-id>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The first is the version-independent bookmark a device keeps across module upgrades. The second is
|
|
|
|
|
the singular bootstrap descriptor containing the current versioned display artifact URL, the
|
|
|
|
|
module's API and realtime bases, and that principal's module-scoped permission hints. There is no
|
|
|
|
|
display inventory or management UI in this pass.
|
|
|
|
|
|
|
|
|
|
Both routes require the manifest's module-owned display permission through the existing identity,
|
|
|
|
|
permission registry, and authorization evaluator. There is no anonymous exception. A future
|
|
|
|
|
device/kiosk/public-display principal is a separate security decision; until then, a display uses an
|
|
|
|
|
ordinary authenticated session and explicit permission.
|
|
|
|
|
|
|
|
|
|
Routes are composed from the eligible set after successful module startup. A dashboard-only,
|
|
|
|
|
disabled, dependency-blocked, failed, or unknown module has no display route or descriptor. A
|
|
|
|
|
running module remains available until process restart under ADR 0028; this decision adds no hot
|
|
|
|
|
disable behavior.
|
|
|
|
|
|
|
|
|
|
### The display browser root is separate from Dashboard chrome
|
|
|
|
|
|
|
|
|
|
`surfaces/shell` builds a second top-level browser application for the display route. It does not
|
|
|
|
|
render `HostApp`, Dashboard navigation, a header, sidebar, breadcrumbs, or operational controls, and
|
|
|
|
|
Dashboard code is not statically imported into its bundle. Keeping both applications in the one
|
|
|
|
|
surface package reuses the security-sensitive principal/API/realtime adaptation already there;
|
|
|
|
|
creating `surfaces/display` would either duplicate it or require a broader shared-package extraction,
|
|
|
|
|
while repository boundaries correctly forbid one surface from importing another.
|
|
|
|
|
|
|
|
|
|
The page gives its mount root the whole viewport, removes default margin and padding, hides document
|
|
|
|
|
overflow, and otherwise does not theme module output. Full-screen means **full viewport**. The host
|
|
|
|
|
does not invoke the browser Fullscreen API; OS/browser kiosk launch policy belongs to deployment.
|
|
|
|
|
|
|
|
|
|
The host owns neutral loading and sanitized failure output, clears partial DOM after a failed mount,
|
|
|
|
|
and provides a deliberate retry without a tight automatic loop. It aborts stale bootstrap work,
|
|
|
|
|
immediately unmounts a load that resolves after teardown, and swallows cleanup failure only while
|
|
|
|
|
teardown is already in progress. Multiple tabs and physical screens create independent loader,
|
|
|
|
|
context, and realtime instances; no browser-global module state is introduced by the host.
|
|
|
|
|
|
|
|
|
|
Some frameworks report a render failure only after their `mount` call has returned. Because the
|
|
|
|
|
display route is a dedicated page containing exactly one module artifact, an uncaught error targeted
|
|
|
|
|
at `window` or an unhandled promise rejection conservatively fails and unmounts that display attempt.
|
|
|
|
|
Element-targeted media/resource errors do not: those remain module state. There is no portable way to
|
|
|
|
|
attribute a global exception to a framework tree, and errors a module catches internally remain that
|
|
|
|
|
module's responsibility.
|
|
|
|
|
|
|
|
|
|
The surface's exact HTML and JavaScript files are read once during API startup and served from
|
|
|
|
|
frozen in-memory text at fixed routes. The API never exposes the surface output directory, derives a
|
|
|
|
|
file from request input, serves its source map, or interpolates a module id into HTML. A missing host
|
|
|
|
|
build fails startup before the listener binds.
|
|
|
|
|
|
|
|
|
|
### Existing delivery and realtime contracts are reused
|
|
|
|
|
|
|
|
|
|
Display UI calls `context.realtime.subscribe("state", ...)`. The existing SSE host supplies the
|
|
|
|
|
immediate authoritative snapshot, native reconnect, fresh post-reconnect snapshot, bounded delivery,
|
|
|
|
|
and cleanup defined in ADR 0032. No Socket.IO, WebSocket, MQTT, display-specific channel, or retry
|
|
|
|
|
transport is added.
|
|
|
|
|
|
|
|
|
|
If state contains opaque signed object URLs from ADR 0031, display UI assigns those URLs directly to
|
|
|
|
|
browser media elements. The display host does not proxy, inspect, rewrite, renew, or regenerate
|
|
|
|
|
them. The module remains responsible for publishing refreshed state before URLs expire. A temporary
|
|
|
|
|
storage failure is therefore module state/error policy, not a host storage abstraction.
|
|
|
|
|
|
|
|
|
|
## Consequences
|
|
|
|
|
|
|
|
|
|
- Management and display products evolve independently without a Dashboard mode branch.
|
|
|
|
|
- A TV bookmark survives module version upgrades while the internal artifact URL remains exact and
|
|
|
|
|
versioned.
|
|
|
|
|
- Display-only modules remain absent from Dashboard navigation.
|
|
|
|
|
- Existing trusted-code and same-realm risks remain: the display artifact is reviewed installed
|
|
|
|
|
code, not a sandbox.
|
|
|
|
|
- Authentication session expiry and the SSE connection-age bound eventually re-run server
|
|
|
|
|
authorization. This pass does not invent unattended-device credentials.
|
|
|
|
|
- A module may be mounted by many browser instances; per-device targeting and identity are deferred.
|
|
|
|
|
- The future standalone host may expose `/display` instead of Ekklesia's outer route without any
|
|
|
|
|
module change.
|
|
|
|
|
|
|
|
|
|
## Rejected alternatives
|
|
|
|
|
|
|
|
|
|
- **Reuse one artifact with an Ekklesia display-mode flag.** It couples portable module UI to this
|
|
|
|
|
host and conflates management with output.
|
|
|
|
|
- **Hide Dashboard chrome with CSS.** The navigation and management application would still be the
|
|
|
|
|
containing product and failure boundary.
|
|
|
|
|
- **Make every authenticated primary-UI viewer a display viewer.** Display access deserves its own
|
|
|
|
|
explicit module permission.
|
|
|
|
|
- **Make displays anonymous to match the legacy Announcements client.** That is an authorization
|
|
|
|
|
bypass, not hosting. A device/public principal needs a later threat model and lifecycle.
|
|
|
|
|
- **Expose a module or surface directory as static files.** It turns request paths into filesystem
|
|
|
|
|
selectors and defeats discovery's exact-artifact trust decision.
|
|
|
|
|
- **Add a second realtime transport or Companion controls.** Display consumes state; Companion's
|
|
|
|
|
future actions, variables, and feedbacks are a separate capability contract.
|
|
|
|
|
- **Invoke the Fullscreen API.** It requires a user gesture and confuses page layout with deployment
|
|
|
|
|
kiosk policy.
|
|
|
|
|
|
|
|
|
|
## Deferred
|
|
|
|
|
|
|
|
|
|
- device registration, pairing, credentials, and public-display policy;
|
|
|
|
|
- a display inventory and management Dashboard;
|
|
|
|
|
- named display variants and per-device targeting;
|
|
|
|
|
- the reusable one-module standalone host;
|
|
|
|
|
- generic CSS asset publication or isolation;
|
|
|
|
|
- an Announcements module migration;
|
|
|
|
|
- Companion action/variable/feedback contracts.
|