feat(732): On Now / Next gets a background box, and is on by default (#843)
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 9s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 25s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m40s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m18s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m12s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m13s
probe742/combined-newest SECOND
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 9s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 25s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m40s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m18s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m12s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m13s
probe742/combined-newest SECOND
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be>
This commit was merged in pull request #843.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
---
|
||||
key: graphics.seeded-template-upgrade-by-fingerprint
|
||||
title: 2026-08-26 — A seeded graphics template is upgraded only when it still matches a shipped default (#732)
|
||||
status: active
|
||||
since: '2026-08-26'
|
||||
supersedes: none
|
||||
superseded-by: none
|
||||
rule: '`GraphicsElementSeeder` keeps every default it has ever shipped as a verbatim fingerprint; on an already-seeded database it rewrites the on-disk template only when the file still matches one of them (line-endings and trailing whitespace normalised), so an untouched install gets the new default while any operator edit is left alone.'
|
||||
signals: 'GraphicsElementSeeder, OnNowNextYamlV1, SupersededDefaults, UpgradeUnmodifiedTemplate, on-now-next.yml upgrade, graphics.on_now_next_seeded · paths: `ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsElementSeeder.cs` · issues: #732, #74'
|
||||
---
|
||||
|
||||
#74 seeded `on-now-next.yml` behind the `graphics.on_now_next_seeded` `ConfigElement` marker, writing
|
||||
the file only when absent so operator edits are never clobbered. That is the right rule for *content*,
|
||||
but it has a consequence nobody stated at the time: **an already-seeded installation never revisits the
|
||||
file at all**, so a change to the shipped default reaches new databases only. #732 hit this directly —
|
||||
the background box is useless if the one install that has the overlay keeps rendering the pre-#732
|
||||
template forever.
|
||||
|
||||
**Decision: upgrade by fingerprint, not by version number or by marker bump.** Every default we have
|
||||
shipped stays in the source as a verbatim constant (`OnNowNextYamlV1`, …) collected in
|
||||
`SupersededDefaults`. On an already-seeded database the seeder reads the file and rewrites it **only**
|
||||
if it still normalises equal to one of those. The comparison ignores line endings and trailing
|
||||
whitespace, because a volume mount or an editor rewrites those without the operator touching content.
|
||||
|
||||
Why this shape:
|
||||
|
||||
- **The safety property is derived, not asserted.** "Did the operator edit this file?" is answered by
|
||||
comparing bytes against what we wrote, rather than assumed — which is exactly the assumption #74
|
||||
refused to make, and which a marker bump would have to make.
|
||||
|
||||
The comparison is not byte-exact in one direction: line endings are normalised and the result is
|
||||
`TrimEnd`ed, so an edit consisting *only* of trailing whitespace at end-of-file does not opt the
|
||||
file out and would be overwritten. That is deliberate (a volume mount or editor rewrites those
|
||||
without operator intent) and the cost is bounded to whitespace nobody can see. Every edit with any
|
||||
visible effect opts out permanently.
|
||||
- **It is self-limiting, so it needs no new marker.** After the rewrite the content equals the
|
||||
*current* default, which is not in `SupersededDefaults`, so the next startup is a no-op.
|
||||
- **A fingerprint entry is not a template.** Never edit an entry in `SupersededDefaults` — it is a
|
||||
record of what we shipped. Changing the current default means adding a new constant and pushing the
|
||||
outgoing one into the list; editing an existing entry silently un-recognises every install carrying
|
||||
it, and the failure is invisible (the upgrade just never fires).
|
||||
|
||||
**The rewrite is write-then-move, with no in-place fallback.** `WriteAllTextAsync` truncates before
|
||||
it writes, so an interrupted write would leave a partial file matching no fingerprint — never
|
||||
repairable by a later boot, and rejected outright by the loader. The temp name is random per call,
|
||||
because a fixed one is shared by two containers on the same config volume — and a process id is not
|
||||
random enough: the image's ENTRYPOINT is exec-form, so every container is PID 1 in its own namespace
|
||||
and would compute the same name. The accepted cost: a
|
||||
template bind-mounted as a single file cannot be replaced by `rename(2)` (EBUSY), so that install
|
||||
never receives the upgrade. Reaching that needs a pinned file that is *also* byte-identical to a
|
||||
shipped default, and the alternative — falling back to an in-place copy — reintroduces the truncation
|
||||
on every IO fault rather than just that one.
|
||||
|
||||
**The upgrade is a one-way door, and a downgrade is lossy.** `GraphicsElementLoader.FromYaml<T>` does
|
||||
not set `IgnoreUnmatchedProperties`, so an unknown YAML key throws and the element is dropped with
|
||||
only a logged warning. Once a template has been upgraded, rolling ErsatzTV back to a build that does
|
||||
not know the new keys makes that element fail to load on **every** channel it is attached to — which,
|
||||
since `graphics.on-now-next-on-by-default`, is every eligible channel that still has it (HLS Direct
|
||||
was never attached, and an operator may have cleared individual channels). The symptom is "the overlay vanished
|
||||
everywhere" and recovery is hand-editing the YAML back. Rollback is a supported operation here
|
||||
(`:prod` is a floating tag promoted manually), so this belongs in the release notes of any version
|
||||
that adds fields to a seeded template, not only in this record.
|
||||
|
||||
**Verify the fingerprint against a real install, not against the constant it was copied from.** The
|
||||
#732 V1 entry was checked byte-for-byte against the live install (md5 `ef9afc088cf6dba252f725babbf3334f`,
|
||||
2026-08-26) before being trusted. A fingerprint that does not match anything in the field is a
|
||||
permanent silent no-op, and no test written from the same source can detect that.
|
||||
Reference in New Issue
Block a user