Compare commits

...

6 Commits

Author SHA1 Message Date
User
9fc7c65454 fix: filter scanner BT MACs in API responses
- /api/latest now filters out devices matching peer bt_mac
- Prevents scanner devices from appearing in scan results

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 18:09:06 +01:00
User
579cea57dc fix: filter scanner BT MACs in database recording
- Skip recording BT observations for addresses matching peer bt_mac
- Prevents scanners from being stored as regular devices
- Filters at database level, not just frontend display

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 18:07:11 +01:00
User
5def3e2214 fix: filter scanner BT MACs during BLE Radar import
- Skip devices whose address matches a registered peer's bt_mac
- Prevents scanners from appearing as regular devices
- Shows count of filtered scanners in import stats

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 18:01:58 +01:00
User
cc6d9ee58d feat: add BLE Radar database import
- Add import_ble_radar.py module for importing BLE Radar exports
- Add /api/import/ble-radar endpoint for file upload
- Add import section to dashboard with file picker
- CLI: python -m rf_mapper.import_ble_radar <file.sqlite>
- Imports devices, RSSI, favorites, and custom labels

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 17:54:31 +01:00
User
b1efb4ae3c docs: enhance CLAUDE.md with maintenance table
- Add table of key docs with "When to Update" guidance
- Include TASKS.md, TODO.md, ROADMAP.md, CHANGELOG.md
- Add deployment section with node update commands
- Add Multi-scanner sync and Termux support to key files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 17:39:15 +01:00
User
91536860ad docs: update CLI commands to use python -m rf_mapper
- Replace rf-mapper with python -m rf_mapper throughout docs
- Add note about activating venv first
- Updated: CLAUDE.md, PROJECT.md, USAGE.md, CHEATSHEET.md, HOME_ASSISTANT.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 17:38:44 +01:00
12 changed files with 466 additions and 68 deletions

View File

@@ -2,11 +2,23 @@
RF Environment Scanner for WiFi and Bluetooth signal mapping on Linux.
## Key Documentation
## Key Documentation (Maintain These!)
| File | Purpose | When to Update |
|------|---------|----------------|
| **[TASKS.md](TASKS.md)** | Current sprint tasks, priorities (P0-P3), status | Start/end of each work session |
| **[TODO.md](TODO.md)** | Backlog by category, completed items | When adding/completing features |
| **[ROADMAP.md](ROADMAP.md)** | Version milestones, long-term vision | When milestones change |
| **[CHANGELOG.md](CHANGELOG.md)** | Version history, notable changes | Each release |
| **[PROJECT.md](PROJECT.md)** | Goals, architecture, dependencies | Major architectural changes |
| **[USAGE.md](USAGE.md)** | User guide, CLI, web interface, API | When adding features |
| **[docs/CHEATSHEET.md](docs/CHEATSHEET.md)** | Quick reference commands | When adding features |
| **[INVENTORY.md](INVENTORY.md)** | Multi-node deployment info (gitignored) | When nodes change |
## Configuration
- **[USAGE.md](USAGE.md)** - User guide with CLI commands, web interface, configuration, and API reference
- **[TODO.md](TODO.md)** - Pending features and improvements
- **[config.yaml](config.yaml)** - Current configuration (GPS, web server, scanner, building settings)
- **[docs/HOME_ASSISTANT.md](docs/HOME_ASSISTANT.md)** - Home Assistant webhook integration
## Project Structure
@@ -22,6 +34,8 @@ src/rf_mapper/
├── bluetooth_*.py # Bluetooth device identification and classification
├── visualize.py # ASCII radar and chart generation
├── profiling.py # CPU/memory profiling utilities
├── termux.py # Termux/Android environment detection
├── sync.py # Multi-scanner peer sync
└── web/
├── app.py # Flask application and API endpoints
├── templates/ # Jinja2 HTML templates (base.html, index.html)
@@ -38,21 +52,35 @@ src/rf_mapper/
| Change web UI | `web/templates/index.html`, `static/js/app.js`, `static/css/style.css` |
| Add configuration | `src/rf_mapper/config.py`, `config.yaml` |
| Home Assistant integration | `src/rf_mapper/homeassistant.py`, `docs/HOME_ASSISTANT.md` |
| Multi-scanner sync | `src/rf_mapper/sync.py`, `web/app.py` |
| Termux/Android support | `src/rf_mapper/termux.py` |
## Running
```bash
source venv/bin/activate
rf-mapper start # Start web server (background)
rf-mapper status # Check if running
rf-mapper stop # Stop server
rf-mapper scan -l room # CLI scan
rf-mapper --help # All commands
python -m rf_mapper start # Start web server (background)
python -m rf_mapper status # Check if running
python -m rf_mapper stop # Stop server
python -m rf_mapper restart # Restart server
python -m rf_mapper scan -l room # CLI scan
python -m rf_mapper --help # All commands
```
## Deployment
See [INVENTORY.md](INVENTORY.md) for multi-node deployment details.
```bash
# Update and restart all nodes
cd ~/git/rf-mapper && source venv/bin/activate && git pull && python -m rf_mapper restart
ssh grokbox "cd ~/git/rf-mapper && source venv/bin/activate && git pull && python -m rf_mapper restart"
ssh jellystar "cd ~/git/rf-mapper && source venv/bin/activate && git pull && python -m rf_mapper restart"
```
## Tech Stack
- Python 3.10+, Flask, PyYAML, requests
- Python 3.10+, Flask, PyYAML, requests, bleak
- Leaflet.js (2D maps), MapLibre GL JS (3D maps)
- Linux tools: `iw`, bleak (BLE via D-Bus)
- SQLite for device history

