LookingGlas/static/js/looking_glass.js

294 lines
No EOL
11 KiB
JavaScript

'use strict';
document.addEventListener('DOMContentLoaded', function() {
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 alertModal = new bootstrap.Modal(document.getElementById('alertModal'));
const alertModalBody = document.getElementById('alertModalBody');
const mainLocationSelector = document.getElementById('main-location-selector');
const locationsDropdownMenu = document.getElementById('locations-dropdown-menu');
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');
let network = null;
let allLocationsData = {};
let clientIpApiUrls = {};
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);
lgForm.innerHTML = `<div class="alert alert-danger">Could not load application configuration. Please check the server logs and your .env file.</div>`;
});
lgForm.addEventListener('submit', async function(event) {
event.preventDefault();
const target = document.getElementById('target').value.trim();
const method = methodSelect.value;
if (!target) {
showModalAlert('Please enter a target.');
return;
}
let isValid = false;
if (method === 'visualize') {
isValid = isValidIpOrCidr(target);
if (!isValid) {
showModalAlert('Invalid input for Visualizer. Please provide a valid IPv4/IPv6 address or CIDR prefix.');
return;
}
} else {
const isSingleIp = isValidIPv4(target) || isValidIPv6(target);
const isHostname = isValidFqdn(target);
isValid = isSingleIp || isHostname;
if (!isValid) {
showModalAlert('Invalid input. For this method, please provide a valid IP address (without a prefix) or a fully qualified domain name.');
return;
}
}
setLoadingState(true);
if (method === 'visualize') {
handleVisualize(target);
} else {
handleCommand(method, target);
}
});
mainLocationSelector.addEventListener('change', updateLocationInfo);
locationsDropdownMenu.addEventListener('click', function(e) {
if (e.target.matches('a.dropdown-item')) {
e.preventDefault();
mainLocationSelector.value = e.target.dataset.location;
mainLocationSelector.dispatchEvent(new Event('change'));
}
});
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 })
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
outputConsole.textContent = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
outputConsole.textContent += decoder.decode(value, { stream: true });
outputConsole.scrollTop = outputConsole.scrollHeight;
}
} catch (error) {
outputConsole.textContent = `--- ERROR ---\n${error.message}`;
} finally {
setLoadingState(false);
}
}
function handleVisualize(target) {
outputConsole.style.display = 'none';
visualizerContainer.style.display = 'block';
const loader = document.getElementById('visualizer-loader');
if (network) {
network.destroy();
}
networkContainer.innerHTML = '';
loader.style.display = 'block';
fetch('/api/visualize', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ip_address: target })
})
.then(response => response.json().then(data => ({ ok: response.ok, data })))
.then(({ ok, data }) => {
if (!ok || data.error) {
throw new Error(data.error || 'Failed to retrieve visualization data.');
}
if (data.not_found) {
visualizerContainer.style.display = 'none';
showModalAlert(`Route information not found for: ${data.target}`);
return;
}
const pathCount = data.path_count || 0;
const dynamicHeight = Math.max(400, 200 + (pathCount * 150));
networkContainer.style.height = `${dynamicHeight}px`;
network = new vis.Network(networkContainer, data, {
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 }
});
})
.catch(error => {
visualizerContainer.style.display = 'none';
outputConsole.style.display = 'block';
outputConsole.textContent = `Visualization Error: ${error.message}`;
})
.finally(() => {
loader.style.display = 'none';
setLoadingState(false);
});
}
function showModalAlert(message) {
alertModalBody.textContent = message;
alertModal.show();
}
function populateLocationSelectors() {
mainLocationSelector.innerHTML = '';
locationsDropdownMenu.innerHTML = '';
for (const locationName in allLocationsData) {
const option = document.createElement('option');
option.value = locationName;
option.textContent = locationName;
mainLocationSelector.appendChild(option);
const li = document.createElement('li');
const a = document.createElement('a');
a.className = 'dropdown-item';
a.href = '#';
a.textContent = locationName;
a.dataset.location = locationName;
li.appendChild(a);
locationsDropdownMenu.appendChild(li);
}
}
function updateLocationInfo() {
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';
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);
});
}
}
function fetchClientIPs() {
clientIpv4Input.value = 'Detecting...';
clientIpv6Input.value = 'Detecting...';
if (clientIpApiUrls && clientIpApiUrls.v4) {
fetch(clientIpApiUrls.v4)
.then(response => response.json())
.then(data => { clientIpv4Input.value = data.ip || 'Unavailable'; })
.catch(() => { clientIpv4Input.value = 'Unavailable'; });
} else {
clientIpv4Input.value = 'Not Configured';
}
if (clientIpApiUrls && clientIpApiUrls.v6) {
fetch(clientIpApiUrls.v6)
.then(response => response.json())
.then(data => { clientIpv6Input.value = data.ip || 'Unavailable'; })
.catch(() => { clientIpv6Input.value = 'Unavailable'; });
} else {
clientIpv6Input.value = 'Not Configured';
}
}
function setLoadingState(isLoading) {
executeBtn.disabled = isLoading;
executeBtn.innerHTML = isLoading
? `<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Executing...`
: 'Execute';
}
});
window.copyToClipboard = function(elementId) {
const input = document.getElementById(elementId);
if (!input) return;
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);
}
} catch (err) {
console.error('Failed to copy text: ', err);
}
}