'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'); // ── Notification (ANSSI / geo) ──────────────────────────────────────────────── const notifOverlay = document.getElementById('notif-overlay'); const notifMessage = document.getElementById('notif-message'); const notifBarInner = document.getElementById('notif-bar-inner'); const softAlarmAudio = new Audio('/soft_alarm.mp3'); let notifTimer = null; function showNotif(message) { notifMessage.textContent = message; // Re-déclencher l'animation de la barre notifBarInner.style.animation = 'none'; void notifBarInner.offsetWidth; notifBarInner.style.animation = ''; notifOverlay.style.animation = 'none'; void notifOverlay.offsetWidth; notifOverlay.style.animation = ''; notifOverlay.classList.remove('hidden'); softAlarmAudio.currentTime = 0; softAlarmAudio.play().catch(err => console.error('soft_alarm:', err)); if (notifTimer) clearTimeout(notifTimer); notifTimer = setTimeout(() => { notifOverlay.classList.add('hidden'); notifTimer = null; }, 10_000); } // ── 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 = ``; 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'); let seenAnssiLinks = null; 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 = '
  • Aucun bulletin
  • '; return; } const currentAnssiLinks = new Set(items.map(i => i.link)); if (seenAnssiLinks === null) { seenAnssiLinks = currentAnssiLinks; } else { const newItems = items.filter(i => !seenAnssiLinks.has(i.link)); if (newItems.length) { showNotif(`Nouveau bulletin ANSSI : ${newItems[0].title}`); seenAnssiLinks = currentAnssiLinks; } } 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 = ` ${escapeHtml(item.title)} ${date ? `${escapeHtml(date)}` : ''} `; 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 = `
  • Erreur : ${escapeHtml(err.message)}
  • `; } } // ── Géopolitique feed ───────────────────────────────────────────────────────── const geoList = document.getElementById('geo-list'); const geoStatus = document.getElementById('geo-status'); let seenGeoLinks = null; async function loadGeo() { geoStatus.textContent = '...'; geoStatus.className = 'widget-status'; try { const resp = await fetch('/api/feeds/geo'); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const items = await resp.json(); geoList.innerHTML = ''; if (!items.length) { geoList.innerHTML = '
  • Aucune actualité
  • '; return; } const currentLinks = new Set(items.map(i => i.link)); if (seenGeoLinks === null) { // Premier chargement : on mémorise sans jouer seenGeoLinks = currentLinks; } else { const hasNew = items.some(i => !seenGeoLinks.has(i.link)); if (hasNew) { showNotif('Nouvelle actualité géopolitique'); seenGeoLinks = currentLinks; } } 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', hour: '2-digit', minute: '2-digit' }) : ''; li.innerHTML = ` ${escapeHtml(item.title)} ${date ? `${escapeHtml(item.source ? item.source + ' · ' : '')}${escapeHtml(date)}` : ''} `; geoList.appendChild(li); }); geoStatus.textContent = `${items.length} articles`; geoStatus.className = 'widget-status ok'; } catch (err) { geoStatus.textContent = 'ERREUR'; geoStatus.className = 'widget-status err'; geoList.innerHTML = `
  • Erreur : ${escapeHtml(err.message)}
  • `; } } // ── Init ───────────────────────────────────────────────────────────────────── function init() { infoHost.textContent = location.host; connectWS(); updateClock(); setInterval(updateClock, 1000); loadAnssi(); loadGeo(); setInterval(loadAnssi, 5 * 60 * 1000); setInterval(loadGeo, 5 * 60 * 1000); } // ── Helpers ─────────────────────────────────────────────────────────────────── function escapeHtml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function escapeAttr(str) { return String(str).replace(/"/g, '"').replace(/'/g, '''); } document.addEventListener('DOMContentLoaded', init);