mirror of
https://github.com/smittix/intercept.git
synced 2026-06-10 23:13:31 -07:00
Enhance distributed agent architecture with full mode support and reliability
Agent improvements: - Add process verification (0.5s delay + poll check) for sensor, pager, APRS, DSC modes - Prevents silent failures when SDR is busy or tools fail to start - Returns clear error messages when subprocess exits immediately Frontend agent integration: - Add agent routing to all SDR modes (pager, sensor, RTLAMR, APRS, listening post, TSCM) - Add agent routing to WiFi and Bluetooth modes with polling fallback - Add agent routing to AIS and DSC dashboards - Implement "Show All Agents" toggle for Bluetooth mode - Add agent badges to device/network lists - Handle controller proxy response format (nested 'result' field) Controller enhancements: - Add running_modes_detail endpoint showing device info per mode - Support SDR conflict detection across modes Documentation: - Expand DISTRIBUTED_AGENTS.md with complete API reference - Add troubleshooting guide and security considerations - Document all supported modes with tools and data formats UI/CSS: - Add agent badge styling for remote vs local sources - Add WiFi and Bluetooth table agent columns
This commit is contained in:
+885
-105
File diff suppressed because it is too large
Load Diff
+575
-22
@@ -21,6 +21,16 @@
|
||||
<span>// INTERCEPT - AIS Tracking</span>
|
||||
</div>
|
||||
<div class="status-bar">
|
||||
<!-- Agent Selector -->
|
||||
<div class="agent-selector-compact" id="agentSection">
|
||||
<select id="agentSelect" class="agent-select-sm" title="Select signal source">
|
||||
<option value="local">Local</option>
|
||||
</select>
|
||||
<span class="agent-status-dot online" id="agentStatusDot"></span>
|
||||
<label class="show-all-label" title="Show vessels from all agents on map">
|
||||
<input type="checkbox" id="showAllAgents" onchange="toggleShowAllAgents()"> All
|
||||
</label>
|
||||
</div>
|
||||
<a href="/" class="back-link">Main Dashboard</a>
|
||||
</div>
|
||||
</header>
|
||||
@@ -173,6 +183,7 @@
|
||||
let markers = {};
|
||||
let selectedMmsi = null;
|
||||
let eventSource = null;
|
||||
let aisPollTimer = null; // Polling fallback for agent mode
|
||||
let isTracking = false;
|
||||
|
||||
// DSC State
|
||||
@@ -181,6 +192,8 @@
|
||||
let dscMessages = {};
|
||||
let dscMarkers = {};
|
||||
let dscAlertCounts = { distress: 0, urgency: 0 };
|
||||
let dscCurrentAgent = null;
|
||||
let dscPollTimer = null;
|
||||
let showTrails = false;
|
||||
let vesselTrails = {};
|
||||
let trailLines = {};
|
||||
@@ -490,6 +503,40 @@
|
||||
const device = document.getElementById('aisDeviceSelect').value;
|
||||
const gain = document.getElementById('aisGain').value;
|
||||
|
||||
// Check if using agent mode
|
||||
const useAgent = typeof aisCurrentAgent !== 'undefined' && aisCurrentAgent !== 'local';
|
||||
|
||||
// For agent mode, check conflicts and route through proxy
|
||||
if (useAgent) {
|
||||
if (typeof checkAgentModeConflict === 'function' && !checkAgentModeConflict('ais')) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/controller/agents/${aisCurrentAgent}/ais/start`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ device, gain })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(result => {
|
||||
const data = result.result || result;
|
||||
if (data.status === 'started' || data.status === 'already_running') {
|
||||
isTracking = true;
|
||||
document.getElementById('startBtn').textContent = 'STOP';
|
||||
document.getElementById('startBtn').classList.add('active');
|
||||
document.getElementById('trackingDot').classList.add('active');
|
||||
document.getElementById('trackingStatus').textContent = 'TRACKING';
|
||||
startSessionTimer();
|
||||
startSSE();
|
||||
} else {
|
||||
alert(data.message || 'Failed to start');
|
||||
}
|
||||
})
|
||||
.catch(err => alert('Error: ' + err.message));
|
||||
return;
|
||||
}
|
||||
|
||||
// Local mode - original behavior unchanged
|
||||
fetch('/ais/start', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -513,7 +560,12 @@
|
||||
}
|
||||
|
||||
function stopTracking() {
|
||||
fetch('/ais/stop', { method: 'POST' })
|
||||
const useAgent = typeof aisCurrentAgent !== 'undefined' && aisCurrentAgent !== 'local';
|
||||
|
||||
// Route to agent or local
|
||||
const url = useAgent ? `/controller/agents/${aisCurrentAgent}/ais/stop` : '/ais/stop';
|
||||
|
||||
fetch(url, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(() => {
|
||||
isTracking = false;
|
||||
@@ -527,18 +579,107 @@
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
if (aisPollTimer) {
|
||||
clearInterval(aisPollTimer);
|
||||
aisPollTimer = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start polling agent data as fallback when push isn't enabled.
|
||||
*/
|
||||
function startAisPolling() {
|
||||
if (aisPollTimer) return;
|
||||
if (typeof aisCurrentAgent === 'undefined' || aisCurrentAgent === 'local') return;
|
||||
|
||||
const pollInterval = 2000; // 2 seconds for AIS
|
||||
console.log('Starting AIS agent polling fallback...');
|
||||
|
||||
aisPollTimer = setInterval(async () => {
|
||||
if (!isTracking) {
|
||||
clearInterval(aisPollTimer);
|
||||
aisPollTimer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/controller/agents/${aisCurrentAgent}/ais/data`);
|
||||
if (!response.ok) return;
|
||||
|
||||
const result = await response.json();
|
||||
const data = result.data || result;
|
||||
|
||||
// Get agent name
|
||||
let agentName = 'Agent';
|
||||
if (typeof agents !== 'undefined') {
|
||||
const agent = agents.find(a => a.id == aisCurrentAgent);
|
||||
if (agent) agentName = agent.name;
|
||||
}
|
||||
|
||||
// Process vessels from polling response
|
||||
if (data && data.vessels) {
|
||||
Object.values(data.vessels).forEach(vessel => {
|
||||
vessel._agent = agentName;
|
||||
updateVessel(vessel);
|
||||
});
|
||||
} else if (data && Array.isArray(data)) {
|
||||
data.forEach(vessel => {
|
||||
vessel._agent = agentName;
|
||||
updateVessel(vessel);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.debug('AIS agent poll error:', err);
|
||||
}
|
||||
}, pollInterval);
|
||||
}
|
||||
|
||||
function startSSE() {
|
||||
if (eventSource) eventSource.close();
|
||||
|
||||
eventSource = new EventSource('/ais/stream');
|
||||
const useAgent = typeof aisCurrentAgent !== 'undefined' && aisCurrentAgent !== 'local';
|
||||
const streamUrl = useAgent ? '/controller/stream/all' : '/ais/stream';
|
||||
|
||||
// Get agent name for filtering
|
||||
let targetAgentName = null;
|
||||
if (useAgent && typeof agents !== 'undefined') {
|
||||
const agent = agents.find(a => a.id == aisCurrentAgent);
|
||||
targetAgentName = agent ? agent.name : null;
|
||||
}
|
||||
|
||||
eventSource = new EventSource(streamUrl);
|
||||
eventSource.onmessage = function(e) {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'vessel') {
|
||||
updateVessel(data);
|
||||
|
||||
if (useAgent) {
|
||||
// Multi-agent stream format
|
||||
if (data.type === 'keepalive') return;
|
||||
|
||||
// Filter to our agent
|
||||
if (targetAgentName && data.agent_name && data.agent_name !== targetAgentName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract vessel data from push payload
|
||||
if (data.scan_type === 'ais' && data.payload) {
|
||||
const payload = data.payload;
|
||||
if (payload.vessels) {
|
||||
Object.values(payload.vessels).forEach(v => {
|
||||
v._agent = data.agent_name;
|
||||
updateVessel({ type: 'vessel', ...v });
|
||||
});
|
||||
} else if (payload.mmsi) {
|
||||
payload._agent = data.agent_name;
|
||||
updateVessel({ type: 'vessel', ...payload });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Local stream format
|
||||
if (data.type === 'vessel') {
|
||||
updateVessel(data);
|
||||
}
|
||||
}
|
||||
} catch (err) {}
|
||||
};
|
||||
@@ -731,12 +872,13 @@
|
||||
container.innerHTML = vesselArray.map(v => {
|
||||
const iconSvg = getShipIconSvg(v.ship_type, 20);
|
||||
const category = getShipCategory(v.ship_type);
|
||||
const agentBadge = v._agent ? `<span class="agent-badge">${v._agent}</span>` : '';
|
||||
return `
|
||||
<div class="vessel-item ${v.mmsi === selectedMmsi ? 'selected' : ''}"
|
||||
data-mmsi="${v.mmsi}" onclick="selectVessel('${v.mmsi}')">
|
||||
<div class="vessel-item-icon">${iconSvg}</div>
|
||||
<div class="vessel-item-info">
|
||||
<div class="vessel-item-name">${v.name || 'Unknown'}</div>
|
||||
<div class="vessel-item-name">${v.name || 'Unknown'}${agentBadge}</div>
|
||||
<div class="vessel-item-type">${category} | ${v.mmsi}</div>
|
||||
</div>
|
||||
<div class="vessel-item-speed">${v.speed ? v.speed + ' kt' : '-'}</div>
|
||||
@@ -881,33 +1023,51 @@
|
||||
const device = document.getElementById('dscDeviceSelect').value;
|
||||
const gain = document.getElementById('dscGain').value;
|
||||
|
||||
fetch('/dsc/start', {
|
||||
// Check if using agent mode
|
||||
const isAgentMode = typeof aisCurrentAgent !== 'undefined' && aisCurrentAgent !== 'local';
|
||||
dscCurrentAgent = isAgentMode ? aisCurrentAgent : null;
|
||||
|
||||
// Determine endpoint based on agent mode
|
||||
const endpoint = isAgentMode
|
||||
? `/controller/agents/${aisCurrentAgent}/dsc/start`
|
||||
: '/dsc/start';
|
||||
|
||||
fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ device, gain })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'started') {
|
||||
// Handle controller proxy response format
|
||||
const scanResult = isAgentMode && data.result ? data.result : data;
|
||||
|
||||
if (scanResult.status === 'started' || scanResult.status === 'success') {
|
||||
isDscTracking = true;
|
||||
document.getElementById('dscStartBtn').textContent = 'STOP DSC';
|
||||
document.getElementById('dscStartBtn').classList.add('active');
|
||||
document.getElementById('dscIndicator').classList.add('active');
|
||||
startDscSSE();
|
||||
} else if (data.error_type === 'DEVICE_BUSY') {
|
||||
alert('SDR device is busy.\n\n' + data.suggestion);
|
||||
startDscSSE(isAgentMode);
|
||||
} else if (scanResult.error_type === 'DEVICE_BUSY') {
|
||||
alert('SDR device is busy.\n\n' + (scanResult.suggestion || ''));
|
||||
} else {
|
||||
alert(data.message || 'Failed to start DSC');
|
||||
alert(scanResult.message || scanResult.error || 'Failed to start DSC');
|
||||
}
|
||||
})
|
||||
.catch(err => alert('Error: ' + err.message));
|
||||
}
|
||||
|
||||
function stopDscTracking() {
|
||||
fetch('/dsc/stop', { method: 'POST' })
|
||||
const isAgentMode = dscCurrentAgent !== null;
|
||||
const endpoint = isAgentMode
|
||||
? `/controller/agents/${dscCurrentAgent}/dsc/stop`
|
||||
: '/dsc/stop';
|
||||
|
||||
fetch(endpoint, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(() => {
|
||||
isDscTracking = false;
|
||||
dscCurrentAgent = null;
|
||||
document.getElementById('dscStartBtn').textContent = 'START DSC';
|
||||
document.getElementById('dscStartBtn').classList.remove('active');
|
||||
document.getElementById('dscIndicator').classList.remove('active');
|
||||
@@ -915,23 +1075,50 @@
|
||||
dscEventSource.close();
|
||||
dscEventSource = null;
|
||||
}
|
||||
// Clear polling timer
|
||||
if (dscPollTimer) {
|
||||
clearInterval(dscPollTimer);
|
||||
dscPollTimer = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function startDscSSE() {
|
||||
function startDscSSE(isAgentMode = false) {
|
||||
if (dscEventSource) dscEventSource.close();
|
||||
|
||||
dscEventSource = new EventSource('/dsc/stream');
|
||||
// Use different stream endpoint for agent mode
|
||||
const streamUrl = isAgentMode ? '/controller/stream/all' : '/dsc/stream';
|
||||
dscEventSource = new EventSource(streamUrl);
|
||||
|
||||
dscEventSource.onmessage = function(e) {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'dsc_message') {
|
||||
handleDscMessage(data);
|
||||
} else if (data.type === 'error') {
|
||||
console.error('DSC error:', data.error);
|
||||
if (data.error_type === 'DEVICE_BUSY') {
|
||||
alert('DSC: Device became busy. ' + (data.suggestion || ''));
|
||||
stopDscTracking();
|
||||
|
||||
if (isAgentMode) {
|
||||
// Handle multi-agent stream format
|
||||
if (data.scan_type === 'dsc' && data.payload) {
|
||||
const payload = data.payload;
|
||||
if (payload.type === 'dsc_message') {
|
||||
payload.agent_name = data.agent_name;
|
||||
handleDscMessage(payload);
|
||||
} else if (payload.type === 'error') {
|
||||
console.error('DSC error:', payload.error);
|
||||
if (payload.error_type === 'DEVICE_BUSY') {
|
||||
alert('DSC: Device became busy. ' + (payload.suggestion || ''));
|
||||
stopDscTracking();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Local stream format
|
||||
if (data.type === 'dsc_message') {
|
||||
handleDscMessage(data);
|
||||
} else if (data.type === 'error') {
|
||||
console.error('DSC error:', data.error);
|
||||
if (data.error_type === 'DEVICE_BUSY') {
|
||||
alert('DSC: Device became busy. ' + (data.suggestion || ''));
|
||||
stopDscTracking();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {}
|
||||
@@ -939,9 +1126,56 @@
|
||||
|
||||
dscEventSource.onerror = function() {
|
||||
setTimeout(() => {
|
||||
if (isDscTracking) startDscSSE();
|
||||
if (isDscTracking) startDscSSE(isAgentMode);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
// Start polling fallback for agent mode
|
||||
if (isAgentMode) {
|
||||
startDscPolling();
|
||||
}
|
||||
}
|
||||
|
||||
// Track last DSC message count for polling
|
||||
let lastDscMessageCount = 0;
|
||||
|
||||
function startDscPolling() {
|
||||
if (dscPollTimer) return;
|
||||
lastDscMessageCount = 0;
|
||||
|
||||
const pollInterval = 2000;
|
||||
dscPollTimer = setInterval(async () => {
|
||||
if (!isDscTracking || !dscCurrentAgent) {
|
||||
clearInterval(dscPollTimer);
|
||||
dscPollTimer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/controller/agents/${dscCurrentAgent}/dsc/data`);
|
||||
if (!response.ok) return;
|
||||
|
||||
const data = await response.json();
|
||||
const result = data.result || data;
|
||||
const messages = result.data || [];
|
||||
|
||||
// Process new messages
|
||||
if (messages.length > lastDscMessageCount) {
|
||||
const newMessages = messages.slice(lastDscMessageCount);
|
||||
newMessages.forEach(msg => {
|
||||
const dscMsg = {
|
||||
type: 'dsc_message',
|
||||
...msg,
|
||||
agent_name: result.agent_name || 'Remote Agent'
|
||||
};
|
||||
handleDscMessage(dscMsg);
|
||||
});
|
||||
lastDscMessageCount = messages.length;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('DSC polling error:', err);
|
||||
}
|
||||
}, pollInterval);
|
||||
}
|
||||
|
||||
function handleDscMessage(data) {
|
||||
@@ -1100,5 +1334,324 @@
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', initMap);
|
||||
</script>
|
||||
|
||||
<!-- Agent styles -->
|
||||
<style>
|
||||
.agent-selector-compact {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
.agent-select-sm {
|
||||
background: rgba(0, 40, 60, 0.8);
|
||||
border: 1px solid var(--border-color, rgba(0, 200, 255, 0.3));
|
||||
color: var(--text-primary, #e0f7ff);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
cursor: pointer;
|
||||
}
|
||||
.agent-select-sm:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-cyan, #00d4ff);
|
||||
}
|
||||
.agent-status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.agent-status-dot.online {
|
||||
background: #4caf50;
|
||||
box-shadow: 0 0 6px #4caf50;
|
||||
}
|
||||
.agent-status-dot.offline {
|
||||
background: #f44336;
|
||||
box-shadow: 0 0 6px #f44336;
|
||||
}
|
||||
.vessel-item .agent-badge {
|
||||
font-size: 9px;
|
||||
color: var(--accent-cyan, #00d4ff);
|
||||
background: rgba(0, 200, 255, 0.1);
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
#agentModeWarning {
|
||||
color: #f0ad4e;
|
||||
font-size: 10px;
|
||||
padding: 4px 8px;
|
||||
background: rgba(240,173,78,0.1);
|
||||
border-radius: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.show-all-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 10px;
|
||||
color: var(--text-muted, #a0c4d0);
|
||||
cursor: pointer;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.show-all-label input {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Agent Manager -->
|
||||
<script src="{{ url_for('static', filename='js/core/agents.js') }}"></script>
|
||||
<script>
|
||||
// AIS-specific agent integration
|
||||
let aisCurrentAgent = 'local';
|
||||
|
||||
function selectAisAgent(agentId) {
|
||||
aisCurrentAgent = agentId;
|
||||
currentAgent = agentId; // Update global agent state
|
||||
|
||||
if (agentId === 'local') {
|
||||
loadDevices();
|
||||
console.log('AIS: Using local device');
|
||||
} else {
|
||||
refreshAgentDevicesForAis(agentId);
|
||||
syncAgentModeStates(agentId);
|
||||
console.log(`AIS: Using agent ${agentId}`);
|
||||
}
|
||||
updateAgentStatus();
|
||||
}
|
||||
|
||||
async function refreshAgentDevicesForAis(agentId) {
|
||||
try {
|
||||
const response = await fetch(`/controller/agents/${agentId}?refresh=true`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.agent && data.agent.interfaces) {
|
||||
const devices = data.agent.interfaces.devices || [];
|
||||
populateAisDeviceSelects(devices);
|
||||
|
||||
// Update observer location if agent has GPS
|
||||
if (data.agent.gps_coords) {
|
||||
const gps = typeof data.agent.gps_coords === 'string'
|
||||
? JSON.parse(data.agent.gps_coords)
|
||||
: data.agent.gps_coords;
|
||||
if (gps.lat && gps.lon) {
|
||||
document.getElementById('obsLat').value = gps.lat.toFixed(4);
|
||||
document.getElementById('obsLon').value = gps.lon.toFixed(4);
|
||||
updateObserverLoc();
|
||||
console.log(`Updated observer location from agent GPS: ${gps.lat}, ${gps.lon}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh agent devices:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function populateAisDeviceSelects(devices) {
|
||||
const aisSelect = document.getElementById('aisDeviceSelect');
|
||||
const dscSelect = document.getElementById('dscDeviceSelect');
|
||||
|
||||
[aisSelect, dscSelect].forEach(select => {
|
||||
if (!select) return;
|
||||
select.innerHTML = '';
|
||||
|
||||
if (devices.length === 0) {
|
||||
select.innerHTML = '<option value="0">No SDR found</option>';
|
||||
} else {
|
||||
devices.forEach(device => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = device.index;
|
||||
opt.textContent = `Device ${device.index}: ${device.name || device.type || 'SDR'}`;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Override startTracking for agent support
|
||||
const originalStartTracking = startTracking;
|
||||
startTracking = function() {
|
||||
const useAgent = aisCurrentAgent !== 'local';
|
||||
|
||||
if (useAgent) {
|
||||
// Check for conflicts
|
||||
if (typeof checkAgentModeConflict === 'function' && !checkAgentModeConflict('ais')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const device = document.getElementById('aisDeviceSelect').value;
|
||||
const gain = document.getElementById('aisGain').value;
|
||||
|
||||
fetch(`/controller/agents/${aisCurrentAgent}/ais/start`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ device, gain })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
// Handle controller proxy response (agent response is nested in 'result')
|
||||
const scanResult = data.result || data;
|
||||
if (scanResult.status === 'started' || scanResult.status === 'already_running' || scanResult.status === 'success') {
|
||||
isTracking = true;
|
||||
document.getElementById('startBtn').textContent = 'STOP';
|
||||
document.getElementById('startBtn').classList.add('active');
|
||||
document.getElementById('trackingDot').classList.add('active');
|
||||
document.getElementById('trackingStatus').textContent = 'TRACKING (AGENT)';
|
||||
document.getElementById('agentSelect').disabled = true;
|
||||
startSessionTimer();
|
||||
startSSE(); // Use multi-agent stream
|
||||
startAisPolling(); // Also start polling as fallback
|
||||
|
||||
if (typeof agentRunningModes !== 'undefined' && !agentRunningModes.includes('ais')) {
|
||||
agentRunningModes.push('ais');
|
||||
}
|
||||
} else {
|
||||
alert(scanResult.message || 'Failed to start');
|
||||
}
|
||||
})
|
||||
.catch(err => alert('Error: ' + err.message));
|
||||
} else {
|
||||
originalStartTracking();
|
||||
}
|
||||
};
|
||||
|
||||
// Override stopTracking for agent support
|
||||
const originalStopTracking = stopTracking;
|
||||
stopTracking = function() {
|
||||
const useAgent = aisCurrentAgent !== 'local';
|
||||
|
||||
if (useAgent) {
|
||||
fetch(`/controller/agents/${aisCurrentAgent}/ais/stop`, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(() => {
|
||||
isTracking = false;
|
||||
document.getElementById('startBtn').textContent = 'START';
|
||||
document.getElementById('startBtn').classList.remove('active');
|
||||
document.getElementById('trackingDot').classList.remove('active');
|
||||
document.getElementById('trackingStatus').textContent = 'STANDBY';
|
||||
document.getElementById('agentSelect').disabled = false;
|
||||
stopSSE();
|
||||
|
||||
if (typeof agentRunningModes !== 'undefined') {
|
||||
agentRunningModes = agentRunningModes.filter(m => m !== 'ais');
|
||||
}
|
||||
})
|
||||
.catch(err => console.error('Stop error:', err));
|
||||
} else {
|
||||
originalStopTracking();
|
||||
}
|
||||
};
|
||||
|
||||
// Hook into page init
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const agentSelect = document.getElementById('agentSelect');
|
||||
if (agentSelect) {
|
||||
agentSelect.addEventListener('change', function(e) {
|
||||
selectAisAgent(e.target.value);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Show All Agents mode - display vessels from all agents on the map
|
||||
let showAllAgentsMode = false;
|
||||
let allAgentsEventSource = null;
|
||||
|
||||
function toggleShowAllAgents() {
|
||||
const checkbox = document.getElementById('showAllAgents');
|
||||
showAllAgentsMode = checkbox ? checkbox.checked : false;
|
||||
|
||||
const agentSelect = document.getElementById('agentSelect');
|
||||
const startBtn = document.getElementById('startBtn');
|
||||
|
||||
if (showAllAgentsMode) {
|
||||
// Disable individual agent selection and start button
|
||||
if (agentSelect) agentSelect.disabled = true;
|
||||
if (startBtn) startBtn.disabled = true;
|
||||
|
||||
// Connect to multi-agent stream (passive listening to all agents)
|
||||
startAllAgentsStream();
|
||||
|
||||
document.getElementById('trackingStatus').textContent = 'ALL AGENTS';
|
||||
document.getElementById('trackingDot').classList.add('active');
|
||||
console.log('Show All Agents mode enabled');
|
||||
} else {
|
||||
// Re-enable controls
|
||||
if (agentSelect) agentSelect.disabled = isTracking;
|
||||
if (startBtn) startBtn.disabled = false;
|
||||
|
||||
// Stop multi-agent stream
|
||||
stopAllAgentsStream();
|
||||
|
||||
if (!isTracking) {
|
||||
document.getElementById('trackingStatus').textContent = 'STANDBY';
|
||||
document.getElementById('trackingDot').classList.remove('active');
|
||||
}
|
||||
console.log('Show All Agents mode disabled');
|
||||
}
|
||||
}
|
||||
|
||||
function startAllAgentsStream() {
|
||||
if (allAgentsEventSource) allAgentsEventSource.close();
|
||||
|
||||
allAgentsEventSource = new EventSource('/controller/stream/all');
|
||||
allAgentsEventSource.onmessage = function(e) {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'keepalive') return;
|
||||
|
||||
// Handle AIS data from any agent
|
||||
if (data.scan_type === 'ais' && data.payload) {
|
||||
const payload = data.payload;
|
||||
if (payload.vessels) {
|
||||
Object.values(payload.vessels).forEach(v => {
|
||||
v._agent = data.agent_name;
|
||||
updateVessel({ type: 'vessel', ...v });
|
||||
});
|
||||
} else if (payload.mmsi) {
|
||||
payload._agent = data.agent_name;
|
||||
updateVessel({ type: 'vessel', ...payload });
|
||||
}
|
||||
}
|
||||
|
||||
// Handle DSC data from any agent
|
||||
if (data.scan_type === 'dsc' && data.payload) {
|
||||
const payload = data.payload;
|
||||
if (payload.messages) {
|
||||
payload.messages.forEach(msg => {
|
||||
msg._agent = data.agent_name;
|
||||
processDscMessage(msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('All agents stream parse error:', err);
|
||||
}
|
||||
};
|
||||
|
||||
allAgentsEventSource.onerror = function() {
|
||||
console.error('All agents stream error');
|
||||
setTimeout(() => {
|
||||
if (showAllAgentsMode) startAllAgentsStream();
|
||||
}, 3000);
|
||||
};
|
||||
}
|
||||
|
||||
function stopAllAgentsStream() {
|
||||
if (allAgentsEventSource) {
|
||||
allAgentsEventSource.close();
|
||||
allAgentsEventSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Process DSC message (wrapper for addDscMessage if it exists)
|
||||
function processDscMessage(msg) {
|
||||
if (typeof addDscMessage === 'function') {
|
||||
addDscMessage(msg);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+519
-94
@@ -574,11 +574,12 @@
|
||||
<th class="sortable" data-sort="rssi">Signal</th>
|
||||
<th class="sortable" data-sort="security">Security</th>
|
||||
<th class="sortable" data-sort="clients">Clients</th>
|
||||
<th class="col-agent sortable" data-sort="agent">Source</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="wifiNetworkTableBody">
|
||||
<tr class="wifi-network-placeholder">
|
||||
<td colspan="6">
|
||||
<td colspan="7">
|
||||
<div class="placeholder-text">Start scanning to discover networks</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -2306,6 +2307,11 @@
|
||||
|
||||
// Check if using remote agent
|
||||
if (typeof currentAgent !== 'undefined' && currentAgent !== 'local') {
|
||||
// Check for conflicts with other running SDR modes
|
||||
if (typeof checkAgentModeConflict === 'function' && !checkAgentModeConflict('sensor')) {
|
||||
return; // User cancelled or conflict not resolved
|
||||
}
|
||||
|
||||
// Route through agent proxy
|
||||
const config = {
|
||||
frequency: freq,
|
||||
@@ -2320,12 +2326,14 @@
|
||||
body: JSON.stringify(config)
|
||||
}).then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'started' || data.status === 'success') {
|
||||
// Handle controller proxy response (agent response is nested in 'result')
|
||||
const scanResult = data.result || data;
|
||||
if (scanResult.status === 'started' || scanResult.status === 'success') {
|
||||
setSensorRunning(true);
|
||||
startAgentSensorStream();
|
||||
showInfo(`Sensor started on remote agent`);
|
||||
} else {
|
||||
alert('Error: ' + (data.message || 'Failed to start sensor on agent'));
|
||||
alert('Error: ' + (scanResult.message || 'Failed to start sensor on agent'));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -2612,6 +2620,10 @@
|
||||
document.getElementById('rtlamrFrequency').value = freq;
|
||||
}
|
||||
|
||||
// RTLAMR mode polling timer for agent mode
|
||||
let rtlamrPollTimer = null;
|
||||
let rtlamrCurrentAgent = null;
|
||||
|
||||
function startRtlamrDecoding() {
|
||||
const freq = document.getElementById('rtlamrFrequency').value;
|
||||
const gain = document.getElementById('rtlamrGain').value;
|
||||
@@ -2621,8 +2633,12 @@
|
||||
const filterid = document.getElementById('rtlamrFilterId').value;
|
||||
const unique = document.getElementById('rtlamrUnique').checked;
|
||||
|
||||
// Check if device is available
|
||||
if (!checkDeviceAvailability('rtlamr')) {
|
||||
// Check if using agent mode
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
rtlamrCurrentAgent = isAgentMode ? currentAgent : null;
|
||||
|
||||
// Check if device is available (only for local mode)
|
||||
if (!isAgentMode && !checkDeviceAvailability('rtlamr')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2637,16 +2653,26 @@
|
||||
format: 'json'
|
||||
};
|
||||
|
||||
fetch('/start_rtlamr', {
|
||||
// Determine endpoint based on agent mode
|
||||
const endpoint = isAgentMode
|
||||
? `/controller/agents/${currentAgent}/rtlamr/start`
|
||||
: '/start_rtlamr';
|
||||
|
||||
fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config)
|
||||
}).then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'started') {
|
||||
reserveDevice(parseInt(device), 'rtlamr');
|
||||
// Handle controller proxy response format
|
||||
const scanResult = isAgentMode && data.result ? data.result : data;
|
||||
|
||||
if (scanResult.status === 'started' || scanResult.status === 'success') {
|
||||
if (!isAgentMode) {
|
||||
reserveDevice(parseInt(device), 'rtlamr');
|
||||
}
|
||||
setRtlamrRunning(true);
|
||||
startRtlamrStream();
|
||||
startRtlamrStream(isAgentMode);
|
||||
|
||||
// Initialize meter filter bar (reuse sensor filter bar since same structure)
|
||||
const filterContainer = document.getElementById('filterBarContainer');
|
||||
@@ -2667,21 +2693,34 @@
|
||||
// Clear existing output
|
||||
output.innerHTML = '<div class="placeholder signal-empty-state" style="display: none;"></div>';
|
||||
} else {
|
||||
alert('Error: ' + data.message);
|
||||
alert('Error: ' + (scanResult.message || scanResult.error || 'Failed to start'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function stopRtlamrDecoding() {
|
||||
fetch('/stop_rtlamr', { method: 'POST' })
|
||||
const isAgentMode = rtlamrCurrentAgent !== null;
|
||||
const endpoint = isAgentMode
|
||||
? `/controller/agents/${rtlamrCurrentAgent}/rtlamr/stop`
|
||||
: '/stop_rtlamr';
|
||||
|
||||
fetch(endpoint, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
releaseDevice('rtlamr');
|
||||
if (!isAgentMode) {
|
||||
releaseDevice('rtlamr');
|
||||
}
|
||||
rtlamrCurrentAgent = null;
|
||||
setRtlamrRunning(false);
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
// Clear polling timer
|
||||
if (rtlamrPollTimer) {
|
||||
clearInterval(rtlamrPollTimer);
|
||||
rtlamrPollTimer = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2701,12 +2740,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
function startRtlamrStream() {
|
||||
function startRtlamrStream(isAgentMode = false) {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
}
|
||||
|
||||
eventSource = new EventSource('/stream_rtlamr');
|
||||
// Use different stream endpoint for agent mode
|
||||
const streamUrl = isAgentMode ? '/controller/stream/all' : '/stream_rtlamr';
|
||||
eventSource = new EventSource(streamUrl);
|
||||
|
||||
eventSource.onopen = function () {
|
||||
showInfo('RTLAMR stream connected...');
|
||||
@@ -2714,20 +2755,86 @@
|
||||
|
||||
eventSource.onmessage = function (e) {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'rtlamr') {
|
||||
addRtlamrReading(data);
|
||||
} else if (data.type === 'status') {
|
||||
if (data.text === 'stopped') {
|
||||
setRtlamrRunning(false);
|
||||
|
||||
if (isAgentMode) {
|
||||
// Handle multi-agent stream format
|
||||
if (data.scan_type === 'rtlamr' && data.payload) {
|
||||
const payload = data.payload;
|
||||
if (payload.type === 'rtlamr') {
|
||||
payload.agent_name = data.agent_name;
|
||||
addRtlamrReading(payload);
|
||||
} else if (payload.type === 'status') {
|
||||
if (payload.text === 'stopped') {
|
||||
setRtlamrRunning(false);
|
||||
}
|
||||
} else if (payload.type === 'info' || payload.type === 'raw') {
|
||||
showInfo(`[${data.agent_name}] ${payload.text}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Local stream format
|
||||
if (data.type === 'rtlamr') {
|
||||
addRtlamrReading(data);
|
||||
} else if (data.type === 'status') {
|
||||
if (data.text === 'stopped') {
|
||||
setRtlamrRunning(false);
|
||||
}
|
||||
} else if (data.type === 'info' || data.type === 'raw') {
|
||||
showInfo(data.text);
|
||||
}
|
||||
} else if (data.type === 'info' || data.type === 'raw') {
|
||||
showInfo(data.text);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = function (e) {
|
||||
console.error('RTLAMR stream error');
|
||||
};
|
||||
|
||||
// Start polling fallback for agent mode
|
||||
if (isAgentMode) {
|
||||
startRtlamrPolling();
|
||||
}
|
||||
}
|
||||
|
||||
// Track last reading count for polling
|
||||
let lastRtlamrReadingCount = 0;
|
||||
|
||||
function startRtlamrPolling() {
|
||||
if (rtlamrPollTimer) return;
|
||||
lastRtlamrReadingCount = 0;
|
||||
|
||||
const pollInterval = 2000;
|
||||
rtlamrPollTimer = setInterval(async () => {
|
||||
if (!isRtlamrRunning || !rtlamrCurrentAgent) {
|
||||
clearInterval(rtlamrPollTimer);
|
||||
rtlamrPollTimer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/controller/agents/${rtlamrCurrentAgent}/rtlamr/data`);
|
||||
if (!response.ok) return;
|
||||
|
||||
const data = await response.json();
|
||||
const result = data.result || data;
|
||||
const readings = result.data || [];
|
||||
|
||||
// Process new readings
|
||||
if (readings.length > lastRtlamrReadingCount) {
|
||||
const newReadings = readings.slice(lastRtlamrReadingCount);
|
||||
newReadings.forEach(reading => {
|
||||
const displayReading = {
|
||||
type: 'rtlamr',
|
||||
...reading,
|
||||
agent_name: result.agent_name || 'Remote Agent'
|
||||
};
|
||||
addRtlamrReading(displayReading);
|
||||
});
|
||||
lastRtlamrReadingCount = readings.length;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('RTLAMR polling error:', err);
|
||||
}
|
||||
}, pollInterval);
|
||||
}
|
||||
|
||||
function addRtlamrReading(data) {
|
||||
@@ -3196,6 +3303,9 @@
|
||||
return protocols;
|
||||
}
|
||||
|
||||
// Pager mode polling timer for agent mode
|
||||
let pagerPollTimer = null;
|
||||
|
||||
function startDecoding() {
|
||||
const freq = document.getElementById('frequency').value;
|
||||
const gain = document.getElementById('gain').value;
|
||||
@@ -3209,13 +3319,16 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if device is available
|
||||
if (!checkDeviceAvailability('pager')) {
|
||||
// Check if using agent mode
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
|
||||
// Check if device is available (only for local mode)
|
||||
if (!isAgentMode && !checkDeviceAvailability('pager')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for remote SDR
|
||||
const remoteConfig = getRemoteSDRConfig();
|
||||
// Check for remote SDR (only for local mode)
|
||||
const remoteConfig = isAgentMode ? null : getRemoteSDRConfig();
|
||||
if (remoteConfig === false) return; // Validation failed
|
||||
|
||||
const config = {
|
||||
@@ -3229,22 +3342,32 @@
|
||||
bias_t: getBiasTEnabled()
|
||||
};
|
||||
|
||||
// Add rtl_tcp params if using remote SDR
|
||||
// Add rtl_tcp params if using remote SDR (local mode only)
|
||||
if (remoteConfig) {
|
||||
config.rtl_tcp_host = remoteConfig.host;
|
||||
config.rtl_tcp_port = remoteConfig.port;
|
||||
}
|
||||
|
||||
fetch('/start', {
|
||||
// Determine endpoint based on agent mode
|
||||
const endpoint = isAgentMode
|
||||
? `/controller/agents/${currentAgent}/pager/start`
|
||||
: '/start';
|
||||
|
||||
fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config)
|
||||
}).then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'started') {
|
||||
reserveDevice(parseInt(device), 'pager');
|
||||
// Handle controller proxy response format (agent response is nested in 'result')
|
||||
const scanResult = isAgentMode && data.result ? data.result : data;
|
||||
|
||||
if (scanResult.status === 'started' || scanResult.status === 'success') {
|
||||
if (!isAgentMode) {
|
||||
reserveDevice(parseInt(device), 'pager');
|
||||
}
|
||||
setRunning(true);
|
||||
startStream();
|
||||
startStream(isAgentMode);
|
||||
|
||||
// Initialize filter bar
|
||||
const filterContainer = document.getElementById('filterBarContainer');
|
||||
@@ -3260,24 +3383,37 @@
|
||||
// Clear address history for fresh session
|
||||
SignalCards.clearAddressHistory('pager');
|
||||
} else {
|
||||
alert('Error: ' + data.message);
|
||||
alert('Error: ' + (scanResult.message || scanResult.error || 'Failed to start pager decoding'));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Start error:', err);
|
||||
alert('Error starting pager decoding: ' + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
function stopDecoding() {
|
||||
fetch('/stop', { method: 'POST' })
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
const endpoint = isAgentMode
|
||||
? `/controller/agents/${currentAgent}/pager/stop`
|
||||
: '/stop';
|
||||
|
||||
fetch(endpoint, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
releaseDevice('pager');
|
||||
if (!isAgentMode) {
|
||||
releaseDevice('pager');
|
||||
}
|
||||
setRunning(false);
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
// Clear polling timer if active
|
||||
if (pagerPollTimer) {
|
||||
clearInterval(pagerPollTimer);
|
||||
pagerPollTimer = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3342,12 +3478,14 @@
|
||||
document.getElementById('stopBtn').style.display = running ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function startStream() {
|
||||
function startStream(isAgentMode = false) {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
}
|
||||
|
||||
eventSource = new EventSource('/stream');
|
||||
// Use different stream endpoint for agent mode
|
||||
const streamUrl = isAgentMode ? '/controller/stream/all' : '/stream';
|
||||
eventSource = new EventSource(streamUrl);
|
||||
|
||||
eventSource.onopen = function () {
|
||||
showInfo('Stream connected...');
|
||||
@@ -3356,24 +3494,101 @@
|
||||
eventSource.onmessage = function (e) {
|
||||
const data = JSON.parse(e.data);
|
||||
|
||||
if (data.type === 'message') {
|
||||
addMessage(data);
|
||||
} else if (data.type === 'status') {
|
||||
if (data.text === 'stopped') {
|
||||
setRunning(false);
|
||||
} else if (data.text === 'started') {
|
||||
showInfo('Decoder started, waiting for signals...');
|
||||
// Handle multi-agent stream format
|
||||
if (isAgentMode) {
|
||||
// Multi-agent stream tags data with scan_type and agent_name
|
||||
if (data.scan_type === 'pager' && data.payload) {
|
||||
const payload = data.payload;
|
||||
if (payload.type === 'message') {
|
||||
// Add agent info to the message
|
||||
payload.agent_name = data.agent_name;
|
||||
addMessage(payload);
|
||||
} else if (payload.type === 'status') {
|
||||
if (payload.text === 'stopped') {
|
||||
setRunning(false);
|
||||
} else if (payload.text === 'started') {
|
||||
showInfo(`Decoder started on ${data.agent_name}, waiting for signals...`);
|
||||
}
|
||||
} else if (payload.type === 'info') {
|
||||
showInfo(`[${data.agent_name}] ${payload.text}`);
|
||||
}
|
||||
} else if (data.type === 'keepalive') {
|
||||
// Ignore keepalive messages
|
||||
}
|
||||
} else {
|
||||
// Local stream format
|
||||
if (data.type === 'message') {
|
||||
addMessage(data);
|
||||
} else if (data.type === 'status') {
|
||||
if (data.text === 'stopped') {
|
||||
setRunning(false);
|
||||
} else if (data.text === 'started') {
|
||||
showInfo('Decoder started, waiting for signals...');
|
||||
}
|
||||
} else if (data.type === 'info') {
|
||||
showInfo(data.text);
|
||||
} else if (data.type === 'raw') {
|
||||
showInfo(data.text);
|
||||
}
|
||||
} else if (data.type === 'info') {
|
||||
showInfo(data.text);
|
||||
} else if (data.type === 'raw') {
|
||||
showInfo(data.text);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = function (e) {
|
||||
checkStatus();
|
||||
};
|
||||
|
||||
// Start polling fallback for agent mode (in case push isn't enabled)
|
||||
if (isAgentMode) {
|
||||
startPagerPolling();
|
||||
}
|
||||
}
|
||||
|
||||
// Track last message count to avoid duplicates during polling
|
||||
let lastPagerMsgCount = 0;
|
||||
|
||||
function startPagerPolling() {
|
||||
if (pagerPollTimer) return;
|
||||
lastPagerMsgCount = 0;
|
||||
|
||||
const pollInterval = 2000; // 2 seconds
|
||||
pagerPollTimer = setInterval(async () => {
|
||||
if (!isRunning) {
|
||||
clearInterval(pagerPollTimer);
|
||||
pagerPollTimer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/controller/agents/${currentAgent}/pager/data`);
|
||||
if (!response.ok) return;
|
||||
|
||||
const data = await response.json();
|
||||
const result = data.result || data;
|
||||
const modeData = result.data || result;
|
||||
|
||||
// Process messages from polling response
|
||||
if (modeData.messages && Array.isArray(modeData.messages)) {
|
||||
const newMsgs = modeData.messages.slice(lastPagerMsgCount);
|
||||
newMsgs.forEach(msg => {
|
||||
// Convert to expected format
|
||||
const displayMsg = {
|
||||
type: 'message',
|
||||
protocol: msg.protocol || 'UNKNOWN',
|
||||
address: msg.address || '',
|
||||
function: msg.function || '',
|
||||
msg_type: msg.msg_type || 'Alpha',
|
||||
message: msg.message || '',
|
||||
timestamp: msg.received_at || new Date().toISOString(),
|
||||
agent_name: result.agent_name || 'Remote Agent'
|
||||
};
|
||||
addMessage(displayMsg);
|
||||
});
|
||||
lastPagerMsgCount = modeData.messages.length;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Pager polling error:', err);
|
||||
}
|
||||
}, pollInterval);
|
||||
}
|
||||
|
||||
function addMessage(msg) {
|
||||
@@ -7084,12 +7299,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
// APRS mode polling timer for agent mode
|
||||
let aprsPollTimer = null;
|
||||
let aprsCurrentAgent = null;
|
||||
|
||||
function startAprs() {
|
||||
// Get values from function bar controls
|
||||
const region = document.getElementById('aprsStripRegion').value;
|
||||
const device = getSelectedDevice();
|
||||
const gain = document.getElementById('aprsStripGain').value;
|
||||
|
||||
// Check if using agent mode
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
aprsCurrentAgent = isAgentMode ? currentAgent : null;
|
||||
|
||||
// Build request body
|
||||
const requestBody = {
|
||||
region,
|
||||
@@ -7107,14 +7330,22 @@
|
||||
requestBody.frequency = customFreq;
|
||||
}
|
||||
|
||||
fetch('/aprs/start', {
|
||||
// Determine endpoint based on agent mode
|
||||
const endpoint = isAgentMode
|
||||
? `/controller/agents/${currentAgent}/aprs/start`
|
||||
: '/aprs/start';
|
||||
|
||||
fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(requestBody)
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'started') {
|
||||
// Handle controller proxy response format
|
||||
const scanResult = isAgentMode && data.result ? data.result : data;
|
||||
|
||||
if (scanResult.status === 'started' || scanResult.status === 'success') {
|
||||
isAprsRunning = true;
|
||||
aprsPacketCount = 0;
|
||||
aprsStationCount = 0;
|
||||
@@ -7138,7 +7369,7 @@
|
||||
document.getElementById('aprsMapStatus').textContent = 'TRACKING';
|
||||
document.getElementById('aprsMapStatus').style.color = 'var(--accent-green)';
|
||||
// Update function bar status
|
||||
updateAprsStatus('listening', data.frequency);
|
||||
updateAprsStatus('listening', scanResult.frequency);
|
||||
// Reset function bar stats
|
||||
document.getElementById('aprsStripStations').textContent = '0';
|
||||
document.getElementById('aprsStripPackets').textContent = '0';
|
||||
@@ -7149,9 +7380,9 @@
|
||||
const customFreqInput = document.getElementById('aprsStripCustomFreq');
|
||||
if (customFreqInput) customFreqInput.disabled = true;
|
||||
startAprsMeterCheck();
|
||||
startAprsStream();
|
||||
startAprsStream(isAgentMode);
|
||||
} else {
|
||||
alert('APRS Error: ' + data.message);
|
||||
alert('APRS Error: ' + (scanResult.message || scanResult.error || 'Failed to start'));
|
||||
updateAprsStatus('error');
|
||||
}
|
||||
})
|
||||
@@ -7162,10 +7393,16 @@
|
||||
}
|
||||
|
||||
function stopAprs() {
|
||||
fetch('/aprs/stop', { method: 'POST' })
|
||||
const isAgentMode = aprsCurrentAgent !== null;
|
||||
const endpoint = isAgentMode
|
||||
? `/controller/agents/${aprsCurrentAgent}/aprs/stop`
|
||||
: '/aprs/stop';
|
||||
|
||||
fetch(endpoint, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
isAprsRunning = false;
|
||||
aprsCurrentAgent = null;
|
||||
// Update function bar buttons
|
||||
document.getElementById('aprsStripStartBtn').style.display = 'inline-block';
|
||||
document.getElementById('aprsStripStopBtn').style.display = 'none';
|
||||
@@ -7192,29 +7429,60 @@
|
||||
aprsEventSource.close();
|
||||
aprsEventSource = null;
|
||||
}
|
||||
// Clear polling timer
|
||||
if (aprsPollTimer) {
|
||||
clearInterval(aprsPollTimer);
|
||||
aprsPollTimer = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function startAprsStream() {
|
||||
function startAprsStream(isAgentMode = false) {
|
||||
if (aprsEventSource) aprsEventSource.close();
|
||||
aprsEventSource = new EventSource('/aprs/stream');
|
||||
|
||||
// Use different stream endpoint for agent mode
|
||||
const streamUrl = isAgentMode ? '/controller/stream/all' : '/aprs/stream';
|
||||
aprsEventSource = new EventSource(streamUrl);
|
||||
|
||||
aprsEventSource.onmessage = function (e) {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'aprs') {
|
||||
aprsPacketCount++;
|
||||
// Update map footer and function bar
|
||||
document.getElementById('aprsPacketCount').textContent = aprsPacketCount;
|
||||
document.getElementById('aprsStripPackets').textContent = aprsPacketCount;
|
||||
// Switch to tracking state on first packet
|
||||
const dot = document.getElementById('aprsStripDot');
|
||||
if (dot && !dot.classList.contains('tracking')) {
|
||||
updateAprsStatus('tracking');
|
||||
|
||||
if (isAgentMode) {
|
||||
// Handle multi-agent stream format
|
||||
if (data.scan_type === 'aprs' && data.payload) {
|
||||
const payload = data.payload;
|
||||
if (payload.type === 'aprs') {
|
||||
aprsPacketCount++;
|
||||
document.getElementById('aprsPacketCount').textContent = aprsPacketCount;
|
||||
document.getElementById('aprsStripPackets').textContent = aprsPacketCount;
|
||||
const dot = document.getElementById('aprsStripDot');
|
||||
if (dot && !dot.classList.contains('tracking')) {
|
||||
updateAprsStatus('tracking');
|
||||
}
|
||||
// Add agent info
|
||||
payload.agent_name = data.agent_name;
|
||||
processAprsPacket(payload);
|
||||
} else if (payload.type === 'meter') {
|
||||
updateAprsMeter(payload.level);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Local stream format
|
||||
if (data.type === 'aprs') {
|
||||
aprsPacketCount++;
|
||||
// Update map footer and function bar
|
||||
document.getElementById('aprsPacketCount').textContent = aprsPacketCount;
|
||||
document.getElementById('aprsStripPackets').textContent = aprsPacketCount;
|
||||
// Switch to tracking state on first packet
|
||||
const dot = document.getElementById('aprsStripDot');
|
||||
if (dot && !dot.classList.contains('tracking')) {
|
||||
updateAprsStatus('tracking');
|
||||
}
|
||||
processAprsPacket(data);
|
||||
} else if (data.type === 'meter') {
|
||||
// Update signal indicator in function bar
|
||||
updateAprsMeter(data.level);
|
||||
}
|
||||
processAprsPacket(data);
|
||||
} else if (data.type === 'meter') {
|
||||
// Update signal indicator in function bar
|
||||
updateAprsMeter(data.level);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7222,6 +7490,61 @@
|
||||
console.error('APRS stream error');
|
||||
updateAprsStatus('error');
|
||||
};
|
||||
|
||||
// Start polling fallback for agent mode
|
||||
if (isAgentMode) {
|
||||
startAprsPolling();
|
||||
}
|
||||
}
|
||||
|
||||
// Track last station count for polling
|
||||
let lastAprsStationCount = 0;
|
||||
|
||||
function startAprsPolling() {
|
||||
if (aprsPollTimer) return;
|
||||
lastAprsStationCount = 0;
|
||||
|
||||
const pollInterval = 2000;
|
||||
aprsPollTimer = setInterval(async () => {
|
||||
if (!isAprsRunning || !aprsCurrentAgent) {
|
||||
clearInterval(aprsPollTimer);
|
||||
aprsPollTimer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/controller/agents/${aprsCurrentAgent}/aprs/data`);
|
||||
if (!response.ok) return;
|
||||
|
||||
const data = await response.json();
|
||||
const result = data.result || data;
|
||||
const stations = result.data || [];
|
||||
|
||||
// Process new stations
|
||||
if (stations.length > lastAprsStationCount) {
|
||||
const newStations = stations.slice(lastAprsStationCount);
|
||||
newStations.forEach(station => {
|
||||
aprsPacketCount++;
|
||||
document.getElementById('aprsPacketCount').textContent = aprsPacketCount;
|
||||
document.getElementById('aprsStripPackets').textContent = aprsPacketCount;
|
||||
const dot = document.getElementById('aprsStripDot');
|
||||
if (dot && !dot.classList.contains('tracking')) {
|
||||
updateAprsStatus('tracking');
|
||||
}
|
||||
// Convert to expected packet format
|
||||
const packet = {
|
||||
type: 'aprs',
|
||||
...station,
|
||||
agent_name: result.agent_name || 'Remote Agent'
|
||||
};
|
||||
processAprsPacket(packet);
|
||||
});
|
||||
lastAprsStationCount = stations.length;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('APRS polling error:', err);
|
||||
}
|
||||
}, pollInterval);
|
||||
}
|
||||
|
||||
// Signal Meter Functions
|
||||
@@ -7710,6 +8033,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Satellite mode agent state
|
||||
let satelliteCurrentAgent = null;
|
||||
|
||||
function calculatePasses() {
|
||||
const lat = parseFloat(document.getElementById('obsLat').value);
|
||||
const lon = parseFloat(document.getElementById('obsLon').value);
|
||||
@@ -7723,18 +8049,30 @@
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/satellite/predict', {
|
||||
// Check if using agent mode
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
satelliteCurrentAgent = isAgentMode ? currentAgent : null;
|
||||
|
||||
// Determine endpoint based on agent mode
|
||||
const endpoint = isAgentMode
|
||||
? `/controller/agents/${currentAgent}/satellite/predict`
|
||||
: '/satellite/predict';
|
||||
|
||||
fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ lat, lon, hours, minEl, satellites })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
satellitePasses = data.passes;
|
||||
// Handle controller proxy response format
|
||||
const result = isAgentMode && data.result ? data.result : data;
|
||||
|
||||
if (result.status === 'success') {
|
||||
satellitePasses = result.passes;
|
||||
renderPassList();
|
||||
document.getElementById('passCount').textContent = data.passes.length;
|
||||
if (data.passes.length > 0) {
|
||||
document.getElementById('passCount').textContent = result.passes.length;
|
||||
if (result.passes.length > 0) {
|
||||
selectPass(0);
|
||||
document.getElementById('satelliteCountdown').style.display = 'block';
|
||||
updateSatelliteCountdown();
|
||||
@@ -7743,7 +8081,7 @@
|
||||
document.getElementById('satelliteCountdown').style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
alert('Error: ' + data.message);
|
||||
alert('Error: ' + (result.message || result.error || 'Failed to predict passes'));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -7941,15 +8279,24 @@
|
||||
const lat = parseFloat(document.getElementById('obsLat').value);
|
||||
const lon = parseFloat(document.getElementById('obsLon').value);
|
||||
|
||||
fetch('/satellite/position', {
|
||||
// Check if using agent mode
|
||||
const isAgentMode = satelliteCurrentAgent !== null;
|
||||
const endpoint = isAgentMode
|
||||
? `/controller/agents/${satelliteCurrentAgent}/satellite/position`
|
||||
: '/satellite/position';
|
||||
|
||||
fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ lat, lon, satellites, includeTrack: true })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success' && data.positions) {
|
||||
updateRealTimeIndicators(data.positions);
|
||||
// Handle controller proxy response format
|
||||
const result = isAgentMode && data.result ? data.result : data;
|
||||
|
||||
if (result.status === 'success' && result.positions) {
|
||||
updateRealTimeIndicators(result.positions);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -8350,10 +8697,33 @@
|
||||
|
||||
async function refreshTscmDevices() {
|
||||
// Fetch available interfaces for TSCM scanning
|
||||
// Check if agent is selected and route accordingly
|
||||
try {
|
||||
const response = await fetch('/tscm/devices');
|
||||
let response;
|
||||
if (typeof currentAgent !== 'undefined' && currentAgent !== 'local') {
|
||||
// Fetch devices from agent capabilities
|
||||
response = await fetch(`/controller/agents/${currentAgent}?refresh=true`);
|
||||
} else {
|
||||
response = await fetch('/tscm/devices');
|
||||
}
|
||||
const data = await response.json();
|
||||
const devices = data.devices || {};
|
||||
|
||||
// Handle both local (/tscm/devices) and agent response formats
|
||||
let devices;
|
||||
const isAgentResponse = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
|
||||
if (isAgentResponse && data.agent) {
|
||||
// Agent response format - extract from capabilities/interfaces
|
||||
const agentInterfaces = data.agent.interfaces || {};
|
||||
const agentCapabilities = data.agent.capabilities || {};
|
||||
devices = {
|
||||
wifi_interfaces: agentInterfaces.wifi_interfaces || [],
|
||||
bt_adapters: agentInterfaces.bt_adapters || [],
|
||||
sdr_devices: agentCapabilities.devices || agentInterfaces.sdr_devices || []
|
||||
};
|
||||
} else {
|
||||
devices = data.devices || {};
|
||||
}
|
||||
|
||||
// Populate WiFi interfaces
|
||||
const wifiSelect = document.getElementById('tscmWifiInterface');
|
||||
@@ -8370,7 +8740,11 @@
|
||||
wifiSelect.value = devices.wifi_interfaces[0].name;
|
||||
}
|
||||
} else {
|
||||
wifiSelect.innerHTML = '<option value="">No WiFi interfaces found</option>';
|
||||
if (isAgentResponse) {
|
||||
wifiSelect.innerHTML = '<option value="">Agent manages WiFi</option>';
|
||||
} else {
|
||||
wifiSelect.innerHTML = '<option value="">No WiFi interfaces found</option>';
|
||||
}
|
||||
}
|
||||
|
||||
// Populate Bluetooth adapters
|
||||
@@ -8388,7 +8762,11 @@
|
||||
btSelect.value = devices.bt_adapters[0].name;
|
||||
}
|
||||
} else {
|
||||
btSelect.innerHTML = '<option value="">No Bluetooth adapters found</option>';
|
||||
if (isAgentResponse) {
|
||||
btSelect.innerHTML = '<option value="">Agent manages Bluetooth</option>';
|
||||
} else {
|
||||
btSelect.innerHTML = '<option value="">No Bluetooth adapters found</option>';
|
||||
}
|
||||
}
|
||||
|
||||
// Populate SDR devices
|
||||
@@ -8397,16 +8775,20 @@
|
||||
if (devices.sdr_devices && devices.sdr_devices.length > 0) {
|
||||
devices.sdr_devices.forEach(dev => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = dev.index;
|
||||
opt.value = dev.index !== undefined ? dev.index : 0;
|
||||
opt.textContent = dev.display_name || dev.name || 'SDR Device';
|
||||
sdrSelect.appendChild(opt);
|
||||
});
|
||||
// Auto-select first SDR if available
|
||||
if (devices.sdr_devices.length > 0) {
|
||||
sdrSelect.value = devices.sdr_devices[0].index;
|
||||
sdrSelect.value = devices.sdr_devices[0].index !== undefined ? devices.sdr_devices[0].index : 0;
|
||||
}
|
||||
} else {
|
||||
sdrSelect.innerHTML = '<option value="">No SDR devices found</option>';
|
||||
if (isAgentResponse) {
|
||||
sdrSelect.innerHTML = '<option value="">Agent manages SDR</option>';
|
||||
} else {
|
||||
sdrSelect.innerHTML = '<option value="">No SDR devices found</option>';
|
||||
}
|
||||
}
|
||||
|
||||
// Show warnings (e.g., not running as root)
|
||||
@@ -8465,8 +8847,23 @@
|
||||
document.getElementById('tscmDeviceWarnings').style.display = 'none';
|
||||
document.getElementById('tscmDeviceWarnings').innerHTML = '';
|
||||
|
||||
// Check for agent mode
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
|
||||
// Check for conflicts if using agent
|
||||
if (isAgentMode && typeof checkAgentModeConflict === 'function') {
|
||||
if (!checkAgentModeConflict('tscm')) {
|
||||
return; // Conflict detected, user cancelled
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/tscm/sweep/start', {
|
||||
// Route to agent or local based on selection
|
||||
const endpoint = isAgentMode
|
||||
? `/controller/agents/${currentAgent}/tscm/start`
|
||||
: '/tscm/sweep/start';
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -8483,7 +8880,9 @@
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.status === 'success') {
|
||||
// Handle controller proxy response (agent response is nested in 'result')
|
||||
const scanResult = isAgentMode && data.result ? data.result : data;
|
||||
if (scanResult.status === 'success' || scanResult.status === 'started') {
|
||||
isTscmRunning = true;
|
||||
tscmSweepStartTime = new Date();
|
||||
tscmSweepEndTime = null;
|
||||
@@ -8496,16 +8895,16 @@
|
||||
document.getElementById('tscmReportBtn').style.display = 'none';
|
||||
|
||||
// Show warnings if any devices unavailable
|
||||
if (data.warnings && data.warnings.length > 0) {
|
||||
if (scanResult.warnings && scanResult.warnings.length > 0) {
|
||||
const warningsDiv = document.getElementById('tscmDeviceWarnings');
|
||||
warningsDiv.innerHTML = data.warnings.map(w =>
|
||||
warningsDiv.innerHTML = scanResult.warnings.map(w =>
|
||||
`<div style="color: #ff9933; font-size: 10px; margin-bottom: 2px;">⚠ ${w}</div>`
|
||||
).join('');
|
||||
warningsDiv.style.display = 'block';
|
||||
}
|
||||
|
||||
// Update device indicators
|
||||
updateTscmDeviceIndicators(data.devices);
|
||||
updateTscmDeviceIndicators(scanResult.devices);
|
||||
|
||||
// Reset displays
|
||||
tscmThreats = [];
|
||||
@@ -8519,9 +8918,9 @@
|
||||
startTscmStream();
|
||||
} else {
|
||||
// Show error with details
|
||||
let errorMsg = data.message || 'Failed to start sweep';
|
||||
if (data.details && data.details.length > 0) {
|
||||
errorMsg += '\n\n' + data.details.join('\n');
|
||||
let errorMsg = scanResult.message || 'Failed to start sweep';
|
||||
if (scanResult.details && scanResult.details.length > 0) {
|
||||
errorMsg += '\n\n' + scanResult.details.join('\n');
|
||||
}
|
||||
alert(errorMsg);
|
||||
}
|
||||
@@ -8552,7 +8951,12 @@
|
||||
|
||||
async function stopTscmSweep() {
|
||||
try {
|
||||
await fetch('/tscm/sweep/stop', { method: 'POST' });
|
||||
// Route to agent or local based on selection
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
const endpoint = isAgentMode
|
||||
? `/controller/agents/${currentAgent}/tscm/stop`
|
||||
: '/tscm/sweep/stop';
|
||||
await fetch(endpoint, { method: 'POST' });
|
||||
} catch (e) {
|
||||
console.error('Error stopping sweep:', e);
|
||||
}
|
||||
@@ -9111,12 +9515,33 @@
|
||||
tscmEventSource.close();
|
||||
}
|
||||
|
||||
tscmEventSource = new EventSource('/tscm/sweep/stream');
|
||||
// Check if using agent - connect to multi-agent stream
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
const streamUrl = isAgentMode
|
||||
? '/controller/stream/all'
|
||||
: '/tscm/sweep/stream';
|
||||
|
||||
tscmEventSource = new EventSource(streamUrl);
|
||||
|
||||
tscmEventSource.onmessage = function (event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
handleTscmEvent(data);
|
||||
|
||||
// If using multi-agent stream, filter for TSCM data
|
||||
if (isAgentMode) {
|
||||
if (data.scan_type === 'tscm' || data.type?.startsWith('tscm') ||
|
||||
data.type === 'wifi_device' || data.type === 'bt_device' ||
|
||||
data.type === 'rf_signal' || data.type === 'threat' ||
|
||||
data.type === 'sweep_progress') {
|
||||
// Add agent info to data for display
|
||||
if (data.agent_name) {
|
||||
data._agent = data.agent_name;
|
||||
}
|
||||
handleTscmEvent(data.payload || data);
|
||||
}
|
||||
} else {
|
||||
handleTscmEvent(data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('TSCM SSE parse error:', e);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,14 @@
|
||||
<!-- Populated by JavaScript with capability warnings -->
|
||||
</div>
|
||||
|
||||
<!-- Show All Agents option (visible when agents are available) -->
|
||||
<div id="btShowAllAgentsContainer" class="section" style="display: none; padding: 8px;">
|
||||
<label class="inline-checkbox" style="font-size: 10px;">
|
||||
<input type="checkbox" id="btShowAllAgents" onchange="if(typeof BluetoothMode !== 'undefined') BluetoothMode.toggleShowAllAgents(this.checked)">
|
||||
Show devices from all agents
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Scanner Configuration</h3>
|
||||
<div class="form-group">
|
||||
|
||||
@@ -11,6 +11,13 @@
|
||||
</button>
|
||||
</div>
|
||||
<div id="wifiCapabilityStatus" class="info-text" style="margin-top: 8px; font-size: 10px;"></div>
|
||||
<!-- Show All Agents option (visible when agents are available) -->
|
||||
<div id="wifiShowAllAgentsContainer" style="margin-top: 8px; display: none;">
|
||||
<label class="inline-checkbox" style="font-size: 10px;">
|
||||
<input type="checkbox" id="wifiShowAllAgents" onchange="if(typeof WiFiMode !== 'undefined') WiFiMode.toggleShowAllAgents(this.checked)">
|
||||
Show networks from all agents
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
|
||||
Reference in New Issue
Block a user