← 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",
    },
  });
}