task-track 0.6.1

A JJ workspace-based task and TODO management CLI tool
Documentation
//! WebUI server implementation.

use crate::webui::routes::{self, WebState};
use crate::webui::sse::sse_handler;
use crate::webui::state::AppState;
use crate::webui::templates::Templates;
use axum::{
    routing::{delete, get, patch, post},
    Router,
};
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::net::TcpListener;
use tower_http::services::ServeDir;

/// Build the WebUI router (shared by server startup and tests).
pub fn build_router(web_state: WebState) -> Router {
    Router::new()
        // Pages
        .route("/", get(routes::index))
        // Card GET endpoints for HTMX updates
        .route("/api/card/description", get(routes::get_description))
        .route("/api/card/ticket", get(routes::get_ticket))
        .route("/api/card/links", get(routes::get_links))
        .route("/api/card/todos", get(routes::get_todos))
        .route("/api/card/scraps", get(routes::get_scraps))
        // Partial endpoints for HTMX
        .route("/partials/todos", get(routes::get_todos))
        .route("/partials/repos", get(routes::get_repos))
        // API endpoints
        .route("/api/status", get(routes::api_status))
        .route("/api/todo", post(routes::add_todo))
        .route("/api/todo/:id", delete(routes::delete_todo))
        .route("/api/todo/:id/next", patch(routes::move_todo_to_next))
        .route("/api/todo/:id/:status", patch(routes::update_todo_status))
        .route("/api/scrap", post(routes::add_scrap))
        .route("/api/description", post(routes::update_description))
        .route("/api/ticket", post(routes::update_ticket))
        .route("/api/link", post(routes::add_link))
        .route("/api/link/:id", delete(routes::delete_link))
        // SSE endpoint
        .route("/api/sse", get(sse_handler))
        // Static files (CSS, JS)
        .nest_service("/static", ServeDir::new("static"))
        .with_state(web_state)
}

/// Start the WebUI server
pub async fn start_server(port: u16, open_browser: bool) -> anyhow::Result<()> {
    // Initialize application state
    let app_state = AppState::new()?;

    // Initialize templates (embedded for single-binary distribution)
    let templates = Arc::new(Templates::embedded());

    let web_state = WebState {
        app: app_state.clone(),
        templates,
    };

    // Spawn background task to poll for database changes
    let poll_state = app_state.clone();
    tokio::spawn(async move {
        poll_state.start_change_detection().await;
    });

    // Build router
    let app = build_router(web_state);

    let addr = SocketAddr::from(([127, 0, 0, 1], port));

    println!("Starting track webui server...");
    println!("  → http://localhost:{}", port);
    println!();
    println!("Press Ctrl+C to stop the server.");

    // Open browser if requested
    if open_browser {
        let url = format!("http://localhost:{}", port);
        if let Err(e) = open::that(&url) {
            eprintln!("Warning: Failed to open browser: {}", e);
        }
    }

    // Start server
    let listener = TcpListener::bind(addr).await?;
    axum::serve(listener, app).await?;

    Ok(())
}