feat: mejoras de legibilidad del rastro - veredicto, jerarquia visual, barras y tooltip de detalle

This commit is contained in:
Aitor
2026-06-03 09:23:23 +02:00
parent 2950559203
commit b5f0c81b88
2 changed files with 117 additions and 15 deletions
+22
View File
@@ -12,6 +12,28 @@ y el versionado sigue [Versionado Semántico](https://semver.org/lang/es/):
--- ---
## [1.5.0] — 2026-06-03
### Cambiado
- Mejoras de legibilidad del rastro de procedencia, para interpretar los datos
de un vistazo:
- Veredicto arriba del rastro: "Camino limpio" (en verde) si nada de lo
explorado toca entidades marcadas, o "Atención" (en ámbar) indicando qué
toca y a cuántos saltos. Responde de un vistazo si hay algo que revisar.
- Jerarquía visual: los eslabones con buena privacidad y sin marcas se
muestran discretos; los que merecen atención (marca o banda no alta) se
destacan con color. El ojo va directo a lo relevante.
- Barra visual de cantidad en cada eslabón, para captar la proporción sin
leer la cifra; el número queda al lado como apoyo.
- Indicador de estructura (entradas → salidas) siempre visible junto a cada
eslabón, útil para detectar consolidaciones grandes de un vistazo.
- Detalle ampliado al pasar el ratón (o tocar en móvil) sobre el indicador
de estructura: número exacto de entradas y salidas, valor en sat y BTC,
bloque, comisión y tasa. Mantiene la vista limpia y el detalle a mano.
---
## [1.4.0] — 2026-06-02 ## [1.4.0] — 2026-06-02
### Añadido ### Añadido
+92 -12
View File
@@ -2420,6 +2420,24 @@
const {marcas, esCoinbase} = useMemo(()=>marcasDeTx(txData,ana),[txData,ana]); const {marcas, esCoinbase} = useMemo(()=>marcasDeTx(txData,ana),[txData,ana]);
const mempoolUrl = base ? `${base}/tx/${txData.txid}` : `https://mempool.space/tx/${txData.txid}`; const mempoolUrl = base ? `${base}/tx/${txData.txid}` : `https://mempool.space/tx/${txData.txid}`;
const inputs = txData.vin || []; const inputs = txData.vin || [];
const [hover,setHover]=useState(false);
// Detalle para el tooltip: lo que un noderunner querría ver de un eslabón
const detalle = useMemo(()=>{
const nIn = (txData.vin||[]).length;
const nOut = (txData.vout||[]).length;
const totalOut = (txData.vout||[]).reduce((s,o)=>s+(o.value||0),0);
const totalIn = (txData.vin||[]).reduce((s,v)=>s+(v.prevout?.value||0),0);
const fee = txData.fee || (totalIn>totalOut ? totalIn-totalOut : null);
const vsize = txData.weight ? Math.ceil(txData.weight/4) : null;
const feeRate = (fee && vsize) ? (fee/vsize) : (txData.feerate ? Number(txData.feerate) : null);
return {
nIn, nOut,
btc: (fluyo/1e8).toFixed(8),
bloque: txData.status?.confirmed ? txData.status.block_height : null,
fee, feeRate,
};
},[txData,fluyo]);
const seguirInput = async (i) => { const seguirInput = async (i) => {
const vin=inputs[i]; const vin=inputs[i];
@@ -2433,28 +2451,79 @@
}catch(e){ setHijos(s=>({...s,[i]:{err:e.message}})); } }catch(e){ setHijos(s=>({...s,[i]:{err:e.message}})); }
}; };
// ¿Este eslabón merece atención? (tiene marca, o banda no es ALTA)
// Lo relevante se destaca; lo limpio se silencia.
const relevante = marcas.length>0 || (ana?.band && ana.band!=="ALTA");
const acento = marcas.length>0 ? marcas[0].color : bandColorRastro(ana?.band);
// Barra visual de cantidad: proporción respecto al flujo de entrada (vout origen).
// Da una idea de tamaño sin leer la cifra. Si no hay referencia, escala suave.
const pctBarra = (() => {
const ref = fluyo || 1;
const outTotal = (txData.vout||[]).reduce((s,o)=>s+(o.value||0),0) || ref;
const p = Math.min(100, Math.round((fluyo/outTotal)*100));
return isFinite(p) && p>0 ? p : 5;
})();
return ( return (
<div style={{padding:"10px 12px",background:C.bg,borderRadius:6,border:`1px solid ${C.border}`,marginTop:8}}> <div
style={{
position:"relative",
padding:"10px 12px",
background: relevante ? `${acento}0d` : C.bg,
borderRadius:6,
border:`1px solid ${relevante ? acento+"55" : C.border}`,
marginTop:8,
opacity: relevante ? 1 : 0.78,
}}>
<div style={{display:"flex",justifyContent:"space-between",alignItems:"center",gap:8,flexWrap:"wrap",marginBottom:6}}> <div style={{display:"flex",justifyContent:"space-between",alignItems:"center",gap:8,flexWrap:"wrap",marginBottom:6}}>
<div style={{display:"flex",alignItems:"center",gap:8,position:"relative"}}>
<a href={mempoolUrl} target="_blank" rel="noopener noreferrer" <a href={mempoolUrl} target="_blank" rel="noopener noreferrer"
style={{fontSize:"0.64rem",fontFamily:"monospace",color:C.blue,textDecoration:"none",wordBreak:"break-all"}}> style={{fontSize:"0.64rem",fontFamily:"monospace",color:C.blue,textDecoration:"none",wordBreak:"break-all"}}>
{abrevTxid(txData.txid)} {abrevTxid(txData.txid)}
</a> </a>
{ana?.band&&( <button
<span style={{fontSize:"0.58rem",fontFamily:"monospace",color:bandColorRastro(ana.band),border:`1px solid ${bandColorRastro(ana.band)}40`,borderRadius:4,padding:"1px 7px"}}>{ana.band}</span> onMouseEnter={()=>setHover(true)}
onMouseLeave={()=>setHover(false)}
onClick={()=>setHover(h=>!h)} title="Ver detalle"
style={{padding:"0 6px",background:hover?C.borderBright:"transparent",border:`1px solid ${C.borderBright}`,borderRadius:4,color:C.t2,fontFamily:"monospace",fontSize:"0.58rem",cursor:"pointer",lineHeight:"1.5"}}>
{detalle.nIn}{detalle.nOut}
</button>
{/* Tooltip de detalle: aparece al pasar el ratón o tocar la pastilla i→o */}
{hover&&(
<div style={{position:"absolute",zIndex:50,bottom:"100%",left:0,marginBottom:6,minWidth:250,padding:"12px 14px",background:"#0a0e14",border:`1px solid ${acento}`,borderRadius:6,boxShadow:"0 10px 36px rgba(0,0,0,0.92)"}}>
<div style={{fontSize:"0.64rem",fontFamily:"monospace",lineHeight:2}}>
<div><span style={{color:C.t1}}>Estructura </span><span style={{color:"#fff",fontWeight:700}}>{detalle.nIn} in {detalle.nOut} out</span></div>
<div><span style={{color:C.t1}}>Fluyó </span><span style={{color:C.amber,fontWeight:700}}>{fmt.num(fluyo)} sat</span><span style={{color:C.t1}}> · </span><span style={{color:"#fff",fontWeight:700}}>{detalle.btc} BTC</span></div>
{detalle.bloque!=null
? <div><span style={{color:C.t1}}>Bloque </span><span style={{color:C.blue,fontWeight:700}}>#{fmt.num(detalle.bloque)}</span></div>
: <div><span style={{color:C.t1}}>Estado </span><span style={{color:C.amber,fontWeight:700}}>sin confirmar (mempool)</span></div>}
{detalle.fee!=null&&<div><span style={{color:C.t1}}>Fee </span><span style={{color:"#fff",fontWeight:700}}>{fmt.num(detalle.fee)} sat</span>{detalle.feeRate&&<span style={{color:C.t1}}> · {detalle.feeRate.toFixed(1)} sat/vB</span>}</div>}
{marcas.length>0&&<div style={{marginTop:4}}>{marcas.map((m,j)=>(<span key={j} style={{color:m.color,fontWeight:700,marginRight:8}}>{m.txt}</span>))}</div>}
</div>
</div>
)} )}
</div> </div>
<div style={{fontSize:"0.6rem",color:C.t2,fontFamily:"monospace",marginBottom:6}}> {ana?.band&&(
Fluyó {fmt.num(fluyo)} sat hacia el eslabón anterior <span style={{fontSize:"0.58rem",fontFamily:"monospace",fontWeight:relevante?700:400,color:bandColorRastro(ana.band),border:`1px solid ${bandColorRastro(ana.band)}${relevante?"":"40"}`,borderRadius:4,padding:"1px 7px"}}>{ana.band}</span>
{txData.status?.block_time&&<span> · {fmt.date(txData.status.block_time)}</span>} )}
</div>
{/* Barra visual de cantidad: el ojo capta proporción sin leer cifras */}
<div style={{display:"flex",alignItems:"center",gap:8,marginBottom:6}}>
<div style={{flex:1,height:4,background:C.border,borderRadius:2,overflow:"hidden"}}>
<div style={{width:`${pctBarra}%`,height:"100%",background:relevante?acento:C.t3,borderRadius:2}}/>
</div>
<span style={{fontSize:"0.58rem",color:C.t2,fontFamily:"monospace",whiteSpace:"nowrap"}}>
<span style={{color:C.t1}}>{fmt.num(fluyo)} sat</span>{txData.status?.block_time&&` · ${fmt.date(txData.status.block_time)}`}
</span>
</div> </div>
{marcas.length>0&&( {marcas.length>0&&(
<div style={{display:"flex",gap:6,flexWrap:"wrap",marginBottom:6}}> <div style={{display:"flex",gap:6,flexWrap:"wrap",marginBottom:6}}>
{marcas.map((m,j)=>(<span key={j} style={{fontSize:"0.56rem",fontFamily:"monospace",color:m.color,border:`1px solid ${m.color}40`,borderRadius:4,padding:"1px 6px"}}>{m.txt}</span>))} {marcas.map((m,j)=>(<span key={j} style={{fontSize:"0.58rem",fontFamily:"monospace",fontWeight:700,color:m.color,background:`${m.color}1a`,border:`1px solid ${m.color}55`,borderRadius:4,padding:"1px 7px"}}>{m.txt}</span>))}
</div> </div>
)} )}
{esCoinbase&&( {esCoinbase&&(
<div style={{fontSize:"0.58rem",color:C.t3,marginTop:2}}>Fin de la rama: estas monedas se crearon aquí (recompensa de minado).</div> <div style={{fontSize:"0.58rem",color:C.green,marginTop:2}}> Origen: estas monedas se crearon aquí (recompensa de minado).</div>
)} )}
{/* Seguir más atrás desde este eslabón (recursivo, bajo demanda) */} {/* Seguir más atrás desde este eslabón (recursivo, bajo demanda) */}
@@ -2563,9 +2632,11 @@
Sigue cada input hacia atrás, un salto a la vez. Cada consulta la pides , para no sobrecargar el nodo. Desde cada eslabón puedes seguir bajando. Sigue cada input hacia atrás, un salto a la vez. Cada consulta la pides , para no sobrecargar el nodo. Desde cada eslabón puedes seguir bajando.
</div> </div>
{resumenDistancia.length>0&&( {resumenDistancia.length>0 ? (
<div style={{marginBottom:12,padding:"10px 12px",background:C.bg,borderRadius:6,border:`1px solid ${C.border}`}}> <div style={{marginBottom:12,padding:"10px 12px",background:`${C.amber}0d`,borderRadius:6,border:`1px solid ${C.amber}55`}}>
<div style={{fontSize:"0.58rem",color:C.t2,fontFamily:"monospace",textTransform:"uppercase",letterSpacing:"0.1em",marginBottom:6}}>De lo explorado, lo más cercano</div> <div style={{fontSize:"0.66rem",color:C.amber,fontFamily:"monospace",fontWeight:700,marginBottom:6}}>
Atención: el rastro toca {resumenDistancia.map(r=>r.cat.replace(/^.. /,"")).join(", ")}
</div>
<div style={{display:"flex",gap:8,flexWrap:"wrap"}}> <div style={{display:"flex",gap:8,flexWrap:"wrap"}}>
{resumenDistancia.map((r,j)=>( {resumenDistancia.map((r,j)=>(
<span key={j} style={{fontSize:"0.6rem",fontFamily:"monospace",color:C.t1,border:`1px solid ${C.border}`,borderRadius:4,padding:"2px 8px"}}> <span key={j} style={{fontSize:"0.6rem",fontFamily:"monospace",color:C.t1,border:`1px solid ${C.border}`,borderRadius:4,padding:"2px 8px"}}>
@@ -2577,7 +2648,16 @@
Solo cuenta las ramas que has seguido. Puede haber más sin explorar. Solo cuenta las ramas que has seguido. Puede haber más sin explorar.
</div> </div>
</div> </div>
)} ) : explorados>0 ? (
<div style={{marginBottom:12,padding:"10px 12px",background:`${C.green}0d`,borderRadius:6,border:`1px solid ${C.green}40`}}>
<div style={{fontSize:"0.66rem",color:C.green,fontFamily:"monospace",fontWeight:700}}>
Camino limpio
</div>
<div style={{fontSize:"0.55rem",color:C.t3,marginTop:4,lineHeight:1.4}}>
De lo explorado, nada toca entidades marcadas. Puede haber más ramas sin seguir.
</div>
</div>
) : null}
{/* ¿La propia tx analizada es coinbase? Entonces es el origen: no hay {/* ¿La propia tx analizada es coinbase? Entonces es el origen: no hay
nada hacia atrás, estas monedas se crearon aquí. */} nada hacia atrás, estas monedas se crearon aquí. */}