Skip to main content

awa_ui/
lib.rs

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