View File

@@ -122,14 +122,17 @@ pip install -e .
## Quick Start
```bash
# Activate virtual environment
source venv/bin/activate
# Start web server (background)
rf-mapper start
python -m rf_mapper start
# Check status
rf-mapper status
python -m rf_mapper status
# CLI scan
rf-mapper scan
python -m rf_mapper scan
# Open http://localhost:5000
```

View File

@@ -65,7 +65,7 @@ For auto-start on device boot, create `~/.termux/boot/start-rf-mapper.sh`:
termux-wake-lock
cd ~/git/rf-mapper
source venv/bin/activate
rf-mapper start
python -m rf_mapper start
```
Make it executable:
@@ -91,92 +91,97 @@ pip install -e .
source venv/bin/activate
# Run interactive scan
rf-mapper
python -m rf_mapper
# Start web server
rf-mapper start
python -m rf_mapper start
```
## CLI Commands
All commands require activating the virtual environment first:
```bash
source venv/bin/activate
```
### Scanning
```bash
# Basic scan (interactive mode)
rf-mapper
python -m rf_mapper
# Scan with location label
rf-mapper scan -l kitchen
python -m rf_mapper scan -l kitchen
# Scan WiFi only
rf-mapper scan --no-bt
python -m rf_mapper scan --no-bt
# Scan Bluetooth only
rf-mapper scan --no-wifi
python -m rf_mapper scan --no-wifi
# Use specific WiFi interface
rf-mapper scan -i wlan1
python -m rf_mapper scan -i wlan1
```
### Visualization (CLI)
```bash
# Visualize latest scan (ASCII radar + charts)
rf-mapper visualize
python -m rf_mapper visualize
# Visualize specific scan file
rf-mapper visualize -f data/scan_20240131_120000_kitchen.json
python -m rf_mapper visualize -f data/scan_20240131_120000_kitchen.json
# Analyze RF environment
rf-mapper analyze
python -m rf_mapper analyze
```
### Scan History
```bash
# List saved scans
rf-mapper list
python -m rf_mapper list
```
### Web Server
```bash
# Start web server (background daemon)
rf-mapper start
python -m rf_mapper start
# Start in foreground (for debugging)
rf-mapper start --foreground
python -m rf_mapper start --foreground
# Custom host/port
rf-mapper start -H 127.0.0.1 -p 8080
python -m rf_mapper start -H 127.0.0.1 -p 8080
# With debug mode
rf-mapper start --foreground --debug
python -m rf_mapper start --foreground --debug
# With request profiling
rf-mapper start --profile-requests
python -m rf_mapper start --profile-requests
# With request logging
rf-mapper start --log-requests
python -m rf_mapper start --log-requests
# Stop the server
rf-mapper stop
python -m rf_mapper stop
# Restart the server
rf-mapper restart
python -m rf_mapper restart
# Check server status
rf-mapper status
python -m rf_mapper status
```
### Configuration
```bash
# Show current configuration
rf-mapper config
python -m rf_mapper config
# Set GPS coordinates
rf-mapper config --set-gps 50.8585 4.3978 --save
python -m rf_mapper config --set-gps 50.8585 4.3978 --save
# Check Termux prerequisites (Android only)
rf-mapper check-termux
@@ -462,7 +467,7 @@ adb shell "settings put global settings_enable_monitor_phantom_procs false"
### Master Dashboard: Node selector not appearing
1. Verify `is_master: true` in config.yaml
2. Restart rf-mapper: `rf-mapper restart`
2. Restart rf-mapper: `python -m rf_mapper restart`
3. Check peers are registered: `curl http://localhost:5000/api/peers | jq '.peers | length'`
4. At least one peer must be registered for selector to appear

