golden 0.230.0 baked, vouched, floor raised - demo-felhom moved itself off the R-403 build (R-410 filed)
gates / gates (push) Successful in 17s

GOLDEN_SHA256 9287f7cef5f13166276e8406005e3f28004004510c5184f1c1c7377f7aafad2e,
657 873 700 B. Evidence documentation/tests/golden-0.230.0-2026-08-31/.

WHY IT WAS OWED: the newest golden was 0.229.0, which IS the build R-403 says deletes a
good copy. Every fresh install and the whole fleet floor still carried it.
golden_currency_gate.py had been red across dddcc80, 6e550ae, 130f7a6 and 32a4c35.

THREE INDEPENDENT READERS agreed before anything was vouched: the bake's own print, the
round trip of the PUBLISHED bytes (HTTP 200, 657873700 B, same sha), and the hub's Day-0
dropdown reading Gitea on a different code path. And the delivered artifact names the
controller it will start - ./etc/felhom-controller-image read OUT of the downloaded
archive says felhom-controller:0.230.0, with 19382 entries under var/lib/felhom/docker/.

BOTH PRE-GATES were shown able to see something before their zeroes were believed: the
404 pre-gate, and the token-leak grep which returns 0 on the committed log and 1 on a
seeded throwaway copy. The transient unit's own properties were grepped for the token
too - 0, with the same seeded positive control returning 1. Acceptance markers counted on
the COMMITTED log: 1/1/1/1 present, 0/0 absent, and the zeroes are believable because the
same including-mount-point pattern returns two real lines on that file.

THE VOUCH IS A THREE-FIELD CHANGE and only one field moved, which is stated rather than
left to look careless: golden_version 0.229.0 -> 0.230.0; agent_version 0.130.0 and
min_agent 0.129.0 UNCHANGED because v0.230.0's CHANGELOG header says MinAgent 0.129.0 and
0.129.0 <= 0.130.0, so this is not the R-216 shape. The 303 flash was not treated as
proof - the page was re-read and golden_behind_fleet confirmed absent.

THE FLOOR is a separate setting and was raised on the operator's explicit answer:
min_controller_version 0.229.0 -> 0.230.0. THE POSITIVE OBSERVABLE, from the agent's own
journal on demo-felhom, which was still running the defective 0.229.0:
  16:21:30 controller-swap: image file written, restarting bootstrap  target=...0.230.0
  16:21:40 controller-swap: new controller healthy                    target=...0.230.0
Both boxes now 0.230.0 healthy. Honest note: the polling loop's first read already said
0.230.0, so the transition was not seen by the loop - the journal is the evidence.

R-410 FILED, found while the gate went green: golden_currency_gate.py is satisfied by a
DIRECTORY NAME (EVIDENCE_RE against os.listdir, :89,:123). I created the evidence
directory before the bake finished and the gate would have passed at that moment. It
already declares that it does not check the vouch; it does not declare that the bake
check is a filename check. Fix: read the GOLDEN_SHA256= line out of the directory's
bake.log, with a red-proof on an empty directory.

R-242 updated - seventh debt, paid the same day, twice in one day.

Teardown: pct destroy 9100 --purge, shred -u AFTER the log was copied out, poweroff,
qemu confirmed exited with ps -eo comm (not pgrep -f, which self-matches), disk reverted
to virgin.

All 13 gates green - the first push this session that needed no --no-verify.

