init: dark dashboard UI SSE
This commit is contained in:
parent
c638f8eada
commit
498e14fef7
|
|
@ -0,0 +1,217 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Hermes Mission Control</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #0a0a12; --surface: #12121f; --surface2: #1a1a2e;
|
||||||
|
--border: #ffffff0f; --text: #e8e8f0; --muted: #666688;
|
||||||
|
--tt: #3b82f6; --nyora: #22c55e; --perso: #f97316;
|
||||||
|
--green: #22c55e; --red: #ef4444; --yellow: #eab308; --blue: #3b82f6;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { background: var(--bg); color: var(--text); font-family: 'JetBrains Mono', 'Fira Code', monospace; min-height: 100vh; }
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.header { background: var(--surface2); border-bottom: 1px solid var(--border); padding: 14px 20px; display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.header-title { font-size: 13px; font-weight: 800; letter-spacing: 3px; color: var(--text); }
|
||||||
|
.header-sub { font-size: 10px; color: var(--muted); margin-top: 2px; }
|
||||||
|
.live-badge { display: flex; align-items: center; gap: 6px; font-size: 10px; color: var(--muted); }
|
||||||
|
.pulse { width: 7px; height: 7px; border-radius: 50%; background: var(--green); animation: pulse 2s infinite; }
|
||||||
|
@keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.5;transform:scale(1.3)} }
|
||||||
|
|
||||||
|
/* Layout */
|
||||||
|
.container { padding: 16px; max-width: 1200px; margin: 0 auto; }
|
||||||
|
.section-label { font-size: 9px; letter-spacing: 2px; color: var(--muted); font-weight: 700; margin-bottom: 10px; }
|
||||||
|
|
||||||
|
/* Instance cards */
|
||||||
|
.instances { display: grid; grid-template-columns: repeat(3,1fr); gap: 12px; margin-bottom: 20px; }
|
||||||
|
.inst-card { background: var(--surface); border-radius: 10px; padding: 14px; border-left: 3px solid var(--border); transition: border-color .3s; }
|
||||||
|
.inst-card.hermes-tt { border-color: var(--tt); }
|
||||||
|
.inst-card.hermes-nyora { border-color: var(--nyora); }
|
||||||
|
.inst-card.hermes-perso { border-color: var(--perso); }
|
||||||
|
.inst-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px; }
|
||||||
|
.inst-name { font-size: 12px; font-weight: 800; }
|
||||||
|
.inst-status { font-size: 9px; padding: 2px 7px; border-radius: 4px; font-weight: 700; }
|
||||||
|
.status-active { background: #22c55e20; color: #22c55e; border: 1px solid #22c55e40; }
|
||||||
|
.status-idle { background: #ffffff08; color: var(--muted); border: 1px solid var(--border); }
|
||||||
|
.status-bloquee { background: #ef444420; color: #ef4444; border: 1px solid #ef444440; }
|
||||||
|
.status-erreur { background: #ef444420; color: #ef4444; border: 1px solid #ef444440; }
|
||||||
|
.inst-metric { font-size: 10px; color: var(--muted); margin-top: 4px; }
|
||||||
|
.inst-metric span { color: var(--text); }
|
||||||
|
.inst-current { margin-top: 8px; font-size: 11px; background: var(--surface2); border-radius: 5px; padding: 6px 8px; color: var(--text); min-height: 28px; }
|
||||||
|
|
||||||
|
/* Crons */
|
||||||
|
.crons { display: grid; grid-template-columns: repeat(2,1fr); gap: 10px; margin-bottom: 20px; }
|
||||||
|
.cron-card { background: var(--surface); border-radius: 8px; padding: 10px 12px; display: flex; justify-content: space-between; align-items: center; border: 1px solid var(--border); }
|
||||||
|
.cron-name { font-size: 11px; font-weight: 700; }
|
||||||
|
.cron-inst { font-size: 9px; color: var(--muted); margin-top: 2px; }
|
||||||
|
.cron-sched { font-size: 10px; color: var(--yellow); background: #eab30810; border: 1px solid #eab30830; border-radius: 4px; padding: 2px 8px; }
|
||||||
|
|
||||||
|
/* Missions table */
|
||||||
|
.missions-table { width: 100%; border-collapse: collapse; font-size: 11px; }
|
||||||
|
.missions-table th { text-align: left; padding: 6px 10px; color: var(--muted); font-size: 9px; letter-spacing: 1px; border-bottom: 1px solid var(--border); }
|
||||||
|
.missions-table td { padding: 7px 10px; border-bottom: 1px solid var(--border); vertical-align: middle; }
|
||||||
|
.missions-table tr:hover td { background: var(--surface2); }
|
||||||
|
.badge { font-size: 9px; padding: 2px 7px; border-radius: 4px; font-weight: 700; white-space: nowrap; }
|
||||||
|
.badge-tt { background: #3b82f620; color: var(--tt); }
|
||||||
|
.badge-nyora { background: #22c55e20; color: var(--nyora); }
|
||||||
|
.badge-perso { background: #f9731620; color: var(--perso); }
|
||||||
|
.badge-terminee { background: #22c55e15; color: #22c55e; }
|
||||||
|
.badge-en-cours { background: #3b82f615; color: #3b82f6; }
|
||||||
|
.badge-bloquee { background: #ef444415; color: #ef4444; }
|
||||||
|
.badge-erreur { background: #ef444415; color: #ef4444; }
|
||||||
|
.badge-en-attente { background: #ffffff08; color: var(--muted); }
|
||||||
|
.mission-text { max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.result-text { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); }
|
||||||
|
.ts-text { color: var(--muted); font-size: 10px; white-space: nowrap; }
|
||||||
|
|
||||||
|
/* Stats bar */
|
||||||
|
.stats { display: grid; grid-template-columns: repeat(4,1fr); gap: 10px; margin-bottom: 20px; }
|
||||||
|
.stat-card { background: var(--surface); border-radius: 8px; padding: 12px; text-align: center; border: 1px solid var(--border); }
|
||||||
|
.stat-value { font-size: 22px; font-weight: 800; }
|
||||||
|
.stat-label { font-size: 9px; color: var(--muted); letter-spacing: 1px; margin-top: 2px; }
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media(max-width:768px){
|
||||||
|
.instances,.crons,.stats { grid-template-columns:1fr; }
|
||||||
|
.missions-table { font-size:10px; }
|
||||||
|
.result-text,.mission-text { max-width:120px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loader */
|
||||||
|
#loader { position:fixed; inset:0; background:var(--bg); display:flex; align-items:center; justify-content:center; font-size:12px; letter-spacing:3px; color:var(--muted); z-index:99; transition:opacity .5s; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="loader">CHARGEMENT...</div>
|
||||||
|
|
||||||
|
<div class="header">
|
||||||
|
<div>
|
||||||
|
<div class="header-title">HERMES MISSION CONTROL</div>
|
||||||
|
<div class="header-sub">3 instances · Nabil Derouiche · Zone Sud TT + Nyora</div>
|
||||||
|
</div>
|
||||||
|
<div class="live-badge">
|
||||||
|
<div class="pulse"></div>
|
||||||
|
<span id="last-update">—</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<!-- Stats -->
|
||||||
|
<div class="stats" id="stats-bar">
|
||||||
|
<div class="stat-card"><div class="stat-value" id="stat-total">—</div><div class="stat-label">MISSIONS TOTALES</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-value" id="stat-active" style="color:var(--green)">—</div><div class="stat-label">EN COURS</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-value" id="stat-done" style="color:var(--blue)">—</div><div class="stat-label">TERMINEES</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-value" id="stat-blocked" style="color:var(--red)">—</div><div class="stat-label">BLOQUEES</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Instance cards -->
|
||||||
|
<div class="section-label">INSTANCES</div>
|
||||||
|
<div class="instances" id="instances-grid"></div>
|
||||||
|
|
||||||
|
<!-- Crons -->
|
||||||
|
<div class="section-label">CRONS ACTIFS</div>
|
||||||
|
<div class="crons" id="crons-grid"></div>
|
||||||
|
|
||||||
|
<!-- Missions -->
|
||||||
|
<div class="section-label">HISTORIQUE MISSIONS</div>
|
||||||
|
<div style="background:var(--surface);border-radius:10px;overflow:hidden;border:1px solid var(--border)">
|
||||||
|
<table class="missions-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th><th>MISSION</th><th>INSTANCE</th><th>STATUT</th>
|
||||||
|
<th>MODELE</th><th>DEBUT</th><th>RESULTAT</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="missions-tbody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const INST_LABELS = {"hermes-tt":"Hermes TT","hermes-nyora":"Hermes Nyora","hermes-perso":"Hermes Perso"};
|
||||||
|
|
||||||
|
function statBadge(s){
|
||||||
|
const cls = s ? s.replace("-","") : "enattente";
|
||||||
|
return `<span class="badge badge-${s||'en-attente'}">${s||'—'}</span>`;
|
||||||
|
}
|
||||||
|
function instBadge(i){
|
||||||
|
const key = i||"";
|
||||||
|
const cls = key.replace("hermes-","");
|
||||||
|
return `<span class="badge badge-${cls}">${INST_LABELS[key]||key||"—"}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderInstances(status){
|
||||||
|
const grid = document.getElementById("instances-grid");
|
||||||
|
grid.innerHTML = Object.entries(status).map(([key,s])=>{
|
||||||
|
const active = s.active_count > 0;
|
||||||
|
const statCls = active ? "status-active" : (s.last_statut==="bloquee"?"status-bloquee":"status-idle");
|
||||||
|
const statLabel = active ? "EN COURS" : (s.last_statut||"IDLE").toUpperCase();
|
||||||
|
return `<div class="inst-card ${key}">
|
||||||
|
<div class="inst-header">
|
||||||
|
<div class="inst-name" style="color:${s.color}">${s.label}</div>
|
||||||
|
<div class="inst-status ${statCls}">${statLabel}</div>
|
||||||
|
</div>
|
||||||
|
<div class="inst-metric">Missions: <span>${s.total}</span></div>
|
||||||
|
<div class="inst-metric">Dernière: <span>${s.last_time||"—"}</span></div>
|
||||||
|
<div class="inst-metric">Modèle: <span style="font-size:9px">${s.last_modele||"—"}</span></div>
|
||||||
|
<div class="inst-current">${s.current ? `⚡ ${s.current}` : (s.last_mission||"—")}</div>
|
||||||
|
</div>`;
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCrons(crons){
|
||||||
|
const grid = document.getElementById("crons-grid");
|
||||||
|
grid.innerHTML = crons.map(c=>`
|
||||||
|
<div class="cron-card">
|
||||||
|
<div><div class="cron-name">${c.name}</div><div class="cron-inst">${INST_LABELS[c.instance]||c.instance}</div></div>
|
||||||
|
<div class="cron-sched">${c.schedule}</div>
|
||||||
|
</div>`).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMissions(missions){
|
||||||
|
const tbody = document.getElementById("missions-tbody");
|
||||||
|
tbody.innerHTML = missions.map(m=>`
|
||||||
|
<tr>
|
||||||
|
<td style="color:var(--muted);font-size:10px">${m.id||"—"}</td>
|
||||||
|
<td><div class="mission-text">${m.mission||"—"}</div></td>
|
||||||
|
<td>${instBadge(m.instance)}</td>
|
||||||
|
<td>${statBadge(m.statut)}</td>
|
||||||
|
<td style="color:var(--muted);font-size:10px">${m.modele||"—"}</td>
|
||||||
|
<td class="ts-text">${m.debut||"—"}</td>
|
||||||
|
<td><div class="result-text">${m.resultat||"—"}</div></td>
|
||||||
|
</tr>`).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStats(missions){
|
||||||
|
document.getElementById("stat-total").textContent = missions.length;
|
||||||
|
document.getElementById("stat-active").textContent = missions.filter(m=>m.actif).length;
|
||||||
|
document.getElementById("stat-done").textContent = missions.filter(m=>m.statut==="terminee").length;
|
||||||
|
document.getElementById("stat-blocked").textContent = missions.filter(m=>m.statut==="bloquee"||m.statut==="erreur").length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(data){
|
||||||
|
document.getElementById("loader").style.opacity = "0";
|
||||||
|
setTimeout(()=>document.getElementById("loader").style.display="none", 500);
|
||||||
|
renderInstances(data.status);
|
||||||
|
renderCrons(data.crons);
|
||||||
|
renderMissions(data.missions);
|
||||||
|
renderStats(data.missions);
|
||||||
|
const d = new Date(data.ts*1000);
|
||||||
|
document.getElementById("last-update").textContent = d.toLocaleTimeString("fr-FR");
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSE
|
||||||
|
const es = new EventSource("/events");
|
||||||
|
es.onmessage = e => update(JSON.parse(e.data));
|
||||||
|
es.onerror = () => {};
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
fetch("/api/data").then(r=>r.json()).then(update).catch(console.error);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue