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 = - '
' + - '' + verdictText + '' + - '' + escapeHtml(host) + '' + - (resolvedFrom ? '→ ' + escapeHtml(resolvedFrom) + '' : '') + - '
' + - '
' + - (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 -
-

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… + -
+ -
+ -
+
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 {