Procházet zdrojové kódy

fix(tools): fix DOM element IDs and add safe null checks in tools handlers

Proxmox Router před 1 týdnem
rodič
revize
1bfe2471bd
2 změnil soubory, kde provedl 107 přidání a 83 odebrání
  1. 99 81
      internal/web/static/app.js
  2. 8 2
      internal/web/static/index.html

+ 99 - 81
internal/web/static/app.js

@@ -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>`;
       }
     }
 

+ 8 - 2
internal/web/static/index.html

@@ -2493,7 +2493,7 @@
           <div id="tools-view-traceroute" class="hidden space-y-5">
             <div class="bg-slate-900/90 border border-slate-800 rounded-xl p-5">
               <form id="tools-traceroute-form" onsubmit="executeTraceroute(event)" class="space-y-4">
-                <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
+                <div class="grid grid-cols-1 md:grid-cols-5 gap-4">
                   <div class="md:col-span-2">
                     <label class="block text-xs font-semibold text-slate-300 mb-1.5">Target Host or IP *</label>
                     <input type="text" id="tools-tr-target" required placeholder="e.g. 1.1.1.1 or google.com" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-xs text-white font-mono">
@@ -2509,6 +2509,12 @@
                       <option value="udp">UDP Probe</option>
                     </select>
                   </div>
+                  <div>
+                    <label class="block text-xs font-semibold text-slate-300 mb-1.5">Source Interface</label>
+                    <select id="tools-tr-iface" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-xs text-white font-mono">
+                      <option value="">Auto (Default Route)</option>
+                    </select>
+                  </div>
                 </div>
 
                 <div class="flex items-center justify-between pt-2">
@@ -4960,6 +4966,6 @@
     </div>
   </div>
 
-  <script src="app.js?v=3.7"></script>
+  <script src="app.js?v=3.8"></script>
 </body>
 </html>