bc8d54df6a
- static/fonts/: the 4 vendored woff2 (byte-copied from
felhom-controller), embedded (embed.go) and served at /static/fonts/
(font/woff2, immutable) mirroring the chart.min.js pattern. No CDN
before, none now.
- templates/icons.html: 12-symbol Lucide sprite partial (icon_sprite),
included at the top of <body> on all 9 pages ({{template}} — the hub
has no shared layout; per-page include is the minimal shared block).
- statusColor now returns v2 semantic tokens (nominal/warn/crit/
neutral) consumed as class suffixes: ok->nominal, warn+stale->warn,
down+fail->crit, pending+disabled->neutral (a not-yet-provisioned or
deliberately paused customer is a normal fleet state), blocked->warn
(intentional operator cut-off, attention-worthy not an outage),
unknown->neutral. The inline style="color: {{statusColor}}" pattern
is dead: dashboard + customer_unified render a class-based
.status-dot-<token>; statusIcon (constant "●") retired from funcmap
and templates.
- Tests (new; the hub web package had no funcmap/template tests):
TestStatusColorTruthTable over the full enumerated status set —
red-proven vs the old implementation (ok returned "#4ade80") — and
TestTemplatesParseWithFuncmap.
154 lines
9.1 KiB
HTML
154 lines
9.1 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Configuration — Felhom Hub</title>
|
|
<link rel="stylesheet" href="/style.css">
|
|
</head>
|
|
<body>
|
|
{{template "icon_sprite"}}
|
|
<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">Customers</a>
|
|
<a href="/apps" class="nav-link">Apps</a>
|
|
<a href="/hosts" class="nav-link">Hosts</a>
|
|
<a href="/configuration" class="nav-link active">Configuration</a>
|
|
</nav>
|
|
</header>
|
|
|
|
<h2 style="margin-bottom: 1rem;">Configuration</h2>
|
|
|
|
{{if eq .Flash "assets_refreshed"}}
|
|
<div class="flash flash-success">Assets refreshed successfully from image seed.</div>
|
|
{{end}}
|
|
{{if eq .Flash "assets_error"}}
|
|
<div class="flash flash-error">Asset refresh failed. Check server logs for details.</div>
|
|
{{end}}
|
|
{{if eq .Flash "assets_not_configured"}}
|
|
<div class="flash flash-error">Asset manager is not configured.</div>
|
|
{{end}}
|
|
{{if eq .Flash "floor_set"}}
|
|
<div class="flash flash-success">Controller-version floor saved.</div>
|
|
{{end}}
|
|
{{if eq .Flash "floor_invalid"}}
|
|
<div class="flash flash-error">Invalid version — use X.Y.Z (or blank to clear).</div>
|
|
{{end}}
|
|
{{if eq .Flash "artifacts_set"}}
|
|
<div class="flash flash-success">Artifact manifest saved.</div>
|
|
{{end}}
|
|
{{if eq .Flash "artifact_ver_invalid"}}
|
|
<div class="flash flash-error">Invalid artifact version — use X.Y.Z (or blank to clear).</div>
|
|
{{end}}
|
|
{{if eq .Flash "artifact_sha_invalid"}}
|
|
<div class="flash flash-error">Couldn't set the checksum — the Gitea sha lookup failed (version missing / Gitea unreachable) or the manually-entered sha is invalid. Manifest unchanged.</div>
|
|
{{end}}
|
|
|
|
<!-- Phase 2 managed updates: global controller-version floor (moved from the Customers page —
|
|
it is a global setting). -->
|
|
<section class="card">
|
|
<h3 style="margin-top: 0;">Managed updates — global floor</h3>
|
|
<p class="text-muted" style="margin: 0 0 0.75rem; font-size: 0.85em;">
|
|
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="/configuration/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>
|
|
</section>
|
|
|
|
<!-- 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. -->
|
|
<section class="card">
|
|
<h3 style="margin-top: 0;">Day-0 artifacts — agent & golden</h3>
|
|
<p class="text-muted" style="margin: 0 0 0.75rem; font-size: 0.85em;">
|
|
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). Pick a version — the sha256 is read from Gitea automatically (no manual
|
|
copy). Choose <em>— none —</em> to clear an artifact.
|
|
</p>
|
|
<form method="POST" action="/configuration/artifacts" style="display: grid; grid-template-columns: auto 12em 1fr; gap: 0.5rem; align-items: center; max-width: 56em;">
|
|
{{.CSRFField}}
|
|
<label style="font-size: 0.9em; color: #cbd5e1;">Agent</label>
|
|
{{if .AgentChoices}}
|
|
<select name="agent_version" id="agent_version" onchange="syncArtifactSha('agent')" style="padding: 0.3em 0.5em;">
|
|
<option value="" data-sha="">— none —</option>
|
|
{{range .AgentChoices}}
|
|
<option value="{{.Version}}" data-sha="{{.SHA256}}" {{if eq .Version $.Artifacts.AgentVersion}}selected{{end}}>{{.Version}}</option>
|
|
{{end}}
|
|
</select>
|
|
{{else}}
|
|
<input type="text" name="agent_version" value="{{.Artifacts.AgentVersion}}" placeholder="0.52.0" style="padding: 0.3em 0.5em;">
|
|
{{end}}
|
|
<input type="text" name="agent_sha256" id="agent_sha256" value="{{.Artifacts.AgentSHA256}}" {{if .AgentChoices}}readonly{{end}} placeholder="64-hex sha256 (blank = none)" style="padding: 0.3em 0.5em; font-family: monospace; {{if .AgentChoices}}opacity: 0.7;{{end}}">
|
|
<label style="font-size: 0.9em; color: #cbd5e1;">Golden</label>
|
|
{{if .GoldenChoices}}
|
|
<select name="golden_version" id="golden_version" onchange="syncArtifactSha('golden')" style="padding: 0.3em 0.5em;">
|
|
<option value="" data-sha="">— none —</option>
|
|
{{range .GoldenChoices}}
|
|
<option value="{{.Version}}" data-sha="{{.SHA256}}" {{if eq .Version $.Artifacts.GoldenVersion}}selected{{end}}>{{.Version}}</option>
|
|
{{end}}
|
|
</select>
|
|
{{else}}
|
|
<input type="text" name="golden_version" value="{{.Artifacts.GoldenVersion}}" placeholder="0.85.1" style="padding: 0.3em 0.5em;">
|
|
{{end}}
|
|
<input type="text" name="golden_sha256" id="golden_sha256" value="{{.Artifacts.GoldenSHA256}}" {{if .GoldenChoices}}readonly{{end}} placeholder="64-hex sha256 (blank = none)" style="padding: 0.3em 0.5em; font-family: monospace; {{if .GoldenChoices}}opacity: 0.7;{{end}}">
|
|
<span></span><span></span>
|
|
<button class="btn btn-sm" type="submit" style="justify-self: start;">Save artifact manifest</button>
|
|
</form>
|
|
<script>
|
|
// When a version is picked, mirror that option's Gitea-resolved sha256 into the read-only
|
|
// display field. The hub re-derives the sha authoritatively on save regardless of this value.
|
|
function syncArtifactSha(kind) {
|
|
var sel = document.getElementById(kind + '_version');
|
|
var sha = document.getElementById(kind + '_sha256');
|
|
if (!sel || !sha) return;
|
|
var opt = sel.options[sel.selectedIndex];
|
|
sha.value = (opt && opt.getAttribute('data-sha')) || '';
|
|
}
|
|
</script>
|
|
</section>
|
|
|
|
<!-- Assets section -->
|
|
<section class="card">
|
|
<h3 style="margin-top: 0;">Assets</h3>
|
|
<p class="text-muted" style="margin-bottom: 1rem;">
|
|
App logos and screenshots served to controllers. Assets are seeded from the Docker image
|
|
and synced to controllers daily via the asset manifest API.
|
|
</p>
|
|
<div class="info-grid">
|
|
<div class="info-item">
|
|
<span class="label">Files in manifest</span>
|
|
<span class="value">{{.AssetCount}}</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<span class="label">Manifest generated</span>
|
|
<span class="value" style="font-family: var(--font-mono); font-size: 0.85em;">{{if .AssetLastSync}}{{.AssetLastSync}}{{else}}—{{end}}</span>
|
|
</div>
|
|
</div>
|
|
<form method="POST" action="/configuration" style="margin-top: 1rem;">
|
|
<input type="hidden" name="_csrf" value="{{.CSRFToken}}">
|
|
<input type="hidden" name="action" value="refresh_assets">
|
|
<button type="submit" class="btn" onclick="this.disabled=true;this.textContent='Refreshing…';this.form.submit();">Refresh Assets from Image</button>
|
|
</form>
|
|
<p class="text-muted" style="margin-top: 0.75rem; font-size: 0.8rem;">
|
|
Re-reads the baked-in asset seed directory and updates changed files.
|
|
Controllers will pick up changes on their next daily sync or manual trigger.
|
|
</p>
|
|
</section>
|
|
|
|
<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>
|