feat(toolbox): #ads dashboard surfaces detected candidates (#802)

ad_stats now returns total_candidates + top_candidates from ad_candidates
(observed ad/tracker hosts awaiting autolearn promotion to active 204-blocking).
The #ads tab gains a '🔎 Trackers/pubs détectés' card so the dashboard reflects
real detection even when total_blocked is 0 (nothing promoted yet) — was
showing all-zeros + empty tables despite live detection. Deployed to gk2.
This commit is contained in:
CyberMind-FR 2026-07-04 14:56:25 +02:00
parent 85d1fd8bcc
commit 06db2ff68e
2 changed files with 22 additions and 1 deletions

View File

@ -173,7 +173,7 @@ def ad_stats(hours: int = 24, top: int = 25) -> dict:
cutoff = time.time() - hours * 3600
out = {"window_hours": hours, "total_blocked": 0, "total_bytes": 0,
"by_action": {"block": 0, "silent": 0}, "top_hosts": [], "top_sites": [],
"top_visitors": []}
"top_visitors": [], "total_candidates": 0, "top_candidates": []}
try:
with _conn() as c:
for action, hits in c.execute(
@ -194,6 +194,18 @@ def ad_stats(hours: int = 24, top: int = 25) -> dict:
"SELECT mac_hash, SUM(hits) FROM ad_block_client_host "
"WHERE last_seen>=? AND mac_hash<>'' GROUP BY mac_hash "
"ORDER BY SUM(hits) DESC LIMIT ?", (cutoff, top))]
# #802 — DETECTED ad/tracker hosts (ad_candidates: observed, awaiting
# autolearn promotion to active 204-blocking). Surface them so the
# dashboard reflects real detection even when nothing is blocked yet.
try:
out["top_candidates"] = [{"host": h, "hits": int(n)} for h, n in c.execute(
"SELECT host, SUM(hits) FROM ad_candidates WHERE last_seen>=? "
"GROUP BY host ORDER BY SUM(hits) DESC LIMIT ?", (cutoff, top))]
r = c.execute("SELECT COUNT(DISTINCT host) FROM ad_candidates WHERE last_seen>=?",
(cutoff,)).fetchone()
out["total_candidates"] = int((r and r[0]) or 0)
except sqlite3.Error:
pass
# #755 — trackers detected/poisoned by the MITM in the window: distinct
# cross-site cookie-identifier hashes seen on social_edges. This is the
# "Trackers" half of the card (the 204 ad-block is the "pubs" half).

View File

@ -185,6 +185,10 @@
<h2>🎯 Top hôtes publicitaires bloqués</h2>
<div id="ads-hosts"><div class="empty">loading…</div></div>
</div>
<div class="card" style="grid-column:1/-1">
<h2>🔎 Trackers/pubs détectés <span style="font-size:.72rem;opacity:.6">(candidats — observés, pas encore promus au blocage)</span></h2>
<div id="ads-candidates"><div class="empty">loading…</div></div>
</div>
<div class="card" style="grid-column:1/-1">
<h2>🌐 Top sites visités</h2>
<div id="ads-sites"><div class="empty">loading…</div></div>
@ -640,6 +644,11 @@ async function loadAds() {
document.getElementById('ads-sites').innerHTML = siteRows
? '<table><thead><tr><th>Site</th><th>trackers/pubs bloqués</th></tr></thead><tbody>'+siteRows+'</tbody></table>'
: '';
// #802 — detected candidates (observed, awaiting promotion to active blocking)
const candRows = (d.top_candidates||[]).slice(0, 15).map(r=>`<tr><td><code>${esc(r.host)}</code></td><td>${r.hits}</td></tr>`).join('');
document.getElementById('ads-candidates').innerHTML = candRows
? `<div style="font-size:.78rem;opacity:.7;margin-bottom:.4rem">${d.total_candidates||0} hôtes détectés dans la fenêtre</div><table><thead><tr><th>Hôte détecté</th><th>observations</th></tr></thead><tbody>${candRows}</tbody></table>`
: '<div class="empty">aucun tracker/pub détecté dans la fenêtre</div>';
const visRows = (d.top_visitors||[]).slice(0, 5).map(r=>{
const mh = esc(r.mac_hash);
return `<tr><td><a href="#" onclick="loadAdsClient('${mh}');return false;"><code>${mh}</code></a></td><td>${r.hits}</td></tr>`;