Add: WM Football Theme für TV Dashboard + Live CVEs aus NVD NIST API

- TVDashboardPage.jsx komplett auf WM Matchday Theme umgestellt (1:1 Design)
- Fußballplatz-Hintergrund (PitchBackground mit SVG-Linien), rollender Ball
- Scoreboard-Header in allen 8 Slides mit LIVE-Indikator
- Jersey-Zahlen (count-up, lime/gold/rot), Confetti bei Clean Sheet / Backup ≥95%
- FBTile ersetzt BTile, WM-Metaphern: Anstoss, Aufstellung, Fitnesstest, Fairplay, Zu Null, Spielaufbau, Abwehr, Gegner-Analyse
- Neuer Backend-Endpoint GET /api/tv/cves: fetcht live von NVD NIST API, 1h In-Memory-Cache
- CVE-Slide zeigt echte aktuelle Sicherheitslücken aus dem Internet

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 12:02:34 +02:00
parent 54114b5b9f
commit c276b23293
3 changed files with 677 additions and 615 deletions

View File

@@ -1,4 +1,52 @@
const { getDatabase } = require('../config/database');
const https = require('https');
// In-memory CVE cache — refresh every hour
let cveCache = { ts: 0, data: [] };
function fetchCVEs() {
return new Promise((resolve) => {
const opts = {
hostname: 'services.nvd.nist.gov',
path: '/rest/json/cves/2.0?resultsPerPage=8&sortBy=published&sortOrder=desc',
headers: { 'User-Agent': 'IT-Nexus-TV/1.0' },
timeout: 8000,
};
const req = https.get(opts, (res) => {
let raw = '';
res.on('data', c => raw += c);
res.on('end', () => {
try {
const json = JSON.parse(raw);
const items = (json.vulnerabilities || []).map(v => {
const cve = v.cve;
const m31 = cve.metrics?.cvssMetricV31 || [];
const m30 = cve.metrics?.cvssMetricV30 || [];
const m = m31[0] || m30[0];
const score = m?.cvssData?.baseScore || 0;
const sevRaw = (m?.cvssData?.baseSeverity || 'medium').toLowerCase();
const sev = ['critical','high','medium','low'].includes(sevRaw) ? sevRaw : 'medium';
const desc = cve.descriptions?.find(d => d.lang === 'en')?.value || '';
const firstSentence = desc.split(/\.\s/)[0].replace(/\s+/g, ' ').trim();
const title = firstSentence.length > 110 ? firstSentence.substring(0, 110) + '…' : firstSentence || cve.id;
const pub = new Date(cve.published);
const diffH = Math.round((Date.now() - pub.getTime()) / 3600000);
const published = diffH < 1 ? '< 1 Std.' : diffH < 24 ? `${diffH} Std.` : `${Math.round(diffH / 24)} Tag${Math.round(diffH / 24) !== 1 ? 'e' : ''}`;
// Vendor from CPE or references
let vendor = 'NVD';
const cpe = cve.configurations?.[0]?.nodes?.[0]?.cpeMatch?.[0]?.criteria || '';
const m2 = cpe.match(/cpe:2\.3:[ao]:([^:]+):/);
if (m2) vendor = m2[1].charAt(0).toUpperCase() + m2[1].slice(1).replace(/_/g,' ');
return { id: cve.id, sev, cvss: score, title, vendor, published };
}).filter(c => c.cvss >= 5).slice(0, 5);
resolve(items.length >= 3 ? items : null);
} catch { resolve(null); }
});
});
req.on('error', () => resolve(null));
req.on('timeout', () => { req.destroy(); resolve(null); });
});
}
async function getStats(req, res) {
try {
@@ -73,4 +121,20 @@ async function getStats(req, res) {
}
}
module.exports = { getStats };
async function getCVEs(req, res) {
const now = Date.now();
if (now - cveCache.ts < 3600000 && cveCache.data.length >= 3) {
return res.json({ status: 'success', data: cveCache.data, cached: true });
}
const fresh = await fetchCVEs();
if (fresh) {
cveCache = { ts: now, data: fresh };
return res.json({ status: 'success', data: fresh, cached: false });
}
if (cveCache.data.length >= 3) {
return res.json({ status: 'success', data: cveCache.data, cached: true });
}
res.status(503).json({ status: 'error', message: 'CVE-Feed nicht erreichbar' });
}
module.exports = { getStats, getCVEs };

View File

@@ -1,7 +1,8 @@
const express = require('express');
const router = express.Router();
const { getStats } = require('../controllers/tv.controller');
const { getStats, getCVEs } = require('../controllers/tv.controller');
router.get('/stats', getStats);
router.get('/cves', getCVEs);
module.exports = router;

File diff suppressed because it is too large Load Diff