feat: exportar informe de tx en JSON y Markdown + arreglo atajo Lab→Auditoría
This commit is contained in:
@@ -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
|
||||
|
||||
### Añadido
|
||||
|
||||
+88
-2
@@ -1069,11 +1069,97 @@
|
||||
const {score,checks,summary,summaryColor,wallets,band} = analysis;
|
||||
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 (
|
||||
<div style={{marginTop:8}}>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<Card glow={summaryColor} style={{display:"flex",alignItems:"center",gap:16,padding:"14px 18px",marginBottom:10}}>
|
||||
@@ -1917,8 +2003,8 @@
|
||||
</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/>}
|
||||
{result&&result.type==="tx"&&<TxResult tx={result.data} onAnalyze={q=>{window.__auditQuery=q; window.__setTab&&window.__setTab("auditoria");}}/>}
|
||||
{result&&result.type==="address"&&<AddressResult addr={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.__setTab&&window.__setTab("auditoria",q);}}/>}
|
||||
{result&&result.type==="block"&&<BlockResult block={result.data}/>}
|
||||
<UTXOMap base={base}/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user