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:
cemaxecuter
2026-01-26 11:44:54 -05:00
parent f980e2e76d
commit b72ddd7c19
14 changed files with 4710 additions and 309 deletions

View File

@@ -9,6 +9,7 @@ const BluetoothMode = (function() {
// State
let isScanning = false;
let eventSource = null;
let agentPollTimer = null; // Polling fallback for agent mode
let devices = new Map();
let baselineSet = false;
let baselineCount = 0;
@@ -36,6 +37,47 @@ const BluetoothMode = (function() {
// Device list filter
let currentDeviceFilter = 'all';
// Agent support
let showAllAgentsMode = false;
let lastAgentId = null;
/**
* Get API base URL, routing through agent proxy if agent is selected.
*/
function getApiBase() {
if (typeof currentAgent !== 'undefined' && currentAgent !== 'local') {
return `/controller/agents/${currentAgent}`;
}
return '';
}
/**
* Get current agent name for tagging data.
*/
function getCurrentAgentName() {
if (typeof currentAgent === 'undefined' || currentAgent === 'local') {
return 'Local';
}
if (typeof agents !== 'undefined') {
const agent = agents.find(a => a.id == currentAgent);
return agent ? agent.name : `Agent ${currentAgent}`;
}
return `Agent ${currentAgent}`;
}
/**
* Check for agent mode conflicts before starting scan.
*/
function checkAgentConflicts() {
if (typeof currentAgent === 'undefined' || currentAgent === 'local') {
return true;
}
if (typeof checkAgentModeConflict === 'function') {
return checkAgentModeConflict('bluetooth');
}
return true;
}
/**
* Initialize the Bluetooth mode
*/
@@ -526,8 +568,37 @@ const BluetoothMode = (function() {
*/
async function checkCapabilities() {
try {
const response = await fetch('/api/bluetooth/capabilities');
const data = await response.json();
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
let data;
if (isAgentMode) {
// Fetch capabilities from agent via controller proxy
const response = await fetch(`/controller/agents/${currentAgent}?refresh=true`);
const agentData = await response.json();
if (agentData.agent && agentData.agent.capabilities) {
const agentCaps = agentData.agent.capabilities;
const agentInterfaces = agentData.agent.interfaces || {};
// Build BT-compatible capabilities object
data = {
available: agentCaps.bluetooth || false,
adapters: (agentInterfaces.bt_adapters || []).map(adapter => ({
id: adapter.id || adapter.name || adapter,
name: adapter.name || adapter,
powered: adapter.powered !== false
})),
issues: [],
preferred_backend: 'auto'
};
console.log('[BT] Agent capabilities:', data);
} else {
data = { available: false, adapters: [], issues: ['Agent does not support Bluetooth'] };
}
} else {
const response = await fetch('/api/bluetooth/capabilities');
data = await response.json();
}
if (!data.available) {
showCapabilityWarning(['Bluetooth not available on this system']);
@@ -599,32 +670,60 @@ const BluetoothMode = (function() {
}
async function startScan() {
// Check for agent mode conflicts
if (!checkAgentConflicts()) {
return;
}
const adapter = adapterSelect?.value || '';
const mode = scanModeSelect?.value || 'auto';
const transport = transportSelect?.value || 'auto';
const duration = parseInt(durationInput?.value || '0', 10);
const minRssi = parseInt(minRssiInput?.value || '-100', 10);
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
try {
const response = await fetch('/api/bluetooth/scan/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
mode: mode,
adapter_id: adapter || undefined,
duration_s: duration > 0 ? duration : undefined,
transport: transport,
rssi_threshold: minRssi
})
});
let response;
if (isAgentMode) {
// Route through agent proxy
response = await fetch(`/controller/agents/${currentAgent}/bluetooth/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
mode: mode,
adapter_id: adapter || undefined,
duration_s: duration > 0 ? duration : undefined,
transport: transport,
rssi_threshold: minRssi
})
});
} else {
response = await fetch('/api/bluetooth/scan/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
mode: mode,
adapter_id: adapter || undefined,
duration_s: duration > 0 ? duration : undefined,
transport: transport,
rssi_threshold: minRssi
})
});
}
const data = await response.json();
if (data.status === 'started' || data.status === 'already_scanning') {
// 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 === 'already_scanning') {
setScanning(true);
startEventStream();
} else if (scanResult.status === 'error') {
showErrorMessage(scanResult.message || 'Failed to start scan');
} else {
showErrorMessage(data.message || 'Failed to start scan');
showErrorMessage(scanResult.message || 'Failed to start scan');
}
} catch (err) {
@@ -634,8 +733,14 @@ const BluetoothMode = (function() {
}
async function stopScan() {
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
try {
await fetch('/api/bluetooth/scan/stop', { method: 'POST' });
if (isAgentMode) {
await fetch(`/controller/agents/${currentAgent}/bluetooth/stop`, { method: 'POST' });
} else {
await fetch('/api/bluetooth/scan/stop', { method: 'POST' });
}
setScanning(false);
stopEventStream();
} catch (err) {
@@ -680,27 +785,84 @@ const BluetoothMode = (function() {
function startEventStream() {
if (eventSource) eventSource.close();
eventSource = new EventSource('/api/bluetooth/stream');
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
const agentName = getCurrentAgentName();
let streamUrl;
eventSource.addEventListener('device_update', (e) => {
try {
const device = JSON.parse(e.data);
handleDeviceUpdate(device);
} catch (err) {
console.error('Failed to parse device update:', err);
}
});
if (isAgentMode) {
// Use multi-agent stream for remote agents
streamUrl = '/controller/stream/all';
console.log('[BT] Starting multi-agent event stream...');
} else {
streamUrl = '/api/bluetooth/stream';
console.log('[BT] Starting local event stream...');
}
eventSource.addEventListener('scan_started', (e) => {
setScanning(true);
});
eventSource = new EventSource(streamUrl);
eventSource.addEventListener('scan_stopped', (e) => {
setScanning(false);
});
if (isAgentMode) {
// Handle multi-agent stream
eventSource.onmessage = (e) => {
try {
const data = JSON.parse(e.data);
// Skip keepalive and non-bluetooth data
if (data.type === 'keepalive') return;
if (data.scan_type !== 'bluetooth') return;
// Filter by current agent if not in "show all" mode
if (!showAllAgentsMode && typeof agents !== 'undefined') {
const currentAgentObj = agents.find(a => a.id == currentAgent);
if (currentAgentObj && data.agent_name && data.agent_name !== currentAgentObj.name) {
return;
}
}
// Transform multi-agent payload to device updates
if (data.payload && data.payload.devices) {
Object.values(data.payload.devices).forEach(device => {
device._agent = data.agent_name || 'Unknown';
handleDeviceUpdate(device);
});
}
} catch (err) {
console.error('Failed to parse multi-agent event:', err);
}
};
// Also start polling as fallback (in case push isn't enabled on agent)
startAgentPolling();
} else {
// Handle local stream
eventSource.addEventListener('device_update', (e) => {
try {
const device = JSON.parse(e.data);
device._agent = 'Local';
handleDeviceUpdate(device);
} catch (err) {
console.error('Failed to parse device update:', err);
}
});
eventSource.addEventListener('scan_started', (e) => {
setScanning(true);
});
eventSource.addEventListener('scan_stopped', (e) => {
setScanning(false);
});
}
eventSource.onerror = () => {
console.warn('Bluetooth SSE connection error');
if (isScanning) {
// Attempt to reconnect
setTimeout(() => {
if (isScanning) {
startEventStream();
}
}, 3000);
}
};
}
@@ -709,6 +871,54 @@ const BluetoothMode = (function() {
eventSource.close();
eventSource = null;
}
if (agentPollTimer) {
clearInterval(agentPollTimer);
agentPollTimer = null;
}
}
/**
* Start polling agent data as fallback when push isn't enabled.
* This polls the controller proxy endpoint for agent data.
*/
function startAgentPolling() {
if (agentPollTimer) return;
const pollInterval = 3000; // 3 seconds
console.log('[BT] Starting agent polling fallback...');
agentPollTimer = setInterval(async () => {
if (!isScanning) {
clearInterval(agentPollTimer);
agentPollTimer = null;
return;
}
try {
const response = await fetch(`/controller/agents/${currentAgent}/bluetooth/data`);
if (!response.ok) return;
const result = await response.json();
const data = result.data || result;
// Process devices from polling response
if (data && data.devices) {
const agentName = getCurrentAgentName();
Object.values(data.devices).forEach(device => {
device._agent = agentName;
handleDeviceUpdate(device);
});
} else if (data && Array.isArray(data)) {
const agentName = getCurrentAgentName();
data.forEach(device => {
device._agent = agentName;
handleDeviceUpdate(device);
});
}
} catch (err) {
console.debug('[BT] Agent poll error:', err);
}
}, pollInterval);
}
function handleDeviceUpdate(device) {
@@ -876,6 +1086,7 @@ const BluetoothMode = (function() {
const trackerType = device.tracker_type;
const trackerConfidence = device.tracker_confidence;
const riskScore = device.risk_score || 0;
const agentName = device._agent || 'Local';
// Calculate RSSI bar width (0-100%)
// RSSI typically ranges from -100 (weak) to -30 (very strong)
@@ -929,6 +1140,10 @@ const BluetoothMode = (function() {
let secondaryParts = [addr];
if (mfr) secondaryParts.push(mfr);
secondaryParts.push('Seen ' + seenCount + '×');
// Add agent name if not Local
if (agentName !== 'Local') {
secondaryParts.push('<span class="agent-badge agent-remote" style="font-size:8px;padding:1px 4px;">' + escapeHtml(agentName) + '</span>');
}
const secondaryInfo = secondaryParts.join(' · ');
// Row border color - highlight trackers in red/orange
@@ -1019,6 +1234,112 @@ const BluetoothMode = (function() {
function showErrorMessage(message) {
console.error('[BT] Error:', message);
if (typeof showNotification === 'function') {
showNotification('Bluetooth Error', message, 'error');
}
}
function showInfo(message) {
console.log('[BT]', message);
if (typeof showNotification === 'function') {
showNotification('Bluetooth', message, 'info');
}
}
// ==========================================================================
// Agent Handling
// ==========================================================================
/**
* Handle agent change - refresh adapters and optionally clear data.
*/
function handleAgentChange() {
const currentAgentId = typeof currentAgent !== 'undefined' ? currentAgent : 'local';
// Check if agent actually changed
if (lastAgentId === currentAgentId) return;
console.log('[BT] Agent changed from', lastAgentId, 'to', currentAgentId);
// Stop any running scan
if (isScanning) {
stopScan();
}
// Clear existing data when switching agents (unless "Show All" is enabled)
if (!showAllAgentsMode) {
clearData();
showInfo(`Switched to ${getCurrentAgentName()} - previous data cleared`);
}
// Refresh capabilities for new agent
checkCapabilities();
lastAgentId = currentAgentId;
}
/**
* Clear all collected data.
*/
function clearData() {
devices.clear();
resetStats();
if (deviceContainer) {
deviceContainer.innerHTML = '';
}
updateDeviceCount();
updateProximityZones();
updateRadar();
}
/**
* Toggle "Show All Agents" mode.
*/
function toggleShowAllAgents(enabled) {
showAllAgentsMode = enabled;
console.log('[BT] Show all agents mode:', enabled);
if (enabled) {
// If currently scanning, switch to multi-agent stream
if (isScanning && eventSource) {
eventSource.close();
startEventStream();
}
showInfo('Showing Bluetooth devices from all agents');
} else {
// Filter to current agent only
filterToCurrentAgent();
}
}
/**
* Filter devices to only show those from current agent.
*/
function filterToCurrentAgent() {
const agentName = getCurrentAgentName();
const toRemove = [];
devices.forEach((device, deviceId) => {
if (device._agent && device._agent !== agentName) {
toRemove.push(deviceId);
}
});
toRemove.forEach(deviceId => devices.delete(deviceId));
// Re-render device list
if (deviceContainer) {
deviceContainer.innerHTML = '';
devices.forEach(device => renderDevice(device));
}
updateDeviceCount();
updateStatsFromDevices();
updateVisualizationPanels();
updateProximityZones();
updateRadar();
}
// Public API
@@ -1033,8 +1354,16 @@ const BluetoothMode = (function() {
selectDevice,
clearSelection,
copyAddress,
// Agent handling
handleAgentChange,
clearData,
toggleShowAllAgents,
// Getters
getDevices: () => Array.from(devices.values()),
isScanning: () => isScanning
isScanning: () => isScanning,
isShowAllAgents: () => showAllAgentsMode
};
})();

View File

@@ -42,6 +42,10 @@ let recentSignalHits = new Map();
let isDirectListening = false;
let currentModulation = 'am';
// Agent mode state
let listeningPostCurrentAgent = null;
let listeningPostPollTimer = null;
// ============== PRESETS ==============
const scannerPresets = {
@@ -145,6 +149,10 @@ function startScanner() {
const dwell = dwellSelect ? parseInt(dwellSelect.value) : 10;
const device = getSelectedDevice();
// Check if using agent mode
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
listeningPostCurrentAgent = isAgentMode ? currentAgent : null;
if (startFreq >= endFreq) {
if (typeof showNotification === 'function') {
showNotification('Scanner Error', 'End frequency must be greater than start');
@@ -152,8 +160,8 @@ function startScanner() {
return;
}
// Check if device is available
if (typeof checkDeviceAvailability === 'function' && !checkDeviceAvailability('scanner')) {
// Check if device is available (only for local mode)
if (!isAgentMode && typeof checkDeviceAvailability === 'function' && !checkDeviceAvailability('scanner')) {
return;
}
@@ -181,7 +189,12 @@ function startScanner() {
document.getElementById('mainRangeEnd').textContent = endFreq.toFixed(1) + ' MHz';
}
fetch('/listening/scanner/start', {
// Determine endpoint based on agent mode
const endpoint = isAgentMode
? `/controller/agents/${currentAgent}/listening_post/start`
: '/listening/scanner/start';
fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -198,8 +211,11 @@ function startScanner() {
})
.then(r => r.json())
.then(data => {
if (data.status === 'started') {
if (typeof reserveDevice === 'function') reserveDevice(device, 'scanner');
// Handle controller proxy response format
const scanResult = isAgentMode && data.result ? data.result : data;
if (scanResult.status === 'started' || scanResult.status === 'success') {
if (!isAgentMode && typeof reserveDevice === 'function') reserveDevice(device, 'scanner');
isScannerRunning = true;
isScannerPaused = false;
scannerSignalActive = false;
@@ -229,7 +245,7 @@ function startScanner() {
const levelMeter = document.getElementById('scannerLevelMeter');
if (levelMeter) levelMeter.style.display = 'block';
connectScannerStream();
connectScannerStream(isAgentMode);
addScannerLogEntry('Scanner started', `Range: ${startFreq}-${endFreq} MHz, Step: ${step} kHz`);
if (typeof showNotification === 'function') {
showNotification('Scanner Started', `Scanning ${startFreq} - ${endFreq} MHz`);
@@ -237,7 +253,7 @@ function startScanner() {
} else {
updateScannerDisplay('ERROR', 'var(--accent-red)');
if (typeof showNotification === 'function') {
showNotification('Scanner Error', data.message || 'Failed to start');
showNotification('Scanner Error', scanResult.message || scanResult.error || 'Failed to start');
}
}
})
@@ -252,13 +268,25 @@ function startScanner() {
}
function stopScanner() {
fetch('/listening/scanner/stop', { method: 'POST' })
const isAgentMode = listeningPostCurrentAgent !== null;
const endpoint = isAgentMode
? `/controller/agents/${listeningPostCurrentAgent}/listening_post/stop`
: '/listening/scanner/stop';
fetch(endpoint, { method: 'POST' })
.then(() => {
if (typeof releaseDevice === 'function') releaseDevice('scanner');
if (!isAgentMode && typeof releaseDevice === 'function') releaseDevice('scanner');
listeningPostCurrentAgent = null;
isScannerRunning = false;
isScannerPaused = false;
scannerSignalActive = false;
// Clear polling timer
if (listeningPostPollTimer) {
clearInterval(listeningPostPollTimer);
listeningPostPollTimer = null;
}
// Update sidebar (with null checks)
const startBtn = document.getElementById('scannerStartBtn');
if (startBtn) {
@@ -386,17 +414,29 @@ function skipSignal() {
// ============== SCANNER STREAM ==============
function connectScannerStream() {
function connectScannerStream(isAgentMode = false) {
if (scannerEventSource) {
scannerEventSource.close();
}
scannerEventSource = new EventSource('/listening/scanner/stream');
// Use different stream endpoint for agent mode
const streamUrl = isAgentMode ? '/controller/stream/all' : '/listening/scanner/stream';
scannerEventSource = new EventSource(streamUrl);
scannerEventSource.onmessage = function(e) {
try {
const data = JSON.parse(e.data);
handleScannerEvent(data);
if (isAgentMode) {
// Handle multi-agent stream format
if (data.scan_type === 'listening_post' && data.payload) {
const payload = data.payload;
payload.agent_name = data.agent_name;
handleScannerEvent(payload);
}
} else {
handleScannerEvent(data);
}
} catch (err) {
console.warn('Scanner parse error:', err);
}
@@ -404,9 +444,68 @@ function connectScannerStream() {
scannerEventSource.onerror = function() {
if (isScannerRunning) {
setTimeout(connectScannerStream, 2000);
setTimeout(() => connectScannerStream(isAgentMode), 2000);
}
};
// Start polling fallback for agent mode
if (isAgentMode) {
startListeningPostPolling();
}
}
// Track last activity count for polling
let lastListeningPostActivityCount = 0;
function startListeningPostPolling() {
if (listeningPostPollTimer) return;
lastListeningPostActivityCount = 0;
const pollInterval = 2000;
listeningPostPollTimer = setInterval(async () => {
if (!isScannerRunning || !listeningPostCurrentAgent) {
clearInterval(listeningPostPollTimer);
listeningPostPollTimer = null;
return;
}
try {
const response = await fetch(`/controller/agents/${listeningPostCurrentAgent}/listening_post/data`);
if (!response.ok) return;
const data = await response.json();
const result = data.result || data;
const modeData = result.data || {};
// Process activity from polling response
const activity = modeData.activity || [];
if (activity.length > lastListeningPostActivityCount) {
const newActivity = activity.slice(lastListeningPostActivityCount);
newActivity.forEach(item => {
// Convert to scanner event format
const event = {
type: 'signal_found',
frequency: item.frequency,
level: item.level || item.signal_level,
modulation: item.modulation,
agent_name: result.agent_name || 'Remote Agent'
};
handleScannerEvent(event);
});
lastListeningPostActivityCount = activity.length;
}
// Update current frequency if available
if (modeData.current_freq) {
handleScannerEvent({
type: 'freq_change',
frequency: modeData.current_freq
});
}
} catch (err) {
console.error('Listening Post polling error:', err);
}
}, pollInterval);
}
function handleScannerEvent(data) {

View File

@@ -28,6 +28,47 @@ const WiFiMode = (function() {
maxProbes: 1000,
};
// ==========================================================================
// Agent Support
// ==========================================================================
/**
* Get the API base URL, routing through agent proxy if agent is selected.
*/
function getApiBase() {
if (typeof currentAgent !== 'undefined' && currentAgent !== 'local') {
return `/controller/agents/${currentAgent}/wifi/v2`;
}
return CONFIG.apiBase;
}
/**
* Get the current agent name for tagging data.
*/
function getCurrentAgentName() {
if (typeof currentAgent === 'undefined' || currentAgent === 'local') {
return 'Local';
}
if (typeof agents !== 'undefined') {
const agent = agents.find(a => a.id == currentAgent);
return agent ? agent.name : `Agent ${currentAgent}`;
}
return `Agent ${currentAgent}`;
}
/**
* Check for agent mode conflicts before starting WiFi scan.
*/
function checkAgentConflicts() {
if (typeof currentAgent === 'undefined' || currentAgent === 'local') {
return true;
}
if (typeof checkAgentModeConflict === 'function') {
return checkAgentModeConflict('wifi');
}
return true;
}
// ==========================================================================
// State
// ==========================================================================
@@ -49,6 +90,10 @@ const WiFiMode = (function() {
let currentFilter = 'all';
let currentSort = { field: 'rssi', order: 'desc' };
// Agent state
let showAllAgentsMode = false; // Show combined results from all agents
let lastAgentId = null; // Track agent switches
// Capabilities
let capabilities = null;
@@ -154,11 +199,43 @@ const WiFiMode = (function() {
async function checkCapabilities() {
try {
const response = await fetch(`${CONFIG.apiBase}/capabilities`);
if (!response.ok) throw new Error('Failed to fetch capabilities');
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
let response;
capabilities = await response.json();
console.log('[WiFiMode] Capabilities:', capabilities);
if (isAgentMode) {
// Fetch capabilities from agent via controller proxy
response = await fetch(`/controller/agents/${currentAgent}?refresh=true`);
if (!response.ok) throw new Error('Failed to fetch agent capabilities');
const data = await response.json();
// Extract WiFi capabilities from agent data
if (data.agent && data.agent.capabilities) {
const agentCaps = data.agent.capabilities;
const agentInterfaces = data.agent.interfaces || {};
// Build WiFi-compatible capabilities object
capabilities = {
can_quick_scan: agentCaps.wifi || false,
can_deep_scan: agentCaps.wifi || false,
interfaces: (agentInterfaces.wifi_interfaces || []).map(iface => ({
name: iface.name || iface,
supports_monitor: iface.supports_monitor !== false
})),
default_interface: agentInterfaces.default_wifi || null,
preferred_quick_tool: 'agent',
issues: []
};
console.log('[WiFiMode] Agent capabilities:', capabilities);
} else {
throw new Error('Agent does not support WiFi mode');
}
} else {
// Local capabilities
response = await fetch(`${CONFIG.apiBase}/capabilities`);
if (!response.ok) throw new Error('Failed to fetch capabilities');
capabilities = await response.json();
console.log('[WiFiMode] Local capabilities:', capabilities);
}
updateCapabilityUI();
populateInterfaceSelect();
@@ -282,17 +359,34 @@ const WiFiMode = (function() {
async function startQuickScan() {
if (isScanning) return;
// Check for agent mode conflicts
if (!checkAgentConflicts()) {
return;
}
console.log('[WiFiMode] Starting quick scan...');
setScanning(true, 'quick');
try {
const iface = elements.interfaceSelect?.value || null;
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
const agentName = getCurrentAgentName();
const response = await fetch(`${CONFIG.apiBase}/scan/quick`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ interface: iface }),
});
let response;
if (isAgentMode) {
// Route through agent proxy
response = await fetch(`/controller/agents/${currentAgent}/wifi/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ interface: iface, scan_type: 'quick' }),
});
} else {
response = await fetch(`${CONFIG.apiBase}/scan/quick`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ interface: iface }),
});
}
if (!response.ok) {
const error = await response.json();
@@ -302,20 +396,26 @@ const WiFiMode = (function() {
const result = await response.json();
console.log('[WiFiMode] Quick scan complete:', result);
// Handle controller proxy response format (agent response is nested in 'result')
const scanResult = isAgentMode && result.result ? result.result : result;
// Check for error first
if (result.error) {
console.error('[WiFiMode] Quick scan error from server:', result.error);
showError(result.error);
if (scanResult.error || scanResult.status === 'error') {
console.error('[WiFiMode] Quick scan error from server:', scanResult.error || scanResult.message);
showError(scanResult.error || scanResult.message || 'Quick scan failed');
setScanning(false);
return;
}
// Handle agent response format
let accessPoints = scanResult.access_points || scanResult.networks || [];
// Check if we got results
if (!result.access_points || result.access_points.length === 0) {
if (accessPoints.length === 0) {
// No error but no results
let msg = 'Quick scan found no networks in range.';
if (result.warnings && result.warnings.length > 0) {
msg += ' Warnings: ' + result.warnings.join('; ');
if (scanResult.warnings && scanResult.warnings.length > 0) {
msg += ' Warnings: ' + scanResult.warnings.join('; ');
}
console.warn('[WiFiMode] ' + msg);
showError(msg + ' Try Deep Scan with monitor mode.');
@@ -323,13 +423,18 @@ const WiFiMode = (function() {
return;
}
// Tag results with agent source
accessPoints.forEach(ap => {
ap._agent = agentName;
});
// Show any warnings even on success
if (result.warnings && result.warnings.length > 0) {
console.warn('[WiFiMode] Quick scan warnings:', result.warnings);
if (scanResult.warnings && scanResult.warnings.length > 0) {
console.warn('[WiFiMode] Quick scan warnings:', scanResult.warnings);
}
// Process results
processQuickScanResult(result);
processQuickScanResult({ ...scanResult, access_points: accessPoints });
// For quick scan, we're done after one scan
// But keep polling if user wants continuous updates
@@ -346,6 +451,11 @@ const WiFiMode = (function() {
async function startDeepScan() {
if (isScanning) return;
// Check for agent mode conflicts
if (!checkAgentConflicts()) {
return;
}
console.log('[WiFiMode] Starting deep scan...');
setScanning(true, 'deep');
@@ -353,22 +463,48 @@ const WiFiMode = (function() {
const iface = elements.interfaceSelect?.value || null;
const band = document.getElementById('wifiBand')?.value || 'all';
const channel = document.getElementById('wifiChannel')?.value || null;
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
const response = await fetch(`${CONFIG.apiBase}/scan/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
interface: iface,
band: band === 'abg' ? 'all' : band === 'bg' ? '2.4' : '5',
channel: channel ? parseInt(channel) : null,
}),
});
let response;
if (isAgentMode) {
// Route through agent proxy
response = await fetch(`/controller/agents/${currentAgent}/wifi/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
interface: iface,
scan_type: 'deep',
band: band === 'abg' ? 'all' : band === 'bg' ? '2.4' : '5',
channel: channel ? parseInt(channel) : null,
}),
});
} else {
response = await fetch(`${CONFIG.apiBase}/scan/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
interface: iface,
band: band === 'abg' ? 'all' : band === 'bg' ? '2.4' : '5',
channel: channel ? parseInt(channel) : null,
}),
});
}
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to start deep scan');
}
// Check for agent error in response
if (isAgentMode) {
const result = await response.json();
const scanResult = result.result || result;
if (scanResult.status === 'error') {
throw new Error(scanResult.message || 'Agent failed to start deep scan');
}
console.log('[WiFiMode] Agent deep scan started:', scanResult);
}
// Start SSE stream for real-time updates
startEventStream();
} catch (error) {
@@ -393,13 +529,17 @@ const WiFiMode = (function() {
eventSource = null;
}
// Stop deep scan on server
if (scanMode === 'deep') {
try {
// Stop scan on server (local or agent)
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
try {
if (isAgentMode) {
await fetch(`/controller/agents/${currentAgent}/wifi/stop`, { method: 'POST' });
} else if (scanMode === 'deep') {
await fetch(`${CONFIG.apiBase}/scan/stop`, { method: 'POST' });
} catch (error) {
console.warn('[WiFiMode] Error stopping scan:', error);
}
} catch (error) {
console.warn('[WiFiMode] Error stopping scan:', error);
}
setScanning(false);
@@ -517,8 +657,20 @@ const WiFiMode = (function() {
eventSource.close();
}
console.log('[WiFiMode] Starting event stream...');
eventSource = new EventSource(`${CONFIG.apiBase}/stream`);
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
const agentName = getCurrentAgentName();
let streamUrl;
if (isAgentMode) {
// Use multi-agent stream for remote agents
streamUrl = '/controller/stream/all';
console.log('[WiFiMode] Starting multi-agent event stream...');
} else {
streamUrl = `${CONFIG.apiBase}/stream`;
console.log('[WiFiMode] Starting local event stream...');
}
eventSource = new EventSource(streamUrl);
eventSource.onopen = () => {
console.log('[WiFiMode] Event stream connected');
@@ -527,7 +679,46 @@ const WiFiMode = (function() {
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
handleStreamEvent(data);
// For multi-agent stream, filter and transform data
if (isAgentMode) {
// Skip keepalive and non-wifi data
if (data.type === 'keepalive') return;
if (data.scan_type !== 'wifi') return;
// Filter by current agent if not in "show all" mode
if (!showAllAgentsMode && typeof agents !== 'undefined') {
const currentAgentObj = agents.find(a => a.id == currentAgent);
if (currentAgentObj && data.agent_name && data.agent_name !== currentAgentObj.name) {
return;
}
}
// Transform multi-agent payload to stream event format
if (data.payload && data.payload.networks) {
data.payload.networks.forEach(net => {
net._agent = data.agent_name || 'Unknown';
handleStreamEvent({
type: 'network_update',
network: net
});
});
}
if (data.payload && data.payload.clients) {
data.payload.clients.forEach(client => {
client._agent = data.agent_name || 'Unknown';
handleStreamEvent({
type: 'client_update',
client: client
});
});
}
} else {
// Local stream - tag with local
if (data.network) data.network._agent = 'Local';
if (data.client) data.client._agent = 'Local';
handleStreamEvent(data);
}
} catch (error) {
console.debug('[WiFiMode] Event parse error:', error);
}
@@ -745,6 +936,10 @@ const WiFiMode = (function() {
const hiddenBadge = network.is_hidden ? '<span class="badge badge-hidden">Hidden</span>' : '';
const newBadge = network.is_new ? '<span class="badge badge-new">New</span>' : '';
// Agent source badge
const agentName = network._agent || 'Local';
const agentClass = agentName === 'Local' ? 'agent-local' : 'agent-remote';
return `
<tr class="wifi-network-row ${network.bssid === selectedNetwork ? 'selected' : ''}"
data-bssid="${escapeHtml(network.bssid)}"
@@ -762,6 +957,9 @@ const WiFiMode = (function() {
<span class="security-badge ${securityClass}">${escapeHtml(network.security)}</span>
</td>
<td class="col-clients">${network.client_count || 0}</td>
<td class="col-agent">
<span class="agent-badge ${agentClass}">${escapeHtml(agentName)}</span>
</td>
</tr>
`;
}
@@ -1071,6 +1269,113 @@ const WiFiMode = (function() {
}
}
// ==========================================================================
// Agent Handling
// ==========================================================================
/**
* Handle agent change - refresh interfaces and optionally clear data.
* Called when user selects a different agent.
*/
function handleAgentChange() {
const currentAgentId = typeof currentAgent !== 'undefined' ? currentAgent : 'local';
// Check if agent actually changed
if (lastAgentId === currentAgentId) return;
console.log('[WiFiMode] Agent changed from', lastAgentId, 'to', currentAgentId);
// Stop any running scan
if (isScanning) {
stopScan();
}
// Clear existing data when switching agents (unless "Show All" is enabled)
if (!showAllAgentsMode) {
clearData();
showInfo(`Switched to ${getCurrentAgentName()} - previous data cleared`);
}
// Refresh capabilities for new agent
checkCapabilities();
lastAgentId = currentAgentId;
}
/**
* Clear all collected data.
*/
function clearData() {
networks.clear();
clients.clear();
probeRequests = [];
channelStats = [];
recommendations = [];
updateNetworkTable();
updateStats();
updateProximityRadar();
updateChannelChart();
}
/**
* Toggle "Show All Agents" mode.
* When enabled, displays combined WiFi results from all agents.
*/
function toggleShowAllAgents(enabled) {
showAllAgentsMode = enabled;
console.log('[WiFiMode] Show all agents mode:', enabled);
if (enabled) {
// If currently scanning, switch to multi-agent stream
if (isScanning && eventSource) {
eventSource.close();
startEventStream();
}
showInfo('Showing WiFi networks from all agents');
} else {
// Filter to current agent only
filterToCurrentAgent();
}
}
/**
* Filter networks to only show those from current agent.
*/
function filterToCurrentAgent() {
const agentName = getCurrentAgentName();
const toRemove = [];
networks.forEach((network, bssid) => {
if (network._agent && network._agent !== agentName) {
toRemove.push(bssid);
}
});
toRemove.forEach(bssid => networks.delete(bssid));
// Also filter clients
const clientsToRemove = [];
clients.forEach((client, mac) => {
if (client._agent && client._agent !== agentName) {
clientsToRemove.push(mac);
}
});
clientsToRemove.forEach(mac => clients.delete(mac));
updateNetworkTable();
updateStats();
updateProximityRadar();
}
/**
* Refresh WiFi interfaces from current agent.
* Called when agent changes.
*/
async function refreshInterfaces() {
await checkCapabilities();
}
// ==========================================================================
// Public API
// ==========================================================================
@@ -1086,12 +1391,19 @@ const WiFiMode = (function() {
exportData,
checkCapabilities,
// Agent handling
handleAgentChange,
clearData,
toggleShowAllAgents,
refreshInterfaces,
// Getters
getNetworks: () => Array.from(networks.values()),
getClients: () => Array.from(clients.values()),
getProbes: () => [...probeRequests],
isScanning: () => isScanning,
getScanMode: () => scanMode,
isShowAllAgents: () => showAllAgentsMode,
// Callbacks
onNetworkUpdate: (cb) => { onNetworkUpdate = cb; },