4runr-os 2.10.39 → 2.10.41
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/index.js +14 -4
- package/apps/gateway/dist/apps/gateway/src/index.js.map +1 -1
- package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.d.ts +18 -0
- package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.d.ts.map +1 -0
- package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.js +117 -0
- package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.js.map +1 -0
- package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.d.ts +2 -0
- package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.d.ts.map +1 -0
- package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.js +54 -0
- package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.js.map +1 -0
- package/apps/gateway/dist/apps/gateway/src/routes/monitoring.d.ts +15 -0
- package/apps/gateway/dist/apps/gateway/src/routes/monitoring.d.ts.map +1 -0
- package/apps/gateway/dist/apps/gateway/src/routes/monitoring.js +164 -0
- package/apps/gateway/dist/apps/gateway/src/routes/monitoring.js.map +1 -0
- package/apps/gateway/package-lock.json +204 -353
- package/apps/gateway/src/index.ts +27 -8
- package/apps/gateway/src/metrics/monitoring-detail.ts +162 -0
- package/apps/gateway/src/middleware/log-capture.ts +70 -0
- package/apps/gateway/src/routes/monitoring.ts +298 -0
- package/dist/gateway-client.d.ts +2 -0
- package/dist/gateway-client.d.ts.map +1 -1
- package/dist/gateway-client.js +22 -0
- package/dist/gateway-client.js.map +1 -1
- package/dist/tui-handlers.js +498 -0
- package/dist/tui-handlers.js.map +1 -1
- package/mk3-tui/src/app/render_scheduler.rs +111 -112
- package/mk3-tui/src/app.rs +1078 -295
- package/mk3-tui/src/debug_log.rs +131 -124
- package/mk3-tui/src/io/mod.rs +63 -66
- package/mk3-tui/src/io/protocol.rs +14 -15
- package/mk3-tui/src/io/stdio.rs +31 -32
- package/mk3-tui/src/io/ws.rs +25 -32
- package/mk3-tui/src/main.rs +774 -212
- package/mk3-tui/src/monitoring/mod.rs +428 -0
- package/mk3-tui/src/screens/mod.rs +53 -39
- package/mk3-tui/src/storage/cache.rs +221 -224
- package/mk3-tui/src/storage/mod.rs +5 -6
- package/mk3-tui/src/ui/agent_builder.rs +1148 -922
- package/mk3-tui/src/ui/agent_list.rs +344 -295
- package/mk3-tui/src/ui/boot.rs +145 -148
- package/mk3-tui/src/ui/connection_portal.rs +121 -98
- package/mk3-tui/src/ui/help.rs +340 -284
- package/mk3-tui/src/ui/layout.rs +966 -803
- package/mk3-tui/src/ui/mod.rs +1 -1
- package/mk3-tui/src/ui/portal_monitoring.rs +1027 -147
- package/mk3-tui/src/ui/run_manager.rs +784 -764
- package/mk3-tui/src/ui/safe_viewport.rs +236 -235
- package/mk3-tui/src/ui/settings.rs +414 -362
- package/mk3-tui/src/ui/setup_portal.rs +158 -101
- package/mk3-tui/src/websocket.rs +315 -308
- package/package.json +2 -2
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
// Phase 0: Portal Monitoring State Management
|
|
2
|
+
// Structures for section-based, interactive monitoring
|
|
3
|
+
|
|
4
|
+
use serde::{Deserialize, Serialize};
|
|
5
|
+
use std::collections::HashMap;
|
|
6
|
+
use std::time::{Duration, Instant};
|
|
7
|
+
|
|
8
|
+
/// Main sections of the monitoring view
|
|
9
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
10
|
+
pub enum MonitoringSection {
|
|
11
|
+
Overview,
|
|
12
|
+
Health,
|
|
13
|
+
Dependencies,
|
|
14
|
+
Metrics,
|
|
15
|
+
Queue,
|
|
16
|
+
Logs,
|
|
17
|
+
System,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
impl MonitoringSection {
|
|
21
|
+
pub fn all() -> Vec<Self> {
|
|
22
|
+
vec![
|
|
23
|
+
Self::Overview,
|
|
24
|
+
Self::Health,
|
|
25
|
+
Self::Dependencies,
|
|
26
|
+
Self::Metrics,
|
|
27
|
+
Self::Queue,
|
|
28
|
+
Self::Logs,
|
|
29
|
+
Self::System,
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
pub fn display_name(&self) -> &'static str {
|
|
34
|
+
match self {
|
|
35
|
+
Self::Overview => "Overview",
|
|
36
|
+
Self::Health => "Health",
|
|
37
|
+
Self::Dependencies => "Dependencies",
|
|
38
|
+
Self::Metrics => "Metrics",
|
|
39
|
+
Self::Queue => "Queue",
|
|
40
|
+
Self::Logs => "Logs",
|
|
41
|
+
Self::System => "System",
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Lowercase slug sent on `monitoring.refresh` (`section` field).
|
|
46
|
+
pub fn cli_slug(&self) -> &'static str {
|
|
47
|
+
match self {
|
|
48
|
+
Self::Overview => "overview",
|
|
49
|
+
Self::Health => "health",
|
|
50
|
+
Self::Dependencies => "dependencies",
|
|
51
|
+
Self::Metrics => "metrics",
|
|
52
|
+
Self::Queue => "queue",
|
|
53
|
+
Self::Logs => "logs",
|
|
54
|
+
Self::System => "system",
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
pub fn from_cli_slug(s: &str) -> Option<Self> {
|
|
59
|
+
let t = s.trim().to_lowercase();
|
|
60
|
+
match t.as_str() {
|
|
61
|
+
"overview" => Some(Self::Overview),
|
|
62
|
+
"health" => Some(Self::Health),
|
|
63
|
+
"dependencies" => Some(Self::Dependencies),
|
|
64
|
+
"metrics" => Some(Self::Metrics),
|
|
65
|
+
"queue" => Some(Self::Queue),
|
|
66
|
+
"logs" => Some(Self::Logs),
|
|
67
|
+
"system" => Some(Self::System),
|
|
68
|
+
_ => None,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Phase 3: Metrics section drill targets (`monitoring.drill` `target` field).
|
|
74
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
75
|
+
pub enum MetricsDrillPanel {
|
|
76
|
+
Http,
|
|
77
|
+
Runs,
|
|
78
|
+
Queue,
|
|
79
|
+
Sse,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
impl MetricsDrillPanel {
|
|
83
|
+
pub fn next(self) -> Self {
|
|
84
|
+
match self {
|
|
85
|
+
Self::Http => Self::Runs,
|
|
86
|
+
Self::Runs => Self::Queue,
|
|
87
|
+
Self::Queue => Self::Sse,
|
|
88
|
+
Self::Sse => Self::Http,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
pub fn cli_target(&self) -> &'static str {
|
|
93
|
+
match self {
|
|
94
|
+
Self::Http => "metrics_http",
|
|
95
|
+
Self::Runs => "metrics_runs",
|
|
96
|
+
Self::Queue => "metrics_queue",
|
|
97
|
+
Self::Sse => "metrics_sse",
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
pub fn label(&self) -> &'static str {
|
|
102
|
+
match self {
|
|
103
|
+
Self::Http => "HTTP",
|
|
104
|
+
Self::Runs => "Runs",
|
|
105
|
+
Self::Queue => "Queue",
|
|
106
|
+
Self::Sse => "SSE",
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
pub fn from_cli_target(s: &str) -> Option<Self> {
|
|
111
|
+
match s.trim() {
|
|
112
|
+
"metrics_http" => Some(Self::Http),
|
|
113
|
+
"metrics_runs" => Some(Self::Runs),
|
|
114
|
+
"metrics_queue" => Some(Self::Queue),
|
|
115
|
+
"metrics_sse" => Some(Self::Sse),
|
|
116
|
+
_ => None,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Status indicator for sections (determines color)
|
|
122
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
123
|
+
pub enum SectionStatus {
|
|
124
|
+
Healthy, // Green
|
|
125
|
+
Degraded, // Yellow
|
|
126
|
+
Unhealthy, // Red
|
|
127
|
+
Unknown, // Gray
|
|
128
|
+
Loading, // Blue/Cyan
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
impl SectionStatus {
|
|
132
|
+
pub fn symbol(&self) -> &'static str {
|
|
133
|
+
match self {
|
|
134
|
+
Self::Healthy => "●",
|
|
135
|
+
Self::Degraded => "⚠",
|
|
136
|
+
Self::Unhealthy => "✖",
|
|
137
|
+
Self::Unknown => "○",
|
|
138
|
+
Self::Loading => "◉",
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// State for each individual section.
|
|
144
|
+
///
|
|
145
|
+
/// **Phase 1 Portal Monitoring:** only `expanded` is authoritative for the UI. Status/summary strings
|
|
146
|
+
/// here are not driven by `portal_monitoring` render (that path recomputes from the CLI snapshot each
|
|
147
|
+
/// frame). Future phases may use `update_section` to populate these; until then do not assume headers
|
|
148
|
+
/// reflect `SectionState::status` / `summary`.
|
|
149
|
+
#[derive(Debug, Clone)]
|
|
150
|
+
pub struct SectionState {
|
|
151
|
+
/// Whether section is expanded (shows detail) or collapsed (shows summary)
|
|
152
|
+
pub expanded: bool,
|
|
153
|
+
/// Current status (for color coding header)
|
|
154
|
+
pub status: SectionStatus,
|
|
155
|
+
/// One-line summary shown when collapsed
|
|
156
|
+
pub summary: String,
|
|
157
|
+
/// When this section was last refreshed
|
|
158
|
+
pub last_refresh: Option<Instant>,
|
|
159
|
+
/// Whether section is currently loading new data
|
|
160
|
+
pub loading: bool,
|
|
161
|
+
/// Error message if last refresh failed
|
|
162
|
+
pub error: Option<String>,
|
|
163
|
+
/// Arbitrary data payload (parsed from Gateway response)
|
|
164
|
+
pub data: Option<serde_json::Value>,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
impl Default for SectionState {
|
|
168
|
+
fn default() -> Self {
|
|
169
|
+
Self {
|
|
170
|
+
expanded: false,
|
|
171
|
+
status: SectionStatus::Unknown,
|
|
172
|
+
summary: String::new(),
|
|
173
|
+
last_refresh: None,
|
|
174
|
+
loading: false,
|
|
175
|
+
error: None,
|
|
176
|
+
data: None,
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// Main monitoring state (Phase 0 foundation).
|
|
182
|
+
///
|
|
183
|
+
/// **Phase 1:** `selected_index` / `selected_section` and `sections[..].expanded` drive the TUI;
|
|
184
|
+
/// per-section `SectionState::status` / `summary` are not used for drawing (see `build_body_lines` in
|
|
185
|
+
/// `ui/portal_monitoring.rs` — status and one-line summaries come from `parse_snapshot` only).
|
|
186
|
+
#[derive(Debug, Clone)]
|
|
187
|
+
pub struct MonitoringState {
|
|
188
|
+
/// Currently selected section (for keyboard navigation)
|
|
189
|
+
pub selected_section: Option<MonitoringSection>,
|
|
190
|
+
/// Index in the section list (0 = Overview, 1 = Health, etc.)
|
|
191
|
+
pub selected_index: usize,
|
|
192
|
+
/// Per-section state
|
|
193
|
+
pub sections: HashMap<MonitoringSection, SectionState>,
|
|
194
|
+
/// Auto-refresh enabled/disabled
|
|
195
|
+
pub live_mode: bool,
|
|
196
|
+
/// Auto-refresh interval (default 5s)
|
|
197
|
+
pub refresh_interval: Duration,
|
|
198
|
+
/// When auto-refresh last triggered
|
|
199
|
+
pub last_auto_refresh: Option<Instant>,
|
|
200
|
+
/// Gateway URL we're monitoring
|
|
201
|
+
pub gateway_url: Option<String>,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
impl Default for MonitoringState {
|
|
205
|
+
fn default() -> Self {
|
|
206
|
+
let mut sections = HashMap::new();
|
|
207
|
+
for section in MonitoringSection::all() {
|
|
208
|
+
sections.insert(section, SectionState::default());
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
Self {
|
|
212
|
+
selected_section: Some(MonitoringSection::Overview),
|
|
213
|
+
selected_index: 0,
|
|
214
|
+
sections,
|
|
215
|
+
live_mode: false, // Start with auto-refresh off
|
|
216
|
+
refresh_interval: Duration::from_secs(5),
|
|
217
|
+
last_auto_refresh: None,
|
|
218
|
+
gateway_url: None,
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
impl MonitoringState {
|
|
224
|
+
pub fn new(gateway_url: Option<String>) -> Self {
|
|
225
|
+
let mut state = Self::default();
|
|
226
|
+
state.gateway_url = gateway_url;
|
|
227
|
+
state
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// Get the currently selected section
|
|
231
|
+
pub fn current_section(&self) -> Option<MonitoringSection> {
|
|
232
|
+
self.selected_section
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/// Move selection up (previous section)
|
|
236
|
+
pub fn select_previous(&mut self) {
|
|
237
|
+
if self.selected_index > 0 {
|
|
238
|
+
self.selected_index -= 1;
|
|
239
|
+
self.selected_section = Some(MonitoringSection::all()[self.selected_index]);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/// Move selection down (next section)
|
|
244
|
+
pub fn select_next(&mut self) {
|
|
245
|
+
let sections = MonitoringSection::all();
|
|
246
|
+
if self.selected_index < sections.len() - 1 {
|
|
247
|
+
self.selected_index += 1;
|
|
248
|
+
self.selected_section = Some(sections[self.selected_index]);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/// Toggle expand/collapse for currently selected section
|
|
253
|
+
pub fn toggle_current_section(&mut self) {
|
|
254
|
+
if let Some(section) = self.selected_section {
|
|
255
|
+
if let Some(state) = self.sections.get_mut(§ion) {
|
|
256
|
+
state.expanded = !state.expanded;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/// Toggle expand/collapse for the section at `index` (Phase 1 Portal Monitoring).
|
|
262
|
+
pub fn toggle_section_at_index(&mut self, index: usize) {
|
|
263
|
+
let sections = MonitoringSection::all();
|
|
264
|
+
if let Some(§ion) = sections.get(index) {
|
|
265
|
+
if let Some(state) = self.sections.get_mut(§ion) {
|
|
266
|
+
state.expanded = !state.expanded;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/// Move selection with wrap (Phase 1).
|
|
272
|
+
pub fn select_previous_wrapped(&mut self) {
|
|
273
|
+
let n = MonitoringSection::all().len();
|
|
274
|
+
if n == 0 {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
self.selected_index = (self.selected_index + n - 1) % n;
|
|
278
|
+
self.selected_section = MonitoringSection::all().get(self.selected_index).copied();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
pub fn select_next_wrapped(&mut self) {
|
|
282
|
+
let n = MonitoringSection::all().len();
|
|
283
|
+
if n == 0 {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
self.selected_index = (self.selected_index + 1) % n;
|
|
287
|
+
self.selected_section = MonitoringSection::all().get(self.selected_index).copied();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/// Expand a specific section
|
|
291
|
+
pub fn expand_section(&mut self, section: MonitoringSection) {
|
|
292
|
+
if let Some(state) = self.sections.get_mut(§ion) {
|
|
293
|
+
state.expanded = true;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/// Collapse a specific section
|
|
298
|
+
pub fn collapse_section(&mut self, section: MonitoringSection) {
|
|
299
|
+
if let Some(state) = self.sections.get_mut(§ion) {
|
|
300
|
+
state.expanded = false;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/// Collapse all sections
|
|
305
|
+
pub fn collapse_all(&mut self) {
|
|
306
|
+
for state in self.sections.values_mut() {
|
|
307
|
+
state.expanded = false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/// Toggle auto-refresh (live mode)
|
|
312
|
+
pub fn toggle_live_mode(&mut self) {
|
|
313
|
+
self.live_mode = !self.live_mode;
|
|
314
|
+
if self.live_mode {
|
|
315
|
+
self.last_auto_refresh = Some(Instant::now());
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/// Check if it's time for auto-refresh
|
|
320
|
+
pub fn should_auto_refresh(&self) -> bool {
|
|
321
|
+
if !self.live_mode {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
match self.last_auto_refresh {
|
|
325
|
+
Some(last) => last.elapsed() >= self.refresh_interval,
|
|
326
|
+
None => true,
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/// Mark that auto-refresh just happened
|
|
331
|
+
pub fn mark_auto_refresh(&mut self) {
|
|
332
|
+
self.last_auto_refresh = Some(Instant::now());
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/// Set loading state for a section
|
|
336
|
+
pub fn set_loading(&mut self, section: MonitoringSection, loading: bool) {
|
|
337
|
+
if let Some(state) = self.sections.get_mut(§ion) {
|
|
338
|
+
state.loading = loading;
|
|
339
|
+
if loading {
|
|
340
|
+
state.status = SectionStatus::Loading;
|
|
341
|
+
state.error = None;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/// Update section data after successful refresh
|
|
347
|
+
pub fn update_section(
|
|
348
|
+
&mut self,
|
|
349
|
+
section: MonitoringSection,
|
|
350
|
+
status: SectionStatus,
|
|
351
|
+
summary: String,
|
|
352
|
+
data: Option<serde_json::Value>,
|
|
353
|
+
) {
|
|
354
|
+
if let Some(state) = self.sections.get_mut(§ion) {
|
|
355
|
+
state.status = status;
|
|
356
|
+
state.summary = summary;
|
|
357
|
+
state.data = data;
|
|
358
|
+
state.last_refresh = Some(Instant::now());
|
|
359
|
+
state.loading = false;
|
|
360
|
+
state.error = None;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/// Set error state for a section
|
|
365
|
+
pub fn set_error(&mut self, section: MonitoringSection, error: String) {
|
|
366
|
+
if let Some(state) = self.sections.get_mut(§ion) {
|
|
367
|
+
state.status = SectionStatus::Unhealthy;
|
|
368
|
+
state.error = Some(error);
|
|
369
|
+
state.loading = false;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/// Get section state (immutable)
|
|
374
|
+
pub fn get_section(&self, section: MonitoringSection) -> Option<&SectionState> {
|
|
375
|
+
self.sections.get(§ion)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/// Get time since last refresh for a section (for display)
|
|
379
|
+
pub fn time_since_refresh(&self, section: MonitoringSection) -> Option<Duration> {
|
|
380
|
+
self.sections
|
|
381
|
+
.get(§ion)
|
|
382
|
+
.and_then(|s| s.last_refresh)
|
|
383
|
+
.map(|t| t.elapsed())
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
#[cfg(test)]
|
|
388
|
+
mod tests {
|
|
389
|
+
use super::*;
|
|
390
|
+
|
|
391
|
+
#[test]
|
|
392
|
+
fn test_section_navigation() {
|
|
393
|
+
let mut state = MonitoringState::default();
|
|
394
|
+
assert_eq!(state.selected_index, 0);
|
|
395
|
+
assert_eq!(state.current_section(), Some(MonitoringSection::Overview));
|
|
396
|
+
|
|
397
|
+
state.select_next();
|
|
398
|
+
assert_eq!(state.selected_index, 1);
|
|
399
|
+
assert_eq!(state.current_section(), Some(MonitoringSection::Health));
|
|
400
|
+
|
|
401
|
+
state.select_previous();
|
|
402
|
+
assert_eq!(state.selected_index, 0);
|
|
403
|
+
assert_eq!(state.current_section(), Some(MonitoringSection::Overview));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
#[test]
|
|
407
|
+
fn test_toggle_expansion() {
|
|
408
|
+
let mut state = MonitoringState::default();
|
|
409
|
+
state.selected_section = Some(MonitoringSection::Health);
|
|
410
|
+
|
|
411
|
+
let health = state.get_section(MonitoringSection::Health).unwrap();
|
|
412
|
+
assert!(!health.expanded);
|
|
413
|
+
|
|
414
|
+
state.toggle_current_section();
|
|
415
|
+
let health = state.get_section(MonitoringSection::Health).unwrap();
|
|
416
|
+
assert!(health.expanded);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
#[test]
|
|
420
|
+
fn test_live_mode() {
|
|
421
|
+
let mut state = MonitoringState::default();
|
|
422
|
+
assert!(!state.live_mode);
|
|
423
|
+
|
|
424
|
+
state.toggle_live_mode();
|
|
425
|
+
assert!(state.live_mode);
|
|
426
|
+
assert!(state.last_auto_refresh.is_some());
|
|
427
|
+
}
|
|
428
|
+
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/// Screen types and navigation system
|
|
2
|
-
///
|
|
2
|
+
///
|
|
3
3
|
/// This module defines the screen/mode system for the TUI.
|
|
4
4
|
/// Screens can be:
|
|
5
5
|
/// - Base screens (Boot, Main) - always visible
|
|
6
6
|
/// - Overlay screens (Agent Builder, Run Manager, Settings) - full-screen modals
|
|
7
7
|
/// - Popup screens (Confirmations, Alerts) - small overlays
|
|
8
|
-
|
|
9
8
|
use serde::{Deserialize, Serialize};
|
|
10
9
|
|
|
11
10
|
/// Screen enum - represents all possible screens in the TUI
|
|
@@ -14,7 +13,7 @@ pub enum Screen {
|
|
|
14
13
|
// Base screens
|
|
15
14
|
Boot,
|
|
16
15
|
Main,
|
|
17
|
-
|
|
16
|
+
|
|
18
17
|
// Overlay screens (full-screen modals)
|
|
19
18
|
AgentBuilder,
|
|
20
19
|
RunManager,
|
|
@@ -24,10 +23,15 @@ pub enum Screen {
|
|
|
24
23
|
SetupPortal,
|
|
25
24
|
/// Gateway traffic / server-style observability (scaffold; live stream TBD)
|
|
26
25
|
PortalMonitoring,
|
|
27
|
-
|
|
26
|
+
|
|
28
27
|
// Popup screens (small overlays)
|
|
29
|
-
Confirmation {
|
|
30
|
-
|
|
28
|
+
Confirmation {
|
|
29
|
+
message: String,
|
|
30
|
+
action: String,
|
|
31
|
+
},
|
|
32
|
+
Alert {
|
|
33
|
+
message: String,
|
|
34
|
+
},
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
impl Screen {
|
|
@@ -38,23 +42,24 @@ impl Screen {
|
|
|
38
42
|
Screen::AgentBuilder | Screen::RunManager | Screen::Settings | Screen::AgentList
|
|
39
43
|
)
|
|
40
44
|
}
|
|
41
|
-
|
|
45
|
+
|
|
42
46
|
/// Returns true if this screen is a popup (small overlay)
|
|
43
47
|
pub fn is_popup(&self) -> bool {
|
|
44
|
-
matches!(
|
|
45
|
-
self,
|
|
46
|
-
Screen::Confirmation { .. } | Screen::Alert { .. }
|
|
47
|
-
)
|
|
48
|
+
matches!(self, Screen::Confirmation { .. } | Screen::Alert { .. })
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
+
|
|
50
51
|
/// Returns true if this screen is a base screen (full-screen standalone)
|
|
51
52
|
pub fn is_base(&self) -> bool {
|
|
52
53
|
matches!(
|
|
53
54
|
self,
|
|
54
|
-
Screen::Boot
|
|
55
|
+
Screen::Boot
|
|
56
|
+
| Screen::Main
|
|
57
|
+
| Screen::ConnectionPortal
|
|
58
|
+
| Screen::SetupPortal
|
|
59
|
+
| Screen::PortalMonitoring
|
|
55
60
|
)
|
|
56
61
|
}
|
|
57
|
-
|
|
62
|
+
|
|
58
63
|
/// Returns the screen name for display
|
|
59
64
|
#[allow(dead_code)]
|
|
60
65
|
pub fn name(&self) -> &str {
|
|
@@ -79,11 +84,11 @@ impl Screen {
|
|
|
79
84
|
pub struct NavigationState {
|
|
80
85
|
/// Current base screen (Boot or Main)
|
|
81
86
|
pub base_screen: Screen,
|
|
82
|
-
|
|
87
|
+
|
|
83
88
|
/// Overlay stack (full-screen modals)
|
|
84
89
|
/// Last item is the currently visible overlay
|
|
85
90
|
pub overlay_stack: Vec<Screen>,
|
|
86
|
-
|
|
91
|
+
|
|
87
92
|
/// Popup stack (small overlays on top of everything)
|
|
88
93
|
/// Last item is the currently visible popup
|
|
89
94
|
pub popup_stack: Vec<Screen>,
|
|
@@ -104,7 +109,7 @@ impl NavigationState {
|
|
|
104
109
|
pub fn new() -> Self {
|
|
105
110
|
Self::default()
|
|
106
111
|
}
|
|
107
|
-
|
|
112
|
+
|
|
108
113
|
/// Get the currently visible screen (topmost in hierarchy)
|
|
109
114
|
pub fn current_screen(&self) -> &Screen {
|
|
110
115
|
// Priority: Popup > Overlay > Base
|
|
@@ -116,7 +121,7 @@ impl NavigationState {
|
|
|
116
121
|
&self.base_screen
|
|
117
122
|
}
|
|
118
123
|
}
|
|
119
|
-
|
|
124
|
+
|
|
120
125
|
/// Navigate to a base screen (Boot or Main)
|
|
121
126
|
pub fn navigate_to_base(&mut self, screen: Screen) {
|
|
122
127
|
if screen.is_base() {
|
|
@@ -126,49 +131,49 @@ impl NavigationState {
|
|
|
126
131
|
self.popup_stack.clear();
|
|
127
132
|
}
|
|
128
133
|
}
|
|
129
|
-
|
|
134
|
+
|
|
130
135
|
/// Push an overlay screen (Agent Builder, Run Manager, Settings)
|
|
131
136
|
pub fn push_overlay(&mut self, screen: Screen) {
|
|
132
137
|
if screen.is_overlay() {
|
|
133
138
|
self.overlay_stack.push(screen);
|
|
134
139
|
}
|
|
135
140
|
}
|
|
136
|
-
|
|
141
|
+
|
|
137
142
|
/// Pop the current overlay and return to previous screen
|
|
138
143
|
pub fn pop_overlay(&mut self) -> Option<Screen> {
|
|
139
144
|
self.overlay_stack.pop()
|
|
140
145
|
}
|
|
141
|
-
|
|
146
|
+
|
|
142
147
|
/// Push a popup screen (Confirmation, Alert, Help)
|
|
143
148
|
pub fn push_popup(&mut self, screen: Screen) {
|
|
144
149
|
if screen.is_popup() {
|
|
145
150
|
self.popup_stack.push(screen);
|
|
146
151
|
}
|
|
147
152
|
}
|
|
148
|
-
|
|
153
|
+
|
|
149
154
|
/// Pop the current popup
|
|
150
155
|
pub fn pop_popup(&mut self) -> Option<Screen> {
|
|
151
156
|
self.popup_stack.pop()
|
|
152
157
|
}
|
|
153
|
-
|
|
158
|
+
|
|
154
159
|
/// Close all overlays and popups (return to base screen)
|
|
155
160
|
#[allow(dead_code)]
|
|
156
161
|
pub fn close_all(&mut self) {
|
|
157
162
|
self.overlay_stack.clear();
|
|
158
163
|
self.popup_stack.clear();
|
|
159
164
|
}
|
|
160
|
-
|
|
165
|
+
|
|
161
166
|
/// Check if we're currently on the base screen (no overlays/popups)
|
|
162
167
|
#[allow(dead_code)]
|
|
163
168
|
pub fn is_on_base(&self) -> bool {
|
|
164
169
|
self.overlay_stack.is_empty() && self.popup_stack.is_empty()
|
|
165
170
|
}
|
|
166
|
-
|
|
171
|
+
|
|
167
172
|
/// Check if we have any overlays open
|
|
168
173
|
pub fn has_overlay(&self) -> bool {
|
|
169
174
|
!self.overlay_stack.is_empty()
|
|
170
175
|
}
|
|
171
|
-
|
|
176
|
+
|
|
172
177
|
/// Check if we have any popups open
|
|
173
178
|
pub fn has_popup(&self) -> bool {
|
|
174
179
|
!self.popup_stack.is_empty()
|
|
@@ -178,7 +183,7 @@ impl NavigationState {
|
|
|
178
183
|
#[cfg(test)]
|
|
179
184
|
mod tests {
|
|
180
185
|
use super::*;
|
|
181
|
-
|
|
186
|
+
|
|
182
187
|
#[test]
|
|
183
188
|
fn test_screen_classification() {
|
|
184
189
|
assert!(Screen::Boot.is_base());
|
|
@@ -188,47 +193,56 @@ mod tests {
|
|
|
188
193
|
assert!(Screen::Settings.is_overlay());
|
|
189
194
|
assert!(Screen::Help.is_popup());
|
|
190
195
|
}
|
|
191
|
-
|
|
196
|
+
|
|
192
197
|
#[test]
|
|
193
198
|
fn test_navigation_state() {
|
|
194
199
|
let mut nav = NavigationState::new();
|
|
195
|
-
|
|
200
|
+
|
|
196
201
|
// Start at Boot
|
|
197
202
|
assert_eq!(nav.current_screen(), &Screen::Boot);
|
|
198
203
|
assert!(nav.is_on_base());
|
|
199
|
-
|
|
204
|
+
|
|
200
205
|
// Navigate to Main
|
|
201
206
|
nav.navigate_to_base(Screen::Main);
|
|
202
207
|
assert_eq!(nav.current_screen(), &Screen::Main);
|
|
203
|
-
|
|
208
|
+
|
|
204
209
|
// Push overlay
|
|
205
210
|
nav.push_overlay(Screen::AgentBuilder);
|
|
206
211
|
assert_eq!(nav.current_screen(), &Screen::AgentBuilder);
|
|
207
212
|
assert!(nav.has_overlay());
|
|
208
213
|
assert!(!nav.is_on_base());
|
|
209
|
-
|
|
214
|
+
|
|
210
215
|
// Push popup
|
|
211
|
-
nav.push_popup(Screen::Alert {
|
|
212
|
-
|
|
216
|
+
nav.push_popup(Screen::Alert {
|
|
217
|
+
message: "Test".to_string(),
|
|
218
|
+
});
|
|
219
|
+
assert_eq!(
|
|
220
|
+
nav.current_screen(),
|
|
221
|
+
&Screen::Alert {
|
|
222
|
+
message: "Test".to_string()
|
|
223
|
+
}
|
|
224
|
+
);
|
|
213
225
|
assert!(nav.has_popup());
|
|
214
|
-
|
|
226
|
+
|
|
215
227
|
// Pop popup
|
|
216
228
|
nav.pop_popup();
|
|
217
229
|
assert_eq!(nav.current_screen(), &Screen::AgentBuilder);
|
|
218
|
-
|
|
230
|
+
|
|
219
231
|
// Pop overlay
|
|
220
232
|
nav.pop_overlay();
|
|
221
233
|
assert_eq!(nav.current_screen(), &Screen::Main);
|
|
222
234
|
assert!(nav.is_on_base());
|
|
223
235
|
}
|
|
224
|
-
|
|
236
|
+
|
|
225
237
|
#[test]
|
|
226
238
|
fn test_close_all() {
|
|
227
239
|
let mut nav = NavigationState::new();
|
|
228
240
|
nav.navigate_to_base(Screen::Main);
|
|
229
241
|
nav.push_overlay(Screen::AgentBuilder);
|
|
230
|
-
nav.push_popup(Screen::Alert {
|
|
231
|
-
|
|
242
|
+
nav.push_popup(Screen::Alert {
|
|
243
|
+
message: "Test".to_string(),
|
|
244
|
+
});
|
|
245
|
+
|
|
232
246
|
nav.close_all();
|
|
233
247
|
assert!(nav.is_on_base());
|
|
234
248
|
assert_eq!(nav.current_screen(), &Screen::Main);
|