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{
|
:root{
|
||||||
--p31-dim:#6b7b8b; --bg-dark:#0d1117; --bg-card:rgba(30,40,55,.8);
|
--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;
|
--bg-row:rgba(20,30,45,.6); --border:rgba(100,150,200,.2); --text:#e8e6d9;
|
||||||
--cyan:#00d4ff; --red:#ff4466; --orange:#ff9944; --yellow:#ffcc00;
|
--cyan:#00d4ff; --red:#ff4466; --green:#00dd44;
|
||||||
--green:#00dd44; --blue:#4488ff; --purple:#a371f7;
|
|
||||||
}
|
}
|
||||||
body{font-family:'Courier Prime',monospace;color:var(--text);background:var(--bg-dark)}
|
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}
|
.main{display:flex;flex-direction:column;height:100vh;box-sizing:border-box}
|
||||||
.header h1{font-size:1.4rem;color:var(--cyan);text-shadow:0 0 10px var(--cyan)}
|
.bar{display:flex;justify-content:space-between;align-items:center;gap:.6rem;flex-wrap:wrap;
|
||||||
.header .sub{font-size:.72rem;color:var(--p31-dim);letter-spacing:.05em;margin-top:.25rem}
|
padding:.55rem .8rem;background:var(--bg-card);border:1px solid var(--border);border-radius:8px;margin-bottom:.6rem}
|
||||||
.btn-group{display:flex;flex-wrap:wrap;gap:.4rem}
|
.bar h1{font-size:1.05rem;color:var(--cyan);text-shadow:0 0 8px var(--cyan);margin:0}
|
||||||
.btn{font-family:inherit;font-size:.72rem;text-transform:uppercase;letter-spacing:.05em;
|
.bar .st{font-size:.72rem;color:var(--p31-dim);margin-left:.6rem}
|
||||||
text-decoration:none;color:var(--text);background:var(--bg-row);border:1px solid var(--border);
|
.dot{display:inline-block;width:.6rem;height:.6rem;border-radius:50%;background:var(--p31-dim);margin-right:.3rem;vertical-align:middle}
|
||||||
border-radius:6px;padding:.45rem .8rem;cursor:pointer;transition:border-color .15s,box-shadow .15s}
|
.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: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.primary{border-color:var(--cyan);color:var(--cyan)}
|
||||||
.btn[disabled]{opacity:.4;cursor:not-allowed}
|
.btn[disabled]{opacity:.4;cursor:not-allowed}
|
||||||
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:.75rem;margin-bottom:1rem}
|
.frame-wrap{flex:1;position:relative;border:1px solid var(--border);border-radius:8px;overflow:hidden;background:#fff}
|
||||||
.stat-card{background:var(--bg-card);border:1px solid var(--border);border-radius:8px;
|
iframe{width:100%;height:100%;border:0;display:block}
|
||||||
padding:.7rem;text-align:center;backdrop-filter:blur(10px)}
|
.placeholder{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
|
||||||
.stat-card .value{font-size:1.3rem;font-weight:bold;text-shadow:0 0 10px currentColor;color:var(--text)}
|
text-align:center;color:var(--p31-dim);font-size:.9rem;background:var(--bg-dark);padding:2rem}
|
||||||
.stat-card .label{font-size:.6rem;color:var(--p31-dim);letter-spacing:.1em;margin-top:.2rem;text-transform:uppercase}
|
.toast{position:fixed;bottom:1rem;right:1rem;background:var(--cyan);color:#07121a;padding:.6rem 1rem;
|
||||||
.stat-card.ok .value{color:var(--green)} .stat-card.off .value{color:var(--red)}
|
border-radius:6px;font-size:.8rem;opacity:0;pointer-events:none;transition:opacity .2s;z-index:50}
|
||||||
.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}
|
.toast.show{opacity:1} .toast.error{background:var(--red);color:#fff}
|
||||||
@media(max-width:768px){.main{margin-left:0}}
|
@media(max-width:768px){.main{margin-left:0}}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -53,100 +44,73 @@
|
||||||
<script src="/shared/sidebar.js"></script>
|
<script src="/shared/sidebar.js"></script>
|
||||||
|
|
||||||
<main class="main">
|
<main class="main">
|
||||||
<header class="header">
|
<div class="bar">
|
||||||
<div>
|
<div>
|
||||||
<h1>🍺 PicoBrew</h1>
|
<h1 style="display:inline">🍺 PicoBrew</h1>
|
||||||
<div class="sub" id="sub">nœud de brassage · chargement…</div>
|
<span class="st" id="st"><span class="dot" id="dot"></span>chargement…</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group">
|
<div class="btns">
|
||||||
<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-start" onclick="act('start','Démarrage')">▶ Démarrer</button>
|
||||||
<button class="btn" id="b-stop" onclick="act('stop','Arrêt')">⏹ Arrêter</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>
|
</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>
|
||||||
|
|
||||||
<div class="card">
|
<div class="frame-wrap">
|
||||||
<h3>Détails</h3>
|
<iframe id="app" title="Interface PicoBrew" referrerpolicy="no-referrer"></iframe>
|
||||||
<div class="kv"><span class="k">Adresse du conteneur</span><span class="v" id="d-ip">–</span></div>
|
<div class="placeholder" id="ph">Conteneur à l'arrêt — démarre-le pour afficher l'interface PicoBrew.</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>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div class="toast" id="toast"></div>
|
<div class="toast" id="toast"></div>
|
||||||
|
|
||||||
<script>
|
<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 API = '/api/v1/picobrew';
|
||||||
const $ = (id) => document.getElementById(id);
|
const $ = (id) => document.getElementById(id);
|
||||||
|
|
||||||
function toast(msg, isErr) {
|
function toast(msg, isErr) {
|
||||||
const t = $('toast');
|
const t = $('toast'); t.textContent = msg;
|
||||||
t.textContent = msg;
|
|
||||||
t.className = 'toast show' + (isErr ? ' error' : '');
|
t.className = 'toast show' + (isErr ? ' error' : '');
|
||||||
setTimeout(() => { t.className = 'toast'; }, 2600);
|
setTimeout(() => { t.className = 'toast'; }, 2600);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function J(path, opts) {
|
async function J(path, opts) {
|
||||||
const r = await fetch(API + path, Object.assign({credentials:'same-origin'}, opts||{}));
|
const r = await fetch(API + path, Object.assign({credentials:'same-origin'}, opts||{}));
|
||||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||||
return r.json();
|
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() {
|
async function load() {
|
||||||
try {
|
try {
|
||||||
const d = await J('/status');
|
const d = await J('/status');
|
||||||
$('sub').textContent = d.error ? ('nœud de brassage · ' + d.error)
|
const running = !!d.running;
|
||||||
: 'nœud de brassage · ' + (d.running ? 'en marche' : 'à l\'arrêt');
|
$('dot').className = 'dot ' + (running ? 'on' : 'off');
|
||||||
badge($('c-installed'), !!d.installed, 'installé', 'absent');
|
const sha = (d.pinned_sha && d.pinned_sha !== 'none') ? d.pinned_sha.slice(0,8) : '—';
|
||||||
badge($('c-running'), !!d.running, 'en marche', 'arrêté');
|
$('st').innerHTML = '<span class="dot ' + (running?'on':'off') + '"></span>' +
|
||||||
badge($('c-session'), !!d.session_active, '🔥 oui', 'non');
|
(d.error ? d.error : (running ? 'en marche' : 'à l\'arrêt')) +
|
||||||
const sha = (d.pinned_sha && d.pinned_sha !== 'none') ? d.pinned_sha : null;
|
' · ' + (d.ip || '—') + ' · v' + sha;
|
||||||
$('c-sha').textContent = sha ? sha.slice(0,10) : '—';
|
$('b-start').disabled = running; $('b-stop').disabled = !running;
|
||||||
$('d-sha').textContent = sha || '—';
|
const app = $('app'), ph = $('ph');
|
||||||
$('d-ip').textContent = d.ip || '—';
|
if (running) {
|
||||||
$('d-session').textContent = d.session_active ? '🔥 oui (mise à jour verrouillée)' : 'non';
|
if (app.getAttribute('src') !== APP_URL) app.setAttribute('src', APP_URL);
|
||||||
// Lien vers l'interface réelle de l'appareil — seulement quand le conteneur tourne (I7).
|
ph.style.display = 'none';
|
||||||
const open = $('a-open');
|
} else {
|
||||||
if (d.running && d.ip) { open.href = 'https://' + d.ip + '/'; open.style.display = ''; }
|
app.removeAttribute('src'); ph.style.display = 'flex';
|
||||||
else { open.style.display = 'none'; }
|
}
|
||||||
$('b-start').disabled = !!d.running;
|
|
||||||
$('b-stop').disabled = !d.running;
|
|
||||||
} catch (e) {
|
} 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);
|
toast('Statut indisponible : ' + e.message, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function act(verb, label) {
|
async function act(verb, label) {
|
||||||
try {
|
try {
|
||||||
const d = await J('/' + verb, {method:'POST'});
|
const d = await J('/' + verb, {method:'POST'});
|
||||||
toast(d && d.ok === false ? (label + ' a échoué') : (label + ' demandé'), d && d.ok === false);
|
toast(d && d.ok === false ? (label + ' a échoué') : (label + ' demandé'), d && d.ok === false);
|
||||||
} catch (e) {
|
} catch (e) { toast(label + ' impossible : ' + e.message, true); }
|
||||||
toast(label + ' impossible : ' + e.message, true);
|
setTimeout(load, 1500);
|
||||||
}
|
|
||||||
setTimeout(load, 1300);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
load();
|
load();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user