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.
- package/apps/gateway/dist/apps/gateway/src/db/docker-manager.d.ts +1 -0
- package/apps/gateway/dist/apps/gateway/src/db/docker-manager.d.ts.map +1 -1
- package/apps/gateway/dist/apps/gateway/src/db/docker-manager.js +22 -2
- package/apps/gateway/dist/apps/gateway/src/db/docker-manager.js.map +1 -1
- package/apps/gateway/dist/apps/gateway/src/db/init.d.ts.map +1 -1
- package/apps/gateway/dist/apps/gateway/src/db/init.js +8 -0
- package/apps/gateway/dist/apps/gateway/src/db/init.js.map +1 -1
- package/apps/gateway/package-lock.json +13 -13
- package/apps/gateway/src/db/docker-manager.ts +26 -3
- package/apps/gateway/src/db/init.ts +299 -289
- package/apps/gateway/src/health/index.ts +268 -268
- package/dist/boot-sequence.d.ts +24 -0
- package/dist/boot-sequence.d.ts.map +1 -0
- package/dist/boot-sequence.js +188 -0
- package/dist/boot-sequence.js.map +1 -0
- package/dist/index.js +52 -6
- package/dist/index.js.map +1 -1
- package/dist/tui-handlers.js +3 -3
- package/dist/tui-handlers.js.map +1 -1
- package/dist/version-check.d.ts.map +1 -1
- package/dist/version-check.js +71 -25
- package/dist/version-check.js.map +1 -1
- package/dist/watchdog.d.ts +13 -0
- package/dist/watchdog.d.ts.map +1 -0
- package/dist/watchdog.js +192 -0
- package/dist/watchdog.js.map +1 -0
- package/mk3-tui/src/ui/portal_monitoring.rs +259 -259
- package/package.json +2 -2
- package/prisma/schema.prisma +470 -470
- package/scripts/postinstall-gateway.js +63 -63
|
@@ -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.
|
|
3
|
+
"version": "2.10.13",
|
|
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.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",
|