dc_ops_env / server /static /index.html
Melikshah's picture
Upload folder using huggingface_hub
5c44f63 verified
<!DOCTYPE html>
<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 &gt;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)">&#x26A0;</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 &gt; late diagnosis &gt; 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 &lt; 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 &lt;unit&gt;</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 &lt;crac&gt; &lt;°C&gt;</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 &lt;crac&gt; &lt;%&gt;</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 &lt;rack&gt; &lt;kW&gt;</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 &lt;crac&gt;</code></td>
<td>Start a standby CRAC unit.</td>
<td><code>start_crac CRAC-3</code></td>
</tr>
<tr>
<td><code>stop_crac &lt;crac&gt;</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 &lt;ups&gt; &lt;mode&gt;</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>