feat: informe de wallet completo (salud, clusters CIOH, historial); refactor: monitor sin dependencias (fuera express), nombres de proceso limpios; mejora: presentación de checks agrupada con didáctica directa
This commit is contained in:
@@ -11,7 +11,42 @@ y el versionado sigue [Versionado Semántico](https://semver.org/lang/es/):
|
|||||||
- **PARCHE** — arreglos de errores
|
- **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
|
## [1.8.0] — 2026-06-04
|
||||||
|
|
||||||
### Añadido
|
### Añadido
|
||||||
|
|||||||
+116
-40
@@ -2150,10 +2150,78 @@
|
|||||||
const [expanded,setExpanded] = useState(null);
|
const [expanded,setExpanded] = useState(null);
|
||||||
const [showDidactic,setShowDidactic] = useState(null);
|
const [showDidactic,setShowDidactic] = useState(null);
|
||||||
const [showReport,setShowReport] = useState(false);
|
const [showReport,setShowReport] = useState(false);
|
||||||
|
const [showOkGroup,setShowOkGroup] = useState(false);
|
||||||
|
const [showInfoGroup,setShowInfoGroup] = useState(false);
|
||||||
if (!analysis) return null;
|
if (!analysis) return null;
|
||||||
const {score,checks,summary,summaryColor,wallets,band} = analysis;
|
const {score,checks,summary,summaryColor,wallets,band} = analysis;
|
||||||
const report = generateReport(analysis, tx);
|
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) => (
|
||||||
|
<div key={check.id} style={{background:C.bgCard,border:`1px solid ${expanded===check.id?C.purple+"60":C.border}`,borderRadius:6,overflow:"hidden"}}>
|
||||||
|
<div onClick={()=>setExpanded(expanded===check.id?null:check.id)}
|
||||||
|
style={{display:"flex",alignItems:"center",gap:8,padding:"10px 14px",cursor:"pointer"}}>
|
||||||
|
<span style={{fontSize:"0.8rem",color:check.informational?C.blue:(check.positive&&!check.pass)?C.green:check.pass?C.green:C.red,flexShrink:0,fontWeight:700}}>
|
||||||
|
{check.informational?"·":(check.positive&&!check.pass)?"✓":check.pass?"✓":"✗"}
|
||||||
|
</span>
|
||||||
|
<span style={{flex:1,fontSize:"0.73rem",color:C.t1,fontFamily:"monospace"}}>{check.label}</span>
|
||||||
|
{!check.pass&&!check.positive&&check.actionability&&(
|
||||||
|
<span style={{fontSize:"0.55rem",fontFamily:"monospace",padding:"2px 6px",borderRadius:3,flexShrink:0,
|
||||||
|
background: check.actionability==="no_corregible"?C.redMuted:check.actionability==="wallet"?C.purpleMuted:C.amberMuted,
|
||||||
|
color: check.actionability==="no_corregible"?C.red:check.actionability==="wallet"?C.purple:C.amber,
|
||||||
|
border: `1px solid ${check.actionability==="no_corregible"?C.red:check.actionability==="wallet"?C.purple:C.amber}30`,
|
||||||
|
}}>
|
||||||
|
{actionLabel(check.actionability)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<CertaintyBadge level={check.certainty}/>
|
||||||
|
</div>
|
||||||
|
{expanded===check.id&&(
|
||||||
|
<div style={{padding:"0 14px 12px 14px",borderTop:`1px solid ${C.border}`}}>
|
||||||
|
<div style={{fontSize:"0.7rem",color:C.t2,lineHeight:1.6,paddingTop:10}}>{check.detail}</div>
|
||||||
|
{isProblem&&check.didactic&&(
|
||||||
|
<div style={{marginTop:8,padding:"10px 12px",background:C.purpleMuted,borderRadius:6,border:`1px solid ${C.purple}20`,fontSize:"0.68rem",color:C.t1,lineHeight:1.7}}>
|
||||||
|
{check.didactic}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isProblem&&check.actionability&&(
|
||||||
|
<div style={{marginTop:8,fontSize:"0.66rem",color:check.actionability==="no_corregible"?C.t2:C.amber,lineHeight:1.5}}>
|
||||||
|
{actionText(check.actionability)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!check.informational&&!check.pass&&check.penalty>0&&(
|
||||||
|
<div style={{marginTop:6,fontSize:"0.62rem",color:C.t2,fontFamily:"monospace"}}>
|
||||||
|
Peso en la banda: {check.penalty>=40?"alto":check.penalty>=20?"medio":"bajo"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isProblem&&check.didactic&&(
|
||||||
|
<div style={{marginTop:8}}>
|
||||||
|
<button onClick={e=>{e.stopPropagation();setShowDidactic(showDidactic===check.id?null:check.id);}}
|
||||||
|
style={{background:"none",border:`1px solid ${C.purple}40`,borderRadius:4,padding:"3px 8px",color:C.purple,fontFamily:"monospace",fontSize:"0.62rem",cursor:"pointer"}}>
|
||||||
|
{showDidactic===check.id?"▲ ocultar":"▼ ¿qué significa esto?"}
|
||||||
|
</button>
|
||||||
|
{showDidactic===check.id&&(
|
||||||
|
<div style={{marginTop:8,padding:"10px 12px",background:C.purpleMuted,borderRadius:6,border:`1px solid ${C.purple}20`,fontSize:"0.68rem",color:C.t1,lineHeight:1.7}}>
|
||||||
|
{check.didactic}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
// ── Exportar informe ──────────────────────────────────────────────
|
// ── Exportar informe ──────────────────────────────────────────────
|
||||||
// Genera el archivo en el navegador y lo descarga en local.
|
// Genera el archivo en el navegador y lo descarga en local.
|
||||||
// No pasa por ningún servidor, no sale nada del nodo.
|
// No pasa por ningún servidor, no sale nada del nodo.
|
||||||
@@ -2301,7 +2369,7 @@
|
|||||||
color: rec.tag==="wallet"?C.purple:C.amber,
|
color: rec.tag==="wallet"?C.purple:C.amber,
|
||||||
border: `1px solid ${rec.tag==="wallet"?C.purple:C.amber}30`,
|
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"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{fontSize:"0.65rem",color:C.t2,lineHeight:1.6,paddingLeft:18}}>{rec.why}</div>
|
<div style={{fontSize:"0.65rem",color:C.t2,lineHeight:1.6,paddingLeft:18}}>{rec.why}</div>
|
||||||
@@ -2315,52 +2383,60 @@
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{(() => {
|
||||||
|
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 (
|
||||||
<div style={{display:"flex",flexDirection:"column",gap:6}}>
|
<div style={{display:"flex",flexDirection:"column",gap:6}}>
|
||||||
{checks.map(check => (
|
{/* Resumen de un vistazo */}
|
||||||
<div key={check.id} style={{background:C.bgCard,border:`1px solid ${expanded===check.id?C.purple+"60":C.border}`,borderRadius:6,overflow:"hidden"}}>
|
<div style={{display:"flex",gap:14,alignItems:"center",padding:"8px 4px",flexWrap:"wrap"}}>
|
||||||
<div onClick={()=>setExpanded(expanded===check.id?null:check.id)}
|
{problems.length>0
|
||||||
|
? <span style={{fontSize:"0.7rem",color:C.red,fontFamily:"monospace",fontWeight:700}}>✗ {problems.length} {problems.length===1?"señal a revisar":"señales a revisar"}</span>
|
||||||
|
: <span style={{fontSize:"0.7rem",color:C.green,fontFamily:"monospace",fontWeight:700}}>✓ Sin señales que revisar</span>}
|
||||||
|
<span style={{fontSize:"0.66rem",color:C.t2,fontFamily:"monospace"}}>✓ {oks.length} correctas</span>
|
||||||
|
{infos.length>0&&<span style={{fontSize:"0.66rem",color:C.t2,fontFamily:"monospace"}}>· {infos.length} informativas</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Problemas: siempre visibles, lo primero */}
|
||||||
|
{problems.map(c => renderCheck(c, true))}
|
||||||
|
|
||||||
|
{/* Correctas: agrupadas, colapsadas por defecto */}
|
||||||
|
{oks.length>0&&(
|
||||||
|
<div style={{background:C.bgCard,border:`1px solid ${C.border}`,borderRadius:6,overflow:"hidden"}}>
|
||||||
|
<div onClick={()=>setShowOkGroup(!showOkGroup)}
|
||||||
style={{display:"flex",alignItems:"center",gap:8,padding:"10px 14px",cursor:"pointer"}}>
|
style={{display:"flex",alignItems:"center",gap:8,padding:"10px 14px",cursor:"pointer"}}>
|
||||||
<span style={{fontSize:"0.8rem",color:check.informational?C.blue:(check.positive&&!check.pass)?C.green:check.pass?C.green:C.red,flexShrink:0,fontWeight:700}}>
|
<span style={{fontSize:"0.8rem",color:C.green,fontWeight:700}}>✓</span>
|
||||||
{check.informational?"·":(check.positive&&!check.pass)?"✓":check.pass?"✓":"✗"}
|
<span style={{flex:1,fontSize:"0.7rem",color:C.t2,fontFamily:"monospace"}}>{oks.length} señales correctas</span>
|
||||||
</span>
|
<span style={{color:C.t2,fontSize:"0.7rem"}}>{showOkGroup?"▲":"▼"}</span>
|
||||||
<span style={{flex:1,fontSize:"0.73rem",color:C.t1,fontFamily:"monospace"}}>{check.label}</span>
|
|
||||||
{!check.pass&&!check.positive&&check.actionability&&(
|
|
||||||
<span style={{fontSize:"0.55rem",fontFamily:"monospace",padding:"2px 6px",borderRadius:3,flexShrink:0,
|
|
||||||
background: check.actionability==="no_corregible"?C.redMuted:check.actionability==="wallet"?C.purpleMuted:C.amberMuted,
|
|
||||||
color: check.actionability==="no_corregible"?C.red:check.actionability==="wallet"?C.purple:C.amber,
|
|
||||||
border: `1px solid ${check.actionability==="no_corregible"?C.red:check.actionability==="wallet"?C.purple:C.amber}30`,
|
|
||||||
}}>
|
|
||||||
{check.actionability==="no_corregible"?"ya en chain":check.actionability==="wallet"?"cambia wallet":"evitable"}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<CertaintyBadge level={check.certainty}/>
|
|
||||||
</div>
|
</div>
|
||||||
{expanded===check.id&&(
|
{showOkGroup&&(
|
||||||
<div style={{padding:"0 14px 12px 14px",borderTop:`1px solid ${C.border}`}}>
|
<div style={{display:"flex",flexDirection:"column",gap:6,padding:"0 8px 8px 8px"}}>
|
||||||
<div style={{fontSize:"0.7rem",color:C.t2,lineHeight:1.6,paddingTop:10}}>{check.detail}</div>
|
{oks.map(c => renderCheck(c, false))}
|
||||||
{!check.informational&&!check.pass&&check.penalty>0&&(
|
|
||||||
<div style={{marginTop:6,fontSize:"0.62rem",color:C.t2,fontFamily:"monospace"}}>
|
|
||||||
Peso en la banda: {check.penalty>=40?"alto":check.penalty>=20?"medio":"bajo"}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{check.didactic&&(
|
</div>
|
||||||
<div style={{marginTop:8}}>
|
)}
|
||||||
<button onClick={e=>{e.stopPropagation();setShowDidactic(showDidactic===check.id?null:check.id);}}
|
|
||||||
style={{background:"none",border:`1px solid ${C.purple}40`,borderRadius:4,padding:"3px 8px",color:C.purple,fontFamily:"monospace",fontSize:"0.62rem",cursor:"pointer"}}>
|
{/* Informativas: agrupadas, colapsadas por defecto */}
|
||||||
{showDidactic===check.id?"▲ ocultar":"▼ ¿qué significa esto?"}
|
{infos.length>0&&(
|
||||||
</button>
|
<div style={{background:C.bgCard,border:`1px solid ${C.border}`,borderRadius:6,overflow:"hidden"}}>
|
||||||
{showDidactic===check.id&&(
|
<div onClick={()=>setShowInfoGroup(!showInfoGroup)}
|
||||||
<div style={{marginTop:8,padding:"10px 12px",background:C.purpleMuted,borderRadius:6,border:`1px solid ${C.purple}20`,fontSize:"0.68rem",color:C.t1,lineHeight:1.7}}>
|
style={{display:"flex",alignItems:"center",gap:8,padding:"10px 14px",cursor:"pointer"}}>
|
||||||
{check.didactic}
|
<span style={{fontSize:"0.8rem",color:C.blue,fontWeight:700}}>·</span>
|
||||||
|
<span style={{flex:1,fontSize:"0.7rem",color:C.t2,fontFamily:"monospace"}}>{infos.length} informativas — esperadas en este tipo de transacción</span>
|
||||||
|
<span style={{color:C.t2,fontSize:"0.7rem"}}>{showInfoGroup?"▲":"▼"}</span>
|
||||||
|
</div>
|
||||||
|
{showInfoGroup&&(
|
||||||
|
<div style={{display:"flex",flexDirection:"column",gap:6,padding:"0 8px 8px 8px"}}>
|
||||||
|
{infos.map(c => renderCheck(c, false))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
</div>
|
})()}
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{display:"flex",gap:12,marginTop:12,flexWrap:"wrap"}}>
|
<div style={{display:"flex",gap:12,marginTop:12,flexWrap:"wrap"}}>
|
||||||
{[{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=>(
|
{[{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 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style={{display:"flex",gap:10,marginTop:8,flexWrap:"wrap"}}>
|
<div style={{display:"flex",gap:10,marginTop:8,flexWrap:"wrap"}}>
|
||||||
{[
|
{[
|
||||||
{label:"ya en chain", color:C.red, desc:"No corregible"},
|
{label:"ya en chain", color:C.red, desc:"Registrado en la cadena, no se puede cambiar"},
|
||||||
{label:"cambia wallet", color:C.purple, desc:"Depende del software"},
|
{label:"depende del wallet", color:C.purple, desc:"Lo determina tu software de wallet"},
|
||||||
{label:"evitable", color:C.amber, desc:"Puedes evitarlo en el futuro"},
|
{label:"puedes evitarlo", color:C.amber, desc:"Depende de cómo construyes la transacción"},
|
||||||
].map(l=>(
|
].map(l=>(
|
||||||
<div key={l.label} style={{display:"flex",alignItems:"center",gap:5}}>
|
<div key={l.label} style={{display:"flex",alignItems:"center",gap:5}}>
|
||||||
<span style={{fontSize:"0.55rem",fontFamily:"monospace",padding:"2px 6px",borderRadius:3,background:"transparent",color:l.color,border:`1px solid ${l.color}40`}}>{l.label}</span>
|
<span style={{fontSize:"0.55rem",fontFamily:"monospace",padding:"2px 6px",borderRadius:3,background:"transparent",color:l.color,border:`1px solid ${l.color}40`}}>{l.label}</span>
|
||||||
|
|||||||
+63
-44
@@ -11,14 +11,12 @@
|
|||||||
* - Caché con TTL: mide una vez cada pocos segundos, sirva a N navegadores
|
* - Caché con TTL: mide una vez cada pocos segundos, sirva a N navegadores
|
||||||
* - Todo asíncrono: nada bloquea el servidor
|
* - Todo asíncrono: nada bloquea el servidor
|
||||||
*
|
*
|
||||||
* Setup:
|
* Setup: ninguno. Sin dependencias — solo Node.js.
|
||||||
* npm install express
|
|
||||||
* node system-metrics.js
|
* node system-metrics.js
|
||||||
*
|
*
|
||||||
* Acceso: http://127.0.0.1:4082
|
* Acceso: http://127.0.0.1:4082
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const express = require("express");
|
|
||||||
const { exec } = require("child_process");
|
const { exec } = require("child_process");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const os = require("os");
|
const os = require("os");
|
||||||
@@ -34,14 +32,6 @@ const PORT = 4082;
|
|||||||
const HOST = "127.0.0.1";
|
const HOST = "127.0.0.1";
|
||||||
// ── FIN CONFIGURACIÓN ───────────────────────────────────────────────────────
|
// ── 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 ──────────────────────────────────────────────────────
|
// ── Helpers asíncronos ──────────────────────────────────────────────────────
|
||||||
const readFile = (path) => {
|
const readFile = (path) => {
|
||||||
try { return fs.readFileSync(path, "utf8").trim(); } catch { return null; }
|
try { return fs.readFileSync(path, "utf8").trim(); } catch { return null; }
|
||||||
@@ -93,7 +83,7 @@ function readProcStat() {
|
|||||||
|
|
||||||
async function getCpuUsage() {
|
async function getCpuUsage() {
|
||||||
const s1 = readProcStat();
|
const s1 = readProcStat();
|
||||||
await sleep(300);
|
await sleep(500);
|
||||||
const s2 = readProcStat();
|
const s2 = readProcStat();
|
||||||
if (!s1 || !s2) return { total: 0, cores: [] };
|
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,
|
// Nombre limpio: basename del ejecutable; si es un intérprete (node,
|
||||||
// python...), añade el basename del script que ejecuta.
|
// python...), añade el basename del script que ejecuta.
|
||||||
const exe = (parts[10] || "").split("/").pop();
|
const exe = (parts[10] || "").split("/").pop();
|
||||||
const arg1 = (parts[11] || "").split("/").pop();
|
const interp = /^(node|python\d?|bash|sh|perl|ruby|java)$/.test(exe);
|
||||||
const interp = /^(node|python\d?|bash|sh|perl)$/.test(exe);
|
let command = exe;
|
||||||
const command = (interp && arg1 ? `${exe} (${arg1})` : exe).slice(0, 24);
|
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 {
|
return {
|
||||||
user: parts[0],
|
user: parts[0],
|
||||||
cpu: parseFloat(parts[2]),
|
cpu: parseFloat(parts[2]),
|
||||||
@@ -278,10 +278,10 @@ async function getLogs(service, lines = 50) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Routes (caché por ruta: el trabajo se hace 1 vez por ventana) ──────────
|
// ── Handlers (caché por ruta: el trabajo se hace 1 vez por ventana) ────────
|
||||||
app.get("/system/info", async (req, res) => {
|
const routes = {
|
||||||
try {
|
|
||||||
const data = await cached("info", 3000, async () => {
|
"/system/info": async () => cached("info", 3000, async () => {
|
||||||
const [cpu, disk, processes] = await Promise.all([
|
const [cpu, disk, processes] = await Promise.all([
|
||||||
getCpuUsage(), getDisk(), getProcesses(),
|
getCpuUsage(), getDisk(), getProcesses(),
|
||||||
]);
|
]);
|
||||||
@@ -300,41 +300,60 @@ app.get("/system/info", async (req, res) => {
|
|||||||
uptime: getUptime(),
|
uptime: getUptime(),
|
||||||
os: `${os.type()} ${os.release()}`,
|
os: `${os.type()} ${os.release()}`,
|
||||||
};
|
};
|
||||||
});
|
}),
|
||||||
res.json(data);
|
|
||||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get("/system/bitcoin", async (req, res) => {
|
"/system/bitcoin": async () => {
|
||||||
try {
|
|
||||||
const info = await cached("bitcoin", 5000, getBitcoinInfo);
|
const info = await cached("bitcoin", 5000, getBitcoinInfo);
|
||||||
if (!info) return res.status(503).json({ error: "No se pudo conectar a Bitcoin Core" });
|
if (!info) return { __status: 503, error: "No se pudo conectar a Bitcoin Core" };
|
||||||
res.json(info);
|
return info;
|
||||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
},
|
||||||
});
|
|
||||||
|
|
||||||
app.get("/system/logs/bitcoin", async (req, res) => {
|
"/system/logs/bitcoin": async (query) => {
|
||||||
try {
|
const lines = parseInt(query.get("lines")) || 50;
|
||||||
const lines = parseInt(req.query.lines) || 50;
|
|
||||||
const logs = await cached(`logs-btc-${lines}`, 10000, () => getLogs("bitcoind", lines));
|
const logs = await cached(`logs-btc-${lines}`, 10000, () => getLogs("bitcoind", lines));
|
||||||
res.json({ logs });
|
return { logs };
|
||||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
},
|
||||||
});
|
|
||||||
|
|
||||||
app.get("/system/logs/fulcrum", async (req, res) => {
|
"/system/logs/fulcrum": async (query) => {
|
||||||
try {
|
const lines = parseInt(query.get("lines")) || 50;
|
||||||
const lines = parseInt(req.query.lines) || 50;
|
|
||||||
const logs = await cached(`logs-ful-${lines}`, 10000, () => getLogs("fulcrum", lines));
|
const logs = await cached(`logs-ful-${lines}`, 10000, () => getLogs("fulcrum", lines));
|
||||||
res.json({ logs });
|
return { logs };
|
||||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
},
|
||||||
});
|
|
||||||
|
|
||||||
app.get("/system/health", (req, res) => {
|
"/system/health": async () => ({ status: "ok", timestamp: Date.now() }),
|
||||||
res.json({ 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 ───────────────────────────────────────────────────────────────────
|
// ── Start ───────────────────────────────────────────────────────────────────
|
||||||
app.listen(PORT, HOST, () => {
|
server.listen(PORT, HOST, () => {
|
||||||
console.log(`
|
console.log(`
|
||||||
₿ Txoko System Metrics v2
|
₿ Txoko System Metrics v2
|
||||||
─────────────────────────────────
|
─────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user