`).join('');
}
function logEntry(dest, hops, timeouts) {
const timestamp = new Date().toLocaleString('en-US');
lookupLog.unshift({ timestamp, dest, hops, timeouts });
if (lookupLog.length > 50) lookupLog.pop(); // Limit log size
saveState();
renderLog();
}
function showMessage(message, isError = true) {
ui.errorMsg.textContent = message;
ui.errorMsg.style.display = 'block';
}
// --- PARSING & ANALYSIS ---
function parseTimes(timeString) {
const timeRegex = /([\d\.]+|\*)\s+ms/g;
const times = (timeString.match(timeRegex) || []).map(t => t.replace(' ms', ''));
return [times[0] || '*', times[1] || '*', times[2] || '*'];
}
function analyzeTrace() {
ui.loader.style.display = 'block';
ui.resultsContainer.style.display = 'none';
ui.errorMsg.style.display = 'none';
const text = ui.inputText.value.trim();
if (!text) {
showMessage("Input is empty.");
ui.loader.style.display = 'none';
return;
}
const lines = text.split('\n');
currentResults = { destination: { host: '', ip: '' }, hops: [], summary: {} };
let hopCount = 0;
let timeoutCount = 0;
let totalLatency = 0;
let validPings = 0;
// Parse destination from first line
const firstLineMatch = lines[0].match(/to\s+(.*?)\s+\((.*?)\)/);
if (firstLineMatch) {
currentResults.destination = { host: firstLineMatch[1], ip: firstLineMatch[2] };
} else {
currentResults.destination = { host: 'N/A', ip: 'N/A' };
}
const timeoutRegex = /^\s*(\d+)\s+\*\s+\*\s+\*$/;
// Regex for: 1 host.com (1.2.3.4) ...times...
const hostIpRegex = /^\s*(\d+)\s+(.+?)\s+\((.*?)\)\s+(.*)$/;
// Regex for: 1 1.2.3.4 ...times...
const ipOnlyRegex = /^\s*(\d+)\s+([\d\.:a-fA-F0-9]+)\s+(.*)$/;
for (const line of lines.slice(1)) {
const timeoutMatch = line.match(timeoutRegex);
if (timeoutMatch) {
currentResults.hops.push({ hop: timeoutMatch[1], host: '*', ip: '*', t1: '*', t2: '*', t3: '*', avg: 'N/A', isTimeout: true });
timeoutCount++;
continue;
}
let hop, host, ip, timeString;
const dataMatch = line.match(hostIpRegex);
const ipOnlyMatch = line.match(ipOnlyRegex);
if (dataMatch) {
[_, hop, host, ip, timeString] = dataMatch;
} else if (ipOnlyMatch) {
[_, hop, host, timeString] = ipOnlyMatch;
ip = host; // Host is the IP
} else {
continue; // Not a line we recognize
}
const [t1, t2, t3] = parseTimes(timeString);
const times = [t1, t2, t3].map(parseFloat).filter(t => !isNaN(t));
let avg = 'N/A';
if (times.length > 0) {
const sum = times.reduce((a, b) => a + b, 0);
avg = sum / times.length;
totalLatency += sum;
validPings += times.length;
}
currentResults.hops.push({ hop, host: host.trim(), ip: ip.trim(), t1, t2, t3, avg, isTimeout: false });
}
hopCount = currentResults.hops.length;
const totalAvg = validPings > 0 ? (totalLatency / validPings) : 0;
currentResults.summary = { hopCount, timeoutCount, totalAvg };
logEntry(currentResults.destination.host || 'N/A', hopCount, timeoutCount);
renderResults();
ui.loader.style.display = 'none';
}
function renderResults() {
const { summary, hops } = currentResults;
ui.summaryDest.textContent = `${currentResults.destination.host} (${currentResults.destination.ip})`;
ui.summaryHops.textContent = summary.hopCount;
ui.summaryTimeouts.textContent = summary.timeoutCount;
ui.summaryAvg.textContent = `${summary.totalAvg.toFixed(2)} ms`;
ui.resultsTbody.innerHTML = '';
hops.forEach(hop => {
const row = document.createElement('tr');
let avgClass = '';
if (hop.isTimeout) {
row.className = 'timeout';
} else if (hop.avg > 100) {
avgClass = 'latency-danger';
} else if (hop.avg > 50) {
avgClass = 'latency-warn';
}
row.innerHTML = `
${hop.hop} |
${escapeHTML(hop.host)} |
${escapeHTML(hop.ip)} |
${hop.avg === 'N/A' ? 'N/A' : hop.avg.toFixed(2)} |
${hop.t1} |
${hop.t2} |
${hop.t3} |
`;
ui.resultsTbody.appendChild(row);
});
ui.resultsContainer.style.display = 'block';
}
// --- PDF LOGIC ---
function downloadReportPDF(data, title, filename) {
const { jsPDF } = window.jspdf;
if (!jsPDF || !window.jspdf.autoTable) { alert("PDF library not loaded!"); return; }
const doc = new jsPDF();
doc.setFontSize(18);
doc.text(title, 14, 22);
if (data.summary) { // Single Report
doc.setFontSize(11);
doc.setTextColor(100);
doc.text(`Report for: ${data.destination.host} (${data.destination.ip})`, 14, 28);
doc.autoTable({
startY: 35,
theme: 'grid',
head: [['Metric', 'Value']],
body: [
['Total Hops', data.summary.hopCount],
['Timeouts', data.summary.timeoutCount],
['Overall Avg. Latency', `${data.summary.totalAvg.toFixed(2)} ms`],
]
});
doc.autoTable({
startY: doc.autoTable.previous.finalY + 10,
head: [['Hop', 'Host', 'IP', 'Avg (ms)', 'T1', 'T2', 'T3']],
body: data.hops.map(h => [h.hop, h.host, h.ip, h.avg === 'N/A' ? 'N/A' : h.avg.toFixed(2), h.t1, h.t2, h.t3]),
theme: 'striped',
headStyles: { fillColor: [29, 78, 216] } // var(--primary-color)
});
} else { // Log Report
doc.autoTable({
startY: 30,
head: [['Timestamp', 'Destination', 'Hops', 'Timeouts']],
body: data.map(log => [log.timestamp, log.dest, log.hops, log.timeouts]),
theme: 'striped',
headStyles: { fillColor: [29, 78, 216] }
});
}
doc.save(filename);
}
// --- EVENT LISTENERS ---
ui.analyzeBtn.addEventListener('click', analyzeTrace);
ui.downloadReportBtn.addEventListener('click', () => {
if (currentResults.hops.length === 0) {
alert("Please analyze a traceroute before downloading.");
return;
}
downloadReportPDF(currentResults, "Traceroute Report", `Traceroute_${currentResults.destination.host}.pdf`);
});
ui.downloadLogBtn.addEventListener('click', () => {
if (lookupLog.length === 0) {
alert("Log is empty.");
return;
}
downloadReportPDF(lookupLog, "Traceroute Analysis Log", "Traceroute_Log.pdf");
});
ui.clearLogBtn.addEventListener('click', () => {
lookupLog = [];
saveState();
renderLog();
});
// --- TABBING LOGIC ---
function switchTab(targetTabId) {
ui.tabContents.forEach(c => c.classList.remove('active'));
ui.tabButtons.forEach(b => b.classList.remove('active'));
document.getElementById(`tat-tab-${targetTabId}`).classList.add('active');
document.querySelector(`.tat-tab-button[data-tab="${targetTabId}"]`).classList.add('active');
}
function navigateTabs(dir) {
const tabs = Array.from(ui.tabButtons);
const active = tabs.find(t => t.classList.contains('active'));
let index = tabs.indexOf(active) + dir;
if (index < 0) index = 0; if (index >= tabs.length) index = tabs.length - 1;
switchTab(tabs[index].dataset.tab);
}
ui.tabButtons.forEach(button => button.addEventListener('click', () => switchTab(button.dataset.tab)));
ui.nextTabBtn.addEventListener('click', () => navigateTabs(1));
ui.prevTabBtn.addEventListener('click', () => navigateTabs(-1));
// --- UTILITY ---
function escapeHTML(str) { const p = document.createElement('p'); p.textContent = str; return p.innerHTML; }
// --- INITIALIZATION ---
loadState();
renderLog();
});