docs(audit): comprehensive 320+ check catalog organized by area
All checks were successful
Build & Push Docker Images / lint (push) Successful in 2m6s
Build & Push Docker Images / build-and-push (push) Successful in 22s

Companion to the 2026-05-15 sweep findings. Catalogues every audit-worthy
surface across 19 areas:

  0. Already-known issues (A1-A20 cross-reference)
  1. Legacy stage enum bleed (the deposit_10pct class) — 20 checks
  2. Routes / page reachability — 30 checks
  3. UX consistency (forms, lists, tables, badges, modals, mobile) — 100 checks
  4. Sales workflows happy + edge cases — 52 checks
  5. Admin workflows — 60 checks
  6. Multi-tenancy port isolation — 11 checks
  7. Security — 30 checks
  8. Realtime / sockets — 9 checks
  9. Performance — 14 checks
  10. Documents / files — 22 checks
  11. Audit log surface — 14 checks
  12. Email / SMTP / IMAP — 19 checks
  13. Integrations (Documenso, NocoDB, S3, AI, BullMQ) — 29 checks
  14. Schema / migration — 15 checks
  15. i18n / l10n — 8 checks
  16. Browser / device — 7 checks
  17. Specific behavioral correctness (legacy stage drift, A1 hard-delete fallout, etc) — 22 checks
  18. Data clean-up jobs — 5 checks
  19. CI / dev experience — 13 checks

