import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Link } from 'react-router-dom'; import networkMonitorService from '../services/networkMonitorService'; import monitoringService from '../services/monitoringService'; import unifiService from '../services/unifiService'; import externalAlertService from '../services/externalAlertService'; import LoadingSpinner from '../components/common/LoadingSpinner'; import { toast } from 'react-toastify'; // ─── Constants ──────────────────────────────────────────────────────────────── const TYPE_META = { host: { label: 'Host', icon: '🖥' }, switch: { label: 'Switch', icon: '🔀' }, ap: { label: 'Access Point',icon: '📶' }, router: { label: 'Router', icon: '🌐' }, printer: { label: 'Drucker', icon: '🖨' }, nas: { label: 'NAS', icon: '💾' }, service: { label: 'Service', icon: '⚙' }, }; const CHECK_TYPES = ['icmp', 'http', 'https', 'tcp', 'snmp']; const INTERVALS = [ { value: 15, label: '15 Sek' }, { value: 30, label: '30 Sek' }, { value: 60, label: '1 Min' }, { value: 300, label: '5 Min' }, { value: 600, label: '10 Min' }, ]; const REFRESH_INTERVAL = 30; const MAX_TS_POINTS = 60; // State colors – IT Nexus palette (nicht Checkmk!) const SC = { ok: '#34d399', warn: '#f59e0b', crit: '#ef4444', unknown: '#6b7280' }; // ─── Helpers ────────────────────────────────────────────────────────────────── const timeAgo = (iso) => { if (!iso) return '–'; const m = Math.floor((Date.now() - new Date(iso).getTime()) / 60000); if (m < 1) return 'gerade eben'; if (m < 60) return `vor ${m} Min`; if (m < 1440) return `vor ${Math.floor(m / 60)} Std`; return `vor ${Math.floor(m / 1440)} Tagen`; }; const duration = (iso) => { if (!iso) return '–'; const m = Math.floor((Date.now() - new Date(iso).getTime()) / 60000); if (m < 60) return `${m} Min`; const h = Math.floor(m / 60), rm = m % 60; if (h < 24) return `${h}h ${rm}min`; return `${Math.floor(h / 24)}d ${h % 24}h`; }; const pct = (val, total) => (total > 0 ? Math.min(100, Math.round((val / total) * 100)) : 0); const uptimeStr = (h) => { if (!h) return '–'; if (h < 24) return `${Math.floor(h)}h`; return `${Math.floor(h / 24)}d ${Math.floor(h % 24)}h`; }; const netColor = (s) => s === 'up' ? SC.ok : s === 'down' ? SC.crit : SC.unknown; const netLabel = (s) => s === 'up' ? 'UP' : s === 'down' ? 'DOWN' : 'UNBEKANNT'; const fmtTime = (d) => d instanceof Date ? d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) : '–'; const warnings = (a) => { const w = []; if (a.cpu_usage_percent > 90) w.push({ type: 'error', msg: `CPU ${a.cpu_usage_percent?.toFixed(0)}%` }); else if (a.cpu_usage_percent > 80) w.push({ type: 'warn', msg: `CPU ${a.cpu_usage_percent?.toFixed(0)}%` }); if (a.disk_free_gb != null && a.disk_free_gb < 5) w.push({ type: 'error', msg: `Disk ${a.disk_free_gb?.toFixed(1)}GB` }); else if (a.disk_free_gb != null && a.disk_free_gb < 15) w.push({ type: 'warn', msg: `Disk ${a.disk_free_gb?.toFixed(0)}GB` }); if (a.ram_total_gb > 0 && pct(a.ram_used_gb, a.ram_total_gb) > 90) w.push({ type: 'warn', msg: 'RAM voll' }); if (a.windows_updates_pending > 20) w.push({ type: 'error', msg: `${a.windows_updates_pending} Updates` }); else if (a.windows_updates_pending > 5) w.push({ type: 'warn', msg: `${a.windows_updates_pending} Updates` }); if (a.bitlocker_status === 'off') w.push({ type: 'warn', msg: 'BitLocker aus' }); if (a.defender_enabled === 0) w.push({ type: 'error', msg: 'Defender inaktiv' }); else if (a.defender_signatures_age > 7) w.push({ type: 'warn', msg: `Defender ${a.defender_signatures_age}d alt` }); return w; }; const agentServices = (a) => [ { name: 'Agent Verbindung', status: a.status === 'online' ? 'ok' : 'crit', value: a.status === 'online' ? 'Online' : 'Offline', detail: `Checkin: ${timeAgo(a.last_checkin)}`, perf: null }, { name: 'CPU Auslastung', status: a.cpu_usage_percent > 90 ? 'crit' : a.cpu_usage_percent > 80 ? 'warn' : 'ok', value: a.cpu_usage_percent != null ? `${a.cpu_usage_percent.toFixed(1)}%` : '–', detail: a.cpu_model?.replace(/\(R\)|\(TM\)/g, '') || '', perf: a.cpu_usage_percent != null ? { val: a.cpu_usage_percent, max: 100, warn: 80, crit: 90, unit: '%' } : null }, { name: 'RAM Auslastung', status: pct(a.ram_used_gb, a.ram_total_gb) > 95 ? 'crit' : pct(a.ram_used_gb, a.ram_total_gb) > 85 ? 'warn' : 'ok', value: a.ram_used_gb != null ? `${a.ram_used_gb.toFixed(1)} / ${a.ram_total_gb?.toFixed(0)} GB` : '–', detail: `${pct(a.ram_used_gb, a.ram_total_gb)}% belegt`, perf: a.ram_total_gb > 0 ? { val: a.ram_used_gb, max: a.ram_total_gb, warn: 85, crit: 95, unit: '%' } : null }, { name: 'Festplatte C:', status: (a.disk_free_gb ?? 99) < 5 ? 'crit' : (a.disk_free_gb ?? 99) < 15 ? 'warn' : 'ok', value: a.disk_free_gb != null ? `${a.disk_free_gb.toFixed(0)} GB frei` : '–', detail: `von ${a.disk_total_gb?.toFixed(0) ?? '–'} GB`, perf: a.disk_total_gb > 0 ? { val: a.disk_total_gb - a.disk_free_gb, max: a.disk_total_gb, warn: 75, crit: 90, unit: '%', labelVal: `${a.disk_free_gb?.toFixed(0)} GB frei` } : null }, { name: 'Windows Updates', status: (a.windows_updates_pending ?? 0) > 20 ? 'crit' : (a.windows_updates_pending ?? 0) > 5 ? 'warn' : 'ok', value: `${a.windows_updates_pending ?? 0} ausstehend`, detail: '', perf: null }, ]; const netDeviceServices = (d, uptimeStat) => { const rtt = d.last_rtt_ms; const up = uptimeStat?.pct ?? null; return [ { name: 'ICMP / Ping', status: d.last_status === 'up' ? 'ok' : d.last_status === 'down' ? 'crit' : 'unknown', value: d.last_status === 'up' ? 'UP' : d.last_status === 'down' ? 'DOWN' : 'UNBEKANNT', detail: rtt != null ? `${Math.round(rtt)} ms` : '', perf: null }, { name: 'Antwortzeit', status: rtt == null ? 'unknown' : rtt > 1000 ? 'crit' : rtt > 200 ? 'warn' : 'ok', value: rtt != null ? `${Math.round(rtt)} ms` : '–', detail: '', perf: rtt != null ? { val: rtt, max: 1000, warn: 200, crit: 500, unit: 'ms', labelVal: `${Math.round(rtt)}ms` } : null }, ...(up != null ? [{ name: 'Uptime (24h)', status: up >= 99 ? 'ok' : up >= 90 ? 'warn' : 'crit', value: `${up}%`, detail: '', perf: { val: 100 - up, max: 100, warn: 10, crit: 20, unit: '%', labelVal: `${up}% up` } }] : []), ]; }; const proxmoxServices = (px) => { if (!px?.node) return []; const { node, vms = [], lxcs = [], storage = [] } = px; const cpuPct = (node.cpu || 0) * 100; const ramPct = node.memory?.total > 0 ? (node.memory.used / node.memory.total) * 100 : 0; const fsPct = node.rootfs?.total > 0 ? (node.rootfs.used / node.rootfs.total) * 100 : 0; const load = node.loadavg?.[0] || 0; const cpus = node.cpuinfo?.cpus || 1; const runVMs = vms.filter(v => v.status === 'running').length; const runCTs = lxcs.filter(c => c.status === 'running').length; const svcs = [ { name: 'CPU Auslastung', status: cpuPct >= 90 ? 'crit' : cpuPct >= 70 ? 'warn' : 'ok', value: `${cpuPct.toFixed(1)}%`, detail: `${node.cpuinfo?.cpus || '?'} CPUs`, perf: { val: cpuPct, max: 100, warn: 70, crit: 90, unit: '%' } }, { name: 'RAM Auslastung', status: ramPct >= 90 ? 'crit' : ramPct >= 80 ? 'warn' : 'ok', value: `${ramPct.toFixed(1)}%`, detail: `${(node.memory?.used/1073741824).toFixed(1)} / ${(node.memory?.total/1073741824).toFixed(1)} GB`, perf: { val: ramPct, max: 100, warn: 80, crit: 90, unit: '%' } }, { name: 'Root Filesystem', status: fsPct >= 90 ? 'crit' : fsPct >= 80 ? 'warn' : 'ok', value: `${fsPct.toFixed(1)}%`, detail: `${(node.rootfs?.avail/1073741824).toFixed(1)} GB frei`, perf: { val: fsPct, max: 100, warn: 80, crit: 90, unit: '%' } }, { name: 'Load Average', status: load > cpus * 0.9 ? 'crit' : load > cpus * 0.7 ? 'warn' : 'ok', value: load.toFixed(2), detail: `${node.loadavg?.map(l => l.toFixed(2)).join(' / ') || ''}`, perf: { val: load, max: cpus, warn: cpus * 0.7, crit: cpus * 0.9, unit: '', labelVal: load.toFixed(2) } }, { name: 'VMs', status: 'ok', value: `${runVMs}/${vms.length} laufend`, detail: '', perf: null }, { name: 'Container', status: 'ok', value: `${runCTs}/${lxcs.length} laufend`, detail: '', perf: null }, ...storage.filter(s => s.active && s.total > 0).map(s => { const p = (s.used / s.total) * 100; return { name: `Storage: ${s.storage}`, status: p >= 90 ? 'crit' : p >= 80 ? 'warn' : 'ok', value: `${p.toFixed(1)}%`, detail: `${(s.avail/1073741824).toFixed(1)} GB frei`, perf: { val: p, max: 100, warn: 80, crit: 90, unit: '%' } }; }), ]; return svcs; }; // ─── UI Primitives ───────────────────────────────────────────────────────────── // Checkmk-style State-Square: kleines farbiges Label mit Zustandstext const StateSquare = ({ state, text, size = 'sm' }) => { const color = SC[state] || SC.unknown; const label = text || state?.toUpperCase() || '?'; return ( {label} ); }; // Checkmk-style Perf-O-Meter mit WARN/CRIT Schwelllinien const PerfBar = ({ val, max, warn = 80, crit = 90, unit = '%', labelVal = null }) => { if (val == null || max == null || max === 0) return ; const fillPct = Math.min((val / max) * 100, 100); const warnPct = warn; const critPct = crit; const color = fillPct >= critPct ? SC.crit : fillPct >= warnPct ? SC.warn : SC.ok; const display = labelVal ?? `${fillPct.toFixed(0)}${unit}`; return (
{/* Fill */}
{/* WARN threshold line */} {warnPct < 100 && (
)} {/* CRIT threshold line */} {critPct < 100 && (
)}
{display}
); }; // Uptime %-Anzeige (für Netzwerkgeräte) const UptimePill = ({ pct: p }) => { if (p == null) return ; const color = p >= 99 ? SC.ok : p >= 90 ? SC.warn : SC.crit; return {p}%; }; const NetDot = ({ status, size = 10 }) => ( ); const UptimeSparkline = ({ checks }) => { if (!checks || checks.length === 0) return
Keine Daten
; const seg = checks.slice(-80); const w = 240, h = 28, sw = w / seg.length; return ( {seg.map((c, i) => )} ); }; const RttSparkline = ({ checks }) => { const vals = checks.filter(c => c.rtt_ms != null).map(c => c.rtt_ms); if (vals.length < 2) return
; const max = Math.max(...vals); const w = 240, h = 40; const pts = vals.slice(-60).map((v, i) => { const x = (i / (vals.slice(-60).length - 1)) * w; const y = h - (v / max) * (h - 4) - 2; return `${x},${y}`; }).join(' '); return ; }; const WarnBadge = ({ w }) => ( {w.msg} ); const TimeSeriesChart = ({ data, key1, key2, height = 110 }) => { const vbW = 400, vbH = height; if (!data || data.length < 2) return
Daten werden gesammelt...
; const v1 = data.map(d => d[key1] || 0), v2 = data.map(d => d[key2] || 0); const maxV = Math.max(...v1, ...v2, 1); const pad = { top: 8, right: 8, bottom: 24, left: 24 }; const cw = vbW - pad.left - pad.right, ch = vbH - pad.top - pad.bottom; const toX = (i) => pad.left + (i / (data.length - 1)) * cw; const toY = (v) => pad.top + ch - (v / maxV) * ch; const path = (vals) => vals.map((v, i) => `${i === 0 ? 'M' : 'L'}${toX(i)},${toY(v)}`).join(' '); const area = (vals) => { const l = vals.map((v, i) => `${i === 0 ? 'M' : 'L'}${toX(i)},${toY(v)}`).join(' '); const n = vals.length - 1; return `${l} L${toX(n)},${toY(0)} L${toX(0)},${toY(0)} Z`; }; const xl = data.length > 1 ? [0, Math.floor(data.length / 2), data.length - 1].map(i => ({ i, t: fmtTime(data[i].time) })) : []; return ( {[0, Math.ceil(maxV / 2), maxV].map(v => )} {[0, Math.ceil(maxV / 2), maxV].map(v => {v})} {xl.map(({ i, t }) => {t})} ); }; // ─── Checkmk-style Tactical Overview ───────────────────────────────────────── const TacticalOverview = ({ devices, agents, problems, acknowledged, countdown, lastUpdated, onTabChange, extAlerts = [] }) => { const openP = problems.filter(p => !acknowledged.has(p.id)); const critN = openP.filter(p => p.severity <= 1).length; const warnN = openP.filter(p => p.severity === 2).length; const hostUp = devices.filter(d => d.last_status === 'up').length; const hostDown = devices.filter(d => d.last_status === 'down').length; const hostUnk = devices.filter(d => !d.last_status || d.last_status === 'unknown').length; const agOnline = agents.filter(a => a.status === 'online').length; const agOffline = agents.filter(a => a.status === 'offline').length; const agWarn = agents.filter(a => warnings(a).some(w => w.type === 'warn') && a.status === 'online').length; const pxAlerts = extAlerts.filter(a => a.source === 'proxmox' && !a.acknowledged); const pxCrit = pxAlerts.filter(a => a.severity === 'CRIT').length; const pxWarn = pxAlerts.filter(a => a.severity === 'WARN').length; const pxOk = pxCrit === 0 && pxWarn === 0; const CountTile = ({ label, count, color, onClick, sub }) => { const active = count > 0; const W = 82, H = 72, cx = W / 2, cy = H / 2, r = 33; const pts = Array.from({ length: 6 }, (_, i) => { const a = (Math.PI / 3) * i; return `${(cx + r * Math.cos(a)).toFixed(1)},${(cy + r * Math.sin(a)).toFixed(1)}`; }).join(' '); return ( { if (active && onClick) e.currentTarget.style.filter = `drop-shadow(0 0 7px ${color}99)`; }} onMouseLeave={e => { e.currentTarget.style.filter = ''; }}> {count} {label} {sub && {sub}} ); }; const Divider = () =>
; return (
{/* Section: Zustand */}
Gesamtzustand
0 ? () => onTabChange('problems') : null} /> 0 ? () => onTabChange('problems') : null} />
{/* Section: Netzwerk */}
Netzwerk-Geräte
0 ? () => onTabChange('network') : null} />
{/* Section: Agents */}
Windows Agents
0 ? () => onTabChange('agents') : null} /> 0 ? () => onTabChange('agents') : null} />
{/* Section: Proxmox */}
Proxmox
0 ? () => onTabChange('overview') : null} /> 0 ? () => onTabChange('overview') : null} />
{/* Right: status + timer */}
Nächster Refresh: {countdown}s
{lastUpdated}
{devices.length + agents.length} Hosts gesamt
); }; // ─── Modals (unverändert) ───────────────────────────────────────────────────── const DiscoverModal = ({ onClose, onAdd }) => { const [subnet, setSubnet] = useState('192.168.0'); const [scanning, setScanning] = useState(false); const [progress, setProgress] = useState(0); const [results, setResults] = useState(null); const [selected, setSelected] = useState({}); const [typeOverrides, setTypeOverrides] = useState({}); const [adding, setAdding] = useState(false); const progressRef = useRef(null); const startScan = async () => { setScanning(true); setResults(null); setSelected({}); setProgress(0); let p = 0; progressRef.current = setInterval(() => { p = Math.min(p + 1.2, 90); setProgress(p); }, 500); try { const res = await networkMonitorService.discover(subnet); clearInterval(progressRef.current); setProgress(100); setResults(res.data || []); const sel = {}; (res.data || []).forEach(d => sel[d.ip] = true); setSelected(sel); } catch { clearInterval(progressRef.current); toast.error('Scan fehlgeschlagen'); } finally { setScanning(false); } }; const addSelected = async () => { const toAdd = (results || []).filter(d => selected[d.ip]); if (!toAdd.length) return; setAdding(true); let added = 0; for (const d of toAdd) { try { const type = typeOverrides[d.ip] || d.type; const device = await networkMonitorService.create({ name: d.hostname || d.ip, type, host: d.ip, check_type: d.check_type, interval_sec: 60, timeout_sec: 5, enabled: 1 }); onAdd(device); added++; } catch {} } toast.success(`${added} Gerät${added !== 1 ? 'e' : ''} hinzugefügt`); setAdding(false); onClose(); }; const toggleAll = (val) => { const sel = {}; (results || []).forEach(d => sel[d.ip] = val); setSelected(sel); }; const inp = { padding: '8px 12px', borderRadius: 7, border: '1px solid var(--border-color)', background: 'var(--bg-secondary)', color: 'var(--text-primary)', fontSize: 13 }; return (
e.target === e.currentTarget && !scanning && onClose()}>

