2025-10-19 04:10:32 -05:00
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<html lang="en">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
2025-10-19 04:54:30 -05:00
|
|
|
<title>Vehicle Dashboard</title>
|
|
|
|
|
<link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@300;400;500;600;700&family=Orbitron:wght@400;500;600;700;900&display=swap" rel="stylesheet">
|
2025-10-19 04:10:32 -05:00
|
|
|
<style>
|
|
|
|
|
* {
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 0;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-19 04:54:30 -05:00
|
|
|
@keyframes gradient-shift {
|
|
|
|
|
0% { background-position: 0% 50%; }
|
|
|
|
|
50% { background-position: 100% 50%; }
|
|
|
|
|
100% { background-position: 0% 50%; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes glow-pulse {
|
|
|
|
|
0%, 100% {
|
|
|
|
|
box-shadow: 0 0 20px rgba(220, 38, 38, 0.3),
|
|
|
|
|
0 0 40px rgba(220, 38, 38, 0.1),
|
|
|
|
|
inset 0 0 20px rgba(220, 38, 38, 0.1);
|
|
|
|
|
}
|
|
|
|
|
50% {
|
|
|
|
|
box-shadow: 0 0 30px rgba(220, 38, 38, 0.5),
|
|
|
|
|
0 0 60px rgba(220, 38, 38, 0.2),
|
|
|
|
|
inset 0 0 30px rgba(220, 38, 38, 0.2);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-19 04:10:32 -05:00
|
|
|
body {
|
2025-10-19 04:54:30 -05:00
|
|
|
font-family: 'Rajdhani', sans-serif;
|
|
|
|
|
background: #000000;
|
|
|
|
|
background-image:
|
|
|
|
|
radial-gradient(circle at 20% 50%, rgba(220, 38, 38, 0.15) 0%, transparent 50%),
|
|
|
|
|
radial-gradient(circle at 80% 50%, rgba(127, 29, 29, 0.15) 0%, transparent 50%),
|
|
|
|
|
linear-gradient(180deg, #000000 0%, #0a0a0a 50%, #000000 100%);
|
|
|
|
|
background-size: 200% 200%;
|
|
|
|
|
animation: gradient-shift 15s ease infinite;
|
|
|
|
|
color: #ffffff;
|
2025-10-19 04:10:32 -05:00
|
|
|
padding: 20px;
|
|
|
|
|
min-height: 100vh;
|
2025-10-19 04:54:30 -05:00
|
|
|
position: relative;
|
|
|
|
|
overflow-x: hidden;
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header {
|
2025-10-19 04:54:30 -05:00
|
|
|
background: rgba(10, 10, 10, 0.6);
|
|
|
|
|
backdrop-filter: blur(20px) saturate(180%);
|
|
|
|
|
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
|
|
|
|
border: 1px solid rgba(220, 38, 38, 0.3);
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
padding: 25px 30px;
|
|
|
|
|
margin-bottom: 25px;
|
|
|
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4),
|
|
|
|
|
0 0 40px rgba(220, 38, 38, 0.1),
|
|
|
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
|
|
|
position: relative;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header::before {
|
|
|
|
|
content: '';
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: -100%;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
background: linear-gradient(90deg, transparent, rgba(220, 38, 38, 0.1), transparent);
|
|
|
|
|
animation: slide 3s ease-in-out infinite;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes slide {
|
|
|
|
|
0% { left: -100%; }
|
|
|
|
|
100% { left: 100%; }
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header h1 {
|
2025-10-19 04:54:30 -05:00
|
|
|
font-family: 'Orbitron', sans-serif;
|
|
|
|
|
font-weight: 900;
|
|
|
|
|
font-size: 32px;
|
|
|
|
|
background: linear-gradient(135deg, #ffffff 0%, #dc2626 50%, #7f1d1d 100%);
|
|
|
|
|
-webkit-background-clip: text;
|
|
|
|
|
-webkit-text-fill-color: transparent;
|
|
|
|
|
background-clip: text;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
letter-spacing: 2px;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
position: relative;
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
2025-10-19 04:54:30 -05:00
|
|
|
.header p {
|
|
|
|
|
color: rgba(255, 255, 255, 0.7);
|
2025-10-19 04:10:32 -05:00
|
|
|
font-size: 14px;
|
2025-10-19 04:54:30 -05:00
|
|
|
letter-spacing: 1px;
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
padding: 8px 20px;
|
|
|
|
|
border-radius: 30px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
letter-spacing: 1px;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
font-family: 'Orbitron', sans-serif;
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status.connected {
|
2025-10-19 04:54:30 -05:00
|
|
|
background: linear-gradient(135deg, rgba(16, 185, 129, 0.3), rgba(5, 150, 105, 0.3));
|
|
|
|
|
border: 1px solid rgba(16, 185, 129, 0.5);
|
|
|
|
|
color: #10b981;
|
|
|
|
|
box-shadow: 0 0 20px rgba(16, 185, 129, 0.3),
|
|
|
|
|
inset 0 0 10px rgba(16, 185, 129, 0.1);
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status.disconnected {
|
2025-10-19 04:54:30 -05:00
|
|
|
background: linear-gradient(135deg, rgba(239, 68, 68, 0.3), rgba(220, 38, 38, 0.3));
|
|
|
|
|
border: 1px solid rgba(239, 68, 68, 0.5);
|
|
|
|
|
color: #ef4444;
|
|
|
|
|
box-shadow: 0 0 20px rgba(239, 68, 68, 0.3),
|
|
|
|
|
inset 0 0 10px rgba(239, 68, 68, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status.connecting {
|
|
|
|
|
background: linear-gradient(135deg, rgba(245, 158, 11, 0.3), rgba(217, 119, 6, 0.3));
|
|
|
|
|
border: 1px solid rgba(245, 158, 11, 0.5);
|
|
|
|
|
color: #f59e0b;
|
|
|
|
|
box-shadow: 0 0 20px rgba(245, 158, 11, 0.3),
|
|
|
|
|
inset 0 0 10px rgba(245, 158, 11, 0.1);
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.vehicle-card {
|
2025-10-19 04:54:30 -05:00
|
|
|
background: rgba(15, 15, 15, 0.7);
|
|
|
|
|
backdrop-filter: blur(20px) saturate(180%);
|
|
|
|
|
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
|
|
|
|
border: 1px solid rgba(220, 38, 38, 0.2);
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
padding: 25px;
|
|
|
|
|
margin-bottom: 25px;
|
|
|
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5),
|
|
|
|
|
0 0 40px rgba(220, 38, 38, 0.05),
|
|
|
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
position: relative;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.vehicle-card::before {
|
|
|
|
|
content: '';
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: -2px;
|
|
|
|
|
left: -2px;
|
|
|
|
|
right: -2px;
|
|
|
|
|
bottom: -2px;
|
|
|
|
|
background: linear-gradient(135deg, rgba(220, 38, 38, 0.3), rgba(127, 29, 29, 0.1));
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transition: opacity 0.3s ease;
|
|
|
|
|
z-index: -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.vehicle-card:hover {
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
border-color: rgba(220, 38, 38, 0.4);
|
|
|
|
|
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.6),
|
|
|
|
|
0 0 60px rgba(220, 38, 38, 0.15),
|
|
|
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.vehicle-card:hover::before {
|
|
|
|
|
opacity: 1;
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.vehicle-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
2025-10-19 04:54:30 -05:00
|
|
|
margin-bottom: 20px;
|
|
|
|
|
padding-bottom: 20px;
|
|
|
|
|
border-bottom: 1px solid rgba(220, 38, 38, 0.2);
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.vehicle-header::after {
|
|
|
|
|
content: '';
|
|
|
|
|
position: absolute;
|
|
|
|
|
bottom: -1px;
|
|
|
|
|
left: 0;
|
|
|
|
|
width: 30%;
|
|
|
|
|
height: 1px;
|
|
|
|
|
background: linear-gradient(90deg, rgba(220, 38, 38, 0.8), transparent);
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.vehicle-header h2 {
|
2025-10-19 04:54:30 -05:00
|
|
|
font-family: 'Orbitron', sans-serif;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
font-size: 26px;
|
|
|
|
|
background: linear-gradient(135deg, #ffffff 0%, #dc2626 100%);
|
|
|
|
|
-webkit-background-clip: text;
|
|
|
|
|
-webkit-text-fill-color: transparent;
|
|
|
|
|
background-clip: text;
|
|
|
|
|
letter-spacing: 1px;
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.vehicle-info {
|
|
|
|
|
display: grid;
|
2025-10-19 04:54:30 -05:00
|
|
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
2025-10-19 04:10:32 -05:00
|
|
|
gap: 15px;
|
2025-10-19 04:54:30 -05:00
|
|
|
margin-bottom: 25px;
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.info-box {
|
2025-10-19 04:54:30 -05:00
|
|
|
background: rgba(20, 20, 20, 0.6);
|
|
|
|
|
backdrop-filter: blur(10px);
|
|
|
|
|
-webkit-backdrop-filter: blur(10px);
|
|
|
|
|
border: 1px solid rgba(220, 38, 38, 0.15);
|
|
|
|
|
padding: 18px;
|
|
|
|
|
border-radius: 12px;
|
2025-10-19 04:10:32 -05:00
|
|
|
text-align: center;
|
2025-10-19 04:54:30 -05:00
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
position: relative;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.info-box::before {
|
|
|
|
|
content: '';
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 2px;
|
|
|
|
|
background: linear-gradient(90deg, transparent, rgba(220, 38, 38, 0.5), transparent);
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transition: opacity 0.3s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.info-box:hover {
|
|
|
|
|
background: rgba(25, 25, 25, 0.8);
|
|
|
|
|
border-color: rgba(220, 38, 38, 0.3);
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
box-shadow: 0 4px 20px rgba(220, 38, 38, 0.15);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.info-box:hover::before {
|
|
|
|
|
opacity: 1;
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.info-box .label {
|
2025-10-19 04:54:30 -05:00
|
|
|
font-size: 11px;
|
|
|
|
|
color: rgba(255, 255, 255, 0.5);
|
2025-10-19 04:10:32 -05:00
|
|
|
text-transform: uppercase;
|
2025-10-19 04:54:30 -05:00
|
|
|
margin-bottom: 8px;
|
|
|
|
|
letter-spacing: 1.5px;
|
|
|
|
|
font-weight: 600;
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.info-box .value {
|
2025-10-19 04:54:30 -05:00
|
|
|
font-family: 'Orbitron', sans-serif;
|
|
|
|
|
font-size: 28px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
text-shadow: 0 0 10px rgba(220, 38, 38, 0.5);
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.controls {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
gap: 15px;
|
2025-10-19 04:54:30 -05:00
|
|
|
margin-bottom: 25px;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
background: rgba(220, 38, 38, 0.1);
|
|
|
|
|
backdrop-filter: blur(10px);
|
|
|
|
|
-webkit-backdrop-filter: blur(10px);
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
border: 1px solid rgba(220, 38, 38, 0.3);
|
|
|
|
|
box-shadow: inset 0 0 20px rgba(220, 38, 38, 0.05);
|
|
|
|
|
animation: glow-pulse 3s ease-in-out infinite;
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.control-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
2025-10-19 04:54:30 -05:00
|
|
|
justify-content: center;
|
|
|
|
|
gap: 8px;
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.control-label {
|
2025-10-19 04:54:30 -05:00
|
|
|
font-weight: 600;
|
|
|
|
|
color: rgba(255, 255, 255, 0.8);
|
|
|
|
|
letter-spacing: 1px;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
font-size: 13px;
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.control-value {
|
2025-10-19 04:54:30 -05:00
|
|
|
font-family: 'Orbitron', sans-serif;
|
|
|
|
|
font-size: 32px;
|
|
|
|
|
font-weight: 900;
|
|
|
|
|
color: #dc2626;
|
|
|
|
|
text-shadow: 0 0 15px rgba(220, 38, 38, 0.7),
|
|
|
|
|
0 0 30px rgba(220, 38, 38, 0.4);
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.strategy-box {
|
2025-10-19 04:54:30 -05:00
|
|
|
background: rgba(220, 38, 38, 0.08);
|
|
|
|
|
backdrop-filter: blur(10px);
|
|
|
|
|
-webkit-backdrop-filter: blur(10px);
|
|
|
|
|
border: 1px solid rgba(220, 38, 38, 0.3);
|
|
|
|
|
padding: 20px;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
margin-bottom: 25px;
|
|
|
|
|
box-shadow: inset 0 0 30px rgba(220, 38, 38, 0.05),
|
|
|
|
|
0 4px 20px rgba(0, 0, 0, 0.3);
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.strategy-box h3 {
|
2025-10-19 04:54:30 -05:00
|
|
|
font-family: 'Orbitron', sans-serif;
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
letter-spacing: 1px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.strategy-box p {
|
|
|
|
|
color: rgba(255, 255, 255, 0.7);
|
|
|
|
|
line-height: 1.6;
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.strategy-box .risk {
|
|
|
|
|
display: inline-block;
|
2025-10-19 04:54:30 -05:00
|
|
|
padding: 4px 12px;
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
font-weight: 700;
|
2025-10-19 04:10:32 -05:00
|
|
|
text-transform: uppercase;
|
2025-10-19 04:54:30 -05:00
|
|
|
margin-left: 12px;
|
|
|
|
|
letter-spacing: 1px;
|
|
|
|
|
font-family: 'Orbitron', sans-serif;
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.risk.low {
|
2025-10-19 04:54:30 -05:00
|
|
|
background: linear-gradient(135deg, rgba(16, 185, 129, 0.3), rgba(5, 150, 105, 0.3));
|
|
|
|
|
border: 1px solid rgba(16, 185, 129, 0.5);
|
|
|
|
|
color: #10b981;
|
|
|
|
|
box-shadow: 0 0 10px rgba(16, 185, 129, 0.3);
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.risk.medium {
|
2025-10-19 04:54:30 -05:00
|
|
|
background: linear-gradient(135deg, rgba(245, 158, 11, 0.3), rgba(217, 119, 6, 0.3));
|
|
|
|
|
border: 1px solid rgba(245, 158, 11, 0.5);
|
|
|
|
|
color: #f59e0b;
|
|
|
|
|
box-shadow: 0 0 10px rgba(245, 158, 11, 0.3);
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.risk.high {
|
2025-10-19 04:54:30 -05:00
|
|
|
background: linear-gradient(135deg, rgba(239, 68, 68, 0.3), rgba(220, 38, 38, 0.3));
|
|
|
|
|
border: 1px solid rgba(239, 68, 68, 0.5);
|
|
|
|
|
color: #ef4444;
|
|
|
|
|
box-shadow: 0 0 10px rgba(239, 68, 68, 0.3);
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.risk.critical {
|
2025-10-19 04:54:30 -05:00
|
|
|
background: linear-gradient(135deg, rgba(127, 29, 29, 0.5), rgba(220, 38, 38, 0.5));
|
|
|
|
|
border: 1px solid rgba(220, 38, 38, 0.7);
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
box-shadow: 0 0 15px rgba(220, 38, 38, 0.5);
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
table {
|
|
|
|
|
width: 100%;
|
|
|
|
|
border-collapse: collapse;
|
2025-10-19 04:54:30 -05:00
|
|
|
background: rgba(10, 10, 10, 0.4);
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
overflow: hidden;
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
th {
|
2025-10-19 04:54:30 -05:00
|
|
|
background: linear-gradient(135deg, rgba(220, 38, 38, 0.3), rgba(127, 29, 29, 0.3));
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
padding: 16px 14px;
|
2025-10-19 04:10:32 -05:00
|
|
|
text-align: left;
|
2025-10-19 04:54:30 -05:00
|
|
|
font-weight: 700;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
letter-spacing: 1.5px;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
border-bottom: 2px solid rgba(220, 38, 38, 0.5);
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
td {
|
2025-10-19 04:54:30 -05:00
|
|
|
padding: 14px;
|
|
|
|
|
border-bottom: 1px solid rgba(220, 38, 38, 0.1);
|
|
|
|
|
color: rgba(255, 255, 255, 0.9);
|
|
|
|
|
font-weight: 500;
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
2025-10-19 04:54:30 -05:00
|
|
|
tbody tr {
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
cursor: pointer;
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
2025-10-19 04:54:30 -05:00
|
|
|
tbody tr:hover {
|
|
|
|
|
background: rgba(220, 38, 38, 0.08);
|
|
|
|
|
box-shadow: inset 0 0 20px rgba(220, 38, 38, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Modal Styles */
|
|
|
|
|
.modal {
|
|
|
|
|
display: none;
|
|
|
|
|
position: fixed;
|
|
|
|
|
z-index: 2000;
|
|
|
|
|
left: 0;
|
|
|
|
|
top: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
background: rgba(0, 0, 0, 0.85);
|
|
|
|
|
backdrop-filter: blur(10px);
|
|
|
|
|
-webkit-backdrop-filter: blur(10px);
|
|
|
|
|
animation: fadeIn 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes fadeIn {
|
|
|
|
|
from { opacity: 0; }
|
|
|
|
|
to { opacity: 1; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-content {
|
|
|
|
|
background: rgba(15, 15, 15, 0.95);
|
|
|
|
|
backdrop-filter: blur(30px) saturate(180%);
|
|
|
|
|
-webkit-backdrop-filter: blur(30px) saturate(180%);
|
|
|
|
|
border: 1px solid rgba(220, 38, 38, 0.3);
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
margin: 5% auto;
|
|
|
|
|
padding: 30px;
|
|
|
|
|
width: 80%;
|
|
|
|
|
max-width: 900px;
|
|
|
|
|
max-height: 80vh;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.8),
|
|
|
|
|
0 0 80px rgba(220, 38, 38, 0.2),
|
|
|
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
|
|
|
animation: slideIn 0.3s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes slideIn {
|
|
|
|
|
from {
|
|
|
|
|
transform: translateY(-50px);
|
|
|
|
|
opacity: 0;
|
|
|
|
|
}
|
|
|
|
|
to {
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 25px;
|
|
|
|
|
padding-bottom: 20px;
|
|
|
|
|
border-bottom: 1px solid rgba(220, 38, 38, 0.2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-header h2 {
|
|
|
|
|
font-family: 'Orbitron', sans-serif;
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
background: linear-gradient(135deg, #ffffff 0%, #dc2626 100%);
|
|
|
|
|
-webkit-background-clip: text;
|
|
|
|
|
-webkit-text-fill-color: transparent;
|
|
|
|
|
background-clip: text;
|
|
|
|
|
letter-spacing: 1px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.close {
|
|
|
|
|
color: rgba(255, 255, 255, 0.6);
|
|
|
|
|
font-size: 32px;
|
|
|
|
|
font-weight: 300;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
line-height: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.close:hover {
|
|
|
|
|
color: #dc2626;
|
|
|
|
|
transform: scale(1.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-body {
|
|
|
|
|
color: rgba(255, 255, 255, 0.9);
|
|
|
|
|
line-height: 1.8;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-section {
|
|
|
|
|
margin-bottom: 25px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-section h3 {
|
|
|
|
|
font-family: 'Orbitron', sans-serif;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #dc2626;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 1px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-section p {
|
|
|
|
|
color: rgba(255, 255, 255, 0.8);
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-data {
|
|
|
|
|
background: rgba(220, 38, 38, 0.05);
|
|
|
|
|
border: 1px solid rgba(220, 38, 38, 0.2);
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
padding: 15px;
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
font-family: 'Rajdhani', sans-serif;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-data-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
padding: 8px 0;
|
|
|
|
|
border-bottom: 1px solid rgba(220, 38, 38, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-data-row:last-child {
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-data-label {
|
|
|
|
|
color: rgba(255, 255, 255, 0.6);
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
letter-spacing: 1px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-data-value {
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
font-family: 'Orbitron', sans-serif;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.no-strategy-message {
|
2025-10-19 04:10:32 -05:00
|
|
|
text-align: center;
|
|
|
|
|
padding: 40px;
|
2025-10-19 04:54:30 -05:00
|
|
|
color: rgba(255, 255, 255, 0.5);
|
|
|
|
|
font-style: italic;
|
|
|
|
|
background: rgba(220, 38, 38, 0.05);
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
border: 1px dashed rgba(220, 38, 38, 0.2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.no-data {
|
|
|
|
|
text-align: center;
|
|
|
|
|
padding: 60px 40px;
|
|
|
|
|
color: rgba(255, 255, 255, 0.4);
|
2025-10-19 04:10:32 -05:00
|
|
|
font-style: italic;
|
2025-10-19 04:54:30 -05:00
|
|
|
font-size: 16px;
|
|
|
|
|
letter-spacing: 1px;
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.timestamp {
|
2025-10-19 04:54:30 -05:00
|
|
|
font-size: 11px;
|
|
|
|
|
color: rgba(255, 255, 255, 0.4);
|
|
|
|
|
letter-spacing: 0.5px;
|
|
|
|
|
font-family: 'Orbitron', sans-serif;
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.badge {
|
|
|
|
|
display: inline-block;
|
2025-10-19 04:54:30 -05:00
|
|
|
padding: 4px 10px;
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
letter-spacing: 1px;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
font-family: 'Orbitron', sans-serif;
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.badge.improving {
|
2025-10-19 04:54:30 -05:00
|
|
|
background: linear-gradient(135deg, rgba(16, 185, 129, 0.25), rgba(5, 150, 105, 0.25));
|
|
|
|
|
border: 1px solid rgba(16, 185, 129, 0.4);
|
|
|
|
|
color: #10b981;
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.badge.stable {
|
2025-10-19 04:54:30 -05:00
|
|
|
background: linear-gradient(135deg, rgba(59, 130, 246, 0.25), rgba(37, 99, 235, 0.25));
|
|
|
|
|
border: 1px solid rgba(59, 130, 246, 0.4);
|
|
|
|
|
color: #3b82f6;
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.badge.declining {
|
2025-10-19 04:54:30 -05:00
|
|
|
background: linear-gradient(135deg, rgba(239, 68, 68, 0.25), rgba(220, 38, 38, 0.25));
|
|
|
|
|
border: 1px solid rgba(239, 68, 68, 0.4);
|
|
|
|
|
color: #ef4444;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Scrollbar styling */
|
|
|
|
|
::-webkit-scrollbar {
|
|
|
|
|
width: 10px;
|
|
|
|
|
height: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
::-webkit-scrollbar-track {
|
|
|
|
|
background: rgba(0, 0, 0, 0.3);
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
|
|
|
background: linear-gradient(180deg, rgba(220, 38, 38, 0.5), rgba(127, 29, 29, 0.5));
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
border: 2px solid rgba(0, 0, 0, 0.3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
::-webkit-scrollbar-thumb:hover {
|
|
|
|
|
background: linear-gradient(180deg, rgba(220, 38, 38, 0.7), rgba(127, 29, 29, 0.7));
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<div class="header">
|
2025-10-19 04:54:30 -05:00
|
|
|
<h1>PERFORMANCE REPORT</h1>
|
|
|
|
|
<p>REAL-TIME VEHICLE TELEMETRY • STRATEGY GENERATION • CONTROL OUTPUTS</p>
|
|
|
|
|
<div style="margin-top: 15px;">
|
|
|
|
|
<span class="status connecting" id="wsStatus">● CONNECTING...</span>
|
2025-10-19 04:10:32 -05:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div id="vehicles">
|
|
|
|
|
<div class="no-data">
|
2025-10-19 04:54:30 -05:00
|
|
|
⚠️ NO VEHICLE CONNECTIONS • AWAITING PI CLIENT TELEMETRY STREAM...
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Modal for displaying lap details and AI reasoning -->
|
|
|
|
|
<div id="lapModal" class="modal">
|
|
|
|
|
<div class="modal-content">
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
<h2 id="modalTitle">LAP DETAILS</h2>
|
|
|
|
|
<span class="close">×</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-body" id="modalBody">
|
|
|
|
|
<!-- Content will be populated by JavaScript -->
|
|
|
|
|
</div>
|
2025-10-19 04:10:32 -05:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
// Store vehicle data
|
|
|
|
|
const vehicles = new Map();
|
|
|
|
|
|
|
|
|
|
// WebSocket connection to backend
|
|
|
|
|
let ws = null;
|
|
|
|
|
let reconnectInterval = null;
|
|
|
|
|
|
2025-10-19 04:54:30 -05:00
|
|
|
function updateStatus() {
|
|
|
|
|
const statusElement = document.getElementById('wsStatus');
|
|
|
|
|
const connectedVehicles = Array.from(vehicles.values()).filter(v => v.connected);
|
|
|
|
|
|
|
|
|
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
|
|
|
statusElement.textContent = '● WS DISCONNECTED';
|
|
|
|
|
statusElement.className = 'status disconnected';
|
|
|
|
|
} else if (connectedVehicles.length === 0) {
|
|
|
|
|
statusElement.textContent = '● NO VEHICLES';
|
|
|
|
|
statusElement.className = 'status disconnected';
|
|
|
|
|
} else if (connectedVehicles.length === 1) {
|
|
|
|
|
statusElement.textContent = '● 1 VEHICLE ACTIVE';
|
|
|
|
|
statusElement.className = 'status connected';
|
|
|
|
|
} else {
|
|
|
|
|
statusElement.textContent = `● ${connectedVehicles.length} VEHICLES ACTIVE`;
|
|
|
|
|
statusElement.className = 'status connected';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-19 04:10:32 -05:00
|
|
|
function connect() {
|
|
|
|
|
ws = new WebSocket('ws://localhost:9000/ws/dashboard');
|
|
|
|
|
|
|
|
|
|
ws.onopen = () => {
|
|
|
|
|
console.log('Dashboard WebSocket connected');
|
2025-10-19 04:54:30 -05:00
|
|
|
updateStatus();
|
2025-10-19 04:10:32 -05:00
|
|
|
clearInterval(reconnectInterval);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ws.onmessage = (event) => {
|
|
|
|
|
const data = JSON.parse(event.data);
|
|
|
|
|
handleMessage(data);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ws.onclose = () => {
|
|
|
|
|
console.log('Dashboard WebSocket disconnected');
|
2025-10-19 04:54:30 -05:00
|
|
|
updateStatus();
|
2025-10-19 04:10:32 -05:00
|
|
|
|
|
|
|
|
// Try to reconnect every 3 seconds
|
|
|
|
|
if (!reconnectInterval) {
|
|
|
|
|
reconnectInterval = setInterval(() => {
|
|
|
|
|
console.log('Attempting to reconnect...');
|
|
|
|
|
connect();
|
|
|
|
|
}, 3000);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ws.onerror = (error) => {
|
|
|
|
|
console.error('WebSocket error:', error);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleMessage(data) {
|
2025-10-19 05:29:30 -05:00
|
|
|
const { type, vehicle_id, lap_data, race_context, control_output, strategy, timestamp } = data;
|
2025-10-19 04:10:32 -05:00
|
|
|
|
|
|
|
|
if (type === 'vehicle_connected') {
|
|
|
|
|
addVehicle(vehicle_id, timestamp);
|
|
|
|
|
} else if (type === 'vehicle_disconnected') {
|
|
|
|
|
removeVehicle(vehicle_id);
|
|
|
|
|
} else if (type === 'lap_data') {
|
2025-10-19 05:29:30 -05:00
|
|
|
addLapData(vehicle_id, lap_data, race_context, control_output, strategy, timestamp);
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addVehicle(vehicleId, timestamp) {
|
|
|
|
|
if (!vehicles.has(vehicleId)) {
|
|
|
|
|
vehicles.set(vehicleId, {
|
|
|
|
|
id: vehicleId,
|
|
|
|
|
connected: true,
|
|
|
|
|
connectedAt: timestamp,
|
|
|
|
|
laps: [],
|
|
|
|
|
currentControls: { brake_bias: 5, differential_slip: 5 },
|
|
|
|
|
currentStrategy: null
|
|
|
|
|
});
|
2025-10-19 04:54:30 -05:00
|
|
|
updateStatus();
|
2025-10-19 04:10:32 -05:00
|
|
|
renderVehicles();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeVehicle(vehicleId) {
|
|
|
|
|
if (vehicles.has(vehicleId)) {
|
|
|
|
|
const vehicle = vehicles.get(vehicleId);
|
|
|
|
|
vehicle.connected = false;
|
2025-10-19 04:54:30 -05:00
|
|
|
updateStatus();
|
2025-10-19 04:10:32 -05:00
|
|
|
renderVehicles();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-19 05:29:30 -05:00
|
|
|
function addLapData(vehicleId, lapData, raceContext, controlOutput, strategy, timestamp) {
|
2025-10-19 04:10:32 -05:00
|
|
|
if (!vehicles.has(vehicleId)) {
|
|
|
|
|
addVehicle(vehicleId, timestamp);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const vehicle = vehicles.get(vehicleId);
|
|
|
|
|
vehicle.laps.push({
|
|
|
|
|
...lapData,
|
2025-10-19 05:29:30 -05:00
|
|
|
race_context: raceContext, // Add race context with position and gaps
|
2025-10-19 04:10:32 -05:00
|
|
|
control_output: controlOutput,
|
|
|
|
|
strategy: strategy,
|
|
|
|
|
timestamp: timestamp
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Update current controls and strategy
|
|
|
|
|
if (controlOutput) {
|
|
|
|
|
vehicle.currentControls = controlOutput;
|
|
|
|
|
}
|
|
|
|
|
if (strategy) {
|
|
|
|
|
vehicle.currentStrategy = strategy;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderVehicles();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderVehicles() {
|
|
|
|
|
const container = document.getElementById('vehicles');
|
|
|
|
|
|
|
|
|
|
if (vehicles.size === 0) {
|
2025-10-19 04:54:30 -05:00
|
|
|
container.innerHTML = '<div class="no-data">⚠️ NO VEHICLE CONNECTIONS • AWAITING PI CLIENT TELEMETRY STREAM...</div>';
|
2025-10-19 04:10:32 -05:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
container.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
vehicles.forEach((vehicle, vehicleId) => {
|
|
|
|
|
const card = document.createElement('div');
|
|
|
|
|
card.className = 'vehicle-card';
|
|
|
|
|
|
|
|
|
|
const statusBadge = vehicle.connected
|
2025-10-19 04:54:30 -05:00
|
|
|
? '<span class="status connected">● CONNECTED</span>'
|
|
|
|
|
: '<span class="status disconnected">● DISCONNECTED</span>';
|
2025-10-19 04:10:32 -05:00
|
|
|
|
|
|
|
|
let strategyHtml = '';
|
|
|
|
|
if (vehicle.currentStrategy) {
|
|
|
|
|
strategyHtml = `
|
|
|
|
|
<div class="strategy-box">
|
|
|
|
|
<h3>
|
2025-10-19 04:54:30 -05:00
|
|
|
ACTIVE STRATEGY: ${vehicle.currentStrategy.strategy_name.toUpperCase()}
|
|
|
|
|
<span class="risk ${vehicle.currentStrategy.risk_level}">${vehicle.currentStrategy.risk_level.toUpperCase()}</span>
|
2025-10-19 04:10:32 -05:00
|
|
|
</h3>
|
|
|
|
|
<p>${vehicle.currentStrategy.brief_description || ''}</p>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
card.innerHTML = `
|
|
|
|
|
<div class="vehicle-header">
|
|
|
|
|
<div>
|
2025-10-19 04:54:30 -05:00
|
|
|
<h2>VEHICLE #${vehicleId}</h2>
|
|
|
|
|
<span class="timestamp">CONNECTED: ${new Date(vehicle.connectedAt).toLocaleString().toUpperCase()}</span>
|
2025-10-19 04:10:32 -05:00
|
|
|
</div>
|
|
|
|
|
${statusBadge}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="vehicle-info">
|
|
|
|
|
<div class="info-box">
|
|
|
|
|
<div class="label">Total Laps</div>
|
|
|
|
|
<div class="value">${vehicle.laps.length}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="info-box">
|
2025-10-19 04:54:30 -05:00
|
|
|
<div class="label">Current Lap</div>
|
2025-10-19 04:10:32 -05:00
|
|
|
<div class="value">${vehicle.laps.length > 0 ? vehicle.laps[vehicle.laps.length - 1].lap : 'N/A'}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="info-box">
|
2025-10-19 04:54:30 -05:00
|
|
|
<div class="label">Strategies</div>
|
2025-10-19 04:10:32 -05:00
|
|
|
<div class="value">${vehicle.laps.filter(l => l.strategy).length}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="controls">
|
|
|
|
|
<div class="control-item">
|
2025-10-19 04:54:30 -05:00
|
|
|
<span class="control-label">Brake Bias</span>
|
2025-10-19 04:10:32 -05:00
|
|
|
<span class="control-value">${vehicle.currentControls.brake_bias}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="control-item">
|
2025-10-19 04:54:30 -05:00
|
|
|
<span class="control-label">Diff Slip</span>
|
2025-10-19 04:10:32 -05:00
|
|
|
<span class="control-value">${vehicle.currentControls.differential_slip}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
${strategyHtml}
|
|
|
|
|
|
2025-10-19 04:54:30 -05:00
|
|
|
<h3 style="margin-bottom: 15px; font-family: 'Orbitron', sans-serif; color: rgba(255,255,255,0.9); font-size: 16px; letter-spacing: 1px;">LAP HISTORY</h3>
|
2025-10-19 04:10:32 -05:00
|
|
|
${renderLapTable(vehicle.laps)}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
container.appendChild(card);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderLapTable(laps) {
|
|
|
|
|
if (laps.length === 0) {
|
2025-10-19 04:54:30 -05:00
|
|
|
return '<div class="no-data">NO LAP DATA RECORDED YET...</div>';
|
2025-10-19 04:10:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reverse to show most recent first
|
|
|
|
|
const reversedLaps = [...laps].reverse();
|
|
|
|
|
|
|
|
|
|
let tableHtml = `
|
|
|
|
|
<table>
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
2025-10-19 04:54:30 -05:00
|
|
|
<th>LAP</th>
|
|
|
|
|
<th>TIRE DEG</th>
|
|
|
|
|
<th>PACE TREND</th>
|
|
|
|
|
<th>CLIFF RISK</th>
|
|
|
|
|
<th>BRAKE</th>
|
|
|
|
|
<th>DIFF</th>
|
|
|
|
|
<th>STRATEGY</th>
|
|
|
|
|
<th>TIME</th>
|
2025-10-19 04:10:32 -05:00
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
`;
|
|
|
|
|
|
2025-10-19 04:54:30 -05:00
|
|
|
reversedLaps.forEach((lap, index) => {
|
2025-10-19 04:10:32 -05:00
|
|
|
const controls = lap.control_output || { brake_bias: '-', differential_slip: '-' };
|
2025-10-19 04:54:30 -05:00
|
|
|
const strategy = lap.strategy ? lap.strategy.strategy_name.toUpperCase() : '-';
|
|
|
|
|
const trendBadge = lap.pace_trend ? `<span class="badge ${lap.pace_trend}">${lap.pace_trend.toUpperCase()}</span>` : '-';
|
2025-10-19 04:10:32 -05:00
|
|
|
|
|
|
|
|
tableHtml += `
|
2025-10-19 04:54:30 -05:00
|
|
|
<tr onclick='showLapDetails(${JSON.stringify(lap).replace(/'/g, "'")})' style="cursor: pointer;">
|
2025-10-19 04:10:32 -05:00
|
|
|
<td><strong>${lap.lap}</strong></td>
|
|
|
|
|
<td>${(lap.tire_degradation_rate * 100).toFixed(0)}%</td>
|
|
|
|
|
<td>${trendBadge}</td>
|
|
|
|
|
<td>${(lap.tire_cliff_risk * 100).toFixed(0)}%</td>
|
|
|
|
|
<td><strong>${controls.brake_bias}</strong></td>
|
|
|
|
|
<td><strong>${controls.differential_slip}</strong></td>
|
|
|
|
|
<td>${strategy}</td>
|
|
|
|
|
<td class="timestamp">${new Date(lap.timestamp).toLocaleTimeString()}</td>
|
|
|
|
|
</tr>
|
|
|
|
|
`;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tableHtml += '</tbody></table>';
|
|
|
|
|
return tableHtml;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-19 04:54:30 -05:00
|
|
|
function showLapDetails(lap) {
|
|
|
|
|
const modal = document.getElementById('lapModal');
|
|
|
|
|
const modalTitle = document.getElementById('modalTitle');
|
|
|
|
|
const modalBody = document.getElementById('modalBody');
|
|
|
|
|
|
|
|
|
|
modalTitle.textContent = `LAP ${lap.lap} DETAILS`;
|
|
|
|
|
|
|
|
|
|
let bodyHtml = '';
|
|
|
|
|
|
2025-10-19 05:29:30 -05:00
|
|
|
// Race Position section (lap_time, position, gaps)
|
|
|
|
|
if (lap.race_context || lap.lap_time) {
|
|
|
|
|
bodyHtml += `
|
|
|
|
|
<div class="modal-section">
|
|
|
|
|
<h3>Race Position</h3>
|
|
|
|
|
<div class="modal-data">
|
|
|
|
|
${lap.lap_time ? `
|
|
|
|
|
<div class="modal-data-row">
|
|
|
|
|
<span class="modal-data-label">Lap Time</span>
|
|
|
|
|
<span class="modal-data-value">${lap.lap_time}</span>
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
${lap.race_context ? `
|
|
|
|
|
<div class="modal-data-row">
|
|
|
|
|
<span class="modal-data-label">Position</span>
|
|
|
|
|
<span class="modal-data-value">P${lap.race_context.position}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-data-row">
|
|
|
|
|
<span class="modal-data-label">Gap from Leader</span>
|
|
|
|
|
<span class="modal-data-value">${lap.race_context.gap_to_leader > 0 ? '+' + lap.race_context.gap_to_leader.toFixed(3) + 's' : (lap.race_context.position === 1 ? 'Leading' : 'N/A')}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-data-row">
|
|
|
|
|
<span class="modal-data-label">Gap from Car Ahead</span>
|
|
|
|
|
<span class="modal-data-value">${lap.race_context.gap_to_ahead > 0 ? '+' + lap.race_context.gap_to_ahead.toFixed(3) + 's' : (lap.race_context.position === 1 ? 'Leading' : 'N/A')}</span>
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-19 04:54:30 -05:00
|
|
|
// Telemetry data section
|
|
|
|
|
bodyHtml += `
|
|
|
|
|
<div class="modal-section">
|
|
|
|
|
<h3>Telemetry Data</h3>
|
|
|
|
|
<div class="modal-data">
|
|
|
|
|
<div class="modal-data-row">
|
|
|
|
|
<span class="modal-data-label">Tire Degradation Rate</span>
|
|
|
|
|
<span class="modal-data-value">${(lap.tire_degradation_rate * 100).toFixed(1)}%</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-data-row">
|
|
|
|
|
<span class="modal-data-label">Tire Cliff Risk</span>
|
|
|
|
|
<span class="modal-data-value">${(lap.tire_cliff_risk * 100).toFixed(1)}%</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-data-row">
|
|
|
|
|
<span class="modal-data-label">Pace Trend</span>
|
|
|
|
|
<span class="modal-data-value">${lap.pace_trend || 'N/A'}</span>
|
|
|
|
|
</div>
|
|
|
|
|
${lap.competitive_pressure !== undefined ? `
|
|
|
|
|
<div class="modal-data-row">
|
|
|
|
|
<span class="modal-data-label">Competitive Pressure</span>
|
|
|
|
|
<span class="modal-data-value">${(lap.competitive_pressure * 100).toFixed(1)}%</span>
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
${lap.position_trend !== undefined ? `
|
|
|
|
|
<div class="modal-data-row">
|
|
|
|
|
<span class="modal-data-label">Position Trend</span>
|
|
|
|
|
<span class="modal-data-value">${lap.position_trend.toUpperCase()}</span>
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
// Control outputs section
|
|
|
|
|
if (lap.control_output) {
|
|
|
|
|
bodyHtml += `
|
|
|
|
|
<div class="modal-section">
|
|
|
|
|
<h3>Control Outputs</h3>
|
|
|
|
|
<div class="modal-data">
|
|
|
|
|
<div class="modal-data-row">
|
|
|
|
|
<span class="modal-data-label">Brake Bias</span>
|
|
|
|
|
<span class="modal-data-value">${lap.control_output.brake_bias}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-data-row">
|
|
|
|
|
<span class="modal-data-label">Differential Slip</span>
|
|
|
|
|
<span class="modal-data-value">${lap.control_output.differential_slip}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Strategy section
|
|
|
|
|
if (lap.strategy) {
|
|
|
|
|
bodyHtml += `
|
|
|
|
|
<div class="modal-section">
|
|
|
|
|
<h3>AI Strategy: ${lap.strategy.strategy_name}</h3>
|
|
|
|
|
<p><strong>Risk Level:</strong> <span class="risk ${lap.strategy.risk_level}">${lap.strategy.risk_level.toUpperCase()}</span></p>
|
|
|
|
|
${lap.strategy.brief_description ? `<p><strong>Description:</strong> ${lap.strategy.brief_description}</p>` : ''}
|
|
|
|
|
${lap.strategy.reasoning ? `
|
|
|
|
|
<div style="margin-top: 15px;">
|
|
|
|
|
<h4 style="color: rgba(255,255,255,0.8); margin-bottom: 10px; font-size: 14px;">AI Reasoning:</h4>
|
|
|
|
|
<div style="background: rgba(0,0,0,0.3); padding: 15px; border-radius: 8px; border-left: 3px solid #dc2626;">
|
|
|
|
|
<p style="white-space: pre-wrap; font-family: 'Rajdhani', sans-serif; line-height: 1.6;">${lap.strategy.reasoning}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
} else {
|
|
|
|
|
bodyHtml += `
|
|
|
|
|
<div class="modal-section">
|
|
|
|
|
<div class="no-strategy-message">
|
|
|
|
|
⚠️ NO AI STRATEGY GENERATED FOR THIS LAP<br>
|
|
|
|
|
<small style="margin-top: 10px; display: block; font-size: 12px;">
|
|
|
|
|
AI strategies are typically generated starting from lap 3 onwards
|
|
|
|
|
</small>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
modalBody.innerHTML = bodyHtml;
|
|
|
|
|
modal.style.display = 'block';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Modal close handlers
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
const modal = document.getElementById('lapModal');
|
|
|
|
|
const closeBtn = document.querySelector('.close');
|
|
|
|
|
|
|
|
|
|
closeBtn.onclick = function() {
|
|
|
|
|
modal.style.display = 'none';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.onclick = function(event) {
|
|
|
|
|
if (event.target == modal) {
|
|
|
|
|
modal.style.display = 'none';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-19 04:10:32 -05:00
|
|
|
// Initialize connection
|
|
|
|
|
connect();
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|