v0.255.0 — the globe on the sign-in-flow pages: styled, and inside the card
gates / gates (push) Successful in 23s

Two defects in v0.254.0's globe, both plain on a browser and neither catchable by anything
that existed — every test read the MARKUP, and the fault was in which CSS file the browser
fetched.

The shells requested /static/style.css with NO ?v=, while layout.html has carried one since
v0.166.0. A browser holding a copy from before v0.254.0 kept serving CSS with no .lang-globe
rules, so the globe came out as a bare unstyled <details> — a stray triangle and two plain
words at the edge of the window. It was FIVE shells, not the three named: both guest share
pages have the same fault for any CSS change, and their visitor is the likeliest of all to be
holding an old copy. And .Version was missing from three of those five data maps, which is
exactly how the next one would be forgotten — it is now filled at the one choke point every
shell renders through.

The globe also floated outside the card, pinned to the corner of the VIEWPORT, reading as part
of the browser rather than the page. It now sits inside the card, centred under the footer, with
the menu opening upward via the shared rule — so the dashboard and the shells cannot drift.

AND A THIRD, caught by a test that already existed: putting the version on the guest share pages
would have printed the controller build onto a page a stranger with a capability URL can open.
TestShareGuest_HeadersTilesNoAdminChrome refused it. Those two now take an opaque per-build tag
— same cache-busting, no disclosure. The fill is ONE function shared with the parity harness,
because a fixture rendered through a different data path is a picture of a page nobody serves,
which the previous release got wrong twice.

15 shell fixtures re-captured; 91 identical, every dashboard page among them.

MinAgent: 0.131.0 (unchanged). No hub release needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-18 15:07:49 +02:00
parent 2e9d40255b
commit 8fb2f9ef9d
28 changed files with 277 additions and 367 deletions
+39
View File
@@ -3,6 +3,8 @@ package web
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"html/template"
"io"
@@ -412,6 +414,35 @@ func (s *Server) hasSession(r *http.Request) bool {
return err == nil && s.isValidSession(c.Value)
}
// addShellAssetData fills the cache-buster fields every page shell's stylesheet link uses.
//
// ONE function, called by executeTemplateLang AND by the parity harness, because a fixture rendered
// through a different data path is a picture of a page nobody serves — which this release's own
// predecessor got wrong twice. Idempotent: an existing Version is left alone, so a fixture case that
// pins its own version keeps it.
//
// `AssetTag` is the buster for a page a STRANGER may open — the two guest share pages, reached by a
// capability URL. `TestShareGuest_HeadersTilesNoAdminChrome` refuses the version string on those for a
// reason worth keeping: telling anyone holding the link exactly which build is running is a gift to
// somebody looking for a known fault. An opaque per-build tag busts the cache just as well.
func (s *Server) addShellAssetData(data map[string]interface{}) {
if data == nil {
return
}
if _, ok := data["Version"]; !ok {
data["Version"] = s.version
}
data["AssetTag"] = s.assetTag()
}
// assetTag is an opaque per-build cache-buster: it changes when the version changes and discloses
// nothing about it. Used by the pages a stranger can open; everywhere else the version itself is
// already on the page and the tag would only be indirection.
func (s *Server) assetTag() string {
sum := sha256.Sum256([]byte("felhom-asset-tag-v1:" + s.version))
return hex.EncodeToString(sum[:4])
}
// HubPushStatusData holds hub push status for the monitoring page.
type HubPushStatusData struct {
LastAttempt time.Time
@@ -915,6 +946,14 @@ func (s *Server) executeTemplateLang(w io.Writer, r *http.Request, name string,
lang := s.langFor(r)
if data != nil {
data["Lang"] = lang
// v0.255.0: the stylesheet cache-buster, set HERE rather than in each handler.
//
// Five shells loaded /static/style.css with no `?v=`, so a browser that had the file cached
// kept serving the old one — and the v0.254.0 globe rendered as a bare, unstyled <details>
// for anyone who had visited before. Three of the five did not carry `Version` in their data
// at all, which is exactly how the next one would be forgotten: setting it at the single choke
// point every shell renders through means a new shell cannot miss it.
s.addShellAssetData(data)
// v0.254.0: the globe, on a page rendered outside the dashboard chrome. WHICH FORM it posts to
// depends on WHO is reading, and getting that wrong makes the globe do nothing:
//