On branch main
Initial commit Changes to be committed: new file: api.php new file: index.php new file: readme.md
This commit is contained in:
325
index.php
Normal file
325
index.php
Normal file
@@ -0,0 +1,325 @@
|
||||
<!doctype html>
|
||||
<html lang="da">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>DNS Propagation Checker</title>
|
||||
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<style>
|
||||
.glass {
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, rgba(255, 255, 255, .05), rgba(255, 255, 255, .12), rgba(255, 255, 255, .05));
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.2s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding-top: .45rem !important;
|
||||
padding-bottom: .45rem !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="min-h-screen bg-slate-950 text-slate-100">
|
||||
<!-- Header -->
|
||||
<header class="border-b border-white/10 bg-white/5 glass">
|
||||
<div class="mx-auto max-w-6xl px-4 py-6">
|
||||
<div class="flex flex-col gap-3">
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-slate-100">
|
||||
DNS Propagation Checker
|
||||
</h1>
|
||||
|
||||
<p class="text-sm leading-relaxed text-slate-300">
|
||||
This tool performs global DNS lookups using public DNS resolvers in multiple countries.
|
||||
Results are shown in a compact overview with TTL and record data displayed directly, making it easy to compare DNS responses across locations.
|
||||
A resolver is only marked as OK when it actually returns valid DNS answers.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-wrap gap-2 text-xs">
|
||||
<span class="rounded-full border border-white/10 bg-slate-950/30 px-2 py-1 text-slate-300">
|
||||
🔒 Privacy-friendly · No data logging
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main content -->
|
||||
|
||||
<div class="mx-auto max-w-6xl px-4 py-8">
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="mt-6 rounded-2xl border border-white/10 bg-white/5 p-4 glass">
|
||||
<form id="form" class="grid grid-cols-1 md:grid-cols-12 gap-3 items-end">
|
||||
<div class="md:col-span-5">
|
||||
<label class="text-sm text-slate-300">Domain</label>
|
||||
<input id="domain" required class="w-full rounded-xl border border-white/10 bg-slate-950/50 px-3 py-2.5" placeholder="example.com" />
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2">
|
||||
<label class="text-sm text-slate-300">Type</label>
|
||||
<select id="type" class="w-full rounded-xl border border-white/10 bg-slate-950/50 px-3 py-2.5">
|
||||
<option>A</option>
|
||||
<option>AAAA</option>
|
||||
<!--<option>CNAME</option> -->
|
||||
<option>MX</option>
|
||||
<option>NS</option>
|
||||
<option>TXT</option>
|
||||
<!-- <option>SOA</option> -->
|
||||
<!-- <option>SRV</option> -->
|
||||
<!-- <option>CAA</option> -->
|
||||
<!-- <option>PTR</option> -->
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-5">
|
||||
<label class="text-sm text-slate-300">Expected value (optional)</label>
|
||||
<input id="expected" class="w-full rounded-xl border border-white/10 bg-slate-950/50 px-3 py-2.5" placeholder="93.184.216.34" />
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-12 flex flex-wrap gap-3 mt-2 items-center justify-between">
|
||||
<button id="submitBtn" class="inline-flex items-center justify-center gap-2 rounded-lg
|
||||
bg-slate-200 text-slate-900
|
||||
px-4 py-2 text-sm font-medium
|
||||
hover:bg-white
|
||||
focus:outline-none focus:ring-2 focus:ring-white/40
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
transition">
|
||||
🔎 Run lookup
|
||||
</button>
|
||||
|
||||
<div class="flex items-center gap-4 text-sm text-slate-300">
|
||||
<label><input id="onlyErrors" type="checkbox"> Errors only</label>
|
||||
<label><input id="onlyMismatches" type="checkbox"> Mismatches only</label>
|
||||
<input id="filterText" class="rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-sm" placeholder="Filter…" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<div id="summaryBadges" class="mt-4 flex flex-wrap gap-2"></div>
|
||||
|
||||
<!-- Results -->
|
||||
<div class="mt-4 rounded-2xl border border-white/10 bg-white/5 glass overflow-hidden">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-slate-950/70 border-b border-white/10">
|
||||
<tr class="text-left text-slate-300">
|
||||
<th class="px-4">Resolver</th>
|
||||
<th class="px-4">Status</th>
|
||||
<th class="px-4">Time</th>
|
||||
<th class="px-4">TTL</th>
|
||||
<th class="px-4">Data</th>
|
||||
<th class="px-4">Match</th>
|
||||
<th class="px-4">Detaljer</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="rows">
|
||||
<tr>
|
||||
<td colspan="7" class="px-4 py-4 text-slate-400">Run lookup to see results…</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="mt-10 border-t border-white/10 bg-white/5 text-center">
|
||||
<div class="max-w-7xl mx-auto px-4 py-4 text-xs text-slate-400">
|
||||
© <span id="year"></span> <a href="https://dicm.dk/">dicm.dk</a>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Auto-opdater årstal
|
||||
document.getElementById('year').textContent = new Date().getFullYear();
|
||||
</script>
|
||||
|
||||
|
||||
<script>
|
||||
function flagFromISO2(code) {
|
||||
if (!code || code.length !== 2) return "";
|
||||
const cc = code.toUpperCase();
|
||||
// regional indicator symbols
|
||||
const A = 0x1F1E6;
|
||||
const first = cc.charCodeAt(0) - 65 + A;
|
||||
const second = cc.charCodeAt(1) - 65 + A;
|
||||
return String.fromCodePoint(first) + String.fromCodePoint(second);
|
||||
}
|
||||
|
||||
|
||||
const $ = id => document.getElementById(id);
|
||||
const rowsEl = $("rows");
|
||||
const summaryEl = $("summaryBadges");
|
||||
|
||||
let lastResponse = null;
|
||||
|
||||
/* ---------- helpers ---------- */
|
||||
|
||||
function hasAnswers(it) {
|
||||
return Array.isArray(it?.answers) && it.answers.length > 0;
|
||||
}
|
||||
|
||||
function effectiveStatus(it) {
|
||||
if (it?.status === "OK" && !hasAnswers(it)) return "NO_RESULT";
|
||||
return it?.status || "ERROR";
|
||||
}
|
||||
|
||||
function badge(text, cls) {
|
||||
return `<span class="rounded-full border px-2 py-0.5 text-xs ${cls}">${text}</span>`;
|
||||
}
|
||||
|
||||
function summarizeData(answers) {
|
||||
if (!answers || !answers.length) return {
|
||||
ttl: "—",
|
||||
data: "—"
|
||||
};
|
||||
return {
|
||||
ttl: answers[0].ttl ?? "—",
|
||||
data: answers.map(a => a.data).filter(Boolean).slice(0, 3).join(" <br><br> ")
|
||||
};
|
||||
}
|
||||
|
||||
/* ---------- rendering ---------- */
|
||||
|
||||
function renderSummary(resp) {
|
||||
const results = resp.results || [];
|
||||
const ok = results.filter(r => effectiveStatus(r) === "OK").length;
|
||||
const err = results.length - ok;
|
||||
|
||||
const expected = !!resp.expected;
|
||||
const matches = expected ? results.filter(r => r.matched === true && effectiveStatus(r) === "OK").length : 0;
|
||||
const mismatches = expected ? results.filter(r => r.matched === false && effectiveStatus(r) === "OK").length : 0;
|
||||
|
||||
summaryEl.innerHTML = [
|
||||
badge(`${results.length} resolvers`, "border-white/10 bg-white/5"),
|
||||
badge(`${ok} OK`, "border-emerald-400/30 bg-emerald-400/10 text-emerald-200"),
|
||||
badge(`${err} errors`, err ? "border-rose-400/30 bg-rose-400/10 text-rose-200" : "border-white/10 bg-white/5"),
|
||||
expected ? badge(`${matches} matches`, "border-emerald-400/30 bg-emerald-400/10 text-emerald-200") : "",
|
||||
expected ? badge(`${mismatches} mismatches`, mismatches ? "border-amber-400/30 bg-amber-400/10 text-amber-200" : "border-white/10 bg-white/5") : ""
|
||||
].join("");
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str ?? "")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
|
||||
function renderRows(resp) {
|
||||
lastResponse = resp;
|
||||
renderSummary(resp);
|
||||
|
||||
const filter = $("filterText").value.toLowerCase();
|
||||
const onlyErrors = $("onlyErrors").checked;
|
||||
const onlyMismatches = $("onlyMismatches").checked && resp.expected;
|
||||
|
||||
const items = resp.results.filter(it => {
|
||||
const st = effectiveStatus(it);
|
||||
if (onlyErrors && st === "OK") return false;
|
||||
if (onlyMismatches && it.matched !== false) return false;
|
||||
if (!filter) return true;
|
||||
return JSON.stringify(it).toLowerCase().includes(filter);
|
||||
});
|
||||
|
||||
if (!items.length) {
|
||||
rowsEl.innerHTML = `<tr><td colspan="7" class="px-4 py-4 text-slate-400">Ingen resultater</td></tr>`;
|
||||
return;
|
||||
}
|
||||
|
||||
rowsEl.innerHTML = items.map(it => {
|
||||
const st = effectiveStatus(it);
|
||||
const statusBadge =
|
||||
st === "OK" ? badge("OK", "border-emerald-400/30 bg-emerald-400/10 text-emerald-200") :
|
||||
st === "NO RESULT" ? badge("NO RESULT", "border-rose-400/30 bg-rose-400/10 text-rose-200") :
|
||||
badge("ERROR", "border-rose-400/30 bg-rose-400/10 text-rose-200");
|
||||
|
||||
const {
|
||||
ttl,
|
||||
data
|
||||
} = summarizeData(it.answers);
|
||||
|
||||
const match =
|
||||
resp.expected ?
|
||||
it.matched === true ? badge("match", "border-emerald-400/30 bg-emerald-400/10 text-emerald-200") :
|
||||
it.matched === false ? badge("no", "border-amber-400/30 bg-amber-400/10 text-amber-200") :
|
||||
"—" :
|
||||
"—";
|
||||
|
||||
const flag = flagFromISO2(it.countryCode);
|
||||
const country = it.country || "";
|
||||
const city = it.city ? ` · ${it.city}` : "";
|
||||
|
||||
return `
|
||||
<tr class="border-b border-white/5 hover:bg-white/5">
|
||||
<td class="px-4 font-semibold"> <div class="font-semibold text-slate-100">
|
||||
${flag ? flag + " " : ""}${escapeHtml(country)} — ${escapeHtml(it.resolver)}
|
||||
</div>
|
||||
<div class="text-xs text-slate-400">${escapeHtml(it.city || "")}</div></td>
|
||||
<td class="px-4">${statusBadge}</td>
|
||||
<td class="px-4">${it.timeMs ?? "—"}ms</td>
|
||||
<td class="px-4">${ttl}</td>
|
||||
<td class="px-4 mono max-w-[520px]">${data}</td>
|
||||
<td class="px-4">${match}</td>
|
||||
<td class="px-4">
|
||||
<details>
|
||||
<summary class="cursor-pointer text-xs underline">vis</summary>
|
||||
<pre class="mono text-xs mt-2 max-h-56 overflow-auto">${JSON.stringify(it, null, 2)}</pre>
|
||||
</details>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
/* ---------- events ---------- */
|
||||
|
||||
$("form").addEventListener("submit", async e => {
|
||||
e.preventDefault();
|
||||
rowsEl.innerHTML = `<tr><td colspan="7" class="px-4 py-4">⏳ Running</td></tr>`;
|
||||
|
||||
const url = new URL("./api.php", location.href);
|
||||
url.searchParams.set("domain", $("domain").value);
|
||||
url.searchParams.set("type", $("type").value);
|
||||
if ($("expected").value.trim()) url.searchParams.set("expected", $("expected").value.trim());
|
||||
|
||||
const r = await fetch(url);
|
||||
const data = await r.json();
|
||||
renderRows(data);
|
||||
});
|
||||
|
||||
["filterText", "onlyErrors", "onlyMismatches"].forEach(id =>
|
||||
$(id).addEventListener("input", () => lastResponse && renderRows(lastResponse))
|
||||
);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user