// Speed test functionality class MediaFlowSpeedTest { constructor() { this.config = null; this.results = { proxy: {}, direct: {} }; this.servers = []; this.currentTestIndex = 0; this.totalTests = 0; this.charts = {}; this.testCancelled = false; this.selectedCdns = new Set(); this.activeAbortControllers = new Set(); this.initializeEventListeners(); this.initializeForm(); this.setupResizeHandler(); } initializeEventListeners() { document.getElementById('configForm').addEventListener('submit', (e) => { e.preventDefault(); this.startSpeedTest(); }); document.getElementById('provider').addEventListener('change', (e) => { const apiKeySection = document.getElementById('apiKeySection'); if (e.target.value === 'all_debrid') { apiKeySection.classList.remove('hidden'); } else { apiKeySection.classList.add('hidden'); } // Clear CDN selection when provider changes this.config = null; this.selectedCdns.clear(); this.showPlaceholderCdnSelection(); this.updateCdnButtonStates(); }); document.getElementById('addServerBtn').addEventListener('click', () => { this.addServerInput(); }); document.getElementById('runAgainBtn').addEventListener('click', () => { this.resetTest(); }); document.getElementById('cancelTestBtn').addEventListener('click', () => { this.cancelTest(); }); document.getElementById('selectAllCdn').addEventListener('click', () => { this.selectAllCdns(true); }); document.getElementById('selectNoneCdn').addEventListener('click', () => { this.selectAllCdns(false); }); document.getElementById('refreshCdnBtn').addEventListener('click', async () => { await this.refreshCdnLocations(); }); // Handle server removal document.addEventListener('click', (e) => { if (e.target.classList.contains('remove-server')) { e.target.closest('.server-input').remove(); this.updateRemoveButtons(); } }); } initializeForm() { // Set current URL as default MediaFlow URL const currentUrl = new URL(window.location.href); const baseUrl = `${currentUrl.protocol}//${currentUrl.host}`; const firstServerUrl = document.querySelector('.server-url'); firstServerUrl.value = baseUrl; firstServerUrl.placeholder = `${baseUrl} (Current Instance)`; // Show placeholder CDN selection initially this.showPlaceholderCdnSelection(); this.updateCdnButtonStates(); } showPlaceholderCdnSelection() { const cdnStatusContainer = document.getElementById('cdnStatusContainer'); const cdnContainer = document.getElementById('cdnSelection'); // Clear status container cdnStatusContainer.innerHTML = ''; // Show placeholder in main CDN container cdnContainer.innerHTML = `
🌐

CDN Locations Not Loaded

Configure your debrid provider settings above, then click "🔄 Refresh CDNs" to load available locations.

Steps:
1. Select your debrid provider
2. Enter API key (if required)
3. Click "🔄 Refresh CDNs"
4. Select desired locations
5. Start speed test
`; } addServerInput() { const container = document.getElementById('serversContainer'); const serverDiv = document.createElement('div'); serverDiv.className = 'server-input grid grid-cols-1 md:grid-cols-3 gap-3 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg'; serverDiv.innerHTML = `
`; container.appendChild(serverDiv); this.updateRemoveButtons(); } updateRemoveButtons() { const serverInputs = document.querySelectorAll('.server-input'); serverInputs.forEach((input, index) => { const removeBtn = input.querySelector('.remove-server'); if (index === 0) { removeBtn.classList.add('hidden'); } else { removeBtn.classList.remove('hidden'); } }); } async startSpeedTest() { try { this.testCancelled = false; // Check if CDN configuration is loaded if (!this.config || !this.config.test_urls) { alert('Please fetch CDN locations first by clicking "🔄 Refresh CDNs" button.'); return; } // Collect server configurations this.collectServerConfigurations(); // Validate server configurations if (this.servers.length === 0) { alert('Please add at least one MediaFlow server to test.'); return; } // Validate CDN selections if (this.selectedCdns.size === 0) { alert('Please select at least one CDN location to test.'); return; } // Validate test options const testProxy = document.getElementById('testProxy').checked; const testDirect = document.getElementById('testDirect').checked; if (!testProxy && !testDirect) { alert('Please select at least one test option (Proxy or Direct).'); return; } // Calculate total tests this.calculateTotalTests(); this.showTestingView(); await this.runTests(); if (!this.testCancelled) { this.showResults(); } } catch (error) { console.error('Speed test failed:', error); if (!this.testCancelled) { alert('Speed test failed: ' + error.message); this.resetTest(); } } } collectServerConfigurations() { // Collect server configurations this.servers = []; const serverInputs = document.querySelectorAll('.server-input'); serverInputs.forEach(input => { const url = input.querySelector('.server-url').value.trim(); const name = input.querySelector('.server-name').value.trim(); const password = input.querySelector('.server-password').value.trim(); if (url) { this.servers.push({ url: url, name: name || new URL(url).host, api_password: password || null }); } }); } async refreshCdnLocations() { const refreshBtn = document.getElementById('refreshCdnBtn'); const originalText = refreshBtn.textContent; try { // Show loading state refreshBtn.textContent = '⏳ Loading...'; refreshBtn.disabled = true; // Show loading in CDN container const cdnStatusContainer = document.getElementById('cdnStatusContainer'); const cdnContainer = document.getElementById('cdnSelection'); cdnStatusContainer.innerHTML = ''; cdnContainer.innerHTML = `

