Files
pn-new-crm/docs/berth-feature-handoff-prompt.md
Matt Ciaccio 180912ba9f fix(audit-final): pre-merge hardening + expense receipt UI
Final audit pass on feat/berth-recommender (3 parallel Opus agents)
caught 5 critical and ~12 high-severity findings. All addressed in-branch;
medium/low items deferred to docs/audit-final-deferred.md.

Critical:
- Add filesystem-backend PUT handler at /api/storage/[token] so
  presigned uploads stop 405-ing in filesystem mode (every browser-driven
  berth-PDF + brochure upload was broken). Same token-verify + replay
  protection as GET, plus magic-byte gate when c=application/pdf.
- Forward req.signal into streamExpensePdf so an aborted 1000-receipt
  export no longer keeps grinding for minutes.
- Strengthen Content-Disposition filename sanitization: \s matches CR/LF
  which would let documentName forge headers; restrict to [\w. -]+ and
  add filename* RFC 5987 fallback.
- Lock public berths feed behind an explicit slug allowlist instead of
  ?portSlug= enumeration.
- Reject cross-port interest_berths upserts (defense-in-depth on top of
  the recommender SQL port filter).

High:
- Recommender: width-only feasibility now caps length via L/W ratio so a
  200ft berth doesn't surface for a 30ft beam request; total_interest_count
  filters out junction rows whose interest is in another port.
- Mooring normalization follow-up migration (0034) catches un-hyphenated
  padded forms (A01) the original 0024 WHERE missed.
- Send-out rate limit moved AFTER validation and scoped per-(port, user)
  so typos don't burn a slot and a multi-port rep can't be DoS'd by
  another tenant.
- Default-brochure path now blocks an archived row from sneaking through
  the partial unique index.
- NocoDB import --update-snapshot honoured under --dry-run so reps can
  refresh the seed JSON without committing DB writes.
- PDF export: orderBy desc(expenseDate); apply isNull(archivedAt) when
  expenseIds are passed (was bypassed); flag rate-unavailable rows with
  an amber footer instead of silently treating them as 1:1; skip the
  USD->EUR chain when source already matches target.
- expense-form-dialog: revokeObjectURL captures the URL in the closure
  instead of revoking the still-displayed one; reset upload state on
  close.
- scan/page: handleClearReceipt resets in-flight scan/upload mutations;
  Save disabled while upload pending.
- updateExpense re-asserts receipt-or-acknowledgement at the merged
  row so PATCH can't slip past the create-time refine.

Plus the in-progress receipt upload UI for the expense form dialog
(receipt picker + "I have no receipt" checkbox + warning banner) and
a noReceiptAcknowledged flag on ExpenseRow for edit-mode hydration.

Includes the canonical plan doc (referenced in CLAUDE.md), the handoff
prompt, and a deferred-findings index for follow-up issues.

1163/1163 vitest passing. Typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 05:11:26 +02:00

9.6 KiB

Handoff prompt for new Claude Code session

Copy everything below the --- line into the new chat as your first message.


I'm continuing work on a comprehensive multi-feature push that was fully designed in a prior session but not yet implemented. The complete plan lives at docs/berth-recommender-and-pdf-plan.md (~1030 lines). Read that file end-to-end before doing anything else — every design decision, schema change, edge case, and confirmed answer to a product question is captured there. Don't re-litigate decisions; if something seems unclear, the answer is almost certainly in the plan.

What the project is

A multi-tenant marina/port-management CRM at /Users/matt/Repos/new-pn-crm. Next.js 15 App Router, React 19, TypeScript strict, Drizzle ORM on Postgres, MinIO for files, BullMQ on Redis, better-auth, shadcn/ui, Tailwind. See CLAUDE.md for the conventions.

What we're building (high level)

