timetable: init
This commit is contained in:
parent
fbc6dbf5c4
commit
62a2597a84
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
74
server.js
74
server.js
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue