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:
@@ -132,7 +132,7 @@ export const indexingJobs = sqliteTable('indexing_jobs', {
|
||||
.references(() => repositories.id, { onDelete: 'cascade' }),
|
||||
versionId: text('version_id'),
|
||||
status: text('status', {
|
||||
enum: ['queued', 'running', 'done', 'failed']
|
||||
enum: ['queued', 'running', 'paused', 'cancelled', 'done', 'failed']
|
||||
})
|
||||
.notNull()
|
||||
.default('queued'),
|
||||
|
||||
@@ -2,7 +2,7 @@ export interface IndexingJobEntityProps {
|
||||
id: string;
|
||||
repository_id: string;
|
||||
version_id: string | null;
|
||||
status: 'queued' | 'running' | 'done' | 'failed';
|
||||
status: 'queued' | 'running' | 'paused' | 'cancelled' | 'done' | 'failed';
|
||||
progress: number;
|
||||
total_files: number;
|
||||
processed_files: number;
|
||||
@@ -16,7 +16,7 @@ export class IndexingJobEntity {
|
||||
id: string;
|
||||
repository_id: string;
|
||||
version_id: string | null;
|
||||
status: 'queued' | 'running' | 'done' | 'failed';
|
||||
status: 'queued' | 'running' | 'paused' | 'cancelled' | 'done' | 'failed';
|
||||
progress: number;
|
||||
total_files: number;
|
||||
processed_files: number;
|
||||
@@ -44,7 +44,7 @@ export interface IndexingJobProps {
|
||||
id: string;
|
||||
repositoryId: string;
|
||||
versionId: string | null;
|
||||
status: 'queued' | 'running' | 'done' | 'failed';
|
||||
status: 'queued' | 'running' | 'paused' | 'cancelled' | 'done' | 'failed';
|
||||
progress: number;
|
||||
totalFiles: number;
|
||||
processedFiles: number;
|
||||
@@ -58,7 +58,7 @@ export class IndexingJob {
|
||||
id: string;
|
||||
repositoryId: string;
|
||||
versionId: string | null;
|
||||
status: 'queued' | 'running' | 'done' | 'failed';
|
||||
status: 'queued' | 'running' | 'paused' | 'cancelled' | 'done' | 'failed';
|
||||
progress: number;
|
||||
totalFiles: number;
|
||||
processedFiles: number;
|
||||
@@ -86,7 +86,7 @@ export interface IndexingJobDtoProps {
|
||||
id: string;
|
||||
repositoryId: string;
|
||||
versionId: string | null;
|
||||
status: 'queued' | 'running' | 'done' | 'failed';
|
||||
status: 'queued' | 'running' | 'paused' | 'cancelled' | 'done' | 'failed';
|
||||
progress: number;
|
||||
totalFiles: number;
|
||||
processedFiles: number;
|
||||
@@ -100,7 +100,7 @@ export class IndexingJobDto {
|
||||
id: string;
|
||||
repositoryId: string;
|
||||
versionId: string | null;
|
||||
status: 'queued' | 'running' | 'done' | 'failed';
|
||||
status: 'queued' | 'running' | 'paused' | 'cancelled' | 'done' | 'failed';
|
||||
progress: number;
|
||||
totalFiles: number;
|
||||
processedFiles: number;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user