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:
boos
2026-05-17 16:56:15 +08:00
parent d7d3670f02
commit fac1aa4a88
5 changed files with 116 additions and 0 deletions
@@ -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>,
}
+1
View File
@@ -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;
+2
View File
@@ -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)
} }
+64
View File
@@ -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" }))),
}
}