{{/* host_detail_body — the shared per-host detail sections, rendered by BOTH the standalone /hosts/{id} page (host_detail.html) and the customer page's Host tab (customer_unified.html, one instance per host). Data = the map built by hostDetailData (web/hosts.go); never carries api_key/escrow/PBS secret values. */}} {{define "host_detail_body"}}

{{.HostID}}

{{.StatusLabel}}
Host ID {{.HostID}}
Agent Version {{if .AgentVersion}}{{.AgentVersion}}{{else}}—{{end}}
{{if .AgentBinaryDrift}}
Agent binary {{if eq .AgentBinaryDrift "ok"}} matches vouched {{slice .ReportedAgentSHA 0 12}}… {{else}} DRIFT — running {{slice .ReportedAgentSHA 0 12}}…, vouched {{slice .VouchedAgentSHA 0 12}}…
Same version as the vouched agent, different bytes (a hand build?). Self-update sees the version as installed and will not correct it (R-349) — deliver the published artifact.
{{end}}
{{end}} {{if .WrapperDrift}}
PBS wrapper {{if eq .WrapperDrift "ok"}} matches vouched {{slice .ReportedWrapperSHA 0 12}}… {{else}} DRIFT — installed {{slice .ReportedWrapperSHA 0 12}}…, vouched {{slice .VouchedWrapperSHA 0 12}}…
/usr/local/sbin/felhom-pbs-apply differs from the manifest. It is delivered unversioned from main (R-50b), so this host may be running privileged code from a different commit.
{{end}}
{{end}}
Enrolled {{timeAgo .CreatedAt}}
Last Report {{if .HasReport}}{{timeAgoPtr .LastReportAt}}{{else}}waiting for first report{{end}}
Box connection {{if eq .BoxConnection.State "connected"}}connected now {{else if eq .BoxConnection.State "not_connected"}}{{if .BoxConnection.NeverSeen}}not connected since the hub started ({{.BoxConnection.Since.UTC.Format "2006-01-02 15:04"}} UTC, {{timeAgo .BoxConnection.Since}}){{else}}last connected {{.BoxConnection.Since.UTC.Format "2006-01-02 15:04"}} UTC ({{timeAgo .BoxConnection.Since}}){{end}} {{else}}unknown (the hub restarted recently, or the host has no customer){{end}}
Desired Generation {{.DesiredGeneration}}
{{if .RecoveryMode}}
Recovery Mode ACTIVE (until {{timeAgoPtr .RecoveryUntil}})
{{end}}
{{if .HasReport}}

Vitals

CPU {{formatFloat .Vitals.CPUPercent}}%
Memory {{formatFloat .Vitals.MemoryPercent}}%
Disk (root fs) {{formatFloat .Vitals.DiskPercent}}%
Cloudflared {{if .Vitals.CloudflaredStatus}}{{.Vitals.CloudflaredStatus}}{{if .Vitals.CloudflaredDetail}} ({{.Vitals.CloudflaredDetail}}){{end}}{{else}}—{{end}}
Guests {{.GuestRunning}}/{{.GuestTotal}} running
{{else}}

Waiting for first report.

Vitals, guests and storage appear once this host's agent sends a host-report.

{{end}}

Guests

{{if .Guests}} {{range .Guests}} {{end}}
VMID Name Status Controller Last Seen
{{.VMID}} {{if .DisplayName}}{{.DisplayName}}{{else}}—{{end}} {{if eq .Status "running"}}{{.Status}}{{else if eq .Status "stopped"}}{{.Status}}{{else}}{{.Status}}{{end}} {{if .ControllerVersion}}{{.ControllerVersion}}{{else}}—{{end}} {{timeAgoPtr .LastSeenAt}}
{{else}}

No guests reported on this host.

{{end}}

Storage Targets

