'use strict'; document.addEventListener('DOMContentLoaded', function() { let network = null; let allNodes = new vis.DataSet(); let allEdges = new vis.DataSet(); let allLocationsData = {}; let clientIpApiUrls = {}; const lgForm = document.getElementById('lg-form'); const executeBtn = document.getElementById('execute-btn'); const methodSelect = document.getElementById('method'); const outputConsole = document.getElementById('output-console'); const formError = document.getElementById('form-error'); const mainLocationSelector = document.getElementById('main-location-selector'); const locationNameDisplay = document.getElementById('location-name-display'); const facilityNameDisplay = document.getElementById('facility-name-display'); const mapLinkBtn = document.getElementById('map-link-btn'); const peeringdbLinkBtn = document.getElementById('peeringdb-link-btn'); const lgIpv4Input = document.getElementById('lg-ipv4'); const lgIpv6Input = document.getElementById('lg-ipv6'); const clientIpv4Input = document.getElementById('client-ipv4'); const clientIpv6Input = document.getElementById('client-ipv6'); const iperfInInput = document.getElementById('iperf-in'); const iperfOutInput = document.getElementById('iperf-out'); const speedtestLinksContainer = document.getElementById('speedtest-links'); const webSpeedtestBtn = document.getElementById('web-speedtest-btn'); const visualizerContainer = document.getElementById('visualizer-container'); const networkContainer = document.getElementById('mynetwork'); const loader = document.getElementById('visualizer-loader'); const legendAndFilters = document.querySelector('.bgprtv-controls-wrapper'); const filterControls = document.querySelectorAll('input[name="bgprtv-filter"]'); const allCommunitiesCheckbox = document.querySelector('input[value="all"]'); const activeCheckbox = document.querySelector('input[value="active"]'); fetch('/api/locations') .then(response => { if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); return response.json(); }) .then(config => { if (config.error) throw new Error(config.error); allLocationsData = config.locations; clientIpApiUrls = config.client_ip_api; populateLocationSelectors(); updateLocationInfo(); fetchClientIPs(); }) .catch(error => { console.error('Fatal Error: Could not fetch initial configuration:', error); if(lgForm) lgForm.innerHTML = `
Could not load application configuration.
`; }); if (lgForm) { lgForm.addEventListener('submit', async function(event) { event.preventDefault(); const target = document.getElementById('target').value.trim(); const method = methodSelect.value; formError.textContent = ''; outputConsole.style.display = 'block'; outputConsole.innerHTML = 'Output will appear here...'; visualizerContainer.style.display = 'none'; if (legendAndFilters) legendAndFilters.style.display = 'none'; if (network) { network.destroy(); network = null; } setLoadingState(true); if (method === 'visualize') { handleVisualize(target); } else if (method === 'bgp_raw') { handleRawBgpLookup(target); } else { handleCommand(method, target); } }); } if (mainLocationSelector) { mainLocationSelector.addEventListener('change', updateLocationInfo); } if (filterControls.length > 0) { filterControls.forEach(checkbox => { checkbox.addEventListener('change', function(e) { const changedValue = e.target.value; if (changedValue === 'all' && e.target.checked) { filterControls.forEach(cb => { if (cb.value !== 'all' && cb.value !== 'active') cb.checked = false; }); } else if (changedValue !== 'all' && changedValue !== 'active' && e.target.checked) { if (allCommunitiesCheckbox) allCommunitiesCheckbox.checked = false; } const specificCommunityChecked = Array.from(filterControls).some(cb => cb.checked && cb.value !== 'all' && cb.value !== 'active'); if (!specificCommunityChecked && allCommunitiesCheckbox) { allCommunitiesCheckbox.checked = true; } filterGraph(); }); }); } async function handleApiRequest(url, body, isStreaming = false) { try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || 'An unknown server error occurred.'); } if (isStreaming) { const reader = response.body.getReader(); const decoder = new TextDecoder(); outputConsole.textContent = ''; while (true) { const { value, done } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); if ((body.method === 'mtr' || body.method === 'mtr6') && chunk.includes('@@@')) { const parts = chunk.split('@@@'); outputConsole.textContent = parts[parts.length - 1]; } else { outputConsole.textContent += chunk; } outputConsole.scrollTop = outputConsole.scrollHeight; } } else { return await response.text(); } } catch (error) { formError.textContent = error.message; outputConsole.textContent = ''; } finally { setLoadingState(false); } } function handleCommand(method, target) { outputConsole.textContent = `Executing ${method} on ${target}...\n\nPlease wait.`; handleApiRequest('/api/execute', { method, target }, true); } async function handleRawBgpLookup(target) { outputConsole.innerHTML = `Looking up BGP route for ${target}...\n\nPlease wait.`; const location = mainLocationSelector.value; const rawText = await handleApiRequest('/api/bgp_raw_lookup', { target, location }); if (rawText) { outputConsole.innerHTML = highlightBGPOutput(rawText); } } async function handleVisualize(target) { outputConsole.style.display = 'none'; visualizerContainer.style.display = 'block'; if(loader) loader.style.display = 'block'; const location = mainLocationSelector.value; try { const response = await fetch('/api/visualize', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ip_address: target, location }) }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || 'Server returned an error.'); } const data = await response.json(); if (data.not_found) throw new Error(`Route information not found for: ${data.target}`); if (!data.nodes || data.nodes.length === 0) throw new Error('Could not parse any valid AS paths.'); if (legendAndFilters) legendAndFilters.style.display = 'flex'; const pathCount = data.path_count || 0; const dynamicHeight = Math.max(600, 200 + (pathCount * 150)); networkContainer.style.height = `${dynamicHeight}px`; allNodes = new vis.DataSet(data.nodes); allEdges = new vis.DataSet(data.edges); const options = { layout: { hierarchical: false }, edges: { arrows: { to: { enabled: true, scaleFactor: 0.7 } }, smooth: { enabled: true, type: "cubicBezier", forceDirection: "horizontal", roundness: 0.85 } }, nodes: { shape: 'box', margin: 15, font: { size: 14, color: '#343a40', multi: 'html', align: 'left' }, borderWidth: 2 }, physics: { enabled: false } }; network = new vis.Network(networkContainer, {}, options); if (activeCheckbox) activeCheckbox.checked = false; if (allCommunitiesCheckbox) allCommunitiesCheckbox.checked = true; filterControls.forEach(cb => { if (cb.value !== 'all' && cb.value !== 'active') cb.checked = false; }); filterGraph(); } catch (error) { formError.textContent = error.message; visualizerContainer.style.display = 'none'; } finally { if(loader) loader.style.display = 'none'; setLoadingState(false); } } function filterGraph() { if (!network) return; const showOnlyActive = activeCheckbox ? activeCheckbox.checked : false; const showAllCommunities = allCommunitiesCheckbox ? allCommunitiesCheckbox.checked : true; const selectedCategories = Array.from(filterControls) .filter(cb => cb.checked && cb.value !== 'active' && cb.value !== 'all') .map(cb => cb.value); const nodeFilter = (item) => { const activeFilterPassed = !showOnlyActive || item.is_active; const categoryFilterPassed = showAllCommunities || selectedCategories.includes(item.path_category); return item.path_category === 'global' || (activeFilterPassed && categoryFilterPassed); }; const edgeFilter = (item) => { const activeFilterPassed = !showOnlyActive || item.is_active; const categoryFilterPassed = showAllCommunities || selectedCategories.includes(item.path_category); return activeFilterPassed && categoryFilterPassed; }; const filteredNodes = allNodes.get({ filter: nodeFilter }); const filteredEdges = allEdges.get({ filter: edgeFilter }); network.setData({ nodes: new vis.DataSet(filteredNodes), edges: new vis.DataSet(filteredEdges) }); } function highlightBGPOutput(text) { /* ... */ } function populateLocationSelectors() { /* ... */ } function updateLocationInfo() { /* ... */ } function fetchClientIPs() { /* ... */ } function setLoadingState(isLoading) { /* ... */ } function highlightBGPOutput(text) { if (!text) return ''; text = text.replace(/&/g, "&").replace(//g, ">"); text = text.split('\n').map(line => { const lowerLine = line.toLowerCase(); if (lowerLine.includes('best') || lowerLine.includes('paths:') || lowerLine.includes('multipath')) { return `${line}`; } return line; }).join('
'); return text.replace(/\b(AS)?\d{4,9}\b/g, match => `${match}`); } function populateLocationSelectors() { if (!mainLocationSelector) return; mainLocationSelector.innerHTML = ''; for (const locationName in allLocationsData) { const option = document.createElement('option'); option.value = locationName; option.textContent = locationName; mainLocationSelector.appendChild(option); } } function updateLocationInfo() { if (!mainLocationSelector) return; const selectedLocationName = mainLocationSelector.value; const locationInfo = allLocationsData[selectedLocationName]; if (!locationInfo) return; if (locationNameDisplay) locationNameDisplay.value = selectedLocationName; if (facilityNameDisplay) facilityNameDisplay.value = locationInfo.facility || 'N/A'; if (mapLinkBtn) mapLinkBtn.href = `https://www.openstreetmap.org/search?query=${encodeURIComponent(selectedLocationName)}`; if (peeringdbLinkBtn) { peeringdbLinkBtn.href = locationInfo.peeringdb_url || '#'; peeringdbLinkBtn.classList.toggle('disabled', !locationInfo.peeringdb_url); } if (lgIpv4Input) lgIpv4Input.value = locationInfo.ipv4 || 'N/A'; if (lgIpv6Input) lgIpv6Input.value = locationInfo.ipv6 || 'N/A'; if (iperfInInput) iperfInInput.value = locationInfo.iperf_in || 'N/A'; if (iperfOutInput) iperfOutInput.value = locationInfo.iperf_out || 'N/A'; if (webSpeedtestBtn) { if (locationInfo.web_speedtest_url) { webSpeedtestBtn.href = locationInfo.web_speedtest_url; webSpeedtestBtn.style.display = 'inline-block'; } else { webSpeedtestBtn.style.display = 'none'; } } if (speedtestLinksContainer) { speedtestLinksContainer.innerHTML = ''; if (locationInfo.speedtest_url_base && Array.isArray(locationInfo.speedtest_files)) { locationInfo.speedtest_files.forEach(fileName => { const link = document.createElement('a'); link.href = locationInfo.speedtest_url_base + fileName; link.textContent = fileName.replace('.bin', '').toUpperCase(); link.className = 'btn btn-sm btn-outline-secondary'; link.target = '_blank'; speedtestLinksContainer.appendChild(link); }); } } } function fetchClientIPs() { if (clientIpv4Input && clientIpApiUrls && clientIpApiUrls.v4) { clientIpv4Input.value = 'Detecting...'; fetch(clientIpApiUrls.v4) .then(response => response.json()) .then(data => { if(clientIpv4Input) clientIpv4Input.value = data.ip || 'Unavailable'; }) .catch(() => { if(clientIpv4Input) clientIpv4Input.value = 'Unavailable'; }); } else if (clientIpv4Input) { clientIpv4Input.value = 'Not Configured'; } if (clientIpv6Input && clientIpApiUrls && clientIpApiUrls.v6) { clientIpv6Input.value = 'Detecting...'; fetch(clientIpApiUrls.v6) .then(response => response.json()) .then(data => { if(clientIpv6Input) clientIpv6Input.value = data.ip || 'Unavailable'; }) .catch(() => { if(clientIpv6Input) clientIpv6Input.value = 'Unavailable'; }); } else if (clientIpv6Input) { clientIpv6Input.value = 'Not Configured'; } } function setLoadingState(isLoading) { if (!executeBtn) return; executeBtn.disabled = isLoading; executeBtn.innerHTML = isLoading ? ` Executing...` : 'Execute'; } }); window.copyToClipboard = function(elementId) { const input = document.getElementById(elementId); if (!input) return; const textToCopy = input.value; if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(textToCopy).then(() => { updateCopyButton(input.nextElementSibling); }).catch(err => { console.error('Modern copy failed: ', err); }); } else { input.select(); try { document.execCommand('copy'); updateCopyButton(input.nextElementSibling); } catch (err) { console.error('Fallback copy failed: ', err); } } } function updateCopyButton(copyButton) { if (copyButton && copyButton.classList.contains('copy-btn')) { const originalText = copyButton.textContent; copyButton.textContent = 'Copied!'; setTimeout(() => { copyButton.textContent = originalText; }, 2000); } }