Dashboard customize mode: reorder/hide modules and individual rows, layout saved server-side (GET/PUT /api/prefs/dashboard)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-17 13:46:41 -07:00
co-authored by Claude Fable 5.1
parent 7053cce7fe
commit eb70daf129
7 changed files with 351 additions and 144 deletions
+32 -2
View File
@@ -19,6 +19,7 @@ import (
"hamlab/internal/auth"
"hamlab/internal/config"
"hamlab/internal/metrics"
"hamlab/internal/store"
"hamlab/internal/term"
)
@@ -30,6 +31,7 @@ type Server struct {
limiter auth.Limiter
mgr *term.Manager
metrics *metrics.Collector
store *store.Store
version string
started time.Time
user string
@@ -38,9 +40,9 @@ type Server struct {
mux *http.ServeMux
}
func New(cfg *config.Config, sessions *auth.Sessions, mgr *term.Manager, col *metrics.Collector, ui fs.FS, version string) *Server {
func New(cfg *config.Config, sessions *auth.Sessions, mgr *term.Manager, col *metrics.Collector, st *store.Store, ui fs.FS, version string) *Server {
host, _ := os.Hostname()
s := &Server{cfg: cfg, sessions: sessions, mgr: mgr, metrics: col, ui: ui, version: version,
s := &Server{cfg: cfg, sessions: sessions, mgr: mgr, metrics: col, store: st, ui: ui, version: version,
started: time.Now(), host: host, user: os.Getenv("USER")}
if u, err := user.Current(); err == nil && u.Username != "" {
s.user = u.Username
@@ -60,6 +62,8 @@ func New(cfg *config.Config, sessions *auth.Sessions, mgr *term.Manager, col *me
m.HandleFunc("GET /api/terminals/{id}/attach", s.auth(s.attach))
m.HandleFunc("GET /api/metrics", s.auth(s.metricsLatest))
m.HandleFunc("GET /api/metrics/history", s.auth(s.metricsHistory))
m.HandleFunc("GET /api/prefs/dashboard", s.auth(s.getDashPrefs))
m.HandleFunc("PUT /api/prefs/dashboard", s.auth(s.putDashPrefs))
m.HandleFunc("/api/", func(w http.ResponseWriter, _ *http.Request) { jsonErr(w, 404, "not found") })
m.HandleFunc("/", s.static)
s.mux = m
@@ -185,6 +189,32 @@ func (s *Server) metricsHistory(w http.ResponseWriter, _ *http.Request) {
jsonOK(w, 200, s.metrics.History())
}
// ---- dashboard layout prefs (opaque JSON object, owned by the UI) ----
func (s *Server) getDashPrefs(w http.ResponseWriter, _ *http.Request) {
var raw json.RawMessage
s.store.View(func(st *store.State) { raw = st.Dashboard })
if len(raw) == 0 {
raw = json.RawMessage("{}")
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
_, _ = w.Write(raw)
}
func (s *Server) putDashPrefs(w http.ResponseWriter, r *http.Request) {
b, err := io.ReadAll(io.LimitReader(r.Body, 64<<10))
if err != nil || !json.Valid(b) || len(b) == 0 || b[0] != '{' {
jsonErr(w, 400, "expected a JSON object")
return
}
if err := s.store.Update(func(st *store.State) { st.Dashboard = json.RawMessage(b) }); err != nil {
jsonErr(w, 500, err.Error())
return
}
w.WriteHeader(204)
}
// ---- terminals ----
func (s *Server) listTerminals(w http.ResponseWriter, _ *http.Request) {