(e)
| 1 | let socket,reconnectAttempts=0;const RECONNECT_WARNING_THRESHOLD=5,RECONNECT_DELAY_MAX=15e3;let currentTab="dashboard",autoRefreshIntervals={},preloadedTabs=new Set,pendingFileHighlight=null,manualModeActive=!1,manualDataPrimed=!1,imagesLoaded=!1;const PWN_STATUS_POLL_INTERVAL=15e3,PWN_STATUS_FAST_INTERVAL=4e3,PWN_LOG_POLL_INTERVAL=2500;let pwnStatus={state:"not_installed",message:"Waiting for status...",phase:"idle",installed:!1,installing:!1,mode:"ragnar",target_mode:"ragnar",last_switch:"",service_active:!1,service_enabled:!1,timestamp:null,log_file:null},lastPwnState=null,_pwnSwapRequestedThisSession=!1,currentPwnStatusInterval=15e3,pwnLogCursor=0,pwnLogStreamTimer=null,pwnLogStreaming=!1,pwnLogStopTimeout=null,pwnLogActiveFile=null,pwnLogFetchInFlight=!1,headlessMode=!1;const RELEASE_GATE_DEFAULT_MESSAGE="A controlled release is rolling out. Updating manually may cause instability.";let releaseGateState={enabled:!1,message:RELEASE_GATE_DEFAULT_MESSAGE},releaseGateResolver=null,releaseGatePendingPromise=null,threatIntelStatusFilter="open",credentialsCache=null,credServiceFilter="all",credIPSearch="",credSortCol="ip",credSortAsc=!0;const CRED_SERVICE_PORTS={ssh:22,smb:445,ftp:21,telnet:23,rdp:3389,sql:3306},CRED_SERVICE_COLORS={ssh:"bg-blue-900 text-blue-300",smb:"bg-purple-900 text-purple-300",ftp:"bg-yellow-900 text-yellow-300",telnet:"bg-orange-900 text-orange-300",rdp:"bg-pink-900 text-pink-300",sql:"bg-green-900 text-green-300"};async function loadCredentials(e=!1){if(e||!credentialsCache)try{const e=await fetchAPI("/api/credentials"),t=[];Object.entries(e).forEach(([e,n])=>{(n||[]).forEach(n=>t.push({...n,service:e}))}),credentialsCache=t,displayCredentials(t)}catch(e){const t=document.getElementById("cred-table-body");t&&(t.innerHTML=`<tr><td colspan="6" class="py-8 text-center text-red-400">Error loading credentials: ${escapeHtml(e.message)}</td></tr>`)}else displayCredentials(credentialsCache)}function onCredSearch(e){credIPSearch=e.trim().toLowerCase(),displayCredentials(credentialsCache)}function filterCredsByService(e){credServiceFilter=e,document.querySelectorAll(".cred-svc-btn").forEach(t=>{const n=t.getAttribute("data-svc")===e;t.classList.toggle("bg-Ragnar-600",n),t.classList.toggle("bg-slate-700",!n),t.classList.toggle("hover:bg-slate-600",!n)}),displayCredentials(credentialsCache)}function sortCredsBy(e){credSortCol===e?credSortAsc=!credSortAsc:(credSortCol=e,credSortAsc=!0),["ip","service","username"].forEach(e=>{const t=document.getElementById(`cred-sort-${e}`);t&&(t.textContent=e===credSortCol?credSortAsc?"↑":"↓":"")}),displayCredentials(credentialsCache)}function displayCredentials(e){if(!e)return;const t=document.getElementById("cred-table-body"),n=document.getElementById("cred-stats"),a=document.getElementById("cred-table-count");if(!t)return;const s={};e.forEach(e=>{s[e.service]=(s[e.service]||0)+1});let o=`<div class="bg-slate-800 rounded-lg p-3 text-center"><div class="text-2xl font-bold">${e.length}</div><div class="text-xs text-gray-400 mt-1">Total</div></div>`;Object.entries(CRED_SERVICE_PORTS).forEach(([e])=>{const t=s[e]||0;o+=`<div class="bg-slate-800 rounded-lg p-3 text-center cursor-pointer hover:bg-slate-700 transition-colors" onclick="filterCredsByService('${e}')">\n <div class="text-2xl font-bold ${t>0?"text-green-400":"text-gray-500"}">${t}</div>\n <div class="text-xs text-gray-400 mt-1 uppercase">${e}</div>\n </div>`}),n&&(n.innerHTML=o);let r=e;if("all"!==credServiceFilter&&(r=r.filter(e=>e.service===credServiceFilter)),credIPSearch&&(r=r.filter(e=>(e.ip||"").toLowerCase().includes(credIPSearch))),r=[...r].sort((e,t)=>{const n=(e[credSortCol]||"").toLowerCase(),a=(t[credSortCol]||"").toLowerCase();return credSortAsc?n.localeCompare(a):a.localeCompare(n)}),0===r.length)return t.innerHTML='<tr><td colspan="6" class="py-8 text-center text-gray-400">No credentials match current filters</td></tr>',void(a&&(a.textContent=""));t.innerHTML=r.map(e=>{const t=CRED_SERVICE_PORTS[e.service]||"—",n=CRED_SERVICE_COLORS[e.service]||"bg-slate-700 text-gray-300",a=e.password?`<span class="font-mono">${escapeHtml(e.password)}</span>`:'<span class="text-gray-500 italic">none</span>',s=e.password?escapeHtml(e.password).replace(/'/g,"'"):"";return`<tr class="border-b border-slate-800 hover:bg-slate-800 transition-colors">\n <td class="py-3 px-4 font-mono text-sm whitespace-nowrap">${escapeHtml(e.ip||"—")}</td>\n <td class="py-3 px-4 whitespace-nowrap"><span class="px-2 py-0.5 rounded text-xs font-semibold uppercase ${n}">${escapeHtml(e.service)}</span></td>\n <td class="py-3 px-4 text-gray-400 whitespace-nowrap">${t}</td>\n <td class="py-3 px-4 font-mono whitespace-nowrap">${escapeHtml(e.username||"—")}</td>\n <td class="py-3 px-4 whitespace-nowrap">${a}</td>\n <td class="py-3 px-4 whitespace-nowrap">\n ${e.password?`<button onclick="copyCredToClipboard('${s}')" class="text-xs text-blue-400 hover:text-blue-300 px-2 py-1 rounded hover:bg-slate-700 transition-colors" title="Copy password">Copy</button>`:""}\n </td>\n </tr>`}).join(""),a&&(a.textContent=`Showing ${r.length} of ${e.length} credential${1!==e.length?"s":""}`)}function copyCredToClipboard(e){navigator.clipboard.writeText(e).then(()=>{addConsoleMessage("Password copied to clipboard","success")}).catch(()=>{addConsoleMessage("Copy failed — check browser permissions","warning")})}function exportCredentialsCSV(){if(!credentialsCache||0===credentialsCache.length)return void addConsoleMessage("No credentials to export","warning");const e=[["IP","Service","Port","Username","Password"]];credentialsCache.forEach(t=>{e.push([t.ip||"",t.service||"",CRED_SERVICE_PORTS[t.service]||"",t.username||"",t.password||""])});const t=e.map(e=>e.map(e=>`"${String(e).replace(/"/g,'""')}"`).join(",")).join("\n"),n=new Blob([t],{type:"text/csv"}),a=URL.createObjectURL(n),s=document.createElement("a");s.href=a,s.download=`ragnar_credentials_${(new Date).toISOString().slice(0,10)}.csv`,s.click(),URL.revokeObjectURL(a)}const configMetadata={manual_mode:{label:"Pentest Mode",description:"Hold Ragnar in hands-on pentest control. Disable this to let the orchestrator continuously discover devices, run actions, and launch vulnerability scans automatically."},debug_mode:{label:"Debug Mode",description:"Enable verbose debug logging for deeper troubleshooting output."},scan_vuln_running:{label:"Vulnerability Scanning",description:"Enable automatic vulnerability scans on discovered hosts based on the configured interval."},scan_vuln_no_ports:{label:"Scan Hosts Without Ports",description:"When enabled, vulnerability scans will scan the top 50 common ports on hosts where no ports were discovered. When disabled, only hosts with discovered ports will be scanned."},enable_attacks:{label:"Enable Automatic Attacks",description:"Allow Ragnar to perform automated attacks (SSH, FTP, SMB, SQL, etc.) on discovered targets. Disable to only scan without attacking."},retry_success_actions:{label:"Retry Successful Actions",description:"Re-run actions that previously succeeded after the success retry delay to keep intelligence fresh."},retry_failed_actions:{label:"Retry Failed Actions",description:"Retry actions that failed after waiting the failed retry delay."},blacklistcheck:{label:"Honor Scan Blacklists",description:"Skip hosts or MAC addresses that appear in the scan blacklist lists when running automated actions."},displaying_csv:{label:"Display Scan CSV",description:"Push the most recent scan CSV results to the display after each network sweep."},log_debug:{label:"Log Debug Messages",description:"Include debug-level entries in Ragnar logs."},log_info:{label:"Log Info Messages",description:"Include informational entries in Ragnar logs."},log_warning:{label:"Log Warning Messages",description:"Include warning-level entries in Ragnar logs."},log_error:{label:"Log Error Messages",description:"Include error-level entries in Ragnar logs."},log_critical:{label:"Log Critical Messages",description:"Include critical-level entries in Ragnar logs."},startup_delay:{label:"Startup Delay (s)",description:"Seconds to wait after boot before the orchestrator begins automated activity."},web_delay:{label:"Web Update Delay (s)",description:"Seconds between refreshes of the web dashboards and API responses."},screen_delay:{label:"Screen Update Delay (s)",description:"Seconds between display refreshes."},comment_delaymin:{label:"Comment Delay Min (s)",description:"Minimum number of seconds between on-screen comment rotations."},comment_delaymax:{label:"Comment Delay Max (s)",description:"Maximum number of seconds between on-screen comment rotations."},livestatus_delay:{label:"Live Status Delay (s)",description:"Seconds between updates to the live status CSV that feeds dashboards."},image_display_delaymin:{label:"Image Display Min (s)",description:"Minimum time an image remains on the display."},image_display_delaymax:{label:"Image Display Max (s)",description:"Maximum time an image remains on the display."},scan_interval:{label:"Scan Interval (s)",description:"Seconds between full network discovery scans."},scan_vuln_interval:{label:"Vulnerability Scan Interval (s)",description:"Seconds between automated vulnerability scan cycles when enabled."},failed_retry_delay:{label:"Failed Retry Delay (s)",description:"Seconds to wait before retrying an action that previously failed."},success_retry_delay:{label:"Success Retry Delay (s)",description:"Seconds to wait before repeating an action that previously succeeded."},ref_width:{label:"Reference Width",description:"Reference pixel width used to scale drawings for the display."},ref_height:{label:"Reference Height",description:"Reference pixel height used to scale drawings for the display."},screen_reversed:{label:"Flip Display Output",description:"Rotate the display output. Use 180° when mounted upside down, or 90°/270° for portrait orientation."},epd_type:{label:"EPD Type",description:"Model identifier for the connected display."},gc9a01_mascot_color:{label:"GC9A01 Mascot Tint Color",description:'Tint color applied to the animated mascot on the GC9A01 1.28" round TFT display. Only visible when GC9A01 is selected.'},lcd1602_i2c_address:{label:"LCD1602 I2C Address",description:"I2C address of the PCF8574 backpack on the LCD1602 16×2 character display. Common values: 0x27 (most common) or 0x3F. Auto-detected if unreachable."},display_brightness:{label:"Display Brightness",description:"Brightness level for non-e-ink displays (SSD1306, GC9A01, MAX7219). Range 0–15. Default: 8."},spi_clock_mhz:{label:"SPI Clock Speed (MHz)",description:"SPI bus clock speed for the e-paper display in MHz. Lower values improve signal integrity when a PiSugar battery is stacked on the GPIO header. Default: 2 MHz. Try 1 MHz if you still see corrupted pixels."},max7219_spi_port:{label:"MAX7219 SPI Port",description:"SPI bus port for MAX7219 LED matrix (default: 0)"},max7219_spi_device:{label:"MAX7219 SPI Device",description:"SPI chip-enable pin for MAX7219 (0 = CE0, 1 = CE1, default: 0)"},max7219_block_orientation:{label:"MAX7219 Block Orientation",description:"Rotation of each 8×8 block (degrees). Try 0, 90, -90, or 180 if display looks wrong. Default: 0"},portlist:{label:"Additional Ports",description:"Comma separated list of extra ports to check on every host in addition to the sequential range."},mac_scan_blacklist:{label:"MAC Scan Blacklist",description:"Comma separated MAC addresses Ragnar should ignore during scans and automated actions."},ip_scan_blacklist:{label:"IP Scan Blacklist",description:"Comma separated IP addresses Ragnar should ignore during scans and automated actions."},steal_file_names:{label:"Target File Names",description:"Comma separated file name fragments that trigger file collection when encountered."},steal_file_extensions:{label:"Target File Extensions",description:"Comma separated file extensions that Ragnar should collect when found."},nmap_scan_aggressivity:{label:"Nmap Aggressiveness",description:"Timing template flag passed to nmap (for example -T2). Adjust to trade accuracy for speed."},portstart:{label:"Port Range Start",description:"First port in the sequential range scanned on every host."},portend:{label:"Port Range End",description:"Last port in the sequential range scanned on every host."},timewait_smb:{label:"SMB Retry Wait (s)",description:"Seconds to wait before retrying SMB actions against a host."},timewait_ssh:{label:"SSH Retry Wait (s)",description:"Seconds to wait before retrying SSH actions against a host."},timewait_telnet:{label:"Telnet Retry Wait (s)",description:"Seconds to wait before retrying Telnet actions against a host."},timewait_ftp:{label:"FTP Retry Wait (s)",description:"Seconds to wait before retrying FTP actions against a host."},timewait_sql:{label:"SQL Retry Wait (s)",description:"Seconds to wait before retrying SQL actions against a host."},timewait_rdp:{label:"RDP Retry Wait (s)",description:"Seconds to wait before retrying RDP actions against a host."},wifi_known_networks:{label:"Known Wi-Fi Networks",description:"Comma separated list of SSIDs Ragnar should automatically join when detected."},wifi_ap_ssid:{label:"AP SSID",description:"Network name broadcast when Ragnar creates its own access point."},wifi_ap_password:{label:"AP Password",description:"Password clients must use to join Ragnar's access point."},wifi_connection_timeout:{label:"Wi-Fi Connection Timeout (s)",description:"Seconds to wait for each Wi-Fi connection attempt before considering it failed."},wifi_max_attempts:{label:"Wi-Fi Max Attempts",description:"Number of Wi-Fi connection retries before giving up or falling back to AP mode."},wifi_scan_interval:{label:"Wi-Fi Scan Interval (s)",description:"Seconds between wireless network scans performed by the Wi-Fi manager."},wifi_monitor_enabled:{label:"Wi-Fi Monitor",description:"Keep the Wi-Fi manager running so connectivity issues are detected quickly."},wifi_auto_ap_fallback:{label:"Auto AP Fallback",description:"Automatically enable Ragnar's access point if normal Wi-Fi connectivity cannot be restored."},wifi_ap_timeout:{label:"AP Timeout (s)",description:"Maximum duration before an active Ragnar access point session shuts down automatically."},wifi_ap_idle_timeout:{label:"AP Idle Timeout (s)",description:"Seconds of inactivity allowed before shutting down the Ragnar access point."},wifi_reconnect_interval:{label:"Wi-Fi Reconnect Interval (s)",description:"Seconds between Wi-Fi reconnect attempts when the device is offline."},wifi_ap_cycle_enabled:{label:"AP Smart Cycling",description:"Periodically cycle the access point when active to limit exposure."},wifi_initial_connection_timeout:{label:"Initial Wi-Fi Timeout (s)",description:"Timeout for the very first Wi-Fi connection attempt during boot."},network_device_retention_days:{label:"Device Retention (days)",description:"Number of days to keep inactive devices in the network database before pruning them."},network_resolution_timeout:{label:"Resolution Timeout (s)",description:"Seconds to wait before re-resolving details for the same device."},network_confirmation_scans:{label:"Confirmation Scans",description:"Number of extra scans required to confirm a detected network change."},network_change_grace:{label:"Change Grace Period (s)",description:"Grace period after detecting a network change before automation responds."},network_intelligence_enabled:{label:"Network Intelligence",description:"Enable the network intelligence engine that tracks devices and their state changes."},network_auto_resolution:{label:"Automatic Resolution",description:"Automatically resolve and enrich newly discovered or changed devices."},network_max_failed_pings:{label:"Max Failed Pings Before Offline",description:"Number of consecutive failed ping/ARP checks before marking a host as offline (red dot). With ARP scans every 60s, setting this to 5 means ~5 minutes, 15 means ~15 minutes, 30 means ~30 minutes before offline status."},ai_enabled:{label:"Enable AI Insights",description:"Enable AI-powered network analysis and vulnerability insights using OpenAI GPT."},openai_api_token:{label:"OpenAI API Token",description:"Your OpenAI API key for AI-powered features. Keep this confidential."},wardriving_enabled:{label:"Enable Wardriving",description:"Enable the wardriving tab for WiFi network discovery with GPS mapping. Requires a USB GPS module for location data. Note: Automatic AP mode is disabled while wardriving is enabled — AP mode (hostapd) would take over wlan0 and block WiFi scanning."},wardriving_display:{label:"Wardriving on Display",description:"Replace the normal Ragnar display with a wardriving dashboard while a session is running. Works on all displays: e-paper, GC9A01 TFT, SSD1306 OLED, LCD1602, and MAX7219 LED matrix."},wardriving_scan_interval:{label:"Scan Interval (s)",description:"Seconds between WiFi scans during wardriving. Lower values capture more networks but use more CPU. Default: 2."},wardriving_gps_port:{label:"GPS Serial Port",description:"Serial port for USB GPS module. Set to 'auto' for automatic detection, or specify a port like /dev/ttyACM0."},wardriving_gps_baudrate:{label:"GPS Baud Rate",description:"Serial baud rate for the GPS module. Most USB GPS modules use 9600. Some high-speed modules use 38400 or 115200."},wardriving_auto_export:{label:"Auto Export on Stop",description:"Automatically export a WiGLE CSV file when a wardriving session is stopped."}};function getConfigLabel(e){return configMetadata[e]&&configMetadata[e].label?configMetadata[e].label:e.replace(/_/g," ").replace(/\b\w/g,e=>e.toUpperCase())}function epdTypeToSizeKey(e){return e&&"auto"!==e?e.startsWith("epd2in13")?"2in13":e.startsWith("epd2in7")?"2in7":e.startsWith("epd2in9")?"2in9":e.startsWith("epd3in7")?"3in7":e.startsWith("epd4in26")?"4in26":"gc9a01"===e?"1in28_tft":"ssd1306"===e?"0in96_oled":"lcd1602"===e?"lcd1602":e:"auto"}const displaySelectOptions={epd_type:[{value:"auto",label:"Auto-detect"},{value:"2in13",label:'2.13" e-Paper (122x250)'},{value:"2in7",label:'2.7" e-Paper (176x264)'},{value:"2in9",label:'2.9" e-Paper (128x296)'},{value:"3in7",label:'3.7" e-Paper (280x480)'},{value:"4in26",label:'4.26" e-Paper (800x480)'},{value:"1in28_tft",label:'1.28" GC9A01 Round TFT (240x240)'},{value:"0in96_oled",label:'0.96" SSD1306 OLED (128x64)'},{value:"lcd1602",label:"16×2 LCD1602 Character LCD (I2C)"},{value:"max7219_8panel",label:"MAX7219 8-panel LED Matrix (64×8)"},{value:"max7219_4panel",label:"MAX7219 4-panel LED Matrix (32×8)"}],screen_reversed:[{value:"0",label:"Normal (0°)"},{value:"90",label:"Rotate 90°"},{value:"180",label:"Rotate 180°"},{value:"270",label:"Rotate 270°"}]};function getConfigDescription(e){return configMetadata[e]&&configMetadata[e].description?configMetadata[e].description:"No additional information available for this setting."}function escapeHtml(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML}function escapeAttr(e){return null==e?"":String(e).replace(/&/g,"&").replace(/'/g,"'").replace(/"/g,""").replace(/</g,"<").replace(/>/g,">").replace(/`/g,"`")}function isValidIPv4(e){if(!e)return!1;return/^(25[0-5]|2[0-4]\d|1?\d{1,2})(\.(25[0-5]|2[0-4]\d|1?\d{1,2})){3}$/.test(e.trim())}function initializeSocket(){socket=io({reconnection:!0,reconnectionDelay:1e3,reconnectionDelayMax:15e3}),socket.on("connect",function(){console.log("Connected to Ragnar server"),updateConnectionStatus(!0),reconnectAttempts=0,addConsoleMessage("Connected to Ragnar server","success"),socket.emit("request_status"),socket.emit("request_logs"),refreshPwnagotchiStatus({silent:!0})}),socket.on("connected",function(e){if(e&&void 0!==e.auth_configured){const t=e.auth_configured,n=document.getElementById("logout-btn");n&&n.classList.toggle("hidden",!t);const a=document.getElementById("mobile-logout-btn");a&&a.classList.toggle("hidden",!t)}}),socket.on("disconnect",function(){console.log("Disconnected from Ragnar server"),updateConnectionStatus(!1),addConsoleMessage("Disconnected from server","error"),setTimeout(()=>{socket&&socket.disconnected&&(console.log("Attempting manual socket reconnection"),socket.connect())},2e3)}),socket.on("status_update",function(e){updateDashboardStatus(e)}),socket.on("log_update",function(e){updateConsole(e)}),socket.on("pwnagotchi_status",function(e){const t=pwnStatus.state;updatePwnagotchiUI(e),e&&e.state&&e.state!==t&&addConsoleMessage(`Pwnagotchi status changed: ${formatPwnStateLabel(e.state)}`,"info")}),socket.on("network_update",function(e){"network"===currentTab&&loadStableNetworkData()}),socket.on("credentials_update",function(e){"discovered"===currentTab&&displayCredentialsTable(e)}),socket.on("loot_update",function(e){"discovered"===currentTab&&displayLootTable(e)}),socket.on("config_updated",function(e){addConsoleMessage("Configuration updated successfully","info"),"config"===currentTab&&displayConfigForm(e),updateAttackWarningBanner(Boolean(e&&e.enable_attacks))}),socket.on("scan_started",function(e){handleScanStarted(e)}),socket.on("scan_progress",function(e){handleScanProgress(e)}),socket.on("scan_host_update",function(e){handleScanHostUpdate(e)}),socket.on("scan_completed",function(e){handleScanCompleted(e)}),socket.on("scan_error",function(e){handleScanError(e)}),socket.on("deep_scan_update",function(e){handleDeepScanUpdate(e)}),socket.on("lynis_update",function(e){handleLynisUpdate(e)}),socket.on("manual_attack_update",function(e){handleManualAttackUpdate(e)}),socket.on("connect_error",function(e){reconnectAttempts++,console.error("Connection error:",e),5===reconnectAttempts?addConsoleMessage("Reconnecting to server… still attempting to reach backend","warning"):reconnectAttempts>5&&reconnectAttempts%5==0&&addConsoleMessage(`Still reconnecting (attempt ${reconnectAttempts}). Will keep trying until successful.`,"warning")})}function updateConnectionStatus(e){const t=document.getElementById("connection-status");t&&(t.innerHTML=e?'\n <span class="w-2 h-2 bg-green-500 rounded-full pulse-glow"></span>\n <span class="text-xs text-gray-400">Connected</span>\n ':'\n <span class="w-2 h-2 bg-red-500 rounded-full"></span>\n <span class="text-xs text-gray-400">Disconnected</span>\n ')}function setupEventListeners(){document.querySelectorAll("[data-tab]").forEach(e=>{e.addEventListener("click",function(){showTab(this.getAttribute("data-tab"))})}),document.addEventListener("click",function(e){e.target.classList.contains("refresh-btn")&&refreshCurrentTab()});const e=document.getElementById("clear-console");e&&e.addEventListener("click",clearConsole);const t=document.getElementById("custom-deep-scan-btn");t&&t.addEventListener("click",handleCustomDeepScanRequest);const n=document.getElementById("custom-deep-scan-ip");n&&n.addEventListener("keydown",function(e){"Enter"===e.key&&(e.preventDefault(),handleCustomDeepScanRequest())});const a=document.getElementById("manual-port-dropdown");a&&a.addEventListener("change",()=>{updateManualActions()});const s=document.getElementById("manual-action-dropdown");s&&s.addEventListener("change",()=>{window.manualActionPreference=s.value});const o=document.getElementById("automation-toggle-btn");o&&o.addEventListener("click",handleAutomationToggle)}function initializeTabs(){showTab("dashboard")}document.addEventListener("DOMContentLoaded",function(){initializeSocket(),initializeTabs(),initializeMobileMenu(),loadInitialData(),setupAutoRefresh(),setupEpaperAutoRefresh(),setupEventListeners(),initializeThreatIntelFilters(),initializePwnUI(),initializePwnagotchiVisibility(),handleHeadlessMode(),applyRusenseTabVisibility(),syncRusenseTabFromServer()});const RUSENSE_SUBTABS=["dashboard","observatory","sensing","nodes","training","settings","about"];let _rusenseLoader=null,_rusenseLoading=null,_rusenseCurrent="dashboard";function _setRusenseActive(e){RUSENSE_SUBTABS.forEach(t=>_setSubtabActive(document.getElementById("rusense-subtab-"+t),t===e))}function loadRusenseLoader(){return _rusenseLoader?Promise.resolve(_rusenseLoader):(_rusenseLoading||(_rusenseLoading=import("/web/rusense/app/loader.js?v=20260630-recstate").then(e=>(_rusenseLoader=e,e)).catch(e=>{throw _rusenseLoading=null,e})),_rusenseLoading)}let _observatoryFsInit=!1;function initObservatoryFullscreenBridge(){_observatoryFsInit||(_observatoryFsInit=!0,window.addEventListener("message",e=>{if(!e.data||"observatory-fullscreen"!==e.data.type)return;const t=document.getElementById("observatory-frame");if(!t)return;if(t.classList.toggle("observatory-fullscreen")){const e=t.requestFullscreen||t.webkitRequestFullscreen;e&&Promise.resolve(e.call(t)).catch(()=>{})}else{const e=document.exitFullscreen||document.webkitExitFullscreen;e&&(document.fullscreenElement||document.webkitFullscreenElement)&&Promise.resolve(e.call(document)).catch(()=>{})}}),document.addEventListener("fullscreenchange",()=>{const e=document.getElementById("observatory-frame");e&&!document.fullscreenElement&&e.classList.remove("observatory-fullscreen")}))}function showRusenseSubtab(e){RUSENSE_SUBTABS.includes(e)||(e="dashboard"),_rusenseCurrent=e,_setRusenseActive(e);const t=document.getElementById("rusense-host"),n=document.getElementById("observatory-frame");if("observatory"!==e)n&&(n.classList.add("hidden"),n.getAttribute("src")&&n.removeAttribute("src")),t&&t.classList.remove("hidden"),loadRusenseLoader().then(n=>n.init(t,e)).catch(e=>{console.error("[rusense] loader failed",e);const t=document.getElementById("rusense-offline-hint");t&&(t.textContent="RuSense UI failed to load: "+(e&&e.message),t.classList.remove("hidden"))});else if(t&&t.classList.add("hidden"),n&&(n.classList.remove("hidden"),n.getAttribute("src")||(n.src="/web/observatory.html?v=20260629-obsserver")),_rusenseLoader)try{_rusenseLoader.suspend()}catch(e){}}function initRusense(){showRusenseSubtab(_rusenseCurrent);const e=document.getElementById("rusense-offline-hint");e&&fetch("/api/v1/status",{cache:"no-store"}).then(t=>e.classList.toggle("hidden",t.ok)).catch(()=>e.classList.remove("hidden"))}initObservatoryFullscreenBridge();let _sensingLogTimer=null;function refreshSensingInstallCard(){const e=document.getElementById("sensing-install-card");e&&fetch("/api/sensing/status",{cache:"no-store"}).then(e=>e.json()).then(t=>{const n=document.getElementById("sensing-status-badge"),a=document.getElementById("sensing-install-btn"),s=document.getElementById("sensing-rebuild-btn"),o=document.getElementById("sensing-uninstall-btn"),r=document.getElementById("sensing-arch-note");if(e.classList.remove("hidden"),r&&(r.textContent=t.has_prebuilt?"Prebuilt binary bundled for this device ("+t.arch+") — installs instantly.":"No prebuilt binary for "+t.arch+" — install will fetch Rust and compile from source (slow)."),n){let e="not installed",a="bg-gray-700 text-gray-300";t.installing?(e="installing…",a="bg-amber-700 text-amber-100"):t.installed&&t.active?(e="running",a="bg-green-700 text-green-100"):t.installed&&(e="stopped",a="bg-red-700 text-red-100"),n.textContent=e,n.className="text-xs px-2 py-0.5 rounded "+a}const i=!!t.installing;a&&(a.disabled=i,a.textContent=t.installed?"Reinstall":"Install"),s&&(s.disabled=i),o&&(o.classList.toggle("hidden",!t.installed),o.disabled=i),t.installing&&startSensingLogPoll()}).catch(()=>{})}function installSensingBackend(e){const t=document.getElementById("sensing-install-log");t&&(t.classList.remove("hidden"),t.textContent="Starting…"),fetch("/api/sensing/install",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({rebuild:!!e})}).then(()=>startSensingLogPoll()).catch(()=>{})}function uninstallSensingBackend(){confirm("Stop and remove the bundled sensing backend?")&&fetch("/api/sensing/uninstall",{method:"POST"}).then(()=>startSensingLogPoll()).catch(()=>{})}function startSensingLogPoll(){if(_sensingLogTimer)return;const e=document.getElementById("sensing-install-log");e&&e.classList.remove("hidden");const t=()=>{fetch("/api/sensing/install-log",{cache:"no-store"}).then(e=>e.json()).then(t=>{if(e&&t.log&&(e.textContent=t.log,e.scrollTop=e.scrollHeight),!t.installing&&(clearInterval(_sensingLogTimer),_sensingLogTimer=null,refreshSensingInstallCard(),t.installed&&t.active)){const e=document.getElementById("rusense-offline-hint");e&&e.classList.add("hidden"),showRusenseSubtab(_rusenseCurrent)}}).catch(()=>{})};t(),_sensingLogTimer=setInterval(t,2e3)}const RUSENSE_TAB_KEY="rusense_tab_visible";function _rusenseTabVisible(){return"1"===localStorage.getItem(RUSENSE_TAB_KEY)}function syncRusenseTabToggle(){const e=document.getElementById("rusense-tab-enabled");e&&(e.checked=_rusenseTabVisible())}function applyRusenseTabVisibility(){const e=_rusenseTabVisible();document.querySelectorAll(".rusense-nav").forEach(t=>t.classList.toggle("hidden",!e)),e||void 0===currentTab||"rusense"!==currentTab||showTab("dashboard"),syncRusenseTabToggle()}async function syncRusenseTabFromServer(e){try{const t=e||await fetchAPI("/api/config");t&&Object.prototype.hasOwnProperty.call(t,RUSENSE_TAB_KEY)&&(localStorage.setItem(RUSENSE_TAB_KEY,t[RUSENSE_TAB_KEY]?"1":"0"),applyRusenseTabVisibility())}catch(e){}}function onRusenseTabToggled(e){localStorage.setItem(RUSENSE_TAB_KEY,e.checked?"1":"0"),applyRusenseTabVisibility(),postAPI("/api/config",{[RUSENSE_TAB_KEY]:e.checked}).catch(e=>console.warn("Failed to persist RuSense tab visibility:",e)),e.checked&&showTab("rusense")}function showTab(e){if("networks"===e)return showTab("network"),void showNetworkSubtab("archive");if("network-map"===e)return showTab("network"),void showNetworkSubtab("map");if("credentials"===e)return showTab("discovered"),void showDiscoveredSubtab("credentials");if(currentTab=e,_rusenseLoader&&"rusense"!==e)try{_rusenseLoader.suspend()}catch(e){}if("rusense"!==e){const e=document.getElementById("observatory-frame");e&&e.getAttribute("src")&&e.removeAttribute("src")}systemMonitoringInterval&&"system"!==e&&(clearInterval(systemMonitoringInterval),systemMonitoringInterval=null),document.querySelectorAll(".tab-content").forEach(e=>{e.classList.add("hidden")}),document.querySelectorAll(".nav-btn, [data-tab]").forEach(e=>{e.classList.remove("bg-Ragnar-600"),e.classList.add("text-gray-300","hover:text-white","hover:bg-gray-700")});const t=document.getElementById(`${e}-tab`);t&&t.classList.remove("hidden");const n=document.querySelector(`[data-tab="${e}"]`);if(n&&(n.classList.add("bg-Ragnar-600"),n.classList.remove("text-gray-300","hover:text-white","hover:bg-gray-700")),loadTabData(e),"config"===e)try{refreshSensingInstallCard(),syncRusenseTabToggle()}catch(e){}const a=document.getElementById("mobile-menu");a&&a.classList.add("hidden")}function _setSubtabActive(e,t){e&&(e.classList.toggle("bg-Ragnar-600",t),e.classList.toggle("text-white",t),e.classList.toggle("text-slate-400",!t),e.classList.toggle("hover:bg-slate-700",!t),e.classList.toggle("hover:text-white",!t))}function showNetworkSubtab(e){const t={hosts:"net-sub-hosts",archive:"net-sub-archive",map:"net-sub-map"};Object.keys(t).forEach(n=>{const a=document.getElementById(t[n]);a&&a.classList.toggle("hidden",n!==e),_setSubtabActive(document.getElementById("net-subtab-"+n),n===e)}),"archive"===e?loadAllNetworksData():"map"===e?_mapInitialized||loadNetworkMap():"hosts"===e&&loadNetworkData()}function showDiscoveredSubtab(e){const t={main:"disc-sub-main",credentials:"disc-sub-credentials"};Object.keys(t).forEach(n=>{const a=document.getElementById(t[n]);a&&a.classList.toggle("hidden",n!==e),_setSubtabActive(document.getElementById("disc-subtab-"+n),n===e)}),"credentials"===e&&loadCredentials()}function refreshCurrentTab(){loadTabData(currentTab),addConsoleMessage(`Refreshed ${currentTab} data`,"info")}function setupAutoRefresh(){autoRefreshIntervals.network=setInterval(()=>{"network"===currentTab&&socket&&socket.connected&&socket.emit("request_network")},1e4),autoRefreshIntervals.connect=setInterval(()=>{"connect"===currentTab&&refreshWifiStatus(),"pentest"===currentTab&&manualModeActive&&refreshBluetoothStatus()},15e3),autoRefreshIntervals.discovered=setInterval(()=>{"discovered"===currentTab&&socket&&socket.connected&&(socket.emit("request_credentials"),socket.emit("request_loot"),loadAttackLogs())},2e4),autoRefreshIntervals.console=setInterval(()=>{"dashboard"===currentTab&&loadConsoleLogs()},1e4),autoRefreshIntervals.dashboard=setInterval(()=>{"dashboard"===currentTab&&loadDashboardData()},2e4),autoRefreshIntervals.updates=setInterval(()=>{checkForUpdatesQuiet()},3e5),setTimeout(()=>{checkForUpdatesQuiet()},3e4),setPwnStatusPollInterval(15e3)}function initializeMobileMenu(){const e=document.getElementById("mobile-menu-btn"),t=document.getElementById("mobile-menu");e&&t&&e.addEventListener("click",()=>{t.classList.toggle("hidden")});const n=document.getElementById("desktop-nav");function a(){n.style.position="absolute",n.style.visibility="hidden",n.style.whiteSpace="nowrap",n.classList.remove("hidden"),n.classList.add("flex");let a=0;let s=0;for(const e of n.children)e.classList.contains("hidden")||(a+=e.scrollWidth,s++);a+=4*Math.max(0,s-1),n.style.position="",n.style.visibility="",n.style.whiteSpace="";const o=n.parentElement,r=o?o.querySelector(".flex.items-center.space-x-3"):null,i=r?r.offsetWidth+24:200;a>(o?o.offsetWidth:window.innerWidth)-i-48?(n.classList.add("hidden"),n.classList.remove("flex"),e.classList.remove("hidden")):(n.classList.remove("hidden"),n.classList.add("flex"),e.classList.add("hidden"),t&&t.classList.add("hidden"))}n&&e&&(a(),window.addEventListener("resize",a),window._updateNavMode=a)}function initializeThreatIntelFilters(){const e=document.querySelectorAll(".threat-intel-filter-btn");e&&0!==e.length&&(e.forEach(e=>{e.addEventListener("click",()=>{setThreatIntelFilter(e.getAttribute("data-status"))})}),setThreatIntelFilter(threatIntelStatusFilter,{skipReload:!0}))}async function loadInitialData(){try{try{const e=await fetchAPI("/api/auth/status"),t=document.getElementById("logout-btn");t&&t.classList.toggle("hidden",!e.configured);const n=document.getElementById("mobile-logout-btn");n&&n.classList.toggle("hidden",!e.configured)}catch(e){}const e=await fetchAPI("/api/dashboard/quick");e&&(updateDashboardStats(e),updateDashboardStatus(e)),setTimeout(()=>{refreshWifiStatus().catch(e=>console.warn("WiFi status load failed:",e)),refreshEthernetStatus().catch(e=>console.warn("LAN status load failed:",e))},200),setTimeout(()=>{loadConsoleLogs().then(()=>{addConsoleMessage("Ragnar Modern Web Interface Initialized","success"),addConsoleMessage("Dashboard loaded successfully","info")}).catch(e=>{console.warn("Console logs load failed:",e),addConsoleMessage("Error loading console logs","warning")})},1e3);let t=!1;const n=()=>{t||(t=!0,console.log("User interaction detected - starting background tab preload"),setTimeout(()=>preloadAllTabs(),100))};document.addEventListener("mousemove",n,{once:!0}),document.addEventListener("click",n,{once:!0}),document.addEventListener("scroll",n,{once:!0}),document.addEventListener("touchstart",n,{once:!0}),setTimeout(()=>{t||(t=!0,console.log("Auto-starting background tab preload after timeout"),preloadAllTabs())},1e4)}catch(e){console.error("Error loading initial data:",e),addConsoleMessage("Error loading critical dashboard data","error")}}async function preloadAllTabs(){console.log("Starting background preload of all tabs...");try{await loadNetworkData().catch(e=>console.warn("Network preload failed:",e)),preloadedTabs.add("network"),await new Promise(e=>setTimeout(e,500)),await Promise.all([loadCredentialsData().catch(e=>console.warn("Credentials preload failed:",e)),loadLootData().catch(e=>console.warn("Loot preload failed:",e)),loadAttackLogs().catch(e=>console.warn("Attack logs preload failed:",e)),loadVulnerabilityIntel().catch(e=>console.warn("Vulnerability intel preload failed:",e))]),preloadedTabs.add("discovered"),await new Promise(e=>setTimeout(e,500)),await loadThreatIntelData().catch(e=>console.warn("Threat intel preload failed:",e)),preloadedTabs.add("threat-intel"),await new Promise(e=>setTimeout(e,500)),await loadConnectData().catch(e=>console.warn("Connect preload failed:",e)),preloadedTabs.add("connect"),await new Promise(e=>setTimeout(e,500)),await loadEpaperDisplay().catch(e=>console.warn("E-Paper preload failed:",e)),preloadedTabs.add("epaper"),await new Promise(e=>setTimeout(e,500)),await Promise.all([loadFilesData().catch(e=>console.warn("Files preload failed:",e)),loadConfigData().catch(e=>console.warn("Config preload failed:",e))]),preloadedTabs.add("files"),preloadedTabs.add("config"),serverModeEnabled&&(await new Promise(e=>setTimeout(e,500)),await Promise.all([loadTrafficAnalysisData().catch(e=>console.warn("Traffic preload failed:",e)),loadAdvancedVulnData().catch(e=>console.warn("Adv vuln preload failed:",e))]),preloadedTabs.add("traffic"),preloadedTabs.add("adv-vuln")),console.log("Background tab preload completed"),addConsoleMessage("All tabs preloaded and ready","success")}catch(e){console.error("Error during tab preloading:",e)}}async function loadTabData(e){const t=preloadedTabs.has(e);switch(e){case"rusense":initRusense();break;case"dashboard":await loadDashboardData(),setTimeout(()=>loadConsoleLogs(),50);break;case"network":await loadNetworkData();break;case"networks":t||await loadAllNetworksData();break;case"connect":t?(await refreshWifiStatus().catch(e=>console.warn("WiFi refresh failed:",e)),await refreshEthernetStatus().catch(e=>console.warn("LAN refresh failed:",e)),await refreshBluetoothStatus().catch(e=>console.warn("Bluetooth refresh failed:",e))):await loadConnectData();break;case"pentest":manualModeActive?(await loadPentestData(),checkAirSnitchInstalled(),populateAirSnitchInterfaceDropdowns(),refreshAirSnitchResults()):(addConsoleMessage("Enable Pentest Mode to access the Pentest tab","warning"),showTab("dashboard"));break;case"discovered":t||await Promise.all([loadCredentialsData(),loadLootData(),loadAttackLogs(),loadVulnerabilityIntel()]),await refreshPwnagotchiStatus({silent:!0});break;case"threat-intel":await loadThreatIntelData();break;case"files":t||await loadFilesData();break;case"system":loadSystemData();break;case"netkb":loadNetkbData();break;case"epaper":t||await loadEpaperDisplay();break;case"config":t?await refreshPwnagotchiStatus({silent:!0}):await loadConfigData();break;case"traffic":loadTrafficAnalysisData();break;case"adv-vuln":loadAdvancedVulnData();break;case"wardriving":loadWardrivingData();break;case"network-map":_mapInitialized||loadNetworkMap();break;case"credentials":loadCredentials()}}async function loadDashboardData(){try{const e=["target-count","target-total-count","target-inactive-count","port-count","vuln-count","cred-count","level-count","scanned-network-count","points-count"];e.forEach(e=>{const t=document.getElementById(e);t&&t.classList.add("animate-pulse")});const t=await fetchAPI("/api/dashboard/quick");if(e.forEach(e=>{const t=document.getElementById(e);t&&t.classList.remove("animate-pulse")}),t){updateDashboardStatus(t);const{network:e}=getSelectedDashboardNetworkKey();e?await refreshDashboardStatsForCurrentSelection({forceRefresh:!0,fallbackData:t}):updateDashboardStats(t)}await loadAIInsights()}catch(e){console.error("Error loading dashboard data:",e);["target-count","target-total-count","target-inactive-count","port-count","vuln-count","cred-count","level-count","scanned-network-count","points-count"].forEach(e=>{const t=document.getElementById(e);t&&t.classList.remove("animate-pulse")})}}function toNumber(e,t=0){const n=Number(e);return Number.isFinite(n)?n:t}function formatRelativeTime(e){if(!Number.isFinite(e))return null;let t=Math.max(0,Math.floor(e));const n=[{label:"d",value:86400},{label:"h",value:3600},{label:"m",value:60},{label:"s",value:1}],a=[];for(const e of n){if(t>=e.value||"s"===e.label&&0===a.length){const n=Math.floor(t/e.value);(n>0||"s"===e.label)&&a.push(`${n}${e.label}`),t-=n*e.value}if(a.length>=2)break}return a.length>0?a.join(" "):"0s"}function buildLastSyncDisplay(e){if(!e)return"Sync pending…";const t=toNumber(e.last_sync_age_seconds,NaN),n=Number.isFinite(t)?`${formatRelativeTime(t)} ago`:"";let a=e.last_sync_iso??e.last_sync_time??e.last_sync_timestamp,s=null;"number"==typeof a?s=new Date(1e3*a).toISOString():"string"==typeof a&&a&&(s=a);let o="";if(s){const e=new Date(s);Number.isNaN(e.getTime())||(o=e.toLocaleString())}return n&&o?`${n} (${o})`:n||(o||"Sync pending…")}function updateDashboardStats(e){if(!e||"object"!=typeof e)return;const t=toNumber(e.active_target_count??e.target_count,0),n=toNumber(e.inactive_target_count??e.offline_target_count,0),a=toNumber(e.total_target_count??t+n,t+n),s=Array.isArray(e.new_target_ips)?e.new_target_ips:Array.isArray(e.new_targets)?e.new_targets:[],o=Array.isArray(e.lost_target_ips)?e.lost_target_ips:Array.isArray(e.lost_targets)?e.lost_targets:[],r=toNumber(e.new_target_count??e.new_targets??s.length,s.length),i=toNumber(e.lost_target_count??e.lost_targets??o.length,o.length),l=toNumber(e.port_count??e.open_port_count,0),c=toNumber(e.vulnerability_count??e.vuln_count,0),d=toNumber(e.vulnerable_hosts_count??e.vulnerable_host_count??0,0),u=toNumber(e.credential_count??e.cred_count,0),p=toNumber(e.level??e.levelnbr,0),g=toNumber(e.points??e.coins,0),m=Math.max(0,toNumber(e.scanned_network_count??e.networks_scanned,0));updateElement("target-count",t),scaleStatNumber("target-count",t),updateElement("target-total-count",a),updateElement("target-inactive-count",n),updateElement("target-new-count",r),updateElement("target-lost-count",i);const f=document.getElementById("target-new-count");f&&(f.title=s.length>0?s.join(", "):"No recent additions");const h=document.getElementById("target-lost-count");h&&(h.title=o.length>0?o.join(", "):"No recent drops"),updateElement("port-count",l),scaleStatNumber("port-count",l),updateElement("vuln-count",c),scaleStatNumber("vuln-count",c),updateElement("dashboard-vulnerable-hosts-count",d),updateElement("cred-count",u),scaleStatNumber("cred-count",u),updateElement("level-count",p),scaleStatNumber("level-count",p),updateElement("scanned-network-count",m),scaleStatNumber("scanned-network-count",m),updateElement("dashboard-scanned-network-count",m),scaleStatNumber("dashboard-scanned-network-count",m),updateElement("points-count",g);const y=r>0?`${r} new`:"No new targets",w=i>0?`${i} lost`:"No targets lost";updateElement("active-target-summary",a>0?`${t}/${a} active`:`${t} active`),updateElement("new-target-summary",y),updateElement("lost-target-summary",w),updateElement("last-sync-display",buildLastSyncDisplay(e))}async function loadNetworkData(){try{await loadStableNetworkData(),updateNetworkStatusBanner()}catch(e){console.error("Error loading network data:",e),addConsoleMessage("Failed to load network data","error")}}async function loadNetworkData(){try{await loadStableNetworkData(),updateNetworkStatusBanner()}catch(e){console.error("Error loading network data:",e),addConsoleMessage("Failed to load network data","error")}}async function loadAllNetworksData(){const e=document.getElementById("networks-list-container");if(e){e.innerHTML='\n <div class="text-center text-gray-400 py-12">\n <svg class="w-8 h-8 inline animate-spin mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>\n </svg>\n <p>Loading AP Archive…</p>\n </div>';try{displayAllNetworks(await fetchAPI("/api/networks/all")),preloadedTabs.add("networks")}catch(t){console.error("Error loading all networks:",t),e.innerHTML=`\n <div class="text-center text-red-400 py-12">\n <p class="text-sm">Failed to load AP Archive: ${escapeHtml(t.message)}</p>\n </div>`}}}function displayAllNetworks(e){const t=document.getElementById("networks-list-container");if(!t)return;const n=e.networks||[];if(0===n.length)return void(t.innerHTML='\n <div class="text-center text-gray-400 py-16">\n <svg class="w-16 h-16 mx-auto mb-4 opacity-30" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.14 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0"></path>\n </svg>\n <p class="text-lg font-medium">No access points recorded yet</p>\n <p class="text-sm text-gray-500 mt-2">Networks will appear here after Ragnar connects and scans them.</p>\n </div>');let a='<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">';n.forEach(e=>{const t=escapeHtml(e.ssid||e.slug),n=escapeHtml(e.last_seen||"Unknown"),s=e.file_count||0,o=[];e.has_loot&&o.push('<span class="px-2 py-0.5 rounded text-xs bg-yellow-500 bg-opacity-20 text-yellow-300">Loot</span>'),e.has_creds&&o.push('<span class="px-2 py-0.5 rounded text-xs bg-green-500 bg-opacity-20 text-green-300">Creds</span>'),e.has_vulns&&o.push('<span class="px-2 py-0.5 rounded text-xs bg-red-500 bg-opacity-20 text-red-300">Vulns</span>'),e.has_scans&&o.push('<span class="px-2 py-0.5 rounded text-xs bg-blue-500 bg-opacity-20 text-blue-300">Scans</span>'),a+=`\n <button type="button"\n onclick="openNetworkFilePanel(${JSON.stringify(e.slug).replace(/"/g,""")}, ${JSON.stringify(e.ssid||e.slug).replace(/"/g,""")})"\n class="text-left bg-gray-800 hover:bg-gray-700 rounded-xl p-5 transition-colors border border-gray-700 hover:border-Ragnar-500 focus:outline-none focus:ring-2 focus:ring-Ragnar-500">\n <div class="flex items-start justify-between mb-3">\n <div class="flex items-center gap-2 min-w-0">\n <svg class="w-5 h-5 text-cyan-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.14 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0"></path>\n </svg>\n <span class="font-semibold text-white truncate" title="${t}">${t}</span>\n </div>\n <svg class="w-4 h-4 text-gray-500 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>\n </svg>\n </div>\n <div class="text-xs text-gray-400 mb-3">\n <span>${s} file${1!==s?"s":""}</span>\n ${n?`<span class="mx-1">·</span><span>${n}</span>`:""}\n </div>\n <div class="flex flex-wrap gap-1.5">\n ${o.length?o.join(""):'<span class="text-xs text-gray-600">No data yet</span>'}\n </div>\n </button>`}),a+="</div>",t.innerHTML=a}async function openNetworkFilePanel(e,t){const n=document.getElementById("networks-file-panel"),a=document.getElementById("networks-list-container"),s=document.getElementById("networks-file-panel-title"),o=document.getElementById("networks-file-list-container");if(n&&s&&o){s.textContent=t,a.classList.add("hidden"),n.classList.remove("hidden"),o.innerHTML='\n <div class="text-center text-gray-400 py-8">\n <svg class="w-6 h-6 inline animate-spin mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>\n </svg>\n <p>Loading files…</p>\n </div>';try{displayNetworkFiles(await fetchAPI(`/api/networks/${encodeURIComponent(e)}/files`))}catch(e){o.innerHTML=`\n <div class="text-center text-red-400 py-8">\n <p class="text-sm">Failed to load files: ${escapeHtml(e.message)}</p>\n </div>`}}}function closeNetworkFilePanel(){const e=document.getElementById("networks-file-panel"),t=document.getElementById("networks-list-container");e&&e.classList.add("hidden"),t&&t.classList.remove("hidden")}const NETWORK_FILE_CATEGORY_LABELS={data_stolen:{label:"Loot",color:"text-yellow-300"},credentials:{label:"Credentials",color:"text-green-300"},vulnerabilities:{label:"Vulnerabilities",color:"text-red-300"},scan_results:{label:"Scan Results",color:"text-blue-300"}};function displayNetworkFiles(e){const t=document.getElementById("networks-file-list-container");if(!t)return;const n=e.files||[];if(0===n.length)return void(t.innerHTML='\n <div class="text-center text-gray-400 py-12">\n <svg class="w-12 h-12 mx-auto mb-3 opacity-30" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h4a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"></path>\n </svg>\n <p>No files collected for this network yet.</p>\n </div>');const a={};n.forEach(e=>{a[e.category]||(a[e.category]=[]),a[e.category].push(e)});let s="";["data_stolen","credentials","vulnerabilities","scan_results"].forEach(e=>{if(!a[e]||0===a[e].length)return;const t=NETWORK_FILE_CATEGORY_LABELS[e]||{label:e,color:"text-gray-300"};s+=`\n <div class="mb-6">\n <h4 class="text-sm font-semibold uppercase tracking-wider ${t.color} mb-3">${t.label} (${a[e].length})</h4>\n <div class="space-y-2">`,a[e].forEach(e=>{const t=escapeHtml(e.filename),n=escapeHtml(e.size||""),a=escapeHtml(e.modified||""),o=e.virtual_path?encodeURIComponent(e.virtual_path):"",r=!!o;s+=`\n <div class="flex items-center justify-between bg-gray-800 rounded-lg px-4 py-3 ${r?"hover:bg-gray-700 cursor-pointer":""} transition-colors"\n ${r?`onclick="openLootFile('${escapeAttr(o)}')"`:""}>\n <div class="flex items-center gap-3 min-w-0">\n <svg class="w-4 h-4 text-gray-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>\n </svg>\n <span class="text-sm text-white truncate" title="${t}">${t}</span>\n </div>\n <div class="flex items-center gap-4 text-xs text-gray-400 flex-shrink-0 ml-4">\n ${n?`<span>${n}</span>`:""}\n ${a?`<span class="hidden sm:inline">${a}</span>`:""}\n ${r?'<svg class="w-4 h-4 text-Ragnar-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path></svg>':""}\n </div>\n </div>`}),s+="</div></div>"}),t.innerHTML=s}async function updateNetworkStatusBanner(){try{const e=await fetchAPI("/api/config");if(e&&e.success){const t=e.config.network_max_failed_pings||15,n=Math.round(60*t/60),a=document.getElementById("network-status-failed-pings"),s=document.getElementById("network-status-offline-time");a&&(a.textContent=t),s&&(s.textContent=`${n} minutes`)}}catch(e){console.debug("Could not update network status banner:",e)}}async function loadStableNetworkData(){try{const{network:e}=getSelectedDashboardNetworkKey()||{},t=e?`/api/network/stable?network=${encodeURIComponent(e)}`:"/api/network/stable",n=await fetchAPI(t);n.success?(displayStableNetworkTable(n),addConsoleMessage(`Network data loaded: ${n.count} hosts`,"info")):addConsoleMessage(`Failed to load network data: ${n.error}`,"error")}catch(e){console.error("Error loading stable network data:",e),addConsoleMessage(`Network data error: ${e.message}`,"error")}}function displayStableNetworkTable(e){const t=document.getElementById("network-hosts-table"),n=document.getElementById("host-count");if(!t)return;if(t.querySelectorAll("tr[data-ip]").forEach(e=>{const t=e.getAttribute("data-ip");t&&saveDeepScanButtonState(t)}),t.innerHTML="",!e.hosts||0===e.hosts.length)return t.innerHTML='\n <tr>\n <td colspan="8" class="text-center py-8 text-gray-400">\n No hosts discovered yet. Network scanning is running in the background.\n </td>\n </tr>\n ',void(n&&(n.textContent="0 hosts"));const a=document.createDocumentFragment();e.hosts.forEach((e,t)=>{t<5&&(console.log(`🔍 Host ${t}:`,e),console.log(" IP field:",e.ip,typeof e.ip),console.log(" All fields:",Object.keys(e)));const n=document.createElement("tr");n.className="border-b border-slate-700 hover:bg-slate-700/50 transition-colors";const s="up"===e.status?'<span class="flex items-center"><div class="w-2 h-2 bg-green-500 rounded-full mr-2"></div>Online</span>':'<span class="flex items-center"><div class="w-2 h-2 bg-gray-500 rounded-full mr-2"></div>Unknown</span>';let o="Unknown"===e.mac?'<span class="text-gray-500">Unknown</span>':`<span class="font-mono text-xs">${e.mac}</span>`,r="Unknown"===e.ports||"Scanning..."===e.ports?'<span class="text-gray-500">Unknown</span>':`<span class="text-xs">${e.ports}</span>`,i="0"===e.vulnerabilities?'<span class="text-gray-500">None</span>':`<span class="text-orange-400">${e.vulnerabilities}</span>`,l="Never"===e.last_scan||"Unknown"===e.last_scan?'<span class="text-gray-500">Never</span>':`<span class="text-xs">${formatTimeAgo(e.last_scan)}</span>`;n.innerHTML=`\n <td class="py-3 px-4">${s}</td>\n <td class="py-3 px-4 font-mono text-sm">${e.ip}</td>\n <td class="py-3 px-4">${"Unknown"===e.hostname?'<span class="text-gray-500">Unknown</span>':e.hostname}</td>\n <td class="py-3 px-4">${o}</td>\n <td class="py-3 px-4">${r}</td>\n <td class="py-3 px-4">${i}</td>\n <td class="py-3 px-4">${l}</td>\n <td class="py-3 px-4">\n <button onclick="triggerDeepScan('${e.ip}', { mode: 'full' })" \n id="deep-scan-btn-${e.ip.replace(/\./g,"-")}"\n data-scan-status="idle"\n class="deep-scan-button bg-purple-600 hover:bg-purple-700 text-white text-xs px-3 py-1 rounded transition-all duration-300"\n title="Scan all 65535 ports with TCP connect (-sT). IP: ${e.ip}">\n Deep Scan\n </button>\n </td>\n `,n.setAttribute("data-ip",e.ip),a.appendChild(n)}),t.appendChild(a),e.hosts.forEach(e=>{restoreDeepScanButtonState(e.ip)}),n&&(n.textContent=`${e.hosts.length} hosts`),cleanupOldDeepScanStates()}function formatTimeAgo(e){try{if(!e||"Never"===e||"Unknown"===e)return"Never";if(e.includes("ago")||e.includes("Recently"))return e;const t=new Date(e);if(isNaN(t.getTime()))return e;const n=new Date-t,a=Math.floor(n/6e4),s=Math.floor(n/36e5),o=Math.floor(n/864e5);return a<1?"Just now":a<60?`${a}m ago`:s<24?`${s}h ago`:o<7?`${o}d ago`:t.toLocaleDateString()}catch(t){return e}}let currentScanState={isScanning:!1,totalHosts:0,scannedHosts:0,currentTarget:"",startTime:null},deepScanButtonStates=new Map;function saveDeepScanButtonState(e){const t=`deep-scan-btn-${e.replace(/\./g,"-")}`,n=document.getElementById(t);if(n){const t={status:n.dataset.scanStatus||"idle",text:n.textContent,classes:n.className,disabled:n.disabled,title:n.title};deepScanButtonStates.set(e,t),console.log(`💾 Saved deep scan button state for ${e}:`,t)}}function restoreDeepScanButtonState(e){const t=`deep-scan-btn-${e.replace(/\./g,"-")}`,n=document.getElementById(t),a=deepScanButtonStates.get(e);n&&a?(n.textContent=a.text,n.className=a.classes,n.disabled=a.disabled,n.title=a.title,n.dataset.scanStatus=a.status,console.log(`🔄 Restored deep scan button state for ${e}:`,a)):n&&!a&&console.log(`⚠️ No saved state found for ${e}, keeping default button state`)}function clearDeepScanButtonState(e){deepScanButtonStates.has(e)&&(deepScanButtonStates.delete(e),console.log(`🗑️ Cleared deep scan button state for ${e}`))}function cleanupOldDeepScanStates(){let e=0;for(const[t]of deepScanButtonStates){const n=`deep-scan-btn-${t.replace(/\./g,"-")}`;document.getElementById(n)||(deepScanButtonStates.delete(t),e++)}e>0&&console.log(`🧹 Cleaned up ${e} old deep scan button states`)}async function startRealtimeScan(){const e=document.getElementById("start-network-scan"),t=document.getElementById("stop-network-scan");try{e.disabled=!0,e.innerHTML="⏳ Starting...";if(!(await networkAwareFetch("/api/scan/start-realtime",{method:"POST",headers:{"Content-Type":"application/json"}})).ok)throw new Error("Failed to start scan");currentScanState.isScanning=!0,currentScanState.startTime=new Date,t.disabled=!1,e.innerHTML="⏳ Scanning...",document.getElementById("scan-progress").classList.remove("hidden"),addConsoleMessage("Real-time network scan started","info")}catch(e){console.error("Error starting scan:",e),addConsoleMessage("Failed to start network scan: "+e.message,"error"),resetScanButtons()}}async function stopRealtimeScan(){const e=document.getElementById("stop-network-scan");try{e.disabled=!0,e.innerHTML="⏳ Stopping...",socket.emit("stop_scan"),addConsoleMessage("Stopping network scan...","info")}catch(t){console.error("Error stopping scan:",t),addConsoleMessage("Failed to stop network scan: "+t.message,"error"),e.disabled=!1,e.innerHTML="⏹️ Stop Scan"}}function resetScanButtons(){const e=document.getElementById("start-network-scan"),t=document.getElementById("stop-network-scan");e.disabled=!1,e.innerHTML='<span class="group-disabled:hidden">🔍</span> Start Full Scan',t.disabled=!0,t.innerHTML="⏹️ Stop Scan",currentScanState.isScanning=!1,document.getElementById("scan-progress").classList.add("hidden")}function handleScanStarted(e){currentScanState.totalHosts=e.total_hosts||0,currentScanState.scannedHosts=0,updateScanProgress(),addConsoleMessage(`Started scanning ${currentScanState.totalHosts} hosts`,"info")}function handleScanProgress(e){currentScanState.scannedHosts=e.completed||0,currentScanState.currentTarget=e.current_target||"",updateScanProgress()}function handleScanHostUpdate(e){if(!e)return;const t=e.type||e.event||"host_update";if("sep_scan_output"!==t){if("sep_scan_error"===t){return void addConsoleMessage(`${e.ip?`sep-scan error for ${e.ip}`:"sep-scan error"}: ${e.message||"Unknown error"}`,"error")}if("sep_scan_completed"===t){return addConsoleMessage(`sep-scan completed for ${e.ip||"target"} ${"success"===e.status?"successfully":"with issues"}`,"success"===e.status?"success":"warning"),void("network"===currentTab&&loadNetworkData())}return"host_updated"===t||e.ip||e.IPs?("network"===currentTab&&updateHostInTable(e),void(e.vulnerabilities&&e.vulnerabilities.length>0&&("threat-intel"===currentTab&&loadThreatIntelData(),"netkb"===currentTab&&loadNetkbData()))):void 0}if(e.message){addConsoleMessage(`${e.ip?`[sep-scan ${e.ip}]`:"[sep-scan]"} ${e.message}`,"info")}}function handleScanCompleted(e){addConsoleMessage(`Network scan completed. Found ${e.hosts_discovered||0} hosts, ${e.vulnerabilities_found||0} vulnerabilities`,"success"),resetScanButtons(),"network"===currentTab&&loadNetworkData()}function handleScanError(e){addConsoleMessage(`Scan error: ${e.error}`,"error"),resetScanButtons()}async function handleCustomDeepScanRequest(){const e=document.getElementById("custom-deep-scan-ip"),t=document.getElementById("custom-deep-scan-status"),n=document.getElementById("custom-deep-scan-btn");if(!e||!t||!n)return;const a=e.value.trim();if(!a)return t.textContent="Enter a target IP address before scanning.",void addConsoleMessage("Manual deep scan aborted: no IP provided","warning");if(!isValidIPv4(a))return t.textContent="Please enter a valid IPv4 address (e.g., 192.168.1.192).",void addConsoleMessage(`Manual deep scan aborted: invalid IPv4 (${a})`,"error");const s=n.dataset.defaultText||n.textContent;n.dataset.defaultText=s,n.disabled=!0,n.classList.add("cursor-wait","opacity-80"),n.textContent="Starting...",t.dataset.currentIp=a,t.textContent=`Launching custom scan for ${a} (top 3000 ports)...`,addConsoleMessage(`Manual deep scan request queued for ${a} (top 3000 ports)`,"info");try{await triggerDeepScan(a,{mode:"top3000",source:"custom"})?t.textContent=`Scan running on ${a}. Watch the console for live updates.`:(t.textContent=`Failed to start scan for ${a}. See console for details.`,t.dataset.currentIp="")}catch(e){t.textContent=`Unexpected error starting scan: ${e.message}`,t.dataset.currentIp=""}finally{n.disabled=!1,n.classList.remove("cursor-wait","opacity-80"),n.textContent=n.dataset.defaultText}}function testDeepScan(){console.log("🧪 Testing deep scan with hardcoded IP..."),triggerDeepScan("192.168.1.211")}async function triggerDeepScan(e,t={}){try{if(console.log("🔍 triggerDeepScan CALLED"),console.log(" Received IP parameter:",e),console.log(" IP type:",typeof e),console.log(" IP length:",e?e.length:"null/undefined"),!e)return console.error("❌ IP parameter is empty in triggerDeepScan!"),void addConsoleMessage("Error: No IP address provided for deep scan","error");const n=(t.mode||"full").toLowerCase(),a=Number.isInteger(t.portstart)?t.portstart:void 0,s=Number.isInteger(t.portend)?t.portend:void 0,o="top3000"===n?"top 3000 ports":"all 65535 ports",r=`deep-scan-btn-${e.replace(/\./g,"-")}`,i=document.getElementById(r);i&&(i.classList.remove("bg-purple-600","hover:bg-purple-700"),i.classList.add("bg-blue-600","cursor-wait"),i.disabled=!0,i.textContent="Initiating...",i.dataset.scanStatus="initiating",saveDeepScanButtonState(e)),addConsoleMessage(`Starting deep scan on ${e} (${o})...`,"info"),console.log("📤 Sending POST request to /api/scan/deep");const l={ip:e};n&&(l.mode=n),void 0!==a&&(l.portstart=a),void 0!==s&&(l.portend=s),console.log(" Request body:",JSON.stringify(l));const c=await networkAwareFetch("/api/scan/deep",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(l)});console.log("📥 Response received, status:",c.status);const d=await c.json();return console.log("📋 Response data:",d),"success"===d.status?(addConsoleMessage(`Deep scan initiated for ${e} - scanning ${o}`,"success"),!0):(addConsoleMessage(`Failed to start deep scan: ${d.message}`,"error"),i&&(i.classList.remove("bg-blue-600","cursor-wait"),i.classList.add("bg-purple-600","hover:bg-purple-700"),i.disabled=!1,i.textContent="Deep Scan",i.dataset.scanStatus="idle",clearDeepScanButtonState(e)),!1)}catch(t){console.error("Error triggering deep scan:",t),addConsoleMessage(`Error starting deep scan: ${t.message}`,"error");const n=`deep-scan-btn-${e.replace(/\./g,"-")}`,a=document.getElementById(n);return a&&(a.classList.remove("bg-blue-600","cursor-wait"),a.classList.add("bg-purple-600","hover:bg-purple-700"),a.disabled=!1,a.textContent="Deep Scan",a.dataset.scanStatus="idle",clearDeepScanButtonState(e)),!1}}function handleLynisUpdate(e){const{type:t,ip:n,message:a,stage:s,details:o}=e,r=document.getElementById("manual-lynis-btn"),i=document.getElementById("lynis-audit-status");switch(t){case"lynis_started":addConsoleMessage(`🔐 ${a}`,"info"),r&&(r.classList.remove("bg-red-600","hover:bg-red-700"),r.classList.add("bg-blue-600","cursor-wait"),r.disabled=!0,r.textContent="Audit started"),i&&(i.textContent=a,i.className="text-sm text-blue-600 mt-2");break;case"lynis_progress":if(r){const t=e.event;"connecting"===t?r.textContent="Connecting...":"connected"===t?r.textContent="Connected":"installing"===t?r.textContent="Installing Lynis...":"lynis_found"===t?r.textContent="Lynis ready":"audit_starting"===t?r.textContent="Running audit...":"processing"===t&&(r.textContent="Processing results...")}i&&a&&(i.textContent=a,o&&(i.textContent+=` (${o})`)),s&&["connection","setup","audit","processing"].includes(s)&&addConsoleMessage(` ${a}`,"info");break;case"lynis_completed":addConsoleMessage(`✅ ${a}`,"success"),r&&(r.classList.remove("bg-blue-600","cursor-wait"),r.classList.add("bg-green-600"),r.textContent="✅ Audit complete",r.disabled=!0,setTimeout(()=>{r&&(r.classList.remove("bg-green-600"),r.classList.add("bg-red-600","hover:bg-red-700"),r.textContent="Run Lynis Audit",r.disabled=!1)},3e3)),i&&(i.textContent=`Audit completed for ${n}. Check vulnerabilities directory for results.`,i.className="text-sm text-green-600 mt-2");break;case"lynis_error":addConsoleMessage(`❌ ${a}`,"error"),r&&(r.classList.remove("bg-blue-600","cursor-wait"),r.classList.add("bg-red-600"),r.textContent="❌ Audit failed",r.disabled=!0,setTimeout(()=>{r&&(r.classList.remove("bg-red-600"),r.classList.add("bg-red-600","hover:bg-red-700"),r.textContent="Run Lynis Audit",r.disabled=!1)},3e3)),i&&(i.textContent=a,i.className="text-sm text-red-600 mt-2")}}function handleDeepScanUpdate(e){const{type:t,ip:n,message:a}=e,s=`deep-scan-btn-${n.replace(/\./g,"-")}`,o=document.getElementById(s),r=document.getElementById("custom-deep-scan-status"),i=r&&r.dataset.currentIp===n;switch(t){case"deep_scan_started":addConsoleMessage(`🔍 ${a}`,"info"),o&&(o.classList.remove("bg-purple-600","hover:bg-purple-700"),o.classList.add("bg-blue-600","cursor-wait"),o.disabled=!0,o.textContent="Scan started",o.dataset.scanStatus="scanning",saveDeepScanButtonState(n)),i&&a&&(r.textContent=a);break;case"deep_scan_progress":if(o){const t=e.event;if("scanning"===t)o.textContent="Scanning...";else if("hostname"===t)o.textContent=a;else if("port_found"===t){const t=e.port;e.service;o.textContent=`Port ${t} found`}o.dataset.scanStatus="scanning",saveDeepScanButtonState(n)}i&&a&&(r.textContent=a);break;case"deep_scan_completed":const t=e.open_ports?e.open_ports.length:0,l=e.scan_duration?e.scan_duration.toFixed(2):"unknown";if(addConsoleMessage(`✅ Deep scan of ${n} complete: ${t} ports found in ${l}s`,"success"),e.open_ports&&e.open_ports.length>0){addConsoleMessage(` Open ports: ${e.open_ports.slice(0,10).join(", ")}${e.open_ports.length>10?` (+${e.open_ports.length-10} more)`:""}`,"info")}o&&(o.classList.remove("bg-blue-600","cursor-wait"),o.classList.add("bg-green-600"),o.textContent=`✅ ${t} ports`,o.disabled=!0,o.dataset.scanStatus="completed",saveDeepScanButtonState(n),setTimeout(()=>{document.getElementById(s)&&(o.classList.remove("bg-green-600"),o.classList.add("bg-purple-600","hover:bg-purple-700"),o.textContent="Deep Scan",o.disabled=!1,o.dataset.scanStatus="idle",clearDeepScanButtonState(n))},3e3)),i&&(r.textContent=`Scan complete for ${n}: ${t} open ports found.`,r.dataset.currentIp=""),"network"===currentTab&&loadNetworkData();break;case"deep_scan_error":addConsoleMessage(`❌ Deep scan error for ${n}: ${a}`,"error"),o&&(o.classList.remove("bg-blue-600","cursor-wait"),o.classList.add("bg-red-600"),o.textContent="❌ Error",o.disabled=!0,o.dataset.scanStatus="error",saveDeepScanButtonState(n),setTimeout(()=>{document.getElementById(s)&&(o.classList.remove("bg-red-600"),o.classList.add("bg-purple-600","hover:bg-purple-700"),o.textContent="Deep Scan",o.disabled=!1,o.dataset.scanStatus="idle",clearDeepScanButtonState(n))},3e3)),i&&(r.textContent=a||`Scan error for ${n}.`,r.dataset.currentIp="");break;default:addConsoleMessage(`Deep scan update: ${a}`,"info"),i&&a&&(r.textContent=a)}}let enhancedNetworkScanInterval=null,isEnhancedRealTimeScanning=!1;async function startEnhancedRealTimeScan(){const e=document.getElementById("start-network-scan"),t=document.getElementById("stop-network-scan");if(e&&t)try{addConsoleMessage("Starting enhanced real-time network scanning (ARP + Nmap)...","info"),e.disabled=!0,t.disabled=!1,isEnhancedRealTimeScanning=!0,document.getElementById("scan-progress").classList.remove("hidden"),await performCombinedNetworkScan(),enhancedNetworkScanInterval=setInterval(async()=>{isEnhancedRealTimeScanning&&await performCombinedNetworkScan()},15e3),addConsoleMessage("Enhanced real-time network scanning started","info")}catch(e){console.error("Error starting enhanced real-time scan:",e),addConsoleMessage("Failed to start network scan: "+e.message,"error"),resetEnhancedScanButtons()}}async function stopEnhancedRealTimeScan(){const e=document.getElementById("stop-network-scan"),t=document.getElementById("start-network-scan");enhancedNetworkScanInterval&&(clearInterval(enhancedNetworkScanInterval),enhancedNetworkScanInterval=null),isEnhancedRealTimeScanning=!1,e&&t&&(addConsoleMessage("Stopping enhanced network scan...","info"),resetEnhancedScanButtons())}function resetEnhancedScanButtons(){const e=document.getElementById("start-network-scan"),t=document.getElementById("stop-network-scan");e&&t&&(e.disabled=!1,t.disabled=!0,isEnhancedRealTimeScanning=!1,document.getElementById("scan-progress").classList.add("hidden"))}async function performCombinedNetworkScan(){try{const e=await fetchAPI("/api/scan/combined-network");e.success?(updateNetworkTableWithScanData(e),addConsoleMessage(`Network scan found ${e.count} hosts (ARP: ${e.arp_count}, Nmap: ${e.nmap_count})`,"success")):addConsoleMessage(`Network scan failed: ${e.error}`,"error")}catch(e){console.error("Error performing network scan:",e),addConsoleMessage(`Network scan error: ${e.message}`,"error")}}function updateNetworkTableWithScanData(e){const t=document.getElementById("network-hosts-table"),n=document.getElementById("host-count");if(!t)return;if(t.innerHTML="",!e.hosts||0===Object.keys(e.hosts).length)return t.innerHTML='\n <tr>\n <td colspan="8" class="text-center py-8 text-gray-400">\n No hosts discovered. Check network connectivity and try again.\n </td>\n </tr>\n ',void(n&&(n.textContent="0 hosts"));const a=Object.values(e.hosts);document.createDocumentFragment();a.forEach(e=>{const n=document.createElement("tr");n.className="border-b border-slate-700 hover:bg-slate-700/50 transition-colors";const a="up"===e.status?'<span class="flex items-center"><div class="w-2 h-2 bg-green-500 rounded-full mr-2"></div>Online</span>':'<span class="flex items-center"><div class="w-2 h-2 bg-red-500 rounded-full mr-2"></div>Offline</span>';let s=e.mac||"Unknown";e.vendor&&(s+=`<br><span class="text-xs text-gray-400">${e.vendor}</span>`);const o={arp:'<span class="text-xs px-2 py-1 bg-blue-600 rounded">ARP</span>',nmap:'<span class="text-xs px-2 py-1 bg-purple-600 rounded">NMAP</span>',"arp+nmap":'<span class="text-xs px-2 py-1 bg-green-600 rounded">ARP+NMAP</span>'}[e.source]||"";n.innerHTML=`\n <td class="py-3 px-4">${a}</td>\n <td class="py-3 px-4 font-mono text-sm">${e.ip}</td>\n <td class="py-3 px-4">${e.hostname||"Unknown"}</td>\n <td class="py-3 px-4 font-mono text-xs">${s}</td>\n <td class="py-3 px-4">\n <span class="text-xs px-2 py-1 bg-gray-600 rounded">Scanning...</span>\n </td>\n <td class="py-3 px-4">\n <span class="text-xs px-2 py-1 bg-gray-600 rounded">Checking...</span>\n </td>\n <td class="py-3 px-4 text-sm text-gray-400">${(new Date).toLocaleTimeString()}</td>\n <td class="py-3 px-4">\n <div class="flex space-x-2">\n ${o}\n <button onclick="scanSingleHostEnhanced('${e.ip}')" \n class="text-xs px-2 py-1 bg-Ragnar-600 hover:bg-Ragnar-700 rounded transition-colors">\n Scan\n </button>\n </div>\n </td>\n `,t.appendChild(n)}),n&&(n.textContent=`${a.length} hosts`)}async function scanSingleHostEnhanced(e){try{addConsoleMessage(`Scanning host ${e}...`,"info");const t=await postAPI("/api/scan/host",{ip:e,scan_type:"full"});t.success?(addConsoleMessage(`Host ${e} scan completed`,"success"),await performCombinedNetworkScan()):addConsoleMessage(`Host ${e} scan failed: ${t.error}`,"error")}catch(e){console.error("Error scanning host:",e),addConsoleMessage(`Host scan error: ${e.message}`,"error")}}function updateScanProgress(){const e=document.getElementById("scan-progress-text"),t=document.getElementById("scan-progress-bar"),n=document.getElementById("current-scan-target"),a=currentScanState.totalHosts>0?currentScanState.scannedHosts/currentScanState.totalHosts*100:0;e&&(e.textContent=`${currentScanState.scannedHosts}/${currentScanState.totalHosts} hosts`),t&&(t.style.width=`${a}%`),n&&(n.textContent=currentScanState.currentTarget?`Currently scanning: ${currentScanState.currentTarget}`:"")}function escapeSelector(e){return window.CSS&&"function"==typeof CSS.escape?CSS.escape(e):e.replace(/([ #;?%&,.+*~\':"!^$\[\]()=>|\/])/g,"\\$1")}function parseCompactTimestamp(e){if(!e)return null;const t=e.replace(/[^0-9]/g,"");if(t.length<8)return null;const n=Number(t.slice(0,4)),a=Number(t.slice(4,6))-1,s=Number(t.slice(6,8)),o=t.length>=10?Number(t.slice(8,10)):0,r=t.length>=12?Number(t.slice(10,12)):0,i=t.length>=14?Number(t.slice(12,14)):0,l=new Date(n,a,s,o,r,i);return Number.isNaN(l.getTime())?null:l}function buildLastScanInfo(e,t){const n={label:"Never",className:"text-gray-400",timestampText:"",tooltip:"",rawStatus:e||"",rawTimestamp:t||""};let a=(e||"").toString().trim(),s=null;if(a.includes("_")){const e=a.split("_");a=e[0];s=parseCompactTimestamp(e.slice(1).join("_"))}if(!s&&t){const e=new Date(t);Number.isNaN(e.getTime())||(s=e)}if(!s&&e){const t=e.replace(/[^0-9]/g,"");if(t.length>=8){const e=parseCompactTimestamp(t);e&&(s=e)}}const o=a.toLowerCase();a?o.startsWith("success")?(n.label="Success",n.className="text-green-400"):o.startsWith("failed")?(n.label="Failed",n.className="text-red-400"):["running","scanning","pending","inprogress","in_progress"].includes(o)?(n.label=a.replace(/_/g," ").replace(/\b\w/g,e=>e.toUpperCase()),n.className="text-yellow-400"):(n.label=a.replace(/_/g," ").replace(/\b\w/g,e=>e.toUpperCase()),n.className="text-slate-300"):s?(n.label="Completed",n.className="text-blue-400"):(n.label="Never",n.className="text-gray-400"),s&&(n.timestampText=s.toLocaleString());const r=[];return n.rawStatus&&r.push(`Status: ${n.rawStatus}`),n.timestampText&&r.push(`Completed: ${n.timestampText}`),n.rawTimestamp&&!n.timestampText&&r.push(`Reported: ${n.rawTimestamp}`),n.tooltip=r.join("\n"),n}function normalizeHostRecord(e){if(!e)return null;const t=e.IPs||e.ip||e.address||e.target||"";if(!t)return null;const n=e.Hostnames||e.Hostname||e.hostname||e.name||"",a=e["MAC Address"]||e.MAC||e.mac||"",s=e.Alive??e.alive??e.Status??e.status??"",o=null==s?"":String(s).trim(),r=o.toLowerCase(),i=["1","true","online","up","active","success"].includes(r),l=["0","false","offline","down","inactive","failed"].includes(r);let c="Unknown";i?c="Active":l?c="Inactive":o&&(c=o.charAt(0).toUpperCase()+o.slice(1));const d=i?"text-green-400":l?"text-red-400":"text-yellow-400",u=e.Ports??e.ports??e.port_list??e.open_ports;let p=[];Array.isArray(u)?p=u.map(e=>String(e).trim()).filter(Boolean):"string"==typeof u?p=u.split(/[,;\s]+/).map(e=>e.trim()).filter(Boolean):u&&(p=[String(u).trim()]);const g=(Array.isArray(e.vulnerabilities)?e.vulnerabilities:[]).map(e=>"string"==typeof e?e:e&&"object"==typeof e&&(e.vulnerability||e.raw_output||e.description||e.id)||"").filter(Boolean);let m=e["Nmap Vulnerabilities"]||e.nmap_vulnerabilities||e.vulnerability_summary||"";m||"string"!=typeof e.NmapVulnerabilities||(m=e.NmapVulnerabilities);const f=[...g,..."string"==typeof m&&m.trim()?m.split(";").map(e=>e.trim()).filter(Boolean):[]],h=[],y=new Set;f.forEach(e=>{const t=e.toLowerCase();y.has(t)||(y.add(t),h.push(e))});const w=buildLastScanInfo(e.NmapVulnScanner||e.nmap_vuln_scanner||e.scan_status||"",e.last_scan||e.LastScan||e.last_vuln_scan||""),v=Array.isArray(e.threats)?e.threats:[];return{ip:String(t).trim(),hostname:n||"",mac:a||"",ports:p,statusText:c,statusClass:d,vulnerabilityCount:h.length,vulnerabilityPreview:h.slice(0,2).join("; "),vulnerabilityFull:h.join("; "),threats:v,lastScan:w,raw:e}}function formatPortsCell(e){if(!e||0===e.length)return'<span class="text-gray-400">None</span>';const t=e.slice(0,5),n=escapeHtml(t.join(", ")),a=e.length>5?"…":"";return`<span title="${escapeHtml(e.join(", "))}">${n}${a}</span>`}function formatVulnerabilityCell(e){if(!e||0===e.vulnerabilityCount)return'<span class="text-gray-400">None</span>';const t=`${e.vulnerabilityCount} ${1===e.vulnerabilityCount?"issue":"issues"}`,n=e.vulnerabilityFull||e.vulnerabilityPreview||t,a=escapeHtml(n);return`<span class="text-red-400 font-medium" title="${a}">${t}</span>${e.vulnerabilityPreview?`<div class="text-xs text-slate-300 truncate max-w-xs" title="${a}">${escapeHtml(e.vulnerabilityPreview)}</div>`:""}`}function formatLastScanCell(e){if(!e)return'<span class="text-gray-400">Never</span>';const t=e.tooltip?` title="${escapeHtml(e.tooltip)}"`:"",n=e.timestampText?`<div class="text-xs text-gray-400">${escapeHtml(e.timestampText)}</div>`:"";return`<div${t}><span class="${e.className}">${escapeHtml(e.label)}</span>${n}</div>`}let _threatMonitorPollTimer=null;function _renderThreatFindings(e,t){const n={critical:{border:"border-red-700",bg:"bg-red-950/40",badge:"bg-red-600 text-white",text:"text-red-300"},high:{border:"border-orange-700",bg:"bg-orange-950/40",badge:"bg-orange-600 text-white",text:"text-orange-300"},medium:{border:"border-yellow-700",bg:"bg-yellow-950/40",badge:"bg-yellow-600 text-black",text:"text-yellow-300"},low:{border:"border-blue-700",bg:"bg-blue-950/40",badge:"bg-blue-600 text-white",text:"text-blue-300"}};if(!e.findings||0===e.findings.length){t.className="mb-4 rounded-lg border border-green-800 bg-green-950/30 p-4";const n=null!=e.sweep_count?`${e.sweep_count} sweep(s) completed`:`Interface: ${escapeHtml(e.interface||"?")}`;return void(t.innerHTML=`\n <div class="flex items-center justify-between">\n <div class="flex items-center space-x-2">\n <span class="text-green-400 text-lg">✓</span>\n <span class="text-green-300 text-sm font-medium">No external threats detected</span>\n </div>\n <button onclick="document.getElementById('threat-sweep-results').classList.add('hidden')" class="text-gray-500 hover:text-gray-300 text-xs">dismiss</button>\n </div>\n <p class="text-green-300/60 text-xs mt-1">${escapeHtml(n)} · ${escapeHtml(e.own_network?'Connected to "'+e.own_network+'"':"")}</p>`)}const a=e.findings[0].severity,s=n[a]||n.medium;t.className=`mb-4 rounded-lg border ${s.border} ${s.bg} p-4`;let o=e.findings.map(e=>{const t=n[e.severity]||n.medium;return`<tr class="border-b border-slate-800/50">\n <td class="py-2 pr-3"><span class="px-1.5 py-0.5 rounded text-xs font-bold ${t.badge}">${escapeHtml(e.severity.toUpperCase())}</span></td>\n <td class="py-2 pr-3 text-sm font-medium ${t.text}">${escapeHtml(e.type)}</td>\n <td class="py-2 pr-3 text-sm font-mono text-gray-300">${escapeHtml(e.ssid)}</td>\n <td class="py-2 pr-3 text-xs font-mono text-gray-400">${escapeHtml(e.bssid)}</td>\n <td class="py-2 pr-3 text-xs text-gray-400">${escapeHtml(String(e.signal))}${"-"!==e.signal?"%":""}</td>\n <td class="py-2 text-xs text-gray-400">${escapeHtml(e.description)}</td>\n </tr>`}).join("");const r=e.total||e.findings.length,i=null!=e.sweep_count?`${e.sweep_count} sweep(s) · Last: ${e.last_sweep?new Date(e.last_sweep).toLocaleTimeString():"-"}`:`Interface: ${escapeHtml(e.interface||"?")} · Connected to "${escapeHtml(e.own_network||"?")}"`;t.innerHTML=`\n <div class="flex flex-wrap items-start justify-between gap-2 mb-3">\n <div class="flex items-center space-x-2 min-w-0">\n <span class="${s.text} text-lg">⚠</span>\n <span class="${s.text} text-sm font-bold">${r} threat${r>1?"s":""} detected in WiFi airspace</span>\n </div>\n <div class="flex items-center space-x-2 shrink-0">\n ${null!=e.sweep_count?'<button onclick="clearThreatMonitorFindings()" class="text-gray-500 hover:text-gray-300 text-xs mr-2">clear</button>':""}\n <button onclick="document.getElementById('threat-sweep-results').classList.add('hidden')" class="text-gray-500 hover:text-gray-300 text-xs">dismiss</button>\n </div>\n </div>\n <div class="overflow-x-auto -mx-2 sm:mx-0">\n <table class="w-full text-left min-w-[520px]">\n <thead><tr class="border-b border-slate-700 text-xs text-gray-500">\n <th class="pb-1 pr-3 pl-2 sm:pl-0">Severity</th><th class="pb-1 pr-3">Type</th>\n <th class="pb-1 pr-3">SSID</th><th class="pb-1 pr-3">BSSID</th>\n <th class="pb-1 pr-3">Signal</th><th class="pb-1">Description</th>\n </tr></thead>\n <tbody>${o}</tbody>\n </table>\n </div>\n <p class="text-xs text-gray-500 mt-2">${escapeHtml(i)}</p>`}function _updateMonitorModeBadge(e){const t=document.getElementById("monitor-mode-badge");t&&(t.classList.remove("hidden"),e?(t.className="inline-flex items-center mt-1 text-xs font-medium px-2 py-0.5 rounded-full bg-green-900/60 text-green-300 border border-green-700",t.innerHTML="📡 Monitor mode active — full deauth detection"):(t.className="inline-flex items-center mt-1 text-xs font-medium px-2 py-0.5 rounded-full bg-yellow-900/50 text-yellow-300 border border-yellow-700",t.innerHTML="⚠ No monitor adapter — limited deauth detection"))}async function runThreatSweep(){const e=document.getElementById("threat-sweep-btn"),t=document.getElementById("threat-sweep-results");if(!e||!t)return;const n=e.innerHTML;e.disabled=!0,e.classList.add("opacity-60"),e.innerHTML='<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path></svg><span>Scanning airspace…</span>',t.classList.remove("hidden"),t.className="mb-4 rounded-lg border border-slate-700 bg-slate-900/50 p-4",t.innerHTML='<div class="flex items-center space-x-2 text-gray-300 text-sm"><svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path></svg><span>Scanning WiFi airspace for rogue APs, evil twins, and suspicious devices… (~10s)</span></div>';try{const e=await fetch("/api/network/threat-sweep",{method:"POST"}),n=await e.json();if(!n.success)return t.className="mb-4 rounded-lg border border-yellow-800 bg-yellow-950/30 p-4",void(t.innerHTML=`<p class="text-yellow-300 text-sm">⚠ Scan failed: ${escapeHtml(n.error||"Unknown error")}</p>`);_renderThreatFindings(n,t),_updateMonitorModeBadge(n.monitor_mode)}catch(e){t.className="mb-4 rounded-lg border border-red-800 bg-red-950/30 p-4",t.innerHTML=`<p class="text-red-300 text-sm">⚠ Error: ${escapeHtml(e.message||"Request failed")}</p>`}finally{e.disabled=!1,e.classList.remove("opacity-60"),e.innerHTML=n}}async function toggleThreatMonitor(){const e=document.getElementById("threat-monitor-toggle"),t=document.getElementById("threat-monitor-warning"),n=document.getElementById("threat-monitor-status"),a=document.getElementById("threat-sweep-results"),s=document.getElementById("threat-monitor-interval");let o=parseInt(s?.value)||60;o=Math.max(10,Math.min(600,o)),s&&(s.value=o);try{const r=await fetch("/api/network/threat-monitor/toggle",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({interval:o})});(await r.json()).enabled?(e&&(e.checked=!0),s&&(s.disabled=!0),t&&t.classList.remove("hidden"),n&&(n.textContent="Monitoring…",n.classList.remove("hidden")),_startThreatMonitorPoll(a)):(e&&(e.checked=!1),s&&(s.disabled=!1),t&&t.classList.add("hidden"),n&&(n.textContent="",n.classList.add("hidden")),_stopThreatMonitorPoll())}catch(t){e&&(e.checked=!e.checked),console.error("Failed to toggle threat monitor:",t)}}function _startThreatMonitorPoll(e){_stopThreatMonitorPoll(),_threatMonitorPollTimer=setInterval(async()=>{try{const t=await fetch("/api/network/threat-monitor"),n=await t.json();if(!n.enabled)return void _stopThreatMonitorPoll();const a=document.getElementById("threat-monitor-status");a&&(a.textContent=`Sweep #${n.sweep_count||0}`,a.classList.remove("hidden")),e&&(e.classList.remove("hidden"),_renderThreatFindings(n,e)),_updateMonitorModeBadge(n.monitor_mode)}catch(e){console.warn("Threat monitor poll failed:",e)}},15e3)}function _stopThreatMonitorPoll(){_threatMonitorPollTimer&&(clearInterval(_threatMonitorPollTimer),_threatMonitorPollTimer=null)}async function clearThreatMonitorFindings(){try{await fetch("/api/network/threat-monitor/clear",{method:"POST"});const e=document.getElementById("threat-sweep-results");e&&(e.className="mb-4 rounded-lg border border-slate-700 bg-slate-900/50 p-4",e.innerHTML='<p class="text-gray-400 text-xs">Findings cleared. Monitoring continues…</p>')}catch(e){console.warn("Failed to clear findings:",e)}}async function _restoreThreatMonitorState(){try{const e=await fetch("/api/network/threat-monitor"),t=await e.json();if(t.enabled){const e=document.getElementById("threat-monitor-toggle"),n=document.getElementById("threat-monitor-warning"),a=document.getElementById("threat-monitor-status"),s=document.getElementById("threat-sweep-results"),o=document.getElementById("threat-monitor-interval");e&&(e.checked=!0),o&&(o.value=t.interval||60,o.disabled=!0),n&&n.classList.remove("hidden"),a&&(a.textContent=`Sweep #${t.sweep_count||0}`,a.classList.remove("hidden")),_updateMonitorModeBadge(t.monitor_mode),s&&t.findings&&t.findings.length>0&&(s.classList.remove("hidden"),_renderThreatFindings(t,s)),_startThreatMonitorPoll(s)}}catch(e){}}function formatThreatBadge(e){if(!e||0===e.length)return"";const t={critical:"bg-red-600 text-white",high:"bg-orange-600 text-white",medium:"bg-yellow-600 text-black",low:"bg-blue-600 text-white"},n=e.reduce((e,t)=>{const n={critical:0,high:1,medium:2,low:3};return(n[e.severity]||3)<=(n[t.severity]||3)?e:t}),a=t[n.severity]||t.medium,s=e.map(e=>`[${e.severity.toUpperCase()}] ${e.name}: ${e.description}`).join("\n"),o=1===e.length?n.name:`${e.length} threats`;return`<span class="px-1.5 py-0.5 rounded text-xs font-bold ${a} cursor-help" title="${escapeHtml(s)}">⚠ ${escapeHtml(o)}</span>`}function renderHostRow(e){const t=e.hostname?escapeHtml(e.hostname):"Unknown",n=e.mac?escapeHtml(e.mac):"Unknown",a=escapeHtml(e.ip),s=e.statusClass.includes("green"),o=e.statusClass.includes("red"),r=`<span class="inline-block w-2 h-2 rounded-full ${s?"bg-green-500":o?"bg-red-500":"bg-yellow-500"} mr-1"></span>`,i=formatThreatBadge(e.threats);return`\n <td class="py-3 px-4" data-label="Status">\n <span class="px-2 py-1 rounded text-xs ${e.statusClass} flex items-center">\n ${r}${escapeHtml(e.statusText)}\n </span>\n ${i}\n </td>\n <td class="py-3 px-4 font-mono" data-label="IP Address">${a}</td>\n <td class="py-3 px-4" data-label="Hostname">${t||"Unknown"}</td>\n <td class="py-3 px-4 font-mono text-sm" data-label="MAC Address">${n||"Unknown"}</td>\n <td class="py-3 px-4 text-sm" data-label="Open Ports">${formatPortsCell(e.ports)}</td>\n <td class="py-3 px-4 text-sm" data-label="Vulnerabilities">${formatVulnerabilityCell(e)}</td>\n <td class="py-3 px-4 text-sm" data-label="Last Scan">${formatLastScanCell(e.lastScan)}</td>\n <td class="py-3 px-4" data-label="Actions">\n <button onclick="triggerDeepScan('${e.ip}', { mode: 'full' })"\n id="deep-scan-btn-${e.ip.replace(/\./g,"-")}"\n data-scan-status="idle"\n class="deep-scan-button bg-purple-600 hover:bg-purple-700 text-white text-xs px-3 py-1 rounded transition-all duration-300"\n title="Scan all 65535 ports with TCP connect (-sT). IP: ${e.ip}">\n Deep Scan\n </button>\n <button onclick="openHostPanel('${e.ip}')" class="bg-slate-600 hover:bg-slate-500 text-white text-xs px-3 py-1 rounded transition-colors ml-1" title="View host details">\n Details\n </button>\n </td>\n `}function openHostPanel(e){const t=document.getElementById("host-detail-panel"),n=document.getElementById("host-detail-overlay");t&&n&&(document.getElementById("hdp-ip").textContent=e,document.getElementById("hdp-hostname").textContent="Loading...",document.getElementById("hdp-status").textContent="—",document.getElementById("hdp-mac").textContent="—",document.getElementById("hdp-lastseen").textContent="—",document.getElementById("hdp-portcount").textContent="—",document.getElementById("hdp-ports").innerHTML='<span class="text-gray-400 text-sm">Loading...</span>',document.getElementById("hdp-creds").innerHTML='<p class="text-gray-400 text-sm">Loading...</p>',document.getElementById("hdp-attacks").innerHTML='<p class="text-gray-400 text-sm">Loading...</p>',document.getElementById("hdp-vuln-section").classList.add("hidden"),n.classList.remove("hidden"),t.classList.remove("translate-x-full"),networkAwareFetch(`/api/host/${encodeURIComponent(e)}`).then(e=>e.json()).then(e=>renderHostPanel(e)).catch(e=>{document.getElementById("hdp-hostname").textContent="Error loading data"}))}function closeHostPanel(){const e=document.getElementById("host-detail-panel"),t=document.getElementById("host-detail-overlay");e&&e.classList.add("translate-x-full"),t&&t.classList.add("hidden")}function renderHostPanel(e){document.getElementById("hdp-ip").textContent=e.ip||"—",document.getElementById("hdp-hostname").textContent=e.hostname||"No hostname",document.getElementById("hdp-status").innerHTML=`<span class="${{alive:"text-green-400",degraded:"text-yellow-400",unknown:"text-gray-400"}[e.status]||"text-gray-400"}">${escapeHtml(e.status||"unknown")}</span>`,document.getElementById("hdp-mac").textContent=e.mac||"—",document.getElementById("hdp-lastseen").textContent=e.last_seen||"—",document.getElementById("hdp-portcount").textContent=`${(e.ports||[]).length} port${1!==(e.ports||[]).length?"s":""}`;const t=document.getElementById("hdp-ports");e.ports&&e.ports.length>0?t.innerHTML=e.ports.map(e=>`<span class="px-2 py-1 bg-slate-700 rounded text-xs font-mono">${escapeHtml(e)}</span>`).join(""):t.innerHTML='<span class="text-gray-500 text-sm">None detected</span>';const n=document.getElementById("hdp-creds");if(e.credentials&&e.credentials.length>0){const t={ssh:"bg-blue-900 text-blue-300",smb:"bg-purple-900 text-purple-300",ftp:"bg-yellow-900 text-yellow-300",telnet:"bg-orange-900 text-orange-300",rdp:"bg-pink-900 text-pink-300",sql:"bg-green-900 text-green-300"};n.innerHTML=e.credentials.map(e=>`\n <div class="flex items-center justify-between bg-slate-800 rounded-lg px-3 py-2">\n <div class="flex items-center gap-2">\n <span class="px-1.5 py-0.5 rounded text-xs font-semibold uppercase ${t[e.service]||"bg-slate-700 text-gray-300"}">${e.service}</span>\n <span class="font-mono text-sm">${escapeHtml(e.username||"—")}</span>\n <span class="text-gray-500">:</span>\n <span class="font-mono text-sm text-green-300">${escapeHtml(e.password||"—")}</span>\n </div>\n </div>`).join("")}else n.innerHTML='<p class="text-gray-500 text-sm">No credentials found</p>';const a=document.getElementById("hdp-attacks");if(e.attack_logs&&e.attack_logs.length>0){const t={success:"text-green-400",failed:"text-red-400",timeout:"text-yellow-400"};a.innerHTML=e.attack_logs.map(e=>`\n <div class="bg-slate-800 rounded-lg px-3 py-2 text-xs">\n <div class="flex items-center justify-between mb-1">\n <span class="font-semibold ${t[e.status]||"text-gray-400"}">${e.attack_type}</span>\n <span class="text-gray-500">${e.timestamp}</span>\n </div>\n ${e.message?`<p class="text-gray-300">${escapeHtml(e.message)}</p>`:""}\n </div>`).join("")}else a.innerHTML='<p class="text-gray-500 text-sm">No attack history</p>';const s=document.getElementById("hdp-vuln-section"),o=document.getElementById("hdp-vuln");e.vuln_summary?(o.textContent=e.vuln_summary,s.classList.remove("hidden")):s.classList.add("hidden")}function updateHostCountDisplay(){const e=document.getElementById("network-hosts-table"),t=document.getElementById("host-count");if(!e||!t)return;const n=e.querySelectorAll("tr[data-ip]").length;t.textContent=`${n} host${1!==n?"s":""}`}function updateHostInTable(e){const t=document.getElementById("network-hosts-table");if(!t)return;const n=normalizeHostRecord(e);if(!n)return;const a=t.querySelector('td[colspan="8"]');a&&a.parentElement.remove();const s=`tr[data-ip="${escapeSelector(n.ip)}"]`;let o=t.querySelector(s);o&&saveDeepScanButtonState(n.ip),o||(o=document.createElement("tr"),o.setAttribute("data-ip",n.ip),o.className="border-b border-slate-700 hover:bg-slate-700/50 transition-colors",t.appendChild(o)),o.innerHTML=renderHostRow(n),restoreDeepScanButtonState(n.ip),updateHostCountDisplay()}async function scanSingleHost(e){try{if(!(await networkAwareFetch("/api/scan/host",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({ip:e})})).ok)throw new Error("Failed to start host scan");addConsoleMessage(`Started scan of ${e}`,"info")}catch(t){console.error("Error scanning host:",t),addConsoleMessage(`Failed to scan ${e}: ${t.message}`,"error")}}async function loadCredentialsData(){try{displayCredentialsTable(await fetchAPI("/api/credentials"))}catch(e){console.error("Error loading credentials:",e)}}async function loadLootData(){try{displayLootTable(await fetchAPI("/api/loot"))}catch(e){console.error("Error loading loot data:",e)}}document.addEventListener("DOMContentLoaded",_restoreThreatMonitorState);let currentAttackFilter="all",currentAttackGroupBy="ip",currentAttackNetworkFilter="all",currentAttackIPSearch="",attackLogsCache=null,attackLogsETag=null,attackLogsInFlight=null;async function loadAttackLogs(e={}){const{force:t=!1}=e;if(attackLogsInFlight)return attackLogsInFlight;const n={};return!t&&attackLogsETag&&(n["If-None-Match"]=attackLogsETag),attackLogsCache||(document.getElementById("attack-logs-container").innerHTML='\n <div class="text-center text-gray-400 py-8">\n <svg class="w-8 h-8 inline animate-spin mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>\n </svg>\n <p>Loading attack logs...</p>\n </div>\n '),attackLogsInFlight=(async()=>{try{const e=await networkAwareFetch("/api/attack?limit=200&days=7",{headers:n});if(304===e.status)return console.debug("Attack logs unchanged; skipping DOM update"),attackLogsCache;if(!e.ok)throw new Error(`HTTP error! status: ${e.status}`);const t=await e.json();return attackLogsCache=t,attackLogsETag=e.headers.get("ETag")||attackLogsETag,displayAttackLogs(t),t}catch(e){return console.error("Error loading attack logs:",e),attackLogsCache||(document.getElementById("attack-logs-container").innerHTML=`\n <div class="text-center text-red-400 py-8">\n <svg class="w-12 h-12 mx-auto mb-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>\n </svg>\n <p>Error loading attack logs</p>\n <p class="text-sm text-gray-500 mt-2">${e.message}</p>\n </div>\n `),attackLogsCache}finally{attackLogsInFlight=null}})(),attackLogsInFlight}function filterAttackLogs(e){currentAttackFilter=e,document.querySelectorAll(".attack-filter-btn").forEach(t=>{t.getAttribute("data-filter")===e?t.classList.add("ring-2","ring-white"):t.classList.remove("ring-2","ring-white")}),attackLogsCache&&displayAttackLogs(attackLogsCache)}async function refreshAttackLogs(){await loadAttackLogs({force:!0})}function setAttackGroupBy(e){currentAttackGroupBy=e,document.querySelectorAll(".attack-groupby-btn").forEach(t=>{const n=t.getAttribute("data-groupby")===e;t.classList.toggle("bg-Ragnar-600",n),t.classList.toggle("text-white",n),t.classList.toggle("text-gray-400",!n),t.classList.toggle("hover:text-white",!n)}),attackLogsCache&&displayAttackLogs(attackLogsCache)}function filterAttackByNetwork(e){currentAttackNetworkFilter=e,attackLogsCache&&displayAttackLogs(attackLogsCache)}function onAttackIPSearch(e){currentAttackIPSearch=e.trim().toLowerCase(),attackLogsCache&&displayAttackLogs(attackLogsCache)}function _updateAttackNetworkDropdown(e){const t=document.getElementById("attack-network-filter");if(!t)return;const n=t.value;t.innerHTML='<option value="all">All Networks</option>',(e||[]).forEach(e=>{const a=document.createElement("option");a.value=e,a.textContent="unknown"===e?"Unknown Network":e,e===n&&(a.selected=!0),t.appendChild(a)})}function _buildAttackHostBlock(e,t){const n=t.filter(e=>"success"===e.status).length,a=t.filter(e=>"failed"===e.status).length,s=t.filter(e=>"timeout"===e.status).length,o=e.replace(/[^a-zA-Z0-9]/g,"-");let r=`\n <div class="bg-slate-800 bg-opacity-50 rounded-lg border border-slate-700 overflow-hidden">\n <div class="px-4 py-3 bg-slate-900 bg-opacity-50 flex items-center justify-between cursor-pointer hover:bg-opacity-70 transition-colors" onclick="toggleAttackHost('${o}')">\n <div class="flex items-center space-x-3">\n <svg class="w-5 h-5 text-Ragnar-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"></path>\n </svg>\n <span class="font-semibold text-lg">${e}</span>\n <span class="text-sm text-gray-400">(${t.length} attacks)</span>\n </div>\n <div class="flex items-center space-x-4">\n <span class="text-sm text-green-400">✓ ${n}</span>\n <span class="text-sm text-red-400">✗ ${a}</span>\n <span class="text-sm text-yellow-400">⏱ ${s}</span>\n <svg id="attack-chevron-${o}" class="w-5 h-5 text-gray-400 transform transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>\n </svg>\n </div>\n </div>\n <div id="attack-host-${o}" class="hidden px-4 py-3 space-y-2">\n `;return t.sort((e,t)=>new Date(t.timestamp)-new Date(e.timestamp)),t.forEach(e=>{const t={success:"bg-green-900 bg-opacity-30 border-green-500",failed:"bg-red-900 bg-opacity-30 border-red-500",timeout:"bg-yellow-900 bg-opacity-30 border-yellow-500"}[e.status]||"bg-gray-900 bg-opacity-30 border-gray-500",n={success:"✓",failed:"✗",timeout:"⏱"}[e.status]||"•",a={success:"text-green-400",failed:"text-red-400",timeout:"text-yellow-400"}[e.status]||"text-gray-400";r+=`\n <div class="border-l-4 ${t} p-3 rounded-r-lg">\n <div class="flex items-start justify-between">\n <div class="flex-1">\n <div class="flex items-center space-x-2 mb-1">\n <span class="font-semibold ${a}">${n} ${e.attack_type}</span>\n ${e.target_port?`<span class="text-xs text-gray-400">Port ${e.target_port}</span>`:""}\n <span class="text-xs text-gray-500">${e.timestamp}</span>\n </div>\n ${e.message?`<p class="text-sm text-gray-300 mb-2">${escapeHtml(e.message)}</p>`:""}\n ${Object.keys(e.details||{}).length>0?`\n <div class="text-xs space-y-1 mt-2">\n ${Object.entries(e.details).map(([e,t])=>`\n <div class="flex items-center space-x-2">\n <span class="text-gray-500">${e}:</span>\n <span class="text-gray-300 font-mono">${escapeHtml(String(t))}</span>\n </div>\n `).join("")}\n </div>\n `:""}\n </div>\n </div>\n </div>\n `}),r+="</div></div>",r}function displayAttackLogs(e){if(!e||!e.attack_logs)return void(document.getElementById("attack-logs-container").innerHTML='\n <div class="text-center text-gray-400 py-8">\n <svg class="w-12 h-12 mx-auto mb-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>\n </svg>\n <p>No attack logs found</p>\n </div>\n ');document.getElementById("attack-stat-total").textContent=e.total_count||0,document.getElementById("attack-stat-success").textContent=e.success_count||0,document.getElementById("attack-stat-failed").textContent=e.failed_count||0;const t=e.attack_logs.filter(e=>"timeout"===e.status).length;document.getElementById("attack-stat-timeout").textContent=t,_updateAttackNetworkDropdown(e.available_networks||[]);let n=e.attack_logs;if("all"!==currentAttackFilter&&(n=n.filter(e=>e.status===currentAttackFilter)),"all"!==currentAttackNetworkFilter&&(n=n.filter(e=>(e.network_ssid||"unknown")===currentAttackNetworkFilter)),currentAttackIPSearch&&(n=n.filter(e=>(e.target_ip||"").toLowerCase().includes(currentAttackIPSearch))),0===n.length)return void(document.getElementById("attack-logs-container").innerHTML='\n <div class="text-center text-gray-400 py-8">\n <svg class="w-12 h-12 mx-auto mb-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>\n </svg>\n <p>No attacks match the current filters</p>\n </div>\n ');let a='<div class="space-y-4">';if("network"===currentAttackGroupBy){const e={};n.forEach(t=>{const n=t.network_ssid||"unknown";e[n]||(e[n]={});const a=t.target_ip||"Unknown";e[n][a]||(e[n][a]=[]),e[n][a].push(t)}),Object.keys(e).sort().forEach(t=>{const n=t.replace(/[^a-zA-Z0-9]/g,"-"),s=Object.values(e[t]).flat(),o=s.filter(e=>"success"===e.status).length,r=s.filter(e=>"failed"===e.status).length,i=s.filter(e=>"timeout"===e.status).length,l=Object.keys(e[t]).length;a+=`\n <div class="bg-slate-900 bg-opacity-60 rounded-xl border border-slate-600 overflow-hidden">\n <div class="px-4 py-3 bg-slate-900 flex items-center justify-between cursor-pointer hover:bg-slate-800 transition-colors" onclick="toggleAttackHost('net-${n}')">\n <div class="flex items-center space-x-3">\n <svg class="w-5 h-5 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0"></path>\n </svg>\n <span class="font-bold text-blue-300">${escapeHtml("unknown"===t?"Unknown Network":t)}</span>\n <span class="text-sm text-gray-400">${l} host${1!==l?"s":""} · ${s.length} attack${1!==s.length?"s":""}</span>\n </div>\n <div class="flex items-center space-x-4">\n <span class="text-sm text-green-400">✓ ${o}</span>\n <span class="text-sm text-red-400">✗ ${r}</span>\n <span class="text-sm text-yellow-400">⏱ ${i}</span>\n <svg id="attack-chevron-net-${n}" class="w-5 h-5 text-gray-400 transform transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>\n </svg>\n </div>\n </div>\n <div id="attack-host-net-${n}" class="hidden px-4 py-3 space-y-3">\n `,Object.keys(e[t]).sort().forEach(n=>{a+=_buildAttackHostBlock(n,e[t][n])}),a+="</div></div>"})}else{const e={};n.forEach(t=>{const n=t.target_ip||"Unknown";e[n]||(e[n]=[]),e[n].push(t)}),Object.keys(e).sort().forEach(t=>{a+=_buildAttackHostBlock(t,e[t])})}a+="</div>",document.getElementById("attack-logs-container").innerHTML=a}function toggleAttackHost(e){const t=document.getElementById(`attack-host-${e}`),n=document.getElementById(`attack-chevron-${e}`);t&&n&&(t.classList.contains("hidden")?(t.classList.remove("hidden"),n.classList.add("rotate-180")):(t.classList.add("hidden"),n.classList.remove("rotate-180")))}async function loadVulnerabilityIntel(){try{const e=document.getElementById("vulnerability-intel-container");e.innerHTML='\n <div class="text-center text-gray-400 py-8">\n <svg class="w-8 h-8 inline animate-spin mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>\n </svg>\n <p>Loading service intelligence...</p>\n </div>\n ';const t=await fetchAPI("/api/vulnerability-intel");if(!t)return void(e.innerHTML='\n <div class="text-center text-red-400 py-8">\n <p>Error loading service intelligence</p>\n </div>\n ');document.getElementById("intel-stat-scanned").textContent=t.statistics.total_scanned||0,document.getElementById("intel-stat-interesting").textContent=t.statistics.interesting_hosts||0,document.getElementById("intel-stat-services").textContent=t.statistics.services_with_intel||0,document.getElementById("intel-stat-scripts").textContent=t.statistics.script_outputs||0,displayVulnerabilityIntel(t.scans)}catch(e){console.error("Error loading vulnerability intelligence:",e);document.getElementById("vulnerability-intel-container").innerHTML=`\n <div class="text-center text-red-400 py-8">\n <svg class="w-12 h-12 mx-auto mb-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>\n </svg>\n <p>Error loading service intelligence</p>\n <p class="text-sm text-gray-500 mt-2">${e.message}</p>\n </div>\n `}}function displayVulnerabilityIntel(e){const t=document.getElementById("vulnerability-intel-container");if(!e||0===e.length)return void(t.innerHTML='\n <div class="text-center text-gray-400 py-8">\n <svg class="w-12 h-12 mx-auto mb-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>\n </svg>\n <p>No interesting intelligence found</p>\n <p class="text-sm text-gray-500 mt-2">Scanned hosts without interesting data are filtered out</p>\n </div>\n ');let n='<div class="space-y-4">';e.forEach(e=>{const t=e.total_services||0,a=e.services.reduce((e,t)=>e+(t.scripts?.length||0),0);n+=`\n <div class="bg-slate-800 bg-opacity-50 rounded-lg border border-slate-700 overflow-hidden">\n <div class="px-4 py-3 bg-slate-900 bg-opacity-50 flex items-center justify-between cursor-pointer hover:bg-opacity-70 transition-colors" onclick="toggleVulnHost('${e.ip}')">\n <div class="flex items-center space-x-3">\n <svg class="w-5 h-5 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>\n </svg>\n <div>\n <div class="font-semibold text-lg">${escapeHtml(e.hostname)}</div>\n <div class="text-sm text-gray-400">${escapeHtml(e.ip)}</div>\n </div>\n </div>\n <div class="flex items-center space-x-4">\n ${e.download_url?`\n <a href="${e.download_url}" target="_blank" rel="noopener noreferrer"\n class="text-xs px-3 py-1 rounded-full bg-cyan-900/60 text-cyan-200 border border-cyan-500/40 hover:bg-cyan-800/80 transition">\n ${"lynis"===e.scan_type?"Download full report":"View full report"}\n </a>\n `:""}\n ${e.log_url&&e.log_url!==e.download_url?`\n <a href="${e.log_url}" target="_blank" rel="noopener noreferrer"\n class="text-xs px-3 py-1 rounded-full bg-slate-900/60 text-slate-200 border border-slate-500/40 hover:bg-slate-800/80 transition">\n View audit log\n </a>\n `:""}\n <span class="text-sm text-cyan-400">📡 ${t} services</span>\n ${a>0?`<span class="text-sm text-purple-400">📜 ${a} scripts</span>`:""}\n <span class="text-xs text-gray-500">${e.scan_date}</span>\n <svg id="vuln-chevron-${e.ip.replace(/\./g,"-")}" class="w-5 h-5 text-gray-400 transform transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>\n </svg>\n </div>\n </div>\n \n <div id="vuln-host-${e.ip.replace(/\./g,"-")}" class="hidden px-4 py-3 border-t border-slate-700">\n <div class="space-y-3">\n ${e.services.sort((e,t)=>{const n="system"===e.port||"lynis pentest"===e.service,a="system"===t.port||"lynis pentest"===t.service;if(n&&!a)return-1;if(!n&&a)return 1;if(!n&&!a){return(parseInt(e.port)||99999)-(parseInt(t.port)||99999)}return 0}).map(e=>{const t=e.scripts&&e.scripts.length>0,n="system"===e.port||"lynis pentest"===e.service;return`\n <div class="${n?"bg-blue-900 bg-opacity-30 border border-blue-500/30":"bg-slate-700 bg-opacity-50"} rounded-lg p-4">\n <div class="flex items-start justify-between mb-2">\n <div class="flex-1">\n <div class="flex items-center space-x-2 mb-1">\n ${n?'<svg class="w-4 h-4 text-blue-400 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>\n </svg>\n <span class="font-semibold text-blue-200">🖥️ System Audit</span>':`<span class="font-semibold text-white">${escapeHtml(e.port)}</span>`}\n <span class="text-sm ${n?"text-blue-300":"text-gray-400"}">${escapeHtml(e.service)}</span>\n </div>\n ${e.version?`\n <div class="text-sm text-cyan-300 font-mono bg-slate-900 bg-opacity-50 px-2 py-1 rounded inline-block">\n ${escapeHtml(e.version)}\n </div>\n `:""}\n </div>\n </div>\n \n ${t?`\n <div class="mt-3 space-y-2">\n ${e.scripts.map(e=>`\n <div class="bg-slate-900 bg-opacity-50 rounded p-3">\n <div class="text-sm font-semibold text-purple-400 mb-2">\n 📜 ${escapeHtml(e.name)}\n </div>\n <pre class="text-xs text-gray-300 font-mono whitespace-pre-wrap overflow-x-auto max-h-96 scrollbar-thin">${escapeHtml(e.output)}</pre>\n </div>\n `).join("")}\n </div>\n `:""}\n </div>\n `}).join("")}\n </div>\n </div>\n </div>\n `}),n+="</div>",t.innerHTML=n}function toggleVulnHost(e){const t=`vuln-host-${e.replace(/\./g,"-")}`,n=`vuln-chevron-${e.replace(/\./g,"-")}`,a=document.getElementById(t),s=document.getElementById(n);a.classList.contains("hidden")?(a.classList.remove("hidden"),s.classList.add("rotate-180")):(a.classList.add("hidden"),s.classList.remove("rotate-180"))}async function refreshVulnerabilityIntel(){showNotification("Refreshing service intelligence...","info"),await loadVulnerabilityIntel()}function escapeHtml(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML}async function loadConfigData(){try{const e=await fetchAPI("/api/config");displayConfigForm(e),syncRusenseTabFromServer(e),loadAIConfiguration(e),loadPushoverConfiguration(e),await loadHardwareProfiles(),displayCurrentProfile(e),updateVulnerabilityCount(),await refreshPwnagotchiStatus({silent:!0}),checkForUpdates(),await loadSecurityConfig(),loadWardrivingOnBootState(),loadWardrivingBackfillState(),loadKioskState(),loadScanIntensity();const t=document.getElementById("wd-import-file"),n=document.getElementById("wd-import-filename");t&&n&&t.addEventListener("change",()=>{n.textContent=t.files.length?t.files[0].name:"No file chosen"})}catch(e){console.error("Error loading config:",e)}}async function loadScanIntensity(){const e=document.getElementById("scan-intensity-select"),t=document.getElementById("scan-intensity-current");if(e)try{const n=await fetchAPI("/api/config/scan-intensity"),a=n&&n.current?n.current:"high";if(e.value=a,t){const e={light:"Light",medium:"Medium",high:"Viking Rage"};t.textContent=e[a]||a,t.className="text-xs px-2 py-0.5 rounded "+("high"===a?"bg-red-700 text-red-200":"medium"===a?"bg-yellow-700 text-yellow-200":"bg-green-700 text-green-200")}}catch(e){console.error("Failed to load scan intensity:",e)}}async function onScanIntensityChanged(e){const t=e.value,n=document.getElementById("scan-intensity-status");try{const e=await fetch("/api/config/scan-intensity",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({intensity:t})}),a=await e.json();if(!e.ok||a.error)throw new Error(a.error||`HTTP ${e.status}`);n&&(n.textContent=`Saved: ${a.label}`,n.className="text-xs mt-2 text-green-400",n.classList.remove("hidden"),setTimeout(()=>n.classList.add("hidden"),3e3)),loadScanIntensity()}catch(e){console.error("Failed to set scan intensity:",e),n&&(n.textContent=`Error: ${e.message}`,n.className="text-xs mt-2 text-red-400",n.classList.remove("hidden"))}}let _securityExpanded=!1;function toggleSecuritySection(){_securityExpanded=!_securityExpanded;const e=document.getElementById("security-config-content"),t=document.getElementById("security-summary"),n=document.getElementById("security-chevron");_securityExpanded?(e.classList.remove("hidden"),t.classList.add("hidden"),n.style.transform="rotate(90deg)"):(e.classList.add("hidden"),t.classList.remove("hidden"),n.style.transform="rotate(0deg)")}async function loadSecurityConfig(){try{const e=await fetchAPI("/api/auth/status"),t=document.getElementById("security-config-content"),n=document.getElementById("security-summary"),a=document.getElementById("security-badge");if(!t)return;const s=document.getElementById("logout-btn");s&&s.classList.toggle("hidden",!e.configured);const o=document.getElementById("mobile-logout-btn");if(o&&o.classList.toggle("hidden",!e.configured),e.configured?(a&&a.classList.remove("hidden"),n&&(n.innerHTML='<p class="text-sm text-green-400">Authentication is enabled. Database is encrypted and hardware-bound. Expand to manage.</p>')):(a&&a.classList.add("hidden"),n&&(n.innerHTML='<p class="text-sm text-red-400">Set up authentication to protect your Ragnar instance. Once enabled, all access will require login. The database will be encrypted and bound to this hardware.</p>')),e.configured){let n=e.hw_match?'<span class="text-green-400 text-xs">Hardware matched</span>':'<span class="text-red-400 text-xs">Hardware mismatch!</span>';t.innerHTML=`\n <div class="space-y-4">\n <div class="flex items-center justify-between">\n <div>\n <span class="text-green-400 font-medium">Authentication Enabled</span>\n <span class="mx-2 text-gray-500">|</span>\n ${n}\n <span class="mx-2 text-gray-500">|</span>\n <span class="text-gray-400 text-xs">${e.recovery_codes_remaining} recovery codes remaining</span>\n </div>\n </div>\n\n \x3c!-- Change Password --\x3e\n <div class="glass rounded-lg p-4">\n <h4 class="font-medium mb-3">Change Password</h4>\n <form id="change-pw-form" onsubmit="handleChangePassword(event)" class="space-y-3">\n <input type="password" id="current-password" required autocomplete="current-password"\n class="w-full px-3 py-2 rounded-lg bg-slate-700 border border-slate-600 text-white placeholder-gray-400 focus:ring-2 focus:ring-sky-500 outline-none"\n placeholder="Current password">\n <input type="password" id="new-password" required minlength="8" autocomplete="new-password"\n class="w-full px-3 py-2 rounded-lg bg-slate-700 border border-slate-600 text-white placeholder-gray-400 focus:ring-2 focus:ring-sky-500 outline-none"\n placeholder="New password (min 8 chars)">\n <input type="password" id="confirm-new-password" required minlength="8" autocomplete="new-password"\n class="w-full px-3 py-2 rounded-lg bg-slate-700 border border-slate-600 text-white placeholder-gray-400 focus:ring-2 focus:ring-sky-500 outline-none"\n placeholder="Confirm new password">\n <div id="change-pw-status" class="hidden p-3 rounded-lg text-sm"></div>\n <button type="submit"\n class="bg-sky-600 hover:bg-sky-700 text-white px-4 py-2 rounded-lg transition-colors text-sm">\n Change Password\n </button>\n </form>\n </div>\n\n \x3c!-- Recovery Codes --\x3e\n <div class="glass rounded-lg p-4">\n <h4 class="font-medium mb-3">Recovery Codes</h4>\n <p class="text-gray-400 text-sm mb-3">\n ${e.recovery_codes_remaining} of 10 codes remaining.\n Regenerate to get 10 new codes (old codes will be invalidated).\n </p>\n <div id="regen-codes-display" class="hidden mb-3"></div>\n <div id="regen-status" class="hidden p-3 rounded-lg text-sm mb-3"></div>\n <button onclick="handleRegenRecovery()"\n class="bg-yellow-600 hover:bg-yellow-700 text-white px-4 py-2 rounded-lg transition-colors text-sm">\n Regenerate Recovery Codes\n </button>\n </div>\n\n \x3c!-- Logout --\x3e\n <div class="glass rounded-lg p-4">\n <h4 class="font-medium mb-3">Session</h4>\n <button onclick="handleLogout()"\n class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg transition-colors text-sm">\n Logout\n </button>\n </div>\n </div>\n `}else t.innerHTML='\n <div class="mb-4">\n <div class="p-3 rounded-lg bg-yellow-900/30 border border-yellow-700 text-sm text-yellow-300 mb-4">\n Warning: Once authentication is enabled, you will need your password or recovery codes to access Ragnar.\n Make sure to save your recovery codes in a safe place.\n </div>\n </div>\n <form id="auth-setup-form" onsubmit="handleAuthSetup(event)" class="space-y-4">\n <div>\n <label class="block text-sm font-medium text-gray-300 mb-2">Username</label>\n <input type="text" id="setup-username" required autocomplete="username"\n class="w-full px-4 py-2 rounded-lg bg-slate-700 border border-slate-600 text-white placeholder-gray-400 focus:ring-2 focus:ring-sky-500 focus:border-transparent outline-none"\n placeholder="Choose a username">\n </div>\n <div>\n <label class="block text-sm font-medium text-gray-300 mb-2">Password</label>\n <input type="password" id="setup-password" required minlength="8" autocomplete="new-password"\n class="w-full px-4 py-2 rounded-lg bg-slate-700 border border-slate-600 text-white placeholder-gray-400 focus:ring-2 focus:ring-sky-500 focus:border-transparent outline-none"\n placeholder="Minimum 8 characters">\n </div>\n <div>\n <label class="block text-sm font-medium text-gray-300 mb-2">Confirm Password</label>\n <input type="password" id="setup-confirm-password" required minlength="8" autocomplete="new-password"\n class="w-full px-4 py-2 rounded-lg bg-slate-700 border border-slate-600 text-white placeholder-gray-400 focus:ring-2 focus:ring-sky-500 focus:border-transparent outline-none"\n placeholder="Confirm password">\n </div>\n <div id="setup-status" class="hidden p-3 rounded-lg text-sm"></div>\n <button type="submit" id="setup-btn"\n class="w-full bg-sky-600 hover:bg-sky-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors">\n Enable Authentication\n </button>\n </form>\n '}catch(e){console.error("Error loading security config:",e)}}async function handleAuthSetup(e){e.preventDefault();const t=document.getElementById("setup-status"),n=document.getElementById("setup-btn"),a=document.getElementById("setup-username").value.trim(),s=document.getElementById("setup-password").value,o=document.getElementById("setup-confirm-password").value;if(s!==o)return t.className="p-3 rounded-lg text-sm bg-red-900/30 border border-red-700 text-red-300",t.textContent="Passwords do not match",void t.classList.remove("hidden");n.disabled=!0,n.textContent="Setting up...";try{const e=await postAPI("/api/auth/setup",{username:a,password:s,confirm_password:o});if(e.success){t.className="p-3 rounded-lg text-sm bg-green-900/30 border border-green-700 text-green-300",t.innerHTML="Authentication enabled! Save your recovery codes below.",t.classList.remove("hidden");document.getElementById("security-config-content").innerHTML=`\n <div class="p-4 rounded-lg bg-green-900/20 border border-green-700 mb-4">\n <h4 class="font-bold text-green-400 mb-2">Authentication Enabled Successfully!</h4>\n <p class="text-sm text-gray-300 mb-3">\n Save these recovery codes in a safe place. Each code can only be used once.\n You will need them if you forget your password.\n </p>\n <p class="text-sm text-yellow-300 mb-4">\n The database will be encrypted automatically when you log out or when Ragnar shuts down.\n From the next start, a login will be required.\n </p>\n <div class="grid grid-cols-2 gap-2 mb-4">\n ${e.recovery_codes.map(e=>`<div class="font-mono text-sm bg-slate-800 border border-sky-700 rounded px-3 py-2 text-center text-sky-300 select-all">${e}</div>`).join("")}\n </div>\n <button onclick="copyRecoveryCodes()" id="copy-codes-btn"\n class="bg-sky-600 hover:bg-sky-700 text-white px-4 py-2 rounded-lg transition-colors text-sm mr-2">\n Copy All Codes\n </button>\n <button onclick="loadSecurityConfig()"\n class="bg-slate-600 hover:bg-slate-700 text-white px-4 py-2 rounded-lg transition-colors text-sm">\n Done\n </button>\n </div>\n `,window._tempRecoveryCodes=e.recovery_codes,addConsoleMessage("Authentication enabled - database encrypted","success")}else t.className="p-3 rounded-lg text-sm bg-red-900/30 border border-red-700 text-red-300",t.textContent=e.error||"Setup failed",t.classList.remove("hidden"),n.disabled=!1,n.textContent="Enable Authentication"}catch(e){t.className="p-3 rounded-lg text-sm bg-red-900/30 border border-red-700 text-red-300",t.textContent="Setup failed: "+(e.message||"Unknown error"),t.classList.remove("hidden"),n.disabled=!1,n.textContent="Enable Authentication"}}function copyRecoveryCodes(){if(window._tempRecoveryCodes){const e=window._tempRecoveryCodes.join("\n");navigator.clipboard.writeText(e).then(()=>{const e=document.getElementById("copy-codes-btn");e.textContent="Copied!",setTimeout(()=>{e.textContent="Copy All Codes"},2e3)}).catch(()=>{addConsoleMessage("Failed to copy - please select and copy manually","warning")})}}async function handleChangePassword(e){e.preventDefault();const t=document.getElementById("change-pw-status"),n=document.getElementById("current-password").value,a=document.getElementById("new-password").value;if(a!==document.getElementById("confirm-new-password").value)return t.className="p-3 rounded-lg text-sm bg-red-900/30 border border-red-700 text-red-300",t.textContent="New passwords do not match",void t.classList.remove("hidden");try{const e=await postAPI("/api/auth/change-password",{current_password:n,new_password:a});e.success?(t.className="p-3 rounded-lg text-sm bg-green-900/30 border border-green-700 text-green-300",t.textContent="Password changed successfully",t.classList.remove("hidden"),document.getElementById("change-pw-form").reset(),addConsoleMessage("Password changed successfully","success")):(t.className="p-3 rounded-lg text-sm bg-red-900/30 border border-red-700 text-red-300",t.textContent=e.error||"Failed to change password",t.classList.remove("hidden"))}catch(e){t.className="p-3 rounded-lg text-sm bg-red-900/30 border border-red-700 text-red-300",t.textContent="Error: "+(e.message||"Unknown error"),t.classList.remove("hidden")}}async function handleRegenRecovery(){const e=prompt("Enter your current password to regenerate recovery codes:");if(!e)return;const t=document.getElementById("regen-status"),n=document.getElementById("regen-codes-display");try{const a=await postAPI("/api/auth/regenerate-recovery",{password:e});a.success?(n.innerHTML=`\n <div class="p-3 rounded-lg bg-slate-800 border border-sky-700">\n <p class="text-sm text-gray-300 mb-2">New recovery codes (save these!):</p>\n <div class="grid grid-cols-2 gap-2">\n ${a.recovery_codes.map(e=>`<div class="font-mono text-sm bg-slate-900 rounded px-2 py-1 text-center text-sky-300 select-all">${e}</div>`).join("")}\n </div>\n </div>\n `,n.classList.remove("hidden"),t.className="p-3 rounded-lg text-sm bg-green-900/30 border border-green-700 text-green-300",t.textContent="Recovery codes regenerated. Save them securely!",t.classList.remove("hidden"),window._tempRecoveryCodes=a.recovery_codes,addConsoleMessage("Recovery codes regenerated","success")):(t.className="p-3 rounded-lg text-sm bg-red-900/30 border border-red-700 text-red-300",t.textContent=a.error||"Failed to regenerate codes",t.classList.remove("hidden"))}catch(e){t.className="p-3 rounded-lg text-sm bg-red-900/30 border border-red-700 text-red-300",t.textContent="Error: "+(e.message||"Unknown error"),t.classList.remove("hidden")}}async function handleLogout(){socket&&socket.disconnect();try{await postAPI("/api/auth/logout",{})}catch(e){}window.location.replace("/login")}function setPwnStatusPollInterval(e=15e3){const t=Math.max(2e3,e||15e3);currentPwnStatusInterval===t&&autoRefreshIntervals.pwn||(autoRefreshIntervals.pwn&&clearInterval(autoRefreshIntervals.pwn),currentPwnStatusInterval=t,autoRefreshIntervals.pwn=setInterval(()=>{"config"!==currentTab&&"discovered"!==currentTab||refreshPwnagotchiStatus({silent:!0})},t))}function initializePwnUI(){if(!document.getElementById("pwn-status-badge"))return;const e=document.getElementById("pwn-install-btn");e&&e.addEventListener("click",handlePwnInstallClick);const t=document.getElementById("pwn-swap-to-pwn-btn");t&&t.addEventListener("click",()=>handlePwnSwap("pwnagotchi"));const n=document.getElementById("pwn-refresh-btn");n&&n.addEventListener("click",()=>refreshPwnagotchiStatus());const a=document.getElementById("pwn-log-refresh-btn");a&&a.addEventListener("click",()=>fetchPwnLogs({initial:0===pwnLogCursor})),updatePwnButtons(),resetPwnLogState(),refreshPwnagotchiStatus({silent:!0});const s=document.getElementById("pwn-config-reload-btn");s&&s.addEventListener("click",()=>loadPwnConfig());const o=document.getElementById("pwn-config-save-btn");o&&o.addEventListener("click",()=>savePwnConfig())}async function refreshPwnagotchiStatus(e={}){const t=Boolean(e&&e.silent);try{const e=await fetchAPI("/api/pwnagotchi/status");if(e&&e.success&&e.status)return updatePwnagotchiUI(e.status),e.status;t||addConsoleMessage("Unable to load Pwnagotchi status","warning")}catch(e){console.error("Error refreshing Pwnagotchi status:",e),t||addConsoleMessage(`Pwnagotchi status error: ${e.message}`,"error")}return null}function updatePwnagotchiUI(e={}){if(!e||"object"!=typeof e)return;const t=Boolean(pwnStatus.installing);pwnStatus={...pwnStatus,...e},pwnStatus.installing=Boolean(pwnStatus.installing);const n=getPwnStateVisuals(pwnStatus),a=document.getElementById("pwn-status-badge");a&&(a.textContent=n.badgeText,a.className=`text-xs font-semibold uppercase tracking-wide px-3 py-1 rounded-full ${n.badgeClass}`),updateElement("pwn-status-message",pwnStatus.message||"Waiting for status...");const s=formatPwnModeLabel(pwnStatus.mode);updateElement("pwn-mode-value",s);const o=document.getElementById("pwn-mode-value");o&&(o.className="font-semibold "+("pwnagotchi"===pwnStatus.mode?"text-fuchsia-300":"text-green-400")),updateElement("pwn-target-value",formatPwnModeLabel(pwnStatus.target_mode)),updateElement("pwn-phase-value",formatPwnPhaseLabel(pwnStatus.phase)),updateElement("pwn-service-state",pwnStatus.service_active?"Running":"Stopped");const r=document.getElementById("pwn-service-state");r&&(r.className="font-semibold "+(pwnStatus.service_active?"text-green-400":"text-slate-200")),updateElement("pwn-service-enabled",pwnStatus.service_enabled?"Enabled":"Disabled");const i=document.getElementById("pwn-service-enabled");i&&(i.className="font-semibold "+(pwnStatus.service_enabled?"text-green-300":"text-slate-200")),updateElement("pwn-last-switch-value",pwnStatus.last_switch?formatTimestamp(pwnStatus.last_switch):"Never"),updateElement("pwn-last-updated",pwnStatus.timestamp?formatTimestamp(pwnStatus.timestamp):(new Date).toLocaleString());const l=document.getElementById("pwn-status-alert");l&&(pwnStatus.message?(l.className=`mt-4 p-4 rounded-lg border text-sm text-gray-200 ${n.alertClass}`,l.innerHTML=`\n <div class="flex items-start gap-3">\n <div class="text-xl">${n.icon}</div>\n <div>\n <p class="font-semibold">${escapeHtml(pwnStatus.message)}</p>\n <p class="text-xs text-gray-300 mt-1">Phase: ${formatPwnPhaseLabel(pwnStatus.phase)} | Mode: ${s}</p>\n </div>\n </div>\n `,l.classList.remove("hidden")):l.classList.add("hidden")),updatePwnDiscoveredCard(pwnStatus,n),updatePwnButtons(),pwnStatus.installing&&!t&&resetPwnLogState("Installer output will stream here during installation."),ensurePwnLogStreamingForStatus(pwnStatus),lastPwnState=pwnStatus.state}function updatePwnButtons(){const e=document.getElementById("pwn-install-card");e&&(e.style.display=pwnStatus.installed?"none":"");const t=document.getElementById("pwn-swap-card");t&&(t.style.display=pwnStatus.installed?"":"none");const n=document.getElementById("pwn-config-card");if(n){const e="none"===n.style.display;n.style.display=pwnStatus.installed?"":"none",e&&pwnStatus.installed&&!n._loaded&&(n._loaded=!0,loadPwnConfig())}const a=document.getElementById("pwn-install-btn");a&&(a.textContent=pwnStatus.installing?"Installing...":"Install Pwnagotchi",a.disabled=pwnStatus.installing,a.classList.toggle("opacity-70",pwnStatus.installing),a.classList.toggle("cursor-not-allowed",pwnStatus.installing));const s=document.getElementById("pwn-swap-to-pwn-btn");if(s){const e="switching"===pwnStatus.state&&"pwnagotchi"===pwnStatus.target_mode,t="running"===pwnStatus.state&&"pwnagotchi"===pwnStatus.target_mode;if(e||t){const e="http://"+window.location.hostname+":8080";if(s.classList.remove("opacity-60","cursor-not-allowed"),s.classList.add("bg-green-600","hover:bg-green-700"),s.classList.remove("bg-amber-600","hover:bg-amber-700"),!s._countdownRunning&&_pwnSwapRequestedThisSession){s._countdownRunning=!0;let t=0;const n=60;s.disabled=!0,s.textContent="Waiting for Pwnagotchi... 0s";const a=setInterval(async()=>{t++,s.textContent=`Waiting for Pwnagotchi... ${t}s`;try{await fetch(e,{mode:"no-cors",cache:"no-store"}),clearInterval(a),s.disabled=!1,s.textContent="Go to Pwnagotchi Portal",s.onclick=function(t){t.preventDefault(),window.open(e,"_blank")}}catch(o){t>=n&&(clearInterval(a),s.disabled=!1,s.textContent="Open Pwnagotchi Portal",s.onclick=function(t){t.preventDefault(),window.open(e,"_blank")})}},1e3)}}else{const e=pwnStatus.installing||"switching"===pwnStatus.state;s.disabled=e,s.textContent="Switch to Pwnagotchi",s.classList.toggle("opacity-60",e),s.classList.toggle("cursor-not-allowed",e),s.classList.remove("bg-green-600","hover:bg-green-700"),s.onclick=null,s._countdownRunning=!1,_pwnSwapRequestedThisSession=!1}}const o=document.getElementById("pwn-swap-hint");if(o){let e="Ragnar UI becomes unavailable once the service stops. Plan to reboot via SSH to come back.";pwnStatus.installing?e="Installer is still running. Swapping will be available once it completes.":"switching"===pwnStatus.state&&"pwnagotchi"===pwnStatus.target_mode?e="Pwnagotchi is starting up. Click the button above to open its portal once ready.":"running"===pwnStatus.state&&"pwnagotchi"===pwnStatus.target_mode?e="Pwnagotchi is running. Ragnar will shut down shortly.":"switching"===pwnStatus.state&&(e="Switch scheduled. Wait for the service hand-off to complete."),o.textContent=e}}async function loadPwnConfig(){const e=document.getElementById("pwn-config-alert");try{const t=await fetchAPI("/api/pwnagotchi/config");if(!t||!t.success)return void _showPwnConfigAlert(e,t?.error||"Failed to load config","error");const n=t.config;_setPwnCfgValue("pwn-cfg-name",n["main.name"]),_setPwnCfgValue("pwn-cfg-whitelist",n["main.whitelist"]),_setPwnCfgChecked("pwn-cfg-deauth",n["personality.deauth"]),_setPwnCfgChecked("pwn-cfg-associate",n["personality.associate"]),_setPwnCfgChecked("pwn-cfg-advertise",n["personality.advertise"]),_setPwnCfgValue("pwn-cfg-min-rssi",String(n["personality.min_rssi"])),_setPwnCfgValue("pwn-cfg-channels",n["personality.channels"]),_setPwnCfgChecked("pwn-cfg-display-enabled",n["ui.display.enabled"]),_setPwnCfgChecked("pwn-cfg-invert",n["ui.invert"]),_setPwnCfgValue("pwn-cfg-rotation",String(n["ui.display.rotation"])),_setPwnCfgValue("pwn-cfg-display-type",n["ui.display.type"]),_setPwnCfgValue("pwn-cfg-web-user",n["ui.web.username"]),_setPwnCfgValue("pwn-cfg-web-pass",n["ui.web.password"]),_setPwnCfgValue("pwn-cfg-web-port",String(n["ui.web.port"])),_setPwnCfgChecked("pwn-cfg-auto-tune",n["main.plugins.auto-tune.enabled"]),_setPwnCfgChecked("pwn-cfg-webcfg",n["main.plugins.webcfg.enabled"]),_setPwnCfgChecked("pwn-cfg-memtemp",n["main.plugins.memtemp.enabled"]),_setPwnCfgChecked("pwn-cfg-grid",n["main.plugins.grid.enabled"]),_setPwnCfgChecked("pwn-cfg-fix-services",n["main.plugins.fix_services.enabled"]);try{const e=await fetchAPI("/api/pwnagotchi/manual-mode");e&&e.success&&_setPwnCfgChecked("pwn-manual-mode",e.enabled)}catch(e){console.error("Error loading Pwnagotchi manual mode:",e)}_showPwnConfigAlert(e,"Configuration loaded","success"),setTimeout(()=>{e&&e.classList.add("hidden")},2e3)}catch(t){console.error("Error loading Pwnagotchi config:",t),_showPwnConfigAlert(e,`Load failed: ${t.message}`,"error")}}async function savePwnConfig(){const e=document.getElementById("pwn-config-alert"),t=document.getElementById("pwn-config-save-btn");try{t&&(t.disabled=!0,t.textContent="Saving...");const n={"main.name":document.getElementById("pwn-cfg-name")?.value||"pwnagotchi","main.whitelist":document.getElementById("pwn-cfg-whitelist")?.value||"","personality.deauth":document.getElementById("pwn-cfg-deauth")?.checked||!1,"personality.associate":document.getElementById("pwn-cfg-associate")?.checked||!1,"personality.advertise":document.getElementById("pwn-cfg-advertise")?.checked||!1,"personality.min_rssi":parseInt(document.getElementById("pwn-cfg-min-rssi")?.value||"-200",10),"personality.channels":document.getElementById("pwn-cfg-channels")?.value||"","ui.display.enabled":document.getElementById("pwn-cfg-display-enabled")?.checked||!1,"ui.invert":document.getElementById("pwn-cfg-invert")?.checked||!1,"ui.display.rotation":parseInt(document.getElementById("pwn-cfg-rotation")?.value||"180",10),"ui.display.type":document.getElementById("pwn-cfg-display-type")?.value||"waveshare_4","ui.web.username":document.getElementById("pwn-cfg-web-user")?.value||"ragnar","ui.web.password":document.getElementById("pwn-cfg-web-pass")?.value||"ragnar","ui.web.port":parseInt(document.getElementById("pwn-cfg-web-port")?.value||"8080",10),"main.plugins.auto-tune.enabled":document.getElementById("pwn-cfg-auto-tune")?.checked||!1,"main.plugins.webcfg.enabled":document.getElementById("pwn-cfg-webcfg")?.checked||!1,"main.plugins.memtemp.enabled":document.getElementById("pwn-cfg-memtemp")?.checked||!1,"main.plugins.grid.enabled":document.getElementById("pwn-cfg-grid")?.checked||!1,"main.plugins.fix_services.enabled":document.getElementById("pwn-cfg-fix-services")?.checked||!1},a=await fetchAPI("/api/pwnagotchi/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({config:n})}),s=document.getElementById("pwn-manual-mode")?.checked||!1,o=await fetchAPI("/api/pwnagotchi/manual-mode",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled:s})});o&&o.success&&(pwnStatus.manual_mode=s),a&&a.success?(_showPwnConfigAlert(e,`Saved ${a.updated?.length||0} setting(s). Changes apply on next Pwnagotchi start.`,"success"),addConsoleMessage("Pwnagotchi config saved","info")):_showPwnConfigAlert(e,a?.error||"Save failed","error")}catch(t){console.error("Error saving Pwnagotchi config:",t),_showPwnConfigAlert(e,`Save failed: ${t.message}`,"error")}finally{t&&(t.disabled=!1,t.textContent="Save Changes")}}function _setPwnCfgValue(e,t){const n=document.getElementById(e);n&&(n.value=t??"")}function _setPwnCfgChecked(e,t){const n=document.getElementById(e);n&&(n.checked=Boolean(t))}function _showPwnConfigAlert(e,t,n){e&&(e.classList.remove("hidden"),e.textContent=t,e.className="success"===n?"mb-4 p-3 rounded-lg text-sm bg-green-900/40 text-green-300 border border-green-700":"mb-4 p-3 rounded-lg text-sm bg-red-900/40 text-red-300 border border-red-700")}function _pwnBadgesHTML(e){let t="";if(e.has_handshake){const n=(e.handshake_types||[]).join(", ");t+=`<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-fuchsia-900 bg-opacity-50 text-fuchsia-300" title="Handshake: ${escapeHtml(n)}">Handshake</span> `}return e.has_gps&&(t+='<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-emerald-900 bg-opacity-50 text-emerald-300" title="GPS coordinates available">GPS</span> '),e.has_netjson&&(t+='<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-900 bg-opacity-50 text-blue-300" title="Network metadata available">NetJSON</span> '),t||'<span class="text-gray-500 text-xs">-</span>'}function _pwnGpsHTML(e){if(e.has_gps&&e.gps){const t=Number(e.gps.latitude).toFixed(5),n=Number(e.gps.longitude).toFixed(5);return`<span class="font-mono text-emerald-300" title="Lat: ${t}, Lng: ${n}">${t}, ${n}</span>`}return'<span class="text-gray-500">-</span>'}function _pwnFileSizeLabel(e){return!e||e<=0?"":e<1024?e+" B":e<1048576?(e/1024).toFixed(1)+" KB":(e/1048576).toFixed(1)+" MB"}function _pwnFilesHTML(e){return Array.isArray(e)&&0!==e.length?'<div class="flex flex-wrap gap-2 mt-2">'+e.map(e=>{const t=escapeHtml(e.name||""),n=_pwnFileSizeLabel(e.size),a=n?` (${n})`:"";return`<a href="/api/pwnagotchi/download?file=${encodeURIComponent(e.name||"")}" download\n class="inline-flex items-center gap-1 px-2 py-1 rounded text-xs bg-slate-700 hover:bg-slate-600 text-slate-200 hover:text-white transition-colors"\n title="Download ${t}${a}">\n <svg class="w-3 h-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>\n <span class="truncate max-w-[10rem]">${t}</span></a>`}).join("")+"</div>":""}function displayPwnNetworksTable(e){const t=document.getElementById("pwn-networks-table");if(!t)return;if(!Array.isArray(e)||0===e.length)return void(t.innerHTML='<p class="text-gray-400">No Pwnagotchi captures found yet.</p>');const n=e.length>10,a=e.slice(0,10),s=n?e.slice(10):[];function o(e){const t=escapeHtml(e.ssid||"Unknown"),n=escapeHtml(e.bssid||"Unknown");return`\n <div class="bg-slate-800 bg-opacity-60 rounded-lg p-4">\n <div class="flex items-start justify-between gap-2 mb-2">\n <span class="text-white font-semibold text-sm break-all">${t}</span>\n <span class="text-xs text-gray-400 whitespace-nowrap">${e.last_seen?formatTimestamp(e.last_seen):"Unknown"}</span>\n </div>\n <div class="text-xs text-gray-400 font-mono mb-2">${n}</div>\n <div class="flex flex-wrap gap-1 mb-1">${_pwnBadgesHTML(e)}</div>\n <div class="text-xs mt-1">${_pwnGpsHTML(e)}</div>\n ${_pwnFilesHTML(e.files)}\n </div>`}function r(e){const t=escapeHtml(e.ssid||"Unknown"),n=escapeHtml(e.bssid||"Unknown"),a=e.last_seen?formatTimestamp(e.last_seen):"Unknown";return`\n <tr class="hover:bg-gray-700 transition-colors">\n <td class="px-4 py-3 text-sm text-white font-semibold">${t}</td>\n <td class="px-4 py-3 text-sm text-gray-300 font-mono">${n}</td>\n <td class="px-4 py-3 text-sm">${_pwnBadgesHTML(e)}</td>\n <td class="px-4 py-3 text-sm">${_pwnGpsHTML(e)}</td>\n <td class="px-4 py-3 text-sm text-gray-400 whitespace-nowrap">${a}</td>\n <td class="px-4 py-3 text-sm">${_pwnFilesHTML(e.files)}</td>\n </tr>`}let i='<div class="md:hidden space-y-3">';a.forEach(e=>{i+=o(e)}),n&&(i+=`\n <button onclick="togglePwnNetworksExpansion()"\n class="flex items-center justify-center w-full py-3 px-4 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors text-gray-300 hover:text-white">\n <span id="pwn-networks-expand-text-m">Show ${s.length} more</span>\n <svg id="pwn-networks-expand-arrow-m" class="w-4 h-4 ml-2 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>\n </svg>\n </button>\n <div id="pwn-networks-hidden-m" class="hidden space-y-3">`,s.forEach(e=>{i+=o(e)}),i+="</div>"),i+="</div>";let l='\n <div class="hidden md:block bg-gray-800 rounded-lg p-4">\n <div class="overflow-x-auto">\n <table class="min-w-full divide-y divide-gray-700">\n <thead>\n <tr>\n <th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase">SSID</th>\n <th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase">BSSID</th>\n <th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase">Data</th>\n <th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase">GPS</th>\n <th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase">Last Seen</th>\n <th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase">Files</th>\n </tr>\n </thead>\n <tbody class="divide-y divide-gray-700">';a.forEach(e=>{l+=r(e)}),l+="</tbody></table></div>",n&&(l+=`\n <div class="mt-4">\n <button onclick="togglePwnNetworksExpansion()"\n class="flex items-center justify-center w-full py-3 px-4 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors text-gray-300 hover:text-white">\n <span id="pwn-networks-expand-text">Show ${s.length} more networks</span>\n <svg id="pwn-networks-expand-arrow" class="w-4 h-4 ml-2 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>\n </svg>\n </button>\n <div id="pwn-networks-hidden" class="hidden mt-2">\n <div class="overflow-x-auto">\n <table class="min-w-full divide-y divide-gray-700">\n <tbody class="divide-y divide-gray-700">`,s.forEach(e=>{l+=r(e)}),l+="</tbody></table></div></div></div>"),l+="</div>",t.innerHTML=i+l}function togglePwnNetworksExpansion(){[["pwn-networks-hidden","pwn-networks-expand-text","pwn-networks-expand-arrow"],["pwn-networks-hidden-m","pwn-networks-expand-text-m","pwn-networks-expand-arrow-m"]].forEach(([e,t,n])=>{const a=document.getElementById(e),s=document.getElementById(t),o=document.getElementById(n);if(!a)return;if(a.classList.contains("hidden"))a.classList.remove("hidden"),s&&(s.textContent="Show less"),o&&(o.style.transform="rotate(180deg)");else{a.classList.add("hidden");const e=a.querySelectorAll("tr, .bg-slate-800").length;s&&(s.textContent=`Show ${e} more`),o&&(o.style.transform="rotate(0deg)")}})}function updatePwnDiscoveredCard(e,t=null){if(!e||"object"!=typeof e)return;const n=document.getElementById("pwn-discovered-card");if(!n)return;if(!arePwnFeaturesEnabled())return void n.classList.add("hidden");if(!Boolean(e.installing||e.installed))return void n.classList.add("hidden");n.classList.remove("hidden");const a=t||getPwnStateVisuals(e),s=document.getElementById("pwn-card-badge");if(s){const t=(e.state||"").toLowerCase();t.includes("error")||t.includes("fail")?s.classList.add("hidden"):(s.classList.remove("hidden"),s.textContent=a.badgeText,s.className=`text-xs font-semibold uppercase tracking-wide px-3 py-1 rounded-full ${a.badgeClass}`)}const o=e.discoveries||{};updateElement("pwn-card-network-count",String(o.network_count||0)),updateElement("pwn-card-handshake-count",String(o.networks_with_handshake||0)),updateElement("pwn-card-gps-count",String(o.networks_with_gps||0)),updateElement("pwn-card-last-discovery",o.last_discovery?formatTimestamp(o.last_discovery):"None"),displayPwnNetworksTable(o.networks||[]),updateElement("pwn-card-updated",`Updated: ${e.timestamp?formatTimestamp(e.timestamp):(new Date).toLocaleString()}`)}function resetPwnLogState(e){pwnLogCursor=0,pwnLogActiveFile=null,clearPwnLogViewer(e||"Installer output will stream here during installation."),updatePwnLogPath(null),setPwnLogIndicator(!1)}function clearPwnLogViewer(e){const t=document.getElementById("pwn-log-viewer"),n=document.getElementById("pwn-log-empty");t&&n&&(t.querySelectorAll('[data-pwn-log-line="true"]').forEach(e=>e.remove()),e&&(n.textContent=e),n.classList.remove("hidden"))}function setPwnLogIndicator(e){const t=document.getElementById("pwn-log-stream-indicator");t&&t.classList.toggle("hidden",!e)}function updatePwnLogPath(e){const t=document.getElementById("pwn-log-path");t&&(e?(t.textContent=e,t.classList.remove("text-gray-500")):(t.textContent="Log path will appear once the installer starts.",t.classList.add("text-gray-500")))}function appendPwnLogEntries(e=[]){const t=document.getElementById("pwn-log-viewer");if(!t||!Array.isArray(e)||0===e.length)return;const n=document.getElementById("pwn-log-empty");n&&n.classList.add("hidden");const a=t.scrollHeight-t.clientHeight-t.scrollTop<40;e.forEach(e=>{const n=document.createElement("div");n.dataset.pwnLogLine="true",n.className=`whitespace-pre-wrap break-words leading-snug ${getPwnLogLineClass(e)}`,n.textContent=e||" ",t.appendChild(n)}),trimPwnLogBuffer(),a&&(t.scrollTop=t.scrollHeight)}function trimPwnLogBuffer(e=600){const t=document.getElementById("pwn-log-viewer");if(!t)return;const n=t.querySelectorAll('[data-pwn-log-line="true"]');if(n.length<=e)return;const a=n.length-e;for(let e=0;e<a;e++)n[e].remove()}function getPwnLogLineClass(e=""){const t=e.toLowerCase();return t.includes("error")||t.includes("failed")||t.includes("[err")?"text-red-300":t.includes("warn")?"text-yellow-200":t.includes("info")||t.includes("[info")?"text-blue-200":"text-gray-200"}function startPwnLogStreaming(e={}){document.getElementById("pwn-log-viewer")&&(pwnLogStreamTimer||(pwnLogStopTimeout&&(clearTimeout(pwnLogStopTimeout),pwnLogStopTimeout=null),pwnLogStreaming=!0,setPwnLogIndicator(!0),fetchPwnLogs({initial:Boolean(e.initial)||0===pwnLogCursor,silent:!0}),pwnLogStreamTimer=setInterval(()=>fetchPwnLogs({silent:!0}),2500)))}function stopPwnLogStreaming(){pwnLogStreamTimer&&(clearInterval(pwnLogStreamTimer),pwnLogStreamTimer=null),pwnLogStreaming=!1,setPwnLogIndicator(!1)}function schedulePwnLogStop(){pwnLogStopTimeout||(pwnLogStopTimeout=setTimeout(()=>{stopPwnLogStreaming(),pwnLogStopTimeout=null},12e3))}function setPwnLogEmptyMessage(e){const t=document.getElementById("pwn-log-empty");t&&(t.textContent=e,t.classList.remove("hidden"))}async function fetchPwnLogs(e={}){if(pwnLogFetchInFlight)return;if(document.getElementById("pwn-log-viewer")){pwnLogFetchInFlight=!0;try{const t=new URLSearchParams;pwnLogCursor>0&&!e.initial?t.set("cursor",pwnLogCursor.toString()):t.set("tail","8192");const n=await fetchAPI(`/api/pwnagotchi/logs?${t.toString()}`);if(!n||!1===n.success)return e.silent||setPwnLogEmptyMessage(n&&n.error?n.error:"Installer log not available yet"),void(n&&n.installing||schedulePwnLogStop());"number"==typeof n.cursor&&(pwnLogCursor=n.cursor),n.file&&n.file!==pwnLogActiveFile&&(pwnLogActiveFile=n.file,clearPwnLogViewer("Streaming installer output…"),updatePwnLogPath(n.file)),Array.isArray(n.entries)&&n.entries.length>0?appendPwnLogEntries(n.entries):e.silent||pwnLogStreaming||setPwnLogEmptyMessage("No installer activity yet."),n.installing||schedulePwnLogStop()}catch(t){console.error("Error fetching Pwnagotchi logs:",t),e.silent||setPwnLogEmptyMessage(`Failed to load installer log (${t.message})`)}finally{pwnLogFetchInFlight=!1}}}function ensurePwnLogStreamingForStatus(e){e&&(e.log_file&&e.log_file!==pwnLogActiveFile&&(pwnLogActiveFile=e.log_file,updatePwnLogPath(e.log_file)),e.installing?(setPwnStatusPollInterval(4e3),startPwnLogStreaming({initial:0===pwnLogCursor})):(setPwnStatusPollInterval(15e3),pwnLogStreaming&&schedulePwnLogStop()))}function getPwnStateVisuals(e){const t=(e.state||"not_installed").toLowerCase();return t.includes("fail")||t.includes("error")?{badgeText:"Error",badgeClass:"bg-red-700 text-red-100",alertClass:"border-red-500 bg-red-900/30",icon:"⚠️"}:e.installing||["preflight","dependencies","python","installing"].includes(t)?{badgeText:"Installing",badgeClass:"bg-yellow-700 text-yellow-100",alertClass:"border-yellow-500 bg-yellow-900/30",icon:"⏳"}:"switching"===t?{badgeText:"Switching",badgeClass:"bg-orange-700 text-orange-100",alertClass:"border-orange-500 bg-orange-900/30",icon:"🔄"}:"running"===t?{badgeText:"Running",badgeClass:"bg-green-700 text-green-100",alertClass:"border-green-500 bg-green-900/30",icon:"✅"}:"installed"===t?{badgeText:"Installed",badgeClass:"bg-blue-700 text-blue-100",alertClass:"border-blue-500 bg-blue-900/30",icon:"ℹ️"}:{badgeText:"Not Installed",badgeClass:"bg-slate-700 text-slate-200",alertClass:"border-slate-700 bg-slate-900",icon:"ℹ️"}}function formatPwnStateLabel(e){return e?e.replace(/_/g," ").replace(/\b\w/g,e=>e.toUpperCase()):"Unknown"}function formatPwnModeLabel(e){return"pwnagotchi"===e?"Pwnagotchi":"Ragnar"}function formatPwnPhaseLabel(e){return e?e.replace(/_/g," ").replace(/\b\w/g,e=>e.toUpperCase()):"Idle"}async function handlePwnInstallClick(){if(pwnStatus.installing)return void addConsoleMessage("Pwnagotchi installer already running","warning");const e=document.getElementById("pwn-install-btn");e&&(e.disabled=!0,e.textContent="Starting installer...",e.classList.add("opacity-70","cursor-not-allowed")),resetPwnLogState("Installer requested. Waiting for output..."),startPwnLogStreaming({initial:!0});try{const e=await postPwnAPI("/api/pwnagotchi/install",{});addConsoleMessage("Pwnagotchi installer started","success"),e&&e.status?updatePwnagotchiUI(e.status):refreshPwnagotchiStatus({silent:!0})}catch(e){console.error("Failed to start Pwnagotchi installer:",e),addConsoleMessage(`Install failed: ${e.message}`,"error"),stopPwnLogStreaming(),setPwnLogEmptyMessage("Installer failed to start. Check Ragnar logs for details.")}finally{updatePwnButtons()}}async function handlePwnSwap(e){const t="pwnagotchi"===e?"pwnagotchi":"ragnar";if("pwnagotchi"===t&&!pwnStatus.installed)return void addConsoleMessage("Install Pwnagotchi before swapping","warning");const n="pwnagotchi"===t?"pwn-swap-to-pwn-btn":"pwn-swap-to-ragnar-btn",a=document.getElementById(n);a&&(a.disabled=!0,a.textContent="Scheduling switch...",a.classList.add("opacity-60","cursor-not-allowed"));try{const e=await postPwnAPI("/api/pwnagotchi/swap",{target:t});"pwnagotchi"===t&&(_pwnSwapRequestedThisSession=!0);addConsoleMessage(e&&e.message?e.message:`Switch scheduled to ${formatPwnModeLabel(t)}`,"info"),e&&e.status?updatePwnagotchiUI(e.status):refreshPwnagotchiStatus({silent:!0})}catch(e){console.error("Failed to schedule Pwnagotchi swap:",e),addConsoleMessage(`Swap failed: ${e.message}`,"error")}finally{updatePwnButtons()}}async function postPwnAPI(e,t={}){const n=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});let a=null;try{a=await n.json()}catch(e){a=null}if(!n.ok||a&&!1===a.success){const e=a&&(a.error||a.message)?a.error||a.message:`Request failed (${n.status})`;throw new Error(e)}return a||{success:!0}}async function loadConnectData(){try{await loadWifiInterfaces(),console.log("Loading connect tab, refreshing connectivity status..."),await Promise.all([refreshWifiStatus(),refreshEthernetStatus(),refreshBluetoothStatus()])}catch(e){console.error("Error loading connect data:",e)}}async function loadFilesData(){try{displayDirectoryTree(),loadFiles("/")}catch(e){console.error("Error loading files data:",e)}}function arePwnFeaturesEnabled(){return"true"===localStorage.getItem("pwnagotchi-enabled")}function applyPwnVisibilityPreference(e){const t=document.getElementById("pwnagotchi-section");t&&(t.style.display=e?"block":"none"),updatePwnDiscoveredCard(pwnStatus),e&&(_bindPwnUpdateButtons(),pwnUpdateInitialChecked||(pwnUpdateInitialChecked=!0,checkPwnUpdates()))}function togglePwnagotchiVisibility(){const e=document.getElementById("pwnagotchi-enabled");if(!e)return;if(e.disabled)return void(e.checked=!1);const t=e.checked;localStorage.setItem("pwnagotchi-enabled",t?"true":"false"),applyPwnVisibilityPreference(t)}function initializePwnagotchiVisibility(){const e=document.getElementById("pwnagotchi-enabled");if(!e)return;const t=arePwnFeaturesEnabled();e.checked=t,applyPwnVisibilityPreference(t)}let pwnUpdateInitialChecked=!1,pwnUpdateInFlight=!1;function _setPwnUpdateBadge(e,t){const n=document.getElementById("pwn-update-status-badge");n&&(n.textContent=e,n.className="text-xs font-semibold uppercase tracking-wide px-3 py-1 rounded-full self-start whitespace-nowrap "+t)}function _setPwnUpdateWarnings(e){const t=document.getElementById("pwn-update-warnings");if(t){if(!e||0===e.length)return t.classList.add("hidden"),void(t.innerHTML="");t.classList.remove("hidden"),t.innerHTML=e.map(e=>`<div>⚠ ${escapeHtml(e)}</div>`).join("")}}function _setPwnUpdatePerformEnabled(e){const t=document.getElementById("pwn-update-perform-btn");t&&(t.disabled=!e,t.className=e?"flex-1 bg-fuchsia-600 hover:bg-fuchsia-700 text-white py-2 px-4 rounded-lg transition-colors":"flex-1 bg-gray-600 text-white py-2 px-4 rounded-lg cursor-not-allowed transition-colors")}function _setPwnUpdateStashVisible(e){const t=document.getElementById("pwn-update-stash-btn");t&&(e?t.classList.remove("hidden"):t.classList.add("hidden"))}async function checkPwnUpdates(){const e=document.getElementById("pwn-update-card");if(e&&!pwnUpdateInFlight){pwnUpdateInFlight=!0,_setPwnUpdateBadge("Checking…","bg-slate-700 text-slate-200");try{const t=await fetchAPI("/api/pwn/check-updates");if(t&&!1===t.installed)return void(e.style.display="none");if(e.style.display="block",t&&t.error)return _setPwnUpdateBadge("Error","bg-red-700 text-red-200"),_setPwnUpdateWarnings([t.error]),_setPwnUpdatePerformEnabled(!1),void _setPwnUpdateStashVisible(!1);const n=document.getElementById("pwn-update-branch"),a=document.getElementById("pwn-update-behind"),s=document.getElementById("pwn-update-current-commit"),o=document.getElementById("pwn-update-latest-commit");n&&(n.textContent=t.current_branch||"—"),a&&(a.textContent=String(t.commits_behind??0)),s&&(s.textContent=t.current_commit||"—"),o&&(o.textContent=t.latest_commit||"—");const r=t.git_status||{},i=[];if(r.has_conflicts)i.push("Local merge conflicts detected in /opt/pwnagotchi");else if(r.is_dirty){const e=(r.modified_files||[]).length;i.push(`${e} local change${1===e?"":"s"} in /opt/pwnagotchi`)}r.has_stash&&i.push(`${r.stash_entries} stash ${1===r.stash_entries?"entry":"entries"} present`),r.status_error&&i.push(r.status_error),_setPwnUpdateWarnings(i),t.updates_available&&(t.commits_behind||0)>0?(_setPwnUpdateBadge("Update Available","bg-orange-700 text-orange-200"),_setPwnUpdatePerformEnabled(!0)):(_setPwnUpdateBadge("Up to Date","bg-green-700 text-green-200"),_setPwnUpdatePerformEnabled(!1)),_setPwnUpdateStashVisible(Boolean(r.is_dirty))}catch(t){e.style.display="block",_setPwnUpdateBadge("Error","bg-red-700 text-red-200"),_setPwnUpdateWarnings([String(t&&t.message||t)]),_setPwnUpdatePerformEnabled(!1),_setPwnUpdateStashVisible(!1)}finally{pwnUpdateInFlight=!1}}}async function performPwnUpdate(){const e=document.getElementById("pwn-update-perform-btn");if(!e||e.disabled)return;if(pwnUpdateInFlight)return;if(confirm("Pull latest Pwnagotchi from origin into /opt/pwnagotchi?\n\nServices are NOT restarted; the new version takes effect on next mode swap or reboot.")){pwnUpdateInFlight=!0,_setPwnUpdateBadge("Updating…","bg-slate-700 text-slate-200"),_setPwnUpdatePerformEnabled(!1);try{const e=await networkAwareFetch("/api/pwn/update",{method:"POST",headers:{"Content-Type":"application/json"}});if(401===e.status)return void(window.location.href="/login");let t=null;try{t=await e.json()}catch(e){t=null}const n=document.getElementById("pwn-update-output");if(n&&(n.textContent=t&&(t.output||t.message)||""),!e.ok||!t||!1===t.success){_setPwnUpdateBadge("Error","bg-red-700 text-red-200");const n=[];return t&&t.error&&n.push(t.error),t&&Array.isArray(t.warnings)&&n.push(...t.warnings),t||n.push(`HTTP ${e.status}`),void _setPwnUpdateWarnings(n.length?n:["Update failed"])}Array.isArray(t.warnings)&&t.warnings.length?_setPwnUpdateWarnings(t.warnings):_setPwnUpdateWarnings([]),_setPwnUpdateBadge("Refreshing…","bg-slate-700 text-slate-200")}catch(e){_setPwnUpdateBadge("Error","bg-red-700 text-red-200"),_setPwnUpdateWarnings([String(e&&e.message||e)])}finally{pwnUpdateInFlight=!1,checkPwnUpdates()}}}async function stashAndUpdatePwn(){const e=document.getElementById("pwn-update-stash-btn");if(!e||e.classList.contains("hidden"))return;if(pwnUpdateInFlight)return;if(confirm("Stash local changes in /opt/pwnagotchi, pull, then drop the stash on success?\n\nLocal changes will be preserved as a git stash if the pull fails.")){pwnUpdateInFlight=!0,_setPwnUpdateBadge("Stashing & Updating…","bg-slate-700 text-slate-200"),_setPwnUpdatePerformEnabled(!1),_setPwnUpdateStashVisible(!1);try{const e=await networkAwareFetch("/api/pwn/stash-update",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({})});if(401===e.status)return void(window.location.href="/login");let t=null;try{t=await e.json()}catch(e){t=null}const n=document.getElementById("pwn-update-output");if(n&&(n.textContent=t&&(t.output||t.message)||""),!e.ok||!t||!1===t.success){_setPwnUpdateBadge("Error","bg-red-700 text-red-200");const n=[];return t&&t.error&&n.push(t.error),t&&t.local_changes_preserved&&n.push("Local changes preserved as a git stash."),t&&Array.isArray(t.warnings)&&n.push(...t.warnings),t||n.push(`HTTP ${e.status}`),void _setPwnUpdateWarnings(n.length?n:["Stash update failed"])}Array.isArray(t.warnings)&&t.warnings.length?_setPwnUpdateWarnings(t.warnings):_setPwnUpdateWarnings([]),_setPwnUpdateBadge("Refreshing…","bg-slate-700 text-slate-200")}catch(e){_setPwnUpdateBadge("Error","bg-red-700 text-red-200"),_setPwnUpdateWarnings([String(e&&e.message||e)])}finally{pwnUpdateInFlight=!1,checkPwnUpdates()}}}function _bindPwnUpdateButtons(){const e=document.getElementById("pwn-update-check-btn"),t=document.getElementById("pwn-update-perform-btn"),n=document.getElementById("pwn-update-stash-btn");e&&!e.dataset.bound&&(e.dataset.bound="1",e.onclick=checkPwnUpdates),t&&!t.dataset.bound&&(t.dataset.bound="1",t.onclick=performPwnUpdate),n&&!n.dataset.bound&&(n.dataset.bound="1",n.onclick=stashAndUpdatePwn)}function updatePwnToggleAvailability(e){headlessMode=Boolean(e);const t=document.getElementById("pwnagotchi-enabled");if(!t)return;const n=document.getElementById("pwn-toggle-wrapper"),a=document.getElementById("pwn-headless-warning");headlessMode?(t.checked&&(t.checked=!1,localStorage.setItem("pwnagotchi-enabled","false"),applyPwnVisibilityPreference(!1)),t.disabled=!0,t.setAttribute("aria-disabled","true"),n&&n.classList.add("cursor-not-allowed","opacity-60","pointer-events-none"),a&&a.classList.remove("hidden")):(t.disabled=!1,t.removeAttribute("aria-disabled"),n&&n.classList.remove("cursor-not-allowed","opacity-60","pointer-events-none"),a&&a.classList.add("hidden"))}async function handleHeadlessMode(){try{const e=await fetch("/api/system/headless"),t=await e.json(),n=!0===t.headless||!0===t.is_headless;headlessMode=n,n?(console.log("[Headless] Headless mode detected - hiding Display UI elements"),applyHeadlessVisibility(!0)):(console.log("[Headless] Display mode detected - Display UI elements visible"),applyHeadlessVisibility(!1)),updatePwnToggleAvailability(n)}catch(e){console.log("[Headless] Detection failed, assuming display mode:",e.message),headlessMode=!1,applyHeadlessVisibility(!1)}}function applyHeadlessVisibility(e){const t=document.querySelectorAll(".requires-display");e?(t.forEach(e=>{e.style.display="none",e.setAttribute("data-hidden-by-headless","true")}),console.log(`[Headless] Hidden ${t.length} Display UI elements`),"epaper"===currentTab&&(console.log("[Headless] Redirecting from Display tab to dashboard"),showTab("dashboard"))):(t.forEach(e=>{"true"===e.getAttribute("data-hidden-by-headless")&&(e.style.display="",e.removeAttribute("data-hidden-by-headless"))}),console.log(`[Headless] Restored ${t.length} Display UI elements`)),window._updateNavMode&&window._updateNavMode()}async function loadHardwareProfiles(){try{const e=await fetchAPI("/api/config/hardware-profiles"),t=document.getElementById("hardware-profile-select");document.getElementById("apply-profile-btn");if(!t)return;t.innerHTML='<option value="">Select a hardware profile...</option>',window.hardwareProfiles=e;for(const[n,a]of Object.entries(e)){const e=document.createElement("option");e.value=n,e.textContent=`${a.name} (${a.ram}MB RAM)`,t.appendChild(e)}t.addEventListener("change",function(){const t=this.value,n=document.getElementById("apply-profile-btn");t&&e[t]?(showProfileDetails(e[t]),n.disabled=!1):(hideProfileDetails(),n.disabled=!0)})}catch(e){console.error("Error loading hardware profiles:",e),addConsoleMessage("Failed to load hardware profiles","error");const t=document.getElementById("hardware-profile-select");t&&(t.innerHTML='<option value="">Error loading profiles</option>')}}function showProfileDetails(e){const t=document.getElementById("profile-details");t&&(document.getElementById("profile-description").textContent=e.description||"No description available",document.getElementById("profile-ram").textContent=`${e.ram}MB`,document.getElementById("profile-threads").textContent=e.settings.scanner_max_threads||"N/A",document.getElementById("profile-concurrent").textContent=e.settings.orchestrator_max_concurrent||"N/A",document.getElementById("profile-speed").textContent=e.settings.nmap_scan_aggressivity||"N/A",t.classList.remove("hidden"))}function hideProfileDetails(){const e=document.getElementById("profile-details");e&&e.classList.add("hidden")}async function applySelectedProfile(){const e=document.getElementById("hardware-profile-select").value;e?await confirmApplyProfile(e,window.hardwareProfiles[e]):addConsoleMessage("Please select a hardware profile first","warning")}async function detectAndApplyHardware(){try{addConsoleMessage("Detecting hardware...","info");const e=document.getElementById("hardware-detection-info");e.innerHTML='<span class="text-Ragnar-400">🔍 Detecting hardware...</span>';const t=await fetchAPI("/api/config/detect-hardware");e.innerHTML=`\n <div class="space-y-2">\n <div class="flex justify-between">\n <span class="text-gray-400">Detected Model:</span>\n <span class="text-white font-semibold">${t.model}</span>\n </div>\n <div class="flex justify-between">\n <span class="text-gray-400">Total RAM:</span>\n <span class="text-white font-semibold">${t.ram_gb} GB (${t.ram_mb} MB)</span>\n </div>\n <div class="flex justify-between">\n <span class="text-gray-400">CPU Cores:</span>\n <span class="text-white font-semibold">${t.cpu_count}</span>\n </div>\n <div class="flex justify-between">\n <span class="text-gray-400">Recommended Profile:</span>\n <span class="text-Ragnar-400 font-semibold">${t.recommended_profile}</span>\n </div>\n </div>\n `,addConsoleMessage(`Detected: ${t.model} with ${t.ram_gb}GB RAM`,"success"),t.recommended_profile&&(addConsoleMessage(`Applying recommended profile: ${t.recommended_profile}`,"info"),await applyHardwareProfile(t.recommended_profile))}catch(e){console.error("Error detecting hardware:",e),addConsoleMessage("Failed to detect hardware","error"),document.getElementById("hardware-detection-info").innerHTML='<span class="text-red-400">❌ Failed to detect hardware. Try manual selection.</span>'}}async function confirmApplyProfile(e,t){confirm(`Apply profile "${t.name}"?\n\n${t.description}\n\nThis will update system resource settings and requires a service restart to take full effect.`)&&await applyHardwareProfile(e)}async function applyHardwareProfile(e){try{addConsoleMessage(`Applying hardware profile: ${e}...`,"info");const t=await postAPI("/api/config/apply-profile",{profile_id:e});t.success?(addConsoleMessage(`✅ Profile applied: ${t.profile.name}`,"success"),addConsoleMessage("⚠️ Service restart required for changes to take effect","warning"),displayCurrentProfile({hardware_profile:e,hardware_profile_name:t.profile.name,hardware_profile_applied:t.profile.hardware_profile_applied||(new Date).toISOString()}),confirm("Hardware profile applied successfully!\n\nRestart the Ragnar service now to apply changes?")&&await restartService()):addConsoleMessage("❌ Failed to apply profile","error")}catch(e){console.error("Error applying hardware profile:",e),addConsoleMessage(`Failed to apply hardware profile: ${e.message}`,"error")}}function displayCurrentProfile(e){const t=document.getElementById("current-profile-status"),n=document.getElementById("current-profile-name"),a=document.getElementById("current-profile-applied");if(e.hardware_profile&&e.hardware_profile_name)if(t.classList.remove("hidden"),n.textContent=e.hardware_profile_name,e.hardware_profile_applied){const t=new Date(e.hardware_profile_applied);a.textContent=`Applied: ${t.toLocaleString()}`}else a.textContent="Applied recently";else t.classList.add("hidden")}function updateReleaseGateState(e={}){const t=Boolean(e&&e.enabled),n="string"==typeof(e&&e.message)?e.message.trim():"";releaseGateState={enabled:t,message:n||RELEASE_GATE_DEFAULT_MESSAGE};const a=document.getElementById("update-btn");if(a&&(a.dataset.releaseGate=t?"true":"false",["ring-2","ring-yellow-500/50","ring-offset-2","ring-offset-slate-900"].forEach(e=>{t?a.classList.add(e):a.classList.remove(e)})),!t&&releaseGateResolver){const e=releaseGateResolver;releaseGateResolver=null,releaseGatePendingPromise=null,hideReleaseGateModal(),e(!0)}}function showReleaseGateModal(){const e=document.getElementById("release-gate-modal"),t=document.getElementById("release-gate-modal-message");t&&(t.textContent=releaseGateState.message||RELEASE_GATE_DEFAULT_MESSAGE),e&&(e.classList.remove("hidden"),e.classList.add("flex"))}function hideReleaseGateModal(){const e=document.getElementById("release-gate-modal");e&&(e.classList.add("hidden"),e.classList.remove("flex"))}function ensureReleaseGateAcknowledged(){return releaseGateState.enabled?releaseGatePendingPromise?(showReleaseGateModal(),releaseGatePendingPromise):(releaseGatePendingPromise=new Promise(e=>{releaseGateResolver=e,showReleaseGateModal()}),releaseGatePendingPromise):Promise.resolve(!0)}function handleReleaseGateDecision(e){if(hideReleaseGateModal(),releaseGateResolver){const t=releaseGateResolver;releaseGateResolver=null,releaseGatePendingPromise=null,t(Boolean(e))}}async function checkForUpdates(){try{const e=document.getElementById("update-btn"),t=document.getElementById("update-status");e&&(e.onclick=performUpdate,e.disabled=!0,e.className="w-full bg-gray-600 text-white py-2 px-4 rounded cursor-not-allowed"),updateElement("update-btn-text","Update System"),updateElement("update-status","Checking..."),t&&(t.className="text-sm px-2 py-1 rounded bg-gray-700 text-gray-300"),updateElement("update-info","Checking for updates..."),addConsoleMessage("Checking for system updates...","info");const n=await fetchAPI("/api/system/check-updates"),a=n.git_status||{};console.log("Update check response:",n),addConsoleMessage(`Debug: Repo path: ${n.repo_path}`,"info"),addConsoleMessage(`Debug: Current commit: ${n.current_commit}`,"info"),addConsoleMessage(`Debug: Latest commit: ${n.latest_commit}`,"info"),addConsoleMessage(`Debug: Commits behind: ${n.commits_behind}`,"info");let s="";n.updates_available&&n.commits_behind>0?(s=`${n.commits_behind} commits behind. Latest: ${n.latest_commit||"Unknown"}`,updateElement("update-status","Update Available"),t&&(t.className="text-sm px-2 py-1 rounded bg-orange-700 text-orange-300"),e&&(e.disabled=!1,e.className="w-full bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded transition-colors"),addConsoleMessage(`Update available: ${n.commits_behind} commits behind`,"warning")):(s="System is up to date",updateElement("update-status","Up to Date"),t&&(t.className="text-sm px-2 py-1 rounded bg-green-700 text-green-300"),e&&(e.disabled=!0,e.className="w-full bg-gray-600 text-white py-2 px-4 rounded cursor-not-allowed"),addConsoleMessage("System is up to date","success"));const o=[],r=Array.isArray(a.modified_files)?a.modified_files.length:0;if(a.has_conflicts?o.push("Local merge conflicts detected"):a.is_dirty&&o.push(`${r} local change${1===r?"":"s"}`),a.status_error&&o.push(`git status error: ${a.status_error}`),updateElement("update-info",s),a.has_conflicts)return e&&(e.disabled=!1,e.onclick=resolveGitConflicts,e.className="w-full bg-red-600 hover:bg-red-700 text-white py-2 px-4 rounded transition-colors",updateElement("update-btn-text","Resolve Git Conflicts")),updateElement("update-status","Local Conflict"),t&&(t.className="text-sm px-2 py-1 rounded bg-red-700 text-red-200"),void addConsoleMessage('Local git conflicts detected. Click "Resolve Git Conflicts" to reset and update.',"warning");n.updates_available&&n.commits_behind>0&&e&&(a.is_dirty?(e.onclick=autoStashAndUpdate,e.className="w-full bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded transition-colors",updateElement("update-btn-text","Update System"),addConsoleMessage("Local edits detected. Ragnar will handle them automatically during the update.","info")):(e.onclick=performUpdate,updateElement("update-btn-text","Update System")))}catch(e){if(console.error("Error checking for updates:",e),updateElement("update-status","Error"),document.getElementById("update-status").className="text-sm px-2 py-1 rounded bg-red-700 text-red-300",e.message&&e.message.includes("safe.directory")){updateElement("update-info","Git safe directory issue detected"),addConsoleMessage("Git safe directory error detected. Click the Fix Git button.","error");const e=document.getElementById("update-btn");e.textContent="Fix Git Config",e.disabled=!1,e.className="w-full bg-yellow-600 hover:bg-yellow-700 text-white py-2 px-4 rounded transition-colors",e.onclick=fixGitConfig}else updateElement("update-info","Failed to check for updates"),addConsoleMessage(`Failed to check for updates: ${e.message}`,"error")}}async function fixGitConfig(){try{updateElement("update-btn-text","Fixing...");const e=document.getElementById("update-btn");e.disabled=!0,addConsoleMessage("Fixing git configuration...","info");const t=await postAPI("/api/system/fix-git",{});t.success?(addConsoleMessage("Git configuration fixed successfully","success"),e.textContent="Update System",e.onclick=performUpdate,setTimeout(()=>{checkForUpdates()},1e3)):(addConsoleMessage(`Failed to fix git configuration: ${t.error}`,"error"),e.disabled=!1,updateElement("update-btn-text","Fix Git Config"))}catch(e){console.error("Error fixing git config:",e),addConsoleMessage("Failed to fix git configuration","error");document.getElementById("update-btn").disabled=!1,updateElement("update-btn-text","Fix Git Config")}}async function resolveGitConflicts(){const e=document.getElementById("update-btn");try{e.disabled=!0,updateElement("update-btn-text","Resolving..."),addConsoleMessage("Resolving git conflicts and pulling latest update...","info");const t=await postAPI("/api/system/resolve-conflicts",{});t.success?(addConsoleMessage("Conflicts resolved and update applied. Restarting...","success"),t.warnings&&t.warnings.length&&t.warnings.forEach(e=>addConsoleMessage(e,"warning")),updateElement("update-btn-text","Done")):(addConsoleMessage(`Failed to resolve conflicts: ${t.error}`,"error"),e.disabled=!1,updateElement("update-btn-text","Resolve Git Conflicts"))}catch(t){console.error("Error resolving git conflicts:",t),addConsoleMessage("Failed to resolve git conflicts","error"),e.disabled=!1,updateElement("update-btn-text","Resolve Git Conflicts")}}async function performUpdate(){if(await ensureReleaseGateAcknowledged()){if(confirm("This will update the system and restart the service. Continue?"))try{updateElement("update-btn-text","Update now");const e=document.getElementById("update-btn");e.disabled=!0,e.className="w-full bg-gray-600 text-white py-2 px-4 rounded cursor-not-allowed",addConsoleMessage("Starting system update...","info");const t=await postAPI("/api/system/update",{});t.success?(addConsoleMessage("Update completed successfully","success"),addConsoleMessage("System will restart automatically...","info"),updateElement("update-info","Update completed. System restarting..."),setTimeout(async()=>{await verifyServiceRestart()},1e4)):(addConsoleMessage(`Update failed: ${t.error||"Unknown error"}`,"error"),updateElement("update-btn-text","Update System"),e.disabled=!1,e.className="w-full bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded transition-colors")}catch(e){console.error("Error performing update:",e),addConsoleMessage("Update failed due to network error","error"),updateElement("update-btn-text","Update System");const t=document.getElementById("update-btn");t.disabled=!1,t.className="w-full bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded transition-colors"}}else addConsoleMessage("Update postponed until the release window opens.","info")}async function autoStashAndUpdate(){if(!await ensureReleaseGateAcknowledged())return void addConsoleMessage("Update postponed until the release window opens.","info");if(!confirm("This will update the system and restart the service. Continue?"))return;const e=document.getElementById("update-btn"),t=(t,n)=>{e&&(e.disabled=!!t,e.className=t?"w-full bg-gray-600 text-white py-2 px-4 rounded cursor-wait":"w-full bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded transition-colors",updateElement("update-btn-text",n))};try{t(!0,"Updating..."),addConsoleMessage("Applying update...","info");const n=await postAPI("/api/system/stash-update",{});if(!n.success)throw new Error(n.error||"Update failed");addConsoleMessage("Update completed successfully.","success"),addConsoleMessage("System will restart automatically...","info"),updateElement("update-info","Update applied. System restarting..."),e&&(e.className="w-full bg-gray-600 text-white py-2 px-4 rounded cursor-not-allowed",updateElement("update-btn-text","Updating...")),setTimeout(async()=>{await verifyServiceRestart()},1e4)}catch(e){console.error("Auto update error:",e),addConsoleMessage(`Update failed: ${e.message}`,"error"),t(!1,"Update System"),updateElement("update-info","Update failed. Fix issues and retry.")}}async function verifyServiceRestart(){let e=0;addConsoleMessage("Verifying service is back online...","info"),updateElement("update-info","Verifying service restart...");const t=async()=>{e++;try{const e=await networkAwareFetch("/api/stats",{method:"GET",headers:{"Content-Type":"application/json"},timeout:5e3});if(e.ok){addConsoleMessage("✅ Service verified online after update","success"),updateElement("update-info","Update completed successfully. Service is online.");const e=document.getElementById("update-btn");return updateElement("update-btn-text","Update System"),e.disabled=!1,e.className="w-full bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded transition-colors",void setTimeout(()=>{checkForUpdates()},5e3)}throw new Error(`HTTP ${e.status}`)}catch(n){if(console.log(`Service check attempt ${e}/12 failed:`,n.message),e>=12){addConsoleMessage("⚠️ Service restart verification timeout. Manual check may be needed.","warning"),updateElement("update-info","Update completed, but service verification timed out.");const e=document.getElementById("update-btn");return updateElement("update-btn-text","Update System"),e.disabled=!1,void(e.className="w-full bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded transition-colors")}addConsoleMessage(`Service check ${e}/12 - waiting for restart...`,"info"),setTimeout(t,1e4)}};t()}async function checkForUpdatesQuiet(){try{const e=await fetchAPI("/api/system/check-updates");if(e.updates_available&&e.commits_behind>0){"config"!==currentTab&&addConsoleMessage(`🔄 System update available: ${e.commits_behind} commits behind`,"warning");const t=document.querySelector('[data-tab="config"]');if(t&&!t.querySelector(".update-indicator")){const e=document.createElement("span");e.className="update-indicator absolute -top-1 -right-1 w-3 h-3 bg-orange-500 rounded-full pulse-glow",t.style.position="relative",t.appendChild(e)}}else{const e=document.querySelector('[data-tab="config"]'),t=e?.querySelector(".update-indicator");t&&t.remove()}}catch(e){console.debug("Background update check failed:",e)}}async function restartService(){if(confirm("This will restart the Ragnar service. The web interface may be temporarily unavailable. Continue?"))try{addConsoleMessage("Restarting Ragnar service...","info"),updateElement("service-status","Restarting..."),document.getElementById("service-status").className="text-sm px-2 py-1 rounded bg-yellow-700 text-yellow-300";const e=await postAPI("/api/system/restart-service",{});e.success?(addConsoleMessage("Service restart initiated","success"),addConsoleMessage("Service will be back online shortly...","info"),setTimeout(()=>{updateElement("service-status","Running"),document.getElementById("service-status").className="text-sm px-2 py-1 rounded bg-green-700 text-green-300",addConsoleMessage("Service restart completed","success")},1e4)):(addConsoleMessage(`Service restart failed: ${e.error||"Unknown error"}`,"error"),updateElement("service-status","Error"),document.getElementById("service-status").className="text-sm px-2 py-1 rounded bg-red-700 text-red-300")}catch(e){console.error("Error restarting service:",e),addConsoleMessage("Failed to restart service","error"),updateElement("service-status","Error"),document.getElementById("service-status").className="text-sm px-2 py-1 rounded bg-red-700 text-red-300"}}async function rebootSystem(){if(confirm("This will reboot the entire system. The device will be offline for several minutes. Continue?"))try{addConsoleMessage("Initiating system reboot...","warning");const e=await postAPI("/api/system/reboot",{});e.success?(addConsoleMessage("System reboot initiated","success"),addConsoleMessage("Device will be offline for several minutes...","warning"),updateConnectionStatus(!1)):addConsoleMessage(`Reboot failed: ${e.error||"Unknown error"}`,"error")}catch(e){console.error("Error rebooting system:",e),addConsoleMessage("Failed to initiate system reboot","error")}}async function shutdownSystem(){if(confirm("⚠️ This will SHUT DOWN the system completely.\n\nYou will need physical access to power the device back on.\n\nContinue?"))try{addConsoleMessage("Initiating system shutdown...","warning"),updateElement("service-status","Shutting down..."),document.getElementById("service-status").className="text-sm px-2 py-1 rounded bg-red-700 text-red-300";const e=await postAPI("/api/system/shutdown",{});e.success?(addConsoleMessage("System shutdown initiated","success"),addConsoleMessage("Device will power off shortly...","warning"),updateConnectionStatus(!1)):(addConsoleMessage(`Shutdown failed: ${e.error||"Unknown error"}`,"error"),updateElement("service-status","Running"),document.getElementById("service-status").className="text-sm px-2 py-1 rounded bg-green-700 text-green-300")}catch(e){console.error("Error shutting down system:",e),addConsoleMessage("Failed to initiate system shutdown","error")}}async function resetVulnerabilities(){if(confirm("⚠️ Reset All Vulnerabilities?\n\nThis will permanently delete:\n• All discovered vulnerabilities\n• Vulnerability scan results\n• Network intelligence vulnerability data\n\nThis action cannot be undone. Continue?"))try{addConsoleMessage("Resetting vulnerabilities...","warning");const e=await postAPI("/api/data/reset-vulnerabilities",{});e.success?(addConsoleMessage(`Vulnerabilities reset: ${e.deleted_count||0} entries removed`,"success"),updateElement("vuln-count","0"),updateElement("config-vuln-count","0"),updateElement("vulnerability-count","0"),"network"!==currentTab&&"discovered"!==currentTab&&"threat-intel"!==currentTab||setTimeout(()=>{refreshCurrentTab()},500)):addConsoleMessage(`Reset failed: ${e.error||"Unknown error"}`,"error")}catch(e){console.error("Error resetting vulnerabilities:",e),addConsoleMessage("Failed to reset vulnerabilities","error")}}async function resetThreatIntelligence(){if(confirm("⚠️ Reset Threat Intelligence?\n\nThis will permanently delete:\n• All threat intelligence findings\n• Enriched threat data\n• Threat cache\n\nThis action cannot be undone. Continue?"))try{addConsoleMessage("Resetting threat intelligence...","warning");const e=await postAPI("/api/data/reset-threat-intel",{});e.success?(addConsoleMessage("Threat intelligence data reset successfully","success"),"threat-intel"===currentTab&&setTimeout(()=>{refreshCurrentTab()},500)):addConsoleMessage(`Reset failed: ${e.error||"Unknown error"}`,"error")}catch(e){console.error("Error resetting threat intelligence:",e),addConsoleMessage("Failed to reset threat intelligence","error")}}async function updateVulnerabilityCount(){try{const e=(await fetchAPI("/api/stats")).vulnerability_count||0;updateElement("vuln-count",e.toString()),updateElement("config-vuln-count",e.toString())}catch(e){console.error("Error updating vulnerability count:",e),updateElement("vuln-count","?")}}async function startAPMode(){if(confirm('Start AP Mode?\n\nThis will:\n• Disconnect from current Wi-Fi\n• Start "Ragnar" access point\n• Enable 3-minute smart cycling\n• Allow Wi-Fi configuration via AP\n\nContinue?'))try{addConsoleMessage("Starting AP Mode...","info"),updateWifiStatus("Starting AP Mode...","connecting");const e=await postAPI("/api/wifi/ap/enable",{});e.success?(addConsoleMessage(`AP Mode started: ${e.ap_config.ssid}`,"success"),updateWifiStatus(`AP Mode Active: "${e.ap_config.ssid}" | ${e.ap_config.timeout}s timeout | Smart cycling enabled`,"ap-mode"),setTimeout(refreshWifiStatus,2e3)):(addConsoleMessage(`Failed to start AP Mode: ${e.message}`,"error"),updateWifiStatus(`Failed to start AP Mode: ${e.message}`,"error"))}catch(e){console.error("Error starting AP mode:",e),addConsoleMessage("Error starting AP Mode","error"),updateWifiStatus("Error starting AP Mode","error")}}async function refreshWifiStatus(){try{let e=getActiveWifiInterface();const t=e?`/api/wifi/status?interface=${encodeURIComponent(e)}`:"/api/wifi/status",n=await fetchAPI(t);console.log("Wi-Fi status data received:",n);const a=n.multi_interface||null;wifiMultiInterfaceState=a,a&&Array.isArray(a.interfaces)?setWifiInterfaceMetadata(a.interfaces):Array.isArray(n.interfaces)&&setWifiInterfaceMetadata(n.interfaces);const s=document.getElementById("wifi-status-indicator"),o=document.getElementById("wifi-info"),r=document.getElementById("wifi-connected-list");if(!s||!o)return console.error("Wi-Fi status elements not found in DOM"),void console.log("Looking for elements: wifi-status-indicator and wifi-info");console.log("Wi-Fi status elements found, updating..."),!e&&n.interface&&(e=setSelectedWifiInterface(n.interface,{skipRefresh:!0})),Array.isArray(n.interfaces)&&renderWifiInterfaceSwitch(n.interfaces);const i=n.interface?n.interface:e,l=n.ip_address?` (${n.ip_address})`:"";if(n.ap_mode_active){const e=`AP Mode Active: "${n.ap_ssid||"Ragnar"}" | Connect to configure Wi-Fi`;console.log("Setting AP mode status:",e),updateWifiStatus(e,"ap-mode"),s.textContent="AP Mode",s.className="text-sm px-2 py-1 rounded bg-orange-700 text-orange-300",o.textContent=e}else if(n.wifi_connected){const e=n.current_ssid||"Unknown Network",t=i?`Connected to: ${e} on ${i}${l}`:`Connected to: ${e}`;console.log("Setting connected status:",t),updateWifiStatus(t,"connected"),s.textContent=i?`${i} • Connected`:"Connected",s.className="text-sm px-2 py-1 rounded bg-green-700 text-green-300",o.textContent=t}else{console.log("Setting disconnected status");const e=i?`Wi-Fi disconnected on ${i}`:"Wi-Fi disconnected";updateWifiStatus(e,"disconnected"),s.textContent=i?`${i} • Disconnected`:"Disconnected",s.className="text-sm px-2 py-1 rounded bg-red-700 text-red-300",o.textContent=e}if(r){const e=Array.isArray(n.interfaces)?n.interfaces:[],t=e.filter(e=>e&&e.connected),a=e.filter(e=>e&&!e.connected),s=(e,t=!1)=>{const n=e&&(e.connected_ssid||e.connection)?escapeHtml(e.connected_ssid||e.connection):"No SSID",a=e&&e.ip_address?`<div class="text-[11px] text-gray-400">${escapeHtml(e.ip_address)}</div>`:"",s=t?"text-green-400":"text-gray-500",o=t?"Online":"Offline",r=t?`SSID: ${n}`:escapeHtml(e?.state||"Unavailable");return`\n <div class="flex items-center justify-between gap-3">\n <div>\n <div class="text-sm font-semibold text-white">${escapeHtml(e?.name||"Unknown")}</div>\n <div class="text-xs text-gray-400">${r}</div>\n </div>\n <div class="text-right">\n <div class="text-xs ${s}">${o}</div>\n ${a}\n </div>\n </div>\n `};let o='<div class="text-[11px] uppercase tracking-wide text-gray-400">Connected Adapters</div>';t.length>0?o+=t.map(e=>s(e,!0)).join(""):o+='<div class="text-xs text-gray-500">No active Wi-Fi connections</div>',a.length>0&&(o+='<div class="text-[11px] uppercase tracking-wide text-gray-400 mt-3 border-t border-slate-700 pt-2">Other Adapters</div>',o+=a.map(e=>s(e,!1)).join("")),r.innerHTML=o,r.classList.remove("hidden")}renderDashboardMultiInterfaceSummary(a,n),renderConnectTabMultiInterface(a),console.log("Wi-Fi status updated successfully")}catch(e){console.error("Error refreshing Wi-Fi status:",e),updateWifiStatus("Error checking Wi-Fi status","error");const t=document.getElementById("wifi-status-indicator"),n=document.getElementById("wifi-info");t&&(t.textContent="Error",t.className="text-sm px-2 py-1 rounded bg-red-700 text-red-300"),n&&(n.textContent="Error checking Wi-Fi status");const a=document.getElementById("wifi-connected-list");a&&(a.innerHTML='<div class="text-xs text-red-300">Unable to load adapter list</div>',a.classList.remove("hidden")),wifiMultiInterfaceState=null,setWifiInterfaceMetadata([]),renderDashboardMultiInterfaceSummary(null),renderConnectTabMultiInterface(null)}}async function refreshEthernetStatus(){const e=document.getElementById("lan-status-indicator"),t=document.getElementById("lan-info"),n=document.getElementById("lan-interface-list");try{const a=await fetchAPI("/api/ethernet/status");if(!e||!t)return;if(a.active&&a.active_interface){const n=a.active_interface;e.className="text-sm px-2 py-1 rounded bg-green-900/50 text-green-400",e.textContent="Connected",t.innerHTML=`<span class="text-green-400 font-medium">${escapeHtml(n.name)}</span> — ${escapeHtml(n.ip_address||"No IP")}`,n.network_cidr&&(t.innerHTML+=`<br><span class="text-gray-500 text-xs">Network: ${escapeHtml(n.network_cidr)}</span>`)}else a.available?(e.className="text-sm px-2 py-1 rounded bg-yellow-900/50 text-yellow-400",e.textContent="No Link",t.textContent="Ethernet interface found but no active connection"):(e.className="text-sm px-2 py-1 rounded bg-gray-700 text-gray-400",e.textContent="Not Found",t.textContent="No Ethernet interfaces detected");n&&a.interfaces&&a.interfaces.length>0&&(n.innerHTML=a.interfaces.map(e=>{const t=e.connected?"text-green-400":e.has_carrier?"text-yellow-400":"text-gray-500",n=e.connected?"Connected":e.has_carrier?"Link Up":"No Link",a=e.ip_address||"—";return`<div class="flex justify-between"><span>${e.name}</span><span>${a}</span><span class="${t}">${n}</span></div>`}).join(""),n.classList.remove("hidden"))}catch(n){e&&(e.className="text-sm px-2 py-1 rounded bg-red-900/50 text-red-400",e.textContent="Error"),t&&(t.textContent="Failed to check Ethernet status")}}function updateWifiStatus(e,t=""){addConsoleMessage(e,"error"===t?"error":"ap-mode"===t?"warning":"info")}let currentWifiNetworks=[],selectedWifiNetwork=null;const WIFI_INTERFACE_STORAGE_KEY="wifi-selected-interface";let selectedWifiInterface=null,wifiInterfaceMetadata=[];const WIFI_NETWORK_CACHE_KEY_DEFAULT="__default__",wifiNetworkResultCache=new Map;let wifiMultiInterfaceState=null;const DASHBOARD_STATS_CACHE_TTL=4e3;let dashboardStatsCache={key:null,timestamp:0,data:null},dashboardStatsRequestState=null;function slugifyNetworkIdentifier(e){if(!e)return null;let t=e;return"function"==typeof t.normalize&&(t=t.normalize("NFKD")),t=t.replace(/[\u0300-\u036f]/g,""),t=t.replace(/[^A-Za-z0-9]+/g,"_"),t=t.replace(/^_+|_+$/g,""),t=t.toLowerCase(),t||null}function normalizeInterfaceMetadata(e){return Array.isArray(e)?e.map(e=>e&&"object"==typeof e?e.network_slug?e:e.connected_ssid?{...e,network_slug:slugifyNetworkIdentifier(e.connected_ssid)}:e:e):[]}function setWifiInterfaceMetadata(e){const t=wifiInterfaceMetadata||[];let n=null;if(selectedWifiInterface){const e=Array.isArray(t)?t.find(e=>e&&e.name===selectedWifiInterface):null;e&&(n=e.network_slug||(e.connected_ssid?slugifyNetworkIdentifier(e.connected_ssid):null))}if(wifiInterfaceMetadata=e?normalizeInterfaceMetadata(e):[],selectedWifiInterface){const e=wifiInterfaceMetadata.find(e=>e&&e.name===selectedWifiInterface);n!==(e?e.network_slug||(e.connected_ssid?slugifyNetworkIdentifier(e.connected_ssid):null):null)&&(clearDashboardStatsCache(),refreshDashboardStatsForCurrentSelection({forceRefresh:!0}).catch(e=>{console.debug("Dashboard stats refresh failed after interface metadata update",e)}))}}function clearDashboardStatsCache(){dashboardStatsCache={key:null,timestamp:0,data:null}}async function fetchDashboardStatsForSelection(e={}){const{forceRefresh:t=!1}=e,{network:n}=getSelectedDashboardNetworkKey(),a=n?`network:${n}`:"network:global",s=Date.now(),o=dashboardStatsCache;if(!t&&o.key===a&&s-o.timestamp<4e3&&o.data)return o.data;if(!t&&dashboardStatsRequestState&&dashboardStatsRequestState.key===a)return dashboardStatsRequestState.promise;const r=n?`/api/dashboard/stats?network=${encodeURIComponent(n)}`:"/api/dashboard/stats",i=(async()=>{try{const e=await fetchAPI(r);return dashboardStatsCache={key:a,timestamp:Date.now(),data:e},e}finally{dashboardStatsRequestState&&dashboardStatsRequestState.key===a&&(dashboardStatsRequestState=null)}})();return dashboardStatsRequestState={key:a,promise:i},i}async function refreshDashboardStatsForCurrentSelection(e={}){const{forceRefresh:t=!1,fallbackData:n=null}=e;try{const e=await fetchDashboardStatsForSelection({forceRefresh:t});return e?updateDashboardStats(e):n&&updateDashboardStats(n),e}catch(e){throw console.warn("Unable to refresh dashboard stats for selection",e),n&&updateDashboardStats(n),e}}function formatInterfaceRole(e){return e?"internal"===e?"Internal":"external"===e?"External":e.replace(/_/g," ").replace(/\b\w/g,e=>e.toUpperCase()):"Adapter"}function formatInterfaceReason(e){if(!e)return"";return{global_disabled:"Multi-scan disabled",user_disabled:"Paused by user",no_ssid:"No SSID",disconnected:"Adapter offline"}[e]||e.replace(/_/g," ")}function renderDashboardMultiInterfaceSummary(e,t={}){const n=document.getElementById("wifi-dashboard-interfaces");if(!n)return;if(!e||!Array.isArray(e.interfaces)||0===e.interfaces.length)return n.innerHTML='<div class="text-gray-500 text-[11px]">No additional adapters detected yet.</div>',void renderDashboardInterfaceSwitch(null);const a=getActiveWifiInterface();let s=null;a&&(s=e.interfaces.find(e=>e.name===a)),s||(s=e.interfaces.find(e=>e.connected&&e.connected_ssid)||e.interfaces[0]);let o="";if(s){const e="external"===s.role?"bg-indigo-900 text-indigo-100":"bg-slate-800 text-slate-100",t=Boolean(s.scan_enabled&&s.connected&&s.connected_ssid),n=t?"Scanning":"Paused",a=t?"text-green-300":"text-gray-400",r=s.reason?` • ${formatInterfaceReason(s.reason)}`:"",i=s.connected_ssid?escapeHtml(s.connected_ssid):"No SSID";o=`\n <div class="flex items-center justify-between gap-2">\n <div class="flex items-center gap-2 text-gray-100">\n <span class="font-semibold text-xs">${escapeHtml(s.name||"iface")}</span>\n <span class="text-[10px] px-2 py-0.5 rounded ${e}">${formatInterfaceRole(s.role)}</span>\n </div>\n <div class="text-right leading-tight text-[11px] ${a}">\n <div>${i}</div>\n <div class="text-[10px] text-gray-400">${n}${r}</div>\n </div>\n </div>\n `}n.innerHTML=o;const r=!!s&&s.connected,i=s?s.connected_ssid:t&&t.current_ssid,l=Boolean(t&&t.ap_mode_active);updateConnectivityIndicator("wifi-status",r||Boolean(t&&t.wifi_connected),i,l),renderDashboardInterfaceSwitch(e)}function renderConnectTabMultiInterface(e){const t=document.getElementById("wifi-multi-status-summary"),n=document.getElementById("wifi-multi-interface-list"),a=document.getElementById("wifi-multi-global-pill"),s=document.getElementById("wifi-multi-limit-note");if(!t||!n||!a)return;if(!e)return a.textContent="Unavailable",a.className="text-xs px-2 py-1 rounded bg-red-700 text-red-200",t.textContent="Multi-interface controller unavailable. Refresh Wi-Fi status to retry.",void(n.innerHTML='<div class="text-xs text-red-300 bg-red-900/30 border border-red-800 rounded-lg p-3">Unable to load adapter details.</div>');const o=Array.isArray(e.interfaces)?e.interfaces:[],r=(e.scan_mode||"single").toLowerCase(),i=e.focus_interface||"",l=e.focus_interface_ssid||"",c="multi"===r;if(a.textContent=c?"Single focus":"All adapters",a.className="text-xs px-2 py-1 rounded "+(c?"bg-green-700 text-green-100":"bg-amber-700 text-amber-100"),s){const t=e.max_interfaces||1;s.textContent=c?`Monitoring up to ${t} adapter${1===t?"":"s"} simultaneously.`:"Automation locks onto the focused adapter to prevent context drift."}if(0===o.length)return t.textContent="Waiting for eligible adapters...",n.innerHTML='<div class="text-xs text-gray-400 bg-slate-900/60 border border-dashed border-slate-700 rounded-lg p-3">Connect adapters to begin multi-interface scanning.</div>',void updateMultiInterfaceModeControls(e);const d=o.filter(e=>e.scan_enabled&&e.connected&&e.connected_ssid).length;if("single"===r){const e=i?escapeHtml(i):"Select an adapter to focus on",n=l?`<span class="text-emerald-300 ml-2">${escapeHtml(l)}</span>`:"";t.innerHTML=`Single-adapter focus: <span class="text-white font-semibold">${e}</span>${n}`}else t.textContent=d>0?`Actively scanning ${d} adapter${1===d?"":"s"}.`:"All adapters are currently paused.";n.innerHTML=o.map(e=>{const t=Boolean(e.scan_enabled&&e.connected&&e.connected_ssid),n=t?"Pause Scans":"Resume Scans",a=t?"text-xs px-3 py-1 rounded bg-yellow-600 hover:bg-yellow-500 text-white transition-colors":"text-xs px-3 py-1 rounded bg-green-600 hover:bg-green-500 text-white transition-colors",s=e.connected?e.connected_ssid?`SSID: ${escapeHtml(e.connected_ssid)}`:"Connected":escapeHtml(e.state||"Disconnected"),o=t?"Scanning":e.reason?`Paused • ${formatInterfaceReason(e.reason)}`:"Paused",r=t?"text-green-300":"text-gray-400",i=e.ip_address?` • ${escapeHtml(e.ip_address)}`:"",l="external"===e.role?"bg-indigo-900 text-indigo-200":"bg-slate-800 text-slate-200",c=e.focus_selected?'<span class="text-[10px] px-2 py-0.5 rounded bg-amber-700/60 text-amber-200">Focus</span>':"";return`\n <div class="border border-slate-700 rounded-lg p-3 bg-slate-900/50 flex flex-col gap-3">\n <div class="flex items-center justify-between gap-3">\n <div>\n <div class="flex items-center gap-2 text-sm font-semibold text-white">\n <span>${escapeHtml(e.name||"iface")}</span>\n <span class="text-[10px] px-2 py-0.5 rounded ${l}">${formatInterfaceRole(e.role)}</span>\n ${c}\n </div>\n <div class="text-[11px] text-gray-400">${s}</div>\n </div>\n <button type="button" class="${a}" data-interface="${escapeHtml(e.name||"")}" data-scan-state="${t?"enabled":"disabled"}">${n}</button>\n </div>\n <div class="text-[11px] ${r}">${o}${i}</div>\n </div>\n `}).join(""),n.querySelectorAll("button[data-interface]").forEach(e=>{e.addEventListener("click",handleScanControlToggle)}),updateMultiInterfaceModeControls(e)}function updateMultiInterfaceModeControls(e){const t=document.getElementById("wifi-multi-mode-buttons"),n=document.getElementById("wifi-multi-focus-controls"),a=document.getElementById("wifi-multi-focus-select");if(!t||!n||!a)return;const s=e&&e.scan_mode?e.scan_mode.toLowerCase():"single",o=e&&e.focus_interface?e.focus_interface:"",r=e&&Array.isArray(e.interfaces)?e.interfaces:[];t.querySelectorAll("button[data-mode]").forEach(e=>{const t=e.dataset.mode;if(!t)return;const n=t===s;e.classList.toggle("bg-Ragnar-500",n),e.classList.toggle("border-Ragnar-400",n),e.classList.toggle("text-white",n),e.classList.toggle("shadow-md",n),e.classList.toggle("bg-slate-800",!n),e.classList.toggle("border-slate-600",!n),e.classList.toggle("text-gray-300",!n),e.onclick=async()=>{"true"!==e.dataset.busy&&t!==s&&await setMultiInterfaceMode(t,e)}});const i="single"===s;n.classList.toggle("hidden",!i);const l=r.filter(e=>e&&e.name).map(e=>({name:e.name,label:`${e.name}${e.connected_ssid?` • ${e.connected_ssid}`:""}`}));if(i){const e=l.length?['<option value="">Select adapter</option>',...l.map(e=>{const t=e.name===o?" selected":"";return`<option value="${escapeHtml(e.name)}"${t}>${escapeHtml(e.label)}</option>`})]:['<option value="">No adapters detected</option>'];a.innerHTML=e.join(""),a.disabled=0===l.length,a.onchange=async()=>{const e=a.value||"";await setMultiInterfaceFocus(e)}}else a.onchange=null}async function setMultiInterfaceMode(e,t){if(e){t&&(t.dataset.busy="true",t.classList.add("opacity-60","cursor-not-allowed"));try{await postAPI("/api/wifi/scan-control/mode",{mode:e}),addConsoleMessage("multi"===e?"Single focus mode enabled":"All adapters mode enabled","info"),await refreshWifiStatus()}catch(e){console.error("Unable to update scan mode:",e),addConsoleMessage("Scan mode update failed","error")}finally{t&&(t.classList.remove("opacity-60","cursor-not-allowed"),delete t.dataset.busy)}}}async function setMultiInterfaceFocus(e){try{await postAPI("/api/wifi/scan-control/mode",{focus_interface:e||""}),addConsoleMessage(e?`Focused on ${e}`:"Cleared scan focus","info"),await refreshWifiStatus()}catch(e){console.error("Unable to update focused adapter:",e),addConsoleMessage("Focus adapter update failed","error")}}function handleScanControlToggle(e){const t=e.currentTarget;if(!t||"true"===t.dataset.busy)return;updateInterfaceScanState(t.dataset.interface,!("enabled"===t.dataset.scanState),t)}async function updateInterfaceScanState(e,t,n){if(!e)return;const a=t?"/api/wifi/scan-control/start":"/api/wifi/scan-control/stop";n&&(n.dataset.busy="true",n.disabled=!0,n.classList.add("opacity-60","cursor-not-allowed"),n.textContent=t?"Resuming…":"Pausing…");let s=!1;try{const n=await postAPI(a,{interface:e});if(!n||!1===n.success)throw new Error(n&&(n.error||n.message)||"Request failed");addConsoleMessage(`${t?"Resumed":"Paused"} scans on ${e}`,"info"),s=!0}catch(t){console.error("Error updating scan interface:",t),addConsoleMessage(`Scan control failed on ${e}: ${t.message}`,"error")}finally{n&&(n.disabled=!1,n.classList.remove("opacity-60","cursor-not-allowed"),delete n.dataset.busy)}if(s)try{await refreshWifiStatus()}catch(e){console.warn("Wi-Fi status refresh failed after scan control change:",e)}}function cacheWifiNetworkResult(e,t){if(!t)return;const n=e||"__default__";wifiNetworkResultCache.set(n,{payload:t,timestamp:Date.now()})}function getCachedWifiNetworkResult(e){const t=e||"__default__",n=wifiNetworkResultCache.get(t);return n?n.payload:null}function hasCachedWifiNetworks(e){return Boolean(getCachedWifiNetworkResult(e))}function displayCachedWifiNetworks(e){const t=getCachedWifiNetworkResult(e);return!!t&&(displayWifiNetworks(t,{forceInterface:e,skipInterfaceCheck:!0,fromCache:!0}),!0)}function handleWifiInterfaceChange(e){setSelectedWifiInterface((e?.target?.value||"").trim()||null)}function getActiveWifiInterface(){if(selectedWifiInterface)return selectedWifiInterface;const e=localStorage.getItem(WIFI_INTERFACE_STORAGE_KEY);if(e)return selectedWifiInterface=e,selectedWifiInterface;const t=document.getElementById("wifi-interface-select");return t&&t.value?(selectedWifiInterface=t.value,selectedWifiInterface):null}function getNetworkSlugForInterface(e){if(!e)return null;const t=Array.isArray(wifiInterfaceMetadata)?wifiInterfaceMetadata.find(t=>t&&t.name===e):null;return t?t.network_slug?t.network_slug:t.connected_ssid?slugifyNetworkIdentifier(t.connected_ssid):null:null}function getSelectedDashboardNetworkKey(){const e=getActiveWifiInterface(),t=getNetworkSlugForInterface(e);return t?{interface:e,network:t}:{interface:e,network:null}}function updateWifiInterfaceSwitchActiveState(e){const t=document.getElementById("wifi-interface-switch-buttons");if(!t)return;t.querySelectorAll("button[data-interface]").forEach(t=>{const n=t.dataset.interface===e;t.classList.remove("bg-Ragnar-600","border-Ragnar-400","text-white","shadow-lg","bg-slate-800","border-slate-600","text-gray-300"),n?t.classList.add("bg-Ragnar-600","border-Ragnar-400","text-white","shadow-lg"):t.classList.add("bg-slate-800","border-slate-600","text-gray-300")})}function updateDashboardInterfaceSwitchActiveState(e){const t=document.getElementById("wifi-dashboard-interface-buttons");t&&t.querySelectorAll("button[data-interface]").forEach(t=>{const n=t.dataset.interface===e;t.classList.remove("bg-Ragnar-500","text-white","border-Ragnar-400","shadow"),t.classList.remove("bg-slate-800","text-gray-300","border-slate-700"),n?t.classList.add("bg-Ragnar-500","text-white","border-Ragnar-400","shadow"):t.classList.add("bg-slate-800","text-gray-300","border-slate-700")})}function renderDashboardInterfaceSwitch(e){const t=document.getElementById("wifi-dashboard-interface-switch"),n=document.getElementById("wifi-dashboard-interface-buttons");if(!t||!n)return;const a=(e&&Array.isArray(e.interfaces)?e.interfaces.filter(e=>e&&e.name):[]).filter(e=>e.connected&&e.connected_ssid);if(n.innerHTML="",a.length<=1)return void t.classList.add("hidden");t.classList.remove("hidden");const s=getActiveWifiInterface();a.forEach(e=>{const t=document.createElement("button");t.type="button",t.dataset.interface=e.name,t.className="px-2 py-1 rounded-full border text-[11px] transition-colors flex items-center gap-1",t.innerHTML=`\n <span class="font-semibold">${escapeHtml(e.name)}</span>\n <span class="text-emerald-300">${escapeHtml(e.connected_ssid||"")}</span>\n `,t.addEventListener("click",()=>{e.name!==getActiveWifiInterface()&&setSelectedWifiInterface(e.name)}),n.appendChild(t)}),updateDashboardInterfaceSwitchActiveState(s)}function renderWifiInterfaceSwitch(e=[]){setWifiInterfaceMetadata(e);const t=document.getElementById("wifi-interface-switch"),n=document.getElementById("wifi-interface-switch-buttons");if(!t||!n)return;const a=wifiInterfaceMetadata.filter(e=>e&&e.connected),s=a.length>=2;if(n.innerHTML="",t.classList.toggle("hidden",!s),!s)return;const o=getActiveWifiInterface()||(a[0]?a[0].name:null);a.forEach(e=>{const t=document.createElement("button");t.type="button",t.dataset.interface=e.name,t.className="wifi-switch-btn flex-1 min-w-[180px] px-3 py-2 rounded-lg border transition-all text-left";const a=e.connected_ssid||e.connection||"No SSID",s=e.ip_address||"No IP",o=e.state||"UNKNOWN";t.innerHTML=`\n <div class="flex items-center justify-between gap-3">\n <div>\n <div class="text-sm font-semibold">${e.name}</div>\n <div class="text-[11px] text-gray-300">${a}</div>\n </div>\n <div class="text-right text-[11px] leading-tight text-gray-400">\n <div>${s}</div>\n <div>${o}</div>\n </div>\n </div>\n `,t.addEventListener("click",()=>{e.name!==getActiveWifiInterface()&&setSelectedWifiInterface(e.name)}),n.appendChild(t)}),updateWifiInterfaceSwitchActiveState(o)}async function refreshWifiNetworksForInterface(e){if(!e)return;if(document.getElementById("wifi-networks-list")){displayCachedWifiNetworks(e);try{const t=`/api/wifi/networks?interface=${encodeURIComponent(e)}`,n=await fetchAPI(t);n&&displayWifiNetworks(n,{forceInterface:e,skipInterfaceCheck:!0})}catch(t){console.warn("Unable to refresh Wi-Fi networks for interface",e,t)}}}function setSelectedWifiInterface(e,t={}){const n=(e||"").trim()||null,a=n!==selectedWifiInterface;selectedWifiInterface=n,selectedWifiInterface?localStorage.setItem(WIFI_INTERFACE_STORAGE_KEY,selectedWifiInterface):localStorage.removeItem(WIFI_INTERFACE_STORAGE_KEY);const s=document.getElementById("wifi-interface-select");if(s&&s.value!==(selectedWifiInterface||"")&&(s.value=selectedWifiInterface||""),updateWifiInterfaceSwitchActiveState(selectedWifiInterface),updateDashboardInterfaceSwitchActiveState(selectedWifiInterface),a&&(clearDashboardStatsCache(),refreshDashboardStatsForCurrentSelection({forceRefresh:!0}).catch(e=>{console.debug("Dashboard stats refresh failed after interface change",e)})),!t.skipRefresh&&a){if(!displayCachedWifiNetworks(selectedWifiInterface)){const e=document.getElementById("wifi-networks-list");e&&(e.innerHTML=`\n <div class="text-center text-gray-400 py-8">\n <p>No cached Wi-Fi data for <span class="font-semibold text-gray-100">${escapeHtml(selectedWifiInterface||"default")}</span>.</p>\n <p class="text-sm mt-2">Fetching fresh scan results...</p>\n </div>\n `)}refreshWifiStatus().catch(e=>console.warn("Wi-Fi status refresh failed for interface change",e)),refreshWifiNetworksForInterface(selectedWifiInterface).catch(e=>console.warn("Wi-Fi networks refresh failed for interface change",e))}return selectedWifiInterface}async function loadWifiInterfaces(){try{const e=await fetchAPI("/api/wifi/interfaces"),t=document.getElementById("wifi-interface-select");if(!t)return;const n=localStorage.getItem(WIFI_INTERFACE_STORAGE_KEY);let a=!1,s=null,o=null;if(e&&Array.isArray(e.interfaces)&&e.interfaces.length>0){if(setWifiInterfaceMetadata(e.interfaces),t.innerHTML="",e.interfaces.forEach(e=>{const r=document.createElement("option");r.value=e.name,r.textContent=`${e.name}${e.is_default?" (default)":""} - ${e.state}`,!s&&e.connected&&(s=e.name),e.is_default&&(o=e.name),!a&&n&&e.name===n&&(r.selected=!0,a=!0),t.appendChild(r)}),!a&&s){const e=Array.from(t.options).find(e=>e.value===s);e&&(e.selected=!0,a=!0)}if(!a&&o){const e=Array.from(t.options).find(e=>e.value===o);e&&(e.selected=!0,a=!0)}console.log("Loaded Wi-Fi interfaces:",e.interfaces)}else t.innerHTML='<option value="wlan0">wlan0 (default)</option>',a=!0;!a&&t.options.length>0&&(t.options[0].selected=!0),setSelectedWifiInterface(t.value||null,{skipRefresh:!0}),t.removeEventListener("change",handleWifiInterfaceChange),t.addEventListener("change",handleWifiInterfaceChange),renderWifiInterfaceSwitch(e&&e.interfaces||[])}catch(e){console.error("Error loading Wi-Fi interfaces:",e);const t=document.getElementById("wifi-interface-select");t&&(t.innerHTML='<option value="wlan0">wlan0 (default)</option>'),setWifiInterfaceMetadata([]),renderWifiInterfaceSwitch([])}}async function scanWifiNetworks(){const e=document.getElementById("scan-wifi-btn"),t=document.getElementById("wifi-networks-list");if(!t)return;const n=getActiveWifiInterface(),a=e=>!!e&&(!!(Array.isArray(e.available)&&e.available.length>0)||!!(Array.isArray(e.networks)&&e.networks.length>0)),s=e=>(e&&n&&!e.interface&&(e.interface=n),e);let o=!1;try{e&&(e.disabled=!0,e.innerHTML='\n <svg class="w-4 h-4 inline mr-1 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>\n </svg>\n Scanning...\n '),t.innerHTML='\n <div class="text-center text-gray-400 py-8">\n <svg class="w-8 h-8 inline animate-spin mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>\n </svg>\n <p>Scanning for Wi-Fi networks...</p>\n </div>\n ';const r=n?{interface:n}:{};let i=null;try{i=s(await postAPI("/api/wifi/scan",r))}catch(e){throw e}i&&(a(i)||i.warning||i.error)&&(displayWifiNetworks(i,{forceInterface:n,skipInterfaceCheck:!0}),o=!0),await new Promise(e=>setTimeout(e,3e3));const l=n?`/api/wifi/networks?interface=${encodeURIComponent(n)}`:"/api/wifi/networks",c=s(await fetchAPI(l));console.log("Wi-Fi networks data:",c),o&&!a(c)||(displayWifiNetworks(c,{forceInterface:n,skipInterfaceCheck:!0}),o=!0)}catch(e){console.error("Error scanning Wi-Fi networks:",e),t.innerHTML=`\n <div class="text-center text-red-400 py-8">\n <p>Error scanning for networks</p>\n <p class="text-sm mt-2">${e.message}</p>\n </div>\n `}finally{e&&(e.disabled=!1,e.innerHTML='\n <svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>\n </svg>\n Scan Networks\n ')}}function displayWifiNetworks(e,t={}){const n=document.getElementById("wifi-networks-list");if(!n)return;let a=[],s=[];const o=t.forceInterface||getActiveWifiInterface(),r=e.interface||null,i=r||o||null;if(cacheWifiNetworkResult(i||null,e),!t.skipInterfaceCheck&&r&&o&&r!==o){console.info(`Ignoring Wi-Fi scan results for ${r} because ${o} is selected`);const t=getCachedWifiNetworkResult(o);return t&&t!==e?void displayWifiNetworks(t,{forceInterface:o,skipInterfaceCheck:!0}):void(n.innerHTML=`\n <div class="text-center text-gray-400 py-8">\n <p>Scan results for <span class="font-semibold text-gray-100">${escapeHtml(r)}</span> are ready.</p>\n <p class="text-sm mt-2">Switch to that interface or run a new scan for <span class="font-semibold text-gray-100">${escapeHtml(o)}</span>.</p>\n </div>\n `)}const l=t.forceInterface||r||o,c=e.warning?`\n <div class="text-xs text-yellow-300 bg-yellow-900/40 border border-yellow-800 rounded px-3 py-2 mb-3">\n ${escapeHtml(e.warning)}\n </div>\n `:"",d=l?`\n <div class="text-xs text-gray-400 mb-3">\n Showing results for interface <span class="font-semibold text-gray-100">${escapeHtml(l)}</span>\n </div>\n `:"";l?n.dataset.interface=l:delete n.dataset.interface,e.available?a=e.available:e.networks&&(a=e.networks),e.known&&(s=e.known.map(e=>e.ssid||e)),console.log("Displaying networks:",a),console.log("Known networks:",s),a&&0!==a.length?(a.sort((e,t)=>(t.signal||0)-(e.signal||0)),currentWifiNetworks=a,n.innerHTML=d+c+a.map(e=>{const t=e.ssid||e.SSID||"Unknown Network",n=e.signal||0,a="open"!==e.security&&"Open"!==e.security,o=e.known||e.has_system_profile||s.includes(t),r=e.in_use||!1;let i="";i=n>=70?'<svg class="w-5 h-5 text-green-400" fill="currentColor" viewBox="0 0 20 20">\n <path d="M2 11a1 1 0 011-1h2a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1v-5zM8 7a1 1 0 011-1h2a1 1 0 011 1v9a1 1 0 01-1 1H9a1 1 0 01-1-1V7zM14 4a1 1 0 011-1h2a1 1 0 011 1v12a1 1 0 01-1 1h-2a1 1 0 01-1-1V4z"></path>\n </svg>':n>=50?'<svg class="w-5 h-5 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">\n <path d="M2 11a1 1 0 011-1h2a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1v-5zM8 7a1 1 0 011-1h2a1 1 0 011 1v9a1 1 0 01-1 1H9a1 1 0 01-1-1V7z"></path>\n </svg>':'<svg class="w-5 h-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">\n <path d="M2 11a1 1 0 011-1h2a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1v-5z"></path>\n </svg>';const l=a?'\n <svg class="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">\n <path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"></path>\n </svg>\n ':"";let c="";r?c='<span class="text-xs px-2 py-1 rounded bg-green-600 text-white ml-2">Connected</span>':o&&(c='<span class="text-xs px-2 py-1 rounded bg-blue-600 text-white ml-2">Saved</span>');const d=o?`\n <button onclick="event.stopPropagation(); openWifiConnectModal('${t.replace(/'/g,"\\'")}', ${o}, ${a}, true)"\n class="p-1 text-gray-400 hover:text-blue-400 transition-colors" title="Update password">\n <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path>\n </svg>\n </button>\n `:"",u=o&&!r?`\n <button onclick="event.stopPropagation(); forgetWifiNetwork('${t.replace(/'/g,"\\\\'")}')"\n class="p-1 text-gray-400 hover:text-red-400 transition-colors" title="Forget network">\n <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>\n </svg>\n </button>\n `:"";return`\n <div class="bg-slate-800 rounded-lg p-3 hover:bg-slate-700 transition-colors cursor-pointer"\n onclick="openWifiConnectModal('${t.replace(/'/g,"\\'")}', ${o}, ${a})">\n <div class="flex items-center justify-between">\n <div class="flex items-center space-x-3 flex-1">\n ${i}\n <div class="flex-1">\n <div class="flex items-center">\n <span class="font-medium">${t}</span>\n ${c}\n </div>\n <div class="text-xs text-gray-400 mt-1">\n ${a?"Secured":"Open"} • Signal: ${n}%\n </div>\n </div>\n </div>\n <div class="flex items-center space-x-2">\n ${u}\n ${d}\n ${l}\n <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>\n </svg>\n </div>\n </div>\n </div>\n `}).join("")):n.innerHTML=`\n ${d}\n ${c}\n <div class="text-center text-gray-400 py-8">\n <p>No Wi-Fi networks found${l?` on <span class="font-semibold text-gray-100">${escapeHtml(l)}</span>`:""}</p>\n <p class="text-sm mt-2">Ensure the adapter is active and try scanning again. You can also switch interfaces above.</p>\n </div>\n `}function openWifiConnectModal(e,t,n,a){const s=document.getElementById("wifi-connect-modal"),o=document.getElementById("wifi-connect-ssid"),r=document.getElementById("wifi-password-section"),i=document.getElementById("wifi-connect-password"),l=document.getElementById("wifi-connect-status"),c=document.getElementById("wifi-connect-submit-btn");s&&o&&(selectedWifiNetwork={ssid:e,isKnown:t,isSecure:n,editMode:!!a},o.value=e,i&&(i.value="",i.placeholder=a?"Enter new password":"Enter Wi-Fi password"),r&&(r.style.display=n?t&&!a?"none":"block":"none"),c&&(c.textContent=a?"Update & Connect":"Connect"),l&&l.classList.add("hidden"),s.classList.remove("hidden"),s.classList.add("flex"))}function closeWifiConnectModal(){const e=document.getElementById("wifi-connect-modal");e&&(e.classList.add("hidden"),e.classList.remove("flex")),selectedWifiNetwork=null}async function forgetWifiNetwork(e){if(confirm(`Forget network "${e}"? You will need to re-enter the password to connect again.`))try{const t=await postAPI("/api/wifi/forget",{ssid:e});t.success?(addConsoleMessage(`Forgot Wi-Fi network: ${e}`,"success"),scanWifiNetworks()):addConsoleMessage(`Failed to forget network: ${t.message||"Unknown error"}`,"error")}catch(e){console.error("Error forgetting network:",e),addConsoleMessage(`Error forgetting network: ${e.message}`,"error")}}function togglePasswordVisibility(){const e=document.getElementById("wifi-connect-password"),t=document.getElementById("password-eye-icon");e&&("password"===e.type?(e.type="text",t&&(t.innerHTML='\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"></path>\n ')):(e.type="password",t&&(t.innerHTML='\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>\n ')))}async function connectToWifiNetwork(){if(!selectedWifiNetwork)return;const e=document.getElementById("wifi-connect-password"),t=document.getElementById("wifi-password-section"),n=document.getElementById("wifi-save-network"),a=document.getElementById("wifi-connect-status"),s=document.getElementById("wifi-connect-submit-btn"),o=selectedWifiNetwork.ssid,r=selectedWifiNetwork.isKnown,i=selectedWifiNetwork.isSecure,l=selectedWifiNetwork.editMode,c=i&&(!r||l),d=c?e?e.value:"":null,u=!n||n.checked;if(!c||d)try{s&&(s.disabled=!0,s.innerHTML='\n <svg class="w-4 h-4 inline mr-2 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>\n </svg>\n Connecting...\n '),a&&(a.classList.remove("hidden"),a.innerHTML=`\n <div class="bg-blue-600 rounded p-3 text-sm">\n <svg class="w-4 h-4 inline mr-2 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>\n </svg>\n Connecting to ${o}...\n </div>\n `);const n=await postAPI("/api/wifi/connect",{ssid:o,password:d,save:u});n.success?(a&&(a.innerHTML=`\n <div class="bg-green-600 rounded p-3 text-sm">\n <svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>\n </svg>\n ${n.message||"Connected successfully!"}\n </div>\n `),addConsoleMessage(`Connected to Wi-Fi: ${o}`,"success"),setTimeout(()=>{closeWifiConnectModal(),refreshWifiStatus()},2e3)):(a&&(a.innerHTML=`\n <div class="bg-red-600 rounded p-3 text-sm">\n <svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>\n </svg>\n ${n.message||"Connection failed"}\n </div>\n `),addConsoleMessage(`Failed to connect to Wi-Fi: ${o}`,"error"),r&&!l&&i&&t&&(t.style.display="block",e&&(e.value="",e.placeholder="Stored password failed - enter correct password"),selectedWifiNetwork&&(selectedWifiNetwork.editMode=!0)))}catch(n){console.error("Error connecting to Wi-Fi:",n),a&&(a.classList.remove("hidden"),a.innerHTML=`\n <div class="bg-red-600 rounded p-3 text-sm">\n Error: ${n.message}\n </div>\n `),addConsoleMessage(`Error connecting to Wi-Fi: ${n.message}`,"error"),r&&!l&&i&&t&&(t.style.display="block",e&&(e.value="",e.placeholder="Connection error - please enter password"),selectedWifiNetwork&&(selectedWifiNetwork.editMode=!0))}finally{s&&(s.disabled=!1,s.innerHTML=selectedWifiNetwork&&selectedWifiNetwork.editMode?"Update & Connect":"Connect")}else a&&(a.classList.remove("hidden"),a.innerHTML='<div class="bg-red-600 rounded p-3 text-sm">Please enter a password</div>')}async function loadConsoleLogs(){try{const e=await fetchAPI("/api/logs");e&&e.logs&&updateConsole(e.logs)}catch(e){console.error("Error loading console logs:",e),addConsoleMessage("Unable to load historical logs from server","warning"),addConsoleMessage("Console will show new messages as they occur","info")}}let currentBluetoothDevices=[],isBluetoothScanning=!1,bluetoothScanInterval=null;async function refreshBluetoothStatus(){try{updateBluetoothStatus(await fetchAPI("/api/bluetooth/status"))}catch(e){console.error("Error refreshing Bluetooth status:",e),updateBluetoothStatus({enabled:!1,discoverable:!1,error:"Failed to get Bluetooth status"})}}function updateBluetoothStatus(e){const t=document.getElementById("bluetooth-status-indicator"),n=document.getElementById("bluetooth-info"),a=document.getElementById("bluetooth-power-btn"),s=document.getElementById("bluetooth-power-text"),o=document.getElementById("bluetooth-discoverable-btn"),r=document.getElementById("bluetooth-discoverable-text");if(t&&n&&a&&s){if(e.error)return t.className="text-sm px-2 py-1 rounded bg-red-700 text-red-300",t.textContent="Error",n.textContent=e.error,s.textContent="Enable Bluetooth",void(r&&(r.textContent="Make Discoverable"));if(e.enabled){t.className="text-sm px-2 py-1 rounded bg-green-700 text-green-300",t.textContent="Enabled",s.textContent="Disable Bluetooth",a.className="w-full bg-red-600 hover:bg-red-700 text-white py-2 px-4 rounded transition-colors";let i="Bluetooth is enabled";e.address&&(i+=` | Address: ${e.address}`),e.name&&(i+=` | Name: ${e.name}`),n.textContent=i,o&&r&&(e.discoverable?(r.textContent="Hide Device",o.className="w-full bg-orange-600 hover:bg-orange-700 text-white py-2 px-4 rounded transition-colors"):(r.textContent="Make Discoverable",o.className="w-full bg-cyan-600 hover:bg-cyan-700 text-white py-2 px-4 rounded transition-colors"),o.disabled=!1)}else t.className="text-sm px-2 py-1 rounded bg-gray-700 text-gray-300",t.textContent="Disabled",n.textContent="Bluetooth is disabled",s.textContent="Enable Bluetooth",a.className="w-full bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded transition-colors",o&&r&&(r.textContent="Make Discoverable",o.className="w-full bg-gray-600 hover:bg-gray-700 text-white py-2 px-4 rounded transition-colors",o.disabled=!0)}}async function toggleBluetoothPower(){const e=document.getElementById("bluetooth-power-btn"),t=document.getElementById("bluetooth-power-text");if(!e||!t)return;const n=t.textContent;t.textContent="Processing...",e.disabled=!0;try{const e="Disable Bluetooth"===n,t=e?"/api/bluetooth/disable":"/api/bluetooth/enable",a=await postAPI(t,{});if(!a.success)throw new Error(a.error||"Failed to toggle Bluetooth");addConsoleMessage(`Bluetooth ${e?"disabled":"enabled"} successfully`,"success"),setTimeout(refreshBluetoothStatus,1e3)}catch(e){console.error("Error toggling Bluetooth power:",e),addConsoleMessage(`Error toggling Bluetooth: ${e.message}`,"error"),t.textContent=n}finally{e.disabled=!1,"Processing..."===t.textContent&&(t.textContent=n)}}async function toggleBluetoothDiscoverable(){const e=document.getElementById("bluetooth-discoverable-btn"),t=document.getElementById("bluetooth-discoverable-text");if(!e||!t)return;const n=t.textContent;t.textContent="Processing...",e.disabled=!0;try{const e="Hide Device"===n,t=e?"/api/bluetooth/discoverable/off":"/api/bluetooth/discoverable/on",a=await postAPI(t,{});if(!a.success)throw new Error(a.error||"Failed to toggle discoverable mode");addConsoleMessage("Bluetooth "+(e?"hidden":"made discoverable"),"success"),setTimeout(refreshBluetoothStatus,1e3)}catch(e){console.error("Error toggling Bluetooth discoverable:",e),addConsoleMessage(`Error toggling discoverable mode: ${e.message}`,"error"),t.textContent=n}finally{e.disabled=!1,"Processing..."===t.textContent&&(t.textContent=n)}}async function startBluetoothScan(){const e=document.getElementById("bluetooth-scan-btn"),t=document.getElementById("bluetooth-scan-text"),n=document.getElementById("bluetooth-scan-status");if(e&&t&&n)if(isBluetoothScanning)stopBluetoothScan();else{isBluetoothScanning=!0,t.textContent="Stop Scan",e.className="w-full bg-red-600 hover:bg-red-700 text-white py-2 px-4 rounded transition-colors mb-2",n.className="text-sm px-2 py-1 rounded bg-blue-700 text-blue-300",n.textContent="Scanning...";try{const e=await postAPI("/api/bluetooth/scan/start",{});if(!e.success)throw new Error(e.error||"Failed to start Bluetooth scan");addConsoleMessage("Started Bluetooth device scan","info");try{displayBluetoothDevices((await fetchAPI("/api/bluetooth/devices")).devices||[])}catch(e){console.error("Error getting initial Bluetooth devices:",e)}bluetoothScanInterval=setInterval(async()=>{try{displayBluetoothDevices((await fetchAPI("/api/bluetooth/devices")).devices||[])}catch(e){console.error("Error getting Bluetooth devices:",e)}},2e3)}catch(e){console.error("Error starting Bluetooth scan:",e),addConsoleMessage(`Error starting Bluetooth scan: ${e.message}`,"error"),stopBluetoothScan()}}}function stopBluetoothScan(){const e=document.getElementById("bluetooth-scan-btn"),t=document.getElementById("bluetooth-scan-text"),n=document.getElementById("bluetooth-scan-status");isBluetoothScanning=!1,bluetoothScanInterval&&(clearInterval(bluetoothScanInterval),bluetoothScanInterval=null),e&&t&&n&&(t.textContent="Start Scan",e.className="w-full bg-indigo-600 hover:bg-indigo-700 text-white py-2 px-4 rounded transition-colors mb-2",n.className="text-sm px-2 py-1 rounded bg-gray-700 text-gray-300",n.textContent="Ready"),postAPI("/api/bluetooth/scan/stop",{}).catch(e=>{}).catch(e=>{console.error("Error stopping Bluetooth scan:",e)}),addConsoleMessage("Stopped Bluetooth device scan","info")}function displayBluetoothDevices(e){const t=document.getElementById("bluetooth-devices-list");t&&(currentBluetoothDevices=e,e&&0!==e.length?t.innerHTML=e.map(e=>`\n <div class="glass rounded-lg p-3 hover:bg-slate-700 transition-colors cursor-pointer"\n onclick="showBluetoothDeviceDetails('${e.address}')">\n <div class="flex items-center justify-between">\n <div class="flex-1">\n <div class="font-medium text-white">\n ${escapeHtml(e.name||"Unknown Device")}\n </div>\n <div class="text-sm text-gray-400">\n ${e.address} ${e.rssi?`• ${e.rssi} dBm`:""}\n </div>\n ${e.device_class?`\n <div class="text-xs text-gray-500 mt-1">\n ${escapeHtml(e.device_class)}\n </div>\n `:""}\n </div>\n <div class="flex items-center space-x-2">\n ${e.rssi?`\n <div class="text-xs px-2 py-1 rounded ${getRSSIClass(e.rssi)}">\n ${e.rssi} dBm\n </div>\n `:""}\n ${e.paired?'\n <div class="text-xs px-2 py-1 rounded bg-green-700 text-green-300">\n Paired\n </div>\n ':""}\n </div>\n </div>\n </div>\n `).join(""):t.innerHTML=`\n <div class="text-center text-gray-400 py-8">\n ${isBluetoothScanning?"Scanning for devices...":"No devices found. Start a scan to discover nearby devices."}\n </div>\n `)}function getRSSIClass(e){return e>=-40?"bg-green-700 text-green-300":e>=-60?"bg-yellow-700 text-yellow-300":e>=-80?"bg-orange-700 text-orange-300":"bg-red-700 text-red-300"}function showBluetoothDeviceDetails(e){const t=currentBluetoothDevices.find(t=>t.address===e);if(!t)return;const n=document.getElementById("bluetooth-device-modal"),a=document.getElementById("bt-device-name"),s=document.getElementById("bt-device-mac"),o=document.getElementById("bt-device-rssi"),r=document.getElementById("bt-device-class"),i=document.getElementById("bt-device-services"),l=document.getElementById("bt-pair-btn");n&&a&&s&&(a.value=t.name||"Unknown Device",s.value=t.address,o&&(o.value=t.rssi?`${t.rssi} dBm`:"Unknown"),r&&(r.value=t.device_class||"Unknown"),i&&(t.services&&t.services.length>0?i.innerHTML=t.services.map(e=>`\n <div class="mb-1 text-sm">${escapeHtml(e)}</div>\n `).join(""):i.innerHTML='<div class="text-gray-400">No services detected</div>'),l&&(t.paired?(l.innerHTML='\n <svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path>\n </svg>\n Unpair Device\n ',l.className="bg-red-600 hover:bg-red-700 text-white py-2 px-4 rounded transition-colors"):(l.innerHTML='\n <svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path>\n </svg>\n Pair Device\n ',l.className="bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded transition-colors"),l.setAttribute("data-device-address",t.address)),n.classList.remove("hidden"),n.classList.add("flex"))}function closeBluetoothDeviceModal(){const e=document.getElementById("bluetooth-device-modal"),t=document.getElementById("bt-device-status");e&&(e.classList.add("hidden"),e.classList.remove("flex")),t&&(t.classList.add("hidden"),t.innerHTML="")}async function pairBluetoothDevice(){const e=document.getElementById("bt-pair-btn"),t=document.getElementById("bt-device-status");if(!e)return;const n=e.getAttribute("data-device-address");if(!n)return;const a=currentBluetoothDevices.find(e=>e.address===n);if(!a)return;const s=e.innerHTML;e.innerHTML="Processing...",e.disabled=!0,t&&(t.classList.remove("hidden"),t.innerHTML=`\n <div class="bg-blue-600 rounded p-3 text-sm">\n ${a.paired?"Unpairing":"Pairing"} device ${a.name||n}...\n </div>\n `);try{const e=a.paired?"/api/bluetooth/unpair":"/api/bluetooth/pair",s=await fetchAPI(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({address:n})});if(!s.success)throw new Error(s.error||`Failed to ${a.paired?"unpair":"pair"} device`);{const e=a.paired?"unpaired":"paired";addConsoleMessage(`Device ${a.name||n} ${e} successfully`,"success"),t&&(t.innerHTML=`\n <div class="bg-green-600 rounded p-3 text-sm">\n Device ${e} successfully\n </div>\n `),setTimeout(()=>{isBluetoothScanning&&fetchAPI("/api/bluetooth/devices").then(e=>{displayBluetoothDevices(e.devices||[])}).catch(e=>{console.error("Error refreshing devices:",e)}),closeBluetoothDeviceModal()},2e3)}}catch(e){console.error("Error pairing/unpairing device:",e),addConsoleMessage(`Error: ${e.message}`,"error"),t&&(t.innerHTML=`\n <div class="bg-red-600 rounded p-3 text-sm">\n Error: ${e.message}\n </div>\n `)}finally{e.disabled=!1,"Processing..."===e.innerHTML&&(e.innerHTML=s)}}async function enumerateBluetoothServices(){const e=document.getElementById("bt-enumerate-btn"),t=document.getElementById("bt-device-status"),n=document.getElementById("bt-device-services");if(!e)return;const a=document.getElementById("bt-pair-btn")?.getAttribute("data-device-address");if(!a)return;const s=currentBluetoothDevices.find(e=>e.address===a);if(!s)return;const o=e.innerHTML;e.innerHTML="Enumerating...",e.disabled=!0,t&&(t.classList.remove("hidden"),t.innerHTML=`\n <div class="bg-blue-600 rounded p-3 text-sm">\n Enumerating services for ${s.name||a}...\n </div>\n `);try{const e=await postAPI("/api/bluetooth/enumerate",{address:a});if(!e.success||!e.services)throw new Error(e.error||"Failed to enumerate services");addConsoleMessage(`Found ${e.services.length} services on ${s.name||a}`,"success"),n&&(e.services.length>0?n.innerHTML=e.services.map(e=>`\n <div class="mb-2 p-2 bg-slate-800 rounded text-sm">\n <div class="font-medium">${escapeHtml(e.name||"Unknown Service")}</div>\n <div class="text-gray-400 text-xs">${e.uuid}</div>\n ${e.description?`<div class="text-gray-500 text-xs mt-1">${escapeHtml(e.description)}</div>`:""}\n </div>\n `).join(""):n.innerHTML='<div class="text-gray-400">No services found</div>'),t&&(t.innerHTML=`\n <div class="bg-green-600 rounded p-3 text-sm">\n Found ${e.services.length} services\n </div>\n `)}catch(e){console.error("Error enumerating services:",e),addConsoleMessage(`Error enumerating services: ${e.message}`,"error"),t&&(t.innerHTML=`\n <div class="bg-red-600 rounded p-3 text-sm">\n Error: ${e.message}\n </div>\n `)}finally{e.disabled=!1,"Enumerating..."===e.innerHTML&&(e.innerHTML=o)}}function clearBluetoothDevices(){const e=document.getElementById("bluetooth-devices-list");e&&(e.innerHTML='\n <div class="text-center text-gray-400 py-8">\n Start a Bluetooth scan to discover nearby devices\n </div>\n '),currentBluetoothDevices=[],addConsoleMessage("Cleared Bluetooth device list","info")}async function startBeaconTracking(){const e=document.getElementById("beacon-track-btn"),t=document.getElementById("beacon-results"),n=document.getElementById("beacon-duration");if(!e||!t||!n)return;const a=parseInt(n.value)||60,s=e.textContent;e.disabled=!0,e.textContent="Tracking...",t.classList.remove("hidden"),t.textContent=`Tracking beacons for ${a} seconds...`;try{const e=await postAPI("/api/bluetooth/pentest/beacon-track",{duration:a});if(!e.success)throw new Error(e.error||"Beacon tracking failed");{const n=e.beacons_found||0;t.innerHTML=`\n <div class="text-green-400">✓ Found ${n} beacon(s)</div>\n <div class="mt-1">${JSON.stringify(e.beacons,null,2)}</div>\n `,addConsoleMessage(`Beacon tracking complete: ${n} beacons found`,"success"),updatePentestSummary("beacon_tracking",e)}}catch(e){console.error("Beacon tracking error:",e),t.innerHTML=`<div class="text-red-400">✗ Error: ${e.message}</div>`,addConsoleMessage(`Beacon tracking failed: ${e.message}`,"error")}finally{e.disabled=!1,e.textContent=s}}async function startDataExfiltration(){const e=document.getElementById("exfil-btn"),t=document.getElementById("exfil-results"),n=document.getElementById("exfil-target");if(!e||!t||!n)return;const a=n.value.trim();if(!a||!a.match(/^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/))return t.classList.remove("hidden"),void(t.innerHTML='<div class="text-red-400">✗ Invalid MAC address format</div>');const s=e.textContent;e.disabled=!0,e.textContent="Exfiltrating...",t.classList.remove("hidden"),t.textContent=`Extracting data from ${a}...`;try{const e=await postAPI("/api/bluetooth/pentest/exfiltrate",{target:a});if(!e.device_info)throw new Error(e.error||"Exfiltration failed");{const n=e.services?.length||0,s=e.files?.length||0,o=e.contacts?.length||0;t.innerHTML=`\n <div class="text-green-400">✓ Exfiltration complete</div>\n <div class="mt-1 text-xs">\n Services: ${n} | Files: ${s} | Contacts: ${o}\n </div>\n `,addConsoleMessage(`Data exfiltration from ${a} complete`,"success"),updatePentestSummary("exfiltration",e)}}catch(e){console.error("Exfiltration error:",e),t.innerHTML=`<div class="text-red-400">✗ Error: ${e.message}</div>`,addConsoleMessage(`Exfiltration failed: ${e.message}`,"error")}finally{e.disabled=!1,e.textContent=s}}async function startBlueBorneScan(){const e=document.getElementById("blueborne-btn"),t=document.getElementById("blueborne-results"),n=document.getElementById("blueborne-target");if(!e||!t||!n)return;const a=n.value.trim();if(!a||!a.match(/^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/))return t.classList.remove("hidden"),void(t.innerHTML='<div class="text-red-400">✗ Invalid MAC address format</div>');const s=e.textContent;e.disabled=!0,e.textContent="Scanning...",t.classList.remove("hidden"),t.textContent=`Scanning ${a} for BlueBorne...`;try{const e=await postAPI("/api/bluetooth/pentest/blueborne-scan",{target:a}),n=e.vulnerabilities?.length||0,s=e.vulnerable||!1;t.innerHTML=`\n <div class="${s?"text-red-400":"text-green-400"}">\n ${s?"⚠ Potentially vulnerable":"✓ No vulnerabilities detected"}\n </div>\n ${n>0?`<div class="mt-1 text-xs">Found ${n} potential issue(s)</div>`:""}\n `,addConsoleMessage(`BlueBorne scan of ${a} complete`,"info"),updatePentestSummary("blueborne_scan",e)}catch(e){console.error("BlueBorne scan error:",e),t.innerHTML=`<div class="text-red-400">✗ Error: ${e.message}</div>`,addConsoleMessage(`BlueBorne scan failed: ${e.message}`,"error")}finally{e.disabled=!1,e.textContent=s}}async function startMovementTracking(){const e=document.getElementById("track-btn"),t=document.getElementById("track-results"),n=document.getElementById("track-target"),a=document.getElementById("track-duration");if(!(e&&t&&n&&a))return;const s=n.value.trim(),o=parseInt(a.value)||300;if(!s||!s.match(/^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/))return t.classList.remove("hidden"),void(t.innerHTML='<div class="text-red-400">✗ Invalid MAC address format</div>');const r=e.textContent;e.disabled=!0,e.textContent="Tracking...",t.classList.remove("hidden"),t.textContent=`Tracking ${s} for ${o}s...`;try{const e=await postAPI("/api/bluetooth/pentest/track-movement",{target:s,duration:o});if(!(e.readings&&e.readings.length>0))throw new Error("No readings collected");{const n=e.readings.reduce((e,t)=>e+t.rssi,0)/e.readings.length,a=e.readings.reduce((e,t)=>e+t.distance_estimate,0)/e.readings.length;t.innerHTML=`\n <div class="text-green-400">✓ Tracking complete</div>\n <div class="mt-1 text-xs">\n Readings: ${e.readings.length} | Avg RSSI: ${n.toFixed(1)} dBm | \n Avg Distance: ${a.toFixed(2)}m\n </div>\n `,addConsoleMessage(`Movement tracking of ${s} complete`,"success"),updatePentestSummary("movement_tracking",e)}}catch(e){console.error("Movement tracking error:",e),t.innerHTML=`<div class="text-red-400">✗ Error: ${e.message}</div>`,addConsoleMessage(`Movement tracking failed: ${e.message}`,"error")}finally{e.disabled=!1,e.textContent=r}}let pentestResults={};function updatePentestSummary(e,t){pentestResults[e]={timestamp:(new Date).toISOString(),data:t};const n=document.getElementById("pentest-summary"),a=document.getElementById("pentest-summary-content");if(!n||!a)return;n.classList.remove("hidden");const s=[];if(pentestResults.beacon_tracking&&s.push(`Beacon Tracking: ${pentestResults.beacon_tracking.data.beacons_found||0} beacons found`),pentestResults.exfiltration){const e=pentestResults.exfiltration.data;s.push(`Data Exfiltration: ${e.services?.length||0} services, ${e.files?.length||0} files`)}if(pentestResults.blueborne_scan){const e=pentestResults.blueborne_scan.data.vulnerable?"Vulnerable":"Safe";s.push(`BlueBorne Scan: ${e}`)}if(pentestResults.movement_tracking){const e=pentestResults.movement_tracking.data.readings?.length||0;s.push(`Movement Tracking: ${e} readings collected`)}if(pentestResults.airsnitch){const e=pentestResults.airsnitch.data,t=e.network_isolated?"Isolated ✓":`${e.vulnerable_count}/${e.total_tests} failed ✗`;s.push(`AirSnitch: ${t}`)}a.innerHTML=s.map(e=>`<div>• ${e}</div>`).join("")}async function downloadPentestReport(){try{const e=await fetchAPI("/api/bluetooth/pentest/report");if(!e||!e.timestamp)throw new Error("No report data available");{const t=new Blob([JSON.stringify(e,null,2)],{type:"application/json"}),n=window.URL.createObjectURL(t),a=document.createElement("a");a.href=n,a.download=`bluetooth_pentest_${Date.now()}.json`,document.body.appendChild(a),a.click(),window.URL.revokeObjectURL(n),document.body.removeChild(a),addConsoleMessage("Pentest report downloaded","success")}}catch(e){console.error("Report download error:",e),addConsoleMessage(`Failed to download report: ${e.message}`,"error")}}let complianceFramework="cis",threatIntelSubtab="vulns";function showThreatIntelSubtab(e){threatIntelSubtab=e;const t=document.getElementById("ti-sub-vulns"),n=document.getElementById("ti-sub-compliance"),a=document.getElementById("ti-subtab-vulns"),s=document.getElementById("ti-subtab-compliance");t&&t.classList.toggle("hidden","vulns"!==e),n&&n.classList.toggle("hidden","compliance"!==e);const o="ti-subtab px-4 py-2 rounded-lg text-sm font-semibold transition-colors bg-Ragnar-600 text-white",r="ti-subtab px-4 py-2 rounded-lg text-sm font-semibold transition-colors text-slate-400 hover:bg-slate-700 hover:text-white";a&&(a.className="vulns"===e?o:r),s&&(s.className="compliance"===e?o:r),"compliance"===e&&loadComplianceData()}function complianceEscape(e){return String(null==e?"":e).replace(/[&<>"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[e]))}function complianceStatusBadge(e){const[t,n]={attention:["Action needed","text-red-400"],review:["Review","text-yellow-400"],ok:["No issues","text-green-400"],not_assessed:["Not assessed","text-gray-400"]}[e]||[e,"text-gray-400"];return`<span class="${n} font-semibold">${t}</span>`}function complianceStatCard(e,t,n){return`<div class="glass rounded-lg p-3 text-center">\n <div class="text-2xl font-bold ${n||"text-white"}">${e}</div>\n <div class="text-xs text-gray-400 uppercase tracking-wide mt-1">${t}</div>\n </div>`}function setComplianceFramework(e){complianceFramework=e;const t=document.getElementById("compliance-tab-cis"),n=document.getElementById("compliance-tab-pci");if(t&&n){const a="px-4 py-1.5 text-sm bg-Ragnar-600 text-white",s="px-4 py-1.5 text-sm bg-slate-700 text-gray-300 hover:bg-slate-600";t.className="cis"===e?a:s,n.className="pci"===e?a:s}loadComplianceData()}function refreshComplianceData(){loadComplianceData()}function exportComplianceReport(){const e=document.createElement("a");e.href="/api/compliance/export",e.download="",document.body.appendChild(e),e.click(),document.body.removeChild(e),addConsoleMessage("Compliance report exported","success")}async function loadComplianceData(){const e=document.getElementById("compliance-summary"),t=document.getElementById("compliance-results");if(t){t.innerHTML='<p class="text-gray-500 text-sm">Loading…</p>',e&&(e.innerHTML="");try{const n=await fetchAPI(`/api/compliance/${complianceFramework}`);"cis"===complianceFramework?renderComplianceCIS(n,e,t):renderCompliancePCI(n,e,t)}catch(e){t.innerHTML=`<p class="text-red-400 text-sm">Failed to load compliance data: ${complianceEscape(e.message)}</p>`}}}function renderComplianceCIS(e,t,n){const a=e.summary||{};t&&(t.innerHTML=[complianceStatCard(a.hosts_assessed||0,"Hosts Audited"),complianceStatCard(a.controls_flagged||0,"Controls Flagged","text-red-400"),complianceStatCard(a.controls_review||0,"Review","text-yellow-400"),complianceStatCard(null==a.avg_hardening_index?"n/a":a.avg_hardening_index,"Avg Hardening Index")].join(""));const s=e.controls||[];if(!s.length)return void(n.innerHTML='<p class="text-gray-400 text-sm">No Lynis audit data found. Run a Lynis SSH audit against a host with known credentials, then refresh.</p>');let o='<table class="min-w-[680px] w-full text-sm"><thead><tr class="text-gray-400 text-left border-b border-slate-700"><th class="py-2 pr-3 whitespace-nowrap">CIS Area</th><th class="py-2 pr-3 whitespace-nowrap">Topic</th><th class="py-2 pr-3 whitespace-nowrap">Status</th><th class="py-2 pr-3 whitespace-nowrap text-right">Warn</th><th class="py-2 pr-3 whitespace-nowrap text-right">Sugg</th><th class="py-2 whitespace-nowrap">Findings</th></tr></thead><tbody>';s.forEach(e=>{const t=e.findings||[],n=t.slice(0,5).map(e=>`<div class="text-xs text-gray-400 break-words"><span class="font-mono text-gray-500">${complianceEscape(e.host)} · ${complianceEscape(e.code)}</span> — ${complianceEscape((e.message||"").slice(0,120))}</div>`).join(""),a=t.length>5?`<div class="text-xs text-gray-600">…and ${t.length-5} more</div>`:"";o+=`<tr class="border-t border-slate-700/60 align-top">\n <td class="py-2 pr-3 text-gray-200 whitespace-nowrap">${complianceEscape(e.control)}</td>\n <td class="py-2 pr-3 text-gray-300">${complianceEscape(e.title)}</td>\n <td class="py-2 pr-3 whitespace-nowrap">${complianceStatusBadge(e.status)}</td>\n <td class="py-2 pr-3 text-right text-gray-300">${e.warnings}</td>\n <td class="py-2 pr-3 text-right text-gray-300">${e.suggestions}</td>\n <td class="py-2 min-w-[220px] break-words">${n}${a||(n?"":"—")}</td>\n </tr>`}),o+="</tbody></table>",n.innerHTML=o}function renderCompliancePCI(e,t,n){const a=e.summary||{};t&&(t.innerHTML=[complianceStatCard(a.requirements_total||0,"Requirements"),complianceStatCard(a.attention||0,"Action Needed","text-red-400"),complianceStatCard(a.ok||0,"No Issues","text-green-400"),complianceStatCard(a.not_assessed||0,"Not Assessed","text-gray-400")].join(""));const s=e.requirements||[];if(!s.length)return void(n.innerHTML='<p class="text-gray-400 text-sm">No data available.</p>');let o='<table class="min-w-[680px] w-full text-sm"><thead><tr class="text-gray-400 text-left border-b border-slate-700"><th class="py-2 pr-3 whitespace-nowrap">Req</th><th class="py-2 pr-3 whitespace-nowrap">Title</th><th class="py-2 pr-3 whitespace-nowrap">Status</th><th class="py-2 pr-3 whitespace-nowrap">Guidance</th><th class="py-2 whitespace-nowrap">Evidence</th></tr></thead><tbody>';s.forEach(e=>{const t=(e.evidence||[]).map(e=>`<div class="text-xs text-gray-400 break-words">${complianceEscape(e)}</div>`).join("")||"—";o+=`<tr class="border-t border-slate-700/60 align-top">\n <td class="py-2 pr-3 font-mono text-gray-200 whitespace-nowrap">${complianceEscape(e.id)}</td>\n <td class="py-2 pr-3 text-gray-300">${complianceEscape(e.title)}</td>\n <td class="py-2 pr-3 whitespace-nowrap">${complianceStatusBadge(e.status)}</td>\n <td class="py-2 pr-3 text-xs text-gray-500 min-w-[180px] break-words">${complianceEscape(e.guidance)}</td>\n <td class="py-2 min-w-[220px] break-words">${t}</td>\n </tr>`}),o+="</tbody></table>",n.innerHTML=o}async function populateAirSnitchInterfaceDropdowns(){const e=document.getElementById("airsnitch-iface-victim"),t=document.getElementById("airsnitch-iface-attacker");if(e&&t)try{const[n,a]=await Promise.allSettled([fetchAPI("/api/wifi/interfaces"),fetchAPI("/api/ethernet/interfaces")]),s=[];if("fulfilled"===n.status&&Array.isArray(n.value?.interfaces)&&n.value.interfaces.forEach(e=>{const t=e.connected_ssid?`${e.name} — ${e.connected_ssid} (${e.state})`:`${e.name} — ${e.state}`;s.push({value:e.name,label:t})}),"fulfilled"===a.status&&Array.isArray(a.value?.interfaces)&&a.value.interfaces.forEach(e=>{const t=`${e.name} — ${e.state||(e.connected?"connected":"disconnected")}`;s.push({value:e.name,label:t})}),0===s.length)return;const o=e.value,r=t.value;[e,t].forEach(e=>{e.innerHTML="",s.forEach(({value:t,label:n})=>{const a=document.createElement("option");a.value=t,a.textContent=n,e.appendChild(a)})}),s.some(e=>e.value===o)&&(e.value=o),s.some(e=>e.value===r)&&(t.value=r)}catch(e){}}async function checkAirSnitchInstalled(){try{const e=await fetchAPI("/api/airsnitch/status"),t=document.getElementById("airsnitch-install-notice");return t&&t.classList.toggle("hidden",!1!==e.installed),e.installed}catch(e){return null}}async function installAirSnitch(){const e=document.getElementById("airsnitch-install-btn"),t=document.getElementById("airsnitch-status"),n=document.getElementById("airsnitch-install-log");e&&(e.disabled=!0,e.textContent="Installing…"),n&&(n.classList.remove("hidden"),n.textContent=""),t&&(t.classList.remove("hidden"),t.textContent="Starting installation…");try{const a=await postAPI("/api/airsnitch/install",{});if(!a.success)throw new Error(a.error||"Install request failed");t&&(t.textContent=a.message||"Installation running…"),addConsoleMessage("AirSnitch installation started","info"),await _pollAirSnitchInstallLog(e,t,n)}catch(a){t&&(t.classList.remove("hidden"),t.textContent=`Install failed: ${a.message}`),n&&(n.textContent+=`\nERROR: ${a.message}`),addConsoleMessage(`AirSnitch install failed: ${a.message}`,"error"),e&&(e.disabled=!1,e.textContent="Retry Install")}}async function _pollAirSnitchInstallLog(e,t,n){return new Promise(a=>{const s=setInterval(async()=>{try{const o=await fetchAPI("/api/airsnitch/install-log");if(n&&void 0!==o.log&&(n.textContent=o.log,n.scrollTop=n.scrollHeight),!o.installing){if(clearInterval(s),o.installed){t&&(t.textContent="Installation complete."),e&&(e.disabled=!1,e.textContent="Installed ✓");const n=document.getElementById("airsnitch-install-notice");n&&n.classList.add("hidden"),addConsoleMessage("AirSnitch installed successfully","success")}else t&&(t.textContent="Installation failed – see log above."),e&&(e.disabled=!1,e.textContent="Retry Install"),addConsoleMessage("AirSnitch installation failed","error");a()}}catch(t){clearInterval(s),e&&(e.disabled=!1,e.textContent="Retry Install"),a()}},2e3)})}async function runAirSnitch(){const e=document.getElementById("airsnitch-run-btn"),t=document.getElementById("airsnitch-status"),n=document.getElementById("airsnitch-iface-victim")?.value.trim()||"wlan1",a=document.getElementById("airsnitch-iface-attacker")?.value.trim()||"wlan2",s=document.getElementById("airsnitch-server")?.value.trim()||"8.8.8.8",o=document.getElementById("airsnitch-same-bss")?.checked||!1,r=[...document.querySelectorAll(".airsnitch-test-check:checked")].map(e=>e.value),i=document.getElementById("airsnitch-victim-ssid")?.value.trim()||"",l=document.getElementById("airsnitch-victim-psk")?.value||"",c=document.getElementById("airsnitch-attacker-ssid")?.value.trim()||"",d=document.getElementById("airsnitch-attacker-psk")?.value||"";if(0===r.length)return void(t&&(t.classList.remove("hidden"),t.textContent="Select at least one test."));const u=e?.textContent||"Run AirSnitch";e&&(e.disabled=!0,e.textContent="Running…"),t&&(t.classList.remove("hidden"),t.textContent="Tests running in background…");try{const e={iface_victim:n,iface_attacker:a,server:s,same_bss:o,tests:r};i&&(e.victim_ssid=i),l&&(e.victim_psk=l),c&&(e.attacker_ssid=c),d&&(e.attacker_psk=d);const u=await postAPI("/api/airsnitch/run",e);t&&(t.textContent=u.message||"Started."),addConsoleMessage("AirSnitch tests started","info"),setTimeout(refreshAirSnitchResults,1e4)}catch(e){t&&(t.textContent=`Error: ${e.message}`),addConsoleMessage(`AirSnitch failed: ${e.message}`,"error")}finally{e&&(e.disabled=!1,e.textContent=u)}}async function refreshAirSnitchResults(){try{const e=await fetchAPI("/api/airsnitch/results"),t=document.getElementById("airsnitch-results"),n=document.getElementById("airsnitch-results-content");if(!t||!n)return;if(!e.results)return;t.classList.remove("hidden");const a=e.results,s=a.summary||{},o=[];o.push(`<div class="text-gray-400">⏱ ${a.timestamp||""}</div>`),o.push(`<div>Interfaces: victim=<span class="text-cyan-400">${a.iface_victim}</span> attacker=<span class="text-cyan-400">${a.iface_attacker}</span></div>`),!0===s.network_isolated?o.push(`<div class="text-green-400 font-bold">✓ Network PASSES client isolation (${s.total_tests} tests)</div>`):s.vulnerable_count>0&&o.push(`<div class="text-red-400 font-bold">✗ Network FAILS client isolation – ${s.vulnerable_count}/${s.total_tests} test(s) vulnerable</div>`);for(const[e,t]of Object.entries(a.tests||{})){const n=t.vulnerable?"✗":"✓",a=t.vulnerable?"text-red-400":"text-green-400",s=e.replace(/_/g," ").replace(/\b\w/g,e=>e.toUpperCase());o.push(`<div class="${a}">${n} ${s}</div>`)}n.innerHTML=o.join(""),updatePentestSummary("airsnitch",{vulnerable_count:s.vulnerable_count||0,total_tests:s.total_tests||0,network_isolated:s.network_isolated})}catch(e){console.error("AirSnitch results error:",e)}}const DEFAULT_MANUAL_ATTACK_MATRIX={ssh:{label:"SSH Brute Force",ports:["22"]},ftp:{label:"FTP Brute Force",ports:["21"]},telnet:{label:"Telnet Brute Force",ports:["23"]},smb:{label:"SMB Brute Force",ports:["139","445"]},rdp:{label:"RDP Brute Force",ports:["3389"]},sql:{label:"SQL Brute Force",ports:["3306"]}};function hydrateManualAttackMatrix(e){const t={};return new Set([...Object.keys(DEFAULT_MANUAL_ATTACK_MATRIX),...e?Object.keys(e):[]]).forEach(n=>{const a=DEFAULT_MANUAL_ATTACK_MATRIX[n]||{},s=e&&e[n]||{},o=s.label||a.label||`${n.toUpperCase()} Attack`,r=Array.isArray(a.ports)?a.ports:[],i=Array.isArray(s.ports)?s.ports:[],l=(i.length?i:r).map(e=>String(e));l.length&&(t[n]={label:o,ports:l})}),Object.keys(t).length?t:{...DEFAULT_MANUAL_ATTACK_MATRIX}}function getManualAttackMatrix(){return window.manualAttackMatrix||{...DEFAULT_MANUAL_ATTACK_MATRIX}}function getValidActionsForPort(e){if(!e)return[];const t=String(e).trim(),n=getManualAttackMatrix();return Object.entries(n).filter(([,e])=>Array.isArray(e.ports)&&e.ports.map(String).includes(t)).map(([e,t])=>({key:e,label:t.label||e.toUpperCase()}))}function isActionAllowedOnPort(e,t){if(!e||!t)return!1;const n=String(t).trim(),a=getManualAttackMatrix()[e];return Boolean(a&&a.ports&&a.ports.map(String).includes(n))}function setManualActionHelper(e,t="muted"){const n=document.getElementById("manual-action-helper");if(!n)return;const a={muted:"text-gray-500",warning:"text-yellow-400",error:"text-red-400",success:"text-green-400"};n.textContent=e,n.className=`text-[11px] mt-1 ${a[t]||a.muted}`}function syncManualModeUI(e){const t=manualModeActive!==e;manualModeActive=e;const n=document.getElementById("manual-mode-hint");n&&n.classList.toggle("hidden",e),document.querySelectorAll(".pentest-nav-btn").forEach(t=>{t.classList.toggle("hidden",!e)}),t&&window._updateNavMode&&window._updateNavMode(),e||"pentest"!==currentTab||showTab("dashboard"),e?manualDataPrimed||(loadManualModeData(),manualDataPrimed=!0):manualDataPrimed=!1}async function loadManualModeData(){try{const e=document.getElementById("manual-ip-dropdown")?.value||"",t=document.getElementById("manual-port-dropdown")?.value||"",n=document.getElementById("manual-action-dropdown")?.value||"",a=document.getElementById("vuln-ip-dropdown")?.value||"all",s=await fetchAPI("/api/manual/targets");window.manualAttackMatrix=hydrateManualAttackMatrix(s.attack_matrix),window.manualActionPreference=n;const o=document.getElementById("manual-ip-dropdown");o&&(o.innerHTML='<option value="">Select IP</option>',s.targets&&s.targets.length>0&&s.targets.forEach(t=>{const n=document.createElement("option");n.value=t.ip,n.textContent=`${t.ip} (${t.hostname})`,t.ip===e&&(n.selected=!0),o.appendChild(n)}));const r=document.getElementById("vuln-ip-dropdown");if(r){r.innerHTML="";const e=document.createElement("option");e.value="all",e.textContent="All Targets","all"!==a&&a||(e.selected=!0),r.appendChild(e),s.targets&&s.targets.length>0&&s.targets.forEach(e=>{const t=document.createElement("option");t.value=e.ip,t.textContent=`${e.ip} (${e.hostname})`,e.ip===a&&(t.selected=!0),r.appendChild(t)})}const i=document.getElementById("manual-action-dropdown");i&&(i.innerHTML='<option value="">Select Action</option>',i.disabled=!0),window.manualTargetsData=s.targets||[],e?(updateManualPorts(),setTimeout(()=>{const e=document.getElementById("manual-port-dropdown");e&&t&&(e.value=t),updateManualActions()},50)):updateManualActions()}catch(e){console.error("Error loading Pentest Mode data:",e),addConsoleMessage("Failed to load Pentest Mode data","error")}}async function loadPentestData(){try{await loadManualModeData(),await refreshBluetoothStatus()}catch(e){console.error("Error loading pentest data:",e),addConsoleMessage("Failed to load pentest data","error")}}function updateManualPorts(){const e=document.getElementById("manual-ip-dropdown"),t=document.getElementById("manual-port-dropdown");if(!e||!t)return;const n=e.value;if(t.innerHTML='<option value="">Select Port</option>',n&&window.manualTargetsData){const e=window.manualTargetsData.find(e=>e.ip===n);e&&e.ports&&e.ports.forEach(e=>{const n=document.createElement("option");n.value=e,n.textContent=e,t.appendChild(n)})}t&&(t.value=""),updateManualActions()}function updateManualActions(){const e=document.getElementById("manual-port-dropdown"),t=document.getElementById("manual-action-dropdown");if(!t)return;const n=e?e.value:"",a=window.manualActionPreference||t.value||"";if(t.innerHTML='<option value="">Select Action</option>',t.disabled=!0,!n)return void setManualActionHelper("Select a port to view compatible attack modules.","muted");const s=getValidActionsForPort(n);if(!s.length)return setManualActionHelper(`No supported attack modules detected for port ${n}.`,"warning"),void(window.manualActionPreference="");s.forEach(e=>{const n=document.createElement("option");n.value=e.key,n.textContent=e.label,t.appendChild(n)}),t.disabled=!1,a&&s.some(e=>e.key===a)?t.value=a:(t.value="",window.manualActionPreference=""),t.value?(window.manualActionPreference=t.value,setManualActionHelper(`${t.options[t.selectedIndex].text} ready for port ${n}.`,"success")):setManualActionHelper(`Found ${s.length} compatible ${1===s.length?"action":"actions"} on port ${n}. Choose one to proceed.`,"success")}"undefined"!=typeof window&&(window.manualAttackMatrix={...DEFAULT_MANUAL_ATTACK_MATRIX},window.manualActionPreference="");const MANUAL_ATTACK_LOG_LIMIT=40;function setManualAttackStatus(e,t="info"){const n=document.getElementById("manual-attack-status"),a=document.getElementById("manual-attack-status-message");if(!n||!a)return;const s={success:"border-green-500/40 bg-green-900/30 text-green-200",error:"border-red-500/50 bg-red-900/40 text-red-200",warning:"border-yellow-500/40 bg-yellow-900/30 text-yellow-200",info:"border-slate-700 bg-slate-900/70 text-gray-200"};n.classList.remove("hidden"),a.className=`rounded-lg px-4 py-3 text-sm ${s[t]||s.info}`,a.textContent=e}function appendManualAttackLog(e,t="info"){const n=document.getElementById("manual-attack-live-log");if(!n)return;const a={success:"text-green-300",error:"text-red-300",warning:"text-yellow-300",info:"text-gray-300"};"true"!==n.dataset.initialized&&(n.innerHTML="",n.dataset.initialized="true"),n.classList.remove("hidden");const s=document.createElement("div");for(s.className=`flex text-xs font-mono ${a[t]||a.info}`,s.textContent=`[${(new Date).toLocaleTimeString()}] ${e}`,n.appendChild(s);n.childElementCount>40;)n.removeChild(n.firstChild);n.scrollTop=n.scrollHeight}function handleManualAttackUpdate(e){if(!e||!e.action&&!e.ip)return;const t=e.status||"info",n=e.stage||"info",a=(e.action||"attack").toUpperCase(),s=e.ip?`${e.ip}${e.port?`:${e.port}`:""}`:"target",o=e.message||`${a} update`;appendManualAttackLog(`${a} on ${s} • ${o}`,t),"running"===n?setManualAttackStatus(o,"info"):"completed"===n?setManualAttackStatus(o,t):"error"===n?setManualAttackStatus(o,"error"):"queued"===n&&setManualAttackStatus(o,"info")}async function executeManualAttack(){const e=document.getElementById("manual-ip-dropdown")?.value,t=document.getElementById("manual-port-dropdown")?.value,n=document.getElementById("manual-action-dropdown")?.value,a=document.getElementById("manual-attack-launch-btn"),s=(e,t)=>{a&&(a.disabled=!!e,a.classList.toggle("cursor-wait",!!e),e?(a.classList.remove("bg-orange-600","hover:bg-orange-700"),a.classList.add("bg-orange-500")):(a.classList.remove("bg-orange-500"),a.classList.add("bg-orange-600","hover:bg-orange-700")),a.textContent=t||(e?"Launching...":"Execute Attack"))};if(!e||!t||!n)return addConsoleMessage("Please select IP, Port, and Action for manual attack","error"),setManualAttackStatus("Please select a target IP, port, and action before launching.","error"),void appendManualAttackLog("Manual attack aborted - missing selections.","error");if(!isActionAllowedOnPort(n,t)){const e=getManualAttackMatrix(),t=(e[n]?.ports||[]).join(", "),a=`${n.toUpperCase()} brute force is only available on port(s): ${t||"restricted"}.`;return addConsoleMessage(a,"error"),setManualAttackStatus(a,"error"),void appendManualAttackLog("Manual attack blocked due to incompatible port selection.","error")}const o=`${n.toUpperCase()} on ${e}:${t}`;try{addConsoleMessage(`Executing manual attack: ${o}`,"info"),setManualAttackStatus(`Dispatching ${o}. This may take up to a minute depending on module output.`,"info"),s(!0,"Launching...");const a=await postAPI("/api/manual/execute-attack",{ip:e,port:t,action:n});if(a.success){const e=a.message||"Manual attack accepted";addConsoleMessage(e,"info"),setManualAttackStatus(`${e}. Awaiting live module output...`,"info"),appendManualAttackLog(e,"info")}else{const e=a.message||"Unknown error";addConsoleMessage(`Manual attack failed: ${e}`,"error"),setManualAttackStatus(`Manual attack failed: ${e}`,"error"),appendManualAttackLog(`Attack failed: ${e}`,"error")}setTimeout(()=>s(!1),1200)}catch(e){console.error("Error executing manual attack:",e),addConsoleMessage("Failed to execute manual attack due to network error","error"),setManualAttackStatus(`Network error launching manual attack: ${e.message}`,"error"),appendManualAttackLog(`Network error: ${e.message}`,"error"),s(!1,"Execute Attack")}}async function startOrchestrator(){const e=document.getElementById("system-control-status");e&&(e.classList.remove("hidden"),e.textContent="Enabling automation...",e.className="text-sm text-blue-600 mt-4"),addConsoleMessage("Enabling automation...","info");const t=await postAPI("/api/automation/orchestrator/start",{});if(t.success){const n=!1!==t.automation_enabled,a=n?"Auto":"Sleeping",s=n?"text-green-400 font-semibold":"text-purple-300 font-semibold";addConsoleMessage(n?"Automation enabled successfully":"Automation queued - waiting for connectivity","success"),updateElement("Ragnar-mode",a);const o=document.getElementById("Ragnar-mode");o&&(o.className=s),e&&(e.textContent=n?"Automation enabled - Orchestrator running":"Automation queued - waiting for connectivity",e.className=n?"text-sm text-green-600 mt-4":"text-sm text-yellow-500 mt-4",setTimeout(()=>{e&&e.classList.add("hidden")},3e3))}else addConsoleMessage(`Failed to start automatic mode: ${t.message||"Unknown error"}`,"error"),e&&(e.textContent=`Error: ${t.message||"Failed to start automatic mode"}`,e.className="text-sm text-red-600 mt-4");return t}async function stopOrchestrator(){const e=document.getElementById("system-control-status");e&&(e.classList.remove("hidden"),e.textContent="Stopping automatic mode...",e.className="text-sm text-orange-600 mt-4"),addConsoleMessage("Disabling automation...","info");const t=await postAPI("/api/automation/orchestrator/stop",{});if(t.success){addConsoleMessage("Automation disabled - Orchestrator sleeping","warning"),updateElement("Ragnar-mode","Sleeping");const t=document.getElementById("Ragnar-mode");t&&(t.className="text-purple-300 font-semibold"),e&&(e.textContent="Automation disabled - Ragnar is sleeping",e.className="text-sm text-orange-600 mt-4",setTimeout(()=>{e&&e.classList.add("hidden")},3e3))}else addConsoleMessage(`Failed to stop automatic mode: ${t.message||"Unknown error"}`,"error"),e&&(e.textContent=`Error: ${t.message||"Failed to stop automatic mode"}`,e.className="text-sm text-red-600 mt-4");return t}async function triggerNetworkScan(){const e=document.getElementById("system-control-status");try{e&&(e.classList.remove("hidden"),e.textContent="Initiating network discovery scan...",e.className="text-sm text-blue-600 mt-4"),addConsoleMessage("Triggering network scan...","info");const t=await postAPI("/api/manual/scan/network",{});t.success?(addConsoleMessage("Network scan triggered successfully","success"),e&&(e.textContent="Network scan started - Check Network tab for progress",e.className="text-sm text-green-600 mt-4",setTimeout(()=>{e&&e.classList.add("hidden")},4e3))):(addConsoleMessage(`Failed to trigger network scan: ${t.message||"Unknown error"}`,"error"),e&&(e.textContent=`Error: ${t.message||"Failed to trigger network scan"}`,e.className="text-sm text-red-600 mt-4"))}catch(t){console.error("Error triggering network scan:",t),addConsoleMessage("Failed to trigger network scan","error"),e&&(e.textContent=`Error: ${t.message}`,e.className="text-sm text-red-600 mt-4")}}async function triggerVulnScan(){const e=document.getElementById("system-control-status");try{const t=document.getElementById("vuln-ip-dropdown"),n=t?t.value:"all",a=!n||"all"===n,s=a?"all targets":n;e&&(e.classList.remove("hidden"),e.textContent=`Starting vulnerability scan for ${s}...`,e.className="text-sm text-purple-600 mt-4"),addConsoleMessage(`Triggering vulnerability scan for ${s}...`,"info");const o=await postAPI("/api/manual/scan/vulnerability",{ip:a?"all":n});o.success?(addConsoleMessage("Vulnerability scan triggered successfully","success"),e&&(e.textContent=`Vulnerability scan initiated for ${s} - Check Threat Intel tab in a few minutes`,e.className="text-sm text-green-600 mt-4",setTimeout(()=>{e&&e.classList.add("hidden")},4e3))):(addConsoleMessage(`Failed to trigger vulnerability scan: ${o.message||"Unknown error"}`,"error"),e&&(e.textContent=`Error: ${o.message||"Failed to trigger vulnerability scan"}`,e.className="text-sm text-red-600 mt-4"))}catch(t){console.error("Error triggering vulnerability scan:",t),addConsoleMessage("Failed to trigger vulnerability scan","error"),e&&(e.textContent=`Error: ${t.message}`,e.className="text-sm text-red-600 mt-4")}}async function runManualLynisPentest(){const e=document.getElementById("manual-lynis-ip"),t=document.getElementById("manual-lynis-username"),n=document.getElementById("manual-lynis-password"),a=document.getElementById("manual-lynis-status"),s=document.getElementById("manual-lynis-status-message"),o=document.getElementById("manual-lynis-btn");if(!e||!t||!n)return void addConsoleMessage("Manual Lynis form is missing elements","error");const r=e.value.trim(),i=t.value.trim(),l=n.value,c=(e,t="info")=>{if(!a||!s)return;const n={success:"border-green-500/40 bg-green-900/30 text-green-200",error:"border-red-500/50 bg-red-900/40 text-red-200",info:"border-slate-700 bg-slate-900/70 text-gray-200"};a.classList.remove("hidden"),s.className=`rounded-lg px-4 py-3 text-sm ${n[t]||n.info}`,s.textContent=e};if(!r||!i||!l)return void c("IP, username, and password are required to run Lynis manually.","error");if(!isValidIPv4(r))return void c("Please enter a valid IPv4 address.","error");const d=document.getElementById("lynis-audit-status");d&&(d.classList.remove("hidden"),d.textContent="Initializing Lynis audit...",d.className="text-sm text-blue-600 mt-2"),o&&(o.disabled=!0,o.textContent="Starting audit...",o.classList.add("bg-blue-600","cursor-wait"),o.classList.remove("bg-red-600","hover:bg-red-700"));try{const e=await networkAwareFetch("/api/manual/pentest/lynis",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({ip:r,username:i,password:l})});let t={};try{t=await e.json()}catch(e){console.warn("Unable to parse manual Lynis response JSON",e)}if(!e.ok||t&&!1===t.success)throw new Error(t&&(t.error||t.message)||`Request failed (${e.status})`);const a=t&&t.message||`Lynis pentest initiated for ${r}`;return c(a,"success"),addConsoleMessage(a,"success"),void(n.value="")}catch(e){console.error("Error running manual Lynis pentest:",e),c(`Failed to start Lynis pentest: ${e.message}`,"error"),addConsoleMessage(`Manual Lynis error: ${e.message}`,"error"),o&&(o.disabled=!1,o.textContent="Run Lynis Pentest",o.classList.remove("bg-blue-600","cursor-wait"),o.classList.add("bg-red-600","hover:bg-red-700")),d&&(d.textContent=`Error: ${e.message}`,d.className="text-sm text-red-600 mt-2")}}const NETWORK_CONTEXT_PARAM="network";function resolveNetworkAwareEndpoint(e){if(!e||"string"!=typeof e)return e;const t=e.trim();if(!t)return e;const{network:n}=getSelectedDashboardNetworkKey()||{};if(!n)return e;const a=t.startsWith("/api/")||t.startsWith("api/");try{const a=new URL(t,window.location.origin);return a.origin===window.location.origin&&a.pathname.startsWith("/api/")?(a.searchParams.has("network")||a.searchParams.set("network",n),t.startsWith("http://")||t.startsWith("https://")?a.toString():`${a.pathname}${a.search}${a.hash}`):e}catch(s){if(!a)return e;console.warn("Unable to normalize endpoint for network context",e,s);const o=t.includes("?");if(t.includes("network="))return e;return`${t}${o?"&":"?"}network=${encodeURIComponent(n)}`}}function networkAwareFetch(e,t={}){const n=resolveNetworkAwareEndpoint(e);return fetch(n,t)}async function fetchAPI(e,t={}){try{const n=await networkAwareFetch(e,t);if(401===n.status)throw window.location.href="/login",new Error("Authentication required");if(!n.ok)throw new Error(`HTTP error! status: ${n.status}`);return await n.json()}catch(t){throw console.error(`Error fetching ${e}:`,t),t}}async function postAPI(e,t){try{const n=await networkAwareFetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(401===n.status)throw window.location.href="/login",new Error("Authentication required");if(!n.ok)throw new Error(`HTTP error! status: ${n.status}`);return await n.json()}catch(t){throw console.error(`Error posting to ${e}:`,t),t}}async function refreshDashboard(){try{const e=await fetchAPI("/api/status");updateDashboardStatus(e),await refreshDashboardStatsForCurrentSelection({forceRefresh:!0,fallbackData:e})}catch(e){console.error("Error refreshing dashboard:",e)}}function updateDashboardStatus(e){refreshDashboardStatsForCurrentSelection({fallbackData:e}).catch(()=>{updateDashboardStats(e)}),updateElement("Ragnar-status",e.ragnar_status||"IDLE"),updateElement("Ragnar-says",e.ragnar_says||"Hacking away...");const t="boolean"==typeof e.automation_enabled?e.automation_enabled:!Boolean(e.manual_mode),n=Boolean(e.manual_mode);let a="Auto",s="text-green-400 font-semibold";t?n&&(a="Manual",s="text-orange-400 font-semibold"):(a="Sleeping",s="text-purple-300 font-semibold"),updateElement("Ragnar-mode",a);const o=document.getElementById("Ragnar-mode");o&&(o.className=s),syncManualModeUI(n),updateAutomationToggleButton(t),updateConnectivityIndicator("wifi-status",e.wifi_connected,e.current_ssid,e.ap_mode_active),updateConnectivityIndicator("bluetooth-status",e.bluetooth_active),updateConnectivityIndicator("usb-status",e.usb_active),updateConnectivityIndicator("pan-status",e.pan_connected),updateLanIndicator(e),updatePrimaryConnectionCard(e),updateReleaseGateState(e.release_gate),updatePwnToggleAvailability(Boolean(e.headless_mode))}function updateAutomationToggleButton(e,t={}){const n=document.getElementById("automation-toggle-btn");if(!n)return;const{force:a=!1}=t;if("true"===n.dataset.busy&&!a)return void(n.dataset.pendingState=e?"enabled":"disabled");a&&delete n.dataset.pendingState;const s="w-full sm:w-auto px-4 py-2 rounded-lg font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-900";e?(n.textContent="Disable Automation",n.className=`${s} bg-orange-600 hover:bg-orange-500 text-white focus:ring-orange-500`,n.dataset.action="stop"):(n.textContent="Enable Automation",n.className=`${s} bg-green-600 hover:bg-green-500 text-white focus:ring-green-500`,n.dataset.action="start")}async function handleAutomationToggle(){const e=document.getElementById("automation-toggle-btn");if(!e||"true"===e.dataset.busy)return;const t=e.dataset.action||"stop",n="start"===t;e.dataset.busy="true",e.disabled=!0,e.classList.add("opacity-70"),e.textContent="start"===t?"Enabling...":"Disabling...";try{let e;e="start"===t?await startOrchestrator():await stopOrchestrator();updateAutomationToggleButton(e&&e.success?Boolean(e.automation_enabled):!n,{force:!0}),refreshDashboard().catch(()=>{})}catch(e){console.error("Automation toggle failed:",e),addConsoleMessage("Failed to toggle automation","error"),updateAutomationToggleButton(!n,{force:!0})}finally{if(e.dataset.busy="false",e.disabled=!1,e.classList.remove("opacity-70"),e.dataset.pendingState){const t="enabled"===e.dataset.pendingState;delete e.dataset.pendingState,updateAutomationToggleButton(t,{force:!0})}}}function updateElement(e,t){const n=document.getElementById(e);if(n){const e=String(t??"");n.textContent!==e&&(n.textContent=e)}}function scaleStatNumber(e,t,n={}){const a=document.getElementById(e);if(!a)return;const s={mediumDigits:3,largeDigits:4,baseClass:"text-3xl",mediumClass:"text-2xl",smallClass:"text-xl",...n},o=Number(t),r=Number.isFinite(o)?Math.trunc(o):0,i=Math.abs(r).toString().length;let l;l=i>=s.largeDigits?s.smallClass:i>=s.mediumDigits?s.mediumClass:s.baseClass,a.classList.contains(l)||(a.classList.remove(s.baseClass,s.mediumClass,s.smallClass),a.classList.add(l))}function updateConnectivityIndicator(e,t,n=null,a=!1){const s=document.getElementById(e);if(s){const e=t?"w-3 h-3 bg-green-500 rounded-full pulse-glow":"w-3 h-3 bg-gray-600 rounded-full";s.className!==e&&(s.className=e)}if("wifi-status"===e){const e=document.getElementById("wifi-ssid-display");if(e){let s,o;a?(s=n?`AP Mode: ${n}`:"AP Mode",o="text-xs text-blue-400 truncate"):t&&n?(s=n,o="text-xs text-gray-400 truncate"):(s="Not connected",o="text-xs text-gray-500 truncate"),e.textContent!==s&&(e.textContent=s),e.className!==o&&(e.className=o)}}}function updateLanIndicator(e){const t=document.getElementById("lan-status"),n=document.getElementById("lan-info-display");if(t||n)if(e.ethernet_connected){const a="w-3 h-3 bg-green-500 rounded-full pulse-glow";if(t&&t.className!==a&&(t.className=a),n){const t=e.ethernet_interface||"eth0",a=e.ethernet_ip||"",s=a?`${t}: ${a}`:t,o="text-xs text-green-400 truncate";n.textContent!==s&&(n.textContent=s),n.className!==o&&(n.className=o)}}else{const e="w-3 h-3 bg-gray-600 rounded-full";if(t&&t.className!==e&&(t.className=e),n){"Not connected"!==n.textContent&&(n.textContent="Not connected");const e="text-xs text-gray-500 truncate";n.className!==e&&(n.className=e)}}}let _lastPrimaryConnectionKey=null;function updatePrimaryConnectionCard(e){const t=document.getElementById("primary-connection-label"),n=document.getElementById("primary-connection-name"),a=document.getElementById("primary-connection-ip"),s=document.getElementById("primary-connection-status"),o=document.getElementById("primary-connection-icon");if(!t)return;let r;if(r=e.ethernet_connected?`lan:${e.ethernet_interface||""}:${e.ethernet_ip||""}`:e.wifi_connected?`wifi:${e.current_ssid||""}:${e.ap_mode_active?1:0}:${e.ap_ssid||""}`:e.pan_connected?"pan":e.bluetooth_active?"bt":"none",r===_lastPrimaryConnectionKey)return;_lastPrimaryConnectionKey=r;if(e.ethernet_connected)t.textContent="LAN",n.textContent=e.ethernet_interface||"Ethernet",a&&(a.textContent=e.ethernet_ip||""),s&&(s.className="w-3 h-3 bg-green-500 rounded-full pulse-glow"),o&&(o.innerHTML='<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2"></path></svg>',o.className="text-green-400");else if(e.wifi_connected){const r=e.current_ssid||"Connected";t.textContent=e.ap_mode_active?"AP Mode":"WiFi",n.textContent=e.ap_mode_active?`AP: ${e.ap_ssid||"Ragnar"}`:r,a&&(a.textContent=""),s&&(s.className="w-3 h-3 bg-green-500 rounded-full pulse-glow"),o&&(o.innerHTML='<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0"></path></svg>',o.className="text-green-400")}else e.pan_connected?(t.textContent="USB/PAN",n.textContent="Connected via USB",a&&(a.textContent=""),s&&(s.className="w-3 h-3 bg-green-500 rounded-full pulse-glow"),o&&(o.className="text-yellow-400")):e.bluetooth_active?(t.textContent="Bluetooth",n.textContent="Bluetooth active",a&&(a.textContent=""),s&&(s.className="w-3 h-3 bg-blue-500 rounded-full pulse-glow"),o&&(o.className="text-blue-400")):(t.textContent="Disconnected",n.textContent="No active connection",a&&(a.textContent=""),s&&(s.className="w-3 h-3 bg-gray-600 rounded-full"),o&&(o.className="text-gray-500"))}const MAX_CONSOLE_LINES=200,CONSOLE_NOISE_PATTERNS=["comment.py - INFO - Comments loaded successfully from cache"],HISTORY_LOG_TYPE_COLORS={success:"text-green-400",error:"text-red-400",warning:"text-yellow-400",info:"text-gray-300"};let consoleBuffer=[],lastConsoleLogLine=null;function addConsoleMessage(e,t="info"){const n={success:"text-green-400",error:"text-red-400",warning:"text-yellow-400",info:"text-blue-400"},a={timestamp:(new Date).toLocaleTimeString(),message:e,type:t,colorClass:n[t]||n.info};consoleBuffer.push(a),consoleBuffer.length>MAX_CONSOLE_LINES&&(consoleBuffer=consoleBuffer.slice(-MAX_CONSOLE_LINES)),updateConsoleDisplay()}function shouldHideConsoleLog(e){return CONSOLE_NOISE_PATTERNS.some(t=>e.includes(t))}function determineConsoleLogType(e){if(!e)return"info";const t=e.toLowerCase();return t.includes("error")?"error":t.includes("warn")?"warning":t.includes("success")?"success":"info"}const LOG_TIMESTAMP_PATTERN=/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;function extractLogTimestamp(e){if(!e||e.length<19)return(new Date).toLocaleTimeString();const t=e.slice(0,19);if(LOG_TIMESTAMP_PATTERN.test(t)){const e=new Date(t.replace(" ","T"));if(!Number.isNaN(e.getTime()))return e.toLocaleTimeString()}return(new Date).toLocaleTimeString()}function createConsoleEntryFromLog(e){const t=determineConsoleLogType(e);return{timestamp:extractLogTimestamp(e),message:e,type:t,colorClass:HISTORY_LOG_TYPE_COLORS[t]||HISTORY_LOG_TYPE_COLORS.info}}function updateConsole(e){if(!e||!Array.isArray(e))return void(0===consoleBuffer.length&&(addConsoleMessage("No historical logs available","warning"),addConsoleMessage("New activity will appear here as it occurs","info")));if(0===e.length)return void(0===consoleBuffer.length&&(addConsoleMessage("No recent activity logged","info"),addConsoleMessage("Waiting for new events...","info")));const t=e.map(e=>"string"==typeof e?e.trim():"").filter(e=>e&&!shouldHideConsoleLog(e));if(0===t.length)return;let n=[];if(lastConsoleLogLine){const e=t.lastIndexOf(lastConsoleLogLine);if(e===t.length-1)return void(lastConsoleLogLine=t[t.length-1]);-1!==e?n=t.slice(e+1):(consoleBuffer=[],n=t.slice(-MAX_CONSOLE_LINES))}else consoleBuffer=[],n=t.slice(-MAX_CONSOLE_LINES);0!==n.length?(n.forEach(e=>{consoleBuffer.push(createConsoleEntryFromLog(e))}),consoleBuffer.length>MAX_CONSOLE_LINES&&(consoleBuffer=consoleBuffer.slice(-MAX_CONSOLE_LINES)),lastConsoleLogLine=t[t.length-1],updateConsoleDisplay()):lastConsoleLogLine=t[t.length-1]}function updateConsoleDisplay(){const e=document.getElementById("console-output");e&&(e.innerHTML=consoleBuffer.map(e=>`<div class="${e.colorClass}">[${e.timestamp}] ${escapeHtml(e.message)}</div>`).join(""),e.scrollTop=e.scrollHeight)}function clearConsole(){consoleBuffer=[];const e=document.getElementById("console-output");e&&(e.innerHTML='<div class="text-green-400">Console cleared</div>')}function escapeHtml(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML}function displayNetworkTable(e){const t=document.getElementById("network-table"),n=document.getElementById("network-hosts-table");if(!t||!n)return;n.querySelectorAll("tr[data-ip]").forEach(e=>{const t=e.getAttribute("data-ip");t&&saveDeepScanButtonState(t)}),n.innerHTML="";const a=Array.isArray(e)?e:e&&Array.isArray(e.hosts)?e.hosts:[];if(!a||0===a.length)return n.innerHTML='\n <tr>\n <td colspan="8" class="text-center py-8 text-gray-400">\n No network data available. Start a scan to discover hosts.\n </td>\n </tr>\n ',void updateHostCountDisplay();a.forEach(e=>{const t=normalizeHostRecord(e);if(!t)return;const a=document.createElement("tr");a.setAttribute("data-ip",t.ip),a.className="border-b border-slate-700 hover:bg-slate-700/50 transition-colors",a.innerHTML=renderHostRow(t),n.appendChild(a),restoreDeepScanButtonState(t.ip)}),updateHostCountDisplay(),cleanupOldDeepScanStates()}function displayCredentialsTable(e){const t=document.getElementById("credentials-table");if(!t)return;if(!e||0===Object.keys(e).length)return void(t.innerHTML='<p class="text-gray-400">No credentials discovered yet</p>');let n='<div class="space-y-6">';Object.entries(e).forEach(([e,t])=>{t&&t.length>0&&(n+=`\n <div class="bg-gray-800 rounded-lg p-4">\n <h3 class="text-lg font-semibold text-Ragnar-400 mb-3">${e.toUpperCase()} (${t.length})</h3>\n <div class="overflow-x-auto">\n <table class="min-w-full divide-y divide-gray-700">\n <thead>\n <tr>\n <th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase">Target</th>\n <th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase">Username</th>\n <th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase">Password</th>\n </tr>\n </thead>\n <tbody class="divide-y divide-gray-700">\n `,t.forEach(e=>{n+=`\n <tr class="hover:bg-gray-700 transition-colors">\n <td class="px-4 py-2 text-sm text-white">${e.ip||"N/A"}</td>\n <td class="px-4 py-2 text-sm text-green-400 font-mono">${e.username||"N/A"}</td>\n <td class="px-4 py-2 text-sm text-yellow-400 font-mono">${e.password||"N/A"}</td>\n </tr>\n `}),n+="\n </tbody>\n </table>\n </div>\n </div>\n ")}),n+="</div>",t.innerHTML='<div class="space-y-6"></div>'===n?'<p class="text-gray-400">No credentials discovered yet</p>':n}function displayLootTable(e){const t=document.getElementById("loot-table");if(!t)return;if(!e||0===e.length)return void(t.innerHTML='<p class="text-gray-400">No loot data available</p>');const n=e.length>6,a=e.slice(0,6),s=n?e.slice(6):[];function o(e){const t=escapeHtml(e.filename||"Unknown File"),n=escapeHtml(e.size||"N/A"),a=escapeHtml(e.source||"Unknown"),s=escapeHtml(e.timestamp||"Unknown"),o=e.path?encodeURIComponent(e.path):"";return`\n <button type="button" class="${"bg-gray-800 rounded-lg p-4 text-left hover:bg-gray-700 transition-colors w-full "+(o?"":"opacity-60 cursor-not-allowed")}" ${o?`onclick="openLootFile('${o}')"`:'disabled aria-disabled="true"'}>\n <div class="flex items-center justify-between mb-2">\n <h3 class="text-lg font-semibold text-Ragnar-400 truncate" title="${t}">${t}</h3>\n <span class="text-xs text-gray-400 ml-2">${n}</span>\n </div>\n <div class="space-y-2 text-sm text-gray-300">\n <p><span class="text-gray-400">Source:</span> ${a}</p>\n <p><span class="text-gray-400">Timestamp:</span> ${s}</p>\n </div>\n ${o?'<p class="text-xs text-Ragnar-400 mt-3">Open in Files →</p>':""}\n </button>\n `}let r='<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" id="loot-preview-items">';a.forEach(e=>{r+=o(e)}),r+="</div>",n&&(r+=`\n <div class="mt-4">\n <button onclick="toggleLootExpansion()" \n class="flex items-center justify-center w-full py-3 px-4 bg-gray-800 hover:bg-gray-700 rounded-lg transition-colors text-gray-300 hover:text-white">\n <span id="loot-expand-text">Show ${s.length} more items</span>\n <svg id="loot-expand-arrow" class="w-4 h-4 ml-2 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>\n </svg>\n </button>\n <div id="loot-hidden-items" class="hidden mt-4">\n <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">\n `,s.forEach(e=>{r+=o(e)}),r+="\n </div>\n </div>\n </div>\n "),t.innerHTML=r}function toggleLootExpansion(){const e=document.getElementById("loot-hidden-items"),t=document.getElementById("loot-expand-text"),n=document.getElementById("loot-expand-arrow");if(!e||!t||!n)return;if(e.classList.contains("hidden"))e.classList.remove("hidden"),t.textContent="Show less",n.style.transform="rotate(180deg)";else{e.classList.add("hidden");const a=e.querySelectorAll("button").length;t.textContent=`Show ${a} more items`,n.style.transform="rotate(0deg)"}}function openLootFile(e){if(e)try{const t=decodeURIComponent(e);if(!t||"/data_stolen"===t)return void showNotification("Unable to locate that file. It may have been moved or deleted.","warning");const n=t.lastIndexOf("/");if(-1===n)return void showNotification("Invalid file path received for loot item.","error");const a=0===n?"/":t.substring(0,n),s=t.substring(n+1);if(!s)return void showNotification("Unable to determine file name for loot item.","error");pendingFileHighlight={directory:a,file:s},showTab("files"),loadFiles(a,s),showNotification(`Opening ${s} in Files tab`,"info")}catch(e){console.error("Failed to open loot file:",e),showNotification("Failed to open file from loot item.","error")}else showNotification("No file path available for this loot item.","warning")}function displayConfigForm(e){const t=document.getElementById("config-form");let n='<div class="space-y-6"><form id="config-update-form">';const a={General:["manual_mode","debug_mode","scan_vuln_running","scan_vuln_no_ports","enable_attacks","blacklistcheck"],Network:["network_max_failed_pings"],Timing:["startup_delay","web_delay","screen_delay","scan_interval"],Display:["epd_type","screen_reversed","spi_clock_mhz","gc9a01_mascot_color","ssd1306_i2c_address","lcd1602_i2c_address","max7219_spi_port","max7219_spi_device","max7219_block_orientation","display_brightness"]},s=["manual_mode","debug_mode","scan_vuln_running","scan_vuln_no_ports","enable_attacks","blacklistcheck","wardriving_enabled","wardriving_display","wardriving_auto_export"],o=new Set(["network_max_failed_pings","gc9a01_mascot_color","ssd1306_i2c_address","lcd1602_i2c_address","spi_clock_mhz","max7219_spi_port","max7219_spi_device","max7219_block_orientation","display_brightness","wardriving_scan_interval","wardriving_gps_port","wardriving_gps_baudrate"]),r={network_max_failed_pings:15,gc9a01_mascot_color:"#96C8FF",ssd1306_i2c_address:"0x3C",lcd1602_i2c_address:"0x27",spi_clock_mhz:2,max7219_spi_port:0,max7219_spi_device:0,max7219_block_orientation:-90,display_brightness:8,wardriving_scan_interval:2,wardriving_gps_port:"auto",wardriving_gps_baudrate:9600},i={scan_vuln_running:"handleVulnScanToggle(this)",enable_attacks:"handleEnableAttacksToggle(this)"};for(const[t,l]of Object.entries(a))n+=`\n <div class="bg-slate-800 bg-opacity-50 rounded-lg p-4">\n <h3 class="text-lg font-bold mb-4 text-Ragnar-400">${t}</h3>\n <div class="grid grid-cols-1 md:grid-cols-2 gap-4">\n `,l.forEach(t=>{const a=Object.prototype.hasOwnProperty.call(e,t);let l=e[t];if(!a&&s.includes(t)&&(l="manual_mode"!==t),!a&&o.has(t)&&(l=r[t]),a||s.includes(t)||o.has(t)){const a=displaySelectOptions[t],s="boolean"==typeof l?"checkbox":"text",o=getConfigLabel(t),r=escapeHtml(getConfigDescription(t));if(Array.isArray(a)){let e="boolean"==typeof l?String(l):l??"";"epd_type"===t&&(e=epdTypeToSizeKey(e)),"screen_reversed"===t&&("true"===e?e="180":"false"===e&&(e="0")),n+=`\n <div class="space-y-2">\n <label class="flex items-center gap-2 text-sm text-gray-400">\n ${o}\n <span class="info-icon" tabindex="0" role="button" aria-label="${r}" data-tooltip="${r}">ⓘ</span>\n </label>\n <select name="${t}" class="w-full px-4 py-2 rounded-lg bg-slate-700 border border-slate-600 focus:border-Ragnar-500 focus:ring-1 focus:ring-Ragnar-500">\n ${a.map(t=>`<option value="${t.value}" ${t.value===String(e)?"selected":""}>${t.label}</option>`).join("")}\n </select>\n </div>\n `}else if("checkbox"===s){const a="scan_vuln_no_ports"!==t||e.scan_vuln_running?"":"disabled";n+=`\n <label class="flex items-center space-x-3 p-3 rounded-lg hover:bg-slate-700 hover:bg-opacity-50 transition-colors cursor-pointer ${a?"opacity-50 cursor-not-allowed":""}">\n <input type="checkbox" name="${t}" ${l?"checked":""} ${a}\n class="w-5 h-5 rounded bg-slate-700 border-slate-600 text-Ragnar-500 focus:ring-Ragnar-500"\n ${i[t]?`onchange="${i[t]}"`:""}>\n <span class="flex items-center gap-2">\n ${o}\n <span class="info-icon" tabindex="0" role="button" aria-label="${r}" data-tooltip="${r}">ⓘ</span>\n </span>\n </label>\n `}else if("gc9a01_mascot_color"===t){const e=l&&"string"==typeof l&&l.startsWith("#")?l:"#96C8FF";n+=`\n <div class="space-y-2" id="cfg-gc9a01-color-row">\n <label class="flex items-center gap-2 text-sm text-gray-400">\n ${o}\n <span class="info-icon" tabindex="0" role="button" aria-label="${r}" data-tooltip="${r}">ⓘ</span>\n </label>\n <div class="flex items-center gap-3">\n <input type="color" name="${t}" value="${e}"\n class="h-9 w-16 cursor-pointer rounded border border-slate-600 bg-slate-700 p-1">\n <span class="text-xs text-gray-500">Mascot tint on GC9A01 round display</span>\n </div>\n </div>\n `}else if("ssd1306_i2c_address"===t){n+=`\n <div class="space-y-2" id="cfg-ssd1306-addr-row">\n <label class="flex items-center gap-2 text-sm text-gray-400">\n ${o}\n <span class="info-icon" tabindex="0" role="button" aria-label="${r}" data-tooltip="${r}">ⓘ</span>\n </label>\n <input type="text" name="${t}" id="cfg-ssd1306-addr-input"\n class="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-sm font-mono"\n value="${l&&"string"==typeof l?l:"0x3C"}" placeholder="0x3C or 0x3D">\n <p class="text-xs text-gray-500">Most modules use 0x3C. Use 0x3D if display doesn't initialize.</p>\n </div>\n `}else if("lcd1602_i2c_address"===t){n+=`\n <div class="space-y-2" id="cfg-lcd1602-addr-row">\n <label class="flex items-center gap-2 text-sm text-gray-400">\n ${o}\n <span class="info-icon" tabindex="0" role="button" aria-label="${r}" data-tooltip="${r}">ⓘ</span>\n </label>\n <input type="text" name="${t}" id="cfg-lcd1602-addr-input"\n class="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-sm font-mono"\n value="${l&&"string"==typeof l?l:"0x27"}" placeholder="0x27 or 0x3F">\n <p class="text-xs text-gray-500">Most PCF8574 backpacks use 0x27. Address is auto-detected if unreachable.</p>\n </div>\n `}else if("max7219_spi_port"===t){n+=`\n <div class="space-y-2" id="cfg-max7219-spi-port-row">\n <label class="flex items-center gap-2 text-sm text-gray-400">\n ${o}\n <span class="info-icon" tabindex="0" role="button" aria-label="${r}" data-tooltip="${r}">ⓘ</span>\n </label>\n <input type="number" name="${t}" id="cfg-max7219-spi-port-input"\n class="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-sm font-mono"\n value="${null!=l?l:0}" min="0" max="1">\n <p class="text-xs text-gray-500">SPI bus (0 = SPI0, 1 = SPI1). Default: 0.</p>\n </div>\n `}else if("max7219_block_orientation"===t){n+=`\n <div class="space-y-2" id="cfg-max7219-block-row">\n <label class="flex items-center gap-2 text-sm text-gray-400">\n ${o}\n <span class="info-icon" tabindex="0" role="button" aria-label="${r}" data-tooltip="${r}">ⓘ</span>\n </label>\n <input type="number" name="${t}" id="cfg-max7219-block-input"\n class="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-sm font-mono"\n value="${null!=l?l:-90}" step="90">\n <p class="text-xs text-gray-500">Rotation of each 8×8 block in degrees (0, 90, -90, 180). Try -90 if display looks sideways.</p>\n </div>\n `}else if("display_brightness"===t){n+=`\n <div class="space-y-2" id="cfg-display-brightness-row">\n <label class="flex items-center gap-2 text-sm text-gray-400">\n ${o}\n <span class="info-icon" tabindex="0" role="button" aria-label="${r}" data-tooltip="${r}">ⓘ</span>\n </label>\n <input type="number" name="${t}" id="cfg-display-brightness-input"\n class="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-sm font-mono"\n value="${null!=l?l:8}" min="0" max="15">\n <p class="text-xs text-gray-500">Brightness 0–15. Applies to MAX7219, SSD1306, GC9A01. Default: 8.</p>\n </div>\n `}else if("spi_clock_mhz"===t){n+=`\n <div class="space-y-2" id="cfg-spi-clock-row">\n <label class="flex items-center gap-2 text-sm text-gray-400">\n ${o}\n <span class="info-icon" tabindex="0" role="button" aria-label="${r}" data-tooltip="${r}">ⓘ</span>\n </label>\n <input type="number" name="${t}" id="cfg-spi-clock-input"\n class="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-sm font-mono"\n value="${null!=l?l:2}" min="0.5" max="4" step="0.5">\n </div>\n `}else n+=`\n <div class="space-y-2">\n <label class="flex items-center gap-2 text-sm text-gray-400">\n ${o}\n <span class="info-icon" tabindex="0" role="button" aria-label="${r}" data-tooltip="${r}">ⓘ</span>\n </label>\n <input type="${s}" name="${t}" value="${l}"\n class="w-full px-4 py-2 rounded-lg bg-slate-700 border border-slate-600 focus:border-Ragnar-500 focus:ring-1 focus:ring-Ragnar-500">\n </div>\n `}}),n+="</div></div>";n+='\n <button type="submit" class="w-full bg-Ragnar-600 hover:bg-Ragnar-700 text-white font-bold py-3 px-6 rounded-lg transition-colors">\n Save Configuration\n </button>\n </form></div>',t.innerHTML=n,document.getElementById("config-update-form").addEventListener("submit",async e=>{e.preventDefault(),await saveConfig(e.target)});const l=document.querySelector('select[name="epd_type"]'),c=document.getElementById("cfg-gc9a01-color-row"),d=document.getElementById("cfg-ssd1306-addr-row"),u=document.getElementById("cfg-lcd1602-addr-row"),p=document.getElementById("cfg-max7219-spi-port-row"),g=document.getElementById("cfg-max7219-spi-device-row"),m=document.getElementById("cfg-max7219-block-row"),f=document.getElementById("cfg-display-brightness-row"),h=document.getElementById("cfg-spi-clock-row");function y(){const e=l?l.value:"",t="max7219_8panel"===e||"max7219_4panel"===e,n="0in96_oled"===e,a="1in28_tft"===e,s="lcd1602"===e,o=!(t||n||a||s);c&&(c.style.display=a?"":"none"),d&&(d.style.display=n?"":"none"),u&&(u.style.display=s?"":"none"),p&&(p.style.display=t?"":"none"),g&&(g.style.display=t?"":"none"),m&&(m.style.display=t?"":"none"),f&&(f.style.display=t||n||a?"":"none"),h&&(h.style.display=o?"":"none")}l&&(l.addEventListener("change",y),y());updateAttackWarningBanner(!e.hasOwnProperty("enable_attacks")||Boolean(e.enable_attacks));const w=document.getElementById("wardriving-config-slot");if(w){let t='<form id="wardriving-config-form" class="bg-slate-800 bg-opacity-50 rounded-lg p-4 mt-4"><h4 class="text-md font-bold mb-4 text-gray-300">Settings</h4><div class="grid grid-cols-1 md:grid-cols-2 gap-4">';["wardriving_scan_interval","wardriving_gps_port","wardriving_gps_baudrate","wardriving_auto_export"].forEach(n=>{const a=Object.prototype.hasOwnProperty.call(e,n);let i=e[n];if(!a&&s.includes(n)&&(i=!0),!a&&o.has(n)&&(i=r[n]),a||s.includes(n)||o.has(n)){const e=getConfigLabel(n),a=escapeHtml(getConfigDescription(n));t+="boolean"==typeof i?`<label class="flex items-center space-x-3 p-3 rounded-lg hover:bg-slate-700 hover:bg-opacity-50 transition-colors cursor-pointer"><input type="checkbox" name="${n}" ${i?"checked":""} class="w-5 h-5 rounded bg-slate-700 border-slate-600 text-Ragnar-500 focus:ring-Ragnar-500"><span class="flex items-center gap-2">${e}<span class="info-icon" tabindex="0" role="button" aria-label="${a}" data-tooltip="${a}">ⓘ</span></span></label>`:`<div class="space-y-2"><label class="flex items-center gap-2 text-sm text-gray-400">${e}<span class="info-icon" tabindex="0" role="button" aria-label="${a}" data-tooltip="${a}">ⓘ</span></label><input type="text" name="${n}" value="${i??""}" class="w-full px-4 py-2 rounded-lg bg-slate-700 border border-slate-600 focus:border-Ragnar-500 focus:ring-1 focus:ring-Ragnar-500"></div>`}}),t+='</div><button type="submit" class="w-full mt-4 bg-Ragnar-600 hover:bg-Ragnar-700 text-white font-bold py-2 px-4 rounded-lg transition-colors">Save Wardriving Settings</button></form>',w.innerHTML=t,document.getElementById("wardriving-config-form").addEventListener("submit",async e=>{e.preventDefault(),await saveConfig(e.target)})}}function handleVulnScanToggle(e){const t=document.querySelector('input[name="scan_vuln_no_ports"]'),n=t?t.closest("label"):null;t&&(t.disabled=!e.checked,n&&(e.checked?n.classList.remove("opacity-50","cursor-not-allowed"):n.classList.add("opacity-50","cursor-not-allowed")))}function updateAttackWarningBanner(e){const t=document.getElementById("attack-warning");t&&(e?t.classList.remove("hidden"):t.classList.add("hidden"))}function handleEnableAttacksToggle(e){if(e.checked){if(!confirm("Warning: enabling automated attacks will run offensive actions (bruteforce, credential reuse, file theft) against discovered hosts. Do you have authorization to continue?"))return e.checked=!1,updateAttackWarningBanner(!1),addConsoleMessage("Automated attacks remain disabled.","warning"),void showNotification("Automated attacks remain disabled.","info");showNotification("Automated attacks are enabled. Ensure you are authorized before proceeding.","warning"),addConsoleMessage("Automated attacks enabled. Ragnar will launch offensive actions on discovered hosts.","warning")}else addConsoleMessage("Automated attacks disabled.","info");updateAttackWarningBanner(e.checked)}async function saveConfig(e){const t=new FormData(e),n={},a=e.querySelectorAll('input[type="checkbox"]');a.forEach(e=>{n[e.name]=!1});for(const[a,s]of t.entries()){const t=e.elements[a];"checkbox"===t.type?n[a]=t.checked:"true"===s||"false"===s?n[a]="true"===s:isNaN(s)||""===s?n[a]=s:n[a]=Number(s)}a.forEach(e=>{n[e.name]=e.checked}),console.log("Saving config:",n);try{await postAPI("/api/config",n);addConsoleMessage("Configuration saved successfully","success"),n.hasOwnProperty("manual_mode")&&setTimeout(()=>{refreshDashboard()},500)}catch(e){console.error("Config save error:",e),addConsoleMessage("Failed to save configuration","error")}}async function loadAIConfiguration(e){const t=document.getElementById("ai-enabled-toggle");if(t){const n=!(!e||!Object.prototype.hasOwnProperty.call(e,"ai_enabled"))&&Boolean(e.ai_enabled);t.checked=n}try{const e=await fetchAPI("/api/ai/token"),t=document.getElementById("openai-api-token");t&&(e.configured&&e.token_preview?(t.value="",t.placeholder=`Configured: ${e.token_preview}`):(t.value="",t.placeholder="sk-..."))}catch(e){console.error("Failed to fetch AI token status:",e)}}async function toggleAIEnabled(){const e=document.getElementById("ai-enabled-toggle"),t=document.getElementById("ai-config-status"),n=document.getElementById("ai-config-status-message");if(!e||!t||!n)return;const a=e.checked,s={ai_enabled:a};try{const e=await postAPI("/api/config",s);if(a&&e&&!1===e.ai_reload_success)throw new Error(e.ai_reload_error||"AI engine failed to initialize. Check server logs.");t.className=a?"p-3 rounded-lg text-sm bg-green-900/30 border border-green-700":"p-3 rounded-lg text-sm bg-blue-900/30 border border-blue-700",n.textContent=a?"✓ AI Insights enabled. Ragnar will request GPT analysis for dashboards.":"ℹ AI Insights disabled. Ragnar will stop requesting GPT analysis until re-enabled.",t.classList.remove("hidden"),setTimeout(()=>{t.classList.add("hidden")},4e3),"dashboard"===currentTab&&setTimeout(()=>{loadAIInsights().catch(e=>console.error("Failed to refresh AI insights after toggle:",e))},500)}catch(s){console.error("Failed to toggle AI insights:",s),e.checked=!a,t.className="p-3 rounded-lg text-sm bg-red-900/30 border border-red-700",n.textContent=`✗ Failed to ${a?"enable":"disable"} AI Insights (${s.message||"unknown error"})`,t.classList.remove("hidden"),setTimeout(()=>{t.classList.add("hidden")},5e3)}}async function saveAIToken(){const e=document.getElementById("openai-api-token"),t=document.getElementById("ai-config-status"),n=document.getElementById("ai-config-status-message"),a=e.value.trim();if(!a)return t.className="p-3 rounded-lg text-sm bg-yellow-900/30 border border-yellow-700",n.textContent="⚠ Please enter an API token.",t.classList.remove("hidden"),void setTimeout(()=>t.classList.add("hidden"),3e3);try{const e=await postAPI("/api/ai/token",{token:a});if(!e.success)throw new Error(e.message||"Failed to save token");{t.className="p-3 rounded-lg text-sm bg-green-900/30 border border-green-700";let a=e.message||"✓ API token saved to .bashrc successfully. AI features are now ready to use.";e.user&&(a+=` (User: ${e.user})`),n.textContent=a,t.classList.remove("hidden"),addConsoleMessage("OpenAI API token saved to environment variable","success");const s=await fetchAPI("/api/config");await loadAIConfiguration(s),setTimeout(()=>{t.classList.add("hidden")},8e3),"dashboard"===currentTab&&setTimeout(()=>refreshDashboard(),500)}}catch(e){console.error("Failed to save AI token:",e),t.className="p-3 rounded-lg text-sm bg-red-900/30 border border-red-700",n.textContent=`✗ Failed to save API token: ${e.message||"Please try again."}`,t.classList.remove("hidden")}}async function loadPushoverConfiguration(e){const t=document.getElementById("pushover-enabled-toggle");t&&(t.checked=Boolean(e&&e.pushover_enabled));const n={"pushover-notify-new-device":"pushover_notify_new_device","pushover-notify-new-vuln":"pushover_notify_new_vulnerability","pushover-notify-new-cred":"pushover_notify_new_credential","pushover-notify-device-lost":"pushover_notify_device_lost","pushover-notify-device-back-online":"pushover_notify_device_back_online"};for(const[t,a]of Object.entries(n)){const n=document.getElementById(t);n&&(n.checked=!(!e||!Object.prototype.hasOwnProperty.call(e,a))&&Boolean(e[a]))}try{const e=await fetchAPI("/api/pushover/keys"),t=document.getElementById("pushover-user-key"),n=document.getElementById("pushover-api-token");t&&(t.value="",t.placeholder=e.user_key_configured?`Configured: ${e.user_key_preview||"••••"}`:"Your user key..."),n&&(n.value="",n.placeholder=e.api_token_configured?`Configured: ${e.api_token_preview||"••••"}`:"Your app token...")}catch(e){console.error("Failed to fetch Pushover key status:",e)}}async function togglePushoverEnabled(){const e=document.getElementById("pushover-enabled-toggle");if(e)try{await postAPI("/api/config",{pushover_enabled:e.checked}),showPushoverStatus(e.checked?"✓ Pushover notifications enabled":"ℹ Pushover notifications disabled",e.checked?"green":"blue")}catch(t){e.checked=!e.checked,showPushoverStatus("✗ Failed to toggle Pushover: "+(t.message||"unknown error"),"red")}}async function savePushoverKeys(){const e=document.getElementById("pushover-user-key"),t=document.getElementById("pushover-api-token"),n=e?e.value.trim():"",a=t?t.value.trim():"";if(n||a)try{const e={};n&&(e.user_key=n),a&&(e.api_token=a);const t=await postAPI("/api/pushover/keys",e);if(!t.success)throw new Error(t.message||"Save failed");{showPushoverStatus("✓ Pushover keys saved successfully!","green"),addConsoleMessage("Pushover keys saved","success");const e=await fetchAPI("/api/config");await loadPushoverConfiguration(e)}}catch(e){showPushoverStatus("✗ Failed to save keys: "+(e.message||"unknown error"),"red")}else showPushoverStatus("⚠ Please enter at least one key.","yellow")}async function savePushoverTriggers(){const e={"pushover-notify-new-device":"pushover_notify_new_device","pushover-notify-new-vuln":"pushover_notify_new_vulnerability","pushover-notify-new-cred":"pushover_notify_new_credential","pushover-notify-device-lost":"pushover_notify_device_lost","pushover-notify-device-back-online":"pushover_notify_device_back_online"},t={};for(const[n,a]of Object.entries(e)){const e=document.getElementById(n);e&&(t[a]=e.checked)}try{await postAPI("/api/config",t),showPushoverStatus("✓ Notification triggers updated","green",2e3)}catch(e){showPushoverStatus("✗ Failed to update triggers: "+(e.message||"unknown"),"red")}}async function testPushover(){showPushoverStatus("Sending test notification...","blue",0);try{const e=await postAPI("/api/pushover/test",{});if(!e.success)throw new Error(e.message||"Send failed");showPushoverStatus("✓ Test notification sent! Check your device.","green",5e3),addConsoleMessage("Pushover test notification sent","success")}catch(e){showPushoverStatus("✗ Test failed: "+(e.message||"unknown error"),"red",6e3)}}function showPushoverStatus(e,t,n){const a=document.getElementById("pushover-config-status"),s=document.getElementById("pushover-config-status-message");if(!a||!s)return;const o={green:"bg-green-900/30 border border-green-700",red:"bg-red-900/30 border border-red-700",yellow:"bg-yellow-900/30 border border-yellow-700",blue:"bg-blue-900/30 border border-blue-700"};a.className="p-3 rounded-lg text-sm "+(o[t]||o.blue),s.textContent=e,a.classList.remove("hidden"),0!==n&&setTimeout(()=>a.classList.add("hidden"),n||4e3)}async function loadEpaperDisplay(){try{const e=await fetchAPI("/api/epaper-display");if(updateElement("epaper-status-1",e.status_text||"Unknown"),updateElement("epaper-status-2",e.status_text2||"Unknown"),e.timestamp){updateElement("epaper-timestamp",new Date(1e3*e.timestamp).toLocaleString())}const t=document.getElementById("epaper-display-image"),n=document.getElementById("epaper-loading"),a=document.getElementById("epaper-connection");e.image?(t.src=e.image,t.style.display="block",n.style.display="none",e.width&&e.height&&updateElement("epaper-resolution",`${e.width} x ${e.height}`),a.textContent="Live",a.className="text-green-400 font-medium"):(t.style.display="none",n.style.display="flex",n.innerHTML=`\n <div class="text-center text-gray-600">\n <svg class="h-8 w-8 mx-auto mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>\n </svg>\n <p>${e.message||"No display image available"}</p>\n </div>\n `,a.textContent="Offline",a.className="text-red-400 font-medium")}catch(e){console.error("Error loading display:",e),addConsoleMessage("Failed to load display","error");const t=document.getElementById("epaper-connection");t.textContent="Error",t.className="text-red-400 font-medium"}}function refreshEpaperDisplay(){addConsoleMessage("Refreshing display...","info"),loadEpaperDisplay()}let epaperSizeMode="large";function toggleEpaperSize(){const e=document.getElementById("epaper-display-image");"large"===epaperSizeMode?(e.style.maxHeight="1200px",e.style.minHeight="600px",epaperSizeMode="xlarge",addConsoleMessage("Display size: Extra Large","info")):"xlarge"===epaperSizeMode?(e.style.maxHeight="600px",e.style.minHeight="300px",epaperSizeMode="medium",addConsoleMessage("Display size: Medium","info")):(e.style.maxHeight="800px",e.style.minHeight="400px",epaperSizeMode="large",addConsoleMessage("Display size: Large","info"))}function setupEpaperAutoRefresh(){setInterval(()=>{"epaper"===currentTab&&loadEpaperDisplay()},5e3)}let systemMonitoringInterval,currentDirectory="/",fileOperationInProgress=!1,currentFileSort="name",currentFileSearch="";function setFileSort(e){currentFileSort=e,document.querySelectorAll(".file-sort-btn").forEach(t=>{const n=t.getAttribute("data-sort")===e;t.classList.toggle("bg-Ragnar-600",n),t.classList.toggle("text-white",n),t.classList.toggle("text-gray-400",!n),t.classList.toggle("hover:text-white",!n)}),refreshFiles()}function onFileSearch(e){currentFileSearch=e.trim().toLowerCase(),refreshFiles()}function loadFiles(e="/",t=null){if(fileOperationInProgress)return;const n=t||(pendingFileHighlight&&pendingFileHighlight.directory===e?pendingFileHighlight.file:null);networkAwareFetch(`/api/files/list?path=${encodeURIComponent(e)}`).then(e=>e.json()).then(t=>{displayFiles(t,e,n);updateCurrentPath(e),n&&(pendingFileHighlight=null)}).catch(e=>{console.error("Error loading files:",e),showFileError("Failed to load files: "+e.message)})}function displayFiles(e,t,n=null){const a=document.getElementById("file-list");if(currentDirectory=t,!a)return!1;if(0===e.length)return a.innerHTML='<p class="text-gray-400 p-4">No files found in this directory</p>',!1;let s='<div class="space-y-2">';if("/"!==t){const e=t.split("/").slice(0,-1).join("/")||"/";s+=`\n <div class="flex items-center p-3 hover:bg-slate-700 rounded-lg cursor-pointer transition-colors" onclick="loadFiles('${escapeAttr(e)}')">\n <svg class="w-5 h-5 mr-3 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>\n </svg>\n <span class="text-blue-400">.. (Parent Directory)</span>\n </div>\n `}if(currentFileSearch&&(e=e.filter(e=>e.is_directory||e.name.toLowerCase().includes(currentFileSearch))),0===e.length&¤tFileSearch)return a.innerHTML=`<p class="text-gray-400 p-4">No files match "<span class="text-white">${escapeHtml(currentFileSearch)}</span>"</p>`,!1;if(e.sort((e,t)=>e.is_directory&&!t.is_directory?-1:!e.is_directory&&t.is_directory?1:"date"===currentFileSort?(t.modified||0)-(e.modified||0):"size"===currentFileSort?(t.size||0)-(e.size||0):e.name.toLowerCase().localeCompare(t.name.toLowerCase())),e.forEach(e=>{const t=e.is_directory?'<svg class="w-5 h-5 mr-3 text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-5l-2-2H5a2 2 0 00-2 2z"></path>\n </svg>':'<svg class="w-5 h-5 mr-3 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>\n </svg>',n=e.is_directory?"":formatBytes(e.size),a=e.modified?new Date(1e3*e.modified).toLocaleDateString():"",o=encodeURIComponent(e.name);s+=`\n <div class="flex items-center justify-between p-3 hover:bg-slate-700 rounded-lg transition-colors" data-file-key="${o}">\n <div class="flex items-center cursor-pointer flex-1" onclick="${e.is_directory?`loadFiles('${escapeAttr(e.path)}')`:`previewFile('${escapeAttr(e.path)}')`}">\n ${t}\n <div class="flex-1">\n <div class="font-medium">${escapeHtml(e.name)}</div>\n ${!e.is_directory&&n?`<div class="text-sm text-gray-400">${n} • ${a}</div>`:""}\n </div>\n </div>\n ${e.is_directory?"":`\n <div class="flex space-x-2">\n <button onclick="downloadFile('${escapeAttr(e.path)}')" class="p-2 text-blue-400 hover:bg-slate-600 rounded" title="Download">\n <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-4-4m4 4l4-4m6 4H6"></path>\n </svg>\n </button>\n <button onclick="deleteFile('${escapeAttr(e.path)}')" class="p-2 text-red-400 hover:bg-slate-600 rounded" title="Delete">\n <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>\n </svg>\n </button>\n </div>\n `}\n </div>\n `}),s+="</div>",a.innerHTML=s,n){const e=encodeURIComponent(n),s=a.querySelector(`[data-file-key="${e}"]`);if(s)return s.classList.add("ring-2","ring-Ragnar-500","ring-offset-2","ring-offset-slate-900"),s.scrollIntoView({behavior:"smooth",block:"center"}),setTimeout(()=>{s.classList.remove("ring-2","ring-Ragnar-500","ring-offset-2","ring-offset-slate-900")},4e3),!0;addConsoleMessage(`Could not highlight ${n} under ${t}`,"warning")}return!1}function displayDirectoryTree(){const e=document.getElementById("directory-tree");if(!e)return;let t='<div class="space-y-1">';[{name:"Data Stolen",path:"/data_stolen",icon:"🗃️"},{name:"Scan Results",path:"/scan_results",icon:"📊"},{name:"Cracked Passwords",path:"/crackedpwd",icon:"🔓"},{name:"Vulnerabilities",path:"/vulnerabilities",icon:"⚠️"},{name:"Logs",path:"/logs",icon:"📋"},{name:"Backups",path:"/backups",icon:"💾"},{name:"Uploads",path:"/uploads",icon:"📤"}].forEach(e=>{t+=`\n <div class="flex items-center p-3 hover:bg-slate-700 rounded-lg cursor-pointer transition-colors" onclick="loadFiles('${e.path}')">\n <span class="mr-3">${e.icon}</span>\n <span>${e.name}</span>\n </div>\n `}),t+="</div>",e.innerHTML=t}function updateCurrentPath(e){const t=document.getElementById("current-path");t&&(t.textContent=e)}function downloadFile(e){if(fileOperationInProgress)return;const t=resolveNetworkAwareEndpoint(`/api/files/download?path=${encodeURIComponent(e)}`),n=document.createElement("a");n.href=t,n.download="",document.body.appendChild(n),n.click(),document.body.removeChild(n),showFileSuccess(`Downloading ${e.split("/").pop()}`)}function previewFile(e){const t=document.getElementById("file-preview-modal"),n=document.getElementById("preview-content"),a=document.getElementById("preview-filename"),s=document.getElementById("preview-truncated-badge"),o=document.getElementById("preview-download-btn");if(!t)return;const r=e.split("/").pop();a.textContent=r,s.classList.add("hidden"),n.innerHTML='<div class="text-center text-gray-400 py-12">\n <svg class="w-8 h-8 inline animate-spin mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>\n </svg>\n <p>Loading preview...</p>\n </div>',o.onclick=()=>downloadFile(e),t.classList.remove("hidden"),t.classList.add("flex"),networkAwareFetch(`/api/files/preview?path=${encodeURIComponent(e)}`).then(e=>e.json()).then(t=>{if(t.error)n.innerHTML=`<p class="text-red-400 p-4">${escapeHtml(t.error)}</p>`;else if("image"===t.type)n.innerHTML=`<div class="flex items-center justify-center h-full p-4">\n <img src="data:${t.mime};base64,${t.data}" alt="${escapeHtml(r)}" class="max-w-full max-h-full object-contain rounded">\n </div>`;else if("text"===t.type){t.truncated&&s.classList.remove("hidden");if(r.toLowerCase().endsWith(".csv")){const e=t.content.split("\n").filter(e=>e.trim());if(e.length>0){const t=e[0].split(","),a=e.slice(1);n.innerHTML=`<div class="overflow-auto"><table class="w-full text-xs border-collapse">\n <thead><tr>${t.map(e=>`<th class="border border-slate-600 px-2 py-1 bg-slate-800 text-left">${escapeHtml(e.trim())}</th>`).join("")}</tr></thead>\n <tbody>${a.map(e=>`<tr class="hover:bg-slate-800">${e.split(",").map(e=>`<td class="border border-slate-700 px-2 py-1 font-mono">${escapeHtml(e.trim())}</td>`).join("")}</tr>`).join("")}</tbody>\n </table></div>`}}else n.innerHTML=`<pre class="text-xs text-gray-300 font-mono whitespace-pre-wrap break-words leading-relaxed">${escapeHtml(t.content)}</pre>`}else"too_large"===t.type?n.innerHTML=`<div class="text-center text-gray-400 py-12">\n <p class="mb-3">File is too large to preview (${formatBytes(t.size)})</p>\n <button onclick="downloadFile('${escapeAttr(e)}')" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm">Download Instead</button>\n </div>`:n.innerHTML=`<div class="text-center text-gray-400 py-12">\n <p class="mb-1">Cannot preview this file type (${escapeHtml(t.mime||"unknown")})</p>\n <p class="text-sm mb-3">Size: ${formatBytes(t.size)}</p>\n <button onclick="downloadFile('${escapeAttr(e)}')" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm">Download</button>\n </div>`}).catch(e=>{n.innerHTML=`<p class="text-red-400 p-4">Failed to load preview: ${escapeHtml(e.message)}</p>`})}function closeFilePreview(){const e=document.getElementById("file-preview-modal");e&&(e.classList.add("hidden"),e.classList.remove("flex"))}function deleteFile(e){if(fileOperationInProgress)return;const t=e.split("/").pop();showFileConfirmModal("Delete File",`Are you sure you want to delete "${t}"? This action cannot be undone.`,()=>{fileOperationInProgress=!0,networkAwareFetch("/api/files/delete",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:e})}).then(e=>e.json()).then(e=>{e.success?(showFileSuccess(`Deleted ${t}`),refreshFiles()):showFileError(`Failed to delete file: ${e.error}`)}).catch(e=>{showFileError(`Error deleting file: ${e.message}`)}).finally(()=>{fileOperationInProgress=!1,closeFileModal()})})}function uploadFile(){const e=document.createElement("input");e.type="file",e.multiple=!0,e.onchange=function(e){const t=e.target.files;if(0===t.length)return;const n=new FormData;for(let e of t)n.append("file",e);n.append("path","/uploads"),fileOperationInProgress=!0,showFileLoading("Uploading files..."),networkAwareFetch("/api/files/upload",{method:"POST",body:n}).then(e=>e.json()).then(e=>{e.success?(showFileSuccess(`Uploaded ${t.length} file(s)`),refreshFiles()):showFileError(`Upload failed: ${e.error}`)}).catch(e=>{showFileError(`Upload error: ${e.message}`)}).finally(()=>{fileOperationInProgress=!1})},e.click()}function clearFiles(){showFileConfirmModal("Clear Files",'\n <div class="space-y-3">\n <p>Choose the type of file clearing:</p>\n <div class="space-y-2">\n <label class="flex items-center">\n <input type="radio" name="clearType" value="light" checked class="mr-2">\n <span>Light Clear (logs, temporary files only)</span>\n </label>\n <label class="flex items-center">\n <input type="radio" name="clearType" value="full" class="mr-2">\n <span>Full Clear (all data including configs)</span>\n </label>\n </div>\n </div>\n ',()=>{const e=document.querySelector('input[name="clearType"]:checked')?.value||"light";fileOperationInProgress=!0,showFileLoading("Clearing files..."),networkAwareFetch("/api/files/clear",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:e})}).then(e=>e.json()).then(e=>{e.success?(showFileSuccess(e.message),refreshFiles()):showFileError(`Clear failed: ${e.error}`)}).catch(e=>{showFileError(`Clear error: ${e.message}`)}).finally(()=>{fileOperationInProgress=!1,closeFileModal()})})}function refreshFiles(){displayDirectoryTree(),loadFiles(currentDirectory)}function showFileSuccess(e){showNotification(e,"success")}function showFileError(e){showNotification(e,"error")}function showFileLoading(e){showNotification(e,"info")}function showFileConfirmModal(e,t,n){const a=document.getElementById("file-operations-modal"),s=document.getElementById("modal-title"),o=document.getElementById("modal-content"),r=document.getElementById("modal-confirm");if(!(a&&s&&o&&r))return;s.textContent=e,o.innerHTML=t;const i=r.cloneNode(!0);r.parentNode.replaceChild(i,r),i.addEventListener("click",n),a.classList.remove("hidden"),a.classList.add("flex")}function closeFileModal(){const e=document.getElementById("file-operations-modal");e&&(e.classList.add("hidden"),e.classList.remove("flex"))}function formatBytes(e){if(0===e)return"0 Bytes";const t=Math.floor(Math.log(e)/Math.log(1024));return parseFloat((e/Math.pow(1024,t)).toFixed(2))+" "+["Bytes","KB","MB","GB"][t]}function showNotification(e,t){const n=document.createElement("div");n.className=`fixed top-4 right-4 z-50 p-4 rounded-lg max-w-sm transform translate-x-full transition-transform duration-300 ${"success"===t?"bg-green-600":"error"===t?"bg-red-600":"bg-blue-600"} text-white`,n.textContent=e,document.body.appendChild(n),setTimeout(()=>{n.style.transform="translateX(0)"},100),setTimeout(()=>{n.style.transform="translateX(100%)",setTimeout(()=>{document.body.removeChild(n)},300)},3e3)}document.addEventListener("keydown",e=>{"Escape"===e.key&&closeFilePreview()}),document.getElementById("file-preview-modal")?.addEventListener("click",function(e){e.target===this&&closeFilePreview()});let currentProcessSort="cpu";function loadSystemData(){fetchSystemStatus(),fetchNetworkStats(),systemMonitoringInterval&&clearInterval(systemMonitoringInterval),systemMonitoringInterval=setInterval(()=>{"system"===currentTab&&(fetchSystemStatus(),fetchNetworkStats())},5e3)}function fetchSystemStatus(){networkAwareFetch("/api/system/status").then(e=>e.json()).then(e=>{e.error?showSystemError("Failed to load system status: "+e.error):(updateSystemOverview(e),updateProcessList(e.processes),updateNetworkInterfaces(e.network_interfaces),updateTemperatureDisplay(e.temperatures))}).catch(e=>{console.error("Error fetching system status:",e),showSystemError("Failed to load system status")})}function fetchNetworkStats(){networkAwareFetch("/api/system/network-stats").then(e=>e.json()).then(e=>{e.error?console.error("Network stats error:",e.error):updateNetworkStats(e)}).catch(e=>{console.error("Error fetching network stats:",e)})}function updateSystemOverview(e){const t=document.getElementById("cpu-usage"),n=document.getElementById("cpu-details"),a=document.getElementById("cpu-progress");t&&(t.textContent=`${e.cpu.percent}%`),n&&(n.textContent=`${e.cpu.count} cores`),a&&(a.style.width=`${e.cpu.percent}%`);const s=document.getElementById("memory-usage"),o=document.getElementById("memory-details"),r=document.getElementById("memory-progress");if(s&&(s.textContent=`${e.memory.percent}%`),o&&(o.textContent=`${e.memory.used_formatted} / ${e.memory.total_formatted}`),r&&(r.style.width=`${e.memory.percent}%`),e.swap){const t=document.getElementById("swap-usage"),n=document.getElementById("swap-details"),a=document.getElementById("swap-progress"),s=Number.isFinite(e.swap.percent)?e.swap.percent:0;t&&(t.textContent=`${s}%`),n&&(n.textContent=`${e.swap.used_formatted} / ${e.swap.total_formatted}`),a&&(a.style.width=`${s}%`)}const i=document.getElementById("disk-usage"),l=document.getElementById("disk-details"),c=document.getElementById("disk-progress");i&&(i.textContent=`${e.disk.percent}%`),l&&(l.textContent=`${e.disk.used_formatted} / ${e.disk.total_formatted}`),c&&(c.style.width=`${e.disk.percent}%`);const d=document.getElementById("battery-card");if(d)if(e.battery){d.classList.remove("hidden");const t=e.battery.level,n=e.battery.charging,a=document.getElementById("battery-usage"),s=document.getElementById("battery-details"),o=document.getElementById("battery-progress"),r=document.getElementById("battery-icon");if(a&&(a.textContent=`${t}%`),s){let t=n?"Charging":"On battery";e.battery.voltage&&(t+=` (${e.battery.voltage}V)`),s.textContent=t}o&&(o.style.width=`${t}%`,o.className="h-2 rounded-full transition-all duration-300 "+(t<=20?"bg-red-500":t<=50?"bg-yellow-500":"bg-emerald-500")),r&&(r.className="w-5 h-5 "+(t<=20?"text-red-400":t<=50?"text-yellow-400":"text-emerald-400"))}else d.classList.add("hidden");const u=document.getElementById("uptime-display");u&&(u.textContent=e.uptime.formatted)}function updateProcessList(e){const t=document.getElementById("process-list");if(!t)return;if(0===e.length)return void(t.innerHTML='<p class="text-gray-400 text-center py-4">No process data available</p>');let n="";e.slice(0,10).forEach(e=>{const t=(e.cpu_percent||0).toFixed(1),a=(e.memory_percent||0).toFixed(1);n+=`\n <div class="flex items-center justify-between p-2 bg-slate-800 rounded text-sm">\n <div class="flex-1 truncate">\n <span class="font-medium">${e.name}</span>\n <span class="text-gray-400 ml-2">PID: ${e.pid}</span>\n </div>\n <div class="flex space-x-3 text-xs">\n <span class="text-blue-400">${t}% CPU</span>\n <span class="text-green-400">${a}% MEM</span>\n </div>\n </div>\n `}),t.innerHTML=n}function updateNetworkInterfaces(e){const t=document.getElementById("network-interfaces");if(!t)return;if(0===e.length)return void(t.innerHTML='<p class="text-gray-400 text-center py-4">No network interfaces found</p>');let n="";e.forEach(e=>{const t=e.is_up?"text-green-400":"text-red-400",a=e.is_up?"UP":"DOWN";n+=`\n <div class="border border-gray-700 rounded p-3">\n <div class="flex items-center justify-between mb-2">\n <span class="font-medium">${e.name}</span>\n <span class="${t} text-xs">${a}</span>\n </div>\n <div class="text-xs text-gray-400 space-y-1">\n ${e.speed>0?`<div>Speed: ${e.speed} Mbps</div>`:""}\n ${e.addresses.map(e=>`<div>${e.address} (${e.family})</div>`).join("")}\n </div>\n </div>\n `}),t.innerHTML=n}function updateNetworkStats(e){const t=document.getElementById("network-stats");if(!t)return;let n="";n+=`\n <div class="bg-slate-800 rounded p-3">\n <h4 class="font-medium mb-2">Connections</h4>\n <div class="text-2xl font-bold text-blue-400">${e.total_connections}</div>\n <div class="text-xs text-gray-400">Total active</div>\n </div>\n `,Object.entries(e.interfaces).slice(0,4).forEach(([e,t])=>{n+=`\n <div class="bg-slate-800 rounded p-3">\n <h4 class="font-medium mb-2">${e}</h4>\n <div class="text-xs space-y-1">\n <div class="flex justify-between">\n <span class="text-gray-400">Sent:</span>\n <span class="text-green-400">${t.bytes_sent_formatted}</span>\n </div>\n <div class="flex justify-between">\n <span class="text-gray-400">Received:</span>\n <span class="text-blue-400">${t.bytes_recv_formatted}</span>\n </div>\n <div class="flex justify-between">\n <span class="text-gray-400">Packets:</span>\n <span>${t.packets_sent+t.packets_recv}</span>\n </div>\n </div>\n </div>\n `}),t.innerHTML=n}function formatSensorName(e){const t={cpu_thermal:"CPU","cpu-thermal":"CPU",cpu_temp:"CPU",gpu_thermal:"GPU","gpu-thermal":"GPU",soc_thermal:"SoC","soc-thermal":"SoC",acpitz:"System (ACPI)",coretemp:"CPU Core",k10temp:"CPU (AMD)",nvme_composite:"NVMe SSD",nvme:"NVMe SSD",wifi:"Wi-Fi",pch:"Chipset",bat:"Battery"},n=e.toLowerCase().replace(/[_\-\s]+\d*$/,"").replace(/[_\-\s]+/g,"_");for(const[e,a]of Object.entries(t))if(n===e||n.startsWith(e))return a;return e.replace(/[_\-]+/g," ").replace(/\s*\d+\s*$/,"").trim().replace(/\b\w/g,e=>e.toUpperCase())||e}function updateTemperatureDisplay(e){const t=document.getElementById("temperature-section"),n=document.getElementById("temperature-display");if(!t||!n)return;if(0===Object.keys(e).length)return void t.classList.add("hidden");t.classList.remove("hidden");let a="";Object.entries(e).forEach(([e,t])=>{const n=t>70?"text-red-400":t>50?"text-yellow-400":"text-green-400";a+=`\n <div class="bg-slate-800 rounded p-3">\n <h4 class="font-medium mb-1 text-sm">${formatSensorName(e)}</h4>\n <div class="text-xl font-bold ${n}">${t.toFixed(1)}°C</div>\n </div>\n `}),n.innerHTML=a}function sortProcesses(e){currentProcessSort=e,document.querySelectorAll(".process-sort-btn").forEach(t=>{t.dataset.sort===e?(t.classList.remove("bg-gray-600"),t.classList.add("bg-Ragnar-600")):(t.classList.remove("bg-Ragnar-600"),t.classList.add("bg-gray-600"))}),networkAwareFetch(`/api/system/processes?sort=${e}`).then(e=>e.json()).then(e=>{updateProcessList(e)}).catch(e=>{console.error("Error sorting processes:",e)})}function refreshSystemStatus(){fetchSystemStatus(),fetchNetworkStats(),showSystemSuccess("System status refreshed")}function showSystemSuccess(e){showNotification(e,"success")}function showSystemError(e){showNotification(e,"error")}let currentNetkbFilter="all",netkbData=[];function loadNetkbData(){fetchNetkbData()}function fetchNetkbData(){networkAwareFetch("/api/netkb/data").then(e=>e.json()).then(e=>{e.error?showNetkbError("Failed to load NetKB data: "+e.error):(netkbData=e.entries||[],updateNetkbStatistics(e.statistics||{}),displayNetkbData(netkbData))}).catch(e=>{console.error("Error fetching NetKB data:",e),showNetkbError("Failed to load NetKB data")})}function updateNetkbStatistics(e){const t=document.getElementById("netkb-total-entries"),n=document.getElementById("netkb-vulnerabilities"),a=document.getElementById("netkb-services"),s=document.getElementById("netkb-hosts");t&&(t.textContent=e.total_entries||0),n&&(n.textContent=e.vulnerabilities||0),a&&(a.textContent=e.services||0),s&&(s.textContent=e.unique_hosts||0)}function displayNetkbData(e){const t=document.getElementById("netkb-table-body");if(!t)return;if(0===e.length)return void(t.innerHTML='<tr><td colspan="7" class="text-center text-gray-400 py-8">No NetKB entries found</td></tr>');let n="";e.forEach(e=>{const t=getSeverityColor(e.severity),a=getTypeIcon(e.type),s=new Date(1e3*e.discovered).toLocaleDateString();n+=`\n <tr class="border-b border-gray-800 hover:bg-slate-800 cursor-pointer" onclick="showNetkbEntryDetail('${e.id}')">\n <td class="p-3">\n <span class="inline-flex items-center">\n ${a}\n <span class="ml-2 capitalize">${e.type}</span>\n </span>\n </td>\n <td class="p-3 font-mono text-sm">${e.host}</td>\n <td class="p-3 font-mono text-sm">${e.port||"-"}</td>\n <td class="p-3">\n <span class="font-medium">${e.service||e.description}</span>\n <div class="text-xs text-gray-400 mt-1">${e.description}</div>\n </td>\n <td class="p-3">\n <span class="px-2 py-1 rounded text-xs font-medium ${t}">\n ${e.severity}\n </span>\n </td>\n <td class="p-3 text-sm text-gray-400">${s}</td>\n <td class="p-3">\n <div class="flex space-x-2">\n <button onclick="event.stopPropagation(); showNetkbEntryDetail('${e.id}')" \n class="text-blue-400 hover:text-blue-300 text-xs">\n View\n </button>\n ${"vulnerability"===e.type?`<button onclick="event.stopPropagation(); researchVulnerability('${e.cve||e.id}')" \n class="text-orange-400 hover:text-orange-300 text-xs">\n Research\n </button>`:""}\n </div>\n </td>\n </tr>\n `}),t.innerHTML=n}function getSeverityColor(e){switch(e.toLowerCase()){case"critical":return"bg-red-900 text-red-200";case"high":return"bg-red-800 text-red-100";case"medium":return"bg-yellow-800 text-yellow-100";case"low":return"bg-blue-800 text-blue-100";case"info":return"bg-gray-700 text-gray-200";default:return"bg-gray-600 text-gray-200"}}function getTypeIcon(e){switch(e.toLowerCase()){case"vulnerability":return'<svg class="w-4 h-4 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>';case"service":return'<svg class="w-4 h-4 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>';case"host":return'<svg class="w-4 h-4 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"></path></svg>';case"exploit":return'<svg class="w-4 h-4 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>';default:return'<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>'}}function filterNetkbData(e){currentNetkbFilter=e,document.querySelectorAll(".netkb-filter-btn").forEach(t=>{t.dataset.filter===e?(t.classList.remove("bg-gray-600"),t.classList.add("bg-Ragnar-600")):(t.classList.remove("bg-Ragnar-600"),t.classList.add("bg-gray-600"))});let t=netkbData;"all"!==e&&(t=netkbData.filter(t=>t.type===e)),displayNetkbData(t)}function searchNetkbData(e){displayNetkbData(netkbData.filter(t=>{const n=e.toLowerCase();return t.host.toLowerCase().includes(n)||t.service.toLowerCase().includes(n)||t.description.toLowerCase().includes(n)||t.type.toLowerCase().includes(n)}))}function clearNetkbSearch(){const e=document.getElementById("netkb-search");e&&(e.value="",filterNetkbData(currentNetkbFilter))}function showNetkbEntryDetail(e){const t=netkbData.find(t=>t.id===e);if(!t)return;const n=document.getElementById("netkb-detail-modal"),a=document.getElementById("netkb-detail-title"),s=document.getElementById("netkb-detail-content");if(!n||!a||!s)return;a.textContent=`${t.type.toUpperCase()}: ${t.host}`;const o=new Date(1e3*t.discovered).toLocaleString(),r=getSeverityColor(t.severity);s.innerHTML=`\n <div class="grid grid-cols-1 md:grid-cols-2 gap-4">\n <div class="space-y-3">\n <div>\n <label class="text-sm text-gray-400">Host/Target</label>\n <div class="font-mono text-lg">${t.host}</div>\n </div>\n <div>\n <label class="text-sm text-gray-400">Service/Port</label>\n <div class="font-mono">${t.port||"N/A"} ${t.service?"("+t.service+")":""}</div>\n </div>\n <div>\n <label class="text-sm text-gray-400">Type</label>\n <div class="capitalize">${t.type}</div>\n </div>\n <div>\n <label class="text-sm text-gray-400">Severity</label>\n <div><span class="px-2 py-1 rounded text-sm ${r}">${t.severity}</span></div>\n </div>\n </div>\n <div class="space-y-3">\n <div>\n <label class="text-sm text-gray-400">Description</label>\n <div class="text-sm">${t.description}</div>\n </div>\n <div>\n <label class="text-sm text-gray-400">Source</label>\n <div class="text-sm">${t.source}</div>\n </div>\n <div>\n <label class="text-sm text-gray-400">Discovered</label>\n <div class="text-sm">${o}</div>\n </div>\n ${t.cve?`\n <div>\n <label class="text-sm text-gray-400">CVE</label>\n <div class="font-mono text-sm">${t.cve}</div>\n </div>\n `:""}\n </div>\n </div>\n \n <div class="mt-6 p-4 bg-slate-800 rounded-lg">\n <h4 class="font-medium mb-2">Recommendations</h4>\n <ul class="text-sm text-gray-300 space-y-1">\n <li>• Monitor this ${t.type} regularly for changes</li>\n <li>• Consider implementing additional security measures</li>\n <li>• Review access controls and firewall rules</li>\n ${"vulnerability"===t.type?"<li>• Apply security patches if available</li>":""}\n ${"service"===t.type?"<li>• Ensure service is properly configured and updated</li>":""}\n </ul>\n </div>\n `;const i=document.getElementById("netkb-exploit-btn");i&&("vulnerability"===t.type?(i.classList.remove("hidden"),i.onclick=()=>exploitVulnerability(t)):i.classList.add("hidden"));const l=document.getElementById("netkb-research-btn");l&&(l.onclick=()=>researchEntry(t)),n.classList.remove("hidden"),n.classList.add("flex")}function closeNetkbModal(){const e=document.getElementById("netkb-detail-modal");e&&(e.classList.add("hidden"),e.classList.remove("flex"))}function refreshNetkbData(){fetchNetkbData(),showNetkbSuccess("NetKB data refreshed")}function exportNetkbData(){const e=prompt("Export format (json/csv):","json");!e||"json"!==e&&"csv"!==e||(window.open(`/api/netkb/export?format=${e}`,"_blank"),showNetkbSuccess(`NetKB data exported as ${e.toUpperCase()}`))}function exportNetkbEntry(){showNetkbInfo("Individual entry export feature coming soon")}function researchEntry(e){let t="";t=e.cve?e.cve:e.service?`${e.service} vulnerability exploit`:`${e.host} ${e.description}`,window.open("https://www.google.com/search?q="+encodeURIComponent(t),"_blank"),showNetkbInfo(`Researching: ${t}`)}function researchVulnerability(e){window.open("https://nvd.nist.gov/vuln/search/results?form_type=Basic&results_type=overview&query="+encodeURIComponent(e),"_blank"),showNetkbInfo(`Researching vulnerability: ${e}`)}function exploitVulnerability(e){const t=`Are you sure you want to attempt exploitation of ${e.cve||e.description} on ${e.host}?`;confirm(t)&&showNetkbInfo("Exploitation feature not yet implemented - this would trigger automated exploit attempts")}function showNetkbSuccess(e){showNotification(e,"success")}function showNetkbError(e){showNotification(e,"error")}function showNetkbInfo(e){showNotification(e,"info")}function setThreatIntelFilter(e,t={}){if(!["open","resolved","all"].includes(e))return;threatIntelStatusFilter=e;if(document.querySelectorAll(".threat-intel-filter-btn").forEach(t=>{const n=t.getAttribute("data-status")===e;t.className=n?"threat-intel-filter-btn px-3 py-2 rounded-lg text-sm font-semibold border border-Ragnar-500 bg-Ragnar-600 text-white shadow-md shadow-Ragnar-500/40":"threat-intel-filter-btn px-3 py-2 rounded-lg text-sm font-semibold border border-slate-700 bg-slate-800 text-slate-300 hover:text-white hover:border-slate-500",t.setAttribute("aria-pressed",n?"true":"false")}),t.skipReload)return;const n=document.getElementById("grouped-vulnerabilities-container");if(n){const t=e.charAt(0).toUpperCase()+e.slice(1);n.innerHTML=`\n <div class="glass rounded-lg p-6 text-center">\n <p class="text-slate-300">Loading ${t} vulnerabilities...</p>\n </div>\n `}loadThreatIntelData()}async function loadThreatIntelData(){try{const e=encodeURIComponent(threatIntelStatusFilter||"open"),t=await networkAwareFetch(`/api/vulnerabilities/grouped?status=${e}`);if(t.ok){displayGroupedVulnerabilities(await t.json())}else{const t=await networkAwareFetch(`/api/vulnerabilities?status=${e}`);if(t.ok){displayFallbackVulnerabilities(await t.json())}}}catch(e){console.error("Error loading vulnerability data:",e);const t=document.getElementById("grouped-vulnerabilities-container");t&&(t.innerHTML=`\n <div class="glass rounded-lg p-6 text-center">\n <p class="text-red-400">Error loading vulnerabilities</p>\n <p class="text-slate-400 text-sm mt-2">${e.message}</p>\n </div>\n `)}}function displayGroupedVulnerabilities(e){const t=document.getElementById("grouped-vulnerabilities-container"),n=document.getElementById("threat-intel-vulnerable-hosts-count");n&&(n.textContent=e.total_hosts||0),document.getElementById("total-vulnerabilities-count").textContent=e.total_vulnerabilities||0;let a=0,s=0;if(e.grouped_vulnerabilities&&e.grouped_vulnerabilities.forEach(e=>{a+=e.severity_counts.critical||0,s+=e.severity_counts.high||0}),document.getElementById("critical-vuln-count").textContent=a,document.getElementById("high-vuln-count").textContent=s,!e.grouped_vulnerabilities||0===e.grouped_vulnerabilities.length){const e={open:{title:"No Open Vulnerabilities",sub:"No unresolved vulnerabilities match the current filter."},resolved:{title:"No Resolved Vulnerabilities",sub:"None of the discovered vulnerabilities have been marked as resolved yet."},all:{title:"No Vulnerabilities Found",sub:"All discovered hosts appear to be secure!"}},n=e[threatIntelStatusFilter]||e.all;return void(t.innerHTML=`\n <div class="glass rounded-lg p-6 text-center">\n <svg class="w-16 h-16 mx-auto mb-4 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>\n </svg>\n <h3 class="text-xl font-semibold text-white mb-2">${n.title}</h3>\n <p class="text-slate-400">${n.sub}</p>\n </div>\n `)}let o="";e.grouped_vulnerabilities.forEach((e,t)=>{const n=e.severity_counts,a=e.total_vulnerabilities;let s="blue",r="Low Risk";n.critical>0?(s="red",r="Critical Risk"):n.high>5?(s="orange",r="High Risk"):n.high>0&&(s="yellow",r="Medium Risk"),o+=`\n <div class="glass rounded-lg p-6">\n \x3c!-- Host Header --\x3e\n <div class="flex items-center justify-between mb-4 pb-4 border-b border-slate-700">\n <div class="flex items-center space-x-4">\n <div class="bg-${s}-500/20 p-3 rounded-lg">\n <svg class="w-8 h-8 text-${s}-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"></path>\n </svg>\n </div>\n <div>\n <h3 class="text-2xl font-bold text-white">${e.ip}</h3>\n <p class="text-sm text-slate-400">\n <span class="bg-${s}-500/20 text-${s}-300 px-2 py-1 rounded text-xs font-semibold">${r}</span>\n <span class="ml-2">${a} Vulnerabilities Found</span>\n </p>\n </div>\n </div>\n <button onclick="toggleHostDetails('host-${t}')" class="bg-Ragnar-600 hover:bg-Ragnar-700 text-white px-4 py-2 rounded-lg transition-colors">\n <span id="host-${t}-toggle">Show Details</span>\n </button>\n </div>\n \n \x3c!-- Quick Stats --\x3e\n <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">\n <div class="bg-slate-800/50 rounded-lg p-3">\n <div class="text-red-400 text-2xl font-bold">${n.critical||0}</div>\n <div class="text-xs text-slate-400">Critical</div>\n </div>\n <div class="bg-slate-800/50 rounded-lg p-3">\n <div class="text-orange-400 text-2xl font-bold">${n.high||0}</div>\n <div class="text-xs text-slate-400">High</div>\n </div>\n <div class="bg-slate-800/50 rounded-lg p-3">\n <div class="text-yellow-400 text-2xl font-bold">${n.medium||0}</div>\n <div class="text-xs text-slate-400">Medium</div>\n </div>\n <div class="bg-slate-800/50 rounded-lg p-3">\n <div class="text-blue-400 text-2xl font-bold">${n.low||0}</div>\n <div class="text-xs text-slate-400">Low</div>\n </div>\n </div>\n \n \x3c!-- Affected Services --\x3e\n <div class="mb-4">\n <div class="text-sm text-slate-400 mb-2">Affected Services</div>\n <div class="flex flex-wrap gap-2">\n ${e.affected_services.map(e=>`<span class="bg-slate-700 px-3 py-1 rounded-full text-sm">${e}</span>`).join("")}\n </div>\n <div class="text-sm text-slate-400 mt-2">\n Ports: ${e.affected_ports.join(", ")}\n </div>\n </div>\n \n \x3c!-- Detailed Vulnerabilities (Initially Hidden) --\x3e\n <div id="host-${t}-details" class="hidden mt-4">\n <div class="border-t border-slate-700 pt-4">\n <h4 class="text-lg font-semibold mb-3 text-white">All Vulnerabilities (${a})</h4>\n <div class="space-y-2 max-h-96 overflow-y-auto scrollbar-thin">\n ${e.vulnerabilities.map(e=>{const t={critical:"red",high:"orange",medium:"yellow",low:"blue"}[e.severity]||"gray",n=e.vulnerability.length>100?e.vulnerability.substring(0,100)+"...":e.vulnerability;return`\n <div class="bg-slate-800/30 rounded p-3 hover:bg-slate-800/50 transition-colors">\n <div class="flex items-start justify-between">\n <div class="flex-1">\n <div class="flex items-center space-x-2 mb-1">\n <span class="bg-${t}-500/20 text-${t}-300 px-2 py-0.5 rounded text-xs font-semibold uppercase">${e.severity}</span>\n <span class="text-slate-400 text-xs">${e.service}:${e.port}</span>\n </div>\n <div class="text-sm text-white font-mono">${n}</div>\n </div>\n <button onclick='showVulnerabilityDetails(${JSON.stringify(e).replace(/'/g,"\\'")})' \n class="ml-2 text-Ragnar-400 hover:text-Ragnar-300 text-xs">\n Details\n </button>\n </div>\n </div>\n `}).join("")}\n </div>\n </div>\n </div>\n </div>\n `}),t.innerHTML=o}function toggleHostDetails(e){const t=document.getElementById(`${e}-details`),n=document.getElementById(`${e}-toggle`);t.classList.contains("hidden")?(t.classList.remove("hidden"),n.textContent="Hide Details"):(t.classList.add("hidden"),n.textContent="Show Details")}function showVulnerabilityDetails(e){const t=document.getElementById("vulnerability-detail-modal");document.getElementById("vuln-detail-content").innerHTML=`\n <div class="space-y-4">\n <div class="bg-slate-800/50 rounded-lg p-4">\n <div class="text-sm text-slate-400 mb-1">Severity</div>\n <div class="${{critical:"text-red-400",high:"text-orange-400",medium:"text-yellow-400",low:"text-blue-400"}[e.severity]} text-2xl font-bold uppercase">${e.severity}</div>\n </div>\n \n <div class="bg-slate-800/50 rounded-lg p-4">\n <div class="text-sm text-slate-400 mb-1">Vulnerability</div>\n ${function(e){const t=e.match(/(CVE-\d{4}-\d{4,7})/gi);if(!t||0===t.length)return`<div class="text-white font-mono text-sm break-all">${e}</div>`;let n='<div class="mt-3 pt-3 border-t border-slate-700">';return n+='<div class="text-sm text-slate-400 mb-2">CVE References:</div>',n+='<div class="flex flex-wrap gap-2">',[...new Set(t)].forEach(e=>{n+=`\n <div class="bg-slate-700/50 rounded px-3 py-2 flex items-center space-x-2">\n <span class="text-Ragnar-400 font-mono text-sm">${e}</span>\n <a href="${`https://nvd.nist.gov/vuln/detail/${e}`}" target="_blank" rel="noopener noreferrer" \n class="text-blue-400 hover:text-blue-300 transition-colors" \n title="View on NIST NVD">\n <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" \n d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>\n </svg>\n </a>\n <a href="${`https://cve.mitre.org/cgi-bin/cvename.cgi?name=${e}`}" target="_blank" rel="noopener noreferrer" \n class="text-green-400 hover:text-green-300 transition-colors" \n title="View on MITRE">\n <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" \n d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>\n </svg>\n </a>\n </div>\n `}),n+="</div></div>",`<div class="text-white font-mono text-sm break-all">${e}</div>${n}`}(e.vulnerability)}\n </div>\n \n <div class="grid grid-cols-2 gap-4">\n <div class="bg-slate-800/50 rounded-lg p-4">\n <div class="text-sm text-slate-400 mb-1">Service</div>\n <div class="text-white">${e.service}</div>\n </div>\n <div class="bg-slate-800/50 rounded-lg p-4">\n <div class="text-sm text-slate-400 mb-1">Port</div>\n <div class="text-white">${e.port}</div>\n </div>\n </div>\n \n <div class="bg-slate-800/50 rounded-lg p-4">\n <div class="text-sm text-slate-400 mb-1">Discovered</div>\n <div class="text-white">${new Date(e.discovered).toLocaleString()}</div>\n </div>\n \n <div class="bg-slate-800/50 rounded-lg p-4">\n <div class="text-sm text-slate-400 mb-1">Status</div>\n <div class="text-white capitalize">${e.status}</div>\n </div>\n </div>\n `,t.classList.remove("hidden"),t.classList.add("flex")}function closeVulnerabilityModal(){const e=document.getElementById("vulnerability-detail-modal");e.classList.add("hidden"),e.classList.remove("flex")}function displayFallbackVulnerabilities(e){const t={};e.vulnerabilities&&e.vulnerabilities.forEach(e=>{t[e.host]||(t[e.host]={ip:e.host,total_vulnerabilities:0,severity_counts:{critical:0,high:0,medium:0,low:0},affected_ports:new Set,affected_services:new Set,vulnerabilities:[]}),t[e.host].total_vulnerabilities++,t[e.host].severity_counts[e.severity]++,t[e.host].affected_ports.add(e.port),t[e.host].affected_services.add(e.service),t[e.host].vulnerabilities.push(e)});const n=Object.values(t).map(e=>({...e,affected_ports:Array.from(e.affected_ports),affected_services:Array.from(e.affected_services)}));displayGroupedVulnerabilities({total_hosts:n.length,total_vulnerabilities:e.vulnerabilities?.length||0,grouped_vulnerabilities:n})}async function triggerManualVulnScan(){try{addConsoleMessage("Starting vulnerability scan on all discovered hosts...","info");const e=await fetchAPI("/api/threat-intelligence/trigger-vuln-scan",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({target:"all"})});"vulnerability_scan_triggered"===e.action?(addConsoleMessage(`✅ ${e.message}`,"success"),addConsoleMessage(`📋 Scanning ${e.discovered_hosts} discovered hosts`,"info"),e.next_steps&&e.next_steps.forEach(e=>{addConsoleMessage(` • ${e}`,"info")}),showNotification(`Vulnerability scan started on ${e.discovered_hosts} hosts. Check back in a few minutes!`,"success"),setTimeout(()=>{"threat-intel"===currentTab&&(loadThreatIntelData(),addConsoleMessage("🔄 Checking for new threat intelligence findings...","info"))},3e4),setTimeout(()=>{"threat-intel"===currentTab&&(loadThreatIntelData(),addConsoleMessage("🔍 Final check for vulnerability scan results...","info"))},12e4)):(addConsoleMessage("❌ Failed to start vulnerability scan","error"),showNotification("Failed to start vulnerability scan","error"))}catch(e){console.error("Error triggering vulnerability scan:",e),addConsoleMessage("❌ Error starting vulnerability scan: "+e.message,"error"),showNotification("Error starting vulnerability scan","error")}}function refreshThreatIntel(){showNotification("Refreshing threat intelligence...","info"),"threat-intel"===currentTab&&setThreatIntelFilter(threatIntelStatusFilter)}function updateThreatIntelStats(e){document.getElementById("threat-sources-count").textContent=e.active_sources||0,document.getElementById("enriched-findings-count").textContent=e.enriched_findings_count||0,document.getElementById("high-risk-count").textContent=e.high_risk_count||0,document.getElementById("active-campaigns-count").textContent=e.active_campaigns||0;const t=e.risk_distribution||{};document.getElementById("critical-risk-count").textContent=t.critical||0,document.getElementById("high-risk-detail-count").textContent=t.high||0,document.getElementById("medium-risk-count").textContent=t.medium||0,document.getElementById("low-risk-count").textContent=t.low||0;const n=e.source_status||{};updateSourceStatus("cisa-status",n.cisa_kev||!1),updateSourceStatus("nvd-status",n.nvd_cve||!1),updateSourceStatus("otx-status",n.alienvault_otx||!1),updateSourceStatus("mitre-status",n.mitre_attack||!1),updateTopThreatsList(e.top_threats||[],e.last_update||e.last_intelligence_update||null)}function updateSourceStatus(e,t){const n=document.getElementById(e);n&&(n.className="w-3 h-3 rounded-full "+(t?"bg-green-400":"bg-red-400"))}function updateEnrichedFindingsTable(e){const t=document.getElementById("enriched-findings-table");e&&0!==e.length?t.innerHTML=e.map(e=>`\n <tr class="border-b border-slate-700 hover:bg-slate-700/50">\n <td class="py-3 px-4 text-white font-mono">${escapeHtml(e.target)}</td>\n <td class="py-3 px-4">\n <span class="px-2 py-1 rounded text-xs font-medium ${getRiskScoreClass(e.risk_score)}">\n ${e.risk_score}/100\n </span>\n </td>\n <td class="py-3 px-4 text-slate-300 max-w-xs truncate" title="${escapeHtml(e.threat_context||"N/A")}">\n ${escapeHtml(e.threat_context||"N/A")}\n </td>\n <td class="py-3 px-4 text-slate-300">${escapeHtml(e.attribution||"Unknown")}</td>\n <td class="py-3 px-4 text-slate-400">${formatTimestamp(e.last_updated)}</td>\n <td class="py-3 px-4">\n <button onclick="downloadThreatReport('${e.target}')" \n class="text-blue-400 hover:text-blue-300 text-sm">\n Report\n </button>\n </td>\n </tr>\n `).join(""):t.innerHTML=`\n <tr>\n <td colspan="6" class="text-center py-12 text-slate-400">\n <div class="space-y-4">\n <div class="text-xl">🛡️ No Threat Intelligence Findings</div>\n <div class="text-sm max-w-md mx-auto space-y-2">\n <p>Threat intelligence enrichment requires vulnerability discoveries first.</p>\n <p class="text-cyan-400">📋 Steps to generate threat intelligence:</p>\n <ol class="text-left text-xs space-y-1 mt-2">\n <li>1. Wait for network discovery to complete (${document.getElementById("target-count")?.textContent||"0"} hosts found)</li>\n <li>2. Run vulnerability scans on discovered hosts</li>\n <li>3. Threat intelligence will enrich discovered vulnerabilities</li>\n </ol>\n <div class="mt-4">\n <button onclick="triggerManualVulnScan()" class="bg-cyan-600 hover:bg-cyan-700 px-4 py-2 rounded text-sm transition-colors">\n 🚀 Start Vulnerability Scan\n </button>\n </div>\n </div>\n </div>\n </td>\n </tr>\n `}function updateTopThreatsList(e,t){const n=document.getElementById("top-threats-list"),a=document.getElementById("top-threats-updated");n&&(a&&(a.textContent=`Last updated: ${t?formatTimestamp(t):"N/A"}`),e&&0!==e.length?n.innerHTML=e.slice(0,5).map(e=>`\n <li class="bg-slate-800/60 rounded-lg p-4 flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">\n <div class="space-y-1">\n <p class="text-white font-semibold">${escapeHtml(e.target||"Unknown Target")}</p>\n <p class="text-slate-400 text-sm">${escapeHtml(e.summary||"No summary available")}</p>\n <div class="text-xs text-slate-500 space-x-3">\n <span>Last Seen: ${formatTimestamp(e.last_seen)}</span>\n ${e.attribution?`<span>Attributed to: ${escapeHtml(e.attribution)}</span>`:""}\n </div>\n </div>\n <span class="self-start sm:self-center px-2 py-1 rounded text-xs font-semibold ${getRiskScoreClass(e.risk_score)}">\n ${e.risk_score}/100\n </span>\n </li>\n `).join(""):n.innerHTML='\n <li class="text-slate-400 text-center py-4">\n <div class="space-y-2">\n <div>🛡️ No active threats detected</div>\n <div class="text-xs">Threat intelligence will appear here when vulnerabilities are discovered and enriched</div>\n </div>\n </li>\n ')}function getRiskScoreClass(e){return e>=90?"bg-red-600 text-white":e>=70?"bg-orange-600 text-white":e>=50?"bg-yellow-600 text-black":"bg-green-600 text-white"}async function enrichTarget(){const e=document.getElementById("enrichment-target"),t=e.value.trim();if(t)try{showNotification(`Enriching target: ${t}...`,"info");const n=await networkAwareFetch("/api/threat-intelligence/enrich-target",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({target:t})});if(n.ok){showNotification(`Target enriched successfully. Risk score: ${(await n.json()).risk_score}/100`,"success"),e.value="","threat-intel"===currentTab&&loadThreatIntelData()}else{showNotification(`Enrichment failed: ${(await n.json()).error}`,"error")}}catch(e){console.error("Error enriching target:",e),showNotification("Error enriching target","error")}else showNotification("Please enter a target (IP, domain, or hash)","error")}async function downloadThreatReport(e){try{showNotification(`Analyzing ${e} for threat intelligence...`,"info");const t=await networkAwareFetch("/api/threat-intelligence/download-report",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({target:e})});if(t.ok){const n=await t.blob(),a=window.URL.createObjectURL(n),s=document.createElement("a");s.href=a;const o=(new Date).toISOString().slice(0,19).replace(/:/g,"-");s.download=`Threat_Intelligence_Report_${e.replace(/[^a-zA-Z0-9.-]/g,"_")}_${o}.txt`,document.body.appendChild(s),s.click(),window.URL.revokeObjectURL(a),document.body.removeChild(s),showNotification(`Threat intelligence report downloaded for ${e}`,"success")}else{const n=await t.json();"no_findings"===n.target_type?showNotification(`No vulnerability findings detected for ${e} - run network scans first to discover vulnerabilities for threat intelligence enrichment`,"warning"):showNotification(`Failed to generate report: ${n.error}`,"error")}}catch(e){console.error("Error downloading threat report:",e),showNotification("Error downloading threat intelligence report","error")}}function formatTimestamp(e){if(!e)return"N/A";try{return new Date(e).toLocaleString()}catch(e){return"Invalid date"}}function escapeHtml(e){if("string"!=typeof e)return e;const t=document.createElement("div");return t.textContent=e,t.innerHTML}function formatAIText(e){if(!e||"string"!=typeof e)return"";const t=escapeHtml(e).split("\n"),n=[];let a=!1;for(let e=0;e<t.length;e++){let s=t[e].trim();if(s){if(s.match(/^\*\*(.+?)\*\*:?$/)){a&&(n.push("</ul>"),a=!1);const e=s.replace(/^\*\*(.+?)\*\*:?$/,"$1");n.push(`<div class="font-bold text-sky-300 mt-4 mb-2">${e}</div>`);continue}if(s.match(/^[\-\*•]\s+(.+)$/)){a||(n.push('<ul class="list-disc list-inside space-y-1 ml-4 text-gray-300">'),a=!0);const e=s.replace(/^[\-\*•]\s+(.+)$/,"$1");n.push(`<li>${e}</li>`);continue}if(s.match(/^\d+\.\s+/)){a||(n.push('<ul class="list-decimal list-inside space-y-2 ml-4 text-gray-300">'),a=!0);let e=s.replace(/^\d+\.\s+/,"");e=e.replace(/\*\*(.+?)\*\*/g,'<strong class="text-sky-200">$1</strong>'),n.push(`<li class="mb-1">${e}</li>`);continue}a&&(n.push("</ul>"),a=!1),s=s.replace(/\*\*(.+?)\*\*/g,'<strong class="text-sky-200">$1</strong>'),n.push(`<p class="mb-2 text-gray-300">${s}</p>`)}else a&&(n.push("</ul>"),a=!1),n.push('<div class="mb-3"></div>')}return a&&n.push("</ul>"),n.join("")}window.loadConsoleLogs=loadConsoleLogs,window.clearConsole=clearConsole,window.refreshEpaperDisplay=refreshEpaperDisplay,window.toggleEpaperSize=toggleEpaperSize,window.checkForUpdates=checkForUpdates,window.checkForUpdatesQuiet=checkForUpdatesQuiet,window.performUpdate=performUpdate,window.handleReleaseGateDecision=handleReleaseGateDecision,window.restartService=restartService,window.rebootSystem=rebootSystem,window.startAPMode=startAPMode,window.refreshWifiStatus=refreshWifiStatus,window.updateManualPorts=updateManualPorts,window.executeManualAttack=executeManualAttack,window.startOrchestrator=startOrchestrator,window.stopOrchestrator=stopOrchestrator,window.triggerNetworkScan=triggerNetworkScan,window.triggerVulnScan=triggerVulnScan,window.refreshDashboard=refreshDashboard,window.handleHeadlessMode=handleHeadlessMode,window.applyHeadlessVisibility=applyHeadlessVisibility,window.loadWifiInterfaces=loadWifiInterfaces,window.scanWifiNetworks=scanWifiNetworks,window.openWifiConnectModal=openWifiConnectModal,window.closeWifiConnectModal=closeWifiConnectModal,window.togglePasswordVisibility=togglePasswordVisibility,window.connectToWifiNetwork=connectToWifiNetwork,window.forgetWifiNetwork=forgetWifiNetwork,window.refreshBluetoothStatus=refreshBluetoothStatus,window.toggleBluetoothPower=toggleBluetoothPower,window.toggleBluetoothDiscoverable=toggleBluetoothDiscoverable,window.startBluetoothScan=startBluetoothScan,window.showBluetoothDeviceDetails=showBluetoothDeviceDetails,window.closeBluetoothDeviceModal=closeBluetoothDeviceModal,window.pairBluetoothDevice=pairBluetoothDevice,window.enumerateBluetoothServices=enumerateBluetoothServices,window.clearBluetoothDevices=clearBluetoothDevices,window.loadFiles=loadFiles,window.downloadFile=downloadFile,window.deleteFile=deleteFile,window.uploadFile=uploadFile,window.clearFiles=clearFiles,window.refreshFiles=refreshFiles,window.closeFileModal=closeFileModal,window.openLootFile=openLootFile,window.loadSystemData=loadSystemData,window.sortProcesses=sortProcesses,window.refreshSystemStatus=refreshSystemStatus,window.loadDashboardData=loadDashboardData,window.updateDashboardStats=updateDashboardStats,window.loadNetkbData=loadNetkbData,window.refreshNetkbData=refreshNetkbData,window.filterNetkbData=filterNetkbData,window.searchNetkbData=searchNetkbData,window.clearNetkbSearch=clearNetkbSearch,window.showNetkbEntryDetail=showNetkbEntryDetail,window.closeNetkbModal=closeNetkbModal,window.exportNetkbData=exportNetkbData,window.exportNetkbEntry=exportNetkbEntry,window.researchEntry=researchEntry,window.researchVulnerability=researchVulnerability,window.exploitVulnerability=exploitVulnerability,window.triggerDeepScan=triggerDeepScan,window.handleCustomDeepScanRequest=handleCustomDeepScanRequest,window.testDeepScan=testDeepScan,window.openHostPanel=openHostPanel,window.closeHostPanel=closeHostPanel,window.renderHostPanel=renderHostPanel,window.debugDeepScanStates=function(){console.log("Current deep scan button states:",Object.fromEntries(deepScanButtonStates))},window.loadThreatIntelData=loadThreatIntelData,window.refreshThreatIntel=refreshThreatIntel,window.enrichTarget=enrichTarget,window.updateThreatIntelStats=updateThreatIntelStats,window.toggleHostDetails=toggleHostDetails,window.showVulnerabilityDetails=showVulnerabilityDetails,window.closeVulnerabilityModal=closeVulnerabilityModal,window.setThreatIntelFilter=setThreatIntelFilter;let aiInsightsCache={data:null,timestamp:null,ttl:36e5};async function loadAIInsights(){try{const e=Date.now();if(aiInsightsCache.data&&aiInsightsCache.timestamp){const t=e-aiInsightsCache.timestamp;if(t<aiInsightsCache.ttl)return console.log(`AI insights cached (${Math.floor(t/1e3)}s old, refreshes in ${Math.floor((aiInsightsCache.ttl-t)/1e3)}s)`),void displayAIInsights(aiInsightsCache.data)}const t=await networkAwareFetch("/api/ai/status"),n=await t.json(),a=document.getElementById("ai-insights-section"),s=document.getElementById("ai-not-configured");if(!n.enabled||!n.configured)return a&&(a.style.display="none"),s&&(s.style.display="block"),aiInsightsCache.data=null,void(aiInsightsCache.timestamp=null);a&&(a.style.display="block"),s&&(s.style.display="none");const o=document.getElementById("ai-model-name");o&&n.model&&(o.textContent=n.model),console.log("Fetching fresh AI insights from server...");const r=await networkAwareFetch("/api/ai/insights"),i=await r.json();aiInsightsCache.data=i,aiInsightsCache.timestamp=e,displayAIInsights(i)}catch(e){console.error("Error loading AI insights:",e)}}function displayAIInsights(e){if(e.enabled){const t=document.getElementById("ai-network-summary");t&&(t.innerHTML=formatAIText(e.network_summary||"Analyzing network..."));const n=document.getElementById("ai-vuln-section"),a=document.getElementById("ai-vuln-summary"),s=document.getElementById("ai-vuln-details"),o=document.getElementById("ai-vuln-toggle");if(a&&s)if(e.vulnerability_analysis){const{summary:t,details:r}=splitAIContent(e.vulnerability_analysis);a.innerHTML=formatAIText(t),s.innerHTML=formatAIText(r),o&&r.trim()?o.style.display="flex":o&&(o.style.display="none"),n&&(n.style.display="block")}else a.textContent="No vulnerabilities detected",s.innerHTML="",o&&(o.style.display="none"),n&&(n.style.display="block");const r=document.getElementById("ai-weakness-section"),i=document.getElementById("ai-weakness-summary"),l=document.getElementById("ai-weakness-details"),c=document.getElementById("ai-weakness-toggle");if(i&&l)if(e.weakness_analysis){const{summary:t,details:n}=splitAIContent(e.weakness_analysis);i.innerHTML=formatAIText(t),l.innerHTML=formatAIText(n),c&&n.trim()?c.style.display="flex":c&&(c.style.display="none"),r&&(r.style.display="block")}else i.textContent="Analyzing network topology...",l.innerHTML="",c&&(c.style.display="none"),r&&(r.style.display="block");const d=document.getElementById("ai-last-update");if(d&&aiInsightsCache.timestamp){const e=Math.floor((Date.now()-aiInsightsCache.timestamp)/1e3),t=Math.floor((aiInsightsCache.ttl-(Date.now()-aiInsightsCache.timestamp))/1e3);d.textContent=e<60?`Updated ${e}s ago (next refresh in ${Math.floor(t/60)}m)`:`Updated ${Math.floor(e/60)}m ago (next refresh in ${Math.floor(t/60)}m)`}}}async function refreshAIInsights(){try{aiInsightsCache.data=null,aiInsightsCache.timestamp=null,await networkAwareFetch("/api/ai/clear-cache",{method:"POST"});const e=document.getElementById("ai-network-summary"),t=document.getElementById("ai-vuln-summary"),n=document.getElementById("ai-weakness-summary");e&&(e.textContent="Generating new AI analysis..."),t&&(t.textContent="Analyzing vulnerabilities..."),n&&(n.textContent="Identifying network weaknesses..."),await loadAIInsights(),showNotification("AI insights refreshed successfully","success")}catch(e){console.error("Error refreshing AI insights:",e),showNotification("Failed to refresh AI insights","error")}}function splitAIContent(e){if(!e||"string"!=typeof e)return{summary:"",details:""};const t=e.split(/\n\n+/);if(t.length<=1)return e.length<=200?{summary:e,details:""}:{summary:e.substring(0,200)+"...",details:e};return{summary:t[0],details:t.slice(1).join("\n\n")}}function toggleAISection(e){const t=`ai-${e}-details`,n=`ai-${e}-toggle-text`,a=`ai-${e}-toggle-icon`,s=document.getElementById(t),o=document.getElementById(n),r=document.getElementById(a);if(!s)return;"none"===s.style.display?(s.style.display="block",o&&(o.textContent="Show Less"),r&&r.classList.add("rotate-180")):(s.style.display="none",o&&(o.textContent="Show More"),r&&r.classList.remove("rotate-180"))}let serverModeEnabled=!1,trafficCaptureRunning=!1,trafficRefreshInterval=null,advVulnRefreshInterval=null,trafficBandwidthHistory=[],trafficBandwidthChart=null,trafficProtocolChart=null;const TRAFFIC_HISTORY_SIZE=60;let ragnarLocalIps=new Set(["127.0.0.1","localhost"]);async function checkServerCapabilities(){try{const e=await fetch("/api/server/capabilities"),t=await e.json();if(console.log("[ServerMode] Capability check response:",t),t.success&&t.features){serverModeEnabled=t.features.server_mode;const e=document.querySelectorAll(".server-mode-feature");console.log(`[ServerMode] Found ${e.length} server-mode UI elements`),e.forEach(e=>{serverModeEnabled?e.classList.remove("hidden"):e.classList.add("hidden")});return applyWardrivingEnabledState(t.features?.wardriving_enabled||!1),serverModeEnabled?(console.log("[ServerMode] ✅ Server mode enabled - unlocking advanced features"),console.log("[ServerMode] Capabilities:",t.capabilities)):(console.log("[ServerMode] ⚠️ Server mode NOT enabled. Reasons:"),console.log("[ServerMode] - Architecture:",t.capabilities?.architecture),console.log("[ServerMode] - RAM:",t.capabilities?.total_ram_gb?.toFixed(2),"GB (need 7.5GB+)"),console.log("[ServerMode] - Cores:",t.capabilities?.cpu_cores,"(need 2+)"),console.log("[ServerMode] - Is Pi Zero:",t.capabilities?.is_pi_zero),console.log("[ServerMode] - Full capabilities:",t.capabilities)),t}t.success||console.error("[ServerMode] API error:",t.error),window._updateNavMode&&window._updateNavMode()}catch(e){console.warn("[ServerMode] Could not check server capabilities:",e)}return null}async function loadTrafficAnalysisData(){try{const[e,...t]=await Promise.all([fetch("/api/traffic/status"),loadTrafficHosts().catch(e=>console.warn("Traffic hosts failed:",e)),loadTrafficConnections().catch(e=>console.warn("Traffic connections failed:",e)),loadTrafficAlerts().catch(e=>console.warn("Traffic alerts failed:",e)),loadTrafficProtocols().catch(e=>console.warn("Traffic protocols failed:",e)),loadTrafficDnsAnalysis().catch(e=>console.warn("Traffic DNS failed:",e)),loadTrafficTopTalkers().catch(e=>console.warn("Traffic top talkers failed:",e)),loadTrafficPortActivity().catch(e=>console.warn("Traffic ports failed:",e))]),n=await e.json();if(!n.success||!n.available)return void showTrafficNotAvailable();hideTrafficNotAvailable(),updateTrafficSummary(n.summary),updateTrafficSecurityMetrics(n.summary),updateTrafficBandwidthChart(n.summary),trafficCaptureRunning="running"===n.summary?.status,updateTrafficCaptureButton()}catch(e){console.error("Error loading traffic analysis:",e),showTrafficNotAvailable()}}function showTrafficNotAvailable(){const e=document.getElementById("traffic-not-available");e&&e.classList.remove("hidden")}function hideTrafficNotAvailable(){const e=document.getElementById("traffic-not-available");e&&e.classList.add("hidden")}function updateTrafficSummary(e){if(!e)return;updateElement("traffic-packets-sec",e.packets_per_second||0),updateElement("traffic-mbps",(e.throughput_mbps||e.mbps||0).toFixed(2)),updateElement("traffic-hosts",e.unique_hosts||e.active_hosts||0),updateElement("traffic-connections",e.active_connections||0),updateElement("traffic-alerts",e.unacknowledged_alerts||e.total_alerts||e.alert_count||0),updateElement("traffic-packets-total",formatNumber(e.total_packets||0)+" total"),updateElement("traffic-bytes-total",formatBytes(e.total_bytes||0)+" total"),updateElement("traffic-dns-queries",(e.dns_queries_captured||e.dns_queries_logged||0)+" DNS");const t=e.excluded_local_ips||e.local_ips;t&&Array.isArray(t)&&(ragnarLocalIps=new Set(t),console.log("[Traffic] Ragnar local IPs:",Array.from(ragnarLocalIps)))}function updateTrafficSecurityMetrics(e){if(!e)return;const t=e.unacknowledged_alerts||e.total_alerts||e.alert_count||0,n=Math.min(100,10*t);let a,s,o;0===n?(a="#22c55e",s="No threats detected",o="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"):n<30?(a="#eab308",s="Low risk activity",o="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"):n<60?(a="#f97316",s="Moderate risk detected",o="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"):(a="#ef4444",s="High risk - investigate",o="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z");const r=document.getElementById("traffic-risk-score"),i=document.getElementById("traffic-risk-label"),l=document.getElementById("traffic-risk-circle"),c=document.getElementById("traffic-risk-icon"),d=document.getElementById("traffic-risk-card");r&&(r.textContent=n,r.style.color=a),i&&(i.textContent=s),l&&(l.setAttribute("stroke",a),l.setAttribute("stroke-dasharray",`${n}, 100`)),c&&(c.setAttribute("stroke",a),c.querySelector("path").setAttribute("d",o)),d&&(d.style.borderColor=a)}function updateTrafficBandwidthChart(e){if(!e)return;for(trafficBandwidthHistory.push({timestamp:Date.now(),mbps:e.throughput_mbps||e.mbps||0,packetsPerSec:e.packets_per_second||0});trafficBandwidthHistory.length>60;)trafficBandwidthHistory.shift();const t=document.getElementById("traffic-bandwidth-chart");if(!t)return;const n=t.getContext("2d"),a=t.parentElement.offsetWidth,s=128;if(t.width=a,t.height=s,n.clearRect(0,0,a,s),trafficBandwidthHistory.length<2)return;const o=Math.max(...trafficBandwidthHistory.map(e=>e.mbps),.1),r=document.getElementById("traffic-chart-max");r&&(r.textContent=o.toFixed(2)+" Mbps"),n.strokeStyle="rgba(100, 116, 139, 0.2)",n.lineWidth=1;for(let e=0;e<=4;e++){const t=32*e;n.beginPath(),n.moveTo(0,t),n.lineTo(a,t),n.stroke()}const i=a/59,l=n.createLinearGradient(0,0,0,s);if(l.addColorStop(0,"rgba(34, 211, 238, 0.3)"),l.addColorStop(1,"rgba(34, 211, 238, 0)"),n.beginPath(),n.moveTo(0,s),trafficBandwidthHistory.forEach((e,t)=>{const a=t*i,r=s-e.mbps/o*118;n.lineTo(a,r)}),n.lineTo((trafficBandwidthHistory.length-1)*i,s),n.closePath(),n.fillStyle=l,n.fill(),n.beginPath(),n.strokeStyle="#22d3ee",n.lineWidth=2,trafficBandwidthHistory.forEach((e,t)=>{const a=t*i,r=s-e.mbps/o*118;0===t?n.moveTo(a,r):n.lineTo(a,r)}),n.stroke(),trafficBandwidthHistory.length>0){const e=trafficBandwidthHistory[trafficBandwidthHistory.length-1],t=(trafficBandwidthHistory.length-1)*i,a=s-e.mbps/o*118;n.beginPath(),n.fillStyle="#22d3ee",n.arc(t,a,4,0,2*Math.PI),n.fill()}}function formatNumber(e){return e>=1e9?(e/1e9).toFixed(1)+"B":e>=1e6?(e/1e6).toFixed(1)+"M":e>=1e3?(e/1e3).toFixed(1)+"K":e.toString()}async function loadTrafficHosts(){try{const e=await fetch("/api/traffic/hosts?limit=15&sort=bytes"),t=await e.json(),n=document.getElementById("traffic-top-hosts");if(!n)return;if(!t.success||!t.hosts?.length)return n.innerHTML='<p class="text-gray-400 text-center py-8">No host data available</p>',void updateTrafficDirectionChart(0,0);let a=0,s=0;t.hosts.forEach(e=>{a+=e.bytes_in||0,s+=e.bytes_out||0}),updateTrafficDirectionChart(a,s),n.innerHTML=t.hosts.map(e=>{const t=ragnarLocalIps.has(e.ip),n=t?'<span class="ml-1 px-1 py-0.5 text-xs bg-purple-600 text-purple-100 rounded">RAGNAR</span>':"",a=t?"bg-purple-900 bg-opacity-30 border border-purple-600 border-opacity-30":"bg-slate-700 bg-opacity-50 hover:bg-slate-600 hover:bg-opacity-50",s=e.ports_contacted?.length||0,o=Object.keys(e.protocols||{}).length;return`\n <div class="flex items-center justify-between p-2 ${a} rounded-lg cursor-pointer transition-colors"\n onclick="showTrafficHostDetail('${escapeHtml(e.ip)}')" title="Click for details">\n <div class="flex items-center space-x-3">\n <div class="w-2 h-2 rounded-full ${e.packets_in>e.packets_out?"bg-green-400":"bg-blue-400"}"></div>\n <div>\n <div class="font-mono text-sm flex items-center">${escapeHtml(e.ip)}${n}</div>\n <div class="text-xs text-gray-400">\n ${formatBytes(e.total_bytes)} | ${s} ports | ${o} protocols\n </div>\n </div>\n </div>\n <div class="text-right flex items-center gap-2">\n <div>\n <div class="text-xs text-green-400">↓ ${formatBytes(e.bytes_in)}</div>\n <div class="text-xs text-blue-400">↑ ${formatBytes(e.bytes_out)}</div>\n </div>\n <svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>\n </svg>\n </div>\n </div>\n `}).join("")}catch(e){console.error("Error loading traffic hosts:",e)}}function updateTrafficDirectionChart(e,t){const n=e+t;let a=50,s=50;n>0&&(a=Math.round(e/n*100),s=100-a);const o=document.getElementById("traffic-direction-in"),r=document.getElementById("traffic-direction-out"),i=document.getElementById("traffic-in-percent"),l=document.getElementById("traffic-out-percent");o&&(o.style.width=`${a}%`),r&&(r.style.width=`${s}%`),i&&(i.textContent=a),l&&(l.textContent=s)}async function loadTrafficConnections(){try{const e=await fetch("/api/traffic/connections?limit=20"),t=await e.json(),n=document.getElementById("traffic-connections-list");if(!n)return;if(!t.success||!t.connections?.length)return void(n.innerHTML='<p class="text-gray-400 text-center py-8">No active connections</p>');n.innerHTML=t.connections.map((e,t)=>{const n=e.duration_seconds?formatDuration(e.duration_seconds):"N/A";return`\n <div class="p-2 bg-slate-700 bg-opacity-50 hover:bg-slate-600 hover:bg-opacity-50 rounded-lg text-xs font-mono cursor-pointer transition-colors"\n onclick="showTrafficConnectionDetail(decodeURIComponent('${encodeURIComponent(JSON.stringify(e))}'))" title="Click for details">\n <div class="flex items-center justify-between">\n <span class="text-cyan-400">${escapeHtml(e.src_ip)}:${e.src_port}</span>\n <span class="text-gray-500 mx-1">→</span>\n <span class="text-green-400">${escapeHtml(e.dst_ip)}:${e.dst_port}</span>\n <svg class="w-3 h-3 text-gray-500 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>\n </svg>\n </div>\n <div class="text-gray-400 mt-1 flex justify-between">\n <span>${e.protocol.toUpperCase()} | ${e.packets_sent} pkts | ${formatBytes(e.bytes_sent)}</span>\n <span class="text-gray-500">${n}</span>\n </div>\n </div>\n `}).join("")}catch(e){console.error("Error loading traffic connections:",e)}}async function loadTrafficAlerts(){try{const e=await fetch("/api/traffic/alerts?limit=20"),t=await e.json(),n=document.getElementById("traffic-alerts-list");if(!n)return;if(!t.success||!t.alerts?.length)return void(n.innerHTML='<p class="text-gray-400 text-center py-8">No alerts - network appears clean</p>');const a={};t.alerts.forEach(e=>{a[e.category]=(a[e.category]||0)+1}),n.innerHTML=t.alerts.map(e=>{const t={critical:"border-red-600 bg-red-900",high:"border-orange-500 bg-orange-900",medium:"border-yellow-500 bg-yellow-900",low:"border-blue-500 bg-blue-900",info:"border-gray-500 bg-gray-800"},n={critical:"bg-red-600 text-white",high:"bg-orange-600 text-white",medium:"bg-yellow-600 text-black",low:"bg-blue-600 text-white",info:"bg-gray-600 text-white"},a=t[e.level]||t.info,s=n[e.level]||n.info,o=e.timestamp?new Date(e.timestamp).toLocaleTimeString():"";return`\n <div class="p-2 ${a} bg-opacity-30 border-l-4 rounded-lg hover:bg-opacity-50 transition-colors">\n <div class="flex items-center justify-between mb-1">\n <div class="flex items-center space-x-2">\n <span class="px-1.5 py-0.5 ${s} text-xs rounded uppercase font-semibold">${e.level}</span>\n <span class="font-medium text-sm">${escapeHtml(formatAlertCategory(e.category))}</span>\n </div>\n <span class="text-xs text-gray-500">${o}</span>\n </div>\n <div class="text-xs text-gray-300">${escapeHtml(e.message)}</div>\n ${e.src_ip?`\n <div class="text-xs text-gray-400 mt-1 font-mono flex items-center justify-between">\n <span>Source: ${escapeHtml(e.src_ip)}${e.dst_ip?" → "+escapeHtml(e.dst_ip):""}</span>\n <button onclick="event.stopPropagation(); showTrafficHostDetail('${escapeHtml(e.src_ip)}')"\n class="px-2 py-0.5 bg-slate-600 hover:bg-slate-500 text-gray-200 rounded text-xs transition-colors"\n title="View source host details">\n Investigate\n </button>\n </div>\n `:""}\n ${e.details?`<div class="text-xs text-gray-500 mt-1">${formatAlertDetails(e.details)}</div>`:""}\n </div>\n `}).join("")}catch(e){console.error("Error loading traffic alerts:",e)}}function formatAlertCategory(e){return{suspicious_port:"Suspicious Port",port_scan:"Port Scan Detected",c2_beacon:"C2 Beacon Pattern",dns_tunnel:"DNS Tunneling Suspect",high_traffic:"High Traffic Volume",unknown_protocol:"Unknown Protocol"}[e]||e.replace(/_/g," ").replace(/\b\w/g,e=>e.toUpperCase())}function formatAlertDetails(e){if(!e||"object"!=typeof e)return"";const t=new Set(["mitre","channels","tags"]),n=new Set(["first_seen","last_seen","protocol"]),a=(e,n)=>{if(null==n||""===n)return null;if(Array.isArray(n)){if(0===n.length)return null;if(t.has(e))return n.map(e=>`<span class="inline-block px-1.5 py-0.5 mr-1 rounded bg-slate-700 text-cyan-300 text-xs">${escapeHtml(String(e))}</span>`).join("");return n.slice(0,5).map(e=>escapeHtml(String(e))).join(", ")+(n.length>5?` (+${n.length-5} more)`:"")}if("object"==typeof n)try{return escapeHtml(JSON.stringify(n))}catch{return""}return"number"!=typeof n||Number.isInteger(n)?escapeHtml(String(n)):escapeHtml(n.toFixed(3))},s=[];for(const[t,o]of Object.entries(e)){if(n.has(t))continue;const e=a(t,o);if(null===e||""===e)continue;const r=t.replace(/_/g," ");s.push(`<div><span class="text-gray-400">${escapeHtml(r)}:</span> <span class="text-gray-200">${e}</span></div>`)}return s.length?`<div class="mt-2 grid grid-cols-1 sm:grid-cols-2 gap-x-3 gap-y-0.5 text-xs">${s.join("")}</div>`:""}async function loadTrafficProtocols(){try{const e=await fetch("/api/traffic/summary"),t=await e.json(),n=document.getElementById("traffic-protocols"),a=document.getElementById("traffic-protocol-total");if(!n)return;if(!t.success||!t.protocols||0===Object.keys(t.protocols).length)return n.innerHTML='<p class="text-gray-400 text-center py-4 text-xs">No protocol data</p>',void drawProtocolDonutChart({});const s=t.protocols,o=Object.values(s).reduce((e,t)=>e+t,0);a&&(a.textContent=formatNumber(o));const r={tcp:"#22d3ee",udp:"#a855f7",icmp:"#f97316",ip:"#3b82f6",arp:"#eab308",other:"#6b7280"};drawProtocolDonutChart(s,r),n.innerHTML=Object.entries(s).sort((e,t)=>t[1]-e[1]).slice(0,6).map(([e,t])=>{const n=o>0?(t/o*100).toFixed(1):0;return`\n <div class="flex items-center justify-between text-xs">\n <div class="flex items-center space-x-2">\n <span class="w-2 h-2 rounded-full" style="background-color: ${r[e.toLowerCase()]||r.other}"></span>\n <span class="uppercase font-mono text-gray-300">${escapeHtml(e)}</span>\n </div>\n <span class="text-gray-400">${n}%</span>\n </div>\n `}).join("")}catch(e){console.error("Error loading traffic protocols:",e)}}function drawProtocolDonutChart(e,t={}){const n=document.getElementById("traffic-protocol-chart");if(!n)return;const a=n.getContext("2d");a.clearRect(0,0,128,128);const s=Object.entries(e).sort((e,t)=>t[1]-e[1]),o=Object.values(e).reduce((e,t)=>e+t,0);if(0===o)return a.beginPath(),a.arc(64,64,55,0,2*Math.PI),a.arc(64,64,35,0,2*Math.PI,!0),a.fillStyle="rgba(100, 116, 139, 0.3)",void a.fill();const r=["#22d3ee","#a855f7","#f97316","#3b82f6","#eab308","#6b7280"];let i=-Math.PI/2;s.forEach(([e,n],s)=>{const l=n/o*Math.PI*2,c=t[e.toLowerCase()]||r[s%r.length];a.beginPath(),a.moveTo(64,64),a.arc(64,64,55,i,i+l),a.closePath(),a.fillStyle=c,a.fill(),i+=l}),a.beginPath(),a.arc(64,64,35,0,2*Math.PI),a.fillStyle="#1e293b",a.fill()}async function loadTrafficDnsAnalysis(){try{const e=await fetch("/api/traffic/summary"),t=await e.json(),n=document.getElementById("traffic-dns-list"),a=document.getElementById("traffic-dns-total"),s=document.getElementById("traffic-dns-unique"),o=document.getElementById("traffic-dns-suspicious");if(!t.success)return void(n&&(n.innerHTML='<p class="text-gray-400 text-center py-4">No DNS data</p>'));const r=t.summary||{},i=r.dns_queries_captured||r.dns_queries_logged||t.dns_queries_logged||0;a&&(a.textContent=i);const l=await fetch("/api/traffic/hosts?limit=50&sort=bytes"),c=await l.json();if(!c.success||!c.hosts)return void(n&&(n.innerHTML='<p class="text-gray-400 text-center py-4">No DNS queries logged</p>'));const d=[],u=new Set;let p=0;if(c.hosts.forEach(e=>{e.dns_queries&&e.dns_queries.length>0&&e.dns_queries.forEach(t=>{d.push({ip:e.ip,query:t});const n=t.match(/([a-zA-Z0-9][-a-zA-Z0-9]*\.)+[a-zA-Z]{2,}/);n&&(u.add(n[0].toLowerCase()),(n[0].length>50||/^[a-f0-9]{16,}\./.test(n[0].toLowerCase()))&&p++)})}),s&&(s.textContent=u.size),o&&(o.textContent=p),updateElement("traffic-threat-c2",p>0?p:0),0===d.length)return void(n&&(n.innerHTML='<p class="text-gray-400 text-center py-4">No DNS queries logged</p>'));n&&(n.innerHTML=d.slice(-20).reverse().map(e=>`\n <div class="flex items-center justify-between p-1 ${e.query.length>100||/[a-f0-9]{16,}/.test(e.query)?"bg-red-900 bg-opacity-20 border-l-2 border-red-500":"bg-slate-700 bg-opacity-30"} rounded hover:bg-slate-600 hover:bg-opacity-50 transition-colors cursor-pointer" onclick="showTrafficHostDetail('${escapeHtml(e.ip)}')" title="Click to view host details">\n <span class="text-gray-300 truncate flex-1" title="${escapeHtml(e.query)}">${escapeHtml(e.query.substring(0,60))}${e.query.length>60?"...":""}</span>\n <span class="text-cyan-400 ml-2 font-mono hover:underline">${escapeHtml(e.ip)}</span>\n </div>\n `).join(""))}catch(e){console.error("Error loading DNS analysis:",e)}}async function loadTrafficTopTalkers(){try{const e=await fetch("/api/traffic/connections?limit=50"),t=await e.json(),n=document.getElementById("traffic-top-talkers");if(!n)return;if(!t.success||!t.connections?.length)return void(n.innerHTML='<p class="text-gray-400 text-center py-4">No connection data</p>');const a={};t.connections.forEach(e=>{const t=`${e.src_ip}→${e.dst_ip}`;a[t]||(a[t]={src:e.src_ip,dst:e.dst_ip,bytes:0,packets:0,connections:0,protocols:new Set}),a[t].bytes+=e.bytes_sent||0,a[t].packets+=e.packets_sent||0,a[t].connections++,a[t].protocols.add(e.protocol)});const s=Object.values(a).sort((e,t)=>t.bytes-e.bytes).slice(0,10),o=s[0]?.bytes||1;n.innerHTML=s.map(e=>{const t=e.bytes/o*100,n=Array.from(e.protocols).join(", ").toUpperCase();return`\n <div class="relative p-2 bg-slate-700 bg-opacity-30 rounded overflow-hidden hover:bg-slate-600 hover:bg-opacity-40 transition-colors">\n <div class="absolute inset-0 bg-gradient-to-r from-cyan-600 to-transparent opacity-20" style="width: ${t}%"></div>\n <div class="relative flex items-center justify-between">\n <div class="flex-1 min-w-0">\n <div class="flex items-center space-x-2 text-xs">\n <span class="text-cyan-400 font-mono truncate cursor-pointer hover:underline" onclick="showTrafficHostDetail('${escapeHtml(e.src)}')" title="View source host details">${escapeHtml(e.src)}</span>\n <span class="text-gray-500">→</span>\n <span class="text-green-400 font-mono truncate cursor-pointer hover:underline" onclick="showTrafficHostDetail('${escapeHtml(e.dst)}')" title="View destination host details">${escapeHtml(e.dst)}</span>\n </div>\n <div class="text-xs text-gray-500 mt-1">\n ${n} | ${e.connections} conn | ${e.packets} pkts\n </div>\n </div>\n <div class="text-right text-xs ml-2">\n <div class="text-white font-semibold">${formatBytes(e.bytes)}</div>\n </div>\n </div>\n </div>\n `}).join("")}catch(e){console.error("Error loading top talkers:",e)}}document.addEventListener("DOMContentLoaded",function(){checkServerCapabilities()});const SUSPICIOUS_PORT_INFO={4444:{name:"Metasploit Default",reason:"Default Metasploit reverse shell listener port",severity:"critical",category:"Exploit Framework"},5555:{name:"Android ADB",reason:"Android Debug Bridge - often exploited for unauthorized device access",severity:"high",category:"Remote Access"},6666:{name:"IRC/Backdoor",reason:"Commonly used by IRC botnets and backdoor trojans",severity:"high",category:"Botnet/C2"},1234:{name:"Common Backdoor",reason:"Frequently used by malware for reverse shells",severity:"medium",category:"Backdoor"},31337:{name:"Elite/Back Orifice",reason:'Historic "elite" port used by Back Orifice and many trojans',severity:"critical",category:"Trojan"},12345:{name:"NetBus Trojan",reason:"Default port for NetBus remote administration trojan",severity:"critical",category:"Trojan"},65535:{name:"Max Port Backdoor",reason:"Maximum port number - often used by malware to avoid detection",severity:"medium",category:"Evasion"},1337:{name:"Leet Port",reason:"Common hacker culture port used by various malware",severity:"medium",category:"Backdoor"},9001:{name:"Tor/Hidden Service",reason:"Default Tor ORPort - may indicate anonymization or C2",severity:"medium",category:"Anonymization"},6667:{name:"IRC Default",reason:"IRC server port - commonly used for botnet C2 communication",severity:"high",category:"Botnet/C2"},6697:{name:"IRC SSL",reason:"IRC over SSL - used by botnets for encrypted C2",severity:"high",category:"Botnet/C2"},8080:{name:"Alt HTTP/Proxy",reason:"Alternative HTTP port - check for unauthorized web services",severity:"low",category:"Web Service"},3128:{name:"Squid Proxy",reason:"Default Squid proxy port - may indicate data exfiltration",severity:"medium",category:"Proxy"},1080:{name:"SOCKS Proxy",reason:"SOCKS proxy port - often used for tunneling and evasion",severity:"medium",category:"Proxy"},7777:{name:"Game/Backdoor",reason:"Used by various trojans and game servers",severity:"medium",category:"Backdoor"},5900:{name:"VNC",reason:"VNC remote desktop - verify if authorized",severity:"medium",category:"Remote Access"},5901:{name:"VNC Display 1",reason:"VNC remote desktop display 1",severity:"medium",category:"Remote Access"},27374:{name:"Sub7 Trojan",reason:"Default port for Sub7 remote access trojan",severity:"critical",category:"Trojan"},20:{name:"FTP Data",reason:"FTP data transfer - unencrypted, credentials may leak",severity:"low",category:"Legacy Protocol"},23:{name:"Telnet",reason:"Unencrypted remote access - credentials sent in plaintext",severity:"high",category:"Legacy Protocol"},69:{name:"TFTP",reason:"Trivial FTP - no authentication, often used in attacks",severity:"medium",category:"Legacy Protocol"},111:{name:"RPCBind",reason:"RPC portmapper - can expose internal services",severity:"medium",category:"Service Exposure"},135:{name:"MS-RPC",reason:"Microsoft RPC endpoint mapper - common attack vector",severity:"medium",category:"Windows Service"},137:{name:"NetBIOS-NS",reason:"NetBIOS Name Service - information disclosure risk",severity:"low",category:"Windows Service"},138:{name:"NetBIOS-DGM",reason:"NetBIOS Datagram - legacy Windows networking",severity:"low",category:"Windows Service"},139:{name:"NetBIOS-SSN",reason:"NetBIOS Session - SMB over NetBIOS",severity:"medium",category:"Windows Service"},445:{name:"SMB",reason:"Server Message Block - common ransomware propagation vector",severity:"high",category:"Windows Service"},512:{name:"rexec",reason:"Remote execution - no encryption, easily exploited",severity:"high",category:"Legacy Protocol"},513:{name:"rlogin",reason:"Remote login - no encryption, trust-based auth",severity:"high",category:"Legacy Protocol"},514:{name:"rsh/syslog",reason:"Remote shell or syslog - no encryption",severity:"high",category:"Legacy Protocol"},2049:{name:"NFS",reason:"Network File System - verify authorization",severity:"medium",category:"File Sharing"}};async function loadTrafficPortActivity(){try{const e=await fetch("/api/traffic/hosts?limit=100&sort=bytes"),t=await e.json();if(!t.success||!t.hosts)return;const n={},a={},s=new Set(Object.keys(SUSPICIOUS_PORT_INFO).map(e=>parseInt(e))),o={};let r=0;t.hosts.forEach(e=>{e.ports_contacted&&(e.ports_contacted.forEach(t=>{n[t]=(n[t]||0)+1,a[t]||(a[t]=new Set),a[t].add(e.ip),s.has(t)&&(o[t]||(o[t]={count:0,hosts:new Set,info:SUSPICIOUS_PORT_INFO[t]}),o[t].count++,o[t].hosts.add(e.ip))}),e.ports_contacted.length>50&&r++)});const i=Object.keys(o).length;updateElement("port-80-count",n[80]||0),updateElement("port-443-count",n[443]||0),updateElement("port-53-count",n[53]||0),updateElement("port-22-count",n[22]||0),updateElement("port-3389-count",n[3389]||0),updateElement("port-suspicious-count",i),updateElement("traffic-threat-port-scans",r),updateElement("traffic-threat-suspicious",i),updateElement("traffic-unique-ports",Object.keys(n).length+" ports");const l=document.getElementById("suspicious-ports-section"),c=document.getElementById("suspicious-ports-list");if(l&&c)if(i>0){l.style.display="block";const e={critical:0,high:1,medium:2,low:3},t=Object.entries(o).sort((t,n)=>e[t[1].info.severity]-e[n[1].info.severity]);c.innerHTML=t.map(([e,t])=>{const n=t.info,a=Array.from(t.hosts).slice(0,3),s=t.hosts.size>3?` +${t.hosts.size-3} more`:"",o={critical:"bg-red-600 text-white",high:"bg-orange-600 text-white",medium:"bg-yellow-600 text-black",low:"bg-blue-600 text-white"},r=o[n.severity]||o.medium,i=a.map(e=>`<span class="cursor-pointer hover:underline" onclick="event.stopPropagation(); showTrafficHostDetail('${escapeHtml(e)}')">${escapeHtml(e)}</span>`).join(", ");return`\n <div class="p-2 bg-slate-800 bg-opacity-50 rounded border-l-4 ${"critical"===n.severity?"border-red-500":"high"===n.severity?"border-orange-500":"medium"===n.severity?"border-yellow-500":"border-blue-500"} hover:bg-slate-700 hover:bg-opacity-50 transition-colors">\n <div class="flex items-center justify-between mb-1">\n <div class="flex items-center space-x-2">\n <span class="font-mono font-bold text-red-400 cursor-pointer hover:underline" onclick="showTrafficPortDetail(${e})" title="View port details">${e}</span>\n <span class="text-xs px-1.5 py-0.5 ${r} rounded uppercase font-semibold">${n.severity}</span>\n <span class="text-xs text-gray-400">${escapeHtml(n.name)}</span>\n </div>\n <span class="text-xs px-2 py-0.5 bg-slate-700 rounded text-gray-300">${escapeHtml(n.category)}</span>\n </div>\n <div class="text-xs text-gray-400 mb-1">${escapeHtml(n.reason)}</div>\n <div class="text-xs text-gray-500">\n <span class="text-yellow-400">${t.count}</span> connections from\n <span class="font-mono text-cyan-400">${i}${s}</span>\n </div>\n </div>\n `}).join("")}else l.style.display="none",c.innerHTML="";const d=document.getElementById("traffic-all-ports");if(d){const e=Object.entries(n).sort((e,t)=>t[1]-e[1]).slice(0,50);0===e.length?d.innerHTML='<span class="text-xs text-gray-500 px-2 py-1 bg-slate-700 rounded">No port data</span>':d.innerHTML=e.map(([e,t])=>{const n=parseInt(e),a=s.has(n),o=[80,443,22,53,25,21,3389,8443].includes(n);let r="bg-slate-600 text-gray-300 hover:bg-slate-500",i=`${t} connections - Click for details`;if(a){r="bg-red-900 text-red-400 border border-red-600 hover:bg-red-800";const e=SUSPICIOUS_PORT_INFO[n];i=`${e.name}: ${e.reason} (${t} connections) - Click for details`}else o&&(r="bg-cyan-900 text-cyan-400 hover:bg-cyan-800");return`<span class="text-xs px-2 py-1 ${r} rounded font-mono cursor-pointer transition-colors" title="${escapeHtml(i)}" onclick="showTrafficPortDetail(${e})">${e}</span>`}).join("")}}catch(e){console.error("Error loading port activity:",e)}}async function toggleTrafficCapture(){try{const e=trafficCaptureRunning?"/api/traffic/stop":"/api/traffic/start",t=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({})}),n=await t.json();n.success?(trafficCaptureRunning=!trafficCaptureRunning,updateTrafficCaptureButton(),trafficCaptureRunning?(trafficRefreshInterval=setInterval(refreshTrafficData,3e3),showNotification("Traffic capture started","success")):(trafficRefreshInterval&&(clearInterval(trafficRefreshInterval),trafficRefreshInterval=null),showNotification("Traffic capture stopped","info"))):showNotification(n.error||"Failed to toggle capture","error")}catch(e){console.error("Error toggling traffic capture:",e),showNotification("Failed to toggle traffic capture","error")}}function updateTrafficCaptureButton(){const e=document.getElementById("traffic-toggle-btn");e&&(trafficCaptureRunning?(e.innerHTML='\n <svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"></path>\n </svg>\n Stop Capture\n ',e.classList.remove("bg-green-600","hover:bg-green-700"),e.classList.add("bg-red-600","hover:bg-red-700")):(e.innerHTML='\n <svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path>\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>\n </svg>\n Start Capture\n ',e.classList.remove("bg-red-600","hover:bg-red-700"),e.classList.add("bg-green-600","hover:bg-green-700")))}async function refreshTrafficData(){"traffic"===currentTab&&await loadTrafficAnalysisData()}async function showTrafficHostDetail(e){const t=document.getElementById("traffic-host-modal"),n=document.getElementById("traffic-host-modal-ip"),a=document.getElementById("traffic-host-modal-content");if(t&&a){n.textContent=e,a.innerHTML='<div class="text-center py-8"><div class="animate-spin w-8 h-8 border-2 border-cyan-400 border-t-transparent rounded-full mx-auto"></div><p class="text-gray-400 mt-2">Loading host details...</p></div>',t.classList.remove("hidden"),t.classList.add("flex");try{const t=await fetch(`/api/traffic/host/${encodeURIComponent(e)}`),n=await t.json();if(!n.success||!n.host)return void(a.innerHTML='<p class="text-red-400 text-center py-8">Failed to load host details</p>');const s=n.host,o=ragnarLocalIps.has(s.ip),r=Object.entries(s.protocols||{}),i=s.ports_contacted||[],l=s.dns_queries||[],[c,d,u,p]=await fetchHostInvestigationData(e),g=s.first_seen?new Date(s.first_seen).toLocaleString():"N/A",m=s.last_seen?new Date(s.last_seen).toLocaleString():"N/A",f=e=>({21:"FTP",22:"SSH",23:"Telnet",25:"SMTP",53:"DNS",67:"DHCP",68:"DHCP",80:"HTTP",110:"POP3",123:"NTP",135:"RPC",137:"NetBIOS",138:"NetBIOS",139:"NetBIOS",143:"IMAP",161:"SNMP",162:"SNMP",389:"LDAP",443:"HTTPS",445:"SMB",465:"SMTPS",514:"Syslog",587:"SMTP",631:"IPP",636:"LDAPS",993:"IMAPS",995:"POP3S",1433:"MSSQL",1521:"Oracle",1883:"MQTT",1900:"UPnP",2049:"NFS",2181:"ZooKeeper",2375:"Docker",3005:"Services",3306:"MySQL",3389:"RDP",5e3:"Services",5060:"SIP",5061:"SIPS",5353:"mDNS",5432:"PostgreSQL",5631:"pcAnywhere",5632:"pcAnywhere",5672:"AMQP",5900:"VNC",6379:"Redis",6667:"IRC",6881:"BitTorrent",7844:"Services",8080:"HTTP-Alt",8081:"HTTP-Alt",8086:"InfluxDB",8181:"HTTP-Alt",8443:"HTTPS-Alt",9e3:"Services",9090:"Prometheus",9100:"Printing",9418:"Git",9999:"Services",25565:"Minecraft",27017:"MongoDB"}[e]||"");a.innerHTML=`\n \x3c!-- Host Header --\x3e\n <div class="p-4 bg-slate-800 rounded-lg">\n <div class="flex items-center justify-between mb-2">\n <div class="flex items-center gap-2">\n <span class="font-mono text-lg text-white">${escapeHtml(s.ip)}</span>\n ${o?'<span class="px-2 py-0.5 text-xs bg-purple-600 text-purple-100 rounded">RAGNAR</span>':""}\n ${s.hostname?`<span class="text-gray-400">(${escapeHtml(s.hostname)})</span>`:""}\n </div>\n ${s.mac?`<span class="font-mono text-xs text-gray-500">${escapeHtml(s.mac)}</span>`:""}\n </div>\n <div class="grid grid-cols-2 sm:grid-cols-4 gap-3 text-center text-sm">\n <div class="bg-slate-700 rounded p-2">\n <div class="text-cyan-400 font-bold">${formatBytes(s.total_bytes)}</div>\n <div class="text-xs text-gray-500">Total Traffic</div>\n </div>\n <div class="bg-slate-700 rounded p-2">\n <div class="text-green-400 font-bold">${s.total_packets||0}</div>\n <div class="text-xs text-gray-500">Packets</div>\n </div>\n <div class="bg-slate-700 rounded p-2">\n <div class="text-blue-400 font-bold">${i.length}</div>\n <div class="text-xs text-gray-500">Ports</div>\n </div>\n <div class="bg-slate-700 rounded p-2">\n <div class="text-purple-400 font-bold">${s.connections_active||0}</div>\n <div class="text-xs text-gray-500">Active Conns</div>\n </div>\n </div>\n </div>\n\n \x3c!-- Traffic Direction --\x3e\n <div class="p-4 bg-slate-800 rounded-lg">\n <h4 class="text-sm font-semibold text-gray-300 mb-3">Traffic Direction</h4>\n <div class="grid grid-cols-2 gap-4">\n <div class="text-center">\n <div class="text-2xl font-bold text-green-400">↓ ${formatBytes(s.bytes_in||0)}</div>\n <div class="text-xs text-gray-500">${s.packets_in||0} packets inbound</div>\n </div>\n <div class="text-center">\n <div class="text-2xl font-bold text-blue-400">↑ ${formatBytes(s.bytes_out||0)}</div>\n <div class="text-xs text-gray-500">${s.packets_out||0} packets outbound</div>\n </div>\n </div>\n </div>\n\n \x3c!-- Protocols --\x3e\n <div class="p-4 bg-slate-800 rounded-lg">\n <h4 class="text-sm font-semibold text-gray-300 mb-3">Protocols (${r.length})</h4>\n <div class="flex flex-wrap gap-2">\n ${r.length>0?r.map(([e,t])=>`\n <span class="px-3 py-1 bg-slate-700 rounded-full text-sm">\n <span class="text-purple-400 font-semibold">${escapeHtml(e.toUpperCase())}</span>\n <span class="text-gray-400 ml-1">${t}</span>\n </span>\n `).join(""):'<span class="text-gray-500">No protocol data</span>'}\n </div>\n </div>\n\n \x3c!-- Ports Contacted --\x3e\n <div class="p-4 bg-slate-800 rounded-lg">\n <h4 class="text-sm font-semibold text-gray-300 mb-3">Ports Contacted (${i.length})</h4>\n <div class="flex flex-wrap gap-2 max-h-32 overflow-y-auto">\n ${i.length>0?i.sort((e,t)=>e-t).map(e=>{const t=f(e);return`<span class="px-2 py-1 ${[4444,5555,6666,31337,12345,54321].includes(e)?"bg-red-900 text-red-300 border border-red-600":t?"bg-cyan-900 bg-opacity-50 text-cyan-300":"bg-slate-700 text-gray-300"} rounded text-xs font-mono" title="${t||"Unknown"}">${e}${t?` (${t})`:""}</span>`}).join(""):'<span class="text-gray-500">No port data</span>'}\n </div>\n </div>\n\n \x3c!-- DNS Queries --\x3e\n ${l.length>0?`\n <div class="p-4 bg-slate-800 rounded-lg">\n <h4 class="text-sm font-semibold text-gray-300 mb-3">DNS Queries (${l.length})</h4>\n <div class="space-y-1 max-h-32 overflow-y-auto text-xs font-mono">\n ${l.map(e=>`\n <div class="p-2 bg-slate-700 rounded text-yellow-300 break-all">${escapeHtml(e)}</div>\n `).join("")}\n </div>\n </div>\n `:""}\n\n ${renderHostBeaconSection(c,s.ip)}\n ${renderHostJA3Section(d,s.ip)}\n ${renderHostIRCSection(u,s.ip)}\n ${renderHostAlertSection(p,s.ip)}\n\n \x3c!-- Timestamps --\x3e\n <div class="p-4 bg-slate-800 rounded-lg">\n <h4 class="text-sm font-semibold text-gray-300 mb-3">Timeline</h4>\n <div class="grid grid-cols-2 gap-4 text-sm">\n <div>\n <div class="text-gray-500">First Seen</div>\n <div class="text-white">${g}</div>\n </div>\n <div>\n <div class="text-gray-500">Last Seen</div>\n <div class="text-white">${m}</div>\n </div>\n </div>\n </div>\n `}catch(e){console.error("Error loading host details:",e),a.innerHTML='<p class="text-red-400 text-center py-8">Error loading host details</p>'}}}function closeTrafficHostModal(){const e=document.getElementById("traffic-host-modal");e&&(e.classList.add("hidden"),e.classList.remove("flex"))}async function fetchHostInvestigationData(e){const t=async e=>{try{const t=await fetch(e);return t.ok?await t.json():null}catch{return null}},[n,a,s,o]=await Promise.all([t("/api/traffic/beacons?limit=200"),t("/api/traffic/ja3?limit=200"),t("/api/traffic/irc?limit=200"),t("/api/traffic/alerts?limit=200")]);return[(n&&n.success&&n.beacons||[]).filter(t=>t.src_ip===e||t.dst_ip===e),(a&&a.success&&a.fingerprints||[]).filter(t=>t.src_ip===e),(s&&s.success&&s.sessions||[]).filter(t=>t.client_ip===e||t.server_ip===e),(o&&o.success&&o.alerts||[]).filter(t=>t.src_ip===e||t.dst_ip===e)]}function renderHostBeaconSection(e,t){if(!e||0===e.length)return"";const n=e.slice(0,10).map(e=>{const n=Math.round(100*(e.score||0)),a=n>=85?"text-red-400":n>=70?"text-orange-400":"text-yellow-400",s=e.src_ip===t?e.dst_ip:e.src_ip;return`\n <div class="p-2 bg-slate-700 bg-opacity-50 rounded text-xs">\n <div class="flex items-center justify-between mb-1">\n <span class="font-mono">→ ${escapeHtml(s)}:${e.dst_port}</span>\n <span class="${a} font-bold">score ${n}/100</span>\n </div>\n <div class="grid grid-cols-2 sm:grid-cols-4 gap-2 text-gray-400">\n <div>Interval <span class="text-gray-200">~${e.mean_interval_s}s</span></div>\n <div>Jitter (CV) <span class="text-gray-200">${e.interval_cv}</span></div>\n <div>Avg size <span class="text-gray-200">${e.mean_size_bytes} B</span></div>\n <div>Samples <span class="text-gray-200">${e.samples}</span></div>\n </div>\n </div>`}).join("");return`\n <div class="p-4 bg-red-900 bg-opacity-20 border border-red-700 rounded-lg">\n <h4 class="text-sm font-semibold text-red-300 mb-3 flex items-center gap-2">\n <span>🛰</span> C2 Beacon Candidates (${e.length})\n <span class="text-xs font-normal text-gray-400">MITRE T1071 / T1573</span>\n </h4>\n <div class="space-y-2">${n}</div>\n </div>`}function renderHostJA3Section(e,t){if(!e||0===e.length)return"";const n=e.slice(0,10).map(e=>{const t=e.match,n=t?`<span class="px-2 py-0.5 rounded ${"malware"===t.category?"bg-red-900 text-red-300":"iot"===t.category?"bg-yellow-900 text-yellow-300":"bg-cyan-900 text-cyan-300"}">${escapeHtml(t.label)} (${escapeHtml(t.confidence)})</span>`:'<span class="text-gray-500">unclassified</span>',a=e.sni?`<span class="text-cyan-300">${escapeHtml(e.sni)}</span>`:'<span class="text-gray-500">no SNI</span>';return`\n <div class="p-2 bg-slate-700 bg-opacity-50 rounded text-xs">\n <div class="flex items-center justify-between mb-1">\n <span class="font-mono text-gray-300">${escapeHtml(e.ja3.slice(0,16))}…</span>\n ${n}\n </div>\n <div class="text-gray-400 flex flex-wrap gap-x-3">\n <span>SNI: ${a}</span>\n <span>Hits: <span class="text-gray-200">${e.count}</span></span>\n ${t&&t.source?`<span>Source: <span class="text-gray-200">${escapeHtml(t.source)}</span></span>`:""}\n </div>\n </div>`}).join("");return`\n <div class="p-4 bg-slate-800 rounded-lg">\n <h4 class="text-sm font-semibold text-gray-300 mb-3 flex items-center gap-2">\n <span>🔐</span> TLS Fingerprints (JA3) — ${e.length}\n </h4>\n <div class="space-y-2">${n}</div>\n </div>`}function renderHostIRCSection(e,t){if(!e||0===e.length)return"";const n=e.slice(0,5).map(e=>{const t=e.channels&&e.channels.length?e.channels.map(e=>`<span class="inline-block px-1.5 py-0.5 mr-1 rounded bg-slate-700 text-cyan-300">${escapeHtml(e)}</span>`).join(""):'<span class="text-gray-500">no channels</span>',n=e.server_banner&&e.server_banner.length?`<div class="mt-1 text-gray-400 italic">"${escapeHtml(e.server_banner[0]).slice(0,200)}"</div>`:"",a=(e.recent_messages||[]).slice(-5).map(e=>`\n <div class="font-mono text-xs ${"c2s"===e.direction?"text-cyan-300":"text-green-300"}">\n ${"c2s"===e.direction?"→":"←"} ${escapeHtml(e.command)} ${escapeHtml((e.params||[]).join(" "))}\n </div>`).join("");return`\n <div class="p-2 bg-slate-700 bg-opacity-50 rounded text-xs">\n <div class="flex items-center justify-between mb-1">\n <span class="font-mono">${escapeHtml(e.client_ip)} → ${escapeHtml(e.server_ip)}:${e.server_port}</span>\n <span class="text-gray-400">nick <span class="text-yellow-300">${escapeHtml(e.nick||"?")}</span></span>\n </div>\n <div class="text-gray-400 mb-1">Channels: ${t}</div>\n ${n}\n ${a?`<div class="mt-2 p-2 bg-slate-900 rounded space-y-0.5">${a}</div>`:""}\n </div>`}).join("");return`\n <div class="p-4 bg-orange-900 bg-opacity-20 border border-orange-700 rounded-lg">\n <h4 class="text-sm font-semibold text-orange-300 mb-3 flex items-center gap-2">\n <span>💬</span> IRC Sessions (DPI) — ${e.length}\n <span class="text-xs font-normal text-gray-400">MITRE T1071.001</span>\n </h4>\n <div class="space-y-2">${n}</div>\n </div>`}function renderHostAlertSection(e,t){if(!e||0===e.length)return"";const n=e.slice(-10).reverse().map(e=>{const t={critical:"border-red-600",high:"border-orange-600",medium:"border-yellow-600",low:"border-blue-600",info:"border-slate-600"}[e.level]||"border-slate-600",n={critical:"bg-red-900 text-red-300",high:"bg-orange-900 text-orange-300",medium:"bg-yellow-900 text-yellow-300",low:"bg-blue-900 text-blue-300",info:"bg-slate-700 text-gray-300"}[e.level]||"bg-slate-700 text-gray-300",a=e.timestamp?new Date(e.timestamp).toLocaleTimeString():"";return`\n <div class="p-2 bg-slate-700 bg-opacity-50 rounded border-l-4 ${t} text-xs">\n <div class="flex items-center justify-between mb-1">\n <span class="px-1.5 py-0.5 rounded text-[10px] font-semibold uppercase ${n}">${escapeHtml(e.level)}</span>\n <span class="text-gray-500">${a}</span>\n </div>\n <div class="text-gray-200">${escapeHtml(e.message||"")}</div>\n ${e.details?formatAlertDetails(e.details):""}\n </div>`}).join("");return`\n <div class="p-4 bg-slate-800 rounded-lg">\n <h4 class="text-sm font-semibold text-gray-300 mb-3 flex items-center gap-2">\n <span>⚠</span> Alerts involving this host (${e.length})\n </h4>\n <div class="space-y-2">${n}</div>\n </div>`}function showTrafficConnectionDetail(e){const t=document.getElementById("traffic-connection-modal"),n=document.getElementById("traffic-connection-modal-content");if(!t||!n)return;let a;try{a="string"==typeof e?JSON.parse(e):e}catch(e){return void console.error("Error parsing connection data:",e)}const s=a.duration_seconds?formatDuration(a.duration_seconds):"N/A",o=a.first_seen?new Date(a.first_seen).toLocaleString():"N/A",r=a.last_seen?new Date(a.last_seen).toLocaleString():"N/A";n.innerHTML=`\n \x3c!-- Connection Header --\x3e\n <div class="p-4 bg-slate-800 rounded-lg">\n <div class="flex items-center justify-center gap-3 text-lg font-mono mb-4">\n <span class="text-cyan-400">${escapeHtml(a.src_ip)}:${a.src_port}</span>\n <span class="text-gray-500">→</span>\n <span class="text-green-400">${escapeHtml(a.dst_ip)}:${a.dst_port}</span>\n </div>\n <div class="flex justify-center">\n <span class="px-3 py-1 bg-blue-900 text-blue-300 rounded-full text-sm font-semibold">\n ${escapeHtml(a.protocol?.toUpperCase()||"UNKNOWN")}\n </span>\n </div>\n </div>\n\n \x3c!-- Statistics --\x3e\n <div class="p-4 bg-slate-800 rounded-lg">\n <h4 class="text-sm font-semibold text-gray-300 mb-3">Statistics</h4>\n <div class="grid grid-cols-2 sm:grid-cols-4 gap-3 text-center text-sm">\n <div class="bg-slate-700 rounded p-2">\n <div class="text-cyan-400 font-bold">${formatBytes(a.bytes_sent||0)}</div>\n <div class="text-xs text-gray-500">Bytes Sent</div>\n </div>\n <div class="bg-slate-700 rounded p-2">\n <div class="text-green-400 font-bold">${formatBytes(a.bytes_recv||0)}</div>\n <div class="text-xs text-gray-500">Bytes Received</div>\n </div>\n <div class="bg-slate-700 rounded p-2">\n <div class="text-blue-400 font-bold">${a.packets_sent||0}</div>\n <div class="text-xs text-gray-500">Packets Sent</div>\n </div>\n <div class="bg-slate-700 rounded p-2">\n <div class="text-purple-400 font-bold">${a.packets_recv||0}</div>\n <div class="text-xs text-gray-500">Packets Received</div>\n </div>\n </div>\n </div>\n\n \x3c!-- Duration --\x3e\n <div class="p-4 bg-slate-800 rounded-lg">\n <h4 class="text-sm font-semibold text-gray-300 mb-3">Duration</h4>\n <div class="text-center">\n <div class="text-2xl font-bold text-yellow-400">${s}</div>\n <div class="text-xs text-gray-500 mt-1">Connection duration</div>\n </div>\n </div>\n\n \x3c!-- Flags --\x3e\n ${a.flags&&a.flags.length>0?`\n <div class="p-4 bg-slate-800 rounded-lg">\n <h4 class="text-sm font-semibold text-gray-300 mb-3">TCP Flags</h4>\n <div class="flex flex-wrap gap-2">\n ${a.flags.map(e=>`\n <span class="px-2 py-1 bg-orange-900 text-orange-300 rounded text-xs font-mono">${escapeHtml(e)}</span>\n `).join("")}\n </div>\n </div>\n `:""}\n\n \x3c!-- Timeline --\x3e\n <div class="p-4 bg-slate-800 rounded-lg">\n <h4 class="text-sm font-semibold text-gray-300 mb-3">Timeline</h4>\n <div class="grid grid-cols-2 gap-4 text-sm">\n <div>\n <div class="text-gray-500">First Seen</div>\n <div class="text-white">${o}</div>\n </div>\n <div>\n <div class="text-gray-500">Last Seen</div>\n <div class="text-white">${r}</div>\n </div>\n </div>\n </div>\n\n \x3c!-- Quick Actions --\x3e\n <div class="flex gap-2">\n <button onclick="showTrafficHostDetail('${escapeHtml(a.src_ip)}')"\n class="flex-1 px-3 py-2 bg-cyan-600 hover:bg-cyan-700 text-white rounded-lg text-sm transition-colors">\n View Source Host\n </button>\n <button onclick="showTrafficHostDetail('${escapeHtml(a.dst_ip)}')"\n class="flex-1 px-3 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg text-sm transition-colors">\n View Dest Host\n </button>\n </div>\n `,t.classList.remove("hidden"),t.classList.add("flex")}function closeTrafficConnectionModal(){const e=document.getElementById("traffic-connection-modal");e&&(e.classList.add("hidden"),e.classList.remove("flex"))}async function showTrafficPortDetail(e,t){const n=document.getElementById("traffic-port-modal"),a=document.getElementById("traffic-port-modal-port"),s=document.getElementById("traffic-port-modal-service"),o=document.getElementById("traffic-port-modal-content");if(!n||!o)return;const r=parseInt(e);a.textContent=e;const i=SUSPICIOUS_PORT_INFO[r],l=i?i.name:{20:"FTP Data",21:"FTP",22:"SSH",23:"Telnet",25:"SMTP",53:"DNS",67:"DHCP Server",68:"DHCP Client",69:"TFTP",80:"HTTP",110:"POP3",119:"NNTP",123:"NTP",135:"MS-RPC",137:"NetBIOS-NS",138:"NetBIOS-DGM",139:"NetBIOS-SSN",143:"IMAP",161:"SNMP",162:"SNMP Trap",389:"LDAP",443:"HTTPS",445:"SMB",465:"SMTPS",514:"Syslog",587:"SMTP Submission",636:"LDAPS",993:"IMAPS",995:"POP3S",1080:"SOCKS",1433:"MSSQL",1521:"Oracle",3306:"MySQL",3389:"RDP",5432:"PostgreSQL",5900:"VNC",6379:"Redis",8080:"HTTP Proxy",8443:"HTTPS Alt",27017:"MongoDB"}[r]||"Unknown Service";s.textContent=l,o.innerHTML='<div class="text-center py-8"><div class="animate-spin w-8 h-8 border-2 border-cyan-400 border-t-transparent rounded-full mx-auto"></div><p class="text-gray-400 mt-2">Loading port details...</p></div>',n.classList.remove("hidden"),n.classList.add("flex");try{const e=await fetch("/api/traffic/hosts?limit=100&sort=bytes"),t=await e.json();if(!t.success||!t.hosts)return void(o.innerHTML='<p class="text-red-400 text-center py-8">Failed to load port details</p>');const n=t.hosts.filter(e=>e.ports_contacted&&e.ports_contacted.includes(r));let a="";if(i){a+=`\n <div class="p-3 ${{critical:"bg-red-900 border-red-500 text-red-300",high:"bg-orange-900 border-orange-500 text-orange-300",medium:"bg-yellow-900 border-yellow-500 text-yellow-300",low:"bg-blue-900 border-blue-500 text-blue-300"}[i.severity]} border-l-4 rounded">\n <div class="flex items-center space-x-2 mb-1">\n <span class="text-lg">⚠️</span>\n <span class="font-semibold uppercase text-xs">${i.severity} Severity</span>\n <span class="text-xs px-2 py-0.5 bg-black bg-opacity-30 rounded">${escapeHtml(i.category)}</span>\n </div>\n <p class="text-sm">${escapeHtml(i.reason)}</p>\n </div>\n `}a+=`\n <div class="grid grid-cols-2 gap-3">\n <div class="bg-slate-800 bg-opacity-50 p-3 rounded">\n <div class="text-xs text-gray-400 uppercase">Total Hosts</div>\n <div class="text-2xl font-bold text-cyan-400">${n.length}</div>\n </div>\n <div class="bg-slate-800 bg-opacity-50 p-3 rounded">\n <div class="text-xs text-gray-400 uppercase">Service</div>\n <div class="text-lg font-semibold text-white">${escapeHtml(l)}</div>\n </div>\n </div>\n `,n.length>0?a+=`\n <div>\n <h4 class="text-sm font-semibold text-gray-300 mb-2">Hosts Using This Port</h4>\n <div class="space-y-1 max-h-60 overflow-y-auto">\n ${n.map(e=>{const t=e.total_bytes||0;return`\n <div class="flex items-center justify-between p-2 bg-slate-700 bg-opacity-30 rounded hover:bg-slate-600 hover:bg-opacity-50 transition-colors cursor-pointer" onclick="closeTrafficPortModal(); showTrafficHostDetail('${escapeHtml(e.ip)}')">\n <div class="flex items-center space-x-2">\n <span class="font-mono text-cyan-400">${escapeHtml(e.ip)}</span>\n ${e.hostname?`<span class="text-gray-500 text-xs">(${escapeHtml(e.hostname)})</span>`:""}\n </div>\n <div class="text-xs text-gray-400">\n ${formatBytes(t)}\n </div>\n </div>\n `}).join("")}\n </div>\n </div>\n `:a+='<p class="text-gray-400 text-center py-4">No active hosts on this port</p>',o.innerHTML=a}catch(e){console.error("Error loading port details:",e),o.innerHTML='<p class="text-red-400 text-center py-8">Error loading port details</p>'}}function closeTrafficPortModal(){const e=document.getElementById("traffic-port-modal");e&&(e.classList.add("hidden"),e.classList.remove("flex"))}let _wardrivingInterval=null;async function loadWardrivingData(){try{const[e,t]=await Promise.all([fetch("/api/wardriving/status"),fetch("/api/wardriving/sessions")]),n=await e.json(),a=await t.json();updateWardrivingUI(n),renderWardrivingSessions(a.sessions||[]),loadWardrivingOnBootState(),loadHuginnConfig(),n.running?(_wardrivingInterval||(_wardrivingInterval=setInterval(refreshWardrivingStatus,3e3)),await loadWardrivingTableByType()):(_wardrivingInterval&&(clearInterval(_wardrivingInterval),_wardrivingInterval=null),await loadWardrivingTableByType())}catch(e){console.error("[Wardriving] Load error:",e)}}async function refreshWardrivingStatus(){try{const e=await fetch("/api/wardriving/status"),t=await e.json();updateWardrivingUI(t),t.running?loadWardrivingTableByType():_wardrivingInterval&&(clearInterval(_wardrivingInterval),_wardrivingInterval=null)}catch(e){console.error("[Wardriving] Refresh error:",e)}}function updateWardrivingUI(e){const t=document.getElementById("wd-status-badge");_wardrivingRunning=!!e.running,updateWardrivingToggleButton(),applyWardrivingBackfillVisibility(!!e.allow_backfill),e.running?t&&(t.textContent="Running",t.className="px-2 py-1 bg-emerald-600 bg-opacity-30 text-emerald-400 text-xs rounded-full animate-pulse"):t&&(t.textContent="Stopped",t.className="px-2 py-1 bg-gray-600 bg-opacity-30 text-gray-400 text-xs rounded-full");const n=e.gps||{},a=document.getElementById("wd-gps-status"),s=document.getElementById("wd-gps-coords"),o=document.getElementById("wd-gps-sats"),r=n.satellites_in_view||0,i=n.satellites||0;if(a&&(n.has_fix?(a.textContent="GPS-Fix OK",a.className="text-sm font-bold text-emerald-400"):n.connected&&e.running?(a.textContent=r>0?`Searching (${r} visible)`:"Searching...",a.className="text-sm font-bold text-yellow-400"):n.connected?(a.textContent="Connected",a.className="text-sm font-bold text-cyan-400"):(a.textContent="No GPS",a.className="text-sm font-bold text-red-400")),s&&(s.textContent=n.latitude&&n.longitude?`${n.latitude.toFixed(5)}, ${n.longitude.toFixed(5)}`:"-"),o){const e=[`Sats: ${i}/${r}`];null!=n.snr_max&&e.push(`SNR ${n.snr_max} dB`),null!=n.hdop&&n.hdop<50&&e.push(`HDOP ${n.hdop.toFixed(1)}`),o.textContent=e.join(" · ")}const l=document.getElementById("wd-speed-val"),c=document.getElementById("wd-heading-val");if(l&&(l.textContent=null!=n.speed_kmh&&n.has_fix?`${n.speed_kmh.toFixed(1)} km/h`:"—"),c)if(null!=n.course&&n.has_fix){const e=["N","NE","E","SE","S","SW","W","NW"],t=Math.round(n.course/45)%8;c.textContent=`${e[t]} (${Math.round(n.course)}°)`}else c.textContent="—";const d=document.getElementById("wd-device-name");d&&!d.matches(":focus")&&e.device_name&&(d.value=e.device_name);const u=e.stats||{};updateElement("wd-total-networks",String(u.total_networks||0)),updateElement("wd-networks-per-scan",`${e.networks_this_scan||0} per scan`),updateElement("wd-open-count",String(u.open_networks||0)),updateElement("wd-wep-count",String(u.wep_networks||0)),updateElement("wd-wpa-count",String(u.wpa_networks||0)),updateElement("wd-band24",String(u.band_2_4ghz||0)),updateElement("wd-band5",String(u.band_5ghz||0)),updateElement("wd-band6",String(u.band_6ghz||0)),updateElement("wd-scans-done",`Scans: ${e.scans_completed||0}`),updateElement("wd-bt-count",String(u.bluetooth_devices||e.bluetooth_count||0)),updateElement("wd-cell-count",String(u.cell_towers||e.cell_count||0)),updateElement("wd-camera-count",String(u.cameras||0));const p=document.getElementById("wd-interfaces-info");e.interfaces&&e.interfaces.length>0&&p&&(p.textContent=`Interfaces: ${e.interfaces.join(", ")}`),_renderCompanionBars(e),_renderWifiAdaptersBar(e),updateSerialStatus(e)}const _ESP_MODE_LABELS={wifi:"WiFi","ble-flipper":"🐬 Flipper","ble-airtag":"🏷️ AirTag","ble-skimmer":"💳 Skimmer",pineap:"🍍 PineAP",ble:"BLE",stations:"Stations",wardrive:"🏎️ Wardrive (fast)"};function _renderCompanionBars(e){const t=document.getElementById("wd-esp-companions");if(!t)return;const n=Array.isArray(e.companions)?e.companions:[];if(0===n.length)return void(t.innerHTML='<div class="bg-slate-800/40 border border-slate-700 rounded-lg px-4 py-2 flex items-center gap-3 flex-wrap">\n <span class="text-xs font-bold text-purple-400">Companion</span>\n <span class="w-2 h-2 rounded-full bg-gray-500 animate-pulse"></span>\n <span class="text-xs text-gray-400">Ragnar looking for Huginn or Piglet...</span>\n </div>');const a=1===n.filter(e=>e.connected).length;t.innerHTML=n.map(t=>_companionBarHtml(t,a,e)).join("")}function _companionBarHtml(e,t,n){const a=e.name||"Companion",s="Piglet"===a||"Piglet Coordinator"===a,o="Piglet Coordinator"===a,r=e.connected?"bg-green-500":"bg-gray-500",i=[],l='<span class="text-xs text-gray-500">|</span>';if(e.connected&&!s){const t=_ESP_MODE_LABELS[e.esp_mode]||e.esp_mode||"";i.push(`${l}\n <span class="text-xs text-gray-400">Mode:</span>\n <span class="text-xs font-bold text-amber-400">${escapeHtml(t)}</span>`)}if(e.connected&&o){const a=(Array.isArray(e.coordinator_nodes)?e.coordinator_nodes:[]).reduce((e,t)=>e+(t.records_rx||0),0);i.push(`${l}\n <span class="text-xs text-gray-400">Records:</span>\n <span class="text-xs font-bold text-emerald-400">${a}</span>`),t&&i.push(`${l}\n <span class="text-xs text-gray-400">WiFi:</span>\n <span class="text-xs font-bold text-emerald-400">${n.serial_seen_unique||0}</span>`)}else e.connected&&i.push(`${l}\n <span class="text-xs text-gray-400">WiFi:</span>\n <span class="text-xs font-bold text-emerald-400">${e.networks||0}</span>`),(e.networks_5||0)>0&&i.push(`<span class="text-xs text-cyan-400">${e.networks_24||0}</span>\n <span class="text-xs text-gray-500">2.4G</span>\n <span class="text-xs text-purple-400">${e.networks_5||0}</span>\n <span class="text-xs text-gray-500">5G</span>`);e.connected&&!s&&i.push(`<span class="text-xs text-gray-400">BLE:</span>\n <span class="text-xs font-bold text-blue-400">${e.esp_ble_count||0}</span>`),e.connected&&t&&(!s||o)&&i.push(`<span class="text-xs text-gray-400">Unique:</span>\n <span class="text-xs font-bold text-cyan-400">${n.serial_unique||0}</span>`),e.connected&&e.mesh_node_count>0&&i.push(`${l}\n <span class="text-xs text-gray-400">Mesh:</span>\n <span class="text-xs font-bold text-orange-400">${e.mesh_node_count}</span>\n <span class="text-xs text-gray-500">nodes</span>`);let c="";if(e.connected&&Array.isArray(e.esp_alerts)&&e.esp_alerts.length>0){const t=e.esp_alerts[e.esp_alerts.length-1];c=`<span class="text-xs font-bold text-red-400 ml-2">⚠️ ${escapeHtml(t.alert||"")}</span>`}const d=e.connected?"":'<span class="text-xs text-gray-400">Ragnar looking for Huginn or Piglet...</span>';return`<div class="bg-slate-800/40 border border-slate-700 rounded-lg px-4 py-2 flex items-center gap-3 flex-wrap">\n <span class="text-xs font-bold text-purple-400">${escapeHtml(a)}</span>\n ${d}\n <span class="w-2 h-2 rounded-full ${r} animate-pulse"></span>\n ${i.join("\n")}\n ${c}\n </div>${_coordNodesHtml(e)}`}function _coordNodesHtml(e){const t=Array.isArray(e.coordinator_nodes)?e.coordinator_nodes:[];if(0===t.length)return"";const n=t.length,a=t.reduce((e,t)=>e+(t.records_rx||0),0),s=[];e.coordinator_board&&s.push(e.coordinator_board),e.coordinator_fw&&s.push(e.coordinator_fw);const o=t.map(e=>{const t=(e.mac||"").toUpperCase(),n=(e.records_rx||0).toLocaleString(),a=Number(e.age_s||0);let s="text-emerald-400";a>60?s="text-gray-500":a>30&&(s="text-orange-400");return`<span class="inline-flex items-center gap-2 bg-slate-900/60 border border-slate-700 rounded px-2 py-1">\n <span class="text-xs font-bold text-purple-300">node ${void 0!==e.idx?`#${e.idx}`:""}</span>\n <span class="text-xs text-gray-400 font-mono">${escapeHtml(t)}</span>\n <span class="text-xs text-gray-500">·</span>\n <span class="text-xs font-bold text-cyan-400">${n}</span>\n <span class="text-xs text-gray-500">rx</span>\n <span class="text-xs text-gray-500">·</span>\n <span class="text-xs ${s}">${a}s</span>\n </span>`}).join("");return`<div class="bg-slate-800/30 border border-slate-700 rounded-lg px-4 py-2">\n <div class="flex items-center gap-2 mb-2">\n <span class="text-xs font-bold text-orange-400">🌐 Mesh nodes</span>\n <span class="text-xs text-gray-400">${n} connected — ${a.toLocaleString()} records total</span>\n <span class="text-xs text-gray-500 ml-auto">${escapeHtml(s.join(" · "))}</span>\n </div>\n <div class="flex flex-wrap gap-2">${o}</div>\n </div>`}function _renderWifiAdaptersBar(e){const t=document.getElementById("wd-adapters-bar");if(!t)return;const n=e.interface_details;if(!n||0===n.length)return void t.classList.add("hidden");t.classList.remove("hidden");const a={"2.4GHz":"text-emerald-400","5GHz":"text-purple-400","6GHz":"text-pink-400"},s=e.coverage||{},o=s.per_interface||{},r=e.band_mode||"redundant",i=e=>null==e||0===e?"text-gray-500":e>=-55?"text-emerald-400":e>=-70?"text-yellow-400":"text-orange-400",l=n.map(e=>{const t=e.is_usb?"🔌":"📡",n=e.manufacturer||e.product||e.driver||e.name,s=new Set((e.sweep_bands||[]).map(e=>e+"GHz")),l=(e.bands||[]).map(e=>{const t="split"!==r||s.has(e);return`<span class="px-1.5 py-0.5 rounded text-[10px] font-bold ${t?"bg-slate-700":"bg-slate-800/60"} ${t?a[e]||"text-gray-400":"text-gray-600"}">${e}</span>`}).join(" "),c=e.networks||0,d=o[e.name];let u="";if(d){const e=d.best_rssi_median,t=d.best_rssi_avg;u=`\n <div class="w-full flex items-center gap-3 flex-wrap pl-7 pt-1 text-[11px]">\n <span class="text-gray-400">Only here:</span>\n <span class="font-bold ${d.only_here>0?"text-cyan-300":"text-gray-500"}">${d.only_here}</span>\n <span class="text-gray-600">·</span>\n <span class="text-gray-400">Median RSSI:</span>\n <span class="font-bold ${i(e)}">${null!=e?e+" dBm":"—"}</span>\n <span class="text-gray-600">·</span>\n <span class="text-gray-400">Avg:</span>\n <span class="font-bold ${i(t)}">${null!=t?t+" dBm":"—"}</span>\n </div>`}let p="";if(0===c&&(e.scan_error||e.current_type&&"managed"!==e.current_type)){const t=[];e.current_type&&"managed"!==e.current_type&&t.push(`mode=<span class="text-orange-400 font-bold">${escapeHtml(e.current_type)}</span>`),e.scan_error&&t.push(`<span class="text-red-400">${escapeHtml(e.scan_error)}</span>`),p=`\n <div class="w-full flex items-center gap-2 flex-wrap pl-7 pt-1 text-[11px]">\n <span class="text-gray-400">⚠ Scan failing:</span>\n ${t.join(' <span class="text-gray-600">·</span> ')}\n </div>`}return`<div class="bg-slate-800/40 border border-slate-700 rounded-lg px-4 py-2 flex items-center gap-3 flex-wrap">\n <span class="text-sm">${t}</span>\n <span class="text-xs font-bold text-cyan-400">${escapeHtml(e.name)}</span>\n <span class="text-xs text-gray-400">${escapeHtml(n)}</span>\n <span class="flex gap-1">${l}</span>\n <span class="text-xs text-gray-500">|</span>\n <span class="text-xs text-gray-400">Networks:</span>\n <span class="text-xs font-bold text-emerald-400">${c}</span>\n ${u}\n ${p}\n </div>`}).join("");let c="";if(n.length>=2&&null!=s.overlap){const e=n.map(e=>e.name),t=e.filter(e=>o[e]&&o[e].best_rssi_median).sort((e,t)=>o[t].best_rssi_median-o[e].best_rssi_median)[0],a=e.filter(e=>o[e]).sort((e,t)=>o[t].unique-o[e].unique)[0],r=(e,t)=>t?`<span class="text-gray-400">${e}:</span> <span class="font-bold text-cyan-300">${escapeHtml(t)}</span>`:"";c=`<div class="bg-slate-800/20 border border-slate-700/50 rounded-lg px-4 py-1.5 flex items-center gap-4 flex-wrap text-[11px] mt-1">\n <span class="text-gray-400">Shared (both saw):</span>\n <span class="font-bold text-emerald-400">${s.overlap}</span>\n <span class="text-gray-600">·</span>\n ${r("Best signal",t)}\n <span class="text-gray-600">·</span>\n ${r("Widest coverage",a)}\n </div>`}let d="";if(n.length>=2){const e="split"===r?"redundant":"split";d=`<div class="text-[11px] mb-1 flex items-center gap-2">\n <span>Scan mode: ${"split"===r?'<span class="text-emerald-400 font-bold">Split bands</span> <span class="text-gray-500">(each adapter on its own band — faster cycles, no redundant sweeps)</span>':'<span class="text-yellow-400 font-bold">Redundant</span> <span class="text-gray-500">(every adapter sweeps every band — more reliable, slower)</span>'}</span>\n <button onclick="toggleWardrivingBandMode('${e}')" class="px-2 py-0.5 rounded bg-slate-700 hover:bg-slate-600 text-cyan-300 text-[10px] font-bold border border-slate-600">switch to ${e}</button>\n </div>`}t.innerHTML=d+l+c}async function toggleWardrivingBandMode(e){if(confirm(`Switch wardriving to "${e}" mode?\n\nThis stops and restarts the current session — your data is preserved (same DB), but the running session ID will change.`))try{if(!(await fetch("/api/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({wardriving_band_mode:e})})).ok)throw new Error("config write failed");await fetch("/api/wardriving/stop",{method:"POST"});const t=await fetch("/api/wardriving/start",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({})}),n=await t.json();if(n.error)return void alert("Start failed: "+n.error);"function"==typeof addConsoleMessage&&addConsoleMessage(`Wardriving band mode → ${e}`,"success")}catch(e){alert("Band-mode switch failed: "+e.message)}}async function saveWardrivingDeviceName(e){try{await fetch("/api/wardriving/device_name",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e})})}catch(e){console.error("[Wardriving] Save device name error:",e)}}async function wipeWardrivingData(){if(confirm("Are you sure you want to delete ALL wardriving data?\n\nThis will permanently remove all session databases and cannot be undone.")&&confirm("This is your last chance. Really wipe all wardriving data?"))try{const e=await fetch("/api/wardriving/wipe",{method:"POST"}),t=await e.json();t.error?alert("Error: "+t.error):(alert("Wardriving data wiped. "+(t.deleted||0)+" files deleted."),"function"==typeof loadWardrivingSessions&&loadWardrivingSessions())}catch(e){alert("Wipe failed: "+e.message)}}function applyWardrivingEnabledState(e){document.querySelectorAll(".wardriving-feature").forEach(t=>{t.classList.toggle("hidden",!e)});const t=document.getElementById("wardriving-config-body");t&&t.classList.toggle("hidden",!e);const n=document.getElementById("wardriving-enable-toggle");n&&(n.checked=!!e)}async function toggleWardrivingEnabled(e){const t=!!e.checked;applyWardrivingEnabledState(t);try{await postAPI("/api/config",{wardriving_enabled:t}),addConsoleMessage("Wardriving "+(t?"enabled":"disabled"),"success")}catch(e){console.error("[Wardriving] enable toggle error:",e),addConsoleMessage("Failed to update wardriving setting","error"),applyWardrivingEnabledState(!t)}}async function toggleWardrivingOnBoot(){const e=document.getElementById("wardriving-on-boot");if(e)try{const t=await fetch("/api/wardriving/on_boot",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled:e.checked})});(await t.json()).success||(e.checked=!e.checked)}catch(t){console.error("[Wardriving] on_boot toggle error:",t),e.checked=!e.checked}}async function loadWardrivingOnBootState(){try{const e=await fetch("/api/wardriving/on_boot"),t=await e.json(),n=document.getElementById("wardriving-on-boot");n&&(n.checked=!!t.wardriving_on_boot)}catch(e){}}function applyWardrivingBackfillVisibility(e){const t=document.getElementById("wd-map-backfill-btn");t&&t.classList.toggle("hidden",!e)}async function toggleWardrivingBackfill(){const e=document.getElementById("wardriving-allow-backfill");if(!e)return;const t=!!e.checked;try{await postAPI("/api/config",{wardriving_allow_backfill:t}),applyWardrivingBackfillVisibility(t),addConsoleMessage("GPS backfill "+(t?"enabled — backfilled data is excluded from WiGLE export":"disabled"),t?"warning":"info")}catch(n){console.error("[Wardriving] backfill toggle error:",n),addConsoleMessage("Failed to update GPS backfill setting","error"),e.checked=!t}}async function loadWardrivingBackfillState(){try{const e=await fetch("/api/config"),t=!!(await e.json()).wardriving_allow_backfill,n=document.getElementById("wardriving-allow-backfill");n&&(n.checked=t),applyWardrivingBackfillVisibility(t)}catch(e){}}let _kioskSettingsDebounce=null,_kioskPollTimer=null;function _setKioskBadge(e){const t=document.getElementById("kiosk-service-badge");if(!t)return;const n={active:"bg-green-900 text-green-300",inactive:"bg-gray-700 text-gray-300",activating:"bg-amber-900 text-amber-300",deactivating:"bg-amber-900 text-amber-300",failed:"bg-red-900 text-red-300",not_installed:"bg-gray-700 text-gray-400",unknown:"bg-gray-700 text-gray-400"};t.textContent=e||"unknown",t.className="text-xs px-2 py-0.5 rounded "+(n[e]||n.unknown)}function _setKioskMessage(e,t){const n=document.getElementById("kiosk-status-message");if(!n)return;if(!e)return n.classList.add("hidden"),void(n.textContent="");const a="error"===t?"text-red-400":"success"===t?"text-green-400":"text-amber-300";n.className="text-xs mt-2 "+a,n.textContent=e,n.classList.remove("hidden")}async function loadKioskState(){try{const e=await fetchAPI("/api/kiosk/status"),t=document.getElementById("kiosk-enabled"),n=document.getElementById("kiosk-url"),a=document.getElementById("kiosk-rotation"),s=document.getElementById("kiosk-hide-cursor"),o=document.getElementById("kiosk-config");t&&(t.checked=!!e.enabled),n&&e.url&&(n.value=e.url),a&&(a.value=String(e.rotation??0)),s&&(s.checked=!!e.hide_cursor),o&&o.classList.toggle("hidden",!e.enabled),_setKioskBadge(e.service_state||"unknown")}catch(e){_setKioskBadge("unknown")}}async function onKioskEnabledToggled(e){const t=!!e.checked,n=document.getElementById("kiosk-config");n&&n.classList.toggle("hidden",!t),_setKioskMessage(t?"Installing kiosk…":"Removing kiosk…","info"),_setKioskBadge(t?"activating":"deactivating");try{const a=await fetch("/api/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({kiosk_enabled:t})}),s=await a.json();if(!s.success)return e.checked=!t,n&&n.classList.toggle("hidden",t),void _setKioskMessage(s.error||"Failed to update kiosk setting","error");_pollKioskStatusUntilStable(t)}catch(a){e.checked=!t,n&&n.classList.toggle("hidden",t),_setKioskMessage("Network error toggling kiosk: "+a.message,"error")}}function onKioskSettingChanged(){_kioskSettingsDebounce&&clearTimeout(_kioskSettingsDebounce),_kioskSettingsDebounce=setTimeout(async()=>{const e=document.getElementById("kiosk-url"),t=document.getElementById("kiosk-rotation"),n=document.getElementById("kiosk-hide-cursor"),a={kiosk_url:e?e.value:"http://localhost:8000",kiosk_rotation:t&&parseInt(t.value,10)||0,kiosk_hide_cursor:!n||!!n.checked};try{const e=await fetch("/api/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)}),t=await e.json();t.success?(_setKioskMessage("Settings saved — kiosk will pick up new values on next launch","info"),_setKioskBadge("activating"),_pollKioskStatusUntilStable(!0)):_setKioskMessage(t.error||"Failed to save kiosk settings","error")}catch(e){_setKioskMessage("Network error: "+e.message,"error")}},500)}function _pollKioskStatusUntilStable(e,t=12){_kioskPollTimer&&clearTimeout(_kioskPollTimer);let n=0;const a=async()=>{n++;try{const t=await fetchAPI("/api/kiosk/status");_setKioskBadge(t.service_state||"unknown");const n=e?"service"===t.mode?"active"===t.service_state:"autostart"===t.mode&&!0===t.installed:!1===t.installed||"not_installed"===t.mode,a="failed"===t.service_state;if(n){let n;return n=e?"autostart"===t.mode?"active"===t.service_state?"Kiosk is running on the display.":"Kiosk installed — launches on next desktop login.":"Kiosk is running on the display.":"Kiosk removed.",_setKioskMessage(n,"success"),void setTimeout(()=>_setKioskMessage("",null),5e3)}if(a)return void _setKioskMessage("Kiosk service failed. Check `journalctl -u ragnar-kiosk` on the Pi.","error")}catch(e){}n<t?_kioskPollTimer=setTimeout(a,2e3):_setKioskMessage("Kiosk state did not stabilize — check the Pi journal.","error")};_kioskPollTimer=setTimeout(a,1500)}function _fmtCoord(e,t){if(null==e||isNaN(e))return"—";const n=Math.abs(e),a=e>=0?t[0]:t[1];return n.toFixed(5)+"° "+a}async function _refreshKioskWardrivingView(){try{const e=await fetch("/api/wardriving/status").then(e=>e.json()).catch(()=>({})),t=(e,t)=>{const n=document.getElementById(e);n&&(n.textContent=null==t||""===t?"—":String(t))};t("kiosk-total-networks",e.total_networks??0),t("kiosk-scans-completed",e.scans_completed??0),t("kiosk-networks-this-scan",e.networks_this_scan??0),t("kiosk-bt-count",e.bluetooth_count??0),t("kiosk-cell-count",e.cell_count??0),t("kiosk-wd-running",e.running?"yes":"no"),t("kiosk-wd-interfaces",Array.isArray(e.interfaces)?e.interfaces.join(", "):"—"),t("kiosk-wd-band",e.band_mode||"—"),t("kiosk-wd-companion",e.companion_name||"—"),t("kiosk-wd-serial-networks",e.serial_networks??0),t("kiosk-wd-mesh",e.mesh_node_count??0);const n=e.gps||{},a=document.getElementById("kiosk-gps-fix");if(a){let e;e=n.connected?n.has_fix?"fix":"no fix":"offline",a.textContent=e,a.className="kiosk-row-value "+(n.has_fix?"kiosk-gps-fix-ok":"kiosk-gps-fix-none")}t("kiosk-gps-lat",_fmtCoord(n.latitude,["N","S"])),t("kiosk-gps-lon",_fmtCoord(n.longitude,["E","W"])),t("kiosk-gps-sats",n.satellites??"—");const s=n.speed_kmh;t("kiosk-gps-speed","number"==typeof s?s.toFixed(1)+" km/h":"—"),t("kiosk-gps-port",n.port||"—")}catch(e){}}async function importWigleCsv(){const e=document.getElementById("wd-import-file"),t=document.getElementById("wd-import-result");if(!e||!e.files.length)return void(t&&(t.textContent="Select a CSV file first.",t.className="text-xs mt-2 text-yellow-400"));const n=new FormData;n.append("file",e.files[0]);try{t&&(t.textContent="Importing...",t.className="text-xs mt-2 text-blue-400");const e=await fetch("/api/wardriving/import",{method:"POST",body:n}),a=await e.json();a.error?t&&(t.textContent="Error: "+a.error,t.className="text-xs mt-2 text-red-400"):(t&&(t.textContent=`Imported: ${a.imported_wifi||0} WiFi, ${a.imported_bluetooth||0} BT, ${a.imported_cell||0} Cell (${a.skipped||0} skipped)`,t.className="text-xs mt-2 text-green-400"),loadWardrivingData())}catch(e){t&&(t.textContent="Import failed: "+e.message,t.className="text-xs mt-2 text-red-400")}}async function detectSerialPort(){const e=document.getElementById("wd-serial-port"),t=document.getElementById("wd-serial-status");try{t&&(t.textContent="Searching...",t.className="text-xs px-2 py-0.5 rounded-full bg-amber-900 text-amber-400");const n=await fetch("/api/wardriving/serial/detect"),a=await n.json();a.found&&a.port?(e&&(e.value=a.port),t&&(t.textContent="Found: "+a.port,t.className="text-xs px-2 py-0.5 rounded-full bg-green-900 text-green-400")):(t&&(t.textContent="Not found",t.className="text-xs px-2 py-0.5 rounded-full bg-red-900 text-red-400"),setTimeout(()=>{t&&(t.textContent="Disconnected",t.className="text-xs px-2 py-0.5 rounded-full bg-gray-700 text-gray-400")},3e3))}catch(e){console.error("[Wardriving] detect error:",e),t&&(t.textContent="Error",t.className="text-xs px-2 py-0.5 rounded-full bg-red-900 text-red-400")}}async function toggleSerialListener(){const e=document.getElementById("wd-serial-port"),t=document.getElementById("wd-serial-status"),n=document.getElementById("wd-serial-btn"),a=t&&"Connected"===t.textContent;try{if(a){const e=await fetch("/api/wardriving/serial",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:"stop"})});(await e.json()).success&&(t&&(t.textContent="Disconnected",t.className="text-xs px-2 py-0.5 rounded-full bg-gray-700 text-gray-400"),n&&(n.textContent="Connect"))}else{const a=e?e.value.trim():"";if(!a)return void alert("Enter a serial port (e.g. /dev/ttyUSB0 or COM3)");const s=await fetch("/api/wardriving/serial",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:"start",port:a})}),o=await s.json();o.success?(t&&(t.textContent="Connected",t.className="text-xs px-2 py-0.5 rounded-full bg-green-900 text-green-400"),n&&(n.textContent="Disconnect")):alert("Serial error: "+(o.error||"Unknown"))}}catch(e){console.error("[Wardriving] serial toggle error:",e)}}function updateSerialStatus(e){const t=document.getElementById("wd-serial-status"),n=document.getElementById("wd-serial-btn"),a=document.getElementById("wd-serial-count"),s=document.getElementById("wd-serial-port");e.serial_connected?(t&&(t.textContent="Connected",t.className="text-xs px-2 py-0.5 rounded-full bg-green-900 text-green-400"),n&&(n.textContent="Disconnect"),s&&e.serial_port&&(s.value=e.serial_port)):(t&&(t.textContent="Disconnected",t.className="text-xs px-2 py-0.5 rounded-full bg-gray-700 text-gray-400"),n&&(n.textContent="Connect")),a&&(a.textContent=e.serial_networks||"0"),_updateHuginnConfigBadge(e)}function _updateHuginnConfigBadge(e){const t=document.getElementById("wd-huginn-config-state");if(!t)return;(Array.isArray(e&&e.companions)?e.companions:[]).some(e=>e.connected&&"Huginn"===e.name)||e&&"Huginn"===e.companion_name&&e.serial_connected?(t.textContent="Live (Huginn connected)",t.className="text-xs px-2 py-0.5 rounded-full bg-green-900 text-green-400"):(t.textContent="Saved (push on connect)",t.className="text-xs px-2 py-0.5 rounded-full bg-gray-700 text-gray-400")}async function loadHuginnConfig(){try{const e=await fetch("/api/wardriving/huginn_config"),t=await e.json();if(t.error)return;const n=document.getElementById("wd-huginn-scan-ms"),a=document.getElementById("wd-huginn-ble-spam"),s=document.getElementById("wd-huginn-skimmer-names");n&&(n.value=t.wifi_scan_duration_ms??""),a&&(a.value=t.ble_spam_threshold??""),s&&(s.value=t.skimmer_names??""),_updateHuginnConfigBadge({companion_name:t.companion,serial_connected:t.connected})}catch(e){console.error("[Wardriving] huginn config load error:",e)}}async function saveHuginnConfig(){const e=document.getElementById("wd-huginn-scan-ms"),t=document.getElementById("wd-huginn-ble-spam"),n=document.getElementById("wd-huginn-skimmer-names"),a=document.getElementById("wd-huginn-config-msg"),s={};e&&""!==e.value&&(s.wifi_scan_duration_ms=parseInt(e.value,10)),t&&""!==t.value&&(s.ble_spam_threshold=parseInt(t.value,10)),n&&(s.skimmer_names=n.value);try{a&&(a.textContent="Saving…",a.className="text-xs text-gray-400");const e=await fetch("/api/wardriving/huginn_config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)}),t=await e.json();if(!e.ok||t.error)return void(a&&(a.textContent="Error: "+(t.error||e.statusText),a.className="text-xs text-red-400"));a&&(a.textContent=t.live?`Saved · pushed ${t.queued.length} command${1===t.queued.length?"":"s"} to Huginn`:"Saved · will push on next Huginn connect",a.className="text-xs text-emerald-400")}catch(e){console.error("[Wardriving] huginn config save error:",e),a&&(a.textContent="Error: "+e.message,a.className="text-xs text-red-400")}}!function(){try{if("1"!==new URLSearchParams(window.location.search).get("kiosk"))return;const e=()=>document.body&&document.body.classList.add("kiosk-mode");document.body?e():document.addEventListener("DOMContentLoaded",e,{once:!0}),"#wardriving"===window.location.hash&&window.addEventListener("load",()=>{_refreshKioskWardrivingView(),setInterval(_refreshKioskWardrivingView,2e3)},{once:!0})}catch(e){}}();let _wdLastRenderedType=null;function loadWardrivingTableByType(){const e=document.getElementById("wd-table-type")?.value||"wifi";_wdLastRenderedType!==e&&(delete _wdTableSignatures[e],_wdLastRenderedType=e),"wifi"===e?loadWardrivingNetworks():"bluetooth"===e?_loadWardrivingBluetooth():"cell"===e?_loadWardrivingCellTable():"cameras"===e&&_loadWardrivingCameras()}const _wdTableSignatures={};function _wdSig(e,t,n){let a=1000003*t.length;for(let e=0;e<t.length;e++)a=a+n(t[e])|0;const s=t.length?n(t[t.length-1]):0;return`${e}:${t.length}:${a}:${s}`}function _wdShouldRender(e,t){return _wdTableSignatures[e]!==t&&(_wdTableSignatures[e]=t,!0)}function _wdInvalidateTableCache(){for(const e in _wdTableSignatures)delete _wdTableSignatures[e]}function _wdSetTbodyHTML(e,t){let n=e;for(;n&&n!==document.body;){const e=window.getComputedStyle(n);if(("auto"===e.overflowY||"scroll"===e.overflowY)&&n.scrollHeight>n.clientHeight)break;n=n.parentElement}const a=n?n.scrollTop:0,s=n?n.scrollLeft:0;e.innerHTML=t,n&&(n.scrollTop=a,n.scrollLeft=s)}const _WD_HEADERS={wifi:'<tr><th class="px-3 py-2">SSID</th><th class="px-3 py-2">BSSID</th><th class="px-3 py-2">Security</th><th class="px-3 py-2">Ch</th><th class="px-3 py-2">Band</th><th class="px-3 py-2">Signal</th><th class="px-3 py-2 text-center">📷</th><th class="px-3 py-2 text-center">GPS</th><th class="px-3 py-2">Seen</th></tr>',bluetooth:'<tr><th class="px-3 py-2">Name</th><th class="px-3 py-2">MAC</th><th class="px-3 py-2">Type</th><th class="px-3 py-2">RSSI</th><th class="px-3 py-2 text-center">GPS</th><th class="px-3 py-2">First Seen</th><th class="px-3 py-2">Seen</th></tr>',cell:'<tr><th class="px-3 py-2">Provider</th><th class="px-3 py-2">Tech</th><th class="px-3 py-2">Cell ID</th><th class="px-3 py-2">MCC/MNC</th><th class="px-3 py-2">Signal</th><th class="px-3 py-2">Band</th><th class="px-3 py-2 text-center">GPS</th><th class="px-3 py-2">Seen</th></tr>',cameras:'<tr><th class="px-3 py-2">SSID</th><th class="px-3 py-2">BSSID</th><th class="px-3 py-2">Security</th><th class="px-3 py-2">Ch</th><th class="px-3 py-2">Band</th><th class="px-3 py-2">Signal</th><th class="px-3 py-2 text-center">GPS</th><th class="px-3 py-2">Seen</th></tr>'};function _setWdTableHeaders(e){const t=document.getElementById("wd-table-head");t&&(t.innerHTML=_WD_HEADERS[e]||_WD_HEADERS.wifi)}async function loadWardrivingNetworks(){_setWdTableHeaders("wifi");try{const e=_wdSelectedSessionId?`&session_id=${encodeURIComponent(_wdSelectedSessionId)}`:"",t=await fetch(`/api/wardriving/networks?limit=200${e}`),n=await t.json(),a=document.getElementById("wd-network-table");if(!a)return;const s=n.networks||[];if(!_wdShouldRender("wifi",_wdSig("wifi",s,e=>(e.scan_count||0)+(e.best_rssi||0))))return;if(0===s.length)return void _wdSetTbodyHTML(a,'<tr><td colspan="9" class="text-center text-gray-500 py-8">No networks yet.</td></tr>');_wdSetTbodyHTML(a,s.map(e=>{const t=e.security&&"--"!==e.security?e.security.includes("WEP")?"text-yellow-400":"text-blue-400":"text-green-400",n=e.best_rssi>-50?"text-emerald-400":e.best_rssi>-70?"text-yellow-400":"text-red-400",a=e.ssid||"<hidden>",s=e.best_lat&&e.best_lon&&0!==e.best_lat&&0!==e.best_lon?`<span title="${e.best_lat.toFixed(5)}, ${e.best_lon.toFixed(5)}" class="text-emerald-400 cursor-help">📍</span>`:'<span class="text-gray-600">—</span>',o=e.is_camera?'<span class="text-pink-400">📷</span>':"";return`<tr class="hover:bg-slate-800/50">\n <td class="px-3 py-1.5 font-mono text-xs" data-label="SSID">${escapeHtml(a)}</td>\n <td class="px-3 py-1.5 font-mono text-xs text-gray-400" data-label="BSSID">${e.bssid}</td>\n <td class="px-3 py-1.5 text-xs ${t}" data-label="Security">${e.security||"Open"}</td>\n <td class="px-3 py-1.5 text-xs text-center" data-label="Ch">${e.channel||"-"}</td>\n <td class="px-3 py-1.5 text-xs" data-label="Band">${e.band||"-"}</td>\n <td class="px-3 py-1.5 text-xs ${n}" data-label="Signal">${e.best_rssi} dBm</td>\n <td class="px-3 py-1.5 text-xs text-center" data-label="Camera">${o||"—"}</td>\n <td class="px-3 py-1.5 text-xs text-center" data-label="GPS">${s}</td>\n <td class="px-3 py-1.5 text-xs text-gray-400" data-label="Seen">${e.scan_count||1}x</td>\n </tr>`}).join(""));const o=document.getElementById("wd-table-info");o&&o.classList.remove("hidden"),updateElement("wd-showing",String(s.length)),updateElement("wd-total",String(n.total||s.length))}catch(e){console.error("[Wardriving] Networks error:",e)}}async function _loadWardrivingBluetooth(){_setWdTableHeaders("bluetooth");const e=document.getElementById("wd-network-table");if(e)try{const t=_wdSelectedSessionId?`?session_id=${encodeURIComponent(_wdSelectedSessionId)}`:"",n=await fetch(`/api/wardriving/bluetooth${t}`),a=await n.json(),s=a.devices||[];if(!_wdShouldRender("bluetooth",_wdSig("bluetooth",s,e=>(e.scan_count||0)+(e.rssi||0))))return;if(0===s.length)return void _wdSetTbodyHTML(e,'<tr><td colspan="7" class="text-center text-gray-500 py-8">No Bluetooth devices found yet.</td></tr>');_wdSetTbodyHTML(e,s.map(e=>{const t=e.latitude&&e.longitude?`<span title="${e.latitude.toFixed(5)}, ${e.longitude.toFixed(5)}" class="text-emerald-400 cursor-help">📍</span>`:'<span class="text-gray-600">—</span>',n=e.rssi>-50?"text-emerald-400":e.rssi>-70?"text-yellow-400":"text-red-400";return`<tr class="hover:bg-slate-800/50">\n <td class="px-3 py-1.5 font-mono text-xs" data-label="Name">${escapeHtml(e.name||"(unknown)")}</td>\n <td class="px-3 py-1.5 font-mono text-xs text-gray-400" data-label="MAC">${e.mac}</td>\n <td class="px-3 py-1.5 text-xs text-orange-400" data-label="Type">${e.device_type||"-"}</td>\n <td class="px-3 py-1.5 text-xs ${n}" data-label="RSSI">${e.rssi||"-"} dBm</td>\n <td class="px-3 py-1.5 text-xs text-center" data-label="GPS">${t}</td>\n <td class="px-3 py-1.5 text-xs text-gray-400" data-label="First Seen">${e.first_seen||"-"}</td>\n <td class="px-3 py-1.5 text-xs text-gray-400" data-label="Seen">${e.scan_count||1}x</td>\n </tr>`}).join(""));const o=document.getElementById("wd-table-info");o&&o.classList.remove("hidden"),updateElement("wd-showing",String(s.length)),updateElement("wd-total",String(a.total||s.length))}catch(e){console.error("[Wardriving] BT table error:",e)}}async function _loadWardrivingCellTable(){_setWdTableHeaders("cell");const e=document.getElementById("wd-network-table");if(e)try{const t=await fetch("/api/wardriving/cells"),n=await t.json(),a=n.towers||[];if(!_wdShouldRender("cell",_wdSig("cell",a,e=>(e.scan_count||0)+(e.signal_dbm||0))))return;if(0===a.length)return void _wdSetTbodyHTML(e,'<tr><td colspan="8" class="text-center text-gray-500 py-8">No cell towers found yet.</td></tr>');_wdSetTbodyHTML(e,a.map(e=>{const t=e.latitude&&e.longitude?`<span title="${e.latitude.toFixed(5)}, ${e.longitude.toFixed(5)}" class="text-emerald-400 cursor-help">📍</span>`:'<span class="text-gray-600">—</span>',n=e.signal_dbm>-70?"text-emerald-400":e.signal_dbm>-90?"text-yellow-400":"text-red-400";return`<tr class="hover:bg-slate-800/50">\n <td class="px-3 py-1.5 text-xs text-fuchsia-400" data-label="Provider">${escapeHtml(e.provider||"-")}</td>\n <td class="px-3 py-1.5 text-xs" data-label="Tech">${e.tech||"-"}</td>\n <td class="px-3 py-1.5 font-mono text-xs text-gray-400" data-label="Cell ID">${e.cell_id||"-"}</td>\n <td class="px-3 py-1.5 text-xs" data-label="MCC/MNC">${e.mcc||"-"}/${e.mnc||"-"}</td>\n <td class="px-3 py-1.5 text-xs ${n}" data-label="Signal">${e.signal_dbm||"-"} dBm</td>\n <td class="px-3 py-1.5 text-xs" data-label="Band">${e.band_freq||"-"}</td>\n <td class="px-3 py-1.5 text-xs text-center" data-label="GPS">${t}</td>\n <td class="px-3 py-1.5 text-xs text-gray-400" data-label="Seen">${e.scan_count||1}x</td>\n </tr>`}).join(""));const s=document.getElementById("wd-table-info");s&&s.classList.remove("hidden"),updateElement("wd-showing",String(a.length)),updateElement("wd-total",String(n.total||a.length))}catch(e){console.error("[Wardriving] Cell table error:",e)}}async function _loadWardrivingCameras(){_setWdTableHeaders("cameras");const e=document.getElementById("wd-network-table");if(e)try{const t=_wdSelectedSessionId?`&session_id=${encodeURIComponent(_wdSelectedSessionId)}`:"",n=await fetch(`/api/wardriving/networks?limit=2000${t}`),a=((await n.json()).networks||[]).filter(e=>e.is_camera);if(!_wdShouldRender("cameras",_wdSig("cameras",a,e=>(e.scan_count||0)+(e.best_rssi||0))))return;if(0===a.length)return void _wdSetTbodyHTML(e,'<tr><td colspan="8" class="text-center text-gray-500 py-8">No cameras detected yet.</td></tr>');_wdSetTbodyHTML(e,a.map(e=>{const t=e.security&&"--"!==e.security?e.security.includes("WEP")?"text-yellow-400":"text-blue-400":"text-green-400",n=e.best_rssi>-50?"text-emerald-400":e.best_rssi>-70?"text-yellow-400":"text-red-400",a=e.best_lat&&e.best_lon&&0!==e.best_lat&&0!==e.best_lon?`<span title="${e.best_lat.toFixed(5)}, ${e.best_lon.toFixed(5)}" class="text-emerald-400 cursor-help">📍</span>`:'<span class="text-gray-600">—</span>';return`<tr class="hover:bg-slate-800/50">\n <td class="px-3 py-1.5 font-mono text-xs text-pink-400" data-label="SSID">${escapeHtml(e.ssid||"<hidden>")} 📷</td>\n <td class="px-3 py-1.5 font-mono text-xs text-gray-400" data-label="BSSID">${e.bssid}</td>\n <td class="px-3 py-1.5 text-xs ${t}" data-label="Security">${e.security||"Open"}</td>\n <td class="px-3 py-1.5 text-xs text-center" data-label="Ch">${e.channel||"-"}</td>\n <td class="px-3 py-1.5 text-xs" data-label="Band">${e.band||"-"}</td>\n <td class="px-3 py-1.5 text-xs ${n}" data-label="Signal">${e.best_rssi} dBm</td>\n <td class="px-3 py-1.5 text-xs text-center" data-label="GPS">${a}</td>\n <td class="px-3 py-1.5 text-xs text-gray-400" data-label="Seen">${e.scan_count||1}x</td>\n </tr>`}).join(""));const s=document.getElementById("wd-table-info");s&&s.classList.remove("hidden"),updateElement("wd-showing",String(a.length)),updateElement("wd-total",String(a.length))}catch(e){console.error("[Wardriving] Camera table error:",e)}}function renderWardrivingSessions(e){const t=document.getElementById("wd-sessions-list");t&&(e&&0!==e.length?t.innerHTML=e.map(e=>{const t=_wdSelectedSessionId===e.session_id,n=t?"ring-2 ring-cyan-500 bg-slate-700/60":"bg-slate-800/40";let a=e.session_id;if(e.start_time){const t=new Date("number"==typeof e.start_time?1e3*e.start_time:e.start_time);isNaN(t.getTime())||(a=t.toLocaleString())}return`\n <div class="flex flex-wrap items-center justify-between ${n} rounded-lg px-3 sm:px-4 py-2 gap-x-3 gap-y-1 cursor-pointer hover:bg-slate-700/50 transition-colors" onclick="selectWardrivingSession('${e.session_id}')">\n <div class="flex flex-wrap items-baseline gap-x-2 gap-y-0.5 min-w-0 flex-1">\n <span class="text-sm font-mono text-gray-300 break-all">${a}</span>\n <span class="text-xs text-gray-500">${e.total_networks||0} networks</span>\n ${t?'<span class="text-xs text-cyan-400">● viewing</span>':""}\n </div>\n <div class="flex gap-3 shrink-0">\n <a href="/api/wardriving/export/${encodeURIComponent(e.session_id)}?format=wigle" class="text-xs text-cyan-400 hover:text-cyan-300 whitespace-nowrap" onclick="event.stopPropagation()">WiGLE CSV</a>\n <a href="/api/wardriving/export/${encodeURIComponent(e.session_id)}?format=kml" class="text-xs text-purple-400 hover:text-purple-300 whitespace-nowrap" onclick="event.stopPropagation()">KML</a>\n </div>\n </div>`}).join(""):t.innerHTML='<p class="text-gray-500 text-sm">No previous sessions.</p>')}function selectWardrivingSession(e){_wdSelectedSessionId=_wdSelectedSessionId===e?null:e,_wdInvalidateTableCache();const t=document.getElementById("wd-session-banner"),n=document.getElementById("wd-back-live-btn");t&&t.classList.toggle("hidden",!_wdSelectedSessionId),n&&n.classList.toggle("hidden",!_wdSelectedSessionId),fetch("/api/wardriving/sessions").then(e=>e.json()).then(e=>renderWardrivingSessions(e.sessions||[]));const a=document.querySelector('#wd-table-tabs .wd-tab-active, #wd-table-tabs [class*="bg-indigo-600"]'),s=a?.getAttribute("data-type")||"wifi";"wifi"===s?loadWardrivingNetworks():"bluetooth"===s?_loadWardrivingBluetooth():"cameras"===s&&_loadWardrivingCameras(),_wdMapVisible&&loadWardrivingMapData()}let _wdMap=null,_wdMapVisible=!1,_wdMapClusterGroup=null,_wdMapAllNetworks=[],_wdMapBtDevices=[],_wdMapCellTowers=[],_wdVikingMarker=null,_wdGpsInterval=null,_wdSelectedSessionId=null;const _VIKING_ICON_HTML='<img src="/web/images/ragnar.ico" alt="Ragnar" style="width:36px;height:48px;display:block;filter:drop-shadow(0 2px 3px rgba(0,0,0,0.5));">';function toggleWardrivingMap(){const e=document.getElementById("wd-map-container");if(!e)return;_wdMapVisible=!_wdMapVisible,e.classList.toggle("hidden",!_wdMapVisible);const t=document.getElementById("wd-map-btn");t&&(_wdMapVisible?(t.classList.remove("bg-indigo-600","hover:bg-indigo-700"),t.classList.add("bg-indigo-800","hover:bg-indigo-900")):(t.classList.remove("bg-indigo-800","hover:bg-indigo-900"),t.classList.add("bg-indigo-600","hover:bg-indigo-700"))),_wdMapVisible?(_wdMap||(_wdMap=L.map("wd-map",{zoomControl:!0}).setView([59.33,18.07],13),L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",{maxZoom:19,attribution:"© OpenStreetMap"}).addTo(_wdMap)),setTimeout(()=>{_wdMap.invalidateSize()},200),loadWardrivingMapData(),_wdGpsInterval||(_wdGpsInterval=setInterval(_updateVikingPosition,3e3))):(_wdGpsInterval&&(clearInterval(_wdGpsInterval),_wdGpsInterval=null),_wdMapFullscreen&&toggleWardrivingMapFullscreen())}let _wdMapFullscreen=!1,_wdMapFsEscHandler=null;function toggleWardrivingMapFullscreen(){const e=document.getElementById("wd-map-container");if(!e||e.classList.contains("hidden"))return;_wdMapFullscreen=!_wdMapFullscreen,e.classList.toggle("wd-map-fullscreen",_wdMapFullscreen),document.body.classList.toggle("wd-map-fs-open",_wdMapFullscreen);const t=document.getElementById("wd-map-fs-icon-enter"),n=document.getElementById("wd-map-fs-icon-exit"),a=document.getElementById("wd-map-fs-label");t&&t.classList.toggle("hidden",_wdMapFullscreen),n&&n.classList.toggle("hidden",!_wdMapFullscreen),a&&(a.textContent=_wdMapFullscreen?"Exit":"Fullscreen"),_wdMapFullscreen?(_wdMapFsEscHandler=e=>{"Escape"===e.key&&_wdMapFullscreen&&toggleWardrivingMapFullscreen()},document.addEventListener("keydown",_wdMapFsEscHandler)):_wdMapFsEscHandler&&(document.removeEventListener("keydown",_wdMapFsEscHandler),_wdMapFsEscHandler=null),_wdMap&&setTimeout(()=>_wdMap.invalidateSize(),220)}function _wdSecurityType(e){return e&&"Open"!==e?e.includes("WEP")?"wep":"wpa":"open"}function _wdMarkerColor(e){const t=_wdSecurityType(e);return"open"===t?"#10b981":"wep"===t?"#f59e0b":"#3b82f6"}let _wdLastFixLat=null,_wdLastFixLon=null;async function _updateVikingPosition(){if(_wdMap&&_wdMapVisible)try{const e=await fetch("/api/wardriving/gps"),t=await e.json();if(t.has_fix&&t.latitude&&t.longitude){const e=t.speed_kmh||0,n=t.hdop||0,a=e<2&&n>3&&null!==_wdLastFixLat,s=a?_wdLastFixLat:t.latitude,o=a?_wdLastFixLon:t.longitude;a||(_wdLastFixLat=t.latitude,_wdLastFixLon=t.longitude);const r=a?'<br><span style="color:#94a3b8">Stationär (drift dämpad)</span>':"",i=`<b>Ragnar</b><br>${e.toFixed(1)} km/h${r}`;if(_wdVikingMarker)_wdVikingMarker.setLatLng([s,o]),_wdVikingMarker.setPopupContent(i);else{const e=L.divIcon({html:_VIKING_ICON_HTML,className:"wd-viking-icon",iconSize:[36,48],iconAnchor:[18,44]});_wdVikingMarker=L.marker([s,o],{icon:e,zIndexOffset:1e3}).addTo(_wdMap),_wdVikingMarker.bindPopup(i)}}}catch(e){}}async function loadWardrivingMapData(){if(_wdMap)try{const e=_wdSelectedSessionId?encodeURIComponent(_wdSelectedSessionId):"",t=2e3,n=10,a=e?`session_id=${e}&`:"",s=[];let o=0;for(let e=0;e<n;e++){const n=e*t,r=await fetch(`/api/wardriving/networks?${a}limit=${t}&offset=${n}&sort=first_seen&order=ASC`),i=await r.json(),l=i.networks||[];if(s.push(...l),o=i.total||s.length,l.length<t)break}const r=e?`/api/wardriving/bluetooth?session_id=${e}`:"/api/wardriving/bluetooth",i=e?`/api/wardriving/cells?session_id=${e}`:"/api/wardriving/cells",[l,c]=await Promise.all([fetch(r),fetch(i)]),d=await l.json(),u=await c.json();_wdMapAllNetworks=s.filter(e=>e.best_lat&&e.best_lon&&0!==e.best_lat&&0!==e.best_lon),_wdMapBtDevices=(d.devices||[]).filter(e=>e.latitude&&e.longitude),_wdMapCellTowers=(u.towers||[]).filter(e=>e.latitude&&e.longitude),_wdMapNetTotalDb=o,applyWardrivingMapFilters(),_updateVikingPosition()}catch(e){console.error("[Wardriving] Map error:",e)}}let _wdMapNetTotalDb=0;async function backfillWardrivingGps(){const e=document.getElementById("wd-map-backfill-btn");e&&(e.disabled=!0,e.textContent="Backfilling…");try{const e=_wdSelectedSessionId?{session_id:_wdSelectedSessionId}:{},t=await fetch("/api/wardriving/backfill_gps",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),n=await t.json();if(n.error)alert("Backfill failed: "+n.error);else{const e=n.backfilled||{},t=`Backfilled ${e.wifi||0} WiFi, ${e.bluetooth||0} BT, ${e.cells||0} cells from ${e.trackpoints||0} GPS trackpoints.`,a=document.getElementById("wd-map-info");if(a){a.textContent;a.textContent=t,a.classList.add("text-emerald-400"),setTimeout(()=>{a.classList.remove("text-emerald-400")},6e3)}console.log("[Wardriving] "+t),await loadWardrivingMapData()}}catch(e){alert("Backfill error: "+e)}finally{e&&(e.disabled=!1,e.innerHTML='<svg class="w-3.5 h-3.5 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>Backfill GPS')}}function applyWardrivingMapFilters(){if(!_wdMap)return;const e=document.getElementById("wd-map-filter-type")?.value||"all",t=document.getElementById("wd-map-filter-security")?.value||"all",n=document.getElementById("wd-map-filter-band")?.value||"all",a=parseInt(document.getElementById("wd-map-filter-signal")?.value||"-100",10);_wdMapClusterGroup&&_wdMap.removeLayer(_wdMapClusterGroup),_wdMapClusterGroup=L.markerClusterGroup({maxClusterRadius:40,spiderfyOnMaxZoom:!0,showCoverageOnHover:!1,iconCreateFunction:function(e){const t=e.getChildCount();let n=t>=50?[44,44]:t>=10?[36,36]:[28,28];return L.divIcon({html:'<div style="background:rgba(99,102,241,0.85);color:#fff;border-radius:50%;width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-weight:bold;font-size:12px;border:2px solid rgba(255,255,255,0.4);">'+t+"</div>",className:"wd-cluster-icon",iconSize:n})}});const s=[];let o=0;if("all"===e||"wifi"===e||"cameras"===e){let r=_wdMapAllNetworks;"cameras"===e&&(r=r.filter(e=>e.is_camera)),"all"!==t&&(r=r.filter(e=>_wdSecurityType(e.security)===t)),"all"!==n&&(r=r.filter(e=>e.band===n)),a>-100&&(r=r.filter(e=>e.best_rssi>=a)),r.forEach(e=>{const t=e.best_lat,n=e.best_lon;s.push([t,n]);const a=e.is_camera?"#ec4899":_wdMarkerColor(e.security),r=L.circleMarker([t,n],{radius:e.is_camera?9:7,fillColor:a,color:"#1e293b",weight:1,fillOpacity:.85}),i=e.ssid||"<hidden>",l=e.is_camera?" 📷":"";r.bindPopup(`<div style="font-family:monospace;min-width:180px;">\n <b style="font-size:13px;">${i}${l}</b><br>\n <span style="color:#888;">BSSID:</span> ${e.bssid}<br>\n <span style="color:#888;">Security:</span> <span style="color:${a}">${e.security||"Open"}</span><br>\n <span style="color:#888;">Channel:</span> ${e.channel||"-"} (${e.band||"-"})<br>\n <span style="color:#888;">Signal:</span> ${e.best_rssi} dBm<br>\n <span style="color:#888;">Seen:</span> ${e.scan_count||1}x<br>\n <span style="color:#888;">Pos:</span> ${t.toFixed(5)}, ${n.toFixed(5)}\n </div>`),_wdMapClusterGroup.addLayer(r),o++})}"all"!==e&&"bluetooth"!==e||_wdMapBtDevices.forEach(e=>{s.push([e.latitude,e.longitude]);const t=L.circleMarker([e.latitude,e.longitude],{radius:6,fillColor:"#f97316",color:"#1e293b",weight:1,fillOpacity:.85});t.bindPopup(`<div style="font-family:monospace;min-width:160px;">\n <b style="font-size:13px;">🔵 ${e.name||e.mac}</b><br>\n <span style="color:#888;">MAC:</span> ${e.mac}<br>\n <span style="color:#888;">Type:</span> ${e.device_type||"-"}<br>\n <span style="color:#888;">RSSI:</span> ${e.rssi} dBm<br>\n <span style="color:#888;">Seen:</span> ${e.scan_count||1}x\n </div>`),_wdMapClusterGroup.addLayer(t),o++}),"all"!==e&&"cell"!==e||_wdMapCellTowers.forEach(e=>{s.push([e.latitude,e.longitude]);const t=L.circleMarker([e.latitude,e.longitude],{radius:10,fillColor:"#d946ef",color:"#1e293b",weight:2,fillOpacity:.8});t.bindPopup(`<div style="font-family:monospace;min-width:160px;">\n <b style="font-size:13px;">📶 ${e.provider||"Cell"} ${e.tech||""}</b><br>\n <span style="color:#888;">CellID:</span> ${e.cell_id}<br>\n <span style="color:#888;">MCC/MNC:</span> ${e.mcc||"-"}/${e.mnc||"-"}<br>\n <span style="color:#888;">Signal:</span> ${e.signal_dbm} dBm<br>\n <span style="color:#888;">Seen:</span> ${e.scan_count||1}x\n </div>`),_wdMapClusterGroup.addLayer(t),o++}),_wdMap.addLayer(_wdMapClusterGroup),s.length>0&&_wdMap.fitBounds(s,{padding:[30,30]});const r=_wdMapAllNetworks.length+_wdMapBtDevices.length+_wdMapCellTowers.length,i=_wdMapNetTotalDb||0,l=i>_wdMapAllNetworks.length?` (${i-_wdMapAllNetworks.length} WiFi without GPS)`:"";document.getElementById("wd-map-info").textContent=0===o?"No GPS-tagged items match filters.":o===r?`${r} items with GPS position${l}`:`${o} / ${r} items match filters${l}`}let _wardrivingRunning=!1,_wardrivingBusy=!1;async function toggleWardriving(){if(_wardrivingBusy)return;_wardrivingBusy=!0;const e=document.getElementById("wd-toggle-btn");e&&(e.disabled=!0,e.classList.add("opacity-50","pointer-events-none"));try{const e=_wardrivingRunning?"/api/wardriving/stop":"/api/wardriving/start",t=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:"{}"}),n=await t.json();n.error?showToast(n.error,"error"):n.success&&(_wardrivingRunning?showToast(`Wardriving stopped. ${n.stats?.total_networks||0} networks found.`,"success"):showToast(`Wardriving started! Session: ${n.session_id}`,"success"))}catch(e){showToast("Failed to toggle wardriving","error")}await loadWardrivingData(),_wardrivingRunning&&(setTimeout(()=>loadWardrivingData(),2e3),setTimeout(()=>loadWardrivingData(),5e3)),_wardrivingBusy=!1,e&&(e.disabled=!1,e.classList.remove("opacity-50","pointer-events-none"))}function updateWardrivingToggleButton(){const e=document.getElementById("wd-toggle-btn");e&&(_wardrivingRunning?(e.innerHTML='\n <svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"></path>\n </svg>\n Stop\n ',e.classList.remove("bg-green-600","hover:bg-green-700"),e.classList.add("bg-red-600","hover:bg-red-700")):(e.innerHTML='\n <svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path>\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>\n </svg>\n Start\n ',e.classList.remove("bg-red-600","hover:bg-red-700"),e.classList.add("bg-green-600","hover:bg-green-700")))}async function loadAdvancedVulnData(){try{const[e,t]=await Promise.all([fetch("/api/vuln-advanced/status"),fetch("/api/vuln-advanced/findings?limit=1000")]),n=await e.json();if(!n.success||!n.available)return void showAdvVulnNotAvailable();hideAdvVulnNotAvailable(),updateScannerStatus(n.scanners,n.nuclei_templates),updateVulnSummary(n.summary);try{const e=await t.json();e.success&&(advVulnFindingsCache=e.findings||[])}catch(e){console.warn("Error parsing findings:",e)}updateActiveScans(n.active_scans),updateVulnStats(n.active_scans,advVulnFindingsCache);(n.active_scans||[]).some(e=>"running"===e.status)&&!advVulnRefreshInterval&&startAdvVulnPolling()}catch(e){console.error("Error loading advanced vuln data:",e),showAdvVulnNotAvailable()}}function updateActiveScans(e,t={}){const n=document.getElementById("adv-vuln-active-scans");if(!n)return;advVulnScansCache=e||[];const a=document.getElementById("adv-vuln-scans-spinner"),s=advVulnScansCache.some(e=>"running"===e.status);if(a&&a.classList.toggle("animate-spin",s),!advVulnScansCache.length)return n.innerHTML='<p class="text-gray-400 text-center py-4 text-sm">No active scans</p>',void updateScansToggleButton(0);const o=buildAdvVulnScanFindingsMap(advVulnScansCache);advVulnScanFindingsMap=o;const r=new Set(advVulnScansCache.map(e=>e.scan_id));advVulnExpandedScanIds.forEach(e=>{r.has(e)||advVulnExpandedScanIds.delete(e)});const i=advVulnShowAllScans?advVulnScansCache:advVulnScansCache.slice(0,3);updateScansToggleButton(advVulnScansCache.length);const l={running:"Running",completed:"Completed",failed:"Failed",cancelled:"Cancelled"},c={critical:"bg-red-600 text-white",high:"bg-orange-500 text-white",medium:"bg-yellow-500 text-black",low:"bg-blue-500 text-white",info:"bg-gray-500 text-white"},d=new Map;for(const e of advVulnExpandedLogIds){const t=document.getElementById(`scan-logs-${e}`);t&&d.set(e,t.scrollTop)}n.innerHTML=i.map(e=>{const t=e.scan_id,n=o.get(t)||{findings:[],counts:{}},a=n.counts||{},s=e.findings_count||n.findings.length||0,r=10*(a.critical||0)+7*(a.high||0)+4*(a.medium||0)+1*(a.low||0),i=r>=50?"text-red-400":r>=30?"text-orange-400":r>=15?"text-yellow-400":"text-green-400",d=getScanDurationSeconds(e),u=e.status||"unknown",p=e.progress_percent||e.progress||0,g=e.current_check||e.current_phase||"Processing...",m=e.error_message||"",f=e.auth_type||"",h=e.auth_status||"",y=e.completed_at||e.started_at,w=e.completed_at?"Completed":"Started",v=advVulnExpandedScanIds.has(t),b=advVulnExpandedLogIds.has(t);return`\n <div>\n <div class="p-4 rounded-lg border ${v||b?(v?"border-cyan-500/50":"border-green-500/50")+" rounded-b-none":"border-slate-700"} bg-slate-800/60 hover:bg-slate-700/70 transition cursor-pointer"\n onclick="toggleAdvVulnScanFindings('${t}')">\n <div class="flex items-start justify-between gap-3">\n <div>\n <div class="text-sm font-semibold text-white">${escapeHtml(formatScanType(e.scan_type))}</div>\n <div class="text-xs text-gray-400 mt-1">${escapeHtml(e.target||"Unknown target")}</div>\n </div>\n <span class="text-[11px] uppercase px-2 py-1 rounded-full bg-slate-700 text-gray-300">${escapeHtml(l[u]||u)}</span>\n </div>\n\n <div class="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-2 text-xs text-gray-300">\n <div>${w}: <span class="text-gray-100">${y?escapeHtml(formatScanTimestamp(y)):"N/A"}</span></div>\n <div>Duration: <span class="text-gray-100">${d?formatDuration(d):"N/A"}</span></div>\n <div>Findings: <span class="text-cyan-300 font-semibold">${s}</span></div>\n <div>Method: <span class="text-gray-100">${escapeHtml(formatScanType(e.scan_type))}</span></div>\n </div>\n\n <div class="mt-3 flex flex-wrap items-center gap-2 text-[11px]">\n ${["critical","high","medium","low","info"].map(e=>`\n <span class="px-2 py-0.5 rounded ${c[e]}">\n ${e.toUpperCase()}: ${a[e]||0}\n </span>\n `).join("")}\n ${s>0?`<span class="ml-auto text-xs font-semibold ${i}">Risk: ${r}</span>`:""}\n </div>\n\n ${f?`\n <div class="flex items-center gap-1 text-xs mt-2">\n <span class="px-1.5 py-0.5 rounded ${h.startsWith("verified")?"bg-green-900 text-green-400":h.startsWith("failed")?"bg-red-900 text-red-400":"bg-yellow-900 text-yellow-400"}">\n ${h.startsWith("verified")?"✓":h.startsWith("failed")?"✗":"🔐"} ${escapeHtml(f)} ${h?`- ${escapeHtml(h)}`:""}\n </span>\n </div>\n `:""}\n\n ${"running"===u?`\n <div class="mt-3">\n <div class="flex justify-between text-xs text-gray-400 mb-1">\n <span>${escapeHtml(g)}</span>\n <span>${p}%</span>\n </div>\n <div class="h-1.5 bg-slate-700 rounded-full overflow-hidden">\n <div class="h-full bg-blue-500 transition-all duration-500 ${0===p?"animate-pulse !w-full opacity-30":""}" style="${p>0?`width: ${p}%`:""}"></div>\n </div>\n </div>\n `:""}\n ${m?`<div class="text-xs text-red-400 mt-2">${escapeHtml(m)}</div>`:""}\n ${"completed"!==u||0!==s||m?"":'\n <div class="text-xs text-yellow-400 mt-2">0 findings - check server logs for details</div>\n '}\n\n <div class="mt-3 flex items-center justify-between">\n <div class="flex gap-3 flex-wrap text-xs">\n ${"running"===u?`\n <button onclick="event.stopPropagation(); cancelAdvScan('${t}')" class="text-red-400 hover:text-red-300 flex items-center gap-1">\n <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>\n Cancel\n </button>\n `:`\n <button onclick="event.stopPropagation(); downloadScanReport('${t}')" class="text-cyan-400 hover:text-cyan-300 flex items-center gap-1">\n <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>\n Report\n </button>\n <button onclick="event.stopPropagation(); deleteAdvScan('${t}')" class="text-gray-400 hover:text-red-400 flex items-center gap-1">\n <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>\n Delete\n </button>\n `}\n </div>\n <div class="flex items-center gap-3">\n <button onclick="event.stopPropagation(); toggleAdvVulnScanLogs('${t}')"\n class="flex items-center gap-1 text-xs ${advVulnExpandedLogIds.has(t)?"text-green-400":"text-gray-400 hover:text-gray-200"} transition-colors">\n <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>\n </svg>\n <span>${advVulnExpandedLogIds.has(t)?"Hide":"Show"} Logs</span>\n </button>\n <button onclick="event.stopPropagation(); toggleAdvVulnScanFindings('${t}')"\n class="flex items-center gap-1 text-xs ${v?"text-cyan-400":"text-gray-400 hover:text-gray-200"} transition-colors">\n <span>${v?"Hide":"Show"} Findings</span>\n <svg class="w-4 h-4 transition-transform ${v?"rotate-180":""}" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>\n </svg>\n </button>\n </div>\n </div>\n </div>\n ${b?renderInlineScanLogs(t,v):""}\n ${v?renderInlineScanFindings(t,e):""}\n </div>\n `}).join("");for(const[e,t]of d){const n=document.getElementById(`scan-logs-${e}`);n&&(n.scrollTop=t)}}function toggleAdvVulnScansExpanded(){advVulnShowAllScans=!advVulnShowAllScans,updateActiveScans(advVulnScansCache,{preserveSelection:!0})}function updateScansToggleButton(e){const t=document.getElementById("adv-vuln-scans-toggle"),n=document.getElementById("adv-vuln-scans-toggle-icon");t&&(e<=3?t.classList.add("hidden"):(t.classList.remove("hidden"),t.querySelector("span").textContent=advVulnShowAllScans?"Show fewer scans":`Show all scans (${e})`,n&&n.classList.toggle("rotate-180",advVulnShowAllScans)))}function buildAdvVulnScanFindingsMap(e){const t=e.map(e=>e.scan_id).filter(Boolean),n=new Map;return e.forEach(e=>{n.set(e.scan_id,{findings:[],counts:{critical:0,high:0,medium:0,low:0,info:0}})}),advVulnFindingsCache.forEach(e=>{const a=getFindingScanId(e,t);if(!a||!n.has(a))return;const s=n.get(a);s.findings.push(e),s.counts.hasOwnProperty(e.severity)&&s.counts[e.severity]++}),n}function getFindingScanId(e,t=[]){return e?e.scan_id?e.scan_id:e.finding_id&&t.find(t=>e.finding_id.startsWith(`${t}-`))||null:null}function formatScanType(e){return e?e.replace(/_/g," ").replace(/\b\w/g,e=>e.toUpperCase()):"Unknown"}function formatScanTimestamp(e){if(!e)return"N/A";const t=new Date(e);return Number.isNaN(t.getTime())?e:t.toLocaleString(void 0,{weekday:"short",month:"short",day:"numeric",hour:"numeric",minute:"2-digit"})}function getScanDurationSeconds(e){if(!e)return 0;if(e.duration_seconds)return e.duration_seconds;if(!e.started_at)return 0;const t=new Date(e.started_at);return Number.isNaN(t.getTime())?0:Math.max(0,(Date.now()-t.getTime())/1e3)}function toggleAdvVulnScanFindings(e){advVulnExpandedScanIds.has(e)?advVulnExpandedScanIds.delete(e):advVulnExpandedScanIds.add(e),updateActiveScans(advVulnScansCache,{preserveSelection:!0})}function renderInlineScanFindings(e,t){const n=(advVulnScanFindingsMap.get(e)||{findings:[],counts:{}}).findings||[],a={critical:"bg-red-600 text-white",high:"bg-orange-500 text-white",medium:"bg-yellow-500 text-black",low:"bg-blue-500 text-white",info:"bg-gray-500 text-white"},s={critical:0,high:1,medium:2,low:3,info:4},o=[...n].sort((e,t)=>(s[e.severity]||5)-(s[t.severity]||5));return`\n <div class="border border-t-0 border-cyan-500/50 rounded-b-lg bg-slate-900/50 p-3 space-y-3 max-h-[600px] overflow-y-auto">\n ${o.length?o.map(e=>{const t=e.timestamp?formatScanTimestamp(e.timestamp):"N/A",n=e.details?JSON.stringify(e.details,null,2):"";return`\n <div class="p-4 rounded-lg border border-slate-700 bg-slate-800/60">\n <div class="flex flex-wrap items-center justify-between gap-2">\n <div class="flex items-center gap-2">\n <span class="px-2 py-1 rounded text-xs uppercase ${a[e.severity]||"bg-gray-600 text-white"}">\n ${escapeHtml(e.severity||"info")}\n </span>\n <span class="text-sm font-semibold text-white">${escapeHtml(e.title||"Untitled Finding")}</span>\n </div>\n <span class="text-xs text-gray-400">${t}</span>\n </div>\n <div class="mt-2 text-xs text-gray-300 flex flex-wrap gap-3">\n <span>Host: <span class="font-mono text-gray-100">${escapeHtml(e.host||"N/A")}${e.port?":"+e.port:""}</span></span>\n <span>Scanner: <span class="text-gray-100">${escapeHtml(e.scanner||"Unknown")}</span></span>\n ${e.cvss_score?`<span>CVSS: <span class="text-gray-100">${escapeHtml(e.cvss_score)}</span></span>`:""}\n ${e.matched_at?`<span>URL: <span class="text-gray-100 break-all">${escapeHtml(e.matched_at)}</span></span>`:""}\n </div>\n ${e.description?`<div class="mt-3 text-sm text-gray-200">${escapeHtml(e.description)}</div>`:""}\n ${e.remediation?`<div class="mt-3 text-sm text-yellow-200"><span class="font-semibold">Remediation:</span> ${escapeHtml(e.remediation)}</div>`:""}\n ${e.evidence?`\n <div class="mt-3">\n <div class="text-xs text-cyan-300 font-semibold">Evidence</div>\n <pre class="mt-1 text-xs text-green-300 bg-slate-900/80 rounded p-3 overflow-x-auto whitespace-pre-wrap">${escapeHtml(e.evidence)}</pre>\n </div>\n `:""}\n ${e.cve_ids?.length||e.cwe_ids?.length?`\n <div class="mt-3 text-xs text-gray-300 flex flex-wrap gap-2">\n ${e.cve_ids?.length?`<span>CVEs: ${e.cve_ids.map(e=>`<span class="text-cyan-300">${escapeHtml(e)}</span>`).join(", ")}</span>`:""}\n ${e.cwe_ids?.length?`<span>CWEs: ${e.cwe_ids.map(e=>`<span class="text-cyan-300">${escapeHtml(e)}</span>`).join(", ")}</span>`:""}\n </div>\n `:""}\n ${e.references?.length?`\n <div class="mt-3 text-xs text-gray-300">\n <div class="text-cyan-300 font-semibold mb-1">References</div>\n <ul class="space-y-1">\n ${e.references.map(e=>`<li><a href="${escapeHtml(e)}" target="_blank" class="text-blue-400 hover:underline break-all">${escapeHtml(e)}</a></li>`).join("")}\n </ul>\n </div>\n `:""}\n ${e.tags?.length?`\n <div class="mt-3 text-xs text-gray-400">Tags: ${e.tags.map(e=>`<span class="px-2 py-0.5 bg-slate-700 rounded">${escapeHtml(e)}</span>`).join(" ")}</div>\n `:""}\n ${n?`\n <div class="mt-3">\n <div class="text-xs text-cyan-300 font-semibold">Details</div>\n <pre class="mt-1 text-xs text-gray-300 bg-slate-900/80 rounded p-3 overflow-x-auto whitespace-pre-wrap">${escapeHtml(n)}</pre>\n </div>\n `:""}\n </div>\n `}).join(""):'<p class="text-gray-400 text-sm py-2">No findings for this scan yet.</p>'}\n </div>\n `}function toggleAdvVulnScanLogs(e){advVulnExpandedLogIds.has(e)?advVulnExpandedLogIds.delete(e):(advVulnExpandedLogIds.add(e),fetchScanLogs(e)),updateActiveScans(advVulnScansCache,{preserveSelection:!0})}async function fetchScanLogs(e){try{const t=advVulnLogCache.get(e)||{entries:[],lastIndex:0},n=await fetch(`/api/vuln-advanced/scan/${e}/logs?since=${t.lastIndex}`),a=await n.json();if(a.success&&a.logs.length>0){t.entries=t.entries.concat(a.logs),t.lastIndex=a.total,advVulnLogCache.set(e,t);const n=document.getElementById(`scan-logs-${e}`);if(n){const e=n.scrollHeight-n.scrollTop-n.clientHeight<50;n.innerHTML=renderLogEntries(t.entries),e&&(n.scrollTop=n.scrollHeight)}}}catch(e){console.error("Error fetching scan logs:",e)}}function renderLogEntries(e){if(!e.length)return'<p class="text-gray-500 text-xs font-mono">Waiting for log entries...</p>';const t={info:"text-blue-400",warning:"text-yellow-400",error:"text-red-400",debug:"text-gray-500"};return e.map(e=>{const n=new Date(e.timestamp).toLocaleTimeString(),a=t[e.level]||"text-gray-300";return`<div class="text-xs font-mono py-0.5 leading-relaxed">\n <span class="text-gray-500">${escapeHtml(n)}</span>\n <span class="${a} uppercase font-semibold">[${escapeHtml(e.level)}]</span>\n <span class="text-gray-200">${escapeHtml(e.message)}</span>\n </div>`}).join("")}function renderInlineScanLogs(e,t=!1){return`\n <div class="border border-t-0 border-green-500/50 ${t?"":"rounded-b-lg"} bg-slate-950/80 p-3 max-h-[300px] overflow-y-auto scrollbar-thin"\n id="scan-logs-${e}">\n ${renderLogEntries((advVulnLogCache.get(e)||{entries:[]}).entries)}\n </div>\n `}async function deleteAdvScan(e){if(confirm("Delete this scan and its findings?"))try{const t=await fetch(`/api/vuln-advanced/scan/${e}`,{method:"DELETE"}),n=await t.json();n.success?(showNotification("Scan deleted","info"),await refreshAdvVulnData()):showNotification(n.error||"Failed to delete scan","error")}catch(e){console.error("Error deleting scan:",e),showNotification("Failed to delete scan","error")}}async function deleteAllAdvScans(){if(confirm("Delete ALL scans and findings? This cannot be undone."))try{const e=await fetch("/api/vuln-advanced/scans",{method:"DELETE"}),t=await e.json();t.success?(showNotification(t.message,"info"),await refreshAdvVulnData()):showNotification(t.error||"Failed to delete scans","error")}catch(e){console.error("Error deleting all scans:",e),showNotification("Failed to delete scans","error")}}function downloadZapReport(e="html"){window.open(`/api/zap/report?format=${e}`,"_blank")}function downloadScanReport(e){window.open(`/api/vuln-advanced/scan/${e}/report?format=html`,"_blank")}async function exportScanReport(){const e=document.getElementById("export-report-btn");e&&(e.disabled=!0,e.textContent="Generating...");try{const e=resolveNetworkAwareEndpoint("/api/report/export"),t=document.createElement("a");t.href=e,t.download="",document.body.appendChild(t),t.click(),document.body.removeChild(t),addConsoleMessage("Report download started","success")}catch(e){addConsoleMessage("Report export failed: "+e.message,"error")}finally{e&&(e.disabled=!1,e.innerHTML='<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-4-4m4 4l4-4m6 4H6"></path></svg>Export Report')}}async function cancelAdvScan(e){try{const t=await fetch(`/api/vuln-advanced/scan/${e}/cancel`,{method:"POST",headers:{"Content-Type":"application/json"}}),n=await t.json();n.success?(showNotification("Scan cancelled","info"),await refreshAdvVulnData()):showNotification(n.error||"Failed to cancel scan","error")}catch(e){console.error("Error cancelling scan:",e),showNotification("Failed to cancel scan","error")}}function showAdvVulnNotAvailable(){const e=document.getElementById("adv-vuln-not-available");e&&e.classList.remove("hidden")}function hideAdvVulnNotAvailable(){const e=document.getElementById("adv-vuln-not-available");e&&e.classList.add("hidden")}function updateScannerStatus(e,t){if(!e)return;advVulnScannersStatusCache=e,void 0!==t&&(advVulnNucleiTemplatesCache=t);["nuclei","nikto","sqlmap","nmap_vuln","whatweb","zap"].forEach(t=>{const n=document.getElementById(`scanner-${t.replace("_","-")}-status`),a=document.getElementById(`scanner-${t.replace("_","-")}`);if(n){const a=e[t];"nuclei"===t?updateNucleiCardStatus(n,a,advVulnNucleiTemplatesCache):"zap"===t?e.zap_running?(n.textContent="Running",n.classList.remove("text-gray-400","text-green-400"),n.classList.add("text-cyan-400")):a?(n.textContent="Installed",n.classList.remove("text-gray-400","text-cyan-400"),n.classList.add("text-green-400")):(n.textContent="Not installed",n.classList.remove("text-green-400","text-cyan-400"),n.classList.add("text-gray-400")):(n.textContent=a?"Available":"Not installed",n.classList.toggle("text-green-400",a),n.classList.toggle("text-gray-400",!a))}a&&a.classList.toggle("opacity-50",!e[t])}),updateZapControlPanel(e);const n=document.getElementById("ajax-spider-browser-warning");n&&(e.ajax_spider_browser&&"htmlunit"!==e.ajax_spider_browser?n.classList.add("hidden"):(n.classList.remove("hidden"),n.innerHTML='\n <div class="flex items-center gap-2 text-xs text-yellow-400 bg-yellow-900/20 border border-yellow-700/30 rounded px-3 py-2 mt-3">\n <svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"></path>\n </svg>\n <span>No browser detected for AJAX spider. Install Chrome/Chromium or Firefox for better JavaScript crawling during scans. On Raspberry Pi, Chromium is usually pre-installed.</span>\n </div>\n '))}let advVulnScannersStatusCache=null,advVulnNucleiTemplatesCache=null;function updateNucleiCardStatus(e,t,n){if(e.classList.remove("text-green-400","text-gray-400","text-yellow-400"),e.onclick=null,e.style.cursor="",e.title="",!t)return e.textContent="Not installed",void e.classList.add("text-gray-400");const a=n?.count||0;if(a>0){e.textContent=`${a.toLocaleString()} templates`,e.classList.add("text-green-400");const t=[];n.version&&t.push(`version ${n.version}`),n.updated_at&&t.push(`updated ${new Date(n.updated_at).toLocaleDateString()}`),e.title=t.length?`Nuclei templates: ${t.join(" · ")}`:"Nuclei templates installed"}else e.textContent="⚠ No templates — install",e.classList.add("text-yellow-400"),e.style.cursor="pointer",e.title="Click to download nuclei templates",e.onclick=()=>updateNucleiTemplates(!1)}async function updateNucleiTemplates(e=!1){try{const t=await fetch("/api/vuln-advanced/nuclei/templates/update",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({force:e})}),n=await t.json();n.success?(showNotification("Nuclei templates downloading in the background — this can take a minute","success"),setTimeout(refreshNucleiTemplateStatus,2e4)):showNotification(n.error||"Failed to start template update","error")}catch(e){console.error("Error updating nuclei templates:",e),showNotification("Failed to start template update","error")}}async function refreshNucleiTemplateStatus(){try{const e=await fetch("/api/vuln-advanced/nuclei/templates"),t=await e.json();t.success&&advVulnScannersStatusCache&&updateScannerStatus(advVulnScannersStatusCache,t.templates)}catch(e){}}function updateVulnSummary(e){e?.severity_counts&&(updateElement("vuln-total-findings",e.total_findings||0),updateElement("vuln-total-scans",e.total_scans||0))}function updateVulnStats(e,t){const n=new Set;if(e&&e.forEach(e=>n.add(e.target)),t&&t.forEach(e=>n.add(e.host)),updateElement("vuln-hosts-count",n.size),e?.length){const t=e.filter(e=>e.completed_at);if(t.length){const e=t.reduce((e,t)=>new Date(e.completed_at).getTime()>new Date(t.completed_at).getTime()?e:t),n=new Date(e.completed_at),a=new Date,s=Math.floor((a-n)/6e4);let o;o=s<1?"Just now":s<60?`${s}m ago`:s<1440?`${Math.floor(s/60)}h ago`:n.toLocaleDateString(),updateElement("vuln-last-scan-time",o)}}}let advVulnFindingsCache=[],advVulnShowAllScans=!1,advVulnExpandedScanIds=new Set,advVulnExpandedLogIds=new Set,advVulnLogCache=new Map,advVulnScansCache=[],advVulnScanFindingsMap=new Map;async function loadAdvVulnFindings(){try{const e=await fetch("/api/vuln-advanced/findings?limit=1000"),t=await e.json();if(!t.success)return;advVulnFindingsCache=t.findings||[]}catch(e){console.error("Error loading vuln findings:",e)}}async function quickRescanHost(e){const t=document.getElementById("adv-vuln-scanner"),n=t?t.value:"nmap_vuln";try{const t=await fetch("/api/vuln-advanced/scan",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({target:e,scan_type:n})}),a=await t.json();a.success?(showNotification(`Started ${n} rescan of ${e}`,"success"),startAdvVulnPolling()):showNotification(a.error||"Failed to start rescan","error")}catch(e){console.error("Error starting rescan:",e),showNotification("Failed to start rescan","error")}}function updateAdvVulnRecentFindings(e){const t=document.getElementById("adv-vuln-findings");if(!t)return;if(!e?.length)return void(t.innerHTML='<p class="text-gray-400 text-center py-4">No findings yet</p>');const n={critical:"border-red-600",high:"border-orange-500",medium:"border-yellow-500",low:"border-blue-500",info:"border-gray-500"},a={critical:"bg-red-900",high:"bg-orange-900",medium:"bg-yellow-900",low:"bg-blue-900",info:"bg-gray-800"};t.innerHTML=e.map((e,t)=>{let s="";if(e.timestamp){const t=new Date(e.timestamp),n=new Date,a=Math.floor((n-t)/6e4);s=a<1?"just now":a<60?`${a}m ago`:a<1440?`${Math.floor(a/60)}h ago`:`${Math.floor(a/1440)}d ago`}const o=getFindingScanId(e,advVulnScansCache.map(e=>e.scan_id));return`\n <div class="p-2 ${a[e.severity]||"bg-slate-800"} bg-opacity-50 rounded-lg border-l-4 ${n[e.severity]||"border-gray-500"} cursor-pointer hover:bg-opacity-70 transition-colors"\n onclick="${o?`toggleAdvVulnScanFindings('${o}')`:""}"\n <div class="flex items-start justify-between gap-2">\n <div class="flex-1 min-w-0">\n <div class="font-medium text-sm truncate">${escapeHtml(e.title)}</div>\n <div class="text-xs text-gray-400">${escapeHtml(e.host)} | ${escapeHtml(e.scanner)}</div>\n </div>\n ${s?`<span class="text-xs text-gray-500 shrink-0">${s}</span>`:""}\n </div>\n ${e.cve_ids?.length?`\n <div class="mt-1 flex flex-wrap gap-1">\n ${e.cve_ids.slice(0,3).map(e=>`<span class="text-xs text-cyan-400">${e}</span>`).join("")}\n ${e.cve_ids.length>3?`<span class="text-xs text-gray-500">+${e.cve_ids.length-3}</span>`:""}\n </div>\n `:""}\n </div>\n `}).join("")}function formatDuration(e){if(!e||e<1)return"<1s";if(e<60)return`${Math.round(e)}s`;if(e<3600){const t=Math.floor(e/60),n=Math.round(e%60);return n>0?`${t}m ${n}s`:`${t}m`}const t=Math.floor(e/3600),n=Math.floor(e%3600/60);return n>0?`${t}h ${n}m`:`${t}h`}async function copyToClipboard(e){try{await navigator.clipboard.writeText(e),showNotification("Copied to clipboard","success")}catch(t){const n=document.createElement("textarea");n.value=e,document.body.appendChild(n),n.select(),document.execCommand("copy"),document.body.removeChild(n),showNotification("Copied to clipboard","success")}}let advVulnScanMode="web";function toggleScanMode(e){advVulnScanMode=e;const t=document.getElementById("scan-mode-web"),n=document.getElementById("scan-mode-api"),a=document.getElementById("api-scan-fields");"api"===e?(t?.classList.remove("bg-blue-600","text-white","font-medium"),t?.classList.add("text-gray-400"),n?.classList.add("bg-blue-600","text-white","font-medium"),n?.classList.remove("text-gray-400"),a?.classList.remove("hidden")):(n?.classList.remove("bg-blue-600","text-white","font-medium"),n?.classList.add("text-gray-400"),t?.classList.add("bg-blue-600","text-white","font-medium"),t?.classList.remove("text-gray-400"),a?.classList.add("hidden"))}function setScanStrength(e){document.getElementById("zap-scan-strength").value=e;document.getElementById("strength-description").textContent={standard:"Balanced speed and coverage. Suitable for most scans.",thorough:"Extended coverage with custom fuzzing. 2-3x longer scan time.",insane:"Maximum coverage with aggressive fuzzing. 5-10x longer. May cause target instability."}[e]||"",["standard","thorough","insane"].forEach(t=>{const n=document.getElementById("strength-"+t);t===e?(n?.classList.add("bg-blue-600","text-white","font-medium"),n?.classList.remove("text-gray-400")):(n?.classList.remove("bg-blue-600","text-white","font-medium"),n?.classList.add("text-gray-400"))})}function toggleRequestBodyField(){const e=document.getElementById("api-http-method")?.value,t=document.getElementById("api-request-body-container");if(!t)return;["POST","PUT","PATCH","DELETE"].includes(e)?t.classList.remove("hidden"):t.classList.add("hidden")}async function startAdvancedScan(){const e=document.getElementById("adv-vuln-target"),t=document.getElementById("adv-vuln-scanner");if(!e||!t)return;const n=e.value.trim(),a=t.value;if(!n)return void showNotification("Please enter a target IP or URL","warning");const s=document.getElementById("zap-scan-strength"),o=s?s.value:"standard",r=document.getElementById("zap-auth-type")?.value,i={scan_strength:o};if(r){const e=getAuthParams(r);if(null===e)return;i.auth_type=r,i.auth_params=e}if("api"===advVulnScanMode){i.scan_mode="api",i.http_method=document.getElementById("api-http-method")?.value||"GET";const e=document.getElementById("api-custom-headers")?.value.trim();e&&(i.custom_headers=e);const t=document.getElementById("api-request-body")?.value.trim();t&&(i.request_body=t);const n=document.getElementById("zap-openapi-url")?.value.trim();n&&(i.openapi_url=n)}try{const t=await fetch("/api/vuln-advanced/scan",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({target:n,scan_type:a,options:i})}),s=await t.json();s.success?(showNotification(`Started ${a} scan: ${s.scan_id}`,"success"),e.value="",startAdvVulnPolling()):showNotification(s.error||"Failed to start scan","error")}catch(e){console.error("Error starting advanced scan:",e),showNotification("Failed to start scan","error")}}function getAuthParams(e){const t=document.getElementById("zap-login-url")?.value.trim(),n=document.getElementById("zap-username")?.value.trim(),a=document.getElementById("zap-password")?.value.trim(),s=document.getElementById("zap-login-data")?.value.trim(),o=document.getElementById("zap-bearer-token")?.value.trim(),r=document.getElementById("zap-api-key")?.value.trim(),i=document.getElementById("zap-api-key-header")?.value.trim()||"X-API-Key",l=document.getElementById("zap-cookie-value")?.value.trim(),c=document.getElementById("zap-wait-for-url")?.value.trim(),d=document.getElementById("zap-login-page-wait")?.value||"5",u=document.getElementById("zap-script-name")?.value.trim(),p={};if("form"===e){if(!n||!a)return showNotification("Please enter username and password","warning"),null;if(!t)return showNotification("Login URL is required for form authentication","warning"),null;p.username=n,p.password=a,p.login_url=t,p.login_request_data=s||"username={%username%}&password={%password%}"}else if("http_basic"===e){if(!n||!a)return showNotification("Please enter username and password","warning"),null;p.username=n,p.password=a,p.http_basic_auth=`${n}:${a}`}else if("oauth2_bba"===e){if(!n||!a)return showNotification("Please enter username and password for OAuth2 login","warning"),null;if(!t)return showNotification("Login URL is required for OAuth2/BBA authentication","warning"),null;p.username=n,p.password=a,p.login_url=t,p.wait_for_url=c||"",p.login_page_wait=parseInt(d)||5}else if("script_auth"===e){if(!n||!a)return showNotification("Please enter username and password for script authentication","warning"),null;if(!t)return showNotification("Login URL is required for script-based authentication","warning"),null;p.username=n,p.password=a,p.login_url=t,p.script_name=u||""}else if("oauth2_client_creds"===e){const e=document.getElementById("zap-oauth2-client-id")?.value.trim(),t=document.getElementById("zap-oauth2-client-secret")?.value.trim(),n=document.getElementById("zap-oauth2-token-url")?.value.trim(),a=document.getElementById("zap-oauth2-scope")?.value.trim();if(!e||!t)return showNotification("Please enter Client ID and Client Secret","warning"),null;if(!n)return showNotification("Token URL is required for OAuth2 Client Credentials","warning"),null;p.client_id=e,p.client_secret=t,p.token_url=n,a&&(p.scope=a)}else if("bearer_token"===e){if(!o)return showNotification("Please enter a bearer token","warning"),null;p.bearer_token=o}else if("api_key"===e){if(!r)return showNotification("Please enter an API key","warning"),null;p.api_key=r,p.api_key_header=i}else if("cookie"===e){if(!l)return showNotification("Please enter a cookie string","warning"),null;p.cookie_value=l}return p}let reconScanId=null,reconPollInterval=null;function setReconState(e){const t=["config","running","handoff","error"];for(const n of t){const t=document.getElementById(`recon-state-${n}`);t&&t.classList.toggle("hidden",n!==e)}}function showReconError(e){const t=document.getElementById("recon-state-error");t&&(t.textContent=e,t.classList.remove("hidden"))}function resetReconCard(){reconPollInterval&&(clearInterval(reconPollInterval),reconPollInterval=null),reconScanId=null;const e=document.getElementById("recon-state-error");e&&e.classList.add("hidden"),setReconState("config")}async function startReconScan(){const e=document.getElementById("adv-vuln-target"),t=(e?.value||"").trim();if(!t)return void showNotification("Enter a target URL first","error");const n=[];if(document.getElementById("recon-tls")?.checked&&n.push("tls_audit"),document.getElementById("recon-dns")?.checked&&n.push("dns_passive"),document.getElementById("recon-content")?.checked&&n.push("content_discovery"),n.length)try{const e=await fetch("/api/recon/scan",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({target:t,recon_types:n})}),a=await e.json();if(!a.success)return void showReconError(a.error||"Failed to start recon");reconScanId=a.scan_id,setReconState("running"),renderReconProgress({results:{}}),startReconPolling()}catch(e){console.error("Recon start failed:",e),showReconError(String(e))}else showNotification("Select at least one recon type","error")}function startReconPolling(){reconPollInterval&&clearInterval(reconPollInterval),reconPollInterval=setInterval(pollReconScan,1500)}async function pollReconScan(){if(reconScanId)try{const e=await fetch(`/api/recon/scan/${reconScanId}`),t=await e.json();if(!t.success)return;const n=t.scan;renderReconProgress(n),"completed"!==n.status&&"cancelled"!==n.status||(clearInterval(reconPollInterval),reconPollInterval=null,"completed"===n.status?await loadReconHandoffOptions():resetReconCard())}catch(e){console.error("Recon poll failed:",e)}}function renderReconProgress(e){const t=document.getElementById("recon-progress");if(!t)return;const n=e.results||{},a=Object.entries({tls_audit:"TLS audit",dns_passive:"DNS subdomain enum",content_discovery:"Content discovery"}).map(([e,t])=>{const a=n[e];let s;return s=a?"ok"===a.status?`<span class="text-emerald-400">ok (${a.findings.length} findings)</span>`:"error"===a.status?`<span class="text-red-400" title="${escapeHtml(a.error_message)}">error</span>`:`<span class="text-yellow-400">${a.status}</span>`:'<span class="text-gray-500">pending</span>',`<div class="flex items-center justify-between"><span class="text-gray-300">${t}</span>${s}</div>`}).join("");t.innerHTML=a}async function cancelReconScan(){if(reconScanId){try{await fetch(`/api/recon/scan/${reconScanId}/cancel`,{method:"POST"})}catch(e){console.error("Recon cancel failed:",e)}resetReconCard()}}async function loadReconHandoffOptions(){if(reconScanId)try{const e=await fetch(`/api/recon/scan/${reconScanId}/handoff-options`),t=await e.json();if(!t.success)return void showReconError(t.error||"Failed to load handoff options");renderReconHandoff(t),setReconState("handoff")}catch(e){console.error("Handoff options failed:",e),showReconError(String(e))}}function renderReconHandoff(e){const t=document.getElementById("recon-subdomains-list"),n=document.getElementById("recon-paths-list");t&&(e.subdomains?.length?t.innerHTML='<p class="text-xs text-gray-400 mb-1">Subdomains:</p>'+e.subdomains.map(e=>`\n <label class="flex items-center gap-2 px-2 py-1 hover:bg-slate-700/50 rounded text-xs cursor-pointer">\n <input type="checkbox" class="recon-subdomain-cb" value="${escapeHtml(e.name)}" ${e.alive?"checked":""}>\n <span class="text-gray-200 flex-1">${escapeHtml(e.name)}</span>\n <span class="${e.alive?"text-emerald-400":"text-gray-500"}">${e.alive?"alive":"dead"}</span>\n <span class="text-gray-500">${e.a_records?.length?"A":""}${e.aaaa_records?.length?" AAAA":""}</span>\n </label>\n `).join(""):t.innerHTML='<p class="text-xs text-gray-500">No additional subdomains discovered.</p>'),n&&(e.paths?.length?n.innerHTML='<p class="text-xs text-gray-400 mb-1 mt-2">Interesting paths:</p>'+e.paths.map(e=>`\n <label class="flex items-center gap-2 px-2 py-1 hover:bg-slate-700/50 rounded text-xs cursor-pointer">\n <input type="checkbox" class="recon-path-cb" value="${escapeHtml(e.path)}" checked>\n <span class="text-gray-200 flex-1">/${escapeHtml(e.path)}</span>\n <span class="text-gray-500">HTTP ${e.status}</span>\n <span class="text-gray-500">${e.length||0}B</span>\n </label>\n `).join(""):n.innerHTML='<p class="text-xs text-gray-500 mt-2">No additional paths discovered.</p>')}async function handoffReconToZap(e){if(!reconScanId)return;const t=Array.from(document.querySelectorAll(".recon-subdomain-cb:checked")).map(e=>e.value),n=Array.from(document.querySelectorAll(".recon-path-cb:checked")).map(e=>e.value),a=document.getElementById("adv-vuln-scanner"),s=a?a.value:"zap_full";try{const a=await fetch(`/api/recon/scan/${reconScanId}/handoff`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({subdomains:t,paths:n,scan_type:s,force:!!e})}),o=await a.json();if(!o.success){if(o.out_of_scope_subdomains?.length){const e=document.getElementById("recon-scope-warning");return void(e&&(e.classList.remove("hidden"),e.innerHTML=`Out of scope: ${o.out_of_scope_subdomains.map(escapeHtml).join(", ")}. <button onclick="handoffReconToZap(true)" class="underline text-red-200">Override</button>`))}return void showReconError(o.error||"Handoff failed")}showNotification(`Handed off ${o.zap_scans.length} ZAP scan(s)`,"success"),resetReconCard(),"function"==typeof startAdvVulnPolling&&startAdvVulnPolling()}catch(e){console.error("Handoff failed:",e),showReconError(String(e))}}function startAdvVulnPolling(){advVulnRefreshInterval&&clearInterval(advVulnRefreshInterval),advVulnRefreshInterval=setInterval(async()=>{if("adv-vuln"!==currentTab)return clearInterval(advVulnRefreshInterval),void(advVulnRefreshInterval=null);await refreshAdvVulnData();for(const e of advVulnExpandedLogIds)fetchScanLogs(e);(advVulnScansCache||[]).some(e=>"running"===e.status)||(clearInterval(advVulnRefreshInterval),advVulnRefreshInterval=null)},2e3)}async function refreshAdvVulnData(){await loadAdvancedVulnData()}function updateZapControlPanel(e){const t=document.getElementById("zap-control-panel"),n=document.getElementById("zap-daemon-status"),a=document.getElementById("zap-start-btn"),s=document.getElementById("zap-stop-btn");if(t){if(!e.zap)return t.classList.add("opacity-50"),void(n&&(n.textContent="Not Installed",n.className="text-xs px-2 py-1 rounded-full bg-gray-700 text-gray-400"));t.classList.remove("opacity-50"),e.zap_running?(n&&(n.textContent="Running",n.className="text-xs px-2 py-1 rounded-full bg-green-900 text-green-400"),a&&(a.disabled=!0),s&&(s.disabled=!1)):(n&&(n.textContent="Stopped",n.className="text-xs px-2 py-1 rounded-full bg-slate-700 text-gray-400"),a&&(a.disabled=!1),s&&(s.disabled=!0)),e.zap_running&&fetchZapStatus()}}async function fetchZapStatus(){try{const e=await fetch("/api/zap/status"),t=await e.json();t.success&&t.status&&(updateElement("zap-hosts-count",t.status.hosts_accessed||0),updateElement("zap-alerts-count",t.status.alerts_count||0),t.status.running&&zapCheckAuthStatus())}catch(e){console.error("Error fetching ZAP status:",e)}}async function startZapDaemon(){try{showNotification("Starting ZAP daemon...","info");const e=await fetch("/api/zap/start",{method:"POST",headers:{"Content-Type":"application/json"}}),t=await e.json();t.success?(showNotification("ZAP daemon started successfully","success"),await refreshAdvVulnData()):showNotification(t.error||"Failed to start ZAP daemon","error")}catch(e){console.error("Error starting ZAP daemon:",e),showNotification("Failed to start ZAP daemon","error")}}async function stopZapDaemon(){try{showNotification("Stopping ZAP daemon...","info");const e=await fetch("/api/zap/stop",{method:"POST",headers:{"Content-Type":"application/json"}}),t=await e.json();t.success?(showNotification("ZAP daemon stopped","success"),await refreshAdvVulnData()):showNotification(t.error||"Failed to stop ZAP daemon","error")}catch(e){console.error("Error stopping ZAP daemon:",e),showNotification("Failed to stop ZAP daemon","error")}}async function zapClearSession(){try{const e=await fetch("/api/zap/clear-session",{method:"POST",headers:{"Content-Type":"application/json"}}),t=await e.json();t.success?(showNotification("ZAP session cleared","success"),await fetchZapStatus()):showNotification(t.error||"Failed to clear ZAP session","error")}catch(e){console.error("Error clearing ZAP session:",e),showNotification("Failed to clear ZAP session","error")}}async function zapImportOpenAPI(){const e=document.getElementById("zap-openapi-url");if(!e)return;const t=e.value.trim();if(!t)return void showNotification("Please enter an OpenAPI/Swagger URL","warning");const n=document.getElementById("adv-vuln-target"),a=n?n.value.trim():"";try{const n={spec_url:t};a&&(n.target_url=a);const s=await fetch("/api/zap/import-openapi",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)}),o=await s.json();o.success?(showNotification("OpenAPI spec imported successfully","success"),e.value=""):showNotification(o.error||"Failed to import OpenAPI spec","error")}catch(e){console.error("Error importing OpenAPI spec:",e),showNotification("Failed to import OpenAPI spec","error")}}async function zapSetAuthentication(){return!0}async function zapCheckAuthStatus(){const e=document.getElementById("zap-auth-status-banner"),t=document.getElementById("zap-clear-auth-btn");e&&e.classList.add("hidden"),t&&t.classList.add("hidden")}async function zapClearAuthentication(){showNotification("Auth is now per-scan - no persistent auth to clear","info")}let _credentialCheckCache={},_credentialCheckTimeout=null;function checkTargetCredentials(e){!e||e.length<3?hideCredentialBadge():(_credentialCheckTimeout&&clearTimeout(_credentialCheckTimeout),_credentialCheckTimeout=setTimeout(async()=>{await _doCredentialCheck(e)},500))}async function _doCredentialCheck(e){const t=e.toLowerCase().replace(/^https?:\/\//,"").split("/")[0];if(void 0===_credentialCheckCache[t])try{const n=await fetch(`/api/zap/credentials/check?target=${encodeURIComponent(e)}`),a=await n.json();_credentialCheckCache[t]=a,updateCredentialBadge(a)}catch(e){console.error("Error checking target credentials:",e),hideCredentialBadge()}else updateCredentialBadge(_credentialCheckCache[t])}function updateCredentialBadge(e){const t=document.getElementById("target-cred-badge"),n=document.getElementById("target-cred-info"),a=document.getElementById("target-cred-text");if(t&&n)if(e.exists){t.classList.remove("hidden"),n.classList.remove("hidden");const s="form"===e.auth_type?"Form-based":"http_basic"===e.auth_type?"HTTP Basic":e.auth_type,o=e.username||"(no username)";a&&(a.textContent=`Auth saved: ${s} login as "${o}"`)}else hideCredentialBadge()}function hideCredentialBadge(){const e=document.getElementById("target-cred-badge"),t=document.getElementById("target-cred-info");e&&e.classList.add("hidden"),t&&t.classList.add("hidden")}function showCredentialsModal(){const e=document.getElementById("zap-credentials-modal");if(e){e.classList.remove("hidden"),e.classList.add("flex");const t=document.getElementById("adv-vuln-target"),n=document.getElementById("cred-target-host");t&&n&&t.value&&(n.value=t.value.replace(/^https?:\/\//,"").split("/")[0]),loadSavedCredentialsList()}}function closeCredentialsModal(){const e=document.getElementById("zap-credentials-modal");e&&(e.classList.add("hidden"),e.classList.remove("flex"))}function toggleCredentialFields(){const e=document.getElementById("cred-auth-type"),t=document.getElementById("cred-login-url-container"),n=document.getElementById("cred-realm-container"),a=document.getElementById("cred-username-container"),s=document.getElementById("cred-password-container"),o=document.getElementById("cred-login-data-container"),r=document.getElementById("cred-bearer-token-container"),i=document.getElementById("cred-api-key-container"),l=document.getElementById("cred-api-key-header-container"),c=document.getElementById("cred-cookie-container");if(!e)return;const d=e.value;t&&t.classList.add("hidden"),n&&n.classList.add("hidden"),a&&a.classList.add("hidden"),s&&s.classList.add("hidden"),o&&o.classList.add("hidden"),r&&r.classList.add("hidden"),i&&i.classList.add("hidden"),l&&l.classList.add("hidden"),c&&c.classList.add("hidden"),"form"===d?(t&&t.classList.remove("hidden"),a&&a.classList.remove("hidden"),s&&s.classList.remove("hidden"),o&&o.classList.remove("hidden")):"http_basic"===d?(n&&n.classList.remove("hidden"),a&&a.classList.remove("hidden"),s&&s.classList.remove("hidden")):"oauth2_bba"===d||"script_auth"===d?(t&&t.classList.remove("hidden"),a&&a.classList.remove("hidden"),s&&s.classList.remove("hidden")):"bearer_token"===d?r&&r.classList.remove("hidden"):"api_key"===d?(i&&i.classList.remove("hidden"),l&&l.classList.remove("hidden")):"cookie"===d&&c&&c.classList.remove("hidden")}function toggleScanAuthFields(){const e=document.getElementById("zap-auth-type"),t=document.getElementById("zap-auth-fields-wrapper"),n=document.getElementById("zap-login-url-container"),a=document.getElementById("zap-username-container"),s=document.getElementById("zap-password-container"),o=document.getElementById("zap-login-data-container"),r=document.getElementById("zap-bearer-token-container"),i=document.getElementById("zap-api-key-container"),l=document.getElementById("zap-api-key-header-container"),c=document.getElementById("zap-cookie-container"),d=document.getElementById("zap-oauth2-bba-container"),u=document.getElementById("zap-oauth2-cc-container"),p=document.getElementById("zap-script-auth-container");if(!e)return;const g=e.value;n&&n.classList.add("hidden"),a&&a.classList.add("hidden"),s&&s.classList.add("hidden"),o&&o.classList.add("hidden"),r&&r.classList.add("hidden"),i&&i.classList.add("hidden"),l&&l.classList.add("hidden"),c&&c.classList.add("hidden"),d&&d.classList.add("hidden"),u&&u.classList.add("hidden"),p&&p.classList.add("hidden"),g?(t&&t.classList.remove("hidden"),"form"===g?(n&&n.classList.remove("hidden"),a&&a.classList.remove("hidden"),s&&s.classList.remove("hidden"),o&&o.classList.remove("hidden")):"http_basic"===g?(a&&a.classList.remove("hidden"),s&&s.classList.remove("hidden")):"oauth2_bba"===g?(n&&n.classList.remove("hidden"),a&&a.classList.remove("hidden"),s&&s.classList.remove("hidden"),d&&d.classList.remove("hidden")):"oauth2_client_creds"===g?u&&u.classList.remove("hidden"):"script_auth"===g?(n&&n.classList.remove("hidden"),a&&a.classList.remove("hidden"),s&&s.classList.remove("hidden"),p&&p.classList.remove("hidden")):"bearer_token"===g?r&&r.classList.remove("hidden"):"api_key"===g?(i&&i.classList.remove("hidden"),l&&l.classList.remove("hidden")):"cookie"===g&&c&&c.classList.remove("hidden")):t&&t.classList.add("hidden")}async function saveTargetCredentials(){const e=document.getElementById("cred-target-host")?.value?.trim(),t=document.getElementById("cred-auth-type")?.value,n=document.getElementById("cred-login-url")?.value?.trim(),a=document.getElementById("cred-username")?.value?.trim(),s=document.getElementById("cred-password")?.value,o=document.getElementById("cred-login-data")?.value?.trim(),r=document.getElementById("cred-http-realm")?.value?.trim(),i=document.getElementById("cred-notes")?.value?.trim(),l=document.getElementById("cred-bearer-token")?.value?.trim(),c=document.getElementById("cred-api-key")?.value?.trim(),d=document.getElementById("cred-api-key-header")?.value?.trim()||"X-API-Key",u=document.getElementById("cred-cookie-value")?.value?.trim();if(e){if("form"===t||"http_basic"===t||"oauth2_bba"===t||"script_auth"===t){if(!a||!s)return void showNotification("Username and password are required","warning");if(("oauth2_bba"===t||"script_auth"===t)&&!n)return void showNotification("Login URL is required for OAuth2/BBA or Script-based authentication","warning")}else if("bearer_token"===t){if(!l)return void showNotification("Bearer token is required","warning")}else if("api_key"===t){if(!c)return void showNotification("API key is required","warning")}else if("cookie"===t&&!u)return void showNotification("Cookie string is required","warning");try{const p=await fetch("/api/zap/credentials",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({target_host:e,auth_type:t,login_url:n,username:a,password:s,login_request_data:o,http_realm:r,notes:i,bearer_token:l,api_key:c,api_key_header:d,cookie_value:u})}),g=await p.json();if(g.success){showNotification(g.message||"Credentials saved successfully","success"),clearCredentialForm(),loadSavedCredentialsList(),_credentialCheckCache={};const e=document.getElementById("adv-vuln-target");e&&e.value&&checkTargetCredentials(e.value)}else showNotification(g.error||"Failed to save credentials","error")}catch(e){console.error("Error saving credentials:",e),showNotification("Failed to save credentials","error")}}else showNotification("Target host is required","warning")}function clearCredentialForm(){["cred-target-host","cred-login-url","cred-username","cred-password","cred-login-data","cred-http-realm","cred-notes","cred-bearer-token","cred-api-key","cred-cookie-value"].forEach(e=>{const t=document.getElementById(e);t&&(t.value="")});const e=document.getElementById("cred-api-key-header");e&&(e.value="X-API-Key");const t=document.getElementById("cred-auth-type");t&&(t.value="form"),toggleCredentialFields()}async function loadSavedCredentialsList(){const e=document.getElementById("saved-credentials-list");if(e){e.innerHTML='<div class="text-gray-400 text-sm text-center py-4">Loading...</div>';try{const t=await fetch("/api/zap/credentials"),n=await t.json();if(n.success&&n.credentials&&n.credentials.length>0){let t="";for(const e of n.credentials){let n;switch(e.auth_type){case"form":n='<span class="px-2 py-0.5 bg-blue-600 text-xs rounded">Form</span>';break;case"http_basic":n='<span class="px-2 py-0.5 bg-purple-600 text-xs rounded">HTTP Basic</span>';break;case"bearer_token":n='<span class="px-2 py-0.5 bg-green-600 text-xs rounded">Bearer Token</span>';break;case"api_key":n='<span class="px-2 py-0.5 bg-yellow-600 text-xs rounded">API Key</span>';break;case"cookie":n='<span class="px-2 py-0.5 bg-orange-600 text-xs rounded">Cookie</span>';break;default:n='<span class="px-2 py-0.5 bg-gray-600 text-xs rounded">None</span>'}t+=`\n <div class="flex items-center justify-between p-3 bg-slate-800 rounded-lg">\n <div class="flex-1">\n <div class="flex items-center gap-2">\n <span class="font-mono text-sm text-white">${escapeHtml(e.target_host)}</span>\n ${n}\n </div>\n <div class="text-xs text-gray-400 mt-1">\n ${e.username?`User: ${escapeHtml(e.username)}`:"bearer_token"===e.auth_type?"Token configured":"api_key"===e.auth_type?`Header: ${escapeHtml(e.api_key_header||"X-API-Key")}`:"cookie"===e.auth_type?"Cookie configured":"No username"}\n ${e.notes?` | ${escapeHtml(e.notes)}`:""}\n </div>\n </div>\n <div class="flex gap-2">\n <button onclick="editTargetCredential('${escapeHtml(e.target_host)}')"\n class="text-blue-400 hover:text-blue-300 text-sm" title="Edit">\n <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>\n </svg>\n </button>\n <button onclick="deleteTargetCredential('${escapeHtml(e.target_host)}')"\n class="text-red-400 hover:text-red-300 text-sm" title="Delete">\n <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>\n </svg>\n </button>\n </div>\n </div>\n `}e.innerHTML=t}else e.innerHTML='<div class="text-gray-400 text-sm text-center py-4">No saved credentials. Add credentials above to use authenticated ZAP scans.</div>'}catch(t){console.error("Error loading credentials:",t),e.innerHTML='<div class="text-red-400 text-sm text-center py-4">Failed to load credentials</div>'}}}async function editTargetCredential(e){try{const t=await fetch(`/api/zap/credentials/${encodeURIComponent(e)}`),n=await t.json();if(n.success&&n.credentials){const e=n.credentials;document.getElementById("cred-target-host").value=e.target_host||"",document.getElementById("cred-auth-type").value=e.auth_type||"form",document.getElementById("cred-login-url").value=e.login_url||"",document.getElementById("cred-username").value=e.username||"",document.getElementById("cred-password").value=e.password||"",document.getElementById("cred-login-data").value=e.login_request_data||"",document.getElementById("cred-http-realm").value=e.http_realm||"",document.getElementById("cred-notes").value=e.notes||"",document.getElementById("cred-bearer-token").value=e.bearer_token||"",document.getElementById("cred-api-key").value=e.api_key||"",document.getElementById("cred-api-key-header").value=e.api_key_header||"X-API-Key",document.getElementById("cred-cookie-value").value=e.cookie_value||"",toggleCredentialFields(),document.getElementById("cred-target-host")?.scrollIntoView({behavior:"smooth",block:"center"}),showNotification("Loaded credentials for editing","info")}else showNotification("Failed to load credentials","error")}catch(e){console.error("Error loading credential for edit:",e),showNotification("Failed to load credentials","error")}}async function deleteTargetCredential(e){if(confirm(`Delete saved credentials for "${e}"?`))try{const t=await fetch(`/api/zap/credentials/${encodeURIComponent(e)}`,{method:"DELETE"}),n=await t.json();if(n.success){showNotification(n.message||"Credentials deleted","success"),loadSavedCredentialsList(),_credentialCheckCache={};const e=document.getElementById("adv-vuln-target");e&&e.value&&checkTargetCredentials(e.value)}else showNotification(n.error||"Failed to delete credentials","error")}catch(e){console.error("Error deleting credentials:",e),showNotification("Failed to delete credentials","error")}}let _mapSimulation=null,_mapInitialized=!1,_mapAiAvailable=!1,_mapAiEnabled=!1,_mapNetworkHash=null,_mapAiCacheValid=!1;function riskColor(e){return 0===e?"#64748b":e<=3?"#22c55e":e<=8?"#f59e0b":"#ef4444"}function _buildMapLegend(e,t){const n=document.getElementById("map-legend");if(!n)return;n.innerHTML="";["router","access_point","extender","switch","ragnar","laptop","workstation","server","nas","sbc","phone","tablet","wearable","printer","camera","smart_tv","speaker","doorbell","thermostat","appliance","iot","media","gaming","vehicle","apple","unknown"].forEach(a=>{const s=e[a],o=t[a];if(!s||!o)return;const r=document.createElement("span");r.className="flex items-center gap-1",r.innerHTML=`<span class="inline-block w-3 h-3 rounded-full" style="background:${s}"></span> ${escapeHtml(o)}`,n.appendChild(r)})}async function _checkMapAiStatus(){try{const e=await networkAwareFetch("/api/ai/status"),t=await e.json();_mapAiAvailable=!!(t.enabled||t.config_enabled&&t.configured);const n=document.getElementById("map-ai-toggle-track");n&&(_mapAiAvailable?n.classList.remove("disabled"):(n.classList.add("disabled"),n.classList.remove("active"),_mapAiEnabled=!1));const a=document.getElementById("map-ai-toggle-wrapper");a&&(a.title=_mapAiAvailable?"Use GPT-5.4 Nano to improve device classification":"AI not available – enable in Settings")}catch(e){console.warn("AI status check failed:",e),_mapAiAvailable=!1}}function onMapAiToggleClick(){if(!_mapAiAvailable)return;const e=document.getElementById("map-ai-toggle-track");e&&(_mapAiEnabled=!_mapAiEnabled,e.classList.toggle("active",_mapAiEnabled),_mapAiEnabled&&(_mapAiCacheValid=!1,refreshNetworkMap()))}function _showMapAiSpinner(e){const t=document.getElementById("map-ai-spinner");t&&t.classList.toggle("hidden",!e)}async function loadNetworkMap(){if("undefined"==typeof d3)return void(document.getElementById("network-map-loading").innerHTML='<p class="text-red-400">D3.js failed to load. Check your internet connection.</p>');_mapInitialized||(_mapInitialized=!0,await _checkMapAiStatus(),loadScanSubnets()),document.getElementById("network-map-loading").style.display="flex",document.getElementById("network-map-svg").style.display="none";const e=_mapAiEnabled&&_mapAiAvailable,t=e&&!_mapAiCacheValid?"1":"0";e&&_showMapAiSpinner(!0);try{const e=await networkAwareFetch(`/api/network/topology?use_ai=${t}`),n=await e.json();if(n.error)throw new Error(n.error);n.network_hash&&(_mapNetworkHash&&_mapNetworkHash!==n.network_hash&&(_mapAiCacheValid=!1),n.ai_used&&(_mapAiCacheValid=!0),_mapNetworkHash=n.network_hash),_buildMapLegend(n.device_colors||{},n.device_labels||{});const a=n.nodes.map(e=>({id:e.ip,ip:e.ip,label:e.hostname||e.ip,mac:e.mac,vendor:e.vendor,type:e.type,type_label:e.type_label,confidence:e.confidence,ports:e.ports||[],status:e.status,risk:e.risk||0,last_seen:e.last_seen,is_gateway:e.is_gateway,is_ragnar:e.is_ragnar}));renderNetworkMap(a,n.links.map(e=>({source:e.source,target:e.target,type:e.type})),n.device_colors||{},n.device_icons||{})}catch(e){document.getElementById("network-map-loading").innerHTML=`<p class="text-red-400">Error loading map: ${escapeHtml(e.message)}</p>`}finally{_showMapAiSpinner(!1)}}function renderNetworkMap(e,t,n,a){const s=document.getElementById("network-map-container"),o=document.getElementById("network-map-svg"),r=document.getElementById("network-map-loading"),i=document.getElementById("map-tooltip");if(!s||!o)return;const l=s.clientWidth||800,c=s.clientHeight||600;_mapSimulation&&_mapSimulation.stop(),d3.select(o).selectAll("*").remove(),o.setAttribute("viewBox",`0 0 ${l} ${c}`);const d=d3.select(o),u=d.append("g");d.call(d3.zoom().scaleExtent([.2,4]).on("zoom",e=>u.attr("transform",e.transform)));const p=u.append("g").selectAll("line").data(t).join("line").attr("stroke",function(e){return"ap_uplink"===e.type?"#8b5cf6":"subnet_inferred"===e.type?"#06b6d4":"ap_inferred"===e.type?"#6366f1":"#334155"}).attr("stroke-width",e=>"ap_uplink"===e.type?2:1.5).attr("stroke-opacity",.6).attr("stroke-dasharray",function(e){return"ap_inferred"===e.type?"4,3":"subnet_inferred"===e.type?"6,3":null});function g(e){return e.is_gateway?22:e.is_ragnar?20:"access_point"===e.type?16:"server"===e.type?15:13}function m(e){return n[e.type]||n.unknown||"#64748b"}const f=u.append("g").selectAll("g").data(e).join("g").attr("cursor","pointer").call(d3.drag().on("start",(e,t)=>{e.active||_mapSimulation.alphaTarget(.3).restart(),t.fx=t.x,t.fy=t.y}).on("drag",(e,t)=>{t.fx=e.x,t.fy=e.y}).on("end",(e,t)=>{e.active||_mapSimulation.alphaTarget(0),t.fx=null,t.fy=null})).on("click",(e,t)=>{t.ip&&openHostPanel(t.ip)}).on("mouseover",(e,t)=>{i.classList.remove("hidden");const n=t.risk>8?"text-red-400":t.risk>3?"text-amber-400":t.risk>0?"text-green-400":"text-gray-400";i.innerHTML=`<div class="font-semibold font-mono mb-1">${escapeHtml(t.ip)}</div>\n ${t.label!==t.ip?`<div class="text-gray-400 text-xs mb-1">${escapeHtml(t.label)}</div>`:""}\n ${t.vendor?`<div class="text-xs text-gray-500">${escapeHtml(t.vendor)}</div>`:""}\n <div class="text-xs mt-1"><span style="color:${m(t)}">${escapeHtml(t.type_label)}</span>${t.confidence<.7?' <span class="text-gray-600">(low conf.)</span>':""}</div>\n <div class="text-xs">Status: <span class="${"alive"===t.status?"text-green-400":"text-yellow-400"}">${escapeHtml(t.status||"unknown")}</span></div>\n <div class="text-xs">Ports: ${t.ports.length}${t.ports.length>0&&t.ports.length<=8?" ("+t.ports.join(", ")+")":""}</div>\n <div class="text-xs">Risk: <span class="${n}">${t.risk}</span></div>\n ${t.is_gateway?'<div class="text-xs text-amber-400 mt-1">Default Gateway</div>':""}\n ${t.is_ragnar?'<div class="text-xs text-sky-400 mt-1">Ragnar Scanner</div>':""}\n <div class="text-xs text-blue-400 mt-1">Click to view details</div>`}).on("mousemove",e=>{const t=s.getBoundingClientRect();i.style.left=e.clientX-t.left+12+"px",i.style.top=e.clientY-t.top-10+"px"}).on("mouseout",()=>i.classList.add("hidden"));f.append("circle").attr("r",e=>g(e)+3).attr("fill","none").attr("stroke",e=>e.risk>8?"#ef4444":e.risk>3?"#f59e0b":"none").attr("stroke-width",2).attr("stroke-opacity",.5),f.append("circle").attr("r",g).attr("fill",m).attr("stroke","#0f172a").attr("stroke-width",2).attr("fill-opacity",.9),f.each(function(e){const t=a[e.is_ragnar?"ragnar":e.type]||a.unknown;if(!t)return;const n=g(e)/16;d3.select(this).append("path").attr("d",t).attr("fill","#0f172a").attr("fill-opacity",.6).attr("transform",`translate(${-12*n},${-12*n}) scale(${n})`)}),f.append("text").attr("dy",e=>g(e)+14).attr("text-anchor","middle").attr("fill","#94a3b8").attr("font-size","10px").text(e=>e.is_gateway?"Gateway":e.is_ragnar?"Ragnar":e.label||e.ip);const h=e.find(e=>e.is_gateway);h&&(h.fx=l/2,h.fy=c/2),_mapSimulation=d3.forceSimulation(e).force("link",d3.forceLink(t).id(e=>e.id).distance(e=>"ap_uplink"===e.type?80:120)).force("charge",d3.forceManyBody().strength(-350)).force("center",d3.forceCenter(l/2,c/2)).force("collision",d3.forceCollide(e=>g(e)+15)).on("tick",()=>{p.attr("x1",e=>e.source.x).attr("y1",e=>e.source.y).attr("x2",e=>e.target.x).attr("y2",e=>e.target.y),f.attr("transform",e=>`translate(${e.x},${e.y})`)}),r.style.display="none",o.style.display="block"}function refreshNetworkMap(){_mapInitialized=!1,loadNetworkMap()}function escapeHtml(e){if(!e)return"";const t=document.createElement("div");return t.textContent=e,t.innerHTML}function toggleSubnetPanel(){const e=document.getElementById("subnet-panel"),t=document.getElementById("subnet-chevron");if(!e)return;const n=e.classList.toggle("hidden");t&&(t.style.transform=n?"":"rotate(90deg)"),n?_stopSubnetLogPolling():(loadScanSubnets(),_startSubnetLogPolling())}async function loadScanSubnets(){try{const e=await networkAwareFetch("/api/config/scan-subnets"),t=await e.json(),n=document.getElementById("subnet-primary");n&&t.primary&&(n.textContent=`Primary (auto-detected): ${t.primary}`);const a=document.getElementById("subnet-count"),s=t.subnets||[];a&&(a.textContent=s.length);const o=document.getElementById("subnet-list");if(!o)return;if(0===s.length)return void(o.innerHTML='<p class="text-xs text-gray-600 italic">No extra subnets configured.</p>');o.innerHTML=s.map(e=>`\n <div class="flex items-center justify-between bg-slate-700/50 rounded px-2 py-1">\n <span class="font-mono text-xs text-gray-300">${escapeHtml(e)}</span>\n <button onclick="removeScanSubnet('${escapeHtml(e)}')" class="text-red-400 hover:text-red-300 text-xs ml-2">×</button>\n </div>`).join("")}catch(e){console.error("loadScanSubnets error",e)}}async function addScanSubnet(){const e=document.getElementById("subnet-input"),t=document.getElementById("subnet-error");if(!e)return;const n=e.value.trim();if(n){t&&(t.textContent="",t.classList.add("hidden"));try{const a=await networkAwareFetch("/api/config/scan-subnets",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({cidr:n})}),s=await a.json();if(!a.ok||s.error)return void(t&&(t.textContent=s.error||"Failed to add subnet",t.classList.remove("hidden")));e.value="",await loadScanSubnets(),triggerSubnetScan(s.subnets?s.subnets[s.subnets.length-1]:n)}catch(e){t&&(t.textContent=e.message,t.classList.remove("hidden"))}}}async function triggerSubnetScan(e){try{await networkAwareFetch("/api/config/scan-subnets/trigger",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({cidr:e})}),_startSubnetLogPolling()}catch(e){console.error("triggerSubnetScan error",e)}}async function removeScanSubnet(e){try{await networkAwareFetch("/api/config/scan-subnets",{method:"DELETE",headers:{"Content-Type":"application/json"},body:JSON.stringify({cidr:e})}),await loadScanSubnets()}catch(e){console.error("removeScanSubnet error",e)}}let _subnetLogTimer=null;const _logStatusStyle={ok:"text-green-400",error:"text-red-400",info:"text-blue-400",skip:"text-yellow-400"},_logStatusIcon={ok:"✅",error:"❌",info:"🔍",skip:"⏭️"};async function loadSubnetScanLog(){const e=document.getElementById("subnet-scan-log");if(e)try{const t=await networkAwareFetch("/api/config/scan-subnets/log"),n=(await t.json()).log||[];if(0===n.length)return void(e.innerHTML='<p class="text-gray-600 italic">No scan data yet — waiting for next scan cycle.</p>');e.innerHTML=n.slice().reverse().map(e=>{const t=_logStatusStyle[e.status]||"text-gray-400",n=_logStatusIcon[e.status]||"•",a=void 0!==e.devices?` (${e.devices} device${1!==e.devices?"s":""})`:"";return`<div class="${t}"><span class="text-gray-600">${escapeHtml(e.ts)}</span> ${n} <span class="text-gray-300">${escapeHtml(e.cidr)}</span> — ${escapeHtml(e.msg)}${a}</div>`}).join(""),e.scrollTop=0}catch(e){console.error("loadSubnetScanLog error",e)}}function _startSubnetLogPolling(){_stopSubnetLogPolling(),loadSubnetScanLog(),_subnetLogTimer=setInterval(loadSubnetScanLog,1e4)}function _stopSubnetLogPolling(){_subnetLogTimer&&(clearInterval(_subnetLogTimer),_subnetLogTimer=null)}window.checkServerCapabilities=checkServerCapabilities,window.loadTrafficAnalysisData=loadTrafficAnalysisData,window.toggleTrafficCapture=toggleTrafficCapture,window.refreshTrafficData=refreshTrafficData,window.showTrafficHostDetail=showTrafficHostDetail,window.closeTrafficHostModal=closeTrafficHostModal,window.showTrafficConnectionDetail=showTrafficConnectionDetail,window.closeTrafficConnectionModal=closeTrafficConnectionModal,window.showTrafficPortDetail=showTrafficPortDetail,window.closeTrafficPortModal=closeTrafficPortModal,window.loadAdvancedVulnData=loadAdvancedVulnData,window.startAdvancedScan=startAdvancedScan,window.toggleScanMode=toggleScanMode,window.setScanStrength=setScanStrength,window.toggleRequestBodyField=toggleRequestBodyField,window.refreshAdvVulnData=refreshAdvVulnData,window.toggleAdvVulnScansExpanded=toggleAdvVulnScansExpanded,window.toggleAdvVulnScanFindings=toggleAdvVulnScanFindings,window.toggleAdvVulnScanLogs=toggleAdvVulnScanLogs,window.cancelAdvScan=cancelAdvScan,window.startZapDaemon=startZapDaemon,window.stopZapDaemon=stopZapDaemon,window.zapClearSession=zapClearSession,window.zapImportOpenAPI=zapImportOpenAPI,window.zapSetAuthentication=zapSetAuthentication,window.zapCheckAuthStatus=zapCheckAuthStatus,window.zapClearAuthentication=zapClearAuthentication,window.fetchZapStatus=fetchZapStatus,window.checkTargetCredentials=checkTargetCredentials,window.showCredentialsModal=showCredentialsModal,window.closeCredentialsModal=closeCredentialsModal,window.toggleCredentialFields=toggleCredentialFields,window.toggleScanAuthFields=toggleScanAuthFields,window.saveTargetCredentials=saveTargetCredentials,window.clearCredentialForm=clearCredentialForm,window.loadSavedCredentialsList=loadSavedCredentialsList,window.editTargetCredential=editTargetCredential,window.deleteTargetCredential=deleteTargetCredential,window.loadNetworkMap=loadNetworkMap,window.refreshNetworkMap=refreshNetworkMap,window.onMapAiToggleClick=onMapAiToggleClick,window.toggleSubnetPanel=toggleSubnetPanel,window.addScanSubnet=addScanSubnet,window.removeScanSubnet=removeScanSubnet; |
no test coverage detected