4runr-os 2.10.39 → 2.10.40

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.
Files changed (30) hide show
  1. package/apps/gateway/dist/apps/gateway/src/index.js +14 -4
  2. package/apps/gateway/dist/apps/gateway/src/index.js.map +1 -1
  3. package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.d.ts +18 -0
  4. package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.d.ts.map +1 -0
  5. package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.js +117 -0
  6. package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.js.map +1 -0
  7. package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.d.ts +2 -0
  8. package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.d.ts.map +1 -0
  9. package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.js +54 -0
  10. package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.js.map +1 -0
  11. package/apps/gateway/dist/apps/gateway/src/routes/monitoring.d.ts +15 -0
  12. package/apps/gateway/dist/apps/gateway/src/routes/monitoring.d.ts.map +1 -0
  13. package/apps/gateway/dist/apps/gateway/src/routes/monitoring.js +164 -0
  14. package/apps/gateway/dist/apps/gateway/src/routes/monitoring.js.map +1 -0
  15. package/apps/gateway/package-lock.json +204 -353
  16. package/apps/gateway/src/index.ts +27 -8
  17. package/apps/gateway/src/metrics/monitoring-detail.ts +162 -0
  18. package/apps/gateway/src/middleware/log-capture.ts +70 -0
  19. package/apps/gateway/src/routes/monitoring.ts +298 -0
  20. package/dist/gateway-client.d.ts +2 -0
  21. package/dist/gateway-client.d.ts.map +1 -1
  22. package/dist/gateway-client.js +22 -0
  23. package/dist/gateway-client.js.map +1 -1
  24. package/dist/tui-handlers.js +498 -0
  25. package/dist/tui-handlers.js.map +1 -1
  26. package/mk3-tui/src/app.rs +569 -8
  27. package/mk3-tui/src/main.rs +248 -0
  28. package/mk3-tui/src/monitoring/mod.rs +428 -0
  29. package/mk3-tui/src/ui/portal_monitoring.rs +1018 -146
  30. package/package.json +2 -2
@@ -11,6 +11,7 @@ use std::time::{Duration, Instant};
11
11
  mod app;
12
12
  mod debug_log;
13
13
  mod io;
14
+ mod monitoring;
14
15
  mod screens;
15
16
  mod storage;
16
17
  mod ui;
@@ -407,6 +408,113 @@ fn main() -> Result<()> {
407
408
  }
408
409
  app.request_render("run_quick_ok");
409
410
  }
