Update static/js/looking_glass.js
This commit is contained in:
parent
b1b1ddace8
commit
e47523c461
1 changed files with 248 additions and 190 deletions
|
|
@ -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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
||||||
|
|
||||||
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, "&").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 `<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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue