mirror of
https://github.com/colonelpanichacks/flock-you.git
synced 2026-06-10 06:03:31 -07:00
1882 lines
66 KiB
HTML
1882 lines
66 KiB
HTML
<!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%;
|
|
}
|
|
|
|
.header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.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;
|
|
position: absolute;
|
|
top: 1rem;
|
|
right: 2rem;
|
|
}
|
|
|
|
.controls {
|
|
display: flex;
|
|
gap: 1rem;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.control-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.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: 2px 6px;
|
|
border-radius: 4px;
|
|
transition: background-color 0.2s ease;
|
|
display: inline-block;
|
|
min-width: 100px;
|
|
}
|
|
|
|
.alias-display:hover {
|
|
background-color: rgba(139, 92, 246, 0.2);
|
|
}
|
|
|
|
.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 {
|
|
padding: 0.75rem;
|
|
border-bottom: 1px solid #4c1d95;
|
|
transition: background-color 0.3s ease;
|
|
background: rgba(45, 27, 105, 0.4);
|
|
}
|
|
|
|
.detection-item:hover {
|
|
background: rgba(74, 27, 105, 0.6);
|
|
}
|
|
|
|
.detection-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.detection-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 0.3rem;
|
|
}
|
|
|
|
.detection-type {
|
|
background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%);
|
|
color: white;
|
|
padding: 0.2rem 0.6rem;
|
|
border-radius: 12px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.detection-time {
|
|
color: #d1d5db;
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.detection-details {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
gap: 0.25rem;
|
|
font-size: 0.8rem;
|
|
margin-top: 0.4rem;
|
|
}
|
|
|
|
.detection-details.compact {
|
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
gap: 0.2rem;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.detail-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0.2rem 0;
|
|
}
|
|
|
|
.detail-label {
|
|
font-weight: 600;
|
|
color: #c084fc;
|
|
min-width: 80px;
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.detail-value {
|
|
color: #e0e0e0;
|
|
text-align: right;
|
|
word-break: break-all;
|
|
max-width: 60%;
|
|
}
|
|
|
|
.gps-info {
|
|
background: rgba(16, 185, 129, 0.2);
|
|
border: 1px solid #10b981;
|
|
border-radius: 6px;
|
|
padding: 0.4rem;
|
|
margin-top: 0.4rem;
|
|
}
|
|
|
|
.gps-info h4 {
|
|
color: #34d399;
|
|
margin-bottom: 0.2rem;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.header-left {
|
|
justify-content: center;
|
|
}
|
|
|
|
.controls {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.control-group {
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.status {
|
|
justify-content: center;
|
|
}
|
|
|
|
.detection-details {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="header">
|
|
<h1 class="flock-you-title">FLOCK-YOU</h1>
|
|
<div class="header-content">
|
|
<div class="controls">
|
|
<div class="control-group">
|
|
<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="control-group">
|
|
<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 class="status">
|
|
<div class="status-group">
|
|
<div class="status-indicator" id="flockStatus"></div>
|
|
<span id="flockStatusText">Sniffer Disconnected</span>
|
|
</div>
|
|
<div class="status-group">
|
|
<div class="status-indicator" id="gpsStatus"></div>
|
|
<span id="gpsStatusText">GPS Disconnected</span>
|
|
</div>
|
|
<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>
|
|
|
|
<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="export-btn" onclick="toggleCompactView()" id="compactViewBtn">Compact View</button>
|
|
<button class="export-btn" onclick="testDetection()" style="background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%);">Test Detection</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');
|
|
const text = document.getElementById('flockStatusText');
|
|
|
|
if (connected) {
|
|
indicator.classList.add('active');
|
|
text.textContent = 'Sniffer Connected';
|
|
} else {
|
|
indicator.classList.remove('active');
|
|
text.textContent = 'Sniffer Disconnected';
|
|
}
|
|
}
|
|
|
|
function updateGpsStatus(connected) {
|
|
const indicator = document.getElementById('gpsStatus');
|
|
const text = document.getElementById('gpsStatusText');
|
|
|
|
if (connected) {
|
|
indicator.classList.add('active');
|
|
text.textContent = 'GPS Connected';
|
|
} else {
|
|
indicator.classList.remove('active');
|
|
text.textContent = 'GPS Disconnected';
|
|
}
|
|
}
|
|
|
|
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 === 'ble').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;
|
|
}
|
|
|
|
const isCompact = container.classList.contains('compact-view');
|
|
|
|
container.innerHTML = detectionsToRender.map(detection => {
|
|
// Build all available fields dynamically
|
|
const fields = [];
|
|
|
|
// Core fields (always show)
|
|
if (detection.protocol) fields.push(['Protocol', detection.protocol]);
|
|
if (detection.mac_address) fields.push(['MAC', detection.mac_address]);
|
|
if (detection.manufacturer) fields.push(['Manufacturer', detection.manufacturer]);
|
|
if (detection.rssi !== undefined) fields.push(['RSSI', `${detection.rssi} dBm`]);
|
|
|
|
// Additional fields (show more in normal view)
|
|
if (!isCompact) {
|
|
if (detection.signal_strength) fields.push(['Signal', detection.signal_strength]);
|
|
if (detection.channel) fields.push(['Channel', detection.channel]);
|
|
if (detection.frequency) fields.push(['Freq', detection.frequency]);
|
|
if (detection.ssid) fields.push(['SSID', detection.ssid]);
|
|
if (detection.device_name) fields.push(['Device', detection.device_name]);
|
|
if (detection.service_uuid) fields.push(['Service', detection.service_uuid]);
|
|
if (detection.tx_power) fields.push(['TX Power', detection.tx_power]);
|
|
if (detection.company_identifier) fields.push(['Company ID', detection.company_identifier]);
|
|
if (detection.advertisement_data) fields.push(['Adv Data', detection.advertisement_data]);
|
|
if (detection.scan_response) fields.push(['Scan Resp', detection.scan_response]);
|
|
if (detection.timestamp) fields.push(['Timestamp', detection.timestamp]);
|
|
if (detection.server_timestamp) fields.push(['Server Time', new Date(detection.server_timestamp).toLocaleTimeString()]);
|
|
} else {
|
|
// Compact view - show only essential fields
|
|
if (detection.ssid) fields.push(['SSID', detection.ssid]);
|
|
if (detection.device_name) fields.push(['Device', detection.device_name]);
|
|
if (detection.channel) fields.push(['Ch', detection.channel]);
|
|
}
|
|
|
|
// Build the details HTML
|
|
const detailsHtml = fields.map(([label, value]) => `
|
|
<div class="detail-item">
|
|
<span class="detail-label">${label}:</span>
|
|
<span class="detail-value">${value}</span>
|
|
</div>
|
|
`).join('');
|
|
|
|
// Build GPS info if available
|
|
let gpsHtml = '';
|
|
if (detection.gps && !isCompact) {
|
|
const gpsFields = [];
|
|
if (detection.gps.latitude !== undefined && detection.gps.longitude !== undefined) {
|
|
gpsFields.push(['Coordinates', `${detection.gps.latitude.toFixed(6)}, ${detection.gps.longitude.toFixed(6)}`]);
|
|
}
|
|
if (detection.gps.altitude !== undefined) gpsFields.push(['Altitude', `${detection.gps.altitude}m`]);
|
|
if (detection.gps.satellites !== undefined) gpsFields.push(['Satellites', detection.gps.satellites]);
|
|
if (detection.gps.fix_quality !== undefined) gpsFields.push(['Fix Quality', detection.gps.fix_quality]);
|
|
if (detection.gps.timestamp) gpsFields.push(['GPS Time', detection.gps.timestamp]);
|
|
|
|
gpsHtml = `
|
|
<div class="gps-info">
|
|
<h4>📍 GPS Location</h4>
|
|
${gpsFields.map(([label, value]) => `
|
|
<div class="detail-item">
|
|
<span class="detail-label">${label}:</span>
|
|
<span class="detail-value">${value}</span>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
`;
|
|
} else if (detection.gps && isCompact) {
|
|
// Compact GPS display
|
|
if (detection.gps.latitude !== undefined && detection.gps.longitude !== undefined) {
|
|
gpsHtml = `
|
|
<div class="gps-info">
|
|
<h4>📍 ${detection.gps.latitude.toFixed(4)}, ${detection.gps.longitude.toFixed(4)}</h4>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return `
|
|
<div class="detection-item">
|
|
<div class="detection-header">
|
|
<span class="detection-type">${detection.detection_method ? detection.detection_method.toUpperCase() : 'UNKNOWN'}</span>
|
|
<span class="detection-time">${detection.detection_time || detection.timestamp || 'Unknown'}</span>
|
|
</div>
|
|
<div class="detection-details ${isCompact ? 'compact' : ''}">
|
|
${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>
|
|
${gpsHtml}
|
|
</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();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function toggleCompactView() {
|
|
const container = document.getElementById('detectionsList');
|
|
const button = document.getElementById('compactViewBtn');
|
|
const isCompact = container.classList.contains('compact-view');
|
|
|
|
if (isCompact) {
|
|
container.classList.remove('compact-view');
|
|
button.textContent = 'Compact View';
|
|
button.style.background = 'linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%)';
|
|
} else {
|
|
container.classList.add('compact-view');
|
|
button.textContent = 'Normal View';
|
|
button.style.background = 'linear-gradient(135deg, #059669 0%, #10b981 100%)';
|
|
}
|
|
|
|
renderDetections(); // Re-render with new view
|
|
}
|
|
|
|
function testDetection() {
|
|
console.log('Testing detection system...');
|
|
fetch('/api/test/detection', {
|
|
method: 'POST'
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
console.log('Test detection response:', data);
|
|
if (data.status === 'success') {
|
|
console.log('Test detection added successfully');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Test detection error:', error);
|
|
});
|
|
}
|
|
|
|
// 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('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>
|