gridmind / dashboard /static /index.html
ShreeshantXD's picture
Fix dashboard paths for /dashboard reverse proxy
832f069
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GridMind-RL | Real-Time Energy Dashboard</title>
<meta name="description" content="Real-time visualization dashboard for the GridMind-RL Industrial Load-Shaping and Demand-Response RL environment." />
<!-- Chart.js CDN -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
<style>
/* ══════════════════════════════════════════════════════════════════
GridMind-RL — Premium Matte Dashboard Design System
══════════════════════════════════════════════════════════════════ */
:root {
/* Matte dark palette — deep, soft, no harsh contrast */
--bg-base: #0c0e14;
--bg-elevated: #12151e;
--bg-surface: #171b27;
--bg-card: #1a1f2e;
--bg-card-hover: #1f2538;
--bg-inset: #141824;
--bg-overlay: rgba(12, 14, 20, 0.92);
/* Borders — ultra subtle */
--border-subtle: rgba(255, 255, 255, 0.04);
--border-default: rgba(255, 255, 255, 0.06);
--border-hover: rgba(255, 255, 255, 0.10);
--border-accent: rgba(99, 179, 237, 0.20);
/* Text — muted and refined */
--text-primary: #e8ecf4;
--text-secondary: #8a94a8;
--text-tertiary: #5a6478;
--text-muted: #3d4558;
/* Accent palette — muted, sophisticated */
--accent-blue: #5b9cf6;
--accent-green: #4ade80;
--accent-amber: #f5a623;
--accent-red: #f06e6e;
--accent-purple: #a78bfa;
--accent-cyan: #34d4e4;
--accent-orange: #fb923c;
--accent-teal: #2dd4bf;
--accent-rose: #fb7185;
/* Gradients */
--grad-blue: linear-gradient(135deg, #5b9cf6 0%, #818cf8 100%);
--grad-green: linear-gradient(135deg, #4ade80 0%, #34d4e4 100%);
--grad-amber: linear-gradient(135deg, #f5a623 0%, #fb923c 100%);
--grad-red: linear-gradient(135deg, #f06e6e 0%, #fb7185 100%);
--grad-purple: linear-gradient(135deg, #a78bfa 0%, #818cf8 100%);
--grad-hero: linear-gradient(160deg, #0c0e14 0%, #12151e 30%, #171b27 100%);
/* Glass */
--glass-bg: rgba(26, 31, 46, 0.60);
--glass-border: rgba(255, 255, 255, 0.06);
--glass-blur: 20px;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
--shadow-md: 0 4px 12px rgba(0,0,0,0.4);
--shadow-lg: 0 8px 32px rgba(0,0,0,0.5);
--shadow-glow-blue: 0 0 24px rgba(91,156,246,0.12);
--shadow-glow-green: 0 0 24px rgba(74,222,128,0.12);
/* Typography */
--font-sans: 'Inter', -apple-system, system-ui, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
/* Spacing & Radius */
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 20px;
--radius-pill: 9999px;
/* Transitions */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--duration-fast: 150ms;
--duration-base: 250ms;
--duration-slow: 400ms;
}
/* ── Reset ────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
body {
font-family: var(--font-sans);
background: var(--bg-base);
color: var(--text-primary);
min-height: 100vh;
overflow-x: hidden;
line-height: 1.5;
}
/* ── Ambient background ───────────────────────────────────────────── */
body::before {
content: '';
position: fixed;
top: -30%; left: -10%;
width: 60%; height: 60%;
background: radial-gradient(circle, rgba(91,156,246,0.04) 0%, transparent 65%);
pointer-events: none;
z-index: 0;
}
body::after {
content: '';
position: fixed;
bottom: -20%; right: -10%;
width: 50%; height: 50%;
background: radial-gradient(circle, rgba(168,85,247,0.03) 0%, transparent 65%);
pointer-events: none;
z-index: 0;
}
/* Subtle dot pattern */
.bg-pattern {
position: fixed;
inset: 0;
background-image: radial-gradient(rgba(255,255,255,0.02) 1px, transparent 1px);
background-size: 24px 24px;
pointer-events: none;
z-index: 0;
}
/* ══════════════════════════════════════════════════════════════════
HEADER — Glass morphism nav bar
══════════════════════════════════════════════════════════════════ */
header {
position: sticky;
top: 0;
z-index: 100;
background: var(--bg-overlay);
backdrop-filter: blur(24px) saturate(1.2);
-webkit-backdrop-filter: blur(24px) saturate(1.2);
border-bottom: 1px solid var(--border-subtle);
padding: 0 1.75rem;
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
}
.logo {
display: flex;
align-items: center;
gap: 12px;
}
.logo-icon {
width: 36px; height: 36px;
background: var(--grad-blue);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
box-shadow: 0 2px 12px rgba(91,156,246,0.25);
position: relative;
overflow: hidden;
}
.logo-icon::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(255,255,255,0.15) 0%, transparent 50%);
}
.logo-icon svg { width: 18px; height: 18px; color: white; position: relative; z-index: 1; }
.logo-text {
font-size: 1.05rem;
font-weight: 700;
letter-spacing: -0.5px;
color: var(--text-primary);
}
.logo-text span {
background: var(--grad-blue);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.logo-tag {
font-size: 0.6rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--text-tertiary);
padding: 2px 8px;
border: 1px solid var(--border-default);
border-radius: var(--radius-pill);
margin-left: 4px;
}
.header-right {
display: flex;
align-items: center;
gap: 1rem;
}
.task-badge {
padding: 5px 14px;
border-radius: var(--radius-pill);
font-size: 0.72rem;
font-weight: 600;
background: rgba(91,156,246,0.10);
border: 1px solid rgba(91,156,246,0.15);
color: var(--accent-blue);
letter-spacing: 0.2px;
}
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 14px;
border-radius: var(--radius-pill);
background: rgba(74,222,128,0.08);
border: 1px solid rgba(74,222,128,0.12);
}
.status-dot {
width: 7px; height: 7px;
border-radius: 50%;
background: var(--accent-green);
box-shadow: 0 0 8px rgba(74,222,128,0.5);
animation: statusPulse 2.5s ease-in-out infinite;
}
@keyframes statusPulse {
0%, 100% { opacity: 1; box-shadow: 0 0 8px rgba(74,222,128,0.5); }
50% { opacity: 0.5; box-shadow: 0 0 16px rgba(74,222,128,0.3); }
}
.status-label {
font-size: 0.72rem;
font-weight: 600;
color: var(--accent-green);
letter-spacing: 0.5px;
text-transform: uppercase;
}
.header-meta {
font-family: var(--font-mono);
font-size: 0.72rem;
color: var(--text-tertiary);
letter-spacing: 0.5px;
}
/* ══════════════════════════════════════════════════════════════════
KPI BAR — Elevated metrics strip
══════════════════════════════════════════════════════════════════ */
.kpi-strip {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: repeat(7, 1fr);
background: var(--bg-elevated);
border-bottom: 1px solid var(--border-subtle);
}
@media (max-width: 1100px) {
.kpi-strip { grid-template-columns: repeat(4, 1fr); }
}
@media (max-width: 640px) {
.kpi-strip { grid-template-columns: repeat(2, 1fr); }
}
.kpi {
padding: 1rem 1.25rem;
border-right: 1px solid var(--border-subtle);
transition: background var(--duration-base) var(--ease-out);
cursor: default;
position: relative;
}
.kpi:last-child { border-right: none; }
.kpi:hover { background: var(--bg-surface); }
.kpi::after {
content: '';
position: absolute;
bottom: 0; left: 50%;
width: 0; height: 2px;
background: var(--grad-blue);
transition: all var(--duration-base) var(--ease-out);
transform: translateX(-50%);
border-radius: 1px;
}
.kpi:hover::after { width: 60%; }
.kpi-label {
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text-tertiary);
margin-bottom: 4px;
}
.kpi-value {
font-family: var(--font-mono);
font-size: 1.35rem;
font-weight: 600;
color: var(--text-primary);
transition: color var(--duration-base);
line-height: 1.2;
}
.kpi-value.good { color: var(--accent-green); }
.kpi-value.warn { color: var(--accent-amber); }
.kpi-value.bad { color: var(--accent-red); }
.kpi-unit {
font-size: 0.65rem;
color: var(--text-muted);
font-family: var(--font-mono);
margin-top: 2px;
}
/* ══════════════════════════════════════════════════════════════════
MAIN LAYOUT — 12-column grid
══════════════════════════════════════════════════════════════════ */
main {
position: relative;
z-index: 1;
max-width: 1560px;
margin: 0 auto;
padding: 1.25rem;
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 1rem;
}
/* ══════════════════════════════════════════════════════════════════
CARD — Matte glass panels
══════════════════════════════════════════════════════════════════ */
.card {
background: var(--bg-card);
border: 1px solid var(--border-default);
border-radius: var(--radius-lg);
padding: 1.25rem;
transition: all var(--duration-base) var(--ease-out);
position: relative;
overflow: hidden;
}
.card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 1px;
background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.06) 50%, transparent 100%);
}
.card:hover {
border-color: var(--border-hover);
background: var(--bg-card-hover);
box-shadow: var(--shadow-md);
transform: translateY(-1px);
}
.card.alert-active {
border-color: rgba(240,110,110,0.35);
box-shadow: 0 0 30px rgba(240,110,110,0.08);
animation: alertGlow 2s ease-in-out infinite;
}
@keyframes alertGlow {
0%, 100% { box-shadow: 0 0 20px rgba(240,110,110,0.06); }
50% { box-shadow: 0 0 40px rgba(240,110,110,0.12); }
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
.card-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.8px;
color: var(--text-secondary);
}
.card-title .icon-wrap {
width: 28px; height: 28px;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.card-title .icon-wrap svg { width: 14px; height: 14px; }
.card-title .icon-wrap.blue { background: rgba(91,156,246,0.12); color: var(--accent-blue); }
.card-title .icon-wrap.green { background: rgba(74,222,128,0.12); color: var(--accent-green); }
.card-title .icon-wrap.amber { background: rgba(245,166,35,0.12); color: var(--accent-amber); }
.card-title .icon-wrap.red { background: rgba(240,110,110,0.12); color: var(--accent-red); }
.card-title .icon-wrap.purple { background: rgba(167,139,250,0.12); color: var(--accent-purple); }
.card-title .icon-wrap.cyan { background: rgba(52,212,228,0.12); color: var(--accent-cyan); }
.card-title .icon-wrap.orange { background: rgba(251,146,60,0.12); color: var(--accent-orange); }
.card-title .icon-wrap.teal { background: rgba(45,212,191,0.12); color: var(--accent-teal); }
.card-badge {
font-size: 0.62rem;
font-weight: 600;
padding: 3px 8px;
border-radius: var(--radius-pill);
background: rgba(255,255,255,0.04);
border: 1px solid var(--border-subtle);
color: var(--text-tertiary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Column spans */
.col-12 { grid-column: span 12; }
.col-8 { grid-column: span 8; }
.col-6 { grid-column: span 6; }
.col-4 { grid-column: span 4; }
.col-3 { grid-column: span 3; }
@media (max-width: 1200px) {
.col-8, .col-6 { grid-column: span 12; }
.col-4, .col-3 { grid-column: span 6; }
}
@media (max-width: 768px) {
.col-3, .col-4 { grid-column: span 12; }
main { padding: 0.75rem; gap: 0.75rem; }
}
/* ── Chart containers ─────────────────────────────────────────────── */
.chart-wrap { position: relative; height: 200px; }
.chart-wrap.tall { height: 260px; }
.chart-wrap.short { height: 150px; }
/* ══════════════════════════════════════════════════════════════════
THERMAL STORAGE — Premium gauge
══════════════════════════════════════════════════════════════════ */
.storage-display {
text-align: center;
padding: 0.5rem 0;
}
.storage-value {
font-family: var(--font-mono);
font-size: 2.5rem;
font-weight: 700;
line-height: 1;
background: var(--grad-green);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.storage-value span {
font-size: 1.2rem;
-webkit-text-fill-color: var(--text-tertiary);
}
.storage-bar-track {
height: 8px;
background: var(--bg-inset);
border-radius: var(--radius-pill);
overflow: hidden;
margin: 0.75rem 0;
position: relative;
}
.storage-bar-fill {
height: 100%;
border-radius: var(--radius-pill);
background: var(--grad-green);
transition: width 0.6s var(--ease-out);
position: relative;
}
.storage-bar-fill::after {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
animation: barShimmer 2.5s ease-in-out infinite;
}
@keyframes barShimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(200%); }
}
/* ══════════════════════════════════════════════════════════════════
STRESS DISPLAY
══════════════════════════════════════════════════════════════════ */
.stress-display {
text-align: center;
padding: 0.25rem 0;
}
.stress-value {
font-family: var(--font-mono);
font-size: 2.8rem;
font-weight: 700;
line-height: 1;
color: var(--text-primary);
transition: color var(--duration-base);
}
.stress-value.low { color: var(--accent-green); }
.stress-value.mid { color: var(--accent-amber); }
.stress-value.high { color: var(--accent-red); }
.stress-sub {
font-size: 0.72rem;
color: var(--text-tertiary);
margin-top: 4px;
}
.stress-meter {
display: flex;
align-items: flex-end;
gap: 3px;
height: 36px;
margin: 0.75rem 0;
padding: 0 0.5rem;
}
.stress-bar {
flex: 1;
background: rgba(255,255,255,0.04);
border-radius: 2px 2px 0 0;
transition: height var(--duration-slow) var(--ease-out),
background var(--duration-base);
min-height: 3px;
}
/* ══════════════════════════════════════════════════════════════════
BATCH GANTT
══════════════════════════════════════════════════════════════════ */
.gantt-container {
display: flex;
flex-direction: column;
gap: 8px;
}
.gantt-row {
display: flex;
align-items: center;
gap: 10px;
font-size: 0.75rem;
}
.gantt-label {
width: 36px;
font-family: var(--font-mono);
font-weight: 600;
color: var(--text-tertiary);
flex-shrink: 0;
font-size: 0.7rem;
}
.gantt-track {
flex: 1;
height: 22px;
background: var(--bg-inset);
border-radius: 6px;
position: relative;
overflow: hidden;
}
.gantt-block {
position: absolute;
top: 3px; bottom: 3px;
border-radius: 4px;
transition: width var(--duration-base), left var(--duration-base);
}
.gantt-block.scheduled { background: var(--grad-blue); opacity: 0.85; }
.gantt-block.completed { background: var(--grad-green); opacity: 0.7; }
.gantt-block.missed { background: var(--grad-red); opacity: 0.75; }
.gantt-deadline {
position: absolute;
top: 2px; bottom: 2px;
width: 2px;
background: var(--accent-amber);
border-radius: 1px;
box-shadow: 0 0 6px rgba(245,166,35,0.3);
}
.gantt-status {
width: 56px;
text-align: right;
flex-shrink: 0;
}
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: var(--radius-pill);
font-size: 0.62rem;
font-weight: 600;
letter-spacing: 0.3px;
text-transform: uppercase;
}
.badge.ok { background: rgba(74,222,128,0.12); color: var(--accent-green); }
.badge.pending { background: rgba(91,156,246,0.12); color: var(--accent-blue); }
.badge.missed { background: rgba(240,110,110,0.12); color: var(--accent-red); }
.badge.running { background: rgba(167,139,250,0.12); color: var(--accent-purple); }
.gantt-empty {
text-align: center;
padding: 2rem 0;
color: var(--text-muted);
font-size: 0.8rem;
}
/* ══════════════════════════════════════════════════════════════════
REWARD ROWS
══════════════════════════════════════════════════════════════════ */
.reward-row {
display: flex;
align-items: center;
gap: 10px;
padding: 4px 0;
}
.reward-label {
width: 90px;
font-size: 0.7rem;
color: var(--text-tertiary);
flex-shrink: 0;
}
.reward-bar-track {
flex: 1;
height: 6px;
background: var(--bg-inset);
border-radius: var(--radius-pill);
overflow: hidden;
}
.reward-bar {
height: 100%;
border-radius: var(--radius-pill);
transition: width 0.5s var(--ease-out);
}
.reward-val {
width: 52px;
text-align: right;
font-family: var(--font-mono);
font-size: 0.68rem;
font-weight: 500;
}
/* ══════════════════════════════════════════════════════════════════
CONTROLS — Premium buttons
══════════════════════════════════════════════════════════════════ */
.controls-grid {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.btn {
padding: 9px 18px;
border-radius: var(--radius-md);
border: 1px solid var(--border-default);
background: var(--bg-surface);
color: var(--text-secondary);
font-size: 0.78rem;
font-weight: 600;
font-family: var(--font-sans);
cursor: pointer;
transition: all var(--duration-base) var(--ease-out);
display: inline-flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
.btn svg { width: 14px; height: 14px; }
.btn:hover {
background: var(--bg-card-hover);
border-color: var(--border-hover);
color: var(--text-primary);
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
.btn:active { transform: translateY(0); }
.btn.primary {
background: var(--grad-blue);
border: none;
color: white;
box-shadow: 0 2px 12px rgba(91,156,246,0.20);
}
.btn.primary:hover {
box-shadow: 0 4px 20px rgba(91,156,246,0.30);
transform: translateY(-2px);
}
.btn.success {
background: rgba(74,222,128,0.12);
border-color: rgba(74,222,128,0.20);
color: var(--accent-green);
}
.btn.success:hover {
background: rgba(74,222,128,0.18);
box-shadow: 0 2px 12px rgba(74,222,128,0.15);
}
.btn.success.active {
background: rgba(245,166,35,0.12);
border-color: rgba(245,166,35,0.20);
color: var(--accent-amber);
}
select.form-select {
padding: 9px 14px;
background: var(--bg-surface);
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
color: var(--text-secondary);
font-size: 0.78rem;
font-weight: 500;
font-family: var(--font-sans);
cursor: pointer;
transition: all var(--duration-base) var(--ease-out);
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%235a6478' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
padding-right: 30px;
}
select.form-select:focus {
outline: none;
border-color: var(--accent-blue);
box-shadow: 0 0 0 3px rgba(91,156,246,0.10);
}
select.form-select:hover {
border-color: var(--border-hover);
background-color: var(--bg-card);
}
.grade-result {
font-family: var(--font-mono);
font-size: 0.82rem;
font-weight: 600;
padding: 5px 14px;
border-radius: var(--radius-pill);
background: rgba(74,222,128,0.08);
color: var(--accent-green);
display: none;
}
.grade-result.show { display: inline-flex; }
/* ══════════════════════════════════════════════════════════════════
CONNECTION BANNER
══════════════════════════════════════════════════════════════════ */
#conn-banner {
display: none;
position: fixed;
top: 60px; left: 0; right: 0;
z-index: 200;
background: rgba(240,110,110,0.08);
backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(240,110,110,0.15);
text-align: center;
padding: 10px;
font-size: 0.8rem;
font-weight: 500;
color: var(--accent-red);
}
#conn-banner.show { display: block; }
/* ══════════════════════════════════════════════════════════════════
FOOTER
══════════════════════════════════════════════════════════════════ */
footer {
position: relative;
z-index: 1;
text-align: center;
padding: 1.5rem;
color: var(--text-muted);
font-size: 0.7rem;
border-top: 1px solid var(--border-subtle);
background: var(--bg-elevated);
}
footer a {
color: var(--text-tertiary);
text-decoration: none;
transition: color var(--duration-fast);
}
footer a:hover { color: var(--accent-blue); }
/* ══════════════════════════════════════════════════════════════════
ANIMATIONS
══════════════════════════════════════════════════════════════════ */
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
.card {
animation: fadeInUp 0.5s var(--ease-out) both;
}
.card:nth-child(1) { animation-delay: 0.05s; }
.card:nth-child(2) { animation-delay: 0.10s; }
.card:nth-child(3) { animation-delay: 0.15s; }
.card:nth-child(4) { animation-delay: 0.20s; }
.card:nth-child(5) { animation-delay: 0.25s; }
.card:nth-child(6) { animation-delay: 0.30s; }
.card:nth-child(7) { animation-delay: 0.35s; }
.card:nth-child(8) { animation-delay: 0.40s; }
.card:nth-child(9) { animation-delay: 0.45s; }
/* Scrollbar styling */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.08);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255,255,255,0.12);
}
</style>
</head>
<body>
<div class="bg-pattern"></div>
<!-- Connection banner -->
<div id="conn-banner">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: -2px; margin-right: 6px;"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
Environment server unreachable — retrying connection...
</div>
<!-- ═══ HEADER ═══ -->
<header>
<div class="logo">
<div class="logo-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
</div>
<div class="logo-text">Grid<span>Mind</span>-RL</div>
<div class="logo-tag">v1.0</div>
</div>
<div class="header-right">
<span id="task-badge" class="task-badge">Task 1 — Cost Minimization</span>
<div class="status-indicator">
<div class="status-dot" id="status-dot"></div>
<span class="status-label" id="status-label">Live</span>
</div>
<span class="header-meta" id="ep-step">ep:— step:—</span>
</div>
</header>
<!-- ═══ KPI STRIP ═══ -->
<div class="kpi-strip">
<div class="kpi">
<div class="kpi-label">Current Price</div>
<div class="kpi-value" id="kpi-price"></div>
<div class="kpi-unit">$/kWh</div>
</div>
<div class="kpi">
<div class="kpi-label">Indoor Temp</div>
<div class="kpi-value" id="kpi-temp"></div>
<div class="kpi-unit">°C · target 21°C</div>
</div>
<div class="kpi">
<div class="kpi-label">Grid Stress</div>
<div class="kpi-value" id="kpi-stress"></div>
<div class="kpi-unit">0 normal · 1 critical</div>
</div>
<div class="kpi">
<div class="kpi-label">Cumulative Cost</div>
<div class="kpi-value" id="kpi-cost"></div>
<div class="kpi-unit">vs baseline: <span id="kpi-baseline"></span></div>
</div>
<div class="kpi">
<div class="kpi-label">Carbon Intensity</div>
<div class="kpi-value" id="kpi-carbon"></div>
<div class="kpi-unit">gCO₂/kWh</div>
</div>
<div class="kpi">
<div class="kpi-label">Process Demand</div>
<div class="kpi-value" id="kpi-demand"></div>
<div class="kpi-unit">kW</div>
</div>
<div class="kpi">
<div class="kpi-label">Thermal Storage</div>
<div class="kpi-value" id="kpi-storage"></div>
<div class="kpi-unit">% capacity</div>
</div>
</div>
<!-- ═══ MAIN CONTENT ═══ -->
<main>
<!-- Row 1: Price Curve + Grid Stress -->
<div class="card col-8">
<div class="card-header">
<div class="card-title">
<div class="icon-wrap amber"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg></div>
Electricity Price Curve
</div>
<span class="card-badge">24H</span>
</div>
<div class="chart-wrap">
<canvas id="chart-price"></canvas>
</div>
</div>
<div class="card col-4" id="card-stress">
<div class="card-header">
<div class="card-title">
<div class="icon-wrap red"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></div>
Grid Stress Signal
</div>
<span class="card-badge">REAL-TIME</span>
</div>
<div class="stress-display">
<div class="stress-value" id="stress-big">0.000</div>
<div class="stress-sub">Demand-response urgency index</div>
</div>
<div class="stress-meter" id="stress-meter"></div>
<div class="chart-wrap short">
<canvas id="chart-stress"></canvas>
</div>
</div>
<!-- Row 2: Temperature + Storage + HVAC -->
<div class="card col-6">
<div class="card-header">
<div class="card-title">
<div class="icon-wrap cyan"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 14.76V3.5a2.5 2.5 0 0 0-5 0v11.26a4.5 4.5 0 1 0 5 0z"/></svg></div>
Temperature Timeline
</div>
<span class="card-badge">INDOOR</span>
</div>
<div class="chart-wrap tall">
<canvas id="chart-temp"></canvas>
</div>
</div>
<div class="card col-3">
<div class="card-header">
<div class="card-title">
<div class="icon-wrap teal"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="6" width="18" height="12" rx="2" ry="2"/><line x1="23" y1="13" x2="23" y2="11"/></svg></div>
Thermal Storage
</div>
</div>
<div class="storage-display">
<div class="storage-value"><span id="storage-pct"></span><span>%</span></div>
</div>
<div class="storage-bar-track">
<div class="storage-bar-fill" id="storage-fill" style="width: 0%"></div>
</div>
<div class="chart-wrap short">
<canvas id="chart-storage"></canvas>
</div>
</div>
<div class="card col-3">
<div class="card-header">
<div class="card-title">
<div class="icon-wrap blue"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg></div>
HVAC + Load Shed
</div>
</div>
<div class="chart-wrap tall">
<canvas id="chart-hvac"></canvas>
</div>
</div>
<!-- Row 3: Cost vs Baseline + Reward Breakdown -->
<div class="card col-8">
<div class="card-header">
<div class="card-title">
<div class="icon-wrap green"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg></div>
Cumulative Cost vs Baseline
</div>
<span class="card-badge">COMPARISON</span>
</div>
<div class="chart-wrap tall">
<canvas id="chart-cost"></canvas>
</div>
</div>
<div class="card col-4">
<div class="card-header">
<div class="card-title">
<div class="icon-wrap purple"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="7"/><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"/></svg></div>
Reward Breakdown
</div>
<span class="card-badge">STEP</span>
</div>
<div id="reward-rows" style="margin-top: 0.25rem"></div>
<div style="margin-top: 0.75rem">
<div class="chart-wrap short">
<canvas id="chart-reward"></canvas>
</div>
</div>
</div>
<!-- Row 4: Batch Gantt + Carbon -->
<div class="card col-6">
<div class="card-header">
<div class="card-title">
<div class="icon-wrap orange"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></div>
Batch Job Timeline
</div>
<span class="card-badge">SCHEDULER</span>
</div>
<div class="gantt-container" id="gantt-wrap">
<div class="gantt-empty">No batch jobs queued</div>
</div>
</div>
<div class="card col-6">
<div class="card-header">
<div class="card-title">
<div class="icon-wrap green"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></div>
Carbon Intensity Curve
</div>
<span class="card-badge">24H</span>
</div>
<div class="chart-wrap">
<canvas id="chart-carbon"></canvas>
</div>
</div>
<!-- Row 5: Controls -->
<div class="card col-12">
<div class="card-header">
<div class="card-title">
<div class="icon-wrap purple"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg></div>
Episode Controls
</div>
</div>
<div class="controls-grid">
<select id="task-select" class="form-select" onchange="onTaskChange()">
<option value="1">Task 1 — Cost Minimization (Easy)</option>
<option value="2">Task 2 — Temperature Management (Medium)</option>
<option value="3">Task 3 — Full Demand Response (Hard)</option>
</select>
<select id="building-select" class="form-select" onchange="onBuildingChange()">
<option value="0">Building 1 (Primary)</option>
<option value="1">Building 2</option>
<option value="2">Building 3</option>
</select>
<button id="btn-reset" class="btn primary" onclick="doReset()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
New Episode
</button>
<button id="btn-live" class="btn success" onclick="toggleLiveSim()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>
Start Live Simulation
</button>
<button class="btn" onclick="doGrade()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
Grade Episode
</button>
<button class="btn" onclick="window.open('/dashboard/api/replay')">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Export Replay
</button>
<span id="grade-result" class="grade-result"></span>
</div>
</div>
</main>
<footer>
GridMind-RL &nbsp;·&nbsp; OpenEnv-compliant RL environment for industrial demand response &nbsp;·&nbsp;
<a href="/dashboard/api/health" target="_blank">API Health</a> &nbsp;·&nbsp;
<a href="/dashboard/api/metrics" target="_blank">Metrics</a>
</footer>
<script src="/dashboard/static/dashboard.js"></script>
</body>
</html>