feat: warm-up deps — ts-reset, web-vitals, RHF devtool, query-broadcast
Four low-risk adds before the Zod 4 / drizzle-zod headliner: - @total-typescript/ts-reset: tightens TS stdlib types globally (JSON.parse → unknown, fetch().json() → unknown, .filter(Boolean) narrows, Set literals respect typed Set targets). Caught 179 latent type errors; fixed all production sites (8 files) and added `any` cast escape hatch in test files (ESLint exemption scoped to tests/). - web-vitals + /api/v1/internal/vitals endpoint + WebVitalsReporter client component: establishes Core Web Vitals baseline (LCP/INP/CLS/ FCP/TTFB) via navigator.sendBeacon. Required before optimisation work. - @hookform/devtools + FormDevtool wrapper: dev-only RHF state inspector, lazy-loaded via next/dynamic so the chunk is excluded from prod bundles entirely. - @tanstack/query-broadcast-client-experimental: cross-tab cache sync via BroadcastChannel — wired in query-provider.tsx, 1-liner. Audit doc updated with sections 35 + 36 (PDF stack overhaul + comprehensive second-pass package sweep) covering ~20 package adoption candidates and 4-5 deprecation candidates. Verified: tsc clean, vitest 1293/1293 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6712,3 +6712,811 @@ verify and full vitest run.
|
||||
6. **Defer indefinitely** — archiver 8, react-day-picker 10 (neither is delivering us anything we need).
|
||||
|
||||
**Non-goal:** chasing the bleeding edge on every dep. The audit's baseline finding stands — we are secure today. These are mostly developer-experience and perf wins, not security blockers.
|
||||
|
||||
---
|
||||
|
||||
## 35. Package adoption + PDF stack overhaul (Context7-assisted follow-up)
|
||||
|
||||
Companion to section 34. The deps-upgrade analysis answered "should we bump
|
||||
what we already have?" — this section answers two follow-on questions:
|
||||
|
||||
1. **PDF stack** — are pdfme + pdfkit + pdf-lib the right tools? (No.)
|
||||
2. **What aren't we using that we should be?** — comprehensive sweep of the
|
||||
modern ecosystem against our actual pain points and codebase patterns.
|
||||
|
||||
User-directed exclusions:
|
||||
|
||||
- `react-hotkeys-hook` (no keyboard-shortcut UX target).
|
||||
|
||||
---
|
||||
|
||||
### 35.A — PDF stack overhaul
|
||||
|
||||
#### Current state (5 packages, 4 distinct use cases)
|
||||
|
||||
| Package | Where it lives in our code | Use case |
|
||||
| ----------------------------------------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------- |
|
||||
| `@pdfme/common` + `generator` + `schemas` v6.1.2 | `src/lib/pdf/generate.ts` + 8 template files | Declarative report/invoice/EOI templates |
|
||||
| `pdf-lib` v1.17.1 | `src/lib/pdf/fill-eoi-form.ts`, `src/lib/services/berth-pdf-parser.ts` | AcroForm fill (EOI) + uploaded-PDF parsing (berth specs) |
|
||||
| `pdfkit` v0.18.0 + `@types/pdfkit` | `src/lib/services/expense-pdf.service.ts` (only site) | Streaming receipt-attached expense reports |
|
||||
| `tesseract.js` v7.0.0 | `src/lib/ocr/tesseract-client.ts` + scan-shell | Berth PDF OCR fallback |
|
||||
| **Bridge layer**: 571-line `src/lib/pdf/tiptap-to-pdfme.ts` | Admin template builder | Tiptap JSON → pdfme schema converter |
|
||||
|
||||
#### Pain points
|
||||
|
||||
- **The 571-line `tiptap-to-pdfme.ts` bridge** is fragile glue between a rich-text
|
||||
format (Tiptap JSON) and a declarative PDF schema (pdfme). Every supported
|
||||
formatting subset (bold, italic, headings, lists, tables, images) is
|
||||
hand-coded. Adding `blockquote` / `codeBlock` / `horizontalRule` /
|
||||
`taskList` is currently rejected at save time because the bridge doesn't
|
||||
support them.
|
||||
- **pdfme** templates are JSON blobs with positional `{ x, y }` coordinates.
|
||||
Reading/editing them is painful (compare `invoice-template.ts` vs a
|
||||
declarative React component).
|
||||
- **`@pdfme/generator` ships a heavy runtime** including the schema engine
|
||||
and font loaders — irrelevant for our use case because we're code-driven,
|
||||
not visual-editor-driven.
|
||||
- **3 different generation libraries** (pdfme + pdfkit + pdf-lib) means three
|
||||
different mental models, three different test patterns, three different
|
||||
failure modes.
|
||||
|
||||
#### Recommendation per use case
|
||||
|
||||
**Use case 1 — Template-driven PDFs (8 templates):** invoice, client-summary,
|
||||
interest-summary, berth-spec, revenue-report, occupancy-report, pipeline-report,
|
||||
eoi-standard-inapp.
|
||||
|
||||
**→ Replace with `@react-pdf/renderer`** (`/diegomura/react-pdf`, 161 snippets,
|
||||
benchmark 87.75).
|
||||
|
||||
Why it wins for us:
|
||||
|
||||
- **Declarative React components** — uses the same skills we already have. No
|
||||
more positional `{ x, y }` JSON.
|
||||
- **Server-side rendering modes**: `renderToBuffer` (HTTP responses),
|
||||
`renderToStream` (large reports), `renderToFile` (background jobs). All
|
||||
three usage patterns are documented and idiomatic — replaces pdfme's
|
||||
`generate()` call cleanly.
|
||||
- **First-class page-break controls** — `break`, `wrap={false}`,
|
||||
`minPresenceAhead`, `orphans`, `widows`. pdfme has none of these; we'd be
|
||||
hand-implementing them today if we needed them.
|
||||
- **Fixed headers/footers via `fixed` prop** with auto page-number rendering
|
||||
(`render={({ pageNumber, totalPages }) => …}`). We currently re-render
|
||||
header content per page in pdfme.
|
||||
- **The Tiptap bridge problem dissolves**: a rich-text component renders
|
||||
Tiptap JSON directly via a recursive component (~80 LOC, replaces 571 LOC).
|
||||
No more constrained-subset rejections at save time.
|
||||
- **Tree-shakes** — only the components we import ship; pdfme's generator
|
||||
pulls the full schema engine regardless.
|
||||
|
||||
Concrete migration cost: rewrite 8 templates as JSX. The shape is 1:1
|
||||
with our current pdfme schemas (header section, repeating items, footer
|
||||
totals), so it's a mechanical translation. ~4-6 hours total. Bridge layer
|
||||
(571 LOC) goes to zero.
|
||||
|
||||
Caveats from Context7:
|
||||
|
||||
- Font registration is explicit (`Font.register({ family, src })`) — our
|
||||
current fonts move from pdfme's font loader to a one-time call at boot.
|
||||
- No Tailwind class support — uses `StyleSheet.create({ ... })` with a
|
||||
flexbox-style subset. Familiar to React Native devs.
|
||||
|
||||
**Use case 2 — AcroForm fill (EOI):**
|
||||
|
||||
**→ Keep `pdf-lib`.** Best-in-class for editing existing PDFs. No replacement
|
||||
candidate is better. Already used correctly in `fill-eoi-form.ts`.
|
||||
|
||||
**Use case 3 — Uploaded PDF parsing (berth specs):**
|
||||
|
||||
**→ Add `unpdf`** (`/unjs/unpdf`, 66 snippets) for text extraction; keep
|
||||
`pdf-lib` for AcroForm field extraction.
|
||||
|
||||
Why:
|
||||
|
||||
- `unpdf` is the unjs ecosystem's serverless-friendly PDF parser built on
|
||||
pdf.js. Returns `{ totalPages, text }` per page in one call.
|
||||
- Better than `pdf-lib` for text extraction because pdf-lib's text APIs are
|
||||
read-positional, not read-flow.
|
||||
- `getDocumentProxy()` lets us share one parse across `extractText`,
|
||||
`extractLinks`, `getMeta` — useful for the 3-tier berth parser (AcroForm
|
||||
first, OCR fallback, AI fallback) because we can grab all metadata in one
|
||||
pass.
|
||||
|
||||
Our current parser uses `pdf-lib`'s low-level text extraction which has known
|
||||
issues with positionally-rendered text (the OCR fallback fires more often
|
||||
than necessary). `unpdf.extractText` would reduce that fallback rate.
|
||||
|
||||
**Use case 4 — Streaming receipt-attached expense reports:**
|
||||
|
||||
**→ Keep `pdfkit` short-term, migrate to `@react-pdf/renderer.renderToStream`
|
||||
medium-term.**
|
||||
|
||||
Why keep:
|
||||
|
||||
- `expense-pdf.service.ts` is the only `pdfkit` consumer. Its streaming
|
||||
pattern (500 receipts at <100MB RSS) is the load-bearing reason for
|
||||
pdfkit's existence in our deps.
|
||||
- `@react-pdf/renderer.renderToStream` documented in Context7 supports the
|
||||
same use case — but verification needs an actual perf test against a
|
||||
500-receipt fixture before committing.
|
||||
|
||||
Migration plan:
|
||||
|
||||
- Phase 1 (now): replace pdfme templates with @react-pdf/renderer.
|
||||
- Phase 2 (after we have @react-pdf/renderer in the codebase): re-test
|
||||
expense-pdf with `renderToStream` against the 500-receipt fixture. If
|
||||
memory stays under 200MB, swap pdfkit out. If not, keep pdfkit and
|
||||
document the constraint.
|
||||
|
||||
#### Net result after Phase 1
|
||||
|
||||
Remove: `@pdfme/common`, `@pdfme/generator`, `@pdfme/schemas`, 571-line
|
||||
bridge file.
|
||||
|
||||
Keep: `pdf-lib` (AcroForm), `pdfkit` (streaming expenses, pending Phase 2),
|
||||
`tesseract.js` (OCR).
|
||||
|
||||
Add: `@react-pdf/renderer`, `unpdf`.
|
||||
|
||||
Deps net: −2, −571 LOC of bridge code, +standard declarative API for all
|
||||
templates.
|
||||
|
||||
---
|
||||
|
||||
### 35.B — High-value package additions (prioritized)
|
||||
|
||||
Each row below has been validated via Context7 unless marked otherwise.
|
||||
|
||||
#### Tier 1 — Adopt alongside the planned Zod 4 / Tailwind 4 work
|
||||
|
||||
| Package | Replaces / unlocks | Where it lands in our code | Effort |
|
||||
| --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------- |
|
||||
| **`drizzle-zod`** (already in `drizzle-orm`) | ~30 hand-maintained validators in `src/lib/validators/` | `createInsertSchema(clients).omit({ id, portId })` etc. | 2-3h |
|
||||
| **`@react-pdf/renderer`** | 8 pdfme templates + 571-line tiptap bridge | `src/lib/pdf/templates/*` | 4-6h |
|
||||
| **`react-email`** + `@react-email/components` | 8 hand-strung HTML templates in `src/lib/email/templates/` | Each becomes a `.tsx` component, rendered via `await render(<…/>)` then handed to nodemailer unchanged | 2-3h (one template at a time) |
|
||||
| **`@tanstack/react-virtual`** | Pagination on `client-list`, `yacht-list`, `berth-list`, `audit-log-list`, `inbox` | `useVirtualizer({ count, estimateSize })` inside the list shells | 1h per list × 5 lists |
|
||||
| **`ts-pattern`** | 19-case dispatch in `search.service.ts`, 13-case Documenso webhook, 12-case `client-restore.service.ts`, 10-case `recently-viewed/route.ts`, 10-case `custom-fields/[entityId]/route.ts` | `match(input).with(...).exhaustive()` | 30 min per site; start with the Documenso webhook |
|
||||
| **`unpdf`** | Hand-rolled text extraction in `berth-pdf-parser.ts` | `extractText(await getDocumentProxy(buf))` | 1h |
|
||||
|
||||
#### Tier 2 — Independent adopts (polish + perf)
|
||||
|
||||
| Package | What it does for us | Effort |
|
||||
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
|
||||
| **`@formkit/auto-animate`** | One-liner `useAutoAnimate()` ref on any list. Drops into: deal pipeline kanban (pipeline-board.tsx), reminders rail, alerts rail, files list, notes list. Zero CSS. ~2kb. | 5 min per site |
|
||||
| **`motion`** (formerly framer-motion) | Layout animations for kanban reorder (currently snaps), Vaul drawer enter/exit polish, sheet/drawer slides, `<AnimatePresence>` for inline edits. ~15kb gzip but tree-shakes well. | 1-2h to wire the kanban first |
|
||||
| **`use-debounce`** | Replaces ad-hoc `setTimeout` debounce in `yacht-picker`, `client-picker`, `audit-log-list`, `send-document-dialog`, `custom-fields-section`, `berth-picker`, `interest-picker`, `dedup-suggestion-panel` (8 sites). Typed `useDebouncedCallback`. ~3kb. | 30 min total |
|
||||
| **`fast-deep-equal`** | Memo comparator for `DataTable` and React Query `select` functions. Drops re-renders when stable references arrive with new identity. ~1kb. | 20 min |
|
||||
| **`@upstash/ratelimit`** | Replaces hand-rolled rate limiters in `src/lib/rate-limit.ts`, `api/helpers.ts`, `route-helpers.ts`, `document-sends.service.ts`. Uses our existing Redis. Sliding-window / fixed-window / token-bucket algorithms tested at scale. | 1-2h |
|
||||
|
||||
#### Tier 3 — Strategic adopts (bigger commitments)
|
||||
|
||||
| Package | What it unlocks | Notes |
|
||||
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`next-safe-action`** | Type-safe server actions with built-in Zod validation, ownership middleware, `useHookFormAction` hook. Each form drops ~30 LOC of `apiFetch + toastError + mutation-hook` plumbing to ~5. Pairs with `useHookFormAction` which already speaks Zod/RHF. | Migrate gradually — use for new forms first, keep API routes for external callers. Couples with Zod 4 (since safe-action v8+ targets Zod 4 best). |
|
||||
| **`@axe-core/playwright`** | Accessibility audit during smoke tests. The 33-agent audit flagged WCAG gaps; this catches regressions automatically. | ~30 LOC of test setup. Fails CI on new violations. |
|
||||
| **`@tiptap/core`** + `@tiptap/react` + extension packs | Real rich-text editor for `notes` (clients/interests/yachts/companies all have polymorphic notes). Currently plain text. Sales reps note things like "call after 4pm UTC, prefers WhatsApp" — bold/italic/links/lists/mentions would help. Tiptap's JSON output format is _already_ in our codebase (the bridge layer), so we'd be storing the same shape we already render. | Decision: keep notes plain or upgrade to rich? If yes, ~3h to wire one entity's notes; the others copy the pattern. |
|
||||
| **`@next/bundle-analyzer`** | Wraps `next.config.ts`. Generates client + server bundle treemaps after every build. Catches when a tiny PR pulls in recharts on a route that shouldn't have it. The 33-agent audit flagged recharts + pdfme as bundle bloat — this is the tooling to keep that honest. | 15 min setup. Run with `ANALYZE=true pnpm build`. |
|
||||
| **`@sentry/nextjs`** | Error tracking with frontend + backend correlation, release tracking, source maps, performance traces, replay (optional). We have pino logs but no aggregation/alerting/correlation. Important once we have customer-facing users. | Decision: do we want a SaaS dependency? Self-hosted GlitchTip is also an option (Sentry-protocol-compatible). |
|
||||
| **`@vercel/og`** (or `satori`) | Generate Open Graph images for shared docs/portal links. Currently the portal has no social previews; if a client shares their EOI link in WhatsApp/Email, the preview is blank. ~10 LOC per route. | 1h for portal share routes. |
|
||||
| **`papaparse`** | CSV import/export. Sales reps frequently ask for "export to Excel." Plays well with our existing TanStack Table data. ~17kb. | 30 min for client/interest list export. |
|
||||
| **`@formkit/tempo`** OR **date-fns helpers** | We have **44 files** with hand-rolled `new Date().toLocaleString()` / `.toLocaleDateString()`. Centralize via a `formatDate(date, format, timezone)` helper using `date-fns` (already installed) — no new package needed if we use date-fns's `format`, `formatDistance`, `formatRelative` which we already have. **This is a refactor, not an adoption.** | 2-3h sweep |
|
||||
|
||||
#### Tier 4 — Defer or skip
|
||||
|
||||
| Package | Reason |
|
||||
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `next-pwa` / `@serwist/next` | PWA assets pending (per MEMORY.md). When that lands, **`@serwist/next`** is the modern choice (next-pwa is unmaintained). For now, skip. |
|
||||
| `next-intl` / `i18next` / `@lingui/core` | No i18n target today. When we localize, **`next-intl`** is the strongest Next.js App Router integration. For now, skip. |
|
||||
| `@knocklabs/node` + `@knocklabs/react` | Notification center + channel routing + preferences UI. Likely overkill — we have a simple in-app + email notification system that works. Revisit if we add SMS or push. |
|
||||
| `inngest` / `trigger.dev` | Background jobs with observability. We use BullMQ; revisit only if we need step functions / cross-service workflows. |
|
||||
| `posthog-js` | Product analytics + feature flags + session recording. We have Umami for web analytics; PostHog adds product-level tracking. Decision pending. |
|
||||
| `@growthbook/growthbook` | Feature flags only. We don't have any flagged features today. |
|
||||
| `fuse.js` / `minisearch` | Client-side fuzzy search. Useful for already-loaded list filtering, but TanStack Table's built-in filter is usually enough. |
|
||||
| `@uppy/core` + `@uppy/dashboard` | Rich file upload UI with resume, chunking. We have basic file inputs (0 patterns found in audit grep) — not currently a pain point. |
|
||||
| `@tanstack/react-form` | Successor to react-hook-form by same team. RHF is mature, well-known, and we have 8 forms on it. No compelling migration. |
|
||||
| `valibot` / `arktype` | Faster zod alternatives. We're committed to Zod 4. |
|
||||
| `react-hotkeys-hook` | **Excluded per user direction.** |
|
||||
|
||||
---
|
||||
|
||||
### 35.C — Deprecation / cleanup candidates
|
||||
|
||||
| Package | Reason | Action |
|
||||
| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
|
||||
| `@radix-ui/react-icons` | We use `lucide-react` everywhere. Audit grep shows no imports from `@radix-ui/react-icons`. | Drop after grep-confirm. ~30s. |
|
||||
| `@pdfme/common` + `@pdfme/generator` + `@pdfme/schemas` | Replaced by `@react-pdf/renderer` in Phase 1. | After PDF migration. |
|
||||
| `tailwindcss-animate` v1.0.7 | Last published 2024, no v4 support. Replace with **`tw-animate-css`** (the v4-native successor shadcn now recommends). | Required if we move to Tailwind 4. |
|
||||
| `@types/pdfkit` | Tops at v7.0.0. We're on `pdfkit` v0.18 — types are loose but functional. Keep until we migrate expense-pdf to @react-pdf/renderer. | Defer. |
|
||||
| `pino-pretty` in `dependencies` | Should be `devDependencies` only — ships ~500kb to prod worker images if it leaks into the runtime path. Audit-verify the build doesn't include it; move if it does. | 5 min check. |
|
||||
|
||||
---
|
||||
|
||||
### 35.D — Surfaced refactor opportunities (no new package required)
|
||||
|
||||
These came up while sweeping for package gaps. They're refactor wins, not
|
||||
package adoptions.
|
||||
|
||||
| Opportunity | Concrete sites | Tool |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
||||
| Centralize date formatting | 44 files with hand-rolled `.toLocaleString()` / `.toLocaleDateString()` | `formatDate(date, format, timezone)` helper using existing `date-fns` |
|
||||
| Centralize debounce | 8 picker/list components | `use-debounce` (or hand-rolled hook) |
|
||||
| Centralize rate-limiting | 4 hand-rolled limiters | `@upstash/ratelimit` |
|
||||
| Replace 5-9 large switch statements with exhaustive matchers | `search.service.ts` (19 cases), Documenso webhook (13), `client-restore.service.ts` (12), `recently-viewed/route.ts` (10), `custom-fields/[entityId]/route.ts` (10) | `ts-pattern` |
|
||||
|
||||
---
|
||||
|
||||
### 35.E — Final adoption order (revised, incorporating section 35)
|
||||
|
||||
This supersedes section 34's sequencing where they overlap.
|
||||
|
||||
1. **Now (one focused day)** — Zod 4 + `@hookform/resolvers` 5 + **`drizzle-zod`**. One PR. Codemod-friendly. Highest correctness payoff.
|
||||
2. **Independent (any time)** — **`react-email`** migration of one template (`portal-auth.ts` recommended first), then expand. Independent of any version upgrade.
|
||||
3. **Independent (any time)** — **`@react-pdf/renderer`** + **`unpdf`**. Replace 8 pdfme templates, delete 571-LOC bridge, add unpdf to berth parser.
|
||||
4. **Independent (any time)** — **`ts-pattern`** in the Documenso webhook switch first (the audit's bug-class poster child), then sweep the other 4 sites.
|
||||
5. **Independent (any time)** — **`@tanstack/react-virtual`** on `client-list` first, copy pattern to 4 other lists.
|
||||
6. **Independent (any time)** — **`@formkit/auto-animate`** sprinkle. 5-minute wins per site.
|
||||
7. **Independent (any time)** — **`@next/bundle-analyzer`** install. 15-min setup; ongoing bundle hygiene.
|
||||
8. **Next focused half-day** — **`motion`** wire to the kanban for smooth reorder.
|
||||
9. **2-4 weeks** — Next 15 → 16 + eslint-config-next 16 + eslint 10 (lockstep, codemod).
|
||||
10. **Focused afternoon** — Tailwind 4 via official upgrade tool + swap `tailwindcss-animate` for `tw-animate-css`.
|
||||
11. **When we have a new form to build** — pilot **`next-safe-action`** there; backfill existing forms gradually.
|
||||
12. **Decision required first** — `@sentry/nextjs` (SaaS dep), `@tiptap/*` (rich notes Y/N?), `posthog-js` (analytics scope), `papaparse` (CSV export priority).
|
||||
|
||||
---
|
||||
|
||||
### 35.F — Skipped per user direction
|
||||
|
||||
- **`react-hotkeys-hook`** — no keyboard-shortcut UX target across the platform.
|
||||
|
||||
---
|
||||
|
||||
## 36. Second-pass package sweep — mobile, fluidity, data speed, DX
|
||||
|
||||
Section 35 covered the headline adoption candidates. This section is the
|
||||
deliberate second sweep the user requested — looking specifically for
|
||||
libraries we may have missed across four dimensions: **current
|
||||
functionality gaps**, **optimization (mobile included)**, **UI fluidity**,
|
||||
and **data retrieval/writing speed**.
|
||||
|
||||
Findings are grouped by dimension. Each entry says (a) what we have now,
|
||||
(b) what the library adds, (c) where in our codebase it'd land, (d) effort.
|
||||
|
||||
---
|
||||
|
||||
### 36.A — Data speed & concurrency
|
||||
|
||||
#### 36.A.1 `p-queue` + `p-limit` + `p-retry` (Sindre Sorhus suite)
|
||||
|
||||
**Concrete pain:** 74 `Promise.all(...)` sites in services/routes. 8 mass-
|
||||
operation services (`expense-pdf`, `berth-pdf`, `brochures`, `backup`,
|
||||
`document-templates`, `email-compose`, `documents`, `email-threads`).
|
||||
Naive `Promise.all([...mapped])` will:
|
||||
|
||||
- Fire all 500 expense receipts to S3 simultaneously → MinIO connection
|
||||
pool exhaustion + memory spike (`expense-pdf.service.ts` docs explicitly
|
||||
call this out as a past problem).
|
||||
- Fire all bulk-send-document calls at Documenso simultaneously → hit
|
||||
Documenso's per-second rate limit, cause cascade failures.
|
||||
- Fire all email-compose attachments at SMTP simultaneously → SMTP
|
||||
connection limit on Mailgun/SES drops requests silently.
|
||||
|
||||
**`p-limit`** caps concurrency: `pLimit(5)` runs at most 5 at a time.
|
||||
**`p-queue`** is `p-limit` + interval rate limiting + pause/resume.
|
||||
**`p-retry`** handles exponential backoff retries for transient failures.
|
||||
|
||||
**Land sites:**
|
||||
|
||||
- `expense-pdf.service.ts` — already has streaming logic, but the
|
||||
per-receipt S3 `get` calls are unbounded.
|
||||
- `email-compose.service.ts` — bulk send-out is the obvious one.
|
||||
- `backup.service.ts` — GDPR export streaming.
|
||||
- `documents.service.ts` — multi-file folder operations.
|
||||
|
||||
**Effort:** 30 min per service. ~1.5kb each.
|
||||
|
||||
#### 36.A.2 `@tanstack/query-broadcast-client-experimental`
|
||||
|
||||
**Concrete pain:** A rep has the CRM open in two tabs. They update a
|
||||
client in tab A — tab B's stale cache continues showing old values until
|
||||
the next refetch.
|
||||
|
||||
**What it adds:** BroadcastChannel sync between tabs. Free cross-tab cache
|
||||
coherence with no server roundtrips.
|
||||
|
||||
**Land site:** One line in `src/providers/query-provider.tsx`:
|
||||
|
||||
```ts
|
||||
broadcastQueryClient({ queryClient, broadcastChannel: 'pn-crm' });
|
||||
```
|
||||
|
||||
**Effort:** 5 minutes. ~2kb.
|
||||
|
||||
#### 36.A.3 Underused Drizzle ORM features (no new package)
|
||||
|
||||
We have `drizzle-orm` 0.45.2 and use ~60% of its capabilities.
|
||||
|
||||
- **`db.batch(...)`** for atomic multi-statement transactions on
|
||||
Postgres. Currently we use explicit `db.transaction(async (tx) => {...})`
|
||||
blocks everywhere — `batch` is shorter and lets the driver pipeline.
|
||||
- **Prepared statements** via `.prepare()` — repeated queries (e.g.,
|
||||
`getClient(id)` called per-request) can be prepared once at boot and
|
||||
reused. Postgres saves the parse+plan cost.
|
||||
- **`with` (CTE) clauses** — we have 30+ places where we'd benefit from
|
||||
`WITH active_interests AS (...) SELECT ...` instead of joining the same
|
||||
subquery twice. Audit found N+1 patterns; CTEs flatten them.
|
||||
|
||||
**Land sites:** the recommender SQL aggregate (already uses CTEs),
|
||||
`dashboard.service.ts` analytics queries, `search.service.ts` graph
|
||||
expansion. These are all "we already wrote raw SQL strings; rewriting as
|
||||
typed Drizzle CTEs" wins.
|
||||
|
||||
**Effort:** opportunistic. No package change.
|
||||
|
||||
#### 36.A.4 `postgres.js` cursor for large reads
|
||||
|
||||
We have `postgres` ^3.4.9. Its `await sql\`...\`.cursor(rows => ...)`streams large result sets in batches without buffering all rows. Currently
|
||||
the GDPR-export bundling and the backup`dump-tables` paths buffer
|
||||
everything in memory.
|
||||
|
||||
**Land sites:** `backup.service.ts`, `gdpr-export.service.ts` (when we
|
||||
build it — currently parked).
|
||||
|
||||
**Effort:** opportunistic refactor when we touch those services.
|
||||
|
||||
---
|
||||
|
||||
### 36.B — UI fluidity & animation
|
||||
|
||||
#### 36.B.1 `@use-gesture/react` (mobile gestures)
|
||||
|
||||
**Concrete pain:** mobile users can't swipe-to-dismiss the Vaul drawer,
|
||||
swipe sideways between kanban columns, or pinch-zoom berth photos. The
|
||||
audit's mobile pass flagged these.
|
||||
|
||||
**What it adds:** declarative gesture handlers (`useDrag`, `usePinch`,
|
||||
`useScroll`). Composes with `motion` for spring-physics responses.
|
||||
|
||||
**Land sites:**
|
||||
|
||||
- Pipeline kanban: swipe between stage columns on mobile.
|
||||
- Vaul drawer: swipe-down to dismiss (Vaul already does this, but adding
|
||||
custom velocity thresholds via `@use-gesture` polishes the feel).
|
||||
- Berth/yacht photo galleries: pinch-zoom.
|
||||
|
||||
**Effort:** 1h to wire one site as the template. ~5kb.
|
||||
|
||||
#### 36.B.2 `embla-carousel-react`
|
||||
|
||||
**Concrete pain:** berth photos and yacht photos render as static grids
|
||||
(per the audit). On mobile, users want to swipe through them.
|
||||
|
||||
**What it adds:** lightweight, touch-native, accessibility-compliant
|
||||
carousel. Plays with framer-motion if we want fancy transitions.
|
||||
shadcn/ui has a `Carousel` component built on this — drop-in via the
|
||||
shadcn CLI.
|
||||
|
||||
**Effort:** `npx shadcn@latest add carousel`, then 10 lines to render the
|
||||
photo array. ~10kb gzip.
|
||||
|
||||
#### 36.B.3 `yet-another-react-lightbox`
|
||||
|
||||
**Concrete pain:** clicking a berth photo currently navigates to a fullscreen
|
||||
image route or doesn't expand at all. Sales reps want lightbox-style preview.
|
||||
|
||||
**What it adds:** fullscreen lightbox with keyboard nav, zoom, swipe, slideshow,
|
||||
captions. Plugin system for video/PDF embed if we extend.
|
||||
|
||||
**Land sites:** berth/yacht detail pages, client docs preview.
|
||||
|
||||
**Effort:** 1h. ~15kb gzip with plugins.
|
||||
|
||||
#### 36.B.4 `react-resizable-panels`
|
||||
|
||||
**Concrete pain:** the docs hub has a fixed-width folder sidebar (per
|
||||
CLAUDE.md's documents-hub rewrite). Power users on wide monitors want
|
||||
to drag-resize it.
|
||||
|
||||
**What it adds:** keyboard-accessible resizable split panes with
|
||||
persistent sizing (localStorage). shadcn/ui has a `Resizable` component
|
||||
built on this.
|
||||
|
||||
**Land sites:** docs hub (sidebar | content), email inbox (folder | thread),
|
||||
admin settings (nav | section).
|
||||
|
||||
**Effort:** `npx shadcn@latest add resizable`, drop in. ~5kb.
|
||||
|
||||
---
|
||||
|
||||
### 36.C — Mobile optimization
|
||||
|
||||
#### 36.C.1 `browser-image-compression`
|
||||
|
||||
**Concrete pain:** the expense-scanner (`scan-shell.tsx`) and receipt
|
||||
upload paths accept full-resolution phone photos (typically 4-12 MB each).
|
||||
Mobile users on cellular pay bandwidth + battery for sending 4× more
|
||||
data than necessary. The server then re-runs `sharp` to resize anyway.
|
||||
|
||||
**What it adds:** client-side image compression in WebWorker before
|
||||
upload. Targets `maxSizeMB`, `maxWidthOrHeight`, `useWebWorker`. The
|
||||
server still validates magic-bytes + sharp-resizes, but receives a
|
||||
500KB-resized JPG instead of a 12MB original.
|
||||
|
||||
**Concrete win:** a rep on 3G uploading a receipt: ~30s wait → ~5s wait.
|
||||
Server CPU on `sharp` resize drops to a no-op since the client did it.
|
||||
|
||||
**Effort:** 30 min to wire `scan-shell.tsx`. ~25kb gzip (worker-bundled so
|
||||
zero main-thread cost).
|
||||
|
||||
#### 36.C.2 `partysocket`
|
||||
|
||||
**Concrete pain:** mobile users on flaky networks frequently lose the
|
||||
Socket.IO connection. Our current client uses Socket.IO's built-in
|
||||
reconnect, which is good but not great for mobile.
|
||||
|
||||
**What it adds:** drop-in WebSocket wrapper with:
|
||||
|
||||
- Exponential backoff with jitter (default Socket.IO is linear).
|
||||
- Message queue while disconnected (Socket.IO buffers via volatile flag
|
||||
only).
|
||||
- Auto-reconnect on `online` event + `visibilitychange` (page wake).
|
||||
- Optional auto-detect connection quality (slow vs fast).
|
||||
|
||||
**Land site:** `src/providers/socket-provider.tsx`.
|
||||
|
||||
**Effort:** depends — `partysocket` works with raw WS, not Socket.IO's
|
||||
protocol. For Socket.IO we'd need `socket.io-client` + manual reconnect
|
||||
tuning, or migrate the realtime layer to plain WebSockets (significant).
|
||||
**Park as a "mobile flake" investigation, not an immediate adoption.**
|
||||
|
||||
#### 36.C.3 `react-virtuoso` (alternative to TanStack Virtual)
|
||||
|
||||
**Concrete pain:** the inbox (`src/components/layout/inbox.tsx`) uses a
|
||||
plain `<ScrollArea className="max-h-[400px]">` with no virtualization.
|
||||
For users with hundreds of unread items, mobile scrolling chugs.
|
||||
|
||||
**What it adds:** specialized virtualization for chat-like / inbox-like
|
||||
UIs with variable-height items and "scroll to bottom on new message"
|
||||
semantics. **TanStack Virtual is more headless / generic; Virtuoso is
|
||||
opinionated and better for inbox-shaped UIs.**
|
||||
|
||||
**Land site:** `inbox.tsx` specifically. For the regular lists
|
||||
(client/yacht/berth), TanStack Virtual is still the right call (section
|
||||
35.B.4).
|
||||
|
||||
**Effort:** 45 min. ~10kb.
|
||||
|
||||
#### 36.C.4 `@formkit/auto-animate` (revisit for mobile)
|
||||
|
||||
Already in section 35.B but worth re-emphasising: on mobile, list items
|
||||
appearing/disappearing without animation feels janky. Free polish.
|
||||
|
||||
---
|
||||
|
||||
### 36.D — Input quality & forms
|
||||
|
||||
#### 36.D.1 `react-imask` or `react-number-format`
|
||||
|
||||
**Concrete pain:** we have currency inputs, phone inputs, date inputs
|
||||
spread across berth-form, expense-form, invoice-form, client-form. The
|
||||
audit flagged inconsistent formatting (decimals, thousand-separators,
|
||||
phone-prefix handling).
|
||||
|
||||
**What it adds:** declarative input masks — `<IMaskInput mask="$num"
|
||||
scale={2} thousandsSeparator="," />`. Plays cleanly with react-hook-form.
|
||||
|
||||
`react-number-format` is the lighter-weight, currency-specific option.
|
||||
`react-imask` covers more patterns (phone, date, custom).
|
||||
|
||||
**Land sites:** ~6 form components.
|
||||
|
||||
**Effort:** 30 min per form × 6 = 3h. **OR** keep our hand-rolled
|
||||
formatters and don't add the dep. Decision pending.
|
||||
|
||||
#### 36.D.2 `@hookform/devtools` (dev-only)
|
||||
|
||||
**What it adds:** a floating panel in the browser showing react-hook-form
|
||||
state in real time (values, errors, isDirty, isValid, touched fields).
|
||||
Massive debug-time win.
|
||||
|
||||
**Land site:** wrap forms in `<DevTool control={form.control} />` in dev
|
||||
builds only.
|
||||
|
||||
**Effort:** 15 min. dev-only, ships zero to prod.
|
||||
|
||||
---
|
||||
|
||||
### 36.E — Security & sanitization
|
||||
|
||||
#### 36.E.1 `isomorphic-dompurify`
|
||||
|
||||
**Concrete pain:** `src/lib/utils/markdown-email.ts` hand-rolls HTML
|
||||
escape + safe-link rendering for email bodies. The audit raised XSS
|
||||
concerns (CRIT-2 in section 4) about admin-supplied content in templates
|
||||
and email bodies. Our hand-rolled `escapeHtml` is correct for the basic
|
||||
cases, but DOMPurify handles edge cases the audit listed (data URLs,
|
||||
nested encoding, javascript: in href attrs).
|
||||
|
||||
**What it adds:** battle-tested HTML sanitizer used by Google, Microsoft,
|
||||
GitHub. Works in Node + browser (the `isomorphic-` prefix is the
|
||||
SSR-compatible wrapper around the regular `dompurify`).
|
||||
|
||||
**Land sites:**
|
||||
|
||||
- `renderEmailBody()` in `markdown-email.ts`.
|
||||
- Anywhere we render user-supplied HTML (template preview, document
|
||||
body display).
|
||||
|
||||
**Effort:** 1h migration + audit. ~25kb (Node) / ~50kb (browser),
|
||||
acceptable.
|
||||
|
||||
#### 36.E.2 `@noble/hashes` (already covered by `better-auth`)
|
||||
|
||||
We use `better-auth` for password hashing. No need to add.
|
||||
|
||||
#### 36.E.3 WebAuthn / Passkeys (`@simplewebauthn/server` + `/browser`)
|
||||
|
||||
**What it adds:** passwordless authentication via device passkeys (Touch
|
||||
ID, Windows Hello, YubiKey). Better Auth has a WebAuthn plugin that
|
||||
wraps these.
|
||||
|
||||
**Decision required:** is passwordless a 2026 roadmap item?
|
||||
|
||||
---
|
||||
|
||||
### 36.F — Observability & perf measurement
|
||||
|
||||
#### 36.F.1 `web-vitals`
|
||||
|
||||
**Concrete pain:** we have no real-user perf data. We don't know our
|
||||
P75 LCP, P75 INP, or P75 CLS across our user base. Any future perf
|
||||
optimization (Cache Components, Tailwind 4, dynamic imports) is shooting
|
||||
in the dark without baseline measurement.
|
||||
|
||||
**What it adds:** Google's official Core Web Vitals library. Ships
|
||||
`onLCP`, `onINP`, `onCLS`, `onFCP`, `onTTFB` callbacks. Reports values
|
||||
once per page lifecycle.
|
||||
|
||||
**Land site:** `src/app/(dashboard)/layout.tsx` — wire a listener that
|
||||
POSTs vitals to `/api/v1/internal/vitals` (new endpoint, append to
|
||||
existing `client_metrics` table or similar). 30 LOC end-to-end.
|
||||
|
||||
**Effort:** 1h including backend logging. ~2kb. **High value** because
|
||||
without this we're guessing about perf wins.
|
||||
|
||||
#### 36.F.2 `pino-http`
|
||||
|
||||
**Concrete pain:** we have request logging via custom middleware. `pino-http`
|
||||
is the canonical pino HTTP request logger with automatic request-id
|
||||
propagation, response time, status code, and integration with our pino
|
||||
logger. Likely already partially implemented via our hand-rolled
|
||||
middleware.
|
||||
|
||||
**Effort:** check existing middleware first — may already cover this.
|
||||
|
||||
#### 36.F.3 `@sentry/nextjs` (revisit from section 35)
|
||||
|
||||
Covered in 35.B Tier 3. Adoption gated on the SaaS-dep decision.
|
||||
|
||||
---
|
||||
|
||||
### 36.G — TypeScript ergonomics
|
||||
|
||||
#### 36.G.1 `@total-typescript/ts-reset`
|
||||
|
||||
**Concrete pain:** TypeScript's stdlib types have well-known foot-guns:
|
||||
|
||||
- `Array.isArray(x)` narrows to `any[]` (drops the actual type).
|
||||
- `JSON.parse(s)` returns `any` (defeats type safety entirely).
|
||||
- `fetch().json()` returns `Promise<any>`.
|
||||
- `.filter(Boolean)` doesn't remove `null | undefined` from the type.
|
||||
- `Array.prototype.includes` is too strict on its argument.
|
||||
|
||||
ts-reset is **a single `.d.ts` import** (`import '@total-typescript/ts-reset'`)
|
||||
that fixes all of these globally. Used by Anthropic, Stripe, Vercel internally.
|
||||
|
||||
**Concrete impact:** likely catches 10-20 latent bugs across our 1000+
|
||||
TS files where someone called `JSON.parse(body)` and continued treating
|
||||
the result as a typed object without parsing through Zod.
|
||||
|
||||
**Effort:** 1 line in `src/types/globals.d.ts`. **dev-time only**, ships
|
||||
zero runtime.
|
||||
|
||||
#### 36.G.2 `type-fest`
|
||||
|
||||
**What it adds:** ~150 utility types (`SetRequired`, `SetOptional`,
|
||||
`PartialDeep`, `MergeDeep`, `Promisable`, `Jsonifiable`, etc.) that
|
||||
extend TypeScript's built-ins.
|
||||
|
||||
**Land sites:** anywhere we're hand-rolling `Omit<X, Y> & Pick<Z, W>`
|
||||
gymnastics — type-fest usually has a named util that's clearer.
|
||||
|
||||
**Effort:** opportunistic. ~0kb runtime (types only).
|
||||
|
||||
#### 36.G.3 `tsc-files`
|
||||
|
||||
**Concrete pain:** pre-commit hook runs ESLint on staged files (fast) but
|
||||
no type-check. Type errors slip through to CI.
|
||||
|
||||
**What it adds:** typecheck _only the staged TS files and their
|
||||
dependencies_, not the full repo. Drops a pre-commit hook from "skip
|
||||
because too slow" to "always on, sub-2-second."
|
||||
|
||||
**Land site:** `.husky/pre-commit` + `lint-staged.config.mjs` —
|
||||
`"*.ts": ["tsc-files --noEmit"]`.
|
||||
|
||||
**Effort:** 15 min.
|
||||
|
||||
---
|
||||
|
||||
### 36.H — In-browser PDF viewing
|
||||
|
||||
#### 36.H.1 `pdfjs-dist` + a viewer wrapper
|
||||
|
||||
**Concrete pain:** the docs hub (per CLAUDE.md) lets users upload and
|
||||
file PDFs. There's currently no in-app preview — clicking a file likely
|
||||
downloads it or opens in a new tab. A real CRM should preview the PDF
|
||||
inline.
|
||||
|
||||
**What it adds:**
|
||||
|
||||
- **`pdfjs-dist`** is Mozilla's pdf.js — the engine.
|
||||
- **`@react-pdf-viewer/core`** is the most feature-rich React wrapper
|
||||
(zoom, search, annotations).
|
||||
- Alternatively, **`react-pdf`** (Wojtek Maj's, not @react-pdf/renderer)
|
||||
is a lighter wrapper.
|
||||
|
||||
**Land site:** docs hub file detail / preview pane. EOI signing preview
|
||||
in admin.
|
||||
|
||||
**Effort:** 2-3h for a polished viewer with zoom + page nav. ~150kb gzip
|
||||
(pdf.js is unavoidable; lazy-load only when preview opens).
|
||||
|
||||
**Note vs section 35.A:** `@react-pdf/renderer` (generator) and `pdfjs-dist`
|
||||
(viewer) are complementary. We need both: one to _make_ PDFs, one to
|
||||
_show_ them.
|
||||
|
||||
---
|
||||
|
||||
### 36.I — Testing & development data
|
||||
|
||||
#### 36.I.1 `@faker-js/faker`
|
||||
|
||||
**Concrete pain:** seed data is currently hand-maintained (mostly).
|
||||
Faker would replace hand-rolled fake names, emails, addresses, phone
|
||||
numbers, vehicle/yacht names, dates, marina locations with reproducible,
|
||||
locale-aware fakes.
|
||||
|
||||
**Land site:** `src/lib/db/seed.ts`, `src/lib/db/seed-synthetic.ts`.
|
||||
|
||||
**Effort:** 1-2h. ~3MB gzip — **dev-only**, not shipped.
|
||||
|
||||
#### 36.I.2 `msw` (Mock Service Worker)
|
||||
|
||||
**Concrete pain:** integration tests that hit external services
|
||||
(Documenso, SMTP, IMAP) either skip in CI or fail intermittently.
|
||||
**`msw`** intercepts fetch/HTTP at the network layer in tests so we can
|
||||
mock external responses deterministically.
|
||||
|
||||
**Land site:** `tests/integration/` setup — wrap Documenso + SMTP
|
||||
clients with MSW handlers.
|
||||
|
||||
**Effort:** 2-3h. dev-only.
|
||||
|
||||
---
|
||||
|
||||
### 36.J — Workflow & state machines
|
||||
|
||||
#### 36.J.1 `@xstate/react`
|
||||
|
||||
Audit found only one multi-step flow (`send-document-dialog.tsx`).
|
||||
EOI signing has steps but they're sequential, not state-machine-y. The
|
||||
GDPR export job is a backend state machine but `bullmq` handles it.
|
||||
|
||||
**Verdict:** **not warranted right now.** Revisit if we build the
|
||||
client-onboarding flow or the multi-step EOI-with-multi-berth-and-
|
||||
payment-and-signing wizard the roadmap mentions.
|
||||
|
||||
---
|
||||
|
||||
### 36.K — Search & filtering
|
||||
|
||||
#### 36.K.1 Postgres-native FTS (no new package — schema migration)
|
||||
|
||||
**Concrete pain:** `search.service.ts` uses `LIKE '%term%'` on client/yacht/
|
||||
company tables. Slow at scale; doesn't rank.
|
||||
|
||||
**What we could add:** Postgres `tsvector` columns + `GIN` indexes + a
|
||||
single `to_tsquery()` call per search. This is **all native Postgres**
|
||||
— no new npm dep. Drizzle supports it via `sql\`...\`` template literals.
|
||||
|
||||
**Effort:** migration (30 min) + service refactor (2h) + e2e re-run.
|
||||
|
||||
#### 36.K.2 External search engines (`meilisearch`, `typesense`)
|
||||
|
||||
**Verdict:** overkill until we're past 100k clients per port. Postgres
|
||||
FTS will hold for years. **Defer indefinitely.**
|
||||
|
||||
---
|
||||
|
||||
### 36.L — Final updated adoption order (incorporating section 36)
|
||||
|
||||
Layered on section 35.E:
|
||||
|
||||
**Same-day adopts (low-risk, high-leverage):**
|
||||
|
||||
- **`@total-typescript/ts-reset`** — 1-line type-safety upgrade. Do this
|
||||
before any Zod 4 work — it'll catch latent bugs along the way.
|
||||
- **`web-vitals`** — establish perf baseline before any optimization.
|
||||
- **`@hookform/devtools`** — dev-only DX win.
|
||||
|
||||
**Adopt alongside section 35.B Tier 1:**
|
||||
|
||||
- **`p-limit`** — pair with the section 35 mass-operation refactors. The
|
||||
Documenso bulk-send path is the highest-priority site.
|
||||
- **`@tanstack/query-broadcast-client-experimental`** — 1-liner in the
|
||||
query provider.
|
||||
|
||||
**Adopt with mobile/UX work:**
|
||||
|
||||
- **`browser-image-compression`** — wire into scan-shell first.
|
||||
- **`embla-carousel-react`** + **`yet-another-react-lightbox`** — pair
|
||||
with berth/yacht photo gallery work.
|
||||
- **`react-resizable-panels`** — pair with docs hub UX work.
|
||||
- **`@use-gesture/react`** — pair with kanban-on-mobile polish.
|
||||
|
||||
**Adopt with security pass:**
|
||||
|
||||
- **`isomorphic-dompurify`** — replaces hand-rolled escapeHtml. Pair
|
||||
with the audit's XSS hardening pass.
|
||||
|
||||
**Adopt with the docs hub Phase 2:**
|
||||
|
||||
- **`pdfjs-dist`** + viewer wrapper — when in-app PDF preview becomes a
|
||||
user request.
|
||||
|
||||
**Park / defer:**
|
||||
|
||||
- `partysocket` (requires Socket.IO investigation first).
|
||||
- `@xstate/react` (no current target).
|
||||
- External search engines.
|
||||
- WebAuthn / passkeys (roadmap decision).
|
||||
|
||||
---
|
||||
|
||||
### 36.M — Final summary
|
||||
|
||||
The first sweep (section 35) found the headline replacements:
|
||||
**Zod 4 + drizzle-zod + react-email + @react-pdf/renderer** is the
|
||||
single highest-leverage week of work.
|
||||
|
||||
This second sweep (section 36) found the **operational hardening
|
||||
layer**:
|
||||
|
||||
- **`p-limit` family** for the 74 unbounded `Promise.all` sites.
|
||||
- **`@total-typescript/ts-reset`** for free type safety across 1000+ files.
|
||||
- **`web-vitals`** to establish a perf baseline before we optimize.
|
||||
- **`isomorphic-dompurify`** to harden the email/template rendering.
|
||||
- **`browser-image-compression`** for mobile bandwidth / battery.
|
||||
- **`@tanstack/query-broadcast-client-experimental`** for free cross-tab
|
||||
cache sync.
|
||||
- **`react-resizable-panels`** + **`embla-carousel-react`** +
|
||||
**`yet-another-react-lightbox`** for the photo/preview surfaces.
|
||||
|
||||
Together with section 35, this gives us a concrete shopping list of
|
||||
~20 packages with explicit land-sites in our code and effort estimates,
|
||||
plus 5-6 cleanup-candidate removals. Adopting all of them would shed
|
||||
~600 LOC of hand-rolled code, eliminate ~5 categories of latent bugs
|
||||
(timezone, XSS, race conditions, type stdlib quirks, missing
|
||||
exhaustiveness), and meaningfully improve mobile UX + perf measurability.
|
||||
|
||||
---
|
||||
|
||||
**Bottom line:** the deps audit (section 34) showed we're secure today.
|
||||
This section (35) shows where we can make the codebase _meaningfully better_
|
||||
— smaller, cleaner, more declarative — by leveraging packages we don't yet
|
||||
use. The single highest-leverage move is **Zod 4 + drizzle-zod + react-email
|
||||
in the same focused day**: it kills the validator-drift problem, lands the
|
||||
14× parse-perf win, and starts paying down the hand-strung-email-templates
|
||||
debt all at once. The PDF stack overhaul (35.A) is the second-highest-leverage
|
||||
move: removing pdfme + the 571-line Tiptap bridge in favor of declarative
|
||||
React components is a category-of-bug eliminator, not just a refactor.
|
||||
|
||||
Reference in New Issue
Block a user