Files
Bjorn/web/index.html

896 lines
37 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>Bjorn Cyberviking Dashboard</title>
<link rel="icon" href="/web/images/favicon.ico" type="image/x-icon" />
<link rel="stylesheet" href="/web/css/global.css" />
<link rel="manifest" href="manifest.json" />
<link rel="apple-touch-icon" sizes="192x192" href="/web/images/icon-192x192.png" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="theme-color" content="#333" />
<script>
// Compat shim pour global.js
window.shouldCloseLauncher = window.shouldCloseLauncher || function(){ return true; };
window.BJORN_DASH = window.BJORN_DASH || {};
</script>
<script src="/web/js/global.js" defer></script>
<style>
/* ===== Harmonisation avec global.css (avec fallbacks) ===== */
:root{
--gap:12px; --radius:14px; --pad:12px;
--fs-meta:12px; --fs-title:22px;
/* Fallbacks si non définis dans global.css */
--bg: var(--app-bg, #0e1012);
--panel: var(--c-panel, rgba(17,20,22,.75));
--panel-2: var(--c-panel-2, rgba(16,22,22,.6));
--ink: var(--text, #e6fff7);
--muted: var(--text-dim, #8affc1cc);
--acid: var(--accent, #00ff9a);
--acid-2: var(--accent-2, #18f0ff);
--ok: var(--c-ok, #2cff7e);
--warning: var(--c-warn, #ffd166);
--danger: var(--c-danger, #ff4d6d);
--c-border: var(--c-border-strong, rgba(255,255,255,.09));
--shadow: var(--shadow-xl, 0 10px 26px rgba(0,0,0,.35));
--glow-weak: color-mix(in oklab, var(--acid-2) 30%, transparent);
--glow-mid: color-mix(in oklab, var(--acid-2) 70%, transparent);
}
@media (min-width:1024px){
:root{ --gap:14px; --radius:16px; --pad:14px; --fs-title:24px; }
}
html, body { background: var(--bg); color: var(--ink); margin: 0; }
/* ===== Cartes ===== */
.card{
border:1px solid var(--c-border);
background: color-mix(in oklab, var(--panel) 92%, transparent);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: var(--pad);
backdrop-filter: saturate(1.05) blur(3px);
}
.head{ display:flex; justify-content:space-between; align-items:center; gap:10px; }
.head .title{ font-size: var(--fs-title); line-height:1.1; margin:0 }
.head .meta{ color:var(--muted); font-size: var(--fs-meta) }
.pill{ font-size:12px; color:var(--muted); }
.hero-grid{
display:grid; gap: var(--gap);
grid-template-columns: 1fr;
}
@media (min-width:1024px){
.hero-grid{ grid-template-columns: minmax(240px,320px) 1fr minmax(220px,300px); }
}
/* ===== Battery (naked) ===== */
/* ===== Battery “naked” (no rectangular card) ===== */
.battery-card.naked{ border:none; background:transparent; box-shadow:none; padding:0; display:grid; place-items:center; } .battery-wrap{ position:relative; width: clamp(180px, 46vw, 260px); aspect-ratio: 1 / 1; height: auto; display:grid; place-items:center; } .battery-ring{ position:absolute; left:50%; top:50%; width:100%; height:100%; transform: translate(-50%, -50%) rotate(-90deg); display:block; } .batt-bg{ fill:none; stroke: color-mix(in oklab, var(--ink) 10%, transparent); stroke-width:16; opacity:.35 } .batt-fg{ --ring1: var(--acid); --ring2: var(--acid-2); --ringGlow: var(--glow-mid); fill:none; stroke: url(#batt-grad); stroke-width:16; stroke-linecap:round; filter:url(#batt-glow); stroke-dasharray:100; stroke-dashoffset:100; transition: stroke-dashoffset .9s ease; } .batt-scan{ fill:none; stroke: var(--glow-mid); stroke-width:16; stroke-linecap:round; stroke-dasharray:8 280; opacity:.14; transform-origin:50% 50%; animation: battSweep 2.2s linear infinite; } @keyframes battSweep{ to{ transform: rotate(360deg); } } .batt-center{ position:absolute; inset:0; display:grid; grid-template-rows:auto auto auto; align-content:center; justify-items:center; gap:6px; padding:6px; text-align:center; } .bjorn-portrait{ position:relative; width:64px; height:64px; display:grid; place-items:center; overflow:hidden; } .bjorn-portrait img{ width:100%; height:100%; object-fit:contain; display:block; opacity:.95 } .bjorn-lvl{ position:absolute; right:-4px; bottom:-4px; font-size:11px; font-weight:700; padding:2px 6px; border-radius:999px; background:#0f1f18; color:#d9ffe7; border:1px solid color-mix(in oklab, var(--ok) 40%, var(--c-border)); box-shadow: 0 0 8px var(--glow-weak); } .batt-val{ font-size: clamp(18px, 5vw, 24px); font-weight:800; text-shadow: 0 0 14px var(--glow-weak); } .batt-state{ color:var(--muted); font-size:11px; display:flex; align-items:center; gap:6px; } .batt-indicator{ width:16px; height:16px; display:inline-grid; place-items:center; } .batt-indicator svg{ width: 18px; height: 18px; stroke:currentColor; fill:none; stroke-width:2; } .pulse{ animation: pulseGlow 1.4s ease-in-out infinite; } @keyframes pulseGlow{ 0%,100%{ transform: scale(1); opacity: .9; } 50%{ transform: scale(1.1); opacity: 1; filter: drop-shadow(0 0 6px var(--glow-mid)); } }
/* ===== Connectivity / Internet / KPIs ===== */
.net-card .globe{ position:relative; width:84px; height:84px; display:grid; place-items:center; background:color-mix(in oklab, var(--panel-2) 92%, transparent) }
.globe svg{ display:block; }
.globe-rim{ fill:none; stroke: color-mix(in oklab, var(--ink) 18%, transparent); stroke-width:3 }
.globe-lines{ fill:none; stroke: var(--acid-2); stroke-opacity:.85; stroke-width:2; stroke-linecap:round; stroke-dasharray:4 5; animation: globeSpin 12s linear infinite; transform-origin: 32px 32px; }
@keyframes globeSpin{ to{ transform: rotate(360deg); } }
.net-badge{
display:inline-flex; align-items:center; gap:8px; padding:6px 10px; border-radius:999px;
border:1px solid var(--c-border); background: color-mix(in oklab, var(--panel-2) 90%, transparent); font-weight:700;
}
.net-on{
color: color-mix(in oklab, var(--ink) 94%, white);
background: var(--ok);
border-color: color-mix(in oklab, var(--ok) 60%, transparent);
text-shadow: 0 1px 0 rgba(0,0,0,.25);
}
.net-off{
background: color-mix(in oklab, var(--danger) 12%, var(--panel));
color: var(--ink);
border-color: color-mix(in oklab, var(--danger) 50%, transparent);
box-shadow: inset 0 0 10px color-mix(in oklab, var(--danger) 35%, transparent);
}
.conn-card .row{
display:grid; grid-template-columns: 22px 1fr auto; gap:10px; align-items:center;
padding:8px; border:1px solid var(--c-border); border-radius:12px;
background: color-mix(in oklab, var(--panel) 96%, transparent);
}
.conn-card .row + .row{ margin-top:8px; }
.conn-card .icon{ width:22px; height:22px; display:grid; place-items:center; }
.conn-card .icon svg{ width:20px; height:20px; stroke: var(--muted); fill:none; stroke-width:2; }
/* Ajout LED physique (radio/link/UDC) avant licône */
.conn-card #row-wifi,
.conn-card #row-bt,
.conn-card #row-eth,
.conn-card #row-usb{
grid-template-columns: 14px 22px 1fr auto; /* LED | ICON | details | state */
}
.conn-card #row-wifi::before,
.conn-card #row-bt::before,
.conn-card #row-eth::before,
.conn-card #row-usb::before{
content:"";
width:10px; height:10px; border-radius:50%;
justify-self:center;
background:#4a4f50;
box-shadow: 0 0 0 2px var(--c-border) inset, 0 0 6px rgba(0,0,0,.35);
opacity:.9;
}
.conn-card #row-wifi[data-physon]::before,
.conn-card #row-bt[data-physon]::before,
.conn-card #row-eth[data-physon]::before,
.conn-card #row-usb[data-physon]::before{
background: var(--ok);
box-shadow: 0 0 0 2px color-mix(in oklab, var(--ok) 40%, transparent) inset, 0 0 12px var(--ok);
}
.conn-card #row-wifi.err::before,
.conn-card #row-bt.err::before,
.conn-card #row-eth.err::before,
.conn-card #row-usb.err::before{
background: var(--danger);
box-shadow: 0 0 12px var(--danger);
}
.state-pill{
padding:3px 8px; border-radius:999px; font-size:12px;
border:1px solid var(--c-border);
background: color-mix(in oklab, var(--panel-2) 90%, transparent);
color:var(--muted);
}
.on .state-pill{
color:#d9ffe7;
background: color-mix(in oklab, var(--ok) 15%, #0f1f18);
border-color: color-mix(in oklab, var(--ok) 40%, var(--c-border));
}
.off .state-pill{ opacity:.8 }
.err .state-pill{
color:#ffdadd;
background: color-mix(in oklab, var(--danger) 15%, #2a1a1a);
border-color: color-mix(in oklab, var(--danger) 40%, var(--c-border));
}
.details{ color:var(--muted); font-size:12px }
.details .key{ color:var(--ink); font-weight:600 }
.details .dim{ opacity:.85 }
.kpi-cards{
display:grid; gap: var(--gap);
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
margin-top: var(--gap);
}
.kpi{
border:1px solid var(--c-border); border-radius: var(--radius);
background: color-mix(in oklab, var(--panel-2) 92%, transparent);
padding: var(--pad); display:grid; gap:6px;
}
.kpi .label{ color:var(--muted); font-size:12px }
.kpi .val{ font-size:20px; font-weight:800 }
.bar{
position:relative; width:100%; height:8px; border-radius:999px; overflow:hidden;
background: color-mix(in oklab, var(--ink) 8%, transparent); border:1px solid var(--c-border);
}
.bar > i{
position:absolute; left:0; top:0; bottom:0; width:0%;
background: linear-gradient(90deg, var(--acid), var(--acid-2));
transition: width .25s ease;
}
.bar > i.warm{ background: linear-gradient(90deg, color-mix(in oklab, var(--warning) 85%, #ffbe55), var(--warning)); }
.bar > i.hot{ background: linear-gradient(90deg, color-mix(in oklab, var(--danger) 85%, #ff6b6b), var(--danger)); }
.delta{
display:inline-flex; gap:6px; align-items:center; padding:2px 8px; border-radius:999px; font-size:12px;
border:1px solid var(--c-border); background: color-mix(in oklab, var(--panel) 92%, transparent); color:var(--muted);
}
.delta.good{
color:#d9ffe7; background: color-mix(in oklab, var(--ok) 15%, #0f1f18); border-color: color-mix(in oklab, var(--ok) 40%, var(--c-border));
}
.delta.bad{
color:#ffdadd; background: color-mix(in oklab, var(--danger) 15%, #2a1a1a); border-color: color-mix(in oklab, var(--danger) 40%, var(--c-border));
}
.submeta{ color:var(--muted); font-size:12px }
</style>
</head>
<body>
<main class="main" id="main">
<!-- HEADER (tap to refresh) -->
<section class="grid-stack" style="margin-bottom:12px">
<div class="card" id="liveops-card" style="cursor:pointer">
<div class="head">
<div><h2 class="title">Live Ops</h2></div>
<span class="pill">Last update: <span id="db-last-update"></span></span>
</div>
</div>
</section>
<!-- HERO -->
<section class="hero-grid">
<!-- BATTERY + BJORN -->
<article class="battery-card naked">
<div class="battery-wrap">
<svg class="battery-ring" viewBox="0 0 220 220" width="220" height="220" aria-hidden="true">
<defs>
<linearGradient id="batt-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="var(--ring1, var(--acid))"/>
<stop offset="100%" stop-color="var(--ring2, var(--acid-2))"/>
</linearGradient>
<filter id="batt-glow" x="-50%" y="-50%" width="200%" height="200%">
<feDropShadow dx="0" dy="0" stdDeviation="6" flood-color="var(--ringGlow, var(--glow-mid))"/>
</filter>
</defs>
<circle cx="110" cy="110" r="92" class="batt-bg"></circle>
<circle id="batt-fg" cx="110" cy="110" r="92" pathLength="100" class="batt-fg"></circle>
<circle id="batt-scan" cx="110" cy="110" r="92" class="batt-scan"></circle>
</svg>
<div class="batt-center" aria-live="polite">
<div class="bjorn-portrait" title="Bjorn">
<img id="bjorn-icon" src="/web/images/bjornwebicon.png" alt="Bjorn"
onerror="this.style.opacity=0.7; this.style.filter='grayscale(100%)'">
<span class="bjorn-lvl" id="bjorn-level">LVL 1</span>
</div>
<div class="batt-val"><span id="sys-battery"></span>%</div>
<div class="batt-state" id="sys-battery-state">
<span id="sys-battery-state-text"></span>
<span class="batt-indicator">
<!-- USB icon -->
<svg id="ico-usb" viewBox="0 0 24 24" style="display:none">
<path d="M12 2v14"/><circle cx="12" cy="20" r="2"/>
<path d="M7 7h5l-2-2 2-2h-5zM12 10h5l-2-2 2-2h-5z"/>
</svg>
<!-- Battery icon -->
<svg id="ico-batt" viewBox="0 0 24 24" style="display:none">
<rect x="2" y="7" width="18" height="10" rx="2"/>
<rect x="20" y="10" width="2" height="4" rx="1"/>
<path d="M9 9l-2 4h4l-2 4"/>
</svg>
</span>
</div>
</div>
</div>
</article>
<!-- CONNECTIVITY -->
<article class="card conn-card" id="conn-card">
<div class="head" style="margin-bottom:6px;">
<span class="title" style="font-size:18px">Connectivity</span>
</div>
<div class="row" id="row-wifi">
<div class="icon">
<svg viewBox="0 0 24 24"><path d="M2 8c5.5-4.5 14.5-4.5 20 0"/><path d="M5 11c3.5-3 10.5-3 14 0"/><path d="M8 14c1.8-1.6 6.2-1.6 8 0"/><circle cx="12" cy="18" r="1.5"/></svg>
</div>
<div class="details" id="wifi-details"></div>
<div class="state"><span class="state-pill" id="wifi-state">OFF</span></div>
</div>
<div class="submeta" id="wifi-under"></div>
<div class="row" id="row-eth">
<div class="icon">
<svg viewBox="0 0 24 24"><rect x="4" y="3" width="16" height="8" rx="2"/><path d="M8 11v5M12 11v5M16 11v5"/><rect x="7" y="16" width="10" height="5" rx="1"/></svg>
</div>
<div class="details" id="eth-details"></div>
<div class="state"><span class="state-pill" id="eth-state">OFF</span></div>
</div>
<div class="submeta" id="eth-under"></div>
<div class="row" id="row-usb">
<div class="icon">
<svg viewBox="0 0 24 24"><path d="M12 2v14"/><circle cx="12" cy="20" r="2"/><path d="M7 7h5l-2-2 2-2h-5zM12 10h5l-2-2 2-2h-5z"/></svg>
</div>
<div class="details" id="usb-details"><span class="key">USB Gadget</span>: <span id="usb-gadget-state" class="dim">OFF</span><span class="key">Lease</span>: <span id="usb-lease" class="dim"></span><span class="key">Mode</span>: <span id="usb-mode" class="dim"></span></div>
<div class="state"><span class="state-pill" id="usb-state">OFF</span></div>
</div>
<div class="row" id="row-bt">
<div class="icon">
<svg viewBox="0 0 24 24"><path d="M7 7l10 10-5 5V2l5 5L7 17"/></svg>
</div>
<div class="details" id="bt-details"><span class="key">BT Gadget</span>: <span id="bt-gadget-state" class="dim">OFF</span><span class="key">Lease</span>: <span id="bt-lease" class="dim"></span><span class="key">Connected to</span>: <span id="bt-connected" class="dim"></span></div>
<div class="state"><span class="state-pill" id="bt-state">OFF</span></div>
</div>
</article>
<!-- INTERNET -->
<article class="card net-card">
<div class="head" style="margin-bottom:6px;">
<span class="title" style="font-size:18px">Internet</span>
</div>
<div style="display:flex; align-items:center; gap:12px;">
<div class="globe">
<svg viewBox="0 0 64 64" width="80" height="80" aria-hidden="true">
<circle cx="32" cy="32" r="28" class="globe-rim"/>
<path d="M4 32h56M32 4c10 8 10 48 0 56M32 4c-10 8-10 48 0 56" class="globe-lines"/>
</svg>
</div>
<div><span class="net-badge" id="net-badge">NO</span></div>
</div>
</article>
</section>
<!-- KPI GRID -->
<section class="kpi-cards">
<div class="kpi" id="kpi-hosts">
<div class="label">Alive hosts</div>
<div class="val"><span id="val-present">0</span> / <span id="val-known">0</span></div>
</div>
<div class="kpi" id="kpi-ports-alive">
<div class="label">Open ports (alive hosts)</div>
<div class="val" id="val-open-ports-alive">0</div>
</div>
<div class="kpi" id="kpi-wardrive">
<div class="label">Known Wi-Fi</div>
<div class="val" id="val-wardrive-known">0</div>
</div>
<div class="kpi" id="kpi-cpu-ram">
<div class="submeta">CPU: <b id="cpu-pct">0%</b></div>
<div class="bar"><i id="cpu-bar"></i></div>
<div class="submeta">RAM: <b id="ram-used">0</b> / <b id="ram-total">0</b></div>
<div class="bar"><i id="ram-bar"></i></div>
</div>
<div class="kpi" id="kpi-storage">
<div class="label">Storage</div>
<div class="submeta">Used: <b id="sto-used">0</b> / <b id="sto-total">0</b></div>
<div class="bar"><i id="sto-bar"></i></div>
</div>
<div class="kpi" id="kpi-gps">
<div class="label">GPS</div>
<div class="val" id="gps-state">OFF</div>
<div class="submeta" id="gps-info"></div>
</div>
<div class="kpi" id="kpi-zombies">
<div class="label">Zombies</div>
<div class="val" id="val-zombies">0</div>
</div>
<div class="kpi" id="kpi-creds">
<div class="label">Credentials</div>
<div class="val" id="val-creds">0</div>
</div>
<div class="kpi" id="kpi-files">
<div class="label">Data / Files collected</div>
<div class="val" id="val-files">0</div>
</div>
<div class="kpi" id="kpi-vulns">
<div class="label">Vulnerabilities</div>
<div class="val"><span id="val-vulns">0</span></div>
<div><span class="delta" id="vuln-delta"></span></div>
</div>
<div class="kpi" id="kpi-scripts">
<div class="label">Attack scripts</div>
<div class="val" id="val-scripts">0</div>
</div>
<div class="kpi" id="kpi-system">
<div class="label">System</div>
<div class="submeta" id="sys-os">OS: —</div>
<div class="submeta" id="sys-arch">Arch: —</div>
<div class="submeta" id="sys-model">Model: —</div>
<div class="submeta" id="sys-epd">Waveshare E-Ink: —</div>
</div>
<div class="kpi" id="kpi-mode">
<div class="label">Mode</div>
<div class="val" id="sys-mode"></div>
</div>
<div class="kpi" id="kpi-uptime">
<div class="label">Uptime</div>
<div class="val" id="sys-uptime"></div>
<div class="submeta" id="bjorn-age">Bjorn age: —</div>
</div>
<div class="kpi" id="kpi-fds">
<div class="label">File Descriptors</div>
<div class="submeta"><b id="fds-used">0</b> / <b id="fds-max">0</b></div>
<div class="bar"><i id="fds-bar"></i></div>
</div>
</section>
</main>
<script>
/* ---------- Helpers DOM ---------- */
const $ = (id) => document.getElementById(id);
const setText = (id, v) => { const el = $(id); if (el) el.textContent = v; };
const setNumber = (id, v) => { const el = $(id); if (el) el.textContent = Number(v ?? 0).toLocaleString(); };
function setPctBar(id, pct){
const el=$(id); if(!el) return;
pct=Math.max(0,Math.min(100, pct||0));
el.style.width=pct+'%';
el.classList.remove('warm','hot');
if (pct>=85) el.classList.add('hot');
else if (pct>=60) el.classList.add('warm');
}
function fmtBytes(n){
if (n==null) return '0';
const u=['B','KB','MB','GB','TB']; let i=0; let x=Number(n);
while (x>=1024 && i<u.length-1){ x/=1024; i++; }
return (x>=10?Math.round(x):Math.round(x*10)/10)+' '+u[i];
}
function setBadge(id, on){
const el = $(id);
if(!el) return;
el.classList.remove('net-on','net-off');
el.classList.add(on ? 'net-on' : 'net-off');
el.textContent = on ? 'YES' : 'NO';
}
function setRowState(rowId, state){ const row=$(rowId); if(!row) return; row.classList.remove('on','off','err'); row.classList.add(state); }
function setRowPhys(rowId, on){
const row = $(rowId);
if(!row) return;
if (on) row.setAttribute('data-physon', '1');
else row.removeAttribute('data-physon');
}
/* ---------- Uptime ticker ---------- */
const Uptime = {
secs: 0, timer: null,
parse(str){
if(!str) return 0;
// Formats: "Xd HH:MM:SS" | "HH:MM:SS"
let days = 0, h=0, m=0, s=0;
const dMatch = str.match(/^(\d+)d\s+(.+)$/i);
if (dMatch){ days = parseInt(dMatch[1],10) || 0; str = dMatch[2]; }
const parts = (str||'').split(':').map(x=>parseInt(x,10)||0);
if (parts.length===3){ [h,m,s] = parts; } else if (parts.length===2){ [m,s]=parts; }
return days*86400 + h*3600 + m*60 + s;
},
fmt(total){
total = Math.max(0, Math.floor(total||0));
const d = Math.floor(total/86400);
let r = total % 86400;
const h = Math.floor(r/3600); r %= 3600;
const m = Math.floor(r/60); const s = r % 60;
const hh = h.toString().padStart(2,'0');
const mm = m.toString().padStart(2,'0');
const ss = s.toString().padStart(2,'0');
return d ? `${d}d ${hh}:${mm}:${ss}` : `${hh}:${mm}:${ss}`;
},
startFrom(uptimeStr){
this.secs = this.parse(uptimeStr);
this.stop();
this.tick();
this.timer = setInterval(()=>this.tick(), 1000);
},
stop(){ if (this.timer) clearInterval(this.timer); this.timer = null; },
tick(){ setText('sys-uptime', this.fmt(this.secs)); this.secs += 1; }
};
/* ---------- Battery ring ---------- */
function updateRingColors(percent){
const fg = $('batt-fg');
if(!fg) return;
let ring1, ring2, glow;
if (percent <= 20){
ring1 = '#ff4d6d'; ring2 = '#ff6b6b'; glow = 'rgba(255,77,109,.9)';
} else if (percent <= 50){
ring1 = '#ffd166'; ring2 = '#ffbe55'; glow = 'rgba(255,209,102,.85)';
} else {
const cs = getComputedStyle(document.documentElement);
ring1 = cs.getPropertyValue('--acid').trim() || '#00ff9a';
ring2 = cs.getPropertyValue('--acid-2').trim() || '#18f0ff';
glow = cs.getPropertyValue('--glow-mid').trim() || 'rgba(24,240,255,.7)';
}
fg.style.setProperty('--ring1', ring1);
fg.style.setProperty('--ring2', ring2);
fg.style.setProperty('--ringGlow', glow);
}
function setBattery({percent=0, charging=false, present=true}){
let mode;
if (!present){ mode = 'plugged'; percent = 100; }
else { mode = charging ? 'charging' : 'discharging'; }
const stateTxt = mode === 'plugged' ? 'Plugged' : (mode === 'charging' ? 'Charging' : 'Discharging');
setText('sys-battery-state-text', stateTxt);
percent = Math.max(0, Math.min(100, percent|0));
setText('sys-battery', percent);
const fg = $('batt-fg'); if (fg) fg.style.strokeDashoffset = (100 - percent).toFixed(2);
const scan = $('batt-scan'); if (scan) scan.style.opacity = (mode === 'charging') ? 0.28 : 0.14;
updateRingColors(percent);
const icoUsb = $('ico-usb');
const icoBatt = $('ico-batt');
const stateEl = $('sys-battery-state');
if (icoUsb && icoBatt){
icoUsb.style.display = (mode === 'plugged') ? '' : 'none';
icoBatt.style.display = (mode !== 'plugged') ? '' : 'none';
icoUsb.classList.remove('pulse'); icoBatt.classList.remove('pulse');
if (mode === 'plugged') icoUsb.classList.add('pulse'); else icoBatt.classList.add('pulse');
if (stateEl){ stateEl.style.color = (mode === 'plugged') ? 'var(--acid-2)' : 'var(--ink)'; }
}
}
/* ---------- Render helpers ---------- */
function wifiLine(ssid, ip){
if(!ssid && !ip) return '—';
const ss = ssid ? `<span class="key">SSID</span>: <span>${ssid}</span>` : '';
const ii = ip ? `<span class="key">IP</span>: <span>${ip}</span>` : '';
return [ss, ii].filter(Boolean).join(' &nbsp;•&nbsp; ');
}
const underLine = (gw, dns) => {
const g = gw ? `GW: ${gw}` : null;
const d = dns ? `DNS: ${dns}` : null;
const parts = [g,d].filter(Boolean);
return parts.length ? parts.join(' • ') : '—';
};
/* ---------- Full paint (60s) ---------- */
window.updateBjornDashboard = function(data){
if(!data) return;
// Battery + Bjorn
const batt = data?.battery || {};
setBattery({
percent: batt.level_pct ?? 0,
charging: /charging/i.test(batt.state || ''),
present: batt.present !== false
});
if (data?.bjorn_icon){ const img=$('bjorn-icon'); if(img) img.src=data.bjorn_icon; }
if (data?.bjorn_level != null) setText('bjorn-level', `LVL ${data.bjorn_level}`);
// Internet
setBadge('net-badge', !!data.internet_access);
// Hosts / Ports
setNumber('val-present', data.alive_hosts ?? 0);
setNumber('val-known', data.known_hosts_total ?? 0);
setNumber('val-open-ports-alive', data.open_ports_alive_total ?? 0);
// Known Wi-Fi
setNumber('val-wardrive-known', data.wardrive_known ?? 0);
// System bars + texts
const sys = data.system || {};
updateCpuRam(sys);
const stUsed = sys.storage_used_bytes ?? 0, stTot = sys.storage_total_bytes ?? 0;
setText('sto-used', fmtBytes(stUsed)); setText('sto-total', fmtBytes(stTot));
const stPct = stTot ? (stUsed/stTot)*100 : 0; setPctBar('sto-bar', stPct);
// GPS
const gps = data.gps || {};
const gpsOn = !!gps.connected;
setText('gps-state', gpsOn ? 'ON' : 'OFF');
$('gps-info').textContent = gpsOn
? (gps.fix_quality ? `Fix: ${gps.fix_quality} • Sats: ${gps.sats ?? '—'}${gps.lat ?? '—'}, ${gps.lon ?? '—'}${gps.speed ?? '—'}` : `Fix: —`)
: '—';
// Zombies / Creds / Files
setNumber('val-zombies', data.zombies || 0);
setNumber('val-creds', data.credentials || data.secrets || 0);
setNumber('val-files', data.files_found || 0);
// Vulns + delta
const totalV = data.vulnerabilities || 0;
setNumber('val-vulns', totalV);
const dEl = $('vuln-delta');
if (dEl){
const deltaMissing = Number(data.vulns_missing_since_last_scan ?? 0);
dEl.classList.remove('good','bad');
if (deltaMissing > 0) dEl.classList.add('good');
if (deltaMissing < 0) dEl.classList.add('bad');
dEl.textContent = deltaMissing === 0 ? '= since last scan'
: (deltaMissing > 0 ? `${Math.abs(deltaMissing)} since last scan`
: `+${Math.abs(deltaMissing)} since last scan`);
}
// Scripts / Mode
setNumber('val-scripts', data.attack_scripts || 0);
setText('sys-mode', (data.mode || '—').toString().toUpperCase());
// Uptime ticker
Uptime.startFrom(data.uptime || '00:00:00');
// Age (statique)
const init_ts = data.first_init_ts || data.first_init_timestamp;
setText('bjorn-age', `Bjorn age: ${humanBjornAge(init_ts)}`);
// System info
const osName = sys.os_name || sys.os || '—';
const osVer = sys.os_version ? ` ${sys.os_version}` : '';
const arch = sys.arch || sys.bits || '—';
const model = sys.model || sys.board || '—';
const epd = sys.waveshare_epd_connected;
const epdType = sys.waveshare_epd_type;
setText('sys-os', `OS: ${osName}${osVer}`);
setText('sys-arch', `Arch: ${arch}`);
setText('sys-model',`Model: ${model}`);
setText('sys-epd', `Waveshare E-Ink: ${epd===true ? 'ON' : epd===false ? 'OFF' : '—'}${epdType ? ` (${epdType})` : ''}`);
// Connectivity
const c = data.connectivity || {};
const wifiOn = !!c.wifi;
setRowState('row-wifi', wifiOn ? 'on' : 'off');
setRowPhys('row-wifi', c.wifi_radio_on === true);
setText('wifi-state', wifiOn ? 'ON' : 'OFF');
$('wifi-details').innerHTML = wifiLine(c.wifi_ssid || c.ssid, c.wifi_ip || c.ip_wifi);
$('wifi-under').textContent = underLine(c.wifi_gw || c.gw_wifi, c.wifi_dns || c.dns_wifi);
const ethOn = !!c.ethernet;
setRowState('row-eth', ethOn ? 'on' : 'off');
setRowPhys('row-eth', c.eth_link_up === true);
setText('eth-state', ethOn ? 'ON' : 'OFF');
$('eth-details').innerHTML = (c.eth_ip || c.ip_eth) ? `<span class="key">IP</span>: <span>${c.eth_ip || c.ip_eth}</span> ` : '—';
$('eth-under').textContent = underLine(c.eth_gw || c.gw_eth, c.eth_dns || c.dns_eth);
const usbG = !!c.usb_gadget;
setRowState('row-usb', (usbG || c.usb_lease_ip) ? 'on' : 'off');
setRowPhys('row-usb', c.usb_phys_on === true);
setText('usb-state', usbG ? 'ON' : 'OFF');
setText('usb-gadget-state', usbG ? 'ON' : 'OFF');
setText('usb-lease', c.usb_lease_ip || c.ip_neigh_lease_usb || '—');
setText('usb-mode', c.usb_mode || 'Device');
const btG = !!c.bt_gadget;
setRowState('row-bt', (btG || c.bt_lease_ip || c.bt_connected_to) ? 'on' : 'off');
setRowPhys('row-bt', c.bt_radio_on === true);
setText('bt-state', btG ? 'ON' : 'OFF');
setText('bt-gadget-state', btG ? 'ON' : 'OFF');
setText('bt-lease', c.bt_lease_ip || c.ip_neigh_lease_bt || '—');
setText('bt-connected', c.bt_connected_to || c.bluetooth_connected_to || '—');
// Last update
const ts = data.timestamp ? new Date(data.timestamp*1000) : new Date();
$('db-last-update').textContent = ts.toLocaleString();
};
// Human-readable "age"
function humanBjornAge(init_ts){
if(!init_ts) return '—';
const now = Date.now() / 1000;
const delta = Math.max(0, now - Number(init_ts));
const days = Math.floor(delta / 86400);
if (days < 60) return `${days} day${days!==1?'s':''}`;
const months = Math.floor(days / 30.44);
if (months < 24) return `${months} month${months!==1?'s':''}`;
const years = days / 365.25;
const ystr = years < 10 ? years.toFixed(1) : Math.round(years).toString();
return `${ystr} year${years>=2?'s':''}`;
}
/* ---------- CPU/RAM light paint (5s) ---------- */
function updateCpuRam(sys){
// CPU
const cpu = Math.max(0, Math.min(100, sys.cpu_pct ?? 0));
setText('cpu-pct', `${Math.round(cpu)}%`);
setPctBar('cpu-bar', cpu);
// RAM
const ramUsed = sys.ram_used_bytes ?? 0, ramTot = sys.ram_total_bytes ?? 0;
setText('ram-used', fmtBytes(ramUsed)); setText('ram-total', fmtBytes(ramTot));
const ramPct = ramTot ? (ramUsed/ramTot)*100 : 0; setPctBar('ram-bar', ramPct);
// FDs
if (sys.open_fds !== undefined && sys.max_fds !== undefined) {
setText('fds-used', sys.open_fds);
setText('fds-max', sys.max_fds);
const fdPct = sys.max_fds ? (sys.open_fds / sys.max_fds) * 100 : 0;
setPctBar('fds-bar', fdPct);
}
}
function paintConnectivity(c){
if(!c) return;
setRowState('row-wifi', c.wifi ? 'on' : 'off');
setRowPhys('row-wifi', c.wifi_radio_on === true);
setText('wifi-state', c.wifi ? 'ON' : 'OFF');
$('wifi-details').innerHTML = wifiLine(c.wifi_ssid || c.ssid, c.wifi_ip || c.ip_wifi);
$('wifi-under').textContent = underLine(c.wifi_gw || c.gw_wifi, c.wifi_dns || c.dns_wifi);
setRowState('row-eth', c.ethernet ? 'on' : 'off');
setRowPhys('row-eth', c.eth_link_up === true);
setText('eth-state', c.ethernet ? 'ON' : 'OFF');
$('eth-details').innerHTML = (c.eth_ip || c.ip_eth) ? `<span class="key">IP</span>: <span>${c.eth_ip || c.ip_eth}</span>` : '—';
$('eth-under').textContent = underLine(c.eth_gw || c.gw_eth, c.eth_dns || c.dns_eth);
const usbG = !!c.usb_gadget;
setRowState('row-usb', (usbG || c.usb_lease_ip) ? 'on' : 'off');
setRowPhys('row-usb', c.usb_phys_on === true);
setText('usb-state', usbG ? 'ON' : 'OFF');
setText('usb-gadget-state', usbG ? 'ON' : 'OFF');
setText('usb-lease', c.usb_lease_ip || c.ip_neigh_lease_usb || '—');
setText('usb-mode', c.usb_mode || 'Device');
const btG = !!c.bt_gadget;
setRowState('row-bt', (btG || c.bt_lease_ip || c.bt_connected_to) ? 'on' : 'off');
setRowPhys('row-bt', c.bt_radio_on === true);
setText('bt-state', btG ? 'ON' : 'OFF');
setText('bt-gadget-state', btG ? 'ON' : 'OFF');
setText('bt-lease', c.bt_lease_ip || c.ip_neigh_lease_bt || '—');
setText('bt-connected', c.bt_connected_to || c.bluetooth_connected_to || '—');
}
/* ---------- Normalisation payload ---------- */
function normalizeStats(payload) {
if (!payload || typeof payload !== 'object') return null;
const s = payload.stats || {};
const sys = payload.system || {};
const battery = payload.battery || {};
const conn = payload.connectivity || {};
const gps = payload.gps || {};
const wardriveKnown =
s.wardrive_known ??
s.known_wifi ??
payload.wardrive_known ??
0;
const vulnsDelta =
payload.vulns_missing_since_last_scan ??
payload.vulns_delta ??
0;
return {
timestamp: payload.timestamp || Math.floor(Date.now()/1000),
first_init_ts: payload.first_init_ts || payload.first_init_timestamp,
alive_hosts: s.alive_hosts_count ?? payload.alive_hosts,
known_hosts_total: s.all_known_hosts_count ?? payload.known_hosts_total,
open_ports_alive_total: s.total_open_ports ?? payload.open_ports_alive_total,
wardrive_known: wardriveKnown,
vulnerabilities: s.vulnerabilities_count ?? payload.vulnerabilities,
zombies: s.zombie_count ?? payload.zombies,
credentials: s.credentials_count ?? payload.credentials ?? payload.secrets,
attack_scripts: s.actions_count ?? payload.attack_scripts,
files_found: payload.files_found ?? 0,
vulns_missing_since_last_scan: vulnsDelta,
internet_access: !!payload.internet_access,
mode: payload.mode || 'AUTO',
uptime: payload.uptime,
bjorn_icon: payload.bjorn_icon,
bjorn_level: payload.bjorn_level,
system: {
os_name: sys.os_name || sys.os,
os_version: sys.os_version,
arch: sys.arch || sys.bits,
model: sys.model || sys.board,
waveshare_epd_connected: sys.waveshare_epd_connected,
waveshare_epd_type: sys.waveshare_epd_type,
cpu_pct: sys.cpu_pct,
ram_used_bytes: sys.ram_used_bytes,
ram_total_bytes: sys.ram_total_bytes,
storage_used_bytes: sys.storage_used_bytes,
storage_total_bytes: sys.storage_total_bytes,
open_fds: sys.open_fds ?? payload.system?.open_fds,
max_fds: sys.max_fds ?? sys.fds_limit ?? payload.system?.fds_limit,
fds_global: sys.fds_global ?? payload.system?.fds_global,
},
battery: {
present: battery.present !== false,
level_pct: battery.level_pct,
state: battery.state,
},
gps: {
connected: !!gps.connected,
fix_quality: gps.fix_quality,
sats: gps.sats,
lat: gps.lat,
lon: gps.lon,
speed: gps.speed,
},
connectivity: {
wifi: !!(conn.wifi || conn.wifi_ssid || conn.wifi_ip),
wifi_radio_on: conn.wifi_radio_on === true, // NEW
wifi_ssid: conn.wifi_ssid || conn.ssid,
wifi_ip: conn.wifi_ip || conn.ip_wifi,
wifi_gw: conn.wifi_gw || conn.gw_wifi,
wifi_dns: conn.wifi_dns || conn.dns_wifi,
ethernet: !!(conn.ethernet || conn.eth_ip),
eth_link_up: conn.eth_link_up === true, // NEW (link physique)
eth_ip: conn.eth_ip || conn.ip_eth,
eth_gw: conn.eth_gw || conn.gw_eth,
eth_dns: conn.eth_dns || conn.dns_eth,
usb_gadget: !!conn.usb_gadget,
usb_phys_on: conn.usb_phys_on === true, // NEW
usb_mode: conn.usb_mode || 'Device',
usb_lease_ip: conn.usb_lease_ip || conn.ip_neigh_lease_usb,
bt_gadget: !!conn.bt_gadget,
bt_radio_on: conn.bt_radio_on === true, // NEW
bt_lease_ip: conn.bt_lease_ip || conn.ip_neigh_lease_bt,
bt_connected_to: conn.bt_connected_to || conn.bluetooth_connected_to,
},
};
}
/* ---------- Fetchers ---------- */
async function fetchBjornStats(){
try{
const resp = await fetch('/api/bjorn/stats', {
method: 'GET',
headers: { 'Accept': 'application/json' },
cache: 'no-store'
});
if (!resp.ok) return null;
const raw = await resp.json();
return normalizeStats(raw);
}catch{
return null;
}
}
// Light fetch (CPU/RAM only)
async function fetchAndPaintLight(){
const data = await fetchBjornStats();
if (!data) return;
if (data.system) updateCpuRam(data.system);
if (data.connectivity) paintConnectivity(data.connectivity); // << NEW
}
// Heavy fetch (full paint)
async function fetchAndPaintHeavy(){
const data = await fetchBjornStats();
if (!data){
// fail soft: ne change rien si indispo
return;
}
window.updateBjornDashboard(data);
}
// Tap to refresh (full)
(function () {
const card = document.getElementById('liveops-card');
if (!card || window.__bjornDashBound) return;
window.__bjornDashBound = true;
card.addEventListener('click', async () => {
await fetchAndPaintHeavy();
});
})();
// Initial paint + auto-refresh tiers
window.addEventListener('DOMContentLoaded', async () => {
await fetchAndPaintHeavy(); // 1) premier paint complet
// Timers
if (window.__bjornHeavyTimer) clearInterval(window.__bjornHeavyTimer);
if (window.__bjornLightTimer) clearInterval(window.__bjornLightTimer);
window.__bjornHeavyTimer = setInterval(fetchAndPaintHeavy, 60000); // 60s
window.__bjornLightTimer = setInterval(fetchAndPaintLight, 5000); // 5s (CPU/RAM)
});
</script>
</body>
</html>