Bundles the rest of the in-flight work from this UAT round into one
checkpoint. Each sub-area is independent; see the headings below.
UAT polish (drained 11 findings from active-uat.md):
- Dialog primitive default bumped sm:max-w-xl/lg:max-w-3xl →
sm:max-w-2xl/lg:max-w-4xl so multi-field forms + PDF previews
aren't cramped at 1440-1920px.
- Notes tab badge aggregation: new countFor{Client,Yacht,Company}
Aggregated helpers in notes.service mirror the listFor*Aggregated
symmetric-reach joins. yacht-tabs + company-tabs render the
badge; client-tabs already had badge support.
- Supplemental-info form polish bundle: BrandedAuthShell gains a
`width: 'sm' | 'md'` prop (md uses min-h-dvh scroll instead of
fixed inset-0 pin so long forms scroll naturally). Form picks up
port branding (logoUrl + backgroundUrl + appName) via
loadByToken. Address fields completed (street + city + region +
postal + country). Port name eyebrow + success-state copy added.
- new-document-menu Upload-file landing toast: per-file completion
emits toast.success with action link to the destination entity
or folder.
- interest-tabs OverviewTab "from client" pill on Email + Phone
rows via new EditableRow `inheritedFrom` prop.
- create-document-wizard subject picker → segmented button strip
(5 types visible at once).
Launch infra:
- UTM column wiring (Init 1b step 4): migration
0089_website_submissions_utm.sql adds utm_source/medium/campaign/
term/content + composite index (port_id, utm_source, received_at)
for per-campaign rollups. website-inquiries intake accepts the
five fields. Residential intake intentionally untouched per audit
scope.
- Invoicing module gate (Init 1c spike): new
invoices-module.service + invoices layout guard + registry entry
invoices_module_enabled (default false). Audit conclusion in
launch-readiness.md: payments table is canonical money path;
/invoices flow is parallel infrastructure now hidden by default.
Smart-back navigation refactor:
- Replaced breadcrumb component with history-aware Back button.
New route-labels.ts + use-smart-back hook +
navigation-history-tracker so back falls through to the parent
route when there's no prior page in history.
- Sidebar / topbar / mobile-topbar adopt the new pattern; old
breadcrumb-store kept for back-compat consumers but the
breadcrumbs component is gone.
- 6 detail pages (admin/errors per-id + codes, invoices/
upload-receipts, reports kind, tenancies detail, analytics
metric, client detail) migrated.
Trackers + docs:
- docs/launch-readiness.md — master pre-launch tracker. Includes
the reports gap audit (cross-cutting filter set, Marketing +
Financial blockers, custom builder remaining entities, scheduled
CSV/XLSX, template scope picker).
- docs/superpowers/audits/active-uat.md — 15 findings flipped
OPEN → SHIPPED locally with fix-applied notes; 4 OPEN remaining
(each blocked on user input or cross-repo).
- CLAUDE.md — minor session notes carried forward.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
241 lines
21 KiB
Markdown
241 lines
21 KiB
Markdown
# Port Nimara CRM
|
||
|
||
Multi-tenant CRM for marina/port management. Next.js 15 App Router (standalone), React 19, TypeScript strict (`noUncheckedIndexedAccess`, no `any`), Drizzle ORM on PostgreSQL.
|
||
|
||
## Quick reference
|
||
|
||
```bash
|
||
pnpm dev # Dev server
|
||
pnpm build # Production build
|
||
pnpm lint / format # ESLint / Prettier
|
||
pnpm db:generate # Generate Drizzle migrations
|
||
pnpm db:push # Push schema to DB
|
||
pnpm db:studio # Drizzle Studio GUI
|
||
pnpm db:seed # Seed (tsx src/lib/db/seed.ts)
|
||
|
||
# Tests
|
||
pnpm exec vitest run # Unit + integration (~3s)
|
||
pnpm exec playwright test --project=smoke # Click-through smoke (~10min)
|
||
pnpm exec playwright test --project=exhaustive # Full UI exhaustive
|
||
pnpm exec playwright test --project=destructive # Archive/delete flows
|
||
pnpm exec playwright test --project=realapi # Real Documenso/IMAP (opt-in)
|
||
pnpm exec playwright test --project=visual # Pixel-diff baselines
|
||
pnpm exec playwright test --project=visual --update-snapshots # Regenerate baselines
|
||
|
||
# Dev helpers
|
||
pnpm tsx scripts/dev-trigger-portal-invite.ts # Send a portal activation email
|
||
pnpm tsx scripts/dev-imap-probe.ts # Dump recent IMAP inbox messages
|
||
|
||
# Cloudflare quick-tunnel (for Documenso webhook testing)
|
||
launchctl load ~/Library/LaunchAgents/solutions.letsbe.pn-crm-tunnel.plist # start
|
||
launchctl unload ~/Library/LaunchAgents/solutions.letsbe.pn-crm-tunnel.plist # stop
|
||
./scripts/tunnel-url.sh --copy # print + copy webhook URL
|
||
|
||
# Schema migration (pnpm db:migrate is broken — apply via psql)
|
||
PGPASSWORD=changeme psql -h localhost -p 5434 -U crm -d port_nimara_crm -f src/lib/db/migrations/0075_*.sql
|
||
```
|
||
|
||
## Working in this repo — skills, MCPs, agents
|
||
|
||
Reach for these before grinding through tasks manually:
|
||
|
||
- **Skills** (invoke with `Skill` tool):
|
||
- `superpowers:brainstorming` before any feature/component work — explores intent + design first
|
||
- `superpowers:test-driven-development` for any feature or bugfix
|
||
- `superpowers:systematic-debugging` for any bug / test failure / unexpected behavior
|
||
- `superpowers:verification-before-completion` before claiming "done" or committing
|
||
- `superpowers:writing-plans` / `executing-plans` for multi-step specs
|
||
- `superpowers:dispatching-parallel-agents` when 2+ tasks are independent
|
||
- `frontend-design:frontend-design` for new UI work (avoids generic AI aesthetics)
|
||
- `code-review:code-review` and `security-review` before merging
|
||
- **MCPs**:
|
||
- **Context7** (`mcp__plugin_context7_context7__*`) — pull current docs for Next 15, Drizzle, better-auth, BullMQ, Tailwind, Radix etc. Prefer over web search; our training data lags.
|
||
- **Playwright** (`mcp__plugin_playwright_playwright__*`) — verify UI changes in a real browser before reporting "done". Default viewport — do NOT call `browser_resize`.
|
||
- **Serena** (`mcp__plugin_serena_serena__*`) — symbol-level navigation (`find_symbol`, `find_referencing_symbols`, `replace_symbol_body`). Much faster than grep for "where is this called".
|
||
- **Postman** (`mcp__claude_ai_Postman__*`) — when designing or auditing API surfaces.
|
||
- **Agents** (via `Agent` tool, `subagent_type=`):
|
||
- `Explore` for any codebase search that would take > 3 queries
|
||
- `feature-dev:code-explorer` / `code-architect` / `code-reviewer` for new feature work
|
||
- **Doctrine**: skills override default behavior except user instructions in this file. If a CLAUDE.md rule conflicts with a skill, this file wins.
|
||
- **Pre-launch tracker**: `docs/launch-readiness.md` is the master pre-launch tracker for the beta phase. Append every launch-blocking initiative or sub-task there with status tags (`OPEN | IN PROGRESS | SHIPPED in <hash> | BLOCKED | DEFERRED`). Read it at the start of any non-trivial task.
|
||
- **Manual UAT — currently active doc**: `docs/superpowers/audits/active-uat.md` is the **live** findings doc. Every UAT finding the user surfaces in chat lands here regardless of which session captures it. Persists across sessions until the user explicitly says to wrap the round and archive — at which point rename to `YYYY-MM-DD-uat.md` and start a fresh `active-uat.md`. Buckets: Quick fixes (<15min), Medium (15min–2h), Features/larger (>2h), Bugs (severity-tagged). Tag every entry with status: `OPEN | IN PROGRESS | SHIPPED in <hash> | QUEUED | BLOCKED`. Don't ask the format each time.
|
||
|
||
## Tech stack (non-obvious choices)
|
||
|
||
- **Auth:** better-auth — session cookie `pn-crm.session_token`
|
||
- **Queue:** BullMQ + Redis (ioredis)
|
||
- **Storage:** pluggable via `getStorageBackend()` — MinIO/S3 default; never import the S3 SDK directly
|
||
- **Realtime:** Socket.IO with Redis adapter
|
||
- **UI:** Radix UI + shadcn/ui (`src/components/ui/`) + Lucide + CVA + tailwind-merge
|
||
- **Forms:** react-hook-form + zod resolvers
|
||
- **State:** Zustand (`src/stores/`) + TanStack React Query
|
||
- **PDF:** pdfme (templates) + pdf-lib (AcroForm fill)
|
||
- **Email:** nodemailer + imapflow + mailparser
|
||
|
||
## Project structure
|
||
|
||
```
|
||
src/
|
||
app/
|
||
(auth)/ # Login/auth pages
|
||
(dashboard)/ # Main app — route: /[portSlug]/...
|
||
(portal)/ # Client portal
|
||
api/ # API routes (route.ts + sibling handlers.ts)
|
||
components/
|
||
ui/ # shadcn/ui base components
|
||
layout/ # Shell, sidebar, header
|
||
[domain]/ # clients, yachts, companies, reservations, berths, …
|
||
shared/ # Cross-domain (BrandedAuthShell, InlineEditableField, …)
|
||
hooks/ # use-auth, use-permissions, use-socket, …
|
||
lib/
|
||
api/ # Route helpers (parseBody, errorResponse, withAuth, …)
|
||
auth/ # better-auth config
|
||
db/schema/ # Drizzle schema — one file per domain, re-exported from index.ts
|
||
db/migrations/ # Generated Drizzle migrations (apply via psql in dev)
|
||
env.ts # Zod env validation (SKIP_ENV_VALIDATION=1 bypasses)
|
||
services/ # Business logic
|
||
storage/ # Pluggable storage backend
|
||
templates/ # Email/document merge fields, berth-range formatter
|
||
validators/ # Zod schemas for API input
|
||
middleware.ts # Auth middleware (cookie check, redirects)
|
||
stores/ # Zustand
|
||
```
|
||
|
||
## Conventions & gotchas
|
||
|
||
### API shape
|
||
|
||
- **Envelope:** `{ data: <T> }` for any returned content (read OR write). Mutations returning nothing emit `204 No Content`. Don't use `{ success: true }` (legacy; normalized away 2026-05-07). Public portal-auth endpoints keep `{ success: true }` so the frontend can chain.
|
||
- **Lists:** `{ data: <T[]>, total?, hasMore? }` — see `/api/v1/clients`.
|
||
- **Errors:** always via `errorResponse(error)` from `@/lib/errors` (request-id propagation + audit-tier mapping).
|
||
- **Body parsing:** always `parseBody(req, schema)` from `@/lib/api/route-helpers`. Raw `req.json() + schema.parse()` produces a generic 500 instead of the field-level 400 the frontend's `toastError` hook expects.
|
||
- **Route handlers:** `route.ts` files can only export `GET|POST|…`. Service-tested handlers live in sibling `handlers.ts` (e.g. `src/app/api/v1/yachts/[id]/handlers.ts`) and are imported by `route.ts` with `withAuth(withPermission(...))`. Integration tests import from `handlers.ts` directly to bypass middleware.
|
||
|
||
### Data model
|
||
|
||
- **Polymorphic ownership:** Yachts and invoice billing-entities use `<entity>_type` + `<entity>_id` pairs (`'client' | 'company'`). Resolve via `src/lib/services/yachts.service.ts` / `eoi-context.ts` — never read the columns ad hoc.
|
||
- **Multi-berth interest model:** `interest_berths` is the source of truth — `interests.berth_id` does not exist (dropped in 0029). Three flags: `is_primary` (≤1 per interest, partial unique index — "the berth for this deal"), `is_specific_interest` (true → public map shows "Under Offer"), `is_in_eoi_bundle` (covered by EOI signature). Read/write only via `src/lib/services/interest-berths.service.ts` helpers.
|
||
- **Notes (polymorphic):** `notes.service.ts` dispatches across `clientNotes`/`interestNotes`/`yachtNotes`/`companyNotes` via an `entityType` discriminator. `<NotesList entityType="…" />` works for all four. `companyNotes` lacks `updatedAt` — service substitutes `createdAt` for shape uniformity.
|
||
- **Mooring number canonical format:** `^[A-Z]+\d+$` (e.g. `A1`, `B12`, `E18`) — no hyphen, no leading zeros. Stored, displayed, URL-encoded, EOI-rendered in this exact form. Regex gates the public `/api/public/berths/[mooringNumber]` route before any DB hit.
|
||
- **Routes:** Multi-tenant via `[portSlug]` dynamic segment. Typed routes enabled.
|
||
|
||
### Schema migrations during dev
|
||
|
||
After `db:push` or applying a migration via `psql` against a running dev server, **restart `next dev`**. Drizzle/postgres.js prepared statements cache stale column lists; symptom is `42703 column X does not exist` 500s on migrated tables.
|
||
|
||
### Documenso
|
||
|
||
- **Webhooks:** plaintext secret in `X-Documenso-Secret` (no HMAC) — timing-safe equality via `verifyDocumensoSecret`. Event names arrive uppercase-enum (`DOCUMENT_SIGNED`, `DOCUMENT_COMPLETED` …); the receiver also normalizes lowercase-dotted for forward-compat. `handleDocumentCompleted` is **idempotent** (early-return when `status='completed' && signedFileId`) so 5xx retries don't double-write. Switch handles SIGNED|COMPLETED|REJECTED|DECLINED|OPENED|EXPIRED + v2 aliases RECIPIENT_VIEWED/SIGNED. Detail: `docs/documenso-integration-audit.md`.
|
||
- **v1 vs v2 routing:** `getPortDocumensoConfig(portId)` resolves per-port `apiVersion`. `documenso-client.ts` exports version-aware wrappers (`getDocument`, `createDocument`, `sendDocument`, `sendReminder`, `downloadSignedPdf`, `voidDocument`, `placeFields`). v2 → `/api/v2/envelope/*` (multipart create, `distribute` returns per-recipient signingUrl, `redistribute` for reminders, `field/create-many` for bulk placement). v1 → `/api/v1/documents/*`. **Template flow stays v1** (`/api/v1/templates/{id}/generate-document` with name-keyed `formValues`) — v2 instances accept via backcompat. v2-only settings honoured: `documenso_signing_order` (PARALLEL/SEQUENTIAL) + `documenso_redirect_url`.
|
||
- **Response normalization:** 2.x uses `documentId` / `recipientId`; v1.13 uses `id`. `normalizeDocument()` surfaces the legacy `id` form to downstream consumers.
|
||
- **`DOCUMENSO_API_URL`:** bare host only — never include `/api/v1`. Client appends versioned paths based on `DOCUMENSO_API_VERSION`. Double-pathing returns 404 with no useful diagnostic.
|
||
|
||
### EOI generation
|
||
|
||
- Two pathways share `EoiContext` (`src/lib/services/eoi-context.ts`). Documenso pathway uses `documenso-payload.ts` → template-generate endpoint; in-app pathway fills `assets/eoi-template.pdf` via `src/lib/pdf/fill-eoi-form.ts`. Routed through `generateAndSign(...)` in `document-templates.ts` with a `pathway` parameter.
|
||
- **Merge fields:** Catalog in `src/lib/templates/merge-fields.ts`; `createTemplateSchema` uses `VALID_MERGE_TOKENS` as an allow-list, rejecting unknown tokens at template creation.
|
||
- **Berth range formatter:** Multi-berth EOIs render the in-bundle berth set as a compact range ("A1-A3, B5-B7") via `formatBerthRange()` (`src/lib/templates/berth-range.ts`). Output populates the existing `Berth Number` Documenso field (single-berth = primary mooring verbatim; multi-berth = range). CRM UI always shows berths as chips. `{{eoi.berthRange}}` token available for template body copy.
|
||
- Detail: `docs/eoi-documenso-field-mapping.md`, `assets/README.md`.
|
||
|
||
### UI patterns
|
||
|
||
- **Sheet vs Drawer:** `<Sheet side="right">` (`src/components/ui/sheet.tsx`, Radix dialog) is the canonical side-panel for both desktop and mobile (`w-3/4 sm:max-w-sm`). Vaul `<Drawer>` (`src/components/shared/drawer.tsx`) is mobile-bottom-sheet only — currently just `MoreSheet`. Need a side panel? Use Sheet. Don't add Vaul without a mobile-bottom-sheet justification.
|
||
- **Inline editing:** Detail pages use `<InlineEditableField>` for text/select/textarea and `<InlineTagEditor>` for tag chips. Each entity exposes `PUT /api/v1/<entity>/[id]/tags` backed by a `set<Entity>Tags` service helper (single-transaction wipe-and-rewrite). No separate "Edit" modals — overview tab is editable in place.
|
||
- **Email + auth surfaces:** Branded HTML in `src/lib/email/templates/`; portal-auth uses `portal-auth.ts`. All templates: table-based, max-width 600, logo + blurred overhead background (`s3.portnimara.com`). CRM `/login`, `/reset-password`, `/set-password` and portal `/portal/login`, `/portal/activate`, `/portal/reset-password` all wrap content in `<BrandedAuthShell>` for visual continuity.
|
||
|
||
### Document folders
|
||
|
||
- Per-port nestable tree (`document_folders.parent_id` self-FK; null parent = root). Documents and files carry nullable `folder_id`. Sibling-name uniqueness via `uniq_document_folders_sibling_name` on `(port_id, COALESCE(parent_id,'__root__'), LOWER(name))`. Folder delete is **soft rescue** (`deleteFolderSoftRescue`) — re-parents children up, drops folder; never CASCADE. `moveFolder` walks ancestor chain to prevent cycles.
|
||
- Three system roots (`Clients/`, `Companies/`, `Yachts/`) auto-created via `ensureSystemRoots`. Entity subfolders are lazy via `ensureEntityFolder` — race-safe via partial unique index `uniq_document_folders_entity` on `(port_id, entity_type, entity_id) WHERE entity_id IS NOT NULL`. System rows mutated only by entity rename/archive/hard-delete (auto-sync via service helpers); `assertNotSystemManaged` rejects direct API mutation.
|
||
- **Auto-deposit on signing completion:** `handleDocumentCompleted` resolves owner via the Owner-wins chain (`document.clientId ?? .companyId ?? .yachtId ?? interest.clientId`), ensures the entity folder, and sets `files.folder_id` + entity FK. Falls back to root when unresolvable.
|
||
- **Aggregated projection:** `listFilesAggregatedByEntity` / `listInflightWorkflowsAggregatedByEntity` walk symmetric reach (Client ↔ Company via `company_memberships` active rows, ↔ Yacht via `yachts.current_owner_type/id`), group by source (DIRECTLY ATTACHED / FROM COMPANY / FROM YACHT / FROM CLIENT), cap 20 per group. **Defense-in-depth `port_id` at every join.** **File-FK snapshot is source of truth** — historical files stay filed even if relationships change.
|
||
- Permission gating: `documents.view` reads; `documents.manage_folders` for create/rename/move/delete (system folders immutable via API).
|
||
- Deploy: migration `0051_documents_hub_split.sql` + `pnpm db:backfill:doc-folders` (idempotent via per-port advisory lock).
|
||
|
||
### Berths
|
||
|
||
- **Public API:** `/api/public/berths` (list) + `/api/public/berths/[mooringNumber]` (single) feed the marketing site. Output mirrors legacy NocoDB shape verbatim. Status precedence: `"Sold"` > `"Under Offer"` (status OR active `is_specific_interest=true` link with open outcome) > `"Available"`. Cache `s-maxage=300, stale-while-revalidate=60`.
|
||
- **Public health:** `/api/public/health` dual-mode — anonymous gets `{status, timestamp}` (never 503); requests with timing-safe `X-Intake-Secret` matching `WEBSITE_INTAKE_SECRET` get full `{checks: {db, redis}}` + 503 on failure. The website uses the authenticated form on startup so it refuses to start when pointed at the wrong env.
|
||
- **Recommender:** Pure SQL (no AI). `src/lib/services/berth-recommender.service.ts`. Tier ladder A/B/C/D from `interest_berths` aggregates. Heat scoring fires only for tier B; weights tuned via `system_settings` (`heat_weight_*`, `recommender_*`, `fallthrough_*`, `tier_ladder_hide_late_stage`). Multi-port isolation enforced at entry point AND in the SQL aggregates CTE.
|
||
- **Rules engine:** `src/lib/services/berth-rules-engine.ts`. Seven triggers, all wired: `eoi_sent`, `eoi_signed`, `deposit_received`, `contract_signed`, `interest_archived`, `interest_completed`, `berth_unlinked`. Callers fire `evaluateRule(...)` via dynamic import (circular-dep avoidance). Defaults vary; admins tune via `berth_rules` setting. Pairs with `advanceStageIfBehind` to keep pipeline stage in sync.
|
||
- **Per-berth PDFs:** Versioned via `berth_pdf_versions`; `berths.current_pdf_version_id` is current. Storage key is UUID per upload (no collisions on concurrent uploads); `pg_advisory_xact_lock` per berth_id serializes version-number allocation. 3-tier parse: AcroForm → OCR (Tesseract.js) → optional AI on low confidence. Magic-byte (`%PDF-`) check on BOTH in-server and presigned-PUT paths. Mooring mismatch → service-level `ConflictError` unless `confirmMooringMismatch: true`.
|
||
- **Brochures:** Per-port, `is_default` enforced by partial unique index `(port_id) WHERE is_default=true AND archived_at IS NULL`. Same upload flow as berth PDFs.
|
||
- **NocoDB re-import:** `pnpm tsx scripts/import-berths-from-nocodb.ts --apply --port-slug port-nimara`. Idempotent (skips rows where `updated_at > last_imported_at` unless `--force`); add `--update-snapshot` to rewrite the seed JSON. Helpers in `src/lib/services/berth-import.ts` are unit-tested.
|
||
- Plan-of-record: `docs/berth-recommender-and-pdf-plan.md`.
|
||
|
||
### Storage
|
||
|
||
- All file I/O through `getStorageBackend()` (`src/lib/storage/`). Interface: `put`, `get`, `head`, `delete`, `listByPrefix`, `presignUpload`, `presignDownload`. Selected via `system_settings.storage_backend` (`'s3' | 'filesystem'`). Switching backends = settings change + `pnpm tsx scripts/migrate-storage.ts` (round-trips every blob in `files`, `berth_pdf_versions`, `brochure_versions`, `gdpr_exports`, verifies SHA-256).
|
||
- MinIO calls wrapped in 30s `withTimeout` to prevent TCP-blackhole stalls. **Filesystem backend is single-node only** — refuses to start when `MULTI_NODE_DEPLOYMENT=true`.
|
||
|
||
### Send-from accounts (sales send-outs)
|
||
|
||
- Configurable via `system_settings`; defaults to `sales@portnimara.com` (human) + `noreply@portnimara.com` (automation). SMTP/IMAP passwords AES-256-GCM at rest; API returns only `*PassIsSet` markers.
|
||
- Audit → `document_sends` (separate from `audit_logs` for volume + binary refs). Body markdown rendered via `renderEmailBody()` (escape-then-allowlist; XSS-tested). Rate limit 50 sends/user/hour. Files > `email_attach_threshold_mb` ship as 24h signed-URL link (filename HTML-escaped against injection). The threshold banner in the compose UI is informational and shows whenever the preview API returns the per-port threshold — it does NOT depend on IMAP. Separately, bounce monitoring (`imap-bounce-poller.ts`) needs IMAP creds and no-ops cleanly when they're unset.
|
||
|
||
### Pre-commit
|
||
|
||
Husky + lint-staged runs ESLint fix + Prettier on staged `.ts`/`.tsx`. **Blocks all `.env*` files** (including `.env.example`) — pass them via a separate workflow if needed.
|
||
|
||
## Environment
|
||
|
||
Copy `.env.example` to `.env`. See `src/lib/env.ts` for the full Zod schema. `SKIP_ENV_VALIDATION=1` bypasses validation (Docker build).
|
||
|
||
Dev/test-only env (not in `.env.example`):
|
||
|
||
- `EMAIL_REDIRECT_TO=<address>` — reroutes every outbound email to this address, prefixes subject `[redirected from <original>]`. Dev safety net; **must be unset in production**.
|
||
- `IMAP_HOST` / `IMAP_PORT` / `IMAP_USER` / `IMAP_PASS` — used by `tests/e2e/realapi/portal-imap-activation.spec.ts`; the spec skips when any are missing.
|
||
|
||
## Testing
|
||
|
||
Six Playwright projects (`playwright.config.ts`):
|
||
|
||
- `setup` — global setup (seeds users, port, berths, system settings)
|
||
- `smoke` — fast click-through, run on every change (~10 min, 125 specs)
|
||
- `exhaustive` — deeper UI coverage
|
||
- `destructive` — archive/delete/cancel paths against throwaway entities
|
||
- `realapi` — opt-in real Documenso send-side + IMAP round-trip. Needs `DOCUMENSO_API_*`, `SMTP_*`, `IMAP_*` env + cloudflared tunnel running for the local webhook receiver
|
||
- `visual` — pixel-diff baselines (`tests/e2e/visual/snapshots.spec.ts-snapshots/`); regenerate with `--update-snapshots`
|
||
|
||
Vitest covers unit + integration with mocked externals (`tests/unit/`, `tests/integration/`).
|
||
|
||
## Docker
|
||
|
||
- `Dockerfile` — production multi-stage (deps → build → runner)
|
||
- `Dockerfile.dev` — dev with bind-mounted source
|
||
- `Dockerfile.worker` — BullMQ worker process
|
||
- `docker-compose.yml` / `.dev.yml` / `.prod.yml`
|
||
|
||
## Architecture docs
|
||
|
||
Numbered specs (`01-CONSOLIDATED-SYSTEM-SPEC.md` … `15-DESIGN-TOKENS.md`) in repo root carry the detailed architecture decisions, schema docs, API catalog, and sequence.
|
||
|
||
### Beta-phase tracker (read this first)
|
||
|
||
We are in pre-launch beta. **`docs/launch-readiness.md` is the canonical
|
||
home for every outstanding initiative we need to ship before
|
||
production cutover.** Read it at the start of any non-trivial task to
|
||
see what's in flight, what's blocked, and what's been deferred. Append
|
||
new launch-blocking items there (status tags: `OPEN | IN PROGRESS |
|
||
SHIPPED in <hash> | BLOCKED | DEFERRED`) — do NOT create a new
|
||
parallel audit doc. Companion files:
|
||
|
||
- `docs/launch-readiness.md` — the master pre-launch tracker (5+
|
||
initiatives: reports overhaul, marketing pipeline cutover, invoicing
|
||
audit, codebase + security audit, website integration, e2e testing,
|
||
data migration)
|
||
- `docs/reports-content-spec.md` — working spec for the reports
|
||
initiative (per-category KPIs / charts / tables); referenced by
|
||
`launch-readiness.md` Initiative 1
|
||
- `docs/superpowers/audits/active-uat.md` — live UAT findings the user
|
||
surfaces in chat; persists across sessions until explicit wrap
|
||
- `docs/BACKLOG.md` — long-tail backlog index (post-launch and
|
||
general)
|
||
|
||
### Domain reference docs
|
||
|
||
- `docs/berth-recommender-and-pdf-plan.md` — berths + PDF + send-outs bundle
|
||
- `docs/eoi-documenso-field-mapping.md` — canonical EoiContext ↔ Documenso/AcroForm mapping
|
||
- `docs/documenso-integration-audit.md` — full Documenso v1/v2 quirks reference
|
||
- `assets/README.md` — in-app EOI source PDF requirements
|