feat: mejoras de legibilidad del rastro - veredicto, jerarquia visual, barras y tooltip de detalle
This commit is contained in:
@@ -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
|
||||
|
||||
### Añadido
|
||||
|
||||
+95
-15
@@ -2420,6 +2420,24 @@
|
||||
const {marcas, esCoinbase} = useMemo(()=>marcasDeTx(txData,ana),[txData,ana]);
|
||||
const mempoolUrl = base ? `${base}/tx/${txData.txid}` : `https://mempool.space/tx/${txData.txid}`;
|
||||
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 vin=inputs[i];
|
||||
@@ -2433,28 +2451,79 @@
|
||||
}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 (
|
||||
<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}}>
|
||||
<a href={mempoolUrl} target="_blank" rel="noopener noreferrer"
|
||||
style={{fontSize:"0.64rem",fontFamily:"monospace",color:C.blue,textDecoration:"none",wordBreak:"break-all"}}>
|
||||
{abrevTxid(txData.txid)} ↗
|
||||
</a>
|
||||
<div style={{display:"flex",alignItems:"center",gap:8,position:"relative"}}>
|
||||
<a href={mempoolUrl} target="_blank" rel="noopener noreferrer"
|
||||
style={{fontSize:"0.64rem",fontFamily:"monospace",color:C.blue,textDecoration:"none",wordBreak:"break-all"}}>
|
||||
{abrevTxid(txData.txid)} ↗
|
||||
</a>
|
||||
<button
|
||||
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>
|
||||
{ana?.band&&(
|
||||
<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>
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
<div style={{fontSize:"0.6rem",color:C.t2,fontFamily:"monospace",marginBottom:6}}>
|
||||
Fluyó {fmt.num(fluyo)} sat hacia el eslabón anterior
|
||||
{txData.status?.block_time&&<span> · {fmt.date(txData.status.block_time)}</span>}
|
||||
{/* 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>
|
||||
{marcas.length>0&&(
|
||||
<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>
|
||||
)}
|
||||
{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) */}
|
||||
@@ -2563,9 +2632,11 @@
|
||||
Sigue cada input hacia atrás, un salto a la vez. Cada consulta la pides tú, para no sobrecargar el nodo. Desde cada eslabón puedes seguir bajando.
|
||||
</div>
|
||||
|
||||
{resumenDistancia.length>0&&(
|
||||
<div style={{marginBottom:12,padding:"10px 12px",background:C.bg,borderRadius:6,border:`1px solid ${C.border}`}}>
|
||||
<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>
|
||||
{resumenDistancia.length>0 ? (
|
||||
<div style={{marginBottom:12,padding:"10px 12px",background:`${C.amber}0d`,borderRadius:6,border:`1px solid ${C.amber}55`}}>
|
||||
<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"}}>
|
||||
{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"}}>
|
||||
@@ -2577,7 +2648,16 @@
|
||||
Solo cuenta las ramas que has seguido. Puede haber más sin explorar.
|
||||
</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
|
||||
nada hacia atrás, estas monedas se crearon aquí. */}
|
||||
|
||||
Reference in New Issue
Block a user