diff --git a/server/src/database/index.js b/server/src/database/index.js index 9abad80..55a5668 100644 --- a/server/src/database/index.js +++ b/server/src/database/index.js @@ -224,6 +224,24 @@ export async function getTestHar(id) { } } +/** + * Most-recent failed test runs within the last 24 h, ordered newest-first. + * Bounded to the same 24 h window as the health pill so the table and the + * pill always agree on what counts as "recent" — unlike Bull's retained- + * failures list, which accumulates indefinitely and never decays. + */ +export async function getRecentFailures(limit) { + const select = + "SELECT id, location, url, scripting_name, label, failed_reason, finished_date FROM sitespeed_io_test_runs WHERE status = 'failed' AND finished_date >= NOW() - INTERVAL '24 hours' ORDER BY finished_date DESC LIMIT $1"; + try { + const result = await DatabaseHelper.getInstance().query(select, [limit]); + return result.rows; + } catch (error) { + logError('Could not get recent failures', error); + return []; + } +} + // Cheap one-shot DB ping for the admin health banner. Unlike // testConnection() (which retries with a 5s delay and is meant for // startup), this returns false fast on any failure so it won't stall diff --git a/server/src/routes/html/admin/index.js b/server/src/routes/html/admin/index.js index 9904163..e247c62 100644 --- a/server/src/routes/html/admin/index.js +++ b/server/src/routes/html/admin/index.js @@ -8,12 +8,14 @@ import { getExistingQueueNames, getQueueCounts, getActiveJobs, - getFailedJobs, - retryFailedJob, isRedisHealthy } from '../../../queuehandler.js'; import { getTestRunners } from '../../../testrunners.js'; -import { isDatabaseHealthy, getStatistics } from '../../../database/index.js'; +import { + isDatabaseHealthy, + getStatistics, + getRecentFailures +} from '../../../database/index.js'; const require = createRequire(import.meta.url); const serverVersion = require('../../../../package.json').version; @@ -86,35 +88,23 @@ async function buildAdminView() { } activeJobs.sort((a, b) => (a.startedAt || 0) - (b.startedAt || 0)); - // Recent failures across non-internal queues. Bull's `failedReason` is - // the first line of whatever the testrunner threw. Truncate it for the - // table so a 4-line stack trace doesn't blow up the layout; the full - // log is one click away on /result/. - const failedJobs = []; - for (const queueName of queues) { - if (INTERNAL_QUEUES.has(queueName)) continue; - const jobs = await getFailedJobs(queueName, 20); - for (const job of jobs) { - const finishedAt = job.finishedOn; - let reason = (job.failedReason || '').split('\n')[0].trim(); - if (reason.length > 160) reason = reason.slice(0, 157) + '…'; - failedJobs.push({ - id: String(job.id), - queue: queueName, - target: - job.data?.scriptingName || job.data?.url || job.data?.label || '', - reason, - attemptsMade: job.attemptsMade || 0, - finishedAt, - secondsAgo: finishedAt - ? Math.max(0, Math.round((now - finishedAt) / 1000)) - : undefined - }); - } - } - failedJobs.sort((a, b) => (b.finishedAt || 0) - (a.finishedAt || 0)); - // Show at most this many across all queues so the page stays scannable. - const recentFailures = failedJobs.slice(0, 25); + const failureRows = await getRecentFailures(25); + const recentFailures = failureRows.map(row => { + let reason = (row.failed_reason || '').split('\n')[0].trim(); + if (reason.length > 160) reason = reason.slice(0, 157) + '…'; + const finishedAt = row.finished_date + ? new Date(row.finished_date).getTime() + : undefined; + return { + id: String(row.id), + target: row.scripting_name || row.url || row.label || '', + reason, + finishedAt, + secondsAgo: finishedAt + ? Math.max(0, Math.round((now - finishedAt) / 1000)) + : undefined + }; + }); // Pull DB-backed activity stats first — the health banner uses the // 24 h failed count from here. The helper itself caches for 60s so @@ -207,15 +197,3 @@ admin.post('/', async function (request, response) { response.set('Cache-Control', 'no-store'); renderAdmin(response, await buildAdminView()); }); - -// Re-enqueue a failed job. Bull's job.retry() pushes it back to the -// queue's wait list, so the next available testrunner on that queue -// picks it up and runs it again — same data, same attempt counter. -admin.post('/retry', async function (request, response) { - const queueName = request.body.queueName; - const jobId = request.body.jobId; - if (queueName && jobId) { - await retryFailedJob(queueName, jobId); - } - response.redirect('/admin/'); -}); diff --git a/server/views/admin/index.pug b/server/views/admin/index.pug index 5862ee7..68e1c88 100644 --- a/server/views/admin/index.pug +++ b/server/views/admin/index.pug @@ -325,7 +325,6 @@ html(lang='en') span.cell.fail-target(role='columnheader') URL / script span.cell.fail-reason(role='columnheader') Reason span.cell.fail-when(role='columnheader') When - span.cell.fail-action(role='columnheader' aria-label='Actions') each job, failIndex in failedJobs - // Format the failure age into a readable label. Bull keeps @@ -353,11 +352,6 @@ html(lang='en') span.cell.fail-reason(role='cell' title=job.reason) | #{job.reason || '(no reason recorded)'} span.cell.fail-when(role='cell') #{whenLabel} - span.cell.fail-action(role='cell') - form(method='POST' action='/admin/retry' style='margin:0') - input(type='hidden' name='queueName' value=job.queue) - input(type='hidden' name='jobId' value=job.id) - button.retry-btn(type='submit' aria-label=`Retry job ${job.id}`) Retry h2.section-heading Queues .container(role='table' aria-label='Queue list') .row.row-head(role='row')