View File

@@ -1,6 +1,6 @@
gps:
latitude: 50.85846541332012
longitude: 4.397570348817993
latitude: 50.858532461583906
longitude: 4.397587773133864
web:
host: 0.0.0.0
port: 5000
@@ -12,7 +12,6 @@ scanner:
longitude: null
floor: null
is_master: true
bt_mac: '2C:CF:67:6F:66:AC'
wifi_interface: wlan0
bt_scan_timeout: 10
path_loss_exponent: 2.5

View File

@@ -6,21 +6,26 @@ Quick reference for RF Mapper commands and configuration.
## CLI Commands
All commands require activating the virtual environment first:
```bash
source venv/bin/activate
```
| Command | Description |
|---------|-------------|
| `rf-mapper` | Interactive scan mode |
| `rf-mapper scan` | Run scan with defaults |
| `rf-mapper scan -l kitchen` | Scan with location label |
| `rf-mapper scan --no-bt` | WiFi only |
| `rf-mapper scan --no-wifi` | Bluetooth only |
| `rf-mapper visualize` | ASCII radar display |
| `rf-mapper analyze` | RF environment analysis |
| `rf-mapper list` | List saved scans |
| `rf-mapper start` | Start web server (background) |
| `rf-mapper stop` | Stop web server |
| `rf-mapper restart` | Restart web server |
| `rf-mapper status` | Check if server is running |
| `rf-mapper config` | Show configuration |
| `python -m rf_mapper` | Interactive scan mode |
| `python -m rf_mapper scan` | Run scan with defaults |
| `python -m rf_mapper scan -l kitchen` | Scan with location label |
| `python -m rf_mapper scan --no-bt` | WiFi only |
| `python -m rf_mapper scan --no-wifi` | Bluetooth only |
| `python -m rf_mapper visualize` | ASCII radar display |
| `python -m rf_mapper analyze` | RF environment analysis |
| `python -m rf_mapper list` | List saved scans |
| `python -m rf_mapper start` | Start web server (background) |
| `python -m rf_mapper stop` | Stop web server |
| `python -m rf_mapper restart` | Restart web server |
| `python -m rf_mapper status` | Check if server is running |
| `python -m rf_mapper config` | Show configuration |
---
@@ -28,18 +33,18 @@ Quick reference for RF Mapper commands and configuration.
```bash
# Lifecycle
rf-mapper start # Start (background daemon)
rf-mapper stop # Stop
rf-mapper restart # Restart
rf-mapper status # Check if running
python -m rf_mapper start # Start (background daemon)
python -m rf_mapper stop # Stop
python -m rf_mapper restart # Restart
python -m rf_mapper status # Check if running
# Start options
rf-mapper start -f # Foreground mode
rf-mapper start -H 127.0.0.1 # Bind to localhost only
rf-mapper start -p 8080 # Custom port
rf-mapper start --debug # Debug mode (requires -f)
rf-mapper start --profile-requests # Per-request profiling
rf-mapper start --log-requests # Request logging
python -m rf_mapper start -f # Foreground mode
python -m rf_mapper start -H 127.0.0.1 # Bind to localhost only
python -m rf_mapper start -p 8080 # Custom port
python -m rf_mapper start --debug # Debug mode (requires -f)
python -m rf_mapper start --profile-requests # Per-request profiling
python -m rf_mapper start --log-requests # Request logging
```
---
@@ -48,10 +53,10 @@ rf-mapper start --log-requests # Request logging
```bash
# Show current config
rf-mapper config
python -m rf_mapper config
# Set GPS coordinates
rf-mapper config --set-gps 50.8585 4.3978 --save
python -m rf_mapper config --set-gps 50.8585 4.3978 --save
```
---

