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