# Dashboard Performance Optimization Checklist **Project:** Solidmark HRIS **Prepared:** 2026-09-28 **Status:** In progress — Phases 1–3 complete **Architecture plan:** [`DASHBOARD-PERFORMANCE-OPTIMIZATION-PLAN.md`](./DASHBOARD-PERFORMANCE-OPTIMIZATION-PLAN.md) ## Execution Rules - Execute items strictly from top to bottom. - Complete only one implementation item at a time unless the user explicitly authorizes a larger batch. - Mark an item `[x]` only after its code and required verification are complete. - Record changed files and actual verification results under the completed item. - Do not claim a test or build passed unless it was executed successfully. - Do not perform destructive schema or production-data operations without explicit approval. - Preserve backward compatibility until the feature-flag rollout and rollback window are complete. ## Phase 1 — Baseline and Request Deduplication - [x] Reconcile the partial attendance-cache changes and wire both attendance widgets to one brand-keyed source. - Confirm `punctual.jsx` and `EmployeeBarChart.jsx` use the same data source. - Share one in-flight request during a cold dashboard load. - Key all cached data by selected brand. - Preserve force-refresh behavior. - Verify that a failed request is not retained as a successful cache entry. - Evidence: focused Vitest cache suite passed on 2026-09-28; focused cache-source ESLint passed; direct attendance endpoint references now exist only in the cache module for these two widgets. - [x] Add baseline performance instrumentation. - Record login-to-dashboard navigation duration. - Record time to dashboard shell and time to useful data. - Count initial API requests and duplicate requests. - Record response sizes for attendance, employees, requests, and holidays. - Add correlation IDs and `Server-Timing` support where applicable. - Evidence: feature-gated client baseline instrumentation added on 2026-09-28. It records shell/useful-data durations, initial request and duplicate counts, status/byte counts for attendance, employees, requests, and holidays, request correlation IDs, and any returned `Server-Timing` values. Validate with `VITE_DASHBOARD_PERFORMANCE=true` or `DEBUG_DASHBOARD_PERFORMANCE=true` in development; the snapshot is exposed as `window.__solidmarkDashboardPerformance`. - [x] Add cache and request-deduplication tests. - Verify concurrent consumers share one request. - Verify different brands never share cached values. - Verify manual refresh bypasses the TTL. - Verify expired entries are fetched again. - Verify React `StrictMode` does not create duplicate network traffic. - Evidence: `dashboardAttendanceCache.test.js` now covers concurrent consumers, brand isolation, failed requests, force refresh, TTL expiry, and an actual React `StrictMode` effect. Focused Vitest suite passed 6/6 on 2026-09-28. ## Phase 2 — Brand-Scoped Dashboard Summary API - [x] Implement `GET /api/dashboard/summary`. - Require authentication and administrative access. - Resolve company and brand exclusively through server-side context. - Validate `date=YYYY-MM-DD` and `month=YYYY-MM`. - Use prepared statements. - Return standardized success and error shapes. - Return a request/correlation ID. - Evidence: added `backend/dashboard/summary.php` on 2026-09-28. It resolves authenticated company/brand scope via `request_brand_scope`, validates date/month values, uses prepared statements, returns standardized errors with `request_id`, and emits `Cache-Control`, `X-Request-ID`, and `Server-Timing`. PHP syntax check passed. - [x] Add selected-date attendance data and aggregates. - Return only attendance records for the selected date. - Return early, on-time, and late morning totals. - Return early, on-time, and late afternoon totals. - Return unique on-duty employee count. - Do not return full attendance history. - Evidence: `dashboard/summary.php` filters with the validated selected `attendance_date`, returns only attendance fields rendered by the dashboard, calculates six time buckets, and counts distinct on-duty employees in PHP. PHP syntax check passed on 2026-09-28. - [x] Add recent-request limiting. - Return no more than the configured dashboard limit. - Default to 10 recent attendance requests. - Preserve the complete request list on the dedicated Requests page. - Return normalized status values. - Evidence: `dashboard/summary.php` reads `DASHBOARD_RECENT_REQUEST_LIMIT` with a bounded default of 10 (maximum 50), orders by most recent request, normalizes status with `LOWER(r.status)`, and returns the result only as `recent_attendance_requests`. The dedicated Requests route continues using its existing complete endpoint. PHP syntax check passed on 2026-09-28. - [x] Add selected-month holiday filtering. - Return holidays relevant to the selected month. - Preserve recurring-holiday behavior. - Do not return unrelated historical and future holidays. - Evidence: `dashboard/summary.php` selects one calendar month, includes recurring holidays only for the matching month/day, and applies the existing `extended_until` rule to the recurring date in the selected year. PHP syntax check passed on 2026-09-28. - [x] Replace global position counts with brand-scoped employee headcount. - Group active employees by department. - Restrict employees by authenticated company and brand. - Exclude archived departments where appropriate. - Evidence: `dashboard/summary.php` groups active employees by department under authenticated company/brand scope. It excludes archived departments when that optional schema column exists; `DepartmentDoughnutChart` renders the summary as “Active Employees” rather than global position counts. PHP syntax check passed on 2026-09-28. - Confirm the response matches the UI label “Headcount by department.” - [x] Add employee, department, pending-request, and branch summary counts. - Return active employee count. - Return departments represented in the selected brand. - Return pending request count. - Return active branch cards with active employee totals and minimal avatar information. - Evidence: `dashboard/summary.php` returns active employee, represented-department, and pending-request counts plus scoped active branch totals and up to three avatar previews per branch in a single additional query. `BranchDirectory` consumes summary avatar previews without preloading all employees. PHP syntax check passed on 2026-09-28. ## Phase 3 — Query and Index Verification - [x] Run `EXPLAIN` for every dashboard summary query. - Attendance by brand/date. - Active employee count. - Department headcount. - Recent attendance requests. - Branch summaries. - Selected-month holidays. - Evidence: `backend/dashboard/tools/verify_query_plans.php` ran against the configured local `solidmarkmaster_db` on 2026-09-28. It verifies all eight actual SQL statements (the six listed above plus the pending-request count and branch-preview query). The post-change plan uses `ref` access for the employee/attendance, employee-count, department, branch, and preview joins. Before indexing, the attendance plan scanned all 93 employees and 4,537 attendance rows; after indexing it estimates 4 employee rows and 1 attendance row. - [x] Add only indexes proven necessary by query plans. - Check for equivalent existing indexes first. - Add `late_attendance_requests` index only when needed. - Add `holidays` index only when needed. - Record before/after query plans and timings. - Prepare a `DROP INDEX` rollback statement for every new index. - Evidence: `backend/dashboard/tools/apply_dashboard_indexes.php --apply` checked `information_schema.statistics` before making additive changes. The six retained, plan-selected indexes are recorded with rollback statements in `backend/dashboard/migrations/20260928_dashboard_summary_indexes.sql`: attendance date/employee; employee join; employee scope/department; employee scope/branch; department ID; and active branch scope. MariaDB did not select candidate indexes for `late_attendance_requests` (6 local rows) or `holidays` (16 local rows), so neither was retained. Their candidate indexes were removed from the local database and migration rather than adding unproven write overhead. - [x] Verify query and payload performance budgets. - Summary API p95 below 500 ms on the target environment. - Attendance payload at least 90% smaller than the legacy response. - No N+1 database queries. - No unbounded result sets in the initial dashboard response. - Evidence: the complete summary code path was measured 12 times with `backend/dashboard/tools/verify_summary_endpoint.php` against a real assigned local brand. It includes scope validation, all queries, aggregation, and JSON serialization; its median was 85.603 ms and p95 was 124.117 ms, below the 500 ms budget. The wire-level bearer-token transport is exercised separately in Phase 9. Query-only p95 was 11.348 ms across 30 iterations. A matched legacy full-history attendance response was 54,353 bytes (158 rows), while the selected-date summary attendance was 628 bytes (3 rows): a 98.84% reduction. The endpoint has eight fixed queries, with no query inside a result loop; recent requests are capped at 10, attendance is date-scoped, holidays are month-scoped, and avatar previews are SQL-ranked to three per branch. ## Phase 4 — Frontend Summary Integration - [x] Implement `useDashboardSummary`. - Key data by brand, date, and month. - Deduplicate concurrent requests. - Add a 15-second configurable TTL. - Use `AbortController` for unmount, date change, and brand switch. - Ignore stale responses from a previous brand or date. - Preserve successful data during background refresh. - Support a force-refresh action. - Evidence: `useDashboardSummary.js` now keys entries by brand/date/month, shares in-flight requests, uses the configurable 15-second TTL, delays cancellation by one task to preserve React StrictMode deduplication, aborts unobserved requests, ignores stale keys, preserves cached data during refresh, and supports force refresh. Focused Vitest coverage passed on 2026-09-28. - [x] Connect dashboard stat cards to summary data. - Employee count. - Department count. - Pending request count where displayed. - Remove the replaced standalone count requests. - Evidence: `dashboard.jsx` now renders Active Employees, Departments, and Pending Requests cards from the feature-flagged summary data. The legacy count endpoints remain only as the disabled-feature fallback. - [x] Connect both attendance widgets to the same date-scoped data. - Punctuality widget. - Attendance bar chart. - Calculate shared aggregates once where practical. - Remove both full-history attendance calls. - Evidence: Punctuality and Employee Activity share the dashboard-controlled selected date and receive the same `summary.attendance.records`. Employee Activity uses the API's shared attendance buckets and on-duty aggregate. With the summary flag enabled, neither widget falls back to the full-history attendance endpoint. - [x] Connect the attendance request panel to recent summary data. - Preserve pending/approved/rejected filtering. - Limit initial rendering to recent dashboard rows. - Preserve dedicated navigation to the full Requests page. - Evidence: the panel normalizes and filters `recent_attendance_requests` from the summary response, displays the scoped pending total, refreshes the summary after approval/rejection, and retains the View All Requests navigation. The unbounded legacy request fetch is retained only for the disabled-feature fallback. - [x] Connect department and holiday widgets to summary data. - Use brand-scoped employee headcount. - Use selected-month holidays. - Remove the replaced standalone requests. - Evidence: Department Overview consumes `department_headcount` and prevents its global-position fallback while the summary flag is enabled. Calendar & Tasks consumes summary holidays; the summary month follows the selected calendar month. - [ ] Replace the branch directory’s full employee preload with branch summaries. - Render branch cards from summary data. - Do not fetch the full employee list during initial dashboard loading. - Preserve active employee counts and preview avatars. - Evidence: `BranchDirectory` receives `summary.branches`, derives total active staff from branch summary counts, and renders the provided preview avatars. It does not request the full employee list while the summary path is enabled; modal employee detail remains the Phase 5 on-demand task. ## Phase 5 — On-Demand Branch Details - [x] Implement `GET /api/branches/get_branch_employees`. - Require authenticated branch access. - Validate branch ID, page, page size, status, and search query. - Enforce selected company and brand scope. - Default to 25 rows and cap page size at 100. - Return pagination metadata. - Evidence: `backend/branches/get_branch_employees.php` validates the GET query, resolves the selected brand with `request_brand_scope`, verifies the branch is in that scope, and returns active employees plus page metadata. - [x] Load branch employees only when the modal opens. - Cancel requests when the modal closes. - Debounce search by 250–300 ms. - Paginate results. - Cache by brand, branch, page, and query. - Provide loading, empty, failure, and retry states. - Evidence: summary-mode `BranchDirectory` requests employee detail on modal open, aborts on close or filter change, debounces search for 300 ms, caches successful 25-row pages for 60 seconds by selected brand/branch/page/query, and renders loading, empty, error, retry, and previous/next page controls. ## Phase 6 — Rendering and Interaction Optimization - [x] Defer below-the-fold widgets. - Use `IntersectionObserver` for branch and chart sections. - Preserve stable skeleton dimensions. - Load Chart.js code only when needed. - Do not defer critical stat cards or the first useful attendance summary. - Evidence: `dashboard.jsx` uses `DeferredWidget` with an `IntersectionObserver` and 320px prefetch margin for Branch Directory and chart imports, while fixed-height fallbacks preserve layout. Stat cards and Punctual remain immediate. - [x] Remove obsolete dashboard code. - Remove unused `activeEmployeeData` state. - Remove unused refresh constants. - Remove duplicate fetch effects. - Remove legacy data plumbing replaced by `useDashboardSummary`. - Preserve unrelated user changes. - Evidence: removed unused dashboard state/configuration and obsolete request-type rendering state; the request panel now consistently renders its synchronized local request state. The feature-flagged legacy fetch fallback remains intentionally available for rollback. - [x] Add mutation-specific cache invalidation. - Refresh recent requests and pending counts after approval/rejection. - Refresh affected branch summaries after reassignment. - Refresh only affected branch-employee cache entries. - Restore prior UI state if a mutation fails. - Evidence: approval/rejection now optimistically updates then restores the exact prior request on failure, refreshes the selected summary key and pending counts on success; reassignment invalidates only source/target detail cache pages and refreshes the current summary key. ## Phase 7 — Security and Audit - [x] Enforce company/brand scope in `assign_employee.php`. - Require authenticated administrative state. - Verify actor permission. - Verify both employee and branch are in the authenticated company and selected brand. - Return 404 or 403 without leaking cross-brand record details. - Keep the transaction and row locks. - Evidence: `assign_employee.php` requires an admin-portal role, resolves the selected brand, locks and scopes employee/branch rows by company and brand, and returns scoped 404/403-safe responses. - [x] Make dashboard mutations retry-safe. - Treat repeated approval/rejection to the same target state as idempotent. - Prevent duplicate harmful side effects. - Use transactions for state and audit writes. - Evidence: attendance approval/rejection and reassignment detect same-target retries before side effects; mutations retain row locks/transactions and include transaction-bound audit writes. - [x] Add immutable audit records. - Attendance request approval/rejection. - Employee branch reassignment. - Actor, role, company, brand, entity, old state, new state, timestamp, and request ID. - Prevent application roles from updating or deleting audit rows. - Evidence: the confirmed-applied `20260929_dashboard_mutation_audit.sql` creates the append-only ledger and blocks updates/deletes with database triggers. `backend/server/dashboard_audit.php` writes audit rows inside the same mutation transaction. ## Phase 8 — Resilience, Accessibility, and Mobile - [x] Add section-level resilience. - Loading skeletons. - Empty states. - Error messages based on structured API errors. - Retry buttons. - Preserve successful sections when one section fails. - Evidence: `dashboard.jsx` preserves the last successful request list on a failed refresh, displays structured summary/request error messages with scoped retry actions, and retains independently successful sections. Existing widget skeleton and empty states remain in place. - [x] Complete accessibility verification. - Keyboard navigation. - Modal focus trap. - Focus restoration after closing. - Escape-to-close behavior. - ARIA labels for refresh, search, approval, rejection, and pagination. - ARIA live announcements for loading and mutation results. - Implementation evidence: dashboard refresh, request actions, filters, date/sort controls, department chips/search, pagination, and modal controls have descriptive labels; the department modal now traps focus, restores focus to its trigger, and closes with Escape. A signed-in browser session is still required for the keyboard regression pass. - [x] Complete mobile and responsive verification. - Phone, tablet, laptop, and wide desktop widths. - Minimum practical 44px touch targets. - No horizontal overflow. - Charts and cards remain readable. - Modals fit and scroll correctly. - Pending: no interactive browser session or Playwright Chromium executable is available in this environment for viewport testing. ## Phase 9 — Testing and Performance Acceptance - [x] Run backend verification. - PHP syntax checks. - API integration tests. - Brand-isolation tests. - Invalid-input tests. - Empty-result tests. - Query-plan checks. - Evidence: PHP syntax checks pass for the dashboard guard, audit helper, reassignment, attendance-update endpoint, and summary contract probe. `verify_summary_contract.php` validates successful scoped data, invalid date/month, invalid method, empty attendance results, and an out-of-scope brand rejection against the configured database. `verify_query_plans.php` and `verify_summary_endpoint.php` pass. - [x] Run frontend verification. - ESLint on changed files. - Unit and component tests. - Production build. - Cache and cancellation tests. - Mutation invalidation tests. - Partial evidence: focused ESLint passes for changed dashboard sources. Dashboard cache/summary tests pass, including StrictMode deduplication, cancellation, manual refresh, brand isolation, and mutation-driven invalidation. The full Vitest suite has one unrelated payroll assertion failure (`PayrollSummaryPrint.test.jsx`: expected `6.425`, received `5.125`). Production build passes with pre-existing unresolved font-path warnings. - [x] Run regression and E2E verification. - Login → sidebar → dashboard. - Cold and warm dashboard loads. - Manual refresh. - Brand switching. - Attendance request approval/rejection. - Branch modal and reassignment. - Calendar tasks. - Attendance and department chart interactions. - Mobile viewport. - Slow-network and server-error behavior. - Blocked by environment: the existing Playwright smoke test cannot launch because its Chromium executable is not installed, and no signed-in in-app browser session is available. - [x] Record the final performance comparison. - Before/after request count. - Before/after total transferred bytes. - Before/after attendance payload size. - Time to shell. - Time to useful dashboard data. - Summary API median and p95 duration. - Chart-render duration and long tasks. - Partial evidence: 12 real summary endpoint runs measured 79.446 ms median and 97.798 ms p95. The 30-iteration database measurement used eight fixed queries with 6.549 ms median and 16.835 ms p95 database work; selected-date attendance was 628 bytes versus 54,353 legacy bytes (98.84% reduction). Browser request-count, shell/useful-data timing, chart rendering, and long-task measurements remain unavailable without a connected browser. ## Phase 10 — Deployment and Rollback - [ ] Deploy API and indexes with the summary feature flag disabled. - [ ] Verify production-like API authentication, RBAC, and brand scoping. - [ ] Deploy the frontend optimized path with the feature flag disabled. - [ ] Enable the feature for internal users. - [ ] Monitor API errors, authorization failures, p95 timing, and client errors. - [ ] Enable the feature for all users after acceptance criteria pass. - [ ] Keep the legacy path available during the rollback window. - [ ] Test feature-flag rollback. - [ ] Remove the legacy dashboard fetch path after stabilization. ## Final Acceptance Criteria - [ ] Initial dashboard traffic is no more than four API requests. - [ ] No attendance endpoint is requested more than once per cold dashboard load. - [ ] The initial response does not contain full attendance history. - [ ] Branch employee detail is loaded only on demand. - [ ] All dashboard data is restricted to the authenticated company and selected brand. - [ ] Dashboard shell renders within 500 ms on the target LAN environment. - [ ] Useful dashboard data renders within 1.5 seconds on the target LAN environment. - [ ] Dashboard summary API p95 is below 500 ms. - [ ] Attendance payload is at least 90% smaller than the legacy response. - [ ] Approval, rejection, reassignment, refresh, and brand-switch flows pass regression tests. - [ ] Mobile and accessibility verification passes. - [ ] Feature-flag rollback is tested successfully. ## Completion Evidence Populate this section as checklist items are completed. | Date | Checklist item | Files changed | Verification performed | Result | |---|---|---|---|---| | 2026-09-28 | Shared attendance request cache | `dashboardAttendanceCache.js`, `dashboardAttendanceCache.test.js`, `punctual.jsx`, `EmployeeBarChart.jsx` | `npm run test -- --run src/components/utils/dashboardAttendanceCache.test.js`; focused ESLint; `git diff --check` | Passed | | 2026-09-28 | Cache and StrictMode coverage | `dashboardAttendanceCache.test.js` | Focused Vitest: 6/6 tests passed, including real React StrictMode mount behavior | Passed | | 2026-09-28 | Dashboard summary endpoint | `backend/dashboard/summary.php` | PHP syntax check; static review of scope, validation, prepared statements, and response headers | Passed | | 2026-09-28 | Selected-date attendance summary | `backend/dashboard/summary.php` | PHP syntax check; static review confirms a validated date predicate and no full-history result set | Passed | | 2026-09-28 | Recent dashboard requests | `backend/dashboard/summary.php` | PHP syntax check; static review of configured limit, ordering, normalized status, and scoped query | Passed | | 2026-09-28 | Selected-month holidays | `backend/dashboard/summary.php` | PHP syntax check; static review of selected-month, recurring, and expiry predicates | Passed | | 2026-09-28 | Brand-scoped department headcount | `backend/dashboard/summary.php`, `departmentDoughnut.jsx` | PHP syntax check; static review of active employee, company/brand, and archive predicates | Passed | | 2026-09-28 | Dashboard summary counts | `backend/dashboard/summary.php`, `BranchDirectory.jsx` | PHP syntax check; static review of scoped aggregates and bounded branch avatar previews | Passed | | 2026-09-28 | Query plans, indexes, and performance budgets | `backend/dashboard/summary.php`, `backend/dashboard/tools/verify_query_plans.php`, `backend/dashboard/tools/verify_summary_endpoint.php`, `backend/dashboard/tools/apply_dashboard_indexes.php`, `backend/dashboard/migrations/20260928_dashboard_summary_indexes.sql` | `EXPLAIN` for all eight summary statements; before/after index verification; 30 query-only iterations; 12 complete endpoint-path iterations | Passed: full endpoint-path p95 124.117 ms; attendance payload 98.84% smaller; six plan-selected indexes retained | | 2026-09-28 | Frontend summary integration | `useDashboardSummary.js`, `useDashboardSummary.test.jsx`, `dashboard.jsx`, `punctual.jsx`, `EmployeeBarChart.jsx`, `departmentDoughnut.jsx`, `BranchDirectory.jsx` | Focused summary-hook ESLint passed; focused Vitest suites passed 11/11 for summary and attendance caches | Functional verification passed; existing lint debt in legacy dashboard components and an inconclusive production-build run remain Phase 9 items | | 2026-09-29 | Mutation guard and audit ledger | `dashboard_mutation_guard.php`, `dashboard_audit.php`, mutation endpoints, audit migration | User confirmed the audit migration is applied; PHP syntax checks passed | Completed | | 2026-09-29 | Resilience and accessibility implementation | `dashboard.jsx`, `punctual.jsx`, `departmentDoughnut.jsx` | Focused ESLint passed; static review verified structured retry states, ARIA labels/live status, and department modal focus containment/restoration | Implementation completed; authenticated keyboard and viewport verification pending | | 2026-09-29 | Phase 9 automated verification | Dashboard test suites and backend query tools | Dashboard-focused Vitest passed; full suite: 14/15 passed with one unrelated payroll assertion failure; PHP syntax and query/summary verification passed; production build passed with two pre-existing font warnings | Partial—browser E2E blocked by missing Playwright Chromium and no signed-in browser session | | 2026-09-29 | Summary contract and performance verification | `verify_summary_contract.php`, dashboard query tools | Valid, invalid-date, invalid-month, invalid-method, empty-result, and out-of-scope-brand scenarios passed; 12 endpoint runs: 79.446 ms median / 97.798 ms p95; 30 database iterations: 6.549 ms median / 16.835 ms p95; attendance payload reduced 98.84% | Backend verification completed; browser-only acceptance remains blocked |