Closes a gap exposed by the comms safety audit: the existing
EMAIL_REDIRECT_TO env var only redirected outbound SMTP via the
sendEmail() bottleneck. Two channels still leaked when set:
1. Documenso e-signature recipients — Documenso's own server emails
them on our behalf, so SMTP redirect doesn't help. We were sending
real client emails to the Documenso REST API, which would then
deliver to the real client.
2. Outbound webhooks — fire from the BullMQ worker to user-configured
URLs. SSRF guard blocks internal hosts but doesn't pause production
endpoints.
Documenso (src/lib/services/documenso-client.ts):
- createDocument: rewrite every recipient.email to EMAIL_REDIRECT_TO
and prefix the recipient.name with the original email so the doc
is traceable.
- generateDocumentFromTemplate: same treatment for both v1.13
formValues.*Email keys and v2.x recipients[]. The redirect happens
BEFORE the API call, so even Documenso's own retry logic can't
reach the original recipient.
- Both paths log when they redirect so it's visible in dev.
Webhooks (src/lib/queue/workers/webhooks.ts):
- When EMAIL_REDIRECT_TO is set, short-circuit the dispatch and write
a `dead_letter` row with reason "Skipped: EMAIL_REDIRECT_TO is set,
outbound comms paused." so the attempt is still visible in the
deliveries listing.
Doc:
docs/operations/outbound-comms-safety.md catalogs every outbound
comms channel (email, Documenso, webhooks, WhatsApp/phone deep-links,
SMS-not-implemented) and explains how each one respects the env flag.
Includes a verification checklist to run before any production data
import + cutover steps for going live.
Single env var EMAIL_REDIRECT_TO now reliably pauses ALL automated
outbound comms. Unset for production.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the audit findings from a 2026-05-03 read-only NocoDB review
plus the algorithm and migration plan for porting the legacy data
into the new client / interest / contacts / addresses model.
Highlights:
- 252 NocoDB Interests rows ≈ ~190–200 unique humans (~20–25% dup
rate). Six duplicate patterns documented from real data, including
"same person, multiple yachts" — exactly the case the new
client/interest split is designed to handle.
- Reuses the battle-tested `client-portal/server/utils/duplicate-
detection.ts` algorithm (blocking + weighted rules) with additions:
metaphone for non-English surnames, compounded confidence when
multiple rules match, negative evidence for split-signal cases.
- Three runtime surfaces (at-create suggestion, interest-level
same-berth guard, background scoring + admin review queue) plus a
one-shot migration script with --dry-run / --apply / --rollback.
- Configurable thresholds via per-port system_settings so the merge
policy can be tuned (defaults to "always confirm" — never
auto-merges out of the box).
- Reversible: every merge writes a clientMergeLog row with the
loser's full pre-state JSON, enabling 7-day undo without engineering.
Implementation decomposes into three plans (P1 library / P2 runtime /
P3 migration) sequenced after the mobile branch lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-execution baseline for the mobile foundation PR:
- Mobile audit harness (tests/e2e/audit/mobile.spec.ts + mobile-audit Playwright project) — visits every page at four anchor iPhone viewports (375/393/402/440), screenshots full-page to .audit/mobile/, generates index.md
- Design spec (docs/superpowers/specs/2026-04-29-mobile-optimization-design.md) — adaptive shell + responsive content; full active-iPhone-range coverage; foundation + per-page migration phases
- Implementation plan (docs/superpowers/plans/2026-04-29-mobile-foundation.md) — 24 TDD tasks for the foundation PR
- .gitignore: ignore /client-portal/ (legacy nested Nuxt repo) and /.audit/ (regenerable screenshots)
- Remove phantom client-portal gitlink (mode 160000 with no .gitmodules)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surveys what it actually takes to ship the AI inbox-triage feature
gated on Google Workspace integration. Walks through three deployment
models with their real costs:
- Model A (Marketplace OAuth app): 4-6 months calendar, $15k-$75k for
the required CASA security assessment, recurring re-verification
- Model B (per-customer Internal OAuth app): ~5 weeks engineering, $0
Google-side, scoped to one workspace per customer
- Model C (forward-to-CRM mailbox): ~1 week, receive-only, no reply
drafts possible
Recommends Model B for the current customer profile, with B → A
promotion only if 3+ customers ask unprompted.
Documents what's already scaffolded (email_accounts/threads/messages
tables, syncInbox stub, BullMQ email queue, ai_usage_ledger, per-port
aiEnabled flag, withRateLimit('ai')) vs what's new (OAuth flow, Pub/
Sub push receiver, gws_user_tokens + email_triage tables, /inbox UI).
End-to-end flow, schema additions, AI cost interaction with the
Phase 3b token budgets, 5-phase build plan (G1-G5), and 5 open
decisions to resolve before scheduling the build. Explicitly out of
scope: M365, sentiment analysis, smart-drafts, cross-staff triage
queue.
No code changes — this is a design doc to drive a go/no-go decision.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two new runbooks under docs/runbooks/ plus the automation scripts the
backup runbook references. Both are written so an operator who has only
the off-site backup credentials and the runbook can recover the system
unaided.
Backup/restore (Phase 4a):
- docs/runbooks/backup-and-restore.md — covers what gets backed up
(Postgres / MinIO / .env+ENCRYPTION_KEY), schedule (hourly DB +
hourly MinIO mirror, 7-day hourly + 30-day daily retention),
cold-restore procedure with row-count verification, weekly drill
- scripts/backup/pg-backup.sh — pg_dump → gzip → optional GPG → mc
upload, fails loud
- scripts/backup/minio-mirror.sh — incremental mc mirror, no --remove
flag so accidental deletes on the live bucket can't cascade
- scripts/backup/restore.sh — interactive prod restore + --drill mode
that runs against a sandbox DB and diffs row counts
Email deliverability (Phase 4b):
- docs/runbooks/email-deliverability.md — what the CRM sends, DNS
records (SPF/DKIM/DMARC/MX), per-port override implications,
diagnosis flow ("didn't arrive" → 4-step checklist starting with
EMAIL_REDIRECT_TO), provider migration plan, realapi suite as the
end-to-end probe
Tests still 778/778 vitest, tsc/lint clean — these phases are docs +
shell scripts, no code changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Multi-area cleanup pass closing partial-implementation gaps surfaced by the
post-i18n audit. No behavior changes for happy-path users; closes real
correctness/security holes.
PR1a Public yacht-interest endpoint i18n. /api/public/interests now accepts
phoneE164/phoneCountry, nationalityIso, address.{countryIso, subdivisionIso},
and company.{incorporationCountryIso, incorporationSubdivisionIso}.
Server-side parsePhone() fallback for legacy raw phone strings.
PR1b Alert rule registry trim. Two rule slots ('document.expiring_soon',
'audit.suspicious_login') were registered but evaluators returned [].
Both required schema/instrumentation that hadn't landed. Removed from
the registry; comments record the dependencies needed to revive them.
Effective rule count: 8 active.
PR1c vi.mock hoist + flake fix. Hoisted vi.mock calls to top-level in 5
integration test files; webhook-delivery uses vi.hoisted for the
queue-add ref. Vitest no longer warns about non-top-level mocks.
Deflaked the 'short value' assertion in security-encryption.test.ts
by switching plaintext from 'ab' to 'XY' (non-hex chars). 5/5 runs green.
PR1d Soft-delete reference audit. listClientOptions and listYachtsForOwner
now filter by isNull(archivedAt). Berths use status (no archivedAt).
PR1e Permission-matrix audit script + report. scripts/audit-permissions.ts
walks every src/app/api/v1/**/route.ts and reports handlers without a
withPermission() wrapper. Initial run found 33 violations.
- Allow-listed 17 with explicit reasons (self-data, admin, alerts,
search, currency, ai, custom-fields — some marked TODO).
- Wrapped 7 routes with concrete permissions: clients/options
(clients:view), berths/options (berths:view), dashboard/*
(reports:view_dashboard), analytics (reports:view_analytics).
Audit report at docs/runbooks/permission-audit.md. Script exits
non-zero on any unallow-listed violation so it can become a CI gate.
Vitest: 741 -> 741 (no new tests; existing suite covers the changes).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Full execution plan for the next phase. Closes the seven priority gaps
the 2026-04-28 Nuxt→Next audit surfaced (analytics, alerts,
interests-by-berth, expense dedup, EOI queue, OCR, audit log read view).
Scope:
- Analytics dashboard with KPI tiles, pipeline funnel, occupancy
timeline, revenue breakdown, lead-source attribution; cached via
`analytics_snapshots` recurring job.
- Alert framework: 10-rule v1 catalog, rule engine evaluates on cron,
fingerprint dedupes, auto-resolves when condition clears, surfaces
in dashboard right rail + dedicated /alerts page.
- Interests-by-berth tab on berth detail.
- Expense duplicate detection (vendor + amount + date ±3d) with
merge action.
- OCR for expense receipts via Claude Vision (Haiku 4.5 + ephemeral
system-prompt cache).
- Audit log admin read view with tsvector search + cursor pagination.
- EOI queue: saved-view tab on the documents hub.
11 PRs, ~10-13 dev days, calendar 2.5-3 weeks. Critical path
graphed. Risk register includes alert false-positive mitigation,
OCR cost ceiling via Haiku + cache, and audit-log scale.
Four open questions for the user in the spec footer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Integration tests use makePort() which writes ports with slug 'test-port-{rand}'
and never cleans up. Result: 17,564 leaked rows in dev that slowed every page
load fetching the port-switcher list (and was contributing to smoke flakes).
Adds tests/global-setup.ts with a teardown() that DELETEs every 'test-port-%'
row plus its dependent rows across 30+ tables in one CTE. Wires it into
vitest.config.ts via globalSetup. Adds closeDb() helper so the teardown can
end the postgres-js pool cleanly (kills the 'Tests closed but Vite server
won't exit' warning).
Also lands docs/superpowers/specs/2026-04-28-country-phone-timezone-design.md
— full-scope agenda for the country dropdown / E.164 phone input /
country-driven timezone autofill work, ~7 dev days across 10 PRs. Per
user request: 'let's do this full-fledged if we're gonna do it'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces yachts and companies as first-class entities with memberships,
ownership history, berth reservations, and dual-path EOI templates.
Explicit non-goals (importer, merge endpoint) carved out as Specs 2 and 3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers migrating ActivePieces inquiry flow into the CRM:
client confirmation emails, sales team notifications,
client addresses table, and admin configuration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>