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:
+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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user