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.
Files changed (51) hide show
  1. package/apps/gateway/dist/apps/gateway/src/index.js +14 -4
  2. package/apps/gateway/dist/apps/gateway/src/index.js.map +1 -1
  3. package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.d.ts +18 -0
  4. package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.d.ts.map +1 -0
  5. package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.js +117 -0
  6. package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.js.map +1 -0
  7. package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.d.ts +2 -0
  8. package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.d.ts.map +1 -0
  9. package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.js +54 -0
  10. package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.js.map +1 -0
  11. package/apps/gateway/dist/apps/gateway/src/routes/monitoring.d.ts +15 -0
  12. package/apps/gateway/dist/apps/gateway/src/routes/monitoring.d.ts.map +1 -0
  13. package/apps/gateway/dist/apps/gateway/src/routes/monitoring.js +164 -0
  14. package/apps/gateway/dist/apps/gateway/src/routes/monitoring.js.map +1 -0
  15. package/apps/gateway/package-lock.json +204 -353
  16. package/apps/gateway/src/index.ts +27 -8
  17. package/apps/gateway/src/metrics/monitoring-detail.ts +162 -0
  18. package/apps/gateway/src/middleware/log-capture.ts +70 -0
  19. package/apps/gateway/src/routes/monitoring.ts +298 -0
  20. package/dist/gateway-client.d.ts +2 -0
  21. package/dist/gateway-client.d.ts.map +1 -1
  22. package/dist/gateway-client.js +22 -0
  23. package/dist/gateway-client.js.map +1 -1
  24. package/dist/tui-handlers.js +498 -0
  25. package/dist/tui-handlers.js.map +1 -1
  26. package/mk3-tui/src/app/render_scheduler.rs +111 -112
  27. package/mk3-tui/src/app.rs +1078 -295
  28. package/mk3-tui/src/debug_log.rs +131 -124
  29. package/mk3-tui/src/io/mod.rs +63 -66
  30. package/mk3-tui/src/io/protocol.rs +14 -15
  31. package/mk3-tui/src/io/stdio.rs +31 -32
  32. package/mk3-tui/src/io/ws.rs +25 -32
  33. package/mk3-tui/src/main.rs +774 -212
  34. package/mk3-tui/src/monitoring/mod.rs +428 -0
  35. package/mk3-tui/src/screens/mod.rs +53 -39
  36. package/mk3-tui/src/storage/cache.rs +221 -224
  37. package/mk3-tui/src/storage/mod.rs +5 -6
  38. package/mk3-tui/src/ui/agent_builder.rs +1148 -922
  39. package/mk3-tui/src/ui/agent_list.rs +344 -295
  40. package/mk3-tui/src/ui/boot.rs +145 -148
  41. package/mk3-tui/src/ui/connection_portal.rs +121 -98
  42. package/mk3-tui/src/ui/help.rs +340 -284
  43. package/mk3-tui/src/ui/layout.rs +966 -803
  44. package/mk3-tui/src/ui/mod.rs +1 -1
  45. package/mk3-tui/src/ui/portal_monitoring.rs +1027 -147
  46. package/mk3-tui/src/ui/run_manager.rs +784 -764
  47. package/mk3-tui/src/ui/safe_viewport.rs +236 -235
  48. package/mk3-tui/src/ui/settings.rs +414 -362
  49. package/mk3-tui/src/ui/setup_portal.rs +158 -101
  50. package/mk3-tui/src/websocket.rs +315 -308
  51. package/package.json +2 -2
