# Dashboard Performance Optimization Plan **Project:** Solidmark HRIS **Prepared:** 2026-09-28 **Status:** Planned; implementation not yet complete **Scope:** Administrator login-to-dashboard flow, dashboard API traffic, database queries, rendering, caching, security, and verification ## Objective Reduce the time between selecting **Dashboard** and seeing useful, accurate data while preserving authentication, RBAC, company/brand isolation, mutation safety, accessibility, and mobile responsiveness. ### Target outcomes - Render the dashboard shell within 500 ms on the local network. - Display useful dashboard data within 1.5 seconds under normal load. - Reduce the initial dashboard request count from approximately 9–10 to 2–4. - Reduce the attendance payload by at least 90%. - Ensure only one attendance request is made during a cold dashboard load. - Keep the dashboard summary API p95 response time below 500 ms. - Prevent cross-brand data from appearing in responses or frontend caches. - Preserve refresh, request approval/rejection, branch reassignment, and brand-switch behavior. ## 0. Discovery and Context Inspection ### Frontend architecture The dashboard route is lazy-loaded from `frontend/src/App.jsx` and renders `frontend/src/components/dashboard/dashboard.jsx`. The dashboard then lazy-loads several widgets: - `frontend/src/components/dashboard/punctual.jsx` - `frontend/src/components/employees/EmployeeBarChart.jsx` - `frontend/src/components/dashboard/BranchDirectory.jsx` - `frontend/src/components/departments/departmentDoughnut.jsx` - `frontend/src/components/dashboard/taskManager.jsx` The application uses React 18, Axios, native `fetch`, React Router, Chart.js, and Vite. `React.StrictMode` is enabled in development, which can intentionally mount components and execute effects twice. Uncached effects can therefore produce duplicate development requests even when production performs only one. ### Current initial data requests | Dashboard consumer | Route | Current payload behavior | Finding | |---|---|---|---| | Pending badge provider | `/api/server/get_pending_counts` | Multiple aggregate counts | Independent request and polling | | Calendar | `/api/holiday/get_holiday` | All holidays | Dashboard only needs the displayed month | | Employee stat card | `/api/employeesSide/employees?count=true` | Employee count | Separate round trip | | Department stat card | `/api/departments/department?count=true` | Global department count | Not company/brand scoped in current schema | | Attendance requests | `/api/late_request_clockInOut/get_late_attendance_requests` | All non-archived requests | No dashboard limit | | Branch directory | `/api/branches/get_branch` | All active branches in scope | Reasonable, but can return summaries through the dashboard API | | Branch directory | `/api/employeesSide/employees` | Full employee rows and joins | Too much data for branch cards | | Punctuality widget | `/api/attendance/attendance` | Complete attendance history | Oversized and duplicated | | Attendance chart | `/api/attendance/attendance` | Complete attendance history | Oversized and duplicated | | Department chart | `/api/graphs/get_positions` | Global position counts | Does not match the UI label “Headcount by department” | The supplied database dump indicates approximately 7,288 attendance records. The dashboard primarily needs records for one selected date plus aggregate counts, so returning the entire history is the largest payload bottleneck. ### Authentication and brand scoping - Authentication is enforced by `backend/server/connection.php`. - Brand context is resolved by `backend/config/brand_context.php`. - Attendance and request endpoints use brand-scope helpers. - Frontend requests receive the token and selected-brand headers through `axiosInstance.js` and its `fetch` wrapper. - Any new dashboard cache must include the selected brand in its key. - Any in-flight request from a previous brand must be cancelled or ignored after a brand switch. ### Existing reusable utilities - Authenticated Axios client: `frontend/src/components/utils/axiosInstance.js` - Selected-brand resolver: `getCurrentBrandCode()` - Brand scope helper: `backend/server/request_brand_scope.php` - Biometrics scope helper: `backend/helpers/biometrics_scope_helper.php` - Session state: `frontend/src/context/SessionContext.jsx` - Pending count polling: `frontend/src/context/PendingCountsContext.jsx` ### Current implementation note The first two quick-win optimizations are complete in the worktree: - `frontend/src/components/utils/dashboardAttendanceCache.js` owns the brand-keyed in-memory attendance cache. - `EmployeeBarChart.jsx` and `punctual.jsx` use that same cache. - Concurrent cold-load consumers share one in-flight request. - A chart manual refresh bypasses the cache. - `dashboardPerformance.js` records an opt-in baseline for login-to-dashboard, shell, useful-data, request count, duplicate count, status, and response size. - Dashboard requests receive safe correlation IDs; the CORS middleware echoes and exposes those IDs and exposes `Server-Timing` when an endpoint provides it. - The baseline is active in development or with `VITE_DASHBOARD_PERFORMANCE=true`; its latest snapshot is available at `window.__solidmarkDashboardPerformance`. The remaining plan replaces this temporary full-history cache with a smaller date-scoped dashboard summary response. ## 1. User Flow and Use Cases ### Primary actor An authenticated administrator, brand administrator, HR user, manager, or other authorized administrative role viewing data within an allowed company/brand scope. ### Preconditions - The user has a valid authenticated session. - The selected brand is included in the user’s allowed brands. - Dashboard permission/RBAC checks pass. - Required company and brand records are active. ### Happy path 1. The user logs in. 2. The backend selects an authorized default brand. 3. The user selects **Dashboard** from the sidebar. 4. The dashboard shell, greeting, and skeletons render immediately. 5. One brand- and date-scoped summary request loads above-the-fold data. 6. Lower-priority widgets are loaded when they approach the viewport. 7. Opening a branch modal loads only that branch’s employees. 8. Manual refresh bypasses the short cache and replaces the displayed data. ### Alternative and error flows - **401:** Clear session state and redirect to login. - **403:** Preserve the current screen and show the backend brand/RBAC message. - **Failed section:** Show a section-level retry without blanking the entire dashboard. - **Brand switch:** Cancel or ignore old requests, clear old-brand UI state, and load the new brand’s data. - **Empty data:** Show zero-count and empty-state components rather than errors. - **Slow connection:** Keep the dashboard shell interactive and progressively fill sections. - **Mutation failure:** Restore optimistic UI state and show a clear error. ## 2. Database Architecture and ERD ### Logical relationships ```mermaid erDiagram COMPANIES ||--o{ BRANDS : contains BRANDS ||--o{ EMPLOYEES : employs BRANDS ||--o{ BRANCHES : contains EMPLOYEES ||--o{ ATTENDANCE : records EMPLOYEES ||--o{ LATE_ATTENDANCE_REQUESTS : submits DEPARTMENTS ||--o{ EMPLOYEES : groups BRANCHES ||--o{ EMPLOYEES : assigns ``` ### Existing tables to use or alter | Table | Action | Purpose | |---|---|---| | `attendance` | Use existing | Query only the selected date and scoped employees | | `employees` | Use existing | Brand scope, employee count, department and branch aggregation | | `branches` | Use existing | Branch summary cards | | `departments` | Use existing | Department names; headcount must be derived through scoped employees | | `late_attendance_requests` | Add index if missing | Efficient recent-request loading | | `holidays` | Add index if missing | Efficient selected-month lookup | | `logs` | Optional additive ALTER | Structured mutation audit fields | ### New tables No new tables are required for the performance optimization. ### Proposed indexes Run `SHOW INDEX` and `EXPLAIN` first. Create an index only when an equivalent left-prefix index is absent. ```sql CREATE INDEX idx_late_requests_employee_archive_requested ON late_attendance_requests ( employee_id, is_archived, requested_at ); CREATE INDEX idx_holidays_date_recurring ON holidays ( holiday_date, is_recurring ); ``` Existing useful indexes in the supplied dump include: - `attendance (attendance_date, employee_id)` - `attendance (employee_id, attendance_date)` - `employees (company_id, brand_id, status, ...)` - `employees (company_id, brand_id, branch_id)` - `branches (company_id, brand_id)` ### Rollback and zero-downtime strategy - Index changes are additive and do not change existing query contracts. - Create indexes in a maintenance window if table-lock behavior is possible on the deployed MariaDB version. - Keep existing endpoints operational while the summary API is behind a feature flag. - Roll back an index using `DROP INDEX index_name ON table_name`. - Roll back the optimized frontend by disabling the feature flag. - Do not remove legacy dashboard endpoint support until the optimized path is stable. ## 3. RESTful PHP API Plan ### New dashboard summary endpoint **Method:** `GET` **Route:** `/api/dashboard/summary` **Query:** `date=YYYY-MM-DD&month=YYYY-MM` **Purpose:** Return the minimum brand-scoped data required for the initial dashboard. Example response: ```json { "success": true, "data": { "employee_count": 120, "department_count": 10, "pending_request_count": 4, "attendance": { "records": [], "early_morning": 3, "on_time_morning": 81, "late_morning": 9, "early_afternoon": 2, "on_time_afternoon": 75, "late_afternoon": 6, "on_duty_count": 93 }, "recent_attendance_requests": [], "branches": [], "department_headcount": [], "holidays": [] }, "meta": { "brand_code": "SOLIDMARK_INC", "date": "2026-09-28", "generated_at": "2026-09-28T10:00:00+08:00" }, "request_id": "request-correlation-id" } ``` Implementation requirements: - Authenticate through the existing connection/auth bootstrap. - Require authorized administrative state and selected-brand access. - Strictly validate `date` and `month`. - Use prepared statements exclusively. - Query attendance by selected date, not full history. - Limit recent requests to a configured maximum, initially 10. - Return active employee headcount grouped by department. - Return only fields rendered by the dashboard. - Include `Cache-Control: private, max-age=15`. - Include an `X-Request-ID` response header and response field. - Add `Server-Timing` entries for the major query groups. - Use a conservative per-user/IP rate limit, such as 60 requests per minute. ### Branch employee endpoint **Method:** `GET` **Route:** `/api/branches/get_branch_employees` **Query:** `branch_id`, `page`, `page_size`, `query`, `status=active` **Purpose:** Load employee detail only after a branch modal is opened. Response requirements: - Verify the branch belongs to the authenticated company and selected brand. - Default `page_size` to 25 and cap it at 100. - Debounce frontend search. - Return total rows and pagination metadata. ### Existing mutations retained | Method | Route | Required improvement | |---|---|---| | `POST` | `/api/late_request_clockInOut/update_late_attendance_request` | Transactional audit, idempotent target-state update, cache invalidation | | `POST` | `/api/branches/assign_employee` | Enforce actor permission and company/brand scope before update | ### Standard error response ```json { "success": false, "message": "Unable to load dashboard summary.", "error": { "code": "DASHBOARD_SUMMARY_FAILED", "fields": {} }, "request_id": "request-correlation-id" } ``` Use HTTP 400, 401, 403, 404, 409, 422, 429, and 500 as appropriate. Retain the top-level `message` for existing frontend compatibility. ## 4. React Frontend Flow ### Proposed component data flow ```mermaid flowchart TD D[Dashboard] --> H[useDashboardSummary] H --> S[Stat cards] H --> P[Punctuality] H --> A[Attendance chart] H --> R[Recent requests] H --> B[Branch summaries] H --> C[Department headcount] H --> K[Calendar holidays] B -->|Modal opened| E[Paginated branch employees] ``` ### State and synchronization - Introduce `useDashboardSummary({ brandCode, date, month })`. - Key cached values by `brandCode + date + month`. - Deduplicate concurrent calls by sharing the in-flight promise. - Use `AbortController` when the component unmounts or brand/date changes. - Keep the last successful result visible during background refresh. - Prevent results from an old brand from updating the current screen. - Invalidate recent requests after approval/rejection. - Invalidate branch summaries and the affected branch-employee page after reassignment. - Manual refresh must bypass the TTL. ### Rendering strategy - Render the dashboard shell and header immediately. - Use fixed-height skeletons to prevent cumulative layout shift. - Load the summary endpoint once for above-the-fold content. - Defer branch directory and heavy charts using `IntersectionObserver`. - Load Chart.js components only when their container approaches the viewport. - Remove duplicate derived state where `useMemo` is sufficient. - Remove unused `activeEmployeeData` and unused refresh constants. - Memoize stable chart datasets and options. - Limit the request panel to recent rows instead of rendering the complete history. ### Heavy data handling - Do not load the full employee list during initial dashboard render. - Paginate branch employees. - Debounce branch-employee search by 250–300 ms. - Use virtualization only if a modal can display more than approximately 100 rows after pagination. ### Modal and accessibility requirements - Trap focus while a modal is open. - Restore focus to the trigger after closing. - Support Escape to close. - Label search, refresh, approve, reject, and pagination controls. - Announce loading and mutation results through an ARIA live region. - Keep keyboard and touch targets at least 44 CSS pixels where practical. - Preserve the existing mobile-first single-column layout. ## 5. Environment, Feature Flags, and Dependencies Recommended configuration: ```env VITE_DASHBOARD_SUMMARY_API_ENABLED=false VITE_DASHBOARD_DEFER_WIDGETS=true VITE_DASHBOARD_CACHE_TTL_MS=15000 DASHBOARD_RECENT_REQUEST_LIMIT=10 ``` No new frontend package is required. Use existing React and Axios capabilities plus `IntersectionObserver`, `AbortController`, and the Performance API. Rollout sequence: 1. Deploy the new API and indexes with the feature flag disabled. 2. Verify brand isolation and response performance. 3. Deploy the frontend hook and components. 4. Enable the optimized path for internal users. 5. Compare metrics and errors against the legacy path. 6. Enable for all users. 7. Remove legacy dashboard fetching only after stabilization. ## 6. Audit and Compliance Strategy Read-only dashboard loads should not generate permanent audit entries. Dashboard mutations must record: - Actor user ID and role - Company and brand IDs - Entity type and entity ID - Previous state - New state - Timestamp - Request/correlation ID - Source IP and user agent where appropriate The audit insert must occur in the same database transaction as the mutation. Application roles must not have permission to update or delete audit rows. At minimum, audit: - Attendance request approval - Attendance request rejection - Employee branch reassignment ## 7. Testing and QA Strategy ### Backend tests - Authorized brand returns only its own employees and attendance. - Cross-brand requests return 403 or 404 without leaking existence. - Invalid dates return 422. - Empty brands return a successful zero/empty response. - Recent request limits are enforced. - Queries use prepared statements. - `EXPLAIN` confirms intended indexes. - Response fields and types remain stable. - Response size stays within the agreed performance budget. ### Frontend tests - Multiple widgets use one summary request. - React `StrictMode` does not cause duplicate network traffic. - Manual refresh bypasses the TTL. - Brand switching never renders old-brand data. - Aborted requests do not surface as user-facing errors. - Section errors do not blank successful sections. - Approve/reject invalidates recent requests and counts. - Branch reassignment invalidates only affected branch data. ### Regression tests - Login and redirect to dashboard - Sidebar dashboard navigation - Brand switching - Calendar task behavior - Attendance request approval/rejection - Branch modal and assignment - Attendance chart interactions - Department chart interactions ### UI and E2E tests - Desktop, tablet, and mobile viewport checks - Keyboard-only navigation - Modal focus trap and focus restoration - Screen-reader labels and live announcements - Slow 3G loading behavior - Offline and server-error fallback states ### Performance verification Capture cold and warm runs for: - Route navigation duration - Time to shell - Time to first useful data - Total request count - Duplicate request count - Attendance response size - Summary API server time - Chart render duration - Long tasks over 50 ms ## 8. Interactive Implementation Checklist Implementation progress is maintained separately in [`DASHBOARD-PERFORMANCE-OPTIMIZATION-CHECKLIST.md`](./DASHBOARD-PERFORMANCE-OPTIMIZATION-CHECKLIST.md). Execute that checklist strictly from top to bottom. Stop after each item for review and verification. ## 9. Final Hand-off Requirements When all checklist items are complete, provide one final summary containing: - Files added, changed, and removed - Database migrations and indexes executed - New and modified endpoints - Frontend components and hooks connected - Cache keys, TTLs, and invalidation rules - Security and brand-isolation verification - Audit coverage - Test commands and actual results - Before/after request count - Before/after payload size - Cold and warm dashboard timing - Known limitations - Feature-flag rollback procedure ## Acceptance Criteria The measurable acceptance criteria are tracked in the separate implementation checklist. The optimization is complete only after every implementation and acceptance item in that document is marked complete with recorded evidence.