()
| 7531 | const _IPINTEL_CLASS_STYLE = { |
| 7532 | vpn: ['bg-amber-950/50 border-amber-800 text-amber-300', 'VPN exit'], |
| 7533 | tor: ['bg-amber-950/50 border-amber-800 text-amber-300', 'Tor exit'], |
| 7534 | hosting: ['bg-sky-950/50 border-sky-800 text-sky-300', 'Cloud / hosting'], |
| 7535 | mobile: ['bg-slate-800 border-slate-700 text-slate-300', 'Mobile carrier'], |
| 7536 | residential: ['bg-slate-800 border-slate-700 text-slate-300', 'Consumer ISP'], |
| 7537 | special: ['bg-slate-800 border-slate-700 text-slate-400', 'Non-public address'], |
| 7538 | unknown: ['bg-slate-800 border-slate-700 text-slate-400', 'Unclassified'], |
| 7539 | }; |
| 7540 | async function runIpIntel() { |
| 7541 | const input = document.getElementById('ipintel-target'); |
| 7542 | const out = document.getElementById('ipintel-results'); |
| 7543 | const target = (input.value || '').trim(); |
| 7544 | if (!target) { input.focus(); return; } |
| 7545 | const btn = event && event.target ? event.target : null; |
| 7546 | _ndBusy(btn, true, 'Looking up…'); |
| 7547 | out.classList.remove('hidden'); |
| 7548 | out.innerHTML = '<p class="text-sm text-gray-400">Querying the regional registry…</p>'; |
| 7549 | try { |
| 7550 | const d = await postAPI('/api/net/ip-intel', { ip: target }); |
| 7551 | if (!d.success) { |
| 7552 | out.innerHTML = '<p class="text-sm text-red-400">Error: ' + escapeHtml(d.error || 'lookup failed') + '</p>'; |
| 7553 | return; |
| 7554 | } |
| 7555 | const [cls, label] = _IPINTEL_CLASS_STYLE[d.classification] || _IPINTEL_CLASS_STYLE.unknown; |
| 7556 | const row = (k, v) => v ? `<div class="flex gap-2"><span class="text-gray-500 w-28 shrink-0">${k}</span><span class="text-gray-200 break-all">${escapeHtml(String(v))}</span></div>` : ''; |
| 7557 | const country = d.country ? d.country + (d.country_routing ? ' (routing: ' + escapeHtml(d.country_routing) + ')' : '') : ''; |
| 7558 | out.innerHTML = ` |
| 7559 | <div class="rounded-lg border ${cls} px-3 py-2 mb-3"> |
| 7560 | <span class="text-sm font-semibold">${escapeHtml(d.ip)}</span> |
| 7561 | <span class="text-xs ml-2">${escapeHtml(label)}</span> |
| 7562 | <span class="text-xs opacity-70 ml-2">${d.confidence != null ? d.confidence + '% ownership resolved' : ''}${d.cached ? ' · cached' : ''}</span> |
| 7563 | </div> |
| 7564 | <div class="space-y-1 text-xs"> |
| 7565 | ${row('Network', (d.prefix || '') + (d.network_name ? ' ' + d.network_name : ''))} |
| 7566 | ${row('Allocated', d.allocated)} |
| 7567 | ${row('ASN', d.asn ? 'AS' + d.asn + (d.as_org ? ' ' + d.as_org : '') : '')} |
| 7568 | ${row('Country', country)} |
| 7569 | ${row('Registry', d.registry)} |
| 7570 | ${row('Reverse DNS', d.ptr)} |
| 7571 | ${row('Abuse contact', d.abuse_email || 'not published')} |
| 7572 | ${row('Org address', d.org_address)} |
| 7573 | ${row('Resolved from', d.resolved_from)} |
| 7574 | </div> |
| 7575 | ${d.country_note ? `<p class="text-xs text-amber-300/80 mt-2">${escapeHtml(d.country_note)}</p>` : ''} |
| 7576 | ${d.attribution_note ? `<p class="text-xs text-amber-300/90 mt-2">${escapeHtml(d.attribution_note)}</p>` : ''} |
| 7577 | <p class="text-xs text-gray-500 mt-2">${escapeHtml(d.location_note || '')}</p>`; |
| 7578 | } catch (e) { |
| 7579 | out.innerHTML = '<p class="text-sm text-red-400">Failed: ' + escapeHtml(e.message) + '</p>'; |
| 7580 | } finally { |
| 7581 | _ndBusy(btn, false); |
| 7582 | } |
| 7583 | } |
| 7584 | |
| 7585 | // ---- Watchtower: unified pane for the standalone watchers ------------------ |
| 7586 | const _WT_SEV_STYLE = { |
| 7587 | critical: ['bg-red-950/60 border-red-800 text-red-300', '🛑'], |
| 7588 | high: ['bg-orange-950/50 border-orange-800 text-orange-300', '⚠'], |
| 7589 | medium: ['bg-amber-950/50 border-amber-800 text-amber-300', '⚠'], |
| 7590 | low: ['bg-slate-800 border-slate-700 text-slate-300', 'ℹ'], |
no test coverage detected