diff --git a/logins.txt b/logins.txt
new file mode 100644
index 0000000..217342c
--- /dev/null
+++ b/logins.txt
@@ -0,0 +1,3 @@
+724433
+546528
+
diff --git a/public/app.js b/public/app.js
index b941e96..35f0b27 100644
--- a/public/app.js
+++ b/public/app.js
@@ -23,6 +23,11 @@ function connectWS() {
showAlert(msg.message, msg.html, msg.image);
} else if (msg.type === 'dismiss') {
hideAlert();
+ } else if (msg.type === 'rootme_update') {
+ renderRootme(msg.ranking);
+ } else if (msg.type === 'rootme_flag') {
+ renderRootme(rootmeCache);
+ showNotif(`FLAG ! ${msg.login} +${msg.gained} PTS — TOTAL : ${msg.newScore} PTS`, true);
}
});
@@ -64,7 +69,7 @@ const notifBarInner = document.getElementById('notif-bar-inner');
const softAlarmAudio = new Audio('/soft_alarm.mp3');
let notifTimer = null;
-function showNotif(message) {
+function showNotif(message, sound = true) {
notifMessage.textContent = message;
// Re-déclencher l'animation de la barre
@@ -78,8 +83,10 @@ function showNotif(message) {
notifOverlay.classList.remove('hidden');
- softAlarmAudio.currentTime = 0;
- softAlarmAudio.play().catch(err => console.error('soft_alarm:', err));
+ if (sound) {
+ softAlarmAudio.currentTime = 0;
+ softAlarmAudio.play().catch(err => console.error('soft_alarm:', err));
+ }
if (notifTimer) clearTimeout(notifTimer);
notifTimer = setTimeout(() => {
@@ -250,9 +257,9 @@ async function loadGeo() {
// 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');
+ const newGeoItems = items.filter(i => !seenGeoLinks.has(i.link));
+ if (newGeoItems.length) {
+ showNotif(`Nouvelle actualité géopolitique : ${newGeoItems[0].title}`, false);
seenGeoLinks = currentLinks;
}
}
@@ -279,19 +286,125 @@ async function loadGeo() {
}
}
+// ── Root-me ranking ───────────────────────────────────────────────────────────
+
+const rootmeList = document.getElementById('rootme-list');
+const rootmeStatus = document.getElementById('rootme-status');
+let rootmeCache = [];
+
+function renderRootme(ranking) {
+ if (!ranking) return;
+ rootmeCache = ranking;
+ rootmeList.innerHTML = '';
+
+ if (!ranking.length) {
+ rootmeList.innerHTML = '
Aucun joueur';
+ rootmeStatus.textContent = '—';
+ rootmeStatus.className = 'widget-status';
+ return;
+ }
+
+ const rankClass = i => i === 0 ? 'gold' : i === 1 ? 'silver' : i === 2 ? 'bronze' : '';
+
+ ranking.forEach((user, idx) => {
+ const li = document.createElement('li');
+ li.className = 'rootme-item';
+ li.innerHTML = `
+ #${idx + 1}
+ ${escapeHtml(user.login)}
+ ${user.score.toLocaleString('fr-FR')} pts
+ `;
+ rootmeList.appendChild(li);
+ });
+
+ rootmeStatus.textContent = `${ranking.length} joueur${ranking.length > 1 ? 's' : ''}`;
+ rootmeStatus.className = 'widget-status ok';
+}
+
+async function loadRootme() {
+ rootmeStatus.textContent = '...';
+ rootmeStatus.className = 'widget-status';
+ try {
+ const resp = await fetch('/api/rootme');
+ if (!resp.ok) {
+ const err = await resp.json().catch(() => ({}));
+ throw new Error(err.error || `HTTP ${resp.status}`);
+ }
+ const ranking = await resp.json();
+ renderRootme(ranking);
+ } catch (err) {
+ rootmeStatus.textContent = 'ERREUR';
+ rootmeStatus.className = 'widget-status err';
+ rootmeList.innerHTML = `Erreur : ${escapeHtml(err.message)}`;
+ }
+}
+
+// ── Carousel ──────────────────────────────────────────────────────────────────
+
+const CAROUSEL_PAGES = [
+ ['widget-kaspersky', 'widget-anssi', 'widget-geo', 'widget-placeholder'],
+ ['widget-kaspersky', 'widget-anssi', 'widget-geo', 'widget-clock'],
+];
+const ALL_CAROUSEL_IDS = [...new Set(CAROUSEL_PAGES.flat())];
+const CAROUSEL_FADE_MS = 450;
+let carouselPage = 0;
+
+function applyCarouselInstant() {
+ const visible = new Set(CAROUSEL_PAGES[carouselPage]);
+ ALL_CAROUSEL_IDS.forEach(id => {
+ const el = document.getElementById(id);
+ if (!el) return;
+ el.style.opacity = '';
+ el.style.display = visible.has(id) ? '' : 'none';
+ });
+}
+
+function rotateCarousel() {
+ const prevVisible = new Set(CAROUSEL_PAGES[carouselPage]);
+ carouselPage = (carouselPage + 1) % CAROUSEL_PAGES.length;
+ const nextVisible = new Set(CAROUSEL_PAGES[carouselPage]);
+
+ const outgoing = ALL_CAROUSEL_IDS.filter(id => prevVisible.has(id) && !nextVisible.has(id));
+ const incoming = ALL_CAROUSEL_IDS.filter(id => !prevVisible.has(id) && nextVisible.has(id));
+
+ // Fade out
+ outgoing.forEach(id => { document.getElementById(id).style.opacity = '0'; });
+
+ setTimeout(() => {
+ // Hide outgoing, reveal incoming at opacity 0 then fade in
+ outgoing.forEach(id => {
+ const el = document.getElementById(id);
+ el.style.display = 'none';
+ el.style.opacity = '';
+ });
+ incoming.forEach(id => {
+ const el = document.getElementById(id);
+ el.style.display = '';
+ el.style.opacity = '0';
+ el.getBoundingClientRect(); // force reflow
+ el.style.opacity = '';
+ });
+ }, CAROUSEL_FADE_MS);
+}
+
// ── Init ─────────────────────────────────────────────────────────────────────
function init() {
infoHost.textContent = location.host;
+ applyCarouselInstant();
+ setInterval(rotateCarousel, 5 * 60 * 1000);
+
connectWS();
updateClock();
setInterval(updateClock, 1000);
loadAnssi();
loadGeo();
+ loadRootme();
setInterval(loadAnssi, 5 * 60 * 1000);
setInterval(loadGeo, 5 * 60 * 1000);
+ // Root-me est mis à jour via WebSocket (rootme_update / rootme_flag)
}
// ── Helpers ───────────────────────────────────────────────────────────────────
diff --git a/public/index.html b/public/index.html
index 9f40921..83fccc8 100644
--- a/public/index.html
+++ b/public/index.html
@@ -81,6 +81,19 @@
+
+
+