4runr-os 2.10.74 → 2.10.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,7 @@ use crate::storage::Cache;
5
5
  use crate::ui::agent_builder::AgentBuilderState;
6
6
  use crate::ui::run_manager::RunManagerState;
7
7
  use crate::ui::sentinel_config::SentinelConfigState;
8
+ use crate::ui::shield_dashboard::ShieldDashboardState;
8
9
  use crate::ui::settings::SettingsState;
9
10
  use crate::websocket::WebSocketClient;
10
11
  use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
@@ -431,6 +432,7 @@ pub struct AppState {
431
432
  pub agent_builder: AgentBuilderState,
432
433
  pub run_manager: RunManagerState,
433
434
  pub sentinel_config: SentinelConfigState,
435
+ pub shield_dashboard: ShieldDashboardState,
434
436
  pub settings: SettingsState,
435
437
  pub agent_list: AgentListState,
436
438
  pub connection_portal: ConnectionPortalState,
@@ -463,6 +465,7 @@ pub struct AppState {
463
465
 
464
466
  pub pending_sentinel_load_id: Option<String>,
465
467
  pub pending_sentinel_apply_id: Option<String>,
468
+ pub pending_shield_load_id: Option<String>,
466
469
 
467
470
  // Deletion confirmation (Step 5.5)
468
471
  pub agent_delete_requested: bool,
@@ -530,6 +533,7 @@ impl Default for AppState {
530
533
  agent_builder: AgentBuilderState::default(),
531
534
  run_manager: RunManagerState::default(),
532
535
  sentinel_config: SentinelConfigState::default(),
536
+ shield_dashboard: ShieldDashboardState::default(),
533
537
  settings: SettingsState::default(),
534
538
  agent_list: AgentListState::default(),
535
539
  connection_portal: ConnectionPortalState::default(),
@@ -554,6 +558,7 @@ impl Default for AppState {
554
558
  pending_run_quick_id: None,
555
559
  pending_sentinel_load_id: None,
556
560
  pending_sentinel_apply_id: None,
561
+ pending_shield_load_id: None,
557
562
  agent_delete_requested: false,
558
563
  operation_mode: OperationMode::Local,
559
564
  cache: Cache::new().ok(),
@@ -832,6 +837,40 @@ impl App {
832
837
  }
833
838
  }
834
839
 
840
+ pub fn open_shield_dashboard(&mut self, ws: Option<&WebSocketClient>) {
841
+ self.state.pending_shield_load_id = None;
842
+ self.push_overlay(Screen::ShieldDashboard);
843
+ self.state
844
+ .logs
845
+ .push_back("[NAV] Opening Shield (production safety layer)...".into());
846
+ if self.state.operation_mode == OperationMode::Connected {
847
+ if let Some(ws) = ws {
848
+ self.begin_shield_load_request(ws);
849
+ }
850
+ } else {
851
+ self.state.shield_dashboard.error = Some(
852
+ "Connect to Gateway first (connect portal).".to_string(),
853
+ );
854
+ }
855
+ self.request_immediate_render("open_shield_dashboard");
856
+ }
857
+
858
+ pub fn begin_shield_load_request(&mut self, ws: &WebSocketClient) {
859
+ if self.state.operation_mode != OperationMode::Connected {
860
+ return;
861
+ }
862
+ if self.state.pending_shield_load_id.is_some() {
863
+ return;
864
+ }
865
+ self.state.shield_dashboard.loading = true;
866
+ self.state.shield_dashboard.error = None;
867
+ if let Ok(id) = ws.send_command("shield.load", None) {
868
+ self.state.pending_shield_load_id = Some(id);
869
+ } else {
870
+ self.state.shield_dashboard.loading = false;
871
+ }
872
+ }
873
+
835
874
  pub fn open_sentinel_config(&mut self, ws: Option<&WebSocketClient>) {
836
875
  self.state.pending_sentinel_load_id = None;
837
876
  self.state.pending_sentinel_apply_id = None;
@@ -1320,6 +1359,11 @@ impl App {
1320
1359
  return self.handle_sentinel_config_input(key, ws_client);
1321
1360
  }
1322
1361
 
1362
+ // === SHIELD DASHBOARD INPUT HANDLING ===
1363
+ if self.state.navigation.current_screen() == &Screen::ShieldDashboard {
1364
+ return self.handle_shield_dashboard_input(key, ws_client);
1365
+ }
1366
+
1323
1367
  // === SETTINGS INPUT HANDLING ===
1324
1368
  if self.state.navigation.current_screen() == &Screen::Settings {
1325
1369
  return self.handle_settings_input(key);
@@ -1493,6 +1537,10 @@ impl App {
1493
1537
  " sentinel - Configure Sentinel policies (templates)"
1494
1538
  .to_string(),
1495
1539
  );
1540
+ self.state.logs.push_back(
1541
+ " shield - Shield safety dashboard (live metrics + enforcement)"
1542
+ .to_string(),
1543
+ );
1496
1544
  self.state.logs.push_back(
1497
1545
  " build - Open Agent Builder (6-step wizard)"
1498
1546
  .to_string(),
@@ -1574,55 +1622,8 @@ impl App {
1574
1622
  "sentinel" | "sentinel config" | "sentinel policies" => {
1575
1623
  self.open_sentinel_config(ws_client);
1576
1624
  }
1577
- "shield" | "shield status" => {
1578
- if self.state.operation_mode == OperationMode::Connected {
1579
- if let Some(ws) = ws_client {
1580
- if let Ok(id) = ws.send_command("shield.status", None) {
1581
- self.add_log(format!(
1582
- "[SHIELD] Fetching live status (id: {})",
1583
- &id[id.len().saturating_sub(8)..]
1584
- ));
1585
- }
1586
- }
1587
- } else {
1588
- self.add_log(
1589
- "[SHIELD] Connect to Gateway first.".to_string(),
1590
- );
1591
- }
1592
- }
1593
- "shield probe" => {
1594
- if self.state.operation_mode == OperationMode::Connected {
1595
- if let Some(ws) = ws_client {
1596
- if let Ok(id) = ws.send_command("shield.probe", None) {
1597
- self.add_log(format!(
1598
- "[SHIELD] Probing injection detector (id: {})",
1599
- &id[id.len().saturating_sub(8)..]
1600
- ));
1601
- }
1602
- }
1603
- } else {
1604
- self.add_log(
1605
- "[SHIELD] Connect to Gateway first.".to_string(),
1606
- );
1607
- }
1608
- }
1609
- "shield demo" => {
1610
- if self.state.operation_mode == OperationMode::Connected {
1611
- if let Some(ws) = ws_client {
1612
- if let Ok(id) = ws.send_command("shield.demo", None) {
1613
- self.add_log(format!(
1614
- "[SHIELD] Starting demo run — check Run Manager (id: {})",
1615
- &id[id.len().saturating_sub(8)..]
1616
- ));
1617
- }
1618
- self.push_overlay(Screen::RunManager);
1619
- self.begin_run_list_request(ws, false);
1620
- }
1621
- } else {
1622
- self.add_log(
1623
- "[SHIELD] Connect to Gateway first.".to_string(),
1624
- );
1625
- }
1625
+ "shield" | "shield status" | "shield dashboard" => {
1626
+ self.open_shield_dashboard(ws_client);
1626
1627
  }
1627
1628
  "config" | "settings" => {
1628
1629
  self.push_overlay(Screen::Settings);
@@ -1908,6 +1909,10 @@ impl App {
1908
1909
  use crate::ui::sentinel_config;
1909
1910
  sentinel_config::render(f, &self.state);
1910
1911
  }
1912
+ Screen::ShieldDashboard => {
1913
+ use crate::ui::shield_dashboard;
1914
+ shield_dashboard::render(f, &self.state);
1915
+ }
1911
1916
  Screen::Settings => {
1912
1917
  use crate::ui::settings;
1913
1918
  settings::render(f, &self.state);
@@ -2642,6 +2647,50 @@ impl App {
2642
2647
  Ok(false)
2643
2648
  }
2644
2649
 
2650
+ fn handle_shield_dashboard_input(
2651
+ &mut self,
2652
+ key: KeyEvent,
2653
+ ws_client: Option<&WebSocketClient>,
2654
+ ) -> anyhow::Result<bool> {
2655
+ use crossterm::event::KeyModifiers;
2656
+
2657
+ if key.modifiers.contains(KeyModifiers::CONTROL) {
2658
+ match key.code {
2659
+ KeyCode::Char('c') | KeyCode::Char('q') => return Ok(true),
2660
+ _ => return Ok(false),
2661
+ }
2662
+ }
2663
+
2664
+ match key.code {
2665
+ KeyCode::Char('r') | KeyCode::Char('R') => {
2666
+ if self.state.operation_mode == OperationMode::Connected {
2667
+ if let Some(ws) = ws_client {
2668
+ self.begin_shield_load_request(ws);
2669
+ }
2670
+ }
2671
+ self.request_render("shield_refresh");
2672
+ }
2673
+ KeyCode::Char('a') | KeyCode::Char('A') => {
2674
+ self.state.shield_dashboard.auto_refresh_enabled =
2675
+ !self.state.shield_dashboard.auto_refresh_enabled;
2676
+ let status = if self.state.shield_dashboard.auto_refresh_enabled {
2677
+ "ON"
2678
+ } else {
2679
+ "OFF"
2680
+ };
2681
+ self.add_log(format!("[SHIELD] Live refresh {}", status));
2682
+ self.request_render("shield_live_toggle");
2683
+ }
2684
+ KeyCode::Esc => {
2685
+ self.pop_overlay();
2686
+ self.request_render("shield_close");
2687
+ }
2688
+ _ => {}
2689
+ }
2690
+
2691
+ Ok(false)
2692
+ }
2693
+
2645
2694
  // ============================================================
2646
2695
  // SETTINGS INPUT HANDLING (Step 4.8)
2647
2696
  // ============================================================
@@ -189,6 +189,27 @@ fn main() -> Result<()> {
189
189
  }
190
190
  }
191
191
 
192
+ // Shield dashboard: live poll shield.load every ~5s when enabled
193
+ if matches!(current_screen, Screen::ShieldDashboard)
194
+ && app.state.operation_mode == OperationMode::Connected
195
+ {
196
+ let shield_poll = {
197
+ let sd = &app.state.shield_dashboard;
198
+ if !sd.auto_refresh_enabled || sd.loading {
199
+ false
200
+ } else {
201
+ sd.last_refresh
202
+ .map(|lr| lr.elapsed() >= sd.auto_refresh_interval)
203
+ .unwrap_or(true)
204
+ }
205
+ };
206
+ if shield_poll {
207
+ if let Some(ws) = &ws_client {
208
+ app.begin_shield_load_request(ws);
209
+ }
210
+ }
211
+ }
212
+
192
213
  // Run Manager: live poll run.list every ~3s when enabled (no manual R)
193
214
  if matches!(current_screen, Screen::RunManager)
194
215
  && app.state.operation_mode == OperationMode::Connected
@@ -669,6 +690,66 @@ fn main() -> Result<()> {
669
690
  }
670
691
  app.request_render("run_quick_ok");
671
692
  }
693
+ // Shield dashboard: load health + config + metrics + history
694
+ else if Some(&resp.id)
695
+ == app.state.pending_shield_load_id.as_ref()
696
+ {
697
+ app.state.pending_shield_load_id = None;
698
+ if obj.get("shieldLoad").and_then(|v| v.as_bool()) == Some(true)
699
+ {
700
+ use crate::ui::shield_dashboard::parse_shield_load;
701
+ let health_val = obj
702
+ .get("health")
703
+ .cloned()
704
+ .unwrap_or(serde_json::Value::Null);
705
+ let config_val = obj
706
+ .get("config")
707
+ .cloned()
708
+ .unwrap_or(serde_json::Value::Null);
709
+ let metrics_val = obj
710
+ .get("metrics")
711
+ .cloned()
712
+ .unwrap_or(serde_json::Value::Null);
713
+ let gateway_health_val = obj
714
+ .get("gatewayHealth")
715
+ .cloned()
716
+ .unwrap_or(serde_json::Value::Null);
717
+ let runs_val = obj
718
+ .get("recentRuns")
719
+ .cloned()
720
+ .unwrap_or(serde_json::Value::Null);
721
+ let warnings_vec: Vec<String> = obj
722
+ .get("warnings")
723
+ .and_then(|v| v.as_array())
724
+ .map(|a| {
725
+ a.iter()
726
+ .filter_map(|w| {
727
+ w.as_str().map(|s| s.to_string())
728
+ })
729
+ .collect()
730
+ })
731
+ .unwrap_or_default();
732
+ let (h, c, m, mut w, recent) = parse_shield_load(
733
+ &health_val,
734
+ &config_val,
735
+ &metrics_val,
736
+ &gateway_health_val,
737
+ &runs_val,
738
+ );
739
+ w.extend(warnings_vec);
740
+ app.state.shield_dashboard.apply_load(
741
+ &h, &c, &m, w, recent,
742
+ );
743
+ app.state.shield_mode = h.mode.clone();
744
+ app.state.shield_blocks_total = m.blocks;
745
+ app.state.shield_masks_total = m.masks;
746
+ app.add_log(format!(
747
+ "✓ [{}] Shield loaded — {} block(s), {} mask(s)",
748
+ short_id, m.blocks, m.masks
749
+ ));
750
+ }
751
+ app.request_render("shield_loaded");
752
+ }
672
753
  // Sentinel config: load templates + current + health
673
754
  else if Some(&resp.id)
674
755
  == app.state.pending_sentinel_load_id.as_ref()
@@ -1542,6 +1623,14 @@ fn main() -> Result<()> {
1542
1623
  "✗ [{}] run.quick failed: {}",
1543
1624
  short_id, error_msg
1544
1625
  ));
1626
+ } else if Some(&resp.id) == app.state.pending_shield_load_id.as_ref() {
1627
+ app.state.pending_shield_load_id = None;
1628
+ app.state.shield_dashboard.loading = false;
1629
+ app.state.shield_dashboard.error = Some(error_msg.clone());
1630
+ app.add_log(format!(
1631
+ "✗ [{}] shield.load failed: {}",
1632
+ short_id, error_msg
1633
+ ));
1545
1634
  } else if Some(&resp.id) == app.state.pending_sentinel_load_id.as_ref()
1546
1635
  {
1547
1636
  app.state.pending_sentinel_load_id = None;
@@ -18,6 +18,7 @@ pub enum Screen {
18
18
  AgentBuilder,
19
19
  RunManager,
20
20
  SentinelConfig,
21
+ ShieldDashboard,
21
22
  Settings,
22
23
  AgentList,
23
24
  ConnectionPortal,
@@ -40,7 +41,7 @@ impl Screen {
40
41
  pub fn is_overlay(&self) -> bool {
41
42
  matches!(
42
43
  self,
43
- Screen::AgentBuilder | Screen::RunManager | Screen::SentinelConfig | Screen::Settings | Screen::AgentList
44
+ Screen::AgentBuilder | Screen::RunManager | Screen::SentinelConfig | Screen::ShieldDashboard | Screen::Settings | Screen::AgentList
44
45
  )
45
46
  }
46
47
 
@@ -70,6 +71,7 @@ impl Screen {
70
71
  Screen::AgentBuilder => "Agent Builder",
71
72
  Screen::RunManager => "Run Manager",
72
73
  Screen::SentinelConfig => "Sentinel Config",
74
+ Screen::ShieldDashboard => "Shield",
73
75
  Screen::Settings => "Settings",
74
76
  Screen::AgentList => "Agent List",
75
77
  Screen::ConnectionPortal => "Connection Portal",
@@ -8,5 +8,6 @@ pub mod portal_monitoring;
8
8
  pub mod run_manager;
9
9
  pub mod safe_viewport;
10
10
  pub mod sentinel_config;
11
+ pub mod shield_dashboard;
11
12
  pub mod settings;
12
13
  pub mod setup_portal;