dashboard/public/app.js

276 lines
9.4 KiB
JavaScript

'use strict';
// ── WebSocket ────────────────────────────────────────────────────────────────
let ws;
let wsReconnectTimer;
function connectWS() {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(`${proto}//${location.host}`);
ws.addEventListener('open', () => {
setWsStatus('CONNECTÉ', 'ok');
updateInfoWS('CONNECTÉ');
if (wsReconnectTimer) { clearTimeout(wsReconnectTimer); wsReconnectTimer = null; }
});
ws.addEventListener('message', evt => {
let msg;
try { msg = JSON.parse(evt.data); } catch { return; }
if (msg.type === 'alert') {
showAlert(msg.message, msg.html, msg.image);
} else if (msg.type === 'dismiss') {
hideAlert();
}
});
ws.addEventListener('close', () => {
setWsStatus('DÉCONNECTÉ', 'err');
updateInfoWS('DÉCONNECTÉ');
wsReconnectTimer = setTimeout(connectWS, 3000);
});
ws.addEventListener('error', () => {
ws.close();
});
}
function setWsStatus(text, cls) {
const el = document.getElementById('ws-status');
el.textContent = text;
el.className = 'widget-status ' + (cls || '');
}
function updateInfoWS(text) {
document.getElementById('info-ws').textContent = text;
}
// ── Alert overlay ────────────────────────────────────────────────────────────
const overlay = document.getElementById('alert-overlay');
const alertMessageEl = document.getElementById('alert-message');
const alertHtmlEl = document.getElementById('alert-html');
const alertIconEl = document.getElementById('alert-icon');
const alertImageEl = document.getElementById('alert-image');
const infoAlert = document.getElementById('info-alert');
// ── Alarm sound ───────────────────────────────────────────────────────────────
const alarmAudio = new Audio('/alert.mp3');
function playAlertSound() {
alarmAudio.currentTime = 0;
alarmAudio.play().catch(err => console.error('Audio play failed:', err));
}
function stopAlertSound() {
alarmAudio.pause();
alarmAudio.currentTime = 0;
}
// ── Alert overlay ─────────────────────────────────────────────────────────────
let autoDismissTimer = null;
function showAlert(message, html, image) {
// Image
if (image && image.trim()) {
alertImageEl.innerHTML = `<img src="${escapeAttr(image)}" alt="">`;
alertIconEl.style.display = 'none';
} else {
alertImageEl.innerHTML = '';
alertIconEl.style.display = '';
}
// Text / HTML
alertMessageEl.textContent = message || '';
alertMessageEl.style.display = (message && !image) ? '' : (message ? '' : 'none');
alertHtmlEl.innerHTML = (html && html.trim()) ? html : '';
// Show overlay
overlay.classList.remove('hidden');
overlay.style.animation = 'none';
void overlay.offsetWidth;
overlay.style.animation = '';
infoAlert.textContent = 'ACTIVE';
infoAlert.style.color = 'var(--red)';
playAlertSound();
if (autoDismissTimer) clearTimeout(autoDismissTimer);
autoDismissTimer = setTimeout(hideAlert, 60_000);
}
function hideAlert() {
if (autoDismissTimer) { clearTimeout(autoDismissTimer); autoDismissTimer = null; }
overlay.classList.add('hidden');
stopAlertSound();
infoAlert.textContent = 'INACTIVE';
infoAlert.style.color = '';
}
// ── Clock ────────────────────────────────────────────────────────────────────
const clockTime = document.getElementById('clock-time');
const clockDate = document.getElementById('clock-date');
const headerClock = document.getElementById('header-clock');
const infoHost = document.getElementById('info-host');
const DAYS = ['Dimanche','Lundi','Mardi','Mercredi','Jeudi','Vendredi','Samedi'];
const MONTHS = ['Jan','Fév','Mar','Avr','Mai','Jun','Jul','Aoû','Sep','Oct','Nov','Déc'];
function pad(n) { return String(n).padStart(2, '0'); }
function updateClock() {
const now = new Date();
const h = pad(now.getHours());
const m = pad(now.getMinutes());
const s = pad(now.getSeconds());
const timeStr = `${h}:${m}:${s}`;
const dateStr = `${DAYS[now.getDay()]} ${pad(now.getDate())} ${MONTHS[now.getMonth()]} ${now.getFullYear()}`;
clockTime.textContent = timeStr;
clockDate.textContent = dateStr;
headerClock.textContent = `${dateStr} ${timeStr}`;
}
// ── ANSSI / CERT-FR feed ─────────────────────────────────────────────────────
const anssiList = document.getElementById('anssi-list');
const anssiStatus = document.getElementById('anssi-status');
async function loadAnssi() {
anssiStatus.textContent = '...';
anssiStatus.className = 'widget-status';
try {
const resp = await fetch('/api/feeds/anssi');
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const items = await resp.json();
anssiList.innerHTML = '';
if (!items.length) {
anssiList.innerHTML = '<li class="feed-loading">Aucun bulletin</li>';
return;
}
items.forEach(item => {
const li = document.createElement('li');
li.className = 'anssi-item';
const date = item.pubDate
? new Date(item.pubDate).toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' })
: '';
li.innerHTML = `
<a href="${escapeAttr(item.link)}" target="_blank" rel="noopener noreferrer">${escapeHtml(item.title)}</a>
${date ? `<span class="anssi-date">${escapeHtml(date)}</span>` : ''}
`;
anssiList.appendChild(li);
});
anssiStatus.textContent = `${items.length} bulletins`;
anssiStatus.className = 'widget-status ok';
} catch (err) {
anssiStatus.textContent = 'ERREUR';
anssiStatus.className = 'widget-status err';
anssiList.innerHTML = `<li class="feed-loading">Erreur : ${escapeHtml(err.message)}</li>`;
}
}
// ── CVE feed ──────────────────────────────────────────────────────────────────
const cveList = document.getElementById('cve-list');
const cveStatus = document.getElementById('cve-status');
function severityClass(cvss) {
if (cvss === undefined || cvss === null) return 'unknown';
const score = parseFloat(cvss);
if (isNaN(score)) return 'unknown';
if (score >= 9.0) return 'critical';
if (score >= 7.0) return 'high';
if (score >= 4.0) return 'medium';
return 'low';
}
function severityLabel(cvss) {
const cls = severityClass(cvss);
const map = { critical: 'CRITIQUE', high: 'ÉLEVÉ', medium: 'MOYEN', low: 'FAIBLE', unknown: '???' };
return map[cls];
}
async function loadCve() {
cveStatus.textContent = '...';
cveStatus.className = 'widget-status';
try {
const resp = await fetch('/api/feeds/cve');
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const items = await resp.json();
cveList.innerHTML = '';
if (!items.length) {
cveList.innerHTML = '<li class="feed-loading">Aucune CVE</li>';
return;
}
items.forEach(item => {
const cvss = item.cvss ?? item['cvss-score'] ?? null;
const cls = severityClass(cvss);
const label = severityLabel(cvss);
const desc = item.summary || item.description || '';
const cveId = item.id || item['cve-id'] || 'CVE-????-????';
const li = document.createElement('li');
li.className = 'cve-item';
li.innerHTML = `
<span class="cve-badge badge-${cls}">${escapeHtml(label)}</span>
<div class="cve-info">
<div class="cve-id">${escapeHtml(cveId)}</div>
<div class="cve-desc" title="${escapeAttr(desc)}">${escapeHtml(desc)}</div>
</div>
`;
cveList.appendChild(li);
});
cveStatus.textContent = `${items.length} CVEs`;
cveStatus.className = 'widget-status ok';
} catch (err) {
cveStatus.textContent = 'ERREUR';
cveStatus.className = 'widget-status err';
cveList.innerHTML = `<li class="feed-loading">Erreur : ${escapeHtml(err.message)}</li>`;
}
}
// ── Init ─────────────────────────────────────────────────────────────────────
function init() {
infoHost.textContent = location.host;
connectWS();
updateClock();
setInterval(updateClock, 1000);
loadAnssi();
loadCve();
setInterval(loadAnssi, 5 * 60 * 1000);
setInterval(loadCve, 5 * 60 * 1000);
}
// ── Helpers ───────────────────────────────────────────────────────────────────
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function escapeAttr(str) {
return String(str).replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
document.addEventListener('DOMContentLoaded', init);