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 #} {# SPDX-License-Identifier: LicenseRef-CMSD-1.0 #}
{# Public landing page — kbin.gk2.secubox.in #} {# Public landing page — kbin.gk2.secubox.in #}
{# Radical-simplify redesign (#543): animated hero + one CTA + install panel {# #683 restyle: aligned with the new /report look — system font, rounded
up top ; everything else folded behind "En savoir plus". #} --panel/--line cards, cleaner accents. Dynamic bits (data-live KPIs + JS,
install panels, cert-probe, ?mh links) unchanged. #}
<!DOCTYPE html> <!DOCTYPE html>
<html lang=fr><head> <html lang=fr><head>
<meta charset=UTF-8> <meta charset=UTF-8>
@ -10,108 +11,84 @@
<title>👁️ VILLAGE3B — Qui te piste ?</title> <title>👁️ VILLAGE3B — Qui te piste ?</title>
<link rel=manifest href=/manifest.json> <link rel=manifest href=/manifest.json>
<style> <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} *{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} 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{color:var(--phos);text-decoration:none}a:hover{text-decoration:underline}
a:hover{text-decoration:underline} .help{color:var(--dim);font-size:.8rem;font-style:italic}
/* ── HERO ── */ /* ── 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)} .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,0.55))} .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)}} @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 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.25rem;margin-top:0.6rem;font-weight:bold} .hero .punch{color:var(--text);font-size:1.2rem;margin-top:.6rem;font-weight:700}.hero .punch b{color:var(--gold)}
.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}
.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" */
.dots{position:absolute;inset:0;pointer-events:none;z-index:0} .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{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(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(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(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(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} .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)}} @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} .hero>*{position:relative;z-index:1}
/* ── CTA row ── */
/* ── big CTA row ── */ .ctas{margin-top:1.4rem;display:flex;gap:.6rem;justify-content:center;flex-wrap:wrap}
.ctas{margin-top:1.4rem;display:flex;gap:0.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{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:hover{text-decoration:none;transform:translateY(-2px)} .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{background:var(--phos);color:#06140a;box-shadow:0 4px 18px rgba(0,221,68,.35)}
.cta.go:hover{box-shadow:0 6px 24px rgba(0,221,68,0.6)}
.cta.alt{background:transparent;color:var(--purple);border:1px solid var(--purple)} .cta.alt{background:transparent;color:var(--purple);border:1px solid var(--purple)}
.cta.alt:hover{background:rgba(158,118,255,0.12)} .cta.alt:hover{background:rgba(158,118,255,.12)}
/* ── quicknav ── */
/* ── quicknav (trimmed) ── */ .quicknav{display:flex;flex-wrap:wrap;justify-content:center;gap:.6rem;margin:1.4rem auto 0;max-width:620px}
.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:.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{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{border-color:var(--purple);transform:translateY(-2px);text-decoration:none}
.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:.62rem;letter-spacing:.04em;color:var(--phos-hot);font-weight:700;white-space:nowrap}
.qi-emoji{font-size:1.5rem;line-height:1} /* ── layout ── */
.qi-label{font-size:0.62rem;letter-spacing:0.04em;color:var(--phos-hot);font-weight:bold;white-space:nowrap} .container{max-width:760px;margin:auto;padding:1.6rem 1.1rem}
.section{margin-bottom:1.7rem}
.container{max-width:1080px;margin:auto;padding:2rem 1.5rem} h2{color:var(--phos-hot);font-size:1.12rem;margin-bottom:.6rem;letter-spacing:.02em}
.section{margin-bottom:2.5rem} h3{color:var(--purple);font-size:.95rem;margin-bottom:.4rem}
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} .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))}
h3{color:var(--purple);font-size:1rem;margin-bottom:0.5rem} .card{border:1px solid var(--line);background:var(--panel);padding:1rem 1.1rem;border-radius:12px}
.grid{display:grid;gap:1rem} .card.purple{border-color:rgba(158,118,255,.4)}.card.amber{border-color:rgba(255,179,71,.4)}
.grid-2{grid-template-columns:repeat(auto-fit,minmax(280px,1fr))} .kpi{text-align:center;padding:.8rem .4rem;background:var(--soft);border:1px solid var(--line);border-radius:12px}
.grid-4{grid-template-columns:repeat(auto-fit,minmax(160px,1fr))} .kpi .v{font-size:1.7rem;font-weight:800;color:var(--phos-hot);display:block}.kpi .l{font-size:.66rem;color:var(--dim)}
.card{border:1px solid var(--dim);background:var(--bg2);padding:1rem 1.2rem;border-radius:4px} .level{display:flex;align-items:start;gap:.8rem;padding:.85rem;border-radius:12px}
.card.purple{border-color:var(--purple);background:rgba(110,64,201,0.05)} .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}
.card.amber{border-color:var(--amber);background:rgba(255,179,71,0.05)} .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)}
.kpi{text-align:center;padding:1rem;background:rgba(0,221,68,0.05);border:1px solid var(--phos);border-radius:4px} .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}
.kpi .v{font-size:2rem;font-weight:bold;color:var(--phos-hot);text-shadow:0 0 6px var(--phos);display:block} .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}
.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}
svg.chart{width:100%;max-width:400px;height:auto} svg.chart{width:100%;max-width:400px;height:auto}
.svg-bar{fill:var(--phos);transition:fill 0.3s} .svg-bar{fill:var(--phos)}.svg-bar.medium{fill:var(--amber)}.svg-bar.high{fill:var(--red)}
.svg-bar.medium{fill:var(--amber)} code{background:var(--soft);padding:.1rem .4rem;border-radius:4px;font-size:.82rem;color:var(--phos-hot);font-family:ui-monospace,Menlo,monospace}
.svg-bar.high{fill:var(--red)} .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)}
code{background:#222;padding:0.1rem 0.4rem;border-radius:2px;font-size:0.85rem;color:var(--phos-hot)} .footer{text-align:center;font-size:.7rem;color:var(--dim);padding:1.4rem;border-top:1px solid var(--line);margin-top:2rem}
.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} .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}
.cta-sm.outline{background:transparent;color:var(--phos);border:1px solid var(--phos)} /* ── install panel ── */
.footer{text-align:center;font-size:0.78rem;color:var(--dim);padding:1.5rem;border-top:1px solid var(--dim);margin-top:2rem} .install-panel{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:.7rem 1rem;margin:.5rem 0;text-align:left}
.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 summary{cursor:pointer;font-size:.95rem;color:var(--phos-hot);list-style:none;outline:none;font-weight:700}
/* ── 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}
.install-panel summary::-webkit-details-marker{display:none} .install-panel summary::-webkit-details-marker{display:none}
.install-panel[open] summary{margin-bottom:0.6rem} .install-panel[open] summary{margin-bottom:.6rem}
.install-panel .emoji{font-size:1.1rem;margin-right:0.3rem} .install-panel .emoji{font-size:1.1rem;margin-right:.3rem}
.install-panel ol{padding-left:1.1rem;line-height:1.5;font-size:0.85rem} .install-panel ol{padding-left:1.1rem;line-height:1.5;font-size:.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 .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 .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 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:0.78rem;margin-top:0.6rem;border-left:2px solid var(--amber);padding-left:0.6rem} .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:5px;margin:0.4rem 0} .install-panel img{max-width:100%;border-radius:8px;margin:.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 pre{background:var(--soft);padding:.5rem .7rem;border-radius:8px;overflow-x:auto;font-size:.76rem;margin:.4rem 0}
/* ── "En savoir plus" fold ── */ /* ── "En savoir plus" fold ── */
.more{max-width:1080px;margin:0 auto;padding:0 1.5rem} .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:0.95rem;letter-spacing:0.05em;padding:0.9rem;border:1px dashed var(--purple);border-radius:8px;margin-bottom:1rem;transition:background .12s} .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::-webkit-details-marker{display:none}
.more>summary:hover{background:rgba(158,118,255,0.1)} .more>summary:hover{background:rgba(158,118,255,.08)}
.more[open]>summary{margin-bottom:1.6rem} .more[open]>summary{margin-bottom:1.4rem}
.more>summary .chev{display:inline-block;transition:transform .2s} .more>summary .chev{display:inline-block;transition:transform .2s}
.more[open]>summary .chev{transform:rotate(90deg)} .more[open]>summary .chev{transform:rotate(90deg)}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}} .v.tick{animation:flash .6s}@keyframes flash{0%{color:var(--gold);transform:scale(1.15)}100%{color:var(--phos-hot);transform:scale(1)}}
.v.tick{animation:flash 0.6s}
@keyframes flash{0%{color:var(--gold);transform:scale(1.15)}100%{color:var(--phos-hot);transform:scale(1)}}
</style></head><body> </style></head><body>
<div class=hero> <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> <div class=ctas>
<a href="/wg/r3-install" class="cta go">✨ Protège-moi (R3)</a> <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> </div>
{# trimmed quick-nav — CA iPhone / CA Android / QR profil moved into the
per-platform install panel below (#543) #}
<div class=quicknav> <div class=quicknav>
<a href="/wg/r3-install" class=qi title="Installer R3 WireGuard"> <a href="/wg/r3-install" class=qi title="Installer R3 WireGuard">
<span class=qi-emoji>🌐</span><span class=qi-label>R3 Install</span> <span class=qi-emoji>🌐</span><span class=qi-label>R3 Install</span>
</a> </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> <span class=qi-emoji>📊</span><span class=qi-label>Mon rapport</span>
</a> </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> <span class=qi-emoji>🕸️</span><span class=qi-label>Ma carto</span>
</a> </a>
<a href="https://github.com/CyberMind-FR/secubox-deb/wiki/R3-WireGuard-install" class=qi title="Wiki R3 multi-OS"> <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=container>
<div class=section style="margin-bottom:1.5rem"> <div class=section style="margin-bottom:1.5rem">
<h2>📥 Installe en 1 tap</h2> <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. 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. Le CA, le QR et le profil sont dedans. Autre appareil ? Déplie le bon panneau.
</p> </p>
{{ install_panels | safe }} {{ 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 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>. à tout moment. Page standalone : <a href=/wg/onboard>/wg/onboard</a>.
</p> </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) ── #} {# ── KPI live (auto-refresh 5s via /cumulative-stats.json) ── #}
<div class=section> <div class=section>
<h2>📊 Cabine en chiffres <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-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:0.7rem;color:var(--dim);margin-left:0.4rem">·</span> <span id=live-stamp style="font-size:.7rem;color:var(--dim);margin-left:.4rem">·</span>
</h2> </h2>
<div class="grid grid-4"> <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> <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 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="font-size:2.4rem;flex-shrink:0" id=cert-probe-emoji>❔</div>
<div style="flex:1;min-width:220px"> <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 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. (depuis le tunnel WireGuard). Le test télécharge une image HTTPS et vérifie le succès.
</p> </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) : 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> </p>
</div> </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>
</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, niveau d'analyse, tu obtiens un rapport détaillé sur les apps, trackers, certificats,
et risques observés pendant ta session. et risques observés pendant ta session.
</p> </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>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, <b>CSPN ANSSI</b> + <b>LCEN</b> : consentement explicite, hash MAC quotidien rotatif,
données effacées après 24h, aucun envoi externe. 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> </div>
<div class="level r3"> <div class="level r3">
<span class=emj>🌐</span> <span class=emj>🧅</span>
<div class=body> <div class=body>
<b>R3 — WireGuard portable<span class=tag-new>NOUVEAU</span></b> <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> </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 mpct = (risk.medium * 100 / total) | round(0) | int %}
{% set hpct = (risk.high * 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"> <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 * 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"/> <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-family=monospace font-size=10>🟢 {{ lpct }}% LOW</text> <text x=10 y=15 fill="#00ff55" 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 * 3 + 10 }}" y=15 fill="#ffb347" 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="{{ (lpct + mpct) * 3 + 10 }}" y=15 fill="#ff4466" font-size=10>🔴 {{ hpct }}% HI</text>
<text x=10 y=55 fill="#666" font-family=monospace font-size=8>{{ total }} sessions analysées</text> <text x=10 y=55 fill="#5a6b60" font-size=8>{{ total }} sessions analysées</text>
</svg> </svg>
<p class=help>La plupart des sessions sont à faible risque.</p>
</div> </div>
<div class=card> <div class=card>
<h3>🛡 Niveau d'opt-in choisi par les visiteurs</h3> <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 %} {% 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"> <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] %} {% 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=10 width="{{ ws[0] }}" height=15 rx=4 fill="#5a6b60"/>
<rect x=0 y=30 width="{{ ws[1] }}" height=15 fill="#00dd44"/> <rect x=0 y=30 width="{{ ws[1] }}" height=15 rx=4 fill="#00dd44"/>
<rect x=0 y=50 width="{{ ws[2] }}" height=15 fill="#ffb347"/> <rect x=0 y=50 width="{{ ws[2] }}" height=15 rx=4 fill="#ffb347"/>
<rect x=0 y=70 width="{{ ws[3] }}" height=15 fill="#9e76ff"/> <rect x=0 y=70 width="{{ ws[3] }}" height=15 rx=4 fill="#9e76ff"/>
<text x="{{ ws[0] + 5 }}" y=22 fill="#888" font-family=monospace font-size=10>🌐 R0 ({{ lvl.r0 }})</text> <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-family=monospace font-size=10>🛡 R1 ({{ lvl.r1 }})</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-family=monospace font-size=10>🔍 R2 ({{ lvl.r2 }})</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-family=monospace font-size=10>🌐 R3 ({{ lvl.r3 }})</text> <text x="{{ ws[3] + 5 }}" y=82 fill="#cbb6ff" font-size=10>🧅 R3 ({{ lvl.r3 }})</text>
</svg> </svg>
<p class=help>R1 (analyse passive) est le choix le plus courant.</p>
</div> </div>
</div> </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> <div class=arch>
LXC mitmproxy 10.100.0.60 WAF (vhosts CyberMind) LXC mitmproxy 10.100.0.60 WAF (vhosts CyberMind)
LXC toolbox-mitm 10.100.0.61 R1/R2 transparent 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 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> 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. (audit citoyen possible, droits d'usage régis par licence CMSD). Pas de boîte noire.
</p> </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" 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> <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> </div>
@ -326,11 +303,11 @@ LXC toolbox-mitm-wg 10.100.0.62 R3 WireGuard
{# ── Contact ── #} {# ── Contact ── #}
<div class=section> <div class=section>
<h2>📡 Contact & soutiens</h2> <h2>📡 Contact &amp; soutiens</h2>
<div class="grid grid-2"> <div class="grid grid-2">
<div class=card> <div class=card>
<h3>💚 Soutenir le projet</h3> <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 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>💳 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> <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>
<div class="card purple"> <div class="card purple">
<h3>🏢 Déploiement collectivité</h3> <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>📡 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>🎓 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> <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'); var ss = String(d.getSeconds()).padStart(2,'0');
return 'maj ' + hh+':'+mm+':'+ss; return 'maj ' + hh+':'+mm+':'+ss;
} }
// count-up: animate each KPI from 0 → its server-rendered value, once.
function countUp(el, target){ function countUp(el, target){
var start = 0, dur = 900, t0 = null; var start = 0, dur = 900, t0 = null;
function step(ts){ function step(ts){
@ -403,7 +379,7 @@ LXC toolbox-mitm-wg 10.100.0.62 R3 WireGuard
if (prev !== next) { if (prev !== next) {
el.textContent = next; el.textContent = next;
el.classList.remove('tick'); el.classList.remove('tick');
void el.offsetWidth; // force reflow void el.offsetWidth;
el.classList.add('tick'); el.classList.add('tick');
} }
}); });
@ -449,8 +425,8 @@ LXC toolbox-mitm-wg 10.100.0.62 R3 WireGuard
} else { } else {
emj.textContent = '🔴'; emj.textContent = '🔴';
txt.innerHTML = '<b>Tunnel R3 actif mais CA R3 NON trusté</b> — HTTPS casse. ' + 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> ' + '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(--orange)">ca.pem Android/PC</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 #} {# SPDX-License-Identifier: LicenseRef-CMSD-1.0 #}
{# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr> #} {# 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> <!DOCTYPE html>
<html lang="fr"><head> <html lang="fr"><head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<meta http-equiv="refresh" content="15"> <meta http-equiv="refresh" content="20">
{# Phase 3 (#492) : PWA tags for iOS Add-to-Home-Screen webclip experience #}
<meta name="apple-mobile-web-app-capable" content="yes"> <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-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="ToolBoX Cabine"> <meta name="apple-mobile-web-app-title" content="ToolBoX Cabine">
<meta name="theme-color" content="#0a0a0f"> <meta name="theme-color" content="#0a0a0f">
<title>Mon rapport Gondwana ToolBoX — live</title> <title>Mon rapport — VILLAGE3B</title>
<style> <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} *{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} 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);text-shadow:0 0 6px var(--phos);font-size:1.6rem;margin-bottom:0.3rem;letter-spacing:0.05em} 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:0.85rem;margin-bottom:1rem;letter-spacing:0.05em} .sub{color:var(--dim);font-size:.82rem;margin-bottom:1.1rem}
.card{border:1px solid var(--dim);background:rgba(0,221,68,0.03);padding:0.9rem 1rem;margin-bottom:1rem} .help{color:var(--dim);font-size:.78rem;font-style:italic;margin-top:.25rem}
.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} .card{border:1px solid var(--line);background:var(--panel);border-radius:12px;padding:1rem 1.1rem;margin-bottom:1rem}
.kv{display:grid;grid-template-columns:auto 1fr;gap:0.2rem 0.8rem;font-size:0.85rem} .card h2{color:var(--phos-hot);font-size:.95rem;margin-bottom:.5rem;letter-spacing:.03em}
.kv .k{color:var(--dim)} /* ── verdict hero ── */
.kv .v{color:var(--phos);text-shadow:0 0 4px var(--phos)} .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)}
ul{list-style:none;padding-left:0.6rem} .gauge{width:170px;height:170px;border-radius:50%;margin:.4rem auto;display:flex;align-items:center;justify-content:center}
li{padding:0.15rem 0;font-size:0.85rem} .gauge-hole{width:124px;height:124px;border-radius:50%;background:var(--bg);display:flex;flex-direction:column;align-items:center;justify-content:center}
li::before{content:"▸ ";color:var(--phos);text-shadow:0 0 4px var(--phos)} .gauge-num{font-size:2.6rem;font-weight:800;line-height:1}
.score{display:inline-block;padding:0.3rem 1rem;font-size:1.2rem;font-weight:bold;border:2px solid;border-radius:4px} .gauge-max{font-size:.8rem;color:var(--dim)}
.score.low{color:var(--phos-hot);border-color:var(--phos);text-shadow:0 0 6px var(--phos)} .verdict{font-size:1.15rem;font-weight:700;margin-top:.3rem}
.score.med{color:var(--amber);border-color:var(--amber);text-shadow:0 0 6px var(--amber)} /* ── KPI row ── */
.score.high{color:var(--red);border-color:var(--red);text-shadow:0 0 6px var(--red)} .kpis{display:grid;grid-template-columns:repeat(3,1fr);gap:.5rem;margin-top:.4rem}
.pin{color:var(--amber)} .kpi{background:#0d0f15;border:1px solid var(--line);border-radius:10px;padding:.6rem .3rem;text-align:center}
.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} .kpi .e{font-size:1.15rem}
.actions{text-align:center;margin:1.5rem 0} .kpi .n{font-size:1.3rem;font-weight:800;color:var(--phos-hot)}
.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} .kpi .l{font-size:.62rem;color:var(--dim);text-transform:uppercase;letter-spacing:.04em}
.actions a:hover{background:rgba(0,221,68,0.1)} /* ── graphs ── */
.refresh{text-align:center;font-size:0.7rem;color:var(--dim);margin-top:1rem;font-style:italic} .graphs{display:grid;grid-template-columns:1fr 1fr;gap:1rem}
.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} @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> </style></head>
<body> <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 m = metrics or {} %}
{% set sc = risk_score or 0 %} {% set sc = risk_score|default(0) %}
{% set rl = risk_label or 'LOW' %} {% set rl = risk_label|default('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)"> {% set ch = charts or {} %}
<h2 style="display:flex;justify-content:space-between;align-items:center"> {% set gcol = 'var(--phos-hot)' if sc < 30 else ('var(--amber)' if sc < 70 else 'var(--red)') %}
📊 Ta session VILLAGE3B {% set palette = ['#00dd44','#9e76ff','#ff8866','#66bbff','#ffb347','#ff4466'] %}
<span style="font-size:0.75rem;padding:0.3rem 0.8rem;border-radius:99px; {% set dpi_cls = dpi_classified or {} %}
background:{% if sc < 30 %}#00cc44{% elif sc < 70 %}#ffb347{% else %}#ff4466{% endif %}; {% set cookies_p = cookies_providers or [] %}
color:#0a0a0f;font-weight:bold"> {% set geo_h = geo_top_hosts or [] %}
{% if sc < 30 %}🟢{% elif sc < 70 %}🟡{% else %}🔴{% endif %} {{ rl }} {{ sc }}/100 {% set n_apps = (dpi_cls.top_apps|default([])|selectattr('app','ne','?')|list|length) %}
</span> {% set n_trackers = (cookies_p|map(attribute='count')|sum) %}
</h2> {% set n_countries = (geo_h|map(attribute='country')|reject('equalto','')|list|unique|list|length) %}
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:0.5rem;margin-top:0.8rem"> {% set _avatar = avatar_analysis or {} %}
{% 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>
{# Phase 3 (#492) : filtering compromissions visibility #} <h1>👁️ VILLAGE3B <span style="font-size:.8rem;color:var(--dim);font-weight:400">· mon rapport</span></h1>
{% set t = transparency|default({}) %} <p class="sub">Diagnostic live de ce que ton appareil envoie sur le réseau · anonyme · se rafraîchit tout seul</p>
{% 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>
{# 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')) %} {% 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)"> <div class="card" style="border-color:var(--phos)">
<h2 style="color:var(--phos-hot)"> <b style="color:var(--phos-hot)">{% if request_args.get('switched') %}🔄 Niveau changé{% else %}🎉 Bienvenue !{% endif %}</b> —
{% if request_args.get('switched') %}🔄 Niveau changé{% else %}🎉 Bienvenue !{% endif %} tu es en mode
</h2> {% 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 %}.
<p style="font-size:0.85rem"> Tu peux surfer normalement.
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> </div>
{% endif %} {% endif %}
{# Phase 3 (#492) : level switcher with active highlight from server-side {# ── VERDICT HERO : score gauge + plain verdict ── #}
current_level. Disables button if user clicks their own level (no-op). #} <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"> <div class="card">
<h2>🔀 Mon niveau d'opt-in</h2> <h2>📊 En un coup d'œil</h2>
<p style="font-size:0.78rem;color:var(--dim);margin-bottom:0.5rem"> <div class="graphs">
📍 Tu es actuellement en
<b style="color:{% if current_level == 'r0' %}var(--text){% elif current_level == 'r2' %}#ffd6a0{% else %}var(--phos-hot){% endif %}"> {# trackers donut #}
{% if current_level == 'r0' %}🌐 R0 — Bypass complet <div>
{% elif current_level == 'r2' %}🔍 R2 — Analyse + bandeau <div style="font-size:.82rem;color:var(--dim);margin-bottom:.4rem">🍪 Qui te trace</div>
{% else %}🛡 R1 — Analyse passive{% endif %} {% if ch.trackers %}
</b> <div class="donut-wrap">
· clique sur un autre niveau pour switcher <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 %})">
</p> <div class="donut-hole">{{ n_trackers }}<br>traceurs</div>
<form method="POST" action="/change-level" style="display:grid;grid-template-columns:repeat(3,1fr);gap:0.4rem"> </div>
<button type="submit" name="level" value="r0" <div class="legend">
style="padding:0.5rem;cursor:pointer;font-family:inherit;font-size:0.85rem; {% for t in ch.trackers %}
{% if current_level == 'r0' %} <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>
background:rgba(255,255,255,0.08);color:var(--text);border:2px solid var(--text);font-weight:bold; {% endfor %}
{% else %} </div>
background:transparent;color:var(--text);border:1px solid var(--dim); </div>
{% endif %}"> {% else %}<div class="empty">Aucun traceur repéré 🎉</div>{% endif %}
{% if current_level == 'r0' %}✓ {% endif %}🌐 R0 </div>
</button>
<button type="submit" name="level" value="r1" {# countries bars #}
style="padding:0.5rem;cursor:pointer;font-family:inherit;font-size:0.85rem; <div>
{% if current_level == 'r1' %} <div style="font-size:.82rem;color:var(--dim);margin-bottom:.4rem">🌍 Vers quels pays</div>
background:rgba(0,221,68,0.25);color:var(--phos-hot);border:2px solid var(--phos);font-weight:bold; {% if ch.countries %}
{% else %} {% for c in ch.countries %}
background:transparent;color:var(--phos);border:1px solid var(--dim); <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>
{% endif %}"> {% endfor %}
{% if current_level == 'r1' %}✓ {% endif %}🛡 R1 {% else %}<div class="empty">Pas encore de données géo</div>{% endif %}
</button> </div>
<button type="submit" name="level" value="r2"
style="padding:0.5rem;cursor:pointer;font-family:inherit;font-size:0.85rem; {# apps bars #}
{% if current_level == 'r2' %} <div style="grid-column:1/-1">
background:rgba(255,179,71,0.25);color:#ffd6a0;border:2px solid #ffb347;font-weight:bold; <div style="font-size:.82rem;color:var(--dim);margin-bottom:.4rem">📺 Quelles apps / services</div>
{% else %} {% if ch.apps %}
background:transparent;color:#9d7846;border:1px solid var(--dim); {% for a in ch.apps %}
{% endif %}"> <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>
{% if current_level == 'r2' %}✓ {% endif %}🔍 R2 {% endfor %}
</button> {% 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> </form>
{% if wg_enabled|default(false) %} <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>
{# 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 %}
</div> </div>
<div class="card"> {# ════════════ DÉTAILS TECHNIQUES (repliés) ════════════ #}
<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>
<div class="card"> <details>
<h2>📊 Métriques session</h2> <summary>🎯 Analyse de compromission &amp; score</summary>
<div class="kv"> <div class="inner">
<span class="k">Connexions</span> <span class="v">{{ metrics.connections }}</span> {% if risk_explanation %}<p style="font-size:.85rem;margin-bottom:.6rem">{{ risk_explanation }}</p>{% endif %}
<span class="k">Hosts uniques</span> <span class="v">{{ metrics.unique_hosts }}</span> <ul>{% for ind in indicators %}<li>{{ ind }}</li>{% endfor %}</ul>
<span class="k">Réussies</span> <span class="v">{{ metrics.successful }}</span> {% if scoring and scoring.breakdown %}
<span class="k">Cert-pin block</span> <span class="v">{{ metrics.tls_pinned }}</span> <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>
</div> {% for b in scoring.breakdown %}
</div> <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 %}
<div class="card"> </tbody></table>
<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>
{% endif %} {% endif %}
</p> </div>
{% if risk_explanation %} </details>
<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>
{% if scoring and scoring.breakdown %} {% if threat_intel_matches or dga_candidates or beaconing_candidates %}
<div class="card"> <details>
<h2>🔬 Breakdown du score (transparent)</h2> <summary class="pin">🚨 Menaces détectées ({{ (threat_intel_matches|default([]))|length + (dga_candidates|default([]))|length + (beaconing_candidates|default([]))|length }})</summary>
<table style="width:100%;font-size:0.82rem;border-collapse:collapse"> <div class="inner">
<thead><tr style="color:var(--dim);border-bottom:1px solid var(--dim)"> {% if threat_intel_matches %}<p style="color:var(--amber);font-size:.82rem;margin:.3rem 0">🚨 Threat-intel (feeds malware)</p><ul>
<th style="padding:0.2rem 0.4rem;text-align:left">Catégorie</th> {% 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 %}
<th style="padding:0.2rem 0.4rem;text-align:right">Signaux</th> {% if dga_candidates %}<p style="color:var(--amber);font-size:.82rem;margin:.5rem 0 .2rem">🔠 Domaines générés (DGA)</p><ul>
<th style="padding:0.2rem 0.4rem;text-align:right">Poids</th> {% for d in dga_candidates[:8] %}<li>{{ d.flag }} [{{ d.score }}] <code>{{ d.host[:50] }}</code></li>{% endfor %}</ul>{% endif %}
</tr></thead> {% if beaconing_candidates %}<p style="color:var(--amber);font-size:.82rem;margin:.5rem 0 .2rem">📡 Beaconing (périodique)</p><ul>
<tbody> {% for b in beaconing_candidates[:8] %}<li>{{ b.flag }} [{{ b.score }}] <code>{{ b.host[:40] }}</code> · {{ b.median_seconds }}s</li>{% endfor %}</ul>{% endif %}
{% for b in scoring.breakdown %} </div>
<tr><td style="padding:0.15rem 0.4rem">{{ b.category }}</td> </details>
<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>
{% endif %} {% endif %}
{% if geo_top_hosts %} {% if geo_top_hosts %}
<div class="card"> <details>
<h2>🌍 Hôtes contactés (par pays + ASN + app)</h2> <summary>🌍 Hôtes contactés ({{ geo_top_hosts|length }})</summary>
<table style="width:100%;font-size:0.82rem"> <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>
<thead><tr style="color:var(--dim);text-align:left"> {% for h in geo_top_hosts[:20] %}
<th>🚩</th><th>App</th><th>Hôte</th><th>ASN</th><th style="text-align:right">Hits</th> <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>
</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>
{% endfor %} {% endfor %}
</tbody> </tbody></table></div>
</table> </details>
</div>
{% endif %} {% endif %}
{% if avatar_analysis and avatar_analysis.devices %} {% if dpi_classified and dpi_classified.top_apps %}
<div class="card"> <details>
<h2>{{ avatar_analysis.most_common_emoji }} Avatar / device fingerprint</h2> <summary>🧭 Apps détectées (nDPI)</summary>
<p style="margin-bottom:0.5rem"> <div class="inner"><table><thead><tr><th>App</th><th>Catégorie</th><th style="text-align:right">Conn.</th></tr></thead><tbody>
<span style="font-size:1.2rem">{{ avatar_analysis.most_common_emoji }}</span> {% for a in dpi_classified.top_apps[:20] %}
<b>{{ avatar_analysis.most_common }}</b> (le plus représenté) <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>
</p> {% endfor %}
<div style="display:flex;flex-wrap:wrap;gap:1rem;font-size:0.85rem"> </tbody></table></div>
<div> </details>
<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>
{% endif %} {% endif %}
{% if cookies_providers %} {% if cookies_providers %}
<div class="card"> <details>
<h2>🍪 Trackers / providers cookies (Phase 2a+)</h2> <summary>🍪 Traceurs / providers cookies ({{ cookies_providers|length }})</summary>
<ul> <div class="inner"><ul>
{% for p in cookies_providers[:10] %} {% 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 %}
<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> </ul></div>
{% endfor %} </details>
</ul>
</div>
{% endif %} {% endif %}
<div class="card"> {% if avatar_analysis and avatar_analysis.devices %}
<h2>📱 Apps détectées (vue IP forensics)</h2> <details>
<ul> <summary>{{ _avatar.most_common_emoji }} Empreinte appareil</summary>
{% for app in apps_detected %}<li>{{ app }}</li>{% endfor %} <div class="inner">
</ul> <p style="margin-bottom:.4rem">{{ _avatar.most_common_emoji }} <b>{{ _avatar.most_common }}</b> · {{ _avatar.raw_count|default(0) }} UAs distincts</p>
</div> <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 class="card"> <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>
<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] }}
</div> </div>
{% endfor %} </div>
{% endif %} </details>
</div>
{% endif %} {% endif %}
{% if soc and soc.indicators %} {% if pinned_apps %}
<div class="card"> <details>
<h2 class="pin">⚠ SOC — indicateurs détectés</h2> <summary>🔒 Apps protégées (cert-pinning) ({{ pinned_apps|length }})</summary>
<p style="font-size:0.78rem;color:var(--dim);margin-bottom:0.5rem"> <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>
Score SOC actuel : <b style="color:var(--amber)">{{ soc.score }}/100</b> <ul>{% for app in pinned_apps %}<li>{{ app }}</li>{% endfor %}</ul></div>
</p> </details>
<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>
{% endif %} {% 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({}) %} {% set t = transparency|default({}) %}
{% if t and t.get('total_events', 0) > 0 %} {% if t and t.get('total_events', 0) > 0 %}
<div class="card"> <details>
<h2>🔎 INSPECTION : CE QU'ON A REGARDÉ (et ce qu'on n'a PAS regardé)</h2> <summary>🔎 Transparence : ce qu'on a regardé</summary>
<p style="font-size:0.78rem;color:var(--dim);margin-bottom:0.6rem;font-style:italic"> <div class="inner">
Honnêteté avant magie : la cabine te dit ce qu'elle a inspecté, ce qu'elle a sciemment bypassé, et pourquoi. <p class="help" style="margin-bottom:.5rem">Honnêteté avant magie : ce qu'on a inspecté, bypassé, et pourquoi.</p>
</p> <div class="kv">
<div class="kv"> {% set b = t.get('breakdown_pct', {}) %}
{% 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('inspected') %} {% if b.get('pinned-failed-mitm') %}<span class="k">🔒 Cert-pinning</span><span class="v">{{ b.get('pinned-failed-mitm') }}%</span>{% endif %}
<div class="k">🔍 Inspecté (HTTPS via notre CA)</div> {% if b.get('e2e-opaque') %}<span class="k">🔐 E2E chiffré</span><span class="v">{{ b.get('e2e-opaque') }}%</span>{% endif %}
<div class="v">{{ b.get('inspected', 0) }}% — contenu visible</div> <span class="k">📊 Total events</span><span class="v">{{ t.get('total_events', 0) }}</span>
{% 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 %}
</div> </div>
</div> {% if t.get('per_host') %}
{% endif %} <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] %}
{# Phase 3 (#492) : whitelist hits — accountability per pattern/category #} <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>
{% 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>
{% endfor %} {% endfor %}
</div> </tbody></table>
<ul style="margin-top:0.4rem"> {% endif %}
{% for p in wh.get('top_patterns', [])[:8] %}
<li style="font-size:0.72rem"><code>{{ p.pattern }}</code> · {{ p.count }} hits</li>
{% endfor %}
</ul>
</div> </div>
{% endif %} </details>
</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 %}
{% endif %} {% 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"> <div class="card">
<h2>💚 Support &amp; soutien projet</h2> <h2>💚 Soutenir le projet</h2>
<p style="font-size:0.82rem;color:var(--text);line-height:1.55"> <p class="help">Commun numérique open-source, sans pub ni revente — CyberMind / Gérald Kerma (Savoie).</p>
Gondwana ToolBoX est un <b>commun numérique</b> open-source maintenu par <ul style="margin-top:.4rem">
CyberMind / Gérald Kerma (Notre-Dame-du-Cruet, Savoie). Pas de pub, pas de <li>💰 <a href="https://liberapay.com/cybermind" style="color:var(--phos)">liberapay.com/cybermind</a></li>
revente, pas de tracking commercial. Si ce service t'a aidé : <li>🐛 <a href="https://github.com/CyberMind-FR/secubox-deb/issues" style="color:var(--phos)">Signaler un bug</a></li>
</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>
</ul> </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>
<div class="footer"> <div class="footer">
Gondwana ToolBoX · LicenseRef-CMSD-1.0 (Source-Disclosed License)<br> Gondwana ToolBoX · LicenseRef-CMSD-1.0 · ↻ rafraîchit toutes les 20 s<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> <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>
CyberMind — Notre-Dame-du-Cruet (73130) · <a href="https://cybermind.fr" style="color:var(--dim)">cybermind.fr</a>
</div> </div>
</body></html> </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 secubox-toolbox (2.7.6-1~bookworm1) bookworm; urgency=medium
* fix(#683): the 🧅 Tor indicator now appears on the ACTUAL injected banner. * 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 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 ───────────────── # ───────────────── Public routes ─────────────────
@router.get("/", response_class=HTMLResponse) @router.get("/", response_class=HTMLResponse)
@ -701,11 +731,15 @@ async def landing(request: Request) -> HTMLResponse:
stats = _cumulative_stats() stats = _cumulative_stats()
platform = _ua_platform(request.headers.get("user-agent") or "") platform = _ua_platform(request.headers.get("user-agent") or "")
install_panels = _install_panels_html(platform) 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( return HTMLResponse(
_env.get_template("landing.html.j2").render( _env.get_template("landing.html.j2").render(
stats=stats, stats=stats,
install_panels=install_panels, install_panels=install_panels,
install_platform=platform, install_platform=platform,
mac_hash=mac_hash,
), ),
headers={"Cache-Control": "private, max-age=60, no-transform"}, headers={"Cache-Control": "private, max-age=60, no-transform"},
) )
@ -2308,6 +2342,46 @@ def _classify_apps(hosts: set[str]) -> list[str]:
return apps 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, # NOTE: route order matters in FastAPI — specific routes (/report/me,
# /report/me/html) MUST be declared BEFORE the catch-all /report/{token}, # /report/me/html) MUST be declared BEFORE the catch-all /report/{token},
# otherwise FastAPI matches /report/me with token="me" and returns 404. # 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 their own report. The hash for R3 = sha256(wg_pubkey)[:16] derived
by inject_banner.py and embedded in the banner 'Mon rapport' link. 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) # Resolve identity the same way everywhere: ?mh → R3 WG peer (wg-peers.json)
mh_qp = (request.query_params.get("mh") or "").strip().lower() # → captive ARP. R3 clients hitting this directly (no ?mh) now resolve too.
if mh_qp and all(c in "0123456789abcdef" for c in mh_qp) and 8 <= len(mh_qp) <= 64: mac_hash = _client_mac_hash(request, _get_salt())
ip = request.client.host if request.client else "?" if not mac_hash:
mac_hash = mh_qp raise HTTPException(
else: 400,
ip, mac = _resolve(request) "client identity unresolved (not on R3 tunnel and not in captive "
if not mac: "subnet) — append ?mh=<hash> from your banner's report link",
raise HTTPException(400, "client MAC unknown (not in captive subnet?) — use ?mh=<hash>") )
salt = _get_salt() ip = _client_ip(request) or (request.client.host if request.client else "?")
mac_hash = macmod.hash_mac(mac, salt)
session = _aggregate_session(mac_hash) session = _aggregate_session(mac_hash)
# Phase 3 (#492) : pass query args + force no-cache so iPhone Safari # Phase 3 (#492) : pass query args + force no-cache so iPhone Safari
# actually fetches the new template. # 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", current_level=store.get_client_level(mac_hash) if mac_hash else "r1",
wg_enabled=wg_enabled, wg_enabled=wg_enabled,
cumulative=cumulative, cumulative=cumulative,
charts=_build_report_charts(session),
**session, **session,
) )
return HTMLResponse(html, headers={ return HTMLResponse(html, headers={

View File

@ -151,12 +151,23 @@ _BANNER_CORE = r"""
return Object.keys(seen).length; return Object.keys(seen).length;
} catch (_) { return 0; } } 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){ function render(b){
if (dismissed) return; if (dismissed) return;
if (document.getElementById("sbx-banner")) return; if (document.getElementById("sbx-banner")) return;
var trk = countTrackers(b.tracker_patterns); var trk = countTrackers(b.tracker_patterns);
var ck = 0; var ck = countCookies();
try { ck = document.cookie ? document.cookie.split(";").filter(function(x){return x.indexOf("=")>=0;}).length : 0; } catch (_) {}
var bar = document.createElement("div"); var bar = document.createElement("div");
bar.id = "sbx-banner"; bar.id = "sbx-banner";
bar.setAttribute("style", "position:fixed;left:0;right:0;top:0;z-index:2147483647;" bar.setAttribute("style", "position:fixed;left:0;right:0;top:0;z-index:2147483647;"
@ -174,8 +185,8 @@ _BANNER_CORE = r"""
+ cspProof + cspProof
+ tor + tor
+ "<span>" + esc((b.level || "r1").toUpperCase()) + "</span>" + "<span>" + esc((b.level || "r1").toUpperCase()) + "</span>"
+ "<span>🛰️ " + trk + " trackers</span>" + "<span id=\"sbx-trk\">🛰️ " + trk + " trackers</span>"
+ "<span>🍪 " + ck + " cookies</span>" + "<span id=\"sbx-ck\">🍪 " + ck + " cookies</span>"
+ pin + pin
+ "<a href=\"" + esc(b.report_url || "#") + "\" style=\"margin-left:auto;color:#2C70C0;text-decoration:none\">report ▸</a>" + "<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>"; + "<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 // 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). // 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), // 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. // plus a light 2s poll as a catch-all for DOM re-renders that drop the banner.
["pushState","replaceState"].forEach(function(m){ ["pushState","replaceState"].forEach(function(m){