81 lines
4.6 KiB
Markdown
81 lines
4.6 KiB
Markdown
# 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.
|