/** * Kanbante — Wallboard: rotation, manual navigation, clock, live. * * One slide per active project, rotating every 30 s. The viewer can * take over at any time: ←/→ keys (or the on-screen ❮ ❯ buttons and * dots) switch projects immediately and pause the rotation for * 90 s; Space toggles a hard pause. The current position survives * the live/safety reloads via sessionStorage, so the wall no longer * appears stuck on the first project after every refresh. */ (function () { var slides = Array.from(document.querySelectorAll('.wall-slide')); var ROTATE_MS = 30000; var MANUAL_PAUSE_MS = 90000; var STORAGE_KEY = 'kanbante-wall-index'; var current = 0; var paused = false; var pausedUntil = 0; var pausedBadge = document.getElementById('wall-paused'); var dotsBox = document.getElementById('wall-dots'); function updateBadge() { if (pausedBadge) { pausedBadge.hidden = !(paused || Date.now() < pausedUntil); } } function updateDots() { if (!dotsBox) return; Array.from(dotsBox.children).forEach(function (dot, i) { dot.classList.toggle('on', i === current); }); } function show(index, manual) { if (!slides.length) return; slides[current].classList.remove('active'); current = ((index % slides.length) + slides.length) % slides.length; slides[current].classList.add('active'); try { sessionStorage.setItem(STORAGE_KEY, String(current)); } catch (e) { /* private mode */ } updateDots(); if (manual) { pausedUntil = Date.now() + MANUAL_PAUSE_MS; updateBadge(); } } // Restore the position kept across reloads. if (slides.length > 1) { var saved = 0; try { saved = parseInt(sessionStorage.getItem(STORAGE_KEY) || '0', 10) || 0; } catch (e) { saved = 0; } if (saved) show(saved, false); // Dots (one per project, clickable). if (dotsBox) { slides.forEach(function (_slide, i) { var dot = document.createElement('button'); dot.type = 'button'; dot.className = 'wall-dot'; dot.title = 'Projet ' + (i + 1); dot.setAttribute('aria-label', 'Afficher le projet ' + (i + 1)); dot.addEventListener('click', function () { show(i, true); }); dotsBox.appendChild(dot); }); updateDots(); } var prev = document.getElementById('wall-prev'); var next = document.getElementById('wall-next'); if (prev) prev.addEventListener('click', function () { show(current - 1, true); }); if (next) next.addEventListener('click', function () { show(current + 1, true); }); document.addEventListener('keydown', function (e) { if (e.key === 'ArrowRight') { e.preventDefault(); show(current + 1, true); } else if (e.key === 'ArrowLeft') { e.preventDefault(); show(current - 1, true); } else if (e.key === ' ') { e.preventDefault(); paused = !paused; if (!paused) pausedUntil = 0; updateBadge(); } }); setInterval(function () { if (!paused && Date.now() >= pausedUntil) show(current + 1, false); }, ROTATE_MS); setInterval(updateBadge, 5000); } function tickClock() { var now = new Date(); var time = now.toLocaleTimeString('fr-CA', { hour: '2-digit', minute: '2-digit' }); var date = now.toLocaleDateString('fr-CA', { weekday: 'long', day: 'numeric', month: 'long', }); document.querySelectorAll('[data-clock]').forEach(function (el) { el.textContent = time; }); document.querySelectorAll('[data-date]').forEach(function (el) { el.textContent = date; }); } tickClock(); setInterval(tickClock, 15000); // Live: board activity reloads the wall. The hub emits NAMED // "gitea" events — onmessage would never fire (fixed in 0.11.0). try { var source = new EventSource('/api/stream'); source.addEventListener('gitea', function () { location.reload(); }); } catch (err) { console.warn('Wallboard live stream unavailable:', err); } // Safety net. setTimeout(function () { location.reload(); }, 5 * 60 * 1000); })();