| <!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." /> |
| |
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script> |
| |
| <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" /> |
| |
| <script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script> |
| <style> |
| |
| |
| |
| :root { |
| |
| --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); |
| |
| |
| --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-primary: #e8ecf4; |
| --text-secondary: #8a94a8; |
| --text-tertiary: #5a6478; |
| --text-muted: #3d4558; |
| |
| |
| --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; |
| |
| |
| --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-bg: rgba(26, 31, 46, 0.60); |
| --glass-border: rgba(255, 255, 255, 0.06); |
| --glass-blur: 20px; |
| |
| |
| --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); |
| |
| |
| --font-sans: 'Inter', -apple-system, system-ui, sans-serif; |
| --font-mono: 'JetBrains Mono', 'Fira Code', monospace; |
| |
| |
| --radius-sm: 8px; |
| --radius-md: 12px; |
| --radius-lg: 16px; |
| --radius-xl: 20px; |
| --radius-pill: 9999px; |
| |
| |
| --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; |
| } |
| |
| |
| *, *::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; |
| } |
| |
| |
| 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; |
| } |
| |
| |
| .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 { |
| 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-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 { |
| 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 { |
| 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; |
| } |
| |
| |
| .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-wrap { position: relative; height: 200px; } |
| .chart-wrap.tall { height: 260px; } |
| .chart-wrap.short { height: 150px; } |
| |
| |
| |
| |
| .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 { |
| 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; |
| } |
| |
| |
| |
| |
| .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-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-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; } |
| |
| |
| |
| |
| #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 { |
| 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); } |
| |
| |
| |
| |
| @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; } |
| |
| |
| ::-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> |
|
|
| |
| <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> |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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 · OpenEnv-compliant RL environment for industrial demand response · |
| <a href="/dashboard/api/health" target="_blank">API Health</a> · |
| <a href="/dashboard/api/metrics" target="_blank">Metrics</a> |
| </footer> |
|
|
| <script src="/dashboard/static/dashboard.js"></script> |
| </body> |
| </html> |
|
|