Quality of life: in-app dialogs, selections in the URL, editable widgets, token dialog, gallery search, status history, stream errors that say why, tab reordering and shortcuts, log out everywhere
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
8ee2a2d293
commit
d7ed4d8549
@@ -107,6 +107,7 @@ func New(cfg *config.Config, sessions *auth.Sessions, mgr *term.Manager, col *me
|
||||
m.HandleFunc("POST /api/machines/{name}/update", s.auth(s.updateMachine))
|
||||
m.HandleFunc("POST /api/machines-update-all", s.auth(s.updateAllMachines))
|
||||
m.HandleFunc("POST /api/logout", s.auth(s.logout))
|
||||
m.HandleFunc("POST /api/logout/all", s.auth(s.logoutAll))
|
||||
m.HandleFunc("GET /api/me", s.auth(s.me))
|
||||
m.HandleFunc("GET /api/terminals", s.auth(s.listTerminals))
|
||||
m.HandleFunc("POST /api/terminals", s.auth(s.createTerminal))
|
||||
@@ -708,6 +709,15 @@ func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(204)
|
||||
}
|
||||
|
||||
// logoutAll ends every session, this one included: a lost phone, a borrowed laptop.
|
||||
// Tokens are not sessions and are untouched; revoke those by name.
|
||||
func (s *Server) logoutAll(w http.ResponseWriter, r *http.Request) {
|
||||
n := s.sessions.DeleteAll()
|
||||
log.Printf("all %d session(s) ended (from %s)", n, clientIP(r))
|
||||
s.setCookie(w, r, "", -1)
|
||||
jsonOK(w, 200, map[string]int{"ended": n})
|
||||
}
|
||||
|
||||
// clientKey is the address the limiter buckets by: the first X-Forwarded-For hop when a
|
||||
// reverse proxy set it, else the socket peer.
|
||||
func clientKey(r *http.Request) string {
|
||||
|
||||
@@ -25,8 +25,14 @@ type statusSensors struct {
|
||||
by map[string][]sensors.Sensor // source → its sensors
|
||||
|
||||
cpuPrev map[string]cpuMark // container ID → CPU time at the previous read
|
||||
|
||||
hist map[string]*sensors.Series // numeric sensors only, statusHistory points each
|
||||
}
|
||||
|
||||
// statusHistory is how many readings each status sensor keeps: at 15 s that is over an
|
||||
// hour of container memory; at 10 min, two days of drive wear.
|
||||
const statusHistory = 300
|
||||
|
||||
const (
|
||||
servicesEvery = 30 * time.Second
|
||||
containersEvery = 15 * time.Second
|
||||
@@ -36,7 +42,7 @@ const (
|
||||
)
|
||||
|
||||
func newStatusSensors(s *Server) *statusSensors {
|
||||
p := &statusSensors{s: s, by: map[string][]sensors.Sensor{}}
|
||||
p := &statusSensors{s: s, by: map[string][]sensors.Sensor{}, hist: map[string]*sensors.Series{}}
|
||||
go p.loop("services", servicesEvery, p.services)
|
||||
go p.loop("containers", containersEvery, p.containers)
|
||||
go p.loop("updates", updatesEvery, p.updates)
|
||||
@@ -52,6 +58,28 @@ func (p *statusSensors) loop(name string, every time.Duration, read func(context
|
||||
cancel()
|
||||
p.mu.Lock()
|
||||
p.by[name] = got
|
||||
live := map[string]bool{}
|
||||
for _, x := range got {
|
||||
v, ok := x.Value.(float64)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
live[x.ID] = true
|
||||
h := p.hist[x.ID]
|
||||
if h == nil {
|
||||
h = &sensors.Series{ID: x.ID, Unit: x.Unit}
|
||||
p.hist[x.ID] = h
|
||||
}
|
||||
h.T, h.V = append(h.T, x.T), append(h.V, v)
|
||||
if len(h.T) > statusHistory {
|
||||
h.T, h.V = h.T[len(h.T)-statusHistory:], h.V[len(h.V)-statusHistory:]
|
||||
}
|
||||
}
|
||||
for id := range p.hist { // a container that is gone takes its history with it
|
||||
if strings.HasPrefix(id, prefixOf[name]) && !live[id] && prefixOf[name] != "" {
|
||||
delete(p.hist, id)
|
||||
}
|
||||
}
|
||||
p.mu.Unlock()
|
||||
if len(got) == 0 { // no pools, no Docker, no SMART: look again, but rarely
|
||||
time.Sleep(10 * every)
|
||||
@@ -61,6 +89,22 @@ func (p *statusSensors) loop(name string, every time.Duration, read func(context
|
||||
}
|
||||
}
|
||||
|
||||
// prefixOf says which IDs a source owns, so its loop forgets only its own departed sensors.
|
||||
var prefixOf = map[string]string{"containers": "container.", "smart": "smart.", "zfs": "zfs."}
|
||||
|
||||
// SensorHistories implements sensors.Historian.
|
||||
func (p *statusSensors) SensorHistories(match func(id string) bool) []sensors.Series {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
var out []sensors.Series
|
||||
for id, h := range p.hist {
|
||||
if match(id) {
|
||||
out = append(out, sensors.Series{ID: h.ID, Unit: h.Unit, T: append([]int64{}, h.T...), V: append([]float64{}, h.V...)})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (p *statusSensors) Sensors() []sensors.Sensor {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
||||
@@ -146,6 +146,15 @@ func (s *Sessions) Valid(tok string) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
// DeleteAll ends every session on every device and says how many there were.
|
||||
func (s *Sessions) DeleteAll() (n int) {
|
||||
_ = s.st.Update(func(state *store.State) {
|
||||
n = len(state.Sessions)
|
||||
state.Sessions = map[string]time.Time{}
|
||||
})
|
||||
return n
|
||||
}
|
||||
|
||||
func (s *Sessions) Delete(tok string) {
|
||||
_ = s.st.Update(func(state *store.State) { delete(state.Sessions, tok) })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user