hub v0.85.0 — Network card: a host's addresses are visible at last

Pairs with agent v0.119.0 and is useless without it.

A managed box's LAN IP was not shown anywhere in the hub, because nothing
reported it — the host report carried no address of any kind. The only IP
reachable from the UI at all was the WireGuard one, on /offsite's peer table
keyed by pubkey, so an operator could go peer->host and never host->peer, which
is the direction anyone actually asks in.

The host page grows a Network card: every routable address the box holds, one row
per (interface, address), plus a WireGuard row. On demo-felhom that is vmbr0
192.168.0.162/24 and tailscale0 100.70.170.35/32 — with the PVE web console at
https://<the LAN address>:8006, the thing the operator wanted and could not get.

WireGuard is rendered as TWO facts, deliberately. WGAssignedIP is the hub's own
allocation (wg_peers, authoritative desired state); WGConfirmed is whether the box
reports actually holding it. Showing the allocation alone would make a peer that
was never applied look healthy — the same shape as reading a timestamp that
records an attempt as if it recorded a result.

The split is keyed on the ALLOCATION, not the interface name: wg-felhom is the
agent's current unit name, and a UI keyed on that string would silently
mis-render the day it changes.

An old agent renders UNKNOWN, never "no addresses". Below agent 0.119.0 the field
is absent from the wire, and an absent signal is not a negative result — the page
says so and names the version needed. Rendering an empty list there would have
stated something false about the host.

No new store table and no new ingest path: the report is already stored opaquely
and GetWGPeerForHost already existed with no UI consumer. This is parse + render.

The report fixture in the tests is the REAL wire — the addresses block copied out
of `felhom-agent --selftest=hub` on demo-felhom running 0.119.0.

Tests 559 -> 566; four red-proofs (inert view-model, unconditional confirmation,
the old-agent branch, and the drift case) each run, observed failing, reverted.
This commit is contained in:
2026-07-31 08:49:44 +02:00
parent b4edc087fa
commit e07d90f0f4
6 changed files with 438 additions and 0 deletions
@@ -264,6 +264,59 @@
{{end}}
</section>
<!-- Network (v0.85.0): where this box actually is. Addresses come from the agent's
addresses[] (agent >= 0.119.0); the WireGuard row pairs the HUB's allocation with
whether the box confirms holding it, because an allocation alone cannot tell a live
tunnel from a peer that was never applied. -->
<section class="card">
<h2>Network</h2>
<div class="info-grid">
<div class="info-item">
<span class="label">WireGuard</span>
<span class="value">
{{if .Network.WGAssignedIP}}<code>{{.Network.WGAssignedIP}}</code>
{{if .Network.WGConfirmed}}<span class="badge badge-ok" title="The box reports holding this address">confirmed</span>
{{else if .Network.AgentTooOld}}<span class="badge badge-neutral" title="This agent does not report its addresses">allocated</span>
{{else}}<span class="badge badge-warn" title="The hub allocated this peer but the box does not report holding the address">not confirmed by the box</span>{{end}}
{{else}}<span class="text-muted">no peer allocated</span>{{end}}
</span>
</div>
</div>
{{if .Network.AgentTooOld}}
<p class="hint" style="color: var(--text-muted); font-size: 0.85rem; margin-top: 0.75rem;">
This host's agent (<code>{{.AgentVersion}}</code>) does not report its addresses &mdash; they are
<strong>unknown</strong>, not absent. Needs agent <code>0.119.0</code> or newer.
</p>
{{else if .Network.Addresses}}
<table class="data-table" style="margin-top: 0.75rem;">
<thead>
<tr><th>Interface</th><th>Address</th></tr>
</thead>
<tbody>
{{range .Network.Addresses}}
<tr>
<td><code>{{.Iface}}</code></td>
<td><code>{{.CIDR}}</code></td>
</tr>
{{end}}
</tbody>
</table>
<p class="hint" style="color: var(--text-muted); font-size: 0.85rem; margin-top: 0.5rem;">
Every routable address the box holds, as the kernel sees it. Loopback and link-local are
excluded &mdash; including the <code>169.254.253.1</code> local-API island, which is identical on
every box. The PVE web console is at <code>https://&lt;the LAN address&gt;:8006</code>.
</p>
{{else if .HasReport}}
<p class="hint" style="color: var(--text-muted); font-size: 0.85rem; margin-top: 0.75rem;">
The agent reports no routable address on this host.
</p>
{{else}}
<p class="hint" style="color: var(--text-muted); font-size: 0.85rem; margin-top: 0.75rem;">
Waiting for the first host report.
</p>
{{end}}
</section>
<!-- DR / Backup -->
<section class="card">
<h2>DR / Backup</h2>