4runr-os 2.10.77 → 2.10.78
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 +21 -21
- package/dist/gateway-observability.d.ts +17 -0
- package/dist/gateway-observability.d.ts.map +1 -1
- package/dist/gateway-observability.js +54 -0
- package/dist/gateway-observability.js.map +1 -1
- package/dist/tui-handlers.d.ts.map +1 -1
- package/dist/tui-handlers.js +116 -14
- 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 +24 -3
- package/mk3-tui/src/main.rs +28 -2
- package/mk3-tui/src/ui/shield_dashboard.rs +388 -44
- package/package.json +2 -2
|
@@ -36,42 +36,62 @@ pub struct ShieldBlockRow {
|
|
|
36
36
|
|
|
37
37
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
38
38
|
pub enum ShieldTab {
|
|
39
|
+
Activity,
|
|
39
40
|
Overview,
|
|
40
41
|
Enforcement,
|
|
41
|
-
Help,
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
impl ShieldTab {
|
|
45
45
|
pub fn label(self) -> &'static str {
|
|
46
46
|
match self {
|
|
47
|
+
ShieldTab::Activity => "Activity",
|
|
47
48
|
ShieldTab::Overview => "Overview",
|
|
48
49
|
ShieldTab::Enforcement => "Enforcement",
|
|
49
|
-
ShieldTab::Help => "Help",
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
pub fn next(self) -> Self {
|
|
54
54
|
match self {
|
|
55
|
+
ShieldTab::Activity => ShieldTab::Overview,
|
|
55
56
|
ShieldTab::Overview => ShieldTab::Enforcement,
|
|
56
|
-
ShieldTab::Enforcement => ShieldTab::
|
|
57
|
-
ShieldTab::Help => ShieldTab::Overview,
|
|
57
|
+
ShieldTab::Enforcement => ShieldTab::Activity,
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
pub fn prev(self) -> Self {
|
|
62
62
|
match self {
|
|
63
|
-
ShieldTab::
|
|
63
|
+
ShieldTab::Activity => ShieldTab::Enforcement,
|
|
64
|
+
ShieldTab::Overview => ShieldTab::Activity,
|
|
64
65
|
ShieldTab::Enforcement => ShieldTab::Overview,
|
|
65
|
-
ShieldTab::Help => ShieldTab::Enforcement,
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
#[derive(Debug, Clone)]
|
|
71
|
+
pub struct ShieldActivityRow {
|
|
72
|
+
pub at: String,
|
|
73
|
+
pub kind: String,
|
|
74
|
+
pub run_id: Option<String>,
|
|
75
|
+
pub run_name: Option<String>,
|
|
76
|
+
pub detail: String,
|
|
77
|
+
pub source: String,
|
|
78
|
+
}
|
|
79
|
+
|
|
70
80
|
#[derive(Debug, Clone)]
|
|
71
81
|
pub struct ShieldDashboardState {
|
|
72
82
|
pub tab: ShieldTab,
|
|
73
83
|
pub selected_block_index: usize,
|
|
84
|
+
pub selected_activity_index: usize,
|
|
74
85
|
pub loaded_once: bool,
|
|
86
|
+
pub snapshot_at: String,
|
|
87
|
+
pub blocks_delta: i64,
|
|
88
|
+
pub masks_delta: i64,
|
|
89
|
+
pub decisions_delta: i64,
|
|
90
|
+
pub breakdown_lines: Vec<String>,
|
|
91
|
+
pub activity_events: Vec<ShieldActivityRow>,
|
|
92
|
+
prev_blocks: u64,
|
|
93
|
+
prev_masks: u64,
|
|
94
|
+
prev_decisions: u64,
|
|
75
95
|
pub enabled: bool,
|
|
76
96
|
pub mode: String,
|
|
77
97
|
pub health_status: String,
|
|
@@ -97,9 +117,19 @@ pub struct ShieldDashboardState {
|
|
|
97
117
|
impl Default for ShieldDashboardState {
|
|
98
118
|
fn default() -> Self {
|
|
99
119
|
Self {
|
|
100
|
-
tab: ShieldTab::
|
|
120
|
+
tab: ShieldTab::Activity,
|
|
101
121
|
selected_block_index: 0,
|
|
122
|
+
selected_activity_index: 0,
|
|
102
123
|
loaded_once: false,
|
|
124
|
+
snapshot_at: String::new(),
|
|
125
|
+
blocks_delta: 0,
|
|
126
|
+
masks_delta: 0,
|
|
127
|
+
decisions_delta: 0,
|
|
128
|
+
breakdown_lines: Vec::new(),
|
|
129
|
+
activity_events: Vec::new(),
|
|
130
|
+
prev_blocks: 0,
|
|
131
|
+
prev_masks: 0,
|
|
132
|
+
prev_decisions: 0,
|
|
103
133
|
enabled: false,
|
|
104
134
|
mode: "off".to_string(),
|
|
105
135
|
health_status: String::new(),
|
|
@@ -133,11 +163,33 @@ impl ShieldDashboardState {
|
|
|
133
163
|
metrics: &ShieldMetricsSnap,
|
|
134
164
|
warnings: Vec<String>,
|
|
135
165
|
recent: Vec<ShieldBlockRow>,
|
|
166
|
+
activity: Vec<ShieldActivityRow>,
|
|
167
|
+
breakdown_lines: Vec<String>,
|
|
168
|
+
snapshot_at: String,
|
|
136
169
|
) {
|
|
170
|
+
if self.loaded_once {
|
|
171
|
+
self.blocks_delta = metrics.blocks as i64 - self.prev_blocks as i64;
|
|
172
|
+
self.masks_delta = metrics.masks as i64 - self.prev_masks as i64;
|
|
173
|
+
self.decisions_delta = metrics.decisions as i64 - self.prev_decisions as i64;
|
|
174
|
+
} else {
|
|
175
|
+
self.blocks_delta = 0;
|
|
176
|
+
self.masks_delta = 0;
|
|
177
|
+
self.decisions_delta = 0;
|
|
178
|
+
}
|
|
179
|
+
self.prev_blocks = metrics.blocks;
|
|
180
|
+
self.prev_masks = metrics.masks;
|
|
181
|
+
self.prev_decisions = metrics.decisions;
|
|
182
|
+
|
|
137
183
|
self.loading = false;
|
|
138
184
|
self.loading_started = None;
|
|
139
185
|
self.error = None;
|
|
140
186
|
self.loaded_once = true;
|
|
187
|
+
self.snapshot_at = snapshot_at;
|
|
188
|
+
self.activity_events = activity;
|
|
189
|
+
self.breakdown_lines = breakdown_lines;
|
|
190
|
+
if self.selected_activity_index >= self.activity_events.len() {
|
|
191
|
+
self.selected_activity_index = 0;
|
|
192
|
+
}
|
|
141
193
|
self.enabled = health.enabled;
|
|
142
194
|
self.mode = health.mode.clone();
|
|
143
195
|
self.health_status = health.status.clone();
|
|
@@ -368,6 +420,91 @@ pub fn parse_shield_load(
|
|
|
368
420
|
(health, config, metrics, warnings, recent)
|
|
369
421
|
}
|
|
370
422
|
|
|
423
|
+
pub fn parse_shield_activity(val: &Value) -> Vec<ShieldActivityRow> {
|
|
424
|
+
let Some(arr) = val.as_array() else {
|
|
425
|
+
return Vec::new();
|
|
426
|
+
};
|
|
427
|
+
arr.iter()
|
|
428
|
+
.filter_map(|item| {
|
|
429
|
+
let o = item.as_object()?;
|
|
430
|
+
Some(ShieldActivityRow {
|
|
431
|
+
at: o
|
|
432
|
+
.get("at")
|
|
433
|
+
.and_then(|v| v.as_str())
|
|
434
|
+
.map(|s| s.chars().take(19).collect())
|
|
435
|
+
.unwrap_or_else(|| "—".to_string()),
|
|
436
|
+
kind: o
|
|
437
|
+
.get("kind")
|
|
438
|
+
.and_then(|v| v.as_str())
|
|
439
|
+
.unwrap_or("log")
|
|
440
|
+
.to_string(),
|
|
441
|
+
run_id: o
|
|
442
|
+
.get("runId")
|
|
443
|
+
.and_then(|v| v.as_str())
|
|
444
|
+
.map(|s| s.to_string()),
|
|
445
|
+
run_name: o
|
|
446
|
+
.get("runName")
|
|
447
|
+
.and_then(|v| v.as_str())
|
|
448
|
+
.map(|s| s.to_string()),
|
|
449
|
+
detail: o
|
|
450
|
+
.get("detail")
|
|
451
|
+
.and_then(|v| v.as_str())
|
|
452
|
+
.unwrap_or("")
|
|
453
|
+
.to_string(),
|
|
454
|
+
source: o
|
|
455
|
+
.get("source")
|
|
456
|
+
.and_then(|v| v.as_str())
|
|
457
|
+
.unwrap_or("?")
|
|
458
|
+
.to_string(),
|
|
459
|
+
})
|
|
460
|
+
})
|
|
461
|
+
.collect()
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
pub fn parse_shield_breakdown(val: &Value) -> Vec<String> {
|
|
465
|
+
val.get("breakdownLines")
|
|
466
|
+
.and_then(|v| v.as_array())
|
|
467
|
+
.map(|arr| {
|
|
468
|
+
arr.iter()
|
|
469
|
+
.filter_map(|x| x.as_str().map(|s| s.to_string()))
|
|
470
|
+
.collect()
|
|
471
|
+
})
|
|
472
|
+
.unwrap_or_default()
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
fn format_delta(delta: i64) -> String {
|
|
476
|
+
if delta > 0 {
|
|
477
|
+
format!(" +{}", delta)
|
|
478
|
+
} else if delta < 0 {
|
|
479
|
+
format!(" {}", delta)
|
|
480
|
+
} else {
|
|
481
|
+
String::new()
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
fn refresh_ago(sd: &ShieldDashboardState) -> String {
|
|
486
|
+
sd.last_refresh
|
|
487
|
+
.map(|lr| {
|
|
488
|
+
let s = lr.elapsed().as_secs();
|
|
489
|
+
if s < 60 {
|
|
490
|
+
format!("{}s ago", s)
|
|
491
|
+
} else {
|
|
492
|
+
format!("{}m ago", s / 60)
|
|
493
|
+
}
|
|
494
|
+
})
|
|
495
|
+
.unwrap_or_else(|| "—".to_string())
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
fn kind_color(kind: &str) -> Color {
|
|
499
|
+
match kind {
|
|
500
|
+
"block" => SHIELD_ORANGE,
|
|
501
|
+
"mask" => CYBER_CYAN,
|
|
502
|
+
"flag" => AMBER_WARN,
|
|
503
|
+
"rewrite" => AMBER_WARN,
|
|
504
|
+
_ => TEXT_DIM,
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
371
508
|
pub fn render(f: &mut Frame, state: &AppState) {
|
|
372
509
|
let area = f.size();
|
|
373
510
|
let sd = &state.shield_dashboard;
|
|
@@ -380,16 +517,34 @@ pub fn render(f: &mut Frame, state: &AppState) {
|
|
|
380
517
|
""
|
|
381
518
|
};
|
|
382
519
|
let live = if sd.auto_refresh_enabled { " · LIVE" } else { "" };
|
|
520
|
+
let delta_blk = if sd.loaded_once {
|
|
521
|
+
format_delta(sd.blocks_delta)
|
|
522
|
+
} else {
|
|
523
|
+
String::new()
|
|
524
|
+
};
|
|
525
|
+
let delta_mask = if sd.loaded_once {
|
|
526
|
+
format_delta(sd.masks_delta)
|
|
527
|
+
} else {
|
|
528
|
+
String::new()
|
|
529
|
+
};
|
|
530
|
+
let ago = if sd.loaded_once {
|
|
531
|
+
format!(" · {}", refresh_ago(sd))
|
|
532
|
+
} else {
|
|
533
|
+
String::new()
|
|
534
|
+
};
|
|
383
535
|
let header_title = format!(
|
|
384
|
-
" 🛡️ Shield — {} · {}
|
|
536
|
+
" 🛡️ Shield — {} · {} blk{}{} mask{}{}{}{} ",
|
|
385
537
|
if sd.loaded_once {
|
|
386
538
|
sd.mode.to_uppercase()
|
|
387
539
|
} else {
|
|
388
540
|
"…".to_string()
|
|
389
541
|
},
|
|
390
542
|
sd.blocks_total,
|
|
543
|
+
delta_blk,
|
|
391
544
|
sd.masks_total,
|
|
545
|
+
delta_mask,
|
|
392
546
|
live,
|
|
547
|
+
ago,
|
|
393
548
|
refresh_hint
|
|
394
549
|
);
|
|
395
550
|
|
|
@@ -433,6 +588,7 @@ pub fn render(f: &mut Frame, state: &AppState) {
|
|
|
433
588
|
);
|
|
434
589
|
} else {
|
|
435
590
|
match sd.tab {
|
|
591
|
+
ShieldTab::Activity => render_activity_panel(f, chunks[2], sd),
|
|
436
592
|
ShieldTab::Overview => {
|
|
437
593
|
let mid = Layout::default()
|
|
438
594
|
.direction(Direction::Horizontal)
|
|
@@ -442,7 +598,6 @@ pub fn render(f: &mut Frame, state: &AppState) {
|
|
|
442
598
|
render_metrics_panel(f, mid[1], sd);
|
|
443
599
|
}
|
|
444
600
|
ShieldTab::Enforcement => render_enforcement_panel(f, chunks[2], sd),
|
|
445
|
-
ShieldTab::Help => render_help_panel(f, chunks[2], sd),
|
|
446
601
|
}
|
|
447
602
|
}
|
|
448
603
|
|
|
@@ -453,9 +608,14 @@ pub fn render(f: &mut Frame, state: &AppState) {
|
|
|
453
608
|
} else {
|
|
454
609
|
String::new()
|
|
455
610
|
};
|
|
611
|
+
let nav_hint = match sd.tab {
|
|
612
|
+
ShieldTab::Activity => "↑↓ Select event · Enter Open run",
|
|
613
|
+
ShieldTab::Enforcement => "↑↓ Select block · Enter Open run",
|
|
614
|
+
ShieldTab::Overview => "Overview — counters update on refresh",
|
|
615
|
+
};
|
|
456
616
|
let footer_msg = format!(
|
|
457
|
-
"{}Tab/←→ Switch
|
|
458
|
-
status_line
|
|
617
|
+
"{}Tab/←→ Switch · {} · R Refresh · A Live · ESC Close",
|
|
618
|
+
status_line, nav_hint
|
|
459
619
|
);
|
|
460
620
|
let footer_color = if sd.error.is_some() {
|
|
461
621
|
Color::Rgb(255, 69, 69)
|
|
@@ -477,7 +637,7 @@ pub fn render(f: &mut Frame, state: &AppState) {
|
|
|
477
637
|
}
|
|
478
638
|
|
|
479
639
|
fn render_tab_bar(f: &mut Frame, area: Rect, sd: &ShieldDashboardState) {
|
|
480
|
-
let tabs = [ShieldTab::
|
|
640
|
+
let tabs = [ShieldTab::Activity, ShieldTab::Overview, ShieldTab::Enforcement];
|
|
481
641
|
let spans: Vec<Span> = tabs
|
|
482
642
|
.iter()
|
|
483
643
|
.flat_map(|tab| {
|
|
@@ -507,35 +667,197 @@ fn render_tab_bar(f: &mut Frame, area: Rect, sd: &ShieldDashboardState) {
|
|
|
507
667
|
f.render_widget(block, area);
|
|
508
668
|
}
|
|
509
669
|
|
|
510
|
-
fn
|
|
511
|
-
|
|
512
|
-
|
|
670
|
+
fn render_activity_panel(f: &mut Frame, area: Rect, sd: &ShieldDashboardState) {
|
|
671
|
+
use ratatui::layout::{Constraint, Direction, Layout};
|
|
672
|
+
|
|
673
|
+
let outer = Layout::default()
|
|
674
|
+
.direction(Direction::Vertical)
|
|
675
|
+
.constraints([Constraint::Length(3), Constraint::Min(6)])
|
|
676
|
+
.split(area);
|
|
677
|
+
|
|
678
|
+
let counter_block = Block::default()
|
|
679
|
+
.title(format!(
|
|
680
|
+
" Live counters (refreshed {}) ",
|
|
681
|
+
refresh_ago(sd)
|
|
682
|
+
))
|
|
683
|
+
.borders(Borders::ALL)
|
|
684
|
+
.border_style(Style::default().fg(NEON_GREEN));
|
|
685
|
+
let counter_inner = counter_block.inner(outer[0]);
|
|
686
|
+
f.render_widget(counter_block, outer[0]);
|
|
687
|
+
f.render_widget(
|
|
688
|
+
Paragraph::new(Line::from(vec![
|
|
689
|
+
Span::styled("Decisions ", Style::default().fg(TEXT_DIM)),
|
|
690
|
+
Span::styled(
|
|
691
|
+
sd.decisions_total.to_string(),
|
|
692
|
+
Style::default().fg(TEXT_PRIMARY).bold(),
|
|
693
|
+
),
|
|
694
|
+
Span::styled(
|
|
695
|
+
format_delta(sd.decisions_delta),
|
|
696
|
+
Style::default().fg(if sd.decisions_delta > 0 {
|
|
697
|
+
NEON_GREEN
|
|
698
|
+
} else {
|
|
699
|
+
TEXT_MUTED
|
|
700
|
+
}),
|
|
701
|
+
),
|
|
702
|
+
Span::raw(" "),
|
|
703
|
+
Span::styled("Blocks ", Style::default().fg(TEXT_DIM)),
|
|
704
|
+
Span::styled(
|
|
705
|
+
sd.blocks_total.to_string(),
|
|
706
|
+
Style::default().fg(SHIELD_ORANGE).bold(),
|
|
707
|
+
),
|
|
708
|
+
Span::styled(
|
|
709
|
+
format_delta(sd.blocks_delta),
|
|
710
|
+
Style::default().fg(if sd.blocks_delta > 0 {
|
|
711
|
+
NEON_GREEN
|
|
712
|
+
} else {
|
|
713
|
+
TEXT_MUTED
|
|
714
|
+
}),
|
|
715
|
+
),
|
|
716
|
+
Span::raw(" "),
|
|
717
|
+
Span::styled("Masks ", Style::default().fg(TEXT_DIM)),
|
|
718
|
+
Span::styled(
|
|
719
|
+
sd.masks_total.to_string(),
|
|
720
|
+
Style::default().fg(CYBER_CYAN).bold(),
|
|
721
|
+
),
|
|
722
|
+
Span::styled(
|
|
723
|
+
format_delta(sd.masks_delta),
|
|
724
|
+
Style::default().fg(if sd.masks_delta > 0 {
|
|
725
|
+
NEON_GREEN
|
|
726
|
+
} else {
|
|
727
|
+
TEXT_MUTED
|
|
728
|
+
}),
|
|
729
|
+
),
|
|
730
|
+
Span::raw(" "),
|
|
731
|
+
Span::styled("Rewrites ", Style::default().fg(TEXT_DIM)),
|
|
732
|
+
Span::styled(
|
|
733
|
+
sd.rewrites_total.to_string(),
|
|
734
|
+
Style::default().fg(AMBER_WARN).bold(),
|
|
735
|
+
),
|
|
736
|
+
])),
|
|
737
|
+
counter_inner,
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
let body = Layout::default()
|
|
741
|
+
.direction(Direction::Horizontal)
|
|
742
|
+
.constraints([Constraint::Percentage(62), Constraint::Percentage(38)])
|
|
743
|
+
.split(outer[1]);
|
|
744
|
+
|
|
745
|
+
let feed_title = format!(
|
|
746
|
+
" Shield events ({}) — ↑↓ select · Enter open run ",
|
|
747
|
+
sd.activity_events.len()
|
|
748
|
+
);
|
|
749
|
+
let feed_block = Block::default()
|
|
750
|
+
.title(feed_title)
|
|
751
|
+
.borders(Borders::ALL)
|
|
752
|
+
.border_style(Style::default().fg(SHIELD_ORANGE))
|
|
753
|
+
.style(Style::default().bg(BG_PANEL));
|
|
754
|
+
let feed_inner = feed_block.inner(body[0]);
|
|
755
|
+
f.render_widget(feed_block, body[0]);
|
|
756
|
+
|
|
757
|
+
if sd.activity_events.is_empty() {
|
|
758
|
+
let empty = vec![
|
|
759
|
+
Line::from(""),
|
|
760
|
+
Line::from("No Shield events in recent Gateway history yet.")
|
|
761
|
+
.style(Style::default().fg(TEXT_DIM)),
|
|
762
|
+
Line::from(""),
|
|
763
|
+
Line::from("Counters above update from Prometheus on every refresh.")
|
|
764
|
+
.style(Style::default().fg(TEXT_MUTED)),
|
|
765
|
+
Line::from("Run an agent with injection/PII to see blocks and masks here.")
|
|
766
|
+
.style(Style::default().fg(TEXT_MUTED)),
|
|
767
|
+
];
|
|
768
|
+
f.render_widget(Paragraph::new(empty).alignment(Alignment::Center), feed_inner);
|
|
769
|
+
} else {
|
|
770
|
+
let items: Vec<ListItem> = sd
|
|
771
|
+
.activity_events
|
|
772
|
+
.iter()
|
|
773
|
+
.enumerate()
|
|
774
|
+
.map(|(idx, e)| {
|
|
775
|
+
let selected = idx == sd.selected_activity_index;
|
|
776
|
+
let prefix = if selected { "▶ " } else { " " };
|
|
777
|
+
let kind_upper = e.kind.to_uppercase();
|
|
778
|
+
let run_hint = e
|
|
779
|
+
.run_name
|
|
780
|
+
.clone()
|
|
781
|
+
.or_else(|| {
|
|
782
|
+
e.run_id.as_ref().map(|id| {
|
|
783
|
+
if id.len() > 8 {
|
|
784
|
+
id[id.len() - 8..].to_string()
|
|
785
|
+
} else {
|
|
786
|
+
id.clone()
|
|
787
|
+
}
|
|
788
|
+
})
|
|
789
|
+
})
|
|
790
|
+
.unwrap_or_else(|| "—".to_string());
|
|
791
|
+
ListItem::new(Line::from(vec![
|
|
792
|
+
Span::styled(prefix, Style::default().fg(SHIELD_ORANGE).bold()),
|
|
793
|
+
Span::styled(
|
|
794
|
+
format!("{} ", e.at),
|
|
795
|
+
Style::default().fg(TEXT_MUTED),
|
|
796
|
+
),
|
|
797
|
+
Span::styled(
|
|
798
|
+
format!("[{}] ", kind_upper),
|
|
799
|
+
Style::default().fg(kind_color(&e.kind)).bold(),
|
|
800
|
+
),
|
|
801
|
+
Span::styled(
|
|
802
|
+
format!("{} ", run_hint),
|
|
803
|
+
Style::default().fg(if selected { CYBER_CYAN } else { TEXT_PRIMARY }),
|
|
804
|
+
),
|
|
805
|
+
Span::styled(
|
|
806
|
+
e.detail.chars().take(52).collect::<String>(),
|
|
807
|
+
Style::default().fg(TEXT_DIM),
|
|
808
|
+
),
|
|
809
|
+
Span::styled(
|
|
810
|
+
format!(" ({})", e.source),
|
|
811
|
+
Style::default().fg(TEXT_MUTED),
|
|
812
|
+
),
|
|
813
|
+
]))
|
|
814
|
+
})
|
|
815
|
+
.collect();
|
|
816
|
+
f.render_widget(List::new(items), feed_inner);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
let breakdown_block = Block::default()
|
|
820
|
+
.title(" Prometheus breakdown ")
|
|
513
821
|
.borders(Borders::ALL)
|
|
514
822
|
.border_style(Style::default().fg(BRAND_PURPLE))
|
|
515
823
|
.style(Style::default().bg(BG_PANEL));
|
|
516
|
-
let
|
|
517
|
-
f.render_widget(
|
|
824
|
+
let breakdown_inner = breakdown_block.inner(body[1]);
|
|
825
|
+
f.render_widget(breakdown_block, body[1]);
|
|
518
826
|
|
|
519
|
-
let mut
|
|
520
|
-
|
|
521
|
-
.
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
827
|
+
let mut breakdown_lines: Vec<Line> = Vec::new();
|
|
828
|
+
if !sd.snapshot_at.is_empty() {
|
|
829
|
+
breakdown_lines.push(Line::from(vec![
|
|
830
|
+
Span::styled("Snapshot: ", Style::default().fg(TEXT_DIM)),
|
|
831
|
+
Span::styled(
|
|
832
|
+
sd.snapshot_at.chars().take(19).collect::<String>(),
|
|
833
|
+
Style::default().fg(TEXT_MUTED),
|
|
834
|
+
),
|
|
835
|
+
]));
|
|
836
|
+
breakdown_lines.push(Line::from(""));
|
|
837
|
+
}
|
|
838
|
+
for line in &sd.breakdown_lines {
|
|
839
|
+
let style = if line.ends_with(':') || !line.starts_with(' ') {
|
|
840
|
+
Style::default().fg(NEON_GREEN).bold()
|
|
841
|
+
} else {
|
|
842
|
+
Style::default().fg(TEXT_PRIMARY)
|
|
843
|
+
};
|
|
844
|
+
breakdown_lines.push(Line::from(line.as_str()).style(style));
|
|
845
|
+
}
|
|
531
846
|
if !sd.warnings.is_empty() {
|
|
532
|
-
|
|
533
|
-
|
|
847
|
+
breakdown_lines.push(Line::from(""));
|
|
848
|
+
breakdown_lines.push(
|
|
849
|
+
Line::from("Warnings:").style(Style::default().fg(AMBER_WARN).bold()),
|
|
850
|
+
);
|
|
534
851
|
for w in &sd.warnings {
|
|
535
|
-
|
|
852
|
+
breakdown_lines.push(
|
|
853
|
+
Line::from(format!(" ⚠ {}", w)).style(Style::default().fg(AMBER_WARN)),
|
|
854
|
+
);
|
|
536
855
|
}
|
|
537
856
|
}
|
|
538
|
-
f.render_widget(
|
|
857
|
+
f.render_widget(
|
|
858
|
+
Paragraph::new(breakdown_lines).wrap(Wrap { trim: false }),
|
|
859
|
+
breakdown_inner,
|
|
860
|
+
);
|
|
539
861
|
}
|
|
540
862
|
|
|
541
863
|
fn render_status_panel(f: &mut Frame, area: Rect, sd: &ShieldDashboardState) {
|
|
@@ -597,8 +919,18 @@ fn render_status_panel(f: &mut Frame, area: Rect, sd: &ShieldDashboardState) {
|
|
|
597
919
|
}
|
|
598
920
|
|
|
599
921
|
lines.push(Line::from(""));
|
|
600
|
-
|
|
601
|
-
|
|
922
|
+
lines.push(Line::from(vec![
|
|
923
|
+
Span::styled("Last refresh: ", Style::default().fg(TEXT_DIM)),
|
|
924
|
+
Span::styled(refresh_ago(sd), Style::default().fg(TEXT_MUTED)),
|
|
925
|
+
]));
|
|
926
|
+
if let Some(first) = sd.activity_events.first() {
|
|
927
|
+
lines.push(Line::from(vec![
|
|
928
|
+
Span::styled("Latest event: ", Style::default().fg(TEXT_DIM)),
|
|
929
|
+
Span::styled(
|
|
930
|
+
format!("[{}] {}", first.kind.to_uppercase(), first.detail.chars().take(40).collect::<String>()),
|
|
931
|
+
Style::default().fg(kind_color(&first.kind)),
|
|
932
|
+
),
|
|
933
|
+
]));
|
|
602
934
|
}
|
|
603
935
|
|
|
604
936
|
f.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner);
|
|
@@ -627,18 +959,16 @@ fn render_metrics_panel(f: &mut Frame, area: Rect, sd: &ShieldDashboardState) {
|
|
|
627
959
|
|
|
628
960
|
let lines = vec![
|
|
629
961
|
Line::from(""),
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
962
|
+
metric_line_delta("Decisions", sd.decisions_total, sd.decisions_delta, TEXT_PRIMARY),
|
|
963
|
+
metric_line_delta("Blocks", sd.blocks_total, sd.blocks_delta, SHIELD_ORANGE),
|
|
964
|
+
metric_line_delta("Masks", sd.masks_total, sd.masks_delta, CYBER_CYAN),
|
|
633
965
|
metric_line("Rewrites total", sd.rewrites_total, AMBER_WARN),
|
|
634
966
|
Line::from(""),
|
|
967
|
+
Line::from(format!("Refreshed {}", refresh_ago(sd))).style(Style::default().fg(TEXT_MUTED)),
|
|
635
968
|
Line::from("Source: Gateway Prometheus /metrics").style(Style::default().fg(TEXT_MUTED)),
|
|
636
|
-
Line::from("
|
|
969
|
+
Line::from("+N = change since last refresh on this screen.").style(Style::default().fg(TEXT_MUTED)),
|
|
637
970
|
Line::from(""),
|
|
638
|
-
Line::from(
|
|
639
|
-
Line::from(ABOUT_SHIELD[5]).style(Style::default().fg(TEXT_DIM)),
|
|
640
|
-
Line::from(ABOUT_SHIELD[6]).style(Style::default().fg(TEXT_DIM)),
|
|
641
|
-
Line::from(ABOUT_SHIELD[7]).style(Style::default().fg(TEXT_DIM)),
|
|
971
|
+
Line::from("Activity tab shows live event feed + breakdown.").style(Style::default().fg(NEON_GREEN)),
|
|
642
972
|
];
|
|
643
973
|
|
|
644
974
|
f.render_widget(Paragraph::new(lines), inner);
|
|
@@ -654,6 +984,20 @@ fn metric_line(label: &str, value: u64, color: Color) -> Line<'static> {
|
|
|
654
984
|
])
|
|
655
985
|
}
|
|
656
986
|
|
|
987
|
+
fn metric_line_delta(label: &str, value: u64, delta: i64, color: Color) -> Line<'static> {
|
|
988
|
+
Line::from(vec![
|
|
989
|
+
Span::styled(format!(" {:<18}", label), Style::default().fg(TEXT_DIM)),
|
|
990
|
+
Span::styled(
|
|
991
|
+
value.to_string(),
|
|
992
|
+
Style::default().fg(color).bold(),
|
|
993
|
+
),
|
|
994
|
+
Span::styled(
|
|
995
|
+
format_delta(delta),
|
|
996
|
+
Style::default().fg(if delta > 0 { NEON_GREEN } else { TEXT_MUTED }),
|
|
997
|
+
),
|
|
998
|
+
])
|
|
999
|
+
}
|
|
1000
|
+
|
|
657
1001
|
fn render_enforcement_panel(f: &mut Frame, area: Rect, sd: &ShieldDashboardState) {
|
|
658
1002
|
let block = Block::default()
|
|
659
1003
|
.title(format!(
|
|
@@ -672,7 +1016,7 @@ fn render_enforcement_panel(f: &mut Frame, area: Rect, sd: &ShieldDashboardState
|
|
|
672
1016
|
Line::from("No blocked runs in Gateway history yet.").style(Style::default().fg(TEXT_DIM)),
|
|
673
1017
|
Line::from(""),
|
|
674
1018
|
Line::from("Shield still evaluates every agent/API run on input.").style(Style::default().fg(TEXT_MUTED)),
|
|
675
|
-
Line::from("
|
|
1019
|
+
Line::from("Activity tab shows live counters and event feed on refresh.").style(Style::default().fg(NEON_GREEN)),
|
|
676
1020
|
];
|
|
677
1021
|
f.render_widget(Paragraph::new(empty).alignment(Alignment::Center), inner);
|
|
678
1022
|
return;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "4runr-os",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.78",
|
|
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.78: Shield Activity tab — live event feed, metric deltas, and Prometheus breakdown on-screen.",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"4runr": "dist/index.js",
|