first commit
This commit is contained in:
Executable
+388
@@ -0,0 +1,388 @@
|
||||
/**
|
||||
* 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 = `<span style="color:#50fa7b">nixiews@nicoleta</span><span style="color:#f8f8f2">:</span><span style="color:#bd93f9">~</span><span style="color:#f8f8f2">$</span> ${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 =>
|
||||
`<div class="status-motd">💬 ${m}</div>`
|
||||
).join('');
|
||||
|
||||
return `
|
||||
<div class="status-header">
|
||||
<span class="status-badge" style="color:${data.couleur};border-color:${data.couleur}20;background:${data.couleur}15">
|
||||
● ${data.status}
|
||||
</span>
|
||||
${data.date ? `<span class="status-date">${data.date}</span>` : ''}
|
||||
</div>
|
||||
${data.message ? `<p class="status-message">${data.message}</p>` : ''}
|
||||
${motdsHtml ? `<div class="status-motds">${motdsHtml}</div>` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
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 = `
|
||||
<h2>Infos du jour</h2>
|
||||
<div class="news-card">
|
||||
<div class="news-loading">
|
||||
<div class="mc-loading-dot"></div>
|
||||
<div class="mc-loading-dot"></div>
|
||||
<div class="mc-loading-dot"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
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 = `<span class="status-error">⚠ Impossible de charger server.status (${e.message})</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user