4runr-os 2.3.9 → 2.4.0
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 +86 -8
- package/mk3-tui/src/main.rs +54 -4
- 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 +421 -30
- package/mk3-tui/src/ui/layout.rs +1 -1
- package/mk3-tui/src/ui/run_manager.rs +346 -71
- package/mk3-tui/src/ui/settings.rs +57 -42
- package/mk3-tui/src/websocket.rs +47 -2
- 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,27 @@ 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
|
+
// WebSocket Commands - Label BEFORE commands with clear separator
|
|
387
|
+
self.state.logs.push_back("[HELP] ─────────────────────────────────────────".into());
|
|
351
388
|
self.state.logs.push_back("[HELP] WebSocket Commands (requires connection):".into());
|
|
389
|
+
self.state.logs.push_back("[HELP] ─────────────────────────────────────────".into());
|
|
352
390
|
self.state.logs.push_back(" agent.list - List all agents".into());
|
|
353
391
|
self.state.logs.push_back(" agent.get - Get agent details (data: {name})".into());
|
|
354
392
|
self.state.logs.push_back(" agent.create - Create agent (use Agent Builder)".into());
|
|
@@ -357,6 +395,8 @@ impl App {
|
|
|
357
395
|
self.state.logs.push_back(" run.list - List runs (requires gateway)".into());
|
|
358
396
|
self.state.logs.push_back(" tool.list - List available tools".into());
|
|
359
397
|
self.state.logs.push_back("".into());
|
|
398
|
+
|
|
399
|
+
// Screen Controls
|
|
360
400
|
self.state.logs.push_back("[HELP] Screen Controls:".into());
|
|
361
401
|
self.state.logs.push_back(" Agent Builder: Enter=Next, Backspace=Prev, ESC=Cancel".into());
|
|
362
402
|
self.state.logs.push_back(" Run Manager: ↑/↓=Navigate, F=Filter, S=Sort, R=Refresh".into());
|
|
@@ -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);
|
|
@@ -989,13 +1052,20 @@ impl App {
|
|
|
989
1052
|
self.request_render("run_manager_refresh");
|
|
990
1053
|
}
|
|
991
1054
|
|
|
992
|
-
// View details
|
|
1055
|
+
// View details / Close detail view
|
|
993
1056
|
KeyCode::Enter => {
|
|
994
|
-
if
|
|
995
|
-
|
|
996
|
-
|
|
1057
|
+
if self.state.run_manager.is_detail_view() {
|
|
1058
|
+
// Close detail view
|
|
1059
|
+
self.state.run_manager.close_detail_view();
|
|
1060
|
+
self.add_log("[RUN] Closed detail view".to_string());
|
|
997
1061
|
} else {
|
|
998
|
-
|
|
1062
|
+
// Open detail view
|
|
1063
|
+
if let Some(run) = self.state.run_manager.selected_run() {
|
|
1064
|
+
self.add_log(format!("[RUN] Viewing run: {}", run.name));
|
|
1065
|
+
self.state.run_manager.toggle_detail_view();
|
|
1066
|
+
} else {
|
|
1067
|
+
self.add_log("[RUN] No run selected".to_string());
|
|
1068
|
+
}
|
|
999
1069
|
}
|
|
1000
1070
|
self.request_render("run_manager_view");
|
|
1001
1071
|
}
|
|
@@ -1022,9 +1092,17 @@ impl App {
|
|
|
1022
1092
|
self.request_render("run_manager_delete");
|
|
1023
1093
|
}
|
|
1024
1094
|
|
|
1025
|
-
// Close
|
|
1095
|
+
// Close detail view or Run Manager
|
|
1026
1096
|
KeyCode::Esc => {
|
|
1027
|
-
self.
|
|
1097
|
+
if self.state.run_manager.is_detail_view() {
|
|
1098
|
+
// Close detail view
|
|
1099
|
+
self.state.run_manager.close_detail_view();
|
|
1100
|
+
self.add_log("[RUN] Closed detail view".to_string());
|
|
1101
|
+
self.request_render("run_manager_close_detail");
|
|
1102
|
+
} else {
|
|
1103
|
+
// Close Run Manager
|
|
1104
|
+
self.pop_overlay();
|
|
1105
|
+
}
|
|
1028
1106
|
}
|
|
1029
1107
|
|
|
1030
1108
|
_ => {}
|
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,6 +151,27 @@ fn main() -> Result<()> {
|
|
|
139
151
|
})
|
|
140
152
|
.collect();
|
|
141
153
|
|
|
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
|
+
|
|
142
175
|
app.add_log(format!(
|
|
143
176
|
"✓ [{}] Loaded {} agents",
|
|
144
177
|
short_id,
|
|
@@ -156,10 +189,26 @@ fn main() -> Result<()> {
|
|
|
156
189
|
}
|
|
157
190
|
} else {
|
|
158
191
|
let short_id = &resp.id[resp.id.len().saturating_sub(8)..];
|
|
192
|
+
let error_msg = resp.payload.error.unwrap_or_else(|| "Unknown error".to_string());
|
|
193
|
+
|
|
194
|
+
// Try to extract command name from response for better error messages
|
|
195
|
+
let command_hint = if resp.id.contains("agent") {
|
|
196
|
+
" (agent command)"
|
|
197
|
+
} else if resp.id.contains("system") {
|
|
198
|
+
" (system command)"
|
|
199
|
+
} else if resp.id.contains("run") {
|
|
200
|
+
" (run command)"
|
|
201
|
+
} else if resp.id.contains("tool") {
|
|
202
|
+
" (tool command)"
|
|
203
|
+
} else {
|
|
204
|
+
""
|
|
205
|
+
};
|
|
206
|
+
|
|
159
207
|
app.add_log(format!(
|
|
160
|
-
"✗ [{}] Error: {}",
|
|
208
|
+
"✗ [{}]{} Error: {}",
|
|
161
209
|
short_id,
|
|
162
|
-
|
|
210
|
+
command_hint,
|
|
211
|
+
error_msg
|
|
163
212
|
));
|
|
164
213
|
}
|
|
165
214
|
}
|
|
@@ -171,7 +220,8 @@ fn main() -> Result<()> {
|
|
|
171
220
|
));
|
|
172
221
|
}
|
|
173
222
|
WsClientMessage::Error(err) => {
|
|
174
|
-
app.add_log(format!("
|
|
223
|
+
app.add_log(format!("[WEBSOCKET] ✗ Error: {}", err));
|
|
224
|
+
app.state.connected = false;
|
|
175
225
|
}
|
|
176
226
|
}
|
|
177
227
|
}
|
|
@@ -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
|
+
}
|