feat: preserve source scanner for synced devices
When devices are synced from a peer, they now retain their original source scanner reference. This ensures: - Device positions are calculated relative to the scanner that detected them, not the local scanner - Moving the local scanner won't affect synced devices' positions - Popup shows "Source: <scanner_id>" for remotely-synced devices Database changes: - Added source_scanner_id, source_scanner_lat, source_scanner_lon columns to devices table - get_devices_since() includes source scanner info in sync data - bulk_update_devices() accepts and stores source scanner position - Added get_all_device_sources() method API changes: - /api/sync/devices GET includes scanner_lat and scanner_lon - /api/sync/devices POST accepts source_scanner_lat/lon - /api/device/floors includes sources dict Frontend changes: - loadDevicePositions() loads source scanner info - getDevicePosition() uses source scanner position for synced devices - Popup shows source scanner info for remotely-synced devices Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,9 @@ let deviceTrails = {}; // { deviceId: { type, name, points: [{ timestamp, distan
|
||||
// Device manual positions - loaded from database
|
||||
let manualPositions = {}; // { deviceId: { lat_offset, lon_offset } }
|
||||
|
||||
// Device source scanner info - loaded from database (for synced devices)
|
||||
let deviceSources = {}; // { deviceId: { scanner_id, lat, lon } }
|
||||
|
||||
// Auto-scan state
|
||||
let autoScanEnabled = false;
|
||||
let autoScanPollInterval = null;
|
||||
@@ -1059,17 +1062,21 @@ async function stopAutoScan() {
|
||||
|
||||
// ========== Device Position Functions ==========
|
||||
|
||||
// Load saved device positions from database
|
||||
// Load saved device positions and source scanner info from database
|
||||
async function loadDevicePositions() {
|
||||
try {
|
||||
const response = await fetch('/api/device/floors');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
// Handle both old format (just floors) and new format (floors + positions)
|
||||
// Handle both old format (just floors) and new format (floors + positions + sources)
|
||||
if (data.positions) {
|
||||
manualPositions = data.positions;
|
||||
console.log('[Positions] Loaded', Object.keys(manualPositions).length, 'manual positions');
|
||||
}
|
||||
if (data.sources) {
|
||||
deviceSources = data.sources;
|
||||
console.log('[Sources] Loaded', Object.keys(deviceSources).length, 'device sources');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading device positions:', error);
|
||||
@@ -1077,31 +1084,49 @@ async function loadDevicePositions() {
|
||||
}
|
||||
|
||||
// Get device position (manual or RSSI-based)
|
||||
// Uses source scanner position for synced devices so they don't move when local scanner moves
|
||||
function getDevicePosition(device, scannerLat, scannerLon, minDistanceM) {
|
||||
const deviceId = device.bssid || device.address;
|
||||
const customPos = manualPositions[deviceId];
|
||||
const sourceInfo = deviceSources[deviceId];
|
||||
|
||||
// If device has manual position, use it
|
||||
// Determine which scanner position to use for this device
|
||||
// If device was synced from another scanner, use that scanner's position
|
||||
let baseLat = scannerLat;
|
||||
let baseLon = scannerLon;
|
||||
let isFromRemoteScanner = false;
|
||||
|
||||
if (sourceInfo && sourceInfo.lat != null && sourceInfo.lon != null) {
|
||||
baseLat = sourceInfo.lat;
|
||||
baseLon = sourceInfo.lon;
|
||||
isFromRemoteScanner = true;
|
||||
}
|
||||
|
||||
// If device has manual position, use it (relative to source scanner)
|
||||
if (customPos && customPos.lat_offset != null && customPos.lon_offset != null) {
|
||||
return {
|
||||
lat: scannerLat + customPos.lat_offset,
|
||||
lon: scannerLon + customPos.lon_offset,
|
||||
isManual: true
|
||||
lat: baseLat + customPos.lat_offset,
|
||||
lon: baseLon + customPos.lon_offset,
|
||||
isManual: true,
|
||||
isRemoteSource: isFromRemoteScanner,
|
||||
sourceScanner: sourceInfo?.scanner_id || null
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise calculate from RSSI/distance
|
||||
// Otherwise calculate from RSSI/distance (relative to source scanner)
|
||||
const effectiveDist = getEffectiveDistance(device);
|
||||
const dist = Math.max(effectiveDist, minDistanceM);
|
||||
const angle = hashString(deviceId) % 360;
|
||||
|
||||
const latOffset = (dist * Math.cos(angle * Math.PI / 180)) / 111000;
|
||||
const lonOffset = (dist * Math.sin(angle * Math.PI / 180)) / (111000 * Math.cos(scannerLat * Math.PI / 180));
|
||||
const lonOffset = (dist * Math.sin(angle * Math.PI / 180)) / (111000 * Math.cos(baseLat * Math.PI / 180));
|
||||
|
||||
return {
|
||||
lat: scannerLat + latOffset,
|
||||
lon: scannerLon + lonOffset,
|
||||
isManual: false
|
||||
lat: baseLat + latOffset,
|
||||
lon: baseLon + lonOffset,
|
||||
isManual: false,
|
||||
isRemoteSource: isFromRemoteScanner,
|
||||
sourceScanner: sourceInfo?.scanner_id || null
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1198,10 +1223,22 @@ async function onScannerDragEnd(marker) {
|
||||
}
|
||||
|
||||
// Handle marker drag end
|
||||
// Uses source scanner position for synced devices
|
||||
async function onMarkerDragEnd(marker, deviceId, scannerLat, scannerLon) {
|
||||
const lngLat = marker.getLngLat();
|
||||
const latOffset = lngLat.lat - scannerLat;
|
||||
const lonOffset = lngLat.lng - scannerLon;
|
||||
|
||||
// Determine base position (source scanner for synced devices, local scanner otherwise)
|
||||
const sourceInfo = deviceSources[deviceId];
|
||||
let baseLat = scannerLat;
|
||||
let baseLon = scannerLon;
|
||||
|
||||
if (sourceInfo && sourceInfo.lat != null && sourceInfo.lon != null) {
|
||||
baseLat = sourceInfo.lat;
|
||||
baseLon = sourceInfo.lon;
|
||||
}
|
||||
|
||||
const latOffset = lngLat.lat - baseLat;
|
||||
const lonOffset = lngLat.lng - baseLon;
|
||||
|
||||
const success = await updateDevicePosition(deviceId, latOffset, lonOffset);
|
||||
|
||||
@@ -1446,6 +1483,7 @@ function update3DMarkers() {
|
||||
const positionClass = hasManualPosition ? 'manual' : 'auto';
|
||||
const resetBtn = hasManualPosition ? `<button class="popup-reset-btn" onclick="resetDevicePosition('${wifiDeviceId}')">Reset to Auto</button>` : '';
|
||||
const dragHint = isDraggable && !hasManualPosition ? '<div style="font-size:0.7rem;color:#888;margin-top:4px;">Drag marker to set position</div>' : '';
|
||||
const sourceInfo = pos.isRemoteSource ? `<div class="popup-source-info">📡 Source: ${pos.sourceScanner}</div>` : '';
|
||||
|
||||
const popup = new maplibregl.Popup({ offset: 25 }).setHTML(`
|
||||
<strong>📶 ${escapeHtml(net.ssid)}</strong><br>
|
||||
@@ -1453,6 +1491,7 @@ function update3DMarkers() {
|
||||
Distance: ${distLabel}<br>
|
||||
Channel: ${net.channel}<br>
|
||||
${escapeHtml(net.manufacturer)}<br>
|
||||
${sourceInfo}
|
||||
<div class="popup-floor-control">
|
||||
<label>Floor:</label>
|
||||
<select onchange="updateDeviceFloor('${wifiDeviceId}', this.value)">
|
||||
@@ -1534,6 +1573,7 @@ function update3DMarkers() {
|
||||
onclick="toggleDeviceTrail('${btDeviceId}', '${escapeHtml(dev.name)}', 'bluetooth')">
|
||||
Show Trail
|
||||
</button>` : '';
|
||||
const btSourceInfo = pos.isRemoteSource ? `<div class="popup-source-info">📡 Source: ${pos.sourceScanner}</div>` : '';
|
||||
|
||||
const popup = new maplibregl.Popup({ offset: 25 }).setHTML(`
|
||||
<strong>${isMoving ? '🟣' : '🔵'} ${escapeHtml(dev.name)}</strong>${isMoving ? ' <span style="color:#9b59b6;font-size:0.8em;">(Moving)</span>' : ''}<br>
|
||||
@@ -1541,6 +1581,7 @@ function update3DMarkers() {
|
||||
Distance: ${btDistLabel}<br>
|
||||
Type: ${escapeHtml(dev.device_type)}<br>
|
||||
${escapeHtml(dev.manufacturer)}<br>
|
||||
${btSourceInfo}
|
||||
<div class="popup-floor-control">
|
||||
<label>Floor:</label>
|
||||
<select onchange="updateDeviceFloor('${btDeviceId}', this.value)">
|
||||
|
||||
Reference in New Issue
Block a user