fix: customer-based board visibility, chat widget UX, atomized Subdivide, static cache-busting

- Board visibility (§4.4): developers now see tasks for any customer with a
  consultant who pooled them, not only tasks whose consultantId is in their
  pool. Fixes a published task being invisible to its own author who is both
  consultant and developer. Adds TestBoardVisibilityIsCustomerBased.
- Chat widget: working ✕ close, distinct (non-ghost) header buttons, reliable
  conversation switching, ← Back, title ellipsis.
- Atomization: atomized/imported subtasks now expose a Subdivide button.
- Static assets: serve with ETag + Cache-Control: no-cache so JS/CSS fixes
  reach clients immediately instead of being masked by stale max-age caching.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
etalon
2026-06-13 10:20:06 +02:00
parent c59aea42ef
commit 05dc91410f
6 changed files with 152 additions and 50 deletions
+30 -6
View File
@@ -1,6 +1,8 @@
package httpx
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"html"
"html/template"
@@ -126,8 +128,19 @@ func (s *Server) routesWeb(mux *http.ServeMux) {
if err != nil {
panic(err) // embed layout is fixed at compile time
}
mux.Handle("GET /static/", http.StripPrefix("/static/",
cacheControl(http.FileServerFS(staticFS), "public, max-age=3600")))
// no-cache + a build-wide strong ETag: browsers revalidate on every
// load (cheap 304) and can never run a stale CSS/JS mix after a deploy
etag := staticETag(staticFS)
fileServer := http.StripPrefix("/static/", http.FileServerFS(staticFS))
mux.Handle("GET /static/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("If-None-Match") == etag {
w.WriteHeader(http.StatusNotModified)
return
}
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("ETag", etag)
fileServer.ServeHTTP(w, r)
}))
mux.HandleFunc("GET /{$}", s.page(func(w http.ResponseWriter, r *http.Request, u *domain.User) {
s.render(w, r, "home.html", &pageData{Title: "Home", User: u})
@@ -273,9 +286,20 @@ func loginErrorMessage(code string) string {
}
}
func cacheControl(next http.Handler, value string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", value)
next.ServeHTTP(w, r)
// staticETag hashes every embedded static asset into one build-wide ETag.
func staticETag(fsys fs.FS) string {
h := sha256.New()
fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
data, err := fs.ReadFile(fsys, path)
if err != nil {
return err
}
h.Write([]byte(path))
h.Write(data)
return nil
})
return `"` + hex.EncodeToString(h.Sum(nil))[:20] + `"`
}