4runr-os 2.10.9 → 2.10.13

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.
@@ -1,259 +1,259 @@
1
- //! Portal Monitoring — live Gateway snapshot from Prometheus `GET /metrics` (via CLI WebSocket).
2
-
3
- use ratatui::layout::{Alignment, Constraint, Direction, Layout};
4
- use ratatui::prelude::*;
5
- use ratatui::widgets::{Block, Borders, Clear, Paragraph, Wrap};
6
-
7
- use crate::app::AppState;
8
-
9
- const BRAND_PURPLE: Color = Color::Rgb(138, 43, 226);
10
- const CYBER_CYAN: Color = Color::Rgb(0, 255, 255);
11
- const NEON_GREEN: Color = Color::Rgb(57, 255, 20);
12
- const TEXT_DIM: Color = Color::Rgb(100, 100, 120);
13
- const TEXT_PRIMARY: Color = Color::Rgb(230, 230, 240);
14
- const TEXT_WARN: Color = Color::Rgb(255, 180, 80);
15
- const TEXT_HEADER: Color = Color::Rgb(0, 200, 255);
16
- const BG_PANEL: Color = Color::Rgb(18, 18, 25);
17
- const SEPARATOR_COLOR: Color = Color::Rgb(60, 60, 80);
18
-
19
- fn gateway_display_url(state: &AppState) -> String {
20
- state
21
- .gateway_url
22
- .as_deref()
23
- .or(state.connection_portal.last_successful_url.as_deref())
24
- .unwrap_or("(not linked)")
25
- .to_string()
26
- }
27
-
28
- /// Full-screen Portal Monitoring (standalone base screen).
29
- pub fn render(f: &mut Frame, state: &mut AppState) {
30
- let area = f.size();
31
- if area.width == 0 || area.height == 0 {
32
- return;
33
- }
34
-
35
- let chunks = Layout::default()
36
- .direction(Direction::Vertical)
37
- .constraints([
38
- Constraint::Length(3),
39
- Constraint::Min(6),
40
- Constraint::Length(3),
41
- ])
42
- .split(area);
43
-
44
- let body_inner_h = chunks[1].height.saturating_sub(2) as usize;
45
- state.portal_monitoring.viewport_lines = body_inner_h.max(5);
46
-
47
- f.render_widget(Clear, area);
48
- f.render_widget(Block::default().style(Style::default().bg(BG_PANEL)), area);
49
-
50
- // Header
51
- let title = Block::default()
52
- .title(" Portal Monitoring ")
53
- .title_style(Style::default().fg(BRAND_PURPLE).bold())
54
- .borders(Borders::ALL)
55
- .border_style(Style::default().fg(CYBER_CYAN))
56
- .style(Style::default().bg(BG_PANEL));
57
- f.render_widget(title, chunks[0]);
58
-
59
- let url = gateway_display_url(state);
60
-
61
- // Build formatted content lines with styling
62
- let mut body_lines: Vec<Line> = vec![];
63
-
64
- // Gateway URL header
65
- body_lines.push(Line::from(vec![
66
- Span::styled("Linked Gateway: ", Style::default().fg(TEXT_DIM)),
67
- Span::styled(url, Style::default().fg(CYBER_CYAN).bold()),
68
- ]));
69
- body_lines.push(Line::from(""));
70
-
71
- if state.portal_monitoring.loading && state.portal_monitoring.content_lines.is_empty() {
72
- body_lines.push(Line::from(Span::styled(
73
- "⏳ Fetching Prometheus /metrics from Gateway…",
74
- Style::default().fg(TEXT_WARN),
75
- )));
76
- } else if let Some(ref err) = state.portal_monitoring.error {
77
- body_lines.push(Line::from(vec![
78
- Span::styled("✗ Error: ", Style::default().fg(Color::Red).bold()),
79
- Span::styled(err, Style::default().fg(TEXT_PRIMARY)),
80
- ]));
81
- body_lines.push(Line::from(""));
82
- body_lines.push(Line::from(Span::styled(
83
- "Press R to retry",
84
- Style::default().fg(TEXT_DIM),
85
- )));
86
- } else if state.portal_monitoring.content_lines.is_empty() {
87
- body_lines.push(Line::from(Span::styled(
88
- "No snapshot yet. Press R to fetch /metrics.",
89
- Style::default().fg(TEXT_DIM),
90
- )));
91
- } else {
92
- // Parse and format content lines with enhanced readability
93
- let mut in_section = false;
94
- let mut section_name = "";
95
-
96
- for raw_line in &state.portal_monitoring.content_lines {
97
- let line_text = raw_line.trim();
98
-
99
- // Detect section headers
100
- if line_text.starts_with("Live link check")
101
- || line_text.starts_with("Dependency checks")
102
- || line_text.starts_with("Prometheus /metrics")
103
- || line_text.starts_with("Top HTTP routes") {
104
- in_section = true;
105
- section_name = line_text;
106
- body_lines.push(Line::from(Span::styled(
107
- format!("━━ {} ━━", line_text),
108
- Style::default().fg(TEXT_HEADER).bold(),
109
- )));
110
- continue;
111
- }
112
-
113
- // Empty lines
114
- if line_text.is_empty() {
115
- body_lines.push(Line::from(""));
116
- in_section = false;
117
- continue;
118
- }
119
-
120
- // Special formatting for different line types
121
- if line_text.starts_with("LIVE:") {
122
- body_lines.push(Line::from(Span::styled(
123
- line_text,
124
- Style::default().fg(NEON_GREEN).bold(),
125
- )));
126
- } else if line_text.starts_with("✓") || line_text.starts_with("/health OK") {
127
- body_lines.push(Line::from(Span::styled(
128
- line_text,
129
- Style::default().fg(NEON_GREEN),
130
- )));
131
- } else if line_text.starts_with("⚠") || line_text.contains("degraded") || line_text.contains("not ready") {
132
- body_lines.push(Line::from(Span::styled(
133
- line_text,
134
- Style::default().fg(TEXT_WARN),
135
- )));
136
- } else if line_text.starts_with(" •") {
137
- // Dependency check lines - color based on status
138
- let color = if line_text.contains(": up") {
139
- NEON_GREEN
140
- } else if line_text.contains(": down") {
141
- Color::Red
142
- } else {
143
- TEXT_WARN
144
- };
145
- body_lines.push(Line::from(Span::styled(
146
- line_text,
147
- Style::default().fg(color),
148
- )));
149
- } else if line_text.starts_with(" ") {
150
- // Indented content (route metrics, etc)
151
- body_lines.push(Line::from(Span::styled(
152
- line_text,
153
- Style::default().fg(TEXT_PRIMARY),
154
- )));
155
- } else if line_text.contains("total") || line_text.contains("latency") || line_text.contains("created") {
156
- // Metrics lines - highlight numbers
157
- let parts: Vec<&str> = line_text.split_whitespace().collect();
158
- if parts.len() >= 2 {
159
- let (label, value) = line_text.split_at(line_text.rfind(char::is_whitespace).unwrap_or(line_text.len()));
160
- body_lines.push(Line::from(vec![
161
- Span::styled(format!("{} ", label.trim()), Style::default().fg(TEXT_DIM)),
162
- Span::styled(value.trim(), Style::default().fg(CYBER_CYAN).bold()),
163
- ]));
164
- } else {
165
- body_lines.push(Line::from(Span::styled(
166
- line_text,
167
- Style::default().fg(TEXT_PRIMARY),
168
- )));
169
- }
170
- } else {
171
- // Default text
172
- body_lines.push(Line::from(Span::styled(
173
- line_text,
174
- Style::default().fg(TEXT_PRIMARY),
175
- )));
176
- }
177
- }
178
- }
179
-
180
- // Calculate scrolling
181
- let total = body_lines.len();
182
- let max_scroll = total.saturating_sub(body_inner_h.min(total.max(1)));
183
- let start = state.portal_monitoring.scroll_offset.min(max_scroll);
184
- let end = (start + body_inner_h).min(total);
185
- let window: Vec<Line> = body_lines[start..end].to_vec();
186
-
187
- let body = Paragraph::new(window)
188
- .wrap(Wrap { trim: true })
189
- .style(Style::default().bg(BG_PANEL))
190
- .block(
191
- Block::default()
192
- .title(vec![
193
- Span::styled(" ", Style::default()),
194
- Span::styled("Traffic & Observability", Style::default().fg(NEON_GREEN).bold()),
195
- Span::styled(" ", Style::default()),
196
- ])
197
- .borders(Borders::ALL)
198
- .border_style(Style::default().fg(SEPARATOR_COLOR))
199
- .style(Style::default().bg(BG_PANEL)),
200
- );
201
- f.render_widget(body, chunks[1]);
202
-
203
- // Footer with better visual hierarchy
204
- let status = if state.portal_monitoring.loading {
205
- vec![
206
- Span::styled("⏳ ", Style::default().fg(TEXT_WARN)),
207
- Span::styled("loading", Style::default().fg(TEXT_WARN)),
208
- ]
209
- } else {
210
- vec![
211
- Span::styled("● ", Style::default().fg(NEON_GREEN)),
212
- Span::styled("live", Style::default().fg(NEON_GREEN)),
213
- ]
214
- };
215
-
216
- // Auto-refresh status
217
- let auto_status = if state.portal_monitoring.auto_refresh_enabled {
218
- Span::styled("auto ✓", Style::default().fg(NEON_GREEN))
219
- } else {
220
- Span::styled("auto ✗", Style::default().fg(TEXT_DIM))
221
- };
222
-
223
- // Last refresh timer
224
- let refresh_info = if let Some(last) = state.portal_monitoring.last_refresh {
225
- let elapsed = last.elapsed().as_secs();
226
- format!(" ~{}s ago", elapsed)
227
- } else {
228
- " never".to_string()
229
- };
230
-
231
- let mut footer_spans = vec![
232
- Span::styled("ESC ", Style::default().fg(BRAND_PURPLE).bold()),
233
- Span::styled("Main ", Style::default().fg(TEXT_DIM)),
234
- Span::styled("│ ", Style::default().fg(SEPARATOR_COLOR)),
235
- Span::styled("R ", Style::default().fg(CYBER_CYAN).bold()),
236
- Span::styled("refresh ", Style::default().fg(TEXT_DIM)),
237
- Span::styled("│ ", Style::default().fg(SEPARATOR_COLOR)),
238
- Span::styled("A ", Style::default().fg(CYBER_CYAN).bold()),
239
- Span::styled("toggle auto ", Style::default().fg(TEXT_DIM)),
240
- Span::styled("│ ", Style::default().fg(SEPARATOR_COLOR)),
241
- Span::styled("↑↓ ", Style::default().fg(CYBER_CYAN).bold()),
242
- Span::styled("scroll ", Style::default().fg(TEXT_DIM)),
243
- Span::styled("│ ", Style::default().fg(SEPARATOR_COLOR)),
244
- ];
245
- footer_spans.extend(status);
246
- footer_spans.push(Span::styled(refresh_info.as_str(), Style::default().fg(TEXT_DIM)));
247
- footer_spans.push(Span::styled(" ", Style::default()));
248
- footer_spans.push(auto_status);
249
-
250
- let footer = Paragraph::new(Line::from(footer_spans))
251
- .alignment(Alignment::Center)
252
- .block(
253
- Block::default()
254
- .borders(Borders::ALL)
255
- .border_style(Style::default().fg(SEPARATOR_COLOR))
256
- .style(Style::default().bg(BG_PANEL)),
257
- );
258
- f.render_widget(footer, chunks[2]);
259
- }
1
+ //! Portal Monitoring — live Gateway snapshot from Prometheus `GET /metrics` (via CLI WebSocket).
2
+
3
+ use ratatui::layout::{Alignment, Constraint, Direction, Layout};
4
+ use ratatui::prelude::*;
5
+ use ratatui::widgets::{Block, Borders, Clear, Paragraph, Wrap};
6
+
7
+ use crate::app::AppState;
8
+
9
+ const BRAND_PURPLE: Color = Color::Rgb(138, 43, 226);
10
+ const CYBER_CYAN: Color = Color::Rgb(0, 255, 255);
11
+ const NEON_GREEN: Color = Color::Rgb(57, 255, 20);
12
+ const TEXT_DIM: Color = Color::Rgb(100, 100, 120);
13
+ const TEXT_PRIMARY: Color = Color::Rgb(230, 230, 240);
14
+ const TEXT_WARN: Color = Color::Rgb(255, 180, 80);
15
+ const TEXT_HEADER: Color = Color::Rgb(0, 200, 255);
16
+ const BG_PANEL: Color = Color::Rgb(18, 18, 25);
17
+ const SEPARATOR_COLOR: Color = Color::Rgb(60, 60, 80);
18
+
19
+ fn gateway_display_url(state: &AppState) -> String {
20
+ state
21
+ .gateway_url
22
+ .as_deref()
23
+ .or(state.connection_portal.last_successful_url.as_deref())
24
+ .unwrap_or("(not linked)")
25
+ .to_string()
26
+ }
27
+
28
+ /// Full-screen Portal Monitoring (standalone base screen).
29
+ pub fn render(f: &mut Frame, state: &mut AppState) {
30
+ let area = f.size();
31
+ if area.width == 0 || area.height == 0 {
32
+ return;
33
+ }
34
+
35
+ let chunks = Layout::default()
36
+ .direction(Direction::Vertical)
37
+ .constraints([
38
+ Constraint::Length(3),
39
+ Constraint::Min(6),
40
+ Constraint::Length(3),
41
+ ])
42
+ .split(area);
43
+
44
+ let body_inner_h = chunks[1].height.saturating_sub(2) as usize;
45
+ state.portal_monitoring.viewport_lines = body_inner_h.max(5);
46
+
47
+ f.render_widget(Clear, area);
48
+ f.render_widget(Block::default().style(Style::default().bg(BG_PANEL)), area);
49
+
50
+ // Header
51
+ let title = Block::default()
52
+ .title(" Portal Monitoring ")
53
+ .title_style(Style::default().fg(BRAND_PURPLE).bold())
54
+ .borders(Borders::ALL)
55
+ .border_style(Style::default().fg(CYBER_CYAN))
56
+ .style(Style::default().bg(BG_PANEL));
57
+ f.render_widget(title, chunks[0]);
58
+
59
+ let url = gateway_display_url(state);
60
+
61
+ // Build formatted content lines with styling
62
+ let mut body_lines: Vec<Line> = vec![];
63
+
64
+ // Gateway URL header
65
+ body_lines.push(Line::from(vec![
66
+ Span::styled("Linked Gateway: ", Style::default().fg(TEXT_DIM)),
67
+ Span::styled(url, Style::default().fg(CYBER_CYAN).bold()),
68
+ ]));
69
+ body_lines.push(Line::from(""));
70
+
71
+ if state.portal_monitoring.loading && state.portal_monitoring.content_lines.is_empty() {
72
+ body_lines.push(Line::from(Span::styled(
73
+ "⏳ Fetching Prometheus /metrics from Gateway…",
74
+ Style::default().fg(TEXT_WARN),
75
+ )));
76
+ } else if let Some(ref err) = state.portal_monitoring.error {
77
+ body_lines.push(Line::from(vec![
78
+ Span::styled("✗ Error: ", Style::default().fg(Color::Red).bold()),
79
+ Span::styled(err, Style::default().fg(TEXT_PRIMARY)),
80
+ ]));
81
+ body_lines.push(Line::from(""));
82
+ body_lines.push(Line::from(Span::styled(
83
+ "Press R to retry",
84
+ Style::default().fg(TEXT_DIM),
85
+ )));
86
+ } else if state.portal_monitoring.content_lines.is_empty() {
87
+ body_lines.push(Line::from(Span::styled(
88
+ "No snapshot yet. Press R to fetch /metrics.",
89
+ Style::default().fg(TEXT_DIM),
90
+ )));
91
+ } else {
92
+ // Parse and format content lines with enhanced readability
93
+ let mut in_section = false;
94
+ let mut section_name = "";
95
+
96
+ for raw_line in &state.portal_monitoring.content_lines {
97
+ let line_text = raw_line.trim();
98
+
99
+ // Detect section headers
100
+ if line_text.starts_with("Live link check")
101
+ || line_text.starts_with("Dependency checks")
102
+ || line_text.starts_with("Prometheus /metrics")
103
+ || line_text.starts_with("Top HTTP routes") {
104
+ in_section = true;
105
+ section_name = line_text;
106
+ body_lines.push(Line::from(Span::styled(
107
+ format!("━━ {} ━━", line_text),
108
+ Style::default().fg(TEXT_HEADER).bold(),
109
+ )));
110
+ continue;
111
+ }
112
+
113
+ // Empty lines
114
+ if line_text.is_empty() {
115
+ body_lines.push(Line::from(""));
116
+ in_section = false;
117
+ continue;
118
+ }
119
+
120
+ // Special formatting for different line types
121
+ if line_text.starts_with("LIVE:") {
122
+ body_lines.push(Line::from(Span::styled(
123
+ line_text,
124
+ Style::default().fg(NEON_GREEN).bold(),
125
+ )));
126
+ } else if line_text.starts_with("✓") || line_text.starts_with("/health OK") {
127
+ body_lines.push(Line::from(Span::styled(
128
+ line_text,
129
+ Style::default().fg(NEON_GREEN),
130
+ )));
131
+ } else if line_text.starts_with("⚠") || line_text.contains("degraded") || line_text.contains("not ready") {
132
+ body_lines.push(Line::from(Span::styled(
133
+ line_text,
134
+ Style::default().fg(TEXT_WARN),
135
+ )));
136
+ } else if line_text.starts_with(" •") {
137
+ // Dependency check lines - color based on status
138
+ let color = if line_text.contains(": up") {
139
+ NEON_GREEN
140
+ } else if line_text.contains(": down") {
141
+ Color::Red
142
+ } else {
143
+ TEXT_WARN
144
+ };
145
+ body_lines.push(Line::from(Span::styled(
146
+ line_text,
147
+ Style::default().fg(color),
148
+ )));
149
+ } else if line_text.starts_with(" ") {
150
+ // Indented content (route metrics, etc)
151
+ body_lines.push(Line::from(Span::styled(
152
+ line_text,
153
+ Style::default().fg(TEXT_PRIMARY),
154
+ )));
155
+ } else if line_text.contains("total") || line_text.contains("latency") || line_text.contains("created") {
156
+ // Metrics lines - highlight numbers
157
+ let parts: Vec<&str> = line_text.split_whitespace().collect();
158
+ if parts.len() >= 2 {
159
+ let (label, value) = line_text.split_at(line_text.rfind(char::is_whitespace).unwrap_or(line_text.len()));
160
+ body_lines.push(Line::from(vec![
161
+ Span::styled(format!("{} ", label.trim()), Style::default().fg(TEXT_DIM)),
162
+ Span::styled(value.trim(), Style::default().fg(CYBER_CYAN).bold()),
163
+ ]));
164
+ } else {
165
+ body_lines.push(Line::from(Span::styled(
166
+ line_text,
167
+ Style::default().fg(TEXT_PRIMARY),
168
+ )));
169
+ }
170
+ } else {
171
+ // Default text
172
+ body_lines.push(Line::from(Span::styled(
173
+ line_text,
174
+ Style::default().fg(TEXT_PRIMARY),
175
+ )));
176
+ }
177
+ }
178
+ }
179
+
180
+ // Calculate scrolling
181
+ let total = body_lines.len();
182
+ let max_scroll = total.saturating_sub(body_inner_h.min(total.max(1)));
183
+ let start = state.portal_monitoring.scroll_offset.min(max_scroll);
184
+ let end = (start + body_inner_h).min(total);
185
+ let window: Vec<Line> = body_lines[start..end].to_vec();
186
+
187
+ let body = Paragraph::new(window)
188
+ .wrap(Wrap { trim: true })
189
+ .style(Style::default().bg(BG_PANEL))
190
+ .block(
191
+ Block::default()
192
+ .title(vec![
193
+ Span::styled(" ", Style::default()),
194
+ Span::styled("Traffic & Observability", Style::default().fg(NEON_GREEN).bold()),
195
+ Span::styled(" ", Style::default()),
196
+ ])
197
+ .borders(Borders::ALL)
198
+ .border_style(Style::default().fg(SEPARATOR_COLOR))
199
+ .style(Style::default().bg(BG_PANEL)),
200
+ );
201
+ f.render_widget(body, chunks[1]);
202
+
203
+ // Footer with better visual hierarchy
204
+ let status = if state.portal_monitoring.loading {
205
+ vec![
206
+ Span::styled("⏳ ", Style::default().fg(TEXT_WARN)),
207
+ Span::styled("loading", Style::default().fg(TEXT_WARN)),
208
+ ]
209
+ } else {
210
+ vec![
211
+ Span::styled("● ", Style::default().fg(NEON_GREEN)),
212
+ Span::styled("live", Style::default().fg(NEON_GREEN)),
213
+ ]
214
+ };
215
+
216
+ // Auto-refresh status
217
+ let auto_status = if state.portal_monitoring.auto_refresh_enabled {
218
+ Span::styled("auto ✓", Style::default().fg(NEON_GREEN))
219
+ } else {
220
+ Span::styled("auto ✗", Style::default().fg(TEXT_DIM))
221
+ };
222
+
223
+ // Last refresh timer
224
+ let refresh_info = if let Some(last) = state.portal_monitoring.last_refresh {
225
+ let elapsed = last.elapsed().as_secs();
226
+ format!(" ~{}s ago", elapsed)
227
+ } else {
228
+ " never".to_string()
229
+ };
230
+
231
+ let mut footer_spans = vec![
232
+ Span::styled("ESC ", Style::default().fg(BRAND_PURPLE).bold()),
233
+ Span::styled("Main ", Style::default().fg(TEXT_DIM)),
234
+ Span::styled("│ ", Style::default().fg(SEPARATOR_COLOR)),
235
+ Span::styled("R ", Style::default().fg(CYBER_CYAN).bold()),
236
+ Span::styled("refresh ", Style::default().fg(TEXT_DIM)),
237
+ Span::styled("│ ", Style::default().fg(SEPARATOR_COLOR)),
238
+ Span::styled("A ", Style::default().fg(CYBER_CYAN).bold()),
239
+ Span::styled("toggle auto ", Style::default().fg(TEXT_DIM)),
240
+ Span::styled("│ ", Style::default().fg(SEPARATOR_COLOR)),
241
+ Span::styled("↑↓ ", Style::default().fg(CYBER_CYAN).bold()),
242
+ Span::styled("scroll ", Style::default().fg(TEXT_DIM)),
243
+ Span::styled("│ ", Style::default().fg(SEPARATOR_COLOR)),
244
+ ];
245
+ footer_spans.extend(status);
246
+ footer_spans.push(Span::styled(refresh_info.as_str(), Style::default().fg(TEXT_DIM)));
247
+ footer_spans.push(Span::styled(" ", Style::default()));
248
+ footer_spans.push(auto_status);
249
+
250
+ let footer = Paragraph::new(Line::from(footer_spans))
251
+ .alignment(Alignment::Center)
252
+ .block(
253
+ Block::default()
254
+ .borders(Borders::ALL)
255
+ .border_style(Style::default().fg(SEPARATOR_COLOR))
256
+ .style(Style::default().bg(BG_PANEL)),
257
+ );
258
+ f.render_widget(footer, chunks[2]);
259
+ }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "4runr-os",
3
- "version": "2.10.9",
3
+ "version": "2.10.13",
4
4
  "type": "module",
5
- "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.10.9: EBUSY fix no parallel `npm -g` while 4r deferred update runs (lock + preinstall guard); longer delay before background npm. v2.10.8: Prisma/PORT. See docs/4RUNR-DB-IMPLEMENTATION-PLAN.md, docs/MONITORING-IMPROVEMENTS-V1.md",
5
+ "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.10.13: Watchdog process stops Docker on ANY exit (crash, force-kill, terminal close). v2.10.12: Graceful shutdown. See docs",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
8
  "4runr": "dist/index.js",