View File

@@ -258,7 +258,7 @@ template:
1. **Enable integration**: Set `home_assistant.enabled: true` in config.yaml
2. **Add HA automations**: Copy webhook automations to HA
3. **Restart RF Mapper**: `rf-mapper restart`
3. **Restart RF Mapper**: `source venv/bin/activate && python -m rf_mapper restart`
4. **Run scan**: Trigger BT scan in RF Mapper web UI
5. **Check HA**: Verify `device_tracker.rf_*` entities appear
6. **Test new device**: Clear device from DB, re-scan, verify notification

View File

@@ -340,6 +340,11 @@ class DeviceDatabase:
cursor = conn.cursor()
timestamp = datetime.now().isoformat()
# Skip if this is a known scanner's BT MAC (don't record scanners as devices)
cursor.execute("SELECT 1 FROM peers WHERE UPPER(bt_mac) = UPPER(?)", (address,))
if cursor.fetchone():
return # Skip scanner device
# Get previous observation for movement detection
cursor.execute("""
SELECT rssi, distance_m, timestamp FROM rssi_history

View File

@@ -0,0 +1,212 @@
"""Import BLE Radar database into RF Mapper.
BLE Radar is an Android app that scans for BLE devices.
This script imports its exported SQLite database into rf-mapper's database.
Usage:
python -m rf_mapper.import_ble_radar /path/to/ble_radar.sqlite
Or from Python:
from rf_mapper.import_ble_radar import import_ble_radar_db
import_ble_radar_db('/path/to/ble_radar.sqlite')
"""
import argparse
import sqlite3
from datetime import datetime
from pathlib import Path
from typing import Optional
from .database import DeviceDatabase, get_database
from .distance import estimate_distance
def import_ble_radar_db(
ble_radar_path: str,
rf_mapper_db: Optional[DeviceDatabase] = None,
scanner_id: str = "ble_radar",
verbose: bool = True
) -> dict:
"""Import BLE Radar database into RF Mapper.
Args:
ble_radar_path: Path to BLE Radar exported SQLite database
rf_mapper_db: RF Mapper database instance (creates default if None)
scanner_id: Source scanner ID for imported devices
verbose: Print progress messages
Returns:
Dict with import statistics
"""
ble_radar_path = Path(ble_radar_path)
if not ble_radar_path.exists():
raise FileNotFoundError(f"BLE Radar database not found: {ble_radar_path}")
# Connect to BLE Radar database
ble_conn = sqlite3.connect(ble_radar_path)
ble_conn.row_factory = sqlite3.Row
ble_cursor = ble_conn.cursor()
# Use global RF Mapper database if not provided
if rf_mapper_db is None:
rf_mapper_db = get_database()
stats = {
"devices_imported": 0,
"devices_updated": 0,
"rssi_records": 0,
"locations_used": 0,
"errors": 0
}
if verbose:
print(f"Importing from: {ble_radar_path}")
# Get scanner BT MACs to filter out (don't import scanners as devices)
scanner_bt_macs = set()
peers = rf_mapper_db.get_peers()
for peer in peers:
if peer.get("bt_mac"):
scanner_bt_macs.add(peer["bt_mac"].upper())
if verbose and scanner_bt_macs:
print(f"Filtering {len(scanner_bt_macs)} scanner BT MAC(s)")
stats["scanners_filtered"] = 0
# Get location data for device-location mapping
ble_cursor.execute("SELECT time, lat, lng FROM location ORDER BY time")
locations = {row["time"]: (row["lat"], row["lng"]) for row in ble_cursor.fetchall()}
stats["locations_used"] = len(locations)
if verbose:
print(f"Found {len(locations)} location records")
# Import devices
ble_cursor.execute("""
SELECT
address, name, manufacturer_name, manufacturer_id,
first_detect_time_ms, last_detect_time_ms, detect_count,
last_seen_rssi, favorite, custom_name, service_uuids,
device_class, is_connectable
FROM device
""")
scan_id = f"ble_radar_import_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
for row in ble_cursor.fetchall():
try:
address = row["address"]
# Skip scanner devices (don't import scanners as regular devices)
if address.upper() in scanner_bt_macs:
stats["scanners_filtered"] += 1
if verbose:
print(f"Skipping scanner: {address}")
continue
name = row["custom_name"] or row["name"] or ""
manufacturer = row["manufacturer_name"] or ""
rssi = row["last_seen_rssi"] or -70
# Determine device type from BLE Radar's device_class
device_class_num = row["device_class"] or 0
if device_class_num == 0:
bt_device_type = "Low Energy Device"
else:
bt_device_type = f"BLE Class {device_class_num}"
# Check if device exists
existing = rf_mapper_db.get_device(address)
# Calculate distance from RSSI
distance = estimate_distance(rssi, tx_power=-65)
# Record the observation (inserts or updates device)
rf_mapper_db.record_bluetooth_observation(
address=address,
name=name,
rssi=rssi,
distance_m=distance,
device_class="BLE",
device_type=bt_device_type,
manufacturer=manufacturer,
floor=None,
scan_id=scan_id,
scanner_id=scanner_id
)
if existing:
stats["devices_updated"] += 1
else:
stats["devices_imported"] += 1
stats["rssi_records"] += 1
# Set favorite if marked in BLE Radar
if row["favorite"]:
rf_mapper_db.set_device_favorite(address, True)
# Set custom label if set in BLE Radar
if row["custom_name"]:
rf_mapper_db.set_device_label(address, row["custom_name"])
# Set source scanner
rf_mapper_db.set_device_source(address, scanner_id, None, None)
except Exception as e:
if verbose:
print(f"Error importing {row['address']}: {e}")
stats["errors"] += 1
ble_conn.close()
if verbose:
print(f"\nImport complete:")
print(f" Devices imported: {stats['devices_imported']}")
print(f" Devices updated: {stats['devices_updated']}")
print(f" RSSI records: {stats['rssi_records']}")
print(f" Locations used: {stats['locations_used']}")
if stats.get("scanners_filtered"):
print(f" Scanners skipped: {stats['scanners_filtered']}")
if stats["errors"]:
print(f" Errors: {stats['errors']}")
return stats
def main():
parser = argparse.ArgumentParser(
description="Import BLE Radar database into RF Mapper"
)
parser.add_argument(
"ble_radar_db",
help="Path to BLE Radar exported SQLite database"
)
parser.add_argument(
"--scanner-id",
default="ble_radar",
help="Source scanner ID for imported devices (default: ble_radar)"
)
parser.add_argument(
"-q", "--quiet",
action="store_true",
help="Suppress output"
)
args = parser.parse_args()
try:
stats = import_ble_radar_db(
args.ble_radar_db,
scanner_id=args.scanner_id,
verbose=not args.quiet
)
return 0 if stats["errors"] == 0 else 1
except Exception as e:
print(f"Error: {e}")
return 1
if __name__ == "__main__":
exit(main())

View File

@@ -666,10 +666,17 @@ def create_app(config: Config | None = None) -> Flask:
with open(scan_files[0]) as f:
scan = json.load(f)
# Get saved floor assignments from database
# Get saved floor assignments and scanner BT MACs from database
db = app.config.get("DATABASE")
saved_floors = db.get_all_device_floors() if db else {}
# Get scanner BT MACs to filter out
scanner_bt_macs = set()
if db:
for peer in db.get_peers():
if peer.get("bt_mac"):
scanner_bt_macs.add(peer["bt_mac"].upper())
# Enrich with distance estimates and saved floor assignments
for net in scan.get("wifi_networks", []):
net["estimated_distance_m"] = round(estimate_distance(net["rssi"]), 2)
@@ -678,12 +685,18 @@ def create_app(config: Config | None = None) -> Flask:
if "height_m" not in net:
net["height_m"] = None
# Filter out scanner devices and enrich BT devices
filtered_bt = []
for dev in scan.get("bluetooth_devices", []):
address = dev.get("address", "").upper()
if address in scanner_bt_macs:
continue # Skip scanner devices
dev["estimated_distance_m"] = round(estimate_distance(dev["rssi"], tx_power=-65), 2)
address = dev.get("address")
dev["floor"] = saved_floors.get(address) if address else dev.get("floor")
dev["floor"] = saved_floors.get(dev.get("address")) if dev.get("address") else dev.get("floor")
if "height_m" not in dev:
dev["height_m"] = None
filtered_bt.append(dev)
scan["bluetooth_devices"] = filtered_bt
scan["gps"] = {
"lat": app.config["CURRENT_LAT"],
@@ -1517,6 +1530,41 @@ def create_app(config: Config | None = None) -> Flask:
result = db.cleanup_old_data(retention_days)
return jsonify(result)
@app.route("/api/import/ble-radar", methods=["POST"])
def api_import_ble_radar():
"""Import BLE Radar database from uploaded file"""
db = app.config.get("DATABASE")
if not db:
return jsonify({"error": "Database not enabled"}), 503
if "file" not in request.files:
return jsonify({"error": "No file uploaded"}), 400
file = request.files["file"]
if file.filename == "":
return jsonify({"error": "No file selected"}), 400
# Save to temp file
import tempfile
with tempfile.NamedTemporaryFile(delete=False, suffix=".sqlite") as tmp:
file.save(tmp.name)
tmp_path = tmp.name
try:
from ..import_ble_radar import import_ble_radar_db
scanner_id = request.form.get("scanner_id", "ble_radar")
stats = import_ble_radar_db(tmp_path, db, scanner_id, verbose=False)
return jsonify({
"status": "success",
"message": f"Imported {stats['devices_imported']} new devices, updated {stats['devices_updated']}",
**stats
})
except Exception as e:
return jsonify({"error": str(e)}), 500
finally:
import os
os.unlink(tmp_path)
# ==================== Peer Sync API ====================
@app.route("/api/peers", methods=["GET"])

View File

@@ -615,6 +615,37 @@ body {
border-color: var(--color-primary);
}
/* Import Controls */
.import-controls {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.import-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.import-row label {
font-size: 0.8rem;
color: var(--color-text-muted);
}
.import-row input[type="file"] {
font-size: 0.75rem;
color: var(--color-text-muted);
max-width: 140px;
}
#import-status {
font-size: 0.75rem;
margin-left: 0.5rem;
}
/* Device Detail Panel */
.device-detail-panel {
position: absolute;

View File

@@ -1537,6 +1537,52 @@ async function stopAutoScan() {
}
}
// ========== Import Functions ==========
// Import BLE Radar database
async function importBleRadar() {
const fileInput = document.getElementById('ble-radar-file');
const statusEl = document.getElementById('import-status');
if (!fileInput.files || !fileInput.files[0]) {
statusEl.textContent = 'No file selected';
statusEl.style.color = '#ef4444';
return;
}
const file = fileInput.files[0];
statusEl.textContent = 'Importing...';
statusEl.style.color = '#fbbf24';
const formData = new FormData();
formData.append('file', file);
formData.append('scanner_id', activeNode === 'local' ? 'ble_radar' : activeNode);
try {
const response = await fetch('/api/import/ble-radar', {
method: 'POST',
body: formData
});
const data = await response.json();
if (response.ok) {
statusEl.textContent = `${data.devices_imported} new, ${data.devices_updated} updated`;
statusEl.style.color = '#4ade80';
fileInput.value = '';
// Refresh the scan to show imported devices
loadLatestScan();
} else {
statusEl.textContent = `Error: ${data.error}`;
statusEl.style.color = '#ef4444';
}
} catch (error) {
statusEl.textContent = `Error: ${error.message}`;
statusEl.style.color = '#ef4444';
console.error('Import error:', error);
}
}
// ========== Device Position Functions ==========
// Load saved device positions, source scanner info, and peer positions

View File

@@ -127,6 +127,22 @@
</div>
</div>
<div class="section">
<div class="section-header">
<span class="section-title">📥 Import Data</span>
</div>
<div class="import-controls">
<div class="import-row">
<label>BLE Radar Database:</label>
<input type="file" id="ble-radar-file" accept=".sqlite,.db">
</div>
<div class="import-row">
<button class="btn btn-small" onclick="importBleRadar()">Import</button>
<span id="import-status"></span>
</div>
</div>
</div>
<div class="section">
<div class="section-header">
<span class="section-title">📊 Statistics</span>