use axum::{
http::{header, HeaderValue, StatusCode},
response::{Html, IntoResponse},
routing::any_service,
Router,
};
use std::path::PathBuf;
use tower_http::services::{ServeDir, ServeFile};
const FALLBACK_INDEX_HTML: &str = r##"<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>mold</title>
<style>
:root { color-scheme: dark; }
html, body { margin: 0; height: 100%; font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; }
body {
background: radial-gradient(ellipse at top, #1e293b, #020617 60%);
color: #e2e8f0;
display: grid; place-items: center; padding: 2rem;
}
.card {
max-width: 32rem; padding: 2rem 2.25rem; border-radius: 1.25rem;
background: rgba(15,23,42,0.6); backdrop-filter: blur(16px);
border: 1px solid rgba(148,163,184,0.15);
box-shadow: 0 24px 60px -28px rgba(15,23,42,0.9);
}
h1 { margin: 0 0 0.5rem; font-size: 1.4rem; letter-spacing: -0.02em; }
p { margin: 0.5rem 0; line-height: 1.55; color: #94a3b8; }
a { color: #7dd3fc; text-decoration: none; }
a:hover { color: #bae6fd; }
code {
font: 13px ui-monospace, SFMono-Regular, Menlo, monospace;
background: rgba(56,189,248,0.08); color: #7dd3fc;
padding: 0.1rem 0.4rem; border-radius: 0.35rem;
}
</style>
</head>
<body>
<div class="card">
<h1>mold is running</h1>
<p>The web gallery UI isn't installed on this server yet.</p>
<p>Build the SPA with <code>cd web && bun install && bun run build</code>, then either set <code>MOLD_WEB_DIR</code> or copy <code>web/dist/</code> to <code>~/.mold/web</code>.</p>
<p>In the meantime: <a href="/api/docs">API docs</a> · <a href="/api/status">/api/status</a> · <a href="/api/gallery">/api/gallery</a></p>
</div>
</body>
</html>
"##;
pub fn router() -> Router {
match resolve_web_dir() {
Some(dir) => {
tracing::info!(path = %dir.display(), "serving web UI");
let index = dir.join("index.html");
let service = ServeDir::new(&dir)
.append_index_html_on_directories(true)
.fallback(ServeFile::new(index));
Router::new().fallback_service(any_service(service))
}
None => {
tracing::info!("no web UI bundle found; serving inline placeholder");
Router::new().fallback(fallback_placeholder)
}
}
}
async fn fallback_placeholder() -> impl IntoResponse {
let mut resp = Html(FALLBACK_INDEX_HTML).into_response();
resp.headers_mut()
.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
*resp.status_mut() = StatusCode::OK;
resp
}
fn resolve_web_dir() -> Option<PathBuf> {
let mut candidates: Vec<PathBuf> = Vec::new();
if let Ok(dir) = std::env::var("MOLD_WEB_DIR") {
candidates.push(PathBuf::from(dir));
}
if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
candidates.push(PathBuf::from(xdg).join("mold").join("web"));
} else if let Ok(home) = std::env::var("HOME") {
candidates.push(
PathBuf::from(&home)
.join(".local")
.join("share")
.join("mold")
.join("web"),
);
}
if let Ok(home) = std::env::var("HOME") {
candidates.push(PathBuf::from(home).join(".mold").join("web"));
}
if let Ok(exe) = std::env::current_exe() {
if let Some(parent) = exe.parent() {
candidates.push(parent.join("web"));
candidates.push(parent.join("..").join("share").join("mold").join("web"));
}
}
candidates.push(PathBuf::from("web").join("dist"));
candidates
.into_iter()
.find(|p| p.join("index.html").is_file())
}