mirror of
https://github.com/smittix/intercept.git
synced 2026-04-27 08:10:00 -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:
@@ -10,6 +10,8 @@ let currentAgent = 'local';
|
||||
let agentEventSource = null;
|
||||
let multiAgentMode = false; // Show combined results from all agents
|
||||
let multiAgentPollInterval = null;
|
||||
let agentRunningModes = []; // Track agent's running modes for conflict detection
|
||||
let agentRunningModesDetail = {}; // Track device info per mode (for multi-SDR agents)
|
||||
|
||||
// ============== AGENT LOADING ==============
|
||||
|
||||
@@ -54,6 +56,28 @@ function updateAgentSelector() {
|
||||
}
|
||||
|
||||
updateAgentStatus();
|
||||
|
||||
// Show/hide "Show All Agents" options based on whether agents exist
|
||||
updateShowAllAgentsVisibility();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show or hide the "Show All Agents" checkboxes in mode panels.
|
||||
*/
|
||||
function updateShowAllAgentsVisibility() {
|
||||
const hasAgents = agents.length > 0;
|
||||
|
||||
// WiFi "Show All Agents" container
|
||||
const wifiContainer = document.getElementById('wifiShowAllAgentsContainer');
|
||||
if (wifiContainer) {
|
||||
wifiContainer.style.display = hasAgents ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// Bluetooth "Show All Agents" container
|
||||
const btContainer = document.getElementById('btShowAllAgentsContainer');
|
||||
if (btContainer) {
|
||||
btContainer.style.display = hasAgents ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function updateAgentStatus() {
|
||||
@@ -88,10 +112,36 @@ function selectAgent(agentId) {
|
||||
if (typeof refreshDevices === 'function') {
|
||||
refreshDevices();
|
||||
}
|
||||
// Refresh TSCM devices if function exists
|
||||
if (typeof refreshTscmDevices === 'function') {
|
||||
refreshTscmDevices();
|
||||
}
|
||||
// Notify WiFi mode of agent change
|
||||
if (typeof WiFiMode !== 'undefined' && WiFiMode.handleAgentChange) {
|
||||
WiFiMode.handleAgentChange();
|
||||
}
|
||||
// Notify Bluetooth mode of agent change
|
||||
if (typeof BluetoothMode !== 'undefined' && BluetoothMode.handleAgentChange) {
|
||||
BluetoothMode.handleAgentChange();
|
||||
}
|
||||
console.log('Agent selected: Local');
|
||||
} else {
|
||||
// Fetch devices from remote agent
|
||||
refreshAgentDevices(agentId);
|
||||
// Sync mode states with agent's actual running state
|
||||
syncAgentModeStates(agentId);
|
||||
// Refresh TSCM devices for agent
|
||||
if (typeof refreshTscmDevices === 'function') {
|
||||
refreshTscmDevices();
|
||||
}
|
||||
// Notify WiFi mode of agent change
|
||||
if (typeof WiFiMode !== 'undefined' && WiFiMode.handleAgentChange) {
|
||||
WiFiMode.handleAgentChange();
|
||||
}
|
||||
// Notify Bluetooth mode of agent change
|
||||
if (typeof BluetoothMode !== 'undefined' && BluetoothMode.handleAgentChange) {
|
||||
BluetoothMode.handleAgentChange();
|
||||
}
|
||||
const agentName = agents.find(a => a.id == agentId)?.name || 'Unknown';
|
||||
console.log(`Agent selected: ${agentName}`);
|
||||
|
||||
@@ -104,6 +154,287 @@ function selectAgent(agentId) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync UI state with agent's actual running modes.
|
||||
* This ensures UI reflects reality when agent was started externally
|
||||
* or when user navigates away and back.
|
||||
*/
|
||||
async function syncAgentModeStates(agentId) {
|
||||
try {
|
||||
const response = await fetch(`/controller/agents/${agentId}/status`, {
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success' && data.agent_status) {
|
||||
agentRunningModes = data.agent_status.running_modes || [];
|
||||
agentRunningModesDetail = data.agent_status.running_modes_detail || {};
|
||||
console.log(`Agent ${agentId} running modes:`, agentRunningModes);
|
||||
console.log(`Agent ${agentId} mode details:`, agentRunningModesDetail);
|
||||
|
||||
// IMPORTANT: Only sync UI if this agent is currently selected
|
||||
// Otherwise we'd start streams for an agent the user hasn't selected
|
||||
const isSelectedAgent = currentAgent == agentId; // Use == for string/number comparison
|
||||
console.log(`Agent ${agentId} is selected: ${isSelectedAgent} (currentAgent=${currentAgent})`);
|
||||
|
||||
if (isSelectedAgent) {
|
||||
// Update UI for each mode based on agent state
|
||||
agentRunningModes.forEach(mode => {
|
||||
syncModeUI(mode, true, agentId);
|
||||
});
|
||||
|
||||
// Also check modes that might need to be marked as stopped
|
||||
const allModes = ['sensor', 'pager', 'adsb', 'wifi', 'bluetooth', 'ais', 'dsc', 'acars', 'aprs', 'rtlamr', 'tscm', 'satellite', 'listening_post'];
|
||||
allModes.forEach(mode => {
|
||||
if (!agentRunningModes.includes(mode)) {
|
||||
syncModeUI(mode, false, agentId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Show warning if SDR modes are running (always show, regardless of selection)
|
||||
showAgentModeWarnings(agentRunningModes, agentRunningModesDetail);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to sync agent mode states:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show warnings about running modes that may cause conflicts.
|
||||
* @param {string[]} runningModes - List of running mode names
|
||||
* @param {Object} modesDetail - Detail info including device per mode
|
||||
*/
|
||||
function showAgentModeWarnings(runningModes, modesDetail = {}) {
|
||||
// SDR modes that can't run simultaneously on same device
|
||||
const sdrModes = ['sensor', 'pager', 'adsb', 'ais', 'acars', 'aprs', 'rtlamr', 'listening_post', 'tscm', 'dsc'];
|
||||
const runningSdrModes = runningModes.filter(m => sdrModes.includes(m));
|
||||
|
||||
let warning = document.getElementById('agentModeWarning');
|
||||
|
||||
if (runningSdrModes.length > 0) {
|
||||
if (!warning) {
|
||||
// Create warning element if it doesn't exist
|
||||
const agentSection = document.getElementById('agentSection');
|
||||
if (agentSection) {
|
||||
warning = document.createElement('div');
|
||||
warning.id = 'agentModeWarning';
|
||||
warning.style.cssText = 'color: #f0ad4e; font-size: 10px; padding: 4px 8px; background: rgba(240,173,78,0.1); border-radius: 4px; margin-top: 4px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap;';
|
||||
agentSection.appendChild(warning);
|
||||
}
|
||||
}
|
||||
if (warning) {
|
||||
// Build mode buttons with device info
|
||||
const modeButtons = runningSdrModes.map(m => {
|
||||
const detail = modesDetail[m] || {};
|
||||
const deviceNum = detail.device !== undefined ? detail.device : '?';
|
||||
return `<button onclick="stopAgentModeWithRefresh('${m}')" style="background:#ff6b6b;color:#fff;border:none;padding:2px 6px;border-radius:3px;font-size:9px;cursor:pointer;" title="Stop ${m} on agent (SDR ${deviceNum})">${m} (SDR ${deviceNum})</button>`;
|
||||
}).join(' ');
|
||||
warning.innerHTML = `<span>⚠️ Running:</span> ${modeButtons} <button onclick="refreshAgentState()" style="background:#555;color:#fff;border:none;padding:2px 6px;border-radius:3px;font-size:9px;cursor:pointer;" title="Refresh agent state">↻</button>`;
|
||||
warning.style.display = 'flex';
|
||||
}
|
||||
} else if (warning) {
|
||||
warning.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop a mode on the agent and refresh state.
|
||||
*/
|
||||
async function stopAgentModeWithRefresh(mode) {
|
||||
if (currentAgent === 'local') return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/controller/agents/${currentAgent}/${mode}/stop`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
const data = await response.json();
|
||||
console.log(`Stop ${mode} response:`, data);
|
||||
|
||||
// Refresh agent state to update UI
|
||||
await refreshAgentState();
|
||||
} catch (error) {
|
||||
console.error(`Failed to stop ${mode} on agent:`, error);
|
||||
alert(`Failed to stop ${mode}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh agent state from server.
|
||||
*/
|
||||
async function refreshAgentState() {
|
||||
if (currentAgent === 'local') return;
|
||||
|
||||
console.log('Refreshing agent state...');
|
||||
await syncAgentModeStates(currentAgent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a mode requires audio streaming (not supported via agents).
|
||||
* @param {string} mode - Mode name
|
||||
* @returns {boolean} - True if mode requires audio
|
||||
*/
|
||||
function isAudioMode(mode) {
|
||||
const audioModes = ['airband', 'listening_post'];
|
||||
return audioModes.includes(mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the IP/hostname from an agent's base URL.
|
||||
* @param {number|string} agentId - Agent ID
|
||||
* @returns {string|null} - Hostname or null
|
||||
*/
|
||||
function getAgentHost(agentId) {
|
||||
const agent = agents.find(a => a.id == agentId);
|
||||
if (!agent || !agent.base_url) return null;
|
||||
try {
|
||||
const url = new URL(agent.base_url);
|
||||
return url.hostname;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if trying to start an audio mode on a remote agent.
|
||||
* Offers rtl_tcp option instead of just blocking.
|
||||
* @param {string} modeToStart - Mode to start
|
||||
* @returns {boolean} - True if OK to proceed
|
||||
*/
|
||||
function checkAgentAudioMode(modeToStart) {
|
||||
if (currentAgent === 'local') return true;
|
||||
|
||||
if (isAudioMode(modeToStart)) {
|
||||
const agentHost = getAgentHost(currentAgent);
|
||||
const agentName = agents.find(a => a.id == currentAgent)?.name || 'remote agent';
|
||||
|
||||
alert(
|
||||
`Audio streaming is not supported via remote agents.\n\n` +
|
||||
`"${modeToStart}" requires real-time audio.\n\n` +
|
||||
`To use audio from a remote SDR:\n\n` +
|
||||
`1. On the agent (${agentName}):\n` +
|
||||
` Run: rtl_tcp -a 0.0.0.0\n\n` +
|
||||
`2. On the Main Dashboard (/):\n` +
|
||||
` - Select "Local" mode\n` +
|
||||
` - Check "Use Remote SDR (rtl_tcp)"\n` +
|
||||
` - Enter host: ${agentHost || '[agent IP]'}\n` +
|
||||
` - Port: 1234\n\n` +
|
||||
`Note: rtl_tcp config is on the Main Dashboard,\n` +
|
||||
`not on specialized dashboards like ADS-B/AIS.`
|
||||
);
|
||||
|
||||
return false; // Don't proceed with agent mode
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if trying to start a mode that conflicts with running modes.
|
||||
* Returns true if OK to proceed, false if conflict exists.
|
||||
* @param {string} modeToStart - Mode to start
|
||||
* @param {number} deviceToUse - Device index to use (optional, for smarter conflict detection)
|
||||
*/
|
||||
function checkAgentModeConflict(modeToStart, deviceToUse = null) {
|
||||
if (currentAgent === 'local') return true; // No conflict checking for local
|
||||
|
||||
// First check if this is an audio mode
|
||||
if (!checkAgentAudioMode(modeToStart)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const sdrModes = ['sensor', 'pager', 'adsb', 'ais', 'acars', 'aprs', 'rtlamr', 'listening_post', 'tscm', 'dsc'];
|
||||
|
||||
// If we're trying to start an SDR mode
|
||||
if (sdrModes.includes(modeToStart)) {
|
||||
// Check for conflicts - if device is specified, only check that device
|
||||
let conflictingModes = [];
|
||||
|
||||
if (deviceToUse !== null && Object.keys(agentRunningModesDetail).length > 0) {
|
||||
// Smart conflict detection: only flag modes using the same device
|
||||
conflictingModes = agentRunningModes.filter(m => {
|
||||
if (!sdrModes.includes(m) || m === modeToStart) return false;
|
||||
const detail = agentRunningModesDetail[m];
|
||||
return detail && detail.device === deviceToUse;
|
||||
});
|
||||
} else {
|
||||
// Fallback: warn about all running SDR modes
|
||||
conflictingModes = agentRunningModes.filter(m =>
|
||||
sdrModes.includes(m) && m !== modeToStart
|
||||
);
|
||||
}
|
||||
|
||||
if (conflictingModes.length > 0) {
|
||||
const modeList = conflictingModes.map(m => {
|
||||
const detail = agentRunningModesDetail[m];
|
||||
return detail ? `${m} (SDR ${detail.device})` : m;
|
||||
}).join(', ');
|
||||
|
||||
const proceed = confirm(
|
||||
`The agent's SDR device is currently running: ${modeList}\n\n` +
|
||||
`Starting ${modeToStart} on the same device will fail.\n\n` +
|
||||
`Do you want to stop the conflicting mode(s) first?`
|
||||
);
|
||||
|
||||
if (proceed) {
|
||||
// Stop conflicting modes
|
||||
conflictingModes.forEach(mode => {
|
||||
stopAgentModeQuiet(mode);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop a mode on the current agent (without UI feedback).
|
||||
*/
|
||||
async function stopAgentModeQuiet(mode) {
|
||||
if (currentAgent === 'local') return;
|
||||
|
||||
try {
|
||||
await fetch(`/controller/agents/${currentAgent}/${mode}/stop`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
console.log(`Stopped ${mode} on agent ${currentAgent}`);
|
||||
// Remove from running modes
|
||||
agentRunningModes = agentRunningModes.filter(m => m !== mode);
|
||||
syncModeUI(mode, false);
|
||||
showAgentModeWarnings(agentRunningModes);
|
||||
} catch (error) {
|
||||
console.error(`Failed to stop ${mode} on agent:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update UI elements for a specific mode based on running state.
|
||||
* @param {string} mode - Mode name (adsb, wifi, etc.)
|
||||
* @param {boolean} isRunning - Whether the mode is running
|
||||
* @param {string|number|null} agentId - Agent ID if running on agent, null for local
|
||||
*/
|
||||
function syncModeUI(mode, isRunning, agentId = null) {
|
||||
// Map mode names to UI setter functions (if they exist)
|
||||
const uiSetters = {
|
||||
'sensor': 'setSensorRunning',
|
||||
'pager': 'setPagerRunning',
|
||||
'adsb': 'setADSBRunning',
|
||||
'wifi': 'setWiFiRunning',
|
||||
'bluetooth': 'setBluetoothRunning'
|
||||
};
|
||||
|
||||
const setterName = uiSetters[mode];
|
||||
if (setterName && typeof window[setterName] === 'function') {
|
||||
// Pass agent ID as source for functions that support it (like setADSBRunning)
|
||||
window[setterName](isRunning, agentId);
|
||||
console.log(`Synced ${mode} UI state: ${isRunning ? 'running' : 'stopped'} (agent: ${agentId || 'local'})`);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAgentDevices(agentId) {
|
||||
console.log(`Refreshing devices for agent ${agentId}...`);
|
||||
try {
|
||||
@@ -430,14 +761,36 @@ function handleMultiAgentData(data) {
|
||||
break;
|
||||
|
||||
case 'wifi':
|
||||
// WiFi mode handles its own multi-agent stream processing
|
||||
// This is a fallback for legacy display or when WiFi mode isn't active
|
||||
if (payload && payload.networks) {
|
||||
Object.values(payload.networks).forEach(net => {
|
||||
net._agent = agentName;
|
||||
// Use legacy display if available
|
||||
if (typeof handleWifiNetworkImmediate === 'function') {
|
||||
handleWifiNetworkImmediate(net);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (payload && payload.clients) {
|
||||
Object.values(payload.clients).forEach(client => {
|
||||
client._agent = agentName;
|
||||
if (typeof handleWifiClientImmediate === 'function') {
|
||||
handleWifiClientImmediate(client);
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'bluetooth':
|
||||
if (payload && payload.devices) {
|
||||
Object.values(payload.devices).forEach(device => {
|
||||
device._agent = agentName;
|
||||
// Update Bluetooth display if handler exists
|
||||
if (typeof addBluetoothDevice === 'function') {
|
||||
addBluetoothDevice(device);
|
||||
}
|
||||
});
|
||||
// Update WiFi display if handler exists
|
||||
if (typeof WiFiMode !== 'undefined' && WiFiMode.updateNetworks) {
|
||||
WiFiMode.updateNetworks(payload.networks);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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; },
|
||||
|
||||
Reference in New Issue
Block a user