Files
felhom-controller/REPORT.md
T
2026-09-17 21:12:29 +02:00

8.1 KiB

REPORT — controller v0.251.0: nothing decides by reading a Hungarian word (R-553 + R-563)

Date: 2026-09-17 · Baseline: e236dca48653 (v0.250.0) · Released: 0194342 → image gitea.dooplex.hu/admin/felhom-controller:0.251.0 · MinAgent: 0.131.0 (unchanged). Architecture read before any claim: felhom.eu/documentation/architecture/10-localisation.md §1/§9 (§9 rewritten by this session), 07-backup-architecture.md, 02-controller-module-map.md. Evidence: felhom.eu/documentation/audits/r553-2026-09-17/.

Claims in the prompt that turned out wrong — named first

  1. „internal/system/mounts_linux.go L176/L194/L199 produce site 3's warnings." They do not. h.Warning from CheckBackupDestination never reaches report.Warnings — the only producer folded into the health report is checkStoragePaths (healthcheck.go). system/ needed no change at all.
  2. „Site 1 matches required field … all producers are ours." Nothing produces required field. The router matched a phrase no code in this repo writes — a dead arm, removed with the rest.
  3. Site 3's condition matched more than the prompt implies. Of the nine warnings that exist today, exactly ONE matches („Az adattároló … nem külön meghajtón van"): the three patterns are lower-case and every other warning capitalises the word. So the fix had to preserve one inline warning, not a family — WarnKindStorageNotSeparate does exactly that.
  4. Line numbers drifted as the prompt warned (router.go:478 → the block moved into a new deployStatusFor; offbox.go:186/921; alerts.go:208; handlers.go:920/931 → 944).
  5. „errors.New(refusal) at the memory site" — correct, and it is the reason the memory refusal could not be pinned by a unit test: memoryVerdict reads the HOST's real memory. Its kind is pinned at the source line instead, and the mapping is covered with a translated memory message.
  6. Confirmed as stated: site 2's classifier has exactly one caller; offbox_handlers.go:132 is a false positive (an [INFO] log line); site 5 is the one rendered byte that changes.

The five sites

# decision signal added producer(s) changed decision changed test HEAD-red → fixed-green
1 deploy HTTP status stacks.ErrRequiredField, ErrPathMissing, ErrNotEnoughMemory, ErrAlreadyDeployed (deploy_errors.go) deploy.go 205/311/326/333/245 api/router.go → new deployStatusFor TestR553_Deploy_DecisionSurvivesWordingChange, TestR553_DeployHandlerUsesTheKind, TestR553_DeployProducersCarryKindAndKeepTheirWords red: every translated row = 500, want 400/409; green after
2 off-site failure class backup.ErrOffsiteQuota offbox.go 921 ClassifyOffsiteFailure TestR553_OffsiteQuota_{Decision,HeadLine}SurvivesWordingChange, TestR553_QuotaProducerKeepsItsWords red: translated quota = "unknown", head line „ismeretlen okból"; green after
3 disk-warning placement monitor.WarnKind* + HealthReport.WarningKinds / addWarning / WarningKindAt healthcheck.go checkStoragePaths web/alerts.go TestR553_StorageWarningsCarryKindsAndKeepTheirWords, TestR553_DiskWarningPlacementSurvivesWordingChange, TestR553_HubReportWarningsAreUnchangedOnTheWire red: translated warning Inline = false, and an unrelated warning wrongly moved inline; green after
4 stale „nothing selected" note settings.OffboxTarget.LastWarningKind + backup.OffboxWarnNoAppsSelected offbox.go 1053/1095 (+ both clear sites, both copy sites) offboxWarningDisplay TestR553_StaleNote*, TestR553_WarningKindIsPersistedAndCopied, TestR553_OffboxRunRecordsTheKind red: translated note shown verbatim; producer without its kind convicted; green after
5 remote-backup poll (R-563) data-status="{{.Offbox.LastStatus}}" backups_remote.html status element the page's own script TestR563_PollStartsFromAttribute (hu + en), TestR563_AttributeFollowsTheStatus red twice: poll back on the word (both languages), word back inline (English page shows Hungarian); green after

Message bytes: every producer's sentence is asserted equal to its pre-change string (sites 1, 2, 4) or rendered identically (site 5). util.KindErrorf / KindError (new, internal/util/errkind.go, documented in REUSE.md §1) build the same bytes fmt.Errorf did and carry the sentinel for errors.Is.

External signatures kept, on purpose: the classifier's restic and ssh arms — unable to open config file, is there a repository at the following location, produced no snapshots, connection refused, connection reset, no route to host, i/o timeout, timed out, permission denied, host key, handshake, could not resolve, network is unreachable. We neither write nor translate that output.

The wire: internal/report/builder.go copies Status/Issues/Warnings only; the kinds are internal. Pinned by field-set + byte test, and confirmed live (the health block on 0.251.0 carries exactly issues, status, warnings).

Gates

go build ./... && go vet ./... && go test ./... → rc 0 before each push. controller_gates.py --fast → rc 0. No --no-verify. One existing test (TestClassifyOffsiteFailure_EachCauseIsDistinct) built the quota error the old way and was adapted to build it as the run does — stated here rather than silently.

Live validation (endpoint-level; no browser on DooPlex) — demo-hp guest 9201

Deployed by hand; fleet floor unchanged (0.250.0), no golden.

  • Hungarian pages 0.250.0 → 0.251.0: /launcher and /monitoring identical; /backups/remote identical apart from the new attribute and the poll line (+23 bytes); /dashboard differs in live numbers only.
  • Deploy refusal: POST /api/stacks/adventurelog/deploy on an already-deployed app → 409 before and after with byte-identical message; the app kept running.
  • Site 5 live: id="offbox-status-value" data-status="ok" and v.dataset.status === 'running'.
  • Hub: health.warnings [] before and after, health keys unchanged.
  • Not provoked live, and why: the 400 refusals (a live probe starts an install), the quota failure (would need a full quota), the stale note (would need clearing the selection), and the disk-warning placement (this box has no storage warning — 0 banner blocks before and after). All four are carried by red-proofed tests.
  • A mistake, and what I did: the FIRST live probe posted an empty value map to vaultwarden, which has no empty required field — so the deploy was accepted (202) and the app installed. I stopped it and removed it (with the data volume it had just created) within two minutes, verified no container remained, and rewrote the probe to the 409 form. Nothing else on the box was touched; the standing apps and bentopdf were not involved.

Teardown

Machine: guest on hu, controller 0.251.0; password file and probe scripts shredded/removed; evidence pulled off before anything was cleaned. Host: transfer files removed. Hub: nothing written; the DB copy shredded. Nothing provisioned.

Observations

  1. Four more API handlers pick their status by matching ENGLISH internal words (protected, not found, not deployed, still running, not orphaned). FILED: R-569
  2. The stale-note display keeps a Hungarian-text fallback for boxes that have not run off-site on ≥ 0.251.0, and slice 2 must not translate that producer until it goes. FILED: R-570
  3. The off-site failure classifier and the alert-placement rule are described in no architecture document. FILED: R-571
  4. required field was matched by the router and produced by nothing — a dead arm. NOT-A-FINDING: deleted with the rest of the text chain in this release; nothing to track.
  5. The i18n inventory's comparison detector, re-run on the fixed tree, reports one Go compare and one indirect compare. NOT-A-FINDING: the first is the known [INFO] log-line false positive, the second is the deliberate legacy fallback carried by R-570; decoys prove the detector still convicts.