From 020f9318aa9228ab6f3c5a920d7e675da9ee366a Mon Sep 17 00:00:00 2001 From: pikaro Date: Mon, 27 Jul 2026 13:22:08 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20cero=20dependencias=20externas=20y=20ca?= =?UTF-8?q?ch=C3=A9=20de=20peticiones=20al=20nodo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Las librerías (React, Babel) venían de unpkg.com y las fuentes de Google. Ninguno veía qué transacciones analizabas, pero ambos recibían tu IP y la hora en cada apertura: sabían que usabas Txoko, cuándo y desde dónde — el metadato que la propia herramienta enseña a proteger. Ahora se sirven desde el nodo, con verificación por hash y versiones fijadas. El dashboard funciona sin internet. Además, caché con TTL y coalescencia en useApi: las transacciones confirmadas son inmutables y se cachean toda la sesión, así que repetir un análisis ya explorado no cuesta ninguna petición al nodo (medido). --- CHANGELOG.md | 32 +++++++++++++++++ README.md | 6 ++-- SETUP.md | 79 ++++++++++++++++++++++++++++++++++++++++++ dashboard.html | 82 ++++++++++++++++++++++++++++++++++++++------ instalar-fuentes.sh | 83 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 269 insertions(+), 13 deletions(-) create mode 100755 instalar-fuentes.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 5af7d92..d403883 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,38 @@ y el versionado sigue [Versionado Semántico](https://semver.org/lang/es/): - **MENOR** — características nuevas que no rompen lo anterior - **PARCHE** — arreglos de errores +--- +## [1.11.0] — 2026-07-27 +### Añadido +- **Las librerías del frontend se sirven desde tu propio nodo.** React, ReactDOM + y Babel se cargaban desde unpkg.com, y las fuentes IBM Plex desde + fonts.googleapis.com. Ninguno de los dos veía qué transacciones analizabas — + pero ambos recibían tu IP pública, la hora y el referer en cada apertura del + dashboard: sabían que usabas Txoko, cuándo y desde dónde. Ese metadato es + justo lo que esta herramienta enseña a proteger. Ahora no sale ni una + petición fuera de tu red, verificable con + `grep -E '(src|href)="https?://' dashboard.html` (no devuelve nada). + - Consecuencia práctica: **el dashboard funciona sin conexión a internet**. + - Las librerías se descargan una vez durante la instalación y se **verifican + por hash** (SHA-256 publicados en SETUP.md). + - Versiones fijadas (`react@18.3.1`, `@babel/standalone@7.23.10`) en vez de + rangos: antes el CDN decidía qué versión te entregaba y podía cambiar sin + aviso. + - `instalar-fuentes.sh` automatiza la descarga de IBM Plex (licencia OFL) y + genera el CSS. Comprueba cada archivo y aborta si algo falla. +- **Caché de respuestas del nodo con TTL y coalescencia** en `useApi`. Las + pestañas se montan y desmontan al navegar, así que ir a otra pestaña y volver + repetía todas las peticiones. Ahora: + - Las **transacciones confirmadas se cachean toda la sesión** — son + inmutables, no hay motivo para volver a pedirlas. Analizador, rastro de + procedencia y peritaje dejan de pedir las mismas por separado. + - Historial de direcciones 60 s, bloques 60 s, mempool y comisiones 20 s, + estado del sistema 2 s (para que el monitor siga mostrando datos vivos). + - **Coalescencia:** dos componentes que piden lo mismo a la vez generan una + sola petición al nodo, no dos. Mismo patrón que ya usaba + `system-metrics.js` en el servidor. + - Medido: repetir un rastreo forense ya explorado cuesta **cero peticiones**. + --- ## [1.10.1] — 2026-07-27 ### Añadido diff --git a/README.md b/README.md index 5ccb18a..94a94b8 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,11 @@ El código es un único archivo HTML autocontenido. Puedes auditarlo tú mismo: grep -E "fetch\(|XMLHttpRequest|src=\"http|href=\"http" dashboard.html ``` -Verás que todas las llamadas van a rutas relativas (`/api/`, `/system/`) que apuntan a tu propio nodo vía nginx. No hay ninguna llamada a dominios externos en el código de análisis. +El comando anterior no debe devolver **ninguna** URL externa. Todas las llamadas van a rutas relativas (`/api/`, `/system/`, `/vendor/`) que apuntan a tu propio nodo vía nginx. -Las únicas dependencias externas son las librerías del frontend (React, Babel) que se cargan desde CDN al abrir la página. Estas no reciben ningún dato de tus transacciones — solo sirven el código de la interfaz. +Esto incluye las librerías del frontend (React y Babel): también se sirven desde tu nodo, no desde un CDN. Hasta la versión 1.10.1 se cargaban desde unpkg.com, y aunque un CDN nunca vio qué transacciones analizabas, sí recibía tu IP pública cada vez que abrías el dashboard — es decir, sabía que usabas Txoko, cuándo y desde dónde. Ese metadato es justo lo que esta herramienta enseña a proteger, así que se eliminó. + +Consecuencia práctica: **el dashboard funciona sin conexión a internet**. Solo necesita tu nodo. Las librerías se descargan una vez durante la instalación y se verifican por hash — ver [SETUP.md](SETUP.md). --- diff --git a/SETUP.md b/SETUP.md index 465da09..b25837a 100644 --- a/SETUP.md +++ b/SETUP.md @@ -159,6 +159,85 @@ Cuatro comandos después de la descarga. Siempre los mismos. --- +## Librerías del frontend (obligatorio) + +El dashboard usa React y Babel. **Se sirven desde tu propio nodo, no desde un +CDN.** El motivo es de privacidad, no de comodidad: un CDN externo no ve qué +transacciones analizas, pero sí ve que usas Txoko, cuándo y desde qué IP +pública — exactamente la clase de metadato que esta herramienta enseña a +proteger. Sirviéndolas en local, *"ninguna consulta sale de tu red"* pasa a ser +literal, y el dashboard funciona sin conexión a internet. + +Estas librerías **no están en el repo** (son de terceros y pesan ~3 MB). Se +descargan una vez, en el nodo: + +```bash +sudo mkdir -p /usr/share/nginx/html/vendor +cd /usr/share/nginx/html/vendor +sudo curl -sSLO https://unpkg.com/react@18.3.1/umd/react.production.min.js +sudo curl -sSLO https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js +sudo curl -sSL -o babel.min.js https://unpkg.com/@babel/standalone@7.23.10/babel.min.js +``` + +### Verifica lo que has descargado + +No te fíes: comprueba que los archivos son los que deben ser. + +```bash +sha256sum *.js +``` + +Debe dar exactamente esto: + +``` +85ba0c7207cf1b1850e40372f26a7e69a481457a29649b7ca19bbfce8604f30c babel.min.js +35f4f974f4b2bcd44da73963347f8952e341f83909e4498227d4e26b98f66f0d react-dom.production.min.js +d949f1c3687aedadcedac85261865f29b17cd273997e7f6b2bfc53b2f9d4c4dd react.production.min.js +``` + +Si algún hash no coincide, **no uses esos archivos**: significa que has +recibido algo distinto a lo esperado. + +Las versiones están fijadas a propósito (`18.3.1`, `7.23.10`). Usar un rango +como `react@18` dejaría que el servidor decidiera qué versión te entrega, y +cambiaría con el tiempo sin que te enteres. + +### Dónde deben quedar los archivos + +El dashboard las busca en `vendor/` con **ruta relativa**, es decir, en un +subdirectorio junto al propio `dashboard.html`. Así funciona tanto si sirves el +dashboard en la raíz como bajo un prefijo, sin tocar nginx. + +Con la configuración típica de Mempool self-hosted: + +```nginx +location /dashboard { + alias /usr/share/nginx/html/; +} +``` + +…el `dashboard.html` vive en `/usr/share/nginx/html/` y las librerías deben ir +en `/usr/share/nginx/html/vendor/` — que es justo donde las deja el comando de +arriba. + +**Importante:** abre el dashboard **con la barra final** (`.../dashboard/`, no +`.../dashboard`). Sin ella, el navegador resuelve las rutas relativas un nivel +por encima y no encuentra las librerías. + +Para comprobar que nginx las sirve bien, lo que importa no es solo el código +200 sino el tipo de contenido: + +```bash +curl -sk -o /dev/null -w '%{http_code} %{content_type}\n' \ + https://localhost:4081/dashboard/vendor/react.production.min.js +``` + +Debe responder `200 application/javascript`. Si devuelve `200 text/html`, +nginx está entregando otra cosa (por ejemplo el index de Mempool) y las rutas +no son las correctas. + +--- + ## HTTPS para watch-only (opcional) La función watch-only (derivar tus direcciones desde el xpub) usa la Web Crypto diff --git a/dashboard.html b/dashboard.html index 40b0c9c..b380945 100644 --- a/dashboard.html +++ b/dashboard.html @@ -4,9 +4,19 @@ Txoko Node Dashboard - - - + + + + + - - + +
@@ -2054,13 +2066,63 @@ } } + // ── Caché de respuestas del nodo ────────────────────────────────────── + // Vive FUERA del hook a propósito. Las pestañas se montan y desmontan al + // navegar ({tab==="node" && }), así que sin esto ir a + // BLOQUES y volver a NODO vuelve a pedirlo todo aunque hayan pasado dos + // segundos. Mismo patrón que ya usa system-metrics.js en el servidor + // (caché con TTL + coalescencia), aplicado ahora también en el navegador. + // + // La coalescencia importa tanto como el TTL: dos componentes que piden lo + // mismo a la vez generan UNA petición al nodo, no dos. + const apiCache = new Map(); // clave -> { at, data, ttl } + const apiInflight = new Map(); // clave -> Promise en vuelo + const API_CACHE_MAX = 400; // tope de entradas, para no crecer sin fin + + // Cuánto vale una respuesta depende de lo que sea. Una transacción ya + // confirmada es INMUTABLE: no hay ningún motivo para volver a pedirla en + // toda la sesión. Es el caso más frecuente en el analizador, el rastro de + // procedencia y el peritaje, que hoy piden las mismas txs por separado. + function apiCacheTtl(path, data) { + if (path.startsWith("/system/")) return 2000; // estado vivo del nodo: casi sin caché + if (/^\/api\/tx\/[0-9a-f]{64}$/i.test(path)) { + return data && data.status && data.status.confirmed ? Infinity : 15000; + } + if (path.startsWith("/api/address/")) return 60000; // historial: cambia poco + if (path.startsWith("/api/block")) return 60000; + return 20000; // mempool, fees y demás + } + + async function apiGetJson(baseUrl, path) { + const key = baseUrl + path; + const hit = apiCache.get(key); + if (hit && (hit.ttl === Infinity || Date.now() - hit.at < hit.ttl)) return hit.data; + + const flying = apiInflight.get(key); + if (flying) return flying; // ya hay una petición idéntica en curso + + const p = (async () => { + const res = await fetchWithTimeout(key); + if (!res.ok) throw new Error(`el nodo respondió HTTP ${res.status}`); + const data = await res.json(); + const ttl = apiCacheTtl(path, data); + if (ttl > 0) { + // FIFO simple: al llenarse, cae la entrada más antigua. + if (apiCache.size >= API_CACHE_MAX) apiCache.delete(apiCache.keys().next().value); + apiCache.set(key, { at: Date.now(), data, ttl }); + } + return data; + })(); + + apiInflight.set(key, p); + try { return await p; } finally { apiInflight.delete(key); } + } + function useApi(baseUrl) { const get = useCallback(async (path, mockData) => { if (!baseUrl) return mockData; try { - const res = await fetchWithTimeout(`${baseUrl}${path}`); - if (!res.ok) throw new Error(`HTTP ${res.status}`); - return res.json(); + return await apiGetJson(baseUrl, path); } catch { return mockData; } }, [baseUrl]); // Variante estricta: propaga el fallo en vez de disfrazarlo de dato @@ -2071,9 +2133,7 @@ // pericial falsa. Quien use getStrict debe capturar y decir qué pasó. const getStrict = useCallback(async (path) => { if (!baseUrl) throw new Error("no hay nodo configurado"); - const res = await fetchWithTimeout(`${baseUrl}${path}`); - if (!res.ok) throw new Error(`el nodo respondió HTTP ${res.status}`); - return res.json(); + return apiGetJson(baseUrl, path); // misma caché; los errores sí suben }, [baseUrl]); return { get, getStrict }; } diff --git a/instalar-fuentes.sh b/instalar-fuentes.sh new file mode 100755 index 0000000..eab2f5b --- /dev/null +++ b/instalar-fuentes.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────────────── +# Txoko — instalar IBM Plex en local (elimina la dependencia de Google Fonts) +# +# Descarga las fuentes IBM Plex (licencia OFL, libre) y genera el CSS que las +# declara. Tras ejecutarlo, el dashboard deja de contactar con +# fonts.googleapis.com y fonts.gstatic.com. +# +# Por qué importa: Google no ve qué transacciones analizas, pero sí recibe tu +# IP, la hora y la página desde la que se pide la fuente, cada vez que abres el +# dashboard. Para una herramienta de privacidad, sobra. +# +# Fuente: paquetes npm oficiales de IBM servidos por unpkg — el mismo origen +# del que ya se descargan React y Babel durante la instalación. +# +# Ejecutar EN EL NODO: +# chmod +x instalar-fuentes.sh && sudo ./instalar-fuentes.sh +# ───────────────────────────────────────────────────────────────────────────── +set -uo pipefail + +DEST="${1:-/usr/share/nginx/html/vendor/fonts}" +MONO="https://unpkg.com/@ibm/plex-mono@1.1.0/fonts/complete/woff2" +SANS="https://unpkg.com/@ibm/plex-sans@1.1.0/fonts/complete/woff2" + +echo "→ Instalando fuentes en: $DEST" +mkdir -p "$DEST" +cd "$DEST" || { echo "✗ No se pudo entrar en $DEST"; exit 1; } + +# Pesos que usa el dashboard: Mono 400/600/700, Sans 400/500/600. +# Formato woff2 (el más comprimido, soportado por todo navegador actual). +URLS=" +$MONO/IBMPlexMono-Regular.woff2 +$MONO/IBMPlexMono-SemiBold.woff2 +$MONO/IBMPlexMono-Bold.woff2 +$SANS/IBMPlexSans-Regular.woff2 +$SANS/IBMPlexSans-Medium.woff2 +$SANS/IBMPlexSans-SemiBold.woff2 +" + +fallos=0 +for url in $URLS; do + archivo=$(basename "$url") + printf " %-32s" "$archivo" + # -w escribe el código HTTP para poder diagnosticar si algo va mal + codigo=$(curl -sSL -o "$archivo" -w "%{http_code}" "$url" 2>/dev/null) + tam=$(stat -c%s "$archivo" 2>/dev/null || echo 0) + if [ "$codigo" = "200" ] && [ "$tam" -gt 10000 ]; then + echo "ok ($((tam/1024)) KB)" + else + echo "FALLÓ (HTTP $codigo, $tam bytes)" + fallos=$((fallos+1)) + fi +done + +if [ "$fallos" -gt 0 ]; then + echo + echo "✗ $fallos archivo(s) no se descargaron correctamente." + echo " No despliegues el dashboard nuevo todavía — avísame con esta salida." + exit 1 +fi + +# ── CSS que declara las fuentes ────────────────────────────────────────────── +# font-display:swap → el texto se ve desde el primer momento con la fuente de +# respaldo y se sustituye al cargar la definitiva. Sin página en blanco. +cat > ibm-plex.css <<'CSS' +/* IBM Plex — servido desde tu propio nodo. Licencia OFL (IBM). + Sustituye a fonts.googleapis.com: ninguna petición sale de tu red. */ +@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:swap;src:url('IBMPlexMono-Regular.woff2') format('woff2')} +@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:600;font-display:swap;src:url('IBMPlexMono-SemiBold.woff2') format('woff2')} +@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:700;font-display:swap;src:url('IBMPlexMono-Bold.woff2') format('woff2')} +@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:400;font-display:swap;src:url('IBMPlexSans-Regular.woff2') format('woff2')} +@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:500;font-display:swap;src:url('IBMPlexSans-Medium.woff2') format('woff2')} +@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:600;font-display:swap;src:url('IBMPlexSans-SemiBold.woff2') format('woff2')} +CSS + +echo +echo "✓ Fuentes instaladas y CSS generado." +echo " Total: $(du -sh . | cut -f1)" +echo +echo " Comprueba que nginx las sirve (debe dar 200):" +echo " curl -sk -o /dev/null -w '%{http_code}\\n' https://localhost:4081/vendor/fonts/ibm-plex.css" +echo +echo " Si da 200, ya puedes desplegar el dashboard.html nuevo."