hub v0.68.1 — fix the Configuration layout broken by the wrapper-sha field
The v0.68.0 row wrapped itself in a <div>, but the artifacts <form> IS the CSS grid (display:grid, no inner container). The stray </div> closed the surrounding card from inside the form and the new <div> was never closed — it swallowed the submit button and ran to </form>, so the row rendered outside the card and Save landed inline. Reported by the operator on first use. The field still submitted (it stayed inside the form), so this was layout damage rather than data loss, but the unbalanced markup put every section below it in the wrong container. Fixed as plain grid cells (grid-column: 2/4), no nested elements. There was no render assertion on this form at all, which is why a hand-edit broke it silently. The new test asserts the field is inside the form, the button has not escaped, the form contains ZERO divs, whole-page div balance holds, and the sections after it survive. Red-proofed against the broken shape.
This commit is contained in:
@@ -1,5 +1,24 @@
|
||||
# Felhom Hub — Changelog
|
||||
|
||||
## v0.68.1 — fix the Configuration page layout broken by the wrapper-sha field (2026-07-21)
|
||||
|
||||
The v0.68.0 wrapper-sha256 row wrapped itself in a `<div>`. The artifacts **`<form>` IS the CSS
|
||||
grid** (`display: grid`, no inner container), so the stray `</div>` closed the surrounding CARD from
|
||||
inside the form, and the newly-opened `<div>` was never closed — it swallowed the submit button and
|
||||
ran to `</form>`. The row rendered outside the card at full page width and "Save artifact manifest"
|
||||
landed inline. Reported by the operator on first use.
|
||||
|
||||
The field still submitted (it remained inside the form), so this was layout damage rather than data
|
||||
loss — but the unbalanced markup put every section below it inside the wrong container.
|
||||
|
||||
Fixed by making the row plain grid cells (`grid-column: 2 / 4` for the input and the hint), with no
|
||||
nested elements at all.
|
||||
|
||||
**There was no render assertion on this form**, which is why a hand-edit could break it silently.
|
||||
`TestConfigurationArtifactsForm_Structure` now asserts the field is inside the form, the submit
|
||||
button has not escaped, the form contains **zero** `<div>`s, whole-page div balance holds, and the
|
||||
sections that render after it still exist. Red-proofed by restoring the broken shape.
|
||||
|
||||
## v0.68.0 — a credential re-issue finally re-arms the box (R-39 fleet fix) + wrapper drift is visible (R-50b(a)) (2026-07-21)
|
||||
|
||||
**Coupling, stated honestly: this release is SAFE for agents at 0.90.0** — the new descriptor field
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http/httptest"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
|
||||
)
|
||||
|
||||
// R-50b(a) — the wrapper-sha field must render INSIDE the artifact form, and the form's grid must
|
||||
// stay structurally intact.
|
||||
//
|
||||
// This test exists because the first attempt at this field shipped broken: it wrapped the row in a
|
||||
// <div> and, since the <form> ITSELF is the CSS grid (`display: grid`, no inner container), the
|
||||
// stray `</div>` closed the surrounding CARD from inside the form and the newly-opened <div> was
|
||||
// never closed — it swallowed the submit button and ran to </form>. The page rendered with the row
|
||||
// escaped outside the card and the Save button inline. Nothing failed: there was no render
|
||||
// assertion on this form at all.
|
||||
func TestConfigurationArtifactsForm_Structure(t *testing.T) {
|
||||
s, _ := newTestServer(t)
|
||||
data := map[string]interface{}{
|
||||
"CSRFToken": "tok",
|
||||
"CSRFField": s.csrfField(httptest.NewRequest("GET", "/", nil)),
|
||||
"AssetCount": 0,
|
||||
"AssetLastSync": "",
|
||||
"GlobalFloor": "0.86.0",
|
||||
"Artifacts": store.ArtifactManifest{
|
||||
AgentVersion: "0.91.2",
|
||||
GoldenVersion: "0.153.0",
|
||||
MinAgent: "0.91.2",
|
||||
WrapperSHA256: "104db0a4401f65bbc476e82bfb1796433bcb36f8f8cce69efb3bb5c40fcb16b3",
|
||||
},
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := s.templates.ExecuteTemplate(&buf, "configuration.html", data); err != nil {
|
||||
t.Fatalf("render: %v", err)
|
||||
}
|
||||
body := buf.String()
|
||||
|
||||
// 1. The field renders, carries its stored value, and is reachable for the operator.
|
||||
if !strings.Contains(body, `name="wrapper_sha256"`) {
|
||||
t.Fatal("the wrapper sha256 input is missing from the Configuration page")
|
||||
}
|
||||
if !strings.Contains(body, "104db0a4401f65bbc476e82bfb1796433bcb36f8f8cce69efb3bb5c40fcb16b3") {
|
||||
t.Error("the stored wrapper sha256 is not rendered into the field — the operator cannot see what is vouched")
|
||||
}
|
||||
|
||||
// 2. THE STRUCTURAL ASSERTION. It must sit inside the artifacts <form>, or it is never submitted.
|
||||
form := artifactsForm(t, body)
|
||||
if !strings.Contains(form, `name="wrapper_sha256"`) {
|
||||
t.Error("the wrapper sha256 input is OUTSIDE the artifacts form — it would never be submitted")
|
||||
}
|
||||
if !strings.Contains(form, "Save artifact manifest") {
|
||||
t.Error("the submit button escaped the artifacts form")
|
||||
}
|
||||
|
||||
// 3. The form is the grid: any <div> inside it breaks the column layout and, unbalanced, breaks
|
||||
// the surrounding card too. That is exactly how this shipped wrong the first time.
|
||||
if n := strings.Count(form, "<div"); n != 0 {
|
||||
t.Errorf("the artifacts form contains %d <div> element(s) — the form IS the CSS grid; "+
|
||||
"a nested div breaks the column layout", n)
|
||||
}
|
||||
if o, c := strings.Count(form, "<div"), strings.Count(form, "</div>"); o != c {
|
||||
t.Errorf("unbalanced div tags inside the artifacts form: %d open, %d close", o, c)
|
||||
}
|
||||
|
||||
// 4. Whole-page div balance — an unclosed div here corrupts every section BELOW it (the login
|
||||
// password card and the assets card both render after this form).
|
||||
if o, c := strings.Count(body, "<div"), strings.Count(body, "</div>"); o != c {
|
||||
t.Errorf("configuration.html has unbalanced divs: %d open, %d close — sections after the "+
|
||||
"artifact form will render inside the wrong container", o, c)
|
||||
}
|
||||
|
||||
// 5. The sections that follow must still be present and not swallowed.
|
||||
for _, want := range []string{"Login password", "Change password", "Assets", "Refresh Assets from Image"} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("section %q missing — likely swallowed by an unclosed element above it", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var artifactsFormRe = regexp.MustCompile(`(?s)<form[^>]*action="/configuration/artifacts"[^>]*>(.*?)</form>`)
|
||||
|
||||
func artifactsForm(t *testing.T, body string) string {
|
||||
t.Helper()
|
||||
m := artifactsFormRe.FindStringSubmatch(body)
|
||||
if m == nil {
|
||||
t.Fatal("could not locate the artifacts form")
|
||||
}
|
||||
return m[1]
|
||||
}
|
||||
|
||||
// An empty stored value must render an empty field (blank = not vouched), not the string "<no value>"
|
||||
// or a zero — the operator reads this field to decide whether anything is vouched at all.
|
||||
func TestConfigurationArtifactsForm_EmptyWrapperSHA(t *testing.T) {
|
||||
s, _ := newTestServer(t)
|
||||
var buf bytes.Buffer
|
||||
err := s.templates.ExecuteTemplate(&buf, "configuration.html", map[string]interface{}{
|
||||
"CSRFToken": "tok", "CSRFField": s.csrfField(httptest.NewRequest("GET", "/", nil)),
|
||||
"AssetCount": 0, "AssetLastSync": "", "GlobalFloor": "",
|
||||
"Artifacts": store.ArtifactManifest{},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("render: %v", err)
|
||||
}
|
||||
form := artifactsForm(t, buf.String())
|
||||
if !strings.Contains(form, `name="wrapper_sha256" value=""`) {
|
||||
t.Errorf("an unvouched wrapper must render an empty value; got:\n%s", form)
|
||||
}
|
||||
}
|
||||
@@ -180,11 +180,10 @@
|
||||
<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>
|
||||
</div>
|
||||
<div style="margin-bottom: 0.75em;">
|
||||
<label style="display:inline-block; min-width: 12em;">PBS wrapper sha256</label>
|
||||
<input type="text" name="wrapper_sha256" value="{{.Artifacts.WrapperSHA256}}" placeholder="64 hex chars (blank = not vouched)" style="padding: 0.3em 0.5em; width: 34em;">
|
||||
<span style="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.</span>
|
||||
<label style="font-size: 0.9em; color: #cbd5e1;">PBS wrapper</label>
|
||||
<input type="text" name="wrapper_sha256" value="{{.Artifacts.WrapperSHA256}}" 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>
|
||||
|
||||
Reference in New Issue
Block a user