Skip to main content

track/webui/
server.rs

1//! WebUI server implementation.
2
3use crate::webui::routes::{self, WebState};
4use crate::webui::sse::sse_handler;
5use crate::webui::state::AppState;
6use crate::webui::templates::Templates;
7use axum::{
8    routing::{delete, get, patch, post},
9    Router,
10};
11use std::net::SocketAddr;
12use std::sync::Arc;
13use tokio::net::TcpListener;
14use tower_http::services::ServeDir;
15
16/// Build the WebUI router (shared by server startup and tests).
17pub fn build_router(web_state: WebState) -> Router {
18    Router::new()
19        // Pages
20        .route("/", get(routes::index))
21        // Card GET endpoints for HTMX updates
22        .route("/api/card/description", get(routes::get_description))
23        .route("/api/card/ticket", get(routes::get_ticket))
24        .route("/api/card/links", get(routes::get_links))
25        .route("/api/card/todos", get(routes::get_todos))
26        .route("/api/card/scraps", get(routes::get_scraps))
27        // Partial endpoints for HTMX
28        .route("/partials/todos", get(routes::get_todos))
29        .route("/partials/repos", get(routes::get_repos))
30        // API endpoints
31        .route("/api/status", get(routes::api_status))
32        .route("/api/todo", post(routes::add_todo))
33        .route("/api/todo/:id", delete(routes::delete_todo))
34        .route("/api/todo/:id/next", patch(routes::move_todo_to_next))
35        .route("/api/todo/:id/:status", patch(routes::update_todo_status))
36        .route("/api/scrap", post(routes::add_scrap))
37        .route("/api/description", post(routes::update_description))
38        .route("/api/ticket", post(routes::update_ticket))
39        .route("/api/link", post(routes::add_link))
40        .route("/api/link/:id", delete(routes::delete_link))
41        // SSE endpoint
42        .route("/api/sse", get(sse_handler))
43        // Static files (CSS, JS)
44        .nest_service("/static", ServeDir::new("static"))
45        .with_state(web_state)
46}
47
48/// Start the WebUI server
49pub async fn start_server(port: u16, open_browser: bool) -> anyhow::Result<()> {
50    // Initialize application state
51    let app_state = AppState::new()?;
52
53    // Initialize templates (embedded for single-binary distribution)
54    let templates = Arc::new(Templates::embedded());
55
56    let web_state = WebState {
57        app: app_state.clone(),
58        templates,
59    };
60
61    // Spawn background task to poll for database changes
62    let poll_state = app_state.clone();
63    tokio::spawn(async move {
64        poll_state.start_change_detection().await;
65    });
66
67    // Build router
68    let app = build_router(web_state);
69
70    let addr = SocketAddr::from(([127, 0, 0, 1], port));
71
72    println!("Starting track webui server...");
73    println!("  → http://localhost:{}", port);
74    println!();
75    println!("Press Ctrl+C to stop the server.");
76
77    // Open browser if requested
78    if open_browser {
79        let url = format!("http://localhost:{}", port);
80        if let Err(e) = open::that(&url) {
81            eprintln!("Warning: Failed to open browser: {}", e);
82        }
83    }
84
85    // Start server
86    let listener = TcpListener::bind(addr).await?;
87    axum::serve(listener, app).await?;
88
89    Ok(())
90}