use axum::Json;
use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use serde::Deserialize;
use serde_json::Value;
use crate::admin::{self, CancelJobOutcome, RunJobNowOutcome};
use crate::sqlite::job::{Job, JobQuery};
use crate::sqlite::status::{JobStatus, UnknownStatus};
use crate::webadmin::AdminState;
use crate::webadmin::error::AdminError;
use crate::webadmin::handlers::paging::{PageParams, page_envelope};
use crate::webadmin::handlers::params::empty_is_absent;
use crate::webadmin::session::{Authenticated, AuthenticatedWrite};
#[derive(Debug, Deserialize, Default)]
pub struct JobListParams {
#[serde(default, deserialize_with = "empty_is_absent")]
pub kind: Option<String>,
#[serde(default, deserialize_with = "empty_is_absent")]
pub status: Option<String>,
pub limit: Option<i64>,
pub offset: Option<i64>,
}
impl JobListParams {
pub fn parsed_status(&self) -> Result<Option<JobStatus>, UnknownStatus> {
self.status.as_deref().map(str::parse).transpose()
}
}
pub(crate) fn bad_status(error: UnknownStatus) -> AdminError {
AdminError::with_code(StatusCode::BAD_REQUEST, "invalid_status", error.to_string())
}
pub async fn list_jobs(
State(state): State<AdminState>,
Query(params): Query<JobListParams>,
_auth: Authenticated,
) -> Result<Json<Value>, AdminError> {
let page = PageParams::from(params.limit, params.offset).resolve(&state.config);
let status = params.parsed_status().map_err(bad_status)?;
let query = JobQuery {
kind: params.kind,
status,
limit: page.limit,
offset: page.offset,
};
let (jobs, total) = Job::search(&query, &state.database).await?;
let items = jobs.iter().map(admin::render_job_json).collect();
Ok(Json(page_envelope(items, total, page)))
}
pub async fn get_job(
State(state): State<AdminState>,
Path(id): Path<String>,
_auth: Authenticated,
) -> Result<Json<Value>, AdminError> {
let detail = admin::load_job_detail(&id, state.database.clone())
.await?
.ok_or_else(|| not_found(&id))?;
Ok(Json(admin::render_job_detail_json(&detail)))
}
pub async fn cancel_job(
State(state): State<AdminState>,
Path(id): Path<String>,
request_context: crate::audit::RequestContext,
AuthenticatedWrite(auth): AuthenticatedWrite,
) -> Result<Json<Value>, AdminError> {
let outcome = admin::cancel_job(
&id,
crate::audit::Actor::admin(&auth.user.username),
state.audit.client(&request_context).await,
state.audit.metrics(),
state.database.clone(),
)
.await
.map_err(|admin::CancelJobError::Database(error)| AdminError::from(error))?;
match outcome {
CancelJobOutcome::NotFound => Err(not_found(&id)),
CancelJobOutcome::NotCancellable(status) => Err(AdminError::conflict(
"job_not_cancellable",
format!("job {id} is {status}; only ready or failed jobs can be cancelled"),
)),
CancelJobOutcome::Cancelled(job) => {
tracing::info!(event = "admin_job_cancelled",
outcome = "success",
surface = "api",
job_id = %id,
job_kind = %job.kind,
order_abandoned = false,
username = %auth.user.username);
Ok(Json(admin::render_job_json(&job)))
}
CancelJobOutcome::CancelledAndOrderAbandoned { job, order_id } => {
tracing::info!(event = "admin_job_cancelled",
outcome = "success",
surface = "api",
job_id = %id,
job_kind = %job.kind,
order_id = %order_id,
order_abandoned = true,
username = %auth.user.username);
Ok(Json(admin::render_job_json(&job)))
}
}
}
pub async fn run_job(
State(state): State<AdminState>,
Path(id): Path<String>,
request_context: crate::audit::RequestContext,
AuthenticatedWrite(auth): AuthenticatedWrite,
) -> Result<Json<Value>, AdminError> {
match admin::run_job_now(
&id,
crate::audit::Actor::admin(&auth.user.username),
state.audit.client(&request_context).await,
state.database.clone(),
)
.await?
{
RunJobNowOutcome::NotFound => Err(not_found(&id)),
RunJobNowOutcome::Refused(status) => Err(AdminError::conflict(
"job_not_runnable",
format!("job {id} is {status}; run-now applies to ready or failed jobs"),
)),
RunJobNowOutcome::Nudged(job) => {
tracing::info!(event = "admin_job_advanced",
outcome = "success",
surface = "api",
job_id = %id,
username = %auth.user.username);
Ok(Json(admin::render_job_json(&job)))
}
RunJobNowOutcome::Revived(job) => {
tracing::info!(event = "admin_job_revived",
outcome = "success",
surface = "api",
job_id = %id,
attempts = job.attempts,
username = %auth.user.username);
Ok(Json(admin::render_job_json(&job)))
}
}
}
fn not_found(id: &str) -> AdminError {
AdminError::not_found(format!("no such job: {id}"))
}