docs(03-agent-portal-shell): create phase 3 plan — 4 plans in 4 waves

This commit is contained in:
Chandler Copeland
2026-03-19 16:13:10 -06:00
parent d4fd2bcdc8
commit 3cda82df51
5 changed files with 1091 additions and 3 deletions

View File

@@ -0,0 +1,199 @@
---
phase: 03-agent-portal-shell
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- teressa-copeland-homes/src/lib/db/schema.ts
- teressa-copeland-homes/middleware.ts
- teressa-copeland-homes/src/lib/auth.config.ts
- teressa-copeland-homes/src/app/agent/(protected)/dashboard/page.tsx
- teressa-copeland-homes/drizzle/0001_clients_documents.sql
autonomous: true
requirements: [CLIENT-01, CLIENT-02, CLIENT-03, DASH-01, DASH-02]
must_haves:
truths:
- "Running `npm run db:generate && npm run db:migrate` produces a migration that creates the `clients` and `documents` tables with no errors"
- "Visiting /portal/dashboard while unauthenticated redirects to /agent/login — not a 404 or blank page"
- "After login, agent is redirected to /portal/dashboard (not /agent/dashboard)"
- "The document_status PostgreSQL enum exists with values Draft, Sent, Viewed, Signed"
artifacts:
- path: "teressa-copeland-homes/src/lib/db/schema.ts"
provides: "clients and documents table definitions + documentStatusEnum"
contains: "pgTable(\"clients\""
- path: "teressa-copeland-homes/middleware.ts"
provides: "Route protection for /portal/:path*"
contains: "/portal/:path*"
- path: "teressa-copeland-homes/src/lib/auth.config.ts"
provides: "Post-login redirect to /portal/dashboard"
contains: "/portal/dashboard"
key_links:
- from: "middleware.ts matcher"
to: "/portal/:path* routes"
via: "matcher array"
pattern: "portal.*path"
- from: "auth.config.ts authorized callback"
to: "session check for /portal routes"
via: "nextUrl.pathname.startsWith('/portal')"
pattern: "startsWith.*portal"
---
<objective>
Add the `clients` and `documents` stub tables to the Drizzle schema, generate and run the migration, and update the middleware + auth config so all `/portal/` routes are protected and post-login redirect lands on `/portal/dashboard`.
Purpose: Every subsequent Phase 3 plan depends on these two tables and on the `/portal` route prefix being protected. This plan lays the data and routing foundations.
Output: Working Drizzle migration for clients + documents tables; middleware protecting `/portal/*`; post-login redirect updated.
</objective>
<execution_context>
@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md
@/Users/ccopeland/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/03-agent-portal-shell/03-CONTEXT.md
@.planning/phases/03-agent-portal-shell/03-RESEARCH.md
<interfaces>
<!-- Existing schema pattern (from src/lib/db/schema.ts) -->
<!-- Executor: match this exact style for new tables -->
Existing users table (for reference pattern):
```typescript
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
// users table already exists — do NOT redefine it
// ADD below the existing users table:
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(),
});
```
Existing middleware.ts pattern (matcher array to extend):
```typescript
export const config = {
matcher: ["/agent/:path*"], // ADD "/portal/:path*" here
};
```
Existing auth.config.ts authorized callback (redirect to update):
```typescript
// Change: redirect(new URL("/agent/dashboard", nextUrl))
// To: redirect(new URL("/portal/dashboard", nextUrl))
// Also add: nextUrl.pathname.startsWith("/portal") to protected routes check
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Extend Drizzle schema with clients and documents tables</name>
<files>teressa-copeland-homes/src/lib/db/schema.ts</files>
<action>
Read the current schema.ts first. Then add, below the existing users table definition:
1. Import `pgEnum` from `drizzle-orm/pg-core` (add to existing import).
2. Export `documentStatusEnum` using `pgEnum("document_status", ["Draft", "Sent", "Viewed", "Signed"])`.
3. Export `clients` table: id (text PK with crypto.randomUUID), name (text not null), email (text not null), createdAt (timestamp defaultNow not null), updatedAt (timestamp defaultNow not null).
4. Export `documents` table (stub — no PDF content yet): id (text PK with crypto.randomUUID), name (text not null), clientId (text not null, FK to clients.id with onDelete: "cascade"), status (documentStatusEnum column default "Draft" not null), sentAt (timestamp nullable), createdAt (timestamp defaultNow not null).
CRITICAL: Export the enum BEFORE the documents table (it is referenced by the documents table column). Export both tables so they are importable by other modules.
Then generate and run the migration:
```
cd teressa-copeland-homes && npm run db:generate && npm run db:migrate
```
The migration SQL file will be created automatically in the `drizzle/` directory. Do NOT hand-write SQL.
PITFALL: pgEnum must be exported AND referenced by a table column or drizzle-kit generate may omit it. Confirm the generated SQL contains `CREATE TYPE document_status AS ENUM`.
</action>
<verify>
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run db:generate && npm run db:migrate 2>&1 | tail -20</automated>
</verify>
<done>Migration runs without error. `clients` and `documents` tables exist in the database. `document_status` enum exists in PostgreSQL. schema.ts exports `clients`, `documents`, and `documentStatusEnum`.</done>
</task>
<task type="auto">
<name>Task 2: Update middleware and auth config to protect /portal routes</name>
<files>
teressa-copeland-homes/middleware.ts
teressa-copeland-homes/src/lib/auth.config.ts
teressa-copeland-homes/src/app/agent/(protected)/dashboard/page.tsx
</files>
<action>
**middleware.ts**: Read the file. Change the matcher to include both `/agent/:path*` and `/portal/:path*`:
```typescript
export const config = {
matcher: ["/agent/:path*", "/portal/:path*"],
};
```
**auth.config.ts**: Read the file. Make two changes:
1. In the `authorized` callback, extend the protected route check to also cover `/portal` routes:
```typescript
// Add alongside the existing /agent check:
if (nextUrl.pathname.startsWith("/portal")) {
if (!isLoggedIn) return Response.redirect(new URL("/agent/login", nextUrl));
}
```
2. Change the post-login redirect (where a logged-in user visiting the login page is sent) from `/agent/dashboard` to `/portal/dashboard`:
```typescript
// Change: new URL("/agent/dashboard", nextUrl)
// To: new URL("/portal/dashboard", nextUrl)
```
PITFALL (from RESEARCH.md): The `authorized` callback in Auth.js v5 beta may handle the redirect differently than a plain middleware — read the existing callback logic carefully and mirror the existing `/agent` protection pattern rather than rewriting it.
**src/app/agent/(protected)/dashboard/page.tsx**: Replace the dashboard stub with a redirect to `/portal/dashboard`. Import `redirect` from `next/navigation` and call `redirect("/portal/dashboard")` as the only content. This ensures any old link to `/agent/dashboard` silently forwards to the new location.
</action>
<verify>
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1</automated>
</verify>
<done>TypeScript compiles with zero errors. middleware.ts matcher includes `/portal/:path*`. auth.config.ts post-login redirect is `/portal/dashboard`. /agent/dashboard/page.tsx redirects to /portal/dashboard.</done>
</task>
</tasks>
<verification>
1. `npm run db:generate && npm run db:migrate` completes without error
2. `npx tsc --noEmit` produces zero TypeScript errors
3. schema.ts contains exports for `clients`, `documents`, and `documentStatusEnum`
4. middleware.ts matcher array contains `"/portal/:path*"`
5. auth.config.ts contains `startsWith("/portal")` in protected route check
6. auth.config.ts post-login redirect is `/portal/dashboard`
</verification>
<success_criteria>
- Drizzle migration creates `clients` and `documents` tables and `document_status` enum in PostgreSQL
- All `/portal/` routes require authentication (middleware protects them)
- After login, agent is redirected to `/portal/dashboard`
- TypeScript compiles cleanly
</success_criteria>
<output>
After completion, create `.planning/phases/03-agent-portal-shell/03-01-SUMMARY.md`
</output>