diff --git a/dashboard.html b/dashboard.html index 2445af0..9446ecf 100644 --- a/dashboard.html +++ b/dashboard.html @@ -4998,42 +4998,63 @@ const [amountInput, setAmountInput] = useState(""); const [declaracion, setDeclaracion] = useState(""); const [maxHops, setMaxHops] = useState(8); - const [scanning, setScanning] = useState(false); + const [busy, setBusy] = useState(false); // hay una petición al nodo en curso (iniciar o avanzar un salto) const [progress, setProgress] = useState(""); const [error, setError] = useState(null); - const [graph, setGraph] = useState(null); const [report, setReport] = useState(null); + const [tick, setTick] = useState(0); // fuerza re-render al mutar traceRef.current + // El rastreo vive en un ref (no en estado) porque lo muta directamente + // advanceForensicHop entre clics del usuario — "tick" es lo que le dice + // a React que vuelva a leerlo. Así el estado (nodos, aristas, frontier) + // sobrevive entre saltos sin tener que reconstruirlo cada vez. + const traceRef = useRef(null); - const iniciar = async (hops) => { + const avanzarSalto = async (trace) => { + setBusy(true); setError(null); + try { + await advanceForensicHop(trace, { maxHops, onProgress: setProgress }); + setTick(t => t+1); + } catch(e) { + setError(e.message); + } + setBusy(false); setProgress(""); + }; + + const iniciar = async () => { const txid = txidInput.trim(); if (!/^[0-9a-f]{64}$/i.test(txid)) { setError("Introduce un txid de origen válido (64 caracteres hex)."); return; } const vout = parseInt(voutInput, 10); if (!Number.isInteger(vout) || vout < 0) { setError("El vout debe ser un número entero ≥ 0."); return; } if (!base) { setError("Necesitas conectar tu nodo (CONFIG) para rastrear — el peritaje consulta tu Mempool self-hosted, no funciona en modo demo."); return; } - setError(null); setScanning(true); setGraph(null); setReport(null); - const effectiveHops = hops || maxHops; + setError(null); setBusy(true); setReport(null); try { const amountStolen = amountInput.trim() ? Math.round(parseFloat(amountInput)*1e8) : null; - const g = await buildForensicGraph({ - get, originTxid: txid, originVout: vout, amountStolen, - maxHops: effectiveHops, - onProgress: (msg) => setProgress(msg), - }); - setGraph(g); - setReport(buildForensicReport(g, declaracion)); + const trace = await initForensicTrace({ get, originTxid: txid, originVout: vout, amountStolen }); + traceRef.current = trace; + setTick(t => t+1); + await avanzarSalto(trace); // "iniciar" ya explora el primer salto — un solo clic para ver algo } catch(e) { setError(e.message); + setBusy(false); } - setScanning(false); setProgress(""); }; - const seguirMasSaltos = () => { - const next = maxHops + 4; - setMaxHops(next); - iniciar(next); + const seguir = () => { + if (traceRef.current && !traceRef.current.done && !busy) avanzarSalto(traceRef.current); }; - const limpiar = () => { setGraph(null); setReport(null); setError(null); }; + const limpiar = () => { + traceRef.current = null; setTick(t=>t+1); + setReport(null); setError(null); setBusy(false); setProgress(""); + }; + + const generarInforme = () => { + if (!traceRef.current) return; + const g = finalizeForensicGraph(traceRef.current); + setReport(buildForensicReport(g, declaracion)); + }; + + const trace = traceRef.current; // solo lectura, para el render — tick fuerza refrescarlo const exportJSON = () => { const exportData = { @@ -5095,44 +5116,91 @@
- Rastrea fondos robados o perdidos hacia adelante, salto a salto, desde la transacción de origen hasta un punto de parada natural (custodio identificado, dilución, CoinJoin, o fondos aún sin gastar). Todo el rastreo corre sobre tu propio nodo — nada sale de tu red. + Rastrea fondos robados o perdidos hacia adelante, salto a salto, desde la transacción de origen hasta un punto de parada natural (custodio identificado, dilución, CoinJoin, o fondos aún sin gastar). A demanda: tú decides cuándo se explora el siguiente salto — nada corre sin que lo pidas, igual que el rastro de procedencia hacia atrás. Todo sobre tu propio nodo — nada sale de tu red.
- setTxidInput(e.target.value)} + setTxidInput(e.target.value)} disabled={!!trace} placeholder="txid de origen (64 caracteres hex)" - style={{flex:3,background:C.bg,border:`1px solid ${C.borderBright}`,borderRadius:6,padding:"10px 12px",color:C.t1,fontFamily:"monospace",fontSize:"0.72rem",outline:"none"}} + style={{flex:3,background:C.bg,border:`1px solid ${C.borderBright}`,borderRadius:6,padding:"10px 12px",color:C.t1,fontFamily:"monospace",fontSize:"0.72rem",outline:"none",opacity:trace?0.5:1}} /> - setVoutInput(e.target.value)} type="number" min="0" + setVoutInput(e.target.value)} type="number" min="0" disabled={!!trace} placeholder="vout" title="Índice del output robado dentro de esa transacción" - style={{flex:1,background:C.bg,border:`1px solid ${C.borderBright}`,borderRadius:6,padding:"10px 12px",color:C.t1,fontFamily:"monospace",fontSize:"0.72rem",outline:"none"}} + style={{flex:1,background:C.bg,border:`1px solid ${C.borderBright}`,borderRadius:6,padding:"10px 12px",color:C.t1,fontFamily:"monospace",fontSize:"0.72rem",outline:"none",opacity:trace?0.5:1}} /> +
- setAmountInput(e.target.value)} type="number" step="0.00000001" min="0" + setAmountInput(e.target.value)} type="number" step="0.00000001" min="0" disabled={!!trace} placeholder="Importe estimado robado en BTC (opcional — activa la detección de dilución)" - style={{background:C.bg,border:`1px solid ${C.borderBright}`,borderRadius:6,padding:"10px 12px",color:C.t1,fontFamily:"monospace",fontSize:"0.72rem",outline:"none"}} + style={{background:C.bg,border:`1px solid ${C.borderBright}`,borderRadius:6,padding:"10px 12px",color:C.t1,fontFamily:"monospace",fontSize:"0.72rem",outline:"none",opacity:trace?0.5:1}} />