mirror of
https://github.com/smittix/intercept.git
synced 2026-05-30 10:29:27 -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:
321
static/css/agents.css
Normal file
321
static/css/agents.css
Normal file
@@ -0,0 +1,321 @@
|
||||
/*
|
||||
* Agents Management CSS
|
||||
* Styles for the remote agent management interface
|
||||
*/
|
||||
|
||||
/* CSS Variables (inherited from main theme) */
|
||||
:root {
|
||||
--bg-primary: #0a0a0f;
|
||||
--bg-secondary: #12121a;
|
||||
--text-primary: #e0e0e0;
|
||||
--text-secondary: #888;
|
||||
--border-color: #1a1a2e;
|
||||
--accent-cyan: #00d4ff;
|
||||
--accent-green: #00ff88;
|
||||
--accent-red: #ff3366;
|
||||
--accent-orange: #ff9f1c;
|
||||
}
|
||||
|
||||
/* Agent indicator in navigation */
|
||||
.agent-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
background: rgba(0, 212, 255, 0.1);
|
||||
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.agent-indicator:hover {
|
||||
background: rgba(0, 212, 255, 0.2);
|
||||
border-color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.agent-indicator-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-green);
|
||||
box-shadow: 0 0 6px var(--accent-green);
|
||||
}
|
||||
|
||||
.agent-indicator-dot.remote {
|
||||
background: var(--accent-cyan);
|
||||
box-shadow: 0 0 6px var(--accent-cyan);
|
||||
}
|
||||
|
||||
.agent-indicator-dot.multiple {
|
||||
background: var(--accent-orange);
|
||||
box-shadow: 0 0 6px var(--accent-orange);
|
||||
}
|
||||
|
||||
.agent-indicator-label {
|
||||
font-size: 11px;
|
||||
color: var(--text-primary);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.agent-indicator-count {
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
background: rgba(0, 212, 255, 0.2);
|
||||
border-radius: 10px;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
/* Agent selector dropdown */
|
||||
.agent-selector {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.agent-selector-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 8px;
|
||||
min-width: 280px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.agent-selector-dropdown.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.agent-selector-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 15px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.agent-selector-header h4 {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--accent-cyan);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.agent-selector-manage {
|
||||
font-size: 11px;
|
||||
color: var(--accent-cyan);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.agent-selector-manage:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.agent-selector-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.agent-selector-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 15px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.agent-selector-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.agent-selector-item:hover {
|
||||
background: rgba(0, 212, 255, 0.1);
|
||||
}
|
||||
|
||||
.agent-selector-item.selected {
|
||||
background: rgba(0, 212, 255, 0.15);
|
||||
border-left: 3px solid var(--accent-cyan);
|
||||
}
|
||||
|
||||
.agent-selector-item.local {
|
||||
border-left: 3px solid var(--accent-green);
|
||||
}
|
||||
|
||||
.agent-selector-item-status {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.agent-selector-item-status.online {
|
||||
background: var(--accent-green);
|
||||
}
|
||||
|
||||
.agent-selector-item-status.offline {
|
||||
background: var(--accent-red);
|
||||
}
|
||||
|
||||
.agent-selector-item-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.agent-selector-item-name {
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.agent-selector-item-url {
|
||||
font-size: 10px;
|
||||
color: var(--text-secondary);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.agent-selector-item-check {
|
||||
color: var(--accent-green);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.agent-selector-item.selected .agent-selector-item-check {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Agent badge in data displays */
|
||||
.agent-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
font-size: 10px;
|
||||
background: rgba(0, 212, 255, 0.1);
|
||||
color: var(--accent-cyan);
|
||||
border-radius: 10px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.agent-badge.local {
|
||||
background: rgba(0, 255, 136, 0.1);
|
||||
color: var(--accent-green);
|
||||
}
|
||||
|
||||
.agent-badge-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
/* Agent column in data tables */
|
||||
.data-table .agent-col {
|
||||
width: 120px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
/* Multi-agent stream indicator */
|
||||
.multi-agent-indicator {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 20px;
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.multi-agent-indicator.active {
|
||||
border-color: var(--accent-cyan);
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.multi-agent-indicator-pulse {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-cyan);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(0.8); }
|
||||
}
|
||||
|
||||
/* Agent connection status toast */
|
||||
.agent-toast {
|
||||
position: fixed;
|
||||
top: 80px;
|
||||
right: 20px;
|
||||
padding: 10px 15px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
z-index: 1001;
|
||||
animation: slideInRight 0.3s ease;
|
||||
}
|
||||
|
||||
.agent-toast.connected {
|
||||
border-color: var(--accent-green);
|
||||
color: var(--accent-green);
|
||||
}
|
||||
|
||||
.agent-toast.disconnected {
|
||||
border-color: var(--accent-red);
|
||||
color: var(--accent-red);
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.agent-indicator {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.agent-indicator-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.agent-selector-dropdown {
|
||||
position: fixed;
|
||||
top: auto;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0;
|
||||
border-radius: 16px 16px 0 0;
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
||||
.agents-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -1840,6 +1840,27 @@ header h1 .tagline {
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* Agent status indicator */
|
||||
.agent-status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.agent-status-dot.online {
|
||||
background: var(--accent-green);
|
||||
box-shadow: 0 0 6px var(--accent-green);
|
||||
}
|
||||
|
||||
.agent-status-dot.offline {
|
||||
background: var(--accent-red);
|
||||
}
|
||||
|
||||
.agent-status-dot.unknown {
|
||||
background: #666;
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
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