@@ -1,308 +1,315 @@
1
- /**
2
- * WebSocket Client for MK3 TUI
3
- * Connects to Node.js backend WebSocket server
4
- * Handles bidirectional JSON message communication
5
- */
6
-
7
- use anyhow::{Result, Context};
8
- use futures_util::{SinkExt, StreamExt};
9
- use serde::{Deserialize, Serialize};
10
- use tokio::sync::mpsc;
11
- use tokio_tungstenite::{connect_async, tungstenite::Message as WsMessage};
12
- use std::sync::{Arc, Mutex};
13
-
14
- // Message types from STEP-2-COMPLETE-SPECIFICATION.md
15
- #[derive(Debug, Clone, Serialize, Deserialize)]
16
- #[serde(tag = "type")]
17
- #[serde(rename_all = "lowercase")]
18
- pub enum Message {
19
- #[serde(rename = "command")]
20
- Command(CommandMessage),
21
- #[serde(rename = "response")]
22
- Response(ResponseMessage),
23
- #[serde(rename = "event")]
24
- Event(EventMessage),
25
- }
26
-
27
- #[derive(Debug, Clone, Serialize, Deserialize)]
28
- pub struct CommandMessage {
29
- pub id: String,
30
- pub timestamp: String,
31
- pub payload: CommandPayload,
32
- }
33
-
34
- #[derive(Debug, Clone, Serialize, Deserialize)]
35
- pub struct CommandPayload {
36
- pub command: String,
37
- #[serde(skip_serializing_if = "Option::is_none")]
38
- pub data: Option<serde_json::Value>,
39
- }
40
-
41
- #[derive(Debug, Clone, Serialize, Deserialize)]
42
- pub struct ResponseMessage {
43
- pub id: String,
44
- pub timestamp: String,
45
- pub payload: ResponsePayload,
46
- }
47
-
48
- #[derive(Debug, Clone, Serialize, Deserialize)]
49
- pub struct ResponsePayload {
50
- pub success: bool,
51
- #[serde(skip_serializing_if = "Option::is_none")]
52
- pub data: Option<serde_json::Value>,
53
- #[serde(skip_serializing_if = "Option::is_none")]
54
- pub error: Option<String>,
55
- }
56
-
57
- #[derive(Debug, Clone, Serialize, Deserialize)]
58
- pub struct EventMessage {
59
- pub timestamp: String,
60
- pub payload: EventPayload,
61
- }
62
-
63
- #[derive(Debug, Clone, Serialize, Deserialize)]
64
- pub struct EventPayload {
65
- pub event: String,
66
- pub data: serde_json::Value,
67
- }
68
-
69
- pub enum WsClientMessage {
70
- Connected,
71
- Disconnected,
72
- Response(ResponseMessage),
73
- Event(EventMessage),
74
- Error(String),
75
- ConnectionLost,
76
- #[allow(dead_code)]
77
- Reconnecting,
78
- #[allow(dead_code)]
79
- ReconnectFailed(String),
80
- }
81
-
82
- pub struct WebSocketClient {
83
- #[allow(dead_code)]
84
- url: String,
85
- tx: mpsc::UnboundedSender<WsMessage>,
86
- rx: Arc<Mutex<mpsc::UnboundedReceiver<WsClientMessage>>>,
87
- connected: Arc<Mutex<bool>>,
88
- pending_commands: Arc<Mutex<Vec<(String, String, Option<serde_json::Value>)>>>, // (id, command, data)
89
- }
90
-
91
- impl WebSocketClient {
92
- pub async fn connect(url: &str) -> Result<Self> {
93
- let (msg_tx, msg_rx) = mpsc::unbounded_channel::<WsMessage>();
94
- let (client_tx, client_rx) = mpsc::unbounded_channel::<WsClientMessage>();
95
-
96
- let url_clone = url.to_string();
97
- let connected = Arc::new(Mutex::new(false));
98
- let connected_clone = connected.clone();
99
-
100
- // Spawn WebSocket connection task
101
- tokio::spawn(async move {
102
- if let Err(e) = Self::run_connection(&url_clone, client_tx.clone(), msg_rx, connected_clone).await {
103
- let _ = client_tx.send(WsClientMessage::Error(e.to_string()));
104
- }
105
- });
106
-
107
- Ok(Self {
108
- url: url.to_string(),
109
- tx: msg_tx,
110
- rx: Arc::new(Mutex::new(client_rx)),
111
- connected,
112
- pending_commands: Arc::new(Mutex::new(Vec::new())),
113
- })
114
- }
115
-
116
- async fn run_connection(
117
- url: &str,
118
- client_tx: mpsc::UnboundedSender<WsClientMessage>,
119
- mut client_rx: mpsc::UnboundedReceiver<WsMessage>,
120
- connected: Arc<Mutex<bool>>,
121
- ) -> Result<()> {
122
- // Connect to WebSocket server
123
- let (ws_stream, _) = connect_async(url)
124
- .await
125
- .context("Failed to connect to WebSocket server")?;
126
-
127
- *connected.lock().unwrap() = true;
128
- let _ = client_tx.send(WsClientMessage::Connected);
129
-
130
- let (mut write, mut read) = ws_stream.split();
131
-
132
- // Spawn task to handle outgoing messages
133
- tokio::spawn(async move {
134
- while let Some(msg) = client_rx.recv().await {
135
- if let Err(e) = write.send(msg).await {
136
- eprintln!("[WebSocket] Error sending message: {}", e);
137
- break;
138
- }
139
- }
140
- });
141
-
142
- // Handle incoming messages
143
- while let Some(msg) = read.next().await {
144
- match msg {
145
- Ok(WsMessage::Text(text)) => {
146
- if let Err(e) = Self::handle_message(&text, &client_tx) {
147
- eprintln!("[WebSocket] Error handling message: {}", e);
148
- }
149
- }
150
- Ok(WsMessage::Close(_)) => {
151
- *connected.lock().unwrap() = false;
152
- let _ = client_tx.send(WsClientMessage::Disconnected);
153
- break;
154
- }
155
- Err(e) => {
156
- *connected.lock().unwrap() = false;
157
- let _ = client_tx.send(WsClientMessage::ConnectionLost);
158
- let _ = client_tx.send(WsClientMessage::Error(format!("Connection error: {}", e)));
159
- break;
160
- }
161
- _ => {}
162
- }
163
- }
164
-
165
- Ok(())
166
- }
167
-
168
- fn handle_message(
169
- text: &str,
170
- client_tx: &mpsc::UnboundedSender<WsClientMessage>,
171
- ) -> Result<()> {
172
- let message: Message = serde_json::from_str(text)
173
- .context("Failed to parse message")?;
174
-
175
- match message {
176
- Message::Response(resp) => {
177
- let _ = client_tx.send(WsClientMessage::Response(resp));
178
- }
179
- Message::Event(event) => {
180
- let _ = client_tx.send(WsClientMessage::Event(event));
181
- }
182
- Message::Command(_) => {
183
- // Client should not receive commands
184
- eprintln!("[WebSocket] Unexpected command message from server");
185
- }
186
- }
187
-
188
- Ok(())
189
- }
190
-
191
- pub fn send_command(&self, command: &str, data: Option<serde_json::Value>) -> Result<String> {
192
- let id = format!("req-{}", uuid::Uuid::new_v4());
193
- let timestamp = chrono::Utc::now().to_rfc3339();
194
-
195
- let cmd = Message::Command(CommandMessage {
196
- id: id.clone(),
197
- timestamp,
198
- payload: CommandPayload {
199
- command: command.to_string(),
200
- data: data.clone(),
201
- },
202
- });
203
-
204
- let json = serde_json::to_string(&cmd)?;
205
-
206
- // Track pending command for retry
207
- if self.is_connected() {
208
- self.pending_commands.lock().unwrap().push((id.clone(), command.to_string(), data));
209
- }
210
-
211
- self.tx.send(WsMessage::Text(json))?;
212
-
213
- Ok(id)
214
- }
215
-
216
- /// Retry pending commands after reconnection
217
- #[allow(dead_code)]
218
- pub fn retry_pending_commands(&self) -> Result<Vec<String>> {
219
- let mut pending = self.pending_commands.lock().unwrap();
220
- let mut retried_ids = Vec::new();
221
-
222
- for (old_id, command, data) in pending.drain(..) {
223
- // Generate new ID for retry
224
- let new_id = format!("retry-{}", uuid::Uuid::new_v4());
225
- let timestamp = chrono::Utc::now().to_rfc3339();
226
-
227
- let cmd = Message::Command(CommandMessage {
228
- id: new_id.clone(),
229
- timestamp,
230
- payload: CommandPayload {
231
- command,
232
- data,
233
- },
234
- });
235
-
236
- let json = serde_json::to_string(&cmd)?;
237
- self.tx.send(WsMessage::Text(json))?;
238
-
239
- retried_ids.push(format!("{} -> {}", &old_id[old_id.len().saturating_sub(8)..], &new_id[new_id.len().saturating_sub(8)..]));
240
- }
241
-
242
- Ok(retried_ids)
243
- }
244
-
245
- /// Clear pending commands (e.g., on explicit disconnect)
246
- #[allow(dead_code)]
247
- pub fn clear_pending_commands(&self) {
248
- self.pending_commands.lock().unwrap().clear();
249
- }
250
-
251
- pub fn try_recv(&self) -> Option<WsClientMessage> {
252
- self.rx.lock().unwrap().try_recv().ok()
253
- }
254
-
255
- pub fn is_connected(&self) -> bool {
256
- *self.connected.lock().unwrap()
257
- }
258
- }
259
-
260
- // Helper for creating UUID
261
- mod uuid {
262
- use std::sync::atomic::{AtomicU64, Ordering};
263
-
264
- static COUNTER: AtomicU64 = AtomicU64::new(0);
265
-
266
- pub struct Uuid;
267
-
268
- impl Uuid {
269
- pub fn new_v4() -> String {
270
- let count = COUNTER.fetch_add(1, Ordering::SeqCst);
271
- let timestamp = std::time::SystemTime::now()
272
- .duration_since(std::time::UNIX_EPOCH)
273
- .unwrap()
274
- .as_secs();
275
- format!("{:016x}{:016x}", timestamp, count)
276
- }
277
- }
278
- }
279
-
280
- // Helper for timestamps
281
- mod chrono {
282
- pub struct Utc;
283
-
284
- impl Utc {
285
- pub fn now() -> DateTime {
286
- DateTime
287
- }
288
- }
289
-
290
- pub struct DateTime;
291
-
292
- impl DateTime {
293
- pub fn to_rfc3339(&self) -> String {
294
- let now = std::time::SystemTime::now()
295
- .duration_since(std::time::UNIX_EPOCH)
296
- .unwrap();
297
-
298
- // Simple ISO 8601 format
299
- format!(
300
- "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
301
- 1970, 1, 1, // Simplified - real impl would calculate actual date
302
- (now.as_secs() / 3600) % 24,
303
- (now.as_secs() / 60) % 60,
304
- now.as_secs() % 60
305
- )
306
- }
307
- }
308
- }
1
+ /**
2
+ * WebSocket Client for MK3 TUI
3
+ * Connects to Node.js backend WebSocket server
4
+ * Handles bidirectional JSON message communication
5
+ */
6
+ use anyhow::{Context, Result};
7
+ use futures_util::{SinkExt, StreamExt};
8
+ use serde::{Deserialize, Serialize};
9
+ use std::sync::{Arc, Mutex};
10
+ use tokio::sync::mpsc;
11
+ use tokio_tungstenite::{connect_async, tungstenite::Message as WsMessage};
12
+
13
+ // Message types from STEP-2-COMPLETE-SPECIFICATION.md
14
+ #[derive(Debug, Clone, Serialize, Deserialize)]
15
+ #[serde(tag = "type")]
16
+ #[serde(rename_all = "lowercase")]
17
+ pub enum Message {
18
+ #[serde(rename = "command")]
19
+ Command(CommandMessage),
20
+ #[serde(rename = "response")]
21
+ Response(ResponseMessage),
22
+ #[serde(rename = "event")]
23
+ Event(EventMessage),
24
+ }
25
+
26
+ #[derive(Debug, Clone, Serialize, Deserialize)]
27
+ pub struct CommandMessage {
28
+ pub id: String,
29
+ pub timestamp: String,
30
+ pub payload: CommandPayload,
31
+ }
32
+
33
+ #[derive(Debug, Clone, Serialize, Deserialize)]
34
+ pub struct CommandPayload {
35
+ pub command: String,
36
+ #[serde(skip_serializing_if = "Option::is_none")]
37
+ pub data: Option<serde_json::Value>,
38
+ }
39
+
40
+ #[derive(Debug, Clone, Serialize, Deserialize)]
41
+ pub struct ResponseMessage {
42
+ pub id: String,
43
+ pub timestamp: String,
44
+ pub payload: ResponsePayload,
45
+ }
46
+
47
+ #[derive(Debug, Clone, Serialize, Deserialize)]
48
+ pub struct ResponsePayload {
49
+ pub success: bool,
50
+ #[serde(skip_serializing_if = "Option::is_none")]
51
+ pub data: Option<serde_json::Value>,
52
+ #[serde(skip_serializing_if = "Option::is_none")]
53
+ pub error: Option<String>,
54
+ }
55
+
56
+ #[derive(Debug, Clone, Serialize, Deserialize)]
57
+ pub struct EventMessage {
58
+ pub timestamp: String,
59
+ pub payload: EventPayload,
60
+ }
61
+
62
+ #[derive(Debug, Clone, Serialize, Deserialize)]
63
+ pub struct EventPayload {
64
+ pub event: String,
65
+ pub data: serde_json::Value,
66
+ }
67
+
68
+ pub enum WsClientMessage {
69
+ Connected,
70
+ Disconnected,
71
+ Response(ResponseMessage),
72
+ Event(EventMessage),
73
+ Error(String),
74
+ ConnectionLost,
75
+ #[allow(dead_code)]
76
+ Reconnecting,
77
+ #[allow(dead_code)]
78
+ ReconnectFailed(String),
79
+ }
80
+
81
+ pub struct WebSocketClient {
82
+ #[allow(dead_code)]
83
+ url: String,
84
+ tx: mpsc::UnboundedSender<WsMessage>,
85
+ rx: Arc<Mutex<mpsc::UnboundedReceiver<WsClientMessage>>>,
86
+ connected: Arc<Mutex<bool>>,
87
+ pending_commands: Arc<Mutex<Vec<(String, String, Option<serde_json::Value>)>>>, // (id, command, data)
88
+ }
89
+
90
+ impl WebSocketClient {
91
+ pub async fn connect(url: &str) -> Result<Self> {
92
+ let (msg_tx, msg_rx) = mpsc::unbounded_channel::<WsMessage>();
93
+ let (client_tx, client_rx) = mpsc::unbounded_channel::<WsClientMessage>();
94
+
95
+ let url_clone = url.to_string();
96
+ let connected = Arc::new(Mutex::new(false));
97
+ let connected_clone = connected.clone();
98
+
99
+ // Spawn WebSocket connection task
100
+ tokio::spawn(async move {
101
+ if let Err(e) =
102
+ Self::run_connection(&url_clone, client_tx.clone(), msg_rx, connected_clone).await
103
+ {
104
+ let _ = client_tx.send(WsClientMessage::Error(e.to_string()));
105
+ }
106
+ });
107
+
108
+ Ok(Self {
109
+ url: url.to_string(),
110
+ tx: msg_tx,
111
+ rx: Arc::new(Mutex::new(client_rx)),
112
+ connected,
113
+ pending_commands: Arc::new(Mutex::new(Vec::new())),
114
+ })
115
+ }
116
+
117
+ async fn run_connection(
118
+ url: &str,
119
+ client_tx: mpsc::UnboundedSender<WsClientMessage>,
120
+ mut client_rx: mpsc::UnboundedReceiver<WsMessage>,
121
+ connected: Arc<Mutex<bool>>,
122
+ ) -> Result<()> {
123
+ // Connect to WebSocket server
124
+ let (ws_stream, _) = connect_async(url)
125
+ .await
126
+ .context("Failed to connect to WebSocket server")?;
127
+
128
+ *connected.lock().unwrap() = true;
129
+ let _ = client_tx.send(WsClientMessage::Connected);
130
+
131
+ let (mut write, mut read) = ws_stream.split();
132
+
133
+ // Spawn task to handle outgoing messages
134
+ tokio::spawn(async move {
135
+ while let Some(msg) = client_rx.recv().await {
136
+ if let Err(e) = write.send(msg).await {
137
+ eprintln!("[WebSocket] Error sending message: {}", e);
138
+ break;
139
+ }
140
+ }
141
+ });
142
+
143
+ // Handle incoming messages
144
+ while let Some(msg) = read.next().await {
145
+ match msg {
146
+ Ok(WsMessage::Text(text)) => {
147
+ if let Err(e) = Self::handle_message(&text, &client_tx) {
148
+ eprintln!("[WebSocket] Error handling message: {}", e);
149
+ }
150
+ }
151
+ Ok(WsMessage::Close(_)) => {
152
+ *connected.lock().unwrap() = false;
153
+ let _ = client_tx.send(WsClientMessage::Disconnected);
154
+ break;
155
+ }
156
+ Err(e) => {
157
+ *connected.lock().unwrap() = false;
158
+ let _ = client_tx.send(WsClientMessage::ConnectionLost);
159
+ let _ =
160
+ client_tx.send(WsClientMessage::Error(format!("Connection error: {}", e)));
161
+ break;
162
+ }
163
+ _ => {}
164
+ }
165
+ }
166
+
167
+ Ok(())
168
+ }
169
+
170
+ fn handle_message(
171
+ text: &str,
172
+ client_tx: &mpsc::UnboundedSender<WsClientMessage>,
173
+ ) -> Result<()> {
174
+ let message: Message = serde_json::from_str(text).context("Failed to parse message")?;
175
+
176
+ match message {
177
+ Message::Response(resp) => {
178
+ let _ = client_tx.send(WsClientMessage::Response(resp));
179
+ }
180
+ Message::Event(event) => {
181
+ let _ = client_tx.send(WsClientMessage::Event(event));
182
+ }
183
+ Message::Command(_) => {
184
+ // Client should not receive commands
185
+ eprintln!("[WebSocket] Unexpected command message from server");
186
+ }
187
+ }
188
+
189
+ Ok(())
190
+ }
191
+
192
+ pub fn send_command(&self, command: &str, data: Option<serde_json::Value>) -> Result<String> {
193
+ let id = format!("req-{}", uuid::Uuid::new_v4());
194
+ let timestamp = chrono::Utc::now().to_rfc3339();
195
+
196
+ let cmd = Message::Command(CommandMessage {
197
+ id: id.clone(),
198
+ timestamp,
199
+ payload: CommandPayload {
200
+ command: command.to_string(),
201
+ data: data.clone(),
202
+ },
203
+ });
204
+
205
+ let json = serde_json::to_string(&cmd)?;
206
+
207
+ // Track pending command for retry
208
+ if self.is_connected() {
209
+ self.pending_commands
210
+ .lock()
211
+ .unwrap()
212
+ .push((id.clone(), command.to_string(), data));
213
+ }
214
+
215
+ self.tx.send(WsMessage::Text(json))?;
216
+
217
+ Ok(id)
218
+ }
219
+
220
+ /// Retry pending commands after reconnection
221
+ #[allow(dead_code)]
222
+ pub fn retry_pending_commands(&self) -> Result<Vec<String>> {
223
+ let mut pending = self.pending_commands.lock().unwrap();
224
+ let mut retried_ids = Vec::new();
225
+
226
+ for (old_id, command, data) in pending.drain(..) {
227
+ // Generate new ID for retry
228
+ let new_id = format!("retry-{}", uuid::Uuid::new_v4());
229
+ let timestamp = chrono::Utc::now().to_rfc3339();
230
+
231
+ let cmd = Message::Command(CommandMessage {
232
+ id: new_id.clone(),
233
+ timestamp,
234
+ payload: CommandPayload { command, data },
235
+ });
236
+
237
+ let json = serde_json::to_string(&cmd)?;
238
+ self.tx.send(WsMessage::Text(json))?;
239
+
240
+ retried_ids.push(format!(
241
+ "{} -> {}",
242
+ &old_id[old_id.len().saturating_sub(8)..],
243
+ &new_id[new_id.len().saturating_sub(8)..]
244
+ ));
245
+ }
246
+
247
+ Ok(retried_ids)
248
+ }
249
+
250
+ /// Clear pending commands (e.g., on explicit disconnect)
251
+ #[allow(dead_code)]
252
+ pub fn clear_pending_commands(&self) {
253
+ self.pending_commands.lock().unwrap().clear();
254
+ }
255
+
256
+ pub fn try_recv(&self) -> Option<WsClientMessage> {
257
+ self.rx.lock().unwrap().try_recv().ok()
258
+ }
259
+
260
+ pub fn is_connected(&self) -> bool {
261
+ *self.connected.lock().unwrap()
262
+ }
263
+ }
264
+
265
+ // Helper for creating UUID
266
+ mod uuid {
267
+ use std::sync::atomic::{AtomicU64, Ordering};
268
+
269
+ static COUNTER: AtomicU64 = AtomicU64::new(0);
270
+
271
+ pub struct Uuid;
272
+
273
+ impl Uuid {
274
+ pub fn new_v4() -> String {
275
+ let count = COUNTER.fetch_add(1, Ordering::SeqCst);
276
+ let timestamp = std::time::SystemTime::now()
277
+ .duration_since(std::time::UNIX_EPOCH)
278
+ .unwrap()
279
+ .as_secs();
280
+ format!("{:016x}{:016x}", timestamp, count)
281
+ }
282
+ }
283
+ }
284
+
285
+ // Helper for timestamps
286
+ mod chrono {
287
+ pub struct Utc;
288
+
289
+ impl Utc {
290
+ pub fn now() -> DateTime {
291
+ DateTime
292
+ }
293
+ }
294
+
295
+ pub struct DateTime;
296
+
297
+ impl DateTime {
298
+ pub fn to_rfc3339(&self) -> String {
299
+ let now = std::time::SystemTime::now()
300
+ .duration_since(std::time::UNIX_EPOCH)
301
+ .unwrap();
302
+
303
+ // Simple ISO 8601 format
304
+ format!(
305
+ "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
306
+ 1970,
307
+ 1,
308
+ 1, // Simplified - real impl would calculate actual date
309
+ (now.as_secs() / 3600) % 24,
310
+ (now.as_secs() / 60) % 60,
311
+ now.as_secs() % 60
312
+ )
313
+ }
314
+ }
315
+ }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "4runr-os",
3
- "version": "2.10.39",
3
+ "version": "2.10.41",
4
4
  "type": "module",
5
- "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.10.39: Gateway /ready gates Postgres+Redis only (queue = degraded, fixes launch/connect); health polish. v2.10.38: readiness + monitoring.",
5
+ "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.10.41: Stabilizes Portal Monitoring refresh/log shortcuts and log scrolling. v2.10.40: Portal Monitoring redesign with sections, drill-down, history, system diagnostics, and help overlay.",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
8
  "4runr": "dist/index.js",