// Endpoint detail drawer function NodeDetail({ node, onClose, onReboot, onMaint }) { // Local state must be declared unconditionally (Rules of Hooks). const [shotBust, setShotBust] = React.useState(0); const [shotBusy, setShotBusy] = React.useState(false); const [shotErr, setShotErr] = React.useState(null); if (!node) return
; const statusLabel = { ok: 'Up', warn: 'Warning', crit: 'Down', off: 'Offline', maint: 'Maintenance', unknown: 'Pending' }[node.status] || node.status; const shot = node.screenshot || { exists: false, capturedAt: 0 }; const shotSrc = shot.exists ? `/api/screenshots/${node.id}.jpg?v=${shotBust || shot.capturedAt}` : null; const shotAge = shot.capturedAt ? window.fmtRel(shot.capturedAt) : null; async function recaptureShot() { setShotBusy(true); setShotErr(null); try { const meta = await window.adminFetch(`/api/admin/screenshots/${node.id}`, { method: 'POST' }); setShotBust(meta.capturedAt || Date.now()); if (meta.error) setShotErr(meta.error); } catch (e) { setShotErr(e.message || 'capture failed'); } finally { setShotBusy(false); } } return (
{node.displayName}
{statusLabel}
{node.url}
{node.group} · {node.protocol}:{node.port}
Screenshot {shot.exists ? `captured ${shotAge}` : (shotBusy ? 'capturing…' : 'not yet captured')}
{shotSrc ? ( ) : (
{shotBusy ? 'Capturing first thumbnail…' : 'No screenshot yet · weekly capture runs in background'}
)} {shot.error && !shotBusy && (
last capture failed: {shot.error}
)}
{shotErr &&
recapture: {shotErr}
}
{[ { label:'response', val: node.status === 'off' ? '—' : `${node.latency.toFixed(0)}`, unit:'ms', color: node.latency > 800 ? 'var(--crit)' : 'var(--text)' }, { label:'http', val: node.httpCode === 0 ? '—' : node.httpCode, color: node.httpCode >= 500 ? 'var(--crit)' : node.httpCode >= 400 ? 'var(--warn)' : node.httpCode > 0 ? 'var(--ok)' : 'var(--text-mute)' }, { label:'tls', val: node.sslDaysLeft, unit:'d', color: node.sslDaysLeft < 14 ? 'var(--crit)' : node.sslDaysLeft < 30 ? 'var(--warn)' : 'var(--text)' }, ].map(k => (
{k.label}
{k.val} {k.unit || ''}
))}
Response time · last 60 checks
800 ? 'var(--crit)' : 'var(--accent)'} />
Network throughput
Details
url
{node.url}
hostname
{node.hostname}
path
{node.path}
protocol
{node.protocol}
port
{node.port}
group
{node.group}
uptime
{node.status === 'off' ? '—' : window.fmtUptime(node.uptime)}
tls expires
in {node.sslDaysLeft} days
agent
{node.status === 'off' ? no response : responsive}
probe
every 30s · HEAD
Recent probe log
{[ {ts: '-00:15', code: node.httpCode, ms: node.latency, ok: node.status === 'ok'}, {ts: '-00:45', code: node.httpCode, ms: node.latency * 0.96, ok: node.status === 'ok'}, {ts: '-01:15', code: node.httpCode, ms: node.latency * 1.02, ok: node.status === 'ok'}, {ts: '-01:45', code: node.httpCode, ms: node.latency * 0.93, ok: node.status === 'ok'}, ].map((p, i) => (
{p.ts} = 500 ? 'var(--crit)' : p.code >= 400 ? 'var(--warn)' : p.code > 0 ? 'var(--ok)' : 'var(--text-mute)'}}>{p.code || '—'} {node.status === 'off' ? '—' : `${p.ms.toFixed(0)}ms`} {p.ok ? '● ok' : '● fail'}
))}
); } Object.assign(window, { NodeDetail });