diff --git a/examples/leaderboard.rs b/examples/leaderboard.rs index 1019c92..25795a6 100644 --- a/examples/leaderboard.rs +++ b/examples/leaderboard.rs @@ -4,7 +4,7 @@ use job_watcher::{ AppBuilder, Heartbeat, TaskLabel, WatchedTask, WatchedTaskOutput, WatcherAppContext, config::{Delay, TaskConfig, WatcherConfig}, }; -use std::sync::Arc; +use std::{convert::Infallible, sync::Arc, time::Duration}; use tokio::net::TcpListener; #[tokio::main] @@ -29,6 +29,7 @@ impl DummyApp { builder.watch_periodic(TaskLabel::new("leaderboard"), LeaderBoard)?; builder.watch_periodic(TaskLabel::new("task_two"), TaskTwo)?; + builder.watch_background_with_status(TaskLabel::new("task_three"), run_task_three)?; builder.wait(listener).await } @@ -125,3 +126,15 @@ async fn update_task_two() -> Result { println!("Finished executing task two."); Ok(WatchedTaskOutput::new("Finished executing task two")) } + +async fn run_task_three(_app: Arc, heartbeat: Heartbeat) -> Result { + let mut i = 1; + loop { + println!("task three"); + heartbeat + .set_status(format!("Status from task three {i}")) + .await; + i += 1; + tokio::time::sleep(Duration::from_secs(1)).await; + } +} diff --git a/src/lib.rs b/src/lib.rs index 2a22c48..342996e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,6 +80,7 @@ pub enum TaskResultValue { Ok(Cow<'static, str>), Err(String), NotYetRun, + Info(Cow<'static, str>), } const NOT_YET_RUN_MESSAGE: &str = "Task has not yet completed a single run"; @@ -90,8 +91,13 @@ impl TaskResultValue { TaskResultValue::Ok(s) => s, TaskResultValue::Err(s) => s, TaskResultValue::NotYetRun => NOT_YET_RUN_MESSAGE, + TaskResultValue::Info(s) => s, } } + + pub fn is_info(&self) -> bool { + matches!(self, TaskResultValue::Info(_)) + } } #[derive(Clone, serde::Serialize, Debug)] @@ -163,6 +169,7 @@ enum ShortStatus { OutOfDateNoAlert, Success, NotYetRun, + Info, } impl ShortStatus { @@ -175,6 +182,7 @@ impl ShortStatus { ShortStatus::Error => "ERROR", ShortStatus::ErrorNoAlert => "ERROR (no alert)", ShortStatus::NotYetRun => "NOT YET RUN", + ShortStatus::Info => "RUNNING", } } @@ -187,6 +195,7 @@ impl ShortStatus { ShortStatus::OutOfDateNoAlert => false, ShortStatus::Success => false, ShortStatus::NotYetRun => false, + ShortStatus::Info => false, } } @@ -199,6 +208,7 @@ impl ShortStatus { ShortStatus::OutOfDateNoAlert => "text-red-300", ShortStatus::Success => "link-success", ShortStatus::NotYetRun => "link-primary", + ShortStatus::Info => "link-primary", } } } @@ -436,6 +446,7 @@ impl ResponseBuilder { writeln!(&mut self.buffer, "{err}")?; } TaskResultValue::NotYetRun => writeln!(&mut self.buffer, "{}", NOT_YET_RUN_MESSAGE)?, + TaskResultValue::Info(cow) => writeln!(&mut self.buffer, "{cow}")?, } writeln!(&mut self.buffer)?; @@ -521,6 +532,17 @@ impl TaskStatus { (OutOfDateType::Very, true) => ShortStatus::OutOfDateError, } } + TaskResultValue::Info(_) => { + match ( + self.is_out_of_date(), + app.triggers_alert(label, selected_label), + ) { + (OutOfDateType::Not, _) => ShortStatus::Info, + (_, false) => ShortStatus::OutOfDateNoAlert, + (OutOfDateType::Slightly, true) => ShortStatus::OutOfDate, + (OutOfDateType::Very, true) => ShortStatus::OutOfDateError, + } + } TaskResultValue::Err(_) => { if app.triggers_alert(label, selected_label) { if self.is_expired() { @@ -582,6 +604,16 @@ pub struct Heartbeat { pub task_status: Arc>, } +impl Heartbeat { + pub async fn set_status(&self, message: impl Into>) { + let mut guard = self.task_status.write().await; + guard.last_result = TaskResult { + value: TaskResultValue::Info(message.into()).into(), + updated: Zoned::now(), + }; + } +} + #[derive(Debug)] pub struct WatchedTaskOutput { /// Should we skip delay between tasks ? If yes, then we dont @@ -679,6 +711,48 @@ impl AppBuilder { self.watcher.set.spawn(task); } + /// Watch a background job that runs continuously, with status reporting. + /// + /// This is similar to `watch_background`, but it also registers the task + /// with the status monitoring page. The provided closure is given a + /// `Heartbeat` instance that can be used to update the task's status. + pub fn watch_background_with_status(&mut self, label: TaskLabel, f: F) -> Result<()> + where + F: FnOnce(Arc, Heartbeat) -> Fut + Send + 'static, + Fut: std::future::Future> + Send + 'static, + { + let task_status = Arc::new(RwLock::new(TaskStatus { + last_result: TaskResult { + value: TaskResultValue::NotYetRun.into(), + updated: Zoned::now(), + }, + last_retry_error: None, + current_run_started: Some(Zoned::now()), + out_of_date: None, + counts: Default::default(), + expire_last_result: None, + last_run_seconds: None, + })); + { + let old = self + .watcher + .statuses + .insert(label.clone(), task_status.clone()); + if old.is_some() { + anyhow::bail!("Two tasks with label {label:?}"); + } + } + let heartbeat = Heartbeat { task_status }; + let app = self.app.clone(); + let future = f(app, heartbeat); + self.watcher.set.spawn(async move { + future + .await + .with_context(|| format!("Background task failed: {}", label)) + }); + Ok(()) + } + pub fn watch_periodic(&mut self, label: TaskLabel, mut task: T) -> Result<()> where T: WatchedTask, @@ -777,6 +851,7 @@ impl AppBuilder { TaskResultValue::NotYetRun => { // Catalog newly started } + TaskResultValue::Info(_cow) => {} } } let last_run_seconds = { diff --git a/templates/status.html b/templates/status.html index 84b61f5..c0cba5c 100644 --- a/templates/status.html +++ b/templates/status.html @@ -12,8 +12,8 @@ body { font-family: 'Inter', sans-serif; } - - /* + + /* Compatibility Layer for Backend Classes The Rust backend returns specific class names like 'link-danger', 'text-red-400', etc. We map these to Tailwind values or custom styles here to ensure they look good. @@ -47,8 +47,8 @@ color: #1d4ed8; /* blue-700 */ } - /* - Red Shade Mapping + /* + Red Shade Mapping The backend uses text-red-100 to text-red-800. We map them to standard Tailwind colors, ensuring visibility. */ @@ -73,7 +73,7 @@
- +
@@ -83,7 +83,7 @@

- +
@@ -138,7 +138,7 @@

Task Overview

{% for status in statuses %}
-
+

{{ status.label }} @@ -156,8 +156,9 @@

- + + {% if status.status.last_result.value.is_info() == false %}
Successes
@@ -172,6 +173,7 @@

{{ status.status.counts.errors }}

+ {% endif %} {% if let Some(started) = status.status.current_run_started.as_ref() %} @@ -229,7 +231,7 @@

Retry in progress

{% endfor %}
- +