diff --git a/lib/aiRuns.js b/lib/aiRuns.js index 8e59ce4..8df7b33 100644 --- a/lib/aiRuns.js +++ b/lib/aiRuns.js @@ -243,7 +243,13 @@ async function getRun(id) { `SELECT i.model_id, m.name, i.status, i.tags, i.error, i.renamed_from, i.renamed_to, i.collections FROM ai_run_items i LEFT JOIN models m ON m.id = i.model_id WHERE i.run_id = $1 AND i.status <> 'pending' ORDER BY i.updated_at DESC LIMIT 12`, [id]); - return { ...shapeRun(run), recent }; + // Every failure, not only the ones still inside that window: a model that + // failed early would otherwise scroll out of sight with no way to find it. + const { rows: failures } = await db.query( + `SELECT i.model_id, m.name, i.error + FROM ai_run_items i LEFT JOIN models m ON m.id = i.model_id + WHERE i.run_id = $1 AND i.status = 'error' ORDER BY i.updated_at DESC LIMIT 200`, [id]); + return { ...shapeRun(run), recent, failures }; } async function stop(id) { @@ -257,7 +263,7 @@ async function stop(id) { async function resume(id) { const resumed = await db.transaction(async (client) => { const { rows: [run] } = await client.query( - `SELECT id FROM ai_runs WHERE id = $1 AND status IN ('paused', 'stopped', 'failed') FOR UPDATE`, [id]); + `SELECT id FROM ai_runs WHERE id = $1 AND status IN ('paused', 'stopped', 'failed', 'done') FOR UPDATE`, [id]); if (!run) return false; const retried = await client.query( `UPDATE ai_run_items SET status = 'pending', error = NULL WHERE run_id = $1 AND status = 'error'`, [id]); diff --git a/public/app.js b/public/app.js index fba20d0..27979f9 100644 --- a/public/app.js +++ b/public/app.js @@ -1420,7 +1420,8 @@ function runHtml(run, detail) { const options = run.options || {}; const cost = options.pricesKnown ? money(run.cost_usd) : 'cost unknown (add prices in Settings)'; const canStop = run.status === 'running' || run.status === 'paused'; - const canResume = ['paused', 'stopped', 'failed'].includes(run.status) && (processed < run.total || run.failed > 0); + // 'done' too: a run that finished with failures still has something to retry. + const canResume = ['paused', 'stopped', 'failed', 'done'].includes(run.status) && (processed < run.total || run.failed > 0); const feed = detail && detail.recent.length ? ` ` : ''; + // Failures are listed in full: the feed above only holds the last dozen items, + // so anything that failed early would otherwise be impossible to track down. + const failed = detail && (detail.failures || []).length ? ` +
+ ${plural(detail.failures.length, 'model')} failed + +
` : ''; return `
diff --git a/public/styles.css b/public/styles.css index 56c7a9f..0f656ac 100644 --- a/public/styles.css +++ b/public/styles.css @@ -464,6 +464,8 @@ label { display: block; font-size: 13px; font-weight: 500; color: var(--text-mut .watch-head { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; font-size: 14px; } .watch-actions { margin-left: auto; display: flex; flex-wrap: wrap; gap: 8px; } .watch-actions .btn-sm { align-self: auto; } +.ai-failures summary { cursor: pointer; font-size: 13px; color: var(--text-muted); } +.ai-failures summary:hover { color: var(--text); } .watch-pending summary { cursor: pointer; font-size: 13px; color: var(--text-muted); } .watch-pending summary:hover { color: var(--text); } .watch-pending .watch-recent { margin-top: 6px; }