Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
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
- „
internal/system/mounts_linux.goL176/L194/L199 produce site 3's warnings." They do not.h.WarningfromCheckBackupDestinationnever reachesreport.Warnings— the only producer folded into the health report ischeckStoragePaths(healthcheck.go).system/needed no change at all. - „Site 1 matches
required field… all producers are ours." Nothing producesrequired field. The router matched a phrase no code in this repo writes — a dead arm, removed with the rest. - 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 —
WarnKindStorageNotSeparatedoes exactly that. - 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). - „
errors.New(refusal)at the memory site" — correct, and it is the reason the memory refusal could not be pinned by a unit test:memoryVerdictreads the HOST's real memory. Its kind is pinned at the source line instead, and the mapping is covered with a translated memory message. - Confirmed as stated: site 2's classifier has exactly one caller;
offbox_handlers.go:132is 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:
/launcherand/monitoringidentical;/backups/remoteidentical apart from the new attribute and the poll line (+23 bytes);/dashboarddiffers in live numbers only. - Deploy refusal:
POST /api/stacks/adventurelog/deployon 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"andv.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 andbentopdfwere 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
- Four more API handlers pick their status by matching ENGLISH internal words (
protected,not found,not deployed,still running,not orphaned). FILED: R-569 - 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
- The off-site failure classifier and the alert-placement rule are described in no architecture document. FILED: R-571
required fieldwas 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.- 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.