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:
Generated
+11
@@ -2355,6 +2355,17 @@ dependencies = [
|
|||||||
"yahweh-widget-tree",
|
"yahweh-widget-tree",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cosmobiologia-web"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"cosmobiologia-render",
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ members = [
|
|||||||
"crates/modules/cosmobiologia/cosmobiologia-canvas",
|
"crates/modules/cosmobiologia/cosmobiologia-canvas",
|
||||||
"crates/modules/cosmobiologia/cosmobiologia-tree",
|
"crates/modules/cosmobiologia/cosmobiologia-tree",
|
||||||
"crates/modules/cosmobiologia/cosmobiologia-panel",
|
"crates/modules/cosmobiologia/cosmobiologia-panel",
|
||||||
|
"crates/modules/cosmobiologia/cosmobiologia-web",
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# apps/ — apps que consumen el protocolo (yahweh modules+shell)
|
# apps/ — apps que consumen el protocolo (yahweh modules+shell)
|
||||||
|
|||||||
@@ -0,0 +1,298 @@
|
|||||||
|
# Cosmobiología — guía de deploy
|
||||||
|
|
||||||
|
Server HTTP single-user, escrito en Rust + axum. Sirve cartas
|
||||||
|
astrológicas computadas con `cosmobiologia-engine` (VSOP2013 en Rust
|
||||||
|
puro) y la página web HTML/JS del cliente. Diseñado para correr
|
||||||
|
**local** o detrás de un reverse proxy con TLS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Build
|
||||||
|
|
||||||
|
### Binario del server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo build --release -p cosmobiologia-server
|
||||||
|
# ./target/release/cosmobiologia-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cliente WASM (opcional pero recomendado)
|
||||||
|
|
||||||
|
Sin esto, el cliente cae al **SSR**: cada interacción pide al server
|
||||||
|
el SVG recompuesto (~12 KB por click). Con WASM, el cliente compone
|
||||||
|
localmente — primera carga ~150 KB, después scrubbing instantáneo
|
||||||
|
sin round-trip.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Una sola vez:
|
||||||
|
cargo install wasm-pack
|
||||||
|
|
||||||
|
# Cada vez que cambie cosmobiologia-render o cosmobiologia-web:
|
||||||
|
cd crates/modules/cosmobiologia/cosmobiologia-web
|
||||||
|
wasm-pack build --release --target web \
|
||||||
|
--out-dir ../../../../apps/cosmobiologia-server/static/wasm
|
||||||
|
```
|
||||||
|
|
||||||
|
`wasm-pack` produce `cosmobiologia_web.js` +
|
||||||
|
`cosmobiologia_web_bg.wasm` en
|
||||||
|
`crates/apps/cosmobiologia-server/static/wasm/`. El server los sirve
|
||||||
|
en `/static/wasm/*` y el `index.html` los importa con
|
||||||
|
`import init, { render_model_to_svg } from
|
||||||
|
'/static/wasm/cosmobiologia_web.js'`.
|
||||||
|
|
||||||
|
Si el directorio NO existe (build incompleto), el server devuelve
|
||||||
|
404 y el cliente cae al SSR automáticamente — sin error visible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Levantar el server
|
||||||
|
|
||||||
|
### Local (single-user, sin reverse proxy)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./target/release/cosmobiologia-server \
|
||||||
|
--port 8787 \
|
||||||
|
--bind 127.0.0.1 \
|
||||||
|
--db ~/.local/share/cosmobiologia/charts.db
|
||||||
|
```
|
||||||
|
|
||||||
|
Abrí `http://127.0.0.1:8787/`. La DB es la misma que usa la app
|
||||||
|
desktop — cualquier carta creada en la app aparece en el browser
|
||||||
|
y viceversa.
|
||||||
|
|
||||||
|
### systemd (server público vía VPS)
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# /etc/systemd/system/cosmobiologia.service
|
||||||
|
[Unit]
|
||||||
|
Description=Cosmobiología (server astrológico)
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=cosmobio
|
||||||
|
Group=cosmobio
|
||||||
|
WorkingDirectory=/opt/cosmobiologia
|
||||||
|
ExecStart=/opt/cosmobiologia/cosmobiologia-server \
|
||||||
|
--port 8787 \
|
||||||
|
--bind 127.0.0.1 \
|
||||||
|
--db /var/lib/cosmobiologia/charts.db \
|
||||||
|
--static-wasm /opt/cosmobiologia/static/wasm
|
||||||
|
Environment=RUST_LOG=cosmobiologia_server=info,tower_http=warn
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=3
|
||||||
|
# Sandboxing básico
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ReadWritePaths=/var/lib/cosmobiologia
|
||||||
|
NoNewPrivileges=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo useradd -r -s /usr/sbin/nologin cosmobio
|
||||||
|
sudo mkdir -p /opt/cosmobiologia/static/wasm /var/lib/cosmobiologia
|
||||||
|
sudo cp target/release/cosmobiologia-server /opt/cosmobiologia/
|
||||||
|
sudo cp -r crates/apps/cosmobiologia-server/static/wasm/* \
|
||||||
|
/opt/cosmobiologia/static/wasm/
|
||||||
|
sudo chown -R cosmobio:cosmobio /opt/cosmobiologia /var/lib/cosmobiologia
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now cosmobiologia
|
||||||
|
sudo systemctl status cosmobiologia
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Reverse proxy (HTTPS + DNS bonito)
|
||||||
|
|
||||||
|
Con dos subdominios apuntando al host:
|
||||||
|
|
||||||
|
| DNS | Función |
|
||||||
|
|-----|---------|
|
||||||
|
| `cosmobiologia.gioser.net` | página web (HTML + WASM) |
|
||||||
|
| `api.cosmobiologia.gioser.net` | endpoints `/api/*` (JSON / SVG) |
|
||||||
|
|
||||||
|
Hoy el server sirve los dos roles en el mismo puerto — el split por
|
||||||
|
subdominio lo hace el proxy, **sin cambiar nada del Rust**.
|
||||||
|
|
||||||
|
### Caddyfile (recomendado — TLS automático con Let's Encrypt)
|
||||||
|
|
||||||
|
```Caddyfile
|
||||||
|
cosmobiologia.gioser.net {
|
||||||
|
encode gzip zstd
|
||||||
|
# Página web + estáticos + WASM
|
||||||
|
@api path /api/*
|
||||||
|
handle @api {
|
||||||
|
# Si el cliente pega un /api/ directo al subdominio principal,
|
||||||
|
# lo dejamos pasar (más amigable que 404).
|
||||||
|
reverse_proxy 127.0.0.1:8787
|
||||||
|
}
|
||||||
|
handle {
|
||||||
|
reverse_proxy 127.0.0.1:8787
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
api.cosmobiologia.gioser.net {
|
||||||
|
encode gzip zstd
|
||||||
|
# Solo los endpoints /api/*; rechaza el resto.
|
||||||
|
@api path /api/*
|
||||||
|
handle @api {
|
||||||
|
reverse_proxy 127.0.0.1:8787
|
||||||
|
}
|
||||||
|
handle {
|
||||||
|
respond "Use cosmobiologia.gioser.net para la página" 404
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### nginx (alternativa)
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# /etc/nginx/sites-available/cosmobiologia
|
||||||
|
server {
|
||||||
|
server_name cosmobiologia.gioser.net;
|
||||||
|
listen 443 ssl http2;
|
||||||
|
# ssl_certificate / ssl_certificate_key — vía certbot
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8787;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
}
|
||||||
|
gzip on;
|
||||||
|
gzip_types application/javascript application/wasm image/svg+xml application/json text/css text/html;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
server_name api.cosmobiologia.gioser.net;
|
||||||
|
listen 443 ssl http2;
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://127.0.0.1:8787;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
}
|
||||||
|
location / { return 404; }
|
||||||
|
gzip on;
|
||||||
|
gzip_types application/json image/svg+xml;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### DNS
|
||||||
|
|
||||||
|
A records (o AAAA si IPv6) hacia tu VPS:
|
||||||
|
|
||||||
|
```
|
||||||
|
cosmobiologia.gioser.net. A <ip-del-VPS>
|
||||||
|
api.cosmobiologia.gioser.net. A <ip-del-VPS>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. CORS y separación cliente↔API
|
||||||
|
|
||||||
|
Hoy el server tiene `CorsLayer::permissive()` — cualquier origen
|
||||||
|
puede hacer fetch contra `/api/*`. Eso es OK para:
|
||||||
|
|
||||||
|
- **Single-user local**: nadie más alcanza al server.
|
||||||
|
- **Demo público single-tenant**: misma DB para todos los visitantes,
|
||||||
|
sin datos sensibles. Los visitantes pueden leer y crear cartas
|
||||||
|
públicamente (es la naturaleza del demo).
|
||||||
|
|
||||||
|
**No use CorsLayer::permissive en producción multi-usuario**. Para
|
||||||
|
eso hay que:
|
||||||
|
1. Agregar auth (sesiones / JWT / API key).
|
||||||
|
2. Reemplazar con `CorsLayer::new().allow_origin(["https://cosmobiologia.gioser.net".parse().unwrap()])`.
|
||||||
|
3. Volverte el `AllowCredentials::yes()` si vas a usar cookies.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Separación demo público ↔ desktop personal
|
||||||
|
|
||||||
|
El path por default de la DB (`~/.local/share/cosmobiologia/charts.db`)
|
||||||
|
es **compartido entre el server y la app desktop**. Eso es lo que
|
||||||
|
querés en tu máquina local — abrís el browser y ves las mismas
|
||||||
|
cartas que tenés en la app gpui.
|
||||||
|
|
||||||
|
**Pero NO querés que el server público en
|
||||||
|
`cosmobiologia.gioser.net` exponga TUS cartas privadas**. Para el
|
||||||
|
demo público:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# En tu VPS:
|
||||||
|
mkdir -p /var/lib/cosmobiologia
|
||||||
|
# Empezás con DB vacía (la app crea las tablas al primer arranque).
|
||||||
|
cosmobiologia-server --db /var/lib/cosmobiologia/charts.db
|
||||||
|
```
|
||||||
|
|
||||||
|
Si querés precargar cartas demo (Einstein, una carta natal pública),
|
||||||
|
podés copiarlas desde tu DB local con la app, exportarlas como JSON
|
||||||
|
via `/api/charts/:id`, y postearlas al server público con POST
|
||||||
|
`/api/charts`. O simplemente abrir el browser, ir a "Nuevo
|
||||||
|
contacto" → "Nueva carta…" y cargarlas a mano.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Backup
|
||||||
|
|
||||||
|
La DB SQLite es **un solo archivo**. Backup = `cp` (mientras el
|
||||||
|
server está parado, o usá `sqlite3 charts.db ".backup
|
||||||
|
charts.bak"` con el server corriendo).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Snapshot diario sin parar el server
|
||||||
|
sqlite3 /var/lib/cosmobiologia/charts.db ".backup /var/backups/cosmobiologia-$(date +%F).db"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Smoke test post-deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Desde tu máquina:
|
||||||
|
curl https://cosmobiologia.gioser.net/api/health
|
||||||
|
# → {"status":"ok","service":"cosmobiologia-server"}
|
||||||
|
|
||||||
|
curl https://cosmobiologia.gioser.net/api/sky | jq .title
|
||||||
|
# → "Cielo 2026-05-19 00:55 UTC"
|
||||||
|
|
||||||
|
# Abrí la página:
|
||||||
|
open https://cosmobiologia.gioser.net/
|
||||||
|
# (deberías ver la rueda del cielo + sidebar con "Cielo ahora")
|
||||||
|
```
|
||||||
|
|
||||||
|
Si el cliente WASM cargó, en la barra inferior verás "WASM".
|
||||||
|
Si cayó al SSR, verás "SSR". Ambos modos son funcionales.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Endpoints públicos (referencia)
|
||||||
|
|
||||||
|
| Método | Path | Función |
|
||||||
|
|--------|------|---------|
|
||||||
|
| GET | `/api/health` | healthcheck |
|
||||||
|
| GET | `/api/tree` | árbol completo (groups/contacts/charts) |
|
||||||
|
| GET | `/api/sky` | RenderModel "Cielo ahora" |
|
||||||
|
| GET | `/api/sky.svg` | SVG agnóstico del cielo (server-side) |
|
||||||
|
| GET | `/api/charts/:id` | Chart JSON |
|
||||||
|
| GET | `/api/charts/:id/render?...` | RenderModel con overlays |
|
||||||
|
| GET | `/api/charts/:id/svg?...` | SVG vía engine (svg_export) |
|
||||||
|
| GET | `/api/charts/:id/wheel.svg?...` | SVG vía render agnóstico |
|
||||||
|
| POST | `/api/charts` | crear carta |
|
||||||
|
| PATCH | `/api/charts/:id` | editar label/birth/config |
|
||||||
|
| DELETE | `/api/charts/:id` | borrar |
|
||||||
|
| POST | `/api/groups` | crear grupo |
|
||||||
|
| PATCH | `/api/groups/:id` | renombrar |
|
||||||
|
| DELETE | `/api/groups/:id` | borrar |
|
||||||
|
| POST | `/api/contacts` | crear contacto |
|
||||||
|
| PATCH | `/api/contacts/:id` | renombrar |
|
||||||
|
| DELETE | `/api/contacts/:id` | borrar |
|
||||||
|
|
||||||
|
Query params del render (`?...`):
|
||||||
|
|
||||||
|
- `offset_min=<i64>` — time scrubbing (minutos desde el natal).
|
||||||
|
- `transit=1` — activa overlay de tránsito al `now` del server.
|
||||||
|
- `prog_age=<f64>` — progresión secundaria a edad N.
|
||||||
|
- `sa_age=<f64>` — solar arc a edad N.
|
||||||
|
- `pd_age=<f64>` — primary directions GR (Naibod).
|
||||||
@@ -53,6 +53,7 @@ use cosmobiologia_model::{
|
|||||||
use cosmobiologia_store::Store;
|
use cosmobiologia_store::Store;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tower_http::cors::CorsLayer;
|
use tower_http::cors::CorsLayer;
|
||||||
|
use tower_http::services::ServeDir;
|
||||||
use tower_http::trace::TraceLayer;
|
use tower_http::trace::TraceLayer;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
@@ -73,6 +74,12 @@ struct Cli {
|
|||||||
/// (`$XDG_DATA_HOME/cosmobiologia/charts.db`).
|
/// (`$XDG_DATA_HOME/cosmobiologia/charts.db`).
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
db: Option<PathBuf>,
|
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)]
|
#[derive(Clone)]
|
||||||
@@ -102,7 +109,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let store = Arc::new(Store::open(&db_path)?);
|
let store = Arc::new(Store::open(&db_path)?);
|
||||||
|
|
||||||
let state = AppState { store };
|
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()?;
|
let addr: SocketAddr = format!("{}:{}", cli.bind, cli.port).parse()?;
|
||||||
info!("listening on http://{}", addr);
|
info!("listening on http://{}", addr);
|
||||||
|
|||||||
@@ -171,30 +171,58 @@
|
|||||||
await refreshSelected();
|
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() {
|
async function refreshSelected() {
|
||||||
const offset = document.getElementById('offset').value || 0;
|
const offset = document.getElementById('offset').value || 0;
|
||||||
const transit = document.getElementById('transit').checked ? '1' : '0';
|
const transit = document.getElementById('transit').checked ? '1' : '0';
|
||||||
const params = new URLSearchParams({ offset_min: offset, transit });
|
const params = new URLSearchParams({ offset_min: offset, transit });
|
||||||
let svgUrl, infoUrl;
|
const jsonUrl = mode === 'sky'
|
||||||
if (mode === 'sky') {
|
? `/api/sky`
|
||||||
svgUrl = `/api/sky.svg`;
|
: `/api/charts/${selectedChartId}/render?${params}`;
|
||||||
infoUrl = `/api/sky`;
|
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 {
|
} else {
|
||||||
svgUrl = `/api/charts/${selectedChartId}/wheel.svg?${params}`;
|
// Fallback: SSR — el server devuelve el SVG ya compuesto.
|
||||||
infoUrl = `/api/charts/${selectedChartId}/render?${params}`;
|
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;
|
document.getElementById('wheel-container').innerHTML = svg;
|
||||||
const info = document.getElementById('info');
|
const info = document.getElementById('info');
|
||||||
if (render) {
|
if (render) {
|
||||||
|
const mode_label = wasm ? 'WASM' : 'SSR';
|
||||||
info.innerHTML =
|
info.innerHTML =
|
||||||
`<b>${escapeHtml(render.title)}</b> · ` +
|
`<b>${escapeHtml(render.title)}</b> · ` +
|
||||||
`Asc ${render.ascendant_deg.toFixed(2)}° · ` +
|
`Asc ${render.ascendant_deg.toFixed(2)}° · ` +
|
||||||
`MC ${render.midheaven_deg.toFixed(2)}° · ` +
|
`MC ${render.midheaven_deg.toFixed(2)}° · ` +
|
||||||
`${render.compute_ms} ms`;
|
`${render.compute_ms} ms · ${mode_label}`;
|
||||||
} else {
|
} else {
|
||||||
info.textContent = '';
|
info.textContent = '';
|
||||||
}
|
}
|
||||||
@@ -238,8 +266,11 @@
|
|||||||
}[c]));
|
}[c]));
|
||||||
}
|
}
|
||||||
|
|
||||||
loadTree();
|
(async () => {
|
||||||
loadSky();
|
await tryLoadWasm();
|
||||||
|
await loadTree();
|
||||||
|
await loadSky();
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user