Spaces:
Sleeping
Sleeping
| <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="{"split_ratio": 0.8, "augmentation": true, "preprocess": 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"><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> |