4runr-os 2.3.9 → 2.4.1
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/mk3-tui/src/app.rs +91 -16
- package/mk3-tui/src/main.rs +62 -9
- package/mk3-tui/src/storage/cache.rs +213 -0
- package/mk3-tui/src/storage/mod.rs +6 -0
- package/mk3-tui/src/ui/agent_builder.rs +921 -530
- package/mk3-tui/src/ui/layout.rs +65 -3
- package/mk3-tui/src/ui/run_manager.rs +676 -401
- package/mk3-tui/src/ui/settings.rs +362 -347
- package/mk3-tui/src/websocket.rs +303 -258
- package/package.json +1 -1
package/mk3-tui/src/app.rs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
use crate::io::IoHandler;
|
|
2
2
|
use crate::screens::{NavigationState, Screen};
|
|
3
|
+
use crate::storage::Cache;
|
|
3
4
|
use crate::ui::agent_builder::AgentBuilderState;
|
|
4
5
|
use crate::ui::run_manager::RunManagerState;
|
|
5
6
|
use crate::ui::settings::SettingsState;
|
|
@@ -95,6 +96,10 @@ pub struct AppState {
|
|
|
95
96
|
pub agent_builder: AgentBuilderState,
|
|
96
97
|
pub run_manager: RunManagerState,
|
|
97
98
|
pub settings: SettingsState,
|
|
99
|
+
|
|
100
|
+
// Local cache
|
|
101
|
+
pub cache: Option<Cache>,
|
|
102
|
+
pub cache_loaded: bool,
|
|
98
103
|
}
|
|
99
104
|
|
|
100
105
|
impl Default for AppState {
|
|
@@ -148,6 +153,8 @@ impl Default for AppState {
|
|
|
148
153
|
agent_builder: AgentBuilderState::default(),
|
|
149
154
|
run_manager: RunManagerState::default(),
|
|
150
155
|
settings: SettingsState::default(),
|
|
156
|
+
cache: Cache::new().ok(),
|
|
157
|
+
cache_loaded: false,
|
|
151
158
|
}
|
|
152
159
|
}
|
|
153
160
|
}
|
|
@@ -170,14 +177,37 @@ impl App {
|
|
|
170
177
|
RunMode::Local => Duration::from_millis(25),
|
|
171
178
|
};
|
|
172
179
|
|
|
180
|
+
let mut state = AppState::default();
|
|
181
|
+
Self::load_cached_data(&mut state);
|
|
182
|
+
|
|
173
183
|
Self {
|
|
174
|
-
state
|
|
184
|
+
state,
|
|
175
185
|
render_scheduler,
|
|
176
186
|
input_debounce: None,
|
|
177
187
|
input_debounce_duration,
|
|
178
188
|
}
|
|
179
189
|
}
|
|
180
190
|
|
|
191
|
+
/// Load cached data on startup
|
|
192
|
+
fn load_cached_data(state: &mut AppState) {
|
|
193
|
+
if let Some(cache) = &state.cache {
|
|
194
|
+
// Load cached agents
|
|
195
|
+
let agents = cache.get_agents();
|
|
196
|
+
if !agents.is_empty() {
|
|
197
|
+
state.capabilities = agents.iter()
|
|
198
|
+
.map(|a| a.name.clone())
|
|
199
|
+
.collect();
|
|
200
|
+
state.cache_loaded = true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Load cached system status
|
|
204
|
+
if let Some(status) = cache.get_system_status() {
|
|
205
|
+
state.network_status = if status.connected { "Connected (cached)".to_string() } else { "Disconnected (cached)".to_string() };
|
|
206
|
+
state.posture_status = format!("{} (cached)", status.posture);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
181
211
|
pub fn request_render(&mut self, reason: &str) -> bool {
|
|
182
212
|
self.render_scheduler.request_render(reason)
|
|
183
213
|
}
|
|
@@ -336,19 +366,33 @@ impl App {
|
|
|
336
366
|
self.state.logs.push_back("[HELP] 4Runr AI Agent OS - Command Reference".into());
|
|
337
367
|
self.state.logs.push_back("[HELP] ═══════════════════════════════════════".into());
|
|
338
368
|
self.state.logs.push_back("".into());
|
|
369
|
+
|
|
370
|
+
// Local Commands
|
|
339
371
|
self.state.logs.push_back("[HELP] Local Commands:".into());
|
|
340
372
|
self.state.logs.push_back(" quit, exit - Exit application".into());
|
|
341
373
|
self.state.logs.push_back(" clear - Clear logs".into());
|
|
342
374
|
self.state.logs.push_back(" help - Show this help".into());
|
|
343
375
|
self.state.logs.push_back(" :perf - Show performance stats".into());
|
|
344
376
|
self.state.logs.push_back("".into());
|
|
377
|
+
|
|
378
|
+
// Navigation Commands
|
|
345
379
|
self.state.logs.push_back("[HELP] Navigation Commands:".into());
|
|
346
380
|
self.state.logs.push_back(" build - Open Agent Builder (6-step wizard)".into());
|
|
347
381
|
self.state.logs.push_back(" runs - Open Run Manager (list, filter, sort)".into());
|
|
348
382
|
self.state.logs.push_back(" config, settings - Open Settings (mode, AI provider)".into());
|
|
349
383
|
self.state.logs.push_back(" ESC - Close overlay/popup or clear input".into());
|
|
350
384
|
self.state.logs.push_back("".into());
|
|
385
|
+
|
|
386
|
+
// Screen Controls
|
|
387
|
+
self.state.logs.push_back("[HELP] Screen Controls:".into());
|
|
388
|
+
self.state.logs.push_back(" Agent Builder: Enter=Next, Backspace=Prev, ESC=Cancel".into());
|
|
389
|
+
self.state.logs.push_back(" Run Manager: ↑/↓=Navigate, F=Filter, S=Sort, R=Refresh".into());
|
|
390
|
+
self.state.logs.push_back(" Settings: ↑/↓=Navigate, Space=Toggle, Enter=Save".into());
|
|
391
|
+
self.state.logs.push_back("".into());
|
|
392
|
+
|
|
393
|
+
// WebSocket Commands - Label BEFORE separator
|
|
351
394
|
self.state.logs.push_back("[HELP] WebSocket Commands (requires connection):".into());
|
|
395
|
+
self.state.logs.push_back("[HELP] ─────────────────────────────────────────".into());
|
|
352
396
|
self.state.logs.push_back(" agent.list - List all agents".into());
|
|
353
397
|
self.state.logs.push_back(" agent.get - Get agent details (data: {name})".into());
|
|
354
398
|
self.state.logs.push_back(" agent.create - Create agent (use Agent Builder)".into());
|
|
@@ -357,11 +401,7 @@ impl App {
|
|
|
357
401
|
self.state.logs.push_back(" run.list - List runs (requires gateway)".into());
|
|
358
402
|
self.state.logs.push_back(" tool.list - List available tools".into());
|
|
359
403
|
self.state.logs.push_back("".into());
|
|
360
|
-
|
|
361
|
-
self.state.logs.push_back(" Agent Builder: Enter=Next, Backspace=Prev, ESC=Cancel".into());
|
|
362
|
-
self.state.logs.push_back(" Run Manager: ↑/↓=Navigate, F=Filter, S=Sort, R=Refresh".into());
|
|
363
|
-
self.state.logs.push_back(" Settings: ↑/↓=Navigate, Space=Toggle, Enter=Save".into());
|
|
364
|
-
self.state.logs.push_back("".into());
|
|
404
|
+
|
|
365
405
|
self.state.logs.push_back("[HELP] Press F12 to toggle performance overlay".into());
|
|
366
406
|
self.state.logs.push_back("[HELP] ═══════════════════════════════════════".into());
|
|
367
407
|
}
|
|
@@ -680,6 +720,29 @@ impl App {
|
|
|
680
720
|
self.handle_agent_builder_backspace();
|
|
681
721
|
self.request_render("agent_builder_backspace");
|
|
682
722
|
}
|
|
723
|
+
// Left Arrow - go back to previous step
|
|
724
|
+
KeyCode::Left => {
|
|
725
|
+
if self.state.agent_builder.current_step > 1 {
|
|
726
|
+
self.state.agent_builder.prev_step();
|
|
727
|
+
self.add_log("Agent Builder: Moved to previous step".to_string());
|
|
728
|
+
self.request_render("agent_builder_prev_step");
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
// Right Arrow - move to next step (same as Enter)
|
|
732
|
+
KeyCode::Right => {
|
|
733
|
+
if self.state.agent_builder.current_step == 6 {
|
|
734
|
+
// Final step - create agent
|
|
735
|
+
self.create_agent_from_builder(ws_client);
|
|
736
|
+
} else {
|
|
737
|
+
// Move to next step
|
|
738
|
+
if self.state.agent_builder.next_step() {
|
|
739
|
+
self.request_render("agent_builder_next");
|
|
740
|
+
} else {
|
|
741
|
+
// Validation failed - errors are in state
|
|
742
|
+
self.request_render("agent_builder_validation");
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
683
746
|
// Tab - move to next field (within step)
|
|
684
747
|
KeyCode::Tab => {
|
|
685
748
|
let shift = key.modifiers.contains(KeyModifiers::SHIFT);
|
|
@@ -971,31 +1034,35 @@ impl App {
|
|
|
971
1034
|
// Filter
|
|
972
1035
|
KeyCode::Char('f') | KeyCode::Char('F') => {
|
|
973
1036
|
self.state.run_manager.next_filter();
|
|
974
|
-
self.add_log(format!("[RUN] Filter: {}", self.state.run_manager.filter.as_str()));
|
|
975
1037
|
self.request_render("run_manager_filter");
|
|
976
1038
|
}
|
|
977
1039
|
|
|
978
1040
|
// Sort
|
|
979
1041
|
KeyCode::Char('s') | KeyCode::Char('S') => {
|
|
980
1042
|
self.state.run_manager.next_sort();
|
|
981
|
-
self.add_log(format!("[RUN] Sort: {}", self.state.run_manager.sort.as_str()));
|
|
982
1043
|
self.request_render("run_manager_sort");
|
|
983
1044
|
}
|
|
984
1045
|
|
|
985
1046
|
// Refresh
|
|
986
1047
|
KeyCode::Char('r') | KeyCode::Char('R') => {
|
|
987
|
-
self.add_log("[RUN] Refreshing run list...".to_string());
|
|
988
1048
|
// TODO: Send run.list command via WebSocket
|
|
989
1049
|
self.request_render("run_manager_refresh");
|
|
990
1050
|
}
|
|
991
1051
|
|
|
992
|
-
// View details
|
|
1052
|
+
// View details / Close detail view
|
|
993
1053
|
KeyCode::Enter => {
|
|
994
|
-
if
|
|
995
|
-
|
|
996
|
-
|
|
1054
|
+
if self.state.run_manager.is_detail_view() {
|
|
1055
|
+
// Close detail view
|
|
1056
|
+
self.state.run_manager.close_detail_view();
|
|
1057
|
+
self.add_log("[RUN] Closed detail view".to_string());
|
|
997
1058
|
} else {
|
|
998
|
-
|
|
1059
|
+
// Open detail view
|
|
1060
|
+
if let Some(run) = self.state.run_manager.selected_run() {
|
|
1061
|
+
self.add_log(format!("[RUN] Viewing run: {}", run.name));
|
|
1062
|
+
self.state.run_manager.toggle_detail_view();
|
|
1063
|
+
} else {
|
|
1064
|
+
self.add_log("[RUN] No run selected".to_string());
|
|
1065
|
+
}
|
|
999
1066
|
}
|
|
1000
1067
|
self.request_render("run_manager_view");
|
|
1001
1068
|
}
|
|
@@ -1022,9 +1089,17 @@ impl App {
|
|
|
1022
1089
|
self.request_render("run_manager_delete");
|
|
1023
1090
|
}
|
|
1024
1091
|
|
|
1025
|
-
// Close
|
|
1092
|
+
// Close detail view or Run Manager
|
|
1026
1093
|
KeyCode::Esc => {
|
|
1027
|
-
self.
|
|
1094
|
+
if self.state.run_manager.is_detail_view() {
|
|
1095
|
+
// Close detail view
|
|
1096
|
+
self.state.run_manager.close_detail_view();
|
|
1097
|
+
self.add_log("[RUN] Closed detail view".to_string());
|
|
1098
|
+
self.request_render("run_manager_close_detail");
|
|
1099
|
+
} else {
|
|
1100
|
+
// Close Run Manager
|
|
1101
|
+
self.pop_overlay();
|
|
1102
|
+
}
|
|
1028
1103
|
}
|
|
1029
1104
|
|
|
1030
1105
|
_ => {}
|
package/mk3-tui/src/main.rs
CHANGED
|
@@ -9,6 +9,7 @@ use std::time::{Duration, Instant};
|
|
|
9
9
|
mod app;
|
|
10
10
|
mod io;
|
|
11
11
|
mod screens;
|
|
12
|
+
mod storage;
|
|
12
13
|
mod ui;
|
|
13
14
|
mod websocket;
|
|
14
15
|
|
|
@@ -99,7 +100,18 @@ fn main() -> Result<()> {
|
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
102
|
WsClientMessage::Disconnected => {
|
|
102
|
-
app.add_log("
|
|
103
|
+
app.add_log("[WEBSOCKET] Disconnected from server".to_string());
|
|
104
|
+
app.state.connected = false;
|
|
105
|
+
}
|
|
106
|
+
WsClientMessage::ConnectionLost => {
|
|
107
|
+
app.add_log("[WEBSOCKET] Connection lost - attempting to reconnect...".to_string());
|
|
108
|
+
app.state.connected = false;
|
|
109
|
+
}
|
|
110
|
+
WsClientMessage::Reconnecting => {
|
|
111
|
+
app.add_log("[WEBSOCKET] Reconnecting...".to_string());
|
|
112
|
+
}
|
|
113
|
+
WsClientMessage::ReconnectFailed(reason) => {
|
|
114
|
+
app.add_log(format!("[WEBSOCKET] Reconnection failed: {}", reason));
|
|
103
115
|
}
|
|
104
116
|
WsClientMessage::Response(resp) => {
|
|
105
117
|
if resp.payload.success {
|
|
@@ -139,11 +151,35 @@ fn main() -> Result<()> {
|
|
|
139
151
|
})
|
|
140
152
|
.collect();
|
|
141
153
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
154
|
+
// Update cache with agent data
|
|
155
|
+
if let Some(cache) = &mut app.state.cache {
|
|
156
|
+
use crate::storage::cache::AgentData;
|
|
157
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
158
|
+
|
|
159
|
+
let agents: Vec<AgentData> = agents_array.iter()
|
|
160
|
+
.filter_map(|agent| {
|
|
161
|
+
let obj = agent.as_object()?;
|
|
162
|
+
Some(AgentData {
|
|
163
|
+
name: obj.get("name")?.as_str()?.to_string(),
|
|
164
|
+
description: obj.get("description").and_then(|d| d.as_str()).map(|s| s.to_string()),
|
|
165
|
+
model: obj.get("model").and_then(|m| m.as_str()).unwrap_or("unknown").to_string(),
|
|
166
|
+
provider: obj.get("provider").and_then(|p| p.as_str()).unwrap_or("unknown").to_string(),
|
|
167
|
+
created_at: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(),
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
.collect();
|
|
171
|
+
|
|
172
|
+
let _ = cache.update_agents(agents);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Only log if agents were actually loaded
|
|
176
|
+
if agents_array.len() > 0 {
|
|
177
|
+
app.add_log(format!(
|
|
178
|
+
"✓ [{}] Loaded {} agents",
|
|
179
|
+
short_id,
|
|
180
|
+
agents_array.len()
|
|
181
|
+
));
|
|
182
|
+
}
|
|
147
183
|
}
|
|
148
184
|
} else {
|
|
149
185
|
app.add_log(format!("✓ [{}] Success", short_id));
|
|
@@ -156,10 +192,26 @@ fn main() -> Result<()> {
|
|
|
156
192
|
}
|
|
157
193
|
} else {
|
|
158
194
|
let short_id = &resp.id[resp.id.len().saturating_sub(8)..];
|
|
195
|
+
let error_msg = resp.payload.error.unwrap_or_else(|| "Unknown error".to_string());
|
|
196
|
+
|
|
197
|
+
// Try to extract command name from response for better error messages
|
|
198
|
+
let command_hint = if resp.id.contains("agent") {
|
|
199
|
+
" (agent command)"
|
|
200
|
+
} else if resp.id.contains("system") {
|
|
201
|
+
" (system command)"
|
|
202
|
+
} else if resp.id.contains("run") {
|
|
203
|
+
" (run command)"
|
|
204
|
+
} else if resp.id.contains("tool") {
|
|
205
|
+
" (tool command)"
|
|
206
|
+
} else {
|
|
207
|
+
""
|
|
208
|
+
};
|
|
209
|
+
|
|
159
210
|
app.add_log(format!(
|
|
160
|
-
"✗ [{}] Error: {}",
|
|
211
|
+
"✗ [{}]{} Error: {}",
|
|
161
212
|
short_id,
|
|
162
|
-
|
|
213
|
+
command_hint,
|
|
214
|
+
error_msg
|
|
163
215
|
));
|
|
164
216
|
}
|
|
165
217
|
}
|
|
@@ -171,7 +223,8 @@ fn main() -> Result<()> {
|
|
|
171
223
|
));
|
|
172
224
|
}
|
|
173
225
|
WsClientMessage::Error(err) => {
|
|
174
|
-
app.add_log(format!("
|
|
226
|
+
app.add_log(format!("[WEBSOCKET] ✗ Error: {}", err));
|
|
227
|
+
app.state.connected = false;
|
|
175
228
|
}
|
|
176
229
|
}
|
|
177
230
|
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/// Cache implementation for local data storage
|
|
2
|
+
/// Stores agents, runs, and system status in JSON format
|
|
3
|
+
|
|
4
|
+
use serde::{Deserialize, Serialize};
|
|
5
|
+
use std::fs;
|
|
6
|
+
use std::path::PathBuf;
|
|
7
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
8
|
+
|
|
9
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
10
|
+
pub struct AgentData {
|
|
11
|
+
pub name: String,
|
|
12
|
+
pub description: Option<String>,
|
|
13
|
+
pub model: String,
|
|
14
|
+
pub provider: String,
|
|
15
|
+
pub created_at: u64,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
19
|
+
pub struct RunData {
|
|
20
|
+
pub id: String,
|
|
21
|
+
pub name: String,
|
|
22
|
+
pub agent: String,
|
|
23
|
+
pub status: String,
|
|
24
|
+
pub started_at: u64,
|
|
25
|
+
pub duration: Option<u64>,
|
|
26
|
+
pub tokens_used: Option<u64>,
|
|
27
|
+
pub cost: Option<f64>,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
31
|
+
pub struct SystemStatusData {
|
|
32
|
+
pub connected: bool,
|
|
33
|
+
pub gateway_url: Option<String>,
|
|
34
|
+
pub posture: String,
|
|
35
|
+
pub last_updated: u64,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
39
|
+
pub struct CacheData {
|
|
40
|
+
pub agents: Vec<AgentData>,
|
|
41
|
+
pub runs: Vec<RunData>,
|
|
42
|
+
pub system_status: Option<SystemStatusData>,
|
|
43
|
+
pub last_updated: u64,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#[derive(Debug, Clone)]
|
|
47
|
+
pub struct Cache {
|
|
48
|
+
cache_path: PathBuf,
|
|
49
|
+
data: CacheData,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
impl Cache {
|
|
53
|
+
/// Create a new cache instance
|
|
54
|
+
pub fn new() -> Result<Self, std::io::Error> {
|
|
55
|
+
let cache_dir = Self::get_cache_dir()?;
|
|
56
|
+
fs::create_dir_all(&cache_dir)?;
|
|
57
|
+
|
|
58
|
+
let cache_path = cache_dir.join("4runr_cache.json");
|
|
59
|
+
let data = Self::load_from_file(&cache_path)?;
|
|
60
|
+
|
|
61
|
+
Ok(Self { cache_path, data })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Get the cache directory path
|
|
65
|
+
fn get_cache_dir() -> Result<PathBuf, std::io::Error> {
|
|
66
|
+
#[cfg(target_os = "windows")]
|
|
67
|
+
{
|
|
68
|
+
let appdata = std::env::var("APPDATA")
|
|
69
|
+
.unwrap_or_else(|_| String::from("C:\\Users\\Default\\AppData\\Roaming"));
|
|
70
|
+
Ok(PathBuf::from(appdata).join("4runr"))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#[cfg(not(target_os = "windows"))]
|
|
74
|
+
{
|
|
75
|
+
let home = std::env::var("HOME")
|
|
76
|
+
.unwrap_or_else(|_| String::from("/tmp"));
|
|
77
|
+
Ok(PathBuf::from(home).join(".4runr"))
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Load cache from file
|
|
82
|
+
fn load_from_file(path: &PathBuf) -> Result<CacheData, std::io::Error> {
|
|
83
|
+
if !path.exists() {
|
|
84
|
+
return Ok(CacheData::default());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let contents = fs::read_to_string(path)?;
|
|
88
|
+
let data: CacheData = serde_json::from_str(&contents)
|
|
89
|
+
.unwrap_or_default();
|
|
90
|
+
|
|
91
|
+
Ok(data)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// Save cache to file
|
|
95
|
+
fn save_to_file(&self) -> Result<(), std::io::Error> {
|
|
96
|
+
let json = serde_json::to_string_pretty(&self.data)?;
|
|
97
|
+
fs::write(&self.cache_path, json)?;
|
|
98
|
+
Ok(())
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Get current timestamp
|
|
102
|
+
fn now() -> u64 {
|
|
103
|
+
SystemTime::now()
|
|
104
|
+
.duration_since(UNIX_EPOCH)
|
|
105
|
+
.unwrap()
|
|
106
|
+
.as_secs()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// === Agent Operations ===
|
|
110
|
+
|
|
111
|
+
/// Get all cached agents
|
|
112
|
+
pub fn get_agents(&self) -> Vec<AgentData> {
|
|
113
|
+
self.data.agents.clone()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Update agents cache
|
|
117
|
+
pub fn update_agents(&mut self, agents: Vec<AgentData>) -> Result<(), std::io::Error> {
|
|
118
|
+
self.data.agents = agents;
|
|
119
|
+
self.data.last_updated = Self::now();
|
|
120
|
+
self.save_to_file()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Add a single agent
|
|
124
|
+
pub fn add_agent(&mut self, agent: AgentData) -> Result<(), std::io::Error> {
|
|
125
|
+
self.data.agents.push(agent);
|
|
126
|
+
self.data.last_updated = Self::now();
|
|
127
|
+
self.save_to_file()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Remove an agent by name
|
|
131
|
+
pub fn remove_agent(&mut self, name: &str) -> Result<(), std::io::Error> {
|
|
132
|
+
self.data.agents.retain(|a| a.name != name);
|
|
133
|
+
self.data.last_updated = Self::now();
|
|
134
|
+
self.save_to_file()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// === Run Operations ===
|
|
138
|
+
|
|
139
|
+
/// Get all cached runs
|
|
140
|
+
pub fn get_runs(&self) -> Vec<RunData> {
|
|
141
|
+
self.data.runs.clone()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/// Update runs cache
|
|
145
|
+
pub fn update_runs(&mut self, runs: Vec<RunData>) -> Result<(), std::io::Error> {
|
|
146
|
+
self.data.runs = runs;
|
|
147
|
+
self.data.last_updated = Self::now();
|
|
148
|
+
self.save_to_file()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Add a single run
|
|
152
|
+
pub fn add_run(&mut self, run: RunData) -> Result<(), std::io::Error> {
|
|
153
|
+
self.data.runs.push(run);
|
|
154
|
+
self.data.last_updated = Self::now();
|
|
155
|
+
self.save_to_file()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// Update a run by ID
|
|
159
|
+
pub fn update_run(&mut self, id: &str, run: RunData) -> Result<(), std::io::Error> {
|
|
160
|
+
if let Some(existing) = self.data.runs.iter_mut().find(|r| r.id == id) {
|
|
161
|
+
*existing = run;
|
|
162
|
+
self.data.last_updated = Self::now();
|
|
163
|
+
self.save_to_file()?;
|
|
164
|
+
}
|
|
165
|
+
Ok(())
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// === System Status Operations ===
|
|
169
|
+
|
|
170
|
+
/// Get cached system status
|
|
171
|
+
pub fn get_system_status(&self) -> Option<SystemStatusData> {
|
|
172
|
+
self.data.system_status.clone()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/// Update system status cache
|
|
176
|
+
pub fn update_system_status(&mut self, status: SystemStatusData) -> Result<(), std::io::Error> {
|
|
177
|
+
self.data.system_status = Some(status);
|
|
178
|
+
self.data.last_updated = Self::now();
|
|
179
|
+
self.save_to_file()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// === Cache Management ===
|
|
183
|
+
|
|
184
|
+
/// Get cache age in seconds
|
|
185
|
+
pub fn get_age(&self) -> u64 {
|
|
186
|
+
Self::now().saturating_sub(self.data.last_updated)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// Check if cache is stale (older than threshold)
|
|
190
|
+
pub fn is_stale(&self, threshold_secs: u64) -> bool {
|
|
191
|
+
self.get_age() > threshold_secs
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/// Clear all cached data
|
|
195
|
+
pub fn clear(&mut self) -> Result<(), std::io::Error> {
|
|
196
|
+
self.data = CacheData::default();
|
|
197
|
+
self.save_to_file()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/// Get cache data reference
|
|
201
|
+
pub fn data(&self) -> &CacheData {
|
|
202
|
+
&self.data
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
impl Default for Cache {
|
|
207
|
+
fn default() -> Self {
|
|
208
|
+
Self::new().unwrap_or_else(|_| Self {
|
|
209
|
+
cache_path: PathBuf::from("4runr_cache.json"),
|
|
210
|
+
data: CacheData::default(),
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
}
|