1use super::*;
2
3pub async fn cmd_schedule_list(url: &str, json: bool) -> Result<(), Box<dyn std::error::Error>> {
4 let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
5 let (OK, ACTION, WARN, DETAIL, ERR) = icons();
6 let c = RoboticusClient::new(url)?;
7 let data = c.get("/api/cron/jobs").await.map_err(|e| {
8 RoboticusClient::check_connectivity_hint(&*e);
9 e
10 })?;
11 if json {
12 println!("{}", serde_json::to_string_pretty(&data)?);
13 return Ok(());
14 }
15 heading("Cron Jobs");
16 let jobs = data["jobs"].as_array();
17 match jobs {
18 Some(arr) if !arr.is_empty() => {
19 let widths = [22, 24, 12, 22, 10, 8];
20 table_header(
21 &["Name", "Intent", "Schedule", "Last Run", "Status", "Errors"],
22 &widths,
23 );
24 for j in arr {
25 let name = j["name"].as_str().unwrap_or("").to_string();
26 let intent = j["description"]
27 .as_str()
28 .map(|d| d.trim())
29 .filter(|d| !d.is_empty())
30 .unwrap_or("no description");
31 let kind = j["schedule_kind"].as_str().unwrap_or("?");
32 let expr = j["schedule_expr"].as_str().unwrap_or("");
33 let schedule = format!("{kind}: {expr}");
34 let last_run = j["last_run_at"]
35 .as_str()
36 .map(|t| if t.len() > 19 { &t[..19] } else { t })
37 .unwrap_or("never")
38 .to_string();
39 let status = j["last_status"].as_str().unwrap_or("pending");
40 let errors = j["consecutive_errors"].as_i64().unwrap_or(0);
41 table_row(
42 &[
43 format!("{ACCENT}{name}{RESET}"),
44 format!("{DIM}{}{RESET}", truncate_id(intent, 24)),
45 truncate_id(&schedule, 12),
46 format!("{DIM}{last_run}{RESET}"),
47 status_badge(status),
48 if errors > 0 {
49 format!("{RED}{errors}{RESET}")
50 } else {
51 format!("{DIM}0{RESET}")
52 },
53 ],
54 &widths,
55 );
56 }
57 eprintln!();
58 eprintln!(" {DIM}{} job(s){RESET}", arr.len());
59 }
60 _ => empty_state("No cron jobs configured"),
61 }
62 eprintln!();
63 Ok(())
64}
65
66pub async fn cmd_schedule_recover(
67 url: &str,
68 names: &[String],
69 all: bool,
70 dry_run: bool,
71 json: bool,
72) -> Result<(), Box<dyn std::error::Error>> {
73 let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
74 let (OK, ACTION, WARN, DETAIL, ERR) = icons();
75
76 let c = RoboticusClient::new(url)?;
77 let data = c.get("/api/cron/jobs").await.map_err(|e| {
78 RoboticusClient::check_connectivity_hint(&*e);
79 e
80 })?;
81 if json {
82 println!("{}", serde_json::to_string_pretty(&data)?);
83 return Ok(());
84 }
85 let jobs = data["jobs"].as_array().cloned().unwrap_or_default();
86
87 let paused: Vec<Value> = jobs
88 .into_iter()
89 .filter(|j| j["last_status"].as_str() == Some("paused_unknown_action"))
90 .collect();
91
92 if paused.is_empty() {
93 heading("Schedule Recovery");
94 empty_state("No paused cron jobs found.");
95 eprintln!();
96 return Ok(());
97 }
98
99 let selected: Vec<Value> = if all {
100 paused.clone()
101 } else if !names.is_empty() {
102 paused
103 .iter()
104 .filter(|j| {
105 let job_name = j["name"].as_str().unwrap_or_default();
106 names.iter().any(|n| n == job_name)
107 })
108 .cloned()
109 .collect()
110 } else {
111 heading("Schedule Recovery");
112 eprintln!(" {WARN} Found {} paused job(s).{RESET}", paused.len());
113 eprintln!(
114 " {DIM}Use {BOLD}roboticus schedule recover --all{RESET}{DIM} to re-enable all, or {BOLD}--name <job>{RESET}{DIM} to select specific jobs.{RESET}"
115 );
116 eprintln!();
117 let widths = [36, 10, 22];
118 table_header(&["Name", "Enabled", "Last Run"], &widths);
119 for j in &paused {
120 let name = j["name"].as_str().unwrap_or("").to_string();
121 let enabled = j["enabled"].as_bool().unwrap_or(false);
122 let last_run = j["last_run_at"]
123 .as_str()
124 .map(|t| if t.len() > 19 { &t[..19] } else { t })
125 .unwrap_or("never")
126 .to_string();
127 table_row(
128 &[
129 format!("{ACCENT}{name}{RESET}"),
130 if enabled {
131 format!("{GREEN}true{RESET}")
132 } else {
133 format!("{YELLOW}false{RESET}")
134 },
135 format!("{DIM}{last_run}{RESET}"),
136 ],
137 &widths,
138 );
139 }
140 eprintln!();
141 return Ok(());
142 };
143
144 if selected.is_empty() {
145 heading("Schedule Recovery");
146 eprintln!(" {ERR} No paused jobs matched the provided name filter.{RESET}");
147 eprintln!();
148 return Ok(());
149 }
150
151 heading("Schedule Recovery");
152 eprintln!(
153 " {ACTION} {} job(s) selected for re-enable{}",
154 selected.len(),
155 if dry_run { " (dry-run)" } else { "" }
156 );
157 eprintln!();
158
159 let widths = [36, 12, 10];
160 table_header(&["Name", "Job ID", "Result"], &widths);
161
162 for j in selected {
163 let id = j["id"].as_str().unwrap_or_default();
164 let name = j["name"].as_str().unwrap_or_default();
165 let result = if dry_run {
166 format!("{CYAN}would-enable{RESET}")
167 } else {
168 match c
169 .put(
170 &format!("/api/cron/jobs/{id}"),
171 serde_json::json!({ "enabled": true }),
172 )
173 .await
174 {
175 Ok(_) => format!("{GREEN}enabled{RESET}"),
176 Err(e) => format!("{RED}failed: {}{RESET}", truncate_id(&e.to_string(), 40)),
177 }
178 };
179 table_row(
180 &[
181 format!("{ACCENT}{name}{RESET}"),
182 format!("{MONO}{}{RESET}", truncate_id(id, 12)),
183 result,
184 ],
185 &widths,
186 );
187 }
188 eprintln!();
189 Ok(())
190}
191
192pub async fn cmd_schedule_run(
193 url: &str,
194 name_or_id: &str,
195 json: bool,
196) -> Result<(), Box<dyn std::error::Error>> {
197 let (_DIM, _BOLD, ACCENT, GREEN, _YELLOW, RED, _CYAN, RESET, _MONO) = colors();
198 let (_OK, ACTION, _WARN, _DETAIL, _ERR) = icons();
199 let c = RoboticusClient::new(url)?;
200 let data = c.get("/api/cron/jobs").await?;
201 let jobs = data["jobs"].as_array().cloned().unwrap_or_default();
202 let Some(job) = jobs
203 .into_iter()
204 .find(|j| j["id"].as_str() == Some(name_or_id) || j["name"].as_str() == Some(name_or_id))
205 else {
206 return Err(format!("cron job not found: {name_or_id}").into());
207 };
208 let id = job["id"].as_str().unwrap_or_default();
209 let name = job["name"].as_str().unwrap_or_default();
210 heading("Schedule Run");
211 eprintln!(" {ACTION} Running {ACCENT}{name}{RESET} now...");
212 let body = c
213 .post(&format!("/api/cron/jobs/{id}/run"), serde_json::json!({}))
214 .await?;
215 if json {
216 println!("{}", serde_json::to_string_pretty(&body)?);
217 return Ok(());
218 }
219 let status = body["status"].as_str().unwrap_or("unknown");
220 let error = body["detail"].as_str().unwrap_or("");
221 let output_text = body["output_text"].as_str().unwrap_or("").trim();
222 if status == "success" {
223 eprintln!(" {GREEN}Ran successfully{RESET}");
224 if !output_text.is_empty() {
225 eprintln!(" Output");
226 eprintln!(" {}", output_text.replace('\n', "\n "));
227 }
228 } else {
229 eprintln!(" {RED}Run failed:{RESET} {}", error);
230 }
231 eprintln!();
232 Ok(())
233}