Fix MVP audit evidence pipeline

This commit is contained in:
2026-06-08 08:33:15 +02:00
parent a45b92ea0a
commit ff18fc202e
16 changed files with 771 additions and 52 deletions

View File

@@ -38,9 +38,40 @@ type SkillAwareAudit = {
internalSummary?: string | null;
};
type CheckedPageScreenshot = {
id: Id<"_storage">;
url: string;
viewport: "desktop" | "mobile";
sourceUrl: string;
width: number;
height: number;
createdAt: number;
};
type CheckedPageEvidence = {
url: string;
sourceUrl: string | null;
finalUrl: string | null;
pageKind: string | null;
title: string | null;
metaDescription: string | null;
headings: string[];
visibleTextExcerpt: string | null;
hasContactFormSignal: boolean | null;
hasContactCtaSignal: boolean | null;
usesHttps: boolean | null;
missingMetaDescription: boolean | null;
brokenInternalLinkCount: number | null;
screenshots: CheckedPageScreenshot[];
createdAt: number | null;
};
type AuditDetailResult = {
audit: SkillAwareAudit;
lead: LeadContext | null;
sourceSummaries: {
checkedPages: CheckedPageEvidence[];
};
} | null;
const statusText: Record<string, string> = {
@@ -54,6 +85,42 @@ function getStatusLabel(status: SkillAwareAudit["status"]) {
return statusText[status] ?? "Unbekannt";
}
function getPageKindLabel(pageKind: string | null) {
const labels: Record<string, string> = {
contact: "Kontakt",
homepage: "Startseite",
imprint: "Impressum",
other: "Unterseite",
service: "Leistung",
};
return pageKind ? labels[pageKind] ?? pageKind : "Geprüft";
}
function signalText(value: boolean | null, positive: string, negative: string) {
if (value === null) {
return "Unbekannt";
}
return value ? positive : negative;
}
function metaSignalText(page: CheckedPageEvidence) {
if (page.metaDescription) {
return "Vorhanden";
}
if (page.missingMetaDescription === true) {
return "Fehlt";
}
if (page.missingMetaDescription === false) {
return "Vorhanden";
}
return "Unbekannt";
}
function leadSummary(lead: LeadContext | null | undefined) {
if (!lead) {
return "Kein Lead-Kontext gespeichert";
@@ -90,6 +157,10 @@ export function AuditDetail({ id }: { id: string | Id<"audits"> }) {
const lead = result?.lead;
const usedSkills = useMemo(() => audit?.usedSkills ?? [], [audit]);
const checkedPageEvidence = useMemo(
() => result?.sourceSummaries.checkedPages ?? [],
[result],
);
if (result === null) {
return (
@@ -149,6 +220,89 @@ export function AuditDetail({ id }: { id: string | Id<"audits"> }) {
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Geprüfte Seiten</CardTitle>
<CardDescription>
Kompakte Evidence aus Website-Enrichment und Screenshot-Erfassung.
</CardDescription>
</CardHeader>
<CardContent>
{checkedPageEvidence.length === 0 ? (
<p className="text-sm text-muted-foreground">Keine Seiten-Evidence gespeichert</p>
) : (
<ul className="grid gap-3">
{checkedPageEvidence.map((page, index) => (
<li
className="rounded-md border p-3 text-sm"
key={`${page.url}-${index}`}
>
<div className="flex flex-wrap items-start justify-between gap-2">
<div className="min-w-0">
<p className="break-words font-medium">
{page.title ?? page.finalUrl ?? page.sourceUrl ?? page.url}
</p>
<p className="mt-1 break-all text-xs text-muted-foreground">
{page.finalUrl ?? page.sourceUrl ?? page.url}
</p>
</div>
<Badge variant="outline">{getPageKindLabel(page.pageKind)}</Badge>
</div>
{page.visibleTextExcerpt ? (
<p className="mt-3 line-clamp-3 text-sm text-muted-foreground">
{page.visibleTextExcerpt}
</p>
) : null}
<div className="mt-3 flex flex-wrap gap-2">
<Badge variant={page.missingMetaDescription === true ? "secondary" : "outline"}>
Meta: {metaSignalText(page)}
</Badge>
<Badge variant="outline">
Kontaktformular:{" "}
{signalText(page.hasContactFormSignal, "Signal", "Kein Signal")}
</Badge>
<Badge variant="outline">
CTA: {signalText(page.hasContactCtaSignal, "Signal", "Kein Signal")}
</Badge>
<Badge variant="outline">
Interne Links: {page.brokenInternalLinkCount ?? "Unbekannt"}
</Badge>
</div>
{page.screenshots.length > 0 ? (
<div className="mt-3 grid gap-2 sm:grid-cols-2">
{page.screenshots.map((screenshot) => (
<figure
className="overflow-hidden rounded-md border bg-muted/20"
key={`${screenshot.id}-${screenshot.viewport}`}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={screenshot.url}
alt={`${screenshot.viewport === "desktop" ? "Desktop" : "Mobile"} Screenshot von ${screenshot.sourceUrl}`}
width={screenshot.width}
height={screenshot.height}
className="aspect-[16/10] w-full object-cover"
/>
<figcaption className="flex items-center justify-between gap-2 border-t px-2 py-1 text-xs text-muted-foreground">
<span className="font-medium">
{screenshot.viewport === "desktop" ? "Desktop" : "Mobil"}
</span>
<span className="truncate">{screenshot.sourceUrl}</span>
</figcaption>
</figure>
))}
</div>
) : null}
</li>
))}
</ul>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Verwendete Skills</CardTitle>