diff --git a/public/app.js b/public/app.js
index 0679c86..70db9c1 100644
--- a/public/app.js
+++ b/public/app.js
@@ -287,6 +287,78 @@ async function loadGeo() {
}
}
+// ── Calendar ──────────────────────────────────────────────────────────────────
+
+const calList = document.getElementById('cal-list');
+const calStatus = document.getElementById('cal-status');
+let calEvents = [];
+const notifiedCourses = new Set();
+
+const DAY_SHORT = ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'];
+
+function renderCalendar() {
+ calList.innerHTML = '';
+ const now = new Date();
+ const todayStart = new Date(now); todayStart.setHours(0, 0, 0, 0);
+ const todayEnd = new Date(now); todayEnd.setHours(23, 59, 59, 999);
+
+ const todayEvents = calEvents.filter(e => {
+ const s = new Date(e.start);
+ return s >= todayStart && s <= todayEnd;
+ });
+
+ if (!todayEvents.length) {
+ calList.innerHTML = '
Aucun cours aujourd\'hui';
+ return;
+ }
+
+ todayEvents.forEach(event => {
+ const start = new Date(event.start);
+ const end = new Date(event.end);
+ const li = document.createElement('li');
+ li.className = 'cal-item' +
+ (now >= start && now < end ? ' active' : '') +
+ (now >= end ? ' past' : '');
+ const timeStr = `${pad(start.getHours())}:${pad(start.getMinutes())} – ${pad(end.getHours())}:${pad(end.getMinutes())}`;
+ li.innerHTML = `
+ ${escapeHtml(timeStr)}
+ ${escapeHtml(event.title)}
+ ${event.location ? `${escapeHtml(event.location)}` : ''}
+ `;
+ calList.appendChild(li);
+ });
+}
+
+async function loadCalendar() {
+ calStatus.textContent = '...';
+ calStatus.className = 'widget-status';
+ try {
+ const resp = await fetch('/api/calendar');
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
+ calEvents = await resp.json();
+ renderCalendar();
+ calStatus.textContent = `${calEvents.length} cours`;
+ calStatus.className = 'widget-status ok';
+ } catch (err) {
+ calStatus.textContent = 'ERREUR';
+ calStatus.className = 'widget-status err';
+ calList.innerHTML = `Erreur : ${escapeHtml(err.message)}`;
+ }
+}
+
+function checkUpcomingCourses() {
+ const now = new Date();
+ calEvents.forEach(event => {
+ const start = new Date(event.start);
+ const diffMin = (start - now) / 60000;
+ if (diffMin > 0 && diffMin <= 5 && !notifiedCourses.has(event.start)) {
+ notifiedCourses.add(event.start);
+ const loc = event.location ? ` — ${event.location}` : '';
+ showNotif(`COURS DANS ${Math.ceil(diffMin)} MIN : ${event.title}${loc}`, null);
+ }
+ });
+}
+
// ── Root-me ranking ───────────────────────────────────────────────────────────
const rootmeList = document.getElementById('rootme-list');
@@ -343,8 +415,8 @@ async function loadRootme() {
// ── Carousel ──────────────────────────────────────────────────────────────────
const CAROUSEL_PAGES = [
- ['widget-kaspersky', 'widget-anssi', 'widget-geo', 'widget-placeholder'],
- ['widget-kaspersky', 'widget-anssi', 'widget-geo', 'widget-clock'],
+ ['widget-kaspersky', 'widget-calendar', '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;
@@ -403,8 +475,13 @@ function init() {
loadAnssi();
loadGeo();
loadRootme();
- setInterval(loadAnssi, 5 * 60 * 1000);
- setInterval(loadGeo, 5 * 60 * 1000);
+ loadCalendar();
+ setInterval(loadAnssi, 5 * 60 * 1000);
+ setInterval(loadGeo, 5 * 60 * 1000);
+ setInterval(loadCalendar, 30 * 60 * 1000);
+ setInterval(renderCalendar, 60 * 1000); // re-render active/past state chaque minute
+ setInterval(checkUpcomingCourses, 30 * 1000);
+ checkUpcomingCourses();
// Root-me est mis à jour via WebSocket (rootme_update / rootme_flag)
}
diff --git a/public/index.html b/public/index.html
index 83fccc8..697b704 100644
--- a/public/index.html
+++ b/public/index.html
@@ -81,6 +81,19 @@
+
+
+