|
|
@@ -194,6 +194,7 @@ async function fetchData(isManual = false) {
|
|
|
if (i.name.startsWith('ep_')) return false;
|
|
|
return !['gre0', 'gretap0', 'erspan0', 'sit0', 'tunl0', 'ip6tnl0', 'ip6gre0', 'ip6erspan0'].includes(i.name);
|
|
|
});
|
|
|
+ try { populateToolsInterfaces(); } catch (e) {}
|
|
|
}
|
|
|
if (vlansRes.ok) {
|
|
|
const vlansData = await vlansRes.json();
|
|
|
@@ -7052,13 +7053,9 @@ function loadToolsView() {
|
|
|
|
|
|
function populateToolsInterfaces() {
|
|
|
const pingSel = document.getElementById('tools-ping-iface');
|
|
|
- const traceSel = document.getElementById('tools-trace-iface');
|
|
|
- if (!pingSel || !traceSel) return;
|
|
|
+ const traceSel = document.getElementById('tools-tr-iface');
|
|
|
|
|
|
- const currentPing = pingSel.value;
|
|
|
- const currentTrace = traceSel.value;
|
|
|
-
|
|
|
- let opts = '<option value="">Default (System Routing)</option>';
|
|
|
+ let opts = '<option value="">Auto (Default Route)</option>';
|
|
|
if (Array.isArray(state.interfaces)) {
|
|
|
state.interfaces.forEach(iface => {
|
|
|
if (iface.name) {
|
|
|
@@ -7067,11 +7064,16 @@ function populateToolsInterfaces() {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- pingSel.innerHTML = opts;
|
|
|
- traceSel.innerHTML = opts;
|
|
|
-
|
|
|
- if (currentPing) pingSel.value = currentPing;
|
|
|
- if (currentTrace) traceSel.value = currentTrace;
|
|
|
+ if (pingSel) {
|
|
|
+ const cur = pingSel.value;
|
|
|
+ pingSel.innerHTML = opts;
|
|
|
+ if (cur) pingSel.value = cur;
|
|
|
+ }
|
|
|
+ if (traceSel) {
|
|
|
+ const cur = traceSel.value;
|
|
|
+ traceSel.innerHTML = opts;
|
|
|
+ if (cur) traceSel.value = cur;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
function switchToolsSubTab(subTab) {
|
|
|
@@ -7145,32 +7147,58 @@ async function executePing(e) {
|
|
|
const resCard = document.getElementById('tools-ping-results');
|
|
|
if (resCard) resCard.classList.remove('hidden');
|
|
|
|
|
|
- document.getElementById('tools-ping-stat-sent').innerText = data.packets_sent ?? 0;
|
|
|
- document.getElementById('tools-ping-stat-rcv').innerText = data.packets_recv ?? 0;
|
|
|
-
|
|
|
+ const sentEl = document.getElementById('tools-ping-stat-sent');
|
|
|
+ if (sentEl) sentEl.innerText = data.packets_sent ?? 0;
|
|
|
+
|
|
|
+ const recvEl = document.getElementById('tools-ping-stat-recv');
|
|
|
+ if (recvEl) recvEl.innerText = data.packets_recv ?? 0;
|
|
|
+
|
|
|
const lossEl = document.getElementById('tools-ping-stat-loss');
|
|
|
if (lossEl) {
|
|
|
const loss = data.packet_loss_pct ?? 0;
|
|
|
- lossEl.innerText = `${loss}%`;
|
|
|
- lossEl.className = loss === 0 ? 'text-lg font-bold font-mono text-emerald-400' : (loss < 50 ? 'text-lg font-bold font-mono text-amber-400' : 'text-lg font-bold font-mono text-rose-400');
|
|
|
+ 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');
|
|
|
+ }
|
|
|
+
|
|
|
+ const avgEl = document.getElementById('tools-ping-stat-avg');
|
|
|
+ if (avgEl) avgEl.innerText = data.avg_rtt_ms ? `${data.avg_rtt_ms.toFixed(2)} ms` : '--';
|
|
|
+
|
|
|
+ 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 = '--';
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- document.getElementById('tools-ping-stat-min').innerText = data.min_rtt_ms ? `${data.min_rtt_ms.toFixed(1)} ms` : '--';
|
|
|
- document.getElementById('tools-ping-stat-avg').innerText = data.avg_rtt_ms ? `${data.avg_rtt_ms.toFixed(1)} ms` : '--';
|
|
|
- document.getElementById('tools-ping-stat-max').innerText = data.max_rtt_ms ? `${data.max_rtt_ms.toFixed(1)} ms` : '--';
|
|
|
- document.getElementById('tools-ping-stat-jitter').innerText = data.mdev_rtt_ms ? `${data.mdev_rtt_ms.toFixed(2)} ms` : '--';
|
|
|
+ const badge = document.getElementById('tools-ping-status-badge');
|
|
|
+ if (badge) {
|
|
|
+ if (data.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';
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- const tbody = document.getElementById('tools-ping-table-body');
|
|
|
+ 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="p-3 text-slate-300">#${r.seq}</td>
|
|
|
- <td class="p-3 font-semibold text-white">${escapeHtml(r.from)}</td>
|
|
|
- <td class="p-3 text-slate-400">${r.bytes} bytes</td>
|
|
|
- <td class="p-3 text-slate-300">${r.ttl}</td>
|
|
|
- <td class="p-3 font-bold text-amber-300">${r.time_ms.toFixed(2)} ms</td>
|
|
|
- <td class="p-3"><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>
|
|
|
+ <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 {
|
|
|
@@ -7192,20 +7220,19 @@ async function executePing(e) {
|
|
|
|
|
|
async function executeTraceroute(e) {
|
|
|
if (e) e.preventDefault();
|
|
|
- const target = document.getElementById('tools-trace-target')?.value?.trim();
|
|
|
- const maxHops = parseInt(document.getElementById('tools-trace-maxhops')?.value || '15', 10);
|
|
|
- const timeout = parseInt(document.getElementById('tools-trace-timeout')?.value || '1', 10);
|
|
|
- const protocol = document.getElementById('tools-trace-protocol')?.value || 'icmp';
|
|
|
- const iface = document.getElementById('tools-trace-iface')?.value || '';
|
|
|
- const resolve = document.getElementById('tools-trace-resolve')?.checked || false;
|
|
|
+ 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';
|
|
|
+ const iface = document.getElementById('tools-tr-iface')?.value || '';
|
|
|
+ const resolve = document.getElementById('tools-tr-resolve')?.checked || false;
|
|
|
|
|
|
if (!target) {
|
|
|
showToast('Input Required', 'Please enter a target host or IP', 'warning');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- const btn = document.getElementById('tools-trace-btn');
|
|
|
- const spinner = document.getElementById('tools-trace-spinner');
|
|
|
+ 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');
|
|
|
|
|
|
@@ -7216,7 +7243,6 @@ async function executeTraceroute(e) {
|
|
|
body: JSON.stringify({
|
|
|
target,
|
|
|
max_hops: maxHops,
|
|
|
- timeout,
|
|
|
protocol,
|
|
|
interface: iface,
|
|
|
resolve
|
|
|
@@ -7229,35 +7255,32 @@ async function executeTraceroute(e) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- const resCard = document.getElementById('tools-trace-results');
|
|
|
+ const resCard = document.getElementById('tools-tr-results');
|
|
|
if (resCard) resCard.classList.remove('hidden');
|
|
|
|
|
|
const hopsCount = Array.isArray(data.hops) ? data.hops.length : 0;
|
|
|
- const statHops = document.getElementById('tools-trace-stat-hops');
|
|
|
- if (statHops) statHops.innerText = hopsCount;
|
|
|
+ const statHops = document.getElementById('tools-tr-hops-count');
|
|
|
+ if (statHops) statHops.innerText = `${hopsCount} Hops`;
|
|
|
|
|
|
- const statStatus = document.getElementById('tools-trace-stat-status');
|
|
|
- if (statStatus) {
|
|
|
- statStatus.innerText = hopsCount > 0 ? 'Trace Complete' : 'No Hops Discovered';
|
|
|
- statStatus.className = hopsCount > 0 ? 'text-xs font-bold font-mono text-emerald-400' : 'text-xs font-bold font-mono text-rose-400';
|
|
|
- }
|
|
|
-
|
|
|
- const tbody = document.getElementById('tools-trace-table-body');
|
|
|
+ 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 rttDisplay = Array.isArray(h.times_ms) && h.times_ms.length > 0 ? h.times_ms.join(', ') : (isTimeout ? '* * *' : '--');
|
|
|
+ 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' : '--'));
|
|
|
+
|
|
|
return `
|
|
|
<tr class="hover:bg-slate-800/40 border-b border-slate-800/60 font-mono text-xs ${isTimeout ? 'opacity-60' : ''}">
|
|
|
- <td class="p-3 text-slate-300 font-bold">${h.hop}</td>
|
|
|
- <td class="p-3 font-semibold ${isTimeout ? 'text-slate-500 italic' : 'text-cyan-300'}">${escapeHtml(h.ip || (isTimeout ? 'Request timed out' : ''))}</td>
|
|
|
- <td class="p-3 text-slate-400">${escapeHtml(h.host || '--')}</td>
|
|
|
- <td class="p-3 font-bold ${isTimeout ? 'text-slate-500' : 'text-amber-300'}">${escapeHtml(rttDisplay)}</td>
|
|
|
- <td class="p-3">
|
|
|
+ <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">REACHABLE</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>
|
|
|
`;
|
|
|
@@ -7267,7 +7290,7 @@ async function executeTraceroute(e) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- const rawEl = document.getElementById('tools-trace-raw');
|
|
|
+ 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');
|
|
|
@@ -7282,7 +7305,7 @@ async function executeTraceroute(e) {
|
|
|
async function executeMTR(e) {
|
|
|
if (e) e.preventDefault();
|
|
|
const target = document.getElementById('tools-mtr-target')?.value?.trim();
|
|
|
- const cycles = parseInt(document.getElementById('tools-mtr-cycles')?.value || '3', 10);
|
|
|
+ const cycles = parseInt(document.getElementById('tools-mtr-count')?.value || '3', 10);
|
|
|
|
|
|
if (!target) {
|
|
|
showToast('Input Required', 'Please enter a target host or IP', 'warning');
|
|
|
@@ -7311,28 +7334,25 @@ async function executeMTR(e) {
|
|
|
if (resCard) resCard.classList.remove('hidden');
|
|
|
|
|
|
const hopsCount = Array.isArray(data.hops) ? data.hops.length : 0;
|
|
|
- const statHops = document.getElementById('tools-mtr-stat-hops');
|
|
|
- if (statHops) statHops.innerText = hopsCount;
|
|
|
+ const statHops = document.getElementById('tools-mtr-hops-count');
|
|
|
+ if (statHops) statHops.innerText = `${hopsCount} Hops`;
|
|
|
|
|
|
- const statDest = document.getElementById('tools-mtr-stat-dest');
|
|
|
- if (statDest) statDest.innerText = data.target || target;
|
|
|
-
|
|
|
- const tbody = document.getElementById('tools-mtr-table-body');
|
|
|
+ const tbody = document.getElementById('tools-mtr-tbody');
|
|
|
if (tbody) {
|
|
|
if (hopsCount > 0) {
|
|
|
tbody.innerHTML = data.hops.map(h => {
|
|
|
const lossClass = h.loss_pct === 0 ? 'text-emerald-400 font-bold' : (h.loss_pct < 20 ? 'text-amber-400 font-bold' : 'text-rose-400 font-bold');
|
|
|
return `
|
|
|
<tr class="hover:bg-slate-800/40 border-b border-slate-800/60 font-mono text-xs">
|
|
|
- <td class="p-3 text-slate-300 font-bold">${h.hop}</td>
|
|
|
- <td class="p-3 font-semibold text-white">${escapeHtml(h.host || '--')}</td>
|
|
|
- <td class="p-3 ${lossClass}">${h.loss_pct.toFixed(1)}%</td>
|
|
|
- <td class="p-3 text-slate-300">${h.sent}</td>
|
|
|
- <td class="p-3 text-slate-300">${h.last_ms.toFixed(1)} ms</td>
|
|
|
- <td class="p-3 font-bold text-amber-300">${h.avg_ms.toFixed(1)} ms</td>
|
|
|
- <td class="p-3 text-emerald-400">${h.best_ms.toFixed(1)} ms</td>
|
|
|
- <td class="p-3 text-rose-400">${h.wrst_ms.toFixed(1)} ms</td>
|
|
|
- <td class="p-3 text-slate-400">${h.stdev_ms.toFixed(1)}</td>
|
|
|
+ <td class="py-2.5 px-3 text-slate-300 font-bold">${h.hop}</td>
|
|
|
+ <td class="py-2.5 px-3 font-semibold text-white">${escapeHtml(h.host || '--')}</td>
|
|
|
+ <td class="py-2.5 px-3 ${lossClass}">${h.loss_pct.toFixed(1)}%</td>
|
|
|
+ <td class="py-2.5 px-3 text-slate-300">${h.sent}</td>
|
|
|
+ <td class="py-2.5 px-3 text-slate-300">${h.last_ms.toFixed(1)}</td>
|
|
|
+ <td class="py-2.5 px-3 font-bold text-amber-300">${h.avg_ms.toFixed(1)}</td>
|
|
|
+ <td class="py-2.5 px-3 text-emerald-400">${h.best_ms.toFixed(1)}</td>
|
|
|
+ <td class="py-2.5 px-3 text-rose-400">${h.wrst_ms.toFixed(1)}</td>
|
|
|
+ <td class="py-2.5 px-3 text-slate-400">${h.stdev_ms.toFixed(1)}</td>
|
|
|
</tr>
|
|
|
`;
|
|
|
}).join('');
|
|
|
@@ -7385,25 +7405,23 @@ async function executeDNS(e) {
|
|
|
const resCard = document.getElementById('tools-dns-results');
|
|
|
if (resCard) resCard.classList.remove('hidden');
|
|
|
|
|
|
- const statTime = document.getElementById('tools-dns-stat-time');
|
|
|
- if (statTime) statTime.innerText = `${data.query_time_ms ?? 0} ms`;
|
|
|
-
|
|
|
- const statServer = document.getElementById('tools-dns-stat-server');
|
|
|
- if (statServer) statServer.innerText = data.server || (server || 'System Resolver');
|
|
|
+ const metaEl = document.getElementById('tools-dns-meta');
|
|
|
+ if (metaEl) metaEl.innerText = `Resolved in ${data.query_time_ms ?? 0} ms via ${escapeHtml(data.server || 'System DNS')}`;
|
|
|
|
|
|
- const tbody = document.getElementById('tools-dns-table-body');
|
|
|
+ const tbody = document.getElementById('tools-dns-tbody');
|
|
|
if (tbody) {
|
|
|
if (Array.isArray(data.records) && data.records.length > 0) {
|
|
|
tbody.innerHTML = data.records.map(r => `
|
|
|
<tr class="hover:bg-slate-800/40 border-b border-slate-800/60 font-mono text-xs">
|
|
|
- <td class="p-3 font-semibold text-white">${escapeHtml(r.name)}</td>
|
|
|
- <td class="p-3 text-slate-400">${r.ttl}s</td>
|
|
|
- <td class="p-3"><span class="px-2 py-0.5 rounded text-[10px] font-bold bg-cyan-950 text-cyan-300 border border-cyan-800/60">${escapeHtml(r.type)}</span></td>
|
|
|
- <td class="p-3 font-bold text-amber-300 break-all">${escapeHtml(r.value)}</td>
|
|
|
+ <td class="py-2.5 px-3 font-semibold text-white">${escapeHtml(r.name)}</td>
|
|
|
+ <td class="py-2.5 px-3 text-slate-400">${r.ttl}s</td>
|
|
|
+ <td class="py-2.5 px-3 text-slate-400">${escapeHtml(r.class || 'IN')}</td>
|
|
|
+ <td class="py-2.5 px-3"><span class="px-2 py-0.5 rounded text-[10px] font-bold bg-cyan-950 text-cyan-300 border border-cyan-800/60">${escapeHtml(r.type)}</span></td>
|
|
|
+ <td class="py-2.5 px-3 font-bold text-amber-300 break-all">${escapeHtml(r.value)}</td>
|
|
|
</tr>
|
|
|
`).join('');
|
|
|
} else {
|
|
|
- tbody.innerHTML = `<tr><td colspan="4" class="p-4 text-center text-xs text-amber-400 font-mono">No answer records returned for ${escapeHtml(type)} query</td></tr>`;
|
|
|
+ tbody.innerHTML = `<tr><td colspan="5" class="p-4 text-center text-xs text-amber-400 font-mono">No answer records returned for ${escapeHtml(type)} query</td></tr>`;
|
|
|
}
|
|
|
}
|
|
|
|