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.
- package/dist/tui-handlers.js +33 -11
- package/dist/tui-handlers.js.map +1 -1
- package/mk3-tui/binaries/win32-x64/mk3-tui.exe +0 -0
- package/mk3-tui/src/app.rs +98 -49
- package/mk3-tui/src/main.rs +89 -0
- package/mk3-tui/src/screens/mod.rs +3 -1
- package/mk3-tui/src/ui/mod.rs +1 -0
- package/mk3-tui/src/ui/shield_dashboard.rs +549 -0
- package/package.json +2 -2
package/mk3-tui/src/app.rs
CHANGED
|
@@ -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
|
-
|
|
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
|
// ============================================================
|
package/mk3-tui/src/main.rs
CHANGED
|
@@ -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",
|