Files
flock-you/api/templates/index.html
T
2025-08-26 13:13:13 -04:00

1889 lines
64 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Flock You - Detection Dashboard</title>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #2d1b69 0%, #1a1033 50%, #4a1b69 100%);
color: #e0e0e0;
min-height: 100vh;
}
.header {
background: rgba(26, 16, 51, 0.95);
color: #e0e0e0;
padding: 1rem 2rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
border-bottom: 2px solid #8b5cf6;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
gap: 1rem;
}
.header-left {
display: flex;
align-items: center;
}
.header-right {
display: flex;
align-items: center;
gap: 1rem;
}
.header h1 {
font-size: 1.6rem;
margin: 0;
white-space: nowrap;
}
.flock-you-title {
font-family: 'Orbitron', 'Courier New', monospace;
font-size: 1.4rem;
font-weight: 700;
color: #ec4899;
text-shadow: 0 0 10px rgba(236, 72, 153, 0.5);
letter-spacing: 2px;
margin: 0;
white-space: nowrap;
flex-shrink: 0;
}
.controls {
display: flex;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
}
.control-group {
display: flex;
align-items: center;
gap: 0.5rem;
position: relative;
}
.device-control-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.header-buttons {
display: flex;
align-items: center;
gap: 1rem;
}
.control-group label {
font-weight: 600;
color: #e0e0e0;
font-size: 0.8rem;
white-space: nowrap;
}
select, button {
padding: 0.3rem 0.6rem;
border: none;
border-radius: 6px;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.3s ease;
}
select {
background: #2d1b69;
color: #e0e0e0;
border: 1px solid #8b5cf6;
min-width: 120px;
max-width: 150px;
}
select:focus {
outline: none;
border-color: #c084fc;
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.3);
}
button {
background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%);
color: white;
font-weight: 600;
border: 1px solid #7c3aed;
}
button:hover {
background: linear-gradient(135deg, #a855f7 0%, #c084fc 100%);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4);
}
button.danger {
background: linear-gradient(135deg, #dc2626 0%, #ef4444 100%);
}
button.danger:hover {
background: linear-gradient(135deg, #ef4444 0%, #f87171 100%);
}
.status {
display: flex;
gap: 1rem;
align-items: center;
}
.status-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.serial-terminal-btn {
background: linear-gradient(135deg, #059669 0%, #10b981 100%);
color: white;
font-weight: 600;
border: 1px solid #047857;
padding: 0.4rem 0.8rem;
font-size: 0.8rem;
margin-left: 1rem;
}
.serial-terminal-btn:hover {
background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
}
.oui-search-btn {
background: linear-gradient(135deg, #7c3aed 0%, #8b5cf6 100%);
color: white;
font-weight: 600;
border: 1px solid #6d28d9;
padding: 0.4rem 0.8rem;
font-size: 0.8rem;
margin-left: 0.5rem;
}
.oui-search-btn:hover {
background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.4);
}
.serial-terminal-container {
background: rgba(45, 27, 105, 0.8);
border: 1px solid #8b5cf6;
border-radius: 12px;
margin-top: 1rem;
overflow: hidden;
box-shadow: 0 8px 32px rgba(139, 92, 246, 0.3);
}
.serial-terminal-header {
background: rgba(45, 27, 105, 0.9);
padding: 1rem 1.5rem;
border-bottom: 1px solid #8b5cf6;
display: flex;
justify-content: space-between;
align-items: center;
}
.serial-terminal-header h2 {
color: #c084fc;
font-size: 1.2rem;
font-weight: 600;
margin: 0;
}
.serial-controls {
display: flex;
align-items: center;
gap: 1rem;
}
.serial-status {
color: #a855f7;
font-size: 0.9rem;
font-weight: 500;
}
.clear-terminal-btn {
background: linear-gradient(135deg, #dc2626 0%, #ef4444 100%);
color: white;
font-size: 0.8rem;
padding: 0.3rem 0.8rem;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
}
.clear-terminal-btn:hover {
background: linear-gradient(135deg, #ef4444 0%, #f87171 100%);
transform: translateY(-1px);
}
.serial-terminal-content {
background: rgba(15, 10, 26, 0.95);
padding: 1rem;
height: 300px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.4;
color: #e0e0e0;
white-space: pre-wrap;
word-wrap: break-word;
scrollbar-width: thin;
scrollbar-color: #8b5cf6 #2d1b69;
}
.serial-terminal-content::-webkit-scrollbar {
width: 8px;
}
.serial-terminal-content::-webkit-scrollbar-track {
background: #2d1b69;
border-radius: 4px;
}
.serial-terminal-content::-webkit-scrollbar-thumb {
background: #8b5cf6;
border-radius: 4px;
}
.serial-terminal-content::-webkit-scrollbar-thumb:hover {
background: #a855f7;
}
.terminal-placeholder {
color: #6b7280;
font-style: italic;
text-align: center;
margin-top: 2rem;
}
.serial-line {
margin: 0;
padding: 0;
}
.serial-line.detection {
color: #c084fc;
font-weight: 600;
}
.serial-line.error {
color: #f87171;
}
.serial-line.info {
color: #60a5fa;
}
.serial-line.success {
color: #34d399;
}
.serial-line.warning {
color: #fbbf24;
}
.oui-search-container {
background: rgba(45, 27, 105, 0.8);
border: 1px solid #8b5cf6;
border-radius: 12px;
margin-top: 1rem;
overflow: hidden;
box-shadow: 0 8px 32px rgba(139, 92, 246, 0.3);
}
.oui-search-header {
background: rgba(45, 27, 105, 0.9);
padding: 1rem 1.5rem;
border-bottom: 1px solid #8b5cf6;
display: flex;
justify-content: space-between;
align-items: center;
}
.oui-search-header h2 {
color: #c084fc;
font-size: 1.2rem;
font-weight: 600;
margin: 0;
}
.oui-search-controls {
display: flex;
align-items: center;
gap: 1rem;
}
.oui-status {
color: #a855f7;
font-size: 0.9rem;
font-weight: 500;
}
.view-all-btn {
background: linear-gradient(135deg, #059669 0%, #10b981 100%);
color: white;
font-size: 0.8rem;
padding: 0.3rem 0.8rem;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
}
.view-all-btn:hover {
background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
transform: translateY(-1px);
}
.refresh-db-btn {
background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%);
color: white;
font-size: 0.8rem;
padding: 0.3rem 0.8rem;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
}
.refresh-db-btn:hover {
background: linear-gradient(135deg, #fbbf24 0%, #fcd34d 100%);
transform: translateY(-1px);
}
.clear-oui-btn {
background: linear-gradient(135deg, #dc2626 0%, #ef4444 100%);
color: white;
font-size: 0.8rem;
padding: 0.3rem 0.8rem;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
}
.clear-oui-btn:hover {
background: linear-gradient(135deg, #ef4444 0%, #f87171 100%);
transform: translateY(-1px);
}
.oui-search-content {
padding: 1rem;
}
.search-input-group {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.oui-search-input {
flex: 1;
background: rgba(15, 10, 26, 0.95);
border: 1px solid #8b5cf6;
border-radius: 6px;
color: #e0e0e0;
padding: 0.5rem;
font-size: 0.9rem;
}
.oui-search-input:focus {
outline: none;
border-color: #c084fc;
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.3);
background: rgba(15, 10, 26, 0.95);
color: #e0e0e0;
}
.oui-search-input::placeholder {
color: #6b7280;
}
.search-btn {
background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%);
color: white;
font-size: 0.9rem;
padding: 0.5rem 1rem;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
}
.search-btn:hover {
background: linear-gradient(135deg, #a855f7 0%, #c084fc 100%);
transform: translateY(-1px);
}
.search-results {
background: rgba(15, 10, 26, 0.95);
border: 1px solid #8b5cf6;
border-radius: 6px;
padding: 1rem;
max-height: 400px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: #8b5cf6 #2d1b69;
}
.oui-results-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.3rem;
max-height: 350px;
overflow-y: auto;
}
.oui-result-item {
background: rgba(45, 27, 105, 0.5);
border: 1px solid #8b5cf6;
border-radius: 4px;
padding: 0.3rem;
margin-bottom: 0;
font-size: 0.7rem;
min-height: 40px;
}
.oui-result-item:last-child {
margin-bottom: 0;
}
.oui-mac {
color: #c084fc;
font-weight: 600;
font-family: 'Courier New', monospace;
font-size: 0.75rem;
}
.oui-manufacturer {
color: #e0e0e0;
margin-top: 0.25rem;
font-size: 0.7rem;
line-height: 1.2;
word-wrap: break-word;
}
.search-results::-webkit-scrollbar {
width: 8px;
}
.search-results::-webkit-scrollbar-track {
background: #2d1b69;
border-radius: 4px;
}
.search-results::-webkit-scrollbar-thumb {
background: #8b5cf6;
border-radius: 4px;
}
.search-results::-webkit-scrollbar-thumb:hover {
background: #a855f7;
}
.search-placeholder {
color: #6b7280;
font-style: italic;
text-align: center;
margin-top: 2rem;
}
.oui-result-item {
background: rgba(45, 27, 105, 0.5);
border: 1px solid #8b5cf6;
border-radius: 6px;
padding: 0.75rem;
margin-bottom: 0.5rem;
}
.oui-result-item:last-child {
margin-bottom: 0;
}
.oui-mac {
color: #c084fc;
font-weight: 600;
font-family: 'Courier New', monospace;
}
.oui-manufacturer {
color: #e0e0e0;
margin-top: 0.25rem;
}
.alias-display {
cursor: pointer;
padding: 0.25rem 0.5rem;
border-radius: 4px;
display: inline-block;
min-width: 100px;
background: #1e40af;
border: 1px solid #3b82f6;
color: #93c5fd;
font-weight: 600;
font-size: 0.85rem;
}
.alias-display:hover {
background: #2563eb;
border-color: #60a5fa;
color: white;
}
.alias-display em {
color: #6b7280;
font-style: italic;
}
.alias-input {
background: rgba(45, 27, 105, 0.8);
border: 1px solid #8b5cf6;
border-radius: 4px;
color: #e0e0e0;
padding: 2px 6px;
font-size: 0.9rem;
width: 120px;
}
.alias-input:focus {
outline: none;
border-color: #c084fc;
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.3);
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background: #dc2626;
animation: pulse 2s infinite;
box-shadow: 0 0 8px rgba(220, 38, 38, 0.6);
}
.status-indicator.active {
background: #10b981;
box-shadow: 0 0 8px rgba(16, 185, 129, 0.6);
}
.device-info {
font-size: 0.8rem;
color: #d1d5db;
margin-top: 0.25rem;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.main-content {
padding: 1rem;
width: 100%;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background: rgba(45, 27, 105, 0.8);
padding: 1.5rem;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
text-align: center;
border: 1px solid #8b5cf6;
backdrop-filter: blur(10px);
}
.stat-number {
font-size: 2rem;
font-weight: bold;
color: #c084fc;
}
.stat-label {
color: #d1d5db;
margin-top: 0.5rem;
}
.detections-container {
background: rgba(45, 27, 105, 0.8);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
overflow: hidden;
border: 1px solid #8b5cf6;
backdrop-filter: blur(10px);
}
.detections-header {
background: rgba(26, 16, 51, 0.9);
padding: 1rem;
border-bottom: 1px solid #8b5cf6;
display: flex;
justify-content: space-between;
align-items: center;
}
.detections-header h2 {
color: #c084fc;
font-size: 1.2rem;
font-weight: 600;
margin: 0;
}
.detections-list {
max-height: 600px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: #8b5cf6 #2d1b69;
}
.detections-list::-webkit-scrollbar {
width: 8px;
}
.detections-list::-webkit-scrollbar-track {
background: #2d1b69;
border-radius: 4px;
}
.detections-list::-webkit-scrollbar-thumb {
background: #8b5cf6;
border-radius: 4px;
}
.detections-list::-webkit-scrollbar-thumb:hover {
background: #a855f7;
}
.detection-item {
background: #1e3a8a;
border: 1px solid #3b82f6;
border-radius: 6px;
padding: 0.5rem;
margin-bottom: 0.5rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
min-height: 5rem;
}
.detection-item:hover {
background: #1e40af;
border-color: #60a5fa;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
}
.detection-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.detection-header-left {
display: flex;
align-items: center;
gap: 0.5rem;
}
.gps-link {
color: #93c5fd;
font-size: 0.8rem;
font-weight: 600;
text-decoration: none;
padding: 0.1rem 0.3rem;
border-radius: 3px;
background: rgba(147, 197, 253, 0.1);
border: 1px solid #3b82f6;
transition: all 0.2s ease;
}
.gps-link:hover {
background: #3b82f6;
color: white;
text-decoration: none;
}
.detection-type-badge {
display: flex;
align-items: center;
gap: 0.5rem;
}
.detection-type {
background: #2563eb;
color: white;
padding: 0.25rem 0.6rem;
border-radius: 4px;
font-size: 0.9rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.detection-count {
background: #059669;
color: white;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.85rem;
font-weight: 700;
min-width: 2.5rem;
text-align: center;
}
.detection-time {
color: #93c5fd;
font-size: 0.9rem;
font-weight: 600;
}
.detection-details {
display: flex;
flex-wrap: wrap;
gap: 1rem;
font-size: 0.9rem;
align-items: center;
}
.detail-item {
display: flex;
align-items: center;
white-space: nowrap;
}
.detail-label {
font-weight: 700;
color: #93c5fd;
font-size: 0.9rem;
margin-right: 0.5rem;
}
.detail-value {
color: #ffffff;
font-weight: 600;
font-size: 0.9rem;
}
.detection-timing {
grid-column: 1 / -1;
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.2);
border-radius: 4px;
padding: 0.2rem;
margin-top: 0.15rem;
font-size: 0.65rem;
}
.no-detections {
text-align: center;
padding: 3rem;
color: #d1d5db;
}
.export-buttons {
display: flex;
gap: 1rem;
margin-top: 1rem;
align-items: center;
}
.detection-search-group {
margin-right: 0.5rem;
}
.detection-search-input {
background: rgba(15, 10, 26, 0.95);
border: 1px solid #8b5cf6;
border-radius: 6px;
color: #e0e0e0;
padding: 0.4rem 0.8rem;
font-size: 0.8rem;
width: 200px;
}
.detection-search-input:focus {
outline: none;
border-color: #c084fc;
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.3);
background: rgba(15, 10, 26, 0.95);
color: #e0e0e0;
}
.detection-search-input::placeholder {
color: #6b7280;
}
.export-btn {
background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%);
border: 1px solid #7c3aed;
}
.export-btn:hover {
background: linear-gradient(135deg, #a855f7 0%, #c084fc 100%);
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4);
}
.clear-btn {
background: linear-gradient(135deg, #ec4899 0%, #f472b6 100%);
border: 1px solid #db2777;
}
.clear-btn:hover {
background: linear-gradient(135deg, #f472b6 0%, #f9a8d4 100%);
box-shadow: 0 4px 12px rgba(236, 72, 153, 0.4);
}
@media (max-width: 768px) {
.header-content {
flex-direction: column;
gap: 1rem;
align-items: stretch;
}
.flock-you-title {
font-size: 1.2rem;
text-align: center;
order: -1;
}
.controls {
flex-direction: column;
align-items: stretch;
gap: 1rem;
width: 100%;
}
.device-control-group,
.control-group {
width: 100%;
justify-content: space-between;
}
.header-buttons {
justify-content: center;
width: 100%;
}
.detection-details {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="header">
<div class="header-content">
<h1 class="flock-you-title">FLOCK-YOU</h1>
<div class="controls">
<div class="device-control-group">
<div class="status-indicator" id="flockStatus"></div>
<label>Sniffer:</label>
<select id="flockDeviceSelect">
<option value="">Select Port</option>
</select>
<button id="connectFlockBtn">Connect</button>
<button id="disconnectFlockBtn" class="danger" style="display: none;">Disconnect</button>
</div>
<div class="device-control-group">
<div class="status-indicator" id="gpsStatus"></div>
<label>GPS:</label>
<select id="gpsPortSelect">
<option value="">Select Port</option>
</select>
<button id="connectGpsBtn">Connect</button>
<button id="disconnectGpsBtn" class="danger" style="display: none;">Disconnect</button>
</div>
<div class="control-group">
<label>Filter:</label>
<select id="filterSelect">
<option value="all">All Detections</option>
<option value="probe_request">WiFi Probe Requests</option>
<option value="beacon">WiFi Beacons</option>
<option value="mac_prefix">MAC Address</option>
<option value="device_name">BLE Device Name</option>
</select>
</div>
</div>
<div class="header-buttons">
<button id="serial-terminal-btn" class="serial-terminal-btn" onclick="toggleSerialTerminal()">Serial Terminal</button>
<button id="oui-search-btn" class="oui-search-btn" onclick="toggleOuiSearch()">Search OUI</button>
</div>
</div>
</div>
<div class="main-content">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number" id="totalDetections">0</div>
<div class="stat-label">Total Detections</div>
</div>
<div class="stat-card">
<div class="stat-number" id="wifiDetections">0</div>
<div class="stat-label">WiFi Detections</div>
</div>
<div class="stat-card">
<div class="stat-number" id="bleDetections">0</div>
<div class="stat-label">BLE Detections</div>
</div>
<div class="stat-card">
<div class="stat-number" id="gpsDetections">0</div>
<div class="stat-label">GPS Tagged</div>
</div>
</div>
<div class="detections-container">
<div class="detections-header">
<h2>Detections</h2>
<div class="export-buttons">
<div class="detection-search-group">
<input type="text" id="detectionSearchInput" class="detection-search-input" placeholder="Search detections..." onkeyup="searchDetections()">
</div>
<button class="export-btn" onclick="exportCSV()">Export CSV</button>
<button class="export-btn" onclick="exportKML()">Export KML</button>
<button class="clear-btn" onclick="clearDetections()">Clear All</button>
</div>
</div>
<div class="detections-list" id="detectionsList">
<div class="no-detections">
<h3>No detections yet</h3>
<p>Start scanning with your Flock You device to see detections here</p>
</div>
</div>
</div>
<div class="serial-terminal-container" id="serialTerminalContainer" style="display: none;">
<div class="serial-terminal-header">
<h2>Serial Terminal</h2>
<div class="serial-controls">
<span class="serial-status">Status: <span id="serialConnectionStatus">Disconnected</span></span>
<button class="clear-terminal-btn" onclick="clearSerialTerminal()">Clear Terminal</button>
</div>
</div>
<div class="serial-terminal-content" id="serialTerminalOutput">
<div class="terminal-placeholder">Connect to a Sniffer device to see serial output...</div>
</div>
</div>
<div class="oui-search-container" id="ouiSearchContainer" style="display: none;">
<div class="oui-search-header">
<h2>OUI Database Search</h2>
<div class="oui-search-controls">
<span class="oui-status">Search IEEE OUI Database</span>
<button class="view-all-btn" onclick="viewAllOui()">View All</button>
<button class="refresh-db-btn" onclick="refreshOuiDatabase()">Refresh DB</button>
<button class="clear-oui-btn" onclick="clearOuiResults()">Clear Results</button>
</div>
</div>
<div class="oui-search-content">
<div class="search-input-group">
<input type="text" id="ouiSearchInput" class="oui-search-input" placeholder="Enter MAC address (e.g., 00:11:22) or manufacturer name..." onkeypress="handleOuiSearch(event)">
<button class="search-btn" onclick="searchOui()">Search</button>
</div>
<div class="search-results" id="ouiSearchResults">
<div class="search-placeholder">Enter a MAC address or manufacturer name to search...</div>
</div>
</div>
</div>
</div>
<script>
const socket = io({
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000
});
let detections = [];
let gpsConnected = false;
const max_reconnect_attempts = 5;
// Initialize
document.addEventListener('DOMContentLoaded', function() {
loadDetections();
loadFlockPorts();
loadGpsPorts();
setupEventListeners();
loadStatus();
// Periodic status refresh every 5 seconds
setInterval(loadStatus, 5000);
// Periodic port refresh every 10 seconds
setInterval(function() {
loadFlockPorts();
loadGpsPorts();
}, 10000);
// Connection health check every 30 seconds
setInterval(function() {
if (!socket.connected) {
console.log('Socket disconnected, attempting to reconnect...');
socket.connect();
}
}, 30000);
});
function setupEventListeners() {
document.getElementById('filterSelect').addEventListener('change', function() {
filterDetections(this.value);
});
document.getElementById('connectFlockBtn').addEventListener('click', connectFlock);
document.getElementById('disconnectFlockBtn').addEventListener('click', disconnectFlock);
document.getElementById('connectGpsBtn').addEventListener('click', connectGps);
document.getElementById('disconnectGpsBtn').addEventListener('click', disconnectGps);
}
function loadDetections() {
fetch('/api/detections')
.then(response => response.json())
.then(data => {
console.log('Loaded detections:', data.length);
detections = data;
updateStats();
renderDetections();
})
.catch(error => {
console.error('Error loading detections:', error);
});
}
function loadGpsPorts() {
fetch('/api/gps/ports')
.then(response => response.json())
.then(ports => {
console.log('GPS ports loaded:', ports.length);
const select = document.getElementById('gpsPortSelect');
select.innerHTML = '<option value="">Select GPS Port</option>';
ports.forEach(port => {
const option = document.createElement('option');
option.value = port.device;
option.textContent = `${port.device} - ${port.description}`;
select.appendChild(option);
});
})
.catch(error => {
console.error('Error loading GPS ports:', error);
});
}
function loadFlockPorts() {
fetch('/api/flock/ports')
.then(response => response.json())
.then(ports => {
console.log('Flock ports loaded:', ports.length);
const select = document.getElementById('flockDeviceSelect');
select.innerHTML = '<option value="">Select Flock You Port</option>';
ports.forEach(port => {
const option = document.createElement('option');
option.value = port.device;
option.textContent = `${port.device} - ${port.description}`;
select.appendChild(option);
});
})
.catch(error => {
console.error('Error loading Flock ports:', error);
});
}
function connectFlock() {
const port = document.getElementById('flockDeviceSelect').value;
if (!port) {
alert('Please select a Sniffer port');
return;
}
console.log('Connecting to Flock device on port:', port);
fetch('/api/flock/connect', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ port: port })
})
.then(response => response.json())
.then(data => {
console.log('Flock connection response:', data);
if (data.status === 'success') {
updateFlockStatus(true);
document.getElementById('connectFlockBtn').style.display = 'none';
document.getElementById('disconnectFlockBtn').style.display = 'inline-block';
} else {
alert('Flock You connection failed: ' + data.message);
}
})
.catch(error => {
console.error('Flock connection error:', error);
alert('Connection failed: ' + error.message);
});
}
function disconnectFlock() {
console.log('Disconnecting Flock device');
fetch('/api/flock/disconnect', {
method: 'POST'
})
.then(response => response.json())
.then(data => {
console.log('Flock disconnect response:', data);
if (data.status === 'success') {
updateFlockStatus(false);
document.getElementById('connectFlockBtn').style.display = 'inline-block';
document.getElementById('disconnectFlockBtn').style.display = 'none';
}
})
.catch(error => {
console.error('Flock disconnect error:', error);
});
}
function connectGps() {
const port = document.getElementById('gpsPortSelect').value;
if (!port) {
alert('Please select a GPS port');
return;
}
console.log('Connecting to GPS device on port:', port);
fetch('/api/gps/connect', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ port: port })
})
.then(response => response.json())
.then(data => {
console.log('GPS connection response:', data);
if (data.status === 'success') {
gpsConnected = true;
updateGpsStatus(true);
document.getElementById('connectGpsBtn').style.display = 'none';
document.getElementById('disconnectGpsBtn').style.display = 'inline-block';
} else {
alert('GPS connection failed: ' + data.message);
}
})
.catch(error => {
console.error('GPS connection error:', error);
alert('Connection failed: ' + error.message);
});
}
function disconnectGps() {
console.log('Disconnecting GPS device');
fetch('/api/gps/disconnect', {
method: 'POST'
})
.then(response => response.json())
.then(data => {
console.log('GPS disconnect response:', data);
if (data.status === 'success') {
gpsConnected = false;
updateGpsStatus(false);
document.getElementById('connectGpsBtn').style.display = 'inline-block';
document.getElementById('disconnectGpsBtn').style.display = 'none';
}
})
.catch(error => {
console.error('GPS disconnect error:', error);
});
}
function updateFlockStatus(connected) {
const indicator = document.getElementById('flockStatus');
if (connected) {
indicator.classList.add('active');
} else {
indicator.classList.remove('active');
}
}
function updateGpsStatus(connected) {
const indicator = document.getElementById('gpsStatus');
if (connected) {
indicator.classList.add('active');
} else {
indicator.classList.remove('active');
}
}
function loadStatus() {
fetch('/api/status')
.then(response => response.json())
.then(data => {
console.log('Status loaded:', data);
updateFlockStatus(data.flock_connected);
updateGpsStatus(data.gps_connected);
if (data.flock_connected) {
document.getElementById('connectFlockBtn').style.display = 'none';
document.getElementById('disconnectFlockBtn').style.display = 'inline-block';
}
if (data.gps_connected) {
document.getElementById('connectGpsBtn').style.display = 'none';
document.getElementById('disconnectGpsBtn').style.display = 'inline-block';
}
})
.catch(error => {
console.error('Error loading status:', error);
});
}
function filterDetections(filter) {
if (filter === 'all') {
renderDetections(detections);
} else {
const filtered = detections.filter(d => d.detection_method === filter);
renderDetections(filtered);
}
}
function searchDetections() {
const searchQuery = document.getElementById('detectionSearchInput').value.toLowerCase().trim();
if (!searchQuery) {
// If search is empty, show all detections
renderDetections(detections);
return;
}
// Search through all detection fields
const filtered = detections.filter(detection => {
return (
detection.mac_address.toLowerCase().includes(searchQuery) ||
(detection.manufacturer && detection.manufacturer.toLowerCase().includes(searchQuery)) ||
(detection.alias && detection.alias.toLowerCase().includes(searchQuery)) ||
detection.detection_method.toLowerCase().includes(searchQuery) ||
detection.protocol.toLowerCase().includes(searchQuery) ||
detection.detection_time.toLowerCase().includes(searchQuery) ||
(detection.ssid && detection.ssid.toLowerCase().includes(searchQuery)) ||
(detection.device_name && detection.device_name.toLowerCase().includes(searchQuery))
);
});
renderDetections(filtered);
}
function updateStats() {
const total = detections.length;
const wifi = detections.filter(d => d.protocol === 'wifi').length;
const ble = detections.filter(d => d.protocol === 'bluetooth_le' || d.protocol === 'bluetooth_classic').length;
const gps = detections.filter(d => d.gps).length;
document.getElementById('totalDetections').textContent = total;
document.getElementById('wifiDetections').textContent = wifi;
document.getElementById('bleDetections').textContent = ble;
document.getElementById('gpsDetections').textContent = gps;
}
function renderDetections(detectionsToRender = detections) {
const container = document.getElementById('detectionsList');
if (detectionsToRender.length === 0) {
container.innerHTML = `
<div class="no-detections">
<h3>No detections found</h3>
<p>Try changing the filter or wait for new detections</p>
</div>
`;
return;
}
container.innerHTML = detectionsToRender.map(detection => {
// Get detection count and timing info
const count = detection.detection_count || 1;
const lastSeen = detection.last_seen ? new Date(detection.last_seen).toLocaleTimeString() : 'Unknown';
// Use last known values for signal data
const rssi = detection.last_rssi !== undefined ? detection.last_rssi : detection.rssi;
const channel = detection.last_channel || detection.channel;
const ssid = detection.last_ssid || detection.ssid;
const deviceName = detection.last_device_name || detection.device_name;
// Build essential fields in a compact layout
const essentialFields = [];
if (detection.protocol) essentialFields.push(['Protocol', detection.protocol]);
if (detection.mac_address) essentialFields.push(['MAC', detection.mac_address]);
if (rssi !== undefined) essentialFields.push(['RSSI', `${rssi} dBm`]);
if (channel) essentialFields.push(['Channel', channel]);
if (ssid) essentialFields.push(['SSID', ssid]);
if (deviceName) essentialFields.push(['Device', deviceName]);
if (detection.manufacturer) essentialFields.push(['Manufacturer', detection.manufacturer]);
// Build the details HTML
const detailsHtml = essentialFields.map(([label, value]) => `
<div class="detail-item">
<span class="detail-label">${label}:</span>
<span class="detail-value">${value}</span>
</div>
`).join('');
// Build GPS link if available (header only)
let gpsLink = '';
if (detection.gps && detection.gps.latitude !== undefined && detection.gps.longitude !== undefined) {
const lat = detection.gps.latitude;
const lon = detection.gps.longitude;
const osmUrl = `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}&zoom=18`;
gpsLink = `<a href="${osmUrl}" target="_blank" class="gps-link">${lat.toFixed(4)}, ${lon.toFixed(4)}</a>`;
}
return `
<div class="detection-item">
<div class="detection-header">
<div class="detection-header-left">
<div class="detection-type-badge">
<span class="detection-type">${detection.detection_method ? detection.detection_method.toUpperCase() : 'UNKNOWN'}</span>
<span class="detection-count">${count}×</span>
</div>
${gpsLink}
</div>
<span class="detection-time">${lastSeen}</span>
</div>
<div class="detection-details">
${detailsHtml}
<div class="detail-item">
<span class="detail-label">Alias:</span>
<span class="alias-display" onclick="editAlias(${detection.id})">
${detection.alias || '<em>Click to add alias</em>'}
</span>
</div>
</div>
</div>
`;
}).join('');
}
function exportCSV() {
window.location.href = '/api/export/csv';
}
function exportKML() {
window.location.href = '/api/export/kml';
}
function clearDetections() {
if (confirm('Are you sure you want to clear all detections?')) {
fetch('/api/clear', {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
detections = [];
updateStats();
renderDetections();
}
});
}
}
// Socket connection events
socket.on('connect', function() {
console.log('Socket connected');
});
socket.on('disconnect', function() {
console.log('Socket disconnected');
});
socket.on('reconnect', function(attemptNumber) {
console.log('Socket reconnected after', attemptNumber, 'attempts');
// Reload data after reconnection
loadDetections();
loadStatus();
});
socket.on('reconnect_failed', function() {
console.log('Socket reconnection failed');
});
socket.on('heartbeat', function() {
socket.emit('heartbeat');
});
socket.on('heartbeat_ack', function() {
// Heartbeat acknowledged
});
// Socket.IO event handlers
socket.on('new_detection', function(detection) {
console.log('New detection received:', detection);
// Validate detection data
if (!detection || !detection.id) {
console.error('Invalid detection data received:', detection);
return;
}
// Check if detection already exists (prevent duplicates)
const existingIndex = detections.findIndex(d => d.id === detection.id);
if (existingIndex !== -1) {
console.log('Detection already exists, updating:', detection.id);
detections[existingIndex] = detection;
} else {
detections.unshift(detection);
}
updateStats();
renderDetections();
});
socket.on('detection_updated', function(detection) {
console.log('Detection updated:', detection);
// Validate detection data
if (!detection || !detection.id) {
console.error('Invalid detection update data received:', detection);
return;
}
// Update existing detection
const existingIndex = detections.findIndex(d => d.id === detection.id);
if (existingIndex !== -1) {
detections[existingIndex] = detection;
updateStats();
renderDetections();
}
});
socket.on('gps_update', function(gpsData) {
console.log('GPS Update:', gpsData);
});
socket.on('gps_disconnected', function() {
console.log('GPS disconnected');
updateGpsStatus(false);
const connectBtn = document.getElementById('connectGpsBtn');
const disconnectBtn = document.getElementById('disconnectGpsBtn');
if (connectBtn) connectBtn.style.display = 'inline-block';
if (disconnectBtn) disconnectBtn.style.display = 'none';
});
socket.on('flock_disconnected', function() {
console.log('Sniffer device disconnected');
updateFlockStatus(false);
const connectBtn = document.getElementById('connectFlockBtn');
const disconnectBtn = document.getElementById('disconnectFlockBtn');
if (connectBtn) connectBtn.style.display = 'inline-block';
if (disconnectBtn) disconnectBtn.style.display = 'none';
});
socket.on('detections_cleared', function() {
console.log('All detections cleared');
detections = [];
updateStats();
renderDetections();
});
socket.on('flock_reconnected', function(data) {
console.log('Sniffer device reconnected:', data);
updateFlockStatus(true);
document.getElementById('connectFlockBtn').style.display = 'none';
document.getElementById('disconnectFlockBtn').style.display = 'inline-block';
if (document.getElementById('serialTerminalContainer').style.display !== 'none') {
addSerialLine(`Sniffer device reconnected on ${data.port}`, 'success');
}
});
socket.on('gps_reconnected', function(data) {
console.log('GPS device reconnected:', data);
updateGpsStatus(true);
document.getElementById('connectGpsBtn').style.display = 'none';
document.getElementById('disconnectGpsBtn').style.display = 'inline-block';
if (document.getElementById('serialTerminalContainer').style.display !== 'none') {
addSerialLine(`GPS device reconnected on ${data.port}`, 'success');
}
});
socket.on('reconnect_failed', function(data) {
console.log('Reconnection failed:', data);
const device = data.device === 'flock' ? 'Sniffer' : 'GPS';
if (document.getElementById('serialTerminalContainer').style.display !== 'none') {
addSerialLine(`${device} reconnection failed after ${max_reconnect_attempts} attempts`, 'error');
}
});
function toggleSerialTerminal() {
const container = document.getElementById('serialTerminalContainer');
const button = document.getElementById('serial-terminal-btn');
if (container.style.display === 'none') {
container.style.display = 'block';
button.textContent = 'Hide Terminal';
button.style.background = 'linear-gradient(135deg, #dc2626 0%, #ef4444 100%)';
// Start serial connection if device is connected
const flockPort = document.getElementById('flockDeviceSelect').value;
if (flockPort) {
startSerialConnection(flockPort);
} else {
addSerialLine('No Sniffer device selected', 'error');
}
} else {
container.style.display = 'none';
button.textContent = 'Serial Terminal';
button.style.background = 'linear-gradient(135deg, #059669 0%, #10b981 100%)';
}
}
function toggleOuiSearch() {
const container = document.getElementById('ouiSearchContainer');
const button = document.getElementById('oui-search-btn');
if (container.style.display === 'none') {
container.style.display = 'block';
button.textContent = 'Hide Search';
button.style.background = 'linear-gradient(135deg, #dc2626 0%, #ef4444 100%)';
} else {
container.style.display = 'none';
button.textContent = 'Search OUI';
button.style.background = 'linear-gradient(135deg, #7c3aed 0%, #8b5cf6 100%)';
}
}
function startSerialConnection(port) {
const statusElement = document.getElementById('serialConnectionStatus');
const outputElement = document.getElementById('serialTerminalOutput');
statusElement.textContent = 'Connecting...';
statusElement.style.color = '#fbbf24';
// Clear placeholder
if (outputElement.querySelector('.terminal-placeholder')) {
outputElement.innerHTML = '';
}
// Add connection message
addSerialLine(`Connecting to ${port} at 115200 baud...`, 'info');
// Request serial terminal connection
socket.emit('request_serial_terminal', {port: port});
// Set a timeout for connection
setTimeout(() => {
if (statusElement.textContent === 'Connecting...') {
statusElement.textContent = 'Connection Timeout';
statusElement.style.color = '#f87171';
addSerialLine('Connection timeout - check device connection', 'error');
}
}, 5000);
}
function addSerialLine(text, type = 'normal') {
console.log('Adding serial line:', text, 'type:', type);
const outputElement = document.getElementById('serialTerminalOutput');
if (!outputElement) {
console.error('Serial terminal output element not found');
return;
}
// Remove placeholder if it exists
const placeholder = outputElement.querySelector('.terminal-placeholder');
if (placeholder) {
placeholder.remove();
}
const line = document.createElement('div');
line.className = `serial-line ${type}`;
line.textContent = text;
outputElement.appendChild(line);
outputElement.scrollTop = outputElement.scrollHeight;
}
function clearSerialTerminal() {
const outputElement = document.getElementById('serialTerminalOutput');
outputElement.innerHTML = '<div class="terminal-placeholder">Terminal cleared...</div>';
}
// Serial terminal socket events
socket.on('serial_data', function(data) {
console.log('Received serial data:', data);
if (data && typeof data === 'string') {
addSerialLine(data, 'normal');
} else {
console.error('Invalid serial data received:', data);
}
});
socket.on('serial_connected', function() {
console.log('Serial terminal connected');
const statusElement = document.getElementById('serialConnectionStatus');
if (statusElement) {
statusElement.textContent = 'Connected';
statusElement.style.color = '#34d399';
}
addSerialLine('Serial connection established', 'success');
});
socket.on('serial_disconnected', function() {
console.log('Serial terminal disconnected');
const statusElement = document.getElementById('serialConnectionStatus');
if (statusElement) {
statusElement.textContent = 'Disconnected';
statusElement.style.color = '#f87171';
}
addSerialLine('Serial connection lost', 'error');
});
socket.on('serial_error', function(data) {
console.log('Serial error:', data);
if (data && data.message) {
addSerialLine(`Error: ${data.message}`, 'error');
} else {
addSerialLine('Unknown serial error', 'error');
}
});
socket.on('flock_reconnected', function(data) {
updateFlockStatus(true);
document.getElementById('connectFlockBtn').style.display = 'none';
document.getElementById('disconnectFlockBtn').style.display = 'inline-block';
addSerialLine(`Sniffer device reconnected on ${data.port}`, 'success');
});
socket.on('gps_reconnected', function(data) {
updateGpsStatus(true);
document.getElementById('connectGpsBtn').style.display = 'none';
document.getElementById('disconnectGpsBtn').style.display = 'inline-block';
addSerialLine(`GPS device reconnected on ${data.port}`, 'success');
});
socket.on('reconnect_failed', function(data) {
const device = data.device === 'flock' ? 'Sniffer' : 'GPS';
addSerialLine(`${device} reconnection failed after ${max_reconnect_attempts} attempts`, 'error');
});
// Alias editing functions
function editAlias(detectionId) {
const aliasDisplay = event.target;
const currentAlias = aliasDisplay.textContent.replace('<em>Click to add alias</em>', '').trim();
const input = document.createElement('input');
input.type = 'text';
input.className = 'alias-input';
input.value = currentAlias;
input.placeholder = 'Enter alias...';
input.addEventListener('blur', function() {
saveAlias(detectionId, input.value);
});
input.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
saveAlias(detectionId, input.value);
}
});
aliasDisplay.innerHTML = '';
aliasDisplay.appendChild(input);
input.focus();
}
function saveAlias(detectionId, alias) {
fetch('/api/detection/alias', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: detectionId,
alias: alias
})
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
// Update the display
const aliasDisplay = event.target.parentElement;
aliasDisplay.innerHTML = alias || '<em>Click to add alias</em>';
}
})
.catch(error => {
console.error('Error saving alias:', error);
});
}
// Handle detection updates
socket.on('detection_updated', function(detection) {
console.log('Detection updated:', detection);
// Update the detection in our local array
const index = detections.findIndex(d => d.id === detection.id);
if (index !== -1) {
detections[index] = detection;
updateStats();
renderDetections();
} else {
console.warn('Detection not found for update:', detection.id);
}
});
// OUI Search functions
function handleOuiSearch(event) {
if (event.key === 'Enter') {
searchOui();
}
}
function searchOui() {
const query = document.getElementById('ouiSearchInput').value.trim();
if (!query) {
return;
}
fetch('/api/oui/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query: query })
})
.then(response => response.json())
.then(data => {
displayOuiResults(data.results);
})
.catch(error => {
console.error('Error searching OUI:', error);
displayOuiResults([]);
});
}
function displayOuiResults(results) {
const resultsContainer = document.getElementById('ouiSearchResults');
if (results.length === 0) {
resultsContainer.innerHTML = '<div class="search-placeholder">No results found</div>';
return;
}
// Check if this is a "view all" request (more than 50 results)
const isViewAll = results.length > 50;
if (isViewAll) {
// Use grid layout for view all - show all entries
const resultsHtml = results.map(result => `
<div class="oui-result-item">
<div class="oui-mac">${formatMacAddress(result.mac)}</div>
<div class="oui-manufacturer">${result.manufacturer}</div>
</div>
`).join('');
resultsContainer.innerHTML = `<div class="oui-results-grid">${resultsHtml}</div>`;
} else {
// Use single column for search results
const resultsHtml = results.map(result => `
<div class="oui-result-item">
<div class="oui-mac">${formatMacAddress(result.mac)}</div>
<div class="oui-manufacturer">${result.manufacturer}</div>
</div>
`).join('');
resultsContainer.innerHTML = resultsHtml;
}
}
function clearOuiResults() {
document.getElementById('ouiSearchInput').value = '';
document.getElementById('ouiSearchResults').innerHTML = '<div class="search-placeholder">Enter a MAC address or manufacturer name to search...</div>';
}
function formatMacAddress(mac) {
// Add colons every 2 characters
return mac.replace(/(.{2})(?=.{2})/g, '$1:');
}
function viewAllOui() {
fetch('/api/oui/all')
.then(response => response.json())
.then(data => {
displayOuiResults(data.results);
})
.catch(error => {
console.error('Error loading all OUI entries:', error);
displayOuiResults([]);
});
}
function refreshOuiDatabase() {
const button = document.querySelector('.refresh-db-btn');
const originalText = button.textContent;
button.textContent = 'Refreshing...';
button.disabled = true;
fetch('/api/oui/refresh', {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
alert(`Database refreshed successfully! Loaded ${data.count} entries.`);
} else {
alert('Error refreshing database: ' + data.message);
}
})
.catch(error => {
console.error('Error refreshing database:', error);
alert('Error refreshing database. Please try again.');
})
.finally(() => {
button.textContent = originalText;
button.disabled = false;
});
}
</script>
</body>
</html>