168 records -> docs/decisions/records/<area>/<topic>.md (163 active, 23 dirs) and docs/decisions/archive/<area>/<topic>.md (5 archived). The filename IS the key, so one-active-record-per-key becomes a filesystem property rather than a validator check, and supersession becomes a `git mv`. WHY: the monolith was a concurrency problem before an aesthetic one. A 3,900-line append target made parallel sessions collide -- PR #605 and PR #614 both hit append-vs-append conflicts during routine rebases, and hand-resolving those inside the corpus is exactly the operation the rationale-rewrite guard exists to police. HOW IT IS VERIFIED: a ~170-file diff cannot be meaningfully read, so correctness does not rest on reading it. The parser was taught BOTH formats first, so the body-diff guard parses the old form at the merge-base and the new form at head -- the migration validates itself, no bypass. The proof is a field-level equivalence harness: 168 records before and after, zero lost, zero gained, zero field mismatches, zero rationale bodies differing. Reviewers should scrutinise the harness; it is the actual evidence. What measuring caught that reading would not have: - ~500 lines sit OUTSIDE any record -- decisions.md's lifecycle schema and each topic file's preamble, mostly the only copy. Source files are kept and stripped, never deleted. They also cannot be filed per-area: topic files hold several areas and 4 of 23 areas span several files. - Archive discovery was a non-recursive glob; after the split it found ZERO archived records, surfacing as four bogus "supersedes points to unknown key" errors rather than an obvious failure. - ~32 live docs point into the corpus BY DATE, which the split dangles. Each stripped file now ends with a generated "Records formerly in this file" index, which also rescues the identical breadcrumbs in old issue comments. - decisions.md's "In this file:" list was 97 same-file anchor bullets that the split makes WRONG, not merely stale. Dropped; the generated index replaces them with links that resolve. The equivalence harness now runs against a checked-in FIXTURE, not the live corpus. The earlier version migrated the real tree, which made it a one-shot: the moment the migration landed there was nothing left to move and the tests failed for reasons unrelated to the code. A fixture keeps them testing the SCRIPT rather than the repo's current state. Keys preserved verbatim, warts included: `sched` (12) and `scheduling` (1) remain two directories for one concept. Renaming a key is not a move -- it changes identity, breaks the equivalence proof, and invalidates MemPalace's per-key drawers. Taxonomy normalisation is separate work. refs #610
2.8 KiB
key, title, status, since, supersedes, superseded-by, rule, signals, mechanics
| key | title | status | since | supersedes | superseded-by | rule | signals | mechanics |
|---|---|---|---|---|---|---|---|---|
| api.channel-preview-capability | 2026-07-21 — Browser channel preview is a server-declared per-channel capability (#60) | active | 2026-07-21 | none | none | Whether a channel can be previewed in the browser is declared by the server, not derived by the SPA, as an additive `Preview` field (`{Availability, ManifestUrl, UnavailableReason}`) on `ChannelResponseModel`. | a Play button that does nothing; preview eligibility inferred from a display string; a green preview on a Transport Stream channel being read as validating its configured pipeline · paths: `ErsatzTV.Core/Api/Channels/ChannelPreviewResponseModel.cs`, `ErsatzTV.Application/Channels/Mapper.cs`, `web/src/screens/channels/ChannelPreviewPanel.tsx` · issues: #60, #552 | `Mapper.GetPreview(StreamingMode, channelNumber, isEnabled, playoutCount)` is pure and JWT-agnostic |
ChannelPreviewAvailability is one of Available, ForcedHlsOnly, or Unavailable, computed in one
place from the real StreamingMode enum plus the channel's enabled/playout state. The SPA renders and
acts on it and derives nothing — deriving it client-side would mean keying behavior off
Mapper.GetStreamingMode's human-readable display label, where a copy tweak would silently break
playback.
Unavailable covers two causes, checked in this order (first match wins): the channel is disabled
(IptvController 404s a disabled channel, so preview must not even try), and the channel has zero
playouts (a manifest request against one blocks indefinitely). At first pass these two were keying
preview on StreamingMode alone, so a disabled or playout-less channel was declared Available and
then failed confusingly.
Only the two HLS modes are browser-playable; a browser cannot play the video/mp2t that the
Transport Stream modes serve over /iptv/*. Those are declared ForcedHlsOnly: preview is offered
only as an explicit opt-in that requests /iptv/channel/{n}.m3u8?mode=segmenter, and is always shown
with a caveat that the check does not exercise the channel's configured pipeline. Fatal HLS errors
are reported, never auto-recovered — a diagnostic surface must show the fault rather than retry past
it; a user-initiated Retry re-issues the manifest request via a real playToken because the manifest
GET starts a server-side session, so a byte-identical repeat URL would otherwise be a no-op.
Originally, a JWT-enabled deployment made preview Unavailable (reason IPTV JWT authentication is enabled) because /iptv/* does not accept the SPA's ctv-session cookie and nothing minted a JWT
for the browser. #552 closed that: the SPA now mints a short-lived token and appends it as
?access_token=, so this projection no longer inspects JWT status at all. See
security.iptv-browser-token.