mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-29 18:36:55 +00:00
feat(picobrew): panel hybrid-dark avec navbar + toasts + lien appliance
Réécrit le panneau selon le look management par défaut du dépôt (hybrid-dark, Courier Prime, sidebar via /shared/sidebar.js), au lieu du squelette minimal C3BOX. Ajoute des toasts d'action (échec start/stop plus silencieux), vérifie response.ok, et un lien vers l'interface réelle de l'appareil (https://<ip>/) affiché quand le conteneur tourne. Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
This commit is contained in:
parent
1dfcb66fa5
commit
9b45487da7
|
|
@ -1,72 +1,153 @@
|
|||
<!DOCTYPE html>
|
||||
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
|
||||
<html lang="fr" class="hybrid-dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>SecuBox — PicoBrew</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/shared/hybrid-dark.css">
|
||||
<style>
|
||||
body { background:#0a0a0f; color:#e8e6d9; font-family:system-ui,sans-serif; margin:0; padding:1.5rem; }
|
||||
h1 { color:#c9a84c; font-size:1.3rem; margin:0 0 1rem; }
|
||||
.kv { display:flex; gap:.6rem; padding:.35rem 0; border-bottom:1px solid #1e1e2a; }
|
||||
.k { color:#6b6b7a; min-width:11rem; } .v { color:#e8e6d9; }
|
||||
.on { color:#00ff41; } .off { color:#e63946; }
|
||||
button { background:#1e1e2a; color:#e8e6d9; border:1px solid #6e40c9; border-radius:4px;
|
||||
padding:.45rem .9rem; margin-right:.5rem; cursor:pointer; }
|
||||
button:hover { border-color:#00d4ff; }
|
||||
.warn { color:#c9a84c; font-size:.82rem; margin-top:1rem; }
|
||||
.err { color:#e63946; font-size:.82rem; margin-top:.6rem; }
|
||||
a.device-link { display:inline-block; margin-top:1rem; color:#00d4ff; }
|
||||
:root{
|
||||
--p31-dim:#6b7b8b; --bg-dark:#0d1117; --bg-card:rgba(30,40,55,.8);
|
||||
--bg-row:rgba(20,30,45,.6); --border:rgba(100,150,200,.2); --text:#e8e6d9;
|
||||
--cyan:#00d4ff; --red:#ff4466; --orange:#ff9944; --yellow:#ffcc00;
|
||||
--green:#00dd44; --blue:#4488ff; --purple:#a371f7;
|
||||
}
|
||||
body{font-family:'Courier Prime',monospace;color:var(--text);background:var(--bg-dark)}
|
||||
.header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;flex-wrap:wrap;gap:.5rem}
|
||||
.header h1{font-size:1.4rem;color:var(--cyan);text-shadow:0 0 10px var(--cyan)}
|
||||
.header .sub{font-size:.72rem;color:var(--p31-dim);letter-spacing:.05em;margin-top:.25rem}
|
||||
.btn-group{display:flex;flex-wrap:wrap;gap:.4rem}
|
||||
.btn{font-family:inherit;font-size:.72rem;text-transform:uppercase;letter-spacing:.05em;
|
||||
text-decoration:none;color:var(--text);background:var(--bg-row);border:1px solid var(--border);
|
||||
border-radius:6px;padding:.45rem .8rem;cursor:pointer;transition:border-color .15s,box-shadow .15s}
|
||||
.btn:hover{border-color:var(--cyan);box-shadow:0 0 8px rgba(0,212,255,.4)}
|
||||
.btn.primary{border-color:var(--cyan);color:var(--cyan)}
|
||||
.btn[disabled]{opacity:.4;cursor:not-allowed}
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:.75rem;margin-bottom:1rem}
|
||||
.stat-card{background:var(--bg-card);border:1px solid var(--border);border-radius:8px;
|
||||
padding:.7rem;text-align:center;backdrop-filter:blur(10px)}
|
||||
.stat-card .value{font-size:1.3rem;font-weight:bold;text-shadow:0 0 10px currentColor;color:var(--text)}
|
||||
.stat-card .label{font-size:.6rem;color:var(--p31-dim);letter-spacing:.1em;margin-top:.2rem;text-transform:uppercase}
|
||||
.stat-card.ok .value{color:var(--green)} .stat-card.off .value{color:var(--red)}
|
||||
.stat-card.cyan .value{color:var(--cyan)} .stat-card.yellow .value{color:var(--yellow)}
|
||||
.card{background:var(--bg-card);border:1px solid var(--border);border-radius:8px;
|
||||
padding:1rem;margin-bottom:1rem;backdrop-filter:blur(10px)}
|
||||
.card h3{font-size:.8rem;color:var(--cyan);text-transform:uppercase;letter-spacing:.08em;margin-bottom:.6rem}
|
||||
.card p{font-size:.8rem;color:var(--p31-dim);line-height:1.5}
|
||||
.kv{display:flex;justify-content:space-between;gap:1rem;padding:.35rem 0;border-bottom:1px solid var(--border);font-size:.82rem}
|
||||
.kv:last-child{border-bottom:0}
|
||||
.kv .k{color:var(--p31-dim)} .kv .v{font-variant-numeric:tabular-nums}
|
||||
code{color:var(--cyan);font-size:.78rem}
|
||||
.toast{position:fixed;bottom:1rem;right:1rem;background:var(--cyan);color:#07121a;
|
||||
padding:.6rem 1rem;border-radius:6px;font-size:.8rem;opacity:0;pointer-events:none;
|
||||
transition:opacity .2s;z-index:50}
|
||||
.toast.show{opacity:1} .toast.error{background:var(--red);color:#fff}
|
||||
@media(max-width:768px){.main{margin-left:0}}
|
||||
</style>
|
||||
<h1>🍺 PicoBrew</h1>
|
||||
<div id="state"><div class="kv"><span class="k">chargement…</span></div></div>
|
||||
<div id="device-link"></div>
|
||||
<div style="margin-top:1rem">
|
||||
<button onclick="act('start')">Démarrer</button>
|
||||
<button onclick="act('stop')">Arrêter</button>
|
||||
<button onclick="load()">Rafraîchir</button>
|
||||
</div>
|
||||
<div class="warn" id="warn"></div>
|
||||
<div class="err" id="err"></div>
|
||||
</head>
|
||||
<body class="hybrid-dark">
|
||||
<nav class="sidebar" id="sidebar"></nav>
|
||||
<script src="/shared/sidebar.js"></script>
|
||||
|
||||
<main class="main">
|
||||
<header class="header">
|
||||
<div>
|
||||
<h1>🍺 PicoBrew</h1>
|
||||
<div class="sub" id="sub">nœud de brassage · chargement…</div>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn" onclick="load()">↻ Rafraîchir</button>
|
||||
<button class="btn" id="b-start" onclick="act('start','Démarrage')">▶ Démarrer</button>
|
||||
<button class="btn" id="b-stop" onclick="act('stop','Arrêt')">⏹ Arrêter</button>
|
||||
<a class="btn primary" id="a-open" target="_blank" rel="noopener" style="display:none">🍺 Interface PicoBrew</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid" id="cards">
|
||||
<div class="stat-card" id="c-installed"><div class="value">–</div><div class="label">📦 Conteneur</div></div>
|
||||
<div class="stat-card" id="c-running"><div class="value">–</div><div class="label">⚙️ État</div></div>
|
||||
<div class="stat-card cyan"><div class="value" id="c-sha" style="font-size:.9rem">–</div><div class="label">🔖 Version figée</div></div>
|
||||
<div class="stat-card" id="c-session"><div class="value">–</div><div class="label">🔥 Session</div></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Détails</h3>
|
||||
<div class="kv"><span class="k">Adresse du conteneur</span><span class="v" id="d-ip">–</span></div>
|
||||
<div class="kv"><span class="k">Version upstream (SHA figé)</span><span class="v"><code id="d-sha">–</code></span></div>
|
||||
<div class="kv"><span class="k">Session de brassage active</span><span class="v" id="d-session">–</span></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>À propos</h3>
|
||||
<p>Ce module redonne vie à un appareil PicoBrew dont le cloud constructeur est éteint depuis 2020,
|
||||
via un conteneur qui parle son protocole. L'appareil est redirigé localement (DNS) vers ce nœud.
|
||||
La version du serveur est <b>figée</b> : aucune mise à jour ne survient pendant un brassage.</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
const API = '/api/v1/picobrew';
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
function toast(msg, isErr) {
|
||||
const t = $('toast');
|
||||
t.textContent = msg;
|
||||
t.className = 'toast show' + (isErr ? ' error' : '');
|
||||
setTimeout(() => { t.className = 'toast'; }, 2600);
|
||||
}
|
||||
|
||||
async function J(path, opts) {
|
||||
const r = await fetch(API + path, Object.assign({credentials:'same-origin'}, opts||{}));
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
function badge(el, on, onText, offText) {
|
||||
el.className = 'stat-card ' + (on ? 'ok' : 'off');
|
||||
el.querySelector('.value').textContent = on ? (onText||'oui') : (offText||'non');
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const el = document.getElementById('state');
|
||||
const linkEl = document.getElementById('device-link');
|
||||
const errEl = document.getElementById('err');
|
||||
try {
|
||||
const resp = await fetch(API + '/status', {credentials:'same-origin'});
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||
const d = await resp.json();
|
||||
const b = (x) => x ? '<span class="on">oui</span>' : '<span class="off">non</span>';
|
||||
el.innerHTML =
|
||||
`<div class="kv"><span class="k">Conteneur installé</span><span class="v">${b(d.installed)}</span></div>` +
|
||||
`<div class="kv"><span class="k">En marche</span><span class="v">${b(d.running)}</span></div>` +
|
||||
`<div class="kv"><span class="k">Adresse</span><span class="v">${d.ip || '—'}</span></div>` +
|
||||
`<div class="kv"><span class="k">Version figée (SHA)</span><span class="v"><code>${(d.pinned_sha||'none').slice(0,12)}</code></span></div>` +
|
||||
`<div class="kv"><span class="k">Session de brassage</span><span class="v">${b(d.session_active)}</span></div>`;
|
||||
document.getElementById('warn').textContent = d.session_active
|
||||
? 'Session active : les mises à jour sont refusées tant qu\'elle dure.'
|
||||
: (d.error || '');
|
||||
// Le lien vers l'interface PicoBrew elle-même n'a de sens que si le
|
||||
// conteneur tourne réellement — sinon il ne mènerait nulle part.
|
||||
linkEl.innerHTML = (d.running && d.ip)
|
||||
? `<a class="device-link" href="https://${d.ip}/" target="_blank" rel="noopener">→ Ouvrir l'interface PicoBrew (${d.ip})</a>`
|
||||
: '';
|
||||
errEl.textContent = '';
|
||||
const d = await J('/status');
|
||||
$('sub').textContent = d.error ? ('nœud de brassage · ' + d.error)
|
||||
: 'nœud de brassage · ' + (d.running ? 'en marche' : 'à l\'arrêt');
|
||||
badge($('c-installed'), !!d.installed, 'installé', 'absent');
|
||||
badge($('c-running'), !!d.running, 'en marche', 'arrêté');
|
||||
badge($('c-session'), !!d.session_active, '🔥 oui', 'non');
|
||||
const sha = (d.pinned_sha && d.pinned_sha !== 'none') ? d.pinned_sha : null;
|
||||
$('c-sha').textContent = sha ? sha.slice(0,10) : '—';
|
||||
$('d-sha').textContent = sha || '—';
|
||||
$('d-ip').textContent = d.ip || '—';
|
||||
$('d-session').textContent = d.session_active ? '🔥 oui (mise à jour verrouillée)' : 'non';
|
||||
// Lien vers l'interface réelle de l'appareil — seulement quand le conteneur tourne (I7).
|
||||
const open = $('a-open');
|
||||
if (d.running && d.ip) { open.href = 'https://' + d.ip + '/'; open.style.display = ''; }
|
||||
else { open.style.display = 'none'; }
|
||||
$('b-start').disabled = !!d.running;
|
||||
$('b-stop').disabled = !d.running;
|
||||
} catch (e) {
|
||||
el.innerHTML = '<div class="kv"><span class="k off">panel injoignable</span></div>';
|
||||
linkEl.innerHTML = '';
|
||||
errEl.textContent = 'Erreur de chargement : ' + e.message;
|
||||
$('sub').textContent = 'panel injoignable (' + e.message + ')';
|
||||
toast('Statut indisponible : ' + e.message, true);
|
||||
}
|
||||
}
|
||||
async function act(a) {
|
||||
const errEl = document.getElementById('err');
|
||||
|
||||
async function act(verb, label) {
|
||||
try {
|
||||
const resp = await fetch(API + '/' + a, {method:'POST', credentials:'same-origin'});
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||
const d = await resp.json();
|
||||
errEl.textContent = d.ok === false ? `Action "${a}" refusée par picobrewctl.` : '';
|
||||
const d = await J('/' + verb, {method:'POST'});
|
||||
toast(d && d.ok === false ? (label + ' a échoué') : (label + ' demandé'), d && d.ok === false);
|
||||
} catch (e) {
|
||||
errEl.textContent = `Action "${a}" échouée : ` + e.message;
|
||||
toast(label + ' impossible : ' + e.message, true);
|
||||
}
|
||||
setTimeout(load, 1200);
|
||||
setTimeout(load, 1300);
|
||||
}
|
||||
|
||||
load();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user