use anyhow::{Context, Result}; use openfut_core::{config, db, seed}; use tracing::info; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; #[tokio::main] async fn main() -> Result<()> { dotenvy::dotenv().ok(); tracing_subscriber::registry() .with( EnvFilter::try_from_default_env() .unwrap_or_else(|_| "openfut_core=debug,tower_http=debug".into()), ) // Diagnostics on stderr so stdout carries only machine output (the // `import`/`seed-dev` subcommands print a clean JSON result there). .with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr)) .init(); let cfg = config::Config::from_env()?; // Opt-in dev subcommand: `openfut-core seed-dev` seeds the FIFA 17 dev // profile/club from the dev content pack, prints a coverage report, and // exits. Normal server startup NEVER seeds dev inventory. if std::env::args().nth(1).as_deref() == Some("seed-dev") { let pool = db::init_pool(&cfg.database_url, cfg.max_connections).await?; db::run_migrations(&pool).await?; let mut card_db = openfut_core::services::card_db::CardDb::load(&cfg.data_dir)?; card_db.load_game_dev(&cfg.data_dir, seed::FIFA17_GAME)?; let report = seed::seed_fifa17_dev(&pool, &card_db).await?; println!("{}", serde_json::to_string_pretty(&report)?); return Ok(()); } // Opt-in generic import subcommand: `openfut-core import `. // Reads a GAME-AGNOSTIC ProfileImportRequest (the importer adapter translates // FIFA17 source data into it), loads production content packs, runs preflight, // and applies one all-or-nothing transaction. FIFA17 semantics live entirely // in the adapter; Core only sees opaque ids + opaque extension bytes. if std::env::args().nth(1).as_deref() == Some("import") { let path = std::env::args() .nth(2) .context("usage: openfut-core import ")?; let pool = db::init_pool(&cfg.database_url, cfg.max_connections).await?; db::run_migrations(&pool).await?; let mut card_db = openfut_core::services::card_db::CardDb::load(&cfg.data_dir)?; for pack in &cfg.content_packs { card_db.load_pack(pack)?; } let raw = std::fs::read_to_string(&path) .with_context(|| format!("read import request {path}"))?; let req: openfut_core::services::import::ProfileImportRequest = serde_json::from_str(&raw).context("parse import request JSON")?; let outcome = openfut_core::services::import::apply_profile_import(&pool, &card_db, &req).await?; println!("{}", serde_json::to_string_pretty(&outcome)?); return Ok(()); } // `openfut-core reclassify [--dry-run]` — correct the // content_kind of already-imported owned rows. A profile import is // once-only, so a taxonomy fix cannot arrive by re-importing; the adapter // supplies card_id -> kind because only it can map its own taxonomy. // Idempotent. `--dry-run` runs the same statements and rolls back, so an // operator can see what a production run would touch before it touches it. if std::env::args().nth(1).as_deref() == Some("reclassify") { let path = std::env::args() .nth(2) .context("usage: openfut-core reclassify [--dry-run]")?; let dry_run = std::env::args().any(|a| a == "--dry-run"); let pool = db::init_pool(&cfg.database_url, cfg.max_connections).await?; db::run_migrations(&pool).await?; let raw = std::fs::read_to_string(&path) .with_context(|| format!("read reclassify request {path}"))?; let mut req: openfut_core::services::import::ReclassifyRequest = serde_json::from_str(&raw).context("parse reclassify request JSON")?; req.dry_run |= dry_run; let outcome = openfut_core::services::import::reclassify_owned_content(&pool, &req).await?; println!("{}", serde_json::to_string_pretty(&outcome)?); return Ok(()); } info!("OpenFUT Core starting on {}", cfg.listen_addr); let pool = db::init_pool(&cfg.database_url, cfg.max_connections).await?; db::run_migrations(&pool).await?; seed::maybe_seed(&pool).await?; let app = openfut_core::app::build(pool, cfg.clone()).await?; let listener = tokio::net::TcpListener::bind(&cfg.listen_addr).await?; info!("Listening on http://{}", cfg.listen_addr); axum::serve(listener, app).await?; Ok(()) }