Loading CDN locations...

`; await this.loadConfiguration(); this.populateCdnSelection(); // Show success message const successMsg = document.createElement('div'); successMsg.className = 'fixed top-4 right-4 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg z-50'; successMsg.textContent = `✅ Loaded ${Object.keys(this.config.test_urls).length} CDN locations`; document.body.appendChild(successMsg); setTimeout(() => { successMsg.remove(); }, 3000); } catch (error) { console.error('Failed to refresh CDN locations:', error); // Show error message const errorMsg = document.createElement('div'); errorMsg.className = 'fixed top-4 right-4 bg-red-500 text-white px-4 py-2 rounded-lg shadow-lg z-50'; errorMsg.textContent = `❌ Failed: ${error.message}`; document.body.appendChild(errorMsg); setTimeout(() => { errorMsg.remove(); }, 5000); this.showPlaceholderCdnSelection(); this.updateCdnButtonStates(); } finally { // Restore button state refreshBtn.textContent = originalText; refreshBtn.disabled = false; } } cancelTest() { this.testCancelled = true; document.getElementById('currentTest').textContent = 'Test cancelled by user'; document.getElementById('progressText').textContent = 'Cancelling...'; // Cancel all active network requests this.activeAbortControllers.forEach(controller => { try { controller.abort(); } catch (e) { console.warn('Error aborting request:', e); } }); this.activeAbortControllers.clear(); setTimeout(() => { this.resetTest(); }, 1000); } populateCdnSelection() { const cdnStatusContainer = document.getElementById('cdnStatusContainer'); const cdnContainer = document.getElementById('cdnSelection'); const locations = Object.keys(this.config.test_urls); // Initialize all CDNs as selected if none are selected if (this.selectedCdns.size === 0) { locations.forEach(location => this.selectedCdns.add(location)); } // Show success status cdnStatusContainer.innerHTML = `
CDN Locations Loaded Successfully ${locations.length} locations
`; // Populate CDN checkboxes in the grid cdnContainer.innerHTML = locations.map(location => `
`).join(''); // Add event listeners to checkboxes document.querySelectorAll('.cdn-checkbox').forEach(checkbox => { checkbox.addEventListener('change', (e) => { const location = e.target.dataset.location; this.toggleCdn(location); }); }); // Update button states this.updateCdnButtonStates(); } toggleCdn(location) { if (this.selectedCdns.has(location)) { this.selectedCdns.delete(location); } else { this.selectedCdns.add(location); } } selectAllCdns(selectAll) { if (!this.config || !this.config.test_urls) { // Show a brief message if CDNs aren't loaded const message = document.createElement('div'); message.className = 'fixed top-4 right-4 bg-yellow-500 text-white px-4 py-2 rounded-lg shadow-lg z-50'; message.textContent = '⚠️ Please load CDN locations first'; document.body.appendChild(message); setTimeout(() => message.remove(), 2000); return; } const checkboxes = document.querySelectorAll('.cdn-checkbox'); const locations = Object.keys(this.config.test_urls); if (selectAll) { this.selectedCdns = new Set(locations); checkboxes.forEach(cb => cb.checked = true); } else { this.selectedCdns.clear(); checkboxes.forEach(cb => cb.checked = false); } } updateCdnButtonStates() { const selectAllBtn = document.getElementById('selectAllCdn'); const selectNoneBtn = document.getElementById('selectNoneCdn'); const hasConfig = this.config && this.config.test_urls; if (hasConfig) { selectAllBtn.disabled = false; selectNoneBtn.disabled = false; selectAllBtn.classList.remove('opacity-50', 'cursor-not-allowed'); selectNoneBtn.classList.remove('opacity-50', 'cursor-not-allowed'); } else { selectAllBtn.disabled = true; selectNoneBtn.disabled = true; selectAllBtn.classList.add('opacity-50', 'cursor-not-allowed'); selectNoneBtn.classList.add('opacity-50', 'cursor-not-allowed'); } } async loadConfiguration() { const provider = document.getElementById('provider').value; const apiKey = document.getElementById('apiKey').value; const currentApiPassword = document.getElementById('currentApiPassword').value; // Use current MediaFlow instance for fetching CDN configuration const currentUrl = new URL(window.location.href); const baseUrl = `${currentUrl.protocol}//${currentUrl.host}`; const requestBody = { provider: provider, api_key: apiKey || null, current_api_password: currentApiPassword || null }; const headers = { 'Content-Type': 'application/json', }; // Build URL with api_password as query parameter if provided // This is more reliable than headers when behind reverse proxies let configUrl = '/speedtest/config'; if (currentApiPassword) { configUrl += `?api_password=${encodeURIComponent(currentApiPassword)}`; } const response = await fetch(configUrl, { method: 'POST', headers: headers, body: JSON.stringify(requestBody) }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Failed to load configuration'); } this.config = await response.json(); } calculateTotalTests() { // Calculate total tests based on selected CDNs and servers const testProxy = document.getElementById('testProxy').checked; const testDirect = document.getElementById('testDirect').checked; const selectedLocationCount = this.selectedCdns.size; const serverCount = this.servers.length; this.totalTests = 0; if (testProxy) this.totalTests += selectedLocationCount * serverCount; if (testDirect) this.totalTests += selectedLocationCount; } showTestingView() { document.getElementById('configView').classList.add('hidden'); document.getElementById('testingView').classList.remove('hidden'); document.getElementById('resultsView').classList.add('hidden'); // Clear previous results and any existing abort controllers this.results = {proxy: {}, direct: {}}; this.activeAbortControllers.forEach(controller => { try { controller.abort(); } catch (e) { console.warn('Error aborting request:', e); } }); this.activeAbortControllers.clear(); document.getElementById('liveResults').innerHTML = ''; } showResults() { document.getElementById('configView').classList.add('hidden'); document.getElementById('testingView').classList.add('hidden'); document.getElementById('resultsView').classList.remove('hidden'); this.renderMetrics(); this.renderCharts(); this.renderDetailedResults(); } resetTest() { this.results = {proxy: {}, direct: {}}; this.currentTestIndex = 0; this.totalTests = 0; this.testCancelled = false; // Cancel and clear any remaining abort controllers this.activeAbortControllers.forEach(controller => { try { controller.abort(); } catch (e) { console.warn('Error aborting request:', e); } }); this.activeAbortControllers.clear(); // Destroy existing charts safely Object.values(this.charts).forEach(chart => { if (chart && typeof chart.destroy === 'function') { try { chart.destroy(); } catch (e) { console.warn('Error destroying chart:', e); } } }); this.charts = {}; // Clear canvas elements ['speedChart', 'serverChart'].forEach(id => { const canvas = document.getElementById(id); if (canvas) { const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); } }); // Clear live results document.getElementById('liveResults').innerHTML = ''; document.getElementById('configView').classList.remove('hidden'); document.getElementById('testingView').classList.add('hidden'); document.getElementById('resultsView').classList.add('hidden'); } async runTests() { const testProxy = document.getElementById('testProxy').checked; const testDirect = document.getElementById('testDirect').checked; const testDuration = parseInt(document.getElementById('testDuration').value) || 10; this.currentTestIndex = 0; // Validate selected CDNs if (this.selectedCdns.size === 0) { throw new Error('Please select at least one CDN location to test'); } // Filter test URLs to only selected CDNs const selectedTestUrls = Object.fromEntries( Object.entries(this.config.test_urls).filter(([location]) => this.selectedCdns.has(location) ) ); // Run proxy tests for each server if (testProxy && !this.testCancelled) { for (const server of this.servers) { if (this.testCancelled) break; for (const [location, url] of Object.entries(selectedTestUrls)) { if (this.testCancelled) break; await this.runSingleTest(location, url, 'proxy', server, testDuration); } } } // Run direct tests if (testDirect && !this.testCancelled) { for (const [location, url] of Object.entries(selectedTestUrls)) { if (this.testCancelled) break; await this.runSingleTest(location, url, 'direct', null, testDuration); } } } async runSingleTest(location, url, testType, server, duration) { if (this.testCancelled) return; let testUrl; let testKey = location; if (testType === 'proxy') { testUrl = `${server.url.replace(/\/$/, '')}/proxy/stream?d=${encodeURIComponent(url)}`; if (server.api_password) { testUrl += `&api_password=${encodeURIComponent(server.api_password)}`; } testKey = `${location}_${server.name}`; } else { testUrl = url; } this.updateProgress(location, testType, server); try { const result = await this.measureSpeed(testUrl, duration); if (this.testCancelled) return; this.results[testType][testKey] = { ...result, server_url: url, test_url: testUrl, server_name: server ? server.name : 'Direct', location: location }; this.updateLiveResults(testKey, testType, this.results[testType][testKey]); } catch (error) { if (this.testCancelled) return; // Don't log or update UI for cancelled tests if (error.message === 'Test cancelled') { return; } console.error(`Test failed for ${location} (${testType}):`, error); this.results[testType][testKey] = { error: error.message, server_url: url, test_url: testUrl, server_name: server ? server.name : 'Direct', location: location }; this.updateLiveResults(testKey, testType, this.results[testType][testKey]); } this.currentTestIndex++; } async measureSpeed(url, duration) { const startTime = performance.now(); let totalBytes = 0; // Create AbortController for this request const abortController = new AbortController(); this.activeAbortControllers.add(abortController); try { // Check if test was cancelled before starting if (this.testCancelled) { throw new Error('Test cancelled'); } const response = await fetch(url, { method: 'GET', headers: { 'Range': 'bytes=0-' }, signal: abortController.signal }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const reader = response.body.getReader(); while (true) { // Check for cancellation in the reading loop if (this.testCancelled) { reader.cancel(); throw new Error('Test cancelled'); } const {done, value} = await reader.read(); if (done) break; totalBytes += value.length; const currentTime = performance.now(); // Check if duration exceeded if (currentTime - startTime >= duration * 1000) { reader.cancel(); break; } } // Final cancellation check before returning results if (this.testCancelled) { throw new Error('Test cancelled'); } const actualDuration = (performance.now() - startTime) / 1000; const speedMbps = (totalBytes * 8) / (actualDuration * 1_000_000); return { speed_mbps: Math.round(speedMbps * 100) / 100, duration: Math.round(actualDuration * 100) / 100, data_transferred: totalBytes, timestamp: new Date().toISOString() }; } catch (error) { // If it's an abort error and test was cancelled, don't propagate the error if (error.name === 'AbortError' && this.testCancelled) { throw new Error('Test cancelled'); } throw new Error(`Network error: ${error.message}`); } finally { // Clean up the abort controller this.activeAbortControllers.delete(abortController); } } updateProgress(location, testType, server) { const progress = this.totalTests > 0 ? (this.currentTestIndex / this.totalTests) * 100 : 0; const progressPercent = Math.min(Math.round(progress), 100); // Cap at 100% document.getElementById('progressBar').style.width = `${progressPercent}%`; document.getElementById('progressText').textContent = `${progressPercent}% complete`; const serverName = server ? server.name : 'Direct'; document.getElementById('currentTest').textContent = `Testing ${location} via ${serverName} (${testType})...`; } updateLiveResults(testKey, testType, result) { const liveResults = document.getElementById('liveResults'); let card = document.getElementById(`result-${testKey}-${testType}`); if (!card) { card = document.createElement('div'); card.id = `result-${testKey}-${testType}`; card.className = 'result-card bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4'; liveResults.appendChild(card); } const speedText = result.error ? `Error: ${result.error}` : `${result.speed_mbps} Mbps`; const badgeColor = testType === 'proxy' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' : 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200'; card.innerHTML = `

