// app/api/tasks/route.ts // GET /api/tasks — list with filters // POST /api/tasks — create a task // // Server-side proxy to PocketBase. The browser calls these routes // over the existing /api/* contract; PB_TOKEN never leaves the // server. import { NextRequest, NextResponse } from "next/server"; import { fetchCollection, createRecord, PBRecord } from "@/lib/pb"; export const dynamic = "force-dynamic"; const COLLECTION = "tasks"; const VALID_STATUS = ["backlog", "ready", "in_progress", "blocked", "done", "failed"] as const; const VALID_PRIORITY = ["low", "normal", "high", "urgent"] as const; const VALID_TASK_TYPE = ["bizportal_search", "bizportal_reserve", "pdf_generate", "email_send", "custom"] as const; export async function GET(req: NextRequest) { const url = new URL(req.url); const status = url.searchParams.get("status"); const taskType = url.searchParams.get("task_type"); const priority = url.searchParams.get("priority"); const assignee = url.searchParams.get("assignee"); const includeDone = url.searchParams.get("include_done") === "true"; const limit = parseInt(url.searchParams.get("limit") || "200", 10); const filters: string[] = []; if (status && status !== "all") { if (!(VALID_STATUS as readonly string[]).includes(status)) { return NextResponse.json({ error: `invalid status: ${status}` }, { status: 400 }); } filters.push(`status="${status}"`); } else if (!includeDone) { // Default: don't pull done/failed tasks older than 200 unless asked filters.push(`(status!="done" && status!="failed")`); } if (taskType) { if (!(VALID_TASK_TYPE as readonly string[]).includes(taskType)) { return NextResponse.json({ error: `invalid task_type: ${taskType}` }, { status: 400 }); } filters.push(`task_type="${taskType}"`); } if (priority) { if (!(VALID_PRIORITY as readonly string[]).includes(priority)) { return NextResponse.json({ error: `invalid priority: ${priority}` }, { status: 400 }); } filters.push(`priority="${priority}"`); } if (assignee) { filters.push(`assignee="${assignee.replace(/"/g, '\\"')}"`); } try { const result = await fetchCollection(COLLECTION, { page: 1, perPage: Math.min(limit, 500), // PB on this instance doesn't support sort on the auto-managed // `created` column. Use priority (desc) then id (desc) — id is // a nanoid prefix so newer tasks come last when sorted desc, and // the worker can poll deterministically. sort: "-priority,-id", filter: filters.length > 0 ? filters.join(" && ") : undefined, }); return NextResponse.json(result); } catch (err: any) { return NextResponse.json({ error: err.message }, { status: 500 }); } } export async function POST(req: NextRequest) { let body: any; try { body = await req.json(); } catch { return NextResponse.json({ error: "invalid JSON body" }, { status: 400 }); } const { title, description, task_type, payload, status, priority, assignee, created_by } = body ?? {}; if (!title || typeof title !== "string") { return NextResponse.json({ error: "title is required" }, { status: 400 }); } if (!task_type || !(VALID_TASK_TYPE as readonly string[]).includes(task_type)) { return NextResponse.json({ error: `task_type must be one of: ${VALID_TASK_TYPE.join(", ")}` }, { status: 400 }); } const finalStatus = status || "backlog"; if (!(VALID_STATUS as readonly string[]).includes(finalStatus)) { return NextResponse.json({ error: `invalid status: ${finalStatus}` }, { status: 400 }); } const finalPriority = priority || "normal"; if (!(VALID_PRIORITY as readonly string[]).includes(finalPriority)) { return NextResponse.json({ error: `invalid priority: ${finalPriority}` }, { status: 400 }); } try { const created = await createRecord(COLLECTION, { title, description: description || null, task_type, payload: payload || {}, status: finalStatus, priority: finalPriority, assignee: assignee || "agent", retry_count: 0, created_by: created_by || "user", }); return NextResponse.json(created, { status: 201 }); } catch (err: any) { return NextResponse.json({ error: err.message }, { status: 500 }); } }