Files
vibe-kanban/backend/src/main.rs

204 lines
6.1 KiB
Rust
Raw Normal View History

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;
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;
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()
}
}
}
}
async fn serve_sound_file(
axum::extract::Path(filename): axum::extract::Path<String>,
) -> impl IntoResponse {
use std::path::Path;
use tokio::fs;
// Validate filename contains only expected sound files
let valid_sounds = [
"abstract-sound1.mp3",
"abstract-sound2.mp3",
"abstract-sound3.mp3",
"abstract-sound4.mp3",
"cow-mooing.mp3",
"phone-vibration.mp3",
"rooster.mp3",
];
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);
match fs::read(&sound_path).await {
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-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
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://{}",
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
// 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))
.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())
.merge(filesystem::filesystem_router())
.merge(config::config_router())
.route("/sounds/:filename", get(serve_sound_file)),
)
.layer(Extension(pool.clone()))
.layer(Extension(config_arc));
2025-06-15 14:16:13 -04:00
let app = Router::new()
.merge(public_routes)
.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(())
}