Skip to main content

roboticus_cli/cli/
schedule.rs

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}