feat(server): web replay viewer (HTML/CSS + WASM bindings)
Wires the WASM module from the previous commit into a minimal web
viewer served at <server>/replays/<id>. Two new server routes:
- `GET /replays/:id` — returns the same embedded HTML page for any
id; the page itself reads the path from window.location in JS and
fetches the replay JSON via /api/replays/:id.
- `/web/*` — ServeDir for the static assets (replay.css, replay.js,
and the wasm-bindgen-generated pkg/).
Web layer:
- index.html — header, board, controls, status. Module script.
- replay.css — midnight-purple palette matching the desktop client,
dark felt board, CSS-grid pile layout, tableau fan via per-card
inline `top` offset.
- replay.js — fetches the replay, instantiates the wasm
ReplayPlayer, drives state(), step(). Controls: Restart, Play/Pause
toggle, Step. Auto-tick at 600 ms.
- pkg/ — generated by wasm-bindgen (committed so deployers don't
have to install wasm-bindgen-cli + the wasm32 target).
`tower-http = "0.6"` added to solitaire_server with the `fs` feature
for ServeDir.
To regenerate pkg/ after a solitaire_wasm change:
RUSTFLAGS='--cfg getrandom_backend="wasm_js"' \
cargo build -p solitaire_wasm \
--target wasm32-unknown-unknown --release
wasm-bindgen --target web \
--out-dir solitaire_server/web/pkg --no-typescript \
target/wasm32-unknown-unknown/release/solitaire_wasm.wasm
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ pub mod sync;
|
||||
use axum::{
|
||||
extract::DefaultBodyLimit,
|
||||
middleware as axum_middleware,
|
||||
response::Html,
|
||||
routing::{delete, get, post},
|
||||
Router,
|
||||
};
|
||||
@@ -25,6 +26,7 @@ use tower_governor::{
|
||||
key_extractor::SmartIpKeyExtractor,
|
||||
GovernorLayer,
|
||||
};
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
/// Shared application state injected into every Axum handler via [`axum::extract::State`].
|
||||
///
|
||||
@@ -104,10 +106,23 @@ fn build_router_inner(state: AppState, rate_limit: bool) -> Router {
|
||||
.route("/api/replays/{id}", get(replays::get_by_id))
|
||||
.route("/health", get(health));
|
||||
|
||||
// Replay web UI: a single HTML page served at `/replays/:id` plus a
|
||||
// ServeDir for the static assets (`web/index.html`, `web/replay.css`,
|
||||
// and the wasm-bindgen-generated `web/pkg/`). The HTML page is the
|
||||
// same regardless of `:id` — it reads the path from `location` in JS
|
||||
// and fetches the replay JSON from `/api/replays/:id`.
|
||||
let web = Router::new()
|
||||
.route(
|
||||
"/replays/{id}",
|
||||
get(|| async { Html(include_str!("../web/index.html")) }),
|
||||
)
|
||||
.nest_service("/web", ServeDir::new("solitaire_server/web"));
|
||||
|
||||
Router::new()
|
||||
.merge(protected)
|
||||
.merge(auth_routes)
|
||||
.merge(public)
|
||||
.merge(web)
|
||||
// Reject request bodies larger than 1 MB.
|
||||
.layer(DefaultBodyLimit::max(1024 * 1024))
|
||||
.with_state(state)
|
||||
|
||||
Reference in New Issue
Block a user