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:
co-authored by
Claude Fable 5.1
parent
468f45251d
commit
b05a053920
+19
-2
@@ -31,7 +31,8 @@ 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 }[] | null>(null)
|
||||
const [tokens, setTokens] = useState<{ name: string; created: string; scope?: string }[] | null>(null)
|
||||
const [fresh, setFresh] = useState<{ name: string; token: string } | null>(null)
|
||||
const [cur, setCur] = useState('')
|
||||
const [pw1, setPw1] = useState('')
|
||||
const [pw2, setPw2] = useState('')
|
||||
@@ -97,16 +98,32 @@ export default function Settings({ me }: { me: Me }) {
|
||||
|
||||
<div className="dash-bar"><h2 className="section">Access tokens</h2></div>
|
||||
<section className="card">
|
||||
<div className="muted small" style={{ marginBottom: 8 }}>Tokens let another Ham Lab (a hub) manage this host. They're issued when a hub adds this machine and revoked when it removes it.</div>
|
||||
<div className="muted small" style={{ marginBottom: 8 }}>A <b>full</b> token lets another Ham Lab (a hub) manage this host; hubs issue and revoke their own. A <b>read-only</b> token can only read sensors — that's the one to give a dashboard.</div>
|
||||
{tokens === null && <Loading label="Loading…" />}
|
||||
{tokens && tokens.length === 0 && <div className="empty">No tokens. No hub manages this host.</div>}
|
||||
{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="muted small">issued {new Date(t.created).toLocaleString()}</span>
|
||||
<Btn className="btn danger" onClick={() => revoke(t.name)}>Revoke</Btn>
|
||||
</div>
|
||||
))}
|
||||
{fresh && (
|
||||
<div className="notice attn" style={{ marginTop: 8 }}>
|
||||
<div>Token for <b>{fresh.name}</b> — shown once, copy it now:</div>
|
||||
<div className="mono small" style={{ wordBreak: 'break-all', userSelect: 'all', margin: '6px 0' }}>{fresh.token}</div>
|
||||
<Btn className="btn" onClick={async () => { try { await navigator.clipboard.writeText(fresh.token); toast('copied') } catch { toast('select and copy it by hand', 'err') } }}>Copy</Btn>{' '}
|
||||
<Btn className="btn" onClick={() => setFresh(null)}>Done</Btn>
|
||||
</div>
|
||||
)}
|
||||
<div className="svc-actions" style={{ marginTop: 8 }}>
|
||||
<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') }
|
||||
}}>+ Read-only sensor token</Btn>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="dash-bar"><h2 className="section">About</h2></div>
|
||||
|
||||
Reference in New Issue
Block a user