Settings page: themes (Dark, Light, Cyberpunk, Retro) with xterm palettes and hub-synced choice, change password, access tokens, about

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-18 13:14:01 -07:00
co-authored by Claude Fable 5.1
parent 254e0cdd58
commit a4f39859b6
11 changed files with 319 additions and 22 deletions
+58
View File
@@ -143,6 +143,9 @@ func New(cfg *config.Config, sessions *auth.Sessions, mgr *term.Manager, col *me
m.HandleFunc("POST /api/storage/zfs/snapshot", s.auth(s.zfsSnapshot))
m.HandleFunc("GET /api/procs", s.auth(s.procList))
m.HandleFunc("POST /api/procs/{pid}/signal", s.auth(s.procSignal))
m.HandleFunc("POST /api/password", s.auth(s.changePassword))
m.HandleFunc("GET /api/prefs/ui", s.auth(s.getUIPrefs))
m.HandleFunc("PUT /api/prefs/ui", s.auth(s.putUIPrefs))
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") })
@@ -1190,6 +1193,61 @@ func (s *Server) procSignal(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(204)
}
// ---- account ----
func (s *Server) changePassword(w http.ResponseWriter, r *http.Request) {
var body struct{ Current, Password string }
if err := json.NewDecoder(io.LimitReader(r.Body, 4096)).Decode(&body); err != nil {
jsonErr(w, 400, "bad request")
return
}
if !auth.VerifyPassword(s.cfg.PasswordHash, body.Current) {
jsonErr(w, 403, "current password is wrong")
return
}
if len(body.Password) < 12 {
jsonErr(w, 400, "password must be at least 12 characters")
return
}
h, err := auth.HashPassword(body.Password)
if err != nil {
jsonErr(w, 500, err.Error())
return
}
if err := s.cfg.Update(func(c *config.Config) { c.PasswordHash = h }); err != nil {
jsonErr(w, 500, err.Error())
return
}
log.Printf("password changed (from %s)", clientIP(r))
w.WriteHeader(204)
}
// ---- ui prefs (theme) ----
func (s *Server) getUIPrefs(w http.ResponseWriter, _ *http.Request) {
var raw json.RawMessage
s.store.View(func(st *store.State) { raw = st.UI })
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) putUIPrefs(w http.ResponseWriter, r *http.Request) {
b, err := io.ReadAll(io.LimitReader(r.Body, 16<<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.UI = json.RawMessage(b) }); err != nil {
jsonErr(w, 500, err.Error())
return
}
w.WriteHeader(204)
}
// ---- dashboard layout prefs (opaque JSON object, owned by the UI) ----
func (s *Server) getDashPrefs(w http.ResponseWriter, _ *http.Request) {