411
+ // Portal Monitoring Phase 2: monitoring.refresh (section slice)
412
+ else if Some(&resp.id) == app.state.pending_monitoring_refresh_id.as_ref() {
413
+ app.state.pending_monitoring_refresh_id = None;
414
+ app.state.portal_monitoring.section_refresh_loading = None;
415
+ if obj.get("monitoringRefresh").and_then(|v| v.as_bool()) == Some(true) {
416
+ let section_str =
417
+ obj.get("section").and_then(|v| v.as_str()).unwrap_or("");
418
+ use crate::monitoring::MonitoringSection;
419
+ if let Some(sec) = MonitoringSection::from_cli_slug(section_str)
420
+ {
421
+ let lines: Vec<String> = obj
422
+ .get("lines")
423
+ .and_then(|a| a.as_array())
424
+ .map(|arr| {
425
+ arr.iter()
426
+ .filter_map(|v| {
427
+ v.as_str().map(|s| s.to_string())
428
+ })
429
+ .collect()
430
+ })
431
+ .unwrap_or_default();
432
+ app.state
433
+ .portal_monitoring
434
+ .section_overrides
435
+ .insert(sec, lines);
436
+ }
437
+ }
438
+ app.request_render("monitoring_refresh_ok");
439
+ }
440
+ // Portal Monitoring Phase 2: monitoring.logs
441
+ else if Some(&resp.id) == app.state.pending_monitoring_logs_id.as_ref() {
442
+ app.state.pending_monitoring_logs_id = None;
443
+ app.state.portal_monitoring.logs_fetch_loading = false;
444
+ if obj.get("monitoringLogs").and_then(|v| v.as_bool()) == Some(true) {
445
+ let lines: Vec<String> = obj
446
+ .get("logs")
447
+ .and_then(|a| a.as_array())
448
+ .map(|arr| {
449
+ arr.iter()
450
+ .filter_map(|entry| {
451
+ let e = entry.as_object()?;
452
+ let ts = e
453
+ .get("timestamp")
454
+ .and_then(|v| v.as_str())
455
+ .unwrap_or("");
456
+ let lvl = e
457
+ .get("level")
458
+ .and_then(|v| v.as_str())
459
+ .unwrap_or("");
460
+ let msg = e
461
+ .get("message")
462
+ .and_then(|v| v.as_str())
463
+ .unwrap_or("");
464
+ if ts.is_empty()
465
+ && lvl.is_empty()
466
+ && msg.is_empty()
467
+ {
468
+ return None;
469
+ }
470
+ Some(format!("{} [{}] {}", ts, lvl, msg))
471
+ })
472
+ .collect()
473
+ })
474
+ .unwrap_or_default();
475
+ use crate::monitoring::MonitoringSection;
476
+ app.state.portal_monitoring.section_overrides.insert(
477
+ MonitoringSection::Logs,
478
+ lines,
479
+ );
480
+ }
481
+ app.request_render("monitoring_logs_ok");
482
+ }
483
+ // Portal Monitoring Phase 3: monitoring.drill (metrics sub-panels / dependency detail)
484
+ else if Some(&resp.id) == app.state.pending_monitoring_drill_id.as_ref() {
485
+ app.state.pending_monitoring_drill_id = None;
486
+ app.state.portal_monitoring.metrics_drill_loading = false;
487
+ app.state.portal_monitoring.section_refresh_loading = None;
488
+ if obj.get("monitoringDrill").and_then(|v| v.as_bool()) == Some(true) {
489
+ let target = obj.get("target").and_then(|v| v.as_str()).unwrap_or("");
490
+ let lines: Vec<String> = obj
491
+ .get("lines")
492
+ .and_then(|a| a.as_array())
493
+ .map(|arr| {
494
+ arr.iter()
495
+ .filter_map(|v| v.as_str().map(|s| s.to_string()))
496
+ .collect()
497
+ })
498
+ .unwrap_or_default();
499
+ use crate::monitoring::{MetricsDrillPanel, MonitoringSection};
500
+ if target == "dependencies_detail" {
501
+ app.state.portal_monitoring.section_overrides.insert(
502
+ MonitoringSection::Dependencies,
503
+ lines,
504
+ );
505
+ } else if target == "system_diagnostics" {
506
+ app.state.portal_monitoring.section_overrides.insert(
507
+ MonitoringSection::System,
508
+ lines,
509
+ );
510
+ } else if let Some(panel) = MetricsDrillPanel::from_cli_target(target)
511
+ {
512
+ app.state.portal_monitoring.metrics_drill = Some(panel);
513
+ app.state.portal_monitoring.metrics_drill_lines = lines;
514
+ }
515
+ }
516
+ app.request_render("monitoring_drill_ok");
517
+ }
410
518
  // Portal Monitoring: gateway.observability (/metrics snapshot)
