feat: close buttons as a red cross in a square; static files revalidated by content, so a release reaches the browser at once

This commit is contained in:
redtea committed 2026-10-14 19:24:12 +03:00
1 parent 55db8f03b3
commit 8eb2bbd35d
3 files changed
+38 -3

No files matched your search

+33 -1
View File
@@ -1,7 +1,9 @@
package server package server
import ( import (
"crypto/sha256"
"embed" "embed"
"encoding/hex"
"html/template" "html/template"
"io/fs" "io/fs"
"log" "log"
@@ -184,7 +186,7 @@ func (s *Server) Handler() http.Handler {
// Static assets (xterm.js etc.) from the embedded FS. // Static assets (xterm.js etc.) from the embedded FS.
staticFS, _ := fs.Sub(webFS, "web/static") 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. // Public: install script + agent binary downloads.
mux.HandleFunc("GET /install.sh", s.handleInstallScript) mux.HandleFunc("GET /install.sh", s.handleInstallScript)
@@ -333,3 +335,33 @@ func clientIP(r *http.Request) string {
} }
return host 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)
})
}
+3
View File
@@ -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. */ /* 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, .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); } .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.small { padding: 6px 10px; font-size: 13px; }
.btn.icon { width: 30px; height: 30px; padding: 0; display: inline-flex; .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; } align-items: center; justify-content: center; font-size: 14px; line-height: 1; text-decoration: none; }
+2 -2
View File
@@ -226,7 +226,7 @@
<button class="btn ghost small" id="vtext" hidden>Редактировать</button> <button class="btn ghost small" id="vtext" hidden>Редактировать</button>
<button class="btn ghost small" id="vhex" hidden>Редактировать бинарно</button> <button class="btn ghost small" id="vhex" hidden>Редактировать бинарно</button>
<button class="btn small" id="save" hidden>Сохранить</button> <button class="btn small" id="save" hidden>Сохранить</button>
<button class="btn ghost small close" id="closev">Закрыть</button> <button class="btn ghost small icon close x" id="closev" title="Закрыть" aria-label="Закрыть"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
<div class="vmsg muted" id="vmsg"></div> <div class="vmsg muted" id="vmsg"></div>
</div> </div>
<div class="vbody"> <div class="vbody">
@@ -261,7 +261,7 @@
<dialog id="updlg"> <dialog id="updlg">
<div class="dlg-h"> <div class="dlg-h">
<strong style="flex:1">Загрузка файлов</strong> <strong style="flex:1">Загрузка файлов</strong>
<button class="btn ghost small close" id="updlg-close">Закрыть</button> <button class="btn ghost small icon close x" id="updlg-close" title="Закрыть" aria-label="Закрыть"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
</div> </div>
<div class="dlg-b" id="updlg-list"></div> <div class="dlg-b" id="updlg-list"></div>
</dialog> </dialog>