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.
@@ -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::Help,
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::Overview => ShieldTab::Help,
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::Overview,
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 — {} · {} block(s) · {} mask(s){}{} ",
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 view · ↑↓ Select block · Enter Open run · R Refresh · O Runs · P Monitoring · A Live · ESC Close",
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::Overview, ShieldTab::Enforcement, ShieldTab::Help];
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 render_help_panel(f: &mut Frame, area: Rect, sd: &ShieldDashboardState) {
511
- let block = Block::default()
512
- .title(" How Shield works in production ")
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 inner = block.inner(area);
517
- f.render_widget(block, area);
824
+ let breakdown_inner = breakdown_block.inner(body[1]);
825
+ f.render_widget(breakdown_block, body[1]);
518
826
 
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"));
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
- lines.push(Line::from(""));
533
- lines.push(Line::from("Active warnings:").style(Style::default().fg(AMBER_WARN).bold()));
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
- lines.push(Line::from(format!(" ⚠ {}", w)).style(Style::default().fg(AMBER_WARN)));
852
+ breakdown_lines.push(
853
+ Line::from(format!(" ⚠ {}", w)).style(Style::default().fg(AMBER_WARN)),
854
+ );
536
855
  }
537
856
  }
538
- f.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner);
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
- for line in ABOUT_SHIELD.iter().take(4) {
601
- lines.push(Line::from(*line).style(Style::default().fg(TEXT_MUTED)));
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
- metric_line("Decisions total", sd.decisions_total, TEXT_PRIMARY),
631
- metric_line("Blocks total", sd.blocks_total, SHIELD_ORANGE),
632
- metric_line("Masks total", sd.masks_total, CYBER_CYAN),
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("Updates on refresh counts all production runs.").style(Style::default().fg(TEXT_MUTED)),
969
+ Line::from("+N = change since last refresh on this screen.").style(Style::default().fg(TEXT_MUTED)),
637
970
  Line::from(""),
638
- Line::from(ABOUT_SHIELD[4]).style(Style::default().fg(TEXT_DIM)),
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("Press O to open Run Manager · P for live Prometheus metrics.").style(Style::default().fg(NEON_GREEN)),
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.77",
3
+ "version": "2.10.78",
4
4
  "type": "module",
5
- "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.10.77: Fix Shield dashboard loadshield.load response no longer swallowed by legacy handler.",
5
+ "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.10.78: Shield Activity tablive event feed, metric deltas, and Prometheus breakdown on-screen.",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
8
  "4runr": "dist/index.js",