use crate::{ config::Config, db::Pool, models::objective::{ObjectiveDefinition, ObjectiveType}, models::pack::PackDefinition, models::sbc::SbcDefinition, routes, services::{ card_db::CardDb, market, objective::load_objective_definitions, pack::load_pack_definitions, sbc::load_sbc_definitions, }, }; use anyhow::Result; use axum::{ routing::{get, post}, Router, }; use std::sync::Arc; use tower_http::{cors::CorsLayer, trace::TraceLayer}; #[derive(Clone)] pub struct AppState { pub pool: Pool, pub card_db: Arc, pub pack_defs: Arc>, pub obj_defs: Arc>, pub sbc_defs: Arc>, } pub async fn build(pool: Pool, cfg: Config) -> Result { let card_db = Arc::new(CardDb::load(&cfg.data_dir)?); let pack_defs = Arc::new(load_pack_definitions(&cfg.data_dir)?); let obj_defs = Arc::new(load_objective_definitions(&cfg.data_dir)?); let sbc_defs = Arc::new(load_sbc_definitions(&cfg.data_dir)?); tracing::info!( "Loaded {} cards, {} packs, {} objectives, {} SBCs", card_db.cards.len(), pack_defs.len(), obj_defs.len(), sbc_defs.len(), ); let state = AppState { pool: pool.clone(), card_db: card_db.clone(), pack_defs, obj_defs: obj_defs.clone(), sbc_defs, }; // Background task: refresh NPC market at startup and every 24 hours { let pool = pool.clone(); let card_db = card_db.clone(); tokio::spawn(async move { if let Err(e) = market::refresh_npc_listings(&pool, &card_db).await { tracing::warn!("initial market refresh failed: {e}"); } loop { tokio::time::sleep(tokio::time::Duration::from_secs(86_400)).await; if let Err(e) = market::refresh_npc_listings(&pool, &card_db).await { tracing::warn!("market refresh failed: {e}"); } } }); } // Background task: reset daily objective progress at midnight UTC { let pool = pool.clone(); let daily_ids: Vec = obj_defs .iter() .filter(|d| matches!(d.objective_type, ObjectiveType::Daily)) .map(|d| d.id.clone()) .collect(); tokio::spawn(async move { loop { let now = chrono::Utc::now(); let tomorrow = now.date_naive() + chrono::Days::new(1); let midnight = tomorrow .and_hms_opt(0, 0, 0) .map(|dt| dt.and_utc()) .unwrap_or_else(|| now + chrono::Duration::hours(24)); let secs = (midnight - now).num_seconds().max(1) as u64; tokio::time::sleep(tokio::time::Duration::from_secs(secs)).await; for id in &daily_ids { if let Err(e) = sqlx::query( "UPDATE objective_progress SET current = 0, completed = 0, claimed = 0 WHERE objective_id = ?", ) .bind(id) .execute(&pool) .await { tracing::warn!("daily reset failed for {id}: {e}"); } } tracing::info!("Daily objectives reset ({} objectives)", daily_ids.len()); } }); } let router = Router::new() .route("/health", get(routes::health::get_health)) .route("/auth/local", post(routes::auth::post_auth_local)) .route("/profile", get(routes::profile::get_profile)) .route("/club", get(routes::club::get_club)) .route("/cards", get(routes::cards::get_cards)) .route("/cards/:card_id", get(routes::cards::get_card)) .route("/collection", get(routes::cards::get_collection)) .route("/packs", get(routes::packs::get_packs)) .route("/packs/buy", post(routes::packs::post_buy_pack)) .route("/packs/open/:pack_id", post(routes::packs::post_open_pack)) .route("/squad", get(routes::squad::get_squad)) .route("/squad", post(routes::squad::post_squad)) .route("/objectives", get(routes::objectives::get_objectives)) .route( "/objectives/claim", post(routes::objectives::post_claim_objective), ) .route("/matches", get(routes::matches::get_matches)) .route("/matches/opponent", get(routes::matches::get_opponent)) .route("/matches/result", post(routes::matches::post_match_result)) .route("/sbc", get(routes::sbc::get_sbcs)) .route("/sbc/:sbc_id", get(routes::sbc::get_sbc)) .route("/sbc/submit", post(routes::sbc::post_sbc_submit)) .route("/market", get(routes::market::get_market)) .route("/market/buy", post(routes::market::post_market_buy)) .route("/market/sell", post(routes::market::post_market_sell)) .route("/market/refresh", post(routes::market::post_market_refresh)) .route("/statistics", get(routes::statistics::get_statistics)) .route( "/statistics/history", get(routes::statistics::get_statistics_history), ) .layer(TraceLayer::new_for_http()) .layer(CorsLayer::permissive()) .with_state(state); Ok(router) }