Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be>
5.2 KiB
key, title, status, since, supersedes, superseded-by, rule, signals
| key | title | status | since | supersedes | superseded-by | rule | signals |
|---|---|---|---|---|---|---|---|
| graphics.seeded-template-upgrade-by-fingerprint | 2026-08-26 — A seeded graphics template is upgraded only when it still matches a shipped default (#732) | active | 2026-08-26 | none | none | `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. | 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
TrimEnded, 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.