feat: Background tasks table + tasks route migrated to Rust
- New migration: background_tasks table
- New model: BackgroundTask (background_task.rs)
- GET /api/tasks with limit param
- GET /api/tasks/{id} with 404 handling
- 18/18 tests pass
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
-- Background tasks tracking
|
||||||
|
CREATE TABLE IF NOT EXISTS background_tasks (
|
||||||
|
id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
task_type TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending',
|
||||||
|
message TEXT DEFAULT '',
|
||||||
|
error TEXT,
|
||||||
|
dedupe_key TEXT,
|
||||||
|
success_title TEXT,
|
||||||
|
failure_title TEXT,
|
||||||
|
notify_on_success INTEGER DEFAULT true,
|
||||||
|
notify_on_failure INTEGER DEFAULT true,
|
||||||
|
result_json TEXT,
|
||||||
|
progress INTEGER DEFAULT 0,
|
||||||
|
progress_total INTEGER DEFAULT 0,
|
||||||
|
created_at TEXT DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT DEFAULT (datetime('now')),
|
||||||
|
started_at TEXT,
|
||||||
|
finished_at TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS background_tasks_status_idx
|
||||||
|
ON background_tasks (status);
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::FromRow;
|
||||||
|
|
||||||
|
/// Background task for tracking async operations (deployments, syncs, etc.)
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
|
pub struct BackgroundTask {
|
||||||
|
pub id: String,
|
||||||
|
pub task_type: String,
|
||||||
|
pub title: String,
|
||||||
|
pub status: String,
|
||||||
|
pub message: Option<String>,
|
||||||
|
pub error: Option<String>,
|
||||||
|
pub dedupe_key: Option<String>,
|
||||||
|
pub success_title: Option<String>,
|
||||||
|
pub failure_title: Option<String>,
|
||||||
|
pub notify_on_success: Option<bool>,
|
||||||
|
pub notify_on_failure: Option<bool>,
|
||||||
|
pub result_json: Option<String>,
|
||||||
|
pub progress: Option<i64>,
|
||||||
|
pub progress_total: Option<i64>,
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
pub started_at: Option<String>,
|
||||||
|
pub finished_at: Option<String>,
|
||||||
|
}
|
||||||
@@ -7,5 +7,6 @@ pub mod route_channel;
|
|||||||
pub mod setting;
|
pub mod setting;
|
||||||
pub mod site;
|
pub mod site;
|
||||||
pub mod site_announcement;
|
pub mod site_announcement;
|
||||||
|
pub mod background_task;
|
||||||
pub mod token_route;
|
pub mod token_route;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ mod events;
|
|||||||
mod health;
|
mod health;
|
||||||
mod proxy;
|
mod proxy;
|
||||||
mod site_announcements;
|
mod site_announcements;
|
||||||
|
mod tasks;
|
||||||
mod user_api_keys;
|
mod user_api_keys;
|
||||||
|
|
||||||
pub fn build_router(state: AppState) -> Router {
|
pub fn build_router(state: AppState) -> Router {
|
||||||
@@ -21,6 +22,7 @@ pub fn build_router(state: AppState) -> Router {
|
|||||||
.merge(events::routes())
|
.merge(events::routes())
|
||||||
.merge(checkin::routes())
|
.merge(checkin::routes())
|
||||||
.merge(site_announcements::routes())
|
.merge(site_announcements::routes())
|
||||||
|
.merge(tasks::routes())
|
||||||
.merge(user_api_keys::routes())
|
.merge(user_api_keys::routes())
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
// Background tasks routes — list and inspect async operations
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
db::models::background_task::BackgroundTask,
|
||||||
|
error::AppResult,
|
||||||
|
AppState,
|
||||||
|
};
|
||||||
|
use axum::{
|
||||||
|
extract::{Path, Query, State},
|
||||||
|
routing::get,
|
||||||
|
Json, Router,
|
||||||
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct ListTasksQuery {
|
||||||
|
limit: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn routes() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/api/tasks", get(list_tasks))
|
||||||
|
.route("/api/tasks/{id}", get(get_task))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /api/tasks — list background tasks
|
||||||
|
async fn list_tasks(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Query(q): Query<ListTasksQuery>,
|
||||||
|
) -> AppResult<Json<Value>> {
|
||||||
|
let limit = q.limit.unwrap_or(50).max(1).min(500);
|
||||||
|
info!("Listing background tasks, limit={}", limit);
|
||||||
|
|
||||||
|
let tasks: Vec<BackgroundTask> = sqlx::query_as::<_, BackgroundTask>(
|
||||||
|
"SELECT * FROM background_tasks ORDER BY created_at DESC LIMIT ?"
|
||||||
|
)
|
||||||
|
.bind(limit)
|
||||||
|
.fetch_all(&*state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Json(json!({ "tasks": tasks })))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /api/tasks/{id} — get a single background task
|
||||||
|
async fn get_task(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> AppResult<Json<Value>> {
|
||||||
|
info!("Getting background task: {}", id);
|
||||||
|
|
||||||
|
let task: Option<BackgroundTask> = sqlx::query_as::<_, BackgroundTask>(
|
||||||
|
"SELECT * FROM background_tasks WHERE id = ?"
|
||||||
|
)
|
||||||
|
.bind(&id)
|
||||||
|
.fetch_optional(&*state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match task {
|
||||||
|
Some(t) => Ok(Json(json!({ "success": true, "task": t }))),
|
||||||
|
None => Ok(Json(json!({ "success": false, "message": "task not found" }))),
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user