Integrate local business workflow and SaaS redesign
This commit is contained in:
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user