import { internal } from "./_generated/api"; import type { Doc, Id } from "./_generated/dataModel"; import { internalMutation } from "./_generated/server"; import { v } from "convex/values"; const PAGE_SPEED_COUNTER_TEMPLATE = { leadsFound: 1, leadsCreated: 0, auditsCreated: 1, outreachPrepared: 0, errors: 0, }; type PageSpeedLead = Pick< Doc<"leads">, "_id" | "contactStatus" > & { websiteUrl: string; }; const runStatus = v.union( v.literal("pending"), v.literal("running"), v.literal("succeeded"), v.literal("failed"), v.literal("canceled"), ); const pageSpeedStrategy = v.union(v.literal("mobile"), v.literal("desktop")); const pageSpeedResultStatus = v.union( v.literal("succeeded"), v.literal("failed"), ); const pageSpeedErrorType = v.union( v.literal("quota"), v.literal("timeout"), v.literal("unavailable"), v.literal("invalid_url"), v.literal("api_error"), v.literal("unknown"), ); export const queueLeadPageSpeedAudit = internalMutation({ args: { leadId: v.id("leads"), parentRunId: v.optional(v.id("agentRuns")), }, returns: v.union(v.id("agentRuns"), v.null()), handler: async (ctx, args): Promise | null> => { const now = Date.now(); const lead = await ctx.db.get(args.leadId); if (!lead || lead.priority === "blocked" || lead.priority === "defer") { return null; } if (!lead.websiteUrl) { return null; } const existingPending = await ctx.db .query("agentRuns") .withIndex("by_type_and_status_and_leadId", (q) => q.eq("type", "audit").eq("status", "pending").eq("leadId", args.leadId), ) .take(1); const existingRunning = await ctx.db .query("agentRuns") .withIndex("by_type_and_status_and_leadId", (q) => q.eq("type", "audit").eq("status", "running").eq("leadId", args.leadId), ) .take(1); if (existingPending.length > 0) { return existingPending[0]._id; } if (existingRunning.length > 0) { return existingRunning[0]._id; } const runId = await ctx.db.insert("agentRuns", { type: "audit", leadId: args.leadId, status: "pending", currentStep: "pagespeed_insights", counters: PAGE_SPEED_COUNTER_TEMPLATE, createdAt: now, updatedAt: now, }); await ctx.db.insert("agentRunEvents", { runId, level: "info", message: "PageSpeed-Analyse wurde in die Warteschlange gesetzt.", details: [ { label: "Lead", value: args.leadId }, ...(args.parentRunId ? [{ label: "Parent-Run", value: args.parentRunId }] : []), ], createdAt: now, }); await ctx.scheduler.runAfter( 0, internal.pageSpeedAction.processPageSpeedAudit, { runId, }, ); return runId; }, }); export const startPageSpeedAuditRun = internalMutation({ args: { runId: v.id("agentRuns"), }, returns: v.union( v.object({ lead: v.object({ _id: v.id("leads"), websiteUrl: v.string(), contactStatus: v.union( v.literal("new"), v.literal("missing_contact"), v.literal("audit_ready"), v.literal("outreach_ready"), v.literal("contacted"), v.literal("replied"), v.literal("do_not_contact"), ), }), auditId: v.optional(v.id("audits")), }), v.null(), ), handler: async (ctx, args): Promise< { lead: PageSpeedLead; auditId?: Id<"audits"> } | null > => { const now = Date.now(); const run = await ctx.db.get(args.runId); if (!run) { return null; } if (run.type !== "audit") { return null; } if (run.status !== "pending") { return null; } if (!run.leadId) { await ctx.db.patch(args.runId, { status: "failed", currentStep: "pagespeed_insights", errorSummary: "Run hat keine Lead-ID.", updatedAt: now, finishedAt: now, }); await ctx.db.insert("agentRunEvents", { runId: args.runId, level: "error", message: "PageSpeed-Analyse konnte nicht gestartet werden: Kein Lead verknüpft.", details: [{ label: "Lead-ID", value: "unbekannt" }], createdAt: now, }); return null; } const lead = await ctx.db.get(run.leadId); if (!lead) { await ctx.db.patch(args.runId, { status: "failed", currentStep: "pagespeed_insights", errorSummary: "Lead wurde nicht gefunden.", updatedAt: now, finishedAt: now, }); await ctx.db.insert("agentRunEvents", { runId: args.runId, level: "error", message: "PageSpeed-Analyse konnte nicht gestartet werden: Kein Lead mit Website-URL.", details: [{ label: "Lead-ID", value: run.leadId }], createdAt: now, }); return null; } if (!lead.websiteUrl) { await ctx.db.patch(args.runId, { status: "failed", currentStep: "pagespeed_insights", errorSummary: "Lead hat keine Website-URL.", updatedAt: now, finishedAt: now, }); await ctx.db.insert("agentRunEvents", { runId: args.runId, level: "error", message: "PageSpeed-Analyse konnte nicht gestartet werden: Kein Lead mit Website-URL.", details: [{ label: "Lead-ID", value: lead._id }], createdAt: now, }); return null; } await ctx.db.patch(args.runId, { status: "running", currentStep: "pagespeed_insights", startedAt: now, updatedAt: now, errorSummary: undefined, }); await ctx.db.insert("agentRunEvents", { runId: args.runId, level: "info", message: "PageSpeed-Analyse gestartet.", details: [{ label: "Lead-ID", value: lead._id }], createdAt: now, }); return { lead: { _id: lead._id, websiteUrl: lead.websiteUrl, contactStatus: lead.contactStatus, }, ...(run.auditId ? { auditId: run.auditId } : {}), }; }, }); export const persistPageSpeedResult = internalMutation({ args: { leadId: v.id("leads"), auditId: v.optional(v.id("audits")), runId: v.id("agentRuns"), strategy: pageSpeedStrategy, status: pageSpeedResultStatus, sourceUrl: v.string(), finalUrl: v.optional(v.string()), rawStorageId: v.optional(v.id("_storage")), errorType: v.optional(pageSpeedErrorType), errorSummary: v.optional(v.string()), fetchedAt: v.number(), normalized: v.optional( v.object({ scores: v.optional( v.object({ performance: v.optional(v.number()), accessibility: v.optional(v.number()), bestPractices: v.optional(v.number()), seo: v.optional(v.number()), }), ), metrics: v.optional( v.object({ firstContentfulPaintMs: v.optional(v.number()), largestContentfulPaintMs: v.optional(v.number()), cumulativeLayoutShift: v.optional(v.number()), totalBlockingTimeMs: v.optional(v.number()), speedIndexMs: v.optional(v.number()), }), ), opportunities: v.optional(v.array(v.string())), implications: v.optional(v.array(v.string())), }), ), }, returns: v.id("pageSpeedResults"), handler: async (ctx, args): Promise> => { return await ctx.db.insert("pageSpeedResults", { ...args, createdAt: Date.now(), }); }, }); export const finishPageSpeedAuditRun = internalMutation({ args: { runId: v.id("agentRuns"), status: runStatus, errorSummary: v.optional(v.string()), errors: v.optional(v.number()), }, handler: async (ctx, args) => { const now = Date.now(); await ctx.db.patch(args.runId, { status: args.status, updatedAt: now, finishedAt: now, currentStep: "pagespeed_insights", errorSummary: args.errorSummary, counters: { ...PAGE_SPEED_COUNTER_TEMPLATE, errors: args.errors ?? 0, }, }); }, });