merge: reconcile prod-snapshot-2026-07-16 into main

Merges the production-only changes (Izzy PDF export via puppeteer,
Adoption Tracker overlay, audit scripts, deploy/utility scripts) that
had been living as uncommitted edits directly on Erik since before the
2026-07-14 backend refactor, on top of current main.

4 real conflicts, resolved by keeping whichever side is actually
functional rather than picking a side mechanically:

- package.json / package-lock.json: trivial union, kept both the
  refactor's pg/playwright deps and the snapshot's puppeteer dep;
  lockfile regenerated via `npm install --package-lock-only` rather
  than hand-edited
- server.js top-of-file: additive, kept both the localDb require
  (backend refactor) and the puppeteer lazy-loader (PDF export)
- server.js /api/health aspa_adoption block: main referenced
  `aspaAdoptionHistory`, a variable that is never declared anywhere
  in this file (confirmed by grep) -- a pre-existing dead reference
  that would throw if this code path ever ran. Kept the snapshot's
  working `aspaAdoptionDailyHistory`/`roaStore.count` version, which
  matches the live /api/health output from production. Also fixed
  the identical bug in the neighboring .catch() fallback block a few
  lines down (same undefined variable, not part of the conflict
  itself -- pre-existing on main, same fix applied for consistency).
- server.js hijack-subscribe/webhooks section (186 lines): main's
  side was just a 2-line comment ("Migrated to
  src/features/hijack-subscribe/"). Kept the snapshot's actual
  working implementation instead, because server.js does not
  require() or import anything from src/features/ or dist/ anywhere
  -- confirmed by grep. The "migration" only ever happened on the
  src/features/ side; server.js's own HTTP routing was never wired
  up to call into it.

IMPORTANT CAVEAT, found while investigating the above and NOT fixed
here: server.js still has 13 other "// Migrated to src/features/..."
comments (bgp-communities, irr-audit, asset-expand, rpki-history,
aspath, looking-glass, ix-matrix, submarine-cables, global-infra,
hijack-alerts (Fastify), changelog, rib, prefix-changes) where the
same thing is true -- the route implementation was deleted from
server.js and never replaced with a working call into src/features/.
These weren't touched by this merge (git didn't flag them as
conflicts, because production's snapshot never touched those
sections either -- only main changed them, by deleting them). If
this server.js is ever deployed as the new production file as-is,
those 13 endpoints will silently 404 or fall through, exactly like
the hijack-subscribe one would have if I'd taken main's side. This
is a pre-existing gap in the 2026-07-14 refactor (PR #1), unrelated
to the Izzy/Adoption Tracker work, and needs a deliberate decision
(wire server.js to src/features/, or confirm those routes are meant
to live on a different process/port entirely) before this branch
should be treated as deployable.

`node --check server.js` passes. Not deployed to Erik as part of
this commit -- Erik's currently-live server.js/public/index.html
are untouched.
This commit is contained in:
Rene Fichtmueller 2026-07-16 08:54:48 +02:00
commit ceb49d0ab6
24 changed files with 48810 additions and 716 deletions

871
aspa-adoption-history.json Normal file
View File

@ -0,0 +1,871 @@
[
{
"date": "2026-04-29",
"ts": 1777506539332,
"aspa_objects": 1781,
"roa_count": 846608,
"atlas_asns_total": 4698,
"atlas_asns_with_aspa": 393,
"coverage_pct_atlas": 8.37,
"aspa_delta": 0,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-04-30",
"ts": 1777591207204,
"aspa_objects": 1789,
"roa_count": 845275,
"atlas_asns_total": 4706,
"atlas_asns_with_aspa": 391,
"coverage_pct_atlas": 8.31,
"aspa_delta": 4,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-01",
"ts": 1777676486871,
"aspa_objects": 1791,
"roa_count": 835376,
"atlas_asns_total": 4698,
"atlas_asns_with_aspa": 390,
"coverage_pct_atlas": 8.3,
"aspa_delta": 1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-02",
"ts": 1777764423571,
"aspa_objects": 1790,
"roa_count": 838422,
"atlas_asns_total": 4698,
"atlas_asns_with_aspa": 389,
"coverage_pct_atlas": 8.28,
"aspa_delta": -1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-03",
"ts": 1777852360413,
"aspa_objects": 1788,
"roa_count": 848778,
"atlas_asns_total": 4698,
"atlas_asns_with_aspa": 391,
"coverage_pct_atlas": 8.32,
"aspa_delta": 0,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-04",
"ts": 1777925641049,
"aspa_objects": 1794,
"roa_count": 849251,
"atlas_asns_total": 4701,
"atlas_asns_with_aspa": 393,
"coverage_pct_atlas": 8.36,
"aspa_delta": 1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-05",
"ts": 1778013577684,
"aspa_objects": 1803,
"roa_count": 849639,
"atlas_asns_total": 4706,
"atlas_asns_with_aspa": 390,
"coverage_pct_atlas": 8.29,
"aspa_delta": 1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-06",
"ts": 1778101514437,
"aspa_objects": 1811,
"roa_count": 851176,
"atlas_asns_total": 4709,
"atlas_asns_with_aspa": 392,
"coverage_pct_atlas": 8.32,
"aspa_delta": 3,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-07",
"ts": 1778189451143,
"aspa_objects": 1819,
"roa_count": 854308,
"atlas_asns_total": 4704,
"atlas_asns_with_aspa": 397,
"coverage_pct_atlas": 8.44,
"aspa_delta": 1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-08",
"ts": 1778277387928,
"aspa_objects": 1826,
"roa_count": 855093,
"atlas_asns_total": 4618,
"atlas_asns_with_aspa": 391,
"coverage_pct_atlas": 8.47,
"aspa_delta": 2,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-09",
"ts": 1778365324597,
"aspa_objects": 1829,
"roa_count": 855728,
"atlas_asns_total": 4617,
"atlas_asns_with_aspa": 386,
"coverage_pct_atlas": 8.36,
"aspa_delta": 2,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-10",
"ts": 1778453261304,
"aspa_objects": 1830,
"roa_count": 855858,
"atlas_asns_total": 4630,
"atlas_asns_with_aspa": 392,
"coverage_pct_atlas": 8.47,
"aspa_delta": 1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-11",
"ts": 1778532854561,
"aspa_objects": 1842,
"roa_count": 852307,
"atlas_asns_total": 4644,
"atlas_asns_with_aspa": 396,
"coverage_pct_atlas": 8.53,
"aspa_delta": 4,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-12",
"ts": 1778619510110,
"aspa_objects": 1851,
"roa_count": 860353,
"atlas_asns_total": 4638,
"atlas_asns_with_aspa": 400,
"coverage_pct_atlas": 8.62,
"aspa_delta": 0,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-13",
"ts": 1778713039453,
"aspa_objects": 1853,
"roa_count": 869026,
"atlas_asns_total": 4644,
"atlas_asns_with_aspa": 399,
"coverage_pct_atlas": 8.59,
"aspa_delta": 1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-14",
"ts": 1778801086379,
"aspa_objects": 1874,
"roa_count": 870195,
"atlas_asns_total": 4641,
"atlas_asns_with_aspa": 402,
"coverage_pct_atlas": 8.66,
"aspa_delta": 5,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-15",
"ts": 1778889133380,
"aspa_objects": 1881,
"roa_count": 871665,
"atlas_asns_total": 4656,
"atlas_asns_with_aspa": 406,
"coverage_pct_atlas": 8.72,
"aspa_delta": 1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-16",
"ts": 1778964126500,
"aspa_objects": 1885,
"roa_count": 871578,
"atlas_asns_total": 4652,
"atlas_asns_with_aspa": 407,
"coverage_pct_atlas": 8.75,
"aspa_delta": 0,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-17",
"ts": 1779051783629,
"aspa_objects": 1884,
"roa_count": 871644,
"atlas_asns_total": 4648,
"atlas_asns_with_aspa": 407,
"coverage_pct_atlas": 8.76,
"aspa_delta": -2,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-18",
"ts": 1779139440853,
"aspa_objects": 1897,
"roa_count": 874620,
"atlas_asns_total": 4659,
"atlas_asns_with_aspa": 408,
"coverage_pct_atlas": 8.76,
"aspa_delta": 6,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-19",
"ts": 1779227098051,
"aspa_objects": 1931,
"roa_count": 877276,
"atlas_asns_total": 4663,
"atlas_asns_with_aspa": 409,
"coverage_pct_atlas": 8.77,
"aspa_delta": 1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-20",
"ts": 1779314755375,
"aspa_objects": 1945,
"roa_count": 882413,
"atlas_asns_total": 0,
"atlas_asns_with_aspa": 0,
"coverage_pct_atlas": 0,
"aspa_delta": 0,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-21",
"ts": 1779402412305,
"aspa_objects": 1956,
"roa_count": 886581,
"atlas_asns_total": 4657,
"atlas_asns_with_aspa": 415,
"coverage_pct_atlas": 8.91,
"aspa_delta": 1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-22",
"ts": 1779490069486,
"aspa_objects": 1963,
"roa_count": 886989,
"atlas_asns_total": 4656,
"atlas_asns_with_aspa": 416,
"coverage_pct_atlas": 8.93,
"aspa_delta": -1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-23",
"ts": 1779577726657,
"aspa_objects": 1966,
"roa_count": 887123,
"atlas_asns_total": 4662,
"atlas_asns_with_aspa": 421,
"coverage_pct_atlas": 9.03,
"aspa_delta": -1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-24",
"ts": 1779665383794,
"aspa_objects": 1973,
"roa_count": 887505,
"atlas_asns_total": 4658,
"atlas_asns_with_aspa": 419,
"coverage_pct_atlas": 9,
"aspa_delta": 3,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-25",
"ts": 1779753040747,
"aspa_objects": 1984,
"roa_count": 889453,
"atlas_asns_total": 4657,
"atlas_asns_with_aspa": 427,
"coverage_pct_atlas": 9.17,
"aspa_delta": 6,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-26",
"ts": 1779826088383,
"aspa_objects": 1993,
"roa_count": 896728,
"atlas_asns_total": 4683,
"atlas_asns_with_aspa": 426,
"coverage_pct_atlas": 9.1,
"aspa_delta": 8,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-27",
"ts": 1779924276466,
"aspa_objects": 2001,
"roa_count": 904935,
"atlas_asns_total": 4673,
"atlas_asns_with_aspa": 426,
"coverage_pct_atlas": 9.12,
"aspa_delta": 1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-28",
"ts": 1780012471849,
"aspa_objects": 2014,
"roa_count": 910888,
"atlas_asns_total": 4628,
"atlas_asns_with_aspa": 421,
"coverage_pct_atlas": 9.1,
"aspa_delta": 4,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-29",
"ts": 1780085968110,
"aspa_objects": 2019,
"roa_count": 916441,
"atlas_asns_total": 4640,
"atlas_asns_with_aspa": 424,
"coverage_pct_atlas": 9.14,
"aspa_delta": 2,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-30",
"ts": 1780174163595,
"aspa_objects": 2026,
"roa_count": 918362,
"atlas_asns_total": 4655,
"atlas_asns_with_aspa": 426,
"coverage_pct_atlas": 9.15,
"aspa_delta": 1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-05-31",
"ts": 1780262359143,
"aspa_objects": 2031,
"roa_count": 918565,
"atlas_asns_total": 4653,
"atlas_asns_with_aspa": 427,
"coverage_pct_atlas": 9.18,
"aspa_delta": 0,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-01",
"ts": 1780350554630,
"aspa_objects": 2040,
"roa_count": 920847,
"atlas_asns_total": 4673,
"atlas_asns_with_aspa": 430,
"coverage_pct_atlas": 9.2,
"aspa_delta": 1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-02",
"ts": 1780438750094,
"aspa_objects": 2050,
"roa_count": 925338,
"atlas_asns_total": 4685,
"atlas_asns_with_aspa": 435,
"coverage_pct_atlas": 9.28,
"aspa_delta": 0,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-03",
"ts": 1780526945736,
"aspa_objects": 2054,
"roa_count": 926656,
"atlas_asns_total": 4692,
"atlas_asns_with_aspa": 440,
"coverage_pct_atlas": 9.38,
"aspa_delta": 2,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-04",
"ts": 1780615141853,
"aspa_objects": 2052,
"roa_count": 929579,
"atlas_asns_total": 4699,
"atlas_asns_with_aspa": 438,
"coverage_pct_atlas": 9.32,
"aspa_delta": 0,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-05",
"ts": 1780703337103,
"aspa_objects": 2057,
"roa_count": 933266,
"atlas_asns_total": 4533,
"atlas_asns_with_aspa": 423,
"coverage_pct_atlas": 9.33,
"aspa_delta": 2,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-06",
"ts": 1780776833375,
"aspa_objects": 2057,
"roa_count": 933707,
"atlas_asns_total": 4457,
"atlas_asns_with_aspa": 415,
"coverage_pct_atlas": 9.31,
"aspa_delta": 0,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-07",
"ts": 1780865028684,
"aspa_objects": 2060,
"roa_count": 934451,
"atlas_asns_total": 4624,
"atlas_asns_with_aspa": 432,
"coverage_pct_atlas": 9.34,
"aspa_delta": 0,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-08",
"ts": 1780953225583,
"aspa_objects": 2064,
"roa_count": 935363,
"atlas_asns_total": 4673,
"atlas_asns_with_aspa": 439,
"coverage_pct_atlas": 9.39,
"aspa_delta": 2,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-09",
"ts": 1781043490445,
"aspa_objects": 2082,
"roa_count": 938346,
"atlas_asns_total": 4693,
"atlas_asns_with_aspa": 446,
"coverage_pct_atlas": 9.5,
"aspa_delta": 4,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-10",
"ts": 1781121824287,
"aspa_objects": 2086,
"roa_count": 942559,
"atlas_asns_total": 4692,
"atlas_asns_with_aspa": 444,
"coverage_pct_atlas": 9.46,
"aspa_delta": 1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-11",
"ts": 1781218962694,
"aspa_objects": 2101,
"roa_count": 949614,
"atlas_asns_total": 4721,
"atlas_asns_with_aspa": 449,
"coverage_pct_atlas": 9.51,
"aspa_delta": 0,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-12",
"ts": 1781305669463,
"aspa_objects": 2104,
"roa_count": 950161,
"atlas_asns_total": 4716,
"atlas_asns_with_aspa": 447,
"coverage_pct_atlas": 9.48,
"aspa_delta": 2,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-13",
"ts": 1781392376261,
"aspa_objects": 2112,
"roa_count": 950030,
"atlas_asns_total": 4727,
"atlas_asns_with_aspa": 450,
"coverage_pct_atlas": 9.52,
"aspa_delta": 2,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-14",
"ts": 1781479082907,
"aspa_objects": 2115,
"roa_count": 950083,
"atlas_asns_total": 4731,
"atlas_asns_with_aspa": 451,
"coverage_pct_atlas": 9.53,
"aspa_delta": 0,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-15",
"ts": 1781565789140,
"aspa_objects": 2119,
"roa_count": 950996,
"atlas_asns_total": 4736,
"atlas_asns_with_aspa": 450,
"coverage_pct_atlas": 9.5,
"aspa_delta": 0,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-16",
"ts": 1781649652082,
"aspa_objects": 2130,
"roa_count": 953250,
"atlas_asns_total": 4733,
"atlas_asns_with_aspa": 450,
"coverage_pct_atlas": 9.51,
"aspa_delta": 3,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-17",
"ts": 1781734424437,
"aspa_objects": 2133,
"roa_count": 954510,
"atlas_asns_total": 4726,
"atlas_asns_with_aspa": 450,
"coverage_pct_atlas": 9.52,
"aspa_delta": -1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-18",
"ts": 1781821528896,
"aspa_objects": 2143,
"roa_count": 957195,
"atlas_asns_total": 4746,
"atlas_asns_with_aspa": 450,
"coverage_pct_atlas": 9.48,
"aspa_delta": 3,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-19",
"ts": 1781908099618,
"aspa_objects": 2146,
"roa_count": 957896,
"atlas_asns_total": 4748,
"atlas_asns_with_aspa": 452,
"coverage_pct_atlas": 9.52,
"aspa_delta": 2,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-20",
"ts": 1781994672218,
"aspa_objects": 2151,
"roa_count": 958031,
"atlas_asns_total": 4735,
"atlas_asns_with_aspa": 452,
"coverage_pct_atlas": 9.55,
"aspa_delta": 1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-21",
"ts": 1782081244003,
"aspa_objects": 2156,
"roa_count": 958067,
"atlas_asns_total": 4739,
"atlas_asns_with_aspa": 453,
"coverage_pct_atlas": 9.56,
"aspa_delta": 0,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-22",
"ts": 1782167816179,
"aspa_objects": 2163,
"roa_count": 958365,
"atlas_asns_total": 4746,
"atlas_asns_with_aspa": 456,
"coverage_pct_atlas": 9.61,
"aspa_delta": 3,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-23",
"ts": 1782253239274,
"aspa_objects": 2173,
"roa_count": 959022,
"atlas_asns_total": 4743,
"atlas_asns_with_aspa": 457,
"coverage_pct_atlas": 9.64,
"aspa_delta": -1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-24",
"ts": 1782338059434,
"aspa_objects": 2184,
"roa_count": 959716,
"atlas_asns_total": 4748,
"atlas_asns_with_aspa": 459,
"coverage_pct_atlas": 9.67,
"aspa_delta": 1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-25",
"ts": 1782422879426,
"aspa_objects": 2194,
"roa_count": 961420,
"atlas_asns_total": 4749,
"atlas_asns_with_aspa": 465,
"coverage_pct_atlas": 9.79,
"aspa_delta": 0,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-26",
"ts": 1782507700593,
"aspa_objects": 2202,
"roa_count": 961846,
"atlas_asns_total": 4748,
"atlas_asns_with_aspa": 462,
"coverage_pct_atlas": 9.73,
"aspa_delta": 1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-27",
"ts": 1782592519478,
"aspa_objects": 2208,
"roa_count": 962630,
"atlas_asns_total": 4746,
"atlas_asns_with_aspa": 459,
"coverage_pct_atlas": 9.67,
"aspa_delta": 0,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-28",
"ts": 1782677339653,
"aspa_objects": 2210,
"roa_count": 962824,
"atlas_asns_total": 4738,
"atlas_asns_with_aspa": 462,
"coverage_pct_atlas": 9.75,
"aspa_delta": 1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-29",
"ts": 1782776296119,
"aspa_objects": 2218,
"roa_count": 963499,
"atlas_asns_total": 4749,
"atlas_asns_with_aspa": 462,
"coverage_pct_atlas": 9.73,
"aspa_delta": 3,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-06-30",
"ts": 1782861116475,
"aspa_objects": 2227,
"roa_count": 964524,
"atlas_asns_total": 4747,
"atlas_asns_with_aspa": 463,
"coverage_pct_atlas": 9.75,
"aspa_delta": 1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-07-01",
"ts": 1782943528014,
"aspa_objects": 2234,
"roa_count": 965150,
"atlas_asns_total": 4747,
"atlas_asns_with_aspa": 470,
"coverage_pct_atlas": 9.9,
"aspa_delta": 3,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-07-02",
"ts": 1783029743491,
"aspa_objects": 2244,
"roa_count": 965634,
"atlas_asns_total": 4744,
"atlas_asns_with_aspa": 473,
"coverage_pct_atlas": 9.97,
"aspa_delta": 1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-07-03",
"ts": 1783110319303,
"aspa_objects": 2253,
"roa_count": 965995,
"atlas_asns_total": 4739,
"atlas_asns_with_aspa": 478,
"coverage_pct_atlas": 10.09,
"aspa_delta": 1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-07-04",
"ts": 1783195400105,
"aspa_objects": 2254,
"roa_count": 966049,
"atlas_asns_total": 4757,
"atlas_asns_with_aspa": 480,
"coverage_pct_atlas": 10.09,
"aspa_delta": 1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-07-05",
"ts": 1783282307341,
"aspa_objects": 2264,
"roa_count": 966292,
"atlas_asns_total": 4740,
"atlas_asns_with_aspa": 483,
"coverage_pct_atlas": 10.19,
"aspa_delta": 3,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-07-06",
"ts": 1783368549946,
"aspa_objects": 2275,
"roa_count": 966360,
"atlas_asns_total": 4736,
"atlas_asns_with_aspa": 482,
"coverage_pct_atlas": 10.18,
"aspa_delta": 0,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-07-07",
"ts": 1783464285059,
"aspa_objects": 2286,
"roa_count": 967521,
"atlas_asns_total": 4747,
"atlas_asns_with_aspa": 483,
"coverage_pct_atlas": 10.17,
"aspa_delta": 3,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-07-08",
"ts": 1783550943346,
"aspa_objects": 2309,
"roa_count": 967933,
"atlas_asns_total": 4750,
"atlas_asns_with_aspa": 486,
"coverage_pct_atlas": 10.23,
"aspa_delta": 1,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-07-09",
"ts": 1783637598811,
"aspa_objects": 2324,
"roa_count": 969050,
"atlas_asns_total": 4759,
"atlas_asns_with_aspa": 492,
"coverage_pct_atlas": 10.34,
"aspa_delta": 2,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-07-10",
"ts": 1783724254815,
"aspa_objects": 2347,
"roa_count": 970180,
"atlas_asns_total": 4766,
"atlas_asns_with_aspa": 495,
"coverage_pct_atlas": 10.39,
"aspa_delta": 5,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-07-11",
"ts": 1783810912350,
"aspa_objects": 2352,
"roa_count": 970494,
"atlas_asns_total": 4765,
"atlas_asns_with_aspa": 495,
"coverage_pct_atlas": 10.39,
"aspa_delta": 2,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-07-12",
"ts": 1783897568710,
"aspa_objects": 2355,
"roa_count": 970426,
"atlas_asns_total": 4769,
"atlas_asns_with_aspa": 500,
"coverage_pct_atlas": 10.48,
"aspa_delta": 0,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-07-13",
"ts": 1783984224592,
"aspa_objects": 2361,
"roa_count": 970721,
"atlas_asns_total": 4780,
"atlas_asns_with_aspa": 505,
"coverage_pct_atlas": 10.56,
"aspa_delta": 3,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-07-14",
"ts": 1784071182188,
"aspa_objects": 2373,
"roa_count": 971705,
"atlas_asns_total": 0,
"atlas_asns_with_aspa": 0,
"coverage_pct_atlas": 0,
"aspa_delta": 0,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-07-15",
"ts": 1784159361526,
"aspa_objects": 2382,
"roa_count": 971732,
"atlas_asns_total": 4775,
"atlas_asns_with_aspa": 504,
"coverage_pct_atlas": 10.55,
"aspa_delta": 2,
"method": "rpki_cloudflare_feed"
},
{
"date": "2026-07-16",
"ts": 1784174057994,
"aspa_objects": 2382,
"roa_count": 971831,
"atlas_asns_total": 4775,
"atlas_asns_with_aspa": 504,
"coverage_pct_atlas": 10.55,
"aspa_delta": 0,
"method": "rpki_cloudflare_feed"
}
]

1
audit/asn_pool.json Normal file

File diff suppressed because one or more lines are too long

176
audit/daily_audit.py Executable file
View File

@ -0,0 +1,176 @@
#!/usr/bin/env python3
"""
PeerCortex Final Quality Audit v4
Tests 103 ASNs against v0.6.7 (running, stable)
Uses cached responses where available, skips ASNs that don't respond in 15s
Cross-validates: prefix counts, RPKI math, IX dedup, facilities, RIR, country
"""
import json, urllib.request, time, sys
from datetime import datetime, timezone
def fetch(url, timeout=15):
try:
req = urllib.request.Request(url, headers={"User-Agent": "PeerCortex-Audit/4.0"})
with urllib.request.urlopen(req, timeout=timeout) as r:
return json.loads(r.read())
except Exception as e:
return None
ASNS = [
1, 174, 714, 1299, 2497, 2516, 2906, 3257, 3303, 3320, 3356,
4134, 4766, 5089, 5483, 5511, 6057, 6128, 6147, 6453, 6695,
6762, 6830, 6939, 7018, 7473, 7545, 7922, 8075, 8151, 8220,
8422, 8708, 9121, 9304, 9318, 9498, 10429, 10474, 10796, 12479,
12714, 12956, 13237, 13335, 14080, 15133, 15169, 15600, 15704,
16276, 16509, 17676, 18403, 19281, 20115, 20562, 20932, 20940,
21928, 22773, 22822, 23106, 24115, 24429, 24940, 24961, 25091,
29049, 32934, 33891, 34465, 34549, 35369, 37100, 38001, 39122,
41327, 42, 42739, 43350, 43996, 44574, 45899, 47692, 48159,
50629, 51088, 54113, 56630, 57695, 58057, 59947, 60068, 60501,
62240, 64271, 132335, 136907, 137409, 139070, 197540, 199121,
]
results = {"ok": 0, "warn": 0, "crit": 0, "skip": 0, "total": len(ASNS)}
all_issues = []
responded = []
print(f"PeerCortex Final Audit v4 — {datetime.now(timezone.utc).isoformat()}")
print(f"Target: http://localhost:3101 | ASNs: {len(ASNS)}")
print("=" * 75)
for i, asn in enumerate(ASNS):
sys.stdout.write(f"\r [{i+1:3d}/{len(ASNS)}] AS{asn}... ")
sys.stdout.flush()
d = fetch(f"http://localhost:3101/api/lookup?asn={asn}", timeout=15)
if not d or "network" not in d or d.get("error"):
results["skip"] += 1
continue
responded.append(asn)
n = d.get("network", {})
p = d.get("prefixes", {})
r = d.get("rpki", {})
ix = d.get("ix_presence", {})
fac = d.get("facilities", {})
meta= d.get("meta", {})
reg = d.get("registration", {})
issues = []
# ── Math checks ─────────────────────────────────────────
if p.get("ipv4", 0) + p.get("ipv6", 0) != p.get("total", -1):
issues.append(("CRITICAL", "prefix_math",
f"v4({p.get('ipv4')})+v6({p.get('ipv6')})={p.get('ipv4',0)+p.get('ipv6',0)} ≠ total({p.get('total')})"))
rpki_sum = r.get("valid",0) + r.get("invalid",0) + r.get("not_found",0)
if rpki_sum != r.get("checked", 0):
issues.append(("CRITICAL", "rpki_sum",
f"{rpki_sum} ≠ checked({r.get('checked')})"))
if r.get("checked", 0) > 0:
exp = round(r.get("valid",0) / r.get("checked") * 100)
if abs(exp - r.get("coverage_percent", 0)) > 1:
issues.append(("CRITICAL", "rpki_pct",
f"expected {exp}% got {r.get('coverage_percent')}%"))
conns = ix.get("connections", [])
dedup = len(set(c.get("ix_id") for c in conns if c.get("ix_id")))
if dedup != ix.get("unique_ixps", 0):
issues.append(("CRITICAL", "ix_dedup",
f"dedup={dedup} ≠ unique_ixps={ix.get('unique_ixps')}"))
if len(conns) != ix.get("total_connections", 0):
issues.append(("WARNING", "ix_total",
f"len={len(conns)} ≠ total_connections={ix.get('total_connections')}"))
if len(fac.get("list",[])) != fac.get("total", 0):
issues.append(("WARNING", "fac_total",
f"len={len(fac.get('list',[]))} ≠ total={fac.get('total')}"))
# ── Mandatory fields ─────────────────────────────────────
if not n.get("name") or n.get("name") == "Unknown":
issues.append(("CRITICAL", "name_empty", f"name={n.get('name')!r}"))
if not n.get("asn"):
issues.append(("CRITICAL", "asn_empty", "ASN field missing"))
if not n.get("rir") and not reg.get("rir"):
issues.append(("WARNING", "rir_empty", f"rir empty, country={n.get('country')!r}"))
if not n.get("country"):
issues.append(("WARNING", "country_empty", "country field empty"))
if meta.get("version") != "0.6.7":
issues.append(("WARNING", "version", f"got {meta.get('version')}"))
# ── Categorize ───────────────────────────────────────────
crits = [x for x in issues if x[0] == "CRITICAL"]
warns = [x for x in issues if x[0] == "WARNING"]
if crits:
results["crit"] += 1
elif warns:
results["warn"] += 1
else:
results["ok"] += 1
if issues:
all_issues.append((asn, n.get("name","?"), issues,
p.get("total"), r.get("coverage_percent"),
ix.get("total_connections"), fac.get("total"),
n.get("rir"), n.get("country")))
print(f"\r Done. {len(responded)} responded, {results['skip']} skipped. ")
print()
print("=" * 75)
print(f"FINAL AUDIT SUMMARY — {len(responded)} ASNs responded ({results['skip']} timed out)")
print("=" * 75)
print(f" ✓ PERFECT (zero issues): {results['ok']:3d}")
print(f" ⚠ WARNING only: {results['warn']:3d}")
print(f" ✗ CRITICAL: {results['crit']:3d}")
print(f" ⏭ Skipped (no response): {results['skip']:3d}")
if results["crit"] == 0:
print("\n ✅ NO CRITICAL DATA ERRORS IN ANY RESPONDED ASN")
# Per-category counts
warn_counts = {}
crit_counts = {}
for _, _, issues, *_ in all_issues:
for sev, field, _ in issues:
if sev == "CRITICAL":
crit_counts[field] = crit_counts.get(field, 0) + 1
else:
warn_counts[field] = warn_counts.get(field, 0) + 1
if crit_counts:
print("\n CRITICAL by field:")
for f, c in sorted(crit_counts.items(), key=lambda x: -x[1]):
print(f" {f:35s}: {c} ASNs")
if warn_counts:
print("\n WARNING by field:")
for f, c in sorted(warn_counts.items(), key=lambda x: -x[1]):
print(f" {f:35s}: {c} ASNs")
# Detail for any issues
if all_issues:
print("\n DETAILS:")
for asn, name, issues, pfx, rpki_pct, ix_cnt, fac_cnt, rir, cc in all_issues:
for sev, field, msg in issues:
print(f" [{sev[:4]}] AS{asn:>7} | {field:30s} | {msg[:60]}")
print()
print(" SAMPLE DATA (responded ASNs):")
print(f" {'ASN':>8} {'Name':24s} {'RIR':>6} {'CC':>3} {'Pfx':>5} {'RPKI%':>6} {'IX':>4} {'Fac':>4}")
print(f" {'-'*70}")
for asn in responded[:25]:
d = fetch(f"http://localhost:3101/api/lookup?asn={asn}", timeout=5)
if not d: continue
n=d.get("network",{}); p=d.get("prefixes",{}); r=d.get("rpki",{}); ix=d.get("ix_presence",{}); fac=d.get("facilities",{})
print(f" AS{asn:>6} {n.get('name','?')[:22]:24s} {n.get('rir','?'):>6} {n.get('country','?'):>3} {p.get('total',0):>5} {r.get('coverage_percent',0):>5}% {ix.get('total_connections',0):>4} {fac.get('total',0):>4}")
print()
print(f"Audit complete: {datetime.now(timezone.utc).isoformat()}")

270
audit/rotating_audit.py Normal file
View File

@ -0,0 +1,270 @@
#!/usr/bin/env python3
"""
PeerCortex Rotating Daily Audit
- Pool of 500+ ASNs
- Each day: deterministic 100-ASN selection (seeded by date reproducible, never same day twice)
- Each ASN: PeerCortex response cross-validated against RIPE Stat + PeeringDB externally
- Math checks: prefix sums, RPKI sums, IX dedup, facility counts
- Results saved to /var/log/peercortex/audit-YYYY-MM-DD.json
"""
import json, urllib.request, urllib.error, time, sys, os
from datetime import datetime, timezone
from hashlib import sha256
# ── ASN Pool (500+ diverse networks across all RIRs) ──────────────────────────
ASN_POOL = [
# Tier-1 / Large Transit
1, 174, 209, 286, 701, 702, 1239, 1273, 1280, 1299, 2914, 3257, 3320,
3356, 3491, 3549, 3561, 4134, 4637, 4657, 5511, 6453, 6461, 6762, 6830,
7018, 7473, 7922, 8220, 8928, 9002, 9304, 12956, 13030,
# Cloud / Hyperscaler
714, 2906, 8075, 13335, 15169, 16509, 16591, 20940, 21342, 32934, 36351,
54113, 136907, 14061, 19604, 63949, 22616,
# European ISPs
1257, 1853, 2119, 2603, 3209, 3215, 3216, 3233, 3243, 3269, 3292, 3301,
3303, 3308, 3327, 5089, 5390, 5432, 5483, 6057, 6128, 6147, 6695, 6728,
6830, 6939, 8220, 8359, 8400, 8422, 8447, 8468, 8473, 8551, 8708, 9121,
9145, 9158, 12714, 13237, 15435, 15547, 15600, 15692, 15830, 16276,
20562, 20932, 21202, 24940, 24961, 25091, 28716, 29049, 29208, 33891,
34465, 34549, 35369, 39122, 41327, 42, 42739, 43350, 47692, 48159,
50629, 51088, 58057, 197540, 199121,
# APNIC
2497, 2516, 4766, 7545, 9318, 9498, 17676, 18403, 23969, 24115, 38001,
45899, 55836, 132335, 136907, 137409, 139070, 4713, 9269, 17488, 23944,
38197, 45352, 63927, 131090, 133481, 134762, 137718, 149440,
# ARIN
701, 1239, 1280, 2828, 3549, 3561, 6128, 6939, 7018, 7922, 10429, 10796,
11427, 13977, 14080, 15133, 19281, 20115, 20325, 21928, 22773, 22822,
23106, 32934, 33070, 35911, 36351, 40191, 43996, 54113, 62240, 64271,
393398, 11404, 14618, 14593, 7046, 26101, 10310, 32590,
# LACNIC
6147, 8167, 10429, 10474, 11419, 11644, 14080, 18881, 22927, 23106,
28573, 52320, 53013, 61832, 262589, 263702,
# AFRINIC
10474, 12091, 23889, 24835, 30844, 36864, 36903, 37100, 37271, 37468,
55330, 327814,
# IXPs / Route Servers
6695, 24115, 34307, 42476, 43366, 47498, 56393,
# CDN / Hosting
2906, 8100, 13335, 15133, 16509, 20940, 22822, 24429, 46489, 54113,
60068, 60501, 62240, 64271, 132335,
# Security / DNS
19281, 15169, 13335, 714, 20473,
# Mixed / Small
29791, 34224, 35332, 39386, 41695, 42473, 44574, 47065, 48858, 50300,
56630, 57695, 58057, 59947, 60068, 60501, 63023, 64271, 132335,
# More European
1200, 2611, 3265, 5388, 5430, 5444, 6128, 8309, 8365, 8560, 8607,
9009, 12897, 13285, 15516, 16150, 20738, 21263, 24586, 24875, 25369,
28917, 29017, 30036, 30781, 31025, 31400, 33986, 34006, 34224, 34655,
35432, 39386, 39737, 41327, 41552, 42652, 43531, 44574, 47065,
# Telcos worldwide
2516, 3320, 3786, 4766, 4800, 5483, 7473, 8151, 8708, 9121, 9304,
9318, 9498, 10429, 12479, 12714, 15704, 16276, 17676, 18403, 22773,
]
# Deduplicate while preserving order
seen = set()
ASN_POOL_DEDUP = [x for x in ASN_POOL if not (x in seen or seen.add(x))]
def load_pool():
"""Load ASN pool from file, fall back to built-in list."""
pool_file = os.path.join(os.path.dirname(__file__), 'asn_pool.json')
try:
with open(pool_file) as f:
return json.load(f)
except:
return ASN_POOL_DEDUP
def select_daily_asns(date_str, count=100):
"""Deterministic daily selection — same date always gives same 100 ASNs."""
import random
pool = load_pool()
seed = int(sha256(date_str.encode()).hexdigest(), 16)
rng = random.Random(seed)
rng.shuffle(pool)
return sorted(pool[:count])
def fetch(url, timeout=12):
try:
req = urllib.request.Request(url, headers={"User-Agent": "PeerCortex-Audit/5.0"})
with urllib.request.urlopen(req, timeout=timeout) as r:
return json.loads(r.read())
except:
return None
def fetch_pc(asn, timeout=20):
try:
with urllib.request.urlopen(f"http://localhost:3101/api/lookup?asn={asn}", timeout=timeout) as r:
return json.loads(r.read())
except:
return None
def audit_asn(asn):
issues = []
result = {"asn": asn, "status": "ok", "issues": [], "data": {}}
# ── 1. PeerCortex response ───────────────────────────────────────────────
d = fetch_pc(asn)
if not d or "network" not in d or d.get("error"):
result["status"] = "skip"
result["issues"].append(("SKIP", "no_response", "PeerCortex did not respond"))
return result
n = d.get("network", {})
p = d.get("prefixes", {})
r = d.get("rpki", {})
ix = d.get("ix_presence", {})
fac = d.get("facilities", {})
meta= d.get("meta", {})
result["data"] = {
"name": n.get("name"),
"rir": n.get("rir"),
"cc": n.get("country"),
"pfx_total": p.get("total", 0),
"rpki_pct": r.get("coverage_percent", 0),
"ix_cnt": ix.get("total_connections", 0),
"fac_cnt": fac.get("total", 0),
"version": meta.get("version"),
}
# ── 2. Math checks (internal consistency) ───────────────────────────────
if p.get("ipv4", 0) + p.get("ipv6", 0) != p.get("total", -1):
issues.append(("CRITICAL", "prefix_math",
f"v4({p.get('ipv4')})+v6({p.get('ipv6')}) ≠ total({p.get('total')})"))
rpki_sum = r.get("valid", 0) + r.get("invalid", 0) + r.get("not_found", 0)
if rpki_sum != r.get("checked", 0):
issues.append(("CRITICAL", "rpki_sum",
f"{rpki_sum} ≠ checked({r.get('checked')})"))
if r.get("checked", 0) > 0:
exp = round(r.get("valid", 0) / r.get("checked") * 100)
if abs(exp - r.get("coverage_percent", 0)) > 1:
issues.append(("CRITICAL", "rpki_pct",
f"expected {exp}% got {r.get('coverage_percent')}%"))
conns = ix.get("connections", [])
dedup = len(set(c.get("ix_id") for c in conns if c.get("ix_id")))
if dedup != ix.get("unique_ixps", 0):
issues.append(("CRITICAL", "ix_dedup",
f"dedup={dedup} ≠ unique_ixps={ix.get('unique_ixps')}"))
if len(conns) != ix.get("total_connections", 0):
issues.append(("WARNING", "ix_total",
f"len={len(conns)} ≠ total_connections={ix.get('total_connections')}"))
if len(fac.get("list", [])) != fac.get("total", 0):
issues.append(("WARNING", "fac_total",
f"len={len(fac.get('list',[]))} ≠ total={fac.get('total')}"))
# ── 3. Mandatory fields ──────────────────────────────────────────────────
if not n.get("name") or n.get("name") == "Unknown":
issues.append(("CRITICAL", "name_empty", f"name={n.get('name')!r}"))
if not n.get("asn"):
issues.append(("CRITICAL", "asn_field", "ASN field missing in response"))
if not n.get("rir"):
issues.append(("WARNING", "rir_empty", f"rir empty, country={n.get('country')!r}"))
if not n.get("country"):
issues.append(("WARNING", "country_empty", "country field empty"))
# ── 4. External cross-validation: RIPE Stat prefix count ────────────────
ripe = fetch(f"https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS{asn}")
if ripe and ripe.get("data", {}).get("prefixes") is not None:
ripe_pfx = len(ripe["data"]["prefixes"])
pc_pfx = p.get("total", 0)
result["data"]["ripe_stat_pfx"] = ripe_pfx
if ripe_pfx > 0 and pc_pfx > 0:
ratio = min(ripe_pfx, pc_pfx) / max(ripe_pfx, pc_pfx)
if ratio < 0.85:
issues.append(("WARNING", "prefix_ext_mismatch",
f"PC={pc_pfx} vs RIPE Stat={ripe_pfx} ({round((1-ratio)*100)}% diff)"))
elif ripe_pfx == 0 and pc_pfx > 0:
issues.append(("WARNING", "prefix_ext_zero",
f"RIPE Stat reports 0 prefixes, PC shows {pc_pfx}"))
# ── 5. External cross-validation: PeeringDB IX count ────────────────────
pdb = fetch(f"https://www.peeringdb.com/api/netixlan?asn={asn}&depth=0")
if pdb and isinstance(pdb.get("data"), list):
pdb_ix = len(pdb["data"])
pc_ix = ix.get("total_connections", 0)
result["data"]["pdb_ix_cnt"] = pdb_ix
if pdb_ix > 0 and pc_ix > 0:
ratio = min(pdb_ix, pc_ix) / max(pdb_ix, pc_ix)
if ratio < 0.85:
issues.append(("WARNING", "ix_ext_mismatch",
f"PC={pc_ix} vs PeeringDB={pdb_ix} ({round((1-ratio)*100)}% diff)"))
# ── Categorize ───────────────────────────────────────────────────────────
result["issues"] = issues
crits = [x for x in issues if x[0] == "CRITICAL"]
warns = [x for x in issues if x[0] == "WARNING"]
if crits:
result["status"] = "critical"
elif warns:
result["status"] = "warning"
else:
result["status"] = "ok"
return result
# ── Main ──────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
asns = select_daily_asns(date_str, 100)
start = time.time()
print(f"PeerCortex Rotating Audit — {date_str}")
print(f"Pool: {len(ASN_POOL_DEDUP)} ASNs | Today: {len(asns)} selected")
print("=" * 70)
results = []
counts = {"ok": 0, "warning": 0, "critical": 0, "skip": 0}
for i, asn in enumerate(asns):
sys.stdout.write(f"\r [{i+1:3d}/100] AS{asn}... ")
sys.stdout.flush()
r = audit_asn(asn)
results.append(r)
counts[r["status"]] += 1
time.sleep(0.3) # be gentle to external APIs
elapsed = time.time() - start
print(f"\r Done in {elapsed:.0f}s" + " " * 30)
# ── Summary ───────────────────────────────────────────────────────────────
print()
print("=" * 70)
print(f"SUMMARY — {date_str}")
print("=" * 70)
print(f" ✓ PERFECT: {counts['ok']:3d}")
print(f" ⚠ WARNING: {counts['warning']:3d}")
print(f" ✗ CRITICAL: {counts['critical']:3d}")
print(f" ⏭ SKIP: {counts['skip']:3d}")
issues_found = [r for r in results if r["status"] in ("critical", "warning")]
if issues_found:
print(f"\n ISSUES ({len(issues_found)} ASNs):")
for r in issues_found:
for sev, field, msg in r["issues"]:
print(f" [{sev[:4]}] AS{r['asn']:>7} | {field:30s} | {msg[:55]}")
# ── Save JSON report ──────────────────────────────────────────────────────
report = {
"date": date_str,
"elapsed_s": round(elapsed),
"asns_tested": asns,
"pool_size": len(ASN_POOL_DEDUP),
"counts": counts,
"results": results,
"generated": datetime.now(timezone.utc).isoformat(),
}
out_path = f"/var/log/peercortex/audit-{date_str}.json"
os.makedirs("/var/log/peercortex", exist_ok=True)
with open(out_path, "w") as f:
json.dump(report, f, indent=2)
print(f"\nReport: {out_path}")
print(f"Audit complete: {datetime.now(timezone.utc).isoformat()}")

6
audit/run_daily_audit.sh Executable file
View File

@ -0,0 +1,6 @@
#!/bin/bash
DATE=$(date +%Y-%m-%d)
LOG=/var/log/peercortex/audit-${DATE}.log
find /var/log/peercortex/ -name 'audit-*.log' -mtime +30 -delete 2>/dev/null
find /var/log/peercortex/ -name 'audit-*.json' -mtime +30 -delete 2>/dev/null
python3 /opt/peercortex-app/audit/rotating_audit.py > "$LOG" 2>&1

226
audit/send_audit_report.py Executable file
View File

@ -0,0 +1,226 @@
#!/usr/bin/env python3
"""PeerCortex Daily Audit Mailer — detailed report via localhost:25"""
import os, json, smtplib
from datetime import datetime, timezone, timedelta
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import make_msgid, formatdate
SMTP_HOST = "localhost"
SMTP_PORT = 25
MAIL_FROM = "ctx-report@fichtmueller.org"
MAIL_TO = "rf@flexoptix.net"
def td(content, color=None, align="left", mono=False, bold=False):
ff = "monospace" if mono else "sans-serif"
fw = "font-weight:700;" if bold else ""
cl = "color:%s;" % color if color else ""
return '<td style="font-family:%s;font-size:11px;padding:3px 8px;text-align:%s;%s%s">%s</td>' % (ff, align, cl, fw, content)
def load_report():
for delta in [0, 1]:
d = (datetime.now(timezone.utc) - timedelta(days=delta)).strftime("%Y-%m-%d")
try:
with open("/var/log/peercortex/audit-%s.json" % d) as f:
return json.load(f), d
except FileNotFoundError:
continue
return None, datetime.now(timezone.utc).strftime("%Y-%m-%d")
def build(report, date_str):
if not report:
return ("<html><body style='background:#0f172a;color:#e2e8f0;padding:24px'><h2>No report for %s</h2></body></html>" % date_str,
"PeerCortex Audit %s — no report" % date_str)
counts = report.get("counts", {})
results = report.get("results", [])
elapsed = report.get("elapsed_s", 0)
pool = report.get("pool_size", 0)
ok = counts.get("ok", 0)
warn = counts.get("warning", 0)
crit = counts.get("critical", 0)
skip = counts.get("skip", 0)
total = len(results)
mins, secs = elapsed // 60, elapsed % 60
if crit == 0 and warn == 0:
sc, emoji, text, spfx = "#22c55e", "", "All Clear — no issues detected", "✅ All Clear"
elif crit == 0:
sc, emoji, text, spfx = "#f97316", "⚠️", "%d warning(s)" % warn, "⚠️ %d Warning(s)" % warn
else:
sc, emoji, text, spfx = "#ef4444", "🚨", "%d CRITICAL issue(s) — action required" % crit, "🚨 %d CRITICAL" % crit
subject = "PeerCortex Daily Audit %s%s" % (date_str, spfx)
# Issues
issue_results = [r for r in results if r.get("status") in ("critical", "warning")]
issues_html = ""
if issue_results:
rows = ""
for r in issue_results:
asn = str(r["asn"])
name = (r.get("data", {}).get("name") or "?")[:28]
for sev, field, msg in r.get("issues", []):
c = "#ef4444" if sev == "CRITICAL" else "#f97316"
rows += "<tr style='border-bottom:1px solid #1e293b'>%s%s%s%s%s</tr>" % (
td("[%s]" % sev[:4], c, mono=True),
td("AS" + asn, "#94a3b8", mono=True),
td(name, "#e2e8f0"),
td(field, "#64748b", mono=True),
td(msg[:60], "#94a3b8"))
issues_html = (
"<h3 style='color:#f97316;font-size:12px;margin:24px 0 6px;text-transform:uppercase'>⚠ Issues (%d ASNs)</h3>" % len(issue_results) +
"<table style='width:100%%;border-collapse:collapse;background:#111827;border-radius:6px;overflow:hidden'>%s</table>" % rows
)
# Skipped
skip_results = [r for r in results if r.get("status") == "skip"]
skipped_html = ""
if skip_results:
rows = ""
for r in skip_results:
asn = str(r["asn"])
reason = r.get("issues", [["SKIP","","no response from PeerCortex"]])[0][2] if r.get("issues") else "no response from PeerCortex"
url = "https://peercortex.org/?asn=" + asn
rows += ("<tr style='border-bottom:1px solid #1e293b'>%s%s<td style='font-size:11px;padding:3px 8px'>"
"<a href='%s' style='color:#3b82f6;text-decoration:none'>→ lookup</a></td></tr>") % (
td("AS" + asn, "#94a3b8", mono=True),
td(reason, "#64748b"),
url)
skipped_html = (
"<h3 style='color:#475569;font-size:12px;margin:24px 0 6px;text-transform:uppercase'>⏭ Skipped ASNs (%d) — no PeerCortex response</h3>" % skip +
"<p style='color:#475569;font-size:11px;margin:0 0 8px'>Possible causes: ASN not in any routing table, very large prefix count, or temporary API timeout.</p>" +
"<table style='width:100%%;border-collapse:collapse;background:#111827;border-radius:6px;overflow:hidden'>%s</table>" % rows
)
# Low RPKI
rpki_bad = sorted(
[r for r in results if r.get("status") != "skip" and r.get("data", {}).get("rpki_pct", 100) < 60],
key=lambda r: r.get("data", {}).get("rpki_pct", 100)
)[:8]
rpki_html = ""
if rpki_bad:
rows = ""
for r in rpki_bad:
d = r.get("data", {})
pct = d.get("rpki_pct", 0)
bar = ("<div style='background:#1e293b;border-radius:3px;height:8px;width:100px;display:inline-block'>"
"<div style='background:#ef4444;border-radius:3px;height:8px;width:%d%%'></div></div>") % max(2, pct)
rows += "<tr style='border-bottom:1px solid #1e293b'>%s%s%s<td style='padding:3px 8px'>%s</td>%s</tr>" % (
td("AS" + str(r["asn"]), "#94a3b8", mono=True),
td((d.get("name") or "?")[:28], "#e2e8f0"),
td(d.get("rir", "?"), "#64748b", "center"),
bar,
td(str(pct) + "%", "#ef4444", "right", bold=True))
rpki_html = (
"<h3 style='color:#94a3b8;font-size:12px;margin:24px 0 6px;text-transform:uppercase'>📉 Low RPKI Coverage (&lt;60%%)</h3>" +
"<table style='width:100%%;border-collapse:collapse;background:#111827;border-radius:6px;overflow:hidden'>%s</table>" % rows
)
# All ASNs table
sorted_results = sorted([r for r in results if r.get("status") != "skip"], key=lambda x: x["asn"])
header = ("<tr style='background:#1e293b'>%s%s%s%s%s%s%s%s%s%s%s</tr>" % (
td("ASN", "#64748b", "right", mono=True),
td("Name", "#64748b"),
td("RIR", "#64748b", "center"),
td("CC", "#64748b", "center"),
td("Pfx", "#64748b", "right"),
td("RIPE Stat", "#64748b", "right"),
td("RPKI%", "#64748b", "right"),
td("IX", "#64748b", "right"),
td("PDB IX", "#64748b", "right"),
td("Fac", "#64748b", "right"),
td("Status", "#64748b", "center")))
rows = ""
for i, r in enumerate(sorted_results):
d = r.get("data", {})
bg = "#0f172a" if i % 2 == 0 else "#111827"
st = r.get("status", "ok")
if st == "critical":
st_html = '<span style="color:#ef4444;font-weight:700">✗ CRIT</span>'
elif st == "warning":
st_html = '<span style="color:#f97316">⚠ WARN</span>'
else:
st_html = '<span style="color:#22c55e">✓</span>'
pct = d.get("rpki_pct", 100)
rpki_c = "#ef4444" if pct < 50 else "#f97316" if pct < 80 else "#94a3b8"
ripe = str(d.get("ripe_stat_pfx", ""))
pdb = str(d.get("pdb_ix_cnt", ""))
rows += "<tr style='background:%s'>%s%s%s%s%s%s%s%s%s%s<td style='font-size:11px;padding:3px 8px;text-align:center'>%s</td></tr>" % (
bg,
td("AS" + str(r["asn"]), "#64748b", "right", mono=True),
td((d.get("name") or "?")[:28], "#e2e8f0"),
td(d.get("rir", "?"), "#94a3b8", "center"),
td(d.get("cc", "?"), "#94a3b8", "center"),
td(str(d.get("pfx_total", 0)), "#94a3b8", "right", mono=True),
td(ripe, "#64748b", "right", mono=True),
td(str(pct) + "%", rpki_c, "right", mono=True),
td(str(d.get("ix_cnt", 0)), "#94a3b8", "right", mono=True),
td(pdb, "#64748b", "right", mono=True),
td(str(d.get("fac_cnt", 0)), "#94a3b8", "right", mono=True),
st_html)
all_table = (
"<h3 style='color:#94a3b8;font-size:12px;margin:24px 0 6px;text-transform:uppercase'>All Tested ASNs (%d)</h3>" % len(sorted_results) +
"<div style='overflow-x:auto'><table style='width:100%%;border-collapse:collapse;border-radius:6px;overflow:hidden'>%s%s</table></div>" % (header, rows)
)
html = """<!DOCTYPE html>
<html><head><meta charset="utf-8"></head>
<body style="margin:0;padding:0;background:#0f172a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif">
<div style="max-width:900px;margin:0 auto;padding:24px 16px">
<div style="margin-bottom:20px">
<span style="color:#3b82f6;font-weight:700;font-size:18px;letter-spacing:1px">PEER</span><span style="color:#fff;font-weight:700;font-size:18px;letter-spacing:1px">CORTEX</span>
<span style="color:#475569;font-size:12px;margin-left:10px">Daily Audit Report &middot; %(date)s</span>
</div>
<div style="background:%(sc)s18;border:1px solid %(sc)s40;border-radius:8px;padding:14px 18px;margin-bottom:20px">
<div style="font-size:17px;font-weight:700;color:%(sc)s">%(emoji)s %(text)s</div>
<div style="color:#94a3b8;font-size:12px;margin-top:4px">%(total)d ASNs tested from pool of %(pool)d &middot; Runtime: %(mins)dm %(secs)ds &middot; Cross-validated: RIPE Stat + PeeringDB</div>
</div>
<table style="width:100%%;border-collapse:collapse;margin-bottom:24px"><tr>
<td style="width:22%%;text-align:center;background:#1e293b;border-radius:8px;padding:16px 8px">
<div style="font-size:32px;font-weight:700;color:#22c55e">%(ok)d</div><div style="color:#64748b;font-size:10px;margin-top:4px">PERFECT</div></td>
<td style="width:3%%"></td>
<td style="width:22%%;text-align:center;background:#1e293b;border-radius:8px;padding:16px 8px">
<div style="font-size:32px;font-weight:700;color:#f97316">%(warn)d</div><div style="color:#64748b;font-size:10px;margin-top:4px">WARNINGS</div></td>
<td style="width:3%%"></td>
<td style="width:22%%;text-align:center;background:#1e293b;border-radius:8px;padding:16px 8px">
<div style="font-size:32px;font-weight:700;color:#ef4444">%(crit)d</div><div style="color:#64748b;font-size:10px;margin-top:4px">CRITICAL</div></td>
<td style="width:3%%"></td>
<td style="width:22%%;text-align:center;background:#1e293b;border-radius:8px;padding:16px 8px">
<div style="font-size:32px;font-weight:700;color:#475569">%(skip)d</div><div style="color:#64748b;font-size:10px;margin-top:4px">SKIPPED</div></td>
</tr></table>
%(issues)s %(skipped)s %(rpki)s %(alltable)s
<div style="margin-top:32px;padding-top:14px;border-top:1px solid #1e293b;color:#475569;font-size:11px">
PeerCortex &middot; <a href="https://peercortex.org" style="color:#3b82f6;text-decoration:none">peercortex.org</a>
&middot; 100 ASNs rotate daily from %(pool)d ASN pool &middot; Audit at 02:00 UTC, report at 06:00 UTC
</div>
</div></body></html>""" % dict(
date=date_str, sc=sc, emoji=emoji, text=text,
total=total, pool=pool, mins=mins, secs=secs,
ok=ok, warn=warn, crit=crit, skip=skip,
issues=issues_html, skipped=skipped_html,
rpki=rpki_html, alltable=all_table)
return html, subject
def send_email(subject, html_body):
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = MAIL_FROM
msg["To"] = MAIL_TO
msg["Date"] = formatdate(localtime=True)
msg["Message-ID"] = make_msgid(domain="fichtmueller.org")
msg.attach(MIMEText("PeerCortex Daily Audit — please view in an HTML-capable email client.\n\nhttps://peercortex.org", "plain", "utf-8"))
msg.attach(MIMEText(html_body, "html", "utf-8"))
with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=15) as s:
s.sendmail(MAIL_FROM, [MAIL_TO], msg.as_bytes())
print("Sent '%s' -> %s" % (subject, MAIL_TO))
if __name__ == "__main__":
report, date_str = load_report()
html, subject = build(report, date_str)
print("Email size: %d bytes" % len(html.encode()))
send_email(subject, html)

