Initial release: Ironpad v0.1.0 - Local-first, file-based project and knowledge management system. Rust backend, Vue 3 frontend, Milkdown editor, Git integration, cross-platform builds. Built with AI using Open Method.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
130
backend/src/main.rs
Normal file
130
backend/src/main.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{routing::get, Router};
|
||||
use tokio::net::TcpListener;
|
||||
use tower_http::cors::CorsLayer;
|
||||
use tower_http::services::ServeDir;
|
||||
use tracing::{info, warn};
|
||||
|
||||
pub mod config;
|
||||
mod models;
|
||||
mod routes;
|
||||
mod services;
|
||||
mod watcher;
|
||||
mod websocket;
|
||||
|
||||
/// Find an available port and return the bound listener.
|
||||
/// Avoids TOCTOU race by keeping the listener alive.
|
||||
async fn find_available_port() -> (TcpListener, u16) {
|
||||
for port in 3000..=3010 {
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
||||
if let Ok(listener) = TcpListener::bind(addr).await {
|
||||
return (listener, port);
|
||||
}
|
||||
}
|
||||
panic!("No available ports in range 3000–3010");
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Logging
|
||||
tracing_subscriber::fmt().init();
|
||||
|
||||
// Resolve data directory (production vs development mode)
|
||||
config::init_data_dir();
|
||||
|
||||
// Find port and bind (listener kept alive to avoid race condition)
|
||||
let (listener, port) = find_available_port().await;
|
||||
|
||||
// WebSocket state (shared across handlers)
|
||||
let ws_state = Arc::new(websocket::WsState::new());
|
||||
|
||||
// Start file watcher
|
||||
let ws_state_clone = ws_state.clone();
|
||||
if let Err(e) = watcher::start_watcher(ws_state_clone).await {
|
||||
warn!("File watcher failed to start: {}", e);
|
||||
}
|
||||
|
||||
// Initialize git repo if needed
|
||||
if let Err(e) = services::git::init_repo() {
|
||||
warn!("Git init skipped: {}", e);
|
||||
}
|
||||
|
||||
// Start auto-commit background task (tries to commit every 60s)
|
||||
services::git::start_auto_commit();
|
||||
|
||||
// CORS layer (permissive for local-only app)
|
||||
let cors = CorsLayer::permissive();
|
||||
|
||||
// API router
|
||||
let api_router = Router::new()
|
||||
// Notes CRUD
|
||||
.route(
|
||||
"/notes",
|
||||
get(routes::notes::list_notes).post(routes::notes::create_note),
|
||||
)
|
||||
.nest("/notes", routes::notes::router())
|
||||
// Tasks
|
||||
.nest("/tasks", routes::tasks::router())
|
||||
// Search
|
||||
.nest("/search", routes::search::router())
|
||||
// Git
|
||||
.nest("/git", routes::git::router())
|
||||
// Projects
|
||||
.nest("/projects", routes::projects::router())
|
||||
// Daily notes
|
||||
.nest("/daily", routes::daily::router())
|
||||
// Assets
|
||||
.nest("/assets", routes::assets::router());
|
||||
|
||||
// App router with WebSocket state
|
||||
let mut app = Router::new()
|
||||
.route("/health", get(|| async { "ok" }))
|
||||
.route(
|
||||
"/ws",
|
||||
get({
|
||||
let ws = ws_state.clone();
|
||||
move |upgrade: axum::extract::WebSocketUpgrade| {
|
||||
websocket::ws_handler(upgrade, axum::extract::State(ws))
|
||||
}
|
||||
}),
|
||||
)
|
||||
.nest("/api", api_router)
|
||||
.layer(cors);
|
||||
|
||||
// Check for embedded frontend (production mode)
|
||||
let static_dir = Path::new("static");
|
||||
let has_frontend = static_dir.join("index.html").exists();
|
||||
|
||||
if has_frontend {
|
||||
// Production mode: serve frontend from static/ and use SPA fallback
|
||||
info!("Production mode: serving frontend from static/");
|
||||
let serve_dir = ServeDir::new("static")
|
||||
.fallback(tower_http::services::ServeFile::new("static/index.html"));
|
||||
app = app.fallback_service(serve_dir);
|
||||
} else {
|
||||
// Development mode: API-only
|
||||
app = app.fallback(|| async {
|
||||
"Ironpad API server running. Use 'npm run dev' in frontend/ for the GUI."
|
||||
});
|
||||
}
|
||||
|
||||
// Start server
|
||||
info!("🚀 Ironpad running on http://localhost:{port}");
|
||||
|
||||
// Auto-open browser in production mode
|
||||
if has_frontend {
|
||||
let url = format!("http://localhost:{}", port);
|
||||
tokio::spawn(async move {
|
||||
// Small delay to ensure server is ready
|
||||
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
|
||||
if let Err(e) = webbrowser::open(&url) {
|
||||
tracing::warn!("Failed to open browser: {}. Open http://localhost:{} manually.", e, port);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
axum::serve(listener, app).await.expect("Server failed");
|
||||
}
|
||||
Reference in New Issue
Block a user