Refactor pipeline task handling and UI flows

This commit is contained in:
2026-06-13 21:09:49 +02:00
parent 21c7e4c9a4
commit ff4c572157
24 changed files with 1346 additions and 236 deletions

View File

@@ -1,9 +1,11 @@
import { v } from "convex/values";
import { normalizeListLimit } from "./domain";
import { internal } from "./_generated/api";
import { internalMutation, mutation, query } from "./_generated/server";
import type { Doc, Id } from "./_generated/dataModel";
import type { MutationCtx, QueryCtx } from "./_generated/server";
import { getAuditProgressForStep } from "../lib/audits/progress";
export const AUDIT_REVIEW_NOTICE_AFTER_MS = 30 * 24 * 60 * 60 * 1000;
const DETAIL_EVIDENCE_LIMIT = 50;
@@ -63,12 +65,27 @@ type AuditDashboardRow =
id: Id<"agentRuns">;
runId: Id<"agentRuns">;
leadId: Id<"leads"> | null;
runType: Doc<"agentRuns">["type"];
title: string;
checkedDomain: string;
status: Doc<"agentRuns">["status"];
latestStage: string;
stageStatus: Doc<"agentRuns">["status"];
errorSummary: string | null;
progress: {
step: number;
total: number;
label: string;
percent: number;
};
retry: {
attempt: number;
maxAttempts: number;
isRetrying: boolean;
lastRetryReason: string | null;
canRetry: boolean;
};
canRetry: boolean;
pageCount: number;
checkedPages: string[];
createdAt: number;
@@ -104,6 +121,38 @@ const latestGenerationStage = (stages: Doc<"auditGenerations">[]) => {
return [...stages].sort((a, b) => b.updatedAt - a.updatedAt)[0] ?? null;
};
const progressForRun = (
run: Doc<"agentRuns">,
latestStage: Doc<"auditGenerations"> | null,
) => {
const fallback = getAuditProgressForStep(latestStage?.stage ?? run.currentStep);
return {
step: run.progressStep ?? fallback.step,
total: run.progressTotal ?? fallback.total,
label: run.progressLabel ?? fallback.label,
percent: run.progressPercent ?? fallback.percent,
};
};
const retryForRun = (run: Doc<"agentRuns">) => {
const attempt = run.attempt ?? 1;
const maxAttempts = run.maxAttempts ?? 3;
const canRetry =
run.type === "audit" &&
(run.status === "failed" || run.status === "canceled") &&
attempt < maxAttempts;
return {
attempt,
maxAttempts,
isRetrying:
(run.status === "pending" || run.status === "running") && attempt > 1,
lastRetryReason: run.lastRetryReason ?? null,
canRetry,
};
};
const normalizeComparableAuditUrl = (value: string | null | undefined) => {
const trimmed = value?.trim();
if (!trimmed) {
@@ -727,6 +776,31 @@ export const list = query({
},
});
export const retryAuditRun = mutation({
args: {
runId: v.id("agentRuns"),
},
handler: async (ctx, args) => {
await requireOperator(ctx);
const run = await ctx.db.get(args.runId);
if (!run || run.type !== "audit") {
throw new Error("Audit-Run wurde nicht gefunden.");
}
const status = run.status;
if (status !== "failed" && status !== "canceled") {
throw new Error("Nur final fehlgeschlagene Audits können neu gestartet werden.");
}
await ctx.scheduler.runAfter(0, internal.auditWorkflow.restartAuditWorkflow, {
runId: args.runId,
});
return { runId: args.runId };
},
});
export const listDashboardRows = query({
args: {
limit: v.optional(v.number()),
@@ -771,29 +845,50 @@ export const listDashboardRows = query({
}
}
const rootAuditRuns = await ctx.db
.query("agentRuns")
.withIndex("by_type", (q) => q.eq("type", "audit"))
.order("desc")
.take(limit);
const rootAuditRunLeadIds = new Set(
rootAuditRuns
.map((run) => run.leadId)
.filter((leadId): leadId is Id<"leads"> => leadId !== undefined),
);
const generationRuns = await ctx.db
.query("agentRuns")
.withIndex("by_type", (q) => q.eq("type", "audit_generation"))
.order("desc")
.take(limit);
for (const run of generationRuns) {
for (const run of [...rootAuditRuns, ...generationRuns]) {
if (!run.leadId) {
continue;
}
if (
run.type === "audit_generation" &&
rootAuditRunLeadIds.has(run.leadId)
) {
continue;
}
const directFinalAudit = run.auditId ? await ctx.db.get(run.auditId) : null;
const leadFinalAudits = await ctx.db
.query("audits")
.withIndex("by_leadId", (q) => q.eq("leadId", run.leadId as Id<"leads">))
.take(1);
const shouldHideBehindFinalAudit =
run.status === "succeeded" || run.type === "audit_generation";
if (
finalAuditRunIds.has(run._id) ||
(run.auditId && finalAuditIds.has(run.auditId)) ||
directFinalAudit ||
finalAuditLeadIds.has(run.leadId) ||
leadFinalAudits.length > 0
(shouldHideBehindFinalAudit && finalAuditRunIds.has(run._id)) ||
(shouldHideBehindFinalAudit && run.auditId && finalAuditIds.has(run.auditId)) ||
(shouldHideBehindFinalAudit && directFinalAudit) ||
(shouldHideBehindFinalAudit && finalAuditLeadIds.has(run.leadId)) ||
(shouldHideBehindFinalAudit && leadFinalAudits.length > 0)
) {
continue;
}
@@ -806,18 +901,24 @@ export const listDashboardRows = query({
const latestStage = latestGenerationStage(stages);
const lead = await ctx.db.get(run.leadId);
const checkedDomain = domainFromLead(lead);
const progress = progressForRun(run, latestStage);
const retry = retryForRun(run);
rows.push({
kind: "generation",
id: run._id,
runId: run._id,
leadId: run.leadId,
runType: run.type,
title: lead?.companyName ?? checkedDomain,
checkedDomain,
status: run.status,
latestStage: latestStage?.stage ?? run.currentStep ?? "audit_generation",
stageStatus: latestStage?.status ?? run.status,
errorSummary: run.errorSummary ?? latestStage?.errorSummary ?? null,
progress,
retry,
canRetry: retry.canRetry,
pageCount: 0,
checkedPages: [],
createdAt: run.createdAt,