feat: initial Speedboard implementation
sitespeed.io web UI with Express/Pug/SQLite — port 3132. Includes job queue, SSE live log, full metrics dashboard, site history, CO2/axe/CWV sections, and Docker support. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
78
views/site.pug
Normal file
78
views/site.pug
Normal file
@@ -0,0 +1,78 @@
|
||||
extends layout
|
||||
|
||||
block content
|
||||
div(class='space-y-6')
|
||||
div(class='flex items-start justify-between gap-4 flex-wrap')
|
||||
div
|
||||
h1(class='text-xl font-bold break-all')= url
|
||||
p(class='text-sm text-gray-400 mt-1') #{jobs.length} completed test(s) on record
|
||||
a(href=`/?url=${encodeURIComponent(url)}` class='bg-indigo-600 text-white text-sm px-4 py-2 rounded hover:bg-indigo-700') + Retest
|
||||
|
||||
if jobs.length === 0
|
||||
div(class='text-center py-16 text-gray-400') No completed tests found for this URL.
|
||||
|
||||
else
|
||||
//- LCP trend chart
|
||||
div(class='bg-white border border-gray-200 rounded-xl p-5')
|
||||
h2(class='text-base font-semibold mb-3') LCP Trend (ms)
|
||||
canvas#lcpChart(height='80')
|
||||
|
||||
//- Timeline table
|
||||
div(class='overflow-x-auto')
|
||||
table(class='w-full bg-white border border-gray-200 rounded-xl overflow-hidden text-sm')
|
||||
thead(class='bg-gray-50 text-gray-500 text-xs uppercase')
|
||||
tr
|
||||
th(class='px-4 py-3 text-left') Date
|
||||
th(class='px-4 py-3 text-center') Browser
|
||||
th(class='px-4 py-3 text-right') LCP
|
||||
th(class='px-4 py-3 text-right') FCP
|
||||
th(class='px-4 py-3 text-right') TBT
|
||||
th(class='px-4 py-3 text-right') Speed Index
|
||||
th(class='px-4 py-3 text-right') Perf Score
|
||||
th(class='px-4 py-3 text-right') Transfer
|
||||
th
|
||||
tbody
|
||||
each job in jobs
|
||||
tr(class='border-t border-gray-100 hover:bg-gray-50')
|
||||
td(class='px-4 py-3 text-gray-500 text-xs')= new Date(job.finished_at).toLocaleString()
|
||||
td(class='px-4 py-3 text-center text-gray-600 text-xs')= job.browser + (job.mobile ? ' 📱' : '')
|
||||
td(class='px-4 py-3 text-right')= job.lcp ? (job.lcp/1000).toFixed(2)+'s' : '—'
|
||||
td(class='px-4 py-3 text-right')= job.fcp ? (job.fcp/1000).toFixed(2)+'s' : '—'
|
||||
td(class='px-4 py-3 text-right')= job.tbt ? job.tbt.toFixed(0)+'ms' : '—'
|
||||
td(class='px-4 py-3 text-right')= job.speed_index ? job.speed_index.toFixed(0) : '—'
|
||||
td(class='px-4 py-3 text-right')
|
||||
if job.score_performance
|
||||
span(class=`font-bold ${job.score_performance >= 90 ? 'text-green-600' : job.score_performance >= 50 ? 'text-yellow-600' : 'text-red-600'}`)
|
||||
= job.score_performance
|
||||
else
|
||||
= '—'
|
||||
td(class='px-4 py-3 text-right text-xs')
|
||||
= job.transfer_total ? (job.transfer_total/1024).toFixed(0)+'KB' : '—'
|
||||
td(class='px-4 py-3')
|
||||
a(href=`/results/${job.id}` class='text-indigo-600 hover:underline text-xs') View
|
||||
|
||||
script(src='https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js')
|
||||
script.
|
||||
const jobs = !{JSON.stringify(jobs.map(j => ({ date: j.finished_at, lcp: j.lcp })))};
|
||||
const labels = jobs.map(j => new Date(j.date).toLocaleDateString()).reverse();
|
||||
const data = jobs.map(j => j.lcp || null).reverse();
|
||||
new Chart(document.getElementById('lcpChart'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{
|
||||
label: 'LCP (ms)',
|
||||
data,
|
||||
borderColor: '#6366f1',
|
||||
backgroundColor: 'rgba(99,102,241,0.1)',
|
||||
tension: 0.3,
|
||||
fill: true,
|
||||
pointRadius: 4,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: { y: { beginAtZero: true } }
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user