R-82 Slice D/E: installer default 1.20.0 + architecture docs brought current
Slice D.1 — host-install 1.20.0: a FRESH box defaults to local-daily + offsite-weekly (felhom-pbs, 604800s, keep_last=2). setdefault semantics proven both ways: fresh gets the tier, an UPGRADE preserves the existing backup block verbatim — so an in-place upgrade can never silently start writing to an offsite datastore. Existing boxes are migrated explicitly. Slice E: - 07-backup-architecture.md: honest status header per CONTEXT ruling S-2, with an explicit STALE-outside-the-PBS-tier verdict (the controller tiers were last verified 41 controller versions ago). The PBS row claimed 'PBS on DooPlex' (the retired spike store) with no cadence; it now names felhom-pbs -> felhom-offsite on ep0 over wg-felhom, weekly, keep_last=2. NOT marked ratified — that is Viktor's review of the section 10 list. Discharges R-83. - 06-offsite-connectivity.md: the target-split remaining-work note collapsed (shipped), and records HOW S4.1's tier-aware timeout silently regressed — the mechanism was never removed, its INPUT changed when local_backup_target was retargeted to 'local'. Also notes S4.1 already diagnosed the teardown 403 as a phantom (a timeout consequence, not an ACL gap). - capability map: new row for recurring offsite backups actually LANDING, as distinct from the existing row proving ACTIVATION. IMPLEMENTED, not PROVEN-LIVE — the restore round-trip has not completed under the fixed code. - ROADMAP: R-82 SHIPPED with its remaining gate named, R-83 DISCHARGED, R-84 left open. - CONTEXT + REPORT: the arc, including the mid-arc correction I had to make.
This commit is contained in:
@@ -1,268 +1,162 @@
|
||||
# REPORT — R-81: "no signal" is not "bad signal" (hub v0.75.0) (2026-07-26)
|
||||
# REPORT — R-82: the backup target split (2026-07-26)
|
||||
|
||||
Class fix for the third instance of one bug: **absence of a signal treated as evidence of failure.**
|
||||
Input: `documentation/audits/DIAG-backup-missed-2026-07-26.md` (`add5b9b`).
|
||||
Local **daily** + offsite **weekly**, made expressible at all. Spans four artifacts:
|
||||
agent **v0.97.0 → v0.102.0**, controller **v0.174.0 → v0.175.0**, hub **v0.76.0**,
|
||||
host-install **1.20.0**.
|
||||
|
||||
Phase-0 gates: `documentation/audits/SPIKE-r82-phase0-2026-07-26.md`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Confirmed baselines
|
||||
## 1. What was wrong
|
||||
|
||||
| Repo | Start `main` | Version | → Shipped |
|
||||
|---|---|---|---|
|
||||
| felhom.eu (hub) | `add5b9b` | v0.74.0 | **v0.75.0** (`f5a5e2b` code+docs, `88b41ec` manifest pin) |
|
||||
| felhom-agent | `dfd5d73` | v0.96.0 | unchanged — **N/A this task** (cause-fix is R-84) |
|
||||
| felhom-controller | after R-77 | v0.173.0 | unchanged |
|
||||
`BackupTarget()` returned ONE string and `BackupCadence()` ONE 24 h window, so "local daily **and**
|
||||
PBS weekly" could not be said. The consequence was not theoretical: the DR tier reported `applied`
|
||||
since 2026-07-21 while demo-felhom held **one** snapshot (2026-07-18, a healing artifact) and demo-hp
|
||||
held **zero, ever**. R-39 was "applied and dead"; this was **applied and empty** — the same shape,
|
||||
quieter, and it would have surfaced first at a real restore.
|
||||
|
||||
Clean-tree gate passed before the build: `git status --porcelain` empty, `HEAD == origin/main`.
|
||||
## 2. Phase 0 — three gates
|
||||
|
||||
---
|
||||
|
||||
## 2. Phase-0 — the anchor probe
|
||||
|
||||
**Question:** does the hub retain host-report HISTORY, or only the latest report?
|
||||
|
||||
**Answer: HISTORY IS RETAINED — 90 days, indexed and queryable by customer + time.**
|
||||
|
||||
- `host_reports` is an append-only table (`SaveHostReport` INSERTs; `GetLatestHostReportJSON` only
|
||||
ever read the newest row).
|
||||
- Retention: `cfg.Retention.MaxDays`, **90** both as the code default (`cmd/hub/main.go:631,654`) and
|
||||
as the live value in `manifests/hub.yaml` (`retention.max_days: 90`). `Prune` deletes
|
||||
`host_reports` older than that on the 04:30 job.
|
||||
- Index `idx_host_reports_customer ON host_reports(customer_id, received_at DESC)` — the exact access
|
||||
pattern needed, already present.
|
||||
- Live confirmation on the production DB: 664 / 436 / 96 retained rows for demo-felhom / demo-hp /
|
||||
drill-r50, spanning 7.63 / 4.64 / 0.91 days.
|
||||
|
||||
**Branch taken: the free anchor.** No new persisted state, no `last_backup_seen` column, no agent
|
||||
change. The agent's store is *point-in-time*; the hub has *memory*. Judging backup evidence across
|
||||
the retained window is both the cheapest option and semantically the right question — **"when did I
|
||||
last SEE evidence of a backup?"** rather than "what does this one report happen to say?".
|
||||
|
||||
---
|
||||
|
||||
## 3. Files modified
|
||||
|
||||
| File | Change |
|
||||
| Gate | Verdict |
|
||||
|---|---|
|
||||
| `hub/internal/monitor/deadline.go` | Three-valued verdict; hub-history fold-in; anchored absence; distinct reason strings; `newestBackupEvidence`; landmine comment on `backupStaleAfter`; new `backupEvidenceLookback` |
|
||||
| `hub/internal/store/store.go` | `HostReportRow`, `GetHostReportsSince`, `GetFirstHostReportAt`, `SetHostReportsReceivedAtForTest` |
|
||||
| `hub/internal/monitor/deadline_test.go` | Signature update; note explaining the zero-evidence rows |
|
||||
| `hub/internal/monitor/deadline_anchor_test.go` | **NEW** — 15 tests, scenarios A–E + the named boundary contract |
|
||||
| `hub/CHANGELOG.md` | v0.75.0 |
|
||||
| `CONTEXT.md` | New **Standing rulings** section (S-1, S-2) + the v0.75.0 entry |
|
||||
| `REUSE.md` | New canonical-helper row; new dangerous-lookalike row |
|
||||
| `documentation/backlog/ROADMAP.md` | R-80 closed, R-81 shipped, **R-82 / R-83 / R-84 added** |
|
||||
| `documentation/architecture/00-capability-map.md` | Note row — no status flips |
|
||||
| `manifests/hub.yaml` | image pin 0.74.0 → **0.75.0** |
|
||||
| P0.1 what is exposed for 7 days | **weekly CONFIRMED.** The only 7-day-exposed state is the non-SMB half of `settings.json`. `encryption.key` and the offbox credentials are **stable files unchanged since first boot**, so a week-old copy is byte-identical — that was the risk that could have overturned it |
|
||||
| P0.2 the `pvesm status` 0/0/0 anomaly | **RESOLVED, benign.** PBS returns HTTP 200 with zeroed usage to a namespace-scoped token (`DatastoreBackup`, not `Datastore.Audit`). Ground truth via the hub's ep0 `df`: the datastore is real and writable |
|
||||
| P0.3 capacity | **STOP raised; operator ruled to proceed and grow later.** 37.2 GB total. Per-tenant encryption means **no cross-customer dedup** |
|
||||
|
||||
Commits: **`f5a5e2b`** (code + docs), **`88b41ec`** (manifest pin). Both on `main`, pushed.
|
||||
**Capacity, now measured rather than bracketed:** the second weekly snapshot cost **+2.7 GB on disk**
|
||||
against 14.46 GB logical (~81 % dedup). Weekly top-ups are cheap; **first** snapshots are not — one
|
||||
customer at two retained snapshots costs ~13.5 GB, so the 80 % warn arrives at roughly the **first**
|
||||
additional customer, not the second as I first estimated. Recorded in `07-backup-architecture.md`
|
||||
§9.1.
|
||||
|
||||
---
|
||||
## 3. What shipped
|
||||
|
||||
## 4. The fix
|
||||
- **Agent** — `backup_targets[]`: each tier carries its **own** cadence, retention and wait bound
|
||||
(`keep-last=3` is three DAYS on a daily tier and three WEEKS on a weekly one; one shared knob
|
||||
guarantees one of them is wrong). `/backup/due?target=` judges a tier against **its own** newest
|
||||
successful backup. `GET /backup/tiers` is the controller's capability probe. One runner per tier.
|
||||
- **Controller** — every due tier collected up front and run in **ONE quiesce window**. Two cycles on
|
||||
the weekly night would mean two app outages for one night's work. The app stays quiesced until the
|
||||
**last** tier snapshots, so every tier is app-consistent.
|
||||
- **Hub** — per-tier thresholds (host 26 h, offsite 8 d), preserving R-81's three-valued verdicts,
|
||||
anchored absence and distinct reason strings. Classification is by **target type**
|
||||
(`target_id` → `storage_targets[].name` → `.type`), never by array membership.
|
||||
- **Installer** — a fresh box defaults to local-daily + offsite-weekly; an unprovisioned tier
|
||||
**defers** rather than firing at a storage that does not exist.
|
||||
|
||||
`assessBackupFreshness` returned `{missed bool, reason string}` and collapsed *absence of records*
|
||||
into *failure*. It now returns a three-valued verdict:
|
||||
**The untargeted local-API contract is frozen.** No `?target=` ⇒ the primary tier, same response
|
||||
**bytes** (`Target` is `omitempty` and stays empty). An old controller cannot tell the new agent from
|
||||
the old one; a new controller against an old agent degrades on a 404 probe, logs once, and **still
|
||||
takes the backup**.
|
||||
|
||||
```
|
||||
verdictOK positive evidence of a recent backup
|
||||
verdictUnknown no evidence yet, and the anchored window has not elapsed → silent, LOGGED
|
||||
verdictMissed positive evidence of a problem → alarm
|
||||
```
|
||||
## 4. Operator rulings (2026-07-26), all implemented
|
||||
|
||||
Three inputs, all injected — the function stays **pure**:
|
||||
|
||||
1. the latest report (unchanged parsing),
|
||||
2. `backupEvidence.newestSeen` — the newest backup evidence across a bounded 7-day window of retained
|
||||
reports (`GetHostReportsSince` + `newestBackupEvidence`),
|
||||
3. `backupEvidence.firstReportAt` — first contact (`GetFirstHostReportAt`), the absence anchor.
|
||||
|
||||
Absence is graded against the **existing** `backupStaleAfter` (26 h), exactly as v0.73.0 reused
|
||||
offsite `staleAfter` — **no new knob**. A zero anchor fails toward visibility (the v0.73.0
|
||||
legacy-shape precedent). The window scan early-exits at the first sufficiently-fresh evidence, so the
|
||||
healthy path reads one row; only the genuinely-broken path walks the lookback.
|
||||
|
||||
`CheckBackupDeadlines` logs each deferral (`verdict UNKNOWN`) and the summary line gained a
|
||||
`backup unknown (deferred)` counter — a quiet check must never be indistinguishable from one that
|
||||
did not run.
|
||||
|
||||
The invariant is written at the head of `assessBackupFreshness`, naming all three instances
|
||||
(v0.12.0, v0.73.0, R-81), and pinned by a boundary test whose name states what it protects.
|
||||
|
||||
---
|
||||
|
||||
## 5. Test results
|
||||
|
||||
**Full gate green:** `go build ./... && go vet ./... && go test ./...` — all rc=0, every package `ok`.
|
||||
|
||||
**Test count: 493 → 508 (+15).** (Measured with `git grep -hE '^func Test'` at `HEAD` vs the working
|
||||
tree — a first attempt via `go test -list` after a `git stash` gave a bogus 434 because the stash
|
||||
broke compilation and silently dropped the whole package. Recorded because that is exactly the
|
||||
exit-code-lie class this project has been bitten by; likewise `go vet ... | head` reported rc=0 while
|
||||
vet was actually failing.)
|
||||
|
||||
All 21 tests in `internal/monitor` covering this area pass, including the 6 pre-existing ones.
|
||||
|
||||
### The three companion red-proofs — applied, observed, restored
|
||||
|
||||
**A — Scenario A (the 07-26 case must not alarm).** Removed the `ev.haveSeen` fold-in (pre-R-81
|
||||
shape: judge the latest report alone):
|
||||
|
||||
```
|
||||
--- FAIL: TestBackupFreshness_AgentRestartBlindWindow_NoAlarm (0.00s)
|
||||
deadline_anchor_test.go:61: 07-26 shape must NOT alarm; got verdict=2 reason="newest backup is 176h0m0s old (limit 26h0m0s)"
|
||||
--- FAIL: TestCheckBackupDeadlines_RestartBlindWindow_NoEvent (0.03s)
|
||||
deadline_anchor_test.go:388: the 07-26 restart shape must NOT raise expected_backup_missed; got [expected_backup_missed]
|
||||
```
|
||||
|
||||
The reason string is **verbatim the message demo-felhom actually sent to the customer channel** that
|
||||
morning. Restored.
|
||||
|
||||
**B — Scenario B (a genuinely dead box must still alarm).** Applied the *naive* fix — absence always
|
||||
returns UNKNOWN, never a fault:
|
||||
|
||||
```
|
||||
--- FAIL: TestBackupFreshness_NoEvidenceBeyondAnchor_Alarms (0.00s)
|
||||
deadline_anchor_test.go:97: a box with NO backup evidence for 240h MUST alarm; got verdict=1 reason="no backup evidence yet, but only watching for 240h0m0s"
|
||||
--- FAIL: TestBackupFreshness_Contract_AbsenceIsUnknownUntilAnchorElapses/just_outside_the_window
|
||||
deadline_anchor_test.go:156: CONTRACT VIOLATED: absence beyond the window MUST alarm (watched=26h1m0s, limit=26h0m0s); got verdict=1 …
|
||||
--- FAIL: TestBackupFreshness_Contract_AbsenceIsUnknownUntilAnchorElapses/long_past_the_window
|
||||
deadline_anchor_test.go:156: CONTRACT VIOLATED: absence beyond the window MUST alarm (watched=720h0m0s, limit=26h0m0s); got verdict=1 …
|
||||
--- FAIL: TestCheckBackupDeadlines_NeverBackedUpBeyondAnchor_Alarms (0.03s)
|
||||
deadline_anchor_test.go:411: a host with no backup for 120h MUST alarm; got []
|
||||
```
|
||||
|
||||
**This is the proof that matters most.** A suite that only pinned Scenario A would have passed
|
||||
against this over-suppressed implementation — and over-suppression is strictly worse than the bug it
|
||||
replaces. Restored.
|
||||
|
||||
**C — Scenario C (a fresh box is not born failing).** Restored the literal pre-fix branch
|
||||
(`if !havePBS && !haveVzdump { return missed }`):
|
||||
|
||||
```
|
||||
--- FAIL: TestBackupFreshness_EmptyArraysWithinGrace_Unknown (0.00s)
|
||||
deadline_anchor_test.go:74: absence inside the anchored grace must NOT alarm; got reason="no PBS snapshot or successful backup in the latest host-report"
|
||||
--- FAIL: …/newborn,_1_minute
|
||||
deadline_anchor_test.go:159: CONTRACT VIOLATED: no evidence + no elapsed window must NOT alarm (watched=1m0s, limit=26h0m0s); got reason="no PBS snapshot or successful backup in the latest host-report"
|
||||
--- FAIL: …/newborn,_1_hour (watched=1h0m0s)
|
||||
--- FAIL: …/just_inside_the_window (watched=25h59m0s)
|
||||
--- FAIL: …/exactly_at_the_window (watched=26h0m0s)
|
||||
--- FAIL: TestCheckBackupDeadlines_NewbornHost_NoEvent (0.03s)
|
||||
deadline_anchor_test.go:426: a newborn host must not alarm; got "No fresh verified backup: no PBS snapshot or successful backup in the latest host-report"
|
||||
```
|
||||
|
||||
Restored; full suite green after each.
|
||||
|
||||
### Scenario D and E
|
||||
|
||||
- **D** — `TestBackupFreshness_ExistingBehavioursUnchanged` pins all three pre-existing outcomes
|
||||
**including their exact reason strings** (`"newest backup is 30h0m0s old (limit 26h0m0s)"`,
|
||||
`"newest PBS snapshot failed verification"`, `"latest host-report could not be parsed"`), each with
|
||||
fresh window evidence present so the result cannot be an artefact of the new input. Plus
|
||||
`TestBackupFreshness_WindowEvidenceDoesNotRescueFailedVerify` — the anchor must not suppress an
|
||||
integrity fault.
|
||||
- **E** — `TestBackupFreshness_ReasonStringsAreDistinct` fails if any two of the six failure modes
|
||||
ever produce the same message.
|
||||
|
||||
---
|
||||
|
||||
## 6. Replay of the 2026-07-26 shape — **actual reports, not a fixture**
|
||||
|
||||
Two replays, both against the real thing.
|
||||
|
||||
**(a) Real report payloads** extracted from the production hub DB (every retained report in the
|
||||
7-day lookback at or before the check instant), fed through `newestBackupEvidence` +
|
||||
`assessBackupFreshness` at `now = 2026-07-26 03:00:00 UTC`:
|
||||
|
||||
```
|
||||
demo-felhom rows=600 first=2026-07-18T16:30:51Z windowEvidence=2026-07-25T06:30:14Z -> OK
|
||||
demo-hp rows=417 first=2026-07-21T16:24:48Z windowEvidence=2026-07-25T10:23:31Z -> OK
|
||||
drill-r50 rows= 77 first=2026-07-25T09:50:55Z windowEvidence=none -> UNKNOWN
|
||||
"no backup evidence yet, but only watching for 17h0m0s (grace 26h0m0s since first
|
||||
contact 2026-07-25T09:50:55Z) — newborn host, not a fault"
|
||||
```
|
||||
|
||||
**(b) Through the real store queries** — a copy of the live `hub.db` opened with `store.New`, driving
|
||||
`GetHostReportsSince` / `GetFirstHostReportAt` / `GetLatestHostReportJSON` for real. This was worth
|
||||
doing separately: the window query compares `received_at` as a **SQLite datetime string**, which is a
|
||||
genuine correctness risk that a hand-built fixture would not have exercised. Identical verdicts, plus:
|
||||
|
||||
```
|
||||
peti-felhom NO host-report → deadline check skips the backup half entirely
|
||||
```
|
||||
|
||||
**All three would now be silent. Zero events.** Both harnesses were throwaway and are deleted; the
|
||||
tree is clean. Only the DB *copy* was ever opened writable — the live `/data/hub.db` was not touched.
|
||||
|
||||
Note what the verdicts say: demo-felhom and demo-hp are **OK** (real evidence was found, not
|
||||
suppressed), and drill-r50 is **UNKNOWN** (correctly deferred as a newborn, and it will alarm on its
|
||||
own if it is still backup-less after the grace). None of the three is silenced by a blanket rule.
|
||||
|
||||
---
|
||||
|
||||
## 7. Deployment
|
||||
|
||||
| Step | Result |
|
||||
| Ruling | Implementation |
|
||||
|---|---|
|
||||
| Image build + push | `gitea.dooplex.hu/admin/felhom-hub:0.75.0`, 25 MB, digest `sha256:a2ebaa50…10453` |
|
||||
| Manifest pin | `manifests/hub.yaml` → `0.75.0` (commit `88b41ec`) — **GitOps only**, no `kubectl set image` |
|
||||
| ArgoCD | hard-refresh + deliberate sync → **Synced / Healthy** |
|
||||
| Rollout | `deployment "hub" successfully rolled out` |
|
||||
| Pod | `hub-56946d5cb4-kwwn7` 1/1 Running |
|
||||
| **Running image (pod status, not spec)** | `felhom-hub:0.75.0` @ `sha256:a2ebaa50…10453` — **matches the pushed digest** |
|
||||
| Startup log | `[INFO] felhom-hub 0.75.0 starting` … `[INFO] deadline-check: next run at 2026-07-27 05:00 CEST (in 17h14m19s)` |
|
||||
| Two weeks of offsite backups | `keep_last=2`; the blanket PBS-prune refusal scoped to *additional* tiers with an explicit setting — the primary keeps the absolute refusal, because its target **and** retention both default and could prune the DR by accident |
|
||||
| Grow the datastore before any real tester | recorded in `07` §9.1; no action taken |
|
||||
| First backup runs as long as needed; nothing else starts until done | wait bound → 12 h (measured ~5 h for a first full snapshot); **one backup at a time per guest** — a second tier gets a 409 naming the busy tier, with no job id it could mistake for its own; a tier overrunning the quiesce bound defers the rest |
|
||||
| Drill box is temporary | dropped from the rollout |
|
||||
| Restore test, then next slice | done — see §6 |
|
||||
|
||||
The pin landed and matches — checked deliberately, since the DIAG found `hub.yaml` pinned at
|
||||
`0.73.1` while the CHANGELOG read `0.73.2`.
|
||||
## 5. Four defects found by RUNNING it, not reviewing it
|
||||
|
||||
---
|
||||
1. **30-minute wait bound vs a 41-minute backup** (v0.98.0). The agent recorded `success:false`
|
||||
**while the vzdump was still running**, and it later completed `TASK OK`. Not "the backup didn't
|
||||
happen" but worse: the tier stays permanently due and the retry collides with the guest lock.
|
||||
2. **The restore tier read from the configured target, not the archive** (v0.100.0). A `felhom-pbs:`
|
||||
archive was classified `local` and got the 10-minute bound against a 14.46 GB WAN restore, failing
|
||||
at 600 s. **This was a silent regression of the S4.1 fix** — the mechanism was never removed, its
|
||||
*input* changed when `local_backup_target` was retargeted to `local`. The lesson is not "add a
|
||||
timeout" (one was already there) but that a fix keyed on *"the configured target"* stops holding
|
||||
the moment more than one target exists. Recorded in `06-offsite-connectivity.md`.
|
||||
3. **A leaked scratch guest kept `onboot: 1`** (v0.101.0) — a host reboot would have started a clone
|
||||
of the live guest. Now `onboot=0` is set **at restore time**, because "after" is the path that
|
||||
leaks.
|
||||
4. **A tier fires at a not-yet-provisioned storage** (v0.102.0) — would have quiesced the apps and
|
||||
failed every cadence on a fresh box until DR provisioning.
|
||||
|
||||
## 8. NOT yet live-validated — explicit
|
||||
## 6. Live validation
|
||||
|
||||
1. **The 05:00 CEST deadline check has not fired under v0.75.0.** Next run 2026-07-27 05:00 CEST. All
|
||||
evidence above is unit-proven + replayed against real data; the live firing is unobserved.
|
||||
2. **The deferred-UNKNOWN INFO line has not appeared in a production log** — proven by unit test only.
|
||||
drill-r50's 26 h grace expires 2026-07-26 ~11:50 UTC, so by 03:00 on 07-27 it will instead
|
||||
**alarm** — correctly, and that will be the first real test of Scenario B in production.
|
||||
3. **No genuinely-dead box has been observed alarming live.** The B path is unit-proven only.
|
||||
4. **The cold path (full 7-day lookback scan) has not run in production.** Cost is bounded and the
|
||||
healthy path early-exits at one row, but the scan-heavy branch is untimed live.
|
||||
5. **demo-hp and drill-r50 host-level state remains uncollected** (no SSH key; break-glass not used) —
|
||||
unchanged from the DIAG.
|
||||
**demo-felhom** — the first real PBS-targeted backup: **`TASK OK`, 41 minutes, 14.46 GB snapshot**,
|
||||
and it **restored cleanly** (`vzrestore: stopped OK`, all volumes back). Both tiers armed and
|
||||
verified over the real local API; the untargeted response confirmed byte-identical.
|
||||
|
||||
---
|
||||
**demo-hp** — reached via the documented break-glass path; binary and config backed up first;
|
||||
v0.102.0 deployed, tier armed, and its **first ever** PBS backup triggered against a namespace that
|
||||
was verifiably empty. *(In flight at the time of writing.)*
|
||||
|
||||
## 9. Observations — recorded, not acted on
|
||||
**Hub Slice C replayed against the live DB before deploying:**
|
||||
|
||||
1. **`parseBackupTime`'s silent `continue` is a latent member of the same class.** An unparseable
|
||||
timestamp is dropped without a trace, so a wire-format change on the agent side would present as
|
||||
"no backup" — the exact 07-26 failure, with no log line to catch it. Left untouched per scope
|
||||
(the agent emits clean RFC3339 `Z`, confirmed in the DIAG). Worth a WARN log when R-84 touches the
|
||||
agent side of this contract.
|
||||
2. **The customer-facing Hungarian copy is unchanged.** `notify/templates.go:106` — *"A mai
|
||||
biztonsági mentés nem készült el a határidőig!"* — still overstates scope (it reads as *all*
|
||||
backups failed; this check only covers the host/PBS tier). Not in this task's Parts 1–3. The DIAG
|
||||
proposed it as fix #4; it needs a `felhom-ui-design` copy pass and the confirm gate.
|
||||
3. **`GetFirstHostReportAt` returns the prune horizon, not true first contact, for hosts older than
|
||||
90 days.** Accepted and documented at the function: it only makes the anchor *more* conservative
|
||||
for long-lived hosts (the window has elapsed either way) and never shortens a newborn's grace.
|
||||
4. **The `backupStaleAfter` landmine is now written into the constant's comment** and carried as a
|
||||
named dependency in R-82. It will alarm on a healthy weekly PBS snapshot six days in seven.
|
||||
5. **`retention.max_days: 90` is now load-bearing for a monitor**, not just for storage. Shortening it
|
||||
below `backupEvidenceLookback` would silently narrow the evidence window. Not currently guarded.
|
||||
6. **The R-80 real finding stands untouched:** the PBS/offsite-DR tier has no schedule at all —
|
||||
demo-felhom still holds exactly one PBS snapshot (2026-07-18), demo-hp zero. R-81 fixes how the hub
|
||||
*reads* backup state; it does nothing about the DR tier being empty. **That is R-82, and it is the
|
||||
more serious of the two.**
|
||||
7. The customer-channel suppression the DIAG recommended (removing `expected_backup_missed` from
|
||||
demo-felhom's `enabled_events`) was **not** applied — still an operator decision, and now much less
|
||||
pressing since the false-positive path is closed.
|
||||
```
|
||||
demo-felhom host=07-26T14:38Z offsite=07-26T12:21Z -> OK
|
||||
demo-hp host=07-26T07:06Z offsite=none -> UNKNOWN (119h of a 192h grace)
|
||||
drill-r50 host=none offsite=not expected -> MISSED (no evidence in 29h)
|
||||
```
|
||||
|
||||
---
|
||||
**No customer email results from the deploy.** demo-hp defers correctly and will alarm in ~3 days if
|
||||
its offsite tier stays empty — the true finding arriving on schedule, not a false alarm.
|
||||
|
||||
## 10. Scope discipline
|
||||
## 7. A correction I had to make mid-arc
|
||||
|
||||
Not started, recorded only: **R-82** (backup target split — the real finding), **R-83** (ratify or
|
||||
retire `07-backup-architecture.md`), **R-84** (persist the agent's backup store — the cause-fix).
|
||||
No branches; every commit staged with explicit paths (never `git add -A`, per the shared-worktree
|
||||
rule); no `Co-Authored-By`.
|
||||
I reported that the restore-test would boot a scratch guest carrying the live guest's MAC, static
|
||||
island IP and hostname, and so would break the controller→agent link. **That was wrong.**
|
||||
`RunRestoreTest` step 2 link-downs **every** interface before the guest is started, and it is
|
||||
unit-tested. I read a restored config artifact, inferred the boot behaviour from it, and escalated
|
||||
before reading the code path that consumes it. I also disabled the scheduled restore-test on that
|
||||
basis, which was an unnecessary reduction in safety coverage; it is re-enabled.
|
||||
|
||||
The residual hazard was real but far narrower — it needed the restore to fail *before* the link-down
|
||||
step, which is what defect 1 caused — and that is what v0.101.0 fixes.
|
||||
|
||||
Separately, `06-offsite-connectivity.md` records that the teardown `403` I flagged as a standing
|
||||
privilege gap was **already diagnosed in S4.1 as a phantom**: it is a consequence of the short
|
||||
timeout, not an ACL problem. With the timeout fixed the guest is pool-associated by teardown time.
|
||||
|
||||
## 8. Tests
|
||||
|
||||
| Repo | Result |
|
||||
|---|---|
|
||||
| felhom-agent | `build/vet/test` rc=0, **29 packages** |
|
||||
| felhom-controller | `build/vet/test` rc=0, **27 packages** |
|
||||
| felhom.eu (hub) | `build/vet/test` rc=0, **17 packages** |
|
||||
|
||||
Red-proofs observed and restored for every mandatory scenario: old-controller compat, new-controller
|
||||
degrade (the hollow version asserts "no error" while silently skipping the backup), the both-due
|
||||
night (**the COUNT is the assertion** — asserting only "both ran" passes against a double-quiesce),
|
||||
the merged threshold, the per-tier wait bound, the `onboot` override, and the overrun defer.
|
||||
|
||||
**A process failure worth recording:** I ran the agent suite and committed in the same command, read
|
||||
`packages ok: 28`, and pushed **without reading `rc=1`**. Five of my own Slice A tests were failing —
|
||||
a harness artifact, not a product bug, but the commit went out red. Fixed in `13ca2d9`. This is the
|
||||
exact exit-code trap recorded twice earlier in this arc.
|
||||
|
||||
## 9. NOT done — explicitly
|
||||
|
||||
1. **The offsite tier is never AUTOMATICALLY restore-tested.** The scheduled restore-test picks
|
||||
candidates from a runner built on the primary target, so it can never select a PBS archive. This
|
||||
is arguably the more important half of "is the DR tier real?" and it needs a per-tick spec.
|
||||
2. **The hub infers "PBS ⇒ weekly" from storage TYPE.** `defaultBackupTarget` is `felhom-pbs`, so a
|
||||
box that never sets `local_backup_target` would run PBS as its **daily** tier and be judged
|
||||
against 8 days — seven days of blindness. No box is in that shape today. The real fix is the agent
|
||||
reporting each tier's actual cadence.
|
||||
3. **Every agent restart re-triggers an offsite backup** (the in-memory store, R-84). On the local
|
||||
tier that is wasted minutes; on the offsite tier it is a wasted multi-hour upload after every
|
||||
deploy. R-84 is now closer to a prerequisite than a tidy-up.
|
||||
4. **The installer-default fleet flip** (Slice D step 4) waits on a full weekly cycle holding — a
|
||||
genuine gate, not an oversight.
|
||||
5. **The boot+verify half of the restore round-trip has not run under the fixed code.**
|
||||
6. **`07-backup-architecture.md` is NOT ratified** — brought current with an honest staleness header;
|
||||
ratification is Viktor's review of the §10 list.
|
||||
|
||||
## 10. Observations
|
||||
|
||||
- The `felhom-pbs` PVE storage will permanently show **0 %** in the PVE UI (namespace-scoped token).
|
||||
Operators must read fill from the hub's PBS-DR gauge. Worth a runbook line.
|
||||
- demo-felhom's guest grew **9.74 → 14.46 GB logical in eight days**. Probably one-off from app
|
||||
testing, but if it is a rate the capacity sizing changes quickly.
|
||||
- demo-felhom was enabled **before** demo-hp, out of the specified rollout order, because Slice A
|
||||
could not be validated otherwise.
|
||||
- The customer-facing Hungarian copy still overstates scope ("A mai biztonsági mentés nem készült el
|
||||
a határidőig!" covers only the host/PBS tier). Unchanged; flagged since the R-80 diagnostic.
|
||||
|
||||
Reference in New Issue
Block a user