f181efd6a7
gates / gates (push) Successful in 18s
Operator ruling 2026-09-13. Above the vouched golden, a floor saved with a declared MinAgent is served under the same agent comparison; an undeclared one is still held beyond the golden. The declaration is stored beside each floor as FLOOR=MINAGENT so it never carries to a later floor. Both floor forms require min_agent above the golden (flash floor_needs_min_agent, nothing stored). The Hosts page and the API log name the source. Vouch path and R-120 gate untouched. Scenarios A-E tested; red-proofs A and C in documentation/audits/rulings-r472-r475-2026-09-13/.
128 lines
7.7 KiB
HTML
128 lines
7.7 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Hosts — Felhom Hub</title>
|
|
<link rel="stylesheet" href="/style.css?v={{hubVersion}}">
|
|
</head>
|
|
<body>
|
|
{{template "icon_sprite"}}
|
|
{{template "inline_confirm_js"}}
|
|
<div class="container">
|
|
<header>
|
|
<h1>Felhom <span>Hub</span></h1>
|
|
<nav class="nav-links">
|
|
<a href="/" class="nav-link">Dashboard</a>
|
|
<a href="/configs" class="nav-link">Customers</a>
|
|
<a href="/apps" class="nav-link">Apps</a>
|
|
<a href="/hosts" class="nav-link active">Hosts</a>
|
|
<a href="/offsite" class="nav-link">Offsite</a>
|
|
<a href="/configuration" class="nav-link">Configuration</a>
|
|
</nav>
|
|
</header>
|
|
|
|
<h2 style="margin-bottom: 1rem;">Hosts</h2>
|
|
|
|
{{if .Flash}}
|
|
<div class="flash flash-success" style="margin-bottom: 1rem;">
|
|
{{if eq .Flash "appliance_bound"}}Appliance bound — its credentials are delivered on its next poll (within ~30s); it then completes day-0 install.
|
|
{{else if eq .Flash "appliance_discarded"}}Appliance discarded — its token is invalidated; further polls are ignored.
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .Unclaimed}}
|
|
<section class="card" style="margin-bottom: 1.5rem; border-color: var(--warn);">
|
|
<h2 style="margin-top: 0;">Unclaimed appliances <span class="text-muted" style="font-size: 0.8em; font-weight: normal;">(booted from the generic ISO, awaiting a bind)</span></h2>
|
|
<p class="text-muted" style="margin-top: 0;">A box that installed from the universal secret-free ISO and registered itself. <strong>Bind</strong> it to a customer to deliver its retrieval passphrase once; <strong>discard</strong> to ignore it.</p>
|
|
<div style="overflow-x: auto;">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr><th>Appliance</th><th>Pairing code</th><th>MACs</th><th>Hardware</th><th>SSH host keys</th><th>Seen</th><th>Bind to customer</th><th></th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .Unclaimed}}
|
|
<tr>
|
|
<td><code style="font-size: 0.8em;">{{.UUID}}</code>
|
|
{{if .Stale}}<br><span class="status-badge status-warn" title="No poll in over 7 days">stale</span>{{end}}
|
|
{{if .Bound}}<br><span class="status-badge status-ok" title="Bound — awaiting the box's next poll">bound → {{.BoundCustomer}}</span>{{end}}
|
|
</td>
|
|
<td style="font-family: var(--font-mono); letter-spacing: 0.05em;">{{if .PairingCode}}<strong>{{.PairingCode}}</strong>{{else}}<span class="text-muted">—</span>{{end}}</td>
|
|
<td style="font-size: 0.78em; font-family: var(--font-mono)">{{range .MACs}}{{.}}<br>{{end}}</td>
|
|
<td style="font-size: 0.8em;">{{if .Product}}{{.Product}}<br>{{end}}{{if .CPU}}<span class="text-muted">{{.CPU}}</span><br>{{end}}{{if .MemGB}}<span class="text-muted">{{.MemGB}}</span>{{end}}</td>
|
|
<td style="font-size: 0.72em; font-family: var(--font-mono)">{{range .SSHFingerprints}}{{.}}<br>{{end}}</td>
|
|
<td style="font-size: 0.78em;">{{if .FirstSeen}}first {{timeAgoPtr .FirstSeen}}<br>{{end}}{{if .LastSeen}}last {{timeAgoPtr .LastSeen}}{{end}}</td>
|
|
<td>
|
|
<form method="POST" action="/appliances/{{.ID}}/bind" style="display: flex; flex-direction: column; gap: 0.3rem;">
|
|
<input type="hidden" name="_csrf" value="{{$.CSRFToken}}">
|
|
<select name="customer_id" required style="max-width: 16em;">
|
|
<option value="">— pick a customer —</option>
|
|
{{range $.CustomerPicker}}<option value="{{.CustomerID}}">{{.CustomerName}} ({{.HostCount}} host{{if ne .HostCount 1}}s{{end}})</option>{{end}}
|
|
</select>
|
|
<button type="submit" class="btn btn-sm" style="border-color: var(--warn); color: var(--warn);">Bind & deliver</button>
|
|
</form>
|
|
</td>
|
|
<td>
|
|
<form method="POST" action="/appliances/{{.ID}}/discard">
|
|
<input type="hidden" name="_csrf" value="{{$.CSRFToken}}">
|
|
<button type="submit" class="btn btn-sm btn-outline" data-confirm="Discard this appliance? Its token is invalidated and further polls are ignored.">Discard</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
{{end}}
|
|
|
|
{{if .Hosts}}
|
|
<section class="card" style="padding: 0; overflow: hidden;">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Host</th>
|
|
<th>Customer</th>
|
|
<th>Agent</th>
|
|
<th>Status</th>
|
|
<th>Guests</th>
|
|
<th>CPU</th>
|
|
<th>Mem</th>
|
|
<th>Disk</th>
|
|
<th>Cloudflared</th>
|
|
<th>Worst Storage</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .Hosts}}
|
|
<tr onclick="window.location='/hosts/{{.HostID}}'" style="cursor: pointer;">
|
|
<td><a href="/hosts/{{.HostID}}">{{.HostID}}</a></td>
|
|
<td>{{if .CustomerName}}{{.CustomerName}}{{else}}{{.CustomerID}}{{end}}</td>
|
|
<td>{{if .AgentVersion}}<code>{{.AgentVersion}}</code>{{else}}—{{end}}{{if .FloorHeld}} <span class="status-badge status-warn" title="{{.HeldReason}}">floor held</span>{{else if eq .FloorSource "declared"}} <span class="status-badge" title="served above the vouched golden: the agent requirement was declared with the floor (R-472)">floor: declared MinAgent</span>{{end}}</td>
|
|
<td><span class="status-badge {{.StatusClass}}">{{.StatusLabel}}</span></td>
|
|
<td>{{if .HasReport}}{{.GuestRunning}}/{{.GuestTotal}}{{else}}—{{end}}</td>
|
|
<td>{{if .HasReport}}{{formatFloat .Vitals.CPUPercent}}%{{else}}—{{end}}</td>
|
|
<td>{{if .HasReport}}{{formatFloat .Vitals.MemoryPercent}}%{{else}}—{{end}}</td>
|
|
<td>{{if .HasReport}}{{formatFloat .Vitals.DiskPercent}}%{{else}}—{{end}}</td>
|
|
<td>{{if .Vitals.CloudflaredStatus}}{{.Vitals.CloudflaredStatus}}{{else}}—{{end}}</td>
|
|
<td>{{if .HasStorage}}{{formatFloat .WorstFillPct}}% <span class="text-muted">{{.WorstFillName}}</span>{{else}}—{{end}}</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
{{else}}
|
|
<div class="empty-state">
|
|
<p>No hosts enrolled yet.</p>
|
|
<p class="hint">A host appears here after its agent enrolls and sends its first host-report.</p>
|
|
</div>
|
|
{{end}}
|
|
|
|
<footer style="margin-top: 2rem; color: var(--text-muted); font-size: 0.8rem; text-align: center;">
|
|
Felhom Hub <span style="font-family: var(--font-mono)">{{hubVersion}}</span>
|
|
</footer>
|
|
</div>
|
|
</body>
|
|
</html>
|