feat(web): add Re-run button to detail view with pre-filled wizard

Adds a "Re-run" button to the run detail view that opens the wizard
pre-populated with the previous run's settings (target, model, votes,
chain depth, recon, max agents, mode flags). Server now includes
launch params in the detail API response by looking up the persisted
job file. Previous findings and reports are preserved (new run ID).
This commit is contained in:
CyberSecurityUP committed 2026-10-18 18:43:02 -03:00
1 parent 78e7d27e2f
commit 33fd6ca508
3 files changed
+50 -1

No files matched your search

+36
View File
@@ -1148,6 +1148,41 @@ $('#btnDeleteRun').addEventListener('click', async () => {
}
});
$('#btnRerun')?.addEventListener('click', () => {
clearInterval(state.detailPoll);
const target = $('#detailTarget').textContent || '';
const sub = $('#detailTargetSub').textContent || '';
const launch = state.detailLaunch;
switchView('wizard');
goToStep(0);
if (launch) {
if (launch.target) $('#fieldTarget').value = launch.target;
if (launch.name) $('#fieldName').value = launch.name + ' (re-run)';
if (launch.repo) $('#fieldRepo').value = launch.repo;
if (launch.mode) { const sel = $('#fieldMode'); if (sel) sel.value = launch.mode; }
if (launch.votes) $('#fieldVotes').value = launch.votes;
if (launch.chainDepth !== undefined) $('#fieldChain').value = launch.chainDepth;
if (launch.recon) $('#fieldRecon').value = launch.recon;
if (launch.maxAgents) $('#fieldMaxAgents').value = launch.maxAgents;
if (launch.quick && $('#fieldQuick')) $('#fieldQuick').checked = true;
if (launch.research && $('#fieldResearch')) $('#fieldResearch').checked = true;
if (launch.subscription) state.authMode = 'subscription';
if (launch.mcp && $('#fieldMcp')) $('#fieldMcp').checked = launch.mcp;
if (launch.models?.length) {
const parts = launch.models[0].split(':');
if (parts.length >= 2) {
$('#fieldProvider').value = parts[0];
$('#fieldProvider').dispatchEvent(new Event('change'));
setTimeout(() => { $('#fieldModelSelect').value = parts.slice(1).join(':'); }, 100);
}
}
} else {
$('#fieldTarget').value = sub || target;
$('#fieldName').value = (target || '') + ' (re-run)';
}
toast('Settings loaded from previous run. Adjust and launch.', 'ok', 4000);
});
$('#btnDetailBack').addEventListener('click', () => { clearInterval(state.detailPoll); switchView('wizard'); });
$('#btnNewEngagement')?.addEventListener('click', () => { leaveLiveJob(); clearInterval(state.detailPoll); switchView('wizard'); });
if ($('#btnNewPentest2')) $('#btnNewPentest2').addEventListener('click', () => { leaveLiveJob(); clearInterval(state.detailPoll); switchView('wizard'); });
@@ -1758,6 +1793,7 @@ async function loadDetail(id) {
$('#detailState').textContent = detail.status?.state || 'unknown';
state.detailFindings = detail.findings;
state.detailPocs = detail.pocs || [];
state.detailLaunch = detail.launch || null;
// Regression vs the previous run of this target — new / recurring badges and a
// "fixed since" banner. Best-effort: it never blocks the findings table.
state.detailRegression = null;
+1
View File
@@ -741,6 +741,7 @@
<button class="btn" id="btnBuildReport" title="Regenerate this run's report from its findings">Generate report</button>
<button class="btn" id="btnGraphRecompute" title="Recompute the attack graph with the CLI (neurosploit graph)">↻ Graph</button>
<button class="btn" id="btnRetest" title="Re-run every finding's recorded proof (neurosploit poc) and see what still reproduces">⟳ Re-test</button>
<button class="btn btn-accent" id="btnRerun" title="Launch a new run against this target with the same (or tweaked) settings">▶ Re-run</button>
<a class="btn" id="detailOpenAudit" target="_blank" hidden title="Every action this run took, hash-chained">Audit trail</a>
<button class="btn btn-danger" id="btnDeleteRun" title="Permanently delete this session and its report">🗑 Delete</button>
<button class="btn" id="btnDetailBack">← Back</button>
+13 -1
View File
@@ -791,7 +791,19 @@ async function runDetail(id) {
const liveRunIds = new Set([...jobs.values()].filter((j) => !j.done && j.runId).map((j) => j.runId));
if (!liveRunIds.has(id)) status.state = 'interrupted';
}
return { id, name: engagementNames.get(id) || '', meta, status, findings, assets, pocs, extras };
let launch = null;
const liveJob = [...jobs.values()].find((j) => j.runId === id);
if (liveJob?.launch) launch = liveJob.launch;
else {
try {
const jfiles = await fsp.readdir(JOBS_DIR);
for (const jf of jfiles) {
const rec = JSON.parse(await fsp.readFile(path.join(JOBS_DIR, jf), 'utf8'));
if (rec.runId === id && rec.launch) { launch = rec.launch; break; }
}
} catch { /* no job files */ }
}
return { id, name: engagementNames.get(id) || '', meta, status, findings, assets, pocs, extras, launch };
}
/// Relative file list under a directory (one level of nesting is enough for