← back to Trademarks Copyright
src/app/api/brands/hunt/route.ts
64 lines
import { NextRequest } from "next/server";
import { runBrandHunter, FASHION_SEEDS } from "@/lib/brandHunter";
import { qwenIsUp, QWEN_MODEL_NAME } from "@/lib/qwen";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
/**
* Server-Sent Events stream. Client calls POST /api/brands/hunt with
* { category?: string; seedDomains?: string[]; maxDiscoveries?: number }.
*/
export async function POST(req: NextRequest) {
const body = await req.json().catch(() => ({}));
if (!body || typeof body !== "object" || Array.isArray(body)) {
return Response.json({ error: "json object body required" }, { status: 400 });
}
const category = typeof body.category === "string" ? body.category : "fashion";
const seedsIn: string[] = Array.isArray(body.seedDomains) && body.seedDomains.length
? body.seedDomains
: FASHION_SEEDS;
// P1 fix 2026-05-04: cap at 50 even if caller passes 99999. Auth gate via
// middleware now blocks unauth callers, but defense-in-depth: a stale admin
// session shouldn't be able to pin Ollama for hours either.
const requested = Number.isFinite(body.maxDiscoveries) ? Number(body.maxDiscoveries) : 25;
const maxDiscoveries = Math.max(1, Math.min(50, requested));
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
const send = (obj: unknown) =>
controller.enqueue(encoder.encode(`data: ${JSON.stringify(obj)}\n\n`));
if (!(await qwenIsUp())) {
send({ kind: "error", domain: "-", message: "Ollama not reachable at :11434. Start with `ollama serve`." });
controller.close();
return;
}
send({ kind: "info", message: `Using model ${QWEN_MODEL_NAME}; ${seedsIn.length} seeds; cap ${maxDiscoveries}.` });
try {
await runBrandHunter({
category,
seedDomains: seedsIn,
maxDiscoveries,
onEvent: (e) => send(e),
});
} catch (e) {
send({ kind: "fatal", message: e instanceof Error ? e.message : String(e) });
} finally {
controller.close();
}
},
});
return new Response(stream, {
headers: {
"content-type": "text/event-stream",
"cache-control": "no-cache, no-transform",
connection: "keep-alive",
"x-accel-buffering": "no",
},
});
}