Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>DC-Ops | Datacenter Operations Console</title> | |
| <style> | |
| *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} | |
| :root{ | |
| --bg:#0a0e17;--bg-card:#111827;--bg-card-hover:#1a2332; | |
| --border:#1e2d3d;--border-active:#3b82f6; | |
| --text:#e2e8f0;--text-dim:#94a3b8;--text-muted:#64748b; | |
| --accent:#3b82f6;--accent-hover:#2563eb; | |
| --green:#22c55e;--green-dim:#166534; | |
| --red:#ef4444;--red-dim:#991b1b; | |
| --yellow:#eab308;--yellow-dim:#854d0e; | |
| --orange:#f97316; | |
| --cyan:#06b6d4; | |
| --terminal-bg:#0d1117; | |
| --font-mono:'JetBrains Mono','Fira Code','SF Mono','Cascadia Code',Consolas,monospace; | |
| --font-sans:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; | |
| --radius:8px;--radius-lg:12px; | |
| } | |
| html{font-size:14px} | |
| body{background:var(--bg);color:var(--text);font-family:var(--font-sans);min-height:100vh;overflow-x:hidden} | |
| /* Layout */ | |
| .app{display:grid;grid-template-rows:auto 1fr;height:100vh} | |
| .header{background:var(--bg-card);border-bottom:1px solid var(--border);padding:0.75rem 1.5rem;display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap} | |
| .header-left{display:flex;align-items:center;gap:0.75rem} | |
| .logo{font-size:1.25rem;font-weight:700;letter-spacing:-0.02em} | |
| .logo span{color:var(--accent)} | |
| .status-badge{display:inline-flex;align-items:center;gap:0.375rem;padding:0.25rem 0.75rem;border-radius:999px;font-size:0.75rem;font-weight:500} | |
| .status-badge.connected{background:var(--green-dim);color:var(--green)} | |
| .status-badge.disconnected{background:var(--red-dim);color:var(--red)} | |
| .status-badge.loading{background:var(--yellow-dim);color:var(--yellow)} | |
| .status-dot{width:6px;height:6px;border-radius:50%;background:currentColor} | |
| .status-badge.connected .status-dot{animation:pulse 2s infinite} | |
| @keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}} | |
| .main{display:grid;grid-template-columns:280px 1fr 300px;gap:0;overflow:hidden} | |
| /* Sidebar - Scenario Browser */ | |
| .sidebar{background:var(--bg-card);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden} | |
| .sidebar-header{padding:0.875rem 1rem;border-bottom:1px solid var(--border);font-weight:600;font-size:0.8rem;text-transform:uppercase;letter-spacing:0.05em;color:var(--text-dim)} | |
| .scenario-list{flex:1;overflow-y:auto;padding:0.5rem} | |
| .scenario-group{margin-bottom:0.75rem} | |
| .scenario-group-title{padding:0.375rem 0.75rem;font-size:0.65rem;font-weight:600;text-transform:uppercase;letter-spacing:0.1em;color:var(--text-muted)} | |
| .scenario-card{padding:0.625rem 0.75rem;margin:0.25rem 0;border-radius:var(--radius);cursor:pointer;transition:all 0.15s;border:1px solid transparent} | |
| .scenario-card:hover{background:var(--bg-card-hover);border-color:var(--border)} | |
| .scenario-card.active{background:rgba(59,130,246,0.08);border-color:var(--accent)} | |
| .scenario-card .sc-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:0.25rem} | |
| .scenario-card .sc-id{font-weight:700;font-family:var(--font-mono);font-size:0.8rem;color:var(--accent)} | |
| .scenario-card .sc-diff{font-size:0.6rem;font-weight:700;padding:0.1rem 0.5rem;border-radius:999px;text-transform:uppercase;letter-spacing:0.05em} | |
| .sc-diff.easy{background:var(--green-dim);color:var(--green)} | |
| .sc-diff.medium{background:var(--yellow-dim);color:var(--yellow)} | |
| .sc-diff.hard{background:var(--red-dim);color:var(--red)} | |
| .scenario-card .sc-name{font-size:0.78rem;font-weight:500;color:var(--text);margin-bottom:0.125rem} | |
| .scenario-card .sc-desc{font-size:0.68rem;color:var(--text-muted);line-height:1.4} | |
| .sidebar-actions{padding:0.75rem;border-top:1px solid var(--border);display:flex;flex-direction:column;gap:0.5rem} | |
| .config-select{width:100%;padding:0.5rem 0.625rem;background:var(--terminal-bg);border:1px solid var(--border);border-radius:var(--radius);color:var(--text);font-size:0.78rem;font-family:var(--font-sans);appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2394a3b8' d='M3 5l3 3 3-3'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 0.5rem center;padding-right:1.5rem} | |
| .config-select:focus{outline:none;border-color:var(--accent)} | |
| .btn{padding:0.625rem 1rem;border-radius:var(--radius);border:none;cursor:pointer;font-weight:600;font-size:0.8rem;transition:all 0.15s;text-align:center;font-family:var(--font-sans);display:flex;align-items:center;justify-content:center;gap:0.5rem} | |
| .btn-primary{background:var(--accent);color:white} | |
| .btn-primary:hover:not(:disabled){background:var(--accent-hover)} | |
| .btn-primary:disabled{opacity:0.5;cursor:not-allowed} | |
| .btn-danger{background:var(--red-dim);color:var(--red);border:1px solid rgba(239,68,68,0.2)} | |
| .btn-danger:hover{background:var(--red);color:white} | |
| .btn-outline{background:transparent;color:var(--text-dim);border:1px solid var(--border)} | |
| .btn-outline:hover{border-color:var(--text-dim);color:var(--text)} | |
| /* Center Panel - Dashboard */ | |
| .center{display:flex;flex-direction:column;overflow:hidden;min-width:0} | |
| .dashboard-container{flex:1;overflow-y:auto;padding:1rem} | |
| .dashboard-box{background:var(--terminal-bg);border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden} | |
| .dashboard-title-bar{display:flex;align-items:center;justify-content:space-between;padding:0.5rem 1rem;background:rgba(255,255,255,0.03);border-bottom:1px solid var(--border)} | |
| .dashboard-title-bar .dots{display:flex;gap:6px} | |
| .dashboard-title-bar .dots span{width:10px;height:10px;border-radius:50%} | |
| .dashboard-title-bar .dots span:nth-child(1){background:#ef4444} | |
| .dashboard-title-bar .dots span:nth-child(2){background:#eab308} | |
| .dashboard-title-bar .dots span:nth-child(3){background:#22c55e} | |
| .dashboard-title-bar .title{font-size:0.72rem;color:var(--text-muted);font-family:var(--font-mono)} | |
| .dashboard-output{padding:1rem;font-family:var(--font-mono);font-size:0.75rem;line-height:1.2;white-space:pre;overflow-x:auto;min-height:200px;color:var(--green)} | |
| /* Action result box */ | |
| .action-result{margin-top:0.75rem;background:var(--terminal-bg);border:1px solid var(--border);border-radius:var(--radius);padding:0.625rem 0.875rem;font-family:var(--font-mono);font-size:0.75rem;max-height:100px;overflow-y:auto;transition:all 0.2s} | |
| .action-result.error{color:var(--red);border-color:rgba(239,68,68,0.3)} | |
| .action-result.success{color:var(--cyan);border-color:rgba(6,182,212,0.3)} | |
| /* Welcome screen */ | |
| .welcome{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:3rem 2rem;min-height:300px;white-space:normal} | |
| .welcome h2{font-size:1.4rem;color:var(--text);margin-bottom:0.75rem;font-weight:700} | |
| .welcome p{max-width:380px;line-height:1.7;font-size:0.85rem;color:var(--text-dim)} | |
| .welcome .hint{margin-top:1.5rem;display:flex;align-items:center;gap:0.5rem;color:var(--accent);font-size:0.8rem;opacity:0.7} | |
| .welcome .hint svg{width:20px;height:20px} | |
| /* Command Bar */ | |
| .command-bar{padding:0.75rem 1rem;border-top:1px solid var(--border);background:var(--bg-card)} | |
| .command-input-group{display:flex;gap:0.5rem} | |
| .command-input{flex:1;padding:0.625rem 0.875rem;background:var(--terminal-bg);border:1px solid var(--border);border-radius:var(--radius);color:var(--text);font-family:var(--font-mono);font-size:0.8rem;min-width:0} | |
| .command-input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(59,130,246,0.12)} | |
| .command-input::placeholder{color:var(--text-muted)} | |
| .command-input:disabled{opacity:0.4} | |
| .quick-actions{display:flex;gap:0.375rem;margin-top:0.5rem;flex-wrap:wrap} | |
| .quick-btn{padding:0.2rem 0.5rem;background:rgba(255,255,255,0.03);border:1px solid var(--border);border-radius:999px;color:var(--text-dim);font-size:0.68rem;cursor:pointer;font-family:var(--font-mono);transition:all 0.15s;white-space:nowrap} | |
| .quick-btn:hover:not(:disabled){border-color:var(--accent);color:var(--accent)} | |
| .quick-btn:disabled{opacity:0.3;cursor:not-allowed} | |
| /* Right Panel - Metrics */ | |
| .right-panel{background:var(--bg-card);border-left:1px solid var(--border);display:flex;flex-direction:column;overflow-y:auto} | |
| .panel-section{padding:0.875rem;border-bottom:1px solid var(--border)} | |
| .panel-section-title{font-size:0.65rem;font-weight:600;text-transform:uppercase;letter-spacing:0.1em;color:var(--text-muted);margin-bottom:0.625rem} | |
| /* Metrics grid */ | |
| .metrics-grid{display:grid;grid-template-columns:1fr 1fr;gap:0.375rem} | |
| .metric{background:var(--terminal-bg);padding:0.5rem 0.625rem;border-radius:var(--radius);border:1px solid var(--border)} | |
| .metric-label{font-size:0.6rem;color:var(--text-muted);margin-bottom:0.2rem;text-transform:uppercase;letter-spacing:0.05em} | |
| .metric-value{font-size:1rem;font-weight:700;font-family:var(--font-mono)} | |
| .metric-value.good{color:var(--green)} | |
| .metric-value.warn{color:var(--yellow)} | |
| .metric-value.danger{color:var(--red)} | |
| .metric-value.neutral{color:var(--text)} | |
| /* Episode info */ | |
| .episode-info{display:flex;flex-direction:column;gap:0.375rem} | |
| .episode-row{display:flex;justify-content:space-between;align-items:center;font-size:0.78rem} | |
| .episode-row .label{color:var(--text-muted)} | |
| .episode-row .value{font-family:var(--font-mono);font-weight:600} | |
| /* Progress bar */ | |
| .progress-bar{height:5px;background:var(--terminal-bg);border-radius:999px;overflow:hidden;border:1px solid var(--border)} | |
| .progress-fill{height:100%;border-radius:999px;transition:width 0.3s;background:var(--accent)} | |
| .progress-fill.low{background:var(--green)} | |
| .progress-fill.mid{background:var(--yellow)} | |
| .progress-fill.high{background:var(--red)} | |
| /* Power status */ | |
| .power-row{display:flex;justify-content:space-between;align-items:center;padding:0.375rem 0.5rem;background:var(--terminal-bg);border-radius:4px;font-size:0.72rem;font-family:var(--font-mono);border:1px solid var(--border);margin-bottom:0.375rem} | |
| .power-row .pw-label{color:var(--text-dim)} | |
| .power-row .pw-val{font-weight:600} | |
| .power-row .pw-val.ok{color:var(--green)} | |
| .power-row .pw-val.warn{color:var(--yellow)} | |
| .power-row .pw-val.bad{color:var(--red)} | |
| /* Reward history */ | |
| .reward-history{display:flex;flex-direction:column;gap:0.25rem;max-height:180px;overflow-y:auto} | |
| .reward-entry{display:flex;justify-content:space-between;align-items:center;padding:0.3rem 0.5rem;background:var(--terminal-bg);border-radius:4px;font-size:0.7rem;font-family:var(--font-mono)} | |
| .reward-entry .step{color:var(--text-muted);width:24px;flex-shrink:0} | |
| .reward-entry .cmd{color:var(--text-dim);flex:1;margin:0 0.5rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} | |
| .reward-entry .rew{font-weight:700;flex-shrink:0} | |
| .reward-entry .rew.pos{color:var(--green)} | |
| .reward-entry .rew.neg{color:var(--red)} | |
| .reward-entry .rew.zero{color:var(--text-muted)} | |
| /* Zone temps bar chart */ | |
| .zone-bars{display:flex;flex-direction:column;gap:0.375rem} | |
| .zone-bar-row{display:flex;align-items:center;gap:0.5rem;font-size:0.72rem} | |
| .zone-bar-label{width:44px;color:var(--text-dim);font-family:var(--font-mono);flex-shrink:0} | |
| .zone-bar-track{flex:1;height:14px;background:var(--terminal-bg);border-radius:3px;position:relative;overflow:hidden;border:1px solid var(--border)} | |
| .zone-bar-fill{height:100%;border-radius:2px;transition:width 0.3s} | |
| .zone-bar-fill.safe{background:linear-gradient(90deg,var(--green-dim),var(--green))} | |
| .zone-bar-fill.warning{background:linear-gradient(90deg,var(--yellow-dim),var(--yellow))} | |
| .zone-bar-fill.critical{background:linear-gradient(90deg,var(--red-dim),var(--red))} | |
| .zone-bar-value{width:52px;text-align:right;font-family:var(--font-mono);font-weight:600;flex-shrink:0} | |
| /* Episode done banner */ | |
| .episode-done-banner{padding:0.625rem 1rem;text-align:center;font-weight:600;font-size:0.8rem;border-radius:var(--radius);margin-bottom:0.75rem;display:none} | |
| .episode-done-banner.show{display:block} | |
| .episode-done-banner.resolved{background:var(--green-dim);color:var(--green);border:1px solid rgba(34,197,94,0.3)} | |
| .episode-done-banner.failed{background:var(--red-dim);color:var(--red);border:1px solid rgba(239,68,68,0.3)} | |
| .episode-done-banner.timeout{background:var(--yellow-dim);color:var(--yellow);border:1px solid rgba(234,179,8,0.3)} | |
| /* No data placeholder */ | |
| .no-data{font-size:0.75rem;color:var(--text-muted);text-align:center;padding:0.75rem 0.5rem} | |
| /* Spinner */ | |
| .spinner{display:inline-block;width:14px;height:14px;border:2px solid rgba(255,255,255,0.2);border-top-color:currentColor;border-radius:50%;animation:spin 0.5s linear infinite} | |
| @keyframes spin{to{transform:rotate(360deg)}} | |
| /* Scrollbar */ | |
| ::-webkit-scrollbar{width:5px;height:5px} | |
| ::-webkit-scrollbar-track{background:transparent} | |
| ::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px} | |
| ::-webkit-scrollbar-thumb:hover{background:var(--text-muted)} | |
| /* Responsive - Tablet */ | |
| @media(max-width:1100px){ | |
| .main{grid-template-columns:1fr;grid-template-rows:auto 1fr auto} | |
| .sidebar{border-right:none;border-bottom:1px solid var(--border);max-height:none} | |
| .sidebar.collapsed{display:none} | |
| .right-panel{border-left:none;border-top:1px solid var(--border);max-height:none} | |
| .right-panel.collapsed{display:none} | |
| .sidebar-header{display:none} | |
| .scenario-list{display:flex;overflow-x:auto;overflow-y:hidden;padding:0.5rem;gap:0.5rem} | |
| .scenario-group{display:flex;gap:0.5rem;margin:0;flex-shrink:0} | |
| .scenario-group-title{writing-mode:vertical-lr;padding:0.5rem 0.25rem;font-size:0.6rem} | |
| .scenario-card{min-width:160px;flex-shrink:0} | |
| .sidebar-actions{flex-direction:row} | |
| .config-select{width:auto;flex:1} | |
| .right-panel .panel-section{padding:0.625rem 0.75rem} | |
| .metrics-grid{grid-template-columns:repeat(4,1fr)} | |
| } | |
| /* Responsive - Mobile */ | |
| @media(max-width:640px){ | |
| .header{padding:0.5rem 0.75rem} | |
| .logo{font-size:1rem} | |
| .dashboard-output{font-size:0.62rem;padding:0.5rem;line-height:1.2} | |
| .metrics-grid{grid-template-columns:1fr 1fr} | |
| html{font-size:13px} | |
| .command-input{font-size:0.75rem} | |
| .scenario-card{min-width:140px} | |
| .sidebar-actions{flex-direction:column} | |
| } | |
| /* Toggle buttons for mobile */ | |
| .mobile-toggles{display:none;gap:0.5rem} | |
| @media(max-width:1100px){.mobile-toggles{display:flex}} | |
| .toggle-btn{padding:0.25rem 0.625rem;background:transparent;border:1px solid var(--border);border-radius:var(--radius);color:var(--text-dim);font-size:0.7rem;cursor:pointer;font-family:var(--font-sans);transition:all 0.15s} | |
| .toggle-btn.active{border-color:var(--accent);color:var(--accent);background:rgba(59,130,246,0.08)} | |
| /* ─── Tab Navigation ─── */ | |
| .header-tabs{display:flex;gap:0.25rem;background:var(--terminal-bg);border-radius:var(--radius);padding:0.2rem} | |
| .tab-btn{padding:0.375rem 1rem;border-radius:6px;border:none;background:transparent;color:var(--text-dim);font-size:0.78rem;font-weight:600;font-family:var(--font-sans);cursor:pointer;transition:all 0.2s;white-space:nowrap;position:relative} | |
| .tab-btn:hover{color:var(--text)} | |
| .tab-btn.active{background:var(--accent);color:#fff;box-shadow:0 1px 4px rgba(59,130,246,0.3)} | |
| .tab-content{display:none;min-height:0} | |
| .tab-content.active{display:block;overflow:hidden;min-height:0} | |
| .tab-content.active > .main{height:100%} | |
| /* ─── Guide Page ─── */ | |
| .guide-page{display:none;overflow-y:auto;padding:2rem 1.5rem;background:var(--bg)} | |
| .guide-page.active{display:block} | |
| .guide-inner{max-width:920px;margin:0 auto} | |
| .guide-hero{text-align:center;padding:2.5rem 1rem 2rem;margin-bottom:2rem} | |
| .guide-hero h1{font-size:2rem;font-weight:800;letter-spacing:-0.03em;margin-bottom:0.5rem} | |
| .guide-hero h1 span{color:var(--accent)} | |
| .guide-hero p{color:var(--text-dim);font-size:0.95rem;max-width:560px;margin:0 auto;line-height:1.7} | |
| /* Guide sections */ | |
| .guide-section{margin-bottom:2.5rem} | |
| .guide-section-header{display:flex;align-items:center;gap:0.75rem;margin-bottom:1.25rem;padding-bottom:0.75rem;border-bottom:1px solid var(--border)} | |
| .guide-section-icon{width:36px;height:36px;border-radius:var(--radius);display:flex;align-items:center;justify-content:center;font-size:1.1rem;flex-shrink:0} | |
| .guide-section-icon.blue{background:rgba(59,130,246,0.12);color:var(--accent)} | |
| .guide-section-icon.green{background:rgba(34,197,94,0.12);color:var(--green)} | |
| .guide-section-icon.orange{background:rgba(249,115,22,0.12);color:var(--orange)} | |
| .guide-section-icon.red{background:rgba(239,68,68,0.12);color:var(--red)} | |
| .guide-section-icon.cyan{background:rgba(6,182,212,0.12);color:var(--cyan)} | |
| .guide-section-icon.yellow{background:rgba(234,179,8,0.12);color:var(--yellow)} | |
| .guide-section h2{font-size:1.15rem;font-weight:700;letter-spacing:-0.01em} | |
| .guide-section p,.guide-section li{font-size:0.85rem;color:var(--text-dim);line-height:1.75} | |
| .guide-section strong{color:var(--text)} | |
| .guide-section ul,.guide-section ol{padding-left:1.25rem;margin:0.75rem 0} | |
| .guide-section li{margin-bottom:0.35rem} | |
| /* Guide cards grid */ | |
| .guide-cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:0.75rem} | |
| .guide-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1rem 1.25rem;transition:border-color 0.2s} | |
| .guide-card:hover{border-color:var(--border-active)} | |
| .guide-card .gc-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:0.5rem} | |
| .guide-card .gc-id{font-family:var(--font-mono);font-weight:800;font-size:0.9rem;color:var(--accent)} | |
| .guide-card .gc-diff{font-size:0.6rem;font-weight:700;padding:0.1rem 0.5rem;border-radius:999px;text-transform:uppercase;letter-spacing:0.05em} | |
| .guide-card .gc-name{font-weight:600;font-size:0.85rem;margin-bottom:0.375rem;color:var(--text)} | |
| .guide-card .gc-desc{font-size:0.78rem;color:var(--text-dim);line-height:1.6;margin-bottom:0.625rem} | |
| .guide-card .gc-hint{font-size:0.72rem;color:var(--cyan);font-family:var(--font-mono);background:var(--terminal-bg);padding:0.375rem 0.625rem;border-radius:4px;border:1px solid var(--border)} | |
| .guide-card .gc-hint strong{color:var(--text);font-size:0.68rem;text-transform:uppercase;letter-spacing:0.05em;display:block;margin-bottom:0.2rem} | |
| /* Guide table */ | |
| .guide-table{width:100%;border-collapse:collapse;font-size:0.78rem;margin:0.75rem 0;border:1px solid var(--border);border-radius:var(--radius);overflow:hidden} | |
| .guide-table thead{background:var(--bg-card)} | |
| .guide-table th{text-align:left;padding:0.625rem 0.875rem;font-weight:600;color:var(--text);border-bottom:1px solid var(--border);font-size:0.72rem;text-transform:uppercase;letter-spacing:0.05em} | |
| .guide-table td{padding:0.5rem 0.875rem;border-bottom:1px solid var(--border);color:var(--text-dim);vertical-align:top} | |
| .guide-table tr:last-child td{border-bottom:none} | |
| .guide-table tbody tr:hover{background:var(--bg-card-hover)} | |
| .guide-table code{font-family:var(--font-mono);font-size:0.75rem;color:var(--cyan);background:var(--terminal-bg);padding:0.1rem 0.375rem;border-radius:3px} | |
| .guide-table .tag{display:inline-block;font-size:0.6rem;font-weight:700;padding:0.1rem 0.4rem;border-radius:999px;text-transform:uppercase} | |
| .tag-pos{background:var(--green-dim);color:var(--green)} | |
| .tag-neg{background:var(--red-dim);color:var(--red)} | |
| .tag-range{background:rgba(6,182,212,0.12);color:var(--cyan)} | |
| /* Guide code block */ | |
| .guide-code{background:var(--terminal-bg);border:1px solid var(--border);border-radius:var(--radius);padding:0.875rem 1rem;font-family:var(--font-mono);font-size:0.75rem;line-height:1.6;color:var(--green);overflow-x:auto;margin:0.75rem 0;white-space:pre} | |
| /* Reward component cards */ | |
| .reward-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:0.75rem;margin:1rem 0} | |
| .reward-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1rem 1.25rem} | |
| .reward-card h4{font-size:0.85rem;font-weight:700;margin-bottom:0.375rem;display:flex;align-items:center;gap:0.5rem} | |
| .reward-card h4 .rc-range{font-size:0.65rem;font-family:var(--font-mono);color:var(--text-muted);font-weight:500} | |
| .reward-card p{font-size:0.78rem;color:var(--text-dim);line-height:1.6} | |
| .reward-card .rc-formula{font-family:var(--font-mono);font-size:0.72rem;color:var(--cyan);background:var(--terminal-bg);padding:0.35rem 0.625rem;border-radius:4px;margin-top:0.5rem;border:1px solid var(--border);display:inline-block} | |
| /* Guide weight profile inline table */ | |
| .weight-profiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:0.75rem;margin:1rem 0} | |
| .weight-profile{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:0.875rem 1rem} | |
| .weight-profile h4{font-size:0.8rem;font-weight:700;margin-bottom:0.625rem;color:var(--accent)} | |
| .weight-bar-row{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.35rem;font-size:0.72rem} | |
| .weight-bar-label{width:72px;color:var(--text-dim);flex-shrink:0} | |
| .weight-bar-track{flex:1;height:8px;background:var(--terminal-bg);border-radius:4px;overflow:hidden} | |
| .weight-bar-fill{height:100%;border-radius:4px;background:var(--accent);transition:width 0.3s} | |
| .weight-bar-val{width:32px;text-align:right;font-family:var(--font-mono);color:var(--text-muted);font-weight:600;flex-shrink:0} | |
| /* ASHRAE visual */ | |
| .ashrae-visual{display:flex;flex-direction:column;gap:0.75rem;margin:1rem 0} | |
| .ashrae-row{display:flex;align-items:center;gap:0.75rem;font-size:0.78rem} | |
| .ashrae-label{width:32px;font-family:var(--font-mono);font-weight:700;color:var(--accent);flex-shrink:0} | |
| .ashrae-bar-container{flex:1;position:relative;height:24px} | |
| .ashrae-bar-bg{position:absolute;inset:0;background:var(--terminal-bg);border-radius:4px;border:1px solid var(--border)} | |
| .ashrae-bar-rec{position:absolute;height:100%;background:rgba(34,197,94,0.2);border:1px solid rgba(34,197,94,0.4);border-radius:4px} | |
| .ashrae-bar-allow{position:absolute;height:100%;background:rgba(234,179,8,0.1);border:1px solid rgba(234,179,8,0.25);border-radius:4px} | |
| .ashrae-bar-label{position:absolute;top:50%;transform:translateY(-50%);font-size:0.65rem;font-family:var(--font-mono);color:var(--text-dim)} | |
| @media(max-width:640px){ | |
| .guide-page{padding:1rem 0.75rem} | |
| .guide-hero h1{font-size:1.5rem} | |
| .guide-cards{grid-template-columns:1fr} | |
| .reward-grid{grid-template-columns:1fr} | |
| .weight-profiles{grid-template-columns:1fr} | |
| .guide-table{font-size:0.7rem} | |
| .guide-table th,.guide-table td{padding:0.375rem 0.5rem} | |
| } | |
| /* ─── Demos Page ─── */ | |
| .demos-page{display:none;overflow-y:auto;padding:2rem 1.5rem;background:var(--bg)} | |
| .demos-page.active{display:block} | |
| .demos-inner{max-width:1000px;margin:0 auto} | |
| .demos-hero{text-align:center;padding:2rem 1rem 1.5rem;margin-bottom:1.5rem} | |
| .demos-hero h1{font-size:2rem;font-weight:800;letter-spacing:-0.03em;margin-bottom:0.5rem} | |
| .demos-hero h1 span{color:var(--accent)} | |
| .demos-hero p{color:var(--text-dim);font-size:0.9rem;max-width:620px;margin:0 auto;line-height:1.7} | |
| /* Demo overview table */ | |
| .demo-overview{width:100%;border-collapse:collapse;font-size:0.78rem;margin:0 0 2rem;border:1px solid var(--border);border-radius:var(--radius);overflow:hidden} | |
| .demo-overview thead{background:var(--bg-card)} | |
| .demo-overview th{text-align:left;padding:0.625rem 0.875rem;font-weight:600;color:var(--text);border-bottom:1px solid var(--border);font-size:0.72rem;text-transform:uppercase;letter-spacing:0.05em} | |
| .demo-overview td{padding:0.5rem 0.875rem;border-bottom:1px solid var(--border);color:var(--text-dim);vertical-align:middle} | |
| .demo-overview tr:last-child td{border-bottom:none} | |
| .demo-overview tbody tr{cursor:pointer;transition:background 0.15s} | |
| .demo-overview tbody tr:hover{background:var(--bg-card-hover)} | |
| .demo-overview code{font-family:var(--font-mono);font-size:0.75rem;color:var(--cyan);background:var(--terminal-bg);padding:0.1rem 0.375rem;border-radius:3px} | |
| /* Demo accordion items */ | |
| .demo-item{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);margin-bottom:1rem;overflow:hidden;transition:border-color 0.2s} | |
| .demo-item:hover{border-color:rgba(59,130,246,0.3)} | |
| .demo-item.open{border-color:var(--accent)} | |
| .demo-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1.25rem;cursor:pointer;user-select:none;gap:1rem} | |
| .demo-header:hover{background:rgba(255,255,255,0.02)} | |
| .demo-header-left{display:flex;align-items:center;gap:0.875rem;min-width:0} | |
| .demo-badge{font-family:var(--font-mono);font-weight:800;font-size:0.85rem;color:var(--accent);background:rgba(59,130,246,0.1);padding:0.3rem 0.625rem;border-radius:var(--radius);white-space:nowrap} | |
| .demo-title-block{min-width:0} | |
| .demo-title-block h3{font-size:0.9rem;font-weight:700;color:var(--text);margin-bottom:0.15rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} | |
| .demo-title-block .demo-subtitle{font-size:0.72rem;color:var(--text-muted);display:flex;gap:0.75rem;flex-wrap:wrap} | |
| .demo-title-block .demo-subtitle span{display:inline-flex;align-items:center;gap:0.25rem} | |
| .demo-header-right{display:flex;align-items:center;gap:0.75rem;flex-shrink:0} | |
| .demo-reward{font-family:var(--font-mono);font-weight:700;font-size:0.85rem;color:var(--green)} | |
| .demo-chevron{width:20px;height:20px;color:var(--text-muted);transition:transform 0.2s;flex-shrink:0} | |
| .demo-item.open .demo-chevron{transform:rotate(180deg)} | |
| .demo-body{display:none;padding:0 1.25rem 1.25rem;animation:fadeIn 0.2s ease} | |
| .demo-item.open .demo-body{display:block} | |
| @keyframes fadeIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}} | |
| /* Demo context */ | |
| .demo-context{background:var(--terminal-bg);border:1px solid var(--border);border-radius:var(--radius);padding:0.875rem 1rem;margin-bottom:1rem;font-size:0.8rem;color:var(--text-dim);line-height:1.65} | |
| /* Demo step table */ | |
| .demo-steps{width:100%;border-collapse:collapse;font-size:0.75rem;margin:0.75rem 0;border:1px solid var(--border);border-radius:var(--radius);overflow:hidden} | |
| .demo-steps thead{background:rgba(59,130,246,0.06)} | |
| .demo-steps th{text-align:left;padding:0.5rem 0.625rem;font-weight:600;color:var(--text);border-bottom:1px solid var(--border);font-size:0.68rem;text-transform:uppercase;letter-spacing:0.05em} | |
| .demo-steps td{padding:0.4rem 0.625rem;border-bottom:1px solid var(--border);color:var(--text-dim);vertical-align:top;line-height:1.5} | |
| .demo-steps tr:last-child td{border-bottom:none} | |
| .demo-steps tbody tr:hover{background:var(--bg-card-hover)} | |
| .demo-steps code{font-family:var(--font-mono);font-size:0.72rem;color:var(--cyan);background:var(--terminal-bg);padding:0.1rem 0.35rem;border-radius:3px;white-space:nowrap} | |
| .demo-steps .step-num{font-family:var(--font-mono);font-weight:700;color:var(--text-muted);width:28px} | |
| .demo-steps .rew-pos{color:var(--green);font-weight:600;font-family:var(--font-mono)} | |
| .demo-steps .rew-neg{color:var(--red);font-weight:600;font-family:var(--font-mono)} | |
| .demo-steps .rew-big{color:var(--green);font-weight:800;font-family:var(--font-mono)} | |
| .demo-steps .resolved-row{background:rgba(34,197,94,0.06)} | |
| /* Demo analysis box */ | |
| .demo-analysis{background:var(--terminal-bg);border:1px solid var(--border);border-radius:var(--radius);padding:0.875rem 1rem;margin-top:1rem;font-size:0.78rem;color:var(--text-dim);line-height:1.65} | |
| .demo-analysis h4{font-size:0.8rem;font-weight:700;color:var(--text);margin-bottom:0.5rem} | |
| .demo-analysis ul{padding-left:1.25rem;margin:0.5rem 0} | |
| .demo-analysis li{margin-bottom:0.25rem} | |
| .demo-analysis strong{color:var(--text)} | |
| /* Demo comparison tables */ | |
| .demo-compare{width:100%;border-collapse:collapse;font-size:0.75rem;margin:0.75rem 0;border:1px solid var(--border);border-radius:var(--radius);overflow:hidden} | |
| .demo-compare thead{background:var(--bg-card)} | |
| .demo-compare th{padding:0.4rem 0.625rem;font-weight:600;color:var(--text);border-bottom:1px solid var(--border);font-size:0.68rem;text-transform:uppercase;text-align:left} | |
| .demo-compare td{padding:0.375rem 0.625rem;border-bottom:1px solid var(--border);color:var(--text-dim);font-family:var(--font-mono);font-size:0.72rem} | |
| .demo-compare tr:last-child td{border-bottom:none} | |
| /* Resolution gate table */ | |
| .demo-gate-table{width:100%;border-collapse:collapse;font-size:0.78rem;margin:1rem 0;border:1px solid var(--border);border-radius:var(--radius);overflow:hidden} | |
| .demo-gate-table thead{background:rgba(239,68,68,0.06)} | |
| .demo-gate-table th{text-align:left;padding:0.5rem 0.75rem;font-weight:600;color:var(--text);border-bottom:1px solid var(--border);font-size:0.72rem;text-transform:uppercase} | |
| .demo-gate-table td{padding:0.5rem 0.75rem;border-bottom:1px solid var(--border);color:var(--text-dim);font-size:0.78rem} | |
| .demo-gate-table tr:last-child td{border-bottom:none} | |
| .demo-gate-table code{font-family:var(--font-mono);font-size:0.72rem;color:var(--cyan);background:var(--terminal-bg);padding:0.1rem 0.35rem;border-radius:3px} | |
| @media(max-width:640px){ | |
| .demos-page{padding:1rem 0.75rem} | |
| .demos-hero h1{font-size:1.5rem} | |
| .demo-header{padding:0.75rem;flex-wrap:wrap} | |
| .demo-header-left{flex:1;min-width:200px} | |
| .demo-steps{font-size:0.68rem} | |
| .demo-steps th,.demo-steps td{padding:0.3rem 0.4rem} | |
| .demo-body{padding:0 0.75rem 0.75rem} | |
| .demo-overview{font-size:0.7rem} | |
| .demo-title-block h3{font-size:0.8rem} | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app"> | |
| <!-- Header --> | |
| <header class="header"> | |
| <div class="header-left"> | |
| <div class="logo">DC<span>-Ops</span></div> | |
| <div id="statusBadge" class="status-badge disconnected"> | |
| <span class="status-dot"></span> | |
| <span id="statusText">Disconnected</span> | |
| </div> | |
| </div> | |
| <div class="header-tabs"> | |
| <button class="tab-btn active" data-tab="console" onclick="switchTab('console')">Console</button> | |
| <button class="tab-btn" data-tab="demos" onclick="switchTab('demos')">Demos</button> | |
| <button class="tab-btn" data-tab="guide" onclick="switchTab('guide')">Guide</button> | |
| </div> | |
| <div class="mobile-toggles"> | |
| <button class="toggle-btn active" id="toggleScenarios" onclick="togglePanel('sidebar')">Scenarios</button> | |
| <button class="toggle-btn active" id="toggleMetrics" onclick="togglePanel('right-panel')">Metrics</button> | |
| </div> | |
| </header> | |
| <!-- Main Layout (Console Tab) --> | |
| <div class="tab-content active" id="tabConsole"> | |
| <div class="main"> | |
| <!-- Left: Scenario Browser --> | |
| <aside class="sidebar" id="sidebar"> | |
| <div class="sidebar-header">Scenario Browser</div> | |
| <div class="scenario-list" id="scenarioList"> | |
| <div class="scenario-group"> | |
| <div class="scenario-group-title">Thermal</div> | |
| <div class="scenario-card" data-id="A1" onclick="selectScenario('A1')"> | |
| <div class="sc-header"> | |
| <span class="sc-id">A1</span> | |
| <span class="sc-diff easy">Easy</span> | |
| </div> | |
| <div class="sc-name">Cooling Setpoint Optimization</div> | |
| <div class="sc-desc">CRACs overcooling at 15°C. Optimize for efficiency while staying in ASHRAE range.</div> | |
| </div> | |
| <div class="scenario-card" data-id="A2" onclick="selectScenario('A2')"> | |
| <div class="sc-header"> | |
| <span class="sc-id">A2</span> | |
| <span class="sc-diff medium">Medium</span> | |
| </div> | |
| <div class="sc-name">Thermal Event Response</div> | |
| <div class="sc-desc">CRAC-3 compressor failure. Diagnose and stabilize all zones.</div> | |
| </div> | |
| <div class="scenario-card" data-id="A4" onclick="selectScenario('A4')"> | |
| <div class="sc-header"> | |
| <span class="sc-id">A4</span> | |
| <span class="sc-diff hard">Hard</span> | |
| </div> | |
| <div class="sc-name">CRAC Failure Cascade</div> | |
| <div class="sc-desc">CRAC-1 compressor + CRAC-3 fan failure. Manage cascading thermal event.</div> | |
| </div> | |
| </div> | |
| <div class="scenario-group"> | |
| <div class="scenario-group-title">Power</div> | |
| <div class="scenario-card" data-id="B1" onclick="selectScenario('B1')"> | |
| <div class="sc-header"> | |
| <span class="sc-id">B1</span> | |
| <span class="sc-diff medium">Medium</span> | |
| </div> | |
| <div class="sc-name">UPS Alarm Response</div> | |
| <div class="sc-desc">UPS transferred to battery after utility event. Diagnose and acknowledge.</div> | |
| </div> | |
| <div class="scenario-card" data-id="B3" onclick="selectScenario('B3')"> | |
| <div class="sc-header"> | |
| <span class="sc-id">B3</span> | |
| <span class="sc-diff easy">Easy</span> | |
| </div> | |
| <div class="sc-name">Generator Test Protocol</div> | |
| <div class="sc-desc">Routine monthly generator test. Follow 5-step protocol correctly.</div> | |
| </div> | |
| <div class="scenario-card" data-id="B4" onclick="selectScenario('B4')"> | |
| <div class="sc-header"> | |
| <span class="sc-id">B4</span> | |
| <span class="sc-diff hard">Hard</span> | |
| </div> | |
| <div class="sc-name">Power Failure Cascade</div> | |
| <div class="sc-desc">Utility loss + extended generator warmup. Manage battery and thermal.</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="sidebar-actions"> | |
| <select id="configSelect" class="config-select"> | |
| <option value="default">Default Facility (2 zones, 160 kW)</option> | |
| <option value="small">Small Facility (1 zone, 80 kW)</option> | |
| <option value="large">Large Facility (4 zones, 600 kW)</option> | |
| </select> | |
| <button id="startBtn" class="btn btn-primary" onclick="startEpisode()" disabled> | |
| Select a Scenario | |
| </button> | |
| <button id="resetBtn" class="btn btn-outline" onclick="resetEpisode()" style="display:none"> | |
| Reset Episode | |
| </button> | |
| </div> | |
| </aside> | |
| <!-- Center: Dashboard Display --> | |
| <div class="center"> | |
| <div class="dashboard-container" id="dashboardContainer"> | |
| <div id="doneBanner" class="episode-done-banner"></div> | |
| <div class="dashboard-box"> | |
| <div class="dashboard-title-bar"> | |
| <div class="dots"><span></span><span></span><span></span></div> | |
| <div class="title" id="terminalTitle">dc-ops-console</div> | |
| </div> | |
| <div class="dashboard-output" id="dashboardOutput"><div class="welcome"> | |
| <h2>DC-Ops Operations Console</h2> | |
| <p>Select a scenario from the panel to begin a datacenter operations episode. Issue commands and monitor the facility in real-time.</p> | |
| <div class="hint"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg> | |
| Pick a scenario to start | |
| </div> | |
| </div></div> | |
| </div> | |
| <div id="actionResult" class="action-result" style="display:none"></div> | |
| </div> | |
| <!-- Command Bar --> | |
| <div class="command-bar"> | |
| <div class="command-input-group"> | |
| <input type="text" id="commandInput" class="command-input" | |
| placeholder="Enter command (e.g., diagnose CRAC-3)" | |
| disabled autocomplete="off" | |
| onkeydown="if(event.key==='Enter'&&!event.shiftKey)sendCommand()"> | |
| <button id="sendBtn" class="btn btn-primary" onclick="sendCommand()" disabled>Send</button> | |
| </div> | |
| <div class="quick-actions" id="quickActions"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Right: Metrics Panel --> | |
| <aside class="right-panel" id="right-panel"> | |
| <div class="panel-section"> | |
| <div class="panel-section-title">Episode</div> | |
| <div class="episode-info"> | |
| <div class="episode-row"> | |
| <span class="label">Scenario</span> | |
| <span class="value" id="metaScenario">--</span> | |
| </div> | |
| <div class="episode-row"> | |
| <span class="label">Step</span> | |
| <span class="value"><span id="metaStep">0</span> / <span id="metaMaxSteps">--</span></span> | |
| </div> | |
| <div class="progress-bar"> | |
| <div class="progress-fill" id="stepProgress" style="width:0%"></div> | |
| </div> | |
| <div class="episode-row"> | |
| <span class="label">Total Reward</span> | |
| <span class="value" id="metaCumReward" style="color:var(--text)">0.00</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="panel-section"> | |
| <div class="panel-section-title">Key Metrics</div> | |
| <div class="metrics-grid"> | |
| <div class="metric"> | |
| <div class="metric-label">PUE</div> | |
| <div class="metric-value neutral" id="metricPUE">--</div> | |
| </div> | |
| <div class="metric"> | |
| <div class="metric-label">IT Load</div> | |
| <div class="metric-value neutral" id="metricIT">--</div> | |
| </div> | |
| <div class="metric"> | |
| <div class="metric-label">Cooling</div> | |
| <div class="metric-value neutral" id="metricCooling">--</div> | |
| </div> | |
| <div class="metric"> | |
| <div class="metric-label">Outside</div> | |
| <div class="metric-value neutral" id="metricOutside">--</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="panel-section"> | |
| <div class="panel-section-title">Zone Temperatures</div> | |
| <div class="zone-bars" id="zoneBars"> | |
| <div class="no-data">No data</div> | |
| </div> | |
| </div> | |
| <div class="panel-section"> | |
| <div class="panel-section-title">Power</div> | |
| <div id="powerInfo"> | |
| <div class="no-data">No data</div> | |
| </div> | |
| </div> | |
| <div class="panel-section"> | |
| <div class="panel-section-title">Reward History</div> | |
| <div class="reward-history" id="rewardHistory"> | |
| <div class="no-data">No steps yet</div> | |
| </div> | |
| </div> | |
| </aside> | |
| </div> | |
| </div><!-- /tabConsole --> | |
| <!-- Demos Tab --> | |
| <div class="demos-page" id="tabDemos"> | |
| <div class="demos-inner"> | |
| <div class="demos-hero"> | |
| <h1>Scenario <span>Demos</span></h1> | |
| <p>Nine verified demo walkthroughs across all scenarios and facility sizes. Each demo resolves in 8–10 steps following proper operational procedure: assess → diagnose → compensate → verify → resolve.</p> | |
| </div> | |
| <!-- Overview Table --> | |
| <table class="demo-overview"> | |
| <thead> | |
| <tr><th>#</th><th>Scenario</th><th>Facility</th><th>Steps</th><th>Reward</th><th>Key Skill</th></tr> | |
| </thead> | |
| <tbody> | |
| <tr onclick="openDemo('demo1')"><td>1</td><td><code>A1</code> Setpoint Optimization</td><td>Default</td><td>9</td><td style="color:var(--green);font-weight:700">+0.324</td><td>PUE optimization</td></tr> | |
| <tr onclick="openDemo('demo2')"><td>2</td><td><code>A2</code> Thermal Event</td><td>Default</td><td>8</td><td style="color:var(--green);font-weight:700">+0.873</td><td>Single-failure response</td></tr> | |
| <tr onclick="openDemo('demo3')"><td>3</td><td><code>A2</code> Thermal Event</td><td>Large</td><td>8</td><td style="color:var(--green);font-weight:700">+0.831</td><td>Multi-zone + H1 isolation</td></tr> | |
| <tr onclick="openDemo('demo4')"><td>4</td><td><code>A4</code> CRAC Cascade</td><td>Default</td><td>8</td><td style="color:var(--green);font-weight:700">+1.230</td><td>Multi-failure triage</td></tr> | |
| <tr onclick="openDemo('demo5')"><td>5</td><td><code>A4</code> CRAC Cascade</td><td>Large</td><td>8</td><td style="color:var(--green);font-weight:700">+1.150</td><td>Multi-zone cascade</td></tr> | |
| <tr onclick="openDemo('demo6')"><td>6</td><td><code>B1</code> UPS Alarm</td><td>Default</td><td>8</td><td style="color:var(--green);font-weight:700">+0.512</td><td>Power chain audit</td></tr> | |
| <tr onclick="openDemo('demo7')"><td>7</td><td><code>B3</code> Generator Test</td><td>Default</td><td>10</td><td style="color:var(--green);font-weight:700">+0.567</td><td>Protocol compliance</td></tr> | |
| <tr onclick="openDemo('demo8')"><td>8</td><td><code>B4</code> Power Failure</td><td>Default</td><td>8</td><td style="color:var(--green);font-weight:700">+0.934</td><td>Battery + gen startup</td></tr> | |
| <tr onclick="openDemo('demo9')"><td>9</td><td><code>B4</code> Power Failure</td><td>Small</td><td>8</td><td style="color:var(--green);font-weight:700">+0.948</td><td>Aggressive load shedding</td></tr> | |
| </tbody> | |
| </table> | |
| <!-- Demo 1: A1 Default --> | |
| <div class="demo-item" id="demo1"> | |
| <div class="demo-header" onclick="toggleDemo('demo1')"> | |
| <div class="demo-header-left"> | |
| <span class="demo-badge">A1</span> | |
| <div class="demo-title-block"> | |
| <h3>Cooling Setpoint Optimization</h3> | |
| <div class="demo-subtitle"> | |
| <span>Default Facility</span> | |
| <span>9 steps</span> | |
| <span style="background:var(--green-dim);color:var(--green);padding:0.05rem 0.4rem;border-radius:999px;font-size:0.6rem;font-weight:700">EASY</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="demo-header-right"> | |
| <span class="demo-reward">+0.324</span> | |
| <svg class="demo-chevron" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"/></svg> | |
| </div> | |
| </div> | |
| <div class="demo-body"> | |
| <div class="demo-context"> | |
| All four CRACs are set to 15°C — far below what the servers need. This wastes energy: compressors run hard, fans blow at 100%, and PUE sits at 1.87. ASHRAE A2 class allows inlet temps up to 27°C. The agent must raise setpoints and reduce fan speeds to approach the PUE target of 1.6 without overheating. | |
| </div> | |
| <table class="demo-steps"> | |
| <thead><tr><th>#</th><th>Command</th><th>Reward</th><th>Cumul</th><th>PUE</th><th>Inlets</th><th>Reasoning</th></tr></thead> | |
| <tbody> | |
| <tr><td class="step-num">1</td><td><code>check_status</code></td><td class="rew-pos">+0.131</td><td>+0.131</td><td>1.87</td><td>17.1 / 17.1</td><td>Procedure bonus (+0.2): must check before adjusting. Baseline — all CRACs at 15°C.</td></tr> | |
| <tr><td class="step-num">2</td><td><code>adjust_setpoint CRAC-1 24</code></td><td class="rew-pos">+0.047</td><td>+0.178</td><td>1.80</td><td>17.7 / 17.1</td><td>Raise from 15→24°C. Compressor works less → immediate PUE drop.</td></tr> | |
| <tr><td class="step-num">3</td><td><code>adjust_setpoint CRAC-2 24</code></td><td class="rew-pos">+0.039</td><td>+0.217</td><td>1.76</td><td>19.2 / 17.0</td><td>PUE continues falling. Zone A inlets rising — still safely below 27°C.</td></tr> | |
| <tr><td class="step-num">4</td><td><code>adjust_setpoint CRAC-3 24</code></td><td class="rew-pos">+0.038</td><td>+0.255</td><td>1.71</td><td>20.7 / 17.6</td><td>Zone B responding. Thermal mass (11.1 kJ/K per server) causes gradual warming.</td></tr> | |
| <tr><td class="step-num">5</td><td><code>adjust_setpoint CRAC-4 24</code></td><td class="rew-pos">+0.025</td><td>+0.280</td><td>1.69</td><td>21.9 / 19.1</td><td>All CRACs at 24°C. PUE 1.69 — still above 1.6 target. Fan reduction needed.</td></tr> | |
| <tr><td class="step-num">6</td><td><code>set_fan_speed CRAC-1 70</code></td><td class="rew-neg">−0.019</td><td>+0.261</td><td>1.68</td><td>22.8 / 20.6</td><td>Fan power follows cubic law: at 70%, power = 34% of rated (66% saving).</td></tr> | |
| <tr><td class="step-num">7</td><td><code>set_fan_speed CRAC-2 70</code></td><td class="rew-neg">−0.017</td><td>+0.244</td><td>1.66</td><td>23.4 / 21.9</td><td>PUE 1.66. Inlets 23.4°C — still 3.6°C below ASHRAE A2 max (27°C).</td></tr> | |
| <tr><td class="step-num">8</td><td><code>set_fan_speed CRAC-3 70</code></td><td class="rew-neg">−0.012</td><td>+0.232</td><td>1.63</td><td>23.9 / 22.7</td><td>Almost there. System approaching equilibrium.</td></tr> | |
| <tr class="resolved-row"><td class="step-num">9</td><td><code>set_fan_speed CRAC-4 70</code></td><td class="rew-big">+0.092</td><td><strong>+0.324</strong></td><td><strong>1.60</strong></td><td>24.3 / 23.3</td><td><strong>RESOLVED.</strong> PUE hits target (≤1.6). Speed bonus: (10−9)/10 = +0.1.</td></tr> | |
| </tbody> | |
| </table> | |
| <div class="demo-analysis"> | |
| <h4>Why This Works</h4> | |
| <ul> | |
| <li><strong>Phase 1 (steps 2–5): Setpoint adjustment.</strong> Raising 15→24°C reduces compressor load. PUE drops 1.87→1.69 (10% improvement).</li> | |
| <li><strong>Phase 2 (steps 6–9): Fan speed reduction.</strong> Cubic fan law means 100→70% cuts fan power by 66%. Pushes PUE from 1.69→1.60.</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Demo 2: A2 Default --> | |
| <div class="demo-item" id="demo2"> | |
| <div class="demo-header" onclick="toggleDemo('demo2')"> | |
| <div class="demo-header-left"> | |
| <span class="demo-badge">A2</span> | |
| <div class="demo-title-block"> | |
| <h3>Thermal Event Response — Default</h3> | |
| <div class="demo-subtitle"> | |
| <span>Default Facility (160 kW)</span> | |
| <span>8 steps</span> | |
| <span style="background:var(--yellow-dim);color:var(--yellow);padding:0.05rem 0.4rem;border-radius:999px;font-size:0.6rem;font-weight:700">MEDIUM</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="demo-header-right"> | |
| <span class="demo-reward">+0.873</span> | |
| <svg class="demo-chevron" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"/></svg> | |
| </div> | |
| </div> | |
| <div class="demo-body"> | |
| <div class="demo-context"> | |
| CRAC-3 compressor has failed. With only 3 of 4 CRACs operational, cooling capacity is reduced. The default facility has N+1 redundancy, so temperatures won't spike catastrophically, but the agent must diagnose the fault and compensate to ensure long-term stability. | |
| </div> | |
| <table class="demo-steps"> | |
| <thead><tr><th>#</th><th>Command</th><th>Reward</th><th>Cumul</th><th>Inlets (A/B)</th><th>Reasoning</th></tr></thead> | |
| <tbody> | |
| <tr><td class="step-num">1</td><td><code>check_status</code></td><td class="rew-pos">+0.204</td><td>+0.204</td><td>19.8 / 20.0</td><td>CRAC-3 shows "!! COMPRESSOR" — no supply temp, no airflow, 0 kW.</td></tr> | |
| <tr><td class="step-num">2</td><td><code>diagnose CRAC-3</code></td><td class="rew-pos">+0.054</td><td>+0.258</td><td>19.8 / 20.2</td><td><strong>Unlocks resolution gate.</strong> Confirms "FAULT: compressor." +0.3 bonus on subsequent setpoint changes.</td></tr> | |
| <tr><td class="step-num">3</td><td><code>diagnose CRAC-1</code></td><td class="rew-neg">−0.021</td><td>+0.237</td><td>19.8 / 20.4</td><td>Verify remaining CRACs healthy. "No faults detected."</td></tr> | |
| <tr><td class="step-num">4</td><td><code>adjust_setpoint CRAC-1 16</code></td><td class="rew-pos">+0.034</td><td>+0.270</td><td>19.6 / 20.5</td><td>Lower setpoint 18→16°C. Increases cooling output. Earns procedure bonus.</td></tr> | |
| <tr><td class="step-num">5</td><td><code>adjust_setpoint CRAC-2 16</code></td><td class="rew-pos">+0.034</td><td>+0.304</td><td>19.3 / 20.6</td><td>Both zone A CRACs overcooling to compensate.</td></tr> | |
| <tr><td class="step-num">6</td><td><code>set_fan_speed CRAC-1 100</code></td><td class="rew-pos">+0.034</td><td>+0.338</td><td>19.0 / 20.8</td><td>Max airflow on surviving CRACs.</td></tr> | |
| <tr><td class="step-num">7</td><td><code>set_fan_speed CRAC-2 100</code></td><td class="rew-pos">+0.034</td><td>+0.372</td><td>18.7 / 20.8</td><td>Zone B stabilizing at ~20.8°C — within ASHRAE recommended range.</td></tr> | |
| <tr class="resolved-row"><td class="step-num">8</td><td><code>set_fan_speed CRAC-4 100</code></td><td class="rew-big">+0.501</td><td><strong>+0.873</strong></td><td>18.5 / 20.9</td><td><strong>RESOLVED.</strong> All zones stable for 2+ steps. Speed bonus: (15−8)/15 = +0.467.</td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <!-- Demo 3: A2 Large --> | |
| <div class="demo-item" id="demo3"> | |
| <div class="demo-header" onclick="toggleDemo('demo3')"> | |
| <div class="demo-header-left"> | |
| <span class="demo-badge">A2</span> | |
| <div class="demo-title-block"> | |
| <h3>Thermal Event Response — Large</h3> | |
| <div class="demo-subtitle"> | |
| <span>Large Facility (600 kW, H1 zone)</span> | |
| <span>8 steps</span> | |
| <span style="background:var(--yellow-dim);color:var(--yellow);padding:0.05rem 0.4rem;border-radius:999px;font-size:0.6rem;font-weight:700">MEDIUM</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="demo-header-right"> | |
| <span class="demo-reward">+0.831</span> | |
| <svg class="demo-chevron" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"/></svg> | |
| </div> | |
| </div> | |
| <div class="demo-body"> | |
| <div class="demo-context"> | |
| Same CRAC-3 failure, but in a larger facility with 4 zones including an H1 (high-density GPU) zone. H1 has a tighter thermal envelope (recommended max 22°C vs 27°C for A2). | |
| </div> | |
| <table class="demo-steps"> | |
| <thead><tr><th>#</th><th>Command</th><th>Reward</th><th>Cumul</th><th>Inlets (A/B/C/D)</th><th>Reasoning</th></tr></thead> | |
| <tbody> | |
| <tr><td class="step-num">1</td><td><code>check_status</code></td><td class="rew-pos">+0.207</td><td>+0.207</td><td>20.0 / 20.8 / 19.2 / 19.7</td><td>4-zone dashboard. Zone B 0.8°C warmer. Zone C (H1) at 19.2°C — 2.8°C below its 22°C max.</td></tr> | |
| <tr><td class="step-num">2</td><td><code>diagnose CRAC-3</code></td><td class="rew-pos">+0.057</td><td>+0.263</td><td>20.0 / 21.6 / 19.2 / 19.7</td><td>FAULT: compressor. Zone B rising +0.8°C/step. Zone C stable.</td></tr> | |
| <tr><td class="step-num">3</td><td><code>diagnose CRAC-1</code></td><td class="rew-neg">−0.019</td><td>+0.245</td><td>20.0 / 22.3 / 19.2 / 19.6</td><td>Confirm CRAC-1 healthy.</td></tr> | |
| <tr><td class="step-num">4</td><td><code>diagnose CRAC-2</code></td><td class="rew-neg">−0.019</td><td>+0.225</td><td>20.0 / 23.0 / 19.2 / 19.6</td><td>Confirm CRAC-2 healthy. Thorough zone B CRAC audit.</td></tr> | |
| <tr><td class="step-num">5</td><td><code>adjust_setpoint CRAC-2 16</code></td><td class="rew-pos">+0.035</td><td>+0.261</td><td>19.9 / 23.6 / 19.2 / 19.6</td><td>Lower surviving zone B CRAC. Procedure bonus earned.</td></tr> | |
| <tr><td class="step-num">6</td><td><code>adjust_setpoint CRAC-4 16</code></td><td class="rew-pos">+0.035</td><td>+0.296</td><td>19.7 / 24.0 / 19.2 / 19.6</td><td>Rate of rise slowing. H1 zone unaffected.</td></tr> | |
| <tr><td class="step-num">7</td><td><code>set_fan_speed CRAC-2 100</code></td><td class="rew-pos">+0.035</td><td>+0.330</td><td>19.6 / 24.3 / 19.2 / 19.6</td><td>Max airflow. Zone B 2.7°C below ASHRAE A2 max.</td></tr> | |
| <tr class="resolved-row"><td class="step-num">8</td><td><code>set_fan_speed CRAC-4 100</code></td><td class="rew-big">+0.501</td><td><strong>+0.831</strong></td><td>19.5 / 24.6 / 19.2 / 19.6</td><td><strong>RESOLVED.</strong> Zone B stabilized. H1 completely unaffected.</td></tr> | |
| </tbody> | |
| </table> | |
| <div class="demo-analysis"> | |
| <h4>Default vs Large Comparison</h4> | |
| <table class="demo-compare"> | |
| <thead><tr><th>Metric</th><th>Default</th><th>Large</th></tr></thead> | |
| <tbody> | |
| <tr><td>Max inlet temp</td><td>20.9°C</td><td>24.6°C</td></tr> | |
| <tr><td>H1 zone impact</td><td>N/A</td><td>None (19.2°C)</td></tr> | |
| <tr><td>Cumulative reward</td><td>+0.873</td><td>+0.831</td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Demo 4: A4 Default --> | |
| <div class="demo-item" id="demo4"> | |
| <div class="demo-header" onclick="toggleDemo('demo4')"> | |
| <div class="demo-header-left"> | |
| <span class="demo-badge">A4</span> | |
| <div class="demo-title-block"> | |
| <h3>CRAC Failure Cascade — Default</h3> | |
| <div class="demo-subtitle"> | |
| <span>Default Facility (160 kW)</span> | |
| <span>8 steps</span> | |
| <span style="background:var(--red-dim);color:var(--red);padding:0.05rem 0.4rem;border-radius:999px;font-size:0.6rem;font-weight:700">HARD</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="demo-header-right"> | |
| <span class="demo-reward">+1.230</span> | |
| <svg class="demo-chevron" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"/></svg> | |
| </div> | |
| </div> | |
| <div class="demo-body"> | |
| <div class="demo-context"> | |
| Two CRACs fail simultaneously: CRAC-1 (compressor) and CRAC-3 (fan). Only CRAC-2 and CRAC-4 remain — 50% cooling capacity lost. The agent must diagnose both failures, aggressively compensate, and consider load shedding. | |
| </div> | |
| <table class="demo-steps"> | |
| <thead><tr><th>#</th><th>Command</th><th>Reward</th><th>Cumul</th><th>Inlets (A/B)</th><th>Reasoning</th></tr></thead> | |
| <tbody> | |
| <tr><td class="step-num">1</td><td><code>check_status</code></td><td class="rew-pos">+0.212</td><td>+0.212</td><td>19.9 / 19.9</td><td>Two red-flagged CRACs. 50% cooling capacity lost.</td></tr> | |
| <tr><td class="step-num">2</td><td><code>diagnose CRAC-1</code></td><td class="rew-pos">+0.062</td><td>+0.274</td><td>20.0 / 20.0</td><td>"FAULT: compressor." First of two required diagnoses.</td></tr> | |
| <tr><td class="step-num">3</td><td><code>diagnose CRAC-3</code></td><td class="rew-pos">+0.027</td><td>+0.301</td><td>20.1 / 20.1</td><td>"FAULT: fan." Both diagnosed → resolution gate unlocked. +0.2 procedure bonus.</td></tr> | |
| <tr><td class="step-num">4</td><td><code>adjust_setpoint CRAC-2 16</code></td><td class="rew-pos">+0.062</td><td>+0.363</td><td>20.2 / 20.2</td><td>Lower surviving CRAC setpoint. Procedure bonus (diagnosis first).</td></tr> | |
| <tr><td class="step-num">5</td><td><code>adjust_setpoint CRAC-4 16</code></td><td class="rew-pos">+0.062</td><td>+0.425</td><td>20.2 / 20.3</td><td>Both survivors at 16°C. Temps stabilizing.</td></tr> | |
| <tr><td class="step-num">6</td><td><code>set_fan_speed CRAC-2 100</code></td><td class="rew-pos">+0.062</td><td>+0.487</td><td>20.1 / 20.2</td><td>Max airflow confirmed.</td></tr> | |
| <tr><td class="step-num">7</td><td><code>set_fan_speed CRAC-4 100</code></td><td class="rew-pos">+0.062</td><td>+0.549</td><td>20.1 / 20.2</td><td>Temps flat at ~20.1°C. Stable consecutive steps.</td></tr> | |
| <tr class="resolved-row"><td class="step-num">8</td><td><code>set_rack_load B-05 4</code></td><td class="rew-big">+0.681</td><td><strong>+1.230</strong></td><td>20.0 / 20.1</td><td><strong>RESOLVED.</strong> Load shed for thermal margin. Speed bonus: (20−8)/20 = +0.600.</td></tr> | |
| </tbody> | |
| </table> | |
| <div class="demo-analysis"> | |
| <h4>Why Load Shedding Matters</h4> | |
| <ul> | |
| <li>Reducing rack B-05 from 8 kW to 4 kW provides additional thermal margin</li> | |
| <li>Demonstrates workload migration capability</li> | |
| <li>Earns action quality bonus (+0.2 for interventions)</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Demo 5: A4 Large --> | |
| <div class="demo-item" id="demo5"> | |
| <div class="demo-header" onclick="toggleDemo('demo5')"> | |
| <div class="demo-header-left"> | |
| <span class="demo-badge">A4</span> | |
| <div class="demo-title-block"> | |
| <h3>CRAC Failure Cascade — Large</h3> | |
| <div class="demo-subtitle"> | |
| <span>Large Facility (600 kW, H1 zone)</span> | |
| <span>8 steps</span> | |
| <span style="background:var(--red-dim);color:var(--red);padding:0.05rem 0.4rem;border-radius:999px;font-size:0.6rem;font-weight:700">HARD</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="demo-header-right"> | |
| <span class="demo-reward">+1.150</span> | |
| <svg class="demo-chevron" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"/></svg> | |
| </div> | |
| </div> | |
| <div class="demo-body"> | |
| <div class="demo-context"> | |
| CRAC-1 and CRAC-3 down out of 8 CRACs. Zones C/D (including H1) have dedicated CRACs and remain unaffected. Zones A/B share the affected CRACs. | |
| </div> | |
| <table class="demo-steps"> | |
| <thead><tr><th>#</th><th>Command</th><th>Reward</th><th>Cumul</th><th>Inlets (A/B/C/D)</th><th>Reasoning</th></tr></thead> | |
| <tbody> | |
| <tr><td class="step-num">1</td><td><code>check_status</code></td><td class="rew-pos">+0.210</td><td>+0.210</td><td>20.4 / 20.4 / 19.2 / 19.7</td><td>CRAC-1 and CRAC-3 down. Zones C/D have own CRACs.</td></tr> | |
| <tr><td class="step-num">2</td><td><code>diagnose CRAC-1</code></td><td class="rew-pos">+0.059</td><td>+0.269</td><td>20.8 / 20.8 / 19.2 / 19.7</td><td>FAULT: compressor. Zone A/B rising ~0.4°C/step.</td></tr> | |
| <tr><td class="step-num">3</td><td><code>diagnose CRAC-3</code></td><td class="rew-pos">+0.024</td><td>+0.293</td><td>21.2 / 21.2 / 19.2 / 19.7</td><td>FAULT: fan. Both diagnosed → gate unlocked.</td></tr> | |
| <tr><td class="step-num">4</td><td><code>diagnose CRAC-2</code></td><td class="rew-pos">+0.024</td><td>+0.317</td><td>21.6 / 21.6 / 19.2 / 19.7</td><td>Verify CRAC-2 healthy. Critical with 2 failures.</td></tr> | |
| <tr><td class="step-num">5</td><td><code>adjust_setpoint CRAC-2 16</code></td><td class="rew-pos">+0.059</td><td>+0.376</td><td>21.9 / 22.0 / 19.2 / 19.6</td><td>Compensate. Zone B at 22.0°C — 13°C below ASHRAE allowable max (35°C).</td></tr> | |
| <tr><td class="step-num">6</td><td><code>adjust_setpoint CRAC-4 16</code></td><td class="rew-pos">+0.058</td><td>+0.434</td><td>22.2 / 22.3 / 19.2 / 19.6</td><td>Rate of rise slowing. H1 zone unaffected.</td></tr> | |
| <tr><td class="step-num">7</td><td><code>set_fan_speed CRAC-2 100</code></td><td class="rew-pos">+0.058</td><td>+0.492</td><td>22.5 / 22.5 / 19.2 / 19.6</td><td>Max airflow. Zone B stabilizing.</td></tr> | |
| <tr class="resolved-row"><td class="step-num">8</td><td><code>set_fan_speed CRAC-4 100</code></td><td class="rew-big">+0.658</td><td><strong>+1.150</strong></td><td>22.7 / 22.8 / 19.2 / 19.6</td><td><strong>RESOLVED.</strong> All zones within allowable. Speed bonus: +0.600.</td></tr> | |
| </tbody> | |
| </table> | |
| <div class="demo-analysis"> | |
| <h4>Default vs Large Comparison</h4> | |
| <table class="demo-compare"> | |
| <thead><tr><th>Metric</th><th>Default</th><th>Large</th></tr></thead> | |
| <tbody> | |
| <tr><td>Final zone B inlet</td><td>20.1°C</td><td>22.8°C</td></tr> | |
| <tr><td>H1 zone impact</td><td>N/A</td><td>None (19.2°C)</td></tr> | |
| <tr><td>Cumulative reward</td><td>+1.230</td><td>+1.150</td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Demo 6: B1 Default --> | |
| <div class="demo-item" id="demo6"> | |
| <div class="demo-header" onclick="toggleDemo('demo6')"> | |
| <div class="demo-header-left"> | |
| <span class="demo-badge">B1</span> | |
| <div class="demo-title-block"> | |
| <h3>UPS Alarm Response</h3> | |
| <div class="demo-subtitle"> | |
| <span>Default Facility (160 kW)</span> | |
| <span>8 steps</span> | |
| <span style="background:var(--yellow-dim);color:var(--yellow);padding:0.05rem 0.4rem;border-radius:999px;font-size:0.6rem;font-weight:700">MEDIUM</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="demo-header-right"> | |
| <span class="demo-reward">+0.512</span> | |
| <svg class="demo-chevron" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"/></svg> | |
| </div> | |
| </div> | |
| <div class="demo-body"> | |
| <div class="demo-context"> | |
| A brief utility dip caused UPS-1 to transfer to battery. Utility has been restored and UPS switched back to double-conversion mode, but the alarm persists. The agent must investigate the entire power chain and acknowledge the alarm. | |
| </div> | |
| <table class="demo-steps"> | |
| <thead><tr><th>#</th><th>Command</th><th>Reward</th><th>Cumul</th><th>Reasoning</th></tr></thead> | |
| <tbody> | |
| <tr><td class="step-num">1</td><td><code>check_status</code></td><td class="rew-neg">−0.007</td><td>−0.007</td><td>Baseline. Utility NORMAL, generator OFF, ATS on UTILITY.</td></tr> | |
| <tr><td class="step-num">2</td><td><code>diagnose UPS-1</code></td><td class="rew-pos">+0.143</td><td>+0.137</td><td><strong>Key step.</strong> mode=double_conversion, SOC=86%. Resolution gate requires this.</td></tr> | |
| <tr><td class="step-num">3</td><td><code>diagnose UPS-2</code></td><td class="rew-neg">−0.007</td><td>+0.130</td><td>Verify redundant UPS. mode=double_conversion, SOC=87%.</td></tr> | |
| <tr><td class="step-num">4</td><td><code>diagnose GEN-1</code></td><td class="rew-neg">−0.007</td><td>+0.123</td><td>Generator in standby — confirming readiness.</td></tr> | |
| <tr><td class="step-num">5</td><td><code>diagnose PDU-A-01</code></td><td class="rew-neg">−0.007</td><td>+0.117</td><td>Verify power distribution intact.</td></tr> | |
| <tr><td class="step-num">6</td><td><code>check_status</code></td><td class="rew-neg">−0.007</td><td>+0.110</td><td>Re-verify before closing incident.</td></tr> | |
| <tr><td class="step-num">7</td><td><code>diagnose PDU-B-01</code></td><td class="rew-neg">−0.007</td><td>+0.103</td><td>Complete the power chain audit.</td></tr> | |
| <tr class="resolved-row"><td class="step-num">8</td><td><code>acknowledge_alarm</code></td><td class="rew-big">+0.408</td><td><strong>+0.512</strong></td><td><strong>RESOLVED.</strong> Alarm acknowledged after thorough investigation. Speed bonus: (10−8)/10 = +0.200.</td></tr> | |
| </tbody> | |
| </table> | |
| <div class="demo-analysis"> | |
| <h4>Reward Structure Note</h4> | |
| <p>Steps 3–7 return −0.007 each because the delta-based progress metric doesn't change during investigation — only the final acknowledgment triggers progress completion. The cumulative reward is still positive (+0.512) thanks to the large resolution step reward.</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Demo 7: B3 Default --> | |
| <div class="demo-item" id="demo7"> | |
| <div class="demo-header" onclick="toggleDemo('demo7')"> | |
| <div class="demo-header-left"> | |
| <span class="demo-badge">B3</span> | |
| <div class="demo-title-block"> | |
| <h3>Generator Test Protocol</h3> | |
| <div class="demo-subtitle"> | |
| <span>Default Facility (160 kW)</span> | |
| <span>10 steps</span> | |
| <span style="background:var(--green-dim);color:var(--green);padding:0.05rem 0.4rem;border-radius:999px;font-size:0.6rem;font-weight:700">EASY</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="demo-header-right"> | |
| <span class="demo-reward">+0.567</span> | |
| <svg class="demo-chevron" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"/></svg> | |
| </div> | |
| </div> | |
| <div class="demo-body"> | |
| <div class="demo-context"> | |
| Routine monthly generator test. No fault, no emergency — the agent must follow the 5-step protocol: check → start → verify → stop → acknowledge. | |
| </div> | |
| <table class="demo-steps"> | |
| <thead><tr><th>#</th><th>Command</th><th>Reward</th><th>Cumul</th><th>Gen State</th><th>Reasoning</th></tr></thead> | |
| <tbody> | |
| <tr><td class="step-num">1</td><td><code>check_status</code></td><td class="rew-neg">−0.007</td><td>−0.007</td><td>OFF</td><td>Baseline. All systems normal.</td></tr> | |
| <tr><td class="step-num">2</td><td><code>diagnose GEN-1</code></td><td class="rew-neg">−0.007</td><td>−0.013</td><td>off</td><td>Pre-test inspection — verify before starting.</td></tr> | |
| <tr><td class="step-num">3</td><td><code>start_generator</code></td><td class="rew-pos">+0.113</td><td>+0.100</td><td>CRANKING</td><td>Generator start sequence initiated.</td></tr> | |
| <tr><td class="step-num">4</td><td><code>wait</code></td><td class="rew-neg">−0.022</td><td>+0.078</td><td>LOADED</td><td>Let generator complete warmup.</td></tr> | |
| <tr><td class="step-num">5</td><td><code>diagnose GEN-1</code></td><td class="rew-pos">+0.043</td><td>+0.122</td><td>ready</td><td><strong>Critical verification.</strong> Confirms generator running properly.</td></tr> | |
| <tr><td class="step-num">6</td><td><code>check_status</code></td><td class="rew-neg">−0.007</td><td>+0.115</td><td>LOADED</td><td>Full dashboard confirms generator loaded.</td></tr> | |
| <tr><td class="step-num">7</td><td><code>stop_generator</code></td><td class="rew-pos">+0.113</td><td>+0.228</td><td>COOLDOWN</td><td>Initiate cooldown (300s for turbocharger).</td></tr> | |
| <tr><td class="step-num">8</td><td><code>wait</code></td><td class="rew-neg">−0.022</td><td>+0.207</td><td>COOLDOWN</td><td>Allow cooldown to proceed.</td></tr> | |
| <tr><td class="step-num">9</td><td><code>diagnose GEN-1</code></td><td class="rew-neg">−0.032</td><td>+0.175</td><td>cooldown</td><td>Post-shutdown inspection.</td></tr> | |
| <tr class="resolved-row"><td class="step-num">10</td><td><code>acknowledge_alarm</code></td><td class="rew-big">+0.392</td><td><strong>+0.567</strong></td><td>COOLDOWN</td><td><strong>RESOLVED.</strong> Protocol complete. Speed bonus: (15−10)/15 = +0.333.</td></tr> | |
| </tbody> | |
| </table> | |
| <div class="demo-analysis"> | |
| <h4>Protocol Enforcement</h4> | |
| <p>B3 tracks four internal flags that must be set <strong>in order</strong>:</p> | |
| <ul> | |
| <li><strong>_started</strong> — <code style="color:var(--cyan);font-size:0.72rem">start_generator</code> issued</li> | |
| <li><strong>_verified</strong> — <code style="color:var(--cyan);font-size:0.72rem">diagnose GEN-1</code> while generator is running</li> | |
| <li><strong>_stopped</strong> — <code style="color:var(--cyan);font-size:0.72rem">stop_generator</code> (only if started + verified)</li> | |
| <li><strong>_completed</strong> — <code style="color:var(--cyan);font-size:0.72rem">acknowledge_alarm</code> (only if stopped)</li> | |
| </ul> | |
| <p>The agent <strong>cannot skip steps</strong> — issuing <code style="color:var(--cyan);font-size:0.72rem">stop_generator</code> before <code style="color:var(--cyan);font-size:0.72rem">diagnose GEN-1</code> won't set <code style="color:var(--cyan);font-size:0.72rem">_stopped</code>.</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Demo 8: B4 Default --> | |
| <div class="demo-item" id="demo8"> | |
| <div class="demo-header" onclick="toggleDemo('demo8')"> | |
| <div class="demo-header-left"> | |
| <span class="demo-badge">B4</span> | |
| <div class="demo-title-block"> | |
| <h3>Power Failure Cascade — Default</h3> | |
| <div class="demo-subtitle"> | |
| <span>Default Facility (160 kW)</span> | |
| <span>8 steps</span> | |
| <span style="background:var(--red-dim);color:var(--red);padding:0.05rem 0.4rem;border-radius:999px;font-size:0.6rem;font-weight:700">HARD</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="demo-header-right"> | |
| <span class="demo-reward">+0.934</span> | |
| <svg class="demo-chevron" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"/></svg> | |
| </div> | |
| </div> | |
| <div class="demo-body"> | |
| <div class="demo-context"> | |
| Total utility power loss. UPS batteries bridging while generator starts. Generator warmup extended (15s vs default 8s). Agent must manage battery life, consider load shedding, and verify generator operation. | |
| </div> | |
| <table class="demo-steps"> | |
| <thead><tr><th>#</th><th>Command</th><th>Reward</th><th>Cumul</th><th>Key Metrics</th><th>Reasoning</th></tr></thead> | |
| <tbody> | |
| <tr><td class="step-num">1</td><td><code>check_status</code></td><td class="rew-pos">+0.108</td><td>+0.108</td><td>UPS battery, SOC ~97%</td><td>Utility LOST. ATS transferring. Generator auto-starting.</td></tr> | |
| <tr><td class="step-num">2</td><td><code>diagnose UPS-1</code></td><td class="rew-pos">+0.131</td><td>+0.239</td><td>on_battery, SOC=95%</td><td><strong>Resolution gate unlocked.</strong> Battery draining ~2%/step at 160 kW.</td></tr> | |
| <tr><td class="step-num">3</td><td><code>diagnose UPS-2</code></td><td class="rew-pos">+0.078</td><td>+0.317</td><td>on_battery, SOC=90%</td><td>Redundant UPS also on battery.</td></tr> | |
| <tr><td class="step-num">4</td><td><code>start_generator</code></td><td class="rew-neg">−0.007</td><td>+0.310</td><td>Gen: CRANKING</td><td>Gen already auto-starting — slight negative for redundant command.</td></tr> | |
| <tr><td class="step-num">5</td><td><code>set_rack_load A-05 4</code></td><td class="rew-pos">+0.062</td><td>+0.371</td><td>IT: 156 kW</td><td>Shed 4 kW to extend battery life.</td></tr> | |
| <tr><td class="step-num">6</td><td><code>set_rack_load B-05 4</code></td><td class="rew-pos">+0.054</td><td>+0.425</td><td>IT: 152 kW</td><td>Total shed: 8 kW (5% of IT load).</td></tr> | |
| <tr><td class="step-num">7</td><td><code>wait</code></td><td class="rew-neg">−0.052</td><td>+0.373</td><td>Gen LOADED, ATS: GEN</td><td>Generator online. Battery recharging.</td></tr> | |
| <tr class="resolved-row"><td class="step-num">8</td><td><code>diagnose GEN-1</code></td><td class="rew-big">+0.561</td><td><strong>+0.934</strong></td><td>state=loaded</td><td><strong>RESOLVED.</strong> Gen loaded, temps OK, SOC >10%. Speed bonus: (20−8)/20 = +0.600.</td></tr> | |
| </tbody> | |
| </table> | |
| <div class="demo-analysis"> | |
| <h4>Battery SOC Timeline</h4> | |
| <table class="demo-compare"> | |
| <thead><tr><th>Step</th><th>SOC (UPS-1)</th><th>Event</th></tr></thead> | |
| <tbody> | |
| <tr><td>0</td><td>97%</td><td>Utility lost</td></tr> | |
| <tr><td>2</td><td>95%</td><td>Diagnosed</td></tr> | |
| <tr><td>4</td><td>~90%</td><td>Gen starting</td></tr> | |
| <tr><td>6</td><td>~87%</td><td>Load shed</td></tr> | |
| <tr><td>7</td><td>~88%</td><td>Gen loaded, recharging begins</td></tr> | |
| <tr><td>8</td><td>~89%</td><td>Resolved</td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Demo 9: B4 Small --> | |
| <div class="demo-item" id="demo9"> | |
| <div class="demo-header" onclick="toggleDemo('demo9')"> | |
| <div class="demo-header-left"> | |
| <span class="demo-badge">B4</span> | |
| <div class="demo-title-block"> | |
| <h3>Power Failure Cascade — Small</h3> | |
| <div class="demo-subtitle"> | |
| <span>Small Facility (80 kW)</span> | |
| <span>8 steps</span> | |
| <span style="background:var(--red-dim);color:var(--red);padding:0.05rem 0.4rem;border-radius:999px;font-size:0.6rem;font-weight:700">HARD</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="demo-header-right"> | |
| <span class="demo-reward">+0.948</span> | |
| <svg class="demo-chevron" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"/></svg> | |
| </div> | |
| </div> | |
| <div class="demo-body"> | |
| <div class="demo-context"> | |
| Same power loss scenario in a smaller facility: 80 kW IT load, 1 zone, 2 CRACs. Less redundancy means more aggressive load shedding is needed. | |
| </div> | |
| <table class="demo-steps"> | |
| <thead><tr><th>#</th><th>Command</th><th>Reward</th><th>Cumul</th><th>Key Metrics</th><th>Reasoning</th></tr></thead> | |
| <tbody> | |
| <tr><td class="step-num">1</td><td><code>check_status</code></td><td class="rew-pos">+0.111</td><td>+0.111</td><td>1 zone, UPS battery</td><td>80 kW IT, 1 zone, 2 CRACs. Less redundancy.</td></tr> | |
| <tr><td class="step-num">2</td><td><code>diagnose UPS-1</code></td><td class="rew-pos">+0.133</td><td>+0.244</td><td>SOC=91%</td><td>Battery draining faster relative to capacity. Gate unlocked.</td></tr> | |
| <tr><td class="step-num">3</td><td><code>start_generator</code></td><td class="rew-pos">+0.073</td><td>+0.317</td><td>Gen starting</td><td>Explicit start command.</td></tr> | |
| <tr><td class="step-num">4</td><td><code>set_rack_load A-05 4</code></td><td class="rew-pos">+0.066</td><td>+0.383</td><td>IT: 76 kW</td><td>5% load reduction.</td></tr> | |
| <tr><td class="step-num">5</td><td><code>set_rack_load A-04 4</code></td><td class="rew-pos">+0.056</td><td>+0.438</td><td>IT: 72 kW</td><td>10% load reduction.</td></tr> | |
| <tr><td class="step-num">6</td><td><code>set_rack_load A-03 4</code></td><td class="rew-pos">+0.042</td><td>+0.481</td><td>IT: 68 kW</td><td>15% total load shed — more aggressive for smaller facility.</td></tr> | |
| <tr><td class="step-num">7</td><td><code>wait</code></td><td class="rew-neg">−0.069</td><td>+0.412</td><td>Gen LOADED</td><td>Generator online. Battery recharging.</td></tr> | |
| <tr class="resolved-row"><td class="step-num">8</td><td><code>diagnose GEN-1</code></td><td class="rew-big">+0.537</td><td><strong>+0.948</strong></td><td>state=loaded</td><td><strong>RESOLVED.</strong> Speed bonus: +0.600.</td></tr> | |
| </tbody> | |
| </table> | |
| <div class="demo-analysis"> | |
| <h4>Small vs Default Comparison</h4> | |
| <table class="demo-compare"> | |
| <thead><tr><th>Metric</th><th>Default (160 kW)</th><th>Small (80 kW)</th></tr></thead> | |
| <tbody> | |
| <tr><td>Racks shed</td><td>2 (8 kW, 5%)</td><td>3 (12 kW, 15%)</td></tr> | |
| <tr><td>Cumulative reward</td><td>+0.934</td><td>+0.948</td></tr> | |
| </tbody> | |
| </table> | |
| <p style="margin-top:0.5rem">The small facility earns slightly higher reward due to more aggressive proportional load shedding, producing a stronger positive signal from the power safety component.</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Resolution Gate Design --> | |
| <div style="margin-top:2rem;padding:1.5rem;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg)"> | |
| <h3 style="font-size:1rem;font-weight:700;margin-bottom:0.75rem;display:flex;align-items:center;gap:0.5rem"> | |
| <span style="color:var(--red)">⚠</span> Resolution Gate Design | |
| </h3> | |
| <p style="font-size:0.8rem;color:var(--text-dim);margin-bottom:1rem;line-height:1.65"> | |
| Each affected scenario requires the agent to actually <strong style="color:var(--text)">do something</strong> before resolution. Without these gates, scenarios A2, A4, and B4 would auto-resolve within 2–3 steps of passive <code style="color:var(--cyan);background:var(--terminal-bg);padding:0.1rem 0.35rem;border-radius:3px;font-size:0.75rem">wait</code> commands. | |
| </p> | |
| <table class="demo-gate-table"> | |
| <thead><tr><th>Scenario</th><th>Diagnosis Gate</th><th>Min Steps</th></tr></thead> | |
| <tbody> | |
| <tr><td><strong style="color:var(--accent)">A2</strong></td><td>Must <code>diagnose CRAC-3</code></td><td>≥ 8 steps</td></tr> | |
| <tr><td><strong style="color:var(--accent)">A4</strong></td><td>Must <code>diagnose CRAC-1</code> AND <code>diagnose CRAC-3</code></td><td>≥ 8 steps</td></tr> | |
| <tr><td><strong style="color:var(--accent)">B4</strong></td><td>Must <code>diagnose UPS-*</code></td><td>≥ 8 steps</td></tr> | |
| </tbody> | |
| </table> | |
| <p style="font-size:0.78rem;color:var(--text-muted);margin-top:0.75rem;line-height:1.6"> | |
| <strong style="color:var(--text)">Reward ordering validates the design:</strong> fast diagnosis > late diagnosis > no diagnosis (never resolves). | |
| </p> | |
| </div> | |
| </div> | |
| </div><!-- /tabDemos --> | |
| <!-- Guide Tab --> | |
| <div class="guide-page" id="tabGuide"> | |
| <div class="guide-inner"> | |
| <div class="guide-hero"> | |
| <h1>DC<span>-Ops</span> Operations Guide</h1> | |
| <p>A comprehensive reference for operating the physics-based datacenter simulation. Master thermal management, power systems, and incident response.</p> | |
| </div> | |
| <!-- ── Getting Started ── --> | |
| <div class="guide-section"> | |
| <div class="guide-section-header"> | |
| <div class="guide-section-icon blue">▶</div> | |
| <h2>Getting Started</h2> | |
| </div> | |
| <ol> | |
| <li><strong>Select a scenario</strong> from the sidebar — each presents a unique datacenter challenge.</li> | |
| <li><strong>Choose a facility config</strong> (Default 160 kW, Small 80 kW, or Large 600 kW).</li> | |
| <li>Click <strong>Start</strong> to begin the episode. You'll see the NOC dashboard.</li> | |
| <li><strong>Issue commands</strong> in the command bar — diagnose equipment, adjust setpoints, manage power.</li> | |
| <li>Each command advances simulation time. You have a limited <strong>step budget</strong>.</li> | |
| <li>Maximize your <strong>cumulative reward</strong> by resolving the scenario efficiently.</li> | |
| </ol> | |
| <p style="margin-top:0.75rem"><strong>Pro tip:</strong> Always <code style="color:var(--cyan);background:var(--terminal-bg);padding:0.1rem 0.35rem;border-radius:3px;font-family:var(--font-mono);font-size:0.8rem">diagnose</code> before making changes — the reward system gives a bonus for proper diagnostic procedures and penalizes blind interventions.</p> | |
| </div> | |
| <!-- ── Scenarios ── --> | |
| <div class="guide-section"> | |
| <div class="guide-section-header"> | |
| <div class="guide-section-icon orange">⚡</div> | |
| <h2>Scenarios</h2> | |
| </div> | |
| <p>Six operational scenarios across two categories and three difficulty levels:</p> | |
| <h3 style="font-size:0.85rem;font-weight:700;margin:1.25rem 0 0.625rem;color:var(--text)">Thermal (Category A)</h3> | |
| <div class="guide-cards"> | |
| <div class="guide-card"> | |
| <div class="gc-header"> | |
| <span class="gc-id">A1</span> | |
| <span class="gc-diff" style="background:var(--green-dim);color:var(--green)">Easy</span> | |
| </div> | |
| <div class="gc-name">Cooling Setpoint Optimization</div> | |
| <div class="gc-desc">CRACs are overcooling at 15°C — wasting energy. Optimize setpoints for efficiency while keeping all zones within ASHRAE recommended range (18–27°C).</div> | |
| <div class="gc-hint"><strong>Strategy</strong>Raise setpoints to ~22°C. Monitor temps. Target PUE < 1.6. Check that all zones stay in recommended range for 2+ steps.</div> | |
| </div> | |
| <div class="guide-card"> | |
| <div class="gc-header"> | |
| <span class="gc-id">A2</span> | |
| <span class="gc-diff" style="background:var(--yellow-dim);color:var(--yellow)">Medium</span> | |
| </div> | |
| <div class="gc-name">Thermal Event Response</div> | |
| <div class="gc-desc">CRAC-3 compressor failure. Zone B temps are rising. Diagnose the fault and redistribute cooling to stabilize all zones.</div> | |
| <div class="gc-hint"><strong>Strategy</strong>Diagnose CRAC-3 first. Lower setpoints on remaining CRACs. Boost fan speeds. Keep all zones in recommended range for 2+ steps.</div> | |
| </div> | |
| <div class="guide-card"> | |
| <div class="gc-header"> | |
| <span class="gc-id">A4</span> | |
| <span class="gc-diff" style="background:var(--red-dim);color:var(--red)">Hard</span> | |
| </div> | |
| <div class="gc-name">CRAC Failure Cascade</div> | |
| <div class="gc-desc">CRAC-1 compressor failure and CRAC-3 fan failure simultaneously. A cascading thermal event threatens multiple zones.</div> | |
| <div class="gc-hint"><strong>Strategy</strong>Diagnose both CRACs. Aggressively lower setpoints on CRAC-2/4. Max fan speeds. Consider load shedding on hot racks. Keep zones in allowable range.</div> | |
| </div> | |
| </div> | |
| <h3 style="font-size:0.85rem;font-weight:700;margin:1.25rem 0 0.625rem;color:var(--text)">Power (Category B)</h3> | |
| <div class="guide-cards"> | |
| <div class="guide-card"> | |
| <div class="gc-header"> | |
| <span class="gc-id">B1</span> | |
| <span class="gc-diff" style="background:var(--yellow-dim);color:var(--yellow)">Medium</span> | |
| </div> | |
| <div class="gc-name">UPS Alarm Response</div> | |
| <div class="gc-desc">UPS transferred to battery after a utility event (now restored). Diagnose the situation and acknowledge the alarm to resolve.</div> | |
| <div class="gc-hint"><strong>Strategy</strong>Diagnose UPS-1 first. Verify utility is restored. Acknowledge the alarm. The UPS should return to normal operation.</div> | |
| </div> | |
| <div class="guide-card"> | |
| <div class="gc-header"> | |
| <span class="gc-id">B3</span> | |
| <span class="gc-diff" style="background:var(--green-dim);color:var(--green)">Easy</span> | |
| </div> | |
| <div class="gc-name">Generator Test Protocol</div> | |
| <div class="gc-desc">Routine monthly generator test. Follow the proper 5-step protocol: diagnose → start → verify → stop → confirm shutdown.</div> | |
| <div class="gc-hint"><strong>Strategy</strong>1. diagnose GEN-1 → 2. start_generator → 3. wait (let it warm) → 4. diagnose GEN-1 (verify running) → 5. stop_generator</div> | |
| </div> | |
| <div class="guide-card"> | |
| <div class="gc-header"> | |
| <span class="gc-id">B4</span> | |
| <span class="gc-diff" style="background:var(--red-dim);color:var(--red)">Hard</span> | |
| </div> | |
| <div class="gc-name">Power Failure Cascade</div> | |
| <div class="gc-desc">Utility power lost with extended generator warmup. UPS running on battery. Manage battery life and thermal conditions until generator loads.</div> | |
| <div class="gc-hint"><strong>Strategy</strong>Start generator immediately. Shed non-critical rack loads to preserve battery. Monitor SOC. Once generator loads, restore loads. Keep temps stable.</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ── Available Commands ── --> | |
| <div class="guide-section"> | |
| <div class="guide-section-header"> | |
| <div class="guide-section-icon cyan">⌨</div> | |
| <h2>Available Commands</h2> | |
| </div> | |
| <table class="guide-table"> | |
| <thead> | |
| <tr> | |
| <th>Command</th> | |
| <th>Description</th> | |
| <th>Example</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td><code>diagnose <unit></code></td> | |
| <td>Inspect a CRAC, UPS, Generator, or PDU for faults and status</td> | |
| <td><code>diagnose CRAC-3</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>adjust_setpoint <crac> <°C></code></td> | |
| <td>Change CRAC supply air setpoint (10–35°C). Supply temp converges over ~30s.</td> | |
| <td><code>adjust_setpoint CRAC-1 22</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>set_fan_speed <crac> <%></code></td> | |
| <td>Set CRAC fan speed (0–100%). Fan power follows cubic law.</td> | |
| <td><code>set_fan_speed CRAC-2 100</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>set_rack_load <rack> <kW></code></td> | |
| <td>Adjust rack IT load (0–30 kW) — simulates workload migration.</td> | |
| <td><code>set_rack_load B-05 4</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>start_crac <crac></code></td> | |
| <td>Start a standby CRAC unit.</td> | |
| <td><code>start_crac CRAC-3</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>stop_crac <crac></code></td> | |
| <td>Put a CRAC into standby mode.</td> | |
| <td><code>stop_crac CRAC-4</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>start_generator</code></td> | |
| <td>Initiate diesel generator start sequence (OFF → CRANKING → WARMING → READY → LOADED).</td> | |
| <td><code>start_generator</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>stop_generator</code></td> | |
| <td>Initiate generator cooldown sequence (300s).</td> | |
| <td><code>stop_generator</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>set_ups_mode <ups> <mode></code></td> | |
| <td>Set UPS mode: <code>eco</code>, <code>double_conversion</code>, <code>line_interactive</code>, or <code>bypass</code>.</td> | |
| <td><code>set_ups_mode UPS-1 eco</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>refuel_generator [liters]</code></td> | |
| <td>Refuel the generator. Omit liters to fill tank.</td> | |
| <td><code>refuel_generator 500</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>acknowledge_alarm</code></td> | |
| <td>Acknowledge the current alert — clears the alert banner.</td> | |
| <td><code>acknowledge_alarm</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>check_status</code></td> | |
| <td>Request full status report. Refreshes the dashboard.</td> | |
| <td><code>check_status</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>escalate</code></td> | |
| <td>Escalate to senior engineer. Ends the episode.</td> | |
| <td><code>escalate</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>wait</code></td> | |
| <td>Take no action — advances simulation time by one step.</td> | |
| <td><code>wait</code></td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| <!-- ── Reward System ── --> | |
| <div class="guide-section"> | |
| <div class="guide-section-header"> | |
| <div class="guide-section-icon green">★</div> | |
| <h2>Reward System</h2> | |
| </div> | |
| <p>The environment uses a <strong>6-component, research-informed</strong> reward function. Each component is bounded to [−1, 1]. The total reward is a weighted sum, clamped to [−1, 1]. Weights auto-adjust based on scenario type.</p> | |
| <div class="reward-grid"> | |
| <div class="reward-card"> | |
| <h4>🌡️ Thermal Safety <span class="rc-range">[−1, +0.1]</span></h4> | |
| <p>Dual softplus barriers at ASHRAE recommended and allowable limits. Violations are penalized smoothly — the closer to the limit, the stronger the gradient. Returns <strong>+0.1 baseline</strong> when all zones are ≥3°C below recommended max (DCRL-Green).</p> | |
| <div class="rc-formula">penalty = softplus((T − T_rec) / 2.0) + 3.0 · softplus((T − T_allow) / 1.5)</div> | |
| </div> | |
| <div class="reward-card"> | |
| <h4>⚡ Power Safety <span class="rc-range">[−1, 0]</span></h4> | |
| <p>Penalizes low UPS battery state-of-charge (SOC) via softplus barrier at 50% threshold. UPS fault adds a fixed penalty of 5.0. Compounds across multiple UPS units.</p> | |
| <div class="rc-formula">penalty = softplus((0.5 − SOC) / 0.15) + 5.0 · [fault]</div> | |
| </div> | |
| <div class="reward-card"> | |
| <h4>📊 Efficiency <span class="rc-range">[−1, 0]</span></h4> | |
| <p>PUE-based energy efficiency. PUE 1.0 (ideal) → 0, PUE 2.0 → −0.46, PUE 3.0 → −0.76. <strong>Suppressed to 0</strong> during power emergencies (UPS on battery or fault) so the agent isn't penalized for correct load shedding.</p> | |
| <div class="rc-formula">reward = −tanh((PUE − 1.0) / 2.0)</div> | |
| </div> | |
| <div class="reward-card"> | |
| <h4>🎯 Scenario Progress <span class="rc-range">[−1, +1]</span></h4> | |
| <p>Delta-based: rewards the <em>change</em> in progress. This provides credit assignment — only the action that actually caused forward progress gets rewarded. Each scenario defines a normalized [0, 1] progress metric.</p> | |
| <div class="rc-formula">reward = progress_now − progress_prev</div> | |
| </div> | |
| <div class="reward-card"> | |
| <h4>📋 Procedure <span class="rc-range">[−1, +1]</span></h4> | |
| <p>Scenario-defined procedural correctness rules. For example, diagnosing before adjusting setpoints earns a bonus (+0.2), while skipping diagnosis incurs a penalty (−0.1). Encourages proper operational procedures.</p> | |
| <div class="rc-formula">reward = scenario.procedure_reward (clamped)</div> | |
| </div> | |
| <div class="reward-card"> | |
| <h4>🎮 Action Quality <span class="rc-range">[−1, +1]</span></h4> | |
| <p>Context-aware assessment: <strong>−0.5</strong> invalid command, <strong>−0.2</strong> repeat (except <code style="font-size:0.7rem">wait</code>/<code style="font-size:0.7rem">check_status</code>), <strong>+0.3</strong> diagnose/check_status, <strong>+0.2</strong> interventions, <strong>+0.1</strong> acknowledge, <strong>−0.1</strong> escalate. Waiting during generator startup: +0.1.</p> | |
| <div class="rc-formula">Heuristic scoring per action type + context</div> | |
| </div> | |
| </div> | |
| <h3 style="font-size:0.85rem;font-weight:700;margin:1.5rem 0 0.75rem;color:var(--text)">Weight Profiles</h3> | |
| <p>Weights auto-select based on scenario type. Components sum to 1.0.</p> | |
| <div class="weight-profiles" id="weightProfiles"></div> | |
| </div> | |
| <!-- ── ASHRAE Guidelines ── --> | |
| <div class="guide-section"> | |
| <div class="guide-section-header"> | |
| <div class="guide-section-icon yellow">🏛</div> | |
| <h2>ASHRAE Thermal Guidelines</h2> | |
| </div> | |
| <p>All safety thresholds follow <strong>ASHRAE TC 9.9, 5th Edition (2021)</strong>. The <span style="color:var(--green)">recommended</span> range is optimal for equipment longevity. The <span style="color:var(--yellow)">allowable</span> range permits short-term operation during incidents.</p> | |
| <table class="guide-table"> | |
| <thead> | |
| <tr> | |
| <th>Class</th> | |
| <th>Recommended</th> | |
| <th>Allowable</th> | |
| <th>Application</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td><strong>A1</strong></td> | |
| <td><span style="color:var(--green)">18–27°C</span></td> | |
| <td><span style="color:var(--yellow)">15–32°C</span></td> | |
| <td>Enterprise servers</td> | |
| </tr> | |
| <tr> | |
| <td><strong>A2</strong></td> | |
| <td><span style="color:var(--green)">18–27°C</span></td> | |
| <td><span style="color:var(--yellow)">10–35°C</span></td> | |
| <td>Volume servers (most common)</td> | |
| </tr> | |
| <tr> | |
| <td><strong>A3</strong></td> | |
| <td><span style="color:var(--green)">18–27°C</span></td> | |
| <td><span style="color:var(--yellow)">5–40°C</span></td> | |
| <td>Extended temperature range</td> | |
| </tr> | |
| <tr> | |
| <td><strong>A4</strong></td> | |
| <td><span style="color:var(--green)">18–27°C</span></td> | |
| <td><span style="color:var(--yellow)">5–45°C</span></td> | |
| <td>Maximum flexibility</td> | |
| </tr> | |
| <tr> | |
| <td><strong>H1</strong></td> | |
| <td><span style="color:var(--green)">18–22°C</span></td> | |
| <td><span style="color:var(--yellow)">5–25°C</span></td> | |
| <td>High-density / AI / HPC (GPU servers)</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| <p style="margin-top:0.75rem;font-size:0.78rem;color:var(--text-dim)"><strong>Key insight:</strong> The reward system uses softplus barriers at both recommended and allowable limits. Staying ≥3°C below recommended max yields a +0.1 thermal safety bonus. Exceeding allowable limits incurs 3× the per-degree penalty of recommended violations.</p> | |
| </div> | |
| <!-- ── Physics Engine ── --> | |
| <div class="guide-section"> | |
| <div class="guide-section-header"> | |
| <div class="guide-section-icon red">⚙</div> | |
| <h2>Physics Engine</h2> | |
| </div> | |
| <h3 style="font-size:0.85rem;font-weight:700;margin:0.75rem 0 0.5rem;color:var(--text)">Thermal Model — RC Network</h3> | |
| <p>The simulation uses a <strong>lumped-capacitance RC thermal network</strong> — the standard approach for datacenter transient thermal analysis. Each zone's temperature evolves according to:</p> | |
| <div class="guide-code">C_total · dT/dt = Q_IT − Q_cooling + Q_envelope + Q_internal | |
| Where: | |
| C_total = C_air + C_equipment (dominated by server thermal mass) | |
| Q_IT = Σ rack IT loads [W] — all electrical power converts to heat | |
| Q_cooling = Σ CRAC outputs [W] — capacity varies with return air temp | |
| Q_envelope = (T_outside − T_zone) / R_envelope [W]</div> | |
| <p>Important CRAC characteristics:</p> | |
| <ul> | |
| <li><strong>Capacity vs. return temp:</strong> Q_actual = Q_rated × [1 + 0.03 × (T_return − T_rated)], so capacity increases when a zone heats up</li> | |
| <li><strong>Fan power:</strong> Cubic law (affinity laws) — P_fan = P_rated × (speed%)³</li> | |
| <li><strong>Supply temp lag:</strong> 30-second time constant between setpoint change and actual supply temp</li> | |
| <li><strong>Recirculation:</strong> Hot air mixing caused by dominant airflow imbalance</li> | |
| </ul> | |
| <h3 style="font-size:0.85rem;font-weight:700;margin:1.25rem 0 0.5rem;color:var(--text)">Power Model</h3> | |
| <p><strong>UPS quadratic loss model</strong> (APC White Paper 108):</p> | |
| <div class="guide-code">η(x) = x / (x + 0.013 + 0.006x + 0.011x²) | |
| 90.5% efficient at 25% load | |
| 93.6% efficient at 50% load | |
| 94.0% efficient at 75% load</div> | |
| <p><strong>Battery discharge:</strong> SOC depletes based on load, UPS efficiency, and temperature derating.</p> | |
| <h3 style="font-size:0.85rem;font-weight:700;margin:1.25rem 0 0.5rem;color:var(--text)">Generator State Machine</h3> | |
| <div class="guide-code">OFF ─→ START_DELAY (4s) ─→ CRANKING (5s) ─→ WARMING (8s) ─→ READY ─→ LOADED | |
| ↓ | |
| COOLDOWN (300s) ─→ OFF</div> | |
| <p>ATS (Automatic Transfer Switch) performs mechanical transfer in 100ms. Retransfer delay is 300 seconds to prevent rapid switching.</p> | |
| </div> | |
| <!-- ── Research References ── --> | |
| <div class="guide-section"> | |
| <div class="guide-section-header"> | |
| <div class="guide-section-icon blue">📚</div> | |
| <h2>Research Foundation</h2> | |
| </div> | |
| <ul> | |
| <li><strong>Google/DeepMind (2017):</strong> Demonstrated 40% cooling energy reduction using RL with softplus barrier functions for safety constraints.</li> | |
| <li><strong>DCRL-Green (ICLR 2025):</strong> Multi-objective reward with softplus barriers and positive safe-state baseline for safe RL in datacenters.</li> | |
| <li><strong>ASHRAE TC 9.9, 5th Edition (2021):</strong> Industry-standard thermal guidelines used for all safety thresholds.</li> | |
| <li><strong>APC White Paper 108:</strong> UPS quadratic loss model with experimentally calibrated coefficients.</li> | |
| <li><strong>Process Reward Models:</strong> Delta-based progress rewards for improved credit assignment in multi-step reasoning.</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div><!-- /tabGuide --> | |
| </div> | |
| <script> | |
| // ─── State ─────────────────────────────────────────────────────────── | |
| let selectedScenario = null; | |
| let episodeActive = false; | |
| let stepCount = 0; | |
| let maxSteps = 0; | |
| let cumulativeReward = 0; | |
| let rewardEntries = []; | |
| let isProcessing = false; | |
| let ws = null; | |
| let pendingResolve = null; // For awaiting WS responses | |
| const BASE_URL = window.location.origin; | |
| // ─── Scenario metadata ────────────────────────────────────────────── | |
| const SCENARIOS = { | |
| A1: { name: 'Cooling Setpoint Optimization', type: 'thermal', diff: 'Easy' }, | |
| A2: { name: 'Thermal Event Response', type: 'thermal', diff: 'Medium' }, | |
| A4: { name: 'CRAC Failure Cascade', type: 'thermal', diff: 'Hard' }, | |
| B1: { name: 'UPS Alarm Response', type: 'power', diff: 'Medium' }, | |
| B3: { name: 'Generator Test Protocol', type: 'power', diff: 'Easy' }, | |
| B4: { name: 'Power Failure Cascade', type: 'power', diff: 'Hard' }, | |
| }; | |
| // Scenario-adaptive quick action definitions | |
| const QUICK_ACTIONS = { | |
| _common: ['check_status', 'wait', 'acknowledge_alarm', 'escalate'], | |
| A1: ['adjust_setpoint CRAC-1 22', 'adjust_setpoint CRAC-2 22', 'adjust_setpoint CRAC-3 22', 'adjust_setpoint CRAC-4 22', 'diagnose CRAC-1'], | |
| A2: ['diagnose CRAC-3', 'diagnose CRAC-1', 'adjust_setpoint CRAC-1 20', 'adjust_setpoint CRAC-2 20', 'set_fan_speed CRAC-1 100', 'set_fan_speed CRAC-2 100'], | |
| A4: ['diagnose CRAC-1', 'diagnose CRAC-3', 'adjust_setpoint CRAC-2 16', 'adjust_setpoint CRAC-4 16', 'set_fan_speed CRAC-2 100', 'set_fan_speed CRAC-4 100', 'set_rack_load B-05 4'], | |
| B1: ['diagnose UPS-1', 'diagnose GEN-1', 'start_generator', 'stop_generator'], | |
| B3: ['start_generator', 'diagnose GEN-1', 'stop_generator'], | |
| B4: ['diagnose UPS-1', 'diagnose GEN-1', 'start_generator', 'set_rack_load A-05 4', 'set_rack_load B-05 4'], | |
| }; | |
| function buildQuickActions(scenarioId) { | |
| const container = document.getElementById('quickActions'); | |
| container.innerHTML = ''; | |
| const specific = QUICK_ACTIONS[scenarioId] || []; | |
| const common = QUICK_ACTIONS._common; | |
| const all = [...specific, ...common]; | |
| for (const cmd of all) { | |
| const btn = document.createElement('button'); | |
| btn.className = 'quick-btn'; | |
| btn.disabled = !episodeActive; | |
| // Short display label | |
| let label = cmd; | |
| if (cmd === 'acknowledge_alarm') label = 'ack_alarm'; | |
| else if (cmd === 'check_status') label = 'check_status'; | |
| else if (cmd === 'start_generator') label = 'start_gen'; | |
| else if (cmd === 'stop_generator') label = 'stop_gen'; | |
| btn.textContent = label; | |
| btn.onclick = () => quickCmd(cmd); | |
| container.appendChild(btn); | |
| } | |
| } | |
| // ─── WebSocket connection ──────────────────────────────────────────── | |
| function connectWebSocket() { | |
| return new Promise((resolve, reject) => { | |
| const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
| const wsUrl = `${wsProtocol}//${window.location.host}/ws`; | |
| ws = new WebSocket(wsUrl); | |
| ws.onopen = () => { | |
| setStatus('connected'); | |
| resolve(); | |
| }; | |
| ws.onmessage = (event) => { | |
| const msg = JSON.parse(event.data); | |
| if (pendingResolve) { | |
| const resolver = pendingResolve; | |
| pendingResolve = null; | |
| resolver(msg); | |
| } | |
| }; | |
| ws.onerror = (err) => { | |
| setStatus('disconnected'); | |
| reject(new Error('WebSocket connection failed')); | |
| }; | |
| ws.onclose = () => { | |
| setStatus('disconnected'); | |
| ws = null; | |
| if (episodeActive) { | |
| episodeActive = false; | |
| setControlsEnabled(false); | |
| showActionResult('WebSocket disconnected. Reset to reconnect.', 'error'); | |
| } | |
| }; | |
| }); | |
| } | |
| function wsSend(message) { | |
| return new Promise((resolve, reject) => { | |
| if (!ws || ws.readyState !== WebSocket.OPEN) { | |
| reject(new Error('WebSocket not connected')); | |
| return; | |
| } | |
| pendingResolve = resolve; | |
| ws.send(JSON.stringify(message)); | |
| // Timeout after 30s | |
| setTimeout(() => { | |
| if (pendingResolve === resolve) { | |
| pendingResolve = null; | |
| reject(new Error('WebSocket request timed out')); | |
| } | |
| }, 30000); | |
| }); | |
| } | |
| function closeWebSocket() { | |
| if (ws) { | |
| ws.close(); | |
| ws = null; | |
| } | |
| pendingResolve = null; | |
| } | |
| // ─── UI helpers ────────────────────────────────────────────────────── | |
| function selectScenario(id) { | |
| if (episodeActive) return; | |
| selectedScenario = id; | |
| document.querySelectorAll('.scenario-card').forEach(c => c.classList.remove('active')); | |
| const card = document.querySelector(`.scenario-card[data-id="${id}"]`); | |
| if (card) card.classList.add('active'); | |
| const btn = document.getElementById('startBtn'); | |
| btn.disabled = false; | |
| btn.textContent = `Start ${id}: ${SCENARIOS[id].name}`; | |
| buildQuickActions(id); | |
| } | |
| function togglePanel(id) { | |
| const panel = document.getElementById(id); | |
| panel.classList.toggle('collapsed'); | |
| const btnId = id === 'sidebar' ? 'toggleScenarios' : 'toggleMetrics'; | |
| document.getElementById(btnId).classList.toggle('active'); | |
| } | |
| function setControlsEnabled(enabled) { | |
| document.getElementById('commandInput').disabled = !enabled; | |
| document.getElementById('sendBtn').disabled = !enabled; | |
| document.querySelectorAll('.quick-btn').forEach(b => b.disabled = !enabled); | |
| // Rebuild quick actions if scenario changed while disabled | |
| if (enabled && selectedScenario) buildQuickActions(selectedScenario); | |
| } | |
| function quickCmd(cmd) { | |
| if (!episodeActive || isProcessing) return; | |
| document.getElementById('commandInput').value = cmd; | |
| sendCommand(); | |
| } | |
| function showActionResult(msg, type) { | |
| const el = document.getElementById('actionResult'); | |
| el.style.display = 'block'; | |
| el.textContent = msg; | |
| el.className = 'action-result ' + type; | |
| } | |
| function setStatus(state) { | |
| const badge = document.getElementById('statusBadge'); | |
| const text = document.getElementById('statusText'); | |
| badge.className = 'status-badge ' + state; | |
| text.textContent = state === 'connected' ? 'Connected' : | |
| state === 'loading' ? 'Loading...' : 'Disconnected'; | |
| } | |
| // ─── Dashboard text parsing ────────────────────────────────────────── | |
| function parseDashboard(dashboard) { | |
| const metrics = {}; | |
| // PUE | |
| const pueMatch = dashboard.match(/PUE:\s+([\d.]+)/); | |
| if (pueMatch) metrics.pue = parseFloat(pueMatch[1]); | |
| // IT Load | |
| const itMatch = dashboard.match(/IT Load:\s+([\d.]+)\s*kW/); | |
| if (itMatch) metrics.itLoad = parseFloat(itMatch[1]); | |
| // Cooling | |
| const coolMatch = dashboard.match(/Cooling:\s+([\d.]+)\s*kW/); | |
| if (coolMatch) metrics.cooling = parseFloat(coolMatch[1]); | |
| // Outside temp | |
| const outMatch = dashboard.match(/Outside:\s+([\d.]+)°C/); | |
| if (outMatch) metrics.outside = parseFloat(outMatch[1]); | |
| // Zone temperatures | |
| metrics.zones = []; | |
| const zoneRegex = /(zone_\w+)\s+([\d.]+)°C\s+([\d.]+)°C\s+([\d.]+)°C/g; | |
| let zm; | |
| while ((zm = zoneRegex.exec(dashboard)) !== null) { | |
| metrics.zones.push({ | |
| id: zm[1], | |
| cold: parseFloat(zm[2]), | |
| hot: parseFloat(zm[3]), | |
| inlet: parseFloat(zm[4]) | |
| }); | |
| } | |
| // Power info | |
| const utilMatch = dashboard.match(/Utility:\s+(\w+)/); | |
| if (utilMatch) metrics.utility = utilMatch[1]; | |
| const genMatch = dashboard.match(/Gen:\s+([^\n|]+)/); | |
| if (genMatch) metrics.generator = genMatch[1].trim(); | |
| const atsMatch = dashboard.match(/ATS:\s+(\w+)/); | |
| if (atsMatch) metrics.ats = atsMatch[1]; | |
| const upsMatch = dashboard.match(/UPS:\s+(.+)/); | |
| if (upsMatch) metrics.ups = upsMatch[1].trim(); | |
| return metrics; | |
| } | |
| // ─── Start episode ─────────────────────────────────────────────────── | |
| async function startEpisode() { | |
| if (!selectedScenario || isProcessing) return; | |
| isProcessing = true; | |
| const btn = document.getElementById('startBtn'); | |
| btn.disabled = true; | |
| btn.innerHTML = '<span class="spinner"></span> Starting...'; | |
| setStatus('loading'); | |
| try { | |
| // Close any existing WebSocket connection | |
| closeWebSocket(); | |
| // Open a fresh WebSocket session (each WS gets its own env instance) | |
| await connectWebSocket(); | |
| // Send reset via WebSocket | |
| const resetData = { scenario: selectedScenario }; | |
| const configName = document.getElementById('configSelect').value; | |
| if (configName && configName !== 'default') { | |
| resetData.config_name = configName; | |
| } | |
| const resp = await wsSend({ type: 'reset', data: resetData }); | |
| if (resp.type === 'error') { | |
| throw new Error(resp.data?.message || 'Reset failed'); | |
| } | |
| // resp: { type: "observation", data: { observation: {...}, reward: float, done: bool } } | |
| episodeActive = true; | |
| stepCount = 0; | |
| cumulativeReward = 0; | |
| rewardEntries = []; | |
| processResponse(resp.data); | |
| setControlsEnabled(true); | |
| document.getElementById('startBtn').style.display = 'none'; | |
| document.getElementById('resetBtn').style.display = 'block'; | |
| document.getElementById('doneBanner').classList.remove('show'); | |
| document.getElementById('rewardHistory').innerHTML = '<div class="no-data">No steps yet</div>'; | |
| document.getElementById('actionResult').style.display = 'none'; | |
| const info = SCENARIOS[selectedScenario]; | |
| document.getElementById('metaScenario').textContent = `${selectedScenario} - ${info.name}`; | |
| } catch (e) { | |
| setStatus('disconnected'); | |
| showActionResult('Failed to start: ' + e.message, 'error'); | |
| btn.disabled = false; | |
| btn.textContent = `Start ${selectedScenario}: ${SCENARIOS[selectedScenario].name}`; | |
| closeWebSocket(); | |
| } finally { | |
| isProcessing = false; | |
| } | |
| } | |
| // ─── Reset episode ─────────────────────────────────────────────────── | |
| function resetEpisode() { | |
| episodeActive = false; | |
| setControlsEnabled(false); | |
| closeWebSocket(); | |
| document.getElementById('startBtn').style.display = 'block'; | |
| document.getElementById('startBtn').disabled = false; | |
| document.getElementById('startBtn').textContent = | |
| selectedScenario ? `Start ${selectedScenario}: ${SCENARIOS[selectedScenario].name}` : 'Select a Scenario'; | |
| document.getElementById('resetBtn').style.display = 'none'; | |
| document.getElementById('doneBanner').classList.remove('show'); | |
| document.getElementById('terminalTitle').textContent = 'dc-ops-console'; | |
| setStatus('disconnected'); | |
| } | |
| // ─── Send command ──────────────────────────────────────────────────── | |
| async function sendCommand() { | |
| const input = document.getElementById('commandInput'); | |
| const cmd = input.value.trim(); | |
| if (!cmd || !episodeActive || isProcessing) return; | |
| input.value = ''; | |
| isProcessing = true; | |
| setControlsEnabled(false); | |
| const sendBtn = document.getElementById('sendBtn'); | |
| sendBtn.disabled = false; | |
| sendBtn.innerHTML = '<span class="spinner"></span>'; | |
| try { | |
| // WebSocket step: { type: "step", data: { command: "...", reasoning: "" } } | |
| const resp = await wsSend({ | |
| type: 'step', | |
| data: { command: cmd, reasoning: '' } | |
| }); | |
| if (resp.type === 'error') { | |
| throw new Error(resp.data?.message || 'Step failed'); | |
| } | |
| stepCount++; | |
| processResponse(resp.data, cmd); | |
| } catch (e) { | |
| showActionResult('Error: ' + e.message, 'error'); | |
| } finally { | |
| isProcessing = false; | |
| sendBtn.textContent = 'Send'; | |
| if (episodeActive) { | |
| setControlsEnabled(true); | |
| input.focus(); | |
| } | |
| } | |
| } | |
| // ─── Process API response ──────────────────────────────────────────── | |
| function processResponse(data, command = null) { | |
| // Response format: { observation: {...}, reward: float|null, done: bool } | |
| const obs = data.observation || {}; | |
| const reward = data.reward || 0; | |
| const done = data.done || false; | |
| // ── Dashboard display ── | |
| const dashEl = document.getElementById('dashboardOutput'); | |
| const dashboard = obs.dashboard || ''; | |
| if (dashboard) { | |
| dashEl.textContent = dashboard; | |
| } | |
| // Auto-scroll dashboard to bottom | |
| const container = document.getElementById('dashboardContainer'); | |
| container.scrollTop = container.scrollHeight; | |
| // ── Action result ── | |
| if (obs.action_result && command) { | |
| const isErr = /error|invalid|unknown|unrecognized|fail/i.test(obs.action_result); | |
| showActionResult(obs.action_result, isErr ? 'error' : 'success'); | |
| } | |
| // ── Parse metrics from dashboard text ── | |
| const metrics = parseDashboard(dashboard); | |
| // ── Steps ── | |
| const stepsRemaining = obs.steps_remaining || 0; | |
| maxSteps = stepsRemaining + stepCount; | |
| document.getElementById('metaStep').textContent = stepCount; | |
| document.getElementById('metaMaxSteps').textContent = maxSteps; | |
| const pct = maxSteps > 0 ? (stepCount / maxSteps) * 100 : 0; | |
| const progEl = document.getElementById('stepProgress'); | |
| progEl.style.width = pct + '%'; | |
| progEl.className = 'progress-fill ' + (pct < 50 ? 'low' : pct < 80 ? 'mid' : 'high'); | |
| // ── Cumulative reward ── | |
| cumulativeReward += reward; | |
| const cumEl = document.getElementById('metaCumReward'); | |
| cumEl.textContent = cumulativeReward.toFixed(2); | |
| cumEl.style.color = cumulativeReward > 0 ? 'var(--green)' : cumulativeReward < -0.5 ? 'var(--red)' : 'var(--text)'; | |
| // ── Key metrics from parsed dashboard ── | |
| if (metrics.pue !== undefined) { | |
| const el = document.getElementById('metricPUE'); | |
| el.textContent = metrics.pue.toFixed(2); | |
| el.className = 'metric-value ' + (metrics.pue < 1.5 ? 'good' : metrics.pue < 1.8 ? 'warn' : 'danger'); | |
| } | |
| if (metrics.itLoad !== undefined) { | |
| document.getElementById('metricIT').textContent = metrics.itLoad.toFixed(0) + ' kW'; | |
| document.getElementById('metricIT').className = 'metric-value neutral'; | |
| } | |
| if (metrics.cooling !== undefined) { | |
| document.getElementById('metricCooling').textContent = metrics.cooling.toFixed(0) + ' kW'; | |
| document.getElementById('metricCooling').className = 'metric-value neutral'; | |
| } | |
| if (metrics.outside !== undefined) { | |
| document.getElementById('metricOutside').textContent = metrics.outside.toFixed(0) + '°C'; | |
| document.getElementById('metricOutside').className = 'metric-value neutral'; | |
| } | |
| // ── Zone temperature bars ── | |
| if (metrics.zones && metrics.zones.length > 0) { | |
| updateZoneBars(metrics.zones); | |
| } | |
| // ── Power info ── | |
| updatePowerInfo(metrics); | |
| // ── Reward history ── | |
| if (command) { | |
| rewardEntries.push({ step: stepCount, cmd: command, reward: reward }); | |
| updateRewardHistory(); | |
| } | |
| // ── Terminal title ── | |
| document.getElementById('terminalTitle').textContent = | |
| `dc-ops — ${selectedScenario} — step ${stepCount}/${maxSteps}`; | |
| // ── Episode done ── | |
| if (done) { | |
| episodeActive = false; | |
| setControlsEnabled(false); | |
| const banner = document.getElementById('doneBanner'); | |
| banner.classList.add('show'); | |
| const alert = obs.alert || ''; | |
| if (alert.toLowerCase().includes('resolved') || alert.toLowerCase().includes('success') || | |
| alert.toLowerCase().includes('complete')) { | |
| banner.className = 'episode-done-banner show resolved'; | |
| banner.textContent = 'Scenario Resolved Successfully'; | |
| } else if (alert.toLowerCase().includes('critical') || alert.toLowerCase().includes('emergency') || | |
| alert.toLowerCase().includes('shutdown')) { | |
| banner.className = 'episode-done-banner show failed'; | |
| banner.textContent = 'Episode Ended — Critical Failure'; | |
| } else { | |
| banner.className = 'episode-done-banner show timeout'; | |
| banner.textContent = `Episode Ended — ${stepCount >= maxSteps ? 'Budget exhausted' : 'Terminated'}`; | |
| } | |
| } | |
| } | |
| // ─── Zone bars ─────────────────────────────────────────────────────── | |
| function updateZoneBars(zones) { | |
| const container = document.getElementById('zoneBars'); | |
| container.innerHTML = ''; | |
| for (const z of zones) { | |
| const temp = z.inlet; | |
| const pct = Math.max(0, Math.min(100, ((temp - 15) / 30) * 100)); | |
| const cls = temp <= 27 ? 'safe' : temp <= 35 ? 'warning' : 'critical'; | |
| const colorVar = cls === 'safe' ? '--green' : cls === 'warning' ? '--yellow' : '--red'; | |
| const label = z.id.replace('zone_', '').toUpperCase(); | |
| const row = document.createElement('div'); | |
| row.className = 'zone-bar-row'; | |
| row.innerHTML = ` | |
| <span class="zone-bar-label">${label}</span> | |
| <div class="zone-bar-track"> | |
| <div class="zone-bar-fill ${cls}" style="width:${pct}%"></div> | |
| </div> | |
| <span class="zone-bar-value" style="color:var(${colorVar})">${temp.toFixed(1)}°C</span>`; | |
| container.appendChild(row); | |
| } | |
| } | |
| // ─── Power info ────────────────────────────────────────────────────── | |
| function updatePowerInfo(metrics) { | |
| const container = document.getElementById('powerInfo'); | |
| let html = ''; | |
| if (metrics.utility) { | |
| const cls = metrics.utility === 'NORMAL' ? 'ok' : 'bad'; | |
| html += `<div class="power-row"><span class="pw-label">Utility</span><span class="pw-val ${cls}">${metrics.utility}</span></div>`; | |
| } | |
| if (metrics.generator) { | |
| const cls = metrics.generator.startsWith('OFF') ? 'ok' : | |
| metrics.generator.startsWith('LOADED') ? 'warn' : 'warn'; | |
| html += `<div class="power-row"><span class="pw-label">Generator</span><span class="pw-val ${cls}">${metrics.generator}</span></div>`; | |
| } | |
| if (metrics.ats) { | |
| const cls = metrics.ats === 'UTILITY' ? 'ok' : 'warn'; | |
| html += `<div class="power-row"><span class="pw-label">ATS</span><span class="pw-val ${cls}">${metrics.ats}</span></div>`; | |
| } | |
| if (metrics.ups) { | |
| const parts = metrics.ups.split('|').map(s => s.trim()).filter(Boolean); | |
| for (const p of parts) { | |
| const hasBattery = /BATTERY/i.test(p); | |
| const hasFault = /FAULT/i.test(p); | |
| const cls = hasFault ? 'bad' : hasBattery ? 'warn' : 'ok'; | |
| html += `<div class="power-row"><span class="pw-label">UPS</span><span class="pw-val ${cls}">${p}</span></div>`; | |
| } | |
| } | |
| container.innerHTML = html || '<div class="no-data">No data</div>'; | |
| } | |
| // ─── Reward history ────────────────────────────────────────────────── | |
| function updateRewardHistory() { | |
| const container = document.getElementById('rewardHistory'); | |
| container.innerHTML = ''; | |
| for (let i = rewardEntries.length - 1; i >= 0; i--) { | |
| const e = rewardEntries[i]; | |
| const cls = e.reward > 0.005 ? 'pos' : e.reward < -0.005 ? 'neg' : 'zero'; | |
| const sign = e.reward >= 0 ? '+' : ''; | |
| const div = document.createElement('div'); | |
| div.className = 'reward-entry'; | |
| div.innerHTML = ` | |
| <span class="step">${e.step}</span> | |
| <span class="cmd" title="${e.cmd}">${e.cmd}</span> | |
| <span class="rew ${cls}">${sign}${e.reward.toFixed(3)}</span>`; | |
| container.appendChild(div); | |
| } | |
| } | |
| // ─── Tab switching ─────────────────────────────────────────────────── | |
| function switchTab(tab) { | |
| document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); | |
| document.querySelector(`.tab-btn[data-tab="${tab}"]`).classList.add('active'); | |
| const consoleEl = document.getElementById('tabConsole'); | |
| const demosEl = document.getElementById('tabDemos'); | |
| const guideEl = document.getElementById('tabGuide'); | |
| consoleEl.classList.remove('active'); | |
| demosEl.classList.remove('active'); | |
| guideEl.classList.remove('active'); | |
| if (tab === 'console') { | |
| consoleEl.classList.add('active'); | |
| } else if (tab === 'demos') { | |
| demosEl.classList.add('active'); | |
| demosEl.scrollTop = 0; | |
| } else { | |
| guideEl.classList.add('active'); | |
| guideEl.scrollTop = 0; | |
| } | |
| } | |
| // ─── Demo accordion ────────────────────────────────────────────────── | |
| function toggleDemo(id) { | |
| const el = document.getElementById(id); | |
| if (el) el.classList.toggle('open'); | |
| } | |
| function openDemo(id) { | |
| const el = document.getElementById(id); | |
| if (el) { | |
| // Close all others | |
| document.querySelectorAll('.demo-item.open').forEach(d => { | |
| if (d.id !== id) d.classList.remove('open'); | |
| }); | |
| el.classList.add('open'); | |
| setTimeout(() => el.scrollIntoView({ behavior: 'smooth', block: 'start' }), 50); | |
| } | |
| } | |
| // ─── Build weight profile visualizations ───────────────────────────── | |
| function buildWeightProfiles() { | |
| const profiles = { | |
| 'Thermal Scenarios': { thermal_safety: 0.30, power_safety: 0.05, efficiency: 0.10, progress: 0.30, procedure: 0.20, action: 0.05 }, | |
| 'Power Scenarios': { thermal_safety: 0.10, power_safety: 0.25, efficiency: 0.05, progress: 0.30, procedure: 0.25, action: 0.05 }, | |
| 'Default': { thermal_safety: 0.30, power_safety: 0.15, efficiency: 0.25, progress: 0.00, procedure: 0.00, action: 0.30 }, | |
| }; | |
| const colors = { | |
| thermal_safety: 'var(--red)', power_safety: 'var(--yellow)', efficiency: 'var(--green)', | |
| progress: 'var(--accent)', procedure: 'var(--cyan)', action: 'var(--orange)' | |
| }; | |
| const labels = { | |
| thermal_safety: 'Thermal', power_safety: 'Power', efficiency: 'Efficiency', | |
| progress: 'Progress', procedure: 'Procedure', action: 'Action' | |
| }; | |
| const container = document.getElementById('weightProfiles'); | |
| for (const [name, weights] of Object.entries(profiles)) { | |
| const card = document.createElement('div'); | |
| card.className = 'weight-profile'; | |
| let html = `<h4>${name}</h4>`; | |
| for (const [key, val] of Object.entries(weights)) { | |
| const pct = val * 100; | |
| html += `<div class="weight-bar-row"> | |
| <span class="weight-bar-label">${labels[key]}</span> | |
| <div class="weight-bar-track"><div class="weight-bar-fill" style="width:${pct * 3.33}%;background:${colors[key]}"></div></div> | |
| <span class="weight-bar-val">${pct.toFixed(0)}%</span> | |
| </div>`; | |
| } | |
| card.innerHTML = html; | |
| container.appendChild(card); | |
| } | |
| } | |
| // ─── Health check ──────────────────────────────────────────────────── | |
| async function checkHealth() { | |
| try { | |
| const resp = await fetch(`${BASE_URL}/health`); | |
| if (resp.ok) setStatus('connected'); | |
| else setStatus('disconnected'); | |
| } catch (e) { | |
| setStatus('disconnected'); | |
| } | |
| } | |
| // ─── Init ──────────────────────────────────────────────────────────── | |
| checkHealth(); | |
| buildWeightProfiles(); | |
| </script> | |
| </body> | |
| </html> | |