Compare commits
5 Commits
6a3e3e8448
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea88343ae7 | ||
|
|
04bbd0b0af | ||
|
|
8e25bf8871 | ||
|
|
2973178cb8 | ||
|
|
827830b043 |
@@ -1,6 +1,6 @@
|
||||
gps:
|
||||
latitude: 50.858495376473314
|
||||
longitude: 4.397614016072339
|
||||
latitude: 50.85846541332012
|
||||
longitude: 4.397570348817993
|
||||
web:
|
||||
host: 0.0.0.0
|
||||
port: 5000
|
||||
|
||||
@@ -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 = ?")
|
||||
@@ -1025,9 +1104,26 @@ class DeviceDatabase:
|
||||
if cursor.rowcount > 0:
|
||||
updated_count += 1
|
||||
else:
|
||||
# Device doesn't exist locally - we can only sync metadata for
|
||||
# devices we've seen, so skip unknown devices
|
||||
pass
|
||||
# Device doesn't exist locally - create it if it has useful metadata
|
||||
if dev.get("assigned_floor") is not None or dev.get("custom_label") or dev.get("is_favorite"):
|
||||
device_type = dev.get("device_type", "bluetooth")
|
||||
name = dev.get("name") or dev.get("ssid") or device_id
|
||||
now = datetime.now().isoformat()
|
||||
|
||||
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,
|
||||
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),
|
||||
dev_source_id, dev_source_lat, dev_source_lon
|
||||
))
|
||||
updated_count += 1
|
||||
|
||||
conn.commit()
|
||||
return updated_count
|
||||
|
||||
@@ -124,7 +124,15 @@ class PeerSync:
|
||||
devices = data.get("devices", [])
|
||||
source_scanner = data.get("scanner_id", "unknown")
|
||||
|
||||
updated = self.db.bulk_update_devices(devices, source_scanner)
|
||||
# Get source scanner position for correct device positioning
|
||||
source_lat = data.get("scanner_lat")
|
||||
source_lon = data.get("scanner_lon")
|
||||
|
||||
updated = self.db.bulk_update_devices(
|
||||
devices, source_scanner,
|
||||
source_scanner_lat=source_lat,
|
||||
source_scanner_lon=source_lon
|
||||
)
|
||||
return updated
|
||||
|
||||
def push_devices_to_peer(self, peer_url: str, since: Optional[str] = None) -> dict:
|
||||
@@ -141,6 +149,8 @@ class PeerSync:
|
||||
|
||||
payload = {
|
||||
"source_scanner": self.scanner_identity["id"],
|
||||
"source_scanner_lat": self.scanner_identity["latitude"],
|
||||
"source_scanner_lon": self.scanner_identity["longitude"],
|
||||
"devices": devices
|
||||
}
|
||||
|
||||
|
||||
@@ -880,17 +880,19 @@ def create_app(config: Config | None = None) -> Flask:
|
||||
|
||||
@app.route("/api/device/floors", methods=["GET"])
|
||||
def api_device_floors():
|
||||
"""Get all saved floor assignments and position offsets"""
|
||||
"""Get all saved floor assignments, position offsets, and source scanner info"""
|
||||
db = app.config.get("DATABASE")
|
||||
if not db:
|
||||
return jsonify({"floors": {}, "positions": {}})
|
||||
return jsonify({"floors": {}, "positions": {}, "sources": {}})
|
||||
|
||||
floors = db.get_all_device_floors()
|
||||
positions = db.get_all_device_positions()
|
||||
sources = db.get_all_device_sources()
|
||||
|
||||
return jsonify({
|
||||
"floors": floors,
|
||||
"positions": positions
|
||||
"positions": positions,
|
||||
"sources": sources
|
||||
})
|
||||
|
||||
@app.route("/api/device/<device_id>/position", methods=["POST"])
|
||||
@@ -1348,9 +1350,12 @@ def create_app(config: Config | None = None) -> Flask:
|
||||
|
||||
since = request.args.get("since")
|
||||
devices = db.get_devices_since(since)
|
||||
scanner_identity = app.config["SCANNER_IDENTITY"]
|
||||
|
||||
return jsonify({
|
||||
"scanner_id": app.config["SCANNER_IDENTITY"]["id"],
|
||||
"scanner_id": scanner_identity["id"],
|
||||
"scanner_lat": scanner_identity["latitude"],
|
||||
"scanner_lon": scanner_identity["longitude"],
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"devices": devices
|
||||
})
|
||||
@@ -1365,8 +1370,14 @@ def create_app(config: Config | None = None) -> Flask:
|
||||
data = request.get_json() or {}
|
||||
devices = data.get("devices", [])
|
||||
source_scanner = data.get("source_scanner", "unknown")
|
||||
source_lat = data.get("source_scanner_lat")
|
||||
source_lon = data.get("source_scanner_lon")
|
||||
|
||||
updated = db.bulk_update_devices(devices, source_scanner)
|
||||
updated = db.bulk_update_devices(
|
||||
devices, source_scanner,
|
||||
source_scanner_lat=source_lat,
|
||||
source_scanner_lon=source_lon
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"status": "synced",
|
||||
@@ -1389,12 +1400,14 @@ def create_app(config: Config | None = None) -> Flask:
|
||||
peer_id = peer["scanner_id"]
|
||||
peer_url = peer["url"]
|
||||
try:
|
||||
updated = peer_sync.sync_devices_from_peer(peer_url)
|
||||
peer_sync.push_devices_to_peer(peer_url)
|
||||
pulled = peer_sync.sync_devices_from_peer(peer_url)
|
||||
push_result = peer_sync.push_devices_to_peer(peer_url)
|
||||
pushed = push_result.get("updated", 0) if push_result else 0
|
||||
results.append({
|
||||
"peer_id": peer_id,
|
||||
"status": "success",
|
||||
"devices_updated": updated
|
||||
"devices_pulled": pulled,
|
||||
"devices_pushed": pushed
|
||||
})
|
||||
except Exception as e:
|
||||
results.append({
|
||||
|
||||
@@ -832,6 +832,16 @@ body {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.popup-source-info {
|
||||
margin-top: 6px;
|
||||
padding: 4px 8px;
|
||||
background: rgba(0, 200, 255, 0.1);
|
||||
border: 1px solid rgba(0, 200, 255, 0.3);
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 0.8rem;
|
||||
color: #00c8ff;
|
||||
}
|
||||
|
||||
.popup-reset-btn {
|
||||
margin-top: 4px;
|
||||
padding: 4px 10px;
|
||||
@@ -951,6 +961,19 @@ body {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.marker-3d.peer-scanner .marker-icon {
|
||||
background: #00c8ff;
|
||||
box-shadow: 0 0 15px rgba(0, 200, 255, 0.6), 0 2px 6px rgba(0, 0, 0, 0.4);
|
||||
border: 2px solid #ffffff;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
z-index: 90;
|
||||
}
|
||||
|
||||
.marker-3d.peer-scanner .marker-floor {
|
||||
background: rgba(0, 200, 255, 0.9);
|
||||
}
|
||||
|
||||
/* Floor Controls */
|
||||
.floor-section {
|
||||
display: block;
|
||||
|
||||
@@ -18,6 +18,12 @@ 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 } }
|
||||
|
||||
// Peer scanner positions - loaded from /api/peers (live positions)
|
||||
let peerScanners = {}; // { scanner_id: { lat, lon, floor, name } }
|
||||
|
||||
// Auto-scan state
|
||||
let autoScanEnabled = false;
|
||||
let autoScanPollInterval = null;
|
||||
@@ -1059,17 +1065,38 @@ async function stopAutoScan() {
|
||||
|
||||
// ========== Device Position Functions ==========
|
||||
|
||||
// Load saved device positions from database
|
||||
// Load saved device positions, source scanner info, and peer positions
|
||||
async function loadDevicePositions() {
|
||||
try {
|
||||
// Load device data
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
// Load peer scanner positions (live/current positions)
|
||||
const peersResponse = await fetch('/api/peers');
|
||||
if (peersResponse.ok) {
|
||||
const peersData = await peersResponse.json();
|
||||
peerScanners = {};
|
||||
(peersData.peers || []).forEach(peer => {
|
||||
peerScanners[peer.scanner_id] = {
|
||||
lat: peer.latitude,
|
||||
lon: peer.longitude,
|
||||
floor: peer.floor,
|
||||
name: peer.name
|
||||
};
|
||||
});
|
||||
console.log('[Peers] Loaded', Object.keys(peerScanners).length, 'peer positions');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading device positions:', error);
|
||||
@@ -1077,31 +1104,60 @@ 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 CURRENT position
|
||||
let baseLat = scannerLat;
|
||||
let baseLon = scannerLon;
|
||||
let isFromRemoteScanner = false;
|
||||
let sourceScannerId = null;
|
||||
|
||||
if (sourceInfo && sourceInfo.scanner_id) {
|
||||
sourceScannerId = sourceInfo.scanner_id;
|
||||
// Use peer's CURRENT position from peerScanners (not stored position)
|
||||
const peerPos = peerScanners[sourceInfo.scanner_id];
|
||||
if (peerPos && peerPos.lat != null && peerPos.lon != null) {
|
||||
baseLat = peerPos.lat;
|
||||
baseLon = peerPos.lon;
|
||||
isFromRemoteScanner = true;
|
||||
} else if (sourceInfo.lat != null && sourceInfo.lon != null) {
|
||||
// Fallback to stored position if peer not in list
|
||||
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: sourceScannerId
|
||||
};
|
||||
}
|
||||
|
||||
// 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: sourceScannerId
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1198,10 +1254,29 @@ 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.scanner_id) {
|
||||
// Use peer's CURRENT position
|
||||
const peerPos = peerScanners[sourceInfo.scanner_id];
|
||||
if (peerPos && peerPos.lat != null && peerPos.lon != null) {
|
||||
baseLat = peerPos.lat;
|
||||
baseLon = peerPos.lon;
|
||||
} else if (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);
|
||||
|
||||
@@ -1408,6 +1483,41 @@ function update3DMarkers() {
|
||||
scannerMarker._deviceId = '__scanner__';
|
||||
map3dMarkers.push(scannerMarker);
|
||||
|
||||
// Add peer scanner markers (absolute positions)
|
||||
fetch('/api/peers')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
(data.peers || []).forEach(peer => {
|
||||
if (peer.latitude && peer.longitude) {
|
||||
const peerFloor = peer.floor ?? 0;
|
||||
const peerOffset = (peerFloor - groundFloor) * pixelsPerFloor;
|
||||
|
||||
const peerEl = document.createElement('div');
|
||||
peerEl.className = 'marker-3d peer-scanner';
|
||||
peerEl.innerHTML = `<div class="marker-icon">📡</div><div class="marker-floor">F${peerFloor}</div>`;
|
||||
peerEl.title = `${peer.name || peer.scanner_id} - Floor ${peerFloor}`;
|
||||
|
||||
const peerMarker = new maplibregl.Marker({
|
||||
element: peerEl,
|
||||
offset: [0, -peerOffset]
|
||||
})
|
||||
.setLngLat([peer.longitude, peer.latitude])
|
||||
.setPopup(new maplibregl.Popup().setHTML(`
|
||||
<strong>📡 ${peer.name || peer.scanner_id}</strong><br>
|
||||
Floor: ${peerFloor}<br>
|
||||
Lat: ${peer.latitude.toFixed(6)}<br>
|
||||
Lon: ${peer.longitude.toFixed(6)}<br>
|
||||
<div style="font-size:0.7rem;color:#888;margin-top:4px;">Peer scanner (read-only)</div>
|
||||
`))
|
||||
.addTo(map3d);
|
||||
|
||||
peerMarker._deviceId = `__peer_${peer.scanner_id}__`;
|
||||
map3dMarkers.push(peerMarker);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(err => console.log('[Peers] Error loading peers:', err));
|
||||
|
||||
if (!scanData) return;
|
||||
|
||||
const wifi = filters.wifi ? (scanData.wifi_networks || []) : [];
|
||||
@@ -1446,6 +1556,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 +1564,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 +1646,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 +1654,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