feat(cosmobiologia): crate WASM + fallback inteligente + DEPLOY.md (fase 3b)

Cierra el requerimiento del módulo web. El cliente puede correr en
modo WASM (render local, scrubbing instantáneo, sin round-trip) o
caer al SSR (server compone el SVG) si el bundle WASM no está
desplegado. Switch automático sin configuración.

cosmobiologia-web (crate nuevo, cdylib + rlib):
- `lib.rs` con un único export wasm-bindgen
  `render_model_to_svg(json, size, rot_offset_deg) -> String` que
  deserializa un `RenderModel`, llama `compose_wheel` +
  `draw_commands_to_svg` de cosmobiologia-render, y devuelve el
  SVG inline listo para `wheel.innerHTML = svg`.
- Cargo.toml con `wasm-bindgen` + `getrandom` con feature
  `wasm_js` solo bajo `target_arch = "wasm32"` (en nativo no se
  arrastran).
- `.cargo/config.toml` con `--cfg getrandom_backend="wasm_js"`
  para que la transitividad
  `uuid → cosmobiologia-model → cosmobiologia-render` compile a
  wasm32-unknown-unknown.
- `cargo check -p cosmobiologia-web` pasa en nativo (valida la
  signature). Build WASM real lo dispara el usuario con
  `wasm-pack build --target web --out-dir ../../../apps/
  cosmobiologia-server/static/wasm` — comando documentado en
  DEPLOY.md y en doc del crate.

cosmobiologia-server — soporte cliente WASM:
- Nuevo flag `--static-wasm <dir>` (default = static/wasm relativo
  al cwd). Si el directorio existe, los archivos WASM se sirven
  en `/static/wasm/*`. Si no existe, devuelve 404 y el cliente
  cae al SSR.
- ServeDir de `tower-http` para fileserver simple.

index.html:
- Nueva función `tryLoadWasm()` que hace `import dinámico` del
  módulo WASM al boot. Si carga OK, `wasm` global queda set; si
  falla (archivo no existe o error de WASM), se loguea info y
  sigue.
- `refreshSelected()` ahora hace fetch del RenderModel JSON
  (`/api/sky` o `/api/charts/:id/render`); si hay WASM, llama
  `wasm.render_model_to_svg(json)` localmente; si no hay WASM o
  el render WASM falla, hace fetch del SVG SSR como fallback.
- Info row muestra "WASM" o "SSR" según el modo activo —
  visualmente claro qué pipeline está corriendo.

cosmobiologia-server/DEPLOY.md (nuevo):
- Build del binario + build del WASM (con wasm-pack).
- systemd service template (sandboxing básico: ProtectSystem
  strict, ProtectHome, PrivateTmp, NoNewPrivileges).
- Caddyfile y nginx para reverse proxy con TLS.
- DNS: A records para cosmobiologia.gioser.net + api.*.
- CORS: warnings sobre permissive vs producción multi-usuario.
- Separación demo público (DB vacía en VPS) vs desktop personal
  (DB compartida en `~/.local/share/cosmobiologia/`).
- Backup con SQLite `.backup`.
- Smoke test post-deploy con curl.
- Tabla de referencia de TODOS los endpoints.

Tests: 10 verdes (cosmobiologia-render::math). El cliente WASM
no agrega tests propios — la lógica testeable vive en render.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-19 01:25:48 +00:00
parent eac8c58974
commit 4619ba3a2b
8 changed files with 467 additions and 14 deletions
+10 -1
View File
@@ -53,6 +53,7 @@ use cosmobiologia_model::{
use cosmobiologia_store::Store;
use serde::{Deserialize, Serialize};
use tower_http::cors::CorsLayer;
use tower_http::services::ServeDir;
use tower_http::trace::TraceLayer;
use tracing::info;
@@ -73,6 +74,12 @@ struct Cli {
/// (`$XDG_DATA_HOME/cosmobiologia/charts.db`).
#[arg(long)]
db: Option<PathBuf>,
/// Directorio con los assets estáticos del cliente WASM
/// (output de `wasm-pack build --out-dir <este path>`). Si el
/// directorio no existe, el endpoint `/static/wasm/*` devuelve
/// 404 y el cliente cae al SSR.
#[arg(long, default_value = "crates/apps/cosmobiologia-server/static/wasm")]
static_wasm: PathBuf,
}
#[derive(Clone)]
@@ -102,7 +109,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let store = Arc::new(Store::open(&db_path)?);
let state = AppState { store };
let app = router().with_state(state);
let app = router()
.nest_service("/static/wasm", ServeDir::new(&cli.static_wasm))
.with_state(state);
let addr: SocketAddr = format!("{}:{}", cli.bind, cli.port).parse()?;
info!("listening on http://{}", addr);