Files
felhom.eu/hub/internal/web/templates/dashboard.html
T
admin 7f11cfb36c hub v0.65.0 — PBS DR storage visibility (ep0 usage op) + Offsite tab split + dual dashboard gauges (R-5)
Makes PBS DR storage visible like the restic pool box (v0.64.0), differentiated. Scoping
correction: restic = subaccounts on the shared Hetzner Storage Box (Hetzner API); PBS DR =
the felhom-offsite PBS datastore on the ep0 endpoint VM (NO Hetzner API). Option A
(Viktor-ruled): a read-only `usage` op on the felhom-tenantsync ep0 forced command (twin of
fingerprint), polled by a new hub checker on the 15-min throttle. READ-ONLY throughout.

Phase-0 (gate PASSED): on ep0 (PBS 4.2.3), df -B1 --output=size,used,avail <datastore path>
yields bytes (39990112256/7627939840/... ~19%), read-only, existing sudo context, no admin token.

- scripts/felhom-tenantsync.sh -> v1.2.0: read-only `usage` short-circuit (df on the datastore
  path), no customer_id, no admin token, NO mutation. + a bash harness proving zero mutation.
- tenantsync.Client.Usage() + BoxUsage; unknown-op -> typed ErrUsageUnsupported (graceful).
- monitor.PBSDRBoxChecker: OffsiteBoxChecker clone over a usageReader seam; 15-min throttle,
  cached PBSBoxSnapshot, escalation-only pbsdr_box_fill on the "pbsdr-box" scope (operator only,
  no SaveEvent), recovery re-arm. Fill only. THREE states: ok / unavailable (ep0 <=v1.1.0,
  neutral no-alert) / degraded (exec failed, keep last).
- config: Alerting.PBSDRBoxFill{Warn,Crit}Percent (80/90); built with the tenantsync client,
  60s sweep, SetPBSDRBox. Hub deploy INDEPENDENT of the ep0 update (graceful degradation).
- web: /offsite splits into Restic + PBS DR hash tabs (endpoint cards under PBS DR); PBS panel;
  the single dashboard tile becomes two gauges (RESTIC pct.ratio, PBS DR pct / n/a).
- runbook offsite-endpoint.md 10: v1.2.0 update steps (no sudoers/authorized_keys change).

Tests: 10 Go + the harness; 3 red-proofs (usage mutation, escalation-only, unavailable-drives-band)
confirmed red then restored. go build/vet/test + bash -n + hub confirm gate all pass.
2026-07-17 21:13:30 +02:00

98 lines
4.9 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Felhom Hub — Customer Overview</title>
<link rel="stylesheet" href="/style.css?v={{hubVersion}}">
<meta http-equiv="refresh" content="60">
</head>
<body>
{{template "icon_sprite"}}
<div class="container">
<header>
<h1>Felhom <span>Hub</span></h1>
<nav class="nav-links">
<a href="/" class="nav-link active">Dashboard</a>
<a href="/configs" class="nav-link">Customers</a>
<a href="/apps" class="nav-link">Apps</a>
<a href="/hosts" class="nav-link">Hosts</a>
<a href="/offsite" class="nav-link">Offsite</a>
<a href="/configuration" class="nav-link">Configuration</a>
</nav>
</header>
{{if or .OffsiteTile .PBSTile}}
<div class="offsite-gauges">
{{with .OffsiteTile}}
<a href="/offsite#tab=restic" class="offsite-tile offsite-tile-{{.Band}}">
<span class="offsite-tile-label">Restic</span>
<span class="offsite-tile-val">{{formatFloat .FillPercent}}% &middot; {{.RatioStr}}</span>
{{if .Degraded}}<span class="offsite-tile-stale">stale</span>{{end}}
</a>
{{end}}
{{with .PBSTile}}
<a href="/offsite#tab=pbsdr" class="offsite-tile offsite-tile-{{.Band}}">
<span class="offsite-tile-label">PBS DR</span>
{{if .Unavailable}}<span class="offsite-tile-val" style="color:var(--text-3);">n/a</span>{{else}}<span class="offsite-tile-val">{{formatFloat .FillPercent}}%</span>{{end}}
{{if .Degraded}}<span class="offsite-tile-stale">stale</span>{{end}}
</a>
{{end}}
</div>
{{end}}
{{if not .Customers}}
<div class="empty-state">
<p>No customer reports received yet.</p>
<p class="hint">Configure <code>hub.enabled: true</code> in customer controller.yaml to start receiving reports.</p>
</div>
{{else}}
<table class="dashboard-table">
<thead>
<tr>
<th>Customer</th>
<th>Status</th>
<th>Events</th>
<th>Last Seen</th>
<th>CPU</th>
<th>Memory</th>
<th>Disk</th>
<th>Containers</th>
<th>Last Backup</th>
<th>Version</th>
</tr>
</thead>
<tbody>
{{range .Customers}}
<tr class="status-{{.OverallStatus}}" onclick="window.location='/customers/{{.CustomerID}}'">
<td class="customer-name">
<span class="status-dot status-dot-{{statusColor .OverallStatus}}"></span>
{{if .CustomerName}}{{.CustomerName}}{{else}}{{.CustomerID}}{{end}}
</td>
<td>
<span class="status-badge status-badge-{{.OverallStatus}}">
{{if eq .OverallStatus "ok"}}OK{{else if eq .OverallStatus "warn"}}WARN{{else if eq .OverallStatus "disabled"}}PAUSED{{else if eq .OverallStatus "pending"}}PENDING{{else}}DOWN{{end}}
</span>
{{if .HostCause}}<span class="status-badge status-badge-warn">{{.HostCause}}</span>{{end}}
</td>
<td>{{if eq .OverallStatus "pending"}}—{{else}}{{if gt (add (add .EventCriticals .EventErrors) .EventWarnings) 0}}{{if gt .EventCriticals 0}}<span class="severity-badge severity-critical">{{.EventCriticals}}</span>{{end}}{{if gt .EventErrors 0}}<span class="severity-badge severity-error">{{.EventErrors}}</span>{{end}}{{if gt .EventWarnings 0}}<span class="severity-badge severity-warning">{{.EventWarnings}}</span>{{end}}{{else}}<span class="text-muted"></span>{{end}}{{end}}</td>
<td>{{if eq .OverallStatus "pending"}}—{{else}}{{timeAgo .ReceivedAt}}{{end}}</td>
<td>{{if eq .OverallStatus "pending"}}—{{else}}{{formatFloat .CPUPercent}}%{{end}}</td>
<td>{{if eq .OverallStatus "pending"}}—{{else}}{{formatFloat .MemoryPercent}}%{{end}}</td>
<td>{{if eq .OverallStatus "pending"}}—{{else}}{{.DiskSummary}}{{end}}</td>
<td>{{if eq .OverallStatus "pending"}}—{{else}}{{.ContainerRunning}}/{{.ContainerTotal}}{{end}}</td>
<td>{{.BackupAge}}</td>
<td>{{if .ControllerVersion}}<code>{{.ControllerVersion}}</code>{{else}}—{{end}}</td>
</tr>
{{end}}
</tbody>
</table>
{{end}}
<footer>
<p>Auto-refreshes every 60 seconds &middot; Felhom Hub {{hubVersion}}</p>
</footer>
</div>
</body>
</html>