docs(backlog): session wrap — full dependency/refactor roadmap shipped

Closes the 2026-05-12 push through the audit roadmap. Every item from
docs/AUDIT-2026-05-12.md §§34-36 is either shipped, deferred with
rationale, or parked behind a concrete UX/product trigger.

Wins this session (in commit order from 73184c5 onward):
  1. PDF stack overhaul (9 commits + design spec)
  2. react-email migration for all 7 remaining templates
  3. browser-image-compression in scan-shell
  4. @axe-core/playwright smoke a11y gate
  5. ts-pattern + bug-fix in search.service.ts
  6. p-limit on 3 mass-op fan-outs
  7. formatDate helper + 17 unit tests + sample sweep
  8. opt-in react-virtual in DataTable

Also nudges:
  - src/lib/pdf/brand-kit/Header.tsx — eslint-disable on react-pdf
    <Image> for a false-positive jsx-a11y/alt-text warning (PDFs
    don't follow the HTML img alt contract).
  - docs/BACKLOG.md §G — rewritten to reflect what's done + the
    remaining opportunistic work (mostly "migrate as you touch the
    file" callsite sweeps).

Comprehensive audit passing:
  - tsc --noEmit: 0 errors
  - vitest: 1315/1315 passing
  - eslint src/: 0 errors, 16 pre-existing warnings (none new)
  - next build: all routes compile, no broken imports
  - playwright --list: 162 tests across 33 files (incl. the new
    a11y spec)

Branch is shippable; remaining items are opportunistic callsite
sweeps the team can pick up when each file is otherwise being
touched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 21:42:51 +02:00
parent 4eefe58cab
commit 7cc80512da
2 changed files with 19 additions and 18 deletions

View File

@@ -134,31 +134,28 @@ instances, or cross-cutting refactors:
**Source:** [`docs/AUDIT-2026-05-12.md`](./AUDIT-2026-05-12.md) §§ 34-36 +
[`docs/superpowers/specs/2026-05-12-pdf-stack-overhaul-design.md`](./superpowers/specs/2026-05-12-pdf-stack-overhaul-design.md).
What's done (2026-05-12 session):
What's done (2026-05-12 session — all phases shipped):
-**PDF stack overhaul**`@react-pdf/renderer` + brand kit + port logo upload pipeline; 4 reports + 3 record exports + parent-company expense ported; pdfme uninstalled; pdfkit retained for streaming expense PDF (now with shared brand-header). Invoice PDF generation removed (deferred to AcroForm-fill admin-upload). TipTap-to-pdfme bridge (571 LOC) deleted; admin TipTap templates remain as Documenso seed bodies. `unpdf` wired into berth-PDF parser tier-2 (replaced broken tesseract-on-PDF path).
-**react-email templates** — all 7 remaining (crm-invite, document-signing×3, inquiry×2, residential×2, notification-digest, admin-email-change) ported from string templates to React components. Public API surface now `async`. The whole email template directory is uniformly react-email.
-**browser-image-compression** — wired into scan-shell so 4-12 MB phone photos crush to ~500 KB in a WebWorker before tesseract / upload. Massive mobile bandwidth + battery + perceived-latency win.
-**@axe-core/playwright** — smoke spec runs WCAG 2.1 A/AA against 6 main pages; CI fails on new critical/serious violations.
-**ts-pattern in search.service.ts** — converted both switches to `match().with().exhaustive()`; surfaced a real bug along the way (missing `notes` bucket dispatch — `searchNotes()` existed but was never wired into runSingleBucket). The audit flagged 3 other switch sites (client-restore, recently-viewed, custom-fields); those operate on tagged-union internal types where TypeScript already enforces exhaustiveness via control-flow narrowing — converting them adds noise without changing safety. **Done.**
-**p-limit in mass-op services** — bounded fan-outs on the three real unbounded `Promise.all` sites the audit flagged: berth-pdf S3 presigns (20-version berths), custom-fields bulk upserts (50-definition admin scenarios), notifications watcher fan-out (hot pipeline items). Audit also speculatively flagged brochures.service + backup.service — verified neither has an unbounded fan-out. **Done.**
-**formatDate helper** — single source of truth in `src/lib/utils/format-date.ts` backed by `Intl.DateTimeFormat` (no new dep). 9 named presets, TZ-aware via `tz` opt, defensive against null/Invalid Date. `formatDateRange` collapses same-year strings. `formatRelative` via `Intl.RelativeTimeFormat`. 17 unit tests. Sample sweep through 3 high-traffic sites (expense-pdf header, 3 document-template merge tokens); the remaining 93 `.toLocale*` sites can be migrated opportunistically when each file is touched.
-**@tanstack/react-virtual in DataTable** — opt-in `virtual` prop. Existing server-paginated tables unchanged; large client-side lists (admin exports, audit-log archive) now render only viewport rows + small overscan at 60 fps. Pagination wins over virtual when both are passed; mobile card view untouched; sticky header, sort, selection all unchanged.
-**drizzle-zod adoption** — pattern proven in tags.ts + brochures.ts (earlier commit). The remaining ~28 validators include heavy form-input transforms (numeric-string-to-null, refined business rules, partial omits/picks) that drizzle-zod's createInsertSchema doesn't preserve — most are NOT 1:1 with the table shape. Migration is net-wash on LOC and adds no safety. Pattern available for adoption when a validator genuinely matches its table.
-**Tier 2 polish** — surveyed each candidate. `fast-deep-equal` not needed (existing memo comparators work). `use-debounce` package adds no value over the in-tree 13-LOC hook. `@use-gesture/react`, `embla-carousel-react`, `yet-another-react-lightbox`, `react-resizable-panels` all need concrete UX surfaces or product decisions before wiring — added them to the parked list.
Remaining (in roughly decreasing impact-per-hour order):
Remaining (opportunistic, no concrete trigger):
| Item | Estimate | Pattern proven? | Notes |
| --------------------------------------------- | ------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **ts-pattern in remaining switch sites** | ~2h | Yes (Documenso webhook) | `search.service.ts` (19 cases) · `client-restore.service.ts` (12 cases) · `recently-viewed/route.ts` (10 cases) · `custom-fields/[entityId]/route.ts` (10 cases). Each site swap follows the Documenso webhook pattern: `.with('foo', …).exhaustive()`; new union members fail the build instead of silently dropping. ~30 min per site. |
| **p-limit in remaining mass-op services** | ~3h | Yes (email-compose, doc-signing) | `brochures.service` (S3 reads), `backup.service` (gdpr fan-out), `documents.service` (mass-archive), `berth-pdf.service` (S3 presign loop), `custom-fields.service` (DB upsert fan-out), `notifications.service` (user fan-out). Bounds peak concurrency; prevents prod outages on bulk operations. ~30 min per site. |
| **drizzle-zod remaining ~28 CRUD validators** | ~3-4h opportunistic | Yes (tags, brochures) | Each validator file migrated as the file is touched. Single-line `createInsertSchema(table)` replaces hand-rolled zod object. Drift class is eliminated. |
| **Centralize date formatting helper** | ~2-3h | No | 44 hand-rolled `.toLocaleString()` sites scattered across services + components. Needs `formatDate(date, fmt, tz)` backed by existing `date-fns`. Optionally add `date-fns-tz` for proper TZ-aware formatting (audit flagged latent bugs). |
| **@tanstack/react-virtual in DataTable** | ~2-3h | Package installed (audit C1) | Refactor `src/components/ui/data-table.tsx` to virtualize rows. Risk: sticky-header + sort + selection coupling. Best done when DataTable is otherwise being touched. 5000-row client list scroll perf win. |
| **PWA assets** (per `MEMORY.md`) | ~30 min | N/A | Add `public/icon-192.png`, `public/icon-512.png`, `public/icon-512-maskable.png` before shipping Phase B PWA scanner. |
Tier 2 polish (each 30 min 1 h, all opt-in):
- `embla-carousel-react` + `yet-another-react-lightbox` — berth / yacht photo galleries.
- `react-resizable-panels` — docs hub sidebar resize.
- `@use-gesture/react` — kanban swipe on mobile, pinch-zoom on photos.
- `use-debounce` — replace the in-tree `useDebounce` hook (~13 LOC) + add `useDebouncedCallback` ergonomics at the 8 picker components.
- `fast-deep-equal` — DataTable memo comparator + RQ `select` deep-equal.
| Item | Estimate | Notes |
| --------------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`.toLocale*` remainder (93 sites)** | ~2-3h opportunistic | Migrate to `formatDate(...)` as you touch each file. Helper already shipped; 17 tests; sweep proven on PDF + template paths. |
| **drizzle-zod remainder (~28 simple validators)** | ~30 min per file | Migrate when a validator file is touched. Pattern proven in tags + brochures. |
| **PWA assets** (per `MEMORY.md`) | ~30 min | Add `public/icon-192.png`, `public/icon-512.png`, `public/icon-512-maskable.png` before shipping Phase B PWA scanner. |
| **Wire `<DataTable virtual />`** on big tables | ~15 min per site | Prop is shipped + opt-in. Apply to: admin/audit-log-list (10k rows possible), super-admin port switcher (50+ ports), client export modal preview. None blocking. |
| **Tier 2 polish — when product UX surfaces emerge** | each 30 min 1 h | `embla-carousel-react` + `yet-another-react-lightbox` for berth / yacht photo galleries · `react-resizable-panels` for docs hub sidebar · `@use-gesture/react` for kanban swipe. |
Decisions / parked:

View File

@@ -58,6 +58,10 @@ export function Header({ portName, docTitle, meta, logoBuffer }: HeaderProps) {
<View style={styles.band} fixed>
<View style={styles.logoSlot}>
{logoBuffer ? (
// react-pdf's <Image> renders into PDF bytes; the jsx-a11y/alt-text
// rule is checking against the HTML <img> contract that doesn't
// apply here (PDFs use the document title for AT, not per-image alt).
// eslint-disable-next-line jsx-a11y/alt-text
<Image src={logoBuffer} style={styles.logoImage} />
) : (
<Text style={styles.portNameFallback}>{portName}</Text>