mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-28 21:17:36 +00:00
feat(picobrew): panel embarque l'UI complète picobrew_pico (iframe) + bandeau de contrôle
Some checks are pending
License Headers / check (push) Waiting to run
Some checks are pending
License Headers / check (push) Waiting to run
Webui admin la plus complète possible sans dev : bandeau de contrôle SecuBox (état + start/stop/rafraîchir + plein écran) au-dessus d'un iframe de l'UI complète de picobrew_pico, servie via son sous-domaine LAN-confiné picobrew.gk2.secubox.in (même domaine parent → cert wildcard valide, pas de mixed-content). L'iframe n'est chargée que conteneur en marche. Réf. montage vhost (live gk2, patron lyrion) : sous-domaine LAN-only (snippet exposure allow-privé/deny-all) → nginx:9080 → LXC:80, TLS terminée par HAProxy, route dans /etc/secubox/waf/haproxy-routes.json, upgrade WebSocket (Upgrade/Connection comme spiderfoot). CAVEAT: un wss:// via wg-toolbox échoue tant que sbxmitm ne pipe pas l'upgrade WebSocket dans un flux MITM'd (général, pas spécifique picobrew). Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
This commit is contained in:
parent
1208374a9a
commit
e287389138
|
|
@ -12,38 +12,29 @@
|
|||
: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;
|
||||
--cyan:#00d4ff; --red:#ff4466; --green:#00dd44;
|
||||
}
|
||||
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}
|
||||
.main{display:flex;flex-direction:column;height:100vh;box-sizing:border-box}
|
||||
.bar{display:flex;justify-content:space-between;align-items:center;gap:.6rem;flex-wrap:wrap;
|
||||
padding:.55rem .8rem;background:var(--bg-card);border:1px solid var(--border);border-radius:8px;margin-bottom:.6rem}
|
||||
.bar h1{font-size:1.05rem;color:var(--cyan);text-shadow:0 0 8px var(--cyan);margin:0}
|
||||
.bar .st{font-size:.72rem;color:var(--p31-dim);margin-left:.6rem}
|
||||
.dot{display:inline-block;width:.6rem;height:.6rem;border-radius:50%;background:var(--p31-dim);margin-right:.3rem;vertical-align:middle}
|
||||
.dot.on{background:var(--green);box-shadow:0 0 6px var(--green)} .dot.off{background:var(--red)}
|
||||
.btns{display:flex;flex-wrap:wrap;gap:.4rem}
|
||||
.btn{font-family:inherit;font-size:.7rem;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:.4rem .75rem;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}
|
||||
.frame-wrap{flex:1;position:relative;border:1px solid var(--border);border-radius:8px;overflow:hidden;background:#fff}
|
||||
iframe{width:100%;height:100%;border:0;display:block}
|
||||
.placeholder{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
|
||||
text-align:center;color:var(--p31-dim);font-size:.9rem;background:var(--bg-dark);padding:2rem}
|
||||
.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>
|
||||
|
|
@ -53,100 +44,73 @@
|
|||
<script src="/shared/sidebar.js"></script>
|
||||
|
||||
<main class="main">
|
||||
<header class="header">
|
||||
<div class="bar">
|
||||
<div>
|
||||
<h1>🍺 PicoBrew</h1>
|
||||
<div class="sub" id="sub">nœud de brassage · chargement…</div>
|
||||
<h1 style="display:inline">🍺 PicoBrew</h1>
|
||||
<span class="st" id="st"><span class="dot" id="dot"></span>chargement…</span>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn" onclick="load()">↻ Rafraîchir</button>
|
||||
<div class="btns">
|
||||
<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>
|
||||
<button class="btn" onclick="load()">↻ Rafraîchir</button>
|
||||
<a class="btn primary" id="a-open" href="https://picobrew.gk2.secubox.in/" target="_blank" rel="noopener">↗ Plein écran</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 class="frame-wrap">
|
||||
<iframe id="app" title="Interface PicoBrew" referrerpolicy="no-referrer"></iframe>
|
||||
<div class="placeholder" id="ph">Conteneur à l'arrêt — démarre-le pour afficher l'interface PicoBrew.</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
// L'interface complète de picobrew_pico est embarquée via son sous-domaine
|
||||
// LAN-confiné (même domaine parent → cert wildcard valide, pas de mixed-content).
|
||||
const APP_URL = 'https://picobrew.gk2.secubox.in/';
|
||||
const API = '/api/v1/picobrew';
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
function toast(msg, isErr) {
|
||||
const t = $('toast');
|
||||
t.textContent = msg;
|
||||
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() {
|
||||
try {
|
||||
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;
|
||||
const running = !!d.running;
|
||||
$('dot').className = 'dot ' + (running ? 'on' : 'off');
|
||||
const sha = (d.pinned_sha && d.pinned_sha !== 'none') ? d.pinned_sha.slice(0,8) : '—';
|
||||
$('st').innerHTML = '<span class="dot ' + (running?'on':'off') + '"></span>' +
|
||||
(d.error ? d.error : (running ? 'en marche' : 'à l\'arrêt')) +
|
||||
' · ' + (d.ip || '—') + ' · v' + sha;
|
||||
$('b-start').disabled = running; $('b-stop').disabled = !running;
|
||||
const app = $('app'), ph = $('ph');
|
||||
if (running) {
|
||||
if (app.getAttribute('src') !== APP_URL) app.setAttribute('src', APP_URL);
|
||||
ph.style.display = 'none';
|
||||
} else {
|
||||
app.removeAttribute('src'); ph.style.display = 'flex';
|
||||
}
|
||||
} catch (e) {
|
||||
$('sub').textContent = 'panel injoignable (' + e.message + ')';
|
||||
$('st').innerHTML = '<span class="dot off"></span>panel injoignable (' + e.message + ')';
|
||||
toast('Statut indisponible : ' + e.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function act(verb, label) {
|
||||
try {
|
||||
const d = await J('/' + verb, {method:'POST'});
|
||||
toast(d && d.ok === false ? (label + ' a échoué') : (label + ' demandé'), d && d.ok === false);
|
||||
} catch (e) {
|
||||
toast(label + ' impossible : ' + e.message, true);
|
||||
} catch (e) { toast(label + ' impossible : ' + e.message, true); }
|
||||
setTimeout(load, 1500);
|
||||
}
|
||||
setTimeout(load, 1300);
|
||||
}
|
||||
|
||||
load();
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user