UNPKG

domainlooker

Version:

A fast CLI and MCP server for inspecting domains: WHOIS/RDAP, DNS, SSL, ports, subdomains, with CSV/JSON export.

173 lines (167 loc) โ€ข 9 kB
import { WORLD_LAND } from './world-land.js'; const W = 1000; const H = 500; function project(lat, lon) { return { x: ((lon + 180) / 360) * W, y: ((90 - lat) / 180) * H }; } function escapeHtml(value) { return value.replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])); } /** ISO-2 country code -> flag emoji via regional indicator symbols. */ function flag(cc) { const code = cc.toUpperCase(); if (!/^[A-Z]{2}$/.test(code)) return '๐Ÿด'; return String.fromCodePoint(...[...code].map(c => 127397 + c.charCodeAt(0))); } function statusClass(s) { return s === 'in-sync' ? 'ok' : s === 'differs' ? 'warn' : 'dead'; } /** All land rings as one SVG path, projected equirectangular to match the markers. */ function continentsPath() { let d = ''; for (const ring of WORLD_LAND) { for (let i = 0; i < ring.length; i++) { const [lon, lat] = ring[i]; const { x, y } = project(lat, lon); d += (i === 0 ? 'M' : 'L') + x.toFixed(1) + ',' + y.toFixed(1); } d += 'Z'; } return d; } /** Render a self-contained interactive world map + list of the propagation result. */ export function renderPropagationMap(result) { const land = continentsPath(); const markers = result.results .map((r, i) => { const { x, y } = project(r.resolver.lat, r.resolver.lon); return `<g class="mk ${statusClass(r.status)}" transform="translate(${x.toFixed(1)},${y.toFixed(1)})" style="--i:${i}"> <circle class="pulse" r="6"></circle> <circle class="dot" r="6"></circle> <text class="g cross" y="3.2">โœ•</text> <text class="g tick" y="3.4">โœ“</text> </g>`; }) .join(''); const rows = result.results .map((r, i) => { const cls = statusClass(r.status); const answer = r.answer ? escapeHtml(r.answer.join(', ')) : ''; const glyph = cls === 'ok' ? 'โœ“' : cls === 'warn' ? 'โœ•' : 'โ€“'; return `<li class="row ${cls}" style="--i:${i}"> <span class="flag">${flag(r.resolver.country)}</span> <span class="loc"><b>${escapeHtml(r.resolver.location)}, ${escapeHtml(r.resolver.country)}</b><span class="prov">${escapeHtml(r.resolver.provider)}</span></span> <span class="ans">${answer}</span> <span class="st">${glyph}</span> </li>`; }) .join(''); const expected = result.expected ? escapeHtml(result.expected.join(', ')) : 'โ€”'; const domain = escapeHtml(result.domain); return `<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>DNS propagation โ€” ${domain}</title> ${STYLE} </head> <body> <main class="wrap"> <header class="head"> <div class="title"> <div class="eyebrow">GLOBAL DNS PROPAGATION</div> <h1>${domain} <span class="rtype">${result.recordType}</span></h1> <div class="expected">Expected: <code>${expected}</code></div> </div> <div class="score"> <div class="pct">${result.percent}<span>%</span></div> <div class="sub">${result.inSync}/${result.total} resolvers in sync</div> <div class="bar"><span style="width:${result.percent}%"></span></div> </div> </header> <div class="grid"> <ol class="list">${rows}</ol> <div class="mapwrap"> <svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMid meet" role="img" aria-label="World map of DNS resolver propagation"> <rect class="ocean" x="0" y="0" width="${W}" height="${H}"></rect> <path class="land" d="${land}"></path> <g class="markers">${markers}</g> </svg> <div class="legend"> <span class="lg ok"><i></i> In sync</span> <span class="lg warn"><i></i> Different answer</span> <span class="lg dead"><i></i> No answer</span> </div> </div> </div> <footer class="foot">${result.total} public resolvers worldwide (regional ISPs + anycast) ยท re-run to refresh</footer> </main> </body> </html>`; } const STYLE = `<style> :root{ --bg:#0a1018; --panel:#0f1826; --panel2:#0c1420; --ink:#e7eef6; --muted:#8397ad; --faint:#5c6f86; --line:#1b2739; --ocean:#0b1524; --land:#1d2c42; --land-edge:#283a55; --ok:#22c55e; --warn:#f59e0b; --dead:#5a6b82; --accent:#38bdf8; --mono:ui-monospace,"SF Mono","JetBrains Mono",Menlo,Consolas,monospace; --sans:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif; } *{box-sizing:border-box;} body{margin:0;background:radial-gradient(1200px 700px at 60% -15%,#132038,var(--bg));color:var(--ink);font-family:var(--sans);} .wrap{max-width:1240px;margin:0 auto;padding:26px 22px 36px;} .head{display:flex;justify-content:space-between;align-items:flex-end;gap:24px;flex-wrap:wrap;margin-bottom:18px;} .eyebrow{font-family:var(--mono);font-size:11px;letter-spacing:.22em;color:var(--accent);margin-bottom:8px;} h1{margin:0;font-size:30px;letter-spacing:-0.02em;} h1 .rtype{font-family:var(--mono);font-size:14px;color:var(--bg);background:var(--accent);padding:2px 8px;border-radius:5px;vertical-align:middle;margin-left:6px;} .expected{margin-top:8px;color:var(--muted);font-size:14px;} .expected code{font-family:var(--mono);color:var(--ink);background:var(--panel);padding:2px 7px;border-radius:5px;} .score{text-align:right;min-width:190px;} .pct{font-family:var(--mono);font-size:40px;font-weight:700;line-height:1;font-variant-numeric:tabular-nums;} .pct span{font-size:19px;color:var(--muted);} .sub{color:var(--muted);font-size:13px;margin:4px 0 8px;} .bar{height:6px;background:var(--line);border-radius:99px;overflow:hidden;} .bar span{display:block;height:100%;background:linear-gradient(90deg,var(--accent),var(--ok));animation:grow 1.1s ease-out both;} @keyframes grow{from{width:0 !important;}} .grid{display:grid;grid-template-columns:minmax(300px,380px) 1fr;gap:18px;align-items:start;} @media (max-width:840px){ .grid{grid-template-columns:1fr;} } .list{list-style:none;margin:0;padding:0;max-height:520px;overflow:auto;border:1px solid var(--line);border-radius:12px;background:var(--panel);} .mapwrap{position:sticky;top:16px;} .row{display:grid;grid-template-columns:26px 1fr auto 20px;align-items:center;gap:10px;padding:9px 14px;border-bottom:1px solid var(--line);font-size:13px;animation:rowin .4s ease-out both;animation-delay:calc(var(--i)*0.05s);} .row:last-child{border-bottom:none;} .flag{font-size:16px;} .loc b{font-weight:600;display:block;} .loc .prov{color:var(--faint);font-size:11.5px;} .ans{font-family:var(--mono);font-size:11.5px;color:var(--muted);text-align:right;max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} .st{font-weight:800;text-align:center;} .row.ok .st{color:var(--ok);} .row.warn .st{color:var(--warn);} .row.dead .st{color:var(--dead);} @keyframes rowin{from{opacity:0;transform:translateY(4px);}} .mapwrap{border:1px solid var(--line);border-radius:14px;overflow:hidden;background:var(--panel2);box-shadow:0 24px 70px -34px #000;} svg{display:block;width:100%;height:auto;} .ocean{fill:var(--ocean);} .land{fill:var(--land);stroke:var(--land-edge);stroke-width:.6;stroke-linejoin:round;} .mk .dot{fill:var(--dead);stroke:#0a1018;stroke-width:1.4;} .mk .g{text-anchor:middle;font-family:var(--sans);font-size:7px;font-weight:800;} .mk .cross{fill:#fff;opacity:1;} .mk .tick{fill:#04210f;opacity:0;} .mk .pulse{fill:none;} .mk.warn .dot{fill:var(--warn);} .mk.dead{opacity:.5;} .mk.ok .dot{fill:#7f1d1d;animation:toGreen .5s ease-out both;animation-delay:calc(var(--i)*0.11s + .35s);} .mk.ok .cross{animation:fadeOut .35s ease-out both;animation-delay:calc(var(--i)*0.11s + .35s);} .mk.ok .tick{animation:fadeIn .35s ease-out both;animation-delay:calc(var(--i)*0.11s + .35s);} .mk.ok .pulse{stroke:var(--ok);stroke-width:2;opacity:0;animation:ping .85s ease-out both;animation-delay:calc(var(--i)*0.11s + .35s);} @keyframes toGreen{from{fill:#7f1d1d;}to{fill:var(--ok);}} @keyframes fadeOut{to{opacity:0;}} @keyframes fadeIn{from{opacity:0;}to{opacity:1;}} @keyframes ping{0%{r:4;opacity:.9;}100%{r:20;opacity:0;}} @media (prefers-reduced-motion: reduce){ .mk.ok .dot,.mk.ok .cross,.mk.ok .tick,.row,.bar span{animation-duration:.01ms !important;animation-delay:0s !important;} .mk.ok .cross{opacity:0;} .mk.ok .tick{opacity:1;} .mk.ok .dot{fill:var(--ok);} } .legend{display:flex;gap:18px;padding:12px 16px;border-top:1px solid var(--line);font-size:12.5px;color:var(--muted);} .lg{display:inline-flex;align-items:center;gap:7px;} .lg i{width:10px;height:10px;border-radius:50%;display:inline-block;} .lg.ok i{background:var(--ok);} .lg.warn i{background:var(--warn);} .lg.dead i{background:var(--dead);} .foot{margin-top:16px;color:var(--faint);font-family:var(--mono);font-size:11px;text-align:center;} </style>`; //# sourceMappingURL=propagation-map.js.map