feat: exportar informe de tx en JSON y Markdown + arreglo atajo Lab→Auditoría

This commit is contained in:
Aitor
2026-06-01 22:38:34 +02:00
parent 0110e84c49
commit 788ed3bada
2 changed files with 109 additions and 2 deletions
+21
View File
@@ -12,6 +12,27 @@ y el versionado sigue [Versionado Semántico](https://semver.org/lang/es/):
--- ---
## [1.2.0] — 2026-06-01
### Añadido
- Exportación del informe de privacidad de una transacción en dos formatos,
desde la pestaña Auditoría. El botón "↓ JSON" descarga el análisis completo
con su estructura (score, banda, wallets detectados y todas las señales con
su detalle), pensado para procesar con scripts o archivar en formato máquina.
El botón "↓ MD" descarga el informe legible en Markdown (diagnóstico, qué
puede saber un analista, limitaciones, recomendaciones y una tabla de señales),
para leer o guardar como documento. Ambos se generan en el navegador y se
descargan en local — no sale nada del nodo.
### Corregido
- El botón "Analizar privacidad" del Lab ahora envía correctamente el txid a
la pestaña Auditoría y lanza el análisis. Antes la pestaña aparecía vacía
porque el identificador no se transmitía entre vistas.
---
## [1.1.0] — 2026-06-01 ## [1.1.0] — 2026-06-01
### Añadido ### Añadido
+88 -2
View File
@@ -1069,11 +1069,97 @@
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);
// ── Exportar informe ──────────────────────────────────────────────
// Genera el archivo en el navegador y lo descarga en local.
// No pasa por ningún servidor, no sale nada del nodo.
const descargar = (contenido, nombre, tipo) => {
const blob = new Blob(["\uFEFF" + contenido], {type:tipo});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url; a.download = nombre;
document.body.appendChild(a); a.click(); document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const nombreBase = () => {
const fecha = new Date().toISOString().slice(0,10);
const txCorto = (tx?.txid || "tx").slice(0,12);
return `txoko-informe-${txCorto}-${fecha}`;
};
const exportJSON = () => {
const datos = {
generado: new Date().toISOString(),
herramienta: "Txoko Node Dashboard",
txid: tx?.txid || null,
privacidad: { score, banda: band, resumen: summary },
wallets_detectados: (wallets||[]).map(w => ({
nombre: w.name || w, confianza: w.confidence || null, señales: w.signals || []
})),
señales: checks.map(c => ({
id: c.id, etiqueta: c.label, certeza: c.certainty,
pasa: c.pass, informativo: !!c.informational,
accionabilidad: c.actionability || null,
penalizacion: c.penalty || 0, detalle: c.detail,
})),
};
descargar(JSON.stringify(datos, null, 2), nombreBase()+".json", "application/json;charset=utf-8");
};
const exportMarkdown = () => {
const L = [];
L.push(`# Informe de privacidad — Txoko`);
L.push("");
L.push(`**Transacción:** \`${tx?.txid || "—"}\` `);
L.push(`**Generado:** ${new Date().toLocaleString("es-ES")} `);
L.push(`**Nivel de privacidad:** ${band} (${score}/100) — ${summary}`);
L.push("");
if (wallets && wallets.length > 0) {
L.push(`**Wallet posible:** ${wallets.map(w=>w.name||w).join(", ")}`);
L.push("");
}
// Informe narrativo (reutiliza generateReport)
report.forEach(sec => {
L.push(`## ${sec.title}`);
L.push("");
if (sec.text) { L.push(sec.text); L.push(""); }
if (sec.recs) {
sec.recs.forEach(r => { L.push(`- **${r.action}** — ${r.why} _(${r.tag})_`); });
L.push("");
}
});
// Tabla de señales detectadas
L.push(`## Señales detectadas`);
L.push("");
L.push(`| Señal | Certeza | Estado | Detalle |`);
L.push(`| --- | --- | --- | --- |`);
checks.forEach(c => {
const estado = c.informational ? "informativo" : (c.pass ? "ok" : "detectado");
const detalle = (c.detail||"").replace(/\|/g,"\\|").replace(/\n/g," ");
L.push(`| ${c.label} | ${c.certainty} | ${estado} | ${detalle} |`);
});
L.push("");
L.push(`---`);
L.push(`_Generado por Txoko Node Dashboard — your node, your rules. Análisis local, ningún dato salió de tu nodo._`);
descargar(L.join("\n"), nombreBase()+".md", "text/markdown;charset=utf-8");
};
return ( return (
<div style={{marginTop:8}}> <div style={{marginTop:8}}>
<div style={{display:"flex",alignItems:"center",gap:8,marginBottom:14,paddingTop:14,borderTop:`1px solid ${C.border}`}}> <div style={{display:"flex",alignItems:"center",gap:8,marginBottom:14,paddingTop:14,borderTop:`1px solid ${C.border}`}}>
<span style={{fontSize:"0.65rem",fontFamily:"monospace",color:C.purple,textTransform:"uppercase",letterSpacing:"0.2em",fontWeight:700}}>Privacy Lab</span> <span style={{fontSize:"0.65rem",fontFamily:"monospace",color:C.purple,textTransform:"uppercase",letterSpacing:"0.2em",fontWeight:700}}>Privacy Lab</span>
<div style={{flex:1,height:1,background:`linear-gradient(to right,${C.purple}40,transparent)`}}/> <div style={{flex:1,height:1,background:`linear-gradient(to right,${C.purple}40,transparent)`}}/>
<button onClick={exportJSON} title="Descargar el informe en JSON (local, no sale del nodo)"
style={{padding:"4px 10px",background:C.bg,border:`1px solid ${C.purple}40`,borderRadius:5,color:C.purple,fontFamily:"monospace",fontSize:"0.6rem",letterSpacing:"0.08em",cursor:"pointer",whiteSpace:"nowrap"}}
onMouseEnter={e=>e.currentTarget.style.borderColor=C.purple}
onMouseLeave={e=>e.currentTarget.style.borderColor=C.purple+"40"}>
JSON
</button>
<button onClick={exportMarkdown} title="Descargar el informe en Markdown (local, no sale del nodo)"
style={{padding:"4px 10px",background:C.bg,border:`1px solid ${C.purple}40`,borderRadius:5,color:C.purple,fontFamily:"monospace",fontSize:"0.6rem",letterSpacing:"0.08em",cursor:"pointer",whiteSpace:"nowrap"}}
onMouseEnter={e=>e.currentTarget.style.borderColor=C.purple}
onMouseLeave={e=>e.currentTarget.style.borderColor=C.purple+"40"}>
MD
</button>
</div> </div>
<Card glow={summaryColor} style={{display:"flex",alignItems:"center",gap:16,padding:"14px 18px",marginBottom:10}}> <Card glow={summaryColor} style={{display:"flex",alignItems:"center",gap:16,padding:"14px 18px",marginBottom:10}}>
@@ -1917,8 +2003,8 @@
</div> </div>
{err&&<div style={{padding:"10px 14px",background:C.redMuted,border:`1px solid ${C.red}30`,borderRadius:6,color:C.red,fontFamily:"monospace",fontSize:"0.75rem"}}>Error: {err}</div>} {err&&<div style={{padding:"10px 14px",background:C.redMuted,border:`1px solid ${C.red}30`,borderRadius:6,color:C.red,fontFamily:"monospace",fontSize:"0.75rem"}}>Error: {err}</div>}
{loading&&<Spinner/>} {loading&&<Spinner/>}
{result&&result.type==="tx"&&<TxResult tx={result.data} onAnalyze={q=>{window.__auditQuery=q; window.__setTab&&window.__setTab("auditoria");}}/>} {result&&result.type==="tx"&&<TxResult tx={result.data} onAnalyze={q=>{window.__setTab&&window.__setTab("auditoria",q);}}/>}
{result&&result.type==="address"&&<AddressResult addr={result.data} onAnalyze={q=>{window.__auditQuery=q; window.__setTab&&window.__setTab("auditoria");}}/>} {result&&result.type==="address"&&<AddressResult addr={result.data} onAnalyze={q=>{window.__setTab&&window.__setTab("auditoria",q);}}/>}
{result&&result.type==="block"&&<BlockResult block={result.data}/>} {result&&result.type==="block"&&<BlockResult block={result.data}/>}
<UTXOMap base={base}/> <UTXOMap base={base}/>
</div> </div>