SidditaVarma/Built-different
0
1from __future__ import annotations2 3 4def render_web_ui() -> str:5 return """6<!doctype html>7<html lang="en">8 <head>9 <meta charset="utf-8">10 <title>Blackstart City</title>11 <style>12 :root {13 --bg: #07111f;14 --panel: #0f172a;15 --border: #1e293b;16 --text: #e2e8f0;17 --muted: #94a3b8;18 --accent: #f59e0b;19 --danger: #ef4444;20 --safe: #22c55e;21 --warn: #eab308;22 }23 *, *::before, *::after { box-sizing: border-box; }24 body {25 font-family: system-ui, -apple-system, "Segoe UI", Arial, sans-serif;26 margin: 0;27 padding: 1.75rem 2rem;28 background:29 radial-gradient(ellipse at top, rgba(245,158,11,0.09), transparent 40%),30 linear-gradient(180deg, #020617 0%, var(--bg) 100%);31 color: var(--text);32 min-height: 100vh;33 }34 .hero {35 display: grid;36 grid-template-columns: 1.15fr 0.85fr;37 gap: 1.25rem;38 margin-bottom: 1.25rem;39 }40 .card {41 background: linear-gradient(160deg, rgba(17,24,39,0.97), rgba(15,23,42,0.97));42 border: 1px solid var(--border);43 border-radius: 16px;44 padding: 1.1rem 1.25rem;45 box-shadow: 0 4px 28px rgba(2,6,23,0.55), inset 0 1px 0 rgba(255,255,255,0.025);46 }47 h1, h2, h3 { margin: 0 0 0.7rem; letter-spacing: -0.01em; }48 h1 { font-size: 1.6rem; }49 h2 { font-size: 1.12rem; color: #cbd5e1; }50 h3 { font-size: 0.98rem; color: #94a3b8; }51 p { color: var(--muted); line-height: 1.6; margin: 0 0 0.5rem; }52 code { color: var(--accent); font-family: "Cascadia Code","Consolas",monospace; font-size: 0.88em; }53 ul { line-height: 1.75; margin: 0; padding-left: 1.2rem; }54 li { color: var(--muted); }55 .grid {56 display: grid;57 grid-template-columns: repeat(2, minmax(0, 1fr));58 gap: 1.1rem;59 }60 .stats {61 display: grid;62 grid-template-columns: repeat(4, minmax(0, 1fr));63 gap: 0.6rem;64 margin-top: 0.85rem;65 }66 .stat {67 border: 1px solid rgba(148,163,184,0.12);68 border-radius: 10px;69 padding: 0.7rem 0.85rem;70 background: rgba(7,17,31,0.7);71 transition: border-color 0.18s;72 }73 .stat:hover { border-color: rgba(148,163,184,0.28); }74 .stat strong {75 display: block;76 font-size: 1.15rem;77 color: white;78 margin-top: 0.18rem;79 font-variant-numeric: tabular-nums;80 }81 .badge {82 display: inline-block;83 font-size: 0.74rem;84 padding: 0.16rem 0.52rem;85 border-radius: 999px;86 border: 1px solid rgba(245,158,11,0.3);87 color: var(--accent);88 margin-right: 0.4rem;89 margin-bottom: 0.5rem;90 letter-spacing: 0.04em;91 }92 .map-wrap {93 background: #030d1a;94 border-radius: 12px;95 border: 1px solid rgba(30,41,59,0.7);96 overflow: hidden;97 }98 svg { width: 100%; height: auto; display: block; }99 .legend {100 display: flex;101 gap: 1rem;102 flex-wrap: wrap;103 margin-top: 0.6rem;104 color: var(--muted);105 font-size: 0.84rem;106 padding: 0 0.2rem;107 }108 .dot {109 width: 0.7rem; height: 0.7rem;110 border-radius: 999px;111 display: inline-block;112 margin-right: 0.28rem;113 vertical-align: middle;114 }115 .small { font-size: 0.87rem; color: var(--muted); }116 .controls {117 display: flex;118 gap: 0.6rem;119 flex-wrap: wrap;120 align-items: center;121 }122 button, select, input, textarea {123 background: #0b1728;124 color: var(--text);125 border: 1px solid rgba(148,163,184,0.18);126 border-radius: 8px;127 padding: 0.58rem 0.85rem;128 font: inherit;129 font-size: 0.9rem;130 outline: none;131 transition: border-color 0.14s, box-shadow 0.14s;132 }133 button:focus, select:focus, input:focus, textarea:focus {134 border-color: rgba(96,165,250,0.45);135 box-shadow: 0 0 0 3px rgba(59,130,246,0.1);136 }137 button {138 cursor: pointer;139 background: linear-gradient(180deg, #2563eb, #1e40af);140 border-color: rgba(96,165,250,0.3);141 font-weight: 500;142 }143 button:hover { background: linear-gradient(180deg, #3b82f6, #2563eb); }144 button.secondary {145 background: linear-gradient(180deg, #374151, #1f2937);146 border-color: rgba(148,163,184,0.18);147 }148 button.secondary:hover { background: linear-gradient(180deg, #4b5563, #374151); }149 button.warn {150 background: linear-gradient(180deg, #b45309, #92400e);151 border-color: rgba(251,191,36,0.25);152 }153 button.warn:hover { background: linear-gradient(180deg, #d97706, #b45309); }154 textarea {155 width: 100%;156 min-height: 132px;157 resize: vertical;158 font-family: "Cascadia Code","Consolas",monospace;159 font-size: 0.87rem;160 line-height: 1.5;161 }162 .data-grid {163 display: grid;164 grid-template-columns: repeat(2, minmax(0, 1fr));165 gap: 1rem;166 }167 .list { display: grid; gap: 0.48rem; }168 .item {169 border: 1px solid rgba(148,163,184,0.1);170 border-radius: 10px;171 padding: 0.68rem 0.85rem;172 background: rgba(2,6,23,0.5);173 transition: border-color 0.14s;174 }175 .item:hover { border-color: rgba(148,163,184,0.22); }176 .item strong { color: white; }177 .item-line {178 display: flex;179 justify-content: space-between;180 gap: 0.7rem;181 flex-wrap: wrap;182 align-items: center;183 }184 .tag {185 display: inline-block;186 border-radius: 999px;187 padding: 0.12rem 0.48rem;188 font-size: 0.74rem;189 font-weight: 500;190 margin-right: 0.28rem;191 letter-spacing: 0.02em;192 }193 .tag.safe { background: rgba(34,197,94,0.13); color: #86efac; border: 1px solid rgba(34,197,94,0.2); }194 .tag.warn { background: rgba(234,179,8,0.13); color: #fde047; border: 1px solid rgba(234,179,8,0.2); }195 .tag.danger { background: rgba(239,68,68,0.13); color: #fca5a5; border: 1px solid rgba(239,68,68,0.2); }196 .mono { font-family: "Cascadia Code","Consolas",monospace; color: #94a3b8; font-size: 0.87em; }197 .warning-list li { color: #fcd34d; }198 .log {199 max-height: 280px;200 overflow: auto;201 font-family: "Cascadia Code","Consolas",monospace;202 font-size: 0.84rem;203 line-height: 1.55;204 white-space: pre-wrap;205 color: #94a3b8;206 padding: 0.4rem 0.2rem;207 }208 .log::-webkit-scrollbar { width: 5px; }209 .log::-webkit-scrollbar-track { background: transparent; }210 .log::-webkit-scrollbar-thumb { background: #1e293b; border-radius: 3px; }211 .footer-note { color: var(--muted); font-size: 0.84rem; margin-top: 0.5rem; line-height: 1.5; }212 .compare-grid {213 display: grid;214 grid-template-columns: repeat(2, minmax(0, 1fr));215 gap: 1rem;216 }217 .compare-card {218 border: 1px solid rgba(148,163,184,0.12);219 border-radius: 12px;220 padding: 0.9rem;221 background: rgba(2,6,23,0.38);222 }223 .objective-box {224 border: 1px solid rgba(148,163,184,0.12);225 border-radius: 12px;226 padding: 0.85rem 0.95rem;227 background: rgba(2,6,23,0.32);228 margin-bottom: 0.85rem;229 }230 .objective-box strong { color: white; }231 .overlay-banner {232 border-radius: 9px;233 padding: 0.48rem 0.7rem;234 margin-top: 0.48rem;235 font-size: 0.87rem;236 border: 1px solid rgba(148,163,184,0.12);237 background: rgba(2,6,23,0.32);238 }239 .overlay-banner.safe { color: #86efac; border-color: rgba(34,197,94,0.28); }240 .overlay-banner.warn { color: #fde047; border-color: rgba(234,179,8,0.28); }241 .overlay-banner.danger { color: #fca5a5; border-color: rgba(239,68,68,0.28); }242 .reward-grid {243 display: grid;244 grid-template-columns: repeat(auto-fill, minmax(128px, 1fr));245 gap: 0.5rem;246 margin-top: 0.55rem;247 }248 .reward-item {249 background: rgba(7,17,31,0.65);250 border: 1px solid rgba(148,163,184,0.09);251 border-radius: 8px;252 padding: 0.48rem;253 text-align: center;254 }255 .reward-item .val {256 display: block;257 font-weight: 700;258 font-size: 0.98rem;259 margin-top: 0.18rem;260 font-family: "Cascadia Code","Consolas",monospace;261 font-variant-numeric: tabular-nums;262 }263 .reward-item.plus .val { color: #86efac; }264 .reward-item.minus .val { color: #fca5a5; }265 266 @keyframes flow { to { stroke-dashoffset: -24; } }267 @keyframes pulse-ring { 0%,100% { opacity: 0.3; } 50% { opacity: 0.65; } }268 @keyframes ticker-blink{ 0%,100% { opacity: 1; } 50% { opacity: 0.55; } }269 270 @media (max-width: 980px) {271 .hero, .grid, .stats, .data-grid, .compare-grid { grid-template-columns: 1fr; }272 body { padding: 1rem; }273 }274 </style>275 </head>276 <body>277 278 <!-- ═══════════════════ HERO ═══════════════════ -->279 <div class="hero">280 <div class="card">281 <div>282 <span class="badge">OpenEnv</span>283 <span class="badge">Wild Card</span>284 <span class="badge">Long-Horizon</span>285 </div>286 <h1>Blackstart City</h1>287 <p>An AI command team must bring a dark city back to life before hospitals exhaust backup power, telecom towers fail, water pressure collapses, public trust breaks down, and one unsafe reconnection triggers a second blackout.</p>288 <div class="stats">289 <div class="stat"><span class="small">Task Families</span><strong>3</strong></div>290 <div class="stat"><span class="small">Critical Systems</span><strong>4</strong></div>291 <div class="stat"><span class="small">Score Range</span><strong>0.01–0.99</strong></div>292 <div class="stat"><span class="small">Main Failure</span><strong>2nd Collapse</strong></div>293 </div>294 </div>295 <div class="card">296 <h2>Demo Narrative</h2>297 <ul>298 <li>Hospitals start on backup power and lose minutes every step.</li>299 <li>Telecom outages reduce visibility and destabilize recovery.</li>300 <li>Water plants create city-scale penalties if left dark.</li>301 <li>Grid, emergency, public-information, and dispatch roles must stay aligned.</li>302 </ul>303 </div>304 </div>305 306 <!-- ═══════════════════ CITY MAP + TASKS ═══════════════════ -->307 <div class="grid">308 <div class="card">309 <h2>City Infrastructure Map</h2>310 <div class="map-wrap">311 <!--312 ╔══════════════════════════════════════════════════════════╗313 ║ BLACKSTART CITY — CONTROL-ROOM INFRASTRUCTURE VIEW ║314 ║ All IDs are referenced by JavaScript below. ║315 ╚══════════════════════════════════════════════════════════╝316 Node positions:317 South Gen → (155, 220)318 Core bus → (330, 115)319 Medical bus→ (330, 335)320 East bus → (528, 232)321 Critical nodes:322 Hospital → rect (16, 94) 102×56323 Telecom → rect (626, 48) 118×56324 Water → rect (626,382) 118×56325 Status banners: y=454326 -->327 <svg viewBox="0 0 760 560" aria-label="Blackstart City infrastructure map">328 <defs>329 <!-- Arrow markers for transmission lines -->330 <marker id="arr-green" viewBox="0 0 10 10" refX="8" refY="5"331 markerWidth="6" markerHeight="6" orient="auto-start-reverse">332 <path d="M2 1L8 5L2 9" fill="none" stroke="#22c55e"333 stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>334 </marker>335 <marker id="arr-red" viewBox="0 0 10 10" refX="8" refY="5"336 markerWidth="6" markerHeight="6" orient="auto-start-reverse">337 <path d="M2 1L8 5L2 9" fill="none" stroke="#ef4444"338 stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>339 </marker>340 <marker id="arr-amber" viewBox="0 0 10 10" refX="8" refY="5"341 markerWidth="6" markerHeight="6" orient="auto-start-reverse">342 <path d="M2 1L8 5L2 9" fill="none" stroke="#f59e0b"343 stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>344 </marker>345 <!-- Glow filters -->346 <filter id="glow-sm" x="-40%" y="-40%" width="180%" height="180%">347 <feGaussianBlur stdDeviation="5" result="b"/>348 <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>349 </filter>350 <!-- Scanline overlay -->351 <pattern id="scan" width="1" height="4" patternUnits="userSpaceOnUse">352 <rect width="1" height="2" fill="rgba(0,0,0,0.06)"/>353 </pattern>354 </defs>355 356 <!-- ══ BASE ════════════════════════════════════════════════ -->357 <rect width="760" height="560" fill="#030c18"/>358 359 <!-- ══ STREET GRID ════════════════════════════════════════ -->360 <line x1="0" y1="200" x2="760" y2="200" stroke="#0a1d2f" stroke-width="8"/>361 <line x1="0" y1="390" x2="760" y2="390" stroke="#0a1d2f" stroke-width="8"/>362 <line x1="270" y1="0" x2="270" y2="560" stroke="#0a1d2f" stroke-width="8"/>363 <line x1="490" y1="0" x2="490" y2="560" stroke="#0a1d2f" stroke-width="8"/>364 <line x1="0" y1="100" x2="760" y2="100" stroke="#060f1c" stroke-width="2"/>365 <line x1="0" y1="295" x2="760" y2="295" stroke="#060f1c" stroke-width="2"/>366 <line x1="0" y1="490" x2="760" y2="490" stroke="#060f1c" stroke-width="2"/>367 <line x1="152" y1="0" x2="152" y2="560" stroke="#060f1c" stroke-width="2"/>368 <line x1="378" y1="0" x2="378" y2="560" stroke="#060f1c" stroke-width="2"/>369 <line x1="626" y1="0" x2="626" y2="560" stroke="#060f1c" stroke-width="2"/>370 371 <!-- ══ DISTRICT ZONES ══════════════════════════════════════ -->372 <!-- Generation / West -->373 <rect x="16" y="148" width="224" height="228" rx="10"374 fill="rgba(34,197,94,0.03)" stroke="rgba(34,197,94,0.22)"375 stroke-width="1" stroke-dasharray="10 6"/>376 <text x="28" y="168" fill="rgba(34,197,94,0.45)"377 font-size="10" font-family="monospace" letter-spacing="2.5">GENERATION</text>378 <!-- Core / North-Center -->379 <rect x="248" y="28" width="216" height="212" rx="10"380 fill="rgba(14,165,233,0.03)" stroke="rgba(14,165,233,0.22)"381 stroke-width="1" stroke-dasharray="10 6"/>382 <text x="260" y="48" fill="rgba(14,165,233,0.45)"383 font-size="10" font-family="monospace" letter-spacing="2.5">CORE DISTRICT</text>384 <!-- Medical / South-Center -->385 <rect x="248" y="308" width="216" height="208" rx="10"386 fill="rgba(239,68,68,0.03)" stroke="rgba(239,68,68,0.22)"387 stroke-width="1" stroke-dasharray="10 6"/>388 <text x="260" y="328" fill="rgba(239,68,68,0.45)"389 font-size="10" font-family="monospace" letter-spacing="2.5">MEDICAL</text>390 <!-- East Grid — tall zone that contains hospital/telecom/water -->391 <rect x="472" y="56" width="282" height="400" rx="10"392 fill="rgba(245,158,11,0.03)" stroke="rgba(245,158,11,0.22)"393 stroke-width="1" stroke-dasharray="10 6"/>394 <text x="484" y="76" fill="rgba(245,158,11,0.45)"395 font-size="10" font-family="monospace" letter-spacing="2.5">EAST GRID</text>396 397 <!-- ══ CITY BUILDING BLOCKS (texture) ═════════════════════ -->398 <rect x="30" y="180" width="22" height="15" rx="2" fill="#060f1c"/>399 <rect x="58" y="177" width="16" height="19" rx="2" fill="#060f1c"/>400 <rect x="80" y="183" width="24" height="13" rx="2" fill="#060f1c"/>401 <rect x="30" y="322" width="18" height="14" rx="2" fill="#060f1c"/>402 <rect x="54" y="318" width="24" height="17" rx="2" fill="#060f1c"/>403 <rect x="82" y="324" width="16" height="13" rx="2" fill="#060f1c"/>404 <rect x="264" y="68" width="23" height="17" rx="2" fill="#050e1b"/>405 <rect x="293" y="65" width="15" height="21" rx="2" fill="#050e1b"/>406 <rect x="316" y="70" width="19" height="15" rx="2" fill="#050e1b"/>407 <rect x="396" y="68" width="17" height="14" rx="2" fill="#050e1b"/>408 <rect x="418" y="66" width="13" height="18" rx="2" fill="#050e1b"/>409 <rect x="264" y="348" width="20" height="14" rx="2" fill="#08090e"/>410 <rect x="290" y="344" width="24" height="17" rx="2" fill="#08090e"/>411 <rect x="320" y="350" width="16" height="13" rx="2" fill="#08090e"/>412 <rect x="506" y="192" width="18" height="15" rx="2" fill="#06100a"/>413 <rect x="530" y="189" width="14" height="18" rx="2" fill="#06100a"/>414 <rect x="612" y="228" width="20" height="15" rx="2" fill="#06100a"/>415 <rect x="638" y="225" width="15" height="19" rx="2" fill="#06100a"/>416 <rect x="608" y="342" width="20" height="14" rx="2" fill="#06100a"/>417 <rect x="634" y="339" width="15" height="17" rx="2" fill="#06100a"/>418 419 <!-- ══ TRANSMISSION CORRIDOR SHADOWS ══════════════════════ -->420 <line x1="130" y1="272" x2="356" y2="138" stroke="#0a1828" stroke-width="18" stroke-linecap="round"/>421 <line x1="130" y1="272" x2="356" y2="390" stroke="#0a1828" stroke-width="18" stroke-linecap="round"/>422 <line x1="356" y1="390" x2="556" y2="262" stroke="#0a1828" stroke-width="18" stroke-linecap="round"/>423 <line x1="356" y1="138" x2="556" y2="262" stroke="#0a1828" stroke-width="18" stroke-linecap="round"/>424 425 <!-- ══ MAIN TRANSMISSION LINES — JS controls stroke + dasharray ══ -->426 <line id="map-line-south-core" x1="130" y1="272" x2="356" y2="138"427 stroke="#ef4444" stroke-width="3" stroke-dasharray="12 8" stroke-linecap="round"428 marker-end="url(#arr-red)"/>429 <line id="map-line-south-medical" x1="130" y1="272" x2="356" y2="390"430 stroke="#22c55e" stroke-width="3" stroke-linecap="round"431 marker-end="url(#arr-green)"/>432 <line id="map-line-medical-east" x1="356" y1="390" x2="556" y2="262"433 stroke="#f59e0b" stroke-width="3" stroke-dasharray="8 6" stroke-linecap="round"434 marker-end="url(#arr-amber)"/>435 <line id="map-line-core-east" x1="356" y1="138" x2="556" y2="262"436 stroke="#ef4444" stroke-width="3" stroke-dasharray="12 8" stroke-linecap="round"437 marker-end="url(#arr-red)"/>438 <!-- Animated power-flow overlay on energized line -->439 <line x1="130" y1="272" x2="356" y2="390"440 stroke="rgba(34,197,94,0.45)" stroke-width="2"441 stroke-dasharray="6 14"442 style="animation:flow 0.95s linear infinite"/>443 444 <!-- ══ LINE LABEL PILLS ═══════════════════════════════════ -->445 <!-- south-core label -->446 <rect x="196" y="188" width="100" height="22" rx="11"447 fill="#3f0f0f" stroke="rgba(239,68,68,0.35)" stroke-width="1"/>448 <text id="map-line-label-a" x="246" y="203" text-anchor="middle"449 fill="#fca5a5" font-size="11" font-family="monospace">damaged tie-line</text>450 <!-- south-medical label -->451 <rect x="182" y="314" width="96" height="22" rx="11"452 fill="#052a0f" stroke="rgba(34,197,94,0.35)" stroke-width="1"/>453 <text id="map-line-label-c" x="230" y="329" text-anchor="middle"454 fill="#86efac" font-size="11" font-family="monospace">energized path</text>455 <!-- core-east label -->456 <rect x="442" y="186" width="80" height="22" rx="11"457 fill="#3f0f0f" stroke="rgba(239,68,68,0.35)" stroke-width="1"/>458 <text id="map-line-label-b" x="482" y="201" text-anchor="middle"459 fill="#fca5a5" font-size="11" font-family="monospace">unsafe corridor</text>460 461 <!-- ══ PYLON ICONS ══════════════════════════════════════════ -->462 <!-- south-medical midpoint ~(243,331) -->463 <g transform="translate(243,331)">464 <line x1="0" y1="9" x2="0" y2="-7" stroke="#162c42" stroke-width="2.5"/>465 <line x1="-8" y1="-1" x2="8" y2="-1" stroke="#162c42" stroke-width="1.5"/>466 <line x1="-5" y1="3" x2="5" y2="3" stroke="#162c42" stroke-width="1"/>467 <circle cy="9" r="2.5" fill="#162c42"/>468 </g>469 <!-- core-east midpoint ~(456,200) -->470 <g transform="translate(456,200)">471 <line x1="0" y1="9" x2="0" y2="-7" stroke="#162c42" stroke-width="2.5"/>472 <line x1="-8" y1="-1" x2="8" y2="-1" stroke="#162c42" stroke-width="1.5"/>473 <line x1="-5" y1="3" x2="5" y2="3" stroke="#162c42" stroke-width="1"/>474 <circle cy="9" r="2.5" fill="#162c42"/>475 </g>476 <!-- medical-east midpoint ~(456,326) -->477 <g transform="translate(456,326)">478 <line x1="0" y1="9" x2="0" y2="-7" stroke="#162c42" stroke-width="2.5"/>479 <line x1="-8" y1="-1" x2="8" y2="-1" stroke="#162c42" stroke-width="1.5"/>480 <line x1="-5" y1="3" x2="5" y2="3" stroke="#162c42" stroke-width="1"/>481 <circle cy="9" r="2.5" fill="#162c42"/>482 </g>483 484 <!-- ══ EAST SUB → CRITICAL NODE CONNECTORS ════════════════ -->485 <!-- East → Hospital (top-right) -->486 <line x1="558" y1="238" x2="594" y2="134"487 stroke="#374151" stroke-width="1.5" stroke-dasharray="5 5"/>488 <!-- East → Telecom (middle-right) -->489 <line x1="576" y1="250" x2="634" y2="198"490 stroke="#374151" stroke-width="1.5" stroke-dasharray="5 5"/>491 <!-- East → Water (bottom-right) -->492 <line x1="578" y1="280" x2="636" y2="402"493 stroke="#374151" stroke-width="1.5" stroke-dasharray="5 5"/>494 495 <!-- ══ CRITICAL NODE: HOSPITAL ══════════════════════════════ -->496 <rect id="map-critical-hospital" x="534" y="82" width="122" height="54" rx="10"497 fill="#450a0a" stroke="#ef4444" stroke-width="2"/>498 <!-- Red cross icon -->499 <rect x="556" y="92" width="16" height="5" rx="1" fill="#ef4444" opacity="0.8"/>500 <rect x="561" y="87" width="6" height="15" rx="1" fill="#ef4444" opacity="0.8"/>501 <text id="map-label-hospital" x="618" y="107" text-anchor="middle"502 fill="#fecaca" font-size="13" font-weight="600">Hospital</text>503 <text id="map-timer-hospital" x="618" y="125" text-anchor="middle"504 fill="#fca5a5" font-size="12" font-weight="700"505 style="animation:ticker-blink 2s ease-in-out infinite">24 min</text>506 507 <!-- ══ CRITICAL NODE: TELECOM ════════════════════════════════ -->508 <rect id="map-critical-telecom" x="634" y="178" width="118" height="54" rx="10"509 fill="#3a1f00" stroke="#eab308" stroke-width="2"/>510 <!-- Antenna icon -->511 <line x1="654" y1="188" x2="654" y2="204" stroke="#f59e0b" stroke-width="2.5"/>512 <line x1="646" y1="194" x2="662" y2="194" stroke="#f59e0b" stroke-width="2"/>513 <line x1="644" y1="190" x2="664" y2="190" stroke="#f59e0b" stroke-width="1" opacity="0.5"/>514 <circle cx="654" cy="188" r="2.5" fill="#f59e0b" opacity="0.7"/>515 <text id="map-label-telecom" x="706" y="201" text-anchor="middle"516 fill="#fef3c7" font-size="13" font-weight="600">Telecom</text>517 <text id="map-timer-telecom" x="706" y="220" text-anchor="middle"518 fill="#fde047" font-size="11" font-family="monospace">15 min</text>519 520 <!-- ══ CRITICAL NODE: WATER ══════════════════════════════════ -->521 <rect id="map-critical-water" x="634" y="382" width="118" height="54" rx="10"522 fill="#0f172a" stroke="#0ea5e9" stroke-width="2"/>523 <!-- Water droplet icon -->524 <path d="M654,392 Q658,386 662,392 Q666,398 662,404 Q658,408 654,404 Q650,398 654,392 Z"525 fill="#38bdf8" opacity="0.65"/>526 <text id="map-label-water" x="706" y="406" text-anchor="middle"527 fill="#bae6fd" font-size="13" font-weight="600">Water Plant</text>528 <text id="map-timer-water" x="706" y="424" text-anchor="middle"529 fill="#7dd3fc" font-size="11" font-family="monospace">22 min</text>530 531 <!-- ══ NODE GLOW HALOS ══════════════════════════════════════ -->532 <!-- South Gen pulse ring -->533 <circle cx="130" cy="272" r="52"534 fill="none" stroke="rgba(34,197,94,0.10)" stroke-width="22"/>535 <circle cx="130" cy="272" r="38"536 fill="none" stroke="rgba(34,197,94,0.14)" stroke-width="8"/>537 <!-- East Sub warning halo -->538 <circle cx="556" cy="262" r="40"539 fill="none" stroke="rgba(245,158,11,0.11)" stroke-width="18"/>540 <!-- Hospital critical blink -->541 <circle cx="595" cy="109" r="38"542 fill="none" stroke="rgba(239,68,68,0.13)" stroke-width="12"543 style="animation:pulse-ring 2s ease-in-out infinite"/>544 545 <!-- ══ BUS NODES — JS controls fill ════════════════════════ -->546 547 <!-- South Gen — blackstart generator -->548 <circle id="map-bus-south" cx="130" cy="272" r="30"549 fill="#166534" filter="url(#glow-sm)"/>550 <path d="M136,258 L124,276 L132,276 L126,288 L141,267 L133,267 Z"551 fill="#052e16" opacity="0.88"/>552 <text id="map-bus-label-south" x="130" y="315" text-anchor="middle"553 fill="#86efac" font-size="12" font-weight="700">South Gen</text>554 <text x="130" y="329" text-anchor="middle"555 fill="rgba(134,239,172,0.48)" font-size="10" font-family="monospace">online · blackstart</text>556 557 <!-- Core substation -->558 <circle id="map-bus-core" cx="356" cy="138" r="27" fill="#075985"/>559 <rect x="343" y="126" width="13" height="13" rx="2"560 fill="none" stroke="#7dd3fc" stroke-width="1.5"/>561 <rect x="359" y="126" width="13" height="13" rx="2"562 fill="none" stroke="#7dd3fc" stroke-width="1.5"/>563 <line x1="356" y1="126" x2="356" y2="139" stroke="#7dd3fc" stroke-width="1" opacity="0.5"/>564 <text id="map-bus-label-core" x="356" y="180" text-anchor="middle"565 fill="#7dd3fc" font-size="12" font-weight="700">Core</text>566 <text x="356" y="194" text-anchor="middle"567 fill="rgba(125,211,252,0.48)" font-size="10" font-family="monospace">substation · bus A</text>568 569 <!-- Medical substation -->570 <circle id="map-bus-medical" cx="356" cy="392" r="27" fill="#075985"/>571 <rect x="343" y="380" width="13" height="13" rx="2"572 fill="none" stroke="#7dd3fc" stroke-width="1.5"/>573 <rect x="359" y="380" width="13" height="13" rx="2"574 fill="none" stroke="#7dd3fc" stroke-width="1.5"/>575 <line x1="356" y1="380" x2="356" y2="393" stroke="#7dd3fc" stroke-width="1" opacity="0.5"/>576 <text id="map-bus-label-medical" x="356" y="434" text-anchor="middle"577 fill="#7dd3fc" font-size="12" font-weight="700">Medical</text>578 <text x="356" y="448" text-anchor="middle"579 fill="rgba(125,211,252,0.48)" font-size="10" font-family="monospace">substation · bus B</text>580 581 <!-- East substation -->582 <circle id="map-bus-east" cx="556" cy="262" r="28" fill="#92400e"/>583 <rect x="545" y="251" width="14" height="14" rx="2"584 fill="none" stroke="#fde047" stroke-width="1.5"/>585 <line x1="552" y1="251" x2="552" y2="265" stroke="#fde047" stroke-width="1" opacity="0.5"/>586 <text id="map-bus-label-east" x="556" y="305" text-anchor="middle"587 fill="#fcd34d" font-size="12" font-weight="700">East Sub</text>588 <text x="556" y="319" text-anchor="middle"589 fill="rgba(252,211,77,0.48)" font-size="10" font-family="monospace">backup power</text>590 591 <!-- ══ ACTION SEQUENCE PANEL (bottom-left) ═════════════════ -->592 <rect x="16" y="460" width="218" height="84" rx="10"593 fill="#040e1c" stroke="#1a2e42" stroke-width="1"/>594 <text x="30" y="478" fill="#f59e0b"595 font-size="11" font-weight="700" font-family="monospace" letter-spacing="1">ACTION SEQUENCE</text>596 <text x="30" y="494" fill="#475569" font-size="11" font-family="monospace">1) start_generator</text>597 <text x="30" y="508" fill="#475569" font-size="11" font-family="monospace">2) energize_substation</text>598 <text x="30" y="522" fill="#475569" font-size="11" font-family="monospace">3) inspect_line -> restore_critical</text>599 <text x="30" y="536" fill="#475569" font-size="11" font-family="monospace">4) restore_zone + publish_status</text>600 601 <!-- ══ STATUS BANNERS (bottom, right of action panel) ══════ -->602 <rect x="248" y="524" width="152" height="28" rx="8"603 fill="#040e1c" stroke="#1a2e42" stroke-width="1"/>604 <text x="258" y="535" fill="#334155" font-size="10" font-family="monospace">⚡ FREQUENCY</text>605 <text id="map-banner-frequency" x="258" y="549"606 fill="#86efac" font-size="12" font-family="monospace" font-weight="600">Frequency stable</text>607 608 <rect x="412" y="524" width="160" height="28" rx="8"609 fill="#040e1c" stroke="#1a2e42" stroke-width="1"/>610 <text x="422" y="535" fill="#334155" font-size="10" font-family="monospace">◈ RESERVE</text>611 <text id="map-banner-reserve" x="422" y="549"612 fill="#86efac" font-size="12" font-family="monospace" font-weight="600">Reserve healthy</text>613 614 <rect x="584" y="524" width="164" height="28" rx="8"615 fill="#040e1c" stroke="#1a2e42" stroke-width="1"/>616 <text x="594" y="535" fill="#334155" font-size="10" font-family="monospace">▲ RISK</text>617 <text id="map-banner-risk" x="594" y="549"618 fill="#86efac" font-size="12" font-family="monospace" font-weight="600">No collapse risk</text>619 620 <!-- ══ HUD TITLE BAR ════════════════════════════════════════ -->621 <rect x="10" y="10" width="196" height="30" rx="6"622 fill="rgba(4,14,28,0.92)" stroke="#1a2e42" stroke-width="1"/>623 <text x="22" y="30" fill="#f59e0b"624 font-size="13" font-weight="700" font-family="monospace" letter-spacing="1">BLACKSTART CITY</text>625 <rect x="216" y="12" width="124" height="26" rx="6"626 fill="rgba(4,14,28,0.82)" stroke="#1a2e42" stroke-width="1"/>627 <text x="226" y="29" fill="#2d4258"628 font-size="10" font-family="monospace" letter-spacing="1">CTRL-ROOM VIEW</text>629 630 <!-- Scanline overlay -->631 <rect width="760" height="560" fill="url(#scan)" opacity="0.6"/>632 </svg>633 </div>634 <div class="legend">635 <span><span class="dot" style="background:#22c55e"></span>online / energized</span>636 <span><span class="dot" style="background:#0ea5e9"></span>substation / bus</span>637 <span><span class="dot" style="background:#f59e0b"></span>on backup power</span>638 <span><span class="dot" style="background:#ef4444"></span>damaged / critical</span>639 <span><span class="dot" style="background:#475569"></span>offline / open</span>640 <span><span class="dot" style="background:#f59e0b; border-radius:2px;"></span>inspected / pending</span>641 </div>642 </div>643 644 <div class="card">645 <h2>Primary Tasks</h2>646 <ul>647 <li><code>local_blackstart</code>: restore one district hospital and a telecom node before backup expires</li>648 <li><code>island_rejoin</code>: inspect a hidden-damage tie-line and safely reconnect islands</li>649 <li><code>city_cascade_recovery</code>: restore city-scale critical services while avoiding a second collapse</li>650 </ul>651 <h3 style="margin-top:1rem;">Command Roles</h3>652 <ul>653 <li><code>grid_operator</code>: energizes feeders and keeps the grid stable</li>654 <li><code>emergency_coordinator</code>: triages hospitals, telecom, water, and emergency load</li>655 <li><code>public_information_officer</code>: preserves public trust with truthful updates</li>656 <li><code>resource_dispatcher</code>: allocates scarce repair crews and support units</li>657 </ul>658 <h3 style="margin-top:1rem;">Reward Breakdown</h3>659 <ul>660 <li>critical-service restoration</li>661 <li>safe load restoration</li>662 <li>stability and reserve margin</li>663 <li>inspection quality</li>664 <li>communication accuracy</li>665 <li>catastrophe penalties</li>666 </ul>667 </div>668 </div>669 670 <!-- ═══════════════════ INTERACTIVE ROLLOUT ═══════════════════ -->671 <div class="card" style="margin-top:1.1rem;">672 <h2>Interactive Rollout</h2>673 <div class="controls" style="margin-bottom:0.85rem;">674 <label>Task675 <select id="task-select">676 <option value="local_blackstart">local_blackstart (easy)</option>677 <option value="island_rejoin">island_rejoin (medium)</option>678 <option value="city_cascade_recovery">city_cascade_recovery (hard)</option>679 <option value="mega_cascade">mega_cascade (extreme)</option>680 </select>681 </label>682 <label>Seed <input id="seed-input" type="number" value="0" min="0" style="width:88px"></label>683 <button id="reset-btn">Reset Scenario</button>684 <button id="heuristic-btn" class="secondary">Heuristic Step</button>685 <button id="auto-btn" class="warn">Autoplay Heuristic</button>686 <button id="suggest-btn" class="secondary">Suggest Action</button>687 </div>688 <div class="objective-box">689 <div class="small">Incident</div>690 <strong id="incident-id">-</strong>691 <div class="small" style="margin-top:0.45rem;">Scenario</div>692 <strong id="scenario-title">-</strong>693 <div class="small" style="margin-top:0.45rem;">Objective</div>694 <div id="scenario-objective" class="small">-</div>695 </div>696 <div class="data-grid">697 <div class="card" style="padding:0.9rem;">698 <h3>Live State</h3>699 <div class="stats" style="grid-template-columns: repeat(4, minmax(0, 1fr));">700 <div class="stat"><span class="small">Step</span><strong id="metric-step">-</strong></div>701 <div class="stat"><span class="small">Frequency</span><strong id="metric-frequency">-</strong></div>702 <div class="stat"><span class="small">Reserve</span><strong id="metric-reserve">-</strong></div>703 <div class="stat"><span class="small">Score</span><strong id="metric-score">-</strong></div>704 </div>705 <div class="stats" style="grid-template-columns: repeat(4, minmax(0, 1fr)); margin-top:0.7rem;">706 <div class="stat"><span class="small">Generation</span><strong id="metric-gen">-</strong></div>707 <div class="stat"><span class="small">Served Load</span><strong id="metric-load">-</strong></div>708 <div class="stat"><span class="small">Unstable Islands</span><strong id="metric-islands">-</strong></div>709 <div class="stat"><span class="small">Done</span><strong id="metric-done">-</strong></div>710 </div>711 <div class="footer-note" id="last-result">No rollout yet.</div>712 <div id="overlay-frequency" class="overlay-banner safe">Grid frequency stable.</div>713 <div id="overlay-reserve" class="overlay-banner safe">Reserve margin healthy.</div>714 <div id="overlay-risk" class="overlay-banner safe">No immediate second-collapse risk.</div>715 716 <h3 style="margin-top:1.2rem;">Reward Decomposition</h3>717 <div class="reward-grid" id="reward-decomposition"></div>718 </div>719 720 <div class="card" style="padding:0.9rem;">721 <h3>Suggested / Manual Action</h3>722 <textarea id="action-json">{"action_type":"start_generator","target_id":"gen_south_blackstart"}</textarea>723 <div class="controls" style="margin-top:0.75rem;">724 <button id="step-btn">Submit Action</button>725 </div>726 <div class="footer-note">Use <code>Suggest Action</code> for the current heuristic next move, then edit if needed.</div>727 </div>728 </div>729 </div>730 731 <!-- ═══════════════════ COMMAND CENTER ═══════════════════ -->732 <div class="grid" style="margin-top:1.1rem;">733 <div class="card">734 <h2>Command Center</h2>735 <div class="stats">736 <div class="stat"><span class="small">Public Trust</span><strong id="metric-trust">-</strong></div>737 <div class="stat"><span class="small">Coordination</span><strong id="metric-coordination">-</strong></div>738 <div class="stat"><span class="small">Phase</span><strong id="metric-phase">-</strong></div>739 <div class="stat"><span class="small">Dispatch Pressure</span><strong id="metric-dispatch">-</strong></div>740 </div>741 <div id="resource-list" class="list" style="margin-top:0.9rem;"></div>742 </div>743 744 <div class="card">745 <h2>Role Recommendations</h2>746 <div id="role-list" class="list"></div>747 <h3 style="margin-top:1rem;">Coordination Messages</h3>748 <div id="message-list" class="list"></div>749 </div>750 </div>751 752 <!-- ═══════════════════ ASSETS ═══════════════════ -->753 <div class="grid" style="margin-top:1.1rem;">754 <div class="card">755 <h2>Critical Nodes</h2>756 <div id="critical-list" class="list"></div>757 </div>758 759 <div class="card">760 <h2>Grid Assets</h2>761 <div id="asset-list" class="list"></div>762 </div>763 </div>764 765 <!-- ═══════════════════ WARNINGS + LOG ═══════════════════ -->766 <div class="grid" style="margin-top:1.1rem;">767 <div class="card">768 <h2>Warnings</h2>769 <ul id="warning-list" class="warning-list"></ul>770 </div>771 772 <div class="card">773 <h2>Rollout Log</h2>774 <div id="rollout-log" class="log"></div>775 </div>776 </div>777 778 <!-- ═══════════════════ CONSTRAINTS + NEWS ═══════════════════ -->779 <div class="grid" style="margin-top:1.1rem;">780 <div class="card">781 <h2>Active Constraints</h2>782 <div id="constraint-list" class="list">783 <div class="small" style="color:var(--muted);">No scenario loaded.</div>784 </div>785 </div>786 787 <div class="card">788 <h2>News Feed</h2>789 <div id="news-list" class="list">790 <div class="small" style="color:var(--muted);">No events yet.</div>791 </div>792 </div>793 </div>794 795 <!-- ═══════════════════ POLICY COMPARISON ═══════════════════ -->796 <div class="card" style="margin-top:1.1rem;">797 <h2>Policy Comparison</h2>798 <div class="controls" style="margin-bottom:0.85rem;">799 <button id="compare-btn">Run Greedy vs Heuristic</button>800 </div>801 <div class="compare-grid">802 <div class="compare-card">803 <h3>Greedy Policy</h3>804 <div id="compare-greedy-summary" class="small">No comparison yet.</div>805 <div id="compare-greedy-log" class="log" style="margin-top:0.6rem; max-height:220px;"></div>806 </div>807 <div class="compare-card">808 <h3>Heuristic Policy</h3>809 <div id="compare-heuristic-summary" class="small">No comparison yet.</div>810 <div id="compare-heuristic-log" class="log" style="margin-top:0.6rem; max-height:220px;"></div>811 </div>812 </div>813 </div>814 815 <!-- ═══════════════════ API ═══════════════════ -->816 <div class="card" style="margin-top:1.1rem;">817 <h2>API</h2>818 <ul>819 <li><code>POST /reset</code> with <code>{ "task_id": ..., "seed": ... }</code></li>820 <li><code>POST /step</code> with a typed <code>BlackstartAction</code></li>821 <li><code>GET /state</code>, <code>/tasks</code>, <code>/grader</code>, <code>/schema</code></li>822 <li><code>GET /command/brief</code> for the multi-agent command snapshot</li>823 <li><code>GET /baseline/next</code> and <code>POST /baseline/step</code> for live heuristic demos</li>824 <li><code>POST /compare</code> for greedy vs heuristic rollout comparison</li>825 </ul>826 </div>827 828 <script>829 let currentObservation = null;830 let autoplayHandle = null;831 832 function setText(id, value) {833 const el = document.getElementById(id);834 if (el) el.textContent = value;835 }836 837 function appendLog(line) {838 const log = document.getElementById('rollout-log');839 log.textContent += line + "\\n";840 log.scrollTop = log.scrollHeight;841 }842 843 function formatTags(tags) {844 return tags.map(tag => `<span class="tag ${tag.kind}">${tag.label}</span>`).join('');845 }846 847 function renderCriticalNodes(nodes) {848 const container = document.getElementById('critical-list');849 container.innerHTML = nodes.map(node => {850 const tags = [];851 tags.push({ kind: node.powered ? 'safe' : (node.backup_minutes_remaining <= 15 ? 'danger' : 'warn'), label: node.powered ? 'powered' : 'backup' });852 tags.push({ kind: node.type === 'hospital' ? 'danger' : node.type === 'water' ? 'warn' : 'safe', label: node.type });853 return `854 <div class="item">855 <div class="item-line">856 <strong>${node.id}</strong>857 <span>${formatTags(tags)}</span>858 </div>859 <div class="small">Demand: ${node.demand_mw} MW | Backup: ${node.backup_minutes_remaining} min | Impact: ${node.population_impact.toLocaleString()}</div>860 </div>861 `;862 }).join('');863 }864 865 function renderAssets(observation) {866 const container = document.getElementById('asset-list');867 const generatorItems = observation.generators.map(item => `868 <div class="item">869 <div class="item-line">870 <strong>${item.id}</strong>871 <span>${item.online ? '<span class="tag safe">online</span>' : '<span class="tag danger">offline</span>'}</span>872 </div>873 <div class="small">Bus: ${item.bus} | Capacity: ${item.capacity_mw} MW</div>874 </div>875 `).join('');876 const lineItems = observation.lines.map(item => `877 <div class="item">878 <div class="item-line">879 <strong>${item.id}</strong>880 <span class="mono">${item.from_bus} → ${item.to_bus}</span>881 </div>882 <div class="small">883 ${item.closed ? '<span class="tag safe">closed</span>' : '<span class="tag warn">open</span>'}884 ${item.tripped ? '<span class="tag danger">tripped</span>' : ''}885 ${item.inspected ? '<span class="tag safe">inspected</span>' : ''}886 ${item.damaged ? '<span class="tag danger">damaged</span>' : ''}887 | Flow ${item.current_flow_mw}/${item.capacity_mw} MW888 </div>889 </div>890 `).join('');891 container.innerHTML = generatorItems + lineItems;892 }893 894 function renderWarnings(warnings) {895 const container = document.getElementById('warning-list');896 container.innerHTML = warnings.map(item => `<li>${item}</li>`).join('');897 }898 899 function actionSummary(action) {900 if (!action) return 'hold current coordination posture';901 const parts = [action.action_type];902 if (action.target_id) parts.push(action.target_id);903 if (action.requested_mw !== undefined && action.requested_mw !== null) parts.push(`${action.requested_mw} MW`);904 return parts.join(' · ');905 }906 907 function renderCommandCenter(commandCenter) {908 setText('metric-trust', `${Math.round(commandCenter.public_trust * 100)}%`);909 setText('metric-coordination', `${Math.round(commandCenter.coordination_score * 100)}%`);910 setText('metric-phase', commandCenter.command_phase);911 setText('metric-dispatch', `${Math.round(commandCenter.resource_state.dispatch_pressure * 100)}%`);912 913 const resources = [914 ['Repair Crews', commandCenter.resource_state.repair_crews_available, commandCenter.resource_state.repair_crews_total],915 ['Mobile Batteries', commandCenter.resource_state.mobile_battery_units_available, commandCenter.resource_state.mobile_battery_units_total],916 ['Telecom Support', commandCenter.resource_state.telecom_support_units_available, commandCenter.resource_state.telecom_support_units_total],917 ];918 const resourceContainer = document.getElementById('resource-list');919 resourceContainer.innerHTML = resources.map(item => `920 <div class="item">921 <div class="item-line">922 <strong>${item[0]}</strong>923 <span class="mono">${item[1]} / ${item[2]}</span>924 </div>925 <div class="small">Operational availability for the command network.</div>926 </div>927 `).join('');928 929 const roleContainer = document.getElementById('role-list');930 roleContainer.innerHTML = commandCenter.role_recommendations.map(item => `931 <div class="item">932 <div class="item-line">933 <strong>${item.role.replaceAll('_', ' ')}</strong>934 <span class="tag ${item.urgency === 'critical' ? 'danger' : item.urgency === 'high' ? 'warn' : 'safe'}">${item.urgency}</span>935 </div>936 <div class="small"><strong>${item.objective}</strong></div>937 <div class="small">${item.rationale}</div>938 <div class="small" style="margin-top:0.4rem;">Proposed action: <span class="mono">${actionSummary(item.proposed_action)}</span></div>939 </div>940 `).join('');941 942 const messageContainer = document.getElementById('message-list');943 messageContainer.innerHTML = commandCenter.coordination_messages.map(item => `944 <div class="item">945 <div class="item-line">946 <strong>${item.role.replaceAll('_', ' ')}</strong>947 <span class="tag ${item.urgency === 'critical' ? 'danger' : item.urgency === 'high' ? 'warn' : 'safe'}">${item.urgency}</span>948 </div>949 <div class="small">To ${item.recipient.replaceAll('_', ' ')}</div>950 <div class="small">${item.summary}</div>951 </div>952 `).join('');953 }954 955 function statusColor(powered, severity) {956 if (powered === true) return '#22c55e';957 if (severity === 'danger') return '#ef4444';958 if (severity === 'warn') return '#eab308';959 return '#64748b';960 }961 962 function updateMap(observation) {963 const subMap = {};964 for (const sub of observation.substations) {965 subMap[sub.id] = sub;966 }967 const lineMap = {};968 for (const line of observation.lines) {969 lineMap[line.id] = line;970 }971 972 const critical = {};973 for (const node of observation.critical_nodes) {974 if (node.type === 'hospital' && !critical.hospital) critical.hospital = node;975 if (node.type === 'telecom' && !critical.telecom) critical.telecom = node;976 if (node.type === 'water' && !critical.water) critical.water = node;977 }978 979 const layout = getScenarioLayout(observation);980 const busBindings = layout.busBindings;981 for (const [id, candidates] of busBindings) {982 const active = candidates.map(c => subMap[c]).find(Boolean);983 const el = document.getElementById(id);984 if (!el) continue;985 const fill = active && active.energized ? '#22c55e' : '#0ea5e9';986 el.setAttribute('fill', fill);987 }988 989 const lineBindings = layout.lineBindings;990 for (const [id, candidates] of lineBindings) {991 const line = candidates.map(c => lineMap[c]).find(Boolean);992 const el = document.getElementById(id);993 if (!line || !el) continue;994 let color = '#64748b';995 if (line.tripped || line.damaged && !line.inspected) color = '#ef4444';996 else if (line.closed) color = '#22c55e';997 else if (line.inspected) color = '#eab308';998 el.setAttribute('stroke', color);999 el.setAttribute('stroke-dasharray', line.closed ? '' : '14 9');1000 }1001 1002 const hospitalEl = document.getElementById('map-critical-hospital');1003 const telecomEl = document.getElementById('map-critical-telecom');1004 const waterEl = document.getElementById('map-critical-water');1005 if (hospitalEl && critical.hospital) {1006 hospitalEl.setAttribute('stroke', statusColor(critical.hospital.powered, critical.hospital.backup_minutes_remaining <= 15 ? 'danger' : 'warn'));1007 }1008 if (telecomEl && critical.telecom) {1009 telecomEl.setAttribute('stroke', statusColor(critical.telecom.powered, critical.telecom.backup_minutes_remaining <= 15 ? 'danger' : 'warn'));1010 }1011 if (waterEl && critical.water) {1012 waterEl.setAttribute('stroke', statusColor(critical.water.powered, critical.water.backup_minutes_remaining <= 15 ? 'danger' : 'warn'));1013 }1014 1015 const timers = [1016 ['map-timer-hospital', critical.hospital],1017 ['map-timer-telecom', critical.telecom],1018 ['map-timer-water', critical.water]1019 ];1020 for (const [id, node] of timers) {1021 const el = document.getElementById(id);1022 if (!el || !node) continue;1023 el.textContent = node.powered ? 'grid' : `${node.backup_minutes_remaining} min`;1024 el.setAttribute('fill', node.powered ? '#86efac' : (node.backup_minutes_remaining <= 15 ? '#fca5a5' : '#fde047'));1025 }1026 1027 const textBindings = [1028 ['map-bus-label-south', layout.busLabels[0]],1029 ['map-bus-label-core', layout.busLabels[1]],1030 ['map-bus-label-medical', layout.busLabels[2]],1031 ['map-bus-label-east', layout.busLabels[3]],1032 ['map-line-label-a', layout.lineLabels[0]],1033 ['map-line-label-b', layout.lineLabels[1]],1034 ['map-line-label-c', layout.lineLabels[2]],1035 ['map-label-hospital', layout.criticalLabels[0]],1036 ['map-label-telecom', layout.criticalLabels[1]],1037 ['map-label-water', layout.criticalLabels[2]]1038 ];1039 for (const [id, label] of textBindings) {1040 const el = document.getElementById(id);1041 if (el) el.textContent = label;1042 }1043 1044 const freqBanner = document.getElementById('map-banner-frequency');1045 const reserveBanner = document.getElementById('map-banner-reserve');1046 const riskBanner = document.getElementById('map-banner-risk');1047 if (freqBanner) {1048 freqBanner.textContent = `Freq ${observation.frequency_hz.toFixed(2)} Hz`;1049 freqBanner.setAttribute('fill', observation.frequency_hz < 59.5 ? '#fca5a5' : observation.frequency_hz < 59.7 ? '#fde047' : '#86efac');1050 }1051 if (reserveBanner) {1052 reserveBanner.textContent = `Reserve ${observation.reserve_margin_mw} MW`;1053 reserveBanner.setAttribute('fill', observation.reserve_margin_mw < 6 ? '#fca5a5' : observation.reserve_margin_mw < 12 ? '#fde047' : '#86efac');1054 }1055 if (riskBanner) {1056 let risk = 'Low risk';1057 let color = '#86efac';1058 if (observation.done && observation.reward_breakdown.catastrophe_penalty > 0) {1059 risk = 'Collapse triggered';1060 color = '#fca5a5';1061 } else if (observation.frequency_hz < 59.5 || observation.reserve_margin_mw < 6) {1062 risk = 'Collapse risk high';1063 color = '#fca5a5';1064 } else if (observation.warnings.some(item => item.toLowerCase().includes('backup below 15'))) {1065 risk = 'Critical timers low';1066 color = '#fde047';1067 }1068 riskBanner.textContent = risk;1069 riskBanner.setAttribute('fill', color);1070 }1071 }1072 1073 function updateOverlays(observation) {1074 const frequencyEl = document.getElementById('overlay-frequency');1075 const reserveEl = document.getElementById('overlay-reserve');1076 const riskEl = document.getElementById('overlay-risk');1077 1078 const setBanner = (el, text, kind) => {1079 if (!el) return;1080 el.textContent = text;1081 el.className = `overlay-banner ${kind}`;1082 };1083 1084 const freqKind = observation.frequency_hz < 59.5 ? 'danger' : observation.frequency_hz < 59.7 ? 'warn' : 'safe';1085 setBanner(frequencyEl, `Grid frequency: ${observation.frequency_hz.toFixed(2)} Hz`, freqKind);1086 1087 const reserveKind = observation.reserve_margin_mw < 6 ? 'danger' : observation.reserve_margin_mw < 12 ? 'warn' : 'safe';1088 setBanner(reserveEl, `Reserve margin: ${observation.reserve_margin_mw} MW`, reserveKind);1089 1090 let riskText = 'No immediate second-collapse risk.';1091 let riskKind = 'safe';1092 if (observation.reward_breakdown.catastrophe_penalty > 0) {1093 riskText = 'Second-collapse conditions triggered.';1094 riskKind = 'danger';1095 } else if (observation.frequency_hz < 59.5 || observation.reserve_margin_mw < 6) {1096 riskText = 'System is one bad action away from another collapse.';1097 riskKind = 'danger';1098 } else if (observation.warnings.some(item => item.toLowerCase().includes('backup below 15'))) {1099 riskText = 'Critical backups are low. Prioritize hospitals and core services.';1100 riskKind = 'warn';1101 }1102 setBanner(riskEl, riskText, riskKind);1103 }1104 1105 function getScenarioLayout(observation) {1106 if (observation.task_id === 'local_blackstart') {1107 return {1108 busBindings: [1109 ['map-bus-south', ['sub_north', 'sub_west', 'sub_south']],1110 ['map-bus-core', ['sub_civic', 'sub_medical_w', 'sub_dispatch']],1111 ['map-bus-medical', ['gen_bus_n', 'gen_bus_w', 'gen_bus_s']],1112 ['map-bus-east', ['sub_north', 'sub_west', 'sub_south']]1113 ],1114 lineBindings: [1115 ['map-line-south-core', ['line_n_1', 'line_w_1', 'line_s_easy_1']],1116 ['map-line-south-medical', ['line_n_2', 'line_w_2', 'line_s_easy_2']],1117 ['map-line-medical-east', ['line_n_2', 'line_w_2', 'line_s_easy_2']],1118 ['map-line-core-east', ['line_n_1', 'line_w_1', 'line_s_easy_1']]1119 ],1120 busLabels: ['District', 'Critical', 'Blackstart', 'Local Feed'],1121 lineLabels: ['feeder path', 'medical spur', 'corridor restore'],1122 criticalLabels: ['Hospital', 'Telecom', 'Water']1123 };1124 }1125 if (observation.task_id === 'island_rejoin') {1126 return {1127 busBindings: [1128 ['map-bus-south', ['sub_east', 'sub_harbor', 'sub_north_i']],1129 ['map-bus-core', ['sub_civic', 'sub_core', 'sub_river']],1130 ['map-bus-medical', ['sub_water', 'sub_telecom_r', 'sub_core']],1131 ['map-bus-east', ['sub_core', 'sub_river', 'sub_telecom_r']]1132 ],1133 lineBindings: [1134 ['map-line-south-core', ['line_tie_1', 'line_tie_hc', 'line_tie_nr']],1135 ['map-line-south-medical', ['line_e_1', 'line_h_1', 'line_nr_1']],1136 ['map-line-medical-east', ['line_c_2', 'line_rr_2', 'line_rr_1']],1137 ['map-line-core-east', ['line_c_1', 'line_core_1', 'line_rr_1']]1138 ],1139 busLabels: ['Island A', 'Island B', 'Critical Spur', 'Sync Path'],1140 lineLabels: ['inspection first', 'island feeder', 'rejoin corridor'],1141 criticalLabels: ['Hospital', 'Telecom', 'Water']1142 };1143 }1144 return {1145 busBindings: [1146 ['map-bus-south', ['sub_south', 'sub_north', 'sub_harbor_h']],1147 ['map-bus-core', ['sub_core', 'sub_core_h', 'sub_core']],1148 ['map-bus-medical', ['sub_medical', 'sub_medical_h', 'sub_water']],1149 ['map-bus-east', ['sub_east', 'sub_corridor_h', 'sub_water_h']]1150 ],1151 lineBindings: [1152 ['map-line-south-core', ['line_tie_city', 'line_tie_nc', 'line_tie_hstorm_1']],1153 ['map-line-south-medical', ['line_s_1', 'line_n_1', 'line_hh_1']],1154 ['map-line-medical-east', ['line_core_2', 'line_c_3', 'line_ch_3']],1155 ['map-line-core-east', ['line_tie_east', 'line_tie_hstorm_2', 'line_ch_1']]1156 ],1157 busLabels: ['Generation', 'Core', 'Critical', 'Outer Grid'],1158 lineLabels: ['damaged tie-line', 'feeder recovery', 'unsafe corridor'],1159 criticalLabels: ['Hospital', 'Telecom', 'Water']1160 };1161 }1162 1163 function renderObservation(observation) {1164 currentObservation = observation;1165 setText('incident-id', observation.incident_id);1166 setText('scenario-title', observation.title);1167 setText('scenario-objective', observation.objective);1168 setText('metric-step', `${observation.step}/${observation.step + observation.steps_remaining}`);1169 setText('metric-frequency', observation.frequency_hz.toFixed(2));1170 setText('metric-reserve', `${observation.reserve_margin_mw} MW`);1171 setText('metric-score', observation.reward_breakdown.current_score.toFixed(2));1172 setText('metric-gen', `${observation.available_generation_mw} MW`);1173 setText('metric-load', `${observation.served_load_mw} MW`);1174 setText('metric-islands', observation.unstable_islands);1175 setText('metric-done', observation.done ? 'yes' : 'no');1176 setText('last-result', observation.last_action_result || 'Scenario reset.');1177 renderCommandCenter(observation.command_center);1178 renderCriticalNodes(observation.critical_nodes);1179 renderAssets(observation);1180 renderWarnings(observation.warnings);1181 updateMap(observation);1182 updateOverlays(observation);1183 renderRewardBreakdown(observation.reward_breakdown);1184 renderConstraints(observation.active_constraints || []);1185 renderNewsFeed(observation.news_feed || []);1186 }1187 1188 function renderConstraints(constraints) {1189 const container = document.getElementById('constraint-list');1190 if (!constraints || constraints.length === 0) {1191 container.innerHTML = '<div class="small" style="color:var(--muted);">No constraints for this scenario.</div>';1192 return;1193 }1194 container.innerHTML = constraints.map(c => {1195 const active = c.active;1196 const violated = c.violated;1197 const kind = violated ? 'danger' : active ? 'warn' : 'safe';1198 const statusLabel = violated ? 'VIOLATED' : active ? 'active' : 'inactive';1199 let detail = '';1200 if (c.constraint_type === 'forbidden_target') {