Ceiling R-409 -> R-410.
This commit is contained in:
2026-08-31 16:25:35 +02:00
parent 32a4c35c9c
commit 2263245cf2
22 changed files with 1851 additions and 19 deletions
@@ -0,0 +1,282 @@
<!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?v=0.109.0">
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 6v6l4 2" /></symbol>
<symbol id="i-boxes" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z" /> <path d="m7 16.5-4.74-2.85" /> <path d="m7 16.5 5-3" /> <path d="M7 16.5v5.17" /> <path d="M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z" /> <path d="m17 16.5-5-3" /> <path d="m17 16.5 4.74-2.85" /> <path d="M17 16.5v5.17" /> <path d="M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z" /> <path d="M12 8 7.26 5.15" /> <path d="m12 8 4.74-2.85" /> <path d="M12 13.5V8" /></symbol>
<symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /> <path d="M16 3.128a4 4 0 0 1 0 7.744" /> <path d="M22 21v-2a4 4 0 0 0-3-3.87" /> <circle cx="9" cy="7" r="4" /></symbol>
</svg>
<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>
<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).
<strong>Saving takes effect immediately</strong> — boxes below the floor update on their next
report, no customer action. Blank = no global floor. This setting is independent of the Day-0
artifact manifest below.
</p>
<p style="margin: 0 0 0.75rem; font-size: 0.85em;">
Effective floor:
<code>v0.229.0</code>
<span style="color: #cbd5e1;">— source: <strong>DB (hub_settings)</strong>; env fallback would be <code>v0.120.0</code></span>
</p>
<form id="global-floor-form" method="POST" action="/configuration/global-floor" style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
<input type="hidden" name="_csrf" value="">
<input type="text" id="global-floor-input" name="min_controller_version" value="0.229.0" placeholder="e.g. 0.86.0 (blank = clear DB override)" style="padding: 0.3em 0.5em; width: 16em;">
<button class="btn btn-sm" type="button" onclick="confirmGlobalFloor()">Save global floor…</button>
<span style="font-size: 0.85em; color: #cbd5e1;">DB override: <code>v0.229.0</code></span>
</form>
<div id="global-floor-confirm" style="display: none; margin-top: 0.75rem; padding: 0.75rem; border: 1px solid #7c3f00; background: #241a0a; border-radius: 6px; max-width: 44em;">
<p id="global-floor-impact" style="margin: 0 0 0.5rem; font-size: 0.9em;">…</p>
<p style="margin: 0 0 0.5rem; font-size: 0.85em; color: #cbd5e1;">Type the version again to confirm (or <code>CLEAR</code> to remove the DB override):</p>
<input type="text" id="global-floor-confirm-input" placeholder="retype the version…" style="padding: 0.3em 0.5em; width: 16em;">
<button class="btn btn-sm" type="button" onclick="submitGlobalFloor()">Confirm &amp; apply</button>
<button class="btn btn-sm btn-ghost" type="button" onclick="document.getElementById('global-floor-confirm').style.display='none';">Cancel</button>
<p id="global-floor-confirm-err" style="margin: 0.4em 0 0; font-size: 0.8em; color: #f87171;"></p>
</div>
<script>
function confirmGlobalFloor() {
var v = document.getElementById('global-floor-input').value.trim();
var box = document.getElementById('global-floor-confirm');
var impact = document.getElementById('global-floor-impact');
document.getElementById('global-floor-confirm-input').value = '';
document.getElementById('global-floor-confirm-err').textContent = '';
box.style.display = 'block';
if (v === '') {
impact.textContent = 'This will CLEAR the DB floor override (the box falls back to the env default). Type CLEAR to confirm.';
return;
}
impact.textContent = 'Checking blast radius…';
fetch('/configuration/global-floor/impact?v=' + encodeURIComponent(v))
.then(function(r){ return r.json(); })
.then(function(d){
if (!d.valid) { impact.textContent = 'Invalid version — use X.Y.Z.'; return; }
impact.textContent = 'Saving the minimum version v' + d.version +
' takes effect immediately — currently ' + d.below +
' box(es) are below this version and would update on their next report.';
})
.catch(function(){ impact.textContent = 'Could not compute the blast radius; proceed with caution.'; });
}
function submitGlobalFloor() {
var v = document.getElementById('global-floor-input').value.trim();
var typed = document.getElementById('global-floor-confirm-input').value.trim();
var err = document.getElementById('global-floor-confirm-err');
var expected = (v === '') ? 'CLEAR' : v;
if (typed !== expected) { err.textContent = 'Confirmation does not match (' + expected + ').'; return; }
document.getElementById('global-floor-form').submit();
}
</script>
</section>
<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;">
<input type="hidden" name="_csrf" value="">
<label style="font-size: 0.9em; color: #cbd5e1;">Agent</label>
<select name="agent_version" id="agent_version" onchange="syncArtifactSha('agent')" style="padding: 0.3em 0.5em;">
<option value="" data-sha="">— none —</option>
<option value="0.130.0" data-sha="a56a92a7bd68f5b46736eaec4806c3d26c16ccb35118c4ac0e3d8094eaefabc3" selected>0.130.0</option>
<option value="0.129.0" data-sha="53a54f0620afbd6d4a1b86607e2a84dfbe7a290f44ed38a9485d6d971eecde8d" >0.129.0</option>
<option value="0.128.0" data-sha="c6eba73bf9b9ad6980cfef57bfb3db31581abc9d643de2ff50d4254576fc1a59" >0.128.0</option>
<option value="0.127.0" data-sha="f0d2c89311f03fd2b9ab6ae242e09a724ff8ea1fd2214759bae247c5db7dbf72" >0.127.0</option>
<option value="0.126.0" data-sha="7ecf8e9cdba237bc2d81003440095eace6418d00c3a485f3ca77742a25e4a93b" >0.126.0</option>
<option value="0.125.0" data-sha="f7d8339b53d92a6c45be7eaf189469a041b6b00b758a64511c0479beae7016b3" >0.125.0</option>
<option value="0.124.1" data-sha="5c279bda64cdec8cbd76f8a800bcd602063bb17175f406b225775e7dece3a21c" >0.124.1</option>
<option value="0.124.0" data-sha="5e4179383bc838a7ad360efcfab5e8f02a2939ed8bcc4bc68ffd85ba02a5c9a4" >0.124.0</option>
<option value="0.123.0" data-sha="74910135ac4feb1b7f0ad4dbd1541d965cbc0fe70d4f47b62ebf7e4bfb962453" >0.123.0</option>
<option value="0.122.0" data-sha="d5f294e56c1ef59055e8e87fb9135aa477632dbbc56a5d4bff46bbd0466c1edf" >0.122.0</option>
<option value="0.121.1" data-sha="afaeeb509d1ed70d6e6bebac0393a3cd5be59d51e3db9ff96ef8524bd78546d7" >0.121.1</option>
<option value="0.121.0" data-sha="b2128f3cd4539225a2842f541f56ffaf5390b1d97f3f3a80076ec5f53dbc7d7a" >0.121.0</option>
</select>
<input type="text" name="agent_sha256" id="agent_sha256" value="a56a92a7bd68f5b46736eaec4806c3d26c16ccb35118c4ac0e3d8094eaefabc3" readonly placeholder="64-hex sha256 (blank = none)" style="padding: 0.3em 0.5em; font-family: monospace; opacity: 0.7;">
<label style="font-size: 0.9em; color: #cbd5e1;">Golden</label>
<select name="golden_version" id="golden_version" onchange="syncArtifactSha('golden')" style="padding: 0.3em 0.5em;">
<option value="" data-sha="">— none —</option>
<option value="0.229.0" data-sha="39aa886df77b21757aef3b298a389343dc0df5134bb0f14e8f92a451d7bdae87" selected>0.229.0</option>
<option value="0.228.0" data-sha="76a3a98b9e7cc23bf8ae51b38a6272f576df285cb34cd22235ac3f06a31e53ec" >0.228.0</option>
<option value="0.227.1" data-sha="66754491dc9bd0130ef8ded9562f63c53a5ffdcfd91baa551141e55fa083ea32" >0.227.1</option>
<option value="0.226.1" data-sha="70ed8e9377dec22a9b493e55f222b0e25a49d7f3caec8c506e0412fd6baefe69" >0.226.1</option>
<option value="0.223.0" data-sha="9eaf39ac39219b42ec9e6cbf890275febcdcc6f53325fe0c0f591d3431044f17" >0.223.0</option>
<option value="0.222.0" data-sha="19f5904f53792684f046ec0bc25426645cb87ad73d5cfc6c03639d9f82706037" >0.222.0</option>
<option value="0.221.1" data-sha="1c8bf6cf08cadabeca6331f38360d905e867c235067cd10c2716915b6e6df089" >0.221.1</option>
</select>
<input type="text" name="golden_sha256" id="golden_sha256" value="39aa886df77b21757aef3b298a389343dc0df5134bb0f14e8f92a451d7bdae87" readonly placeholder="64-hex sha256 (blank = none)" style="padding: 0.3em 0.5em; font-family: monospace; opacity: 0.7;">
<label style="font-size: 0.9em; color: #cbd5e1;">Min agent</label>
<input type="text" name="min_agent" value="0.129.0" placeholder="e.g. 0.81.0 (blank = uncoupled)" style="padding: 0.3em 0.5em;">
<span style="font-size: 0.8em; color: #94a6bf;">The golden's controller CHANGELOG <code>MinAgent:</code>. The hub HOLDS the floor for any box whose agent is below this — blank = uncoupled release, no gating.</span>
<label style="font-size: 0.9em; color: #cbd5e1;">PBS wrapper</label>
<input type="text" name="wrapper_sha256" value="104db0a4401f65bbc476e82bfb1796433bcb36f8f8cce69efb3bb5c40fcb16b3" placeholder="64-hex sha256 (blank = not vouched)" style="grid-column: 2 / 4; padding: 0.3em 0.5em; font-family: monospace;">
<span></span>
<span style="grid-column: 2 / 4; font-size: 0.8em; color: #94a6bf;">sha256 of <code>configs/felhom-pbs-apply</code> (R-50b). Unlike the agent and golden this root-owned wrapper is installed from <code>raw/branch/main</code> — unversioned and unpinned. Recording it here does not fix the channel; it makes host drift <em>visible</em>: agents report the installed file's hash and a mismatch is surfaced on the host page.</span>
<span></span><span></span>
<button class="btn btn-sm" type="submit" style="justify-self: start;">Save artifact manifest</button>
</form>
<script>
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>
<section class="card">
<h3 style="margin-top: 0;">Login password</h3>
<p class="text-muted" style="margin: 0 0 0.75rem; font-size: 0.85em;">
The password for signing in to this hub UI. <strong>Changing it takes effect immediately</strong>
for the next sign-in — your current session stays logged in. Enter your current password to confirm.
If you ever lose it, the deployment ConfigMap (<code>auth.password_hash</code>) remains the reset path.
</p>
<form method="POST" action="/configuration/password" style="display: grid; grid-template-columns: auto 20em; gap: 0.5rem; align-items: center; max-width: 40em;"
onsubmit="return felhomCheckNewPw(this);">
<input type="hidden" name="_csrf" value="">
<label style="font-size: 0.9em; color: #cbd5e1;">Current password</label>
<input type="password" name="current_password" autocomplete="current-password" required style="padding: 0.3em 0.5em;">
<label style="font-size: 0.9em; color: #cbd5e1;">New password</label>
<input type="password" id="new_password" name="new_password" autocomplete="new-password" minlength="8" maxlength="72" required style="padding: 0.3em 0.5em;">
<label style="font-size: 0.9em; color: #cbd5e1;">Confirm new password</label>
<input type="password" id="confirm_password" name="confirm_password" autocomplete="new-password" minlength="8" maxlength="72" required style="padding: 0.3em 0.5em;">
<span></span>
<span>
<button class="btn btn-sm" type="submit">Change password</button>
<span id="pw-client-err" style="margin-left: 0.6em; font-size: 0.8em; color: #f87171;"></span>
</span>
</form>
<script>
function felhomCheckNewPw(form) {
var a = form.new_password.value;
var b = form.confirm_password.value;
var err = document.getElementById('pw-client-err');
err.textContent = '';
if (a.length < 8) { err.textContent = 'New password must be at least 8 characters.'; return false; }
if (a !== b) { err.textContent = 'New password and confirmation do not match.'; return false; }
return true;
}
</script>
</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">211</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;">2026-08-30T16:51:40Z</span>
</div>
</div>
<form method="POST" action="/configuration" style="margin-top: 1rem;">
<input type="hidden" name="_csrf" value="">
<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)">0.109.0</span>
</footer>
</div>
</body>
</html>
@@ -0,0 +1,9 @@
=== preconditions ===
2026-08-31T14:09:11Z
controller image 0.230.0 in registry: HTTP 200
MinAgent (CHANGELOG v0.230.0): 0.129.0
hub BEFORE: golden=0.229.0 agent=0.130.0 min_agent=0.129.0
newest published golden: 0.229.0
/dev/sda1 9.1T 3.1T 5.5T 37% /mnt/5_hdd
/dev/sdb1 445G 216G 207G 51% /
revert rc=0 (also proves no qemu holds the qcow2)
@@ -0,0 +1,44 @@
Warning: Permanently added '[localhost]:2222' (ED25519) to the list of known hosts.
=== pveam update (the virgin snapshot INDEX is stale too) ===
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
LANGUAGE = (unset),
LC_ALL = (unset),
LC_CTYPE = "UTF-8",
LC_NUMERIC = (unset),
LC_COLLATE = (unset),
LC_TIME = (unset),
LC_MESSAGES = (unset),
LC_MONETARY = (unset),
LC_ADDRESS = (unset),
LC_IDENTIFICATION = (unset),
LC_MEASUREMENT = (unset),
LC_PAPER = (unset),
LC_TELEPHONE = (unset),
LC_NAME = (unset),
LANG = "en_US.UTF-8"
are supported and installed on your system.
perl: warning: Falling back to a fallback locale ("en_US.UTF-8").
update successful
=== available debian-13 ===
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
LANGUAGE = (unset),
LC_ALL = (unset),
LC_CTYPE = "UTF-8",
LC_NUMERIC = (unset),
LC_COLLATE = (unset),
LC_TIME = (unset),
LC_MESSAGES = (unset),
LC_MONETARY = (unset),
LC_ADDRESS = (unset),
LC_IDENTIFICATION = (unset),
LC_MEASUREMENT = (unset),
LC_PAPER = (unset),
LC_TELEPHONE = (unset),
LC_NAME = (unset),
LANG = "en_US.UTF-8"
are supported and installed on your system.
perl: warning: Falling back to a fallback locale ("en_US.UTF-8").
system debian-13-standard_13.6-1_amd64.tar.zst
system debian-13-standard_13.6-1_arm64.tar.zst
@@ -0,0 +1,8 @@
65536K ........ ........ ........ ........ 77% 8.84M 3s
98304K ........ ........ ........ ... 100% 11.2M=12s
2026-08-31 16:10:24 (9.98 MB/s) - '/var/lib/vz/template/cache/debian-13-standard_13.6-1_amd64.tar.zst.tmp_dwnl.1223' saved [129954319/129954319]
calculating checksum...OK, checksum verified
download of 'http://download.proxmox.com/images/system/debian-13-standard_13.6-1_amd64.tar.zst' to '/var/lib/vz/template/cache/debian-13-standard_13.6-1_amd64.tar.zst' finished
=== templates present ===
NAME SIZE
local:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst 123.93MB
@@ -0,0 +1,4 @@
-rw------- 1 root root 41 Aug 31 16:10 /root/.gitea-token
-rwx------ 1 root root 30373 Aug 31 16:10 /root/build-golden.sh
script version: GOLDEN_SCRIPT_VERSION="3.0.0"
token bytes: 41 (value never printed)
@@ -0,0 +1,6 @@
Running as unit: golden-bake.service; invocation ID: d9094d8ab9c2481a8e337afa5121e25d
=== token-leak check on the unit properties (must be 0) ===
0
=== positive control that the grep WORKS (must be 1) ===
1
active
@@ -0,0 +1,322 @@
[golden] build-golden.sh v3.0.0 — baking controller gitea.dooplex.hu/admin/felhom-controller:0.230.0
[golden] creating build LXC 9100 (nesting=1,keyctl=1, unprivileged; rootfs 32G + ONE data volume 24G @ /var/lib/felhom, backup=1) …
Logical volume "vm-9100-disk-0" created.
Logical volume pve/vm-9100-disk-0 changed.
Creating filesystem with 8388608 4k blocks and 2097152 inodes
Filesystem UUID: 6570f8d5-8322-44b9-a8d7-6c7bca23ed01
Superblock backups stored on blocks:
32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,
4096000, 7962624
Logical volume "vm-9100-disk-1" created.
Logical volume pve/vm-9100-disk-1 changed.
Creating filesystem with 6291456 4k blocks and 1572864 inodes
Filesystem UUID: df74688f-5799-4023-a704-5f4b14420724
Superblock backups stored on blocks:
32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,
extracting archive '/var/lib/vz/template/cache/debian-13-standard_13.6-1_amd64.tar.zst'
Total bytes read: 553512960 (528MiB, 107MiB/s)
Detected container architecture: amd64
Creating SSH host key 'ssh_host_rsa_key' - this may take some time ...
done: SHA256:wTmy+E+RiSYEvFoekpADhdBpm+/2LBnQDCQOnmMfgoA root@felhom-golden
Creating SSH host key 'ssh_host_ecdsa_key' - this may take some time ...
done: SHA256:iJkyqbDKh1UwnLgYXubhMGIym2GNsfsjT75R3RgKdQI root@felhom-golden
Creating SSH host key 'ssh_host_ed25519_key' - this may take some time ...
done: SHA256:t09l63iIyQtdG8HN/lbqwmfKZ2r4pQI6lHgOWW2wIME root@felhom-golden
[golden] starting + installing Docker (official repo, trixie channel) …
apt-listchanges: Can't set locale; make sure $LC_* and $LANG are correct!
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
LANGUAGE = (unset),
LC_ALL = (unset),
LC_CTYPE = (unset),
LC_NUMERIC = (unset),
LC_COLLATE = (unset),
LC_TIME = (unset),
LC_MESSAGES = (unset),
LC_MONETARY = (unset),
LC_ADDRESS = (unset),
LC_IDENTIFICATION = (unset),
LC_MEASUREMENT = (unset),
LC_PAPER = (unset),
LC_TELEPHONE = (unset),
LC_NAME = (unset),
LANG = "en_US.UTF-8"
are supported and installed on your system.
perl: warning: Falling back to the standard locale ("C").
apt-listchanges: Can't set locale; make sure $LC_* and $LANG are correct!
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
LANGUAGE = (unset),
LC_ALL = (unset),
LC_CTYPE = (unset),
LC_NUMERIC = (unset),
LC_COLLATE = (unset),
LC_TIME = (unset),
LC_MESSAGES = (unset),
LC_MONETARY = (unset),
LC_ADDRESS = (unset),
LC_IDENTIFICATION = (unset),
LC_MEASUREMENT = (unset),
LC_PAPER = (unset),
LC_TELEPHONE = (unset),
LC_NAME = (unset),
LANG = "en_US.UTF-8"
are supported and installed on your system.
perl: warning: Falling back to the standard locale ("C").
[golden] baking daemon.json: classic overlay2 driver (containerd-snapshotter OFF) + log rotation …
[golden] wiring the single data volume (R-165 variant V-c): /var/lib/felhom/{docker,sys_drive} -> binds …
[golden] verifying Docker works in the build guest (storage driver should be overlay2 on the ext4 data volume) …
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
4f55086f7dd0: Pulling fs layer
4f55086f7dd0: Verifying Checksum
4f55086f7dd0: Download complete
4f55086f7dd0: Pull complete
Digest: sha256:5dd0d3e6e255913fc30f90b9f2b1d359cc2cbdb48090cc4b65f1676e203243cc
Status: Downloaded newer image for hello-world:latest
docker OK (overlay2; data-root /var/lib/docker)
/var/lib/docker is a real mount: /dev/mapper/pve-vm--9100--disk--1[/docker] ext4
/mnt/sys_drive is a real mount: /dev/mapper/pve-vm--9100--disk--1[/sys_drive] ext4
both paths are ONE filesystem: /dev/mapper/pve-vm--9100--disk--1 23317576
[golden] baking the in-guest controller image gitea.dooplex.hu/admin/felhom-controller:0.230.0 (no registry cred at deploy) …
WARNING! Your credentials are stored unencrypted in '/root/.docker/config.json'.
Configure a credential helper to remove this warning. See
https://docs.docker.com/go/credential-store/
0.230.0: Pulling from admin/felhom-controller
a8ac7f6c67ab: Pulling fs layer
bf30769d36e7: Pulling fs layer
044b66fbe46c: Pulling fs layer
b5c41a28e83f: Pulling fs layer
21d94e6c63f0: Pulling fs layer
23c8544b7435: Pulling fs layer
b5c41a28e83f: Waiting
21d94e6c63f0: Waiting
23c8544b7435: Waiting
a8ac7f6c67ab: Verifying Checksum
a8ac7f6c67ab: Download complete
b5c41a28e83f: Download complete
21d94e6c63f0: Verifying Checksum
21d94e6c63f0: Download complete
23c8544b7435: Verifying Checksum
23c8544b7435: Download complete
044b66fbe46c: Verifying Checksum
044b66fbe46c: Download complete
bf30769d36e7: Verifying Checksum
bf30769d36e7: Download complete
a8ac7f6c67ab: Pull complete
bf30769d36e7: Pull complete
044b66fbe46c: Pull complete
b5c41a28e83f: Pull complete
21d94e6c63f0: Pull complete
23c8544b7435: Pull complete
Digest: sha256:50659db2900c615ca13c72eaa8c5e88a8f9a76b808a7b29721f818ec7b2fefb7
Status: Downloaded newer image for gitea.dooplex.hu/admin/felhom-controller:0.230.0
gitea.dooplex.hu/admin/felhom-controller:0.230.0
[golden] asking the controller which infra images it manages …
[golden] baking infra images (4): traefik:v3.6.7 cloudflare/cloudflared:2026.6.0 gtstef/filebrowser:1.3.3-stable gitea.dooplex.hu/admin/felhom-samba:1.1.0 …
v3.6.7: Pulling from library/traefik
589002ba0eae: Pulling fs layer
ef63511ea6cc: Pulling fs layer
0738e5cb835e: Pulling fs layer
3e6813f70c64: Pulling fs layer
3e6813f70c64: Waiting
589002ba0eae: Verifying Checksum
589002ba0eae: Download complete
ef63511ea6cc: Verifying Checksum
ef63511ea6cc: Download complete
3e6813f70c64: Verifying Checksum
3e6813f70c64: Download complete
0738e5cb835e: Verifying Checksum
0738e5cb835e: Download complete
589002ba0eae: Pull complete
ef63511ea6cc: Pull complete
0738e5cb835e: Pull complete
3e6813f70c64: Pull complete
Digest: sha256:a9890c898f379c1905ee5b28342f6b408dc863f08db2dab20e46c267d1ff463a
Status: Downloaded newer image for traefik:v3.6.7
docker.io/library/traefik:v3.6.7
2026.6.0: Pulling from cloudflare/cloudflared
47de5dd0b812: Pulling fs layer
c172f21841df: Pulling fs layer
99515e7b4d35: Pulling fs layer
99ba982a9142: Pulling fs layer
d6b1b89eccac: Pulling fs layer
2780920e5dbf: Pulling fs layer
7c12895b777b: Pulling fs layer
3214acf345c0: Pulling fs layer
52630fc75a18: Pulling fs layer
dd64bf2dd177: Pulling fs layer
b839dfae01f6: Pulling fs layer
ebddc55facdc: Pulling fs layer
bdfd7f7e5bf6: Pulling fs layer
2d4d7adf6272: Pulling fs layer
40008157d8d2: Pulling fs layer
bd8962e29291: Pulling fs layer
cac2ae0193cb: Pulling fs layer
74d1dac84ecc: Pulling fs layer
dd64bf2dd177: Waiting
b839dfae01f6: Waiting
ebddc55facdc: Waiting
bdfd7f7e5bf6: Waiting
2d4d7adf6272: Waiting
40008157d8d2: Waiting
bd8962e29291: Waiting
cac2ae0193cb: Waiting
74d1dac84ecc: Waiting
99ba982a9142: Waiting
d6b1b89eccac: Waiting
2780920e5dbf: Waiting
7c12895b777b: Waiting
3214acf345c0: Waiting
52630fc75a18: Waiting
47de5dd0b812: Verifying Checksum
47de5dd0b812: Download complete
c172f21841df: Verifying Checksum
c172f21841df: Download complete
99515e7b4d35: Verifying Checksum
99515e7b4d35: Download complete
99ba982a9142: Verifying Checksum
99ba982a9142: Download complete
47de5dd0b812: Pull complete
d6b1b89eccac: Verifying Checksum
d6b1b89eccac: Download complete
2780920e5dbf: Verifying Checksum
2780920e5dbf: Download complete
7c12895b777b: Verifying Checksum
7c12895b777b: Download complete
3214acf345c0: Verifying Checksum
3214acf345c0: Download complete
52630fc75a18: Verifying Checksum
52630fc75a18: Download complete
dd64bf2dd177: Verifying Checksum
dd64bf2dd177: Download complete
b839dfae01f6: Verifying Checksum
b839dfae01f6: Download complete
ebddc55facdc: Verifying Checksum
ebddc55facdc: Download complete
bdfd7f7e5bf6: Verifying Checksum
bdfd7f7e5bf6: Download complete
c172f21841df: Pull complete
40008157d8d2: Verifying Checksum
40008157d8d2: Download complete
bd8962e29291: Verifying Checksum
bd8962e29291: Download complete
2d4d7adf6272: Verifying Checksum
2d4d7adf6272: Download complete
cac2ae0193cb: Verifying Checksum
cac2ae0193cb: Download complete
74d1dac84ecc: Verifying Checksum
74d1dac84ecc: Download complete
99515e7b4d35: Pull complete
99ba982a9142: Pull complete
d6b1b89eccac: Pull complete
2780920e5dbf: Pull complete
7c12895b777b: Pull complete
3214acf345c0: Pull complete
52630fc75a18: Pull complete
dd64bf2dd177: Pull complete
b839dfae01f6: Pull complete
ebddc55facdc: Pull complete
bdfd7f7e5bf6: Pull complete
2d4d7adf6272: Pull complete
40008157d8d2: Pull complete
bd8962e29291: Pull complete
cac2ae0193cb: Pull complete
74d1dac84ecc: Pull complete
Digest: sha256:ba461b8aa9c042156dbd39c38657fe7431bafa063220eab8d5330a523863da9f
Status: Downloaded newer image for cloudflare/cloudflared:2026.6.0
docker.io/cloudflare/cloudflared:2026.6.0
1.3.3-stable: Pulling from gtstef/filebrowser
6a0ac1617861: Pulling fs layer
ef8806083e82: Pulling fs layer
b74107c861c7: Pulling fs layer
adc935def003: Pulling fs layer
4f4fb700ef54: Pulling fs layer
18695ccc900a: Pulling fs layer
45d119d5c397: Pulling fs layer
dac52db4fc51: Pulling fs layer
6d598f86b2f2: Pulling fs layer
8aa349c8396c: Pulling fs layer
45d119d5c397: Waiting
dac52db4fc51: Waiting
6d598f86b2f2: Waiting
8aa349c8396c: Waiting
adc935def003: Waiting
4f4fb700ef54: Waiting
18695ccc900a: Waiting
6a0ac1617861: Verifying Checksum
6a0ac1617861: Download complete
adc935def003: Verifying Checksum
adc935def003: Download complete
4f4fb700ef54: Verifying Checksum
4f4fb700ef54: Download complete
b74107c861c7: Verifying Checksum
b74107c861c7: Download complete
6a0ac1617861: Pull complete
45d119d5c397: Verifying Checksum
45d119d5c397: Download complete
ef8806083e82: Verifying Checksum
ef8806083e82: Download complete
6d598f86b2f2: Verifying Checksum
6d598f86b2f2: Download complete
dac52db4fc51: Verifying Checksum
dac52db4fc51: Download complete
8aa349c8396c: Verifying Checksum
8aa349c8396c: Download complete
18695ccc900a: Verifying Checksum
18695ccc900a: Download complete
ef8806083e82: Pull complete
b74107c861c7: Pull complete
adc935def003: Pull complete
4f4fb700ef54: Pull complete
18695ccc900a: Pull complete
45d119d5c397: Pull complete
dac52db4fc51: Pull complete
6d598f86b2f2: Pull complete
8aa349c8396c: Pull complete
Digest: sha256:eb3733681db8757412632c61a99ad656f0d94ed6781bb2ea114b4d70babab78c
Status: Downloaded newer image for gtstef/filebrowser:1.3.3-stable
docker.io/gtstef/filebrowser:1.3.3-stable
1.1.0: Pulling from admin/felhom-samba
897d797d2723: Pulling fs layer
3051591aa250: Pulling fs layer
ce57a3f93416: Pulling fs layer
fb94eeec2fe1: Pulling fs layer
fb94eeec2fe1: Waiting
ce57a3f93416: Verifying Checksum
ce57a3f93416: Download complete
fb94eeec2fe1: Verifying Checksum
fb94eeec2fe1: Download complete
897d797d2723: Verifying Checksum
897d797d2723: Download complete
3051591aa250: Verifying Checksum
3051591aa250: Download complete
897d797d2723: Pull complete
3051591aa250: Pull complete
ce57a3f93416: Pull complete
fb94eeec2fe1: Pull complete
Digest: sha256:1c17c09422bec0366d7cf0e0fcfc1486ba6c90334a0a5d5c851073a9342f8f10
Status: Downloaded newer image for gitea.dooplex.hu/admin/felhom-samba:1.1.0
gitea.dooplex.hu/admin/felhom-samba:1.1.0
[golden] baking the controller-bootstrap unit (deploys the BAKED controller from the config mount) …
Created symlink '/etc/systemd/system/multi-user.target.wants/felhom-controller-bootstrap.service' → '/etc/systemd/system/felhom-controller-bootstrap.service'.
[golden] baking the controller-bootstrap PATH unit (starts the service on bootstrap-mount hot-plug — B1) …
Created symlink '/etc/systemd/system/multi-user.target.wants/felhom-controller-bootstrap.path' → '/etc/systemd/system/felhom-controller-bootstrap.path'.
[golden] baking the first-boot SSH host-key regeneration unit (F3) …
Created symlink '/etc/systemd/system/multi-user.target.wants/felhom-regen-hostkeys.service' → '/etc/systemd/system/felhom-regen-hostkeys.service'.
[golden] identity-clean + minimize …
[golden] stop + archive …
INFO: including mount point rootfs ('/') in backup
INFO: including mount point mp0 ('/var/lib/felhom') in backup
INFO: archive file size: 627MB
INFO: Finished Backup of VM 9100 (00:00:41)
[golden] DONE. golden archive volid: local:backup/vzdump-lxc-9100-2026_08_31-16_15_14.tar.zst (rootfs 32G + ONE data volume 24G @ /var/lib/felhom, all in the archive)
[golden] publishing golden (657873700 bytes, sha256 9287f7cef5f13166…) → https://gitea.dooplex.hu/api/packages/admin/generic/felhom-golden/0.230.0/golden.tar.zst
[golden] pre-delete existing: HTTP 404 (404/204 expected)
[golden] upload OK (HTTP 201)
GOLDEN_VERSION=0.230.0
GOLDEN_SHA256=9287f7cef5f13166276e8406005e3f28004004510c5184f1c1c7377f7aafad2e
[golden] Record in the hub operator UI (Configs → Day-0 artifacts): golden 0.230.0 / 9287f7cef5f13166276e8406005e3f28004004510c5184f1c1c7377f7aafad2e
[golden] (the build guest 9100 is stopped; destroy it with: pct destroy 9100 --purge)
@@ -0,0 +1,328 @@
[golden] build-golden.sh v3.0.0 — baking controller gitea.dooplex.hu/admin/felhom-controller:0.230.0
[golden] creating build LXC 9100 (nesting=1,keyctl=1, unprivileged; rootfs 32G + ONE data volume 24G @ /var/lib/felhom, backup=1) …
Logical volume "vm-9100-disk-0" created.
Logical volume pve/vm-9100-disk-0 changed.
Creating filesystem with 8388608 4k blocks and 2097152 inodes
Filesystem UUID: 6570f8d5-8322-44b9-a8d7-6c7bca23ed01
Superblock backups stored on blocks:
32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,
4096000, 7962624
Logical volume "vm-9100-disk-1" created.
Logical volume pve/vm-9100-disk-1 changed.
Creating filesystem with 6291456 4k blocks and 1572864 inodes
Filesystem UUID: df74688f-5799-4023-a704-5f4b14420724
Superblock backups stored on blocks:
32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,
extracting archive '/var/lib/vz/template/cache/debian-13-standard_13.6-1_amd64.tar.zst'
Total bytes read: 553512960 (528MiB, 107MiB/s)
Detected container architecture: amd64
Creating SSH host key 'ssh_host_rsa_key' - this may take some time ...
done: SHA256:wTmy+E+RiSYEvFoekpADhdBpm+/2LBnQDCQOnmMfgoA root@felhom-golden
Creating SSH host key 'ssh_host_ecdsa_key' - this may take some time ...
done: SHA256:iJkyqbDKh1UwnLgYXubhMGIym2GNsfsjT75R3RgKdQI root@felhom-golden
Creating SSH host key 'ssh_host_ed25519_key' - this may take some time ...
done: SHA256:t09l63iIyQtdG8HN/lbqwmfKZ2r4pQI6lHgOWW2wIME root@felhom-golden
[golden] starting + installing Docker (official repo, trixie channel) …
apt-listchanges: Can't set locale; make sure $LC_* and $LANG are correct!
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
LANGUAGE = (unset),
LC_ALL = (unset),
LC_CTYPE = (unset),
LC_NUMERIC = (unset),
LC_COLLATE = (unset),
LC_TIME = (unset),
LC_MESSAGES = (unset),
LC_MONETARY = (unset),
LC_ADDRESS = (unset),
LC_IDENTIFICATION = (unset),
LC_MEASUREMENT = (unset),
LC_PAPER = (unset),
LC_TELEPHONE = (unset),
LC_NAME = (unset),
LANG = "en_US.UTF-8"
are supported and installed on your system.
perl: warning: Falling back to the standard locale ("C").
locale: Cannot set LC_CTYPE to default locale: No such file or directory
locale: Cannot set LC_MESSAGES to default locale: No such file or directory
locale: Cannot set LC_ALL to default locale: No such file or directory
apt-listchanges: Can't set locale; make sure $LC_* and $LANG are correct!
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
LANGUAGE = (unset),
LC_ALL = (unset),
LC_CTYPE = (unset),
LC_NUMERIC = (unset),
LC_COLLATE = (unset),
LC_TIME = (unset),
LC_MESSAGES = (unset),
LC_MONETARY = (unset),
LC_ADDRESS = (unset),
LC_IDENTIFICATION = (unset),
LC_MEASUREMENT = (unset),
LC_PAPER = (unset),
LC_TELEPHONE = (unset),
LC_NAME = (unset),
LANG = "en_US.UTF-8"
are supported and installed on your system.
perl: warning: Falling back to the standard locale ("C").
locale: Cannot set LC_CTYPE to default locale: No such file or directory
locale: Cannot set LC_MESSAGES to default locale: No such file or directory
locale: Cannot set LC_ALL to default locale: No such file or directory
[golden] baking daemon.json: classic overlay2 driver (containerd-snapshotter OFF) + log rotation …
[golden] wiring the single data volume (R-165 variant V-c): /var/lib/felhom/{docker,sys_drive} -> binds …
[golden] verifying Docker works in the build guest (storage driver should be overlay2 on the ext4 data volume) …
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
4f55086f7dd0: Pulling fs layer
4f55086f7dd0: Verifying Checksum
4f55086f7dd0: Download complete
4f55086f7dd0: Pull complete
Digest: sha256:5dd0d3e6e255913fc30f90b9f2b1d359cc2cbdb48090cc4b65f1676e203243cc
Status: Downloaded newer image for hello-world:latest
docker OK (overlay2; data-root /var/lib/docker)
/var/lib/docker is a real mount: /dev/mapper/pve-vm--9100--disk--1[/docker] ext4
/mnt/sys_drive is a real mount: /dev/mapper/pve-vm--9100--disk--1[/sys_drive] ext4
both paths are ONE filesystem: /dev/mapper/pve-vm--9100--disk--1 23317576
[golden] baking the in-guest controller image gitea.dooplex.hu/admin/felhom-controller:0.230.0 (no registry cred at deploy) …
WARNING! Your credentials are stored unencrypted in '/root/.docker/config.json'.
Configure a credential helper to remove this warning. See
https://docs.docker.com/go/credential-store/
0.230.0: Pulling from admin/felhom-controller
a8ac7f6c67ab: Pulling fs layer
bf30769d36e7: Pulling fs layer
044b66fbe46c: Pulling fs layer
b5c41a28e83f: Pulling fs layer
21d94e6c63f0: Pulling fs layer
23c8544b7435: Pulling fs layer
b5c41a28e83f: Waiting
21d94e6c63f0: Waiting
23c8544b7435: Waiting
a8ac7f6c67ab: Verifying Checksum
a8ac7f6c67ab: Download complete
b5c41a28e83f: Download complete
21d94e6c63f0: Verifying Checksum
21d94e6c63f0: Download complete
23c8544b7435: Verifying Checksum
23c8544b7435: Download complete
044b66fbe46c: Verifying Checksum
044b66fbe46c: Download complete
bf30769d36e7: Verifying Checksum
bf30769d36e7: Download complete
a8ac7f6c67ab: Pull complete
bf30769d36e7: Pull complete
044b66fbe46c: Pull complete
b5c41a28e83f: Pull complete
21d94e6c63f0: Pull complete
23c8544b7435: Pull complete
Digest: sha256:50659db2900c615ca13c72eaa8c5e88a8f9a76b808a7b29721f818ec7b2fefb7
Status: Downloaded newer image for gitea.dooplex.hu/admin/felhom-controller:0.230.0
gitea.dooplex.hu/admin/felhom-controller:0.230.0
[golden] asking the controller which infra images it manages …
[golden] baking infra images (4): traefik:v3.6.7 cloudflare/cloudflared:2026.6.0 gtstef/filebrowser:1.3.3-stable gitea.dooplex.hu/admin/felhom-samba:1.1.0 …
v3.6.7: Pulling from library/traefik
589002ba0eae: Pulling fs layer
ef63511ea6cc: Pulling fs layer
0738e5cb835e: Pulling fs layer
3e6813f70c64: Pulling fs layer
3e6813f70c64: Waiting
589002ba0eae: Verifying Checksum
589002ba0eae: Download complete
ef63511ea6cc: Verifying Checksum
ef63511ea6cc: Download complete
3e6813f70c64: Verifying Checksum
3e6813f70c64: Download complete
0738e5cb835e: Verifying Checksum
0738e5cb835e: Download complete
589002ba0eae: Pull complete
ef63511ea6cc: Pull complete
0738e5cb835e: Pull complete
3e6813f70c64: Pull complete
Digest: sha256:a9890c898f379c1905ee5b28342f6b408dc863f08db2dab20e46c267d1ff463a
Status: Downloaded newer image for traefik:v3.6.7
docker.io/library/traefik:v3.6.7
2026.6.0: Pulling from cloudflare/cloudflared
47de5dd0b812: Pulling fs layer
c172f21841df: Pulling fs layer
99515e7b4d35: Pulling fs layer
99ba982a9142: Pulling fs layer
d6b1b89eccac: Pulling fs layer
2780920e5dbf: Pulling fs layer
7c12895b777b: Pulling fs layer
3214acf345c0: Pulling fs layer
52630fc75a18: Pulling fs layer
dd64bf2dd177: Pulling fs layer
b839dfae01f6: Pulling fs layer
ebddc55facdc: Pulling fs layer
bdfd7f7e5bf6: Pulling fs layer
2d4d7adf6272: Pulling fs layer
40008157d8d2: Pulling fs layer
bd8962e29291: Pulling fs layer
cac2ae0193cb: Pulling fs layer
74d1dac84ecc: Pulling fs layer
dd64bf2dd177: Waiting
b839dfae01f6: Waiting
ebddc55facdc: Waiting
bdfd7f7e5bf6: Waiting
2d4d7adf6272: Waiting
40008157d8d2: Waiting
bd8962e29291: Waiting
cac2ae0193cb: Waiting
74d1dac84ecc: Waiting
99ba982a9142: Waiting
d6b1b89eccac: Waiting
2780920e5dbf: Waiting
7c12895b777b: Waiting
3214acf345c0: Waiting
52630fc75a18: Waiting
47de5dd0b812: Verifying Checksum
47de5dd0b812: Download complete
c172f21841df: Verifying Checksum
c172f21841df: Download complete
99515e7b4d35: Verifying Checksum
99515e7b4d35: Download complete
99ba982a9142: Verifying Checksum
99ba982a9142: Download complete
47de5dd0b812: Pull complete
d6b1b89eccac: Verifying Checksum
d6b1b89eccac: Download complete
2780920e5dbf: Verifying Checksum
2780920e5dbf: Download complete
7c12895b777b: Verifying Checksum
7c12895b777b: Download complete
3214acf345c0: Verifying Checksum
3214acf345c0: Download complete
52630fc75a18: Verifying Checksum
52630fc75a18: Download complete
dd64bf2dd177: Verifying Checksum
dd64bf2dd177: Download complete
b839dfae01f6: Verifying Checksum
b839dfae01f6: Download complete
ebddc55facdc: Verifying Checksum
ebddc55facdc: Download complete
bdfd7f7e5bf6: Verifying Checksum
bdfd7f7e5bf6: Download complete
c172f21841df: Pull complete
40008157d8d2: Verifying Checksum
40008157d8d2: Download complete
bd8962e29291: Verifying Checksum
bd8962e29291: Download complete
2d4d7adf6272: Verifying Checksum
2d4d7adf6272: Download complete
cac2ae0193cb: Verifying Checksum
cac2ae0193cb: Download complete
74d1dac84ecc: Verifying Checksum
74d1dac84ecc: Download complete
99515e7b4d35: Pull complete
99ba982a9142: Pull complete
d6b1b89eccac: Pull complete
2780920e5dbf: Pull complete
7c12895b777b: Pull complete
3214acf345c0: Pull complete
52630fc75a18: Pull complete
dd64bf2dd177: Pull complete
b839dfae01f6: Pull complete
ebddc55facdc: Pull complete
bdfd7f7e5bf6: Pull complete
2d4d7adf6272: Pull complete
40008157d8d2: Pull complete
bd8962e29291: Pull complete
cac2ae0193cb: Pull complete
74d1dac84ecc: Pull complete
Digest: sha256:ba461b8aa9c042156dbd39c38657fe7431bafa063220eab8d5330a523863da9f
Status: Downloaded newer image for cloudflare/cloudflared:2026.6.0
docker.io/cloudflare/cloudflared:2026.6.0
1.3.3-stable: Pulling from gtstef/filebrowser
6a0ac1617861: Pulling fs layer
ef8806083e82: Pulling fs layer
b74107c861c7: Pulling fs layer
adc935def003: Pulling fs layer
4f4fb700ef54: Pulling fs layer
18695ccc900a: Pulling fs layer
45d119d5c397: Pulling fs layer
dac52db4fc51: Pulling fs layer
6d598f86b2f2: Pulling fs layer
8aa349c8396c: Pulling fs layer
45d119d5c397: Waiting
dac52db4fc51: Waiting
6d598f86b2f2: Waiting
8aa349c8396c: Waiting
adc935def003: Waiting
4f4fb700ef54: Waiting
18695ccc900a: Waiting
6a0ac1617861: Verifying Checksum
6a0ac1617861: Download complete
adc935def003: Verifying Checksum
adc935def003: Download complete
4f4fb700ef54: Verifying Checksum
4f4fb700ef54: Download complete
b74107c861c7: Verifying Checksum
b74107c861c7: Download complete
6a0ac1617861: Pull complete
45d119d5c397: Verifying Checksum
45d119d5c397: Download complete
ef8806083e82: Verifying Checksum
ef8806083e82: Download complete
6d598f86b2f2: Verifying Checksum
6d598f86b2f2: Download complete
dac52db4fc51: Verifying Checksum
dac52db4fc51: Download complete
8aa349c8396c: Verifying Checksum
8aa349c8396c: Download complete
18695ccc900a: Verifying Checksum
18695ccc900a: Download complete
ef8806083e82: Pull complete
b74107c861c7: Pull complete
adc935def003: Pull complete
4f4fb700ef54: Pull complete
18695ccc900a: Pull complete
45d119d5c397: Pull complete
dac52db4fc51: Pull complete
6d598f86b2f2: Pull complete
8aa349c8396c: Pull complete
Digest: sha256:eb3733681db8757412632c61a99ad656f0d94ed6781bb2ea114b4d70babab78c
Status: Downloaded newer image for gtstef/filebrowser:1.3.3-stable
docker.io/gtstef/filebrowser:1.3.3-stable
1.1.0: Pulling from admin/felhom-samba
897d797d2723: Pulling fs layer
3051591aa250: Pulling fs layer
ce57a3f93416: Pulling fs layer
fb94eeec2fe1: Pulling fs layer
fb94eeec2fe1: Waiting
ce57a3f93416: Verifying Checksum
ce57a3f93416: Download complete
fb94eeec2fe1: Verifying Checksum
fb94eeec2fe1: Download complete
897d797d2723: Verifying Checksum
897d797d2723: Download complete
3051591aa250: Verifying Checksum
3051591aa250: Download complete
897d797d2723: Pull complete
3051591aa250: Pull complete
ce57a3f93416: Pull complete
fb94eeec2fe1: Pull complete
Digest: sha256:1c17c09422bec0366d7cf0e0fcfc1486ba6c90334a0a5d5c851073a9342f8f10
Status: Downloaded newer image for gitea.dooplex.hu/admin/felhom-samba:1.1.0
gitea.dooplex.hu/admin/felhom-samba:1.1.0
[golden] baking the controller-bootstrap unit (deploys the BAKED controller from the config mount) …
Created symlink '/etc/systemd/system/multi-user.target.wants/felhom-controller-bootstrap.service' → '/etc/systemd/system/felhom-controller-bootstrap.service'.
[golden] baking the controller-bootstrap PATH unit (starts the service on bootstrap-mount hot-plug — B1) …
Created symlink '/etc/systemd/system/multi-user.target.wants/felhom-controller-bootstrap.path' → '/etc/systemd/system/felhom-controller-bootstrap.path'.
[golden] baking the first-boot SSH host-key regeneration unit (F3) …
Created symlink '/etc/systemd/system/multi-user.target.wants/felhom-regen-hostkeys.service' → '/etc/systemd/system/felhom-regen-hostkeys.service'.
[golden] identity-clean + minimize …
[golden] stop + archive …
INFO: including mount point rootfs ('/') in backup
INFO: including mount point mp0 ('/var/lib/felhom') in backup
INFO: archive file size: 627MB
INFO: Finished Backup of VM 9100 (00:00:41)
[golden] DONE. golden archive volid: local:backup/vzdump-lxc-9100-2026_08_31-16_15_14.tar.zst (rootfs 32G + ONE data volume 24G @ /var/lib/felhom, all in the archive)
[golden] publishing golden (657873700 bytes, sha256 9287f7cef5f13166…) → https://gitea.dooplex.hu/api/packages/admin/generic/felhom-golden/0.230.0/golden.tar.zst
[golden] pre-delete existing: HTTP 404 (404/204 expected)
[golden] upload OK (HTTP 201)
GOLDEN_VERSION=0.230.0
GOLDEN_SHA256=9287f7cef5f13166276e8406005e3f28004004510c5184f1c1c7377f7aafad2e
[golden] Record in the hub operator UI (Configs → Day-0 artifacts): golden 0.230.0 / 9287f7cef5f13166276e8406005e3f28004004510c5184f1c1c7377f7aafad2e
[golden] (the build guest 9100 is stopped; destroy it with: pct destroy 9100 --purge)
@@ -0,0 +1,13 @@
+ pct destroy 9100 --purge
Logical volume "vm-9100-disk-0" successfully removed.
Logical volume "vm-9100-disk-1" successfully removed.
purging CT 9100 from related configurations..
+ shred -u /root/.gitea-token /root/bake-run.sh /root/build-golden.sh /root/bake.log
+ ls -A /root
+ grep -iE 'gitea-token|bake|build-golden'
leftovers-rc=1 (1 = clean)
+ echo 'leftovers-rc=1 (1 = clean)'
+ pct list
Snapshot list:
ID TAG VM_SIZE DATE VM_CLOCK ICOUNT
1 virgin 0 B 2026-07-03 14:12:15 0000:00:00.000 0
@@ -0,0 +1,4 @@
=== ROUND TRIP: download the published bytes and hash THEM, not the local file ===
http=200 bytes=657873700
downloaded sha256: 9287f7cef5f13166276e8406005e3f28004004510c5184f1c1c7377f7aafad2e
bake printed : 9287f7cef5f13166276e8406005e3f28004004510c5184f1c1c7377f7aafad2e
@@ -0,0 +1,9 @@
=== VOUCH: the THREE fields moved together (R-216 shape avoided) ===
golden_version 0.229.0 -> 0.230.0
agent_version 0.130.0 -> 0.130.0 (UNCHANGED, already >= MinAgent)
min_agent 0.129.0 -> 0.129.0 (UNCHANGED, v0.230.0 CHANGELOG says MinAgent: 0.129.0)
min_agent (0.129.0) <= agent_version (0.130.0): the R-216 hold is satisfied
POST http=303
--- location header (read from -D, never from %{redirect_url} — R-132) ---
Location: /configuration?flash=artifacts_set
@@ -0,0 +1,284 @@
<!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?v=0.109.0">
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 6v6l4 2" /></symbol>
<symbol id="i-boxes" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z" /> <path d="m7 16.5-4.74-2.85" /> <path d="m7 16.5 5-3" /> <path d="M7 16.5v5.17" /> <path d="M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z" /> <path d="m17 16.5-5-3" /> <path d="m17 16.5 4.74-2.85" /> <path d="M17 16.5v5.17" /> <path d="M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z" /> <path d="M12 8 7.26 5.15" /> <path d="m12 8 4.74-2.85" /> <path d="M12 13.5V8" /></symbol>
<symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /> <path d="M16 3.128a4 4 0 0 1 0 7.744" /> <path d="M22 21v-2a4 4 0 0 0-3-3.87" /> <circle cx="9" cy="7" r="4" /></symbol>
</svg>
<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>
<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).
<strong>Saving takes effect immediately</strong> — boxes below the floor update on their next
report, no customer action. Blank = no global floor. This setting is independent of the Day-0
artifact manifest below.
</p>
<p style="margin: 0 0 0.75rem; font-size: 0.85em;">
Effective floor:
<code>v0.229.0</code>
<span style="color: #cbd5e1;">— source: <strong>DB (hub_settings)</strong>; env fallback would be <code>v0.120.0</code></span>
</p>
<form id="global-floor-form" method="POST" action="/configuration/global-floor" style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
<input type="hidden" name="_csrf" value="">
<input type="text" id="global-floor-input" name="min_controller_version" value="0.229.0" placeholder="e.g. 0.86.0 (blank = clear DB override)" style="padding: 0.3em 0.5em; width: 16em;">
<button class="btn btn-sm" type="button" onclick="confirmGlobalFloor()">Save global floor…</button>
<span style="font-size: 0.85em; color: #cbd5e1;">DB override: <code>v0.229.0</code></span>
</form>
<div id="global-floor-confirm" style="display: none; margin-top: 0.75rem; padding: 0.75rem; border: 1px solid #7c3f00; background: #241a0a; border-radius: 6px; max-width: 44em;">
<p id="global-floor-impact" style="margin: 0 0 0.5rem; font-size: 0.9em;">…</p>
<p style="margin: 0 0 0.5rem; font-size: 0.85em; color: #cbd5e1;">Type the version again to confirm (or <code>CLEAR</code> to remove the DB override):</p>
<input type="text" id="global-floor-confirm-input" placeholder="retype the version…" style="padding: 0.3em 0.5em; width: 16em;">
<button class="btn btn-sm" type="button" onclick="submitGlobalFloor()">Confirm &amp; apply</button>
<button class="btn btn-sm btn-ghost" type="button" onclick="document.getElementById('global-floor-confirm').style.display='none';">Cancel</button>
<p id="global-floor-confirm-err" style="margin: 0.4em 0 0; font-size: 0.8em; color: #f87171;"></p>
</div>
<script>
function confirmGlobalFloor() {
var v = document.getElementById('global-floor-input').value.trim();
var box = document.getElementById('global-floor-confirm');
var impact = document.getElementById('global-floor-impact');
document.getElementById('global-floor-confirm-input').value = '';
document.getElementById('global-floor-confirm-err').textContent = '';
box.style.display = 'block';
if (v === '') {
impact.textContent = 'This will CLEAR the DB floor override (the box falls back to the env default). Type CLEAR to confirm.';
return;
}
impact.textContent = 'Checking blast radius…';
fetch('/configuration/global-floor/impact?v=' + encodeURIComponent(v))
.then(function(r){ return r.json(); })
.then(function(d){
if (!d.valid) { impact.textContent = 'Invalid version — use X.Y.Z.'; return; }
impact.textContent = 'Saving the minimum version v' + d.version +
' takes effect immediately — currently ' + d.below +
' box(es) are below this version and would update on their next report.';
})
.catch(function(){ impact.textContent = 'Could not compute the blast radius; proceed with caution.'; });
}
function submitGlobalFloor() {
var v = document.getElementById('global-floor-input').value.trim();
var typed = document.getElementById('global-floor-confirm-input').value.trim();
var err = document.getElementById('global-floor-confirm-err');
var expected = (v === '') ? 'CLEAR' : v;
if (typed !== expected) { err.textContent = 'Confirmation does not match (' + expected + ').'; return; }
document.getElementById('global-floor-form').submit();
}
</script>
</section>
<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;">
<input type="hidden" name="_csrf" value="">
<label style="font-size: 0.9em; color: #cbd5e1;">Agent</label>
<select name="agent_version" id="agent_version" onchange="syncArtifactSha('agent')" style="padding: 0.3em 0.5em;">
<option value="" data-sha="">— none —</option>
<option value="0.130.0" data-sha="a56a92a7bd68f5b46736eaec4806c3d26c16ccb35118c4ac0e3d8094eaefabc3" selected>0.130.0</option>
<option value="0.129.0" data-sha="53a54f0620afbd6d4a1b86607e2a84dfbe7a290f44ed38a9485d6d971eecde8d" >0.129.0</option>
<option value="0.128.0" data-sha="c6eba73bf9b9ad6980cfef57bfb3db31581abc9d643de2ff50d4254576fc1a59" >0.128.0</option>
<option value="0.127.0" data-sha="f0d2c89311f03fd2b9ab6ae242e09a724ff8ea1fd2214759bae247c5db7dbf72" >0.127.0</option>
<option value="0.126.0" data-sha="7ecf8e9cdba237bc2d81003440095eace6418d00c3a485f3ca77742a25e4a93b" >0.126.0</option>
<option value="0.125.0" data-sha="f7d8339b53d92a6c45be7eaf189469a041b6b00b758a64511c0479beae7016b3" >0.125.0</option>
<option value="0.124.1" data-sha="5c279bda64cdec8cbd76f8a800bcd602063bb17175f406b225775e7dece3a21c" >0.124.1</option>
<option value="0.124.0" data-sha="5e4179383bc838a7ad360efcfab5e8f02a2939ed8bcc4bc68ffd85ba02a5c9a4" >0.124.0</option>
<option value="0.123.0" data-sha="74910135ac4feb1b7f0ad4dbd1541d965cbc0fe70d4f47b62ebf7e4bfb962453" >0.123.0</option>
<option value="0.122.0" data-sha="d5f294e56c1ef59055e8e87fb9135aa477632dbbc56a5d4bff46bbd0466c1edf" >0.122.0</option>
<option value="0.121.1" data-sha="afaeeb509d1ed70d6e6bebac0393a3cd5be59d51e3db9ff96ef8524bd78546d7" >0.121.1</option>
<option value="0.121.0" data-sha="b2128f3cd4539225a2842f541f56ffaf5390b1d97f3f3a80076ec5f53dbc7d7a" >0.121.0</option>
</select>
<input type="text" name="agent_sha256" id="agent_sha256" value="a56a92a7bd68f5b46736eaec4806c3d26c16ccb35118c4ac0e3d8094eaefabc3" readonly placeholder="64-hex sha256 (blank = none)" style="padding: 0.3em 0.5em; font-family: monospace; opacity: 0.7;">
<label style="font-size: 0.9em; color: #cbd5e1;">Golden</label>
<select name="golden_version" id="golden_version" onchange="syncArtifactSha('golden')" style="padding: 0.3em 0.5em;">
<option value="" data-sha="">— none —</option>
<option value="0.230.0" data-sha="9287f7cef5f13166276e8406005e3f28004004510c5184f1c1c7377f7aafad2e" selected>0.230.0</option>
<option value="0.229.0" data-sha="39aa886df77b21757aef3b298a389343dc0df5134bb0f14e8f92a451d7bdae87" >0.229.0</option>
<option value="0.228.0" data-sha="76a3a98b9e7cc23bf8ae51b38a6272f576df285cb34cd22235ac3f06a31e53ec" >0.228.0</option>
<option value="0.227.1" data-sha="66754491dc9bd0130ef8ded9562f63c53a5ffdcfd91baa551141e55fa083ea32" >0.227.1</option>
<option value="0.226.1" data-sha="70ed8e9377dec22a9b493e55f222b0e25a49d7f3caec8c506e0412fd6baefe69" >0.226.1</option>
<option value="0.223.0" data-sha="9eaf39ac39219b42ec9e6cbf890275febcdcc6f53325fe0c0f591d3431044f17" >0.223.0</option>
<option value="0.222.0" data-sha="19f5904f53792684f046ec0bc25426645cb87ad73d5cfc6c03639d9f82706037" >0.222.0</option>
<option value="0.221.1" data-sha="1c8bf6cf08cadabeca6331f38360d905e867c235067cd10c2716915b6e6df089" >0.221.1</option>
</select>
<input type="text" name="golden_sha256" id="golden_sha256" value="9287f7cef5f13166276e8406005e3f28004004510c5184f1c1c7377f7aafad2e" readonly placeholder="64-hex sha256 (blank = none)" style="padding: 0.3em 0.5em; font-family: monospace; opacity: 0.7;">
<label style="font-size: 0.9em; color: #cbd5e1;">Min agent</label>
<input type="text" name="min_agent" value="0.129.0" placeholder="e.g. 0.81.0 (blank = uncoupled)" style="padding: 0.3em 0.5em;">
<span style="font-size: 0.8em; color: #94a6bf;">The golden's controller CHANGELOG <code>MinAgent:</code>. The hub HOLDS the floor for any box whose agent is below this — blank = uncoupled release, no gating.</span>
<label style="font-size: 0.9em; color: #cbd5e1;">PBS wrapper</label>
<input type="text" name="wrapper_sha256" value="104db0a4401f65bbc476e82bfb1796433bcb36f8f8cce69efb3bb5c40fcb16b3" placeholder="64-hex sha256 (blank = not vouched)" style="grid-column: 2 / 4; padding: 0.3em 0.5em; font-family: monospace;">
<span></span>
<span style="grid-column: 2 / 4; font-size: 0.8em; color: #94a6bf;">sha256 of <code>configs/felhom-pbs-apply</code> (R-50b). Unlike the agent and golden this root-owned wrapper is installed from <code>raw/branch/main</code> — unversioned and unpinned. Recording it here does not fix the channel; it makes host drift <em>visible</em>: agents report the installed file's hash and a mismatch is surfaced on the host page.</span>
<span></span><span></span>
<button class="btn btn-sm" type="submit" style="justify-self: start;">Save artifact manifest</button>
</form>
<script>
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>
<section class="card">
<h3 style="margin-top: 0;">Login password</h3>
<p class="text-muted" style="margin: 0 0 0.75rem; font-size: 0.85em;">
The password for signing in to this hub UI. <strong>Changing it takes effect immediately</strong>
for the next sign-in — your current session stays logged in. Enter your current password to confirm.
If you ever lose it, the deployment ConfigMap (<code>auth.password_hash</code>) remains the reset path.
</p>
<form method="POST" action="/configuration/password" style="display: grid; grid-template-columns: auto 20em; gap: 0.5rem; align-items: center; max-width: 40em;"
onsubmit="return felhomCheckNewPw(this);">
<input type="hidden" name="_csrf" value="">
<label style="font-size: 0.9em; color: #cbd5e1;">Current password</label>
<input type="password" name="current_password" autocomplete="current-password" required style="padding: 0.3em 0.5em;">
<label style="font-size: 0.9em; color: #cbd5e1;">New password</label>
<input type="password" id="new_password" name="new_password" autocomplete="new-password" minlength="8" maxlength="72" required style="padding: 0.3em 0.5em;">
<label style="font-size: 0.9em; color: #cbd5e1;">Confirm new password</label>
<input type="password" id="confirm_password" name="confirm_password" autocomplete="new-password" minlength="8" maxlength="72" required style="padding: 0.3em 0.5em;">
<span></span>
<span>
<button class="btn btn-sm" type="submit">Change password</button>
<span id="pw-client-err" style="margin-left: 0.6em; font-size: 0.8em; color: #f87171;"></span>
</span>
</form>
<script>
function felhomCheckNewPw(form) {
var a = form.new_password.value;
var b = form.confirm_password.value;
var err = document.getElementById('pw-client-err');
err.textContent = '';
if (a.length < 8) { err.textContent = 'New password must be at least 8 characters.'; return false; }
if (a !== b) { err.textContent = 'New password and confirmation do not match.'; return false; }
return true;
}
</script>
</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">211</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;">2026-08-30T16:51:40Z</span>
</div>
</div>
<form method="POST" action="/configuration" style="margin-top: 1rem;">
<input type="hidden" name="_csrf" value="">
<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)">0.109.0</span>
</footer>
</div>
</body>
</html>
@@ -0,0 +1,9 @@
=== PERSISTED STATE, re-read from the page (a 303 + success flash is not proof) ===
agent_version SELECTED = 0.130.0 sha=a56a92a7bd68f5b46736eaec...
golden_version SELECTED = 0.230.0 sha=9287f7cef5f13166276e8406...
min_agent value = 0.129.0
golden_sha256 value = 9287f7cef5f13166276e8406005e3f28004004510c5184f1c1c7377f7aafad2e
agent_sha256 value = a56a92a7bd68f5b46736eaec4806c3d26c16ccb35118c4ac0e3d8094eaefabc3
wrapper_sha256 value = 104db0a4401f65bbc476e82bfb1796433bcb36f8f8cce69efb3bb5c40fcb16b3
refusal banner present? False
@@ -0,0 +1,3 @@
newest released controller : 0.230.0 (## v0.230.0 — a poorer copy must never delete a richer one (2026-08-31, R-403))
newest golden baked : 0.230.0 (documentation/tests/golden-0.230.0-2026-08-31)
golden currency gate OK — the newest released controller has a golden (NOTE: this checks the BAKE, not the vouch — see the module docstring)
@@ -0,0 +1,284 @@
<!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?v=0.109.0">
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 6v6l4 2" /></symbol>
<symbol id="i-boxes" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z" /> <path d="m7 16.5-4.74-2.85" /> <path d="m7 16.5 5-3" /> <path d="M7 16.5v5.17" /> <path d="M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z" /> <path d="m17 16.5-5-3" /> <path d="m17 16.5 4.74-2.85" /> <path d="M17 16.5v5.17" /> <path d="M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z" /> <path d="M12 8 7.26 5.15" /> <path d="m12 8 4.74-2.85" /> <path d="M12 13.5V8" /></symbol>
<symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /> <path d="M16 3.128a4 4 0 0 1 0 7.744" /> <path d="M22 21v-2a4 4 0 0 0-3-3.87" /> <circle cx="9" cy="7" r="4" /></symbol>
</svg>
<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>
<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).
<strong>Saving takes effect immediately</strong> — boxes below the floor update on their next
report, no customer action. Blank = no global floor. This setting is independent of the Day-0
artifact manifest below.
</p>
<p style="margin: 0 0 0.75rem; font-size: 0.85em;">
Effective floor:
<code>v0.230.0</code>
<span style="color: #cbd5e1;">— source: <strong>DB (hub_settings)</strong>; env fallback would be <code>v0.120.0</code></span>
</p>
<form id="global-floor-form" method="POST" action="/configuration/global-floor" style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
<input type="hidden" name="_csrf" value="">
<input type="text" id="global-floor-input" name="min_controller_version" value="0.230.0" placeholder="e.g. 0.86.0 (blank = clear DB override)" style="padding: 0.3em 0.5em; width: 16em;">
<button class="btn btn-sm" type="button" onclick="confirmGlobalFloor()">Save global floor…</button>
<span style="font-size: 0.85em; color: #cbd5e1;">DB override: <code>v0.230.0</code></span>
</form>
<div id="global-floor-confirm" style="display: none; margin-top: 0.75rem; padding: 0.75rem; border: 1px solid #7c3f00; background: #241a0a; border-radius: 6px; max-width: 44em;">
<p id="global-floor-impact" style="margin: 0 0 0.5rem; font-size: 0.9em;">…</p>
<p style="margin: 0 0 0.5rem; font-size: 0.85em; color: #cbd5e1;">Type the version again to confirm (or <code>CLEAR</code> to remove the DB override):</p>
<input type="text" id="global-floor-confirm-input" placeholder="retype the version…" style="padding: 0.3em 0.5em; width: 16em;">
<button class="btn btn-sm" type="button" onclick="submitGlobalFloor()">Confirm &amp; apply</button>
<button class="btn btn-sm btn-ghost" type="button" onclick="document.getElementById('global-floor-confirm').style.display='none';">Cancel</button>
<p id="global-floor-confirm-err" style="margin: 0.4em 0 0; font-size: 0.8em; color: #f87171;"></p>
</div>
<script>
function confirmGlobalFloor() {
var v = document.getElementById('global-floor-input').value.trim();
var box = document.getElementById('global-floor-confirm');
var impact = document.getElementById('global-floor-impact');
document.getElementById('global-floor-confirm-input').value = '';
document.getElementById('global-floor-confirm-err').textContent = '';
box.style.display = 'block';
if (v === '') {
impact.textContent = 'This will CLEAR the DB floor override (the box falls back to the env default). Type CLEAR to confirm.';
return;
}
impact.textContent = 'Checking blast radius…';
fetch('/configuration/global-floor/impact?v=' + encodeURIComponent(v))
.then(function(r){ return r.json(); })
.then(function(d){
if (!d.valid) { impact.textContent = 'Invalid version — use X.Y.Z.'; return; }
impact.textContent = 'Saving the minimum version v' + d.version +
' takes effect immediately — currently ' + d.below +
' box(es) are below this version and would update on their next report.';
})
.catch(function(){ impact.textContent = 'Could not compute the blast radius; proceed with caution.'; });
}
function submitGlobalFloor() {
var v = document.getElementById('global-floor-input').value.trim();
var typed = document.getElementById('global-floor-confirm-input').value.trim();
var err = document.getElementById('global-floor-confirm-err');
var expected = (v === '') ? 'CLEAR' : v;
if (typed !== expected) { err.textContent = 'Confirmation does not match (' + expected + ').'; return; }
document.getElementById('global-floor-form').submit();
}
</script>
</section>
<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;">
<input type="hidden" name="_csrf" value="">
<label style="font-size: 0.9em; color: #cbd5e1;">Agent</label>
<select name="agent_version" id="agent_version" onchange="syncArtifactSha('agent')" style="padding: 0.3em 0.5em;">
<option value="" data-sha="">— none —</option>
<option value="0.130.0" data-sha="a56a92a7bd68f5b46736eaec4806c3d26c16ccb35118c4ac0e3d8094eaefabc3" selected>0.130.0</option>
<option value="0.129.0" data-sha="53a54f0620afbd6d4a1b86607e2a84dfbe7a290f44ed38a9485d6d971eecde8d" >0.129.0</option>
<option value="0.128.0" data-sha="c6eba73bf9b9ad6980cfef57bfb3db31581abc9d643de2ff50d4254576fc1a59" >0.128.0</option>
<option value="0.127.0" data-sha="f0d2c89311f03fd2b9ab6ae242e09a724ff8ea1fd2214759bae247c5db7dbf72" >0.127.0</option>
<option value="0.126.0" data-sha="7ecf8e9cdba237bc2d81003440095eace6418d00c3a485f3ca77742a25e4a93b" >0.126.0</option>
<option value="0.125.0" data-sha="f7d8339b53d92a6c45be7eaf189469a041b6b00b758a64511c0479beae7016b3" >0.125.0</option>
<option value="0.124.1" data-sha="5c279bda64cdec8cbd76f8a800bcd602063bb17175f406b225775e7dece3a21c" >0.124.1</option>
<option value="0.124.0" data-sha="5e4179383bc838a7ad360efcfab5e8f02a2939ed8bcc4bc68ffd85ba02a5c9a4" >0.124.0</option>
<option value="0.123.0" data-sha="74910135ac4feb1b7f0ad4dbd1541d965cbc0fe70d4f47b62ebf7e4bfb962453" >0.123.0</option>
<option value="0.122.0" data-sha="d5f294e56c1ef59055e8e87fb9135aa477632dbbc56a5d4bff46bbd0466c1edf" >0.122.0</option>
<option value="0.121.1" data-sha="afaeeb509d1ed70d6e6bebac0393a3cd5be59d51e3db9ff96ef8524bd78546d7" >0.121.1</option>
<option value="0.121.0" data-sha="b2128f3cd4539225a2842f541f56ffaf5390b1d97f3f3a80076ec5f53dbc7d7a" >0.121.0</option>
</select>
<input type="text" name="agent_sha256" id="agent_sha256" value="a56a92a7bd68f5b46736eaec4806c3d26c16ccb35118c4ac0e3d8094eaefabc3" readonly placeholder="64-hex sha256 (blank = none)" style="padding: 0.3em 0.5em; font-family: monospace; opacity: 0.7;">
<label style="font-size: 0.9em; color: #cbd5e1;">Golden</label>
<select name="golden_version" id="golden_version" onchange="syncArtifactSha('golden')" style="padding: 0.3em 0.5em;">
<option value="" data-sha="">— none —</option>
<option value="0.230.0" data-sha="9287f7cef5f13166276e8406005e3f28004004510c5184f1c1c7377f7aafad2e" selected>0.230.0</option>
<option value="0.229.0" data-sha="39aa886df77b21757aef3b298a389343dc0df5134bb0f14e8f92a451d7bdae87" >0.229.0</option>
<option value="0.228.0" data-sha="76a3a98b9e7cc23bf8ae51b38a6272f576df285cb34cd22235ac3f06a31e53ec" >0.228.0</option>
<option value="0.227.1" data-sha="66754491dc9bd0130ef8ded9562f63c53a5ffdcfd91baa551141e55fa083ea32" >0.227.1</option>
<option value="0.226.1" data-sha="70ed8e9377dec22a9b493e55f222b0e25a49d7f3caec8c506e0412fd6baefe69" >0.226.1</option>
<option value="0.223.0" data-sha="9eaf39ac39219b42ec9e6cbf890275febcdcc6f53325fe0c0f591d3431044f17" >0.223.0</option>
<option value="0.222.0" data-sha="19f5904f53792684f046ec0bc25426645cb87ad73d5cfc6c03639d9f82706037" >0.222.0</option>
<option value="0.221.1" data-sha="1c8bf6cf08cadabeca6331f38360d905e867c235067cd10c2716915b6e6df089" >0.221.1</option>
</select>
<input type="text" name="golden_sha256" id="golden_sha256" value="9287f7cef5f13166276e8406005e3f28004004510c5184f1c1c7377f7aafad2e" readonly placeholder="64-hex sha256 (blank = none)" style="padding: 0.3em 0.5em; font-family: monospace; opacity: 0.7;">
<label style="font-size: 0.9em; color: #cbd5e1;">Min agent</label>
<input type="text" name="min_agent" value="0.129.0" placeholder="e.g. 0.81.0 (blank = uncoupled)" style="padding: 0.3em 0.5em;">
<span style="font-size: 0.8em; color: #94a6bf;">The golden's controller CHANGELOG <code>MinAgent:</code>. The hub HOLDS the floor for any box whose agent is below this — blank = uncoupled release, no gating.</span>
<label style="font-size: 0.9em; color: #cbd5e1;">PBS wrapper</label>
<input type="text" name="wrapper_sha256" value="104db0a4401f65bbc476e82bfb1796433bcb36f8f8cce69efb3bb5c40fcb16b3" placeholder="64-hex sha256 (blank = not vouched)" style="grid-column: 2 / 4; padding: 0.3em 0.5em; font-family: monospace;">
<span></span>
<span style="grid-column: 2 / 4; font-size: 0.8em; color: #94a6bf;">sha256 of <code>configs/felhom-pbs-apply</code> (R-50b). Unlike the agent and golden this root-owned wrapper is installed from <code>raw/branch/main</code> — unversioned and unpinned. Recording it here does not fix the channel; it makes host drift <em>visible</em>: agents report the installed file's hash and a mismatch is surfaced on the host page.</span>
<span></span><span></span>
<button class="btn btn-sm" type="submit" style="justify-self: start;">Save artifact manifest</button>
</form>
<script>
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>
<section class="card">
<h3 style="margin-top: 0;">Login password</h3>
<p class="text-muted" style="margin: 0 0 0.75rem; font-size: 0.85em;">
The password for signing in to this hub UI. <strong>Changing it takes effect immediately</strong>
for the next sign-in — your current session stays logged in. Enter your current password to confirm.
If you ever lose it, the deployment ConfigMap (<code>auth.password_hash</code>) remains the reset path.
</p>
<form method="POST" action="/configuration/password" style="display: grid; grid-template-columns: auto 20em; gap: 0.5rem; align-items: center; max-width: 40em;"
onsubmit="return felhomCheckNewPw(this);">
<input type="hidden" name="_csrf" value="">
<label style="font-size: 0.9em; color: #cbd5e1;">Current password</label>
<input type="password" name="current_password" autocomplete="current-password" required style="padding: 0.3em 0.5em;">
<label style="font-size: 0.9em; color: #cbd5e1;">New password</label>
<input type="password" id="new_password" name="new_password" autocomplete="new-password" minlength="8" maxlength="72" required style="padding: 0.3em 0.5em;">
<label style="font-size: 0.9em; color: #cbd5e1;">Confirm new password</label>
<input type="password" id="confirm_password" name="confirm_password" autocomplete="new-password" minlength="8" maxlength="72" required style="padding: 0.3em 0.5em;">
<span></span>
<span>
<button class="btn btn-sm" type="submit">Change password</button>
<span id="pw-client-err" style="margin-left: 0.6em; font-size: 0.8em; color: #f87171;"></span>
</span>
</form>
<script>
function felhomCheckNewPw(form) {
var a = form.new_password.value;
var b = form.confirm_password.value;
var err = document.getElementById('pw-client-err');
err.textContent = '';
if (a.length < 8) { err.textContent = 'New password must be at least 8 characters.'; return false; }
if (a !== b) { err.textContent = 'New password and confirmation do not match.'; return false; }
return true;
}
</script>
</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">211</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;">2026-08-30T16:51:40Z</span>
</div>
</div>
<form method="POST" action="/configuration" style="margin-top: 1rem;">
<input type="hidden" name="_csrf" value="">
<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)">0.109.0</span>
</footer>
</div>
</body>
</html>
@@ -0,0 +1,9 @@
=== BEFORE: min_controller_version ===
name="min_controller_version" value="0.229.0"
=== POST /configuration/global-floor 0.229.0 -> 0.230.0 ===
POST http=303
Location: /configuration?flash=floor_set
=== AFTER: re-read from the page, not from the flash ===
name="min_controller_version" value="0.230.0"
@@ -0,0 +1,30 @@
=== watching demo-felhom for the POSITIVE observable (the image tag changing), not for an absent error ===
t0=14:21:37Z starting tag=gitea.dooplex.hu/admin/felhom-controller:0.230.0
[1] 14:21:39Z gitea.dooplex.hu/admin/felhom-controller:0.230.0
SELF-UPDATE OBSERVED
--- running container ---
gitea.dooplex.hu/admin/felhom-controller:0.230.0 Up 8 seconds (healthy)
=== HONEST NOTE: my first poll already read 0.230.0 — the update landed between the floor POST and 14:21:37Z.
=== So the proof is the RESTART, not my polling. Container was 'Up 8 seconds' at first read.
--- controller startup line, demo-felhom ---
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
LANGUAGE = (unset),
LC_ALL = (unset),
LC_CTYPE = "UTF-8",
LC_NUMERIC = (unset),
--- the floor decision in the agent journal ---
Aug 31 16:21:29 demo-felhom sudo[2071156]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/pct exec 9201 -- docker image inspect gitea.dooplex.hu/admin/felhom-controller:0.230.0
Aug 31 16:21:29 demo-felhom sudo[2071169]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/pct exec 9201 -- tee /etc/felhom-controller-image
Aug 31 16:21:30 demo-felhom felhom-agent[2564828]: time=2026-08-31T16:21:30.589+02:00 level=INFO msg="controller-swap: image file written, restarting bootstrap" vmid=9201 target=gitea.dooplex.hu/admin/felhom-controller:0.230.0
Aug 31 16:21:30 demo-felhom sudo[2071175]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/pct exec 9201 -- systemctl restart felhom-controller-bootstrap.service
Aug 31 16:21:31 demo-felhom sudo[2071294]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/pct exec 9201 -- docker inspect -f '{{.State.Running}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}|{{.Config.Image}}|{{.RestartCount}}' felhom-controller
Aug 31 16:21:35 demo-felhom sudo[2071659]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/pct exec 9201 -- docker inspect -f '{{.State.Running}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}|{{.Config.Image}}|{{.RestartCount}}' felhom-controller
Aug 31 16:21:39 demo-felhom sudo[2071976]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/pct exec 9201 -- docker inspect -f '{{.State.Running}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}|{{.Config.Image}}|{{.RestartCount}}' felhom-controller
Aug 31 16:21:40 demo-felhom felhom-agent[2564828]: time=2026-08-31T16:21:40.156+02:00 level=INFO msg="controller-swap: new controller healthy" vmid=9201 target=gitea.dooplex.hu/admin/felhom-controller:0.230.0
--- both boxes now ---
demo-felhom: gitea.dooplex.hu/admin/felhom-controller:0.230.0 Up 23 seconds (healthy)
demo-hp : gitea.dooplex.hu/admin/felhom-controller:0.230.0 Up 2 hours (healthy)
@@ -0,0 +1,9 @@
=== re-download the PUBLISHED bytes and read INSIDE them ===
http=200 bytes=657873700
sha256 of the DOWNLOADED bytes: 9287f7cef5f13166276e8406005e3f28004004510c5184f1c1c7377f7aafad2e
--- the controller tag the DELIVERED artifact will start ---
gitea.dooplex.hu/admin/felhom-controller:0.230.0
--- and the baked controller image is present in the archive's docker store ---
19382
@@ -0,0 +1,140 @@
# Golden bake 0.230.0 — 2026-08-31
Baked, published, round-trip verified, **vouched**, and the fleet floor raised. `demo-felhom` picked
up the new controller **by itself, unattended** — the agent's own journal is the proof, not an absent
error.
**Why it was owed:** v0.230.0 is the build that stops a poorer copy deleting a richer one (R-403).
Until this bake the newest golden was **0.229.0 — the build that has the defect** — so every fresh
install and the whole fleet floor still carried it. `golden_currency_gate.py` had been RED since the
v0.230.0 release and was red at `dddcc80`, `6e550ae`, `130f7a6` and `32a4c35`.
## What was produced
| | |
|---|---|
| `GOLDEN_VERSION` | **0.230.0** |
| `GOLDEN_SHA256` | `9287f7cef5f13166276e8406005e3f28004004510c5184f1c1c7377f7aafad2e` |
| size | **657 873 700 B** |
| package URL | `…/api/packages/admin/generic/felhom-golden/0.230.0/golden.tar.zst` |
| baked controller | `gitea.dooplex.hu/admin/felhom-controller:0.230.0` |
| `MinAgent` | **0.129.0** — read from the controller `CHANGELOG.md` header, not assumed |
| script | `build-golden.sh v3.0.0`, sha256 `7b0fb5cf…73b6a1` on DooPlex |
| venue | the drill VM on DooPlex, reverted to `virgin` and **cold-booted** first (`pve-manager/9.2.2`) |
| template | `debian-13-standard_13.6-1_amd64.tar.zst`, after `pveam update` — the virgin snapshot's INDEX is stale too, and that failure reads as a bogus `400 no such template` |
| archive volid | `local:backup/vzdump-lxc-9100-2026_08_31-16_15_14.tar.zst` |
| bake wall clock | ~16:11 → 16:15 CEST, `vzdump` leg 00:00:41 |
**One thing the 0.229.0 bake did that this one did not**, said plainly rather than left as an
implied equivalence: the script's sha256 was **not** compared across the hop. It was `scp`'d
file→file and its DooPlex-side sha is recorded above; a corrupted copy would have failed the bake
rather than produced a wrong golden, but that is an argument, not a measurement.
## Acceptance markers — counted on the COMMITTED log, not eyeballed
```
docker OK (overlay2 : 1
including mount point rootfs : 1 (line 317)
including mount point mp0 : 1 (line 318) <- there is no mp1 since v3.0.0 (R-165/R-233)
upload OK (HTTP 201) : 1 (line 324)
--- must be ZERO ---
excluding : 0
FATAL : 0
```
**The zeroes are believable because the greps are shown to work on this file:** the same
`including mount point` pattern that returns 0 for `excluding` returns 2 real lines. An instrument
that can silently drop results is not a measurement.
## Three independent readers agreed before anything was vouched
1. **The bake itself** printed `GOLDEN_SHA256=9287f7ce…ad2e`.
2. **The round trip** — the published bytes downloaded back: `HTTP 200`, `657 873 700 B`,
sha256 `9287f7ce…ad2e`. The hash is of the **downloaded** bytes, never the local file.
3. **The hub's Day-0 dropdown**, a different code path, read `data-sha="9287f7cef5f13166276e…"`
straight from Gitea.
**And the delivered artifact names the controller it will start**, read out of the downloaded
archive itself:
```
$ tar --zstd -xOf golden.tar.zst ./etc/felhom-controller-image
gitea.dooplex.hu/admin/felhom-controller:0.230.0
```
with **19 382** entries under `var/lib/felhom/docker/` — the baked image store is in the archive, not
a promise that it will be pulled later.
## Both pre-gates were proven able to see something first
| gate | negative result | the positive control that makes it believable |
|---|---|---|
| 404 pre-gate on the package URL | `HTTP 404` for 0.230.0 before the bake | the same URL shape returns `HTTP 200` for the controller image manifest, and the bake's own pre-delete logged `HTTP 404 (404/204 expected)` |
| token-leak grep on the committed log | **0** in `06-bake.log` and `06-bake-clean.log` | the token appended to a throwaway copy of the same log greps **1**; the copy was then `shred -u`'d |
The token was also kept off every command line: the transient unit's own properties were grepped for
it — `systemctl show golden-bake -p Environment -p ExecStart` → **0**, with the same seeded positive
control returning **1**.
## The vouch — a THREE-field change, checked rather than assumed
| field | before | after | why |
|---|---|---|---|
| `golden_version` | 0.229.0 | **0.230.0** | the new bake |
| `agent_version` | 0.130.0 | 0.130.0 | **unchanged** — already ≥ MinAgent |
| `min_agent` | 0.129.0 | 0.129.0 | **unchanged** — v0.230.0's CHANGELOG header says `MinAgent: 0.129.0` |
`min_agent` (0.129.0) ≤ `agent_version` (0.130.0), so this is **not** the R-216 shape the hub holds
against. Only one field actually moved, and that is stated rather than left to look like a one-field
vouch performed carelessly.
`POST /configuration/artifacts` → `303 Location: /configuration?flash=artifacts_set`. **The flash was
not treated as proof:** the page was re-read and the selected options confirmed
(`golden_version` selected 0.230.0, sha `9287f7ce…`), and the R-120 refusal banner
(`golden_behind_fleet`) confirmed **absent**.
## The fleet floor, and the unattended proof that it worked
`POST /configuration/global-floor` `min_controller_version` **0.229.0 → 0.230.0**, re-read from the
page afterwards. This is a **separate setting from the Day-0 artifacts** — the page says so itself —
and it is the one that moves the existing fleet rather than fresh installs.
**`demo-felhom` was on controller 0.229.0 — the R-403 build — and moved itself.** From the agent's
journal on that host, which is a POSITIVE observable and not a missing error:
```
16:21:30 controller-swap: image file written, restarting bootstrap target=…felhom-controller:0.230.0
16:21:40 controller-swap: new controller healthy target=…felhom-controller:0.230.0
```
**Honest note on the watch:** the polling loop's *first* read already said 0.230.0, so the transition
was not observed by the loop. The evidence is the journal above plus the container reading
`Up 8 seconds (healthy)` at that first read. Both boxes now:
```
demo-felhom gitea.dooplex.hu/admin/felhom-controller:0.230.0 Up (healthy)
demo-hp gitea.dooplex.hu/admin/felhom-controller:0.230.0 Up (healthy)
```
## Teardown — all of it, and it is not "nothing was created"
`pct destroy 9100 --purge` (both logical volumes removed), then `shred -u` of the token, the runner
script, `build-golden.sh` and `bake.log` **after** the log was copied out for evidence — `ls | grep`
in `/root` returns nothing. Then `poweroff`, waited for the qemu process to actually exit (checked
with `ps -eo comm`, **not** `pgrep -f`, which self-matches), and `qemu-img snapshot -a virgin`. The
disk carries the single `virgin` snapshot and nothing else.
## Files here
| file | what |
|---|---|
| `01-preconditions.txt` | disk headroom, image present, MinAgent, hub state before |
| `02-pveam.txt`, `03-template.txt` | the index refresh and the template download |
| `04-staged.txt`, `05-bake-launch.txt` | staging + the token-leak check on the unit properties |
| `06-bake.log`, `06-bake-clean.log` | the bake, raw and with the locale noise stripped |
| `07-teardown-vm.txt` | destroy, shred, revert to virgin |
| `08-roundtrip.txt`, `16-archive-content-proof.txt` | the published bytes, and what is inside them |
| `00-`, `10-`, `13-hub-*.html` | the hub configuration page before, after the vouch, after the floor |
| `09-vouch-post.txt`, `11-vouch-verified.txt`, `14-floor-raise.txt` | the two POSTs and their re-reads |
| `12-golden-currency-gate.txt` | the gate that was red, now green |
| `15-demo-felhom-selfupdate.txt` | the unattended pickup |