diff --git a/public/app.min.js b/public/app.min.js
index 78243ef..e8315e8 100644
--- a/public/app.min.js
+++ b/public/app.min.js
@@ -1 +1 @@
-function initTheme(){"light"===localStorage.theme?document.documentElement.classList.remove("dark"):document.documentElement.classList.add("dark")}function toggleTheme(){const e=document.documentElement;e.classList.contains("dark")?(e.classList.remove("dark"),localStorage.theme="light",updateMapTheme("light")):(e.classList.add("dark"),localStorage.theme="dark",updateMapTheme("dark"))}let map;document.getElementById("year").innerText=(new Date).getFullYear(),initTheme();let pendingMapLocation=null,currentStyle="dark";const MAP_STYLES={dark:"https://tiles.openfreemap.org/styles/dark",light:"https://tiles.openfreemap.org/styles/positron",satellite:{version:8,sources:{esri:{type:"raster",tiles:["https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"],tileSize:256,attribution:'Tiles © Esri',maxzoom:19}},layers:[{id:"esri-tiles",type:"raster",source:"esri"}]}};function updateStyleSwitcherState(){const e=document.getElementById("map-style-switcher");if(!e)return;const t=["dark","light","satellite"];Array.from(e.children).forEach((e,n)=>{e.className=t[n]===currentStyle?"bg-zinc-200 dark:bg-zinc-700 text-zinc-900 dark:text-zinc-100 font-bold text-[10px] uppercase tracking-wide !px-3 !w-auto h-[29px] border-r border-zinc-300 dark:border-zinc-600 last:border-0 transition-colors":"bg-transparent text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800 hover:text-zinc-900 dark:hover:text-zinc-200 text-[10px] uppercase tracking-wide !px-3 !w-auto h-[29px] border-r border-zinc-200 dark:border-zinc-700/50 last:border-0 transition-colors"})}function addStyleSwitcher(){const e=document.createElement("div");e.id="map-style-switcher",e.className="maplibregl-ctrl maplibregl-ctrl-group flex flex-row overflow-hidden",e.style.boxShadow="0 0 0 2px rgba(0,0,0,0.1)";[{id:"dark",label:"Dark"},{id:"light",label:"Light"},{id:"satellite",label:"Sat"}].forEach(t=>{const n=document.createElement("button");n.type="button",n.textContent=t.label,n.title=t.label+" Mode",n.style.width="auto",n.style.padding="0 12px",n.style.display="flex",n.style.alignItems="center",n.style.justifyContent="center",n.addEventListener("click",()=>switchMapStyle(t.id)),e.appendChild(n)});const t={onAdd:()=>e,onRemove:()=>e.remove()};map.addControl(t,"top-left"),updateStyleSwitcherState()}function switchMapStyle(e){if(!map||e===currentStyle)return;currentStyle=e,updateStyleSwitcherState();const t=map.getCenter(),n=map.getZoom();map.setStyle(MAP_STYLES[e]),map.once("style.load",()=>{map.setCenter(t),map.setZoom(n),marker&&marker.addTo(map)})}class CustomAttributionControl{onAdd(e){this._map=e,this._container=document.createElement("div"),this._container.className="maplibregl-ctrl maplibregl-ctrl-group",this._container.style.display="flex",this._container.style.flexDirection="row-reverse",this._container.style.alignItems="center",this._container.style.maxWidth="29px",this._container.style.transition="max-width 0.3s ease",this._container.style.overflow="hidden",this._container.style.whiteSpace="nowrap";const t=document.createElement("button");return t.type="button",t.title="Map Attributions",t.className="custom-map-btn",t.style.width="29px",t.style.height="29px",t.style.flexShrink="0",t.style.display="flex",t.style.alignItems="center",t.style.justifyContent="center",t.innerHTML='',this._textDiv=document.createElement("div"),this._textDiv.className="text-[10px] text-zinc-600 dark:text-zinc-400 pl-3 pr-1",this._textDiv.style.opacity="0",this._textDiv.style.transition="opacity 0.2s ease",this._updateAttributions=()=>{const e=this._map.getStyle();if(!e||!e.sources)return;const t=new Set;Object.keys(e.sources).forEach(e=>{const n=this._map.getSource(e);n&&n.attribution&&t.add(n.attribution)});let n=Array.from(t).join(" | ");!n&&this._textDiv.innerHTML&&"Map data"!==this._textDiv.innerHTML||(n=n.replace(/{this._updateAttributions(),this._container.style.maxWidth="500px",this._textDiv.style.opacity="1"}),this._container.addEventListener("mouseleave",()=>{this._container.style.maxWidth="29px",this._textDiv.style.opacity="0"}),this._container.appendChild(t),this._container.appendChild(this._textDiv),this._container}onRemove(){this._map.off("styledata",this._updateAttributions),this._map.off("sourcedata",this._updateAttributions),this._container.parentNode.removeChild(this._container),this._map=void 0}}class CenterMapControl{onAdd(e){this._map=e,this._container=document.createElement("div"),this._container.className="maplibregl-ctrl maplibregl-ctrl-group";const t=document.createElement("button");return t.type="button",t.title="Center on IP",t.className="custom-map-btn",t.style.display="flex",t.style.alignItems="center",t.style.justifyContent="center",t.innerHTML='',t.onclick=()=>{window.currentCoords&&e.flyTo({center:window.currentCoords,zoom:13,speed:1.5})},this._container.appendChild(t),this._container}onRemove(){this._container.parentNode.removeChild(this._container),this._map=void 0}}function setOrQueueMapLocation(e,t,n){if(!map)return void(pendingMapLocation={lat:e,lon:t,city:n});map.resize(),window.currentCoords=[t,e],map.flyTo({center:[t,e],zoom:13,speed:1.5}),marker&&marker.remove();const a=document.createElement("div");a.className="custom-marker-wrapper",a.innerHTML='\n \n ';const r=new maplibregl.Popup({offset:15,focusAfterOpen:!1}).setHTML(`${n}`);marker=new maplibregl.Marker({element:a}).setLngLat([t,e]).setPopup(r).addTo(map),marker.togglePopup()}function setupLazyMap(){const e=document.getElementById("map");if(!e)return;const t=()=>{if(!map)if(window.maplibregl){try{initMap()}catch(e){console.error("Map initialization failed:",e)}pendingMapLocation&&(setOrQueueMapLocation(pendingMapLocation.lat,pendingMapLocation.lon,pendingMapLocation.city),pendingMapLocation=null)}else setTimeout(t,100)},n=()=>{if("IntersectionObserver"in window){const n=new IntersectionObserver(e=>{e[0].isIntersecting&&(t(),n.disconnect())},{rootMargin:"300px"});n.observe(e)}else t()};"complete"===document.readyState?setTimeout(n,500):window.addEventListener("load",()=>setTimeout(n,500))}function initMap(){const e=document.documentElement.classList.contains("dark");currentStyle=e?"dark":"light";const t="/maplibre/maplibre-gl-worker.js?v=20260926";window.maplibregl.setWorkerUrl?window.maplibregl.setWorkerUrl(t):window.maplibregl.config&&(window.maplibregl.config.WORKER_URL=t),map=new maplibregl.Map({container:"map",style:MAP_STYLES[currentStyle],center:[-.09,51.505],zoom:13,cooperativeGestures:!0,attributionControl:!1}),map.addControl(new maplibregl.NavigationControl({showCompass:!1}),"top-right"),map.addControl(new CenterMapControl,"top-right"),map.addControl(new CustomAttributionControl,"bottom-right"),map.on("load",()=>{map.resize(),addStyleSwitcher()})}function updateMapTheme(e){map&&"satellite"!==currentStyle&&switchMapStyle("dark"===e?"dark":"light")}let marker;window.currentScanIp="";let lastReputationResult=null;function showToast(e){const t=document.getElementById("toast");t.innerText=e,t.className="show",setTimeout(()=>{t.className=t.className.replace("show","")},3e3)}function copyWithFeedback(e,t,n){navigator.clipboard.writeText(e).then(()=>{showToast(`${n} Copied!`);const e=document.getElementById(t),a=e.innerHTML;e.innerHTML='',setTimeout(()=>{e.innerHTML=a},2e3)})}function createIpRow(e,t){const n="IPv6"===t,a=`copy-icon-${t}`;return`\n \n `}async function fetchSmartIPs(){const e=document.getElementById("ip-display-area");try{let t="/api/info";const n=window.location.search.substring(1).trim();let a=null;if(n){const e=new URLSearchParams(window.location.search);e.has("ip")?a=e.get("ip"):n.includes("=")||(a=n)}if(a){t=`/api/info?ip=${encodeURIComponent(a)}`;const e=document.getElementById("searchInput");e&&(e.value=a)}const[r,o]=await Promise.all([fetch("/api/config").catch(()=>null),fetch(t)]);let s={};if(r&&r.ok)try{s=await r.json()}catch(e){}setupLazyMap();const i=await o.json();if(i.error)throw new Error(i.error);const d=i.ip.includes(":"),l=d?"IPv6":"IPv4";if(e.innerHTML=createIpRow(i.ip,l),populateDetails(i),!a&&s.v4_url&&s.v6_url){const t=d?s.v4_url:s.v6_url,n=d?"IPv4":"IPv6";try{const a=await fetch(t);if(a.ok){const t=await a.json();if(t.ip&&t.ip!==i.ip){const a=t.ip.includes(":");(d&&!a||!d&&a)&&(e.innerHTML+=createIpRow(t.ip,n))}}}catch(e){console.log("Secondary protocol unavailable.")}}}catch(t){e.innerHTML='Error loading IP',console.error(t)}}function populateDetails(e){window.currentScanIp=e.ip,resetReputationUI(),document.getElementById("dataOrg").innerText=e.org||"N/A",document.getElementById("dataAsn").innerText=e.asn||"N/A",e.network&&"N/A"!==e.network?(document.getElementById("dataNetwork").innerText=e.network,document.getElementById("networkWrapper").classList.remove("hidden")):document.getElementById("networkWrapper").classList.add("hidden"),e.hostname&&"N/A"!==e.hostname?(document.getElementById("dataHostname").innerText=e.hostname,document.getElementById("hostnameWrapper").classList.remove("hidden")):document.getElementById("hostnameWrapper").classList.add("hidden"),document.getElementById("dataCity").innerText=e.city,document.getElementById("mainLocation").innerText=`${e.city}, ${e.country}`,document.getElementById("dataRegion").innerText=`${e.region}, ${e.country}`,e.zip&&"N/A"!==e.zip&&"-"!==e.zip?(document.getElementById("dataZip").innerText=e.zip,document.getElementById("zipWrapper").style.display="inline"):document.getElementById("zipWrapper").style.display="none",document.getElementById("dataTimezone").innerText=e.timezone,document.getElementById("dataCoords").innerText=e.coordinates,document.getElementById("jsonPreview").innerHTML=syntaxHighlight(e);const t=document.getElementById("dataProxyBadge");e.is_proxy?(t.innerText=e.proxy_type,t.className="px-2 py-0.5 rounded text-xs font-bold bg-red-500/20 text-red-600 dark:text-red-400 border border-red-500/20 shadow-[0_0_10px_rgba(239,68,68,0.2)]"):"Residential"===e.usage_type||"Mobile Data"===e.usage_type?(t.innerText=e.usage_type,t.className="px-2 py-0.5 rounded text-xs font-bold bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20"):(t.innerText=e.usage_type||"Clean",t.className="px-2 py-0.5 rounded text-xs font-bold bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20");const n=document.getElementById("providerRow"),a=document.getElementById("dataProvider");e.provider&&"N/A"!==e.provider&&"-"!==e.provider?(a.innerText=e.provider,n.style.display="flex"):n.style.display="none";const r=document.getElementById("threatRow"),o=document.getElementById("dataThreat");e.threat&&"None"!==e.threat&&"-"!==e.threat?(o.innerText=e.threat,r.style.display="flex",e.threat.includes("High")||e.threat.includes("SPAM")||e.threat.includes("SCANNER")?o.className="text-sm font-bold text-red-600 dark:text-red-400":o.className="text-sm font-medium text-orange-600 dark:text-orange-400"):r.style.display="none",e.latitude&&e.longitude&&setOrQueueMapLocation(parseFloat(e.latitude),parseFloat(e.longitude),e.city)}async function searchIp(){const e=document.getElementById("searchInput").value.trim();if(e)try{const t=await fetch(`/api/info?ip=${encodeURIComponent(e)}`),n=await t.json();populateDetails(n);const a=e.includes(":")?"IPv6":"IPv4";document.getElementById("ip-display-area").innerHTML=createIpRow(n.ip,a),document.getElementById("mainLocation").innerText=`${n.city}, ${n.country}`}catch(e){console.error(e)}}function copyText(e){navigator.clipboard.writeText(document.getElementById(e).innerText.replace("$ ","")).then(()=>showToast("Command Copied!"))}function resetReputationUI(){const e=document.getElementById("btn-scan");e&&(e.classList.remove("hidden"),e.disabled=!1,e.innerHTML=' SCAN THREATS'),document.getElementById("rep-badge").classList.add("hidden"),lastReputationResult=null}async function checkReputation(){const e=document.getElementById("btn-scan"),t=document.getElementById("rep-badge");e.disabled=!0,e.innerHTML=' SCANNING...';try{const n=await fetch(`/api/reputation?ip=${window.currentScanIp}`),a=await n.json();if(lastReputationResult=a,e.classList.add("hidden"),e.disabled=!1,t.classList.remove("hidden"),t.onclick=openReputationModal,t.style.cursor="pointer",a.is_clean)t.innerHTML='NO THREATS DETECTED ⓘ',t.className="px-2 py-1 rounded text-[11px] font-bold bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30 hover:bg-emerald-500/30 transition-colors whitespace-nowrap flex items-center gap-1";else{if(!a.detections)throw new Error("Invalid API response");t.innerText=`${a.detections.length} THREATS FOUND ⚠️`,t.className="px-2 py-0.5 rounded text-xs font-bold bg-red-500/20 text-red-600 dark:text-red-400 border border-red-500/30 shadow-[0_0_10px_rgba(239,68,68,0.3)] hover:bg-red-500/30 transition-colors"}}catch(t){console.error(t),e.innerHTML="ERROR - TRY AGAIN",e.disabled=!1}}function openReputationModal(){if(!lastReputationResult)return;const e=document.getElementById("rep-modal"),t=document.getElementById("modal-content"),n=document.getElementById("rep-modal-backdrop"),a=document.getElementById("rep-modal-panel");if(e.classList.remove("hidden"),document.body.style.overflow="hidden",lastReputationResult.is_clean){const e=`IP: ${lastReputationResult.ip} - Status: CLEAN (No threats detected in active feeds)`;t.innerHTML=`\n \n
\n
\n
No Threats Detected
\n
IP: ${lastReputationResult.ip}
\n
\n
\n \n
\n This IP address was not found in any of our active threat intelligence feeds (Blocklists, Spam lists, or CrowdSec).\n
\n ⚠️ Note:\n "No threats detected" does not guarantee safety. An IP can be malicious but not yet listed.\n
\n
\n \n `}else{let e="",n=[];lastReputationResult.detections.forEach(t=>{n.push(t.source),e+=`\n \n
\n ${t.source}\n ${t.reason||"Listed in blocklist"}\n
\n
Listed\n
\n `});const a=`⚠️ THREAT DETECTED ⚠️\\nIP: ${lastReputationResult.ip}\\nListed By: ${n.join(", ")}`;t.innerHTML=`\n \n
\n
\n
Threats Detected
\n
IP: ${lastReputationResult.ip}
\n
\n
\n The following security providers have flagged this IP:
\n \n ${e}\n
\n \n `}setTimeout(()=>{n.classList.remove("opacity-0"),a.classList.remove("opacity-0","scale-95"),a.classList.add("opacity-100","scale-100")},10)}function closeReputationModal(){const e=document.getElementById("rep-modal"),t=document.getElementById("rep-modal-backdrop"),n=document.getElementById("rep-modal-panel");t.classList.add("opacity-0"),n.classList.add("opacity-0","scale-95"),n.classList.remove("opacity-100","scale-100"),setTimeout(()=>{e.classList.add("hidden"),document.body.style.overflow=""},300)}async function checkWhois(){const e=document.getElementById("btn-whois"),t=document.getElementById("whois-content");e.disabled=!0,e.innerText="LOADING...";const n=document.getElementById("whois-modal"),a=document.getElementById("whois-modal-backdrop"),r=document.getElementById("whois-modal-panel");t.innerHTML='\n \n
\n
Looking up registry data...
\n
\n ',n.classList.remove("hidden"),document.body.style.overflow="hidden",setTimeout(()=>{a.classList.remove("opacity-0"),r.classList.remove("opacity-0","scale-95"),r.classList.add("opacity-100","scale-100")},10);try{const e=await fetch(`/api/whois?ip=${window.currentScanIp}`),n=await e.json();if(n.error)throw new Error(n.error);if(n.is_raw){const e=/([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)/gi;let a=n.raw_whois.replace(//g,">").replace(e,'$1');t.innerHTML=`\n
\n${a}\n
\n Fallback: Raw WHOIS Data shown due to unformatted registry
\n `}else{let e=n.abuse_contacts.length>0?n.abuse_contacts.map(e=>`${e}`).join(", "):'Not provided';t.innerHTML=`\n \n
\n Network Name\n ${n.network_name}\n
\n
\n IP Range\n ${n.network_range}\n
\n
\n Organization\n ${n.organization}\n
\n
\n Allocation Type\n ${n.type}\n
\n
\n Registry Handle\n ${n.handle} (${n.country})\n
\n
\n Registered\n ${n.registration_date}\n
\n
\n Last Updated\n ${n.updated_date}\n
\n
\n \n
Abuse Contacts\n
${e}
\n
Use these contacts to report malicious activity from this IP.
\n
\n `}}catch(e){t.innerHTML='\n \n
\n
WHOIS lookup failed. The registry may be rate limiting.
\n
\n '}finally{e.innerText="WHOIS",e.disabled=!1}}function closeWhoisModal(){const e=document.getElementById("whois-modal"),t=document.getElementById("whois-modal-backdrop"),n=document.getElementById("whois-modal-panel");t.classList.add("opacity-0"),n.classList.add("opacity-0","scale-95"),n.classList.remove("opacity-100","scale-100"),setTimeout(()=>{e.classList.add("hidden"),document.body.style.overflow=""},300)}fetchSmartIPs(),document.addEventListener("keydown",e=>{if("Escape"===e.key){const e=document.getElementById("rep-modal"),t=document.getElementById("whois-modal"),n=document.getElementById("dnsleak-modal");e&&!e.classList.contains("hidden")&&closeReputationModal(),t&&!t.classList.contains("hidden")&&closeWhoisModal(),n&&!n.classList.contains("hidden")&&closeDnsLeakModal()}});const repModal=document.getElementById("rep-modal");repModal&&repModal.addEventListener("click",e=>{"rep-modal"!==e.target.id&&"rep-modal-backdrop"!==e.target.id||closeReputationModal()});const whoisModal=document.getElementById("whois-modal");whoisModal&&whoisModal.addEventListener("click",e=>{"whois-modal"!==e.target.id&&"whois-modal-backdrop"!==e.target.id||closeWhoisModal()});const dnsLeakModal=document.getElementById("dnsleak-modal");function syntaxHighlight(e){return"string"!=typeof e&&(e=JSON.stringify(e,void 0,2)),(e=e.replace(/&/g,"&").replace(//g,">")).replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,function(e){let t="text-emerald-400 dark:text-emerald-300";return/^"/.test(e)?t=/:$/.test(e)?"text-indigo-500 dark:text-indigo-300 font-semibold":"text-emerald-500 dark:text-emerald-400":/true|false/.test(e)?t="text-orange-500 dark:text-orange-400":/null/.test(e)&&(t="text-zinc-400"),''+e+""})}dnsLeakModal&&dnsLeakModal.addEventListener("click",e=>{"dnsleak-modal"!==e.target.id&&"dnsleak-modal-backdrop"!==e.target.id||closeDnsLeakModal()});
\ No newline at end of file
+function initTheme(){"light"===localStorage.theme?document.documentElement.classList.remove("dark"):document.documentElement.classList.add("dark")}function toggleTheme(){const e=document.documentElement;e.classList.contains("dark")?(e.classList.remove("dark"),localStorage.theme="light",updateMapTheme("light")):(e.classList.add("dark"),localStorage.theme="dark",updateMapTheme("dark"))}let map;document.getElementById("year").innerText=(new Date).getFullYear(),initTheme();let pendingMapLocation=null,currentStyle="dark";const MAP_STYLES={dark:"https://tiles.openfreemap.org/styles/dark",light:"https://tiles.openfreemap.org/styles/positron",satellite:{version:8,sources:{esri:{type:"raster",tiles:["https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"],tileSize:256,attribution:'Tiles © Esri',maxzoom:19}},layers:[{id:"esri-tiles",type:"raster",source:"esri"}]}};function updateStyleSwitcherState(){const e=document.getElementById("map-style-switcher");if(!e)return;const t=["dark","light","satellite"];Array.from(e.children).forEach((e,n)=>{e.className=t[n]===currentStyle?"bg-zinc-200 dark:bg-zinc-700 text-zinc-900 dark:text-zinc-100 font-bold text-[10px] uppercase tracking-wide !px-3 !w-auto h-[29px] border-r border-zinc-300 dark:border-zinc-600 last:border-0 transition-colors":"bg-transparent text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800 hover:text-zinc-900 dark:hover:text-zinc-200 text-[10px] uppercase tracking-wide !px-3 !w-auto h-[29px] border-r border-zinc-200 dark:border-zinc-700/50 last:border-0 transition-colors"})}function addStyleSwitcher(){const e=document.createElement("div");e.id="map-style-switcher",e.className="maplibregl-ctrl maplibregl-ctrl-group flex flex-row overflow-hidden",e.style.boxShadow="0 0 0 2px rgba(0,0,0,0.1)";[{id:"dark",label:"Dark"},{id:"light",label:"Light"},{id:"satellite",label:"Sat"}].forEach(t=>{const n=document.createElement("button");n.type="button",n.textContent=t.label,n.title=t.label+" Mode",n.style.width="auto",n.style.padding="0 12px",n.style.display="flex",n.style.alignItems="center",n.style.justifyContent="center",n.addEventListener("click",()=>switchMapStyle(t.id)),e.appendChild(n)});const t={onAdd:()=>e,onRemove:()=>e.remove()};map.addControl(t,"top-left"),updateStyleSwitcherState()}function switchMapStyle(e){if(!map||e===currentStyle)return;currentStyle=e,updateStyleSwitcherState();const t=map.getCenter(),n=map.getZoom();map.setStyle(MAP_STYLES[e]),map.once("style.load",()=>{map.setCenter(t),map.setZoom(n),marker&&marker.addTo(map)})}class CustomAttributionControl{onAdd(e){this._map=e,this._container=document.createElement("div"),this._container.className="maplibregl-ctrl maplibregl-ctrl-group",this._container.style.display="flex",this._container.style.flexDirection="row-reverse",this._container.style.alignItems="center",this._container.style.maxWidth="29px",this._container.style.transition="max-width 0.3s ease",this._container.style.overflow="hidden",this._container.style.whiteSpace="nowrap";const t=document.createElement("button");return t.type="button",t.title="Map Attributions",t.className="custom-map-btn",t.style.width="29px",t.style.height="29px",t.style.flexShrink="0",t.style.display="flex",t.style.alignItems="center",t.style.justifyContent="center",t.innerHTML='',this._textDiv=document.createElement("div"),this._textDiv.className="text-[10px] text-zinc-600 dark:text-zinc-400 pl-3 pr-1",this._textDiv.style.opacity="0",this._textDiv.style.transition="opacity 0.2s ease",this._updateAttributions=()=>{const e=this._map.getStyle();if(!e||!e.sources)return;const t=new Set;Object.keys(e.sources).forEach(e=>{const n=this._map.getSource(e);n&&n.attribution&&t.add(n.attribution)});let n=Array.from(t).join(" | ");!n&&this._textDiv.innerHTML&&"Map data"!==this._textDiv.innerHTML||(n=n.replace(/{this._updateAttributions(),this._container.style.maxWidth="500px",this._textDiv.style.opacity="1"}),this._container.addEventListener("mouseleave",()=>{this._container.style.maxWidth="29px",this._textDiv.style.opacity="0"}),this._container.appendChild(t),this._container.appendChild(this._textDiv),this._container}onRemove(){this._map.off("styledata",this._updateAttributions),this._map.off("sourcedata",this._updateAttributions),this._container.parentNode.removeChild(this._container),this._map=void 0}}class CenterMapControl{onAdd(e){this._map=e,this._container=document.createElement("div"),this._container.className="maplibregl-ctrl maplibregl-ctrl-group";const t=document.createElement("button");return t.type="button",t.title="Center on IP",t.className="custom-map-btn",t.style.display="flex",t.style.alignItems="center",t.style.justifyContent="center",t.innerHTML='',t.onclick=()=>{window.currentCoords&&e.flyTo({center:window.currentCoords,zoom:13,speed:1.5})},this._container.appendChild(t),this._container}onRemove(){this._container.parentNode.removeChild(this._container),this._map=void 0}}function setOrQueueMapLocation(e,t,n){if(!map)return void(pendingMapLocation={lat:e,lon:t,city:n});map.resize(),window.currentCoords=[t,e],map.flyTo({center:[t,e],zoom:13,speed:1.5}),marker&&marker.remove();const a=document.createElement("div");a.className="custom-marker-wrapper",a.innerHTML='\n \n ';const r=new maplibregl.Popup({offset:15,focusAfterOpen:!1}).setHTML(`${n}`);marker=new maplibregl.Marker({element:a}).setLngLat([t,e]).setPopup(r).addTo(map),marker.togglePopup()}function setupLazyMap(){const e=document.getElementById("map");if(!e)return;const t=()=>{if(!map)if(window.maplibregl){try{initMap()}catch(e){console.error("Map initialization failed:",e)}pendingMapLocation&&(setOrQueueMapLocation(pendingMapLocation.lat,pendingMapLocation.lon,pendingMapLocation.city),pendingMapLocation=null)}else if(!document.getElementById("maplibre-script")){const e=document.createElement("link");e.rel="stylesheet",e.href="/maplibre/maplibre-gl.css",document.head.appendChild(e);const t=document.createElement("script");t.id="maplibre-script",t.src="/maplibre/maplibre-gl.js",t.onload=()=>{try{initMap()}catch(e){console.error("Map initialization failed:",e)}pendingMapLocation&&(setOrQueueMapLocation(pendingMapLocation.lat,pendingMapLocation.lon,pendingMapLocation.city),pendingMapLocation=null)},document.body.appendChild(t)}},n=()=>{if("IntersectionObserver"in window){const n=new IntersectionObserver(e=>{e[0].isIntersecting&&(t(),n.disconnect())},{rootMargin:"50px"});n.observe(e)}else t()};"complete"===document.readyState?setTimeout(n,500):window.addEventListener("load",()=>setTimeout(n,500))}function initMap(){const e=document.documentElement.classList.contains("dark");currentStyle=e?"dark":"light";const t="/maplibre/maplibre-gl-worker.js?v=20260926";window.maplibregl.setWorkerUrl?window.maplibregl.setWorkerUrl(t):window.maplibregl.config&&(window.maplibregl.config.WORKER_URL=t),map=new maplibregl.Map({container:"map",style:MAP_STYLES[currentStyle],center:[-.09,51.505],zoom:13,cooperativeGestures:!0,attributionControl:!1}),map.addControl(new maplibregl.NavigationControl({showCompass:!1}),"top-right"),map.addControl(new CenterMapControl,"top-right"),map.addControl(new CustomAttributionControl,"bottom-right"),map.on("load",()=>{map.resize(),addStyleSwitcher()})}function updateMapTheme(e){map&&"satellite"!==currentStyle&&switchMapStyle("dark"===e?"dark":"light")}let marker;window.currentScanIp="";let lastReputationResult=null;function showToast(e){const t=document.getElementById("toast");t.innerText=e,t.className="show",setTimeout(()=>{t.className=t.className.replace("show","")},3e3)}function copyWithFeedback(e,t,n){navigator.clipboard.writeText(e).then(()=>{showToast(`${n} Copied!`);const e=document.getElementById(t),a=e.innerHTML;e.innerHTML='',setTimeout(()=>{e.innerHTML=a},2e3)})}function createIpRow(e,t){const n="IPv6"===t,a=`copy-icon-${t}`;return`\n \n `}async function fetchSmartIPs(){const e=document.getElementById("ip-display-area");try{let t="/api/info";const n=window.location.search.substring(1).trim();let a=null;if(n){const e=new URLSearchParams(window.location.search);e.has("ip")?a=e.get("ip"):n.includes("=")||(a=n)}if(a){t=`/api/info?ip=${encodeURIComponent(a)}`;const e=document.getElementById("searchInput");e&&(e.value=a)}const[r,o]=await Promise.all([fetch("/api/config").catch(()=>null),fetch(t)]);let i={};if(r&&r.ok)try{i=await r.json()}catch(e){}setupLazyMap();const s=await o.json();if(s.error)throw new Error(s.error);const d=s.ip.includes(":"),l=d?"IPv6":"IPv4";if(e.innerHTML=createIpRow(s.ip,l),populateDetails(s),!a&&i.v4_url&&i.v6_url){const t=d?i.v4_url:i.v6_url,n=d?"IPv4":"IPv6";try{const a=await fetch(t);if(a.ok){const t=await a.json();if(t.ip&&t.ip!==s.ip){const a=t.ip.includes(":");(d&&!a||!d&&a)&&(e.innerHTML+=createIpRow(t.ip,n))}}}catch(e){console.log("Secondary protocol unavailable.")}}}catch(t){e.innerHTML='Error loading IP',console.error(t)}}function populateDetails(e){window.currentScanIp=e.ip,resetReputationUI(),document.getElementById("dataOrg").innerText=e.org||"N/A",document.getElementById("dataAsn").innerText=e.asn||"N/A",e.network&&"N/A"!==e.network?(document.getElementById("dataNetwork").innerText=e.network,document.getElementById("networkWrapper").classList.remove("hidden")):document.getElementById("networkWrapper").classList.add("hidden"),e.hostname&&"N/A"!==e.hostname?(document.getElementById("dataHostname").innerText=e.hostname,document.getElementById("hostnameWrapper").classList.remove("hidden")):document.getElementById("hostnameWrapper").classList.add("hidden"),document.getElementById("dataCity").innerText=e.city,document.getElementById("mainLocation").innerText=`${e.city}, ${e.country}`,document.getElementById("dataRegion").innerText=`${e.region}, ${e.country}`,e.zip&&"N/A"!==e.zip&&"-"!==e.zip?(document.getElementById("dataZip").innerText=e.zip,document.getElementById("zipWrapper").style.display="inline"):document.getElementById("zipWrapper").style.display="none",document.getElementById("dataTimezone").innerText=e.timezone,document.getElementById("dataCoords").innerText=e.coordinates,document.getElementById("jsonPreview").innerHTML=syntaxHighlight(e);const t=document.getElementById("dataProxyBadge");e.is_proxy?(t.innerText=e.proxy_type,t.className="px-2 py-0.5 rounded text-xs font-bold bg-red-500/20 text-red-600 dark:text-red-400 border border-red-500/20 shadow-[0_0_10px_rgba(239,68,68,0.2)]"):"Residential"===e.usage_type||"Mobile Data"===e.usage_type?(t.innerText=e.usage_type,t.className="px-2 py-0.5 rounded text-xs font-bold bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20"):(t.innerText=e.usage_type||"Clean",t.className="px-2 py-0.5 rounded text-xs font-bold bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20");const n=document.getElementById("providerRow"),a=document.getElementById("dataProvider");e.provider&&"N/A"!==e.provider&&"-"!==e.provider?(a.innerText=e.provider,n.style.display="flex"):n.style.display="none";const r=document.getElementById("threatRow"),o=document.getElementById("dataThreat");e.threat&&"None"!==e.threat&&"-"!==e.threat?(o.innerText=e.threat,r.style.display="flex",e.threat.includes("High")||e.threat.includes("SPAM")||e.threat.includes("SCANNER")?o.className="text-sm font-bold text-red-600 dark:text-red-400":o.className="text-sm font-medium text-orange-600 dark:text-orange-400"):r.style.display="none",e.latitude&&e.longitude&&setOrQueueMapLocation(parseFloat(e.latitude),parseFloat(e.longitude),e.city)}async function searchIp(){const e=document.getElementById("searchInput").value.trim();if(e)try{const t=await fetch(`/api/info?ip=${encodeURIComponent(e)}`),n=await t.json();populateDetails(n);const a=e.includes(":")?"IPv6":"IPv4";document.getElementById("ip-display-area").innerHTML=createIpRow(n.ip,a),document.getElementById("mainLocation").innerText=`${n.city}, ${n.country}`}catch(e){console.error(e)}}function copyText(e){navigator.clipboard.writeText(document.getElementById(e).innerText.replace("$ ","")).then(()=>showToast("Command Copied!"))}function resetReputationUI(){const e=document.getElementById("btn-scan");e&&(e.classList.remove("hidden"),e.disabled=!1,e.innerHTML=' SCAN THREATS'),document.getElementById("rep-badge").classList.add("hidden"),lastReputationResult=null}async function checkReputation(){const e=document.getElementById("btn-scan"),t=document.getElementById("rep-badge");e.disabled=!0,e.innerHTML=' SCANNING...';try{const n=await fetch(`/api/reputation?ip=${window.currentScanIp}`),a=await n.json();if(lastReputationResult=a,e.classList.add("hidden"),e.disabled=!1,t.classList.remove("hidden"),t.onclick=openReputationModal,t.style.cursor="pointer",a.is_clean)t.innerHTML='NO THREATS DETECTED ⓘ',t.className="px-2 py-1 rounded text-[11px] font-bold bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30 hover:bg-emerald-500/30 transition-colors whitespace-nowrap flex items-center gap-1";else{if(!a.detections)throw new Error("Invalid API response");t.innerText=`${a.detections.length} THREATS FOUND ⚠️`,t.className="px-2 py-0.5 rounded text-xs font-bold bg-red-500/20 text-red-600 dark:text-red-400 border border-red-500/30 shadow-[0_0_10px_rgba(239,68,68,0.3)] hover:bg-red-500/30 transition-colors"}}catch(t){console.error(t),e.innerHTML="ERROR - TRY AGAIN",e.disabled=!1}}function openReputationModal(){if(!lastReputationResult)return;const e=document.getElementById("rep-modal"),t=document.getElementById("modal-content"),n=document.getElementById("rep-modal-backdrop"),a=document.getElementById("rep-modal-panel");if(e.classList.remove("hidden"),document.body.style.overflow="hidden",lastReputationResult.is_clean){const e=`IP: ${lastReputationResult.ip} - Status: CLEAN (No threats detected in active feeds)`;t.innerHTML=`\n \n
\n
\n
No Threats Detected
\n
IP: ${lastReputationResult.ip}
\n
\n
\n \n
\n This IP address was not found in any of our active threat intelligence feeds (Blocklists, Spam lists, or CrowdSec).\n
\n ⚠️ Note:\n "No threats detected" does not guarantee safety. An IP can be malicious but not yet listed.\n
\n
\n \n `}else{let e="",n=[];lastReputationResult.detections.forEach(t=>{n.push(t.source),e+=`\n \n
\n ${t.source}\n ${t.reason||"Listed in blocklist"}\n
\n
Listed\n
\n `});const a=`⚠️ THREAT DETECTED ⚠️\\nIP: ${lastReputationResult.ip}\\nListed By: ${n.join(", ")}`;t.innerHTML=`\n \n
\n
\n
Threats Detected
\n
IP: ${lastReputationResult.ip}
\n
\n
\n The following security providers have flagged this IP:
\n \n ${e}\n
\n \n `}setTimeout(()=>{n.classList.remove("opacity-0"),a.classList.remove("opacity-0","scale-95"),a.classList.add("opacity-100","scale-100")},10)}function closeReputationModal(){const e=document.getElementById("rep-modal"),t=document.getElementById("rep-modal-backdrop"),n=document.getElementById("rep-modal-panel");t.classList.add("opacity-0"),n.classList.add("opacity-0","scale-95"),n.classList.remove("opacity-100","scale-100"),setTimeout(()=>{e.classList.add("hidden"),document.body.style.overflow=""},300)}async function checkWhois(){const e=document.getElementById("btn-whois"),t=document.getElementById("whois-content");e.disabled=!0,e.innerText="LOADING...";const n=document.getElementById("whois-modal"),a=document.getElementById("whois-modal-backdrop"),r=document.getElementById("whois-modal-panel");t.innerHTML='\n \n
\n
Looking up registry data...
\n
\n ',n.classList.remove("hidden"),document.body.style.overflow="hidden",setTimeout(()=>{a.classList.remove("opacity-0"),r.classList.remove("opacity-0","scale-95"),r.classList.add("opacity-100","scale-100")},10);try{const e=await fetch(`/api/whois?ip=${window.currentScanIp}`),n=await e.json();if(n.error)throw new Error(n.error);if(n.is_raw){const e=/([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)/gi;let a=n.raw_whois.replace(//g,">").replace(e,'$1');t.innerHTML=`\n \n${a}\n
\n Fallback: Raw WHOIS Data shown due to unformatted registry
\n `}else{let e=n.abuse_contacts.length>0?n.abuse_contacts.map(e=>`${e}`).join(", "):'Not provided';t.innerHTML=`\n \n
\n Network Name\n ${n.network_name}\n
\n
\n IP Range\n ${n.network_range}\n
\n
\n Organization\n ${n.organization}\n
\n
\n Allocation Type\n ${n.type}\n
\n
\n Registry Handle\n ${n.handle} (${n.country})\n
\n
\n Registered\n ${n.registration_date}\n
\n
\n Last Updated\n ${n.updated_date}\n
\n
\n \n
Abuse Contacts\n
${e}
\n
Use these contacts to report malicious activity from this IP.
\n
\n `}}catch(e){t.innerHTML='\n \n
\n
WHOIS lookup failed. The registry may be rate limiting.
\n
\n '}finally{e.innerText="WHOIS",e.disabled=!1}}function closeWhoisModal(){const e=document.getElementById("whois-modal"),t=document.getElementById("whois-modal-backdrop"),n=document.getElementById("whois-modal-panel");t.classList.add("opacity-0"),n.classList.add("opacity-0","scale-95"),n.classList.remove("opacity-100","scale-100"),setTimeout(()=>{e.classList.add("hidden"),document.body.style.overflow=""},300)}fetchSmartIPs(),document.addEventListener("keydown",e=>{if("Escape"===e.key){const e=document.getElementById("rep-modal"),t=document.getElementById("whois-modal"),n=document.getElementById("dnsleak-modal");e&&!e.classList.contains("hidden")&&closeReputationModal(),t&&!t.classList.contains("hidden")&&closeWhoisModal(),n&&!n.classList.contains("hidden")&&closeDnsLeakModal()}});const repModal=document.getElementById("rep-modal");repModal&&repModal.addEventListener("click",e=>{"rep-modal"!==e.target.id&&"rep-modal-backdrop"!==e.target.id||closeReputationModal()});const whoisModal=document.getElementById("whois-modal");whoisModal&&whoisModal.addEventListener("click",e=>{"whois-modal"!==e.target.id&&"whois-modal-backdrop"!==e.target.id||closeWhoisModal()});const dnsLeakModal=document.getElementById("dnsleak-modal");function syntaxHighlight(e){return"string"!=typeof e&&(e=JSON.stringify(e,void 0,2)),(e=e.replace(/&/g,"&").replace(//g,">")).replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,function(e){let t="text-emerald-400 dark:text-emerald-300";return/^"/.test(e)?t=/:$/.test(e)?"text-indigo-500 dark:text-indigo-300 font-semibold":"text-emerald-500 dark:text-emerald-400":/true|false/.test(e)?t="text-orange-500 dark:text-orange-400":/null/.test(e)&&(t="text-zinc-400"),''+e+""})}dnsLeakModal&&dnsLeakModal.addEventListener("click",e=>{"dnsleak-modal"!==e.target.id&&"dnsleak-modal-backdrop"!==e.target.id||closeDnsLeakModal()});
\ No newline at end of file
diff --git a/views/app.js b/views/app.js
index c362619..dcea76e 100644
--- a/views/app.js
+++ b/views/app.js
@@ -277,9 +277,36 @@ function setupLazyMap() {
const triggerInit = () => {
if (map) return;
- // Ensure maplibregl is available (it's loaded via defer script in head)
+ // Dynamically load MapLibre JS and CSS only when the map is scrolled into view
if (!window.maplibregl) {
- setTimeout(triggerInit, 100);
+ if (!document.getElementById("maplibre-script")) {
+ // Load CSS
+ const link = document.createElement("link");
+ link.rel = "stylesheet";
+ link.href = "/maplibre/maplibre-gl.css";
+ document.head.appendChild(link);
+
+ // Load JS
+ const script = document.createElement("script");
+ script.id = "maplibre-script";
+ script.src = "/maplibre/maplibre-gl.js";
+ script.onload = () => {
+ try {
+ initMap();
+ } catch (e) {
+ console.error("Map initialization failed:", e);
+ }
+ if (pendingMapLocation) {
+ setOrQueueMapLocation(
+ pendingMapLocation.lat,
+ pendingMapLocation.lon,
+ pendingMapLocation.city
+ );
+ pendingMapLocation = null;
+ }
+ };
+ document.body.appendChild(script);
+ }
return;
}
@@ -305,7 +332,7 @@ function setupLazyMap() {
triggerInit();
observer.disconnect();
}
- }, { rootMargin: "300px" });
+ }, { rootMargin: "50px" });
observer.observe(mapContainer);
} else {
triggerInit();
diff --git a/views/index.html b/views/index.html
index 96a8bd9..a205e16 100644
--- a/views/index.html
+++ b/views/index.html
@@ -17,9 +17,7 @@
-
-
-
+
@@ -49,12 +47,7 @@
-
-
-