mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 14:50:00 -07:00
Add distributed agent architecture for multi-node signal intelligence
Features: - Standalone agent server (intercept_agent.py) for remote sensor nodes - Controller API blueprint for agent management and data aggregation - Push mechanism for agents to send data to controller - Pull mechanism for controller to proxy requests to agents - Multi-agent SSE stream for combined data view - Agent management page at /controller/manage - Agent selector dropdown in main UI - GPS integration for location tagging - API key authentication for secure agent communication - Integration with Intercept's dependency checking system New files: - intercept_agent.py: Remote agent HTTP server - intercept_agent.cfg: Agent configuration template - routes/controller.py: Controller API endpoints - utils/agent_client.py: HTTP client for agents - utils/trilateration.py: Multi-agent position calculation - static/js/core/agents.js: Frontend agent management - templates/agents.html: Agent management page - docs/DISTRIBUTED_AGENTS.md: System documentation Modified: - app.py: Register controller blueprint - utils/database.py: Add agents and push_payloads tables - templates/index.html: Add agent selector section
This commit is contained in:
450
static/js/core/agents.js
Normal file
450
static/js/core/agents.js
Normal file
@@ -0,0 +1,450 @@
|
||||
/**
|
||||
* Intercept - Agent Manager
|
||||
* Handles remote agent selection and API routing
|
||||
*/
|
||||
|
||||
// ============== AGENT STATE ==============
|
||||
|
||||
let agents = [];
|
||||
let currentAgent = 'local';
|
||||
let agentEventSource = null;
|
||||
let multiAgentMode = false; // Show combined results from all agents
|
||||
let multiAgentPollInterval = null;
|
||||
|
||||
// ============== AGENT LOADING ==============
|
||||
|
||||
async function loadAgents() {
|
||||
try {
|
||||
const response = await fetch('/controller/agents');
|
||||
const data = await response.json();
|
||||
agents = data.agents || [];
|
||||
updateAgentSelector();
|
||||
return agents;
|
||||
} catch (error) {
|
||||
console.error('Failed to load agents:', error);
|
||||
agents = [];
|
||||
updateAgentSelector();
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function updateAgentSelector() {
|
||||
const selector = document.getElementById('agentSelect');
|
||||
if (!selector) return;
|
||||
|
||||
// Keep current selection if possible
|
||||
const currentValue = selector.value;
|
||||
|
||||
// Clear and rebuild options
|
||||
selector.innerHTML = '<option value="local">Local (This Device)</option>';
|
||||
|
||||
agents.forEach(agent => {
|
||||
const option = document.createElement('option');
|
||||
option.value = agent.id;
|
||||
const status = agent.healthy !== false ? '●' : '○';
|
||||
option.textContent = `${status} ${agent.name}`;
|
||||
option.dataset.baseUrl = agent.base_url;
|
||||
option.dataset.healthy = agent.healthy !== false;
|
||||
selector.appendChild(option);
|
||||
});
|
||||
|
||||
// Restore selection if still valid
|
||||
if (currentValue && selector.querySelector(`option[value="${currentValue}"]`)) {
|
||||
selector.value = currentValue;
|
||||
}
|
||||
|
||||
updateAgentStatus();
|
||||
}
|
||||
|
||||
function updateAgentStatus() {
|
||||
const selector = document.getElementById('agentSelect');
|
||||
const statusDot = document.getElementById('agentStatusDot');
|
||||
const statusText = document.getElementById('agentStatusText');
|
||||
|
||||
if (!selector || !statusDot) return;
|
||||
|
||||
if (currentAgent === 'local') {
|
||||
statusDot.className = 'agent-status-dot online';
|
||||
if (statusText) statusText.textContent = 'Local';
|
||||
} else {
|
||||
const agent = agents.find(a => a.id == currentAgent);
|
||||
if (agent) {
|
||||
const isOnline = agent.healthy !== false;
|
||||
statusDot.className = `agent-status-dot ${isOnline ? 'online' : 'offline'}`;
|
||||
if (statusText) statusText.textContent = isOnline ? 'Connected' : 'Offline';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============== AGENT SELECTION ==============
|
||||
|
||||
function selectAgent(agentId) {
|
||||
currentAgent = agentId;
|
||||
updateAgentStatus();
|
||||
|
||||
// Update device list based on selected agent
|
||||
if (agentId === 'local') {
|
||||
// Use local devices - call refreshDevices if it exists (defined in main page)
|
||||
if (typeof refreshDevices === 'function') {
|
||||
refreshDevices();
|
||||
}
|
||||
console.log('Agent selected: Local');
|
||||
} else {
|
||||
// Fetch devices from remote agent
|
||||
refreshAgentDevices(agentId);
|
||||
const agentName = agents.find(a => a.id == agentId)?.name || 'Unknown';
|
||||
console.log(`Agent selected: ${agentName}`);
|
||||
|
||||
// Show visual feedback
|
||||
const statusText = document.getElementById('agentStatusText');
|
||||
if (statusText) {
|
||||
statusText.textContent = `Loading ${agentName}...`;
|
||||
setTimeout(() => updateAgentStatus(), 2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAgentDevices(agentId) {
|
||||
console.log(`Refreshing devices for agent ${agentId}...`);
|
||||
try {
|
||||
const response = await fetch(`/controller/agents/${agentId}?refresh=true`, {
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
const data = await response.json();
|
||||
console.log('Agent data received:', data);
|
||||
|
||||
if (data.agent && data.agent.interfaces) {
|
||||
const devices = data.agent.interfaces.devices || [];
|
||||
console.log(`Found ${devices.length} devices on agent`);
|
||||
populateDeviceSelect(devices);
|
||||
|
||||
// Update SDR type dropdown if device has sdr_type
|
||||
if (devices.length > 0 && devices[0].sdr_type) {
|
||||
const sdrTypeSelect = document.getElementById('sdrTypeSelect');
|
||||
if (sdrTypeSelect) {
|
||||
sdrTypeSelect.value = devices[0].sdr_type;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn('No interfaces found in agent data');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh agent devices:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function populateDeviceSelect(devices) {
|
||||
const select = document.getElementById('deviceSelect');
|
||||
if (!select) return;
|
||||
|
||||
select.innerHTML = '';
|
||||
|
||||
if (devices.length === 0) {
|
||||
const option = document.createElement('option');
|
||||
option.value = '0';
|
||||
option.textContent = 'No devices found';
|
||||
select.appendChild(option);
|
||||
} else {
|
||||
devices.forEach(device => {
|
||||
const option = document.createElement('option');
|
||||
option.value = device.index;
|
||||
option.dataset.sdrType = device.sdr_type || 'rtlsdr';
|
||||
option.textContent = `${device.index}: ${device.name}`;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ============== API ROUTING ==============
|
||||
|
||||
/**
|
||||
* Route an API call to local or remote agent based on current selection.
|
||||
* @param {string} localPath - Local API path (e.g., '/sensor/start')
|
||||
* @param {Object} options - Fetch options
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async function agentFetch(localPath, options = {}) {
|
||||
if (currentAgent === 'local') {
|
||||
return fetch(localPath, options);
|
||||
}
|
||||
|
||||
// Route through controller proxy
|
||||
const proxyPath = `/controller/agents/${currentAgent}${localPath}`;
|
||||
return fetch(proxyPath, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a mode on the selected agent.
|
||||
* @param {string} mode - Mode name (pager, sensor, adsb, wifi, etc.)
|
||||
* @param {Object} params - Mode parameters
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function agentStartMode(mode, params = {}) {
|
||||
const path = `/${mode}/start`;
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(params)
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await agentFetch(path, options);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`Failed to start ${mode} on agent:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop a mode on the selected agent.
|
||||
* @param {string} mode - Mode name
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function agentStopMode(mode) {
|
||||
const path = `/${mode}/stop`;
|
||||
const options = { method: 'POST' };
|
||||
|
||||
try {
|
||||
const response = await agentFetch(path, options);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`Failed to stop ${mode} on agent:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data from a mode on the selected agent.
|
||||
* @param {string} mode - Mode name
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function agentGetData(mode) {
|
||||
const path = `/${mode}/data`;
|
||||
|
||||
try {
|
||||
const response = await agentFetch(path);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`Failed to get ${mode} data from agent:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ============== SSE STREAM ==============
|
||||
|
||||
/**
|
||||
* Connect to SSE stream (local or multi-agent).
|
||||
* @param {string} mode - Mode name for the stream
|
||||
* @param {function} onMessage - Callback for messages
|
||||
* @returns {EventSource}
|
||||
*/
|
||||
function connectAgentStream(mode, onMessage) {
|
||||
// Close existing connection
|
||||
if (agentEventSource) {
|
||||
agentEventSource.close();
|
||||
}
|
||||
|
||||
let streamUrl;
|
||||
if (currentAgent === 'local') {
|
||||
streamUrl = `/${mode}/stream`;
|
||||
} else {
|
||||
// For remote agents, we could either:
|
||||
// 1. Use the multi-agent stream: /controller/stream/all
|
||||
// 2. Or proxy through controller (not implemented yet)
|
||||
// For now, use multi-agent stream which includes agent_name tagging
|
||||
streamUrl = '/controller/stream/all';
|
||||
}
|
||||
|
||||
agentEventSource = new EventSource(streamUrl);
|
||||
|
||||
agentEventSource.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// If using multi-agent stream, filter by current agent if needed
|
||||
if (streamUrl === '/controller/stream/all' && currentAgent !== 'local') {
|
||||
const agent = agents.find(a => a.id == currentAgent);
|
||||
if (agent && data.agent_name && data.agent_name !== agent.name) {
|
||||
return; // Skip messages from other agents
|
||||
}
|
||||
}
|
||||
|
||||
onMessage(data);
|
||||
} catch (e) {
|
||||
console.error('Error parsing SSE message:', e);
|
||||
}
|
||||
};
|
||||
|
||||
agentEventSource.onerror = (error) => {
|
||||
console.error('SSE connection error:', error);
|
||||
};
|
||||
|
||||
return agentEventSource;
|
||||
}
|
||||
|
||||
function disconnectAgentStream() {
|
||||
if (agentEventSource) {
|
||||
agentEventSource.close();
|
||||
agentEventSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============== INITIALIZATION ==============
|
||||
|
||||
function initAgentManager() {
|
||||
// Load agents on page load
|
||||
loadAgents();
|
||||
|
||||
// Set up agent selector change handler
|
||||
const selector = document.getElementById('agentSelect');
|
||||
if (selector) {
|
||||
selector.addEventListener('change', (e) => {
|
||||
selectAgent(e.target.value);
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh agents periodically
|
||||
setInterval(loadAgents, 30000);
|
||||
}
|
||||
|
||||
// ============== MULTI-AGENT MODE ==============
|
||||
|
||||
/**
|
||||
* Toggle multi-agent mode to show combined results from all agents.
|
||||
*/
|
||||
function toggleMultiAgentMode() {
|
||||
const checkbox = document.getElementById('showAllAgents');
|
||||
multiAgentMode = checkbox ? checkbox.checked : false;
|
||||
|
||||
const selector = document.getElementById('agentSelect');
|
||||
const statusText = document.getElementById('agentStatusText');
|
||||
|
||||
if (multiAgentMode) {
|
||||
// Disable individual agent selection
|
||||
if (selector) selector.disabled = true;
|
||||
if (statusText) statusText.textContent = 'All Agents';
|
||||
|
||||
// Connect to multi-agent stream
|
||||
connectMultiAgentStream();
|
||||
|
||||
console.log('Multi-agent mode enabled - showing all agents');
|
||||
} else {
|
||||
// Re-enable individual selection
|
||||
if (selector) selector.disabled = false;
|
||||
updateAgentStatus();
|
||||
|
||||
// Disconnect multi-agent stream
|
||||
disconnectMultiAgentStream();
|
||||
|
||||
console.log('Multi-agent mode disabled');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the combined multi-agent SSE stream.
|
||||
*/
|
||||
function connectMultiAgentStream() {
|
||||
disconnectMultiAgentStream();
|
||||
|
||||
agentEventSource = new EventSource('/controller/stream/all');
|
||||
|
||||
agentEventSource.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// Skip keepalive messages
|
||||
if (data.type === 'keepalive') return;
|
||||
|
||||
// Route to appropriate handler based on scan_type
|
||||
handleMultiAgentData(data);
|
||||
} catch (e) {
|
||||
console.error('Error parsing multi-agent SSE:', e);
|
||||
}
|
||||
};
|
||||
|
||||
agentEventSource.onerror = (error) => {
|
||||
console.error('Multi-agent SSE error:', error);
|
||||
};
|
||||
}
|
||||
|
||||
function disconnectMultiAgentStream() {
|
||||
if (agentEventSource) {
|
||||
agentEventSource.close();
|
||||
agentEventSource = null;
|
||||
}
|
||||
if (multiAgentPollInterval) {
|
||||
clearInterval(multiAgentPollInterval);
|
||||
multiAgentPollInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle data from multi-agent stream and route to display.
|
||||
*/
|
||||
function handleMultiAgentData(data) {
|
||||
const agentName = data.agent_name || 'Unknown';
|
||||
const scanType = data.scan_type;
|
||||
const payload = data.payload;
|
||||
|
||||
// Add agent badge to the data for display
|
||||
if (payload) {
|
||||
payload._agent = agentName;
|
||||
}
|
||||
|
||||
// Route based on scan type
|
||||
switch (scanType) {
|
||||
case 'sensor':
|
||||
if (payload && payload.sensors) {
|
||||
payload.sensors.forEach(sensor => {
|
||||
sensor._agent = agentName;
|
||||
if (typeof displaySensorMessage === 'function') {
|
||||
displaySensorMessage(sensor);
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'pager':
|
||||
if (payload && payload.messages) {
|
||||
payload.messages.forEach(msg => {
|
||||
msg._agent = agentName;
|
||||
// Display pager message if handler exists
|
||||
if (typeof addPagerMessage === 'function') {
|
||||
addPagerMessage(msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'adsb':
|
||||
if (payload && payload.aircraft) {
|
||||
Object.values(payload.aircraft).forEach(ac => {
|
||||
ac._agent = agentName;
|
||||
// Update aircraft display if handler exists
|
||||
if (typeof updateAircraft === 'function') {
|
||||
updateAircraft(ac);
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'wifi':
|
||||
if (payload && payload.networks) {
|
||||
Object.values(payload.networks).forEach(net => {
|
||||
net._agent = agentName;
|
||||
});
|
||||
// Update WiFi display if handler exists
|
||||
if (typeof WiFiMode !== 'undefined' && WiFiMode.updateNetworks) {
|
||||
WiFiMode.updateNetworks(payload.networks);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log(`Multi-agent data from ${agentName}: ${scanType}`, payload);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', initAgentManager);
|
||||
Reference in New Issue
Block a user