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>
14 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 07-audit-trail-and-download | 02 | execute | 2 |
|
|
true |
|
|
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).
<execution_context> @/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md @/Users/ccopeland/.claude/get-shit-done/templates/summary.md </execution_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// 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 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)
type DocumentRow = {
id: string;
name: string;
clientName: string | null;
status: "Draft" | "Sent" | "Viewed" | "Signed";
sentAt: Date | null;
clientId: string;
// ADD: signedAt: Date | null;
};
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)
// ...
status: documentStatusEnum("status").notNull().default("Draft"),
signedFilePath: text("signed_file_path"), // null until signed
signedAt: timestamp("signed_at"), // null until signed
// 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
1. Extend PreparePanelProps interface:
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:
// 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.
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20
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
Add import for createAgentDownloadToken at the top:
import { createAgentDownloadToken } from '@/lib/signing/token';
After the existing const [doc, docClient] = await Promise.all([...]) block, add:
// 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:
<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:
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>:
<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:
<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:
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.
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20 && npm run build 2>&1 | tail -10
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
<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>