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:
@@ -171,30 +171,58 @@
|
||||
await refreshSelected();
|
||||
}
|
||||
|
||||
// Cliente WASM opcional. Si `/static/wasm/cosmobiologia_web.js`
|
||||
// existe (= el usuario corrió `wasm-pack build` después de
|
||||
// `cargo build`), el rendering se hace localmente sin pedirle
|
||||
// el SVG al server por cada interacción. Si NO existe (deploy
|
||||
// mínimo), caemos al SSR de `/api/*.svg`. Toggle automático,
|
||||
// sin configuración.
|
||||
let wasm = null;
|
||||
async function tryLoadWasm() {
|
||||
try {
|
||||
const mod = await import('/static/wasm/cosmobiologia_web.js');
|
||||
await mod.default(); // wasm-pack: inicializa
|
||||
wasm = mod;
|
||||
document.getElementById('info').textContent = 'WASM cargado — render local';
|
||||
} catch (e) {
|
||||
console.info('WASM no disponible, usando SSR:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshSelected() {
|
||||
const offset = document.getElementById('offset').value || 0;
|
||||
const transit = document.getElementById('transit').checked ? '1' : '0';
|
||||
const params = new URLSearchParams({ offset_min: offset, transit });
|
||||
let svgUrl, infoUrl;
|
||||
if (mode === 'sky') {
|
||||
svgUrl = `/api/sky.svg`;
|
||||
infoUrl = `/api/sky`;
|
||||
const jsonUrl = mode === 'sky'
|
||||
? `/api/sky`
|
||||
: `/api/charts/${selectedChartId}/render?${params}`;
|
||||
const ssrUrl = mode === 'sky'
|
||||
? `/api/sky.svg`
|
||||
: `/api/charts/${selectedChartId}/wheel.svg?${params}`;
|
||||
|
||||
const render = await fetch(jsonUrl).then(r => r.json()).catch(() => null);
|
||||
let svg;
|
||||
if (wasm && render) {
|
||||
// Render local — WASM compose_wheel + draw_commands_to_svg.
|
||||
try {
|
||||
svg = wasm.render_model_to_svg(JSON.stringify(render), 600, 0);
|
||||
} catch (e) {
|
||||
console.warn('WASM render falló, fallback SSR:', e);
|
||||
svg = await fetch(ssrUrl).then(r => r.text());
|
||||
}
|
||||
} else {
|
||||
svgUrl = `/api/charts/${selectedChartId}/wheel.svg?${params}`;
|
||||
infoUrl = `/api/charts/${selectedChartId}/render?${params}`;
|
||||
// Fallback: SSR — el server devuelve el SVG ya compuesto.
|
||||
svg = await fetch(ssrUrl).then(r => r.text());
|
||||
}
|
||||
const [svg, render] = await Promise.all([
|
||||
fetch(svgUrl).then(r => r.text()),
|
||||
fetch(infoUrl).then(r => r.json()).catch(() => null),
|
||||
]);
|
||||
document.getElementById('wheel-container').innerHTML = svg;
|
||||
const info = document.getElementById('info');
|
||||
if (render) {
|
||||
const mode_label = wasm ? 'WASM' : 'SSR';
|
||||
info.innerHTML =
|
||||
`<b>${escapeHtml(render.title)}</b> · ` +
|
||||
`Asc ${render.ascendant_deg.toFixed(2)}° · ` +
|
||||
`MC ${render.midheaven_deg.toFixed(2)}° · ` +
|
||||
`${render.compute_ms} ms`;
|
||||
`${render.compute_ms} ms · ${mode_label}`;
|
||||
} else {
|
||||
info.textContent = '';
|
||||
}
|
||||
@@ -238,8 +266,11 @@
|
||||
}[c]));
|
||||
}
|
||||
|
||||
loadTree();
|
||||
loadSky();
|
||||
(async () => {
|
||||
await tryLoadWasm();
|
||||
await loadTree();
|
||||
await loadSky();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user