- Add manufacturer display to AP detection banner - Show "your router (Manufacturer)" when OUI is known - Show "your router" as fallback when OUI unknown - Expand OUI database with more manufacturer entries The AP detection banner now shows the router manufacturer when available from OUI lookup, making it easier for users to identify their router during the onboarding process. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
263 lines
8.5 KiB
JavaScript
263 lines
8.5 KiB
JavaScript
/**
|
|
* 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, '"')
|
|
.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 = `
|
|
<div class="ap-detection-content">
|
|
<div class="ap-detection-icon">📡</div>
|
|
<div class="ap-detection-message">
|
|
<div class="ap-detection-title">Router Detected!</div>
|
|
<div class="ap-detection-subtitle">
|
|
I detected ${routerDesc}. Place it on the floor plan to improve accuracy.
|
|
</div>
|
|
</div>
|
|
<div class="ap-detection-actions">
|
|
<button class="ap-btn ap-btn-secondary" id="ap-dismiss-btn">Later</button>
|
|
<button class="ap-btn ap-btn-primary" id="ap-confirm-btn">Place Router</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(banner);
|
|
|
|
// Attach event listeners
|
|
document.getElementById('ap-dismiss-btn').addEventListener('click', dismissAPDetection);
|
|
document.getElementById('ap-confirm-btn').addEventListener('click', confirmAPAsSignalSource);
|
|
|
|
// Animate in
|
|
requestAnimationFrame(() => {
|
|
banner.classList.add('ap-detection-banner-visible');
|
|
});
|
|
}
|
|
|
|
function hideAPDetectionBanner() {
|
|
const banner = document.getElementById('ap-detection-banner');
|
|
if (banner) {
|
|
banner.classList.remove('ap-detection-banner-visible');
|
|
setTimeout(() => {
|
|
if (banner.parentNode) {
|
|
banner.parentNode.removeChild(banner);
|
|
}
|
|
}, 300);
|
|
}
|
|
}
|
|
|
|
function showPlacementInstructions() {
|
|
SpaxelPanels.openSidebar({
|
|
title: 'Place Your Router',
|
|
content: `
|
|
<div class="ap-placement-content">
|
|
<p><strong>Drag the router icon</strong> in the 3D view to its actual location in your home.</p>
|
|
<ul class="ap-placement-list">
|
|
<li>The router should be placed at its <strong>actual physical location</strong></li>
|
|
<li>For best results, place it at <strong>router height</strong> (typically on a desk or shelf)</li>
|
|
<li>The virtual node helps the system understand signal geometry</li>
|
|
</ul>
|
|
<div class="ap-placement-tips">
|
|
<div class="ap-placement-tip">
|
|
<strong>Tip:</strong> You can fine-tune the position later in Setup Mode
|
|
</div>
|
|
</div>
|
|
<button class="panel-btn panel-btn-secondary panel-btn-full" id="ap-cancel-placement">
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
`,
|
|
width: '350px'
|
|
});
|
|
|
|
document.getElementById('ap-cancel-placement').addEventListener('click', () => {
|
|
if (window.SpaxelViz3D) {
|
|
window.SpaxelViz3D.cancelNodePlacement();
|
|
}
|
|
apState.placementMode = false;
|
|
SpaxelPanels.closeSidebar();
|
|
});
|
|
}
|
|
|
|
function hidePlacementInstructions() {
|
|
if (window.SpaxelPanels) {
|
|
window.SpaxelPanels.closeSidebar();
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// Public API
|
|
// ============================================
|
|
|
|
window.SpaxelAPDetection = {
|
|
checkForDetectedAP,
|
|
confirmAPAsSignalSource,
|
|
dismissAPDetection,
|
|
updateAPPosition,
|
|
getState: () => apState
|
|
};
|
|
|
|
// Auto-check on page load
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
setTimeout(checkForDetectedAP, 2000);
|
|
});
|
|
} else {
|
|
setTimeout(checkForDetectedAP, 2000);
|
|
}
|
|
|
|
// Check periodically for new AP detection
|
|
setInterval(checkForDetectedAP, 30000);
|
|
|
|
console.log('[APDetection] AP detection module loaded');
|
|
})();
|