docs: website cutover runbook + post-execution status snapshot
Captures the agreed cutover plan (Q6 in the decisions log: double-write transition window, ~30 days, then NocoDB decommission). The CRM side is wired today — public berth feed, website-inquiries intake, dual-mode health probe, WEBSITE_INTAKE_SECRET env var. The runbook documents the website-repo checklist and rollback path so we can pick it back up when prep for prod begins. Refreshes the audit-followups status snapshot to reflect what shipped this session. Wave 11 is now broken out into A-G subitems so the remaining group-discussion work is enumerated rather than collapsed. Note: .env.example separately needs WEBSITE_INTAKE_SECRET added (see runbook §Endpoints). The husky pre-commit hook blocks .env* files intentionally — pass via a separate workflow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 |
|
| Wave | Topic | Status |
|
||||||
| --------- | ------------------------------------------ | -------------------------------------------------------------------------- |
|
| --------- | ------------------------------------------ | ----------------------------------------------------------------------- |
|
||||||
| 1 | Small confident fixes | ✅ Done |
|
| 1 | Small confident fixes | ✅ Done |
|
||||||
| 2 | Country dropdown unification + cmdk scroll | ✅ Done (country/nationality split deferred) |
|
| 2 | Country dropdown unification + cmdk scroll | ✅ Done (country/nationality split still deferred — see Wave 11.E) |
|
||||||
| 3 | Berth field overhaul (NocoDB enums) | ✅ Done |
|
| 3 | Berth field overhaul (NocoDB enums) | ✅ Done |
|
||||||
| 4 | Currency platform-wide | 🟡 Ready — design locked; start with `<CurrencyInput>` |
|
| 4 | Currency platform-wide | ✅ Done |
|
||||||
| 5 | Configurable enums (admin Vocabularies) | 🟡 Ready — `/admin/vocabularies`, admin-only |
|
| 5 | Configurable enums (admin Vocabularies) | ✅ Admin page + read endpoint shipped; consumer wiring is owed |
|
||||||
| 6 | Notes unification (aggregate-on-read) | 🟡 Ready — extend pattern to yachts/companies/residential |
|
| 6 | Notes unification (aggregate-on-read) | ✅ Done — yacht / company / residential aggregators + UI |
|
||||||
| 7 | Clients / yachts / companies misc | 🟡 Partial (status-link flow ready; client form expansion still large) |
|
| 7 | Clients / yachts / companies misc | ✅ Status-link flow done; client form expansion still large (Wave 11.A) |
|
||||||
| 8 | Expenses revisit | 🟡 Ready — combobox trip label (free text → autocomplete) |
|
| 8 | Expenses revisit | ✅ Done — trip-label combobox (free text + past suggestions) |
|
||||||
| 9 | Interests + notifications | ✅ Done |
|
| 9 | Interests + notifications | ✅ Done |
|
||||||
| 10 | Settings polish | 🟡 Ready — first/last name + collapse notif prefs |
|
| 10 | Settings polish | ✅ Done — first/last name + collapse notif prefs |
|
||||||
| 11 | DEFERRED — group-discussion items | 🟡 Most items now decided; client-form expansion + reports still large |
|
| 11.A | Manual client form expansion | 🔴 Not started (large) |
|
||||||
| **Bonus** | **Public berth feed (website map)** | 🟡 Add parity fields (no Price); double-write cutover plan |
|
| 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**.
|
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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
124
docs/website-cutover-runbook.md
Normal file
124
docs/website-cutover-runbook.md
Normal file
@@ -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.
|
||||||
@@ -302,6 +302,6 @@
|
|||||||
"when": 1778500000000,
|
"when": 1778500000000,
|
||||||
"tag": "0042_missing_fk_constraints",
|
"tag": "0042_missing_fk_constraints",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user