39ef64e128
Hub (v0.16.0):
- store: ArtifactManifest{agent,golden version+sha256} in hub_settings; Get/SetArtifactManifest.
- handler: GET /api/v1/artifacts/{id} (passphrase auth, mirrors config-retrieve). Unset => 200 empty.
- web: operator UI "Day-0 artifacts" card (POST /configs/artifacts), semver + 64-hex validation.
- artifact_test.go: returned-verbatim / unset-empty / 401 / 404 / store round-trip.
host-install (v1.1.0):
- new step 5/8 agent-install: manifest + git token (config-retrieve) -> fetch binary from Gitea ->
verify sha256 vs hub manifest (abort on mismatch) -> install non-root felhom-agent user + binary +
sudoers (visudo -cf) + canonical unit. Idempotent.
- new step 7/8 golden: local fallback else fetch+verify+import from Gitea (--force-gitea-golden).
- agent now runs non-root (privileged.mode sudo), config chowned to the service user.
- README prerequisites trimmed to: install PVE + create customer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
137 lines
7.8 KiB
HTML
137 lines
7.8 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 — Customers</title>
|
|
<link rel="stylesheet" href="/style.css">
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<header>
|
|
<h1>Felhom Hub</h1>
|
|
<nav class="nav-links">
|
|
<a href="/" class="nav-link">Dashboard</a>
|
|
<a href="/configs" class="nav-link active">Customers</a>
|
|
<a href="/apps" class="nav-link">Apps</a>
|
|
<a href="/configuration" class="nav-link">Configuration</a>
|
|
</nav>
|
|
</header>
|
|
|
|
{{if .Flash}}
|
|
<div class="flash flash-success">
|
|
{{if eq .Flash "deleted"}}Customer configuration deleted.
|
|
{{else if eq .Flash "floor_set"}}Controller-version floor saved.
|
|
{{else if eq .Flash "floor_invalid"}}Invalid version — use X.Y.Z (or blank to clear).
|
|
{{else if eq .Flash "artifacts_set"}}Artifact manifest saved.
|
|
{{else if eq .Flash "artifact_ver_invalid"}}Invalid artifact version — use X.Y.Z (or blank to clear).
|
|
{{else if eq .Flash "artifact_sha_invalid"}}Invalid sha256 — use 64 hex chars (or blank to clear).
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
|
|
<!-- Phase 2 managed updates: global controller-version floor -->
|
|
<div class="card" style="margin-bottom: 1rem; padding: 1rem; border: 1px solid #334155; border-radius: 8px;">
|
|
<h2 style="margin: 0 0 0.5rem;">Managed updates — global floor</h2>
|
|
<p style="font-size: 0.85em; color: #94a3b8; margin: 0 0 0.5rem;">
|
|
The minimum controller version every box auto-updates to (unless a per-customer override is set).
|
|
Boxes below the floor update on their next report — no customer action. Blank = no global floor.
|
|
</p>
|
|
<form method="POST" action="/configs/global-floor" style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
|
|
{{.CSRFField}}
|
|
<input type="text" name="min_controller_version" value="{{.GlobalFloor}}" placeholder="e.g. 0.86.0 (blank = none)" style="padding: 0.3em 0.5em; width: 14em;">
|
|
<button class="btn btn-sm" type="submit">Save global floor</button>
|
|
<span style="font-size: 0.85em; color: #cbd5e1;">Current: {{if .GlobalFloor}}<code>v{{.GlobalFloor}}</code>{{else}}<span class="text-muted">unset</span>{{end}}</span>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- BUNDLE slice: Day-0 artifact manifest (agent binary + golden archive). The hub is the
|
|
checksum TRUST ROOT — the host-bootstrap script verifies Gitea-fetched artifacts against
|
|
these sha256s before installing them. Record the version + sha256 printed by
|
|
publish-agent.sh / build-golden.sh. -->
|
|
<div class="card" style="margin-bottom: 1rem; padding: 1rem; border: 1px solid #334155; border-radius: 8px;">
|
|
<h2 style="margin: 0 0 0.5rem;">Day-0 artifacts — agent & golden</h2>
|
|
<p style="font-size: 0.85em; color: #94a3b8; margin: 0 0 0.5rem;">
|
|
The current agent binary + golden archive the host-bootstrap script fetches from Gitea and
|
|
verifies (sha256) before installing. The hub vouches for these checksums (a different trust
|
|
root than Gitea). Paste the version + sha256 printed by <code>publish-agent.sh</code> /
|
|
<code>build-golden.sh</code>. Blank a field to clear it.
|
|
</p>
|
|
<form method="POST" action="/configs/artifacts" style="display: grid; grid-template-columns: auto 8em 1fr; gap: 0.5rem; align-items: center; max-width: 56em;">
|
|
{{.CSRFField}}
|
|
<label style="font-size: 0.9em; color: #cbd5e1;">Agent</label>
|
|
<input type="text" name="agent_version" value="{{.Artifacts.AgentVersion}}" placeholder="0.43.0" style="padding: 0.3em 0.5em;">
|
|
<input type="text" name="agent_sha256" value="{{.Artifacts.AgentSHA256}}" placeholder="64-hex sha256 (blank = none)" style="padding: 0.3em 0.5em; font-family: monospace;">
|
|
<label style="font-size: 0.9em; color: #cbd5e1;">Golden</label>
|
|
<input type="text" name="golden_version" value="{{.Artifacts.GoldenVersion}}" placeholder="0.85.1" style="padding: 0.3em 0.5em;">
|
|
<input type="text" name="golden_sha256" value="{{.Artifacts.GoldenSHA256}}" placeholder="64-hex sha256 (blank = none)" style="padding: 0.3em 0.5em; font-family: monospace;">
|
|
<span></span><span></span>
|
|
<button class="btn btn-sm" type="submit" style="justify-self: start;">Save artifact manifest</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
|
<h2 style="margin: 0;">Customers</h2>
|
|
<a href="/configs/new" class="btn">+ Add Customer</a>
|
|
</div>
|
|
|
|
{{if not .Customers}}
|
|
<div class="empty-state">
|
|
<p>No customers yet.</p>
|
|
<p class="hint">Add a customer configuration or wait for a controller to report in.</p>
|
|
</div>
|
|
{{else}}
|
|
<table class="dashboard-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Customer ID</th>
|
|
<th>Name</th>
|
|
<th>Domain</th>
|
|
<th>Status</th>
|
|
<th>Version</th>
|
|
<th>Floor</th>
|
|
<th>Config</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .Customers}}
|
|
<tr class="{{if .IsBlocked}}row-blocked{{end}}" onclick="window.location='/customers/{{.CustomerID}}'">
|
|
<td><code>{{.CustomerID}}</code></td>
|
|
<td>{{if .CustomerName}}{{.CustomerName}}{{else}}<span class="text-muted">—</span>{{end}}</td>
|
|
<td>{{if .Domain}}{{.Domain}}{{else}}<span class="text-muted">—</span>{{end}}</td>
|
|
<td>
|
|
{{if .IsBlocked}}
|
|
<span class="config-badge config-badge-blocked">BLOCKED</span>
|
|
{{else if .OverallStatus}}
|
|
<span class="status-badge status-badge-{{.OverallStatus}}">
|
|
{{if eq .OverallStatus "ok"}}OK{{else if eq .OverallStatus "warn"}}WARN{{else if eq .OverallStatus "down"}}DOWN{{else if eq .OverallStatus "disabled"}}PAUSED{{else if eq .OverallStatus "pending"}}PENDING{{else}}{{.OverallStatus}}{{end}}
|
|
</span>
|
|
{{else}}
|
|
<span class="text-muted">—</span>
|
|
{{end}}
|
|
</td>
|
|
<td>{{if .ControllerVersion}}<code>{{.ControllerVersion}}</code>{{else}}<span class="text-muted">—</span>{{end}}</td>
|
|
<td>
|
|
{{if .EffectiveFloor}}<code>v{{.EffectiveFloor}}</code>{{if .FloorOverride}}<span class="text-muted" style="font-size:0.8em;"> (override)</span>{{end}}{{if .BelowFloor}}<span style="color:#f59e0b;" title="below floor — will auto-update"> ●</span>{{end}}
|
|
{{else}}<span class="text-muted">—</span>{{end}}
|
|
</td>
|
|
<td>
|
|
{{if .HasConfig}}
|
|
<span class="config-badge config-badge-managed">MANAGED</span>
|
|
{{else}}
|
|
<span class="config-badge config-badge-manual">MANUAL</span>
|
|
{{end}}
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
{{end}}
|
|
|
|
<footer>
|
|
<p>Felhom Hub {{hubVersion}} — Customer Management</p>
|
|
</footer>
|
|
</div>
|
|
</body>
|
|
</html>
|