Each check tagged with effort (XS/S/M/L), severity (🔴/🟠/🟡/🟢), and
current coverage (/⚠️//). Recommended priority tiering at the bottom.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-15 00:54:08 +02:00
parent ff5e71092e
commit 3b3ac287e0

733
docs/AUDIT-CATALOG.md Normal file
View File

@@ -0,0 +1,733 @@
# Comprehensive Audit Catalog — 2026-05-15
Every audit-worthy surface in Port Nimara CRM, organized by area. Each entry is a discrete check we _could_ run. Pick the subset you want to actually execute.
**Legend:**
- **Effort:** XS (~minutes) · S (~30 min) · M (~half day) · L (~1+ day)
- **Severity if broken:** 🔴 critical · 🟠 high · 🟡 medium · 🟢 cosmetic
- **Coverage today:** ✅ confirmed working · ⚠️ partially checked · ❓ unchecked · ❌ known broken (see prior audits)
---
## 0. Already-known issues (cross-reference)
These were caught in the 2026-05-15 sweep (`docs/audit-2026-05-15.md`) but listed here so we don't re-discover them:
| ID | Issue | Status |
| ----- | -------------------------------------------------------------------------- | --------------------- |
| A1 | Dashboard activity feed surfaces raw `permission_denied` rows, no label | ❌ unfixed |
| A2 | Activity feed renders legacy 9-stage enum values (`deposit_10pct` etc.) | ❌ unfixed |
| A3 | react-grab CSP error spam in dev | ❌ unfixed (dev only) |
| A4 | New Client form silently rejects when contact row has empty value | ❌ unfixed |
| A5 | Socket.IO WebSocket never connects in `pnpm dev` | ❌ unfixed |
| A6 | Some DialogContent missing `aria-describedby` | ❌ unfixed |
| A8 | Legacy `statusOverrideMode = "auto"` values still in DB | ❌ unfixed |
| A9 | Catch-up wizard defaults to "New Enquiry" instead of "EOI" for under_offer | ❌ unfixed |
| A16 | File upload at documents-hub root fails with null vs string validator | ❌ unfixed |
| A17 | `/api/v1/admin/ports` is super-admin-only but used as bootstrap resolver | ❌ unfixed |
| A18 | 404 vs 403 inconsistency on permission denials | ❌ unfixed |
| A19 | F27 same-stage PATCH returns 200 + body instead of 204 | ❌ unfixed |
| A20 | OwnerPicker Client/Company toggle hidden until popover opens | ❌ unfixed |
| A19_b | Portal `/portal/login` shows "unavailable" — scope undefined | ❌ unfixed |
---
## 1. Legacy stage enum bleed (the `deposit_10pct` class of bug)
**Why this matters:** the pipeline was refactored 9 stages → 7 stages but historical data still carries the old enum values in audit logs, soft-deleted rows, and possibly some hard-coded UI lookups. Every place that renders a stage value should map legacy → modern.
| ID | Check | Effort | Severity | Coverage |
| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -------- | -------- |
| L-001 | Grep entire `src/` for hard-coded references to legacy stage names: `details_sent`, `in_communication`, `eoi_sent`, `eoi_signed`, `deposit_10pct`, `contract_sent`, `contract_signed`, `completed` (as stage) | S | 🟠 | ❓ |
| L-002 | Audit log diff display: does old `pipelineStage` value get human-friendly mapping? | S | 🟡 | ❌ (A2) |
| L-003 | Activity feed labels: same mapping needed | S | 🟡 | ❌ (A2) |
| L-004 | Email templates: any merge token surfacing raw stage values? | XS | 🟡 | ❓ |
| L-005 | Documenso payload (`buildDocumensoPayload`): any stage references? | XS | 🟠 | ❓ |
| L-006 | Public berths API: is `status` filter accepting any legacy values? | XS | 🟡 | ❓ |
| L-007 | Webhook payloads: do outbound `interest.updated` events use 7-stage or legacy? | S | 🟠 | ❓ |
| L-008 | Reports / analytics SQL: are funnel rollups using 7-stage enum exclusively? | M | 🟠 | ❓ |
| L-009 | Search FTS indexes: do they include the mapped human stage or the raw enum? | S | 🟡 | ❓ |
| L-010 | Notification copy: does "Stage moved to X" use the mapped label? | XS | 🟢 | ❓ |
| L-011 | CSV import templates / column mappers: does anyone still accept legacy stage names? | XS | 🟢 | ❓ |
| L-012 | Seed data: confirm no legacy stages in current seed (was migrated in `seed-synthetic-data.ts`) | XS | 🟢 | ✅ |
| L-013 | Migration safety: would a re-import via NocoDB re-introduce legacy values? | S | 🟠 | ❓ |
| L-014 | Status override mode: legacy `"auto"` value (see A8) — same class of bug | XS | 🟢 | ❌ (A8) |
| L-015 | Outcome enum: confirm `won` / `lost_*` are the only modern values; no legacy `completed` outcome anywhere | S | 🟡 | ❓ |
| L-016 | Lead category enum: any legacy values? | XS | 🟢 | ❓ |
| L-017 | Lead source enum: ditto | XS | 🟢 | ❓ |
| L-018 | Tenure type enum: ditto | XS | 🟢 | ❓ |
| L-019 | Document doc-status sub-states: `sent`, `signed`, `completed`, `expired`, `rejected` — are they consistently applied? | S | 🟡 | ❓ |
| L-020 | Reservation/contract status enum: any legacy / deprecated values lingering? | S | 🟡 | ❓ |
---
## 2. Routes — every page reachable and correct
| ID | Check | Effort | Severity | Coverage |
| ----- | ----------------------------------------------------------------------------------------------------------- | ------ | -------- | ------------------- |
| R-001 | All `/[portSlug]/*` routes return 200 for super-admin (sweep) | S | 🟠 | ⚠️ admin only |
| R-002 | All `/[portSlug]/*` routes return 200 or proper 403/redirect for sales-agent | S | 🟠 | ⚠️ partial |
| R-003 | All `/[portSlug]/*` routes for viewer | S | 🟡 | ❓ |
| R-004 | Cross-port URL access: paste `/port-amador/clients/<port-nimara-uuid>` → expects 404, not silent | XS | 🟠 | ✅ (F17) |
| R-005 | Archived entity detail page: 404 with "Restored?" affordance | XS | 🟡 | ❓ |
| R-006 | Soft-deleted folder URL: expects 404 / fallback to parent | XS | 🟡 | ❓ |
| R-007 | Hard-deleted berth UUID URL (e.g. A1 in port-amador): expects 404 | XS | 🟡 | ❓ |
| R-008 | URL-encoded mooring number (`A1` vs `A%201` vs `a1`): canonicalization | XS | 🟡 | ❓ |
| R-009 | Trailing slash redirects | XS | 🟢 | ❓ |
| R-010 | Query-string preservation across nav (filters, sort, page) | S | 🟡 | ❓ |
| R-011 | Browser back/forward state on detail pages (does Tab selection persist?) | S | 🟡 | ❓ |
| R-012 | Deep-link with `?folder=<id>` on documents (F25 verified for root, what about deep folder?) | XS | 🟢 | ⚠️ |
| R-013 | Deep-link to specific interest tab (`?tab=documents`) | XS | 🟢 | ❓ |
| R-014 | Deep-link with filter pre-applied (`/interests?stage=eoi`) | XS | 🟡 | ❓ |
| R-015 | typedRoutes enforcement: any string-as-route escapes via `as never` casts that point to non-existent paths? | M | 🟡 | ❓ |
| R-016 | Middleware / proxy.ts: public-path allow-list correctness (regex anchors, prefix matches) | S | 🟠 | ❓ |
| R-017 | Auth redirect: visiting `/dashboard` while logged-out → `/login?next=...` | XS | 🟠 | ❓ |
| R-018 | Post-login redirect honours `next` param | XS | 🟠 | ❓ |
| R-019 | Portal routes when `client_portal_enabled=false`: gate page (verified A19_b) | XS | 🟢 | ✅ |
| R-020 | Portal routes when `client_portal_enabled=true`: dashboard, docs, activate flows | S | 🟠 | ❓ |
| R-021 | `/setup` bootstrap flow on fresh DB (no super admin yet) | M | 🔴 | ❓ (F1 fixed proxy) |
| R-022 | Reset-password token validity + expiry | S | 🟠 | ❓ |
| R-023 | Set-password (first-time after invite) flow | S | 🟠 | ❓ |
| R-024 | Portal activate via `#token` fragment | S | 🟠 | ❓ |
| R-025 | API routes that should be HEAD-cacheable (public/berths) return correct cache headers | S | 🟢 | ❓ |
| R-026 | Public health: anonymous mode minimal payload | XS | 🟡 | ❓ |
| R-027 | Public health: secret mode full payload | XS | 🟡 | ❓ |
| R-028 | OPTIONS preflight on API routes (CORS) | XS | 🟡 | ❓ |
| R-029 | API rate-limit headers on auth endpoints | XS | 🟡 | ❓ |
| R-030 | `/api/v1/me` returns expected user shape | XS | 🟢 | ✅ |
---
## 3. UX consistency — every list, detail, form
### 3a. Empty / loading / error states
| ID | Surface | Effort | Severity | Coverage |
| ----- | ------------------------------------------------------------------------------------------------------------------------- | ------ | -------- | -------- |
| U-001 | Clients list: empty state copy + CTA | XS | 🟢 | ❓ |
| U-002 | Yachts list: empty state | XS | 🟢 | ❓ |
| U-003 | Companies list: empty state | XS | 🟢 | ❓ |
| U-004 | Interests list: empty state | XS | 🟢 | ❓ |
| U-005 | Berths list: empty state | XS | 🟢 | ❓ |
| U-006 | Reservations list: empty state | XS | 🟢 | ❓ |
| U-007 | Invoices list: empty state | XS | 🟢 | ❓ |
| U-008 | Inbox: empty state | XS | 🟢 | ❓ |
| U-009 | Documents hub root: empty state | XS | 🟢 | ❓ |
| U-010 | Documents hub folder: empty state (verified earlier) | XS | 🟢 | ✅ |
| U-011 | Audit log: empty state (filter to nothing) | XS | 🟢 | ❓ |
| U-012 | Reconcile berths: empty state (verified) | XS | 🟢 | ✅ |
| U-013 | Recommender: empty result copy (verified F28) | XS | 🟢 | ✅ |
| U-014 | All list pages: loading skeleton vs spinner — is the pattern consistent? | S | 🟢 | ❓ |
| U-015 | All detail pages: 404 fallback (DetailNotFound) — confirmed for 5 entities, check residential/reservation/invoice/expense | S | 🟡 | ⚠️ |
| U-016 | All forms: server-error toast surfaces requestId | S | 🟡 | ❓ |
| U-017 | All forms: validation summary at top vs inline messages | S | 🟡 | ❓ |
| U-018 | All forms: submit-while-pending state (button disabled + spinner) | S | 🟢 | ❓ |
| U-019 | Drag-drop file zone: hover state visible | XS | 🟢 | ❓ |
| U-020 | Drag-drop file zone: drop-target overlay on entity folder | XS | 🟢 | ❓ |
### 3b. Form design
| ID | Check | Effort | Severity | Coverage |
| ----- | --------------------------------------------------------------------- | ------ | -------- | -------- |
| U-021 | Required-field markers consistent ("\*" vs label suffix vs help text) | S | 🟢 | ❓ |
| U-022 | Field-help-text discoverability (tooltip vs always-visible) | S | 🟢 | ❓ |
| U-023 | Field-level errors: every field has visible error after blur+submit | M | 🟡 | ❓ |
| U-024 | Cancel behaviour: discards or saves draft? | S | 🟡 | ❓ |
| U-025 | Unsaved changes warning on dialog dismiss | S | 🟡 | ❓ |
| U-026 | Multi-step wizards: persist state across step nav | M | 🟡 | ❓ |
| U-027 | Phone E.164 conversion preview | S | 🟢 | ❓ |
| U-028 | Currency input: locale-aware separators | S | 🟡 | ❓ |
| U-029 | Date picker: keyboard input + calendar both work | S | 🟢 | ❓ |
| U-030 | Date range constraint enforcement (start ≤ end) | XS | 🟡 | ❓ |
| U-031 | File-type accept attribute matches server magic-byte check | XS | 🟡 | ❓ |
| U-032 | File-size limit copy matches server limit | XS | 🟢 | ❓ |
| U-033 | Combobox keyboard nav (↑↓, Enter, Esc, type-ahead) | S | 🟢 | ❓ |
| U-034 | Multi-select chip removal (X button + backspace) | S | 🟢 | ❓ |
| U-035 | Tag colour-picker: contrast check | XS | 🟢 | ❓ |
| U-036 | "Save changes" copy consistency (vs "Update" vs "Save") | S | 🟢 | ❓ |
| U-037 | Inline-edit save trigger (blur vs Enter vs explicit save) | S | 🟢 | ❓ |
| U-038 | Inline-edit cancel (Esc reverts) | XS | 🟢 | ❓ |
| U-039 | Inline-tag-editor: tab order across the chip strip | XS | 🟢 | ❓ |
### 3c. Tables / lists / filters
| ID | Check | Effort | Severity | Coverage |
| ----- | ------------------------------------------------------- | ------ | -------- | -------- |
| U-040 | Sort direction indicator on column header | XS | 🟢 | ❓ |
| U-041 | Multi-column sort (shift-click) | S | 🟢 | ❓ |
| U-042 | Filter chips dismissable via X | XS | 🟢 | ❓ |
| U-043 | "Clear all filters" button presence | XS | 🟢 | ❓ |
| U-044 | Pagination: page size selector | XS | 🟢 | ❓ |
| U-045 | Pagination: jump-to-page | XS | 🟢 | ❓ |
| U-046 | Pagination: total count accuracy with filters | XS | 🟡 | ❓ |
| U-047 | Row selection: select-all-page vs select-all-filtered | S | 🟡 | ❓ |
| U-048 | Bulk action toolbar appearance + dismiss | S | 🟢 | ❓ |
| U-049 | Sticky header on scroll | XS | 🟢 | ❓ |
| U-050 | Column resize / reorder / show-hide persistence | S | 🟢 | ❓ |
| U-051 | Virtual list performance with 1000+ rows | M | 🟡 | ❓ |
| U-052 | CSV export of current view (respects filters + columns) | S | 🟡 | ❓ |
| U-053 | Sorted-by-relevance vs sorted-by-date default | XS | 🟢 | ❓ |
### 3d. Badges, icons, colours
| ID | Check | Effort | Severity | Coverage |
| ----- | ----------------------------------------------------------------------------- | ------ | -------- | -------- |
| U-054 | Stage badge palette: 7 stages each have a distinct, consistent colour | XS | 🟢 | ❓ |
| U-055 | Outcome badge: won = green, lost\_\* = red shades, distinct enough | XS | 🟢 | ❓ |
| U-056 | Berth status pill: available/under_offer/sold colour consistency | XS | 🟢 | ✅ |
| U-057 | Document status pill: draft/sent/partial/completed/expired/cancelled/rejected | XS | 🟢 | ❓ |
| U-058 | "Manual" chip on berth list (F67 phase 2) | XS | 🟢 | ✅ |
| U-059 | Icon usage: Lucide-only — no decorative unicode glyphs (memory: avoid emoji) | S | 🟡 | ⚠️ |
| U-060 | Button hierarchy: primary/secondary/ghost/destructive used consistently | S | 🟢 | ❓ |
| U-061 | Destructive actions colour-coded red | XS | 🟡 | ❓ |
| U-062 | Loading spinner sizing consistent (size-3.5 vs size-4 vs animate-spin) | S | 🟢 | ❓ |
| U-063 | Tooltip delay + position consistency | S | 🟢 | ❓ |
| U-064 | Status pill withDot vs no dot: is the rule consistent? | XS | 🟢 | ❓ |
### 3e. Modal / sheet / drawer doctrine
| ID | Check | Effort | Severity | Coverage |
| ----- | ------------------------------------------------------------------------------ | ------ | -------- | -------- |
| U-065 | Sheet used for forms + previews on desktop AND mobile (per CLAUDE.md doctrine) | S | 🟡 | ❓ |
| U-066 | Vaul Drawer only used for mobile-bottom-sheet (only `MoreSheet` qualifies) | XS | 🟢 | ❓ |
| U-067 | AlertDialog used for destructive confirmations | XS | 🟢 | ❓ |
| U-068 | Dialog used for short interactive forms (new yacht, catch-up, won-dialog) | XS | 🟢 | ❓ |
| U-069 | Esc closes all overlays consistently | XS | 🟢 | ❓ |
| U-070 | Click-outside closes / doesn't close: rule consistent | S | 🟡 | ❓ |
| U-071 | Focus trap inside overlays | S | 🟠 | ❓ |
| U-072 | Focus restoration to trigger element on close | S | 🟡 | ❓ |
### 3f. Toasts / feedback
| ID | Check | Effort | Severity | Coverage |
| ----- | -------------------------------------------------------------------------- | ------ | -------- | -------- |
| U-073 | Toast position consistent (top-right, sonner config) | XS | 🟢 | ✅ |
| U-074 | Success toast on every mutation (create, update, archive, delete, restore) | M | 🟡 | ⚠️ |
| U-075 | Error toast includes copyable requestId | S | 🟡 | ⚠️ |
| U-076 | Toast timing (auto-dismiss vs persistent for errors) | XS | 🟢 | ❓ |
| U-077 | Multiple toasts stack vs replace | XS | 🟢 | ❓ |
### 3g. Accessibility / keyboard
| ID | Check | Effort | Severity | Coverage |
| ----- | ---------------------------------------------------------- | ------ | -------- | -------- |
| U-078 | Tab order natural on each form | M | 🟡 | ❓ |
| U-079 | All icons inside buttons have `aria-label` or sibling text | S | 🟡 | ❓ |
| U-080 | All `<img>` have alt | XS | 🟡 | ❓ |
| U-081 | Heading hierarchy (h1 → h2 → h3, no skips) | S | 🟢 | ❓ |
| U-082 | Color contrast WCAG AA (4.5:1 body, 3:1 large) | M | 🟡 | ❓ |
| U-083 | Focus rings visible on all interactive elements | S | 🟡 | ❓ |
| U-084 | Skip-to-content link | XS | 🟢 | ❓ |
| U-085 | Reduced-motion media query honoured | S | 🟢 | ❓ |
| U-086 | `aria-describedby` set on DialogContent (A6) | S | 🟡 | ❌ |
| U-087 | Live regions for async updates (toast, notification count) | S | 🟢 | ❓ |
| U-088 | Form errors announced to screen readers | S | 🟡 | ❓ |
| U-089 | Touch target min 44×44px on mobile | S | 🟡 | ❓ |
### 3h. Mobile-specific UX
| ID | Check | Effort | Severity | Coverage |
| ----- | ----------------------------------------------------------------- | ------ | -------- | -------- |
| U-090 | Bottom-tab nav reachable on every page | XS | 🟢 | ✅ |
| U-091 | Mobile topbar shows correct title via `useMobileChrome` | S | 🟢 | ⚠️ |
| U-092 | More sheet contains every nav item not on bottom bar | XS | 🟡 | ❓ |
| U-093 | Search overlay covers viewport on tap | XS | 🟢 | ❓ |
| U-094 | iOS safe-area-inset-top / bottom respected | S | 🟡 | ❓ |
| U-095 | Pull-to-refresh: present or absent? (consistency) | XS | 🟢 | ❓ |
| U-096 | Camera capture on file upload (image\* mime type triggers camera) | S | 🟢 | ❓ |
| U-097 | Soft keyboard occlusion on form input (visualViewport handling) | S | 🟡 | ❓ |
| U-098 | Long-press menu absence (not native iOS overrides) | XS | 🟢 | ❓ |
| U-099 | Sheet side="right" responsiveness | XS | 🟢 | ❓ |
| U-100 | Mobile bottom tab active-state highlight | XS | 🟢 | ❓ |
---
## 4. Sales workflows — every end-to-end path
### 4a. Happy paths
| ID | Flow | Effort | Severity | Coverage |
| ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -------- | -------- |
| W-001 | Create client → create interest → link yacht → advance to EOI → send EOI → receive webhook → auto-advance to Reservation → record deposit → auto-advance to Deposit Paid → send contract → mark contract signed → mark won | L | 🔴 | ⚠️ |
| W-002 | Multi-berth interest: link 3 berths, mark one primary, send EOI bundle with range formatter | M | 🟠 | ❓ |
| W-003 | Company-owned yacht: company → membership → yacht owned by company → interest | M | 🟠 | ❓ |
| W-004 | Residential client + residential interest end-to-end | M | 🟡 | ❓ |
| W-005 | Public berth inquiry → admin/inquiries triage → create client via prefill | M | 🟠 | ❓ |
| W-006 | Catch-up wizard from berth list row-menu | S | 🟠 | ⚠️ |
| W-007 | Catch-up wizard from reconcile queue (verified) | S | 🟢 | ✅ |
| W-008 | Mark won → reopen → outcome cleared toast (F26) | XS | 🟢 | ⚠️ |
| W-009 | Mark lost (each lost reason) | S | 🟢 | ❓ |
| W-010 | Mark externally signed | S | 🟡 | ❓ |
### 4b. Edge cases
| ID | Flow | Effort | Severity | Coverage |
| ----- | ---------------------------------------------------------------------- | ------ | -------- | --------- |
| W-011 | Try to leave Enquiry without yacht → F23 inline prereq picker fires | XS | 🟢 | ✅ |
| W-012 | Try forbidden transition (e.g. Reservation → Enquiry) without override | XS | 🟡 | ❓ |
| W-013 | Override transition: requires reason ≥ 5 chars | XS | 🟡 | ❓ |
| W-014 | Override transition: insufficient permission → blocked tooltip | XS | 🟡 | ❓ |
| W-015 | Rewind to enquiry with linked berths → unlink-or-keep prompt | S | 🟡 | ❓ |
| W-016 | Same-stage write (F27): expects 204 | XS | 🟢 | ❌ (A19) |
| W-017 | Concurrent stage edits (two browser tabs) | M | 🟡 | ❓ |
| W-018 | Stage transition emits audit log + realtime event | S | 🟡 | ❓ |
| W-019 | Auto-advance via berth-rule on `deposit_received` | S | 🟠 | ❓ |
| W-020 | Auto-advance via Documenso webhook (`DOCUMENT_SIGNED`) | S | 🟠 | ❓ |
| W-021 | Webhook arrives twice (idempotency) | S | 🟠 | ✅ (R2-G) |
| W-022 | Webhook with v2 envelope shape | S | 🟠 | ❓ |
| W-023 | Webhook lowercase-dotted event name → forward-compat | XS | 🟢 | ❓ |
| W-024 | Webhook with wrong secret → 401 + rate limit | S | 🟠 | ❓ |
| W-025 | Berth unlink mid-EOI → rule fires? | S | 🟡 | ❓ |
| W-026 | Yacht reassignment mid-deal | S | 🟡 | ❓ |
| W-027 | Client merge (duplicate dedup) — interest carry-over | M | 🟠 | ❓ |
| W-028 | Recommender on 0ft yacht (empty dims) | XS | 🟢 | ❓ |
| W-029 | Recommender on 300ft yacht (no matching berth) | XS | 🟢 | ✅ (F28) |
| W-030 | Recommender weight tuning re-ranks | S | 🟡 | ❓ |
| W-031 | Recommender fallthrough policy (cooldown after lost) | M | 🟡 | ❓ |
| W-032 | Recommender tier ladder A/B/C/D classification | M | 🟠 | ❓ |
| W-033 | Heat scoring weights (recency, furthest stage, count, EOI count) | M | 🟡 | ❓ |
| W-034 | Reservation cancel mid-flow | S | 🟡 | ❓ |
| W-035 | EOI document expiry | S | 🟡 | ❓ |
| W-036 | Contract sent + bounced email | S | 🟡 | ❓ |
| W-037 | Reminder snooze / dismiss | S | 🟢 | ❓ |
| W-038 | Reminder digest delivery | M | 🟢 | ❓ |
| W-039 | Default-owner auto-assign on new interest | XS | 🟢 | ❓ |
| W-040 | Reassignment notification email | S | 🟢 | ❓ |
| W-041 | Cascading invites (secondary signers) | M | 🟠 | ❓ |
| W-042 | Field-level signing verification | M | 🟡 | ❓ |
| W-043 | Voice-note attach on activity | S | 🟢 | ❓ |
| W-044 | Quick-template log entry | S | 🟢 | ❓ |
| W-045 | Note add / edit / delete (polymorphic across entities) | S | 🟢 | ❓ |
| W-046 | Tag add via inline-tag-editor (verified F16 inline create flow) | XS | 🟢 | ⚠️ |
| W-047 | Tag delete cascade (remove tag from all entities) | S | 🟡 | ❓ |
| W-048 | Bulk archive (clients) | S | 🟡 | ❓ |
| W-049 | Bulk archive (interests) | S | 🟡 | ❓ |
| W-050 | Restore archived (any entity) | S | 🟡 | ❓ |
| W-051 | Hard-delete request (GDPR Article 17) | M | 🟠 | ❓ |
| W-052 | GDPR export download | M | 🟠 | ✅ (R2-O) |
---
## 5. Admin workflows
| ID | Flow | Effort | Severity | Coverage |
| ------ | ---------------------------------------------------------------------------- | ------ | -------- | --------------- |
| AD-001 | Role create + permission edit | S | 🟠 | ❓ |
| AD-002 | Per-port role override | S | 🟠 | ❓ |
| AD-003 | User invite send + email delivered | M | 🟠 | ❓ |
| AD-004 | Invite accept + activate (token in #fragment) | S | 🟠 | ❓ |
| AD-005 | Invitation revoke / resend | XS | 🟡 | ❓ |
| AD-006 | User edit (display name, residential access toggle) | XS | 🟢 | ❓ |
| AD-007 | User deactivate | S | 🟠 | ❓ |
| AD-008 | System settings key update | XS | 🟡 | ❓ |
| AD-009 | Branding logo upload + render in email templates | S | 🟢 | ❓ |
| AD-010 | Branding primary colour propagation | S | 🟢 | ❓ |
| AD-011 | Document template create with merge tokens | S | 🟠 | ❓ |
| AD-012 | Template merge field validation (unknown token rejected) | XS | 🟢 | ❓ |
| AD-013 | Email template subject preview / override | S | 🟢 | ❓ |
| AD-014 | Tag create + colour pick + delete | XS | 🟢 | ✅ |
| AD-015 | Vocabulary list edit (interest temperatures, etc) | S | 🟢 | ❓ |
| AD-016 | Custom field add (text, number, select, date) | S | 🟡 | ❓ |
| AD-017 | Custom field retrofit on existing rows | S | 🟡 | ❓ |
| AD-018 | Webhook create + secret rotate | S | 🟠 | ❓ |
| AD-019 | Webhook delivery log + retry | S | 🟡 | ❓ |
| AD-020 | Brochure upload + magic-byte check | S | 🟡 | ❓ |
| AD-021 | Brochure default toggle (partial unique index) | S | 🟡 | ❓ |
| AD-022 | Brochure archive | XS | 🟢 | ❓ |
| AD-023 | Per-berth PDF upload + parse | M | 🟠 | ❓ |
| AD-024 | Per-berth PDF version rollback | S | 🟡 | ❓ |
| AD-025 | OCR parse confidence threshold + AI parse fallback | M | 🟡 | ❓ |
| AD-026 | NocoDB import: --apply, --force, --update-snapshot | M | 🟠 | ❓ |
| AD-027 | NocoDB import idempotency (re-run after no changes) | S | 🟡 | ❓ |
| AD-028 | NocoDB import vs human-edited row skip (updated_at > last_imported_at) | S | 🟡 | ❓ |
| AD-029 | Bulk berth add wizard end-to-end | S | 🟠 | ⚠️ (loads only) |
| AD-030 | CSV import (clients) — column mapper | M | 🟠 | ❓ |
| AD-031 | CSV import (yachts) | M | 🟡 | ❓ |
| AD-032 | CSV import error report (rejected rows) | S | 🟡 | ❓ |
| AD-033 | Duplicates queue review + merge | M | 🟠 | ❓ |
| AD-034 | Duplicates queue: false-positive dismiss | XS | 🟢 | ❓ |
| AD-035 | Audit log search/FTS — text query | S | 🟡 | ❓ |
| AD-036 | Audit log filter by action / entity / user / date range | S | 🟡 | ❓ |
| AD-037 | Audit log diff display (old vs new) | S | 🟡 | ❓ |
| AD-038 | Audit log mask of sensitive fields (passwords, tokens) | S | 🟠 | ❓ |
| AD-039 | Backup status read | XS | 🟢 | ❓ |
| AD-040 | Storage backend swap dry-run (filesystem ↔ s3) | M | 🟠 | ❓ |
| AD-041 | Multi-node deployment refuses filesystem backend | XS | 🟠 | ❓ |
| AD-042 | Documenso health check Test button (v1 + v2) | S | 🟠 | ❓ |
| AD-043 | Documenso API version toggle per-port | S | 🟠 | ❓ |
| AD-044 | Documenso signing-order setting (parallel/sequential) | S | 🟡 | ❓ |
| AD-045 | Documenso redirect URL setting | XS | 🟢 | ❓ |
| AD-046 | AI provider credentials test | S | 🟡 | ❓ |
| AD-047 | Receipt OCR config + retry on bad parse | M | 🟡 | ❓ |
| AD-048 | Send-from account config + encrypted secret roundtrip | M | 🟠 | ❓ |
| AD-049 | Bounce monitoring (IMAP probe + dev-imap-probe script) | M | 🟡 | ❓ |
| AD-050 | Reminders default behaviour + digest window edit | S | 🟢 | ❓ |
| AD-051 | Residential pipeline stages edit + reassignment on stage removal | M | 🟡 | ❓ |
| AD-052 | Qualification criteria reorder (DnD) | S | 🟢 | ❓ |
| AD-053 | Berth rules engine config (7 triggers, 3 modes) | M | 🟠 | ❓ |
| AD-054 | Recommender weights tune | S | 🟡 | ❓ |
| AD-055 | Onboarding checklist progression | S | 🟢 | ❓ |
| AD-056 | Reports: pipeline funnel, occupancy timeline, revenue breakdown, lead source | S | 🟡 | ❓ |
| AD-057 | Forms: form template create + public submission roundtrip | M | 🟠 | ❓ |
| AD-058 | Inquiry inbox triage → convert to client | M | 🟠 | ❓ |
| AD-059 | Website analytics (Umami) config | S | 🟢 | ❓ |
| AD-060 | Queue monitoring dashboard (BullMQ stats) | XS | 🟢 | ❓ |
---
## 6. Multi-tenancy (port isolation)
| ID | Check | Effort | Severity | Coverage |
| ----- | -------------------------------------------------------------------- | ------ | -------- | --------- |
| MT-01 | GET /api/v1/clients/<other-port-uuid> with X-Port-Id=this-port → 404 | XS | 🟠 | ✅ (R2-N) |
| MT-02 | PATCH /api/v1/interests/<other-port-uuid> → 404 | XS | 🟠 | ❓ |
| MT-03 | Berth recommender cross-port leak guard (entry + SQL CTE) | S | 🔴 | ✅ |
| MT-04 | Document folder defense-in-depth port_id filter on every join | S | 🟠 | ❓ |
| MT-05 | Audit log scope per port | XS | 🟠 | ❓ |
| MT-06 | Webhook subscriptions scoped to port | XS | 🟠 | ❓ |
| MT-07 | System settings per-port | XS | 🟡 | ❓ |
| MT-08 | Tags scoped to port | XS | 🟡 | ❓ |
| MT-09 | Custom fields scoped to port | XS | 🟡 | ❓ |
| MT-10 | Vocabularies scoped to port | XS | 🟡 | ❓ |
| MT-11 | Seed runs idempotent across ports | S | 🟡 | ❓ |
---
## 7. Security
| ID | Check | Effort | Severity | Coverage |
| ---- | ---------------------------------------------------------- | ------ | -------- | --------- |
| S-01 | XSS via client.fullName render (verified ✓) | XS | 🟠 | ✅ |
| S-02 | XSS via tag.name | XS | 🟠 | ❓ |
| S-03 | XSS via note.content (markdown) | S | 🟠 | ❓ |
| S-04 | XSS via email body markdown (verified) | S | 🟠 | ✅ (R2-I) |
| S-05 | SQL injection via search query | S | 🔴 | ❓ |
| S-06 | Path traversal in folder name | S | 🟠 | ❓ |
| S-07 | Path traversal in file name | XS | 🟠 | ❓ |
| S-08 | SSRF via attachment URL or webhook target | S | 🟠 | ❓ |
| S-09 | Open redirect on `next` param | XS | 🟠 | ❓ |
| S-10 | CSRF on state-changing requests (proxy.ts checks) | S | 🟠 | ❓ |
| S-11 | Cookie flags: HttpOnly, Secure, SameSite | XS | 🟠 | ❓ |
| S-12 | CSP headers (production) | S | 🟡 | ❓ |
| S-13 | CORS allow-list narrow | XS | 🟡 | ❓ |
| S-14 | Rate limit on login (verified F7) | XS | 🟠 | ✅ |
| S-15 | Rate limit on forget-password | XS | 🟠 | ✅ |
| S-16 | Rate limit on file upload | S | 🟡 | ❓ |
| S-17 | Session fixation (regen sid on login) | S | 🟠 | ❓ |
| S-18 | Token expiry / refresh (better-auth) | S | 🟠 | ❓ |
| S-19 | Audit log tamper-resistance (append-only) | S | 🟡 | ❓ |
| S-20 | Documenso webhook secret rotation (verified) | S | 🟠 | ✅ |
| S-21 | SMTP credential at-rest encryption (AES-256-GCM) | S | 🟠 | ❓ |
| S-22 | IMAP credential at-rest encryption | S | 🟠 | ❓ |
| S-23 | Storage credential at-rest encryption | S | 🟠 | ❓ |
| S-24 | Privilege escalation: viewer → agent → admin paths | M | 🔴 | ❓ |
| S-25 | Direct ID enumeration (UUID guess immune) | XS | 🟢 | ✅ (R2) |
| S-26 | Audit log read-back of own permission denials | S | 🟢 | ❓ |
| S-27 | Magic-byte verification on every uploaded file (verified) | S | 🟠 | ✅ |
| S-28 | Filename HTML-escape in download links | XS | 🟡 | ❓ |
| S-29 | Bounce-monitor email subject parsing (injection) | S | 🟡 | ❓ |
| S-30 | Email body redirect mode never escapes in prod (env guard) | XS | 🟠 | ❓ |
---
## 8. Realtime / sockets
| ID | Check | Effort | Severity | Coverage |
| ----- | -------------------------------------------------------------- | ------ | -------- | -------- |
| RT-01 | Socket.IO server actually running in dev (A5) | S | 🟡 | ❌ |
| RT-02 | Realtime invalidation: interest:updated fires from another tab | S | 🟡 | ❓ |
| RT-03 | document:completed event invalidates files | S | 🟡 | ❓ |
| RT-04 | folder:created event invalidates document-folders | S | 🟡 | ❓ |
| RT-05 | berth:statusChanged event invalidates berths | S | 🟡 | ❓ |
| RT-06 | Subscription teardown on unmount (no leaks) | S | 🟡 | ❓ |
| RT-07 | Cross-tab broadcast (BroadcastChannel?) | M | 🟢 | ❓ |
| RT-08 | Reconnect after server restart | S | 🟡 | ❓ |
| RT-09 | Room-level scoping (port:X room) | XS | 🟠 | ❓ |
---
## 9. Performance
| ID | Check | Effort | Severity | Coverage |
| ---- | ------------------------------------------------------------------------ | ------ | -------- | --------------------------- |
| P-01 | Web vitals report endpoint accepts beacons (verified — A2 is dev cancel) | XS | 🟢 | ✅ |
| P-02 | LCP under 2.5s on dashboard | S | 🟡 | ❓ |
| P-03 | CLS under 0.1 | S | 🟢 | ❓ |
| P-04 | TTI under 3s | S | 🟡 | ❓ |
| P-05 | N+1 detection on interests list (tags / berths / yacht joins) | M | 🟡 | ❓ |
| P-06 | DataTable virtual rendering for 1000+ rows | M | 🟡 | ⚠️ (audit-log uses virtual) |
| P-07 | Image lazy-load on documents list | XS | 🟢 | ❓ |
| P-08 | Bundle size growth budget | S | 🟢 | ❓ |
| P-09 | Slow-query log review | M | 🟡 | ❓ |
| P-10 | DB connection pool exhaustion behaviour (verified F8 fix landed) | S | 🟠 | ✅ |
| P-11 | Memory leak after long session (open same form 50 times) | M | 🟡 | ❓ |
| P-12 | Worker queue throughput under load | M | 🟡 | ❓ |
| P-13 | Search FTS query plan (uses GIN index?) | S | 🟡 | ❓ |
| P-14 | API response size budget (paginated list ≤ 256 KB) | XS | 🟢 | ❓ |
---
## 10. Documents / files
| ID | Check | Effort | Severity | Coverage |
| ---- | ----------------------------------------------------------------------------- | ------ | -------- | -------- |
| D-01 | Upload via drag-drop on hub root (A16 — broken) | XS | 🟠 | ❌ |
| D-02 | Upload via drag-drop on entity folder | S | 🟠 | ❓ |
| D-03 | Upload via file picker on dialog | XS | 🟠 | ❌ (A16) |
| D-04 | PDF preview inline | S | 🟢 | ❓ |
| D-05 | Image preview inline (jpg, png, webp, gif) | S | 🟢 | ❓ |
| D-06 | Word / Excel: download fallback | XS | 🟢 | ❓ |
| D-07 | Signed PDF download from completed workflow | S | 🟠 | ❓ |
| D-08 | Folder soft-rescue on delete (children re-parent) | S | 🟠 | ❓ |
| D-09 | Folder rename → entity name sync | S | 🟡 | ❓ |
| D-10 | Folder move cycle prevention | S | 🟡 | ❓ |
| D-11 | Folder permission: system folders immutable through API | S | 🟠 | ❓ |
| D-12 | Aggregated entity view (Clients/Companies/Yachts subfolders) | S | 🟡 | ❓ |
| D-13 | Hub root view: 3 cards (in-progress, files, completed) | S | 🟢 | ❓ |
| D-14 | EntityFolderView: signing-in-progress + files | S | 🟢 | ❓ |
| D-15 | "View signing details" link on signed file row | XS | 🟢 | ❓ |
| D-16 | Auto-deposit on signing completion (resolves owner via Owner-wins chain) | M | 🟠 | ❓ |
| D-17 | listFilesAggregatedByEntity walks Client↔Company↔Yacht reach symmetrically | M | 🟠 | ❓ |
| D-18 | Folder URL state with `?folder=<uuid>` (F25 deep folder) | XS | 🟢 | ⚠️ |
| D-19 | Concurrent ensureEntityFolder race-safety (partial unique index) | M | 🟡 | ❓ |
| D-20 | Magic-byte verification on presign + post-upload paths | S | 🟠 | ✅ |
| D-21 | Filename HTML-escape in fallback download link | XS | 🟡 | ❓ |
| D-22 | File size > email_attach_threshold_mb → signed-URL link instead of attachment | M | 🟡 | ❓ |
---
## 11. Audit log
| ID | Check | Effort | Severity | Coverage |
| ----- | --------------------------------------------------------------------------------- | ------ | -------- | -------- |
| AU-01 | Every mutation creates an audit row (sample 10 endpoints) | M | 🟠 | ⚠️ |
| AU-02 | Sensitive-field mask works (test: password rotation row) | S | 🟠 | ❓ |
| AU-03 | FTS query returns expected results | S | 🟡 | ❓ |
| AU-04 | Filter by action: only stage_change shows | XS | 🟢 | ❓ |
| AU-05 | Filter by entity type: only berth/interest/etc shows | XS | 🟢 | ❓ |
| AU-06 | Filter by user | XS | 🟢 | ❓ |
| AU-07 | Filter by date range | XS | 🟢 | ❓ |
| AU-08 | Diff display correctly highlights old vs new | S | 🟡 | ❓ |
| AU-09 | "Reconcile" event tag visible in metadata | XS | 🟢 | ✅ |
| AU-10 | Cascade events grouped or distinct? (e.g. archive client + auto-archive interest) | S | 🟡 | ❓ |
| AU-11 | Permission-denied entries render readable (A1) | XS | 🟡 | ❌ |
| AU-12 | Audit log export to CSV | S | 🟢 | ❓ |
| AU-13 | Outcome-change action tag distinct from generic 'update' (R2-B finding) | S | 🟡 | ❓ |
| AU-14 | Tier-mapping (audit_logs.audit_tier_map) — high-tier vs noise tier | S | 🟡 | ❓ |
---
## 12. Email / SMTP / IMAP
| ID | Check | Effort | Severity | Coverage |
| ----- | ---------------------------------------------------------------- | ------ | -------- | -------- |
| EM-01 | Per-port SMTP override picks up | S | 🟠 | ❓ |
| EM-02 | Default sales send-from (`sales@portnimara.com`) | XS | 🟢 | ❓ |
| EM-03 | Default noreply send-from (`noreply@portnimara.com`) | XS | 🟢 | ❓ |
| EM-04 | EMAIL_REDIRECT_TO in dev: subject prefix `[redirected from ...]` | XS | 🟡 | ❓ |
| EM-05 | Branded template render (logo, blurred bg, max-w-600) | S | 🟢 | ❓ |
| EM-06 | Reply-to override | XS | 🟡 | ❓ |
| EM-07 | CC/BCC handling | S | 🟡 | ❓ |
| EM-08 | Send rate limit 50/user/hour | XS | 🟡 | ❓ |
| EM-09 | Send size > threshold falls back to signed link | M | 🟡 | ❓ |
| EM-10 | IMAP bounce probe (`dev-imap-probe.ts`) | M | 🟢 | ❓ |
| EM-11 | Bounce subject parse + interest linking | M | 🟡 | ❓ |
| EM-12 | Document_sends audit row per send | S | 🟡 | ❓ |
| EM-13 | Portal activation email arrives & token works | M | 🟠 | ❓ |
| EM-14 | Reset-password email | S | 🟠 | ❓ |
| EM-15 | Invite email | M | 🟠 | ❓ |
| EM-16 | Reminder digest email | M | 🟢 | ❓ |
| EM-17 | EOI generated PDF attached or inline? | S | 🟡 | ❓ |
| EM-18 | Outbound email markdown body XSS (verified) | S | 🟠 | ✅ |
| EM-19 | Subject override CSP/XSS | S | 🟠 | ✅ |
---
## 13. Integrations
| ID | Check | Effort | Severity | Coverage |
| ----- | --------------------------------------------------------------------- | ------ | -------- | -------- |
| IN-01 | Documenso send EOI via v1 template-generate | M | 🟠 | ❓ |
| IN-02 | Documenso v2 envelope/create multipart | M | 🟠 | ❓ |
| IN-03 | Documenso distribute (v2) | S | 🟠 | ❓ |
| IN-04 | Documenso redistribute / send reminder | S | 🟡 | ❓ |
| IN-05 | Documenso downloadSignedPdf | S | 🟠 | ❓ |
| IN-06 | Documenso voidDocument | S | 🟡 | ❓ |
| IN-07 | Documenso placeFields (v2 field/create-many) | M | 🟡 | ❓ |
| IN-08 | Documenso normalizeDocument id ↔ documentId | XS | 🟡 | ❓ |
| IN-09 | NocoDB import idempotency | S | 🟡 | ❓ |
| IN-10 | S3 / MinIO upload + download | S | 🟠 | ❓ |
| IN-11 | S3 presigned URL expiry | XS | 🟡 | ❓ |
| IN-12 | Filesystem backend: MULTI_NODE_DEPLOYMENT guard | XS | 🟠 | ❓ |
| IN-13 | BullMQ job retry on failure | S | 🟡 | ❓ |
| IN-14 | BullMQ Redis `noeviction` policy (verified) | XS | 🟠 | ✅ |
| IN-15 | Worker process boot + queue subscribe | S | 🟠 | ❓ |
| IN-16 | Public berths API: anon cache headers | XS | 🟢 | ❓ |
| IN-17 | Public berths API: status filter (`Under Offer`, `Sold`, `Available`) | S | 🟡 | ❓ |
| IN-18 | Public berths single endpoint via mooringNumber (canonical format) | S | 🟡 | ❓ |
| IN-19 | Public health anonymous mode (verified A26) | XS | 🟡 | ✅ |
| IN-20 | Public health secret mode (verified A26) | XS | 🟡 | ✅ |
| IN-21 | OpenAI / AI parser credentials test | S | 🟡 | ❓ |
| IN-22 | Tesseract OCR positional heuristics on per-berth PDF | M | 🟡 | ❓ |
| IN-23 | Receipt OCR: full receipt parse end-to-end | M | 🟡 | ❓ |
| IN-24 | Pdfme PDF generation (any per-port template) | M | 🟡 | ❓ |
| IN-25 | PDF-lib AcroForm fill (in-app EOI pathway) | M | 🟠 | ❓ |
| IN-26 | EOI merge token expansion (`{{eoi.berthRange}}` etc) | S | 🟠 | ❓ |
| IN-27 | Berth-range formatter (single + multi-berth) | S | 🟡 | ❓ |
| IN-28 | Portal magic-link consume | S | 🟠 | ❓ |
| IN-29 | Umami analytics widget render | XS | 🟢 | ❓ |
---
## 14. Schema / migration
| ID | Check | Effort | Severity | Coverage |
| ----- | ------------------------------------------------------------------------------- | ------ | -------- | -------- |
| SC-01 | All migrations idempotent (re-run safe) | M | 🟠 | ❓ |
| SC-02 | All FKs have ON DELETE behaviour spec'd (CASCADE, SET NULL, RESTRICT) | S | 🟠 | ❓ |
| SC-03 | All soft-delete columns indexed (`archivedAt IS NULL`) | S | 🟡 | ❓ |
| SC-04 | All search columns have GIN/FTS indexes | S | 🟡 | ❓ |
| SC-05 | Composite unique constraints (sibling folder name, default brochure) | S | 🟡 | ❓ |
| SC-06 | Partial unique constraints (entity-folder, isPrimary) | S | 🟡 | ❓ |
| SC-07 | CHECK constraints (chk_system_folder_shape) | XS | 🟢 | ❓ |
| SC-08 | Generated column accuracy (FTS search_text) | S | 🟡 | ❓ |
| SC-09 | Column nullability matches Drizzle schema | M | 🟡 | ❓ |
| SC-10 | Schema migration restart-after-push (CLAUDE.md gotcha) | XS | 🟠 | ❓ |
| SC-11 | Backfill scripts idempotent (`backfill-document-folders.ts`) | S | 🟡 | ❓ |
| SC-12 | Legacy enum migration drift (every place that compared against an old value) | M | 🟠 | ❓ |
| SC-13 | Currency code enum | XS | 🟡 | ❓ |
| SC-14 | Address-component enum | XS | 🟢 | ❓ |
| SC-15 | Polymorphic owner: every read-site uses the service helper, not raw column read | M | 🟠 | ❓ |
---
## 15. i18n / l10n
| ID | Check | Effort | Severity | Coverage |
| ---- | ---------------------------------------------- | ------ | -------- | -------- |
| L-01 | Currency formatting per locale | S | 🟢 | ❓ |
| L-02 | Date formatting per timezone | S | 🟢 | ❓ |
| L-03 | Number formatting (1,000.5 vs 1.000,5) | S | 🟢 | ❓ |
| L-04 | Plural forms | S | 🟢 | ❓ |
| L-05 | RTL support (test with Arabic UA) | S | 🟢 | ❓ |
| L-06 | Translation completeness (Phase C status) | M | 🟢 | ❓ |
| L-07 | next-intl messages.json coverage | S | 🟢 | ❓ |
| L-08 | Server-rendered locale match (Accept-Language) | S | 🟢 | ❓ |
---
## 16. Browser / device
| ID | Check | Effort | Severity | Coverage |
| ----- | --------------------------------------------------- | ------ | -------- | -------- |
| BR-01 | Safari (macOS) primary flows | M | 🟡 | ❓ |
| BR-02 | Safari (iOS) primary flows | M | 🟡 | ❓ |
| BR-03 | Firefox (latest) | M | 🟢 | ❓ |
| BR-04 | Edge (latest) | M | 🟢 | ❓ |
| BR-05 | Chrome (latest) — primary | S | 🟢 | ✅ |
| BR-06 | iPad (Safari) — tier "click" via computer-use rules | M | 🟢 | ❓ |
| BR-07 | Print stylesheet (interest detail, invoice) | S | 🟢 | ❓ |
---
## 17. Specific behavioral correctness checks
| ID | Check | Effort | Severity | Coverage |
| ---- | ----------------------------------------------------------------------------------------------------------------------------- | ------ | -------- | ----------- |
| B-01 | Berth A1 hard-deleted earlier; confirm no 404 anywhere (interests' linked-berth, public feed, recommender) | M | 🟠 | ❓ |
| B-02 | Sara Laurent interest in stage=contract WITHOUT yachtId → render correctness | XS | 🟡 | ❓ |
| B-03 | Outcome-set interests filtered from active queries via `activeInterestsWhere` | S | 🟠 | ❓ |
| B-04 | EOI bundle range formatter: `A1-A3, B5` for non-contiguous berths | S | 🟡 | ❓ |
| B-05 | EOI single-berth case formats to just mooring (`A1`) | XS | 🟢 | ❓ |
| B-06 | Activity timeline 7-day window inclusive of today | XS | 🟢 | ✅ (F2 fix) |
| B-07 | Heat-scoring tier B only fires for lost/cancelled-only history | M | 🟡 | ❓ |
| B-08 | Permission-denied audit row sequencing (does denied API call still log?) | S | 🟡 | ❓ |
| B-09 | Same-stage no-op DOES NOT emit audit/socket event (F27) | S | 🟢 | ⚠️ |
| B-10 | Documenso webhook with empty body / malformed payload | S | 🟠 | ❓ |
| B-11 | Berth status_override_mode transitions through automated → manual → null | M | 🟡 | ❓ |
| B-12 | Reconcile clear stamps reason correctly with interest id (verified) | XS | 🟢 | ✅ |
| B-13 | Catch-up wizard "contract" stage auto-sets `outcome=won` | S | 🟡 | ❓ |
| B-14 | Catch-up wizard surfaces in API audit log as `reconcile_manual` type | XS | 🟢 | ✅ |
| B-15 | Mobile shell when initialFormFactor is wrong (Playwright UA = desktop, viewport = mobile) — shell ends up correct after mount | XS | 🟢 | ✅ |
| B-16 | Resizing across breakpoint mid-form-edit: state preservation? | S | 🟡 | ❓ |
| B-17 | Berths bulk-add wizard: step transitions persist input | M | 🟡 | ❓ |
| B-18 | NotesList polymorphic across all 4 entity types (clients, interests, yachts, companies) | S | 🟡 | ❓ |
| B-19 | InlineEditableField on every detail page works | M | 🟡 | ❓ |
| B-20 | InlineTagEditor: focus management (F45 verified) | S | 🟢 | ⚠️ |
| B-21 | OwnerPicker: client+company tabs render correctly (F44 verified) | XS | 🟢 | ✅ |
| B-22 | Mark externally signed sets `documentId=null`, `signedAt=now` | S | 🟡 | ❓ |
---
## 18. Data-clean-up jobs
| ID | Check | Effort | Severity | Coverage |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -------- | -------- |
| DC-01 | Orphan-blob cleanup on document delete | S | 🟠 | ❓ |
| DC-02 | Soft-deleted entities older than X days hard-purged | M | 🟡 | ❓ |
| DC-03 | Test entities in DB (per prior audit notes): `Smoke Test Client (renamed)`, `Aurora Marine Holdings Ltd`, `Bad Email Test`, `Phone Test`, `François 🏄 المعتمد`, `CSRF Test`, etc — `db:reseed:synthetic`? | S | 🟢 | ❓ |
| DC-04 | Berth A1 hard-deletion in port-amador: was that recovered? | S | 🟡 | ❓ |
| DC-05 | Legacy `statusOverrideMode = "auto"` normalize migration | XS | 🟢 | ❌ (A8) |
---
## 19. CI / dev experience
| ID | Check | Effort | Severity | Coverage |
| ----- | ---------------------------------------------------------------- | ------ | -------- | -------- |
| CI-01 | Husky lint-staged blocks bad commits | XS | 🟢 | ✅ |
| CI-02 | `pnpm exec tsc --noEmit` clean | XS | 🟢 | ✅ |
| CI-03 | `pnpm lint` zero errors | XS | 🟢 | ✅ |
| CI-04 | `pnpm exec vitest run` 1373/1373 pass | S | 🟢 | ✅ |
| CI-05 | `pnpm exec playwright test --project=smoke` ~10min | M | 🟢 | ❓ |
| CI-06 | `pnpm exec playwright test --project=destructive` | M | 🟢 | ❓ |
| CI-07 | `pnpm exec playwright test --project=realapi` (Documenso + IMAP) | M | 🟢 | ❓ |
| CI-08 | `pnpm exec playwright test --project=visual` baselines current | S | 🟢 | ❓ |
| CI-09 | Gitea CI lint + build-and-push workflows | S | 🟢 | ❓ |
| CI-10 | Docker prod build succeeds | M | 🟠 | ❓ |
| CI-11 | docker-compose dev startup with all services | S | 🟢 | ❓ |
| CI-12 | Pre-commit hook also blocks `.env*` files | XS | 🟢 | ❓ |
| CI-13 | `SKIP_ENV_VALIDATION=1` actually bypasses in Docker build | XS | 🟢 | ❓ |
---
## Recommendation: priority short-list
If we want maximum coverage with limited time, I'd pick:
### Tier 0 — fix what's already known (from A1-A20)
- A4 (client form silent-fail)
- A16 (file upload null vs string)
- A17 (/admin/ports bootstrap)
- A19 (F27 204 implementation)
- A9 (catch-up wizard stage default)
- A1/A2 (activity feed labels)
### Tier 1 — discover new
- **L-001** through **L-020** — legacy stage enum hunt (the user's specific concern)
- **W-001** — full end-to-end happy-path workflow (one full deal)
- **U-001** through **U-013** — every empty state surface
- **MT-01-11** — multi-tenancy cross-port checks (full sweep)
- **AU-01-14** — audit log surface (search, filters, mask, FTS)
- **U-021-039** — form design sweep across major forms
### Tier 2 — fill in coverage
- **R-001-030** — route correctness
- **AD-\* (admin pages)** — at least one mutation per admin section to confirm wiring
- **D-01-22** — documents/files end-to-end
### Tier 3 — depth checks
- **S-\* (security)** — penetration sweep
- **P-\* (performance)** — load + LCP + N+1
- **W-011-052** — every edge-case workflow
---
**Total surfaces catalogued:** 320+ discrete checks across 19 areas.
Pick what you want and I'll run it.