diff --git a/cmd/check-ip/web/app.js b/cmd/check-ip/web/app.js
index 37e7620..1f40d69 100644
--- a/cmd/check-ip/web/app.js
+++ b/cmd/check-ip/web/app.js
@@ -1,221 +1,192 @@
-(function () {
- "use strict";
+const App = {
+ myIpMode: false,
- function $(sel, $parent) {
- return ($parent || document).querySelector(sel);
- }
+ submit(event) {
+ event.preventDefault();
+ const input = event.target.querySelector('[data-input]');
+ const host = input.value.trim();
+ this.check(host);
+ },
- function $$(sel, $parent) {
- return Array.from(($parent || document).querySelectorAll(sel));
- }
-
- const form = $(".search-form");
- const input = $("input[type=\"text\"]", form);
- const btn = $("button", form);
- const myIpLink = $("#my-ip-link");
- const loading = $(".loading");
- const errorEl = $(".error-msg");
- const resultEl = $(".result");
- let myIpVisible = false;
-
- function show(el) { el.classList.add("visible"); }
- function hide(el) { el.classList.remove("visible"); }
-
- function render(result, isMyIp) {
- hide(errorEl);
- hide(resultEl);
- if (isMyIp) myIpVisible = true;
-
- const host = result.ip;
- const resolvedFrom = result.resolved_from || "";
- const blocked = result.blocked;
- const blockedIn = result.blocked_inbound;
- const blockedOut = result.blocked_outbound;
- const whitelisted = result.allowlisted;
- const geo = result.geo || {};
-
- // Verdict
- let verdictClass = "unknown";
- let verdictText = "UNKNOWN";
- if (whitelisted) {
- verdictClass = "whitelist";
- verdictText = "ALLOWED (whitelist)";
- } else if (blocked) {
- verdictClass = "blocked";
- verdictText = "BLOCKED";
- } else {
- verdictClass = "allowed";
- verdictText = "ALLOWED";
- }
-
- // Geo info
- let geoParts = [];
- if (geo.city) geoParts.push(geo.city);
- if (geo.region) {
- geoParts.push(geo.region + (geo.region_iso ? " (" + geo.region_iso + ")" : ""));
- } else if (geo.region_iso) {
- geoParts.push(geo.region_iso);
- }
- if (geo.country) {
- geoParts.push(geo.country + (geo.country_iso ? " (" + geo.country_iso + ")" : ""));
- }
- if (geo.continent) {
- geoParts.push(geo.continent + (geo.continent_code ? " (" + geo.continent_code + ")" : ""));
- }
- let geoStr = geoParts.length ? geoParts.join(", ") : "—";
- let latLonStr = "";
- if (geo.latitude || geo.longitude) {
- latLonStr = geo.latitude.toFixed(4) + ", " + geo.longitude.toFixed(4);
- }
- let asnStr = geo.asn ? "AS" + geo.asn + " " + (geo.asn_org || "") : "—";
- let postalStr = geo.postal_code || "";
- let tzStr = geo.timezone || "";
- let metroStr = geo.metro_code ? String(geo.metro_code) : "";
- let accuracyStr = geo.accuracy_radius ? String(geo.accuracy_radius) : "";
- let registeredStr = geo.registered_country ? (geo.registered_country + (geo.registered_country_iso ? " (" + geo.registered_country_iso + ")" : "")) : "";
-
- // Trait badges
- let traitBadges = "";
- if (geo.anycast) {
- traitBadges += 'anycast';
- }
- if (geo.anonymous_proxy) {
- traitBadges += 'anonymous proxy';
- }
- if (geo.satellite) {
- traitBadges += 'satellite';
- }
- if (geo.country_in_eu) {
- traitBadges += 'EU';
- }
-
- // Block details
- let blockHtml = "";
- if (blockedIn || blockedOut) {
- const parts = [];
- if (blockedIn) parts.push("inbound");
- if (blockedOut) parts.push("outbound");
- blockHtml = 'BLOCKED: ' + parts.join(" + ") + '';
- } else {
- blockHtml = 'clean';
- }
-
- resultEl.innerHTML =
- '
' +
- '' +
- (geoStr !== "—" ?
- '
' +
- '
Location
' +
- '
' + escapeHtml(geoStr) + '
' +
- '
' : '') +
- (latLonStr ?
- '
' +
- '
Coordinates
' +
- '
' + escapeHtml(latLonStr) + '
' +
- '
' : '') +
- (postalStr || tzStr || metroStr || accuracyStr ?
- '
' +
- '
Details
' +
- '
' +
- (postalStr ? escapeHtml(postalStr) + " " : "") +
- (tzStr ? escapeHtml(tzStr) + " " : "") +
- (metroStr ? "Metro " + escapeHtml(metroStr) + " " : "") +
- (accuracyStr ? "±" + escapeHtml(accuracyStr) + "km" : "") +
- '
' +
- '
' : '') +
- (traitBadges ?
- '
' +
- '
Traits
' +
- '
' + traitBadges + '
' +
- '
' : '') +
- (registeredStr ?
- '
' +
- '
Registered Country
' +
- '
' + escapeHtml(registeredStr) + '
' +
- '
' : '') +
- '
' +
- '
ASN
' +
- '
' + escapeHtml(asnStr) + '
' +
- '
' +
- '
' +
- '
Blocklist
' +
- '
' + blockHtml + '
' +
- '
' +
- '
';
-
- show(resultEl);
- }
-
- function escapeHtml(s) {
- const d = document.createElement("div");
- d.textContent = s;
- return d.innerHTML;
- }
-
- function submitHost(host) {
- hide(errorEl);
- hide(resultEl);
- show(loading);
+ check(host) {
+ const btn = document.querySelector('[data-submit-btn]');
btn.disabled = true;
- var isMyIp = host === "";
+ this.clearState();
+ document.querySelector('[data-state="loading"]').hidden = false;
+ this.myIpMode = host === '';
- fetch("/check?host=" + encodeURIComponent(host) + "&format=json")
- .then(function (r) {
- if (!r.ok) throw new Error("HTTP " + r.status);
+ fetch('/check?host=' + encodeURIComponent(host) + '&format=json')
+ .then(function(r) {
+ if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
- .then(function (data) {
- hide(loading);
- render(data, isMyIp);
+ .then(function(data) {
+ document.querySelector('[data-state="loading"]').hidden = true;
+ document.querySelector('[data-state="result"]').hidden = false;
+ App.render(data);
})
- .catch(function (err) {
- hide(loading);
- errorEl.textContent = "Error: " + err.message;
- show(errorEl);
+ .catch(function(err) {
+ document.querySelector('[data-state="loading"]').hidden = true;
+ document.querySelector('[data-state="error"]').hidden = false;
+ document.querySelector('[data-error-text]').textContent = err.message;
})
- .finally(function () {
+ .finally(function() {
btn.disabled = false;
});
- }
+ },
- form.addEventListener("submit", function (e) {
- e.preventDefault();
- submitHost(input.value.trim());
- });
+ clearState() {
+ document.querySelector('[data-state="result"]').hidden = true;
+ document.querySelector('[data-state="error"]').hidden = true;
+ document.querySelectorAll('[data-verdict]').forEach(function(el) { el.hidden = true; });
+ document.querySelectorAll('[data-section]').forEach(function(el) { el.hidden = true; });
+ document.querySelectorAll('[data-trait]').forEach(function(el) { el.hidden = true; });
+ document.querySelectorAll('[data-block]').forEach(function(el) { el.hidden = true; });
+ document.querySelector('[data-resolved-from]').hidden = true;
+ },
- myIpLink.addEventListener("click", function (e) {
- e.preventDefault();
- if (myIpVisible) {
- hide(resultEl);
- myIpLink.textContent = "Show my IP Info";
- myIpVisible = false;
- } else {
- myIpLink.textContent = "Hide my IP Info";
- submitHost("");
+ render(data) {
+ /* ── Verdict ──────────────────────────────────────────── */
+ document.querySelectorAll('[data-verdict]').forEach(function(el) { el.hidden = true; });
+ var verdictKey = 'allowed';
+ if (data.allowlisted) {
+ verdictKey = 'whitelist';
+ } else if (data.blocked_inbound && data.blocked_outbound) {
+ verdictKey = 'blocked-inbound-outbound';
+ } else if (data.blocked_inbound) {
+ verdictKey = 'blocked-inbound';
+ } else if (data.blocked_outbound) {
+ verdictKey = 'blocked-outbound';
}
- });
+ document.querySelector('[data-verdict="' + verdictKey + '"]').hidden = false;
- // Pre-fill from URL query param or hash fragment and auto-submit
- (function init() {
- let host = new URLSearchParams(window.location.search).get("host");
- // Also check hash fragment (e.g. /#/?host=google.com)
+ /* ── IP address ───────────────────────────────────────── */
+ document.querySelector('[data-value="ip"]').textContent = data.ip;
+
+ /* ── Resolved from ────────────────────────────────────── */
+ if (data.resolved_from) {
+ document.querySelector('[data-value="resolved-from"]').textContent = data.resolved_from;
+ document.querySelector('[data-resolved-from]').hidden = false;
+ }
+
+ /* ── Geo ──────────────────────────────────────────────── */
+ var geo = data.geo || {};
+
+ /* Location */
+ var locParts = [];
+ if (geo.city) locParts.push(geo.city);
+ if (geo.region) {
+ locParts.push(geo.region + (geo.region_iso ? ' (' + geo.region_iso + ')' : ''));
+ } else if (geo.region_iso) {
+ locParts.push(geo.region_iso);
+ }
+ if (geo.country) {
+ locParts.push(geo.country + (geo.country_iso ? ' (' + geo.country_iso + ')' : ''));
+ }
+ if (geo.continent) {
+ locParts.push(geo.continent + (geo.continent_code ? ' (' + geo.continent_code + ')' : ''));
+ }
+ if (locParts.length) {
+ document.querySelector('[data-value="location"]').textContent = locParts.join(', ');
+ document.querySelector('[data-section="location"]').hidden = false;
+ }
+
+ /* Coordinates */
+ if (geo.latitude || geo.longitude) {
+ document.querySelector('[data-value="coordinates"]').textContent =
+ geo.latitude.toFixed(4) + ', ' + geo.longitude.toFixed(4);
+ document.querySelector('[data-section="coordinates"]').hidden = false;
+ }
+
+ /* Details */
+ var detailParts = [];
+ if (geo.postal_code) detailParts.push(geo.postal_code);
+ if (geo.timezone) detailParts.push(geo.timezone);
+ if (geo.metro_code) detailParts.push('Metro ' + String(geo.metro_code));
+ if (geo.accuracy_radius) detailParts.push('±' + String(geo.accuracy_radius) + 'km');
+ if (detailParts.length) {
+ document.querySelector('[data-value="details"]').textContent = detailParts.join(' ');
+ document.querySelector('[data-section="details"]').hidden = false;
+ }
+
+ /* Traits */
+ var traitMap = {
+ anycast: 'anycast',
+ anonymous: 'anonymous_proxy',
+ satellite: 'satellite',
+ eu: 'country_in_eu'
+ };
+ var hasTraits = false;
+ var traitKeys = Object.keys(traitMap);
+ for (var i = 0; i < traitKeys.length; i++) {
+ if (geo[traitMap[traitKeys[i]]]) {
+ document.querySelector('[data-trait="' + traitKeys[i] + '"]').hidden = false;
+ hasTraits = true;
+ }
+ }
+ if (hasTraits) {
+ document.querySelector('[data-section="traits"]').hidden = false;
+ }
+
+ /* Registered country */
+ if (geo.registered_country) {
+ var rcParts = geo.registered_country +
+ (geo.registered_country_iso ? ' (' + geo.registered_country_iso + ')' : '');
+ document.querySelector('[data-value="registered-country"]').textContent = rcParts;
+ document.querySelector('[data-section="registered-country"]').hidden = false;
+ }
+
+ /* ASN */
+ if (geo.asn) {
+ document.querySelector('[data-value="asn"]').textContent =
+ 'AS' + geo.asn + ' ' + (geo.asn_org || '');
+ } else {
+ document.querySelector('[data-value="asn"]').textContent = '—';
+ }
+
+ /* Blocklist */
+ var hasBlock = false;
+ if (data.blocked_inbound) {
+ document.querySelector('[data-block="inbound"]').hidden = false;
+ hasBlock = true;
+ }
+ if (data.blocked_outbound) {
+ document.querySelector('[data-block="outbound"]').hidden = false;
+ hasBlock = true;
+ }
+ if (!hasBlock) {
+ document.querySelector('[data-block="clean"]').hidden = false;
+ }
+ },
+
+ toggleIpInfo(event) {
+ event.preventDefault();
+ var link = document.querySelector('[data-my-ip-link]');
+ if (this.myIpMode) {
+ document.querySelector('[data-state="result"]').hidden = true;
+ link.textContent = 'Show my IP Info';
+ this.myIpMode = false;
+ } else {
+ link.textContent = 'Hide my IP Info';
+ this.check('');
+ }
+ },
+
+ init() {
+ var host = new URLSearchParams(window.location.search).get('host');
if (!host) {
- const hashQuery = window.location.hash.split("?")[1];
+ var hashQuery = window.location.hash.split('?')[1];
if (hashQuery) {
- host = new URLSearchParams(hashQuery).get("host");
+ host = new URLSearchParams(hashQuery).get('host');
}
}
if (host) {
- input.value = host;
- submitHost(host);
+ document.querySelector('[data-input]').value = host;
+ this.check(host);
+ } else {
+ document.querySelector('[data-input]').focus();
}
- // Focus input on load (only if no auto-submit triggered)
- if (!host) {
- input.focus();
- }
- })();
-})();
+ }
+};
+
+App.init();
diff --git a/cmd/check-ip/web/index.html b/cmd/check-ip/web/index.html
index 167ed8d..6492a62 100644
--- a/cmd/check-ip/web/index.html
+++ b/cmd/check-ip/web/index.html
@@ -3,33 +3,88 @@
-
IP Location & Status
+
+ IP Location & Status
Look up an IP address, domain, or your own IP against blocklists and GeoIP databases.
-
- Show my IP Info
+ Show my IP Info
-
-
Resolving and checking…
+
+
+ Resolving and checking…
-
+
+ Error:
+
+
-
+
+
+
+
+
+
+
+
Traits
+
+ anycast
+ anonymous proxy
+ satellite
+ EU
+
+
+
+
+
+
Blocklist
+
+ inbound
+ outbound
+ clean
+
+
+
+
-
+
diff --git a/cmd/check-ip/web/style.css b/cmd/check-ip/web/style.css
index 3e8b3a8..ea095dd 100644
--- a/cmd/check-ip/web/style.css
+++ b/cmd/check-ip/web/style.css
@@ -1,5 +1,6 @@
/* ── Reset & base ─────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+[hidden] { display: none !important; }
:root {
--bg: #fafafa;
@@ -60,7 +61,7 @@ html {
}
body {
- min-height: 100dvh;
+ min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
@@ -126,6 +127,7 @@ button {
button:hover { background: var(--primary-hover); }
button:active { transform: scale(.98); }
+button:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
button:disabled {
opacity: .6;
cursor: not-allowed;
@@ -133,7 +135,6 @@ button:disabled {
/* ── Result card ──────────────────────────────────────────── */
.result {
- display: none;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
@@ -141,8 +142,6 @@ button:disabled {
overflow: hidden;
}
-.result.visible { display: block; }
-
.result-header {
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--border);
@@ -165,6 +164,7 @@ button:disabled {
word-break: break-all;
}
+/* ── Verdict badge ────────────────────────────────────────── */
.verdict {
display: inline-flex;
align-items: center;
@@ -178,30 +178,25 @@ button:disabled {
white-space: nowrap;
}
-.verdict.allowed {
+.verdict.verdict-allowed {
background: var(--ok-bg);
color: var(--ok);
border: 1px solid var(--ok-border);
}
-.verdict.blocked {
+.verdict.verdict-blocked {
background: var(--bad-bg);
color: var(--bad);
border: 1px solid var(--bad-border);
}
-.verdict.whitelist {
+.verdict.verdict-whitelist {
background: var(--warn-bg);
color: var(--warn);
border: 1px solid var(--warn-border);
}
-.verdict.unknown {
- background: var(--bg);
- color: var(--muted);
- border: 1px solid var(--border);
-}
-
+/* ── Result body ──────────────────────────────────────────── */
.result-body {
padding: 1.25rem;
}
@@ -237,6 +232,7 @@ button:disabled {
gap: .375rem;
}
+/* ── Block badge ──────────────────────────────────────────── */
.block-badge {
font-size: .75rem;
font-weight: 600;
@@ -313,15 +309,12 @@ button:disabled {
/* ── Loading spinner ──────────────────────────────────────── */
.loading {
- display: none;
text-align: center;
padding: 2rem;
color: var(--muted);
font-size: .875rem;
}
-.loading.visible { display: block; }
-
.spinner {
display: inline-block;
width: 1.25rem;
@@ -338,7 +331,6 @@ button:disabled {
/* ── Error ────────────────────────────────────────────────── */
.error-msg {
- display: none;
background: var(--bad-bg);
color: var(--bad);
border: 1px solid var(--bad-border);
@@ -348,8 +340,6 @@ button:disabled {
font-size: .875rem;
}
-.error-msg.visible { display: block; }
-
/* ── My IP link ───────────────────────────────────────────── */
.my-ip-link {
display: inline-block;
@@ -364,6 +354,7 @@ button:disabled {
}
.my-ip-link:hover { text-decoration-color: var(--primary); }
+.my-ip-link:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
/* ── Footer ───────────────────────────────────────────────── */
footer {