docs(07-audit-trail-and-download): create phase 7 plan

3 plans in 3 sequential waves: agent download token + API route (01),
UI wiring for download button + signedAt column (02), human verification
checkpoint (03). Covers SIGN-07 and LEGAL-03.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Chandler Copeland
2026-03-21 10:30:05 -06:00
parent 45f49ce498
commit 9fe7936304
4 changed files with 794 additions and 3 deletions

View File

@@ -0,0 +1,396 @@
---
phase: 07-audit-trail-and-download
plan: "02"
type: execute
wave: 2
depends_on:
- "07-01"
files_modified:
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx
- teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx
- teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx
autonomous: true
requirements:
- SIGN-07
- LEGAL-03
must_haves:
truths:
- "Agent sees a Download button on the document detail page when document status is Signed"
- "Clicking the Download button triggers browser PDF download dialog (no login prompt, no 404)"
- "Download button is absent when document status is Draft, Sent, or Viewed"
- "Dashboard table shows a Date Signed column populated for Signed documents"
- "Dashboard StatusBadge shows Signed for documents that have completed signing"
artifacts:
- path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx"
provides: "Server component that generates agentDownloadUrl and passes it to PreparePanel"
contains: "createAgentDownloadToken"
- path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx"
provides: "Download button rendered only when currentStatus === Signed and agentDownloadUrl is non-null"
contains: "agentDownloadUrl"
- path: "teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx"
provides: "DocumentRow type with signedAt field + Date Signed column in table"
contains: "signedAt"
- path: "teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx"
provides: "Select includes signedAt from documents table"
contains: "signedAt: documents.signedAt"
key_links:
- from: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx"
to: "teressa-copeland-homes/src/lib/signing/token.ts"
via: "import { createAgentDownloadToken } from '@/lib/signing/token'"
pattern: "createAgentDownloadToken"
- from: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx"
to: "PreparePanel"
via: "agentDownloadUrl prop"
pattern: "agentDownloadUrl"
- from: "teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx"
to: "DocumentsTable"
via: "rows prop including signedAt field"
pattern: "signedAt"
---
<objective>
Wire the agent-facing download UI: generate a presigned download URL in the document detail server component and pass it to PreparePanel for Signed documents; add signedAt to the dashboard table.
Purpose: Complete SIGN-07 (agent can download signed PDF) by surfacing the API from Plan 01 in the portal UI. Satisfies LEGAL-03 by ensuring the only download path is the presigned token route — no direct file URLs anywhere in the UI.
Output: Four modified files — document detail page (token generation), PreparePanel (Download button), DocumentsTable (signedAt type + column), dashboard page (signedAt select).
</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/07-audit-trail-and-download/07-RESEARCH.md
@.planning/phases/07-audit-trail-and-download/07-01-SUMMARY.md
<interfaces>
<!-- Current document detail page server component -->
<!-- Source: teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx -->
```typescript
// Current: fetches doc + docClient, renders PdfViewerWrapper + PreparePanel
// Phase 7 change: generate agentDownloadUrl server-side for Signed docs and pass as prop
export default async function DocumentPage({ params }: { params: Promise<{ docId: string }> }) {
// ...existing auth, db fetch...
// ADD after fetching doc:
const agentDownloadUrl = doc.signedFilePath
? `/api/documents/${docId}/download?adt=${await createAgentDownloadToken(docId)}`
: null;
// MODIFY PreparePanel call to pass new props:
<PreparePanel
docId={docId}
defaultEmail={docClient?.email ?? ''}
clientName={docClient?.name ?? ''}
currentStatus={doc.status}
agentDownloadUrl={agentDownloadUrl} // ADD
signedAt={doc.signedAt ?? null} // ADD
/>
}
```
<!-- Current PreparePanel — 'use client' component -->
<!-- Source: teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx -->
```typescript
// Current interface:
interface PreparePanelProps {
docId: string;
defaultEmail: string;
clientName: string;
currentStatus: string;
}
// Phase 7 change: extend interface, add Download button section for Signed status
// CRITICAL: Token generation must NOT happen in PreparePanel — PreparePanel is 'use client'
// PreparePanel only renders an <a href={agentDownloadUrl}> anchor — it does not call createAgentDownloadToken
// Current non-Draft status return (replace with status-aware rendering):
if (currentStatus !== 'Draft') {
return (
<div ...>
Document status is <strong>{currentStatus}</strong> preparation is only available for Draft documents.
</div>
);
}
// Phase 7: When currentStatus === 'Signed' and agentDownloadUrl !== null, show download section instead
// When currentStatus === 'Sent' or 'Viewed', keep the read-only message (no download button)
```
<!-- Current DocumentsTable row type -->
<!-- Source: teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx -->
```typescript
type DocumentRow = {
id: string;
name: string;
clientName: string | null;
status: "Draft" | "Sent" | "Viewed" | "Signed";
sentAt: Date | null;
clientId: string;
// ADD: signedAt: Date | null;
};
```
<!-- Current dashboard page select -->
<!-- Source: teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx -->
```typescript
const allRows = await db
.select({
id: documents.id,
name: documents.name,
status: documents.status,
sentAt: documents.sentAt,
clientName: clients.name,
clientId: documents.clientId,
// ADD: signedAt: documents.signedAt,
})
.from(documents)
// ...
```
<!-- documents table columns (schema) -->
```typescript
status: documentStatusEnum("status").notNull().default("Draft"),
signedFilePath: text("signed_file_path"), // null until signed
signedAt: timestamp("signed_at"), // null until signed
```
<!-- Token function created in Plan 01 -->
```typescript
// From teressa-copeland-homes/src/lib/signing/token.ts (after Plan 01):
export async function createAgentDownloadToken(documentId: string): Promise<string>
// Returns a JWT with purpose:'agent-download', 5-min TTL
// Used in server component only — import forbidden in 'use client' files
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Update PreparePanel props interface and add Download button for Signed status</name>
<files>teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx</files>
<action>
Two changes to PreparePanel.tsx:
**1. Extend PreparePanelProps interface:**
```typescript
interface PreparePanelProps {
docId: string;
defaultEmail: string;
clientName: string;
currentStatus: string;
agentDownloadUrl?: string | null; // ADD
signedAt?: Date | null; // ADD
}
```
**2. Replace the non-Draft early return block with status-aware rendering.** Currently the function returns a generic message for any non-Draft status. Replace with:
```typescript
// For Signed status: show download section with optional date
if (currentStatus === 'Signed') {
return (
<div style={{ borderRadius: '0.5rem', border: '1px solid #D1FAE5', padding: '1rem', backgroundColor: '#F0FDF4' }}>
<p style={{ fontSize: '0.875rem', color: '#065F46', fontWeight: 600, marginBottom: '0.5rem' }}>
Document Signed
</p>
{signedAt && (
<p style={{ fontSize: '0.75rem', color: '#6B7280', marginBottom: '0.75rem' }}>
Signed on{' '}
{new Date(signedAt).toLocaleString('en-US', {
timeZone: 'America/Denver',
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit',
})}
</p>
)}
{agentDownloadUrl ? (
<a
href={agentDownloadUrl}
style={{
display: 'inline-block',
padding: '0.5rem 1rem',
backgroundColor: '#1B2B4B',
color: '#FFFFFF',
borderRadius: '0.375rem',
fontSize: '0.875rem',
fontWeight: 500,
textDecoration: 'none',
}}
>
Download Signed PDF
</a>
) : (
<p style={{ fontSize: '0.75rem', color: '#9CA3AF' }}>Signed PDF not available.</p>
)}
</div>
);
}
// For Sent/Viewed: keep existing read-only message
if (currentStatus !== 'Draft') {
return (
<div style={{ borderRadius: '0.5rem', border: '1px solid #E5E7EB', padding: '1rem', backgroundColor: '#F9FAFB', fontSize: '0.875rem', color: '#6B7280' }}>
Document status is <strong>{currentStatus}</strong> preparation is only available for Draft documents.
</div>
);
}
```
Important: The Download button is a plain `<a href={agentDownloadUrl}>` anchor — no fetch(), no onClick handler. The browser follows the link directly, which triggers the Content-Disposition: attachment response from the API route.
Do not destructure `agentDownloadUrl` or `signedAt` from props in the function signature until you have updated the interface. Update interface first.
</action>
<verify>
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20</automated>
</verify>
<done>PreparePanel accepts agentDownloadUrl and signedAt props; TypeScript passes; Signed status renders download section; Sent/Viewed status renders read-only message; Draft status renders full prepare form unchanged</done>
</task>
<task type="auto">
<name>Task 2: Wire document detail page, update dashboard table for signedAt</name>
<files>
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx
teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx
teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx
</files>
<action>
**File 1: documents/[docId]/page.tsx**
Add import for createAgentDownloadToken at the top:
```typescript
import { createAgentDownloadToken } from '@/lib/signing/token';
```
After the existing `const [doc, docClient] = await Promise.all([...])` block, add:
```typescript
// Generate agent download URL server-side for Signed documents
// Must be done here (server component) — PreparePanel is 'use client' and cannot call createAgentDownloadToken
const agentDownloadUrl = doc.signedFilePath
? `/api/documents/${docId}/download?adt=${await createAgentDownloadToken(docId)}`
: null;
```
Pass the two new props to PreparePanel:
```tsx
<PreparePanel
docId={docId}
defaultEmail={docClient?.email ?? ''}
clientName={docClient?.name ?? ''}
currentStatus={doc.status}
agentDownloadUrl={agentDownloadUrl}
signedAt={doc.signedAt ?? null}
/>
```
Note: `doc.signedAt` is available on the document object — it's a column in the documents table (timestamp("signed_at")).
---
**File 2: DocumentsTable.tsx**
Add `signedAt: Date | null` to the DocumentRow type:
```typescript
type DocumentRow = {
id: string;
name: string;
clientName: string | null;
status: "Draft" | "Sent" | "Viewed" | "Signed";
sentAt: Date | null;
signedAt: Date | null; // ADD
clientId: string;
};
```
Add a "Date Signed" column header after the "Date Sent" `<th>`:
```tsx
<th style={{ textAlign: "left", fontSize: "0.75rem", fontWeight: 600, color: "#6B7280", textTransform: "uppercase", letterSpacing: "0.05em", padding: "0.75rem 1.5rem" }}>
Date Signed
</th>
```
Add a "Date Signed" `<td>` in the row map after the "Date Sent" cell:
```tsx
<td style={{ padding: "0.875rem 1.5rem", color: "#6B7280" }}>
{row.signedAt
? new Date(row.signedAt).toLocaleDateString("en-US", {
timeZone: "America/Denver",
month: "short",
day: "numeric",
year: "numeric",
})
: "—"}
</td>
```
---
**File 3: dashboard/page.tsx**
Add `signedAt: documents.signedAt` to the select object:
```typescript
const allRows = await db
.select({
id: documents.id,
name: documents.name,
status: documents.status,
sentAt: documents.sentAt,
signedAt: documents.signedAt, // ADD
clientName: clients.name,
clientId: documents.clientId,
})
.from(documents)
.leftJoin(clients, eq(documents.clientId, clients.id))
.orderBy(desc(documents.createdAt));
```
The `allRows` inferred type will now include `signedAt: Date | null`, which matches the updated DocumentRow type in DocumentsTable.tsx.
</action>
<verify>
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20 && npm run build 2>&1 | tail -10</automated>
</verify>
<done>
tsc --noEmit passes with no errors;
npm run build passes cleanly;
DocumentsTable accepts rows with signedAt field;
Dashboard query selects signedAt;
Document detail page imports and calls createAgentDownloadToken for signed docs;
PreparePanel receives agentDownloadUrl and signedAt props
</done>
</task>
</tasks>
<verification>
1. `npx tsc --noEmit` passes — no type errors across all four modified files
2. `npm run build` completes successfully
3. PreparePanel renders three distinct states: Draft (prepare form), Sent/Viewed (read-only message), Signed (green panel with download link)
4. agentDownloadUrl is generated in the server component (page.tsx), not in PreparePanel
5. DocumentsTable has Date Signed column
6. Dashboard query includes signedAt in select
</verification>
<success_criteria>
- Agent can navigate to a Signed document detail page and see a green "Document Signed" panel with signed timestamp and "Download Signed PDF" anchor link
- Download button is absent for Draft/Sent/Viewed documents
- Dashboard table shows "Date Signed" column with date for Signed documents, "—" for others
- Build passes with no TypeScript errors
</success_criteria>
<output>
After completion, create `.planning/phases/07-audit-trail-and-download/07-02-SUMMARY.md`
</output>