/** * Sparklines & Easter Egg - Nixiews Dashboard * Ajoute des mini-graphiques dans les stat-cards existantes * + easter egg clavier dans le loader * + lecture du fichier server.status (remplace news.json) */ (function () { 'use strict'; // ========================================================= // CONFIG // ========================================================= const MAX_POINTS = 20; const CANVAS_H = 36; const COLOR_OK = '#50fa7b'; const COLOR_WARN = '#ffb86c'; const COLOR_DANGER = '#ff5555'; const COLOR_LINE = 'rgba(98,114,164,0.35)'; const COLOR_FILL_OK = 'rgba(80,250,123,0.12)'; const TRACKED = [ { id: 'cpu', max: 100, warn: 50, danger: 80, parse: v => parseFloat(v), }, { id: 'ram', max: null, maxId: 'ram-subtitle', warn: 60, danger: 80, parse: (v, sub) => { const m = sub && sub.match(/\((\d+\.?\d*)%\)/); return m ? parseFloat(m[1]) : null; }, }, { id: 'load', max: 8, warn: 50, danger: 80, parse: v => Math.min((parseFloat(v) / 8) * 100, 100), }, ]; const history = {}; TRACKED.forEach(t => { history[t.id] = []; }); // ========================================================= // CANVAS / SPARKLINES // ========================================================= function injectCanvas(statCard) { if (statCard.querySelector('.spark-canvas')) return; const canvas = document.createElement('canvas'); canvas.className = 'spark-canvas'; canvas.width = statCard.offsetWidth || 150; canvas.height = CANVAS_H; canvas.style.cssText = ` display: block; width: 100%; height: ${CANVAS_H}px; margin-top: 0.5rem; border-radius: 6px; opacity: 0.85; `; statCard.appendChild(canvas); return canvas; } function drawSparkline(canvas, data, warn, danger) { if (!canvas || data.length < 2) return; const ctx = canvas.getContext('2d'); const W = canvas.offsetWidth || canvas.width; const H = canvas.height; canvas.width = W; ctx.clearRect(0, 0, W, H); const range = 100; const stepX = W / (MAX_POINTS - 1); const xOf = i => i * stepX; const yOf = v => H - ((v / range) * (H - 4)) - 2; const last = data[data.length - 1]; const color = last > danger ? COLOR_DANGER : last > warn ? COLOR_WARN : COLOR_OK; ctx.beginPath(); ctx.moveTo(xOf(0), H); data.forEach((v, i) => ctx.lineTo(xOf(i), yOf(v))); ctx.lineTo(xOf(data.length - 1), H); ctx.closePath(); ctx.fillStyle = last > danger ? 'rgba(255,85,85,0.10)' : last > warn ? 'rgba(255,184,108,0.10)' : COLOR_FILL_OK; ctx.fill(); ctx.beginPath(); data.forEach((v, i) => { i === 0 ? ctx.moveTo(xOf(i), yOf(v)) : ctx.lineTo(xOf(i), yOf(v)); }); ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.lineJoin = 'round'; ctx.stroke(); const lx = xOf(data.length - 1); const ly = yOf(last); ctx.beginPath(); ctx.arc(lx, ly, 3, 0, Math.PI * 2); ctx.fillStyle = color; ctx.fill(); [50, 80].forEach(pct => { const gy = yOf(pct); ctx.beginPath(); ctx.moveTo(0, gy); ctx.lineTo(W, gy); ctx.strokeStyle = COLOR_LINE; ctx.lineWidth = 0.8; ctx.setLineDash([3, 4]); ctx.stroke(); ctx.setLineDash([]); }); } function sample() { TRACKED.forEach(t => { const el = document.getElementById(t.id); const subEl = document.getElementById(t.id + '-subtitle'); if (!el) return; const raw = el.textContent.trim(); const sub = subEl ? subEl.textContent.trim() : ''; const val = t.parse(raw, sub); if (val === null || isNaN(val)) return; history[t.id].push(val); if (history[t.id].length > MAX_POINTS) history[t.id].shift(); const card = el.closest('.stat-card'); if (!card) return; let canvas = card.querySelector('.spark-canvas'); if (!canvas) canvas = injectCanvas(card); if (!canvas) return; drawSparkline(canvas, history[t.id], t.warn, t.danger); }); } function watchStats() { const section = document.querySelector('.stats-section'); if (!section) return; const obs = new MutationObserver(() => sample()); obs.observe(section, { subtree: true, characterData: true, childList: true }); setTimeout(sample, 6000); } // ========================================================= // EASTER EGG // ========================================================= const EASTER_COMMANDS = { 'sudo': '⚠ [sudo] password for nixiews: \n💀 sudo: permission refusée. T\'es pas root ici.', 'rm -rf /': '💀 SIGTERM — Au revoir cruel monde... \n ... nan je déconne, j\'ai pas les droits.', 'rm -rf': '💀 Haha non. Pas sur mon serveur.', 'emerge': '🟢 emerge: Calcul du dépôt world...\n ETA: 3 jours, 14 heures, 7 minutes.\n (C\'est Gentoo, t\'avais qu\'à pas.)', 'nixos-rebuild': '❓ nixos-rebuild: command not found\n Ici c\'est Gentoo/Debian. Le pseudo c\'est juste un pseudo.', 'pacman': '🔴 erreur: pacman not found. Ici c\'est Gentoo/Debian.', 'apt': '🟡 apt: command not found (on Gentoo side)\n Essaie emerge plutôt.', 'reboot': '♻ Reboot programmé dans... nan, j\'ai changé d\'avis.', 'uname': '🐧 Linux nicoleta 6.x.x-gentoo #1 SMP PREEMPT_DYNAMIC\n x86_64 GNU/Linux', 'htop': '📊 htop: trop stylé pour être lancé dans un easter egg.', 'ls': '📁 . .. index.html data/ secret/ binpkg/ fun/ minecraft/', 'cat /etc/passwd': '😏 root:x:0:0::/root:/bin/bash\n nixiews:x:1000:1000::/home/nixiews:/bin/zsh\n claude:x:9999:9999:meilleur ami:/dev/null:/bin/sh', 'help': '📖 Commandes disponibles:\n sudo, rm -rf /, emerge, nixos-rebuild, pacman, apt,\n reboot, uname, htop, ls, cat /etc/passwd, help\n (Nixiews = pseudo, pas une distro 🙃)', }; let typedBuffer = ''; let eggTimeout = null; function resetBuffer() { typedBuffer = ''; } function checkEasterEgg(key) { const loader = document.getElementById('loader'); if (!loader || loader.style.display === 'none' || loader.style.opacity === '0') return; typedBuffer += key.toLowerCase(); if (typedBuffer.length > 30) typedBuffer = typedBuffer.slice(-30); clearTimeout(eggTimeout); eggTimeout = setTimeout(resetBuffer, 2500); for (const [cmd, response] of Object.entries(EASTER_COMMANDS)) { if (typedBuffer.endsWith(cmd)) { triggerEgg(cmd, response); typedBuffer = ''; break; } } } function triggerEgg(cmd, response) { const logs = document.querySelector('#loader .logs'); if (!logs) return; const cmdLine = document.createElement('div'); cmdLine.style.cssText = ` color: #8be9fd; font-family: 'Courier New', monospace; font-size: 0.85rem; margin-top: 0.3rem; animation: fadeIn 0.2s ease; `; cmdLine.innerHTML = `nixiews@nicoleta:~$ ${cmd}`; logs.appendChild(cmdLine); setTimeout(() => { const responseLine = document.createElement('div'); responseLine.style.cssText = ` color: #f1fa8c; font-family: 'Courier New', monospace; font-size: 0.82rem; white-space: pre-wrap; margin-bottom: 0.3rem; animation: fadeIn 0.3s ease; `; responseLine.textContent = response; logs.appendChild(responseLine); logs.scrollTop = logs.scrollHeight; }, 300); logs.scrollTop = logs.scrollHeight; } // ========================================================= // SERVER STATUS — lecture directe du .status // ========================================================= const STATUS_ENDPOINT = '/data/server.status'; function parseStatusFile(text) { const result = { status: '', couleur: '#bd93f9', date: '', message: '', motds: [] }; const lines = text.split('\n'); for (const line of lines) { const m = line.match(/^\[(\w+)\]\s*(.*)$/); if (!m) continue; const [, key, val] = m; if (key === 'motd') { result.motds.push(val.trim()); } else { result[key] = val.trim(); } } return result; } function buildStatusCard(data) { const motdsHtml = data.motds.map(m => `