feat: initial Signage 0.1.0
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
# 0001: Signage owns timing; a provider owns content
|
||||
|
||||
- Status: Accepted
|
||||
- Date: 2026-08-27
|
||||
- Related: Ekklesia [ADR 0006](https://git.riverofglorymn.org/River_of_Glory_Church/Ekklesia)
|
||||
(versioned capabilities), [ADR 0041](https://git.riverofglorymn.org/River_of_Glory_Church/Ekklesia)
|
||||
(browser contributions)
|
||||
|
||||
## Context
|
||||
|
||||
Church screens show announcements, scripture, trivia, service times, giving prompts, and photographs.
|
||||
The obvious design — one module that knows about all of them — is the design the legacy Announcements
|
||||
container has, and it is why adding trivia to that product means changing that product.
|
||||
|
||||
Signage exists to make the opposite true: adding trivia to the screens means installing a Trivia
|
||||
module, and Signage never changes.
|
||||
|
||||
That only works if the line between the two is drawn somewhere principled. Drawn in the wrong place,
|
||||
Signage ends up with an `announcementId` column, or a provider ends up owning a timer and two
|
||||
displays drift apart.
|
||||
|
||||
## Decision
|
||||
|
||||
**Signage owns everything about *when*. A provider owns everything about *what*.**
|
||||
|
||||
| Signage decides | A provider decides |
|
||||
| --- | --- |
|
||||
| Which slot is on screen, starting exactly when, until exactly when | Which of its items is next in a given selection scope |
|
||||
| That a slot's turn has come and a selection must be made | Whether that item may repeat, and when its pool is exhausted |
|
||||
| That every channel in a group advances together | What the item looks like, in its own React renderer |
|
||||
| How long a slot lasts when nothing overrides it | Its own default duration, and its own availability window |
|
||||
| That a resolved selection is persisted, revised, and replayed identically | What is inside the payload it hands back |
|
||||
|
||||
Concretely:
|
||||
|
||||
- Signage calls a provider; a provider never calls Signage's playback. A provider can *notify*
|
||||
Signage that something changed (invalidation) but cannot start, stop, skip, extend, or reorder
|
||||
anything.
|
||||
- Signage supplies a **stable opaque selection-scope id** and asks for the next item. The provider
|
||||
advances its own cursor within that scope, in its own tables. Signage stores no cursor of the
|
||||
provider's, and the provider stores no timing of Signage's.
|
||||
- Signage persists the *result* — item identity, revision, payload per audience, duration,
|
||||
availability — so that the same slot renders identically for a viewer that joined ten seconds later,
|
||||
survives a restart, and can be replayed by `previous` without asking the provider again.
|
||||
- Signage has no column, field, route, or type naming an announcement, a passage, a question, a
|
||||
campus, or a service. A payload is opaque JSON on the way through and opaque JSON at rest.
|
||||
|
||||
## The one exception, and why it is not one
|
||||
|
||||
Native images are Signage's own content: it stores them, processes them, and renders them. That looks
|
||||
like an exception to the rule until you see how it is built — as **Signage's own built-in content
|
||||
provider**, `signage:native-images`, implementing the same registration shape an external provider
|
||||
implements, resolving through the same code path, and persisting into the same resolved-selection
|
||||
table.
|
||||
|
||||
There is therefore exactly one selection pipeline, not two. A native image slot and a Trivia slot
|
||||
differ in which provider answers, and in nothing else. Had native images been special-cased, every
|
||||
later feature — history, offline continuation, audience variants, `previous` — would have had to be
|
||||
written twice and would have drifted.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A provider module can be written, installed, versioned, and removed without Signage knowing it
|
||||
exists. Signage never enumerates provider modules and declares no dependency on one.
|
||||
- A church that never installs Trivia has a Signage that is not missing anything; a timeline simply
|
||||
has no Trivia slot.
|
||||
- Provider failure cannot corrupt playback, because playback state was never the provider's to hold.
|
||||
What a failure costs is at most the freshness of one slot's content.
|
||||
- Signage cannot offer a feature that requires understanding content — "show announcements sorted by
|
||||
campus" is a Trivia-shaped request that belongs to whichever module owns campuses.
|
||||
|
||||
## Alternatives
|
||||
|
||||
- **One module that knows every content type.** Rejected: it is the legacy design, and every new kind
|
||||
of content is a change to the module every screen depends on.
|
||||
- **Providers push content into Signage's tables.** Rejected: two writers to one timeline, no way to
|
||||
attribute a bad row, and a provider outage that leaves stale rows nobody owns.
|
||||
- **Providers own their own timing and Signage only composes screens.** Rejected outright: two
|
||||
independent clocks cannot hold two televisions on the same slide, which is the single thing a
|
||||
synchronized channel group exists to guarantee.
|
||||
Reference in New Issue
Block a user