k1rl-quasar / dashboard.html
RealComp's picture
Upload 32 files
047d482 verified
Raw
History Blame Contribute Delete
158 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, viewport-fit=cover">
<title>K1RL QUASAR — Quantitative Intelligence Observatory</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@200;300;400;500;600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<!-- ✅ MIGRATED: Ably removed — now using Redis-backed HTTP polling via HF Spaces API -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<style>
/* ============ RESET & VARIABLES ============ */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
:root {
/* Base font - DESKTOP FIRST */
font-size: 16px;
/* ===== TYPOGRAPHY SCALE ===== */
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-md: 1.25rem;
--text-lg: 1.5rem;
--text-xl: 2rem;
--text-2xl: 2.5rem;
--text-3xl: 3.5rem;
--text-4xl: 5rem;
--text-hero: 6rem;
/* Font weights */
--weight-light: 200;
--weight-regular: 400;
--weight-medium: 500;
--weight-semibold: 600;
--weight-bold: 700;
/* Letter spacing */
--tracking-tight: -0.02em;
--tracking-normal: 0;
--tracking-wide: 0.05em;
--tracking-wider: 0.1em;
--tracking-widest: 0.2em;
--tracking-ultra: 0.4em;
/* Line heights */
--leading-none: 1;
--leading-tight: 1.1;
--leading-snug: 1.2;
--leading-normal: 1.4;
--leading-relaxed: 1.6;
/* Colors */
--bg-deep: #000810;
--bg-space: #001020;
--accent-cyan: #00d4ff;
--accent-cyan-dim: rgba(0, 212, 255, 0.3);
--accent-cyan-glow: rgba(0, 212, 255, 0.15);
--success: #00ff88;
--danger: #ff4466;
--warning: #ffaa00;
--text-primary: #ffffff;
--text-secondary: rgba(255, 255, 255, 0.7);
--text-muted: rgba(255, 255, 255, 0.5);
--text-dim: rgba(255, 255, 255, 0.35);
--glass-bg: rgba(0, 20, 40, 0.4);
--glass-border: rgba(0, 212, 255, 0.2);
}
body {
font-family: 'Outfit', sans-serif;
background: var(--bg-deep);
color: var(--text-primary);
line-height: var(--leading-normal);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
width: 100%;
max-width: 100vw;
overflow-x: hidden;
}
/* ============ TYPOGRAPHY UTILITIES ============ */
.font-mono { font-family: 'Space Mono', monospace; }
.tracking-tight { letter-spacing: var(--tracking-tight); }
.tracking-normal { letter-spacing: var(--tracking-normal); }
.tracking-wide { letter-spacing: var(--tracking-wide); }
.tracking-wider { letter-spacing: var(--tracking-wider); }
.tracking-widest { letter-spacing: var(--tracking-widest); }
.tracking-ultra { letter-spacing: var(--tracking-ultra); }
.text-xs { font-size: var(--text-xs); }
.text-sm { font-size: var(--text-sm); }
.text-base { font-size: var(--text-base); }
.text-md { font-size: var(--text-md); }
.text-lg { font-size: var(--text-lg); }
.text-xl { font-size: var(--text-xl); }
.text-2xl { font-size: var(--text-2xl); }
.text-3xl { font-size: var(--text-3xl); }
.text-4xl { font-size: var(--text-4xl); }
.text-hero { font-size: var(--text-hero); }
.weight-light { font-weight: var(--weight-light); }
.weight-regular { font-weight: var(--weight-regular); }
.weight-medium { font-weight: var(--weight-medium); }
.weight-semibold { font-weight: var(--weight-semibold); }
.weight-bold { font-weight: var(--weight-bold); }
/* ============ THREE.JS BACKGROUND ============ */
#canvas-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: none;
}
.gradient-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(ellipse at 50% 100%, transparent 0%, rgba(0,8,16,0.7) 50%, rgba(0,8,16,0.95) 100%);
z-index: 1;
pointer-events: none;
}
/* ============ LAYOUT - DESKTOP FIRST ============ */
.main-content {
position: relative;
z-index: 10;
max-width: 1400px;
margin: 0 auto;
padding: 100px 40px 40px;
width: 100%;
}
/* ============ NAVIGATION ============ */
.nav-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 40px;
background: rgba(0,8,16,0.85);
backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(0,212,255,0.1);
}
.nav-logo {
font-size: var(--text-lg);
font-weight: var(--weight-light);
letter-spacing: var(--tracking-widest);
color: var(--text-primary);
}
.nav-logo span {
color: var(--accent-cyan);
font-weight: var(--weight-semibold);
}
.nav-links {
display: flex;
gap: 40px;
list-style: none;
}
.nav-links a {
color: var(--text-secondary);
text-decoration: none;
font-size: var(--text-sm);
font-weight: var(--weight-medium);
letter-spacing: var(--tracking-wider);
transition: color 0.2s;
}
.nav-links a:hover {
color: var(--accent-cyan);
}
/* ============ LIVE INDICATOR ============ */
.live-indicator {
position: fixed;
top: 20px;
right: 40px;
z-index: 101;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 20px;
background: rgba(0, 255, 136, 0.08);
border: 1px solid rgba(0, 255, 136, 0.2);
border-radius: 100px;
backdrop-filter: blur(8px);
}
.live-dot {
width: 8px;
height: 8px;
background: var(--success);
border-radius: 50%;
animation: pulse 1.5s infinite;
}
.live-text {
font-size: var(--text-xs);
font-weight: var(--weight-semibold);
letter-spacing: var(--tracking-wider);
color: var(--success);
font-family: 'Space Mono', monospace;
}
/* ============ HEADER ============ */
.header {
text-align: center;
margin-bottom: 32px;
}
.logo-container {
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
margin-bottom: 16px;
}
.logo-icon {
width: 56px;
height: 56px;
position: relative;
}
.logo-icon::before {
content: '';
position: absolute;
inset: 0;
border: 2px solid var(--accent-cyan);
border-radius: 50%;
animation: pulse-ring 2s infinite;
}
.logo-icon::after {
content: '';
position: absolute;
inset: 14px;
background: var(--accent-cyan);
border-radius: 50%;
box-shadow: 0 0 30px var(--accent-cyan);
}
.logo-text {
font-size: var(--text-hero);
font-weight: var(--weight-light);
letter-spacing: var(--tracking-ultra);
color: var(--text-primary);
text-shadow: 0 0 40px var(--accent-cyan-dim);
line-height: var(--leading-none);
}
.logo-text span {
font-weight: var(--weight-bold);
color: var(--accent-cyan);
}
.subtitle {
font-size: var(--text-sm);
font-weight: var(--weight-light);
letter-spacing: var(--tracking-widest);
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 8px;
}
.timestamp {
font-family: 'Space Mono', monospace;
font-size: var(--text-xs);
color: var(--text-dim);
letter-spacing: var(--tracking-wide);
}
.inst-bar {
text-align: center;
font-family: 'Space Mono', monospace;
font-size: var(--text-xs);
letter-spacing: var(--tracking-wider);
color: rgba(255,255,255,0.3);
margin: 20px 0 32px;
}
/* ============ GLASS CARDS ============ */
.glass-card {
background: var(--glass-bg);
backdrop-filter: blur(16px);
border: 1px solid var(--glass-border);
border-radius: 24px;
padding: 28px;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
}
.glass-card:hover {
border-color: var(--accent-cyan-dim);
box-shadow: 0 20px 40px rgba(0, 212, 255, 0.1);
}
.card-title {
font-family: 'Outfit', sans-serif;
font-size: var(--text-xs);
font-weight: var(--weight-semibold);
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.card-title::before {
content: '';
width: 4px;
height: 4px;
background: var(--accent-cyan);
border-radius: 50%;
box-shadow: 0 0 10px var(--accent-cyan);
}
.card-content {
flex: 1;
display: flex;
flex-direction: column;
}
/* ============ SIGNAL PANEL ============ */
.signal-panel {
text-align: center;
padding: 40px;
margin-bottom: 32px;
}
.signal-label {
font-family: 'Outfit', sans-serif;
font-size: var(--text-sm);
font-weight: var(--weight-medium);
letter-spacing: var(--tracking-widest);
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 16px;
}
.signal-value {
font-size: var(--text-4xl);
font-weight: var(--weight-bold);
letter-spacing: var(--tracking-wider);
margin-bottom: 20px;
line-height: var(--leading-tight);
}
.signal-buy {
color: var(--success);
text-shadow: 0 0 40px var(--success), 0 0 80px rgba(0, 255, 136, 0.2);
}
.signal-sell {
color: var(--danger);
text-shadow: 0 0 40px var(--danger), 0 0 80px rgba(255, 68, 102, 0.2);
}
.signal-neutral {
color: var(--warning);
text-shadow: 0 0 40px var(--warning), 0 0 80px rgba(255, 170, 0, 0.2);
}
.signal-stats {
font-family: 'Space Mono', monospace;
font-size: var(--text-sm);
color: var(--text-secondary);
letter-spacing: var(--tracking-wide);
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 16px;
margin-bottom: 20px;
}
/* ============ FORCE BALANCE - COMPLETE REDESIGN ============ */
.force-balance {
margin-top: 32px;
width: 100%;
max-width: 600px;
margin-left: auto;
margin-right: auto;
padding: 0 20px;
}
/* Pressure Labels Row */
.pressure-labels {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 40px;
margin-bottom: 16px;
}
.pressure-item {
display: flex;
flex-direction: column;
}
.pressure-item.buy {
align-items: flex-start;
}
.pressure-item.sell {
align-items: flex-end;
text-align: right;
}
.pressure-label {
font-family: 'Outfit', sans-serif;
font-size: var(--text-xs);
font-weight: var(--weight-semibold);
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 6px;
}
.pressure-value {
font-family: 'Space Mono', monospace;
font-size: var(--text-xl);
font-weight: var(--weight-bold);
letter-spacing: var(--tracking-wide);
}
.pressure-value.buy-value {
color: #4fc3f7;
text-shadow: 0 0 20px rgba(79, 195, 247, 0.3);
}
.pressure-value.sell-value {
color: #ef5350;
text-shadow: 0 0 20px rgba(239, 83, 80, 0.3);
}
/* Pressure Gauge Track */
.pressure-gauge {
position: relative;
margin: 24px 0;
}
.gauge-track {
position: relative;
height: 32px;
background: linear-gradient(90deg,
rgba(21, 101, 192, 0.15) 0%,
rgba(0, 0, 0, 0.4) 50%,
rgba(198, 40, 40, 0.15) 100%);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.08);
overflow: visible;
}
.gauge-fill-buy {
position: absolute;
top: 2px;
bottom: 2px;
left: 2px;
right: 50%;
background: linear-gradient(90deg, #1565C0, #42A5F5);
border-radius: 6px 0 0 6px;
transform-origin: right center;
transform: scaleX(0);
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.gauge-fill-sell {
position: absolute;
top: 2px;
bottom: 2px;
left: 50%;
right: 2px;
background: linear-gradient(90deg, #ef5350, #c62828);
border-radius: 0 6px 6px 0;
transform-origin: left center;
transform: scaleX(0);
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Center Line */
.gauge-center {
position: absolute;
left: 50%;
top: -6px;
bottom: -6px;
width: 2px;
background: rgba(255, 255, 255, 0.3);
transform: translateX(-50%);
z-index: 5;
}
.gauge-center::before {
content: '';
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 8px;
height: 8px;
background: rgba(255, 255, 255, 0.4);
border-radius: 50%;
}
.gauge-center::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 8px;
height: 8px;
background: rgba(255, 255, 255, 0.4);
border-radius: 50%;
}
/* Position Marker */
.gauge-marker {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
background: white;
border: 3px solid rgba(0, 0, 0, 0.8);
border-radius: 50%;
z-index: 10;
box-shadow:
0 0 20px rgba(255, 255, 255, 0.5),
0 2px 8px rgba(0, 0, 0, 0.4);
transition: left 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.gauge-marker.buy-dominant {
background: #4fc3f7;
border-color: #1565C0;
}
.gauge-marker.sell-dominant {
background: #ef5350;
border-color: #c62828;
}
/* Scale Labels */
.gauge-scale {
display: flex;
justify-content: space-between;
margin-top: 10px;
padding: 0 4px;
}
.gauge-scale span {
font-family: 'Space Mono', monospace;
font-size: 0.65rem;
color: rgba(255, 255, 255, 0.25);
letter-spacing: 0.05em;
}
.gauge-scale span:nth-child(3) {
color: rgba(255, 255, 255, 0.4);
font-weight: 700;
}
/* Net Position */
.net-position {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.net-position-label {
font-family: 'Outfit', sans-serif;
font-size: var(--text-xs);
font-weight: var(--weight-semibold);
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--text-dim);
}
.net-position-value {
font-family: 'Space Mono', monospace;
font-size: var(--text-xl);
font-weight: var(--weight-bold);
letter-spacing: var(--tracking-wide);
min-width: 80px;
text-align: center;
}
.net-position-value.positive {
color: #4fc3f7;
text-shadow: 0 0 15px rgba(79, 195, 247, 0.3);
}
.net-position-value.negative {
color: #ef5350;
text-shadow: 0 0 15px rgba(239, 83, 80, 0.3);
}
.net-position-value.neutral {
color: var(--text-secondary);
}
/* ============ GRID LAYOUTS - IMPROVED ============ */
.grid-3 {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 28px;
margin-bottom: 28px;
}
/* ============ METRIC ROWS - FILL SPACE ============ */
.metric-row {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: 16px 0;
border-bottom: 1px solid rgba(255,255,255,0.03);
}
.metric-row:last-child {
border-bottom: none;
padding-bottom: 0;
}
.metric-row:first-child {
padding-top: 0;
}
.metric-label {
font-family: 'Outfit', sans-serif;
font-size: var(--text-sm);
font-weight: var(--weight-regular);
color: var(--text-secondary);
letter-spacing: var(--tracking-wide);
}
.metric-value {
font-family: 'Space Mono', monospace;
font-size: var(--text-base);
font-weight: var(--weight-semibold);
color: var(--text-primary);
letter-spacing: var(--tracking-normal);
}
.metric-value.positive { color: var(--success); }
.metric-value.warning { color: var(--warning); }
.metric-value.negative { color: var(--danger); }
/* ============ BIG NUMBERS ============ */
.big-number {
font-family: 'Space Mono', monospace;
font-size: var(--text-2xl);
font-weight: var(--weight-bold);
text-align: center;
color: var(--accent-cyan);
text-shadow: 0 0 30px var(--accent-cyan-dim);
margin: 20px 0;
line-height: var(--leading-tight);
letter-spacing: var(--tracking-wide);
}
/* ============ PROCESS STATUS - FILL SPACE ============ */
.process-status {
text-align: center;
padding: 24px;
margin-top: auto;
border-radius: 16px;
}
.process-ok {
background: rgba(0,255,136,0.08);
border: 1px solid rgba(0,255,136,0.15);
}
.process-warning {
background: rgba(255,68,102,0.08);
border: 1px solid rgba(255,68,102,0.15);
}
.process-label {
font-family: 'Outfit', sans-serif;
font-size: var(--text-xs);
font-weight: var(--weight-medium);
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 10px;
}
.process-value {
font-family: 'Space Mono', monospace;
font-size: var(--text-2xl);
font-weight: var(--weight-bold);
line-height: var(--leading-tight);
}
.process-ok .process-value { color: var(--success); }
.process-warning .process-value { color: var(--danger); }
/* ============ STATUS BADGES ============ */
.status-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 14px;
border-radius: 100px;
font-family: 'Outfit', sans-serif;
font-size: var(--text-xs);
font-weight: var(--weight-semibold);
text-transform: uppercase;
letter-spacing: var(--tracking-wider);
}
.status-active {
background: rgba(0,255,136,0.1);
color: var(--success);
border: 1px solid rgba(0,255,136,0.2);
}
.status-unknown {
background: rgba(255,170,0,0.1);
color: #ffaa00;
border: 1px solid rgba(255,170,0,0.3);
}
.status-inactive {
background: rgba(255,68,102,0.1);
color: var(--danger);
border: 1px solid rgba(255,68,102,0.2);
}
.status-badge::before {
content: '';
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
/* ============ PROGRESS BAR ============ */
.progress-container {
margin-top: 16px;
}
.progress-bar {
width: 100%;
height: 8px;
background: rgba(255,255,255,0.06);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--accent-cyan), var(--success));
border-radius: 4px;
transition: width 0.4s ease;
}
/* ============ LOG CONTAINER ============ */
.log-container {
background: rgba(0,0,0,0.25);
border-radius: 16px;
padding: 20px;
height: 240px;
overflow-y: auto;
font-family: 'Space Mono', monospace;
font-size: var(--text-xs);
line-height: var(--leading-relaxed);
border: 1px solid rgba(255,255,255,0.03);
}
.log-line {
padding: 5px 0;
word-break: break-all;
color: var(--text-secondary);
}
.log-error { color: var(--danger); }
.log-warning { color: var(--warning); }
.log-info { color: var(--success); }
/* ============ ACCURACY CHART ============ */
.accuracy-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
gap: 20px;
margin-bottom: 24px;
}
.accuracy-metrics {
display: flex;
gap: 32px;
flex-wrap: wrap;
}
.accuracy-stat {
text-align: center;
}
.accuracy-stat-label {
font-family: 'Outfit', sans-serif;
font-size: var(--text-xs);
font-weight: var(--weight-medium);
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 6px;
}
.accuracy-stat-value {
font-family: 'Space Mono', monospace;
font-size: var(--text-xl);
font-weight: var(--weight-bold);
}
.accuracy-legend {
display: flex;
gap: 20px;
align-items: center;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-family: 'Outfit', sans-serif;
font-size: var(--text-xs);
color: var(--text-secondary);
}
.legend-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.legend-dot.current { background: var(--success); }
.legend-dot.average { background: var(--accent-cyan); }
.legend-dot.trend { background: var(--warning); }
.accuracy-trend-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 100px;
font-family: 'Outfit', sans-serif;
font-size: var(--text-xs);
font-weight: var(--weight-semibold);
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
}
.accuracy-trend-badge.rising {
background: rgba(0, 255, 136, 0.1);
border: 1px solid rgba(0, 255, 136, 0.2);
color: var(--success);
}
.accuracy-trend-badge.stable {
background: rgba(0, 212, 255, 0.1);
border: 1px solid rgba(0, 212, 255, 0.2);
color: var(--accent-cyan);
}
.accuracy-trend-badge.declining {
background: rgba(255, 68, 102, 0.1);
border: 1px solid rgba(255, 68, 102, 0.2);
color: var(--danger);
}
.trend-arrow {
font-weight: var(--weight-bold);
font-size: var(--text-sm);
}
.chart-container {
height: 200px;
width: 100%;
margin-top: 16px;
}
/* ============ VISITOR BADGE ============ */
.visitor-badge {
position: fixed;
bottom: 20px;
left: 24px;
background: rgba(0,20,30,0.9);
border: 1px solid rgba(0,212,170,0.2);
border-radius: 12px;
padding: 12px 20px;
display: flex;
align-items: center;
gap: 14px;
z-index: 200;
backdrop-filter: blur(12px);
}
.visitor-dot {
width: 10px;
height: 10px;
background: #00d4aa;
border-radius: 50%;
animation: pulse 2s infinite;
}
.visitor-count {
color: #00d4aa;
font-size: var(--text-base);
font-weight: var(--weight-bold);
font-family: 'Space Mono', monospace;
}
.visitor-label {
font-family: 'Outfit', sans-serif;
color: rgba(255,255,255,0.5);
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: var(--tracking-wider);
}
.visitor-total {
color: rgba(255,255,255,0.3);
font-size: 0.7rem;
display: block;
font-family: 'Space Mono', monospace;
}
/* ============ CONNECTION STATUS ============ */
.connection-status {
position: fixed;
bottom: 20px;
right: 24px;
padding: 10px 20px;
border-radius: 100px;
font-size: var(--text-xs);
font-family: 'Space Mono', monospace;
backdrop-filter: blur(12px);
z-index: 200;
letter-spacing: var(--tracking-wide);
}
.connection-ok {
background: rgba(0,255,136,0.1);
border: 1px solid rgba(0,255,136,0.2);
color: var(--success);
}
.connection-error {
background: rgba(255,68,102,0.1);
border: 1px solid rgba(255,68,102,0.2);
color: var(--danger);
}
/* ============ LOADING OVERLAY ============ */
.loading-overlay {
position: fixed;
bottom: 90px;
left: 24px;
background: rgba(0,20,30,0.95);
border: 1px solid rgba(0,212,255,0.2);
border-radius: 12px;
padding: 14px 24px;
display: flex;
align-items: center;
gap: 14px;
z-index: 300;
backdrop-filter: blur(12px);
font-size: var(--text-sm);
}
.loading-spinner {
width: 18px;
height: 18px;
border: 2px solid rgba(0,212,255,0.2);
border-top-color: var(--accent-cyan);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.hidden {
display: none !important;
}
/* ============ FOOTER ============ */
.footer {
text-align: center;
padding: 50px 24px 24px;
color: var(--text-dim);
font-size: var(--text-xs);
letter-spacing: var(--tracking-wider);
}
.footer p {
margin-bottom: 6px;
}
/* ============ ANIMATIONS ============ */
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(0.8); }
}
@keyframes pulse-ring {
0% { transform: scale(1); opacity: 1; }
100% { transform: scale(1.5); opacity: 0; }
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.glass-card {
animation: fadeInUp 0.5s ease forwards;
}
.grid-3 .glass-card:nth-child(1) { animation-delay: 0.1s; }
.grid-3 .glass-card:nth-child(2) { animation-delay: 0.15s; }
.grid-3 .glass-card:nth-child(3) { animation-delay: 0.2s; }
/* ============ TABLET STYLES (1024px and below) ============ */
@media (max-width: 1024px) {
.grid-3 {
grid-template-columns: repeat(2, 1fr);
gap: 24px;
}
}
/* ============ MOBILE STYLES (768px and below) ============ */
@media (max-width: 768px) {
:root {
font-size: 14px;
}
.main-content {
padding: 80px 16px 20px;
}
.nav-bar {
padding: 14px 16px;
}
.nav-links {
display: none;
}
.nav-logo {
font-size: var(--text-base);
letter-spacing: var(--tracking-wider);
}
.live-indicator {
top: 14px;
right: 16px;
padding: 8px 14px;
}
.logo-icon {
width: 40px;
height: 40px;
}
.logo-icon::after {
inset: 10px;
}
.logo-text {
font-size: 3rem;
letter-spacing: 0.25em;
}
.subtitle {
font-size: 0.7rem;
letter-spacing: 0.2em;
}
.grid-3 {
grid-template-columns: 1fr;
gap: 16px;
}
.glass-card {
padding: 20px;
border-radius: 20px;
}
.signal-panel {
padding: 24px 16px;
}
.signal-value {
font-size: var(--text-2xl);
}
.signal-stats {
font-size: var(--text-xs);
gap: 12px;
}
/* Force Balance Mobile */
.force-balance {
padding: 0 10px;
}
.pressure-labels {
gap: 20px;
}
.pressure-value {
font-size: var(--text-lg);
}
.gauge-track {
height: 28px;
}
.gauge-marker {
width: 16px;
height: 16px;
}
.net-position-value {
font-size: var(--text-lg);
}
.accuracy-header {
flex-direction: column;
align-items: flex-start;
}
.accuracy-metrics {
gap: 24px;
}
.accuracy-stat-value {
font-size: var(--text-lg);
}
.big-number {
font-size: var(--text-xl);
}
.visitor-badge {
left: 16px;
bottom: 16px;
padding: 10px 16px;
}
.connection-status {
right: 16px;
bottom: 16px;
padding: 8px 16px;
}
}
/* ============ SMALL MOBILE (480px and below) ============ */
@media (max-width: 480px) {
:root {
font-size: 12px;
}
.logo-icon {
width: 32px;
height: 32px;
}
.logo-icon::after {
inset: 8px;
}
.logo-text {
font-size: 2.2rem;
letter-spacing: 0.15em;
}
.subtitle {
font-size: 0.6rem;
letter-spacing: 0.15em;
}
.force-balance {
margin-top: 20px;
padding: 0 5px;
}
.pressure-labels {
gap: 10px;
}
.pressure-label {
font-size: 0.65rem;
}
.pressure-value {
font-size: var(--text-md);
}
.gauge-track {
height: 24px;
}
.gauge-scale span {
font-size: 0.55rem;
}
.net-position {
flex-direction: column;
gap: 8px;
}
.accuracy-metrics {
gap: 16px;
}
.accuracy-legend {
flex-wrap: wrap;
gap: 12px;
}
}
/* ============ VERY SMALL MOBILE (360px and below) ============ */
@media (max-width: 360px) {
:root {
font-size: 11px;
}
}
/* ============ HOVER STATES - DESKTOP ONLY ============ */
@media (hover: hover) and (pointer: fine) {
.glass-card:hover {
transform: translateY(-2px);
box-shadow: 0 24px 48px rgba(0, 212, 255, 0.12);
}
.nav-links a:hover {
color: var(--accent-cyan);
}
}
/* Disable hover effects on touch devices */
@media (hover: none) {
.glass-card:hover {
transform: none;
box-shadow: none;
}
}
</style>
</head>
<body>
<!-- Three.js Canvas -->
<div id="canvas-container">
<canvas id="three-canvas"></canvas>
</div>
<div class="gradient-overlay"></div>
<!-- Loading Overlay -->
<div class="loading-overlay" id="loadingOverlay">
<div class="loading-spinner"></div>
<span id="loadingText">Initializing Observatory...</span>
</div>
<!-- Connection Status -->
<div class="connection-status connection-ok" id="connectionStatus">● Connected</div>
<!-- Visitor Counter -->
<div class="visitor-badge">
<div class="visitor-dot"></div>
<div>
<span class="visitor-count" id="activeVisitors"></span>
<span class="visitor-label">Online</span>
<span class="visitor-total" id="totalVisitors">— total</span>
</div>
</div>
<!-- Navigation -->
<nav class="nav-bar">
<div class="nav-logo">K1RL <span>QUASAR</span></div>
<ul class="nav-links">
<li><a href="#dashboard">Dashboard</a></li>
<li><a href="#metrics">Metrics</a></li>
<li><a href="#system">System</a></li>
</ul>
</nav>
<!-- Live Indicator -->
<div class="live-indicator">
<div class="live-dot"></div>
<span class="live-text">LIVE</span>
</div>
<!-- Main Content -->
<div class="main-content">
<!-- Header -->
<header class="header" id="dashboard">
<div class="logo-container">
<div class="logo-icon"></div>
<h1 class="logo-text">QU<span>A</span>SAR</h1>
</div>
<p class="subtitle">Quantitative Intelligence Observatory</p>
<p class="timestamp" id="lastUpdate">Last Observation: Initializing...</p>
<div class="inst-bar" id="instBar">VOLATILITY 75 INDEX │ DERIV</div>
</header>
<!-- SIGNAL PANEL -->
<div class="glass-card signal-panel">
<div class="card-title">Market Directive — 10M Agent — Last 5 Minutes</div>
<div class="signal-label">Dominant Signal</div>
<div class="signal-value signal-neutral" id="dominantSignal">NEUTRAL</div>
<div class="signal-stats" id="signalStats">
<span>BUY: 0 (0%)</span><span>SELL: 0 (0%)</span><span>TOTAL: 0</span>
</div>
<!-- FORCE BALANCE -->
<div class="force-balance">
<!-- Pressure Labels -->
<div class="pressure-labels">
<div class="pressure-item buy">
<span class="pressure-label">Buy Pressure</span>
<span class="pressure-value buy-value" id="forceBuyValue">0.0%</span>
</div>
<div class="pressure-item sell">
<span class="pressure-label">Sell Pressure</span>
<span class="pressure-value sell-value" id="forceSellValue">0.0%</span>
</div>
</div>
<!-- Pressure Gauge -->
<div class="pressure-gauge">
<div class="gauge-track">
<div class="gauge-fill-buy" id="gaugeFillBuy"></div>
<div class="gauge-fill-sell" id="gaugeFillSell"></div>
<div class="gauge-center"></div>
<div class="gauge-marker" id="gaugeMarker"></div>
</div>
<div class="gauge-scale">
<span>100</span>
<span>50</span>
<span>0</span>
<span>50</span>
<span>100</span>
</div>
</div>
<!-- Net Position -->
<div class="net-position">
<span class="net-position-label">Net Position</span>
<span class="net-position-value neutral" id="netPositionValue">0.0</span>
</div>
</div>
</div>
<!-- TRAINING & REWARDS -->
<div class="grid-3" id="metrics">
<!-- Training Progression -->
<div class="glass-card">
<div class="card-title">Training Progression</div>
<div class="card-content">
<div class="metric-row">
<span class="metric-label">Iterations Completed</span>
<span class="metric-value" id="trainingSteps" style="color: #00ff88;"></span>
</div>
<div class="metric-row">
<span class="metric-label">Actor Loss</span>
<span class="metric-value" id="actorLoss" style="color: #00ff88;"></span>
</div>
<div class="metric-row">
<span class="metric-label">Critic Loss</span>
<span class="metric-value" id="criticLoss"></span>
</div>
<div class="metric-row">
<span class="metric-label">AVN Loss</span>
<span class="metric-value" id="avnLoss"></span>
</div>
<div class="metric-row">
<span class="metric-label">AVN Accuracy</span>
<span class="metric-value" id="avnAccuracy" style="color: #00d4aa;"></span>
</div>
<div class="metric-row">
<span class="metric-label">Buffer Capacity</span>
<span class="metric-value" id="bufferSize"></span>
</div>
</div>
</div>
<!-- Reward Mechanisms -->
<div class="glass-card">
<div class="card-title">Reward Mechanisms</div>
<div class="card-content">
<div class="metric-row">
<span class="metric-label">Matched Signals</span>
<span class="metric-value positive" id="matchedRewards"></span>
</div>
<div class="metric-row">
<span class="metric-label">Unmatched Signals</span>
<span class="metric-value warning" id="unmatchedRewards"></span>
</div>
<div class="metric-row">
<span class="metric-label">Duplicate Entries</span>
<span class="metric-value" id="duplicates"></span>
</div>
<div class="metric-row">
<span class="metric-label">Match Efficacy</span>
<span class="metric-value" id="matchRate"></span>
</div>
</div>
</div>
<!-- Service Registry -->
<div class="glass-card" id="system">
<div class="card-title">Service Registry</div>
<div class="card-content">
<div class="metric-row">
<span class="metric-label">Quasar Engine</span>
<span class="status-badge status-active" id="statusQuasar">Active</span>
</div>
<div class="metric-row">
<span class="metric-label">Feature Pipeline</span>
<span class="status-badge status-active" id="statusFeatures">Active</span>
</div>
<div class="metric-row">
<span class="metric-label">Reward System</span>
<span class="status-badge status-active" id="statusRewards">Active</span>
</div>
<div class="process-status process-ok" id="processCount">
<div class="process-label">Quasar Processes</div>
<div class="process-value" id="processCountValue">1</div>
</div>
</div>
</div>
</div>
<!-- SYSTEM RESOURCES -->
<div class="grid-3">
<!-- Computational Load -->
<div class="glass-card">
<div class="card-title">Computational Load</div>
<div class="card-content">
<div class="big-number" id="cpuPercent">—%</div>
<div class="progress-container">
<div class="progress-bar">
<div class="progress-fill" id="cpuBar" style="width:0%"></div>
</div>
</div>
</div>
</div>
<!-- Memory Allocation -->
<div class="glass-card">
<div class="card-title">Memory Allocation</div>
<div class="card-content">
<div class="big-number" id="memoryPercent">—%</div>
<div class="metric-row">
<span class="metric-label">Used</span>
<span class="metric-value" id="memoryUsed">—/— GB</span>
</div>
<div class="metric-row">
<span class="metric-label">Quasar</span>
<span class="metric-value positive" id="quasarMemory">— GB</span>
</div>
</div>
</div>
<!-- Latest Checkpoint -->
<div class="glass-card">
<div class="card-title">Latest Checkpoint</div>
<div class="card-content">
<div class="metric-row">
<span class="metric-label">Filename</span>
<span class="metric-value" id="checkpointFile" style="font-size:0.7rem;"></span>
</div>
<div class="metric-row">
<span class="metric-label">Training Step</span>
<span class="metric-value" id="checkpointStep"></span>
</div>
<div class="metric-row">
<span class="metric-label">Size</span>
<span class="metric-value" id="checkpointSize"></span>
</div>
<div class="metric-row">
<span class="metric-label">Modified</span>
<span class="metric-value" id="checkpointModified" style="font-size:0.75rem;"></span>
</div>
</div>
</div>
</div>
<!-- ACCURACY TRENDS -->
<div class="glass-card">
<div class="card-title">Signal Precision Analytics</div>
<div class="accuracy-header">
<div class="accuracy-metrics">
<div class="accuracy-stat">
<div class="accuracy-stat-label">Current</div>
<div class="accuracy-stat-value" id="accuracy-current" style="color: var(--success);"></div>
</div>
<div class="accuracy-stat">
<div class="accuracy-stat-label">Average</div>
<div class="accuracy-stat-value" id="accuracy-avg" style="color: var(--accent-cyan);"></div>
</div>
<div class="accuracy-stat">
<div class="accuracy-stat-label">Last 10</div>
<div class="accuracy-stat-value" id="accuracy-last10" style="color: var(--warning);"></div>
</div>
</div>
<div class="accuracy-trend-badge stable" id="accuracy-trend">
<span class="trend-arrow"></span>
<span>STABLE</span>
</div>
</div>
<div class="chart-container">
<canvas id="accuracyChart"></canvas>
</div>
</div>
<!-- SYSTEM LOGS -->
<div class="glass-card">
<div class="card-title">Quasar Intelligence Stream</div>
<div class="log-container" id="logContainer">
<div class="log-line">Awaiting signal intelligence...</div>
</div>
</div>
<!-- HuggingFace Spaces Platform Badge -->
<div style="position:fixed;bottom:20px;right:20px;z-index:101;padding:8px 16px;background:rgba(255,136,0,0.1);border:1px solid rgba(255,136,0,0.3);border-radius:20px;backdrop-filter:blur(8px);font-size:0.75rem;font-weight:500;color:#ffaa00;letter-spacing:0.05em;">🤗 HuggingFace Spaces</div>
<!-- Footer -->
<footer class="footer">
<p>K1RL QUASAR Observatory v10.0 — HF Spaces Edition</p>
<p>Reinforcement Learning • Quantitative Intelligence • VOLATILITY 75 Index • Redis-Backed</p>
</footer>
</div>
<script>
// ==================== THREE.JS WITH REALISTIC EARTH ====================
(function() {
const canvas = document.getElementById('three-canvas');
const isMobile = window.innerWidth <= 768;
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: !isMobile,
alpha: true,
powerPreference: 'high-performance'
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, isMobile ? 1.5 : 2));
renderer.setClearColor(0x000810, 1);
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x000810, 0.0006);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 3000);
camera.position.set(0, 50, 500);
camera.lookAt(0, -50, 0);
const textureLoader = new THREE.TextureLoader();
// ===== STARFIELD =====
const starCount = isMobile ? 800 : 2000;
const starGeo = new THREE.BufferGeometry();
const starPos = new Float32Array(starCount * 3);
for (let i = 0; i < starCount * 3; i += 3) {
starPos[i] = (Math.random() - 0.5) * 2000;
starPos[i+1] = (Math.random() - 0.5) * 2000;
starPos[i+2] = (Math.random() - 0.5) * 2000;
}
starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
const stars = new THREE.Points(starGeo, new THREE.PointsMaterial({
color: 0xffffff,
size: 1.2,
transparent: true,
opacity: 0.8,
sizeAttenuation: true
}));
scene.add(stars);
// ==================== NEURAL NETWORK WITH PHYSICS ====================
const nodeCount = isMobile ? 60 : 100;
const nodes = [];
// Physics constants
const PHYSICS = {
// Core forces
centralGravity: 0.00008, // Pull towards center
repulsion: 800, // Node-to-node repulsion strength
repulsionDistance: 120, // Max distance for repulsion
springStrength: 0.0003, // Connection spring force
springRestLength: 100, // Ideal connection length
// Boundaries
minRadius: 200,
maxRadius: 550,
// Damping & limits
damping: 0.985, // Velocity decay
maxVelocity: 2.5,
// Mouse interaction
mouseRadius: 200,
mouseForce: 0.15,
mouseAttract: false, // false = repel, true = attract
// Connection
connectionDistance: 140,
maxConnections: 6
};
// Mouse tracking
const mouse = { x: 0, y: 0, z: 0, active: false };
const mouseVector = new THREE.Vector3();
const raycaster = new THREE.Raycaster();
const mouse2D = new THREE.Vector2();
// Create nodes with physics properties
const nodeGeometry = new THREE.BufferGeometry();
const nodePositions = new Float32Array(nodeCount * 3);
const nodeSizes = new Float32Array(nodeCount);
const nodeColors = new Float32Array(nodeCount * 3);
for (let i = 0; i < nodeCount; i++) {
const i3 = i * 3;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
const radius = PHYSICS.minRadius + Math.random() * (PHYSICS.maxRadius - PHYSICS.minRadius);
const x = radius * Math.sin(phi) * Math.cos(theta);
const y = radius * Math.sin(phi) * Math.sin(theta);
const z = radius * Math.cos(phi);
nodePositions[i3] = x;
nodePositions[i3 + 1] = y;
nodePositions[i3 + 2] = z;
// Random mass affects node size and physics response
const mass = 0.5 + Math.random() * 1.5;
nodeSizes[i] = isMobile ? mass * 3 : mass * 4;
// Color gradient: cyan to blue based on distance from center
const colorFactor = Math.random();
nodeColors[i3] = 0.0 + colorFactor * 0.2; // R
nodeColors[i3 + 1] = 0.7 + colorFactor * 0.3; // G
nodeColors[i3 + 2] = 1.0; // B
nodes.push({
x, y, z,
vx: (Math.random() - 0.5) * 0.5,
vy: (Math.random() - 0.5) * 0.5,
vz: (Math.random() - 0.5) * 0.5,
mass: mass,
connections: [],
energy: 0.5 + Math.random() * 0.5 // For pulse effects
});
}
nodeGeometry.setAttribute('position', new THREE.BufferAttribute(nodePositions, 3));
nodeGeometry.setAttribute('size', new THREE.BufferAttribute(nodeSizes, 1));
nodeGeometry.setAttribute('color', new THREE.BufferAttribute(nodeColors, 3));
// Custom shader for variable-size glowing nodes
const nodeShaderMaterial = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
pixelRatio: { value: renderer.getPixelRatio() }
},
vertexShader: `
attribute float size;
attribute vec3 color;
varying vec3 vColor;
varying float vSize;
uniform float time;
uniform float pixelRatio;
void main() {
vColor = color;
vSize = size;
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
// Subtle pulse based on position
float pulse = 1.0 + 0.15 * sin(time * 2.0 + position.x * 0.01 + position.y * 0.01);
gl_PointSize = size * pulse * pixelRatio * (300.0 / -mvPosition.z);
gl_Position = projectionMatrix * mvPosition;
}
`,
fragmentShader: `
varying vec3 vColor;
varying float vSize;
void main() {
vec2 center = gl_PointCoord - vec2(0.5);
float dist = length(center);
if (dist > 0.5) discard;
// Soft glow falloff
float alpha = 1.0 - smoothstep(0.0, 0.5, dist);
float glow = exp(-dist * 3.0);
vec3 finalColor = vColor + vec3(0.3, 0.5, 0.8) * glow * 0.5;
gl_FragColor = vec4(finalColor, alpha * 0.9);
}
`,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending
});
const nodePoints = new THREE.Points(nodeGeometry, nodeShaderMaterial);
scene.add(nodePoints);
// Connection lines with varying opacity
const maxLines = nodeCount * PHYSICS.maxConnections;
const linePositions = new Float32Array(maxLines * 6);
const lineColors = new Float32Array(maxLines * 6);
const linesGeometry = new THREE.BufferGeometry();
linesGeometry.setAttribute('position', new THREE.BufferAttribute(linePositions, 3));
linesGeometry.setAttribute('color', new THREE.BufferAttribute(lineColors, 3));
const linesMaterial = new THREE.LineBasicMaterial({
vertexColors: true,
transparent: true,
opacity: 0.6,
blending: THREE.AdditiveBlending
});
const lines = new THREE.LineSegments(linesGeometry, linesMaterial);
scene.add(lines);
// Build spatial hash for efficient neighbor lookup
function buildSpatialHash() {
const cellSize = PHYSICS.repulsionDistance;
const hash = {};
for (let i = 0; i < nodeCount; i++) {
const node = nodes[i];
const cellX = Math.floor(node.x / cellSize);
const cellY = Math.floor(node.y / cellSize);
const cellZ = Math.floor(node.z / cellSize);
const key = `${cellX},${cellY},${cellZ}`;
if (!hash[key]) hash[key] = [];
hash[key].push(i);
}
return hash;
}
// Get neighbors from spatial hash
function getNeighborCells(x, y, z, cellSize) {
const cellX = Math.floor(x / cellSize);
const cellY = Math.floor(y / cellSize);
const cellZ = Math.floor(z / cellSize);
const neighbors = [];
for (let dx = -1; dx <= 1; dx++) {
for (let dy = -1; dy <= 1; dy++) {
for (let dz = -1; dz <= 1; dz++) {
neighbors.push(`${cellX + dx},${cellY + dy},${cellZ + dz}`);
}
}
}
return neighbors;
}
// Physics simulation step
function simulatePhysics(deltaTime) {
const dt = Math.min(deltaTime, 0.033); // Cap at ~30fps worth
const spatialHash = buildSpatialHash();
// Reset connections
for (let i = 0; i < nodeCount; i++) {
nodes[i].connections = [];
}
// Calculate forces for each node
for (let i = 0; i < nodeCount; i++) {
const node = nodes[i];
let fx = 0, fy = 0, fz = 0;
// 1. Central gravity - pull towards origin
const distFromCenter = Math.sqrt(node.x * node.x + node.y * node.y + node.z * node.z);
if (distFromCenter > 0.01) {
const gravityStrength = PHYSICS.centralGravity * node.mass;
fx -= (node.x / distFromCenter) * gravityStrength * distFromCenter;
fy -= (node.y / distFromCenter) * gravityStrength * distFromCenter;
fz -= (node.z / distFromCenter) * gravityStrength * distFromCenter;
}
// 2. Boundary force - soft repulsion at edges
if (distFromCenter > PHYSICS.maxRadius) {
const overflow = distFromCenter - PHYSICS.maxRadius;
const boundaryForce = overflow * 0.01;
fx -= (node.x / distFromCenter) * boundaryForce;
fy -= (node.y / distFromCenter) * boundaryForce;
fz -= (node.z / distFromCenter) * boundaryForce;
} else if (distFromCenter < PHYSICS.minRadius) {
const underflow = PHYSICS.minRadius - distFromCenter;
const boundaryForce = underflow * 0.01;
fx += (node.x / distFromCenter) * boundaryForce;
fy += (node.y / distFromCenter) * boundaryForce;
fz += (node.z / distFromCenter) * boundaryForce;
}
// 3. Node-to-node repulsion (using spatial hash for efficiency)
const neighborCells = getNeighborCells(node.x, node.y, node.z, PHYSICS.repulsionDistance);
for (const cellKey of neighborCells) {
const cellNodes = spatialHash[cellKey];
if (!cellNodes) continue;
for (const j of cellNodes) {
if (i === j) continue;
const other = nodes[j];
const dx = node.x - other.x;
const dy = node.y - other.y;
const dz = node.z - other.z;
const distSq = dx * dx + dy * dy + dz * dz;
const dist = Math.sqrt(distSq);
if (dist < PHYSICS.repulsionDistance && dist > 0.1) {
// Inverse square repulsion
const force = PHYSICS.repulsion / (distSq + 100);
fx += (dx / dist) * force / node.mass;
fy += (dy / dist) * force / node.mass;
fz += (dz / dist) * force / node.mass;
// Track connections for springs and rendering
if (dist < PHYSICS.connectionDistance && node.connections.length < PHYSICS.maxConnections) {
if (!node.connections.includes(j)) {
node.connections.push(j);
}
}
}
}
}
// 4. Spring forces along connections
for (const j of node.connections) {
const other = nodes[j];
const dx = other.x - node.x;
const dy = other.y - node.y;
const dz = other.z - node.z;
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (dist > 0.1) {
const displacement = dist - PHYSICS.springRestLength;
const springForce = displacement * PHYSICS.springStrength;
fx += (dx / dist) * springForce;
fy += (dy / dist) * springForce;
fz += (dz / dist) * springForce;
}
}
// 5. Mouse interaction
if (mouse.active) {
const mdx = node.x - mouse.x;
const mdy = node.y - mouse.y;
const mdz = node.z - mouse.z;
const mouseDist = Math.sqrt(mdx * mdx + mdy * mdy + mdz * mdz);
if (mouseDist < PHYSICS.mouseRadius && mouseDist > 1) {
const mouseStrength = (1 - mouseDist / PHYSICS.mouseRadius) * PHYSICS.mouseForce;
const direction = PHYSICS.mouseAttract ? -1 : 1;
fx += direction * (mdx / mouseDist) * mouseStrength;
fy += direction * (mdy / mouseDist) * mouseStrength;
fz += direction * (mdz / mouseDist) * mouseStrength;
}
}
// Apply forces to velocity
node.vx += fx * dt;
node.vy += fy * dt;
node.vz += fz * dt;
// Damping
node.vx *= PHYSICS.damping;
node.vy *= PHYSICS.damping;
node.vz *= PHYSICS.damping;
// Velocity limit
const speed = Math.sqrt(node.vx * node.vx + node.vy * node.vy + node.vz * node.vz);
if (speed > PHYSICS.maxVelocity) {
const scale = PHYSICS.maxVelocity / speed;
node.vx *= scale;
node.vy *= scale;
node.vz *= scale;
}
}
// Update positions
const positions = nodeGeometry.attributes.position.array;
const sizes = nodeGeometry.attributes.size.array;
for (let i = 0; i < nodeCount; i++) {
const node = nodes[i];
const i3 = i * 3;
node.x += node.vx;
node.y += node.vy;
node.z += node.vz;
positions[i3] = node.x;
positions[i3 + 1] = node.y;
positions[i3 + 2] = node.z;
// Size varies with connections (more connected = larger)
const connectionBonus = node.connections.length * 0.3;
sizes[i] = (isMobile ? 3 : 4) * node.mass + connectionBonus;
}
nodeGeometry.attributes.position.needsUpdate = true;
nodeGeometry.attributes.size.needsUpdate = true;
}
// Update connection lines
function updateConnections() {
const linePos = linesGeometry.attributes.position.array;
const lineCol = linesGeometry.attributes.color.array;
let lineIndex = 0;
for (let i = 0; i < nodeCount; i++) {
const node = nodes[i];
for (const j of node.connections) {
if (j > i) continue; // Avoid duplicates
const other = nodes[j];
const dx = other.x - node.x;
const dy = other.y - node.y;
const dz = other.z - node.z;
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
// Line positions
const idx = lineIndex * 6;
linePos[idx] = node.x;
linePos[idx + 1] = node.y;
linePos[idx + 2] = node.z;
linePos[idx + 3] = other.x;
linePos[idx + 4] = other.y;
linePos[idx + 5] = other.z;
// Color based on distance (closer = brighter)
const intensity = 1 - (dist / PHYSICS.connectionDistance);
const r = 0.0 * intensity;
const g = 0.83 * intensity;
const b = 1.0 * intensity;
lineCol[idx] = r;
lineCol[idx + 1] = g;
lineCol[idx + 2] = b;
lineCol[idx + 3] = r;
lineCol[idx + 4] = g;
lineCol[idx + 5] = b;
lineIndex++;
}
}
// Clear unused line segments
for (let i = lineIndex * 6; i < linePos.length; i++) {
linePos[i] = 0;
lineCol[i] = 0;
}
linesGeometry.attributes.position.needsUpdate = true;
linesGeometry.attributes.color.needsUpdate = true;
linesGeometry.setDrawRange(0, lineIndex * 2);
}
// Mouse event handlers
function onMouseMove(event) {
mouse2D.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse2D.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse2D, camera);
// Project mouse into 3D space at network depth
const planeZ = -100;
const planeNormal = new THREE.Vector3(0, 0, 1);
const planePoint = new THREE.Vector3(0, 0, planeZ);
const plane = new THREE.Plane().setFromNormalAndCoplanarPoint(planeNormal, planePoint);
const intersectPoint = new THREE.Vector3();
raycaster.ray.intersectPlane(plane, intersectPoint);
if (intersectPoint) {
mouse.x = intersectPoint.x;
mouse.y = intersectPoint.y;
mouse.z = intersectPoint.z;
mouse.active = true;
}
}
function onMouseLeave() {
mouse.active = false;
}
// Touch support
function onTouchMove(event) {
if (event.touches.length > 0) {
const touch = event.touches[0];
onMouseMove({ clientX: touch.clientX, clientY: touch.clientY });
}
}
function onTouchEnd() {
mouse.active = false;
}
// Add event listeners
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseleave', onMouseLeave);
document.addEventListener('touchmove', onTouchMove, { passive: true });
document.addEventListener('touchend', onTouchEnd);
// ==================== REALISTIC EARTH ====================
const earthRadius = isMobile ? 160 : 180;
const earthGroup = new THREE.Group();
earthGroup.position.set(0, isMobile ? -350 : -380, -100);
scene.add(earthGroup);
// Earth texture URLs (from three-globe)
const textureURLs = {
dayMap: 'https://unpkg.com/three-globe@2.24.13/example/img/earth-day.jpg',
nightMap: 'https://unpkg.com/three-globe@2.24.13/example/img/earth-night.jpg',
bumpMap: 'https://unpkg.com/three-globe@2.24.13/example/img/earth-topology.png',
cloudsMap: 'https://unpkg.com/three-globe@2.24.13/example/img/earth-clouds.png'
};
// Load textures
const earthDayTexture = textureLoader.load(textureURLs.dayMap);
const earthNightTexture = textureLoader.load(textureURLs.nightMap);
const earthBumpTexture = textureLoader.load(textureURLs.bumpMap);
const earthCloudsTexture = textureLoader.load(textureURLs.cloudsMap);
// Earth geometry
const earthGeometry = new THREE.SphereGeometry(earthRadius, isMobile ? 48 : 64, isMobile ? 48 : 64);
// Custom shader for day/night cycle
const earthMaterial = new THREE.ShaderMaterial({
uniforms: {
dayTexture: { value: earthDayTexture },
nightTexture: { value: earthNightTexture },
sunDirection: { value: new THREE.Vector3(1, 0.5, 1).normalize() }
},
vertexShader: `
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vPosition;
void main() {
vUv = uv;
vNormal = normalize(normalMatrix * normal);
vPosition = (modelMatrix * vec4(position, 1.0)).xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D dayTexture;
uniform sampler2D nightTexture;
uniform vec3 sunDirection;
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vPosition;
void main() {
vec3 dayColor = texture2D(dayTexture, vUv).rgb;
vec3 nightColor = texture2D(nightTexture, vUv).rgb;
float sunIntensity = dot(vNormal, sunDirection);
float dayFactor = smoothstep(-0.2, 0.4, sunIntensity);
nightColor *= 1.5;
vec3 color = mix(nightColor, dayColor, dayFactor);
color += vec3(0.02, 0.03, 0.05);
float fresnel = pow(1.0 - abs(dot(vNormal, vec3(0.0, 0.0, 1.0))), 3.0);
vec3 atmosphereColor = vec3(0.3, 0.6, 1.0);
color = mix(color, atmosphereColor, fresnel * 0.3);
gl_FragColor = vec4(color, 1.0);
}
`
});
const earth = new THREE.Mesh(earthGeometry, earthMaterial);
earthGroup.add(earth);
// Cloud layer
const cloudGeometry = new THREE.SphereGeometry(earthRadius * 1.02, isMobile ? 48 : 64, isMobile ? 48 : 64);
const cloudMaterial = new THREE.MeshPhongMaterial({
map: earthCloudsTexture,
transparent: true,
opacity: 0.4,
depthWrite: false,
side: THREE.DoubleSide
});
const clouds = new THREE.Mesh(cloudGeometry, cloudMaterial);
earthGroup.add(clouds);
// Atmosphere glow
const atmosphereGeometry = new THREE.SphereGeometry(earthRadius * 1.15, isMobile ? 48 : 64, isMobile ? 48 : 64);
const atmosphereMaterial = new THREE.ShaderMaterial({
uniforms: {
glowColor: { value: new THREE.Color(0x3388ff) },
viewVector: { value: camera.position }
},
vertexShader: `
uniform vec3 viewVector;
varying float intensity;
void main() {
vec3 vNormal = normalize(normalMatrix * normal);
vec3 vNormel = normalize(normalMatrix * viewVector);
intensity = pow(0.7 - dot(vNormal, vNormel), 2.0);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform vec3 glowColor;
varying float intensity;
void main() {
vec3 glow = glowColor * intensity;
gl_FragColor = vec4(glow, intensity * 0.6);
}
`,
side: THREE.BackSide,
blending: THREE.AdditiveBlending,
transparent: true
});
const atmosphereOuter = new THREE.Mesh(atmosphereGeometry, atmosphereMaterial);
earthGroup.add(atmosphereOuter);
// ===== LIGHTING =====
const ambientLight = new THREE.AmbientLight(isMobile ? 0x555555 : 0x333333);
scene.add(ambientLight);
const sunLight = new THREE.DirectionalLight(0xffffff, 1.5);
sunLight.position.set(5, 3, 5);
scene.add(sunLight);
// ==================== AMBIENT ASTRONAUT — DETAILED EVA SUIT ====================
let updateAstronaut = null;
const astronautEnabled = !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (astronautEnabled) {
const astronautGroup = new THREE.Group(); // Keep name for compatibility
// ═══════════════════════════════════════════════════════════════
// ██ UFO SPACESHIP CONSTRUCTION ██
// ═══════════════════════════════════════════════════════════════
// ──────────── MATERIALS LIBRARY ────────────
const mobileEmissiveBoost = isMobile ? 1.8 : 1.0; // Brighter on mobile
const mat = {
// Main hull - lighter metallic with self-illumination
hull: new THREE.MeshStandardMaterial({
color: 0x5a7a9a, roughness: 0.3, metalness: 0.7,
emissive: 0x2a4a6a, emissiveIntensity: 0.4 * mobileEmissiveBoost
}),
// Hull accent - lighter
hullAccent: new THREE.MeshStandardMaterial({
color: 0x6a8aaa, roughness: 0.4, metalness: 0.6,
emissive: 0x3a5a7a, emissiveIntensity: 0.35 * mobileEmissiveBoost
}),
// Chrome trim - brighter
chrome: new THREE.MeshStandardMaterial({
color: 0xddddee, roughness: 0.1, metalness: 1.0,
emissive: 0x556677, emissiveIntensity: 0.4 * mobileEmissiveBoost
}),
// Dome glass - brighter tint
domeGlass: new THREE.MeshStandardMaterial({
color: 0x55bbdd, roughness: 0.05, metalness: 0.3,
transparent: true, opacity: 0.6,
emissive: 0x338899, emissiveIntensity: 0.7 * mobileEmissiveBoost
}),
// Inner dome glow - brighter
domeInner: new THREE.MeshStandardMaterial({
color: 0x00ffcc, emissive: 0x00ffaa, emissiveIntensity: 1.5 * mobileEmissiveBoost,
transparent: true, opacity: 0.7
}),
// Cyan lights (primary) - brighter
lightCyan: new THREE.MeshStandardMaterial({
color: 0x00ffff, emissive: 0x00ffff, emissiveIntensity: 4.0 * mobileEmissiveBoost,
transparent: true, opacity: 1.0
}),
// Green lights (accent) - brighter
lightGreen: new THREE.MeshStandardMaterial({
color: 0x00ff88, emissive: 0x00ff88, emissiveIntensity: 3.5 * mobileEmissiveBoost
}),
// Blue thrust core - brighter
thrustCore: new THREE.MeshStandardMaterial({
color: 0x00ccff, emissive: 0x00aaff, emissiveIntensity: 5.0 * mobileEmissiveBoost,
transparent: true, opacity: 0.95
}),
// Thrust mid layer
thrustMid: new THREE.MeshStandardMaterial({
color: 0x00aaff, emissive: 0x0088ee, emissiveIntensity: 2.5 * mobileEmissiveBoost,
transparent: true, opacity: 0.55, side: THREE.DoubleSide
}),
// Thrust outer glow
thrustOuter: new THREE.MeshStandardMaterial({
color: 0x0088dd, emissive: 0x0066bb, emissiveIntensity: 1.2 * mobileEmissiveBoost,
transparent: true, opacity: 0.3, side: THREE.DoubleSide
}),
// Dark panels - lighter
panel: new THREE.MeshStandardMaterial({
color: 0x3a5a7a, roughness: 0.7, metalness: 0.4,
emissive: 0x2a3a4a, emissiveIntensity: 0.3 * mobileEmissiveBoost
})
};
// ──────────── MAIN SAUCER BODY ────────────
// Upper dome housing (flattened sphere)
const upperDome = new THREE.Mesh(
new THREE.SphereGeometry(8, 32, 16, 0, Math.PI * 2, 0, Math.PI * 0.5),
mat.hull
);
upperDome.scale.set(1, 0.5, 1);
upperDome.position.y = 2;
astronautGroup.add(upperDome);
// Glass cockpit dome
const glassDome = new THREE.Mesh(
new THREE.SphereGeometry(5, 32, 16, 0, Math.PI * 2, 0, Math.PI * 0.55),
mat.domeGlass
);
glassDome.scale.set(1, 0.7, 1);
glassDome.position.y = 3.5;
astronautGroup.add(glassDome);
// Inner dome glow
const innerGlow = new THREE.Mesh(
new THREE.SphereGeometry(4.5, 24, 12, 0, Math.PI * 2, 0, Math.PI * 0.5),
mat.domeInner
);
innerGlow.scale.set(1, 0.6, 1);
innerGlow.position.y = 3.5;
astronautGroup.add(innerGlow);
// Main disc body (torus-like shape using lathe)
const discProfile = [];
for (let i = 0; i <= 20; i++) {
const t = i / 20;
const angle = t * Math.PI;
const r = 8 + Math.sin(angle) * 7;
const y = Math.cos(angle) * 2.5;
discProfile.push(new THREE.Vector2(r, y));
}
const discGeo = new THREE.LatheGeometry(discProfile, 48);
const mainDisc = new THREE.Mesh(discGeo, mat.hull);
mainDisc.position.y = 0;
astronautGroup.add(mainDisc);
// Chrome ring (equator trim)
const chromeRing = new THREE.Mesh(
new THREE.TorusGeometry(15, 0.4, 16, 64),
mat.chrome
);
chromeRing.rotation.x = Math.PI / 2;
chromeRing.position.y = 0;
astronautGroup.add(chromeRing);
// Upper chrome ring
const upperRing = new THREE.Mesh(
new THREE.TorusGeometry(8, 0.25, 12, 48),
mat.chrome
);
upperRing.rotation.x = Math.PI / 2;
upperRing.position.y = 2;
astronautGroup.add(upperRing);
// ──────────── BOTTOM STRUCTURE ────────────
// Bottom dome (inverted, flatter)
const bottomDome = new THREE.Mesh(
new THREE.SphereGeometry(10, 32, 16, 0, Math.PI * 2, Math.PI * 0.5, Math.PI * 0.5),
mat.hullAccent
);
bottomDome.scale.set(1, 0.35, 1);
bottomDome.position.y = -2.5;
astronautGroup.add(bottomDome);
// Central thrust housing
const thrustHousing = new THREE.Mesh(
new THREE.CylinderGeometry(4, 5, 2, 24),
mat.hull
);
thrustHousing.position.y = -4;
astronautGroup.add(thrustHousing);
// Thrust emitter ring
const thrustEmitterRing = new THREE.Mesh(
new THREE.TorusGeometry(4.5, 0.3, 12, 32),
mat.chrome
);
thrustEmitterRing.rotation.x = Math.PI / 2;
thrustEmitterRing.position.y = -5;
astronautGroup.add(thrustEmitterRing);
// ──────────── RIM LIGHTS (8 around edge) ────────────
const rimLightCount = 8;
const rimLights = [];
const rimLightMats = [];
for (let i = 0; i < rimLightCount; i++) {
const angle = (i / rimLightCount) * Math.PI * 2;
const lightGroup = new THREE.Group();
// Light housing
const housing = new THREE.Mesh(
new THREE.SphereGeometry(1.2, 16, 12),
mat.hullAccent
);
housing.scale.set(1, 0.6, 1);
lightGroup.add(housing);
// Light dome (glowing) - unique material per light for animation
const lightMat = new THREE.MeshStandardMaterial({
color: 0x00ffff, emissive: 0x00ffff, emissiveIntensity: 3.5 * mobileEmissiveBoost,
transparent: true, opacity: 1.0
});
const lightDome = new THREE.Mesh(
new THREE.SphereGeometry(1, 16, 12, 0, Math.PI * 2, 0, Math.PI * 0.6),
lightMat
);
lightDome.position.y = 0.3;
lightGroup.add(lightDome);
lightGroup.position.set(
Math.cos(angle) * 14,
0.5,
Math.sin(angle) * 14
);
astronautGroup.add(lightGroup);
rimLights.push(lightDome);
rimLightMats.push(lightMat);
}
// ──────────── TOP DOME LIGHTS (4 around dome) ────────────
const domeLightCount = 4;
const domeLights = [];
const domeLightMats = [];
for (let i = 0; i < domeLightCount; i++) {
const angle = (i / domeLightCount) * Math.PI * 2 + Math.PI / 4;
const lightMat = new THREE.MeshStandardMaterial({
color: 0x00ff88, emissive: 0x00ff88, emissiveIntensity: 3.0 * mobileEmissiveBoost
});
const domeLight = new THREE.Mesh(
new THREE.SphereGeometry(0.6, 12, 8),
lightMat
);
domeLight.position.set(
Math.cos(angle) * 6,
4,
Math.sin(angle) * 6
);
astronautGroup.add(domeLight);
domeLights.push(domeLight);
domeLightMats.push(lightMat);
}
// ──────────── PANEL DETAILS ────────────
for (let i = 0; i < 12; i++) {
const angle = (i / 12) * Math.PI * 2;
const panelLine = new THREE.Mesh(
new THREE.BoxGeometry(0.1, 0.3, 5),
mat.panel
);
panelLine.position.set(
Math.cos(angle) * 11,
1,
Math.sin(angle) * 11
);
panelLine.rotation.y = -angle;
astronautGroup.add(panelLine);
}
// ══════════════════════════════════════════════════════════════
// ██ BLUE/RED THRUST BEAM SYSTEM (Signal Reactive) ██
// ══════════════════════════════════════════════════════════════
const mobileLightBoost = isMobile ? 2.0 : 1.0; // Extra brightness on mobile
const thrustGroup = new THREE.Group();
thrustGroup.position.y = -5;
astronautGroup.add(thrustGroup);
// Core beam (bright center) - LONGER & WIDER
const beamCoreGeo = new THREE.CylinderGeometry(1, 5, 30, 24, 1, true);
const beamCore = new THREE.Mesh(beamCoreGeo, mat.thrustCore);
beamCore.position.y = -15;
thrustGroup.add(beamCore);
// Mid beam layer - LONGER & WIDER
const beamMidGeo = new THREE.CylinderGeometry(2.5, 8, 35, 24, 1, true);
const beamMid = new THREE.Mesh(beamMidGeo, mat.thrustMid);
beamMid.position.y = -17;
thrustGroup.add(beamMid);
// Outer glow cone - LONGER & WIDER
const beamOuterGeo = new THREE.CylinderGeometry(5, 14, 40, 24, 1, true);
const beamOuter = new THREE.Mesh(beamOuterGeo, mat.thrustOuter);
beamOuter.position.y = -20;
thrustGroup.add(beamOuter);
// Thrust particles - wider spread, longer travel (reduced on mobile)
const thrustParticleCount = isMobile ? 80 : 200;
const thrustParticleGeo = new THREE.BufferGeometry();
const thrustParticlePos = new Float32Array(thrustParticleCount * 3);
const thrustParticleSpeeds = [];
for (let i = 0; i < thrustParticleCount; i++) {
const angle = Math.random() * Math.PI * 2;
const radius = Math.random() * 5;
thrustParticlePos[i * 3] = Math.cos(angle) * radius;
thrustParticlePos[i * 3 + 1] = -Math.random() * 35;
thrustParticlePos[i * 3 + 2] = Math.sin(angle) * radius;
thrustParticleSpeeds.push(0.6 + Math.random() * 1.8);
}
thrustParticleGeo.setAttribute('position', new THREE.BufferAttribute(thrustParticlePos, 3));
const thrustParticleMat = new THREE.PointsMaterial({
color: 0x00eeff, size: isMobile ? 1.0 : 0.7, transparent: true, opacity: 1.0,
blending: THREE.AdditiveBlending
});
const thrustParticles = new THREE.Points(thrustParticleGeo, thrustParticleMat);
thrustGroup.add(thrustParticles);
// Point light for thrust illumination - stronger
const thrustLight = new THREE.PointLight(0x00aaff, 5 * mobileLightBoost, 100);
thrustLight.position.y = -15;
thrustGroup.add(thrustLight);
// ──────────── POINT LIGHTS (brighter for visibility) ────────────
const ufoMainLight = new THREE.PointLight(0x00d4ff, 1.5 * mobileLightBoost, 150);
ufoMainLight.position.set(0, 8, 0);
astronautGroup.add(ufoMainLight);
const ufoRimLight = new THREE.PointLight(0x00ffaa, 1.0 * mobileLightBoost, 120);
ufoRimLight.position.set(0, -5, 0);
astronautGroup.add(ufoRimLight);
// Additional lights for hull illumination
const ufoTopLight = new THREE.PointLight(0x88aacc, 0.8 * mobileLightBoost, 80);
ufoTopLight.position.set(0, 15, 0);
astronautGroup.add(ufoTopLight);
const ufoFrontLight = new THREE.PointLight(0x00ccff, 0.6 * mobileLightBoost, 60);
ufoFrontLight.position.set(0, 0, 20);
astronautGroup.add(ufoFrontLight);
// Extra bottom light for mobile visibility
if (isMobile) {
const ufoBottomLight = new THREE.PointLight(0x00aaff, 1.5, 100);
ufoBottomLight.position.set(0, -15, 0);
astronautGroup.add(ufoBottomLight);
}
// ──────────── FINAL SETUP ────────────
const ufoScale = isMobile ? 0.7 : 0.8; // Slightly smaller on mobile
astronautGroup.scale.setScalar(ufoScale);
scene.add(astronautGroup);
// ════════════════════════════════════════════════════════════════════
// ██ SUPERMAN FLIGHT SYSTEM — Spline-Driven Heroic Motion ██
// ════════════════════════════════════════════════════════════════════
// ──────────── MATH UTILITIES ────────────
function smoothstep(edge0, edge1, x) {
const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
return t * t * (3 - 2 * t);
}
function smootherstep(edge0, edge1, x) {
const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
return t * t * t * (t * (t * 6 - 15) + 10);
}
function catmullRom(p0, p1, p2, p3, t) {
const t2 = t * t;
const t3 = t2 * t;
return new THREE.Vector3(
0.5 * ((2 * p1.x) + (-p0.x + p2.x) * t + (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 + (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3),
0.5 * ((2 * p1.y) + (-p0.y + p2.y) * t + (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 + (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3),
0.5 * ((2 * p1.z) + (-p0.z + p2.z) * t + (2 * p0.z - 5 * p1.z + 4 * p2.z - p3.z) * t2 + (-p0.z + 3 * p1.z - 3 * p2.z + p3.z) * t3)
);
}
function catmullRomTangent(p0, p1, p2, p3, t) {
const t2 = t * t;
return new THREE.Vector3(
0.5 * ((-p0.x + p2.x) + 2 * (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t + 3 * (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t2),
0.5 * ((-p0.y + p2.y) + 2 * (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t + 3 * (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t2),
0.5 * ((-p0.z + p2.z) + 2 * (2 * p0.z - 5 * p1.z + 4 * p2.z - p3.z) * t + 3 * (-p0.z + 3 * p1.z - 3 * p2.z + p3.z) * t2)
).normalize();
}
function quaternionFromDirection(direction, up) {
up = up || new THREE.Vector3(0, 1, 0);
const matrix = new THREE.Matrix4();
matrix.lookAt(new THREE.Vector3(0, 0, 0), direction, up);
const quat = new THREE.Quaternion();
quat.setFromRotationMatrix(matrix);
return quat;
}
// ──────────── FLIGHT BOUNDS ────────────
const flightBounds = {
xMin: -180, xMax: 180,
yMin: 60, yMax: 220,
zMin: -120, zMax: 80
};
// ──────────── WAYPOINT GENERATOR ────────────
function generateWaypoint(currentPos, currentDirection) {
const minDist = 100;
const maxDist = 200;
const dist = minDist + Math.random() * (maxDist - minDist);
const forwardBias = 0.6;
let newDir = new THREE.Vector3(
currentDirection.x * forwardBias + (Math.random() - 0.5) * (1 - forwardBias) * 2,
currentDirection.y * forwardBias + (Math.random() - 0.5) * (1 - forwardBias) * 1.5,
currentDirection.z * forwardBias + (Math.random() - 0.5) * (1 - forwardBias) * 2
).normalize();
let target = new THREE.Vector3(
currentPos.x + newDir.x * dist,
currentPos.y + newDir.y * dist,
currentPos.z + newDir.z * dist
);
const margin = 30;
if (target.x < flightBounds.xMin + margin) target.x = flightBounds.xMin + margin + Math.random() * 40;
if (target.x > flightBounds.xMax - margin) target.x = flightBounds.xMax - margin - Math.random() * 40;
if (target.y < flightBounds.yMin + margin) target.y = flightBounds.yMin + margin + Math.random() * 30;
if (target.y > flightBounds.yMax - margin) target.y = flightBounds.yMax - margin - Math.random() * 30;
if (target.z < flightBounds.zMin + margin) target.z = flightBounds.zMin + margin + Math.random() * 40;
if (target.z > flightBounds.zMax - 50) target.z = flightBounds.zMax - 50 - Math.random() * 30;
return target;
}
// ──────────── BEHAVIORAL STATE ────────────
const ec = earthGroup.position;
const startPos = new THREE.Vector3(ec.x + 50, ec.y + 130, ec.z - 30);
const st = {
waypoints: [
startPos.clone().add(new THREE.Vector3(-100, 20, -50)),
startPos.clone(),
startPos.clone().add(new THREE.Vector3(80, -10, 30)),
startPos.clone().add(new THREE.Vector3(160, 30, -20))
],
t: 0,
segmentDuration: 4,
cruiseSpeed: 1.2,
currentSpeed: 0,
currentQuat: new THREE.Quaternion(),
targetQuat: new THREE.Quaternion(),
bankAngle: 0,
targetBankAngle: 0,
verticalWavePhase: Math.random() * Math.PI * 2,
verticalWaveAmp: 1.5,
verticalWaveFreq: 0.08,
prevVelocity: new THREE.Vector3(1, 0, 0),
totalTime: 0,
thrusterIntensity: 1.0,
opacity: 1.0,
targetOp: 1.0,
// Teleport state
teleportCooldown: 15 + Math.random() * 20, // Time until next teleport
teleportTimer: 0,
isTeleporting: false,
teleportPhase: 0,
teleportDuration: 3.5, // How long to stay at Market Directive
teleportFlashIntensity: 0,
preTeleportPos: null,
preTeleportQuat: null
};
const initialDir = new THREE.Vector3().subVectors(st.waypoints[2], st.waypoints[1]).normalize();
st.currentQuat = quaternionFromDirection(initialDir);
st.targetQuat = st.currentQuat.clone();
astronautGroup.quaternion.copy(st.currentQuat);
astronautGroup.position.copy(st.waypoints[1]);
function advanceWaypoint() {
st.waypoints[0] = st.waypoints[1].clone();
st.waypoints[1] = st.waypoints[2].clone();
st.waypoints[2] = st.waypoints[3].clone();
const currentDir = new THREE.Vector3().subVectors(st.waypoints[2], st.waypoints[1]).normalize();
st.waypoints[3] = generateWaypoint(st.waypoints[2], currentDir);
st.t = 0;
st.segmentDuration = 3 + Math.random() * 3;
st.cruiseSpeed = 1.0 + Math.random() * 0.5;
}
// ──────────── UPDATE FUNCTION (UFO Flight) ────────────
updateAstronaut = function(time, deltaTime) {
const dt = Math.min(deltaTime || 0.016, 0.05);
st.totalTime += dt;
// ════════════════════════════════════════════════════════
// █ SUPERMAN TRAJECTORY — Spline + Velocity Profile █
// ════════════════════════════════════════════════════════
const accelZone = 0.15;
const decelZone = 0.85;
let speedMultiplier;
if (st.t < accelZone) {
speedMultiplier = smootherstep(0, accelZone, st.t);
} else if (st.t > decelZone) {
speedMultiplier = smootherstep(1, decelZone, st.t);
} else {
speedMultiplier = 1.0;
}
st.currentSpeed = speedMultiplier * st.cruiseSpeed;
const baseProgress = dt / st.segmentDuration;
st.t += baseProgress * (0.5 + st.currentSpeed * 1.2);
if (st.t >= 1.0) {
st.t = 0;
advanceWaypoint();
}
const splinePos = catmullRom(
st.waypoints[0], st.waypoints[1],
st.waypoints[2], st.waypoints[3],
st.t
);
st.verticalWavePhase += dt * st.verticalWaveFreq * Math.PI * 2;
const verticalOffset = Math.sin(st.verticalWavePhase) * st.verticalWaveAmp;
splinePos.y += verticalOffset;
// ════════════════════════════════════════════════════════
// █ TELEPORT TO MARKET DIRECTIVE █
// ════════════════════════════════════════════════════════
st.teleportTimer += dt;
// Check if it's time to teleport
if (!st.isTeleporting && st.teleportTimer >= st.teleportCooldown) {
// Start teleport sequence
st.isTeleporting = true;
st.teleportPhase = 0;
st.teleportFlashIntensity = 1.0;
st.preTeleportPos = astronautGroup.position.clone();
st.preTeleportQuat = astronautGroup.quaternion.clone();
// Get Market Directive element position
const directiveEl = document.querySelector('.signal-panel');
if (directiveEl) {
const rect = directiveEl.getBoundingClientRect();
// Convert screen position to normalized device coordinates
const screenX = (rect.left + rect.width / 2) / window.innerWidth * 2 - 1;
const screenY = -((rect.top + rect.height / 2) / window.innerHeight * 2 - 1);
// Project to 3D space (in front of camera)
const teleportZ = 150; // Close to camera
const fov = camera.fov * Math.PI / 180;
const height = 2 * Math.tan(fov / 2) * teleportZ;
const width = height * camera.aspect;
st.teleportTarget = new THREE.Vector3(
screenX * width / 2,
screenY * height / 2 + 20, // Slightly above center
camera.position.z - teleportZ
);
}
}
// Handle teleport animation
if (st.isTeleporting) {
st.teleportPhase += dt;
// Flash effect fades
st.teleportFlashIntensity = Math.max(0, st.teleportFlashIntensity - dt * 2);
if (st.teleportPhase < st.teleportDuration) {
// At Market Directive - hover with gentle movement
if (st.teleportTarget) {
const hoverOffset = new THREE.Vector3(
Math.sin(st.teleportPhase * 2) * 8,
Math.sin(st.teleportPhase * 1.5) * 5,
Math.sin(st.teleportPhase * 1.8) * 4
);
astronautGroup.position.copy(st.teleportTarget).add(hoverOffset);
// Face the camera (slightly tilted)
const lookDir = new THREE.Vector3(0, 0, 1);
const tiltAngle = Math.sin(st.teleportPhase * 2) * 0.15;
st.targetQuat = quaternionFromDirection(lookDir);
const tiltQuat = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(1, 0, 0), tiltAngle
);
st.targetQuat.multiply(tiltQuat);
st.currentQuat.slerp(st.targetQuat, 0.1);
astronautGroup.quaternion.copy(st.currentQuat);
}
} else {
// Teleport back - flash and return
if (st.teleportPhase < st.teleportDuration + 0.5) {
st.teleportFlashIntensity = 1.0 - (st.teleportPhase - st.teleportDuration) * 2;
} else {
// End teleport
st.isTeleporting = false;
st.teleportTimer = 0;
st.teleportCooldown = 20 + Math.random() * 25; // Next teleport in 20-45 seconds
st.teleportFlashIntensity = 0;
// Reset waypoints from current spline position
st.waypoints[1] = splinePos.clone();
const dir = st.prevVelocity.clone().normalize();
st.waypoints[0] = splinePos.clone().sub(dir.clone().multiplyScalar(50));
st.waypoints[2] = generateWaypoint(splinePos, dir);
st.waypoints[3] = generateWaypoint(st.waypoints[2], dir);
st.t = 0;
}
}
} else {
// Normal flight - apply spline position
astronautGroup.position.copy(splinePos);
}
// ════════════════════════════════════════════════════════
// █ ORIENTATION — Face Velocity + Banking █
// ════════════════════════════════════════════════════════
// Only do normal orientation when not teleporting
if (!st.isTeleporting) {
const velocity = catmullRomTangent(
st.waypoints[0], st.waypoints[1],
st.waypoints[2], st.waypoints[3],
st.t
);
const turnAxis = new THREE.Vector3().crossVectors(st.prevVelocity, velocity);
const turnAmount = turnAxis.y;
st.targetBankAngle = -turnAmount * 35 * (Math.PI / 180);
st.bankAngle += (st.targetBankAngle - st.bankAngle) * 0.08;
st.prevVelocity.copy(velocity);
st.targetQuat = quaternionFromDirection(velocity);
const bankQuat = new THREE.Quaternion();
bankQuat.setFromAxisAngle(velocity, st.bankAngle);
st.targetQuat.multiply(bankQuat);
st.currentQuat.slerp(st.targetQuat, 0.12);
astronautGroup.quaternion.copy(st.currentQuat);
}
// ════════════════════════════════════════════════════════
// █ UFO LIGHT ANIMATIONS █
// ════════════════════════════════════════════════════════
// Rim lights pulse in sequence (brighter)
for (let i = 0; i < rimLightCount; i++) {
const phase = time * 2 + (i / rimLightCount) * Math.PI * 2;
rimLightMats[i].emissiveIntensity = 2.5 + Math.sin(phase) * 1.0;
rimLightMats[i].opacity = 0.85 + Math.sin(phase) * 0.15;
}
// Dome lights alternate (brighter)
for (let i = 0; i < domeLightCount; i++) {
const phase = time * 3 + (i / domeLightCount) * Math.PI * 2;
domeLightMats[i].emissiveIntensity = 2.0 + Math.sin(phase) * 1.0;
}
// Inner dome pulse (brighter)
mat.domeInner.emissiveIntensity = 0.8 + Math.sin(time * 1.5) * 0.4;
mat.domeInner.opacity = 0.5 + Math.sin(time * 1.5) * 0.15;
// ════════════════════════════════════════════════════════
// █ THRUST BEAM ANIMATIONS (Signal-Reactive Color) █
// ════════════════════════════════════════════════════════
// Check dominant signal for beam color
const signalEl = document.getElementById('dominantSignal');
const dominantSignal = signalEl ? signalEl.textContent.trim().toUpperCase() : 'NEUTRAL';
const isSellSignal = dominantSignal === 'SELL';
const isBuySignal = dominantSignal === 'BUY';
const isNeutral = !isSellSignal && !isBuySignal;
// Color targets based on signal
// SELL = Red, BUY = Blue, NEUTRAL = Golden
let targetCoreColor, targetCoreEmissive, targetMidColor, targetMidEmissive;
let targetOuterColor, targetOuterEmissive, targetParticleColor, targetLightColor;
if (isSellSignal) {
// RED for SELL
targetCoreColor = new THREE.Color(0xff4444);
targetCoreEmissive = new THREE.Color(0xff2222);
targetMidColor = new THREE.Color(0xff6644);
targetMidEmissive = new THREE.Color(0xee4422);
targetOuterColor = new THREE.Color(0xcc4422);
targetOuterEmissive = new THREE.Color(0xaa3311);
targetParticleColor = new THREE.Color(0xff6655);
targetLightColor = 0xff4433;
} else if (isBuySignal) {
// BLUE for BUY
targetCoreColor = new THREE.Color(0x00ccff);
targetCoreEmissive = new THREE.Color(0x00aaff);
targetMidColor = new THREE.Color(0x00aaff);
targetMidEmissive = new THREE.Color(0x0088ee);
targetOuterColor = new THREE.Color(0x0088dd);
targetOuterEmissive = new THREE.Color(0x0066bb);
targetParticleColor = new THREE.Color(0x00eeff);
targetLightColor = 0x00aaff;
} else {
// GOLDEN for NEUTRAL
targetCoreColor = new THREE.Color(0xffcc44);
targetCoreEmissive = new THREE.Color(0xffaa22);
targetMidColor = new THREE.Color(0xddaa33);
targetMidEmissive = new THREE.Color(0xcc9922);
targetOuterColor = new THREE.Color(0xbb8822);
targetOuterEmissive = new THREE.Color(0x996611);
targetParticleColor = new THREE.Color(0xffdd66);
targetLightColor = 0xffaa33;
}
// Smoothly lerp colors
mat.thrustCore.color.lerp(targetCoreColor, 0.08);
mat.thrustCore.emissive.lerp(targetCoreEmissive, 0.08);
mat.thrustMid.color.lerp(targetMidColor, 0.08);
mat.thrustMid.emissive.lerp(targetMidEmissive, 0.08);
mat.thrustOuter.color.lerp(targetOuterColor, 0.08);
mat.thrustOuter.emissive.lerp(targetOuterEmissive, 0.08);
thrustParticleMat.color.lerp(targetParticleColor, 0.08);
// Lerp light color
const currentLightColor = new THREE.Color(thrustLight.color);
currentLightColor.lerp(new THREE.Color(targetLightColor), 0.08);
thrustLight.color.copy(currentLightColor);
// Thrust intensity based on speed (boost during teleport)
const isAccelerating = st.t < accelZone && st.currentSpeed > 0.3;
const isTurning = Math.abs(st.targetBankAngle) > 0.05;
const thrustActive = isAccelerating || isTurning || st.currentSpeed > 0.5 || st.isTeleporting;
if (thrustActive) {
st.thrusterIntensity = Math.min(st.thrusterIntensity + dt * 3, st.isTeleporting ? 1.2 : 1);
} else {
st.thrusterIntensity = Math.max(st.thrusterIntensity - dt * 2, 0.3);
}
// Core beam intensity fluctuation (brighter)
mat.thrustCore.emissiveIntensity = (3.5 + Math.sin(time * 8) * 0.8) * st.thrusterIntensity;
mat.thrustCore.opacity = (0.9 + Math.sin(time * 6) * 0.1) * st.thrusterIntensity;
// Beam layers subtle rotation
beamCore.rotation.y = time * 0.5;
beamMid.rotation.y = -time * 0.3;
beamOuter.rotation.y = time * 0.2;
// Beam scale based on thrust intensity
const thrustScale = 0.5 + st.thrusterIntensity * 0.5;
thrustGroup.scale.set(thrustScale, thrustScale, thrustScale);
// Thrust light flicker (brighter)
thrustLight.intensity = (4.0 + Math.sin(time * 10) * 1.0 + Math.sin(time * 17) * 0.5) * st.thrusterIntensity;
// Animate thrust particles - longer travel distance
const positions = thrustParticleGeo.attributes.position.array;
for (let i = 0; i < thrustParticleCount; i++) {
positions[i * 3 + 1] -= thrustParticleSpeeds[i] * 0.6 * st.thrusterIntensity;
if (positions[i * 3 + 1] < -38) {
const angle = Math.random() * Math.PI * 2;
const radius = Math.random() * 5;
positions[i * 3] = Math.cos(angle) * radius;
positions[i * 3 + 1] = 0;
positions[i * 3 + 2] = Math.sin(angle) * radius;
}
positions[i * 3] *= 1.003;
positions[i * 3 + 2] *= 1.003;
}
thrustParticleGeo.attributes.position.needsUpdate = true;
thrustParticleMat.opacity = 0.85 * st.thrusterIntensity;
// Main UFO light pulse (brighter)
ufoMainLight.intensity = 1.2 + Math.sin(time * 1.5) * 0.4 + st.teleportFlashIntensity * 3;
ufoRimLight.intensity = 0.8 + st.thrusterIntensity * 0.5 + st.teleportFlashIntensity * 2;
// Teleport flash on all rim lights
if (st.teleportFlashIntensity > 0) {
for (let i = 0; i < rimLightCount; i++) {
rimLightMats[i].emissiveIntensity += st.teleportFlashIntensity * 5;
}
for (let i = 0; i < domeLightCount; i++) {
domeLightMats[i].emissiveIntensity += st.teleportFlashIntensity * 4;
}
mat.domeInner.emissiveIntensity += st.teleportFlashIntensity * 3;
}
// ════════════════════════════════════════════════════════
// █ DEPTH OPACITY █
// ════════════════════════════════════════════════════════
// Make UFO fully visible during teleport or on mobile
if (st.isTeleporting || isMobile) {
st.targetOp = 1.0;
st.opacity = 1.0;
} else {
const distCam = astronautGroup.position.distanceTo(camera.position);
st.targetOp = distCam < 250 ? 0.4 : distCam < 450 ? 0.7 : 1.0;
st.opacity += (st.targetOp - st.opacity) * 0.015;
}
};
}
// ===== ANIMATION =====
let time = 0;
let lastTime = performance.now();
function animate() {
requestAnimationFrame(animate);
const currentTime = performance.now();
const deltaTime = (currentTime - lastTime) / 1000;
lastTime = currentTime;
time += 0.016; // Consistent time for visual effects
// Stars rotation
stars.rotation.y += 0.0001;
stars.rotation.x += 0.00005;
// ===== PHYSICS SIMULATION =====
simulatePhysics(deltaTime);
updateConnections();
// Update node shader time uniform
nodeShaderMaterial.uniforms.time.value = time;
// Gentle rotation of entire network
nodePoints.rotation.y += 0.0002;
lines.rotation.y += 0.0002;
// Earth rotation
earth.rotation.y += 0.0005;
clouds.rotation.y += 0.0006;
clouds.rotation.x += 0.0001;
// Sun position for day/night cycle
const sunAngle = time * 0.05;
const sunDir = new THREE.Vector3(
Math.cos(sunAngle),
0.3,
Math.sin(sunAngle)
).normalize();
earthMaterial.uniforms.sunDirection.value = sunDir;
sunLight.position.set(sunDir.x * 5, sunDir.y * 5, sunDir.z * 5);
// Update atmosphere view vector
atmosphereMaterial.uniforms.viewVector.value = new THREE.Vector3().subVectors(
camera.position,
earthGroup.position
);
// ===== ASTRONAUT UPDATE =====
if (updateAstronaut) updateAstronaut(time, deltaTime);
renderer.render(scene, camera);
}
// ===== RESIZE HANDLER =====
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Initialize connections and start animation
updateConnections();
animate();
// Hide loading after init
setTimeout(() => {
document.getElementById('loadingOverlay')?.classList.add('hidden');
}, 1500);
})();
// ==================== DASHBOARD LOGIC ====================
const signalHistory = [];
const SIGNAL_WINDOW = 5 * 60 * 1000;
const TARGET_AGENT = '10m';
let consecutiveErrors = 0;
// ==================== REDIS-BACKED SIGNAL TRACKING ====================
// ✅ MIGRATED: Signals now extracted from log polling instead of Ably WebSocket
// This is compatible with HuggingFace Spaces + Redis architecture
function extractAgentName(signalKey) {
if (!signalKey) return 'unknown';
signalKey = signalKey.toString().trim().toLowerCase();
if (signalKey.includes('_')) {
return signalKey.split('_')[0];
}
const match = signalKey.match(/\d+[mhdw]/);
return match ? match[0] : 'unknown';
}
// Force Balance - Updated for new gauge design
function updateForceBalance(buyPercent, sellPercent, dominant) {
const buyFill = document.getElementById('gaugeFillBuy');
const sellFill = document.getElementById('gaugeFillSell');
const marker = document.getElementById('gaugeMarker');
const buyVal = document.getElementById('forceBuyValue');
const sellVal = document.getElementById('forceSellValue');
const netVal = document.getElementById('netPositionValue');
// Clamp values
buyPercent = Math.min(100, Math.max(0, buyPercent || 0));
sellPercent = Math.min(100, Math.max(0, sellPercent || 0));
// Update fill bars using scaleX transform (0 to 1 range)
if (buyFill) {
buyFill.style.transform = `scaleX(${buyPercent / 100})`;
}
if (sellFill) {
sellFill.style.transform = `scaleX(${sellPercent / 100})`;
}
// Update percentage values
if (buyVal) buyVal.textContent = buyPercent.toFixed(1) + '%';
if (sellVal) sellVal.textContent = sellPercent.toFixed(1) + '%';
// Calculate and display net position
const net = buyPercent - sellPercent;
if (netVal) {
netVal.textContent = (net >= 0 ? '+' : '') + net.toFixed(1);
netVal.classList.remove('positive', 'negative', 'neutral');
if (net > 0) {
netVal.classList.add('positive');
} else if (net < 0) {
netVal.classList.add('negative');
} else {
netVal.classList.add('neutral');
}
}
// Position marker: 50% = center
// Buy pushes left (towards 0%), Sell pushes right (towards 100%)
const markerPos = 50 + ((sellPercent - buyPercent) / 2);
if (marker) {
marker.style.left = markerPos + '%';
marker.classList.remove('buy-dominant', 'sell-dominant');
if (dominant === 'BUY') {
marker.classList.add('buy-dominant');
} else if (dominant === 'SELL') {
marker.classList.add('sell-dominant');
}
}
}
function updateDominantSignal() {
const now = Date.now();
const recentSignals = signalHistory.filter(s => now - s.timestamp < SIGNAL_WINDOW);
const buyCount = recentSignals.filter(s => s.action === 'BUY').length;
const sellCount = recentSignals.filter(s => s.action === 'SELL').length;
const total = buyCount + sellCount;
const el = document.getElementById('dominantSignal');
const statsEl = document.getElementById('signalStats');
let dominant = 'NEUTRAL';
if (total > 0) {
if (buyCount > sellCount) dominant = 'BUY';
else if (sellCount > buyCount) dominant = 'SELL';
}
if (el) {
el.textContent = dominant;
el.className = 'signal-value signal-' + dominant.toLowerCase();
}
if (statsEl && total > 0) {
const buyPct = ((buyCount / total) * 100).toFixed(0);
const sellPct = ((sellCount / total) * 100).toFixed(0);
statsEl.innerHTML = `<span>BUY: ${buyCount} (${buyPct}%)</span> • <span>SELL: ${sellCount} (${sellPct}%)</span> • <span>TOTAL: ${total}</span>`;
updateForceBalance(parseFloat(buyPct), parseFloat(sellPct), dominant);
}
}
// ==================== HELPER FUNCTIONS ====================
// ✅ Safe text setter — guards against null, undefined, NaN, and "undefined" strings
function setText(id, text) {
const el = document.getElementById(id);
if (el) {
if (text === null || text === undefined || String(text).includes('undefined') || String(text).includes('NaN')) {
el.textContent = '—';
} else {
el.textContent = text;
}
}
}
// ✅ Number formatter — compact display for large numbers
function formatNumber(num) {
if (num === undefined || num === null || isNaN(Number(num))) return '—';
num = Number(num);
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
return num.toLocaleString();
}
// ✅ Safe number formatter with decimal control
function safeNum(val, decimals = 1) {
if (val === undefined || val === null || val === '' || isNaN(Number(val))) return null;
return Number(val).toFixed(decimals);
}
// ✅ Safe format with decimal control (returns string or dash)
function formatNum(val, decimals) {
if (val === undefined || val === null || val === '' || isNaN(Number(val))) return '—';
return Number(val).toFixed(decimals);
}
function updateConnectionStatus(connected) {
const el = document.getElementById('connectionStatus');
if (el) {
el.textContent = connected ? '● Connected' : '● Disconnected';
el.className = 'connection-status ' + (connected ? 'connection-ok' : 'connection-error');
}
}
// ✅ MIGRATED: HF Spaces compatible metrics fetch with fallback patterns
function updateMetrics() {
fetch('/api/metrics')
.then(r => r.json())
.then(data => {
consecutiveErrors = 0;
updateConnectionStatus(true);
const now = new Date();
setText('lastUpdate', 'Last Observation: ' + now.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}));
// V75: Update instrument bar from API asset field
if (data.asset) {
const instBar = document.getElementById('instBar');
if (instBar) instBar.textContent = data.asset;
}
// Training metrics - use data.metrics path
if (data.metrics) {
setText('trainingSteps', formatNumber(data.metrics.training_steps));
setText('actorLoss', formatNum(data.metrics.actor_loss, 2));
setText('criticLoss', formatNum(data.metrics.critic_loss, 4));
setText('avnLoss', formatNum(data.metrics.avn_loss, 4));
// AVN Accuracy with color coding
const accEl = document.getElementById('avnAccuracy');
if (accEl) {
const accRaw = data.metrics.avn_accuracy;
const accValid = accRaw !== undefined && accRaw !== null && !isNaN(Number(accRaw));
if (accValid) {
const accVal = Number(accRaw).toFixed(1);
accEl.textContent = accVal + '%';
accEl.style.color = parseFloat(accVal) >= 55 ? '#00d4aa' : '#ffa500';
} else {
accEl.textContent = '—';
}
}
setText('bufferSize', formatNumber(data.metrics.buffer_size));
// Reward metrics with match efficiency calculation
const matched = data.metrics.matched_rewards || 0;
const unmatched = data.metrics.unmatched_rewards || 0;
const total = matched + unmatched;
const matchRate = total > 0 ? (matched / total * 100).toFixed(1) + '%' : '—';
setText('matchedRewards', matched > 0 ? formatNumber(matched) : '—');
setText('unmatchedRewards', unmatched > 0 ? formatNumber(unmatched) : '—');
setText('duplicates', formatNumber(data.metrics.duplicates));
setText('matchRate', matchRate);
}
// ✅ Service status — handles different name formats from HF Spaces
if (data.services) {
updateServiceStatus('statusQuasar', data.services['quasar_engine'] || data.services['quasar'] || 'unknown');
updateServiceStatus('statusFeatures', data.services['features'] || data.services['feature_pipeline'] || 'active');
updateServiceStatus('statusRewards', data.services['rewards'] || data.services['reward_system'] || 'active');
}
// Process count — handles HF Spaces format
const processCount = data.process_count || data.resources?.process_count || 1;
const procEl = document.getElementById('processCount');
const procVal = document.getElementById('processCountValue');
if (procVal) procVal.textContent = processCount;
if (procEl) {
procEl.classList.remove('process-ok', 'process-warning');
procEl.classList.add(processCount >= 1 ? 'process-ok' : 'process-warning');
}
// ✅ System resources — handles HF Spaces data shapes
if (data.resources) {
const cpuVal = safeNum(data.resources.cpu_percent);
setText('cpuPercent', cpuVal !== null ? cpuVal + '%' : '—');
const cpuBar = document.getElementById('cpuBar');
if (cpuBar) {
cpuBar.style.width = (cpuVal !== null ? Math.min(parseFloat(cpuVal), 100) : 0) + '%';
cpuBar.className = 'progress-fill' + (parseFloat(cpuVal) > 80 ? ' warning' : '');
}
const memPct = safeNum(data.resources.memory_percent);
setText('memoryPercent', memPct !== null ? memPct + '%' : '—');
const memUsed = data.resources.memory_used_gb;
const memTotal = data.resources.memory_total_gb;
const memUsedValid = memUsed !== undefined && memUsed !== null && memUsed !== '';
const memTotalValid = memTotal !== undefined && memTotal !== null && memTotal !== '';
setText('memoryUsed', (memUsedValid && memTotalValid)
? memUsed + ' / ' + memTotal + ' GB'
: '—');
const quasarMem = data.resources.quasar_memory_gb;
const quasarValid = quasarMem !== undefined && quasarMem !== null && quasarMem !== '';
setText('quasarMemory', quasarValid ? quasarMem + ' GB' : '—');
} else {
setText('cpuPercent', '—');
setText('memoryPercent', '—');
setText('memoryUsed', '—');
setText('quasarMemory', '—');
}
// Checkpoint data
if (data.checkpoint) {
setText('checkpointFile', data.checkpoint.filename ?? '—');
setText('checkpointStep', data.checkpoint.step?.toLocaleString() ?? '—');
setText('checkpointSize', data.checkpoint.size_mb !== undefined ? data.checkpoint.size_mb + ' MB' : '—');
setText('checkpointModified', data.checkpoint.modified ?? '—');
}
})
.catch(error => {
console.error('Error fetching metrics:', error);
consecutiveErrors++;
if (consecutiveErrors >= 3) updateConnectionStatus(false);
});
}
function updateServiceStatus(elementId, status) {
const elem = document.getElementById(elementId);
if (!elem) return;
elem.textContent = (status || 'unknown').toUpperCase();
if (status === 'active') {
elem.className = 'status-badge status-active';
} else if (status === 'unknown' || status === undefined || status === null) {
// Unknown = booting/pending, not necessarily failed — show amber
elem.className = 'status-badge status-unknown';
elem.textContent = 'PENDING';
} else {
elem.className = 'status-badge status-inactive';
}
}
// ✅ MIGRATED: HF Spaces compatible logs fetch
// Strategy: Try /logs/quasar_engine (text) → fallback /api/logs/quasar (JSON)
function updateLogs() {
fetch('/logs/quasar_engine')
.then(response => {
if (!response.ok) {
// Fallback to alternative endpoint
return fetch('/api/logs/quasar');
}
return response;
})
.then(response => {
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
return response.json().then(data => (data.lines || []).join('\n'));
}
return response.text();
})
.then(logText => {
const cont = document.getElementById('logContainer');
if (!cont) return;
cont.innerHTML = '';
const lines = logText.split('\n').filter(line => line.trim());
const recentLines = lines.slice(-50);
recentLines.forEach(line => {
if (!line.trim()) return;
const div = document.createElement('div');
div.className = 'log-line';
// Style log lines based on content
if (line.includes('ERROR') || line.includes('error') || line.includes('Error') || line.includes('❌')) {
div.classList.add('log-error');
} else if (line.includes('WARNING') || line.includes('warning') || line.includes('Warning') || line.includes('⚠️')) {
div.classList.add('log-warning');
} else if (line.includes('INFO') || line.includes('✅') || line.includes('SUCCESS')) {
div.classList.add('log-info');
}
// Extract signals for dominant signal tracker - filter by target agent
let agent = null;
const agentMatch1 = line.match(/(\d+[mhdw])_\d+/i);
if (agentMatch1) agent = agentMatch1[1].toLowerCase();
if (!agent) {
const agentMatch2 = line.match(/[\[\(](\d+[mhdw])[\]\)]/i);
if (agentMatch2) agent = agentMatch2[1].toLowerCase();
}
if (!agent) {
const agentMatch3 = line.match(/AVN[\s\-_]?(\d+[mhdw])[:\s]/i);
if (agentMatch3) agent = agentMatch3[1].toLowerCase();
}
// Skip signals from other agents
if (agent && agent !== TARGET_AGENT) {
return;
}
if (line.includes('AVN') && (line.includes('BUY') || line.includes('SELL'))) {
const buyMatch = line.match(/BUY.*conf[idence]*[=:]\s*([\d.]+)/i);
const sellMatch = line.match(/SELL.*conf[idence]*[=:]\s*([\d.]+)/i);
if (buyMatch || sellMatch) {
const signal = {
action: buyMatch ? 'BUY' : 'SELL',
confidence: buyMatch ? parseFloat(buyMatch[1]) : parseFloat(sellMatch[1]),
timestamp: Date.now()
};
signalHistory.push(signal);
if (signalHistory.length > 100) {
signalHistory.shift();
}
}
}
div.textContent = line;
cont.appendChild(div);
});
cont.scrollTop = cont.scrollHeight;
updateDominantSignal();
})
.catch(error => {
console.error('Error fetching logs:', error);
});
}
// Visitors
function updateVisitors() {
fetch('/api/visitors')
.then(r => r.json())
.then(v => {
setText('activeVisitors', v.active_now || 0);
const tel = document.getElementById('totalVisitors');
if (tel) tel.textContent = (v.all_time_unique || 0) + ' total';
})
.catch(() => {});
}
// Accuracy Chart
let accuracyChart;
function initChart() {
const ctx = document.getElementById('accuracyChart')?.getContext('2d');
if (!ctx) return;
// Chart.js gradient with earth tones
const gradient = ctx.createLinearGradient(0, 0, 0, 200);
gradient.addColorStop(0, 'rgba(0, 255, 136, 0.25)');
gradient.addColorStop(0.5, 'rgba(64, 192, 160, 0.1)');
gradient.addColorStop(1, 'rgba(0, 255, 136, 0)');
accuracyChart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'AVN Accuracy',
borderColor: '#00ff88',
backgroundColor: gradient,
borderWidth: 2.5,
fill: true,
tension: 0.4,
pointRadius: 0,
pointHoverRadius: 6,
pointHoverBackgroundColor: '#00ff88',
pointHoverBorderColor: '#fff',
pointHoverBorderWidth: 2,
data: []
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index'
},
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: 'rgba(0, 20, 40, 0.95)',
titleColor: '#fff',
titleFont: { size: 12, weight: '600', family: 'Outfit' },
bodyColor: '#00ff88',
bodyFont: { size: 14, weight: '700', family: 'Space Mono' },
borderColor: 'rgba(0, 255, 136, 0.3)',
borderWidth: 1,
padding: 12,
cornerRadius: 8,
displayColors: false,
callbacks: {
title: (items) => `Iteration ${items[0].label}`,
label: (item) => `Accuracy: ${item.raw.toFixed(1)}%`
}
}
},
scales: {
y: {
min: 0,
max: 100,
grid: {
color: 'rgba(255, 255, 255, 0.04)',
drawBorder: false
},
border: { display: false },
ticks: {
color: 'rgba(255, 255, 255, 0.4)',
callback: v => v + '%',
font: { size: 11, family: 'Space Mono' },
padding: 10,
stepSize: 20
}
},
x: {
grid: { display: false },
border: { display: false },
ticks: {
color: 'rgba(255, 255, 255, 0.4)',
maxTicksLimit: window.innerWidth <= 768 ? 5 : 8,
font: { size: 10, family: 'Space Mono' },
padding: 8
}
}
}
}
});
}
// ✅ MIGRATED: Resilient accuracy fetch with fallbacks for HF Spaces
async function updateAccuracy() {
try {
// Try dedicated accuracy endpoints first
let stats = null;
let history = [];
try {
stats = await fetch('/api/accuracy/stats').then(r => r.json());
} catch (e) {
// Fallback: extract from main metrics
try {
const metricsData = await fetch('/api/metrics').then(r => r.json());
if (metricsData.metrics && metricsData.metrics.avn_accuracy !== undefined) {
stats = {
current: metricsData.metrics.avn_accuracy,
average: metricsData.metrics.avn_accuracy,
last_10_avg: metricsData.metrics.avn_accuracy,
trend: 'stable'
};
}
} catch (e2) {
console.warn('Accuracy fallback also failed:', e2);
}
}
if (stats) {
setText('accuracy-current', (stats.current?.toFixed(1) || '—') + (stats.current !== undefined ? '%' : ''));
setText('accuracy-avg', (stats.average?.toFixed(1) || '—') + (stats.average !== undefined ? '%' : ''));
setText('accuracy-last10', (stats.last_10_avg?.toFixed(1) || '—') + (stats.last_10_avg !== undefined ? '%' : ''));
const trend = document.getElementById('accuracy-trend');
if (trend && stats.trend) {
trend.classList.remove('rising', 'stable', 'declining');
if (stats.trend === 'up') {
trend.classList.add('rising');
trend.innerHTML = '<span class="trend-arrow">▲</span><span>RISING</span>';
} else if (stats.trend === 'down') {
trend.classList.add('declining');
trend.innerHTML = '<span class="trend-arrow">▼</span><span>DECLINING</span>';
} else {
trend.classList.add('stable');
trend.innerHTML = '<span class="trend-arrow">―</span><span>STABLE</span>';
}
}
}
// Try to get accuracy history — real data only
try {
history = await fetch('/api/accuracy/history').then(r => r.json());
} catch (e) {
console.warn('Accuracy history endpoint unavailable');
}
// Update chart
if (history.length > 0 && accuracyChart) {
const recent = history.slice(-(window.innerWidth <= 768 ? 30 : 50));
accuracyChart.data.labels = recent.map((_, i) => i + 1);
accuracyChart.data.datasets[0].data = recent.map(d => d.accuracy);
accuracyChart.update('none');
}
} catch(e) {
console.warn('Accuracy fetch failed:', e);
}
}
// ✅ INITIALIZATION — Tuned for HuggingFace Spaces + Redis
updateMetrics();
updateLogs();
updateVisitors();
initChart();
setTimeout(updateAccuracy, 500);
// Intervals — gentler on HF Spaces (slightly slower than local dev)
setInterval(updateMetrics, 5000); // Every 5 seconds
setInterval(updateLogs, 8000); // Every 8 seconds
setInterval(updateVisitors, 30000); // Every 30 seconds
setInterval(updateAccuracy, 15000); // Every 15 seconds
// ✅ Debug info for HF Spaces
console.log('🌌 K1RL QUASAR Full-Featured Dashboard initialized');
console.log('Platform: HuggingFace Spaces (Redis-backed)');
console.log('Signal Tracking: Log-based extraction (replaced Ably WebSocket)');
console.log('Target Agent:', TARGET_AGENT);
console.log('API Endpoints: /api/metrics, /logs/quasar_engine, /api/visitors, /api/accuracy/*');
</script>
</body>
</html>