4runr-os 2.10.75 → 2.10.77
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/apps/gateway/package-lock.json +55 -55
- package/dist/tui-handlers.d.ts.map +1 -1
- package/dist/tui-handlers.js +3 -1
- 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 +97 -7
- package/mk3-tui/src/main.rs +85 -63
- package/mk3-tui/src/ui/shield_dashboard.rs +198 -24
- package/package.json +2 -2
package/mk3-tui/src/app.rs
CHANGED
|
@@ -838,16 +838,16 @@ impl App {
|
|
|
838
838
|
}
|
|
839
839
|
|
|
840
840
|
pub fn open_shield_dashboard(&mut self, ws: Option<&WebSocketClient>) {
|
|
841
|
-
self.state.pending_shield_load_id = None;
|
|
842
841
|
self.push_overlay(Screen::ShieldDashboard);
|
|
843
842
|
self.state
|
|
844
843
|
.logs
|
|
845
844
|
.push_back("[NAV] Opening Shield (production safety layer)...".into());
|
|
846
845
|
if self.state.operation_mode == OperationMode::Connected {
|
|
847
846
|
if let Some(ws) = ws {
|
|
848
|
-
self.begin_shield_load_request(ws);
|
|
847
|
+
self.begin_shield_load_request(ws, false);
|
|
849
848
|
}
|
|
850
849
|
} else {
|
|
850
|
+
self.state.shield_dashboard.loading = false;
|
|
851
851
|
self.state.shield_dashboard.error = Some(
|
|
852
852
|
"Connect to Gateway first (connect portal).".to_string(),
|
|
853
853
|
);
|
|
@@ -855,20 +855,39 @@ impl App {
|
|
|
855
855
|
self.request_immediate_render("open_shield_dashboard");
|
|
856
856
|
}
|
|
857
857
|
|
|
858
|
-
pub fn begin_shield_load_request(&mut self, ws: &WebSocketClient) {
|
|
858
|
+
pub fn begin_shield_load_request(&mut self, ws: &WebSocketClient, force: bool) {
|
|
859
859
|
if self.state.operation_mode != OperationMode::Connected {
|
|
860
860
|
return;
|
|
861
861
|
}
|
|
862
|
-
if self.state.pending_shield_load_id.is_some() {
|
|
862
|
+
if !force && self.state.pending_shield_load_id.is_some() {
|
|
863
863
|
return;
|
|
864
864
|
}
|
|
865
|
-
|
|
865
|
+
if force {
|
|
866
|
+
self.state.pending_shield_load_id = None;
|
|
867
|
+
}
|
|
868
|
+
self.state.shield_dashboard.mark_loading();
|
|
866
869
|
self.state.shield_dashboard.error = None;
|
|
867
870
|
if let Ok(id) = ws.send_command("shield.load", None) {
|
|
868
871
|
self.state.pending_shield_load_id = Some(id);
|
|
869
872
|
} else {
|
|
870
|
-
self.state.shield_dashboard.
|
|
873
|
+
self.state.shield_dashboard.fail_loading(
|
|
874
|
+
"Failed to send shield.load to CLI.".to_string(),
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
pub fn open_run_from_shield_block(&mut self, ws: Option<&WebSocketClient>, run_id: &str) {
|
|
880
|
+
self.push_overlay(Screen::RunManager);
|
|
881
|
+
self.state.run_manager.detail_run_id = Some(run_id.to_string());
|
|
882
|
+
self.add_log(format!(
|
|
883
|
+
"[SHIELD] Opening blocked run {} in Run Manager",
|
|
884
|
+
&run_id[run_id.len().saturating_sub(8)..]
|
|
885
|
+
));
|
|
886
|
+
if let Some(ws) = ws {
|
|
887
|
+
self.begin_run_list_request(ws, false);
|
|
888
|
+
self.begin_run_get_request(ws, run_id);
|
|
871
889
|
}
|
|
890
|
+
self.request_immediate_render("shield_open_run");
|
|
872
891
|
}
|
|
873
892
|
|
|
874
893
|
pub fn open_sentinel_config(&mut self, ws: Option<&WebSocketClient>) {
|
|
@@ -2652,6 +2671,7 @@ impl App {
|
|
|
2652
2671
|
key: KeyEvent,
|
|
2653
2672
|
ws_client: Option<&WebSocketClient>,
|
|
2654
2673
|
) -> anyhow::Result<bool> {
|
|
2674
|
+
use crate::ui::shield_dashboard::ShieldTab;
|
|
2655
2675
|
use crossterm::event::KeyModifiers;
|
|
2656
2676
|
|
|
2657
2677
|
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
|
@@ -2662,14 +2682,84 @@ impl App {
|
|
|
2662
2682
|
}
|
|
2663
2683
|
|
|
2664
2684
|
match key.code {
|
|
2685
|
+
KeyCode::Tab => {
|
|
2686
|
+
self.state.shield_dashboard.tab = self.state.shield_dashboard.tab.next();
|
|
2687
|
+
self.request_render("shield_tab");
|
|
2688
|
+
}
|
|
2689
|
+
KeyCode::BackTab => {
|
|
2690
|
+
self.state.shield_dashboard.tab = self.state.shield_dashboard.tab.prev();
|
|
2691
|
+
self.request_render("shield_tab_back");
|
|
2692
|
+
}
|
|
2693
|
+
KeyCode::Left => {
|
|
2694
|
+
self.state.shield_dashboard.tab = self.state.shield_dashboard.tab.prev();
|
|
2695
|
+
self.request_render("shield_tab_left");
|
|
2696
|
+
}
|
|
2697
|
+
KeyCode::Right => {
|
|
2698
|
+
self.state.shield_dashboard.tab = self.state.shield_dashboard.tab.next();
|
|
2699
|
+
self.request_render("shield_tab_right");
|
|
2700
|
+
}
|
|
2701
|
+
KeyCode::Up => {
|
|
2702
|
+
if self.state.shield_dashboard.tab == ShieldTab::Enforcement
|
|
2703
|
+
&& self.state.shield_dashboard.selected_block_index > 0
|
|
2704
|
+
{
|
|
2705
|
+
self.state.shield_dashboard.selected_block_index -= 1;
|
|
2706
|
+
self.request_render("shield_block_up");
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
KeyCode::Down => {
|
|
2710
|
+
if self.state.shield_dashboard.tab == ShieldTab::Enforcement {
|
|
2711
|
+
let max = self.state.shield_dashboard.recent_blocks.len();
|
|
2712
|
+
if max > 0 && self.state.shield_dashboard.selected_block_index + 1 < max {
|
|
2713
|
+
self.state.shield_dashboard.selected_block_index += 1;
|
|
2714
|
+
self.request_render("shield_block_down");
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
KeyCode::Enter => {
|
|
2719
|
+
if self.state.shield_dashboard.tab == ShieldTab::Enforcement {
|
|
2720
|
+
if let Some(row) = self
|
|
2721
|
+
.state
|
|
2722
|
+
.shield_dashboard
|
|
2723
|
+
.recent_blocks
|
|
2724
|
+
.get(self.state.shield_dashboard.selected_block_index)
|
|
2725
|
+
{
|
|
2726
|
+
let run_id = row.id.clone();
|
|
2727
|
+
self.open_run_from_shield_block(ws_client, &run_id);
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2665
2731
|
KeyCode::Char('r') | KeyCode::Char('R') => {
|
|
2666
2732
|
if self.state.operation_mode == OperationMode::Connected {
|
|
2667
2733
|
if let Some(ws) = ws_client {
|
|
2668
|
-
self.begin_shield_load_request(ws);
|
|
2734
|
+
self.begin_shield_load_request(ws, true);
|
|
2669
2735
|
}
|
|
2736
|
+
} else {
|
|
2737
|
+
self.state.shield_dashboard.fail_loading(
|
|
2738
|
+
"Connect to Gateway first (connect portal).".to_string(),
|
|
2739
|
+
);
|
|
2670
2740
|
}
|
|
2671
2741
|
self.request_render("shield_refresh");
|
|
2672
2742
|
}
|
|
2743
|
+
KeyCode::Char('o') | KeyCode::Char('O') => {
|
|
2744
|
+
self.push_overlay(Screen::RunManager);
|
|
2745
|
+
if let Some(ws) = ws_client {
|
|
2746
|
+
self.begin_run_list_request(ws, false);
|
|
2747
|
+
}
|
|
2748
|
+
self.add_log("[SHIELD] Opened Run Manager".to_string());
|
|
2749
|
+
self.request_render("shield_open_runs");
|
|
2750
|
+
}
|
|
2751
|
+
KeyCode::Char('p') | KeyCode::Char('P') => {
|
|
2752
|
+
if self.state.operation_mode == OperationMode::Connected {
|
|
2753
|
+
self.state.navigation.navigate_to_base(Screen::PortalMonitoring);
|
|
2754
|
+
if let Some(ws) = ws_client {
|
|
2755
|
+
self.begin_portal_observability_request(ws);
|
|
2756
|
+
}
|
|
2757
|
+
self.add_log("[SHIELD] Opened Portal Monitoring".to_string());
|
|
2758
|
+
} else {
|
|
2759
|
+
self.add_log("[SHIELD] Connect to Gateway first.".to_string());
|
|
2760
|
+
}
|
|
2761
|
+
self.request_render("shield_open_monitoring");
|
|
2762
|
+
}
|
|
2673
2763
|
KeyCode::Char('a') | KeyCode::Char('A') => {
|
|
2674
2764
|
self.state.shield_dashboard.auto_refresh_enabled =
|
|
2675
2765
|
!self.state.shield_dashboard.auto_refresh_enabled;
|
package/mk3-tui/src/main.rs
CHANGED
|
@@ -205,9 +205,26 @@ fn main() -> Result<()> {
|
|
|
205
205
|
};
|
|
206
206
|
if shield_poll {
|
|
207
207
|
if let Some(ws) = &ws_client {
|
|
208
|
-
app.begin_shield_load_request(ws);
|
|
208
|
+
app.begin_shield_load_request(ws, false);
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
|
+
|
|
212
|
+
// Loading timeout — never spin forever if CLI deduped or dropped a response
|
|
213
|
+
let loading_stuck = {
|
|
214
|
+
let sd = &app.state.shield_dashboard;
|
|
215
|
+
sd.loading
|
|
216
|
+
&& sd
|
|
217
|
+
.loading_started
|
|
218
|
+
.map(|t| t.elapsed() >= std::time::Duration::from_secs(12))
|
|
219
|
+
.unwrap_or(false)
|
|
220
|
+
};
|
|
221
|
+
if loading_stuck {
|
|
222
|
+
app.state.shield_dashboard.fail_loading(
|
|
223
|
+
"Shield load timed out — press R to refresh.".to_string(),
|
|
224
|
+
);
|
|
225
|
+
app.state.pending_shield_load_id = None;
|
|
226
|
+
app.request_render("shield_load_timeout");
|
|
227
|
+
}
|
|
211
228
|
}
|
|
212
229
|
|
|
213
230
|
// Run Manager: live poll run.list every ~3s when enabled (no manual R)
|
|
@@ -361,6 +378,70 @@ fn main() -> Result<()> {
|
|
|
361
378
|
.unwrap_or("?")
|
|
362
379
|
));
|
|
363
380
|
}
|
|
381
|
+
// Shield dashboard — match pending id before legacy shieldStatus branch
|
|
382
|
+
else if Some(&resp.id)
|
|
383
|
+
== app.state.pending_shield_load_id.as_ref()
|
|
384
|
+
{
|
|
385
|
+
app.state.pending_shield_load_id = None;
|
|
386
|
+
if obj.get("shieldLoad").and_then(|v| v.as_bool()) == Some(true)
|
|
387
|
+
{
|
|
388
|
+
use crate::ui::shield_dashboard::parse_shield_load;
|
|
389
|
+
let health_val = obj
|
|
390
|
+
.get("health")
|
|
391
|
+
.cloned()
|
|
392
|
+
.unwrap_or(serde_json::Value::Null);
|
|
393
|
+
let config_val = obj
|
|
394
|
+
.get("config")
|
|
395
|
+
.cloned()
|
|
396
|
+
.unwrap_or(serde_json::Value::Null);
|
|
397
|
+
let metrics_val = obj
|
|
398
|
+
.get("metrics")
|
|
399
|
+
.cloned()
|
|
400
|
+
.unwrap_or(serde_json::Value::Null);
|
|
401
|
+
let gateway_health_val = obj
|
|
402
|
+
.get("gatewayHealth")
|
|
403
|
+
.cloned()
|
|
404
|
+
.unwrap_or(serde_json::Value::Null);
|
|
405
|
+
let runs_val = obj
|
|
406
|
+
.get("recentRuns")
|
|
407
|
+
.cloned()
|
|
408
|
+
.unwrap_or(serde_json::Value::Null);
|
|
409
|
+
let warnings_vec: Vec<String> = obj
|
|
410
|
+
.get("warnings")
|
|
411
|
+
.and_then(|v| v.as_array())
|
|
412
|
+
.map(|a| {
|
|
413
|
+
a.iter()
|
|
414
|
+
.filter_map(|w| {
|
|
415
|
+
w.as_str().map(|s| s.to_string())
|
|
416
|
+
})
|
|
417
|
+
.collect()
|
|
418
|
+
})
|
|
419
|
+
.unwrap_or_default();
|
|
420
|
+
let (h, c, m, mut w, recent) = parse_shield_load(
|
|
421
|
+
&health_val,
|
|
422
|
+
&config_val,
|
|
423
|
+
&metrics_val,
|
|
424
|
+
&gateway_health_val,
|
|
425
|
+
&runs_val,
|
|
426
|
+
);
|
|
427
|
+
w.extend(warnings_vec);
|
|
428
|
+
app.state.shield_dashboard.apply_load(
|
|
429
|
+
&h, &c, &m, w, recent,
|
|
430
|
+
);
|
|
431
|
+
app.state.shield_mode = h.mode.clone();
|
|
432
|
+
app.state.shield_blocks_total = m.blocks;
|
|
433
|
+
app.state.shield_masks_total = m.masks;
|
|
434
|
+
app.add_log(format!(
|
|
435
|
+
"✓ [{}] Shield loaded — {} block(s), {} mask(s)",
|
|
436
|
+
short_id, m.blocks, m.masks
|
|
437
|
+
));
|
|
438
|
+
} else {
|
|
439
|
+
app.state.shield_dashboard.fail_loading(
|
|
440
|
+
"Shield load: unexpected response shape".to_string(),
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
app.request_render("shield_loaded");
|
|
444
|
+
}
|
|
364
445
|
else if obj.get("shieldDemo").and_then(|v| v.as_bool())
|
|
365
446
|
== Some(true)
|
|
366
447
|
{
|
|
@@ -690,66 +771,6 @@ fn main() -> Result<()> {
|
|
|
690
771
|
}
|
|
691
772
|
app.request_render("run_quick_ok");
|
|
692
773
|
}
|
|
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
|
-
}
|
|
753
774
|
// Sentinel config: load templates + current + health
|
|
754
775
|
else if Some(&resp.id)
|
|
755
776
|
== app.state.pending_sentinel_load_id.as_ref()
|
|
@@ -1625,8 +1646,9 @@ fn main() -> Result<()> {
|
|
|
1625
1646
|
));
|
|
1626
1647
|
} else if Some(&resp.id) == app.state.pending_shield_load_id.as_ref() {
|
|
1627
1648
|
app.state.pending_shield_load_id = None;
|
|
1628
|
-
app.state
|
|
1629
|
-
|
|
1649
|
+
app.state
|
|
1650
|
+
.shield_dashboard
|
|
1651
|
+
.fail_loading(error_msg.clone());
|
|
1630
1652
|
app.add_log(format!(
|
|
1631
1653
|
"✗ [{}] shield.load failed: {}",
|
|
1632
1654
|
short_id, error_msg
|
|
@@ -34,8 +34,44 @@ pub struct ShieldBlockRow {
|
|
|
34
34
|
pub created_at: String,
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
38
|
+
pub enum ShieldTab {
|
|
39
|
+
Overview,
|
|
40
|
+
Enforcement,
|
|
41
|
+
Help,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
impl ShieldTab {
|
|
45
|
+
pub fn label(self) -> &'static str {
|
|
46
|
+
match self {
|
|
47
|
+
ShieldTab::Overview => "Overview",
|
|
48
|
+
ShieldTab::Enforcement => "Enforcement",
|
|
49
|
+
ShieldTab::Help => "Help",
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
pub fn next(self) -> Self {
|
|
54
|
+
match self {
|
|
55
|
+
ShieldTab::Overview => ShieldTab::Enforcement,
|
|
56
|
+
ShieldTab::Enforcement => ShieldTab::Help,
|
|
57
|
+
ShieldTab::Help => ShieldTab::Overview,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
pub fn prev(self) -> Self {
|
|
62
|
+
match self {
|
|
63
|
+
ShieldTab::Overview => ShieldTab::Help,
|
|
64
|
+
ShieldTab::Enforcement => ShieldTab::Overview,
|
|
65
|
+
ShieldTab::Help => ShieldTab::Enforcement,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
37
70
|
#[derive(Debug, Clone)]
|
|
38
71
|
pub struct ShieldDashboardState {
|
|
72
|
+
pub tab: ShieldTab,
|
|
73
|
+
pub selected_block_index: usize,
|
|
74
|
+
pub loaded_once: bool,
|
|
39
75
|
pub enabled: bool,
|
|
40
76
|
pub mode: String,
|
|
41
77
|
pub health_status: String,
|
|
@@ -51,6 +87,7 @@ pub struct ShieldDashboardState {
|
|
|
51
87
|
pub warnings: Vec<String>,
|
|
52
88
|
pub recent_blocks: Vec<ShieldBlockRow>,
|
|
53
89
|
pub loading: bool,
|
|
90
|
+
pub loading_started: Option<std::time::Instant>,
|
|
54
91
|
pub error: Option<String>,
|
|
55
92
|
pub last_refresh: Option<std::time::Instant>,
|
|
56
93
|
pub auto_refresh_enabled: bool,
|
|
@@ -60,6 +97,9 @@ pub struct ShieldDashboardState {
|
|
|
60
97
|
impl Default for ShieldDashboardState {
|
|
61
98
|
fn default() -> Self {
|
|
62
99
|
Self {
|
|
100
|
+
tab: ShieldTab::Overview,
|
|
101
|
+
selected_block_index: 0,
|
|
102
|
+
loaded_once: false,
|
|
63
103
|
enabled: false,
|
|
64
104
|
mode: "off".to_string(),
|
|
65
105
|
health_status: String::new(),
|
|
@@ -75,6 +115,7 @@ impl Default for ShieldDashboardState {
|
|
|
75
115
|
warnings: Vec::new(),
|
|
76
116
|
recent_blocks: Vec::new(),
|
|
77
117
|
loading: false,
|
|
118
|
+
loading_started: None,
|
|
78
119
|
error: None,
|
|
79
120
|
last_refresh: None,
|
|
80
121
|
auto_refresh_enabled: true,
|
|
@@ -94,7 +135,9 @@ impl ShieldDashboardState {
|
|
|
94
135
|
recent: Vec<ShieldBlockRow>,
|
|
95
136
|
) {
|
|
96
137
|
self.loading = false;
|
|
138
|
+
self.loading_started = None;
|
|
97
139
|
self.error = None;
|
|
140
|
+
self.loaded_once = true;
|
|
98
141
|
self.enabled = health.enabled;
|
|
99
142
|
self.mode = health.mode.clone();
|
|
100
143
|
self.health_status = health.status.clone();
|
|
@@ -109,8 +152,24 @@ impl ShieldDashboardState {
|
|
|
109
152
|
self.rewrites_total = metrics.rewrites;
|
|
110
153
|
self.warnings = warnings;
|
|
111
154
|
self.recent_blocks = recent;
|
|
155
|
+
if self.selected_block_index >= self.recent_blocks.len() {
|
|
156
|
+
self.selected_block_index = 0;
|
|
157
|
+
}
|
|
112
158
|
self.last_refresh = Some(std::time::Instant::now());
|
|
113
159
|
}
|
|
160
|
+
|
|
161
|
+
pub fn mark_loading(&mut self) {
|
|
162
|
+
self.loading = true;
|
|
163
|
+
if self.loading_started.is_none() {
|
|
164
|
+
self.loading_started = Some(std::time::Instant::now());
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
pub fn fail_loading(&mut self, message: String) {
|
|
169
|
+
self.loading = false;
|
|
170
|
+
self.loading_started = None;
|
|
171
|
+
self.error = Some(message);
|
|
172
|
+
}
|
|
114
173
|
}
|
|
115
174
|
|
|
116
175
|
#[derive(Debug, Clone, Default)]
|
|
@@ -315,21 +374,31 @@ pub fn render(f: &mut Frame, state: &AppState) {
|
|
|
315
374
|
|
|
316
375
|
use ratatui::layout::{Constraint, Direction, Layout};
|
|
317
376
|
|
|
377
|
+
let refresh_hint = if sd.loading {
|
|
378
|
+
" ↻"
|
|
379
|
+
} else {
|
|
380
|
+
""
|
|
381
|
+
};
|
|
318
382
|
let live = if sd.auto_refresh_enabled { " · LIVE" } else { "" };
|
|
319
383
|
let header_title = format!(
|
|
320
|
-
" 🛡️ Shield — {} · {} block(s) · {} mask(s){} ",
|
|
321
|
-
sd.
|
|
384
|
+
" 🛡️ Shield — {} · {} block(s) · {} mask(s){}{} ",
|
|
385
|
+
if sd.loaded_once {
|
|
386
|
+
sd.mode.to_uppercase()
|
|
387
|
+
} else {
|
|
388
|
+
"…".to_string()
|
|
389
|
+
},
|
|
322
390
|
sd.blocks_total,
|
|
323
391
|
sd.masks_total,
|
|
324
|
-
live
|
|
392
|
+
live,
|
|
393
|
+
refresh_hint
|
|
325
394
|
);
|
|
326
395
|
|
|
327
396
|
let chunks = Layout::default()
|
|
328
397
|
.direction(Direction::Vertical)
|
|
329
398
|
.constraints([
|
|
330
399
|
Constraint::Length(3),
|
|
331
|
-
Constraint::
|
|
332
|
-
Constraint::
|
|
400
|
+
Constraint::Length(3),
|
|
401
|
+
Constraint::Min(8),
|
|
333
402
|
Constraint::Length(3),
|
|
334
403
|
])
|
|
335
404
|
.split(area);
|
|
@@ -341,22 +410,53 @@ pub fn render(f: &mut Frame, state: &AppState) {
|
|
|
341
410
|
.style(Style::default().bg(BG_PANEL));
|
|
342
411
|
f.render_widget(header, chunks[0]);
|
|
343
412
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
413
|
+
render_tab_bar(f, chunks[1], sd);
|
|
414
|
+
|
|
415
|
+
if !sd.loaded_once && sd.loading {
|
|
416
|
+
let block = Block::default()
|
|
417
|
+
.title(" Loading ")
|
|
418
|
+
.borders(Borders::ALL)
|
|
419
|
+
.border_style(Style::default().fg(CYBER_CYAN));
|
|
420
|
+
let inner = block.inner(chunks[2]);
|
|
421
|
+
f.render_widget(block, chunks[2]);
|
|
422
|
+
f.render_widget(
|
|
423
|
+
Paragraph::new(vec![
|
|
424
|
+
Line::from(""),
|
|
425
|
+
Line::from("Fetching Shield health, config, metrics, and run history…")
|
|
426
|
+
.style(Style::default().fg(TEXT_PRIMARY)),
|
|
427
|
+
Line::from(""),
|
|
428
|
+
Line::from("Press R to retry if this takes more than a few seconds.")
|
|
429
|
+
.style(Style::default().fg(TEXT_DIM)),
|
|
430
|
+
])
|
|
431
|
+
.alignment(Alignment::Center),
|
|
432
|
+
inner,
|
|
433
|
+
);
|
|
434
|
+
} else {
|
|
435
|
+
match sd.tab {
|
|
436
|
+
ShieldTab::Overview => {
|
|
437
|
+
let mid = Layout::default()
|
|
438
|
+
.direction(Direction::Horizontal)
|
|
439
|
+
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
|
440
|
+
.split(chunks[2]);
|
|
441
|
+
render_status_panel(f, mid[0], sd);
|
|
442
|
+
render_metrics_panel(f, mid[1], sd);
|
|
443
|
+
}
|
|
444
|
+
ShieldTab::Enforcement => render_enforcement_panel(f, chunks[2], sd),
|
|
445
|
+
ShieldTab::Help => render_help_panel(f, chunks[2], sd),
|
|
446
|
+
}
|
|
447
|
+
}
|
|
352
448
|
|
|
353
|
-
let
|
|
354
|
-
"
|
|
355
|
-
} else if
|
|
356
|
-
|
|
449
|
+
let status_line = if let Some(e) = &sd.error {
|
|
450
|
+
format!("⚠ {} | ", e)
|
|
451
|
+
} else if sd.loading {
|
|
452
|
+
"Refreshing… | ".to_string()
|
|
357
453
|
} else {
|
|
358
|
-
|
|
454
|
+
String::new()
|
|
359
455
|
};
|
|
456
|
+
let footer_msg = format!(
|
|
457
|
+
"{}Tab/←→ Switch view · ↑↓ Select block · Enter Open run · R Refresh · O Runs · P Monitoring · A Live · ESC Close",
|
|
458
|
+
status_line
|
|
459
|
+
);
|
|
360
460
|
let footer_color = if sd.error.is_some() {
|
|
361
461
|
Color::Rgb(255, 69, 69)
|
|
362
462
|
} else {
|
|
@@ -376,6 +476,68 @@ pub fn render(f: &mut Frame, state: &AppState) {
|
|
|
376
476
|
);
|
|
377
477
|
}
|
|
378
478
|
|
|
479
|
+
fn render_tab_bar(f: &mut Frame, area: Rect, sd: &ShieldDashboardState) {
|
|
480
|
+
let tabs = [ShieldTab::Overview, ShieldTab::Enforcement, ShieldTab::Help];
|
|
481
|
+
let spans: Vec<Span> = tabs
|
|
482
|
+
.iter()
|
|
483
|
+
.flat_map(|tab| {
|
|
484
|
+
let active = *tab == sd.tab;
|
|
485
|
+
[
|
|
486
|
+
Span::styled(
|
|
487
|
+
format!(" {} ", tab.label()),
|
|
488
|
+
Style::default()
|
|
489
|
+
.fg(if active { SHIELD_ORANGE } else { TEXT_DIM })
|
|
490
|
+
.add_modifier(if active {
|
|
491
|
+
Modifier::BOLD
|
|
492
|
+
} else {
|
|
493
|
+
Modifier::empty()
|
|
494
|
+
}),
|
|
495
|
+
),
|
|
496
|
+
Span::raw("│"),
|
|
497
|
+
]
|
|
498
|
+
})
|
|
499
|
+
.collect();
|
|
500
|
+
let block = Block::default()
|
|
501
|
+
.borders(Borders::LEFT | Borders::RIGHT)
|
|
502
|
+
.border_style(Style::default().fg(TEXT_MUTED));
|
|
503
|
+
f.render_widget(
|
|
504
|
+
Paragraph::new(Line::from(spans)).alignment(Alignment::Center),
|
|
505
|
+
block.inner(area),
|
|
506
|
+
);
|
|
507
|
+
f.render_widget(block, area);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
fn render_help_panel(f: &mut Frame, area: Rect, sd: &ShieldDashboardState) {
|
|
511
|
+
let block = Block::default()
|
|
512
|
+
.title(" How Shield works in production ")
|
|
513
|
+
.borders(Borders::ALL)
|
|
514
|
+
.border_style(Style::default().fg(BRAND_PURPLE))
|
|
515
|
+
.style(Style::default().bg(BG_PANEL));
|
|
516
|
+
let inner = block.inner(area);
|
|
517
|
+
f.render_widget(block, area);
|
|
518
|
+
|
|
519
|
+
let mut lines: Vec<Line> = ABOUT_SHIELD
|
|
520
|
+
.iter()
|
|
521
|
+
.map(|l| Line::from(*l).style(Style::default().fg(TEXT_PRIMARY)))
|
|
522
|
+
.collect();
|
|
523
|
+
lines.push(Line::from(""));
|
|
524
|
+
lines.push(
|
|
525
|
+
Line::from("Navigation from this screen:")
|
|
526
|
+
.style(Style::default().fg(NEON_GREEN).bold()),
|
|
527
|
+
);
|
|
528
|
+
lines.push(Line::from(" O — open Run Manager (all runs, SHIELD badge on blocks)"));
|
|
529
|
+
lines.push(Line::from(" P — open Portal Monitoring (Prometheus Shield counters)"));
|
|
530
|
+
lines.push(Line::from(" Enforcement tab → Enter on a row opens that blocked run"));
|
|
531
|
+
if !sd.warnings.is_empty() {
|
|
532
|
+
lines.push(Line::from(""));
|
|
533
|
+
lines.push(Line::from("Active warnings:").style(Style::default().fg(AMBER_WARN).bold()));
|
|
534
|
+
for w in &sd.warnings {
|
|
535
|
+
lines.push(Line::from(format!(" ⚠ {}", w)).style(Style::default().fg(AMBER_WARN)));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
f.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner);
|
|
539
|
+
}
|
|
540
|
+
|
|
379
541
|
fn render_status_panel(f: &mut Frame, area: Rect, sd: &ShieldDashboardState) {
|
|
380
542
|
let block = Block::default()
|
|
381
543
|
.title(" Status & detectors ")
|
|
@@ -495,7 +657,7 @@ fn metric_line(label: &str, value: u64, color: Color) -> Line<'static> {
|
|
|
495
657
|
fn render_enforcement_panel(f: &mut Frame, area: Rect, sd: &ShieldDashboardState) {
|
|
496
658
|
let block = Block::default()
|
|
497
659
|
.title(format!(
|
|
498
|
-
"
|
|
660
|
+
" Blocked runs ({}) — ↑↓ select · Enter open in Run Manager ",
|
|
499
661
|
sd.recent_blocks.len()
|
|
500
662
|
))
|
|
501
663
|
.borders(Borders::ALL)
|
|
@@ -508,23 +670,31 @@ fn render_enforcement_panel(f: &mut Frame, area: Rect, sd: &ShieldDashboardState
|
|
|
508
670
|
let empty = vec![
|
|
509
671
|
Line::from(""),
|
|
510
672
|
Line::from("No blocked runs in Gateway history yet.").style(Style::default().fg(TEXT_DIM)),
|
|
511
|
-
Line::from("
|
|
512
|
-
Line::from("
|
|
673
|
+
Line::from(""),
|
|
674
|
+
Line::from("Shield still evaluates every agent/API run on input.").style(Style::default().fg(TEXT_MUTED)),
|
|
675
|
+
Line::from("Press O to open Run Manager · P for live Prometheus metrics.").style(Style::default().fg(NEON_GREEN)),
|
|
513
676
|
];
|
|
514
|
-
f.render_widget(Paragraph::new(empty), inner);
|
|
677
|
+
f.render_widget(Paragraph::new(empty).alignment(Alignment::Center), inner);
|
|
515
678
|
return;
|
|
516
679
|
}
|
|
517
680
|
|
|
518
681
|
let items: Vec<ListItem> = sd
|
|
519
682
|
.recent_blocks
|
|
520
683
|
.iter()
|
|
521
|
-
.
|
|
684
|
+
.enumerate()
|
|
685
|
+
.map(|(idx, r)| {
|
|
522
686
|
let short_id = if r.id.len() > 8 {
|
|
523
687
|
&r.id[r.id.len() - 8..]
|
|
524
688
|
} else {
|
|
525
689
|
&r.id
|
|
526
690
|
};
|
|
691
|
+
let prefix = if idx == sd.selected_block_index {
|
|
692
|
+
"▶ "
|
|
693
|
+
} else {
|
|
694
|
+
" "
|
|
695
|
+
};
|
|
527
696
|
ListItem::new(Line::from(vec![
|
|
697
|
+
Span::styled(prefix, Style::default().fg(SHIELD_ORANGE).bold()),
|
|
528
698
|
Span::styled(
|
|
529
699
|
format!("{} ", r.created_at),
|
|
530
700
|
Style::default().fg(TEXT_MUTED),
|
|
@@ -535,7 +705,11 @@ fn render_enforcement_panel(f: &mut Frame, area: Rect, sd: &ShieldDashboardState
|
|
|
535
705
|
),
|
|
536
706
|
Span::styled(
|
|
537
707
|
format!("{} — ", r.name),
|
|
538
|
-
Style::default().fg(
|
|
708
|
+
Style::default().fg(if idx == sd.selected_block_index {
|
|
709
|
+
CYBER_CYAN
|
|
710
|
+
} else {
|
|
711
|
+
TEXT_PRIMARY
|
|
712
|
+
}),
|
|
539
713
|
),
|
|
540
714
|
Span::styled(
|
|
541
715
|
r.reason.chars().take(48).collect::<String>(),
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "4runr-os",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.77",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.10.
|
|
5
|
+
"description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.10.77: Fix Shield dashboard load — shield.load response no longer swallowed by legacy handler.",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"4runr": "dist/index.js",
|