simpleadmin-web/internal/web/assets.go
Astra 17232bd5a2 Staff push notifications, and revalidate embedded assets
Notifications: staff turn them on per device from a new Notifications page,
pick each type (bans, gags, mutes and silences added or lifted, server down
and back up) and quiet hours. A watcher polls SimpleAdmin's tables, so in-game
penalties are sent too. Web Push with VAPID and RFC 8291 encryption in
internal/push, standard library only. Tables in sql/push.sql, created at
startup; the VAPID key is made on first start and kept in saw_settings.
Penalty notifications link to the public list searching for the player
(#bans?q=<steamid64>). The Android app delegates notifications (versionCode 3).

Caching: pages, scripts and styles are served with Cache-Control: no-cache
and an ETag, so a deploy shows up at once instead of after Cloudflare's
default 4-hour browser cache.
2026-10-03 13:49:40 +01:00

47 lines
1.4 KiB
Go

package web
import (
"crypto/sha256"
"encoding/hex"
"io/fs"
"net/http"
"sync"
)
// The pages, scripts and styles are embedded, so they only change with a new build. They're served
// with Cache-Control: no-cache and an ETag (a hash of the file): browsers, the Android app and
// Cloudflare check each time and get a quick 304 while the file is unchanged, and a new deploy
// shows up at once. Without these headers Cloudflare tells browsers to keep them for 4 hours.
var assetTags = sync.OnceValue(func() map[string]string {
tags := map[string]string{}
static, _ := fs.Sub(assets, "assets")
_ = fs.WalkDir(static, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
data, err := fs.ReadFile(static, path)
if err != nil {
return err
}
sum := sha256.Sum256(data)
tags[path] = `"` + hex.EncodeToString(sum[:8]) + `"`
return nil
})
return tags
})
// revalidate sets the caching headers for an embedded file. http.ServeContent answers a matching
// If-None-Match with 304 from the ETag set here.
func revalidate(w http.ResponseWriter, name string) {
w.Header().Set("Cache-Control", "no-cache")
if tag, ok := assetTags()[name]; ok {
w.Header().Set("ETag", tag)
}
}
// serveAsset serves one embedded file, see revalidate.
func serveAsset(w http.ResponseWriter, r *http.Request, static fs.FS, name string) {
revalidate(w, name)
http.ServeFileFS(w, r, static, name)
}