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:
Mitchell-Johnson
2026-09-19 10:34:59 -07:00
co-authored by Claude Fable 5.1
parent 8ee2a2d293
commit d7ed4d8549
24 changed files with 361 additions and 101 deletions
+45 -1
View File
@@ -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()