diff --git a/internal/server/server.go b/internal/server/server.go index c89bd5b..d40d981 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1,7 +1,9 @@ package server import ( + "crypto/sha256" "embed" + "encoding/hex" "html/template" "io/fs" "log" @@ -184,7 +186,7 @@ func (s *Server) Handler() http.Handler { // Static assets (xterm.js etc.) from the embedded FS. staticFS, _ := fs.Sub(webFS, "web/static") - mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS)))) + mux.Handle("GET /static/", http.StripPrefix("/static/", staticHandler(staticFS))) // Public: install script + agent binary downloads. mux.HandleFunc("GET /install.sh", s.handleInstallScript) @@ -333,3 +335,33 @@ func clientIP(r *http.Request) string { } return host } + +// staticHandler serves the embedded assets with a content hash as ETag. The +// browser asks each time whether a file changed (no-cache) and gets a 304 +// when it did not, so a new release reaches it at once without reloading +// what stayed the same. +func staticHandler(fsys fs.FS) http.Handler { + files := http.FileServer(http.FS(fsys)) + var mu sync.Mutex + etags := map[string]string{} + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + name := strings.TrimPrefix(r.URL.Path, "/") + mu.Lock() + tag, ok := etags[name] + mu.Unlock() + if !ok { + if data, err := fs.ReadFile(fsys, name); err == nil { + sum := sha256.Sum256(data) + tag = `"` + hex.EncodeToString(sum[:8]) + `"` + } + mu.Lock() + etags[name] = tag + mu.Unlock() + } + if tag != "" { + w.Header().Set("ETag", tag) + } + w.Header().Set("Cache-Control", "no-cache") + files.ServeHTTP(w, r) + }) +} diff --git a/internal/server/web/static/app.css b/internal/server/web/static/app.css index 446c3e0..b09f924 100644 --- a/internal/server/web/static/app.css +++ b/internal/server/web/static/app.css @@ -65,6 +65,9 @@ button:hover, .btn:hover { background: var(--accent-hover); text-decoration: non /* Close buttons: red text and border on the ghost button. */ .btn.close, .btn.ghost.close { background: transparent; border: 1px solid rgba(255, 92, 92, .6); color: var(--danger); } .btn.close:hover, .btn.ghost.close:hover { background: #3a1d1d; border-color: var(--danger); } +/* A close button that is just a cross: square, the height of the buttons by it. */ +.btn.close.x { width: 32px; height: 32px; padding: 0; display: inline-flex; align-items: center; justify-content: center; } +.btn.small.close.x { width: 28px; height: 28px; } .btn.small { padding: 6px 10px; font-size: 13px; } .btn.icon { width: 30px; height: 30px; padding: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 14px; line-height: 1; text-decoration: none; } diff --git a/internal/server/web/templates/files.html b/internal/server/web/templates/files.html index e7a6d6d..17faa45 100644 --- a/internal/server/web/templates/files.html +++ b/internal/server/web/templates/files.html @@ -226,7 +226,7 @@ - +
@@ -261,7 +261,7 @@
Загрузка файлов - +