{{if .StorageTargets}} {{range .StorageTargets}} {{end}}
Name Role Type State Fill Thin Pool SMART Temp Wear
{{.Name}} {{if .Role}}{{.Role}}{{else}}—{{end}} {{if .Type}}{{.Type}}{{else}}—{{end}} {{if .State}}{{.State}}{{else}}—{{end}} {{formatFloat .FillPct}}% {{if .HasThin}}{{formatFloat .ThinDataPct}}%{{else}}—{{end}} {{if .SmartHealth}}{{if eq .SmartHealth "PASSED"}}{{.SmartHealth}}{{else if eq .SmartHealth "FAILED"}}{{.SmartHealth}}{{else}}{{.SmartHealth}}{{end}}{{else}}—{{end}} {{if .TempC}}{{.TempC}}°C{{else}}—{{end}} {{if .WearPct}}{{.WearPct}}%{{else}}—{{end}}
{{else}}

No storage targets reported.

{{end}}

Capabilities

{{if .NeedsDRMigration}}

This box predates the uniform DR plumbing (host-install < v1.15.0) — the pbsdr wrapper is missing. Migration one-liner set (as root on the host): fetch configs/felhom-pbs-apply from the agent repo to /usr/local/sbin/felhom-pbs-apply (0755), apt-get install -y age, set wg_tunnel.enabled: true in agent.json, restart felhom-agent — see documentation/runbooks/day0-install.md (retrofit).

{{end}} {{if .Capabilities}} {{range .Capabilities}} {{end}}
Capability Status Feature / reason
{{.Name}}{{if .Critical}} critical{{end}} {{.Status}} {{.Feature}}{{if .Reason}} — {{.Reason}}{{end}}
{{else}}

No capability report yet (waiting for the first host report).

{{end}}

Diagnostics — Log Bundles

Pull-based: the box ships its debug ring on its own next cycle — controller ≤ one report interval (~15 min), agent ≈ one heartbeat. The pull is recorded in the box's own log (customer-visible). Bundles expire after 72 h.

{{if .LogBundles}} {{range .LogBundles}} {{end}}
Component State Collected Received Size
{{.Component}} {{if eq .State "pending"}}pending {{else if eq .State "blocked"}}blocked: possible secret {{else}}available{{end}} {{if .CollectedAt.IsZero}}—{{else}}{{timeAgo .CollectedAt}}{{end}} {{if .ReceivedAt.IsZero}}—{{else}}{{timeAgo .ReceivedAt}}{{end}} {{if .SizeBytes}}{{.SizeBytes}} B{{else}}—{{end}} {{if eq .State "available"}} View Download {{end}}
{{else}}

No log bundles. Use the request buttons above — the box delivers on its next cycle.

{{end}}

Operator Actions

{{if .CustomerID}}

The box acts on its next report reply (usually seconds, at most one report interval) and answers on the report after that. Each press runs once. Recorded in the box's own log and in Events. Unanswered after a day: expired.

{{if .OperatorActions}} {{range .OperatorActions}} {{end}}
# Action Requested By Outcome
{{.ID}} {{.Action}}{{if .Arg}} {{.Arg}}{{end}} {{timeAgo .RequestedAt}} {{.RequestedBy}} {{if not .DoneAt}}pending {{else if eq .Outcome "done"}}done {{else if eq .Outcome "failed"}}failed {{else}}{{.Outcome}}{{end}} {{if .Message}}{{.Message}}{{end}}
{{else}}

No operator actions for this box yet.

{{end}} {{else}}

This host has no customer, so there is no controller to act.

{{end}}

Network

WireGuard {{if .Network.WGAssignedIP}}{{.Network.WGAssignedIP}} {{if .Network.WGConfirmed}}confirmed {{else if .Network.AgentTooOld}}allocated {{else}}not confirmed by the box{{end}} {{else}}no peer allocated{{end}}
{{if .Network.AgentTooOld}}

This host's agent ({{.AgentVersion}}) does not report its addresses — they are unknown, not absent. Needs agent 0.119.0 or newer.

