The customer page's Backup card read `Snapshots 0 / Repo Size 0 MB / Integrity
Unknown` for EVERY customer, indefinitely. Measured on demo-hp 2026-08-30 while
that night's controller log said `[offbox] backup OK: 8 app(s) backed up, 67
snapshot(s), 2m14s` and the box held snapshot_count:67, repo_size_bytes:
140829678, stats_known:true.
A card reading "no backups" over a working backup is worse than no card -- the
R-88 direction of failure (degrade to NO BACKUP rather than to UNKNOWN) on the
one screen that answers "is this customer protected?".
The data was never missing. The card rendered the report's `backup` object,
whose snapshot/size/integrity fields have had no producer since slice 8C. The
live numbers are in the `offsite` object, which THIS PACKAGE already reads for
the Offsite page and which monitor.OffsiteChecker already alarms from. Proof the
bytes were arriving: the Offsite page rendered demo-hp's usage as 0.1 GB from
that very object while the Backup card said 0 MB. So this is a render fix over
an existing feed, not a new pipeline.
Not a one-line swap, because snapshot_count:0 means two opposite things --
"holds nothing" and "never measured". R-225 measured that confusion one layer
down. backup_card.go resolves a three-way ruling in Go (a {{if}} chain over
map[string]interface{} float64s cannot keep the absent/zero distinction the card
is entirely about):
no offsite object -> "No off-site data reported", and says explicitly that
this is NOT the same as "no backups"
disabled + state -> names the blocker (needs_credential)
stats_known:false -> em-dash + "never been measured". NEVER 0
stats_known:true -> the real numbers, INCLUDING a real 0
A pre-v0.225.0 controller sends no stats_known -> false -> "unknown". That
direction is pinned: upgrading the hub ahead of the fleet must not report every
un-upgraded customer as having zero backups.
The Integrity row is DELETED, not re-sourced: nothing produces it, the
controller runs no integrity check, and NotifyIntegrityOK/Failed are called from
nowhere.
RED-PROOF: restore the pre-fix card markup -> all four tests fail, reporting 67
and 134.3 MB absent from the rendered page and the Integrity row present. The
tests drive handleCustomerUnified and grep the HTML on purpose: the defect was
the template's choice of source object, so a test one layer below it would have
been green against the shipped bug.
Green gate clean: 18 packages, rc 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LB8FmJaGd2cyjvy6dbEjpM
5.6 KiB
REPORT — R-331: the operator Backup card said every customer had no backups
Hub v0.109.0 (with controller v0.225.0) · 2026-08-30
1. What was wrong
The hub customer page's Backup card read, for every customer, indefinitely:
Enabled Yes Snapshots 0
Repo Size 0 MB Integrity Unknown
Measured on demo-hp 2026-08-30, at which moment the truth was:
| source | value |
|---|---|
the box's own settings.json |
snapshot_count: 67, repo_size_bytes: 140829678, stats_known: true |
| that night's controller log | [offbox] backup OK: 8 app(s) backed up, 67 snapshot(s), 2m14s |
| this hub's own Offsite page | 0.1 GB used of a 50 GB quota — read from the same stored report |
A card that reads "no backups" over a working backup is worse than no card. It is the R-88 direction of failure — degrading to no backup rather than to unknown — on the one screen an operator consults to answer "is this customer protected?".
2. Root cause
The card rendered the report's backup object. Its snapshot_count, repo_size_mb and
integrity_ok fields have had no producer since disk-tier restic moved to the host agent (slice
8C) — the controller's buildBackupReport leaves them zero deliberately and says so in a comment.
The zeros were correct values for dead fields, rendered as if live.
The data was never missing. The live numbers ride in the report's offsite object, which this
package already reads for the Offsite page (offsiteUsageBytes) and which monitor.OffsiteChecker
already drives fill and staleness alarms from. That the Offsite page rendered demo-hp's real usage
from the same stored report, at the same moment the Backup card said 0 MB, is the proof the bytes
were arriving. This is a render fix over an existing feed, not a new pipeline.
3. Why it was not a one-line template swap
snapshot_count: 0 means two opposite things — this repository holds nothing and nobody has ever
measured this repository. R-225 measured that confusion one layer down: a rebuilt box rendered
„Tarolo meret · 0 pillanatkep" over a store that really held snapshot f3d9cd67, and the controller's
StatsKnown fixed it there. It was never on the wire, so rendering the count without it would have
moved R-225 up to the hub instead of fixing anything. Controller v0.225.0 now forwards
stats_known.
4. What changed
hub/internal/web/backup_card.go builds a typed backupCardView — resolved in Go, because the card's
whole subject is a distinction a template {{if}} chain over map[string]interface{} float64s cannot
keep:
| report state | card shows |
|---|---|
no offsite object at all |
"No off-site data reported" — and says explicitly this is not the same as "no backups" |
enabled:false + declared state |
the blocker by name (needs_credential) — a different operator action from "not enabled" |
enabled, stats_known:false |
—, plus "never been measured". Never 0 |
enabled, stats_known:true |
the real count and size, including a real 0 — measured empty is knowledge |
A pre-v0.225.0 controller sends no stats_known, which unmarshals to false → "unknown". That is
the fail-safe direction: upgrading the hub ahead of the fleet must not tell the operator that every
un-upgraded customer has zero backups. Pinned by a test.
The Integrity row is deleted, not re-sourced. Nothing produces it: the controller runs no integrity
check, and NotifyIntegrityOK / NotifyIntegrityFailed exist and are called from nowhere. A row
that can only ever read "Unknown" is not information, and one that could read "OK" from an unwritten
field would be a lie.
fmtBytesAuto is new rather than reusing fmtBytesGB: that one is fixed at GB because it renders
against GB quotas, and it turns demo-hp's real 140 829 678 bytes into 0.1 GB — which on a card whose
entire defect was under-reporting a real backup reads as "nearly nothing".
5. Tests and the red-proof
r331_backup_card_test.go asserts the rendered page, using demo-hp's real reported values, so a
regression fails against the same numbers the defect was measured against. The defect lived in the
template's choice of source object, so a test one layer below it would have been green against the
shipped bug — which is why these drive handleCustomerUnified and grep the HTML.
RED-PROOF (run 2026-08-30): restoring the pre-fix card markup fails all four tests —
the rendered Backup card does not contain demo-hp's real snapshot count (67),
the card does not carry the real repository size (134.3 MB ...),
the card still shows an Integrity row, plus every branch of the three-way ruling. Restored
immediately; git diff clean.
Green gate: go build ./... && go vet ./... && go test ./... in hub/ — 18 packages, rc 0.
6. Deployment
PENDING at the time of writing — see the follow-up commit. The two halves ship independently and in either order: the hub renders "unknown" for any box still on controller 0.224.0, which is correct rather than wrong.
7. Not done, and why
- No staleness verdict on the card.
monitor.OffsiteCheckeralready owns that and alarms on it. A second verdict over the same data is two things that can disagree — a shape this codebase has already paid for (LastRunvsLastSuccess, R-100). - The dead
backupfields were not removed from the controller's wire format. Removing them would stop historical reports already in this hub's store from parsing, for no gain — nothing renders them now, and a controller-side test fails if anything starts producing them.