use std::sync::Arc; use axum::{ extract::{Path, State}, http::StatusCode, response::{IntoResponse, Response}, routing::get, Json, Router, }; use serde::Deserialize; use crate::AppState; use crate::db::Timer; type ApiResult = Result, Response>; fn db_err(e: sqlx::Error) -> Response { tracing::error!("Database error: {}", e); (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response() } #[derive(Deserialize)] pub struct CreateTimer { pub name: String, pub interval_secs: i64, pub requirement: String, } #[derive(Deserialize)] pub struct UpdateTimer { pub name: Option, pub interval_secs: Option, pub requirement: Option, pub enabled: Option, } pub fn router(state: Arc) -> Router { Router::new() .route("/projects/{id}/timers", get(list_timers).post(create_timer)) .route("/timers/{id}", get(get_timer).put(update_timer).delete(delete_timer)) .with_state(state) } async fn list_timers( State(state): State>, Path(project_id): Path, ) -> ApiResult> { sqlx::query_as::<_, Timer>( "SELECT * FROM timers WHERE project_id = ? ORDER BY created_at DESC" ) .bind(&project_id) .fetch_all(&state.db.pool) .await .map(Json) .map_err(db_err) } async fn create_timer( State(state): State>, Path(project_id): Path, Json(input): Json, ) -> ApiResult { if input.interval_secs < 60 { return Err((StatusCode::BAD_REQUEST, "Minimum interval is 60 seconds").into_response()); } let id = uuid::Uuid::new_v4().to_string(); sqlx::query_as::<_, Timer>( "INSERT INTO timers (id, project_id, name, interval_secs, requirement) VALUES (?, ?, ?, ?, ?) RETURNING *" ) .bind(&id) .bind(&project_id) .bind(&input.name) .bind(input.interval_secs) .bind(&input.requirement) .fetch_one(&state.db.pool) .await .map(Json) .map_err(db_err) } async fn get_timer( State(state): State>, Path(timer_id): Path, ) -> ApiResult { sqlx::query_as::<_, Timer>("SELECT * FROM timers WHERE id = ?") .bind(&timer_id) .fetch_optional(&state.db.pool) .await .map_err(db_err)? .map(Json) .ok_or_else(|| (StatusCode::NOT_FOUND, "Timer not found").into_response()) } async fn update_timer( State(state): State>, Path(timer_id): Path, Json(input): Json, ) -> ApiResult { // Fetch existing let existing = sqlx::query_as::<_, Timer>("SELECT * FROM timers WHERE id = ?") .bind(&timer_id) .fetch_optional(&state.db.pool) .await .map_err(db_err)?; let Some(existing) = existing else { return Err((StatusCode::NOT_FOUND, "Timer not found").into_response()); }; let name = input.name.unwrap_or(existing.name); let interval_secs = input.interval_secs.unwrap_or(existing.interval_secs); let requirement = input.requirement.unwrap_or(existing.requirement); let enabled = input.enabled.unwrap_or(existing.enabled); if interval_secs < 60 { return Err((StatusCode::BAD_REQUEST, "Minimum interval is 60 seconds").into_response()); } sqlx::query_as::<_, Timer>( "UPDATE timers SET name = ?, interval_secs = ?, requirement = ?, enabled = ? WHERE id = ? RETURNING *" ) .bind(&name) .bind(interval_secs) .bind(&requirement) .bind(enabled) .bind(&timer_id) .fetch_one(&state.db.pool) .await .map(Json) .map_err(db_err) } async fn delete_timer( State(state): State>, Path(timer_id): Path, ) -> Result { let result = sqlx::query("DELETE FROM timers WHERE id = ?") .bind(&timer_id) .execute(&state.db.pool) .await .map_err(db_err)?; if result.rows_affected() > 0 { Ok(StatusCode::NO_CONTENT) } else { Err((StatusCode::NOT_FOUND, "Timer not found").into_response()) } }