diff --git a/hub/CHANGELOG.md b/hub/CHANGELOG.md index 57be110..b5c97ca 100644 --- a/hub/CHANGELOG.md +++ b/hub/CHANGELOG.md @@ -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 `
`. The artifacts **`
` IS the CSS +grid** (`display: grid`, no inner container), so the stray `
` closed the surrounding CARD from +inside the form, and the newly-opened `
` was never closed — it swallowed the submit button and +ran to ``. 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** `
`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 diff --git a/hub/internal/web/artifacts_form_render_test.go b/hub/internal/web/artifacts_form_render_test.go new file mode 100644 index 0000000..ca1221d --- /dev/null +++ b/hub/internal/web/artifacts_form_render_test.go @@ -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 +//
and, since the
ITSELF is the CSS grid (`display: grid`, no inner container), the +// stray `
` closed the surrounding CARD from inside the form and the newly-opened
was +// never closed — it swallowed the submit button and ran to . 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
, 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
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, " element(s) — the form IS the CSS grid; "+ + "a nested div breaks the column layout", n) + } + if o, c := strings.Count(form, ""); 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, ""); 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)]*action="/configuration/artifacts"[^>]*>(.*?)`) + +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 "" +// 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) + } +} diff --git a/hub/internal/web/templates/configuration.html b/hub/internal/web/templates/configuration.html index 5a38ec1..d651d37 100644 --- a/hub/internal/web/templates/configuration.html +++ b/hub/internal/web/templates/configuration.html @@ -180,11 +180,10 @@ The golden's controller CHANGELOG MinAgent:. The hub HOLDS the floor for any box whose agent is below this — blank = uncoupled release, no gating. -
-
- - - sha256 of configs/felhom-pbs-apply (R-50b). Unlike the agent and golden, this root-owned wrapper is installed from raw/branch/main — unversioned and unpinned. Recording it here does not fix the channel; it makes host drift visible: agents report the installed file's hash and a mismatch is surfaced on the host. + + + + sha256 of configs/felhom-pbs-apply (R-50b). Unlike the agent and golden this root-owned wrapper is installed from raw/branch/main — unversioned and unpinned. Recording it here does not fix the channel; it makes host drift visible: agents report the installed file's hash and a mismatch is surfaced on the host page.