Files
vibe-kanban/backend/src/main.rs
Louis Knight-Webb fd0cdff0e4 Squashed commit of the following:
commit ca21aa40163902dfb20582d6dced8c884b4b0119
Author: Louis Knight-Webb <louis@bloop.ai>
Date:   Tue Jun 24 16:50:43 2025 +0100

    Fixes

commit 75c982209a71704d0df15982b9ac0aca87aa68de
Author: Louis Knight-Webb <louis@bloop.ai>
Date:   Tue Jun 24 16:35:58 2025 +0100

    Improve process killing

commit f58fd3b8a315880cc940d7e59719d23428c72e92
Author: Louis Knight-Webb <louis@bloop.ai>
Date:   Tue Jun 24 16:23:59 2025 +0100

    WIP

commit 7a6cd4772e15a5df0d760fe79776979c3ba206e8
Author: Louis Knight-Webb <louis@bloop.ai>
Date:   Tue Jun 24 12:34:13 2025 +0100

    Fix dev server activity not showing

commit 09eb3095c1850b5f3173b72b6b220811ef68524c
Author: Louis Knight-Webb <louis@bloop.ai>
Date:   Tue Jun 24 12:27:01 2025 +0100

    Add activity for dev server

commit 73db9a20312a8ed15c130760c6aacfa720d102d7
Author: Louis Knight-Webb <louis@bloop.ai>
Date:   Tue Jun 24 12:04:38 2025 +0100

    Lint

commit 0a0ad901773e14f634ded8a68a108efc2fbca0ae
Author: Louis Knight-Webb <louis@bloop.ai>
Date:   Tue Jun 24 12:01:37 2025 +0100

    WIP dev server
2025-06-24 16:50:58 +01:00

204 lines
6.1 KiB
Rust

use axum::{
body::Body,
extract::Extension,
http::{header, HeaderValue, StatusCode},
response::{IntoResponse, Json as ResponseJson, Response},
routing::{get, post},
Json, Router,
};
use rust_embed::RustEmbed;
use sqlx::{sqlite::SqliteConnectOptions, SqlitePool};
use std::str::FromStr;
use std::sync::Arc;
use tokio::sync::RwLock;
use tower_http::cors::CorsLayer;
mod app_state;
mod execution_monitor;
mod executor;
mod executors;
mod models;
mod routes;
mod utils;
use app_state::AppState;
use execution_monitor::execution_monitor;
use models::{ApiResponse, Config};
use routes::{config, filesystem, health, projects, task_attempts, tasks};
#[derive(RustEmbed)]
#[folder = "../frontend/dist"]
struct Assets;
async fn echo_handler(
Json(payload): Json<serde_json::Value>,
) -> ResponseJson<ApiResponse<serde_json::Value>> {
ResponseJson(ApiResponse {
success: true,
data: Some(payload),
message: Some("Echo successful".to_string()),
})
}
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(),
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt().init();
// Create asset directory if it doesn't exist
if !utils::asset_dir().exists() {
std::fs::create_dir_all(utils::asset_dir())?;
}
// Database connection
let database_url = format!(
"sqlite://{}",
utils::asset_dir().join("db.sqlite").to_string_lossy()
);
let options = SqliteConnectOptions::from_str(&database_url)?.create_if_missing(true);
let pool = SqlitePool::connect_with(options).await?;
sqlx::migrate!("./migrations").run(&pool).await?;
// Load configuration
let config_path = utils::config_path();
let config = Config::load(&config_path)?;
let config_arc = Arc::new(RwLock::new(config));
// Create app state
let app_state = AppState::new(pool.clone(), config_arc.clone());
// 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;
});
// Public routes (no auth required)
let public_routes = Router::new()
.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())
.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));
let app = Router::new()
.merge(public_routes)
.merge(app_routes)
// Static file serving routes
.route("/", get(index_handler))
.route("/*path", get(static_handler))
.layer(Extension(pool))
.layer(Extension(app_state))
.layer(CorsLayer::permissive());
let port: u16 = if cfg!(debug_assertions) { 3001 } else { 0 }; // 0 = random port
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}"))?;
}
axum::serve(listener, app).await?;
Ok(())
}