The plan bundles 8 capabilities into one branch (feat/berth-recommender):

  1. /clients + /interests list-column fix (the original bug — list views show - everywhere because the service didn't join contacts/yachts)
  2. Full NocoDB Berths import + seeding + mooring-number normalization (current CRM has A-01..E-18; canonical is A1..E18)
  3. Schema refactor to many-to-many interest_berths with role flags (is_primary, is_specific_interest, is_in_eoi_bundle)
  4. Berth recommender (SQL ranking, tier ladder, heat scoring, UI panel) — no AI; pure SQL
  5. EOI bundle support (multi-berth EOIs + range formatter for the Documenso PDF: ["A1","A2","A3","B5","B6"]"A1-A3, B5-B6")
  6. Pluggable storage backend (s3-compatible OR local filesystem) so admins can run without MinIO if they want
  7. Per-berth PDFs (versioned uploads, OCR-based reverse parser, conflict-resolution diff dialog)
  8. Sales send-out emails (berth PDF + brochure) with full audit + size-aware fallback to download links

Phase ordering (from plan §2)

Phase 0:  Full NocoDB berth import + mooring normalization + 5 new pricing columns
Phase 1:  /clients + /interests list column fix
Phase 2:  M:M interest_berths schema refactor + desired dimensions on interests
Phase 3:  CRM /api/public/berths endpoint + website cutover
Phase 4:  Recommender SQL + tier ladder + heat + UI panel
Phase 5:  EOI bundle + range formatter
Phase 6a: Pluggable storage backend + migration CLI + admin UI
Phase 6b: Per-berth PDF storage (versioned) + reverse parser
Phase 7:  Sales send-outs + brochure admin + email-from settings
Phase 8:  CLAUDE.md updates + final validation

Start with Phase 0.

Working tree state at handoff

  • Branch: main (you'll create feat/berth-recommender from here)
  • Recent commits (already pushed):
    • 8699f81 chore(style): codebase em-dash sweep + minor layout polish
    • d62822c fix(migration): NocoDB import safety + dedup helpers + lead-source backfill
    • 089f4a6 feat(receipts): upload guide page + scanner head-tag fix
    • 77ad10c feat(dashboard): custom date range + KPI port-hydration gate
    • e598cc0 feat(layout): unified Inbox + UserMenu extraction
    • f5772ce feat(analytics): Umami integration with per-port admin settings
    • 49d34e0 feat(website-intake): dual-write endpoint + migration chain repair
  • Untracked / uncommitted at handoff:
    • docs/berth-recommender-and-pdf-plan.md (the plan — read this first)
    • docs/berth-feature-handoff-prompt.md (this file)
    • berth_pdf_example/ (two reference files — see below)
    • .env.example (modified — adds WEBSITE_INTAKE_SECRET=; pre-commit hook blocks .env* files so user adds this manually)
  • Dev DB state:
    • 245 clients (210 with no nationality_iso — Phase 1 backfills from primary phone's value_country)
    • 4 test rows in website_submissions (from a previous live audit; safe to ignore)
    • 90 berths with mooring_number in A-01 format (Phase 0 normalizes to A1)
    • vitest: 956 tests passing
    • tsc: clean (one pre-existing issue in scripts/smoke-test-redirect.ts that's unrelated)

Reference files

  • berth_pdf_example/Berth_Spec_Sheet_A1.pdf (358 KB) — sample per-berth PDF. 0 AcroForm fields (confirmed via pdf-lib) so OCR with positional heuristics is the primary parser tier; the AcroForm tier is built defensively. Plan §9.2 captures the layout structure.
  • berth_pdf_example/Port-Nimara-Brochure-March-2025_5nT92g.pdf (10.26 MB) — sample brochure. Sized so it ships as an attachment under the 15 MB threshold. Plan §11.1 covers brochure handling.

NocoDB access

You have mcp__NocoDB_Base_-_Port_Nimara__* tools available. Tables you'll touch most:

  • mczgos9hr3oa9qc — Berths (Phase 0 imports from here; mooring numbers are stored as A1..E18)
  • mbs9hjauug4eseo — Interests (the combined client+deal table the old system used)

Branch & commit conventions

  • Create the branch: git checkout -b feat/berth-recommender
  • Commit messages match recent history style: <type>(<scope>): <subject> lowercase, terse subject, body explains why not what.
  • Pre-commit hook blocks any .env* file including .env.example. If you need to update .env.example, leave it staged and tell the user to commit manually with --no-verify (they're aware of this).
  • Don't push without explicit user permission. Commits are fine; pushes need approval.
  • Don't run git rebase, git push --force, or anything destructive without checking. The branch is solo-owned but the repo's main is shared.

User communication preferences (from prior session)

  • Direct, no fluff. If something is a bad idea, say so — don't sycophant.
  • When proposing changes, include trade-offs explicitly.
  • For multi-question decisions, use AskUserQuestion rather than long bulleted lists.
  • Run validation (vitest + tsc) at logical checkpoints. Don't ship a commit with regressions.
  • The user prefers small focused commits over mega-commits. Within Phase 0 alone there will probably be 2-3 commits (e.g. mooring normalization, schema additions, NocoDB import script).

Critical rules (from plan §14)

Eleven 🔴 critical items requiring tests before their phase ships:

  1. NocoDB mooring collisions → unique constraint + ON CONFLICT
  2. Non-PDF disguised upload → magic-byte check
  3. Recipient email typos → pre-send confirmation
  4. XSS in email body markdown → DOMPurify + payload tests
  5. SMTP credentials silently failing → loud error + failed document_sends row
  6. Wrong-environment CRM_PUBLIC_URL → health-check env match
  7. Mooring format drift breaking /berths/A1 URLs → Phase 0 normalization gates Phase 3
  8. Multi-port isolation in recommender → explicit port_id filter + cross-port test
  9. Permission escalation on SMTP creds → per-port admin only, no rep visibility
  10. Filesystem backend in multi-node deployment → refuse to start; documented + health-check enforced
  11. Path traversal via storage key in filesystem mode → strict regex validation + path realpath check

Pending items (from plan §9)

These are non-blocking but worth knowing:

  • Sample brochure already provided (the 10.26 MB file above).
  • SMTP app password for sales@portnimara.com — not yet obtained; expected close to production cutover. Phase 7 ships the admin UI immediately and the credential gets entered when available.
  • CRM_PUBLIC_URL confirmed as https://crm.portnimara.com once live; configurable via env.
  • GDPR cascade behavior for document_sends (delete vs. anonymize-PII vs. keep) — left OPEN in §14.10, default lean: anonymize-PII. Revisit when Phase 7 schema lands.

Scope reminder

  • No prod data depends on the current CRM schema — refactors don't need backwards-compatibility shims. But every schema change still ships as a Drizzle migration with pnpm db:generate.
  • Pluggable storage rejects Postgres bytea as an option (§4.7a). The two backends are s3-compatible (MinIO/AWS/B2/R2/etc.) and local filesystem. Filesystem is single-node only.

What to do first

  1. Read docs/berth-recommender-and-pdf-plan.md end-to-end. Don't skim. The edge-case audit in §14 alone is critical context.
  2. Confirm you've understood the plan by stating back the 8-phase outline and the 11 critical items, then ask the user if they want to proceed with Phase 0.
  3. Once approved, create feat/berth-recommender and start Phase 0.

Phase 0 deliverables (per plan):

  • One commit normalizing existing CRM mooring numbers from A-01A1 form (via regexp_replace migration). Delete the offending scripts/load-berths-to-port-nimara.ts.
  • One commit adding the 5 new berth columns (weekly_rate_high_usd, weekly_rate_low_usd, daily_rate_high_usd, daily_rate_low_usd, pricing_valid_until, last_imported_at). Run pnpm db:generate. Verify meta/_journal.json prevId chain stays contiguous.
  • One commit adding scripts/import-berths-from-nocodb.ts — the idempotent NocoDB import (handles updates, preserves CRM-side edits via last_imported_at vs updated_at check, pg_advisory_lock, dry-run flag, etc. per §4.1 and §14.1).
  • Update src/lib/db/seed-data.ts with the imported berth set so fresh installs get them.
  • Final vitest + tsc validation at the end of Phase 0.

Don't

  • Don't push to remote during this session (user will batch the push later).
  • Don't commit .env* files (hook blocks them anyway).
  • Don't edit .gitignore to exclude generated artifacts; the repo's existing ignores are correct.
  • Don't add documentation files unless the plan asks for them — the plan itself is the doc.
  • Don't add features not in the plan. If something seems missing, ask.
  • Don't use AI for the recommender (plan §1 + §13). Pure SQL ranking.

Once you've read the plan and confirmed understanding, ask me whether to proceed with Phase 0.