${result.location}

${result.server_name}
${speedText}
${!result.error ? `
${(result.data_transferred / 1024 / 1024).toFixed(2)} MB in ${result.duration}s
` : ''} `; } renderMetrics() { const proxyResults = Object.values(this.results.proxy).filter(r => !r.error); const directResults = Object.values(this.results.direct).filter(r => !r.error); // Find best proxy result const bestProxyResult = proxyResults.length > 0 ? proxyResults.reduce((best, current) => current.speed_mbps > best.speed_mbps ? current : best) : null; // Find best direct result const bestDirectResult = directResults.length > 0 ? directResults.reduce((best, current) => current.speed_mbps > best.speed_mbps ? current : best) : null; const bestProxySpeed = bestProxyResult ? bestProxyResult.speed_mbps : 0; const bestDirectSpeed = bestDirectResult ? bestDirectResult.speed_mbps : 0; // Calculate averages const avgProxySpeed = proxyResults.length > 0 ? proxyResults.reduce((sum, r) => sum + r.speed_mbps, 0) / proxyResults.length : 0; // Speed difference based on best speeds (more relevant) const speedDifference = bestDirectSpeed > 0 ? ((bestProxySpeed - bestDirectSpeed) / bestDirectSpeed * 100) : 0; // Update metrics document.getElementById('bestProxySpeed').textContent = `${bestProxySpeed.toFixed(2)} Mbps`; document.getElementById('bestDirectSpeed').textContent = `${bestDirectSpeed.toFixed(2)} Mbps`; document.getElementById('avgProxySpeed').textContent = `${avgProxySpeed.toFixed(2)} Mbps`; document.getElementById('speedDifference').textContent = `${speedDifference >= 0 ? '+' : ''}${speedDifference.toFixed(1)}%`; // Update additional info document.getElementById('bestProxyServer').textContent = bestProxyResult ? `${bestProxyResult.server_name} - ${bestProxyResult.location}` : '--'; document.getElementById('bestDirectLocation').textContent = bestDirectResult ? bestDirectResult.location : '--'; document.getElementById('proxyTestCount').textContent = `${proxyResults.length} tests`; // Update metric card colors based on performance const bestProxyMetric = document.getElementById('bestProxyMetric'); const speedDiffMetric = document.getElementById('speedDiffMetric'); // Reset classes bestProxyMetric.className = 'metric-card text-white p-4 rounded-lg text-center'; speedDiffMetric.className = 'metric-card text-white p-4 rounded-lg text-center'; if (bestProxySpeed >= bestDirectSpeed * 0.8) { bestProxyMetric.className += ' success'; } else if (bestProxySpeed >= bestDirectSpeed * 0.5) { bestProxyMetric.className += ' warning'; } if (speedDifference >= -20) { speedDiffMetric.className += ' success'; } else { speedDiffMetric.className += ' warning'; } // Render server comparison metrics this.renderServerMetrics(); } renderServerMetrics() { const serverComparisonGrid = document.getElementById('serverComparisonGrid'); const proxyResults = Object.values(this.results.proxy).filter(r => !r.error); // Group results by server const serverStats = {}; proxyResults.forEach(result => { if (!serverStats[result.server_name]) { serverStats[result.server_name] = []; } serverStats[result.server_name].push(result); }); const serverMetrics = Object.entries(serverStats).map(([serverName, results]) => { const speeds = results.map(r => r.speed_mbps); const avgSpeed = speeds.reduce((sum, speed) => sum + speed, 0) / speeds.length; const bestSpeed = Math.max(...speeds); const testCount = results.length; const bestLocation = results.find(r => r.speed_mbps === bestSpeed)?.location || '--'; return { name: serverName, avgSpeed, bestSpeed, testCount, bestLocation }; }).sort((a, b) => b.bestSpeed - a.bestSpeed); serverComparisonGrid.innerHTML = serverMetrics.map((server, index) => { const rankClass = index === 0 ? 'border-green-500 bg-green-50 dark:bg-green-900/20' : index === 1 ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20' : 'border-gray-300 dark:border-gray-600'; const rankIcon = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : `#${index + 1}`; return `

