hub v0.45.0: floor-UI separation + effective-floor source + per-box MinAgent conditional floor

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-11 15:33:14 +02:00
parent 37e60b46d2
commit bbecf0592e
15 changed files with 730 additions and 44 deletions
+67 -7
View File
@@ -48,20 +48,77 @@
<div class="flash flash-error">Couldn't set the checksum — the Gitea sha lookup failed (version missing / Gitea unreachable) or the manually-entered sha is invalid. Manifest unchanged.</div>
{{end}}
<!-- Phase 2 managed updates: global controller-version floor (moved from the Customers page —
it is a global setting). -->
<!-- Phase 2 managed updates: global controller-version floor. ITS OWN card, separate from the
Day-0 artifact manifest below (a manifest save must NEVER touch the live floor — the
publish-train 0.81/0.113 incident). Saving acts IMMEDIATELY, so it is behind a
type-to-confirm dialog that states the live below-floor blast radius first. -->
<section class="card">
<h3 style="margin-top: 0;">Managed updates — global floor</h3>
<p class="text-muted" style="margin: 0 0 0.75rem; font-size: 0.85em;">
The minimum controller version every box auto-updates to (unless a per-customer override is set).
Boxes below the floor update on their next report — no customer action. Blank = no global floor.
<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>
<form method="POST" action="/configuration/global-floor" style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
<!-- Effective-floor-and-source line: makes the DB-override-vs-env-fallback precedence
permanently visible (the 9-minute-skew incident's root cause). -->
<p style="margin: 0 0 0.75rem; font-size: 0.85em;">
Effective floor:
{{if .FloorRes.Effective}}<code>v{{.FloorRes.Effective}}</code>{{else}}<span class="text-muted">none</span>{{end}}
{{if eq .FloorRes.Source "db"}}
<span style="color: #cbd5e1;">— source: <strong>DB (hub_settings)</strong>{{if .FloorRes.EnvValue}}; env fallback would be <code>v{{.FloorRes.EnvValue}}</code>{{end}}</span>
{{else if eq .FloorRes.Source "env"}}
<span style="color: #cbd5e1;">— source: <strong>env fallback (DEFAULT_MIN_CONTROLLER_VERSION)</strong>; no DB override set</span>
{{else}}
<span class="text-muted">— no floor from either source</span>
{{end}}
</p>
<form id="global-floor-form" method="POST" action="/configuration/global-floor" style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
{{.CSRFField}}
<input type="text" name="min_controller_version" value="{{.GlobalFloor}}" placeholder="e.g. 0.86.0 (blank = none)" style="padding: 0.3em 0.5em; width: 14em;">
<button class="btn btn-sm" type="submit">Save global floor</button>
<span style="font-size: 0.85em; color: #cbd5e1;">Current: {{if .GlobalFloor}}<code>v{{.GlobalFloor}}</code>{{else}}<span class="text-muted">unset</span>{{end}}</span>
<input type="text" id="global-floor-input" name="min_controller_version" value="{{.FloorRes.DBValue}}" 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: {{if .FloorRes.DBValue}}<code>v{{.FloorRes.DBValue}}</code>{{else}}<span class="text-muted">none</span>{{end}}</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>
<!-- BUNDLE slice: Day-0 artifact manifest (agent binary + golden archive). The hub is the
@@ -102,6 +159,9 @@
<input type="text" name="golden_version" value="{{.Artifacts.GoldenVersion}}" placeholder="0.85.1" style="padding: 0.3em 0.5em;">
{{end}}
<input type="text" name="golden_sha256" id="golden_sha256" value="{{.Artifacts.GoldenSHA256}}" {{if .GoldenChoices}}readonly{{end}} placeholder="64-hex sha256 (blank = none)" style="padding: 0.3em 0.5em; font-family: monospace; {{if .GoldenChoices}}opacity: 0.7;{{end}}">
<label style="font-size: 0.9em; color: #cbd5e1;">Min agent</label>
<input type="text" name="min_agent" value="{{.Artifacts.MinAgent}}" 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>
<span></span><span></span>
<button class="btn btn-sm" type="submit" style="justify-self: start;">Save artifact manifest</button>
</form>