Add host network info display and custom subnet scan to Receivers tab
Shows the server's network interfaces (IP, subnet) in the admin Receivers tab so users know which networks are being scanned. Adds an optional subnet input field to scan arbitrary networks (e.g. VLANs the server can route to but isn't directly attached to), with validation and a /20 size cap. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -325,6 +325,35 @@ textarea {
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#network-info-box {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-info-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-info-table th,
|
||||||
|
.network-info-table td {
|
||||||
|
text-align: left;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-bottom: 1px solid rgba(92, 148, 252, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-info-table th {
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 8px;
|
||||||
|
color: #b4d4ec;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-info-table td {
|
||||||
|
color: #54fc54;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
/* Toast Notification */
|
/* Toast Notification */
|
||||||
.toast {
|
.toast {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
@@ -76,6 +76,10 @@
|
|||||||
<!-- Receivers Tab -->
|
<!-- Receivers Tab -->
|
||||||
<section id="tab-receivers" class="tab-pane hidden">
|
<section id="tab-receivers" class="tab-pane hidden">
|
||||||
<h2>Receiver Configuration</h2>
|
<h2>Receiver Configuration</h2>
|
||||||
|
<div id="network-info-box" class="info-box">
|
||||||
|
<h3>Host Network</h3>
|
||||||
|
<div id="network-info">Loading...</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Discovery Mode</label>
|
<label>Discovery Mode</label>
|
||||||
<select id="receiver-mode">
|
<select id="receiver-mode">
|
||||||
@@ -91,6 +95,10 @@
|
|||||||
<label>Receiver Port</label>
|
<label>Receiver Port</label>
|
||||||
<input type="number" id="receiver-port" value="30003" min="1" max="65535">
|
<input type="number" id="receiver-port" value="30003" min="1" max="65535">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Custom Subnet (optional)</label>
|
||||||
|
<input type="text" id="scan-subnet" placeholder="e.g. 10.0.0.0/24 (leave empty to scan all local subnets)">
|
||||||
|
</div>
|
||||||
<div class="button-row">
|
<div class="button-row">
|
||||||
<button id="scan-receivers-btn" class="btn">SCAN NOW</button>
|
<button id="scan-receivers-btn" class="btn">SCAN NOW</button>
|
||||||
<button id="restart-receivers-btn" class="btn">RESTART CONNECTIONS</button>
|
<button id="restart-receivers-btn" class="btn">RESTART CONNECTIONS</button>
|
||||||
|
|||||||
+36
-2
@@ -27,6 +27,7 @@
|
|||||||
adminPanel.classList.remove('hidden');
|
adminPanel.classList.remove('hidden');
|
||||||
loadConfig();
|
loadConfig();
|
||||||
loadThemes();
|
loadThemes();
|
||||||
|
loadNetworkInfo();
|
||||||
refreshStatus();
|
refreshStatus();
|
||||||
statusInterval = setInterval(refreshStatus, 5000);
|
statusInterval = setInterval(refreshStatus, 5000);
|
||||||
}
|
}
|
||||||
@@ -170,6 +171,31 @@
|
|||||||
} catch (e) { /* ignore */ }
|
} catch (e) { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------- Network Info -------
|
||||||
|
async function loadNetworkInfo() {
|
||||||
|
const el = document.getElementById('network-info');
|
||||||
|
if (!el) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/admin/network-info');
|
||||||
|
if (!res.ok) { el.textContent = 'Failed to load'; return; }
|
||||||
|
const data = await res.json();
|
||||||
|
const ifaces = data.interfaces || [];
|
||||||
|
if (!ifaces.length) {
|
||||||
|
el.textContent = 'No network interfaces found';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let html = '<table class="network-info-table">';
|
||||||
|
html += '<tr><th>Interface</th><th>IP Address</th><th>Network</th></tr>';
|
||||||
|
ifaces.forEach(i => {
|
||||||
|
html += `<tr><td>${i.name}</td><td>${i.ip}</td><td>${i.network}</td></tr>`;
|
||||||
|
});
|
||||||
|
html += '</table>';
|
||||||
|
el.innerHTML = html;
|
||||||
|
} catch (e) {
|
||||||
|
el.textContent = 'Error loading network info';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ------- Receivers -------
|
// ------- Receivers -------
|
||||||
function toggleManualIps(mode) {
|
function toggleManualIps(mode) {
|
||||||
document.getElementById('manual-ips-group').classList.toggle('hidden', mode === 'AUTO');
|
document.getElementById('manual-ips-group').classList.toggle('hidden', mode === 'AUTO');
|
||||||
@@ -184,12 +210,20 @@
|
|||||||
btn.textContent = 'SCANNING...';
|
btn.textContent = 'SCANNING...';
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/admin/scan-receivers', { method: 'POST' });
|
const subnet = (document.getElementById('scan-subnet').value || '').trim();
|
||||||
|
const fetchOpts = { method: 'POST' };
|
||||||
|
if (subnet) {
|
||||||
|
fetchOpts.headers = { 'Content-Type': 'application/json' };
|
||||||
|
fetchOpts.body = JSON.stringify({ subnet });
|
||||||
|
}
|
||||||
|
const res = await fetch('/api/admin/scan-receivers', fetchOpts);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const box = document.getElementById('scan-results');
|
const box = document.getElementById('scan-results');
|
||||||
const list = document.getElementById('scan-results-list');
|
const list = document.getElementById('scan-results-list');
|
||||||
box.classList.remove('hidden');
|
box.classList.remove('hidden');
|
||||||
if (data.receivers && data.receivers.length) {
|
if (data.error) {
|
||||||
|
list.textContent = data.error;
|
||||||
|
} else if (data.receivers && data.receivers.length) {
|
||||||
list.textContent = data.receivers.join(', ');
|
list.textContent = data.receivers.join(', ');
|
||||||
} else {
|
} else {
|
||||||
list.textContent = 'No receivers found on network';
|
list.textContent = 'No receivers found on network';
|
||||||
|
|||||||
@@ -302,6 +302,25 @@ async def connect_to_receiver(host: str, port: int = 30003):
|
|||||||
await asyncio.sleep(reconnect_delay)
|
await asyncio.sleep(reconnect_delay)
|
||||||
|
|
||||||
|
|
||||||
|
async def _scan_subnet(network, port):
|
||||||
|
"""Scan a single subnet for hosts with an open port. Returns list of IPs."""
|
||||||
|
found = []
|
||||||
|
print(f"Scanning {network} ({network.num_addresses} hosts) on port {port}...")
|
||||||
|
host_ips = [str(host) for host in network.hosts()]
|
||||||
|
|
||||||
|
async def check_ip(test_ip):
|
||||||
|
if await test_port(test_ip, port):
|
||||||
|
return test_ip
|
||||||
|
return None
|
||||||
|
|
||||||
|
results = await asyncio.gather(*[check_ip(h) for h in host_ips])
|
||||||
|
for result in results:
|
||||||
|
if result:
|
||||||
|
print(f"Found receiver at {result}:{port}")
|
||||||
|
found.append(result)
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
async def scan_for_receivers():
|
async def scan_for_receivers():
|
||||||
"""Scan local network for ADS-B receivers on configured port"""
|
"""Scan local network for ADS-B receivers on configured port"""
|
||||||
port = config['receiver_port']
|
port = config['receiver_port']
|
||||||
@@ -317,20 +336,7 @@ async def scan_for_receivers():
|
|||||||
if ip and netmask and not ip.startswith('127.'):
|
if ip and netmask and not ip.startswith('127.'):
|
||||||
try:
|
try:
|
||||||
network = ipaddress.IPv4Network(f"{ip}/{netmask}", strict=False)
|
network = ipaddress.IPv4Network(f"{ip}/{netmask}", strict=False)
|
||||||
print(f"Scanning {network} ({network.num_addresses} hosts)...")
|
found.extend(await _scan_subnet(network, port))
|
||||||
|
|
||||||
host_ips = [str(host) for host in network.hosts()]
|
|
||||||
|
|
||||||
async def check_ip(test_ip):
|
|
||||||
if await test_port(test_ip, port):
|
|
||||||
return test_ip
|
|
||||||
return None
|
|
||||||
|
|
||||||
results = await asyncio.gather(*[check_ip(h) for h in host_ips])
|
|
||||||
for result in results:
|
|
||||||
if result:
|
|
||||||
print(f"Found receiver at {result}:{port}")
|
|
||||||
found.append(result)
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
print(f"Invalid network {ip}/{netmask}: {e}")
|
print(f"Invalid network {ip}/{netmask}: {e}")
|
||||||
|
|
||||||
@@ -856,10 +862,58 @@ async def handle_admin_password(request):
|
|||||||
return web.json_response({"ok": True})
|
return web.json_response({"ok": True})
|
||||||
|
|
||||||
|
|
||||||
|
@require_auth
|
||||||
|
async def handle_admin_network_info(request):
|
||||||
|
"""GET /api/admin/network-info - Return the server's network interfaces."""
|
||||||
|
interfaces = []
|
||||||
|
for iface in netifaces.interfaces():
|
||||||
|
addrs = netifaces.ifaddresses(iface)
|
||||||
|
if netifaces.AF_INET in addrs:
|
||||||
|
for addr in addrs[netifaces.AF_INET]:
|
||||||
|
ip = addr.get('addr')
|
||||||
|
netmask = addr.get('netmask')
|
||||||
|
if ip and netmask and not ip.startswith('127.'):
|
||||||
|
try:
|
||||||
|
network = ipaddress.IPv4Network(f"{ip}/{netmask}", strict=False)
|
||||||
|
interfaces.append({
|
||||||
|
"name": iface,
|
||||||
|
"ip": ip,
|
||||||
|
"netmask": netmask,
|
||||||
|
"network": str(network)
|
||||||
|
})
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return web.json_response({"interfaces": interfaces})
|
||||||
|
|
||||||
|
|
||||||
@require_auth
|
@require_auth
|
||||||
async def handle_admin_scan_receivers(request):
|
async def handle_admin_scan_receivers(request):
|
||||||
"""POST /api/admin/scan-receivers - Trigger a receiver scan."""
|
"""POST /api/admin/scan-receivers - Trigger a receiver scan."""
|
||||||
|
# Check for optional subnet in request body
|
||||||
|
subnet = None
|
||||||
|
try:
|
||||||
|
body = await request.json()
|
||||||
|
subnet = body.get("subnet")
|
||||||
|
except:
|
||||||
|
pass # No body or invalid JSON — fall back to full scan
|
||||||
|
|
||||||
|
if subnet:
|
||||||
|
# Validate and scan a specific subnet
|
||||||
|
try:
|
||||||
|
network = ipaddress.IPv4Network(subnet, strict=False)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return web.json_response(
|
||||||
|
{"error": f"Invalid subnet: {subnet}"}, status=400)
|
||||||
|
|
||||||
|
if network.prefixlen < 20:
|
||||||
|
return web.json_response(
|
||||||
|
{"error": "Subnet too large (max /20 = 4096 hosts)"}, status=400)
|
||||||
|
|
||||||
|
port = config['receiver_port']
|
||||||
|
found = await _scan_subnet(network, port)
|
||||||
|
else:
|
||||||
found = await scan_for_receivers()
|
found = await scan_for_receivers()
|
||||||
|
|
||||||
return web.json_response({"receivers": found})
|
return web.json_response({"receivers": found})
|
||||||
|
|
||||||
|
|
||||||
@@ -1046,6 +1100,7 @@ async def start_http_server():
|
|||||||
app.router.add_put('/api/admin/themes/{id}', handle_admin_theme_rename)
|
app.router.add_put('/api/admin/themes/{id}', handle_admin_theme_rename)
|
||||||
app.router.add_post('/api/admin/themes/{id}/{direction}', handle_admin_theme_upload)
|
app.router.add_post('/api/admin/themes/{id}/{direction}', handle_admin_theme_upload)
|
||||||
app.router.add_post('/api/admin/password', handle_admin_password)
|
app.router.add_post('/api/admin/password', handle_admin_password)
|
||||||
|
app.router.add_get('/api/admin/network-info', handle_admin_network_info)
|
||||||
app.router.add_post('/api/admin/scan-receivers', handle_admin_scan_receivers)
|
app.router.add_post('/api/admin/scan-receivers', handle_admin_scan_receivers)
|
||||||
app.router.add_post('/api/admin/restart-receivers', handle_admin_restart_receivers)
|
app.router.add_post('/api/admin/restart-receivers', handle_admin_restart_receivers)
|
||||||
app.router.add_get('/api/admin/sprites', handle_admin_sprites)
|
app.router.add_get('/api/admin/sprites', handle_admin_sprites)
|
||||||
|
|||||||
Reference in New Issue
Block a user