// ============================================================================ // 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 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, isMobile: currentDevice === 'mobile' }) }); 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'); // Update Metrics updateMetric('score-perf', Math.round(data.scores.performance), true); updateMetric('metric-lcp', Math.round(data.metrics.lcp)); updateMetric('metric-cls', data.metrics.cls.toFixed(3)); updateMetric('metric-tbt', Math.round(data.metrics.tbt)); // 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-images').onclick = (e) => { e.preventDefault(); window.open(`/images.html?id=${data.id}`, '_blank'); }; } // 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 updateMetric(id, value, isScore = false) { const el = document.getElementById(id); el.textContent = value; if (isScore) { el.className = 'metric-value'; // Reset if (value >= 90) el.classList.add('score-good'); else if (value >= 50) el.classList.add('score-average'); else el.classList.add('score-poor'); } } function showError(msg) { const el = document.getElementById('error-msg'); el.textContent = msg; el.style.display = 'block'; } async function loadHistory() { try { const response = await fetch('/api/history', { headers: { 'x-user-uuid': getUserUuid() } }); const history = await response.json(); const container = document.getElementById('history-list'); container.innerHTML = '

Recent Tests

'; // Add comparison controls container.innerHTML += ` `; if (history.length === 0) { container.innerHTML += '

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 = `
${test.url}
${date} â€ĸ ${test.isMobile ? '📱 Mobile' : 'đŸ’ģ Desktop'}
📊 Waterfall View Report
${perfScore}
`; container.innerHTML += html; }); // Setup comparison functionality setupComparisonControls(); } catch (error) { console.error('Failed to load history', error); } } function setupComparisonControls() { const checkboxes = document.querySelectorAll('.compare-checkbox'); const controls = document.getElementById('comparison-controls'); const status = document.getElementById('comparison-status'); const compareBtn = document.getElementById('compare-btn'); const clearBtn = document.getElementById('clear-selection-btn'); function updateComparisonStatus() { const selected = Array.from(checkboxes).filter(cb => cb.checked); if (selected.length === 0) { controls.style.display = 'none'; } else { controls.style.display = 'block'; status.textContent = `${selected.length} test${selected.length > 1 ? 's' : ''} selected`; compareBtn.disabled = selected.length !== 2; if (selected.length > 2) { // Uncheck oldest selections selected[0].checked = false; updateComparisonStatus(); } } } checkboxes.forEach(cb => { cb.addEventListener('change', updateComparisonStatus); }); compareBtn.addEventListener('click', () => { const selected = Array.from(checkboxes).filter(cb => cb.checked); if (selected.length === 2) { const test1 = selected[0].dataset.testId; const test2 = selected[1].dataset.testId; window.open(`/compare.html?test1=${test1}&test2=${test2}`, '_blank'); } }); clearBtn.addEventListener('click', () => { checkboxes.forEach(cb => cb.checked = false); updateComparisonStatus(); }); } // ============================================================================ // Git Version Badge // ============================================================================ async function updateVersionBadge() { try { const response = await fetch('/api/git-info'); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); const commitIdEl = document.getElementById('commit-id'); const commitAgeEl = document.getElementById('commit-age'); if (data.error || !data.commitId) { commitIdEl.textContent = 'local'; commitAgeEl.textContent = 'dev mode'; commitIdEl.style.color = 'var(--color-text-muted)'; } else { commitIdEl.textContent = data.commitId; commitAgeEl.textContent = data.commitAge; commitIdEl.style.color = 'var(--color-accent-success)'; } } catch (error) { console.error('Failed to fetch git info:', error); const commitIdEl = document.getElementById('commit-id'); const commitAgeEl = document.getElementById('commit-age'); commitIdEl.textContent = 'local'; commitAgeEl.textContent = 'dev mode'; commitIdEl.style.color = 'var(--color-text-tertiary)'; } } // ============================================================================ // Identity Management // ============================================================================ function getUserUuid() { let uuid = localStorage.getItem('user-uuid'); if (!uuid) { uuid = 'user_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); localStorage.setItem('user-uuid', uuid); } return uuid; } // Multi-run progress polling async function pollSuiteStatus(suiteId, totalRuns) { const pollInterval = setInterval(async () => { try { const response = await fetch(`/api/suite-status/${suiteId}`); const suite = await response.json(); // Update progress const progress = (suite.completed_runs / suite.run_count) * 100; document.getElementById('progress-fill').style.width = `${progress}%`; document.getElementById('progress-text').textContent = `Run ${suite.completed_runs} of ${suite.run_count} completed...`; // Check if complete if (suite.status === 'completed') { clearInterval(pollInterval); document.getElementById('progress-text').textContent = 'Calculating statistics...'; // Display multi-run results displayMultiRunResults(suite); document.getElementById('multi-run-progress').style.display = 'none'; } else if (suite.status === 'failed') { clearInterval(pollInterval); throw new Error('Some test runs failed'); } } catch (error) { clearInterval(pollInterval); console.error('Polling error:', error); document.getElementById('error-msg').textContent = 'Error tracking progress: ' + error.message; document.getElementById('error-msg').style.display = 'block'; document.getElementById('multi-run-progress').style.display = 'none'; } }, 2000); // Poll every 2 seconds } function displayMultiRunResults(suite) { // Show statistics summary const statsHtml = `
${suite.median_performance_score?.toFixed(0) || 'N/A'}
Median Performance
${suite.avg_performance_score?.toFixed(0) || 'N/A'}
Average Performance
Âą${suite.stddev_performance_score?.toFixed(1) || 'N/A'}
Std Deviation

Individual Runs

${suite.runs?.map(run => ` `).join('') || ''}
Run Performance LCP (ms) CLS TBT (ms) Actions
#${run.run_number} ${run.is_median ? '⭐ Median' : ''} - - - - View Details
No run data available
`; document.getElementById('results-area').innerHTML = statsHtml; document.getElementById('results-area').style.display = 'block'; } function getUserUuid() { let uuid = localStorage.getItem('user_uuid'); if (!uuid) { uuid = crypto.randomUUID(); localStorage.setItem('user_uuid', uuid); } return uuid; } async function loadOptimizations(testId) { try { const response = await fetch(`/reports/${testId}.optimizations.json`); if (!response.ok) throw new Error('Optimizations not found'); const data = await response.json(); const container = document.getElementById('optimization-checklist'); const scoreEl = document.getElementById('optimization-score'); const itemsEl = document.getElementById('optimization-items'); // Display score const score = data.summary.score; scoreEl.textContent = `${score}%`; scoreEl.style.color = score >= 80 ? '#4CAF50' : score >= 50 ? '#FFC107' : '#F44336'; // Display checks let html = ''; data.checks.forEach(check => { const icon = check.status === 'error' ? '❌' : check.status === 'warning' ? 'âš ī¸' : 'â„šī¸'; const color = check.status === 'error' ? '#F44336' : check.status === 'warning' ? '#FFC107' : '#2196F3'; html += `
${icon} ${check.title}
${check.description}
${check.savings ? `
Potential savings: ${(check.savings / 1000).toFixed(1)}s
` : ''}
`; }); if (data.checks.length === 0) { html = '

✅ All optimization checks passed!

'; } itemsEl.innerHTML = html; container.style.display = 'block'; } catch (error) { console.error('Failed to load optimizations:', error); } } // 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); });