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:
@@ -216,6 +216,22 @@ class DeviceDatabase:
|
||||
except sqlite3.OperationalError:
|
||||
pass # Column already exists
|
||||
|
||||
# Add source_scanner columns for peer sync (device positions relative to source scanner)
|
||||
try:
|
||||
cursor.execute("ALTER TABLE devices ADD COLUMN source_scanner_id TEXT")
|
||||
except sqlite3.OperationalError:
|
||||
pass # Column already exists
|
||||
|
||||
try:
|
||||
cursor.execute("ALTER TABLE devices ADD COLUMN source_scanner_lat REAL")
|
||||
except sqlite3.OperationalError:
|
||||
pass # Column already exists
|
||||
|
||||
try:
|
||||
cursor.execute("ALTER TABLE devices ADD COLUMN source_scanner_lon REAL")
|
||||
except sqlite3.OperationalError:
|
||||
pass # Column already exists
|
||||
|
||||
conn.commit()
|
||||
|
||||
def record_scan(self, scan_id: str, timestamp: str, location_label: str,
|
||||
@@ -586,6 +602,42 @@ class DeviceDatabase:
|
||||
cursor.execute("SELECT device_id, assigned_floor FROM devices WHERE assigned_floor IS NOT NULL")
|
||||
return {row['device_id']: row['assigned_floor'] for row in cursor.fetchall()}
|
||||
|
||||
def get_all_device_sources(self) -> dict:
|
||||
"""Get all device source scanner info as a dict.
|
||||
|
||||
Returns:
|
||||
Dict mapping device_id to {scanner_id, lat, lon} or None if local
|
||||
"""
|
||||
conn = self._get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT device_id, source_scanner_id, source_scanner_lat, source_scanner_lon
|
||||
FROM devices
|
||||
WHERE source_scanner_id IS NOT NULL
|
||||
""")
|
||||
return {
|
||||
row['device_id']: {
|
||||
'scanner_id': row['source_scanner_id'],
|
||||
'lat': row['source_scanner_lat'],
|
||||
'lon': row['source_scanner_lon']
|
||||
}
|
||||
for row in cursor.fetchall()
|
||||
}
|
||||
|
||||
def set_device_source(self, device_id: str, scanner_id: str,
|
||||
scanner_lat: float, scanner_lon: float):
|
||||
"""Set source scanner info for a device (where it was detected/positioned)."""
|
||||
conn = self._get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
UPDATE devices
|
||||
SET source_scanner_id = ?, source_scanner_lat = ?, source_scanner_lon = ?
|
||||
WHERE device_id = ?
|
||||
""", (scanner_id, scanner_lat, scanner_lon, device_id))
|
||||
conn.commit()
|
||||
|
||||
def set_device_position(self, device_id: str, lat_offset: float, lon_offset: float):
|
||||
"""Set custom position offset for a device (relative to scanner position)"""
|
||||
conn = self._get_connection()
|
||||
@@ -931,14 +983,17 @@ class DeviceDatabase:
|
||||
Returns:
|
||||
List of device dicts with sync-relevant fields.
|
||||
Note: Position offsets are NOT synced as they are relative to each scanner's location.
|
||||
Source scanner info IS synced so receiving scanners can calculate positions correctly.
|
||||
"""
|
||||
conn = self._get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Include source scanner info so receiving scanners know where device was detected
|
||||
# Don't sync position offsets - they're relative to each scanner's location
|
||||
query = """
|
||||
SELECT device_id, device_type, name, ssid, manufacturer,
|
||||
custom_label, assigned_floor, is_favorite, notes, updated_at
|
||||
custom_label, assigned_floor, is_favorite, notes, updated_at,
|
||||
source_scanner_id, source_scanner_lat, source_scanner_lon
|
||||
FROM devices
|
||||
WHERE (custom_label IS NOT NULL OR assigned_floor IS NOT NULL
|
||||
OR is_favorite = 1 OR notes IS NOT NULL)
|
||||
@@ -954,15 +1009,21 @@ class DeviceDatabase:
|
||||
cursor.execute(query, params)
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
|
||||
def bulk_update_devices(self, devices: list[dict], source_scanner: str) -> int:
|
||||
def bulk_update_devices(self, devices: list[dict], source_scanner: str,
|
||||
source_scanner_lat: Optional[float] = None,
|
||||
source_scanner_lon: Optional[float] = None) -> int:
|
||||
"""Bulk update device metadata from peer sync.
|
||||
|
||||
Uses timestamp-based conflict resolution: newer wins.
|
||||
Only updates non-null fields from peer.
|
||||
Preserves source scanner info so device positions are calculated relative to
|
||||
the scanner that originally detected them.
|
||||
|
||||
Args:
|
||||
devices: List of device dicts from peer
|
||||
source_scanner: Scanner ID that sent the update
|
||||
source_scanner_lat: Latitude of source scanner (for position calculation)
|
||||
source_scanner_lon: Longitude of source scanner (for position calculation)
|
||||
|
||||
Returns:
|
||||
Number of devices updated
|
||||
@@ -978,11 +1039,17 @@ class DeviceDatabase:
|
||||
|
||||
# Get existing device
|
||||
cursor.execute(
|
||||
"SELECT updated_at FROM devices WHERE device_id = ?",
|
||||
"SELECT updated_at, source_scanner_id FROM devices WHERE device_id = ?",
|
||||
(device_id,)
|
||||
)
|
||||
existing = cursor.fetchone()
|
||||
|
||||
# Determine source scanner info for this device
|
||||
# Use device's original source if present, otherwise use the peer sending the data
|
||||
dev_source_id = dev.get("source_scanner_id") or source_scanner
|
||||
dev_source_lat = dev.get("source_scanner_lat") or source_scanner_lat
|
||||
dev_source_lon = dev.get("source_scanner_lon") or source_scanner_lon
|
||||
|
||||
if existing:
|
||||
# Check timestamp - skip if local is newer
|
||||
local_updated = existing["updated_at"] or ""
|
||||
@@ -1012,6 +1079,18 @@ class DeviceDatabase:
|
||||
updates.append("notes = ?")
|
||||
params.append(dev["notes"])
|
||||
|
||||
# Update source scanner info if not already set locally
|
||||
# (preserve original source, don't overwrite with intermediate peer)
|
||||
if not existing["source_scanner_id"] and dev_source_id:
|
||||
updates.append("source_scanner_id = ?")
|
||||
params.append(dev_source_id)
|
||||
if dev_source_lat is not None:
|
||||
updates.append("source_scanner_lat = ?")
|
||||
params.append(dev_source_lat)
|
||||
if dev_source_lon is not None:
|
||||
updates.append("source_scanner_lon = ?")
|
||||
params.append(dev_source_lon)
|
||||
|
||||
if updates:
|
||||
# Keep the peer's updated_at to preserve timeline
|
||||
updates.append("updated_at = ?")
|
||||
@@ -1034,13 +1113,15 @@ class DeviceDatabase:
|
||||
cursor.execute("""
|
||||
INSERT INTO devices (device_id, device_type, name, ssid, manufacturer,
|
||||
custom_label, assigned_floor, is_favorite, notes,
|
||||
first_seen, last_seen, total_observations, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?)
|
||||
first_seen, last_seen, total_observations, updated_at,
|
||||
source_scanner_id, source_scanner_lat, source_scanner_lon)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?)
|
||||
""", (
|
||||
device_id, device_type, name, dev.get("ssid"), dev.get("manufacturer"),
|
||||
dev.get("custom_label"), dev.get("assigned_floor"),
|
||||
1 if dev.get("is_favorite") else 0, dev.get("notes"),
|
||||
now, now, dev.get("updated_at", now)
|
||||
now, now, dev.get("updated_at", now),
|
||||
dev_source_id, dev_source_lat, dev_source_lon
|
||||
))
|
||||
updated_count += 1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user