import { useCallback, useEffect, useRef, useState } from 'react'; import { api } from '../api.js'; import ServerDetail, { Meter } from '../components/ServerDetail.jsx'; import { fmtNum, fmtTemp, fmtUptime, fullestDisk, maxTemp } from '../serverStats.js'; const REFRESH_SECONDS = 30; const TYPES = { http: { label: 'Webseite (HTTP)', placeholder: 'https://jellyfin.example.de', hint: 'Online, wenn die Seite antwortet.' }, tcp: { label: 'Port (TCP)', placeholder: '192.168.0.10:22', hint: 'Online, wenn der Port Verbindungen annimmt.' }, agent: { label: 'Server-Auslastung (P42-S.M.)', placeholder: 'http://p42-sm:9142', hint: 'CPU, RAM, Platten, Netzwerk und Verlauf über den P42-S.M.-Agenten. Läuft er auf demselben Server, lautet die Adresse http://p42-sm:9142.', }, }; const fmtTime = (iso) => new Date(iso).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); export default function Monitoring() { const [monitors, setMonitors] = useState(null); const [status, setStatus] = useState({}); const [checking, setChecking] = useState(false); const [error, setError] = useState(null); const [editing, setEditing] = useState(null); // null | {} (neu) | monitor const [detail, setDetail] = useState(null); // geöffneter Server (Typ agent) const dialog = useRef(null); const check = useCallback(async () => { setChecking(true); try { const results = await api('GET', '/api/monitors/status'); setStatus(Object.fromEntries(results.map((r) => [r.id, r]))); } catch (e) { setError(e.message); } finally { setChecking(false); } }, []); const load = useCallback(async () => { try { setMonitors(await api('GET', '/api/monitors')); await check(); } catch (e) { setError(e.message); } }, [check]); useEffect(() => { load(); const timer = setInterval(check, REFRESH_SECONDS * 1000); return () => clearInterval(timer); }, [load, check]); // Erst rendern (Formular ist dann schon befüllt), dann öffnen function openDialog(monitor) { setEditing({ ...(monitor || {}), opened: Date.now() }); } useEffect(() => { if (editing) dialog.current?.showModal(); }, [editing]); async function remove(m) { if (!window.confirm(`„${m.name}“ aus dem Monitoring entfernen?`)) return; try { await api('DELETE', `/api/monitors/${m.id}`); setMonitors((list) => list.filter((x) => x.id !== m.id)); } catch (e) { setError(e.message); } } const online = monitors ? monitors.filter((m) => status[m.id]?.ok).length : 0; const lastCheck = Object.values(status).map((s) => s.checkedAt).sort().at(-1); return ( <>

Server-Monitoring

Deine Server – nur für dein Konto sichtbar. Wird alle {REFRESH_SECONDS} Sekunden geprüft.

{monitors?.length > 0 && ( )}
{error &&

{error}

} {!monitors && !error &&

Lade …

} {monitors?.length === 0 && (
Noch keine Server eingetragen. Über „+ Server hinzufügen“ legst du fest, welche Webseiten oder Ports geprüft werden.
)} {monitors?.length > 0 && ( <>

{online} von {monitors.length} online {lastCheck && Stand {fmtTime(lastCheck)}}

{monitors.map((m) => { const s = status[m.id]; const state = !s ? 'unknown' : s.ok ? 'ok' : 'bad'; const isAgent = m.type === 'agent'; // Server-Karten öffnen per Klick die große Ansicht const open = isAgent ? { role: 'button', tabIndex: 0, 'aria-label': `${m.name}: Details und Verlauf öffnen`, onClick: (e) => !e.target.closest('button') && setDetail(m), onKeyDown: (e) => (e.key === 'Enter' || e.key === ' ') && e.target === e.currentTarget && (e.preventDefault(), setDetail(m)), } : {}; return (

{m.name}

{state === 'ok' ? 'Online' : state === 'bad' ? 'Offline' : 'Wird geprüft …'}

{isAgent && s?.metrics ? `${s.metrics.host.hostname} · ${s.metrics.host.os}` : m.target}

{isAgent && s?.metrics ? ( ) : (
Typ
{TYPES[m.type]?.label}
Antwortzeit
{s?.ms != null ? `${s.ms} ms` : '—'}
Ergebnis
{s?.detail || '—'}
)}
{s ? `Geprüft ${fmtTime(s.checkedAt)}` : ''}{isAgent && ' · Klicken für Details'}
); })}
)} {detail && ( setDetail(null)} /> )} ); } function AgentSummary({ metrics: x }) { const disk = fullestDisk(x); return (
{disk && }
Load
{fmtNum(x.cpu.load['1m'])}
Temperatur
{fmtTemp(maxTemp(x))}
Läuft seit
{fmtUptime(x.host.uptimeSeconds)}
); } function MonitorDialog({ ref, monitor, onSaved }) { // Wird pro Öffnen neu erzeugt (key beim Aufrufer), daher direkt vorbelegen const [form, setForm] = useState(() => ({ name: monitor?.name || '', type: monitor?.type || 'http', target: monitor?.target || '', token: '' })); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); const isNew = !monitor?.id; const field = (name) => ({ value: form[name], onChange: (e) => setForm((f) => ({ ...f, [name]: e.target.value })) }); async function submit(e) { e.preventDefault(); setBusy(true); setError(null); try { const body = { ...form, token: form.type === 'agent' ? form.token : undefined }; if (isNew) await api('POST', '/api/monitors', body); else await api('PUT', `/api/monitors/${monitor.id}`, body); ref.current.close(); await onSaved(); } catch (err) { setError(err.message); } finally { setBusy(false); } } const type = TYPES[form.type]; return (

{isNew ? 'Server hinzufügen' : 'Server bearbeiten'}

{form.type === 'agent' && ( )}

{type.hint}

{error &&

{error}

}
); }