4runr-os 2.10.74 → 2.10.76

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.
@@ -0,0 +1,723 @@
1
+ use crate::app::AppState;
2
+ use ratatui::prelude::*;
3
+ use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph, Wrap};
4
+ use serde_json::Value;
5
+
6
+ const BRAND_PURPLE: Color = Color::Rgb(138, 43, 226);
7
+ const CYBER_CYAN: Color = Color::Rgb(0, 255, 255);
8
+ const NEON_GREEN: Color = Color::Rgb(57, 255, 20);
9
+ const AMBER_WARN: Color = Color::Rgb(255, 191, 0);
10
+ const SHIELD_ORANGE: Color = Color::Rgb(255, 140, 0);
11
+ const TEXT_PRIMARY: Color = Color::Rgb(230, 230, 240);
12
+ const TEXT_DIM: Color = Color::Rgb(100, 100, 120);
13
+ const TEXT_MUTED: Color = Color::Rgb(60, 60, 80);
14
+ const BG_PANEL: Color = Color::Rgb(18, 18, 25);
15
+
16
+ const ABOUT_SHIELD: &[&str] = &[
17
+ "Shield is 4Runr's input/output safety layer on the Gateway run path.",
18
+ "",
19
+ "Production enforcement (every real run):",
20
+ " • Input check before agent execution starts",
21
+ " • Injection patterns → block (failed run)",
22
+ " • PII in prompts → mask or block per policy",
23
+ " • Output check after agent returns",
24
+ "",
25
+ "This screen shows live Gateway state — no test runs required.",
26
+ "Counters increment as real agent/API traffic is evaluated.",
27
+ ];
28
+
29
+ #[derive(Debug, Clone)]
30
+ pub struct ShieldBlockRow {
31
+ pub id: String,
32
+ pub name: String,
33
+ pub reason: String,
34
+ pub created_at: String,
35
+ }
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
+
70
+ #[derive(Debug, Clone)]
71
+ pub struct ShieldDashboardState {
72
+ pub tab: ShieldTab,
73
+ pub selected_block_index: usize,
74
+ pub loaded_once: bool,
75
+ pub enabled: bool,
76
+ pub mode: String,
77
+ pub health_status: String,
78
+ pub pii_enabled: bool,
79
+ pub injection_enabled: bool,
80
+ pub injection_action: String,
81
+ pub hallucination_enabled: bool,
82
+ pub hallucination_action: String,
83
+ pub decisions_total: u64,
84
+ pub blocks_total: u64,
85
+ pub masks_total: u64,
86
+ pub rewrites_total: u64,
87
+ pub warnings: Vec<String>,
88
+ pub recent_blocks: Vec<ShieldBlockRow>,
89
+ pub loading: bool,
90
+ pub loading_started: Option<std::time::Instant>,
91
+ pub error: Option<String>,
92
+ pub last_refresh: Option<std::time::Instant>,
93
+ pub auto_refresh_enabled: bool,
94
+ pub auto_refresh_interval: std::time::Duration,
95
+ }
96
+
97
+ impl Default for ShieldDashboardState {
98
+ fn default() -> Self {
99
+ Self {
100
+ tab: ShieldTab::Overview,
101
+ selected_block_index: 0,
102
+ loaded_once: false,
103
+ enabled: false,
104
+ mode: "off".to_string(),
105
+ health_status: String::new(),
106
+ pii_enabled: false,
107
+ injection_enabled: false,
108
+ injection_action: String::new(),
109
+ hallucination_enabled: false,
110
+ hallucination_action: String::new(),
111
+ decisions_total: 0,
112
+ blocks_total: 0,
113
+ masks_total: 0,
114
+ rewrites_total: 0,
115
+ warnings: Vec::new(),
116
+ recent_blocks: Vec::new(),
117
+ loading: false,
118
+ loading_started: None,
119
+ error: None,
120
+ last_refresh: None,
121
+ auto_refresh_enabled: true,
122
+ auto_refresh_interval: std::time::Duration::from_secs(5),
123
+ }
124
+ }
125
+ }
126
+
127
+ impl ShieldDashboardState {
128
+
129
+ pub fn apply_load(
130
+ &mut self,
131
+ health: &ShieldHealthSnap,
132
+ config: &ShieldConfigSnap,
133
+ metrics: &ShieldMetricsSnap,
134
+ warnings: Vec<String>,
135
+ recent: Vec<ShieldBlockRow>,
136
+ ) {
137
+ self.loading = false;
138
+ self.loading_started = None;
139
+ self.error = None;
140
+ self.loaded_once = true;
141
+ self.enabled = health.enabled;
142
+ self.mode = health.mode.clone();
143
+ self.health_status = health.status.clone();
144
+ self.pii_enabled = config.pii_enabled;
145
+ self.injection_enabled = config.injection_enabled;
146
+ self.injection_action = config.injection_action.clone();
147
+ self.hallucination_enabled = config.hallucination_enabled;
148
+ self.hallucination_action = config.hallucination_action.clone();
149
+ self.decisions_total = metrics.decisions;
150
+ self.blocks_total = metrics.blocks;
151
+ self.masks_total = metrics.masks;
152
+ self.rewrites_total = metrics.rewrites;
153
+ self.warnings = warnings;
154
+ self.recent_blocks = recent;
155
+ if self.selected_block_index >= self.recent_blocks.len() {
156
+ self.selected_block_index = 0;
157
+ }
158
+ self.last_refresh = Some(std::time::Instant::now());
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
+ }
173
+ }
174
+
175
+ #[derive(Debug, Clone, Default)]
176
+ pub struct ShieldHealthSnap {
177
+ pub enabled: bool,
178
+ pub mode: String,
179
+ pub status: String,
180
+ }
181
+
182
+ #[derive(Debug, Clone, Default)]
183
+ pub struct ShieldConfigSnap {
184
+ pub pii_enabled: bool,
185
+ pub injection_enabled: bool,
186
+ pub injection_action: String,
187
+ pub hallucination_enabled: bool,
188
+ pub hallucination_action: String,
189
+ }
190
+
191
+ #[derive(Debug, Clone, Default)]
192
+ pub struct ShieldMetricsSnap {
193
+ pub decisions: u64,
194
+ pub blocks: u64,
195
+ pub masks: u64,
196
+ pub rewrites: u64,
197
+ }
198
+
199
+ pub fn parse_shield_load(
200
+ health_val: &Value,
201
+ config_val: &Value,
202
+ metrics_val: &Value,
203
+ gateway_health_val: &Value,
204
+ runs_val: &Value,
205
+ ) -> (
206
+ ShieldHealthSnap,
207
+ ShieldConfigSnap,
208
+ ShieldMetricsSnap,
209
+ Vec<String>,
210
+ Vec<ShieldBlockRow>,
211
+ ) {
212
+ let health_obj = health_val.as_object();
213
+ let health = ShieldHealthSnap {
214
+ enabled: health_obj
215
+ .and_then(|o| o.get("enabled"))
216
+ .and_then(|v| v.as_bool())
217
+ .unwrap_or(false),
218
+ mode: health_obj
219
+ .and_then(|o| o.get("mode"))
220
+ .and_then(|v| v.as_str())
221
+ .unwrap_or("off")
222
+ .to_string(),
223
+ status: if health_obj
224
+ .and_then(|o| o.get("enabled"))
225
+ .and_then(|v| v.as_bool())
226
+ .unwrap_or(false)
227
+ {
228
+ "active".to_string()
229
+ } else {
230
+ "disabled".to_string()
231
+ },
232
+ };
233
+
234
+ let cfg = config_val
235
+ .get("config")
236
+ .or(Some(config_val))
237
+ .and_then(|v| v.as_object());
238
+ let pii = cfg.and_then(|c| c.get("pii")).and_then(|v| v.as_object());
239
+ let injection = cfg.and_then(|c| c.get("injection")).and_then(|v| v.as_object());
240
+ let hallucination = cfg
241
+ .and_then(|c| c.get("hallucination"))
242
+ .and_then(|v| v.as_object());
243
+
244
+ let config = ShieldConfigSnap {
245
+ pii_enabled: pii
246
+ .and_then(|o| o.get("enabled"))
247
+ .and_then(|v| v.as_bool())
248
+ .unwrap_or(false),
249
+ injection_enabled: injection
250
+ .and_then(|o| o.get("enabled"))
251
+ .and_then(|v| v.as_bool())
252
+ .unwrap_or(false),
253
+ injection_action: injection
254
+ .and_then(|o| o.get("action"))
255
+ .and_then(|v| v.as_str())
256
+ .unwrap_or("—")
257
+ .to_string(),
258
+ hallucination_enabled: hallucination
259
+ .and_then(|o| o.get("enabled"))
260
+ .and_then(|v| v.as_bool())
261
+ .unwrap_or(false),
262
+ hallucination_action: hallucination
263
+ .and_then(|o| o.get("action"))
264
+ .and_then(|v| v.as_str())
265
+ .unwrap_or("—")
266
+ .to_string(),
267
+ };
268
+
269
+ let metrics = ShieldMetricsSnap {
270
+ decisions: metrics_val
271
+ .get("decisions")
272
+ .and_then(|v| v.as_u64())
273
+ .unwrap_or(0),
274
+ blocks: metrics_val
275
+ .get("blocks")
276
+ .and_then(|v| v.as_u64())
277
+ .unwrap_or(0),
278
+ masks: metrics_val
279
+ .get("masks")
280
+ .and_then(|v| v.as_u64())
281
+ .unwrap_or(0),
282
+ rewrites: metrics_val
283
+ .get("rewrites")
284
+ .and_then(|v| v.as_u64())
285
+ .unwrap_or(0),
286
+ };
287
+
288
+ let mut warnings: Vec<String> = Vec::new();
289
+ if let Some(sh) = gateway_health_val
290
+ .get("checks")
291
+ .and_then(|c| c.get("shield"))
292
+ .and_then(|v| v.as_object())
293
+ {
294
+ if let Some(st) = sh.get("status").and_then(|v| v.as_str()) {
295
+ if st != "ok" && st != "up" {
296
+ warnings.push(format!("Gateway health: {}", st));
297
+ }
298
+ }
299
+ if let Some(arr) = sh.get("warnings").and_then(|v| v.as_array()) {
300
+ for w in arr {
301
+ if let Some(s) = w.as_str() {
302
+ warnings.push(s.to_string());
303
+ }
304
+ }
305
+ }
306
+ }
307
+
308
+ let mut recent: Vec<ShieldBlockRow> = Vec::new();
309
+ let runs_arr = runs_val
310
+ .as_array()
311
+ .or_else(|| runs_val.get("runs").and_then(|v| v.as_array()));
312
+ if let Some(arr) = runs_arr {
313
+ for item in arr {
314
+ let o = match item.as_object() {
315
+ Some(o) => o,
316
+ None => continue,
317
+ };
318
+ let output = o.get("output").and_then(|v| v.as_object());
319
+ let err = output
320
+ .and_then(|oo| oo.get("error"))
321
+ .and_then(|v| v.as_str())
322
+ .unwrap_or("");
323
+ let logs_shield = o
324
+ .get("logs")
325
+ .and_then(|l| l.as_array())
326
+ .map(|arr| {
327
+ arr.iter().any(|e| {
328
+ e.get("message")
329
+ .and_then(|m| m.as_str())
330
+ .map(|s| s.to_lowercase().contains("shield"))
331
+ .unwrap_or(false)
332
+ })
333
+ })
334
+ .unwrap_or(false);
335
+ if !err.to_lowercase().contains("shield") && !logs_shield {
336
+ continue;
337
+ }
338
+ let id = o
339
+ .get("id")
340
+ .and_then(|v| v.as_str())
341
+ .unwrap_or("?")
342
+ .to_string();
343
+ let name = o
344
+ .get("name")
345
+ .and_then(|v| v.as_str())
346
+ .unwrap_or("Run")
347
+ .to_string();
348
+ let reason = output
349
+ .and_then(|oo| oo.get("reason"))
350
+ .and_then(|v| v.as_str())
351
+ .unwrap_or(err)
352
+ .to_string();
353
+ let created_at = o
354
+ .get("createdAt")
355
+ .and_then(|v| v.as_str())
356
+ .map(|s| s.chars().take(19).collect())
357
+ .unwrap_or_else(|| "—".to_string());
358
+ recent.push(ShieldBlockRow {
359
+ id,
360
+ name,
361
+ reason,
362
+ created_at,
363
+ });
364
+ }
365
+ }
366
+ recent.truncate(12);
367
+
368
+ (health, config, metrics, warnings, recent)
369
+ }
370
+
371
+ pub fn render(f: &mut Frame, state: &AppState) {
372
+ let area = f.size();
373
+ let sd = &state.shield_dashboard;
374
+
375
+ use ratatui::layout::{Constraint, Direction, Layout};
376
+
377
+ let refresh_hint = if sd.loading {
378
+ " ↻"
379
+ } else {
380
+ ""
381
+ };
382
+ let live = if sd.auto_refresh_enabled { " · LIVE" } else { "" };
383
+ let header_title = format!(
384
+ " 🛡️ Shield — {} · {} block(s) · {} mask(s){}{} ",
385
+ if sd.loaded_once {
386
+ sd.mode.to_uppercase()
387
+ } else {
388
+ "…".to_string()
389
+ },
390
+ sd.blocks_total,
391
+ sd.masks_total,
392
+ live,
393
+ refresh_hint
394
+ );
395
+
396
+ let chunks = Layout::default()
397
+ .direction(Direction::Vertical)
398
+ .constraints([
399
+ Constraint::Length(3),
400
+ Constraint::Length(3),
401
+ Constraint::Min(8),
402
+ Constraint::Length(3),
403
+ ])
404
+ .split(area);
405
+
406
+ let header = Block::default()
407
+ .title(header_title)
408
+ .borders(Borders::ALL)
409
+ .border_style(Style::default().fg(SHIELD_ORANGE))
410
+ .style(Style::default().bg(BG_PANEL));
411
+ f.render_widget(header, chunks[0]);
412
+
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
+ }
448
+
449
+ let status_line = if let Some(e) = &sd.error {
450
+ format!("⚠ {} | ", e)
451
+ } else if sd.loading {
452
+ "Refreshing… | ".to_string()
453
+ } else {
454
+ String::new()
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
+ );
460
+ let footer_color = if sd.error.is_some() {
461
+ Color::Rgb(255, 69, 69)
462
+ } else {
463
+ TEXT_DIM
464
+ };
465
+ let footer = Block::default()
466
+ .title(" ⌨️ Actions ")
467
+ .borders(Borders::ALL)
468
+ .border_style(Style::default().fg(TEXT_MUTED));
469
+ let inner = footer.inner(chunks[3]);
470
+ f.render_widget(footer, chunks[3]);
471
+ f.render_widget(
472
+ Paragraph::new(footer_msg)
473
+ .style(Style::default().fg(footer_color))
474
+ .alignment(Alignment::Center),
475
+ inner,
476
+ );
477
+ }
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
+
541
+ fn render_status_panel(f: &mut Frame, area: Rect, sd: &ShieldDashboardState) {
542
+ let block = Block::default()
543
+ .title(" Status & detectors ")
544
+ .borders(Borders::ALL)
545
+ .border_style(Style::default().fg(CYBER_CYAN))
546
+ .style(Style::default().bg(BG_PANEL));
547
+ let inner = block.inner(area);
548
+ f.render_widget(block, area);
549
+
550
+ let mode_color = match sd.mode.as_str() {
551
+ "enforce" => NEON_GREEN,
552
+ "monitor" => AMBER_WARN,
553
+ _ => TEXT_MUTED,
554
+ };
555
+
556
+ let mut lines = vec![
557
+ Line::from(vec![
558
+ Span::styled("Enabled: ", Style::default().fg(TEXT_DIM)),
559
+ Span::styled(
560
+ if sd.enabled { "YES" } else { "NO" },
561
+ Style::default()
562
+ .fg(if sd.enabled { NEON_GREEN } else { TEXT_MUTED })
563
+ .bold(),
564
+ ),
565
+ ]),
566
+ Line::from(vec![
567
+ Span::styled("Mode: ", Style::default().fg(TEXT_DIM)),
568
+ Span::styled(
569
+ sd.mode.to_uppercase(),
570
+ Style::default().fg(mode_color).bold(),
571
+ ),
572
+ ]),
573
+ Line::from(""),
574
+ Line::from("Detectors (production path):").style(Style::default().fg(TEXT_DIM)),
575
+ detector_line("PII", sd.pii_enabled, "mask on input/output".to_string()),
576
+ detector_line(
577
+ "Injection",
578
+ sd.injection_enabled,
579
+ format!("action: {}", sd.injection_action),
580
+ ),
581
+ detector_line(
582
+ "Hallucination",
583
+ sd.hallucination_enabled,
584
+ format!("action: {}", sd.hallucination_action),
585
+ ),
586
+ ];
587
+
588
+ if !sd.warnings.is_empty() {
589
+ lines.push(Line::from(""));
590
+ lines.push(
591
+ Line::from("Warnings:")
592
+ .style(Style::default().fg(AMBER_WARN).bold()),
593
+ );
594
+ for w in &sd.warnings {
595
+ lines.push(Line::from(format!(" ⚠ {}", w)).style(Style::default().fg(AMBER_WARN)));
596
+ }
597
+ }
598
+
599
+ 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)));
602
+ }
603
+
604
+ f.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner);
605
+ }
606
+
607
+ fn detector_line(name: &str, on: bool, detail: String) -> Line<'static> {
608
+ Line::from(vec![
609
+ Span::raw(" "),
610
+ Span::styled(
611
+ if on { "●" } else { "○" },
612
+ Style::default().fg(if on { NEON_GREEN } else { TEXT_MUTED }),
613
+ ),
614
+ Span::styled(format!(" {:<14}", name), Style::default().fg(TEXT_PRIMARY)),
615
+ Span::styled(detail, Style::default().fg(TEXT_DIM)),
616
+ ])
617
+ }
618
+
619
+ fn render_metrics_panel(f: &mut Frame, area: Rect, sd: &ShieldDashboardState) {
620
+ let block = Block::default()
621
+ .title(" Gateway metrics (cumulative) ")
622
+ .borders(Borders::ALL)
623
+ .border_style(Style::default().fg(BRAND_PURPLE))
624
+ .style(Style::default().bg(BG_PANEL));
625
+ let inner = block.inner(area);
626
+ f.render_widget(block, area);
627
+
628
+ let lines = vec![
629
+ 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),
633
+ metric_line("Rewrites total", sd.rewrites_total, AMBER_WARN),
634
+ Line::from(""),
635
+ 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)),
637
+ 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)),
642
+ ];
643
+
644
+ f.render_widget(Paragraph::new(lines), inner);
645
+ }
646
+
647
+ fn metric_line(label: &str, value: u64, color: Color) -> Line<'static> {
648
+ Line::from(vec![
649
+ Span::styled(format!(" {:<18}", label), Style::default().fg(TEXT_DIM)),
650
+ Span::styled(
651
+ value.to_string(),
652
+ Style::default().fg(color).bold(),
653
+ ),
654
+ ])
655
+ }
656
+
657
+ fn render_enforcement_panel(f: &mut Frame, area: Rect, sd: &ShieldDashboardState) {
658
+ let block = Block::default()
659
+ .title(format!(
660
+ " Blocked runs ({}) — ↑↓ select · Enter open in Run Manager ",
661
+ sd.recent_blocks.len()
662
+ ))
663
+ .borders(Borders::ALL)
664
+ .border_style(Style::default().fg(SHIELD_ORANGE))
665
+ .style(Style::default().bg(BG_PANEL));
666
+ let inner = block.inner(area);
667
+ f.render_widget(block, area);
668
+
669
+ if sd.recent_blocks.is_empty() {
670
+ let empty = vec![
671
+ Line::from(""),
672
+ Line::from("No blocked runs in Gateway history yet.").style(Style::default().fg(TEXT_DIM)),
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)),
676
+ ];
677
+ f.render_widget(Paragraph::new(empty).alignment(Alignment::Center), inner);
678
+ return;
679
+ }
680
+
681
+ let items: Vec<ListItem> = sd
682
+ .recent_blocks
683
+ .iter()
684
+ .enumerate()
685
+ .map(|(idx, r)| {
686
+ let short_id = if r.id.len() > 8 {
687
+ &r.id[r.id.len() - 8..]
688
+ } else {
689
+ &r.id
690
+ };
691
+ let prefix = if idx == sd.selected_block_index {
692
+ "▶ "
693
+ } else {
694
+ " "
695
+ };
696
+ ListItem::new(Line::from(vec![
697
+ Span::styled(prefix, Style::default().fg(SHIELD_ORANGE).bold()),
698
+ Span::styled(
699
+ format!("{} ", r.created_at),
700
+ Style::default().fg(TEXT_MUTED),
701
+ ),
702
+ Span::styled(
703
+ format!("[{}] ", short_id),
704
+ Style::default().fg(CYBER_CYAN),
705
+ ),
706
+ Span::styled(
707
+ format!("{} — ", r.name),
708
+ Style::default().fg(if idx == sd.selected_block_index {
709
+ CYBER_CYAN
710
+ } else {
711
+ TEXT_PRIMARY
712
+ }),
713
+ ),
714
+ Span::styled(
715
+ r.reason.chars().take(48).collect::<String>(),
716
+ Style::default().fg(SHIELD_ORANGE),
717
+ ),
718
+ ]))
719
+ })
720
+ .collect();
721
+
722
+ f.render_widget(List::new(items), inner);
723
+ }