use axum::{ Json, extract::{Path, State}, }; use sea_orm::{EntityTrait, QueryOrder, QuerySelect}; use std::sync::Arc; use uuid::Uuid; use super::AppState; use super::auth::AuthenticatedUser; use crate::domain::tasks::{ self, CreateTaskRequest, RecentRunResponse, TaskResponse, UpdateTaskRequest, }; use crate::error::AppResult; pub async fn list_tasks( _user: AuthenticatedUser, State(state): State>, ) -> AppResult>> { let tasks = crate::entities::task::Entity::find() .find_with_related(crate::entities::task_run::Entity) .order_by_desc(crate::entities::task::Column::CreatedAt) .all(&state.db) .await?; let response = tasks .into_iter() .map(|(t, mut runs)| { runs.sort_by(|a, b| b.created_at.cmp(&a.created_at)); TaskResponse { id: t.id, goal: t.goal, cron: t.cron, created_at: t.created_at, runs: runs .into_iter() .map(|r| tasks::TaskRunResponse { id: r.id, status: r.status, logs: r.logs, answer: r.answer, created_at: r.created_at, }) .collect(), } }) .collect(); Ok(Json(response)) } pub async fn create_task( _user: AuthenticatedUser, State(state): State>, Json(payload): Json, ) -> AppResult> { if payload.goal.trim().is_empty() { return Err(crate::error::AppError::InvalidRequest( "Goal cannot be empty".into(), )); } if payload.goal.trim().len() < 5 { return Err(crate::error::AppError::InvalidRequest( "Goal is too short (min 5 characters)".into(), )); } let task_id = Uuid::new_v4(); tracing::info!(%task_id, goal = %payload.goal, "Creating new task"); let new_task = crate::entities::task::ActiveModel { id: sea_orm::Set(task_id), goal: sea_orm::Set(payload.goal), cron: sea_orm::Set(payload.cron.clone()), created_at: sea_orm::Set(chrono::Utc::now().into()), }; use sea_orm::ActiveModelTrait; new_task.insert(&state.db).await?; if let Some(cron) = &payload.cron { let _ = state.scheduler.add_task_job(task_id, cron).await; } let task_response = tasks::get_task_inner(task_id, &state.db).await?; let _ = state .tx .send(crate::server::notifications::WsEvent::TaskCreated( task_response.clone(), )); Ok(Json(task_response)) } pub async fn rerun_task( _user: AuthenticatedUser, State(state): State>, Path(id): Path, ) -> AppResult> { let task = crate::entities::task::Entity::find_by_id(id) .one(&state.db) .await? .ok_or_else(|| crate::error::AppError::NotFound("Task not found".to_string()))?; tracing::info!(task_id = %task.id, "Manually triggering task rerun"); tasks::execute_agent_run( &state.db, &state.scheduler, &state.config, task.id, task.goal, ) .await .map(Json) .map_err(|e| crate::error::AppError::Internal(e.to_string())) } pub async fn update_task( _user: AuthenticatedUser, State(state): State>, Path(id): Path, Json(payload): Json, ) -> AppResult> { if payload.goal.trim().is_empty() { return Err(crate::error::AppError::InvalidRequest( "Goal cannot be empty".into(), )); } if payload.goal.trim().len() < 5 { return Err(crate::error::AppError::InvalidRequest( "Goal is too short (min 5 characters)".into(), )); } let task: crate::entities::task::ActiveModel = crate::entities::task::Entity::find_by_id(id) .one(&state.db) .await? .ok_or_else(|| crate::error::AppError::NotFound("Task not found".to_string()))? .into(); let mut task = task; tracing::info!(task_id = %id, goal = %payload.goal, "Updating task"); task.goal = sea_orm::Set(payload.goal); task.cron = sea_orm::Set(payload.cron.clone()); use sea_orm::ActiveModelTrait; task.update(&state.db).await?; if let Some(cron) = &payload.cron { let _ = state.scheduler.add_task_job(id, cron).await; } else { let _ = state.scheduler.remove_task_job(id).await; } let task_response = tasks::get_task_inner(id, &state.db).await?; let _ = state .tx .send(crate::server::notifications::WsEvent::TaskUpdated( task_response.clone(), )); Ok(Json(task_response)) } pub async fn get_task( _user: AuthenticatedUser, Path(id): Path, State(state): State>, ) -> AppResult> { tasks::get_task_inner(id, &state.db) .await .map(Json) .map_err(|e| crate::error::AppError::Internal(e.to_string())) } pub async fn get_recent_runs( _user: AuthenticatedUser, State(state): State>, ) -> AppResult>> { let results = crate::entities::task_run::Entity::find() .find_also_related(crate::entities::task::Entity) .order_by_desc(crate::entities::task_run::Column::CreatedAt) .limit(50) .all(&state.db) .await?; let response = results .into_iter() .filter_map(|(run, task_opt)| { task_opt.map(|task| RecentRunResponse { id: run.id, task_id: run.task_id, goal: task.goal, status: run.status, created_at: run.created_at, }) }) .collect(); Ok(Json(response)) }