CoolFace
Apppublic

SidditaVarma/Built-different

sourceHugging Facemitupdated 5mo agoView on Hugging Face
0likes
web_ui.py1392 linesDownload Raw Back to server
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 -&gt; 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') {

Showing the first 1,200 of 1392 lines. Download the file for the rest.