From a6fe438b0612916e72934a0a565ea9a8508bfc7f Mon Sep 17 00:00:00 2001 From: Aitor Date: Fri, 12 Jun 2026 22:35:09 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20informe=20de=20wallet=20completo=20(sal?= =?UTF-8?q?ud,=20clusters=20CIOH,=20historial);=20refactor:=20monitor=20si?= =?UTF-8?q?n=20dependencias=20(fuera=20express),=20nombres=20de=20proceso?= =?UTF-8?q?=20limpios;=20mejora:=20presentaci=C3=B3n=20de=20checks=20agrup?= =?UTF-8?q?ada=20con=20did=C3=A1ctica=20directa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 35 ++++++++++ dashboard.html | 160 ++++++++++++++++++++++++++++++++++------------ system-metrics.js | 143 +++++++++++++++++++++++------------------ 3 files changed, 234 insertions(+), 104 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69cdbb4..658e582 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,42 @@ y el versionado sigue [Versionado Semántico](https://semver.org/lang/es/): - **PARCHE** — arreglos de errores --- +## [1.9.0] — 2026-06-12 +### Añadido +- Informe de wallet completo. Tras cargar tu xpub (watch-only), el botón + "Analizar wallet" examina el conjunto de tus transacciones y produce un + informe con tres partes: salud general (ALTA/MEDIA/BAJA según reutilización + de direcciones y vinculación), clusters de vinculación detectados por la + heurística CIOH (common-input-ownership: direcciones que gastaste juntas y + que un observador asume del mismo dueño), e historial de cada transacción + con su banda de privacidad. Exportable a JSON y Markdown. Es la visión de + conjunto que ningún explorador ofrece: no analiza una transacción aislada, + sino qué puede agrupar un observador de toda tu actividad. El escaneo se + hace por lotes con pausas para no saturar el nodo, y todo el cálculo ocurre + en el navegador. +- Presentación de resultados reorganizada. El informe de privacidad ahora + abre con un resumen ("N señales a revisar · M correctas · K informativas"), + mantiene los problemas siempre visibles y agrupa las señales correctas e + informativas en secciones colapsables. Al desplegar un problema, la + explicación didáctica aparece directa, con una línea "Qué puedes hacer" + según el caso (ya está en la cadena / depende de tu wallet / puedes + evitarlo al construir la transacción). +### Cambiado +- Las recomendaciones usan un lenguaje más claro y menos técnico: "depende + del wallet" y "puedes evitarlo" en lugar de etiquetas crípticas. El peso de + cada señal en la banda se describe como alto/medio/bajo en vez de un número. + +### Corregido +- El monitor del sistema (system-metrics.js) ya no usa la librería express: + ahora funciona solo con Node.js, sin dependencias ni `npm install`. Descargar + y ejecutar. Esto elimina el error "Cannot find module 'express'" que veían + los usuarios nuevos. +- Los nombres de proceso en "Procesos destacados" se muestran limpios: un + servicio Node con flags (p.ej. Mempool con --max-old-space-size) aparece + como "node (index.js)" en vez de mostrar el argumento crudo. +- La medición de CPU del monitor usa una ventana de 500ms para una lectura + más estable y coherente con la de herramientas como `top`. ## [1.8.0] — 2026-06-04 ### Añadido diff --git a/dashboard.html b/dashboard.html index ec9c300..2a8681b 100644 --- a/dashboard.html +++ b/dashboard.html @@ -2150,10 +2150,78 @@ const [expanded,setExpanded] = useState(null); const [showDidactic,setShowDidactic] = useState(null); const [showReport,setShowReport] = useState(false); + const [showOkGroup,setShowOkGroup] = useState(false); + const [showInfoGroup,setShowInfoGroup] = useState(false); if (!analysis) return null; const {score,checks,summary,summaryColor,wallets,band} = analysis; const report = generateReport(analysis, tx); + // Texto de acción según accionabilidad — orienta sin sonar a reproche + const actionText = (a) => + a==="no_corregible" ? "Esto ya quedó registrado en la cadena y no se puede cambiar. Tenlo en cuenta para próximas transacciones." + : a==="wallet" ? "Qué puedes hacer: esto depende de tu software de wallet. Otro wallet (o configuración) lo evitaría." + : "Qué puedes hacer: este patrón depende de cómo se construye la transacción. Puedes evitarlo en próximos envíos."; + + const actionLabel = (a) => + a==="no_corregible" ? "ya en chain" : a==="wallet" ? "depende del wallet" : "puedes evitarlo"; + + // Render de un check. isProblem=true → didáctica visible directamente al + // expandir (sin segundo clic); en correctas/informativas queda tras botón. + const renderCheck = (check, isProblem) => ( +
+
setExpanded(expanded===check.id?null:check.id)} + style={{display:"flex",alignItems:"center",gap:8,padding:"10px 14px",cursor:"pointer"}}> + + {check.informational?"·":(check.positive&&!check.pass)?"✓":check.pass?"✓":"✗"} + + {check.label} + {!check.pass&&!check.positive&&check.actionability&&( + + {actionLabel(check.actionability)} + + )} + +
+ {expanded===check.id&&( +
+
{check.detail}
+ {isProblem&&check.didactic&&( +
+ {check.didactic} +
+ )} + {isProblem&&check.actionability&&( +
+ {actionText(check.actionability)} +
+ )} + {!check.informational&&!check.pass&&check.penalty>0&&( +
+ Peso en la banda: {check.penalty>=40?"alto":check.penalty>=20?"medio":"bajo"} +
+ )} + {!isProblem&&check.didactic&&( +
+ + {showDidactic===check.id&&( +
+ {check.didactic} +
+ )} +
+ )} +
+ )} +
+ ); + // ── Exportar informe ────────────────────────────────────────────── // Genera el archivo en el navegador y lo descarga en local. // No pasa por ningún servidor, no sale nada del nodo. @@ -2301,7 +2369,7 @@ color: rec.tag==="wallet"?C.purple:C.amber, border: `1px solid ${rec.tag==="wallet"?C.purple:C.amber}30`, }}> - {rec.tag==="wallet"?"cambia wallet":"evitable"} + {rec.tag==="wallet"?"depende del wallet":"puedes evitarlo"}
{rec.why}
@@ -2315,52 +2383,60 @@ )} -
- {checks.map(check => ( -
-
setExpanded(expanded===check.id?null:check.id)} - style={{display:"flex",alignItems:"center",gap:8,padding:"10px 14px",cursor:"pointer"}}> - - {check.informational?"·":(check.positive&&!check.pass)?"✓":check.pass?"✓":"✗"} - - {check.label} - {!check.pass&&!check.positive&&check.actionability&&( - - {check.actionability==="no_corregible"?"ya en chain":check.actionability==="wallet"?"cambia wallet":"evitable"} - - )} - + {(() => { + const problems = checks.filter(c => !c.informational && !c.pass && !c.positive); + const oks = checks.filter(c => !c.informational && (c.pass || c.positive)); + const infos = checks.filter(c => c.informational); + return ( +
+ {/* Resumen de un vistazo */} +
+ {problems.length>0 + ? ✗ {problems.length} {problems.length===1?"señal a revisar":"señales a revisar"} + : ✓ Sin señales que revisar} + ✓ {oks.length} correctas + {infos.length>0&&· {infos.length} informativas}
- {expanded===check.id&&( -
-
{check.detail}
- {!check.informational&&!check.pass&&check.penalty>0&&( -
- Peso en la banda: {check.penalty>=40?"alto":check.penalty>=20?"medio":"bajo"} + + {/* Problemas: siempre visibles, lo primero */} + {problems.map(c => renderCheck(c, true))} + + {/* Correctas: agrupadas, colapsadas por defecto */} + {oks.length>0&&( +
+
setShowOkGroup(!showOkGroup)} + style={{display:"flex",alignItems:"center",gap:8,padding:"10px 14px",cursor:"pointer"}}> + + {oks.length} señales correctas + {showOkGroup?"▲":"▼"} +
+ {showOkGroup&&( +
+ {oks.map(c => renderCheck(c, false))}
)} - {check.didactic&&( -
- - {showDidactic===check.id&&( -
- {check.didactic} -
- )} +
+ )} + + {/* Informativas: agrupadas, colapsadas por defecto */} + {infos.length>0&&( +
+
setShowInfoGroup(!showInfoGroup)} + style={{display:"flex",alignItems:"center",gap:8,padding:"10px 14px",cursor:"pointer"}}> + · + {infos.length} informativas — esperadas en este tipo de transacción + {showInfoGroup?"▲":"▼"} +
+ {showInfoGroup&&( +
+ {infos.map(c => renderCheck(c, false))}
)}
)}
- ))} -
+ ); + })()}
{[{label:"CERTEZA",color:C.green,desc:"Hecho verificable on-chain"},{label:"PROBABLE",color:C.amber,desc:"Alta probabilidad"},{label:"POSIBLE",color:C.blue,desc:"Inferencia"}].map(l=>( @@ -2369,9 +2445,9 @@
{[ - {label:"ya en chain", color:C.red, desc:"No corregible"}, - {label:"cambia wallet", color:C.purple, desc:"Depende del software"}, - {label:"evitable", color:C.amber, desc:"Puedes evitarlo en el futuro"}, + {label:"ya en chain", color:C.red, desc:"Registrado en la cadena, no se puede cambiar"}, + {label:"depende del wallet", color:C.purple, desc:"Lo determina tu software de wallet"}, + {label:"puedes evitarlo", color:C.amber, desc:"Depende de cómo construyes la transacción"}, ].map(l=>(
{l.label} diff --git a/system-metrics.js b/system-metrics.js index fd0ea13..303c781 100644 --- a/system-metrics.js +++ b/system-metrics.js @@ -11,14 +11,12 @@ * - Caché con TTL: mide una vez cada pocos segundos, sirva a N navegadores * - Todo asíncrono: nada bloquea el servidor * - * Setup: - * npm install express + * Setup: ninguno. Sin dependencias — solo Node.js. * node system-metrics.js * * Acceso: http://127.0.0.1:4082 */ -const express = require("express"); const { exec } = require("child_process"); const fs = require("fs"); const os = require("os"); @@ -34,14 +32,6 @@ const PORT = 4082; const HOST = "127.0.0.1"; // ── FIN CONFIGURACIÓN ─────────────────────────────────────────────────────── -const app = express(); - -app.use((req, res, next) => { - res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader("Access-Control-Allow-Methods", "GET"); - next(); -}); - // ── Helpers asíncronos ────────────────────────────────────────────────────── const readFile = (path) => { try { return fs.readFileSync(path, "utf8").trim(); } catch { return null; } @@ -93,7 +83,7 @@ function readProcStat() { async function getCpuUsage() { const s1 = readProcStat(); - await sleep(300); + await sleep(500); const s2 = readProcStat(); if (!s1 || !s2) return { total: 0, cores: [] }; @@ -186,9 +176,19 @@ async function getProcesses() { // Nombre limpio: basename del ejecutable; si es un intérprete (node, // python...), añade el basename del script que ejecuta. const exe = (parts[10] || "").split("/").pop(); - const arg1 = (parts[11] || "").split("/").pop(); - const interp = /^(node|python\d?|bash|sh|perl)$/.test(exe); - const command = (interp && arg1 ? `${exe} (${arg1})` : exe).slice(0, 24); + const interp = /^(node|python\d?|bash|sh|perl|ruby|java)$/.test(exe); + let command = exe; + if (interp) { + // Saltar flags (--max-old-space-size=…, -e, etc.) y quedarse con el + // primer argumento "real": el script (suele acabar en .js/.py o ser ruta) + const args = parts.slice(11); + const script = args.find(a => a && !a.startsWith("-")); + if (script) { + const name = script.split("/").pop(); + command = `${exe} (${name})`; + } + } + command = command.slice(0, 24); return { user: parts[0], cpu: parseFloat(parts[2]), @@ -278,63 +278,82 @@ async function getLogs(service, lines = 50) { }); } -// ── Routes (caché por ruta: el trabajo se hace 1 vez por ventana) ────────── -app.get("/system/info", async (req, res) => { - try { - const data = await cached("info", 3000, async () => { - const [cpu, disk, processes] = await Promise.all([ - getCpuUsage(), getDisk(), getProcesses(), - ]); - return { - cpu: { - usage: cpu.total, - cores: cpu.cores, - temp: getCpuTemp(), - count: os.cpus().length, - model: os.cpus()[0]?.model?.trim() || "—", - }, - ram: getRam(), - disk, - load: getLoad(), - processes, - uptime: getUptime(), - os: `${os.type()} ${os.release()}`, - }; - }); - res.json(data); - } catch (e) { res.status(500).json({ error: e.message }); } -}); +// ── Handlers (caché por ruta: el trabajo se hace 1 vez por ventana) ──────── +const routes = { -app.get("/system/bitcoin", async (req, res) => { - try { + "/system/info": async () => cached("info", 3000, async () => { + const [cpu, disk, processes] = await Promise.all([ + getCpuUsage(), getDisk(), getProcesses(), + ]); + return { + cpu: { + usage: cpu.total, + cores: cpu.cores, + temp: getCpuTemp(), + count: os.cpus().length, + model: os.cpus()[0]?.model?.trim() || "—", + }, + ram: getRam(), + disk, + load: getLoad(), + processes, + uptime: getUptime(), + os: `${os.type()} ${os.release()}`, + }; + }), + + "/system/bitcoin": async () => { const info = await cached("bitcoin", 5000, getBitcoinInfo); - if (!info) return res.status(503).json({ error: "No se pudo conectar a Bitcoin Core" }); - res.json(info); - } catch (e) { res.status(500).json({ error: e.message }); } -}); + if (!info) return { __status: 503, error: "No se pudo conectar a Bitcoin Core" }; + return info; + }, -app.get("/system/logs/bitcoin", async (req, res) => { - try { - const lines = parseInt(req.query.lines) || 50; + "/system/logs/bitcoin": async (query) => { + const lines = parseInt(query.get("lines")) || 50; const logs = await cached(`logs-btc-${lines}`, 10000, () => getLogs("bitcoind", lines)); - res.json({ logs }); - } catch (e) { res.status(500).json({ error: e.message }); } -}); + return { logs }; + }, -app.get("/system/logs/fulcrum", async (req, res) => { - try { - const lines = parseInt(req.query.lines) || 50; + "/system/logs/fulcrum": async (query) => { + const lines = parseInt(query.get("lines")) || 50; const logs = await cached(`logs-ful-${lines}`, 10000, () => getLogs("fulcrum", lines)); - res.json({ logs }); - } catch (e) { res.status(500).json({ error: e.message }); } -}); + return { logs }; + }, -app.get("/system/health", (req, res) => { - res.json({ status: "ok", timestamp: Date.now() }); + "/system/health": async () => ({ status: "ok", timestamp: Date.now() }), +}; + +// ── Servidor HTTP nativo (sin dependencias) ───────────────────────────────── +const server = http.createServer(async (req, res) => { + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Methods", "GET"); + res.setHeader("Content-Type", "application/json"); + + const url = new URL(req.url, `http://${req.headers.host || "localhost"}`); + const handler = routes[url.pathname]; + + if (!handler) { + res.writeHead(404); + return res.end(JSON.stringify({ error: "Not found" })); + } + + try { + const data = await handler(url.searchParams); + if (data && data.__status) { + const { __status, ...body } = data; + res.writeHead(__status); + return res.end(JSON.stringify(body)); + } + res.writeHead(200); + res.end(JSON.stringify(data)); + } catch (e) { + res.writeHead(500); + res.end(JSON.stringify({ error: e.message || "Internal error" })); + } }); // ── Start ─────────────────────────────────────────────────────────────────── -app.listen(PORT, HOST, () => { +server.listen(PORT, HOST, () => { console.log(` ₿ Txoko System Metrics v2 ─────────────────────────────────