feat: cero dependencias externas y caché de peticiones al nodo
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).
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+71
-11
@@ -4,9 +4,19 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Txoko Node Dashboard</title>
|
||||
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone@7.23.10/babel.min.js"></script>
|
||||
<!-- Librerías servidas desde TU nodo, no desde un CDN. Un CDN externo no ve
|
||||
qué transacciones analizas, pero sí que usas Txoko, cuándo y desde qué
|
||||
IP — justo la clase de metadato que esta herramienta enseña a proteger.
|
||||
Sirviéndolas en local, "ninguna consulta sale de tu red" es literal, y
|
||||
el dashboard funciona sin conexión a internet.
|
||||
Cómo obtenerlas y verificarlas: ver SETUP.md. -->
|
||||
<!-- Rutas RELATIVAS a propósito: el dashboard puede servirse en la raíz o
|
||||
bajo un prefijo (con Mempool self-hosted suele ser /dashboard/), y así
|
||||
funciona en ambos casos sin tocar nginx. Requiere abrirlo con la barra
|
||||
final: .../dashboard/ y no .../dashboard -->
|
||||
<script src="vendor/react.production.min.js"></script>
|
||||
<script src="vendor/react-dom.production.min.js"></script>
|
||||
<script src="vendor/babel.min.js"></script>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { background: #05080d; color: #dde6f0; font-family: 'IBM Plex Sans', 'Helvetica Neue', sans-serif; }
|
||||
@@ -17,8 +27,10 @@
|
||||
button:disabled { opacity:0.4; cursor:not-allowed !important; }
|
||||
input::placeholder { color: #4a6380; }
|
||||
</style>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600;700&family=IBM+Plex+Sans:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<!-- Fuentes servidas desde TU nodo. Antes venían de fonts.googleapis.com,
|
||||
lo que le daba a Google tu IP y la hora en cada apertura del dashboard.
|
||||
Instalación: instalar-fuentes.sh (ver SETUP.md). -->
|
||||
<link href="vendor/fonts/ibm-plex.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
@@ -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" && <NodeOverview/>}), 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 };
|
||||
}
|
||||
|
||||
Executable
+83
@@ -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."
|
||||
Reference in New Issue
Block a user