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:
+118
-42
@@ -2150,10 +2150,78 @@
|
||||
const [expanded,setExpanded] = useState(null);
|
||||
const [showDidactic,setShowDidactic] = useState(null);
|
||||
const [showReport,setShowReport] = useState(false);
|
||||
const [showOkGroup,setShowOkGroup] = useState(false);
|
||||
const [showInfoGroup,setShowInfoGroup] = useState(false);
|
||||
if (!analysis) return null;
|
||||
const {score,checks,summary,summaryColor,wallets,band} = analysis;
|
||||
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 ──────────────────────────────────────────────
|
||||
// Genera el archivo en el navegador y lo descarga en local.
|
||||
// No pasa por ningún servidor, no sale nada del nodo.
|
||||
@@ -2301,7 +2369,7 @@
|
||||
color: rec.tag==="wallet"?C.purple:C.amber,
|
||||
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>
|
||||
</div>
|
||||
<div style={{fontSize:"0.65rem",color:C.t2,lineHeight:1.6,paddingLeft:18}}>{rec.why}</div>
|
||||
@@ -2315,52 +2383,60 @@
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{display:"flex",flexDirection:"column",gap:6}}>
|
||||
{checks.map(check => (
|
||||
<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`,
|
||||
}}>
|
||||
{check.actionability==="no_corregible"?"ya en chain":check.actionability==="wallet"?"cambia wallet":"evitable"}
|
||||
</span>
|
||||
)}
|
||||
<CertaintyBadge level={check.certainty}/>
|
||||
{(() => {
|
||||
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}}>
|
||||
{/* Resumen de un vistazo */}
|
||||
<div style={{display:"flex",gap:14,alignItems:"center",padding:"8px 4px",flexWrap:"wrap"}}>
|
||||
{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>
|
||||
{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>
|
||||
{!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"}
|
||||
|
||||
{/* 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"}}>
|
||||
<span style={{fontSize:"0.8rem",color:C.green,fontWeight:700}}>✓</span>
|
||||
<span style={{flex:1,fontSize:"0.7rem",color:C.t2,fontFamily:"monospace"}}>{oks.length} señales correctas</span>
|
||||
<span style={{color:C.t2,fontSize:"0.7rem"}}>{showOkGroup?"▲":"▼"}</span>
|
||||
</div>
|
||||
{showOkGroup&&(
|
||||
<div style={{display:"flex",flexDirection:"column",gap:6,padding:"0 8px 8px 8px"}}>
|
||||
{oks.map(c => renderCheck(c, false))}
|
||||
</div>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
|
||||
{/* Informativas: agrupadas, colapsadas por defecto */}
|
||||
{infos.length>0&&(
|
||||
<div style={{background:C.bgCard,border:`1px solid ${C.border}`,borderRadius:6,overflow:"hidden"}}>
|
||||
<div onClick={()=>setShowInfoGroup(!showInfoGroup)}
|
||||
style={{display:"flex",alignItems:"center",gap:8,padding:"10px 14px",cursor:"pointer"}}>
|
||||
<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 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=>(
|
||||
@@ -2369,9 +2445,9 @@
|
||||
</div>
|
||||
<div style={{display:"flex",gap:10,marginTop:8,flexWrap:"wrap"}}>
|
||||
{[
|
||||
{label:"ya en chain", color:C.red, desc:"No corregible"},
|
||||
{label:"cambia wallet", color:C.purple, desc:"Depende del software"},
|
||||
{label:"evitable", color:C.amber, desc:"Puedes evitarlo en el futuro"},
|
||||
{label:"ya en chain", color:C.red, desc:"Registrado en la cadena, no se puede cambiar"},
|
||||
{label:"depende del wallet", color:C.purple, desc:"Lo determina tu software de wallet"},
|
||||
{label:"puedes evitarlo", color:C.amber, desc:"Depende de cómo construyes la transacción"},
|
||||
].map(l=>(
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user