Surface audit generations on dashboard audits

This commit is contained in:
2026-06-06 18:14:27 +02:00
parent 3efbc06e40
commit e9463e8ef2
20 changed files with 3181 additions and 38 deletions

View File

@@ -2,6 +2,7 @@ import { v } from "convex/values";
import { normalizeListLimit } from "./domain";
import { internalMutation, mutation, query } from "./_generated/server";
import type { Doc, Id } from "./_generated/dataModel";
import type { MutationCtx, QueryCtx } from "./_generated/server";
export const AUDIT_REVIEW_NOTICE_AFTER_MS = 30 * 24 * 60 * 60 * 1000;
@@ -40,13 +41,67 @@ const publicOfferValidator = v.object({
ctaHref: v.optional(v.string()),
});
const requireOperator = async (ctx: MutationCtx) => {
type AuditDashboardRow =
| {
kind: "audit";
id: Id<"audits">;
auditId: Id<"audits">;
slug: string;
title: string;
checkedDomain: string;
status: Doc<"audits">["status"];
pageCount: number;
checkedPages: string[];
detailHref: string;
createdAt: number;
updatedAt: number;
}
| {
kind: "generation";
id: Id<"agentRuns">;
runId: Id<"agentRuns">;
leadId: Id<"leads"> | null;
title: string;
checkedDomain: string;
status: Doc<"agentRuns">["status"];
latestStage: string;
stageStatus: Doc<"agentRuns">["status"];
errorSummary: string | null;
pageCount: number;
checkedPages: string[];
createdAt: number;
updatedAt: number;
};
const requireOperator = async (ctx: MutationCtx | QueryCtx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("Nicht autorisiert.");
}
};
const domainFromLead = (
lead: Pick<Doc<"leads">, "companyName" | "websiteDomain" | "websiteUrl"> | null,
) => {
if (lead?.websiteDomain) {
return lead.websiteDomain;
}
if (lead?.websiteUrl) {
try {
return new URL(lead.websiteUrl).hostname;
} catch {
return lead.websiteUrl;
}
}
return "unbekannte-domain";
};
const latestGenerationStage = (stages: Doc<"auditGenerations">[]) => {
return [...stages].sort((a, b) => b.updatedAt - a.updatedAt)[0] ?? null;
};
const toIsoDate = (timestamp: number | undefined, fallback: number) => {
return new Date(timestamp ?? fallback).toISOString();
};
@@ -466,3 +521,105 @@ export const list = query({
return await ctx.db.query("audits").order("desc").take(limit);
},
});
export const listDashboardRows = query({
args: {
limit: v.optional(v.number()),
},
handler: async (ctx: QueryCtx, args): Promise<AuditDashboardRow[]> => {
await requireOperator(ctx);
const limit = normalizeListLimit(args.limit);
const audits = await ctx.db.query("audits").order("desc").take(limit);
const finalAuditLeadIds = new Set<string>();
const finalAuditRunIds = new Set<string>();
const finalAuditIds = new Set<string>();
const rows: AuditDashboardRow[] = audits.map((audit) => {
finalAuditLeadIds.add(audit.leadId);
finalAuditIds.add(audit._id);
return {
kind: "audit",
id: audit._id,
auditId: audit._id,
slug: audit.slug,
title: audit.slug,
checkedDomain: audit.checkedDomain,
status: audit.status,
pageCount: audit.checkedPages.length,
checkedPages: audit.checkedPages,
detailHref: `/dashboard/audits/${audit._id}`,
createdAt: audit.createdAt,
updatedAt: audit.updatedAt,
};
});
for (const audit of audits) {
const linkedGenerations = await ctx.db
.query("auditGenerations")
.withIndex("by_auditId", (q) => q.eq("auditId", audit._id))
.take(20);
for (const generation of linkedGenerations) {
finalAuditRunIds.add(generation.runId);
}
}
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) {
if (!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);
if (
finalAuditRunIds.has(run._id) ||
(run.auditId && finalAuditIds.has(run.auditId)) ||
directFinalAudit ||
finalAuditLeadIds.has(run.leadId) ||
leadFinalAudits.length > 0
) {
continue;
}
const stages = await ctx.db
.query("auditGenerations")
.withIndex("by_runId", (q) => q.eq("runId", run._id))
.order("desc")
.take(20);
const latestStage = latestGenerationStage(stages);
const lead = await ctx.db.get(run.leadId);
const checkedDomain = domainFromLead(lead);
rows.push({
kind: "generation",
id: run._id,
runId: run._id,
leadId: run.leadId,
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,
pageCount: 0,
checkedPages: [],
createdAt: run.createdAt,
updatedAt: Math.max(run.updatedAt, latestStage?.updatedAt ?? 0),
});
}
return rows.sort((a, b) => b.updatedAt - a.updatedAt).slice(0, limit);
},
});