feat: timeline multi-select, demand popover, resource hover card, merged tooltips, dark mode fixes
Major timeline enhancements: - Right-click drag multi-selection with floating action bar (batch delete/assign) - DemandPopover for demand strip details (replaces broken "Loading" modal) - ResourceHoverCard on name hover showing skills, rates, role, chapter - Merged heatmap+vacation tooltips into unified TimelineTooltip component - Fixed overbooking blink animation (date normalization, z-index ordering) - Fixed dark mode sticky column bleed-through in project view - System roles admin page, notification task management, performance review docs Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -0,0 +1,417 @@
|
||||
# Performance Optimization Review
|
||||
|
||||
Date: 2026-03-18
|
||||
|
||||
Scope: analysis only. No runtime behavior was changed in this pass.
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The biggest performance costs in Planarchy currently come from three patterns:
|
||||
|
||||
1. Broad data fetches followed by repeated in-memory filtering/grouping.
|
||||
2. Expensive client-side derivations on screens with large datasets, especially Timeline.
|
||||
3. Broad cache invalidation that forces heavy queries to refetch and recompute after small mutations.
|
||||
|
||||
The highest-value optimization target is the Timeline stack. After that, the best wins are server-side aggregation for chargeability/dashboard workloads and narrowing resource/report queries to visible scope.
|
||||
|
||||
## Main Hotspots
|
||||
|
||||
### 1. Timeline
|
||||
|
||||
Relevant files:
|
||||
|
||||
- [TimelineContext.tsx](/home/hartmut/Documents/Copilot/planarchy/apps/web/src/components/timeline/TimelineContext.tsx)
|
||||
- [TimelineView.tsx](/home/hartmut/Documents/Copilot/planarchy/apps/web/src/components/timeline/TimelineView.tsx)
|
||||
- [TimelineResourcePanel.tsx](/home/hartmut/Documents/Copilot/planarchy/apps/web/src/components/timeline/TimelineResourcePanel.tsx)
|
||||
- [TimelineProjectPanel.tsx](/home/hartmut/Documents/Copilot/planarchy/apps/web/src/components/timeline/TimelineProjectPanel.tsx)
|
||||
- [timeline.ts](/home/hartmut/Documents/Copilot/planarchy/packages/api/src/router/timeline.ts)
|
||||
|
||||
Observed issues:
|
||||
|
||||
- `TimelineContext` derives multiple large collections in the client from the same raw payload.
|
||||
- Resource and project filtering is partly pushed to the backend, but large read-model payloads are still constructed and further filtered/grouped in the browser.
|
||||
- Timeline mutations invalidate multiple heavy queries at once.
|
||||
- Timeline SSE events also trigger broad invalidations.
|
||||
|
||||
Impact:
|
||||
|
||||
- Slow initial render.
|
||||
- Lag on interaction and hover.
|
||||
- Slow recovery after edits because the full timeline dataset is often fetched again.
|
||||
|
||||
### 2. Chargeability Report
|
||||
|
||||
Relevant files:
|
||||
|
||||
- [ChargeabilityReportClient.tsx](/home/hartmut/Documents/Copilot/planarchy/apps/web/src/components/reports/ChargeabilityReportClient.tsx)
|
||||
- [chargeability-report.ts](/home/hartmut/Documents/Copilot/planarchy/packages/api/src/router/chargeability-report.ts)
|
||||
|
||||
Observed issues:
|
||||
|
||||
- The server loads matching resources, then all bookings in range, then repeatedly filters bookings and vacations per resource and per month.
|
||||
- The client renders a wide report grid and still performs filtering/grouping locally.
|
||||
- Public holiday support is still marked as TODO in the route, so future correctness work will likely add more cost unless the data model is optimized first.
|
||||
|
||||
Impact:
|
||||
|
||||
- CPU-heavy report generation.
|
||||
- Larger-than-needed payloads.
|
||||
- Increased render cost on wide month ranges.
|
||||
|
||||
### 3. Dashboard Chargeability / Analytics Widgets
|
||||
|
||||
Relevant files:
|
||||
|
||||
- [dashboard.ts](/home/hartmut/Documents/Copilot/planarchy/packages/api/src/router/dashboard.ts)
|
||||
- [get-chargeability-overview.ts](/home/hartmut/Documents/Copilot/planarchy/packages/application/src/use-cases/dashboard/get-chargeability-overview.ts)
|
||||
- [get-overview.ts](/home/hartmut/Documents/Copilot/planarchy/packages/application/src/use-cases/dashboard/get-overview.ts)
|
||||
|
||||
Observed issues:
|
||||
|
||||
- `getDashboardChargeabilityOverview()` fetches all relevant resources and all bookings for the month, then filters the full booking set again for each resource.
|
||||
- Similar patterns exist in dashboard summary use cases where relatively small widgets are backed by broad read logic.
|
||||
|
||||
Impact:
|
||||
|
||||
- Avoidable server CPU and latency.
|
||||
- Widgets become more expensive as the database grows even if the UI remains small.
|
||||
|
||||
### 4. Resources Screen
|
||||
|
||||
Relevant files:
|
||||
|
||||
- [ResourcesClient.tsx](/home/hartmut/Documents/Copilot/planarchy/apps/web/src/app/(app)/resources/ResourcesClient.tsx)
|
||||
- [resource.ts](/home/hartmut/Documents/Copilot/planarchy/packages/api/src/router/resource.ts)
|
||||
|
||||
Observed issues:
|
||||
|
||||
- Infinite scrolling is in place, which is good, but the screen still does substantial client-side sort/filter/order work.
|
||||
- Separate global stats queries can be expensive when only the currently visible rows need enriched values.
|
||||
- Large dropdown/filter datasets are fully loaded to support selection UIs.
|
||||
|
||||
Impact:
|
||||
|
||||
- Good enough at small scale, but it will degrade with larger teams and imported planning data.
|
||||
|
||||
### 5. Shared Booking Read Model
|
||||
|
||||
Relevant files:
|
||||
|
||||
- [list-assignment-bookings.ts](/home/hartmut/Documents/Copilot/planarchy/packages/application/src/use-cases/allocation/list-assignment-bookings.ts)
|
||||
|
||||
Observed issues:
|
||||
|
||||
- This is a clean shared entry point, but it returns broad booking rows and leaves most grouping/aggregation to callers.
|
||||
- Several heavy endpoints call this and then repeatedly scan the returned array.
|
||||
|
||||
Impact:
|
||||
|
||||
- Logic is easy to reuse, but performance scales poorly because each consumer re-aggregates independently.
|
||||
|
||||
## Current Database Index Baseline
|
||||
|
||||
Relevant schema:
|
||||
|
||||
- [schema.prisma](/home/hartmut/Documents/Copilot/planarchy/packages/db/prisma/schema.prisma)
|
||||
|
||||
Good coverage already exists for:
|
||||
|
||||
- `Assignment`: `resourceId`, `projectId`, `status`, date ranges, and composite indices.
|
||||
- `DemandRequirement`: `projectId`, `status`, date ranges.
|
||||
- `Vacation`: `resourceId`, `status`, date ranges.
|
||||
- `Resource`: `chapter`, `isActive`, `countryId`, `orgUnitId`, `resourceType`.
|
||||
- `Project`: `status`, date range, `orderType`, `clientId`.
|
||||
|
||||
Likely gaps for current query patterns:
|
||||
|
||||
- `Resource` filters used together in analytics screens: `isActive`, `chgResponsibility`, `departed`, `rolledOff`, `countryId`, `managementLevelGroupId`.
|
||||
- `Project` filters used in timeline/report contexts: `clientId` combined with active date windows.
|
||||
- Potential overlap-heavy access paths where PostgreSQL may benefit more from specialized query shapes or materialized read models than from adding more conventional B-tree indexes.
|
||||
|
||||
Conclusion:
|
||||
|
||||
The schema is not unindexed. The main issue is query shape and repeated in-memory work, not just missing indexes. Index work should support query redesign, not replace it.
|
||||
|
||||
## Recommended Optimizations
|
||||
|
||||
### A. Move Timeline Derivations Closer to the Backend
|
||||
|
||||
Proposal:
|
||||
|
||||
- Add a timeline read endpoint that returns data already grouped for the active view mode and active filters.
|
||||
- Return normalized maps and compact slices instead of large repeated nested objects.
|
||||
- Keep the raw endpoint only if another screen truly needs it.
|
||||
|
||||
Pros:
|
||||
|
||||
- Largest likely user-visible win.
|
||||
- Reduces browser CPU, garbage collection, and hover lag.
|
||||
- Shrinks payload size if redundant nested fields are removed.
|
||||
|
||||
Cons:
|
||||
|
||||
- More backend read-model code.
|
||||
- Slightly higher coupling between API shape and timeline UI needs.
|
||||
- Requires careful migration to avoid breaking existing interactions.
|
||||
|
||||
Recommended priority: P0
|
||||
|
||||
Estimated effort: medium to large
|
||||
|
||||
### B. Build Indexed Maps Once Instead of Repeated Array Scans
|
||||
|
||||
Proposal:
|
||||
|
||||
- In timeline/report/dashboard code, replace repeated `.filter()`, `.find()`, and `.some()` passes with prebuilt maps keyed by `resourceId`, `projectId`, `monthKey`, and status where appropriate.
|
||||
- Standardize this as shared helper utilities for read-model construction.
|
||||
|
||||
Pros:
|
||||
|
||||
- Low-risk improvement.
|
||||
- Can be introduced incrementally.
|
||||
- Helps both client and server hot paths.
|
||||
|
||||
Cons:
|
||||
|
||||
- Does not solve oversized payloads by itself.
|
||||
- Adds some code complexity if done ad hoc instead of through shared helpers.
|
||||
|
||||
Recommended priority: P0
|
||||
|
||||
Estimated effort: small to medium
|
||||
|
||||
### C. Replace Broad Invalidations with Scoped Cache Updates
|
||||
|
||||
Proposal:
|
||||
|
||||
- Audit timeline mutations and SSE handlers that currently invalidate `getEntries`, `getEntriesView`, `getProjectContext`, and `getBudgetStatus` together.
|
||||
- Invalidate only the affected query keys.
|
||||
- Where practical, patch query cache locally after small edits instead of refetching everything.
|
||||
|
||||
Pros:
|
||||
|
||||
- Faster feedback after drag/drop and edit flows.
|
||||
- Less network churn.
|
||||
- Lower server load during active planning sessions.
|
||||
|
||||
Cons:
|
||||
|
||||
- Cache correctness becomes more complex.
|
||||
- Needs strong regression coverage around drag/drop and split allocation flows.
|
||||
|
||||
Recommended priority: P0
|
||||
|
||||
Estimated effort: medium
|
||||
|
||||
### D. Push Chargeability Aggregation into Dedicated Server Read Models
|
||||
|
||||
Proposal:
|
||||
|
||||
- Replace repeated per-resource filtering of a flat booking array with server-side grouped structures.
|
||||
- Precompute `bookingsByResource` and `vacationsByResource` once before month iteration.
|
||||
- Consider a dedicated monthly chargeability read model for reports and dashboard widgets.
|
||||
|
||||
Pros:
|
||||
|
||||
- Strong improvement for reports and widgets.
|
||||
- Eliminates duplicate aggregation logic across dashboard/resource/report endpoints.
|
||||
- Better foundation for public holiday integration.
|
||||
|
||||
Cons:
|
||||
|
||||
- Requires consolidating overlapping business logic.
|
||||
- Read-model duplication is possible if not designed carefully.
|
||||
|
||||
Recommended priority: P1
|
||||
|
||||
Estimated effort: medium
|
||||
|
||||
### E. Narrow Data Fetches to Visible Scope
|
||||
|
||||
Proposal:
|
||||
|
||||
- Avoid loading large supporting datasets such as `resource.list({ limit: 500 })` or “all projects” queries when only label resolution for selected IDs is required.
|
||||
- Add lightweight lookup endpoints like `getByIds()` or `resolveLabels()`.
|
||||
- On the resources page, fetch chargeability/stat enrichments only for the visible page or current filtered result slice.
|
||||
|
||||
Pros:
|
||||
|
||||
- Smaller payloads.
|
||||
- Faster filter bar and screen startup.
|
||||
- Simple, targeted changes.
|
||||
|
||||
Cons:
|
||||
|
||||
- More API endpoints or variants.
|
||||
- Some UI components become slightly more stateful.
|
||||
|
||||
Recommended priority: P1
|
||||
|
||||
Estimated effort: small to medium
|
||||
|
||||
### F. Add Read-Optimized Analytics Endpoints or Materialized Views
|
||||
|
||||
Proposal:
|
||||
|
||||
- For expensive monthly analytics, introduce a read-optimized table or materialized view refreshed on schedule or after import batches.
|
||||
- Use this for dashboard and chargeability summaries, not for transactional edit screens.
|
||||
|
||||
Pros:
|
||||
|
||||
- Best long-term scalability for imported dispo-scale data.
|
||||
- Predictable response times for reports.
|
||||
- Reduces repeated heavy joins and calculations.
|
||||
|
||||
Cons:
|
||||
|
||||
- More infrastructure and refresh logic.
|
||||
- Risk of stale analytics if refresh semantics are unclear.
|
||||
- Higher implementation complexity than direct query improvements.
|
||||
|
||||
Recommended priority: P2
|
||||
|
||||
Estimated effort: large
|
||||
|
||||
### G. Add Composite Indexes Only Where Query Patterns Justify Them
|
||||
|
||||
Proposal:
|
||||
|
||||
- Review actual PostgreSQL query plans for:
|
||||
- resource analytics filters
|
||||
- timeline date-overlap filters
|
||||
- project/client/date combinations
|
||||
- Add composite indexes only after measuring the slow plans.
|
||||
|
||||
Pros:
|
||||
|
||||
- Cheap improvement when aligned to real plans.
|
||||
- Complements query redesign.
|
||||
|
||||
Cons:
|
||||
|
||||
- Limited value if broad in-memory processing remains.
|
||||
- Too many indexes can slow writes/imports.
|
||||
|
||||
Recommended priority: P1
|
||||
|
||||
Estimated effort: small
|
||||
|
||||
Suggested candidates to measure first:
|
||||
|
||||
- `Resource(isActive, departed, rolledOff, chgResponsibility, countryId)`
|
||||
- `Resource(isActive, managementLevelGroupId, countryId)`
|
||||
- `Project(clientId, status, startDate, endDate)`
|
||||
|
||||
### H. Consider Row Virtualization for Heavy Tables
|
||||
|
||||
Proposal:
|
||||
|
||||
- Add virtualization for very large row-based screens if profiling confirms DOM/render cost remains high after data/query optimizations.
|
||||
- Candidate screens: resources table, chargeability report table, timeline side panels.
|
||||
|
||||
Pros:
|
||||
|
||||
- Strong render win when many rows are mounted.
|
||||
- Independent of backend changes.
|
||||
|
||||
Cons:
|
||||
|
||||
- More UI complexity.
|
||||
- Can complicate drag/drop, sticky headers, and keyboard navigation.
|
||||
- Should not be the first fix for timeline if the main problem is data churn.
|
||||
|
||||
Recommended priority: P2
|
||||
|
||||
Estimated effort: medium
|
||||
|
||||
## Priority Order
|
||||
|
||||
### Phase 1
|
||||
|
||||
- A. Move timeline derivations closer to the backend.
|
||||
- B. Replace repeated array scans with indexed maps.
|
||||
- C. Scope cache invalidation for timeline mutations and SSE.
|
||||
|
||||
Expected result:
|
||||
|
||||
- Best chance of making timeline interaction materially faster without removing functionality.
|
||||
|
||||
### Phase 2
|
||||
|
||||
- D. Server-side chargeability read models.
|
||||
- E. Narrow data fetches to visible scope.
|
||||
- G. Add measured composite indexes.
|
||||
|
||||
Expected result:
|
||||
|
||||
- Faster reports, dashboard widgets, and filter-heavy screens.
|
||||
|
||||
### Phase 3
|
||||
|
||||
- F. Materialized analytics/read models.
|
||||
- H. Virtualization where profiling still shows render bottlenecks.
|
||||
|
||||
Expected result:
|
||||
|
||||
- Better scalability once data volume grows further.
|
||||
|
||||
## Suggested Measurement Plan
|
||||
|
||||
Before implementation:
|
||||
|
||||
- Capture browser performance traces for:
|
||||
- timeline initial load
|
||||
- timeline hover/selection
|
||||
- timeline mutation save path
|
||||
- chargeability report load
|
||||
- resources page load and scroll
|
||||
- Capture server timings for:
|
||||
- `timeline.getEntriesView`
|
||||
- `chargeabilityReport.getReport`
|
||||
- `dashboard.getChargeabilityOverview`
|
||||
- `resource.getChargeabilityStats`
|
||||
- Record PostgreSQL query plans for the slowest endpoints.
|
||||
|
||||
During implementation:
|
||||
|
||||
- Compare payload sizes before and after endpoint changes.
|
||||
- Track React commit duration on Timeline and Resources screens.
|
||||
- Measure query invalidation counts after common edits.
|
||||
|
||||
Success criteria:
|
||||
|
||||
- Timeline hover and selection feel immediate under imported dispo-scale data.
|
||||
- Timeline mutation flows no longer trigger full-screen refetch storms.
|
||||
- Chargeability report load time drops materially for multi-month ranges.
|
||||
- Dashboard widgets stop scanning large booking arrays per resource.
|
||||
|
||||
## No-Regression Guidance
|
||||
|
||||
Do not trade away any of the following:
|
||||
|
||||
- Existing planning semantics for assignments, proposed work, and demand visibility.
|
||||
- Timeline drag/drop correctness.
|
||||
- Tooltip, right-click, and selection behavior.
|
||||
- Anonymization behavior.
|
||||
- Filter parity between resources, timeline, and reports.
|
||||
|
||||
Recommended safeguards:
|
||||
|
||||
- Add endpoint-level benchmarks for the heavy routes.
|
||||
- Add UI regression checks around timeline interaction paths.
|
||||
- Roll out timeline changes behind a temporary feature flag if the payload shape changes significantly.
|
||||
- Compare old and new aggregation outputs on the same imported dataset before removing legacy paths.
|
||||
|
||||
## Suggested Ticket Breakdown
|
||||
|
||||
1. Profile and baseline the four slowest endpoints plus timeline render path.
|
||||
2. Refactor timeline read model to return pre-grouped data for current filters/view.
|
||||
3. Replace timeline broad invalidations with scoped invalidation and selective cache patching.
|
||||
4. Refactor chargeability report aggregation into grouped server-side structures.
|
||||
5. Narrow lookup/filter support queries to selected IDs or visible pages.
|
||||
6. Review PostgreSQL query plans and add only measured composite indexes.
|
||||
7. Re-profile and decide whether virtualization or materialized analytics are still needed.
|
||||
|
||||
## Bottom Line
|
||||
|
||||
This codebase can be made substantially faster without removing functionality. The main gains will not come from cosmetic micro-optimizations. They will come from changing where data is shaped, reducing repeated scans, and stopping broad cache churn on heavy views.
|
||||
Reference in New Issue
Block a user