MorphGuard / templates /setup-improved.html
juanquy's picture
Fix navigation button issue in Hugging Face Space iframe
676a099
Raw
History Blame Contribute Delete
157 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MorphGuard - Setup Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="/static/css/design-tokens.css">
<link rel="stylesheet" href="/static/css/setup-improved.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<script>
mermaid.initialize({ startOnLoad: false, securityLevel: 'loose' });
</script>
</head>
<body>
<div class="header-gradient text-white py-4">
<div class="container mx-auto px-4 flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold mb-1">MorphGuard Setup</h1>
<p class="text-sm opacity-90">Advanced Configuration Dashboard</p>
</div>
<div class="flex items-center space-x-3">
<button type="button" data-nav-url="{{ url_for('main.demo') }}" class="px-3 py-1.5 rounded-lg border border-white border-opacity-35 text-white hover:bg-white hover:text-blue-900 hover:bg-opacity-100 text-sm font-semibold transition-all duration-200 shadow-sm" style="cursor: pointer; color: white !important; border: 1px solid rgba(255,255,255,0.4); background: transparent; position: relative; z-index: 10000;">Demo</button>
<button type="button" data-nav-url="{{ url_for('auth.logout') }}" class="px-3 py-1.5 rounded-lg bg-red-600 bg-opacity-80 hover:bg-opacity-100 text-white text-sm font-semibold transition-all duration-200 shadow-sm" style="cursor: pointer; color: white !important; background-color: rgba(220,38,38,0.8) !important; border: 1px solid rgba(220,38,38,0.6); position: relative; z-index: 10000;">Logout</button>
</div>
</div>
</div>
<script>
(function(){
document.addEventListener('click', function(e) {
var btn = e.target.closest('[data-nav-url]');
if (btn) {
e.preventDefault();
e.stopImmediatePropagation();
window.location.assign(btn.getAttribute('data-nav-url'));
}
}, true);
})();
</script>
<div class="container mx-auto px-4 py-6 main-scroll-container">
<!-- Alerts Banner -->
<div id="alerts-banner" class="hidden mb-4 px-4 py-2 rounded bg-red-600 text-white"></div>
<!-- Setup Wizard Stepper -->
<div id="setup-stepper" class="flex justify-between mb-6">
<div class="stepper-step px-4 py-2 cursor-pointer setup-step-active" data-step="dashboard">Dashboard</div>
<div class="stepper-step px-4 py-2 cursor-pointer" data-step="dataset">Data Setup</div>
<div class="stepper-step px-4 py-2 cursor-pointer" data-step="training">Training</div>
<div class="stepper-step px-4 py-2 cursor-pointer" data-step="monitoring">Metrics</div>
<div class="stepper-step px-4 py-2 cursor-pointer" data-step="dronelink">🚁 Drone-Link</div>
<div class="stepper-step px-4 py-2 cursor-pointer" data-step="users">System Users</div>
</div>
<!-- Dashboard Panel -->
<div id="panel-dashboard" class="panel">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<!-- System Status Card -->
<div class="card rounded-lg p-3">
<h3 class="text-lg font-semibold mb-3">System Status</h3>
<div class="mb-2 flex justify-between">
<span class="text-sm">Detector Service</span>
<span class="px-2 py-0.5 text-xs bg-green-900 text-green-300 rounded">Running</span>
</div>
<div class="mb-2 flex justify-between">
<span class="text-sm">Demorphing Service</span>
<span class="px-2 py-0.5 text-xs bg-green-900 text-green-300 rounded">Running</span>
</div>
<div class="mb-2 flex justify-between">
<span class="text-sm">Verification Service</span>
<span class="px-2 py-0.5 text-xs bg-green-900 text-green-300 rounded">Running</span>
</div>
<div class="mb-2 flex justify-between">
<span class="text-sm">Database Connection</span>
<span class="px-2 py-0.5 text-xs bg-green-900 text-green-300 rounded">Connected</span>
</div>
<div class="mb-2 flex justify-between">
<span class="text-sm">Database Connection</span>
<span class="px-2 py-0.5 text-xs bg-green-900 text-green-300 rounded">Connected</span>
</div>
</div>
<!-- Model Status Card -->
<div class="card rounded-lg p-3">
<h3 class="text-lg font-semibold mb-3">Model Status</h3>
<div class="mb-2">
<div class="flex justify-between mb-1">
<span class="text-sm">Detector Model</span>
<span class="text-xs text-blue-400">v2.3.0</span>
</div>
<div class="w-full bg-gray-700 rounded-full h-2">
<div class="bg-blue-600 h-2 rounded-full w-94"></div>
</div>
<div class="text-xs text-gray-400 mt-1">Accuracy: 94%</div>
</div>
<div class="mb-2">
<div class="flex justify-between mb-1">
<span class="text-sm">Demorpher Model</span>
<span class="text-xs text-blue-400">v1.8.5</span>
</div>
<div class="w-full bg-gray-700 rounded-full h-2">
<div class="bg-blue-600 h-2 rounded-full w-87"></div>
</div>
<div class="text-xs text-gray-400 mt-1">Accuracy: 87%</div>
</div>
<div class="mb-2">
<div class="flex justify-between mb-1">
<span class="text-sm">Verification Model</span>
<span class="text-xs text-blue-400">v2.1.1</span>
</div>
<div class="w-full bg-gray-700 rounded-full h-2">
<div class="bg-blue-600 h-2 rounded-full w-95"></div>
</div>
<div class="text-xs text-gray-400 mt-1">Accuracy: 95%</div>
</div>
</div>
<!-- Resource Usage Card -->
<div class="card rounded-lg p-3">
<h3 class="text-lg font-semibold mb-3">Resource Usage</h3>
<div class="mb-2">
<div class="flex justify-between mb-1">
<span class="text-sm">CPU Usage</span>
<span class="text-xs">42%</span>
</div>
<div class="w-full bg-gray-700 rounded-full h-2">
<div class="bg-yellow-500 h-2 rounded-full w-42"></div>
</div>
</div>
<div class="mb-2">
<div class="flex justify-between mb-1">
<span class="text-sm">GPU Memory</span>
<span class="text-xs">6.2GB / 11GB</span>
</div>
<div class="w-full bg-gray-700 rounded-full h-2">
<div class="bg-green-500 h-2 rounded-full w-56"></div>
</div>
</div>
<div class="mb-2">
<div class="flex justify-between mb-1">
<span class="text-sm">System Memory</span>
<span class="text-xs">8.4GB / 32GB</span>
</div>
<div class="w-full bg-gray-700 rounded-full h-2">
<div class="bg-green-500 h-2 rounded-full w-26"></div>
</div>
</div>
<div class="mb-2">
<div class="flex justify-between mb-1">
<span class="text-sm">Disk Usage</span>
<span class="text-xs">412GB / 1TB</span>
</div>
<div class="w-full bg-gray-700 rounded-full h-2">
<div class="bg-green-500 h-2 rounded-full w-41"></div>
</div>
</div>
</div>
<!-- Forensic Evidence Settings Card -->
<div class="card rounded-lg p-3">
<h3 class="text-lg font-semibold mb-3">Forensic Evidence</h3>
<p class="text-xs text-gray-400 mb-3">Configure immutable evidence logging for compliance and legal
requirements.</p>
<div class="mb-3">
<label class="flex items-center cursor-pointer">
<input type="checkbox" id="blockchain-enabled-global" class="sr-only peer">
<div
class="relative w-11 h-6 bg-gray-600 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600">
</div>
<span class="ml-3 text-sm font-medium">Enable Blockchain Logging</span>
</label>
<p class="text-xs text-gray-500 mt-1 ml-14">Log all detections to Ethereum for immutable audit
trails.</p>
</div>
<div class="mb-3">
<label for="blockchain-network" class="block text-sm mb-1">Network</label>
<select id="blockchain-network" class="w-full p-2 text-sm bg-gray-700 rounded"
aria-label="Blockchain Network">
<option value="sepolia">Ethereum Sepolia (Testnet)</option>
<option value="mainnet">Ethereum Mainnet</option>
<option value="polygon">Polygon</option>
<option value="local">Local Node (Development)</option>
</select>
</div>
<div id="blockchain-status" class="text-xs">
<span class="text-gray-400">Status:</span>
<span id="blockchain-connection-status" class="text-yellow-400">Not Connected</span>
</div>
<button id="save-blockchain-settings"
class="mt-3 w-full btn-primary py-2 px-4 rounded text-sm font-medium">
Save Settings
</button>
</div>
<!-- Recent Activity -->
<div class="card rounded-lg p-3">
<h3 class="text-lg font-semibold mb-2">Recent Activity</h3>
<div class="overflow-x-auto">
<table class="min-w-full">
<thead>
<tr class="border-b border-gray-700">
<th class="px-4 py-2 text-left text-xs text-gray-400">Time</th>
<th class="px-4 py-2 text-left text-xs text-gray-400">Event</th>
<th class="px-4 py-2 text-left text-xs text-gray-400">User</th>
<th class="px-4 py-2 text-left text-xs text-gray-400">Status</th>
</tr>
</thead>
<tbody id="user-table-body">
<tr class="border-b border-gray-700">
<td class="px-4 py-2 text-sm">2025-05-15 06:32:15</td>
<td class="px-4 py-2 text-sm">Detector model training completed</td>
<td class="px-4 py-2 text-sm">admin</td>
<td class="px-4 py-2"><span
class="px-2 py-0.5 text-xs bg-green-900 text-green-300 rounded">Success</span>
</td>
</tr>
<tr class="border-b border-gray-700">
<td class="px-4 py-2 text-sm">2025-05-15 05:48:22</td>
<td class="px-4 py-2 text-sm">System backup completed</td>
<td class="px-4 py-2 text-sm">system</td>
<td class="px-4 py-2"><span
class="px-2 py-0.5 text-xs bg-green-900 text-green-300 rounded">Success</span>
</td>
</tr>
<tr class="border-b border-gray-700">
<td class="px-4 py-2 text-sm">2025-05-15 04:15:09</td>
<td class="px-4 py-2 text-sm">New dataset uploaded</td>
<td class="px-4 py-2 text-sm">researcher1</td>
<td class="px-4 py-2"><span
class="px-2 py-0.5 text-xs bg-green-900 text-green-300 rounded">Success</span>
</td>
</tr>
<tr class="border-b border-gray-700">
<td class="px-4 py-2 text-sm">2025-05-14 22:30:58</td>
<td class="px-4 py-2 text-sm">Demorpher model optimization</td>
<td class="px-4 py-2 text-sm">admin</td>
<td class="px-4 py-2"><span
class="px-2 py-0.5 text-xs bg-green-900 text-green-300 rounded">Success</span>
</td>
</tr>
<tr>
<td class="px-4 py-2 text-sm">2025-05-14 18:11:42</td>
<td class="px-4 py-2 text-sm">GPU driver update</td>
<td class="px-4 py-2 text-sm">system</td>
<td class="px-4 py-2"><span
class="px-2 py-0.5 text-xs bg-yellow-900 text-yellow-300 rounded">Warning</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- End panel-dashboard -->
<!-- Dataset Management Panel -->
<div id="panel-dataset" class="panel hidden">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-3">
<!-- Dataset Stats Card -->
<div class="card rounded-lg p-3">
<h3 class="text-lg font-semibold mb-2">Dataset Statistics</h3>
<div class="grid grid-cols-2 gap-4">
<div class="p-3 bg-gray-800 rounded">
<p class="text-sm mb-1">Training Images</p>
<p class="text-2xl font-bold">{{ stats.total_training }}</p>
<div class="text-xs text-gray-400 mt-1">
<span class="text-green-400">▲ {{ stats.train_morph }}</span> morph images
</div>
</div>
<div class="p-3 bg-gray-800 rounded">
<p class="text-sm mb-1">Validation Images</p>
<p class="text-2xl font-bold">{{ stats.total_validation }}</p>
<div class="text-xs text-gray-400 mt-1">
<span class="text-green-400">▲ {{ stats.val_morph }}</span> morph images
</div>
</div>
<div class="p-3 bg-gray-800 rounded">
<p class="text-sm mb-1">Morph Images</p>
<p class="text-2xl font-bold">{{ stats.total_morph }}</p>
<div class="text-xs text-gray-400 mt-1">
<span class="text-green-400">▲ {{ stats.val_morph }}</span> validation morph
</div>
</div>
<div class="p-3 bg-gray-800 rounded">
<p class="text-sm mb-1">Real Images</p>
<p class="text-2xl font-bold">{{ stats.total_real }}</p>
<div class="text-xs text-gray-400 mt-1">
<span class="text-green-400">▲ {{ stats.external_faces }}</span> external faces
</div>
</div>
</div>
<div class="mt-4">
<h4 class="text-sm font-medium mb-2">Class Distribution</h4>
<div class="w-full bg-gray-800 rounded-full h-4 overflow-hidden">
<div class="flex h-full">
<div class="bg-blue-600 h-full dynamic-bar" data-width="{{ stats.morph_percent }}">
</div>
<div class="bg-green-500 h-full dynamic-bar" data-width="{{ stats.real_percent }}">
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.dynamic-bar').forEach(el => el.style.width = el.getAttribute('data-width') + '%');
});
</script>
<div class="flex justify-between text-xs mt-1">
<div class="flex items-center">
<div class="w-3 h-3 bg-blue-600 rounded-full mr-1"></div>
<span>Morph ({{ stats.morph_percent }}%)</span>
</div>
<div class="flex items-center">
<div class="w-3 h-3 bg-green-500 rounded-full mr-1"></div>
<span>Real ({{ stats.real_percent }}%)</span>
</div>
</div>
</div>
</div>
<!-- Download Datasets Card -->
<div class="card rounded-lg p-3">
<h3 class="text-lg font-semibold mb-2">Pre-built Datasets</h3>
<div class="mb-2">
<label class="block text-sm font-medium mb-1">Dataset Type</label>
<select id="dataset-type-selector" class="w-full p-2 rounded" aria-label="Dataset Type"
title="Select Dataset Type">
<option>Morph Detection Training</option>
<option>Demorphing Training</option>
<option>Verification Training</option>
<option>Liveness Detection</option>
</select>
</div>
<div id="download-datasets-container" class="mb-2 max-h-48 overflow-y-auto">
<!-- Dataset list will be populated by JavaScript -->
<div class="flex justify-center items-center h-32">
<div class="loader border-4 border-gray-600 rounded-full w-8 h-8"></div>
</div>
</div>
<div class="bg-gray-900 rounded p-3 mb-2">
<p class="text-xs text-gray-400 mb-2">Ready-to-use datasets provide:</p>
<ul class="text-xs text-gray-400 list-disc pl-4">
<li>Pre-labeled training samples</li>
<li>Balanced class distribution</li>
<li>Automatic train/validation split</li>
<li>Organized folder structure</li>
</ul>
</div>
</div>
<!-- Upload Custom Dataset Card -->
<div class="card rounded-lg p-3">
<h3 class="text-lg font-semibold mb-2">Upload Custom Dataset</h3>
<div class="mb-2">
<label class="block text-sm font-medium mb-1">Dataset Type</label>
<select class="w-full p-2 rounded" aria-label="Custom Dataset Type"
title="Select Custom Dataset Type">
<option>Morph Detection Training</option>
<option>Demorphing Training</option>
<option>Verification Training</option>
<option>Liveness Detection</option>
</select>
</div>
<div class="mb-2">
<label class="block text-sm font-medium mb-1">Dataset Files (ZIP)</label>
<input type="file" class="w-full p-2 rounded" accept=".zip" aria-label="Dataset File"
title="Upload Dataset File">
</div>
<div class="mb-2">
<label class="block text-sm font-medium mb-1">Dataset Configuration</label>
<textarea class="w-full p-2 rounded" rows="4"
placeholder="{&#34;split_ratio&#34;: 0.8, &#34;augmentation&#34;: true, &#34;preprocess&#34;: true}"></textarea>
</div>
<div class="flex items-center mb-2">
<div class="custom-checkbox"></div>
<label class="text-sm">Auto-split train/validation</label>
</div>
<div class="flex items-center mb-2">
<div class="custom-checkbox"></div>
<label class="text-sm">Apply standard augmentation</label>
</div>
<div class="flex justify-end">
<button class="btn-primary px-4 py-2 rounded font-medium">Upload Dataset</button>
</div>
</div>
<!-- API Key Management Card -->
<div class="card rounded-lg p-3">
<h3 class="text-lg font-semibold mb-2">API Key Management</h3>
<div class="mb-2">
<label class="block text-sm font-medium mb-1">AWS Access Key</label>
<input type="text" id="accessKey" class="w-full p-2 rounded" placeholder="Access Key">
</div>
<div class="mb-3">
<label class="block text-sm font-medium mb-1">AWS Secret Key</label>
<input type="password" id="secretKey" class="w-full p-2 rounded" placeholder="Secret Key">
</div>
<div class="mb-3">
<label class="block text-sm font-medium mb-1">Pexels API Key</label>
<input type="text" id="pexelsKey" class="w-full p-2 rounded" placeholder="Pexels Key">
</div>
<div class="flex justify-end">
<button id="saveKeys" class="btn-primary px-4 py-2 rounded font-medium">Save API
Keys</button>
</div>
</div>
<!-- External Dataset Links Card -->
<div class="card rounded-lg p-3">
<h3 class="text-lg font-semibold mb-3">External Dataset Links</h3>
<ul class="list-disc list-inside text-sm">
<li><a href="https://www.robots.ox.ac.uk/~vgg/data/vgg_face2/" target="_blank"
rel="noopener noreferrer" class="text-blue-400">VGGFace2</a></li>
<li><a href="http://www.cbsr.ia.ac.cn/english/CASIA-WebFace-Database.html" target="_blank"
rel="noopener noreferrer" class="text-blue-400">CASIA-WebFace</a></li>
<li><a href="https://github.com/Tencent/TFace/tree/master/CelebA-HQ" target="_blank"
rel="noopener noreferrer" class="text-blue-400">CelebA-HQ</a></li>
<li><a href="https://www.kaggle.com/datasets/" target="_blank" rel="noopener noreferrer"
class="text-blue-400">Kaggle Face
Datasets</a></li>
</ul>
</div>
</div>
<!-- Data Ingestion & Bootstrapping Card -->
<div class="card rounded-lg p-3 mb-3">
<h3 class="text-lg font-semibold mb-2">Data Ingestion & Bootstrapping</h3>
<div class="mb-3">
<label class="block text-sm font-medium mb-1">Real Faces (train)</label>
<div class="flex">
<input type="number" id="realCount" class="w-full p-2 rounded" value="20" min="1"
aria-label="Real Face Count" title="Number of Real Faces">
<button id="fetchReal" class="btn-primary ml-2 px-4 py-2 rounded">Fetch Real</button>
</div>
</div>
<div class="mb-3">
<label class="block text-sm font-medium mb-1">Morph Faces (train)</label>
<div class="flex">
<input type="number" id="morphCount" class="w-full p-2 rounded" value="20" min="1"
aria-label="Morph Face Count" title="Number of Morph Faces">
<button id="genMorphs" class="btn-primary ml-2 px-4 py-2 rounded">Generate Morphs</button>
</div>
</div>
<div class="mb-3">
<label class="block text-sm font-medium mb-1">Real Faces (val)</label>
<div class="flex">
<input type="number" id="valRealCount" class="w-full p-2 rounded" value="20" min="1"
aria-label="Validation Real Count" title="Number of Validation Real Faces">
<button id="fetchValReal" class="btn-primary ml-2 px-4 py-2 rounded">Fetch Val Real</button>
</div>
</div>
<div class="mb-3">
<label class="block text-sm font-medium mb-1">Morph Faces (val)</label>
<div class="flex">
<input type="number" id="valMorphCount" class="w-full p-2 rounded" value="20" min="1"
aria-label="Validation Morph Count" title="Number of Validation Morph Faces">
<button id="genValMorphs" class="btn-primary ml-2 px-4 py-2 rounded">Generate Val
Morphs</button>
</div>
</div>
<hr class="my-4 border-gray-600">
<div class="mb-3">
<label class="block text-sm font-medium mb-1">LFW Count</label>
<div class="flex">
<input type="number" id="lfwCount" class="w-full p-2 rounded" value="100" min="1"
aria-label="LFW Count" title="Number of LFW Images">
<button id="fetchLFW" class="btn-primary ml-2 px-4 py-2 rounded">Fetch LFW</button>
</div>
</div>
<div class="mb-3">
<label class="block text-sm font-medium mb-1">Pexels Count</label>
<div class="flex">
<input type="number" id="pexelsCount" class="w-full p-2 rounded" value="50" min="1"
aria-label="Pexels Count" title="Number of Pexels Images">
<button id="fetchPexels" class="btn-primary ml-2 px-4 py-2 rounded">Fetch Pexels</button>
</div>
</div>
<div class="mb-3">
<label class="block text-sm font-medium mb-1">UTKFace Count</label>
<div class="flex">
<input type="number" id="utkCount" class="w-full p-2 rounded" value="100" min="1"
aria-label="UTKFace Count" title="Number of UTKFace Images">
<button id="fetchUTKFace" class="btn-primary ml-2 px-4 py-2 rounded">Fetch UTKFace</button>
</div>
</div>
<div class="mb-3">
<label class="block text-sm font-medium mb-1">CelebA Count</label>
<div class="flex">
<input type="number" id="celebaCount" class="w-full p-2 rounded" value="100" min="1"
aria-label="CelebA Count" title="Number of CelebA Images">
<button id="fetchCelebA" class="btn-primary ml-2 px-4 py-2 rounded">Fetch CelebA</button>
</div>
</div>
<div class="mb-3">
<h3 class="text-sm text-gray-400 mb-1">VGGFace2 (Manual)</h3>
<button id="fetchVGGFace2" class="btn-primary px-4 py-2 rounded">Fetch VGGFace2</button>
</div>
<div class="mt-4">
<h3 class="text-sm font-medium mb-2">Bootstrap Dataset (train & val)</h3>
<div class="flex space-x-2">
<input type="number" id="bootstrapTrainCount" class="w-24 p-2 rounded" value="20" min="1"
aria-label="Bootstrap Train Count" title="Bootstrap Training Count">
<input type="number" id="bootstrapValCount" class="w-24 p-2 rounded" value="20" min="1"
aria-label="Bootstrap Validation Count" title="Bootstrap Validation Count">
<button id="bootstrapData" class="btn-primary px-4 py-2 rounded">Bootstrap Data</button>
</div>
</div>
</div>
<!-- Dataset Browser -->
<div class="card rounded-lg p-3 mb-3">
<div class="flex justify-between items-center mb-2">
<h3 class="text-lg font-semibold">Dataset Browser</h3>
<div class="flex space-x-2">
<select class="p-1 text-sm rounded" aria-label="Filter Dataset Type" title="Filter by Type">
<option>All Types</option>
<option>Morph</option>
<option>Real</option>
</select>
<input type="text" placeholder="Search..." class="p-1 text-sm rounded">
</div>
</div>
<div id="dataset-browser" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
<!-- Images will be dynamically populated via JavaScript -->
<!-- Empty State Placeholder -->
<div id="dataset-empty-state" class="col-span-full text-center py-12">
<div class="text-6xl mb-4 opacity-50">📁</div>
<h4 class="text-lg font-semibold text-gray-300 mb-2">No Dataset Images Found</h4>
<p class="text-sm text-gray-500 mb-4">Use the data ingestion tools on the left to populate your
training dataset.</p>
<div class="text-xs text-gray-600 max-w-md mx-auto">
<p class="mb-2">Quick Start Options:</p>
<ul class="list-disc list-inside text-left inline-block">
<li>Click <strong>Fetch Real</strong> to download real face images</li>
<li>Click <strong>Generate Morphs</strong> to create morph training data</li>
<li>Click <strong>Bootstrap Data</strong> for a quick start dataset</li>
</ul>
</div>
</div>
</div>
<div class="flex justify-between mt-4">
<button id="prev-page" class="text-sm text-gray-400 hover:text-white opacity-50 cursor-not-allowed"
disabled>
Previous</button>
<div class="text-sm">Page <span id="current-page">1</span> of <span id="total-pages">{{
((stats.total_morph + stats.total_real) / 6) | round(0, 'ceil') | int }}</span></div>
<button id="next-page" class="text-sm text-gray-400 hover:text-white">Next →</button>
</div>
</div>
<!-- Flagged Images -->
<div class="card rounded-lg p-3">
<h3 class="text-lg font-semibold mb-3">Flagged Images</h3>
<div class="overflow-x-auto">
<table id="flagged-images-table" class="min-w-full">
<thead>
<tr class="border-b border-gray-700">
<th class="px-4 py-2 text-left text-xs text-gray-400">Image</th>
<th class="px-4 py-2 text-left text-xs text-gray-400">Filename</th>
<th class="px-4 py-2 text-left text-xs text-gray-400">Current Label</th>
<th class="px-4 py-2 text-left text-xs text-gray-400">Flagged As</th>
<th class="px-4 py-2 text-left text-xs text-gray-400">Reason</th>
<th class="px-4 py-2 text-left text-xs text-gray-400">Actions</th>
</tr>
</thead>
<tbody id="flagged-images-tbody">
<tr class="border-b border-gray-700">
<td colspan="6" class="px-4 py-8 text-center text-gray-500">
<div class="text-3xl mb-2 opacity-50"></div>
<p class="text-sm">No flagged images to review</p>
<p class="text-xs text-gray-600 mt-1">Images flagged during detection will appear
here</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Training Panel -->
<div id="panel-training" class="panel hidden">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-3">
<!-- Training Configuration Card -->
<div class="card rounded-lg p-3">
<h3 class="text-lg font-semibold mb-2">Training Configuration</h3>
<div class="mb-2">
<label class="block text-sm font-medium mb-1">Model Type</label>
<select name="model" class="w-full p-2 rounded" aria-label="Model Type"
title="Select Model Type">
<option>Morph Detector</option>
<option>Demorpher</option>
<option>Identity Verifier</option>
<option>Liveness Detector</option>
</select>
</div>
<div class="mb-2">
<label class="block text-sm font-medium mb-1">Architecture</label>
<select class="w-full p-2 rounded" aria-label="Model Architecture" title="Select Architecture">
<option>Vision Transformer (ViT-Base)</option>
<option>Vision Transformer (ViT-Large)</option>
<option>ResNet-50</option>
<option>EfficientNet-B3</option>
<option>Custom M2TR</option>
</select>
</div>
<div class="mb-2">
<label class="block text-sm font-medium mb-1">Batch Size</label>
<input type="number" name="batch_size" class="w-full p-2 rounded" value="32"
aria-label="Batch Size" title="Training Batch Size">
</div>
<div class="mb-2">
<label class="block text-sm font-medium mb-1">Learning Rate</label>
<input type="number" name="learning_rate" class="w-full p-2 rounded" value="0.0001"
step="0.0001" aria-label="Learning Rate" title="Training Learning Rate">
</div>
<div class="mb-2">
<label class="block text-sm font-medium mb-1">Epochs</label>
<input type="number" name="epochs" class="w-full p-2 rounded" value="50" aria-label="Epochs"
title="Number of Epochs">
</div>
<div class="grid grid-cols-2 gap-3 mb-2">
<div>
<label class="block text-sm font-medium mb-1">Optimizer</label>
<select class="w-full p-2 rounded" aria-label="Optimizer" title="Select Optimizer">
<option>Adam</option>
<option>SGD</option>
<option>AdamW</option>
<option>RMSprop</option>
</select>
</div>
<div>
<label class="block text-sm font-medium mb-1">Loss Function</label>
<select class="w-full p-2 rounded" aria-label="Loss Function" title="Select Loss Function">
<option>Binary Cross Entropy</option>
<option>Focal Loss</option>
<option>MSE</option>
<option>Custom Loss</option>
</select>
</div>
</div>
<div class="flex items-center mb-2">
<div class="custom-checkbox checked"></div>
<label class="text-sm">Enable mixed precision</label>
</div>
<div class="flex items-center mb-2">
<div class="custom-checkbox checked"></div>
<label class="text-sm">Checkpoint best model</label>
</div>
<div class="flex items-center mb-2">
<div class="custom-checkbox"></div>
<label class="text-sm">Enable early stopping</label>
</div>
<div class="flex justify-end">
<button class="btn-primary px-4 py-2 rounded font-medium start-training-btn">Start
Training</button>
</div>
</div>
<!-- Advanced Configuration Card -->
<div class="card rounded-lg p-3">
<h3 class="text-lg font-semibold mb-2">Advanced Configuration</h3>
<div class="mb-2">
<label class="block text-sm font-medium mb-1">Loss Weighting</label>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs text-gray-400 mb-1">Morph Weight</label>
<input type="number" class="w-full p-2 rounded" value="1.0" step="0.1"
aria-label="Morph Weight" title="Loss Weight for Morph Class">
</div>
<div>
<label class="block text-xs text-gray-400 mb-1">Real Weight</label>
<input type="number" class="w-full p-2 rounded" value="1.0" step="0.1"
aria-label="Real Weight" title="Loss Weight for Real Class">
</div>
</div>
</div>
<div class="mb-2">
<label class="block text-sm font-medium mb-1">Regularization</label>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs text-gray-400 mb-1">Weight Decay</label>
<input type="number" class="w-full p-2 rounded" value="0.01" step="0.01"
aria-label="Weight Decay" title="Regularization Weight Decay">
</div>
<div>
<label class="block text-xs text-gray-400 mb-1">Dropout Rate</label>
<input type="number" class="w-full p-2 rounded" value="0.2" step="0.1"
aria-label="Dropout Rate" title="Regularization Dropout Rate">
</div>
</div>
</div>
<div class="mb-2">
<label class="block text-sm font-medium mb-1">Learning Rate Schedule</label>
<select class="w-full p-2 rounded" aria-label="Learning Rate Schedule"
title="Select LR Schedule">
<option>Cosine Annealing</option>
<option>Step Decay</option>
<option>Exponential Decay</option>
<option>None</option>
</select>
</div>
<div class="mb-2">
<label class="block text-sm font-medium mb-1">Data Augmentation</label>
<div class="grid grid-cols-2 gap-3">
<div class="flex items-center">
<div class="custom-checkbox checked"></div>
<label class="text-xs">Random Crop</label>
</div>
<div class="flex items-center">
<div class="custom-checkbox checked"></div>
<label class="text-xs">Horizontal Flip</label>
</div>
<div class="flex items-center">
<div class="custom-checkbox checked"></div>
<label class="text-xs">Color Jitter</label>
</div>
<div class="flex items-center">
<div class="custom-checkbox"></div>
<label class="text-xs">Random Rotation</label>
</div>
<div class="flex items-center">
<div class="custom-checkbox"></div>
<label class="text-xs">Gaussian Blur</label>
</div>
<div class="flex items-center">
<div class="custom-checkbox"></div>
<label class="text-xs">Random Erasing</label>
</div>
</div>
</div>
<div class="mb-2">
<label class="block text-sm font-medium mb-1">Monitoring</label>
<div class="grid grid-cols-2 gap-3">
<div class="flex items-center">
<div class="custom-checkbox checked"></div>
<label class="text-xs">TensorBoard</label>
</div>
<div class="flex items-center">
<div class="custom-checkbox checked"></div>
<label class="text-xs">TimescaleDB</label>
</div>
<div class="flex items-center">
<div class="custom-checkbox checked"></div>
<label class="text-xs">Grafana</label>
</div>
<div class="flex items-center">
<div class="custom-checkbox"></div>
<label class="text-xs">WandB</label>
</div>
</div>
</div>
<div class="mb-3">
<label class="block text-sm font-medium mb-1" for="advanced-yaml-config">Advanced YAML
Configuration</label>
<textarea id="advanced-yaml-config" class="w-full p-2 rounded bg-gray-900 font-mono text-xs"
rows="4" aria-label="YAML Config" title="Edit YAML Configuration">model:
name: vit_base_patch16_224
pretrained: true
dropout: 0.2
optimizer:
name: adamw
lr: 0.0001
weight_decay: 0.01</textarea>
</div>
</div>
</div>
<!-- Training Progress Card -->
<div class="card rounded-lg p-3 mb-3">
<h3 class="text-lg font-semibold mb-2">Current Training Progress</h3>
<div class="flex items-center mb-4">
<div class="mr-3">
<div class="text-xs text-gray-400">Status</div>
<div class="text-sm font-medium">Training in Progress</div>
</div>
<div class="mr-3">
<div class="text-xs text-gray-400">Model</div>
<div class="text-sm font-medium">Morph Detector (M2TR)</div>
</div>
<div class="mr-3">
<div class="text-xs text-gray-400">Epoch</div>
<div class="text-sm font-medium">14 / 50</div>
</div>
<div class="mr-3">
<div class="text-xs text-gray-400">ETA</div>
<div class="text-sm font-medium">3h 22m</div>
</div>
<div class="mr-3">
<div class="text-xs text-gray-400">Best Accuracy</div>
<div class="text-sm font-medium">94.2%</div>
</div>
</div>
<div class="mb-3">
<div class="flex justify-between mb-1">
<span class="text-sm">Progress</span>
<span class="text-xs">28% (14/50 epochs)</span>
</div>
<div class="w-full bg-gray-700 rounded-full h-2">
<div class="bg-blue-600 h-2 rounded-full w-28"></div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h4 class="text-sm font-medium mb-2">Loss Curve</h4>
<div class="bg-gray-800 rounded h-48 p-3">
<!-- Loss curve graph would be here -->
<div class="h-full flex items-center justify-center text-gray-500 text-sm">
Loss visualization chart
</div>
</div>
</div>
<div>
<h4 class="text-sm font-medium mb-2">Accuracy Curve</h4>
<div class="bg-gray-800 rounded h-48 p-3">
<!-- Accuracy curve graph would be here -->
<div class="h-full flex items-center justify-center text-gray-500 text-sm">
Accuracy visualization chart
</div>
</div>
</div>
</div>
<div class="flex justify-end mt-4 space-x-3">
<button class="px-3 py-1 bg-yellow-700 text-white rounded text-sm">Pause Training</button>
<button class="px-3 py-1 bg-red-700 text-white rounded text-sm">Stop Training</button>
</div>
</div>
<!-- Model History Card -->
<div class="card rounded-lg p-3">
<h3 class="text-lg font-semibold mb-3">Model Training History</h3>
<div class="overflow-x-auto">
<table class="min-w-full">
<thead>
<tr class="border-b border-gray-700">
<th class="px-4 py-2 text-left text-xs text-gray-400">Date</th>
<th class="px-4 py-2 text-left text-xs text-gray-400">Model</th>
<th class="px-4 py-2 text-left text-xs text-gray-400">Architecture</th>
<th class="px-4 py-2 text-left text-xs text-gray-400">Accuracy</th>
<th class="px-4 py-2 text-left text-xs text-gray-400">Loss</th>
<th class="px-4 py-2 text-left text-xs text-gray-400">Status</th>
<th class="px-4 py-2 text-left text-xs text-gray-400">Actions</th>
</tr>
</thead>
<tbody>
<tr class="border-b border-gray-700">
<td class="px-4 py-2 text-sm">2025-05-15</td>
<td class="px-4 py-2 text-sm">Morph Detector v2.3.0</td>
<td class="px-4 py-2 text-sm">M2TR (ViT-Base)</td>
<td class="px-4 py-2 text-sm">94.2%</td>
<td class="px-4 py-2 text-sm">0.187</td>
<td class="px-4 py-2"><span
class="px-2 py-0.5 text-xs bg-blue-900 text-blue-300 rounded">In
Progress</span>
</td>
<td class="px-4 py-2">
<button class="px-2 py-1 text-xs bg-gray-800 text-white rounded">View
Logs</button>
</td>
</tr>
<tr class="border-b border-gray-700">
<td class="px-4 py-2 text-sm">2025-05-12</td>
<td class="px-4 py-2 text-sm">Demorpher v1.8.5</td>
<td class="px-4 py-2 text-sm">GAN (pix2pixHD)</td>
<td class="px-4 py-2 text-sm">87.3%</td>
<td class="px-4 py-2 text-sm">0.346</td>
<td class="px-4 py-2"><span
class="px-2 py-0.5 text-xs bg-green-900 text-green-300 rounded">Completed</span>
</td>
<td class="px-4 py-2">
<div class="flex space-x-2">
<button class="px-2 py-1 text-xs bg-blue-800 text-white rounded">Deploy</button>
<button class="px-2 py-1 text-xs bg-gray-800 text-white rounded">Logs</button>
</div>
</td>
</tr>
<tr class="border-b border-gray-700">
<td class="px-4 py-2 text-sm">2025-05-05</td>
<td class="px-4 py-2 text-sm">Morph Detector v2.2.8</td>
<td class="px-4 py-2 text-sm">ResNet-50</td>
<td class="px-4 py-2 text-sm">91.7%</td>
<td class="px-4 py-2 text-sm">0.215</td>
<td class="px-4 py-2"><span
class="px-2 py-0.5 text-xs bg-green-900 text-green-300 rounded">Completed</span>
</td>
<td class="px-4 py-2">
<div class="flex space-x-2">
<button class="px-2 py-1 text-xs bg-blue-800 text-white rounded">Deploy</button>
<button class="px-2 py-1 text-xs bg-gray-800 text-white rounded">Logs</button>
</div>
</td>
</tr>
<tr>
<td class="px-4 py-2 text-sm">2025-04-28</td>
<td class="px-4 py-2 text-sm">Verifier v2.1.1</td>
<td class="px-4 py-2 text-sm">EfficientNet-B3</td>
<td class="px-4 py-2 text-sm">95.4%</td>
<td class="px-4 py-2 text-sm">0.112</td>
<td class="px-4 py-2"><span
class="px-2 py-0.5 text-xs bg-green-900 text-green-300 rounded">Completed</span>
</td>
<td class="px-4 py-2">
<div class="flex space-x-2">
<button
class="px-2 py-1 text-xs bg-gray-600 text-white rounded">Deployed</button>
<button class="px-2 py-1 text-xs bg-gray-800 text-white rounded">Logs</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- System Monitoring Panel -->
<div id="panel-monitoring" class="panel hidden">
<!-- Grafana Dashboard Embedding -->
<div class="card rounded-lg p-3 mb-3">
<div class="flex justify-between items-center mb-2">
<h3 class="text-lg font-semibold">System Resources Dashboard</h3>
</div>
<!-- Embedded Model Resources Dashboard -->
<iframe src="/static/dashboards/resources_dashboard.html" class="grafana-container rounded"
title="System Resources Dashboard" frameborder="0" allowfullscreen></iframe>
</div>
<!-- CPU Monitoring Card -->
<div class="card rounded-lg p-3">
<h3 class="text-lg font-semibold mb-2">CPU Usage</h3>
<div class="grid grid-cols-2 gap-4 mb-3">
<div class="p-3 bg-gray-800 rounded">
<p class="text-sm mb-1">Current Usage</p>
<p class="text-2xl font-bold">42%</p>
</div>
<div class="p-3 bg-gray-800 rounded">
<p class="text-sm mb-1">Temperature</p>
<p class="text-2xl font-bold">62°C</p>
</div>
<div class="p-3 bg-gray-800 rounded">
<p class="text-sm mb-1">Processes</p>
<p class="text-2xl font-bold">128</p>
</div>
<div class="p-3 bg-gray-800 rounded">
<p class="text-sm mb-1">Load Average</p>
<p class="text-2xl font-bold">2.14</p>
</div>
</div>
<div class="bg-gray-800 rounded h-48 p-3">
<!-- CPU usage graph would be here -->
<div class="h-full flex items-center justify-center text-gray-500 text-sm">
CPU usage over time chart
</div>
</div>
<div class="mt-3">
<h4 class="text-sm font-medium mb-2">Top Processes</h4>
<div class="bg-gray-900 p-2 rounded font-mono text-xs">
<div class="grid grid-cols-5 gap-2 mb-1">
<div>PID</div>
<div>USER</div>
<div>%CPU</div>
<div>%MEM</div>
<div>COMMAND</div>
</div>
<div class="grid grid-cols-5 gap-2 border-b border-gray-700 py-1">
<div>12345</div>
<div>root</div>
<div>15.2</div>
<div>6.4</div>
<div>python app.py</div>
</div>
<div class="grid grid-cols-5 gap-2 border-b border-gray-700 py-1">
<div>12346</div>
<div>root</div>
<div>8.7</div>
<div>4.2</div>
<div>python train_pl.py</div>
</div>
<div class="grid grid-cols-5 gap-2 py-1">
<div>12347</div>
<div>root</div>
<div>2.1</div>
<div>1.8</div>
<div>nginx</div>
</div>
</div>
</div>
</div>
<!-- GPU Monitoring Card -->
<div class="card rounded-lg p-3">
<h3 class="text-lg font-semibold mb-2">GPU Monitoring</h3>
<div class="grid grid-cols-2 gap-4 mb-3">
<div class="p-3 bg-gray-800 rounded">
<p class="text-sm mb-1">GPU Utilization</p>
<p class="text-2xl font-bold">76%</p>
</div>
<div class="p-3 bg-gray-800 rounded">
<p class="text-sm mb-1">Temperature</p>
<p class="text-2xl font-bold">78°C</p>
</div>
<div class="p-3 bg-gray-800 rounded">
<p class="text-sm mb-1">Memory Used</p>
<p class="text-2xl font-bold">6.2 GB</p>
</div>
<div class="p-3 bg-gray-800 rounded">
<p class="text-sm mb-1">Power Draw</p>
<p class="text-2xl font-bold">185W</p>
</div>
</div>
<div class="bg-gray-800 rounded h-48 p-3">
<!-- GPU usage graph would be here -->
<div class="h-full flex items-center justify-center text-gray-500 text-sm">
GPU utilization over time chart
</div>
</div>
<div class="mt-3">
<h4 class="text-sm font-medium mb-2">GPU Processes</h4>
<div class="bg-gray-900 p-2 rounded font-mono text-xs">
<div class="grid grid-cols-5 gap-2 mb-1">
<div>PID</div>
<div>USER</div>
<div>GPU-MEM</div>
<div>%GPU</div>
<div>COMMAND</div>
</div>
<div class="grid grid-cols-5 gap-2 border-b border-gray-700 py-1">
<div>12346</div>
<div>root</div>
<div>4512MB</div>
<div>72%</div>
<div>python train_pl.py</div>
</div>
<div class="grid grid-cols-5 gap-2 border-b border-gray-700 py-1">
<div>12345</div>
<div>root</div>
<div>1724MB</div>
<div>4%</div>
<div>python app.py</div>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Memory Monitoring Card -->
<div class="card rounded-lg p-3">
<h3 class="text-lg font-semibold mb-2">Memory Usage</h3>
<div class="mb-3">
<div class="flex justify-between mb-1">
<span class="text-sm">System Memory</span>
<span class="text-xs">8.4GB / 32GB (26%)</span>
</div>
<div class="w-full bg-gray-700 rounded-full h-2.5">
<div class="bg-green-500 h-2.5 rounded-full w-26"></div>
</div>
</div>
<div class="mb-3">
<div class="flex justify-between mb-1">
<span class="text-sm">Swap Memory</span>
<span class="text-xs">0.2GB / 8GB (2.5%)</span>
</div>
<div class="w-full bg-gray-700 rounded-full h-2.5">
<div class="bg-green-500 h-2.5 rounded-full w-2-5"></div>
</div>
</div>
<div class="mb-3">
<div class="flex justify-between mb-1">
<span class="text-sm">GPU Memory</span>
<span class="text-xs">6.2GB / 11GB (56%)</span>
</div>
<div class="w-full bg-gray-700 rounded-full h-2.5">
<div class="bg-green-500 h-2.5 rounded-full w-56"></div>
</div>
</div>
<div class="bg-gray-800 rounded h-48 p-3">
<!-- Memory usage graph would be here -->
<div class="h-full flex items-center justify-center text-gray-500 text-sm">
Memory usage over time chart
</div>
</div>
</div>
<!-- Disk Monitoring Card -->
<div class="card rounded-lg p-3">
<h3 class="text-lg font-semibold mb-2">Storage</h3>
<div class="mb-3">
<div class="flex justify-between mb-1">
<span class="text-sm">System Disk (SSD)</span>
<span class="text-xs">412GB / 1TB (41%)</span>
</div>
<div class="w-full bg-gray-700 rounded-full h-2.5">
<div class="bg-green-500 h-2.5 rounded-full w-41"></div>
</div>
</div>
<div class="mb-3">
<div class="flex justify-between mb-1">
<span class="text-sm">Data Disk (HDD)</span>
<span class="text-xs">3.6TB / 8TB (45%)</span>
</div>
<div class="w-full bg-gray-700 rounded-full h-2.5">
<div class="bg-green-500 h-2.5 rounded-full w-45"></div>
</div>
</div>
<div class="bg-gray-800 rounded h-48 p-3">
<!-- Disk usage graph would be here -->
<div class="h-full flex items-center justify-center text-gray-500 text-sm">
Disk I/O over time chart
</div>
</div>
<div class="mt-3">
<h4 class="text-sm font-medium mb-2">Directory Usage</h4>
<div class="bg-gray-900 p-2 rounded font-mono text-xs">
<div class="grid grid-cols-3 gap-2 mb-1">
<div>DIRECTORY</div>
<div>SIZE</div>
<div>% OF TOTAL</div>
</div>
<div class="grid grid-cols-3 gap-2 border-b border-gray-700 py-1">
<div>/data/train</div>
<div>156GB</div>
<div>15.6%</div>
</div>
<div class="grid grid-cols-3 gap-2 border-b border-gray-700 py-1">
<div>/models</div>
<div>98GB</div>
<div>9.8%</div>
</div>
<div class="grid grid-cols-3 gap-2 py-1">
<div>/static/uploads</div>
<div>45GB</div>
<div>4.5%</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Drone-Link Panel -->
<div id="panel-dronelink" class="panel hidden">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-3">
<!-- MAVLink Connection Card -->
<div class="card rounded-lg p-4">
<h3 class="text-lg font-semibold mb-3">🚁 Connection Manager</h3>
<!-- Connection Paths Status -->
<div class="mb-4 p-3 bg-gray-800 rounded-lg">
<div class="flex justify-between items-center mb-2">
<span class="text-sm font-medium text-gray-300">🔗 Active Paths</span>
<span id="active-path-count" class="text-xs px-2 py-0.5 bg-gray-600 rounded">0 Active</span>
</div>
<div class="space-y-2 text-xs">
<div class="flex items-center justify-between p-2 bg-gray-900 rounded">
<div class="flex items-center gap-2">
<span id="path-pi-wifi-indicator" class="w-2 h-2 rounded-full bg-gray-500"></span>
<span>📶 Pi WiFi</span>
</div>
<span id="path-pi-wifi-status" class="text-gray-500">Not Connected</span>
</div>
<div class="flex items-center justify-between p-2 bg-gray-900 rounded">
<div class="flex items-center gap-2">
<span id="path-usb-indicator" class="w-2 h-2 rounded-full bg-gray-500"></span>
<span>🔌 Pixracer USB</span>
</div>
<span id="path-usb-status" class="text-gray-500">Not Connected</span>
</div>
<div class="flex items-center justify-between p-2 bg-gray-900 rounded">
<div class="flex items-center gap-2">
<span id="path-radio-indicator" class="w-2 h-2 rounded-full bg-gray-500"></span>
<span>📻 915MHz Radio</span>
</div>
<span id="path-radio-status" class="text-gray-500">Not Connected</span>
</div>
</div>
</div>
<!-- Quick Connect Presets -->
<div class="mb-3">
<label class="block text-sm font-medium mb-1">Quick Connect</label>
<div class="flex flex-wrap gap-2">
<button onclick="setMavlinkPreset('companion')"
class="px-3 py-1 text-xs rounded bg-purple-700 hover:bg-purple-600">
📡 Companion (Pi WiFi)
</button>
<button onclick="setMavlinkPreset('usb')"
class="px-3 py-1 text-xs rounded bg-green-700 hover:bg-green-600">
🔌 USB (Bench Test)
</button>
<button onclick="setMavlinkPreset('radio')"
class="px-3 py-1 text-xs rounded bg-orange-700 hover:bg-orange-600">
📻 915MHz Radio
</button>
<button onclick="setMavlinkPreset('sitl')"
class="px-3 py-1 text-xs rounded bg-gray-600 hover:bg-gray-500">
🖥️ SITL
</button>
</div>
</div>
<div class="mb-3" id="container-mavlink-uri">
<label class="block text-sm font-medium mb-1">Connection URI</label>
<input type="text" id="mavlink-uri" class="w-full p-2 rounded bg-gray-700 text-white"
value="/dev/ttyACM0" placeholder="serial:/dev/ttyACM0:57600">
</div>
<div class="mb-3">
<label class="block text-sm font-medium mb-1">Connection Type</label>
<select id="mavlink-type" class="w-full p-2 rounded bg-gray-700 text-white"
title="MAVLink connection type" onchange="toggleMavlinkInputs()">
<option value="companion" selected>★ Companion Computer (WiFi/Ethernet)</option>
<option value="usb">USB (Pixracer via /dev/ttyACM0)</option>
<option value="serial">Serial UART (Pi TELEM port)</option>
<option value="udp">UDP (WiFi/915MHz Radio)</option>
<option value="tcp">TCP (Remote Ground Station)</option>
</select>
</div>
<div class="mb-3 hidden" id="container-mavlink-address">
<label class="block text-sm font-medium mb-1">Connection Address</label>
<input type="text" id="mavlink-address" class="w-full p-2 rounded bg-gray-700 text-white"
value="192.168.200.101" placeholder="IP Address or Serial Port">
</div>
<div class="mb-3 hidden" id="baud-container">
<label class="block text-sm font-medium mb-1">Baud Rate</label>
<select id="mavlink-baud" class="w-full p-2 rounded bg-gray-700 text-white"
title="Serial baud rate">
<option value="57600" selected>57600 (USB default)</option>
<option value="115200">115200</option>
<option value="921600">921600 (UART high-speed)</option>
</select>
</div>
<!-- Failover Configuration -->
<div class="mb-3 p-3 bg-gray-800 rounded-lg">
<div class="flex items-center justify-between mb-2">
<label class="text-sm font-medium flex items-center gap-2">
<input type="checkbox" id="enable-failover" class="rounded" checked>
<span>Enable Auto-Failover</span>
</label>
<span class="text-xs text-gray-400">&lt;10s switch</span>
</div>
<div id="failover-options" class="space-y-2 text-sm">
<div class="flex items-center gap-2">
<span class="text-gray-400 w-16">Backup:</span>
<select id="failover-backup" class="flex-1 p-1 rounded bg-gray-700 text-white text-xs"
title="Backup connection path">
<option value="radio">915MHz Radio</option>
<option value="usb">USB Direct</option>
<option value="none">None</option>
</select>
</div>
</div>
</div>
<div class="flex space-x-2">
<button id="drone-connect-btn" class="btn-primary px-4 py-2 rounded flex-1">Connect</button>
</div>
<div class="mt-3 text-sm">
<span class="text-gray-400">Status:</span>
<span id="drone-connection-status" class="ml-2 text-yellow-400">Disconnected</span>
</div>
<!-- Data Source Attribution -->
<div class="mt-2 p-2 bg-gray-800 rounded text-xs">
<div class="flex justify-between">
<span class="text-gray-400">Telemetry Source:</span>
<span id="telemetry-source" class="text-gray-500">--</span>
</div>
<div class="flex justify-between">
<span class="text-gray-400">Video Source:</span>
<span id="video-source" class="text-gray-500">--</span>
</div>
</div>
<div class="mt-2 text-xs text-gray-500">
<span id="mavlink-messages"></span>
</div>
</div>
<!-- Pixracer Telemetry Card -->
<div class="card rounded-lg p-4">
<h3 class="text-lg font-semibold mb-3">📡 Pixracer Telemetry</h3>
<div class="space-y-2 text-sm">
<!-- GPS Row -->
<div class="bg-gray-800 rounded p-2">
<div class="flex justify-between items-center">
<span class="text-gray-400">🛰️ GPS Position</span>
<span id="companion-gps" class="font-mono text-yellow-400">Waiting...</span>
</div>
<div class="flex justify-between items-center mt-1">
<span class="text-gray-500 text-xs">Altitude (MSL)</span>
<span id="telemetry-alt" class="font-mono text-xs">--</span>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-500 text-xs">Relative Alt (AGL)</span>
<span id="telemetry-rel-alt" class="font-mono text-xs">--</span>
</div>
</div>
<!-- Attitude Row -->
<div class="bg-gray-800 rounded p-2">
<div class="text-gray-400 mb-1">🎯 Attitude</div>
<div class="grid grid-cols-3 gap-2 text-center">
<div>
<div class="text-xs text-gray-500">Roll</div>
<div id="telemetry-roll" class="font-mono">--°</div>
</div>
<div>
<div class="text-xs text-gray-500">Pitch</div>
<div id="telemetry-pitch" class="font-mono">--°</div>
</div>
<div>
<div class="text-xs text-gray-500">Yaw/Hdg</div>
<div id="telemetry-yaw" class="font-mono">--°</div>
</div>
</div>
</div>
<!-- Status Row -->
<div class="grid grid-cols-2 gap-2">
<div class="bg-gray-800 rounded p-2">
<div class="text-gray-400 text-xs">🔋 Battery</div>
<div id="telemetry-battery" class="font-mono">--V</div>
</div>
<div class="bg-gray-800 rounded p-2">
<div class="text-gray-400 text-xs">⚡ Armed</div>
<div id="telemetry-armed" class="font-mono text-green-400">Disarmed</div>
</div>
</div>
<!-- System Info -->
<div class="flex justify-between text-xs text-gray-500">
<span>System ID: <span id="telemetry-sysid">--</span></span>
<span>Last Update: <span id="telemetry-timestamp">--</span></span>
</div>
</div>
<div class="mt-3 flex space-x-2">
<button id="companion-ping-btn"
class="text-sm px-3 py-1 rounded bg-gray-700 hover:bg-gray-600 flex-1">
🔄 Refresh Telemetry
</button>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Live Face Detection Feed -->
<div class="card rounded-lg p-4">
<h3 class="text-lg font-semibold mb-3">👤 Live Face Detection</h3>
<div id="drone-feed-container"
class="bg-gray-900 rounded-lg overflow-hidden border border-gray-600 mb-4 min-h-[360px] relative">
<img id="drone-stream-img" class="w-full h-full object-contain"
src="/static/images/placeholder.svg" alt="Drone Stream">
<div id="drone-overlay" class="absolute inset-0 pointer-events-none p-4">
<div class="flex justify-between items-start">
<div class="bg-black/50 p-2 rounded text-xs text-green-400 font-mono">
<p>📡 <span id="drone-status-text">Disconnected</span></p>
<p>GPS: <span id="drone-gps">--</span></p>
<p>Alt: <span id="drone-alt">--</span>m</p>
<p>Batt: <span id="drone-batt">--</span>%</p>
</div>
<div class="bg-black/50 p-2 rounded text-xs text-blue-400 font-mono">
<p>Faces: <span id="drone-face-count">0</span></p>
<p>Morphs: <span id="drone-morph-count">0</span></p>
</div>
</div>
</div>
</div>
<!-- Connection is automatic via Connection Manager -->
<div class="flex items-center justify-center text-sm text-gray-400 py-2">
<span>📡 Video feed connects automatically via Connection Manager</span>
</div>
<div id="ws-debug-status" class="text-xs text-center font-mono text-gray-500 mb-2">WS Status:
Waiting...</div>
<div class="mt-3 flex justify-between text-sm">
<span class="text-gray-400">Stream Quality:</span>
<select id="stream-quality" class="p-1 rounded bg-gray-700 text-white text-xs"
title="Video Stream Quality">
<option value="low">Low (320x240)</option>
<option value="medium" selected>Medium (640x480)</option>
<option value="high">High (1280x720)</option>
</select>
</div>
</div>
</div>
<div class="mt-2 flex space-x-2">
<button id="btn-snapshot-drone"
class="btn-primary w-full py-2 rounded text-sm bg-blue-600 hover:bg-blue-700">
📸 Capture Snapshot
</button>
</div>
<!-- Capture Preview Modal (Hidden by default) -->
<div id="capture-preview-modal"
class="hidden fixed inset-0 bg-black/80 z-50 flex items-center justify-center p-4">
<div class="bg-gray-800 rounded-lg max-w-2xl w-full p-4 border border-gray-600">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold text-white">📸 Captured Snapshot</h3>
<button onclick="document.getElementById('capture-preview-modal').classList.add('hidden')"
class="text-gray-400 hover:text-white"></button>
</div>
<div class="bg-black rounded-lg overflow-hidden mb-4 border border-gray-700">
<img id="capture-preview-img" src="" alt="Captured Snapshot Preview"
class="w-full h-auto object-contain max-h-[60vh]">
</div>
<div class="flex justify-end gap-2">
<a id="download-snapshot-btn" href="#" download="drone-snapshot.jpg"
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded text-sm">
💾 Download
</a>
<button onclick="document.getElementById('capture-preview-modal').classList.add('hidden')"
class="px-4 py-2 bg-gray-600 hover:bg-gray-500 text-white rounded text-sm">
Close
</button>
</div>
</div>
</div>
</div>
<!-- Detection Results Stream -->
<div class="card rounded-lg p-4">
<h3 class="text-lg font-semibold mb-3">🔍 Detection Events</h3>
<div id="detection-events-log" class="bg-gray-900 rounded-lg h-64 overflow-y-auto p-3 text-xs font-mono">
<div class="text-gray-500 text-center py-4">
No detection events yet.
<br>Connect to start receiving face detection alerts.
</div>
</div>
<div class="mt-3 flex items-center space-x-4 text-sm">
<label class="flex items-center cursor-pointer">
<input type="checkbox" id="auto-analyze" class="mr-2" checked>
<span>Auto-analyze in MorphGuard</span>
</label>
<label class="flex items-center cursor-pointer">
<input type="checkbox" id="log-gps" class="mr-2" checked>
<span>Include GPS</span>
</label>
</div>
</div>
</div>
<!-- Radio Link Status -->
<div class="card rounded-lg p-4 mt-4">
<h3 class="text-lg font-semibold mb-3">📻 Radio Link (915MHz)</h3>
<div class="grid grid-cols-4 gap-4 text-sm text-center">
<div>
<div class="text-gray-400 mb-1">Signal Strength</div>
<div id="radio-rssi" class="text-lg font-bold text-green-400">-- dBm</div>
</div>
<div>
<div class="text-gray-400 mb-1">Noise</div>
<div id="radio-noise" class="text-lg font-bold">-- dBm</div>
</div>
<div>
<div class="text-gray-400 mb-1">TX Rate</div>
<div id="radio-tx" class="text-lg font-bold">-- kbps</div>
</div>
<div>
<div class="text-gray-400 mb-1">RX Rate</div>
<div id="radio-rx" class="text-lg font-bold">-- kbps</div>
</div>
</div>
</div>
</div>
<!-- User Management Panel -->
<div id="panel-users" class="panel hidden">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-3">
<!-- User Stats Card -->
<div class="card rounded-lg p-3">
<h3 class="text-lg font-semibold mb-2">User Statistics</h3>
<div class="grid grid-cols-2 gap-4">
<div class="p-3 bg-gray-800 rounded">
<p class="text-sm mb-1">Total Users</p>
<p class="text-2xl font-bold">27</p>
</div>
<div class="p-3 bg-gray-800 rounded">
<p class="text-sm mb-1">Active Now</p>
<p class="text-2xl font-bold">3</p>
</div>
<div class="p-3 bg-gray-800 rounded">
<p class="text-sm mb-1">Admins</p>
<p class="text-2xl font-bold">5</p>
</div>
<div class="p-3 bg-gray-800 rounded">
<p class="text-sm mb-1">Researchers</p>
<p class="text-2xl font-bold">22</p>
</div>
</div>
<div class="mt-4">
<h4 class="text-sm font-medium mb-2">User Roles Distribution</h4>
<div class="w-full bg-gray-800 rounded-full h-4 overflow-hidden">
<div class="flex h-full">
<div class="bg-blue-600 h-full w-18-5"></div>
<div class="bg-green-500 h-full w-44-5"></div>
<div class="bg-yellow-500 h-full w-37"></div>
</div>
</div>
<div class="flex justify-between text-xs mt-1">
<div class="flex items-center">
<div class="w-3 h-3 bg-blue-600 rounded-full mr-1"></div>
<span>Admin (18.5%)</span>
</div>
<div class="flex items-center">
<div class="w-3 h-3 bg-green-500 rounded-full mr-1"></div>
<span>Researcher (44.5%)</span>
</div>
<div class="flex items-center">
<div class="w-3 h-3 bg-yellow-500 rounded-full mr-1"></div>
<span>Viewer (37%)</span>
</div>
</div>
</div>
</div>
<!-- Add User Card -->
<div class="card rounded-lg p-3 md:col-span-2">
<h3 class="text-lg font-semibold mb-2">Add New User</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="mb-3">
<label class="block text-sm font-medium mb-1" for="new-username">Username</label>
<input id="new-username" type="text" class="w-full p-2 rounded" placeholder="e.g., researcher3">
</div>
<div class="mb-3">
<label class="block text-sm font-medium mb-1" for="new-email">Email</label>
<input id="new-email" type="email" class="w-full p-2 rounded"
placeholder="e.g., user@example.com">
</div>
<div class="mb-3">
<label class="block text-sm font-medium mb-1" for="new-password">Password</label>
<input id="new-password" type="password" class="w-full p-2 rounded"
aria-label="New User Password" title="Enter Password">
</div>
<div class="mb-3">
<label class="block text-sm font-medium mb-1" for="new-password-confirm">Confirm
Password</label>
<input id="new-password-confirm" type="password" class="w-full p-2 rounded"
aria-label="Confirm Password" title="Confirm Password">
</div>
<div class="mb-3">
<label class="block text-sm font-medium mb-1" for="new-role">Role</label>
<select id="new-role" class="w-full p-2 rounded" aria-label="User Role"
title="Select User Role">
<option>Admin</option>
<option>Researcher</option>
<option>Viewer</option>
</select>
</div>
<div class="mb-3">
<label class="block text-sm font-medium mb-1" for="new-department">Department</label>
<input id="new-department" type="text" class="w-full p-2 rounded" placeholder="e.g., R&D">
</div>
</div>
<div class="mb-3">
<label class="block text-sm font-medium mb-1">Permissions</label>
<div class="grid grid-cols-2 md:grid-cols-3 gap-2">
<div class="flex items-center">
<div class="custom-checkbox checked"></div>
<label class="text-xs">View Datasets</label>
</div>
<div class="flex items-center">
<div class="custom-checkbox checked"></div>
<label class="text-xs">Upload Datasets</label>
</div>
<div class="flex items-center">
<div class="custom-checkbox checked"></div>
<label class="text-xs">View Models</label>
</div>
<div class="flex items-center">
<div class="custom-checkbox"></div>
<label class="text-xs">Train Models</label>
</div>
<div class="flex items-center">
<div class="custom-checkbox"></div>
<label class="text-xs">Modify Systems</label>
</div>
<div class="flex items-center">
<div class="custom-checkbox"></div>
<label class="text-xs">Manage Users</label>
</div>
</div>
</div>
<div class="flex justify-end">
<button id="add-user-btn" class="btn-primary px-4 py-2 rounded font-medium">Add
User</button>
</div>
</div>
</div>
<!-- User List Card -->
<div class="card rounded-lg p-3">
<div class="flex justify-between items-center mb-3">
<h3 class="text-lg font-semibold">User List</h3>
<div class="flex space-x-2">
<select class="p-1 text-sm rounded" aria-label="Filter User Role" title="Filter by Role">
<option>All Roles</option>
<option>Admin</option>
<option>Researcher</option>
<option>Viewer</option>
</select>
<input type="text" placeholder="Search..." class="p-1 text-sm rounded" aria-label="Search Users"
title="Search for Users">
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full">
<thead>
<tr class="border-b border-gray-700">
<th class="px-4 py-2 text-left text-xs text-gray-400">Username</th>
<th class="px-4 py-2 text-left text-xs text-gray-400">Email</th>
<th class="px-4 py-2 text-left text-xs text-gray-400">Role</th>
<th class="px-4 py-2 text-left text-xs text-gray-400">Last Login</th>
<th class="px-4 py-2 text-left text-xs text-gray-400">Status</th>
<th class="px-4 py-2 text-left text-xs text-gray-400">Actions</th>
</tr>
</thead>
<tbody>
<tr class="border-b border-gray-700">
<td class="px-4 py-2 text-sm">admin</td>
<td class="px-4 py-2 text-sm">admin@morphguard.com</td>
<td class="px-4 py-2"><span
class="px-2 py-0.5 text-xs bg-blue-900 text-blue-300 rounded">Admin</span>
</td>
<td class="px-4 py-2 text-sm">2025-05-15 06:12:33</td>
<td class="px-4 py-2"><span
class="px-2 py-0.5 text-xs bg-green-900 text-green-300 rounded">Active</span>
</td>
<td class="px-4 py-2">
<div class="flex space-x-2">
<button class="px-2 py-1 text-xs bg-blue-800 text-white rounded">Edit</button>
<button class="px-2 py-1 text-xs bg-red-800 text-white rounded">Disable</button>
</div>
</td>
</tr>
<tr class="border-b border-gray-700">
<td class="px-4 py-2 text-sm">researcher1</td>
<td class="px-4 py-2 text-sm">researcher1@morphguard.com</td>
<td class="px-4 py-2"><span
class="px-2 py-0.5 text-xs bg-green-900 text-green-300 rounded">Researcher</span>
</td>
<td class="px-4 py-2 text-sm">2025-05-15 04:08:52</td>
<td class="px-4 py-2"><span
class="px-2 py-0.5 text-xs bg-green-900 text-green-300 rounded">Active</span>
</td>
<td class="px-4 py-2">
<div class="flex space-x-2">
<button class="px-2 py-1 text-xs bg-blue-800 text-white rounded">Edit</button>
<button class="px-2 py-1 text-xs bg-red-800 text-white rounded">Disable</button>
</div>
</td>
</tr>
<tr class="border-b border-gray-700">
<td class="px-4 py-2 text-sm">viewer1</td>
<td class="px-4 py-2 text-sm">viewer1@morphguard.com</td>
<td class="px-4 py-2"><span
class="px-2 py-0.5 text-xs bg-yellow-900 text-yellow-300 rounded">Viewer</span>
</td>
<td class="px-4 py-2 text-sm">2025-05-14 15:42:18</td>
<td class="px-4 py-2"><span
class="px-2 py-0.5 text-xs bg-green-900 text-green-300 rounded">Active</span>
</td>
<td class="px-4 py-2">
<div class="flex space-x-2">
<button class="px-2 py-1 text-xs bg-blue-800 text-white rounded">Edit</button>
<button class="px-2 py-1 text-xs bg-red-800 text-white rounded">Disable</button>
</div>
</td>
</tr>
<tr class="border-b border-gray-700">
<td class="px-4 py-2 text-sm">researcher2</td>
<td class="px-4 py-2 text-sm">researcher2@morphguard.com</td>
<td class="px-4 py-2"><span
class="px-2 py-0.5 text-xs bg-green-900 text-green-300 rounded">Researcher</span>
</td>
<td class="px-4 py-2 text-sm">2025-05-13 09:23:05</td>
<td class="px-4 py-2"><span
class="px-2 py-0.5 text-xs bg-red-900 text-red-300 rounded">Inactive</span>
</td>
<td class="px-4 py-2">
<div class="flex space-x-2">
<button class="px-2 py-1 text-xs bg-blue-800 text-white rounded">Edit</button>
<button class="px-2 py-1 text-xs bg-green-800 text-white rounded">Enable</button>
</div>
</td>
</tr>
<tr>
<td class="px-4 py-2 text-sm">admin2</td>
<td class="px-4 py-2 text-sm">admin2@morphguard.com</td>
<td class="px-4 py-2"><span
class="px-2 py-0.5 text-xs bg-blue-900 text-blue-300 rounded">Admin</span>
</td>
<td class="px-4 py-2 text-sm">2025-05-10 11:14:27</td>
<td class="px-4 py-2"><span
class="px-2 py-0.5 text-xs bg-green-900 text-green-300 rounded">Active</span>
</td>
<td class="px-4 py-2">
<div class="flex space-x-2">
<button class="px-2 py-1 text-xs bg-blue-800 text-white rounded">Edit</button>
<button class="px-2 py-1 text-xs bg-red-800 text-white rounded">Disable</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="flex justify-between mt-4">
<button class="text-sm text-gray-400 hover:text-white">← Previous</button>
<div class="text-sm">Page 1 of 3</div>
<button class="text-sm text-gray-400 hover:text-white">Next →</button>
</div>
</div>
</div>
</div>
<!-- Load JavaScript -->
<script src="{{ url_for('static', filename='js/drone-feed.js') }}?v=6"></script>
<script src="/static/js/dataset_browser.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="/static/js/flagged_images.js"></script>
<script src="/static/js/dataset_download.js"></script>
<script>
// Tab switching
document.querySelectorAll('#tab-dashboard, #tab-dataset, #tab-training, #tab-monitoring, #tab-users').forEach(tab => {
tab.addEventListener('click', function () {
// Reset tab styles
document.querySelectorAll('#tab-dashboard, #tab-dataset, #tab-training, #tab-monitoring, #tab-users').forEach(t => {
t.classList.remove('tab-active');
t.classList.add('text-gray-400');
});
// Set active tab
this.classList.add('tab-active');
this.classList.remove('text-gray-400');
// Hide all panels
document.querySelectorAll('.panel').forEach(p => p.classList.add('hidden'));
// Show selected panel
const panelId = this.id.replace('tab-', 'panel-');
document.getElementById(panelId).classList.remove('hidden');
});
});
// Custom checkbox functionality
document.querySelectorAll('.custom-checkbox').forEach(checkbox => {
checkbox.addEventListener('click', function () {
this.classList.toggle('checked');
});
});
// Add more JavaScript functionality as needed for dashboard interactions
// API Key Management
document.getElementById('saveKeys').onclick = async () => {
try {
const res = await fetch('/api/setup/keys', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
access_key: document.getElementById('accessKey').value.trim(),
secret_key: document.getElementById('secretKey').value.trim(),
pexels_key: document.getElementById('pexelsKey').value.trim(),
})
});
const data = await res.json();
if (res.ok && data.status === 'success') alert('API keys saved.');
else alert('Error saving keys: ' + (data.error || res.statusText));
} catch (e) { alert('Error saving keys: ' + e); }
};
// Data Ingestion & Bootstrapping events
document.getElementById('fetchReal').onclick = async () => {
const count = document.getElementById('realCount').value;
try {
const res = await fetch('/api/setup/fetch_real', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ count }) });
const data = await res.json();
if (res.ok) alert(`Downloaded ${data.downloaded || data.downloaded} real images.`);
else alert('Error: ' + data.error);
} catch (e) { alert('Error fetching real faces: ' + e); }
};
document.getElementById('genMorphs').onclick = async () => {
const count = document.getElementById('morphCount').value;
try {
const res = await fetch('/api/setup/generate_morphs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ count }) });
const data = await res.json();
if (res.ok) alert(`Generated ${data.generated || data.generated} morph images.`);
else alert('Error: ' + data.error);
} catch (e) { alert('Error generating morphs: ' + e); }
};
document.getElementById('fetchValReal').onclick = async () => {
const count = document.getElementById('valRealCount').value;
try {
const res = await fetch('/api/setup/fetch_real', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ count, split: 'val' }) });
const data = await res.json();
if (res.ok) alert(`Downloaded ${data.downloaded} validation real images.`);
else alert('Error: ' + data.error);
} catch (e) { alert('Error fetching validation real faces: ' + e); }
};
document.getElementById('genValMorphs').onclick = async () => {
const count = document.getElementById('valMorphCount').value;
try {
const res = await fetch('/api/setup/generate_morphs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ count, split: 'val' }) });
const data = await res.json();
if (res.ok) alert(`Generated ${data.generated} validation morph images.`);
else alert('Error: ' + data.error);
} catch (e) { alert('Error generating validation morphs: ' + e); }
};
document.getElementById('fetchLFW').onclick = async () => {
const count = document.getElementById('lfwCount').value;
try {
const res = await fetch('/api/setup/fetch_lfw', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ count, split: 'train' }) });
const data = await res.json();
if (res.ok) alert(`Downloaded ${data.downloaded} LFW images.`);
else alert('Error: ' + data.error);
} catch (e) { alert('Error fetching LFW faces: ' + e); }
};
document.getElementById('fetchPexels').onclick = async () => {
const count = document.getElementById('pexelsCount').value;
try {
const res = await fetch('/api/setup/fetch_pexels', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ count, split: 'train' }) });
const data = await res.json();
if (res.ok) alert(`Downloaded ${data.downloaded} Pexels images.`);
else alert('Error: ' + data.error);
} catch (e) { alert('Error fetching Pexels faces: ' + e); }
};
document.getElementById('fetchUTKFace').onclick = async () => {
const count = document.getElementById('utkCount').value;
try {
const res = await fetch('/api/setup/fetch_utkface', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ count, split: 'train' }) });
const data = await res.json();
if (res.ok) alert(`Downloaded ${data.downloaded} UTKFace images.`);
else alert('Error: ' + data.error);
} catch (e) { alert('Error fetching UTKFace faces: ' + e); }
};
document.getElementById('fetchCelebA').onclick = async () => {
const count = document.getElementById('celebaCount').value;
try {
const res = await fetch('/api/setup/fetch_celeba', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ count, split: 'train' }) });
const data = await res.json();
if (res.ok) alert(`Fetched ${data.fetched} CelebA images.`);
else alert('Error: ' + data.error);
} catch (e) { alert('Error fetching CelebA faces: ' + e); }
};
document.getElementById('fetchVGGFace2').onclick = async () => {
try {
const res = await fetch('/api/setup/fetch_vggface2', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ count: 0, split: 'train' }) });
const data = await res.json();
if (res.ok) alert(`Fetched ${data.fetched} VGGFace2 images.`);
else alert('Error: ' + data.error);
} catch (e) { alert('Error fetching VGGFace2 faces: ' + e); }
};
document.getElementById('bootstrapData').onclick = async () => {
const trainCount = parseInt(document.getElementById('bootstrapTrainCount').value, 10);
const valCount = parseInt(document.getElementById('bootstrapValCount').value, 10);
try {
await fetch('/api/setup/fetch_real', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ count: trainCount, split: 'train' }) });
await fetch('/api/setup/generate_morphs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ count: trainCount, split: 'train' }) });
await fetch('/api/setup/fetch_real', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ count: valCount, split: 'val' }) });
await fetch('/api/setup/generate_morphs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ count: valCount, split: 'val' }) });
alert('Dataset bootstrap complete.');
} catch (e) { alert('Error bootstrapping dataset: ' + e); }
};
// Real-time system info polling
async function loadSystemInfo() {
try {
const resCpu = await fetch('/api/devices/cpu'); const cpu = await resCpu.json();
const cpuEl = document.querySelector('#panel-dashboard .card:nth-child(3) .text-xs:nth-child(1) span');
if (cpuEl) cpuEl.textContent = cpu.cpu_percent + '%';
const memEl = document.querySelector('#panel-dashboard .card:nth-child(3) .text-xs:nth-child(2) span');
if (memEl) memEl.textContent = `${cpu.mem_used_mb}/${cpu.mem_total_mb}MB`;
} catch (e) { }
}
loadSystemInfo(); setInterval(loadSystemInfo, 10000);
// Training start and sync events
const startTrainingBtn = document.getElementById('startTraining');
if (startTrainingBtn) startTrainingBtn.onclick = async () => {
const payload = {
epochs: parseInt(document.getElementById('trainEpochs').value, 10),
batch_size: parseInt(document.getElementById('trainBatchSize').value, 10),
lr: parseFloat(document.getElementById('trainLR').value),
model_name: document.getElementById('trainModel').value,
gpu: parseInt(document.getElementById('trainGPU').value, 10),
auto_retrain: document.getElementById('autoRetrain').classList.contains('checked'),
optimizer: document.getElementById('trainOptimizer').value,
weight_decay: parseFloat(document.getElementById('trainWeightDecay').value),
scheduler: document.getElementById('trainScheduler').value,
step_size: parseInt(document.getElementById('schedStepSize').value, 10),
gamma: parseFloat(document.getElementById('schedGamma').value),
tmax: parseInt(document.getElementById('schedTmax').value, 10),
early_stopping: parseInt(document.getElementById('trainEarlyStop').value, 10),
aug_flip: document.getElementById('augFlip').classList.contains('checked'),
aug_rotation: parseFloat(document.getElementById('augRotation').value),
aug_crop_min: parseFloat(document.getElementById('augCropMin').value),
aug_crop_max: parseFloat(document.getElementById('augCropMax').value)
};
// Preview command
const cmdParts = [
'python scripts/train_detector.py',
'--data-dir data',
`--epochs ${payload.epochs}`,
`--batch-size ${payload.batch_size}`,
`--lr ${payload.lr}`,
`--model-name ${payload.model_name}`,
`--gpu ${payload.gpu}`,
`--optimizer ${payload.optimizer}`,
`--weight-decay ${payload.weight_decay}`,
`--scheduler ${payload.scheduler}`,
`--step-size ${payload.step_size}`,
`--gamma ${payload.gamma}`,
`--tmax ${payload.tmax}`,
`--early-stopping ${payload.early_stopping}`
];
if (payload.aug_flip) cmdParts.push('--aug-flip');
if (payload.aug_rotation > 0) cmdParts.push(`--aug-rotation ${payload.aug_rotation}`);
if (payload.aug_crop_min < 1.0 || payload.aug_crop_max < 1.0) {
cmdParts.push(`--aug-crop-min ${payload.aug_crop_min}`, `--aug-crop-max ${payload.aug_crop_max}`);
}
cmdParts.push('--save-path models/morph_detector.pth', '--stats-file training_stats.json', '--job-id detector');
document.getElementById('trainCommand').textContent = cmdParts.join(' ');
try {
const res = await fetch('/api/train/detector', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
const data = await res.json();
if (res.ok) alert(`Training started (PID: ${data.pid})`);
else alert('Error: ' + data.error);
} catch (e) { alert('Error starting training: ' + e); }
};
document.getElementById('syncFlags').onclick = async () => {
try {
const res = await fetch('/api/setup/sync_flags', { method: 'POST' });
const data = await res.json();
if (res.ok) alert(`Synced ${data.synced} flagged images (skipped ${data.skipped}).`);
else alert('Error syncing flagged images: ' + (data.error || res.statusText));
} catch (e) { alert('Error syncing flagged images: ' + e); }
};
document.getElementById('trainScheduler').addEventListener('change', function () {
document.getElementById('schedulerParams').style.display = this.value === 'none' ? 'none' : 'block';
});
</script>
<!-- Alerts Banner Script -->
<script>
document.addEventListener('DOMContentLoaded', function () {
fetch('/api/alerts')
.then(res => res.json())
.then(json => {
if (json.alerts && json.alerts.length) {
const banner = document.getElementById('alerts-banner');
banner.textContent = json.alerts[0].message || 'Alert: check system logs';
banner.classList.remove('hidden');
}
})
.catch(err => console.error('Failed to load alerts:', err));
});
</script>
<!-- User Management Script -->
<script>
document.addEventListener('DOMContentLoaded', function () {
// Load user list
function loadUsers() {
fetch('/api/setup/users')
.then(res => res.json())
.then(data => {
const tbody = document.getElementById('user-table-body');
tbody.innerHTML = '';
data.users.forEach(u => {
const tr = document.createElement('tr');
tr.className = 'border-b border-gray-700';
const status = u.active ? 'Active' : 'Inactive';
const statusColor = u.active ? 'green' : 'red';
const roleColor = u.type === 'Admin' ? 'blue' : u.type === 'Researcher' ? 'green' : 'yellow';
tr.innerHTML = `
<td class="px-4 py-2 text-sm">${u.username}</td>
<td class="px-4 py-2 text-sm">${u.email || ''}</td>
<td class="px-4 py-2"><span class="px-2 py-0.5 text-xs bg-${roleColor}-900 text-${roleColor}-300 rounded">${u.type}</span></td>
<td class="px-4 py-2 text-sm">${u.last_login || ''}</td>
<td class="px-4 py-2"><span class="px-2 py-0.5 text-xs bg-${statusColor}-900 text-${statusColor}-300 rounded">${status}</span></td>
<td class="px-4 py-2"><div class="flex space-x-2">
<button data-id="${u.id}" class="btn-toggle-status px-2 py-1 text-xs ${u.active ? 'bg-red-800' : 'bg-green-800'} text-white rounded">${u.active ? 'Disable' : 'Enable'}</button>
</div></td>
`;
tbody.appendChild(tr);
});
// Attach toggle handlers
document.querySelectorAll('.btn-toggle-status').forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.getAttribute('data-id');
const action = btn.textContent.trim().toLowerCase();
let url = '/api/setup/users/';
let payload = { id: id };
if (action === 'disable') {
url += 'delete';
} else {
url += 'update';
payload.role = 'user';
}
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}).then(() => loadUsers());
});
});
});
}
// Initial load
loadUsers();
// Handle Add User
document.getElementById('add-user-btn').addEventListener('click', () => {
const username = document.getElementById('new-username').value;
const email = document.getElementById('new-email').value;
const password = document.getElementById('new-password').value;
const confirm = document.getElementById('new-password-confirm').value;
const role = document.getElementById('new-role').value;
if (!username || !password || password !== confirm) {
alert('Username and matching passwords are required');
return;
}
fetch('/api/setup/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password, email, role })
})
.then(res => res.json())
.then(json => {
if (json.error) {
alert('Error: ' + json.error);
} else {
loadUsers();
alert('User added successfully');
}
});
});
});
</script>
<!-- Grafana health check to toggle live embed -->
<script>
document.addEventListener('DOMContentLoaded', function () {
const metricsIframe = document.getElementById('grafana-metrics-iframe');
const metricsDown = document.getElementById('grafana-down-msg');
const mapIframe = document.getElementById('grafana-model-map-iframe');
const mapDown = document.getElementById('grafana-model-map-down-msg');
fetch('/grafana/api/health')
.then(res => {
if (res.status !== 200) {
metricsIframe.classList.add('hidden');
metricsDown.classList.remove('hidden');
mapIframe.classList.add('hidden');
mapDown.classList.remove('hidden');
}
})
.catch(() => {
metricsIframe.classList.add('hidden');
metricsDown.classList.remove('hidden');
mapIframe.classList.add('hidden');
mapDown.classList.remove('hidden');
});
});
</script>
<!-- Button Loading State Helper -->
<script>
function setButtonLoading(button, isLoading) {
if (isLoading) {
button.disabled = true;
button.classList.add('btn-loading');
button.dataset.originalText = button.textContent;
button.textContent = 'Processing...';
} else {
button.disabled = false;
button.classList.remove('btn-loading');
button.textContent = button.dataset.originalText || button.textContent;
}
}
</script>
<!-- Training Button Handler -->
<script>
document.addEventListener('DOMContentLoaded', () => {
const trainBtn = document.querySelector('.start-training-btn');
if (trainBtn) {
trainBtn.addEventListener('click', async () => {
setButtonLoading(trainBtn, true);
const config = {
model_name: document.querySelector('[name="model"]')?.value || 'vit_base_patch16_224',
epochs: parseInt(document.querySelector('[name="epochs"]')?.value) || 10,
batch_size: parseInt(document.querySelector('[name="batch_size"]')?.value) || 32,
learning_rate: parseFloat(document.querySelector('[name="learning_rate"]')?.value) || 0.0001,
optimizer: document.querySelector('select')?.value || 'Adam'
};
try {
const response = await fetch('/api/train/detector', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
const result = await response.json();
if (result.status === 'started') {
alert('Training started successfully!');
} else {
alert('Error: ' + (result.error || 'Unknown error'));
}
} catch (error) {
alert('Failed to start training: ' + error.message);
} finally {
setButtonLoading(trainBtn, false);
}
});
}
});
</script>
<!-- Stepper Navigation Script -->
<script>
document.addEventListener('DOMContentLoaded', function () {
const steps = document.querySelectorAll('#setup-stepper .stepper-step');
steps.forEach(stepEl => {
stepEl.addEventListener('click', () => {
// Highlight active step
steps.forEach(s => s.classList.remove('setup-step-active'));
stepEl.classList.add('setup-step-active');
// Show corresponding panel
const target = stepEl.getAttribute('data-step');
document.querySelectorAll('.panel').forEach(p => p.classList.add('hidden'));
const panel = document.getElementById('panel-' + target);
if (panel) panel.classList.remove('hidden');
});
});
});
</script>
<!-- WebSocket Real-Time Updates -->
<script>
// Initialize Socket.IO connection (optional - may not be available)
let socket = null;
try {
if (typeof io !== 'undefined') {
socket = io({ reconnection: true, reconnectionAttempts: 3, timeout: 5000 });
socket.on('connect_error', () => {
console.warn('Socket.IO connection failed, real-time updates disabled');
});
}
} catch (e) {
console.warn('Socket.IO not available:', e.message);
}
// Only set up listeners if socket connected
if (socket) {
// Listen for training updates
socket.on('training_update', (data) => {
// Update progress bar
const progressBar = document.querySelector('.training-progress-bar');
if (progressBar) {
progressBar.style.width = `${data.progress}%`;
progressBar.textContent = `${data.progress}%`;
}
// Update epoch/loss display
const currentEpoch = document.getElementById('current-epoch');
if (currentEpoch) {
currentEpoch.textContent = `Epoch: ${data.epoch}/${data.total_epochs}`;
}
const currentLoss = document.getElementById('current-loss');
if (currentLoss) {
currentLoss.textContent = `Loss: ${data.loss.toFixed(4)}`;
}
const currentAccuracy = document.getElementById('current-accuracy');
if (currentAccuracy) {
currentAccuracy.textContent = `Accuracy: ${(data.accuracy * 100).toFixed(2)}%`;
}
});
// Listen for GPU metrics
socket.on('gpu_metrics', (data) => {
const gpuUsage = document.getElementById('gpu-usage');
if (gpuUsage) {
gpuUsage.textContent = `${data.utilization}%`;
}
const gpuMemory = document.getElementById('gpu-memory');
if (gpuMemory) {
gpuMemory.textContent = `${data.memory_used}/${data.memory_total} MB`;
}
const gpuTemp = document.getElementById('gpu-temp');
if (gpuTemp) {
gpuTemp.textContent = `${data.temperature}°C`;
}
});
// Listen for detection results
socket.on('detection_result', (data) => {
// Show toast notification
const message = data.type === 'deepfake'
? `Deepfake Detection: ${data.is_deepfake ? 'Synthetic' : 'Genuine'} (${(data.confidence * 100).toFixed(1)}%)`
: `Detection complete: ${data.is_morphed ? 'Morphed' : 'Genuine'} (${(data.confidence * 100).toFixed(1)}%)`;
showToast(message);
});
function showToast(message) {
const toast = document.createElement('div');
toast.className = 'fixed top-4 right-4 bg-blue-600 text-white px-4 py-2 rounded shadow-lg z-50';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
} // Close if (socket) block
</script>
<!-- Drone-Link Tab JavaScript -->
<script>
// Drone-Link State
const droneState = {
connected: false,
statusInterval: null,
eventsInterval: null,
telemetryInterval: null
};
// Toggle inputs based on connection type
function toggleMavlinkInputs() {
const type = document.getElementById('mavlink-type').value;
const uriContainer = document.getElementById('container-mavlink-uri');
const addrContainer = document.getElementById('container-mavlink-address');
const baudContainer = document.getElementById('baud-container');
if (type === 'usb' || type === 'serial') {
uriContainer.classList.remove('hidden');
baudContainer.classList.remove('hidden');
addrContainer.classList.add('hidden');
} else {
uriContainer.classList.add('hidden');
baudContainer.classList.add('hidden');
addrContainer.classList.remove('hidden');
}
}
document.addEventListener('DOMContentLoaded', () => {
const connectBtn = document.getElementById('drone-connect-btn');
const disconnectBtn = document.getElementById('drone-disconnect-btn');
const pingBtn = document.getElementById('companion-ping-btn');
const mavlinkUri = document.getElementById('mavlink-uri');
const mavlinkType = document.getElementById('mavlink-type');
const statusEl = document.getElementById('drone-connection-status');
if (!connectBtn) return; // Not on drone-link page
// Telemetry polling disabled - WebSocket provides telemetry
// startTelemetryPolling();
// Connect/Disconnect Toggle Handler
connectBtn.addEventListener('click', async () => {
const isConnected = connectBtn.textContent === 'Disconnect';
if (isConnected) {
// DISCONNECT LOGIC
try {
connectBtn.disabled = true;
connectBtn.textContent = 'Disconnecting...';
// Stop Video Feed via WS close
if (typeof droneFeed !== 'undefined' && droneFeed.disconnect) {
try { droneFeed.disconnect(); } catch (e) { console.error("DroneFeed disconnect error:", e); }
}
// Try to disconnect paths - ignore errors
try {
await fetch('/api/drone/paths/disconnect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
} catch (e) { console.warn("Paths disconnect warning:", e); }
// Disconnect legacy - ignore errors
try {
await fetch('/api/drone/disconnect', { method: 'POST' });
} catch (e) { console.warn("Legacy disconnect warning:", e); }
// Force UI Reset regardless of API response
droneState.connected = false;
statusEl.textContent = 'Disconnected';
statusEl.className = 'ml-2 text-yellow-400';
// Reset Button UI
connectBtn.disabled = false;
connectBtn.textContent = 'Connect';
connectBtn.classList.remove('bg-red-700', 'text-white');
connectBtn.classList.add('btn-primary');
connectBtn.style.backgroundColor = ''; // Clear inline styles
// Stop all polling
stopPolling();
stopPathStatusPolling();
// Reset UI indicators
if (typeof pathState !== 'undefined') {
pathState.piWifi.connected = false;
pathState.usb.connected = false;
pathState.radio.connected = false;
}
updatePathStatus('pi-wifi', false);
updatePathStatus('usb', false);
updatePathStatus('radio', false);
updateDataSources(null, null);
showToast('🔌 Disconnected');
} catch (e) {
console.error("Disconnect Error:", e);
// Fallback reset
connectBtn.disabled = false;
connectBtn.textContent = 'Connect';
showToast('⚠️ Disconnected (with warnings)');
}
} else {
// CONNECT LOGIC
const connType = mavlinkType.value || 'usb';
const baud = document.getElementById('mavlink-baud')?.value || 57600;
const failoverEnabled = document.getElementById('enable-failover')?.checked ?? true;
const backupPath = document.getElementById('failover-backup')?.value || 'radio';
let uri;
if (connType === 'usb' || connType === 'serial') {
uri = mavlinkUri.value || '/dev/ttyACM0';
} else {
uri = document.getElementById('mavlink-address').value || '192.168.200.101';
}
// Map UI connection type to API path type
const pathTypeMap = {
'companion': 'pi_wifi',
'usb': 'usb',
'serial': 'serial',
'udp': 'radio_915',
'tcp': 'tcp'
};
const pathType = pathTypeMap[connType] || 'pi_wifi';
connectBtn.disabled = true;
connectBtn.textContent = 'Connecting...';
statusEl.textContent = 'Connecting...';
statusEl.className = 'ml-2 text-blue-400';
try {
// Use new Connection Manager API
const response = await fetch('/api/drone/paths/connect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
path_type: pathType,
uri: uri,
baud: parseInt(baud),
set_primary: true
})
});
const data = await response.json();
if (response.ok) {
droneState.connected = true;
droneState.connType = connType;
droneState.pathType = pathType;
statusEl.textContent = `Connected (${pathType})`;
statusEl.className = 'ml-2 text-green-400';
// Update Button to Disconnect State
connectBtn.disabled = false;
connectBtn.textContent = 'Disconnect';
connectBtn.classList.remove('btn-primary');
connectBtn.classList.add('bg-red-700', 'text-white');
// Configure failover if enabled
if (failoverEnabled && backupPath !== 'none') {
const backupPathType = pathTypeMap[backupPath] || 'radio_915';
await fetch('/api/drone/paths/failover', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
enabled: true,
backup_path: backupPathType,
timeout_seconds: 10
})
});
}
// Fetch and update all path statuses
await fetchPathStatuses();
startStatusPolling();
// NOTE: Telemetry polling DISABLED - WebSocket already provides telemetry via video feed
// startTelemetryPolling();
startPathStatusPolling();
// Start video feed WebSocket automatically
if (typeof droneFeed !== 'undefined' && droneFeed.connect) {
try {
droneFeed.connect();
} catch (err) {
console.error("Error connecting video feed:", err);
}
}
showToast(`✅ Connected via ${pathType}!`);
} else {
throw new Error(data.error || 'Connection failed');
}
} catch (e) {
statusEl.textContent = e.message || 'Connection Failed';
statusEl.className = 'ml-2 text-red-400';
connectBtn.disabled = false;
connectBtn.textContent = 'Connect';
showToast(`❌ ${e.message || 'Failed to connect'}`);
}
}
});
// Ping companion button
if (pingBtn) {
pingBtn.addEventListener('click', async () => {
pingBtn.textContent = '⏳ Pinging...';
try {
const response = await fetch('/api/drone/status');
const data = await response.json();
updateCompanionStatus(data);
pingBtn.textContent = '🔄 Ping Companion';
showToast('📡 Companion status updated');
} catch (e) {
pingBtn.textContent = '❌ Ping Failed';
setTimeout(() => pingBtn.textContent = '🔄 Ping Companion', 2000);
}
});
}
// Snapshot Button Logic
const snapBtn = document.getElementById('btn-snapshot-drone');
if (snapBtn) {
snapBtn.addEventListener('click', () => {
if (typeof droneFeed !== 'undefined' && droneFeed.captureFrame) {
const frame = droneFeed.captureFrame();
if (frame) {
const modal = document.getElementById('capture-preview-modal');
const img = document.getElementById('capture-preview-img');
const dl = document.getElementById('download-snapshot-btn');
if (modal && img) {
img.src = frame;
modal.classList.remove('hidden');
if (dl) {
dl.href = frame;
dl.download = `drone_snap_${new Date().toISOString()}.jpg`;
}
}
} else {
showToast('⚠️ No video frame available to capture');
}
} else {
showToast('❌ Drone feed library not ready');
}
});
}
});
function startStatusPolling() {
droneState.statusInterval = setInterval(async () => {
try {
const response = await fetch('/api/drone/status');
const data = await response.json();
updateCompanionStatus(data);
} catch (e) {
console.error('Status poll error:', e);
}
}, 2000);
}
function startEventsPolling() {
droneState.eventsInterval = setInterval(async () => {
try {
const response = await fetch('/api/drone/events?limit=20');
const data = await response.json();
updateEventsLog(data.events);
} catch (e) {
console.error('Events poll error:', e);
}
}, 1000);
}
function startTelemetryPolling() {
console.log('[DEBUG] Starting telemetry polling...');
droneState.telemetryInterval = setInterval(async () => {
try {
const response = await fetch('/api/drone/mavlink/telemetry');
console.log('[DEBUG] Telemetry response status:', response.status);
if (response.ok) {
const data = await response.json();
console.log('[DEBUG] Telemetry data received:', data);
updateTelemetryDisplay(data);
} else {
console.warn('[DEBUG] Telemetry response not ok:', response.status);
}
} catch (e) {
console.error('Telemetry poll error:', e);
}
}, 1000);
}
// Path status polling (placeholder - paths managed by Connection Manager)
function startPathStatusPolling() {
console.log('[DEBUG] Starting path status polling...');
// Poll path statuses every 5 seconds
setInterval(async () => {
try {
const response = await fetch('/api/drone/paths');
if (response.ok) {
const data = await response.json();
// Update path status indicators if needed
console.log('[DEBUG] Path status:', data);
}
} catch (e) {
console.warn('Path status poll error:', e);
}
}, 5000);
}
function updateTelemetryDisplay(data) {
console.log('[DEBUG] updateTelemetryDisplay called with:', data);
// GPS
const gpsEl = document.getElementById('companion-gps');
console.log('[DEBUG] gpsEl found:', !!gpsEl, 'data.gps:', data.gps);
if (gpsEl && data.gps) {
gpsEl.textContent = `${data.gps.lat.toFixed(6)}, ${data.gps.lon.toFixed(6)}`;
gpsEl.className = 'font-mono text-green-400';
}
// Altitude
const altEl = document.getElementById('telemetry-alt');
if (altEl && data.gps) {
altEl.textContent = `${data.gps.alt.toFixed(1)}m`;
}
const relAltEl = document.getElementById('telemetry-rel-alt');
if (relAltEl && data.gps) {
relAltEl.textContent = `${data.gps.relative_alt.toFixed(1)}m`;
}
// Attitude - Support Flat and Nested
const rollEl = document.getElementById('telemetry-roll');
const pitchEl = document.getElementById('telemetry-pitch');
const yawEl = document.getElementById('telemetry-yaw');
if (data.attitude) {
if (rollEl) rollEl.textContent = `${data.attitude.roll.toFixed(1)}°`;
if (pitchEl) pitchEl.textContent = `${data.attitude.pitch.toFixed(1)}°`;
if (yawEl) yawEl.textContent = `${(data.gps?.hdg || data.attitude.yaw).toFixed(1)}°`;
} else if (data.roll != undefined) {
// Flat structure support (Common in direct MAVLink relay)
// Check if radians (small val) or degrees
let r = data.roll, p = data.pitch, y = data.yaw;
if (Math.abs(r) < 7) { r *= 57.2958; p *= 57.2958; y *= 57.2958; }
if (rollEl) rollEl.textContent = `${r.toFixed(1)}°`;
if (pitchEl) pitchEl.textContent = `${p.toFixed(1)}°`;
if (yawEl) yawEl.textContent = `${y.toFixed(1)}°`;
}
// Battery - Support Flat
const batEl = document.getElementById('telemetry-battery');
if (batEl) {
if (data.battery && data.battery.voltage) {
batEl.textContent = `${data.battery.voltage.toFixed(1)}V (${data.battery.remaining}%)`;
} else if (data.batt_voltage) {
batEl.textContent = `${data.batt_voltage.toFixed(1)}V (${data.batt_remaining || '?'}%)`;
} else {
// Don't overwrite with "No data" if we previously had data from WebSocket
// Only overwrite if it was already "Waiting..." or empty
if (batEl.textContent === 'Waiting...' || batEl.textContent === '') {
batEl.textContent = 'No data';
}
}
}
// Armed status
const armedEl = document.getElementById('telemetry-armed');
if (armedEl) {
if (data.armed) {
armedEl.textContent = '🔴 ARMED';
armedEl.className = 'font-mono text-red-400 font-bold animate-pulse';
} else {
armedEl.textContent = '🟢 Disarmed';
armedEl.className = 'font-mono text-green-400';
}
}
// Timestamp
const tsEl = document.getElementById('telemetry-timestamp');
if (tsEl && data.timestamp) {
const time = new Date(data.timestamp).toLocaleTimeString();
tsEl.textContent = time;
}
}
function stopPolling() {
if (droneState.statusInterval) {
clearInterval(droneState.statusInterval);
droneState.statusInterval = null;
}
if (droneState.eventsInterval) {
clearInterval(droneState.eventsInterval);
droneState.eventsInterval = null;
}
if (droneState.telemetryInterval) {
clearInterval(droneState.telemetryInterval);
droneState.telemetryInterval = null;
}
}
function updateCompanionStatus(data) {
const fpsEl = document.getElementById('companion-fps');
const facesEl = document.getElementById('companion-faces');
const gpsEl = document.getElementById('companion-gps');
const modelEl = document.getElementById('companion-model');
if (fpsEl && data.inference_fps !== undefined) {
fpsEl.textContent = `${data.inference_fps} FPS`;
}
if (facesEl) {
facesEl.textContent = data.faces_received || 0;
}
if (gpsEl && data.last_gps) {
const gps = data.last_gps;
gpsEl.textContent = `${gps.lat?.toFixed(4) || '--'}, ${gps.lon?.toFixed(4) || '--'}`;
}
if (modelEl && data.model) {
modelEl.textContent = data.model;
modelEl.className = 'text-green-400';
}
}
function updateEventsLog(events) {
const logEl = document.getElementById('detection-events-log');
if (!logEl || !events || events.length === 0) return;
let html = '';
events.reverse().forEach(event => {
const time = new Date(event.timestamp).toLocaleTimeString();
const gps = event.gps ? `${event.gps.lat?.toFixed(4)}, ${event.gps.lon?.toFixed(4)}` : 'No GPS';
const conf = (event.confidence * 100).toFixed(0);
html += `<div class="mb-1 ${event.analyzed ? 'text-green-400' : 'text-gray-400'}">
<span class="text-gray-500">[${time}]</span>
Face detected (${conf}%) @ ${gps}
${event.analyzed ? '✓' : ''}
</div>`;
});
logEl.innerHTML = html || '<div class="text-gray-500 text-center py-4">No detection events yet.</div>';
}
// MAVLink Quick Connect Presets
function setMavlinkPreset(preset) {
const uriEl = document.getElementById('mavlink-uri');
const typeEl = document.getElementById('mavlink-type');
const baudEl = document.getElementById('mavlink-baud');
const addrEl = document.getElementById('mavlink-address');
const presets = {
'companion': { addr: '192.168.200.101', type: 'companion', baud: '57600' },
'usb': { uri: '/dev/ttyACM0', type: 'usb', baud: '57600' },
'radio': { addr: 'udp:0.0.0.0:14550', type: 'udp', baud: '57600' },
'sitl': { addr: 'tcp:127.0.0.1:5760', type: 'tcp', baud: '57600' }
};
const config = presets[preset];
if (config) {
// Determine which input to update based on preset type
if (config.type === 'usb' || config.type === 'serial') {
uriEl.value = config.uri;
} else {
addrEl.value = config.addr;
}
typeEl.value = config.type;
baudEl.value = config.baud;
// Trigger UI update
toggleMavlinkInputs();
showToast(`📡 Preset loaded: ${preset.toUpperCase()}`);
}
}
// Path status management
const pathState = {
piWifi: { connected: false, lastSeen: null },
usb: { connected: false, lastSeen: null },
radio: { connected: false, lastSeen: null }
};
function updatePathStatus(path, connected, statusText) {
const indicator = document.getElementById(`path-${path}-indicator`);
const status = document.getElementById(`path-${path}-status`);
if (indicator) {
indicator.className = `w-2 h-2 rounded-full ${connected ? 'bg-green-400' : 'bg-gray-500'}`;
}
if (status) {
status.textContent = statusText || (connected ? 'Connected' : 'Not Connected');
status.className = connected ? 'text-green-400' : 'text-gray-500';
}
// Update active path count
updateActivePathCount();
}
function updateActivePathCount() {
const count = [pathState.piWifi, pathState.usb, pathState.radio]
.filter(p => p.connected).length;
const countEl = document.getElementById('active-path-count');
if (countEl) {
countEl.textContent = `${count} Active`;
countEl.className = `text-xs px-2 py-0.5 rounded ${count > 0 ? 'bg-green-700' : 'bg-gray-600'}`;
}
}
function updateDataSources(telemetrySource, videoSource) {
const telEl = document.getElementById('telemetry-source');
const vidEl = document.getElementById('video-source');
if (telEl) {
telEl.textContent = telemetrySource || '--';
telEl.className = telemetrySource ? 'text-green-400' : 'text-gray-500';
}
if (vidEl) {
vidEl.textContent = videoSource || '--';
vidEl.className = videoSource ? 'text-green-400' : 'text-gray-500';
}
}
// Initialize state on load
document.addEventListener('DOMContentLoaded', () => {
toggleMavlinkInputs();
startPathStatusPolling();
// Also try to fetch status immediately
fetchPathStatuses();
});
// Fetch path statuses from Connection Manager API
async function fetchPathStatuses() {
try {
const response = await fetch('/api/drone/paths');
if (!response.ok) return;
const data = await response.json();
// Update path status indicators
if (data.status && data.status.paths) {
const paths = data.status.paths;
let anyConnected = false;
// Pi WiFi
if (paths.pi_wifi) {
pathState.piWifi.connected = paths.pi_wifi.status === 'connected';
updatePathStatus('pi-wifi', pathState.piWifi.connected,
paths.pi_wifi.healthy ? 'Connected' : paths.pi_wifi.status);
if (pathState.piWifi.connected) anyConnected = true;
}
// USB
if (paths.usb) {
pathState.usb.connected = paths.usb.status === 'connected';
updatePathStatus('usb', pathState.usb.connected,
paths.usb.healthy ? 'Connected' : paths.usb.status);
if (pathState.usb.connected) anyConnected = true;
}
// 915MHz Radio
if (paths.radio_915) {
pathState.radio.connected = paths.radio_915.status === 'connected';
updatePathStatus('radio', pathState.radio.connected,
paths.radio_915.healthy ? 'Connected' : paths.radio_915.status);
if (pathState.radio.connected) anyConnected = true;
}
// Auto-sync UI state if backend has active connections but frontend thinks disconnected
if (anyConnected && !droneState.connected) {
console.log("Detected active backend connection, syncing UI...");
droneState.connected = true;
const statusEl = document.getElementById('drone-connection-status');
const connectBtn = document.getElementById('drone-connect-btn');
const disconnectBtn = document.getElementById('drone-disconnect-btn');
if (statusEl) {
statusEl.textContent = 'Connected (Auto)';
statusEl.className = 'ml-2 text-green-400';
}
if (connectBtn) {
connectBtn.disabled = true;
connectBtn.textContent = 'Connected';
}
if (disconnectBtn) disconnectBtn.disabled = false;
// Start other pollers
if (!droneState.statusInterval) startStatusPolling();
if (!droneState.telemetryInterval) startTelemetryPolling();
// Note: events poller is separate or handled by drone-feed.js
}
}
// Update data sources
if (data.status) {
const activePaths = data.status.active_paths || [];
// Determine telemetry source
let telemetrySource = '--';
let videoSource = '--';
if (activePaths.includes('pi_wifi') || activePaths.includes('pi_ethernet')) {
telemetrySource = 'Pi WiFi (via Pixracer)';
videoSource = 'Pi Camera';
} else if (activePaths.includes('usb')) {
telemetrySource = 'USB Direct';
} else if (activePaths.includes('radio_915')) {
telemetrySource = '915MHz Radio';
}
updateDataSources(telemetrySource, videoSource);
}
} catch (e) {
console.error('Path status fetch error:', e);
}
}
</script>
</body>
</html>