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 (
);
}
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 (
);
}
/* ════════════════════════════════════════════════════════════
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.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',
};