timetable: init

This commit is contained in:
Lopinosaurus 2026-03-12 13:52:24 +01:00
parent fbc6dbf5c4
commit 62a2597a84
4 changed files with 217 additions and 7 deletions

View File

@ -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 = '<li class="feed-loading">Aucun cours aujourd\'hui</li>';
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 = `
<span class="cal-time">${escapeHtml(timeStr)}</span>
<span class="cal-title">${escapeHtml(event.title)}</span>
${event.location ? `<span class="cal-location">${escapeHtml(event.location)}</span>` : ''}
`;
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 = `<li class="feed-loading">Erreur : ${escapeHtml(err.message)}</li>`;
}
}
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)
}

View File

@ -81,6 +81,19 @@
</div>
</section>
<!-- Widget 6: Emploi du temps -->
<section class="widget" id="widget-calendar">
<div class="widget-header">
<span class="widget-title">EMPLOI DU TEMPS</span>
<span class="widget-status" id="cal-status">...</span>
</div>
<div class="widget-body">
<ul id="cal-list" class="feed-list">
<li class="feed-loading">Chargement...</li>
</ul>
</div>
</section>
<!-- Widget 5: Root-me ranking -->
<section class="widget" id="widget-placeholder">
<div class="widget-header">

View File

@ -2,9 +2,9 @@
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0a0a0a;
--bg-widget: #0f0f0f;
--bg-header: #111111;
--bg: #000000;
--bg-widget: #050505;
--bg-header: #080808;
--border: #1a2a1a;
--green: #00ff41;
--green-dim: #00aa2a;
@ -65,6 +65,7 @@ main.grid {
/* ── Carousel grid positions ─────────────────────────────────────────────── */
#widget-kaspersky { grid-column: 1; grid-row: 1; }
#widget-anssi { grid-column: 2; grid-row: 1; }
#widget-calendar { grid-column: 2; grid-row: 1; }
#widget-geo { grid-column: 1; grid-row: 2; }
#widget-clock { grid-column: 2; grid-row: 2; }
#widget-placeholder { grid-column: 2; grid-row: 2; }
@ -80,6 +81,51 @@ main.grid {
transition: opacity 0.45s ease;
}
/* ── Calendar ────────────────────────────────────────────────────────────── */
#cal-list {
overflow: hidden;
justify-content: space-evenly;
}
.cal-item {
border-bottom: 1px solid var(--border);
padding: 0 6px;
display: flex;
flex-direction: column;
justify-content: center;
gap: 3px;
flex: 1;
}
.cal-item.active {
border-left: 2px solid var(--green);
padding-left: 10px;
background: rgba(0, 255, 65, 0.04);
}
.cal-item.past .cal-title,
.cal-item.past .cal-time,
.cal-item.past .cal-location { opacity: 0.35; }
.cal-time {
color: var(--text-dim);
font-size: 13px;
letter-spacing: 1px;
}
.cal-title {
color: var(--green);
font-size: 17px;
font-weight: bold;
line-height: 1.3;
}
.cal-location {
color: var(--green-dim);
font-size: 13px;
letter-spacing: 1px;
}
/* ── Root-me ranking ─────────────────────────────────────────────────────── */
.rootme-item {
display: grid;

View File

@ -145,6 +145,80 @@ app.get('/api/feeds/geo', async (req, res) => {
}
});
// ── ICS / Calendar ──────────────────────────────────────────────────────────
const CALENDAR_URL = 'https://zeus.ionis-it.com/api/group/22/ics/TwBFCoxyY3?startDate=2026-01-01';
const CALENDAR_CACHE_TTL = 30 * 60 * 1000;
let calendarCache = null;
let calendarCacheTime = 0;
function parseICS(text) {
const unfolded = text.replace(/\r\n[ \t]/g, '').replace(/\n[ \t]/g, '');
const lines = unfolded.split(/\r?\n/);
const events = [];
let cur = null;
for (const line of lines) {
if (line === 'BEGIN:VEVENT') { cur = {}; }
else if (line === 'END:VEVENT' && cur) { events.push(cur); cur = null; }
else if (cur) {
const ci = line.indexOf(':');
if (ci === -1) continue;
cur[line.slice(0, ci).split(';')[0]] = line.slice(ci + 1);
}
}
return events;
}
function parseICSDate(str) {
if (!str) return null;
const utc = str.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/);
if (utc) return new Date(`${utc[1]}-${utc[2]}-${utc[3]}T${utc[4]}:${utc[5]}:${utc[6]}Z`);
const loc = str.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})$/);
if (loc) return new Date(`${loc[1]}-${loc[2]}-${loc[3]}T${loc[4]}:${loc[5]}:${loc[6]}`);
const day = str.match(/^(\d{4})(\d{2})(\d{2})$/);
if (day) return new Date(`${day[1]}-${day[2]}-${day[3]}`);
return null;
}
app.get('/api/calendar', async (req, res) => {
if (calendarCache && Date.now() - calendarCacheTime < CALENDAR_CACHE_TTL) {
return res.json(calendarCache);
}
try {
const response = await fetch(CALENDAR_URL, {
headers: { 'User-Agent': 'CyberDashboard/1.0' },
timeout: 10000
});
const raw = parseICS(await response.text());
const now = new Date();
const weekStart = new Date(now);
weekStart.setHours(0, 0, 0, 0);
const d = weekStart.getDay();
weekStart.setDate(weekStart.getDate() - (d === 0 ? 6 : d - 1));
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 7);
const unescape = s => (s || '').replace(/\\,/g, ',').replace(/\\n/g, ' ').replace(/\\;/g, ';').trim();
const events = raw
.map(e => {
const start = parseICSDate(e['DTSTART']);
const end = parseICSDate(e['DTEND']);
if (!start || !end) return null;
return { title: unescape(e['SUMMARY']), location: unescape(e['LOCATION']), start: start.toISOString(), end: end.toISOString() };
})
.filter(e => e && new Date(e.start) >= weekStart && new Date(e.start) < weekEnd)
.sort((a, b) => new Date(a.start) - new Date(b.start));
calendarCache = events;
calendarCacheTime = Date.now();
res.json(events);
} catch (err) {
res.status(502).json({ error: 'Calendar fetch failed', detail: err.message });
}
});
// Root-me ranking
const ROOTME_POLL_MS = 10 * 60 * 1000;
let rootmeCache = null;