feat: dashboard theme terrain - light Inter font 15px
This commit is contained in:
parent
498e14fef7
commit
f477b50eea
|
|
@ -4,214 +4,276 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Hermes Mission Control</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<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; }
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--bg:#f5f7fa;--card:#fff;--card-hover:#f8fafc;--border:#e2e8f0;
|
||||
--text:#1e293b;--dim:#64748b;--muted:#94a3b8;
|
||||
--accent:#0891b2;--accent-dark:#0e7490;
|
||||
--green:#059669;--green-bg:#ecfdf5;--green-bd:#a7f3d0;
|
||||
--amber:#d97706;--amber-bg:#fffbeb;
|
||||
--red:#dc2626;--red-bg:#fef2f2;
|
||||
--blue:#3b82f6;--blue-bg:#eff6ff;
|
||||
--r:12px;--rs:8px;
|
||||
--sh:0 1px 2px rgba(0,0,0,.05),0 1px 3px rgba(0,0,0,.08);
|
||||
--shl:0 4px 6px rgba(0,0,0,.04),0 10px 30px rgba(0,0,0,.08);
|
||||
--tt:#3b82f6;--nyora:#059669;--perso:#d97706;
|
||||
}
|
||||
html{font-size:15px}
|
||||
body{font-family:'Inter',-apple-system,sans-serif;background:var(--bg);color:var(--text);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)} }
|
||||
.header{background:linear-gradient(135deg,#0e7490 0%,#0891b2 50%,#059669 100%);padding:0 32px;box-shadow:0 2px 12px rgba(8,145,178,.25)}
|
||||
.header-inner{max-width:1280px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;height:60px}
|
||||
.hicon{width:36px;height:36px;background:rgba(255,255,255,.2);border-radius:9px;display:flex;align-items:center;justify-content:center;font-size:18px}
|
||||
.htitle{font-size:1.05rem;font-weight:700;color:#fff}
|
||||
.hsub{font-size:.72rem;color:rgba(255,255,255,.7);margin-top:1px}
|
||||
.hleft{display:flex;align-items:center;gap:12px}
|
||||
.hright{display:flex;align-items:center;gap:16px}
|
||||
.live-badge{display:flex;align-items:center;gap:6px;font-size:.75rem;color:rgba(255,255,255,.85)}
|
||||
.dot{width:8px;height:8px;background:#4ade80;border-radius:50%;animation:pulse 2s infinite;box-shadow:0 0 0 2px rgba(74,222,128,.3)}
|
||||
@keyframes pulse{0%,100%{box-shadow:0 0 0 2px rgba(74,222,128,.3)}50%{box-shadow:0 0 0 5px rgba(74,222,128,.1)}}
|
||||
.uts{font-size:.72rem;color:rgba(255,255,255,.6)}
|
||||
|
||||
/* 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; }
|
||||
.main{max-width:1280px;margin:0 auto;padding:24px 32px 40px}
|
||||
.stitle{font-size:.78rem;font-weight:700;color:var(--dim);text-transform:uppercase;letter-spacing:.08em;margin-bottom:12px}
|
||||
|
||||
/* 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; }
|
||||
.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:22px}
|
||||
.sc{background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:18px 20px;box-shadow:var(--sh)}
|
||||
.sc .lbl{font-size:.7rem;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:6px}
|
||||
.sc .val{font-size:2rem;font-weight:800;letter-spacing:-.04em;line-height:1}
|
||||
.sc .sub{font-size:.72rem;color:var(--muted);margin-top:4px}
|
||||
.c-blue{color:var(--blue)}.c-green{color:var(--green)}.c-amber{color:var(--amber)}.c-red{color:var(--red)}
|
||||
|
||||
/* 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; }
|
||||
.instances{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-bottom:22px}
|
||||
.ic{background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:20px;box-shadow:var(--sh);position:relative;overflow:hidden}
|
||||
.ic::before{content:'';position:absolute;top:0;left:0;right:0;height:3px}
|
||||
.ic.tt::before{background:var(--tt)}.ic.nyora::before{background:var(--nyora)}.ic.perso::before{background:var(--perso)}
|
||||
.it{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:12px}
|
||||
.iname{font-size:1rem;font-weight:700}
|
||||
.iname.tt{color:var(--tt)}.iname.nyora{color:var(--nyora)}.iname.perso{color:var(--perso)}
|
||||
.iport{font-size:.7rem;color:var(--muted);margin-top:2px}
|
||||
.istatus{font-size:.7rem;font-weight:600;padding:3px 9px;border-radius:20px;white-space:nowrap}
|
||||
.istatus.up{background:var(--green-bg);color:var(--green);border:1px solid var(--green-bd)}
|
||||
.istatus.down{background:var(--red-bg);color:var(--red)}
|
||||
.irow{display:flex;gap:16px;margin-bottom:10px}
|
||||
.isv{font-size:1.3rem;font-weight:800;letter-spacing:-.03em}
|
||||
.isl{font-size:.65rem;color:var(--muted);text-transform:uppercase;letter-spacing:.06em}
|
||||
.ilast{font-size:.75rem;color:var(--dim);background:var(--bg);border-radius:6px;padding:7px 10px;border:1px solid var(--border)}
|
||||
.ilbl{font-size:.65rem;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:2px}
|
||||
.iclip{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
|
||||
/* 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; }
|
||||
.two-col{display:grid;grid-template-columns:1fr 320px;gap:18px;margin-bottom:22px}
|
||||
.crons-wrap{background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:20px;box-shadow:var(--sh)}
|
||||
.clist{display:flex;flex-direction:column;gap:8px}
|
||||
.ci{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:var(--bg);border-radius:var(--rs);border:1px solid var(--border)}
|
||||
.cidot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
|
||||
.cidot.tt{background:var(--tt)}.cidot.nyora{background:var(--nyora)}.cidot.perso{background:var(--perso)}
|
||||
.cileft{display:flex;align-items:center;gap:10px}
|
||||
.ciname{font-size:.82rem;font-weight:600}
|
||||
.ciinst{font-size:.68rem;color:var(--muted)}
|
||||
.cisched{font-size:.72rem;font-weight:600;color:var(--accent);background:#ecfeff;border:1px solid #a5f3fc;padding:2px 8px;border-radius:4px;font-family:'SF Mono','Fira Code',monospace}
|
||||
|
||||
/* 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; }
|
||||
.budget{background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:20px;box-shadow:var(--sh)}
|
||||
.bamt{font-size:2.2rem;font-weight:800;letter-spacing:-.04em;margin:10px 0 4px}
|
||||
.bbar-w{margin:14px 0 8px;background:var(--border);border-radius:4px;height:6px;overflow:hidden}
|
||||
.bbar{height:100%;border-radius:4px;background:linear-gradient(90deg,var(--green),var(--accent));transition:width .5s}
|
||||
.bbar.warn{background:linear-gradient(90deg,var(--amber),var(--red))}
|
||||
.bdet{font-size:.75rem;color:var(--muted)}
|
||||
|
||||
/* Responsive */
|
||||
@media(max-width:768px){
|
||||
.instances,.crons,.stats { grid-template-columns:1fr; }
|
||||
.missions-table { font-size:10px; }
|
||||
.result-text,.mission-text { max-width:120px; }
|
||||
}
|
||||
.mwrap{background:var(--card);border:1px solid var(--border);border-radius:var(--r);box-shadow:var(--sh);overflow:hidden}
|
||||
.mhdr{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--border)}
|
||||
table.mt{width:100%;border-collapse:collapse;font-size:.82rem}
|
||||
table.mt thead th{padding:10px 14px;text-align:left;font-size:.68rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.07em;background:var(--bg);border-bottom:1px solid var(--border)}
|
||||
table.mt tbody td{padding:11px 14px;border-bottom:1px solid var(--border);vertical-align:middle}
|
||||
table.mt tbody tr:last-child td{border-bottom:none}
|
||||
table.mt tbody tr:hover td{background:var(--card-hover)}
|
||||
.badge{display:inline-flex;align-items:center;font-size:.7rem;font-weight:700;padding:3px 8px;border-radius:20px;white-space:nowrap}
|
||||
.b-tt{background:var(--blue-bg);color:var(--tt)}
|
||||
.b-nyora{background:var(--green-bg);color:var(--nyora)}
|
||||
.b-perso{background:var(--amber-bg);color:var(--amber)}
|
||||
.b-terminee{background:var(--green-bg);color:var(--green)}
|
||||
.b-en-cours{background:var(--blue-bg);color:var(--blue)}
|
||||
.b-en-attente{background:#f8fafc;color:var(--muted);border:1px solid var(--border)}
|
||||
.b-bloquee{background:var(--amber-bg);color:var(--amber)}
|
||||
.b-erreur{background:var(--red-bg);color:var(--red)}
|
||||
.cm{max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}
|
||||
.cr{max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dim)}
|
||||
.cts{font-size:.72rem;color:var(--muted);white-space:nowrap}
|
||||
.cmo{font-size:.72rem;color:var(--dim);font-family:'SF Mono','Fira Code',monospace}
|
||||
.nodata{text-align:center;padding:40px 20px;color:var(--muted);font-size:.85rem}
|
||||
|
||||
/* 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; }
|
||||
#loader{position:fixed;inset:0;background:#f5f7fa;display:flex;align-items:center;justify-content:center;z-index:100;transition:opacity .4s}
|
||||
.li{text-align:center}
|
||||
.lic{font-size:2.5rem;margin-bottom:12px;animation:spin 2s linear infinite;display:inline-block}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
.lit{font-size:.85rem;color:var(--muted)}
|
||||
|
||||
@media(max-width:900px){.main{padding:16px}.stats,.instances{grid-template-columns:1fr 1fr}.two-col{grid-template-columns:1fr}}
|
||||
@media(max-width:600px){.stats,.instances{grid-template-columns:1fr}.header{padding:0 16px}}
|
||||
</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 id="loader"><div class="li"><div class="lic">+</div><div class="lit">Chargement Mission Control...</div></div></div>
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<div class="hleft">
|
||||
<div class="hicon">[H]</div>
|
||||
<div><div class="htitle">Hermes Mission Control</div><div class="hsub">Tunisie Telecom . Zone Sud</div></div>
|
||||
</div>
|
||||
<div class="hright">
|
||||
<div class="live-badge"><span class="dot"></span>Live</div>
|
||||
<div class="uts">MAJ <span id="last-update">--</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="live-badge">
|
||||
<div class="pulse"></div>
|
||||
<span id="last-update">—</span>
|
||||
</header>
|
||||
<main class="main">
|
||||
<div class="stats">
|
||||
<div class="sc"><div class="lbl">Missions totales</div><div class="val c-blue" id="st-total">--</div><div class="sub">dans Mission Control</div></div>
|
||||
<div class="sc"><div class="lbl">En cours</div><div class="val c-amber" id="st-active">--</div><div class="sub">actives maintenant</div></div>
|
||||
<div class="sc"><div class="lbl">Terminees</div><div class="val c-green" id="st-done">--</div><div class="sub">avec succes</div></div>
|
||||
<div class="sc"><div class="lbl">Erreurs</div><div class="val c-red" id="st-err">--</div><div class="sub">bloquees ou erreur</div></div>
|
||||
</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 class="stitle">Instances Hermes</div>
|
||||
<div class="instances" id="inst-grid"></div>
|
||||
<div class="two-col">
|
||||
<div class="crons-wrap"><div class="stitle">Crons planifies</div><div class="clist" id="cron-list"></div></div>
|
||||
<div class="budget">
|
||||
<div class="stitle">Budget OpenRouter</div>
|
||||
<div class="bamt" id="b-val">--</div>
|
||||
<div class="bdet" id="b-sub">sur EUR15.00 alloues</div>
|
||||
<div class="bbar-w"><div class="bbar" id="b-bar" style="width:0%"></div></div>
|
||||
<div class="bdet" id="b-pct">calcul en cours...</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>
|
||||
<div class="mwrap">
|
||||
<div class="mhdr">
|
||||
<div class="stitle" style="margin-bottom:0">Journal des missions</div>
|
||||
<span class="badge b-en-cours" id="m-count">0 missions</span>
|
||||
</div>
|
||||
<table class="mt">
|
||||
<thead><tr><th>Mission</th><th>Instance</th><th>Statut</th><th>Modele</th><th>Debut</th><th>Resultat</th></tr></thead>
|
||||
<tbody id="m-tbody"><tr><td colspan="6" class="nodata">Chargement...</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
<script>
|
||||
const INST_LABELS = {"hermes-tt":"Hermes TT","hermes-nyora":"Hermes Nyora","hermes-perso":"Hermes Perso"};
|
||||
const INST={
|
||||
'hermes-tt':{lbl:'Hermes TT',cls:'tt',port:3010},
|
||||
'hermes-nyora':{lbl:'Hermes Nyora',cls:'nyora',port:3020},
|
||||
'hermes-perso':{lbl:'Hermes Perso',cls:'perso',port:3030}
|
||||
};
|
||||
const STAT_LBL={'terminee':'Terminee','en-cours':'En cours','en-attente':'En attente','bloquee':'Bloquee','erreur':'Erreur'};
|
||||
|
||||
function statBadge(s){
|
||||
const cls = s ? s.replace("-","") : "enattente";
|
||||
return `<span class="badge badge-${s||'en-attente'}">${s||'—'}</span>`;
|
||||
function iB(inst){
|
||||
const k=(inst||'').toLowerCase();
|
||||
const i=INST[k]||{lbl:inst||'?',cls:'tt'};
|
||||
return `<span class="badge b-${i.cls}">${i.lbl}</span>`;
|
||||
}
|
||||
function instBadge(i){
|
||||
const key = i||"";
|
||||
const cls = key.replace("hermes-","");
|
||||
return `<span class="badge badge-${cls}">${INST_LABELS[key]||key||"—"}</span>`;
|
||||
function sB(s){
|
||||
return `<span class="badge b-${s||'en-attente'}">${STAT_LBL[s]||s||'?'}</span>`;
|
||||
}
|
||||
function fd(d){
|
||||
if(!d)return '--';
|
||||
const dt=new Date(d);
|
||||
if(isNaN(dt))return d;
|
||||
return dt.toLocaleString('fr-FR',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'});
|
||||
}
|
||||
|
||||
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>
|
||||
function renderInst(status){
|
||||
const g=document.getElementById('inst-grid');
|
||||
g.innerHTML=Object.entries(INST).map(([k,info])=>{
|
||||
const s=(status||{})[k]||{};
|
||||
const up=s.running!==false;
|
||||
const last=s.last_mission||'Aucune mission';
|
||||
const ts=s.last_ts?fd(s.last_ts):'--';
|
||||
return `<div class="ic ${info.cls}">
|
||||
<div class="it">
|
||||
<div><div class="iname ${info.cls}">${info.lbl}</div><div class="iport">:${info.port}</div></div>
|
||||
<span class="istatus ${up?'up':'down'}">${up?'En ligne':'Hors ligne'}</span>
|
||||
</div>
|
||||
<div class="irow">
|
||||
<div><div class="isv c-blue">${s.missions_total||0}</div><div class="isl">Missions</div></div>
|
||||
<div><div class="isv c-green">${s.missions_done||0}</div><div class="isl">Terminees</div></div>
|
||||
</div>
|
||||
<div class="ilast">
|
||||
<div class="ilbl">Derniere mission</div>
|
||||
<div class="iclip">${last}</div>
|
||||
<div style="font-size:.68rem;color:var(--muted);margin-top:2px">${ts}</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("");
|
||||
}).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("");
|
||||
const el=document.getElementById('cron-list');
|
||||
if(!crons||!crons.length){el.innerHTML='<div class="nodata" style="padding:16px">Aucun cron configure</div>';return;}
|
||||
const ic={'hermes-tt':'tt','hermes-nyora':'nyora','hermes-perso':'perso'};
|
||||
el.innerHTML=crons.map(c=>{
|
||||
const cls=ic[c.instance]||'tt';
|
||||
return `<div class="ci">
|
||||
<div class="cileft"><span class="cidot ${cls}"></span>
|
||||
<div><div class="ciname">${c.name}</div><div class="ciinst">${INST[c.instance]?.lbl||c.instance}</div></div>
|
||||
</div>
|
||||
<span class="cisched">${c.schedule}</span>
|
||||
</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 renderMissions(ms){
|
||||
const tb=document.getElementById('m-tbody');
|
||||
document.getElementById('m-count').textContent=`${ms.length} missions`;
|
||||
if(!ms.length){
|
||||
tb.innerHTML='<tr><td colspan="6" class="nodata">Aucune mission. Les agents Hermes alimenteront ce journal automatiquement.</td></tr>';
|
||||
return;
|
||||
}
|
||||
tb.innerHTML=ms.map(m=>`<tr>
|
||||
<td><div class="cm" title="${m.mission||''}">${m.mission||'--'}</div></td>
|
||||
<td>${iB(m.instance)}</td>
|
||||
<td>${sB(m.statut)}</td>
|
||||
<td class="cmo">${m.modele||'--'}</td>
|
||||
<td class="cts">${fd(m.debut)}</td>
|
||||
<td><div class="cr" title="${m.resultat||''}">${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 renderStats(ms){
|
||||
document.getElementById('st-total').textContent=ms.length;
|
||||
document.getElementById('st-active').textContent=ms.filter(m=>m.actif).length;
|
||||
document.getElementById('st-done').textContent=ms.filter(m=>m.statut==='terminee').length;
|
||||
document.getElementById('st-err').textContent=ms.filter(m=>m.statut==='bloquee'||m.statut==='erreur').length;
|
||||
}
|
||||
|
||||
function renderBudget(b){
|
||||
if(!b)return;
|
||||
const used=b.used||0,total=b.total||15,pct=Math.min(100,Math.round(used/total*100));
|
||||
document.getElementById('b-val').textContent=`EUR${used.toFixed(2)}`;
|
||||
document.getElementById('b-sub').textContent=`sur EUR${total.toFixed(2)} alloues`;
|
||||
document.getElementById('b-pct').textContent=`${pct}% consomme . reste EUR${(total-used).toFixed(2)}`;
|
||||
const bar=document.getElementById('b-bar');
|
||||
bar.style.width=pct+'%';
|
||||
if(pct>75)bar.classList.add('warn');else bar.classList.remove('warn');
|
||||
}
|
||||
|
||||
function update(data){
|
||||
document.getElementById("loader").style.opacity = "0";
|
||||
setTimeout(()=>document.getElementById("loader").style.display="none", 500);
|
||||
renderInstances(data.status);
|
||||
document.getElementById('loader').style.opacity='0';
|
||||
setTimeout(()=>document.getElementById('loader').style.display='none',400);
|
||||
renderInst(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");
|
||||
renderBudget(data.budget);
|
||||
document.getElementById('last-update').textContent=new Date(data.ts*1000).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);
|
||||
const es=new EventSource('/events');
|
||||
es.onmessage=e=>update(JSON.parse(e.data));
|
||||
es.onerror=()=>{};
|
||||
fetch('/api/data').then(r=>r.json()).then(update).catch(()=>{
|
||||
document.getElementById('loader').style.opacity='0';
|
||||
setTimeout(()=>document.getElementById('loader').style.display='none',400);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Reference in New Issue