Compare commits

..

4 Commits

Author SHA1 Message Date
47eae4a774 feat(toolbox): restyle landing to match the new report (ref #683)
System font, rounded --panel/--line cards, cleaner accents, softer rounded SVG
bars + helper lines; R3 panel + arch note mention the 🧅 Tor egress option.
Dynamic bits unchanged: live KPIs + auto-refresh JS, per-OS install panels,
cert-probe, ?mh links. Verified live on kbin (2.7.11).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 19:18:59 +02:00
db5d5dbcf1 feat(toolbox): regenerate /report — verdict-first, graphs, collapsible (ref #683)
Simple, easy-to-process report:
- Verdict hero: conic-gradient score gauge + plain-language verdict + helper.
- 6 KPIs (connexions/hôtes/trackers/pays/apps/cert-pin).
- 3 graphs computed server-side (_build_report_charts): trackers donut
  (conic-gradient), countries bars, apps bars — with one-line helpers.
- ALL deep technical cards (threat-intel/DGA/beaconing, hosts, apps, cookies,
  avatar, transparency+per-host grades, identity, reco) collapsed into <details>.
- Mobile-first, system-font, rounded cards.
- /report/me/html resolves identity via shared _client_mac_hash (?mh → R3 WG
  peer → captive ARP) so R3 clients reach it without ?mh.

Verified live on kbin: 200, 11.7KB, gauge + 3 graphs + details render for an
R3 peer via X-R3-Peer and via ?mh. toolbox 2.7.10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 18:07:41 +02:00
41dbdadaa2 fix(toolbox): landing report/carto links carry ?mh= → no 'identity unresolved' (ref #683)
Clicking "Ma carto" / "Mon rapport" / "Qui me piste ?" hit /social/me +
/report/me with no ?mh=, so identity was re-resolved at click-time and could
400 "client identity unresolved" (off-tunnel/captive, or when X-R3-Peer wasn't
present on that request). The landing already knows the caller — now it resolves
mac_hash (new _client_mac_hash: ?mh → R3 WG peer → captive ARP) and bakes ?mh=
into the links so they always open the right client's view.

Verified live: R3 peer 10.99.1.2 → links carry ?mh=1b0ec958…; captive caller →
?mh from ARP. toolbox 2.7.8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 17:32:35 +02:00
e1b2e6ccbb fix(toolbox): injected banner trackers/cookies stuck at 0 — count live (ref #683)
The bar counted trackers (Resource Timing) + cookies (document.cookie) ONCE at
render time, which fires early — before resources/cookies have loaded — so it
showed 0, and the 2s poll's ensure() early-returned once the banner existed, so
it never refreshed. Spans now carry ids (sbx-trk/sbx-ck) and updateCounts()
re-counts on the poll → values climb to real within ~2s. toolbox 2.7.7.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 17:21:04 +02:00
5 changed files with 515 additions and 741 deletions

View File

@ -1,7 +1,8 @@
{# SPDX-License-Identifier: LicenseRef-CMSD-1.0 #}
{# Public landing page — kbin.gk2.secubox.in #}
{# Radical-simplify redesign (#543): animated hero + one CTA + install panel
up top ; everything else folded behind "En savoir plus". #}
{# #683 restyle: aligned with the new /report look — system font, rounded
--panel/--line cards, cleaner accents. Dynamic bits (data-live KPIs + JS,
install panels, cert-probe, ?mh links) unchanged. #}
<!DOCTYPE html>
<html lang=fr><head>
<meta charset=UTF-8>
@ -10,108 +11,84 @@
<title>👁️ VILLAGE3B — Qui te piste ?</title>
<link rel=manifest href=/manifest.json>
<style>
:root{--bg:#0a0a0f;--bg2:#0e0e15;--phos:#00dd44;--phos-hot:#00ff55;--dim:#006622;--text:#e8e6d9;--purple:#9e76ff;--gold:#c9a84c;--amber:#ffb347;--red:#ff4466;--cyan:#00d4ff}
:root{--bg:#0a0a0f;--panel:#11131a;--soft:#0d0f15;--phos:#00dd44;--phos-hot:#00ff55;--dim:#5a6b60;--line:#1e2630;--text:#e8e6d9;--purple:#9e76ff;--gold:#c9a84c;--amber:#ffb347;--red:#ff4466;--cyan:#66bbff}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Courier New',Menlo,monospace;background:var(--bg);color:var(--text);line-height:1.55;padding-bottom:3rem}
a{color:var(--phos);text-decoration:none}
a:hover{text-decoration:underline}
body{font-family:system-ui,-apple-system,'Segoe UI',sans-serif;background:var(--bg);color:var(--text);line-height:1.55;padding-bottom:3rem}
a{color:var(--phos);text-decoration:none}a:hover{text-decoration:underline}
.help{color:var(--dim);font-size:.8rem;font-style:italic}
/* ── HERO ── */
.hero{position:relative;overflow:hidden;background:radial-gradient(120% 120% at 50% -10%,#221041 0%,#0a0a0f 60%);padding:3rem 1.5rem 2.4rem;text-align:center;border-bottom:2px solid var(--phos)}
.eye{font-size:3.4rem;line-height:1;display:inline-block;animation:gaze 5s ease-in-out infinite;filter:drop-shadow(0 0 14px rgba(0,255,85,0.55))}
.hero{position:relative;overflow:hidden;background:radial-gradient(120% 120% at 50% -10%,#1a1030 0%,#0a0a0f 62%);padding:3rem 1.5rem 2.4rem;text-align:center;border-bottom:1px solid var(--line)}
.eye{font-size:3.4rem;line-height:1;display:inline-block;animation:gaze 5s ease-in-out infinite;filter:drop-shadow(0 0 14px rgba(0,255,85,.5))}
@keyframes gaze{0%,100%{transform:translateX(0) scale(1)}25%{transform:translateX(-6px) scale(1.04)}60%{transform:translateX(7px) scale(1.04)}}
.hero h1{font-size:2.6rem;color:var(--phos-hot);text-shadow:0 0 10px var(--phos);letter-spacing:0.08em;margin-top:0.3rem}
.hero .punch{color:var(--text);font-size:1.25rem;margin-top:0.6rem;font-weight:bold}
.hero .punch b{color:var(--gold)}
.hero .sub{color:var(--dim);font-size:0.82rem;margin-top:0.5rem;max-width:560px;margin-left:auto;margin-right:auto}
/* floating tracker dots = "who's watching" */
.hero h1{font-size:2.4rem;color:var(--phos-hot);letter-spacing:.06em;margin-top:.3rem;font-weight:800}
.hero .punch{color:var(--text);font-size:1.2rem;margin-top:.6rem;font-weight:700}.hero .punch b{color:var(--gold)}
.hero .sub{color:var(--dim);font-size:.82rem;margin-top:.5rem;max-width:560px;margin-left:auto;margin-right:auto}
.dots{position:absolute;inset:0;pointer-events:none;z-index:0}
.dots i{position:absolute;width:7px;height:7px;border-radius:50%;opacity:0.0;animation:float 7s ease-in-out infinite}
.dots i:nth-child(1){left:12%;top:30%;background:var(--cyan);animation-delay:.0s}
.dots i{position:absolute;width:7px;height:7px;border-radius:50%;opacity:0;animation:float 7s ease-in-out infinite}
.dots i:nth-child(1){left:12%;top:30%;background:var(--cyan);animation-delay:0s}
.dots i:nth-child(2){left:82%;top:24%;background:var(--amber);animation-delay:1.1s}
.dots i:nth-child(3){left:24%;top:68%;background:var(--red);animation-delay:2.3s}
.dots i:nth-child(4){left:70%;top:64%;background:var(--purple);animation-delay:.7s}
.dots i:nth-child(5){left:50%;top:14%;background:var(--cyan);animation-delay:3.0s}
.dots i:nth-child(5){left:50%;top:14%;background:var(--cyan);animation-delay:3s}
.dots i:nth-child(6){left:90%;top:54%;background:var(--red);animation-delay:1.8s}
@keyframes float{0%{opacity:0;transform:translateY(8px) scale(.6)}30%{opacity:.85}70%{opacity:.7}100%{opacity:0;transform:translateY(-14px) scale(1.1)}}
.hero>*{position:relative;z-index:1}
/* ── big CTA row ── */
.ctas{margin-top:1.4rem;display:flex;gap:0.6rem;justify-content:center;flex-wrap:wrap}
.cta{display:inline-block;padding:0.85rem 1.6rem;font-weight:bold;border-radius:8px;font-size:1.02rem;text-shadow:none;transition:transform .12s,box-shadow .12s}
/* ── CTA row ── */
.ctas{margin-top:1.4rem;display:flex;gap:.6rem;justify-content:center;flex-wrap:wrap}
.cta{display:inline-block;padding:.85rem 1.6rem;font-weight:700;border-radius:10px;font-size:1.02rem;transition:transform .12s}
.cta:hover{text-decoration:none;transform:translateY(-2px)}
.cta.go{background:var(--phos);color:#0a0a0f;box-shadow:0 4px 18px rgba(0,221,68,0.4)}
.cta.go:hover{box-shadow:0 6px 24px rgba(0,221,68,0.6)}
.cta.go{background:var(--phos);color:#06140a;box-shadow:0 4px 18px rgba(0,221,68,.35)}
.cta.alt{background:transparent;color:var(--purple);border:1px solid var(--purple)}
.cta.alt:hover{background:rgba(158,118,255,0.12)}
/* ── quicknav (trimmed) ── */
.quicknav{display:flex;flex-wrap:wrap;justify-content:center;gap:0.6rem;margin-top:1.4rem;max-width:620px;margin-left:auto;margin-right:auto}
.qi{display:flex;flex-direction:column;align-items:center;gap:4px;padding:0.5rem 0.4rem;min-width:74px;background:rgba(110,64,201,0.08);border:1px solid var(--purple);border-radius:8px;text-decoration:none;color:var(--text);transition:all 0.12s;font-family:inherit}
.qi:hover{background:rgba(110,64,201,0.22);transform:translateY(-2px);box-shadow:0 4px 14px rgba(158,118,255,0.35);text-decoration:none}
.qi-emoji{font-size:1.5rem;line-height:1}
.qi-label{font-size:0.62rem;letter-spacing:0.04em;color:var(--phos-hot);font-weight:bold;white-space:nowrap}
.container{max-width:1080px;margin:auto;padding:2rem 1.5rem}
.section{margin-bottom:2.5rem}
h2{color:var(--phos-hot);text-shadow:0 0 4px var(--phos);font-size:1.3rem;margin-bottom:0.8rem;border-bottom:1px solid var(--dim);padding-bottom:0.4rem;letter-spacing:0.04em}
h3{color:var(--purple);font-size:1rem;margin-bottom:0.5rem}
.grid{display:grid;gap:1rem}
.grid-2{grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}
.grid-4{grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}
.card{border:1px solid var(--dim);background:var(--bg2);padding:1rem 1.2rem;border-radius:4px}
.card.purple{border-color:var(--purple);background:rgba(110,64,201,0.05)}
.card.amber{border-color:var(--amber);background:rgba(255,179,71,0.05)}
.kpi{text-align:center;padding:1rem;background:rgba(0,221,68,0.05);border:1px solid var(--phos);border-radius:4px}
.kpi .v{font-size:2rem;font-weight:bold;color:var(--phos-hot);text-shadow:0 0 6px var(--phos);display:block}
.kpi .l{font-size:0.75rem;color:var(--dim)}
.level{display:flex;align-items:start;gap:0.8rem;padding:0.9rem;border-radius:4px}
.level .emj{font-size:1.8rem;flex-shrink:0}
.level .body{flex:1}
.level .body b{display:block;font-size:1rem;margin-bottom:0.2rem}
.level .body .desc{font-size:0.85rem;color:var(--text);opacity:0.85}
.level.r0{background:rgba(255,255,255,0.03);border:1px solid var(--dim)}
.level.r1{background:rgba(0,221,68,0.08);border:1px solid var(--phos);color:var(--phos-hot)}
.level.r2{background:rgba(255,179,71,0.08);border:1px solid var(--amber)}
.level.r3{background:rgba(158,118,255,0.08);border:1px solid var(--purple)}
.tag-recommended{display:inline-block;background:var(--phos);color:#0a0a0f;font-size:0.65rem;padding:0.1rem 0.4rem;border-radius:99px;font-weight:bold;margin-left:0.3rem;vertical-align:middle}
.tag-new{display:inline-block;background:var(--purple);color:#fff;font-size:0.65rem;padding:0.1rem 0.4rem;border-radius:99px;font-weight:bold;margin-left:0.3rem;vertical-align:middle}
.cta.alt:hover{background:rgba(158,118,255,.12)}
/* ── quicknav ── */
.quicknav{display:flex;flex-wrap:wrap;justify-content:center;gap:.6rem;margin:1.4rem auto 0;max-width:620px}
.qi{display:flex;flex-direction:column;align-items:center;gap:4px;padding:.55rem .45rem;min-width:74px;background:var(--soft);border:1px solid var(--line);border-radius:12px;color:var(--text);transition:.12s;font-family:inherit}
.qi:hover{border-color:var(--purple);transform:translateY(-2px);text-decoration:none}
.qi-emoji{font-size:1.5rem;line-height:1}.qi-label{font-size:.62rem;letter-spacing:.04em;color:var(--phos-hot);font-weight:700;white-space:nowrap}
/* ── layout ── */
.container{max-width:760px;margin:auto;padding:1.6rem 1.1rem}
.section{margin-bottom:1.7rem}
h2{color:var(--phos-hot);font-size:1.12rem;margin-bottom:.6rem;letter-spacing:.02em}
h3{color:var(--purple);font-size:.95rem;margin-bottom:.4rem}
.grid{display:grid;gap:1rem}.grid-2{grid-template-columns:repeat(auto-fit,minmax(260px,1fr))}.grid-4{grid-template-columns:repeat(auto-fit,minmax(140px,1fr))}
.card{border:1px solid var(--line);background:var(--panel);padding:1rem 1.1rem;border-radius:12px}
.card.purple{border-color:rgba(158,118,255,.4)}.card.amber{border-color:rgba(255,179,71,.4)}
.kpi{text-align:center;padding:.8rem .4rem;background:var(--soft);border:1px solid var(--line);border-radius:12px}
.kpi .v{font-size:1.7rem;font-weight:800;color:var(--phos-hot);display:block}.kpi .l{font-size:.66rem;color:var(--dim)}
.level{display:flex;align-items:start;gap:.8rem;padding:.85rem;border-radius:12px}
.level .emj{font-size:1.7rem;flex-shrink:0}.level .body{flex:1}.level .body b{display:block;font-size:.98rem;margin-bottom:.2rem}.level .body .desc{font-size:.84rem;color:var(--text);opacity:.85}
.level.r0{background:var(--soft);border:1px solid var(--line)}.level.r1{background:rgba(0,221,68,.08);border:1px solid var(--phos)}.level.r2{background:rgba(255,179,71,.08);border:1px solid var(--amber)}.level.r3{background:rgba(158,118,255,.08);border:1px solid var(--purple)}
.tag-recommended{display:inline-block;background:var(--phos);color:#06140a;font-size:.65rem;padding:.1rem .45rem;border-radius:99px;font-weight:700;margin-left:.3rem;vertical-align:middle}
.tag-new{display:inline-block;background:var(--purple);color:#fff;font-size:.65rem;padding:.1rem .45rem;border-radius:99px;font-weight:700;margin-left:.3rem;vertical-align:middle}
svg.chart{width:100%;max-width:400px;height:auto}
.svg-bar{fill:var(--phos);transition:fill 0.3s}
.svg-bar.medium{fill:var(--amber)}
.svg-bar.high{fill:var(--red)}
code{background:#222;padding:0.1rem 0.4rem;border-radius:2px;font-size:0.85rem;color:var(--phos-hot)}
.cta-sm{display:inline-block;background:var(--phos);color:#0a0a0f;padding:0.7rem 1.4rem;text-decoration:none;font-weight:bold;border-radius:4px;margin:0.5rem 0.3rem 0.5rem 0;text-shadow:none}
.cta-sm.outline{background:transparent;color:var(--phos);border:1px solid var(--phos)}
.footer{text-align:center;font-size:0.78rem;color:var(--dim);padding:1.5rem;border-top:1px solid var(--dim);margin-top:2rem}
.arch{font-family:monospace;font-size:0.75rem;color:var(--phos-hot);text-shadow:0 0 4px var(--phos);background:var(--bg2);padding:1rem;border:1px solid var(--dim);border-radius:4px;overflow-x:auto;white-space:pre;line-height:1.4}
/* ── install panel (kept up top) ── */
.install-panel{background:rgba(0,255,65,0.04);border:1px solid rgba(0,255,65,0.25);border-radius:6px;padding:0.6rem 0.9rem;margin:0.45rem 0;text-align:left}
.install-panel summary{cursor:pointer;font-size:0.95rem;color:var(--phos-hot);list-style:none;outline:none}
.svg-bar{fill:var(--phos)}.svg-bar.medium{fill:var(--amber)}.svg-bar.high{fill:var(--red)}
code{background:var(--soft);padding:.1rem .4rem;border-radius:4px;font-size:.82rem;color:var(--phos-hot);font-family:ui-monospace,Menlo,monospace}
.cta-sm{display:inline-block;background:var(--phos);color:#06140a;padding:.6rem 1.2rem;font-weight:700;border-radius:10px;margin:.4rem .3rem .4rem 0}.cta-sm.outline{background:transparent;color:var(--phos);border:1px solid var(--phos)}
.footer{text-align:center;font-size:.7rem;color:var(--dim);padding:1.4rem;border-top:1px solid var(--line);margin-top:2rem}
.arch{font-family:ui-monospace,Menlo,monospace;font-size:.74rem;color:var(--phos-hot);background:var(--soft);padding:1rem;border:1px solid var(--line);border-radius:12px;overflow-x:auto;white-space:pre;line-height:1.4}
/* ── install panel ── */
.install-panel{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:.7rem 1rem;margin:.5rem 0;text-align:left}
.install-panel summary{cursor:pointer;font-size:.95rem;color:var(--phos-hot);list-style:none;outline:none;font-weight:700}
.install-panel summary::-webkit-details-marker{display:none}
.install-panel[open] summary{margin-bottom:0.6rem}
.install-panel .emoji{font-size:1.1rem;margin-right:0.3rem}
.install-panel ol{padding-left:1.1rem;line-height:1.5;font-size:0.85rem}
.install-panel .btn{display:inline-block;padding:0.45rem 0.75rem;margin:0.25rem 0.2rem 0.25rem 0;background:var(--purple);color:#fff;text-decoration:none;border-radius:5px;font-weight:bold;font-size:0.82rem}
.install-panel[open] summary{margin-bottom:.6rem}
.install-panel .emoji{font-size:1.1rem;margin-right:.3rem}
.install-panel ol{padding-left:1.1rem;line-height:1.5;font-size:.85rem}
.install-panel .btn{display:inline-block;padding:.45rem .8rem;margin:.25rem .2rem .25rem 0;background:var(--purple);color:#fff;text-decoration:none;border-radius:8px;font-weight:700;font-size:.82rem}
.install-panel .btn.alt{background:transparent;border:1px solid var(--purple);color:var(--purple)}
.install-panel code{background:rgba(0,0,0,0.4);padding:0.1rem 0.35rem;border-radius:3px;font-size:0.8rem;color:var(--phos-hot)}
.install-panel .note{color:var(--dim);font-size:0.78rem;margin-top:0.6rem;border-left:2px solid var(--amber);padding-left:0.6rem}
.install-panel img{max-width:100%;border-radius:5px;margin:0.4rem 0}
.install-panel pre{background:rgba(0,0,0,0.4);padding:0.5rem 0.7rem;border-radius:4px;overflow-x:auto;font-size:0.78rem;margin:0.4rem 0}
.install-panel code{background:var(--soft);padding:.1rem .35rem;border-radius:4px;font-size:.8rem;color:var(--phos-hot)}
.install-panel .note{color:var(--dim);font-size:.78rem;margin-top:.6rem;border-left:2px solid var(--amber);padding-left:.6rem}
.install-panel img{max-width:100%;border-radius:8px;margin:.4rem 0}
.install-panel pre{background:var(--soft);padding:.5rem .7rem;border-radius:8px;overflow-x:auto;font-size:.76rem;margin:.4rem 0}
/* ── "En savoir plus" fold ── */
.more{max-width:1080px;margin:0 auto;padding:0 1.5rem}
.more>summary{cursor:pointer;list-style:none;text-align:center;color:var(--purple);font-size:0.95rem;letter-spacing:0.05em;padding:0.9rem;border:1px dashed var(--purple);border-radius:8px;margin-bottom:1rem;transition:background .12s}
.more{max-width:760px;margin:0 auto;padding:0 1.1rem}
.more>summary{cursor:pointer;list-style:none;text-align:center;color:var(--purple);font-size:.92rem;letter-spacing:.04em;padding:.85rem;border:1px solid var(--line);border-radius:12px;margin-bottom:1rem;transition:background .12s}
.more>summary::-webkit-details-marker{display:none}
.more>summary:hover{background:rgba(158,118,255,0.1)}
.more[open]>summary{margin-bottom:1.6rem}
.more>summary:hover{background:rgba(158,118,255,.08)}
.more[open]>summary{margin-bottom:1.4rem}
.more>summary .chev{display:inline-block;transition:transform .2s}
.more[open]>summary .chev{transform:rotate(90deg)}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}
.v.tick{animation:flash 0.6s}
@keyframes flash{0%{color:var(--gold);transform:scale(1.15)}100%{color:var(--phos-hot);transform:scale(1)}}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
.v.tick{animation:flash .6s}@keyframes flash{0%{color:var(--gold);transform:scale(1.15)}100%{color:var(--phos-hot);transform:scale(1)}}
</style></head><body>
<div class=hero>
@ -123,19 +100,17 @@ code{background:#222;padding:0.1rem 0.4rem;border-radius:2px;font-size:0.85rem;c
<div class=ctas>
<a href="/wg/r3-install" class="cta go">✨ Protège-moi (R3)</a>
<a href="/social/me" class="cta alt">🕸️ Qui me piste ?</a>
<a href="/social/me{{ '?mh=' + mac_hash if mac_hash else '' }}" class="cta alt">🕸️ Qui me piste ?</a>
</div>
{# trimmed quick-nav — CA iPhone / CA Android / QR profil moved into the
per-platform install panel below (#543) #}
<div class=quicknav>
<a href="/wg/r3-install" class=qi title="Installer R3 WireGuard">
<span class=qi-emoji>🌐</span><span class=qi-label>R3 Install</span>
</a>
<a href="/report/me/html" class=qi title="Mon rapport live">
<a href="/report/me/html{{ '?mh=' + mac_hash if mac_hash else '' }}" class=qi title="Mon rapport live">
<span class=qi-emoji>📊</span><span class=qi-label>Mon rapport</span>
</a>
<a href="/social/me" class=qi title="Cartographie sociale — qui me piste, où ?">
<a href="/social/me{{ '?mh=' + mac_hash if mac_hash else '' }}" class=qi title="Cartographie sociale — qui me piste, où ?">
<span class=qi-emoji>🕸️</span><span class=qi-label>Ma carto</span>
</a>
<a href="https://github.com/CyberMind-FR/secubox-deb/wiki/R3-WireGuard-install" class=qi title="Wiki R3 multi-OS">
@ -151,12 +126,12 @@ code{background:#222;padding:0.1rem 0.4rem;border-radius:2px;font-size:0.85rem;c
<div class=container>
<div class=section style="margin-bottom:1.5rem">
<h2>📥 Installe en 1 tap</h2>
<p style="font-size:0.85rem;color:var(--dim);margin-bottom:0.8rem">
<p class=help style="margin-bottom:.8rem">
On a détecté <code>{{ install_platform }}</code> — le panneau adapté est ouvert.
Le CA, le QR et le profil sont dedans. Autre appareil ? Déplie le bon panneau.
</p>
{{ install_panels | safe }}
<p style="margin-top:0.8rem;font-size:0.78rem;color:var(--dim)">
<p class=help style="margin-top:.8rem">
R3 marche hors-cabine (4G/5G, autre WiFi), couvre tout le HTTPS, et se révoque
à tout moment. Page standalone : <a href=/wg/onboard>/wg/onboard</a>.
</p>
@ -170,8 +145,8 @@ code{background:#222;padding:0.1rem 0.4rem;border-radius:2px;font-size:0.85rem;c
{# ── KPI live (auto-refresh 5s via /cumulative-stats.json) ── #}
<div class=section>
<h2>📊 Cabine en chiffres
<span id=live-badge style="font-size:0.7rem;background:var(--red);color:#fff;padding:0.15rem 0.5rem;border-radius:99px;margin-left:0.5rem;letter-spacing:0.05em;animation:pulse 1.5s infinite">🔴 LIVE</span>
<span id=live-stamp style="font-size:0.7rem;color:var(--dim);margin-left:0.4rem">·</span>
<span id=live-badge style="font-size:.7rem;background:var(--red);color:#fff;padding:.15rem .5rem;border-radius:99px;margin-left:.5rem;letter-spacing:.04em;animation:pulse 1.5s infinite">🔴 LIVE</span>
<span id=live-stamp style="font-size:.7rem;color:var(--dim);margin-left:.4rem">·</span>
</h2>
<div class="grid grid-4">
<div class=kpi><span class=v data-live=sessions.last_7d>{{ stats.sessions.last_7d if stats.sessions else 0 }}</span><span class=l>👥 sessions uniques (7j)</span></div>
@ -193,16 +168,16 @@ code{background:#222;padding:0.1rem 0.4rem;border-radius:2px;font-size:0.85rem;c
<div class="card purple" style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap">
<div style="font-size:2.4rem;flex-shrink:0" id=cert-probe-emoji>❔</div>
<div style="flex:1;min-width:220px">
<p id=cert-probe-text style="font-size:0.92rem;line-height:1.5">
<p id=cert-probe-text style="font-size:.92rem;line-height:1.5">
Clique sur <b>Tester</b> pour vérifier que ton CA R3 est bien installé sur ton device
(depuis le tunnel WireGuard). Le test télécharge une image HTTPS et vérifie le succès.
</p>
<p style="font-size:0.75rem;color:var(--dim);margin-top:0.3rem">
<p class=help style="margin-top:.3rem">
Empreinte SHA1 CA R3 (à vérifier dans Réglages iPhone) :
<code id=cert-fp-r3 style="font-size:0.7rem">…</code>
<code id=cert-fp-r3 style="font-size:.7rem">…</code>
</p>
</div>
<button id=cert-probe-btn style="background:var(--purple);color:#0a0a0f;border:0;padding:0.7rem 1.2rem;font-weight:bold;border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9rem">🔬 Tester</button>
<button id=cert-probe-btn style="background:var(--purple);color:#06140a;border:0;padding:.7rem 1.2rem;font-weight:700;border-radius:10px;cursor:pointer;font-family:inherit;font-size:.9rem">🔬 Tester</button>
</div>
</div>
@ -215,7 +190,7 @@ code{background:#222;padding:0.1rem 0.4rem;border-radius:2px;font-size:0.85rem;c
niveau d'analyse, tu obtiens un rapport détaillé sur les apps, trackers, certificats,
et risques observés pendant ta session.
</p>
<p style="margin-top:0.6rem">
<p style="margin-top:.6rem">
<b>Conçu par CyberMind / Gérald Kerma</b> à Notre-Dame-du-Cruet (Savoie). Conforme
<b>CSPN ANSSI</b> + <b>LCEN</b> : consentement explicite, hash MAC quotidien rotatif,
données effacées après 24h, aucun envoi externe.
@ -248,10 +223,10 @@ code{background:#222;padding:0.1rem 0.4rem;border-radius:2px;font-size:0.85rem;c
</div>
</div>
<div class="level r3">
<span class=emj>🌐</span>
<span class=emj>🧅</span>
<div class=body>
<b>R3 — WireGuard portable<span class=tag-new>NOUVEAU</span></b>
<p class=desc>VPN tunnel mitm. Bandeau sur TOUT (HTTPS + QUIC). <b>Marche hors VILLAGE3B</b> (4G/5G, autre WiFi). Profile install 1 tap via QR.</p>
<p class=desc>VPN tunnel mitm. Bandeau sur TOUT (HTTPS + QUIC). <b>Marche hors VILLAGE3B</b> (4G/5G, autre WiFi). Option sortie 🧅 Tor anonymisée. Profile install 1 tap via QR.</p>
</div>
</div>
</div>
@ -269,14 +244,15 @@ code{background:#222;padding:0.1rem 0.4rem;border-radius:2px;font-size:0.85rem;c
{% set mpct = (risk.medium * 100 / total) | round(0) | int %}
{% set hpct = (risk.high * 100 / total) | round(0) | int %}
<svg class=chart viewBox="0 0 300 60" xmlns="http://www.w3.org/2000/svg">
<rect x=0 y=20 width="{{ lpct * 3 }}" height=20 class=svg-bar/>
<rect x=0 y=20 width="{{ lpct * 3 }}" height=20 rx=4 class=svg-bar/>
<rect x="{{ lpct * 3 }}" y=20 width="{{ mpct * 3 }}" height=20 class="svg-bar medium"/>
<rect x="{{ (lpct + mpct) * 3 }}" y=20 width="{{ hpct * 3 }}" height=20 class="svg-bar high"/>
<text x=10 y=15 fill="#00ff55" font-family=monospace font-size=10>🟢 {{ lpct }}% LOW</text>
<text x="{{ lpct * 3 + 10 }}" y=15 fill="#ffb347" font-family=monospace font-size=10>🟡 {{ mpct }}% MED</text>
<text x="{{ (lpct + mpct) * 3 + 10 }}" y=15 fill="#ff4466" font-family=monospace font-size=10>🔴 {{ hpct }}% HI</text>
<text x=10 y=55 fill="#666" font-family=monospace font-size=8>{{ total }} sessions analysées</text>
<rect x="{{ (lpct + mpct) * 3 }}" y=20 width="{{ hpct * 3 }}" height=20 rx=4 class="svg-bar high"/>
<text x=10 y=15 fill="#00ff55" font-size=10>🟢 {{ lpct }}% LOW</text>
<text x="{{ lpct * 3 + 10 }}" y=15 fill="#ffb347" font-size=10>🟡 {{ mpct }}% MED</text>
<text x="{{ (lpct + mpct) * 3 + 10 }}" y=15 fill="#ff4466" font-size=10>🔴 {{ hpct }}% HI</text>
<text x=10 y=55 fill="#5a6b60" font-size=8>{{ total }} sessions analysées</text>
</svg>
<p class=help>La plupart des sessions sont à faible risque.</p>
</div>
<div class=card>
<h3>🛡 Niveau d'opt-in choisi par les visiteurs</h3>
@ -284,15 +260,16 @@ code{background:#222;padding:0.1rem 0.4rem;border-radius:2px;font-size:0.85rem;c
{% set ltotal = (lvl.r0 + lvl.r1 + lvl.r2 + lvl.r3) or 1 %}
<svg class=chart viewBox="0 0 300 90" xmlns="http://www.w3.org/2000/svg">
{% set ws = [(lvl.r0 * 300 / ltotal)|round|int, (lvl.r1 * 300 / ltotal)|round|int, (lvl.r2 * 300 / ltotal)|round|int, (lvl.r3 * 300 / ltotal)|round|int] %}
<rect x=0 y=10 width="{{ ws[0] }}" height=15 fill="#666"/>
<rect x=0 y=30 width="{{ ws[1] }}" height=15 fill="#00dd44"/>
<rect x=0 y=50 width="{{ ws[2] }}" height=15 fill="#ffb347"/>
<rect x=0 y=70 width="{{ ws[3] }}" height=15 fill="#9e76ff"/>
<text x="{{ ws[0] + 5 }}" y=22 fill="#888" font-family=monospace font-size=10>🌐 R0 ({{ lvl.r0 }})</text>
<text x="{{ ws[1] + 5 }}" y=42 fill="#00ff55" font-family=monospace font-size=10>🛡 R1 ({{ lvl.r1 }})</text>
<text x="{{ ws[2] + 5 }}" y=62 fill="#ffd6a0" font-family=monospace font-size=10>🔍 R2 ({{ lvl.r2 }})</text>
<text x="{{ ws[3] + 5 }}" y=82 fill="#cbb6ff" font-family=monospace font-size=10>🌐 R3 ({{ lvl.r3 }})</text>
<rect x=0 y=10 width="{{ ws[0] }}" height=15 rx=4 fill="#5a6b60"/>
<rect x=0 y=30 width="{{ ws[1] }}" height=15 rx=4 fill="#00dd44"/>
<rect x=0 y=50 width="{{ ws[2] }}" height=15 rx=4 fill="#ffb347"/>
<rect x=0 y=70 width="{{ ws[3] }}" height=15 rx=4 fill="#9e76ff"/>
<text x="{{ ws[0] + 5 }}" y=22 fill="#888" font-size=10>🌐 R0 ({{ lvl.r0 }})</text>
<text x="{{ ws[1] + 5 }}" y=42 fill="#00ff55" font-size=10>🛡 R1 ({{ lvl.r1 }})</text>
<text x="{{ ws[2] + 5 }}" y=62 fill="#ffd6a0" font-size=10>🔍 R2 ({{ lvl.r2 }})</text>
<text x="{{ ws[3] + 5 }}" y=82 fill="#cbb6ff" font-size=10>🧅 R3 ({{ lvl.r3 }})</text>
</svg>
<p class=help>R1 (analyse passive) est le choix le plus courant.</p>
</div>
</div>
</div>
@ -303,7 +280,7 @@ code{background:#222;padding:0.1rem 0.4rem;border-radius:2px;font-size:0.85rem;c
<div class=arch>
LXC mitmproxy 10.100.0.60 WAF (vhosts CyberMind)
LXC toolbox-mitm 10.100.0.61 R1/R2 transparent
LXC toolbox-mitm-wg 10.100.0.62 R3 WireGuard
LXC toolbox-mitm-wg 10.100.0.62 R3 WireGuard (+🧅 Tor)
CAs séparées · addons partagés · DB unifiée
@ -318,7 +295,7 @@ LXC toolbox-mitm-wg 10.100.0.62 R3 WireGuard
Le code est <b>publié intégralement</b> sous licence <b>CyberMind Source-Disclosed v1.0</b>
(audit citoyen possible, droits d'usage régis par licence CMSD). Pas de boîte noire.
</p>
<div style="margin-top:0.6rem">
<div style="margin-top:.6rem">
<a href="https://github.com/CyberMind-FR/secubox-deb" class=cta-sm>📂 Code source GitHub</a>
<a href="https://github.com/CyberMind-FR/secubox-deb/blob/master/LICENCE-CMSD-1.0.md" class="cta-sm outline">📜 Licence CMSD-1.0</a>
</div>
@ -326,11 +303,11 @@ LXC toolbox-mitm-wg 10.100.0.62 R3 WireGuard
{# ── Contact ── #}
<div class=section>
<h2>📡 Contact & soutiens</h2>
<h2>📡 Contact &amp; soutiens</h2>
<div class="grid grid-2">
<div class=card>
<h3>💚 Soutenir le projet</h3>
<ul style="list-style:none;padding-left:0;font-size:0.85rem">
<ul style="list-style:none;padding-left:0;font-size:.85rem">
<li>💰 Don récurrent : <a href="https://liberapay.com/cybermind">liberapay.com/cybermind</a></li>
<li>💳 Don ponctuel : <a href="https://cybermind.fr/don">cybermind.fr/don</a></li>
<li>📧 Support : <a href="mailto:support@cybermind.fr">support@cybermind.fr</a></li>
@ -338,7 +315,7 @@ LXC toolbox-mitm-wg 10.100.0.62 R3 WireGuard
</div>
<div class="card purple">
<h3>🏢 Déploiement collectivité</h3>
<ul style="list-style:none;padding-left:0;font-size:0.85rem">
<ul style="list-style:none;padding-left:0;font-size:.85rem">
<li>📡 Borne grand public : <a href="mailto:gondwana@cybermind.fr">gondwana@cybermind.fr</a></li>
<li>🎓 Formation cybersécurité : <a href="mailto:contact@cybermind.fr">contact@cybermind.fr</a></li>
<li>🛡 Audit SecuBox premium : <a href="mailto:contact@cybermind.fr">contact@cybermind.fr</a></li>
@ -376,7 +353,6 @@ LXC toolbox-mitm-wg 10.100.0.62 R3 WireGuard
var ss = String(d.getSeconds()).padStart(2,'0');
return 'maj ' + hh+':'+mm+':'+ss;
}
// count-up: animate each KPI from 0 → its server-rendered value, once.
function countUp(el, target){
var start = 0, dur = 900, t0 = null;
function step(ts){
@ -403,7 +379,7 @@ LXC toolbox-mitm-wg 10.100.0.62 R3 WireGuard
if (prev !== next) {
el.textContent = next;
el.classList.remove('tick');
void el.offsetWidth; // force reflow
void el.offsetWidth;
el.classList.add('tick');
}
});
@ -449,8 +425,8 @@ LXC toolbox-mitm-wg 10.100.0.62 R3 WireGuard
} else {
emj.textContent = '🔴';
txt.innerHTML = '<b>Tunnel R3 actif mais CA R3 NON trusté</b> — HTTPS casse. ' +
'Installe le <a href=/wg/ca.mobileconfig style="color:var(--orange)">profil CA R3 iPhone</a> ' +
'ou le <a href=/wg/ca.pem style="color:var(--orange)">ca.pem Android/PC</a>.';
'Installe le <a href=/wg/ca.mobileconfig style="color:var(--amber)">profil CA R3 iPhone</a> ' +
'ou le <a href=/wg/ca.pem style="color:var(--amber)">ca.pem Android/PC</a>.';
}
}

View File

@ -1,662 +1,325 @@
{# SPDX-License-Identifier: LicenseRef-CMSD-1.0 #}
{# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr> #}
{# #683 report redesign — verdict-first, graphs, plain-language helpers, deep
technical cards collapsed into <details>. Same data model as before. #}
<!DOCTYPE html>
<html lang="fr"><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta http-equiv="refresh" content="15">
{# Phase 3 (#492) : PWA tags for iOS Add-to-Home-Screen webclip experience #}
<meta http-equiv="refresh" content="20">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="ToolBoX Cabine">
<meta name="theme-color" content="#0a0a0f">
<title>Mon rapport Gondwana ToolBoX — live</title>
<title>Mon rapport — VILLAGE3B</title>
<style>
:root{--bg:#0a0a0f;--phos:#00dd44;--phos-hot:#00ff55;--dim:#006622;--text:#e8e6d9;--red:#ff4466;--amber:#ffb347}
:root{--bg:#0a0a0f;--panel:#11131a;--phos:#00dd44;--phos-hot:#00ff55;--dim:#5a6b60;--line:#1e2630;--text:#e8e6d9;--red:#ff4466;--amber:#ffb347;--violet:#9e76ff;--blue:#66bbff}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Courier New',Menlo,monospace;background:var(--bg);color:var(--text);padding:1rem;max-width:760px;margin:auto;line-height:1.5}
h1{color:var(--phos-hot);text-shadow:0 0 6px var(--phos);font-size:1.6rem;margin-bottom:0.3rem;letter-spacing:0.05em}
.sub{color:var(--dim);font-size:0.85rem;margin-bottom:1rem;letter-spacing:0.05em}
.card{border:1px solid var(--dim);background:rgba(0,221,68,0.03);padding:0.9rem 1rem;margin-bottom:1rem}
.card h2{color:var(--phos-hot);text-shadow:0 0 4px var(--phos);font-size:0.95rem;margin-bottom:0.5rem;border-bottom:1px solid var(--dim);padding-bottom:0.3rem;letter-spacing:0.05em}
.kv{display:grid;grid-template-columns:auto 1fr;gap:0.2rem 0.8rem;font-size:0.85rem}
.kv .k{color:var(--dim)}
.kv .v{color:var(--phos);text-shadow:0 0 4px var(--phos)}
ul{list-style:none;padding-left:0.6rem}
li{padding:0.15rem 0;font-size:0.85rem}
li::before{content:"▸ ";color:var(--phos);text-shadow:0 0 4px var(--phos)}
.score{display:inline-block;padding:0.3rem 1rem;font-size:1.2rem;font-weight:bold;border:2px solid;border-radius:4px}
.score.low{color:var(--phos-hot);border-color:var(--phos);text-shadow:0 0 6px var(--phos)}
.score.med{color:var(--amber);border-color:var(--amber);text-shadow:0 0 6px var(--amber)}
.score.high{color:var(--red);border-color:var(--red);text-shadow:0 0 6px var(--red)}
.pin{color:var(--amber)}
.url{font-family:monospace;font-size:0.78rem;color:var(--text);background:rgba(0,221,68,0.05);padding:0.15rem 0.4rem;border-radius:2px;margin:0.1rem 0;display:inline-block;max-width:100%;overflow-wrap:break-word;word-break:break-all}
.actions{text-align:center;margin:1.5rem 0}
.actions a{display:inline-block;margin:0 0.3rem;padding:0.5rem 1rem;border:1px solid var(--phos);color:var(--phos-hot);text-decoration:none;text-shadow:0 0 4px var(--phos);font-size:0.85rem}
.actions a:hover{background:rgba(0,221,68,0.1)}
.refresh{text-align:center;font-size:0.7rem;color:var(--dim);margin-top:1rem;font-style:italic}
.footer{text-align:center;font-size:0.65rem;color:var(--dim);margin-top:1.5rem;border-top:1px solid var(--dim);padding-top:0.6rem}
body{font-family:system-ui,-apple-system,'Segoe UI',sans-serif;background:var(--bg);color:var(--text);padding:1rem;max-width:740px;margin:auto;line-height:1.5}
h1{color:var(--phos-hot);font-size:1.35rem;letter-spacing:.04em;display:flex;align-items:center;gap:.4rem}
.sub{color:var(--dim);font-size:.82rem;margin-bottom:1.1rem}
.help{color:var(--dim);font-size:.78rem;font-style:italic;margin-top:.25rem}
.card{border:1px solid var(--line);background:var(--panel);border-radius:12px;padding:1rem 1.1rem;margin-bottom:1rem}
.card h2{color:var(--phos-hot);font-size:.95rem;margin-bottom:.5rem;letter-spacing:.03em}
/* ── verdict hero ── */
.hero{text-align:center;background:radial-gradient(120% 120% at 50% 0%,rgba(0,221,68,.10),rgba(110,64,201,.05) 70%,transparent);border-color:var(--phos)}
.gauge{width:170px;height:170px;border-radius:50%;margin:.4rem auto;display:flex;align-items:center;justify-content:center}
.gauge-hole{width:124px;height:124px;border-radius:50%;background:var(--bg);display:flex;flex-direction:column;align-items:center;justify-content:center}
.gauge-num{font-size:2.6rem;font-weight:800;line-height:1}
.gauge-max{font-size:.8rem;color:var(--dim)}
.verdict{font-size:1.15rem;font-weight:700;margin-top:.3rem}
/* ── KPI row ── */
.kpis{display:grid;grid-template-columns:repeat(3,1fr);gap:.5rem;margin-top:.4rem}
.kpi{background:#0d0f15;border:1px solid var(--line);border-radius:10px;padding:.6rem .3rem;text-align:center}
.kpi .e{font-size:1.15rem}
.kpi .n{font-size:1.3rem;font-weight:800;color:var(--phos-hot)}
.kpi .l{font-size:.62rem;color:var(--dim);text-transform:uppercase;letter-spacing:.04em}
/* ── graphs ── */
.graphs{display:grid;grid-template-columns:1fr 1fr;gap:1rem}
@media(max-width:560px){.graphs{grid-template-columns:1fr}.kpis{grid-template-columns:repeat(2,1fr)}}
.donut-wrap{display:flex;gap:.8rem;align-items:center}
.donut{width:96px;height:96px;border-radius:50%;flex:0 0 auto}
.donut-hole{width:54px;height:54px;border-radius:50%;background:var(--panel);margin:21px;display:flex;align-items:center;justify-content:center;font-size:.7rem;color:var(--dim);text-align:center}
.legend{font-size:.78rem;display:flex;flex-direction:column;gap:.2rem;min-width:0}
.legend .row{display:flex;align-items:center;gap:.35rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.dot{width:.6rem;height:.6rem;border-radius:50%;flex:0 0 auto}
.bar-row{display:grid;grid-template-columns:5.5rem 1fr 2rem;gap:.45rem;align-items:center;font-size:.78rem;margin:.2rem 0}
.bar-lbl{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.bar-track{background:#0d0f15;border-radius:99px;height:.7rem;overflow:hidden}
.bar-fill{display:block;height:100%;border-radius:99px;background:linear-gradient(90deg,var(--phos),var(--phos-hot))}
.bar-val{text-align:right;color:var(--phos);font-weight:700}
.empty{color:var(--dim);font-size:.8rem;font-style:italic;padding:.4rem 0}
/* ── details ── */
details{border:1px solid var(--line);background:var(--panel);border-radius:12px;margin-bottom:1rem;overflow:hidden}
details>summary{cursor:pointer;padding:.85rem 1.1rem;font-weight:700;color:var(--phos-hot);font-size:.92rem;list-style:none}
details>summary::-webkit-details-marker{display:none}
details>summary::before{content:"▸ ";color:var(--phos)}
details[open]>summary::before{content:"▾ "}
details .inner{padding:0 1.1rem 1rem}
.kv{display:grid;grid-template-columns:auto 1fr;gap:.2rem .8rem;font-size:.84rem}
.kv .k{color:var(--dim)}.kv .v{color:var(--phos)}
table{width:100%;font-size:.8rem;border-collapse:collapse}
th{color:var(--dim);text-align:left;font-weight:600;border-bottom:1px solid var(--line);padding:.25rem .3rem}
td{padding:.2rem .3rem;border-bottom:1px solid rgba(255,255,255,.03)}
code{font-family:ui-monospace,Menlo,monospace;font-size:.74rem;color:var(--text);word-break:break-all}
ul{list-style:none;padding-left:.2rem}li{padding:.12rem 0;font-size:.82rem}li::before{content:"▸ ";color:var(--phos)}
.pin h2,.pin{color:var(--amber)}
.lvl{display:grid;grid-template-columns:repeat(3,1fr);gap:.4rem;margin-top:.4rem}
.lvl button{padding:.55rem;cursor:pointer;font-family:inherit;font-size:.85rem;border-radius:8px;background:transparent;color:var(--text);border:1px solid var(--line)}
.lvl button.on{border-width:2px;font-weight:700}
.actions{display:flex;gap:.5rem;flex-wrap:wrap;justify-content:center;margin:1.2rem 0}
.actions a{padding:.55rem 1rem;border:1px solid var(--phos);color:var(--phos-hot);text-decoration:none;border-radius:8px;font-size:.85rem}
.footer{text-align:center;font-size:.66rem;color:var(--dim);margin-top:1.4rem;border-top:1px solid var(--line);padding-top:.7rem}
.url{font-family:ui-monospace,monospace;font-size:.72rem;background:#0d0f15;padding:.12rem .35rem;border-radius:4px;margin:.1rem 0;display:block;word-break:break-all}
</style></head>
<body>
<h1>📡 GONDWANA TOOLBOX</h1>
<p class="sub">// Rapport live — Cabine numérique VILLAGE3B</p>
{# Phase 3 (#492) : hero widgets — same shape as PDF #}
{% set m = metrics or {} %}
{% set sc = risk_score or 0 %}
{% set rl = risk_label or 'LOW' %}
<div class="card" style="background:linear-gradient(135deg,rgba(0,221,68,0.08),rgba(110,64,201,0.05));border-color:var(--phos)">
<h2 style="display:flex;justify-content:space-between;align-items:center">
📊 Ta session VILLAGE3B
<span style="font-size:0.75rem;padding:0.3rem 0.8rem;border-radius:99px;
background:{% if sc < 30 %}#00cc44{% elif sc < 70 %}#ffb347{% else %}#ff4466{% endif %};
color:#0a0a0f;font-weight:bold">
{% if sc < 30 %}🟢{% elif sc < 70 %}🟡{% else %}🔴{% endif %} {{ rl }} {{ sc }}/100
</span>
</h2>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:0.5rem;margin-top:0.8rem">
{% for emoji, val, lbl in [
('🌐', m.connections|default(0), 'connexions'),
('📡', m.unique_hosts|default(0), 'hôtes uniques'),
('✅', m.successful|default(0), 'OK 2xx/3xx'),
('🔒', m.tls_pinned|default(0), 'cert-pinning')
] %}
<div style="padding:0.6rem;background:rgba(0,0,0,0.3);border-radius:4px;text-align:center">
<div style="font-size:1.2rem">{{ emoji }}</div>
<div style="font-size:1.3rem;font-weight:bold;color:var(--phos-hot);text-shadow:0 0 4px var(--phos)">{{ val }}</div>
<div style="font-size:0.65rem;color:var(--dim)">{{ lbl }}</div>
</div>
{% endfor %}
</div>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:0.5rem;margin-top:0.5rem">
{% set dpi_cls = dpi_classified or {} %}
{% set cookies_p = cookies_providers or [] %}
{% set geo_h = geo_top_hosts or [] %}
{% set n_apps = (dpi_cls.top_apps|default([])|selectattr('app','ne','?')|list|length) %}
{% set n_trackers = (cookies_p|map(attribute='count')|sum) %}
{% set countries = geo_h|map(attribute='country')|reject('equalto','')|list %}
{% set n_countries = countries|unique|list|length %}
<div style="padding:0.6rem;background:rgba(25,25,45,0.5);border-radius:4px;text-align:center">
<div style="font-size:1.2rem">📺</div>
<div style="font-size:1.3rem;font-weight:bold;color:#9e76ff">{{ n_apps }}</div>
<div style="font-size:0.65rem;color:var(--dim)">apps détectées</div>
</div>
<div style="padding:0.6rem;background:rgba(45,25,35,0.5);border-radius:4px;text-align:center">
<div style="font-size:1.2rem">🍪</div>
<div style="font-size:1.3rem;font-weight:bold;color:#ff8866">{{ n_trackers }}</div>
<div style="font-size:0.65rem;color:var(--dim)">trackers</div>
</div>
<div style="padding:0.6rem;background:rgba(25,40,35,0.5);border-radius:4px;text-align:center">
<div style="font-size:1.2rem">🌍</div>
<div style="font-size:1.3rem;font-weight:bold;color:#66bbff">{{ n_countries }}</div>
<div style="font-size:0.65rem;color:var(--dim)">pays/ASN</div>
</div>
<div style="padding:0.6rem;background:rgba(60,50,15,0.5);border-radius:4px;text-align:center">
<div style="font-size:1.2rem">{% if sc < 30 %}🟢{% elif sc < 70 %}🟡{% else %}🔴{% endif %}</div>
<div style="font-size:1.3rem;font-weight:bold;color:{% if sc < 30 %}var(--phos-hot){% elif sc < 70 %}#ffb347{% else %}#ff4466{% endif %}">{{ sc }}</div>
<div style="font-size:0.65rem;color:var(--dim)">risque /100</div>
</div>
</div>
{# 9th widget (poster sync #497) : 📱 empreinte device — full-width below #}
{% set _avatar = avatar_analysis or {} %}
{% set _dev_emj = _avatar.most_common_emoji or '❔' %}
{% set _dev_label = _avatar.most_common or 'unknown' %}
{% set _devices = _avatar.devices or {} %}
{% set _dev_info = _devices.get(_dev_label, {}) %}
{% set _os_label = _dev_info.os_label or _dev_label %}
{% set _browsers = _avatar.browsers or {} %}
<div style="margin-top:0.5rem;padding:0.7rem;background:rgba(45,25,60,0.5);border-radius:4px;text-align:center">
<div style="font-size:1.4rem">📱 <b style="color:#cbb6ff">{{ _dev_emj }} Empreinte device</b></div>
<div style="font-size:0.92rem;color:var(--phos-hot);margin-top:0.3rem;text-shadow:0 0 3px var(--phos)">
{{ _os_label }}
{% for br, info in _browsers.items() if br != 'unknown' %}
· {{ info.emoji or '' }} {{ info.label or br }}
{% endfor %}
</div>
<div style="font-size:0.65rem;color:var(--dim);margin-top:0.2rem">{{ _avatar.raw_count or 0 }} UAs distincts observés</div>
</div>
{# Top device + app + ASN line #}
{% set avatar = avatar_analysis or {} %}
{% set top_dev = avatar.most_common or '?' %}
{% set top_dev_emj = avatar.most_common_emoji or '' %}
{% set top_app = (dpi_cls.top_apps and dpi_cls.top_apps[0]) or {} %}
{% set top_geo = (geo_h and geo_h[0]) or {} %}
{% set top_asn_raw = top_geo.asn_org if top_geo.asn_org else '?' %}
<p style="font-size:0.78rem;color:var(--dim);margin-top:0.6rem;text-align:center;padding-top:0.5rem;border-top:1px solid var(--dim)">
Top device : {{ top_dev_emj }} {{ top_dev }} ·
Top app : {{ top_app.emoji|default('') }} {{ top_app.app|default('?') }} ·
Top ASN : {{ top_geo.flag|default('') }} {{ top_asn_raw[:30] }}
</p>
</div>
{% set sc = risk_score|default(0) %}
{% set rl = risk_label|default('LOW') %}
{% set ch = charts or {} %}
{% set gcol = 'var(--phos-hot)' if sc < 30 else ('var(--amber)' if sc < 70 else 'var(--red)') %}
{% set palette = ['#00dd44','#9e76ff','#ff8866','#66bbff','#ffb347','#ff4466'] %}
{% set dpi_cls = dpi_classified or {} %}
{% set cookies_p = cookies_providers or [] %}
{% set geo_h = geo_top_hosts or [] %}
{% set n_apps = (dpi_cls.top_apps|default([])|selectattr('app','ne','?')|list|length) %}
{% set n_trackers = (cookies_p|map(attribute='count')|sum) %}
{% set n_countries = (geo_h|map(attribute='country')|reject('equalto','')|list|unique|list|length) %}
{% set _avatar = avatar_analysis or {} %}
{# Phase 3 (#492) : filtering compromissions visibility #}
{% set t = transparency|default({}) %}
{% set sens = t.get('sensitivity') %}
<div class="card" style="background:rgba(255,68,102,0.06);border-color:var(--red)">
<h2 style="color:var(--red)">🚨 Filtering compromissions actif</h2>
<div class="kv" style="font-size:0.82rem">
<span class="k">Sensibilité actuelle</span>
<span class="v">{{ sens.label|default('—') if sens else '—' }}</span>
<span class="k">Description</span>
{% set sens_desc = (sens.description if sens and sens.description else 'Engine pas chargé') %}
<span class="v" style="font-size:0.75rem">{{ sens_desc[:90] }}</span>
<span class="k">🔍 Threat-intel feeds</span>
<span class="v">{{ (threat_intel_matches|default([]))|length }} matches actifs</span>
<span class="k">🎲 DGA candidates</span>
<span class="v">{{ (dga_candidates|default([]))|length }} hôtes suspects</span>
<span class="k">📡 Beaconing patterns</span>
<span class="v">{{ (beaconing_candidates|default([]))|length }} détectés</span>
<span class="k">🚫 Connexions bloquées</span>
<span class="v">{{ t.get('attempts', {}).get('blocked', 0) }} <span style="font-size:0.7rem;color:var(--dim)">(Phase 4 : observation only)</span></span>
</div>
<p style="font-size:0.7rem;color:var(--dim);margin-top:0.5rem;font-style:italic">
Le moteur détecte mais ne bloque pas encore — passive transparency. La règle engine
décidera blocage actif quand R3 / Phase 4 sera wired.
</p>
</div>
<h1>👁️ VILLAGE3B <span style="font-size:.8rem;color:var(--dim);font-weight:400">· mon rapport</span></h1>
<p class="sub">Diagnostic live de ce que ton appareil envoie sur le réseau · anonyme · se rafraîchit tout seul</p>
{# Phase 3 (#492) : welcome / switch confirmation banner — uses server-side
current_level (NOT request_args.level which is stale after refresh). #}
{% if request_args and (request_args.get('welcome') or request_args.get('switched')) %}
<div class="card" style="background:rgba(0,221,68,0.08);border-color:var(--phos)">
<h2 style="color:var(--phos-hot)">
{% if request_args.get('switched') %}🔄 Niveau changé{% else %}🎉 Bienvenue !{% endif %}
</h2>
<p style="font-size:0.85rem">
Tu es maintenant en mode
<b style="color:var(--phos-hot)">
{% if current_level == 'r0' %}🌐 R0 — Bypass complet (aucune analyse)
{% elif current_level == 'r2' %}🔍 R2 — Analyse + bandeau Safari
{% else %}🛡 R1 — Analyse passive recommandée{% endif %}
</b>.
Tu peux maintenant <a href="http://captive.apple.com/hotspot-detect.html" target="_blank" style="color:var(--phos-hot);text-decoration:underline">surfer normalement</a> — ce rapport se met à jour toutes les 15s.
</p>
<div class="card" style="border-color:var(--phos)">
<b style="color:var(--phos-hot)">{% if request_args.get('switched') %}🔄 Niveau changé{% else %}🎉 Bienvenue !{% endif %}</b> —
tu es en mode
{% if current_level == 'r0' %}🌐 R0 (aucune analyse){% elif current_level == 'r2' %}🔍 R2 (analyse + bandeau){% elif current_level == 'r3' %}🧅 R3 (tunnel){% else %}🛡 R1 (analyse passive){% endif %}.
Tu peux surfer normalement.
</div>
{% endif %}
{# Phase 3 (#492) : level switcher with active highlight from server-side
current_level. Disables button if user clicks their own level (no-op). #}
{# ── VERDICT HERO : score gauge + plain verdict ── #}
<div class="card hero">
<div class="gauge" style="background:conic-gradient({{ gcol }} {{ sc }}%, #1a1a22 {{ sc }}% 100%)">
<div class="gauge-hole">
<span class="gauge-num" style="color:{{ gcol }}">{{ sc }}</span>
<span class="gauge-max">/ 100</span>
</div>
</div>
<div class="verdict" style="color:{{ gcol }}">
{% if sc < 30 %}🟢 Tout va bien — {{ rl }}{% elif sc < 70 %}🟡 À surveiller — {{ rl }}{% else %}🔴 Attention — {{ rl }}{% endif %}
</div>
<p class="help">Score de risque de ton appareil. Plus il est <b>bas</b>, mieux tu es protégé.</p>
{% if risk_explanation %}<p style="font-size:.85rem;margin-top:.5rem">{{ risk_explanation }}</p>{% endif %}
</div>
{# ── KPI row ── #}
<div class="kpis">
<div class="kpi"><div class="e">🌐</div><div class="n">{{ m.connections|default(0) }}</div><div class="l">connexions</div></div>
<div class="kpi"><div class="e">📡</div><div class="n">{{ m.unique_hosts|default(0) }}</div><div class="l">hôtes</div></div>
<div class="kpi"><div class="e">🍪</div><div class="n">{{ n_trackers }}</div><div class="l">trackers</div></div>
<div class="kpi"><div class="e">🌍</div><div class="n">{{ n_countries }}</div><div class="l">pays</div></div>
<div class="kpi"><div class="e">📺</div><div class="n">{{ n_apps }}</div><div class="l">apps</div></div>
<div class="kpi"><div class="e">🔒</div><div class="n">{{ m.tls_pinned|default(0) }}</div><div class="l">cert-pin</div></div>
</div>
<p class="help" style="text-align:center;margin-bottom:1rem">Ton appareil a contacté {{ m.unique_hosts|default(0) }} serveurs dans {{ n_countries }} pays, avec {{ n_trackers }} traceurs repérés.</p>
{# ── GRAPHS ── #}
<div class="card">
<h2>🔀 Mon niveau d'opt-in</h2>
<p style="font-size:0.78rem;color:var(--dim);margin-bottom:0.5rem">
📍 Tu es actuellement en
<b style="color:{% if current_level == 'r0' %}var(--text){% elif current_level == 'r2' %}#ffd6a0{% else %}var(--phos-hot){% endif %}">
{% if current_level == 'r0' %}🌐 R0 — Bypass complet
{% elif current_level == 'r2' %}🔍 R2 — Analyse + bandeau
{% else %}🛡 R1 — Analyse passive{% endif %}
</b>
· clique sur un autre niveau pour switcher
</p>
<form method="POST" action="/change-level" style="display:grid;grid-template-columns:repeat(3,1fr);gap:0.4rem">
<button type="submit" name="level" value="r0"
style="padding:0.5rem;cursor:pointer;font-family:inherit;font-size:0.85rem;
{% if current_level == 'r0' %}
background:rgba(255,255,255,0.08);color:var(--text);border:2px solid var(--text);font-weight:bold;
{% else %}
background:transparent;color:var(--text);border:1px solid var(--dim);
{% endif %}">
{% if current_level == 'r0' %}✓ {% endif %}🌐 R0
</button>
<button type="submit" name="level" value="r1"
style="padding:0.5rem;cursor:pointer;font-family:inherit;font-size:0.85rem;
{% if current_level == 'r1' %}
background:rgba(0,221,68,0.25);color:var(--phos-hot);border:2px solid var(--phos);font-weight:bold;
{% else %}
background:transparent;color:var(--phos);border:1px solid var(--dim);
{% endif %}">
{% if current_level == 'r1' %}✓ {% endif %}🛡 R1
</button>
<button type="submit" name="level" value="r2"
style="padding:0.5rem;cursor:pointer;font-family:inherit;font-size:0.85rem;
{% if current_level == 'r2' %}
background:rgba(255,179,71,0.25);color:#ffd6a0;border:2px solid #ffb347;font-weight:bold;
{% else %}
background:transparent;color:#9d7846;border:1px solid var(--dim);
{% endif %}">
{% if current_level == 'r2' %}✓ {% endif %}🔍 R2
</button>
<h2>📊 En un coup d'œil</h2>
<div class="graphs">
{# trackers donut #}
<div>
<div style="font-size:.82rem;color:var(--dim);margin-bottom:.4rem">🍪 Qui te trace</div>
{% if ch.trackers %}
<div class="donut-wrap">
<div class="donut" style="background:conic-gradient({% for t in ch.trackers %}{{ palette[loop.index0 % palette|length] }} {{ t.start }}% {{ t.end }}%{% if not loop.last %},{% endif %}{% endfor %})">
<div class="donut-hole">{{ n_trackers }}<br>traceurs</div>
</div>
<div class="legend">
{% for t in ch.trackers %}
<span class="row"><span class="dot" style="background:{{ palette[loop.index0 % palette|length] }}"></span>{{ t.emoji }} {{ t.label[:14] }} <b style="color:var(--text)">{{ t.pct }}%</b></span>
{% endfor %}
</div>
</div>
{% else %}<div class="empty">Aucun traceur repéré 🎉</div>{% endif %}
</div>
{# countries bars #}
<div>
<div style="font-size:.82rem;color:var(--dim);margin-bottom:.4rem">🌍 Vers quels pays</div>
{% if ch.countries %}
{% for c in ch.countries %}
<div class="bar-row"><span class="bar-lbl">{{ c.flag }} {{ c.label[:8] }}</span><span class="bar-track"><span class="bar-fill" style="width:{{ c.pct }}%"></span></span><span class="bar-val">{{ c.count }}</span></div>
{% endfor %}
{% else %}<div class="empty">Pas encore de données géo</div>{% endif %}
</div>
{# apps bars #}
<div style="grid-column:1/-1">
<div style="font-size:.82rem;color:var(--dim);margin-bottom:.4rem">📺 Quelles apps / services</div>
{% if ch.apps %}
{% for a in ch.apps %}
<div class="bar-row"><span class="bar-lbl">{{ a.emoji }} {{ a.label[:16] }}</span><span class="bar-track"><span class="bar-fill" style="width:{{ a.pct }}%;background:linear-gradient(90deg,var(--violet),#c9b6ff)"></span></span><span class="bar-val" style="color:var(--violet)">{{ a.count }}</span></div>
{% endfor %}
{% else %}<div class="empty">Aucune app classifiée</div>{% endif %}
</div>
</div>
<p class="help">Les traceurs suivent ta navigation entre sites. Les apps cert-pinning (🔒) refusent l'analyse — c'est bon signe.</p>
</div>
{# ── LEVEL SWITCHER (action) ── #}
<div class="card">
<h2>🔀 Mon niveau de protection</h2>
<form method="POST" action="/change-level" class="lvl">
<button type="submit" name="level" value="r0" class="{{ 'on' if current_level=='r0' }}" style="{% if current_level=='r0' %}border-color:var(--text){% endif %}">{% if current_level=='r0' %}✓ {% endif %}🌐 R0</button>
<button type="submit" name="level" value="r1" class="{{ 'on' if current_level=='r1' }}" style="{% if current_level=='r1' %}border-color:var(--phos);color:var(--phos-hot){% endif %}">{% if current_level=='r1' %}✓ {% endif %}🛡 R1</button>
<button type="submit" name="level" value="r2" class="{{ 'on' if current_level=='r2' }}" style="{% if current_level=='r2' %}border-color:var(--amber);color:var(--amber){% endif %}">{% if current_level=='r2' %}✓ {% endif %}🔍 R2</button>
</form>
{% if wg_enabled|default(false) %}
{# Phase 6 (#496) : R3 WireGuard separate action (different flow — install profile first) #}
<p style="font-size:0.7rem;color:var(--dim);margin-top:0.5rem;text-align:center">
Pour le mode R3 WireGuard (mobile + tout-décrypté) :
<a href="/wg/r3-install" style="color:#9e76ff;text-decoration:underline">🌐 installer le profil</a>
</p>
{% if current_level == 'r3' %}
<p style="font-size:0.72rem;color:#9e76ff;margin-top:0.3rem;text-align:center;font-weight:bold">
✓ Tu es actuellement en mode R3 — tunnel WG actif
</p>
{% endif %}
{% endif %}
<p class="help">R0 = aucune analyse · R1 = analyse passive (recommandé) · R2 = analyse + bandeau.{% if wg_enabled|default(false) %} <a href="/wg/r3-install" style="color:var(--violet)">🧅 R3 tunnel mobile</a>.{% endif %}</p>
</div>
<div class="card">
<h2>👤 Identifiant anonyme</h2>
<div class="kv">
<span class="k">Hash session</span> <span class="v">{{ mac_hash }}</span>
<span class="k">Sandbox IP</span> <span class="v">{{ ip }}</span>
<span class="k">Appareil</span> <span class="v">{{ device_type }}</span>
</div>
</div>
{# ════════════ DÉTAILS TECHNIQUES (repliés) ════════════ #}
<div class="card">
<h2>📊 Métriques session</h2>
<div class="kv">
<span class="k">Connexions</span> <span class="v">{{ metrics.connections }}</span>
<span class="k">Hosts uniques</span> <span class="v">{{ metrics.unique_hosts }}</span>
<span class="k">Réussies</span> <span class="v">{{ metrics.successful }}</span>
<span class="k">Cert-pin block</span> <span class="v">{{ metrics.tls_pinned }}</span>
</div>
</div>
<div class="card">
<h2>🎯 Analyse compromission</h2>
<p style="text-align:center;margin:0.5rem 0">
{% if risk_score < 30 %}
<span class="score low">Score : {{ risk_score }}/100 — {{ risk_label|default('LOW') }}</span>
{% elif risk_score < 70 %}
<span class="score med">Score : {{ risk_score }}/100 — {{ risk_label|default('MEDIUM') }}</span>
{% else %}
<span class="score high">Score : {{ risk_score }}/100 — {{ risk_label|default('HIGH') }}</span>
<details>
<summary>🎯 Analyse de compromission &amp; score</summary>
<div class="inner">
{% if risk_explanation %}<p style="font-size:.85rem;margin-bottom:.6rem">{{ risk_explanation }}</p>{% endif %}
<ul>{% for ind in indicators %}<li>{{ ind }}</li>{% endfor %}</ul>
{% if scoring and scoring.breakdown %}
<table style="margin-top:.6rem"><thead><tr><th>Catégorie</th><th style="text-align:right">Signaux</th><th style="text-align:right">Poids</th></tr></thead><tbody>
{% for b in scoring.breakdown %}
<tr><td>{{ b.category }}</td><td style="text-align:right">{{ b.raw_signal_count }}</td><td style="text-align:right;color:var(--amber)">+{{ b.weight_subtotal }}</td></tr>
{% endfor %}
</tbody></table>
{% endif %}
</p>
{% if risk_explanation %}
<p style="font-size:0.85rem;color:var(--text);margin-bottom:0.8rem;padding:0.5rem;background:rgba(0,221,68,0.05);border-left:2px solid var(--phos)">{{ risk_explanation }}</p>
{% endif %}
<ul>
{% for ind in indicators %}<li>{{ ind }}</li>{% endfor %}
</ul>
</div>
</div>
</details>
{% if scoring and scoring.breakdown %}
<div class="card">
<h2>🔬 Breakdown du score (transparent)</h2>
<table style="width:100%;font-size:0.82rem;border-collapse:collapse">
<thead><tr style="color:var(--dim);border-bottom:1px solid var(--dim)">
<th style="padding:0.2rem 0.4rem;text-align:left">Catégorie</th>
<th style="padding:0.2rem 0.4rem;text-align:right">Signaux</th>
<th style="padding:0.2rem 0.4rem;text-align:right">Poids</th>
</tr></thead>
<tbody>
{% for b in scoring.breakdown %}
<tr><td style="padding:0.15rem 0.4rem">{{ b.category }}</td>
<td style="padding:0.15rem 0.4rem;text-align:right">{{ b.raw_signal_count }}</td>
<td style="padding:0.15rem 0.4rem;text-align:right;color:var(--amber);text-shadow:0 0 4px var(--amber)">+{{ b.weight_subtotal }}</td></tr>
{% if b.examples %}
<tr><td colspan="3" style="padding:0;font-size:0.75rem;color:var(--text)">
{% for ex in b.examples[:3] %}<div style="margin-left:1rem;padding:0.1rem 0">▸ <code>{{ ex }}</code></div>{% endfor %}
</td></tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% if threat_intel_matches %}
<div class="card">
<h2 class="pin">🚨 Threat-intel : matches feeds malware</h2>
<ul>
{% for m in threat_intel_matches[:10] %}
<li>{{ m.flag }} <span style="color:var(--red)">[{{ m.source }}/{{ m.weight }}]</span> <b>{{ m.label }}</b> : <code>{{ m.ioc[:60] }}</code>{% if m.asn_org %} <span style="font-size:0.75rem;color:var(--dim)">({{ m.asn_org }})</span>{% endif %}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if dga_candidates %}
<div class="card">
<h2 class="pin">🔠 DGA — domaines suspects</h2>
<ul>
{% for d in dga_candidates[:8] %}
<li>{{ d.flag }} <span style="color:var(--amber)">[{{ d.score }}]</span> <code>{{ d.host[:60] }}</code> <span style="font-size:0.75rem;color:var(--dim)">({{ d.indicators|join(', ') }})</span></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if beaconing_candidates %}
<div class="card">
<h2 class="pin">📡 Beaconing — patterns périodiques suspects</h2>
<table style="width:100%;font-size:0.82rem">
<thead><tr style="color:var(--dim);text-align:left">
<th>🚩</th><th>Score</th><th>Host</th><th style="text-align:right">Median</th><th style="text-align:right">CV</th><th>ASN</th>
</tr></thead>
<tbody>
{% for b in beaconing_candidates[:8] %}
<tr><td>{{ b.flag }}</td><td style="color:var(--amber)">{{ b.score }}</td><td><code>{{ b.host[:40] }}</code></td>
<td style="text-align:right">{{ b.median_seconds }}s</td>
<td style="text-align:right">{{ b.cv }}</td>
<td style="font-size:0.78rem;color:var(--dim)">{{ b.asn_org[:30] if b.asn_org else '' }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% if dpi_classified and dpi_classified.top_apps %}
<div class="card">
<h2>🧭 Apps détectées (classification nDPI-style)</h2>
<p style="font-size:0.78rem;color:var(--dim);margin-bottom:0.5rem">
Catégories : {% for cat, n in dpi_classified.by_category.items() %}{{ dpi_classified.category_emoji.get(cat, '❔') }} {{ cat }}({{ n }}){% if not loop.last %} · {% endif %}{% endfor %}
</p>
<table style="width:100%;font-size:0.85rem">
<thead><tr style="color:var(--dim);text-align:left">
<th>App</th><th>Catégorie</th><th style="text-align:right">Connexions</th>
</tr></thead>
<tbody>
{% for a in dpi_classified.top_apps[:15] %}
<tr><td>{{ a.emoji }} <b>{{ a.app }}</b></td><td style="font-size:0.78rem;color:var(--dim)">{{ a.category }}</td>
<td style="text-align:right;color:var(--phos);text-shadow:0 0 4px var(--phos)">{{ a.count }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% if threat_intel_matches or dga_candidates or beaconing_candidates %}
<details>
<summary class="pin">🚨 Menaces détectées ({{ (threat_intel_matches|default([]))|length + (dga_candidates|default([]))|length + (beaconing_candidates|default([]))|length }})</summary>
<div class="inner">
{% if threat_intel_matches %}<p style="color:var(--amber);font-size:.82rem;margin:.3rem 0">🚨 Threat-intel (feeds malware)</p><ul>
{% for x in threat_intel_matches[:10] %}<li>{{ x.flag }} <span style="color:var(--red)">[{{ x.source }}]</span> <b>{{ x.label }}</b> <code>{{ x.ioc[:50] }}</code></li>{% endfor %}</ul>{% endif %}
{% if dga_candidates %}<p style="color:var(--amber);font-size:.82rem;margin:.5rem 0 .2rem">🔠 Domaines générés (DGA)</p><ul>
{% for d in dga_candidates[:8] %}<li>{{ d.flag }} [{{ d.score }}] <code>{{ d.host[:50] }}</code></li>{% endfor %}</ul>{% endif %}
{% if beaconing_candidates %}<p style="color:var(--amber);font-size:.82rem;margin:.5rem 0 .2rem">📡 Beaconing (périodique)</p><ul>
{% for b in beaconing_candidates[:8] %}<li>{{ b.flag }} [{{ b.score }}] <code>{{ b.host[:40] }}</code> · {{ b.median_seconds }}s</li>{% endfor %}</ul>{% endif %}
</div>
</details>
{% endif %}
{% if geo_top_hosts %}
<div class="card">
<h2>🌍 Hôtes contactés (par pays + ASN + app)</h2>
<table style="width:100%;font-size:0.82rem">
<thead><tr style="color:var(--dim);text-align:left">
<th>🚩</th><th>App</th><th>Hôte</th><th>ASN</th><th style="text-align:right">Hits</th>
</tr></thead>
<tbody>
{% for h in geo_top_hosts[:15] %}
<tr><td>{{ h.flag }}</td><td>{{ h.emoji }} {{ h.app[:18] }}</td><td><code style="font-size:0.78rem">{{ h.host[:45] }}</code></td>
<td style="font-size:0.75rem;color:var(--dim)">{{ h.asn_org[:25] if h.asn_org else '' }}</td>
<td style="text-align:right;color:var(--phos)">{{ h.count }}</td></tr>
<details>
<summary>🌍 Hôtes contactés ({{ geo_top_hosts|length }})</summary>
<div class="inner"><table><thead><tr><th>🚩</th><th>App</th><th>Hôte</th><th style="text-align:right">Hits</th></tr></thead><tbody>
{% for h in geo_top_hosts[:20] %}
<tr><td>{{ h.flag }}</td><td>{{ h.emoji }} {{ h.app[:14] }}</td><td><code>{{ h.host[:42] }}</code></td><td style="text-align:right;color:var(--phos)">{{ h.count }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</tbody></table></div>
</details>
{% endif %}
{% if avatar_analysis and avatar_analysis.devices %}
<div class="card">
<h2>{{ avatar_analysis.most_common_emoji }} Avatar / device fingerprint</h2>
<p style="margin-bottom:0.5rem">
<span style="font-size:1.2rem">{{ avatar_analysis.most_common_emoji }}</span>
<b>{{ avatar_analysis.most_common }}</b> (le plus représenté)
</p>
<div style="display:flex;flex-wrap:wrap;gap:1rem;font-size:0.85rem">
<div>
<div style="color:var(--dim);font-size:0.78rem;margin-bottom:0.3rem">Devices :</div>
{% for dev, info in avatar_analysis.devices.items() %}
<div>{{ info.emoji }} <b>{{ info.os_label }}</b> <span style="color:var(--dim)">({{ info.count }})</span></div>
{% endfor %}
</div>
<div>
<div style="color:var(--dim);font-size:0.78rem;margin-bottom:0.3rem">Browsers :</div>
{% for br, info in avatar_analysis.browsers.items() %}
<div>{{ info.emoji }} <b>{{ info.label }}</b> <span style="color:var(--dim)">({{ info.count }})</span></div>
{% endfor %}
</div>
</div>
</div>
{% if dpi_classified and dpi_classified.top_apps %}
<details>
<summary>🧭 Apps détectées (nDPI)</summary>
<div class="inner"><table><thead><tr><th>App</th><th>Catégorie</th><th style="text-align:right">Conn.</th></tr></thead><tbody>
{% for a in dpi_classified.top_apps[:20] %}
<tr><td>{{ a.emoji }} <b>{{ a.app }}</b></td><td style="color:var(--dim)">{{ a.category }}</td><td style="text-align:right;color:var(--phos)">{{ a.count }}</td></tr>
{% endfor %}
</tbody></table></div>
</details>
{% endif %}
{% if cookies_providers %}
<div class="card">
<h2>🍪 Trackers / providers cookies (Phase 2a+)</h2>
<ul>
{% for p in cookies_providers[:10] %}
<li>{{ p.emoji }} <b>{{ p.provider }}</b> <span style="font-size:0.78rem;color:var(--dim)">({{ p.category }})</span> <span style="color:var(--phos)">×{{ p.count }}</span></li>
{% endfor %}
</ul>
</div>
<details>
<summary>🍪 Traceurs / providers cookies ({{ cookies_providers|length }})</summary>
<div class="inner"><ul>
{% for p in cookies_providers[:15] %}<li>{{ p.emoji }} <b>{{ p.provider }}</b> <span style="color:var(--dim)">({{ p.category }})</span> <span style="color:var(--phos)">×{{ p.count }}</span></li>{% endfor %}
</ul></div>
</details>
{% endif %}
<div class="card">
<h2>📱 Apps détectées (vue IP forensics)</h2>
<ul>
{% for app in apps_detected %}<li>{{ app }}</li>{% endfor %}
</ul>
</div>
<div class="card">
<h2 class="pin">🔒 Apps protégées par cert-pinning</h2>
<p style="font-size:0.78rem;color:var(--dim);margin-bottom:0.5rem">
Ces apps refusent notre certificat — ToolBoX ne peut PAS lire leur contenu. C'est un BON signe sécurité.
</p>
<ul>
{% for app in pinned_apps %}<li>{{ app }}</li>{% endfor %}
</ul>
</div>
{% if dpi and dpi.top_hosts %}
<div class="card">
<h2>🔍 DPI — hôtes les plus contactés</h2>
<table style="width:100%;font-size:0.82rem;border-collapse:collapse">
<thead><tr style="color:var(--dim);text-align:left;border-bottom:1px solid var(--dim)">
<th style="padding:0.2rem 0.4rem">Hôte</th><th style="padding:0.2rem 0.4rem;text-align:right">Requêtes</th>
</tr></thead>
<tbody>
{% for entry in dpi.top_hosts[:10] %}
<tr><td style="padding:0.15rem 0.4rem;font-family:monospace">{{ entry.host[:70] }}</td>
<td style="padding:0.15rem 0.4rem;text-align:right;color:var(--phos);text-shadow:0 0 4px var(--phos)">{{ entry.count }}</td></tr>
{% endfor %}
</tbody>
</table>
{% if dpi.methods %}
<p style="font-size:0.78rem;color:var(--dim);margin-top:0.6rem">
Méthodes : {% for m,c in dpi.methods.items() %}<span style="color:var(--phos)">{{ m }}({{ c }})</span>{% if not loop.last %} · {% endif %}{% endfor %}
</p>
{% endif %}
{% if dpi.user_agents %}
<p style="font-size:0.75rem;color:var(--dim);margin-top:0.4rem">
UA détectés : {{ dpi.user_agents|length }}
{% if dpi.user_agents %}<br><span style="color:var(--text);font-family:monospace;font-size:0.7rem">{{ dpi.user_agents[0][:90] }}</span>{% endif %}
</p>
{% endif %}
</div>
{% endif %}
{% if cookies and (cookies.total_set or cookies.details) %}
<div class="card">
<h2>🍪 Cookies / trackers</h2>
<div class="kv" style="margin-bottom:0.5rem">
<span class="k">Set-Cookie reçus</span> <span class="v">{{ cookies.total_set }}</span>
<span class="k">Cookies envoyés</span> <span class="v">{{ cookies.total_sent }}</span>
</div>
{% if cookies.details %}
<p style="font-size:0.78rem;color:var(--dim);margin-top:0.4rem">URLs avec activité cookies (top {{ cookies.details|length }}) :</p>
{% for d in cookies.details[:10] %}
<div class="url">
<span style="color:var(--amber)">set={{ d.set }}/sent={{ d.sent }}</span> · {{ d.url[:90] }}
{% if avatar_analysis and avatar_analysis.devices %}
<details>
<summary>{{ _avatar.most_common_emoji }} Empreinte appareil</summary>
<div class="inner">
<p style="margin-bottom:.4rem">{{ _avatar.most_common_emoji }} <b>{{ _avatar.most_common }}</b> · {{ _avatar.raw_count|default(0) }} UAs distincts</p>
<div style="display:flex;flex-wrap:wrap;gap:1.2rem;font-size:.84rem">
<div><div style="color:var(--dim);font-size:.75rem">Devices</div>{% for dev,info in _avatar.devices.items() %}<div>{{ info.emoji }} {{ info.os_label }} ({{ info.count }})</div>{% endfor %}</div>
<div><div style="color:var(--dim);font-size:.75rem">Browsers</div>{% for br,info in (_avatar.browsers or {}).items() %}<div>{{ info.emoji }} {{ info.label }} ({{ info.count }})</div>{% endfor %}</div>
</div>
{% endfor %}
{% endif %}
</div>
</div>
</details>
{% endif %}
{% if soc and soc.indicators %}
<div class="card">
<h2 class="pin">⚠ SOC — indicateurs détectés</h2>
<p style="font-size:0.78rem;color:var(--dim);margin-bottom:0.5rem">
Score SOC actuel : <b style="color:var(--amber)">{{ soc.score }}/100</b>
</p>
<ul>
{% for ind in soc.indicators[:10] %}
<li><span style="color:var(--amber)">[poids {{ ind.weight }}]</span> {{ ind.kind }} : <code>{{ ind.host[:60] }}</code></li>
{% endfor %}
</ul>
</div>
{% if pinned_apps %}
<details>
<summary>🔒 Apps protégées (cert-pinning) ({{ pinned_apps|length }})</summary>
<div class="inner"><p class="help" style="margin-bottom:.4rem">Ces apps refusent notre certificat — on ne peut PAS lire leur contenu. Bon signe.</p>
<ul>{% for app in pinned_apps %}<li>{{ app }}</li>{% endfor %}</ul></div>
</details>
{% endif %}
{% if ja4 and ja4.snis_seen %}
<div class="card">
<h2>🔐 JA4 — empreintes TLS</h2>
<p style="font-size:0.78rem;color:var(--dim);margin-bottom:0.4rem">
SNIs vus (top {{ ja4.snis_seen|length }}) :
</p>
{% for sni in ja4.snis_seen[:8] %}
<div class="url">{{ sni }}</div>
{% endfor %}
{% if ja4.alpns_seen %}
<p style="font-size:0.75rem;color:var(--dim);margin-top:0.5rem">
ALPN protocols : <span style="color:var(--phos)">{{ ja4.alpns_seen|join(', ') }}</span>
</p>
{% endif %}
</div>
{% endif %}
{% if inspected_urls and not (cookies and cookies.details) %}
<div class="card">
<h2>👁 Trafic inspecté (R2 consent explicite)</h2>
{% for url in inspected_urls[:15] %}
<div class="url">{{ url }}</div>
{% endfor %}
{% if inspected_urls|length > 15 %}
<p style="font-size:0.75rem;color:var(--dim);margin-top:0.4rem">… et {{ inspected_urls|length - 15 }} autres URLs</p>
{% endif %}
</div>
{% endif %}
<div class="card">
<h2>✅ Recommandations</h2>
<ul>
{% for rec in recommendations %}<li>{{ rec }}</li>{% endfor %}
</ul>
</div>
<div class="actions">
{# Phase 6 (#496) : pass mh= so R3 WG clients can download from kbin too #}
<a href="/report/me?mh={{ mac_hash }}">⬇ Télécharger PDF</a>
<a href="/">↩ Retour splash</a>
<a href="/status">📊 Statut JSON</a>
</div>
{# Phase 3 (#492) : transparency layer — inspection breakdown + per-host quality #}
{% set t = transparency|default({}) %}
{% if t and t.get('total_events', 0) > 0 %}
<div class="card">
<h2>🔎 INSPECTION : CE QU'ON A REGARDÉ (et ce qu'on n'a PAS regardé)</h2>
<p style="font-size:0.78rem;color:var(--dim);margin-bottom:0.6rem;font-style:italic">
Honnêteté avant magie : la cabine te dit ce qu'elle a inspecté, ce qu'elle a sciemment bypassé, et pourquoi.
</p>
<div class="kv">
{% set b = t.get('breakdown_pct', {}) %}
{% if b.get('inspected') %}
<div class="k">🔍 Inspecté (HTTPS via notre CA)</div>
<div class="v">{{ b.get('inspected', 0) }}% — contenu visible</div>
{% endif %}
{% if b.get('bypassed-whitelist') %}
<div class="k">🛡 Bypass whitelist</div>
<div class="v">{{ b.get('bypassed-whitelist', 0) }}% — décision policy (cert-pinning vendor)</div>
{% endif %}
{% if b.get('pinned-failed-mitm') %}
<div class="k">🔒 Cert-pinning détecté</div>
<div class="v">{{ b.get('pinned-failed-mitm', 0) }}% — l'app refuse notre CA, normal+bon signe</div>
{% endif %}
{% if b.get('e2e-opaque') %}
<div class="k">🔐 E2E messaging</div>
<div class="v">{{ b.get('e2e-opaque', 0) }}% — opaque par design, ton chiffrement marche</div>
{% endif %}
<div class="k">📊 Total events analysés</div>
<div class="v">{{ t.get('total_events', 0) }}</div>
{% if t.get('whitelist_stats') %}
<div class="k">📜 Patterns whitelist actifs</div>
<div class="v">{{ t.get('whitelist_stats', {}).get('count', 0) }} (baseline + override)</div>
{% endif %}
{% if t.get('sensitivity') %}
<div class="k">🎛 Sensibilité active</div>
<div class="v">{{ t.get('sensitivity', {}).get('label', '?') }} — {{ t.get('sensitivity', {}).get('description', '')[:80] }}</div>
{% endif %}
</div>
{# Phase 3 (#492) : tentatives counters — full transparency #}
{% set a = t.get('attempts', {}) %}
{% if a.get('total', 0) > 0 %}
<div style="margin-top:0.8rem;padding-top:0.6rem;border-top:1px solid var(--dim)">
<p style="font-size:0.78rem;color:var(--dim);margin-bottom:0.4rem"><b>📈 Tentatives observées (toutes catégories)</b></p>
<div class="kv" style="font-size:0.8rem">
<span class="k">Total observé</span><span class="v">{{ a.get('total', 0) }}</span>
<span class="k">🔍 Inspecté</span><span class="v">{{ a.get('inspected', 0) }}</span>
<span class="k">🛡 Bypass</span><span class="v">{{ a.get('bypassed_whitelist', 0) }}</span>
<span class="k">🔒 Cert-pinning</span><span class="v">{{ a.get('pinned_failed', 0) }}</span>
<span class="k">🔐 E2E opaque</span><span class="v">{{ a.get('e2e_opaque', 0) }}</span>
{% if a.get('blocked', 0) > 0 %}
<span class="k">🚫 Bloqué</span><span class="v" style="color:var(--red)">{{ a.get('blocked', 0) }}</span>
{% endif %}
<details>
<summary>🔎 Transparence : ce qu'on a regardé</summary>
<div class="inner">
<p class="help" style="margin-bottom:.5rem">Honnêteté avant magie : ce qu'on a inspecté, bypassé, et pourquoi.</p>
<div class="kv">
{% set b = t.get('breakdown_pct', {}) %}
{% if b.get('inspected') %}<span class="k">🔍 Inspecté</span><span class="v">{{ b.get('inspected') }}%</span>{% endif %}
{% if b.get('pinned-failed-mitm') %}<span class="k">🔒 Cert-pinning</span><span class="v">{{ b.get('pinned-failed-mitm') }}%</span>{% endif %}
{% if b.get('e2e-opaque') %}<span class="k">🔐 E2E chiffré</span><span class="v">{{ b.get('e2e-opaque') }}%</span>{% endif %}
<span class="k">📊 Total events</span><span class="v">{{ t.get('total_events', 0) }}</span>
</div>
</div>
{% endif %}
{# Phase 3 (#492) : whitelist hits — accountability per pattern/category #}
{% set wh = t.get('whitelist_hits', {}) %}
{% if wh.get('total', 0) > 0 %}
<div style="margin-top:0.8rem;padding-top:0.6rem;border-top:1px solid var(--dim)">
<p style="font-size:0.78rem;color:var(--dim);margin-bottom:0.4rem"><b>📜 Tes bypass whitelist en détail</b> — {{ wh.get('total') }} connexions sciemment non-inspectées</p>
<div class="kv" style="font-size:0.78rem">
{% for cat, cnt in (wh.get('by_category', {}) | dictsort(by='value', reverse=True))[:8] %}
<span class="k">{{ cat }}</span><span class="v">{{ cnt }} hits</span>
{% if t.get('per_host') %}
<table style="margin-top:.7rem"><thead><tr><th>Grade</th><th>Destination</th><th>Statut</th></tr></thead><tbody>
{% for h in t.get('per_host', [])[:15] %}
<tr><td style="font-weight:700;color:{% if h.grade in ['A+','A'] %}var(--phos-hot){% elif h.grade=='B' %}var(--phos){% elif h.grade=='C' %}var(--amber){% else %}var(--red){% endif %}">{{ h.grade }}</td><td><code>{{ h.host[:48] }}</code></td><td style="color:var(--dim);font-size:.72rem">{{ h.status }}</td></tr>
{% endfor %}
</div>
<ul style="margin-top:0.4rem">
{% for p in wh.get('top_patterns', [])[:8] %}
<li style="font-size:0.72rem"><code>{{ p.pattern }}</code> · {{ p.count }} hits</li>
{% endfor %}
</ul>
</tbody></table>
{% endif %}
</div>
{% endif %}
</div>
{% if t.get('per_host') %}
<div class="card">
<h2>🎯 QUALITÉ SÉCURITÉ PAR DESTINATION</h2>
<p style="font-size:0.78rem;color:var(--dim);margin-bottom:0.6rem;font-style:italic">
Grade A+/A/B/C/D/F basé sur TLS version + JA4 + headers + cookies. Worst-first :
</p>
<table style="width:100%;font-size:0.78rem;border-collapse:collapse">
<thead>
<tr style="color:var(--phos);text-align:left;border-bottom:1px solid var(--dim)">
<th style="padding:0.3rem">Grade</th>
<th style="padding:0.3rem">Destination</th>
<th style="padding:0.3rem">Statut analyse</th>
</tr>
</thead>
<tbody>
{% for h in t.get('per_host', [])[:15] %}
<tr style="border-bottom:1px solid rgba(0,221,68,0.1)">
<td style="padding:0.25rem;font-weight:bold;color:{% if h.grade in ['A+','A'] %}var(--phos-hot){% elif h.grade == 'B' %}var(--phos){% elif h.grade == 'C' %}var(--amber){% else %}var(--red){% endif %}">{{ h.grade }}</td>
<td style="padding:0.25rem;font-family:monospace;color:var(--text)">{{ h.host[:60] }}</td>
<td style="padding:0.25rem;color:var(--dim);font-size:0.72rem">{{ h.status }} — {{ h.reason[:70] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</details>
{% endif %}
<p class="refresh">↻ Auto-refresh toutes les 15 secondes</p>
<details>
<summary>👤 Identité &amp; recommandations</summary>
<div class="inner">
<div class="kv">
<span class="k">Hash session</span><span class="v"><code>{{ mac_hash }}</code></span>
<span class="k">Sandbox IP</span><span class="v">{{ ip }}</span>
<span class="k">Appareil</span><span class="v">{{ device_type }}</span>
</div>
{% if recommendations %}<p style="color:var(--phos-hot);font-size:.85rem;margin:.6rem 0 .2rem">✅ Recommandations</p><ul>{% for rec in recommendations %}<li>{{ rec }}</li>{% endfor %}</ul>{% endif %}
</div>
</details>
<div class="actions">
<a href="/report/me?mh={{ mac_hash }}">⬇ Télécharger le PDF</a>
<a href="/social/me?mh={{ mac_hash }}">🕸️ Ma carto</a>
<a href="/landing">↩ Accueil</a>
</div>
<div class="card">
<h2>💚 Support &amp; soutien projet</h2>
<p style="font-size:0.82rem;color:var(--text);line-height:1.55">
Gondwana ToolBoX est un <b>commun numérique</b> open-source maintenu par
CyberMind / Gérald Kerma (Notre-Dame-du-Cruet, Savoie). Pas de pub, pas de
revente, pas de tracking commercial. Si ce service t'a aidé :
</p>
<ul style="margin-top:0.5rem">
<li>💰 <b>Don récurrent</b> : <a href="https://liberapay.com/cybermind" style="color:var(--phos);text-decoration:underline">liberapay.com/cybermind</a></li>
<li>💳 <b>Don ponctuel</b> : <a href="https://cybermind.fr/don" style="color:var(--phos);text-decoration:underline">cybermind.fr/don</a> (carte, virement SEPA)</li>
<li>📧 <b>Support technique</b> : <a href="mailto:support@cybermind.fr" style="color:var(--phos);text-decoration:underline">support@cybermind.fr</a></li>
<li>🐛 <b>Signaler un bug</b> : <a href="https://github.com/CyberMind-FR/secubox-deb/issues" style="color:var(--phos);text-decoration:underline">github.com/CyberMind-FR/secubox-deb/issues</a></li>
<li>📡 <b>Déployer une borne</b> près de chez toi : <a href="mailto:gondwana@cybermind.fr" style="color:var(--phos);text-decoration:underline">gondwana@cybermind.fr</a></li>
<h2>💚 Soutenir le projet</h2>
<p class="help">Commun numérique open-source, sans pub ni revente — CyberMind / Gérald Kerma (Savoie).</p>
<ul style="margin-top:.4rem">
<li>💰 <a href="https://liberapay.com/cybermind" style="color:var(--phos)">liberapay.com/cybermind</a></li>
<li>🐛 <a href="https://github.com/CyberMind-FR/secubox-deb/issues" style="color:var(--phos)">Signaler un bug</a></li>
</ul>
<p style="font-size:0.78rem;color:var(--dim);margin-top:0.7rem;border-top:1px solid var(--dim);padding-top:0.5rem">
Audit SecuBox premium / déploiement collectivité / formation cybersécurité :
<a href="mailto:contact@cybermind.fr" style="color:var(--phos);text-decoration:underline">contact@cybermind.fr</a>
</p>
</div>
<div class="footer">
Gondwana ToolBoX · LicenseRef-CMSD-1.0 (Source-Disclosed License)<br>
Source : <a href="https://github.com/CyberMind-FR/secubox-deb" style="color:var(--dim)">github.com/CyberMind-FR/secubox-deb</a> (#474 #475 #477)<br>
CyberMind — Notre-Dame-du-Cruet (73130) · <a href="https://cybermind.fr" style="color:var(--dim)">cybermind.fr</a>
Gondwana ToolBoX · LicenseRef-CMSD-1.0 · ↻ rafraîchit toutes les 20 s<br>
<a href="https://github.com/CyberMind-FR/secubox-deb" style="color:var(--dim)">github.com/CyberMind-FR/secubox-deb</a> · <a href="https://cybermind.fr" style="color:var(--dim)">cybermind.fr</a>
</div>
</body></html>

View File

@ -1,3 +1,53 @@
secubox-toolbox (2.7.11-1~bookworm1) bookworm; urgency=medium
* feat: landing (kbin.gk2) restyled to match the new report — system font,
rounded --panel/--line cards, cleaner accents, softer SVG bars, helper lines,
R3 panel mentions the 🧅 Tor egress option. Dynamic bits unchanged (live KPIs
+ JS, per-OS install panels, cert-probe, ?mh links).
-- Gerald KERMA <devel@cybermind.fr> Fri, 19 Jun 2026 20:20:00 +0200
secubox-toolbox (2.7.10-1~bookworm1) bookworm; urgency=medium
* fix: /report/me/html now resolves identity via the shared _client_mac_hash
(?mh → R3 WG peer → captive ARP), so R3 clients reach their report without
?mh (it previously only did captive ARP → 400 "client MAC unknown").
-- Gerald KERMA <devel@cybermind.fr> Fri, 19 Jun 2026 19:55:00 +0200
secubox-toolbox (2.7.9-1~bookworm1) bookworm; urgency=medium
* feat: report (/report/me/html) regenerated — verdict-first + graphs + plain
helpers. Top: a score gauge (conic-gradient) + 6 KPIs + 3 graphs (trackers
donut, countries bars, apps bars) computed server-side (_build_report_charts).
All the deep technical cards (threat-intel, DGA, beaconing, hosts, apps,
cookies, avatar, transparency, per-host grades, identity, reco) collapse into
<details> so the page reads instantly. System-font, rounded, mobile-first.
-- Gerald KERMA <devel@cybermind.fr> Fri, 19 Jun 2026 19:30:00 +0200
secubox-toolbox (2.7.8-1~bookworm1) bookworm; urgency=medium
* fix: landing "Ma carto" / "Mon rapport" / "Qui me piste ?" links hit
/social/me + /report/me with NO ?mh=, so clicking them re-resolved identity
at click-time and could 400 "client identity unresolved". The landing now
resolves the caller's mac_hash (new _client_mac_hash helper: ?mh → R3 WG peer
→ captive ARP) and bakes ?mh= into those links, so they always open the right
client's view. No change to identity precedence; resolution stays server-side.
-- Gerald KERMA <devel@cybermind.fr> Fri, 19 Jun 2026 18:40:00 +0200
secubox-toolbox (2.7.7-1~bookworm1) bookworm; urgency=medium
* fix: injected banner trackers/cookies counts were stuck at 0. They were
computed ONCE at render time — which fires early, before page resources +
cookies have loaded (Resource Timing empty) — and the 2s poll's ensure()
early-returned once the banner existed, so the counts never refreshed. Now
the trackers/cookies spans have ids and updateCounts() re-counts live on the
poll, so they climb to real values within ~2s.
-- Gerald KERMA <devel@cybermind.fr> Fri, 19 Jun 2026 18:10:00 +0200
secubox-toolbox (2.7.6-1~bookworm1) bookworm; urgency=medium
* fix(#683): the 🧅 Tor indicator now appears on the ACTUAL injected banner.

View File

@ -328,6 +328,36 @@ def _client_ip(request: Request) -> str | None:
return request.client.host if request.client else None
def _client_mac_hash(request: Request, salt: str) -> str | None:
"""Resolve the caller's identity hash, same precedence as /social/me:
explicit ?mh= R3 WG peer (wg-peers.json) captive ARP. Returns None when
unresolvable. Used to bake ?mh= into the landing links so they never hit the
'identity unresolved' 400 (the page already knows who you are)."""
mh_qp = (request.query_params.get("mh") or "").strip().lower()
if mh_qp and all(c in "0123456789abcdef" for c in mh_qp) and 8 <= len(mh_qp) <= 64:
return mh_qp
ip = _client_ip(request)
if ip and ip.startswith("10.99.1."):
try:
import hashlib as _h
import json as _j
from pathlib import Path as _P
_db = _P("/var/lib/secubox/toolbox/wg-peers.json")
if _db.exists():
for pubkey, meta in _j.loads(_db.read_text()).get("peers", {}).items():
if meta.get("ip") == ip:
return _h.sha256(pubkey.encode()).hexdigest()[:16]
except Exception:
pass
try:
_ip, mac = _resolve(request)
if mac:
return macmod.hash_mac(mac, salt)
except Exception:
pass
return None
# ───────────────── Public routes ─────────────────
@router.get("/", response_class=HTMLResponse)
@ -701,11 +731,15 @@ async def landing(request: Request) -> HTMLResponse:
stats = _cumulative_stats()
platform = _ua_platform(request.headers.get("user-agent") or "")
install_panels = _install_panels_html(platform)
# Resolve identity now (the page knows who you are) so the report/carto links
# carry ?mh= and never hit the /social/me "identity unresolved" 400.
mac_hash = _client_mac_hash(request, _get_salt()) or ""
return HTMLResponse(
_env.get_template("landing.html.j2").render(
stats=stats,
install_panels=install_panels,
install_platform=platform,
mac_hash=mac_hash,
),
headers={"Cache-Control": "private, max-age=60, no-transform"},
)
@ -2308,6 +2342,46 @@ def _classify_apps(hosts: set[str]) -> list[str]:
return apps
def _build_report_charts(session: dict) -> dict:
"""Graph-ready aggregates for the simplified report (trackers donut,
countries bars, apps bars). Defensive / fail-empty. Each list item has
{label, emoji/flag, count, pct}; trackers also carry cumulative start/end
for a CSS conic-gradient donut."""
def _top_pct(items: list, n: int = 6) -> list:
items = [it for it in items if it.get("count")]
items.sort(key=lambda x: x["count"], reverse=True)
items = items[:n]
total = sum(x["count"] for x in items) or 1
for it in items:
it["pct"] = round(100 * it["count"] / total)
return items
cp = session.get("cookies_providers") or []
trackers = _top_pct([
{"label": p.get("provider", "?"), "emoji": p.get("emoji", "🍪"),
"count": int(p.get("count", 0) or 0)} for p in cp])
cum = 0
for it in trackers:
it["start"] = cum
cum += it["pct"]
it["end"] = cum
by_country: dict = {}
for h in (session.get("geo_top_hosts") or []):
key = (h.get("flag") or "🏴", h.get("country") or "?")
by_country[key] = by_country.get(key, 0) + int(h.get("count", 0) or 0)
countries = _top_pct([
{"flag": k[0], "label": k[1], "count": v} for k, v in by_country.items()])
dc = session.get("dpi_classified") or {}
apps = _top_pct([
{"label": a.get("app", "?"), "emoji": a.get("emoji", "📦"),
"count": int(a.get("count", 0) or 0)}
for a in (dc.get("top_apps") or []) if a.get("app") not in (None, "", "?")])
return {"trackers": trackers, "countries": countries, "apps": apps}
# NOTE: route order matters in FastAPI — specific routes (/report/me,
# /report/me/html) MUST be declared BEFORE the catch-all /report/{token},
# otherwise FastAPI matches /report/me with token="me" and returns 404.
@ -2323,17 +2397,16 @@ async def report_me_html(request: Request) -> HTMLResponse:
their own report. The hash for R3 = sha256(wg_pubkey)[:16] derived
by inject_banner.py and embedded in the banner 'Mon rapport' link.
"""
# Bypass path : explicit mac_hash in query (R3 WG or kbin remote viewer)
mh_qp = (request.query_params.get("mh") or "").strip().lower()
if mh_qp and all(c in "0123456789abcdef" for c in mh_qp) and 8 <= len(mh_qp) <= 64:
ip = request.client.host if request.client else "?"
mac_hash = mh_qp
else:
ip, mac = _resolve(request)
if not mac:
raise HTTPException(400, "client MAC unknown (not in captive subnet?) — use ?mh=<hash>")
salt = _get_salt()
mac_hash = macmod.hash_mac(mac, salt)
# Resolve identity the same way everywhere: ?mh → R3 WG peer (wg-peers.json)
# → captive ARP. R3 clients hitting this directly (no ?mh) now resolve too.
mac_hash = _client_mac_hash(request, _get_salt())
if not mac_hash:
raise HTTPException(
400,
"client identity unresolved (not on R3 tunnel and not in captive "
"subnet) — append ?mh=<hash> from your banner's report link",
)
ip = _client_ip(request) or (request.client.host if request.client else "?")
session = _aggregate_session(mac_hash)
# Phase 3 (#492) : pass query args + force no-cache so iPhone Safari
# actually fetches the new template.
@ -2347,6 +2420,7 @@ async def report_me_html(request: Request) -> HTMLResponse:
current_level=store.get_client_level(mac_hash) if mac_hash else "r1",
wg_enabled=wg_enabled,
cumulative=cumulative,
charts=_build_report_charts(session),
**session,
)
return HTMLResponse(html, headers={

View File

@ -151,12 +151,23 @@ _BANNER_CORE = r"""
return Object.keys(seen).length;
} catch (_) { return 0; }
}
function countCookies(){
try { return document.cookie ? document.cookie.split(";").filter(function(x){return x.indexOf("=")>=0;}).length : 0; } catch (_) { return 0; }
}
// #683 — counts are taken at render time, but resources + cookies keep loading
// AFTER the banner appears (early render stuck at 0). Re-count live on the
// 2s poll so trackers/cookies climb to their real values.
function updateCounts(b){
var t = document.getElementById("sbx-trk");
if (t) t.textContent = "🛰️ " + countTrackers((b || {}).tracker_patterns) + " trackers";
var c = document.getElementById("sbx-ck");
if (c) c.textContent = "🍪 " + countCookies() + " cookies";
}
function render(b){
if (dismissed) return;
if (document.getElementById("sbx-banner")) return;
var trk = countTrackers(b.tracker_patterns);
var ck = 0;
try { ck = document.cookie ? document.cookie.split(";").filter(function(x){return x.indexOf("=")>=0;}).length : 0; } catch (_) {}
var ck = countCookies();
var bar = document.createElement("div");
bar.id = "sbx-banner";
bar.setAttribute("style", "position:fixed;left:0;right:0;top:0;z-index:2147483647;"
@ -174,8 +185,8 @@ _BANNER_CORE = r"""
+ cspProof
+ tor
+ "<span>" + esc((b.level || "r1").toUpperCase()) + "</span>"
+ "<span>🛰️ " + trk + " trackers</span>"
+ "<span>🍪 " + ck + " cookies</span>"
+ "<span id=\"sbx-trk\">🛰️ " + trk + " trackers</span>"
+ "<span id=\"sbx-ck\">🍪 " + ck + " cookies</span>"
+ pin
+ "<a href=\"" + esc(b.report_url || "#") + "\" style=\"margin-left:auto;color:#2C70C0;text-decoration:none\">report ▸</a>"
+ "<button aria-label=\"dismiss\" style=\"background:none;border:0;color:#8A9AA8;cursor:pointer;font-size:14px\">✕</button>";
@ -186,7 +197,7 @@ _BANNER_CORE = r"""
}
// ensure(): (re)render the banner if it's absent and the bundle is loaded and
// the user hasn't dismissed it. Cheap (a getElementById guard inside render).
function ensure(){ if (bundle && !dismissed) ready(function(){ render(bundle); }); }
function ensure(){ if (bundle && !dismissed) ready(function(){ if (document.getElementById("sbx-banner")) updateCounts(bundle); else render(bundle); }); }
// SPA re-assert: wrap history nav + popstate (defer so the framework settles),
// plus a light 2s poll as a catch-all for DOM re-renders that drop the banner.
["pushState","replaceState"].forEach(function(m){