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

@@ -0,0 +1,45 @@
/**
* POST /api/v1/jobs/:id/cancel — cancel a job.
*/
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getClient } from '$lib/server/db/client.js';
import { IndexingJobMapper } from '$lib/server/mappers/indexing-job.mapper.js';
import { JobQueue } from '$lib/server/pipeline/job-queue.js';
import { handleServiceError, NotFoundError, InvalidInputError } from '$lib/server/utils/validation.js';
export const POST: RequestHandler = ({ params }) => {
try {
const db = getClient();
const queue = new JobQueue(db);
const job = queue.getJob(params.id);
if (!job) throw new NotFoundError(`Job ${params.id} not found`);
const success = queue.cancelJob(params.id);
if (!success) {
throw new InvalidInputError(
`Cannot cancel job ${params.id} - job is already done or failed`
);
}
// Fetch updated job
const updated = queue.getJob(params.id)!;
return json({ success: true, job: IndexingJobMapper.toDto(updated) });
} catch (err) {
return handleServiceError(err);
}
};
export const OPTIONS: RequestHandler = () => {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
}
});
};

View File

@@ -0,0 +1,45 @@
/**
* POST /api/v1/jobs/:id/pause — pause a running or queued job.
*/
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getClient } from '$lib/server/db/client.js';
import { IndexingJobMapper } from '$lib/server/mappers/indexing-job.mapper.js';
import { JobQueue } from '$lib/server/pipeline/job-queue.js';
import { handleServiceError, NotFoundError, InvalidInputError } from '$lib/server/utils/validation.js';
export const POST: RequestHandler = ({ params }) => {
try {
const db = getClient();
const queue = new JobQueue(db);
const job = queue.getJob(params.id);
if (!job) throw new NotFoundError(`Job ${params.id} not found`);
const success = queue.pauseJob(params.id);
if (!success) {
throw new InvalidInputError(
`Cannot pause job ${params.id} - only queued or running jobs can be paused`
);
}
// Fetch updated job
const updated = queue.getJob(params.id)!;
return json({ success: true, job: IndexingJobMapper.toDto(updated) });
} catch (err) {
return handleServiceError(err);
}
};
export const OPTIONS: RequestHandler = () => {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
}
});
};

View File

@@ -0,0 +1,43 @@
/**
* POST /api/v1/jobs/:id/resume — resume a paused job.
*/
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getClient } from '$lib/server/db/client.js';
import { IndexingJobMapper } from '$lib/server/mappers/indexing-job.mapper.js';
import { JobQueue } from '$lib/server/pipeline/job-queue.js';
import { handleServiceError, NotFoundError, InvalidInputError } from '$lib/server/utils/validation.js';
export const POST: RequestHandler = ({ params }) => {
try {
const db = getClient();
const queue = new JobQueue(db);
const job = queue.getJob(params.id);
if (!job) throw new NotFoundError(`Job ${params.id} not found`);
const success = queue.resumeJob(params.id);
if (!success) {
throw new InvalidInputError(`Cannot resume job ${params.id} - only paused jobs can be resumed`);
}
// Fetch updated job
const updated = queue.getJob(params.id)!;
return json({ success: true, job: IndexingJobMapper.toDto(updated) });
} catch (err) {
return handleServiceError(err);
}
};
export const OPTIONS: RequestHandler = () => {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
}
});
};