mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-06-30 14:10:44 +00:00
Compare commits
2 Commits
dc6505a2f2
...
92b203cbbc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92b203cbbc | ||
| 7bd265fe1a |
|
|
@ -1,3 +1,21 @@
|
||||||
|
secubox-toolbox (2.6.18-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Cartographie donut-bubble view + geography rollups (#553).
|
||||||
|
- social.py fetch_graph: nodes gain country_iso / country_flag /
|
||||||
|
continent / tier ; new by_country, by_continent (flags + per-tier
|
||||||
|
breakdown) and by_tier aggregations (additive, read-time). stats
|
||||||
|
gains total_countries / total_continents. IP/ASN intentionally
|
||||||
|
NOT used as a graph dimension. Helpers _flag_emoji / _continent_of
|
||||||
|
/ _tracker_tier (no GeoIP/publicsuffix dep).
|
||||||
|
- social.js: new "🍩 Donuts" view (default, ⇄ "👁️ Œil" toggle) —
|
||||||
|
a d3.pack of continent backdrop bubbles, each country a donut sized
|
||||||
|
by tracking impact (hits), ring split by severity tier, flag in the
|
||||||
|
hole ; tier legend ; click → country summary in the detail panel.
|
||||||
|
Eye force-graph kept as one-click fallback ; node detail now fills
|
||||||
|
the country flag.
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Sat, 13 Jun 2026 14:30:00 +0200
|
||||||
|
|
||||||
secubox-toolbox (2.6.17-1~bookworm1) bookworm; urgency=medium
|
secubox-toolbox (2.6.17-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
* Social correlation: domain-rollup + history + target↔tracker (#549).
|
* Social correlation: domain-rollup + history + target↔tracker (#549).
|
||||||
|
|
|
||||||
|
|
@ -708,6 +708,58 @@ def _registrable_domain(host: str) -> str:
|
||||||
return last_two
|
return last_two
|
||||||
|
|
||||||
|
|
||||||
|
# ── geography helpers (#553) : flag + continent rollup, no IP/ASN ──
|
||||||
|
def _flag_emoji(iso2: Optional[str]) -> str:
|
||||||
|
"""ISO-3166 alpha-2 → flag emoji (regional indicator pair). '' if unknown."""
|
||||||
|
if not iso2 or len(iso2) != 2 or not iso2.isalpha():
|
||||||
|
return ""
|
||||||
|
base = 0x1F1E6
|
||||||
|
return "".join(chr(base + (ord(ch) - ord("A"))) for ch in iso2.upper())
|
||||||
|
|
||||||
|
|
||||||
|
# Compact ISO-3166 alpha-2 → continent map (covers the countries a French
|
||||||
|
# civic kiosk realistically surfaces ; unknowns fall back to "Autre").
|
||||||
|
_CONTINENT = {
|
||||||
|
# Europe
|
||||||
|
"FR": "Europe", "DE": "Europe", "GB": "Europe", "UK": "Europe", "IE": "Europe",
|
||||||
|
"NL": "Europe", "BE": "Europe", "LU": "Europe", "ES": "Europe", "PT": "Europe",
|
||||||
|
"IT": "Europe", "CH": "Europe", "AT": "Europe", "SE": "Europe", "NO": "Europe",
|
||||||
|
"FI": "Europe", "DK": "Europe", "PL": "Europe", "CZ": "Europe", "RO": "Europe",
|
||||||
|
"BG": "Europe", "GR": "Europe", "HU": "Europe", "SK": "Europe", "HR": "Europe",
|
||||||
|
"RS": "Europe", "UA": "Europe", "RU": "Europe", "IS": "Europe", "EE": "Europe",
|
||||||
|
"LV": "Europe", "LT": "Europe", "SI": "Europe",
|
||||||
|
# Americas
|
||||||
|
"US": "Amériques", "CA": "Amériques", "MX": "Amériques", "BR": "Amériques",
|
||||||
|
"AR": "Amériques", "CL": "Amériques", "CO": "Amériques", "PE": "Amériques",
|
||||||
|
# Asia
|
||||||
|
"CN": "Asie", "JP": "Asie", "KR": "Asie", "IN": "Asie", "SG": "Asie",
|
||||||
|
"HK": "Asie", "TW": "Asie", "TH": "Asie", "ID": "Asie", "VN": "Asie",
|
||||||
|
"IL": "Asie", "TR": "Asie", "AE": "Asie", "SA": "Asie",
|
||||||
|
# Africa
|
||||||
|
"ZA": "Afrique", "EG": "Afrique", "MA": "Afrique", "DZ": "Afrique",
|
||||||
|
"TN": "Afrique", "NG": "Afrique", "KE": "Afrique", "SN": "Afrique",
|
||||||
|
# Oceania
|
||||||
|
"AU": "Océanie", "NZ": "Océanie",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _continent_of(iso2: Optional[str]) -> str:
|
||||||
|
if not iso2:
|
||||||
|
return "Inconnu"
|
||||||
|
return _CONTINENT.get(iso2.upper(), "Autre")
|
||||||
|
|
||||||
|
|
||||||
|
def _tracker_tier(opgrade, antibot, cdn) -> str:
|
||||||
|
"""Severity tier used for the donut breakdown (highest wins)."""
|
||||||
|
if opgrade:
|
||||||
|
return "opgrade"
|
||||||
|
if antibot:
|
||||||
|
return "antibot"
|
||||||
|
if cdn:
|
||||||
|
return "cdn"
|
||||||
|
return "other"
|
||||||
|
|
||||||
|
|
||||||
def fetch_graph(mac_hash: str, since_seconds: int = 86400) -> Dict:
|
def fetch_graph(mac_hash: str, since_seconds: int = 86400) -> Dict:
|
||||||
"""Return the per-client graph JSON contract.
|
"""Return the per-client graph JSON contract.
|
||||||
|
|
||||||
|
|
@ -727,8 +779,8 @@ def fetch_graph(mac_hash: str, since_seconds: int = 86400) -> Dict:
|
||||||
# can colour/label by edge-network vendor.
|
# can colour/label by edge-network vendor.
|
||||||
for r in c.execute(
|
for r in c.execute(
|
||||||
"SELECT n.tracker_domain, n.hits, n.first_seen, n.last_seen, "
|
"SELECT n.tracker_domain, n.hits, n.first_seen, n.last_seen, "
|
||||||
"n.sites_jsonl, m.cdn_vendor, m.cache_status, m.antibot_vendor, "
|
"n.sites_jsonl, n.country_iso, m.cdn_vendor, m.cache_status, "
|
||||||
"m.opgrade_vendor, m.opgrade_category "
|
"m.antibot_vendor, m.opgrade_vendor, m.opgrade_category "
|
||||||
"FROM social_nodes n "
|
"FROM social_nodes n "
|
||||||
"LEFT JOIN social_host_meta m ON m.tracker_domain = n.tracker_domain "
|
"LEFT JOIN social_host_meta m ON m.tracker_domain = n.tracker_domain "
|
||||||
"WHERE n.client_mac_hash = ? AND n.last_seen >= ? "
|
"WHERE n.client_mac_hash = ? AND n.last_seen >= ? "
|
||||||
|
|
@ -739,6 +791,7 @@ def fetch_graph(mac_hash: str, since_seconds: int = 86400) -> Dict:
|
||||||
sites = json.loads(r["sites_jsonl"])
|
sites = json.loads(r["sites_jsonl"])
|
||||||
except Exception:
|
except Exception:
|
||||||
sites = []
|
sites = []
|
||||||
|
cc = (r["country_iso"] or "").upper() or None
|
||||||
out["nodes"].append(
|
out["nodes"].append(
|
||||||
{
|
{
|
||||||
"id": r["tracker_domain"],
|
"id": r["tracker_domain"],
|
||||||
|
|
@ -748,6 +801,11 @@ def fetch_graph(mac_hash: str, since_seconds: int = 86400) -> Dict:
|
||||||
"sites": sites,
|
"sites": sites,
|
||||||
"first_seen": r["first_seen"],
|
"first_seen": r["first_seen"],
|
||||||
"last_seen": r["last_seen"],
|
"last_seen": r["last_seen"],
|
||||||
|
"country_iso": cc,
|
||||||
|
"country_flag": _flag_emoji(cc),
|
||||||
|
"continent": _continent_of(cc),
|
||||||
|
"tier": _tracker_tier(r["opgrade_vendor"], r["antibot_vendor"],
|
||||||
|
r["cdn_vendor"]),
|
||||||
"cdn_vendor": r["cdn_vendor"],
|
"cdn_vendor": r["cdn_vendor"],
|
||||||
"cache_status": r["cache_status"],
|
"cache_status": r["cache_status"],
|
||||||
"antibot_vendor": r["antibot_vendor"],
|
"antibot_vendor": r["antibot_vendor"],
|
||||||
|
|
@ -871,10 +929,52 @@ def fetch_graph(mac_hash: str, since_seconds: int = 86400) -> Dict:
|
||||||
})
|
})
|
||||||
out["history"] = history
|
out["history"] = history
|
||||||
|
|
||||||
|
# (d) #553 geography + tier rollups for the donut-bubble view.
|
||||||
|
_TIERS = ("opgrade", "antibot", "cdn", "other")
|
||||||
|
tier_tot = {t: 0 for t in _TIERS}
|
||||||
|
_ctry: Dict[str, dict] = {}
|
||||||
|
_cont: Dict[str, dict] = {}
|
||||||
|
for n in out["nodes"]:
|
||||||
|
tier = n["tier"]; hits = n["hits"] or 0
|
||||||
|
tier_tot[tier] += hits
|
||||||
|
cc = n["country_iso"] or "??"
|
||||||
|
ct = _ctry.setdefault(cc, {
|
||||||
|
"country_iso": None if cc == "??" else cc,
|
||||||
|
"flag": n["country_flag"], "continent": n["continent"],
|
||||||
|
"tracker_count": 0, "hits": 0,
|
||||||
|
"tiers": {t: 0 for t in _TIERS}, "_sites": set(),
|
||||||
|
})
|
||||||
|
ct["tracker_count"] += 1; ct["hits"] += hits
|
||||||
|
ct["tiers"][tier] += hits; ct["_sites"].update(n["sites"])
|
||||||
|
co = _cont.setdefault(n["continent"], {
|
||||||
|
"continent": n["continent"], "country_count_set": set(),
|
||||||
|
"tracker_count": 0, "hits": 0, "tiers": {t: 0 for t in _TIERS},
|
||||||
|
})
|
||||||
|
co["country_count_set"].add(cc)
|
||||||
|
co["tracker_count"] += 1; co["hits"] += hits
|
||||||
|
co["tiers"][tier] += hits
|
||||||
|
by_country = sorted(
|
||||||
|
({"country_iso": v["country_iso"], "flag": v["flag"],
|
||||||
|
"continent": v["continent"], "tracker_count": v["tracker_count"],
|
||||||
|
"hits": v["hits"], "sites_count": len(v["_sites"]),
|
||||||
|
"tiers": v["tiers"]} for v in _ctry.values()),
|
||||||
|
key=lambda x: -x["hits"])
|
||||||
|
by_continent = sorted(
|
||||||
|
({"continent": v["continent"],
|
||||||
|
"country_count": len(v["country_count_set"]),
|
||||||
|
"tracker_count": v["tracker_count"], "hits": v["hits"],
|
||||||
|
"tiers": v["tiers"]} for v in _cont.values()),
|
||||||
|
key=lambda x: -x["hits"])
|
||||||
|
out["by_country"] = by_country
|
||||||
|
out["by_continent"] = by_continent
|
||||||
|
out["by_tier"] = tier_tot
|
||||||
|
|
||||||
out["stats"] = {
|
out["stats"] = {
|
||||||
"total_trackers": (stats_row["total_trackers"] or 0) if stats_row else 0,
|
"total_trackers": (stats_row["total_trackers"] or 0) if stats_row else 0,
|
||||||
"total_sites": sites_count,
|
"total_sites": sites_count,
|
||||||
"total_domains": len(by_domain),
|
"total_domains": len(by_domain),
|
||||||
|
"total_countries": len([c for c in by_country if c["country_iso"]]),
|
||||||
|
"total_continents": len([c for c in by_continent if c["continent"] not in ("Inconnu",)]),
|
||||||
"first_seen": stats_row["first_seen"] if stats_row else None,
|
"first_seen": stats_row["first_seen"] if stats_row else None,
|
||||||
"last_seen": stats_row["last_seen"] if stats_row else None,
|
"last_seen": stats_row["last_seen"] if stats_row else None,
|
||||||
"antibot_sites": len({a["src_site"] for a in antibot}),
|
"antibot_sites": len({a["src_site"] for a in antibot}),
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,54 @@
|
||||||
|
|
||||||
// ─── graph state ───
|
// ─── graph state ───
|
||||||
let simulation = null;
|
let simulation = null;
|
||||||
|
let lastGraph = null;
|
||||||
|
let view = 'donuts'; // #553 : 'donuts' (default) | 'eye'
|
||||||
|
|
||||||
|
// Severity-tier palette shared by the donut view + legend.
|
||||||
|
const TIER = {
|
||||||
|
opgrade: { c: 'var(--void-purple)', label: '📡 opérateur' },
|
||||||
|
antibot: { c: 'var(--cinnabar)', label: '🤖 anti-bot' },
|
||||||
|
cdn: { c: 'var(--cyber-cyan)', label: '☁ CDN' },
|
||||||
|
other: { c: 'var(--gold-hermetic)', label: '• autre' },
|
||||||
|
};
|
||||||
|
const TIER_ORDER = ['opgrade', 'antibot', 'cdn', 'other'];
|
||||||
|
|
||||||
|
function draw(graph) {
|
||||||
|
if (!graph) return;
|
||||||
|
if (view === 'donuts') renderDonuts(graph);
|
||||||
|
else render(graph);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject the view toggle (🍩 donuts ⇄ 👁️ œil) once, above the svg.
|
||||||
|
function ensureToggle() {
|
||||||
|
if (document.getElementById('view-toggle') || !svgEl) return;
|
||||||
|
const bar = document.createElement('div');
|
||||||
|
bar.id = 'view-toggle';
|
||||||
|
bar.style.cssText = 'display:flex;gap:.4rem;justify-content:center;margin:.4rem 0';
|
||||||
|
const mk = (id, txt) => {
|
||||||
|
const b = document.createElement('button');
|
||||||
|
b.type = 'button'; b.dataset.view = id; b.textContent = txt;
|
||||||
|
b.style.cssText = 'cursor:pointer;border:1px solid var(--void-purple,#6e40c9);'
|
||||||
|
+ 'background:transparent;color:var(--text-primary,#e8e6d9);border-radius:6px;'
|
||||||
|
+ 'padding:.35rem .8rem;font:inherit';
|
||||||
|
b.addEventListener('click', () => {
|
||||||
|
view = id; syncToggle();
|
||||||
|
draw(lastGraph);
|
||||||
|
});
|
||||||
|
return b;
|
||||||
|
};
|
||||||
|
bar.appendChild(mk('donuts', '🍩 Donuts'));
|
||||||
|
bar.appendChild(mk('eye', '👁️ Œil'));
|
||||||
|
svgEl.parentNode.insertBefore(bar, svgEl);
|
||||||
|
syncToggle();
|
||||||
|
}
|
||||||
|
function syncToggle() {
|
||||||
|
document.querySelectorAll('#view-toggle button').forEach((b) => {
|
||||||
|
const on = b.dataset.view === view;
|
||||||
|
b.style.background = on ? 'var(--void-purple,#6e40c9)' : 'transparent';
|
||||||
|
b.style.color = on ? '#0a0a0f' : 'var(--text-primary,#e8e6d9)';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function svgSize() {
|
function svgSize() {
|
||||||
// Measure actual rendered size so the force center scales with the
|
// Measure actual rendered size so the force center scales with the
|
||||||
|
|
@ -130,6 +178,8 @@
|
||||||
sites: n.sites,
|
sites: n.sites,
|
||||||
first_seen: n.first_seen,
|
first_seen: n.first_seen,
|
||||||
last_seen: n.last_seen,
|
last_seen: n.last_seen,
|
||||||
|
country_iso: n.country_iso || null,
|
||||||
|
country_flag: n.country_flag || '',
|
||||||
cdn_vendor: n.cdn_vendor || null,
|
cdn_vendor: n.cdn_vendor || null,
|
||||||
cache_status: n.cache_status || null,
|
cache_status: n.cache_status || null,
|
||||||
antibot_vendor: n.antibot_vendor || null,
|
antibot_vendor: n.antibot_vendor || null,
|
||||||
|
|
@ -339,11 +389,118 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── #553 donut-bubble view ───
|
||||||
|
// Continents are faint backdrop bubbles ; each country is a donut sized
|
||||||
|
// by tracking impact (hits), its ring split by severity tier, with the
|
||||||
|
// flag in the centre. No IP/ASN — geography + impact + tier only.
|
||||||
|
function renderDonuts(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);
|
||||||
|
updateAntibotTile(graph.stats.antibot_sites || 0, graph.stats.antibot_vendors || []);
|
||||||
|
updateOpgradeTile(graph.stats.opgrade_sites || 0, graph.stats.opgrade_vendors || []);
|
||||||
|
|
||||||
|
const countries = graph.by_country || [];
|
||||||
|
if (!countries.length) return;
|
||||||
|
|
||||||
|
// hierarchy : root → continent → country(leaf, value=hits)
|
||||||
|
const byCont = new Map();
|
||||||
|
for (const c of countries) {
|
||||||
|
const k = c.continent || 'Autre';
|
||||||
|
if (!byCont.has(k)) byCont.set(k, []);
|
||||||
|
byCont.get(k).push(c);
|
||||||
|
}
|
||||||
|
const root = {
|
||||||
|
name: 'root',
|
||||||
|
children: [...byCont.entries()].map(([cont, list]) => ({
|
||||||
|
name: cont, continent: true,
|
||||||
|
children: list.map(c => ({
|
||||||
|
leaf: true, name: c.country_iso || '??', flag: c.flag || '🏴',
|
||||||
|
value: Math.max(c.hits || 0, 1), tiers: c.tiers || {},
|
||||||
|
tracker_count: c.tracker_count || 0,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
const h = d3.hierarchy(root).sum(d => d.value || 0)
|
||||||
|
.sort((a, b) => (b.value || 0) - (a.value || 0));
|
||||||
|
d3.pack().size([W, H]).padding(d => d.depth === 1 ? 14 : 4)(h);
|
||||||
|
|
||||||
|
const content = svg.append('g').attr('class', 'content');
|
||||||
|
|
||||||
|
// continent backdrop bubbles + labels
|
||||||
|
content.append('g').selectAll('circle.cont')
|
||||||
|
.data(h.descendants().filter(d => d.depth === 1)).join('circle')
|
||||||
|
.attr('class', 'cont')
|
||||||
|
.attr('cx', d => d.x).attr('cy', d => d.y).attr('r', d => d.r)
|
||||||
|
.attr('fill', 'rgba(110,64,201,0.06)')
|
||||||
|
.attr('stroke', 'rgba(110,64,201,0.45)').attr('stroke-dasharray', '3,3');
|
||||||
|
content.append('g').selectAll('text.cont')
|
||||||
|
.data(h.descendants().filter(d => d.depth === 1)).join('text')
|
||||||
|
.attr('class', 'cont').attr('x', d => d.x).attr('y', d => d.y - d.r + 14)
|
||||||
|
.attr('text-anchor', 'middle').attr('fill', 'var(--void-purple,#9e76ff)')
|
||||||
|
.attr('font-size', 12).attr('font-weight', 'bold')
|
||||||
|
.text(d => '🌍 ' + d.data.name);
|
||||||
|
|
||||||
|
// country donut bubbles
|
||||||
|
const pie = d3.pie().sort(null).value(d => d[1]);
|
||||||
|
const leaves = content.append('g').selectAll('g.country')
|
||||||
|
.data(h.leaves()).join('g')
|
||||||
|
.attr('class', 'country node')
|
||||||
|
.attr('transform', d => `translate(${d.x},${d.y})`)
|
||||||
|
.style('cursor', 'pointer')
|
||||||
|
.on('click', (ev, d) => focusCountry(d.data));
|
||||||
|
leaves.each(function (d) {
|
||||||
|
const g = d3.select(this);
|
||||||
|
const r = d.r;
|
||||||
|
const inner = Math.max(r * 0.5, r - 7);
|
||||||
|
const arc = d3.arc().innerRadius(inner).outerRadius(r);
|
||||||
|
const data = TIER_ORDER.map(k => [k, (d.data.tiers && d.data.tiers[k]) || 0])
|
||||||
|
.filter(e => e[1] > 0);
|
||||||
|
const slices = data.length ? pie(data) : pie([['other', 1]]);
|
||||||
|
g.selectAll('path').data(slices).join('path')
|
||||||
|
.attr('d', arc)
|
||||||
|
.attr('fill', s => (TIER[s.data[0]] || TIER.other).c)
|
||||||
|
.attr('stroke', '#0a0a0f').attr('stroke-width', 0.6);
|
||||||
|
// flag (or ISO) in the hole
|
||||||
|
g.append('text').attr('text-anchor', 'middle').attr('dy', '.35em')
|
||||||
|
.attr('font-size', Math.max(9, Math.min(inner * 1.1, 26)))
|
||||||
|
.text(d.data.flag || d.data.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
// tier legend (bottom-left)
|
||||||
|
const lg = content.append('g').attr('transform', `translate(12,${H - 12 - TIER_ORDER.length * 16})`);
|
||||||
|
TIER_ORDER.forEach((k, i) => {
|
||||||
|
const row = lg.append('g').attr('transform', `translate(0,${i * 16})`);
|
||||||
|
row.append('rect').attr('width', 10).attr('height', 10).attr('rx', 2)
|
||||||
|
.attr('fill', TIER[k].c);
|
||||||
|
row.append('text').attr('x', 15).attr('y', 9).attr('font-size', 10)
|
||||||
|
.attr('fill', 'var(--text-muted,#6b6b7a)').text(TIER[k].label);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Donut country click → reuse the detail panel with a country summary.
|
||||||
|
function focusCountry(c) {
|
||||||
|
if (!ndEl) return;
|
||||||
|
bind('nd_domain', (c.flag || '') + ' ' + (c.name || '?'));
|
||||||
|
bind('nd_country', (c.flag || '') + ' ' + (c.name || '—'));
|
||||||
|
bind('nd_asn', '—');
|
||||||
|
const tier = TIER_ORDER.filter(k => (c.tiers || {})[k])
|
||||||
|
.map(k => `${TIER[k].label}:${c.tiers[k]}`).join(' · ') || '—';
|
||||||
|
bind('nd_cdn', tier);
|
||||||
|
bind('nd_antibot', (c.tiers && c.tiers.antibot) ? '🤖 ' + c.tiers.antibot : '—');
|
||||||
|
bind('nd_opgrade', (c.tiers && c.tiers.opgrade) ? '📡 ' + c.tiers.opgrade : '—');
|
||||||
|
bind('nd_sites', (c.tracker_count || 0) + ' traceurs');
|
||||||
|
bind('nd_first_seen', '—'); bind('nd_last_seen', '—');
|
||||||
|
ndEl.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── focus / detail panel ───
|
// ─── focus / detail panel ───
|
||||||
function focusNode(node, linkSel) {
|
function focusNode(node, linkSel) {
|
||||||
if (node.kind !== 'tracker') { ndEl.hidden = true; return; }
|
if (node.kind !== 'tracker') { ndEl.hidden = true; return; }
|
||||||
bind('nd_domain', node.label);
|
bind('nd_domain', node.label);
|
||||||
bind('nd_country', '—'); // Phase C dependency (GeoIP)
|
bind('nd_country', node.country_flag ? (node.country_flag + ' ' + (node.country_iso || '')) : '—');
|
||||||
bind('nd_asn', '—');
|
bind('nd_asn', '—');
|
||||||
bind('nd_cdn', node.cdn_vendor ? (node.cdn_vendor + (node.cache_status ? ' · ' + node.cache_status : '')) : '—');
|
bind('nd_cdn', node.cdn_vendor ? (node.cdn_vendor + (node.cache_status ? ' · ' + node.cache_status : '')) : '—');
|
||||||
bind('nd_antibot', node.antibot_vendor ? ('🤖 ' + node.antibot_vendor) : '—');
|
bind('nd_antibot', node.antibot_vendor ? ('🤖 ' + node.antibot_vendor) : '—');
|
||||||
|
|
@ -413,7 +570,9 @@
|
||||||
const r = await fetch(`/social/graph/${encodeURIComponent(token)}?since=86400`);
|
const r = await fetch(`/social/graph/${encodeURIComponent(token)}?since=86400`);
|
||||||
if (!r.ok) throw new Error('http ' + r.status);
|
if (!r.ok) throw new Error('http ' + r.status);
|
||||||
const g = await r.json();
|
const g = await r.json();
|
||||||
render(g);
|
lastGraph = g;
|
||||||
|
ensureToggle();
|
||||||
|
draw(g);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[social] fetch failed', e);
|
console.error('[social] fetch failed', e);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user