diff --git a/CHANGELOG.md b/CHANGELOG.md
index 03b02e0..499899e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,31 @@ 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.12.1] — 2026-07-27
+### Corregido
+- **El monitor mostraba una CPU por proceso que engañaba.** La columna `%CPU`
+ de `ps aux` no es el consumo actual sino la **media desde que el proceso
+ arrancó** (tiempo de CPU dividido por tiempo de vida). Un proceso que trabajó
+ mucho hace tres días seguía apareciendo alto para siempre, y se leía como si
+ estuviera saturando la máquina. Se detectó porque los porcentajes por proceso
+ no cambiaban NUNCA entre lecturas mientras la CPU global sí variaba.
+ - Ahora se mide de verdad: dos lecturas de `/proc/PID/stat` separadas 500 ms,
+ el mismo método que ya usaba `getCpuUsage` para el total. Las dos esperas
+ corren en paralelo dentro del mismo `Promise.all`, así que no cuesta tiempo.
+ - La interfaz muestra el **porcentaje sobre el total de la máquina**, que es
+ lo que suele querer saberse, con el porcentaje de un núcleo y la media de
+ `ps` en el tooltip para poder comparar con `top`.
+ - Comprobado con carga artificial: el proceso ocupado marcaba 100% de un
+ núcleo (25% de un sistema de 4) mientras `ps` daba 152%, imposible para un
+ proceso de un solo hilo.
+
+### Cambiado
+- El UTXO Map pedía las transacciones de contexto con `fetchWithTimeout`
+ directo, saltándose la caché añadida en la 1.11.0. Era el único punto que no
+ la aprovechaba. Ahora pasa por `get()`, así que esas transacciones —ya
+ confirmadas, e inmutables— se guardan toda la sesión.
+
---
## [1.12.0] — 2026-07-27
### Añadido
diff --git a/dashboard.html b/dashboard.html
index 0f80c04..14ceaab 100644
--- a/dashboard.html
+++ b/dashboard.html
@@ -2837,11 +2837,22 @@
{p.command}
- CPU {p.cpu}%
+ {/* Se muestra el % del sistema (lo que de verdad se
+ quiere saber: cuánto de la máquina se lleva). El
+ % de un núcleo va en el tooltip, para comparar con
+ top si hace falta. */}
+ 25?C.red:p.cpuSistema>10?C.amber:C.t2}>
+
+ CPU {p.cpuSistema!=null?p.cpuSistema:p.cpu}%
+
+
30?C.red:p.mem>15?C.amber:C.t2}>RAM {p.mem}%
))}
+
+ CPU sobre el total de la máquina ({sysData.cpu?.count||"?"} núcleos), medida en una ventana de 500 ms — no es la media desde el arranque que muestra ps, que engaña porque un proceso que trabajó mucho hace días sigue apareciendo alto.
+
)}
@@ -3571,13 +3582,15 @@
if(sorted.length>MAX_UTXOS) setErr(`Mostrando ${MAX_UTXOS} de ${sorted.length} UTXOs — se muestran los de mayor valor.`);
// Cargar txs de origen para contexto (máx 10, las más grandes)
+ // Vía get() y no fetch directo: así pasa por la caché compartida.
+ // Las transacciones confirmadas son inmutables y se guardan toda la
+ // sesión, de modo que volver a este UTXO Map (o analizar una de estas
+ // txs en otra pestaña) no cuesta ni una petición más al nodo.
const cache={};
const toFetch=limited.slice(0,10);
await Promise.all(toFetch.map(async u=>{
- try{
- const res=await fetchWithTimeout(`${base}/api/tx/${u.txid}`);
- if(res.ok) cache[u.txid]=await res.json();
- }catch{}
+ const t = await get(`/api/tx/${u.txid}`, null);
+ if (t) cache[u.txid] = t;
}));
setTxCache(cache);
diff --git a/system-metrics.js b/system-metrics.js
index 303c781..afc54bf 100644
--- a/system-metrics.js
+++ b/system-metrics.js
@@ -168,10 +168,33 @@ async function getDisk() {
}
// ── Procesos destacados (nombre limpio del binario) ────────────────────────
+// Tiempo de CPU consumido por un proceso, en ticks, desde /proc/PID/stat.
+// Campos 14 (utime) y 15 (stime). El nombre del proceso va entre paréntesis y
+// puede contener espacios, así que se corta por el ÚLTIMO ')' antes de partir.
+function readProcCpuTicks(pid) {
+ try {
+ const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
+ const resto = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
+ // resto[0] es el campo 3 (estado), así que utime=campo14 → resto[11]
+ const utime = parseInt(resto[11], 10);
+ const stime = parseInt(resto[12], 10);
+ if (Number.isNaN(utime) || Number.isNaN(stime)) return null;
+ return utime + stime;
+ } catch { return null; }
+}
+
+// OJO con la columna %CPU de `ps`: NO es el consumo actual, sino la media del
+// proceso desde que arrancó (tiempo de CPU / tiempo de vida). Un proceso que
+// trabajó mucho al principio y ahora está ocioso sigue mostrando un número
+// alto días después, y se lee como si estuviera saturando la máquina.
+// Aquí se mide de verdad: dos lecturas de /proc separadas 500 ms, el mismo
+// método que ya usa getCpuUsage para el total del sistema. Las dos esperas
+// corren en paralelo (van dentro del mismo Promise.all), así que no cuesta
+// tiempo extra.
async function getProcesses() {
const result = await sh("ps aux --no-headers --sort=-%mem | head -8");
if (!result) return [];
- return result.split("\n").map(line => {
+ const filas = result.split("\n").map(line => {
const parts = line.trim().split(/\s+/);
// Nombre limpio: basename del ejecutable; si es un intérprete (node,
// python...), añade el basename del script que ejecuta.
@@ -190,12 +213,33 @@ async function getProcesses() {
}
command = command.slice(0, 24);
return {
+ pid: parts[1],
user: parts[0],
- cpu: parseFloat(parts[2]),
+ cpuMedia: parseFloat(parts[2]), // media desde el arranque (lo que da ps)
mem: parseFloat(parts[3]),
command,
};
}).filter(p => p.mem > 0.5);
+
+ const ticks1 = filas.map(p => readProcCpuTicks(p.pid));
+ await sleep(500);
+ const ticks2 = filas.map(p => readProcCpuTicks(p.pid));
+
+ const USER_HZ = 100; // estándar en Linux
+ const VENTANA_S = 0.5;
+ const nucleos = os.cpus().length || 1;
+
+ return filas.map((p, i) => {
+ let cpu = null, cpuSistema = null;
+ if (ticks1[i] !== null && ticks2[i] !== null) {
+ const seg = (ticks2[i] - ticks1[i]) / USER_HZ;
+ // % de UN núcleo (criterio de top/ps: puede pasar de 100 si va en varios)
+ cpu = Math.max(0, Math.round((seg / VENTANA_S) * 1000) / 10);
+ // % del total de la máquina, que es lo que suele querer saberse
+ cpuSistema = Math.round((cpu / nucleos) * 10) / 10;
+ }
+ return { user: p.user, command: p.command, mem: p.mem, cpu, cpuSistema, cpuMedia: p.cpuMedia };
+ });
}
// ── Load / Uptime ───────────────────────────────────────────────────────────