feat(TRUEREF-0020): add job status page with pause/resume/cancel controls

- Extend indexing_jobs schema to support 'paused' and 'cancelled' status
- Add JobQueue methods: pauseJob(), resumeJob(), cancelJob()
- Create POST /api/v1/jobs/[id]/{pause,resume,cancel} endpoints
- Implement /admin/jobs page with auto-refresh (3s polling)
- Add JobStatusBadge component with color-coded status display
- Action buttons appear contextually based on job status
- Optimistic UI updates with error handling
- All 477 existing tests pass, no regressions
This commit is contained in:
Giancarmine Salucci
2026-03-25 20:38:14 +01:00
parent 9519a66cef
commit e7a2a83cdb
8 changed files with 496 additions and 8 deletions

View File

@@ -209,9 +209,78 @@ export class JobQueue {
params.push(options.status);
}
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND')}` : '';
const sql = `SELECT COUNT(*) as n FROM indexing_jobs ${where}`;
const row = this.db.prepare<unknown[], { n: number }>(sql).get(...params);
return row?.n ?? 0;
}
/**
* Pause a job that is currently queued or running.
* Returns true if the job was successfully paused, false otherwise.
*/
pauseJob(id: string): boolean {
const job = this.getJob(id);
if (!job) return false;
// Only queued or running jobs can be paused
if (job.status !== 'queued' && job.status !== 'running') {
return false;
}
this.db
.prepare(`UPDATE indexing_jobs SET status = 'paused' WHERE id = ?`)
.run(id);
return true;
}
/**
* Resume a paused job by changing its status back to 'queued' and
* triggering the queue drain.
* Returns true if the job was successfully resumed, false otherwise.
*/
resumeJob(id: string): boolean {
const job = this.getJob(id);
if (!job) return false;
// Only paused jobs can be resumed
if (job.status !== 'paused') {
return false;
}
this.db
.prepare(`UPDATE indexing_jobs SET status = 'queued' WHERE id = ?`)
.run(id);
// Trigger queue processing in case the queue was idle
this.drainQueued();
return true;
}
/**
* Cancel a job if it's not already completed.
* Returns true if the job was successfully cancelled, false otherwise.
*/
cancelJob(id: string): boolean {
const job = this.getJob(id);
if (!job) return false;
// Can't cancel jobs that are already done or failed
if (job.status === 'done' || job.status === 'failed') {
return false;
}
const now = Math.floor(Date.now() / 1000);
this.db
.prepare(
`UPDATE indexing_jobs
SET status = 'cancelled', completed_at = ?
WHERE id = ?`
)
.run(now, id);
return true;
}
}