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
package/mk3-tui/src/main.rs
CHANGED
|
@@ -3,7 +3,8 @@ use crossterm::cursor;
|
|
|
3
3
|
use crossterm::event::{self, Event, KeyCode, KeyEventKind, MouseEventKind};
|
|
4
4
|
use crossterm::execute;
|
|
5
5
|
use crossterm::terminal::{
|
|
6
|
-
disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen,
|
|
6
|
+
disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen,
|
|
7
|
+
LeaveAlternateScreen, SetTitle,
|
|
7
8
|
};
|
|
8
9
|
use ratatui::prelude::*;
|
|
9
10
|
use std::time::{Duration, Instant};
|
|
@@ -11,6 +12,7 @@ use std::time::{Duration, Instant};
|
|
|
11
12
|
mod app;
|
|
12
13
|
mod debug_log;
|
|
13
14
|
mod io;
|
|
15
|
+
mod monitoring;
|
|
14
16
|
mod screens;
|
|
15
17
|
mod storage;
|
|
16
18
|
mod ui;
|
|
@@ -23,6 +25,22 @@ use websocket::{WebSocketClient, WsClientMessage};
|
|
|
23
25
|
/// Default local WebSocket port for TUI ↔ Node (keep in sync with `DEFAULT_TUI_WS_PORT` in `packages/os-cli/src/tui-defaults.ts`).
|
|
24
26
|
const DEFAULT_TUI_WS_PORT: u16 = 8081;
|
|
25
27
|
|
|
28
|
+
fn sanitize_monitoring_line(raw: &str) -> String {
|
|
29
|
+
let mut line: String = raw
|
|
30
|
+
.chars()
|
|
31
|
+
.map(|ch| match ch {
|
|
32
|
+
'\r' | '\n' => ' ',
|
|
33
|
+
ch if ch.is_control() && ch != '\t' => ' ',
|
|
34
|
+
ch => ch,
|
|
35
|
+
})
|
|
36
|
+
.collect();
|
|
37
|
+
if line.chars().count() > 500 {
|
|
38
|
+
line = line.chars().take(500).collect();
|
|
39
|
+
line.push('…');
|
|
40
|
+
}
|
|
41
|
+
line
|
|
42
|
+
}
|
|
43
|
+
|
|
26
44
|
fn main() -> Result<()> {
|
|
27
45
|
// Window / tab title (otherwise hosts often show the executable name, e.g. mk3-tui)
|
|
28
46
|
let window_title = match std::env::var("4RUNR_CLI_VERSION") {
|
|
@@ -33,36 +51,36 @@ fn main() -> Result<()> {
|
|
|
33
51
|
// Setup terminal (boot screen is now part of TUI)
|
|
34
52
|
enable_raw_mode()?;
|
|
35
53
|
let mut stdout = std::io::stdout();
|
|
36
|
-
|
|
54
|
+
|
|
37
55
|
// Enter alternate screen, hide cursor, clear screen, move to top-left
|
|
38
56
|
execute!(
|
|
39
|
-
stdout,
|
|
57
|
+
stdout,
|
|
40
58
|
EnterAlternateScreen,
|
|
41
59
|
SetTitle(window_title),
|
|
42
60
|
cursor::Hide,
|
|
43
61
|
Clear(ClearType::All),
|
|
44
62
|
cursor::MoveTo(0, 0)
|
|
45
63
|
)?;
|
|
46
|
-
|
|
64
|
+
|
|
47
65
|
let backend = CrosstermBackend::new(stdout);
|
|
48
66
|
let mut terminal = Terminal::new(backend)?;
|
|
49
|
-
|
|
67
|
+
|
|
50
68
|
// Clear the terminal buffer to ensure no artifacts
|
|
51
69
|
terminal.clear()?;
|
|
52
70
|
|
|
53
71
|
// Create app
|
|
54
72
|
let mut app = App::new();
|
|
55
73
|
let mut io_handler = IoHandler::new()?;
|
|
56
|
-
|
|
74
|
+
|
|
57
75
|
// Create runtime for async WebSocket client
|
|
58
76
|
let rt = tokio::runtime::Runtime::new()?;
|
|
59
|
-
|
|
77
|
+
|
|
60
78
|
// Get TUI port from environment variable or default (see DEFAULT_TUI_WS_PORT)
|
|
61
79
|
let tui_port = std::env::var("TUI_PORT")
|
|
62
80
|
.ok()
|
|
63
81
|
.and_then(|p| p.parse::<u16>().ok())
|
|
64
82
|
.unwrap_or(DEFAULT_TUI_WS_PORT);
|
|
65
|
-
|
|
83
|
+
|
|
66
84
|
// Try to connect to WebSocket server
|
|
67
85
|
let ws_url = format!("ws://localhost:{}", tui_port);
|
|
68
86
|
let ws_client = rt.block_on(async {
|
|
@@ -78,17 +96,17 @@ fn main() -> Result<()> {
|
|
|
78
96
|
}
|
|
79
97
|
}
|
|
80
98
|
});
|
|
81
|
-
|
|
99
|
+
|
|
82
100
|
// Track real time for uptime (not from tick counter)
|
|
83
101
|
let start_time = Instant::now();
|
|
84
102
|
let mut last_tick = Instant::now();
|
|
85
103
|
let mut portal_connect_ui_tick = Instant::now();
|
|
86
|
-
|
|
104
|
+
|
|
87
105
|
// Track previous screen to detect portal navigation
|
|
88
106
|
let mut previous_screen: Option<crate::screens::Screen> = None;
|
|
89
107
|
// Last terminal size while Connection or Setup portal is visible (windowed hosts: sync buffer + clear on dimension drift).
|
|
90
108
|
let mut standalone_portal_last_terminal_dims: Option<(u16, u16)> = None;
|
|
91
|
-
|
|
109
|
+
|
|
92
110
|
// Force initial render
|
|
93
111
|
app.request_render("initial");
|
|
94
112
|
|
|
@@ -98,7 +116,7 @@ fn main() -> Result<()> {
|
|
|
98
116
|
// ┌─────────────────────────────────────────────────────────┐
|
|
99
117
|
// │ STEP 1: UPDATE ANIMATIONS (runs EVERY loop iteration) │
|
|
100
118
|
// └─────────────────────────────────────────────────────────┘
|
|
101
|
-
|
|
119
|
+
|
|
102
120
|
// Check if portal is active - if so, skip base screen state updates
|
|
103
121
|
// This prevents base screen from affecting portal performance
|
|
104
122
|
use crate::screens::Screen;
|
|
@@ -107,7 +125,7 @@ fn main() -> Result<()> {
|
|
|
107
125
|
current_screen,
|
|
108
126
|
Screen::ConnectionPortal | Screen::SetupPortal | Screen::PortalMonitoring
|
|
109
127
|
);
|
|
110
|
-
|
|
128
|
+
|
|
111
129
|
// DEBUG: Log portal detection
|
|
112
130
|
#[cfg(debug_assertions)]
|
|
113
131
|
{
|
|
@@ -115,47 +133,54 @@ fn main() -> Result<()> {
|
|
|
115
133
|
unsafe {
|
|
116
134
|
LOG_COUNT += 1;
|
|
117
135
|
if LOG_COUNT % 100 == 0 {
|
|
118
|
-
eprintln!(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
136
|
+
eprintln!(
|
|
137
|
+
"[MAIN] Current screen: {:?}, Is portal active: {}",
|
|
138
|
+
current_screen, is_portal_active
|
|
139
|
+
);
|
|
140
|
+
eprintln!(
|
|
141
|
+
"[MAIN] Base screen: {:?}, Overlay stack: {:?}",
|
|
142
|
+
app.state.navigation.base_screen, app.state.navigation.overlay_stack
|
|
143
|
+
);
|
|
122
144
|
}
|
|
123
145
|
}
|
|
124
146
|
}
|
|
125
|
-
|
|
147
|
+
|
|
126
148
|
// Only update base screen state when NOT in a portal
|
|
127
149
|
// Portals are standalone screens and don't need base screen updates
|
|
128
150
|
if !is_portal_active {
|
|
129
151
|
// Uptime - always update from real clock (OUTSIDE poll block)
|
|
130
152
|
app.state.uptime_secs = start_time.elapsed().as_secs();
|
|
131
|
-
|
|
153
|
+
|
|
132
154
|
// Spinner - update on timer (every 150ms) - OUTSIDE poll block
|
|
133
155
|
if last_tick.elapsed() >= Duration::from_millis(150) {
|
|
134
|
-
app.tick();
|
|
156
|
+
app.tick(); // Advances spinner_frame
|
|
135
157
|
last_tick = Instant::now();
|
|
136
158
|
app.request_render("animation_tick"); // Request render for animation update
|
|
137
159
|
}
|
|
138
160
|
}
|
|
139
161
|
|
|
140
162
|
// Connection Portal: redraw while waiting on gateway.connect so the elapsed timer advances
|
|
141
|
-
if matches!(current_screen, Screen::ConnectionPortal)
|
|
163
|
+
if matches!(current_screen, Screen::ConnectionPortal)
|
|
164
|
+
&& app.state.connection_portal.connecting
|
|
165
|
+
{
|
|
142
166
|
if portal_connect_ui_tick.elapsed() >= Duration::from_millis(500) {
|
|
143
167
|
portal_connect_ui_tick = Instant::now();
|
|
144
168
|
app.request_immediate_render("portal_connecting_tick");
|
|
145
169
|
}
|
|
146
170
|
}
|
|
147
|
-
|
|
171
|
+
|
|
148
172
|
// Portal Monitoring: auto-refresh every 5 seconds when enabled
|
|
149
173
|
if matches!(current_screen, Screen::PortalMonitoring) {
|
|
150
174
|
let should_auto_refresh = {
|
|
151
175
|
let pm = &app.state.portal_monitoring;
|
|
152
176
|
pm.auto_refresh_enabled
|
|
153
177
|
&& !pm.loading
|
|
154
|
-
&& pm
|
|
178
|
+
&& pm
|
|
179
|
+
.last_refresh
|
|
155
180
|
.map(|lr| lr.elapsed() >= pm.auto_refresh_interval)
|
|
156
181
|
.unwrap_or(true)
|
|
157
182
|
};
|
|
158
|
-
|
|
183
|
+
|
|
159
184
|
if should_auto_refresh {
|
|
160
185
|
if let Some(ws) = &ws_client {
|
|
161
186
|
app.state.portal_monitoring.last_refresh = Some(Instant::now());
|
|
@@ -163,7 +188,7 @@ fn main() -> Result<()> {
|
|
|
163
188
|
}
|
|
164
189
|
}
|
|
165
190
|
}
|
|
166
|
-
|
|
191
|
+
|
|
167
192
|
// ┌─────────────────────────────────────────────────────────┐
|
|
168
193
|
// │ STEP 2: CHECK WEBSOCKET MESSAGES │
|
|
169
194
|
// └─────────────────────────────────────────────────────────┘
|
|
@@ -172,17 +197,26 @@ fn main() -> Result<()> {
|
|
|
172
197
|
match msg {
|
|
173
198
|
WsClientMessage::Connected => {
|
|
174
199
|
app.add_log("WebSocket connected".to_string());
|
|
175
|
-
|
|
200
|
+
|
|
176
201
|
// Step 4.9: Send startup commands to load real data
|
|
177
202
|
if let Ok(id) = client.send_command("system.status", None) {
|
|
178
|
-
app.add_log(format!(
|
|
203
|
+
app.add_log(format!(
|
|
204
|
+
"Sent system.status (id: {})",
|
|
205
|
+
&id[id.len().saturating_sub(8)..]
|
|
206
|
+
));
|
|
179
207
|
}
|
|
180
208
|
if let Ok(id) = client.send_command("agent.list", None) {
|
|
181
|
-
app.add_log(format!(
|
|
209
|
+
app.add_log(format!(
|
|
210
|
+
"Sent agent.list (id: {})",
|
|
211
|
+
&id[id.len().saturating_sub(8)..]
|
|
212
|
+
));
|
|
182
213
|
}
|
|
183
214
|
}
|
|
184
215
|
WsClientMessage::Disconnected => {
|
|
185
|
-
app.add_log(
|
|
216
|
+
app.add_log(
|
|
217
|
+
"[WEBSOCKET] Disconnected from CLI backend — Gateway link cleared"
|
|
218
|
+
.to_string(),
|
|
219
|
+
);
|
|
186
220
|
app.on_cli_backend_disconnect();
|
|
187
221
|
}
|
|
188
222
|
WsClientMessage::ConnectionLost => {
|
|
@@ -199,7 +233,7 @@ fn main() -> Result<()> {
|
|
|
199
233
|
if resp.payload.success {
|
|
200
234
|
// Format response nicely and update state based on command
|
|
201
235
|
let short_id = &resp.id[resp.id.len().saturating_sub(8)..];
|
|
202
|
-
|
|
236
|
+
|
|
203
237
|
// Try to extract and apply data to state
|
|
204
238
|
if let Some(data) = &resp.payload.data {
|
|
205
239
|
if let Some(obj) = data.as_object() {
|
|
@@ -217,7 +251,9 @@ fn main() -> Result<()> {
|
|
|
217
251
|
"✓ [{}] Mode: {}, Posture: {}",
|
|
218
252
|
short_id,
|
|
219
253
|
mode.as_str().unwrap_or("?"),
|
|
220
|
-
obj.get("posture")
|
|
254
|
+
obj.get("posture")
|
|
255
|
+
.and_then(|p| p.as_str())
|
|
256
|
+
.unwrap_or("?")
|
|
221
257
|
));
|
|
222
258
|
}
|
|
223
259
|
// Handle agent.list response
|
|
@@ -225,63 +261,100 @@ fn main() -> Result<()> {
|
|
|
225
261
|
if let Some(agents_array) = agents_data.as_array() {
|
|
226
262
|
// Parse agents into AgentInfo structs
|
|
227
263
|
use crate::app::AgentInfo;
|
|
228
|
-
let agents: Vec<AgentInfo> = agents_array
|
|
264
|
+
let agents: Vec<AgentInfo> = agents_array
|
|
265
|
+
.iter()
|
|
229
266
|
.filter_map(|agent| {
|
|
230
267
|
let obj = agent.as_object()?;
|
|
231
268
|
Some(AgentInfo {
|
|
232
|
-
name: obj
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
269
|
+
name: obj
|
|
270
|
+
.get("name")?
|
|
271
|
+
.as_str()?
|
|
272
|
+
.to_string(),
|
|
273
|
+
description: obj
|
|
274
|
+
.get("description")
|
|
275
|
+
.and_then(|d| d.as_str())
|
|
276
|
+
.map(|s| s.to_string()),
|
|
277
|
+
model: obj
|
|
278
|
+
.get("model")
|
|
279
|
+
.and_then(|m| m.as_str())
|
|
280
|
+
.unwrap_or("unknown")
|
|
281
|
+
.to_string(),
|
|
282
|
+
provider: obj
|
|
283
|
+
.get("provider")
|
|
284
|
+
.and_then(|p| p.as_str())
|
|
285
|
+
.unwrap_or("unknown")
|
|
286
|
+
.to_string(),
|
|
287
|
+
system_prompt: obj
|
|
288
|
+
.get("systemPrompt")
|
|
289
|
+
.and_then(|sp| sp.as_str())
|
|
290
|
+
.map(|s| s.to_string()),
|
|
291
|
+
temperature: obj
|
|
292
|
+
.get("temperature")
|
|
293
|
+
.and_then(|t| t.as_f64())
|
|
294
|
+
.map(|f| f as f32),
|
|
295
|
+
max_tokens: obj
|
|
296
|
+
.get("maxTokens")
|
|
297
|
+
.and_then(|mt| mt.as_u64())
|
|
298
|
+
.map(|u| u as u32),
|
|
299
|
+
tools: obj
|
|
300
|
+
.get("tools")
|
|
240
301
|
.and_then(|t| t.as_array())
|
|
241
|
-
.map(|arr|
|
|
242
|
-
.
|
|
243
|
-
|
|
302
|
+
.map(|arr| {
|
|
303
|
+
arr.iter()
|
|
304
|
+
.filter_map(|v| {
|
|
305
|
+
v.as_str()
|
|
306
|
+
.map(|s| s.to_string())
|
|
307
|
+
})
|
|
308
|
+
.collect()
|
|
309
|
+
})
|
|
244
310
|
.unwrap_or_default(),
|
|
245
311
|
})
|
|
246
312
|
})
|
|
247
313
|
.collect();
|
|
248
|
-
|
|
314
|
+
|
|
249
315
|
// Update capabilities with agent names
|
|
250
|
-
app.state.capabilities =
|
|
251
|
-
.map(|a| a.name.clone())
|
|
252
|
-
|
|
253
|
-
|
|
316
|
+
app.state.capabilities =
|
|
317
|
+
agents.iter().map(|a| a.name.clone()).collect();
|
|
318
|
+
|
|
254
319
|
// Update cache with agent data
|
|
255
320
|
if let Some(cache) = &mut app.state.cache {
|
|
256
321
|
use crate::storage::cache::AgentData;
|
|
257
322
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
258
|
-
|
|
259
|
-
let cache_agents: Vec<AgentData> = agents
|
|
323
|
+
|
|
324
|
+
let cache_agents: Vec<AgentData> = agents
|
|
325
|
+
.iter()
|
|
260
326
|
.map(|agent| AgentData {
|
|
261
327
|
name: agent.name.clone(),
|
|
262
328
|
description: agent.description.clone(),
|
|
263
329
|
model: agent.model.clone(),
|
|
264
330
|
provider: agent.provider.clone(),
|
|
265
|
-
created_at: SystemTime::now()
|
|
331
|
+
created_at: SystemTime::now()
|
|
332
|
+
.duration_since(UNIX_EPOCH)
|
|
333
|
+
.unwrap()
|
|
334
|
+
.as_secs(),
|
|
266
335
|
})
|
|
267
336
|
.collect();
|
|
268
|
-
|
|
337
|
+
|
|
269
338
|
let _ = cache.update_agents(cache_agents);
|
|
270
339
|
}
|
|
271
|
-
|
|
340
|
+
|
|
272
341
|
// Update agent list state
|
|
273
342
|
app.state.agent_list.agents = agents;
|
|
274
343
|
app.state.agent_list.selected_index = 0;
|
|
275
344
|
app.state.agent_list.detail_view = None;
|
|
276
|
-
|
|
345
|
+
|
|
277
346
|
// Clear pending agent.list ID if this response matches (Step 5.2)
|
|
278
|
-
if Some(&resp.id)
|
|
347
|
+
if Some(&resp.id)
|
|
348
|
+
== app.state.pending_agent_list_id.as_ref()
|
|
349
|
+
{
|
|
279
350
|
app.state.pending_agent_list_id = None;
|
|
280
351
|
}
|
|
281
|
-
|
|
352
|
+
|
|
282
353
|
// Only open AgentList overlay if we're on Main screen (not during boot)
|
|
283
354
|
use crate::screens::Screen;
|
|
284
|
-
if app.state.navigation.current_screen()
|
|
355
|
+
if app.state.navigation.current_screen()
|
|
356
|
+
== &Screen::Main
|
|
357
|
+
{
|
|
285
358
|
app.push_overlay(Screen::AgentList);
|
|
286
359
|
app.request_render("agent_list_opened");
|
|
287
360
|
}
|
|
@@ -290,51 +363,76 @@ fn main() -> Result<()> {
|
|
|
290
363
|
// Handle agent.create response (Step 5.1)
|
|
291
364
|
else if let Some(agent) = obj.get("agent") {
|
|
292
365
|
// Check if this is a response to our agent.create command
|
|
293
|
-
if Some(&resp.id)
|
|
366
|
+
if Some(&resp.id)
|
|
367
|
+
== app.state.pending_agent_create_id.as_ref()
|
|
368
|
+
{
|
|
294
369
|
app.state.pending_agent_create_id = None;
|
|
295
|
-
|
|
370
|
+
|
|
296
371
|
// Extract agent name for success message
|
|
297
|
-
let agent_name = agent
|
|
372
|
+
let agent_name = agent
|
|
373
|
+
.as_object()
|
|
298
374
|
.and_then(|a| a.get("name"))
|
|
299
375
|
.and_then(|n| n.as_str())
|
|
300
376
|
.unwrap_or("unknown");
|
|
301
|
-
|
|
302
|
-
app.add_log(format!(
|
|
303
|
-
|
|
377
|
+
|
|
378
|
+
app.add_log(format!(
|
|
379
|
+
"✓ [{}] Agent '{}' created successfully",
|
|
380
|
+
short_id, agent_name
|
|
381
|
+
));
|
|
382
|
+
|
|
304
383
|
// Refresh agent list (Step 5.3)
|
|
305
384
|
if let Some(ref ws) = ws_client {
|
|
306
|
-
if let Ok(list_id) =
|
|
385
|
+
if let Ok(list_id) =
|
|
386
|
+
ws.send_command("agent.list", None)
|
|
387
|
+
{
|
|
307
388
|
app.state.pending_agent_list_id = Some(list_id);
|
|
308
389
|
}
|
|
309
390
|
}
|
|
310
391
|
} else {
|
|
311
|
-
app.add_log(format!(
|
|
392
|
+
app.add_log(format!(
|
|
393
|
+
"✓ [{}] Agent response received",
|
|
394
|
+
short_id
|
|
395
|
+
));
|
|
312
396
|
}
|
|
313
397
|
}
|
|
314
398
|
// Handle agent.delete response (Step 5.5)
|
|
315
399
|
else if let Some(deleted) = obj.get("deleted") {
|
|
316
400
|
if deleted.as_bool().unwrap_or(false) {
|
|
317
|
-
if Some(&resp.id)
|
|
401
|
+
if Some(&resp.id)
|
|
402
|
+
== app.state.pending_agent_delete_id.as_ref()
|
|
403
|
+
{
|
|
318
404
|
app.state.pending_agent_delete_id = None;
|
|
319
|
-
app.add_log(format!(
|
|
320
|
-
|
|
405
|
+
app.add_log(format!(
|
|
406
|
+
"✓ [{}] Agent deleted successfully",
|
|
407
|
+
short_id
|
|
408
|
+
));
|
|
409
|
+
|
|
321
410
|
// Refresh agent list (Step 5.5)
|
|
322
411
|
if let Some(ref ws) = ws_client {
|
|
323
|
-
if let Ok(list_id) =
|
|
324
|
-
|
|
412
|
+
if let Ok(list_id) =
|
|
413
|
+
ws.send_command("agent.list", None)
|
|
414
|
+
{
|
|
415
|
+
app.state.pending_agent_list_id =
|
|
416
|
+
Some(list_id);
|
|
325
417
|
}
|
|
326
418
|
}
|
|
327
419
|
}
|
|
328
420
|
}
|
|
329
421
|
}
|
|
330
422
|
// Step 6: run.list (match pending id first — avoids stealing the chain on unrelated `runs` keys)
|
|
331
|
-
else if Some(&resp.id)
|
|
423
|
+
else if Some(&resp.id)
|
|
424
|
+
== app.state.pending_run_list_id.as_ref()
|
|
425
|
+
{
|
|
332
426
|
app.state.pending_run_list_id = None;
|
|
333
|
-
if let Some(runs_arr) =
|
|
427
|
+
if let Some(runs_arr) =
|
|
428
|
+
obj.get("runs").and_then(|r| r.as_array())
|
|
429
|
+
{
|
|
334
430
|
use crate::ui::run_manager::run_info_from_gateway_value;
|
|
335
431
|
let mut parsed = Vec::new();
|
|
336
432
|
for item in runs_arr {
|
|
337
|
-
if let Some(info) =
|
|
433
|
+
if let Some(info) =
|
|
434
|
+
run_info_from_gateway_value(item)
|
|
435
|
+
{
|
|
338
436
|
parsed.push(info);
|
|
339
437
|
}
|
|
340
438
|
}
|
|
@@ -353,18 +451,23 @@ fn main() -> Result<()> {
|
|
|
353
451
|
}
|
|
354
452
|
}
|
|
355
453
|
// Step 6: run.get (detail) — pending id first so `run` never blocks other handlers
|
|
356
|
-
else if Some(&resp.id)
|
|
454
|
+
else if Some(&resp.id)
|
|
455
|
+
== app.state.pending_run_get_id.as_ref()
|
|
456
|
+
{
|
|
357
457
|
app.state.pending_run_get_id = None;
|
|
358
458
|
if let Some(run_val) = obj.get("run") {
|
|
359
459
|
use crate::ui::run_manager::run_info_from_gateway_value;
|
|
360
|
-
if let Some(info) = run_info_from_gateway_value(run_val)
|
|
460
|
+
if let Some(info) = run_info_from_gateway_value(run_val)
|
|
461
|
+
{
|
|
361
462
|
app.state.run_manager.merge_run_detail(info);
|
|
362
463
|
}
|
|
363
464
|
}
|
|
364
465
|
app.request_render("run_detail_loaded");
|
|
365
466
|
}
|
|
366
467
|
// Step 6: run.cancel
|
|
367
|
-
else if Some(&resp.id)
|
|
468
|
+
else if Some(&resp.id)
|
|
469
|
+
== app.state.pending_run_cancel_id.as_ref()
|
|
470
|
+
{
|
|
368
471
|
app.state.pending_run_cancel_id = None;
|
|
369
472
|
if let (Some(rid), Some(st)) = (
|
|
370
473
|
obj.get("runId").and_then(|v| v.as_str()),
|
|
@@ -375,18 +478,24 @@ fn main() -> Result<()> {
|
|
|
375
478
|
short_id, rid, st
|
|
376
479
|
));
|
|
377
480
|
} else {
|
|
378
|
-
app.add_log(format!(
|
|
481
|
+
app.add_log(format!(
|
|
482
|
+
"✓ [{}] Run cancel acknowledged",
|
|
483
|
+
short_id
|
|
484
|
+
));
|
|
379
485
|
}
|
|
380
486
|
if let Some(ref ws) = ws_client {
|
|
381
487
|
let data = serde_json::json!({ "limit": 50 });
|
|
382
|
-
if let Ok(lid) = ws.send_command("run.list", Some(data))
|
|
488
|
+
if let Ok(lid) = ws.send_command("run.list", Some(data))
|
|
489
|
+
{
|
|
383
490
|
app.state.pending_run_list_id = Some(lid);
|
|
384
491
|
}
|
|
385
492
|
}
|
|
386
493
|
app.request_render("run_cancelled");
|
|
387
494
|
}
|
|
388
495
|
// Step 6: run.quick
|
|
389
|
-
else if Some(&resp.id)
|
|
496
|
+
else if Some(&resp.id)
|
|
497
|
+
== app.state.pending_run_quick_id.as_ref()
|
|
498
|
+
{
|
|
390
499
|
app.state.pending_run_quick_id = None;
|
|
391
500
|
if let (Some(rid), Some(st)) = (
|
|
392
501
|
obj.get("runId").and_then(|v| v.as_str()),
|
|
@@ -401,42 +510,197 @@ fn main() -> Result<()> {
|
|
|
401
510
|
}
|
|
402
511
|
if let Some(ref ws) = ws_client {
|
|
403
512
|
let data = serde_json::json!({ "limit": 50 });
|
|
404
|
-
if let Ok(lid) = ws.send_command("run.list", Some(data))
|
|
513
|
+
if let Ok(lid) = ws.send_command("run.list", Some(data))
|
|
514
|
+
{
|
|
405
515
|
app.state.pending_run_list_id = Some(lid);
|
|
406
516
|
}
|
|
407
517
|
}
|
|
408
518
|
app.request_render("run_quick_ok");
|
|
409
519
|
}
|
|
520
|
+
// Portal Monitoring Phase 2: monitoring.refresh (section slice)
|
|
521
|
+
else if Some(&resp.id)
|
|
522
|
+
== app.state.pending_monitoring_refresh_id.as_ref()
|
|
523
|
+
{
|
|
524
|
+
app.state.pending_monitoring_refresh_id = None;
|
|
525
|
+
app.state.portal_monitoring.section_refresh_loading = None;
|
|
526
|
+
if obj.get("monitoringRefresh").and_then(|v| v.as_bool())
|
|
527
|
+
== Some(true)
|
|
528
|
+
{
|
|
529
|
+
let section_str = obj
|
|
530
|
+
.get("section")
|
|
531
|
+
.and_then(|v| v.as_str())
|
|
532
|
+
.unwrap_or("");
|
|
533
|
+
use crate::monitoring::MonitoringSection;
|
|
534
|
+
if let Some(sec) =
|
|
535
|
+
MonitoringSection::from_cli_slug(section_str)
|
|
536
|
+
{
|
|
537
|
+
let lines: Vec<String> = obj
|
|
538
|
+
.get("lines")
|
|
539
|
+
.and_then(|a| a.as_array())
|
|
540
|
+
.map(|arr| {
|
|
541
|
+
arr.iter()
|
|
542
|
+
.filter_map(|v| {
|
|
543
|
+
v.as_str()
|
|
544
|
+
.map(sanitize_monitoring_line)
|
|
545
|
+
})
|
|
546
|
+
.collect()
|
|
547
|
+
})
|
|
548
|
+
.unwrap_or_default();
|
|
549
|
+
app.state
|
|
550
|
+
.portal_monitoring
|
|
551
|
+
.section_overrides
|
|
552
|
+
.insert(sec, lines);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
app.request_render("monitoring_refresh_ok");
|
|
556
|
+
}
|
|
557
|
+
// Portal Monitoring Phase 2: monitoring.logs
|
|
558
|
+
else if Some(&resp.id)
|
|
559
|
+
== app.state.pending_monitoring_logs_id.as_ref()
|
|
560
|
+
{
|
|
561
|
+
app.state.pending_monitoring_logs_id = None;
|
|
562
|
+
app.state.portal_monitoring.logs_fetch_loading = false;
|
|
563
|
+
if obj.get("monitoringLogs").and_then(|v| v.as_bool())
|
|
564
|
+
== Some(true)
|
|
565
|
+
{
|
|
566
|
+
let lines: Vec<String> = obj
|
|
567
|
+
.get("logs")
|
|
568
|
+
.and_then(|a| a.as_array())
|
|
569
|
+
.map(|arr| {
|
|
570
|
+
arr.iter()
|
|
571
|
+
.filter_map(|entry| {
|
|
572
|
+
let e = entry.as_object()?;
|
|
573
|
+
let ts = e
|
|
574
|
+
.get("timestamp")
|
|
575
|
+
.and_then(|v| v.as_str())
|
|
576
|
+
.unwrap_or("");
|
|
577
|
+
let lvl = e
|
|
578
|
+
.get("level")
|
|
579
|
+
.and_then(|v| v.as_str())
|
|
580
|
+
.unwrap_or("");
|
|
581
|
+
let msg = e
|
|
582
|
+
.get("message")
|
|
583
|
+
.and_then(|v| v.as_str())
|
|
584
|
+
.unwrap_or("");
|
|
585
|
+
if ts.is_empty()
|
|
586
|
+
&& lvl.is_empty()
|
|
587
|
+
&& msg.is_empty()
|
|
588
|
+
{
|
|
589
|
+
return None;
|
|
590
|
+
}
|
|
591
|
+
Some(sanitize_monitoring_line(
|
|
592
|
+
&format!(
|
|
593
|
+
"{} [{}] {}",
|
|
594
|
+
ts, lvl, msg
|
|
595
|
+
),
|
|
596
|
+
))
|
|
597
|
+
})
|
|
598
|
+
.collect()
|
|
599
|
+
})
|
|
600
|
+
.unwrap_or_default();
|
|
601
|
+
use crate::monitoring::MonitoringSection;
|
|
602
|
+
app.state
|
|
603
|
+
.portal_monitoring
|
|
604
|
+
.section_overrides
|
|
605
|
+
.insert(MonitoringSection::Logs, lines);
|
|
606
|
+
}
|
|
607
|
+
app.request_render("monitoring_logs_ok");
|
|
608
|
+
}
|
|
609
|
+
// Portal Monitoring Phase 3: monitoring.drill (metrics sub-panels / dependency detail)
|
|
610
|
+
else if Some(&resp.id)
|
|
611
|
+
== app.state.pending_monitoring_drill_id.as_ref()
|
|
612
|
+
{
|
|
613
|
+
app.state.pending_monitoring_drill_id = None;
|
|
614
|
+
app.state.portal_monitoring.metrics_drill_loading = false;
|
|
615
|
+
app.state.portal_monitoring.section_refresh_loading = None;
|
|
616
|
+
if obj.get("monitoringDrill").and_then(|v| v.as_bool())
|
|
617
|
+
== Some(true)
|
|
618
|
+
{
|
|
619
|
+
let target = obj
|
|
620
|
+
.get("target")
|
|
621
|
+
.and_then(|v| v.as_str())
|
|
622
|
+
.unwrap_or("");
|
|
623
|
+
let lines: Vec<String> = obj
|
|
624
|
+
.get("lines")
|
|
625
|
+
.and_then(|a| a.as_array())
|
|
626
|
+
.map(|arr| {
|
|
627
|
+
arr.iter()
|
|
628
|
+
.filter_map(|v| {
|
|
629
|
+
v.as_str().map(sanitize_monitoring_line)
|
|
630
|
+
})
|
|
631
|
+
.collect()
|
|
632
|
+
})
|
|
633
|
+
.unwrap_or_default();
|
|
634
|
+
use crate::monitoring::{
|
|
635
|
+
MetricsDrillPanel, MonitoringSection,
|
|
636
|
+
};
|
|
637
|
+
if target == "dependencies_detail" {
|
|
638
|
+
app.state
|
|
639
|
+
.portal_monitoring
|
|
640
|
+
.section_overrides
|
|
641
|
+
.insert(MonitoringSection::Dependencies, lines);
|
|
642
|
+
} else if target == "system_diagnostics" {
|
|
643
|
+
app.state
|
|
644
|
+
.portal_monitoring
|
|
645
|
+
.section_overrides
|
|
646
|
+
.insert(MonitoringSection::System, lines);
|
|
647
|
+
} else if let Some(panel) =
|
|
648
|
+
MetricsDrillPanel::from_cli_target(target)
|
|
649
|
+
{
|
|
650
|
+
app.state.portal_monitoring.metrics_drill =
|
|
651
|
+
Some(panel);
|
|
652
|
+
app.state.portal_monitoring.metrics_drill_lines =
|
|
653
|
+
lines;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
app.request_render("monitoring_drill_ok");
|
|
657
|
+
}
|
|
410
658
|
// Portal Monitoring: gateway.observability (/metrics snapshot)
|
|
411
|
-
else if Some(&resp.id)
|
|
659
|
+
else if Some(&resp.id)
|
|
660
|
+
== app.state.pending_gateway_observability_id.as_ref()
|
|
661
|
+
{
|
|
412
662
|
app.state.pending_gateway_observability_id = None;
|
|
413
663
|
app.state.portal_monitoring.loading = false;
|
|
414
|
-
app.state.portal_observability_last_poll =
|
|
415
|
-
|
|
664
|
+
app.state.portal_observability_last_poll =
|
|
665
|
+
Some(Instant::now());
|
|
666
|
+
if obj.get("observability").and_then(|v| v.as_bool())
|
|
667
|
+
== Some(true)
|
|
668
|
+
{
|
|
416
669
|
let snapshot = obj
|
|
417
670
|
.get("snapshotAt")
|
|
418
671
|
.and_then(|v| v.as_str())
|
|
419
672
|
.unwrap_or("")
|
|
420
673
|
.to_string();
|
|
674
|
+
// Snapshot markers — keep substring order compatible with
|
|
675
|
+
// `portal_monitoring::SNAPSHOT_MARKER_*` (CLI healthLines + layout below).
|
|
421
676
|
let mut lines: Vec<String> = vec![
|
|
422
677
|
"Portal snapshot (health + metrics on each refresh / auto poll)."
|
|
423
678
|
.to_string(),
|
|
424
679
|
format!("Fetched: {}", if snapshot.is_empty() { "—" } else { snapshot.as_str() }),
|
|
425
680
|
String::new(),
|
|
426
681
|
];
|
|
427
|
-
if let Some(tot) = obj
|
|
682
|
+
if let Some(tot) = obj
|
|
683
|
+
.get("totals")
|
|
684
|
+
.and_then(|v| v.get("httpRequests"))
|
|
685
|
+
{
|
|
428
686
|
let current = tot
|
|
429
687
|
.as_u64()
|
|
430
688
|
.or_else(|| tot.as_f64().map(|f| f as u64));
|
|
431
689
|
if let Some(c) = current {
|
|
432
690
|
if let (Some(prev), Some(last_t)) = (
|
|
433
|
-
app.state
|
|
434
|
-
|
|
691
|
+
app.state
|
|
692
|
+
.portal_monitoring
|
|
693
|
+
.last_http_for_delta,
|
|
694
|
+
app.state
|
|
695
|
+
.portal_monitoring
|
|
696
|
+
.last_instant_for_delta,
|
|
435
697
|
) {
|
|
436
698
|
let d = c.saturating_sub(prev);
|
|
437
|
-
let el =
|
|
699
|
+
let el =
|
|
700
|
+
last_t.elapsed().as_secs_f32().max(0.1);
|
|
438
701
|
let per_min = (d as f32 / el) * 60.0;
|
|
439
|
-
let pulse =
|
|
702
|
+
let pulse =
|
|
703
|
+
if d > 0 { " *" } else { " --" };
|
|
440
704
|
lines.push(format!(
|
|
441
705
|
"LIVE: +{} HTTP since last screen refresh (~{:.0}/min){}",
|
|
442
706
|
d, per_min, pulse
|
|
@@ -444,13 +708,138 @@ fn main() -> Result<()> {
|
|
|
444
708
|
} else {
|
|
445
709
|
lines.push("LIVE: first sample — next refresh shows HTTP request rate (poll interval).".to_string());
|
|
446
710
|
}
|
|
447
|
-
app.state
|
|
448
|
-
|
|
711
|
+
app.state
|
|
712
|
+
.portal_monitoring
|
|
713
|
+
.last_http_for_delta = Some(c);
|
|
714
|
+
app.state
|
|
715
|
+
.portal_monitoring
|
|
716
|
+
.last_instant_for_delta =
|
|
449
717
|
Some(std::time::Instant::now());
|
|
718
|
+
app.state
|
|
719
|
+
.portal_monitoring
|
|
720
|
+
.http_total_sparkline
|
|
721
|
+
.push_back(c);
|
|
722
|
+
while app
|
|
723
|
+
.state
|
|
724
|
+
.portal_monitoring
|
|
725
|
+
.http_total_sparkline
|
|
726
|
+
.len()
|
|
727
|
+
> 48
|
|
728
|
+
{
|
|
729
|
+
app.state
|
|
730
|
+
.portal_monitoring
|
|
731
|
+
.http_total_sparkline
|
|
732
|
+
.pop_front();
|
|
733
|
+
}
|
|
450
734
|
}
|
|
451
735
|
}
|
|
736
|
+
if let Some(totals) =
|
|
737
|
+
obj.get("totals").and_then(|v| v.as_object())
|
|
738
|
+
{
|
|
739
|
+
app.state
|
|
740
|
+
.portal_monitoring
|
|
741
|
+
.metric_history
|
|
742
|
+
.push_back(
|
|
743
|
+
crate::app::metric_history_entry_from_totals(
|
|
744
|
+
if snapshot.is_empty() {
|
|
745
|
+
"—".to_string()
|
|
746
|
+
} else {
|
|
747
|
+
snapshot.clone()
|
|
748
|
+
},
|
|
749
|
+
totals,
|
|
750
|
+
),
|
|
751
|
+
);
|
|
752
|
+
while app
|
|
753
|
+
.state
|
|
754
|
+
.portal_monitoring
|
|
755
|
+
.metric_history
|
|
756
|
+
.len()
|
|
757
|
+
> 720
|
|
758
|
+
{
|
|
759
|
+
app.state
|
|
760
|
+
.portal_monitoring
|
|
761
|
+
.metric_history
|
|
762
|
+
.pop_front();
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
// Phase 5 v1: dependency alerts are text-derived from
|
|
766
|
+
// observability health lines, not a structured readiness enum.
|
|
767
|
+
let dependency_status = {
|
|
768
|
+
let mut status = if obj
|
|
769
|
+
.get("healthOk")
|
|
770
|
+
.and_then(|v| v.as_bool())
|
|
771
|
+
== Some(false)
|
|
772
|
+
{
|
|
773
|
+
"unhealthy"
|
|
774
|
+
} else {
|
|
775
|
+
"healthy"
|
|
776
|
+
};
|
|
777
|
+
if let Some(arr) = obj
|
|
778
|
+
.get("healthLines")
|
|
779
|
+
.and_then(|a| a.as_array())
|
|
780
|
+
{
|
|
781
|
+
for v in arr {
|
|
782
|
+
if let Some(s) = v.as_str() {
|
|
783
|
+
let l = s.to_lowercase();
|
|
784
|
+
if l.contains("✗")
|
|
785
|
+
|| l.contains(": down")
|
|
786
|
+
|| l.contains("not ready")
|
|
787
|
+
{
|
|
788
|
+
status = "unhealthy";
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
791
|
+
if l.contains("⚠")
|
|
792
|
+
|| l.contains("degraded")
|
|
793
|
+
|| l.contains("warning")
|
|
794
|
+
{
|
|
795
|
+
status = "degraded";
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
status.to_string()
|
|
801
|
+
};
|
|
802
|
+
if let Some(prev) = app
|
|
803
|
+
.state
|
|
804
|
+
.portal_monitoring
|
|
805
|
+
.last_dependency_status
|
|
806
|
+
.clone()
|
|
807
|
+
{
|
|
808
|
+
if prev != dependency_status {
|
|
809
|
+
app.state
|
|
810
|
+
.portal_monitoring
|
|
811
|
+
.dependency_alerts
|
|
812
|
+
.push_back(
|
|
813
|
+
crate::app::DependencyAlertEntry {
|
|
814
|
+
timestamp: if snapshot.is_empty() {
|
|
815
|
+
"—".to_string()
|
|
816
|
+
} else {
|
|
817
|
+
snapshot.clone()
|
|
818
|
+
},
|
|
819
|
+
previous: prev,
|
|
820
|
+
current: dependency_status.clone(),
|
|
821
|
+
},
|
|
822
|
+
);
|
|
823
|
+
while app
|
|
824
|
+
.state
|
|
825
|
+
.portal_monitoring
|
|
826
|
+
.dependency_alerts
|
|
827
|
+
.len()
|
|
828
|
+
> 10
|
|
829
|
+
{
|
|
830
|
+
app.state
|
|
831
|
+
.portal_monitoring
|
|
832
|
+
.dependency_alerts
|
|
833
|
+
.pop_front();
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
app.state.portal_monitoring.last_dependency_status =
|
|
838
|
+
Some(dependency_status);
|
|
452
839
|
lines.push(String::new());
|
|
453
|
-
if let Some(arr) =
|
|
840
|
+
if let Some(arr) =
|
|
841
|
+
obj.get("healthLines").and_then(|a| a.as_array())
|
|
842
|
+
{
|
|
454
843
|
for v in arr {
|
|
455
844
|
if let Some(s) = v.as_str() {
|
|
456
845
|
lines.push(s.to_string());
|
|
@@ -463,7 +852,9 @@ fn main() -> Result<()> {
|
|
|
463
852
|
.to_string(),
|
|
464
853
|
);
|
|
465
854
|
lines.push(String::new());
|
|
466
|
-
if let Some(arr) =
|
|
855
|
+
if let Some(arr) =
|
|
856
|
+
obj.get("statsLines").and_then(|a| a.as_array())
|
|
857
|
+
{
|
|
467
858
|
for v in arr {
|
|
468
859
|
if let Some(s) = v.as_str() {
|
|
469
860
|
lines.push(s.to_string());
|
|
@@ -471,8 +862,13 @@ fn main() -> Result<()> {
|
|
|
471
862
|
}
|
|
472
863
|
}
|
|
473
864
|
lines.push(String::new());
|
|
474
|
-
lines.push(
|
|
475
|
-
|
|
865
|
+
lines.push(
|
|
866
|
+
"Top HTTP routes (from http_requests_total labels)"
|
|
867
|
+
.to_string(),
|
|
868
|
+
);
|
|
869
|
+
if let Some(arr) =
|
|
870
|
+
obj.get("routeLines").and_then(|a| a.as_array())
|
|
871
|
+
{
|
|
476
872
|
if !arr.is_empty() {
|
|
477
873
|
lines.push(
|
|
478
874
|
" (method route) count".to_string(),
|
|
@@ -490,11 +886,21 @@ fn main() -> Result<()> {
|
|
|
490
886
|
}
|
|
491
887
|
} else {
|
|
492
888
|
lines.push(
|
|
493
|
-
" (no route breakdown in response.)"
|
|
889
|
+
" (no route breakdown in response.)"
|
|
890
|
+
.to_string(),
|
|
494
891
|
);
|
|
495
892
|
}
|
|
496
|
-
app.state.portal_monitoring.last_updated =
|
|
893
|
+
app.state.portal_monitoring.last_updated =
|
|
894
|
+
Some(snapshot);
|
|
497
895
|
app.state.portal_monitoring.content_lines = lines;
|
|
896
|
+
app.state.portal_monitoring.section_overrides.clear();
|
|
897
|
+
app.state.portal_monitoring.section_refresh_loading =
|
|
898
|
+
None;
|
|
899
|
+
app.state.portal_monitoring.logs_fetch_loading = false;
|
|
900
|
+
app.state.portal_monitoring.metrics_drill = None;
|
|
901
|
+
app.state.portal_monitoring.metrics_drill_lines.clear();
|
|
902
|
+
app.state.portal_monitoring.metrics_drill_loading =
|
|
903
|
+
false;
|
|
498
904
|
app.state.portal_monitoring.error = None;
|
|
499
905
|
app.state.portal_monitoring.scroll_offset = 0;
|
|
500
906
|
} else {
|
|
@@ -507,41 +913,55 @@ fn main() -> Result<()> {
|
|
|
507
913
|
// Handle gateway.connect response (Step 7.5)
|
|
508
914
|
else if let Some(connected) = obj.get("connected") {
|
|
509
915
|
if connected.as_bool().unwrap_or(false) {
|
|
510
|
-
if Some(&resp.id)
|
|
916
|
+
if Some(&resp.id)
|
|
917
|
+
== app.state.pending_gateway_connect_id.as_ref()
|
|
918
|
+
{
|
|
511
919
|
app.state.pending_gateway_connect_id = None;
|
|
512
|
-
|
|
920
|
+
|
|
513
921
|
use crate::app::OperationMode;
|
|
514
922
|
app.state.operation_mode = OperationMode::Connected;
|
|
515
|
-
|
|
516
|
-
let url = obj
|
|
923
|
+
|
|
924
|
+
let url = obj
|
|
925
|
+
.get("url")
|
|
517
926
|
.and_then(|v| v.as_str())
|
|
518
927
|
.unwrap_or("unknown");
|
|
519
|
-
|
|
928
|
+
|
|
520
929
|
app.state.gateway_url = Some(url.to_string());
|
|
521
|
-
app.state.connection_portal.last_successful_url =
|
|
930
|
+
app.state.connection_portal.last_successful_url =
|
|
931
|
+
Some(url.to_string());
|
|
522
932
|
app.state.connection_portal.finish_connecting();
|
|
523
|
-
app.state.connection_portal.connection_success =
|
|
524
|
-
|
|
933
|
+
app.state.connection_portal.connection_success =
|
|
934
|
+
true; // Phase 1.3: Set success flag
|
|
935
|
+
// Dashboard "Demo Mode / NET" used `connected`, which was never set — align with Gateway link
|
|
525
936
|
app.state.connected = true;
|
|
526
|
-
|
|
937
|
+
|
|
527
938
|
// Parse activity log if present
|
|
528
939
|
if let Some(activity_log) = obj.get("activityLog") {
|
|
529
|
-
if let Some(log_array) = activity_log.as_array()
|
|
530
|
-
|
|
940
|
+
if let Some(log_array) = activity_log.as_array()
|
|
941
|
+
{
|
|
942
|
+
app.state
|
|
943
|
+
.connection_portal
|
|
944
|
+
.activity_log
|
|
945
|
+
.clear();
|
|
531
946
|
for entry in log_array {
|
|
532
|
-
if let Some(entry_obj) =
|
|
533
|
-
|
|
947
|
+
if let Some(entry_obj) =
|
|
948
|
+
entry.as_object()
|
|
949
|
+
{
|
|
950
|
+
let timestamp = entry_obj
|
|
951
|
+
.get("timestamp")
|
|
534
952
|
.and_then(|v| v.as_str())
|
|
535
953
|
.unwrap_or("")
|
|
536
954
|
.to_string();
|
|
537
|
-
let level_str = entry_obj
|
|
955
|
+
let level_str = entry_obj
|
|
956
|
+
.get("level")
|
|
538
957
|
.and_then(|v| v.as_str())
|
|
539
958
|
.unwrap_or("info");
|
|
540
|
-
let message = entry_obj
|
|
959
|
+
let message = entry_obj
|
|
960
|
+
.get("message")
|
|
541
961
|
.and_then(|v| v.as_str())
|
|
542
962
|
.unwrap_or("")
|
|
543
963
|
.to_string();
|
|
544
|
-
|
|
964
|
+
|
|
545
965
|
use crate::app::LogLevel;
|
|
546
966
|
let level = match level_str {
|
|
547
967
|
"success" => LogLevel::Success,
|
|
@@ -549,36 +969,55 @@ fn main() -> Result<()> {
|
|
|
549
969
|
"error" => LogLevel::Error,
|
|
550
970
|
_ => LogLevel::Info,
|
|
551
971
|
};
|
|
552
|
-
|
|
553
|
-
app.state
|
|
972
|
+
|
|
973
|
+
app.state
|
|
974
|
+
.connection_portal
|
|
975
|
+
.add_log_entry(
|
|
976
|
+
timestamp, level, message,
|
|
977
|
+
);
|
|
554
978
|
}
|
|
555
979
|
}
|
|
556
980
|
}
|
|
557
981
|
}
|
|
558
|
-
|
|
982
|
+
|
|
559
983
|
// Phase 1.2: Prominent connection success notification
|
|
560
|
-
app.add_log(
|
|
984
|
+
app.add_log(
|
|
985
|
+
"═══════════════════════════════════════════"
|
|
986
|
+
.to_string(),
|
|
987
|
+
);
|
|
561
988
|
app.add_log(format!("✅ CONNECTED TO GATEWAY"));
|
|
562
989
|
app.add_log(format!(" URL: {}", url));
|
|
563
990
|
app.add_log(format!(" Status: Connected"));
|
|
564
991
|
app.add_log(" Note: connect checks /health + /ready; Shield/Sentinel run on the Gateway during jobs.".to_string());
|
|
565
|
-
app.add_log(
|
|
566
|
-
|
|
567
|
-
|
|
992
|
+
app.add_log(
|
|
993
|
+
"═══════════════════════════════════════════"
|
|
994
|
+
.to_string(),
|
|
995
|
+
);
|
|
996
|
+
app.add_log(format!(
|
|
997
|
+
"[MODE] Switched to CONNECTED mode"
|
|
998
|
+
));
|
|
999
|
+
|
|
568
1000
|
// Phase 2.1: Immediately verify connection with health check
|
|
569
|
-
app.add_log(format!(
|
|
1001
|
+
app.add_log(format!(
|
|
1002
|
+
"[GATEWAY] Verifying connection health..."
|
|
1003
|
+
));
|
|
570
1004
|
if let Some(ref ws) = ws_client {
|
|
571
|
-
if let Ok(health_id) =
|
|
572
|
-
|
|
1005
|
+
if let Ok(health_id) =
|
|
1006
|
+
ws.send_command("gateway.health", None)
|
|
1007
|
+
{
|
|
1008
|
+
app.state.pending_gateway_health_id =
|
|
1009
|
+
Some(health_id);
|
|
573
1010
|
}
|
|
574
1011
|
// Refresh posture / NET labels from CLI (matches gateway-backed system.status)
|
|
575
1012
|
let _ = ws.send_command("system.status", None);
|
|
576
1013
|
}
|
|
577
|
-
|
|
1014
|
+
|
|
578
1015
|
// CRITICAL: Request render after state change to update UI
|
|
579
1016
|
// CRITICAL: Use immediate render for instant success display
|
|
580
|
-
app.request_immediate_render(
|
|
581
|
-
|
|
1017
|
+
app.request_immediate_render(
|
|
1018
|
+
"gateway_connect_success",
|
|
1019
|
+
);
|
|
1020
|
+
|
|
582
1021
|
// Phase 1.3: Portal will show success state (user closes with ESC)
|
|
583
1022
|
// Don't auto-close - let user see success message
|
|
584
1023
|
}
|
|
@@ -590,11 +1029,20 @@ fn main() -> Result<()> {
|
|
|
590
1029
|
app.state.connected = false;
|
|
591
1030
|
app.state.gateway_healthy = false;
|
|
592
1031
|
app.state.gateway_db_summary = None;
|
|
593
|
-
app.add_log(format!(
|
|
1032
|
+
app.add_log(format!(
|
|
1033
|
+
"✓ [{}] Disconnected from Gateway",
|
|
1034
|
+
short_id
|
|
1035
|
+
));
|
|
594
1036
|
app.add_log(format!("[MODE] Switched to LOCAL mode"));
|
|
595
1037
|
app.state.pending_gateway_observability_id = None;
|
|
1038
|
+
app.state.pending_monitoring_refresh_id = None;
|
|
1039
|
+
app.state.pending_monitoring_logs_id = None;
|
|
1040
|
+
app.state.pending_monitoring_drill_id = None;
|
|
596
1041
|
app.state.portal_observability_last_poll = None;
|
|
597
|
-
app.state.portal_monitoring =
|
|
1042
|
+
app.state.portal_monitoring =
|
|
1043
|
+
crate::app::PortalMonitoringState::default();
|
|
1044
|
+
app.state.advanced_monitoring =
|
|
1045
|
+
crate::app::AdvancedMonitoringState::default();
|
|
598
1046
|
if let Some(ref ws) = ws_client {
|
|
599
1047
|
let _ = ws.send_command("system.status", None);
|
|
600
1048
|
}
|
|
@@ -602,18 +1050,33 @@ fn main() -> Result<()> {
|
|
|
602
1050
|
}
|
|
603
1051
|
// Handle setup.detect response
|
|
604
1052
|
else if let Some(local_bundle) = obj.get("localBundle") {
|
|
605
|
-
if Some(&resp.id)
|
|
1053
|
+
if Some(&resp.id)
|
|
1054
|
+
== app.state.pending_setup_detect_id.as_ref()
|
|
1055
|
+
{
|
|
606
1056
|
app.state.pending_setup_detect_id = None;
|
|
607
1057
|
app.state.setup_portal.detecting = false;
|
|
608
1058
|
app.state.setup_portal.detecting_since = None;
|
|
609
|
-
|
|
610
|
-
use crate::app::{
|
|
611
|
-
|
|
612
|
-
|
|
1059
|
+
|
|
1060
|
+
use crate::app::{
|
|
1061
|
+
BundleStatus, CloudStatus, DetectionResult,
|
|
1062
|
+
};
|
|
1063
|
+
|
|
1064
|
+
let bundle_status = if let Some(bundle_obj) =
|
|
1065
|
+
local_bundle.as_object()
|
|
1066
|
+
{
|
|
613
1067
|
BundleStatus {
|
|
614
|
-
available: bundle_obj
|
|
615
|
-
|
|
616
|
-
|
|
1068
|
+
available: bundle_obj
|
|
1069
|
+
.get("available")
|
|
1070
|
+
.and_then(|v| v.as_bool())
|
|
1071
|
+
.unwrap_or(false),
|
|
1072
|
+
running: bundle_obj
|
|
1073
|
+
.get("running")
|
|
1074
|
+
.and_then(|v| v.as_bool())
|
|
1075
|
+
.unwrap_or(false),
|
|
1076
|
+
path: bundle_obj
|
|
1077
|
+
.get("path")
|
|
1078
|
+
.and_then(|v| v.as_str())
|
|
1079
|
+
.map(|s| s.to_string()),
|
|
617
1080
|
}
|
|
618
1081
|
} else {
|
|
619
1082
|
app.state.setup_portal.error = Some(
|
|
@@ -625,33 +1088,54 @@ fn main() -> Result<()> {
|
|
|
625
1088
|
path: None,
|
|
626
1089
|
}
|
|
627
1090
|
};
|
|
628
|
-
|
|
629
|
-
let cloud_obj =
|
|
1091
|
+
|
|
1092
|
+
let cloud_obj =
|
|
1093
|
+
obj.get("cloudServer").and_then(|v| v.as_object());
|
|
630
1094
|
let cloud_status = CloudStatus {
|
|
631
|
-
available: cloud_obj
|
|
632
|
-
|
|
1095
|
+
available: cloud_obj
|
|
1096
|
+
.and_then(|o| o.get("available"))
|
|
1097
|
+
.and_then(|v| v.as_bool())
|
|
1098
|
+
.unwrap_or(false),
|
|
1099
|
+
status: cloud_obj
|
|
1100
|
+
.and_then(|o| o.get("status"))
|
|
1101
|
+
.and_then(|v| v.as_str())
|
|
1102
|
+
.unwrap_or("Unknown")
|
|
1103
|
+
.to_string(),
|
|
633
1104
|
};
|
|
634
|
-
|
|
635
|
-
app.state.setup_portal.detection_result =
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
1105
|
+
|
|
1106
|
+
app.state.setup_portal.detection_result =
|
|
1107
|
+
Some(DetectionResult {
|
|
1108
|
+
local_bundle: bundle_status,
|
|
1109
|
+
cloud_server: cloud_status,
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
app.add_log(
|
|
1113
|
+
"[SETUP] Gateway detection complete".to_string(),
|
|
1114
|
+
);
|
|
641
1115
|
}
|
|
642
1116
|
}
|
|
643
1117
|
// Phase 2.1: Handle gateway.health response
|
|
644
1118
|
else if let Some(healthy) = obj.get("healthy") {
|
|
645
|
-
if Some(&resp.id)
|
|
1119
|
+
if Some(&resp.id)
|
|
1120
|
+
== app.state.pending_gateway_health_id.as_ref()
|
|
1121
|
+
{
|
|
646
1122
|
app.state.pending_gateway_health_id = None;
|
|
647
|
-
|
|
1123
|
+
|
|
648
1124
|
let is_healthy = healthy.as_bool().unwrap_or(false);
|
|
649
|
-
let latency = obj
|
|
650
|
-
|
|
1125
|
+
let latency = obj
|
|
1126
|
+
.get("latency")
|
|
1127
|
+
.and_then(|v| v.as_u64())
|
|
1128
|
+
.unwrap_or(0);
|
|
1129
|
+
let deps_ready =
|
|
1130
|
+
obj.get("ready").and_then(|v| v.as_bool());
|
|
651
1131
|
|
|
652
1132
|
let mut db_summary: Option<String> = None;
|
|
653
|
-
if let Some(h) =
|
|
654
|
-
|
|
1133
|
+
if let Some(h) =
|
|
1134
|
+
obj.get("health").and_then(|v| v.as_object())
|
|
1135
|
+
{
|
|
1136
|
+
if let Some(fr) =
|
|
1137
|
+
h.get("fourRunrDb").and_then(|v| v.as_object())
|
|
1138
|
+
{
|
|
655
1139
|
let mode = fr
|
|
656
1140
|
.get("mode")
|
|
657
1141
|
.and_then(|v| v.as_str())
|
|
@@ -689,15 +1173,20 @@ fn main() -> Result<()> {
|
|
|
689
1173
|
app.state.gateway_healthy = false;
|
|
690
1174
|
app.state.gateway_db_summary = None;
|
|
691
1175
|
app.state.connection_portal.finish_connecting();
|
|
692
|
-
let error = obj
|
|
693
|
-
|
|
1176
|
+
let error = obj
|
|
1177
|
+
.get("error")
|
|
1178
|
+
.and_then(|v| v.as_str())
|
|
1179
|
+
.unwrap_or("Unknown error");
|
|
1180
|
+
app.add_log(format!(
|
|
1181
|
+
"⚠ [{}] Gateway health check failed: {} ({}ms)",
|
|
1182
|
+
short_id, error, latency
|
|
1183
|
+
));
|
|
694
1184
|
}
|
|
695
|
-
|
|
1185
|
+
|
|
696
1186
|
// CRITICAL: Request render after health state change
|
|
697
1187
|
app.request_render("gateway_health_update");
|
|
698
1188
|
}
|
|
699
|
-
}
|
|
700
|
-
else {
|
|
1189
|
+
} else {
|
|
701
1190
|
app.add_log(format!("✓ [{}] Success", short_id));
|
|
702
1191
|
}
|
|
703
1192
|
} else {
|
|
@@ -708,41 +1197,60 @@ fn main() -> Result<()> {
|
|
|
708
1197
|
}
|
|
709
1198
|
} else {
|
|
710
1199
|
let short_id = &resp.id[resp.id.len().saturating_sub(8)..];
|
|
711
|
-
let error_msg = resp
|
|
712
|
-
|
|
1200
|
+
let error_msg = resp
|
|
1201
|
+
.payload
|
|
1202
|
+
.error
|
|
1203
|
+
.unwrap_or_else(|| "Unknown error".to_string());
|
|
1204
|
+
|
|
713
1205
|
// Check if this is a response to agent.create command (Step 5.1)
|
|
714
1206
|
if Some(&resp.id) == app.state.pending_agent_create_id.as_ref() {
|
|
715
1207
|
app.state.pending_agent_create_id = None;
|
|
716
|
-
app.add_log(format!(
|
|
1208
|
+
app.add_log(format!(
|
|
1209
|
+
"✗ [{}] Agent creation failed: {}",
|
|
1210
|
+
short_id, error_msg
|
|
1211
|
+
));
|
|
717
1212
|
}
|
|
718
1213
|
// Check if this is a response to agent.delete command (Step 5.5)
|
|
719
1214
|
else if Some(&resp.id) == app.state.pending_agent_delete_id.as_ref() {
|
|
720
1215
|
app.state.pending_agent_delete_id = None;
|
|
721
|
-
app.add_log(format!(
|
|
722
|
-
|
|
723
|
-
|
|
1216
|
+
app.add_log(format!(
|
|
1217
|
+
"✗ [{}] Agent deletion failed: {}",
|
|
1218
|
+
short_id, error_msg
|
|
1219
|
+
));
|
|
1220
|
+
} else if Some(&resp.id) == app.state.pending_run_list_id.as_ref() {
|
|
724
1221
|
app.state.pending_run_list_id = None;
|
|
725
1222
|
app.state.run_manager.loading = false;
|
|
726
|
-
app.add_log(format!(
|
|
727
|
-
|
|
728
|
-
|
|
1223
|
+
app.add_log(format!(
|
|
1224
|
+
"✗ [{}] run.list failed: {}",
|
|
1225
|
+
short_id, error_msg
|
|
1226
|
+
));
|
|
1227
|
+
} else if Some(&resp.id) == app.state.pending_run_get_id.as_ref() {
|
|
729
1228
|
app.state.pending_run_get_id = None;
|
|
730
|
-
app.add_log(format!(
|
|
731
|
-
|
|
732
|
-
|
|
1229
|
+
app.add_log(format!(
|
|
1230
|
+
"✗ [{}] run.get failed: {}",
|
|
1231
|
+
short_id, error_msg
|
|
1232
|
+
));
|
|
1233
|
+
} else if Some(&resp.id) == app.state.pending_run_cancel_id.as_ref() {
|
|
733
1234
|
app.state.pending_run_cancel_id = None;
|
|
734
|
-
app.add_log(format!(
|
|
735
|
-
|
|
736
|
-
|
|
1235
|
+
app.add_log(format!(
|
|
1236
|
+
"✗ [{}] run.cancel failed: {}",
|
|
1237
|
+
short_id, error_msg
|
|
1238
|
+
));
|
|
1239
|
+
} else if Some(&resp.id) == app.state.pending_run_quick_id.as_ref() {
|
|
737
1240
|
app.state.pending_run_quick_id = None;
|
|
738
|
-
app.add_log(format!(
|
|
1241
|
+
app.add_log(format!(
|
|
1242
|
+
"✗ [{}] run.quick failed: {}",
|
|
1243
|
+
short_id, error_msg
|
|
1244
|
+
));
|
|
739
1245
|
}
|
|
740
1246
|
// Check if this is a response to gateway.connect command (Step 7.5)
|
|
741
|
-
else if Some(&resp.id)
|
|
1247
|
+
else if Some(&resp.id)
|
|
1248
|
+
== app.state.pending_gateway_connect_id.as_ref()
|
|
1249
|
+
{
|
|
742
1250
|
app.state.pending_gateway_connect_id = None;
|
|
743
1251
|
app.state.connection_portal.finish_connecting();
|
|
744
1252
|
app.state.connection_portal.error = Some(error_msg.clone());
|
|
745
|
-
|
|
1253
|
+
|
|
746
1254
|
// Parse activity log from error response if present
|
|
747
1255
|
if let Some(data_value) = &resp.payload.data {
|
|
748
1256
|
if let Some(obj) = data_value.as_object() {
|
|
@@ -751,18 +1259,21 @@ fn main() -> Result<()> {
|
|
|
751
1259
|
app.state.connection_portal.activity_log.clear();
|
|
752
1260
|
for entry in log_array {
|
|
753
1261
|
if let Some(entry_obj) = entry.as_object() {
|
|
754
|
-
let timestamp = entry_obj
|
|
1262
|
+
let timestamp = entry_obj
|
|
1263
|
+
.get("timestamp")
|
|
755
1264
|
.and_then(|v| v.as_str())
|
|
756
1265
|
.unwrap_or("")
|
|
757
1266
|
.to_string();
|
|
758
|
-
let level_str = entry_obj
|
|
1267
|
+
let level_str = entry_obj
|
|
1268
|
+
.get("level")
|
|
759
1269
|
.and_then(|v| v.as_str())
|
|
760
1270
|
.unwrap_or("info");
|
|
761
|
-
let message = entry_obj
|
|
1271
|
+
let message = entry_obj
|
|
1272
|
+
.get("message")
|
|
762
1273
|
.and_then(|v| v.as_str())
|
|
763
1274
|
.unwrap_or("")
|
|
764
1275
|
.to_string();
|
|
765
|
-
|
|
1276
|
+
|
|
766
1277
|
use crate::app::LogLevel;
|
|
767
1278
|
let level = match level_str {
|
|
768
1279
|
"success" => LogLevel::Success,
|
|
@@ -770,17 +1281,22 @@ fn main() -> Result<()> {
|
|
|
770
1281
|
"error" => LogLevel::Error,
|
|
771
1282
|
_ => LogLevel::Info,
|
|
772
1283
|
};
|
|
773
|
-
|
|
774
|
-
app.state.connection_portal.add_log_entry(
|
|
1284
|
+
|
|
1285
|
+
app.state.connection_portal.add_log_entry(
|
|
1286
|
+
timestamp, level, message,
|
|
1287
|
+
);
|
|
775
1288
|
}
|
|
776
1289
|
}
|
|
777
1290
|
}
|
|
778
1291
|
}
|
|
779
1292
|
}
|
|
780
1293
|
}
|
|
781
|
-
|
|
782
|
-
app.add_log(format!(
|
|
783
|
-
|
|
1294
|
+
|
|
1295
|
+
app.add_log(format!(
|
|
1296
|
+
"✗ [{}] Gateway connection failed: {}",
|
|
1297
|
+
short_id, error_msg
|
|
1298
|
+
));
|
|
1299
|
+
|
|
784
1300
|
// CRITICAL: Use immediate render for instant error display
|
|
785
1301
|
app.request_immediate_render("gateway_connect_error");
|
|
786
1302
|
}
|
|
@@ -790,24 +1306,69 @@ fn main() -> Result<()> {
|
|
|
790
1306
|
app.state.setup_portal.detecting = false;
|
|
791
1307
|
app.state.setup_portal.detecting_since = None;
|
|
792
1308
|
app.state.setup_portal.error = Some(error_msg.clone());
|
|
793
|
-
app.add_log(format!(
|
|
1309
|
+
app.add_log(format!(
|
|
1310
|
+
"✗ [{}] Setup detection failed: {}",
|
|
1311
|
+
short_id, error_msg
|
|
1312
|
+
));
|
|
794
1313
|
}
|
|
795
1314
|
// Phase 2.1: Check if this is a response to gateway.health command
|
|
796
|
-
else if Some(&resp.id) == app.state.pending_gateway_health_id.as_ref()
|
|
1315
|
+
else if Some(&resp.id) == app.state.pending_gateway_health_id.as_ref()
|
|
1316
|
+
{
|
|
797
1317
|
app.state.pending_gateway_health_id = None;
|
|
798
1318
|
app.state.gateway_healthy = false;
|
|
799
1319
|
app.state.gateway_db_summary = None;
|
|
800
|
-
app.add_log(format!(
|
|
801
|
-
|
|
1320
|
+
app.add_log(format!(
|
|
1321
|
+
"⚠ [{}] Gateway health check failed: {}",
|
|
1322
|
+
short_id, error_msg
|
|
1323
|
+
));
|
|
1324
|
+
|
|
802
1325
|
// CRITICAL: Request render after health state change
|
|
803
1326
|
app.request_render("gateway_health_error");
|
|
804
|
-
}
|
|
805
|
-
|
|
1327
|
+
} else if Some(&resp.id)
|
|
1328
|
+
== app.state.pending_gateway_observability_id.as_ref()
|
|
1329
|
+
{
|
|
806
1330
|
app.state.pending_gateway_observability_id = None;
|
|
807
1331
|
app.state.portal_monitoring.loading = false;
|
|
808
1332
|
app.state.portal_observability_last_poll = Some(Instant::now());
|
|
809
1333
|
app.state.portal_monitoring.error = Some(error_msg.clone());
|
|
810
1334
|
app.request_render("portal_observability_err");
|
|
1335
|
+
} else if Some(&resp.id)
|
|
1336
|
+
== app.state.pending_monitoring_refresh_id.as_ref()
|
|
1337
|
+
{
|
|
1338
|
+
app.state.pending_monitoring_refresh_id = None;
|
|
1339
|
+
app.state.portal_monitoring.section_refresh_loading = None;
|
|
1340
|
+
app.state.portal_monitoring.error =
|
|
1341
|
+
Some(format!("monitoring.refresh failed: {}", error_msg));
|
|
1342
|
+
app.add_log(format!(
|
|
1343
|
+
"✗ [{}] monitoring.refresh failed: {}",
|
|
1344
|
+
short_id, error_msg
|
|
1345
|
+
));
|
|
1346
|
+
app.request_render("monitoring_refresh_err");
|
|
1347
|
+
} else if Some(&resp.id)
|
|
1348
|
+
== app.state.pending_monitoring_logs_id.as_ref()
|
|
1349
|
+
{
|
|
1350
|
+
app.state.pending_monitoring_logs_id = None;
|
|
1351
|
+
app.state.portal_monitoring.logs_fetch_loading = false;
|
|
1352
|
+
app.state.portal_monitoring.error =
|
|
1353
|
+
Some(format!("monitoring.logs failed: {}", error_msg));
|
|
1354
|
+
app.add_log(format!(
|
|
1355
|
+
"✗ [{}] monitoring.logs failed: {}",
|
|
1356
|
+
short_id, error_msg
|
|
1357
|
+
));
|
|
1358
|
+
app.request_render("monitoring_logs_err");
|
|
1359
|
+
} else if Some(&resp.id)
|
|
1360
|
+
== app.state.pending_monitoring_drill_id.as_ref()
|
|
1361
|
+
{
|
|
1362
|
+
app.state.pending_monitoring_drill_id = None;
|
|
1363
|
+
app.state.portal_monitoring.metrics_drill_loading = false;
|
|
1364
|
+
app.state.portal_monitoring.section_refresh_loading = None;
|
|
1365
|
+
app.state.portal_monitoring.error =
|
|
1366
|
+
Some(format!("monitoring.drill failed: {}", error_msg));
|
|
1367
|
+
app.add_log(format!(
|
|
1368
|
+
"✗ [{}] monitoring.drill failed: {}",
|
|
1369
|
+
short_id, error_msg
|
|
1370
|
+
));
|
|
1371
|
+
app.request_render("monitoring_drill_err");
|
|
811
1372
|
}
|
|
812
1373
|
// Generic error handling
|
|
813
1374
|
else {
|
|
@@ -823,12 +1384,10 @@ fn main() -> Result<()> {
|
|
|
823
1384
|
} else {
|
|
824
1385
|
""
|
|
825
1386
|
};
|
|
826
|
-
|
|
1387
|
+
|
|
827
1388
|
app.add_log(format!(
|
|
828
1389
|
"✗ [{}]{} Error: {}",
|
|
829
|
-
short_id,
|
|
830
|
-
command_hint,
|
|
831
|
-
error_msg
|
|
1390
|
+
short_id, command_hint, error_msg
|
|
832
1391
|
));
|
|
833
1392
|
}
|
|
834
1393
|
}
|
|
@@ -856,6 +1415,9 @@ fn main() -> Result<()> {
|
|
|
856
1415
|
if app.state.navigation.current_screen() == &Screen::PortalMonitoring
|
|
857
1416
|
&& app.state.operation_mode == OperationMode::Connected
|
|
858
1417
|
&& app.state.pending_gateway_observability_id.is_none()
|
|
1418
|
+
&& app.state.pending_monitoring_refresh_id.is_none()
|
|
1419
|
+
&& app.state.pending_monitoring_logs_id.is_none()
|
|
1420
|
+
&& app.state.pending_monitoring_drill_id.is_none()
|
|
859
1421
|
{
|
|
860
1422
|
let interval = Duration::from_secs(5);
|
|
861
1423
|
let due = app
|
|
@@ -869,7 +1431,7 @@ fn main() -> Result<()> {
|
|
|
869
1431
|
}
|
|
870
1432
|
}
|
|
871
1433
|
}
|
|
872
|
-
|
|
1434
|
+
|
|
873
1435
|
// ┌─────────────────────────────────────────────────────────┐
|
|
874
1436
|
// │ STEP 3: RENDER (conditional - only when needed) │
|
|
875
1437
|
// └─────────────────────────────────────────────────────────┘
|
|
@@ -892,24 +1454,28 @@ fn main() -> Result<()> {
|
|
|
892
1454
|
| crate::screens::Screen::SetupPortal
|
|
893
1455
|
| crate::screens::Screen::PortalMonitoring
|
|
894
1456
|
);
|
|
895
|
-
|
|
1457
|
+
|
|
896
1458
|
// Clear only on screen switch (but not when switching TO Setup Portal; we'll clear after autoresize)
|
|
897
1459
|
let is_switching = match (&previous_screen, ¤t_screen) {
|
|
898
1460
|
(Some(prev), current) => prev != current,
|
|
899
1461
|
(None, _) => true,
|
|
900
1462
|
};
|
|
901
|
-
let switching_to_setup_portal =
|
|
1463
|
+
let switching_to_setup_portal =
|
|
1464
|
+
is_switching && matches!(current_screen, crate::screens::Screen::SetupPortal);
|
|
902
1465
|
|
|
903
1466
|
if is_switching && !switching_to_setup_portal {
|
|
904
1467
|
#[cfg(debug_assertions)]
|
|
905
|
-
eprintln!(
|
|
1468
|
+
eprintln!(
|
|
1469
|
+
"[MAIN] SCREEN SWITCH: {:?} -> {:?} (clearing)",
|
|
1470
|
+
previous_screen, current_screen
|
|
1471
|
+
);
|
|
906
1472
|
debug_log::log_screen(
|
|
907
1473
|
&format!("{:?}", previous_screen.as_ref().unwrap_or(¤t_screen)),
|
|
908
1474
|
&format!("{:?}", current_screen),
|
|
909
1475
|
);
|
|
910
1476
|
terminal.clear()?;
|
|
911
1477
|
}
|
|
912
|
-
|
|
1478
|
+
|
|
913
1479
|
// Connection + Setup portals: autoresize every frame (windowed CMD/conhost buffer vs viewport drift).
|
|
914
1480
|
// Clear on Setup entry (generic switch clear is skipped for Setup); clear when terminal dimensions
|
|
915
1481
|
// change while either portal is open. See docs/SETUP-PORTAL-WINDOWED-FIX-PLAN.md.
|
|
@@ -945,7 +1511,7 @@ fn main() -> Result<()> {
|
|
|
945
1511
|
standalone_portal_last_terminal_dims = Some(dims);
|
|
946
1512
|
}
|
|
947
1513
|
}
|
|
948
|
-
|
|
1514
|
+
|
|
949
1515
|
let render_start = Instant::now();
|
|
950
1516
|
terminal.draw(|f| app.render(f))?;
|
|
951
1517
|
let render_duration = render_start.elapsed().as_millis() as u64;
|
|
@@ -953,11 +1519,11 @@ fn main() -> Result<()> {
|
|
|
953
1519
|
if on_standalone_portal {
|
|
954
1520
|
debug_log::log_flicker_debug("DRAW_DONE", &format!("ms={}", render_duration));
|
|
955
1521
|
}
|
|
956
|
-
|
|
1522
|
+
|
|
957
1523
|
// Update previous screen tracking
|
|
958
1524
|
previous_screen = Some(current_screen);
|
|
959
1525
|
}
|
|
960
|
-
|
|
1526
|
+
|
|
961
1527
|
// Setup Portal: if detection has been running too long, unstick so user can ESC or retry
|
|
962
1528
|
if app.state.navigation.current_screen() == &screens::Screen::SetupPortal
|
|
963
1529
|
&& app.state.setup_portal.detecting
|
|
@@ -966,13 +1532,14 @@ fn main() -> Result<()> {
|
|
|
966
1532
|
if since.elapsed() >= Duration::from_secs(8) {
|
|
967
1533
|
app.state.setup_portal.detecting = false;
|
|
968
1534
|
app.state.setup_portal.detecting_since = None;
|
|
969
|
-
app.state.setup_portal.error =
|
|
1535
|
+
app.state.setup_portal.error =
|
|
1536
|
+
Some("Detection timed out. Press ESC to close or try again.".to_string());
|
|
970
1537
|
app.add_log("[SETUP] Detection timed out".to_string());
|
|
971
1538
|
app.request_immediate_render("setup_detect_timeout");
|
|
972
1539
|
}
|
|
973
1540
|
}
|
|
974
1541
|
}
|
|
975
|
-
|
|
1542
|
+
|
|
976
1543
|
// ┌─────────────────────────────────────────────────────────┐
|
|
977
1544
|
// │ STEP 4: CHECK FOR INPUT (non-blocking with timeout) │
|
|
978
1545
|
// └─────────────────────────────────────────────────────────┘
|
|
@@ -1033,25 +1600,20 @@ fn main() -> Result<()> {
|
|
|
1033
1600
|
}
|
|
1034
1601
|
}
|
|
1035
1602
|
// If no input, loop continues immediately - animations keep running!
|
|
1036
|
-
|
|
1603
|
+
|
|
1037
1604
|
// Check for pending agent deletion (Step 5.5)
|
|
1038
1605
|
if app.state.agent_delete_requested {
|
|
1039
1606
|
app.state.agent_delete_requested = false;
|
|
1040
1607
|
app.delete_selected_agent(&ws_client);
|
|
1041
1608
|
}
|
|
1042
|
-
|
|
1609
|
+
|
|
1043
1610
|
// Check for IO updates (non-blocking)
|
|
1044
1611
|
io_handler.update(&mut app).ok();
|
|
1045
1612
|
}
|
|
1046
1613
|
|
|
1047
1614
|
// Restore terminal
|
|
1048
1615
|
disable_raw_mode()?;
|
|
1049
|
-
execute!(
|
|
1050
|
-
terminal.backend_mut(),
|
|
1051
|
-
cursor::Show,
|
|
1052
|
-
LeaveAlternateScreen
|
|
1053
|
-
)?;
|
|
1616
|
+
execute!(terminal.backend_mut(), cursor::Show, LeaveAlternateScreen)?;
|
|
1054
1617
|
|
|
1055
1618
|
Ok(())
|
|
1056
1619
|
}
|
|
1057
|
-
|