diff --git a/server.js b/server.js index 24c1431..b9b3831 100644 --- a/server.js +++ b/server.js @@ -1160,7 +1160,7 @@ function buildPdfHtml(data, aspa, format) { /** * Build comparison PDF HTML for two ASNs. */ -function buildComparisonPdfHtml(d1, aspa1, d2, aspa2) { +function buildComparisonPdfHtml(d1, aspa1, l1, d2, aspa2, l2) { const ts = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); function col(d, aspa) { @@ -1204,6 +1204,98 @@ function buildComparisonPdfHtml(d1, aspa1, d2, aspa2) { const colHtml = col(d1, aspa1) + col(d2, aspa2); + // ── Peering Intelligence: common IXPs + common facilities ───────────────── + const ixList1 = (l1?.ix_presence?.connections || []).map(x => ({ id: x.ix_id, name: x.ix_name, city: x.city || '', speed: x.speed_mbps })); + const ixList2 = (l2?.ix_presence?.connections || []).map(x => ({ id: x.ix_id, name: x.ix_name, city: x.city || '', speed: x.speed_mbps })); + const ixIds1 = new Set(ixList1.map(x => x.id)); + const ixIds2 = new Set(ixList2.map(x => x.id)); + const commonIxIds = [...ixIds1].filter(id => ixIds2.has(id)); + const commonIxps = commonIxIds.slice(0, 20).map(id => { + const x1 = ixList1.find(x => x.id === id); + const x2 = ixList2.find(x => x.id === id); + return { id, name: x1?.name || 'Unknown', city: x1?.city || '', speed1: x1?.speed, speed2: x2?.speed }; + }); + + const facList1 = (l1?.facilities?.list || []).map(f => ({ id: f.fac_id, name: f.name, city: f.city || '', country: f.country || '' })); + const facList2 = (l2?.facilities?.list || []).map(f => ({ id: f.fac_id, name: f.name, city: f.city || '', country: f.country || '' })); + const facIds1 = new Set(facList1.map(f => f.id)); + const facIds2 = new Set(facList2.map(f => f.id)); + const commonFacIds = [...facIds1].filter(id => facIds2.has(id)); + const commonFacs = commonFacIds.slice(0, 15).map(id => facList1.find(f => f.id === id)); + + // IXPs of ASN1 where ASN2 is NOT present (opportunity IXPs for ASN2) + const oppsFor2 = ixList1.filter(x => !ixIds2.has(x.id)).slice(0, 8); + const oppsFor1 = ixList2.filter(x => !ixIds1.has(x.id)).slice(0, 8); + + const commonIxHtml = commonIxps.length > 0 + ? ` + + ${commonIxps.map(x => ` + + + + + + `).join('')} +
IXP NameCityAS${d1.asn} SpeedAS${d2.asn} Speed
${escHtml(x.name)}${escHtml(x.city)}${x.speed1 ? (x.speed1/1000).toLocaleString()+'G' : '—'}${x.speed2 ? (x.speed2/1000).toLocaleString()+'G' : '—'}
` + : '

No common IXPs found — these networks do not share any Internet Exchange Points.

'; + + const commonFacHtml = commonFacs.length > 0 + ? ` + + ${commonFacs.map(f => ` + + + + + `).join('')} +
Datacenter / Colocation FacilityCityCountry
${escHtml(f?.name||'')}${escHtml(f?.city||'')}${escHtml(f?.country||'')}
` + : '

No shared colocation facilities found.

'; + + const oppsHtml = (asns, opps) => opps.length === 0 + ? `

Already maximally peered on these exchanges — or no data available.

` + : opps.map(x => `
+ ${escHtml(x.name)} + ${x.city ? ` · ${escHtml(x.city)}` : ''} + ${x.speed ? `${(x.speed/1000).toLocaleString()}G` : ''} +
`).join(''); + + const peeringPage = (commonIxps.length + commonFacs.length > 0 || oppsFor1.length + oppsFor2.length > 0) ? ` +
+

Peering Intelligence

+

Common Internet Exchange Points and colocation facilities — potential peering locations between AS${d1.asn} and AS${d2.asn}.

+ +
+
+
${commonIxps.length}
+
Common IXPs
+
+
+
${commonFacs.length}
+
Shared Facilities
+
+
+ +

Common Internet Exchange Points (direct peering possible)

+ ${commonIxHtml} + +

Shared Colocation Facilities

+ ${commonFacHtml} + + ${oppsFor1.length + oppsFor2.length > 0 ? ` +

Expansion Opportunities

+
+
+
AS${d2.asn} could join (AS${d1.asn} is there)
+ ${oppsHtml(d2.asn, oppsFor2)} +
+
+
AS${d1.asn} could join (AS${d2.asn} is there)
+ ${oppsHtml(d1.asn, oppsFor1)} +
+
` : ''} +
` : ''; + // Side-by-side check comparison const allChecks = [...new Set([ ...(d1.score_breakdown || []).map(c => c.check), @@ -1249,8 +1341,9 @@ function buildComparisonPdfHtml(d1, aspa1, d2, aspa2) { ${checkRows} + ${peeringPage} @@ -6280,17 +6373,19 @@ ${html} return res.end(cached); } try { - const [v1, a1, v2, a2] = await Promise.all([ + const [v1, a1, l1, v2, a2, l2] = await Promise.all([ fetchInternal(`/api/validate?asn=${asn1}`), fetchInternal(`/api/aspa?asn=${asn1}`).catch(() => null), + fetchInternal(`/api/lookup?asn=${asn1}`).catch(() => null), fetchInternal(`/api/validate?asn=${asn2}`), fetchInternal(`/api/aspa?asn=${asn2}`).catch(() => null), + fetchInternal(`/api/lookup?asn=${asn2}`).catch(() => null), ]); if (v1.error || v2.error) { res.writeHead(400, {'Content-Type':'application/json'}); return res.end(JSON.stringify({ error: v1.error || v2.error })); } - const html = buildComparisonPdfHtml(v1, a1, v2, a2); + const html = buildComparisonPdfHtml(v1, a1, l1, v2, a2, l2); const pdfBuf = await renderHtmlToPdf(html); pdfCacheSet(cacheKey, pdfBuf); res.writeHead(200, { ...{'Content-Type':'application/json'}, 'Content-Type': 'application/pdf', 'Content-Disposition': `inline; filename="peercortex-AS${asn1}-vs-AS${asn2}.pdf"`, 'Content-Length': pdfBuf.length });