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
@@ -0,0 +1,7 @@
# Forzamos el backend `wasm_js` de getrandom para el target
# wasm32-unknown-unknown — sin esta flag, el getrandom transitivo
# vía `uuid → cosmobiologia-model → cosmobiologia-render` falla a
# compilar a WASM con "wasm32-unknown-unknown targets are not
# supported by default".
[target.wasm32-unknown-unknown]
rustflags = ['--cfg', 'getrandom_backend="wasm_js"']
@@ -0,0 +1,23 @@
[package]
name = "cosmobiologia-web"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
description = "Cosmobiología — cliente WASM. Reusa cosmobiologia-render para componer la rueda en SVG localmente, sin round-trip al server por cada interacción."
[dependencies]
cosmobiologia-render = { path = "../cosmobiologia-render" }
serde = { workspace = true }
serde_json = { workspace = true }
# wasm-bindgen solo se compila para target wasm32; en nativo el
# crate igual compila (rlib), pero esta API no se expone.
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = { workspace = true }
# Backend de getrandom para WASM: pide al embedder (browser) que
# provea la randomness vía Web Crypto. Se activa con el cfg
# `getrandom_backend = "wasm_js"` desde .cargo/config.toml.
getrandom = { version = "0.3", features = ["wasm_js"] }
[lib]
crate-type = ["cdylib", "rlib"]
@@ -0,0 +1,73 @@
//! `cosmobiologia-web` — cdylib WASM que renderiza la rueda
//! astrológica desde el browser, sin round-trip al server por cada
//! interacción.
//!
//! ## Flujo
//!
//! 1. El cliente JS hace `await fetch('/api/sky')` o
//! `/api/charts/:id/render?...` y recibe un `RenderModel` JSON.
//! 2. JS llama `render_model_to_svg(json)` (exportado desde WASM) que
//! deserializa + corre `cosmobiologia_render::compose_wheel` +
//! serializa SVG.
//! 3. JS hace `wheelContainer.innerHTML = svg`.
//!
//! ## Build
//!
//! ```bash
//! cargo install wasm-pack # una vez
//! cd crates/modules/cosmobiologia/cosmobiologia-web
//! wasm-pack build --target web --out-dir ../../../../apps/cosmobiologia-server/static/wasm
//! ```
//!
//! Esto produce un módulo ES6 (`cosmobiologia_web.js` +
//! `cosmobiologia_web_bg.wasm`) que el `index.html` del server
//! importa con `import init, { render_model_to_svg } from
//! '/static/wasm/cosmobiologia_web.js';`.
#![forbid(unsafe_code)]
#![warn(rust_2018_idioms)]
// La API pública SOLO se expone con `wasm-bindgen` en target
// wasm32. En nativo (rlib) el crate compila para validar la
// signature pero no exporta nada — los tests del render ya viven
// en `cosmobiologia-render::math`.
#[cfg(target_arch = "wasm32")]
mod wasm {
use cosmobiologia_render::{
compose_wheel, draw_commands_to_svg, CompositionOpts, RenderModel,
};
use wasm_bindgen::prelude::*;
/// Renderea un `RenderModel` (JSON string) como SVG. El JSON sale
/// de `/api/sky` o `/api/charts/:id/render` del server.
///
/// `size` es el lado del cuadrado contenedor en px (default 600).
/// `rot_offset_deg` permite rotar la vista (jog-dial / preview).
#[wasm_bindgen]
pub fn render_model_to_svg(json: &str, size: f32, rot_offset_deg: f32) -> Result<String, JsValue> {
let model: RenderModel = serde_json::from_str(json)
.map_err(|e| JsValue::from_str(&format!("parse RenderModel: {}", e)))?;
let opts = CompositionOpts {
size: if size > 0.0 { size } else { 600.0 },
rot_offset_deg,
include_bodies: true,
};
let cmds = compose_wheel(&model, &opts);
Ok(draw_commands_to_svg(&cmds, opts.size))
}
/// Hook de inicialización opcional — wasm_pack lo invoca al
/// cargar el módulo. Útil para instalar un panic hook hacia
/// `console.error`. Por ahora no-op.
#[wasm_bindgen(start)]
pub fn main_js() {}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn _native_marker() {
// Sin target wasm32, el crate solo expone el render como
// transitivo. Esta función vive para que `cargo check -p
// cosmobiologia-web` valide la compilación nativa sin
// wasm-bindgen — útil en CI y en desarrollo desktop.
let _ = std::any::type_name::<cosmobiologia_render::RenderModel>();
}