docs(01-foundation): create phase 1 plan

This commit is contained in:
Chandler Copeland
2026-03-19 13:24:04 -06:00
parent e1c6e3cce7
commit 41703bf74b
4 changed files with 1009 additions and 3 deletions

View File

@@ -32,9 +32,12 @@ Decimal phases appear between their surrounding integers in numeric order.
3. Visiting any `/agent/*` route while unauthenticated redirects to the login page (not a 404 or blank screen)
4. Agent can log out from the portal and is returned to the login page
5. Database schema is deployed to Neon, Vercel Blob bucket is created, and the project is deployed to Vercel with environment variables wired
**Plans**: TBD
**Plans**: 3 plans
Plans: none yet
Plans:
- [ ] 01-01-PLAN.md — Next.js scaffold, Drizzle schema, Auth.js v5 config, route-protection middleware, seed script
- [ ] 01-02-PLAN.md — Branded login page, agent portal layout, dashboard stub, logout mechanism
- [ ] 01-03-PLAN.md — Git push, Vercel deployment, production auth flow verification (checkpoint)
### Phase 2: Marketing Site
**Goal**: Visitors can find Teressa online, see her brand, view the listings placeholder, and submit a contact inquiry
@@ -124,7 +127,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7
| Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------|
| 1. Foundation | 0/? | Not started | - |
| 1. Foundation | 0/3 | Planned | - |
| 2. Marketing Site | 0/? | Not started | - |
| 3. Agent Portal Shell | 0/? | Not started | - |
| 4. PDF Ingest | 0/? | Not started | - |

View File

