add pipeline job management with /jobs pages and per-step output viewer

- Pipeline jobs now persist to DB with progress tracking and cost accumulation
- Jobs survive WebSocket disconnects with in-memory event buffer replay
- New /jobs list page with search, status badges, and cost display
- New /jobs/:id detail page with live WebSocket attachment and REST fallback
- Two-column layout using WorkspaceLayout for resizable steps/output panels
- Streaming messages tagged with stepIndex/iterationLabel for per-step output grouping
- TaskRunnerModal links to job detail page once job is created
- Dock entry added for Jobs page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 08:48:50 +00:00
co-authored by Claude Opus 4.6
parent 22c60d4a57
commit 5e38343f44
22 changed files with 4506 additions and 252 deletions
@@ -0,0 +1,29 @@
import { createRouter } from '../../create-router';
import { getPipelineJobsForUser, getPipelineJob } from 'officerdb';
export const pipelineJobsRouter = createRouter();
// GET /pipeline-jobs — list user's pipeline jobs
pipelineJobsRouter.get('/', async (c) => {
const user = c.get('user');
const jobs = await getPipelineJobsForUser(user.id);
return c.json(jobs.map((j) => ({
id: j.id,
taskDirName: j.taskDirName,
taskName: j.taskName,
status: j.status,
totalCost: j.totalCost,
createdAt: j.createdAt,
startedAt: j.startedAt,
completedAt: j.completedAt,
error: j.error,
})));
});
// GET /pipeline-jobs/:id — single job detail
pipelineJobsRouter.get('/:id', async (c) => {
const user = c.get('user');
const job = await getPipelineJob(c.req.param('id'));
if (!job || job.userId !== user.id) return c.json({ error: 'Not found' }, 404);
return c.json(job);
});