${server.name}

${rankIcon}
Best Speed: ${server.bestSpeed.toFixed(2)} Mbps
Avg Speed: ${server.avgSpeed.toFixed(2)} Mbps
Best Location: ${server.bestLocation}
Tests: ${server.testCount}
`; }).join(''); if (serverMetrics.length === 0) { serverComparisonGrid.innerHTML = `
No proxy test results available
`; } } renderCharts() { const isDark = html.classList.contains('dark'); const textColor = isDark ? '#e5e7eb' : '#374151'; const gridColor = isDark ? '#374151' : '#e5e7eb'; // Add a small delay to ensure DOM is ready setTimeout(() => { // Speed Comparison Chart this.renderSpeedChart(textColor, gridColor); // Server Performance Chart this.renderServerChart(textColor, gridColor); }, 100); } setupResizeHandler() { let resizeTimeout; window.addEventListener('resize', () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { if (document.getElementById('resultsView').classList.contains('hidden')) { return; // Don't resize if results view is not visible } this.renderCharts(); }, 250); }); } renderSpeedChart(textColor, gridColor) { const canvas = document.getElementById('speedChart'); const ctx = canvas.getContext('2d'); if (this.charts.speedChart) { this.charts.speedChart.destroy(); } // Ensure proper canvas sizing const container = canvas.parentElement; const containerRect = container.getBoundingClientRect(); canvas.style.width = '100%'; canvas.style.height = '100%'; const locations = [...new Set([ ...Object.values(this.results.proxy).map(r => r.location), ...Object.values(this.results.direct).map(r => r.location) ])].filter(Boolean); if (locations.length === 0) { return; } // Group proxy results by server and location const serverNames = [...new Set(Object.values(this.results.proxy).map(r => r.server_name))]; // Create datasets for each server + direct const datasets = []; // Color palette for different servers const colors = [ {bg: 'rgba(59, 130, 246, 0.8)', border: 'rgba(59, 130, 246, 1)'}, // Blue {bg: 'rgba(16, 185, 129, 0.8)', border: 'rgba(16, 185, 129, 1)'}, // Green {bg: 'rgba(245, 158, 11, 0.8)', border: 'rgba(245, 158, 11, 1)'}, // Yellow {bg: 'rgba(239, 68, 68, 0.8)', border: 'rgba(239, 68, 68, 1)'}, // Red {bg: 'rgba(168, 85, 247, 0.8)', border: 'rgba(168, 85, 247, 1)'}, // Purple ]; // Add datasets for each server serverNames.forEach((serverName, index) => { const color = colors[index % colors.length]; const data = locations.map(location => { const result = Object.values(this.results.proxy).find(r => r.location === location && r.server_name === serverName && !r.error ); return result ? result.speed_mbps : 0; }); datasets.push({ label: `${serverName} (Proxy)`, data: data, backgroundColor: color.bg, borderColor: color.border, borderWidth: 1 }); }); // Add direct speed dataset const directData = locations.map(location => { const result = this.results.direct[location]; return result && !result.error ? result.speed_mbps : 0; }); datasets.push({ label: 'Direct Connection', data: directData, backgroundColor: 'rgba(107, 114, 128, 0.8)', borderColor: 'rgba(107, 114, 128, 1)', borderWidth: 1 }); this.charts.speedChart = new Chart(ctx, { type: 'bar', data: { labels: locations, datasets: datasets }, options: { responsive: true, maintainAspectRatio: false, interaction: { intersect: false, }, plugins: { legend: { labels: { color: textColor, usePointStyle: true, padding: 15, font: { size: 12 } } } }, scales: { y: { beginAtZero: true, title: { display: true, text: 'Speed (Mbps)', color: textColor }, ticks: { color: textColor, maxTicksLimit: 8 }, grid: { color: gridColor } }, x: { ticks: { color: textColor, maxRotation: 45 }, grid: { color: gridColor } } } } }); } renderServerChart(textColor, gridColor) { const canvas = document.getElementById('serverChart'); const ctx = canvas.getContext('2d'); if (!ctx) { console.error('Failed to get canvas context'); return; } if (this.charts.serverChart) { this.charts.serverChart.destroy(); } // Ensure proper canvas sizing const container = canvas.parentElement; const containerRect = container.getBoundingClientRect(); canvas.style.width = '100%'; canvas.style.height = '100%'; // Group results by server const serverStats = {}; Object.values(this.results.proxy).forEach(result => { if (!result.error && result.server_name) { if (!serverStats[result.server_name]) { serverStats[result.server_name] = []; } serverStats[result.server_name].push(result.speed_mbps); } }); const serverNames = Object.keys(serverStats); if (serverNames.length === 0) { // Show a message for no data ctx.fillStyle = textColor; ctx.font = '16px Arial'; ctx.textAlign = 'center'; ctx.fillText('No server data available', canvas.width / 2, canvas.height / 2); return; } // Use a bar chart instead of radar for better clarity const avgSpeeds = serverNames.map(name => { const speeds = serverStats[name]; return speeds.reduce((sum, speed) => sum + speed, 0) / speeds.length; }); const maxSpeeds = serverNames.map(name => Math.max(...serverStats[name])); const minSpeeds = serverNames.map(name => Math.min(...serverStats[name])); this.charts.serverChart = new Chart(ctx, { type: 'bar', data: { labels: serverNames, datasets: [ { label: 'Best Speed', data: maxSpeeds, backgroundColor: 'rgba(34, 197, 94, 0.8)', borderColor: 'rgba(34, 197, 94, 1)', borderWidth: 1, order: 1 }, { label: 'Average Speed', data: avgSpeeds, backgroundColor: 'rgba(59, 130, 246, 0.8)', borderColor: 'rgba(59, 130, 246, 1)', borderWidth: 1, order: 2 }, { label: 'Worst Speed', data: minSpeeds, backgroundColor: 'rgba(239, 68, 68, 0.8)', borderColor: 'rgba(239, 68, 68, 1)', borderWidth: 1, order: 3 } ] }, options: { responsive: true, maintainAspectRatio: false, interaction: { intersect: false, }, plugins: { legend: { labels: { color: textColor, usePointStyle: true, padding: 15, font: { size: 12 } } }, tooltip: { callbacks: { afterLabel: function (context) { const serverName = context.label; const speeds = serverStats[serverName]; return `Tests: ${speeds.length}`; } } } }, scales: { y: { beginAtZero: true, title: { display: true, text: 'Speed (Mbps)', color: textColor }, ticks: { color: textColor, maxTicksLimit: 8 }, grid: { color: gridColor } }, x: { ticks: { color: textColor, maxRotation: 45 }, grid: { color: gridColor } } } } }); } renderDetailedResults() { const detailedResults = document.getElementById('detailedResults'); const locations = [...new Set([ ...Object.values(this.results.proxy).map(r => r.location), ...Object.values(this.results.direct).map(r => r.location) ])].filter(Boolean); detailedResults.innerHTML = locations.map(location => { const proxyResults = Object.values(this.results.proxy).filter(r => r.location === location); const directResult = this.results.direct[location]; return `

${location}

${proxyResults.map(result => `
${result.server_name}
${result.error ? `
Error: ${result.error}
` : `
${result.speed_mbps} Mbps
${(result.data_transferred / 1024 / 1024).toFixed(2)} MB in ${result.duration}s
`}
`).join('')} ${directResult ? `
Direct
${directResult.error ? `
Error: ${directResult.error}
` : `
${directResult.speed_mbps} Mbps
${(directResult.data_transferred / 1024 / 1024).toFixed(2)} MB in ${directResult.duration}s
`}
` : '
Direct test not performed
'}
`; }).join(''); } } // Initialize the speed test when the page loads let speedTest; document.addEventListener('DOMContentLoaded', () => { speedTest = new MediaFlowSpeedTest(); });