New crate crates/maestro-web/: axum server that * embeds the web/dist SPA bundle via include_dir! (graceful placeholder when pnpm build hasn't run yet — keeps fresh checkouts compiling) * reverse-proxies /api/* HTTP to a co-located daemon (default http://127.0.0.1:7070) via reqwest, stripping hop-by-hop headers * reverse-proxies /api/attach/:id and /api/events WebSockets via tokio_tungstenite::connect_async + a tokio task pumping both directions * reads bind_addr / daemon_url / dev_proxy_target from the new [web] config section; env-var overrides MAESTRO_WEB_BIND_ADDR / _DAEMON_URL / _DEV_PROXY_TARGET Integration tests spin up a mock daemon + maestro-web pair on random ports and verify HTTP forward, SPA fallback, WS echo, and 502 on unreachable daemon. Adds a top-level Justfile (`just web` = pnpm install + pnpm build + cargo build), crates/maestro-web/README.md (config + trust model + dev workflow), and a docs/INSTALL.md section on optional web UI install. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
49 lines
1.8 KiB
Rust
49 lines
1.8 KiB
Rust
//! HTTP server wiring — composes the static bundle, the HTTP proxy, and the
|
|
//! WS proxy into a single axum router.
|
|
|
|
use axum::extract::{Request, State, WebSocketUpgrade};
|
|
use axum::response::{IntoResponse, Response};
|
|
use axum::routing::{any, get};
|
|
use axum::Router;
|
|
|
|
use crate::config::WebConfig;
|
|
use crate::proxy::{pump_ws, upstream_ws_url, ProxyState, UpstreamConfig};
|
|
use crate::static_bundle;
|
|
|
|
/// Build the public router. The returned router is ready to hand to
|
|
/// `axum::serve`.
|
|
pub fn router(config: &WebConfig) -> Router {
|
|
let upstream = UpstreamConfig::from_daemon_url(&config.daemon_url);
|
|
let proxy_state = ProxyState::new(upstream);
|
|
|
|
// Mount each route at its real path (not under `nest`, which would strip
|
|
// the `/api` prefix before the proxy could forward it). WS routes go
|
|
// first so the HTTP fallback doesn't shadow them.
|
|
Router::new()
|
|
.route("/api/attach/:id", any(ws_proxy))
|
|
.route("/api/events", any(ws_proxy))
|
|
.route("/", get(serve_index))
|
|
.fallback(catch_all)
|
|
.with_state(proxy_state)
|
|
}
|
|
|
|
/// Default handler: `/api/*` goes to the daemon, anything else falls through
|
|
/// to the embedded SPA bundle (with SPA-style fallback to `index.html`).
|
|
async fn catch_all(State(state): State<ProxyState>, req: Request) -> Response {
|
|
if req.uri().path().starts_with("/api/") {
|
|
return crate::proxy::proxy_http(State(state), req).await;
|
|
}
|
|
static_bundle::serve_path(req.uri().path())
|
|
}
|
|
|
|
async fn ws_proxy(State(state): State<ProxyState>, ws: WebSocketUpgrade, req: Request) -> Response {
|
|
let upstream_url = upstream_ws_url(&state, req.uri());
|
|
ws.on_upgrade(move |socket| async move {
|
|
pump_ws(socket, upstream_url).await;
|
|
})
|
|
}
|
|
|
|
async fn serve_index() -> Response {
|
|
static_bundle::serve_index().into_response()
|
|
}
|