Integrate local business workflow and SaaS redesign

This commit is contained in:
2026-06-12 21:08:35 +02:00
parent f00c5a3193
commit 21c7e4c9a4
88 changed files with 2683 additions and 849 deletions

View File

@@ -1,6 +1,11 @@
import { internal } from "./_generated/api";
import type { Doc, Id } from "./_generated/dataModel";
import { internalMutation } from "./_generated/server";
import {
internalMutation,
mutation,
query,
} from "./_generated/server";
import type { MutationCtx, QueryCtx } from "./_generated/server";
import { v } from "convex/values";
const PAGE_SPEED_COUNTER_TEMPLATE = {
@@ -17,6 +22,13 @@ type PageSpeedLead = Pick<
> & {
websiteUrl: string;
};
type AuditStartState = {
leadId: Id<"leads">;
canStart: boolean;
reason?: string;
activeRunId?: Id<"agentRuns">;
activeRunStatus?: Doc<"agentRuns">["status"];
};
const runStatus = v.union(
v.literal("pending"),
@@ -39,6 +51,231 @@ const pageSpeedErrorType = v.union(
v.literal("unknown"),
);
const requireOperator = async (ctx: MutationCtx | QueryCtx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("Nicht autorisiert.");
}
};
async function getActivePageSpeedAuditRun(
ctx: MutationCtx | QueryCtx,
leadId: Id<"leads">,
) {
const existingPending = await ctx.db
.query("agentRuns")
.withIndex("by_type_and_status_and_leadId", (q) =>
q.eq("type", "audit").eq("status", "pending").eq("leadId", leadId),
)
.take(1);
if (existingPending[0]) {
return existingPending[0];
}
const existingRunning = await ctx.db
.query("agentRuns")
.withIndex("by_type_and_status_and_leadId", (q) =>
q.eq("type", "audit").eq("status", "running").eq("leadId", leadId),
)
.take(1);
return existingRunning[0] ?? null;
}
async function getActiveAuditGenerationRun(
ctx: MutationCtx | QueryCtx,
leadId: Id<"leads">,
) {
const existingPending = await ctx.db
.query("agentRuns")
.withIndex("by_type_and_status_and_leadId", (q) =>
q
.eq("type", "audit_generation")
.eq("status", "pending")
.eq("leadId", leadId),
)
.take(1);
if (existingPending[0]) {
return existingPending[0];
}
const existingRunning = await ctx.db
.query("agentRuns")
.withIndex("by_type_and_status_and_leadId", (q) =>
q
.eq("type", "audit_generation")
.eq("status", "running")
.eq("leadId", leadId),
)
.take(1);
return existingRunning[0] ?? null;
}
async function getLeadAuditStartState(
ctx: MutationCtx | QueryCtx,
leadId: Id<"leads">,
): Promise<AuditStartState> {
const lead = await ctx.db.get(leadId);
if (!lead) {
return {
leadId,
canStart: false,
reason: "Lead nicht gefunden.",
};
}
if (
lead.priority === "blocked" ||
lead.priority === "defer" ||
lead.blacklistStatus === "blocked" ||
lead.contactStatus === "do_not_contact"
) {
return {
leadId,
canStart: false,
reason: "Lead ist gesperrt oder zurueckgestellt.",
};
}
if (!lead.websiteUrl) {
return {
leadId,
canStart: false,
reason: "Keine Website hinterlegt.",
};
}
const activeAuditRun =
(await getActivePageSpeedAuditRun(ctx, leadId)) ??
(await getActiveAuditGenerationRun(ctx, leadId));
if (activeAuditRun) {
return {
leadId,
canStart: false,
reason: "Audit laeuft bereits.",
activeRunId: activeAuditRun._id,
activeRunStatus: activeAuditRun.status,
};
}
return {
leadId,
canStart: true,
};
}
async function queueLeadPageSpeedAuditForLead(
ctx: MutationCtx,
args: {
leadId: Id<"leads">;
parentRunId?: Id<"agentRuns">;
triggeredBy: "internal" | "manual";
},
): Promise<Id<"agentRuns"> | null> {
const state = await getLeadAuditStartState(ctx, args.leadId);
if (!state.canStart) {
return state.activeRunId ?? null;
}
const now = Date.now();
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:
args.triggeredBy === "manual"
? "Audit-Start wurde manuell angefordert."
: "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 getLeadAuditStartStates = query({
args: {
leadIds: v.array(v.id("leads")),
},
returns: v.array(
v.object({
leadId: v.id("leads"),
canStart: v.boolean(),
reason: v.optional(v.string()),
activeRunId: v.optional(v.id("agentRuns")),
activeRunStatus: v.optional(runStatus),
}),
),
handler: async (ctx, args): Promise<AuditStartState[]> => {
await requireOperator(ctx);
const states: AuditStartState[] = [];
for (const leadId of args.leadIds.slice(0, 120)) {
states.push(await getLeadAuditStartState(ctx, leadId));
}
return states;
},
});
export const requestLeadAudit = mutation({
args: {
leadId: v.id("leads"),
},
returns: v.object({
runId: v.union(v.id("agentRuns"), v.null()),
message: v.string(),
}),
handler: async (ctx, args): Promise<{ runId: Id<"agentRuns"> | null; message: string }> => {
await requireOperator(ctx);
const state = await getLeadAuditStartState(ctx, args.leadId);
if (!state.canStart) {
return {
runId: state.activeRunId ?? null,
message: state.reason ?? "Audit kann aktuell nicht gestartet werden.",
};
}
const runId = await queueLeadPageSpeedAuditForLead(ctx, {
leadId: args.leadId,
triggeredBy: "manual",
});
return {
runId,
message: "Audit-Start wurde manuell angefordert.",
};
},
});
export const queueLeadPageSpeedAudit = internalMutation({
args: {
leadId: v.id("leads"),
@@ -46,68 +283,11 @@ export const queueLeadPageSpeedAudit = internalMutation({
},
returns: v.union(v.id("agentRuns"), v.null()),
handler: async (ctx, args): Promise<Id<"agentRuns"> | 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",
return await queueLeadPageSpeedAuditForLead(ctx, {
leadId: args.leadId,
status: "pending",
currentStep: "pagespeed_insights",
counters: PAGE_SPEED_COUNTER_TEMPLATE,
createdAt: now,
updatedAt: now,
parentRunId: args.parentRunId,
triggeredBy: "internal",
});
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;
},
});