Retire the Dashboard tab; Processes is the front page

The app repeated itself. The module list, /api/prefs/dashboard, applyPrefs and the
per-user dashboard blob are gone. Widgets stays and now says plainly that it is for
dashboards outside Ham Lab.

This also closes a bug: applyPrefs set the sampling rate from intervalMs inside the
dashboard layout, and reset it to the default when that field was absent — so with
per-user layouts, one account saving a dashboard could reset the sampling rate for the
whole machine. The sampler now has one source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-23 23:03:49 -07:00
co-authored by Claude Opus 5
parent 1eb59fd08c
commit 9bf60deffc
12 changed files with 60 additions and 581 deletions
+4 -58
View File
@@ -102,18 +102,8 @@ func New(cfg *config.Config, sessions *auth.Sessions, mgr *term.Manager, col *me
s.sensors.Add(newStatusSensors(s))
s.alerts = newAlertEngine(s)
// The saved dashboard layout carries the sampling interval; apply it on boot.
var raw json.RawMessage
var histSec int
st.View(func(x *store.State) {
raw, histSec = x.Dashboard, x.HistorySec
if len(raw) == 0 {
for _, u := range x.Users {
raw = u.Dashboard
break
}
}
})
s.applyPrefs(raw)
st.View(func(x *store.State) { histSec = x.HistorySec })
if histSec > 0 {
col.SetWindow(time.Duration(histSec) * time.Second)
}
@@ -289,8 +279,6 @@ func New(cfg *config.Config, sessions *auth.Sessions, mgr *term.Manager, col *me
m.HandleFunc("PUT /api/prefs/sampler", s.auth(s.putSampler))
m.HandleFunc("GET /api/prefs/history", s.auth(s.getHistoryWindow))
m.HandleFunc("PUT /api/prefs/history", s.auth(s.putHistoryWindow))
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
@@ -930,15 +918,11 @@ func (s *Server) migrateUsers() {
st.Sessions[tok] = ses
}
}
if len(st.UI) > 0 || len(st.Dashboard) > 0 {
u := st.UserState(first)
if len(u.UI) == 0 {
if len(st.UI) > 0 {
if u := st.UserState(first); len(u.UI) == 0 {
u.UI = st.UI
}
if len(u.Dashboard) == 0 {
u.Dashboard = st.Dashboard
}
st.UI, st.Dashboard = nil, nil
st.UI = nil
}
})
}
@@ -2282,44 +2266,6 @@ func (s *Server) pushHistoryWindow(seconds int) {
}
}
// ---- dashboard layout prefs (opaque JSON object, owned by the UI) ----
func (s *Server) getDashPrefs(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-store")
jsonOK(w, 200, s.userPrefs(r, func(u *store.UserState) json.RawMessage { return u.Dashboard }))
}
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 user := actorOf(r).User; user != "" {
if err := s.store.Update(func(st *store.State) { st.UserState(user).Dashboard = json.RawMessage(b) }); err != nil {
jsonErr(w, 500, err.Error())
return
}
}
s.applyPrefs(b)
w.WriteHeader(204)
}
// applyPrefs picks the few layout fields the server acts on (just the interval).
func (s *Server) applyPrefs(raw []byte) {
var p struct {
IntervalMs int `json:"intervalMs"`
}
if len(raw) == 0 || json.Unmarshal(raw, &p) != nil {
return
}
if p.IntervalMs > 0 {
s.metrics.SetInterval(time.Duration(p.IntervalMs) * time.Millisecond)
} else {
s.metrics.SetInterval(metrics.DefaultInterval)
}
}
// ---- terminals ----
func (s *Server) listTerminals(w http.ResponseWriter, _ *http.Request) {
+3 -5
View File
@@ -41,17 +41,15 @@ func (s *Session) UnmarshalJSON(b []byte) error {
// UserState is what follows an account rather than the machine: how the console looks
// to that person and how their dashboard is laid out.
type UserState struct {
Dashboard json.RawMessage `json:"dashboard,omitempty"` // layout prefs, opaque to the server
UI json.RawMessage `json:"ui,omitempty"` // theme, accents; opaque to the server
UI json.RawMessage `json:"ui,omitempty"` // theme, accents; opaque to the server
}
type State struct {
Terminals []Terminal `json:"terminals"`
Sessions map[string]Session `json:"sessions"` // token -> session
Users map[string]*UserState `json:"users,omitempty"`
// Dashboard and UI are the pre-users machine-wide prefs; on first boot with users they
// move to the first account. Kept so an old state file still reads.
Dashboard json.RawMessage `json:"dashboard,omitempty"`
// UI is the pre-users machine-wide look; on first boot with users it moves to the first
// account. Kept so an old state file still reads.
UI json.RawMessage `json:"ui,omitempty"`
Alerts json.RawMessage `json:"alerts,omitempty"` // rules, channels and recent events (internal/api/alerts.go)
SamplerMs int `json:"sampler_ms,omitempty"` // how often this machine samples; the hub keeps every machine on the console's refresh rate