Files
lead-scraper/app/api/leads/quick-serp/route.ts
Timo Uttenweiler 042fbeb672 feat: LeadVault - zentrale Lead-Datenbank mit CRM-Funktionen
- Prisma-Schema: Lead + LeadEvent Modelle (Migration 20260320)
- lib/services/leadVault.ts: sinkLeadsToVault mit Deduplizierung
- Auto-Sync: alle 4 Pipelines schreiben Leads in LeadVault
- GET /api/leads: Filter, Sortierung, Pagination (Server-side)
- PATCH/DELETE /api/leads/[id]: Status, Priorität, Tags, Notizen
- POST /api/leads/bulk: Bulk-Aktionen für mehrere Leads
- GET /api/leads/stats: Statistiken + 7-Tage-Sparkline
- POST /api/leads/quick-serp: SERP-Capture ohne Enrichment
- GET /api/leads/export: CSV-Export mit allen Feldern
- app/leadvault/page.tsx: vollständige UI mit Stats, Quick SERP,
  Filter-Leiste, sortierbare Tabelle, Bulk-Aktionen, Side Panel
- Sidebar: LeadVault-Eintrag mit Live-Badge (neue Leads)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 17:33:12 +01:00

72 lines
2.4 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { getApiKey } from "@/lib/utils/apiKey";
import { runGoogleSerpScraper, pollRunStatus, fetchDatasetItems } from "@/lib/services/apify";
import { isSocialOrDirectory } from "@/lib/utils/domains";
import { sinkLeadsToVault } from "@/lib/services/leadVault";
function sleep(ms: number) { return new Promise(r => setTimeout(r, ms)); }
export async function POST(req: NextRequest) {
try {
const body = await req.json() as {
query: string;
count: number;
country: string;
language: string;
filterSocial: boolean;
};
const { query, count, country, language, filterSocial } = body;
if (!query?.trim()) return NextResponse.json({ error: "Kein Suchbegriff" }, { status: 400 });
const apifyToken = await getApiKey("apify");
if (!apifyToken) return NextResponse.json({ error: "Apify API-Key fehlt" }, { status: 400 });
const maxPages = Math.ceil(count / 10);
const runId = await runGoogleSerpScraper(query, maxPages, country, language, apifyToken);
let runStatus = "";
let datasetId = "";
while (runStatus !== "SUCCEEDED" && runStatus !== "FAILED" && runStatus !== "ABORTED") {
await sleep(3000);
const result = await pollRunStatus(runId, apifyToken);
runStatus = result.status;
datasetId = result.defaultDatasetId;
}
if (runStatus !== "SUCCEEDED") throw new Error(`Apify run ${runStatus}`);
let serpResults = await fetchDatasetItems(datasetId, apifyToken);
if (filterSocial) {
serpResults = serpResults.filter(r => !isSocialOrDirectory(r.domain));
}
// Deduplicate by domain
const seen = new Set<string>();
const unique = serpResults.filter(r => {
if (!r.domain || seen.has(r.domain)) return false;
seen.add(r.domain);
return true;
}).slice(0, count);
const stats = await sinkLeadsToVault(
unique.map((r, i) => ({
domain: r.domain,
companyName: r.title || null,
serpTitle: r.title || null,
serpSnippet: r.description || null,
serpRank: r.position ?? i + 1,
serpUrl: r.url || null,
})),
"quick-serp",
query,
);
return NextResponse.json(stats);
} catch (err) {
console.error("POST /api/leads/quick-serp error:", err);
return NextResponse.json({ error: err instanceof Error ? err.message : "Fehler" }, { status: 500 });
}
}