Files
felhom-controller/REPORT.md
T
admin d0d431b42b
gates / gates (push) Successful in 25s
Correct a stale count repeated in four places: 10 floating pins, not 23
Recounted at catalog 18a6d2d8: 66 unique pins — 48 full X.Y.Z, 6 two-part
lines, 4 major lines (10 float), 8 exact versions wearing a variant suffix.
The '23' carried since v0.233.0 matches no definition the catalog supports.
Definition written down beside the number so it can be rechecked.

Also: CONTEXT said the fleet floor was 0.257.0; the hub says 0.259.0.

Comment and doc only — no behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-21 13:02:48 +02:00

7.2 KiB

REPORT — controller v0.260.0: a box ahead of the catalog, and a pin that never moves backwards

R-524 (P2). Base 19ef0329ab66 → v0.260.0 (8f8a64cad7a5). MinAgent 0.131.0 unchanged. Architecture read first and named: felhom.eu/documentation/architecture/09-update-architecture.md (§3 the nine decisions, §5.4 the render table, §6.1 slice 4 as shipped, §8 the limitations).

What was wrong

Measured, not imagined — BIGNIGHT Phase 6, 2026-09-15, VM 333. privatebin was updated 2.0.5 → 2.0.6 through the guarded Update; the catalog was then reverted to 2.0.5. At 22:13:37Z the box read installed privatebin/pdo:2.0.6, catalog privatebin/pdo:2.0.5, and the app page showed „Frissítés elérhető — ma" with a title inviting the household to press Frissítés. The comparison asked only does the installed reference DIFFER?, so a catalog revert — an operator act on our side — presented itself to a customer as an update, and the guarded Update behind it would have advanced the pin 2.0.6 → 2.0.5, onto a datadir the newer version may already have migrated, with §4's ruling saying that cannot be undone.

What shipped

  • stacks.CatalogOrder (controller/internal/stacks/updateorder.go) — the comparison gains a fourth verdict (Unknown / Current / Behind / Ahead) and moves out of web. That move is the substance: two callers must reach the same verdict — the badge and Manager.UpdatePreflight — and a comparison implemented twice is a comparison that drifts. web.compareInstalledToTemplate is now a thin wrapper and keeps every property it had (absent means UNKNOWN and never „Naprakész"; it reads CatalogImages and never TemplateImages; it queries no registry).
  • The badge. Ahead reads „Naprakész" / "Up to date", tag-ok — the same word and class as level, because there is nothing for the household to do — with a title that says why (badge.update.ahead.title, born as a key in both bundles). No version number reaches the customer.
  • The refusal. UpdatePreflight returns reason downgrade, HTTP 409, „Ez a változat újabb a katalógusban lévőnél — visszalépés csak az üzemeltető kérésére.", logged with both image maps. The API now renders update refusals through errText — without that one line the new key would have been a seam built and never wired, which is a documented failure class here.
  • Ahead is the NARROW arm. Every differing service must be orderable AND newer; one older, one unorderable, and the verdict falls back to Behind — i.e. to v0.233.0..v0.259.0 behaviour. This gate can BLOCK an update, so it errs towards letting one run.
  • Ordering is util.Version.Compare and nothing else (house rule: one comparator). The new code is a tag NORMALISER in front of it.

What the fixture caught that the design did not

The first implementation called every real catalog tag unorderable. It accepted only bare X.Y/X.Y.Z, and the test fixture uses nextcloud:31.0.14-apache — the real catalog pin. The refusal test failed with got nil, and the cause was the code being right about a rule that was wrong. The rule now takes the version at the FRONT of the tag and requires the trailing suffix to be identical on both sides, so 31.0.14-apache → 31.0.15-apache orders while 26.05.2-ls310 → -ls311 (a build number with no rule), postgres:16-alpine (a major LINE, not a version), kimai/kimai2:apache-2.57.0 (version at the back), a date stamp and a digest pin all stay unorderable. Measured against the real catalog: 8 of 66 pins float and one puts its version last.

Red-proofs — three, each SEEN to fail

# the mutation what failed
1 make CatalogOrder's Ahead arm unreachable TestR524_PreflightRefusesDowngrade/ahead — "this update must be REFUSED with reason "downgrade", got nil". The update is ALLOWED and the next thing it does is move the pin back.
2 treat an UNORDERABLE pair as ahead (cmp < 0 with the ok dropped) the floating-tag, different-image and digest-pin cases all fail with Ahead — the verdict that would suppress a real „Frissítés elérhető" on the floating pins
3 delete the ahead arm from localeFuncs TestUpdateBadgeFollowsTheLanguage — "an app ahead of the catalog must carry a badge"

An honest note on #1: the first attempt deleted the preflight block and failed to BUILD (an unused import), which proves nothing. It was redone by neutering the Ahead arm instead, and that failed for the right reason.

A claim in the brief that was wrong, named

R-589 was NOT open. The brief said, reviewer-verified, that updatebadge.go builds the badge from four raw Hungarian literals with no key and that R-589 is therefore open. The literals are real; the conclusion does not follow. They are the Hungarian form and are deliberately frozen — that IS the parity guarantee — and the ENGLISH form has been rebuilt from the bundle in web.localeFuncs since v0.258.0, pinned by TestUpdateBadgeFollowsTheLanguage and proven live on a fresh box the same morning (DRILL-first-hour-en-0258-2026-09-20.md item 9). The register row was stale, not the code; it is closed with that citation. The general lesson: a reviewer who reads one producer cannot see a second producer that overrides it. updatebadge.go now says so in its own comment, and v0.260.0's new arm was written into BOTH producers with a test that fails if either is missing.

Green gate and delivery

go build ./... && go vet ./... && go test ./... — all green (full suite, not a subset). controller_gates.py --fast — 17 gates, all OK; go-parity convicted first and was satisfied properly, by registering both new keys as BORN-AS-KEYS in i18n_go_keys.json with the test that pins each. No --no-verify; the pre-push hook ran and passed.

Image gitea.dooplex.hu/admin/felhom-controller:0.260.0 built and pushed. Deployed and healthy on three guests: demo-felhom 9201, demo-hp 9201, and demo-hp 9202 (the scratch guest, upgraded from 0.245.0 for the live proof).

The fleet floor was NOT raised, and the number in the repo's own CONTEXT was stale. CONTEXT.md said the floor stood at 0.257.0; the hub says 0.259.0 (read live from /configs, not from a document — the operator raised it after that entry was written, which felhom.eu/STATUS.md records). v0.260.0 therefore reaches the two demo boxes by hand and no further.

Stated rather than silently skipped: the standing rule asks for the floor to be raised to deliver a release, and this session deliberately did not. Why: a floor raise reaches peti-felhom, a real customer box, and the unprompted-work fence puts anything that changes risk to customer data behind an operator word. The two preceding sessions recorded the raise as the operator's own act, and the one that happened came after the operator asked for it. Put to the operator with what happens if they do nothing: the fix stays on the two demo boxes and the rest of the fleet keeps offering a downgrade as an update — which is a badge and a button, not data at risk, so waiting costs little.

Live proof, drift numbers and the state of the whole arc: felhom.eu/documentation/audits/UPDATE-ARC-STATE-2026-09-21.md and audits/update-arc-2026-09-21/.