Files
logstream/static.go
T
cedricandClaude Opus 5.5 3e703c8792 UI files: ETag and revalidation on each load
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>
2026-11-01 16:11:41 +01:00

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)
})
}