use std::path::{Path, PathBuf};
use axum::Router;
use tower_http::services::{ServeDir, ServeFile};
pub fn dashboard_router(dist_path: &Path) -> Router {
let index_html = dist_path.join("index.html");
let serve_dir = ServeDir::new(dist_path).fallback(ServeFile::new(index_html));
Router::new().fallback_service(serve_dir)
}
pub fn find_dashboard_dist() -> Option<PathBuf> {
find_dashboard_dist_in(
std::env::var("AAASM_DASHBOARD_DIST").ok(),
std::env::current_exe()
.ok()
.as_deref()
.and_then(Path::parent)
.and_then(Path::parent)
.map(Path::to_path_buf),
Some(Path::new(env!("CARGO_MANIFEST_DIR")).join("..")),
)
}
fn find_dashboard_dist_in(
env_override: Option<String>,
installed_root: Option<PathBuf>,
dev_root: Option<PathBuf>,
) -> Option<PathBuf> {
if let Some(env_path) = env_override {
let p = PathBuf::from(env_path);
if p.is_dir() {
return Some(p);
}
}
for root in [installed_root, dev_root].into_iter().flatten() {
let candidate = root.join("dashboard").join("dist");
if candidate.is_dir() {
return Some(candidate);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use axum::body::{to_bytes, Body};
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
fn make_stub_dist() -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(
dir.path().join("index.html"),
r#"<!doctype html><html><body><div id="root"></div></body></html>"#,
)
.expect("write index.html");
std::fs::create_dir_all(dir.path().join("assets")).expect("mkdir assets");
std::fs::write(dir.path().join("assets/main.js"), "export const main = () => 42;\n")
.expect("write assets/main.js");
dir
}
async fn get(router: Router, path: &str) -> axum::http::Response<Body> {
router
.oneshot(Request::builder().uri(path).body(Body::empty()).expect("build request"))
.await
.expect("router.oneshot")
}
#[tokio::test]
async fn dashboard_router_serves_index_at_root() {
let dist = make_stub_dist();
let response = get(dashboard_router(dist.path()), "/").await;
assert_eq!(response.status(), StatusCode::OK);
let bytes = to_bytes(response.into_body(), 64 * 1024).await.expect("body");
let body = std::str::from_utf8(&bytes).expect("utf8");
assert!(
body.contains(r#"<div id="root">"#),
"GET / must serve index.html with the React mount node; got: {body}"
);
}
#[tokio::test]
async fn dashboard_router_serves_static_assets_with_javascript_content_type() {
let dist = make_stub_dist();
let response = get(dashboard_router(dist.path()), "/assets/main.js").await;
assert_eq!(response.status(), StatusCode::OK);
let ctype = response
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or_default()
.to_owned();
assert!(
ctype.contains("javascript"),
"asset must be served with a JavaScript content-type; got {ctype:?}"
);
let bytes = to_bytes(response.into_body(), 64 * 1024).await.expect("body");
assert_eq!(
std::str::from_utf8(&bytes).expect("utf8"),
"export const main = () => 42;\n",
"asset body must match the file on disk"
);
}
#[test]
fn find_dashboard_dist_prefers_env_override() {
let tmp = tempfile::tempdir().expect("tempdir");
let installed = tempfile::tempdir().expect("installed root");
std::fs::create_dir_all(installed.path().join("dashboard").join("dist")).expect("installed dist");
let resolved = find_dashboard_dist_in(
Some(tmp.path().to_string_lossy().into_owned()),
Some(installed.path().to_path_buf()),
None,
);
assert_eq!(
resolved.as_deref(),
Some(tmp.path()),
"env-var override must beat the installed-layout root"
);
}
#[test]
fn find_dashboard_dist_falls_through_when_env_path_missing() {
let installed = tempfile::tempdir().expect("installed root");
let installed_dist = installed.path().join("dashboard").join("dist");
std::fs::create_dir_all(&installed_dist).expect("installed dist");
let resolved = find_dashboard_dist_in(
Some("/definitely/does/not/exist".to_string()),
Some(installed.path().to_path_buf()),
None,
);
assert_eq!(
resolved.as_deref(),
Some(installed_dist.as_path()),
"missing env-var path must fall through to the installed root"
);
}
#[test]
fn find_dashboard_dist_returns_none_when_no_candidate_resolves() {
let installed = tempfile::tempdir().expect("installed root"); let dev = tempfile::tempdir().expect("dev root");
let resolved = find_dashboard_dist_in(
None,
Some(installed.path().to_path_buf()),
Some(dev.path().to_path_buf()),
);
assert_eq!(resolved, None, "missing every candidate must yield None");
}
#[tokio::test]
async fn dashboard_router_falls_back_to_index_on_unknown_path() {
let dist = make_stub_dist();
let response = get(dashboard_router(dist.path()), "/agents/abc").await;
assert_eq!(
response.status(),
StatusCode::OK,
"SPA fallback must return 200, not 404"
);
let bytes = to_bytes(response.into_body(), 64 * 1024).await.expect("body");
let body = std::str::from_utf8(&bytes).expect("utf8");
assert!(
body.contains(r#"<div id="root">"#),
"fallback body must be index.html; got: {body}"
);
}
}