100 lines
5.0 KiB
Markdown
100 lines
5.0 KiB
Markdown
# 0003: Timelines are ordered patterns; a source instance is a selection scope
|
|
|
|
- Status: Accepted
|
|
- Date: 2026-08-27
|
|
- Related: [0002](0002-content-provider-contract.md), [0004](0004-channel-groups-and-one-clock.md)
|
|
|
|
## Context
|
|
|
|
An operator building a Sunday-morning loop wants something like:
|
|
|
|
```text
|
|
Announcements Announcements Announcements Trivia Announcements Announcements Scripture ↻
|
|
```
|
|
|
|
Five Announcement slots. They must not all show the same announcement, and they must not each start a
|
|
separate rotation that happens to begin at the same place. They are five *turns* of one rotation.
|
|
|
|
## Decision
|
|
|
|
Three concepts, and no more.
|
|
|
|
### A timeline is an ordered, repeating pattern
|
|
|
|
`signage_timelines` holds a name, an optional eligibility window, and a revision. It holds no cursor,
|
|
no clock, and no channel: a timeline is a *program*, and running one is what a channel group does.
|
|
The same timeline may be activated on two groups at once and they will not interfere.
|
|
|
|
### A slot is a position in that pattern
|
|
|
|
`signage_timeline_slots` rows are `(timeline_id, position)` with `position` **unique and contiguous**
|
|
from zero. Contiguity is enforced inside the transaction that changes it, never by renumbering on
|
|
read: a gap would make "the next slot" ambiguous exactly when playback needs it least.
|
|
|
|
A slot's identity is its `id`, which is stable across every reorder. Playback history, resolved
|
|
selections, and a display's cursor all reference the id, so dragging a slot up the list never
|
|
rewrites what already happened.
|
|
|
|
Each slot is one of two kinds:
|
|
|
|
- **`provider-source`** — "ask this source for the next item when this slot's turn comes".
|
|
- **`pinned-item`** — "show exactly this item". A native Signage image is a pinned item of the
|
|
built-in image provider, which is why the two need no separate code path (ADR 0001).
|
|
|
|
Every slot may carry an `enabled` flag, a duration override, its own eligibility window, and — for
|
|
native images only — presentation overrides.
|
|
|
|
### A source instance is a selection scope
|
|
|
|
`signage_provider_sources` is the piece that makes the five-Announcement pattern work. One row is
|
|
"the Announcements module's `active-slides` source, as used by this timeline", and it carries a stable
|
|
opaque `selection_scope` string.
|
|
|
|
Every slot referencing that row passes the **same** scope id to `resolveNext`. The provider advances
|
|
its own cursor within that scope, so the five occurrences walk through five different announcements
|
|
and the sixth wraps according to *the provider's* repeat policy — which Signage neither implements
|
|
nor knows about.
|
|
|
|
Two source rows for the same provider source are two independent rotations, which is exactly how an
|
|
operator expresses "a fast loop here and a slow one there".
|
|
|
|
Signage generates the scope id and stores it. It is opaque to the provider in the sense that its
|
|
shape means nothing; it is stable in the sense that it survives edits, restarts, and reorders. It is
|
|
**not** derived from the timeline id or the slot id, because either would silently reset a
|
|
provider's rotation when an operator renamed or reordered something.
|
|
|
|
### Eligibility is `[start, end)`, in absolute instants
|
|
|
|
Both a timeline and a slot may carry a window. It is half-open: active exactly at `start`, inactive
|
|
exactly at `end`, which is the only convention under which two adjacent windows neither overlap nor
|
|
leave a gap.
|
|
|
|
Every stored instant is absolute (`timestamptz`). The installation's IANA zone is an operator setting
|
|
used to *write and read* those instants for a human, never to interpret an ambiguous local timestamp
|
|
at run time. A daylight-saving transition therefore cannot move a slot.
|
|
|
|
Signage decides whether a source *participates*; the provider decides what is *available*. Leaving
|
|
Trivia out of a service means using a timeline without a Trivia slot, or disabling that slot — and
|
|
Signage still never learns what Trivia is.
|
|
|
|
## Consequences
|
|
|
|
- Reordering, renaming, duplicating, and disabling are cheap and safe: none of them touch a scope id
|
|
or a selection's identity.
|
|
- A timeline with every slot disabled, or every slot ineligible, is a legitimate state — it resolves
|
|
to nothing, and the display shows its slate rather than an error.
|
|
- An operator can express "three announcements, then trivia, then two more announcements" without any
|
|
concept of grouping, nesting, or weighting. The pattern *is* the expression.
|
|
|
|
## Alternatives
|
|
|
|
- **Slot position as identity.** Rejected: every reorder would rewrite history and break a display's
|
|
cursor mid-service.
|
|
- **Scope derived from `(timeline, provider, source)`.** Rejected: it silently merges two source rows
|
|
an operator deliberately created as separate rotations, and changes meaning when a timeline is
|
|
duplicated.
|
|
- **A `repeat: n` count on one slot instead of repeated slots.** Rejected: the pattern is the thing an
|
|
operator reads, and a count hides where in the loop the repeats actually fall.
|
|
- **Local wall-clock schedules.** Rejected: one ambiguous hour every autumn, resolved silently, on the
|
|
screens a congregation is looking at.
|