From 4a4233059d2377e939923fd279083a288538a39c Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Tue, 21 Jul 2026 10:34:34 +0200 Subject: [PATCH] =?UTF-8?q?hub=20v0.68.1=20=E2=80=94=20fix=20the=20Configu?= =?UTF-8?q?ration=20layout=20broken=20by=20the=20wrapper-sha=20field?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v0.68.0 row wrapped itself in a
, but the artifacts
IS the CSS grid (display:grid, no inner container). The stray
closed the surrounding card from inside the form and the new
was never closed — it swallowed the submit button and ran to , 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. --- hub/CHANGELOG.md | 19 +++ .../web/artifacts_form_render_test.go | 113 ++++++++++++++++++ hub/internal/web/templates/configuration.html | 9 +- 3 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 hub/internal/web/artifacts_form_render_test.go 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.