// ============================================================================ // State & Config // ============================================================================ let currentDevice = 'desktop'; // ============================================================================ // UI Functions // ============================================================================ function setDevice(device) { currentDevice = device; document.querySelectorAll('.toggle-option').forEach(el => { el.classList.toggle('active', el.dataset.value === device); }); } function setLoading(isLoading) { const btn = document.getElementById('run-btn'); const spinner = document.getElementById('loading-spinner'); const btnText = btn.querySelector('span'); if (isLoading) { btn.disabled = true; spinner.style.display = 'block'; btnText.textContent = 'Running Test...'; } else { btn.disabled = false; spinner.style.display = 'none'; btnText.textContent = 'Run Test'; } } // ============================================================================ // API Handlers // ============================================================================ async function runTest() { console.log('Run Test triggered'); const urlInput = document.getElementById('test-url'); const url = urlInput.value.trim(); const captureFilmstrip = document.getElementById('capture-filmstrip')?.checked || false; const errorMsg = document.getElementById('error-msg'); const resultsArea = document.getElementById('results-area'); if (!url) { showError('Please enter a valid URL'); return; } try { new URL(url); } catch { showError('Invalid URL format (include http:// or https://)'); return; } // Reset UI errorMsg.style.display = 'none'; resultsArea.classList.remove('visible'); setLoading(true); try { const response = await fetch('/api/run-test', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-user-uuid': getUserUuid() }, body: JSON.stringify({ url: url, url: url, isMobile: currentDevice === 'mobile', captureFilmstrip: captureFilmstrip }) }); if (!response.ok) throw new Error('Test failed to start'); const data = await response.json(); displayResults(data); loadHistory(); // Refresh history } catch (error) { console.error(error); showError('Test execution failed. Check console for details.'); } finally { setLoading(false); } } function displayResults(data) { const resultsArea = document.getElementById('results-area'); // Calculate grades using grades.js if available, otherwise simplified logic let overallGrade = 'F'; let structureScore = 50; if (typeof calculateAllGrades === 'function') { // Assume grades.js available const grades = calculateAllGrades(data.metrics); // Map average score to grade? Simplified: // Use Performance Score as primary grade driver } const perfScore = Math.round(data.scores.performance); overallGrade = perfScore >= 90 ? 'A' : perfScore >= 80 ? 'B' : perfScore >= 70 ? 'C' : perfScore >= 60 ? 'D' : 'F'; // Structure Score (Average of non-perf categories) structureScore = Math.round(( (data.scores.seo || 0) + (data.scores.bestPractices || data.scores['best-practices'] || 0) + (data.scores.accessibility || 0) ) / 3); // Update Dashboard UI const gradeCircle = document.getElementById('overall-grade'); const gradeLetter = gradeCircle.querySelector('.grade-letter'); // Animate Grade gradeLetter.textContent = overallGrade; gradeCircle.className = 'grade-circle grade-' + overallGrade.toLowerCase(); document.getElementById('performance-score').textContent = perfScore + '%'; document.getElementById('structure-score').textContent = structureScore + '%'; // Web Vitals const lcpVal = data.metrics.lcp < 1000 ? (data.metrics.lcp/1000).toFixed(2) + 's' : Math.round(data.metrics.lcp) + 'ms'; const tbtVal = Math.round(data.metrics.tbt) + 'ms'; const clsVal = data.metrics.cls.toFixed(2); document.getElementById('vital-lcp').textContent = lcpVal; document.getElementById('vital-tbt').textContent = tbtVal; document.getElementById('vital-cls').textContent = clsVal; // Display Filmstrip if (data.filmstrip && data.filmstrip.length > 0) { displayFilmstrip(data.filmstrip); } else { document.getElementById('filmstrip-section').style.display = 'none'; } // Remove existing actions if any const existingActions = resultsArea.querySelector('.report-actions'); if (existingActions) existingActions.remove(); // Add Report Button const actionsDiv = document.createElement('div'); actionsDiv.className = 'report-actions'; actionsDiv.innerHTML = ` đ View Full Report `; resultsArea.appendChild(actionsDiv); // Show waterfall link const waterfallContainer = document.getElementById('waterfall-link-container'); if (waterfallContainer) { waterfallContainer.style.display = 'block'; document.getElementById('view-waterfall').onclick = (e) => { e.preventDefault(); window.open(`/waterfall.html?id=${data.id}`, '_blank'); }; document.getElementById('view-video').onclick = (e) => { e.preventDefault(); if (data.filmstrip && data.filmstrip.length > 0) { openVideoModal(data.filmstrip); } else { alert('No video data available for this test.'); } }; document.getElementById('view-images').onclick = (e) => { e.preventDefault(); if (data.filmstrip && data.filmstrip.length > 0) { // Open images.html?id=... (Assuming logic exists, user requested it back) // Or reusing filmstrip display? User said "View Images function". // Checking previous code: it opened `/images.html?id=${data.id}` window.open(`/images.html?id=${data.id}`, '_blank'); } else { alert('No images available.'); } }; } // Load content breakdown if (typeof renderContentBreakdown === 'function') { renderContentBreakdown(data.id); } // Load and display optimizations loadOptimizations(data.id); // Wire export buttons document.getElementById('export-buttons').style.display = 'block'; document.getElementById('export-har').href = `/api/export/${data.id}/har`; document.getElementById('export-csv').href = `/api/export/${data.id}/csv`; resultsArea.classList.add('visible'); // Scroll to results resultsArea.scrollIntoView({ behavior: 'smooth' }); } function displayFilmstrip(items) { const section = document.getElementById('filmstrip-section'); const container = document.getElementById('filmstrip-container'); section.style.display = 'block'; // Filter/Sample items if too many const frames = items; container.innerHTML = frames.map(frame => `
No tests run yet.
'; return; } history.slice(0, 10).forEach(test => { const date = new Date(test.timestamp).toLocaleString(); const perfScore = Math.round(test.scores.performance); const colorClass = perfScore >= 90 ? 'score-good' : (perfScore >= 50 ? 'score-average' : 'score-poor'); const html = `| Run | Performance | LCP (ms) | CLS | TBT (ms) | Actions |
|---|---|---|---|---|---|
| #${run.run_number} ${run.is_median ? 'â Median' : ''} | - | - | - | - | View Details |
| No run data available | |||||
â All optimization checks passed!
'; } itemsEl.innerHTML = html; container.style.display = 'block'; } catch (error) { console.error('Failed to load optimizations:', error); } } // Video Player State let videoFrames = []; let isPlaying = false; let currentFrameIndex = 0; let videoInterval = null; function openVideoModal(frames) { if (!frames || frames.length === 0) return; videoFrames = frames; currentFrameIndex = 0; isPlaying = false; document.getElementById('video-modal').style.display = 'block'; updateVideoFrame(); } function closeVideoModal() { stopVideo(); document.getElementById('video-modal').style.display = 'none'; } function toggleVideoPlay() { if (isPlaying) { stopVideo(); } else { playVideo(); } } function playVideo() { if (isPlaying) return; isPlaying = true; document.getElementById('video-play-btn').textContent = '⸠Pause'; if (currentFrameIndex >= videoFrames.length - 1) { currentFrameIndex = 0; } videoInterval = setInterval(() => { currentFrameIndex++; if (currentFrameIndex >= videoFrames.length) { stopVideo(); return; } updateVideoFrame(); }, 100); // 10fps } function stopVideo() { isPlaying = false; document.getElementById('video-play-btn').textContent = 'âļ Play'; if (videoInterval) clearInterval(videoInterval); } function updateVideoFrame() { const frame = videoFrames[currentFrameIndex]; document.getElementById('video-img').src = frame.data; document.getElementById('video-time').textContent = (frame.timing / 1000).toFixed(1) + 's'; const progress = ((currentFrameIndex + 1) / videoFrames.length) * 100; document.getElementById('video-progress-fill').style.width = `${progress}%`; } // Initialization document.addEventListener('DOMContentLoaded', () => { // Ensure we have an identity const userUuid = getUserUuid(); console.log('User Identity:', userUuid); updateVersionBadge(); loadHistory(); // Attach event listener programmatically const runBtn = document.getElementById('run-btn'); if (runBtn) { runBtn.addEventListener('click', runTest); console.log('Run Test button listener attached'); } else { console.error('Run Test button not found'); } // Auto-refresh Git badge setInterval(updateVersionBadge, 5 * 60 * 1000); }); // ============================================================================ // Extra Features (Diagnostics & Bulk) // ============================================================================ function toggleSection(id) { const el = document.getElementById(id); el.style.display = el.style.display === 'none' ? 'block' : 'none'; } async function runTraceroute() { const host = document.getElementById('trace-host').value; const out = document.getElementById('trace-output'); if (!host) return; out.style.display = 'block'; out.textContent = 'Running traceroute...'; try { const res = await fetch(`/api/traceroute?host=${host}`); const data = await res.json(); out.textContent = data.output; } catch (e) { out.textContent = 'Error: ' + e.message; } } async function runBulkTest() { const text = document.getElementById('bulk-urls').value; const urls = text.split('\n').map(u => u.trim()).filter(u => u); if (urls.length === 0) { alert('No URLs provided'); return; } const progress = document.getElementById('bulk-progress'); progress.innerHTML = `Starting batch of ${urls.length} tests...`; // Simple Frontend orchestration for (let i = 0; i < urls.length; i++) { const url = urls[i]; progress.innerHTML += `