📡 Netzwerk scannen

{!scanning && }
setSubnet(e.target.value)} disabled={scanning} />
{(scanning || results !== null) && (
{scanning ? `Scanne ${subnet}.1–${subnet}.254...` : `${results?.length || 0} Geräte gefunden`} {Math.round(progress)}%
)} {results !== null && results.length === 0 &&
Keine neuen Geräte im Subnetz {subnet}.x
} {results && results.length > 0 && ( <>
{Object.values(selected).filter(Boolean).length} / {results.length} ausgewählt
{results.map(d => (
setSelected(s => ({ ...s, [d.ip]: !s[d.ip] }))} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 12px', borderRadius: 8, background: selected[d.ip] ? `${SC.ok}10` : 'var(--bg-secondary)', border: `1px solid ${selected[d.ip] ? SC.ok + '50' : 'var(--border-color)'}`, cursor: 'pointer' }}> {}} style={{ accentColor: 'var(--accent)' }} /> {TYPE_META[typeOverrides[d.ip] || d.type]?.icon || '🖥'}
{d.ip}
{d.hostname &&
{d.hostname}
} {d.open_ports?.length > 0 &&
Ports: {d.open_ports.join(', ')}
}
))}
)}
{results && results.length > 0 && (
)}
); }; const DeviceFormModal = ({ device, onSave, onClose }) => { const [form, setForm] = useState({ name: '', type: 'host', host: '', check_type: 'icmp', port: '', http_path: '/', http_keyword: '', snmp_community: 'public', snmp_version: '2c', interval_sec: 60, timeout_sec: 5, enabled: 1, notify_email: '', location: '', ...(device || {}) }); const [saving, setSaving] = useState(false); const set = (k, v) => setForm(f => ({ ...f, [k]: v })); const save = async () => { if (!form.name || !form.host) return toast.error('Name und Host sind Pflichtfelder'); setSaving(true); try { const r = device ? await networkMonitorService.update(device.id, form) : await networkMonitorService.create(form); onSave(r); onClose(); toast.success(device ? 'Aktualisiert' : 'Hinzugefügt'); } catch { toast.error('Fehler beim Speichern'); } finally { setSaving(false); } }; const inp = { width: '100%', padding: '8px 10px', borderRadius: 6, border: '1px solid var(--border-color)', background: 'var(--bg-primary)', color: 'var(--text-primary)', fontSize: 13, boxSizing: 'border-box' }; const lbl = { fontSize: 11, color: 'var(--text-muted)', fontWeight: 700, marginBottom: 4, display: 'block', textTransform: 'uppercase', letterSpacing: '.4px' }; return (
e.target === e.currentTarget && onClose()}>

{device ? 'Gerät bearbeiten' : 'Neues Gerät'}

set('name', e.target.value)} />
set('host', e.target.value)} />
set('location', e.target.value)} />
{['tcp','http','https'].includes(form.check_type) &&
set('port', e.target.value)} />
}
{['http','https'].includes(form.check_type) && (
set('http_path', e.target.value)} />
set('http_keyword', e.target.value)} />
)} {form.check_type === 'snmp' && (
set('snmp_community', e.target.value)} placeholder="public" />
)}
set('timeout_sec', parseInt(e.target.value))} />
set('notify_email', e.target.value)} />
set('enabled', e.target.checked ? 1 : 0)} />
); }; const NetworkDetailModal = ({ device, onClose, onEdit, onDelete, onCheckNow }) => { const [checksData, setChecksData] = useState(null); const [timeRange, setTimeRange] = useState(24); const [checking, setChecking] = useState(false); useEffect(() => { networkMonitorService.getChecks(device.id, timeRange).then(setChecksData).catch(() => {}); }, [device.id, timeRange]); const doCheckNow = async () => { setChecking(true); try { await onCheckNow(device.id); toast.success('Check ausgeführt'); const d = await networkMonitorService.getChecks(device.id, timeRange); setChecksData(d); } catch { toast.error('Fehlgeschlagen'); } finally { setChecking(false); } }; const meta = TYPE_META[device.type] || TYPE_META.host; const uptime = checksData?.uptime, checks = checksData?.checks || []; return (
e.target === e.currentTarget && onClose()}>
{meta.icon}
{device.name}
{device.host} · {meta.label}
{[{ label: 'Letzter Check', value: timeAgo(device.last_checked) }, { label: 'Antwortzeit', value: device.last_rtt_ms != null ? `${Math.round(device.last_rtt_ms)} ms` : '–' }, { label: `Uptime ${timeRange}h`, value: uptime?.total > 0 ? `${uptime.pct}%` : '–', color: uptime?.pct >= 99 ? SC.ok : uptime?.pct >= 90 ? SC.warn : SC.crit }].map(m => (
{m.label}
{m.value}
))}
Uptime-Verlauf
{[24, 48, 168].map(h => )}
Grün = Online · Rot = Offline{uptime?.up ?? 0}/{uptime?.total ?? 0} OK
{checks.some(c => c.rtt_ms != null) && (
Antwortzeit (ms)
)}
Konfiguration
{[['Check-Typ', device.check_type?.toUpperCase()], ['Intervall', `${device.interval_sec}s`], ['Timeout', `${device.timeout_sec}s`], device.port && ['Port', device.port], device.location && ['Standort', device.location], device.notify_email && ['Alert E-Mail', device.notify_email]].filter(Boolean).map(([k, v]) => ( {k}{v} ))}
{[...checks].reverse().slice(0, 15).length > 0 && (
Letzte Checks
{[...checks].reverse().slice(0, 15).map((c, i) => ( {c.error_msg && } ))}
{c.rtt_ms != null ? `${Math.round(c.rtt_ms)} ms` : '–'} {new Date(c.checked_at).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}{c.error_msg}
)}
); }; const AgentDetailModal = ({ agent, onClose, onDelete }) => { const [swSearch, setSwSearch] = useState(''); const w = warnings(agent); const sw = (agent.installed_software || []).filter(s => !swSearch || s.toLowerCase().includes(swSearch.toLowerCase())); return (
e.stopPropagation()}>
{agent.hostname}
{agent.os_name} {agent.os_version}
{w.length > 0 &&
{w.map((x, i) => )}
}
{[{ label: 'CPU', val: `${agent.cpu_usage_percent?.toFixed(0) ?? '–'}%`, sub: agent.cpu_model?.replace(/\(R\)|\(TM\)/g, '') || '–' }, { label: 'RAM', val: `${agent.ram_used_gb?.toFixed(1) ?? '–'} / ${agent.ram_total_gb?.toFixed(0) ?? '–'} GB`, sub: `${pct(agent.ram_used_gb, agent.ram_total_gb)}% belegt` }, { label: 'Disk C:', val: `${agent.disk_free_gb?.toFixed(0) ?? '–'} GB frei`, sub: `von ${agent.disk_total_gb?.toFixed(0) ?? '–'} GB` }].map(m => (
{m.label}
{m.val}
{m.sub}
))}
Systeminfos
{[['IP-Adresse', agent.ip_address || '–'], ['MAC-Adresse', agent.mac_address || '–'], ['Domain', agent.domain || '–'], ['Letzter Benutzer', agent.last_user || '–'], ['Uptime', uptimeStr(agent.uptime_hours)], ['CPU-Kerne', agent.cpu_cores || '–'], ['Windows Updates', `${agent.windows_updates_pending ?? 0} ausstehend`], ['Agent Version', `v${agent.agent_version || '?'}`], ['Letzter Checkin', timeAgo(agent.last_checkin)], ['Registriert', new Date(agent.created_at).toLocaleDateString('de-DE')]].map(([k, v]) => (
{k}{v}
))}
Sicherheit
{[ ['BitLocker', (() => { const s = agent.bitlocker_status; if (s === 'encrypted') return { label: '✓ Verschlüsselt', color: '#22c55e' }; if (s === 'off') return { label: '✗ Nicht aktiv', color: '#ef4444' }; return { label: '? Unbekannt', color: 'var(--text-muted)' }; })()], ['Microsoft Defender', (() => { if (agent.defender_enabled == null) return { label: '? Unbekannt', color: 'var(--text-muted)' }; if (!agent.defender_enabled) return { label: '✗ Inaktiv', color: '#ef4444' }; const age = agent.defender_signatures_age; if (age == null || age < 0) return { label: '✓ Aktiv', color: '#22c55e' }; if (age <= 3) return { label: '✓ Aktiv · Signaturen aktuell', color: '#22c55e' }; return { label: `⚠ Aktiv · Signaturen ${age}d alt`, color: '#f59e0b' }; })()], ['Seriennummer', { label: agent.hardware_serial || '–', color: agent.hardware_serial ? 'var(--text-primary)' : 'var(--text-muted)' }], ['TPM', { label: agent.tpm_v2 ? '✓ TPM 2.0' : agent.tpm_present ? `TPM ${agent.tpm_version || '?'}` : '✗ Nicht vorhanden', color: agent.tpm_v2 ? '#22c55e' : agent.tpm_present ? '#f59e0b' : '#ef4444' }], ['Secure Boot', { label: agent.secure_boot ? '✓ Aktiv' : '✗ Inaktiv', color: agent.secure_boot ? '#22c55e' : '#ef4444' }], ['Win 11 Ready', { label: agent.win11_ready ? '✓ Ja' : '✗ Nein', color: agent.win11_ready ? '#22c55e' : 'var(--text-muted)' }], ].map(([k, v]) => (
{k} {v.label}
))}
{agent.installed_software?.length > 0 && (
Software ({agent.installed_software.length})
setSwSearch(e.target.value)} style={{ padding: '4px 10px', borderRadius: 6, border: '1px solid var(--border-color)', background: 'var(--bg-secondary)', color: 'var(--text-primary)', fontSize: 12, width: 160 }} />
{sw.map((s, i) =>
{s}
)}
)}
); }; // ─── Unifi Config Modal (top-level so React doesn't remount on every render) ── const UnifiConfigModal = ({ unifiConfig, onSave, onClose }) => { const [form, setForm] = useState({ controller_url: '', username: '', password: '', site: 'default', poll_interval_min: 5, ...(unifiConfig || {}), enabled: !!unifiConfig?.enabled, }); const [saving, setSaving] = useState(false); const setF = (k, v) => setForm(f => ({ ...f, [k]: v })); const inp = { width: '100%', padding: '8px 10px', borderRadius: 6, border: '1px solid var(--border-color)', background: 'var(--bg-primary)', color: 'var(--text-primary)', fontSize: 13, boxSizing: 'border-box' }; const lbl = { fontSize: 11, color: 'var(--text-muted)', fontWeight: 700, marginBottom: 4, display: 'block', textTransform: 'uppercase', letterSpacing: '.4px' }; const save = async () => { setSaving(true); try { const c = await unifiService.saveConfig(form); onSave(c); onClose(); } catch (e) { alert(e.message || 'Fehler'); } finally { setSaving(false); } }; return (
e.target === e.currentTarget && onClose()}>

