Sensors: read-only tokens, live stream, Prometheus, status sensors, widgets, embeds

- sensors:read tokens open the sensor endpoints (here and on managed machines
  through the hub) and nothing else; full tokens are never accepted from a URL
- /api/sensors/stream: a shaped query that keeps answering, over SSE
- /metrics: Prometheus text format
- services, updates, ZFS and SMART as background status sensors
- widget library over the sensor API; custom widgets on the dashboard
- /embed/widget: one live widget in its own bundle, the only frameable path
- Dashboard tab is now Monitors

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-19 07:37:10 -07:00
co-authored by Claude Fable 5.1
parent 468f45251d
commit b05a053920
20 changed files with 950 additions and 56 deletions
+25
View File
@@ -165,6 +165,9 @@ type Collector struct {
procAt time.Time
lastTable *ProcTable
upsProbe func() *UPS // nil when no UPS daemon is installed
subMu sync.Mutex
subs map[chan struct{}]struct{} // told after every sample; see Subscribe
}
func NewCollector() *Collector {
@@ -309,6 +312,28 @@ func (c *Collector) sample() {
c.ring = c.ring[len(c.ring)-History:]
}
c.mu.Unlock()
c.subMu.Lock()
for ch := range c.subs {
select {
case ch <- struct{}{}:
default: // a slow listener misses a tick; it reads the latest on the next one
}
}
c.subMu.Unlock()
}
// Subscribe returns a channel that gets a nudge after every new sample, and the func
// that ends the subscription.
func (c *Collector) Subscribe() (<-chan struct{}, func()) {
ch := make(chan struct{}, 1)
c.subMu.Lock()
if c.subs == nil {
c.subs = map[chan struct{}]struct{}{}
}
c.subs[ch] = struct{}{}
c.subMu.Unlock()
return ch, func() { c.subMu.Lock(); delete(c.subs, ch); c.subMu.Unlock() }
}
type cpuTimes struct{ busy, total uint64 }