Skip to main content

track/webui/
server.rs

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