|
|
@@ -7105,12 +7105,104 @@ function setPortPreset(port) {
|
|
|
if (input) input.value = port;
|
|
|
}
|
|
|
|
|
|
+let pingAbortController = null;
|
|
|
+let tracerouteAbortController = null;
|
|
|
+
|
|
|
+function clearPingTable() {
|
|
|
+ const tbody = document.getElementById('tools-ping-tbody');
|
|
|
+ if (tbody) tbody.innerHTML = '';
|
|
|
+ const rawEl = document.getElementById('tools-ping-raw');
|
|
|
+ if (rawEl) rawEl.innerText = '';
|
|
|
+ const sentEl = document.getElementById('tools-ping-stat-sent');
|
|
|
+ if (sentEl) sentEl.innerText = '0';
|
|
|
+ const recvEl = document.getElementById('tools-ping-stat-recv');
|
|
|
+ if (recvEl) recvEl.innerText = '0';
|
|
|
+ const lossEl = document.getElementById('tools-ping-stat-loss');
|
|
|
+ if (lossEl) lossEl.innerText = '0%';
|
|
|
+ const avgEl = document.getElementById('tools-ping-stat-avg');
|
|
|
+ if (avgEl) avgEl.innerText = '0 ms';
|
|
|
+ const minMaxEl = document.getElementById('tools-ping-stat-minmax');
|
|
|
+ if (minMaxEl) minMaxEl.innerText = '-';
|
|
|
+ const badge = document.getElementById('tools-ping-status-badge');
|
|
|
+ if (badge) {
|
|
|
+ badge.className = '';
|
|
|
+ badge.innerText = '';
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function clearTracerouteTable() {
|
|
|
+ const tbody = document.getElementById('tools-tr-tbody');
|
|
|
+ if (tbody) tbody.innerHTML = '';
|
|
|
+ const rawEl = document.getElementById('tools-tr-raw');
|
|
|
+ if (rawEl) rawEl.innerText = '';
|
|
|
+ const statHops = document.getElementById('tools-tr-hops-count');
|
|
|
+ if (statHops) statHops.innerText = '0 Hops';
|
|
|
+}
|
|
|
+
|
|
|
+function resetPingButton() {
|
|
|
+ const btn = document.getElementById('tools-ping-btn');
|
|
|
+ const btnText = document.getElementById('tools-ping-btn-text');
|
|
|
+ const spinner = document.getElementById('tools-ping-spinner');
|
|
|
+ if (btn) {
|
|
|
+ btn.disabled = false;
|
|
|
+ btn.className = 'px-5 py-2 rounded-lg font-semibold text-xs bg-amber-500 hover:bg-amber-400 text-slate-950 shadow-lg shadow-amber-500/20 transition flex items-center gap-2';
|
|
|
+ }
|
|
|
+ if (btnText) btnText.innerText = 'Start Ping';
|
|
|
+ if (spinner) spinner.classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function setPingRunningButton() {
|
|
|
+ const btn = document.getElementById('tools-ping-btn');
|
|
|
+ const btnText = document.getElementById('tools-ping-btn-text');
|
|
|
+ const spinner = document.getElementById('tools-ping-spinner');
|
|
|
+ if (btn) {
|
|
|
+ btn.disabled = false;
|
|
|
+ btn.className = 'px-5 py-2 rounded-lg font-semibold text-xs bg-rose-600 hover:bg-rose-500 text-white shadow-lg shadow-rose-600/30 transition flex items-center gap-2';
|
|
|
+ }
|
|
|
+ if (btnText) btnText.innerText = 'Stop Ping';
|
|
|
+ if (spinner) spinner.classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function resetTracerouteButton() {
|
|
|
+ const btn = document.getElementById('tools-tr-btn');
|
|
|
+ const btnText = document.getElementById('tools-tr-btn-text');
|
|
|
+ const spinner = document.getElementById('tools-tr-spinner');
|
|
|
+ if (btn) {
|
|
|
+ btn.disabled = false;
|
|
|
+ btn.className = 'px-5 py-2 rounded-lg font-semibold text-xs bg-amber-500 hover:bg-amber-400 text-slate-950 shadow-lg shadow-amber-500/20 transition flex items-center gap-2';
|
|
|
+ }
|
|
|
+ if (btnText) btnText.innerText = 'Start Traceroute';
|
|
|
+ if (spinner) spinner.classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function setTracerouteRunningButton() {
|
|
|
+ const btn = document.getElementById('tools-tr-btn');
|
|
|
+ const btnText = document.getElementById('tools-tr-btn-text');
|
|
|
+ const spinner = document.getElementById('tools-tr-spinner');
|
|
|
+ if (btn) {
|
|
|
+ btn.disabled = false;
|
|
|
+ btn.className = 'px-5 py-2 rounded-lg font-semibold text-xs bg-rose-600 hover:bg-rose-500 text-white shadow-lg shadow-rose-600/30 transition flex items-center gap-2';
|
|
|
+ }
|
|
|
+ if (btnText) btnText.innerText = 'Stop Traceroute';
|
|
|
+ if (spinner) spinner.classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
async function executePing(e) {
|
|
|
if (e) e.preventDefault();
|
|
|
+
|
|
|
+ // If already running, clicking the button stops the stream!
|
|
|
+ if (pingAbortController) {
|
|
|
+ pingAbortController.abort();
|
|
|
+ pingAbortController = null;
|
|
|
+ resetPingButton();
|
|
|
+ showToast('Ping Stopped', 'Live ping stopped by user', 'info');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
const target = document.getElementById('tools-ping-target')?.value?.trim();
|
|
|
const count = parseInt(document.getElementById('tools-ping-count')?.value || '4', 10);
|
|
|
const size = parseInt(document.getElementById('tools-ping-size')?.value || '56', 10);
|
|
|
- const timeout = parseInt(document.getElementById('tools-ping-timeout')?.value || '5', 10);
|
|
|
+ const timeout = parseInt(document.getElementById('tools-ping-timeout')?.value || '3', 10);
|
|
|
const iface = document.getElementById('tools-ping-iface')?.value || '';
|
|
|
const dontFrag = document.getElementById('tools-ping-df')?.checked || false;
|
|
|
|
|
|
@@ -7119,13 +7211,15 @@ async function executePing(e) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- const btn = document.getElementById('tools-ping-btn');
|
|
|
- const spinner = document.getElementById('tools-ping-spinner');
|
|
|
- if (btn) btn.disabled = true;
|
|
|
- if (spinner) spinner.classList.remove('hidden');
|
|
|
+ clearPingTable();
|
|
|
+ const resCard = document.getElementById('tools-ping-results');
|
|
|
+ if (resCard) resCard.classList.remove('hidden');
|
|
|
+
|
|
|
+ pingAbortController = new AbortController();
|
|
|
+ setPingRunningButton();
|
|
|
|
|
|
try {
|
|
|
- const res = await fetch(`${API_BASE}/tools/ping`, {
|
|
|
+ const res = await fetch(`${API_BASE}/tools/ping/stream`, {
|
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
body: JSON.stringify({
|
|
|
@@ -7135,91 +7229,177 @@ async function executePing(e) {
|
|
|
timeout,
|
|
|
interface: iface,
|
|
|
dont_frag: dontFrag
|
|
|
- })
|
|
|
+ }),
|
|
|
+ signal: pingAbortController.signal
|
|
|
});
|
|
|
|
|
|
- const data = await res.json();
|
|
|
if (!res.ok) {
|
|
|
- showToast('Ping Error', data.error || 'Failed to execute ping', 'error');
|
|
|
+ const errText = await res.text();
|
|
|
+ showToast('Ping Error', errText || 'Failed to start live ping', 'error');
|
|
|
+ resetPingButton();
|
|
|
+ pingAbortController = null;
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- const resCard = document.getElementById('tools-ping-results');
|
|
|
- if (resCard) resCard.classList.remove('hidden');
|
|
|
+ const reader = res.body.getReader();
|
|
|
+ const decoder = new TextDecoder('utf-8');
|
|
|
+ let buffer = '';
|
|
|
|
|
|
- const sentEl = document.getElementById('tools-ping-stat-sent');
|
|
|
- if (sentEl) sentEl.innerText = data.packets_sent ?? 0;
|
|
|
+ while (true) {
|
|
|
+ const { done, value } = await reader.read();
|
|
|
+ if (done) break;
|
|
|
|
|
|
- const recvEl = document.getElementById('tools-ping-stat-recv');
|
|
|
- if (recvEl) recvEl.innerText = data.packets_recv ?? 0;
|
|
|
+ buffer += decoder.decode(value, { stream: true });
|
|
|
+ const blocks = buffer.split('\n\n');
|
|
|
+ buffer = blocks.pop() || '';
|
|
|
|
|
|
- const lossEl = document.getElementById('tools-ping-stat-loss');
|
|
|
- if (lossEl) {
|
|
|
- const loss = data.packet_loss_pct ?? 0;
|
|
|
- lossEl.innerText = `${loss.toFixed(0)}%`;
|
|
|
- lossEl.className = loss === 0
|
|
|
- ? 'text-base font-bold font-mono text-emerald-400 mt-1'
|
|
|
- : (loss < 50 ? 'text-base font-bold font-mono text-amber-400 mt-1' : 'text-base font-bold font-mono text-rose-400 mt-1');
|
|
|
+ for (const block of blocks) {
|
|
|
+ const trimmed = block.trim();
|
|
|
+ if (!trimmed) continue;
|
|
|
+ const line = trimmed.startsWith('data: ') ? trimmed.slice(6) : trimmed;
|
|
|
+ try {
|
|
|
+ const evt = JSON.parse(line);
|
|
|
+ handleLivePingEvent(evt);
|
|
|
+ } catch (parseErr) {
|
|
|
+ console.warn('Ping SSE parse error:', parseErr, line);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
+ } catch (err) {
|
|
|
+ if (err.name === 'AbortError') {
|
|
|
+ // User aborted normally
|
|
|
+ } else {
|
|
|
+ showToast('Ping Error', err.message, 'error');
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ pingAbortController = null;
|
|
|
+ resetPingButton();
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
- const avgEl = document.getElementById('tools-ping-stat-avg');
|
|
|
- if (avgEl) avgEl.innerText = data.avg_rtt_ms ? `${data.avg_rtt_ms.toFixed(2)} ms` : '--';
|
|
|
+function handleLivePingEvent(evt) {
|
|
|
+ const sentEl = document.getElementById('tools-ping-stat-sent');
|
|
|
+ const recvEl = document.getElementById('tools-ping-stat-recv');
|
|
|
+ const lossEl = document.getElementById('tools-ping-stat-loss');
|
|
|
+ const avgEl = document.getElementById('tools-ping-stat-avg');
|
|
|
+ const minMaxEl = document.getElementById('tools-ping-stat-minmax');
|
|
|
+ const badge = document.getElementById('tools-ping-status-badge');
|
|
|
+ const tbody = document.getElementById('tools-ping-tbody');
|
|
|
+ const rawEl = document.getElementById('tools-ping-raw');
|
|
|
|
|
|
- const minMaxEl = document.getElementById('tools-ping-stat-minmax');
|
|
|
- if (minMaxEl) {
|
|
|
- if (data.min_rtt_ms !== undefined && data.max_rtt_ms !== undefined && data.min_rtt_ms > 0) {
|
|
|
- minMaxEl.innerText = `${data.min_rtt_ms.toFixed(1)} / ${data.max_rtt_ms.toFixed(1)} ms`;
|
|
|
- } else {
|
|
|
- minMaxEl.innerText = '--';
|
|
|
- }
|
|
|
- }
|
|
|
+ if (evt.raw && rawEl) {
|
|
|
+ rawEl.innerText += (rawEl.innerText ? '\n' : '') + evt.raw;
|
|
|
+ rawEl.scrollTop = rawEl.scrollHeight;
|
|
|
+ }
|
|
|
|
|
|
- const badge = document.getElementById('tools-ping-status-badge');
|
|
|
+ if (evt.type === 'reply') {
|
|
|
+ if (sentEl && evt.packets_sent !== undefined) sentEl.innerText = evt.packets_sent;
|
|
|
+ if (recvEl && evt.packets_recv !== undefined) recvEl.innerText = evt.packets_recv;
|
|
|
+ if (lossEl && evt.packet_loss_pct !== undefined) {
|
|
|
+ lossEl.innerText = `${evt.packet_loss_pct.toFixed(0)}%`;
|
|
|
+ lossEl.className = evt.packet_loss_pct === 0
|
|
|
+ ? 'text-base font-bold font-mono text-emerald-400 mt-1'
|
|
|
+ : (evt.packet_loss_pct < 50 ? 'text-base font-bold font-mono text-amber-400 mt-1' : 'text-base font-bold font-mono text-rose-400 mt-1');
|
|
|
+ }
|
|
|
+ if (avgEl && evt.avg_rtt_ms !== undefined) avgEl.innerText = `${evt.avg_rtt_ms.toFixed(2)} ms`;
|
|
|
+ if (minMaxEl && evt.min_rtt_ms !== undefined && evt.max_rtt_ms !== undefined) {
|
|
|
+ minMaxEl.innerText = `${evt.min_rtt_ms.toFixed(1)} / ${evt.max_rtt_ms.toFixed(1)} ms`;
|
|
|
+ }
|
|
|
if (badge) {
|
|
|
- if (data.packet_loss_pct === 0) {
|
|
|
+ if (evt.packet_loss_pct === 0) {
|
|
|
badge.className = 'px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-emerald-950 text-emerald-300 border border-emerald-800/60';
|
|
|
badge.innerText = '100% OK';
|
|
|
- } else if (data.packets_recv > 0) {
|
|
|
- badge.className = 'px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-amber-950 text-amber-300 border border-amber-800/60';
|
|
|
- badge.innerText = `${data.packet_loss_pct}% LOSS`;
|
|
|
} else {
|
|
|
- badge.className = 'px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-rose-950 text-rose-300 border border-rose-800/60';
|
|
|
- badge.innerText = 'UNREACHABLE';
|
|
|
+ badge.className = 'px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-amber-950 text-amber-300 border border-amber-800/60';
|
|
|
+ badge.innerText = `${evt.packet_loss_pct.toFixed(0)}% LOSS`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- const tbody = document.getElementById('tools-ping-tbody') || document.getElementById('tools-ping-table-body');
|
|
|
if (tbody) {
|
|
|
- if (Array.isArray(data.replies) && data.replies.length > 0) {
|
|
|
- tbody.innerHTML = data.replies.map(r => `
|
|
|
- <tr class="hover:bg-slate-800/40 border-b border-slate-800/60 font-mono text-xs">
|
|
|
- <td class="py-2 px-3 text-slate-300">#${r.seq}</td>
|
|
|
- <td class="py-2 px-3 font-semibold text-white">${escapeHtml(r.from)}</td>
|
|
|
- <td class="py-2 px-3 text-slate-400">${r.bytes} bytes</td>
|
|
|
- <td class="py-2 px-3 text-slate-300">${r.ttl}</td>
|
|
|
- <td class="py-2 px-3 font-bold text-amber-300">${r.time_ms.toFixed(2)} ms</td>
|
|
|
- <td class="py-2 px-3 text-right"><span class="px-2 py-0.5 rounded text-[10px] font-bold bg-emerald-950 text-emerald-300 border border-emerald-800/60">REPLY</span></td>
|
|
|
- </tr>
|
|
|
- `).join('');
|
|
|
- } else {
|
|
|
- tbody.innerHTML = `<tr><td colspan="6" class="p-4 text-center text-xs text-rose-400 font-mono">No replies received (100% packet loss or timed out)</td></tr>`;
|
|
|
- }
|
|
|
+ const tr = document.createElement('tr');
|
|
|
+ tr.className = 'hover:bg-slate-800/40 border-b border-slate-800/60 font-mono text-xs';
|
|
|
+ tr.innerHTML = `
|
|
|
+ <td class="py-2 px-3 text-slate-300">#${evt.seq}</td>
|
|
|
+ <td class="py-2 px-3 font-semibold text-white">${escapeHtml(evt.from)}</td>
|
|
|
+ <td class="py-2 px-3 text-slate-400">${evt.bytes} bytes</td>
|
|
|
+ <td class="py-2 px-3 text-slate-300">${evt.ttl}</td>
|
|
|
+ <td class="py-2 px-3 font-bold text-amber-300">${evt.time_ms.toFixed(2)} ms</td>
|
|
|
+ <td class="py-2 px-3 text-right"><span class="px-2 py-0.5 rounded text-[10px] font-bold bg-emerald-950 text-emerald-300 border border-emerald-800/60">REPLY</span></td>
|
|
|
+ `;
|
|
|
+ tbody.appendChild(tr);
|
|
|
+ }
|
|
|
+ } else if (evt.type === 'timeout') {
|
|
|
+ if (sentEl && evt.packets_sent !== undefined) sentEl.innerText = evt.packets_sent;
|
|
|
+ if (lossEl && evt.packet_loss_pct !== undefined) {
|
|
|
+ lossEl.innerText = `${evt.packet_loss_pct.toFixed(0)}%`;
|
|
|
+ lossEl.className = evt.packet_loss_pct < 50 ? 'text-base font-bold font-mono text-amber-400 mt-1' : 'text-base font-bold font-mono text-rose-400 mt-1';
|
|
|
+ }
|
|
|
+ if (badge) {
|
|
|
+ badge.className = 'px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-rose-950 text-rose-300 border border-rose-800/60';
|
|
|
+ badge.innerText = `${evt.packet_loss_pct?.toFixed(0) || 100}% LOSS`;
|
|
|
}
|
|
|
|
|
|
- const rawEl = document.getElementById('tools-ping-raw');
|
|
|
- if (rawEl) rawEl.innerText = data.raw_output || 'No output';
|
|
|
+ if (tbody) {
|
|
|
+ const tr = document.createElement('tr');
|
|
|
+ tr.className = 'hover:bg-slate-800/40 border-b border-slate-800/60 font-mono text-xs opacity-75';
|
|
|
+ tr.innerHTML = `
|
|
|
+ <td class="py-2 px-3 text-slate-400">#${evt.seq}</td>
|
|
|
+ <td class="py-2 px-3 font-semibold text-slate-500 italic">Request timed out</td>
|
|
|
+ <td class="py-2 px-3 text-slate-600">--</td>
|
|
|
+ <td class="py-2 px-3 text-slate-600">--</td>
|
|
|
+ <td class="py-2 px-3 font-bold text-slate-500">* * *</td>
|
|
|
+ <td class="py-2 px-3 text-right"><span class="px-2 py-0.5 rounded text-[10px] font-bold bg-slate-800 text-slate-400 border border-slate-700">TIMEOUT</span></td>
|
|
|
+ `;
|
|
|
+ tbody.appendChild(tr);
|
|
|
+ }
|
|
|
+ } else if (evt.type === 'unreach') {
|
|
|
+ if (sentEl && evt.packets_sent !== undefined) sentEl.innerText = evt.packets_sent;
|
|
|
+ if (lossEl && evt.packet_loss_pct !== undefined) {
|
|
|
+ lossEl.innerText = `${evt.packet_loss_pct.toFixed(0)}%`;
|
|
|
+ lossEl.className = 'text-base font-bold font-mono text-rose-400 mt-1';
|
|
|
+ }
|
|
|
+ if (badge) {
|
|
|
+ badge.className = 'px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-rose-950 text-rose-300 border border-rose-800/60';
|
|
|
+ badge.innerText = 'UNREACHABLE';
|
|
|
+ }
|
|
|
|
|
|
- showToast('Ping Finished', `Sent ${data.packets_sent}, received ${data.packets_recv} (${data.packet_loss_pct}% loss)`, data.packet_loss_pct === 0 ? 'success' : 'info');
|
|
|
- } catch (err) {
|
|
|
- showToast('Ping Error', err.message, 'error');
|
|
|
- } finally {
|
|
|
- if (btn) btn.disabled = false;
|
|
|
- if (spinner) spinner.classList.add('hidden');
|
|
|
+ if (tbody) {
|
|
|
+ const tr = document.createElement('tr');
|
|
|
+ tr.className = 'hover:bg-slate-800/40 border-b border-slate-800/60 font-mono text-xs';
|
|
|
+ tr.innerHTML = `
|
|
|
+ <td class="py-2 px-3 text-slate-400">#${evt.seq}</td>
|
|
|
+ <td class="py-2 px-3 font-semibold text-rose-400">${escapeHtml(evt.from || 'Gateway')}</td>
|
|
|
+ <td class="py-2 px-3 text-slate-600">--</td>
|
|
|
+ <td class="py-2 px-3 text-slate-600">--</td>
|
|
|
+ <td class="py-2 px-3 font-bold text-rose-400 text-xs">${escapeHtml(evt.message || 'Unreachable')}</td>
|
|
|
+ <td class="py-2 px-3 text-right"><span class="px-2 py-0.5 rounded text-[10px] font-bold bg-rose-950 text-rose-300 border border-rose-800/60">UNREACH</span></td>
|
|
|
+ `;
|
|
|
+ tbody.appendChild(tr);
|
|
|
+ }
|
|
|
+ } else if (evt.type === 'stats') {
|
|
|
+ if (sentEl && evt.packets_sent !== undefined) sentEl.innerText = evt.packets_sent;
|
|
|
+ if (recvEl && evt.packets_recv !== undefined) recvEl.innerText = evt.packets_recv;
|
|
|
+ if (lossEl && evt.packet_loss_pct !== undefined) lossEl.innerText = `${evt.packet_loss_pct.toFixed(0)}%`;
|
|
|
+ if (avgEl && evt.avg_rtt_ms !== undefined) avgEl.innerText = `${evt.avg_rtt_ms.toFixed(2)} ms`;
|
|
|
+ if (minMaxEl && evt.min_rtt_ms !== undefined && evt.max_rtt_ms !== undefined) {
|
|
|
+ minMaxEl.innerText = `${evt.min_rtt_ms.toFixed(1)} / ${evt.max_rtt_ms.toFixed(1)} ms`;
|
|
|
+ }
|
|
|
+ } else if (evt.type === 'done') {
|
|
|
+ showToast('Ping Finished', `Sent ${evt.packets_sent}, received ${evt.packets_recv} (${evt.packet_loss_pct?.toFixed(0)}% loss)`, evt.packet_loss_pct === 0 ? 'success' : 'info');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function executeTraceroute(e) {
|
|
|
if (e) e.preventDefault();
|
|
|
+
|
|
|
+ if (tracerouteAbortController) {
|
|
|
+ tracerouteAbortController.abort();
|
|
|
+ tracerouteAbortController = null;
|
|
|
+ resetTracerouteButton();
|
|
|
+ showToast('Traceroute Stopped', 'Traceroute stopped by user', 'info');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
const target = document.getElementById('tools-tr-target')?.value?.trim();
|
|
|
const maxHops = parseInt(document.getElementById('tools-tr-maxhops')?.value || '15', 10);
|
|
|
const protocol = document.getElementById('tools-tr-proto')?.value || 'icmp';
|
|
|
@@ -7231,13 +7411,15 @@ async function executeTraceroute(e) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- const btn = document.getElementById('tools-tr-btn');
|
|
|
- const spinner = document.getElementById('tools-tr-spinner');
|
|
|
- if (btn) btn.disabled = true;
|
|
|
- if (spinner) spinner.classList.remove('hidden');
|
|
|
+ clearTracerouteTable();
|
|
|
+ const resCard = document.getElementById('tools-tr-results');
|
|
|
+ if (resCard) resCard.classList.remove('hidden');
|
|
|
+
|
|
|
+ tracerouteAbortController = new AbortController();
|
|
|
+ setTracerouteRunningButton();
|
|
|
|
|
|
try {
|
|
|
- const res = await fetch(`${API_BASE}/tools/traceroute`, {
|
|
|
+ const res = await fetch(`${API_BASE}/tools/traceroute/stream`, {
|
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
body: JSON.stringify({
|
|
|
@@ -7246,59 +7428,91 @@ async function executeTraceroute(e) {
|
|
|
protocol,
|
|
|
interface: iface,
|
|
|
resolve
|
|
|
- })
|
|
|
+ }),
|
|
|
+ signal: tracerouteAbortController.signal
|
|
|
});
|
|
|
|
|
|
- const data = await res.json();
|
|
|
if (!res.ok) {
|
|
|
- showToast('Traceroute Error', data.error || 'Failed to execute traceroute', 'error');
|
|
|
+ const errText = await res.text();
|
|
|
+ showToast('Traceroute Error', errText || 'Failed to start traceroute', 'error');
|
|
|
+ resetTracerouteButton();
|
|
|
+ tracerouteAbortController = null;
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- const resCard = document.getElementById('tools-tr-results');
|
|
|
- if (resCard) resCard.classList.remove('hidden');
|
|
|
+ const reader = res.body.getReader();
|
|
|
+ const decoder = new TextDecoder('utf-8');
|
|
|
+ let buffer = '';
|
|
|
|
|
|
- const hopsCount = Array.isArray(data.hops) ? data.hops.length : 0;
|
|
|
- const statHops = document.getElementById('tools-tr-hops-count');
|
|
|
- if (statHops) statHops.innerText = `${hopsCount} Hops`;
|
|
|
+ while (true) {
|
|
|
+ const { done, value } = await reader.read();
|
|
|
+ if (done) break;
|
|
|
|
|
|
- const tbody = document.getElementById('tools-tr-tbody');
|
|
|
- if (tbody) {
|
|
|
- if (hopsCount > 0) {
|
|
|
- tbody.innerHTML = data.hops.map(h => {
|
|
|
- const isTimeout = h.status === 'timeout' || (!h.ip && !h.host);
|
|
|
- const rtt1 = Array.isArray(h.times_ms) && h.times_ms[0] ? `${h.times_ms[0]} ms` : (isTimeout ? '*' : '--');
|
|
|
- const rtt2 = Array.isArray(h.times_ms) && h.times_ms[1] ? `${h.times_ms[1]} ms` : (isTimeout ? '*' : '--');
|
|
|
- const displayHost = h.host && h.host !== h.ip ? `${escapeHtml(h.host)} (${escapeHtml(h.ip || '')})` : escapeHtml(h.ip || (isTimeout ? 'Request timed out' : '--'));
|
|
|
+ buffer += decoder.decode(value, { stream: true });
|
|
|
+ const blocks = buffer.split('\n\n');
|
|
|
+ buffer = blocks.pop() || '';
|
|
|
|
|
|
- return `
|
|
|
- <tr class="hover:bg-slate-800/40 border-b border-slate-800/60 font-mono text-xs ${isTimeout ? 'opacity-60' : ''}">
|
|
|
- <td class="py-2.5 px-3 text-slate-300 font-bold">${h.hop}</td>
|
|
|
- <td class="py-2.5 px-3 font-semibold ${isTimeout ? 'text-slate-500 italic' : 'text-cyan-300'}">${displayHost}</td>
|
|
|
- <td class="py-2.5 px-3 font-bold ${isTimeout ? 'text-slate-500' : 'text-amber-300'}">${escapeHtml(rtt1)}</td>
|
|
|
- <td class="py-2.5 px-3 font-bold ${isTimeout ? 'text-slate-500' : 'text-amber-300'}">${escapeHtml(rtt2)}</td>
|
|
|
- <td class="py-2.5 px-3 text-right">
|
|
|
- ${isTimeout
|
|
|
- ? '<span class="px-2 py-0.5 rounded text-[10px] font-bold bg-slate-800 text-slate-400 border border-slate-700">TIMEOUT</span>'
|
|
|
- : '<span class="px-2 py-0.5 rounded text-[10px] font-bold bg-emerald-950 text-emerald-300 border border-emerald-800/60">OK</span>'}
|
|
|
- </td>
|
|
|
- </tr>
|
|
|
- `;
|
|
|
- }).join('');
|
|
|
- } else {
|
|
|
- tbody.innerHTML = `<tr><td colspan="5" class="p-4 text-center text-xs text-rose-400 font-mono">No route or hops discovered</td></tr>`;
|
|
|
+ for (const block of blocks) {
|
|
|
+ const trimmed = block.trim();
|
|
|
+ if (!trimmed) continue;
|
|
|
+ const line = trimmed.startsWith('data: ') ? trimmed.slice(6) : trimmed;
|
|
|
+ try {
|
|
|
+ const evt = JSON.parse(line);
|
|
|
+ handleLiveTracerouteEvent(evt);
|
|
|
+ } catch (parseErr) {
|
|
|
+ console.warn('Traceroute SSE parse error:', parseErr, line);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- const rawEl = document.getElementById('tools-tr-raw');
|
|
|
- if (rawEl) rawEl.innerText = data.raw_output || 'No output';
|
|
|
-
|
|
|
- showToast('Traceroute Finished', `Traced ${hopsCount} hops to ${target}`, 'info');
|
|
|
} catch (err) {
|
|
|
- showToast('Traceroute Error', err.message, 'error');
|
|
|
+ if (err.name === 'AbortError') {
|
|
|
+ // User aborted normally
|
|
|
+ } else {
|
|
|
+ showToast('Traceroute Error', err.message, 'error');
|
|
|
+ }
|
|
|
} finally {
|
|
|
- if (btn) btn.disabled = false;
|
|
|
- if (spinner) spinner.classList.add('hidden');
|
|
|
+ tracerouteAbortController = null;
|
|
|
+ resetTracerouteButton();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function handleLiveTracerouteEvent(evt) {
|
|
|
+ const statHops = document.getElementById('tools-tr-hops-count');
|
|
|
+ const tbody = document.getElementById('tools-tr-tbody');
|
|
|
+ const rawEl = document.getElementById('tools-tr-raw');
|
|
|
+
|
|
|
+ if (evt.raw && rawEl) {
|
|
|
+ rawEl.innerText += (rawEl.innerText ? '\n' : '') + evt.raw;
|
|
|
+ rawEl.scrollTop = rawEl.scrollHeight;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (evt.hops_count !== undefined && statHops) {
|
|
|
+ statHops.innerText = `${evt.hops_count} Hops`;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (evt.type === 'hop' && evt.hop && tbody) {
|
|
|
+ const h = evt.hop;
|
|
|
+ const isTimeout = h.status === 'timeout' || (!h.ip && !h.host);
|
|
|
+ const rtt1 = Array.isArray(h.times_ms) && h.times_ms[0] ? `${h.times_ms[0]} ms` : (isTimeout ? '*' : '--');
|
|
|
+ const rtt2 = Array.isArray(h.times_ms) && h.times_ms[1] ? `${h.times_ms[1]} ms` : (isTimeout ? '*' : '--');
|
|
|
+ const displayHost = h.host && h.host !== h.ip ? `${escapeHtml(h.host)} (${escapeHtml(h.ip || '')})` : escapeHtml(h.ip || (isTimeout ? 'Request timed out' : '--'));
|
|
|
+
|
|
|
+ const tr = document.createElement('tr');
|
|
|
+ tr.className = `hover:bg-slate-800/40 border-b border-slate-800/60 font-mono text-xs ${isTimeout ? 'opacity-60' : ''}`;
|
|
|
+ tr.innerHTML = `
|
|
|
+ <td class="py-2.5 px-3 text-slate-300 font-bold">${h.hop}</td>
|
|
|
+ <td class="py-2.5 px-3 font-semibold ${isTimeout ? 'text-slate-500 italic' : 'text-cyan-300'}">${displayHost}</td>
|
|
|
+ <td class="py-2.5 px-3 font-bold ${isTimeout ? 'text-slate-500' : 'text-amber-300'}">${escapeHtml(rtt1)}</td>
|
|
|
+ <td class="py-2.5 px-3 font-bold ${isTimeout ? 'text-slate-500' : 'text-amber-300'}">${escapeHtml(rtt2)}</td>
|
|
|
+ <td class="py-2.5 px-3 text-right">
|
|
|
+ ${isTimeout
|
|
|
+ ? '<span class="px-2 py-0.5 rounded text-[10px] font-bold bg-slate-800 text-slate-400 border border-slate-700">TIMEOUT</span>'
|
|
|
+ : '<span class="px-2 py-0.5 rounded text-[10px] font-bold bg-emerald-950 text-emerald-300 border border-emerald-800/60">OK</span>'}
|
|
|
+ </td>
|
|
|
+ `;
|
|
|
+ tbody.appendChild(tr);
|
|
|
+ } else if (evt.type === 'done') {
|
|
|
+ showToast('Traceroute Finished', `Traced ${evt.hops_count || 0} hops`, 'info');
|
|
|
}
|
|
|
}
|
|
|
|