mirror of
https://github.com/smittix/intercept.git
synced 2026-06-17 18:09:45 -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
|
## Controller API
|
||||||
|
|
||||||
### Agent Management
|
### Agent Management
|
||||||
@@ -396,6 +435,62 @@ bluetooth = true
|
|||||||
4. **Firewall**: Restrict agent ports to controller IP only
|
4. **Firewall**: Restrict agent ports to controller IP only
|
||||||
5. **allowed_ips**: Use this config option to restrict agent connections
|
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
|
## Files
|
||||||
|
|
||||||
| File | Description |
|
| File | Description |
|
||||||
@@ -407,3 +502,5 @@ bluetooth = true
|
|||||||
| `utils/database.py` | Agent CRUD operations |
|
| `utils/database.py` | Agent CRUD operations |
|
||||||
| `static/js/core/agents.js` | Frontend agent management |
|
| `static/js/core/agents.js` | Frontend agent management |
|
||||||
| `templates/agents.html` | Agent management page |
|
| `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 |
|
||||||
|
|||||||
+1386
-3
File diff suppressed because it is too large
Load Diff
@@ -240,6 +240,33 @@ def refresh_agent_metadata(agent_id: int):
|
|||||||
}), 503
|
}), 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
|
# Proxy Operations - Forward requests to agents
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -1710,6 +1710,12 @@ body {
|
|||||||
box-shadow: 0 0 10px var(--accent-red);
|
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 {
|
.strip-time {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|||||||
+23
-1
@@ -206,11 +206,33 @@
|
|||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'JetBrains Mono', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-badge.local {
|
.agent-badge.local,
|
||||||
|
.agent-badge.agent-local {
|
||||||
background: rgba(0, 255, 136, 0.1);
|
background: rgba(0, 255, 136, 0.1);
|
||||||
color: var(--accent-green);
|
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 {
|
.agent-badge-dot {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
|
|||||||
+357
-4
@@ -10,6 +10,8 @@ let currentAgent = 'local';
|
|||||||
let agentEventSource = null;
|
let agentEventSource = null;
|
||||||
let multiAgentMode = false; // Show combined results from all agents
|
let multiAgentMode = false; // Show combined results from all agents
|
||||||
let multiAgentPollInterval = null;
|
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 ==============
|
// ============== AGENT LOADING ==============
|
||||||
|
|
||||||
@@ -54,6 +56,28 @@ function updateAgentSelector() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateAgentStatus();
|
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() {
|
function updateAgentStatus() {
|
||||||
@@ -88,10 +112,36 @@ function selectAgent(agentId) {
|
|||||||
if (typeof refreshDevices === 'function') {
|
if (typeof refreshDevices === 'function') {
|
||||||
refreshDevices();
|
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');
|
console.log('Agent selected: Local');
|
||||||
} else {
|
} else {
|
||||||
// Fetch devices from remote agent
|
// Fetch devices from remote agent
|
||||||
refreshAgentDevices(agentId);
|
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';
|
const agentName = agents.find(a => a.id == agentId)?.name || 'Unknown';
|
||||||
console.log(`Agent selected: ${agentName}`);
|
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) {
|
async function refreshAgentDevices(agentId) {
|
||||||
console.log(`Refreshing devices for agent ${agentId}...`);
|
console.log(`Refreshing devices for agent ${agentId}...`);
|
||||||
try {
|
try {
|
||||||
@@ -430,14 +761,36 @@ function handleMultiAgentData(data) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'wifi':
|
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) {
|
if (payload && payload.networks) {
|
||||||
Object.values(payload.networks).forEach(net => {
|
Object.values(payload.networks).forEach(net => {
|
||||||
net._agent = agentName;
|
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;
|
break;
|
||||||
|
|
||||||
|
|||||||
+361
-32
@@ -9,6 +9,7 @@ const BluetoothMode = (function() {
|
|||||||
// State
|
// State
|
||||||
let isScanning = false;
|
let isScanning = false;
|
||||||
let eventSource = null;
|
let eventSource = null;
|
||||||
|
let agentPollTimer = null; // Polling fallback for agent mode
|
||||||
let devices = new Map();
|
let devices = new Map();
|
||||||
let baselineSet = false;
|
let baselineSet = false;
|
||||||
let baselineCount = 0;
|
let baselineCount = 0;
|
||||||
@@ -36,6 +37,47 @@ const BluetoothMode = (function() {
|
|||||||
// Device list filter
|
// Device list filter
|
||||||
let currentDeviceFilter = 'all';
|
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
|
* Initialize the Bluetooth mode
|
||||||
*/
|
*/
|
||||||
@@ -526,8 +568,37 @@ const BluetoothMode = (function() {
|
|||||||
*/
|
*/
|
||||||
async function checkCapabilities() {
|
async function checkCapabilities() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/bluetooth/capabilities');
|
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||||
const data = await response.json();
|
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) {
|
if (!data.available) {
|
||||||
showCapabilityWarning(['Bluetooth not available on this system']);
|
showCapabilityWarning(['Bluetooth not available on this system']);
|
||||||
@@ -599,32 +670,60 @@ const BluetoothMode = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function startScan() {
|
async function startScan() {
|
||||||
|
// Check for agent mode conflicts
|
||||||
|
if (!checkAgentConflicts()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const adapter = adapterSelect?.value || '';
|
const adapter = adapterSelect?.value || '';
|
||||||
const mode = scanModeSelect?.value || 'auto';
|
const mode = scanModeSelect?.value || 'auto';
|
||||||
const transport = transportSelect?.value || 'auto';
|
const transport = transportSelect?.value || 'auto';
|
||||||
const duration = parseInt(durationInput?.value || '0', 10);
|
const duration = parseInt(durationInput?.value || '0', 10);
|
||||||
const minRssi = parseInt(minRssiInput?.value || '-100', 10);
|
const minRssi = parseInt(minRssiInput?.value || '-100', 10);
|
||||||
|
|
||||||
|
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/bluetooth/scan/start', {
|
let response;
|
||||||
method: 'POST',
|
if (isAgentMode) {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
// Route through agent proxy
|
||||||
body: JSON.stringify({
|
response = await fetch(`/controller/agents/${currentAgent}/bluetooth/start`, {
|
||||||
mode: mode,
|
method: 'POST',
|
||||||
adapter_id: adapter || undefined,
|
headers: { 'Content-Type': 'application/json' },
|
||||||
duration_s: duration > 0 ? duration : undefined,
|
body: JSON.stringify({
|
||||||
transport: transport,
|
mode: mode,
|
||||||
rssi_threshold: minRssi
|
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();
|
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);
|
setScanning(true);
|
||||||
startEventStream();
|
startEventStream();
|
||||||
|
} else if (scanResult.status === 'error') {
|
||||||
|
showErrorMessage(scanResult.message || 'Failed to start scan');
|
||||||
} else {
|
} else {
|
||||||
showErrorMessage(data.message || 'Failed to start scan');
|
showErrorMessage(scanResult.message || 'Failed to start scan');
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -634,8 +733,14 @@ const BluetoothMode = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function stopScan() {
|
async function stopScan() {
|
||||||
|
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||||
|
|
||||||
try {
|
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);
|
setScanning(false);
|
||||||
stopEventStream();
|
stopEventStream();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -680,27 +785,84 @@ const BluetoothMode = (function() {
|
|||||||
function startEventStream() {
|
function startEventStream() {
|
||||||
if (eventSource) eventSource.close();
|
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) => {
|
if (isAgentMode) {
|
||||||
try {
|
// Use multi-agent stream for remote agents
|
||||||
const device = JSON.parse(e.data);
|
streamUrl = '/controller/stream/all';
|
||||||
handleDeviceUpdate(device);
|
console.log('[BT] Starting multi-agent event stream...');
|
||||||
} catch (err) {
|
} else {
|
||||||
console.error('Failed to parse device update:', err);
|
streamUrl = '/api/bluetooth/stream';
|
||||||
}
|
console.log('[BT] Starting local event stream...');
|
||||||
});
|
}
|
||||||
|
|
||||||
eventSource.addEventListener('scan_started', (e) => {
|
eventSource = new EventSource(streamUrl);
|
||||||
setScanning(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.addEventListener('scan_stopped', (e) => {
|
if (isAgentMode) {
|
||||||
setScanning(false);
|
// 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 = () => {
|
eventSource.onerror = () => {
|
||||||
console.warn('Bluetooth SSE connection error');
|
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.close();
|
||||||
eventSource = null;
|
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) {
|
function handleDeviceUpdate(device) {
|
||||||
@@ -876,6 +1086,7 @@ const BluetoothMode = (function() {
|
|||||||
const trackerType = device.tracker_type;
|
const trackerType = device.tracker_type;
|
||||||
const trackerConfidence = device.tracker_confidence;
|
const trackerConfidence = device.tracker_confidence;
|
||||||
const riskScore = device.risk_score || 0;
|
const riskScore = device.risk_score || 0;
|
||||||
|
const agentName = device._agent || 'Local';
|
||||||
|
|
||||||
// Calculate RSSI bar width (0-100%)
|
// Calculate RSSI bar width (0-100%)
|
||||||
// RSSI typically ranges from -100 (weak) to -30 (very strong)
|
// RSSI typically ranges from -100 (weak) to -30 (very strong)
|
||||||
@@ -929,6 +1140,10 @@ const BluetoothMode = (function() {
|
|||||||
let secondaryParts = [addr];
|
let secondaryParts = [addr];
|
||||||
if (mfr) secondaryParts.push(mfr);
|
if (mfr) secondaryParts.push(mfr);
|
||||||
secondaryParts.push('Seen ' + seenCount + '×');
|
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(' · ');
|
const secondaryInfo = secondaryParts.join(' · ');
|
||||||
|
|
||||||
// Row border color - highlight trackers in red/orange
|
// Row border color - highlight trackers in red/orange
|
||||||
@@ -1019,6 +1234,112 @@ const BluetoothMode = (function() {
|
|||||||
|
|
||||||
function showErrorMessage(message) {
|
function showErrorMessage(message) {
|
||||||
console.error('[BT] Error:', 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
|
// Public API
|
||||||
@@ -1033,8 +1354,16 @@ const BluetoothMode = (function() {
|
|||||||
selectDevice,
|
selectDevice,
|
||||||
clearSelection,
|
clearSelection,
|
||||||
copyAddress,
|
copyAddress,
|
||||||
|
|
||||||
|
// Agent handling
|
||||||
|
handleAgentChange,
|
||||||
|
clearData,
|
||||||
|
toggleShowAllAgents,
|
||||||
|
|
||||||
|
// Getters
|
||||||
getDevices: () => Array.from(devices.values()),
|
getDevices: () => Array.from(devices.values()),
|
||||||
isScanning: () => isScanning
|
isScanning: () => isScanning,
|
||||||
|
isShowAllAgents: () => showAllAgentsMode
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ let recentSignalHits = new Map();
|
|||||||
let isDirectListening = false;
|
let isDirectListening = false;
|
||||||
let currentModulation = 'am';
|
let currentModulation = 'am';
|
||||||
|
|
||||||
|
// Agent mode state
|
||||||
|
let listeningPostCurrentAgent = null;
|
||||||
|
let listeningPostPollTimer = null;
|
||||||
|
|
||||||
// ============== PRESETS ==============
|
// ============== PRESETS ==============
|
||||||
|
|
||||||
const scannerPresets = {
|
const scannerPresets = {
|
||||||
@@ -145,6 +149,10 @@ function startScanner() {
|
|||||||
const dwell = dwellSelect ? parseInt(dwellSelect.value) : 10;
|
const dwell = dwellSelect ? parseInt(dwellSelect.value) : 10;
|
||||||
const device = getSelectedDevice();
|
const device = getSelectedDevice();
|
||||||
|
|
||||||
|
// Check if using agent mode
|
||||||
|
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||||
|
listeningPostCurrentAgent = isAgentMode ? currentAgent : null;
|
||||||
|
|
||||||
if (startFreq >= endFreq) {
|
if (startFreq >= endFreq) {
|
||||||
if (typeof showNotification === 'function') {
|
if (typeof showNotification === 'function') {
|
||||||
showNotification('Scanner Error', 'End frequency must be greater than start');
|
showNotification('Scanner Error', 'End frequency must be greater than start');
|
||||||
@@ -152,8 +160,8 @@ function startScanner() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if device is available
|
// Check if device is available (only for local mode)
|
||||||
if (typeof checkDeviceAvailability === 'function' && !checkDeviceAvailability('scanner')) {
|
if (!isAgentMode && typeof checkDeviceAvailability === 'function' && !checkDeviceAvailability('scanner')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +189,12 @@ function startScanner() {
|
|||||||
document.getElementById('mainRangeEnd').textContent = endFreq.toFixed(1) + ' MHz';
|
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',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -198,8 +211,11 @@ function startScanner() {
|
|||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.status === 'started') {
|
// Handle controller proxy response format
|
||||||
if (typeof reserveDevice === 'function') reserveDevice(device, 'scanner');
|
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;
|
isScannerRunning = true;
|
||||||
isScannerPaused = false;
|
isScannerPaused = false;
|
||||||
scannerSignalActive = false;
|
scannerSignalActive = false;
|
||||||
@@ -229,7 +245,7 @@ function startScanner() {
|
|||||||
const levelMeter = document.getElementById('scannerLevelMeter');
|
const levelMeter = document.getElementById('scannerLevelMeter');
|
||||||
if (levelMeter) levelMeter.style.display = 'block';
|
if (levelMeter) levelMeter.style.display = 'block';
|
||||||
|
|
||||||
connectScannerStream();
|
connectScannerStream(isAgentMode);
|
||||||
addScannerLogEntry('Scanner started', `Range: ${startFreq}-${endFreq} MHz, Step: ${step} kHz`);
|
addScannerLogEntry('Scanner started', `Range: ${startFreq}-${endFreq} MHz, Step: ${step} kHz`);
|
||||||
if (typeof showNotification === 'function') {
|
if (typeof showNotification === 'function') {
|
||||||
showNotification('Scanner Started', `Scanning ${startFreq} - ${endFreq} MHz`);
|
showNotification('Scanner Started', `Scanning ${startFreq} - ${endFreq} MHz`);
|
||||||
@@ -237,7 +253,7 @@ function startScanner() {
|
|||||||
} else {
|
} else {
|
||||||
updateScannerDisplay('ERROR', 'var(--accent-red)');
|
updateScannerDisplay('ERROR', 'var(--accent-red)');
|
||||||
if (typeof showNotification === 'function') {
|
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() {
|
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(() => {
|
.then(() => {
|
||||||
if (typeof releaseDevice === 'function') releaseDevice('scanner');
|
if (!isAgentMode && typeof releaseDevice === 'function') releaseDevice('scanner');
|
||||||
|
listeningPostCurrentAgent = null;
|
||||||
isScannerRunning = false;
|
isScannerRunning = false;
|
||||||
isScannerPaused = false;
|
isScannerPaused = false;
|
||||||
scannerSignalActive = false;
|
scannerSignalActive = false;
|
||||||
|
|
||||||
|
// Clear polling timer
|
||||||
|
if (listeningPostPollTimer) {
|
||||||
|
clearInterval(listeningPostPollTimer);
|
||||||
|
listeningPostPollTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Update sidebar (with null checks)
|
// Update sidebar (with null checks)
|
||||||
const startBtn = document.getElementById('scannerStartBtn');
|
const startBtn = document.getElementById('scannerStartBtn');
|
||||||
if (startBtn) {
|
if (startBtn) {
|
||||||
@@ -386,17 +414,29 @@ function skipSignal() {
|
|||||||
|
|
||||||
// ============== SCANNER STREAM ==============
|
// ============== SCANNER STREAM ==============
|
||||||
|
|
||||||
function connectScannerStream() {
|
function connectScannerStream(isAgentMode = false) {
|
||||||
if (scannerEventSource) {
|
if (scannerEventSource) {
|
||||||
scannerEventSource.close();
|
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) {
|
scannerEventSource.onmessage = function(e) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(e.data);
|
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) {
|
} catch (err) {
|
||||||
console.warn('Scanner parse error:', err);
|
console.warn('Scanner parse error:', err);
|
||||||
}
|
}
|
||||||
@@ -404,9 +444,68 @@ function connectScannerStream() {
|
|||||||
|
|
||||||
scannerEventSource.onerror = function() {
|
scannerEventSource.onerror = function() {
|
||||||
if (isScannerRunning) {
|
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) {
|
function handleScannerEvent(data) {
|
||||||
|
|||||||
+347
-35
@@ -28,6 +28,47 @@ const WiFiMode = (function() {
|
|||||||
maxProbes: 1000,
|
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
|
// State
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
@@ -49,6 +90,10 @@ const WiFiMode = (function() {
|
|||||||
let currentFilter = 'all';
|
let currentFilter = 'all';
|
||||||
let currentSort = { field: 'rssi', order: 'desc' };
|
let currentSort = { field: 'rssi', order: 'desc' };
|
||||||
|
|
||||||
|
// Agent state
|
||||||
|
let showAllAgentsMode = false; // Show combined results from all agents
|
||||||
|
let lastAgentId = null; // Track agent switches
|
||||||
|
|
||||||
// Capabilities
|
// Capabilities
|
||||||
let capabilities = null;
|
let capabilities = null;
|
||||||
|
|
||||||
@@ -154,11 +199,43 @@ const WiFiMode = (function() {
|
|||||||
|
|
||||||
async function checkCapabilities() {
|
async function checkCapabilities() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${CONFIG.apiBase}/capabilities`);
|
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||||
if (!response.ok) throw new Error('Failed to fetch capabilities');
|
let response;
|
||||||
|
|
||||||
capabilities = await response.json();
|
if (isAgentMode) {
|
||||||
console.log('[WiFiMode] Capabilities:', capabilities);
|
// 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();
|
updateCapabilityUI();
|
||||||
populateInterfaceSelect();
|
populateInterfaceSelect();
|
||||||
@@ -282,17 +359,34 @@ const WiFiMode = (function() {
|
|||||||
async function startQuickScan() {
|
async function startQuickScan() {
|
||||||
if (isScanning) return;
|
if (isScanning) return;
|
||||||
|
|
||||||
|
// Check for agent mode conflicts
|
||||||
|
if (!checkAgentConflicts()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[WiFiMode] Starting quick scan...');
|
console.log('[WiFiMode] Starting quick scan...');
|
||||||
setScanning(true, 'quick');
|
setScanning(true, 'quick');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const iface = elements.interfaceSelect?.value || null;
|
const iface = elements.interfaceSelect?.value || null;
|
||||||
|
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||||
|
const agentName = getCurrentAgentName();
|
||||||
|
|
||||||
const response = await fetch(`${CONFIG.apiBase}/scan/quick`, {
|
let response;
|
||||||
method: 'POST',
|
if (isAgentMode) {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
// Route through agent proxy
|
||||||
body: JSON.stringify({ interface: iface }),
|
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) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
@@ -302,20 +396,26 @@ const WiFiMode = (function() {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
console.log('[WiFiMode] Quick scan complete:', result);
|
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
|
// Check for error first
|
||||||
if (result.error) {
|
if (scanResult.error || scanResult.status === 'error') {
|
||||||
console.error('[WiFiMode] Quick scan error from server:', result.error);
|
console.error('[WiFiMode] Quick scan error from server:', scanResult.error || scanResult.message);
|
||||||
showError(result.error);
|
showError(scanResult.error || scanResult.message || 'Quick scan failed');
|
||||||
setScanning(false);
|
setScanning(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle agent response format
|
||||||
|
let accessPoints = scanResult.access_points || scanResult.networks || [];
|
||||||
|
|
||||||
// Check if we got results
|
// Check if we got results
|
||||||
if (!result.access_points || result.access_points.length === 0) {
|
if (accessPoints.length === 0) {
|
||||||
// No error but no results
|
// No error but no results
|
||||||
let msg = 'Quick scan found no networks in range.';
|
let msg = 'Quick scan found no networks in range.';
|
||||||
if (result.warnings && result.warnings.length > 0) {
|
if (scanResult.warnings && scanResult.warnings.length > 0) {
|
||||||
msg += ' Warnings: ' + result.warnings.join('; ');
|
msg += ' Warnings: ' + scanResult.warnings.join('; ');
|
||||||
}
|
}
|
||||||
console.warn('[WiFiMode] ' + msg);
|
console.warn('[WiFiMode] ' + msg);
|
||||||
showError(msg + ' Try Deep Scan with monitor mode.');
|
showError(msg + ' Try Deep Scan with monitor mode.');
|
||||||
@@ -323,13 +423,18 @@ const WiFiMode = (function() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tag results with agent source
|
||||||
|
accessPoints.forEach(ap => {
|
||||||
|
ap._agent = agentName;
|
||||||
|
});
|
||||||
|
|
||||||
// Show any warnings even on success
|
// Show any warnings even on success
|
||||||
if (result.warnings && result.warnings.length > 0) {
|
if (scanResult.warnings && scanResult.warnings.length > 0) {
|
||||||
console.warn('[WiFiMode] Quick scan warnings:', result.warnings);
|
console.warn('[WiFiMode] Quick scan warnings:', scanResult.warnings);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process results
|
// Process results
|
||||||
processQuickScanResult(result);
|
processQuickScanResult({ ...scanResult, access_points: accessPoints });
|
||||||
|
|
||||||
// For quick scan, we're done after one scan
|
// For quick scan, we're done after one scan
|
||||||
// But keep polling if user wants continuous updates
|
// But keep polling if user wants continuous updates
|
||||||
@@ -346,6 +451,11 @@ const WiFiMode = (function() {
|
|||||||
async function startDeepScan() {
|
async function startDeepScan() {
|
||||||
if (isScanning) return;
|
if (isScanning) return;
|
||||||
|
|
||||||
|
// Check for agent mode conflicts
|
||||||
|
if (!checkAgentConflicts()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[WiFiMode] Starting deep scan...');
|
console.log('[WiFiMode] Starting deep scan...');
|
||||||
setScanning(true, 'deep');
|
setScanning(true, 'deep');
|
||||||
|
|
||||||
@@ -353,22 +463,48 @@ const WiFiMode = (function() {
|
|||||||
const iface = elements.interfaceSelect?.value || null;
|
const iface = elements.interfaceSelect?.value || null;
|
||||||
const band = document.getElementById('wifiBand')?.value || 'all';
|
const band = document.getElementById('wifiBand')?.value || 'all';
|
||||||
const channel = document.getElementById('wifiChannel')?.value || null;
|
const channel = document.getElementById('wifiChannel')?.value || null;
|
||||||
|
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||||
|
|
||||||
const response = await fetch(`${CONFIG.apiBase}/scan/start`, {
|
let response;
|
||||||
method: 'POST',
|
if (isAgentMode) {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
// Route through agent proxy
|
||||||
body: JSON.stringify({
|
response = await fetch(`/controller/agents/${currentAgent}/wifi/start`, {
|
||||||
interface: iface,
|
method: 'POST',
|
||||||
band: band === 'abg' ? 'all' : band === 'bg' ? '2.4' : '5',
|
headers: { 'Content-Type': 'application/json' },
|
||||||
channel: channel ? parseInt(channel) : null,
|
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) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
throw new Error(error.error || 'Failed to start deep scan');
|
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
|
// Start SSE stream for real-time updates
|
||||||
startEventStream();
|
startEventStream();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -393,13 +529,17 @@ const WiFiMode = (function() {
|
|||||||
eventSource = null;
|
eventSource = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop deep scan on server
|
// Stop scan on server (local or agent)
|
||||||
if (scanMode === 'deep') {
|
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||||
try {
|
|
||||||
|
try {
|
||||||
|
if (isAgentMode) {
|
||||||
|
await fetch(`/controller/agents/${currentAgent}/wifi/stop`, { method: 'POST' });
|
||||||
|
} else if (scanMode === 'deep') {
|
||||||
await fetch(`${CONFIG.apiBase}/scan/stop`, { method: 'POST' });
|
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);
|
setScanning(false);
|
||||||
@@ -517,8 +657,20 @@ const WiFiMode = (function() {
|
|||||||
eventSource.close();
|
eventSource.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[WiFiMode] Starting event stream...');
|
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||||
eventSource = new EventSource(`${CONFIG.apiBase}/stream`);
|
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 = () => {
|
eventSource.onopen = () => {
|
||||||
console.log('[WiFiMode] Event stream connected');
|
console.log('[WiFiMode] Event stream connected');
|
||||||
@@ -527,7 +679,46 @@ const WiFiMode = (function() {
|
|||||||
eventSource.onmessage = (event) => {
|
eventSource.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data);
|
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) {
|
} catch (error) {
|
||||||
console.debug('[WiFiMode] Event parse error:', 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 hiddenBadge = network.is_hidden ? '<span class="badge badge-hidden">Hidden</span>' : '';
|
||||||
const newBadge = network.is_new ? '<span class="badge badge-new">New</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 `
|
return `
|
||||||
<tr class="wifi-network-row ${network.bssid === selectedNetwork ? 'selected' : ''}"
|
<tr class="wifi-network-row ${network.bssid === selectedNetwork ? 'selected' : ''}"
|
||||||
data-bssid="${escapeHtml(network.bssid)}"
|
data-bssid="${escapeHtml(network.bssid)}"
|
||||||
@@ -762,6 +957,9 @@ const WiFiMode = (function() {
|
|||||||
<span class="security-badge ${securityClass}">${escapeHtml(network.security)}</span>
|
<span class="security-badge ${securityClass}">${escapeHtml(network.security)}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="col-clients">${network.client_count || 0}</td>
|
<td class="col-clients">${network.client_count || 0}</td>
|
||||||
|
<td class="col-agent">
|
||||||
|
<span class="agent-badge ${agentClass}">${escapeHtml(agentName)}</span>
|
||||||
|
</td>
|
||||||
</tr>
|
</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
|
// Public API
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
@@ -1086,12 +1391,19 @@ const WiFiMode = (function() {
|
|||||||
exportData,
|
exportData,
|
||||||
checkCapabilities,
|
checkCapabilities,
|
||||||
|
|
||||||
|
// Agent handling
|
||||||
|
handleAgentChange,
|
||||||
|
clearData,
|
||||||
|
toggleShowAllAgents,
|
||||||
|
refreshInterfaces,
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
getNetworks: () => Array.from(networks.values()),
|
getNetworks: () => Array.from(networks.values()),
|
||||||
getClients: () => Array.from(clients.values()),
|
getClients: () => Array.from(clients.values()),
|
||||||
getProbes: () => [...probeRequests],
|
getProbes: () => [...probeRequests],
|
||||||
isScanning: () => isScanning,
|
isScanning: () => isScanning,
|
||||||
getScanMode: () => scanMode,
|
getScanMode: () => scanMode,
|
||||||
|
isShowAllAgents: () => showAllAgentsMode,
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
onNetworkUpdate: (cb) => { onNetworkUpdate = cb; },
|
onNetworkUpdate: (cb) => { onNetworkUpdate = cb; },
|
||||||
|
|||||||
+885
-105
File diff suppressed because it is too large
Load Diff
+575
-22
@@ -21,6 +21,16 @@
|
|||||||
<span>// INTERCEPT - AIS Tracking</span>
|
<span>// INTERCEPT - AIS Tracking</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-bar">
|
<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>
|
<a href="/" class="back-link">Main Dashboard</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -173,6 +183,7 @@
|
|||||||
let markers = {};
|
let markers = {};
|
||||||
let selectedMmsi = null;
|
let selectedMmsi = null;
|
||||||
let eventSource = null;
|
let eventSource = null;
|
||||||
|
let aisPollTimer = null; // Polling fallback for agent mode
|
||||||
let isTracking = false;
|
let isTracking = false;
|
||||||
|
|
||||||
// DSC State
|
// DSC State
|
||||||
@@ -181,6 +192,8 @@
|
|||||||
let dscMessages = {};
|
let dscMessages = {};
|
||||||
let dscMarkers = {};
|
let dscMarkers = {};
|
||||||
let dscAlertCounts = { distress: 0, urgency: 0 };
|
let dscAlertCounts = { distress: 0, urgency: 0 };
|
||||||
|
let dscCurrentAgent = null;
|
||||||
|
let dscPollTimer = null;
|
||||||
let showTrails = false;
|
let showTrails = false;
|
||||||
let vesselTrails = {};
|
let vesselTrails = {};
|
||||||
let trailLines = {};
|
let trailLines = {};
|
||||||
@@ -490,6 +503,40 @@
|
|||||||
const device = document.getElementById('aisDeviceSelect').value;
|
const device = document.getElementById('aisDeviceSelect').value;
|
||||||
const gain = document.getElementById('aisGain').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', {
|
fetch('/ais/start', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -513,7 +560,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stopTracking() {
|
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(r => r.json())
|
||||||
.then(() => {
|
.then(() => {
|
||||||
isTracking = false;
|
isTracking = false;
|
||||||
@@ -527,18 +579,107 @@
|
|||||||
eventSource.close();
|
eventSource.close();
|
||||||
eventSource = null;
|
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() {
|
function startSSE() {
|
||||||
if (eventSource) eventSource.close();
|
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) {
|
eventSource.onmessage = function(e) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(e.data);
|
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) {}
|
} catch (err) {}
|
||||||
};
|
};
|
||||||
@@ -731,12 +872,13 @@
|
|||||||
container.innerHTML = vesselArray.map(v => {
|
container.innerHTML = vesselArray.map(v => {
|
||||||
const iconSvg = getShipIconSvg(v.ship_type, 20);
|
const iconSvg = getShipIconSvg(v.ship_type, 20);
|
||||||
const category = getShipCategory(v.ship_type);
|
const category = getShipCategory(v.ship_type);
|
||||||
|
const agentBadge = v._agent ? `<span class="agent-badge">${v._agent}</span>` : '';
|
||||||
return `
|
return `
|
||||||
<div class="vessel-item ${v.mmsi === selectedMmsi ? 'selected' : ''}"
|
<div class="vessel-item ${v.mmsi === selectedMmsi ? 'selected' : ''}"
|
||||||
data-mmsi="${v.mmsi}" onclick="selectVessel('${v.mmsi}')">
|
data-mmsi="${v.mmsi}" onclick="selectVessel('${v.mmsi}')">
|
||||||
<div class="vessel-item-icon">${iconSvg}</div>
|
<div class="vessel-item-icon">${iconSvg}</div>
|
||||||
<div class="vessel-item-info">
|
<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 class="vessel-item-type">${category} | ${v.mmsi}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="vessel-item-speed">${v.speed ? v.speed + ' kt' : '-'}</div>
|
<div class="vessel-item-speed">${v.speed ? v.speed + ' kt' : '-'}</div>
|
||||||
@@ -881,33 +1023,51 @@
|
|||||||
const device = document.getElementById('dscDeviceSelect').value;
|
const device = document.getElementById('dscDeviceSelect').value;
|
||||||
const gain = document.getElementById('dscGain').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',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ device, gain })
|
body: JSON.stringify({ device, gain })
|
||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.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;
|
isDscTracking = true;
|
||||||
document.getElementById('dscStartBtn').textContent = 'STOP DSC';
|
document.getElementById('dscStartBtn').textContent = 'STOP DSC';
|
||||||
document.getElementById('dscStartBtn').classList.add('active');
|
document.getElementById('dscStartBtn').classList.add('active');
|
||||||
document.getElementById('dscIndicator').classList.add('active');
|
document.getElementById('dscIndicator').classList.add('active');
|
||||||
startDscSSE();
|
startDscSSE(isAgentMode);
|
||||||
} else if (data.error_type === 'DEVICE_BUSY') {
|
} else if (scanResult.error_type === 'DEVICE_BUSY') {
|
||||||
alert('SDR device is busy.\n\n' + data.suggestion);
|
alert('SDR device is busy.\n\n' + (scanResult.suggestion || ''));
|
||||||
} else {
|
} else {
|
||||||
alert(data.message || 'Failed to start DSC');
|
alert(scanResult.message || scanResult.error || 'Failed to start DSC');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => alert('Error: ' + err.message));
|
.catch(err => alert('Error: ' + err.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopDscTracking() {
|
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(r => r.json())
|
||||||
.then(() => {
|
.then(() => {
|
||||||
isDscTracking = false;
|
isDscTracking = false;
|
||||||
|
dscCurrentAgent = null;
|
||||||
document.getElementById('dscStartBtn').textContent = 'START DSC';
|
document.getElementById('dscStartBtn').textContent = 'START DSC';
|
||||||
document.getElementById('dscStartBtn').classList.remove('active');
|
document.getElementById('dscStartBtn').classList.remove('active');
|
||||||
document.getElementById('dscIndicator').classList.remove('active');
|
document.getElementById('dscIndicator').classList.remove('active');
|
||||||
@@ -915,23 +1075,50 @@
|
|||||||
dscEventSource.close();
|
dscEventSource.close();
|
||||||
dscEventSource = null;
|
dscEventSource = null;
|
||||||
}
|
}
|
||||||
|
// Clear polling timer
|
||||||
|
if (dscPollTimer) {
|
||||||
|
clearInterval(dscPollTimer);
|
||||||
|
dscPollTimer = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function startDscSSE() {
|
function startDscSSE(isAgentMode = false) {
|
||||||
if (dscEventSource) dscEventSource.close();
|
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) {
|
dscEventSource.onmessage = function(e) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(e.data);
|
const data = JSON.parse(e.data);
|
||||||
if (data.type === 'dsc_message') {
|
|
||||||
handleDscMessage(data);
|
if (isAgentMode) {
|
||||||
} else if (data.type === 'error') {
|
// Handle multi-agent stream format
|
||||||
console.error('DSC error:', data.error);
|
if (data.scan_type === 'dsc' && data.payload) {
|
||||||
if (data.error_type === 'DEVICE_BUSY') {
|
const payload = data.payload;
|
||||||
alert('DSC: Device became busy. ' + (data.suggestion || ''));
|
if (payload.type === 'dsc_message') {
|
||||||
stopDscTracking();
|
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) {}
|
} catch (err) {}
|
||||||
@@ -939,9 +1126,56 @@
|
|||||||
|
|
||||||
dscEventSource.onerror = function() {
|
dscEventSource.onerror = function() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (isDscTracking) startDscSSE();
|
if (isDscTracking) startDscSSE(isAgentMode);
|
||||||
}, 2000);
|
}, 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) {
|
function handleDscMessage(data) {
|
||||||
@@ -1100,5 +1334,324 @@
|
|||||||
// Initialize
|
// Initialize
|
||||||
document.addEventListener('DOMContentLoaded', initMap);
|
document.addEventListener('DOMContentLoaded', initMap);
|
||||||
</script>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+519
-94
@@ -574,11 +574,12 @@
|
|||||||
<th class="sortable" data-sort="rssi">Signal</th>
|
<th class="sortable" data-sort="rssi">Signal</th>
|
||||||
<th class="sortable" data-sort="security">Security</th>
|
<th class="sortable" data-sort="security">Security</th>
|
||||||
<th class="sortable" data-sort="clients">Clients</th>
|
<th class="sortable" data-sort="clients">Clients</th>
|
||||||
|
<th class="col-agent sortable" data-sort="agent">Source</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="wifiNetworkTableBody">
|
<tbody id="wifiNetworkTableBody">
|
||||||
<tr class="wifi-network-placeholder">
|
<tr class="wifi-network-placeholder">
|
||||||
<td colspan="6">
|
<td colspan="7">
|
||||||
<div class="placeholder-text">Start scanning to discover networks</div>
|
<div class="placeholder-text">Start scanning to discover networks</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -2306,6 +2307,11 @@
|
|||||||
|
|
||||||
// Check if using remote agent
|
// Check if using remote agent
|
||||||
if (typeof currentAgent !== 'undefined' && currentAgent !== 'local') {
|
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
|
// Route through agent proxy
|
||||||
const config = {
|
const config = {
|
||||||
frequency: freq,
|
frequency: freq,
|
||||||
@@ -2320,12 +2326,14 @@
|
|||||||
body: JSON.stringify(config)
|
body: JSON.stringify(config)
|
||||||
}).then(r => r.json())
|
}).then(r => r.json())
|
||||||
.then(data => {
|
.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);
|
setSensorRunning(true);
|
||||||
startAgentSensorStream();
|
startAgentSensorStream();
|
||||||
showInfo(`Sensor started on remote agent`);
|
showInfo(`Sensor started on remote agent`);
|
||||||
} else {
|
} else {
|
||||||
alert('Error: ' + (data.message || 'Failed to start sensor on agent'));
|
alert('Error: ' + (scanResult.message || 'Failed to start sensor on agent'));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
@@ -2612,6 +2620,10 @@
|
|||||||
document.getElementById('rtlamrFrequency').value = freq;
|
document.getElementById('rtlamrFrequency').value = freq;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RTLAMR mode polling timer for agent mode
|
||||||
|
let rtlamrPollTimer = null;
|
||||||
|
let rtlamrCurrentAgent = null;
|
||||||
|
|
||||||
function startRtlamrDecoding() {
|
function startRtlamrDecoding() {
|
||||||
const freq = document.getElementById('rtlamrFrequency').value;
|
const freq = document.getElementById('rtlamrFrequency').value;
|
||||||
const gain = document.getElementById('rtlamrGain').value;
|
const gain = document.getElementById('rtlamrGain').value;
|
||||||
@@ -2621,8 +2633,12 @@
|
|||||||
const filterid = document.getElementById('rtlamrFilterId').value;
|
const filterid = document.getElementById('rtlamrFilterId').value;
|
||||||
const unique = document.getElementById('rtlamrUnique').checked;
|
const unique = document.getElementById('rtlamrUnique').checked;
|
||||||
|
|
||||||
// Check if device is available
|
// Check if using agent mode
|
||||||
if (!checkDeviceAvailability('rtlamr')) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2637,16 +2653,26 @@
|
|||||||
format: 'json'
|
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',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(config)
|
body: JSON.stringify(config)
|
||||||
}).then(r => r.json())
|
}).then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.status === 'started') {
|
// Handle controller proxy response format
|
||||||
reserveDevice(parseInt(device), 'rtlamr');
|
const scanResult = isAgentMode && data.result ? data.result : data;
|
||||||
|
|
||||||
|
if (scanResult.status === 'started' || scanResult.status === 'success') {
|
||||||
|
if (!isAgentMode) {
|
||||||
|
reserveDevice(parseInt(device), 'rtlamr');
|
||||||
|
}
|
||||||
setRtlamrRunning(true);
|
setRtlamrRunning(true);
|
||||||
startRtlamrStream();
|
startRtlamrStream(isAgentMode);
|
||||||
|
|
||||||
// Initialize meter filter bar (reuse sensor filter bar since same structure)
|
// Initialize meter filter bar (reuse sensor filter bar since same structure)
|
||||||
const filterContainer = document.getElementById('filterBarContainer');
|
const filterContainer = document.getElementById('filterBarContainer');
|
||||||
@@ -2667,21 +2693,34 @@
|
|||||||
// Clear existing output
|
// Clear existing output
|
||||||
output.innerHTML = '<div class="placeholder signal-empty-state" style="display: none;"></div>';
|
output.innerHTML = '<div class="placeholder signal-empty-state" style="display: none;"></div>';
|
||||||
} else {
|
} else {
|
||||||
alert('Error: ' + data.message);
|
alert('Error: ' + (scanResult.message || scanResult.error || 'Failed to start'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopRtlamrDecoding() {
|
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(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
releaseDevice('rtlamr');
|
if (!isAgentMode) {
|
||||||
|
releaseDevice('rtlamr');
|
||||||
|
}
|
||||||
|
rtlamrCurrentAgent = null;
|
||||||
setRtlamrRunning(false);
|
setRtlamrRunning(false);
|
||||||
if (eventSource) {
|
if (eventSource) {
|
||||||
eventSource.close();
|
eventSource.close();
|
||||||
eventSource = null;
|
eventSource = null;
|
||||||
}
|
}
|
||||||
|
// Clear polling timer
|
||||||
|
if (rtlamrPollTimer) {
|
||||||
|
clearInterval(rtlamrPollTimer);
|
||||||
|
rtlamrPollTimer = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2701,12 +2740,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startRtlamrStream() {
|
function startRtlamrStream(isAgentMode = false) {
|
||||||
if (eventSource) {
|
if (eventSource) {
|
||||||
eventSource.close();
|
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 () {
|
eventSource.onopen = function () {
|
||||||
showInfo('RTLAMR stream connected...');
|
showInfo('RTLAMR stream connected...');
|
||||||
@@ -2714,20 +2755,86 @@
|
|||||||
|
|
||||||
eventSource.onmessage = function (e) {
|
eventSource.onmessage = function (e) {
|
||||||
const data = JSON.parse(e.data);
|
const data = JSON.parse(e.data);
|
||||||
if (data.type === 'rtlamr') {
|
|
||||||
addRtlamrReading(data);
|
if (isAgentMode) {
|
||||||
} else if (data.type === 'status') {
|
// Handle multi-agent stream format
|
||||||
if (data.text === 'stopped') {
|
if (data.scan_type === 'rtlamr' && data.payload) {
|
||||||
setRtlamrRunning(false);
|
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) {
|
eventSource.onerror = function (e) {
|
||||||
console.error('RTLAMR stream error');
|
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) {
|
function addRtlamrReading(data) {
|
||||||
@@ -3196,6 +3303,9 @@
|
|||||||
return protocols;
|
return protocols;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pager mode polling timer for agent mode
|
||||||
|
let pagerPollTimer = null;
|
||||||
|
|
||||||
function startDecoding() {
|
function startDecoding() {
|
||||||
const freq = document.getElementById('frequency').value;
|
const freq = document.getElementById('frequency').value;
|
||||||
const gain = document.getElementById('gain').value;
|
const gain = document.getElementById('gain').value;
|
||||||
@@ -3209,13 +3319,16 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if device is available
|
// Check if using agent mode
|
||||||
if (!checkDeviceAvailability('pager')) {
|
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||||
|
|
||||||
|
// Check if device is available (only for local mode)
|
||||||
|
if (!isAgentMode && !checkDeviceAvailability('pager')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for remote SDR
|
// Check for remote SDR (only for local mode)
|
||||||
const remoteConfig = getRemoteSDRConfig();
|
const remoteConfig = isAgentMode ? null : getRemoteSDRConfig();
|
||||||
if (remoteConfig === false) return; // Validation failed
|
if (remoteConfig === false) return; // Validation failed
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
@@ -3229,22 +3342,32 @@
|
|||||||
bias_t: getBiasTEnabled()
|
bias_t: getBiasTEnabled()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add rtl_tcp params if using remote SDR
|
// Add rtl_tcp params if using remote SDR (local mode only)
|
||||||
if (remoteConfig) {
|
if (remoteConfig) {
|
||||||
config.rtl_tcp_host = remoteConfig.host;
|
config.rtl_tcp_host = remoteConfig.host;
|
||||||
config.rtl_tcp_port = remoteConfig.port;
|
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',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(config)
|
body: JSON.stringify(config)
|
||||||
}).then(r => r.json())
|
}).then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.status === 'started') {
|
// Handle controller proxy response format (agent response is nested in 'result')
|
||||||
reserveDevice(parseInt(device), 'pager');
|
const scanResult = isAgentMode && data.result ? data.result : data;
|
||||||
|
|
||||||
|
if (scanResult.status === 'started' || scanResult.status === 'success') {
|
||||||
|
if (!isAgentMode) {
|
||||||
|
reserveDevice(parseInt(device), 'pager');
|
||||||
|
}
|
||||||
setRunning(true);
|
setRunning(true);
|
||||||
startStream();
|
startStream(isAgentMode);
|
||||||
|
|
||||||
// Initialize filter bar
|
// Initialize filter bar
|
||||||
const filterContainer = document.getElementById('filterBarContainer');
|
const filterContainer = document.getElementById('filterBarContainer');
|
||||||
@@ -3260,24 +3383,37 @@
|
|||||||
// Clear address history for fresh session
|
// Clear address history for fresh session
|
||||||
SignalCards.clearAddressHistory('pager');
|
SignalCards.clearAddressHistory('pager');
|
||||||
} else {
|
} else {
|
||||||
alert('Error: ' + data.message);
|
alert('Error: ' + (scanResult.message || scanResult.error || 'Failed to start pager decoding'));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('Start error:', err);
|
console.error('Start error:', err);
|
||||||
|
alert('Error starting pager decoding: ' + err.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopDecoding() {
|
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(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
releaseDevice('pager');
|
if (!isAgentMode) {
|
||||||
|
releaseDevice('pager');
|
||||||
|
}
|
||||||
setRunning(false);
|
setRunning(false);
|
||||||
if (eventSource) {
|
if (eventSource) {
|
||||||
eventSource.close();
|
eventSource.close();
|
||||||
eventSource = null;
|
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';
|
document.getElementById('stopBtn').style.display = running ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
function startStream() {
|
function startStream(isAgentMode = false) {
|
||||||
if (eventSource) {
|
if (eventSource) {
|
||||||
eventSource.close();
|
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 () {
|
eventSource.onopen = function () {
|
||||||
showInfo('Stream connected...');
|
showInfo('Stream connected...');
|
||||||
@@ -3356,24 +3494,101 @@
|
|||||||
eventSource.onmessage = function (e) {
|
eventSource.onmessage = function (e) {
|
||||||
const data = JSON.parse(e.data);
|
const data = JSON.parse(e.data);
|
||||||
|
|
||||||
if (data.type === 'message') {
|
// Handle multi-agent stream format
|
||||||
addMessage(data);
|
if (isAgentMode) {
|
||||||
} else if (data.type === 'status') {
|
// Multi-agent stream tags data with scan_type and agent_name
|
||||||
if (data.text === 'stopped') {
|
if (data.scan_type === 'pager' && data.payload) {
|
||||||
setRunning(false);
|
const payload = data.payload;
|
||||||
} else if (data.text === 'started') {
|
if (payload.type === 'message') {
|
||||||
showInfo('Decoder started, waiting for signals...');
|
// 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) {
|
eventSource.onerror = function (e) {
|
||||||
checkStatus();
|
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) {
|
function addMessage(msg) {
|
||||||
@@ -7084,12 +7299,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// APRS mode polling timer for agent mode
|
||||||
|
let aprsPollTimer = null;
|
||||||
|
let aprsCurrentAgent = null;
|
||||||
|
|
||||||
function startAprs() {
|
function startAprs() {
|
||||||
// Get values from function bar controls
|
// Get values from function bar controls
|
||||||
const region = document.getElementById('aprsStripRegion').value;
|
const region = document.getElementById('aprsStripRegion').value;
|
||||||
const device = getSelectedDevice();
|
const device = getSelectedDevice();
|
||||||
const gain = document.getElementById('aprsStripGain').value;
|
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
|
// Build request body
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
region,
|
region,
|
||||||
@@ -7107,14 +7330,22 @@
|
|||||||
requestBody.frequency = customFreq;
|
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',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(requestBody)
|
body: JSON.stringify(requestBody)
|
||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.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;
|
isAprsRunning = true;
|
||||||
aprsPacketCount = 0;
|
aprsPacketCount = 0;
|
||||||
aprsStationCount = 0;
|
aprsStationCount = 0;
|
||||||
@@ -7138,7 +7369,7 @@
|
|||||||
document.getElementById('aprsMapStatus').textContent = 'TRACKING';
|
document.getElementById('aprsMapStatus').textContent = 'TRACKING';
|
||||||
document.getElementById('aprsMapStatus').style.color = 'var(--accent-green)';
|
document.getElementById('aprsMapStatus').style.color = 'var(--accent-green)';
|
||||||
// Update function bar status
|
// Update function bar status
|
||||||
updateAprsStatus('listening', data.frequency);
|
updateAprsStatus('listening', scanResult.frequency);
|
||||||
// Reset function bar stats
|
// Reset function bar stats
|
||||||
document.getElementById('aprsStripStations').textContent = '0';
|
document.getElementById('aprsStripStations').textContent = '0';
|
||||||
document.getElementById('aprsStripPackets').textContent = '0';
|
document.getElementById('aprsStripPackets').textContent = '0';
|
||||||
@@ -7149,9 +7380,9 @@
|
|||||||
const customFreqInput = document.getElementById('aprsStripCustomFreq');
|
const customFreqInput = document.getElementById('aprsStripCustomFreq');
|
||||||
if (customFreqInput) customFreqInput.disabled = true;
|
if (customFreqInput) customFreqInput.disabled = true;
|
||||||
startAprsMeterCheck();
|
startAprsMeterCheck();
|
||||||
startAprsStream();
|
startAprsStream(isAgentMode);
|
||||||
} else {
|
} else {
|
||||||
alert('APRS Error: ' + data.message);
|
alert('APRS Error: ' + (scanResult.message || scanResult.error || 'Failed to start'));
|
||||||
updateAprsStatus('error');
|
updateAprsStatus('error');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -7162,10 +7393,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stopAprs() {
|
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(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
isAprsRunning = false;
|
isAprsRunning = false;
|
||||||
|
aprsCurrentAgent = null;
|
||||||
// Update function bar buttons
|
// Update function bar buttons
|
||||||
document.getElementById('aprsStripStartBtn').style.display = 'inline-block';
|
document.getElementById('aprsStripStartBtn').style.display = 'inline-block';
|
||||||
document.getElementById('aprsStripStopBtn').style.display = 'none';
|
document.getElementById('aprsStripStopBtn').style.display = 'none';
|
||||||
@@ -7192,29 +7429,60 @@
|
|||||||
aprsEventSource.close();
|
aprsEventSource.close();
|
||||||
aprsEventSource = null;
|
aprsEventSource = null;
|
||||||
}
|
}
|
||||||
|
// Clear polling timer
|
||||||
|
if (aprsPollTimer) {
|
||||||
|
clearInterval(aprsPollTimer);
|
||||||
|
aprsPollTimer = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function startAprsStream() {
|
function startAprsStream(isAgentMode = false) {
|
||||||
if (aprsEventSource) aprsEventSource.close();
|
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) {
|
aprsEventSource.onmessage = function (e) {
|
||||||
const data = JSON.parse(e.data);
|
const data = JSON.parse(e.data);
|
||||||
if (data.type === 'aprs') {
|
|
||||||
aprsPacketCount++;
|
if (isAgentMode) {
|
||||||
// Update map footer and function bar
|
// Handle multi-agent stream format
|
||||||
document.getElementById('aprsPacketCount').textContent = aprsPacketCount;
|
if (data.scan_type === 'aprs' && data.payload) {
|
||||||
document.getElementById('aprsStripPackets').textContent = aprsPacketCount;
|
const payload = data.payload;
|
||||||
// Switch to tracking state on first packet
|
if (payload.type === 'aprs') {
|
||||||
const dot = document.getElementById('aprsStripDot');
|
aprsPacketCount++;
|
||||||
if (dot && !dot.classList.contains('tracking')) {
|
document.getElementById('aprsPacketCount').textContent = aprsPacketCount;
|
||||||
updateAprsStatus('tracking');
|
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');
|
console.error('APRS stream error');
|
||||||
updateAprsStatus('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
|
// Signal Meter Functions
|
||||||
@@ -7710,6 +8033,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Satellite mode agent state
|
||||||
|
let satelliteCurrentAgent = null;
|
||||||
|
|
||||||
function calculatePasses() {
|
function calculatePasses() {
|
||||||
const lat = parseFloat(document.getElementById('obsLat').value);
|
const lat = parseFloat(document.getElementById('obsLat').value);
|
||||||
const lon = parseFloat(document.getElementById('obsLon').value);
|
const lon = parseFloat(document.getElementById('obsLon').value);
|
||||||
@@ -7723,18 +8049,30 @@
|
|||||||
return;
|
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',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ lat, lon, hours, minEl, satellites })
|
body: JSON.stringify({ lat, lon, hours, minEl, satellites })
|
||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.status === 'success') {
|
// Handle controller proxy response format
|
||||||
satellitePasses = data.passes;
|
const result = isAgentMode && data.result ? data.result : data;
|
||||||
|
|
||||||
|
if (result.status === 'success') {
|
||||||
|
satellitePasses = result.passes;
|
||||||
renderPassList();
|
renderPassList();
|
||||||
document.getElementById('passCount').textContent = data.passes.length;
|
document.getElementById('passCount').textContent = result.passes.length;
|
||||||
if (data.passes.length > 0) {
|
if (result.passes.length > 0) {
|
||||||
selectPass(0);
|
selectPass(0);
|
||||||
document.getElementById('satelliteCountdown').style.display = 'block';
|
document.getElementById('satelliteCountdown').style.display = 'block';
|
||||||
updateSatelliteCountdown();
|
updateSatelliteCountdown();
|
||||||
@@ -7743,7 +8081,7 @@
|
|||||||
document.getElementById('satelliteCountdown').style.display = 'none';
|
document.getElementById('satelliteCountdown').style.display = 'none';
|
||||||
}
|
}
|
||||||
} else {
|
} 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 lat = parseFloat(document.getElementById('obsLat').value);
|
||||||
const lon = parseFloat(document.getElementById('obsLon').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',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ lat, lon, satellites, includeTrack: true })
|
body: JSON.stringify({ lat, lon, satellites, includeTrack: true })
|
||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.status === 'success' && data.positions) {
|
// Handle controller proxy response format
|
||||||
updateRealTimeIndicators(data.positions);
|
const result = isAgentMode && data.result ? data.result : data;
|
||||||
|
|
||||||
|
if (result.status === 'success' && result.positions) {
|
||||||
|
updateRealTimeIndicators(result.positions);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -8350,10 +8697,33 @@
|
|||||||
|
|
||||||
async function refreshTscmDevices() {
|
async function refreshTscmDevices() {
|
||||||
// Fetch available interfaces for TSCM scanning
|
// Fetch available interfaces for TSCM scanning
|
||||||
|
// Check if agent is selected and route accordingly
|
||||||
try {
|
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 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
|
// Populate WiFi interfaces
|
||||||
const wifiSelect = document.getElementById('tscmWifiInterface');
|
const wifiSelect = document.getElementById('tscmWifiInterface');
|
||||||
@@ -8370,7 +8740,11 @@
|
|||||||
wifiSelect.value = devices.wifi_interfaces[0].name;
|
wifiSelect.value = devices.wifi_interfaces[0].name;
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
// Populate Bluetooth adapters
|
||||||
@@ -8388,7 +8762,11 @@
|
|||||||
btSelect.value = devices.bt_adapters[0].name;
|
btSelect.value = devices.bt_adapters[0].name;
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
// Populate SDR devices
|
||||||
@@ -8397,16 +8775,20 @@
|
|||||||
if (devices.sdr_devices && devices.sdr_devices.length > 0) {
|
if (devices.sdr_devices && devices.sdr_devices.length > 0) {
|
||||||
devices.sdr_devices.forEach(dev => {
|
devices.sdr_devices.forEach(dev => {
|
||||||
const opt = document.createElement('option');
|
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';
|
opt.textContent = dev.display_name || dev.name || 'SDR Device';
|
||||||
sdrSelect.appendChild(opt);
|
sdrSelect.appendChild(opt);
|
||||||
});
|
});
|
||||||
// Auto-select first SDR if available
|
// Auto-select first SDR if available
|
||||||
if (devices.sdr_devices.length > 0) {
|
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 {
|
} 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)
|
// Show warnings (e.g., not running as root)
|
||||||
@@ -8465,8 +8847,23 @@
|
|||||||
document.getElementById('tscmDeviceWarnings').style.display = 'none';
|
document.getElementById('tscmDeviceWarnings').style.display = 'none';
|
||||||
document.getElementById('tscmDeviceWarnings').innerHTML = '';
|
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 {
|
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',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -8483,7 +8880,9 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
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;
|
isTscmRunning = true;
|
||||||
tscmSweepStartTime = new Date();
|
tscmSweepStartTime = new Date();
|
||||||
tscmSweepEndTime = null;
|
tscmSweepEndTime = null;
|
||||||
@@ -8496,16 +8895,16 @@
|
|||||||
document.getElementById('tscmReportBtn').style.display = 'none';
|
document.getElementById('tscmReportBtn').style.display = 'none';
|
||||||
|
|
||||||
// Show warnings if any devices unavailable
|
// 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');
|
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>`
|
`<div style="color: #ff9933; font-size: 10px; margin-bottom: 2px;">⚠ ${w}</div>`
|
||||||
).join('');
|
).join('');
|
||||||
warningsDiv.style.display = 'block';
|
warningsDiv.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update device indicators
|
// Update device indicators
|
||||||
updateTscmDeviceIndicators(data.devices);
|
updateTscmDeviceIndicators(scanResult.devices);
|
||||||
|
|
||||||
// Reset displays
|
// Reset displays
|
||||||
tscmThreats = [];
|
tscmThreats = [];
|
||||||
@@ -8519,9 +8918,9 @@
|
|||||||
startTscmStream();
|
startTscmStream();
|
||||||
} else {
|
} else {
|
||||||
// Show error with details
|
// Show error with details
|
||||||
let errorMsg = data.message || 'Failed to start sweep';
|
let errorMsg = scanResult.message || 'Failed to start sweep';
|
||||||
if (data.details && data.details.length > 0) {
|
if (scanResult.details && scanResult.details.length > 0) {
|
||||||
errorMsg += '\n\n' + data.details.join('\n');
|
errorMsg += '\n\n' + scanResult.details.join('\n');
|
||||||
}
|
}
|
||||||
alert(errorMsg);
|
alert(errorMsg);
|
||||||
}
|
}
|
||||||
@@ -8552,7 +8951,12 @@
|
|||||||
|
|
||||||
async function stopTscmSweep() {
|
async function stopTscmSweep() {
|
||||||
try {
|
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) {
|
} catch (e) {
|
||||||
console.error('Error stopping sweep:', e);
|
console.error('Error stopping sweep:', e);
|
||||||
}
|
}
|
||||||
@@ -9111,12 +9515,33 @@
|
|||||||
tscmEventSource.close();
|
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) {
|
tscmEventSource.onmessage = function (event) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data);
|
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) {
|
} catch (e) {
|
||||||
console.error('TSCM SSE parse error:', e);
|
console.error('TSCM SSE parse error:', e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,14 @@
|
|||||||
<!-- Populated by JavaScript with capability warnings -->
|
<!-- Populated by JavaScript with capability warnings -->
|
||||||
</div>
|
</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">
|
<div class="section">
|
||||||
<h3>Scanner Configuration</h3>
|
<h3>Scanner Configuration</h3>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@@ -11,6 +11,13 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="wifiCapabilityStatus" class="info-text" style="margin-top: 8px; font-size: 10px;"></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>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
|
|||||||
Reference in New Issue
Block a user