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

90 lines
8.1 KiB
Markdown

# 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.**