mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-01 10:47:30 +00:00
Compare commits
4 Commits
75e414eb01
...
46277413c3
| Author | SHA1 | Date | |
|---|---|---|---|
| 46277413c3 | |||
| d87b2ea936 | |||
| 7b0314c21a | |||
| bf0b8ff323 |
|
|
@ -5,10 +5,18 @@
|
|||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
||||
<title>{{ t.page_title }} — VILLAGE3B</title>
|
||||
<link rel="stylesheet" href="/toolbox/social.css">
|
||||
<script>
|
||||
// Inline JSON safer than a data-* attribute : FR text has
|
||||
// apostrophes (l'effacement) that would break single-quoted
|
||||
// HTML attrs. Wrapped in <script> JSON is verbatim so long as
|
||||
// it doesn't contain </script (json.dumps from Python never
|
||||
// emits raw "</" in normal output).
|
||||
window.__SOCIAL_I18N__ = {{ t_json | safe }};
|
||||
</script>
|
||||
<script defer src="/toolbox/d3.v7.min.js"></script>
|
||||
<script defer src="/toolbox/social.js"></script>
|
||||
</head>
|
||||
<body data-token="{{ token }}" data-lang="{{ lang }}" data-i18n='{{ t_json | safe }}'>
|
||||
<body data-token="{{ token }}" data-lang="{{ lang }}">
|
||||
|
||||
<header class="social-header">
|
||||
<div class="brand">
|
||||
|
|
@ -20,21 +28,13 @@
|
|||
</header>
|
||||
|
||||
<main class="social-main">
|
||||
<p class="social-intro">{{ t.intro }}</p>
|
||||
|
||||
<section class="stats" aria-live="polite">
|
||||
<div class="stat-tile"><span class="stat-n" data-bind="total_trackers">0</span><span class="stat-l">{{ t.stats_total_trackers }}</span></div>
|
||||
<div class="stat-tile"><span class="stat-n" data-bind="total_sites">0</span><span class="stat-l">{{ t.stats_total_sites }}</span></div>
|
||||
</section>
|
||||
|
||||
<section class="graph-wrap" aria-label="{{ t.header_subtitle }}">
|
||||
<section class="stats" aria-live="polite">
|
||||
<div class="stat-tile"><span class="stat-n" data-bind="total_trackers">0</span><span class="stat-l">{{ t.stats_total_trackers }}</span></div>
|
||||
<div class="stat-tile"><span class="stat-n" data-bind="total_sites">0</span><span class="stat-l">{{ t.stats_total_sites }}</span></div>
|
||||
</section>
|
||||
<p class="graph-hint">{{ t.graph_swipe_hint }}</p>
|
||||
<svg id="social-graph" viewBox="0 0 600 600" role="img"></svg>
|
||||
<div id="social-empty" class="social-empty" hidden>
|
||||
<div class="empty-title">{{ t.empty_title }}</div>
|
||||
<div class="empty-body">{{ t.empty_body }}</div>
|
||||
</div>
|
||||
<div id="social-loading" class="social-loading">{{ t.loading }}</div>
|
||||
<svg id="social-graph" role="img" preserveAspectRatio="xMidYMid meet"></svg>
|
||||
</section>
|
||||
|
||||
<aside id="node-detail" class="node-detail" hidden aria-live="polite">
|
||||
|
|
@ -56,20 +56,20 @@
|
|||
</dl>
|
||||
</aside>
|
||||
|
||||
<details class="card">
|
||||
<summary>{{ t.card_evidence }}</summary>
|
||||
<p class="card-pending">{{ t.card_evidence_pending }}</p>
|
||||
</details>
|
||||
|
||||
<details class="card card-wipe">
|
||||
<summary>{{ t.card_wipe }}</summary>
|
||||
<button type="button" class="wipe-btn" data-action="open-wipe">{{ t.wipe_button }}</button>
|
||||
</details>
|
||||
|
||||
<details class="card">
|
||||
<summary>{{ t.card_pdf }}</summary>
|
||||
<p class="card-pending">{{ t.card_pdf_pending }}</p>
|
||||
</details>
|
||||
<nav class="cards-row">
|
||||
<details class="card">
|
||||
<summary>{{ t.card_evidence }}</summary>
|
||||
<p class="card-pending">{{ t.card_evidence_pending }}</p>
|
||||
</details>
|
||||
<details class="card card-wipe">
|
||||
<summary>{{ t.card_wipe }}</summary>
|
||||
<button type="button" class="wipe-btn" data-action="open-wipe">{{ t.wipe_button }}</button>
|
||||
</details>
|
||||
<details class="card">
|
||||
<summary>{{ t.card_pdf }}</summary>
|
||||
<p class="card-pending">{{ t.card_pdf_pending }}</p>
|
||||
</details>
|
||||
</nav>
|
||||
</main>
|
||||
|
||||
<dialog id="wipe-modal" aria-labelledby="wipe-modal-title">
|
||||
|
|
|
|||
|
|
@ -18,6 +18,14 @@ BYPASS_FILE=/var/lib/secubox/toolbox/mitm-bypass.conf
|
|||
DYNAMIC_FILE=/var/lib/secubox/toolbox/mitm-bypass-dynamic.conf # noqa: SC2034 (used by addon hint)
|
||||
ADDON_DIR=/usr/lib/secubox/toolbox/mitmproxy_addons
|
||||
|
||||
# Phase 11.A (#505) — make the secubox_toolbox package importable from
|
||||
# addons running inside /opt/mitmproxy-toolbox/bin/python3 (a venv that
|
||||
# has its own site-packages and never sees /usr/lib/secubox/toolbox).
|
||||
# Without this PYTHONPATH every addon's `from secubox_toolbox import …`
|
||||
# silently degrades — inject_banner loses host classification + GeoIP,
|
||||
# social_graph loses its SQLite store reference, etc.
|
||||
export PYTHONPATH="/usr/lib/secubox/toolbox${PYTHONPATH:+:$PYTHONPATH}"
|
||||
|
||||
# ── Compose ignore_hosts regex : merge static + dynamic bypass lists ──
|
||||
# Phase 6.N (#496) : the dynamic file is auto-populated by the cert_pin_detect
|
||||
# addon when it observes repeated TLS handshake failures (cert pinning).
|
||||
|
|
|
|||
|
|
@ -19,25 +19,33 @@
|
|||
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; background: var(--cosmos-black); color: var(--text-primary); }
|
||||
html, body { height: 100%; }
|
||||
body {
|
||||
font-family: 'IM Fell English', Georgia, serif;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
min-height: 100vh;
|
||||
/* Full-viewport flex layout : header (sticky), main (flex 1, owns the
|
||||
graph), node-detail (slides over). Cards live inside main as a
|
||||
compact bottom row so the graph stays hero. */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
/* Header — compact, sticky, doesn't steal vertical space from the graph. */
|
||||
.social-header {
|
||||
display: flex; align-items: baseline; justify-content: space-between;
|
||||
padding: 14px 18px;
|
||||
flex: 0 0 auto;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 8px 14px;
|
||||
border-bottom: 1px solid var(--gold-hermetic);
|
||||
background: linear-gradient(180deg, rgba(201,168,76,.06), transparent);
|
||||
}
|
||||
.brand-title {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-weight: 600;
|
||||
font-size: 22px;
|
||||
font-size: 18px;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--gold-hermetic);
|
||||
}
|
||||
|
|
@ -52,37 +60,52 @@ body {
|
|||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
/* Main */
|
||||
.social-main { padding: 14px 16px 24px; max-width: 720px; margin: 0 auto; }
|
||||
.social-intro { color: var(--text-muted); font-size: 14px; margin: 8px 0 16px; }
|
||||
|
||||
/* Stats tiles */
|
||||
.stats { display: flex; gap: 12px; margin-bottom: 18px; }
|
||||
.stat-tile {
|
||||
/* Main : flex column, takes all remaining height after the header.
|
||||
Graph fills the body of main ; stats + cards float over the corners. */
|
||||
.social-main {
|
||||
flex: 1;
|
||||
background: rgba(0, 212, 255, .04);
|
||||
border: 1px solid rgba(0, 212, 255, .2);
|
||||
border-radius: 4px;
|
||||
padding: 10px 12px;
|
||||
display: flex; flex-direction: column;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0; /* allow children to shrink past content size */
|
||||
overflow: hidden;
|
||||
}
|
||||
/* Phase 11.B v2 — intro paragraph hidden in full-viewport mode ; the
|
||||
graph itself + the hover hint communicate the same thing. */
|
||||
.social-intro { display: none; }
|
||||
|
||||
/* Stats tiles — compact horizontal strip floating top-left over the
|
||||
graph. Glassy background so the graph stays visually dominant. */
|
||||
.stats {
|
||||
position: absolute; top: 8px; left: 8px;
|
||||
display: flex; gap: 6px;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.stat-tile {
|
||||
background: rgba(10, 10, 15, .65);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
border: 1px solid rgba(0, 212, 255, .35);
|
||||
border-radius: 3px;
|
||||
padding: 4px 9px;
|
||||
display: flex; align-items: baseline; gap: 5px;
|
||||
}
|
||||
.stat-n {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 22px;
|
||||
font-size: 16px;
|
||||
color: var(--cyber-cyan);
|
||||
line-height: 1;
|
||||
}
|
||||
.stat-l { font-size: 12px; color: var(--text-muted); }
|
||||
.stat-l { font-size: 10px; color: var(--text-muted); }
|
||||
|
||||
/* Graph */
|
||||
/* Graph fills all remaining vertical space inside .social-main. */
|
||||
.graph-wrap {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
background: rgba(110, 64, 201, .03);
|
||||
border: 1px solid rgba(110, 64, 201, .25);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 1 / 1;
|
||||
max-height: 70vh;
|
||||
}
|
||||
.graph-hint {
|
||||
position: absolute; top: 8px; right: 12px;
|
||||
|
|
@ -91,20 +114,6 @@ body {
|
|||
pointer-events: none;
|
||||
}
|
||||
#social-graph { display: block; width: 100%; height: 100%; touch-action: none; }
|
||||
.social-loading {
|
||||
position: absolute; inset: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: var(--text-muted); font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
.social-empty {
|
||||
position: absolute; inset: 0;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
color: var(--matrix-green);
|
||||
text-align: center;
|
||||
font-family: 'Cinzel', serif;
|
||||
}
|
||||
.empty-title { font-size: 22px; letter-spacing: 0.1em; margin-bottom: 8px; }
|
||||
.empty-body { font-family: 'IM Fell English', serif; font-style: italic; color: var(--matrix-green); opacity: .8; }
|
||||
|
||||
/* d3 node + edge styling — colors come from the data class on each element. */
|
||||
.node circle { stroke: var(--cosmos-black); stroke-width: 1.5; }
|
||||
|
|
@ -154,21 +163,49 @@ body {
|
|||
.node-detail dt { color: var(--text-muted); font-style: italic; }
|
||||
.node-detail dd { margin: 0; color: var(--text-primary); font-family: 'JetBrains Mono', monospace; font-size: 12px; word-break: break-word; }
|
||||
|
||||
/* Cards */
|
||||
/* Cards row — floats at the bottom of the viewport as a 3-cell
|
||||
horizontal strip ; each <details> opens upward as a popover so the
|
||||
graph stays visible while you interact with a card. */
|
||||
.card {
|
||||
background: rgba(201, 168, 76, .04);
|
||||
border: 1px solid rgba(201, 168, 76, .25);
|
||||
border-radius: 4px;
|
||||
padding: 10px 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.cards-row {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 6px;
|
||||
padding: 6px 8px env(safe-area-inset-bottom);
|
||||
background: rgba(10, 10, 15, .85);
|
||||
border-top: 1px solid var(--gold-hermetic);
|
||||
}
|
||||
.cards-row .card {
|
||||
margin: 0;
|
||||
padding: 6px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.card summary {
|
||||
cursor: pointer;
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 15px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--gold-hermetic);
|
||||
list-style: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
/* Open-card popover : opens upward so cards stay readable above the
|
||||
bottom strip. */
|
||||
.cards-row .card[open] {
|
||||
position: absolute;
|
||||
left: 8px; right: 8px;
|
||||
bottom: calc(50px + env(safe-area-inset-bottom));
|
||||
background: rgba(10, 10, 15, .96);
|
||||
border-color: var(--gold-hermetic);
|
||||
padding: 12px 16px;
|
||||
z-index: 8;
|
||||
}
|
||||
.card summary::-webkit-details-marker { display: none; }
|
||||
.card summary::after { content: ' ▾'; color: var(--text-muted); font-size: 12px; }
|
||||
|
|
|
|||
|
|
@ -22,7 +22,10 @@
|
|||
|
||||
const body = document.body;
|
||||
const token = body.dataset.token;
|
||||
const i18n = JSON.parse(body.dataset.i18n || '{}');
|
||||
// i18n is injected via <script>window.__SOCIAL_I18N__ = { … }</script>
|
||||
// in the template head — keeps FR apostrophes intact (was a JSON.parse
|
||||
// crash when inlined as a data-* attribute).
|
||||
const i18n = window.__SOCIAL_I18N__ || {};
|
||||
|
||||
// ─── i18n helper ───
|
||||
function t(key, vars = {}) {
|
||||
|
|
@ -36,8 +39,6 @@
|
|||
// ─── DOM refs ───
|
||||
const svgEl = document.getElementById('social-graph');
|
||||
const svg = d3.select(svgEl);
|
||||
const loadingEl = document.getElementById('social-loading');
|
||||
const emptyEl = document.getElementById('social-empty');
|
||||
const ndEl = document.getElementById('node-detail');
|
||||
const wipeModal = document.getElementById('wipe-modal');
|
||||
|
||||
|
|
@ -49,7 +50,14 @@
|
|||
|
||||
// ─── graph state ───
|
||||
let simulation = null;
|
||||
const W = 600, H = 600;
|
||||
|
||||
function svgSize() {
|
||||
// Measure actual rendered size so the force center scales with the
|
||||
// viewport. Falls back to a sane default if the layout hasn't
|
||||
// settled yet.
|
||||
const r = svgEl.getBoundingClientRect();
|
||||
return { W: Math.max(r.width, 320), H: Math.max(r.height, 320) };
|
||||
}
|
||||
|
||||
function clearGraph() {
|
||||
svg.selectAll('*').remove();
|
||||
|
|
@ -58,16 +66,15 @@
|
|||
|
||||
function render(graph) {
|
||||
clearGraph();
|
||||
const { W, H } = svgSize();
|
||||
svg.attr('viewBox', `0 0 ${W} ${H}`);
|
||||
|
||||
bind('total_trackers', graph.stats.total_trackers || 0);
|
||||
bind('total_sites', graph.stats.total_sites || 0);
|
||||
|
||||
loadingEl.hidden = true;
|
||||
if (!graph.nodes.length) {
|
||||
emptyEl.hidden = false;
|
||||
return;
|
||||
}
|
||||
emptyEl.hidden = true;
|
||||
// Empty graph → just return ; the stats tiles already show 0/0 and
|
||||
// the user knows. No persistent overlay message.
|
||||
if (!graph.nodes.length) return;
|
||||
|
||||
// Build d3 dataset: sites are union of all node.sites + tracker nodes themselves.
|
||||
const siteSet = new Set();
|
||||
|
|
@ -124,13 +131,18 @@
|
|||
.force('center', d3.forceCenter(W / 2, H / 2))
|
||||
.force('collide', d3.forceCollide().radius(22));
|
||||
|
||||
const linkSel = svg.append('g').attr('class', 'links')
|
||||
// Phase 11.B v3 — content group that owns links + nodes ; the
|
||||
// d3.zoom behavior applies its transform here so pan/pinch don't
|
||||
// move the SVG itself (or its viewBox).
|
||||
const content = svg.append('g').attr('class', 'content');
|
||||
|
||||
const linkSel = content.append('g').attr('class', 'links')
|
||||
.selectAll('line').data([...links, ...accentLinks]).join('line')
|
||||
.attr('class', d => d.accent ? 'edge accent' : 'edge')
|
||||
.attr('stroke-width', d => Math.max(1, Math.log(1 + (d.reuse || 0)) * 1.8))
|
||||
.attr('stroke-dasharray', d => d.accent ? '4,3' : null);
|
||||
|
||||
const nodeG = svg.append('g').attr('class', 'nodes')
|
||||
const nodeG = content.append('g').attr('class', 'nodes')
|
||||
.selectAll('g').data(nodes).join('g')
|
||||
.attr('class', d => 'node node-' + d.kind)
|
||||
.call(d3.drag()
|
||||
|
|
@ -147,11 +159,60 @@
|
|||
.attr('x', 12).attr('y', 4)
|
||||
.text(d => d.label.length > 22 ? d.label.slice(0, 21) + '…' : d.label);
|
||||
|
||||
// ─── pan + pinch-zoom on the SVG (transform applies to content) ──
|
||||
// Drag on a node calls d3.drag, drag on empty SVG calls d3.zoom's
|
||||
// pan ; pinch and wheel always zoom. Touch-action: none on the
|
||||
// svg (css) keeps the browser from intercepting these gestures.
|
||||
const zoom = d3.zoom()
|
||||
.scaleExtent([0.2, 6])
|
||||
.filter((ev) => {
|
||||
// Allow pan when the gesture didn't start on a node element.
|
||||
// Allow all wheel + touch (multi-finger pinch).
|
||||
if (ev.type === 'wheel' || (ev.touches && ev.touches.length > 1)) return true;
|
||||
return !ev.target.closest('.node');
|
||||
})
|
||||
.on('zoom', (ev) => content.attr('transform', ev.transform));
|
||||
svg.call(zoom).on('dblclick.zoom', () => autoFit(800));
|
||||
|
||||
// Auto-fit once the simulation cools so all nodes are visible.
|
||||
let fitDone = false;
|
||||
function autoFit(duration = 600) {
|
||||
if (!nodes.length) return;
|
||||
const bbox = content.node().getBBox();
|
||||
if (!bbox.width || !bbox.height) return;
|
||||
const pad = 28;
|
||||
const scale = Math.min(
|
||||
(W - pad * 2) / bbox.width,
|
||||
(H - pad * 2) / bbox.height,
|
||||
2.5,
|
||||
);
|
||||
const tx = (W - bbox.width * scale) / 2 - bbox.x * scale;
|
||||
const ty = (H - bbox.height * scale) / 2 - bbox.y * scale;
|
||||
svg.transition().duration(duration).call(
|
||||
zoom.transform,
|
||||
d3.zoomIdentity.translate(tx, ty).scale(scale),
|
||||
);
|
||||
}
|
||||
|
||||
simulation.on('tick', () => {
|
||||
linkSel
|
||||
.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
|
||||
.attr('x2', d => d.target.x).attr('y2', d => d.target.y);
|
||||
nodeG.attr('transform', d => `translate(${d.x},${d.y})`);
|
||||
if (!fitDone && simulation.alpha() < 0.1) {
|
||||
fitDone = true;
|
||||
autoFit();
|
||||
}
|
||||
});
|
||||
// Re-fit on viewport resize.
|
||||
let resizeTimer;
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(() => {
|
||||
const sz = svgSize();
|
||||
svg.attr('viewBox', `0 0 ${sz.W} ${sz.H}`);
|
||||
autoFit(400);
|
||||
}, 150);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -201,9 +262,6 @@
|
|||
const j = await r.json();
|
||||
wipeModal.close();
|
||||
alert(t('wipe_success', { n: j.rows_deleted || 0 }));
|
||||
// Refresh
|
||||
loadingEl.hidden = false;
|
||||
svgEl.style.display = '';
|
||||
fetchGraph();
|
||||
} catch (e) {
|
||||
console.error('[social] wipe failed', e);
|
||||
|
|
@ -232,7 +290,6 @@
|
|||
render(g);
|
||||
} catch (e) {
|
||||
console.error('[social] fetch failed', e);
|
||||
loadingEl.textContent = t('error');
|
||||
}
|
||||
}
|
||||
fetchGraph();
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user