Files
speedboard/routes/status.js
Malin 280e5f133f 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>
2026-04-06 19:36:13 +02:00

75 lines
1.8 KiB
JavaScript

import { Router } from 'express';
import { getJob } from '../db.js';
import { subscribe } from '../queue.js';
const router = Router();
// Status page (HTML)
router.get('/:id', (req, res) => {
const job = getJob(req.params.id);
if (!job) return res.status(404).render('error', { title: '404', message: 'Job not found.' });
// Already done — redirect immediately
if (job.status === 'done') return res.redirect(`/results/${job.id}`);
if (job.status === 'error') {
return res.render('running', { title: 'Test Failed', job, error: job.error_msg });
}
res.render('running', { title: `Testing ${job.url}`, job });
});
// SSE stream
router.get('/:id/stream', (req, res) => {
const job = getJob(req.params.id);
if (!job) {
res.status(404).end();
return;
}
// If already done, immediately emit done event
if (job.status === 'done') {
res.set({
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
res.write(`event: done\ndata: ${JSON.stringify({ jobId: job.id })}\n\n`);
res.end();
return;
}
if (job.status === 'error') {
res.set({
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
res.write(`event: error\ndata: ${JSON.stringify({ message: job.error_msg })}\n\n`);
res.end();
return;
}
res.set({
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
'X-Accel-Buffering': 'no',
});
// Keep-alive ping every 15s
const ping = setInterval(() => {
res.write(': ping\n\n');
}, 15000);
const unsubscribe = subscribe(job.id, (payload) => {
res.write(payload);
});
req.on('close', () => {
clearInterval(ping);
unsubscribe();
});
});
export default router;