{{else if .Network.Addresses}} {{range .Network.Addresses}} {{end}}
InterfaceAddress
{{.Iface}} {{.CIDR}}

Every routable address the box holds, as the kernel sees it. Loopback and link-local are excluded — including the 169.254.253.1 local-API island, which is identical on every box. The PVE web console is at https://<the LAN address>:8006.

{{else if .HasReport}}

The agent reports no routable address on this host.

{{else}}

Waiting for the first host report.

{{end}}

Guest network {{if .GuestNet.AgentTooOld}}unknown {{else if not .GuestNet.Reported}}unknown {{else if .GuestNet.Degraded}}needs attention {{else if .GuestNet.UnknownCount}}partly unknown {{else if .GuestNet.Guests}}healthy {{end}}

{{if .GuestNet.AgentTooOld}}

This host's agent ({{.AgentVersion}}) does not run the guest-network watchdog — its guests' networking is unknown, not healthy. Needs agent 0.92.0 or newer.

{{else if not .GuestNet.Reported}}

This host reported no guest-network state, so it is unknown — not healthy. The watchdog is default-on; a silent capable agent means it was switched off (guest_net.disable), or this report predates it on this box.

{{else if .GuestNet.Guests}} {{range .GuestNet.Guests}} {{end}}
GuestStateAddressRoutedhclientRepairs (1h)
{{.VMID}} {{if .Unknown}}unknown {{else if eq .State "healthy"}}healthy {{else if eq .State "static_fault"}}static fault {{else}}unhealthy{{end}} {{if .HealFailed}}repair failed{{end}} {{if .Damped}}damped{{end}} {{if .IP}}{{.IP}} ({{.Mode}}){{else}}—{{end}} {{if .HasRoute}}yes{{else}}no{{end}} {{if .DHClientAlive}}yes{{else if eq .Mode "static"}}n/a{{else}}no{{end}} {{if .Repairing}}{{.RepairCount}} {{else}}0{{end}}
{{if .GuestNet.RepairingCount}}

{{.GuestNet.RepairingCount}} guest(s) had their networking repaired in the last hour. A guest that keeps being repaired reads healthy between repairs and is not. A killed dhclient once took a tunnel down for 1 h 15 m with nobody told; this row is that signal.

{{end}}

Last swept {{.GuestNet.CheckedAt}}. One row per owned running guest the watchdog has probed.

{{else}}

The watchdog ran and has no guest to report (swept {{.GuestNet.CheckedAt}}). This is distinguishable from silence: the stanza arrived, its guest list is empty.

{{end}}

DR / Backup

DR Recipe {{if .DRPresent}}present{{else}}none{{end}}
Key Escrow {{if .EscrowPresent}}present{{else}}none{{end}}{{if gt .SupersededEscrowCount 0}} · {{.SupersededEscrowCount}} superseded escrow blob(s) retained{{end}}

Console access

{{if .RecoveryVaulted}}
User {{.RecoveryUsername}}
Password set {{timeAgo .RecoverySetAt}}
••••••••••••••••

Break-glass credential for the PVE web console at https://<host-ip>:8006 (realm: Linux PAM standard authentication). Copy puts it straight on the clipboard without showing it; Reveal displays it for 60 s. Either one is recorded on the customer's event timeline. Last vaulted value — if root@pam was changed on the box without re-vaulting, this is stale.

{{else}}

not vaulted

No console credential is vaulted for this host. Expected for a byo host — the owner manages root@pam. Otherwise the installer's step 4b did not run; re-run felhom-host-install.sh, or set and vault one per the break-glass runbook §5.

{{end}}
{{if .RecoveryVaulted}} {{end}} {{if .Deletable}}

Danger zone

Removing this host deletes its reports, guests, log bundles and WireGuard peer, and permanently invalidates its API key — a still-running agent would receive 401s. Re-enrollment requires the Day-0 passphrase flow.

{{end}} {{end}}