feat(server): embed SPA via memory-serve behind embed-web feature

Adds `memory-serve` 2.1 as an optional workspace dependency, a `build.rs`
that runs `load_directory` only when `CARGO_FEATURE_EMBED_WEB` is set, a
`web_assets` module serving `web/dist` at `/` with SPA fallback (200 OK)
for unknown client-side routes, and a feature-gated integration test.

The default build (no feature) compiles and tests cleanly without `web/dist`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 23:22:26 +02:00
parent 1d1be5fbe9
commit 7170be016d
7 changed files with 240 additions and 0 deletions
+14
View File
@@ -2,6 +2,9 @@
mod config;
#[cfg(feature = "embed-web")]
mod web_assets;
pub use config::Config;
use anyhow::Context;
@@ -65,6 +68,9 @@ pub async fn run(config: Config) -> anyhow::Result<()> {
pub async fn serve(listener: TcpListener, state: AppState) -> anyhow::Result<()> {
let app = build_app(state);
#[cfg(feature = "embed-web")]
let app = app.merge(web_assets::routes());
axum::serve(listener, app)
.await
.context("running the HTTP server")?;
@@ -72,6 +78,14 @@ pub async fn serve(listener: TcpListener, state: AppState) -> anyhow::Result<()>
Ok(())
}
#[cfg(feature = "embed-web")]
pub mod test_support {
/// The SPA-asset router, for tests.
pub fn web_router() -> axum::Router {
super::web_assets::routes()
}
}
/// Create a user from the CLI (admin bootstrap). Opens its own connection (CLI
/// one-shot); reads the password from the `BOOTSTRAP_PASSWORD` env var if set,
/// otherwise prompts (hidden input). The plaintext is not zeroized, but it is