Upload files to "static/js"
This commit is contained in:
parent
dbcf644146
commit
a5f93d98b8
5 changed files with 377 additions and 0 deletions
7
static/js/bootstrap.bundle.min.js
vendored
Normal file
7
static/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
294
static/js/looking_glass.js
Normal file
294
static/js/looking_glass.js
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
'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);
|
||||
}
|
||||
}
|
||||
6
static/js/materialize.min.js
vendored
Normal file
6
static/js/materialize.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
36
static/js/validation.js
Normal file
36
static/js/validation.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
'use strict';
|
||||
|
||||
function isValidIPv4(str) {
|
||||
const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||
return ipv4Regex.test(str);
|
||||
}
|
||||
|
||||
function isValidIPv6(str) {
|
||||
const ipv6Regex = /(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/i;
|
||||
return ipv6Regex.test(str);
|
||||
}
|
||||
|
||||
function isValidIpOrCidr(str) {
|
||||
const parts = str.split('/');
|
||||
if (parts.length > 2) return false;
|
||||
|
||||
const isIP = isValidIPv4(parts[0]) || isValidIPv6(parts[0]);
|
||||
if (!isIP) return false;
|
||||
|
||||
if (parts.length === 2) {
|
||||
const mask = parseInt(parts[1], 10);
|
||||
if (isNaN(mask) || mask < 0 || mask > (isValidIPv4(parts[0]) ? 32 : 128)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function isValidFqdn(str) {
|
||||
if (isValidIPv4(str) || isValidIPv6(str)) {
|
||||
return false;
|
||||
}
|
||||
const fqdnRegex = /^(?!-)(?:[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.){1,126}(?!-d$)[a-zA-Z0-9-]{2,63}$/;
|
||||
return fqdnRegex.test(str);
|
||||
}
|
||||
34
static/js/vis-network.min.js
vendored
Normal file
34
static/js/vis-network.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue