From c23b05e0729c9108d163326177c946066264c6c8 Mon Sep 17 00:00:00 2001 From: Sibi Prabakaran Date: Wed, 11 Feb 2026 09:19:36 +0530 Subject: [PATCH 1/4] feat: add background task status tracking The `AppBuilder` now supports continuous background tasks that report their execution status. This feature allows long-running processes to be monitored via the status registry by initializing and storing task metadata before spawning the future into the runtime. --- src/lib.rs | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 2a22c48..7cb5839 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -679,6 +679,44 @@ 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, task: Fut) -> Result<()> + where + 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:?}"); + } + } + self.watcher.set.spawn(async move { + task + .await + .with_context(|| format!("Background task failed: {}", label)) + }); + Ok(()) + } + pub fn watch_periodic(&mut self, label: TaskLabel, mut task: T) -> Result<()> where T: WatchedTask, From 2889261c2bc87d02bfb31c6ab8ee8e1d54edf4f7 Mon Sep 17 00:00:00 2001 From: Sibi Prabakaran Date: Wed, 11 Feb 2026 09:43:05 +0530 Subject: [PATCH 2/4] Add have special TaskResultValue for background tasks --- src/lib.rs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7cb5839..2256b31 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,6 +91,7 @@ impl TaskResultValue { TaskResultValue::Ok(s) => s, TaskResultValue::Err(s) => s, TaskResultValue::NotYetRun => NOT_YET_RUN_MESSAGE, + TaskResultValue::Info(s) => s, } } } @@ -436,6 +438,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)?; @@ -510,7 +513,7 @@ impl TaskStatus { selected_label: Option<&TaskLabel>, ) -> ShortStatus { match self.last_result.value.as_ref() { - TaskResultValue::Ok(_) => { + TaskResultValue::Ok(_) | TaskResultValue::Info(_) => { match ( self.is_out_of_date(), app.triggers_alert(label, selected_label), @@ -582,6 +585,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 @@ -684,8 +697,9 @@ impl AppBuilder { /// 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, task: Fut) -> Result<()> + pub fn watch_background_with_status(&mut self, label: TaskLabel, f: F) -> Result<()> where + F: FnOnce(Heartbeat) -> Fut + Send + 'static, Fut: std::future::Future> + Send + 'static, { let task_status = Arc::new(RwLock::new(TaskStatus { @@ -709,8 +723,10 @@ impl AppBuilder { anyhow::bail!("Two tasks with label {label:?}"); } } + let heartbeat = Heartbeat { task_status }; + let future = f(heartbeat); self.watcher.set.spawn(async move { - task + future .await .with_context(|| format!("Background task failed: {}", label)) }); @@ -815,6 +831,7 @@ impl AppBuilder { TaskResultValue::NotYetRun => { // Catalog newly started } + TaskResultValue::Info(_cow) => {} } } let last_run_seconds = { From 879511bd2886d8644607c5cd995218f2fc736705 Mon Sep 17 00:00:00 2001 From: Sibi Prabakaran Date: Wed, 11 Feb 2026 10:01:29 +0530 Subject: [PATCH 3/4] feat: enhance background task status reporting The update introduces a dedicated Info status for tasks, primarily targeting background workers that provide continuous status updates. This includes UI changes to highlight these tasks and hide irrelevant success/error metrics for persistent jobs. --- examples/leaderboard.rs | 15 ++++++++++++++- src/lib.rs | 21 ++++++++++++++++++++- templates/status.html | 20 +++++++++++--------- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/examples/leaderboard.rs b/examples/leaderboard.rs index 1019c92..eeee275 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(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 2256b31..b8be41a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -94,6 +94,10 @@ impl TaskResultValue { TaskResultValue::Info(s) => s, } } + + pub fn is_info(&self) -> bool { + matches!(self, TaskResultValue::Info(_)) + } } #[derive(Clone, serde::Serialize, Debug)] @@ -165,6 +169,7 @@ enum ShortStatus { OutOfDateNoAlert, Success, NotYetRun, + Info, } impl ShortStatus { @@ -177,6 +182,7 @@ impl ShortStatus { ShortStatus::Error => "ERROR", ShortStatus::ErrorNoAlert => "ERROR (no alert)", ShortStatus::NotYetRun => "NOT YET RUN", + ShortStatus::Info => "RUNNING", } } @@ -189,6 +195,7 @@ impl ShortStatus { ShortStatus::OutOfDateNoAlert => false, ShortStatus::Success => false, ShortStatus::NotYetRun => false, + ShortStatus::Info => false, } } @@ -201,6 +208,7 @@ impl ShortStatus { ShortStatus::OutOfDateNoAlert => "text-red-300", ShortStatus::Success => "link-success", ShortStatus::NotYetRun => "link-primary", + ShortStatus::Info => "link-primary", } } } @@ -513,7 +521,7 @@ impl TaskStatus { selected_label: Option<&TaskLabel>, ) -> ShortStatus { match self.last_result.value.as_ref() { - TaskResultValue::Ok(_) | TaskResultValue::Info(_) => { + TaskResultValue::Ok(_) => { match ( self.is_out_of_date(), app.triggers_alert(label, selected_label), @@ -524,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() { 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 %}
- + From 215754d81c4789f9ab7e28395580055e825f54f2 Mon Sep 17 00:00:00 2001 From: Sibi Prabakaran Date: Wed, 11 Feb 2026 10:27:38 +0530 Subject: [PATCH 4/4] feat: pass app context to status background tasks Pass the application context to the closure in the background task watcher. This allows tasks that report their status to interact with shared application state, mirroring the behavior of standard background tasks. --- examples/leaderboard.rs | 2 +- src/lib.rs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/leaderboard.rs b/examples/leaderboard.rs index eeee275..25795a6 100644 --- a/examples/leaderboard.rs +++ b/examples/leaderboard.rs @@ -127,7 +127,7 @@ async fn update_task_two() -> Result { Ok(WatchedTaskOutput::new("Finished executing task two")) } -async fn run_task_three(heartbeat: Heartbeat) -> Result { +async fn run_task_three(_app: Arc, heartbeat: Heartbeat) -> Result { let mut i = 1; loop { println!("task three"); diff --git a/src/lib.rs b/src/lib.rs index b8be41a..342996e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -718,7 +718,7 @@ impl AppBuilder { /// `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(Heartbeat) -> Fut + Send + 'static, + F: FnOnce(Arc, Heartbeat) -> Fut + Send + 'static, Fut: std::future::Future> + Send + 'static, { let task_status = Arc::new(RwLock::new(TaskStatus { @@ -743,7 +743,8 @@ impl AppBuilder { } } let heartbeat = Heartbeat { task_status }; - let future = f(heartbeat); + let app = self.app.clone(); + let future = f(app, heartbeat); self.watcher.set.spawn(async move { future .await