// Agents admin — Windows machines that send heartbeats to the central // server (instead of being probed). Lives at /agents view; admin-only. // // Data flow: // GET /api/admin/agents list // POST /api/admin/agents create → returns enrollment_token (visible ONLY here) // PATCH /api/admin/agents/{id} rename / change group / disable // POST /api/admin/agents/{id}/pause pause heartbeat alerting // POST /api/admin/agents/{id}/resume // POST /api/admin/agents/{id}/regenerate-enrollment // DELETE /api/admin/agents/{id} // // Hooks rules: every useState / useEffect declared above any early // return — same pattern as users_admin.jsx and admin.jsx. const AGENT_INPUT = { width: '100%', padding: '5px 9px', background: 'var(--surface)', border: '1px solid var(--border-2)', borderRadius: 2, color: 'var(--text)', fontFamily: 'var(--mono)', fontSize: 12, outline: 'none', }; function EnrollmentTokenModal({ token, agent, expires, onClose, onCopied }) { const [copied, setCopied] = React.useState(false); if (!token) return null; const expiresIn = expires ? Math.max(0, Math.round((expires * 1000 - Date.now()) / 3600000)) : 24; async function copy() { try { await navigator.clipboard.writeText(token); setCopied(true); onCopied && onCopied(); setTimeout(() => setCopied(false), 1800); } catch (_) { window.prompt('Copy this enrollment code:', token); } } return (
Enrollment code · {agent ? agent.label : ''}
Този код се показва само сега. Копирай го и го въведи в pulse-agent.exe wizard-а на Windows машината. Изтича след ~{expiresIn}h.
{token}
download pulse-agent.exe
); } function AgentEditor({ agent, onClose, onSaved }) { const open = !!agent; const [label, setLabel] = React.useState(''); const [group, setGroup] = React.useState(''); const [busy, setBusy] = React.useState(false); const [err, setErr] = React.useState(null); React.useEffect(() => { if (!agent) return; setLabel(agent.label || ''); setGroup(agent.group || 'Windows clients'); setErr(null); }, [agent?.id]); if (!open) return
; async function save() { setBusy(true); setErr(null); try { const updated = await window.apiFetch(`/api/admin/agents/${agent.id}`, { method: 'PATCH', body: JSON.stringify({ label: label.trim(), group: group.trim() }), }); onSaved(updated); } catch (e) { setErr(e.message || 'save failed'); } finally { setBusy(false); } } return (
Edit agent
{agent.id}
setLabel(e.target.value)} placeholder="Reception PC – CityDent Sofia" />
setGroup(e.target.value)} placeholder="Windows clients" />
{agent.hostname || '—'}{agent.windowsUser ? ` · user ${agent.windowsUser}` : ''}
machine guid: {agent.machineGuid || '—'}
mac: {(agent.macAddresses || []).join(', ') || '—'}
agent v{agent.agentVersion || '—'}
{err &&
{err}
}
); } function AddAgentForm({ onCreated }) { const [label, setLabel] = React.useState(''); const [group, setGroup] = React.useState('Windows clients'); const [busy, setBusy] = React.useState(false); const [err, setErr] = React.useState(null); async function submit(e) { e.preventDefault(); setBusy(true); setErr(null); try { const res = await window.apiFetch('/api/admin/agents', { method: 'POST', body: JSON.stringify({ label: label.trim(), group: group.trim() || 'Windows clients' }), }); onCreated(res); setLabel(''); } catch (e) { setErr(e.message || 'create failed'); } finally { setBusy(false); } } return (
setLabel(e.target.value)} placeholder="Reception PC – CityDent Sofia" required />
setGroup(e.target.value)} placeholder="Windows clients" />
{err &&
{err}
}
); } function AgentsAdmin() { const [agents, setAgents] = React.useState(null); const [editing, setEditing] = React.useState(null); const [confirmDelete, setConfirmDelete] = React.useState(null); const [tokenModal, setTokenModal] = React.useState(null); // { token, agent, expires } const [err, setErr] = React.useState(null); const [now, setNow] = React.useState(Date.now()); async function load() { setErr(null); try { const data = await window.apiFetch('/api/admin/agents'); setAgents(data.agents || []); } catch (e) { if (e.message !== 'unauthorized') setErr(e.message); } } React.useEffect(() => { load(); }, []); React.useEffect(() => { // Refresh list every 10s so liveness flips visible without manual reload. const t = setInterval(load, 10_000); // Tick state every second so the "last seen" relative timestamps move. const tick = setInterval(() => setNow(Date.now()), 1000); return () => { clearInterval(t); clearInterval(tick); }; }, []); async function onCreated(res) { setTokenModal({ token: res.enrollmentToken, agent: res.agent, expires: res.enrollmentExpires }); await load(); } async function onSaved() { setEditing(null); await load(); } async function onTogglePause(a) { try { await window.apiFetch(`/api/admin/agents/${a.id}/${a.paused ? 'resume' : 'pause'}`, { method: 'POST' }); await load(); } catch (e) { setErr(e.message); } } async function onRegenerate(a) { if (!confirm(`Re-issue enrollment code for "${a.label || a.id}"?\n\nThe existing device key will be revoked — the running pulse-agent.exe will start failing with 401.`)) return; try { const res = await window.apiFetch(`/api/admin/agents/${a.id}/regenerate-enrollment`, { method: 'POST' }); setTokenModal({ token: res.enrollmentToken, agent: res.agent, expires: res.enrollmentExpires }); await load(); } catch (e) { setErr(e.message); } } async function onDelete(a) { try { await window.apiFetch(`/api/admin/agents/${a.id}`, { method: 'DELETE' }); await load(); } catch (e) { setErr(e.message); } } const counts = React.useMemo(() => { if (!agents) return { ok: 0, warn: 0, crit: 0, unknown: 0, paused: 0 }; const c = { ok: 0, warn: 0, crit: 0, unknown: 0, paused: 0 }; for (const a of agents) { if (a.paused) { c.paused++; continue; } c[a.status] = (c[a.status] || 0) + 1; } return c; }, [agents]); return (

Windows agents

{agents ? `${agents.length} · ${counts.ok} live · ${counts.warn} late · ${counts.crit} offline · ${counts.paused} paused` : 'loading…'} download .exe
{err &&
{err}
}
{(agents || []).map(a => { const lastText = a.lastSeen ? window.fmtRel(a.lastSeen) : (a.enrollmentPending ? 'awaiting enrollment' : 'never'); const statusLabel = a.paused ? 'paused' : a.status === 'ok' ? 'live' : a.status === 'warn' ? 'late' : a.status === 'crit' ? 'offline' : a.status === 'unknown' ? 'pending' : a.status; return ( ); })} {(!agents || agents.length === 0) && ( )}
label hostname group last seen version status actions
{a.label || a.id} {a.hostname || '—'}{a.windowsUser ? ` · ${a.windowsUser}` : ''} {a.group} {lastText} {a.agentVersion || '—'} {statusLabel}
{confirmDelete === a.id ? ( ) : ( )}
{agents === null ? 'Loading…' : 'No agents yet. Click "new agent" to create your first enrollment code.'}

How it works

  1. Click new agent. Enter a friendly label (e.g. "Reception PC – Sofia").
  2. Copy the one-time enrollment code shown in the modal — it's visible only once.
  3. Download pulse-agent.exe on the Windows machine and run it.
  4. The wizard asks for the server URL (default https://pulse.sitnov.work) and the enrollment code.
  5. Once enrolled, the agent runs in the system tray and pings the server every ~30 seconds.
  6. If a heartbeat is missed for 90 s the row goes offline and the regular alert pipeline fires.
setEditing(null)} onSaved={onSaved} /> {tokenModal && ( setTokenModal(null)} /> )}
); } Object.assign(window, { AgentsAdmin });