Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,6 @@ next-env.d.ts
.env

# transformers.js model cache
.transformers-cache/
.transformers-cache/
.env*
/.swc
64 changes: 0 additions & 64 deletions app/api/ai/reindex-blog/route.ts

This file was deleted.

64 changes: 0 additions & 64 deletions app/api/ai/reindex-knowledge/route.ts

This file was deleted.

74 changes: 74 additions & 0 deletions app/api/capture/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
export const runtime = "nodejs";

import { z } from "zod";
import { start } from "workflow/api";
import { rateLimit } from "@/lib/rate/limit";
import {
createErrorResponse,
createSuccessResponse,
validateRequestBody,
} from "@/lib/api/middleware";
import { ZContactSeed } from "@/lib/db/models";
import { insertContactSeed } from "@/lib/db/contacts";
import { enrichContactWorkflow } from "@/lib/workflows/enrich-contact";

// The public form also submits a honeypot field which must stay empty.
const ZCaptureRequest = ZContactSeed.extend({
website: z.string().optional(),
});

/**
* Public capture endpoint for the portfolio NFC landing page.
* POST /api/capture
*
* Stores a contact seed, kicks off the durable enrichment workflow, and returns
* a pre-filled wa.me link so the visitor lands in WhatsApp.
*/
export async function POST(request: Request) {
// Rate limit: 5 submissions per hour per client (spam control).
const rl = await rateLimit(request, "capture", { windowSec: 3600, max: 5 });
if (!rl.ok) {
return createErrorResponse(
"Too many submissions. Please try again later.",
429,
{ retryAfterSec: rl.retryAfterSec },
"RATE_LIMIT_EXCEEDED"
);
}

const parsed = await validateRequestBody(request, ZCaptureRequest);
if (!parsed.success) return parsed.response;

const { website, ...seed } = parsed.data;

// Honeypot tripped — pretend success without storing anything.
if (website && website.trim().length > 0) {
return createSuccessResponse({ ok: true });
}

const ownerNumber = process.env.OWNER_WHATSAPP;
if (!ownerNumber) {
return createErrorResponse(
"Capture is not configured yet.",
503,
undefined,
"NOT_CONFIGURED"
);
}

const id = await insertContactSeed(seed);

// Fire the durable enrichment pipeline. Don't fail the capture if the
// workflow backend is unavailable — the cron fallback will pick it up.
try {
await start(enrichContactWorkflow, [id]);
} catch (error) {
console.error("Failed to start enrichment workflow:", error);
}

// Build the pre-filled WhatsApp link (open-ended so they can add detail).
const text = `Hi! We just met — I'm ${seed.name}.`;
const waUrl = `https://wa.me/${ownerNumber}?text=${encodeURIComponent(text)}`;

return createSuccessResponse({ id, waUrl });
}
122 changes: 0 additions & 122 deletions app/api/chat/route.ts

This file was deleted.

35 changes: 35 additions & 0 deletions app/api/cron/enrich/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export const runtime = "nodejs";

import { start } from "workflow/api";
import { createErrorResponse, createSuccessResponse } from "@/lib/api/middleware";
import { listPendingContacts } from "@/lib/db/contacts";
import { enrichContactWorkflow } from "@/lib/workflows/enrich-contact";

/**
* Fallback enrichment sweep for any contacts left in `pending`
* (e.g. if a workflow failed to start at capture time).
*
* GET /api/cron/enrich — intended to run on a Vercel Cron schedule.
* Authenticated with the Vercel-provided `Authorization: Bearer <CRON_SECRET>`.
*/
export async function GET(request: Request) {
const secret = process.env.CRON_SECRET;
const auth = request.headers.get("authorization");
if (!secret || auth !== `Bearer ${secret}`) {
return createErrorResponse("Unauthorized", 401, undefined, "UNAUTHORIZED");
}

const pending = await listPendingContacts(25);
const started: string[] = [];

for (const contact of pending) {
try {
await start(enrichContactWorkflow, [contact.id]);
started.push(contact.id);
} catch (error) {
console.error(`Failed to start enrichment for ${contact.id}:`, error);
}
}

return createSuccessResponse({ pending: pending.length, started: started.length });
}
Loading
Loading