📶 Unifi Controller

setF('controller_url', e.target.value)} placeholder="https://192.168.0.1:8443" />
setF('username', e.target.value)} />
setF('password', e.target.value)} placeholder="leer = unverändert" />
setF('site', e.target.value)} placeholder="default" />
setF('poll_interval_min', parseInt(e.target.value))} />
setF('enabled', e.target.checked)} />
); }; // ─── Main Page ──────────────────────────────────────────────────────────────── const MonitoringPage = () => { const [devices, setDevices] = useState([]); const [agents, setAgents] = useState([]); const [loading, setLoading] = useState(true); const [timeSeriesData, setTimeSeriesData] = useState([]); const [recentChecks, setRecentChecks] = useState([]); const [selectedDevice, setSelectedDevice] = useState(null); const [selectedAgent, setSelectedAgent] = useState(null); const [showDeviceForm, setShowDeviceForm] = useState(false); const [editDevice, setEditDevice] = useState(null); const [showDiscover, setShowDiscover] = useState(false); const [showDeploy, setShowDeploy] = useState(false); const [countdown, setCountdown] = useState(REFRESH_INTERVAL); const [activeTab, setActiveTab] = useState('overview'); const [netSearch, setNetSearch] = useState(''); const [netFilter, setNetFilter] = useState('all'); const [agentSearch, setAgentSearch] = useState(''); const [agentFilter, setAgentFilter] = useState('all'); const [expandedAgents, setExpandedAgents] = useState(new Set()); const [expandedHosts, setExpandedHosts] = useState(new Set()); const [agentSort, setAgentSort] = useState('cpu'); const [hostFilter, setHostFilter] = useState('all'); const [hostSearch, setHostSearch] = useState(''); const [proxmoxData, setProxmoxData] = useState(null); const [acknowledged, setAcknowledged] = useState(new Set()); const [showAcked, setShowAcked] = useState(false); const [uptimeStats, setUptimeStats] = useState({}); const [collapsedGroups,setCollapsedGroups]= useState(new Set()); const [unifiDevices, setUnifiDevices] = useState([]); const [unifiConfig, setUnifiConfig] = useState(null); const [showUnifiCfg, setShowUnifiCfg] = useState(false); const [extAlerts, setExtAlerts] = useState([]); const [expandedAlerts, setExpandedAlerts] = useState({}); const [hideAcknowledged, setHideAcknowledged] = useState(true); const esRef = useRef(null), countRef = useRef(REFRESH_INTERVAL), agentsRef = useRef([]), devicesRef = useRef([]); agentsRef.current = agents; devicesRef.current = devices; const pushSnapshot = useCallback((devs, agts) => { setTimeSeriesData(prev => [...prev.slice(-(MAX_TS_POINTS - 1)), { time: new Date(), devDown: devs.filter(d => d.last_status === 'down').length, devWarn: 0, agentOffline: agts.filter(a => a.status === 'offline').length, agentWarn: agts.filter(a => warnings(a).some(w => w.type === 'warn')).length }]); }, []); const loadRecentChecks = useCallback(async (devs) => { if (!devs?.length) return; const results = []; await Promise.allSettled(devs.slice(0, 8).map(async d => { try { const data = await networkMonitorService.getChecks(d.id, 1); (data?.checks || []).slice(-3).forEach(c => results.push({ deviceName: d.name, status: c.status, checked_at: c.checked_at, rtt_ms: c.rtt_ms })); } catch {} })); results.sort((a, b) => new Date(b.checked_at) - new Date(a.checked_at)); setRecentChecks(results.slice(0, 12)); }, []); useEffect(() => { let pollTimer = null, sseConnected = false; const loadViaApi = () => networkMonitorService.getAll().then(d => { setDevices(d); setLoading(false); pushSnapshot(d, agentsRef.current); }).catch(() => setLoading(false)); const startPolling = () => { if (pollTimer) return; loadViaApi(); pollTimer = setInterval(loadViaApi, 15000); }; let es; try { es = networkMonitorService.createSSE(); esRef.current = es; const to = setTimeout(() => { if (!sseConnected) { es.close(); startPolling(); } }, 4000); es.addEventListener('initial_state', e => { sseConnected = true; clearTimeout(to); const d = JSON.parse(e.data); setDevices(d); setLoading(false); pushSnapshot(d, agentsRef.current); }); es.addEventListener('device_update', e => { const u = JSON.parse(e.data); setDevices(prev => { const ex = prev.find(d => d.id === u.id); const n = ex ? prev.map(d => d.id === u.id ? u : d) : [...prev, u]; pushSnapshot(n, agentsRef.current); return n; }); setSelectedDevice(prev => prev?.id === u.id ? u : prev); }); es.addEventListener('device_deleted', e => { const { id } = JSON.parse(e.data); setDevices(prev => prev.filter(d => d.id !== id)); }); es.onerror = () => { clearTimeout(to); if (!sseConnected) { es.close(); startPolling(); } }; } catch { startPolling(); } return () => { es?.close(); if (pollTimer) clearInterval(pollTimer); }; }, [pushSnapshot]); const loadAgents = useCallback(async (silent = false) => { try { const d = await monitoringService.getAll(); setAgents(d); countRef.current = REFRESH_INTERVAL; setCountdown(REFRESH_INTERVAL); pushSnapshot(devicesRef.current, d); if (!silent) setLoading(false); } catch { if (!silent) { toast.error('Fehler beim Laden'); setLoading(false); } } }, [pushSnapshot]); useEffect(() => { loadAgents(); const r = setInterval(() => loadAgents(true), REFRESH_INTERVAL * 1000); const t = setInterval(() => { countRef.current = Math.max(0, countRef.current - 1); setCountdown(countRef.current); }, 1000); return () => { clearInterval(r); clearInterval(t); }; }, [loadAgents]); useEffect(() => { if (devices.length > 0) loadRecentChecks(devices); }, [devices, loadRecentChecks]); useEffect(() => { if (devices.length > 0) networkMonitorService.getUptimeStats().then(setUptimeStats).catch(() => {}); }, [devices.length]); const loadUnifi = useCallback(async () => { try { const [devs, cfg] = await Promise.all([unifiService.getDevices(), unifiService.getConfig()]); setUnifiDevices(devs); setUnifiConfig(cfg); } catch {} }, []); useEffect(() => { loadUnifi(); }, [loadUnifi]); const loadExtAlerts = useCallback(async () => { try { const d = await externalAlertService.getAll(); setExtAlerts(d); } catch {} }, []); useEffect(() => { loadExtAlerts(); const r = setInterval(loadExtAlerts, 60000); return () => clearInterval(r); }, [loadExtAlerts]); const loadProxmox = useCallback(async () => { try { const { default: api } = await import('../services/api'); const r = await api.get('/proxmox/overview'); setProxmoxData(r.data); } catch {} }, []); useEffect(() => { loadProxmox(); const r = setInterval(loadProxmox, 30000); return () => clearInterval(r); }, [loadProxmox]); const handleExtAcknowledge = async (id) => { try { await externalAlertService.acknowledge(id); loadExtAlerts(); toast.success('Quittiert'); } catch { toast.error('Fehler'); } }; const handleExtCreateTicket = async (id) => { try { await externalAlertService.createTicket(id); loadExtAlerts(); toast.success('Ticket erstellt'); } catch { toast.error('Fehler beim Erstellen'); } }; const handleExtRemove = async (id) => { if (!window.confirm('Alert wirklich löschen?')) return; try { await externalAlertService.remove(id); loadExtAlerts(); toast.success('Gelöscht'); } catch { toast.error('Fehler'); } }; const handleAgentDelete = async (id, hostname) => { if (!window.confirm(`Agent "${hostname}" wirklich entfernen?`)) return; try { await monitoringService.delete(id); toast.success('Entfernt'); setSelectedAgent(null); loadAgents(true); } catch { toast.error('Fehler'); } }; const handleNetDelete = async (d) => { if (!window.confirm(`${d.name} wirklich löschen?`)) return; try { await networkMonitorService.delete(d.id); setSelectedDevice(null); toast.success('Gelöscht'); } catch { toast.error('Fehler'); } }; const handleNetSave = (d) => setDevices(prev => { const ex = prev.find(x => x.id === d.id); return ex ? prev.map(x => x.id === d.id ? d : x) : [...prev, d]; }); const toggleAgent = id => setExpandedAgents(prev => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; }); const toggleGroup = g => setCollapsedGroups(prev => { const n = new Set(prev); n.has(g) ? n.delete(g) : n.add(g); return n; }); // Derived const problems = [ ...devices.filter(d => d.last_status !== 'up').map(d => ({ id: `dev-${d.id}`, severity: d.last_status === 'down' ? 0 : 2, statusColor: netColor(d.last_status), statusLabel: netLabel(d.last_status), name: d.name, service: TYPE_META[d.type]?.label || d.type, typeIcon: TYPE_META[d.type]?.icon || '🖥', lastCheck: d.last_checked, isDevice: true, raw: d })), ...agents.filter(a => a.status !== 'online' || warnings(a).length > 0).map(a => { const w = warnings(a); const hasErr = w.some(x => x.type === 'error'); const isOffline = a.status === 'offline'; return { id: `agt-${a.id}`, severity: isOffline ? 3 : hasErr ? 1 : 2, statusColor: isOffline ? SC.unknown : hasErr ? SC.crit : SC.warn, statusLabel: isOffline ? 'OFFLINE' : hasErr ? 'CRIT' : 'WARN', name: a.hostname, service: 'Windows Agent', typeIcon: '🖥', lastCheck: a.last_checkin, isDevice: false, raw: a }; }), ].sort((a, b) => a.severity - b.severity); const INACTIVE_DAYS = 7; const isLongInactive = (p) => { if (!p.lastCheck) return false; const days = (Date.now() - new Date(p.lastCheck).getTime()) / 86400000; return days >= INACTIVE_DAYS; }; const openProblems = problems.filter(p => !acknowledged.has(p.id)); const activeProblems = openProblems.filter(p => !isLongInactive(p)); const inactiveProblems = openProblems.filter(p => isLongInactive(p)); const lastUpdated = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); if (loading) return ; // ── Row background helper (Checkmk-style) ── const rowBg = (state) => state === 'crit' || state === 'down' ? `${SC.crit}0a` : state === 'warn' ? `${SC.warn}08` : 'transparent'; const rowBorder = (state) => state === 'crit' || state === 'down' ? SC.crit : state === 'warn' ? SC.warn : 'transparent'; // ── View toolbar helper ── const pill = (active, onClick, label) => ( ); const card = (ex = {}) => ({ background: 'var(--bg-secondary)', borderRadius: 10, border: '1px solid var(--border-color)', ...ex }); // ─── Tab: Übersicht ─────────────────────────────────────────────────────── const renderOverview = () => { const toggleHost = (id) => setExpandedHosts(prev => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; }); const pxState = proxmoxData?.node ? (() => { const cpu = (proxmoxData.node.cpu || 0) * 100; const ram = proxmoxData.node.memory?.total > 0 ? (proxmoxData.node.memory.used / proxmoxData.node.memory.total) * 100 : 0; const load = proxmoxData.node.loadavg?.[0] || 0; const cpus = proxmoxData.node.cpuinfo?.cpus || 1; if (cpu >= 90 || ram >= 90 || load > cpus * 0.9) return 'crit'; if (cpu >= 70 || ram >= 80 || load > cpus * 0.7) return 'warn'; return 'ok'; })() : 'unknown'; const allHosts = [ ...(proxmoxData ? [{ id: 'px-hve01', type: 'proxmox', name: proxmoxData.node_name || 'hve-01', ip: '192.168.0.184', icon: '🖥️', subLabel: 'Proxmox VE Node', state: pxState, services: proxmoxServices(proxmoxData), lastCheck: proxmoxData.fetched_at }] : []), ...devices.map(d => { const ut = uptimeStats[d.id]; const state = d.last_status === 'up' ? 'ok' : d.last_status === 'down' ? 'crit' : 'unknown'; return { id: `net-${d.id}`, type: 'network', name: d.name, ip: d.host, icon: TYPE_META[d.type]?.icon || '🌐', subLabel: TYPE_META[d.type]?.label || 'Netzwerkgerät', state, services: netDeviceServices(d, ut), lastCheck: d.last_checked, raw: d }; }), ...agents.map(a => { const w = warnings(a); const hasErr = w.some(x => x.type === 'error'); const state = a.status === 'offline' ? 'crit' : hasErr ? 'crit' : w.length ? 'warn' : 'ok'; return { id: `ag-${a.id}`, type: 'agent', name: a.hostname, ip: a.ip_address, icon: '💻', subLabel: 'Windows Agent', state, services: agentServices(a), lastCheck: a.last_checkin, raw: a }; }), ].sort((a, b) => { const o = { crit: 0, warn: 1, unknown: 2, ok: 3 }; return (o[a.state] ?? 3) - (o[b.state] ?? 3) || a.name.localeCompare(b.name); }); const filtered = allHosts .filter(h => hostFilter === 'all' || h.state !== 'ok') .filter(h => !hostSearch || h.name.toLowerCase().includes(hostSearch.toLowerCase()) || (h.ip || '').includes(hostSearch)); const stateLabel = { ok: 'OK', warn: 'WARN', crit: 'CRIT', unknown: '?' }; const stateColor = { ok: SC.ok, warn: SC.warn, crit: SC.crit, unknown: SC.unknown }; return ( <> {/* Filter + Search */}
{[['all', `Alle (${allHosts.length})`], ['problems', `Probleme (${allHosts.filter(h => h.state !== 'ok').length})`]].map(([v, l]) => ( ))} setHostSearch(e.target.value)} placeholder="🔍 Host suchen..." style={{ marginLeft: 'auto', padding: '5px 12px', borderRadius: 8, border: '1px solid var(--border-color)', background: 'var(--bg-secondary)', color: 'var(--text-primary)', fontSize: 13, minWidth: 200 }} />
{/* CheckMK Host Table */}
{/* Header */}
Host / IP
Status
OK
WARN
CRIT
Letzter Check
{filtered.map(host => { const expanded = expandedHosts.has(host.id); const okC = host.services.filter(s => s.status === 'ok').length; const warnC = host.services.filter(s => s.status === 'warn').length; const critC = host.services.filter(s => s.status === 'crit').length; const hColor = stateColor[host.state] || SC.unknown; return (
{/* Host row */}
toggleHost(host.id)} style={{ display: 'grid', gridTemplateColumns: '32px 1fr 90px 60px 60px 60px 130px', padding: '10px 14px', borderBottom: '1px solid var(--border-color)', cursor: 'pointer', background: host.state === 'crit' ? `${SC.crit}12` : host.state === 'warn' ? `${SC.warn}09` : 'transparent', borderLeft: `3px solid ${hColor}`, transition: 'background .1s' }} onMouseEnter={e => e.currentTarget.style.filter = 'brightness(1.08)'} onMouseLeave={e => e.currentTarget.style.filter = ''}>
{expanded ? '▼' : '▶'}
{host.icon} {host.name} {host.ip && {host.ip}}
{host.subLabel}
{stateLabel[host.state] || '?'}
{okC}
0 ? 700 : 400, fontSize: 13, color: warnC > 0 ? SC.warn : 'var(--text-muted)' }}>{warnC}
0 ? 700 : 400, fontSize: 13, color: critC > 0 ? SC.crit : 'var(--text-muted)' }}>{critC}
{timeAgo(host.lastCheck)}
{/* Service rows */} {expanded && host.services.map((svc, i) => { const sc = stateColor[svc.status] || SC.unknown; return (
{svc.name}
{(svc.status || 'ok').toUpperCase()}
{svc.value} {svc.detail && {svc.detail}}
{svc.perf && }
); })}
); })} {filtered.length === 0 && (
✅ {hostFilter === 'problems' ? 'Alle Services in Ordnung — keine Probleme' : 'Keine Hosts gefunden'}
)}
); }; // ─── Tab: Netzwerk ──────────────────────────────────────────────────────── const renderNetwork = () => { const filtered = devices.filter(d => { if (netSearch && !d.name.toLowerCase().includes(netSearch.toLowerCase()) && !d.host.includes(netSearch)) return false; if (netFilter === 'up' && d.last_status !== 'up') return false; if (netFilter === 'down' && d.last_status !== 'down') return false; if (netFilter === 'unknown' && d.last_status === 'up') return false; return true; }); const grouped = {}; filtered.forEach(d => { const g = d.type || 'host'; if (!grouped[g]) grouped[g] = []; grouped[g].push(d); }); return ( <> {/* Toolbar */}
setNetSearch(e.target.value)} placeholder="Suchen..." style={{ padding: '7px 12px', borderRadius: 7, border: '1px solid var(--border-color)', background: 'var(--bg-secondary)', color: 'var(--text-primary)', fontSize: 13, width: 200 }} />
{pill(netFilter === 'all', () => setNetFilter('all'), 'Alle')} {pill(netFilter === 'up', () => setNetFilter('up'), 'UP')} {pill(netFilter === 'down', () => setNetFilter('down'), 'DOWN')} {pill(netFilter === 'unknown', () => setNetFilter('unknown'), 'UNBEKANNT')}
{filtered.length} Einträge
{/* Deploy panel */} {showDeploy && (
Agent bereitstellen
{[{ href: '/downloads/IT-Nexus-Agent-Setup-v1.0.0.exe', icon: '⬇', title: 'Installer (.exe)', sub: 'Manuelle Installation' }, { href: '/downloads/IT-Nexus-Agent-Setup-v1.0.0.intunewin', icon: '📦', title: 'Intune (.intunewin)', sub: 'Microsoft Intune' }].map(({ href, icon, title, sub }) => (
e.currentTarget.style.borderColor = 'var(--accent)'} onMouseLeave={e => e.currentTarget.style.borderColor = 'var(--border-color)'}> {icon}
{title}
{sub}
))}
{[['Install', 'IT-Nexus-Agent-Setup-v1.0.0.exe /VERYSILENT /NORESTART'], ['Uninstall', '{app}\\unins000.exe /VERYSILENT'], ['Detect', 'C:\\ProgramData\\IT Nexus Agent\\it-nexus-agent.ps1']].map(([k, v]) => ( {k}{v} ))}
)} {/* Grouped device tables – Checkmk-style */} {Object.keys(grouped).length === 0 ?
Keine Geräte gefunden
: Object.entries(grouped).map(([type, devList]) => { const meta = TYPE_META[type] || TYPE_META.host; const collapsed = collapsedGroups.has(type); const downCount = devList.filter(d => d.last_status === 'down').length; return (
{/* Group header */}
toggleGroup(type)} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 16px', cursor: 'pointer', borderBottom: collapsed ? 'none' : '1px solid var(--border-color)', background: 'var(--bg-primary)', userSelect: 'none' }}> {meta.icon} {meta.label} {devList.length} {downCount > 0 && } {collapsed ? '▸' : '▾'}
{!collapsed && ( {['Status', 'Host', 'IP-Adresse', 'Standort', '24h Uptime', 'RTT', 'Letzter Check', 'Aktionen'].map(h => ( ))} {devList.map(d => { const state = d.last_status === 'up' ? 'ok' : d.last_status === 'down' ? 'crit' : 'unknown'; const ut = uptimeStats[d.id]; return ( setSelectedDevice(d)} style={{ borderBottom: '1px solid var(--border-color)', background: rowBg(state), borderLeft: `3px solid ${rowBorder(state)}`, cursor: 'pointer', transition: 'filter .1s' }} onMouseEnter={e => e.currentTarget.style.filter = 'brightness(1.1)'} onMouseLeave={e => e.currentTarget.style.filter = ''}> ); })}
{h}
{d.name} {d.host} {d.location || '–'} {ut?.total > 0 ? : } {d.last_rtt_ms != null ? `${Math.round(d.last_rtt_ms)} ms` : '–'} {timeAgo(d.last_checked)} e.stopPropagation()}>
)}
); }) } {/* ── Unifi-Geräte ─────────────────────────────────────────── */} {unifiConfig?.enabled && unifiDevices.length > 0 && (() => { const upStr = (s) => { if (!s) return '–'; const h = Math.floor(s / 3600); return h < 24 ? `${h}h` : `${Math.floor(h / 24)}d`; }; const groups = [ { key: 'uap', label: 'Unifi Access Points', icon: '📶', items: unifiDevices.filter(d => d.type === 'uap') }, { key: 'usw', label: 'Unifi Switches', icon: '🔀', items: unifiDevices.filter(d => d.type === 'usw') }, { key: 'other', label: 'Unifi Sonstige', icon: '🔌', items: unifiDevices.filter(d => d.type !== 'uap' && d.type !== 'usw') }, ].filter(g => g.items.length > 0); return groups.map(({ key, label, icon, items }) => { const collapsed = collapsedGroups.has('unifi_' + key); const downCount = items.filter(d => d.state !== 1).length; const hasClients = items.some(d => d.num_sta > 0); return (
toggleGroup('unifi_' + key)} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 16px', cursor: 'pointer', borderBottom: collapsed ? 'none' : '1px solid var(--border-color)', background: 'var(--bg-primary)', userSelect: 'none' }}> {icon} {label} {items.length} {downCount > 0 && } via Unifi Controller {collapsed ? '▸' : '▾'}
{!collapsed && ( {['Status','Name','IP','Modell','Uptime', hasClients && 'Clients','CPU','RAM'].filter(Boolean).map(h => ( ))} {items.map(d => ( {hasClients && } ))}
{h}
{d.name || d.mac} {d.ip || '–'} {d.model || '–'} {upStr(d.uptime)} {d.num_sta > 0 ? {d.num_sta} : } {d.cpu_pct != null ? : } {d.ram_pct != null ? : }
)}
); }); })()} ); }; // ─── Tab: Agents ────────────────────────────────────────────────────────── const renderAgents = () => { const isAgentOffline = (a) => { if (!a.last_checkin) return true; return Date.now() - new Date(a.last_checkin).getTime() > 5 * 60 * 1000; }; const filtered = agents.filter(a => { if (agentSearch && !a.hostname.toLowerCase().includes(agentSearch.toLowerCase()) && !(a.ip_address || '').includes(agentSearch)) return false; if (agentFilter === 'online' && a.status !== 'online') return false; if (agentFilter === 'offline' && a.status !== 'offline') return false; if (agentFilter === 'warn' && warnings(a).length === 0) return false; return true; }); const sorted = [...filtered].sort((a, b) => { const aOffline = isAgentOffline(a); const bOffline = isAgentOffline(b); // Offline immer ans Ende if (aOffline !== bOffline) return aOffline ? 1 : -1; if (agentSort === 'cpu') { const av = a.cpu_usage_percent ?? (a.ram_total_gb > 0 ? (a.ram_used_gb / a.ram_total_gb) * 100 : 0); const bv = b.cpu_usage_percent ?? (b.ram_total_gb > 0 ? (b.ram_used_gb / b.ram_total_gb) * 100 : 0); return bv - av; } if (agentSort === 'ram') { const av = a.ram_total_gb > 0 ? (a.ram_used_gb / a.ram_total_gb) * 100 : 0; const bv = b.ram_total_gb > 0 ? (b.ram_used_gb / b.ram_total_gb) * 100 : 0; return bv - av; } if (agentSort === 'name') return (a.hostname || '').localeCompare(b.hostname || ''); if (agentSort === 'seen') return new Date(b.last_checkin || 0) - new Date(a.last_checkin || 0); return 0; }); return ( <>
setAgentSearch(e.target.value)} placeholder="Hostname / IP..." style={{ padding: '7px 12px', borderRadius: 7, border: '1px solid var(--border-color)', background: 'var(--bg-secondary)', color: 'var(--text-primary)', fontSize: 13, width: 220 }} />
{pill(agentFilter === 'all', () => setAgentFilter('all'), 'Alle')} {pill(agentFilter === 'online', () => setAgentFilter('online'), 'ONLINE')} {pill(agentFilter === 'offline', () => setAgentFilter('offline'), 'OFFLINE')} {pill(agentFilter === 'warn', () => setAgentFilter('warn'), 'WARNUNG')}
{sorted.length} Einträge
{sorted.length === 0 ?
Keine Agents gefunden
: ( /* Checkmk-style service table */
))} {sorted.map(a => { const w = warnings(a), hasErr = w.some(x => x.type === 'error'); const state = a.status === 'offline' ? 'crit' : hasErr ? 'crit' : w.length ? 'warn' : 'ok'; const isExp = expandedAgents.has(a.id); const services = agentServices(a); return ( {/* Agent row */} toggleAgent(a.id)}> {/* Expanded: service checks (Checkmk-style sub-table) */} {isExp && ( )} ); })}
{['Host', 'Status', 'CPU', 'RAM', 'Disk', 'Uptime', 'Letzter Checkin', ''].map(h => ( {h}
{isExp ? '▼' : '▶'} e.stopPropagation()} style={{ color: 'inherit', textDecoration: 'none', borderBottom: '1px dotted var(--text-muted)' }} title="Geräte-Details öffnen"> {a.hostname} {a.cpu_usage_percent != null ? : } {a.ram_total_gb > 0 ? : } {a.disk_total_gb > 0 ? : } {uptimeStr(a.uptime_hours)} {timeAgo(a.last_checkin)} e.stopPropagation()}>
{['Status', 'Service', 'Perf-O-Meter', 'Wert', 'Details'].map(h => ( ))} {services.map((svc, si) => ( ))}
{h}
{svc.name} {svc.perf ? : } {svc.value} {svc.detail}
) } ); }; // ─── Tab: Probleme ──────────────────────────────────────────────────────── const renderProblems = () => { const visible = showAcked ? problems : activeProblems; const renderTable = (rows) => (
{['Schwere', 'Status', 'Host', 'Service', 'Seit', 'Bestätigen'].map(h => ( ))} {rows.map(p => { const isAck = acknowledged.has(p.id); const state = p.severity <= 1 ? 'crit' : 'warn'; return ( ); })}
{h}
{isAck && ACK}
{p.typeIcon}
{p.name}
{p.raw?.host &&
{p.raw.host}
}
{p.service} {duration(p.lastCheck)} {!isAck ? : }
); return ( <>
{activeProblems.length} aktiv · {inactiveProblems.length} langfristig offline · {acknowledged.size} bestätigt
{visible.length === 0 && inactiveProblems.length === 0 ? (
Keine offenen Probleme
Alle {devices.length + agents.length} Hosts sind in Ordnung
) : ( <> {visible.length > 0 && renderTable(visible)} {inactiveProblems.length > 0 && (
{!collapsedGroups.has('inactive') && (
{renderTable(inactiveProblems)}
)}
)} ) } ); }; const extUnacked = extAlerts.filter(a => !a.acknowledged).length; const netgoAlerts = extAlerts.filter(a => a.source === 'netgo'); const netgoUnacked = netgoAlerts.filter(a => !a.acknowledged).length; const awAlerts = extAlerts.filter(a => a.source === 'arcticwolf'); const awUnacked = awAlerts.filter(a => !a.acknowledged).length; const mdoAlerts = extAlerts.filter(a => a.source === 'mdo'); const mdoUnacked = mdoAlerts.filter(a => !a.acknowledged).length; const toggleAlertExpand = (id) => setExpandedAlerts(p => ({ ...p, [id]: !p[id] })); const renderExtAlerts = () => { const sevColor = (s) => s === 'CRIT' ? SC.crit : s === 'WARN' ? SC.warn : s === 'OK' ? SC.ok : SC.unknown; const renderAlertCard = (alert) => { const color = sevColor(alert.severity); const isAW = alert.source === 'arcticwolf'; const expanded = expandedAlerts[alert.id]; let aw = null; if (isAW && alert.raw_body) { try { aw = JSON.parse(alert.raw_body); } catch { /* ignore */ } } const actions = (
{!alert.ticket_id && ( )} {!alert.acknowledged && ( )}
); // ── Arctic Wolf Detailkarte ────────────────────────────────────── if (isAW) { const labelStyle = { fontSize: 11, color: 'var(--text-muted)', minWidth: 130, flexShrink: 0 }; const valStyle = { fontSize: 12, color: 'var(--text-primary)', wordBreak: 'break-word' }; const row = (label, val) => val ? (
{label} {val}
) : null; const section = (title, text) => text ? (
{title}
{text}
) : null; return (
{/* Header */}
{aw?.severity_label || alert.severity} ARCTIC WOLF {alert.acknowledged && ✓ Quittiert} {alert.ticket_id && 🎫 Ticket #{alert.ticket_id}}
{alert.message || aw?.incident_name}
🖥 {alert.device}{aw?.ip ? ` · ${aw.ip}` : ''} {alert.state_time && 🕐 {alert.state_time}} Eingang: {timeAgo(alert.created_at)}
{actions}
{/* Schnell-Info-Grid */} {aw && (
{row('Event-Typ', aw.event_type)} {row('Application', aw.application)} {row('User', aw.user)} {row('Process', aw.process)}
)} {/* Aufklapp-Button */} {aw && (aw.what_is_it || aw.why_matters || aw.next_steps || aw.recommend) && ( )} {/* Erweiterte Sektionen */} {expanded && aw && (
{section('Was ist passiert?', aw.what_is_it)} {section('Warum ist das relevant?', aw.why_matters)} {section('Wie wurde es erkannt?', aw.how_detected)} {section('Nächste Schritte', aw.next_steps)} {section('Empfehlungen Arctic Wolf', aw.recommend)}
)}
); } // ── Standard-Karte (Netgo & andere) ──────────────────────────── return (
{alert.severity || 'UNKNOWN'} {alert.acknowledged && ✓ Quittiert} {alert.ticket_id && 🎫 Ticket #{alert.ticket_id}} {alert.source?.toUpperCase()}
{alert.device &&
🖥 {alert.device}{alert.service ? ` · ${alert.service}` : ''}
} {alert.state_transition &&
Status: {alert.state_transition}
} {alert.message &&
{alert.message}
}
{alert.customer && Kunde: {alert.customer}} {alert.state_time && 🕐 {alert.state_time}} Eingang: {timeAgo(alert.created_at)}
{actions}
); }; const otherAlerts = extAlerts.filter(a => a.source !== 'netgo' && a.source !== 'arcticwolf' && a.source !== 'mdo'); const otherUnacked = otherAlerts.filter(a => !a.acknowledged).length; const hiddenCount = otherAlerts.filter(a => a.acknowledged).length; const visibleAlerts = hideAcknowledged ? otherAlerts.filter(a => !a.acknowledged) : otherAlerts; return (
{otherAlerts.length} Alert{otherAlerts.length !== 1 ? 's' : ''} · {otherUnacked} nicht quittiert {hiddenCount > 0 && ( )}
{visibleAlerts.length === 0 && (
✅ Keine sonstigen Alerts vorhanden
)}
{visibleAlerts.map(renderAlertCard)}
); }; const renderNetGoAlerts = () => { const sevColor = (s) => s === 'CRIT' ? SC.crit : s === 'WARN' ? SC.warn : s === 'OK' ? SC.ok : SC.unknown; const parseAi = (alert) => { if (!alert.ai_analysis) return null; try { return typeof alert.ai_analysis === 'string' ? JSON.parse(alert.ai_analysis) : alert.ai_analysis; } catch { return null; } }; const parseTransition = (st) => { if (!st) return { from: '', to: '' }; const m = st.match(/^(.+?)\s+(?:to|→)\s+(.+)$/i); return m ? { from: m[1].trim(), to: m[2].trim() } : { from: '', to: st }; }; const hiddenCount = netgoAlerts.filter(a => a.acknowledged).length; const visible = hideAcknowledged ? netgoAlerts.filter(a => !a.acknowledged) : netgoAlerts; // Kanban-Spalten const columns = [ { key: 'CRIT', label: 'Kritisch', icon: '🔴', color: SC.crit, alerts: visible.filter(a => (parseAi(a)?.severity || a.severity) === 'CRIT') }, { key: 'WARN', label: 'Warnung', icon: '🟡', color: SC.warn, alerts: visible.filter(a => (parseAi(a)?.severity || a.severity) === 'WARN') }, { key: 'OK', label: 'Wiederhergestellt', icon: '🟢', color: SC.ok, alerts: visible.filter(a => (parseAi(a)?.severity || a.severity) === 'OK') }, { key: 'UNKNOWN', label: 'Unbekannt', icon: '⚪', color: SC.unknown, alerts: visible.filter(a => !['CRIT','WARN','OK'].includes(parseAi(a)?.severity || a.severity)) }, ].filter(col => col.alerts.length > 0); const KanbanCard = ({ alert }) => { const ai = parseAi(alert); const sev = ai?.severity || alert.severity; const col = sevColor(sev); const trans = parseTransition(alert.state_transition); const stateFrom = ai?.state_from || trans.from; const stateTo = ai?.state_to || trans.to; const device = ai?.device || (alert.device?.length < 40 ? alert.device : '–'); const service = ai?.service || (alert.service?.length < 80 ? alert.service : ''); return (
{/* Card-Header */}
🖥 {device}
{timeAgo(alert.created_at)}
{service &&
{service}
}
{/* State Transition */} {(stateFrom || stateTo) && (
{stateFrom && {stateFrom}} {stateFrom && stateTo && } {stateTo && {stateTo}}
)} {/* KI-Block */}
{ai?.metrics && (
📊 {ai.metrics}
)} {ai?.what ? ( <>
{ai.what}
{ai.recommendation && (
{ai.action_needed ? '⚠️' : '✅'} {ai.recommendation}
)} ) : (
⏳ KI-Analyse ausstehend…
)}
{/* Footer */}
{ai && 🤖 KI} {alert.acknowledged && ✓ Quittiert}
{!alert.acknowledged && ( )}
); }; return (
{/* Header */}
{netgoAlerts.length} Alerts · {netgoUnacked} nicht quittiert {hiddenCount > 0 && ( )}
{visible.length === 0 && (
✅ Keine aktiven NetGo-Alerts
)} {/* Kanban Board */}
{columns.map(col => (
{/* Spalten-Header */}
{col.icon} {col.label} {col.alerts.length}
{/* Cards */}
{col.alerts.map(alert => )}
))}
); }; const renderArcticWolfAlerts = () => { const sevColor = (s) => s === 'CRIT' ? SC.crit : s === 'WARN' ? SC.warn : s === 'OK' ? SC.ok : SC.unknown; const hiddenCount = awAlerts.filter(a => a.acknowledged).length; const visible = hideAcknowledged ? awAlerts.filter(a => !a.acknowledged) : awAlerts; const renderAWCard = (alert) => { const color = sevColor(alert.severity); const expanded = expandedAlerts[alert.id]; let aw = null; if (alert.raw_body) { try { aw = JSON.parse(alert.raw_body); } catch {} } const labelStyle = { fontSize: 11, color: 'var(--text-muted)', minWidth: 130, flexShrink: 0 }; const valStyle = { fontSize: 12, color: 'var(--text-primary)', wordBreak: 'break-word' }; const row = (label, val) => val ? (
{label}{val}
) : null; const section = (title, text) => text ? (
{title}
{text}
) : null; return (
{aw?.severity_label || alert.severity} {alert.acknowledged && ✓ Quittiert} {alert.ticket_id && 🎫 Ticket #{alert.ticket_id}}
{alert.message || aw?.incident_name}
🖥 {alert.device}{aw?.ip ? ` · ${aw.ip}` : ''} {alert.state_time && 🕐 {alert.state_time}} Eingang: {timeAgo(alert.created_at)}
{!alert.ticket_id && ( )} {!alert.acknowledged && ( )}
{aw && (
{row('Event-Typ', aw.event_type)} {row('Application', aw.application)} {row('User', aw.user)} {row('Process', aw.process)}
)} {aw && (aw.what_is_it || aw.why_matters || aw.next_steps || aw.recommend) && ( )} {expanded && aw && (
{section('Was ist passiert?', aw.what_is_it)} {section('Warum ist das relevant?', aw.why_matters)} {section('Wie wurde es erkannt?', aw.how_detected)} {section('Nächste Schritte', aw.next_steps)} {section('Empfehlungen Arctic Wolf', aw.recommend)}
)}
); }; return (
{awAlerts.length} Alert{awAlerts.length !== 1 ? 's' : ''} · {awUnacked} nicht quittiert {hiddenCount > 0 && ( )}
{visible.length === 0 && (
✅ Keine Arctic Wolf Alerts vorhanden
)}
{visible.map(renderAWCard)}
); }; const renderMdoAlerts = () => { const sevColor = (s) => s === 'CRIT' ? SC.crit : s === 'WARN' ? SC.warn : s === 'OK' ? SC.ok : SC.unknown; const parseAi = (alert) => { try { return alert.ai_analysis ? (typeof alert.ai_analysis === 'string' ? JSON.parse(alert.ai_analysis) : alert.ai_analysis) : null; } catch { return null; } }; const parseRaw = (alert) => { try { return alert.raw_body ? (typeof alert.raw_body === 'string' ? JSON.parse(alert.raw_body) : alert.raw_body) : null; } catch { return null; } }; const hiddenCount = mdoAlerts.filter(a => a.acknowledged).length; const visible = hideAcknowledged ? mdoAlerts.filter(a => !a.acknowledged) : mdoAlerts; const labelStyle = { fontSize: 11, color: 'var(--text-muted)', minWidth: 140, flexShrink: 0 }; const valStyle = { fontSize: 12, color: 'var(--text-primary)', wordBreak: 'break-word' }; const row = (label, val) => val ? (
{label}{val}
) : null; const renderMdoCard = (alert) => { const color = sevColor(alert.severity); const expanded = expandedAlerts[alert.id]; const ai = parseAi(alert); const raw = parseRaw(alert); return (
{alert.severity} MS DEFENDER {alert.acknowledged && ✓ Quittiert} {alert.ticket_id && 🎫 Ticket #{alert.ticket_id}}
{alert.message}
👤 {alert.device || '(kein Benutzer)'} {alert.service && 📂 {alert.service}} {alert.state_time && 🕐 {new Date(alert.state_time).toLocaleString('de-DE')}} Eingang: {timeAgo(alert.created_at)}
{!alert.ticket_id && } {!alert.acknowledged && }
{raw && (
{row('Kategorie', raw.category)} {row('Bedrohungsfamilie', raw.threatFamilyName)} {row('Angriffsvektor', ai?.attack_vector || (raw.attackTechniques || [])[0])} {row('Bedrohungstyp', ai?.threat_type)} {row('Status (Defender)', alert.state_transition)}
)} {(ai?.what || ai?.recommendation) && ( )} {!ai &&
⏳ KI-Analyse ausstehend…
} {expanded && ai && (
{ai.what && (
Was ist passiert?
{ai.what}
)} {ai.recommendation && (
{ai.action_needed ? '⚠️' : '✅'} {ai.recommendation}
)}
)}
); }; return (
{mdoAlerts.length} Alert{mdoAlerts.length !== 1 ? 's' : ''} · {mdoUnacked} nicht quittiert {hiddenCount > 0 && ( )}
{visible.length === 0 &&
✅ Keine aktiven Microsoft Defender Alerts
}
{visible.map(renderMdoCard)}
); }; const tabs = [ { id: 'overview', label: '🖥 Host-Übersicht', badge: null }, { id: 'network', label: 'Netzwerk', badge: devices.length + unifiDevices.length }, { id: 'agents', label: 'Agents', badge: agents.length }, { id: 'problems', label: 'Probleme', badge: activeProblems.length, red: activeProblems.length > 0 }, { id: 'netgo', label: 'NetGo', badge: netgoUnacked, red: netgoUnacked > 0 }, { id: 'arcticwolf', label: 'Arctic Wolf', badge: awUnacked, red: awUnacked > 0 }, { id: 'mdo', label: 'MS Defender', badge: mdoUnacked, red: mdoUnacked > 0 }, { id: 'ext-alerts', label: 'Sonstige', badge: extUnacked - netgoUnacked - awUnacked - mdoUnacked > 0 ? extUnacked - netgoUnacked - awUnacked - mdoUnacked : null, red: false }, ]; return (

📡 Monitoring

{/* Checkmk-style Tactical Overview */} {/* Tab bar */}
{tabs.map(({ id, label, badge, red }) => ( ))}
{activeTab === 'overview' && renderOverview()} {activeTab === 'network' && renderNetwork()} {activeTab === 'agents' && renderAgents()} {activeTab === 'problems' && renderProblems()} {activeTab === 'netgo' && renderNetGoAlerts()} {activeTab === 'arcticwolf' && renderArcticWolfAlerts()} {activeTab === 'mdo' && renderMdoAlerts()} {activeTab === 'ext-alerts' && renderExtAlerts()} {selectedDevice && setSelectedDevice(null)} onEdit={d => { setEditDevice(d); setShowDeviceForm(true); }} onDelete={d => { handleNetDelete(d); setSelectedDevice(null); }} onCheckNow={networkMonitorService.checkNow} />} {showDeviceForm && { setShowDeviceForm(false); setEditDevice(null); }} />} {showDiscover && setShowDiscover(false)} onAdd={handleNetSave} />} {selectedAgent && setSelectedAgent(null)} onDelete={handleAgentDelete} />} {showUnifiCfg && { setUnifiConfig(cfg); loadUnifi(); toast.success('Gespeichert'); }} onClose={() => setShowUnifiCfg(false)} />}
); }; export default MonitoringPage;