inicio: dashboard de privacidad Bitcoin con análisis on-chain
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* txoko-metrics — Backend de métricas del nodo Bitcoin
|
||||
*
|
||||
* Servidor HTTP Node.js que escucha en 127.0.0.1:4082.
|
||||
* Sirve métricas del sistema y estado de Bitcoin Core al dashboard.
|
||||
*
|
||||
* CONFIGURACIÓN:
|
||||
* Editar las variables de la sección "CONFIGURACIÓN LOCAL" con
|
||||
* los datos de tu nodo antes de arrancar.
|
||||
*
|
||||
* ARRANCAR:
|
||||
* node system-metrics.js
|
||||
*
|
||||
* O como servicio systemd:
|
||||
* sudo systemctl start txoko-metrics
|
||||
*/
|
||||
|
||||
// ── CONFIGURACIÓN LOCAL ──────────────────────────────────────────────
|
||||
// Copia este archivo como system-metrics.local.js y edita estos valores.
|
||||
// El archivo .local.js está en .gitignore — nunca se sube al repo.
|
||||
|
||||
const RPC_HOST = '127.0.0.1';
|
||||
const RPC_PORT = 8332;
|
||||
const RPC_USER = 'TU_RPC_USER'; // Ver bitcoin.conf → rpcuser
|
||||
const RPC_PASSWORD = 'TU_RPC_PASSWORD'; // Ver bitcoin.conf → rpcpassword
|
||||
// Alternativa: autenticación por cookie (más seguro)
|
||||
// const RPC_COOKIE = '/data/bitcoin/.cookie';
|
||||
|
||||
const METRICS_PORT = 4082;
|
||||
const METRICS_HOST = '127.0.0.1'; // Solo localhost — no exponer al exterior
|
||||
|
||||
// ── FIN CONFIGURACIÓN ────────────────────────────────────────────────
|
||||
|
||||
const http = require('http');
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
|
||||
// Cliente RPC Bitcoin Core
|
||||
async function rpcCall(method, params = []) {
|
||||
const body = JSON.stringify({ jsonrpc: '1.0', id: 'txoko', method, params });
|
||||
const auth = Buffer.from(`${RPC_USER}:${RPC_PASSWORD}`).toString('base64');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http.request({
|
||||
host: RPC_HOST, port: RPC_PORT, method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': `Basic ${auth}` }
|
||||
}, res => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
if (parsed.error) reject(parsed.error);
|
||||
else resolve(parsed.result);
|
||||
} catch (e) { reject(e); }
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// Endpoints
|
||||
const routes = {
|
||||
|
||||
'/system/info': async () => {
|
||||
const cpus = os.cpus();
|
||||
const totalMem = os.totalmem();
|
||||
const freeMem = os.freemem();
|
||||
const usedMem = totalMem - freeMem;
|
||||
|
||||
// CPU usage (snapshot simple)
|
||||
const cpuUsage = cpus.reduce((sum, cpu) => {
|
||||
const total = Object.values(cpu.times).reduce((a, b) => a + b, 0);
|
||||
const idle = cpu.times.idle;
|
||||
return sum + ((total - idle) / total) * 100;
|
||||
}, 0) / cpus.length;
|
||||
|
||||
return {
|
||||
cpu: { usage: cpuUsage.toFixed(1), cores: cpus.length, model: cpus[0]?.model },
|
||||
memory: {
|
||||
total: totalMem, free: freeMem, used: usedMem,
|
||||
usedPercent: ((usedMem / totalMem) * 100).toFixed(1)
|
||||
},
|
||||
uptime: os.uptime(),
|
||||
platform: os.platform(),
|
||||
hostname: os.hostname()
|
||||
};
|
||||
},
|
||||
|
||||
'/system/bitcoin': async () => {
|
||||
const [info, netinfo, mempoolinfo] = await Promise.all([
|
||||
rpcCall('getblockchaininfo'),
|
||||
rpcCall('getnetworkinfo'),
|
||||
rpcCall('getmempoolinfo')
|
||||
]);
|
||||
return { blockchain: info, network: netinfo, mempool: mempoolinfo };
|
||||
},
|
||||
|
||||
'/system/logs/bitcoin': async () => {
|
||||
try {
|
||||
const lines = execSync('journalctl -u bitcoind -n 80 --no-pager -o short-iso 2>/dev/null || tail -80 /data/bitcoin/debug.log 2>/dev/null || echo ""')
|
||||
.toString().trim().split('\n').filter(Boolean);
|
||||
return { logs: lines.map(line => ({
|
||||
line,
|
||||
level: line.includes('error') || line.includes('ERROR') ? 'error'
|
||||
: line.includes('warn') || line.includes('WARN') ? 'warn'
|
||||
: 'info'
|
||||
}))};
|
||||
} catch { return { logs: [] }; }
|
||||
},
|
||||
|
||||
'/system/logs/fulcrum': async () => {
|
||||
try {
|
||||
const lines = execSync('journalctl -u fulcrum -n 80 --no-pager -o short-iso 2>/dev/null || echo ""')
|
||||
.toString().trim().split('\n').filter(Boolean);
|
||||
return { logs: lines.map(line => ({
|
||||
line,
|
||||
level: line.includes('Error') || line.includes('error') ? 'error'
|
||||
: line.includes('Warn') || line.includes('warn') ? 'warn'
|
||||
: 'info'
|
||||
}))};
|
||||
} catch { return { logs: [] }; }
|
||||
},
|
||||
|
||||
'/system/health': async () => ({ status: 'ok', ts: Date.now() })
|
||||
};
|
||||
|
||||
// Servidor HTTP
|
||||
const server = http.createServer(async (req, res) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
|
||||
// Extraer ruta sin query string
|
||||
const path = req.url.split('?')[0];
|
||||
const handler = routes[path];
|
||||
|
||||
if (!handler) {
|
||||
res.writeHead(404);
|
||||
return res.end(JSON.stringify({ error: 'Not found' }));
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await handler();
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify(data));
|
||||
} catch (err) {
|
||||
res.writeHead(500);
|
||||
res.end(JSON.stringify({ error: err.message || 'Internal error' }));
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(METRICS_PORT, METRICS_HOST, () => {
|
||||
console.log(`txoko-metrics escuchando en ${METRICS_HOST}:${METRICS_PORT}`);
|
||||
});
|
||||
Reference in New Issue
Block a user