PeerCortex/backups/index.html.20260327_133135

2773 lines
166 KiB
Plaintext
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PeerCortex - Network Intelligence Dashboard</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{
--bg:#0f0f1a;--card:#1a1b26;--card-hover:#1f2030;--border:#2a2b3d;--border-light:#363750;
--purple:#bb9af7;--blue:#7aa2f7;--green:#9ece6a;--orange:#ff9e64;--red:#f7768e;
--cyan:#7dcfff;--yellow:#e0af68;--white:#c0caf5;--muted:#565f89;--dim:#414868;
--text:#c0caf5;--text-dim:#a9b1d6;
}
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;min-height:100vh}
a{color:var(--blue);text-decoration:none;transition:color .2s}a:hover{color:var(--cyan)}
.header{background:linear-gradient(180deg,#1a1b2e 0%,var(--bg) 100%);border-bottom:1px solid var(--border);padding:1.5rem 0}
.header-inner{max-width:1200px;margin:0 auto;padding:0 1.5rem;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:1rem}
.logo{display:flex;align-items:center;gap:.75rem}
.logo svg{width:36px;height:36px}
.logo h1{font-size:1.4rem;font-weight:700;color:var(--purple);letter-spacing:-.02em}
.logo span{font-size:.75rem;color:var(--muted);font-weight:400}
.quick-links{display:flex;gap:.75rem;flex-wrap:wrap}
.quick-links a{font-size:.75rem;padding:.35rem .7rem;border:1px solid var(--border);border-radius:6px;color:var(--muted);transition:all .2s}
.quick-links a:hover{border-color:var(--blue);color:var(--blue)}
.search-section{max-width:1200px;margin:2rem auto;padding:0 1.5rem}
.search-box{display:flex;gap:.75rem;align-items:stretch}
.search-input{flex:1;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:.85rem 1.2rem;font-size:1rem;color:var(--text);font-family:inherit;outline:none;transition:border-color .2s}
.search-input:focus{border-color:var(--purple)}
.search-input::placeholder{color:var(--dim)}
.search-btn{background:linear-gradient(135deg,var(--purple),var(--blue));border:none;border-radius:10px;padding:.85rem 2rem;font-size:1rem;font-weight:600;color:#fff;cursor:pointer;font-family:inherit;transition:opacity .2s;white-space:nowrap}
.search-btn:hover{opacity:.9}
.search-btn:disabled{opacity:.5;cursor:not-allowed}
.dashboard{max-width:1200px;margin:0 auto;padding:0 1.5rem 3rem;display:grid;grid-template-columns:1fr 1fr;gap:1.25rem}
@media(max-width:768px){.dashboard{grid-template-columns:1fr}}
.card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:1.5rem;transition:border-color .2s}
.card:hover{border-color:var(--border-light)}
.card.full{grid-column:1/-1}
.card-title{font-size:.85rem;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--purple);margin-bottom:1rem;display:flex;align-items:center;gap:.5rem}
.card-title svg{width:18px;height:18px;opacity:.7}
.stat-row{display:flex;gap:1.5rem;flex-wrap:wrap;margin-bottom:1rem}
.stat{text-align:center}
.stat-val{font-size:1.8rem;font-weight:700;color:var(--green);line-height:1.2}
.stat-val.blue{color:var(--blue)}.stat-val.purple{color:var(--purple)}.stat-val.orange{color:var(--orange)}.stat-val.red{color:var(--red)}.stat-val.cyan{color:var(--cyan)}
.stat-label{font-size:.7rem;color:var(--muted);text-transform:uppercase;letter-spacing:.05em}
.badge{display:inline-block;padding:.2rem .6rem;border-radius:5px;font-size:.7rem;font-weight:600;margin-right:.4rem;margin-bottom:.3rem}
.badge-purple{background:rgba(187,154,247,.15);color:var(--purple)}
.badge-blue{background:rgba(122,162,247,.15);color:var(--blue)}
.badge-green{background:rgba(158,206,106,.15);color:var(--green)}
.badge-orange{background:rgba(255,158,100,.15);color:var(--orange)}
.badge-red{background:rgba(247,118,142,.15);color:var(--red)}
.badge-cyan{background:rgba(125,207,255,.15);color:var(--cyan)}
.progress-wrap{height:8px;background:var(--border);border-radius:4px;overflow:hidden;margin:.5rem 0}
.progress-bar{height:100%;border-radius:4px;transition:width .5s ease}
.progress-bar.green{background:var(--green)}.progress-bar.red{background:var(--red)}.progress-bar.orange{background:var(--orange)}.progress-bar.blue{background:var(--blue)}
.progress-multi{display:flex;height:8px;border-radius:4px;overflow:hidden;margin:.5rem 0;background:var(--border)}
.progress-multi>div{height:100%;transition:width .5s ease}
.tbl{width:100%;border-collapse:collapse;font-size:.8rem}
.tbl th{text-align:left;padding:.5rem .6rem;color:var(--muted);font-weight:600;font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--border)}
.tbl td{padding:.5rem .6rem;border-bottom:1px solid rgba(42,43,61,.5);color:var(--text-dim)}
.tbl tr:hover td{background:rgba(187,154,247,.03)}
.tbl .asn-link{color:var(--blue);cursor:pointer;font-weight:500}
.tbl .asn-link:hover{color:var(--cyan);text-decoration:underline}
.rpki-valid{color:var(--green)}.rpki-invalid{color:var(--red)}.rpki-unknown{color:var(--muted)}
.big-score{font-size:4rem;font-weight:800;line-height:1;margin:.5rem 0}
.big-score.high{color:var(--green)}.big-score.mid{color:var(--orange)}.big-score.low{color:var(--red)}
.ext-links{display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.75rem}
.ext-link{font-size:.75rem;padding:.3rem .65rem;border:1px solid var(--border);border-radius:6px;color:var(--text-dim);transition:all .2s}
.ext-link:hover{border-color:var(--blue);color:var(--blue)}
.net-name{font-size:1.6rem;font-weight:700;color:var(--white);margin-bottom:.25rem}
.net-aka{font-size:.9rem;color:var(--muted);margin-bottom:.75rem}
.compare-section{max-width:1200px;margin:0 auto;padding:0 1.5rem 1rem}
.compare-box{display:flex;gap:.75rem;align-items:stretch;flex-wrap:wrap}
.compare-input{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:.65rem 1rem;font-size:.9rem;color:var(--text);font-family:inherit;outline:none;width:160px;transition:border-color .2s}
.compare-input:focus{border-color:var(--purple)}
.compare-btn{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:.65rem 1.5rem;font-size:.85rem;font-weight:600;color:var(--purple);cursor:pointer;font-family:inherit;transition:all .2s}
.compare-btn:hover{border-color:var(--purple);background:rgba(187,154,247,.1)}
.expand-toggle{font-size:.75rem;color:var(--blue);cursor:pointer;margin-top:.5rem;display:inline-block}
.expand-toggle:hover{text-decoration:underline}
.expand-body{display:none;margin-top:.5rem}
.expand-body.open{display:block}
.skeleton{background:linear-gradient(90deg,var(--border) 25%,var(--border-light) 50%,var(--border) 75%);background-size:200% 100%;animation:shimmer 1.5s infinite;border-radius:6px;height:1rem;margin:.4rem 0}
@keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
.skeleton.h2{height:2rem;width:60%}.skeleton.h3{height:1.2rem;width:40%}.skeleton.wide{width:100%}.skeleton.med{width:70%}
.meta-bar{text-align:center;font-size:.75rem;color:var(--dim);margin-top:1rem;padding:0 1.5rem;display:flex;align-items:center;justify-content:center;gap:.75rem;flex-wrap:wrap}
.dq-badge{display:inline-flex;align-items:center;gap:.35rem;padding:.25rem .65rem;border-radius:6px;font-size:.7rem;font-weight:600;cursor:help;position:relative;transition:all .2s}
.dq-badge.high{background:rgba(158,206,106,.15);color:var(--green);border:1px solid rgba(158,206,106,.3)}
.dq-badge.medium{background:rgba(255,158,100,.15);color:var(--orange);border:1px solid rgba(255,158,100,.3)}
.dq-badge.low{background:rgba(247,118,142,.15);color:var(--red);border:1px solid rgba(247,118,142,.3)}
.dq-badge svg{width:12px;height:12px}
.dq-tooltip{display:none;position:absolute;bottom:calc(100% + 8px);left:50%;transform:translateX(-50%);background:var(--card);border:1px solid var(--border-light);border-radius:8px;padding:.75rem;font-size:.7rem;font-weight:400;color:var(--text-dim);min-width:280px;max-width:360px;z-index:100;box-shadow:0 4px 12px rgba(0,0,0,.3);text-align:left;line-height:1.5}
.dq-badge:hover .dq-tooltip{display:block}
.dq-tooltip-row{display:flex;justify-content:space-between;padding:.2rem 0;border-bottom:1px solid rgba(42,43,61,.5)}
.dq-tooltip-row:last-child{border-bottom:none}
.dq-tooltip-label{color:var(--muted)}
.dq-tooltip-value{font-weight:600}
.dq-tooltip-value.agree{color:var(--green)}.dq-tooltip-value.warn{color:var(--orange)}.dq-tooltip-value.bad{color:var(--red)}.dq-tooltip-value.na{color:var(--dim)}
.footer{text-align:center;padding:2rem 1.5rem;color:var(--dim);font-size:.75rem;border-top:1px solid var(--border);margin-top:2rem}
.footer a{color:var(--muted)}
.flag{font-size:1.2rem;margin-right:.3rem}
.hidden{display:none !important}
.scroll-wrap{max-height:300px;overflow-y:auto}
.scroll-wrap::-webkit-scrollbar{width:6px}
.scroll-wrap::-webkit-scrollbar-track{background:transparent}
.scroll-wrap::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
.compare-results{max-width:1200px;margin:0 auto;padding:0 1.5rem 1rem}
/* ASPA specific */
.aspa-template{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:1rem;font-family:'Courier New',monospace;font-size:.75rem;color:var(--cyan);white-space:pre-wrap;word-break:break-all;position:relative;margin:.5rem 0}
.copy-btn{position:absolute;top:.5rem;right:.5rem;background:var(--card);border:1px solid var(--border);border-radius:6px;padding:.3rem .6rem;font-size:.7rem;color:var(--muted);cursor:pointer;transition:all .2s}
.copy-btn:hover{border-color:var(--purple);color:var(--purple)}
/* Status indicator */
.status-yes{color:var(--green);font-weight:600}
.status-no{color:var(--red);font-weight:600}
.status-unknown{color:var(--muted);font-weight:600}
/* Loading spinner for sections */
.section-loading{text-align:center;padding:1rem;color:var(--muted);font-size:.8rem}
.section-loading::before{content:'';display:inline-block;width:16px;height:16px;border:2px solid var(--border);border-top-color:var(--purple);border-radius:50%;animation:spin .8s linear infinite;margin-right:.5rem;vertical-align:middle}
@keyframes spin{to{transform:rotate(360deg)}}
/* ASPA Deep Analysis */
.aspa-gauge{position:relative;width:140px;height:140px;margin:0 auto .5rem}
.aspa-gauge svg{width:100%;height:100%;transform:rotate(-90deg)}
.aspa-gauge-bg{fill:none;stroke:var(--border);stroke-width:10}
.aspa-gauge-fill{fill:none;stroke-width:10;stroke-linecap:round;transition:stroke-dashoffset .8s ease,stroke .3s}
.aspa-gauge-text{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center}
.aspa-gauge-score{font-size:2.2rem;font-weight:800;line-height:1}
.aspa-gauge-label{font-size:.65rem;color:var(--muted);text-transform:uppercase;letter-spacing:.05em}
.aspa-breakdown{display:grid;grid-template-columns:1fr 1fr;gap:.75rem;margin:1rem 0}
.aspa-breakdown-item{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:.75rem}
.aspa-breakdown-label{font-size:.7rem;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;margin-bottom:.25rem}
.aspa-breakdown-score{font-size:1.2rem;font-weight:700}
.aspa-breakdown-bar{height:4px;background:var(--border);border-radius:2px;margin-top:.35rem;overflow:hidden}
.aspa-breakdown-bar>div{height:100%;border-radius:2px;transition:width .5s ease}
.valley-alert{background:rgba(247,118,142,.1);border:1px solid rgba(247,118,142,.3);border-radius:8px;padding:.75rem;margin:.5rem 0;font-size:.8rem;color:var(--red)}
.asset-alert{background:rgba(255,158,100,.1);border:1px solid rgba(255,158,100,.3);border-radius:8px;padding:.75rem;margin:.5rem 0;font-size:.8rem;color:var(--orange)}
.path-result-badge{display:inline-block;padding:.15rem .5rem;border-radius:4px;font-size:.7rem;font-weight:600}
.path-valid{background:rgba(158,206,106,.15);color:var(--green)}
.path-invalid{background:rgba(247,118,142,.15);color:var(--red)}
.path-unknown{background:rgba(86,95,137,.2);color:var(--muted)}
.hop-detail{font-size:.7rem;color:var(--text-dim);margin-top:.3rem}
.hop-arrow{color:var(--dim);margin:0 .15rem}
.hop-pp{color:var(--green)}.hop-npp{color:var(--red)}.hop-na{color:var(--muted)}
.audit-row{display:flex;align-items:center;gap:.5rem;padding:.35rem 0;border-bottom:1px solid rgba(42,43,61,.3);font-size:.8rem}
.audit-missing{color:var(--orange)}.audit-extra{color:var(--cyan)}.audit-ok{color:var(--green)}
.ix-traffic-stats{display:flex;gap:1rem;flex-wrap:wrap;margin-top:.75rem;padding:.75rem;background:var(--bg);border:1px solid var(--border);border-radius:8px}
.ix-traffic-stat{text-align:center}
.ix-traffic-val{font-size:1.1rem;font-weight:700;color:var(--cyan)}
.ix-traffic-label{font-size:.65rem;color:var(--muted);text-transform:uppercase}
.whois-grid{display:grid;grid-template-columns:140px 1fr;gap:.3rem .75rem;font-size:.8rem}
.whois-key{color:var(--muted);font-weight:600;text-align:right}
.whois-val{color:var(--text-dim);word-break:break-all}
/* Search history badges */
.history-badge{display:inline-block;padding:.25rem .6rem;border-radius:6px;font-size:.75rem;font-weight:500;background:var(--card);border:1px solid var(--border);color:var(--text-dim);cursor:pointer;transition:all .2s}
.history-badge:hover{border-color:var(--purple);color:var(--purple)}
.history-clear{font-size:.7rem;color:var(--dim);cursor:pointer;padding:.25rem .5rem}
.history-clear:hover{color:var(--red)}
/* Prefix detail modal */
.modal-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.6);z-index:1000;display:flex;align-items:center;justify-content:center}
.modal-content{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:1.5rem;max-width:600px;width:90%;max-height:80vh;overflow-y:auto;position:relative}
.modal-close{position:absolute;top:1rem;right:1rem;background:none;border:none;color:var(--muted);font-size:1.2rem;cursor:pointer;padding:.3rem}
.modal-close:hover{color:var(--red)}
.modal-title{font-size:1rem;font-weight:700;color:var(--purple);margin-bottom:1rem}
/* Clickable prefix */
.prefix-link{color:var(--cyan);cursor:pointer;font-family:monospace;font-size:.8rem}
.prefix-link:hover{text-decoration:underline;color:var(--blue)}
/* Clickable IX */
.ix-link{color:var(--green);cursor:pointer}
.ix-link:hover{text-decoration:underline;color:var(--cyan)}
/* Provider graph */
.provider-graph{width:100%;max-width:600px;margin:0 auto}
.provider-graph svg{width:100%;height:auto}
/* Compare full panel */
.compare-grid{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem}
.compare-col{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:1rem}
.compare-col-title{font-size:.85rem;font-weight:600;margin-bottom:.75rem;display:flex;align-items:center;gap:.5rem}
.compare-metric{display:flex;justify-content:space-between;padding:.4rem 0;border-bottom:1px solid rgba(42,43,61,.3);font-size:.8rem}
.compare-metric-label{color:var(--muted)}
.compare-metric-val{font-weight:600}
.compare-venn{text-align:center;margin:1rem 0}
/* Network Health Report */
.health-gauge{position:relative;width:160px;height:160px;margin:0 auto .5rem}
.health-gauge svg{width:100%;height:100%;transform:rotate(-90deg)}
.health-gauge-bg{fill:none;stroke:var(--border);stroke-width:12}
.health-gauge-fill{fill:none;stroke-width:12;stroke-linecap:round;transition:stroke-dashoffset .8s ease,stroke .3s}
.health-gauge-text{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center}
.health-gauge-score{font-size:2.8rem;font-weight:800;line-height:1}
.health-gauge-label{font-size:.7rem;color:var(--muted);text-transform:uppercase;letter-spacing:.05em}
.health-checks{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:.5rem;margin:1rem 0}
.health-check-item{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;background:var(--bg);border:1px solid var(--border);border-radius:8px;font-size:.8rem;transition:border-color .2s}
.health-check-item:hover{border-color:var(--border-light)}
.health-check-icon{font-size:1rem;flex-shrink:0}
.health-check-name{flex:1;color:var(--text-dim)}
.health-check-score{font-size:.75rem;font-weight:600;min-width:2rem;text-align:right}
.health-check-item{position:relative;cursor:pointer}
.health-tooltip{display:none;position:absolute;bottom:calc(100% + 8px);left:50%;transform:translateX(-50%);background:#1a1b26;border:1px solid #3b3d56;border-radius:8px;padding:12px 16px;min-width:300px;max-width:400px;z-index:1000;box-shadow:0 4px 20px rgba(0,0,0,0.5);font-size:13px;line-height:1.6;color:#a9b1d6;pointer-events:none}
.health-tooltip::after{content:'';position:absolute;top:100%;left:50%;transform:translateX(-50%);border:6px solid transparent;border-top-color:#3b3d56}
.health-tooltip .tt-section{margin-bottom:6px}
.health-tooltip .tt-label{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#565f89;font-weight:600}
.health-tooltip .tt-value{color:#c0caf5}
.health-tooltip .tt-fix{color:#ff9e64;font-style:italic}
.health-check-item:hover .health-tooltip{display:block}
.show-more-btn{font-size:.8rem;color:var(--blue);cursor:pointer;padding:.5rem 0;margin-top:.25rem;transition:color .2s;user-select:none}
.show-more-btn:hover{color:var(--cyan);text-decoration:underline}
.sort-toggle{font-size:.7rem;color:var(--muted);cursor:pointer;padding:.2rem .5rem;border:1px solid var(--border);border-radius:4px;transition:all .2s;user-select:none}
.sort-toggle:hover{color:var(--blue);border-color:var(--blue)}
/* Routing Overview - Propagation Bars */
.routing-stats-row{display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:1.25rem}
.routing-stat-card{flex:1;min-width:100px;background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:.875rem 1rem;text-align:center}
.routing-stat-val{font-size:1.6rem;font-weight:700;line-height:1.2}
.routing-stat-label{font-size:.7rem;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-top:.25rem}
.prop-section{margin-bottom:1rem}
.prop-label{font-size:.8rem;font-weight:600;color:var(--text-dim);margin-bottom:.4rem}
.prop-bar-wrap{display:flex;align-items:center;gap:.75rem}
.prop-bar{flex:1;height:12px;border-radius:6px;background:#1e2030;overflow:hidden}
.prop-fill{height:100%;border-radius:6px;transition:width 1.2s cubic-bezier(.4,0,.2,1);width:0}
.prop-fill.green{background:linear-gradient(90deg,#9ece6a,#73daca)}
.prop-fill.orange{background:linear-gradient(90deg,#e0af68,#ff9e64)}
.prop-fill.red{background:linear-gradient(90deg,#f7768e,#db4b4b)}
.prop-pct{font-size:.9rem;font-weight:700;min-width:50px;text-align:right}
.prop-detail{font-size:.7rem;color:var(--muted);margin-top:.2rem}
.prefix-dist{margin-top:1rem}
.prefix-dist-label{font-size:.8rem;font-weight:600;color:var(--text-dim);margin-bottom:.5rem}
.prefix-badges{display:flex;flex-wrap:wrap;gap:.4rem}
.prefix-badge{font-size:.7rem;padding:.2rem .5rem;border-radius:4px;background:var(--bg);border:1px solid var(--border);color:var(--text-dim);font-family:'Inter',monospace}
.routing-footer{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:.75rem;margin-top:1rem;padding-top:.75rem;border-top:1px solid var(--border)}
.routing-footer-left{display:flex;align-items:center;gap:.5rem;font-size:.8rem;color:var(--text-dim)}
</style>
</head>
<body>
<!-- Header -->
<header class="header">
<div class="header-inner">
<div class="logo">
<svg viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="18" cy="18" r="16" stroke="#bb9af7" stroke-width="2"/>
<circle cx="18" cy="10" r="3" fill="#bb9af7"/>
<circle cx="10" cy="24" r="3" fill="#7aa2f7"/>
<circle cx="26" cy="24" r="3" fill="#9ece6a"/>
<line x1="18" y1="13" x2="11" y2="22" stroke="#565f89" stroke-width="1.5"/>
<line x1="18" y1="13" x2="25" y2="22" stroke="#565f89" stroke-width="1.5"/>
<line x1="13" y1="24" x2="23" y2="24" stroke="#565f89" stroke-width="1.5"/>
</svg>
<div><h1>PeerCortex <span style="font-size:.45em;background:linear-gradient(135deg,#f59e0b,#ef4444);color:#fff;padding:2px 10px;border-radius:4px;vertical-align:middle;letter-spacing:.08em;font-weight:700">BETA</span></h1><span>Network Intelligence Dashboard v0.5.0-beta</span></div>
</div>
<nav class="quick-links">
<a href="https://github.com/peercortex/peercortex" target="_blank">GitHub</a>
<a href="https://www.peeringdb.com" target="_blank">PeeringDB</a>
<a href="https://stat.ripe.net" target="_blank">RIPE Stat</a>
<a href="https://bgp.he.net" target="_blank">bgp.he.net</a>
<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>
</nav>
</div>
</header>
<!-- Search -->
<section class="search-section">
<div class="search-box">
<input type="text" class="search-input" id="asnInput" placeholder="Enter ASN (e.g. 13335, 6939, 174)" value="" autofocus>
<button class="search-btn" id="searchBtn" onclick="doLookup()">Lookup</button>
</div>
<div id="searchHistory" style="margin-top:.75rem;display:flex;flex-wrap:wrap;gap:.4rem"></div>
</section>
<!-- Meta bar -->
<div class="meta-bar" id="metaBar"></div>
<!-- Dashboard -->
<div class="dashboard hidden" id="dashboard">
<!-- Network Overview -->
<div class="card full" id="overviewCard">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
Network Overview
</div>
<div id="overviewContent"></div>
</div>
<!-- Prefixes -->
<div class="card" id="prefixCard">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16v16H4z"/><path d="M4 12h16M12 4v16"/></svg>
Announced Prefixes
</div>
<div id="prefixContent"></div>
</div>
<!-- RPKI Compliance -->
<div class="card" id="rpkiCard">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
RPKI Compliance
</div>
<div id="rpkiContent"></div>
</div>
<!-- Atlas Probes -->
<div class="card full" id="atlasCard">
<!-- Network Health Report -->
<div class="card full" id="healthCard">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
Network Health Report
</div>
<div id="healthContent"><div class="section-loading">Running comprehensive validation...</div></div>
</div>
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/><path d="M12 2v4M12 18v4M2 12h4M18 12h4"/></svg>
RIPE Atlas Probes
</div>
<div id="atlasContent"></div>
</div>
<!-- ASPA Intelligence (NEW) -->
<div class="card full" id="aspaCard">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2v-4M9 21H5a2 2 0 0 1-2-2v-4"/></svg>
ASPA Status
</div>
<div id="aspaContent"><div class="section-loading">Loading ASPA data...</div></div>
</div>
<!-- ASPA Deep Analysis (RFC-Compliant Verification) -->
<div class="card full" id="aspaDeepCard">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4"/></svg>
ASPA Deep Analysis (RFC-Compliant)
</div>
<div id="aspaDeepContent"><div class="section-loading">Loading ASPA deep analysis...</div></div>
</div>
<!-- bgproutes.io (NEW) -->
<div class="card full" id="bgroutesCard">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
bgproutes.io
</div>
<div id="bgroutesContent"><div class="section-loading">Loading bgproutes.io data...</div></div>
</div>
<!-- Routing Overview (enhanced Feature 24) -->
<div class="card full hidden" id="bgpHeCard">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
Routing Overview
</div>
<div id="bgpHeContent"></div>
</div>
<!-- WHOIS Details (Feature 27) -->
<div class="card full" id="whoisCard">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
WHOIS Details
</div>
<div id="whoisContent"><div class="section-loading">Loading WHOIS data...</div></div>
</div>
<!-- Neighbours -->
<div class="card full" id="neighbourCard">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/></svg>
AS Neighbours
</div>
<div id="neighbourContent"></div>
</div>
<!-- IX Presence -->
<div class="card full" id="ixCard">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="1" width="22" height="22" rx="2"/><path d="M7 1v22M17 1v22M1 12h22M1 7h22M1 17h22"/></svg>
IX Presence
</div>
<div id="ixContent"></div>
</div>
<!-- Facilities -->
<div class="card" id="facCard">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 21h18M3 7v14M21 7v14M6 21V10M10 21V10M14 21V10M18 21V10M12 7l9-4H3l9 4z"/></svg>
Facilities
</div>
<div id="facContent"></div>
</div>
<!-- Network Footprint Map -->
<div class="card full" id="mapCard" style="display:none">
<div class="card-title" style="cursor:pointer" onclick="toggleExpand(this)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 10-16 0c0 3 2.7 7 8 11.7z"/></svg>
Network Footprint Map
</div>
<div class="expand-body">
<div id="networkMap" style="height:450px;border-radius:8px;background:#1a1b26"></div>
</div>
</div>
<!-- Provider Relationship Graph -->
<div class="card full hidden" id="providerGraphCard">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="5" r="3"/><circle cx="5" cy="19" r="3"/><circle cx="19" cy="19" r="3"/><line x1="12" y1="8" x2="5" y2="16"/><line x1="12" y1="8" x2="19" y2="16"/></svg>
Provider Relationship Graph
</div>
<div id="providerGraphContent"></div>
</div>
<!-- ASPA Change Alert -->
<div class="card hidden" id="aspaAlertCard">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
ASPA Change Tracking
</div>
<div id="aspaAlertContent"></div>
</div>
<!-- Quick Compare -->
<div class="card" id="compareCard">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 20V10M12 20V4M6 20v-6"/></svg>
Quick Compare
</div>
<div id="compareContent">
<p style="font-size:.8rem;color:var(--muted);margin-bottom:.75rem">Compare IX, facility, upstream overlap and RPKI coverage with another network.</p>
<div class="compare-box">
<input type="text" class="compare-input" id="compareAsn" placeholder="Second ASN">
<button class="compare-btn" onclick="doCompare()">Compare</button>
</div>
<div id="compareResults" style="margin-top:1rem"></div>
</div>
</div>
</div>
<!-- Full Compare Results -->
<div class="dashboard hidden" id="fullComparePanel" style="margin-top:1rem"></div>
<!-- Loading Skeleton -->
<div class="dashboard hidden" id="skeleton">
<div class="card full"><div class="card-title">Network Overview</div><div class="skeleton h2"></div><div class="skeleton h3"></div><div class="skeleton wide"></div><div class="skeleton med"></div></div>
<div class="card"><div class="card-title">Announced Prefixes</div><div class="skeleton h2"></div><div class="skeleton wide"></div><div class="skeleton med"></div></div>
<div class="card"><div class="card-title">RPKI Compliance</div><div class="skeleton h2"></div><div class="skeleton wide"></div></div>
<div class="card full"><div class="card-title">RIPE Atlas Probes</div><div class="skeleton wide"></div><div class="skeleton med"></div></div>
<div class="card full"><div class="card-title">ASPA Status</div><div class="skeleton wide"></div><div class="skeleton med"></div></div>
<div class="card full"><div class="card-title">ASPA Deep Analysis</div><div class="skeleton h2"></div><div class="skeleton wide"></div><div class="skeleton med"></div><div class="skeleton wide"></div></div>
<div class="card full"><div class="card-title">bgproutes.io</div><div class="skeleton wide"></div><div class="skeleton med"></div></div>
<div class="card full"><div class="card-title">Routing Overview</div><div class="skeleton wide"></div><div class="skeleton med"></div><div class="skeleton wide"></div></div>
<div class="card full"><div class="card-title">WHOIS Details</div><div class="skeleton wide"></div><div class="skeleton med"></div></div>
<div class="card full"><div class="card-title">AS Neighbours</div><div class="skeleton wide"></div><div class="skeleton wide"></div><div class="skeleton med"></div></div>
<div class="card full"><div class="card-title">IX Presence</div><div class="skeleton wide"></div><div class="skeleton wide"></div></div>
<div class="card"><div class="card-title">Facilities</div><div class="skeleton wide"></div><div class="skeleton med"></div></div>
<div class="card"><div class="card-title">Quick Compare</div><div class="skeleton med"></div></div>
</div>
<!-- Footer -->
<!-- Peering Recommendations -->
<div class="card full hidden" id="peeringRecCard">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
Peering Recommendations
</div>
<div id="peeringRecContent"><div style="color:var(--dim);font-size:.85rem">Analyzing IX overlap with top networks...</div></div>
</div>
<!-- Integrated Sources of Trust -->
<div class="card full hidden" id="sourcesCard">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
Integrated Sources of Trust
</div>
<div id="sourcesContent">
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:1rem">
<div style="background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:1rem;display:flex;gap:.75rem;align-items:flex-start">
<div style="width:36px;height:36px;border-radius:8px;background:rgba(156,206,106,.12);border:1px solid rgba(156,206,106,.2);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:1rem">🌐</div>
<div><div style="font-weight:700;font-size:.85rem;color:var(--green)">PeeringDB</div><div style="font-size:.7rem;color:var(--muted);margin-top:.15rem">Network profiles, IX presence, facilities, peering policy. The authoritative source for interconnection data.</div><a href="https://www.peeringdb.com" target="_blank" style="font-size:.65rem;color:var(--blue)">peeringdb.com</a></div>
</div>
<div style="background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:1rem;display:flex;gap:.75rem;align-items:flex-start">
<div style="width:36px;height:36px;border-radius:8px;background:rgba(122,162,247,.12);border:1px solid rgba(122,162,247,.2);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:1rem">📊</div>
<div><div style="font-weight:700;font-size:.85rem;color:var(--blue)">RIPE Stat</div><div style="font-size:.7rem;color:var(--muted);margin-top:.15rem">Announced prefixes, AS neighbours, routing visibility, BGP updates, geolocation, abuse contacts.</div><a href="https://stat.ripe.net" target="_blank" style="font-size:.65rem;color:var(--blue)">stat.ripe.net</a></div>
</div>
<div style="background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:1rem;display:flex;gap:.75rem;align-items:flex-start">
<div style="width:36px;height:36px;border-radius:8px;background:rgba(187,154,247,.12);border:1px solid rgba(187,154,247,.2);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:1rem">🛡️</div>
<div><div style="font-weight:700;font-size:.85rem;color:var(--purple)">RPKI / ROA Validation</div><div style="font-size:.7rem;color:var(--muted);margin-top:.15rem">Per-prefix Route Origin Authorization validation via RIPE RPKI validators. Detects invalid or missing ROAs.</div><a href="https://rpki.cloudflare.com" target="_blank" style="font-size:.65rem;color:var(--blue)">rpki.cloudflare.com</a></div>
</div>
<div style="background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:1rem;display:flex;gap:.75rem;align-items:flex-start">
<div style="width:36px;height:36px;border-radius:8px;background:rgba(255,158,100,.12);border:1px solid rgba(255,158,100,.2);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:1rem">🔐</div>
<div><div style="font-weight:700;font-size:.85rem;color:var(--orange)">ASPA (RFC 9582)</div><div style="font-size:.7rem;color:var(--muted);margin-top:.15rem">AS Provider Authorization from Cloudflare RPKI JSON feed. RFC-compliant upstream/downstream path verification with valley detection.</div><a href="https://www.ietf.org/archive/id/draft-ietf-sidrops-aspa-verification-14.html" target="_blank" style="font-size:.65rem;color:var(--blue)">IETF Draft-14</a></div>
</div>
<div style="background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:1rem;display:flex;gap:.75rem;align-items:flex-start">
<div style="width:36px;height:36px;border-radius:8px;background:rgba(125,207,255,.12);border:1px solid rgba(125,207,255,.2);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:1rem">📡</div>
<div><div style="font-weight:700;font-size:.85rem;color:var(--cyan)">RIPE Atlas</div><div style="font-size:.7rem;color:var(--muted);margin-top:.15rem">Active measurement infrastructure. Probe presence, connectivity status, and anchor detection per ASN.</div><a href="https://atlas.ripe.net" target="_blank" style="font-size:.65rem;color:var(--blue)">atlas.ripe.net</a></div>
</div>
<div style="background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:1rem;display:flex;gap:.75rem;align-items:flex-start">
<div style="width:36px;height:36px;border-radius:8px;background:rgba(224,175,104,.12);border:1px solid rgba(224,175,104,.2);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:1rem">🔭</div>
<div><div style="font-weight:700;font-size:.85rem;color:var(--yellow)">bgproutes.io</div><div style="font-size:.7rem;color:var(--muted);margin-top:.15rem">Next-gen BGP data collection. 3,294+ vantage points, RIB queries, ROV and ASPA validation status per route.</div><a href="https://bgproutes.io" target="_blank" style="font-size:.65rem;color:var(--blue)">bgproutes.io</a></div>
</div>
<div style="background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:1rem;display:flex;gap:.75rem;align-items:flex-start">
<div style="width:36px;height:36px;border-radius:8px;background:rgba(156,206,106,.12);border:1px solid rgba(156,206,106,.2);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:1rem">🔍</div>
<div><div style="font-weight:700;font-size:.85rem;color:var(--green)">NLNOG IRR Explorer</div><div style="font-size:.7rem;color:var(--muted);margin-top:.15rem">Cross-references BGP origin announcements with Internet Routing Registry records. Detects mismatches and unauthorized announcements.</div><a href="https://irrexplorer.nlnog.net" target="_blank" style="font-size:.65rem;color:var(--blue)">irrexplorer.nlnog.net</a></div>
</div>
<div style="background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:1rem;display:flex;gap:.75rem;align-items:flex-start">
<div style="width:36px;height:36px;border-radius:8px;background:rgba(247,119,142,.12);border:1px solid rgba(247,119,142,.2);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:1rem">🤝</div>
<div><div style="font-weight:700;font-size:.85rem;color:var(--red)">MANRS Observatory</div><div style="font-size:.7rem;color:var(--muted);margin-top:.15rem">Mutually Agreed Norms for Routing Security. Checks membership, conformance level, and routing security commitment.</div><a href="https://observatory.manrs.org" target="_blank" style="font-size:.65rem;color:var(--blue)">observatory.manrs.org</a></div>
</div>
<div style="background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:1rem;display:flex;gap:.75rem;align-items:flex-start">
<div style="width:36px;height:36px;border-radius:8px;background:rgba(192,202,245,.12);border:1px solid rgba(192,202,245,.2);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:1rem">🌍</div>
<div><div style="font-weight:700;font-size:.85rem;color:var(--white)">bgp.he.net</div><div style="font-size:.7rem;color:var(--muted);margin-top:.15rem">Hurricane Electric BGP Toolkit. AS information, prefix lists, peer counts, and country attribution.</div><a href="https://bgp.he.net" target="_blank" style="font-size:.65rem;color:var(--blue)">bgp.he.net</a></div>
</div>
<div style="background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:1rem;display:flex;gap:.75rem;align-items:flex-start">
<div style="width:36px;height:36px;border-radius:8px;background:rgba(192,202,245,.12);border:1px solid rgba(192,202,245,.2);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:1rem">🚫</div>
<div><div style="font-weight:700;font-size:.85rem;color:var(--white)">Team Cymru Bogon Reference</div><div style="font-size:.7rem;color:var(--muted);margin-top:.15rem">Bogon prefix and ASN detection. Identifies reserved, unallocated, and private address space in BGP announcements.</div><a href="https://team-cymru.com/community-services/bogon-reference/" target="_blank" style="font-size:.65rem;color:var(--blue)">team-cymru.com</a></div>
</div>
<div style="background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:1rem;display:flex;gap:.75rem;align-items:flex-start">
<div style="width:36px;height:36px;border-radius:8px;background:rgba(122,162,247,.12);border:1px solid rgba(122,162,247,.2);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:1rem">📂</div>
<div><div style="font-weight:700;font-size:.85rem;color:var(--blue)">RIPE DB / IRR</div><div style="font-size:.7rem;color:var(--muted);margin-top:.15rem">Internet Routing Registry objects (aut-num, route, as-set). RPSL policy validation and object completeness checks.</div><a href="https://apps.db.ripe.net" target="_blank" style="font-size:.65rem;color:var(--blue)">apps.db.ripe.net</a></div>
</div>
<div style="background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:1rem;display:flex;gap:.75rem;align-items:flex-start">
<div style="width:36px;height:36px;border-radius:8px;background:rgba(125,207,255,.12);border:1px solid rgba(125,207,255,.2);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:1rem">📈</div>
<div><div style="font-weight:700;font-size:.85rem;color:var(--cyan)">Route Views / RIPE RIS</div><div style="font-size:.7rem;color:var(--muted);margin-top:.15rem">BGP route collectors providing global routing table visibility. Used for path analysis, visibility scoring, and anomaly detection.</div><a href="http://www.routeviews.org" target="_blank" style="font-size:.65rem;color:var(--blue)">routeviews.org</a></div>
</div>
</div>
<div style="margin-top:1rem;font-size:.7rem;color:var(--dim);text-align:center">All data is queried in real-time from authoritative sources. No data is stored or cached beyond 5 minutes.</div>
</div>
</div>
<footer class="footer">
<div style="margin-bottom:.75rem;font-size:.7rem;color:var(--dim)">
Data powered by
<a href="https://www.peeringdb.com" target="_blank">PeeringDB</a> &middot;
<a href="https://stat.ripe.net" target="_blank">RIPE Stat</a> &middot;
<a href="https://atlas.ripe.net" target="_blank">RIPE Atlas</a> &middot;
<a href="https://www.routeviews.org" target="_blank">Route Views</a> &middot;
<a href="https://bgp.he.net" target="_blank">bgp.he.net</a> &middot;
<a href="https://bgproutes.io" target="_blank">bgproutes.io</a> &middot;
<a href="https://www.ripe.net/manage-ips-and-asns/db" target="_blank">RIPE DB</a> &middot;
<a href="https://rpki.cloudflare.com" target="_blank">Cloudflare RPKI</a>
</div>
PeerCortex v0.5.0 &mdash; Open Source &mdash; MIT License<br>
<a href="https://github.com/renefichtmueller/PaperCortex" target="_blank">PaperCortex</a> &middot;
<a href="https://github.com/renefichtmueller/PeerCortex" target="_blank">PeerCortex GitHub</a>
</footer>
<script>
const $ = id => document.getElementById(id);
let currentAsn = null;
let currentLookupData = null;
function countryFlag(code) {
if (!code || code.length !== 2) return '';
const c = code.toUpperCase();
return String.fromCodePoint(...[...c].map(ch => 0x1F1E6 + ch.charCodeAt(0) - 65));
}
function fmtSpeed(mbps) {
if (!mbps || mbps === 0) return '0 Gbps';
if (mbps >= 1000) return (mbps / 1000).toFixed(0) + ' Gbps';
return mbps + ' Mbps';
}
function rpkiIcon(status) {
if (status === 'valid') return '<span class="rpki-valid" title="RPKI Valid">&#10003;</span>';
if (status === 'invalid') return '<span class="rpki-invalid" title="RPKI Invalid">&#10007;</span>';
return '<span class="rpki-unknown" title="Not Found">?</span>';
}
function pct(n, total) {
if (!total) return 0;
return Math.round((n / total) * 100);
}
function worstStatus(raw) {
if (!raw) return 'unknown';
var parts = raw.split(',').map(function(s) { return s.trim().toLowerCase(); });
if (parts.indexOf('invalid') >= 0) return 'invalid';
if (parts.indexOf('unknown') >= 0) return 'unknown';
if (parts.indexOf('valid') >= 0) return 'valid';
return parts[0] || 'unknown';
}
function asnLink(asn) {
return '<span class="asn-link" onclick="lookupAsn(' + asn + ')" title="Lookup AS' + asn + '">AS' + asn + '</span>';
}
function lookupAsn(asn) {
$('asnInput').value = asn;
doLookup();
}
function copyToClipboard(text, btn) {
navigator.clipboard.writeText(text).then(function() {
var orig = btn.textContent;
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = orig; }, 1500);
});
}
function renderAuditList(containerId, list, sortBy, type) {
var sorted = list.slice();
if (sortBy === 'asn') sorted.sort(function(a, b) { return a.asn - b.asn; });
else if (sortBy === 'frequency') sorted.sort(function(a, b) { return (b.frequency || 0) - (a.frequency || 0); });
else if (sortBy === 'name') sorted.sort(function(a, b) { return (a.name || '').localeCompare(b.name || ''); });
var labelId = type === 'missing' ? 'missingSortLabel' : 'extraSortLabel';
var labelEl = document.getElementById(labelId);
if (labelEl) labelEl.textContent = sortBy === 'asn' ? 'by ASN' : sortBy === 'frequency' ? 'by frequency' : 'by name';
var LIMIT = 5;
var h = '';
sorted.slice(0, LIMIT).forEach(function(item) {
h += '<div class="audit-row audit-' + type + '">';
h += '<span style="font-weight:600">' + asnLink(item.asn) + '</span>';
if (item.name && item.name !== 'AS' + item.asn) h += ' <span style="color:var(--muted)">' + escHtml(item.name) + '</span>';
if (type === 'missing') h += ' <span class="badge badge-orange">seen in ' + (item.frequency_pct || 0) + '% of paths (' + (item.frequency || 0) + ')</span>';
else h += ' <span class="badge badge-cyan">not seen in any path</span>';
h += '</div>';
});
if (sorted.length > LIMIT) {
var moreId = containerId + 'More';
h += '<div id="' + moreId + '" style="display:none">';
sorted.slice(LIMIT).forEach(function(item) {
h += '<div class="audit-row audit-' + type + '">';
h += '<span style="font-weight:600">' + asnLink(item.asn) + '</span>';
if (item.name && item.name !== 'AS' + item.asn) h += ' <span style="color:var(--muted)">' + escHtml(item.name) + '</span>';
if (type === 'missing') h += ' <span class="badge badge-orange">seen in ' + (item.frequency_pct || 0) + '% of paths (' + (item.frequency || 0) + ')</span>';
else h += ' <span class="badge badge-cyan">not seen in any path</span>';
h += '</div>';
});
h += '</div>';
var remaining = sorted.length - LIMIT;
h += '<div class="show-more-btn" onclick="var el=document.getElementById(\'' + moreId + '\');if(el.style.display===\'none\'){el.style.display=\'block\';this.textContent=\'Hide ' + remaining + ' entries\';}else{el.style.display=\'none\';this.textContent=\'Show ' + remaining + ' more...\';}">Show ' + remaining + ' more...</div>';
}
document.getElementById(containerId).innerHTML = h;
}
function renderMetaBar(d) {
var bar = $('metaBar');
// Clear previous content
while (bar.firstChild) bar.removeChild(bar.firstChild);
// Meta text span
var metaSpan = document.createElement('span');
metaSpan.textContent = 'Query: ' + d.meta.query + ' | Duration: ' + d.meta.duration_ms + 'ms | RPKI checked: ' + d.meta.rpki_prefixes_checked + '/' + d.meta.total_prefixes + ' prefixes | ' + d.meta.timestamp;
bar.appendChild(metaSpan);
// Data Quality badge
var dq = d.data_quality;
if (dq) {
var conf = dq.overall_confidence || 'high';
var badge = document.createElement('span');
badge.className = 'dq-badge ' + conf;
// Shield icon
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('fill', 'none');
svg.setAttribute('stroke', 'currentColor');
svg.setAttribute('stroke-width', '2');
var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z');
svg.appendChild(path);
badge.appendChild(svg);
var label = document.createElement('span');
var confLabel = conf === 'high' ? 'High Confidence' : conf === 'medium' ? 'Medium Confidence' : 'Low Confidence';
label.textContent = confLabel;
badge.appendChild(label);
// Tooltip
var tooltip = document.createElement('div');
tooltip.className = 'dq-tooltip';
var title = document.createElement('div');
title.style.cssText = 'font-weight:600;color:var(--white);margin-bottom:.5rem;font-size:.75rem';
title.textContent = 'Data Quality Report';
tooltip.appendChild(title);
var sources = document.createElement('div');
sources.style.cssText = 'margin-bottom:.5rem;color:var(--dim);font-size:.65rem';
sources.textContent = 'Sources Queried: ' + (dq.sources_queried || []).join(', ');
tooltip.appendChild(sources);
// Cross-Checks header
var checksHeader = document.createElement('div');
checksHeader.style.cssText = 'font-weight:600;color:var(--white);margin-bottom:.3rem;margin-top:.3rem;font-size:.7rem';
checksHeader.textContent = 'Cross-Checks';
tooltip.appendChild(checksHeader);
var checks = dq.cross_checks || {};
var checkNames = [['rpki', 'RPKI Validation'], ['prefixes', 'Prefix Count'], ['neighbours', 'Neighbours']];
var totalAgreement = 0;
var totalChecks = 0;
for (var ci = 0; ci < checkNames.length; ci++) {
var key = checkNames[ci][0];
var name = checkNames[ci][1];
var check = checks[key] || {};
var row = document.createElement('div');
row.className = 'dq-tooltip-row';
var rowLabel = document.createElement('span');
rowLabel.className = 'dq-tooltip-label';
rowLabel.textContent = name + ' (' + (check.sources || 0) + ' sources)';
row.appendChild(rowLabel);
var rowVal = document.createElement('span');
var pct = check.agreement_pct;
if (pct == null) {
rowVal.className = 'dq-tooltip-value na';
rowVal.textContent = 'N/A';
} else {
rowVal.className = 'dq-tooltip-value ' + (pct > 90 ? 'agree' : pct >= 70 ? 'warn' : 'bad');
rowVal.textContent = 'Agreement: ' + pct + '%';
totalAgreement += pct;
totalChecks++;
}
row.appendChild(rowVal);
tooltip.appendChild(row);
}
// Disagreements Found detail
var rpkiCheck = checks.rpki || {};
if (rpkiCheck.disagreements && rpkiCheck.disagreements.length > 0) {
var disDiv = document.createElement('div');
disDiv.style.cssText = 'margin-top:.4rem;font-size:.6rem;color:var(--orange)';
disDiv.textContent = 'Disagreements Found: ' + rpkiCheck.disagreements.map(function(dd) { return dd.prefix; }).join(', ');
tooltip.appendChild(disDiv);
}
// Prefix note
var pfxCheck = checks.prefixes || {};
if (pfxCheck.note) {
var noteDiv = document.createElement('div');
noteDiv.style.cssText = 'margin-top:.3rem;font-size:.6rem;color:var(--dim)';
noteDiv.textContent = pfxCheck.note;
tooltip.appendChild(noteDiv);
}
// Overall note
var avgAgreement = totalChecks > 0 ? totalAgreement / totalChecks : 100;
var overallNote = document.createElement('div');
overallNote.style.cssText = 'margin-top:.4rem;font-size:.6rem;font-style:italic;color:var(--muted)';
if (avgAgreement > 90) {
overallNote.textContent = 'All sources agree';
overallNote.style.color = 'var(--green)';
} else if (avgAgreement >= 70) {
overallNote.textContent = 'Minor discrepancies between sources';
overallNote.style.color = 'var(--orange)';
} else {
overallNote.textContent = 'Significant data disagreements detected';
overallNote.style.color = 'var(--red)';
}
tooltip.appendChild(overallNote);
badge.appendChild(tooltip);
bar.appendChild(badge);
}
}
async function doLookup() {
const raw = $('asnInput').value.trim().replace(/[^0-9]/g, '');
if (!raw) return;
currentAsn = raw;
$('searchBtn').disabled = true;
$('searchBtn').textContent = 'Loading...';
$('dashboard').classList.add('hidden');
$('skeleton').classList.remove('hidden');
$('metaBar').textContent = '';
try {
const resp = await fetch('/api/lookup?asn=' + raw);
const d = await resp.json();
if (d.error) {
$('skeleton').classList.add('hidden');
$('metaBar').textContent = 'Error: ' + d.error;
return;
}
currentLookupData = d;
renderDashboard(d);
$('skeleton').classList.add('hidden');
$('dashboard').classList.remove('hidden');
renderMetaBar(d);
history.replaceState(null, '', '?asn=' + raw);
saveToHistory(raw, d.network ? d.network.name : 'AS' + raw);
$('sourcesCard').classList.remove('hidden');
// Load peering recommendations
if (d.ix_presence && d.ix_presence.connections) loadPeeringRecommendations(currentAsn, d.ix_presence.connections, d);
// Load ASPA and bgproutes.io data asynchronously
loadHealthReport(raw);
loadAspaData(raw);
loadAspaVerifyData(raw);
loadBgroutesData(raw);
loadWhoisData(raw);
} catch (e) {
$('skeleton').classList.add('hidden');
$('metaBar').textContent = 'Error: ' + e.message;
} finally {
$('searchBtn').disabled = false;
$('searchBtn').textContent = 'Lookup';
}
}
async function loadAspaData(asn) {
$('aspaContent').innerHTML = '<div class="section-loading">Loading ASPA data...</div>';
try {
const resp = await fetch('/api/aspa?asn=' + asn);
if (!resp.ok) { $('aspaContent').textContent = 'ASPA data unavailable (server ' + resp.status + ')'; renderProviderGraphFromLookupFallback(asn); return; }
var text = await resp.text();
if (!text || text[0] === '<') { $('aspaContent').textContent = 'ASPA data unavailable (timeout). Provider data shown from lookup.'; renderProviderGraphFromLookupFallback(asn); return; }
const d = JSON.parse(text);
if (d.error) { $('aspaContent').textContent = 'ASPA check failed: ' + d.error; renderProviderGraphFromLookupFallback(asn); return; }
renderAspa(d);
} catch (e) {
$('aspaContent').innerHTML = '<div style="color:var(--red);font-size:.85rem">ASPA check failed: ' + escHtml(e.message) + '</div>';
renderProviderGraphFromLookupFallback(asn);
}
}
function renderProviderGraphFromLookupFallback(asn) {
if (!currentLookupData || !currentLookupData.neighbours) return;
var upstreams = currentLookupData.neighbours.upstreams || [];
if (upstreams.length === 0) return;
var providers = upstreams.map(function(u) {
return { asn: u.asn, name: u.name || '', frequency_pct: u.power ? Math.min(u.power * 10, 100) : 0 };
});
renderProviderGraph(asn, providers);
}
async function loadBgroutesData(asn) {
$('bgroutesContent').innerHTML = '<div class="section-loading">Loading bgproutes.io data...</div>';
try {
const resp = await fetch('/api/bgproutes?asn=' + asn);
const d = await resp.json();
if (d.error) {
$('bgroutesContent').innerHTML = '<div style="color:var(--red);font-size:.85rem">bgproutes.io query failed: ' + escHtml(d.error) + '</div>';
return;
}
renderBgroutes(d);
} catch (e) {
$('bgroutesContent').innerHTML = '<div style="color:var(--red);font-size:.85rem">bgproutes.io query failed: ' + escHtml(e.message) + '</div>';
}
}
function renderAspa(d) {
let h = '';
// ASPA status
h += '<div style="display:flex;gap:2rem;flex-wrap:wrap;margin-bottom:1rem">';
h += '<div><div style="font-size:.75rem;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.25rem">ASPA Object</div>';
if (d.aspa_object_exists) {
h += '<div class="status-yes">Found in RPKI</div>';
} else {
h += '<div class="status-no">Not Found</div>';
}
h += '</div>';
if (d.aspa_object_exists && d.aspa_declared_providers && d.aspa_declared_providers.length > 0) {
h += '<div><div style="font-size:.75rem;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.25rem">RPKI-Declared Providers</div>';
h += '<div style="font-size:1.5rem;font-weight:700;color:var(--green)">' + d.aspa_declared_count + '</div>';
h += '</div>';
}
h += '<div><div style="font-size:.75rem;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.25rem">Detected Providers</div>';
h += '<div style="font-size:1.5rem;font-weight:700;color:var(--blue)">' + d.provider_count + '</div>';
h += '</div>';
h += '<div><div style="font-size:.75rem;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.25rem">BGP Paths Analyzed</div>';
h += '<div style="font-size:1.5rem;font-weight:700;color:var(--cyan)">' + (d.path_analysis ? d.path_analysis.total_paths_seen : 0) + '</div>';
h += '</div>';
h += '</div>';
// Detected providers (collapsible after 10)
if (d.detected_providers && d.detected_providers.length > 0) {
var provLimit = 10;
var provList = d.detected_providers.slice().sort(function(a, b) { return a.asn - b.asn; });
h += '<div style="font-size:.8rem;font-weight:600;color:var(--orange);margin:.75rem 0 .4rem">Detected Upstream Providers (' + provList.length + ')</div>';
h += '<div style="display:flex;flex-wrap:wrap;gap:.3rem;margin-bottom:.25rem">';
provList.slice(0, provLimit).forEach(function(p) {
var nameStr = (p.name && p.name !== 'AS' + p.asn) ? ' ' + escHtml(p.name) : '';
h += '<span class="badge badge-orange">' + asnLink(p.asn) + nameStr + '</span>';
});
h += '</div>';
if (provList.length > provLimit) {
var provMoreId = 'provMore' + Date.now();
h += '<div id="' + provMoreId + '" style="display:none;flex-wrap:wrap;gap:.3rem;margin-bottom:.25rem">';
provList.slice(provLimit).forEach(function(p) {
var nameStr = (p.name && p.name !== 'AS' + p.asn) ? ' ' + escHtml(p.name) : '';
h += '<span class="badge badge-orange">' + asnLink(p.asn) + nameStr + '</span>';
});
h += '</div>';
h += '<div class="show-more-btn" onclick="var el=document.getElementById(\'' + provMoreId + '\');if(el.style.display===\'none\'){el.style.display=\'flex\';this.textContent=\'Hide ' + (provList.length - provLimit) + ' providers\';}else{el.style.display=\'none\';this.textContent=\'Show ' + (provList.length - provLimit) + ' more providers...\';}">Show ' + (provList.length - provLimit) + ' more providers...</div>';
}
}
// RPKI-declared providers (when ASPA object exists)
if (d.aspa_object_exists && d.aspa_declared_providers && d.aspa_declared_providers.length > 0) {
var declaredList = d.aspa_declared_providers.slice().sort(function(a, b) { return a.asn - b.asn; });
h += '<div style="font-size:.8rem;font-weight:600;color:var(--green);margin:.75rem 0 .4rem">RPKI-Declared Providers (' + declaredList.length + ')</div>';
h += '<div style="display:flex;flex-wrap:wrap;gap:.3rem;margin-bottom:.25rem">';
declaredList.forEach(function(p) {
var label = p.asn === 0 ? 'AS0 (Tier-1 / No Provider)' : asnLink(p.asn);
h += '<span class="badge badge-green">' + label + '</span>';
});
h += '</div>';
}
// Recommended ASPA template (scrollable, max 200px)
if (d.recommended_aspa) {
h += '<div style="font-size:.8rem;font-weight:600;color:var(--cyan);margin:.75rem 0 .4rem">Recommended ASPA Object</div>';
h += '<div class="aspa-template" id="aspaTemplate" style="max-height:200px;overflow-y:auto">' + escHtml(d.recommended_aspa);
h += '<button class="copy-btn" onclick="copyToClipboard(document.getElementById(\'aspaTemplate\').innerText, this)">Copy</button>';
h += '</div>';
}
// Sample path analysis
if (d.path_analysis && d.path_analysis.sample_paths && d.path_analysis.sample_paths.length > 0) {
h += '<div class="expand-toggle" onclick="toggleExpand(this)">Show sample BGP paths (' + d.path_analysis.sample_paths.length + ')</div>';
h += '<div class="expand-body"><div class="scroll-wrap"><table class="tbl"><thead><tr><th>RRC</th><th>Prefix</th><th>AS Path</th><th>Provider</th></tr></thead><tbody>';
d.path_analysis.sample_paths.forEach(function(p) {
h += '<tr><td>' + escHtml(p.rrc || '') + '</td>';
h += '<td style="font-family:monospace;font-size:.75rem">' + escHtml(p.prefix || '') + '</td>';
h += '<td style="font-family:monospace;font-size:.7rem">' + escHtml(p.path || '') + '</td>';
h += '<td>' + (p.detected_provider ? '<span class="badge badge-green">' + escHtml(p.detected_provider) + '</span>' : '-') + '</td></tr>';
});
h += '</tbody></table></div></div>';
}
h += '<div style="font-size:.7rem;color:var(--dim);margin-top:.5rem">Analyzed in ' + (d.meta ? d.meta.duration_ms : '?') + 'ms</div>';
$('aspaContent').innerHTML = h;
// Feature 8: ASPA Change Alerting
if (d.detected_providers && currentAsn) {
checkAspaChanges(currentAsn, d.detected_providers);
}
// Feature 9: Provider Relationship Graph
if (d.detected_providers && d.detected_providers.length > 0 && currentAsn) {
renderProviderGraph(currentAsn, d.detected_providers);
}
}
function renderBgroutes(d) {
let h = '';
h += '<div style="display:flex;gap:2rem;flex-wrap:wrap;margin-bottom:1rem">';
// Vantage points
if (d.vantage_points) {
h += '<div><div style="font-size:.75rem;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.25rem">Vantage Points</div>';
h += '<div style="font-size:1.5rem;font-weight:700;color:var(--green)">' + (d.vantage_points.count || 0) + '</div>';
h += '</div>';
}
// Route data status
if (d.routes) {
h += '<div><div style="font-size:.75rem;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.25rem">Route Data</div>';
if (d.routes.status === 'unavailable') {
h += '<div style="font-size:.85rem;color:var(--orange)">' + escHtml(d.routes.message) + '</div>';
} else {
h += '<div style="font-size:1.5rem;font-weight:700;color:var(--blue)">' + (d.routes.count || 0) + ' routes</div>';
}
h += '</div>';
}
h += '</div>';
// VP list
if (d.vantage_points && d.vantage_points.list && d.vantage_points.list.length > 0) {
h += '<div class="expand-toggle" onclick="toggleExpand(this)">Show vantage points (' + d.vantage_points.list.length + ')</div>';
h += '<div class="expand-body"><div class="scroll-wrap" style="max-height:200px"><table class="tbl"><thead><tr><th>ID</th><th>Name</th><th>Location</th><th>ASN</th></tr></thead><tbody>';
d.vantage_points.list.forEach(function(vp) {
h += '<tr><td>' + escHtml(vp.id || vp.vp_id || '') + '</td>';
h += '<td>' + escHtml(vp.name || vp.description || '') + '</td>';
h += '<td>' + escHtml(vp.location || vp.city || vp.country || '') + '</td>';
h += '<td>' + escHtml(vp.asn || vp.peer_asn || '') + '</td></tr>';
});
h += '</tbody></table></div></div>';
}
// Route samples
if (d.routes && d.routes.sample && d.routes.sample.length > 0) {
if (d.routes.vp_used) {
h += '<div style="font-size:.75rem;color:var(--muted);margin-bottom:.5rem">VP: ' + escHtml(d.routes.vp_used.org || '') + ' (' + escHtml(d.routes.vp_used.country || '') + ', ID ' + (d.routes.vp_used.id || '') + ')</div>';
}
h += '<div class="expand-toggle" onclick="toggleExpand(this)">Show route data (' + d.routes.count + ' routes, showing ' + d.routes.sample.length + ')</div>';
h += '<div class="expand-body"><div class="scroll-wrap"><table class="tbl"><thead><tr><th>Prefix</th><th>AS Path</th><th>ROV</th><th>ASPA</th></tr></thead><tbody>';
d.routes.sample.forEach(function(r) {
var rov = worstStatus(r.rov_status);
var aspa = worstStatus(r.aspa_status);
var rovBadge = rov === 'valid' ? 'badge-green' : rov === 'invalid' ? 'badge-red' : 'badge-orange';
var aspaBadge = aspa === 'valid' ? 'badge-green' : aspa === 'invalid' ? 'badge-red' : 'badge-orange';
h += '<tr><td style="font-family:monospace;font-size:.7rem">' + escHtml(r.prefix || '') + '</td>';
h += '<td style="font-family:monospace;font-size:.65rem;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escAttr(r.as_path || '') + '">' + escHtml(r.as_path || '') + '</td>';
h += '<td><span class="badge ' + rovBadge + '">' + escHtml(rov) + '</span></td>';
h += '<td><span class="badge ' + aspaBadge + '">' + escHtml(aspa) + '</span></td></tr>';
});
h += '</tbody></table></div></div>';
}
h += '<div style="font-size:.7rem;color:var(--dim);margin-top:.5rem">Queried in ' + (d.meta ? d.meta.duration_ms : '?') + 'ms</div>';
$('bgroutesContent').innerHTML = h;
}
function renderDashboard(d) {
const n = d.network;
const p = d.prefixes;
const r = d.rpki;
const nb = d.neighbours;
const ix = d.ix_presence;
const fac = d.facilities;
// Overview
let ov = '<div class="net-name">' + escHtml(n.name) + ' <span style="color:var(--muted);font-size:1rem">AS' + n.asn + '</span></div>';
if (n.aka) ov += '<div class="net-aka">AKA: ' + escHtml(n.aka) + '</div>';
if (n.country) ov += '<span class="flag">' + countryFlag(n.country) + '</span>';
if (n.rir) ov += '<span class="badge badge-cyan">' + escHtml(n.rir) + '</span>';
if (n.type) ov += '<span class="badge badge-purple">' + escHtml(n.type) + '</span>';
if (n.policy) ov += '<span class="badge badge-blue">' + escHtml(n.policy) + '</span>';
if (n.ratio) ov += '<span class="badge badge-green">' + escHtml(n.ratio) + '</span>';
if (n.scope) ov += '<span class="badge badge-orange">' + escHtml(n.scope) + '</span>';
if (n.traffic) ov += '<span class="badge badge-cyan">' + escHtml(n.traffic) + '</span>';
if (n.website) ov += '<div style="margin-top:.5rem"><a href="' + escAttr(n.website) + '" target="_blank">' + escHtml(n.website) + '</a></div>';
if (n.org_name) ov += '<div style="margin-top:.4rem;font-size:.85rem;color:var(--dim)">' + escHtml(n.org_name) + '</div>';
if (n.notes) ov += '<div style="margin-top:.3rem;font-size:.8rem;color:var(--dim);line-height:1.5;max-height:4.5em;overflow:hidden">' + escHtml(n.notes) + '</div>';
ov += '<div class="ext-links">';
if (n.peeringdb_id) ov += '<a class="ext-link" href="https://www.peeringdb.com/net/' + n.peeringdb_id + '" target="_blank">PeeringDB</a>';
ov += '<a class="ext-link" href="https://bgp.he.net/AS' + n.asn + '" target="_blank">bgp.he.net</a>';
ov += '<a class="ext-link" href="https://stat.ripe.net/AS' + n.asn + '" target="_blank">RIPE Stat</a>';
ov += '<a class="ext-link" href="https://www.routeviews.org/routeviews/index.php/prefix/?asn=' + n.asn + '" target="_blank">Route Views</a>';
ov += '<a class="ext-link" href="https://bgproutes.io/search/AS' + n.asn + '" target="_blank">bgproutes.io</a>';
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 += '</div>';
$('overviewContent').innerHTML = ov;
// Prefixes
let px = '<div class="stat-row">';
px += '<div class="stat"><div class="stat-val blue">' + p.total + '</div><div class="stat-label">Total</div></div>';
px += '<div class="stat"><div class="stat-val green">' + p.ipv4 + '</div><div class="stat-label">IPv4</div></div>';
px += '<div class="stat"><div class="stat-val purple">' + p.ipv6 + '</div><div class="stat-label">IPv6</div></div>';
px += '</div>';
const v4pct = pct(p.ipv4, p.total);
px += '<div style="font-size:.7rem;color:var(--muted)">IPv4 ' + v4pct + '% / IPv6 ' + (100 - v4pct) + '%</div>';
px += '<div class="progress-multi"><div style="width:' + v4pct + '%;background:var(--green)"></div><div style="width:' + (100 - v4pct) + '%;background:var(--purple)"></div></div>';
if (p.list && p.list.length > 0) {
px += '<div class="expand-toggle" onclick="toggleExpand(this)">Show all ' + p.list.length + ' prefixes</div>';
px += '<div class="expand-body"><div class="scroll-wrap"><table class="tbl"><thead><tr><th>Prefix</th><th>RPKI</th></tr></thead><tbody>';
const rpkiMap = {};
(r.details || []).forEach(function(rd) { rpkiMap[rd.prefix] = rd.status; });
p.list.forEach(function(pfx) {
const st = rpkiMap[pfx] || 'not_checked';
px += '<tr><td><span class="prefix-link" onclick="showPrefixDetail(\'' + escAttr(pfx) + '\')">' + escHtml(pfx) + '</span></td><td>' + rpkiIcon(st) + '</td></tr>';
});
px += '</tbody></table></div></div>';
}
$('prefixContent').innerHTML = px;
// RPKI
let rk = '';
const scoreClass = r.coverage_percent >= 90 ? 'high' : r.coverage_percent >= 70 ? 'mid' : 'low';
rk += '<div class="big-score ' + scoreClass + '">' + r.coverage_percent + '%</div>';
rk += '<div style="font-size:.8rem;color:var(--muted);margin-bottom:.5rem">RPKI Coverage (' + r.checked + ' prefixes checked)</div>';
const vpct = pct(r.valid, r.checked);
const ipct = pct(r.invalid, r.checked);
const npct = 100 - vpct - ipct;
rk += '<div class="progress-multi">';
rk += '<div style="width:' + vpct + '%;background:var(--green)" title="Valid ' + vpct + '%"></div>';
rk += '<div style="width:' + ipct + '%;background:var(--red)" title="Invalid ' + ipct + '%"></div>';
rk += '<div style="width:' + npct + '%;background:var(--dim)" title="Not Found ' + npct + '%"></div>';
rk += '</div>';
rk += '<div style="display:flex;gap:1.5rem;margin-top:.5rem;font-size:.8rem">';
rk += '<div><span style="color:var(--green);font-weight:600">' + r.valid + '</span> <span style="color:var(--muted)">valid</span></div>';
rk += '<div><span style="color:var(--red);font-weight:600">' + r.invalid + '</span> <span style="color:var(--muted)">invalid</span></div>';
rk += '<div><span style="color:var(--dim);font-weight:600">' + r.not_found + '</span> <span style="color:var(--muted)">not found</span></div>';
rk += '</div>';
// Expandable per-prefix RPKI details
if (r.details && r.details.length > 0) {
rk += '<div class="expand-toggle" onclick="toggleExpand(this)">Show per-prefix RPKI details (' + r.details.length + ')</div>';
rk += '<div class="expand-body"><div class="scroll-wrap"><table class="tbl"><thead><tr><th>Prefix</th><th>Status</th><th>ROAs</th></tr></thead><tbody>';
r.details.forEach(function(rd) {
var icon = rpkiIcon(rd.status);
var statusText = rd.status === 'valid' ? '<span style="color:var(--green)">Valid</span>' : rd.status === 'invalid' ? '<span style="color:var(--red)">Invalid</span>' : '<span style="color:var(--muted)">Not Found</span>';
rk += '<tr><td style="font-family:monospace;font-size:.75rem">' + escHtml(rd.prefix) + '</td>';
rk += '<td>' + icon + ' ' + statusText + '</td>';
rk += '<td>' + (rd.validating_roas || 0) + '</td></tr>';
});
rk += '</tbody></table></div></div>';
}
$('rpkiContent').innerHTML = rk;
// Atlas Probes
renderAtlas(d.atlas);
// Neighbours
let ne = '<div class="stat-row">';
ne += '<div class="stat"><div class="stat-val">' + nb.total + '</div><div class="stat-label">Total</div></div>';
ne += '<div class="stat"><div class="stat-val orange">' + nb.upstream_count + '</div><div class="stat-label">Upstreams</div></div>';
ne += '<div class="stat"><div class="stat-val cyan">' + nb.peer_count + '</div><div class="stat-label">Peers</div></div>';
ne += '<div class="stat"><div class="stat-val blue">' + nb.downstream_count + '</div><div class="stat-label">Downstreams</div></div>';
ne += '</div>';
ne += '<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:1rem">'; // 3 columns for upstream/downstream/peer
ne += '<div><div style="font-size:.75rem;font-weight:600;color:var(--orange);margin-bottom:.4rem">Top Upstreams</div>';
ne += '<div class="scroll-wrap"><table class="tbl"><thead><tr><th>ASN</th><th>Name</th><th>Power</th></tr></thead><tbody>';
(nb.upstreams || []).slice(0, 10).forEach(function(u) {
var nameDisplay = (u.name && u.name !== 'AS' + u.asn) ? escHtml(u.name) : '';
ne += '<tr><td>' + asnLink(u.asn) + '</td><td>' + nameDisplay + '</td><td>' + (u.power || '-') + '</td></tr>';
});
ne += '</tbody></table></div></div>';
ne += '<div><div style="font-size:.75rem;font-weight:600;color:var(--blue);margin-bottom:.4rem">Top Downstreams</div>';
ne += '<div class="scroll-wrap"><table class="tbl"><thead><tr><th>ASN</th><th>Name</th><th>Power</th></tr></thead><tbody>';
(nb.downstreams || []).slice(0, 10).forEach(function(u) {
var nameDisplay = (u.name && u.name !== 'AS' + u.asn) ? escHtml(u.name) : '';
ne += '<tr><td>' + asnLink(u.asn) + '</td><td>' + nameDisplay + '</td><td>' + (u.power || '-') + '</td></tr>';
});
ne += '</tbody></table></div></div>';
ne += '<div><div style="font-size:.75rem;font-weight:600;color:var(--cyan);margin-bottom:.4rem">Top Peers</div>';
ne += '<div class="scroll-wrap"><table class="tbl"><thead><tr><th>ASN</th><th>Name</th><th>Power</th></tr></thead><tbody>';
(nb.peers || []).slice(0, 10).forEach(function(u) {
var nameDisplay = (u.name && u.name !== 'AS' + u.asn) ? escHtml(u.name) : '';
ne += '<tr><td>' + asnLink(u.asn) + '</td><td>' + nameDisplay + '</td><td>' + (u.power || '-') + '</td></tr>';
});
ne += '</tbody></table></div></div>';
ne += '</div>';
$('neighbourContent').innerHTML = ne;
// IX Presence
let ixh = '<div class="stat-row">';
ixh += '<div class="stat"><div class="stat-val green">' + ix.total_connections + '</div><div class="stat-label">Connections</div></div>';
ixh += '<div class="stat"><div class="stat-val purple">' + ix.unique_ixps + '</div><div class="stat-label">Unique IXPs</div></div>';
ixh += '</div>';
if (ix.connections && ix.connections.length > 0) {
ixh += '<div class="scroll-wrap"><table class="tbl"><thead><tr><th>IX Name</th><th>Speed</th><th>IPv4</th><th>IPv6</th></tr></thead><tbody>';
ix.connections.forEach(function(c) {
const ixUrl = c.ix_id ? 'https://www.peeringdb.com/ix/' + c.ix_id : '#';
ixh += '<tr><td><span class="ix-link" onclick="showIXDetail(' + (c.ix_id || 0) + ', \'' + escAttr(c.ix_name) + '\')">' + escHtml(c.ix_name) + '</span></td>';
ixh += '<td>' + fmtSpeed(c.speed_mbps) + '</td>';
ixh += '<td style="font-family:monospace;font-size:.75rem">' + (c.ipv4 || '-') + '</td>';
ixh += '<td style="font-family:monospace;font-size:.75rem">' + (c.ipv6 || '-') + '</td></tr>';
});
ixh += '</tbody></table></div>';
}
// Feature 26: IX traffic stats
ixh += renderIxTrafficStats(ix.connections);
$('ixContent').innerHTML = ixh;
// Facilities
let fh = '<div class="stat-row"><div class="stat"><div class="stat-val blue">' + fac.total + '</div><div class="stat-label">Facilities</div></div></div>';
if (fac.list && fac.list.length > 0) {
fh += '<div class="scroll-wrap"><table class="tbl"><thead><tr><th>Facility</th><th>City</th><th>Country</th></tr></thead><tbody>';
fac.list.forEach(function(f) {
const fUrl = f.fac_id ? 'https://www.peeringdb.com/fac/' + f.fac_id : '#';
fh += '<tr><td><a href="' + fUrl + '" target="_blank">' + escHtml(f.name) + '</a></td>';
fh += '<td>' + escHtml(f.city) + '</td>';
fh += '<td>' + countryFlag(f.country) + ' ' + escHtml(f.country) + '</td></tr>';
});
fh += '</tbody></table></div>';
}
$('facContent').innerHTML = fh;
// Network Footprint Map
renderNetworkMap(d);
// Feature 24: Render bgp.he.net data
renderRoutingOverview(d.bgp_he_net, d.routing);
}
// Note: innerHTML usage above is the existing pattern in this codebase; all user-facing
// strings are escaped via escHtml/escAttr before insertion.
var _pcMap = null;
function renderNetworkMap(d) {
var mapCard = document.getElementById('mapCard');
var mapDiv = document.getElementById('networkMap');
if (!mapCard || !mapDiv || typeof L === 'undefined') return;
// Collect all markers
var markers = [];
var facs = (d.facilities && d.facilities.list) || [];
facs.forEach(function(f) {
if (f.latitude && f.longitude) {
markers.push({ lat: f.latitude, lng: f.longitude, type: 'fac', name: f.name, detail: f.city + (f.country ? ', ' + f.country : '') });
}
});
var ixLocs = d.ix_locations || [];
var ixConns = (d.ix_presence && d.ix_presence.connections) || [];
var ixSpeedMap = {};
ixConns.forEach(function(c) { if (c.ix_id) ixSpeedMap[c.ix_id] = (ixSpeedMap[c.ix_id] || 0) + (c.speed_mbps || 0); });
ixLocs.forEach(function(ix) {
if (ix.latitude && ix.longitude) {
var spd = ixSpeedMap[ix.ix_id] || 0;
markers.push({ lat: ix.latitude, lng: ix.longitude, type: 'ix', name: ix.name, detail: (ix.city || '') + (spd ? ' | ' + fmtSpeed(spd) : '') });
}
});
if (markers.length === 0) { mapCard.style.display = 'none'; return; }
// KEY FIX: Set display + explicit pixel dimensions BEFORE creating Leaflet map
mapCard.style.display = 'block';
var cardWidth = mapCard.getBoundingClientRect().width;
mapDiv.style.width = (cardWidth - 40) + 'px';
mapDiv.style.height = '450px';
// Destroy previous map
if (_pcMap) { _pcMap.remove(); _pcMap = null; }
// Use setTimeout to let the browser fully lay out the container
setTimeout(function() {
// Re-measure after layout
var w = mapDiv.getBoundingClientRect().width;
if (w < 100) { mapDiv.style.width = '100%'; }
_pcMap = L.map(mapDiv, { scrollWheelZoom: false, attributionControl: false });
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
maxZoom: 18, subdomains: 'abcd'
}).addTo(_pcMap);
L.control.attribution({ prefix: false }).addAttribution('&copy; <a href="https://carto.com">CARTO</a>').addTo(_pcMap);
var bounds = L.latLngBounds();
markers.forEach(function(m) {
var color = m.type === 'fac' ? '#7dcfff' : '#ff9e64';
var radius = m.type === 'fac' ? 7 : 6;
var circle = L.circleMarker([m.lat, m.lng], {
radius: radius, fillColor: color, fillOpacity: 0.85, color: color, weight: 1, opacity: 0.6
}).addTo(_pcMap);
var popupDiv = document.createElement('div');
popupDiv.style.cssText = 'font-family:Inter,sans-serif;font-size:12px';
var nameEl = document.createElement('b');
nameEl.textContent = m.name;
popupDiv.appendChild(nameEl);
if (m.detail) {
popupDiv.appendChild(document.createElement('br'));
var detailEl = document.createElement('span');
detailEl.textContent = m.detail;
popupDiv.appendChild(detailEl);
}
circle.bindPopup(popupDiv);
bounds.extend([m.lat, m.lng]);
});
// Faint lines between facilities in same country
var facByCountry = {};
facs.forEach(function(f) {
if (f.latitude && f.longitude && f.country) {
if (!facByCountry[f.country]) facByCountry[f.country] = [];
facByCountry[f.country].push([f.latitude, f.longitude]);
}
});
Object.keys(facByCountry).forEach(function(cc) {
var pts = facByCountry[cc];
if (pts.length >= 2 && pts.length <= 15) {
for (var i = 0; i < pts.length - 1; i++) {
L.polyline([pts[i], pts[i + 1]], { color: '#bb9af7', weight: 1, opacity: 0.3 }).addTo(_pcMap);
}
}
});
if (bounds.isValid()) {
_pcMap.fitBounds(bounds, { padding: [30, 30], maxZoom: 6 });
}
// Final invalidateSize after everything is rendered
setTimeout(function() {
if (_pcMap) _pcMap.invalidateSize();
}, 300);
}, 50);
}
function renderAtlas(atlas) {
if (!atlas) {
$('atlasContent').innerHTML = '<div style="font-size:.85rem;color:var(--muted)">No RIPE Atlas data available.</div>';
return;
}
var h = '';
// Summary badges
h += '<div class="stat-row">';
h += '<div class="stat"><div class="stat-val blue">' + atlas.total_probes + '</div><div class="stat-label">Total Probes</div></div>';
h += '<div class="stat"><div class="stat-val green">' + atlas.connected + '</div><div class="stat-label">Connected</div></div>';
h += '<div class="stat"><div class="stat-val red">' + atlas.disconnected + '</div><div class="stat-label">Disconnected</div></div>';
h += '<div class="stat"><div class="stat-val orange">' + atlas.anchors + '</div><div class="stat-label">Anchors</div></div>';
h += '</div>';
if (atlas.total_probes > 0) {
// Connection ratio bar
var connPct = pct(atlas.connected, atlas.total_probes);
h += '<div style="font-size:.7rem;color:var(--muted)">Connected ' + connPct + '% / Disconnected ' + (100 - connPct) + '%</div>';
h += '<div class="progress-multi"><div style="width:' + connPct + '%;background:var(--green)"></div><div style="width:' + (100 - connPct) + '%;background:var(--red)"></div></div>';
}
if (atlas.probes && atlas.probes.length > 0) {
h += '<div class="expand-toggle" onclick="toggleExpand(this)">Show probe details (' + atlas.probes.length + (atlas.total_probes > atlas.probes.length ? ' of ' + atlas.total_probes : '') + ')</div>';
h += '<div class="expand-body"><div class="scroll-wrap"><table class="tbl"><thead><tr><th>ID</th><th>Status</th><th>Anchor</th><th>Country</th><th>Prefix</th><th>Description</th></tr></thead><tbody>';
atlas.probes.forEach(function(p) {
var pStatus = typeof p.status === 'object' ? (p.status && p.status.name ? p.status.name : '') : (p.status || p.status_name || ''); var statusClass = pStatus.toLowerCase() === 'connected' ? 'badge-green' : 'badge-red';
var anchorBadge = p.is_anchor ? '<span class="badge badge-orange">Anchor</span>' : '-';
var prefix = p.prefix_v4 || p.prefix_v6 || '-';
h += '<tr>';
h += '<td><a href="https://atlas.ripe.net/probes/' + p.id + '/" target="_blank" style="color:var(--blue)">' + p.id + '</a></td>';
h += '<td><span class="badge ' + statusClass + '">' + escHtml(pStatus) + '</span></td>';
h += '<td>' + anchorBadge + '</td>';
h += '<td>' + countryFlag(p.country) + ' ' + escHtml(p.country) + '</td>';
h += '<td style="font-family:monospace;font-size:.75rem">' + escHtml(prefix) + '</td>';
h += '<td style="font-size:.75rem">' + escHtml(p.description) + '</td>';
h += '</tr>';
});
h += '</tbody></table></div></div>';
} else if (atlas.total_probes === 0) {
h += '<div style="margin-top:.75rem;font-size:.85rem;color:var(--muted)">No RIPE Atlas probes found for this network. <a href="https://atlas.ripe.net/get-involved/become-a-host/" target="_blank" style="color:var(--blue)">Host a probe?</a></div>';
}
$('atlasContent').innerHTML = h;
}
async function doCompare() {
if (!currentAsn) return;
const raw2 = $('compareAsn').value.trim().replace(/[^0-9]/g, '');
if (!raw2) return;
$('compareResults').innerHTML = '<div class="skeleton wide"></div><div class="skeleton med"></div>';
doFullCompare();
try {
const resp = await fetch('/api/compare?asn1=' + currentAsn + '&asn2=' + raw2);
const d = await resp.json();
let h = '<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1rem">';
h += '<div style="text-align:center"><div class="stat-val blue">' + escHtml(d.asn1.name) + '</div><div class="stat-label">AS' + d.asn1.asn + ' (' + d.asn1.ix_count + ' IXPs)</div></div>';
h += '<div style="text-align:center"><div class="stat-val purple">' + escHtml(d.asn2.name) + '</div><div class="stat-label">AS' + d.asn2.asn + ' (' + d.asn2.ix_count + ' IXPs)</div></div>';
h += '</div>';
// RPKI comparison
if (d.rpki_comparison) {
h += '<div style="font-size:.8rem;font-weight:600;color:var(--purple);margin:.75rem 0 .4rem">RPKI Coverage Comparison</div>';
h += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:.75rem">';
var s1class = d.rpki_comparison.asn1_coverage >= 90 ? 'green' : d.rpki_comparison.asn1_coverage >= 70 ? 'orange' : 'red';
var s2class = d.rpki_comparison.asn2_coverage >= 90 ? 'green' : d.rpki_comparison.asn2_coverage >= 70 ? 'orange' : 'red';
h += '<div style="text-align:center"><div style="font-size:2rem;font-weight:700;color:var(--' + s1class + ')">' + d.rpki_comparison.asn1_coverage + '%</div><div style="font-size:.7rem;color:var(--muted)">AS' + d.asn1.asn + ' (' + d.rpki_comparison.asn1_checked + ' checked)</div></div>';
h += '<div style="text-align:center"><div style="font-size:2rem;font-weight:700;color:var(--' + s2class + ')">' + d.rpki_comparison.asn2_coverage + '%</div><div style="font-size:.7rem;color:var(--muted)">AS' + d.asn2.asn + ' (' + d.rpki_comparison.asn2_checked + ' checked)</div></div>';
h += '</div>';
if (d.rpki_comparison.better !== 'equal') {
h += '<div style="text-align:center;font-size:.8rem;color:var(--green);margin-bottom:.5rem">' + escHtml(d.rpki_comparison.better) + ' has better RPKI coverage</div>';
}
}
// Common upstreams
if (d.common_upstreams && d.common_upstreams.length > 0) {
h += '<div style="font-size:.8rem;font-weight:600;color:var(--yellow);margin:.5rem 0">Common Upstreams (' + d.common_upstreams.length + ')</div>';
h += '<div style="display:flex;flex-wrap:wrap;gap:.3rem;margin-bottom:.5rem">';
d.common_upstreams.forEach(function(u) { var nameStr = (u.name && u.name !== 'AS' + u.asn) ? ' ' + escHtml(u.name) : ''; h += '<span class="badge badge-orange">' + asnLink(u.asn) + nameStr + '</span>'; });
h += '</div>';
} else if (d.common_upstreams) {
h += '<div style="font-size:.8rem;color:var(--muted);margin:.5rem 0">No common upstreams detected</div>';
}
h += '<div style="font-size:.8rem;font-weight:600;color:var(--green);margin:.5rem 0">Common IXPs (' + d.common_ixps.length + ')</div>';
if (d.common_ixps.length > 0) {
h += '<div style="display:flex;flex-wrap:wrap;gap:.3rem">';
d.common_ixps.forEach(function(ix) { h += '<span class="badge badge-green">' + escHtml(ix.name) + '</span>'; });
h += '</div>';
} else {
h += '<div style="font-size:.8rem;color:var(--muted)">No common IXPs</div>';
}
h += '<div style="font-size:.8rem;font-weight:600;color:var(--orange);margin:.5rem 0">Only AS' + d.asn1.asn + ' (' + d.only_asn1_ixps.length + ')</div>';
if (d.only_asn1_ixps.length > 0) {
h += '<div style="display:flex;flex-wrap:wrap;gap:.3rem">';
d.only_asn1_ixps.slice(0, 15).forEach(function(ix) { h += '<span class="badge badge-orange">' + escHtml(ix.name) + '</span>'; });
if (d.only_asn1_ixps.length > 15) h += '<span class="badge badge-orange">+' + (d.only_asn1_ixps.length - 15) + ' more</span>';
h += '</div>';
}
h += '<div style="font-size:.8rem;font-weight:600;color:var(--blue);margin:.5rem 0">Only AS' + d.asn2.asn + ' (' + d.only_asn2_ixps.length + ')</div>';
if (d.only_asn2_ixps.length > 0) {
h += '<div style="display:flex;flex-wrap:wrap;gap:.3rem">';
d.only_asn2_ixps.slice(0, 15).forEach(function(ix) { h += '<span class="badge badge-blue">' + escHtml(ix.name) + '</span>'; });
if (d.only_asn2_ixps.length > 15) h += '<span class="badge badge-blue">+' + (d.only_asn2_ixps.length - 15) + ' more</span>';
h += '</div>';
}
if (d.common_facilities && d.common_facilities.length > 0) {
h += '<div style="font-size:.8rem;font-weight:600;color:var(--cyan);margin:.5rem 0">Common Facilities (' + d.common_facilities.length + ')</div>';
h += '<div style="display:flex;flex-wrap:wrap;gap:.3rem">';
d.common_facilities.forEach(function(f) { h += '<span class="badge badge-cyan">' + escHtml(f.name) + '</span>'; });
h += '</div>';
}
h += '<div style="font-size:.7rem;color:var(--dim);margin-top:.5rem">Compared in ' + d.meta.duration_ms + 'ms</div>';
$('compareResults').innerHTML = h;
} catch (e) {
$('compareResults').innerHTML = '<div style="color:var(--red);font-size:.8rem">Compare failed: ' + escHtml(e.message) + '</div>';
}
}
function toggleExpand(el) {
const body = el.nextElementSibling;
const isOpen = body.classList.contains('open');
body.classList.toggle('open');
el.textContent = isOpen ? el.textContent.replace('Hide', 'Show') : el.textContent.replace('Show', 'Hide');
}
function escHtml(s) {
if (!s) return '';
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function escAttr(s) {
return escHtml(s);
}
async function loadAspaVerifyData(asn) {
$('aspaDeepContent').innerHTML = '<div class="section-loading">Running RFC-compliant ASPA verification...</div>';
try {
const resp = await fetch('/api/aspa/verify?asn=' + asn);
if (!resp.ok) { $('aspaDeepContent').textContent = 'ASPA verification unavailable (server ' + resp.status + ')'; return; }
var text = await resp.text();
if (!text || text[0] === '<') { $('aspaDeepContent').textContent = 'ASPA verification unavailable (timeout for large ASNs)'; return; }
const d = JSON.parse(text);
if (d.error) { $('aspaDeepContent').textContent = 'ASPA verification failed: ' + d.error; return; }
renderAspaDeep(d);
} catch (e) {
$('aspaDeepContent').textContent = 'ASPA verification failed: ' + e.message;
}
}
function renderAspaDeep(d) {
var h = '';
var score = d.readiness_score || { total: 0, breakdown: {} };
var pv = d.path_verification || {};
var audit = d.provider_audit || {};
// === READINESS SCORE GAUGE ===
h += '<div style="display:grid;grid-template-columns:200px 1fr;gap:2rem;align-items:start;margin-bottom:1.5rem">';
// Gauge
var scoreColor = score.total >= 75 ? 'var(--green)' : score.total >= 50 ? 'var(--orange)' : 'var(--red)';
var circumference = 2 * Math.PI * 54;
var offset = circumference - (score.total / 100) * circumference;
h += '<div>';
h += '<div class="aspa-gauge">';
h += '<svg viewBox="0 0 120 120"><circle class="aspa-gauge-bg" cx="60" cy="60" r="54"/>';
h += '<circle class="aspa-gauge-fill" cx="60" cy="60" r="54" stroke="' + scoreColor + '" stroke-dasharray="' + circumference.toFixed(1) + '" stroke-dashoffset="' + offset.toFixed(1) + '"/></svg>';
h += '<div class="aspa-gauge-text"><div class="aspa-gauge-score" style="color:' + scoreColor + '">' + score.total + '</div><div class="aspa-gauge-label">Readiness</div></div>';
h += '</div>';
// ASPA object status below gauge
h += '<div style="text-align:center;margin-top:.5rem">';
if (d.aspa_object_exists) {
h += '<span class="badge badge-green">ASPA Object Found</span>';
} else {
h += '<span class="badge badge-red">No ASPA Object</span>';
}
h += '</div>';
h += '</div>';
// Breakdown grid
h += '<div>';
h += '<div class="aspa-breakdown">';
var bd = score.breakdown || {};
var items = [
{ key: 'roa_coverage', label: 'ROA Coverage', suffix: '%' },
{ key: 'aspa_object', label: 'ASPA Object', suffix: '' },
{ key: 'provider_completeness', label: 'Provider Match', suffix: '%' },
{ key: 'path_validation', label: 'Path Validation', suffix: '%' },
];
items.forEach(function(item) {
var b = bd[item.key] || { score: 0, max: 25, value: 0 };
var pctFill = (b.score / b.max * 100).toFixed(0);
var color = pctFill >= 80 ? 'var(--green)' : pctFill >= 50 ? 'var(--orange)' : 'var(--red)';
var displayVal = item.key === 'aspa_object' ? (b.value ? 'Yes' : 'No') : b.value + item.suffix;
h += '<div class="aspa-breakdown-item">';
h += '<div class="aspa-breakdown-label">' + item.label + '</div>';
h += '<div class="aspa-breakdown-score" style="color:' + color + '">' + b.score + '<span style="color:var(--dim);font-size:.8rem">/' + b.max + '</span></div>';
h += '<div style="font-size:.7rem;color:var(--muted)">' + displayVal + '</div>';
h += '<div class="aspa-breakdown-bar"><div style="width:' + pctFill + '%;background:' + color + '"></div></div>';
h += '</div>';
});
h += '</div>';
h += '</div>';
h += '</div>';
// === PATH VERIFICATION SUMMARY ===
h += '<div style="display:flex;gap:1.5rem;flex-wrap:wrap;margin-bottom:1rem;padding:1rem;background:var(--bg);border-radius:8px;border:1px solid var(--border)">';
h += '<div class="stat"><div class="stat-val green">' + (pv.valid || 0) + '</div><div class="stat-label">Valid Paths</div></div>';
h += '<div class="stat"><div class="stat-val red">' + (pv.invalid || 0) + '</div><div class="stat-label">Invalid Paths</div></div>';
h += '<div class="stat"><div class="stat-val">' + (pv.unknown || 0) + '</div><div class="stat-label">Unknown Paths</div></div>';
h += '<div class="stat"><div class="stat-val orange">' + (pv.as_set_flagged || 0) + '</div><div class="stat-label">AS_SET Flagged</div></div>';
h += '<div class="stat"><div class="stat-val red">' + (pv.valley_detected || 0) + '</div><div class="stat-label">Valley Detected</div></div>';
h += '<div class="stat"><div class="stat-val cyan">' + (pv.total || 0) + '</div><div class="stat-label">Total Analyzed</div></div>';
h += '</div>';
// Valid percentage bar
if (pv.total > 0) {
var vPct = pct(pv.valid, pv.total);
var iPct = pct(pv.invalid, pv.total);
var uPct = 100 - vPct - iPct;
h += '<div style="font-size:.7rem;color:var(--muted);margin-bottom:.25rem">Path Validation: ' + vPct + '% valid, ' + iPct + '% invalid, ' + uPct + '% unknown</div>';
h += '<div class="progress-multi"><div style="width:' + vPct + '%;background:var(--green)"></div><div style="width:' + iPct + '%;background:var(--red)"></div><div style="width:' + uPct + '%;background:var(--dim)"></div></div>';
}
// === AS_SET WARNINGS ===
if (pv.as_set_flagged > 0) {
h += '<div class="asset-alert"><strong>AS_SET Deprecation Warning:</strong> ' + pv.as_set_flagged + ' path(s) contain AS_SET segments. AS_SET is deprecated per RFC 6472 and these paths are automatically marked Invalid per ASPA RFC.</div>';
// Show AS_SET paths detail
var asSetPaths = (pv.results || []).filter(function(r) { return r.has_as_set; });
if (asSetPaths.length > 0) {
h += '<div class="expand-toggle" onclick="toggleExpand(this)">Show AS_SET paths (' + asSetPaths.length + ')</div>';
h += '<div class="expand-body"><div class="scroll-wrap" style="max-height:200px"><table class="tbl"><thead><tr><th>Prefix</th><th>AS Path</th><th>RRC</th></tr></thead><tbody>';
asSetPaths.forEach(function(p) {
h += '<tr><td style="font-family:monospace;font-size:.7rem">' + escHtml(p.prefix || '') + '</td>';
h += '<td style="font-family:monospace;font-size:.65rem;color:var(--orange)">' + escHtml(p.path || '') + '</td>';
h += '<td>' + escHtml(p.rrc || '') + '</td></tr>';
});
h += '</tbody></table></div></div>';
}
}
// === VALLEY DETECTION ALERTS ===
if (pv.valley_detected > 0) {
var valleyPaths = (pv.results || []).filter(function(r) { return r.valleys && r.valleys.length > 0; });
h += '<div class="valley-alert"><strong>Valley Detection:</strong> ' + pv.valley_detected + ' path(s) show potential route leaks (up-down-up pattern).</div>';
if (valleyPaths.length > 0) {
h += '<div class="expand-toggle" onclick="toggleExpand(this)">Show valley details (' + valleyPaths.length + ' paths)</div>';
h += '<div class="expand-body"><div class="scroll-wrap" style="max-height:200px">';
valleyPaths.forEach(function(vp) {
vp.valleys.forEach(function(v) {
h += '<div style="padding:.4rem 0;border-bottom:1px solid var(--border);font-size:.75rem">';
h += '<span style="color:var(--red);font-weight:600">Valley:</span> ' + escHtml(v.description);
h += '<br><span style="color:var(--muted)">Path segment: ' + v.path_segment.join(' -> ') + '</span>';
h += '</div>';
});
});
h += '</div></div>';
}
}
// === PROVIDER AUDIT ===
h += '<div style="margin-top:1.25rem"><div style="font-size:.85rem;font-weight:600;color:var(--purple);margin-bottom:.5rem">Provider Audit</div>';
h += '<div style="display:flex;gap:1.5rem;flex-wrap:wrap;margin-bottom:.75rem">';
h += '<div><span style="font-size:.7rem;color:var(--muted);text-transform:uppercase">Detected</span><div style="font-size:1.3rem;font-weight:700;color:var(--blue)">' + (audit.detected_count || 0) + '</div></div>';
h += '<div><span style="font-size:.7rem;color:var(--muted);text-transform:uppercase">Declared</span><div style="font-size:1.3rem;font-weight:700;color:var(--cyan)">' + (audit.declared_count || 0) + '</div></div>';
h += '<div><span style="font-size:.7rem;color:var(--muted);text-transform:uppercase">Completeness</span><div style="font-size:1.3rem;font-weight:700;color:' + ((audit.completeness_pct || 0) >= 80 ? 'var(--green)' : 'var(--orange)') + '">' + (audit.completeness_pct || 0) + '%</div></div>';
h += '</div>';
// Missing from ASPA (collapsible, sortable)
if (audit.missing_from_aspa && audit.missing_from_aspa.length > 0) {
window._auditMissing = audit.missing_from_aspa;
window._auditMissingSort = 'frequency';
h += '<div style="display:flex;align-items:center;gap:.75rem;margin:.5rem 0 .3rem">';
h += '<span style="font-size:.8rem;font-weight:600;color:var(--orange)">Missing from ASPA Declaration (' + audit.missing_from_aspa.length + ')</span>';
h += '<span class="sort-toggle" onclick="window._auditMissingSort=window._auditMissingSort===\'frequency\'?\'asn\':\'frequency\';renderAuditList(\'missingListEl\',window._auditMissing,window._auditMissingSort,\'missing\')" title="Toggle sort order">Sort: <span id="missingSortLabel">by frequency</span> ↕</span>';
h += '</div>';
h += '<div id="missingListEl"></div>';
}
// Extra in ASPA (collapsible, sortable)
if (audit.extra_in_aspa && audit.extra_in_aspa.length > 0) {
window._auditExtra = audit.extra_in_aspa;
window._auditExtraSort = 'asn';
h += '<div style="display:flex;align-items:center;gap:.75rem;margin:.5rem 0 .3rem">';
h += '<span style="font-size:.8rem;font-weight:600;color:var(--cyan)">Extra in ASPA (not seen in paths) (' + audit.extra_in_aspa.length + ')</span>';
h += '<span class="sort-toggle" onclick="window._auditExtraSort=window._auditExtraSort===\'asn\'?\'name\':\'asn\';renderAuditList(\'extraListEl\',window._auditExtra,window._auditExtraSort,\'extra\')" title="Toggle sort order">Sort: <span id="extraSortLabel">by ASN</span> ↕</span>';
h += '</div>';
h += '<div id="extraListEl"></div>';
}
if ((!audit.missing_from_aspa || audit.missing_from_aspa.length === 0) && (!audit.extra_in_aspa || audit.extra_in_aspa.length === 0)) {
h += '<div class="audit-row audit-ok">Provider declarations match observed BGP paths</div>';
}
h += '</div>';
// === DETECTED PROVIDERS WITH FREQUENCY ===
if (d.detected_providers && d.detected_providers.length > 0) {
h += '<div style="margin-top:1.25rem"><div style="font-size:.85rem;font-weight:600;color:var(--blue);margin-bottom:.5rem">Detected Providers (by frequency)</div>';
h += '<div class="scroll-wrap" style="max-height:200px"><table class="tbl"><thead><tr><th>Provider</th><th>Name</th><th>Seen in Paths</th><th>Frequency</th></tr></thead><tbody>';
var sortedProviders = (d.detected_providers || []).slice().sort(function(a, b) { return (b.frequency || 0) - (a.frequency || 0) || a.asn - b.asn; });
sortedProviders.forEach(function(p) {
h += '<tr><td>' + asnLink(p.asn) + '</td>';
var provName = (p.name && p.name !== 'AS' + p.asn) ? escHtml(p.name) : '';
h += '<td>' + provName + '</td>';
h += '<td>' + (p.frequency || 0) + '</td>';
h += '<td><div style="display:flex;align-items:center;gap:.5rem"><div class="progress-wrap" style="flex:1;margin:0"><div class="progress-bar green" style="width:' + (p.frequency_pct || 0) + '%"></div></div><span style="font-size:.7rem;color:var(--muted)">' + (p.frequency_pct || 0) + '%</span></div></td>';
h += '</tr>';
});
h += '</tbody></table></div></div>';
}
// === PATH VERIFICATION DETAILS ===
if (pv.results && pv.results.length > 0) {
h += '<div style="margin-top:1.25rem">';
h += '<div class="expand-toggle" onclick="toggleExpand(this)">Show path verification details (' + pv.results.length + ' paths)</div>';
h += '<div class="expand-body"><div class="scroll-wrap" style="max-height:400px"><table class="tbl"><thead><tr><th>RRC</th><th>Prefix</th><th>Path</th><th>Upstream</th><th>Downstream</th><th>Overall</th><th style="min-width:120px">Hop Detail</th></tr></thead><tbody>';
pv.results.forEach(function(r) {
var oBadge = r.overall === 'Valid' ? 'path-valid' : r.overall === 'Invalid' ? 'path-invalid' : 'path-unknown';
var uBadge = r.upstream_verification.result === 'Valid' ? 'path-valid' : r.upstream_verification.result === 'Invalid' ? 'path-invalid' : 'path-unknown';
var dBadge = r.downstream_verification.result === 'Valid' ? 'path-valid' : r.downstream_verification.result === 'Invalid' ? 'path-invalid' : 'path-unknown';
h += '<tr>';
h += '<td style="font-size:.7rem">' + escHtml(r.rrc || '') + '</td>';
h += '<td style="font-family:monospace;font-size:.7rem">' + escHtml(r.prefix || '') + '</td>';
h += '<td style="font-family:monospace;font-size:.65rem;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escAttr(r.collapsed_path || r.path || '') + '">' + escHtml(r.collapsed_path || r.path || '') + '</td>';
h += '<td><span class="path-result-badge ' + uBadge + '">' + r.upstream_verification.result + '</span></td>';
h += '<td><span class="path-result-badge ' + dBadge + '">' + r.downstream_verification.result + '</span></td>';
h += '<td><span class="path-result-badge ' + oBadge + '">' + r.overall + '</span>';
if (r.has_as_set) h += '<br><span class="badge badge-orange" style="font-size:.6rem;margin-top:.2rem">AS_SET</span>';
if (r.valleys && r.valleys.length > 0) h += '<br><span class="badge badge-red" style="font-size:.6rem;margin-top:.2rem">VALLEY</span>';
h += '</td>';
// Hop detail
h += '<td class="hop-detail">';
var hops = r.upstream_verification.hops || [];
if (hops.length > 0) {
hops.forEach(function(hop, idx) {
var cls = hop.result === 'ProviderPlus' ? 'hop-pp' : hop.result === 'NotProviderPlus' ? 'hop-npp' : 'hop-na';
if (idx > 0) h += '<span class="hop-arrow">,</span>';
h += '<span class="' + cls + '" title="AS' + hop.from + '->AS' + hop.to + ': ' + hop.result + '">' + hop.result.charAt(0) + '</span>';
});
} else {
h += '<span style="color:var(--dim)">-</span>';
}
h += '</td>';
h += '</tr>';
});
h += '</tbody></table></div>';
h += '<div style="font-size:.65rem;color:var(--dim);margin-top:.5rem">Hop legend: <span class="hop-pp">P</span>=ProviderPlus, <span class="hop-npp">N</span>=NotProviderPlus, <span class="hop-na">N</span>=NoAttestation</div>';
h += '</div></div>';
}
h += '<div style="font-size:.7rem;color:var(--dim);margin-top:.75rem">ASPA verification completed in ' + (d.meta ? d.meta.duration_ms : '?') + 'ms | ' + (d.meta ? d.meta.paths_analyzed : '?') + ' of ' + (d.meta ? d.meta.total_paths_seen : '?') + ' paths analyzed</div>';
$('aspaDeepContent').innerHTML = h;
// Initial render of sortable audit lists
if (window._auditMissing && window._auditMissing.length > 0) {
renderAuditList('missingListEl', window._auditMissing, 'frequency', 'missing');
}
if (window._auditExtra && window._auditExtra.length > 0) {
renderAuditList('extraListEl', window._auditExtra, 'asn', 'extra');
}
}
// ============================================================
// Feature 5: Search History (localStorage)
// ============================================================
function loadSearchHistory() {
try {
return JSON.parse(localStorage.getItem('peercortex_history') || '[]');
} catch(e) { return []; }
}
function saveToHistory(asn, name) {
var history = loadSearchHistory();
// Remove existing entry for this ASN
history = history.filter(function(h) { return h.asn !== asn; });
// Add to front
history.unshift({ asn: asn, name: name || 'AS' + asn, ts: Date.now() });
// Keep only 10
history = history.slice(0, 10);
localStorage.setItem('peercortex_history', JSON.stringify(history));
renderSearchHistory();
}
function renderSearchHistory() {
var history = loadSearchHistory();
var el = $('searchHistory');
if (!el || history.length === 0) { if(el) el.innerHTML = ''; return; }
var h = '<span style="font-size:.7rem;color:var(--dim);margin-right:.3rem">Recent:</span>';
history.forEach(function(item) {
h += '<span class="history-badge" onclick="lookupAsn(' + item.asn + ')" title="' + escAttr(item.name) + '">AS' + item.asn + '</span>';
});
h += '<span class="history-clear" onclick="clearHistory()" title="Clear history">x clear</span>';
el.innerHTML = h;
}
function clearHistory() {
localStorage.removeItem('peercortex_history');
renderSearchHistory();
}
// ============================================================
// Feature 3: Prefix Detail View (modal)
// ============================================================
async function showPrefixDetail(prefix) {
// Create modal
var overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.onclick = function(e) { if (e.target === overlay) document.body.removeChild(overlay); };
var modal = document.createElement('div');
modal.className = 'modal-content';
modal.innerHTML = '<button class="modal-close" onclick="this.closest(\'\'.modal-overlay\'\').remove()">&times;</button>' +
'<div class="modal-title">Prefix Detail: ' + escHtml(prefix) + '</div>' +
'<div class="section-loading">Loading prefix details...</div>';
overlay.appendChild(modal);
document.body.appendChild(overlay);
try {
var resp = await fetch('/api/prefix/detail?prefix=' + encodeURIComponent(prefix));
var d = await resp.json();
if (d.error) {
modal.innerHTML = '<button class="modal-close" onclick="this.closest(\'.modal-overlay\').remove()">&times;</button>' +
'<div class="modal-title">Prefix Detail: ' + escHtml(prefix) + '</div>' +
'<div style="color:var(--red)">' + escHtml(d.error) + '</div>';
return;
}
var h = '<button class="modal-close" onclick="this.closest(\'.modal-overlay\').remove()">&times;</button>';
h += '<div class="modal-title">Prefix Detail: ' + escHtml(prefix) + '</div>';
// Origins
if (d.origins && d.origins.length > 0) {
h += '<div style="margin-bottom:.75rem"><span style="font-size:.75rem;color:var(--muted);text-transform:uppercase">Origin AS:</span> ';
d.origins.forEach(function(o) { h += asnLink(o.asn) + ' '; });
h += '</div>';
}
// RPKI
h += '<div style="margin-bottom:.75rem"><span style="font-size:.75rem;color:var(--muted);text-transform:uppercase">RPKI Status:</span> ';
var rpkiCls = d.rpki.status === 'valid' ? 'badge-green' : d.rpki.status === 'invalid' ? 'badge-red' : 'badge-orange';
h += '<span class="badge ' + rpkiCls + '">' + escHtml(d.rpki.status) + '</span>';
h += ' <span style="font-size:.75rem;color:var(--muted)">(' + d.rpki.validating_roas + ' ROAs)</span>';
h += '</div>';
// IRR
h += '<div style="margin-bottom:.75rem"><span style="font-size:.75rem;color:var(--muted);text-transform:uppercase">IRR Status:</span> ';
h += '<span class="badge ' + (d.irr_status === 'found' ? 'badge-green' : 'badge-orange') + '">' + escHtml(d.irr_status) + '</span></div>';
// Visibility
h += '<div style="margin-bottom:.75rem"><span style="font-size:.75rem;color:var(--muted);text-transform:uppercase">Visibility:</span> ';
h += '<span style="font-weight:600;color:var(--blue)">' + (d.visibility.ris_peers_seeing || 0) + '</span>';
h += ' <span style="color:var(--muted)">/ ' + (d.visibility.total_probes || 0) + ' RIS peers</span></div>';
// First seen
if (d.first_seen) {
h += '<div style="margin-bottom:.75rem"><span style="font-size:.75rem;color:var(--muted);text-transform:uppercase">First Seen:</span> ';
h += '<span style="color:var(--text-dim)">' + escHtml(d.first_seen) + '</span></div>';
}
h += '<div style="font-size:.7rem;color:var(--dim);margin-top:.75rem">Queried in ' + d.meta.duration_ms + 'ms</div>';
modal.innerHTML = h;
} catch(e) {
modal.innerHTML = '<button class="modal-close" onclick="this.closest(\'.modal-overlay\').remove()">&times;</button>' +
'<div class="modal-title">Prefix Detail</div>' +
'<div style="color:var(--red)">Error: ' + escHtml(e.message) + '</div>';
}
}
// ============================================================
// Feature 4: IX Detail View (modal)
// ============================================================
async function showIXDetail(ixId, ixName) {
var overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.onclick = function(e) { if (e.target === overlay) document.body.removeChild(overlay); };
var modal = document.createElement('div');
modal.className = 'modal-content';
modal.innerHTML = '<button class="modal-close" onclick="this.closest(\'.modal-overlay\').remove()">&times;</button>' +
'<div class="modal-title">IX Detail: ' + escHtml(ixName || 'IX ' + ixId) + '</div>' +
'<div class="section-loading">Loading IX details...</div>';
overlay.appendChild(modal);
document.body.appendChild(overlay);
try {
var resp = await fetch('/api/ix/detail?ix_id=' + ixId);
var d = await resp.json();
if (d.error) {
modal.innerHTML = '<button class="modal-close" onclick="this.closest(\'.modal-overlay\').remove()">&times;</button>' +
'<div class="modal-title">IX Detail</div><div style="color:var(--red)">' + escHtml(d.error) + '</div>';
return;
}
var h = '<button class="modal-close" onclick="this.closest(\'.modal-overlay\').remove()">&times;</button>';
h += '<div class="modal-title">' + escHtml(d.ix.name) + '</div>';
if (d.ix.city || d.ix.country) {
h += '<div style="margin-bottom:.5rem;font-size:.85rem;color:var(--muted)">' + escHtml(d.ix.city) + (d.ix.country ? ', ' + escHtml(d.ix.country) : '') + '</div>';
}
h += '<div style="display:flex;gap:1.5rem;margin-bottom:1rem;flex-wrap:wrap">';
h += '<div class="stat"><div class="stat-val green">' + d.total_members + '</div><div class="stat-label">Total Members</div></div>';
h += '</div>';
if (d.ix.website) {
h += '<div style="margin-bottom:.5rem"><a href="' + escAttr(d.ix.website) + '" target="_blank" style="font-size:.8rem">' + escHtml(d.ix.website) + '</a></div>';
}
h += '<div style="margin-bottom:1rem"><a href="' + escAttr(d.ix.peeringdb_url) + '" target="_blank" class="ext-link" style="font-size:.8rem">View on PeeringDB</a></div>';
// Top members by speed
if (d.top_members_by_speed && d.top_members_by_speed.length > 0) {
h += '<div style="font-size:.8rem;font-weight:600;color:var(--purple);margin-bottom:.5rem">Top Members by Speed</div>';
h += '<div class="scroll-wrap" style="max-height:300px"><table class="tbl"><thead><tr><th>ASN</th><th>Name</th><th>Speed</th></tr></thead><tbody>';
d.top_members_by_speed.forEach(function(m) {
h += '<tr><td>' + asnLink(m.asn) + '</td><td>' + escHtml(m.name) + '</td><td>' + escHtml(m.speed_display) + '</td></tr>';
});
h += '</tbody></table></div>';
}
h += '<div style="font-size:.7rem;color:var(--dim);margin-top:.75rem">Queried in ' + d.meta.duration_ms + 'ms</div>';
modal.innerHTML = h;
} catch(e) {
modal.innerHTML = '<button class="modal-close" onclick="this.closest(\'.modal-overlay\').remove()">&times;</button>' +
'<div class="modal-title">IX Detail</div><div style="color:var(--red)">Error: ' + escHtml(e.message) + '</div>';
}
}
// ============================================================
// Feature 8: ASPA Change Alerting
// ============================================================
function checkAspaChanges(asn, currentProviders) {
var key = 'peercortex_aspa_' + asn;
var prev = null;
try { prev = JSON.parse(localStorage.getItem(key)); } catch(e) {}
var card = $('aspaAlertCard');
if (!card) return;
card.classList.remove('hidden');
var h = '';
var now = new Date().toISOString();
if (prev && prev.providers) {
var prevSet = new Set(prev.providers.map(function(p) { return p.asn; }));
var currSet = new Set(currentProviders.map(function(p) { return p.asn; }));
var added = currentProviders.filter(function(p) { return !prevSet.has(p.asn); });
var removed = prev.providers.filter(function(p) { return !currSet.has(p.asn); });
h += '<div style="font-size:.8rem;color:var(--muted);margin-bottom:.75rem">Last checked: ' + escHtml(prev.timestamp || 'unknown') + '</div>';
h += '<div style="font-size:.8rem;color:var(--muted);margin-bottom:.75rem">Current check: ' + escHtml(now) + '</div>';
if (added.length === 0 && removed.length === 0) {
h += '<div style="font-size:.85rem;color:var(--green);font-weight:600">No provider changes detected since last check.</div>';
} else {
if (added.length > 0) {
h += '<div style="font-size:.8rem;font-weight:600;color:var(--green);margin:.5rem 0 .3rem">New Providers Detected (' + added.length + ')</div>';
added.forEach(function(p) {
h += '<div class="audit-row" style="color:var(--green)">+ ' + asnLink(p.asn) + ' ' + escHtml(p.name || '') + '</div>';
});
}
if (removed.length > 0) {
h += '<div style="font-size:.8rem;font-weight:600;color:var(--red);margin:.5rem 0 .3rem">Removed Providers (' + removed.length + ')</div>';
removed.forEach(function(p) {
h += '<div class="audit-row" style="color:var(--red)">- ' + asnLink(p.asn) + ' ' + escHtml(p.name || '') + '</div>';
});
}
}
} else {
h += '<div style="font-size:.8rem;color:var(--muted);margin-bottom:.5rem">First check: ' + escHtml(now) + '</div>';
h += '<div style="font-size:.85rem;color:var(--muted)">No previous data. Provider snapshot saved for future comparison.</div>';
}
// Save current state
localStorage.setItem(key, JSON.stringify({
providers: currentProviders,
timestamp: now
}));
$('aspaAlertContent').innerHTML = h;
}
// ============================================================
// Feature 9: Provider Relationship Graph (SVG)
// ============================================================
function renderProviderGraph(asn, providers) {
var graphCard = $('providerGraphCard');
if (!providers || providers.length === 0) { graphCard.classList.add('hidden'); return; }
graphCard.classList.remove('hidden');
var tier1List = [174, 209, 701, 1239, 1299, 2914, 3257, 3320, 3356, 5511, 6453, 6461, 6762, 6830, 7018, 12956];
var tier1 = providers.filter(function(p) { return tier1List.indexOf(p.asn) >= 0; });
var transit = providers.filter(function(p) { return tier1List.indexOf(p.asn) < 0 && (p.frequency_pct || 0) >= 20; });
var peers = providers.filter(function(p) { return tier1List.indexOf(p.asn) < 0 && (p.frequency_pct || 0) < 20; });
tier1.sort(function(a,b) { return (b.frequency_pct||0) - (a.frequency_pct||0); });
transit.sort(function(a,b) { return (b.frequency_pct||0) - (a.frequency_pct||0); });
peers.sort(function(a,b) { return (b.frequency_pct||0) - (a.frequency_pct||0); });
function provCard(p, color, label) {
var n = escHtml(p.name || '');
var f = p.frequency_pct ? p.frequency_pct.toFixed(0) + '%' : '';
return '<div onclick="lookupAsn(' + p.asn + ')" style="cursor:pointer;background:' + color + '08;border:1px solid ' + color + '30;border-radius:10px;padding:.65rem .85rem;display:flex;align-items:center;gap:.65rem;transition:all .15s" onmouseenter="this.style.transform=\'translateY(-1px)\';this.style.borderColor=\'' + color + '\'" onmouseleave="this.style.transform=\'none\';this.style.borderColor=\'' + color + '30\'">' +
'<div style="width:36px;height:36px;border-radius:50%;background:' + color + '18;border:2px solid ' + color + '60;display:flex;align-items:center;justify-content:center;flex-shrink:0"><span style="font-size:.6rem;font-weight:800;color:' + color + '">' + label + '</span></div>' +
'<div style="flex:1;min-width:0"><div style="font-weight:700;font-size:.85rem;color:#e2e8f0">AS' + p.asn + '</div><div style="font-size:.72rem;color:#94a3b8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + n + '</div></div>' +
(f ? '<div style="font-size:.72rem;font-weight:700;color:' + color + ';padding:.15rem .45rem;border-radius:5px;background:' + color + '12;flex-shrink:0">' + f + '</div>' : '') +
'</div>';
}
var h = '';
h += '<div style="text-align:center;margin-bottom:1.5rem"><div style="display:inline-flex;align-items:center;gap:.75rem;background:linear-gradient(135deg,#5b21b6,#7c3aed);padding:.6rem 1.75rem;border-radius:14px;border:2px solid #8b5cf680"><span style="font-size:1.1rem;font-weight:800;color:#fff">AS' + asn + '</span><span style="font-size:.65rem;color:#c4b5fd;text-transform:uppercase;letter-spacing:2px">Target</span></div></div>';
function section(items, color, title, label, limit) {
if (!items.length) return '';
var s = '<div style="margin-bottom:1.25rem"><div style="font-size:.7rem;font-weight:700;color:' + color + ';text-transform:uppercase;letter-spacing:.08em;margin-bottom:.5rem;display:flex;align-items:center;gap:.4rem"><span style="width:7px;height:7px;border-radius:50%;background:' + color + '"></span> ' + title + ' (' + items.length + ')</div>';
s += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:.4rem">';
var show = items.slice(0, limit);
show.forEach(function(p) { s += provCard(p, color, label); });
s += '</div>';
if (items.length > limit) {
var moreId = 'pg_more_' + label;
s += '<div class="show-more-btn" onclick="var el=document.getElementById(\'' + moreId + '\');if(el.style.display===\'none\'){el.style.display=\'grid\';this.textContent=\'Hide\';}else{el.style.display=\'none\';this.textContent=\'Show ' + (items.length - limit) + ' more...\';}">Show ' + (items.length - limit) + ' more...</div>';
s += '<div id="' + moreId + '" style="display:none;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:.4rem">';
items.slice(limit).forEach(function(p) { s += provCard(p, color, label); });
s += '</div>';
}
s += '</div>';
return s;
}
h += section(tier1, '#fbbf24', 'Tier 1 Providers', 'T1', 20);
h += section(transit, '#60a5fa', 'Transit Providers', 'TR', 12);
h += section(peers, '#4ade80', 'IX / Peers', 'IX', 12);
h += '<div style="font-size:.7rem;color:var(--dim);text-align:center;margin-top:.75rem">' + providers.length + ' providers total (Tier 1: ' + tier1.length + ' \u00b7 Transit: ' + transit.length + ' \u00b7 IX/Peer: ' + peers.length + ')</div>';
$('providerGraphContent').innerHTML = h;
}
// ============================================================
// Feature 2: Full Compare UI
// ============================================================
async function doFullCompare() {
if (!currentAsn) return;
var raw2 = $('compareAsn').value.trim().replace(/[^0-9]/g, '');
if (!raw2) return;
var panel = $('fullComparePanel');
panel.classList.remove('hidden');
panel.innerHTML = '<div class="card full"><div class="section-loading">Loading full comparison...</div></div>';
try {
var resp = await fetch('/api/compare?asn1=' + currentAsn + '&asn2=' + raw2);
var d = await resp.json();
if (d.error) {
panel.innerHTML = '<div class="card full"><div style="color:var(--red)">' + escHtml(d.error) + '</div></div>';
return;
}
renderFullCompare(d);
} catch(e) {
panel.innerHTML = '<div class="card full"><div style="color:var(--red)">Compare failed: ' + escHtml(e.message) + '</div></div>';
}
}
function renderFullCompare(d) {
var panel = $('fullComparePanel');
var h = '';
// Header card with both networks
h += '<div class="card full">';
h += '<div class="card-title"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 20V10M12 20V4M6 20v-6"/></svg> Network Comparison</div>';
// Side-by-side metrics
h += '<div class="compare-grid">';
// ASN1 column
h += '<div class="compare-col">';
h += '<div class="compare-col-title"><span style="color:var(--blue);font-size:1.2rem;font-weight:700">' + escHtml(d.asn1.name) + '</span></div>';
h += '<div style="font-size:.85rem;color:var(--muted);margin-bottom:.75rem">AS' + d.asn1.asn + '</div>';
h += '<div class="compare-metric"><span class="compare-metric-label">IXPs</span><span class="compare-metric-val" style="color:var(--blue)">' + d.asn1.ix_count + '</span></div>';
h += '<div class="compare-metric"><span class="compare-metric-label">Facilities</span><span class="compare-metric-val" style="color:var(--blue)">' + d.asn1.fac_count + '</span></div>';
h += '<div class="compare-metric"><span class="compare-metric-label">Upstreams</span><span class="compare-metric-val" style="color:var(--blue)">' + d.asn1.upstream_count + '</span></div>';
var r1cls = d.asn1.rpki_coverage >= 90 ? 'green' : d.asn1.rpki_coverage >= 70 ? 'orange' : 'red';
h += '<div class="compare-metric"><span class="compare-metric-label">RPKI Coverage</span><span class="compare-metric-val" style="color:var(--' + r1cls + ')">' + d.asn1.rpki_coverage + '%</span></div>';
h += '</div>';
// ASN2 column
h += '<div class="compare-col">';
h += '<div class="compare-col-title"><span style="color:var(--purple);font-size:1.2rem;font-weight:700">' + escHtml(d.asn2.name) + '</span></div>';
h += '<div style="font-size:.85rem;color:var(--muted);margin-bottom:.75rem">AS' + d.asn2.asn + '</div>';
h += '<div class="compare-metric"><span class="compare-metric-label">IXPs</span><span class="compare-metric-val" style="color:var(--purple)">' + d.asn2.ix_count + '</span></div>';
h += '<div class="compare-metric"><span class="compare-metric-label">Facilities</span><span class="compare-metric-val" style="color:var(--purple)">' + d.asn2.fac_count + '</span></div>';
h += '<div class="compare-metric"><span class="compare-metric-label">Upstreams</span><span class="compare-metric-val" style="color:var(--purple)">' + d.asn2.upstream_count + '</span></div>';
var r2cls = d.asn2.rpki_coverage >= 90 ? 'green' : d.asn2.rpki_coverage >= 70 ? 'orange' : 'red';
h += '<div class="compare-metric"><span class="compare-metric-label">RPKI Coverage</span><span class="compare-metric-val" style="color:var(--' + r2cls + ')">' + d.asn2.rpki_coverage + '%</span></div>';
h += '</div>';
h += '</div>'; // end compare-grid
// Common IXPs
h += '<div style="margin-top:1.5rem">';
h += '<div style="font-size:.85rem;font-weight:600;color:var(--green);margin-bottom:.5rem">Common IXPs (' + d.common_ixps.length + ')</div>';
if (d.common_ixps.length > 0) {
h += '<div style="display:flex;flex-wrap:wrap;gap:.3rem;margin-bottom:.75rem">';
d.common_ixps.forEach(function(ix) { h += '<span class="badge badge-green">' + escHtml(ix.name) + '</span>'; });
h += '</div>';
} else {
h += '<div style="font-size:.8rem;color:var(--muted);margin-bottom:.75rem">No common IXPs</div>';
}
// Only in ASN1
h += '<div style="font-size:.85rem;font-weight:600;color:var(--blue);margin-bottom:.5rem">Only in AS' + d.asn1.asn + ' (' + d.only_asn1_ixps.length + ')</div>';
if (d.only_asn1_ixps.length > 0) {
h += '<div style="display:flex;flex-wrap:wrap;gap:.3rem;margin-bottom:.75rem">';
d.only_asn1_ixps.slice(0, 30).forEach(function(ix) { h += '<span class="badge badge-blue">' + escHtml(ix.name) + '</span>'; });
if (d.only_asn1_ixps.length > 30) h += '<span class="badge badge-blue">+' + (d.only_asn1_ixps.length - 30) + ' more</span>';
h += '</div>';
}
// Only in ASN2
h += '<div style="font-size:.85rem;font-weight:600;color:var(--purple);margin-bottom:.5rem">Only in AS' + d.asn2.asn + ' (' + d.only_asn2_ixps.length + ')</div>';
if (d.only_asn2_ixps.length > 0) {
h += '<div style="display:flex;flex-wrap:wrap;gap:.3rem;margin-bottom:.75rem">';
d.only_asn2_ixps.slice(0, 30).forEach(function(ix) { h += '<span class="badge badge-purple">' + escHtml(ix.name) + '</span>'; });
if (d.only_asn2_ixps.length > 30) h += '<span class="badge badge-purple">+' + (d.only_asn2_ixps.length - 30) + ' more</span>';
h += '</div>';
}
// Common upstreams
if (d.common_upstreams && d.common_upstreams.length > 0) {
h += '<div style="font-size:.85rem;font-weight:600;color:var(--orange);margin-bottom:.5rem">Common Upstreams (' + d.common_upstreams.length + ')</div>';
h += '<div style="display:flex;flex-wrap:wrap;gap:.3rem;margin-bottom:.75rem">';
d.common_upstreams.forEach(function(u) { h += '<span class="badge badge-orange">' + asnLink(u.asn) + ' ' + escHtml(u.name || '') + '</span>'; });
h += '</div>';
}
// Common facilities
if (d.common_facilities && d.common_facilities.length > 0) {
h += '<div style="font-size:.85rem;font-weight:600;color:var(--cyan);margin-bottom:.5rem">Common Facilities (' + d.common_facilities.length + ')</div>';
h += '<div style="display:flex;flex-wrap:wrap;gap:.3rem;margin-bottom:.75rem">';
d.common_facilities.forEach(function(f) { h += '<span class="badge badge-cyan">' + escHtml(f.name) + '</span>'; });
h += '</div>';
}
h += '</div>';
h += '<div style="font-size:.7rem;color:var(--dim);margin-top:.75rem">Compared in ' + d.meta.duration_ms + 'ms</div>';
h += '</div>'; // end card
panel.innerHTML = h;
// Scroll into view
panel.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// ============================================================
// Feature 24: Routing Overview (enhanced bgp.he.net + RIPE visibility)
// ============================================================
function renderRoutingOverview(heData, routing) {
var hasHe = heData && (heData.peer_count || heData.prefixes_v4 || heData.prefixes_v6 || heData.country);
var hasRouting = routing && (routing.ipv4_prefixes > 0 || routing.ipv6_prefixes > 0);
if (!hasHe && !hasRouting) { $('bgpHeCard').classList.add('hidden'); return; }
$('bgpHeCard').classList.remove('hidden');
var r = routing || {};
var he = heData || {};
var v4p = r.ipv4_prefixes || he.prefixes_v4 || 0;
var v6p = r.ipv6_prefixes || he.prefixes_v6 || 0;
var peers = he.peer_count || 0;
var h = '';
// Top stat cards
h += '<div class="routing-stats-row">';
h += '<div class="routing-stat-card"><div class="routing-stat-val" style="color:var(--green)">' + v4p + '</div><div class="routing-stat-label">IPv4 Prefixes</div></div>';
h += '<div class="routing-stat-card"><div class="routing-stat-val" style="color:var(--purple)">' + v6p + '</div><div class="routing-stat-label">IPv6 Prefixes</div></div>';
if (peers > 0) {
h += '<div class="routing-stat-card"><div class="routing-stat-val" style="color:var(--cyan)">' + peers + '</div><div class="routing-stat-label">Observed Peers</div></div>';
}
h += '</div>';
// Propagation bars
function propColor(pct) { return pct >= 90 ? 'green' : pct >= 70 ? 'orange' : 'red'; }
function propBar(label, pct, totalPeers, pfxCount) {
if (pfxCount <= 0) return '';
var seenBy = Math.round(pct / 100 * totalPeers);
var barId = 'prop-' + label.replace(/\s/g,'').toLowerCase() + '-' + Math.random().toString(36).substr(2,5);
var s = '<div class="prop-section">';
s += '<div class="prop-label">' + label + '</div>';
s += '<div class="prop-bar-wrap">';
s += '<div class="prop-bar"><div class="prop-fill ' + propColor(pct) + '" id="' + barId + '" data-width="' + Math.min(pct, 100) + '%"></div></div>';
s += '<div class="prop-pct" style="color:var(--' + propColor(pct) + ')">' + pct.toFixed(1) + '%</div>';
s += '</div>';
if (totalPeers > 0) {
s += '<div class="prop-detail">Seen by ' + seenBy + ' of ' + totalPeers + ' RIS peers</div>';
}
s += '</div>';
return s;
}
if (r.ipv4_visibility_avg > 0 || r.ipv6_visibility_avg > 0) {
h += propBar('IPv4 Route Propagation', r.ipv4_visibility_avg || 0, r.total_ris_peers_v4 || 0, v4p);
h += propBar('IPv6 Route Propagation', r.ipv6_visibility_avg || 0, r.total_ris_peers_v6 || 0, v6p);
}
// Prefix distribution
var psv4 = (r.prefix_sizes_v4 || []);
var psv6 = (r.prefix_sizes_v6 || []);
if (psv4.length > 0 || psv6.length > 0) {
h += '<div class="prefix-dist">';
h += '<div class="prefix-dist-label">Prefix Distribution</div>';
h += '<div class="prefix-badges">';
if (psv4.length > 0) {
psv4.forEach(function(p) {
h += '<span class="prefix-badge" style="border-color:var(--green);color:var(--green)">/' + p.size + ' \u00d7' + p.count + '</span>';
});
}
if (psv6.length > 0) {
psv6.forEach(function(p) {
h += '<span class="prefix-badge" style="border-color:var(--purple);color:var(--purple)">/' + p.size + ' \u00d7' + p.count + '</span>';
});
}
h += '</div></div>';
}
// Footer: country + links
h += '<div class="routing-footer">';
h += '<div class="routing-footer-left">';
if (he.country) {
h += '<span>' + escHtml(he.country) + '</span>';
}
if (he.irr_record) {
h += '<span style="color:var(--dim)">|</span><span style="color:var(--muted)">IRR: ' + escHtml(he.irr_record) + '</span>';
}
if (he.looking_glass) {
h += '<span style="color:var(--dim)">|</span><a class="ext-link" href="' + escAttr(he.looking_glass) + '" target="_blank">Looking Glass</a>';
}
h += '</div>';
if (he.source_url) {
h += '<a class="ext-link" href="' + escAttr(he.source_url) + '" target="_blank">View on bgp.he.net \u2192</a>';
}
h += '</div>';
$('bgpHeContent').innerHTML = h;
// Animate propagation bars after render
setTimeout(function() {
var fills = document.querySelectorAll('.prop-fill[data-width]');
fills.forEach(function(el) { el.style.width = el.getAttribute('data-width'); });
}, 50);
}
// Keep backward-compat alias
function renderBgpHeNet(data) {
renderRoutingOverview(data, null);
}
// ============================================================
// Feature 26: IX Traffic Stats
// ============================================================
function renderIxTrafficStats(ixConnections) {
var totalSpeed = (ixConnections || []).reduce(function(sum, c) { return sum + (c.speed_mbps || 0); }, 0);
var decixConns = (ixConnections || []).filter(function(c) { return (c.ix_name || '').toLowerCase().indexOf('de-cix') >= 0; });
var h = '';
if (totalSpeed > 0 || decixConns.length > 0) {
h += '<div class="ix-traffic-stats">';
h += '<div class="ix-traffic-stat"><div class="ix-traffic-val">' + fmtSpeed(totalSpeed) + '</div><div class="ix-traffic-label">Total IX Capacity</div></div>';
if (decixConns.length > 0) {
var decixSpeed = decixConns.reduce(function(sum, c) { return sum + (c.speed_mbps || 0); }, 0);
h += '<div class="ix-traffic-stat"><div class="ix-traffic-val">' + decixConns.length + '</div><div class="ix-traffic-label">DE-CIX Ports</div></div>';
h += '<div class="ix-traffic-stat"><div class="ix-traffic-val">' + fmtSpeed(decixSpeed) + '</div><div class="ix-traffic-label">DE-CIX Capacity</div></div>';
}
var ixByName = {};
(ixConnections || []).forEach(function(c) {
var name = c.ix_name || 'Unknown';
if (!ixByName[name]) ixByName[name] = { name: name, total_speed: 0, ports: 0 };
ixByName[name].total_speed += c.speed_mbps || 0;
ixByName[name].ports++;
});
var topIx = Object.values(ixByName).sort(function(a, b) { return b.total_speed - a.total_speed; }).slice(0, 5);
if (topIx.length > 0) {
h += '<div class="ix-traffic-stat" style="flex:1;min-width:200px"><div class="ix-traffic-label" style="text-align:left;margin-bottom:.3rem">Top IXPs by Capacity</div>';
topIx.forEach(function(ix) {
var pctOfTotal = totalSpeed > 0 ? Math.round((ix.total_speed / totalSpeed) * 100) : 0;
h += '<div style="display:flex;align-items:center;gap:.5rem;font-size:.75rem;margin-bottom:.2rem">';
h += '<span style="color:var(--text-dim);min-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml(ix.name) + '</span>';
h += '<div class="progress-wrap" style="flex:1;margin:0;height:6px"><div class="progress-bar blue" style="width:' + pctOfTotal + '%"></div></div>';
h += '<span style="color:var(--muted);min-width:60px;text-align:right">' + fmtSpeed(ix.total_speed) + '</span>';
h += '</div>';
});
h += '</div>';
}
h += '</div>';
}
return h;
}
// ============================================================
// Feature 27: WHOIS rendering
// ============================================================
async function loadWhoisData(asn) {
$('whoisContent').innerHTML = '<div class="section-loading">Loading WHOIS data...</div>';
try {
var resp = await fetch('/api/whois?resource=AS' + asn);
var d = await resp.json();
if (d.error) { $('whoisContent').innerHTML = '<div style="color:var(--orange);font-size:.85rem">WHOIS: ' + escHtml(d.error) + '</div>'; return; }
renderWhois(d);
} catch (e) {
$('whoisContent').innerHTML = '<div style="color:var(--red);font-size:.85rem">WHOIS lookup failed: ' + escHtml(e.message) + '</div>';
}
}
function renderWhois(d) {
var h = '';
var data = d.data;
if (!data) { $('whoisContent').innerHTML = '<div style="color:var(--muted);font-size:.85rem">No WHOIS data found for ' + escHtml(d.resource) + '</div>'; return; }
if (Array.isArray(data)) { if (data.length === 0) { $('whoisContent').innerHTML = '<div style="color:var(--muted)">No results.</div>'; return; } data = data[0]; }
h += '<div style="font-size:.8rem;color:var(--muted);margin-bottom:.75rem">Source: RIPE DB | Type: ' + escHtml(d.type || 'unknown') + '</div>';
h += '<div class="expand-toggle" onclick="toggleExpand(this)">Show WHOIS details</div>';
h += '<div class="expand-body"><div class="whois-grid">';
var fields = [
['aut-num / inetnum', data.aut_num || data.inetnum || ''],
['AS Name / Netname', data.as_name || data.netname || ''],
['Description', (data.descr || []).join(', ')],
['Organisation', data.org || ''],
['Country', data.country || ''],
['Admin-C', (data.admin_c || []).join(', ')],
['Tech-C', (data.tech_c || []).join(', ')],
['Maintained By', (data.mnt_by || []).join(', ')],
['Status', data.status || ''],
['Created', data.created || ''],
['Last Modified', data.last_modified || ''],
['Source', data.source || ''],
];
fields.forEach(function(f) {
if (f[1]) { h += '<div class="whois-key">' + escHtml(f[0]) + '</div><div class="whois-val">' + escHtml(f[1]) + '</div>'; }
});
h += '</div>';
if (data.import && data.import.length > 0) {
h += '<div style="margin-top:.75rem;font-size:.8rem;font-weight:600;color:var(--orange)">Import Policy</div>';
h += '<div style="font-family:monospace;font-size:.7rem;color:var(--text-dim);max-height:150px;overflow-y:auto">';
data.import.forEach(function(imp) { h += escHtml(imp) + '<br>'; });
h += '</div>';
}
if (data.export && data.export.length > 0) {
h += '<div style="margin-top:.5rem;font-size:.8rem;font-weight:600;color:var(--cyan)">Export Policy</div>';
h += '<div style="font-family:monospace;font-size:.7rem;color:var(--text-dim);max-height:150px;overflow-y:auto">';
data.export.forEach(function(exp) { h += escHtml(exp) + '<br>'; });
h += '</div>';
}
if (data.remarks && data.remarks.length > 0) {
h += '<div style="margin-top:.5rem;font-size:.8rem;font-weight:600;color:var(--muted)">Remarks</div>';
h += '<div style="font-family:monospace;font-size:.7rem;color:var(--dim);max-height:150px;overflow-y:auto">';
data.remarks.forEach(function(r) { h += escHtml(r) + '<br>'; });
h += '</div>';
}
h += '</div>';
h += '<div style="font-size:.7rem;color:var(--dim);margin-top:.5rem">Queried in ' + (d.meta ? d.meta.duration_ms : '?') + 'ms</div>';
$('whoisContent').innerHTML = h;
}
$('asnInput').addEventListener('keydown', function(e) {
if (e.key === 'Enter') doLookup();
});
(function() {
renderSearchHistory();
const params = new URLSearchParams(window.location.search);
const asn = params.get('asn');
if (asn) {
$('asnInput').value = asn;
doLookup();
}
})();
async function loadHealthReport(asn) {
$('healthContent').innerHTML = '<div class="section-loading">Running comprehensive validation (13 checks)...</div>';
try {
var resp = await fetch('/api/validate?asn=' + asn);
var d = await resp.json();
if (d.error) {
$('healthContent').innerHTML = '<div style="color:var(--red);font-size:.85rem">Validation failed: ' + escHtml(d.error) + '</div>';
return;
}
renderHealthReport(d);
} catch (e) {
$('healthContent').innerHTML = '<div style="color:var(--red);font-size:.85rem">Validation failed: ' + escHtml(e.message) + '</div>';
}
}
function buildHealthTooltip(key, v, info) {
var t = '';
var isFail = v.status === 'fail' || v.status === 'warning';
// What was checked
t += '<div class="tt-section"><span class="tt-label">Checked: </span><span class="tt-value">';
if (key === 'bogon') {
t += 'Scanned ' + (v.total_prefixes_checked || 0) + ' prefixes against RFC 1918/5737/6598 bogon ranges and reserved ASN lists.';
t += '</span></div>';
t += '<div class="tt-section"><span class="tt-label">Result: </span><span class="tt-value">';
if (v.bogon_prefixes && v.bogon_prefixes.length > 0) {
t += v.bogon_prefixes.length + ' bogon prefix(es) found: ' + v.bogon_prefixes.map(function(b){return b.prefix}).join(', ');
} else { t += 'No bogon prefixes detected.'; }
if (v.bogon_asns_in_paths && v.bogon_asns_in_paths.length > 0) { t += ' Bogon ASNs in paths: ' + v.bogon_asns_in_paths.join(', '); }
t += '</span></div>';
t += '<div class="tt-section"><span class="tt-label">Why it matters: </span><span class="tt-value">Bogon announcements indicate misconfiguration or prefix hijacking.</span></div>';
if (isFail) t += '<div class="tt-section tt-fix">Fix: Remove bogon prefixes from BGP announcements and add prefix filters.</div>';
} else if (key === 'rpki_completeness') {
t += (v.with_roa || 0) + ' of ' + (v.total_checked || 0) + ' prefixes have valid ROAs (' + (v.coverage_pct || 0) + '% coverage).';
t += '</span></div>';
t += '<div class="tt-section"><span class="tt-label">Result: </span><span class="tt-value">';
if (v.over_specific && v.over_specific.length > 0) { t += 'Over-specific prefixes (/25+): ' + v.over_specific.join(', ') + '. '; }
t += (v.coverage_pct || 0) + '% ROA coverage.';
t += '</span></div>';
t += '<div class="tt-section"><span class="tt-label">Why it matters: </span><span class="tt-value">ROAs protect against origin hijacking by validating the authorized origin AS for each prefix.</span></div>';
if (isFail) t += '<div class="tt-section tt-fix">Fix: Create ROAs in your RIR portal for all announced prefixes with correct origin ASN and max-length.</div>';
} else if (key === 'resource_cert') {
t += 'Checked RPKI CA and ROA existence for announced prefixes.';
t += '</span></div>';
t += '<div class="tt-section"><span class="tt-label">Result: </span><span class="tt-value">';
t += (v.roa_count || 0) + ' ROA(s) found across ' + (v.checked || 0) + ' prefixes \u2014 RPKI CA is ' + (v.has_roas ? 'active' : 'missing') + '.';
t += '</span></div>';
t += '<div class="tt-section"><span class="tt-label">Why it matters: </span><span class="tt-value">Without an RPKI CA, no origin validation protection exists for your prefixes.</span></div>';
if (isFail) t += '<div class="tt-section tt-fix">Fix: Set up an RPKI CA through your RIR (RIPE, ARIN, APNIC) and publish ROAs for all prefixes.</div>';
} else if (key === 'blocklist') {
t += 'Scanned ' + (v.checked || 0) + ' prefixes against Spamhaus DROP/EDROP and other blocklists.';
t += '</span></div>';
t += '<div class="tt-section"><span class="tt-label">Result: </span><span class="tt-value">';
if (v.listed_prefixes && v.listed_prefixes.length > 0) {
t += v.listed_prefixes.length + ' prefix(es) listed: ';
v.listed_prefixes.forEach(function(lp) { t += lp.prefix + ' (' + (lp.sources||[]).join(', ') + ') '; });
} else { t += 'No blocklist entries found.'; }
t += '</span></div>';
t += '<div class="tt-section"><span class="tt-label">Why it matters: </span><span class="tt-value">Blocklisted prefixes indicate abuse, compromise, or hijacking and cause traffic to be rejected.</span></div>';
if (isFail) t += '<div class="tt-section tt-fix">Fix: Investigate abuse sources, contact the blocklist operator for delisting, and implement BCP38 source validation.</div>';
} else if (key === 'irr') {
t += 'Compared BGP-observed origins with IRR route objects for ' + (v.total_entries || 0) + ' entries.';
t += '</span></div>';
t += '<div class="tt-section"><span class="tt-label">Result: </span><span class="tt-value">';
t += (v.total_entries || 0) + ' IRR entries checked, ' + (v.mismatch_count || 0) + ' mismatch(es).';
if (v.mismatches && v.mismatches.length > 0) { t += ' Mismatched: ' + v.mismatches.slice(0,3).map(function(m){return m.prefix}).join(', '); }
t += '</span></div>';
t += '<div class="tt-section"><span class="tt-label">Why it matters: </span><span class="tt-value">IRR objects are used by peers for automated prefix filtering \u2014 mismatches may cause routes to be rejected.</span></div>';
if (isFail) t += '<div class="tt-section tt-fix">Fix: Create or update route/route6 objects in the appropriate IRR database (RIPE, RADB, etc.).</div>';
} else if (key === 'abuse_contact') {
t += 'Verified abuse contact email in RIR database and checked MX record validity.';
t += '</span></div>';
t += '<div class="tt-section"><span class="tt-label">Result: </span><span class="tt-value">';
t += 'Contacts: ' + (v.contacts && v.contacts.length > 0 ? v.contacts.join(', ') : 'none') + '. Valid email: ' + (v.has_valid_email ? 'Yes' : 'No') + '.';
t += '</span></div>';
t += '<div class="tt-section"><span class="tt-label">Why it matters: </span><span class="tt-value">A valid abuse contact is required by RIPE policy ripe-786 and essential for incident response.</span></div>';
if (isFail) t += '<div class="tt-section tt-fix">Fix: Update the abuse-c attribute on your aut-num object with a valid, monitored email address with working MX records.</div>';
} else if (key === 'manrs') {
t += 'Checked MANRS (Mutually Agreed Norms for Routing Security) participation status.';
t += '</span></div>';
t += '<div class="tt-section"><span class="tt-label">Result: </span><span class="tt-value">';
t += 'Participant: ' + (v.participant ? 'Yes' : 'No');
if (v.score !== undefined) t += ', Conformance score: ' + v.score + '%';
t += '.</span></div>';
t += '<div class="tt-section"><span class="tt-label">Why it matters: </span><span class="tt-value">MANRS participation is a trust signal demonstrating commitment to routing security best practices.</span></div>';
if (isFail) t += '<div class="tt-section tt-fix">Fix: Join MANRS at manrs.org \u2014 implement filtering, anti-spoofing, coordination, and global validation.</div>';
} else if (key === 'visibility') {
t += 'Measured prefix visibility across ' + (v.total_rrcs || 0) + ' RIPE RIS route collectors.';
t += '</span></div>';
t += '<div class="tt-section"><span class="tt-label">Result: </span><span class="tt-value">';
t += 'Seen by ' + (v.seen_by || 0) + ' of ' + (v.total_rrcs || 0) + ' RRCs (' + (v.visibility_score || 0) + '%). Origin changes: ' + (v.origin_changes || 0) + '.';
t += '</span></div>';
t += '<div class="tt-section"><span class="tt-label">Why it matters: </span><span class="tt-value">Low BGP visibility means routing problems, poor reachability, or possible prefix hijacking.</span></div>';
if (isFail) t += '<div class="tt-section tt-fix">Fix: Verify BGP sessions are up, check prefix filters at upstreams, and ensure prefixes are not too specific (/25+).</div>';
} else if (key === 'rdns') {
t += 'Checked reverse DNS (PTR) delegation for announced prefixes.';
t += '</span></div>';
t += '<div class="tt-section"><span class="tt-label">Result: </span><span class="tt-value">';
t += (v.coverage_pct || 0) + '% rDNS coverage across ' + (v.checked || 0) + ' prefixes checked.';
t += '</span></div>';
t += '<div class="tt-section"><span class="tt-label">Why it matters: </span><span class="tt-value">Missing reverse DNS causes email rejection, breaks traceroute readability, and indicates poor network hygiene.</span></div>';
if (isFail) t += '<div class="tt-section tt-fix">Fix: Set up rDNS delegations in your RIR portal and configure PTR records on your authoritative DNS servers.</div>';
} else if (key === 'rpsl') {
t += 'Looked up aut-num object in IRR for routing policy declarations.';
t += '</span></div>';
t += '<div class="tt-section"><span class="tt-label">Result: </span><span class="tt-value">';
t += 'aut-num: ' + (v.exists ? 'Exists' : 'Missing');
if (v.exists) { t += '. Import policy: ' + (v.has_import ? 'Yes' : 'No') + ', Export policy: ' + (v.has_export ? 'Yes' : 'No'); }
t += '.</span></div>';
t += '<div class="tt-section"><span class="tt-label">Why it matters: </span><span class="tt-value">Well-maintained IRR aut-num objects enable automated peering setup and prefix filtering.</span></div>';
if (isFail) t += '<div class="tt-section tt-fix">Fix: Create an aut-num object with import/export/mp-import/mp-export policies in your RIR\'s IRR database.</div>';
} else if (key === 'ix_route_server') {
t += 'Checked route server participation across ' + (v.total_ix_connections || 0) + ' IX connections.';
t += '</span></div>';
t += '<div class="tt-section"><span class="tt-label">Result: </span><span class="tt-value">';
t += (v.rs_peer_count || 0) + ' of ' + (v.total_ix_connections || 0) + ' IX connections use route servers (' + (v.rs_peer_pct || 0) + '%).';
if (v.message) t += ' ' + v.message;
t += '</span></div>';
t += '<div class="tt-section"><span class="tt-label">Why it matters: </span><span class="tt-value">Route server participation means RPKI-based filtering is applied, improving routing security.</span></div>';
if (isFail) t += '<div class="tt-section tt-fix">Fix: Enable route server peering at your IXPs \u2014 most IXPs offer this for free with RPKI filtering.</div>';
} else if (key === 'communities') {
t += 'Analyzed BGP community usage across ' + (v.total_updates || 0) + ' BGP updates.';
t += '</span></div>';
t += '<div class="tt-section"><span class="tt-label">Result: </span><span class="tt-value">';
t += (v.unique_communities || 0) + ' unique communities found.';
if (v.well_known_detected && v.well_known_detected.length > 0) {
t += ' Well-known: ' + v.well_known_detected.map(function(c){return c.community + ' (' + c.well_known + ')'}).join(', ') + '.';
}
t += '</span></div>';
t += '<div class="tt-section"><span class="tt-label">Why it matters: </span><span class="tt-value">BGP communities reveal routing policy sophistication and enable traffic engineering control.</span></div>';
if (isFail) t += '<div class="tt-section tt-fix">Fix: Implement BGP community tagging for traffic engineering and use well-known communities (blackhole, no-export) where appropriate.</div>';
} else if (key === 'geolocation') {
t += 'Compared prefix geolocation data with PeeringDB facility locations.';
t += '</span></div>';
t += '<div class="tt-section"><span class="tt-label">Result: </span><span class="tt-value">';
t += 'Geo countries: ' + ((v.geo_countries || []).join(', ') || 'unknown');
if (v.pdb_facility_countries && v.pdb_facility_countries.length > 0) { t += '. PDB facilities: ' + v.pdb_facility_countries.join(', '); }
if (v.country_mismatches && v.country_mismatches.length > 0) { t += '. Mismatches: ' + v.country_mismatches.join(', '); }
t += '.</span></div>';
t += '<div class="tt-section"><span class="tt-label">Why it matters: </span><span class="tt-value">Geolocation mismatches between announced prefixes and facility locations can indicate prefix hijacking.</span></div>';
if (isFail) t += '<div class="tt-section tt-fix">Fix: Update geofeed data, verify PeeringDB facility records, and check for unauthorized prefix announcements.</div>';
} else {
t += info.desc;
t += '</span></div>';
t += '<div class="tt-section"><span class="tt-label">Result: </span><span class="tt-value">' + (v.message || v.status || 'Unknown') + '</span></div>';
}
return t;
}
function renderHealthReport(d) {
var h = '';
var score = d.health_score || 0;
var scoreColor = score >= 80 ? 'var(--green)' : score >= 60 ? 'var(--orange)' : 'var(--red)';
var circumference = 2 * Math.PI * 54;
var offset = circumference - (score / 100) * circumference;
h += '<div style="display:grid;grid-template-columns:200px 1fr;gap:2rem;align-items:start;margin-bottom:1.5rem">';
// Gauge
h += '<div>';
h += '<div class="health-gauge">';
h += '<svg viewBox="0 0 120 120"><circle class="health-gauge-bg" cx="60" cy="60" r="54"/>';
h += '<circle class="health-gauge-fill" cx="60" cy="60" r="54" stroke="' + scoreColor + '" stroke-dasharray="' + circumference.toFixed(1) + '" stroke-dashoffset="' + offset.toFixed(1) + '"/></svg>';
h += '<div class="health-gauge-text"><div class="health-gauge-score" style="color:' + scoreColor + '">' + score + '</div><div class="health-gauge-label">Health Score</div></div>';
h += '</div>';
h += '<div style="text-align:center;font-size:.8rem;color:var(--muted);margin-top:.5rem">' + escHtml(d.name || '') + '</div>';
h += '</div>';
// Checks grid
h += '<div><div class="health-checks">';
var checkLabels = {
bogon: { label: "Bogon Detection", desc: "RFC 1918, RFC 5737, CGN & reserved ASNs" },
irr: { label: "IRR Validation", desc: "BGP vs IRR origin consistency" },
rpki_completeness: { label: "RPKI ROA Coverage", desc: "ROA coverage and over-specific prefixes" },
abuse_contact: { label: "Abuse Contact", desc: "Valid abuse email in RIR database" },
blocklist: { label: "Blocklist Check", desc: "Spamhaus DROP and blocklist status" },
manrs: { label: "MANRS Compliance", desc: "MANRS participation and conformance" },
rdns: { label: "Reverse DNS", desc: "rDNS delegation for prefixes" },
visibility: { label: "BGP Visibility", desc: "Route visibility across RIS collectors" },
communities: { label: "BGP Communities", desc: "Well-known community usage" },
geolocation: { label: "Geolocation", desc: "Geo vs PeeringDB facility verification" },
rpsl: { label: "IRR Object", desc: "aut-num with routing policy" },
ix_route_server: { label: "IX Route Servers", desc: "Route server peering participation" },
resource_cert: { label: "Resource Cert", desc: "RPKI CA / ROA existence" }
};
var checkOrder = ["bogon","rpki_completeness","resource_cert","blocklist","irr","abuse_contact","manrs","visibility","rdns","rpsl","ix_route_server","communities","geolocation"];
var validations = d.validations || {};
checkOrder.forEach(function(key) {
var v = validations[key];
if (!v) return;
var info = checkLabels[key] || { label: key, desc: "" };
var icon = v.status === "pass" ? "\u2705" : v.status === "warning" ? "\u26A0\uFE0F" : v.status === "fail" ? "\u274C" : "\u2753";
var clr = v.status === "pass" ? "var(--green)" : v.status === "warning" ? "var(--orange)" : v.status === "fail" ? "var(--red)" : "var(--muted)";
var txt = v.status === "pass" ? "Pass" : v.status === "warning" ? "Warn" : v.status === "fail" ? "Fail" : "Err";
var tt = buildHealthTooltip(key, v, info);
h += '<div class="health-check-item">';
h += '<span class="health-check-icon">' + icon + '</span>';
h += '<span class="health-check-name">' + info.label + '</span>';
h += '<span class="health-check-score" style="color:' + clr + '">' + txt + '</span>';
h += '<div class="health-tooltip">' + tt + '</div>';
h += '</div>';
});
h += '</div></div></div>';
// === DATA ACCURACY SECTION ===
h += '<div style="margin:1.5rem 0;padding:1rem;background:rgba(122,162,247,.06);border:1px solid rgba(122,162,247,.15);border-radius:10px">';
h += '<div style="display:flex;align-items:center;gap:.5rem;margin-bottom:.75rem"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="#7aa2f7" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>';
h += '<span style="font-size:.85rem;font-weight:600;color:#7aa2f7">Score Breakdown — Why ' + score + '/100?</span></div>';
// Score calculation table
h += '<table style="width:100%;font-size:.78rem;border-collapse:collapse">';
h += '<tr style="color:var(--muted);border-bottom:1px solid var(--border)"><th style="text-align:left;padding:.3rem .5rem">Check</th><th style="width:60px;text-align:center;padding:.3rem">Weight</th><th style="width:60px;text-align:center;padding:.3rem">Earned</th><th style="text-align:left;padding:.3rem .5rem">Reason</th></tr>';
var weightMap = { bogon: 15, irr: 10, rpki_completeness: 15, abuse_contact: 5, blocklist: 15, manrs: 5, rdns: 5, visibility: 10, rpsl: 5, ix_route_server: 5, resource_cert: 10 };
var totalW = 0, totalE = 0;
checkOrder.forEach(function(key) {
var v = validations[key];
if (!v) return;
var w = weightMap[key] || 0;
if (w === 0) return; // communities, geolocation not scored
var info = checkLabels[key] || { label: key };
var earned = 0;
var reason = '';
if (v.status === 'info') {
reason = '<span style="color:var(--muted)">Excluded — unable to verify (API requires authentication)</span>';
// info checks excluded from scoring
} else {
if (v.status === 'pass') { earned = w; reason = '<span style="color:var(--green)">Full points</span>'; }
else if (v.status === 'warning') { earned = Math.round(w * 0.5); reason = '<span style="color:var(--orange)">' + escHtml(v.note || v.message || 'Partial compliance') + '</span>'; }
else { earned = 0; reason = '<span style="color:var(--red)">' + escHtml(v.note || v.message || 'Check failed') + '</span>'; }
totalW += w;
totalE += earned;
}
var statusIcon = v.status === 'pass' ? '✅' : v.status === 'warning' ? '⚠️' : v.status === 'fail' ? '❌' : '';
h += '<tr style="border-bottom:1px solid rgba(255,255,255,.03)"><td style="padding:.35rem .5rem">' + statusIcon + ' ' + info.label + '</td>';
h += '<td style="text-align:center;padding:.35rem;color:var(--muted)">' + (v.status === 'info' ? '—' : w) + '</td>';
h += '<td style="text-align:center;padding:.35rem;font-weight:600;color:' + (earned === w ? 'var(--green)' : earned > 0 ? 'var(--orange)' : v.status === 'info' ? 'var(--muted)' : 'var(--red)') + '">' + (v.status === 'info' ? '—' : earned) + '</td>';
h += '<td style="padding:.35rem .5rem;font-size:.72rem">' + reason + '</td></tr>';
});
var calcScore = totalW > 0 ? Math.round((totalE / totalW) * 100) : 0;
h += '<tr style="border-top:2px solid var(--border);font-weight:700"><td style="padding:.4rem .5rem">Total</td>';
h += '<td style="text-align:center;padding:.4rem">' + totalW + '</td>';
h += '<td style="text-align:center;padding:.4rem;color:' + scoreColor + '">' + totalE + '</td>';
h += '<td style="padding:.4rem .5rem;color:' + scoreColor + '">' + calcScore + '/100 = ' + totalE + '/' + totalW + ' weighted points</td></tr>';
h += '</table>';
// Data source note
h += '<div style="margin-top:.75rem;padding-top:.6rem;border-top:1px solid rgba(255,255,255,.05);font-size:.72rem;color:var(--muted)">';
h += '<strong>Data Sources:</strong> PeeringDB (profile, IX, facilities), RIPE Stat (prefixes, neighbours, visibility, RPKI), ';
h += 'RIPE Atlas (probes), Cloudflare RPKI (ROA + ASPA), MANRS Observatory, RIPE DB (IRR objects).<br>';
h += '<strong>Scoring:</strong> Each check has a weight reflecting its importance to routing security. ';
h += '"Pass" = full weight, "Warning" = 50%, "Fail" = 0%, "Info" = excluded (unable to verify). ';
h += 'Score = earned / total_weight × 100. Checks marked "info" (e.g., MANRS when API is unavailable) are excluded from the denominator to avoid unfair penalties.';
h += '</div></div>';
// Expandable details
h += '<div class="expand-toggle" onclick="toggleExpand(this)">Show detailed validation results</div>';
h += '<div class="expand-body">';
checkOrder.forEach(function(key) {
var v = validations[key];
if (!v) return;
var info = checkLabels[key] || { label: key, desc: "" };
var icon = v.status === "pass" ? "\u2705" : v.status === "warning" ? "\u26A0\uFE0F" : v.status === "fail" ? "\u274C" : "\u2753";
var bc = v.status === "pass" ? "var(--green)" : v.status === "warning" ? "var(--orange)" : v.status === "fail" ? "var(--red)" : "var(--border)";
h += '<div style="padding:.75rem;margin-bottom:.5rem;background:var(--bg);border:1px solid var(--border);border-left:3px solid ' + bc + ';border-radius:8px">';
h += '<div style="display:flex;align-items:center;gap:.5rem;margin-bottom:.4rem"><span>' + icon + '</span><strong style="font-size:.85rem">' + info.label + '</strong><span style="font-size:.7rem;color:var(--muted);margin-left:auto">' + info.desc + '</span></div>';
h += '<div style="font-size:.8rem;color:var(--text-dim)">';
if (key === "bogon") {
h += 'Prefixes checked: ' + (v.total_prefixes_checked || 0);
if (v.bogon_prefixes && v.bogon_prefixes.length > 0) {
h += '<div style="color:var(--red);margin-top:.3rem">Bogon prefixes:</div>';
v.bogon_prefixes.forEach(function(b) { h += '<div style="font-family:monospace;font-size:.75rem;margin-left:1rem">' + escHtml(b.prefix) + ' - ' + escHtml(b.reason) + '</div>'; });
} else { h += '<div style="color:var(--green)">No bogon prefixes</div>'; }
if (v.bogon_asns_in_paths && v.bogon_asns_in_paths.length > 0) { h += '<div style="color:var(--red)">Bogon ASNs: ' + v.bogon_asns_in_paths.join(', ') + '</div>'; }
} else if (key === "irr") {
h += 'IRR entries: ' + (v.total_entries || 0) + ', Mismatches: ' + (v.mismatch_count || 0);
if (v.mismatches && v.mismatches.length > 0) { v.mismatches.slice(0,5).forEach(function(m) { h += '<div style="font-family:monospace;font-size:.75rem;margin:.2rem 0">' + escHtml(m.prefix) + '</div>'; }); }
} else if (key === "rpki_completeness") {
var cc = (v.coverage_pct||0) >= 90 ? 'var(--green)' : 'var(--orange)';
h += 'Coverage: <strong style="color:' + cc + '">' + (v.coverage_pct||0) + '%</strong> (' + (v.with_roa||0) + '/' + (v.total_checked||0) + ')';
if (v.over_specific && v.over_specific.length > 0) { h += '<div style="color:var(--orange)">Over-specific (/25+): ' + v.over_specific.join(', ') + '</div>'; }
} else if (key === "abuse_contact") {
h += 'Contacts: ' + (v.contacts ? v.contacts.join(', ') : 'none');
h += '<br>Valid email: ' + (v.has_valid_email ? '<span style="color:var(--green)">Yes</span>' : '<span style="color:var(--red)">No</span>');
} else if (key === "blocklist") {
h += 'Checked: ' + (v.checked || 0);
if (v.listed_prefixes && v.listed_prefixes.length > 0) { h += '<div style="color:var(--red)">Listed:</div>'; v.listed_prefixes.forEach(function(lp) { h += '<div style="font-family:monospace;font-size:.75rem;margin-left:1rem">' + escHtml(lp.prefix) + ' on: ' + (lp.sources||[]).join(', ') + '</div>'; }); }
else { h += '<div style="color:var(--green)">No blocklist entries</div>'; }
} else if (key === "manrs") {
h += 'Participant: ' + (v.participant ? '<span style="color:var(--green)">Yes</span>' : '<span style="color:var(--orange)">No</span>');
if (v.score !== undefined) { h += ', Score: ' + v.score; }
} else if (key === "rdns") {
h += 'Coverage: ' + (v.coverage_pct || 0) + '% (' + (v.checked || 0) + ' checked)';
} else if (key === "visibility") {
h += 'Score: ' + (v.visibility_score || 0) + '%, Seen by ' + (v.seen_by || 0) + '/' + (v.total_rrcs || 0) + ' RRCs, Origin changes: ' + (v.origin_changes || 0);
} else if (key === "communities") {
h += 'Updates: ' + (v.total_updates || 0) + ', Unique communities: ' + (v.unique_communities || 0);
if (v.well_known_detected && v.well_known_detected.length > 0) { h += '<br>Well-known: '; v.well_known_detected.forEach(function(c) { h += '<span class="badge badge-orange">' + escHtml(c.community) + ' (' + c.well_known + ')</span> '; }); }
} else if (key === "geolocation") {
h += 'Geo countries: ' + (v.geo_countries || []).join(', ');
if (v.pdb_facility_countries && v.pdb_facility_countries.length > 0) { h += '<br>PDB countries: ' + v.pdb_facility_countries.join(', '); }
if (v.country_mismatches && v.country_mismatches.length > 0) { h += '<br><span style="color:var(--orange)">Mismatches: ' + v.country_mismatches.join(', ') + '</span>'; }
} else if (key === "rpsl") {
h += 'aut-num: ' + (v.exists ? '<span style="color:var(--green)">Exists</span>' : '<span style="color:var(--red)">Missing</span>');
if (v.exists) { h += ', Import: ' + (v.has_import ? 'Yes' : 'No') + ', Export: ' + (v.has_export ? 'Yes' : 'No'); }
} else if (key === "ix_route_server") {
h += 'IX connections: ' + (v.total_ix_connections || 0) + ', RS peers: ' + (v.rs_peer_count || 0) + ' (' + (v.rs_peer_pct || 0) + '%)';
if (v.message) { h += '<br><span style="color:var(--muted)">' + escHtml(v.message) + '</span>'; }
} else if (key === "resource_cert") {
h += 'ROAs: ' + (v.has_roas ? '<span style="color:var(--green)">Yes</span>' : '<span style="color:var(--red)">No RPKI CA</span>') + ' (' + (v.roa_count || 0) + '/' + (v.checked || 0) + ')';
} else {
h += escHtml(JSON.stringify(v).substring(0, 200));
}
h += '</div></div>';
});
h += '</div>';
h += '<div style="font-size:.7rem;color:var(--dim);margin-top:.75rem">Validation completed in ' + (d.meta ? d.meta.duration_ms : '?') + 'ms | ' + (d.meta ? d.meta.total_prefixes : '?') + ' prefixes, ' + (d.meta ? d.meta.prefixes_sampled : '?') + ' sampled</div>';
$('healthContent').innerHTML = h;
}
function loadPeeringRecommendations(asn, ixConnections, lookupData) {
if (!ixConnections || ixConnections.length === 0) return;
lookupData = lookupData || {};
$('peeringRecCard').classList.remove('hidden');
// Get the IXPs this network is on
var myIxIds = new Set(ixConnections.map(function(ix) { return ix.ix_id; }));
var myIxNames = {};
ixConnections.forEach(function(ix) { myIxNames[ix.ix_id] = ix.ix_name; });
// Get existing BGP neighbours (to filter out already-established peering)
var existingPeers = new Set();
var nb = lookupData.neighbours || {};
(nb.upstreams || []).forEach(function(n) { existingPeers.add(n.asn); });
(nb.downstreams || []).forEach(function(n) { existingPeers.add(n.asn); });
(nb.peers || []).forEach(function(n) { existingPeers.add(n.asn); });
// Top networks to check peering potential with
var topNets = [13335, 15169, 32934, 16509, 8075, 20940, 6939, 174, 1299, 2914, 3356, 3257, 714, 36459, 13414, 46489, 14618, 54113, 396982, 2906];
$('peeringRecContent').innerHTML = '<div style="color:var(--dim);font-size:.85rem">Checking peering potential with top 20 networks...</div>';
// Fetch IX presence for top networks
Promise.all(topNets.map(function(targetAsn) {
return fetch('/api/lookup?asn=' + targetAsn).then(function(r) { return r.json(); }).then(function(d) {
var name = d.network ? d.network.name : 'AS' + targetAsn;
var theirIx = (d.ix_presence && d.ix_presence.connections) || [];
var theirIxIds = new Set(theirIx.map(function(ix) { return ix.ix_id; }));
var common = [];
myIxIds.forEach(function(id) { if (theirIxIds.has(id)) common.push(myIxNames[id] || 'IX-' + id); });
return { asn: targetAsn, name: name, common_ixps: common, their_total: theirIx.length };
}).catch(function() { return null; });
})).then(function(results) {
results = results.filter(function(r) { return r && r.asn !== parseInt(asn); });
results.sort(function(a, b) { return b.common_ixps.length - a.common_ixps.length; });
// Split into 3 categories: established, potential new, no shared IXP
var established = results.filter(function(r) { return r.common_ixps.length > 0 && existingPeers.has(r.asn); });
var potential = results.filter(function(r) { return r.common_ixps.length > 0 && !existingPeers.has(r.asn); });
var without = results.filter(function(r) { return r.common_ixps.length === 0; });
var h = '';
// NEW PEERING OPPORTUNITIES (not yet peering, shared IXPs exist)
if (potential.length > 0) {
h += '<div style="font-size:.7rem;font-weight:700;color:var(--green);text-transform:uppercase;letter-spacing:.08em;margin-bottom:.5rem">\uD83D\uDE80 New Peering Opportunities (' + potential.length + ')</div>';
h += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:.5rem;margin-bottom:1rem">';
potential.forEach(function(r) {
h += '<div onclick="lookupAsn(' + r.asn + ')" style="cursor:pointer;background:var(--bg);border:1px solid rgba(156,206,106,.2);border-radius:10px;padding:.65rem .85rem;transition:all .15s" onmouseenter="this.style.borderColor=\'var(--green)\'" onmouseleave="this.style.borderColor=\'rgba(156,206,106,.2)\'">';
h += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.3rem"><span style="font-weight:700;font-size:.85rem;color:var(--green)">AS' + r.asn + '</span><span style="font-size:.65rem;color:var(--muted)">' + r.common_ixps.length + ' shared IXPs</span></div>';
h += '<div style="font-size:.75rem;color:var(--text-dim);margin-bottom:.3rem">' + escHtml(r.name) + '</div>';
h += '<div style="display:flex;flex-wrap:wrap;gap:.25rem">';
r.common_ixps.slice(0, 5).forEach(function(ix) {
h += '<span style="font-size:.6rem;padding:.1rem .35rem;border-radius:4px;background:rgba(156,206,106,.1);color:var(--green);border:1px solid rgba(156,206,106,.15)">' + escHtml(ix) + '</span>';
});
if (r.common_ixps.length > 5) h += '<span style="font-size:.6rem;color:var(--muted)">+' + (r.common_ixps.length - 5) + ' more</span>';
h += '</div></div>';
});
h += '</div>';
}
// ALREADY ESTABLISHED (peering exists + shared IXPs)
if (established.length > 0) {
h += '<div style="font-size:.7rem;font-weight:700;color:#7aa2f7;text-transform:uppercase;letter-spacing:.08em;margin-bottom:.5rem">\u2705 Already Peering (' + established.length + ')</div>';
h += '<div style="display:flex;flex-wrap:wrap;gap:.4rem;margin-bottom:1rem">';
established.forEach(function(r) {
h += '<span onclick="lookupAsn(' + r.asn + ')" style="cursor:pointer;font-size:.7rem;padding:.25rem .5rem;border-radius:6px;background:rgba(122,162,247,.08);border:1px solid rgba(122,162,247,.15);color:#7aa2f7">' + escHtml(r.name) + ' (AS' + r.asn + ') — ' + r.common_ixps.length + ' IXPs</span>';
});
h += '</div>';
}
// NO SHARED IXP
if (without.length > 0) {
h += '<div style="font-size:.7rem;font-weight:700;color:var(--orange);text-transform:uppercase;letter-spacing:.08em;margin-bottom:.5rem">\u26a0\ufe0f No Shared IXP (' + without.length + ')</div>';
h += '<div style="display:flex;flex-wrap:wrap;gap:.4rem">';
without.forEach(function(r) {
var alreadyPeer = existingPeers.has(r.asn) ? ' \u2714 peered via transit' : '';
h += '<span onclick="lookupAsn(' + r.asn + ')" style="cursor:pointer;font-size:.7rem;padding:.25rem .5rem;border-radius:6px;background:rgba(255,158,100,.08);border:1px solid rgba(255,158,100,.15);color:var(--orange)">' + escHtml(r.name) + ' (AS' + r.asn + ')' + alreadyPeer + '</span>';
});
h += '</div>';
}
if (potential.length === 0 && established.length > 0) {
h += '<div style="margin-top:.75rem;padding:.5rem;background:rgba(156,206,106,.06);border:1px solid rgba(156,206,106,.1);border-radius:8px;font-size:.75rem;color:var(--green);text-align:center">\u2705 Already peering with all top networks at shared IXPs</div>';
}
h += '<div style="font-size:.65rem;color:var(--dim);margin-top:.75rem;text-align:center">Compared with top 20 global networks — existing peering detected via BGP neighbour data</div>';
$('peeringRecContent').innerHTML = h;
});
}
</script>
</body>
</html>