/** * Spaxel Dashboard - Router AP Detection Panel * * Shows when a router AP is auto-detected for passive radar mode. * Allows the user to place the virtual router node in the 3D editor. */ (function() { 'use strict'; // ============================================ // State // ============================================ const apState = { detectedAP: null, dismissed: false, placementMode: false }; // ============================================ // API // ============================================ /** * Check for detected AP and show notification if found */ function checkForDetectedAP() { if (apState.dismissed) { return Promise.resolve(null); } return fetch('/api/nodes') .then(res => res.json()) .then(nodes => { const virtualAP = nodes.find(n => n.virtual && n.node_type === 'ap'); if (virtualAP && !apState.detectedAP) { apState.detectedAP = virtualAP; showAPDetectionBanner(virtualAP); } return virtualAP; }) .catch(err => { console.error('[APDetection] Error checking for AP:', err); return null; }); } /** * Confirm the AP as a signal source and place it in 3D */ function confirmAPAsSignalSource() { if (!apState.detectedAP) return; hideAPDetectionBanner(); apState.placementMode = true; // Switch to 3D view and enable placement mode if (window.SpaxelRouter) { window.SpaxelRouter.setMode('3d'); } // Show placement instructions showPlacementInstructions(); // Enable drag-to-place for the virtual AP if (window.SpaxelViz3D) { window.SpaxelViz3D.enableNodePlacement(apState.detectedAP.mac, { onComplete: function(position) { updateAPPosition(apState.detectedAP.mac, position); apState.placementMode = false; hidePlacementInstructions(); SpaxelPanels.showSuccess('Router placed successfully! Passive radar is now active.'); }, onCancel: function() { apState.placementMode = false; hidePlacementInstructions(); } }); } } /** * Update AP position in database */ function updateAPPosition(mac, position) { return fetch('/api/nodes/' + mac, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ position: { x: position.x, y: position.y, z: position.z } }) }) .then(res => { if (!res.ok) { throw new Error('Failed to update AP position'); } return res.json(); }) .then(node => { // Update local state if (window.SpaxelState && window.SpaxelState.nodes) { window.SpaxelState.nodes[mac] = node; } return node; }) .catch(err => { console.error('[APDetection] Error updating AP position:', err); SpaxelPanels.showError('Failed to place router: ' + err.message); throw err; }); } /** * Dismiss the AP detection banner */ function dismissAPDetection() { apState.dismissed = true; hideAPDetectionBanner(); } // ============================================ // Helpers // ============================================ function escapeHtml(s) { if (!s) return ''; return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } // ============================================ // UI Rendering // ============================================ function showAPDetectionBanner(ap) { // Remove existing banner if present hideAPDetectionBanner(); const banner = document.createElement('div'); banner.id = 'ap-detection-banner'; banner.className = 'ap-detection-banner'; // Build the router description with manufacturer if available let routerDesc = 'your router'; if (ap.manufacturer) { routerDesc = 'your router (' + escapeHtml(ap.manufacturer) + ')'; } banner.innerHTML = `
Drag the router icon in the 3D view to its actual location in your home.