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:
2026-04-06 19:36:13 +02:00
commit 280e5f133f
28 changed files with 1222 additions and 0 deletions

68
runner.js Normal file
View File

@@ -0,0 +1,68 @@
import { spawn } from 'child_process';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
// Path to sitespeed.io — works both locally and in Docker
const SITESPEED_BIN = process.env.SITESPEED_BIN ||
join(__dirname, '..', 'sitespeed.io', 'bin', 'sitespeed.js');
export function runTest(job, onLine) {
return new Promise((resolve, reject) => {
const outputFolder = join(__dirname, 'reports', job.id);
const args = [
SITESPEED_BIN,
job.url,
'--browser', job.browser,
'-n', String(job.runs),
'--outputFolder', outputFolder,
'--json',
'--sustainable.enable',
'--axe.enable',
'--coach',
'--headless',
];
if (job.mobile) {
args.push('--mobile');
}
// In Docker we need --xvfb false and --browsertime.chrome.args=no-sandbox
if (process.env.IN_DOCKER) {
args.push('--browsertime.chrome.args', 'no-sandbox');
args.push('--browsertime.chrome.args', 'disable-dev-shm-usage');
}
onLine(`[runner] Starting: node ${args.slice(0, 3).join(' ')} ...`);
onLine(`[runner] Output folder: ${outputFolder}`);
const child = spawn('node', args, {
cwd: __dirname,
env: { ...process.env, DISPLAY: process.env.DISPLAY || ':99' },
});
child.stdout.on('data', (data) => {
const lines = data.toString().split('\n').filter(Boolean);
for (const line of lines) onLine(line);
});
child.stderr.on('data', (data) => {
const lines = data.toString().split('\n').filter(Boolean);
for (const line of lines) onLine(`[stderr] ${line}`);
});
child.on('close', (code) => {
if (code === 0) {
resolve(outputFolder);
} else {
reject(new Error(`sitespeed.io exited with code ${code}`));
}
});
child.on('error', (err) => {
reject(new Error(`Failed to spawn sitespeed.io: ${err.message}`));
});
});
}