Workflow-App/Workflow/app/api/tasks/[id]/fail/route.ts
sjcoetzeesj-coder 11d0178ab1 feat: commit Workflow app source; move CIPC credentials to env
Adds the full Next.js app (app/, components/, lib/, scripts/, assets/,
docs/, Dockerfile, compose, configs) to version control.

Security:
- CIPC BizPortal customer-code logins (Login 1 + Login 3) now read
  CIPC_LOGIN1_CODE / CIPC_LOGIN1_PASSWORD / CIPC_LOGIN3_CODE /
  CIPC_LOGIN3_PASSWORD from .env.local instead of source.
- BizPortal personal login in apply-bizportal route reads
  BIZPORTAL_ID_NO / BIZPORTAL_PASSWORD.
- Added .env.example as the committed template.

.gitignore excludes .env*, node_modules, .next, logs, sqlite, Playwright
storage_state.json (live CIPC cookies), scratch dirs, client PDFs and
debug screenshots.
2026-10-18 15:21:45 +02:00

73 lines
2.2 KiB
TypeScript

// app/api/tasks/[id]/fail/route.ts
// POST /api/tasks/[id]/fail
// Atomic state transition: in_progress → failed. Body: { error, log, retry }.
import { NextRequest, NextResponse } from "next/server";
import { BASE_URL, fetchRecord, updateRecord, PBRecord } from "@/lib/pb";
export const dynamic = "force-dynamic";
const COLLECTION = "tasks";
const MAX_RETRY = 3;
export async function POST(req: NextRequest, { params }: { params: { id: string } }) {
let body: any = {};
try {
body = await req.json();
} catch {
return NextResponse.json({ error: "invalid JSON body" }, { status: 400 });
}
const errorMsg = (body?.error ?? "unknown error").toString().slice(0, 5000);
const log = body?.log ?? null;
// Atomic transition: in_progress → failed.
const url = `${BASE_URL}/api/collections/${COLLECTION}/records/${encodeURIComponent(params.id)}?filter=status="in_progress"`;
const res = await fetch(url, {
method: "PATCH",
headers: {
Authorization: process.env.PB_TOKEN as string,
"Content-Type": "application/json",
},
body: JSON.stringify({
status: "failed",
error: errorMsg,
log,
completed_at: new Date().toISOString().replace("T", " ").replace("Z", ""),
}),
cache: "no-store",
});
if (res.status === 404) {
return NextResponse.json(
{ ok: false, error: "task not found or not in progress" },
{ status: 409 },
);
}
if (!res.ok) {
const text = await res.text();
return NextResponse.json({ ok: false, error: `PB ${res.status}: ${text.slice(0, 200)}` }, { status: 500 });
}
const failed: PBRecord = await res.json();
// Auto-retry if under the cap and the caller asked for it.
let requeued = false;
if (body?.retry !== false) {
const current = await fetchRecord<PBRecord>(COLLECTION, params.id);
const retryCount = (current.retry_count as number) ?? 0;
if (retryCount < MAX_RETRY) {
await updateRecord<PBRecord>(COLLECTION, params.id, {
status: "ready",
retry_count: retryCount + 1,
error: null,
completed_at: null,
claimed_by: null,
claimed_at: null,
started_at: null,
});
requeued = true;
}
}
return NextResponse.json({ ok: true, task: failed, requeued });
}