mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40: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:
@@ -252,6 +252,45 @@ Response:
|
||||
}
|
||||
```
|
||||
|
||||
## Supported Modes
|
||||
|
||||
All modes are fully implemented in the agent with the following tools and data formats:
|
||||
|
||||
| Mode | Tool(s) | Data Format | Notes |
|
||||
|------|---------|-------------|-------|
|
||||
| `sensor` | rtl_433 | JSON readings | ISM band devices (433/868/915 MHz) |
|
||||
| `pager` | rtl_fm + multimon-ng | POCSAG/FLEX messages | Address, function, message content |
|
||||
| `adsb` | dump1090 | SBS-format aircraft | ICAO, callsign, position, altitude |
|
||||
| `ais` | AIS-catcher | JSON vessels | MMSI, position, speed, vessel info |
|
||||
| `acars` | acarsdec | JSON messages | Aircraft tail, label, message text |
|
||||
| `aprs` | rtl_fm + direwolf | APRS packets | Callsign, position, path |
|
||||
| `wifi` | airodump-ng | Networks + clients | BSSID, ESSID, signal, clients |
|
||||
| `bluetooth` | bluetoothctl | Device list | MAC, name, RSSI |
|
||||
| `rtlamr` | rtl_tcp + rtlamr | Meter readings | Meter ID, consumption data |
|
||||
| `dsc` | rtl_fm (+ dsc-decoder) | DSC messages | MMSI, distress category, position |
|
||||
| `tscm` | WiFi/BT analysis | Anomaly reports | New/rogue devices detected |
|
||||
| `satellite` | skyfield (TLE) | Pass predictions | No SDR required |
|
||||
| `listening_post` | rtl_fm scanner | Signal detections | Frequency, modulation |
|
||||
|
||||
### Mode-Specific Notes
|
||||
|
||||
**Listening Post**: Full FFT streaming isn't practical over HTTP. Instead, the agent provides:
|
||||
- Signal detection events when activity is found
|
||||
- Current scanning frequency
|
||||
- Activity log of detected signals
|
||||
|
||||
**TSCM**: Analyzes WiFi and Bluetooth data for anomalies:
|
||||
- Builds baseline of known devices
|
||||
- Reports new/unknown devices as anomalies
|
||||
- No SDR required (uses WiFi/BT data)
|
||||
|
||||
**Satellite**: Pure computational mode:
|
||||
- Calculates pass predictions from TLE data
|
||||
- Requires observer location (lat/lon)
|
||||
- No SDR required
|
||||
|
||||
**Audio Modes**: Modes requiring real-time audio (airband, listening_post audio) are limited via agents. Use rtl_tcp for remote audio streaming instead.
|
||||
|
||||
## Controller API
|
||||
|
||||
### Agent Management
|
||||
@@ -396,6 +435,62 @@ bluetooth = true
|
||||
4. **Firewall**: Restrict agent ports to controller IP only
|
||||
5. **allowed_ips**: Use this config option to restrict agent connections
|
||||
|
||||
## Dashboard Integration
|
||||
|
||||
Agent support has been integrated into the following specialized dashboards:
|
||||
|
||||
### ADS-B Dashboard (`/adsb/dashboard`)
|
||||
- Agent selector in header bar
|
||||
- Routes tracking start/stop through agent proxy when remote agent selected
|
||||
- Connects to multi-agent stream for data from remote agents
|
||||
- Displays agent badge on aircraft from remote sources
|
||||
- Updates observer location from agent's GPS coordinates
|
||||
|
||||
### AIS Dashboard (`/ais/dashboard`)
|
||||
- Agent selector in header bar
|
||||
- Routes AIS and DSC mode operations through agent proxy
|
||||
- Connects to multi-agent stream for vessel data
|
||||
- Displays agent badge on vessels from remote sources
|
||||
- Updates observer location from agent's GPS coordinates
|
||||
|
||||
### Main Dashboard (`/`)
|
||||
- Agent selector in sidebar
|
||||
- Supports sensor, pager, WiFi, Bluetooth modes via agents
|
||||
- SDR conflict detection with device-aware warnings
|
||||
- Real-time sync with agent's running mode state
|
||||
|
||||
### Multi-SDR Agent Support
|
||||
|
||||
For agents with multiple SDR devices, the system now tracks which device each mode is using:
|
||||
|
||||
```json
|
||||
{
|
||||
"running_modes": ["sensor", "adsb"],
|
||||
"running_modes_detail": {
|
||||
"sensor": {"device": 0, "started_at": "2024-01-15T10:30:00Z"},
|
||||
"adsb": {"device": 1, "started_at": "2024-01-15T10:35:00Z"}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This allows:
|
||||
- Smart conflict detection (only warns if same device is in use)
|
||||
- Display of which device each mode is using
|
||||
- Parallel operation of multiple SDR modes on multi-SDR agents
|
||||
|
||||
### Agent Mode Warnings
|
||||
|
||||
When an agent has SDR modes running, the UI displays:
|
||||
- Warning banner showing active modes with device numbers
|
||||
- Stop buttons for each running mode
|
||||
- Refresh button to re-sync with agent state
|
||||
|
||||
### Pages Without Agent Support
|
||||
|
||||
The following pages don't require SDR-based agent support:
|
||||
- **Satellite Dashboard** (`/satellite/dashboard`) - Uses TLE orbital calculations, no SDR
|
||||
- **History pages** - Display stored data, not live SDR streams
|
||||
|
||||
## Files
|
||||
|
||||
| File | Description |
|
||||
@@ -407,3 +502,5 @@ bluetooth = true
|
||||
| `utils/database.py` | Agent CRUD operations |
|
||||
| `static/js/core/agents.js` | Frontend agent management |
|
||||
| `templates/agents.html` | Agent management page |
|
||||
| `templates/adsb_dashboard.html` | ADS-B page with agent integration |
|
||||
| `templates/ais_dashboard.html` | AIS page with agent integration |
|
||||
|
||||
1389
intercept_agent.py
1389
intercept_agent.py
File diff suppressed because it is too large
Load Diff
@@ -240,6 +240,33 @@ def refresh_agent_metadata(agent_id: int):
|
||||
}), 503
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Agent Status - Get running state
|
||||
# =============================================================================
|
||||
|
||||
@controller_bp.route('/agents/<int:agent_id>/status', methods=['GET'])
|
||||
def get_agent_status(agent_id: int):
|
||||
"""Get an agent's current status including running modes."""
|
||||
agent = get_agent(agent_id)
|
||||
if not agent:
|
||||
return jsonify({'status': 'error', 'message': 'Agent not found'}), 404
|
||||
|
||||
try:
|
||||
client = create_client_from_agent(agent)
|
||||
status = client.get_status()
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'agent_id': agent_id,
|
||||
'agent_name': agent['name'],
|
||||
'agent_status': status
|
||||
})
|
||||
except (AgentHTTPError, AgentConnectionError) as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Failed to reach agent: {e}'
|
||||
}), 503
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Proxy Operations - Forward requests to agents
|
||||
# =============================================================================
|
||||
|
||||
@@ -1710,6 +1710,12 @@ body {
|
||||
box-shadow: 0 0 10px var(--accent-red);
|
||||
}
|
||||
|
||||
.strip-status .status-dot.warn {
|
||||
background: var(--accent-yellow, #ffcc00);
|
||||
box-shadow: 0 0 10px var(--accent-yellow, #ffcc00);
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.strip-time {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
|
||||
@@ -206,11 +206,33 @@
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.agent-badge.local {
|
||||
.agent-badge.local,
|
||||
.agent-badge.agent-local {
|
||||
background: rgba(0, 255, 136, 0.1);
|
||||
color: var(--accent-green);
|
||||
}
|
||||
|
||||
.agent-badge.agent-remote {
|
||||
background: rgba(0, 212, 255, 0.1);
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
/* WiFi table agent column */
|
||||
.wifi-networks-table .col-agent {
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wifi-networks-table th.col-agent {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* Bluetooth table agent column */
|
||||
.bt-devices-table .col-agent {
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.agent-badge-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
|
||||
@@ -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; },
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,16 @@
|
||||
<span>// INTERCEPT - AIS Tracking</span>
|
||||
</div>
|
||||
<div class="status-bar">
|
||||
<!-- Agent Selector -->
|
||||
<div class="agent-selector-compact" id="agentSection">
|
||||
<select id="agentSelect" class="agent-select-sm" title="Select signal source">
|
||||
<option value="local">Local</option>
|
||||
</select>
|
||||
<span class="agent-status-dot online" id="agentStatusDot"></span>
|
||||
<label class="show-all-label" title="Show vessels from all agents on map">
|
||||
<input type="checkbox" id="showAllAgents" onchange="toggleShowAllAgents()"> All
|
||||
</label>
|
||||
</div>
|
||||
<a href="/" class="back-link">Main Dashboard</a>
|
||||
</div>
|
||||
</header>
|
||||
@@ -173,6 +183,7 @@
|
||||
let markers = {};
|
||||
let selectedMmsi = null;
|
||||
let eventSource = null;
|
||||
let aisPollTimer = null; // Polling fallback for agent mode
|
||||
let isTracking = false;
|
||||
|
||||
// DSC State
|
||||
@@ -181,6 +192,8 @@
|
||||
let dscMessages = {};
|
||||
let dscMarkers = {};
|
||||
let dscAlertCounts = { distress: 0, urgency: 0 };
|
||||
let dscCurrentAgent = null;
|
||||
let dscPollTimer = null;
|
||||
let showTrails = false;
|
||||
let vesselTrails = {};
|
||||
let trailLines = {};
|
||||
@@ -490,6 +503,40 @@
|
||||
const device = document.getElementById('aisDeviceSelect').value;
|
||||
const gain = document.getElementById('aisGain').value;
|
||||
|
||||
// Check if using agent mode
|
||||
const useAgent = typeof aisCurrentAgent !== 'undefined' && aisCurrentAgent !== 'local';
|
||||
|
||||
// For agent mode, check conflicts and route through proxy
|
||||
if (useAgent) {
|
||||
if (typeof checkAgentModeConflict === 'function' && !checkAgentModeConflict('ais')) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/controller/agents/${aisCurrentAgent}/ais/start`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ device, gain })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(result => {
|
||||
const data = result.result || result;
|
||||
if (data.status === 'started' || data.status === 'already_running') {
|
||||
isTracking = true;
|
||||
document.getElementById('startBtn').textContent = 'STOP';
|
||||
document.getElementById('startBtn').classList.add('active');
|
||||
document.getElementById('trackingDot').classList.add('active');
|
||||
document.getElementById('trackingStatus').textContent = 'TRACKING';
|
||||
startSessionTimer();
|
||||
startSSE();
|
||||
} else {
|
||||
alert(data.message || 'Failed to start');
|
||||
}
|
||||
})
|
||||
.catch(err => alert('Error: ' + err.message));
|
||||
return;
|
||||
}
|
||||
|
||||
// Local mode - original behavior unchanged
|
||||
fetch('/ais/start', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -513,7 +560,12 @@
|
||||
}
|
||||
|
||||
function stopTracking() {
|
||||
fetch('/ais/stop', { method: 'POST' })
|
||||
const useAgent = typeof aisCurrentAgent !== 'undefined' && aisCurrentAgent !== 'local';
|
||||
|
||||
// Route to agent or local
|
||||
const url = useAgent ? `/controller/agents/${aisCurrentAgent}/ais/stop` : '/ais/stop';
|
||||
|
||||
fetch(url, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(() => {
|
||||
isTracking = false;
|
||||
@@ -527,18 +579,107 @@
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
if (aisPollTimer) {
|
||||
clearInterval(aisPollTimer);
|
||||
aisPollTimer = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start polling agent data as fallback when push isn't enabled.
|
||||
*/
|
||||
function startAisPolling() {
|
||||
if (aisPollTimer) return;
|
||||
if (typeof aisCurrentAgent === 'undefined' || aisCurrentAgent === 'local') return;
|
||||
|
||||
const pollInterval = 2000; // 2 seconds for AIS
|
||||
console.log('Starting AIS agent polling fallback...');
|
||||
|
||||
aisPollTimer = setInterval(async () => {
|
||||
if (!isTracking) {
|
||||
clearInterval(aisPollTimer);
|
||||
aisPollTimer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/controller/agents/${aisCurrentAgent}/ais/data`);
|
||||
if (!response.ok) return;
|
||||
|
||||
const result = await response.json();
|
||||
const data = result.data || result;
|
||||
|
||||
// Get agent name
|
||||
let agentName = 'Agent';
|
||||
if (typeof agents !== 'undefined') {
|
||||
const agent = agents.find(a => a.id == aisCurrentAgent);
|
||||
if (agent) agentName = agent.name;
|
||||
}
|
||||
|
||||
// Process vessels from polling response
|
||||
if (data && data.vessels) {
|
||||
Object.values(data.vessels).forEach(vessel => {
|
||||
vessel._agent = agentName;
|
||||
updateVessel(vessel);
|
||||
});
|
||||
} else if (data && Array.isArray(data)) {
|
||||
data.forEach(vessel => {
|
||||
vessel._agent = agentName;
|
||||
updateVessel(vessel);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.debug('AIS agent poll error:', err);
|
||||
}
|
||||
}, pollInterval);
|
||||
}
|
||||
|
||||
function startSSE() {
|
||||
if (eventSource) eventSource.close();
|
||||
|
||||
eventSource = new EventSource('/ais/stream');
|
||||
const useAgent = typeof aisCurrentAgent !== 'undefined' && aisCurrentAgent !== 'local';
|
||||
const streamUrl = useAgent ? '/controller/stream/all' : '/ais/stream';
|
||||
|
||||
// Get agent name for filtering
|
||||
let targetAgentName = null;
|
||||
if (useAgent && typeof agents !== 'undefined') {
|
||||
const agent = agents.find(a => a.id == aisCurrentAgent);
|
||||
targetAgentName = agent ? agent.name : null;
|
||||
}
|
||||
|
||||
eventSource = new EventSource(streamUrl);
|
||||
eventSource.onmessage = function(e) {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'vessel') {
|
||||
updateVessel(data);
|
||||
|
||||
if (useAgent) {
|
||||
// Multi-agent stream format
|
||||
if (data.type === 'keepalive') return;
|
||||
|
||||
// Filter to our agent
|
||||
if (targetAgentName && data.agent_name && data.agent_name !== targetAgentName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract vessel data from push payload
|
||||
if (data.scan_type === 'ais' && data.payload) {
|
||||
const payload = data.payload;
|
||||
if (payload.vessels) {
|
||||
Object.values(payload.vessels).forEach(v => {
|
||||
v._agent = data.agent_name;
|
||||
updateVessel({ type: 'vessel', ...v });
|
||||
});
|
||||
} else if (payload.mmsi) {
|
||||
payload._agent = data.agent_name;
|
||||
updateVessel({ type: 'vessel', ...payload });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Local stream format
|
||||
if (data.type === 'vessel') {
|
||||
updateVessel(data);
|
||||
}
|
||||
}
|
||||
} catch (err) {}
|
||||
};
|
||||
@@ -731,12 +872,13 @@
|
||||
container.innerHTML = vesselArray.map(v => {
|
||||
const iconSvg = getShipIconSvg(v.ship_type, 20);
|
||||
const category = getShipCategory(v.ship_type);
|
||||
const agentBadge = v._agent ? `<span class="agent-badge">${v._agent}</span>` : '';
|
||||
return `
|
||||
<div class="vessel-item ${v.mmsi === selectedMmsi ? 'selected' : ''}"
|
||||
data-mmsi="${v.mmsi}" onclick="selectVessel('${v.mmsi}')">
|
||||
<div class="vessel-item-icon">${iconSvg}</div>
|
||||
<div class="vessel-item-info">
|
||||
<div class="vessel-item-name">${v.name || 'Unknown'}</div>
|
||||
<div class="vessel-item-name">${v.name || 'Unknown'}${agentBadge}</div>
|
||||
<div class="vessel-item-type">${category} | ${v.mmsi}</div>
|
||||
</div>
|
||||
<div class="vessel-item-speed">${v.speed ? v.speed + ' kt' : '-'}</div>
|
||||
@@ -881,33 +1023,51 @@
|
||||
const device = document.getElementById('dscDeviceSelect').value;
|
||||
const gain = document.getElementById('dscGain').value;
|
||||
|
||||
fetch('/dsc/start', {
|
||||
// Check if using agent mode
|
||||
const isAgentMode = typeof aisCurrentAgent !== 'undefined' && aisCurrentAgent !== 'local';
|
||||
dscCurrentAgent = isAgentMode ? aisCurrentAgent : null;
|
||||
|
||||
// Determine endpoint based on agent mode
|
||||
const endpoint = isAgentMode
|
||||
? `/controller/agents/${aisCurrentAgent}/dsc/start`
|
||||
: '/dsc/start';
|
||||
|
||||
fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ device, gain })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'started') {
|
||||
// Handle controller proxy response format
|
||||
const scanResult = isAgentMode && data.result ? data.result : data;
|
||||
|
||||
if (scanResult.status === 'started' || scanResult.status === 'success') {
|
||||
isDscTracking = true;
|
||||
document.getElementById('dscStartBtn').textContent = 'STOP DSC';
|
||||
document.getElementById('dscStartBtn').classList.add('active');
|
||||
document.getElementById('dscIndicator').classList.add('active');
|
||||
startDscSSE();
|
||||
} else if (data.error_type === 'DEVICE_BUSY') {
|
||||
alert('SDR device is busy.\n\n' + data.suggestion);
|
||||
startDscSSE(isAgentMode);
|
||||
} else if (scanResult.error_type === 'DEVICE_BUSY') {
|
||||
alert('SDR device is busy.\n\n' + (scanResult.suggestion || ''));
|
||||
} else {
|
||||
alert(data.message || 'Failed to start DSC');
|
||||
alert(scanResult.message || scanResult.error || 'Failed to start DSC');
|
||||
}
|
||||
})
|
||||
.catch(err => alert('Error: ' + err.message));
|
||||
}
|
||||
|
||||
function stopDscTracking() {
|
||||
fetch('/dsc/stop', { method: 'POST' })
|
||||
const isAgentMode = dscCurrentAgent !== null;
|
||||
const endpoint = isAgentMode
|
||||
? `/controller/agents/${dscCurrentAgent}/dsc/stop`
|
||||
: '/dsc/stop';
|
||||
|
||||
fetch(endpoint, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(() => {
|
||||
isDscTracking = false;
|
||||
dscCurrentAgent = null;
|
||||
document.getElementById('dscStartBtn').textContent = 'START DSC';
|
||||
document.getElementById('dscStartBtn').classList.remove('active');
|
||||
document.getElementById('dscIndicator').classList.remove('active');
|
||||
@@ -915,23 +1075,50 @@
|
||||
dscEventSource.close();
|
||||
dscEventSource = null;
|
||||
}
|
||||
// Clear polling timer
|
||||
if (dscPollTimer) {
|
||||
clearInterval(dscPollTimer);
|
||||
dscPollTimer = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function startDscSSE() {
|
||||
function startDscSSE(isAgentMode = false) {
|
||||
if (dscEventSource) dscEventSource.close();
|
||||
|
||||
dscEventSource = new EventSource('/dsc/stream');
|
||||
// Use different stream endpoint for agent mode
|
||||
const streamUrl = isAgentMode ? '/controller/stream/all' : '/dsc/stream';
|
||||
dscEventSource = new EventSource(streamUrl);
|
||||
|
||||
dscEventSource.onmessage = function(e) {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'dsc_message') {
|
||||
handleDscMessage(data);
|
||||
} else if (data.type === 'error') {
|
||||
console.error('DSC error:', data.error);
|
||||
if (data.error_type === 'DEVICE_BUSY') {
|
||||
alert('DSC: Device became busy. ' + (data.suggestion || ''));
|
||||
stopDscTracking();
|
||||
|
||||
if (isAgentMode) {
|
||||
// Handle multi-agent stream format
|
||||
if (data.scan_type === 'dsc' && data.payload) {
|
||||
const payload = data.payload;
|
||||
if (payload.type === 'dsc_message') {
|
||||
payload.agent_name = data.agent_name;
|
||||
handleDscMessage(payload);
|
||||
} else if (payload.type === 'error') {
|
||||
console.error('DSC error:', payload.error);
|
||||
if (payload.error_type === 'DEVICE_BUSY') {
|
||||
alert('DSC: Device became busy. ' + (payload.suggestion || ''));
|
||||
stopDscTracking();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Local stream format
|
||||
if (data.type === 'dsc_message') {
|
||||
handleDscMessage(data);
|
||||
} else if (data.type === 'error') {
|
||||
console.error('DSC error:', data.error);
|
||||
if (data.error_type === 'DEVICE_BUSY') {
|
||||
alert('DSC: Device became busy. ' + (data.suggestion || ''));
|
||||
stopDscTracking();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {}
|
||||
@@ -939,9 +1126,56 @@
|
||||
|
||||
dscEventSource.onerror = function() {
|
||||
setTimeout(() => {
|
||||
if (isDscTracking) startDscSSE();
|
||||
if (isDscTracking) startDscSSE(isAgentMode);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
// Start polling fallback for agent mode
|
||||
if (isAgentMode) {
|
||||
startDscPolling();
|
||||
}
|
||||
}
|
||||
|
||||
// Track last DSC message count for polling
|
||||
let lastDscMessageCount = 0;
|
||||
|
||||
function startDscPolling() {
|
||||
if (dscPollTimer) return;
|
||||
lastDscMessageCount = 0;
|
||||
|
||||
const pollInterval = 2000;
|
||||
dscPollTimer = setInterval(async () => {
|
||||
if (!isDscTracking || !dscCurrentAgent) {
|
||||
clearInterval(dscPollTimer);
|
||||
dscPollTimer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/controller/agents/${dscCurrentAgent}/dsc/data`);
|
||||
if (!response.ok) return;
|
||||
|
||||
const data = await response.json();
|
||||
const result = data.result || data;
|
||||
const messages = result.data || [];
|
||||
|
||||
// Process new messages
|
||||
if (messages.length > lastDscMessageCount) {
|
||||
const newMessages = messages.slice(lastDscMessageCount);
|
||||
newMessages.forEach(msg => {
|
||||
const dscMsg = {
|
||||
type: 'dsc_message',
|
||||
...msg,
|
||||
agent_name: result.agent_name || 'Remote Agent'
|
||||
};
|
||||
handleDscMessage(dscMsg);
|
||||
});
|
||||
lastDscMessageCount = messages.length;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('DSC polling error:', err);
|
||||
}
|
||||
}, pollInterval);
|
||||
}
|
||||
|
||||
function handleDscMessage(data) {
|
||||
@@ -1100,5 +1334,324 @@
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', initMap);
|
||||
</script>
|
||||
|
||||
<!-- Agent styles -->
|
||||
<style>
|
||||
.agent-selector-compact {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
.agent-select-sm {
|
||||
background: rgba(0, 40, 60, 0.8);
|
||||
border: 1px solid var(--border-color, rgba(0, 200, 255, 0.3));
|
||||
color: var(--text-primary, #e0f7ff);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
cursor: pointer;
|
||||
}
|
||||
.agent-select-sm:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-cyan, #00d4ff);
|
||||
}
|
||||
.agent-status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.agent-status-dot.online {
|
||||
background: #4caf50;
|
||||
box-shadow: 0 0 6px #4caf50;
|
||||
}
|
||||
.agent-status-dot.offline {
|
||||
background: #f44336;
|
||||
box-shadow: 0 0 6px #f44336;
|
||||
}
|
||||
.vessel-item .agent-badge {
|
||||
font-size: 9px;
|
||||
color: var(--accent-cyan, #00d4ff);
|
||||
background: rgba(0, 200, 255, 0.1);
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
#agentModeWarning {
|
||||
color: #f0ad4e;
|
||||
font-size: 10px;
|
||||
padding: 4px 8px;
|
||||
background: rgba(240,173,78,0.1);
|
||||
border-radius: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.show-all-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 10px;
|
||||
color: var(--text-muted, #a0c4d0);
|
||||
cursor: pointer;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.show-all-label input {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Agent Manager -->
|
||||
<script src="{{ url_for('static', filename='js/core/agents.js') }}"></script>
|
||||
<script>
|
||||
// AIS-specific agent integration
|
||||
let aisCurrentAgent = 'local';
|
||||
|
||||
function selectAisAgent(agentId) {
|
||||
aisCurrentAgent = agentId;
|
||||
currentAgent = agentId; // Update global agent state
|
||||
|
||||
if (agentId === 'local') {
|
||||
loadDevices();
|
||||
console.log('AIS: Using local device');
|
||||
} else {
|
||||
refreshAgentDevicesForAis(agentId);
|
||||
syncAgentModeStates(agentId);
|
||||
console.log(`AIS: Using agent ${agentId}`);
|
||||
}
|
||||
updateAgentStatus();
|
||||
}
|
||||
|
||||
async function refreshAgentDevicesForAis(agentId) {
|
||||
try {
|
||||
const response = await fetch(`/controller/agents/${agentId}?refresh=true`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.agent && data.agent.interfaces) {
|
||||
const devices = data.agent.interfaces.devices || [];
|
||||
populateAisDeviceSelects(devices);
|
||||
|
||||
// Update observer location if agent has GPS
|
||||
if (data.agent.gps_coords) {
|
||||
const gps = typeof data.agent.gps_coords === 'string'
|
||||
? JSON.parse(data.agent.gps_coords)
|
||||
: data.agent.gps_coords;
|
||||
if (gps.lat && gps.lon) {
|
||||
document.getElementById('obsLat').value = gps.lat.toFixed(4);
|
||||
document.getElementById('obsLon').value = gps.lon.toFixed(4);
|
||||
updateObserverLoc();
|
||||
console.log(`Updated observer location from agent GPS: ${gps.lat}, ${gps.lon}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh agent devices:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function populateAisDeviceSelects(devices) {
|
||||
const aisSelect = document.getElementById('aisDeviceSelect');
|
||||
const dscSelect = document.getElementById('dscDeviceSelect');
|
||||
|
||||
[aisSelect, dscSelect].forEach(select => {
|
||||
if (!select) return;
|
||||
select.innerHTML = '';
|
||||
|
||||
if (devices.length === 0) {
|
||||
select.innerHTML = '<option value="0">No SDR found</option>';
|
||||
} else {
|
||||
devices.forEach(device => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = device.index;
|
||||
opt.textContent = `Device ${device.index}: ${device.name || device.type || 'SDR'}`;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Override startTracking for agent support
|
||||
const originalStartTracking = startTracking;
|
||||
startTracking = function() {
|
||||
const useAgent = aisCurrentAgent !== 'local';
|
||||
|
||||
if (useAgent) {
|
||||
// Check for conflicts
|
||||
if (typeof checkAgentModeConflict === 'function' && !checkAgentModeConflict('ais')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const device = document.getElementById('aisDeviceSelect').value;
|
||||
const gain = document.getElementById('aisGain').value;
|
||||
|
||||
fetch(`/controller/agents/${aisCurrentAgent}/ais/start`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ device, gain })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
// Handle controller proxy response (agent response is nested in 'result')
|
||||
const scanResult = data.result || data;
|
||||
if (scanResult.status === 'started' || scanResult.status === 'already_running' || scanResult.status === 'success') {
|
||||
isTracking = true;
|
||||
document.getElementById('startBtn').textContent = 'STOP';
|
||||
document.getElementById('startBtn').classList.add('active');
|
||||
document.getElementById('trackingDot').classList.add('active');
|
||||
document.getElementById('trackingStatus').textContent = 'TRACKING (AGENT)';
|
||||
document.getElementById('agentSelect').disabled = true;
|
||||
startSessionTimer();
|
||||
startSSE(); // Use multi-agent stream
|
||||
startAisPolling(); // Also start polling as fallback
|
||||
|
||||
if (typeof agentRunningModes !== 'undefined' && !agentRunningModes.includes('ais')) {
|
||||
agentRunningModes.push('ais');
|
||||
}
|
||||
} else {
|
||||
alert(scanResult.message || 'Failed to start');
|
||||
}
|
||||
})
|
||||
.catch(err => alert('Error: ' + err.message));
|
||||
} else {
|
||||
originalStartTracking();
|
||||
}
|
||||
};
|
||||
|
||||
// Override stopTracking for agent support
|
||||
const originalStopTracking = stopTracking;
|
||||
stopTracking = function() {
|
||||
const useAgent = aisCurrentAgent !== 'local';
|
||||
|
||||
if (useAgent) {
|
||||
fetch(`/controller/agents/${aisCurrentAgent}/ais/stop`, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(() => {
|
||||
isTracking = false;
|
||||
document.getElementById('startBtn').textContent = 'START';
|
||||
document.getElementById('startBtn').classList.remove('active');
|
||||
document.getElementById('trackingDot').classList.remove('active');
|
||||
document.getElementById('trackingStatus').textContent = 'STANDBY';
|
||||
document.getElementById('agentSelect').disabled = false;
|
||||
stopSSE();
|
||||
|
||||
if (typeof agentRunningModes !== 'undefined') {
|
||||
agentRunningModes = agentRunningModes.filter(m => m !== 'ais');
|
||||
}
|
||||
})
|
||||
.catch(err => console.error('Stop error:', err));
|
||||
} else {
|
||||
originalStopTracking();
|
||||
}
|
||||
};
|
||||
|
||||
// Hook into page init
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const agentSelect = document.getElementById('agentSelect');
|
||||
if (agentSelect) {
|
||||
agentSelect.addEventListener('change', function(e) {
|
||||
selectAisAgent(e.target.value);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Show All Agents mode - display vessels from all agents on the map
|
||||
let showAllAgentsMode = false;
|
||||
let allAgentsEventSource = null;
|
||||
|
||||
function toggleShowAllAgents() {
|
||||
const checkbox = document.getElementById('showAllAgents');
|
||||
showAllAgentsMode = checkbox ? checkbox.checked : false;
|
||||
|
||||
const agentSelect = document.getElementById('agentSelect');
|
||||
const startBtn = document.getElementById('startBtn');
|
||||
|
||||
if (showAllAgentsMode) {
|
||||
// Disable individual agent selection and start button
|
||||
if (agentSelect) agentSelect.disabled = true;
|
||||
if (startBtn) startBtn.disabled = true;
|
||||
|
||||
// Connect to multi-agent stream (passive listening to all agents)
|
||||
startAllAgentsStream();
|
||||
|
||||
document.getElementById('trackingStatus').textContent = 'ALL AGENTS';
|
||||
document.getElementById('trackingDot').classList.add('active');
|
||||
console.log('Show All Agents mode enabled');
|
||||
} else {
|
||||
// Re-enable controls
|
||||
if (agentSelect) agentSelect.disabled = isTracking;
|
||||
if (startBtn) startBtn.disabled = false;
|
||||
|
||||
// Stop multi-agent stream
|
||||
stopAllAgentsStream();
|
||||
|
||||
if (!isTracking) {
|
||||
document.getElementById('trackingStatus').textContent = 'STANDBY';
|
||||
document.getElementById('trackingDot').classList.remove('active');
|
||||
}
|
||||
console.log('Show All Agents mode disabled');
|
||||
}
|
||||
}
|
||||
|
||||
function startAllAgentsStream() {
|
||||
if (allAgentsEventSource) allAgentsEventSource.close();
|
||||
|
||||
allAgentsEventSource = new EventSource('/controller/stream/all');
|
||||
allAgentsEventSource.onmessage = function(e) {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'keepalive') return;
|
||||
|
||||
// Handle AIS data from any agent
|
||||
if (data.scan_type === 'ais' && data.payload) {
|
||||
const payload = data.payload;
|
||||
if (payload.vessels) {
|
||||
Object.values(payload.vessels).forEach(v => {
|
||||
v._agent = data.agent_name;
|
||||
updateVessel({ type: 'vessel', ...v });
|
||||
});
|
||||
} else if (payload.mmsi) {
|
||||
payload._agent = data.agent_name;
|
||||
updateVessel({ type: 'vessel', ...payload });
|
||||
}
|
||||
}
|
||||
|
||||
// Handle DSC data from any agent
|
||||
if (data.scan_type === 'dsc' && data.payload) {
|
||||
const payload = data.payload;
|
||||
if (payload.messages) {
|
||||
payload.messages.forEach(msg => {
|
||||
msg._agent = data.agent_name;
|
||||
processDscMessage(msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('All agents stream parse error:', err);
|
||||
}
|
||||
};
|
||||
|
||||
allAgentsEventSource.onerror = function() {
|
||||
console.error('All agents stream error');
|
||||
setTimeout(() => {
|
||||
if (showAllAgentsMode) startAllAgentsStream();
|
||||
}, 3000);
|
||||
};
|
||||
}
|
||||
|
||||
function stopAllAgentsStream() {
|
||||
if (allAgentsEventSource) {
|
||||
allAgentsEventSource.close();
|
||||
allAgentsEventSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Process DSC message (wrapper for addDscMessage if it exists)
|
||||
function processDscMessage(msg) {
|
||||
if (typeof addDscMessage === 'function') {
|
||||
addDscMessage(msg);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -574,11 +574,12 @@
|
||||
<th class="sortable" data-sort="rssi">Signal</th>
|
||||
<th class="sortable" data-sort="security">Security</th>
|
||||
<th class="sortable" data-sort="clients">Clients</th>
|
||||
<th class="col-agent sortable" data-sort="agent">Source</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="wifiNetworkTableBody">
|
||||
<tr class="wifi-network-placeholder">
|
||||
<td colspan="6">
|
||||
<td colspan="7">
|
||||
<div class="placeholder-text">Start scanning to discover networks</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -2306,6 +2307,11 @@
|
||||
|
||||
// Check if using remote agent
|
||||
if (typeof currentAgent !== 'undefined' && currentAgent !== 'local') {
|
||||
// Check for conflicts with other running SDR modes
|
||||
if (typeof checkAgentModeConflict === 'function' && !checkAgentModeConflict('sensor')) {
|
||||
return; // User cancelled or conflict not resolved
|
||||
}
|
||||
|
||||
// Route through agent proxy
|
||||
const config = {
|
||||
frequency: freq,
|
||||
@@ -2320,12 +2326,14 @@
|
||||
body: JSON.stringify(config)
|
||||
}).then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'started' || data.status === 'success') {
|
||||
// Handle controller proxy response (agent response is nested in 'result')
|
||||
const scanResult = data.result || data;
|
||||
if (scanResult.status === 'started' || scanResult.status === 'success') {
|
||||
setSensorRunning(true);
|
||||
startAgentSensorStream();
|
||||
showInfo(`Sensor started on remote agent`);
|
||||
} else {
|
||||
alert('Error: ' + (data.message || 'Failed to start sensor on agent'));
|
||||
alert('Error: ' + (scanResult.message || 'Failed to start sensor on agent'));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -2612,6 +2620,10 @@
|
||||
document.getElementById('rtlamrFrequency').value = freq;
|
||||
}
|
||||
|
||||
// RTLAMR mode polling timer for agent mode
|
||||
let rtlamrPollTimer = null;
|
||||
let rtlamrCurrentAgent = null;
|
||||
|
||||
function startRtlamrDecoding() {
|
||||
const freq = document.getElementById('rtlamrFrequency').value;
|
||||
const gain = document.getElementById('rtlamrGain').value;
|
||||
@@ -2621,8 +2633,12 @@
|
||||
const filterid = document.getElementById('rtlamrFilterId').value;
|
||||
const unique = document.getElementById('rtlamrUnique').checked;
|
||||
|
||||
// Check if device is available
|
||||
if (!checkDeviceAvailability('rtlamr')) {
|
||||
// Check if using agent mode
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
rtlamrCurrentAgent = isAgentMode ? currentAgent : null;
|
||||
|
||||
// Check if device is available (only for local mode)
|
||||
if (!isAgentMode && !checkDeviceAvailability('rtlamr')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2637,16 +2653,26 @@
|
||||
format: 'json'
|
||||
};
|
||||
|
||||
fetch('/start_rtlamr', {
|
||||
// Determine endpoint based on agent mode
|
||||
const endpoint = isAgentMode
|
||||
? `/controller/agents/${currentAgent}/rtlamr/start`
|
||||
: '/start_rtlamr';
|
||||
|
||||
fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config)
|
||||
}).then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'started') {
|
||||
reserveDevice(parseInt(device), 'rtlamr');
|
||||
// Handle controller proxy response format
|
||||
const scanResult = isAgentMode && data.result ? data.result : data;
|
||||
|
||||
if (scanResult.status === 'started' || scanResult.status === 'success') {
|
||||
if (!isAgentMode) {
|
||||
reserveDevice(parseInt(device), 'rtlamr');
|
||||
}
|
||||
setRtlamrRunning(true);
|
||||
startRtlamrStream();
|
||||
startRtlamrStream(isAgentMode);
|
||||
|
||||
// Initialize meter filter bar (reuse sensor filter bar since same structure)
|
||||
const filterContainer = document.getElementById('filterBarContainer');
|
||||
@@ -2667,21 +2693,34 @@
|
||||
// Clear existing output
|
||||
output.innerHTML = '<div class="placeholder signal-empty-state" style="display: none;"></div>';
|
||||
} else {
|
||||
alert('Error: ' + data.message);
|
||||
alert('Error: ' + (scanResult.message || scanResult.error || 'Failed to start'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function stopRtlamrDecoding() {
|
||||
fetch('/stop_rtlamr', { method: 'POST' })
|
||||
const isAgentMode = rtlamrCurrentAgent !== null;
|
||||
const endpoint = isAgentMode
|
||||
? `/controller/agents/${rtlamrCurrentAgent}/rtlamr/stop`
|
||||
: '/stop_rtlamr';
|
||||
|
||||
fetch(endpoint, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
releaseDevice('rtlamr');
|
||||
if (!isAgentMode) {
|
||||
releaseDevice('rtlamr');
|
||||
}
|
||||
rtlamrCurrentAgent = null;
|
||||
setRtlamrRunning(false);
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
// Clear polling timer
|
||||
if (rtlamrPollTimer) {
|
||||
clearInterval(rtlamrPollTimer);
|
||||
rtlamrPollTimer = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2701,12 +2740,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
function startRtlamrStream() {
|
||||
function startRtlamrStream(isAgentMode = false) {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
}
|
||||
|
||||
eventSource = new EventSource('/stream_rtlamr');
|
||||
// Use different stream endpoint for agent mode
|
||||
const streamUrl = isAgentMode ? '/controller/stream/all' : '/stream_rtlamr';
|
||||
eventSource = new EventSource(streamUrl);
|
||||
|
||||
eventSource.onopen = function () {
|
||||
showInfo('RTLAMR stream connected...');
|
||||
@@ -2714,20 +2755,86 @@
|
||||
|
||||
eventSource.onmessage = function (e) {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'rtlamr') {
|
||||
addRtlamrReading(data);
|
||||
} else if (data.type === 'status') {
|
||||
if (data.text === 'stopped') {
|
||||
setRtlamrRunning(false);
|
||||
|
||||
if (isAgentMode) {
|
||||
// Handle multi-agent stream format
|
||||
if (data.scan_type === 'rtlamr' && data.payload) {
|
||||
const payload = data.payload;
|
||||
if (payload.type === 'rtlamr') {
|
||||
payload.agent_name = data.agent_name;
|
||||
addRtlamrReading(payload);
|
||||
} else if (payload.type === 'status') {
|
||||
if (payload.text === 'stopped') {
|
||||
setRtlamrRunning(false);
|
||||
}
|
||||
} else if (payload.type === 'info' || payload.type === 'raw') {
|
||||
showInfo(`[${data.agent_name}] ${payload.text}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Local stream format
|
||||
if (data.type === 'rtlamr') {
|
||||
addRtlamrReading(data);
|
||||
} else if (data.type === 'status') {
|
||||
if (data.text === 'stopped') {
|
||||
setRtlamrRunning(false);
|
||||
}
|
||||
} else if (data.type === 'info' || data.type === 'raw') {
|
||||
showInfo(data.text);
|
||||
}
|
||||
} else if (data.type === 'info' || data.type === 'raw') {
|
||||
showInfo(data.text);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = function (e) {
|
||||
console.error('RTLAMR stream error');
|
||||
};
|
||||
|
||||
// Start polling fallback for agent mode
|
||||
if (isAgentMode) {
|
||||
startRtlamrPolling();
|
||||
}
|
||||
}
|
||||
|
||||
// Track last reading count for polling
|
||||
let lastRtlamrReadingCount = 0;
|
||||
|
||||
function startRtlamrPolling() {
|
||||
if (rtlamrPollTimer) return;
|
||||
lastRtlamrReadingCount = 0;
|
||||
|
||||
const pollInterval = 2000;
|
||||
rtlamrPollTimer = setInterval(async () => {
|
||||
if (!isRtlamrRunning || !rtlamrCurrentAgent) {
|
||||
clearInterval(rtlamrPollTimer);
|
||||
rtlamrPollTimer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/controller/agents/${rtlamrCurrentAgent}/rtlamr/data`);
|
||||
if (!response.ok) return;
|
||||
|
||||
const data = await response.json();
|
||||
const result = data.result || data;
|
||||
const readings = result.data || [];
|
||||
|
||||
// Process new readings
|
||||
if (readings.length > lastRtlamrReadingCount) {
|
||||
const newReadings = readings.slice(lastRtlamrReadingCount);
|
||||
newReadings.forEach(reading => {
|
||||
const displayReading = {
|
||||
type: 'rtlamr',
|
||||
...reading,
|
||||
agent_name: result.agent_name || 'Remote Agent'
|
||||
};
|
||||
addRtlamrReading(displayReading);
|
||||
});
|
||||
lastRtlamrReadingCount = readings.length;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('RTLAMR polling error:', err);
|
||||
}
|
||||
}, pollInterval);
|
||||
}
|
||||
|
||||
function addRtlamrReading(data) {
|
||||
@@ -3196,6 +3303,9 @@
|
||||
return protocols;
|
||||
}
|
||||
|
||||
// Pager mode polling timer for agent mode
|
||||
let pagerPollTimer = null;
|
||||
|
||||
function startDecoding() {
|
||||
const freq = document.getElementById('frequency').value;
|
||||
const gain = document.getElementById('gain').value;
|
||||
@@ -3209,13 +3319,16 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if device is available
|
||||
if (!checkDeviceAvailability('pager')) {
|
||||
// Check if using agent mode
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
|
||||
// Check if device is available (only for local mode)
|
||||
if (!isAgentMode && !checkDeviceAvailability('pager')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for remote SDR
|
||||
const remoteConfig = getRemoteSDRConfig();
|
||||
// Check for remote SDR (only for local mode)
|
||||
const remoteConfig = isAgentMode ? null : getRemoteSDRConfig();
|
||||
if (remoteConfig === false) return; // Validation failed
|
||||
|
||||
const config = {
|
||||
@@ -3229,22 +3342,32 @@
|
||||
bias_t: getBiasTEnabled()
|
||||
};
|
||||
|
||||
// Add rtl_tcp params if using remote SDR
|
||||
// Add rtl_tcp params if using remote SDR (local mode only)
|
||||
if (remoteConfig) {
|
||||
config.rtl_tcp_host = remoteConfig.host;
|
||||
config.rtl_tcp_port = remoteConfig.port;
|
||||
}
|
||||
|
||||
fetch('/start', {
|
||||
// Determine endpoint based on agent mode
|
||||
const endpoint = isAgentMode
|
||||
? `/controller/agents/${currentAgent}/pager/start`
|
||||
: '/start';
|
||||
|
||||
fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config)
|
||||
}).then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'started') {
|
||||
reserveDevice(parseInt(device), 'pager');
|
||||
// Handle controller proxy response format (agent response is nested in 'result')
|
||||
const scanResult = isAgentMode && data.result ? data.result : data;
|
||||
|
||||
if (scanResult.status === 'started' || scanResult.status === 'success') {
|
||||
if (!isAgentMode) {
|
||||
reserveDevice(parseInt(device), 'pager');
|
||||
}
|
||||
setRunning(true);
|
||||
startStream();
|
||||
startStream(isAgentMode);
|
||||
|
||||
// Initialize filter bar
|
||||
const filterContainer = document.getElementById('filterBarContainer');
|
||||
@@ -3260,24 +3383,37 @@
|
||||
// Clear address history for fresh session
|
||||
SignalCards.clearAddressHistory('pager');
|
||||
} else {
|
||||
alert('Error: ' + data.message);
|
||||
alert('Error: ' + (scanResult.message || scanResult.error || 'Failed to start pager decoding'));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Start error:', err);
|
||||
alert('Error starting pager decoding: ' + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
function stopDecoding() {
|
||||
fetch('/stop', { method: 'POST' })
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
const endpoint = isAgentMode
|
||||
? `/controller/agents/${currentAgent}/pager/stop`
|
||||
: '/stop';
|
||||
|
||||
fetch(endpoint, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
releaseDevice('pager');
|
||||
if (!isAgentMode) {
|
||||
releaseDevice('pager');
|
||||
}
|
||||
setRunning(false);
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
// Clear polling timer if active
|
||||
if (pagerPollTimer) {
|
||||
clearInterval(pagerPollTimer);
|
||||
pagerPollTimer = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3342,12 +3478,14 @@
|
||||
document.getElementById('stopBtn').style.display = running ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function startStream() {
|
||||
function startStream(isAgentMode = false) {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
}
|
||||
|
||||
eventSource = new EventSource('/stream');
|
||||
// Use different stream endpoint for agent mode
|
||||
const streamUrl = isAgentMode ? '/controller/stream/all' : '/stream';
|
||||
eventSource = new EventSource(streamUrl);
|
||||
|
||||
eventSource.onopen = function () {
|
||||
showInfo('Stream connected...');
|
||||
@@ -3356,24 +3494,101 @@
|
||||
eventSource.onmessage = function (e) {
|
||||
const data = JSON.parse(e.data);
|
||||
|
||||
if (data.type === 'message') {
|
||||
addMessage(data);
|
||||
} else if (data.type === 'status') {
|
||||
if (data.text === 'stopped') {
|
||||
setRunning(false);
|
||||
} else if (data.text === 'started') {
|
||||
showInfo('Decoder started, waiting for signals...');
|
||||
// Handle multi-agent stream format
|
||||
if (isAgentMode) {
|
||||
// Multi-agent stream tags data with scan_type and agent_name
|
||||
if (data.scan_type === 'pager' && data.payload) {
|
||||
const payload = data.payload;
|
||||
if (payload.type === 'message') {
|
||||
// Add agent info to the message
|
||||
payload.agent_name = data.agent_name;
|
||||
addMessage(payload);
|
||||
} else if (payload.type === 'status') {
|
||||
if (payload.text === 'stopped') {
|
||||
setRunning(false);
|
||||
} else if (payload.text === 'started') {
|
||||
showInfo(`Decoder started on ${data.agent_name}, waiting for signals...`);
|
||||
}
|
||||
} else if (payload.type === 'info') {
|
||||
showInfo(`[${data.agent_name}] ${payload.text}`);
|
||||
}
|
||||
} else if (data.type === 'keepalive') {
|
||||
// Ignore keepalive messages
|
||||
}
|
||||
} else {
|
||||
// Local stream format
|
||||
if (data.type === 'message') {
|
||||
addMessage(data);
|
||||
} else if (data.type === 'status') {
|
||||
if (data.text === 'stopped') {
|
||||
setRunning(false);
|
||||
} else if (data.text === 'started') {
|
||||
showInfo('Decoder started, waiting for signals...');
|
||||
}
|
||||
} else if (data.type === 'info') {
|
||||
showInfo(data.text);
|
||||
} else if (data.type === 'raw') {
|
||||
showInfo(data.text);
|
||||
}
|
||||
} else if (data.type === 'info') {
|
||||
showInfo(data.text);
|
||||
} else if (data.type === 'raw') {
|
||||
showInfo(data.text);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = function (e) {
|
||||
checkStatus();
|
||||
};
|
||||
|
||||
// Start polling fallback for agent mode (in case push isn't enabled)
|
||||
if (isAgentMode) {
|
||||
startPagerPolling();
|
||||
}
|
||||
}
|
||||
|
||||
// Track last message count to avoid duplicates during polling
|
||||
let lastPagerMsgCount = 0;
|
||||
|
||||
function startPagerPolling() {
|
||||
if (pagerPollTimer) return;
|
||||
lastPagerMsgCount = 0;
|
||||
|
||||
const pollInterval = 2000; // 2 seconds
|
||||
pagerPollTimer = setInterval(async () => {
|
||||
if (!isRunning) {
|
||||
clearInterval(pagerPollTimer);
|
||||
pagerPollTimer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/controller/agents/${currentAgent}/pager/data`);
|
||||
if (!response.ok) return;
|
||||
|
||||
const data = await response.json();
|
||||
const result = data.result || data;
|
||||
const modeData = result.data || result;
|
||||
|
||||
// Process messages from polling response
|
||||
if (modeData.messages && Array.isArray(modeData.messages)) {
|
||||
const newMsgs = modeData.messages.slice(lastPagerMsgCount);
|
||||
newMsgs.forEach(msg => {
|
||||
// Convert to expected format
|
||||
const displayMsg = {
|
||||
type: 'message',
|
||||
protocol: msg.protocol || 'UNKNOWN',
|
||||
address: msg.address || '',
|
||||
function: msg.function || '',
|
||||
msg_type: msg.msg_type || 'Alpha',
|
||||
message: msg.message || '',
|
||||
timestamp: msg.received_at || new Date().toISOString(),
|
||||
agent_name: result.agent_name || 'Remote Agent'
|
||||
};
|
||||
addMessage(displayMsg);
|
||||
});
|
||||
lastPagerMsgCount = modeData.messages.length;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Pager polling error:', err);
|
||||
}
|
||||
}, pollInterval);
|
||||
}
|
||||
|
||||
function addMessage(msg) {
|
||||
@@ -7084,12 +7299,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
// APRS mode polling timer for agent mode
|
||||
let aprsPollTimer = null;
|
||||
let aprsCurrentAgent = null;
|
||||
|
||||
function startAprs() {
|
||||
// Get values from function bar controls
|
||||
const region = document.getElementById('aprsStripRegion').value;
|
||||
const device = getSelectedDevice();
|
||||
const gain = document.getElementById('aprsStripGain').value;
|
||||
|
||||
// Check if using agent mode
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
aprsCurrentAgent = isAgentMode ? currentAgent : null;
|
||||
|
||||
// Build request body
|
||||
const requestBody = {
|
||||
region,
|
||||
@@ -7107,14 +7330,22 @@
|
||||
requestBody.frequency = customFreq;
|
||||
}
|
||||
|
||||
fetch('/aprs/start', {
|
||||
// Determine endpoint based on agent mode
|
||||
const endpoint = isAgentMode
|
||||
? `/controller/agents/${currentAgent}/aprs/start`
|
||||
: '/aprs/start';
|
||||
|
||||
fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(requestBody)
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'started') {
|
||||
// Handle controller proxy response format
|
||||
const scanResult = isAgentMode && data.result ? data.result : data;
|
||||
|
||||
if (scanResult.status === 'started' || scanResult.status === 'success') {
|
||||
isAprsRunning = true;
|
||||
aprsPacketCount = 0;
|
||||
aprsStationCount = 0;
|
||||
@@ -7138,7 +7369,7 @@
|
||||
document.getElementById('aprsMapStatus').textContent = 'TRACKING';
|
||||
document.getElementById('aprsMapStatus').style.color = 'var(--accent-green)';
|
||||
// Update function bar status
|
||||
updateAprsStatus('listening', data.frequency);
|
||||
updateAprsStatus('listening', scanResult.frequency);
|
||||
// Reset function bar stats
|
||||
document.getElementById('aprsStripStations').textContent = '0';
|
||||
document.getElementById('aprsStripPackets').textContent = '0';
|
||||
@@ -7149,9 +7380,9 @@
|
||||
const customFreqInput = document.getElementById('aprsStripCustomFreq');
|
||||
if (customFreqInput) customFreqInput.disabled = true;
|
||||
startAprsMeterCheck();
|
||||
startAprsStream();
|
||||
startAprsStream(isAgentMode);
|
||||
} else {
|
||||
alert('APRS Error: ' + data.message);
|
||||
alert('APRS Error: ' + (scanResult.message || scanResult.error || 'Failed to start'));
|
||||
updateAprsStatus('error');
|
||||
}
|
||||
})
|
||||
@@ -7162,10 +7393,16 @@
|
||||
}
|
||||
|
||||
function stopAprs() {
|
||||
fetch('/aprs/stop', { method: 'POST' })
|
||||
const isAgentMode = aprsCurrentAgent !== null;
|
||||
const endpoint = isAgentMode
|
||||
? `/controller/agents/${aprsCurrentAgent}/aprs/stop`
|
||||
: '/aprs/stop';
|
||||
|
||||
fetch(endpoint, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
isAprsRunning = false;
|
||||
aprsCurrentAgent = null;
|
||||
// Update function bar buttons
|
||||
document.getElementById('aprsStripStartBtn').style.display = 'inline-block';
|
||||
document.getElementById('aprsStripStopBtn').style.display = 'none';
|
||||
@@ -7192,29 +7429,60 @@
|
||||
aprsEventSource.close();
|
||||
aprsEventSource = null;
|
||||
}
|
||||
// Clear polling timer
|
||||
if (aprsPollTimer) {
|
||||
clearInterval(aprsPollTimer);
|
||||
aprsPollTimer = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function startAprsStream() {
|
||||
function startAprsStream(isAgentMode = false) {
|
||||
if (aprsEventSource) aprsEventSource.close();
|
||||
aprsEventSource = new EventSource('/aprs/stream');
|
||||
|
||||
// Use different stream endpoint for agent mode
|
||||
const streamUrl = isAgentMode ? '/controller/stream/all' : '/aprs/stream';
|
||||
aprsEventSource = new EventSource(streamUrl);
|
||||
|
||||
aprsEventSource.onmessage = function (e) {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'aprs') {
|
||||
aprsPacketCount++;
|
||||
// Update map footer and function bar
|
||||
document.getElementById('aprsPacketCount').textContent = aprsPacketCount;
|
||||
document.getElementById('aprsStripPackets').textContent = aprsPacketCount;
|
||||
// Switch to tracking state on first packet
|
||||
const dot = document.getElementById('aprsStripDot');
|
||||
if (dot && !dot.classList.contains('tracking')) {
|
||||
updateAprsStatus('tracking');
|
||||
|
||||
if (isAgentMode) {
|
||||
// Handle multi-agent stream format
|
||||
if (data.scan_type === 'aprs' && data.payload) {
|
||||
const payload = data.payload;
|
||||
if (payload.type === 'aprs') {
|
||||
aprsPacketCount++;
|
||||
document.getElementById('aprsPacketCount').textContent = aprsPacketCount;
|
||||
document.getElementById('aprsStripPackets').textContent = aprsPacketCount;
|
||||
const dot = document.getElementById('aprsStripDot');
|
||||
if (dot && !dot.classList.contains('tracking')) {
|
||||
updateAprsStatus('tracking');
|
||||
}
|
||||
// Add agent info
|
||||
payload.agent_name = data.agent_name;
|
||||
processAprsPacket(payload);
|
||||
} else if (payload.type === 'meter') {
|
||||
updateAprsMeter(payload.level);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Local stream format
|
||||
if (data.type === 'aprs') {
|
||||
aprsPacketCount++;
|
||||
// Update map footer and function bar
|
||||
document.getElementById('aprsPacketCount').textContent = aprsPacketCount;
|
||||
document.getElementById('aprsStripPackets').textContent = aprsPacketCount;
|
||||
// Switch to tracking state on first packet
|
||||
const dot = document.getElementById('aprsStripDot');
|
||||
if (dot && !dot.classList.contains('tracking')) {
|
||||
updateAprsStatus('tracking');
|
||||
}
|
||||
processAprsPacket(data);
|
||||
} else if (data.type === 'meter') {
|
||||
// Update signal indicator in function bar
|
||||
updateAprsMeter(data.level);
|
||||
}
|
||||
processAprsPacket(data);
|
||||
} else if (data.type === 'meter') {
|
||||
// Update signal indicator in function bar
|
||||
updateAprsMeter(data.level);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7222,6 +7490,61 @@
|
||||
console.error('APRS stream error');
|
||||
updateAprsStatus('error');
|
||||
};
|
||||
|
||||
// Start polling fallback for agent mode
|
||||
if (isAgentMode) {
|
||||
startAprsPolling();
|
||||
}
|
||||
}
|
||||
|
||||
// Track last station count for polling
|
||||
let lastAprsStationCount = 0;
|
||||
|
||||
function startAprsPolling() {
|
||||
if (aprsPollTimer) return;
|
||||
lastAprsStationCount = 0;
|
||||
|
||||
const pollInterval = 2000;
|
||||
aprsPollTimer = setInterval(async () => {
|
||||
if (!isAprsRunning || !aprsCurrentAgent) {
|
||||
clearInterval(aprsPollTimer);
|
||||
aprsPollTimer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/controller/agents/${aprsCurrentAgent}/aprs/data`);
|
||||
if (!response.ok) return;
|
||||
|
||||
const data = await response.json();
|
||||
const result = data.result || data;
|
||||
const stations = result.data || [];
|
||||
|
||||
// Process new stations
|
||||
if (stations.length > lastAprsStationCount) {
|
||||
const newStations = stations.slice(lastAprsStationCount);
|
||||
newStations.forEach(station => {
|
||||
aprsPacketCount++;
|
||||
document.getElementById('aprsPacketCount').textContent = aprsPacketCount;
|
||||
document.getElementById('aprsStripPackets').textContent = aprsPacketCount;
|
||||
const dot = document.getElementById('aprsStripDot');
|
||||
if (dot && !dot.classList.contains('tracking')) {
|
||||
updateAprsStatus('tracking');
|
||||
}
|
||||
// Convert to expected packet format
|
||||
const packet = {
|
||||
type: 'aprs',
|
||||
...station,
|
||||
agent_name: result.agent_name || 'Remote Agent'
|
||||
};
|
||||
processAprsPacket(packet);
|
||||
});
|
||||
lastAprsStationCount = stations.length;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('APRS polling error:', err);
|
||||
}
|
||||
}, pollInterval);
|
||||
}
|
||||
|
||||
// Signal Meter Functions
|
||||
@@ -7710,6 +8033,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Satellite mode agent state
|
||||
let satelliteCurrentAgent = null;
|
||||
|
||||
function calculatePasses() {
|
||||
const lat = parseFloat(document.getElementById('obsLat').value);
|
||||
const lon = parseFloat(document.getElementById('obsLon').value);
|
||||
@@ -7723,18 +8049,30 @@
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/satellite/predict', {
|
||||
// Check if using agent mode
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
satelliteCurrentAgent = isAgentMode ? currentAgent : null;
|
||||
|
||||
// Determine endpoint based on agent mode
|
||||
const endpoint = isAgentMode
|
||||
? `/controller/agents/${currentAgent}/satellite/predict`
|
||||
: '/satellite/predict';
|
||||
|
||||
fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ lat, lon, hours, minEl, satellites })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
satellitePasses = data.passes;
|
||||
// Handle controller proxy response format
|
||||
const result = isAgentMode && data.result ? data.result : data;
|
||||
|
||||
if (result.status === 'success') {
|
||||
satellitePasses = result.passes;
|
||||
renderPassList();
|
||||
document.getElementById('passCount').textContent = data.passes.length;
|
||||
if (data.passes.length > 0) {
|
||||
document.getElementById('passCount').textContent = result.passes.length;
|
||||
if (result.passes.length > 0) {
|
||||
selectPass(0);
|
||||
document.getElementById('satelliteCountdown').style.display = 'block';
|
||||
updateSatelliteCountdown();
|
||||
@@ -7743,7 +8081,7 @@
|
||||
document.getElementById('satelliteCountdown').style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
alert('Error: ' + data.message);
|
||||
alert('Error: ' + (result.message || result.error || 'Failed to predict passes'));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -7941,15 +8279,24 @@
|
||||
const lat = parseFloat(document.getElementById('obsLat').value);
|
||||
const lon = parseFloat(document.getElementById('obsLon').value);
|
||||
|
||||
fetch('/satellite/position', {
|
||||
// Check if using agent mode
|
||||
const isAgentMode = satelliteCurrentAgent !== null;
|
||||
const endpoint = isAgentMode
|
||||
? `/controller/agents/${satelliteCurrentAgent}/satellite/position`
|
||||
: '/satellite/position';
|
||||
|
||||
fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ lat, lon, satellites, includeTrack: true })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success' && data.positions) {
|
||||
updateRealTimeIndicators(data.positions);
|
||||
// Handle controller proxy response format
|
||||
const result = isAgentMode && data.result ? data.result : data;
|
||||
|
||||
if (result.status === 'success' && result.positions) {
|
||||
updateRealTimeIndicators(result.positions);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -8350,10 +8697,33 @@
|
||||
|
||||
async function refreshTscmDevices() {
|
||||
// Fetch available interfaces for TSCM scanning
|
||||
// Check if agent is selected and route accordingly
|
||||
try {
|
||||
const response = await fetch('/tscm/devices');
|
||||
let response;
|
||||
if (typeof currentAgent !== 'undefined' && currentAgent !== 'local') {
|
||||
// Fetch devices from agent capabilities
|
||||
response = await fetch(`/controller/agents/${currentAgent}?refresh=true`);
|
||||
} else {
|
||||
response = await fetch('/tscm/devices');
|
||||
}
|
||||
const data = await response.json();
|
||||
const devices = data.devices || {};
|
||||
|
||||
// Handle both local (/tscm/devices) and agent response formats
|
||||
let devices;
|
||||
const isAgentResponse = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
|
||||
if (isAgentResponse && data.agent) {
|
||||
// Agent response format - extract from capabilities/interfaces
|
||||
const agentInterfaces = data.agent.interfaces || {};
|
||||
const agentCapabilities = data.agent.capabilities || {};
|
||||
devices = {
|
||||
wifi_interfaces: agentInterfaces.wifi_interfaces || [],
|
||||
bt_adapters: agentInterfaces.bt_adapters || [],
|
||||
sdr_devices: agentCapabilities.devices || agentInterfaces.sdr_devices || []
|
||||
};
|
||||
} else {
|
||||
devices = data.devices || {};
|
||||
}
|
||||
|
||||
// Populate WiFi interfaces
|
||||
const wifiSelect = document.getElementById('tscmWifiInterface');
|
||||
@@ -8370,7 +8740,11 @@
|
||||
wifiSelect.value = devices.wifi_interfaces[0].name;
|
||||
}
|
||||
} else {
|
||||
wifiSelect.innerHTML = '<option value="">No WiFi interfaces found</option>';
|
||||
if (isAgentResponse) {
|
||||
wifiSelect.innerHTML = '<option value="">Agent manages WiFi</option>';
|
||||
} else {
|
||||
wifiSelect.innerHTML = '<option value="">No WiFi interfaces found</option>';
|
||||
}
|
||||
}
|
||||
|
||||
// Populate Bluetooth adapters
|
||||
@@ -8388,7 +8762,11 @@
|
||||
btSelect.value = devices.bt_adapters[0].name;
|
||||
}
|
||||
} else {
|
||||
btSelect.innerHTML = '<option value="">No Bluetooth adapters found</option>';
|
||||
if (isAgentResponse) {
|
||||
btSelect.innerHTML = '<option value="">Agent manages Bluetooth</option>';
|
||||
} else {
|
||||
btSelect.innerHTML = '<option value="">No Bluetooth adapters found</option>';
|
||||
}
|
||||
}
|
||||
|
||||
// Populate SDR devices
|
||||
@@ -8397,16 +8775,20 @@
|
||||
if (devices.sdr_devices && devices.sdr_devices.length > 0) {
|
||||
devices.sdr_devices.forEach(dev => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = dev.index;
|
||||
opt.value = dev.index !== undefined ? dev.index : 0;
|
||||
opt.textContent = dev.display_name || dev.name || 'SDR Device';
|
||||
sdrSelect.appendChild(opt);
|
||||
});
|
||||
// Auto-select first SDR if available
|
||||
if (devices.sdr_devices.length > 0) {
|
||||
sdrSelect.value = devices.sdr_devices[0].index;
|
||||
sdrSelect.value = devices.sdr_devices[0].index !== undefined ? devices.sdr_devices[0].index : 0;
|
||||
}
|
||||
} else {
|
||||
sdrSelect.innerHTML = '<option value="">No SDR devices found</option>';
|
||||
if (isAgentResponse) {
|
||||
sdrSelect.innerHTML = '<option value="">Agent manages SDR</option>';
|
||||
} else {
|
||||
sdrSelect.innerHTML = '<option value="">No SDR devices found</option>';
|
||||
}
|
||||
}
|
||||
|
||||
// Show warnings (e.g., not running as root)
|
||||
@@ -8465,8 +8847,23 @@
|
||||
document.getElementById('tscmDeviceWarnings').style.display = 'none';
|
||||
document.getElementById('tscmDeviceWarnings').innerHTML = '';
|
||||
|
||||
// Check for agent mode
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
|
||||
// Check for conflicts if using agent
|
||||
if (isAgentMode && typeof checkAgentModeConflict === 'function') {
|
||||
if (!checkAgentModeConflict('tscm')) {
|
||||
return; // Conflict detected, user cancelled
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/tscm/sweep/start', {
|
||||
// Route to agent or local based on selection
|
||||
const endpoint = isAgentMode
|
||||
? `/controller/agents/${currentAgent}/tscm/start`
|
||||
: '/tscm/sweep/start';
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -8483,7 +8880,9 @@
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.status === 'success') {
|
||||
// Handle controller proxy response (agent response is nested in 'result')
|
||||
const scanResult = isAgentMode && data.result ? data.result : data;
|
||||
if (scanResult.status === 'success' || scanResult.status === 'started') {
|
||||
isTscmRunning = true;
|
||||
tscmSweepStartTime = new Date();
|
||||
tscmSweepEndTime = null;
|
||||
@@ -8496,16 +8895,16 @@
|
||||
document.getElementById('tscmReportBtn').style.display = 'none';
|
||||
|
||||
// Show warnings if any devices unavailable
|
||||
if (data.warnings && data.warnings.length > 0) {
|
||||
if (scanResult.warnings && scanResult.warnings.length > 0) {
|
||||
const warningsDiv = document.getElementById('tscmDeviceWarnings');
|
||||
warningsDiv.innerHTML = data.warnings.map(w =>
|
||||
warningsDiv.innerHTML = scanResult.warnings.map(w =>
|
||||
`<div style="color: #ff9933; font-size: 10px; margin-bottom: 2px;">⚠ ${w}</div>`
|
||||
).join('');
|
||||
warningsDiv.style.display = 'block';
|
||||
}
|
||||
|
||||
// Update device indicators
|
||||
updateTscmDeviceIndicators(data.devices);
|
||||
updateTscmDeviceIndicators(scanResult.devices);
|
||||
|
||||
// Reset displays
|
||||
tscmThreats = [];
|
||||
@@ -8519,9 +8918,9 @@
|
||||
startTscmStream();
|
||||
} else {
|
||||
// Show error with details
|
||||
let errorMsg = data.message || 'Failed to start sweep';
|
||||
if (data.details && data.details.length > 0) {
|
||||
errorMsg += '\n\n' + data.details.join('\n');
|
||||
let errorMsg = scanResult.message || 'Failed to start sweep';
|
||||
if (scanResult.details && scanResult.details.length > 0) {
|
||||
errorMsg += '\n\n' + scanResult.details.join('\n');
|
||||
}
|
||||
alert(errorMsg);
|
||||
}
|
||||
@@ -8552,7 +8951,12 @@
|
||||
|
||||
async function stopTscmSweep() {
|
||||
try {
|
||||
await fetch('/tscm/sweep/stop', { method: 'POST' });
|
||||
// Route to agent or local based on selection
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
const endpoint = isAgentMode
|
||||
? `/controller/agents/${currentAgent}/tscm/stop`
|
||||
: '/tscm/sweep/stop';
|
||||
await fetch(endpoint, { method: 'POST' });
|
||||
} catch (e) {
|
||||
console.error('Error stopping sweep:', e);
|
||||
}
|
||||
@@ -9111,12 +9515,33 @@
|
||||
tscmEventSource.close();
|
||||
}
|
||||
|
||||
tscmEventSource = new EventSource('/tscm/sweep/stream');
|
||||
// Check if using agent - connect to multi-agent stream
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
const streamUrl = isAgentMode
|
||||
? '/controller/stream/all'
|
||||
: '/tscm/sweep/stream';
|
||||
|
||||
tscmEventSource = new EventSource(streamUrl);
|
||||
|
||||
tscmEventSource.onmessage = function (event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
handleTscmEvent(data);
|
||||
|
||||
// If using multi-agent stream, filter for TSCM data
|
||||
if (isAgentMode) {
|
||||
if (data.scan_type === 'tscm' || data.type?.startsWith('tscm') ||
|
||||
data.type === 'wifi_device' || data.type === 'bt_device' ||
|
||||
data.type === 'rf_signal' || data.type === 'threat' ||
|
||||
data.type === 'sweep_progress') {
|
||||
// Add agent info to data for display
|
||||
if (data.agent_name) {
|
||||
data._agent = data.agent_name;
|
||||
}
|
||||
handleTscmEvent(data.payload || data);
|
||||
}
|
||||
} else {
|
||||
handleTscmEvent(data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('TSCM SSE parse error:', e);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,14 @@
|
||||
<!-- Populated by JavaScript with capability warnings -->
|
||||
</div>
|
||||
|
||||
<!-- Show All Agents option (visible when agents are available) -->
|
||||
<div id="btShowAllAgentsContainer" class="section" style="display: none; padding: 8px;">
|
||||
<label class="inline-checkbox" style="font-size: 10px;">
|
||||
<input type="checkbox" id="btShowAllAgents" onchange="if(typeof BluetoothMode !== 'undefined') BluetoothMode.toggleShowAllAgents(this.checked)">
|
||||
Show devices from all agents
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Scanner Configuration</h3>
|
||||
<div class="form-group">
|
||||
|
||||
@@ -11,6 +11,13 @@
|
||||
</button>
|
||||
</div>
|
||||
<div id="wifiCapabilityStatus" class="info-text" style="margin-top: 8px; font-size: 10px;"></div>
|
||||
<!-- Show All Agents option (visible when agents are available) -->
|
||||
<div id="wifiShowAllAgentsContainer" style="margin-top: 8px; display: none;">
|
||||
<label class="inline-checkbox" style="font-size: 10px;">
|
||||
<input type="checkbox" id="wifiShowAllAgents" onchange="if(typeof WiFiMode !== 'undefined') WiFiMode.toggleShowAllAgents(this.checked)">
|
||||
Show networks from all agents
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
|
||||
Reference in New Issue
Block a user