Surface audit generations on dashboard audits
This commit is contained in:
159
convex/audits.ts
159
convex/audits.ts
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user