Spaces:
Sleeping
Sleeping
Update server/dashboard.py
Browse files- server/dashboard.py +154 -50
server/dashboard.py
CHANGED
|
@@ -84,6 +84,10 @@ DIFF_COL = {
|
|
| 84 |
"Trivial": "#94a3b8", "Easy": "#34d399", "Medium": "#22d3ee",
|
| 85 |
"Hard": "#fbbf24", "Very Hard": "#fb923c", "Expert": "#fb7185",
|
| 86 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
PLOT_HEIGHT = 440
|
| 89 |
PLOT_HEIGHT_TALL = 500
|
|
@@ -322,30 +326,45 @@ def create_topology_graph(obs_dict: Dict[str, Any]) -> go.Figure:
|
|
| 322 |
# ═══════════════════════════════════════════════════════════════════
|
| 323 |
# PANEL 2 — FREQUENCY GAUGE
|
| 324 |
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
def create_frequency_gauge(freq_hz: float,
|
| 326 |
history: List[float] | None = None) -> go.Figure:
|
| 327 |
freq_hz = round(freq_hz, 3)
|
| 328 |
-
|
| 329 |
-
if freq_hz < 59.0:
|
| 330 |
-
col, zone = C["crimson"], "BLACKOUT"
|
| 331 |
-
elif freq_hz < 59.2:
|
| 332 |
-
col, zone = C["red"], "CRITICAL"
|
| 333 |
-
elif freq_hz < 59.5:
|
| 334 |
-
col, zone = C["orange"], "DANGER"
|
| 335 |
-
elif freq_hz < 59.7:
|
| 336 |
-
col, zone = C["amber"], "WARNING"
|
| 337 |
-
elif freq_hz <= 60.3:
|
| 338 |
-
col, zone = C["green"], "NOMINAL"
|
| 339 |
-
else:
|
| 340 |
-
col, zone = C["amber"], "ELEVATED"
|
| 341 |
-
|
| 342 |
-
delta = round(freq_hz - 60.0, 3)
|
| 343 |
-
delta_sign = "+" if delta >= 0 else ""
|
| 344 |
|
| 345 |
fig = make_subplots(
|
| 346 |
rows=2, cols=1,
|
| 347 |
-
row_heights=[0.
|
| 348 |
-
vertical_spacing=0.
|
| 349 |
specs=[[{"type": "indicator"}], [{"type": "scatter"}]],
|
| 350 |
)
|
| 351 |
|
|
@@ -354,7 +373,7 @@ def create_frequency_gauge(freq_hz: float,
|
|
| 354 |
value=freq_hz,
|
| 355 |
number=dict(
|
| 356 |
suffix=" Hz",
|
| 357 |
-
font=dict(size=
|
| 358 |
valueformat=".3f",
|
| 359 |
),
|
| 360 |
title=dict(text=""),
|
|
@@ -364,7 +383,7 @@ def create_frequency_gauge(freq_hz: float,
|
|
| 364 |
tickvals=[59.0, 59.5, 60.0, 60.5, 61.0],
|
| 365 |
tickwidth=1,
|
| 366 |
tickcolor=C["text_mid"],
|
| 367 |
-
tickfont=dict(size=
|
| 368 |
),
|
| 369 |
bar=dict(color=col, thickness=0.30),
|
| 370 |
bgcolor="#1e293b",
|
|
@@ -407,21 +426,7 @@ def create_frequency_gauge(freq_hz: float,
|
|
| 407 |
row=2, col=1,
|
| 408 |
)
|
| 409 |
fig.update_layout(
|
| 410 |
-
**_pro_layout(height=PLOT_HEIGHT, margin=dict(l=
|
| 411 |
-
annotations=[
|
| 412 |
-
dict(
|
| 413 |
-
text=f"Grid Frequency · {zone}",
|
| 414 |
-
xref="paper", yref="paper", x=0.5, y=1.0,
|
| 415 |
-
showarrow=False, xanchor="center", yanchor="bottom",
|
| 416 |
-
font=dict(size=FS["zone"], color=col),
|
| 417 |
-
),
|
| 418 |
-
dict(
|
| 419 |
-
text=f"Δ {delta_sign}{delta:.3f} Hz from nominal",
|
| 420 |
-
xref="paper", yref="paper", x=0.5, y=0.955,
|
| 421 |
-
showarrow=False, xanchor="center", yanchor="bottom",
|
| 422 |
-
font=dict(size=11, color=C["text_mid"]),
|
| 423 |
-
),
|
| 424 |
-
],
|
| 425 |
)
|
| 426 |
return fig
|
| 427 |
|
|
@@ -995,16 +1000,16 @@ def format_mission_board(scores: Dict[int, float], active_id: int) -> str:
|
|
| 995 |
pct = int(sc * 100)
|
| 996 |
cards.append(f"""
|
| 997 |
<div class="mission-card {active}">
|
| 998 |
-
<div class="mission-
|
| 999 |
<span class="mission-id">T{tid}</span>
|
| 1000 |
-
<span class="mission-diff" style="color:{dcol}">{diff}</span>
|
| 1001 |
</div>
|
| 1002 |
<div class="mission-name">{t['name']}</div>
|
| 1003 |
<div class="mission-bar"><div class="mission-fill" style="width:{pct}%;background:{dcol}"></div></div>
|
| 1004 |
<div class="mission-score">{sc:.2f} / 1.00</div>
|
| 1005 |
</div>
|
| 1006 |
""")
|
| 1007 |
-
return f'<div class="mission-board">{"".join(cards)}</div>'
|
| 1008 |
|
| 1009 |
|
| 1010 |
# ═══════════════════════════════════════════════════════════════════
|
|
@@ -1194,6 +1199,7 @@ def _refresh():
|
|
| 1194 |
return (
|
| 1195 |
_safe_chart(create_topology_graph, obs),
|
| 1196 |
_safe_chart(create_frequency_gauge, st.frequency_hz, st.freq_history),
|
|
|
|
| 1197 |
_safe_chart(create_power_flow_sankey, obs),
|
| 1198 |
_safe_chart(create_task_radar, st.task_scores),
|
| 1199 |
_safe_chart(create_reward_breakdown, st.reward_history),
|
|
@@ -1389,6 +1395,10 @@ body, .gradio-container, .main, .wrap, .app {{
|
|
| 1389 |
position: relative !important;
|
| 1390 |
z-index: 3 !important;
|
| 1391 |
isolation: isolate !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1392 |
}}
|
| 1393 |
.layout-main-col {{
|
| 1394 |
position: relative !important;
|
|
@@ -1435,6 +1445,18 @@ body, .gradio-container, .main, .wrap, .app {{
|
|
| 1435 |
.plot-panel-wide .js-plotly-plot, .plot-panel-wide .plotly-graph-div {{
|
| 1436 |
min-height: {PLOT_HEIGHT_WIDE}px !important;
|
| 1437 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1438 |
.mission-board, .legend-block, .ctrl-box, .kpi-grid, .fleet-strip {{
|
| 1439 |
position: relative;
|
| 1440 |
z-index: 1;
|
|
@@ -1573,6 +1595,53 @@ body, .gradio-container, .main, .wrap, .app {{
|
|
| 1573 |
margin-left: auto;
|
| 1574 |
}}
|
| 1575 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1576 |
.mission-board {{
|
| 1577 |
display: grid;
|
| 1578 |
grid-template-columns: repeat(3, 1fr);
|
|
@@ -1584,15 +1653,48 @@ body, .gradio-container, .main, .wrap, .app {{
|
|
| 1584 |
border: 1px solid var(--border);
|
| 1585 |
border-radius: 8px;
|
| 1586 |
padding: 10px 12px;
|
|
|
|
|
|
|
| 1587 |
}}
|
| 1588 |
.mission-active {{
|
| 1589 |
border-color: {C['accent']};
|
| 1590 |
box-shadow: 0 0 0 1px rgba(34,211,238,0.25);
|
| 1591 |
}}
|
| 1592 |
-
.mission-
|
| 1593 |
-
|
| 1594 |
-
|
| 1595 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1596 |
.mission-bar {{ height: 4px; background: {C['border']}; border-radius: 2px; overflow: hidden; }}
|
| 1597 |
.mission-fill {{ height: 100%; border-radius: 2px; }}
|
| 1598 |
.mission-score {{ font-family: "JetBrains Mono", monospace; font-size: 0.68rem; color: var(--text-mid); margin-top: 6px; }}
|
|
@@ -1614,7 +1716,7 @@ body, .gradio-container, .main, .wrap, .app {{
|
|
| 1614 |
|
| 1615 |
@media (max-width: 1100px) {{
|
| 1616 |
.kpi-grid, .fleet-strip {{ grid-template-columns: repeat(3, 1fr); }}
|
| 1617 |
-
.mission-board {{ grid-template-columns: repeat(2, 1fr); }}
|
| 1618 |
}}
|
| 1619 |
@media (max-width: 900px) {{
|
| 1620 |
.layout-main-row {{
|
|
@@ -1698,7 +1800,7 @@ def create_dashboard() -> gr.Blocks:
|
|
| 1698 |
)
|
| 1699 |
|
| 1700 |
with gr.Row(equal_height=False, elem_classes=["layout-main-row"]):
|
| 1701 |
-
with gr.Column(scale=1, min_width=
|
| 1702 |
gr.HTML('<div class="ctrl-box"><h3>Episode Control</h3></div>')
|
| 1703 |
task_select = gr.Slider(0, 5, value=0, step=1, label="Mission / Task ID")
|
| 1704 |
seed_input = gr.Number(value=42, label="Random seed", precision=0)
|
|
@@ -1752,9 +1854,10 @@ def create_dashboard() -> gr.Blocks:
|
|
| 1752 |
with gr.Column(scale=3):
|
| 1753 |
gr.HTML(_shell('Grid Topology · 20-node network', C['accent']))
|
| 1754 |
topo_plot = gr.Plot(label='', elem_classes=['plot-panel', 'plot-panel-tall'])
|
| 1755 |
-
with gr.Column(scale=2):
|
| 1756 |
gr.HTML(_shell('Grid Frequency Monitor', C['green']))
|
| 1757 |
-
|
|
|
|
| 1758 |
|
| 1759 |
with gr.Row(equal_height=False, elem_classes=["layout-chart-row"]):
|
| 1760 |
with gr.Column(scale=3):
|
|
@@ -1764,9 +1867,10 @@ def create_dashboard() -> gr.Blocks:
|
|
| 1764 |
gr.HTML(_shell('Mission Performance Radar', C['purple']))
|
| 1765 |
radar_plot = gr.Plot(label='', elem_classes=['plot-panel'])
|
| 1766 |
|
| 1767 |
-
gr.
|
| 1768 |
-
|
| 1769 |
-
|
|
|
|
| 1770 |
|
| 1771 |
gr.HTML(_section('03', 'Cyber Layer', 'SCADA · Kirchhoff oracle · agent log'))
|
| 1772 |
with gr.Row(equal_height=False, elem_classes=["layout-chart-row"]):
|
|
@@ -1785,7 +1889,7 @@ def create_dashboard() -> gr.Blocks:
|
|
| 1785 |
|
| 1786 |
|
| 1787 |
all_out = [
|
| 1788 |
-
topo_plot, freq_plot, sankey_plot, radar_plot, reward_plot,
|
| 1789 |
threat_feed, action_trace, status_bar, kpi_panel, mission_panel,
|
| 1790 |
]
|
| 1791 |
reset_btn.click(fn=on_reset, inputs=[task_select, seed_input], outputs=all_out)
|
|
|
|
| 84 |
"Trivial": "#94a3b8", "Easy": "#34d399", "Medium": "#22d3ee",
|
| 85 |
"Hard": "#fbbf24", "Very Hard": "#fb923c", "Expert": "#fb7185",
|
| 86 |
}
|
| 87 |
+
DIFF_SHORT = {
|
| 88 |
+
"Trivial": "Trivial", "Easy": "Easy", "Medium": "Medium",
|
| 89 |
+
"Hard": "Hard", "Very Hard": "V.Hard", "Expert": "Expert",
|
| 90 |
+
}
|
| 91 |
|
| 92 |
PLOT_HEIGHT = 440
|
| 93 |
PLOT_HEIGHT_TALL = 500
|
|
|
|
| 326 |
# ═══════════════════════════════════════════════════════════════════
|
| 327 |
# PANEL 2 — FREQUENCY GAUGE
|
| 328 |
# ═══════════════════════════════════════════════════════════════════
|
| 329 |
+
def _freq_zone(freq_hz: float) -> tuple[str, str]:
|
| 330 |
+
"""Return (accent_color, zone_label) for a grid frequency reading."""
|
| 331 |
+
f = round(freq_hz, 3)
|
| 332 |
+
if f < 59.0:
|
| 333 |
+
return C["crimson"], "BLACKOUT"
|
| 334 |
+
if f < 59.2:
|
| 335 |
+
return C["red"], "CRITICAL"
|
| 336 |
+
if f < 59.5:
|
| 337 |
+
return C["orange"], "DANGER"
|
| 338 |
+
if f < 59.7:
|
| 339 |
+
return C["amber"], "WARNING"
|
| 340 |
+
if f <= 60.3:
|
| 341 |
+
return C["green"], "NOMINAL"
|
| 342 |
+
return C["amber"], "ELEVATED"
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
def format_freq_status(freq_hz: float) -> str:
|
| 346 |
+
"""HTML status strip above the gauge — keeps titles out of the Plotly canvas."""
|
| 347 |
+
col, zone = _freq_zone(freq_hz)
|
| 348 |
+
delta = round(freq_hz - 60.0, 3)
|
| 349 |
+
sign = "+" if delta >= 0 else ""
|
| 350 |
+
return (
|
| 351 |
+
f'<div class="freq-status-bar">'
|
| 352 |
+
f'<span class="freq-status-zone" style="color:{col}">Grid Frequency · {zone}</span>'
|
| 353 |
+
f'<span class="freq-status-delta">Δ {sign}{delta:.3f} Hz from nominal</span>'
|
| 354 |
+
f'<span class="freq-status-val" style="color:{col}">{freq_hz:.3f} Hz</span>'
|
| 355 |
+
f'</div>'
|
| 356 |
+
)
|
| 357 |
+
|
| 358 |
+
|
| 359 |
def create_frequency_gauge(freq_hz: float,
|
| 360 |
history: List[float] | None = None) -> go.Figure:
|
| 361 |
freq_hz = round(freq_hz, 3)
|
| 362 |
+
col, zone = _freq_zone(freq_hz)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
|
| 364 |
fig = make_subplots(
|
| 365 |
rows=2, cols=1,
|
| 366 |
+
row_heights=[0.52, 0.48],
|
| 367 |
+
vertical_spacing=0.26,
|
| 368 |
specs=[[{"type": "indicator"}], [{"type": "scatter"}]],
|
| 369 |
)
|
| 370 |
|
|
|
|
| 373 |
value=freq_hz,
|
| 374 |
number=dict(
|
| 375 |
suffix=" Hz",
|
| 376 |
+
font=dict(size=28, color=col, family="Inter, sans-serif"),
|
| 377 |
valueformat=".3f",
|
| 378 |
),
|
| 379 |
title=dict(text=""),
|
|
|
|
| 383 |
tickvals=[59.0, 59.5, 60.0, 60.5, 61.0],
|
| 384 |
tickwidth=1,
|
| 385 |
tickcolor=C["text_mid"],
|
| 386 |
+
tickfont=dict(size=10, color=C["text_mid"]),
|
| 387 |
),
|
| 388 |
bar=dict(color=col, thickness=0.30),
|
| 389 |
bgcolor="#1e293b",
|
|
|
|
| 426 |
row=2, col=1,
|
| 427 |
)
|
| 428 |
fig.update_layout(
|
| 429 |
+
**_pro_layout(height=PLOT_HEIGHT, margin=dict(l=32, r=32, t=16, b=32)),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
)
|
| 431 |
return fig
|
| 432 |
|
|
|
|
| 1000 |
pct = int(sc * 100)
|
| 1001 |
cards.append(f"""
|
| 1002 |
<div class="mission-card {active}">
|
| 1003 |
+
<div class="mission-head">
|
| 1004 |
<span class="mission-id">T{tid}</span>
|
| 1005 |
+
<span class="mission-diff-badge" style="color:{dcol};border-color:{dcol}">{DIFF_SHORT.get(diff, diff)}</span>
|
| 1006 |
</div>
|
| 1007 |
<div class="mission-name">{t['name']}</div>
|
| 1008 |
<div class="mission-bar"><div class="mission-fill" style="width:{pct}%;background:{dcol}"></div></div>
|
| 1009 |
<div class="mission-score">{sc:.2f} / 1.00</div>
|
| 1010 |
</div>
|
| 1011 |
""")
|
| 1012 |
+
return f'<div class="mission-wrap"><div class="mission-board">{"".join(cards)}</div></div>'
|
| 1013 |
|
| 1014 |
|
| 1015 |
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
| 1199 |
return (
|
| 1200 |
_safe_chart(create_topology_graph, obs),
|
| 1201 |
_safe_chart(create_frequency_gauge, st.frequency_hz, st.freq_history),
|
| 1202 |
+
format_freq_status(st.frequency_hz),
|
| 1203 |
_safe_chart(create_power_flow_sankey, obs),
|
| 1204 |
_safe_chart(create_task_radar, st.task_scores),
|
| 1205 |
_safe_chart(create_reward_breakdown, st.reward_history),
|
|
|
|
| 1395 |
position: relative !important;
|
| 1396 |
z-index: 3 !important;
|
| 1397 |
isolation: isolate !important;
|
| 1398 |
+
flex: 0 0 min(340px, 100%) !important;
|
| 1399 |
+
max-width: 100% !important;
|
| 1400 |
+
overflow-x: hidden !important;
|
| 1401 |
+
overflow-y: visible !important;
|
| 1402 |
}}
|
| 1403 |
.layout-main-col {{
|
| 1404 |
position: relative !important;
|
|
|
|
| 1445 |
.plot-panel-wide .js-plotly-plot, .plot-panel-wide .plotly-graph-div {{
|
| 1446 |
min-height: {PLOT_HEIGHT_WIDE}px !important;
|
| 1447 |
}}
|
| 1448 |
+
.plot-panel-reward {{
|
| 1449 |
+
width: 100% !important;
|
| 1450 |
+
max-width: 100% !important;
|
| 1451 |
+
margin-top: 4px !important;
|
| 1452 |
+
}}
|
| 1453 |
+
.layout-reward-section {{
|
| 1454 |
+
width: 100% !important;
|
| 1455 |
+
max-width: 100% !important;
|
| 1456 |
+
overflow: hidden !important;
|
| 1457 |
+
clear: both !important;
|
| 1458 |
+
margin-bottom: 20px !important;
|
| 1459 |
+
}}
|
| 1460 |
.mission-board, .legend-block, .ctrl-box, .kpi-grid, .fleet-strip {{
|
| 1461 |
position: relative;
|
| 1462 |
z-index: 1;
|
|
|
|
| 1595 |
margin-left: auto;
|
| 1596 |
}}
|
| 1597 |
|
| 1598 |
+
/* Frequency status — lives outside Plotly so titles never collide with the gauge */
|
| 1599 |
+
.freq-status-bar {{
|
| 1600 |
+
display: flex;
|
| 1601 |
+
flex-direction: column;
|
| 1602 |
+
gap: 4px;
|
| 1603 |
+
padding: 10px 14px;
|
| 1604 |
+
margin-bottom: 8px;
|
| 1605 |
+
background: {C['surface_hi']};
|
| 1606 |
+
border: 1px solid var(--border);
|
| 1607 |
+
border-radius: var(--radius);
|
| 1608 |
+
}}
|
| 1609 |
+
.freq-status-zone {{
|
| 1610 |
+
font-size: 0.8rem;
|
| 1611 |
+
font-weight: 700;
|
| 1612 |
+
letter-spacing: 0.06em;
|
| 1613 |
+
text-transform: uppercase;
|
| 1614 |
+
}}
|
| 1615 |
+
.freq-status-delta {{
|
| 1616 |
+
font-size: 0.72rem;
|
| 1617 |
+
color: var(--text-mid);
|
| 1618 |
+
}}
|
| 1619 |
+
.freq-status-val {{
|
| 1620 |
+
font-family: "JetBrains Mono", monospace;
|
| 1621 |
+
font-size: 1.1rem;
|
| 1622 |
+
font-weight: 700;
|
| 1623 |
+
margin-top: 2px;
|
| 1624 |
+
}}
|
| 1625 |
+
.freq-col {{
|
| 1626 |
+
min-width: 0 !important;
|
| 1627 |
+
max-width: 100% !important;
|
| 1628 |
+
}}
|
| 1629 |
+
.plot-panel-freq .js-plotly-plot, .plot-panel-freq .plotly-graph-div {{
|
| 1630 |
+
min-height: {PLOT_HEIGHT}px !important;
|
| 1631 |
+
}}
|
| 1632 |
+
|
| 1633 |
+
.mission-wrap {{
|
| 1634 |
+
width: 100%;
|
| 1635 |
+
max-width: 100%;
|
| 1636 |
+
overflow: hidden;
|
| 1637 |
+
margin-bottom: 12px;
|
| 1638 |
+
}}
|
| 1639 |
+
.layout-sidebar .mission-board {{
|
| 1640 |
+
display: flex;
|
| 1641 |
+
flex-direction: column;
|
| 1642 |
+
gap: 10px;
|
| 1643 |
+
grid-template-columns: unset;
|
| 1644 |
+
}}
|
| 1645 |
.mission-board {{
|
| 1646 |
display: grid;
|
| 1647 |
grid-template-columns: repeat(3, 1fr);
|
|
|
|
| 1653 |
border: 1px solid var(--border);
|
| 1654 |
border-radius: 8px;
|
| 1655 |
padding: 10px 12px;
|
| 1656 |
+
min-width: 0;
|
| 1657 |
+
overflow: hidden;
|
| 1658 |
}}
|
| 1659 |
.mission-active {{
|
| 1660 |
border-color: {C['accent']};
|
| 1661 |
box-shadow: 0 0 0 1px rgba(34,211,238,0.25);
|
| 1662 |
}}
|
| 1663 |
+
.mission-head {{
|
| 1664 |
+
display: flex;
|
| 1665 |
+
align-items: center;
|
| 1666 |
+
justify-content: space-between;
|
| 1667 |
+
gap: 10px;
|
| 1668 |
+
min-height: 26px;
|
| 1669 |
+
margin-bottom: 4px;
|
| 1670 |
+
}}
|
| 1671 |
+
.mission-id {{
|
| 1672 |
+
flex-shrink: 0;
|
| 1673 |
+
font-family: "JetBrains Mono", monospace;
|
| 1674 |
+
font-weight: 700;
|
| 1675 |
+
font-size: 0.9rem;
|
| 1676 |
+
color: {C['accent']};
|
| 1677 |
+
}}
|
| 1678 |
+
.mission-diff-badge {{
|
| 1679 |
+
flex-shrink: 0;
|
| 1680 |
+
font-size: 0.58rem;
|
| 1681 |
+
font-weight: 700;
|
| 1682 |
+
text-transform: uppercase;
|
| 1683 |
+
letter-spacing: 0.06em;
|
| 1684 |
+
padding: 2px 6px;
|
| 1685 |
+
border-radius: 4px;
|
| 1686 |
+
border: 1px solid;
|
| 1687 |
+
background: rgba(0,0,0,0.25);
|
| 1688 |
+
white-space: nowrap;
|
| 1689 |
+
}}
|
| 1690 |
+
.mission-name {{
|
| 1691 |
+
font-size: 0.78rem;
|
| 1692 |
+
color: var(--text);
|
| 1693 |
+
margin: 4px 0 6px;
|
| 1694 |
+
font-weight: 600;
|
| 1695 |
+
line-height: 1.35;
|
| 1696 |
+
word-break: break-word;
|
| 1697 |
+
}}
|
| 1698 |
.mission-bar {{ height: 4px; background: {C['border']}; border-radius: 2px; overflow: hidden; }}
|
| 1699 |
.mission-fill {{ height: 100%; border-radius: 2px; }}
|
| 1700 |
.mission-score {{ font-family: "JetBrains Mono", monospace; font-size: 0.68rem; color: var(--text-mid); margin-top: 6px; }}
|
|
|
|
| 1716 |
|
| 1717 |
@media (max-width: 1100px) {{
|
| 1718 |
.kpi-grid, .fleet-strip {{ grid-template-columns: repeat(3, 1fr); }}
|
| 1719 |
+
.layout-main-col .mission-board {{ grid-template-columns: repeat(2, 1fr); }}
|
| 1720 |
}}
|
| 1721 |
@media (max-width: 900px) {{
|
| 1722 |
.layout-main-row {{
|
|
|
|
| 1800 |
)
|
| 1801 |
|
| 1802 |
with gr.Row(equal_height=False, elem_classes=["layout-main-row"]):
|
| 1803 |
+
with gr.Column(scale=1, min_width=300, elem_classes=["layout-sidebar"]):
|
| 1804 |
gr.HTML('<div class="ctrl-box"><h3>Episode Control</h3></div>')
|
| 1805 |
task_select = gr.Slider(0, 5, value=0, step=1, label="Mission / Task ID")
|
| 1806 |
seed_input = gr.Number(value=42, label="Random seed", precision=0)
|
|
|
|
| 1854 |
with gr.Column(scale=3):
|
| 1855 |
gr.HTML(_shell('Grid Topology · 20-node network', C['accent']))
|
| 1856 |
topo_plot = gr.Plot(label='', elem_classes=['plot-panel', 'plot-panel-tall'])
|
| 1857 |
+
with gr.Column(scale=2, elem_classes=["freq-col"]):
|
| 1858 |
gr.HTML(_shell('Grid Frequency Monitor', C['green']))
|
| 1859 |
+
freq_status = gr.HTML(format_freq_status(dashboard_state.frequency_hz))
|
| 1860 |
+
freq_plot = gr.Plot(label='', elem_classes=['plot-panel', 'plot-panel-freq'])
|
| 1861 |
|
| 1862 |
with gr.Row(equal_height=False, elem_classes=["layout-chart-row"]):
|
| 1863 |
with gr.Column(scale=3):
|
|
|
|
| 1867 |
gr.HTML(_shell('Mission Performance Radar', C['purple']))
|
| 1868 |
radar_plot = gr.Plot(label='', elem_classes=['plot-panel'])
|
| 1869 |
|
| 1870 |
+
with gr.Column(elem_classes=["layout-reward-section"]):
|
| 1871 |
+
gr.HTML(_section('02', 'Agent Training', 'Reward rubric signals'))
|
| 1872 |
+
gr.HTML(_shell('Reward Signal Decomposition', C['green']))
|
| 1873 |
+
reward_plot = gr.Plot(label='', elem_classes=['plot-panel', 'plot-panel-reward'])
|
| 1874 |
|
| 1875 |
gr.HTML(_section('03', 'Cyber Layer', 'SCADA · Kirchhoff oracle · agent log'))
|
| 1876 |
with gr.Row(equal_height=False, elem_classes=["layout-chart-row"]):
|
|
|
|
| 1889 |
|
| 1890 |
|
| 1891 |
all_out = [
|
| 1892 |
+
topo_plot, freq_plot, freq_status, sankey_plot, radar_plot, reward_plot,
|
| 1893 |
threat_feed, action_trace, status_bar, kpi_panel, mission_panel,
|
| 1894 |
]
|
| 1895 |
reset_btn.click(fn=on_reset, inputs=[task_select, seed_input], outputs=all_out)
|