3
audit/send_audit_report.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
export $(grep -E '^SMTP' /opt/peercortex-app/.env | xargs)
python3 /opt/peercortex-app/audit/send_audit_report.py

9
deploy-from-scp.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/bash
# Deploy PeerCortex from pre-copied files (server.js already in place via SCP)
BACKUP_DIR=/opt/peercortex-app/backups
mkdir -p $BACKUP_DIR
TS=$(date +%Y%m%d_%H%M%S)
cp /opt/peercortex-app/server.js $BACKUP_DIR/server.js.$TS
ls -t $BACKUP_DIR/server.js.* 2>/dev/null | tail -n +21 | xargs rm -f 2>/dev/null
echo "[backup] Saved server.js ($TS)"
pm2 restart peercortex

24550
hijack-alerts.json Normal file

File diff suppressed because it is too large Load Diff

2169
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -76,6 +76,7 @@
"ollama": "^0.5.12",
"pg": "^8.11.0",
"playwright": "^1.40.0",
"puppeteer": "^24.42.0",
"zod": "^3.24.0"
},
"devDependencies": {

View File

@ -417,6 +417,18 @@ body.dark .card{border-top-color:#e8e4dc}
.ac-name { font-family: var(--body); font-size: .82rem; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ac-meta { font-family: var(--mono); font-size: .6rem; color: var(--muted); white-space: nowrap; }
.ac-source { font-family: var(--mono); font-size: .55rem; color: var(--dim); margin-left: auto; padding-left: .4rem; white-space: nowrap; }
.pdf-menu-item {
display: block;
padding: .45rem 1rem;
font-family: var(--mono);
font-size: .72rem;
color: var(--text);
text-decoration: none;
border-bottom: 1px solid var(--border);
}
.pdf-menu-item:last-child { border-bottom: none; }
.pdf-menu-item:hover { background: var(--purple); color: #fff !important; }
</style>
</head>
<body>
@ -439,6 +451,7 @@ body.dark .card{border-top-color:#e8e4dc}
<a href="https://atlas.ripe.net" target="_blank">RIPE Atlas</a>
<a href="https://www.routeviews.org" target="_blank">Route Views</a>
<a href="https://bgproutes.io" target="_blank">bgproutes.io</a>
<a href="#" onclick="openAdoptionPanel();return false" style="color:var(--purple);border-bottom:2px solid var(--purple);padding-bottom:2px" title="ASPA &amp; IPv6 Adoption Tracker">Adoption ✦</a>
<span class="share-dropdown" id="shareDropdown">
<a href="#" onclick="toggleShareMenu();return false" id="shareNavLink" title="Share" style="display:flex;align-items:center;gap:.35rem">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>
@ -934,6 +947,7 @@ body.dark .card{border-top-color:#e8e4dc}
<a href="https://stat.ripe.net" target="_blank">RIPE Stat</a>
<a href="https://atlas.ripe.net" target="_blank">RIPE Atlas</a>
<a href="https://bgproutes.io" target="_blank">bgproutes.io</a>
<a href="#" onclick="openAdoptionPanel();return false" style="color:var(--purple);border-bottom:2px solid var(--purple);padding-bottom:2px" title="ASPA &amp; IPv6 Adoption Tracker">Adoption ✦</a>
<a href="https://github.com/renefichtmueller/PeerCortex" target="_blank">GitHub</a>
</nav>
<div class="ed-footer-copy">v0.6.9 · Open Source · MIT License · Data powered by PeeringDB · RIPE Stat · RIPE Atlas · Route Views · bgp.he.net · bgproutes.io · RIPE DB · Cloudflare RPKI</div>
@ -1461,6 +1475,17 @@ function renderDashboard(d) {
if (n.looking_glass) ov += '<a class="ext-link" href="' + escAttr(n.looking_glass) + '" target="_blank">Looking Glass</a>';
if (n.route_server) ov += '<a class="ext-link" href="' + escAttr(n.route_server) + '" target="_blank">Route Server</a>';
ov += '<a class="ext-link" href="#" onclick="exportRawJson(event)" title="Download full lookup data as JSON">⬇ Raw JSON</a>';
ov += '<span style="display:inline-block;vertical-align:middle;margin:0 .5rem">';
ov += '<span style="font-family:var(--mono);font-size:.58rem;color:var(--muted);letter-spacing:.06em;font-style:italic">Addon only for Izzy</span>';
ov += '</span>';
ov += '<span class="pdf-btn-wrap" style="position:relative;display:inline-block">';
ov += '<a class="ext-link" href="#" onclick="togglePdfMenu(event)" style="color:var(--purple)">⬇ PDF ▾</a>';
ov += '<div id="pdfMenu" style="display:none;position:absolute;top:1.6rem;left:0;background:var(--bg);border:1px solid var(--border);z-index:9000;white-space:nowrap;min-width:200px;box-shadow:0 4px 16px rgba(0,0,0,.2)">';
ov += '<a href="/api/export/pdf?asn=' + n.asn + '&format=report" target="_blank" class="pdf-menu-item">Full Report (20+ pages)</a>';
ov += '<a href="/api/export/pdf?asn=' + n.asn + '&format=executive" target="_blank" class="pdf-menu-item">Executive Summary (3-5 pages)</a>';
ov += '<a href="/api/export/pdf?asn=' + n.asn + '&format=technical" target="_blank" class="pdf-menu-item">Technical Deep-Dive (10-15 pages)</a>';
ov += '</div>';
ov += '</span>';
ov += '</div>';
$('overviewContent').innerHTML = ov;
@ -2058,6 +2083,9 @@ async function doCompare() {
}
h += '<div style="font-size:.7rem;color:var(--dim);margin-top:.5rem">Compared in ' + d.meta.duration_ms + 'ms</div>';
h += '<div style="margin-top:.75rem;padding-top:.75rem;border-top:1px solid var(--border);text-align:right">';
h += '<a href="/api/export/pdf/compare?asn1=' + currentAsn + '&asn2=' + raw2 + '" target="_blank" style="display:inline-block;padding:.45rem 1rem;background:var(--purple);color:#fff;font-family:var(--mono);font-size:.7rem;letter-spacing:.06em;text-transform:uppercase;text-decoration:none" title="Download comparison as PDF">⬇ Download Comparison PDF</a>';
h += '</div>';
$('compareResults').innerHTML = h;
} catch (e) {
$('compareResults').innerHTML = '<div style="color:var(--red);font-size:.8rem">Compare failed: ' + escHtml(e.message) + '</div>';
@ -2701,6 +2729,10 @@ function renderFullCompare(d) {
h += '</div>';
h += '<div style="font-size:.7rem;color:var(--dim);margin-top:.75rem">Compared in ' + d.meta.duration_ms + 'ms</div>';
h += '<div style="margin-top:1rem;padding-top:.75rem;border-top:1px solid var(--border);display:flex;align-items:center;gap:.75rem">';
h += '<span style="font-family:var(--mono);font-size:.6rem;color:var(--muted);letter-spacing:.06em;font-style:italic">Addon only for Izzy</span>';
h += '<a href="/api/export/pdf/compare?asn1=' + d.asn1.asn + '&asn2=' + d.asn2.asn + '" target="_blank" style="display:inline-block;padding:.4rem 1rem;background:var(--purple);color:#fff;font-family:var(--mono);font-size:.68rem;letter-spacing:.06em;text-transform:uppercase;text-decoration:none">⬇ Download Comparison PDF</a>';
h += '</div>';
h += '</div>'; // end card
panel.innerHTML = h;
@ -4319,6 +4351,18 @@ function renderSourceTiming(d) {
}
// ── Raw JSON Export ────────────────────────────────────────────
function togglePdfMenu(e) {
e.preventDefault();
e.stopPropagation();
var menu = document.getElementById('pdfMenu');
if (!menu) return;
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
}
document.addEventListener('click', function() {
var menu = document.getElementById('pdfMenu');
if (menu) menu.style.display = 'none';
});
function exportRawJson(e) {
e.preventDefault();
if (!window._lastLookupData) { alert('No lookup data available.'); return; }
@ -4419,5 +4463,314 @@ document.addEventListener('keydown', e => { if (e.key === 'Escape') closeChangel
<div id="changelogContent" style="padding:0 2rem 2rem;max-height:72vh;overflow-y:auto"></div>
</div>
</div>
<!-- ADOPTION TRACKER OVERLAY -->
<div id="adoptionOverlay" style="display:none;position:fixed;inset:0;background:rgba(28,25,23,.6);z-index:9999;align-items:flex-start;justify-content:center;padding:2rem 1rem;overflow-y:auto;backdrop-filter:blur(2px)">
<div style="background:var(--bg);max-width:900px;width:100%;border-top:3px solid var(--text);position:relative">
<div style="padding:1.5rem 2rem 1rem;border-bottom:1px solid var(--border);display:flex;align-items:baseline;justify-content:space-between">
<div>
<div style="font-family:var(--serif);font-size:1.6rem;font-weight:900;letter-spacing:-.02em;line-height:1">Adoption Tracker</div>
<div style="font-family:var(--mono);font-size:.6rem;color:var(--muted);letter-spacing:.1em;margin-top:.2rem">GLOBAL INTERNET ADOPTION METRICS</div>
</div>
<button onclick="closeAdoptionPanel()" style="background:none;border:none;color:var(--muted);cursor:pointer;font-family:var(--mono);font-size:.65rem;letter-spacing:.08em;text-transform:uppercase;padding:.3rem .5rem">Close ✕</button>
</div>
<div style="display:flex;border-bottom:1px solid var(--border);padding:0 2rem">
<button id="adoptTab-aspa" onclick="switchAdoptionTab('aspa')" style="background:none;border:none;border-bottom:2px solid var(--purple);color:var(--text);cursor:pointer;font-family:var(--mono);font-size:.65rem;letter-spacing:.08em;text-transform:uppercase;padding:.75rem 1.2rem .65rem;margin-bottom:-1px">ASPA Adoption</button>
<button id="adoptTab-ipv6" onclick="switchAdoptionTab('ipv6')" style="background:none;border:none;border-bottom:2px solid transparent;color:var(--muted);cursor:pointer;font-family:var(--mono);font-size:.65rem;letter-spacing:.08em;text-transform:uppercase;padding:.75rem 1.2rem .65rem;margin-bottom:-1px">IPv6 per RIR</button>
</div>
<div style="padding:1.5rem 2rem 2rem;max-height:75vh;overflow-y:auto">
<div id="adoptPanel-aspa">
<div id="aspaAdoptContent"><p style="font-family:var(--mono);font-size:.75rem;color:var(--muted);text-align:center;padding:2rem">Loading ASPA data…</p></div>
</div>
<div id="adoptPanel-ipv6" style="display:none">
<div id="ipv6AdoptContent"><p style="font-family:var(--mono);font-size:.75rem;color:var(--muted);text-align:center;padding:2rem">Loading IPv6 data…</p></div>
</div>
</div>
</div>
</div>
<script>
(function() {
var _adoptionLoaded = false;
window.openAdoptionPanel = function() {
var overlay = document.getElementById('adoptionOverlay');
overlay.style.display = 'flex';
document.body.style.overflow = 'hidden';
if (!_adoptionLoaded) { _adoptionLoaded = true; loadAllAdoptionData(); }
};
window.closeAdoptionPanel = function() {
document.getElementById('adoptionOverlay').style.display = 'none';
document.body.style.overflow = '';
};
document.getElementById('adoptionOverlay').addEventListener('click', function(e) {
if (e.target === this) window.closeAdoptionPanel();
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') window.closeAdoptionPanel();
});
window.switchAdoptionTab = function(tab) {
['aspa','ipv6'].forEach(function(t) {
var btn = document.getElementById('adoptTab-' + t);
var panel = document.getElementById('adoptPanel-' + t);
var active = t === tab;
btn.style.borderBottomColor = active ? 'var(--purple)' : 'transparent';
btn.style.color = active ? 'var(--text)' : 'var(--muted)';
panel.style.display = active ? 'block' : 'none';
});
};
function loadAllAdoptionData() {
loadAspaAdoptionData();
loadIpv6AdoptionData();
}
/* ── ASPA ───────────────────────────────────────────────── */
function loadAspaAdoptionData() {
var el = document.getElementById('aspaAdoptContent');
fetch('/api/aspa-adoption-stats?period=90d')
.then(function(r) { return r.json(); })
.then(function(d) {
el.innerHTML = renderAspaAdoption(d);
setTimeout(function() { drawAspaChart(d); }, 60);
})
.catch(function() {
el.innerHTML = '<p style="font-family:var(--mono);font-size:.75rem;color:#b83a1b;text-align:center;padding:2rem">Failed to load ASPA data</p>';
});
}
function renderAspaAdoption(d) {
var snaps = d.trend || [];
var latestSnap = snaps.length ? snaps[snaps.length - 1] : {};
var pct = (d.current || {}).coverage_pct_atlas || 0; var current = d.current || {};
var atlasTotal = current.atlas_asns_total || 0;
var aspaCnt = current.atlas_asns_with_aspa || 0;
var totalAspa = current.aspa_objects || 0;
var firstSnap = snaps.length > 1 ? snaps[0] : null; var currentDate = (d.current || {}).date || '';
var delta = firstSnap ? Math.round((pct - (firstSnap.coverage_pct_atlas || 0)) * 100) / 100 : 0;
var trend = delta >= 0 ? '+' + delta : '' + delta;
var deltaColor = delta >= 0 ? '#2d6a4f' : '#b83a1b';
var rows = snaps.slice().reverse().slice(0, 15).map(function(s) {
return '<tr style="border-bottom:1px solid var(--border)">'
+ '<td style="padding:.35rem .5rem">' + s.date + '</td>'
+ '<td style="text-align:right;padding:.35rem .5rem;color:var(--purple);font-weight:700">' + s.coverage_pct_atlas + '%</td>'
+ '<td style="text-align:right;padding:.35rem .5rem">' + (s.atlas_asns_with_aspa||0) + '</td>'
+ '<td style="text-align:right;padding:.35rem .5rem">' + (s.atlas_asns_total||0) + '</td>'
+ '<td style="text-align:right;padding:.35rem .5rem">' + (s.aspa_objects||0).toLocaleString() + '</td>'
+ '</tr>';
}).join('');
return '<div style="margin-bottom:1.5rem">'
+ '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:1rem;margin-bottom:1.5rem">'
+ '<div style="border:1px solid var(--border);padding:1rem 1.25rem">'
+ '<div style="font-family:var(--mono);font-size:.6rem;color:var(--muted);letter-spacing:.08em;text-transform:uppercase;margin-bottom:.3rem">Atlas Coverage</div>'
+ '<div style="font-family:var(--serif);font-size:2rem;font-weight:900;letter-spacing:-.03em;color:var(--purple)">' + pct + '%</div>'
+ '<div style="font-family:var(--mono);font-size:.65rem;color:var(--muted);margin-top:.25rem">' + aspaCnt + ' / ' + atlasTotal + ' ASNs</div>'
+ '</div>'
+ '<div style="border:1px solid var(--border);padding:1rem 1.25rem">'
+ '<div style="font-family:var(--mono);font-size:.6rem;color:var(--muted);letter-spacing:.08em;text-transform:uppercase;margin-bottom:.3rem">ASPA Objects</div>'
+ '<div style="font-family:var(--serif);font-size:2rem;font-weight:900;letter-spacing:-.03em">' + totalAspa.toLocaleString() + '</div>'
+ '<div style="font-family:var(--mono);font-size:.65rem;color:var(--muted);margin-top:.25rem">in RPKI</div>'
+ '</div>'
+ '<div style="border:1px solid var(--border);padding:1rem 1.25rem">'
+ '<div style="font-family:var(--mono);font-size:.6rem;color:var(--muted);letter-spacing:.08em;text-transform:uppercase;margin-bottom:.3rem">90d Change</div>'
+ '<div style="font-family:var(--serif);font-size:2rem;font-weight:900;letter-spacing:-.03em;color:' + deltaColor + '">' + trend + '%</div>'
+ '<div style="font-family:var(--mono);font-size:.65rem;color:var(--muted);margin-top:.25rem">vs. 90 days ago</div>'
+ '</div>'
+ '</div>'
+ '<div style="border:1px solid var(--border);padding:1rem 1.25rem;margin-bottom:1rem">'
+ '<div style="font-family:var(--mono);font-size:.6rem;color:var(--muted);letter-spacing:.08em;text-transform:uppercase;margin-bottom:.75rem">Atlas Coverage Trend (' + (d.period||'90d') + ')</div>'
+ '<canvas id="aspaChartCanvas" height="160" style="width:100%;display:block"></canvas>'
+ '</div>'
+ '<div style="border:1px solid var(--border);padding:1rem 1.25rem">'
+ '<div style="font-family:var(--mono);font-size:.6rem;color:var(--muted);letter-spacing:.08em;text-transform:uppercase;margin-bottom:.75rem">Recent Snapshots</div>'
+ '<table style="width:100%;border-collapse:collapse;font-family:var(--mono);font-size:.72rem">'
+ '<thead><tr style="border-bottom:1px solid var(--border)">'
+ '<th style="text-align:left;padding:.35rem .5rem;color:var(--muted);font-weight:400">Date</th>'
+ '<th style="text-align:right;padding:.35rem .5rem;color:var(--muted);font-weight:400">Coverage %</th>'
+ '<th style="text-align:right;padding:.35rem .5rem;color:var(--muted);font-weight:400">ASNs w/ ASPA</th>'
+ '<th style="text-align:right;padding:.35rem .5rem;color:var(--muted);font-weight:400">Total Atlas</th>'
+ '<th style="text-align:right;padding:.35rem .5rem;color:var(--muted);font-weight:400">ASPA Objects</th>'
+ '</tr></thead>'
+ '<tbody>' + rows + '</tbody>'
+ '</table>'
+ '</div>'
+ '</div>';
}
function drawAspaChart(d) {
var canvas = document.getElementById('aspaChartCanvas');
if (!canvas) return;
var snaps = d.snapshots || [];
if (snaps.length < 2) { canvas.style.display = 'none'; return; }
var pts = snaps.map(function(s) { return s.coverage_pct_atlas; });
var W = canvas.offsetWidth || 800;
canvas.width = W;
var H = 160;
canvas.height = H;
var ctx = canvas.getContext('2d');
var minV = Math.max(0, Math.min.apply(null, pts) - 1);
var maxV = Math.max.apply(null, pts) + 1;
var pad = { t:10, b:24, l:40, r:10 };
var w = W - pad.l - pad.r, h = H - pad.t - pad.b;
var xf = function(i) { return pad.l + (i / (pts.length - 1)) * w; };
var yf = function(v) { return pad.t + h - ((v - minV) / (maxV - minV)) * h; };
var st = getComputedStyle(document.documentElement);
var bc = st.getPropertyValue('--border').trim() || '#C9C3B6';
var mc = st.getPropertyValue('--muted').trim() || '#8a8278';
ctx.strokeStyle = bc; ctx.lineWidth = 1;
for (var tick = 0; tick <= 4; tick++) {
var yy = pad.t + (tick / 4) * h;
ctx.beginPath(); ctx.moveTo(pad.l, yy); ctx.lineTo(pad.l + w, yy); ctx.stroke();
ctx.fillStyle = mc; ctx.font = '10px monospace'; ctx.textAlign = 'right';
ctx.fillText((maxV - (tick / 4) * (maxV - minV)).toFixed(1) + '%', pad.l - 4, yy + 4);
}
ctx.beginPath(); ctx.moveTo(xf(0), yf(pts[0]));
for (var i2 = 1; i2 < pts.length; i2++) ctx.lineTo(xf(i2), yf(pts[i2]));
ctx.lineTo(xf(pts.length - 1), H - pad.b); ctx.lineTo(xf(0), H - pad.b);
ctx.closePath(); ctx.fillStyle = 'rgba(184,58,27,.12)'; ctx.fill();
ctx.beginPath(); ctx.moveTo(xf(0), yf(pts[0]));
for (var i3 = 1; i3 < pts.length; i3++) ctx.lineTo(xf(i3), yf(pts[i3]));
ctx.strokeStyle = '#B83A1B'; ctx.lineWidth = 2; ctx.stroke();
for (var i4 = 0; i4 < pts.length; i4++) {
ctx.beginPath(); ctx.arc(xf(i4), yf(pts[i4]), 3, 0, Math.PI * 2);
ctx.fillStyle = '#B83A1B'; ctx.fill();
}
ctx.fillStyle = mc; ctx.font = '9px monospace'; ctx.textAlign = 'center';
var step = Math.max(1, Math.ceil(pts.length / 6));
for (var i5 = 0; i5 < snaps.length; i5++) {
if (i5 % step === 0 || i5 === snaps.length - 1)
ctx.fillText(snaps[i5].date.slice(5), xf(i5), H - pad.b + 14);
}
}
/* ── IPv6 ───────────────────────────────────────────────── */
var RIR_COLORS = { RIPE:'#4361ee', ARIN:'#3a0ca3', APNIC:'#7209b7', AFRINIC:'#f72585', LACNIC:'#4cc9f0' };
function loadIpv6AdoptionData() {
var el = document.getElementById('ipv6AdoptContent');
fetch('/api/ipv6-adoption-stats')
.then(function(r) { return r.json(); })
.then(function(d) {
el.innerHTML = renderIpv6Adoption(d);
setTimeout(function() { drawIpv6Chart(d); }, 60);
})
.catch(function() {
el.innerHTML = '<p style="font-family:var(--mono);font-size:.75rem;color:#b83a1b;text-align:center;padding:2rem">Failed to load IPv6 data</p>';
});
}
function renderIpv6Adoption(d) {
var rirs = d.rirs || [];
var global = d.global || {};
var fetchedAt = d.fetched_at ? new Date(d.fetched_at).toLocaleDateString() : 'N/A';
var statCards = rirs.map(function(r) {
return '<div style="border:1px solid var(--border);padding:1rem 1.25rem">'
+ '<div style="font-family:var(--mono);font-size:.6rem;color:var(--muted);letter-spacing:.08em;text-transform:uppercase;margin-bottom:.3rem">' + r.rir + '</div>'
+ '<div style="font-family:var(--serif);font-size:1.8rem;font-weight:900;letter-spacing:-.03em;color:' + (RIR_COLORS[r.rir] || 'var(--text)') + '">' + r.ipv6_pct + '%</div>'
+ '<div style="font-family:var(--mono);font-size:.65rem;color:var(--muted);margin-top:.25rem">' + (r.ipv6_blocks||0).toLocaleString() + ' / ' + (r.total_blocks||0).toLocaleString() + ' blocks</div>'
+ '</div>';
}).join('');
var tableRows = rirs.map(function(r) {
return '<tr style="border-bottom:1px solid var(--border)">'
+ '<td style="padding:.35rem .5rem;font-weight:700">' + r.rir + '</td>'
+ '<td style="text-align:right;padding:.35rem .5rem;color:' + (RIR_COLORS[r.rir]||'var(--text)') + ';font-weight:700">' + r.ipv6_pct + '%</td>'
+ '<td style="text-align:right;padding:.35rem .5rem">' + (r.ipv6_blocks||0).toLocaleString() + '</td>'
+ '<td style="text-align:right;padding:.35rem .5rem">' + (r.ipv4_blocks||0).toLocaleString() + '</td>'
+ '<td style="text-align:right;padding:.35rem .5rem">' + (r.asn_records||0).toLocaleString() + '</td>'
+ '<td style="text-align:right;padding:.35rem .5rem">' + (r.total_blocks||0).toLocaleString() + '</td>'
+ '</tr>';
}).join('');
tableRows += '<tr style="border-top:2px solid var(--border);font-weight:700">'
+ '<td style="padding:.4rem .5rem">Global</td>'
+ '<td style="text-align:right;padding:.4rem .5rem;color:var(--purple)">' + (global.ipv6_pct||0) + '%</td>'
+ '<td style="text-align:right;padding:.4rem .5rem">' + (global.ipv6_blocks||0).toLocaleString() + '</td>'
+ '<td style="text-align:right;padding:.4rem .5rem">' + (global.ipv4_blocks||0).toLocaleString() + '</td>'
+ '<td style="text-align:right;padding:.4rem .5rem">' + (global.asn_records||0).toLocaleString() + '</td>'
+ '<td style="text-align:right;padding:.4rem .5rem">' + (global.total_blocks||0).toLocaleString() + '</td>'
+ '</tr>';
return '<div>'
+ '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:1rem;margin-bottom:1.5rem">'
+ '<div style="border:1px solid var(--border);padding:1rem 1.25rem">'
+ '<div style="font-family:var(--mono);font-size:.6rem;color:var(--muted);letter-spacing:.08em;text-transform:uppercase;margin-bottom:.3rem">Global IPv6 Rate</div>'
+ '<div style="font-family:var(--serif);font-size:2rem;font-weight:900;letter-spacing:-.03em;color:var(--purple)">' + (global.ipv6_pct||0) + '%</div>'
+ '<div style="font-family:var(--mono);font-size:.65rem;color:var(--muted);margin-top:.25rem">of all address blocks</div>'
+ '</div>'
+ statCards
+ '</div>'
+ '<div style="border:1px solid var(--border);padding:1rem 1.25rem;margin-bottom:1rem">'
+ '<div style="font-family:var(--mono);font-size:.6rem;color:var(--muted);letter-spacing:.08em;text-transform:uppercase;margin-bottom:.75rem">IPv6 Adoption by RIR</div>'
+ '<canvas id="ipv6ChartCanvas" height="180" style="width:100%;display:block"></canvas>'
+ '</div>'
+ '<div style="border:1px solid var(--border);padding:1rem 1.25rem">'
+ '<div style="font-family:var(--mono);font-size:.6rem;color:var(--muted);letter-spacing:.08em;text-transform:uppercase;margin-bottom:.75rem">Breakdown by RIR</div>'
+ '<table style="width:100%;border-collapse:collapse;font-family:var(--mono);font-size:.72rem">'
+ '<thead><tr style="border-bottom:1px solid var(--border)">'
+ '<th style="text-align:left;padding:.35rem .5rem;color:var(--muted);font-weight:400">RIR</th>'
+ '<th style="text-align:right;padding:.35rem .5rem;color:var(--muted);font-weight:400">IPv6 %</th>'
+ '<th style="text-align:right;padding:.35rem .5rem;color:var(--muted);font-weight:400">IPv6 Blocks</th>'
+ '<th style="text-align:right;padding:.35rem .5rem;color:var(--muted);font-weight:400">IPv4 Blocks</th>'
+ '<th style="text-align:right;padding:.35rem .5rem;color:var(--muted);font-weight:400">ASN Records</th>'
+ '<th style="text-align:right;padding:.35rem .5rem;color:var(--muted);font-weight:400">Total</th>'
+ '</tr></thead>'
+ '<tbody>' + tableRows + '</tbody>'
+ '</table>'
+ '<p style="font-family:var(--mono);font-size:.6rem;color:var(--muted);margin-top:.75rem">Source: RIR delegation files (RIPE, ARIN, APNIC, AFRINIC, LACNIC) · Updated: ' + fetchedAt + '</p>'
+ '</div>'
+ '</div>';
}
function drawIpv6Chart(d) {
var canvas = document.getElementById('ipv6ChartCanvas');
if (!canvas) return;
var rirs = d.rirs || [];
if (!rirs.length) return;
var W = canvas.offsetWidth || 800;
canvas.width = W;
var H = 180;
canvas.height = H;
var ctx = canvas.getContext('2d');
var pad = { t:10, b:32, l:42, r:10 };
var w = W - pad.l - pad.r, h = H - pad.t - pad.b;
var maxPct = Math.ceil(Math.max.apply(null, rirs.map(function(r) { return r.ipv6_pct; })) / 5) * 5 + 5;
var barW = Math.floor(w / rirs.length * 0.55);
var gap = w / rirs.length;
var st2 = getComputedStyle(document.documentElement);
var bc2 = st2.getPropertyValue('--border').trim() || '#C9C3B6';
var mc2 = st2.getPropertyValue('--muted').trim() || '#8a8278';
ctx.strokeStyle = bc2; ctx.lineWidth = 1;
for (var tick = 0; tick <= 4; tick++) {
var yy = pad.t + (tick / 4) * h;
ctx.beginPath(); ctx.moveTo(pad.l, yy); ctx.lineTo(pad.l + w, yy); ctx.stroke();
ctx.fillStyle = mc2; ctx.font = '10px monospace'; ctx.textAlign = 'right';
ctx.fillText((maxPct - (tick / 4) * maxPct).toFixed(0) + '%', pad.l - 4, yy + 4);
}
rirs.forEach(function(r, i) {
var barH = (r.ipv6_pct / maxPct) * h;
var bx = pad.l + gap * i + (gap - barW) / 2;
var by = pad.t + h - barH;
ctx.fillStyle = RIR_COLORS[r.rir] || '#666';
ctx.fillRect(bx, by, barW, barH);
if (barH > 18) {
ctx.fillStyle = '#fff'; ctx.font = 'bold 11px monospace'; ctx.textAlign = 'center';
ctx.fillText(r.ipv6_pct + '%', bx + barW / 2, by + barH / 2 + 4);
}
ctx.fillStyle = mc2; ctx.font = '10px monospace'; ctx.textAlign = 'center';
ctx.fillText(r.rir, bx + barW / 2, H - pad.b + 14);
});
}
})(); // end IIFE
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

4126
public/public/index.html Normal file

File diff suppressed because it is too large Load Diff

2357
public/public/index.html.bak Normal file

File diff suppressed because it is too large Load Diff

488
public/public/lia.html Normal file
View File

@ -0,0 +1,488 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lia's Paradise — RIPE Atlas Coverage Explorer</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--bg:#0f0f1a;--card:#1a1b26;--border:#2a2b3d;--border-light:#363750;--text:#e2e8f0;--text-dim:#94a3b8;--muted:#64748b;--dim:#475569;--purple:#a78bfa;--blue:#60a5fa;--green:#4ade80;--orange:#fbbf24;--red:#f87171;--cyan:#22d3ee;--pink:#f472b6}
body{font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
.header{background:linear-gradient(135deg,#1a1b26 0%,#1e1f30 100%);border-bottom:1px solid var(--border);padding:1.5rem 2rem;text-align:center}
.header h1{font-size:1.8rem;font-weight:800;background:linear-gradient(135deg,var(--pink),var(--purple),var(--cyan));-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:.25rem}
.header p{color:var(--text-dim);font-size:.85rem}
.easter-egg{font-size:.65rem;color:var(--dim);margin-top:.25rem;font-style:italic}
.controls{max-width:1400px;margin:1.5rem auto;padding:0 1.5rem;display:flex;gap:1rem;flex-wrap:wrap;align-items:center}
.search-box{flex:1;min-width:200px;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:.7rem 1rem;font-size:.9rem;color:var(--text);font-family:inherit;outline:none}
.search-box:focus{border-color:var(--purple)}
.search-box::placeholder{color:var(--dim)}
.rir-tabs{display:flex;gap:.4rem;flex-wrap:wrap}
.rir-tab{padding:.5rem 1rem;border-radius:8px;border:1px solid var(--border);background:var(--card);color:var(--text-dim);font-size:.8rem;font-weight:600;cursor:pointer;transition:all .2s;font-family:inherit}
.rir-tab:hover{border-color:var(--purple);color:var(--text)}
.rir-tab.active{background:linear-gradient(135deg,#5b21b6,#7c3aed);color:#fff;border-color:#7c3aed}
.export-btn{padding:.5rem 1.2rem;border-radius:8px;border:1px solid var(--cyan);background:transparent;color:var(--cyan);font-size:.8rem;font-weight:600;cursor:pointer;transition:all .2s;font-family:inherit}
.export-btn:hover{background:var(--cyan);color:var(--bg)}
.stats-bar{max-width:1400px;margin:.75rem auto;padding:0 1.5rem;display:flex;gap:1.5rem;flex-wrap:wrap}
.stat-pill{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:.5rem 1rem;display:flex;align-items:center;gap:.5rem}
.stat-pill .num{font-size:1.1rem;font-weight:800}
.stat-pill .label{font-size:.7rem;color:var(--muted);text-transform:uppercase;letter-spacing:.05em}
.content{max-width:1400px;margin:1rem auto;padding:0 1.5rem}
.loading{text-align:center;padding:3rem;color:var(--muted);font-size:.9rem}
.loading .spinner{display:inline-block;width:20px;height:20px;border:2px solid var(--border);border-top-color:var(--purple);border-radius:50%;animation:spin .6s linear infinite;margin-right:.5rem;vertical-align:middle}
@keyframes spin{to{transform:rotate(360deg)}}
.country-section{margin-bottom:1.5rem}
.country-header{display:flex;align-items:center;gap:.75rem;padding:.6rem 1rem;background:var(--card);border:1px solid var(--border);border-radius:10px;cursor:pointer;transition:all .2s;margin-bottom:.5rem}
.country-header:hover{border-color:var(--purple)}
.country-flag{font-size:1.2rem}
.country-name{font-weight:700;font-size:.95rem;flex:1}
.country-count{font-size:.75rem;color:var(--muted)}
.country-badge{padding:.2rem .6rem;border-radius:6px;font-size:.7rem;font-weight:700}
.badge-red{background:#f8717118;color:var(--red);border:1px solid #f8717130}
.badge-green{background:#4ade8018;color:var(--green);border:1px solid #4ade8030}
.badge-orange{background:#fbbf2418;color:var(--orange);border:1px solid #fbbf2430}
.asn-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:.4rem;padding:0 0 .5rem 0}
.asn-row{display:flex;align-items:center;gap:.65rem;padding:.5rem .85rem;background:var(--bg);border:1px solid var(--border);border-radius:8px;font-size:.8rem;transition:all .15s;cursor:pointer}
.asn-row:hover{border-color:var(--purple);transform:translateY(-1px)}
.asn-num{font-weight:700;color:var(--purple);min-width:80px;font-family:'JetBrains Mono',monospace}
.asn-name{flex:1;color:var(--text-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.asn-type{font-size:.65rem;padding:.15rem .4rem;border-radius:4px;background:var(--card);color:var(--dim);border:1px solid var(--border)}
.no-probe{color:var(--red);font-size:.65rem;font-weight:600}
.has-probe{color:var(--green);font-size:.65rem;font-weight:600}
.summary-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:.75rem;margin-bottom:1.5rem}
.summary-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:1.25rem;text-align:center}
.summary-card .rir-name{font-size:.75rem;color:var(--muted);text-transform:uppercase;letter-spacing:.1em;margin-bottom:.5rem}
.summary-card .big-num{font-size:2rem;font-weight:800}
.summary-card .sub{font-size:.7rem;color:var(--dim);margin-top:.25rem}
.progress-bar{height:6px;background:var(--bg);border-radius:3px;margin-top:.5rem;overflow:hidden}
.progress-fill{height:100%;border-radius:3px;transition:width .5s ease}
.hidden{display:none}
.footer{text-align:center;padding:2rem;color:var(--dim);font-size:.7rem;margin-top:2rem;border-top:1px solid var(--border)}
.footer a{color:var(--purple);text-decoration:none}
</style>
</head>
<body>
<div class="header">
<h1>Lia's Paradise</h1>
<p>RIPE Atlas Coverage Explorer — Networks without Atlas Probes, Anchors & Software Probes</p>
<div class="easter-egg">For Lia, who makes the Internet measurable — one probe at a time</div>
</div>
<div class="controls">
<input type="text" class="search-box" id="searchInput" placeholder="Search by country, ASN, or network name..." oninput="filterResults()">
<div class="rir-tabs" id="rirTabs">
<button class="rir-tab active" onclick="switchRIR('all',this)">All RIRs</button>
<button class="rir-tab" onclick="switchRIR('ripencc',this)">RIPE NCC</button>
<button class="rir-tab" onclick="switchRIR('arin',this)">ARIN</button>
<button class="rir-tab" onclick="switchRIR('apnic',this)">APNIC</button>
<button class="rir-tab" onclick="switchRIR('lacnic',this)">LACNIC</button>
<button class="rir-tab" onclick="switchRIR('afrinic',this)">AFRINIC</button>
</div>
<button class="export-btn" onclick="exportPDF()">Export PDF</button>
</div>
<div style="max-width:1400px;margin:.75rem auto;padding:0 1.5rem">
<div style="background:var(--card);border:1px solid var(--border);border-radius:12px;padding:1rem 1.5rem">
<div style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap">
<div style="font-size:.8rem;font-weight:600;color:var(--pink)">FILE LOOKUP</div>
<div style="font-size:.75rem;color:var(--dim)">Upload a file with ASNs or company names — we'll check probe coverage for each</div>
<div style="flex:1"></div>
<label style="padding:.45rem 1rem;border-radius:8px;border:1px solid var(--pink);background:transparent;color:var(--pink);font-size:.8rem;font-weight:600;cursor:pointer;transition:all .2s;display:inline-flex;align-items:center;gap:.4rem" onmouseenter="this.style.background='var(--pink)';this.style.color='var(--bg)'" onmouseleave="this.style.background='transparent';this.style.color='var(--pink)'">
<span>Upload File</span>
<input type="file" id="fileUpload" accept=".csv,.txt,.pdf,.xls,.xlsx,.doc,.docx" style="display:none" onchange="handleFileUpload(this)">
</label>
<span id="fileStatus" style="font-size:.7rem;color:var(--dim)"></span>
</div>
<div id="fileResults" class="hidden" style="margin-top:1rem"></div>
</div>
</div>
<div class="stats-bar" id="statsBar"></div>
<div id="summaryGrid" class="summary-grid" style="max-width:1400px;margin:1rem auto;padding:0 1.5rem"></div>
<div class="content" id="mainContent">
<div class="loading"><span class="spinner"></span>Loading Atlas coverage data across all RIRs... This may take a moment.</div>
</div>
<div class="footer">
<a href="/">Back to PeerCortex</a> &middot; Data from <a href="https://atlas.ripe.net" target="_blank">RIPE Atlas</a> &amp; <a href="https://www.peeringdb.com" target="_blank">PeeringDB</a>
<br>Made with love for the Atlas community
</div>
<script>
var allData = [];
var currentRIR = 'all';
var countryFlags = {};
function $(id) { return document.getElementById(id); }
function escHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
// Country code to flag emoji
function flag(cc) {
if (!cc || cc.length !== 2) return '';
return String.fromCodePoint(...cc.toUpperCase().split('').map(c => 0x1F1E6 + c.charCodeAt(0) - 65));
}
// RIR mapping by country
var rirMap = {
ripencc: new Set(['AT','BE','BG','HR','CY','CZ','DK','EE','FI','FR','DE','GR','HU','IS','IE','IT','LV','LT','LU','MT','NL','NO','PL','PT','RO','SK','SI','ES','SE','CH','GB','UA','RU','TR','GE','AZ','AM','MD','BY','RS','BA','ME','MK','AL','XK','LI','MC','SM','VA','AD','FO','GL','AX','GG','JE','IM','BH','IQ','IR','IL','JO','KW','LB','OM','PS','QA','SA','SY','AE','YE','DZ','EG','LY','MA','TN','EH']),
arin: new Set(['US','CA','PR','VI','GU','AS','MP','MH','FM','PW','UM']),
apnic: new Set(['AU','NZ','JP','KR','CN','HK','MO','TW','IN','BD','PK','LK','NP','BT','MV','AF','MM','TH','VN','LA','KH','MY','SG','ID','PH','BN','TL','PG','FJ','WS','TO','VU','SB','KI','NR','TV','MN','KZ','KG','TJ','TM','UZ']),
lacnic: new Set(['MX','GT','BZ','SV','HN','NI','CR','PA','CO','VE','EC','PE','BO','CL','AR','UY','PY','BR','GY','SR','GF','CU','JM','HT','DO','TT','BB','AG','DM','GD','KN','LC','VC','BS','CW','AW','SX','BQ','TC','KY','BM']),
afrinic: new Set(['ZA','NG','KE','GH','TZ','UG','ET','RW','SN','CI','CM','CD','CG','GA','AO','MZ','ZW','ZM','MW','BW','NA','SZ','LS','MG','MU','SC','DJ','ER','SO','SD','SS','TD','NE','ML','BF','GW','GN','SL','LR','TG','BJ','CF','GQ']),
};
function getRIR(cc) {
if (!cc) return 'unknown';
for (var r in rirMap) { if (rirMap[r].has(cc.toUpperCase())) return r; }
return 'unknown';
}
async function loadData() {
try {
// Fetch PeeringDB networks with IX presence (active networks)
var resp = await fetch('/api/lia/coverage');
var d = await resp.json();
if (d.error) throw new Error(d.error);
allData = d.networks || [];
renderAll();
} catch (e) {
$('mainContent').innerHTML = '<div class="loading" style="color:var(--red)">Failed to load: ' + escHtml(e.message) + '</div>';
}
}
function renderAll() {
renderSummary();
renderStats();
renderCountries();
}
function renderSummary() {
var rir_stats = { ripencc: {total:0,noProbe:0}, arin: {total:0,noProbe:0}, apnic: {total:0,noProbe:0}, lacnic: {total:0,noProbe:0}, afrinic: {total:0,noProbe:0} };
var rirLabels = { ripencc:'RIPE NCC', arin:'ARIN', apnic:'APNIC', lacnic:'LACNIC', afrinic:'AFRINIC' };
var rirColors = { ripencc:'var(--blue)', arin:'var(--green)', apnic:'var(--orange)', lacnic:'var(--pink)', afrinic:'var(--cyan)' };
allData.forEach(function(n) {
var r = getRIR(n.country);
if (rir_stats[r]) {
rir_stats[r].total++;
if (!n.has_probe) rir_stats[r].noProbe++;
}
});
var h = '';
for (var r in rir_stats) {
var s = rir_stats[r];
var covPct = s.total > 0 ? ((s.total - s.noProbe) / s.total * 100).toFixed(1) : '0';
var col = rirColors[r];
h += '<div class="summary-card">';
h += '<div class="rir-name">' + rirLabels[r] + '</div>';
h += '<div class="big-num" style="color:' + col + '">' + s.noProbe.toLocaleString() + '</div>';
h += '<div class="sub">of ' + s.total.toLocaleString() + ' networks without a probe</div>';
h += '<div class="progress-bar"><div class="progress-fill" style="width:' + covPct + '%;background:' + col + '"></div></div>';
h += '<div class="sub" style="margin-top:.25rem">' + covPct + '% coverage</div>';
h += '</div>';
}
$('summaryGrid').innerHTML = h;
}
function renderStats() {
var filtered = getFiltered();
var noProbe = filtered.filter(function(n) { return !n.has_probe; }).length;
var withProbe = filtered.length - noProbe;
var countries = new Set(filtered.map(function(n) { return n.country; })).size;
var h = '';
h += '<div class="stat-pill"><span class="num" style="color:var(--red)">' + noProbe.toLocaleString() + '</span><span class="label">Without Probe</span></div>';
h += '<div class="stat-pill"><span class="num" style="color:var(--green)">' + withProbe.toLocaleString() + '</span><span class="label">With Probe</span></div>';
h += '<div class="stat-pill"><span class="num" style="color:var(--purple)">' + filtered.length.toLocaleString() + '</span><span class="label">Total Networks</span></div>';
h += '<div class="stat-pill"><span class="num" style="color:var(--cyan)">' + countries + '</span><span class="label">Countries</span></div>';
$('statsBar').innerHTML = h;
}
function getFiltered() {
var q = ($('searchInput').value || '').toLowerCase();
return allData.filter(function(n) {
if (currentRIR !== 'all' && getRIR(n.country) !== currentRIR) return false;
if (q) {
var haystack = (n.name + ' ' + n.country + ' ' + n.country_name + ' AS' + n.asn + ' ' + (n.info_type || '')).toLowerCase();
if (haystack.indexOf(q) < 0) return false;
}
return true;
});
}
function renderCountries() {
var filtered = getFiltered();
// Group by country
var byCountry = {};
filtered.forEach(function(n) {
var cc = n.country || 'XX';
if (!byCountry[cc]) byCountry[cc] = { name: n.country_name || cc, networks: [] };
byCountry[cc].networks.push(n);
});
// Sort countries by number of networks without probes (descending)
var countries = Object.keys(byCountry).sort(function(a, b) {
var aNo = byCountry[a].networks.filter(function(n) { return !n.has_probe; }).length;
var bNo = byCountry[b].networks.filter(function(n) { return !n.has_probe; }).length;
return bNo - aNo;
});
var h = '';
countries.forEach(function(cc) {
var c = byCountry[cc];
var noProbe = c.networks.filter(function(n) { return !n.has_probe; });
var withProbe = c.networks.filter(function(n) { return n.has_probe; });
var covPct = (withProbe.length / c.networks.length * 100).toFixed(0);
var badgeClass = covPct >= 70 ? 'badge-green' : covPct >= 30 ? 'badge-orange' : 'badge-red';
var secId = 'country_' + cc;
h += '<div class="country-section">';
h += '<div class="country-header" onclick="var el=document.getElementById(\'' + secId + '\');el.classList.toggle(\'hidden\');this.querySelector(\'.arrow\').textContent=el.classList.contains(\'hidden\')?\'\u25B6\':\'\u25BC\'">';
h += '<span class="country-flag">' + flag(cc) + '</span>';
h += '<span class="country-name">' + escHtml(c.name) + ' (' + cc + ')</span>';
h += '<span class="country-count">' + noProbe.length + ' without probe / ' + c.networks.length + ' total</span>';
h += '<span class="' + badgeClass + ' country-badge">' + covPct + '% coverage</span>';
h += '<span class="arrow" style="color:var(--muted)">\u25B6</span>';
h += '</div>';
h += '<div id="' + secId + '" class="hidden">';
// Show networks WITHOUT probes first
if (noProbe.length > 0) {
h += '<div style="font-size:.7rem;color:var(--red);font-weight:600;margin:.5rem 0 .3rem;padding-left:.5rem">NO PROBE (' + noProbe.length + ')</div>';
h += '<div class="asn-grid">';
noProbe.sort(function(a,b) { return a.asn - b.asn; }).forEach(function(n) {
h += '<div class="asn-row" onclick="window.open(\'/?asn=' + n.asn + '\',\'_blank\')">';
h += '<span class="asn-num">AS' + n.asn + '</span>';
h += '<span class="asn-name">' + escHtml(n.name || '') + '</span>';
if (n.info_type) h += '<span class="asn-type">' + escHtml(n.info_type) + '</span>';
h += '<span class="no-probe">\u2718 No Probe</span>';
h += '</div>';
});
h += '</div>';
}
// Networks WITH probes
if (withProbe.length > 0) {
h += '<div style="font-size:.7rem;color:var(--green);font-weight:600;margin:.75rem 0 .3rem;padding-left:.5rem">HAS PROBE (' + withProbe.length + ')</div>';
h += '<div class="asn-grid">';
withProbe.sort(function(a,b) { return a.asn - b.asn; }).slice(0, 20).forEach(function(n) {
h += '<div class="asn-row" onclick="window.open(\'/?asn=' + n.asn + '\',\'_blank\')">';
h += '<span class="asn-num">AS' + n.asn + '</span>';
h += '<span class="asn-name">' + escHtml(n.name || '') + '</span>';
if (n.info_type) h += '<span class="asn-type">' + escHtml(n.info_type) + '</span>';
h += '<span class="has-probe">\u2714 Probe</span>';
h += '</div>';
});
if (withProbe.length > 20) h += '<div style="font-size:.75rem;color:var(--dim);padding:.3rem .5rem">+ ' + (withProbe.length - 20) + ' more with probes</div>';
h += '</div>';
}
h += '</div>';
h += '</div>';
});
if (countries.length === 0) {
h = '<div class="loading">No results found. Try a different search or RIR filter.</div>';
}
$('mainContent').innerHTML = h;
}
function switchRIR(rir, btn) {
currentRIR = rir;
document.querySelectorAll('.rir-tab').forEach(function(t) { t.classList.remove('active'); });
btn.classList.add('active');
renderStats();
renderCountries();
}
function filterResults() {
renderStats();
renderCountries();
}
function exportPDF() {
// Generate a printable version
var w = window.open('', '_blank');
var filtered = getFiltered().filter(function(n) { return !n.has_probe; });
var byCountry = {};
filtered.forEach(function(n) {
var cc = n.country || 'XX';
if (!byCountry[cc]) byCountry[cc] = { name: n.country_name || cc, networks: [] };
byCountry[cc].networks.push(n);
});
var html = '<!DOCTYPE html><html><head><title>Atlas Coverage Report — Lia\'s Paradise</title>';
html += '<style>body{font-family:Arial,sans-serif;margin:2rem;color:#1a1a2e}h1{color:#5b21b6}h2{color:#7c3aed;margin-top:1.5rem;border-bottom:2px solid #e2e8f0;padding-bottom:.3rem}table{width:100%;border-collapse:collapse;margin:.5rem 0 1rem}th,td{text-align:left;padding:.4rem .6rem;border-bottom:1px solid #e2e8f0;font-size:.85rem}th{background:#f8fafc;font-weight:600}.no-probe{color:#dc2626;font-weight:600}.meta{color:#64748b;font-size:.8rem;margin-bottom:2rem}</style></head><body>';
html += '<h1>RIPE Atlas Coverage Report</h1>';
html += '<div class="meta">Generated by Lia\'s Paradise (peercortex.org/lia) on ' + new Date().toISOString().split('T')[0] + '<br>';
html += 'Filter: ' + (currentRIR === 'all' ? 'All RIRs' : currentRIR.toUpperCase()) + ' | Networks without Atlas Probe: ' + filtered.length + '</div>';
Object.keys(byCountry).sort(function(a,b) { return byCountry[b].networks.length - byCountry[a].networks.length; }).forEach(function(cc) {
var c = byCountry[cc];
html += '<h2>' + c.name + ' (' + cc + ') — ' + c.networks.length + ' networks</h2>';
html += '<table><thead><tr><th>ASN</th><th>Name</th><th>Type</th><th>Status</th></tr></thead><tbody>';
c.networks.sort(function(a,b) { return a.asn - b.asn; }).forEach(function(n) {
html += '<tr><td>AS' + n.asn + '</td><td>' + (n.name || '') + '</td><td>' + (n.info_type || '-') + '</td><td class="no-probe">No Probe</td></tr>';
});
html += '</tbody></table>';
});
html += '<div class="meta" style="margin-top:3rem">Data from RIPE Atlas &amp; PeeringDB | peercortex.org</div></body></html>';
w.document.write(html);
w.document.close();
w.print();
}
// ============================================================
// File Upload: Parse ASNs/company names from uploaded files
// ============================================================
function handleFileUpload(input) {
var file = input.files[0];
if (!file) return;
var status = $('fileStatus');
status.textContent = 'Processing ' + file.name + '...';
status.style.color = 'var(--cyan)';
var ext = file.name.split('.').pop().toLowerCase();
if (ext === 'csv' || ext === 'txt') {
file.text().then(function(text) { processFileText(text, file.name); });
} else if (ext === 'pdf' || ext === 'doc' || ext === 'docx' || ext === 'xls' || ext === 'xlsx') {
// For binary formats, upload to server for parsing
var reader = new FileReader();
reader.onload = function(e) {
var base64 = e.target.result.split(',')[1];
fetch('/api/lia/parse-file', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: file.name, data: base64 })
}).then(function(r) { return r.json(); }).then(function(d) {
if (d.error) {
status.textContent = 'Error: ' + d.error;
status.style.color = 'var(--red)';
return;
}
processFileText(d.text || '', file.name);
}).catch(function(err) {
// Fallback: try reading as text
file.text().then(function(text) { processFileText(text, file.name); }).catch(function() {
status.textContent = 'Cannot parse ' + ext.toUpperCase() + ' files client-side. Use CSV or TXT.';
status.style.color = 'var(--red)';
});
});
};
reader.readAsDataURL(file);
} else {
status.textContent = 'Unsupported file type: ' + ext;
status.style.color = 'var(--red)';
}
}
function processFileText(text, filename) {
var status = $('fileStatus');
// Extract ASNs (AS12345 or just numbers that look like ASNs)
var asnMatches = text.match(/\bAS?(\d{3,7})\b/gi) || [];
var asns = new Set();
asnMatches.forEach(function(m) {
var num = parseInt(m.replace(/^AS/i, ''));
if (num >= 100 && num <= 9999999) asns.add(num);
});
// Also try to match company names against our loaded data
var nameMatches = [];
if (allData.length > 0) {
var lines = text.split(/[\n\r,;]+/).map(function(l) { return l.trim().toLowerCase(); }).filter(function(l) { return l.length > 2; });
lines.forEach(function(line) {
allData.forEach(function(n) {
if (n.name && n.name.toLowerCase().indexOf(line) >= 0 && line.length > 4) {
asns.add(n.asn);
}
if (line.indexOf(n.name ? n.name.toLowerCase() : '###') >= 0 && n.name && n.name.length > 4) {
asns.add(n.asn);
}
});
});
}
if (asns.size === 0) {
status.textContent = 'No ASNs or matching networks found in ' + filename;
status.style.color = 'var(--orange)';
return;
}
// Match against our data
var results = [];
var asnArr = Array.from(asns);
var dataMap = {};
allData.forEach(function(n) { dataMap[n.asn] = n; });
asnArr.forEach(function(asn) {
var n = dataMap[asn];
results.push({
asn: asn,
name: n ? n.name : '(Unknown)',
country: n ? n.country : '',
info_type: n ? n.info_type : '',
has_probe: n ? n.has_probe : false,
in_peeringdb: !!n,
});
});
results.sort(function(a, b) { return (a.has_probe ? 1 : 0) - (b.has_probe ? 1 : 0); });
var withProbe = results.filter(function(r) { return r.has_probe; }).length;
var noProbe = results.length - withProbe;
status.innerHTML = '<span style="color:var(--green)">' + withProbe + ' with probe</span> &middot; <span style="color:var(--red)">' + noProbe + ' without probe</span> &middot; ' + results.length + ' total from ' + escHtml(filename);
var h = '<div style="display:flex;gap:.5rem;margin-bottom:.75rem;flex-wrap:wrap">';
h += '<button class="export-btn" onclick="exportFileResults()" style="font-size:.7rem;padding:.3rem .8rem">Export Results as PDF</button>';
h += '</div>';
h += '<div class="asn-grid">';
results.forEach(function(r) {
h += '<div class="asn-row" onclick="window.open(\'/?asn=' + r.asn + '\',\'_blank\')">';
h += '<span class="asn-num">AS' + r.asn + '</span>';
h += '<span class="asn-name">' + escHtml(r.name) + '</span>';
if (r.country) h += '<span style="font-size:.65rem;color:var(--dim)">' + flag(r.country) + ' ' + r.country + '</span>';
if (r.info_type) h += '<span class="asn-type">' + escHtml(r.info_type) + '</span>';
h += r.has_probe ? '<span class="has-probe">\u2714 Probe</span>' : '<span class="no-probe">\u2718 No Probe</span>';
if (!r.in_peeringdb) h += '<span style="font-size:.6rem;color:var(--dim)">(not in PeeringDB)</span>';
h += '</div>';
});
h += '</div>';
$('fileResults').innerHTML = h;
$('fileResults').classList.remove('hidden');
// Store for export
window._fileResults = results;
window._fileName = filename;
}
function exportFileResults() {
if (!window._fileResults) return;
var results = window._fileResults;
var w = window.open('', '_blank');
var html = '<!DOCTYPE html><html><head><title>Atlas Probe Check — ' + escHtml(window._fileName) + '</title>';
html += '<style>body{font-family:Arial,sans-serif;margin:2rem;color:#1a1a2e}h1{color:#5b21b6;font-size:1.3rem}table{width:100%;border-collapse:collapse;margin:1rem 0}th,td{text-align:left;padding:.4rem .6rem;border-bottom:1px solid #e2e8f0;font-size:.85rem}th{background:#f8fafc;font-weight:600}.yes{color:#16a34a;font-weight:700}.no{color:#dc2626;font-weight:700}.meta{color:#64748b;font-size:.8rem}</style></head><body>';
html += '<h1>Atlas Probe Coverage Check</h1>';
html += '<div class="meta">Source: ' + escHtml(window._fileName) + ' | Generated: ' + new Date().toISOString().split('T')[0] + ' | peercortex.org/lia</div>';
html += '<table><thead><tr><th>ASN</th><th>Name</th><th>Country</th><th>Type</th><th>Atlas Probe</th></tr></thead><tbody>';
results.forEach(function(r) {
html += '<tr><td>AS' + r.asn + '</td><td>' + escHtml(r.name) + '</td><td>' + (r.country || '-') + '</td><td>' + (r.info_type || '-') + '</td>';
html += '<td class="' + (r.has_probe ? 'yes' : 'no') + '">' + (r.has_probe ? 'YES' : 'NO') + '</td></tr>';
});
html += '</tbody></table></body></html>';
w.document.write(html);
w.document.close();
w.print();
}
// Boot
loadData();
</script>
</body>
</html>

420
public/public/shell.html Normal file
View File

@ -0,0 +1,420 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PeerCortex Shell — Feedback Admin</title>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%;background:#0a0a0a;color:#00ff41;font-family:'IBM Plex Mono','Courier New',monospace;font-size:.82rem;line-height:1.7;overflow:hidden}
#shell{display:flex;flex-direction:column;height:100vh;max-width:900px;margin:0 auto;padding:0 1rem}
/* Title bar */
#titlebar{display:flex;align-items:center;gap:.5rem;padding:.5rem 0;border-bottom:1px solid rgba(0,255,65,.12);flex-shrink:0;margin-bottom:.25rem}
.tb-dot{width:11px;height:11px;border-radius:50%;display:inline-block;flex-shrink:0}
#tb-title{flex:1;text-align:center;color:rgba(0,255,65,.45);font-size:.68rem;letter-spacing:.1em}
/* Output */
#output{flex:1;overflow-y:auto;padding:.5rem 0;word-break:break-word}
#output div{padding:.05rem 0}
/* Input bar */
#inputbar{display:flex;align-items:center;gap:.5rem;padding:.5rem 0;border-top:1px solid rgba(0,255,65,.12);flex-shrink:0}
#prompt{color:rgba(0,255,65,.55);white-space:nowrap;flex-shrink:0}
#cmd{flex:1;background:transparent;border:none;outline:none;color:#00ff41;font-family:'IBM Plex Mono','Courier New',monospace;font-size:.82rem;caret-color:#00ff41}
/* Scrollbar */
#output::-webkit-scrollbar{width:4px}
#output::-webkit-scrollbar-track{background:transparent}
#output::-webkit-scrollbar-thumb{background:rgba(0,255,65,.2);border-radius:2px}
</style>
</head>
<body>
<div id="shell">
<div id="titlebar">
<span class="tb-dot" style="background:#ff5f56"></span>
<span class="tb-dot" style="background:#ffbd2e"></span>
<span class="tb-dot" style="background:#27c93f"></span>
<span id="tb-title">shell.peercortex.org — admin terminal</span>
</div>
<div id="output"></div>
<div id="inputbar">
<span id="prompt">shell:~$</span>
<input id="cmd" type="text" autocomplete="off" spellcheck="false">
</div>
</div>
<script>
var token = null;
var allEntries = [];
var G = 'color:#27c93f';
var DIM = 'color:rgba(0,255,65,.35)';
var MUT = 'color:rgba(0,255,65,.45)';
var Y = 'color:#ffbd2e';
var R = 'color:rgba(255,100,100,.8)';
var W = 'color:#fff';
// Safe DOM output — no innerHTML on user data
function print(template, fallbackColor) {
var out = document.getElementById('output');
var line = document.createElement('div');
if (fallbackColor) line.style.color = fallbackColor;
var parts = template.split(/(<span[^>]*>[^<]*<\/span>)/g);
parts.forEach(function(part) {
var m = part.match(/^<span([^>]*)>(.*?)<\/span>$/);
if (m) {
var sp = document.createElement('span');
var sm = m[1].match(/style="([^"]*)"/);
if (sm) sp.style.cssText = sm[1];
sp.textContent = m[2];
line.appendChild(sp);
} else if (part) {
line.appendChild(document.createTextNode(part));
}
});
out.appendChild(line);
out.scrollTop = out.scrollHeight;
}
function printText(text, color) {
var out = document.getElementById('output');
var line = document.createElement('div');
line.textContent = text;
if (color) line.style.color = color;
out.appendChild(line);
out.scrollTop = out.scrollHeight;
}
function blank() { print(''); }
function clear() { document.getElementById('output').textContent = ''; }
function setPrompt(p) { document.getElementById('prompt').textContent = p; }
// Boot sequence
function boot() {
clear();
token = null;
allEntries = [];
setPrompt('shell:~$');
var lines = [
'<span style="' + DIM + '">══════════════════════════════════════════════════════════</span>',
'',
' <span style="' + G + ';font-weight:600">PeerCortex — Feedback Administration Shell</span>',
' <span style="' + MUT + '">Unauthorized access prohibited. All sessions logged.</span>',
'',
'<span style="' + DIM + '">══════════════════════════════════════════════════════════</span>',
'',
'Type <span style="' + G + '">login</span> to authenticate.',
''
];
var d = 0;
lines.forEach(function(l){ setTimeout(function(){ print(l); }, d); d += 40; });
}
// Command dispatch
function dispatch(raw) {
var val = raw.trim();
var low = val.toLowerCase();
var args = val.split(/\s+/);
var cmd = args[0].toLowerCase();
// Echo input safely
(function(){
var out = document.getElementById('output');
var d = document.createElement('div');
var sp = document.createElement('span');
sp.style.color = 'rgba(0,255,65,.35)';
sp.textContent = document.getElementById('prompt').textContent;
d.appendChild(sp);
d.appendChild(document.createTextNode(' ' + val));
out.appendChild(d);
out.scrollTop = out.scrollHeight;
})();
if (!token) {
// Unauthenticated mode
if (cmd === 'login') {
doLogin();
} else if (cmd === 'help') {
showHelp(false);
} else if (cmd === 'clear') {
clear();
} else if (val === '') {
// noop
} else {
printText('Permission denied. Type login first.', 'rgba(255,100,100,.75)');
}
return;
}
// Authenticated mode
if (cmd === 'list') {
var cat = args[1] || null;
doList(cat);
} else if (cmd === 'show') {
var idx = parseInt(args[1], 10);
doShow(idx);
} else if (cmd === 'stats') {
doStats();
} else if (cmd === 'export') {
doExport();
} else if (cmd === 'refresh') {
doFetch(function(){ print('<span style="' + G + '">✓ Data refreshed. ' + allEntries.length + ' entries loaded.</span>'); });
} else if (cmd === 'clear') {
clear();
} else if (cmd === 'logout') {
token = null; allEntries = [];
setPrompt('shell:~$');
blank();
printText('Session terminated.', MUT.slice(6));
blank();
printText('Type login to re-authenticate.');
} else if (cmd === 'help') {
showHelp(true);
} else if (val === '') {
// noop
} else {
printText('shell: ' + val + ': command not found', 'rgba(255,100,100,.75)');
}
}
// Login flow
var loginState = 0;
function doLogin() {
loginState = 1;
setPrompt('token:');
blank();
printText('Enter FEEDBACK_TOKEN:');
document.getElementById('cmd').type = 'password';
}
function handleLogin(val) {
document.getElementById('cmd').type = 'text';
loginState = 0;
setPrompt('shell:~$');
blank();
// Verify token
fetch('/api/feedback?token=' + encodeURIComponent(val))
.then(function(r){ return r.json(); })
.then(function(d){
if (d.ok) {
token = val;
allEntries = d.entries || [];
setPrompt('root@peercortex:~#');
print('<span style="' + G + '">✓ Authenticated. ' + allEntries.length + ' feedback entries loaded.</span>');
blank();
showHelp(true);
} else {
printText('✗ Authentication failed. Wrong token.', 'rgba(255,100,100,.8)');
blank();
}
}).catch(function(){
printText('Network error during authentication.', 'rgba(255,100,100,.8)');
});
}
// Fetch fresh data from server
function doFetch(cb) {
fetch('/api/feedback?token=' + encodeURIComponent(token))
.then(function(r){ return r.json(); })
.then(function(d){
if (d.ok) { allEntries = d.entries || []; if (cb) cb(); }
else { printText('Fetch error: ' + (d.error || 'unknown'), 'rgba(255,100,100,.8)'); }
}).catch(function(){ printText('Network error.', 'rgba(255,100,100,.8)'); });
}
// list [category]
function doList(filterCat) {
var data = allEntries;
if (filterCat) {
var lf = filterCat.toLowerCase();
data = data.filter(function(e){ return e.category && e.category.toLowerCase().indexOf(lf) >= 0; });
}
blank();
if (data.length === 0) {
printText('No entries' + (filterCat ? ' matching "' + filterCat + '"' : '') + '.', MUT.slice(6));
blank();
return;
}
print('<span style="' + DIM + '">── # DATE CATEGORY NAME ASN ──</span>');
data.forEach(function(e, i) {
var realIdx = allEntries.indexOf(e);
var date = e.timestamp ? e.timestamp.slice(0,10) : '?';
var cat = (e.category || 'General').slice(0,18).padEnd(18);
var name = (e.name || 'Anonymous').slice(0,16).padEnd(16);
var asn = e.asn ? ('AS' + e.asn) : '—';
var num = String(realIdx + 1).padStart(3);
(function(entry, n){
var out = document.getElementById('output');
var line = document.createElement('div');
var snr = document.createElement('span');
snr.style.color = 'rgba(0,255,65,.4)';
snr.textContent = num + ' ';
var sdat = document.createElement('span');
sdat.style.color = '#ffbd2e';
sdat.textContent = date + ' ';
var scat = document.createElement('span');
scat.style.color = '#00ff41';
scat.textContent = cat + ' ';
var snm = document.createElement('span');
snm.style.color = 'rgba(0,255,65,.7)';
snm.textContent = name + ' ';
var sasn = document.createElement('span');
sasn.style.color = 'rgba(0,255,65,.45)';
sasn.textContent = asn;
line.appendChild(snr); line.appendChild(sdat); line.appendChild(scat);
line.appendChild(snm); line.appendChild(sasn);
out.appendChild(line);
out.scrollTop = out.scrollHeight;
})(e, realIdx);
});
print('<span style="' + DIM + '">──────────────────────────────────────────────────────────────</span>');
var out2 = document.getElementById('output');
var tot = document.createElement('div');
tot.style.color = 'rgba(0,255,65,.45)';
tot.textContent = data.length + ' entr' + (data.length === 1 ? 'y' : 'ies') + (filterCat ? ' (filtered)' : '') + ' — type show <n> for full message';
out2.appendChild(tot);
out2.scrollTop = out2.scrollHeight;
blank();
}
// show <n>
function doShow(n) {
if (isNaN(n) || n < 1 || n > allEntries.length) {
printText('Usage: show <number> (1' + allEntries.length + ')', 'rgba(255,189,46,.8)');
return;
}
var e = allEntries[n - 1];
blank();
print('<span style="' + DIM + '">────────────────────────────────────────────────────────────</span>');
(function(){
var fields = [
['ID', e.id || '?'],
['Timestamp', e.timestamp || '?'],
['Category', e.category || 'General'],
['Name', e.name || 'Anonymous'],
['ASN', e.asn ? 'AS' + e.asn : '—'],
['IP', e.ip || '—'],
];
fields.forEach(function(f){
var out = document.getElementById('output');
var d = document.createElement('div');
var lbl = document.createElement('span');
lbl.style.color = 'rgba(0,255,65,.45)';
lbl.textContent = f[0].padEnd(12) + ' ';
var val = document.createElement('span');
val.style.color = '#00ff41';
val.textContent = f[1];
d.appendChild(lbl); d.appendChild(val);
out.appendChild(d);
out.scrollTop = out.scrollHeight;
});
})();
blank();
printText('Message:', 'rgba(0,255,65,.45)');
printText(e.message || '(empty)', '#fff');
blank();
print('<span style="' + DIM + '">────────────────────────────────────────────────────────────</span>');
blank();
}
// stats
function doStats() {
if (allEntries.length === 0) { printText('No feedback entries yet.', MUT.slice(6)); return; }
var cats = {};
var asns = {};
allEntries.forEach(function(e){
var c = e.category || 'General';
cats[c] = (cats[c] || 0) + 1;
if (e.asn) { asns[e.asn] = (asns[e.asn] || 0) + 1; }
});
blank();
print('<span style="' + G + ';font-weight:600">─── Feedback Statistics ───────────────────</span>');
var out = document.getElementById('output');
var tot = document.createElement('div');
tot.style.color = '#ffbd2e';
tot.textContent = 'Total entries: ' + allEntries.length;
out.appendChild(tot);
blank();
printText('By category:', 'rgba(0,255,65,.45)');
Object.keys(cats).sort(function(a,b){ return cats[b]-cats[a]; }).forEach(function(c){
var d2 = document.createElement('div');
var bar = '█'.repeat(Math.round(cats[c] / allEntries.length * 20));
var sp1 = document.createElement('span');
sp1.style.color = 'rgba(0,255,65,.4)';
sp1.textContent = ' ' + c.padEnd(20);
var sp2 = document.createElement('span');
sp2.style.color = '#27c93f';
sp2.textContent = bar + ' ' + cats[c];
d2.appendChild(sp1); d2.appendChild(sp2);
out.appendChild(d2);
});
var topAsn = Object.keys(asns);
if (topAsn.length > 0) {
blank();
printText('Top ASNs reported:', 'rgba(0,255,65,.45)');
topAsn.sort(function(a,b){ return asns[b]-asns[a]; }).slice(0,5).forEach(function(a){
var d3 = document.createElement('div');
d3.style.color = 'rgba(0,255,65,.7)';
d3.textContent = ' AS' + a + ' — ' + asns[a] + ' report' + (asns[a]===1?'':'s');
out.appendChild(d3);
});
}
out.scrollTop = out.scrollHeight;
blank();
}
// export — trigger JSON file download
function doExport() {
var blob = new Blob([JSON.stringify(allEntries, null, 2)], {type:'application/json'});
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'peercortex-feedback-' + new Date().toISOString().slice(0,10) + '.json';
a.click();
URL.revokeObjectURL(url);
print('<span style="' + G + '">✓ Export downloaded: ' + allEntries.length + ' entries.</span>');
blank();
}
// help
function showHelp(authed) {
blank();
print('<span style="' + G + ';font-weight:600">Available commands:</span>');
if (!authed) {
print(' <span style="' + G + '">login</span> — authenticate with FEEDBACK_TOKEN');
print(' <span style="' + G + '">clear</span> — clear screen');
} else {
print(' <span style="' + G + '">list</span> — show all feedback entries');
print(' <span style="' + G + '">list [category]</span> — filter by category (bug, feature, design, general)');
print(' <span style="' + G + '">show [n]</span> — show full message for entry #n');
print(' <span style="' + G + '">stats</span> — category + ASN statistics');
print(' <span style="' + G + '">export</span> — download all entries as JSON');
print(' <span style="' + G + '">refresh</span> — reload entries from server');
print(' <span style="' + G + '">clear</span> — clear screen');
print(' <span style="' + G + '">logout</span> — end session');
}
blank();
}
// Input handler
document.getElementById('cmd').addEventListener('keydown', function(e){
if (e.key !== 'Enter') return;
var val = this.value;
this.value = '';
if (loginState === 1) {
handleLogin(val.trim());
} else {
dispatch(val);
}
});
// Start
boot();
</script>
</body>
</html>

11
scripts/refresh-peeringdb.sh Executable file
View File

@ -0,0 +1,11 @@
#!/bin/bash
# Refresh PeeringDB SQLite mirror daily
LOG=/var/log/peercortex/peeringdb-refresh.log
mkdir -p /var/log/peercortex
exec >> $LOG 2>&1
echo "[$(date +%Y-%m-%dT%H:%M:%S)] Starting PeeringDB sync..."
cd /opt/peeringdb-data
peeringdb sync -q
echo "[$(date +%Y-%m-%dT%H:%M:%S)] Sync complete. File: $(du -sh peeringdb.sqlite3 | cut -f1)"

29
scripts/tunnel-cleanup.sh Executable file
View File

@ -0,0 +1,29 @@
#!/bin/bash
# Auto-heal Cloudflare tunnel: clean stale connectors from foreign IPs
TUNNEL=3262c64b-51d5-479f-ad26-a9925b705bd3
LOG=/var/log/peercortex/tunnel-cleanup.log
mkdir -p /var/log/peercortex
# Restart cloudflared if not running
if ! systemctl is-active --quiet cloudflared; then
echo "[$(date +%Y-%m-%dT%H:%M:%S)] cloudflared not running — starting" >> "$LOG"
systemctl start cloudflared
sleep 5
fi
MY_IP=$(curl -s --max-time 5 https://ifconfig.me 2>/dev/null)
[ -z "$MY_IP" ] && exit 0
# Connector rows start with a UUID (36 chars: 8-4-4-4-12)
# Column 5 in connector rows = ORIGIN IP
STALE=$(cloudflared tunnel info "$TUNNEL" 2>/dev/null | \
grep -E '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | \
awk -v myip="$MY_IP" '$5 != myip {print $5}')
if [ -n "$STALE" ]; then
echo "[$(date +%Y-%m-%dT%H:%M:%S)] Stale connectors from $STALE — cleaning" >> "$LOG"
cloudflared tunnel cleanup "$TUNNEL" >> "$LOG" 2>&1
systemctl restart cloudflared
echo "[$(date +%Y-%m-%dT%H:%M:%S)] Done" >> "$LOG"
fi

1667
server.js

File diff suppressed because it is too large Load Diff

1
webhook-subs.json Normal file
View File

@ -0,0 +1 @@
[]