diff --git a/src/app/audits/page.tsx b/src/app/audits/page.tsx index dbb764a..5c8d49a 100644 --- a/src/app/audits/page.tsx +++ b/src/app/audits/page.tsx @@ -446,17 +446,34 @@ export default async function AuditsPage() { ) : runs.length === 0 ? (
- No audit runs yet.{" "} - - Create the first one. +

+ No audits yet +

+

+ Run your first domain audit to start building your evidence library. +

+ + Start an audit
) : ( diff --git a/src/app/intake/page.tsx b/src/app/intake/page.tsx index 377e6aa..d68d7e4 100644 --- a/src/app/intake/page.tsx +++ b/src/app/intake/page.tsx @@ -1,10 +1,15 @@ +import Link from "next/link"; import { submitDomainAction } from "@/app/intake/actions"; import { IntakeSuccessTrigger } from "@/components/intake-success-trigger"; import { SubmitButton } from "@/components/submit-button"; +import { listRecentAuditRuns, type AuditRunListItem } from "@/db/report"; +import { AUDIT_STATUS_META, REPORT_READY_STATUSES } from "@/lib/report-presentation"; // Allow up to 5 minutes for the server-side after() worker trigger to run. export const maxDuration = 300; +export const dynamic = "force-dynamic"; + type SearchParams = Record; function getValue(params: SearchParams, key: string) { @@ -17,6 +22,88 @@ async function resolveSearchParams(searchParams?: Promise) { return Promise.resolve(searchParams ?? {}); } +function formatDate(date: Date | null) { + if (!date) return "—"; + + return new Date(date).toLocaleString("en-GB", { + day: "2-digit", + month: "short", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +} + +function RecentAuditRow({ run }: { run: AuditRunListItem }) { + const meta = AUDIT_STATUS_META[run.status]; + const isReady = REPORT_READY_STATUSES.includes(run.status); + const actionLabel = isReady ? "View report" : "View status"; + + return ( +
+
+

+ {run.domain} +

+

+ {formatDate(run.createdAt)} +

+
+ + {meta.label} + + + {actionLabel} + +
+ ); +} + export default async function IntakePage({ searchParams, }: { @@ -30,81 +117,281 @@ export default async function IntakePage({ const error = getValue(params, "error"); const showError = Boolean(error) && !success; + let recentRuns: AuditRunListItem[] = []; + try { + recentRuns = await listRecentAuditRuns(10); + } catch { + // Recent audits panel is informational — silent fallback keeps intake usable. + } + return ( -
-

- SiteSignal Internal -

-

- Run a brand audit -

-

- Submit a public domain to start an evidence-backed audit of brand clarity, trust signals, conversion path, and experience flow. The pipeline captures up to five priority pages and produces deterministic findings before optional LLM enrichment. -

- -
- - - - - - - {success && auditRunId ? ( - - ) : null} - - {showError ? ( +
+
+ {/* Page header */} +
+
+

+ SiteSignal Internal +

+

+ Brand audit command center +

+

+ Submit a public domain to capture up to five priority pages and produce + deterministic, evidence-backed findings on brand clarity, conversion path, trust + signals, and experience flow. +

+
+ + +
+ + {/* Two-column body */}
-

Unable to create audit job.

-

{error}

- {auditRunId ?

Audit run id: {auditRunId}

: null} - {status ?

Status: {status}

: null} + {/* Left: audit form */} +
+
+

+ New audit +

+

+ Run a brand audit +

+ +
+ + + + + +
+ + {showError ? ( +
+

+ Unable to create audit job. +

+

{error}

+ {auditRunId ? ( +

Audit run id: {auditRunId}

+ ) : null} + {status ?

Status: {status}

: null} +
+ ) : null} + + {success && auditRunId ? ( + + ) : null} +
+ + {/* Right: recent audits */} +
+
+
+
+

+ Recent audits +

+

+ Latest runs +

+
+ + View all → + +
+ + {recentRuns.length === 0 ? ( +
+ No audits yet. Submit a domain above to get started. +
+ ) : ( +
+ {recentRuns.map((run) => ( + + ))} +
+ )} +
+
- ) : null} +
); } diff --git a/src/app/report/[auditRunId]/page.tsx b/src/app/report/[auditRunId]/page.tsx index 733b506..aaa69dc 100644 --- a/src/app/report/[auditRunId]/page.tsx +++ b/src/app/report/[auditRunId]/page.tsx @@ -7,7 +7,6 @@ import { getAuditFailurePresentation } from "@/lib/audit-failure"; import type { OutreachAsset } from "@/lib/types"; import type { ProspectAuditAgentResult } from "@/server/agents/prospect-audit-agent"; import { - AUDIT_STATUS_META, CATEGORY_LABELS, EVIDENCE_COLORS, getReportBadge, diff --git a/tests/intake/intake-page.test.ts b/tests/intake/intake-page.test.ts new file mode 100644 index 0000000..9975916 --- /dev/null +++ b/tests/intake/intake-page.test.ts @@ -0,0 +1,229 @@ +import * as React from "react"; +import { createElement, type ReactNode } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +const { listRecentAuditRunsMock } = vi.hoisted(() => ({ + listRecentAuditRunsMock: vi.fn(), +})); + +vi.mock("next/link", () => ({ + default: ({ + href, + children, + ...props + }: { + href: string; + children: ReactNode; + }) => createElement("a", { href, ...props }, children), +})); + +vi.mock("@/app/intake/actions", () => ({ + submitDomainAction: vi.fn(), +})); + +vi.mock("@/components/submit-button", () => ({ + SubmitButton: ({ label }: { label: string }) => createElement("button", { type: "submit" }, label), +})); + +vi.mock("@/components/intake-success-trigger", () => ({ + IntakeSuccessTrigger: ({ auditRunId, domain }: { auditRunId: string; domain: string }) => + createElement("div", { "data-testid": "success-trigger" }, `${domain} ${auditRunId}`), +})); + +vi.mock("@/db/report", async (importOriginal) => { + const actual = await importOriginal(); + + return { + ...actual, + listRecentAuditRuns: listRecentAuditRunsMock, + }; +}); + +import IntakePage from "@/app/intake/page"; + +vi.stubGlobal("React", React); + +const now = new Date("2026-04-21T09:00:00.000Z"); + +function makeRun(overrides: Partial = {}) { + return { + auditRunId: "run-1", + domain: "example.com", + status: "complete" as const, + createdAt: now, + completedAt: now, + homepageOnly: false, + failureReason: null, + failureKind: null, + failureStage: null, + failureDetails: null, + limitationNote: null, + ...overrides, + }; +} + +describe("IntakePage", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("renders the audit form and page header", async () => { + listRecentAuditRunsMock.mockResolvedValue([]); + + const element = await IntakePage({}); + const html = renderToStaticMarkup(element); + + expect(html).toContain("SiteSignal Internal"); + expect(html).toContain("Brand audit command center"); + expect(html).toContain("domain"); + expect(html).toContain("Start audit"); + }); + + it("renders top-right nav links", async () => { + listRecentAuditRunsMock.mockResolvedValue([]); + + const element = await IntakePage({}); + const html = renderToStaticMarkup(element); + + expect(html).toContain("View all audits"); + expect(html).toContain('href="/audits"'); + expect(html).toContain("Logout"); + expect(html).toContain('href="/internal-logout"'); + }); + + it("renders recent audits section with runs", async () => { + listRecentAuditRunsMock.mockResolvedValue([ + makeRun({ auditRunId: "run-ready", domain: "ready.example", status: "complete" }), + makeRun({ auditRunId: "run-proc", domain: "proc.example", status: "analyzing", completedAt: null }), + ]); + + const element = await IntakePage({}); + const html = renderToStaticMarkup(element); + + expect(html).toContain("Latest runs"); + expect(html).toContain("ready.example"); + expect(html).toContain("proc.example"); + }); + + it("shows View report for report-ready runs", async () => { + listRecentAuditRunsMock.mockResolvedValue([ + makeRun({ auditRunId: "run-ready", domain: "ready.example", status: "complete" }), + ]); + + const element = await IntakePage({}); + const html = renderToStaticMarkup(element); + + expect(html).toContain("View report"); + expect(html).toContain('href="/report/run-ready"'); + }); + + it("shows View report for partial_complete runs", async () => { + listRecentAuditRunsMock.mockResolvedValue([ + makeRun({ auditRunId: "run-partial", domain: "partial.example", status: "partial_complete" }), + ]); + + const element = await IntakePage({}); + const html = renderToStaticMarkup(element); + + expect(html).toContain("View report"); + expect(html).toContain('href="/report/run-partial"'); + }); + + it("shows View status for in-progress runs", async () => { + listRecentAuditRunsMock.mockResolvedValue([ + makeRun({ auditRunId: "run-proc", domain: "proc.example", status: "capturing", completedAt: null }), + ]); + + const element = await IntakePage({}); + const html = renderToStaticMarkup(element); + + expect(html).toContain("View status"); + expect(html).toContain('href="/report/run-proc"'); + }); + + it("shows View status for failed runs", async () => { + listRecentAuditRunsMock.mockResolvedValue([ + makeRun({ auditRunId: "run-fail", domain: "fail.example", status: "failed", failureReason: "Blocked." }), + ]); + + const element = await IntakePage({}); + const html = renderToStaticMarkup(element); + + expect(html).toContain("View status"); + expect(html).toContain('href="/report/run-fail"'); + }); + + it("renders empty recent audits state when no runs exist", async () => { + listRecentAuditRunsMock.mockResolvedValue([]); + + const element = await IntakePage({}); + const html = renderToStaticMarkup(element); + + expect(html).toContain("No audits yet"); + expect(html).not.toContain("View report"); + expect(html).not.toContain("View status"); + }); + + it("renders the form and header even when recent audit query fails", async () => { + listRecentAuditRunsMock.mockRejectedValue(new Error("db unavailable")); + + const element = await IntakePage({}); + const html = renderToStaticMarkup(element); + + expect(html).toContain("Brand audit command center"); + expect(html).toContain("Start audit"); + expect(html).toContain("No audits yet"); + }); + + it("does not expose raw artifact storage URLs", async () => { + listRecentAuditRunsMock.mockResolvedValue([ + makeRun({ auditRunId: "run-1" }), + ]); + + const element = await IntakePage({}); + const html = renderToStaticMarkup(element); + + expect(html).not.toMatch(/storage\.googleapis\.com/); + expect(html).not.toMatch(/blob\.vercel-storage\.com/); + expect(html).not.toMatch(/s3\.amazonaws\.com/); + }); + + it("does not render the success trigger without a successful submission", async () => { + listRecentAuditRunsMock.mockResolvedValue([]); + + const element = await IntakePage({}); + const html = renderToStaticMarkup(element); + + expect(html).not.toContain("data-testid=\"success-trigger\""); + }); + + it("renders error banner when error param is present", async () => { + listRecentAuditRunsMock.mockResolvedValue([]); + + const element = await IntakePage({ + searchParams: Promise.resolve({ error: "Domain is invalid.", auditRunId: "run-x" }), + }); + const html = renderToStaticMarkup(element); + + expect(html).toContain("Unable to create audit job."); + expect(html).toContain("Domain is invalid."); + expect(html).toContain("run-x"); + }); + + it("renders success trigger when success=1 and auditRunId are present", async () => { + listRecentAuditRunsMock.mockResolvedValue([]); + + const element = await IntakePage({ + searchParams: Promise.resolve({ + success: "1", + auditRunId: "run-success", + domain: "good.example", + }), + }); + const html = renderToStaticMarkup(element); + + expect(html).toContain("data-testid=\"success-trigger\""); + expect(html).not.toContain("Unable to create audit job."); + }); +}); diff --git a/tests/report/audits-page.test.ts b/tests/report/audits-page.test.ts index 612caf3..867c561 100644 --- a/tests/report/audits-page.test.ts +++ b/tests/report/audits-page.test.ts @@ -115,6 +115,17 @@ describe("AuditsPage", () => { expect(html).not.toContain("completed using accessible public secondary pages"); }); + it("renders the empty state with a CTA when no runs exist", async () => { + listRecentAuditRunsMock.mockResolvedValue([]); + + const element = await AuditsPage(); + const html = renderToStaticMarkup(element); + + expect(html).toContain("No audits yet"); + expect(html).toContain('href="/intake"'); + expect(html).toContain("Start an audit"); + }); + it("renders a diagnostic state when the audit list query fails", async () => { const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined); const error = new Error("column ar.failure_kind does not exist") as Error & { code: string };