Quelltext des aktuellen Standes
<?php $this->layout('layouts/main', ['page_title' => $page_title, 'current_route' => 'cve', 'full_width' => true]); ?>
<style>
/* CVE-Modul – modul-spezifische Komponenten (Prefix: cve-).
Standard-Komponenten kommen aus app.css: page-body, kpi-row/kpi-col,
table-wrapper/data-table, status-pill, empty-state. */
/* Aufgeklappte Detail-Zeile */
tr.cve-open td { background:var(--sky-50); }
.cve-detail-row { display:none; }
.cve-detail-row.open { display:table-row; }
.cve-detail-cell { padding:.75rem 1rem 1rem 2.5rem !important; background:var(--slate-50); border-bottom:2px solid var(--gray-200) !important; }
.cve-detail-inner { font-size:.82rem; color:var(--gray-700); line-height:1.6; }
.cve-affected-chip { display:inline-block; background:var(--gray-100); border:1px solid var(--gray-200); border-radius:4px; font-size:.72rem; padding:.1rem .45rem; color:var(--gray-700); margin:.1rem .15rem; }
/* Keyword-Chips */
.cve-keyword-chip { display:inline-flex; align-items:center; gap:.35rem; background:var(--blue-50); border:1px solid var(--blue-200); border-radius:20px; padding:.25rem .75rem; font-size:.82rem; color:var(--blue-700); }
.cve-keyword-del { background:none; border:none; color:var(--blue-300); cursor:pointer; font-size:1rem; padding:0; line-height:1; }
.cve-keyword-del:hover { color:var(--red-600); }
/* NVD-Launchpad */
.cve-nvd-card { background:var(--color-bg); border:1px solid var(--gray-200); border-radius:.75rem; padding:1.25rem 1.5rem; margin-top:1.25rem; }
.cve-nvd-card h3 { margin:0 0 .35rem; font-size:.95rem; font-weight:700; color:var(--gray-900); display:flex; align-items:center; gap:.4rem; }
.cve-nvd-card p { margin:0 0 .9rem; font-size:.83rem; color:var(--gray-500); }
.cve-nvd-row { display:flex; gap:.5rem; flex-wrap:wrap; align-items:center; }
.cve-nvd-crit { border-color:var(--red-300); color:var(--red-600); }
.cve-nvd-high { border-color:var(--orange-300); color:var(--orange-600); }
.cve-nvd-search { display:flex; gap:.5rem; margin-top:.85rem; max-width:480px; }
/* Filter-Buttons (innerhalb der Standard-Filter-Card / .filter-card-body) */
.cve-filterbar { margin-bottom:0; }
.cve-kw-list { display:inline-flex; flex-wrap:wrap; gap:.2rem; align-items:center; }
.cve-kw-tag { background:var(--blue-50); border:1px solid var(--blue-200); border-radius:4px; padding:.05rem .4rem; font-size:.72rem; color:var(--blue-700); }
/* Refresh-Button Spin */
@keyframes cve-spin { to { transform:rotate(360deg); } }
.cve-spin { animation:cve-spin .8s linear infinite; display:inline-block; }
/* Keyword-Modal: Overlay/Karte kommen zentral aus app.css (.modal-overlay/.modal-card);
lokal nur Zustands-Toggle (.open), z-index über der Topbar und die Modul-Breite. */
.cve-modal { display:none; z-index:1050; }
.cve-modal.open { display:flex; }
.cve-modal .modal-card { max-width:520px; }
.cve-pager { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:.5rem; margin:.75rem 0 0; }
.cve-pager-info { font-size:.8rem; color:var(--gray-500); }
/* BSI-Verknüpfung */
.cve-bsi-badge { display:inline-block; margin-left:.4rem; padding:.05rem .4rem; border-radius:4px; background:var(--amber-100); border:1px solid var(--amber-300); color:var(--amber-800); font-size:.68rem; font-weight:700; text-decoration:none; vertical-align:middle; white-space:nowrap; }
.cve-bsi-badge:hover { background:var(--amber-200); }
.cve-sources-note { display:flex; align-items:flex-start; gap:.6rem; background:var(--blue-50); border:1px solid var(--blue-200); border-radius:.5rem; padding:.7rem 1rem; margin-bottom:1.25rem; font-size:.83rem; color:var(--color-primary-deep); line-height:1.55; }
.cve-sources-icon { font-size:1.05rem; line-height:1.4; }
</style>
<?php
$keywordCves = $keyword_cves ?? [];
$keywords = $keywords ?? [];
$stats = $stats ?? ['CRITICAL'=>0,'HIGH'=>0,'MEDIUM'=>0,'LOW'=>0,'total'=>0];
$lastRefresh = $last_refresh ?? null;
$filterSev = $filter_sev ?? '';
$filterQ = $filter_q ?? '';
$canManage = $can_manage ?? false;
$cacheStale = $cache_stale ?? false;
$csrfToken = $this->csrf();
$hasKeywords = !empty($keywords);
$bsiEnabled = $bsi_enabled ?? false;
$bsiRelevant = $bsi_relevant ?? [];
$bsiOther = $bsi_other ?? [];
$bsiByCve = $bsi_by_cve ?? [];
$cvePage = $page ?? 1;
$cvePerPage = $per_page ?? 100;
$cveTotal = $total ?? count($keywordCves);
$cvePages = max(1, (int)ceil($cveTotal / $cvePerPage));
// Aktuelle Sortierung
$sortCol = $sort_col ?? 'published';
$sortDir = $sort_dir ?? 'desc';
// Filter + Sortierung für Pager-Links erhalten
$pagerQuery = function (int $p) use ($filterSev, $filterQ, $sortCol, $sortDir): string {
$qs = ['page' => $p];
if ($filterSev !== '') { $qs['severity'] = $filterSev; }
if ($filterQ !== '') { $qs['q'] = $filterQ; }
$qs['sort'] = $sortCol;
$qs['dir'] = $sortDir;
return '/cve?' . http_build_query($qs);
};
// Sortier-Link je Spalte: klick = aufsteigend, erneuter Klick = absteigend.
// Filter bleiben erhalten; Seite springt auf 1 zurück.
$sortUrl = function (string $col) use ($filterSev, $filterQ, $sortCol, $sortDir): string {
$newDir = ($sortCol === $col && $sortDir === 'asc') ? 'desc' : 'asc';
$qs = ['sort' => $col, 'dir' => $newDir];
if ($filterSev !== '') { $qs['severity'] = $filterSev; }
if ($filterQ !== '') { $qs['q'] = $filterQ; }
return '/cve?' . http_build_query($qs);
};
$sevClass = fn($s) => match(strtoupper($s)) {
'CRITICAL' => 'status-pill--danger', 'HIGH' => 'status-pill--orange',
'MEDIUM' => 'status-pill--warning', 'LOW' => 'status-pill--info', default => 'status-pill--neutral',
};
$sevLabel = fn($s) => match(strtoupper($s)) {
'CRITICAL' => 'Kritisch', 'HIGH' => 'Hoch', 'MEDIUM' => 'Mittel', 'LOW' => 'Niedrig',
default => strtoupper($s ?: 'Unbekannt'),
};
$nvdSearch = 'https://nvd.nist.gov/vuln/search/results?form_type=Basic&results_type=overview&search_type=all&isCpeNameSearch=false';
?>
<div class="cve-outer">
<!-- Content Header -->
<div class="content-header">
<div>
<h1><?= $this->te('CVE-Sicherheitsmeldungen') ?></h1>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
<li class="breadcrumb-item active"><?= $this->te('CVE-Meldungen') ?></li>
</ol>
</div>
<div class="content-header-actions">
<?php if ((int)($stats['CRITICAL'] ?? 0) > 0): ?>
<span class="status-pill status-pill--danger">🚨 <?= $this->te('{anzahl} kritisch', ['anzahl' => (int)$stats['CRITICAL']]) ?></span>
<?php endif; ?>
<?php if ($lastRefresh): ?>
<span style="font-size:.78rem;color:var(--gray-400);"><?= $this->te('Aktualisiert:') ?> <?= $this->te('{zeitpunkt} Uhr', ['zeitpunkt' => date('d.m.Y H:i', strtotime($lastRefresh))]) ?></span>
<?php endif; ?>
<?php if ($canManage): ?>
<button class="btn btn-default" onclick="document.getElementById('cveKeywordModal').classList.add('open')">
<?= $this->t('Keywords verwalten') ?>
<?php if ($hasKeywords): ?>
<span style="background:var(--color-primary);color:var(--color-on-accent);border-radius:9999px;padding:.05rem .45rem;font-size:.72rem;margin-left:.25rem;"><?= count($keywords) ?></span>
<?php endif; ?>
</button>
<button class="btn btn-default" id="refreshBtn" onclick="doCveRefresh()" <?= $hasKeywords ? '' : 'disabled title="Erst Keywords anlegen"' ?>>
<span id="refreshIcon">↻</span> <?= $this->te('Aktualisieren') ?>
</button>
<?php endif; ?>
</div>
</div>
<div class="page-body">
<span id="cveRefreshMsg" style="display:none;"></span>
<?php if ($bsiEnabled && $hasKeywords): ?>
<div class="cve-sources-note">
<?= $this->t('<span class="cve-sources-icon">📚</span> <span> <strong>Zwei Quellen, eine Sicht:</strong> Oben die technischen <strong>NVD/CVE</strong>-Einträge zu deinen überwachten Produkten (Quelle der Wahrheit, englisch). Darunter die kuratierten deutschen <strong>BSI CERT-Bund</strong>-Warnungen dazu. Eine CVE mit deutscher Warnung trägt ein <span class="cve-bsi-badge" style="position:static;">🇩🇪 BSI</span>-Badge – Klick führt zur Einordnung. </span>') ?>
</div>
<?php endif; ?>
<?php if ($cacheStale && $hasKeywords): ?>
<div class="alert alert-warning" style="margin-bottom:1.25rem;display:flex;align-items:flex-start;gap:.75rem;">
<span style="font-size:1.1rem;">⚠</span>
<span>
<?= $this->t('<strong>Cache veraltet</strong> – Die CVE-Daten wurden seit mehr als 6 Stunden nicht aktualisiert.') ?>
<?php if ($canManage): ?>
<?= $this->t('Klicke auf <strong>Aktualisieren</strong>, oder aktiviere den geplanten Task <strong>„CVE-Cache aktualisieren"</strong> unter <a href="/admin/cronjobs">Einstellungen → Cronjobs</a> (läuft über den zentralen Scheduler).') ?>
<?php else: ?>
<?= $this->te('Bitte einen Administrator bitten, den Cache zu aktualisieren.') ?>
<?php endif; ?>
</span>
</div>
<?php endif; ?>
<!-- KPI small-boxes (Watchlist) -->
<?php if ($hasKeywords): ?>
<div class="kpi-row">
<div class="kpi-col">
<a href="/cve?severity=CRITICAL">
<div class="small-box bg-danger">
<div class="inner"><h3><?= (int)$stats['CRITICAL'] ?></h3><p><?= $this->te('Kritisch') ?></p></div>
<div class="icon"><i class="fas fa-exclamation-circle"></i></div>
</div>
</a>
</div>
<div class="kpi-col">
<a href="/cve?severity=HIGH">
<div class="small-box bg-warning">
<div class="inner"><h3><?= (int)$stats['HIGH'] ?></h3><p><?= $this->te('Hoch') ?></p></div>
<div class="icon"><i class="fas fa-exclamation-triangle"></i></div>
</div>
</a>
</div>
<div class="kpi-col">
<a href="/cve?severity=MEDIUM">
<div class="small-box bg-info">
<div class="inner"><h3><?= (int)$stats['MEDIUM'] ?></h3><p><?= $this->te('Mittel') ?></p></div>
<div class="icon"><i class="fas fa-info-circle"></i></div>
</div>
</a>
</div>
<div class="kpi-col">
<a href="/cve">
<div class="small-box bg-secondary">
<div class="inner"><h3><?= (int)$stats['total'] ?></h3><p><?= $this->te('Watchlist gesamt') ?></p></div>
<div class="icon"><i class="fas fa-shield-alt"></i></div>
<span class="small-box-footer"><?= $this->te('Alle anzeigen') ?></span>
</div>
</a>
</div>
</div>
<?php endif; ?>
<?php if (!$hasKeywords): ?>
<!-- Kein Keyword: CTA + NVD-Launchpad -->
<div class="empty-state">
<span class="empty-icon">🔍</span>
<strong><?= $this->te('Keine Keywords konfiguriert') ?></strong><br>
<span style="font-size:.88rem;">
<?= $this->te('Dieses Modul überwacht gezielt deine Produkte – es spiegelt nicht die komplette NVD-Datenbank.') ?>
<?php if ($canManage): ?><?= $this->t('Lege Keywords an (z. B. <em>nginx</em>, <em>fortigate</em>, <em>exchange</em>), um relevante CVEs zu sammeln.') ?><?php endif; ?>
</span>
<?php if ($canManage): ?>
<div style="margin-top:1.1rem;">
<button class="btn btn-primary" onclick="document.getElementById('cveKeywordModal').classList.add('open')"><?= $this->te('Keywords anlegen') ?></button>
</div>
<?php endif; ?>
</div>
<?php else: ?>
<!-- Filterleiste (Styleguide § 8) -->
<div class="card card-outline card-primary mb-3">
<div class="filter-card-body cve-filterbar">
<?php
$sevFilters = ['' => 'Alle', 'CRITICAL' => '🚨 Kritisch', 'HIGH' => '⚠ Hoch', 'MEDIUM' => 'ⓘ Mittel', 'LOW' => 'Niedrig'];
$sevBtnClass = ['' => 'btn-primary', 'CRITICAL' => 'btn-danger', 'HIGH' => 'btn-warning', 'MEDIUM' => 'btn-info', 'LOW' => 'btn-default'];
foreach ($sevFilters as $val => $label):
$cls = ($filterSev === $val) ? ($sevBtnClass[$val] ?? 'btn-default') : 'btn-default';
?>
<a href="/cve?severity=<?= urlencode($val) ?>&q=<?= urlencode($filterQ) ?>" class="btn btn-sm <?= $cls ?>"><?= $label ?></a>
<?php endforeach; ?>
<form method="get" action="/cve" class="filter-input-wrap" style="max-width:340px;">
<input type="hidden" name="severity" value="<?= $this->e($filterSev) ?>">
<svg class="filter-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input type="search" name="q" class="form-control form-control-sm"
value="<?= $this->e($filterQ) ?>" placeholder="<?= $this->te('In Watchlist suchen…') ?>"
oninput="this.form.submit()" autocomplete="off">
</form>
<span class="cve-kw-list">
<?php foreach ($keywords as $kw): ?>
<span class="cve-kw-tag"><?= $this->e($kw['keyword']) ?></span>
<?php endforeach; ?>
</span>
</div>
</div>
<!-- Watchlist-Tabelle -->
<?php if (!empty($keywordCves)): ?>
<?php $tableCves = $keywordCves; $bsiByCveMap = $bsiByCve; include __DIR__ . '/_cve_table.php'; ?>
<?php
$firstRow = ($cvePage - 1) * $cvePerPage + 1;
$lastRow = min($cveTotal, $cvePage * $cvePerPage);
?>
<div class="cve-pager">
<span class="cve-pager-info"><?= $this->te('{von}–{bis} von {gesamt} Meldungen', ['von' => $firstRow, 'bis' => $lastRow, 'gesamt' => $cveTotal]) ?></span>
<?php if ($cvePages > 1): ?>
<div class="pagination">
<a class="pagination-link <?= $cvePage <= 1 ? 'disabled' : '' ?>" href="<?= $cvePage <= 1 ? '#' : $this->e($pagerQuery($cvePage - 1)) ?>"><?= $this->te('« Zurück') ?></a>
<span class="pagination-link active"><?= $this->te('Seite {seite} / {gesamt}', ['seite' => $cvePage, 'gesamt' => $cvePages]) ?></span>
<a class="pagination-link <?= $cvePage >= $cvePages ? 'disabled' : '' ?>" href="<?= $cvePage >= $cvePages ? '#' : $this->e($pagerQuery($cvePage + 1)) ?>"><?= $this->te('Weiter »') ?></a>
</div>
<?php endif; ?>
</div>
<?php else: ?>
<div class="empty-state">
<span class="empty-icon">🔎</span>
<strong><?= ($filterSev !== '' || $filterQ !== '') ? 'Keine Treffer für den Filter' : 'Noch keine CVEs im Cache' ?></strong><br>
<span style="font-size:.85rem;">
<?php if ($filterSev !== '' || $filterQ !== ''): ?>
<a href="/cve"><?= $this->te('Filter zurücksetzen') ?></a>
<?php else: ?>
<?= $this->te('Klicke auf „Aktualisieren", um die Keywords bei der NVD abzufragen.') ?>
<?php endif; ?>
</span>
</div>
<?php endif; ?>
<?php endif; ?>
<?php if ($bsiEnabled): ?>
<!-- BSI CERT-Bund Warnungen – mit der Watchlist verknüpft -->
<section class="detail-card" style="margin-top:1.5rem;">
<div class="detail-card-header">
<h2 class="detail-card-title"><?= $this->te('🇩🇪 BSI CERT-Bund – deutschsprachige Warnungen zu deiner Watchlist') ?></h2>
</div>
<div class="detail-card-body">
<p style="margin:0 0 1rem;font-size:.83rem;color:var(--gray-500);line-height:1.55;">
<?= $this->te('Hier siehst du die deutschsprachigen CERT-Bund-Warnungen, die eine deiner überwachten CVEs referenzieren oder ein Watchlist-Stichwort treffen – als verständliche Einordnung und Handlungshilfe zu den technischen NVD-Einträgen oben.') ?>
</p>
<?php if (empty($bsiRelevant)): ?>
<div class="empty-state" style="padding:1.25rem;">
<span style="font-size:.88rem;color:var(--gray-500);">
<?= $filterQ !== ''
? $this->te('Aktuell keine CERT-Bund-Warnung mit Bezug zu deiner Watchlist für den Filter.')
: $this->te('Aktuell keine CERT-Bund-Warnung mit Bezug zu deiner Watchlist.') ?>
<?php if (!empty($bsiOther)): ?><?= $this->te('Weiter unten findest du die aktuellsten Warnungen allgemein.') ?><?php endif; ?>
</span>
</div>
<?php else: ?>
<?php $bsiRows = $bsiRelevant; include __DIR__ . '/_bsi_table.php'; ?>
<?php endif; ?>
<?php if (!empty($bsiOther)): ?>
<details style="margin-top:1rem;">
<summary style="cursor:pointer;font-size:.85rem;color:var(--blue-700);font-weight:600;">
<?= $this->te('Weitere aktuelle CERT-Bund-Warnungen anzeigen ({anzahl})', ['anzahl' => count($bsiOther)]) ?>
</summary>
<div style="margin-top:.75rem;">
<?php $bsiRows = $bsiOther; include __DIR__ . '/_bsi_table.php'; ?>
</div>
</details>
<?php endif; ?>
</div>
</section>
<?php endif; ?>
<!-- NVD-Launchpad: alles Übrige direkt bei der Quelle -->
<div class="cve-nvd-card">
<h3><?= $this->te('↗ Weitere CVEs direkt bei der NVD suchen') ?></h3>
<p><?= $this->te('Für alles außerhalb deiner Watchlist springst du am besten direkt zur offiziellen NVD-Datenbank – so bleibt diese Anwendung schlank.') ?></p>
<div class="cve-nvd-row">
<a class="btn btn-sm btn-default cve-nvd-crit" href="https://nvd.nist.gov/vuln/search/results?cvss_version=3&cvss_v3_severity=CRITICAL" target="_blank" rel="noopener"><?= $this->te('🚨 Kritische CVEs') ?></a>
<a class="btn btn-sm btn-default cve-nvd-high" href="https://nvd.nist.gov/vuln/search/results?cvss_version=3&cvss_v3_severity=HIGH" target="_blank" rel="noopener"><?= $this->te('⚠ Hohe CVEs') ?></a>
<a class="btn btn-sm btn-default" href="https://nvd.nist.gov/vuln/search" target="_blank" rel="noopener"><?= $this->te('Erweiterte Suche') ?></a>
</div>
<div class="cve-nvd-search">
<input type="search" id="nvdSearchInput" class="form-control form-control-sm" placeholder="<?= $this->te('Begriff bei der NVD suchen…') ?>"
onkeydown="if(event.key==='Enter'){event.preventDefault();nvdSearch();}">
<button type="button" class="btn btn-default btn-sm" onclick="nvdSearch()"><?= $this->te('Bei NVD suchen') ?></button>
</div>
</div>
</div><!-- /page-body -->
</div><!-- /cve-outer -->
<?php if ($canManage): ?>
<!-- Keyword-Modal (zentrale Modal-Komponenten aus app.css) -->
<div class="modal-overlay cve-modal" id="cveKeywordModal">
<div class="modal-card" role="dialog" aria-modal="true" aria-label="<?= $this->te('Keywords verwalten') ?>">
<div class="modal-header">
<h2><?= $this->te('🔍 Keyword-Watchlist verwalten') ?></h2>
<button type="button" class="modal-close" onclick="document.getElementById('cveKeywordModal').classList.remove('open')" title="<?= $this->te('Schließen') ?>">×</button>
</div>
<div class="modal-body">
<p style="font-size:.85rem;color:var(--gray-500);margin-bottom:1rem;">
<?= $this->t('Trage Produktnamen oder Begriffe ein (z. B. <em>nginx</em>, <em>apache</em>, <em>windows</em>). Beim nächsten Refresh werden gezielt diese bei der NVD abgefragt und gespeichert.') ?>
</p>
<?php if ($hasKeywords): ?>
<div style="display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:1.25rem;">
<?php foreach ($keywords as $kw): ?>
<span class="cve-keyword-chip">
<?= $this->e($kw['keyword']) ?>
<form action="/cve/keywords/<?= (int)$kw['id'] ?>" method="POST" style="display:inline;">
<input type="hidden" name="_csrf" value="<?= $csrfToken ?>">
<input type="hidden" name="_method" value="DELETE">
<button type="button" class="cve-keyword-del" title="<?= $this->te('Entfernen') ?>"
onclick="itConfirmSubmit(this, 'Keyword entfernen?', 'Das Keyword „<?= $this->e(addslashes($kw['keyword'])) ?>“ wird aus der Watchlist entfernt.', 'Entfernen')">×</button>
</form>
</span>
<?php endforeach; ?>
</div>
<?php else: ?>
<p style="font-size:.85rem;color:var(--gray-400);margin-bottom:1rem;"><?= $this->te('Noch keine Keywords eingetragen.') ?></p>
<?php endif; ?>
<form action="/cve/keywords" method="POST" style="display:flex;gap:.5rem;">
<input type="hidden" name="_csrf" value="<?= $csrfToken ?>">
<input type="text" name="keyword" class="form-control form-control-sm" placeholder="<?= $this->te('Keyword hinzufügen…') ?>" required autofocus>
<button type="submit" class="btn btn-primary btn-sm"><?= $this->te('Hinzufügen') ?></button>
</form>
</div>
</div>
</div>
<?php endif; ?>
<?= $this->partial('confirm_modal') ?>
<script>
function nvdSearch() {
var v = (document.getElementById('nvdSearchInput').value || '').trim();
var url = v
? 'https://nvd.nist.gov/vuln/search/results?form_type=Basic&results_type=overview&search_type=all&query=' + encodeURIComponent(v)
: 'https://nvd.nist.gov/vuln/search';
window.open(url, '_blank', 'noopener');
}
var CVE_BSI_ENABLED = <?= $bsiEnabled ? 'true' : 'false' ?>;
function doCveRefresh() {
var btn = document.getElementById('refreshBtn');
var icon = document.getElementById('refreshIcon');
var msg = document.getElementById('cveRefreshMsg');
if (!btn || btn.disabled) return;
btn.disabled = true;
icon.innerHTML = '<span class="cve-spin">↻</span>';
if (msg) msg.style.display = 'none';
// Der NVD-Abruf kann je nach Watchlist/Rate-Limit lange dauern. Da die
// BSI-Warnungen serverseitig ZUERST aktualisiert werden, laden wir nach
// einem Timeout trotzdem neu, damit die deutschen Meldungen erscheinen –
// der NVD-Abruf läuft serverseitig weiter.
var ctrl = new AbortController();
var timedOut = false;
var timer = setTimeout(function () { timedOut = true; ctrl.abort(); }, 25000);
fetch('/cve/refresh', {
method: 'POST',
signal: ctrl.signal,
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content,
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(r => r.json())
.then(data => {
clearTimeout(timer);
if (data.ok) { window.location.reload(); }
else if (data.bsi_count !== null && data.bsi_count !== undefined) {
// NVD fehlgeschlagen, aber BSI aktualisiert – neu laden zeigt die Warnungen.
window.location.reload();
} else { showRefreshError(data.error || 'Unbekannter Fehler'); }
})
.catch(() => {
clearTimeout(timer);
if (timedOut && CVE_BSI_ENABLED) {
// BSI ist bereits aktualisiert; NVD läuft im Hintergrund weiter.
showRefreshInfo('Die BSI-Warnungen wurden aktualisiert. Der NVD-Abruf läuft im Hintergrund weiter – die Seite wird neu geladen…');
setTimeout(function () { window.location.reload(); }, 1800);
} else if (timedOut) {
showRefreshInfo('Der NVD-Abruf dauert länger und läuft im Hintergrund weiter. Lade die Seite in einigen Minuten erneut.');
btn.disabled = false; icon.textContent = '⟳';
} else {
showRefreshError('Verbindungsfehler');
}
});
function showRefreshError(text) {
btn.disabled = false; icon.textContent = '⟳';
if (!msg) return;
msg.textContent = 'Aktualisierung fehlgeschlagen: ' + text;
msg.style.cssText = 'display:block;margin-bottom:1rem;padding:.6rem 1rem;background:#fef2f2;border:1px solid #fca5a5;border-radius:.4rem;color:#dc2626;font-size:.83rem;';
}
function showRefreshInfo(text) {
if (!msg) return;
msg.textContent = text;
msg.style.cssText = 'display:block;margin-bottom:1rem;padding:.6rem 1rem;background:#eff6ff;border:1px solid #bfdbfe;border-radius:.4rem;color:#1d4ed8;font-size:.83rem;';
}
}
// Keyword-Modal per Backdrop-Klick schließen
document.getElementById('cveKeywordModal')?.addEventListener('click', function(e) {
if (e.target === this) this.classList.remove('open');
});
// Detail-Aufklapp per Zeilen-Klick
document.addEventListener('click', function(e) {
var row = e.target.closest('tr[data-cve]');
if (!row) return;
var id = row.dataset.cve;
var det = document.getElementById('cve-det-' + id);
if (!det) return;
var isOpen = det.classList.contains('open');
document.querySelectorAll('.cve-detail-row.open').forEach(function(el) {
el.classList.remove('open');
el.previousElementSibling?.classList.remove('cve-open');
});
if (!isOpen) { det.classList.add('open'); row.classList.add('cve-open'); }
});
</script>