Single coherent commit completing § 1.1 (hot-path correctness) plus
§ 1.1.4.5 (multi-berth EOI mooring fix). Numbers users see are now
self-consistent across dashboard / kanban / hot deals / PDF reports.
## Active-interest sweep (canonical predicate everywhere)
Routed every "active interest" filter through `activeInterestsWhere`
(commit b966d81 helper). The helper enforces port-scoping + archivedAt
IS NULL + outcome IS NULL — strict definition, won is closed.
Touched sites:
- src/lib/services/reminders.service.ts:digestPort — no longer fires
reminders for won/lost/cancelled deals
- src/lib/services/berths.service.ts:getLatestInterestStageByBerth
- src/lib/services/client-archive-dossier.service.ts (next-in-line
others lookup)
- src/lib/services/client-archive.service.ts (remaining-under-offer
recount before flipping berth back to available)
- src/lib/services/client-restore.service.ts (yacht-usage check)
- src/lib/services/interests.service.ts:listInterestsForBoard +
getInterestStageCounts + the "others on same berth" lookup —
kanban / board now exclude terminal deals
- src/lib/services/report-generators.ts: fetchPipelineData,
fetchRevenueData stage breakdowns, top-N interests
## Pipeline-value currency conversion
`getKpis()` now fetches the port's defaultCurrency from `ports` and
converts each berth's `priceCurrency`→port-default via
`currency.service`. Returns `pipelineValue` + `pipelineValueCurrency`
instead of the lying `pipelineValueUsd`. Missing rates fall through to
raw amount summing (so the tile still shows an approximate number) —
behind a follow-up to surface a "rates incomplete" indicator.
3 consumers updated: KpiCards, PipelineValueTile, ActiveDealsTile.
## Occupancy = sold only
Both the dashboard KPI tile and the revenue-report PDF occupancy data
now count only `berth.status='sold'`. `under_offer` is a hold, not
occupation. The analytics timeline switches from
`berth_reservations`-derived to a cumulative-won-deals derivation via
`interests.outcome='won' AND outcome_at::date <= day` — same source of
truth, historical shape preserved.
## Revenue PDF two-card layout
Added `totalForecast` + `pipelineWeights` to `RevenueData`. Summary
section now renders both:
- "Completed revenue (won)" — money in the bank
- "Forecast revenue (pipeline-weighted)" — expected pipeline value
Pipeline weights resolve from `system_settings.pipeline_weights`
(per-port admin override) and fall back to STAGE_WEIGHTS defaults. PDF
and dashboard forecast tiles reconcile.
## Multi-berth EOI mooring (4.5)
Documenso `Berth Number` form field now carries the formatBerthRange
output for BOTH single- and multi-berth EOIs. Single-berth output is
byte-identical to the legacy primary-only path
(`formatBerthRange(['A1']) === 'A1'`). Multi-berth EOIs now render
the full range ("A1-A3, B5") in the existing field instead of being
silently dropped against a nonexistent `Berth Range` field.
Dropped:
- `'Berth Range'` from the Documenso formValues payload + TS type
- `setBerthRange()` helper from fill-eoi-form.ts (now redundant)
- The "missing Berth Range AcroForm field" warning log
Updated CLAUDE.md to reflect — no Documenso admin template change
needed.
## Tests
- Updated `documenso-payload.test.ts` — new fixture asserts
formatBerthRange output flows into Berth Number; multi-berth case
added.
- Updated `analytics-service.test.ts:computeOccupancyTimeline` —
fixture creates a won interest instead of a reservation.
- Updated `alerts-engine.test.ts:interest.stale` — fixture stage
switched from dead `'in_communication'` to canonical `'qualified'`.
- Updated `report-templates.test.tsx:revenue` — fixture carries
`totalForecast` + `pipelineWeights` to match new RevenueData.
1373/1373 vitest pass. tsc + eslint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swap the one outlier (client-interests-tab.tsx) from Vaul Drawer to
Sheet side=right so every detail-preview surface uses the same
primitive. Document the doctrine: Sheet for side panels on both desktop
and mobile; Vaul Drawer reserved for mobile-only bottom-sheet UX
(currently just MoreSheet).
Closes ui/ux M11.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Documents the new document_folders self-FK tree, the sibling-name
uniqueness invariant, and the soft-rescue delete behaviour so future
sessions don't try to wire CASCADE.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The route is dual-mode — anonymous probes get a minimal `{status,
timestamp}` (so uptime monitors that can't carry the secret keep
working and never 503 on the platform), authenticated probes
(timing-safe X-Intake-Secret match against WEBSITE_INTAKE_SECRET) get
the full `{status, env, appUrl, timestamp, checks}` envelope and a
503 on hard dependency failures. Old doc only described the second
shape and didn't mention the secret gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wave through the remaining audit-final-deferred items that aren't blocked
on the back-burnered Documenso work.
Multi-tenant isolation:
- Storage proxy ProxyTokenPayload gains optional `p` (port slug) claim;
verifier asserts `key.startsWith(${p}/)`. Defense-in-depth against a
buggy issuer in some future code path that mixes port scopes — every
storage key generated by generateStorageKey() already prefixes the
slug. document-sends opts in for 24h emailed download links; other
callers continue working unchanged via the optional field.
DB schema reconciliation:
- Migration 0047 rebuilds system_settings unique index with NULLS NOT
DISTINCT (Postgres 15+) so global settings (port_id IS NULL) are
uniquely keyed by `key` alone. Surfaced + dedupe'd 65 duplicate
(storage_backend, NULL) rows that had accumulated from race-prone
delete-then-insert patterns in ocr-config / settings / residential-
stages / ai-budget services. All four services converted to true
onConflictDoUpdate upserts so the race window is closed.
API uniformity:
- Response shape standardization: 16 routes converted from
`{ success: true }` to 204 No Content. CLAUDE.md documents the
convention (`{ data: <T> }` for content, 204 for empty mutations,
portal-auth retains `{ success: true }` for the frontend's auth chain).
- req.json() → parseBody() migration across 9 admin/CRM routes
(custom-fields, expenses/export ×3, currency convert,
search/recently-viewed, admin/duplicates, berths/pdf-{upload-url,
versions, parse-results}). Uniform 400 error shapes for
ZodError-flagged bodies.
Custom-fields merge tokens (shipped end-to-end):
- merge-fields.ts gains CUSTOM_MERGE_TOKEN_RE + helpers for the
`{{custom.<fieldName>}}` shape.
- document-templates validator accepts the dynamic shape alongside
the static catalog tokens.
- document-sends.service mergeCustomFieldValues resolver fetches
per-port custom_field_definitions for client/interest/berth contexts
and substitutes stored values keyed by `{{custom.fieldName}}`.
- custom-fields-manager amber banner updated to reflect that merge
tokens now expand (search index + entity-diff remain documented
design limitations).
/api/v1/files cross-entity filtering:
- Validator + listFiles + uploadFile accept companyId AND yachtId
alongside clientId. file-upload-zone propagates both.
- New CompanyFilesTab component mirrors ClientFilesTab; restored as a
visible Documents tab in company-tabs.tsx (was a hidden stub).
Inline TODOs:
- Reviewed remaining two TODOs (per-user reminder schedule, import
worker handlers). Both are placeholders for future feature surfaces,
not bugs — per-port digest works for every customer; nothing
currently enqueues import jobs (verified). Annotated in BACKLOG.
BACKLOG.md updated to reflect what landed and what's still pending
(Documenso-related items still bundled with the back-burnered phases).
Tests: 1185/1185 vitest, tsc clean.
Phase 8: capture the new conventions established by the 19-commit
berth-recommender refactor so future Claude sessions don't re-litigate
the design decisions.
Added to the Conventions section:
- Multi-berth interest model + interest_berths role flags
- Mooring number canonical format
- Public berths API + health env-match
- Berth recommender (pure SQL, no AI; tier ladder; heat tunables)
- EOI bundle range formatter
- Pluggable storage backend (filesystem single-node-only constraint)
- Per-berth PDFs (UUID storage keys + advisory lock + 3-tier parser)
- Brochures (default-uniqueness via partial unique index)
- Send-from accounts (encrypted creds, *PassIsSet boolean, XSS guard,
size-threshold link fallback, 50/hour rate limit)
- NocoDB berth import script
Updated Architecture docs section to note:
- The Documenso template needs the new "Berth Range" field added.
- Pointer to the comprehensive plan doc.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Quick reference: add commands for every Playwright project + dev tsx helpers
- Conventions: document the Documenso webhook auth pattern (X-Documenso-Secret
plaintext, not HMAC), the v1.13/2.x response shape normalization layer,
the email template module location + responsive table layout, and the
PortalAuthShell pattern that unifies the in-app and email branding
- Environment: document EMAIL_REDIRECT_TO and IMAP_* dev/test-only vars
- New Testing section enumerating the five Playwright projects (setup,
smoke, exhaustive, destructive, realapi, visual) and what each covers
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR 15 (docs): the numbered spec files mostly described the new model
already at the conceptual level, but two needed concrete updates:
- 07-DATABASE-SCHEMA.md: schema overview now lists the new Yacht /
Company / Reservation domains alongside the existing ones, names the
partial unique indexes (idx_yoh_active, idx_br_active) that enforce
exclusivity, and notes that yacht/company details are no longer
stored on `clients`.
- CLAUDE.md: the Conventions section now points future contributors at
the new schema files, the polymorphic ownership pattern, the
EoiContext/dual-path EOI flow, and the merge-token allow-list. Adds
a pointer to the husky `.env*` block so it doesn't trip people up.
References the new field-mapping doc and `assets/README.md`.
Task 15.3 (Tier 4 golden-image PDF regression) is deferred — those
tests need committed reference PDFs that come out of a real, manually
verified EOI render. Best landed once the actual `assets/eoi-template.pdf`
is in place; tracking as a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add SKIP_ENV_VALIDATION to bypass Zod env check during next build
- Bundle custom server.ts with esbuild so production uses Socket.io
- Create worker entry point (src/worker.ts) with all BullMQ workers
- Add esbuild build scripts for server and worker bundles
- Fix Dockerfile.worker to include its own build stage
- Fix pre-commit hook to work without global pnpm
- Add CLAUDE.md with project conventions and quick reference
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>