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 (
);
}