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
+21
View File
@@ -393,3 +393,24 @@ h2.section { margin: 0; font-size: 13px; font-weight: 600; color: var(--muted);
.header .link, .header .icon-btn { min-height: 36px; display: inline-flex; align-items: center; }
.mod-tools { margin-top: 0; }
}
/* sensor widgets */
.gauge-svg { width: 100%; max-width: 180px; display: block; margin: 0 auto; }
.w-gauge-v { fill: var(--fg); font-size: 13px; font-weight: 600; font-variant-numeric: tabular-nums; }
.tile .value.warn, .w-row .num.warn { color: var(--s2); } .tile .value.bad, .w-row .num.bad { color: var(--bad); }
.tile .meter { margin-top: 8px; }
.tile.w-list { grid-column: span 2; }
.w-list { display: flex; flex-direction: column; gap: 6px; }
.w-row { display: grid; grid-template-columns: 1fr auto; gap: 2px 10px; font-size: 13px; align-items: baseline; }
.w-row .k { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .w-row .meter { grid-column: 1 / 3; margin-top: 0; }
.tile.add-widget { border-style: dashed; color: var(--accent2); min-height: 72px; display: flex; align-items: center; justify-content: center; cursor: pointer; }
.modal.picker { width: min(520px, 100%); max-height: 90vh; }
.picker-list { overflow-y: auto; max-height: 34vh; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); }
.picker-row { display: flex; justify-content: space-between; gap: 10px; width: 100%; padding: 7px 10px; text-align: left; font-size: 12px; border-bottom: 1px solid var(--border); }
.picker-row:hover, .picker-row.active { background: var(--bg3); } .picker-row.active { box-shadow: inset 3px 0 0 var(--accent); }
@media (max-width: 800px) { .tile.w-list { grid-column: 1 / -1; } }
/* a widget alone in an iframe */
.embed { min-height: 100vh; padding: 10px; background: var(--bg2); box-sizing: border-box; display: flex; flex-direction: column; justify-content: center; }
.embed.bare { background: transparent; }
.embed .tile { border: 0; padding: 0; background: none; }