411
519
  else if Some(&resp.id) == app.state.pending_gateway_observability_id.as_ref() {
412
520
  app.state.pending_gateway_observability_id = None;
@@ -418,6 +526,8 @@ fn main() -> Result<()> {
418
526
  .and_then(|v| v.as_str())
419
527
  .unwrap_or("")
420
528
  .to_string();
529
+ // Snapshot markers — keep substring order compatible with
530
+ // `portal_monitoring::SNAPSHOT_MARKER_*` (CLI healthLines + layout below).
421
531
  let mut lines: Vec<String> = vec![
422
532
  "Portal snapshot (health + metrics on each refresh / auto poll)."
423
533
  .to_string(),
@@ -447,8 +557,92 @@ fn main() -> Result<()> {
447
557
  app.state.portal_monitoring.last_http_for_delta = Some(c);
448
558
  app.state.portal_monitoring.last_instant_for_delta =
449
559
  Some(std::time::Instant::now());
560
+ app.state
561
+ .portal_monitoring
562
+ .http_total_sparkline
563
+ .push_back(c);
564
+ while app.state.portal_monitoring.http_total_sparkline.len() > 48 {
565
+ app.state
566
+ .portal_monitoring
567
+ .http_total_sparkline
568
+ .pop_front();
569
+ }
570
+ }
571
+ }
572
+ if let Some(totals) = obj.get("totals").and_then(|v| v.as_object()) {
573
+ app.state.portal_monitoring.metric_history.push_back(
574
+ crate::app::metric_history_entry_from_totals(
575
+ if snapshot.is_empty() {
576
+ "—".to_string()
577
+ } else {
578
+ snapshot.clone()
579
+ },
580
+ totals,
581
+ ),
582
+ );
583
+ while app.state.portal_monitoring.metric_history.len() > 720 {
584
+ app.state.portal_monitoring.metric_history.pop_front();
450
585
  }
451
586
  }
587
+ // Phase 5 v1: dependency alerts are text-derived from
588
+ // observability health lines, not a structured readiness enum.
589
+ let dependency_status = {
590
+ let mut status = if obj
591
+ .get("healthOk")
592
+ .and_then(|v| v.as_bool())
593
+ == Some(false)
594
+ {
595
+ "unhealthy"
596
+ } else {
597
+ "healthy"
598
+ };
599
+ if let Some(arr) = obj.get("healthLines").and_then(|a| a.as_array()) {
600
+ for v in arr {
601
+ if let Some(s) = v.as_str() {
602
+ let l = s.to_lowercase();
603
+ if l.contains("✗")
604
+ || l.contains(": down")
605
+ || l.contains("not ready")
606
+ {
607
+ status = "unhealthy";
608
+ break;
609
+ }
610
+ if l.contains("⚠")
611
+ || l.contains("degraded")
612
+ || l.contains("warning")
613
+ {
614
+ status = "degraded";
615
+ }
616
+ }
617
+ }
618
+ }
619
+ status.to_string()
620
+ };
621
+ if let Some(prev) = app
622
+ .state
623
+ .portal_monitoring
624
+ .last_dependency_status
625
+ .clone()
626
+ {
627
+ if prev != dependency_status {
628
+ app.state.portal_monitoring.dependency_alerts.push_back(
629
+ crate::app::DependencyAlertEntry {
630
+ timestamp: if snapshot.is_empty() {
631
+ "—".to_string()
632
+ } else {
633
+ snapshot.clone()
634
+ },
635
+ previous: prev,
636
+ current: dependency_status.clone(),
637
+ },
638
+ );
639
+ while app.state.portal_monitoring.dependency_alerts.len() > 10 {
640
+ app.state.portal_monitoring.dependency_alerts.pop_front();
641
+ }
642
+ }
643
+ }
644
+ app.state.portal_monitoring.last_dependency_status =
645
+ Some(dependency_status);
452
646
  lines.push(String::new());
453
647
  if let Some(arr) = obj.get("healthLines").and_then(|a| a.as_array()) {
454
648
  for v in arr {
@@ -495,6 +689,12 @@ fn main() -> Result<()> {
495
689
  }
496
690
  app.state.portal_monitoring.last_updated = Some(snapshot);
497
691
  app.state.portal_monitoring.content_lines = lines;
692
+ app.state.portal_monitoring.section_overrides.clear();
693
+ app.state.portal_monitoring.section_refresh_loading = None;
694
+ app.state.portal_monitoring.logs_fetch_loading = false;
695
+ app.state.portal_monitoring.metrics_drill = None;
696
+ app.state.portal_monitoring.metrics_drill_lines.clear();
697
+ app.state.portal_monitoring.metrics_drill_loading = false;
498
698
  app.state.portal_monitoring.error = None;
499
699
  app.state.portal_monitoring.scroll_offset = 0;
500
700
  } else {
@@ -593,8 +793,13 @@ fn main() -> Result<()> {
593
793
  app.add_log(format!("✓ [{}] Disconnected from Gateway", short_id));
594
794
  app.add_log(format!("[MODE] Switched to LOCAL mode"));
595
795
  app.state.pending_gateway_observability_id = None;
796
+ app.state.pending_monitoring_refresh_id = None;
797
+ app.state.pending_monitoring_logs_id = None;
798
+ app.state.pending_monitoring_drill_id = None;
596
799
  app.state.portal_observability_last_poll = None;
597
800
  app.state.portal_monitoring = crate::app::PortalMonitoringState::default();
801
+ app.state.advanced_monitoring =
802
+ crate::app::AdvancedMonitoringState::default();
598
803
  if let Some(ref ws) = ws_client {
599
804
  let _ = ws.send_command("system.status", None);
600
805
  }
@@ -809,6 +1014,46 @@ fn main() -> Result<()> {
809
1014
  app.state.portal_monitoring.error = Some(error_msg.clone());
810
1015
  app.request_render("portal_observability_err");
811
1016
  }
1017
+ else if Some(&resp.id) == app.state.pending_monitoring_refresh_id.as_ref() {
1018
+ app.state.pending_monitoring_refresh_id = None;
1019
+ app.state.portal_monitoring.section_refresh_loading = None;
1020
+ app.state.portal_monitoring.error = Some(format!(
1021
+ "monitoring.refresh failed: {}",
1022
+ error_msg
1023
+ ));
1024
+ app.add_log(format!(
1025
+ "✗ [{}] monitoring.refresh failed: {}",
1026
+ short_id, error_msg
1027
+ ));
1028
+ app.request_render("monitoring_refresh_err");
1029
+ }
1030
+ else if Some(&resp.id) == app.state.pending_monitoring_logs_id.as_ref() {
1031
+ app.state.pending_monitoring_logs_id = None;
1032
+ app.state.portal_monitoring.logs_fetch_loading = false;
1033
+ app.state.portal_monitoring.error = Some(format!(
1034
+ "monitoring.logs failed: {}",
1035
+ error_msg
1036
+ ));
1037
+ app.add_log(format!(
1038
+ "✗ [{}] monitoring.logs failed: {}",
1039
+ short_id, error_msg
1040
+ ));
1041
+ app.request_render("monitoring_logs_err");
1042
+ }
1043
+ else if Some(&resp.id) == app.state.pending_monitoring_drill_id.as_ref() {
1044
+ app.state.pending_monitoring_drill_id = None;
1045
+ app.state.portal_monitoring.metrics_drill_loading = false;
1046
+ app.state.portal_monitoring.section_refresh_loading = None;
1047
+ app.state.portal_monitoring.error = Some(format!(
1048
+ "monitoring.drill failed: {}",
1049
+ error_msg
1050
+ ));
1051
+ app.add_log(format!(
1052
+ "✗ [{}] monitoring.drill failed: {}",
1053
+ short_id, error_msg
1054
+ ));
1055
+ app.request_render("monitoring_drill_err");
1056
+ }
812
1057
  // Generic error handling
813
1058
  else {
814
1059
  // Try to extract command name from response for better error messages
@@ -856,6 +1101,9 @@ fn main() -> Result<()> {
856
1101
  if app.state.navigation.current_screen() == &Screen::PortalMonitoring
857
1102
  && app.state.operation_mode == OperationMode::Connected
858
1103
  && app.state.pending_gateway_observability_id.is_none()
1104
+ && app.state.pending_monitoring_refresh_id.is_none()
1105
+ && app.state.pending_monitoring_logs_id.is_none()
1106
+ && app.state.pending_monitoring_drill_id.is_none()
859
1107
  {
860
1108
  let interval = Duration::from_secs(5);
861
1109
  let due = app