Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

174 lines
10 KiB
Markdown
Raw Permalink 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.

# PRD — Tickets List Load-Time Reduction (bundle/hydration)
**Status:** Draft for review
**Owner:** TBD
**Created:** 2026-06-15
**Plan slug:** 2026-06-15-tickets-list-bundle-reduction
---
## 1. Problem statement & user value
The MSP ticketing dashboard (`/msp/tickets`) is slow to become usable. A production
performance trace (algapsa.com, warm cache) shows:
- **LCP 3,048 ms**, of which **2,774 ms (91%) is render delay** — the browser receives
HTML quickly (TTFB 274 ms) but can't paint until it finishes executing the route's JS.
- **~9.3 MB of decoded JavaScript across 103 chunks** hydrating on the list route.
- Backend is **not** the bottleneck: server actions return in ~2427 ms.
- The list route eagerly bundles heavy client code that is **not on screen at first paint**:
- 7 dialogs imported only by the list (`BulkAssign/Tags/DueDate/Status/Priority`, `TicketImportDialog`, `TicketExportDialog`).
- The full `ClientDetails` component (2,215 lines + all client tabs) for the side-drawer quick view, even though the drawer only ever shows the first ("details") tab.
- Each list view also fires **48 redundant RSC prefetch requests** (two `<Link>`s per
row × ~24 rows) that force the server to render ticket detail pages nobody asked for.
**User value:** the tickets list paints and becomes interactive noticeably faster, and the
server stops doing wasted detail-page renders on every list view.
## 2. Goals
- **G1 (primary):** Reduce tickets-list initial JavaScript / hydration cost and improve
LCP / render delay, measured by build output (list route first-load JS) and a repeat
production-style performance trace.
- **G2:** Remove the redundant per-row RSC prefetch traffic from the list.
- **G3:** Replace the full-weight client drawer with a genuinely lightweight
`ClientQuickView`, and reuse it at **every** client quick-view call site.
- **G4:** Move the 7 list-only dialogs out of the list route bundle via route-level
code splitting (static imports only).
## 3. Non-goals
- **No `next/dynamic` / `React.lazy` / `import()` code-splitting.** Architectural
decision: the team avoids dynamic imports to keep module boundaries clean.
- **No changes to `QuickAddTicket` or its rich-text editor.** It is shared across 9
surfaces (including the global header quick-create), so list-only extraction yields
little app-wide benefit. Tracked as a possible separate future plan.
- **No change to `QuickAddCategory`** — shared by 4 components, not cleanly list-only.
- App-shell server-action burst (~20 POSTs during hydration) and the 15 s job-metrics
poll (`Header.tsx`) — real but shell-level; out of scope here.
- Deep-linkable / refresh-safe dialog URLs are **not a goal** (acceptable side effect of
intercepting routes, but we will not invest extra to guarantee it).
## 4. Hard constraints
- **C1 — No visual change for end users.** Dialogs must still appear as modal overlays on
top of the list (not full-page navigations). The client drawer must look identical.
- **C2 — Dialogs must respect all current list filters** (and, for bulk actions, the
current row selection), exactly as today.
- **C3 — No dynamic imports** (see non-goals).
- **C4 — Multi-tenant safety unchanged.** This is primarily a UI/bundling refactor; any
server actions reused by new routes keep their existing `tenant` scoping.
> **Routing decision (derived from C1 + C2 + C3):** Plain separate full pages are ruled
> out (they change the UX from modal → full navigation, violating C1). Keeping the dialogs
> inside the list component keeps them in the list bundle (fails G1/G4). With dynamic
> imports off the table (C3), the only approach that satisfies all three is **Next.js
> intercepting routes + a parallel `@modal` slot**: each dialog lives in its own route
> segment (so it's a separate, statically-imported route bundle) but renders as a modal
> overlay over the still-mounted list. This pattern does not exist in the codebase yet.
## 5. Affected users / flows
- MSP internal users on the tickets list: open Quick View on a client, run bulk actions on
selected tickets, import/export tickets. All flows must look and behave the same.
- Any user hitting the other client quick-view surfaces (clients list, billing dashboard,
ticket detail drawer, projects, interactions, user list) — they get the new lighter
component with identical appearance.
## 6. Approach (workstreams)
### A. Row-link prefetch fix (smallest, immediate)
- Add `prefetch={false}` to the two `<Link>`s in
`packages/tickets/src/lib/ticket-columns.tsx` (ticket-number col ~line 219, title col
~line 273). Clicks are already intercepted (`e.preventDefault()``onTicketClick`), and
middle-click / open-in-new-tab keep working via the retained `href`.
### B. Lightweight `ClientQuickView` + reuse everywhere
- Rebuild `packages/clients/src/components/clients/ClientQuickView.tsx` so it imports only
the "details" tab content (what `ClientDetails` renders when `quickView`
`tabContent[0]`), **not** the full `ClientDetails` module (which statically imports
contacts list, billing config, interactions feed, notes panel, and 3 Hudu tabs).
- Extract the shared details-tab content into a small component so both `ClientDetails`
(full page) and `ClientQuickView` can use it without `ClientQuickView` dragging in the
other tabs.
- Wire **all** client quick-view call sites to `ClientQuickView`:
- `packages/msp-composition/src/tickets/MspTicketsPageClient.tsx:18`
- `packages/msp-composition/src/tickets/MspTicketDetailsContainerClient.tsx:71`
- `packages/msp-composition/src/clients/MspClientDrawerProvider.tsx:36`
- `packages/msp-composition/src/billing/MspBillingDashboardClient.tsx:18`
- `packages/msp-composition/src/projects/MspClientIntegrationProvider.tsx:44`
- `packages/clients/src/components/clients/Clients.tsx:1779`
- `packages/clients/src/components/interactions/InteractionDetails.tsx:209`
- `packages/projects/src/components/Projects.tsx` (renderClientDetails, ~1024)
- `server/src/components/settings/general/UserList.tsx:206`
- (verify contact-context `<ClientDetails>` usages: `Contacts.tsx:697`,
`MspContactTickets.tsx:230` — classify as client quick view or leave)
### C. Route-extract the 7 list-only dialogs (intercepting + parallel routes)
- Add `server/src/app/msp/tickets/layout.tsx` hosting `{children}` + a `@modal` parallel
slot, and `server/src/app/msp/tickets/@modal/default.tsx` returning `null`.
- Create route segments for each dialog under `msp/tickets/...` with an intercepting
`(.)` variant in `@modal` that renders the dialog as an overlay, plus a plain segment
for the hard-load fallback.
- Triggers on the list become navigations (`router.push` / `<Link>`) to those routes
instead of toggling local `isOpen` state; the dialog components move into the route
segments so they leave the list route's module graph.
- **Shared state:** introduce a tickets-segment client context provider (in
`tickets/layout.tsx`) holding the current **filters** and **selected ticket ids**, so the
intercepting modal routes (sibling subtrees that cannot read the list page's local state)
can honor C2. The list writes to it; dialog routes read from it. Filters are already
partly URL-synced (`TicketingDashboard.tsx:510,625`) and can be read from `searchParams`
as a backstop.
- Dialog order by effort/benefit: **Import → Export** (no selection dependency, easiest,
likely heaviest) first; then the **5 bulk dialogs** (require the selection context).
## 7. Data model / API notes
No schema changes. Existing server actions (bulk assign/tags/due-date/status/priority,
import, export, client fetch) are reused unchanged and keep their `tenant` scoping.
Export/bulk-select-all must continue to pass the current `ITicketListFilters` to the
server action so results respect filters (C2).
## 8. Risks & mitigations
- **R1 — Intercepting/parallel routes are a new pattern here.** Risk of focus/scroll/
back-button regressions and SSR quirks. *Mitigation:* prototype one dialog (Import)
end-to-end first; add a `default.tsx`; manual + e2e checks for open/close/refresh/back.
- **R2 — Selection-state lifting is the highest-effort item.** Selection lives deep in
`TicketingDashboard` (`selectedTicketIds`, line 252). *Mitigation:* lift to a small
tickets-scoped context; if a bulk dialog's effort:benefit is poor, it may stay in-list
(re-evaluate during A/B; user scoped "all" but ROI matters).
- **R3 — Filter fidelity (C2).** Modal routes must see the exact active filters.
*Mitigation:* shared context is source of truth; e2e assert each dialog acts on filtered
set.
- **R4 — `ClientQuickView` is reused at ~10 sites.** Rebuilding it risks regressions
across clients/billing/projects/tickets. *Mitigation:* keep visual output identical to
current `quickView` details tab; verify each call site.
- **R5 — "No visual change" (C1).** Modal-from-route must match current modal styling/
behavior. *Mitigation:* reuse the existing `Dialog` components inside the route segments.
## 9. Success metrics / Definition of Done
- **DoD-1:** Production-style repeat trace of `/msp/tickets` shows reduced render delay and
LCP vs. the 2,774 ms / 3,048 ms baseline (record exact numbers in SCRATCHPAD).
- **DoD-2:** `next build` shows the tickets-list route **first-load JS reduced**; the 7
dialogs and full `ClientDetails` no longer appear in the list route chunk graph.
- **DoD-3:** Loading the list fires **0** per-row RSC prefetch requests; clicking a ticket
still opens it; middle-click/open-in-new-tab still work.
- **DoD-4:** Every client quick-view surface renders the lightweight `ClientQuickView` and
looks identical to today.
- **DoD-5:** Each dialog opens as a modal overlay (no full-page nav), respects current
filters/selection, and performs its action correctly. No visual change reported.
- **DoD-6:** No `next/dynamic` / `React.lazy` introduced.
## 10. Open questions
- OQ1: Confirm the intercepting-routes approach is acceptable as a new pattern (it's the
only option satisfying C1+C2+C3). If not, we must relax C1 (allow full pages) or C3.
- OQ2: Is there an existing client-side store pattern (zustand/jotai/context) preferred for
the tickets selection/filter context, or should we add a plain React context?
- OQ3: For bulk dialogs with poor effort:benefit, is leaving them in-list acceptable, or is
full extraction of all 7 required?
- OQ4: Should `Contacts.tsx:697` / `MspContactTickets.tsx:230` `<ClientDetails>` usages be
migrated to `ClientQuickView` too, or are they intentionally full?