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>
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):
- /clients + /interests list-column fix (the original bug — list views show
-everywhere because the service didn't join contacts/yachts) - Full NocoDB Berths import + seeding + mooring-number normalization (current CRM has
A-01..E-18; canonical isA1..E18) - Schema refactor to many-to-many
interest_berthswith role flags (is_primary,is_specific_interest,is_in_eoi_bundle) - Berth recommender (SQL ranking, tier ladder, heat scoring, UI panel) — no AI; pure SQL
- EOI bundle support (multi-berth EOIs + range formatter for the Documenso PDF:
["A1","A2","A3","B5","B6"]→"A1-A3, B5-B6") - Pluggable storage backend (s3-compatible OR local filesystem) so admins can run without MinIO if they want
- Per-berth PDFs (versioned uploads, OCR-based reverse parser, conflict-resolution diff dialog)
- 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 createfeat/berth-recommenderfrom here) - Recent commits (already pushed):
8699f81 chore(style): codebase em-dash sweep + minor layout polishd62822c fix(migration): NocoDB import safety + dedup helpers + lead-source backfill089f4a6 feat(receipts): upload guide page + scanner head-tag fix77ad10c feat(dashboard): custom date range + KPI port-hydration gatee598cc0 feat(layout): unified Inbox + UserMenu extractionf5772ce feat(analytics): Umami integration with per-port admin settings49d34e0 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 — addsWEBSITE_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'svalue_country) - 4 test rows in
website_submissions(from a previous live audit; safe to ignore) - 90 berths with
mooring_numberinA-01format (Phase 0 normalizes toA1) - vitest: 956 tests passing
- tsc: clean (one pre-existing issue in
scripts/smoke-test-redirect.tsthat's unrelated)
- 245 clients (210 with no
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 asA1..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'smainis 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
AskUserQuestionrather 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:
- NocoDB mooring collisions → unique constraint + ON CONFLICT
- Non-PDF disguised upload → magic-byte check
- Recipient email typos → pre-send confirmation
- XSS in email body markdown → DOMPurify + payload tests
- SMTP credentials silently failing → loud error + failed
document_sendsrow - Wrong-environment
CRM_PUBLIC_URL→ health-check env match - Mooring format drift breaking
/berths/A1URLs → Phase 0 normalization gates Phase 3 - Multi-port isolation in recommender → explicit
port_idfilter + cross-port test - Permission escalation on SMTP creds → per-port admin only, no rep visibility
- Filesystem backend in multi-node deployment → refuse to start; documented + health-check enforced
- 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_URLconfirmed ashttps://crm.portnimara.comonce live; configurable via env.- GDPR cascade behavior for
document_sends(delete vs. anonymize-PII vs. keep) — leftOPENin §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
byteaas 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
- Read
docs/berth-recommender-and-pdf-plan.mdend-to-end. Don't skim. The edge-case audit in §14 alone is critical context. - 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.
- Once approved, create
feat/berth-recommenderand start Phase 0.
Phase 0 deliverables (per plan):
- One commit normalizing existing CRM mooring numbers from
A-01→A1form (viaregexp_replacemigration). Delete the offendingscripts/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). Runpnpm db:generate. Verifymeta/_journal.jsonprevId chain stays contiguous. - One commit adding
scripts/import-berths-from-nocodb.ts— the idempotent NocoDB import (handles updates, preserves CRM-side edits vialast_imported_at vs updated_atcheck,pg_advisory_lock, dry-run flag, etc. per §4.1 and §14.1). - Update
src/lib/db/seed-data.tswith 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
.gitignoreto 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.