From e47523c461fdaf1716d9cc267512dd2ff276f3f7 Mon Sep 17 00:00:00 2001 From: Blackwhitebear8 Date: Thu, 14 Aug 2025 17:53:53 +0200 Subject: [PATCH] Update static/js/looking_glass.js --- static/js/looking_glass.js | 438 +++++++++++++++++++++---------------- 1 file changed, 248 insertions(+), 190 deletions(-) diff --git a/static/js/looking_glass.js b/static/js/looking_glass.js index acdef14..145b747 100644 --- a/static/js/looking_glass.js +++ b/static/js/looking_glass.js @@ -1,12 +1,16 @@ '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 visualizerContainer = document.getElementById('visualizer-container'); - const networkContainer = document.getElementById('mynetwork'); const formError = document.getElementById('form-error'); const mainLocationSelector = document.getElementById('main-location-selector'); const locationNameDisplay = document.getElementById('location-name-display'); @@ -20,137 +24,97 @@ document.addEventListener('DOMContentLoaded', function() { const iperfInInput = document.getElementById('iperf-in'); const iperfOutInput = document.getElementById('iperf-out'); const speedtestLinksContainer = document.getElementById('speedtest-links'); - let network = null; - let allLocationsData = {}; - let clientIpApiUrls = {}; + 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}`); - } + if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); return response.json(); }) .then(config => { - if (config.error) { - throw new Error(config.error); - } + 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); - lgForm.innerHTML = `
Could not load application configuration. Please check the server logs and your .env file.
`; + if(lgForm) lgForm.innerHTML = `
Could not load application configuration.
`; }); - lgForm.addEventListener('submit', async function(event) { - event.preventDefault(); - - const target = document.getElementById('target').value.trim(); - const method = methodSelect.value; - - formError.textContent = ''; - outputConsole.innerHTML = 'Output will appear here...'; - visualizerContainer.style.display = 'none'; - if (network) { - network.destroy(); - networkContainer.innerHTML = ''; - } - - setLoadingState(true); - - if (method === 'visualize') { - handleVisualize(target); - } else if (method === 'bgp_raw') { - handleRawBgpLookup(target); - } else { - handleCommand(method, target); - } - }); - - mainLocationSelector.addEventListener('change', updateLocationInfo); - - function highlightBGPOutput(text) { - if (!text) return ''; - - text = text.replace(/&/g, "&").replace(//g, ">"); - - text = text.replace(/\b215085\b/g, '215085'); - - text = text.replace(/\b\d{4,6}\b/g, match => { - if (match === '215085') return `215085`; - return `${match}`; - }); - - text = text.split('\n').map(line => { - const lowerLine = line.toLowerCase(); - if (lowerLine.includes('best') || lowerLine.includes('table entry') || lowerLine.includes('multipath')) { - return `${line}`; + 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; } - return line; - }).join('\n'); - - return text.replace(/\n/g, '
'); - } - - async function handleRawBgpLookup(target) { - visualizerContainer.style.display = 'none'; - outputConsole.style.display = 'block'; - outputConsole.innerHTML = `Looking up BGP route for ${target}...\n\nThis may take a few moments. Please wait.`; - - try { - const response = await fetch('/api/bgp_raw_lookup', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ target }) - }); - - const contentType = response.headers.get("content-type"); - - if (contentType && contentType.indexOf("application/json") !== -1) { - const data = await response.json(); - if (data.error) { - formError.textContent = data.error; - outputConsole.innerHTML = ''; - } - } else if (response.ok && contentType && contentType.indexOf("text/plain") !== -1) { - const rawText = await response.text(); - outputConsole.innerHTML = highlightBGPOutput(rawText); - } else { - throw new Error('Received an unexpected response from the server.'); - } - } catch (error) { - formError.textContent = error.message; - } finally { - setLoadingState(false); - } - } - - async function handleCommand(method, target) { - visualizerContainer.style.display = 'none'; - outputConsole.style.display = 'block'; - outputConsole.textContent = `Executing ${method} on ${target}...\n\nThis may take a few moments. Please wait.`; - - try { - const response = await fetch('/api/execute', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ method, target }) - }); + setLoadingState(true); - const contentType = response.headers.get("content-type"); + if (method === 'visualize') { + handleVisualize(target); + } else if (method === 'bgp_raw') { + handleRawBgpLookup(target); + } else { + handleCommand(method, target); + } + }); + } - if (contentType && contentType.indexOf("application/json") !== -1) { - const data = await response.json(); - if (data.error) { - formError.textContent = data.error; - outputConsole.textContent = ''; + 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; } - } else if (response.ok && contentType && contentType.indexOf("text/plain") !== -1) { + 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 = ''; @@ -158,55 +122,73 @@ document.addEventListener('DOMContentLoaded', function() { while (true) { const { value, done } = await reader.read(); if (done) break; - outputConsole.textContent += decoder.decode(value, { stream: true }); + 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 { - throw new Error('Received an unexpected response from the server.'); + return await response.text(); } - } catch (error) { formError.textContent = error.message; + outputConsole.textContent = ''; } finally { setLoadingState(false); } } - function handleVisualize(target) { + 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 loader = document.getElementById('visualizer-loader'); - if (network) { - network.destroy(); - } - networkContainer.innerHTML = ''; - 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 }) + }); - fetch('/api/visualize', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ ip_address: target }) - }) - .then(response => response.json()) - .then(data => { - if (data.error) { - formError.textContent = data.error; - visualizerContainer.style.display = 'none'; - return; + 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 (data.not_found) { - formError.textContent = `Route information not found for: ${data.target}`; - visualizerContainer.style.display = 'none'; - return; - } + 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`; - network = new vis.Network(networkContainer, data, { + 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 } }, @@ -218,19 +200,71 @@ document.addEventListener('DOMContentLoaded', function() { borderWidth: 2 }, physics: { enabled: false } - }); - }) - .catch(error => { + }; + + 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'; - formError.textContent = `An unexpected error occurred: ${error.message}`; - }) - .finally(() => { - loader.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'); @@ -241,57 +275,71 @@ document.addEventListener('DOMContentLoaded', function() { } function updateLocationInfo() { + if (!mainLocationSelector) return; const selectedLocationName = mainLocationSelector.value; const locationInfo = allLocationsData[selectedLocationName]; if (!locationInfo) return; - locationNameDisplay.value = selectedLocationName; - facilityNameDisplay.value = locationInfo.facility || 'N/A'; - mapLinkBtn.href = `https://www.openstreetmap.org/search?query=${encodeURIComponent(selectedLocationName)}`; - peeringdbLinkBtn.href = locationInfo.peeringdb_url || '#'; - peeringdbLinkBtn.classList.toggle('disabled', !locationInfo.peeringdb_url); - lgIpv4Input.value = locationInfo.ipv4 || 'N/A'; - lgIpv6Input.value = locationInfo.ipv6 || 'N/A'; - iperfInInput.value = locationInfo.iperf_in || 'N/A'; - iperfOutInput.value = locationInfo.iperf_out || 'N/A'; + 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'; + } + } - speedtestLinksContainer.innerHTML = ''; - if (locationInfo.speedtest_url_base && 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); - }); + 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() { - clientIpv4Input.value = 'Detecting...'; - clientIpv6Input.value = 'Detecting...'; - - if (clientIpApiUrls && clientIpApiUrls.v4) { + if (clientIpv4Input && clientIpApiUrls && clientIpApiUrls.v4) { + clientIpv4Input.value = 'Detecting...'; fetch(clientIpApiUrls.v4) .then(response => response.json()) - .then(data => { clientIpv4Input.value = data.ip || 'Unavailable'; }) - .catch(() => { clientIpv4Input.value = 'Unavailable'; }); - } else { + .then(data => { if(clientIpv4Input) clientIpv4Input.value = data.ip || 'Unavailable'; }) + .catch(() => { if(clientIpv4Input) clientIpv4Input.value = 'Unavailable'; }); + } else if (clientIpv4Input) { clientIpv4Input.value = 'Not Configured'; } - if (clientIpApiUrls && clientIpApiUrls.v6) { + if (clientIpv6Input && clientIpApiUrls && clientIpApiUrls.v6) { + clientIpv6Input.value = 'Detecting...'; fetch(clientIpApiUrls.v6) .then(response => response.json()) - .then(data => { clientIpv6Input.value = data.ip || 'Unavailable'; }) - .catch(() => { clientIpv6Input.value = 'Unavailable'; }); - } else { + .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...` @@ -302,19 +350,29 @@ document.addEventListener('DOMContentLoaded', function() { window.copyToClipboard = function(elementId) { const input = document.getElementById(elementId); if (!input) return; + const textToCopy = input.value; - const copyButton = input.nextElementSibling; - input.select(); - input.setSelectionRange(0, 99999); - - try { - document.execCommand('copy'); - if (copyButton) { - const originalText = copyButton.textContent; - copyButton.textContent = 'Copied!'; - setTimeout(() => { copyButton.textContent = originalText; }, 2000); + 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); } - } catch (err) { - console.error('Failed to copy text: ', err); + } +} + +function updateCopyButton(copyButton) { + if (copyButton && copyButton.classList.contains('copy-btn')) { + const originalText = copyButton.textContent; + copyButton.textContent = 'Copied!'; + setTimeout(() => { copyButton.textContent = originalText; }, 2000); } } \ No newline at end of file