- SUMMARY.md updated to reflect all 9 verification steps passed
- TMPL-05 through TMPL-09 confirmed by human live browser test
- TMPL-15 and TMPL-16 marked complete in REQUIREMENTS.md
- STATE.md: Phase 19 marked COMPLETE, progress 98%
- ROADMAP.md: Phase 19 all 3 plans complete
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Server component at /portal/templates/[id] queries documentTemplates with formTemplate relation
- notFound() for archived/missing templates
- TemplatePageClient: state owner mirroring DocumentPageClient pattern
- deriveRolesFromFields initializes Buyer/Seller defaults or extracts from existing fields
- handlePersist merges textFillData hints into f.hint for type='text' fields
- handleAiAutoPlace POSTs to /api/templates/[id]/ai-prepare and increments aiPlacementKey
- Role rename/remove re-fetches fields and PATCHes with updated signerEmail values
- TemplatePanel: 280px right panel with inline styles matching portal design system
- Signers/Roles section with color dots, click-to-rename, remove with ConfirmDialog
- Add role input with preset chips (Buyer, Co-Buyer, Seller, Co-Seller)
- AI Auto-place button (navy) with spinner/loading state and error display
- Save Template button (gold) with success 'Saved' text fading after 3s
- Server component at /portal/templates queries documentTemplates with LEFT JOIN to formTemplates
- TemplatesListClient renders list rows with name, form name, field count, last updated
- Create modal POSTs to /api/templates and navigates to /portal/templates/[id] on success
- Empty state with CTA when no templates exist
- Gold #C9A84C accent, navy #1B2B4B headings, inline styles matching portal patterns
- GET /api/templates/[id]/file: streams form PDF from seeds/forms/ with path traversal guard
- GET /api/templates/[id]/fields: returns signatureFields array (or []) from documentTemplates
- POST /api/templates/[id]/ai-prepare: runs AI field placement with null client context, writes to DB
- All routes: auth guard + isNull(archivedAt) soft-delete filter consistent with Phase 18
- PATCH accepts name and signatureFields for rename and field save per D-10
- PATCH sets updatedAt: new Date() explicitly per D-05
- PATCH returns 404 for archived or missing templates
- DELETE soft-deletes by setting archivedAt: new Date() per D-07 (no hard delete)
- DELETE sets updatedAt: new Date() per D-05, returns 204 No Content
- Both handlers auth-gated with auth() per D-11
- GET lists active templates (archivedAt IS NULL) with LEFT JOIN for formName
- GET computes fieldCount server-side from signatureFields.length per D-12
- POST validates name + formTemplateId, verifies FK exists, returns 201
- Both handlers auth-gated with auth() per D-11
- CREATE TABLE document_templates with 7 columns
- FK constraint form_template_id -> form_templates(id) ON DELETE no action
- No alterations to existing tables
- env_file: .env.production — secrets injected at runtime, not baked into image
- dns array [8.8.8.8, 1.1.1.1] + NODE_OPTIONS=--dns-result-order=ipv4first for SMTP EAI_AGAIN fix
- Named volume uploads:/app/uploads persists PDFs across container restarts
- restart: unless-stopped, port 3000:3000
- .gitignore: added /uploads/ entry for production Docker volume path
- Three-stage node:20-slim Dockerfile with --platform=linux/amd64 on all 3 FROM lines
- Non-root nextjs:nodejs user, seeds/ copied for form library, uploads/ dir pre-created
- HEALTHCHECK via wget pointing to /api/health, CMD node server.js
- .dockerignore excludes node_modules, .next, .git, .env*, uploads/, *.md
- .env.production.example with exactly 11 required vars (template, no real secrets, force-added past .env* glob)
- Runs SELECT 1 via Drizzle db client
- Returns { ok: true, db: 'connected' } with status 200 on success
- Returns { ok: false, error: message } with status 503 on DB failure
- No auth check — intentionally public for Docker HEALTHCHECK