import React, { useState, useEffect, useCallback } from 'react'; const API = process.env.REACT_APP_API_URL || '/api'; const apiFetch = (url) => fetch(url).then(r => r.ok ? r.json() : null).catch(() => null); /* ── Design tokens ─────────────────────────────────────────── */ const ACCENT = '#4FD1C5'; const C_OK = '#34D399'; const C_WARN = '#F59E0B'; const C_ERROR = '#EF4444'; const C_INFO = '#60A5FA'; // const C_CRIT = '#F43F5E'; // reserved for future use /* ── Mesh palettes per slide tint ──────────────────────────── */ const MESH_PAL = { mint: ['#0a3d3a','#0d4f48','#082b29','#0b1f1d'], red: ['#3d0a1a','#4f0d22','#2b0810','#1f0b0e'], blue: ['#0a1f3d','#0d2a4f','#08152b','#0b121f'], violet: ['#22093d','#2d0c4f','#16062b','#10071f'], neutral: ['#0a1014','#0e1419','#070b0e','#04070a'], }; /* ════════════════════════════════════════════════════════════ HOOKS ════════════════════════════════════════════════════════════ */ function useNow(interval = 1000) { const [now, setNow] = useState(() => new Date()); useEffect(() => { const id = setInterval(() => setNow(new Date()), interval); return () => clearInterval(id); }, [interval]); return now; } function useCountUp(target, durationMs = 1400, deps = []) { const [val, setVal] = useState(target); useEffect(() => { if (document.visibilityState !== 'visible') { setVal(target); return; } let raf; const start = performance.now(); const to = Number(target) || 0; setVal(0); const tick = (t) => { const p = Math.min(1, (t - start) / durationMs); const eased = 1 - Math.pow(1 - p, 3); setVal(to * eased); if (p < 1) raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); const safety = setTimeout(() => setVal(to), 200); return () => { cancelAnimationFrame(raf); clearTimeout(safety); }; // eslint-disable-next-line }, deps); return val; } function useStageScale(w = 1920, h = 1080) { const [scale, setScale] = useState(1); useEffect(() => { const fit = () => { const vw = window.innerWidth, vh = window.innerHeight; if (vw < 4 || vh < 4) return; setScale(Math.min(vw / w, vh / h)); }; fit(); window.addEventListener('resize', fit); const retries = [50, 150, 400].map(t => setTimeout(fit, t)); return () => { window.removeEventListener('resize', fit); retries.forEach(clearTimeout); }; }, [w, h]); return scale; } /* ════════════════════════════════════════════════════════════ ATOMS ════════════════════════════════════════════════════════════ */ function MeshBackground({ tint = 'neutral', intensity = 1 }) { const p = MESH_PAL[tint] || MESH_PAL.neutral; const k = intensity; return (
")` }} />
); } function ClockBig() { const now = useNow(1000); const hh = String(now.getHours()).padStart(2, '0'); const mm = String(now.getMinutes()).padStart(2, '0'); const ss = String(now.getSeconds()).padStart(2, '0'); const dateStr = now.toLocaleDateString('de-DE', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }); return (
{hh} : {mm} : {ss}
{dateStr}
); } function BrandChip({ section }) { return (
IT NEXUS {section && <> {section} }
); } function SlideIndicator({ total, active, durationMs, autoplay }) { return (
{Array.from({ length: total }).map((_, i) => (
{i === active && autoplay && (
)}
))}
); } function Sparkline({ data = [], width = 400, height = 80, color = ACCENT, fill = true, strokeW = 2 }) { if (!data || data.length < 2) return null; const min = Math.min(...data), max = Math.max(...data); const range = max - min || 1; const step = width / (data.length - 1); const pts = data.map((v, i) => [i * step, height - ((v - min) / range) * (height - 8) - 4]); const path = pts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(' '); const area = `${path} L${width},${height} L0,${height} Z`; const id = `sg-${color.replace(/[^a-z0-9]/gi, '')}-${width}`; return ( {fill && } ); } function Donut({ value, size = 220, stroke = 16, color = ACCENT, label, sublabel, animateKey }) { const animated = useCountUp(value, 1400, [animateKey, value]); const r = (size - stroke) / 2; const c = 2 * Math.PI * r; const off = c - (animated / 100) * c; return (
{Math.round(animated)}%
{label &&
{label}
} {sublabel &&
{sublabel}
}
); } function BigNumber({ value, suffix = '', decimals = 0, animateKey }) { const animated = useCountUp(value, 1400, [animateKey, value]); const display = decimals === 0 ? Math.round(animated).toLocaleString('de-DE') : animated.toFixed(decimals).replace('.', ','); return {display}{suffix}; } function StatusDot({ status, size = 10 }) { const colors = { ok: C_OK, online: C_OK, warn: C_WARN, error: C_ERROR, offline: C_ERROR, info: C_INFO }; const c = colors[status] || '#94A3B8'; const pulse = status === 'ok' || status === 'online'; return ( ); } /* ── Bento tile ─────────────────────────────────────────────── */ const TILE_GLOWS = { hero: 'rgba(79,209,197,0.22)', danger: 'rgba(244,63,94,0.22)', warn: 'rgba(245,158,11,0.20)', info: 'rgba(96,165,250,0.20)', success: 'rgba(52,211,153,0.22)', }; const TILE_BG = { hero: 'linear-gradient(165deg,rgba(79,209,197,0.16),rgba(79,209,197,0.025) 60%,rgba(255,255,255,0.012))', danger: 'linear-gradient(165deg,rgba(244,63,94,0.16),rgba(244,63,94,0.025) 60%,rgba(255,255,255,0.012))', warn: 'linear-gradient(165deg,rgba(245,158,11,0.14),rgba(245,158,11,0.025) 60%,rgba(255,255,255,0.012))', info: 'linear-gradient(165deg,rgba(96,165,250,0.14),rgba(96,165,250,0.025) 60%,rgba(255,255,255,0.012))', success: 'linear-gradient(165deg,rgba(52,211,153,0.14),rgba(52,211,153,0.025) 60%,rgba(255,255,255,0.012))', '': 'linear-gradient(180deg,rgba(255,255,255,0.045),rgba(255,255,255,0.012))', }; const TILE_BORDER = { hero: 'rgba(79,209,197,0.22)', danger: 'rgba(244,63,94,0.22)', warn: 'rgba(245,158,11,0.22)', info: 'rgba(96,165,250,0.22)', success: 'rgba(52,211,153,0.22)', '': 'rgba(255,255,255,0.07)', }; function BTile({ children, style, variant = '', animDelay = 0, cornerGlyph }) { return (
{/* hairline */}
{/* glow */} {TILE_GLOWS[variant] && (
)} {cornerGlyph && (
{cornerGlyph}
)} {children}
); } function BLabel({ children, style }) { return
{children}
; } function SlideHeader({ eyebrow, title, accentSpan, right }) { return (
{eyebrow}
{title}{accentSpan && <> {accentSpan}}
{right}
); } function BentoGrid({ children, style }) { return (
{children}
); } function SlideWrap({ k, tint, children }) { return (
{children}
); } /* ════════════════════════════════════════════════════════════ DATA PROCESSING ════════════════════════════════════════════════════════════ */ function processData(data) { const now = Date.now(); const rawAgents = data.agentList || []; const agentList = rawAgents.map(a => { const ms = a.last_checkin ? new Date(a.last_checkin + 'Z').getTime() : 0; const online = ms && (now - ms) < 15 * 60 * 1000; return { name: a.hostname || '?', cpu: a.cpu_usage_percent || 0, ram: a.ram_usage_percent || 0, status: online ? 'online' : 'offline' }; }); const agentsOnline = agentList.filter(a => a.status === 'online'); const avgCpu = agentsOnline.length > 0 ? agentsOnline.reduce((s, a) => s + a.cpu, 0) / agentsOnline.length : 0; // Synthetic 24h CPU trend const cpuTrend24h = Array.from({ length: 24 }, (_, i) => { const t = i / 23; return Math.max(0, Math.min(100, avgCpu * (0.5 + t * 0.5) + Math.sin(i * 0.8) * 6)); }); const total = agentList.length; const online = agentsOnline.length; const offline = total - online; const patch = data.patch || {}; const patchTotal = patch.total || total || 1; const compliancePct = Math.round(((patch.compliant || 0) / patchTotal) * 100); const noEncrypt = data.monitoring?.noEncrypt || 0; const encrypted = total - noEncrypt; const bitlockerPct = total > 0 ? Math.round((encrypted / total) * 100) : 0; const secReport = data.lastSecReport; const loginFailures = secReport?.analysis?.login_stats?.failed || 0; // Synthetic 14-day login failure trend const failuresTrend14d = Array.from({ length: 14 }, (_, i) => { const base = loginFailures; return Math.max(0, Math.round(base * (0.3 + i * 0.05) + Math.sin(i * 0.9) * base * 0.12)); }); const tickets = data.tickets || {}; const ticketPerDay7 = data.ticketMetrics?.perDay7 || data.ticketMetrics?.daily?.slice(-7)?.map(d => d.count) || [0, 0, 0, 0, 0, 0, 0]; return { agentList, agents: { online, offline, total, cpuTrend24h }, patches: { compliancePct, current: patch.compliant || 0, warning: patch.warnings || 0, offline: patch.offline || 0, pending: patch.total_pending_updates || 0, total: patchTotal, }, security: { bitlockerPct, encrypted, unencrypted: noEncrypt, loginFailures, failuresTrend14d, lastReport: secReport ? new Date(secReport.created_at).toLocaleDateString('de-DE') : '—', risk: (loginFailures > 5000 || noEncrypt > 5) ? 'HOCH' : (loginFailures > 1000 || noEncrypt > 1) ? 'MITTEL' : 'NIEDRIG', }, tickets: { open: tickets.open || 0, inProgress: tickets.inProgress || 0, solvedToday: tickets.closedToday || 0, byPriority: { critical: tickets.critical || 0, high: tickets.high || 0, medium: tickets.medium || 0, low: tickets.low || 0 }, perDay7: ticketPerDay7, }, // Static: network, backup, CVE (no backend endpoint yet) network: [ { name: 'Internet', status: 'ok', latency: '12 ms', detail: '1 Gbit/s' }, { name: 'VPN', status: 'ok', latency: '24 ms', detail: 'Aktive Sessions' }, { name: 'Exchange Online', status: 'ok', latency: '48 ms', detail: 'Microsoft 365' }, { name: 'SharePoint', status: 'ok', latency: '52 ms', detail: 'OK' }, { name: 'Proxmox Cluster', status: 'ok', latency: '3 ms', detail: '3 Nodes' }, { name: 'NoSpamProxy', status: 'ok', latency: '18 ms', detail: 'OK' }, { name: 'Swyx Telefonie', status: 'ok', latency: '9 ms', detail: 'OK' }, { name: 'SelectLine ERP', status: 'ok', latency: '14 ms', detail: 'OK' }, ], backups: { coverage: 96, successful: 42, failed: 1, running: 0, lastSuccess: '06:14', lastFailJob: 'FAM102223 · Vollsicherung', trend14d: [98,99,100,100,97,95,100,100,99,98,100,96,94,96], }, cves: [ { id: 'CVE-2026-13421', sev: 'critical', cvss: 9.8, title: 'Apache Tomcat — Pre-Auth RCE via AJP-Connector', vendor: 'Apache', published: '2 Std.' }, { id: 'CVE-2026-13380', sev: 'critical', cvss: 9.6, title: 'Microsoft Outlook — Zero-Click via Preview Pane', vendor: 'Microsoft', published: '5 Std.' }, { id: 'CVE-2026-13298', sev: 'high', cvss: 8.4, title: 'Fortinet FortiGate — Auth Bypass', vendor: 'Fortinet', published: '11 Std.' }, { id: 'CVE-2026-13211', sev: 'high', cvss: 7.9, title: 'VMware ESXi — Privilege Escalation', vendor: 'VMware', published: '18 Std.' }, { id: 'CVE-2026-13104', sev: 'medium', cvss: 6.5, title: 'Chrome V8 — Type Confusion', vendor: 'Google', published: '1 Tag' }, ], }; } /* ════════════════════════════════════════════════════════════ SLIDES ════════════════════════════════════════════════════════════ */ /* 1 – Overview */ function SlideOverview({ d, k }) { const onlinePct = d.agents.total > 0 ? Math.round((d.agents.online / d.agents.total) * 100) : 0; return ( {/* Hero — Agents */} Agents online
/ {d.agents.total}
{/* Fleet strip */}
{d.agentList.map((a, i) => (
))}
{d.agents.online} online {d.agents.offline} offline
{onlinePct} %
{/* Tickets */} Offene Tickets
{d.tickets.byPriority.critical} kritisch · {d.tickets.byPriority.high} hoch
{/* Security */} Login-Fehler · 30 Tage
Risiko: {d.security.risk}
{/* Updates */} Updates ausstehend
{d.patches.compliancePct} % konform
{/* Backup */} Backup Coverage
%
Letzter Lauf {d.backups.lastSuccess} Uhr
{/* CVE */} Kritische CVEs
{d.cves.filter(c => c.sev === 'critical').length}
{d.cves[0].vendor} · {d.cves[0].id}
); } /* 2 – Agents */ function SlideAgents({ d, k }) { const pct = d.agents.total > 0 ? Math.round((d.agents.online / d.agents.total) * 100) : 0; const onlineAgents = d.agentList.filter(a => a.status === 'online'); const top = [...onlineAgents].sort((a, b) => b.cpu - a.cpu).slice(0, 6); const avgCpu = onlineAgents.length > 0 ? Math.round(onlineAgents.reduce((s, a) => s + a.cpu, 0) / onlineAgents.length) : 0; return (
{d.agents.online} online {d.agents.offline} offline
Cluster-CPU-Last
Ø {avgCpu} % · jetzt {Math.round(d.agents.cpuTrend24h[d.agents.cpuTrend24h.length - 1])} %
vor 24 Stdjetzt
Top Auslastung
{(top.length > 0 ? top : Array(6).fill({ name: '—', cpu: 0 })).map((a, i) => (
{a.name}
50 ? C_WARN : ACCENT, fontFeatureSettings: '"tnum" 1' }}>{a.cpu.toFixed(1)} %
50 ? C_WARN : ACCENT }} />
))}
); } /* 3 – Patch Compliance */ function SlideCompliance({ d, k }) { const pct = d.patches.compliancePct; const color = pct < 50 ? C_ERROR : pct < 80 ? C_WARN : C_OK; return ( ⚠ unter Ziel · 95 %
} />
Ziel: 95 % Abweichung −{Math.max(0, 95 - pct)} %
Aktuell · konform
{d.patches.total > 0 ? Math.round((d.patches.current / d.patches.total) * 100) : 0} %
0 ? (d.patches.current / d.patches.total) * 100 : 0}%` }} />
Updates fehlen
0 ? (d.patches.warning / d.patches.total) * 100 : 0}%` }} />
Offline
0 ? (d.patches.offline / d.patches.total) * 100 : 0}%` }} />
); } /* 4 – Security */ function SlideSecurity({ d, k }) { return ( ⚠ Risiko {d.security.risk}
} /> Login-Fehler
fehlgeschlagene Anmeldeversuche · Microsoft 365
vor 14 Tagenheute · {d.security.failuresTrend14d[d.security.failuresTrend14d.length - 1]}/Tag
Verschlüsselungs-Abdeckung
= 90 ? C_OK : C_WARN }}> {d.security.encrypted}/{d.agents.total}
= 90 ? C_OK : C_WARN, transition: 'width 1.4s cubic-bezier(.2,.7,.2,1)', width: `${d.security.bitlockerPct}%` }} />
Unverschlüsselt
BitLocker deaktiviert
Report
{d.security.lastReport}
SIEM · auto
); } /* 5 – Helpdesk */ function SlideHelpdesk({ d, k }) { const prios = [ { label: 'Kritisch', v: d.tickets.byPriority.critical, c: C_ERROR }, { label: 'Hoch', v: d.tickets.byPriority.high, c: C_WARN }, { label: 'Mittel', v: d.tickets.byPriority.medium, c: C_INFO }, { label: 'Niedrig', v: d.tickets.byPriority.low, c: 'rgba(244,245,247,0.62)' }, ]; return (
offene Tickets · {d.tickets.byPriority.critical === 0 ? 'keine Eskalationen' : `${d.tickets.byPriority.critical} kritisch`}
Verteilung
{prios.map(p => (
{p.label}
))}
Tickets pro Tag
0 ? d.tickets.perDay7 : [0, 0, 0, 0, 0, 0, 0]} color={ACCENT} width={500} height={130} />
{['Mo','Di','Mi','Do','Fr','Sa','So'].map(d => {d})}
); } /* 6 – Network */ function SlideNetwork({ d, k }) { const trends = d.network.map((s, i) => Array.from({ length: 12 }, (_, j) => { const base = parseInt(s.latency, 10) || 20; return base + Math.sin(j * 0.6 + i) * (s.status === 'warn' ? 80 : 10) + (j % 3) * 4; })); const okCount = d.network.filter(s => s.status === 'ok').length; const warnCount = d.network.filter(s => s.status === 'warn').length; return (
{okCount} OK
{warnCount > 0 &&
{warnCount} Warnung
}
} /> {d.network.map((s, i) => { const color = s.status === 'ok' ? C_OK : s.status === 'warn' ? C_WARN : C_ERROR; const variant = s.status === 'warn' ? 'warn' : s.status === 'error' ? 'danger' : ''; return (
{s.name}
{s.latency}
{s.detail}
); })}
); } /* 7 – Backup */ function SlideBackup({ d, k }) { return (
Letzte Sicherung um {d.backups.lastSuccess} Uhr
Verlauf
vor 14 Tagen Min {Math.min(...d.backups.trend14d)} % · jetzt {d.backups.trend14d[d.backups.trend14d.length - 1]} %
Erfolgreich
24 Std
Laufend
Jobs aktiv
0 ? 'warn' : 'success'} style={{ gridColumn: 'span 2', gridRow: 'span 2' }} animDelay={320} cornerGlyph="FAIL"> Fehler
0 ? C_WARN : C_OK, marginTop: 8 }}>
{d.backups.lastFailJob}
); } /* 8 – CVE Watch */ function SlideCVE({ d, k }) { const [top, ...rest] = d.cves; const sevCount = { critical: 0, high: 0, medium: 0, low: 0 }; d.cves.forEach(c => sevCount[c.sev] = (sevCount[c.sev] || 0) + 1); const sevStyle = { critical: { bg: 'rgba(244,63,94,0.15)', color: '#FCA5A5', border: 'rgba(244,63,94,0.3)' }, high: { bg: 'rgba(245,158,11,0.15)', color: '#FCD34D', border: 'rgba(245,158,11,0.3)' }, medium: { bg: 'rgba(96,165,250,0.15)', color: '#93C5FD', border: 'rgba(96,165,250,0.3)' }, low: { bg: 'rgba(148,163,184,0.15)', color: '#CBD5E1', border: 'rgba(148,163,184,0.25)' }, }; const SevPill = ({ sev, cvss }) => { const s = sevStyle[sev] || sevStyle.low; return ( {sev} · CVSS {cvss} ); }; return ( {sevCount.critical || 0} critical {sevCount.high || 0} high {sevCount.medium || 0} medium
} />
vor {top.published}
{top.title}
{top.vendor} · Sicherheitshinweis
{rest.map((c, i) => (
vor {c.published}
{c.title}
{c.vendor}
))}
); } /* ════════════════════════════════════════════════════════════ MAIN ════════════════════════════════════════════════════════════ */ const SLIDE_DURATION = 12000; const SLIDES = [ { id: 'overview', title: 'Übersicht', Component: SlideOverview }, { id: 'agents', title: 'Windows Agents', Component: SlideAgents }, { id: 'compliance', title: 'Patch Compliance', Component: SlideCompliance }, { id: 'security', title: 'Sicherheit', Component: SlideSecurity }, { id: 'helpdesk', title: 'Helpdesk', Component: SlideHelpdesk }, { id: 'network', title: 'Netzwerk', Component: SlideNetwork }, { id: 'backup', title: 'Backup', Component: SlideBackup }, { id: 'cve', title: 'CVE Watch', Component: SlideCVE }, ]; export default function TVDashboardPage() { const [rawData, setRawData] = useState({}); const [idx, setIdx] = useState(0); const [k, setK] = useState(0); const [paused, setPaused] = useState(false); const scale = useStageScale(1920, 1080); const loadData = useCallback(async () => { const r = await apiFetch(`${API}/tv/stats`); if (!r?.data) return; const d = r.data; const agents = d.agentList || []; const noEncrypt = agents.filter(a => a.bitlocker_status && !['encrypted', 'on'].includes(String(a.bitlocker_status).toLowerCase())).length; setRawData({ monitoring: { ...d.monitoring, noEncrypt }, agentList: agents, tickets: d.tickets || {}, patch: d.patch || {}, lastSecReport: d.lastSecReport || null, ticketMetrics: d.ticketMetrics || {}, }); }, []); useEffect(() => { loadData(); const id = setInterval(loadData, 60000); return () => clearInterval(id); }, [loadData]); const goTo = useCallback((i) => { setIdx(i); setK(x => x + 1); }, []); const next = useCallback(() => goTo((idx + 1) % SLIDES.length), [idx, goTo]); const prev = useCallback(() => goTo((idx - 1 + SLIDES.length) % SLIDES.length), [idx, goTo]); useEffect(() => { if (paused) return; const t = setTimeout(next, SLIDE_DURATION); return () => clearTimeout(t); }, [idx, paused, next]); useEffect(() => { const h = (e) => { if (e.key === 'ArrowRight' || e.key === ' ') { next(); e.preventDefault(); } else if (e.key === 'ArrowLeft') { prev(); e.preventDefault(); } else if (e.key === 'p' || e.key === 'P') setPaused(x => !x); else if (e.key === 'f' || e.key === 'F') { if (document.fullscreenElement) document.exitFullscreen?.(); else document.documentElement.requestFullscreen?.(); } }; window.addEventListener('keydown', h); return () => window.removeEventListener('keydown', h); }, [next, prev]); const d = processData(rawData); const { Component } = SLIDES[idx]; return (
{/* ── 1920×1080 scaled stage ── */}
{/* Controls overlay (bottom) */}
{idx + 1}/{SLIDES.length} · ←/→ · P · F
); } const ctrlBtn = { background: 'rgba(255,255,255,0.08)', border: '1px solid rgba(255,255,255,0.14)', color: 'rgba(255,255,255,0.55)', borderRadius: 10, width: 36, height: 36, cursor: 'pointer', fontSize: 16, display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'all 0.15s', };