Two real catalog pushes travelling the real 15-minute cycle. The non-image change reached the pinned app; the image change did not; and the restart that used to take 18.3 seconds and pull a new image took 0.1 seconds, did not recreate the container, and pulled nothing. The Update button still moves the version, with the pin advancing 17 seconds before the pull. The frozen app read 'Frissites elerheto - 56 napja' while the other eight read Naprakesz. Teardown returned the container to the baseline digest byte for byte. The correction at the top: the task's Scenario B says to freeze to the stored definition and says nothing about keeping that store current. The store is written when the PIN is written, so a fix delivered afterwards - Scenario A's own case - lands in the live file and not in the store, and the first freeze reverts it. Seen live at 08:20:29Z. Fixed in the same run; the observation is kept as its evidence. Also named: the syncer now imports the stacks PACKAGE for two pure symbols rather than duplicating a compose parser, which honours the task's intent and not its letter; and syncer.Start() had to move after adoption, which the task did not say. Three red-proofs run and reverted. A fourth defect was caught by a test: the syncer would have written an empty compose file over a live app.
12 KiB
REPORT — v0.235.0, update arc slice 3: freeze the version, keep the fixes flowing (2026-09-06)
Overwritten each run. This records the most recent implementation only.
Read this first — one claim in the task was wrong, and one thing this run FOUND that the task did not anticipate.
The task's §7 Scenario B is incomplete, and the live run is what showed it. It asks that a catalog version move freezes the app to "the stored applied definition", and says nothing about keeping that store current. But the store is written when the PIN is written, so a fix delivered afterwards (Scenario A's own case) lands in the live file and not in the store — and the first freeze then reverts it. Observed live at 08:20:29Z: the frozen file came back with
interval: 30s, nineteen minutes after45shad been delivered. Left unfixed, slice 3 would have silently undone the half of the ruling that says fixes keep flowing. The equal-images branch now refreshes the store as it delivers. Everything else in §5's symbol table was accurate; the remaining corrections are in §10.
1. Confirmed baselines — none had moved
| repo | task's baseline | found |
|---|---|---|
| felhom-controller | 998aa319588f |
998aa319588f |
| felhom.eu | bc47dd4ef997 |
bc47dd4ef997 |
| app-catalog-felhom.eu | 8220f8d82e53 |
8220f8d82e53 |
| felhom-agent | — | untouched |
Highest R- id: 457, confirmed. Minted R-458.
2. Files created and modified
Created: controller/internal/stacks/pin.go, controller/internal/stacks/pin_test.go,
controller/internal/sync/render_test.go.
Modified: controller/internal/stacks/deploy.go (the PinnedImages field + the deploy writer),
controller/internal/stacks/manager.go (Stack.CatalogImages, ScanStacks, UpdateStack),
controller/internal/sync/sync.go (the seam + the render table),
controller/internal/web/updatebadge.go, controller/internal/web/updatebadge_test.go,
controller/cmd/controller/main.go (the seam, adoption, and the startup ordering), plus CHANGELOG.md,
CONTEXT.md, REUSE.md, controller/README.md.
3. Commits pushed to main
| repo | commit | what |
|---|---|---|
| felhom-controller | 8a0e0a59adc7 |
v0.235.0 — the pin, the render, adoption, the badge fix |
| felhom-controller | 2a56f557d048 |
the stored definition must follow a delivered fix (found live) |
| app-catalog-felhom.eu | dc7e548 / 09b4ff5 |
the two live-test pushes |
| app-catalog-felhom.eu | 1798ce6 / 17cc784 / 7b9b9b3 |
their reverts, and the CHANGELOG entry recording them as a measurement |
| felhom.eu | 417df06f3529 |
the architecture doc, module map, register, roadmap, capability map, STATUS, live evidence |
No branches. All gate runs passed on push.
4. Tests: 1729 → 1746 (+17). 28 packages, 0 FAIL.
go build ./... && go vet ./... && go test ./... green in the controller module.
| group | what it pins |
|---|---|
| A | a non-image template change reaches a pinned, matching app |
| B | an image change freezes it — and the file content is the stored definition, with the new template's body (NEXTCLOUD_TRUSTED_DOMAINS) asserted absent |
| C | self-healing from a corrupted file in both branches |
| D | the update advances the pin, re-renders and stores, all before the pull; and refuses when the catalog cannot be read, while leaving an unpinned app alone |
| E | adoption skips an incomplete observation and a complete-but-mismatched one, manufactures no applied file, is idempotent, and runs no docker command at all |
| F | a restore's pin is reported to the syncer with its stored definition (R-441's contract) |
| G | the badge reads the catalog, not the rendered file; no catalog entry renders nothing |
| H | the AST walk: the seam and adoption are wired, and their order against the backfill and syncer.Start() |
| — | the render table's remaining rows, the nil seam, and an empty stored definition |
All three companion red-proofs — mutation, observed failure, revert
| # | mutation | observed failure | reverted |
|---|---|---|---|
| 1 | renderSource returns the catalog template on the moved branch |
TestGroupB — "a pinned app must NOT receive the catalog's new version" |
yes |
| 2 | observationCoversTemplate guard removed from AdoptPins |
TestGroupE/incomplete_observation — "pinned 1, want 0 — only 1 of 2 services was observed" |
yes |
| 3 | compareInstalledToTemplate reads TemplateImages again |
TestGroupG — "THE FEATURE IS INVERTED", and „Naprakész" with no catalog entry |
yes |
Full suite re-run green after each revert.
A fourth defect was caught by a test rather than by review: the syncer trusted the applied path it was handed and would have written an empty compose file over a live app. It now re-reads and falls back to the catalog.
5. Deployed version
$ ssh hp "pct exec 9201 -- docker ps --filter name=felhom-controller --format '{{.Image}} {{.Status}}'"
gitea.dooplex.hu/admin/felhom-controller:0.235.0 Up 31 minutes (healthy)
Previous: 0.234.0. Fleet after the run: 21 containers up, none unhealthy.
6. Live evidence
Full quotes: felhom.eu/documentation/tests/VALIDATION-update-slice3-2026-09-06.md.
Method: endpoint level, plus two REAL catalog pushes travelling the REAL 15-minute cycle — a
hand-edited file on the box would have proved nothing about a change to the syncer.
Adoption: 9 pinned, 0 already pinned, 0 left unpinned, multi-service apps pinned per service.
Scenario A — catalog dc7e548 (healthcheck 30s → 45s, no image):
08:01:51 [INFO] [sync] Updated bentopdf/docker-compose.yml
live: image …:v2.8.6 interval: 45s container: v2.8.6, started 03:30:20Z (untouched)
Scenario B — catalog 09b4ff5 (v2.8.6 → v2.8.5):
08:20:29 [INFO] [sync] Updated bentopdf/docker-compose.yml
catalog: v2.8.5 live: v2.8.6 pin: v2.8.6
and then POST /api/stacks/bentopdf/restart:
| before v0.235.0 (spike §2) | now | |
|---|---|---|
| elapsed | 18.3 s | 0.1 s |
| pulled? | yes, v2.8.5 entered the local store | no — docker images lists only v2.8.6 |
| container | recreated | not recreated, started unchanged |
| digest | changed | sha256:eaeea1e4…, identical to baseline |
Scenario D — POST …/update, 16.7 s:
08:22:21 update bentopdf: pin advanced to the catalog's current definition (…:v2.8.5)
08:22:38 Stack bentopdf updated successfully (took 16.6s)
container v2.8.5, digest sha256:2d867aac… live/pin/applied all v2.8.5
The pin advanced 17 s before the update completed — i.e. before the pull, as the design requires.
Scenario G — quoted from the live page while frozen:
<span class="tag tag-warn" title="Újabb változat érhető el ehhez az alkalmazáshoz. …">Frissítés elérhető — 56 napja</span>
ASCII fragments (grep -oF): Naprak 0 on the frozen app page and 8 on the list (the eight
current apps); napja 1; positive control BentoPDF 4; negative controls zzz-never-present and
Nem-karbantartott-XYZ both 0.
The freeze holds in BOTH directions: with the catalog reverted to v2.8.6 and the app pinned to
v2.8.5, the sync logged hash match, skipped and the app stayed on v2.8.5. The pin is what the
customer HAS, not what is newest.
Teardown: a final Update returned the container to sha256:eaeea1e4… — byte-identical to the
pre-run baseline — with interval: 30s restored. Both catalog commits reverted; the catalog tree is
byte-identical to 8220f8d. This run provisioned nothing.
7. R-441 closed by MEASUREMENT
The restore writer now pins to the unit's captured compose and stores it, so the render obeys the
restored definition. The closure rests on the render behaviour being measured live (§6 Scenarios B
and 6b — a pinned app's file surviving a sync that would previously have overwritten it within 15
minutes), plus TestGroupF for the contract the syncer relies on. What was NOT done: a full live
restore. That needs a restore rehearsal, which is a phase, not a check — named in §8.
8. NOT yet live-validated — an explicit list
- A live RESTORE setting the pin. The mechanism is measured; this specific entry point is not.
- The adoption skips — on demo-hp all nine apps were complete and matching, so
0 left unpinned. The skip branches are unit-tested with a red-proof; no live app exercised them. - The
no stored definitionrender row. Every pinned app on the box has one. - A multi-service app through a freeze. bentopdf is single-service; the multi-service half is covered by adoption (docmost, paperless-ngx, romm pinned per service) and by unit tests.
- Any box other than demo-hp. demo-felhom is still on 0.234.0.
9. Register — 203 open before, 203 after; closed 163 → 167
Closed: R-447 (slice 3 shipped), R-441 (by measurement), R-438 (both halves discharged), R-455
(the operator added a Docker Hub PAT). Opened: R-458 — .felhom.yml keeps flowing to a frozen app
(P3-LOW, CC), with what would settle it by measurement rather than code. All four compressed into
CLOSED-ITEMS.md, each naming bc47dd4ef997.
10. Every claim in the task that turned out to be wrong, named
- §7 Scenario B is incomplete — see the box at the top. The stored definition must FOLLOW a delivered fix, or the first freeze reverts it. Found live, not by review.
- §5 says "the syncer must not import the stack manager". It now imports the
stacksPACKAGE for two pure symbols —RenderPlanandParseComposeImages— and never touchesManagerorapp.yaml. The alternative was a second compose-image parser, which is the duplicationREUSE.mdexists to prevent. The intent is honoured; the letter is not, and the import carries a comment saying so. - §2.2 says to call adoption "right after
BackfillInstalledImages()" and stops there. That is necessary but not sufficient:syncer.Start()fires an immediate sync, and at its original position (~L392) that first sync would have run while every app was still unpinned — copying the catalog over a deployed app once per boot.Start()was moved to after adoption; the order is pinned byTestGroupH. - All line-number landmarks were accurate (
AppConfig~L100,ScanStacks~L468 withTemplateImagesat ~L539/~L550, the syncer wiring ~L389,RecreateStackDefinitionFromUnit~L2582), and §3's warning was exactly right — the badge would have inverted silently, and red-proof 3 shows it doing so.
11. Observations — noticed, documented, NOT acted on
- The syncer's DEBUG hash line now prints two identical hashes and the word
(changed).logFileHashesre-reads src and dst after the copy, so they always match; with the render,srcis sometimes the applied file, which makes the oddity conspicuous. Cosmetic, DEBUG-only, and pre-existing. NOT-A-FINDING: it misleads no verdict — theUpdated <app>/<file>INFO line above it is the real signal, and changing a debug helper inside a release about the syncer would add unreviewed noise to the one path that touches every app on every box. - An app pinned before v0.235.0's refresh logic shipped carries a store one fix behind until its next delivered fix, which self-corrects it. Observed on bentopdf at 08:20:29. NOT-A-FINDING: the state converges by itself on the next delivered fix, and the only alternative — rewriting every stored definition at boot — would touch every app on every box to correct something that costs nothing.
- The golden is three releases behind (
0.232.0vs0.235.0), per the push advisory. NOT-A-FINDING: thegolden-noticegate raises it on every release andSTATUS.mdcarries the debt; a register row would duplicate an instrument that already fires.