Files
pn-new-crm/docs/AUDIT-CATALOG.md
Matt 3b3ac287e0
All checks were successful
Build & Push Docker Images / lint (push) Successful in 2m6s
Build & Push Docker Images / build-and-push (push) Successful in 22s
docs(audit): comprehensive 320+ check catalog organized by area
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>
2026-05-15 00:54:08 +02:00

734 lines
76 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.