Skip to main content

awa_ui/
lib.rs

1pub mod error;
2pub mod handlers;
3pub mod state;
4
5use axum::http::header;
6use axum::response::{Html, IntoResponse, Response};
7use axum::routing::{get, post};
8use axum::Router;
9use rust_embed::Embed;
10use sqlx::PgPool;
11use tower_http::cors::CorsLayer;
12
13use crate::state::{detect_read_only, AppState};
14
15#[derive(Embed)]
16#[folder = "static/"]
17struct StaticAssets;
18
19/// Create the awa-ui router with all API routes and static file serving.
20pub async fn router(pool: PgPool) -> Result<Router, sqlx::Error> {
21    let read_only = detect_read_only(&pool).await?;
22    let state = AppState::new(pool, read_only);
23    let api = Router::new()
24        // Jobs
25        .route("/jobs", get(handlers::jobs::list_jobs))
26        .route("/jobs/{id}", get(handlers::jobs::get_job))
27        .route("/jobs/{id}/retry", post(handlers::jobs::retry_job))
28        .route("/jobs/{id}/cancel", post(handlers::jobs::cancel_job))
29        .route("/jobs/bulk-retry", post(handlers::jobs::bulk_retry))
30        .route("/jobs/bulk-cancel", post(handlers::jobs::bulk_cancel))
31        // Queues
32        .route("/queues", get(handlers::queues::list_queues))
33        .route(
34            "/queues/runtime",
35            get(handlers::runtime::list_queue_runtime),
36        )
37        .route("/queues/{queue}/pause", post(handlers::queues::pause_queue))
38        .route(
39            "/queues/{queue}/resume",
40            post(handlers::queues::resume_queue),
41        )
42        .route("/queues/{queue}/drain", post(handlers::queues::drain_queue))
43        // Cron
44        .route("/cron", get(handlers::cron::list_cron_jobs))
45        .route(
46            "/cron/{name}/trigger",
47            post(handlers::cron::trigger_cron_job),
48        )
49        // Stats
50        .route("/stats", get(handlers::stats::get_stats))
51        .route("/stats/timeseries", get(handlers::stats::get_timeseries))
52        .route("/stats/kinds", get(handlers::stats::get_distinct_kinds))
53        .route("/stats/queues", get(handlers::stats::get_distinct_queues))
54        .route("/capabilities", get(handlers::stats::get_capabilities))
55        // Runtime
56        .route("/runtime", get(handlers::runtime::get_runtime));
57
58    Ok(Router::new()
59        .nest("/api", api)
60        .fallback(static_handler)
61        .layer(CorsLayer::permissive())
62        .with_state(state))
63}
64
65/// Serve embedded static files, falling back to index.html for SPA routing.
66async fn static_handler(uri: axum::http::Uri) -> Response {
67    let path = uri.path().trim_start_matches('/');
68
69    // Try to serve the exact file
70    if let Some(file) = StaticAssets::get(path) {
71        let mime = mime_guess::from_path(path).first_or_octet_stream();
72        return ([(header::CONTENT_TYPE, mime.as_ref())], file.data.to_vec()).into_response();
73    }
74
75    // SPA fallback: serve index.html for non-API routes
76    if let Some(index) = StaticAssets::get("index.html") {
77        return Html(index.data.to_vec()).into_response();
78    }
79
80    // No frontend built — serve placeholder
81    Html(PLACEHOLDER_HTML).into_response()
82}
83
84const PLACEHOLDER_HTML: &str = r#"<!DOCTYPE html>
85<html>
86<head><title>AWA</title></head>
87<body>
88  <h1>AWA Job Queue</h1>
89  <p>API available at <a href="/api/stats">/api/stats</a></p>
90  <p>Frontend not built. Run <code>cd awa-ui/frontend && npm install && npm run build</code> to build the UI.</p>
91</body>
92</html>"#;