From 27f23cc967e2e81e835923f6e2ffae6dc5da2716 Mon Sep 17 00:00:00 2001 From: pikaro Date: Thu, 16 Jul 2026 14:24:29 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20rastreo=20forense=20a=20demanda=20?= =?UTF-8?q?=E2=80=94=20salto=20a=20salto,=20bot=C3=B3n=20"seguir=20el=20ra?= =?UTF-8?q?stro"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PeritajeForense pasa de "un clic, rastreo automático completo" a "un clic por salto": el usuario ve el progreso (salto actual, tx exploradas, ramas pendientes, ramas terminales) y decide cuándo avanzar, igual que ya funciona el rastro de procedencia hacia atrás (RastroProcedencia). El rastreo se guarda en un ref mutable (traceRef) entre clics — initForensicTrace + advanceForensicHop del commit anterior encajan directamente, sin cambios. "Iniciar rastreo forense" ahora también ejecuta el primer salto (un clic para ver algo). "Generar informe" está disponible desde el primer salto, no solo al terminar, y puede volver a pulsarse en cualquier momento — incluida la declaración del afectado, editable mientras el rastreo sigue en marcha. Los campos txid/vout/importe se deshabilitan una vez iniciado (no tienen efecto a mitad de rastreo); el tope de saltos y la declaración siguen editables. Quita el mecanismo antiguo "seguir más saltos (+4)": ya no tiene sentido con control manual salto a salto, y el texto de ramas truncadas por el tope se reescribe para no prometer una reanudación que el motor no soporta (una rama ya truncada no se puede retomar suelta). Verificado en navegador con un servidor HTTP local que sirve la cadena de peeling de 3 saltos ya usada en pruebas anteriores, pulsando "seguir el rastro" cuatro veces manualmente: el progreso avanza correctamente salto a salto (1→2→3→4), termina con "rastro completo", y el informe generado es idéntico en contenido al que produce el modo automático (misma cadena de peeling CERTEZA, mismas direcciones atribuidas, mismos fondos sin gastar). Export JSON/MD y "Limpiar" verificados sin errores de consola. --- dashboard.html | 152 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 108 insertions(+), 44 deletions(-) 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}} />