From 28a376a7f37d0e8d21fd01b27be3fe528c8d4deb Mon Sep 17 00:00:00 2001 From: mAi Date: Fri, 15 May 2026 20:38:48 +0200 Subject: [PATCH] fix(ui+server): tool cursor wins on canvas children; no-cache static assets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue 1 — cursor lies about armed tool. .svg-draggable { cursor: grab } on frame/device rects beat the .canvas-wrap.tool-device #canvas { cursor: crosshair } rule because element-level wins over descendant. m saw "grab" hovering a frame with +Dev armed and thought the tool was broken even though clicks routed correctly after the previous fix. Add a descendant rule with !important so tool-armed wraps any child cursor: .canvas-wrap.tool-frame #canvas *, .canvas-wrap.tool-device #canvas * { cursor: crosshair !important; } Issue 2 — stale browser cache after each redeploy. http.FileServerFS served embedded assets with no Cache-Control header, so browsers held on to the previous main.js/style.css until hard-reload. New noCache middleware on the static handler emits Cache-Control: no-cache. Note: embedded FS files have zero ModTime, so http.FileServer suppresses Last-Modified — every fetch is a fresh 200 rather than a 304. Fine at ~30KB of JS+CSS, and fixes the staleness problem completely. Middleware is wrapped only around the static handler. /api/* responses write their own headers and aren't touched. Verified locally: curl -I /main.js → Cache-Control: no-cache curl -I /style.css → Cache-Control: no-cache + contains the new rule curl -I /api/healthz → unaffected (no Cache-Control from us) go test -race ./... still green. --- internal/server/server.go | 21 ++++++++++++++++++++- web/static/style.css | 10 ++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/internal/server/server.go b/internal/server/server.go index c272811..e28c0e9 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -46,7 +46,26 @@ func New(store *db.Store, frontend fs.FS) http.Handler { mux.HandleFunc("DELETE /api/projects/{pid}/devices/{id}", h.deleteDevice) // Frontend (embedded). Serve "/" → index.html via http.FileServerFS. - mux.Handle("/", http.FileServerFS(frontend)) + // Wrap in noCache so the browser revalidates with the ETag/Last-Modified + // the file server already emits — without this, browsers cache aggressively + // and m sees the old main.js after every redeploy until hard-reload. + mux.Handle("/", noCache(http.FileServerFS(frontend))) return mux } + +// noCache wraps a static handler so each response carries +// Cache-Control: no-cache. Combined with the ETag/Last-Modified headers +// http.FileServer(FS) already emits, this turns every fetch into a +// cheap revalidation request — the browser uses its cached body when +// the ETag matches but always asks first, so freshly-built assets show +// up on the next page load without a hard-reload. +// +// Applied to the static-asset handler only — API responses write their +// own headers and aren't routed through this. +func noCache(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "no-cache") + h.ServeHTTP(w, r) + }) +} diff --git a/web/static/style.css b/web/static/style.css index 5f1e0e4..605ca5b 100644 --- a/web/static/style.css +++ b/web/static/style.css @@ -203,9 +203,15 @@ body { .svg-draggable { cursor: grab; } .svg-draggable.dragging { cursor: grabbing; } -/* tool cursor on the empty canvas while a tool is armed */ +/* Tool cursor while a tool is armed. The `* { ... !important }` descendant + rule is the load-bearing part: without it, the `.svg-draggable` rules + on individual frame/device rects win by element specificity and + override the SVG-root cursor — so hovering a frame with +Dev armed + shows `grab`, which lies about what a click will do. */ .canvas-wrap.tool-frame #canvas, -.canvas-wrap.tool-device #canvas { cursor: crosshair; } +.canvas-wrap.tool-frame #canvas *, +.canvas-wrap.tool-device #canvas, +.canvas-wrap.tool-device #canvas * { cursor: crosshair !important; } .rubber-band { fill: rgba(25, 113, 194, 0.08);