Update static/js/looking_glass.js

This commit is contained in:
Blackwhitebear8 2025-08-14 17:53:53 +02:00
parent b1b1ddace8
commit e47523c461

View file

@ -1,12 +1,16 @@
'use strict'; 'use strict';
document.addEventListener('DOMContentLoaded', function() { 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 lgForm = document.getElementById('lg-form');
const executeBtn = document.getElementById('execute-btn'); const executeBtn = document.getElementById('execute-btn');
const methodSelect = document.getElementById('method'); const methodSelect = document.getElementById('method');
const outputConsole = document.getElementById('output-console'); const outputConsole = document.getElementById('output-console');
const visualizerContainer = document.getElementById('visualizer-container');
const networkContainer = document.getElementById('mynetwork');
const formError = document.getElementById('form-error'); const formError = document.getElementById('form-error');
const mainLocationSelector = document.getElementById('main-location-selector'); const mainLocationSelector = document.getElementById('main-location-selector');
const locationNameDisplay = document.getElementById('location-name-display'); const locationNameDisplay = document.getElementById('location-name-display');
@ -20,137 +24,97 @@ document.addEventListener('DOMContentLoaded', function() {
const iperfInInput = document.getElementById('iperf-in'); const iperfInInput = document.getElementById('iperf-in');
const iperfOutInput = document.getElementById('iperf-out'); const iperfOutInput = document.getElementById('iperf-out');
const speedtestLinksContainer = document.getElementById('speedtest-links'); const speedtestLinksContainer = document.getElementById('speedtest-links');
let network = null; const webSpeedtestBtn = document.getElementById('web-speedtest-btn');
let allLocationsData = {}; const visualizerContainer = document.getElementById('visualizer-container');
let clientIpApiUrls = {}; 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') fetch('/api/locations')
.then(response => { .then(response => {
if (!response.ok) { if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json(); return response.json();
}) })
.then(config => { .then(config => {
if (config.error) { if (config.error) throw new Error(config.error);
throw new Error(config.error);
}
allLocationsData = config.locations; allLocationsData = config.locations;
clientIpApiUrls = config.client_ip_api; clientIpApiUrls = config.client_ip_api;
populateLocationSelectors(); populateLocationSelectors();
updateLocationInfo(); updateLocationInfo();
fetchClientIPs(); fetchClientIPs();
}) })
.catch(error => { .catch(error => {
console.error('Fatal Error: Could not fetch initial configuration:', error); console.error('Fatal Error: Could not fetch initial configuration:', error);
lgForm.innerHTML = `<div class="alert alert-danger">Could not load application configuration. Please check the server logs and your .env file.</div>`; if(lgForm) lgForm.innerHTML = `<div class="alert alert-danger">Could not load application configuration.</div>`;
}); });
lgForm.addEventListener('submit', async function(event) { if (lgForm) {
event.preventDefault(); lgForm.addEventListener('submit', async function(event) {
event.preventDefault();
const target = document.getElementById('target').value.trim(); const target = document.getElementById('target').value.trim();
const method = methodSelect.value; const method = methodSelect.value;
formError.textContent = '';
formError.textContent = ''; outputConsole.style.display = 'block';
outputConsole.innerHTML = 'Output will appear here...'; outputConsole.innerHTML = 'Output will appear here...';
visualizerContainer.style.display = 'none'; visualizerContainer.style.display = 'none';
if (network) { if (legendAndFilters) legendAndFilters.style.display = 'none';
network.destroy(); if (network) {
networkContainer.innerHTML = ''; network.destroy();
} network = null;
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
text = text.replace(/\b215085\b/g, '<span class="asn215085-highlight">215085</span>');
text = text.replace(/\b\d{4,6}\b/g, match => {
if (match === '215085') return `<span class="asn215085-highlight">215085</span>`;
return `<span class="asn-highlight">${match}</span>`;
});
text = text.split('\n').map(line => {
const lowerLine = line.toLowerCase();
if (lowerLine.includes('best') || lowerLine.includes('table entry') || lowerLine.includes('multipath')) {
return `<strong class="best-line">${line}</strong>`;
} }
return line; setLoadingState(true);
}).join('\n');
return text.replace(/\n/g, '<br>'); if (method === 'visualize') {
} handleVisualize(target);
} else if (method === 'bgp_raw') {
async function handleRawBgpLookup(target) { 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 { } else {
throw new Error('Received an unexpected response from the server.'); handleCommand(method, target);
} }
} catch (error) { });
formError.textContent = error.message;
} finally {
setLoadingState(false);
}
} }
async function handleCommand(method, target) { if (mainLocationSelector) {
visualizerContainer.style.display = 'none'; mainLocationSelector.addEventListener('change', updateLocationInfo);
outputConsole.style.display = 'block'; }
outputConsole.textContent = `Executing ${method} on ${target}...\n\nThis may take a few moments. Please wait.`;
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 { try {
const response = await fetch('/api/execute', { const response = await fetch(url, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ method, target }) body: JSON.stringify(body)
}); });
const contentType = response.headers.get("content-type"); if (!response.ok) {
if (contentType && contentType.indexOf("application/json") !== -1) {
const data = await response.json(); const data = await response.json();
if (data.error) { throw new Error(data.error || 'An unknown server error occurred.');
formError.textContent = data.error; }
outputConsole.textContent = '';
} if (isStreaming) {
} else if (response.ok && contentType && contentType.indexOf("text/plain") !== -1) {
const reader = response.body.getReader(); const reader = response.body.getReader();
const decoder = new TextDecoder(); const decoder = new TextDecoder();
outputConsole.textContent = ''; outputConsole.textContent = '';
@ -158,55 +122,73 @@ document.addEventListener('DOMContentLoaded', function() {
while (true) { while (true) {
const { value, done } = await reader.read(); const { value, done } = await reader.read();
if (done) break; 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; outputConsole.scrollTop = outputConsole.scrollHeight;
} }
} else { } else {
throw new Error('Received an unexpected response from the server.'); return await response.text();
} }
} catch (error) { } catch (error) {
formError.textContent = error.message; formError.textContent = error.message;
outputConsole.textContent = '';
} finally { } finally {
setLoadingState(false); 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'; outputConsole.style.display = 'none';
visualizerContainer.style.display = 'block'; visualizerContainer.style.display = 'block';
if(loader) loader.style.display = 'block';
const loader = document.getElementById('visualizer-loader'); const location = mainLocationSelector.value;
if (network) { try {
network.destroy(); const response = await fetch('/api/visualize', {
} method: 'POST',
networkContainer.innerHTML = ''; headers: { 'Content-Type': 'application/json' },
loader.style.display = 'block'; body: JSON.stringify({ ip_address: target, location })
});
fetch('/api/visualize', { if (!response.ok) {
method: 'POST', const data = await response.json();
headers: { 'Content-Type': 'application/json' }, throw new Error(data.error || 'Server returned an error.');
body: JSON.stringify({ ip_address: target })
})
.then(response => response.json())
.then(data => {
if (data.error) {
formError.textContent = data.error;
visualizerContainer.style.display = 'none';
return;
} }
const data = await response.json();
if (data.not_found) { if (data.not_found) throw new Error(`Route information not found for: ${data.target}`);
formError.textContent = `Route information not found for: ${data.target}`; if (!data.nodes || data.nodes.length === 0) throw new Error('Could not parse any valid AS paths.');
visualizerContainer.style.display = 'none';
return; if (legendAndFilters) legendAndFilters.style.display = 'flex';
}
const pathCount = data.path_count || 0; const pathCount = data.path_count || 0;
const dynamicHeight = Math.max(600, 200 + (pathCount * 150)); const dynamicHeight = Math.max(600, 200 + (pathCount * 150));
networkContainer.style.height = `${dynamicHeight}px`; 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 }, layout: { hierarchical: false },
edges: { edges: {
arrows: { to: { enabled: true, scaleFactor: 0.7 } }, arrows: { to: { enabled: true, scaleFactor: 0.7 } },
@ -218,19 +200,71 @@ document.addEventListener('DOMContentLoaded', function() {
borderWidth: 2 borderWidth: 2
}, },
physics: { enabled: false } 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'; visualizerContainer.style.display = 'none';
formError.textContent = `An unexpected error occurred: ${error.message}`; } finally {
}) if(loader) loader.style.display = 'none';
.finally(() => {
loader.style.display = 'none';
setLoadingState(false); 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
text = text.split('\n').map(line => {
const lowerLine = line.toLowerCase();
if (lowerLine.includes('best') || lowerLine.includes('paths:') || lowerLine.includes('multipath')) {
return `<strong class="best-line">${line}</strong>`;
}
return line;
}).join('<br>');
return text.replace(/\b(AS)?\d{4,9}\b/g, match => `<span class="asn-highlight">${match}</span>`);
} }
function populateLocationSelectors() { function populateLocationSelectors() {
if (!mainLocationSelector) return;
mainLocationSelector.innerHTML = ''; mainLocationSelector.innerHTML = '';
for (const locationName in allLocationsData) { for (const locationName in allLocationsData) {
const option = document.createElement('option'); const option = document.createElement('option');
@ -241,57 +275,71 @@ document.addEventListener('DOMContentLoaded', function() {
} }
function updateLocationInfo() { function updateLocationInfo() {
if (!mainLocationSelector) return;
const selectedLocationName = mainLocationSelector.value; const selectedLocationName = mainLocationSelector.value;
const locationInfo = allLocationsData[selectedLocationName]; const locationInfo = allLocationsData[selectedLocationName];
if (!locationInfo) return; if (!locationInfo) return;
locationNameDisplay.value = selectedLocationName; if (locationNameDisplay) locationNameDisplay.value = selectedLocationName;
facilityNameDisplay.value = locationInfo.facility || 'N/A'; if (facilityNameDisplay) facilityNameDisplay.value = locationInfo.facility || 'N/A';
mapLinkBtn.href = `https://www.openstreetmap.org/search?query=${encodeURIComponent(selectedLocationName)}`; if (mapLinkBtn) mapLinkBtn.href = `https://www.openstreetmap.org/search?query=${encodeURIComponent(selectedLocationName)}`;
peeringdbLinkBtn.href = locationInfo.peeringdb_url || '#'; if (peeringdbLinkBtn) {
peeringdbLinkBtn.classList.toggle('disabled', !locationInfo.peeringdb_url); peeringdbLinkBtn.href = locationInfo.peeringdb_url || '#';
lgIpv4Input.value = locationInfo.ipv4 || 'N/A'; peeringdbLinkBtn.classList.toggle('disabled', !locationInfo.peeringdb_url);
lgIpv6Input.value = locationInfo.ipv6 || 'N/A'; }
iperfInInput.value = locationInfo.iperf_in || 'N/A'; if (lgIpv4Input) lgIpv4Input.value = locationInfo.ipv4 || 'N/A';
iperfOutInput.value = locationInfo.iperf_out || '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';
speedtestLinksContainer.innerHTML = ''; if (webSpeedtestBtn) {
if (locationInfo.speedtest_url_base && locationInfo.speedtest_files) { if (locationInfo.web_speedtest_url) {
locationInfo.speedtest_files.forEach(fileName => { webSpeedtestBtn.href = locationInfo.web_speedtest_url;
const link = document.createElement('a'); webSpeedtestBtn.style.display = 'inline-block';
link.href = locationInfo.speedtest_url_base + fileName; } else {
link.textContent = fileName.replace('.bin', '').toUpperCase(); webSpeedtestBtn.style.display = 'none';
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() { function fetchClientIPs() {
clientIpv4Input.value = 'Detecting...'; if (clientIpv4Input && clientIpApiUrls && clientIpApiUrls.v4) {
clientIpv6Input.value = 'Detecting...'; clientIpv4Input.value = 'Detecting...';
if (clientIpApiUrls && clientIpApiUrls.v4) {
fetch(clientIpApiUrls.v4) fetch(clientIpApiUrls.v4)
.then(response => response.json()) .then(response => response.json())
.then(data => { clientIpv4Input.value = data.ip || 'Unavailable'; }) .then(data => { if(clientIpv4Input) clientIpv4Input.value = data.ip || 'Unavailable'; })
.catch(() => { clientIpv4Input.value = 'Unavailable'; }); .catch(() => { if(clientIpv4Input) clientIpv4Input.value = 'Unavailable'; });
} else { } else if (clientIpv4Input) {
clientIpv4Input.value = 'Not Configured'; clientIpv4Input.value = 'Not Configured';
} }
if (clientIpApiUrls && clientIpApiUrls.v6) { if (clientIpv6Input && clientIpApiUrls && clientIpApiUrls.v6) {
clientIpv6Input.value = 'Detecting...';
fetch(clientIpApiUrls.v6) fetch(clientIpApiUrls.v6)
.then(response => response.json()) .then(response => response.json())
.then(data => { clientIpv6Input.value = data.ip || 'Unavailable'; }) .then(data => { if(clientIpv6Input) clientIpv6Input.value = data.ip || 'Unavailable'; })
.catch(() => { clientIpv6Input.value = 'Unavailable'; }); .catch(() => { if(clientIpv6Input) clientIpv6Input.value = 'Unavailable'; });
} else { } else if (clientIpv6Input) {
clientIpv6Input.value = 'Not Configured'; clientIpv6Input.value = 'Not Configured';
} }
} }
function setLoadingState(isLoading) { function setLoadingState(isLoading) {
if (!executeBtn) return;
executeBtn.disabled = isLoading; executeBtn.disabled = isLoading;
executeBtn.innerHTML = isLoading executeBtn.innerHTML = isLoading
? `<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Executing...` ? `<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Executing...`
@ -302,19 +350,29 @@ document.addEventListener('DOMContentLoaded', function() {
window.copyToClipboard = function(elementId) { window.copyToClipboard = function(elementId) {
const input = document.getElementById(elementId); const input = document.getElementById(elementId);
if (!input) return; if (!input) return;
const textToCopy = input.value;
const copyButton = input.nextElementSibling; if (navigator.clipboard && window.isSecureContext) {
input.select(); navigator.clipboard.writeText(textToCopy).then(() => {
input.setSelectionRange(0, 99999); updateCopyButton(input.nextElementSibling);
}).catch(err => {
try { console.error('Modern copy failed: ', err);
document.execCommand('copy'); });
if (copyButton) { } else {
const originalText = copyButton.textContent; input.select();
copyButton.textContent = 'Copied!'; try {
setTimeout(() => { copyButton.textContent = originalText; }, 2000); 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);
} }
} }