The embedded UI was served without any cache header, so a browser or a proxy could keep an old app.js after an update (seen with the API token display). Each file now has an ETag (hash of its content, computed at startup); the page, app.js and style.css are sent with "no-cache", so each load costs a 304 without body until the next version. Fonts are cached for 30 days. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
45 lines
1.3 KiB
Go
45 lines
1.3 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"io/fs"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// staticFiles serves the embedded UI with an ETag per file (a hash of its
|
|
// content, computed at startup). Pages, scripts and styles are revalidated on
|
|
// every load ("no-cache"): an unchanged file costs a 304 without body, and the
|
|
// files of a new version are always taken after an update. Fonts are kept for
|
|
// 30 days.
|
|
func staticFiles(static fs.FS) http.Handler {
|
|
etags := map[string]string{}
|
|
_ = fs.WalkDir(static, ".", func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil || d.IsDir() {
|
|
return err
|
|
}
|
|
b, err := fs.ReadFile(static, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sum := sha256.Sum256(b)
|
|
etags["/"+path] = `"` + hex.EncodeToString(sum[:8]) + `"`
|
|
return nil
|
|
})
|
|
etags["/"] = etags["/index.html"]
|
|
files := http.FileServer(http.FS(static))
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if tag, ok := etags[r.URL.Path]; ok {
|
|
// http.FileServer answers If-None-Match with 304 from this header.
|
|
w.Header().Set("ETag", tag)
|
|
if strings.HasPrefix(r.URL.Path, "/fonts/") {
|
|
w.Header().Set("Cache-Control", "public, max-age=2592000")
|
|
} else {
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
}
|
|
}
|
|
files.ServeHTTP(w, r)
|
|
})
|
|
}
|