Files
CableGUI/internal/server/server.go
mAi 28a376a7f3 fix(ui+server): tool cursor wins on canvas children; no-cache static assets
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.
2026-05-15 20:38:48 +02:00

72 lines
2.8 KiB
Go

// Package server wires the HTTP API + the embedded frontend onto a
// single net/http handler. Routes use Go 1.22 ServeMux pattern matching
// (no router framework).
package server
import (
"io/fs"
"net/http"
"mgit.msbls.de/m/mcables/internal/db"
)
// New returns an http.Handler serving the mCables API at /api/ and the
// embedded frontend at /. The frontend FS should be rooted such that
// "index.html" is at its root.
func New(store *db.Store, frontend fs.FS) http.Handler {
mux := http.NewServeMux()
h := &handlers{store: store}
// Health
mux.HandleFunc("GET /api/healthz", h.healthz)
// Projects
mux.HandleFunc("GET /api/projects", h.listProjects)
mux.HandleFunc("POST /api/projects", h.createProject)
mux.HandleFunc("GET /api/projects/{pid}", h.getProject)
mux.HandleFunc("PATCH /api/projects/{pid}", h.patchProject)
mux.HandleFunc("DELETE /api/projects/{pid}", h.deleteProject)
// Cable types (global)
mux.HandleFunc("GET /api/cable-types", h.listCableTypes)
mux.HandleFunc("POST /api/cable-types", h.createCableType)
mux.HandleFunc("PATCH /api/cable-types/{id}", h.patchCableType)
mux.HandleFunc("DELETE /api/cable-types/{id}", h.deleteCableType)
// Frames (project-scoped)
mux.HandleFunc("GET /api/projects/{pid}/frames", h.listFrames)
mux.HandleFunc("POST /api/projects/{pid}/frames", h.createFrame)
mux.HandleFunc("PATCH /api/projects/{pid}/frames/{id}", h.patchFrame)
mux.HandleFunc("DELETE /api/projects/{pid}/frames/{id}", h.deleteFrame)
// Devices (project-scoped)
mux.HandleFunc("GET /api/projects/{pid}/devices", h.listDevices)
mux.HandleFunc("POST /api/projects/{pid}/devices", h.createDevice)
mux.HandleFunc("PATCH /api/projects/{pid}/devices/{id}", h.patchDevice)
mux.HandleFunc("DELETE /api/projects/{pid}/devices/{id}", h.deleteDevice)
// Frontend (embedded). Serve "/" → index.html via http.FileServerFS.
// 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)
})
}