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
JavaScript
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 => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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 `
<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 ;}}
.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 ;animation-delay:0s ;} .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