Sensors: per-service and container sensors, pattern-limited tokens, fleet query, steady sampler

- service.<unit>.active|state for services that are enabled, up or failed
- containers.* and container.<name>.running|state|health|mem|cpu (cgroup reads, no docker stats)
- sensors:read tokens can be limited to ID patterns, enforced on every endpoint and,
  through the hub's filter header, on managed machines; the header can only narrow
- /api/fleet/sensors/query: one shaped query, every machine, one request
- the sampler waits what's left of the interval, so 250 ms means 4 a second
- Monitors asks for the families it draws instead of *
- unpowered hwmon sentinels (-93 °C) are not temperatures

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-19 08:08:44 -07:00
co-authored by Claude Fable 5.1
parent 4ddf10be51
commit 4c2eb57ea6
11 changed files with 383 additions and 51 deletions
+5 -3
View File
@@ -31,7 +31,7 @@ function Preview({ vars }: { vars: Record<string, string> }) {
export default function Settings({ me }: { me: Me }) {
const [theme, setTheme] = useState(currentThemeId())
const [refresh, setRefresh] = useState(getRefreshMs())
const [tokens, setTokens] = useState<{ name: string; created: string; scope?: string }[] | null>(null)
const [tokens, setTokens] = useState<{ name: string; created: string; scope?: string; sensors?: string[] }[] | null>(null)
const [fresh, setFresh] = useState<{ name: string; token: string } | null>(null)
const [cur, setCur] = useState('')
const [pw1, setPw1] = useState('')
@@ -104,7 +104,7 @@ export default function Settings({ me }: { me: Me }) {
{tokens && tokens.map((t) => (
<div className="row" key={t.name}>
<span className="k mono">{t.name}</span>
<span className="badge" style={{ marginLeft: 0 }}>{t.scope ? 'read-only' : 'full'}</span>
<span className="badge" style={{ marginLeft: 0 }} title={t.sensors?.length ? t.sensors.join(', ') : undefined}>{t.scope ? (t.sensors?.length ? `read-only · ${t.sensors.join(' ')}` : 'read-only') : 'full'}</span>
<span className="muted small">issued {new Date(t.created).toLocaleString()}</span>
<Btn className="btn danger" onClick={() => revoke(t.name)}>Revoke</Btn>
</div>
@@ -121,7 +121,9 @@ export default function Settings({ me }: { me: Me }) {
<Btn className="btn" onClick={async () => {
const name = window.prompt('Name for the read-only token (what will use it)', 'glance')?.trim()
if (!name) return
try { setFresh(await api.createReadToken(name)); loadTokens() } catch (err) { toast(err instanceof Error ? err.message : 'failed', 'err') }
const pats = window.prompt('Limit it to sensor patterns? Comma-separated, e.g. cpu.*, temp.*, zfs.tank.* — leave empty for every sensor', '')
if (pats === null) return
try { setFresh(await api.createReadToken(name, pats.split(',').map((x) => x.trim()).filter(Boolean))); loadTokens() } catch (err) { toast(err instanceof Error ? err.message : 'failed', 'err') }
}}>+ Read-only sensor token</Btn>
</div>
</section>