hub v0.104.0: the guest network gets a reader (R-319), and the hub half of the naming (R-295)
gates / gates (push) Successful in 14s
gates / gates (push) Successful in 14s
Four paper debts and one fact given a reader. Hub-only — nothing to bake. A4 — the entry about "the tester's machine" named a risk correctly and labelled it in a way that invited deleting it. Established from the hub's own store: `peti-felhom` is a REAL machine (482 reports, 2026-02-27 → 2026-07-15, a named person's own box) and the 3.6 GB with no key and no backup is real. `david` → `tester-1` is a DIFFERENT record with no host, no escrow and no report, ever — deleted 07:55:49 and re-created 07:56:47 this morning. The prompt's premise conflated the two; the register now says which is which. A1 — R-312/R-313/R-303 recorded as DECIDED with their re-open triggers, and moved out of STATUS's "Waiting on you", which is now empty. A3 — day0-install §C.1 said pushing the installer publishes it. It has not since R-110. Corrected, with the two manifest pins named and an outside-verification command; the one copy that repeated it (a dated audit, true when written) carries a superseded note. A5 — standing rule 5: evidence comes off the machine at the end of the phase that produced it, before any revert. Earned twice in three days on the same box at the same point (R-320). Four homes, plus what to do when it is already gone. R-295 hub half — „Beállító kód" everywhere; „Visszaállító kód" retired. New `reenroll` mail kind so the mail names the page a REBUILT box actually shows („A szerver beállítása"), not the „Elfelejtett jelszó" page it has no login screen to reach. Naming only; the acceptance pin proves the secret is untouched. R-319 — the hub models `guest_net` after 23 days of receiving and discarding it. The signal is `heals_last_hour`, not `state`: a guest the watchdog keeps repairing reads healthy between repairs. `heal_succeeded` decoded too (R-260's lesson). Unknown is never drawn as healthy — three absences, three sentences. No alarm, deliberately. Three red-proofs, mutations asserted applied. Wire-gate checked tags 182 → 190. B1 — the operator's 2026-08-12 dispositions were NOT in the register; they are now. Third allowlist kind for the five ruled "no reader wanted"; `reporting_disabled` reclassified redundant. 8 read · 5 deliberately unread · 1 redundant · 6 still owed. Also filed: R-321 (a deliberately-silent box still alarms stale/down — the checker is age-only, and decoding the flag would not have fixed it), R-322 (the claim guard has never scanned the hub; a hand scan returns zero, so it is a scope gap, not a defect).
This commit is contained in:
@@ -263,6 +263,175 @@ func (s *Server) hostNetwork(host *store.Host, reportJSON string) hostNetworkVie
|
||||
return v
|
||||
}
|
||||
|
||||
// ── R-319 — the guest-network watchdog gets a reader ────────────────────────────────────────────
|
||||
//
|
||||
// FIRST OF THE R-264 READERS. The agent has reported `guest_net` on every heartbeat since v0.92.0
|
||||
// (R-54, 2026-07-21) and the hub — the component that emails the operator — modelled NONE of it: the
|
||||
// string `guest_net` occurred nowhere in this repository. It was stored as raw text inside
|
||||
// `report_json` and read by nothing.
|
||||
//
|
||||
// WHY THIS ONE FIRST. It has a live incident behind it: a killed `dhclient` in a guest took a tunnel
|
||||
// down for 1 h 15 m with nobody told (`audits/INCIDENT-guest-dhclient-killed-2026-07-20.md`), and the
|
||||
// watchdog built afterwards has been reporting exactly that condition ever since — to a hub that
|
||||
// modelled none of it. R-264 named it "the strongest candidate of the twenty-one".
|
||||
//
|
||||
// WHAT IS ACTUALLY BEING READ, and it is NOT just the current state. The signal is
|
||||
// `heals_last_hour`: a guest whose network the watchdog keeps REPAIRING is healthy at every instant
|
||||
// anyone looks and is nevertheless failing. Rendering only `state` would give that machine a green
|
||||
// tick — which is the exact shape of the defect that drew a failed disk as a healthy empty disk.
|
||||
// RepairCount is therefore surfaced beside the state, not behind it.
|
||||
//
|
||||
// AN UNKNOWN IS NEVER DRAWN AS HEALTHY. Three distinct absences are kept apart, following the
|
||||
// companion-flag convention this project settled in August (`hostNetworkView` above is the sibling):
|
||||
// - AgentTooOld — the agent predates v0.92.0, so absence is EXPECTED but still tells us nothing;
|
||||
// - Reported=false — a new-enough agent sent no stanza (watchdog disabled, or a report that
|
||||
// predates the feature on this box);
|
||||
// - a guest whose `state` is empty or unrecognised → rendered "unknown", never healthy.
|
||||
// A malformed stanza degrades to Reported=false and must never 500 the page.
|
||||
|
||||
// minAgentForGuestNet is the agent release that first reported `guest_net` (v0.92.0, R-54). Below it
|
||||
// the stanza is absent BY CONSTRUCTION, so its absence is unknown-and-expected rather than a finding.
|
||||
const minAgentForGuestNet = "0.92.0"
|
||||
|
||||
// guestNetGuestView is one owned guest's network health as the watchdog last saw it.
|
||||
type guestNetGuestView struct {
|
||||
VMID int
|
||||
State string // healthy | unhealthy | static_fault | unknown ("" → unknown)
|
||||
Mode string // dhcp | static | unknown
|
||||
IP string
|
||||
HasRoute bool
|
||||
DHClientAlive bool
|
||||
// RepairCount is `heals_last_hour` — THE SIGNAL. A machine repairing itself over and over is
|
||||
// telling you something that its current state cannot.
|
||||
RepairCount int
|
||||
LastHealAt string
|
||||
Damped bool
|
||||
Message string
|
||||
// Healed / HealSucceeded are carried DELIBERATELY, and together. R-260 is this project's
|
||||
// warning: the OOB decoder mirrored five of the agent's eight fields, and the three it dropped
|
||||
// included the one that decided the question the checker existed to answer. A repair that FAILED
|
||||
// is a different fact from a repair that worked, and the count alone cannot express it — six
|
||||
// successful repairs is a nuisance, six FAILED ones is a guest that is down right now.
|
||||
Healed bool
|
||||
HealSucceeded bool
|
||||
}
|
||||
|
||||
// HealFailed reports that the watchdog TRIED to repair this guest and did not succeed. Kept separate
|
||||
// from State because the two can disagree: the sweep that failed to heal is not necessarily the sweep
|
||||
// that set the state.
|
||||
func (g guestNetGuestView) HealFailed() bool { return g.Healed && !g.HealSucceeded }
|
||||
|
||||
// Unknown reports whether this guest's state is one the watchdog did not positively assert. An empty
|
||||
// or unrecognised state is drawn as unknown; it is never allowed to fall through to the healthy
|
||||
// branch. Value receiver — a pointer receiver compiles, vets, passes the suite and 500s at render.
|
||||
func (g guestNetGuestView) Unknown() bool {
|
||||
switch g.State {
|
||||
case "healthy", "unhealthy", "static_fault":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Repairing reports whether the watchdog has had to repair this guest inside the last hour. This is
|
||||
// deliberately independent of State: the whole point is that a REPAIRED guest reads healthy.
|
||||
func (g guestNetGuestView) Repairing() bool { return g.RepairCount > 0 }
|
||||
|
||||
// guestNetView is the Guest network card's whole view-model.
|
||||
type guestNetView struct {
|
||||
// Reported is the ONLY thing that licenses drawing any health at all. False → unknown.
|
||||
Reported bool
|
||||
// AgentTooOld distinguishes "this agent cannot report it" from "a capable agent said nothing".
|
||||
// Both render as unknown; they need different words, and conflating them is how an operator
|
||||
// starts ignoring the card.
|
||||
AgentTooOld bool
|
||||
CheckedAt string
|
||||
Guests []guestNetGuestView
|
||||
// RepairingCount / UnhealthyCount drive the card's summary badge. Counted rather than derived in
|
||||
// the template: template logic that computes a verdict is logic nobody tests.
|
||||
RepairingCount int
|
||||
UnhealthyCount int
|
||||
UnknownCount int
|
||||
// HealFailedCount — repairs ATTEMPTED and not succeeded. Counted separately from
|
||||
// RepairingCount: a failing repair is a harder fact than a frequent one.
|
||||
HealFailedCount int
|
||||
}
|
||||
|
||||
// Degraded is true when any owned guest is unhealthy OR is being repeatedly repaired. The repair leg
|
||||
// is the one that matters: without it a guest the watchdog fixes every ten minutes reports "healthy"
|
||||
// for ever.
|
||||
func (v guestNetView) Degraded() bool {
|
||||
return v.UnhealthyCount > 0 || v.RepairingCount > 0 || v.HealFailedCount > 0
|
||||
}
|
||||
|
||||
// parseGuestNet extracts the `guest_net` stanza. A missing or malformed body yields Reported=false
|
||||
// (unknown), never an error and never a partial claim of health.
|
||||
func parseGuestNet(reportJSON string) guestNetView {
|
||||
v := guestNetView{Guests: []guestNetGuestView{}}
|
||||
if reportJSON == "" {
|
||||
return v
|
||||
}
|
||||
var body struct {
|
||||
GuestNet *struct {
|
||||
CheckedAt string `json:"checked_at"`
|
||||
Guests []struct {
|
||||
VMID int `json:"vmid"`
|
||||
State string `json:"state"`
|
||||
Mode string `json:"mode"`
|
||||
IP string `json:"ip"`
|
||||
HasRoute bool `json:"has_route"`
|
||||
DHClientAlive bool `json:"dhclient_alive"`
|
||||
HealsLastHour int `json:"heals_last_hour"`
|
||||
Healed bool `json:"healed"`
|
||||
HealSucceeded bool `json:"heal_succeeded"`
|
||||
LastHealAt string `json:"last_heal_at"`
|
||||
Damped bool `json:"damped"`
|
||||
Message string `json:"message"`
|
||||
} `json:"guests"`
|
||||
} `json:"guest_net"`
|
||||
}
|
||||
// A decode error is NOT propagated: one box's bad field must not break the page for the rest of
|
||||
// the fleet. It degrades to Reported=false, which renders as unknown — the safe direction.
|
||||
if err := json.Unmarshal([]byte(reportJSON), &body); err != nil || body.GuestNet == nil {
|
||||
return v
|
||||
}
|
||||
v.Reported = true
|
||||
v.CheckedAt = body.GuestNet.CheckedAt
|
||||
for _, g := range body.GuestNet.Guests {
|
||||
gv := guestNetGuestView{
|
||||
VMID: g.VMID, State: g.State, Mode: g.Mode, IP: g.IP,
|
||||
HasRoute: g.HasRoute, DHClientAlive: g.DHClientAlive,
|
||||
RepairCount: g.HealsLastHour, LastHealAt: g.LastHealAt,
|
||||
Damped: g.Damped, Message: g.Message,
|
||||
Healed: g.Healed, HealSucceeded: g.HealSucceeded,
|
||||
}
|
||||
switch {
|
||||
case gv.Unknown():
|
||||
v.UnknownCount++
|
||||
case gv.State == "unhealthy" || gv.State == "static_fault":
|
||||
v.UnhealthyCount++
|
||||
}
|
||||
if gv.Repairing() {
|
||||
v.RepairingCount++
|
||||
}
|
||||
if gv.HealFailed() {
|
||||
v.HealFailedCount++
|
||||
}
|
||||
v.Guests = append(v.Guests, gv)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// guestNet builds the card's view-model, applying the version gate before the report is consulted so
|
||||
// that an old agent's silence is named as such rather than rendered as a finding.
|
||||
func (s *Server) guestNet(host *store.Host, reportJSON string) guestNetView {
|
||||
if host.AgentVersion != "" && semver.Valid(host.AgentVersion) &&
|
||||
semver.Compare(host.AgentVersion, minAgentForGuestNet) < 0 {
|
||||
return guestNetView{Guests: []guestNetGuestView{}, AgentTooOld: true}
|
||||
}
|
||||
return parseGuestNet(reportJSON)
|
||||
}
|
||||
|
||||
// storageTargetView is the rich per-drive row the host-detail Storage Targets table renders:
|
||||
// fill %, role/state, thin-pool, and SMART health/temp/wear. Parsed from the latest report's
|
||||
// storage_targets[] (the full hostStorageTarget wire shape lives in the api package; this view
|
||||
@@ -533,6 +702,9 @@ func (s *Server) hostDetailData(host *store.Host, r *http.Request) map[string]in
|
||||
// v0.85.0 Network — the host's addresses + its WireGuard allocation.
|
||||
network := s.hostNetwork(host, reportJSON)
|
||||
|
||||
// R-319 Guest network — the R-54 watchdog's per-guest verdict AND its repair count.
|
||||
guestNet := s.guestNet(host, reportJSON)
|
||||
|
||||
// v0.84.0 Console access — presence + username + set_at ONLY. GetHostRecoveryMeta cannot carry
|
||||
// the secret (its query does not select the column); the plaintext reaches the operator solely
|
||||
// through POST /hosts/{id}/reveal-recovery-credential.
|
||||
@@ -573,6 +745,7 @@ func (s *Server) hostDetailData(host *store.Host, r *http.Request) map[string]in
|
||||
// v0.84.0 break-glass Console access card. NEVER add a key holding the secret.
|
||||
// v0.85.0 Network card (addresses + WireGuard allocation/confirmation).
|
||||
"Network": network,
|
||||
"GuestNet": guestNet,
|
||||
"RecoveryVaulted": recoveryMeta != nil,
|
||||
"RecoveryUsername": func() string {
|
||||
if recoveryMeta != nil {
|
||||
|
||||
Reference in New Issue
Block a user