Add campaign scheduling lifecycle jobs
This commit is contained in:
199
convex/scheduledJobs.ts
Normal file
199
convex/scheduledJobs.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { internal } from "./_generated/api";
|
||||
import { internalMutation } from "./_generated/server";
|
||||
import { canStartAgentRun, isStalePendingAgentRun } from "../lib/lead-discovery-run";
|
||||
|
||||
export const AUDIT_REVIEW_NOTICE_AFTER_MS = 30 * 24 * 60 * 60 * 1000;
|
||||
export const AUDIT_AUTO_DEACTIVATE_AFTER_MS = 60 * 24 * 60 * 60 * 1000;
|
||||
|
||||
const RUN_COUNTERS_ZERO = {
|
||||
leadsFound: 0,
|
||||
leadsCreated: 0,
|
||||
auditsCreated: 0,
|
||||
outreachPrepared: 0,
|
||||
errors: 0,
|
||||
};
|
||||
|
||||
export const runDueCampaigns = internalMutation({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const now = Date.now();
|
||||
const activeRuns = [
|
||||
...(await ctx.db
|
||||
.query("agentRuns")
|
||||
.withIndex("by_status", (q) => q.eq("status", "pending"))
|
||||
.take(20)),
|
||||
...(await ctx.db
|
||||
.query("agentRuns")
|
||||
.withIndex("by_status", (q) => q.eq("status", "running"))
|
||||
.take(20)),
|
||||
];
|
||||
|
||||
for (const run of activeRuns.filter((run) => isStalePendingAgentRun(run, now))) {
|
||||
await ctx.db.patch(run._id, {
|
||||
status: "canceled",
|
||||
currentStep: "campaign_cron_stale_pending",
|
||||
errorSummary: "Ausstehender Lauf wurde nach Timeout automatisch abgebrochen.",
|
||||
finishedAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
await ctx.db.insert("agentRunEvents", {
|
||||
runId: run._id,
|
||||
level: "warning",
|
||||
message: "Ausstehender Lauf wurde nach Timeout automatisch abgebrochen.",
|
||||
createdAt: now,
|
||||
});
|
||||
}
|
||||
|
||||
if (!canStartAgentRun(activeRuns, now)) {
|
||||
const skippedRunId = await ctx.db.insert("agentRuns", {
|
||||
type: "campaign",
|
||||
status: "canceled",
|
||||
currentStep: "campaign_cron_skipped",
|
||||
errorSummary: "Es läuft bereits ein Agentenlauf.",
|
||||
counters: RUN_COUNTERS_ZERO,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
finishedAt: now,
|
||||
});
|
||||
|
||||
await ctx.db.insert("agentRunEvents", {
|
||||
runId: skippedRunId,
|
||||
level: "warning",
|
||||
message: "Es läuft bereits ein Agentenlauf. Kampagnen-Cron wurde übersprungen.",
|
||||
createdAt: now,
|
||||
});
|
||||
|
||||
return { started: 0, skipped: 1 };
|
||||
}
|
||||
|
||||
const dueCampaigns = await ctx.db
|
||||
.query("campaigns")
|
||||
.withIndex("by_status_and_nextRunAt", (q) =>
|
||||
q.eq("status", "active").lte("nextRunAt", now),
|
||||
)
|
||||
.take(1);
|
||||
const campaign = dueCampaigns[0];
|
||||
|
||||
if (!campaign || campaign.recurrence === "manual") {
|
||||
return { started: 0, skipped: 0 };
|
||||
}
|
||||
|
||||
const runId = await ctx.db.insert("agentRuns", {
|
||||
type: "campaign",
|
||||
campaignId: campaign._id,
|
||||
status: "pending",
|
||||
currentStep: "campaign_cron_queued",
|
||||
counters: RUN_COUNTERS_ZERO,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
await ctx.db.insert("agentRunEvents", {
|
||||
runId,
|
||||
level: "info",
|
||||
message: "Kampagnenlauf wurde durch Cadence-Cron geplant.",
|
||||
details: [{ label: "Kampagne", value: campaign.name }],
|
||||
createdAt: now,
|
||||
});
|
||||
|
||||
await ctx.scheduler.runAfter(0, internal.leadDiscovery.processCampaignRun, {
|
||||
runId,
|
||||
});
|
||||
|
||||
return { started: 1, skipped: 0 };
|
||||
},
|
||||
});
|
||||
|
||||
export const runAuditLifecycle = internalMutation({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const now = Date.now();
|
||||
const runId = await ctx.db.insert("agentRuns", {
|
||||
type: "lifecycle",
|
||||
status: "running",
|
||||
currentStep: "audit_lifecycle",
|
||||
counters: RUN_COUNTERS_ZERO,
|
||||
startedAt: now,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
let notifications = 0;
|
||||
let deactivated = 0;
|
||||
|
||||
const publishedAudits = await ctx.db
|
||||
.query("audits")
|
||||
.withIndex("by_status", (q) => q.eq("status", "published"))
|
||||
.take(100);
|
||||
|
||||
for (const audit of publishedAudits) {
|
||||
const publishedAt = audit.publishedAt ?? audit.updatedAt;
|
||||
const extendedUntil = audit.lifecycleExtendedUntil ?? 0;
|
||||
const isExtended = extendedUntil > now;
|
||||
|
||||
if (!isExtended && now - publishedAt >= AUDIT_AUTO_DEACTIVATE_AFTER_MS) {
|
||||
await ctx.db.patch(audit._id, {
|
||||
status: "deactivated",
|
||||
deactivatedAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
await ctx.db.insert("dashboardNotifications", {
|
||||
auditId: audit._id,
|
||||
runId,
|
||||
kind: "audit_auto_deactivated",
|
||||
title: "Audit automatisch deaktiviert",
|
||||
message: "Ein veröffentlichtes Audit war älter als 60 Tage und wurde deaktiviert.",
|
||||
status: "unread",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
deactivated += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
!audit.lifecycleNotificationAt &&
|
||||
now - publishedAt >= AUDIT_REVIEW_NOTICE_AFTER_MS
|
||||
) {
|
||||
await ctx.db.patch(audit._id, {
|
||||
lifecycleNotificationAt: now,
|
||||
reviewDueAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
await ctx.db.insert("dashboardNotifications", {
|
||||
auditId: audit._id,
|
||||
runId,
|
||||
kind: "audit_review_due",
|
||||
title: "Audit-Aktivität prüfen",
|
||||
message: "Soll dieses Audit aktiv bleiben? Es ist seit 30 Tagen veröffentlicht.",
|
||||
status: "unread",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
notifications += 1;
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.db.patch(runId, {
|
||||
status: "succeeded",
|
||||
finishedAt: now,
|
||||
updatedAt: now,
|
||||
counters: {
|
||||
...RUN_COUNTERS_ZERO,
|
||||
auditsCreated: notifications,
|
||||
errors: deactivated,
|
||||
},
|
||||
});
|
||||
await ctx.db.insert("agentRunEvents", {
|
||||
runId,
|
||||
level: "info",
|
||||
message: "Audit-Lifecycle geprüft.",
|
||||
details: [
|
||||
{ label: "Hinweise", value: String(notifications) },
|
||||
{ label: "Deaktiviert", value: String(deactivated) },
|
||||
],
|
||||
createdAt: now,
|
||||
});
|
||||
|
||||
return { notifications, deactivated };
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user