mirror of
https://github.com/smittix/intercept.git
synced 2026-06-18 02:19:46 -07:00
Add distributed agent architecture for multi-node signal intelligence
Features: - Standalone agent server (intercept_agent.py) for remote sensor nodes - Controller API blueprint for agent management and data aggregation - Push mechanism for agents to send data to controller - Pull mechanism for controller to proxy requests to agents - Multi-agent SSE stream for combined data view - Agent management page at /controller/manage - Agent selector dropdown in main UI - GPS integration for location tagging - API key authentication for secure agent communication - Integration with Intercept's dependency checking system New files: - intercept_agent.py: Remote agent HTTP server - intercept_agent.cfg: Agent configuration template - routes/controller.py: Controller API endpoints - utils/agent_client.py: HTTP client for agents - utils/trilateration.py: Multi-agent position calculation - static/js/core/agents.js: Frontend agent management - templates/agents.html: Agent management page - docs/DISTRIBUTED_AGENTS.md: System documentation Modified: - app.py: Register controller blueprint - utils/database.py: Add agents and push_payloads tables - templates/index.html: Add agent selector section
This commit is contained in:
@@ -0,0 +1,555 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>iNTERCEPT // Remote Agents</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/agents.css') }}">
|
||||
<style>
|
||||
.agents-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.agents-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.agents-header h1 {
|
||||
font-size: 24px;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.agents-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.agent-card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.agent-card:hover {
|
||||
border-color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.agent-card.offline {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.agent-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.agent-name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.agent-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.agent-status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.agent-status-dot.online {
|
||||
background: var(--accent-green);
|
||||
box-shadow: 0 0 6px var(--accent-green);
|
||||
}
|
||||
|
||||
.agent-status-dot.offline {
|
||||
background: var(--accent-red);
|
||||
}
|
||||
|
||||
.agent-status-dot.unknown {
|
||||
background: #666;
|
||||
}
|
||||
|
||||
.agent-url {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.agent-capabilities {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.capability-badge {
|
||||
font-size: 10px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
background: rgba(0, 212, 255, 0.1);
|
||||
color: var(--accent-cyan);
|
||||
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||
}
|
||||
|
||||
.capability-badge.disabled {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #666;
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
.agent-meta {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.agent-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.agent-btn {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.agent-btn:hover {
|
||||
border-color: var(--accent-cyan);
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.agent-btn.danger:hover {
|
||||
border-color: var(--accent-red);
|
||||
color: var(--accent-red);
|
||||
}
|
||||
|
||||
.agent-btn.primary {
|
||||
background: var(--accent-cyan);
|
||||
color: #000;
|
||||
border-color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.agent-btn.primary:hover {
|
||||
background: #00b8d9;
|
||||
}
|
||||
|
||||
/* Add Agent Form */
|
||||
.add-agent-section {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.add-agent-section h2 {
|
||||
font-size: 16px;
|
||||
margin-bottom: 15px;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
padding: 10px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
color: var(--text-primary);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 15px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Back button */
|
||||
.back-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--accent-cyan);
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Toast notifications */
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
padding: 12px 20px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
z-index: 1000;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
.toast.success {
|
||||
border-color: var(--accent-green);
|
||||
}
|
||||
|
||||
.toast.error {
|
||||
border-color: var(--accent-red);
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header style="padding: 15px 20px;">
|
||||
<div class="logo" style="display: inline-block; vertical-align: middle;">
|
||||
<svg width="40" height="40" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 30 Q5 50, 15 70" stroke="#00d4ff" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.5"/>
|
||||
<path d="M22 35 Q14 50, 22 65" stroke="#00d4ff" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.7"/>
|
||||
<path d="M29 40 Q23 50, 29 60" stroke="#00d4ff" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
<path d="M85 30 Q95 50, 85 70" stroke="#00d4ff" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.5"/>
|
||||
<path d="M78 35 Q86 50, 78 65" stroke="#00d4ff" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.7"/>
|
||||
<path d="M71 40 Q77 50, 71 60" stroke="#00d4ff" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
<circle cx="50" cy="22" r="6" fill="#00ff88"/>
|
||||
<rect x="44" y="35" width="12" height="45" rx="2" fill="#00d4ff"/>
|
||||
<rect x="38" y="35" width="24" height="4" rx="1" fill="#00d4ff"/>
|
||||
<rect x="38" y="76" width="24" height="4" rx="1" fill="#00d4ff"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 style="display: inline-block; vertical-align: middle; margin-left: 10px;">
|
||||
iNTERCEPT <span class="tagline">// Remote Agents</span>
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<div class="agents-container">
|
||||
<a href="/" class="back-link">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
Back to Dashboard
|
||||
</a>
|
||||
|
||||
<div class="agents-header">
|
||||
<h1>Remote Agents</h1>
|
||||
<button class="agent-btn primary" onclick="refreshAllAgents()">
|
||||
Refresh All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Add Agent Form -->
|
||||
<div class="add-agent-section">
|
||||
<h2>Register New Agent</h2>
|
||||
<form id="addAgentForm" onsubmit="return addAgent(event)">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="agentName">Agent Name *</label>
|
||||
<input type="text" id="agentName" placeholder="sensor-node-1" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="agentUrl">Base URL *</label>
|
||||
<input type="url" id="agentUrl" placeholder="http://192.168.1.50:8020" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="agentApiKey">API Key (optional)</label>
|
||||
<input type="text" id="agentApiKey" placeholder="shared-secret">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="agentDescription">Description (optional)</label>
|
||||
<input type="text" id="agentDescription" placeholder="Rooftop sensor node">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="agent-btn primary" style="width: auto; padding: 10px 24px;">
|
||||
Register Agent
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Agents Grid -->
|
||||
<div id="agentsGrid" class="agents-grid">
|
||||
<!-- Populated by JavaScript -->
|
||||
</div>
|
||||
|
||||
<div id="emptyState" class="empty-state" style="display: none;">
|
||||
<div class="empty-state-icon">
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<rect x="2" y="2" width="20" height="8" rx="2"/>
|
||||
<rect x="2" y="14" width="20" height="8" rx="2"/>
|
||||
<line x1="6" y1="6" x2="6.01" y2="6"/>
|
||||
<line x1="6" y1="18" x2="6.01" y2="18"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>No Remote Agents</h3>
|
||||
<p>Register your first remote agent to get started with distributed signal intelligence.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Agent management functions
|
||||
let agents = [];
|
||||
|
||||
async function loadAgents() {
|
||||
try {
|
||||
const response = await fetch('/controller/agents?refresh=true', {
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
const data = await response.json();
|
||||
agents = data.agents || [];
|
||||
renderAgents();
|
||||
} catch (error) {
|
||||
console.error('Failed to load agents:', error);
|
||||
showToast('Failed to load agents', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function renderAgents() {
|
||||
const grid = document.getElementById('agentsGrid');
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
|
||||
if (agents.length === 0) {
|
||||
grid.innerHTML = '';
|
||||
emptyState.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
emptyState.style.display = 'none';
|
||||
|
||||
grid.innerHTML = agents.map(agent => {
|
||||
const isOnline = agent.healthy !== false && agent.is_active;
|
||||
const statusClass = isOnline ? 'online' : 'offline';
|
||||
const statusText = isOnline ? 'Online' : 'Offline';
|
||||
|
||||
const capabilities = agent.capabilities || {};
|
||||
const capBadges = Object.entries(capabilities)
|
||||
.filter(([k, v]) => v === true)
|
||||
.map(([k]) => `<span class="capability-badge">${k}</span>`)
|
||||
.join('');
|
||||
|
||||
const lastSeen = agent.last_seen
|
||||
? new Date(agent.last_seen).toLocaleString()
|
||||
: 'Never';
|
||||
|
||||
return `
|
||||
<div class="agent-card ${statusClass}">
|
||||
<div class="agent-card-header">
|
||||
<span class="agent-name">${escapeHtml(agent.name)}</span>
|
||||
<span class="agent-status">
|
||||
<span class="agent-status-dot ${statusClass}"></span>
|
||||
${statusText}
|
||||
</span>
|
||||
</div>
|
||||
<div class="agent-url">${escapeHtml(agent.base_url)}</div>
|
||||
<div class="agent-capabilities">
|
||||
${capBadges || '<span class="capability-badge disabled">No capabilities detected</span>'}
|
||||
</div>
|
||||
<div class="agent-meta">
|
||||
Last seen: ${lastSeen}<br>
|
||||
${agent.description ? `Note: ${escapeHtml(agent.description)}` : ''}
|
||||
</div>
|
||||
<div class="agent-actions">
|
||||
<button class="agent-btn" onclick="refreshAgent(${agent.id})">Refresh</button>
|
||||
<button class="agent-btn" onclick="testAgent(${agent.id})">Test</button>
|
||||
<button class="agent-btn danger" onclick="deleteAgent(${agent.id}, '${escapeHtml(agent.name)}')">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function addAgent(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const name = document.getElementById('agentName').value.trim();
|
||||
const baseUrl = document.getElementById('agentUrl').value.trim();
|
||||
const apiKey = document.getElementById('agentApiKey').value.trim();
|
||||
const description = document.getElementById('agentDescription').value.trim();
|
||||
|
||||
try {
|
||||
const response = await fetch('/controller/agents', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
base_url: baseUrl,
|
||||
api_key: apiKey || null,
|
||||
description: description || null
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showToast(`Agent "${name}" registered successfully`, 'success');
|
||||
document.getElementById('addAgentForm').reset();
|
||||
loadAgents();
|
||||
} else {
|
||||
showToast(data.message || 'Failed to register agent', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding agent:', error);
|
||||
showToast('Failed to register agent', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAgent(agentId) {
|
||||
try {
|
||||
const response = await fetch(`/controller/agents/${agentId}/refresh`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showToast('Agent refreshed', 'success');
|
||||
loadAgents();
|
||||
} else {
|
||||
showToast(data.message || 'Failed to refresh agent', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Failed to refresh agent', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAllAgents() {
|
||||
showToast('Refreshing all agents...', 'info');
|
||||
await loadAgents();
|
||||
showToast('All agents refreshed', 'success');
|
||||
}
|
||||
|
||||
async function testAgent(agentId) {
|
||||
const agent = agents.find(a => a.id === agentId);
|
||||
if (!agent) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/controller/agents/${agentId}?refresh=true`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.agent && data.agent.healthy !== false) {
|
||||
showToast(`Agent "${agent.name}" is responding`, 'success');
|
||||
} else {
|
||||
showToast(`Agent "${agent.name}" is not responding`, 'error');
|
||||
}
|
||||
loadAgents();
|
||||
} catch (error) {
|
||||
showToast(`Cannot reach agent "${agent.name}"`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAgent(agentId, agentName) {
|
||||
if (!confirm(`Are you sure you want to remove agent "${agentName}"?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/controller/agents/${agentId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showToast(`Agent "${agentName}" removed`, 'success');
|
||||
loadAgents();
|
||||
} else {
|
||||
showToast('Failed to remove agent', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Failed to remove agent', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(message, type = 'info') {
|
||||
// Remove existing toasts
|
||||
document.querySelectorAll('.toast').forEach(t => t.remove());
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast ${type}`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => toast.remove(), 3000);
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Load agents on page load
|
||||
document.addEventListener('DOMContentLoaded', loadAgents);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+137
-3
@@ -329,6 +329,8 @@
|
||||
</button>
|
||||
<button class="nav-tool-btn" onclick="showDependencies()" title="Check Tool Dependencies"
|
||||
id="depsBtn"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span></button>
|
||||
<a href="/controller/monitor" class="nav-tool-btn" title="Network Monitor - Multi-Agent View" style="text-decoration: none;"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></span></a>
|
||||
<a href="/controller/manage" class="nav-tool-btn" title="Manage Remote Agents" style="text-decoration: none;"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg></span></a>
|
||||
<button class="nav-tool-btn" onclick="showHelp()" title="Help & Documentation">?</button>
|
||||
<button class="nav-tool-btn" onclick="logout(event)" title="Logout">
|
||||
<span class="power-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg></span>
|
||||
@@ -359,6 +361,33 @@
|
||||
<div class="container">
|
||||
<div class="main-content">
|
||||
<div class="sidebar mobile-drawer" id="mainSidebar">
|
||||
<!-- Agent Selector -->
|
||||
<div class="section" id="agentSection">
|
||||
<h3>Signal Source</h3>
|
||||
<div class="form-group">
|
||||
<label style="font-size: 11px; color: #888; margin-bottom: 4px;">Agent</label>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<select id="agentSelect" style="flex: 1;">
|
||||
<option value="local">Local (This Device)</option>
|
||||
</select>
|
||||
<span id="agentStatusDot" class="agent-status-dot online" title="Agent status"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="agentInfo" class="info-text" style="font-size: 10px; color: #666; margin-top: 4px;">
|
||||
<span id="agentStatusText">Local</span>
|
||||
</div>
|
||||
<!-- Multi-agent mode toggle -->
|
||||
<div class="form-group" style="margin-top: 10px;">
|
||||
<label class="inline-checkbox" style="display: flex; align-items: center; gap: 8px;">
|
||||
<input type="checkbox" id="showAllAgents" onchange="toggleMultiAgentMode()">
|
||||
<span style="font-size: 11px;">Show All Agents Combined</span>
|
||||
</label>
|
||||
</div>
|
||||
<a href="/controller/manage" class="preset-btn" style="display: block; text-align: center; text-decoration: none; margin-top: 8px; font-size: 11px;">
|
||||
Manage Agents
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="section" id="rtlDeviceSection">
|
||||
<h3>SDR Device</h3>
|
||||
<div class="form-group">
|
||||
@@ -1541,6 +1570,7 @@
|
||||
<!-- Intercept JS Modules -->
|
||||
<script src="{{ url_for('static', filename='js/core/utils.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/core/audio.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/core/agents.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/components/radio-knob.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/components/signal-guess.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/components/signal-cards.js') }}"></script>
|
||||
@@ -1973,10 +2003,10 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Collapse all sections by default (except SDR Device which is first)
|
||||
// Collapse all sections by default (except Signal Source and SDR Device)
|
||||
document.querySelectorAll('.section').forEach((section, index) => {
|
||||
// Keep first section expanded, collapse rest
|
||||
if (index > 0) {
|
||||
// Keep first two sections expanded (Signal Source, SDR Device), collapse rest
|
||||
if (index > 1) {
|
||||
section.classList.add('collapsed');
|
||||
}
|
||||
});
|
||||
@@ -2190,6 +2220,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Show agent selector for modes that support remote agents
|
||||
const agentSection = document.getElementById('agentSection');
|
||||
const agentModes = ['pager', 'sensor', 'rtlamr', 'listening', 'aprs', 'wifi', 'bluetooth', 'aircraft'];
|
||||
if (agentSection) agentSection.style.display = agentModes.includes(mode) ? 'block' : 'none';
|
||||
|
||||
// Show RTL-SDR device section for modes that use it
|
||||
const rtlDeviceSection = document.getElementById('rtlDeviceSection');
|
||||
if (rtlDeviceSection) rtlDeviceSection.style.display = (mode === 'pager' || mode === 'sensor' || mode === 'rtlamr' || mode === 'listening' || mode === 'aprs') ? 'block' : 'none';
|
||||
@@ -2269,6 +2304,36 @@
|
||||
const ppm = document.getElementById('sensorPpm').value;
|
||||
const device = getSelectedDevice();
|
||||
|
||||
// Check if using remote agent
|
||||
if (typeof currentAgent !== 'undefined' && currentAgent !== 'local') {
|
||||
// Route through agent proxy
|
||||
const config = {
|
||||
frequency: freq,
|
||||
gain: gain,
|
||||
ppm: ppm,
|
||||
device: device
|
||||
};
|
||||
|
||||
fetch(`/controller/agents/${currentAgent}/sensor/start`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config)
|
||||
}).then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'started' || data.status === 'success') {
|
||||
setSensorRunning(true);
|
||||
startAgentSensorStream();
|
||||
showInfo(`Sensor started on remote agent`);
|
||||
} else {
|
||||
alert('Error: ' + (data.message || 'Failed to start sensor on agent'));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
alert('Error connecting to agent: ' + err.message);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if device is available
|
||||
if (!checkDeviceAvailability('sensor')) {
|
||||
return;
|
||||
@@ -2327,6 +2392,25 @@
|
||||
|
||||
// Stop sensor decoding
|
||||
function stopSensorDecoding() {
|
||||
// Check if using remote agent
|
||||
if (typeof currentAgent !== 'undefined' && currentAgent !== 'local') {
|
||||
fetch(`/controller/agents/${currentAgent}/sensor/stop`, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
setSensorRunning(false);
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
if (agentPollInterval) {
|
||||
clearInterval(agentPollInterval);
|
||||
agentPollInterval = null;
|
||||
}
|
||||
showInfo('Sensor stopped on remote agent');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/stop_sensor', { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
@@ -2339,6 +2423,56 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Polling interval for agent data
|
||||
let agentPollInterval = null;
|
||||
|
||||
// Start polling agent for sensor data
|
||||
function startAgentSensorStream() {
|
||||
if (agentPollInterval) {
|
||||
clearInterval(agentPollInterval);
|
||||
}
|
||||
|
||||
// Poll every 2 seconds for new data
|
||||
agentPollInterval = setInterval(() => {
|
||||
if (!isSensorRunning || currentAgent === 'local') {
|
||||
clearInterval(agentPollInterval);
|
||||
agentPollInterval = null;
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/controller/agents/${currentAgent}/sensor/data`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.sensors) {
|
||||
data.sensors.forEach(sensor => {
|
||||
displaySensorMessage(sensor);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(err => console.error('Agent poll error:', err));
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Display a sensor message (works for both local and remote)
|
||||
function displaySensorMessage(msg) {
|
||||
const output = document.getElementById('output');
|
||||
if (!output) return;
|
||||
|
||||
// Remove placeholder
|
||||
const placeholder = output.querySelector('.placeholder');
|
||||
if (placeholder) placeholder.style.display = 'none';
|
||||
|
||||
// Create signal card if SignalCards is available
|
||||
if (typeof SignalCards !== 'undefined' && SignalCards.createFromSensor) {
|
||||
const card = SignalCards.createFromSensor(msg);
|
||||
if (card) {
|
||||
output.insertBefore(card, output.firstChild);
|
||||
sensorCount++;
|
||||
updateStats();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setSensorRunning(running) {
|
||||
isSensorRunning = running;
|
||||
document.getElementById('statusDot').classList.toggle('running', running);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user