Add result and job management APIs

This commit is contained in:
syntaxbullet
2026-06-17 14:19:30 +02:00
parent 99569d2cf3
commit b23d401a41
7 changed files with 176 additions and 5 deletions

View File

@@ -11,7 +11,7 @@ const MAX_PARALLEL_SUMMARIES = 2;
const MAX_ACTIVE_JOBS = Number(process.env.BMP_MAX_ACTIVE_SUMMARY_JOBS ?? 10);
const EXPECTED_LLM_CALLS_PER_COMPANY = 13;
type JobStatus = "queued" | "running" | "done" | "error";
type JobStatus = "queued" | "running" | "done" | "error" | "cancelled";
interface SummaryJob {
id: string;
@@ -203,6 +203,10 @@ function activeJobCount(): number {
return [...jobs.values()].filter((job) => job.status === "queued" || job.status === "running").length;
}
function isCancelled(job: SummaryJob): boolean {
return job.status === "cancelled";
}
const jobs = new Map<string, SummaryJob>();
function createJob(stem: string, model: string, total: number): SummaryJob {
@@ -262,12 +266,14 @@ async function runWithConcurrency<T>(
}
async function executeJob(job: SummaryJob): Promise<void> {
if (isCancelled(job)) return;
job.status = "running";
job.statusMessage = "Auswertung wird vorbereitet.";
const companies = job.stem === "__all__" ? await listCompanies() : [job.stem];
try {
await runWithConcurrency(companies, MAX_PARALLEL_SUMMARIES, async (company) => {
if (isCancelled(job)) return;
job.current = company;
job.statusMessage = `LLM-Auswertung für ${company} läuft.`;
const summary = await summarizeCompany(company, job.model, {
@@ -286,17 +292,31 @@ async function executeJob(job: SummaryJob): Promise<void> {
},
});
job.statusMessage = `Downloads für ${company} werden vorbereitet.`;
if (isCancelled(job)) return;
const outputPaths = await writeSummary(company, summary, OUTPUTS_DIR);
job.files.push(...outputPaths.map((path) => basename(path)));
job.downloads.push(...outputPaths.map((path) => buildDownload(company, path)));
job.completed += 1;
});
if (isCancelled(job)) {
job.current = undefined;
job.statusMessage = "Auswertung abgebrochen.";
job.finishedAt = new Date().toISOString();
return;
}
job.status = "done";
job.current = undefined;
job.statusMessage = "Auswertung fertig.";
job.finishedAt = new Date().toISOString();
} catch (error) {
if (isCancelled(job)) {
job.current = undefined;
job.statusMessage = "Auswertung abgebrochen.";
job.finishedAt = new Date().toISOString();
return;
}
job.status = "error";
job.errors.push(String(error));
job.statusMessage = "Auswertung fehlgeschlagen.";
@@ -368,6 +388,15 @@ export const routes = {
}),
},
"/api/summarizer/jobs": {
GET: withAuth(async () => {
const summaries = [...jobs.values()]
.toSorted((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt))
.map(publicJob);
return Response.json({ jobs: summaries });
}),
},
"/api/summarizer/jobs/:id/llm-calls": {
GET: withAuth(async (req: Request) => {
const id = (req as Request & { params: Record<string, string> }).params.id;
@@ -402,5 +431,24 @@ export const routes = {
}
return Response.json(publicJob(job));
}),
DELETE: withAuth(async (req: Request) => {
const id = (req as Request & { params: Record<string, string> }).params.id;
const job = jobs.get(id);
if (!job) {
return Response.json({ error: "Job not found" }, { status: 404 });
}
if (job.status === "queued" || job.status === "running") {
job.status = "cancelled";
job.current = undefined;
job.statusMessage = "Auswertung abgebrochen.";
job.finishedAt = new Date().toISOString();
return Response.json({ ok: true, job: publicJob(job) });
}
jobs.delete(id);
return Response.json({ ok: true, deleted: true });
}, {
csrf: true,
limit: { key: "summarizer-job-delete", max: 30, windowMs: 60_000 },
}),
},
} as const;