bot/src/server/tasks.rs
2026-02-11 16:55:55 +01:00

204 lines
5.8 KiB
Rust

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<Arc<AppState>>,
) -> AppResult<Json<Vec<TaskResponse>>> {
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<Arc<AppState>>,
Json(payload): Json<CreateTaskRequest>,
) -> AppResult<Json<TaskResponse>> {
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<Arc<AppState>>,
Path(id): Path<Uuid>,
) -> AppResult<Json<TaskResponse>> {
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<Arc<AppState>>,
Path(id): Path<Uuid>,
Json(payload): Json<UpdateTaskRequest>,
) -> AppResult<Json<TaskResponse>> {
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<Uuid>,
State(state): State<Arc<AppState>>,
) -> AppResult<Json<TaskResponse>> {
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<Arc<AppState>>,
) -> AppResult<Json<Vec<RecentRunResponse>>> {
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))
}