4runr-os 2.10.42 → 2.10.44
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 +3 -3
- package/mk3-tui/src/app.rs +174 -122
- package/mk3-tui/src/main.rs +40 -5
- package/mk3-tui/src/monitoring/mod.rs +11 -0
- package/mk3-tui/src/ui/portal_monitoring.rs +33 -9
- package/package.json +2 -2
|
@@ -4782,9 +4782,9 @@
|
|
|
4782
4782
|
}
|
|
4783
4783
|
},
|
|
4784
4784
|
"node_modules/electron-to-chromium": {
|
|
4785
|
-
"version": "1.5.
|
|
4786
|
-
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.
|
|
4787
|
-
"integrity": "sha512-
|
|
4785
|
+
"version": "1.5.354",
|
|
4786
|
+
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.354.tgz",
|
|
4787
|
+
"integrity": "sha512-JaBHwWcfIdmSAfWM5l3uwjGd431j8YEMikZ+K/2nXVuBqJKyZ0f+2h4n4JY5AyNiZmnY9qQr2RU3v9DxDmHMNg==",
|
|
4788
4788
|
"dev": true,
|
|
4789
4789
|
"license": "ISC"
|
|
4790
4790
|
},
|
package/mk3-tui/src/app.rs
CHANGED
|
@@ -311,7 +311,7 @@ impl Default for PortalMonitoringState {
|
|
|
311
311
|
viewport_lines: 18,
|
|
312
312
|
last_refresh: None,
|
|
313
313
|
auto_refresh_interval: Duration::from_secs(5),
|
|
314
|
-
auto_refresh_enabled:
|
|
314
|
+
auto_refresh_enabled: false,
|
|
315
315
|
last_http_for_delta: None,
|
|
316
316
|
last_instant_for_delta: None,
|
|
317
317
|
section_overrides: HashMap::new(),
|
|
@@ -922,6 +922,31 @@ impl App {
|
|
|
922
922
|
Ok(display)
|
|
923
923
|
}
|
|
924
924
|
|
|
925
|
+
pub fn scroll_portal_monitoring_by(&mut self, delta: isize) {
|
|
926
|
+
use crate::ui::portal_monitoring::portal_monitoring_scroll_max;
|
|
927
|
+
let inner_h = self.state.portal_monitoring.viewport_lines.max(5);
|
|
928
|
+
let sw = self.state.portal_monitoring.summary_clip_width.max(24);
|
|
929
|
+
let max_scroll = portal_monitoring_scroll_max(&self.state, sw, inner_h);
|
|
930
|
+
let current = self.state.portal_monitoring.scroll_offset;
|
|
931
|
+
self.state.portal_monitoring.scroll_offset = if delta < 0 {
|
|
932
|
+
current.saturating_sub(delta.unsigned_abs())
|
|
933
|
+
} else {
|
|
934
|
+
current.saturating_add(delta as usize).min(max_scroll)
|
|
935
|
+
};
|
|
936
|
+
self.request_immediate_render("portal_obs_scroll");
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
fn cancel_portal_monitoring_requests(&mut self) {
|
|
940
|
+
self.state.pending_gateway_observability_id = None;
|
|
941
|
+
self.state.pending_monitoring_refresh_id = None;
|
|
942
|
+
self.state.pending_monitoring_logs_id = None;
|
|
943
|
+
self.state.pending_monitoring_drill_id = None;
|
|
944
|
+
self.state.portal_monitoring.loading = false;
|
|
945
|
+
self.state.portal_monitoring.section_refresh_loading = None;
|
|
946
|
+
self.state.portal_monitoring.logs_fetch_loading = false;
|
|
947
|
+
self.state.portal_monitoring.metrics_drill_loading = false;
|
|
948
|
+
}
|
|
949
|
+
|
|
925
950
|
/// CLI ↔ TUI WebSocket lost: drop Gateway link UI and leave Portal Monitoring / Connection Portal so the session does not appear still "connected".
|
|
926
951
|
pub fn on_cli_backend_disconnect(&mut self) {
|
|
927
952
|
use crate::screens::Screen;
|
|
@@ -2693,7 +2718,9 @@ impl App {
|
|
|
2693
2718
|
key: KeyEvent,
|
|
2694
2719
|
ws_client: Option<&WebSocketClient>,
|
|
2695
2720
|
) -> anyhow::Result<bool> {
|
|
2696
|
-
use crate::ui::portal_monitoring::
|
|
2721
|
+
use crate::ui::portal_monitoring::{
|
|
2722
|
+
portal_monitoring_scroll_max, portal_monitoring_section_offset,
|
|
2723
|
+
};
|
|
2697
2724
|
use crossterm::event::KeyModifiers;
|
|
2698
2725
|
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
|
2699
2726
|
match key.code {
|
|
@@ -2734,37 +2761,13 @@ impl App {
|
|
|
2734
2761
|
self.request_immediate_render("portal_help_open");
|
|
2735
2762
|
return Ok(false);
|
|
2736
2763
|
}
|
|
2737
|
-
// R is
|
|
2738
|
-
// section → `monitoring.refresh` for that section only (cheaper; matches expanded detail).
|
|
2764
|
+
// R is deliberately global: refresh the full snapshot without changing the current view.
|
|
2739
2765
|
if key.code == KeyCode::Char('r') || key.code == KeyCode::Char('R') {
|
|
2740
2766
|
if let Some(ws) = ws_client {
|
|
2741
|
-
let sections = MonitoringSection::all();
|
|
2742
|
-
let idx = self
|
|
2743
|
-
.state
|
|
2744
|
-
.advanced_monitoring
|
|
2745
|
-
.monitoring_state
|
|
2746
|
-
.selected_index;
|
|
2747
|
-
let current = sections.get(idx).copied();
|
|
2748
2767
|
self.state.portal_observability_last_poll = None;
|
|
2749
|
-
self.
|
|
2750
|
-
self.state.pending_monitoring_refresh_id = None;
|
|
2751
|
-
self.state.pending_monitoring_logs_id = None;
|
|
2752
|
-
self.state.pending_monitoring_drill_id = None;
|
|
2753
|
-
self.state.portal_monitoring.loading = false;
|
|
2754
|
-
self.state.portal_monitoring.section_refresh_loading = None;
|
|
2755
|
-
self.state.portal_monitoring.logs_fetch_loading = false;
|
|
2756
|
-
self.state.portal_monitoring.metrics_drill = None;
|
|
2757
|
-
self.state.portal_monitoring.metrics_drill_lines.clear();
|
|
2758
|
-
self.state.portal_monitoring.metrics_drill_loading = false;
|
|
2768
|
+
self.cancel_portal_monitoring_requests();
|
|
2759
2769
|
self.state.portal_monitoring.last_refresh = Some(std::time::Instant::now());
|
|
2760
|
-
|
|
2761
|
-
Some(MonitoringSection::Overview) | None => {
|
|
2762
|
-
self.begin_portal_observability_request(ws);
|
|
2763
|
-
}
|
|
2764
|
-
Some(sec) => {
|
|
2765
|
-
self.begin_monitoring_refresh_request(ws, sec);
|
|
2766
|
-
}
|
|
2767
|
-
}
|
|
2770
|
+
self.begin_portal_observability_request(ws);
|
|
2768
2771
|
self.request_immediate_render("portal_obs_refresh");
|
|
2769
2772
|
} else {
|
|
2770
2773
|
self.state.portal_monitoring.error =
|
|
@@ -2774,12 +2777,25 @@ impl App {
|
|
|
2774
2777
|
return Ok(false);
|
|
2775
2778
|
}
|
|
2776
2779
|
if key.code == KeyCode::Char('l') || key.code == KeyCode::Char('L') {
|
|
2780
|
+
self.state
|
|
2781
|
+
.advanced_monitoring
|
|
2782
|
+
.monitoring_state
|
|
2783
|
+
.select_section(MonitoringSection::Logs);
|
|
2784
|
+
self.state
|
|
2785
|
+
.advanced_monitoring
|
|
2786
|
+
.monitoring_state
|
|
2787
|
+
.expand_section(MonitoringSection::Logs);
|
|
2777
2788
|
if let Some(ws) = ws_client {
|
|
2789
|
+
self.cancel_portal_monitoring_requests();
|
|
2778
2790
|
self.begin_monitoring_logs_request(ws);
|
|
2779
|
-
self.state
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
.
|
|
2791
|
+
let inner_h = self.state.portal_monitoring.viewport_lines.max(5);
|
|
2792
|
+
let sw = self.state.portal_monitoring.summary_clip_width.max(24);
|
|
2793
|
+
self.state.portal_monitoring.scroll_offset = portal_monitoring_section_offset(
|
|
2794
|
+
&self.state,
|
|
2795
|
+
sw,
|
|
2796
|
+
inner_h,
|
|
2797
|
+
MonitoringSection::Logs,
|
|
2798
|
+
);
|
|
2783
2799
|
self.request_immediate_render("portal_monitoring_logs");
|
|
2784
2800
|
} else {
|
|
2785
2801
|
self.state.portal_monitoring.error =
|
|
@@ -2790,7 +2806,23 @@ impl App {
|
|
|
2790
2806
|
}
|
|
2791
2807
|
|
|
2792
2808
|
if key.code == KeyCode::Char('h') || key.code == KeyCode::Char('H') {
|
|
2809
|
+
self.state
|
|
2810
|
+
.advanced_monitoring
|
|
2811
|
+
.monitoring_state
|
|
2812
|
+
.select_section(MonitoringSection::Metrics);
|
|
2813
|
+
self.state
|
|
2814
|
+
.advanced_monitoring
|
|
2815
|
+
.monitoring_state
|
|
2816
|
+
.expand_section(MonitoringSection::Metrics);
|
|
2793
2817
|
self.state.portal_monitoring.trend_view = !self.state.portal_monitoring.trend_view;
|
|
2818
|
+
let inner_h = self.state.portal_monitoring.viewport_lines.max(5);
|
|
2819
|
+
let sw = self.state.portal_monitoring.summary_clip_width.max(24);
|
|
2820
|
+
self.state.portal_monitoring.scroll_offset = portal_monitoring_section_offset(
|
|
2821
|
+
&self.state,
|
|
2822
|
+
sw,
|
|
2823
|
+
inner_h,
|
|
2824
|
+
MonitoringSection::Metrics,
|
|
2825
|
+
);
|
|
2794
2826
|
let status = if self.state.portal_monitoring.trend_view {
|
|
2795
2827
|
"enabled"
|
|
2796
2828
|
} else {
|
|
@@ -2817,103 +2849,97 @@ impl App {
|
|
|
2817
2849
|
|
|
2818
2850
|
// Phase 3: extended dependency view (GET /api/monitoring/dependencies/detail)
|
|
2819
2851
|
if key.code == KeyCode::Char('t') || key.code == KeyCode::Char('T') {
|
|
2820
|
-
|
|
2821
|
-
let idx = self
|
|
2822
|
-
.state
|
|
2852
|
+
self.state
|
|
2823
2853
|
.advanced_monitoring
|
|
2824
2854
|
.monitoring_state
|
|
2825
|
-
.
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
.
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
return Ok(false);
|
|
2855
|
+
.select_section(MonitoringSection::Dependencies);
|
|
2856
|
+
self.state
|
|
2857
|
+
.advanced_monitoring
|
|
2858
|
+
.monitoring_state
|
|
2859
|
+
.expand_section(MonitoringSection::Dependencies);
|
|
2860
|
+
let inner_h = self.state.portal_monitoring.viewport_lines.max(5);
|
|
2861
|
+
let sw = self.state.portal_monitoring.summary_clip_width.max(24);
|
|
2862
|
+
self.state.portal_monitoring.scroll_offset = portal_monitoring_section_offset(
|
|
2863
|
+
&self.state,
|
|
2864
|
+
sw,
|
|
2865
|
+
inner_h,
|
|
2866
|
+
MonitoringSection::Dependencies,
|
|
2867
|
+
);
|
|
2868
|
+
if let Some(ws) = ws_client {
|
|
2869
|
+
self.cancel_portal_monitoring_requests();
|
|
2870
|
+
self.begin_dependencies_detail_drill(ws);
|
|
2871
|
+
self.request_immediate_render("portal_deps_drill");
|
|
2872
|
+
} else {
|
|
2873
|
+
self.state.portal_monitoring.error =
|
|
2874
|
+
Some("WebSocket to CLI is not connected.".to_string());
|
|
2875
|
+
self.request_immediate_render("portal_obs_no_ws");
|
|
2847
2876
|
}
|
|
2877
|
+
return Ok(false);
|
|
2848
2878
|
}
|
|
2849
2879
|
|
|
2850
2880
|
// Phase 4: diagnostics for the local CLI/TUI bridge + Gateway reachability.
|
|
2851
2881
|
if key.code == KeyCode::Char('s') || key.code == KeyCode::Char('S') {
|
|
2852
|
-
|
|
2853
|
-
let idx = self
|
|
2854
|
-
.state
|
|
2882
|
+
self.state
|
|
2855
2883
|
.advanced_monitoring
|
|
2856
2884
|
.monitoring_state
|
|
2857
|
-
.
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
.
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
return Ok(false);
|
|
2885
|
+
.select_section(MonitoringSection::System);
|
|
2886
|
+
self.state
|
|
2887
|
+
.advanced_monitoring
|
|
2888
|
+
.monitoring_state
|
|
2889
|
+
.expand_section(MonitoringSection::System);
|
|
2890
|
+
let inner_h = self.state.portal_monitoring.viewport_lines.max(5);
|
|
2891
|
+
let sw = self.state.portal_monitoring.summary_clip_width.max(24);
|
|
2892
|
+
self.state.portal_monitoring.scroll_offset = portal_monitoring_section_offset(
|
|
2893
|
+
&self.state,
|
|
2894
|
+
sw,
|
|
2895
|
+
inner_h,
|
|
2896
|
+
MonitoringSection::System,
|
|
2897
|
+
);
|
|
2898
|
+
if let Some(ws) = ws_client {
|
|
2899
|
+
self.cancel_portal_monitoring_requests();
|
|
2900
|
+
self.begin_system_diagnostics_request(ws);
|
|
2901
|
+
self.request_immediate_render("portal_system_diagnostics");
|
|
2902
|
+
} else {
|
|
2903
|
+
self.state.portal_monitoring.error =
|
|
2904
|
+
Some("WebSocket to CLI is not connected.".to_string());
|
|
2905
|
+
self.request_immediate_render("portal_obs_no_ws");
|
|
2879
2906
|
}
|
|
2907
|
+
return Ok(false);
|
|
2880
2908
|
}
|
|
2881
2909
|
|
|
2882
2910
|
if key.code == KeyCode::Right {
|
|
2883
|
-
|
|
2884
|
-
let idx = self
|
|
2885
|
-
.state
|
|
2911
|
+
self.state
|
|
2886
2912
|
.advanced_monitoring
|
|
2887
2913
|
.monitoring_state
|
|
2888
|
-
.
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
.
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
return Ok(false);
|
|
2914
|
+
.select_section(MonitoringSection::Metrics);
|
|
2915
|
+
self.state
|
|
2916
|
+
.advanced_monitoring
|
|
2917
|
+
.monitoring_state
|
|
2918
|
+
.expand_section(MonitoringSection::Metrics);
|
|
2919
|
+
let inner_h = self.state.portal_monitoring.viewport_lines.max(5);
|
|
2920
|
+
let sw = self.state.portal_monitoring.summary_clip_width.max(24);
|
|
2921
|
+
self.state.portal_monitoring.scroll_offset = portal_monitoring_section_offset(
|
|
2922
|
+
&self.state,
|
|
2923
|
+
sw,
|
|
2924
|
+
inner_h,
|
|
2925
|
+
MonitoringSection::Metrics,
|
|
2926
|
+
);
|
|
2927
|
+
if let Some(ws) = ws_client {
|
|
2928
|
+
self.cancel_portal_monitoring_requests();
|
|
2929
|
+
let panel = self
|
|
2930
|
+
.state
|
|
2931
|
+
.portal_monitoring
|
|
2932
|
+
.metrics_drill
|
|
2933
|
+
.map(|p| p.next())
|
|
2934
|
+
.unwrap_or(MetricsDrillPanel::Http);
|
|
2935
|
+
self.begin_monitoring_drill_request(ws, panel);
|
|
2936
|
+
self.request_immediate_render("portal_metrics_drill");
|
|
2937
|
+
} else {
|
|
2938
|
+
self.state.portal_monitoring.error =
|
|
2939
|
+
Some("WebSocket to CLI is not connected.".to_string());
|
|
2940
|
+
self.request_immediate_render("portal_obs_no_ws");
|
|
2916
2941
|
}
|
|
2942
|
+
return Ok(false);
|
|
2917
2943
|
}
|
|
2918
2944
|
|
|
2919
2945
|
if key.code == KeyCode::Left {
|
|
@@ -2963,20 +2989,46 @@ impl App {
|
|
|
2963
2989
|
|
|
2964
2990
|
match key.code {
|
|
2965
2991
|
KeyCode::Up => {
|
|
2966
|
-
self
|
|
2992
|
+
if self
|
|
2993
|
+
.state
|
|
2967
2994
|
.advanced_monitoring
|
|
2968
2995
|
.monitoring_state
|
|
2969
|
-
.
|
|
2970
|
-
|
|
2971
|
-
|
|
2996
|
+
.selected_section
|
|
2997
|
+
== Some(MonitoringSection::Logs)
|
|
2998
|
+
&& self.state.portal_monitoring.scroll_offset > 0
|
|
2999
|
+
{
|
|
3000
|
+
self.state.portal_monitoring.scroll_offset =
|
|
3001
|
+
self.state.portal_monitoring.scroll_offset.saturating_sub(1);
|
|
3002
|
+
self.request_immediate_render("portal_obs_scroll");
|
|
3003
|
+
} else {
|
|
3004
|
+
self.state
|
|
3005
|
+
.advanced_monitoring
|
|
3006
|
+
.monitoring_state
|
|
3007
|
+
.select_previous_wrapped();
|
|
3008
|
+
self.state.portal_monitoring.scroll_offset = 0;
|
|
3009
|
+
self.request_immediate_render("portal_monitoring_select");
|
|
3010
|
+
}
|
|
2972
3011
|
}
|
|
2973
3012
|
KeyCode::Down => {
|
|
2974
|
-
self
|
|
3013
|
+
if self
|
|
3014
|
+
.state
|
|
2975
3015
|
.advanced_monitoring
|
|
2976
3016
|
.monitoring_state
|
|
2977
|
-
.
|
|
2978
|
-
|
|
2979
|
-
|
|
3017
|
+
.selected_section
|
|
3018
|
+
== Some(MonitoringSection::Logs)
|
|
3019
|
+
&& self.state.portal_monitoring.scroll_offset < max_scroll
|
|
3020
|
+
{
|
|
3021
|
+
self.state.portal_monitoring.scroll_offset =
|
|
3022
|
+
(self.state.portal_monitoring.scroll_offset + 1).min(max_scroll);
|
|
3023
|
+
self.request_immediate_render("portal_obs_scroll");
|
|
3024
|
+
} else {
|
|
3025
|
+
self.state
|
|
3026
|
+
.advanced_monitoring
|
|
3027
|
+
.monitoring_state
|
|
3028
|
+
.select_next_wrapped();
|
|
3029
|
+
self.state.portal_monitoring.scroll_offset = 0;
|
|
3030
|
+
self.request_immediate_render("portal_monitoring_select");
|
|
3031
|
+
}
|
|
2980
3032
|
}
|
|
2981
3033
|
KeyCode::PageUp => {
|
|
2982
3034
|
let step = inner_h.saturating_sub(3).max(1);
|
package/mk3-tui/src/main.rs
CHANGED
|
@@ -890,19 +890,46 @@ fn main() -> Result<()> {
|
|
|
890
890
|
.to_string(),
|
|
891
891
|
);
|
|
892
892
|
}
|
|
893
|
+
let preserve_scroll =
|
|
894
|
+
app.state.portal_monitoring.scroll_offset;
|
|
895
|
+
let preserve_metrics_drill =
|
|
896
|
+
app.state.portal_monitoring.metrics_drill;
|
|
897
|
+
let preserve_metrics_drill_lines = app
|
|
898
|
+
.state
|
|
899
|
+
.portal_monitoring
|
|
900
|
+
.metrics_drill_lines
|
|
901
|
+
.clone();
|
|
902
|
+
let preserve_logs = app
|
|
903
|
+
.state
|
|
904
|
+
.portal_monitoring
|
|
905
|
+
.section_overrides
|
|
906
|
+
.get(&crate::monitoring::MonitoringSection::Logs)
|
|
907
|
+
.cloned();
|
|
893
908
|
app.state.portal_monitoring.last_updated =
|
|
894
909
|
Some(snapshot);
|
|
895
910
|
app.state.portal_monitoring.content_lines = lines;
|
|
896
911
|
app.state.portal_monitoring.section_overrides.clear();
|
|
912
|
+
if let Some(logs) = preserve_logs {
|
|
913
|
+
app.state
|
|
914
|
+
.portal_monitoring
|
|
915
|
+
.section_overrides
|
|
916
|
+
.insert(
|
|
917
|
+
crate::monitoring::MonitoringSection::Logs,
|
|
918
|
+
logs,
|
|
919
|
+
);
|
|
920
|
+
}
|
|
897
921
|
app.state.portal_monitoring.section_refresh_loading =
|
|
898
922
|
None;
|
|
899
923
|
app.state.portal_monitoring.logs_fetch_loading = false;
|
|
900
|
-
app.state.portal_monitoring.metrics_drill =
|
|
901
|
-
|
|
924
|
+
app.state.portal_monitoring.metrics_drill =
|
|
925
|
+
preserve_metrics_drill;
|
|
926
|
+
app.state.portal_monitoring.metrics_drill_lines =
|
|
927
|
+
preserve_metrics_drill_lines;
|
|
902
928
|
app.state.portal_monitoring.metrics_drill_loading =
|
|
903
929
|
false;
|
|
904
930
|
app.state.portal_monitoring.error = None;
|
|
905
|
-
app.state.portal_monitoring.scroll_offset =
|
|
931
|
+
app.state.portal_monitoring.scroll_offset =
|
|
932
|
+
preserve_scroll;
|
|
906
933
|
} else {
|
|
907
934
|
app.state.portal_monitoring.error = Some(
|
|
908
935
|
"Unexpected observability response.".to_string(),
|
|
@@ -1548,6 +1575,8 @@ fn main() -> Result<()> {
|
|
|
1548
1575
|
// Handle ESC on both Press and Release so we never miss it (e.g. when stuck on "Detecting...")
|
|
1549
1576
|
if crossterm::event::poll(Duration::from_millis(1))? {
|
|
1550
1577
|
let on_setup = app.state.navigation.current_screen() == &screens::Screen::SetupPortal;
|
|
1578
|
+
let on_portal_monitoring =
|
|
1579
|
+
app.state.navigation.current_screen() == &screens::Screen::PortalMonitoring;
|
|
1551
1580
|
let on_standalone_input = matches!(
|
|
1552
1581
|
app.state.navigation.current_screen(),
|
|
1553
1582
|
&screens::Screen::ConnectionPortal
|
|
@@ -1571,8 +1600,14 @@ fn main() -> Result<()> {
|
|
|
1571
1600
|
if on_standalone_input {
|
|
1572
1601
|
debug_log::log_input("Mouse", &format!("{:?}", mouse));
|
|
1573
1602
|
}
|
|
1574
|
-
// Handle mouse scroll events for
|
|
1575
|
-
if
|
|
1603
|
+
// Handle mouse scroll events for standalone portals
|
|
1604
|
+
if on_portal_monitoring {
|
|
1605
|
+
match mouse.kind {
|
|
1606
|
+
MouseEventKind::ScrollUp => app.scroll_portal_monitoring_by(-3),
|
|
1607
|
+
MouseEventKind::ScrollDown => app.scroll_portal_monitoring_by(3),
|
|
1608
|
+
_ => {}
|
|
1609
|
+
}
|
|
1610
|
+
} else if on_setup {
|
|
1576
1611
|
if !app.state.setup_portal.detecting {
|
|
1577
1612
|
match mouse.kind {
|
|
1578
1613
|
MouseEventKind::ScrollUp => {
|
|
@@ -294,6 +294,17 @@ impl MonitoringState {
|
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
/// Move keyboard focus directly to a specific section.
|
|
298
|
+
pub fn select_section(&mut self, section: MonitoringSection) {
|
|
299
|
+
if let Some(index) = MonitoringSection::all()
|
|
300
|
+
.iter()
|
|
301
|
+
.position(|candidate| *candidate == section)
|
|
302
|
+
{
|
|
303
|
+
self.selected_index = index;
|
|
304
|
+
self.selected_section = Some(section);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
297
308
|
/// Collapse a specific section
|
|
298
309
|
pub fn collapse_section(&mut self, section: MonitoringSection) {
|
|
299
310
|
if let Some(state) = self.sections.get_mut(§ion) {
|
|
@@ -468,6 +468,27 @@ pub fn portal_monitoring_scroll_max(
|
|
|
468
468
|
total.saturating_sub(content_h.min(total.max(1)))
|
|
469
469
|
}
|
|
470
470
|
|
|
471
|
+
/// Best-effort scroll offset that brings a section header into view.
|
|
472
|
+
pub fn portal_monitoring_section_offset(
|
|
473
|
+
state: &AppState,
|
|
474
|
+
summary_width: usize,
|
|
475
|
+
body_inner_h: usize,
|
|
476
|
+
section: MonitoringSection,
|
|
477
|
+
) -> usize {
|
|
478
|
+
let body_lines = build_body_lines(state, summary_width);
|
|
479
|
+
let target = section.display_name();
|
|
480
|
+
let raw_offset = body_lines
|
|
481
|
+
.iter()
|
|
482
|
+
.position(|line| {
|
|
483
|
+
line.spans
|
|
484
|
+
.iter()
|
|
485
|
+
.any(|span| span.content.as_ref().contains(target))
|
|
486
|
+
})
|
|
487
|
+
.unwrap_or(0);
|
|
488
|
+
let max_scroll = portal_monitoring_scroll_max(state, summary_width, body_inner_h);
|
|
489
|
+
raw_offset.min(max_scroll)
|
|
490
|
+
}
|
|
491
|
+
|
|
471
492
|
/// Build scrollable section lines. **Phase 1:** collapsed summaries and ●/⚠/✖ colors are derived only from
|
|
472
493
|
/// `parse_snapshot` of `content_lines`; `MonitoringState.sections` controls **expand** + **selection** only
|
|
473
494
|
/// (not `SectionState.status` / `summary`).
|
|
@@ -857,7 +878,7 @@ pub fn render(f: &mut Frame, state: &mut AppState) {
|
|
|
857
878
|
]);
|
|
858
879
|
|
|
859
880
|
let keys_hint = Line::from(vec![Span::styled(
|
|
860
|
-
"R
|
|
881
|
+
"R full refresh · L logs · wheel/Pg scroll · H metrics trends · → metrics drill · T dependencies · S system · A live",
|
|
861
882
|
Style::default().fg(TEXT_DIM),
|
|
862
883
|
)]);
|
|
863
884
|
|
|
@@ -962,7 +983,7 @@ pub fn render(f: &mut Frame, state: &mut AppState) {
|
|
|
962
983
|
Span::styled("expand ", Style::default().fg(TEXT_DIM)),
|
|
963
984
|
Span::styled("│ ", Style::default().fg(SEPARATOR_COLOR)),
|
|
964
985
|
Span::styled("R ", Style::default().fg(CYBER_CYAN).bold()),
|
|
965
|
-
Span::styled("
|
|
986
|
+
Span::styled("refresh ", Style::default().fg(TEXT_DIM)),
|
|
966
987
|
Span::styled("│ ", Style::default().fg(SEPARATOR_COLOR)),
|
|
967
988
|
Span::styled("L ", Style::default().fg(CYBER_CYAN).bold()),
|
|
968
989
|
Span::styled("logs ", Style::default().fg(TEXT_DIM)),
|
|
@@ -1011,16 +1032,19 @@ pub fn render(f: &mut Frame, state: &mut AppState) {
|
|
|
1011
1032
|
)]),
|
|
1012
1033
|
Line::from(""),
|
|
1013
1034
|
Line::from("Navigation"),
|
|
1014
|
-
Line::from(
|
|
1035
|
+
Line::from(
|
|
1036
|
+
" ↑/↓ select section Enter/Space expand/collapse PgUp/PgDn/mouse scroll",
|
|
1037
|
+
),
|
|
1038
|
+
Line::from(" When Logs is selected, ↑/↓ scroll log content first."),
|
|
1015
1039
|
Line::from(" ESC close help / return to Main"),
|
|
1016
1040
|
Line::from(""),
|
|
1017
1041
|
Line::from("Actions"),
|
|
1018
|
-
Line::from(" R refresh
|
|
1019
|
-
Line::from(
|
|
1020
|
-
|
|
1021
|
-
),
|
|
1022
|
-
Line::from("
|
|
1023
|
-
Line::from("
|
|
1042
|
+
Line::from(" R full snapshot refresh without closing your current section"),
|
|
1043
|
+
Line::from(" L jump to Logs + fetch Gateway logs"),
|
|
1044
|
+
Line::from(" H jump to Metrics + toggle trends → Metrics drill ← leave drill"),
|
|
1045
|
+
Line::from(" T jump to Dependencies detail S jump to System diagnostics"),
|
|
1046
|
+
Line::from(" A toggle live auto-refresh (off by default for stable reading)"),
|
|
1047
|
+
Line::from(" E export diagnostics JSON to the current working directory"),
|
|
1024
1048
|
Line::from(""),
|
|
1025
1049
|
Line::from("Recovery"),
|
|
1026
1050
|
Line::from(
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "4runr-os",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.44",
|
|
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.44: Stabilizes Portal Monitoring reading mode, global refresh, logs, and shortcut behavior. v2.10.43: Makes Portal Monitoring footer shortcuts jump to their sections and fixes log scrolling.",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"4runr": "dist/index.js",
|