The full switch: Monitors and Disks read only the sensor API; /api/metrics retired

- stock Monitors modules fed by one sensor stream, the catalog and one batch of
  histories (GET /api/sensors/history?ids=…), through sampleAdapter.ts
- the stream closes while the page is hidden and refills its curves on return
- Disks tab live rates from the stream
- new sensors: hamlab.sample_interval, ups.last_event
- stream: asking for about the sampling rate means every sample
- NUT runtime rounded at the source

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-19 07:52:14 -07:00
co-authored by Claude Fable 5.1
parent f01175ceca
commit 4ddf10be51
13 changed files with 283 additions and 173 deletions
+39 -7
View File
@@ -46,9 +46,10 @@ type Provider interface {
Sensors() []Sensor
}
// Historian is a Provider that also remembers. ok is false for an ID it doesn't hold.
// Historian is a Provider that also remembers: the recent past of every numeric sensor
// of its own that match accepts.
type Historian interface {
SensorHistory(id string) (s Series, ok bool)
SensorHistories(match func(id string) bool) []Series
}
// Ticker is a Provider that can say when its values have changed.
@@ -125,16 +126,47 @@ func (r *Registry) All() []Sensor {
return out
}
// History finds the provider that remembers id.
// History is one sensor's recent past. ok is false when nothing remembers it.
func (r *Registry) History(id string) (Series, bool) {
got := r.Histories(func(x string) bool { return x == id })
if len(got) == 0 {
return Series{}, false
}
return got[0], true
}
// Histories is the recent past of every numeric sensor match accepts, sorted by ID.
func (r *Registry) Histories(match func(id string) bool) []Series {
out := []Series{}
for _, p := range r.providers {
if h, ok := p.(Historian); ok {
if s, ok := h.SensorHistory(id); ok {
return s, true
}
out = append(out, h.SensorHistories(match)...)
}
}
return Series{}, false
sort.SliceStable(out, func(i, j int) bool { return out[i].ID < out[j].ID })
return out
}
// Matcher turns comma-separated selectors (IDs and globs) into a match function.
func Matcher(selectors string) (func(id string) bool, error) {
var sels []string
for _, sel := range strings.Split(selectors, ",") {
if sel = strings.TrimSpace(sel); sel == "" {
continue
}
if _, err := path.Match(sel, ""); err != nil {
return nil, fmt.Errorf("bad pattern %q", sel)
}
sels = append(sels, sel)
}
return func(id string) bool {
for _, sel := range sels {
if ok, _ := path.Match(sel, id); ok {
return true
}
}
return false
}, nil
}
// IsGlob reports whether a selector names many sensors rather than one.