+
+
+
{children}
+
+ );
+}
+
+function WMHeader({ eyebrow, title, accent, scoreboard }) {
+ return (
+
+
+
{eyebrow}
+
{title}{accent && <> {accent}>}
+
+ {scoreboard}
+
+ );
+}
+
+function FBTile({ children, style, className = '', animDelay = 0, corner, ...rest }) {
+ return (
+
+ {corner &&
{corner}
}
+ {children}
);
}
@@ -311,6 +303,12 @@ function SlideWrap({ k, tint, children }) {
/* ════════════════════════════════════════════════════════════
DATA PROCESSING
════════════════════════════════════════════════════════════ */
+const FALLBACK_CVES = [
+ { id: 'CVE-2026-0001', sev: 'critical', cvss: 9.8, title: 'Lade CVEs vom NVD …', vendor: 'NVD', published: '—' },
+ { id: 'CVE-2026-0002', sev: 'high', cvss: 8.1, title: 'Verbindung zum NIST NVD wird aufgebaut', vendor: 'NVD', published: '—' },
+ { id: 'CVE-2026-0003', sev: 'medium', cvss: 6.5, title: 'Bitte warten …', vendor: 'NVD', published: '—' },
+];
+
function processData(data) {
const now = Date.now();
const rawAgents = data.agentList || [];
@@ -321,12 +319,10 @@ function processData(data) {
});
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;
@@ -341,468 +337,385 @@ function processData(data) {
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];
+ const ticketPerDay7 = data.ticketMetrics?.perDay7 || [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,
+ 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,
+ 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' },
+ { 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],
+ 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
+ SLIDES — WM Matchday
════════════════════════════════════════════════════════════ */
-/* 1 – Overview */
-function SlideOverview({ d, k }) {
+/* 1 – Anstoß / Übersicht */
+function WMOverview({ 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}
+
+
+
+
+
+
+
+ Einsatzquote
+
+
- {/* Fleet strip */}
-
- {d.agentList.map((a, i) => (
-
- ))}
+
{d.agents.online} von {d.agents.total} Spielern fit
+
+
+ Offene Tickets
+
+ Weiße Weste · keine Gegentore
+
+
+ Updates offen
+
+ {d.patches.compliancePct}% match-fit
+
+
+ Login-Fehler · 30T
+
+ Microsoft 365
+
+
+ CPU-Last der Mannschaft
+
+
-
-
- {d.agents.online} online
- {d.agents.offline} offline
-
-
{onlinePct} %
+
+
+ Backup-Abdeckung
+
+
-
-
- {/* 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}
-
-
-
-
+ Letzte Parade {d.backups.lastSuccess} Uhr
+
+
+
);
}
-/* 2 – Agents */
-function SlideAgents({ d, k }) {
+/* 2 – Aufstellung / Agents */
+function WMAgents({ 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;
+ const avg = 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
+
+ {d.agents.online} fit
+ {d.agents.offline} Bank
-
-
-
-
-
Cluster-CPU-Last
-
- Ø {avgCpu} % · jetzt {Math.round(d.agents.cpuTrend24h[d.agents.cpuTrend24h.length - 1])} %
-
+
+
+
+
Laufleistung der Mannschaft
+
Ø {avg}% · jetzt {Math.round(d.agents.cpuTrend24h[d.agents.cpuTrend24h.length - 1])}%
-
+
-
- vor 24 Stdjetzt
-
-
-
-
- Top Auslastung
-
+
+
+ Aktivste Spieler
+
{(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 }} />
-
+
+
{a.name}
+
50 ? '#FFC53D' : LIME, letterSpacing: -0.5 }}>{a.cpu.toFixed(1)}%
+
50 ? '#FFC53D' : LIME }} />
))}
-
-
-
+
+
+
);
}
-/* 3 – Patch Compliance */
-function SlideCompliance({ d, k }) {
+/* 3 – Fitnesstest / Compliance */
+function WMCompliance({ d, k }) {
const pct = d.patches.compliancePct;
- const color = pct < 50 ? C_ERROR : pct < 80 ? C_WARN : C_OK;
+ const color = pct < 50 ? '#FF3B30' : pct < 80 ? '#FFC53D' : LIME;
return (
-
- ⚠ unter Ziel · 95 %}
+
+ }
/>
-
-
+
+
-
+
-
-
Ziel: 95 %
-
Abweichung −{Math.max(0, 95 - pct)} %
+
+ Saisonziel: 95%Rückstand −{Math.max(0, 95 - pct)}%
-
-
-
- Aktuell · konform
-
-
-
{d.patches.total > 0 ? Math.round((d.patches.current / d.patches.total) * 100) : 0} %
+
+
+ Voll fit · einsatzbereit
+
+
+ {d.agents.total > 0 ? Math.round((d.patches.current / d.agents.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}%` }} />
-
-
-
-
+
0 ? (d.patches.current / d.agents.total) * 100 : 0}%`, background: LIME }} />
+
+
+ Updates fehlen
+
+ 0 ? (d.patches.warning / d.agents.total) * 100 : 0}%`, background: '#FFC53D' }} />
+
+
+ Offline
+
+ 0 ? (d.patches.offline / d.agents.total) * 100 : 0}%`, background: '#FF3B30' }} />
+
+
+
);
}
-/* 4 – Security */
-function SlideSecurity({ d, k }) {
+/* 4 – Fairplay / Security */
+function WMSecurity({ d, k }) {
return (
-
- ⚠ Risiko {d.security.risk} }
+
+ }
/>
-
-
- Login-Fehler
-
-
+
+
+
+
+
Login-Fehler · Microsoft 365
- fehlgeschlagene Anmeldeversuche · Microsoft 365
-
-
+
+
-
+
fehlgeschlagene Anmeldeversuche
+
+
+
+
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}
+
+
+
+
+
Verschlüsselungs-Quote
-
-
= 90 ? C_OK : C_WARN, transition: 'width 1.4s cubic-bezier(.2,.7,.2,1)', width: `${d.security.bitlockerPct}%` }} />
+
+
+ {d.security.encrypted}/{d.agents.total}
-
-
-
- Unverschlüsselt
-
- BitLocker deaktiviert
-
-
-
- Report
- {d.security.lastReport}
- SIEM · auto
-
-
-
+
+
+
+ Unverschlüsselt
+
+ BitLocker aus
+
+
+ Letzter
+ {d.security.lastReport}
+ SIEM · auto
+
+
+
);
}
-/* 5 – Helpdesk */
-function SlideHelpdesk({ d, k }) {
+/* 5 – Zu Null / Helpdesk */
+function WMHelpdesk({ d, k }) {
+ const cleanSheet = d.tickets.open === 0;
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)' },
+ { label: 'Kritisch', v: d.tickets.byPriority.critical, c: '#FF3B30' },
+ { label: 'Hoch', v: d.tickets.byPriority.high, c: '#FFC53D' },
+ { label: 'Mittel', v: d.tickets.byPriority.medium, c: '#60A5FA' },
+ { label: 'Niedrig', v: d.tickets.byPriority.low, c: 'rgba(255,255,255,0.6)' },
];
return (
-
-
-
-
+
+
+ }
+ />
+
+
-
-
-
-
offene Tickets · {d.tickets.byPriority.critical === 0 ? 'keine Eskalationen' : `${d.tickets.byPriority.critical} kritisch`}
+ {cleanSheet && (
+
+ WEISSE WESTE
+
+ )}
+
+
offene Tickets · keine Gegentore kassiert
-
-
-
- Verteilung
-
+
+
+ Nach Priorität
+
{prios.map(p => (
-
-
-
- {p.label}
+
+
+
+ {p.label}
-
+
))}
-
-
-
- Tickets pro Tag
+
+
+ Tickets pro Tag
- 0 ? d.tickets.perDay7 : [0, 0, 0, 0, 0, 0, 0]} color={ACCENT} width={500} height={130} />
+ 0 ? d.tickets.perDay7 : [0,0,0,0,0,0,0]} color={LIME} width={500} height={120} />
-
- {['Mo','Di','Mi','Do','Fr','Sa','So'].map(d =>
{d})}
+
+ {['Mo','Di','Mi','Do','Fr','Sa','So'].map(t => {t})}
-
-
-
+
+
+
);
}
-/* 6 – Network */
-function SlideNetwork({ d, k }) {
+/* 6 – Spielaufbau / Network */
+function WMNetwork({ 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' : '';
+ const color = s.status === 'ok' ? LIME : s.status === 'warn' ? '#FFC53D' : '#FF3B30';
+ const cls = s.status === 'warn' ? 'gold' : s.status === 'error' ? 'red' : '';
return (
-
-
-
+
+
+
-
-
{s.latency}
-
{s.detail}
-
+
{s.latency}
+
{s.detail}
+
);
})}
-
-
+
+
);
}
-/* 7 – Backup */
-function SlideBackup({ d, k }) {
+/* 7 – Abwehr / Backup */
+function WMBackup({ d, k }) {
+ const great = d.backups.coverage >= 95;
return (
-
-
-
-
+
+
+ }
+ />
+
+
-
+
- Letzte Sicherung um {d.backups.lastSuccess} Uhr
-
-
-
- Verlauf
+ Letzte Parade um {d.backups.lastSuccess} Uhr
+
+
+ Abwehrleistung
-
+
-
- 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}
-
-
-
+
+
+ Erfolgreich
+
+ 24 Std
+
+
+ Laufend
+
+ Jobs
+
+
+ Fehler
+
+ {d.backups.lastFailJob}
+
+
+
);
}
-/* 8 – CVE Watch */
-function SlideCVE({ d, k }) {
+/* 8 – Gegner-Analyse / CVE */
+function WMCVE({ 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);
+ if (!top) return Lade CVE-Daten …
;
+ const crit = d.cves.filter(c => c.sev === 'critical').length;
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)' },
@@ -818,37 +731,83 @@ function SlideCVE({ d, k }) {
);
};
return (
-
-
- ● {sevCount.critical || 0} critical
- ● {sevCount.high || 0} high
- ● {sevCount.medium || 0} medium
- }
+
+ }
/>
-
-
-
+
+
+
- vor {top.published}
+ vor {top.published}
-
- {top.title}
-
- {top.vendor} · Sicherheitshinweis
-
+ {top.title}
+ {top.vendor} · NVD Sicherheitshinweis
+
{rest.map((c, i) => (
-
-
+
+
- vor {c.published}
+ vor {c.published}
- {c.title}
- {c.vendor}
-
+ {c.title}
+ {c.vendor}
+
))}
-
-
+
+
+ );
+}
+
+/* ════════════════════════════════════════════════════════════
+ CHROME — Brand, Clock, Indicator
+════════════════════════════════════════════════════════════ */
+function BrandChip({ section }) {
+ return (
+
+
+ IT NEXUS
+ {section && <>
+
+ {section}
+ >}
+
+ );
+}
+
+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 SlideIndicator({ total, active, durationMs, autoplay }) {
+ return (
+
+ {Array.from({ length: total }).map((_, i) => (
+
+ {i === active && autoplay && (
+
+ )}
+
+ ))}
+
);
}
@@ -858,18 +817,19 @@ function SlideCVE({ d, k }) {
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 },
+ { id: 'overview', title: 'ANSTOSS', Component: WMOverview },
+ { id: 'agents', title: 'AUFSTELLUNG', Component: WMAgents },
+ { id: 'compliance', title: 'FITNESSTEST', Component: WMCompliance },
+ { id: 'security', title: 'FAIRPLAY', Component: WMSecurity },
+ { id: 'helpdesk', title: 'ZU NULL', Component: WMHelpdesk },
+ { id: 'network', title: 'SPIELAUFBAU', Component: WMNetwork },
+ { id: 'backup', title: 'ABWEHR', Component: WMBackup },
+ { id: 'cve', title: 'GEGNER', Component: WMCVE },
];
export default function TVDashboardPage() {
const [rawData, setRawData] = useState({});
+ const [cves, setCves] = useState(FALLBACK_CVES);
const [idx, setIdx] = useState(0);
const [k, setK] = useState(0);
const [paused, setPaused] = useState(false);
@@ -878,20 +838,26 @@ export default function TVDashboardPage() {
const loadData = useCallback(async () => {
const r = await apiFetch(`${API}/tv/stats`);
if (!r?.data) return;
- const d = r.data;
- const agents = d.agentList || [];
+ const data = r.data;
+ const agents = data.agentList || [];
const noEncrypt = agents.filter(a => a.bitlocker_status && !['encrypted', 'on'].includes(String(a.bitlocker_status).toLowerCase())).length;
setRawData({
- monitoring: { ...d.monitoring, noEncrypt },
+ monitoring: { ...data.monitoring, noEncrypt },
agentList: agents,
- tickets: d.tickets || {},
- patch: d.patch || {},
- lastSecReport: d.lastSecReport || null,
- ticketMetrics: d.ticketMetrics || {},
+ tickets: data.tickets || {},
+ patch: data.patch || {},
+ lastSecReport: data.lastSecReport || null,
+ ticketMetrics: data.ticketMetrics || {},
});
}, []);
+ const loadCVEs = useCallback(async () => {
+ const r = await apiFetch(`${API}/tv/cves`);
+ if (r?.data?.length >= 3) setCves(r.data);
+ }, []);
+
useEffect(() => { loadData(); const id = setInterval(loadData, 60000); return () => clearInterval(id); }, [loadData]);
+ useEffect(() => { loadCVEs(); const id = setInterval(loadCVEs, 3600000); return () => clearInterval(id); }, [loadCVEs]);
const goTo = useCallback((i) => { setIdx(i); setK(x => x + 1); }, []);
const next = useCallback(() => goTo((idx + 1) % SLIDES.length), [idx, goTo]);
@@ -917,7 +883,7 @@ export default function TVDashboardPage() {
return () => window.removeEventListener('keydown', h);
}, [next, prev]);
- const d = processData(rawData);
+ const d = { ...processData(rawData), cves };
const { Component } = SLIDES[idx];
return (
@@ -925,7 +891,6 @@ export default function TVDashboardPage() {
fontFamily: '"SF Pro Display","Inter",-apple-system,BlinkMacSystemFont,system-ui,sans-serif',
WebkitFontSmoothing: 'antialiased' }}>
- {/* ── 1920×1080 scaled stage ── */}
- {/* Controls overlay (bottom) */}
-
+
{idx + 1}/{SLIDES.length} · ←/→ · P · F
@@ -953,73 +917,106 @@ export default function TVDashboardPage() {
);
}
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)',
+ background: 'rgba(124,245,62,0.08)',
+ border: '1px solid rgba(124,245,62,0.2)',
+ color: 'rgba(124,245,62,0.7)',
borderRadius: 10, width: 36, height: 36,
cursor: 'pointer', fontSize: 16,
display: 'flex', alignItems: 'center', justifyContent: 'center',