Abineshsdata commited on
Commit
f33bec9
·
verified ·
1 Parent(s): 905f6cc

Update server/dashboard.py

Browse files
Files changed (1) hide show
  1. 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.58, 0.42],
348
- vertical_spacing=0.22,
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=30, color=col, family="Inter, sans-serif"),
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=11, color=C["text_mid"]),
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=36, r=36, t=88, b=36)),
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-top">
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-top {{ display: flex; justify-content: space-between; align-items: center; }}
1593
- .mission-id {{ font-family: "JetBrains Mono", monospace; font-weight: 700; color: {C['accent']}; }}
1594
- .mission-diff {{ font-size: 0.65rem; font-weight: 700; text-transform: uppercase; }}
1595
- .mission-name {{ font-size: 0.78rem; color: var(--text); margin: 6px 0; font-weight: 600; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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=240, elem_classes=["layout-sidebar"]):
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
- freq_plot = gr.Plot(label='', elem_classes=['plot-panel'])
 
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.HTML(_section('02', 'Agent Training', 'Reward rubric signals'))
1768
- gr.HTML(_shell('Reward Signal Decomposition', C['green']))
1769
- reward_plot = gr.Plot(label='', elem_classes=['plot-panel'])
 
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)