mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-09-02 17:49:20 +00:00
feat(web): DB-backed Jobs tab + scheduler-dispatched local jobs (#436)
* feat(web): DB-backed Jobs tab, scheduler-dispatched local jobs, remove active-jobs-bar - Remove active-jobs-bar UI element (HTML, CSS, JS polling) - Move job handlers from server.rs to handlers/jobs.rs - Remove user_id scoping (single-user gateway) - Add list_agent_jobs() and agent_job_summary() to Database trait (both postgres and libsql backends) for non-sandbox job visibility - Wire SchedulerSlot into CreateJobTool so execute_local dispatches via scheduler (persists to DB + spawns worker) instead of creating phantom ContextManager-only jobs - Update /status and /list slash commands to read from DB for consistency with Jobs tab - Fix worker mark_completed: skip if already terminal or stuck - Add agent job cancel via DB update in both web handler and slash cmd - Add Stuck → Completed guard with tracing in worker completion path Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix: address PR review comments - Log warning when get_context fails in worker completion path - Extract duplicated status-counting logic into AgentJobSummary::add_count() helper, used by both postgres and libsql backends Co-Authored-By: Claude Sonnet 4.6 <[email protected]> --------- Co-authored-by: Claude Sonnet 4.6 <[email protected]> Co-authored-by: Nick Pismenkov <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
Nick Pismenkov
parent
afb49597ac
commit
6481448d50
+2
-2
@@ -14,6 +14,6 @@ pub use analytics::{JobStats, ToolStats};
|
||||
#[cfg(feature = "postgres")]
|
||||
pub use store::Store;
|
||||
pub use store::{
|
||||
ConversationMessage, ConversationSummary, JobEventRecord, LlmCallRecord, SandboxJobRecord,
|
||||
SandboxJobSummary, SettingRow,
|
||||
AgentJobRecord, AgentJobSummary, ConversationMessage, ConversationSummary, JobEventRecord,
|
||||
LlmCallRecord, SandboxJobRecord, SandboxJobSummary, SettingRow,
|
||||
};
|
||||
|
||||
@@ -487,6 +487,45 @@ pub struct SandboxJobSummary {
|
||||
pub interrupted: usize,
|
||||
}
|
||||
|
||||
/// Lightweight record for agent (non-sandbox) jobs, used by the web Jobs tab.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AgentJobRecord {
|
||||
pub id: Uuid,
|
||||
pub title: String,
|
||||
pub status: String,
|
||||
pub user_id: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub started_at: Option<DateTime<Utc>>,
|
||||
pub completed_at: Option<DateTime<Utc>>,
|
||||
pub failure_reason: Option<String>,
|
||||
}
|
||||
|
||||
/// Summary counts for agent (non-sandbox) jobs.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct AgentJobSummary {
|
||||
pub total: usize,
|
||||
pub pending: usize,
|
||||
pub in_progress: usize,
|
||||
pub completed: usize,
|
||||
pub failed: usize,
|
||||
pub stuck: usize,
|
||||
}
|
||||
|
||||
impl AgentJobSummary {
|
||||
/// Accumulate a status/count pair into the summary buckets.
|
||||
pub fn add_count(&mut self, status: &str, count: usize) {
|
||||
self.total += count;
|
||||
match status {
|
||||
"pending" => self.pending += count,
|
||||
"in_progress" => self.in_progress += count,
|
||||
"completed" | "submitted" | "accepted" => self.completed += count,
|
||||
"failed" | "cancelled" => self.failed += count,
|
||||
"stuck" => self.stuck += count,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
impl Store {
|
||||
/// Insert a new sandbox job into `agent_jobs`.
|
||||
@@ -754,6 +793,55 @@ impl Store {
|
||||
}
|
||||
Ok(summary)
|
||||
}
|
||||
|
||||
/// List all agent (non-sandbox) jobs, most recent first.
|
||||
pub async fn list_agent_jobs(&self) -> Result<Vec<AgentJobRecord>, DatabaseError> {
|
||||
let conn = self.conn().await?;
|
||||
let rows = conn
|
||||
.query(
|
||||
r#"
|
||||
SELECT id, title, status, user_id, failure_reason,
|
||||
created_at, started_at, completed_at
|
||||
FROM agent_jobs WHERE source = 'direct'
|
||||
ORDER BY created_at DESC
|
||||
"#,
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|r| AgentJobRecord {
|
||||
id: r.get("id"),
|
||||
title: r.get("title"),
|
||||
status: r.get("status"),
|
||||
user_id: r.get::<_, Option<String>>("user_id").unwrap_or_default(),
|
||||
created_at: r.get("created_at"),
|
||||
started_at: r.get("started_at"),
|
||||
completed_at: r.get("completed_at"),
|
||||
failure_reason: r.get("failure_reason"),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Summary counts for agent (non-sandbox) jobs.
|
||||
pub async fn agent_job_summary(&self) -> Result<AgentJobSummary, DatabaseError> {
|
||||
let conn = self.conn().await?;
|
||||
let rows = conn
|
||||
.query(
|
||||
"SELECT status, COUNT(*) as cnt FROM agent_jobs WHERE source = 'direct' GROUP BY status",
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut summary = AgentJobSummary::default();
|
||||
for row in &rows {
|
||||
let status: String = row.get("status");
|
||||
let count: i64 = row.get("cnt");
|
||||
summary.add_count(&status, count as usize);
|
||||
}
|
||||
Ok(summary)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Job Events ====================
|
||||
|
||||
Reference in New Issue
Block a user