Files
felhom.eu/hub/internal/web/templates/configuration.html
T
admin 13203c2452 hub: S2 /offsite registry page (read-only) + nav + WGPeer.CreatedAt
Endpoint card + peers table (truncated pubkeys with full-value title attr,
bound peers link to /hosts/<id>); Offsite nav link in all 9 page templates;
render tests for endpoint/peers, empty, and not-configured states.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-04 00:48:03 +02:00

155 lines
9.2 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 <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">Hosts</a>
<a href="/offsite" class="nav-link">Offsite</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 &amp; 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>