Files
2026-03-19 16:04:17 -06:00

576 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 3: Agent Portal Shell - Research
**Researched:** 2026-03-19
**Domain:** Next.js 16 App Router — authenticated portal UI, Drizzle ORM schema extension, Server Actions, Tailwind v4 utility-class modals
**Confidence:** HIGH
<user_constraints>
## User Constraints (from CONTEXT.md)
### Locked Decisions
**Portal Navigation**
- Top nav (horizontal nav bar) — not sidebar
- Agent lands on Dashboard after login
- All portal routes live under a `/portal` prefix (e.g., `/portal/dashboard`, `/portal/clients`, `/portal/clients/[id]`)
- Visual feel: same brand as marketing site (shared colors/fonts) but cleaner and more utilitarian — clearly a logged-in app, not a marketing page
**Client List Design**
- Card grid layout (not table/list)
- Each card shows: client name, email, document count, last activity date
- New client created via modal/dialog triggered from the client list page (not a separate page)
- Empty state: friendly message ("No clients yet") with a prominent "+ Add your first client" CTA button
**Dashboard Document View**
- Table layout with color-coded status badges: Draft=gray, Sent=blue, Viewed=amber/yellow, Signed=green
- Columns: Document name, Client, Status badge, Date sent
- Controls: filter by status (dropdown) + sort by date (most recent first)
- Phase 3 seeds a few placeholder document rows (real documents arrive in Phase 4)
- Empty state not needed since data is seeded
**Client Profile Page**
- Two-section layout: client details header (name, email, edit button) above a documents table
- Back link to Clients list
- Edit client: modal/dialog (consistent with create pattern) — no inline editing
- Documents section: same table style as dashboard (doc name, status badge, date)
- Delete client: delete button on profile with confirmation dialog before removing
### Claude's Discretion
- Exact card dimensions and spacing on client grid
- Color palette values for status badges (within brand colors)
- Skeleton/loading states
- Exact wording for empty states and confirmation dialogs
- Mobile responsiveness details
### Deferred Ideas (OUT OF SCOPE)
- None — discussion stayed within phase scope
</user_constraints>
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|-----------------|
| CLIENT-01 | Agent can create a client record with name and email address | Server Action + Drizzle insert pattern; modal form with useActionState |
| CLIENT-02 | Agent can view a list of all clients | Drizzle select with LEFT JOIN to count documents; card grid layout |
| CLIENT-03 | Agent can view a client's profile and their associated documents | Dynamic route `/portal/clients/[id]`; params is a Promise in Next.js 16; two-section layout |
| DASH-01 | Agent can see all documents with their current status: Draft / Sent / Viewed / Signed | Placeholder documents table seeded in DB; status badge pattern with Tailwind |
| DASH-02 | Agent can see which client each document was sent to and when | Drizzle JOIN across clients + documents tables; columns: document name, client, status, date sent |
</phase_requirements>
---
## Summary
This phase builds the authenticated portal shell for the Teressa Copeland Homes agent. The existing foundation (Phase 1) established authentication under `/agent/:path*` with Next-Auth v5 beta. Phase 3 extends to a new `/portal` prefix with a proper top-nav layout and three views: Dashboard, Clients list, and Client Profile.
The most important integration challenge is the routing prefix change: CONTEXT.md requires `/portal/dashboard`, `/portal/clients`, etc., but the existing middleware and auth config protect `/agent/:path*` and redirect logins to `/agent/login`. The middleware matcher and `auth.config.ts` authorized callback must be updated to also cover `/portal/:path*`. The existing `/agent/(protected)/` route group should be migrated or the portal routes added as a new protected segment. The login redirect should remain `/agent/login` (same page, different portal prefix).
The schema needs two new tables: `clients` and `documents`. The `documents` table in Phase 3 is a stub — it holds name, status, client_id, and sent_at. Real document content (PDF blobs, signature fields) arrives in Phases 45. Phase 3 seeds placeholder document rows using the existing `scripts/seed.ts` pattern via `npm run db:seed`.
**Primary recommendation:** Add `/portal` to middleware matcher alongside `/agent`; create `src/app/portal/(protected)/` route group with its own layout.tsx containing the top-nav; add `clients` and `documents` (stub) tables to schema; all mutations via Server Actions with `revalidatePath`; modals as pure client components with `useActionState`.
---
## Standard Stack
### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| Next.js | 16.2.0 (pinned in package.json) | App Router, Server Actions, layouts, dynamic routes | Already in project; provides SSR + Server Actions for free |
| Drizzle ORM | ^0.45.1 (in package.json) | Schema definition, migrations, typed queries | Already established pattern in project |
| next-auth | 5.0.0-beta.30 (pinned) | Session access in Server Components, middleware | Pinned exact version — DO NOT upgrade |
| Zod | ^4.3.6 (in package.json) | Server Action input validation | Already used in auth.ts; same pattern for client forms |
| Tailwind CSS | ^4 (in package.json) | Styling — utility classes, CSS variables via `@theme` | Already configured; brand variables defined in globals.css |
| lucide-react | ^0.577.0 (in package.json) | Icons (UserPlus, Users, LayoutDashboard, etc.) | Already installed in Phase 2 |
| postgres | ^3.4.8 (in package.json) | PostgreSQL driver | Already used |
### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| tsx | ^4.21.0 (devDep) | Run seed scripts | Already used for `scripts/seed.ts` |
| drizzle-kit | ^0.31.10 (devDep) | Generate and run migrations | Already used |
### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| Plain Tailwind modals | shadcn/ui Dialog | shadcn is not installed; adding it adds complexity; raw Tailwind modals are 2030 lines and fully controlled |
| Server Action mutations | Route Handlers (API routes) | Server Actions are simpler — no fetch, no JSON, same validation pipeline; already used in Phase 2 (contact form) |
| Separate "create" page | Route-intercepting modal | Intercepting routes add parallel route complexity; client-side state modal is simpler for a single-agent app |
**Installation:** No new packages needed. All dependencies are already installed.
---
## Architecture Patterns
### Recommended Project Structure
```
src/app/
├── agent/ # Existing — keep intact
│ ├── login/ # Existing login page
│ └── (protected)/ # Existing minimal layout — CAN BE MIGRATED
│ └── dashboard/ # Phase 1 stub — REPLACE with redirect to /portal/dashboard
├── portal/ # NEW — Phase 3
│ ├── (protected)/ # Route group — applies portal layout
│ │ ├── layout.tsx # Top-nav + auth check
│ │ ├── dashboard/
│ │ │ └── page.tsx # DASH-01, DASH-02
│ │ ├── clients/
│ │ │ └── page.tsx # CLIENT-01, CLIENT-02 (card grid + create modal)
│ │ └── clients/
│ │ └── [id]/
│ │ └── page.tsx # CLIENT-03 (profile + documents table)
│ └── _components/ # Portal-scoped components (not shared with marketing)
│ ├── PortalNav.tsx # Top nav component
│ ├── ClientCard.tsx # Card for clients grid
│ ├── ClientModal.tsx # Create/edit modal (shared mode prop)
│ ├── ConfirmDialog.tsx # Delete confirmation dialog
│ ├── StatusBadge.tsx # Color-coded status badge
│ └── DocumentsTable.tsx # Reusable table for dashboard + profile
src/lib/
├── db/
│ ├── schema.ts # ADD: clients + documents tables
│ └── index.ts # Existing lazy proxy singleton — unchanged
└── actions/
└── clients.ts # NEW: createClient, updateClient, deleteClient server actions
scripts/
└── seed.ts # EXTEND: add client + placeholder document rows
drizzle/
└── 0001_....sql # NEW migration for clients + documents tables
```
### Pattern 1: Portal Layout with Top Nav and Auth Check
**What:** A route-group layout that checks session and renders top nav.
**When to use:** Every portal page.
```typescript
// src/app/portal/(protected)/layout.tsx
// Source: /teressa-copeland-homes/node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/layout.md
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
import { PortalNav } from "../_components/PortalNav";
export default async function PortalLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await auth();
if (!session) redirect("/agent/login");
return (
<div className="min-h-screen bg-[var(--cream)]">
<PortalNav userEmail={session.user?.email ?? ""} />
<main className="max-w-7xl mx-auto px-6 py-8">{children}</main>
</div>
);
}
```
### Pattern 2: Dynamic Route — params is a Promise (Next.js 16)
**What:** In Next.js 16, `params` prop is a Promise, not a plain object.
**When to use:** Every page with `[id]` segment.
```typescript
// src/app/portal/(protected)/clients/[id]/page.tsx
// Source: /teressa-copeland-homes/node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/page.md
export default async function ClientProfilePage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
// db query with id...
}
```
### Pattern 3: Server Action with Zod Validation + revalidatePath
**What:** Validated server mutation that refreshes the relevant page cache.
**When to use:** createClient, updateClient, deleteClient.
```typescript
// src/lib/actions/clients.ts
// Source: /teressa-copeland-homes/node_modules/next/dist/docs/01-app/02-guides/forms.md
"use server";
import { z } from "zod";
import { db } from "@/lib/db";
import { clients } from "@/lib/db/schema";
import { auth } from "@/lib/auth";
import { revalidatePath } from "next/cache";
const clientSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Valid email required"),
});
export async function createClient(
_prevState: { error?: string } | null,
formData: FormData
) {
const session = await auth();
if (!session) return { error: "Unauthorized" };
const parsed = clientSchema.safeParse({
name: formData.get("name"),
email: formData.get("email"),
});
if (!parsed.success) {
return { error: parsed.error.flatten().fieldErrors.name?.[0] ?? "Invalid input" };
}
await db.insert(clients).values(parsed.data);
revalidatePath("/portal/clients");
return { success: true };
}
```
### Pattern 4: Client Component Modal with useActionState
**What:** Modal opened by client-side state, form submits Server Action, closes on success.
**When to use:** Create client, edit client dialogs.
```typescript
// src/app/portal/_components/ClientModal.tsx
// Source: /teressa-copeland-homes/node_modules/next/dist/docs/01-app/02-guides/forms.md (useActionState)
"use client";
import { useActionState, useEffect } from "react";
import { createClient } from "@/lib/actions/clients";
type Props = {
isOpen: boolean;
onClose: () => void;
};
export function ClientModal({ isOpen, onClose }: Props) {
const [state, formAction, pending] = useActionState(createClient, null);
useEffect(() => {
if (state?.success) onClose();
}, [state, onClose]);
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-white rounded-xl shadow-xl p-6 w-full max-w-md">
<h2 className="text-lg font-semibold text-[var(--navy)] mb-4">Add Client</h2>
<form action={formAction} className="space-y-4">
<input name="name" type="text" placeholder="Full name" required
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm" />
<input name="email" type="email" placeholder="Email address" required
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm" />
{state?.error && (
<p className="text-sm text-red-600">{state.error}</p>
)}
<div className="flex justify-end gap-3 pt-2">
<button type="button" onClick={onClose}
className="px-4 py-2 text-sm text-gray-600 hover:text-gray-900">
Cancel
</button>
<button type="submit" disabled={pending}
className="px-4 py-2 text-sm bg-[var(--navy)] text-white rounded-lg disabled:opacity-60">
{pending ? "Saving..." : "Add Client"}
</button>
</div>
</form>
</div>
</div>
);
}
```
### Pattern 5: Status Badge Component
**What:** Reusable color-coded pill for document status.
**When to use:** Dashboard table + client profile documents table.
```typescript
// src/app/portal/_components/StatusBadge.tsx
type DocumentStatus = "Draft" | "Sent" | "Viewed" | "Signed";
const STATUS_STYLES: Record<DocumentStatus, string> = {
Draft: "bg-gray-100 text-gray-600",
Sent: "bg-blue-100 text-blue-700",
Viewed: "bg-amber-100 text-amber-700",
Signed: "bg-green-100 text-green-700",
};
export function StatusBadge({ status }: { status: DocumentStatus }) {
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${STATUS_STYLES[status]}`}>
{status}
</span>
);
}
```
### Pattern 6: Drizzle Schema with Relations
**What:** New `clients` and `documents` (stub) tables with foreign key.
**When to use:** Phase 3 schema migration.
```typescript
// src/lib/db/schema.ts — additions
// Source: Drizzle ORM docs (verified against node_modules/drizzle-orm)
import { pgTable, text, timestamp, pgEnum } from "drizzle-orm/pg-core";
export const users = pgTable("users", { /* existing */ });
export const clients = pgTable("clients", {
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
name: text("name").notNull(),
email: text("email").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
export const documentStatusEnum = pgEnum("document_status", [
"Draft", "Sent", "Viewed", "Signed"
]);
export const documents = pgTable("documents", {
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
name: text("name").notNull(),
clientId: text("client_id").notNull().references(() => clients.id, { onDelete: "cascade" }),
status: documentStatusEnum("status").notNull().default("Draft"),
sentAt: timestamp("sent_at"),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
```
### Pattern 7: Dashboard Query with JOIN
**What:** Fetch all documents with client name in one query.
**When to use:** Dashboard page server component data fetch.
```typescript
// Inside dashboard/page.tsx (server component)
import { db } from "@/lib/db";
import { documents, clients } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
const rows = await db
.select({
id: documents.id,
name: documents.name,
status: documents.status,
sentAt: documents.sentAt,
clientName: clients.name,
clientId: documents.clientId,
})
.from(documents)
.leftJoin(clients, eq(documents.clientId, clients.id))
.orderBy(/* desc(documents.sentAt) or desc(documents.createdAt) */);
```
### Anti-Patterns to Avoid
- **Synchronous params access on Next.js 16:** `params.id` crashes. Always `const { id } = await params` first.
- **Fetching data in Client Components with useEffect:** Portal pages are server components — query DB directly, pass data as props to client child components.
- **Inline editing of client fields:** CONTEXT.md locked this to modal/dialog only — no `contentEditable` or inline input trick.
- **Separate page for "create client":** The decision is modal-on-client-list — do not create `/portal/clients/new/page.tsx`.
- **Calling `revalidatePath` without 'use server' boundary:** It only works in Server Functions and Route Handlers, not in Client Components.
- **Hover event handlers on Server Components:** Already a known project pitfall from Phase 2 — use Tailwind `hover:` classes, never `onMouseEnter`/`onMouseLeave` on server components.
---
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Form validation | Custom validators | Zod (already installed) | Already established pattern in auth.ts |
| Dialog/modal | CSS + JS from scratch | 30-line Tailwind modal pattern (see Pattern 4 above) | Simpler than installing shadcn for two dialogs |
| UUID generation | `uuid` package | `crypto.randomUUID()` (already used in schema.ts) | Built-in, already established |
| Cache invalidation after mutation | Router.refresh() hack | `revalidatePath` from `next/cache` | Official Next.js 16 pattern for server actions |
| Filter/sort state | URL query string manually | `useSearchParams` + `useRouter` | Filter dropdown state can be URL-driven for shareability |
**Key insight:** The project already has all necessary packages. Adding shadcn, headlessui, or any other UI component library is unnecessary overhead for the two modals this phase requires.
---
## Common Pitfalls
### Pitfall 1: Middleware Doesn't Cover `/portal`
**What goes wrong:** The existing `middleware.ts` matcher is `["/agent/:path*"]` only. Routes under `/portal/` will not be protected — unauthenticated users can access portal pages.
**Why it happens:** Middleware matcher is explicitly set to `/agent` only.
**How to avoid:** Update `middleware.ts` matcher to `["/agent/:path*", "/portal/:path*"]` AND update `auth.config.ts` `authorized` callback to include `nextUrl.pathname.startsWith("/portal")` check.
**Warning signs:** Dashboard page renders without session check passing.
### Pitfall 2: Login Redirect Loops with New `/portal` Prefix
**What goes wrong:** The `authorized` callback in `auth.config.ts` redirects to `/agent/dashboard` on successful login from login page. If the agent is already on a `/portal` route, double-redirect can occur.
**Why it happens:** Hard-coded redirect destination in `authorized` callback.
**How to avoid:** Change the login-success redirect in `auth.config.ts` from `/agent/dashboard` to `/portal/dashboard`. Keep the login page at `/agent/login` (user decision: portal prefix is `/portal/dashboard`, not `/portal/login`).
**Warning signs:** After login, browser navigates to `/agent/dashboard` (old route) not `/portal/dashboard`.
### Pitfall 3: `params` Is a Promise in Next.js 16
**What goes wrong:** `const { id } = params` on a client profile page returns `undefined`; the page renders with no data or throws.
**Why it happens:** Next.js 15+ changed `params` from a synchronous object to a Promise. In Next.js 16, accessing it synchronously may still "work" during local dev (backwards compat shim) but causes issues at build time.
**How to avoid:** Always `const { id } = await params` in async server components.
**Warning signs:** `id` is `undefined` on profile page, or TypeScript error about `Promise<{id:string}>` not having `.id`.
### Pitfall 4: `revalidatePath` Import Location
**What goes wrong:** `import { revalidatePath } from "next/cache"` fails or throws in a Client Component.
**Why it happens:** `revalidatePath` is server-only. It must be in a `'use server'` file.
**How to avoid:** Keep all `revalidatePath` calls inside `src/lib/actions/*.ts` files marked `'use server'`.
**Warning signs:** Runtime error "revalidatePath is not a function" or "Cannot import server-only module."
### Pitfall 5: Drizzle pgEnum Must Be Exported and Used in Migration
**What goes wrong:** `pgEnum` values in schema don't appear in the migration SQL, causing runtime Drizzle errors when inserting.
**Why it happens:** If the enum is defined but not exported or referenced by a table column, `drizzle-kit generate` may skip it.
**How to avoid:** Export the enum, reference it in the table column, then run `npm run db:generate && npm run db:migrate`.
**Warning signs:** `invalid input value for enum` PostgreSQL error when seeding.
### Pitfall 6: `useActionState` Imported from `react` Not `react-dom`
**What goes wrong:** `useFormState` / wrong import path causes runtime error.
**Why it happens:** This was a known project pitfall in Phase 2 — `useActionState` is from `'react'` in React 19, not `'react-dom'`.
**How to avoid:** `import { useActionState } from 'react'` — the project already has this pattern in the contact form.
**Warning signs:** "useActionState is not a function" or "useFormState is not exported."
### Pitfall 7: Brand Colors in Tailwind v4 — Use CSS Variables
**What goes wrong:** One-off hex values like `bg-[#1B2B4B]` get missed by Tailwind v4 JIT and don't appear in output.
**Why it happens:** Phase 2 pitfall documented in STATE.md: "Brand colors applied via inline style props — Tailwind JIT may miss one-off hex values."
**How to avoid:** Use CSS variable references: `bg-[var(--navy)]`, `text-[var(--gold)]`. The variables are defined in `globals.css`.
**Warning signs:** Background color doesn't appear in production build; works in dev but not build.
---
## Code Examples
Verified patterns from official sources:
### Drizzle Insert with onConflictDoNothing (seed pattern)
```typescript
// Source: scripts/seed.ts (existing project pattern)
await db.insert(clients).values([
{ name: "Sarah Johnson", email: "sarah.j@example.com" },
{ name: "Mike Torres", email: "m.torres@example.com" },
]).onConflictDoNothing();
```
### Drizzle Delete with WHERE
```typescript
// Source: drizzle-orm (installed in project, verified in node_modules)
import { eq } from "drizzle-orm";
await db.delete(clients).where(eq(clients.id, id));
```
### revalidatePath After Mutation
```typescript
// Source: /teressa-copeland-homes/node_modules/next/dist/docs/01-app/03-api-reference/04-functions/revalidatePath.md
import { revalidatePath } from "next/cache";
revalidatePath("/portal/clients");
revalidatePath(`/portal/clients/${id}`);
```
### Filter by Status (URL search params for filter state)
```typescript
// src/app/portal/(protected)/dashboard/page.tsx
export default async function DashboardPage({
searchParams,
}: {
searchParams: Promise<{ status?: string; sort?: string }>;
}) {
const { status, sort } = await searchParams;
// filter docs in query based on status param
}
```
---
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Sync `params` access | `params` is a Promise → must await | Next.js 15+ | All dynamic pages MUST be async and await params |
| `useFormState` from `react-dom` | `useActionState` from `react` | React 19 | Import from `react`, not `react-dom` |
| `tailwind.config.js` with content paths | `@import "tailwindcss"` in CSS, no config file | Tailwind v4 | No tailwind.config.ts needed; CSS variables via `@theme inline` |
| Server Actions imported from page files | Dedicated `actions/` files with `'use server'` at top | Next.js 15+ convention | Cleaner separation; action files can be shared across multiple pages |
**Deprecated/outdated:**
- `useFormState` (react-dom): Removed in React 19 — project already using `useActionState` from `react`
- Synchronous `params` access: Works in dev (compat shim) but deprecated and will fail at build
- `tailwind.config.js` content scanning: Replaced by `@import "tailwindcss"` in Tailwind v4 — already done in this project
---
## Routing Architecture Notes
### Current state (Phase 1/2)
```
/agent/login — login page
/agent/(protected)/ — route group, layout.tsx with basic auth check + logout
/agent/(protected)/dashboard — stub "coming in Phase 3" page
middleware.ts matcher: ["/agent/:path*"]
auth.config.ts: redirect logged-in users on login page → /agent/dashboard
```
### Required changes for Phase 3
1. **middleware.ts**: Change matcher to `["/agent/:path*", "/portal/:path*"]`
2. **auth.config.ts authorized callback**: Add `nextUrl.pathname.startsWith("/portal")` to protected routes check; change post-login redirect from `/agent/dashboard` to `/portal/dashboard`
3. **`/agent/(protected)/dashboard/page.tsx`**: Replace with redirect to `/portal/dashboard` (or delete and let the middleware redirect handle it)
4. **New `src/app/portal/(protected)/layout.tsx`**: Full portal nav layout
This migration is clean — no destructive changes to auth. Login page stays at `/agent/login`.
---
## Open Questions
1. **Should `/agent/(protected)/` be removed or left as redirect shell?**
- What we know: The existing `layout.tsx` and `dashboard/page.tsx` under `/agent/(protected)/` are Phase 1 stubs meant to be replaced here
- What's unclear: Whether any external link or test points to `/agent/dashboard`
- Recommendation: Replace `/agent/(protected)/dashboard/page.tsx` with `redirect("/portal/dashboard")` and update `auth.config.ts` post-login redirect. Keep `/agent/(protected)/layout.tsx` as a minimal pass-through or delete it — the portal layout replaces it.
2. **Dashboard filter state: URL params vs. client state**
- What we know: CONTEXT.md specifies filter dropdown and sort controls on dashboard
- What's unclear: Whether filter should survive page refresh (URL params) or be ephemeral (client state)
- Recommendation: URL search params (`?status=Signed`) — enables bookmarking and is the Next.js idiomatic approach for filter state in server components. The `searchParams` prop on the dashboard page handles this without any client JS.
3. **`lastActivityDate` on client cards**
- What we know: CONTEXT.md says client cards show "last activity date"
- What's unclear: Whether this is `clients.updatedAt` or the `MAX(documents.sentAt)` for that client
- Recommendation: Use `MAX(documents.sentAt)` via a Drizzle subquery, falling back to `clients.createdAt` if no documents. This is more meaningful than update timestamp.
---
## Sources
### Primary (HIGH confidence)
- `/teressa-copeland-homes/node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/page.md` — params as Promise, page conventions
- `/teressa-copeland-homes/node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/layout.md` — layout conventions, params
- `/teressa-copeland-homes/node_modules/next/dist/docs/01-app/02-guides/forms.md` — Server Actions, useActionState, form validation with Zod
- `/teressa-copeland-homes/node_modules/next/dist/docs/01-app/03-api-reference/04-functions/revalidatePath.md` — cache invalidation after mutations
- `/teressa-copeland-homes/node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/intercepting-routes.md` — modal routing options (considered, not used)
- `/teressa-copeland-homes/src/lib/db/schema.ts` — existing schema pattern
- `/teressa-copeland-homes/src/lib/auth.config.ts` — existing middleware/auth config
- `/teressa-copeland-homes/middleware.ts` — existing matcher config
- `/teressa-copeland-homes/src/app/globals.css` — brand CSS variables
- `/teressa-copeland-homes/package.json` — exact installed versions
### Secondary (MEDIUM confidence)
- drizzle-orm installed in `node_modules/drizzle-orm` — pgEnum, relations, query patterns verified by inspecting existing schema.ts usage and drizzle docs folder (not exhaustively read, but schema pattern consistent with existing code)
### Tertiary (LOW confidence)
- None
---
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH — all packages already in project, verified in package.json
- Architecture: HIGH — routing docs read directly from installed Next.js 16 dist; existing project structure inspected
- Pitfalls: HIGH — derived from official docs + STATE.md accumulated decisions from Phases 12
- Schema patterns: HIGH — matches existing schema.ts conventions exactly
**Research date:** 2026-03-19
**Valid until:** 2026-04-19 (next-auth beta and Next.js 16 move fast; recheck if upgrading either)