diff --git a/docs/AUDIT-FOLLOWUPS.md b/docs/AUDIT-FOLLOWUPS.md index 0ec72a6c..8aaa6a91 100644 --- a/docs/AUDIT-FOLLOWUPS.md +++ b/docs/AUDIT-FOLLOWUPS.md @@ -13,26 +13,34 @@ message order where possible. --- -## Quick status snapshot — 2026-05-09 (decisions locked) +## Quick status snapshot — 2026-05-09 (post-execution) -| Wave | Topic | Status | -| --------- | ------------------------------------------ | -------------------------------------------------------------------------- | -| 1 | Small confident fixes | ✅ Done | -| 2 | Country dropdown unification + cmdk scroll | ✅ Done (country/nationality split deferred) | -| 3 | Berth field overhaul (NocoDB enums) | ✅ Done | -| 4 | Currency platform-wide | 🟡 Ready — design locked; start with `` | -| 5 | Configurable enums (admin Vocabularies) | 🟡 Ready — `/admin/vocabularies`, admin-only | -| 6 | Notes unification (aggregate-on-read) | 🟡 Ready — extend pattern to yachts/companies/residential | -| 7 | Clients / yachts / companies misc | 🟡 Partial (status-link flow ready; client form expansion still large) | -| 8 | Expenses revisit | 🟡 Ready — combobox trip label (free text → autocomplete) | -| 9 | Interests + notifications | ✅ Done | -| 10 | Settings polish | 🟡 Ready — first/last name + collapse notif prefs | -| 11 | DEFERRED — group-discussion items | 🟡 Most items now decided; client-form expansion + reports still large | -| **Bonus** | **Public berth feed (website map)** | 🟡 Add parity fields (no Price); double-write cutover plan | +| Wave | Topic | Status | +| --------- | ------------------------------------------ | ----------------------------------------------------------------------- | +| 1 | Small confident fixes | ✅ Done | +| 2 | Country dropdown unification + cmdk scroll | ✅ Done (country/nationality split still deferred — see Wave 11.E) | +| 3 | Berth field overhaul (NocoDB enums) | ✅ Done | +| 4 | Currency platform-wide | ✅ Done | +| 5 | Configurable enums (admin Vocabularies) | ✅ Admin page + read endpoint shipped; consumer wiring is owed | +| 6 | Notes unification (aggregate-on-read) | ✅ Done — yacht / company / residential aggregators + UI | +| 7 | Clients / yachts / companies misc | ✅ Status-link flow done; client form expansion still large (Wave 11.A) | +| 8 | Expenses revisit | ✅ Done — trip-label combobox (free text + past suggestions) | +| 9 | Interests + notifications | ✅ Done | +| 10 | Settings polish | ✅ Done — first/last name + collapse notif prefs | +| 11.A | Manual client form expansion | 🔴 Not started (large) | +| 11.B | Documents folders (unlimited nesting) | 🔴 Not started — needs deep design (sidebar tree + breadcrumb) | +| 11.C | Reports system + templates | 🔴 Not started | +| 11.D | Receipts inline in expense PDF | 🔴 Not started | +| 11.E | Country / Nationality split on Client form | 🔴 Not started | +| 11.F | Inquiry triage | 🔴 Deferred | +| 11.G | Per-port email branding admin UI | 🔴 Deferred | +| **Bonus** | **Public berth feed (website map)** | ✅ Parity fields shipped; cutover deferred (see runbook) | +| **Bonus** | **Website cutover runbook** | ✅ Doc shipped (`docs/website-cutover-runbook.md`); execution deferred | +| **Bonus** | **Berth Documents tab → Spec + Deal** | ✅ Done | -Test status: `pnpm exec vitest run` → **1185/1185 pass**. +Test status: `pnpm exec vitest run` → **1187/1187 pass**. TS check: `pnpm exec tsc --noEmit` → **clean**. -Git: 23 files modified, 2 new files (no commits yet). +Git: 9 commits this session (Waves 4-10 + admin Vocabularies + status-change link + Berth Documents tab split + decisions log). --- diff --git a/docs/website-cutover-runbook.md b/docs/website-cutover-runbook.md new file mode 100644 index 00000000..cd49cc4f --- /dev/null +++ b/docs/website-cutover-runbook.md @@ -0,0 +1,124 @@ +# Website ↔ CRM cutover runbook + +This document captures the agreed plan (per the 2026-05-09 audit, Q6) for +moving the marketing website off the legacy NocoDB Berths table and onto +the CRM as the source of truth. Decision: **double-write transition +window** — both feeds stay live for ~30 days, then NocoDB is decommissioned. + +The CRM side is fully wired today. Most outstanding work lives in the +**website repo**. + +--- + +## Endpoints involved + +### Public berth feed (replaces NocoDB Berths read path) + +- `GET /api/public/berths` — list (NocoDB-verbatim shape; see + `src/lib/services/public-berths.ts`) +- `GET /api/public/berths/[mooringNumber]` — single +- Cache: `s-maxage=300, stale-while-revalidate=60` (5 min) +- Status mapping: `Sold` > `Under Offer` > `Available` + +### Public inquiry intake (replaces NocoDB inquiry write path) + +- `POST /api/public/website-inquiries` — accepts inquiry form submissions + from the marketing site +- Auth: shared secret in `X-Intake-Secret` header, compared via timing-safe + equality against `WEBSITE_INTAKE_SECRET`. Refuses every request when the + env var is unset (correct posture for dev / staging until the website is + also configured). + +### Health endpoint (monitoring contract) + +- `GET /api/public/health` — anonymous: `{status, timestamp}` (always 200, + for uptime monitors). Authenticated with `X-Intake-Secret`: full + `{status, env, appUrl, timestamp, checks: {db, redis}}` payload, returns + 503 when any dependency is down. The website calls the authenticated + variant on startup so it refuses to boot when its `CRM_PUBLIC_URL` + points at the wrong env. + +--- + +## Pre-cutover checklist (CRM side — done) + +- [x] `/api/public/berths` serves Map Data (117 rows backfilled + 2026-05-09). +- [x] PublicBerth payload exposes verbatim NocoDB fields, plus + booleans / metric variants / timestamps (commit `72ab718`). Price + intentionally omitted (decision Q4). +- [x] `/api/public/website-inquiries` POST handler exists, gated on + `WEBSITE_INTAKE_SECRET`. +- [x] `WEBSITE_INTAKE_SECRET` documented in `.env.example`. + +## Pre-cutover checklist (website repo — owed) + +- [ ] Generate a strong shared secret (`openssl rand -hex 32`) and set + `CRM_INTAKE_SECRET` (website) **and** `WEBSITE_INTAKE_SECRET` (CRM) + to the same value in production. +- [ ] Wire the website's berth-map fetch to `${CRM_PUBLIC_URL}/api/public/berths`. + Keep the existing NocoDB fetch in parallel for the transition window. +- [ ] Wire the website's inquiry submit handler to `POST` to + `${CRM_PUBLIC_URL}/api/public/website-inquiries` with the + `X-Intake-Secret` header. Keep the existing NocoDB write in parallel. +- [ ] Add a startup probe to `${CRM_PUBLIC_URL}/api/public/health` + (authenticated) so the website fails fast on misconfigured env. + +## Double-write window (target: 30 days) + +During the window: + +1. Marketing site reads from BOTH feeds for any change-detection or + reconciliation jobs (or just CRM if reads can flip atomically). +2. Marketing site writes inquiries to BOTH NocoDB and CRM. The CRM + surface is treated as authoritative for triage; NocoDB stays as a + passive backup so the rollback path is one DNS / env flip away. +3. Berth status edits made in CRM are NOT synced back to NocoDB. + NocoDB will progressively go stale — accepted because the website is + already preferring the CRM read. NocoDB stays usable as a snapshot of + pre-cutover state. +4. Daily sanity check: `curl -s ${CRM_PUBLIC_URL}/api/public/berths | jq '.pageInfo'` + — confirms the public feed still serves and the row count matches + expectations (117 berths in port-nimara). + +## Cutover steps (target: ~Day 30) + +1. Stop the NocoDB-side writes from the website (drop the dual write). +2. Stop the NocoDB-side reads from the website (CRM-only). +3. Mark the NocoDB Berths table read-only via NocoDB ACL. +4. Wait 7 days; if no one notices anything missing, drop the NocoDB + Berths table and revoke the NocoDB MCP token from `~/.claude.json`. + +## Rollback path + +The double-write design means rollback within the 30-day window is a +single env / DNS flip: + +- Website: change `CRM_PUBLIC_URL` to the old NocoDB-fronted URL OR + toggle a feature flag back to NocoDB. +- CRM: no change required — the public endpoints stay live for any + consumer that didn't roll back. + +After NocoDB is decommissioned, rollback requires restoring the table +from backup. That's the trade-off for the cleaner final state. + +--- + +## Open follow-ups + +- **Berth `archived_at`** — when retiring a berth, the public feed will + still serve it. Add a soft-delete column + filter on + `/api/public/berths` before any berth is permanently removed. (Not + blocking the cutover; flagged in the audit.) +- **CRM-edit drift vs re-imports** — `scripts/import-berths-from-nocodb.ts` + skips rows where `updated_at > last_imported_at`. After cutover the + website MUST stop writing to NocoDB; if any straggler write hits + NocoDB and someone re-runs the import script, those edits would + silently win over CRM data. Mitigation: the script is opt-in, and the + `updated_at` guard means a full re-import only overwrites when the + rep explicitly passes `--force`. Decommission the script once cutover + is irreversible. +- **5-minute cache** — `s-maxage=300` on `/api/public/berths` means a + CRM-side status flip won't show on the website for up to 5 minutes. + Acceptable for marketing; bump if marketing wants near-real-time + updates. diff --git a/src/lib/db/migrations/meta/_journal.json b/src/lib/db/migrations/meta/_journal.json index d07bd480..e8f56e4f 100644 --- a/src/lib/db/migrations/meta/_journal.json +++ b/src/lib/db/migrations/meta/_journal.json @@ -302,6 +302,6 @@ "when": 1778500000000, "tag": "0042_missing_fk_constraints", "breakpoints": true - }, + } ] -} \ No newline at end of file +}