2025-06-14 15:14:08 -04:00
|
|
|
use axum::{
|
2025-06-17 11:24:03 -04:00
|
|
|
body::Body,
|
2025-06-14 17:36:54 -04:00
|
|
|
extract::Extension,
|
2025-06-17 11:24:03 -04:00
|
|
|
http::{header, HeaderValue, StatusCode},
|
|
|
|
|
response::{IntoResponse, Json as ResponseJson, Response},
|
2025-06-14 16:26:48 -04:00
|
|
|
routing::{get, post},
|
|
|
|
|
Json, Router,
|
2025-06-14 15:14:08 -04:00
|
|
|
};
|
2025-06-17 11:24:03 -04:00
|
|
|
use rust_embed::RustEmbed;
|
2025-06-17 15:22:47 -04:00
|
|
|
use sqlx::{sqlite::SqliteConnectOptions, SqlitePool};
|
|
|
|
|
use std::str::FromStr;
|
2025-06-20 21:47:13 +01:00
|
|
|
use std::sync::Arc;
|
|
|
|
|
use tokio::sync::RwLock;
|
2025-06-14 16:26:48 -04:00
|
|
|
use tower_http::cors::CorsLayer;
|
2025-06-14 15:14:08 -04:00
|
|
|
|
2025-06-20 22:14:31 +01:00
|
|
|
mod app_state;
|
2025-06-16 18:20:17 -04:00
|
|
|
mod execution_monitor;
|
2025-06-16 18:37:19 -04:00
|
|
|
mod executor;
|
|
|
|
|
mod executors;
|
2025-06-14 15:14:08 -04:00
|
|
|
mod models;
|
2025-06-14 16:26:48 -04:00
|
|
|
mod routes;
|
2025-06-19 12:53:41 -04:00
|
|
|
mod utils;
|
2025-06-14 15:14:08 -04:00
|
|
|
|
2025-06-20 22:14:31 +01:00
|
|
|
use app_state::AppState;
|
|
|
|
|
use execution_monitor::execution_monitor;
|
2025-06-19 12:53:41 -04:00
|
|
|
use models::{ApiResponse, Config};
|
2025-06-20 23:03:29 +01:00
|
|
|
use routes::{config, filesystem, health, projects, task_attempts, tasks};
|
2025-06-14 15:14:08 -04:00
|
|
|
|
2025-06-17 11:24:03 -04:00
|
|
|
#[derive(RustEmbed)]
|
|
|
|
|
#[folder = "../frontend/dist"]
|
|
|
|
|
struct Assets;
|
|
|
|
|
|
2025-06-14 16:26:48 -04:00
|
|
|
async fn echo_handler(
|
|
|
|
|
Json(payload): Json<serde_json::Value>,
|
|
|
|
|
) -> ResponseJson<ApiResponse<serde_json::Value>> {
|
2025-06-14 15:14:08 -04:00
|
|
|
ResponseJson(ApiResponse {
|
|
|
|
|
success: true,
|
|
|
|
|
data: Some(payload),
|
|
|
|
|
message: Some("Echo successful".to_string()),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-17 11:24:03 -04:00
|
|
|
async fn static_handler(uri: axum::extract::Path<String>) -> impl IntoResponse {
|
|
|
|
|
let path = uri.trim_start_matches('/');
|
|
|
|
|
serve_file(path).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn index_handler() -> impl IntoResponse {
|
|
|
|
|
serve_file("index.html").await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn serve_file(path: &str) -> impl IntoResponse {
|
|
|
|
|
let file = Assets::get(path);
|
|
|
|
|
|
|
|
|
|
match file {
|
|
|
|
|
Some(content) => {
|
|
|
|
|
let mime = mime_guess::from_path(path).first_or_octet_stream();
|
|
|
|
|
|
|
|
|
|
Response::builder()
|
|
|
|
|
.status(StatusCode::OK)
|
|
|
|
|
.header(
|
|
|
|
|
header::CONTENT_TYPE,
|
|
|
|
|
HeaderValue::from_str(mime.as_ref()).unwrap(),
|
|
|
|
|
)
|
|
|
|
|
.body(Body::from(content.data.into_owned()))
|
|
|
|
|
.unwrap()
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
// For SPA routing, serve index.html for unknown routes
|
|
|
|
|
if let Some(index) = Assets::get("index.html") {
|
|
|
|
|
Response::builder()
|
|
|
|
|
.status(StatusCode::OK)
|
|
|
|
|
.header(header::CONTENT_TYPE, HeaderValue::from_static("text/html"))
|
|
|
|
|
.body(Body::from(index.data.into_owned()))
|
|
|
|
|
.unwrap()
|
|
|
|
|
} else {
|
|
|
|
|
Response::builder()
|
|
|
|
|
.status(StatusCode::NOT_FOUND)
|
|
|
|
|
.body(Body::from("404 Not Found"))
|
|
|
|
|
.unwrap()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-24 16:50:58 +01:00
|
|
|
async fn serve_sound_file(
|
|
|
|
|
axum::extract::Path(filename): axum::extract::Path<String>,
|
|
|
|
|
) -> impl IntoResponse {
|
2025-06-24 10:50:20 +01:00
|
|
|
use std::path::Path;
|
2025-06-24 16:50:58 +01:00
|
|
|
use tokio::fs;
|
2025-06-24 10:50:20 +01:00
|
|
|
|
|
|
|
|
// Validate filename contains only expected sound files
|
2025-06-24 16:50:58 +01:00
|
|
|
let valid_sounds = [
|
|
|
|
|
"abstract-sound1.mp3",
|
|
|
|
|
"abstract-sound2.mp3",
|
|
|
|
|
"abstract-sound3.mp3",
|
|
|
|
|
"abstract-sound4.mp3",
|
|
|
|
|
"cow-mooing.mp3",
|
|
|
|
|
"phone-vibration.mp3",
|
|
|
|
|
"rooster.mp3",
|
|
|
|
|
];
|
|
|
|
|
|
2025-06-24 10:50:20 +01:00
|
|
|
if !valid_sounds.contains(&filename.as_str()) {
|
|
|
|
|
return Response::builder()
|
|
|
|
|
.status(StatusCode::NOT_FOUND)
|
|
|
|
|
.body(Body::from("Sound file not found"))
|
|
|
|
|
.unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let sound_path = Path::new("backend/sounds").join(&filename);
|
2025-06-24 16:50:58 +01:00
|
|
|
|
2025-06-24 10:50:20 +01:00
|
|
|
match fs::read(&sound_path).await {
|
2025-06-24 16:50:58 +01:00
|
|
|
Ok(content) => Response::builder()
|
|
|
|
|
.status(StatusCode::OK)
|
|
|
|
|
.header(header::CONTENT_TYPE, HeaderValue::from_static("audio/mpeg"))
|
|
|
|
|
.body(Body::from(content))
|
|
|
|
|
.unwrap(),
|
|
|
|
|
Err(_) => Response::builder()
|
|
|
|
|
.status(StatusCode::NOT_FOUND)
|
|
|
|
|
.body(Body::from("Sound file not found"))
|
|
|
|
|
.unwrap(),
|
2025-06-24 10:50:20 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-14 15:14:08 -04:00
|
|
|
#[tokio::main]
|
|
|
|
|
async fn main() -> anyhow::Result<()> {
|
2025-06-17 20:36:25 -04:00
|
|
|
tracing_subscriber::fmt().init();
|
2025-06-14 15:14:08 -04:00
|
|
|
|
2025-06-17 19:59:10 -04:00
|
|
|
// Create asset directory if it doesn't exist
|
2025-06-19 12:53:41 -04:00
|
|
|
if !utils::asset_dir().exists() {
|
|
|
|
|
std::fs::create_dir_all(utils::asset_dir())?;
|
2025-06-17 19:59:10 -04:00
|
|
|
}
|
|
|
|
|
|
2025-06-14 15:34:24 -04:00
|
|
|
// Database connection
|
2025-06-17 19:59:10 -04:00
|
|
|
let database_url = format!(
|
|
|
|
|
"sqlite://{}",
|
2025-06-19 12:53:41 -04:00
|
|
|
utils::asset_dir().join("db.sqlite").to_string_lossy()
|
2025-06-17 19:59:10 -04:00
|
|
|
);
|
2025-06-17 15:22:47 -04:00
|
|
|
|
|
|
|
|
let options = SqliteConnectOptions::from_str(&database_url)?.create_if_missing(true);
|
|
|
|
|
let pool = SqlitePool::connect_with(options).await?;
|
|
|
|
|
sqlx::migrate!("./migrations").run(&pool).await?;
|
2025-06-14 15:34:24 -04:00
|
|
|
|
2025-06-19 12:53:41 -04:00
|
|
|
// Load configuration
|
|
|
|
|
let config_path = utils::config_path();
|
|
|
|
|
let config = Config::load(&config_path)?;
|
|
|
|
|
let config_arc = Arc::new(RwLock::new(config));
|
|
|
|
|
|
2025-06-16 18:09:50 -04:00
|
|
|
// Create app state
|
2025-06-20 21:46:28 +01:00
|
|
|
let app_state = AppState::new(pool.clone(), config_arc.clone());
|
2025-06-16 18:09:50 -04:00
|
|
|
|
|
|
|
|
// Start background task to check for init status and spawn processes
|
|
|
|
|
let state_clone = app_state.clone();
|
|
|
|
|
tokio::spawn(async move {
|
|
|
|
|
execution_monitor(state_clone).await;
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-15 14:16:13 -04:00
|
|
|
// Public routes (no auth required)
|
|
|
|
|
let public_routes = Router::new()
|
2025-06-17 11:24:03 -04:00
|
|
|
.route("/api/health", get(health::health_check))
|
2025-06-17 14:17:31 -04:00
|
|
|
.route("/api/echo", post(echo_handler));
|
|
|
|
|
|
|
|
|
|
// All routes (no auth required)
|
|
|
|
|
let app_routes = Router::new()
|
|
|
|
|
.nest(
|
|
|
|
|
"/api",
|
|
|
|
|
Router::new()
|
|
|
|
|
.merge(projects::projects_router())
|
|
|
|
|
.merge(tasks::tasks_router())
|
2025-06-20 23:03:29 +01:00
|
|
|
.merge(task_attempts::task_attempts_router())
|
2025-06-19 12:53:41 -04:00
|
|
|
.merge(filesystem::filesystem_router())
|
2025-06-24 10:50:20 +01:00
|
|
|
.merge(config::config_router())
|
|
|
|
|
.route("/sounds/:filename", get(serve_sound_file)),
|
2025-06-17 14:17:31 -04:00
|
|
|
)
|
2025-06-19 12:53:41 -04:00
|
|
|
.layer(Extension(pool.clone()))
|
|
|
|
|
.layer(Extension(config_arc));
|
2025-06-15 14:16:13 -04:00
|
|
|
|
|
|
|
|
let app = Router::new()
|
|
|
|
|
.merge(public_routes)
|
2025-06-17 14:17:31 -04:00
|
|
|
.merge(app_routes)
|
2025-06-17 11:24:03 -04:00
|
|
|
// Static file serving routes
|
|
|
|
|
.route("/", get(index_handler))
|
|
|
|
|
.route("/*path", get(static_handler))
|
2025-06-14 15:34:24 -04:00
|
|
|
.layer(Extension(pool))
|
2025-06-16 18:09:50 -04:00
|
|
|
.layer(Extension(app_state))
|
2025-06-14 15:14:08 -04:00
|
|
|
.layer(CorsLayer::permissive());
|
|
|
|
|
|
2025-06-17 20:51:04 -04:00
|
|
|
let port: u16 = if cfg!(debug_assertions) { 3001 } else { 0 }; // 0 = random port
|
2025-06-14 16:26:48 -04:00
|
|
|
|
2025-06-17 20:51:04 -04:00
|
|
|
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{port}")).await?;
|
|
|
|
|
let actual_port = listener.local_addr()?.port(); // get → 53427 (example)
|
|
|
|
|
|
|
|
|
|
tracing::info!("Server running on http://0.0.0.0:{actual_port}");
|
|
|
|
|
|
|
|
|
|
if !cfg!(debug_assertions) {
|
|
|
|
|
tracing::info!("Opening browser...");
|
|
|
|
|
open::that(format!("http://127.0.0.1:{actual_port}"))?;
|
|
|
|
|
}
|
2025-06-14 16:26:48 -04:00
|
|
|
|
2025-06-14 15:14:08 -04:00
|
|
|
axum::serve(listener, app).await?;
|
2025-06-14 16:26:48 -04:00
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|