Files
intercept/templates/agents.html
Smittix f70deb32a2 feat: Add back button to navigation on dashboard pages
Add browser history back button alongside existing dashboard links on
vessels, aircraft, network monitor, and remote agents pages.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 23:23:13 +00:00

572 lines
20 KiB
HTML

<!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;
}
/* Navigation links */
.nav-links {
display: flex;
gap: 16px;
margin-bottom: 20px;
}
.back-link {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--accent-cyan);
text-decoration: none;
font-size: 14px;
}
.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; display: flex; align-items: center; gap: 12px;">
<div class="logo">
<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="margin: 0;">
iNTERCEPT <span class="tagline">// Remote Agents</span>
</h1>
</header>
<div class="agents-container">
<div class="nav-links">
<a href="#" onclick="history.back(); return false;" 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
</a>
<a href="/" class="back-link">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>
Dashboard
</a>
</div>
<div class="agents-header">
<h1>Remote Agents</h1>
</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>
<div style="display: flex; gap: 10px;">
<button type="submit" class="agent-btn primary" style="width: auto; padding: 10px 24px;">
Register Agent
</button>
<button type="button" class="agent-btn" style="width: auto; padding: 10px 24px;" onclick="refreshAllAgents()">
Refresh All
</button>
</div>
</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>