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>
71 lines
2.1 KiB
Rust
71 lines
2.1 KiB
Rust
//! `maestro-web` binary entry point. See `lib.rs` for the architecture.
|
|
|
|
use std::error::Error;
|
|
use std::path::PathBuf;
|
|
use std::process::ExitCode;
|
|
|
|
use clap::Parser;
|
|
use maestro_web::config::{default_config_path, EnvOverrides};
|
|
use maestro_web::{load_config, router};
|
|
use tokio::net::TcpListener;
|
|
use tracing_subscriber::EnvFilter;
|
|
|
|
#[derive(Debug, Parser)]
|
|
#[command(
|
|
name = "maestro-web",
|
|
version,
|
|
about = "Serves the Maestro web UI and reverse-proxies to a co-located maestrod"
|
|
)]
|
|
struct Cli {
|
|
/// Override the unified Maestro config-file path.
|
|
#[arg(long, env = "MAESTRO_CONFIG")]
|
|
config: Option<PathBuf>,
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> ExitCode {
|
|
let cli = Cli::parse();
|
|
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
|
|
tracing_subscriber::fmt().with_env_filter(filter).init();
|
|
|
|
match run(cli.config).await {
|
|
Ok(()) => ExitCode::SUCCESS,
|
|
Err(e) => {
|
|
tracing::error!(error = %e, "maestro-web failed");
|
|
eprintln!("maestro-web: {e}");
|
|
ExitCode::FAILURE
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn run(config_override: Option<PathBuf>) -> Result<(), Box<dyn Error>> {
|
|
let path = match config_override {
|
|
Some(p) => p,
|
|
None => default_config_path()?,
|
|
};
|
|
let cfg = load_config(&path, EnvOverrides::from_env())?;
|
|
tracing::info!(
|
|
bind = %cfg.bind_addr,
|
|
daemon = %cfg.daemon_url,
|
|
dev_proxy = ?cfg.dev_proxy_target,
|
|
"loaded config",
|
|
);
|
|
if cfg.dev_proxy_target.is_some() {
|
|
tracing::warn!(
|
|
"dev_proxy_target is set in config but not yet honored by the server \
|
|
(NAP-241 v1 ships only the embedded-bundle path)"
|
|
);
|
|
}
|
|
|
|
let router = router(&cfg);
|
|
let listener = TcpListener::bind(cfg.bind_addr).await?;
|
|
tracing::info!(addr = %listener.local_addr()?, "serving");
|
|
axum::serve(listener, router)
|
|
.with_graceful_shutdown(async {
|
|
let _ = tokio::signal::ctrl_c().await;
|
|
tracing::info!("ctrl-c received — shutting down");
|
|
})
|
|
.await?;
|
|
Ok(())
|
|
}
|