/** * 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 => `
💬 ${m}
` ).join(''); return `
● ${data.status} ${data.date ? `${data.date}` : ''}
${data.message ? `

${data.message}

` : ''} ${motdsHtml ? `
${motdsHtml}
` : ''} `; } function injectStatusSection() { const mainContent = document.querySelector('.main-content'); const otherSection = document.querySelector('.other-section'); if (!mainContent) return null; const section = document.createElement('div'); section.className = 'news-section'; section.innerHTML = `

Infos du jour

`; if (otherSection) { mainContent.insertBefore(section, otherSection); } else { mainContent.appendChild(section); } const style = document.createElement('style'); style.textContent = ` .news-section h2 { color: #bd93f9; font-size: 1.5rem; margin-bottom: 1rem; } .news-card { background-color: rgba(68,71,90,0.95); border-radius: 12px; padding: 1.4rem 1.6rem; border-left: 3px solid #bd93f9; transition: box-shadow 0.2s ease; } .news-card:hover { box-shadow: 0 8px 20px rgba(0,0,0,0.4); } .status-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 0.8rem; } .status-badge { font-size: 0.82rem; font-weight: 700; padding: 0.2rem 0.7rem; border-radius: 20px; border: 1px solid; white-space: nowrap; } .status-date { font-size: 0.78rem; color: #6272a4; font-family: 'Courier New', monospace; margin-left: auto; } .status-message { color: #f8f8f2; font-size: 0.95rem; margin-bottom: 0.9rem; line-height: 1.5; } .status-motds { display: flex; flex-direction: column; gap: 0.4rem; } .status-motd { font-size: 0.85rem; color: #6272a4; padding: 0.3rem 0.6rem; background: rgba(40,42,54,0.6); border-radius: 6px; font-family: 'Courier New', monospace; } .news-loading { display: flex; align-items: center; justify-content: center; gap: 0.4rem; padding: 0.6rem 0; } .status-error { color: #ff5555; font-size: 0.85rem; font-family: 'Courier New', monospace; } `; document.head.appendChild(style); return section.querySelector('.news-card'); } async function fetchStatus(card) { try { const res = await fetch(STATUS_ENDPOINT, { cache: 'no-cache' }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const text = await res.text(); card.innerHTML = buildStatusCard(parseStatusFile(text)); } catch (e) { card.innerHTML = `⚠ Impossible de charger server.status (${e.message})`; } } function initStatus() { const card = injectStatusSection(); if (card) fetchStatus(card); } // ========================================================= // INIT // ========================================================= function init() { document.addEventListener('keypress', e => { if (e.key && e.key.length === 1) checkEasterEgg(e.key); }); document.addEventListener('keydown', e => { if (e.key === 'Enter') checkEasterEgg(' '); }); if (document.querySelector('.stats-section')) { watchStats(); } else { document.addEventListener('DOMContentLoaded', watchStats); } initStatus(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();