@@ -0,0 +1,440 @@
---
phase: 01-foundation
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- package.json
- tsconfig.json
- next.config.ts
- tailwind.config.ts
- drizzle.config.ts
- src/lib/db/index.ts
- src/lib/db/schema.ts
- src/lib/auth.ts
- middleware.ts
- scripts/seed.ts
- drizzle/
autonomous: true
requirements:
- AUTH-01
- AUTH-02
- AUTH-03
user_setup:
- service: neon
why: "PostgreSQL database for Drizzle ORM"
env_vars:
- name: DATABASE_URL
source: "Neon console → your project → Connection string → Pooled connection (use the ?sslmode=require URL)"
dashboard_config:
- task: "Create a new Neon project named 'teressa-copeland-homes' in the us-east-1 region"
location: "https://console.neon.tech/app/new"
- service: vercel-blob
why: "File/blob storage for signed PDFs and templates (provisioned now, used in Phase 5+)"
env_vars:
- name: BLOB_READ_WRITE_TOKEN
source: "Vercel Dashboard → your project → Storage → Blob → Create Store → token appears after creation"
dashboard_config:
- task: "Create a Blob store named 'teressa-copeland-homes-blob' linked to the Vercel project"
location: "Vercel Dashboard → Storage tab"
- service: vercel
why: "Deployment host and environment variable manager"
env_vars:
- name: AUTH_SECRET
source: "Generate with: npx auth secret — copies to clipboard; paste into Vercel dashboard"
- name: AGENT_EMAIL
source: "Teressa's login email address (e.g. teressa@teressacopelandhomes.com)"
- name: AGENT_PASSWORD
source: "A strong password for Teressa's portal login"
dashboard_config:
- task: "Create a new Vercel project linked to the Git repo; set all 5 env vars (DATABASE_URL, BLOB_READ_WRITE_TOKEN, AUTH_SECRET, AGENT_EMAIL, AGENT_PASSWORD) in the project's Environment Variables settings"
location: "https://vercel.com/new"
must_haves:
truths:
- "Next.js project builds without TypeScript errors (npm run build succeeds)"
- "Drizzle schema generates a valid SQL migration file for the users table"
- "Auth.js configuration exports handlers, auth, signIn, signOut without import errors"
- "Middleware.ts exists at project root (not inside src/) and imports from @/lib/auth"
- "Seed script creates Teressa's hashed-password account in the database when run"
- "Vercel Blob store is provisioned and BLOB_READ_WRITE_TOKEN is available"
artifacts:
- path: "src/lib/db/schema.ts"
provides: "users table definition (id, email, password_hash, created_at)"
exports: ["users"]
- path: "src/lib/db/index.ts"
provides: "Drizzle client singleton using Neon HTTP driver"
exports: ["db"]
- path: "src/lib/auth.ts"
provides: "Auth.js v5 configuration — JWT strategy, Credentials provider, 7-day rolling session"
exports: ["handlers", "auth", "signIn", "signOut"]
- path: "middleware.ts"
provides: "Edge middleware that redirects unauthenticated /agent/* requests to /agent/login"
- path: "scripts/seed.ts"
provides: "One-time seed script that creates Teressa's account from AGENT_EMAIL + AGENT_PASSWORD env vars"
- path: "drizzle.config.ts"
provides: "Drizzle Kit configuration pointing to schema.ts and /drizzle output directory"
- path: "drizzle/"
provides: "Generated SQL migration files committed to repo"
key_links:
- from: "src/lib/auth.ts"
to: "src/lib/db"
via: "Credentials authorize() callback calls db.select(users)"
pattern: "db\\.select.*from.*users"
- from: "middleware.ts"
to: "src/lib/auth.ts"
via: "import { auth } from @/lib/auth"
pattern: "import.*auth.*from.*@/lib/auth"
- from: "scripts/seed.ts"
to: "src/lib/db/schema.ts"
via: "import { users } from ../src/lib/db/schema"
pattern: "import.*users.*from"
---
<objective>
Scaffold the Next.js 15 project, install all Phase 1 dependencies, define the database schema, configure Drizzle ORM, write the Auth.js v5 configuration with JWT strategy + Credentials provider, create the route-protection middleware, and write the database seed script. This plan produces all the non-UI foundation contracts that Plans 02 and 03 build on top of.
Purpose: Every subsequent plan in this phase and every future phase depends on these files. Getting the contracts right here prevents cascading rework.
Output: Scaffolded project with auth.ts, schema.ts, db/index.ts, middleware.ts, seed.ts, and drizzle migration committed.
</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/01-foundation/01-CONTEXT.md
@.planning/phases/01-foundation/01-RESEARCH.md
<interfaces>
<!-- Contracts this plan creates — downstream plans depend on these exports. -->
<!-- Do not deviate from these shapes without updating Plans 02 and 03. -->
src/lib/db/schema.ts will export:
```typescript
export const users = pgTable("users", {
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
email: text("email").notNull().unique(),
passwordHash: text("password_hash").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
```
src/lib/auth.ts will export:
```typescript
export const { handlers, auth, signIn, signOut } = NextAuth({ ... });
```
src/lib/db/index.ts will export:
```typescript
export const db = drizzle({ client: sql, schema });
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Scaffold Next.js 15 project and install all dependencies</name>
<files>package.json, tsconfig.json, next.config.ts, tailwind.config.ts, .gitignore</files>
<action>
Run the following scaffold command from the parent directory of where the project should live. If the directory `/Users/ccopeland/temp/red/teressa-copeland-homes` already exists, skip scaffolding and install missing deps into the existing project.
```bash
npx create-next-app@latest teressa-copeland-homes \
--typescript \
--tailwind \
--eslint \
--app \
--src-dir \
--import-alias "@/*" \
--no-git
```
After scaffolding (cd into the project directory), install Phase 1 dependencies:
```bash
# Auth + DB + Blob
npm install next-auth@beta
npm install drizzle-orm @neondatabase/serverless
npm install bcryptjs @vercel/blob zod
npm install -D drizzle-kit tsx @types/bcryptjs dotenv
```
After install, note the exact installed `next-auth` version with `npm ls next-auth` and pin it in package.json (replace `"next-auth": "beta"` with the exact version string like `"next-auth": "5.0.0-beta.25"`).
Add these npm scripts to package.json:
```json
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:seed": "tsx scripts/seed.ts",
"db:studio": "drizzle-kit studio"
```
Ensure `.gitignore` includes:
```
.env
.env.local
.env*.local
```
Do NOT create a `.env` file or `.env.local` file — all secrets go through Vercel dashboard per user decision.
Confirm `npm run build` succeeds (will show only placeholder page errors, not import errors).
</action>
<verify>npm run build passes with no TypeScript compilation errors; npm ls next-auth shows a pinned specific version (not "beta")</verify>
<done>Project directory exists, all dependencies installed, exact next-auth version pinned in package.json, npm run build succeeds</done>
</task>
<task type="auto">
<name>Task 2: Define database schema, configure Drizzle Kit, and write seed script</name>
<files>src/lib/db/schema.ts, src/lib/db/index.ts, drizzle.config.ts, scripts/seed.ts, drizzle/ (generated)</files>
<action>
Create `drizzle.config.ts` at the project root:
```typescript
import "dotenv/config";
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/lib/db/schema.ts",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
```
Create `src/lib/db/schema.ts`:
```typescript
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
email: text("email").notNull().unique(),
passwordHash: text("password_hash").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
```
Create `src/lib/db/index.ts`:
```typescript
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";
import * as schema from "./schema";
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle({ client: sql, schema });
```
Create `scripts/seed.ts`:
```typescript
import "dotenv/config";
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";
import { users } from "../src/lib/db/schema";
import bcrypt from "bcryptjs";
const sql = neon(process.env.DATABASE_URL!);
const db = drizzle({ client: sql });
async function seed() {
const email = process.env.AGENT_EMAIL;
const password = process.env.AGENT_PASSWORD;
if (!email || !password) {
throw new Error("AGENT_EMAIL and AGENT_PASSWORD env vars are required");
}
const passwordHash = await bcrypt.hash(password, 12);
await db.insert(users).values({ email, passwordHash }).onConflictDoNothing();
console.log(`Seeded agent account: ${email}`);
process.exit(0);
}
seed().catch((err) => {
console.error(err);
process.exit(1);
});
```
Generate migration files (requires DATABASE_URL to be set — if not yet available, generate schema only and note that `db:migrate` and `db:seed` run after user sets up Neon):
```bash
# If DATABASE_URL is available locally (via `vercel env pull` after Vercel project creation):
npm run db:generate # Creates drizzle/0000_*.sql migration file
npm run db:migrate # Applies migration to Neon database
npm run db:seed # Creates Teressa's account
# If DATABASE_URL is NOT yet available:
# Generate migration file only (does not need DB connection):
npm run db:generate
# Note in SUMMARY.md that db:migrate and db:seed must run after user sets up Neon
```
The generated `/drizzle` directory with SQL migration files MUST be committed to the repo (per user decision — version-controlled, auditable migrations).
</action>
<verify>drizzle/ directory contains at least one .sql migration file; src/lib/db/schema.ts and index.ts export users and db without TypeScript errors (npx tsc --noEmit)</verify>
<done>Schema file exports users table, db/index.ts exports db singleton, drizzle.config.ts present, seed script present, migration SQL file exists in /drizzle and committed to git</done>
</task>
<task type="auto">
<name>Task 3: Configure Auth.js v5 with JWT strategy and create route-protection middleware</name>
<files>src/lib/auth.ts, src/app/api/auth/[...nextauth]/route.ts, middleware.ts</files>
<action>
Create `src/lib/auth.ts` — the single source of truth for authentication configuration:
```typescript
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { db } from "@/lib/db";
import { users } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
import bcrypt from "bcryptjs";
import { z } from "zod";
const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(1),
});
export const { handlers, auth, signIn, signOut } = NextAuth({
session: {
strategy: "jwt",
maxAge: 7 * 24 * 60 * 60, // 7 days — rolling session per user decision
updateAge: 24 * 60 * 60, // Refresh cookie once per day (not every request)
},
pages: {
signIn: "/agent/login", // Custom login page — per user decision
},
providers: [
Credentials({
async authorize(credentials) {
const parsed = loginSchema.safeParse(credentials);
if (!parsed.success) return null;
const [user] = await db
.select()
.from(users)
.where(eq(users.email, parsed.data.email))
.limit(1);
if (!user) return null;
const passwordMatch = await bcrypt.compare(
parsed.data.password,
user.passwordHash
);
if (!passwordMatch) return null;
return { id: user.id, email: user.email };
},
}),
],
callbacks: {
jwt({ token, user }) {
if (user) token.id = user.id;
return token;
},
session({ session, token }) {
session.user.id = token.id as string;
return session;
},
},
});
```
Create `src/app/api/auth/[...nextauth]/route.ts`:
```typescript
import { handlers } from "@/lib/auth";
export const { GET, POST } = handlers;
```
Create `middleware.ts` at the PROJECT ROOT (not inside src/ — critical, middleware placed in src/ is silently ignored by Next.js):
```typescript
import { auth } from "@/lib/auth";
import { NextResponse } from "next/server";
export default auth((req) => {
const isLoggedIn = !!req.auth;
const isAgentRoute = req.nextUrl.pathname.startsWith("/agent");
const isLoginPage = req.nextUrl.pathname === "/agent/login";
// Protect all /agent/* routes except the login page itself
if (isAgentRoute && !isLoginPage && !isLoggedIn) {
return NextResponse.redirect(new URL("/agent/login", req.nextUrl.origin));
}
// Redirect already-authenticated users away from the login page
if (isLoginPage && isLoggedIn) {
return NextResponse.redirect(new URL("/agent/dashboard", req.nextUrl.origin));
}
});
export const config = {
matcher: [
// Run on /agent/* routes, excluding Next.js internals and static files
"/((?!_next/static|_next/image|favicon.ico).*)",
],
};
```
Verify `middleware.ts` is at the same level as `package.json` — NOT inside `src/`. This is the single most common pitfall: middleware inside src/ is silently ignored and routes are unprotected.
AUTH_SECRET env var must be set for Auth.js to function. The user will add this to Vercel dashboard. For local development, note this requirement in SUMMARY.md — `vercel env pull` will populate `.env.local` once the Vercel project is created.
</action>
<verify>npx tsc --noEmit passes with no errors in auth.ts, middleware.ts, and route.ts; middleware.ts file exists at project root (same level as package.json), NOT inside src/</verify>
<done>src/lib/auth.ts exports handlers, auth, signIn, signOut; API route handler exists at src/app/api/auth/[...nextauth]/route.ts; middleware.ts exists at project root with /agent/* protection logic</done>
</task>
</tasks>
<verification>
Run these commands from the project root directory after completing all tasks:
```bash
# TypeScript: zero errors across all created files
npx tsc --noEmit
# Build: confirms Next.js can compile the project
npm run build
# Migration file exists (committed to git)
ls drizzle/*.sql
# Middleware location (MUST be at root, not src/)
ls middleware.ts && echo "OK" || echo "MISSING — check location"
ls src/middleware.ts 2>/dev/null && echo "WRONG LOCATION" || echo "Correctly absent from src/"
# Auth exports are importable (no circular deps or missing modules)
node -e "import('./src/lib/auth.ts').then(() => console.log('auth.ts OK')).catch(e => console.error(e.message))"
```
</verification>
<success_criteria>
- npm run build succeeds with no TypeScript errors
- drizzle/ directory contains at least one committed SQL migration file
- middleware.ts exists at project root, NOT inside src/
- src/lib/auth.ts exports handlers, auth, signIn, signOut (JWT strategy, 7-day maxAge, Credentials provider)
- src/lib/db/schema.ts exports users table with id, email, passwordHash, createdAt columns
- src/lib/db/index.ts exports db (Drizzle + Neon HTTP driver singleton)
- src/app/api/auth/[...nextauth]/route.ts exports GET and POST
- scripts/seed.ts reads AGENT_EMAIL and AGENT_PASSWORD from env and creates hashed user row
- next-auth version is pinned to a specific beta version (not "beta" or "^5...")
</success_criteria>
<output>
After completion, create `.planning/phases/01-foundation/01-01-SUMMARY.md` using the summary template.
Include in the summary:
- Exact next-auth beta version pinned
- Whether db:migrate and db:seed were run successfully (or if they are pending user Neon setup)
- Location of middleware.ts confirmed (root vs src/)
- Any deviations from the planned patterns and why
</output>

View File

@@ -0,0 +1,348 @@
---
phase: 01-foundation
plan: 02
type: execute
wave: 2
depends_on:
- "01-01"
files_modified:
- src/app/agent/login/page.tsx
- src/app/agent/dashboard/page.tsx
- src/app/agent/layout.tsx
- src/components/ui/LogoutButton.tsx
- src/app/layout.tsx
- src/app/page.tsx
- public/red.jpg
autonomous: true
requirements:
- AUTH-01
- AUTH-02
- AUTH-03
- AUTH-04
must_haves:
truths:
- "Visiting /agent/login displays a branded form with email, password (with show/hide toggle), and a submit button"
- "Submitting invalid credentials shows 'Invalid email or password' without hinting which field is wrong"
- "Submitting valid credentials redirects to /agent/dashboard"
- "After login, the agent's email is visible somewhere on the dashboard"
- "The dashboard has a logout button that, when clicked, redirects to /agent/login"
- "Visiting /agent/login after logout shows 'You've been signed out' confirmation message"
- "Visiting /agent/dashboard without a session redirects to /agent/login (enforced by middleware from Plan 01)"
artifacts:
- path: "src/app/agent/login/page.tsx"
provides: "Branded login page with email/password form, password toggle, error display, signed-out confirmation"
exports: ["default (LoginPage)"]
- path: "src/app/agent/dashboard/page.tsx"
provides: "Protected blank dashboard — server component that calls auth() and shows agent email + logout button"
exports: ["default (DashboardPage)"]
- path: "src/app/agent/layout.tsx"
provides: "Agent portal layout wrapper — defense-in-depth session check, shared nav shell"
exports: ["default (AgentLayout)"]
- path: "src/components/ui/LogoutButton.tsx"
provides: "Client component with logout form that calls signOut server action"
exports: ["LogoutButton"]
key_links:
- from: "src/app/agent/login/page.tsx"
to: "src/lib/auth.ts"
via: "loginAction() calls signIn('credentials', ...) from @/lib/auth"
pattern: "signIn.*credentials"
- from: "src/components/ui/LogoutButton.tsx"
to: "src/lib/auth.ts"
via: "logoutAction() calls signOut({ redirectTo: '/agent/login?signed_out=1' })"
pattern: "signOut.*signed_out"
- from: "src/app/agent/dashboard/page.tsx"
to: "src/lib/auth.ts"
via: "const session = await auth() — defense-in-depth beyond middleware"
pattern: "await auth\\(\\)"
---
<objective>
Build the two agent-facing pages (login and dashboard) and the logout mechanism. The login page is fully branded with Teressa's photo and implements all user-specified behaviors. The dashboard is a blank stub that confirms the session is valid and provides a logout button. Together these pages satisfy all four AUTH requirements.
Purpose: These pages are the visible proof that Phase 1 works. The middleware from Plan 01 protects routes — these pages are what the agent actually sees.
Output: /agent/login (branded form) and /agent/dashboard (blank, protected, with logout).
</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/phases/01-foundation/01-CONTEXT.md
@.planning/phases/01-foundation/01-RESEARCH.md
@.planning/phases/01-foundation/01-01-SUMMARY.md
<interfaces>
<!-- Contracts from Plan 01 that this plan consumes. -->
<!-- Do not import from other paths — these are the established contracts. -->
From src/lib/auth.ts (created in Plan 01):
```typescript
export const { handlers, auth, signIn, signOut } = NextAuth({ ... });
// auth() — call in server components to get session
// signIn("credentials", { email, password, redirectTo }) — call in server actions
// signOut({ redirectTo }) — call in server actions
```
From src/lib/db/schema.ts (created in Plan 01):
```typescript
export const users = pgTable("users", { ... });
// Not needed directly in UI — session comes from auth()
```
Key behaviors locked by user decisions (CONTEXT.md):
- Login route: /agent/login
- Post-login redirect: /agent/dashboard
- Error message: "Invalid email or password" (generic — do not vary by field)
- Password visibility toggle: included
- Session behavior: 7-day rolling (handled by auth.ts — no code needed here)
- Logout: signOut({ redirectTo: "/agent/login?signed_out=1" })
- Logout confirmation: "You've been signed out" (shown when ?signed_out=1 is in URL)
- No forgot-password link (per deferred decisions — do NOT add it)
Brand assets:
- Agent photo: /Users/ccopeland/Downloads/red.jpg — copy to public/red.jpg
- Brand colors/logo: Use discretion — create CSS custom properties for easy swap later
- Suggested color palette: warm gold (#C9A84C) for accent, deep navy (#1B2B4B) for primary, off-white (#FAF9F7) for background
- Login page should feel like a premium real estate brand, not a generic SaaS admin
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Build the branded login page with password toggle and error handling</name>
<files>src/app/agent/login/page.tsx, public/red.jpg</files>
<action>
Copy the agent photo from its source path to the public directory:
```bash
cp /Users/ccopeland/Downloads/red.jpg /path/to/project/public/red.jpg
```
If the file does not exist at that path, use a placeholder image (create a simple Next.js Image with a colored background placeholder) and note it in the SUMMARY.md.
Create `src/app/agent/login/page.tsx` as a **server component** (no "use client") that:
1. Reads `searchParams` to detect `?error=invalid` and `?signed_out=1`
2. Contains an inline `loginAction` server action that calls signIn
3. Contains an inline `SignedOutMessage` and `ErrorMessage` based on searchParams
4. Renders a full-page branded layout with a left/right split or centered card
The login page structure:
- Page fills the full viewport height (min-h-screen)
- Left side (hidden on mobile): Teressa's photo (`/red.jpg`) as a cover image, overlaid with a subtle dark gradient and brand name text
- Right side (or centered on mobile): Login form card
- "Agent Portal" heading (h1, per user decision)
- If `?signed_out=1`: green/teal banner — "You've been signed out."
- If `?error=invalid`: red banner — "Invalid email or password."
- Email input: type="email", name="email", required, label="Email address"
- Password input: type="password" by default, name="password", required, label="Password"
- Password toggle button (show/hide) — this is a client interaction, so wrap just the password field and toggle button in a small "use client" sub-component called `PasswordField`
- Submit button: "Sign in" text, full-width, shows loading state during submission
- Brand accent color on the submit button and focus rings
The `loginAction` server action (inside the page file):
```typescript
async function loginAction(formData: FormData) {
"use server";
try {
await signIn("credentials", {
email: formData.get("email") as string,
password: formData.get("password") as string,
redirectTo: "/agent/dashboard",
});
} catch (error) {
// CRITICAL: Only catch AuthError. Everything else (including NEXT_REDIRECT) must re-throw.
if (error instanceof AuthError) {
redirect("/agent/login?error=invalid");
}
throw error; // Re-throws NEXT_REDIRECT — allows the redirect to fire
}
}
```
The `PasswordField` sub-component (can live in same file or separate file, use client):
- Manages `showPassword` boolean state
- Renders `<input type={showPassword ? "text" : "password"} ... />`
- Renders a toggle button (eye/eye-off icon or text "Show"/"Hide")
- Use Tailwind for all styling — no additional UI libraries
Brand color guide (CSS custom properties or Tailwind classes):
- Primary background: #FAF9F7 (warm off-white)
- Primary text: #1B2B4B (deep navy)
- Accent (buttons, focus rings): #C9A84C (warm gold)
- Error background: #FEF2F2, error text: #991B1B
- Success background: #F0FDF4, success text: #166534
Do NOT add a "Forgot password?" link — this is explicitly deferred per user decisions.
</action>
<verify>
Navigate to /agent/login in browser (or run: curl -s http://localhost:3000/agent/login | grep -i "Agent Portal"). Verify:
1. Page renders without errors
2. Submitting wrong credentials reloads with "Invalid email or password" banner
3. Password toggle switches input type between password/text
4. npm run build passes with no TypeScript errors in this file
</verify>
<done>Login page renders at /agent/login with branded design, password toggle works, error displays on failed login, signed-out message displays when ?signed_out=1, no TypeScript errors</done>
</task>
<task type="auto">
<name>Task 2: Build the agent portal layout, dashboard stub, and logout mechanism</name>
<files>src/app/agent/layout.tsx, src/app/agent/dashboard/page.tsx, src/components/ui/LogoutButton.tsx, src/app/page.tsx</files>
<action>
Create `src/components/ui/LogoutButton.tsx` as a client component with a server action for logout:
```typescript
// src/components/ui/LogoutButton.tsx
import { signOut } from "@/lib/auth";
async function logoutAction() {
"use server";
await signOut({ redirectTo: "/agent/login?signed_out=1" });
}
export function LogoutButton() {
return (
<form action={logoutAction}>
<button
type="submit"
className="text-sm text-gray-600 hover:text-gray-900 underline"
>
Sign out
</button>
</form>
);
}
```
Note: signOut() server action must NOT be caught — it throws NEXT_REDIRECT which must bubble up. The form action pattern avoids the issue.
Create `src/app/agent/layout.tsx` — the shared agent portal layout:
```typescript
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
import { LogoutButton } from "@/components/ui/LogoutButton";
export default async function AgentLayout({
children,
}: {
children: React.ReactNode;
}) {
// Defense-in-depth: middleware handles most cases, this catches edge cases
const session = await auth();
if (!session) redirect("/agent/login");
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white border-b border-gray-200 px-6 py-4 flex items-center justify-between">
<span className="text-sm font-medium text-gray-700">Agent Portal</span>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-500">{session.user?.email}</span>
<LogoutButton />
</div>
</header>
<main className="p-6">{children}</main>
</div>
);
}
```
Create `src/app/agent/dashboard/page.tsx` — blank dashboard stub:
```typescript
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
// Defense-in-depth session check (layout also checks, this is belt-and-suspenders)
const session = await auth();
if (!session) redirect("/agent/login");
return (
<div>
<h1 className="text-2xl font-semibold text-gray-900">Dashboard</h1>
<p className="mt-2 text-gray-500">
Welcome back, {session.user?.email}. Portal content coming in Phase 3.
</p>
</div>
);
}
```
Update `src/app/page.tsx` (public homepage — placeholder for Phase 2):
```typescript
export default function HomePage() {
return (
<main className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold text-gray-900">Teressa Copeland Homes</h1>
<p className="mt-4 text-gray-600">Real estate expertise for Utah home buyers and sellers.</p>
<p className="mt-2 text-sm text-gray-400">Marketing site coming in Phase 2.</p>
</div>
</main>
);
}
```
AUTH-04 verification: The LogoutButton must be accessible from the dashboard (it is in the agent layout header). Clicking it calls logoutAction which calls signOut() with ?signed_out=1, which redirects to /agent/login, which reads the query param and shows "You've been signed out."
AUTH-02 verification: The 7-day rolling JWT session is handled entirely in auth.ts (Plan 01). No additional code needed here — the httpOnly encrypted cookie persists across browser restarts by default. Do not add any sessionStorage usage.
AUTH-03 verification: The middleware from Plan 01 handles unauthenticated redirects. The layout's auth() check is defense-in-depth — not the primary enforcement mechanism. Both layers must be in place.
</action>
<verify>
1. Run npm run build — no TypeScript errors
2. Verify agent layout exists: ls src/app/agent/layout.tsx
3. Verify dashboard exists: ls src/app/agent/dashboard/page.tsx
4. Verify LogoutButton exists: ls src/components/ui/LogoutButton.tsx
5. TypeScript check: npx tsc --noEmit
</verify>
<done>AgentLayout renders portal header with agent email and sign-out button; DashboardPage shows welcome message with agent email; LogoutButton calls signOut({ redirectTo: "/agent/login?signed_out=1" }); npm run build and npx tsc --noEmit pass with no errors</done>
</task>
</tasks>
<verification>
Full auth flow verification (requires running dev server):
```bash
npm run dev
```
Manual verification checklist:
1. Visit http://localhost:3000/agent/dashboard — should redirect to /agent/login (AUTH-03)
2. Submit wrong credentials on /agent/login — should show "Invalid email or password" (AUTH-01)
3. Submit correct credentials — should redirect to /agent/dashboard with agent email visible (AUTH-01)
4. Refresh /agent/dashboard — should stay logged in, not redirect (AUTH-02 partial — session persists across refresh)
5. Click "Sign out" — should redirect to /agent/login with "You've been signed out" message (AUTH-04)
6. Visit /agent/dashboard while logged out — should redirect to /agent/login (AUTH-03)
Build verification:
```bash
npm run build && echo "BUILD OK"
npx tsc --noEmit && echo "TYPECHECK OK"
```
</verification>
<success_criteria>
- /agent/login renders with branded design — photo, "Agent Portal" heading, email/password form
- Password show/hide toggle works
- Invalid login shows "Invalid email or password" (not a more specific error)
- Valid login redirects to /agent/dashboard
- /agent/dashboard shows agent email and Sign out button
- Signing out redirects to /agent/login with "You've been signed out" message
- /agent/dashboard (and all future /agent/* routes) redirect to /agent/login when unauthenticated
- npm run build passes with no errors
- No forgot-password link exists on any page (deferred per user decisions)
</success_criteria>
<output>
After completion, create `.planning/phases/01-foundation/01-02-SUMMARY.md` using the summary template.
Include in the summary:
- Confirmation that the agent photo was copied from /Users/ccopeland/Downloads/red.jpg (or note if placeholder used)
- Confirmation of the brand colors used (or CSS custom properties defined)
- Manual verification results for the full auth flow (items 1-6 above)
- Any deviations from planned patterns and why
</output>

View File

@@ -0,0 +1,215 @@
---
phase: 01-foundation
plan: 03
type: execute
wave: 3
depends_on:
- "01-01"
- "01-02"
files_modified: []
autonomous: false
requirements:
- AUTH-01
- AUTH-02
- AUTH-03
- AUTH-04
must_haves:
truths:
- "Project is deployed and live at a Vercel URL (teressacopelandhomes.com or a *.vercel.app preview URL)"
- "Agent can log in with email and password in the production deployment"
- "Agent session persists after browser tab close and reopen"
- "Visiting any /agent/* route while logged out redirects to /agent/login"
- "Agent can log out and sees 'You've been signed out'"
- "Database schema is live in Neon with Teressa's seeded account"
- "Vercel Blob store exists and BLOB_READ_WRITE_TOKEN is set in Vercel environment"
artifacts:
- path: "Vercel project"
provides: "Production deployment with all 5 env vars set"
- path: "Neon database"
provides: "users table with Teressa's seeded account"
- path: "Vercel Blob store"
provides: "Single blob store for future document storage (Phase 4+)"
key_links:
- from: "Vercel deployment"
to: "Neon database"
via: "DATABASE_URL env var in Vercel project settings"
pattern: "DATABASE_URL set in Vercel env"
- from: "Vercel deployment"
to: "Auth.js"
via: "AUTH_SECRET env var in Vercel project settings"
pattern: "AUTH_SECRET set in Vercel env"
---
<objective>
Deploy the project to Vercel, wire all environment variables, run the database migration and seed on the production Neon database, and verify the complete auth flow end-to-end in the live deployment. This plan is a blocking checkpoint — a human must confirm the production deployment works before Phase 1 is complete.
Purpose: Phase 1's success criterion explicitly requires the project to be deployed to Vercel with environment variables wired. This checkpoint ensures that is true before declaring Phase 1 done.
Output: Live production deployment of teressacopelandhomes.com (or *.vercel.app) with working auth.
</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/ROADMAP.md
@.planning/phases/01-foundation/01-CONTEXT.md
@.planning/phases/01-foundation/01-01-SUMMARY.md
@.planning/phases/01-foundation/01-02-SUMMARY.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Push to Git and verify Vercel auto-deploy triggers</name>
<files></files>
<action>
Initialize a git repo if not already done, then push to the remote that Vercel's Git integration watches:
```bash
# From the project root directory:
git init
git add .
git commit -m "feat(01-foundation): Next.js scaffold, auth, database schema, and login UI"
git branch -M main
git remote add origin <your-github-repo-url>
git push -u origin main
```
Per user decision: Vercel native Git integration is used (push to main → auto-deploy). GitHub Actions are NOT used.
After pushing:
1. Open the Vercel dashboard for the project
2. Watch the deployment in the "Deployments" tab — it should trigger automatically
3. If the deployment FAILS because of missing env vars (expected on first deploy), that is fine — the checkpoint task below addresses this
4. Note the Vercel project URL (either custom domain or *.vercel.app URL)
IMPORTANT: If the Vercel project was not yet created (user setup not yet done), the push will not trigger a deployment. Claude cannot create the Vercel project or set env vars — the user_setup block in Plan 01's frontmatter lists these steps. Only proceed here if the user confirms setup is complete.
If user setup IS complete and the deployment succeeds:
```bash
# Pull env vars for local use (creates .env.local — already in .gitignore)
vercel env pull
# Run migration against production Neon database
npm run db:migrate
# Seed Teressa's account
npm run db:seed
```
If db:migrate and db:seed were already run locally against Neon during Plan 01 (because DATABASE_URL was available early), skip running them again — the onConflictDoNothing in seed.ts makes re-runs safe but unnecessary.
</action>
<verify>Vercel dashboard shows deployment status; git push completes without error; vercel env pull creates a .env.local file with all 5 required env vars (DATABASE_URL, AUTH_SECRET, BLOB_READ_WRITE_TOKEN, AGENT_EMAIL, AGENT_PASSWORD)</verify>
<done>Code is pushed to main, Vercel deployment has triggered (or completed), .env.local populated via vercel env pull</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 2: Verify complete auth flow in production</name>
<files></files>
<action>Human verification of the complete Phase 1 auth flow in the live Vercel production deployment. See how-to-verify for the 7 test cases.</action>
<what-built>
Complete Phase 1 auth system:
- Branded login page at /agent/login with password toggle
- Protected agent dashboard at /agent/dashboard
- 7-day rolling JWT session (persistent across browser restarts)
- Middleware-enforced route protection for all /agent/* routes
- Logout with "You've been signed out" confirmation
- Neon database with users table and Teressa's seeded account
- Vercel Blob store provisioned
- Deployed to Vercel with all environment variables wired
</what-built>
<how-to-verify>
Open the production URL from the Vercel dashboard (either https://teressacopelandhomes.com or the *.vercel.app preview URL).
**Test 1 — Route protection (AUTH-03):**
- Navigate directly to: `[your-vercel-url]/agent/dashboard`
- Expected: Redirected to `/agent/login` (not a 404 or blank page)
- Pass / Fail?
**Test 2 — Login with wrong credentials (AUTH-01):**
- On /agent/login, enter any email + wrong password
- Expected: Page reloads, shows "Invalid email or password" banner
- The error should NOT say which field is wrong
- Pass / Fail?
**Test 3 — Login with correct credentials (AUTH-01):**
- On /agent/login, enter Teressa's AGENT_EMAIL and AGENT_PASSWORD
- Expected: Redirects to /agent/dashboard showing Teressa's email address
- Pass / Fail?
**Test 4 — Session persistence (AUTH-02):**
- After logging in, close the browser tab (or close and reopen the browser window)
- Navigate back to `[your-vercel-url]/agent/dashboard`
- Expected: Still logged in — dashboard shows without redirecting to login
- Pass / Fail?
**Test 5 — Logout (AUTH-04):**
- On /agent/dashboard, click "Sign out"
- Expected: Redirected to /agent/login with a "You've been signed out" message visible on the page
- Pass / Fail?
**Test 6 — Post-logout protection (AUTH-03):**
- After logging out, navigate to `[your-vercel-url]/agent/dashboard`
- Expected: Redirected to /agent/login (session is invalidated)
- Pass / Fail?
**Test 7 — Password toggle:**
- On /agent/login, type any password in the password field
- Click the show/hide toggle
- Expected: Password becomes visible as plain text
- Pass / Fail?
If all 7 tests pass: type "approved" to complete Phase 1.
If any test fails: describe which test failed and what you saw.
</how-to-verify>
<verify>All 7 tests pass and human types "approved"</verify>
<done>Human has approved all 7 auth flow tests in the production Vercel deployment</done>
<resume-signal>Type "approved" if all 7 tests pass, or describe failures</resume-signal>
</task>
</tasks>
<verification>
All verification is human-driven in the checkpoint above. The 7 tests map directly to the Phase 1 success criteria from ROADMAP.md:
| ROADMAP Criterion | Tests |
|-------------------|-------|
| Agent can log in with email and password | Tests 2 + 3 |
| Session persists after browser refresh/tab close | Test 4 |
| Unauthenticated /agent/* routes redirect to login | Tests 1 + 6 |
| Agent can log out | Test 5 |
| Database deployed to Neon, Blob created, Vercel wired | Implicit in Tests 3 + 4 working in production |
</verification>
<success_criteria>
All 7 verification tests pass in the production Vercel deployment:
1. /agent/dashboard redirects unauthenticated users to /agent/login
2. Wrong credentials show "Invalid email or password"
3. Correct credentials grant access to /agent/dashboard
4. Session survives browser tab close and reopen
5. Logout redirects to /agent/login with confirmation message
6. Post-logout /agent/dashboard visit redirects to login
7. Password toggle switches between hidden/visible
Human approves the checkpoint with "approved".
</success_criteria>
<output>
After human approval, create `.planning/phases/01-foundation/01-03-SUMMARY.md` using the summary template.
Include in the summary:
- Production deployment URL
- Confirmation that all 7 verification tests passed
- Neon project name and region
- Whether db:migrate and db:seed ran successfully
- Vercel Blob store name
- Any issues encountered during deployment and how they were resolved
Then update STATE.md:
- Current focus: Phase 2 - Marketing Site
- Phase 1 status: Complete
- Last activity: [today's date] — Phase 1 complete; auth flow verified in production
</output>