Skip to content

High-level approach

  • Goal: Introduce a global, consistent error and permission handling experience that can be reused by all pages (system pages, role-scoped pages, and shared pages) without breaking the existing feature-first / domain-based folder structure.
  • Strategy: Leverage the existing **RoleBasedRouter**, **PermissionGuard**, **UnauthorizedPage**, **PermissionsError**, and **handleApiError** primitives to create:
    • A top-level error boundary / layout wrapper that can catch route-level and data-loading failures and render friendly guidance.
    • A generic page-level error shell component that domain pages can use for API/data errors.
    • A unified permission error surface that is used consistently whenever access is denied (both from route guards and from backend error codes).

Current architecture understanding

  • Role-based routing
    • RoleBasedRouter in [apps/web/src/router/RoleBasedRouter.tsx](apps/web/src/router/RoleBasedRouter.tsx):
      • Checks auth and permissionsLoadFailed via useAuth.
      • When permissions cannot be loaded, it renders PermissionsError.
      • Otherwise it wraps the main app in MainLayout and delegates feature routing to AppRoutes.
    • AppRoutes in [apps/web/src/router/AppRoutes.tsx](apps/web/src/router/AppRoutes.tsx):
      • Defines feature-first routes (/users, /maintenance/*, /system/*, /department, /team, /reports, /requests, etc.).
      • Uses withGuard(pathKey, element) + PermissionGuard + getRouteAccess to protect routes.
      • Shared authenticated pages like /profile, /settings, /security, /sessions, /notifications are unguarded but still require auth via the outer RoleBasedRouter.
  • Permission and role logic
    • RouteAccessConfig and ROUTE_ACCESS in [apps/web/src/router/config/routeConfig.ts](apps/web/src/router/config/routeConfig.ts) define required permissions and role fallbacks for each feature key (e.g. users, maintenance, systemMonitor).
    • PermissionGuard in [apps/web/src/router/guards/PermissionGuard.tsx](apps/web/src/router/guards/PermissionGuard.tsx):
      • Enforces login, treats super_admin as bypass.
      • When the user has explicit permissions from the API, it checks requiredPermissions / requiredAnyPermissions; otherwise it falls back to allowedRoles / minimumRole.
      • On failure it renders a fallback (defaults to <Navigate to="/unauthorized" />).
    • RoleGuard in RoleBasedRouter.tsx supports additional role/permission checks for non-config-based scenarios, using the same hasPermission / hasMinimumRoleLevel helpers.
  • Sidebar and navigation
    • Sidebar in [apps/web/src/components/navigation/Sidebar.tsx](apps/web/src/components/navigation/Sidebar.tsx) (barrel: @/components/navigation):
      • Uses getRoleNavigation(userRole) to determine main nav entries per role.
      • Injects a System section where each item (/system/monitor, /system/logs, /system/audit, /system/settings) is shown based on hasPermission(user, permission) unless the user is super_admin.
      • This means nav visibility is permission-aware and already aligned with ROUTE_ACCESS.
  • Existing error handling primitives
    • PermissionsError in [apps/web/src/router/PermissionsError.tsx](apps/web/src/router/PermissionsError.tsx) handles "permissions cannot be loaded" as a full-page error with clear retry/logout actions.
    • UnauthorizedPage in [apps/web/src/domains/shared/pages/unauthorized/index.tsx](apps/web/src/domains/shared/pages/unauthorized/index.tsx) is a full-page permission-denied view with a CTA back to /dashboard.
    • handleApiError and useErrorHandler in [apps/web/src/lib/error/handler.ts](apps/web/src/lib/error/handler.ts) already centralize API error codes → toast/redirect/logout/reload behavior, with specific mapping for permission codes (FORBIDDEN, ERR_PERMISSION, INSUFFICIENT_PERMISSIONS).
    • Many domain pages (e.g. system monitor/logs/audit) do local isError checks and render inline Card-based error UIs with minimal guidance.

Target state

  • Unified permission failure UX
    • All permission denials (from route-level PermissionGuard, RoleGuard, and backend ApiError codes) should ultimately surface either:
      • The **UnauthorizedPage** (full-page, when the user navigated to a page they cannot access), or
      • A permission-specific inline/info component when the user remains on a page but an action was blocked.
  • Unified data/API error UX
    • All page-level data fetching hooks (useSystemMonitoringQuery, useSystemLogsQuery, useSystemAuditQuery, and other TanStack queries in domains/shared/pages/**) should have:
      • A shared page shell for unrecoverable errors or when there is no data yet (e.g. initial load failed).
      • Reuse of the central handleApiError logic for toasts/redirects where appropriate.
  • Top-level catch-all surface
    • A global app error boundary or layout-level error wrapper plugged into MainLayout or RoleBasedRouter that:
      • Prevents ugly React error boundaries from leaking to the user.
      • Shows a friendly general error page for unexpected runtime errors.

Proposed components & wiring

  1. **AppErrorBoundary for runtime React errors**
  • Location: New component under something like [apps/web/src/router/AppErrorBoundary.tsx](apps/web/src/router/AppErrorBoundary.tsx) or [apps/web/src/components/layout/AppErrorBoundary.tsx](apps/web/src/components/layout/AppErrorBoundary.tsx) to keep structure clear.
  • Behavior:
    • Implements a standard React error boundary (class or react-error-boundary wrapper) that catches render errors under MainLayout.
    • Renders a reusable **GlobalErrorPage** component with:
      • High-level message ("Something went wrong").
      • Optional context text.
      • Actions: "Try again" (reload), "Back to dashboard", maybe "Sign out".
    • Logs the error (e.g. console.error now, later can integrate with your workers audit/logging pipeline).
  • Integration:
    • Wrap the MainLayout + Routes portion in RoleBasedRouter with AppErrorBoundary so all protected routes are covered.
  1. **GlobalErrorPage component (generic full-page error view)**
  • Location: [apps/web/src/domains/shared/pages/errors/GlobalErrorPage.tsx](apps/web/src/domains/shared/pages/errors/GlobalErrorPage.tsx) or similar under domains/shared/pages/errors to keep with the domains/shared convention.
  • Behavior:
    • Accepts props like title, description, primaryAction, secondaryAction, variant ('runtime' | 'network' | 'permissions' | 'notFound'), optional icon.
    • Can be composed to mirror UnauthorizedPage and PermissionsError as pre-configured variants, or reuse their visual style.
  • Usage:
    • Used by AppErrorBoundary for runtime errors.
    • Can later be used by route-level fallbacks or deep domain pages for full-page error states.
  1. Refine permission-denied routing behavior
  • Route-level (navigation to restricted pages):
    • Keep PermissionGuard and ROUTE_ACCESS as-is for central RBAC logic.
    • Standardize its fallback to always go through **UnauthorizedPage** (either via Navigate to /unauthorized as now, or by rendering UnauthorizedPage directly) to keep UX consistent.
    • Optionally, extend UnauthorizedPage to accept a reason query param or internal state (e.g. ?reason=missing-permission&feature=systemAudit) and display a contextual message.
  • Action-level (API denies permission):
    • Rely on handleApiError mappings for FORBIDDEN, ERR_PERMISSION, INSUFFICIENT_PERMISSIONS to show toasts.
    • For certain high-value actions (e.g. audit.export, user.permissions), domain pages can also render an inline **PermissionInlineNotice** component near the disabled controls if the backend returns a permission error.
    • This inline component can live under domains/shared/components/PermissionNotice.tsx and take props like requiredPermission and context.
  1. **PageErrorState / DataErrorState component for query-based pages**
  • Location: [apps/web/src/domains/shared/components/PageErrorState.tsx](apps/web/src/domains/shared/components/PageErrorState.tsx) (or within domains/shared/components) to be reused across pages like system monitor/logs/audit, maintenance, users, etc.
  • Behavior:
    • Props: title, description, onRetry, optional error, and flags like isInitialLoad.
    • Renders a Card with icon (e.g. AlertCircle), friendly copy, and an optional retry button.
    • Optionally uses handleApiError under the hood when an ApiError is passed, to ensure codes are handled centrally while still showing a local card.
  • Usage:
    • Replace repeated inline error card logic in:
      • SystemMonitorPage (isError || !data block).
      • SystemLogsPage ((isError || error) block).
      • SystemAuditPage ((isError || error) block).
      • Similar list/detail pages under domains/shared/pages/users, maintenance, etc. where isError and error are checked.
    • Maintain the existing feature layouts, just swap the error block to the shared component for consistency.
  1. Top-level data error wrapper for route-level loaders (optional/next)
  • If you introduce route-level data loading (React Router loaders or equivalent), add a **RouteErrorBoundary** in the router config that uses the same GlobalErrorPage so route errors are visually consistent with runtime and query errors.

Wiring into the existing folder tree

  • Preserve feature-first / domains structure
    • Keep RBAC and routing files under src/router as they are (RoleBasedRouter, AppRoutes, routeConfig, PermissionGuard, PermissionsError).
    • Add shared error view components under src/domains/shared (e.g. pages/errors, components/PageErrorState.tsx) so they are clearly part of the shared app surface and easy to reuse from any domain.
    • Keep handleApiError where it is (src/lib/error/handler.ts) and simply consume it from new shared components as needed.
  • Minimal changes to core primitives
    • Avoid changing the semantics of PermissionGuard or ROUTE_ACCESS to avoid regressions; only:
      • Optionally normalize their fallback usage to route consistently to /unauthorized.
      • Optionally add some telemetry hooks later.

Step-by-step implementation plan

  1. Introduce GlobalErrorPage (shared full-page error)
  • Create domains/shared/pages/errors/GlobalErrorPage.tsx with props for title/description/actions.
  • Implement variants for runtime/general errors and (optionally) a fallback for unknown permission issues.
  • Align the visual design with UnauthorizedPage / PermissionsError (use the same Card/Button components and spacing).
  1. Create AppErrorBoundary and wrap MainLayout routes
  • Add router/AppErrorBoundary.tsx implementing a standard React error boundary that renders GlobalErrorPage.
  • In RoleBasedRouter, wrap the <MainLayout> ... </MainLayout> block with AppErrorBoundary so all authenticated routes share this safety net.
  1. Create PageErrorState component for data/API errors
  • Add domains/shared/components/PageErrorState.tsx.
  • Provide sensible defaults ("We couldn’t load this data", generic retry) and allow domain pages to override copy.
  • Optionally accept an ApiError | unknown value and internally call handleApiError to reuse central mapping, while still rendering the card.
  1. Refactor key shared/system pages to use PageErrorState
  • Update SystemMonitorPage, SystemLogsPage, SystemAuditPage to:
    • Delegate their error blocks (isError || !data, (isError || error)) to PageErrorState with appropriate copy and onRetry.
  • Identify a couple more high-visibility shared pages (e.g. users list, maintenance list) and convert their error UI to the shared component, keeping the feature layouts as-is.
  1. Standardize permission-denied UX
  • Ensure PermissionGuard uses a consistent fallback:
    • Either keep Navigate to /unauthorized and ensure that page is the single source of truth for permission-denied messaging.
    • Or, in specific cases, render a full-page GlobalErrorPage permission variant when you don’t want a location change.
  • Optionally extend UnauthorizedPage to read an optional reason (via query string or state) to surface more context without changing routing logic.
  1. Optional: inline permission notices for critical actions
  • Add a small shared PermissionNotice component under domains/shared/components for cases like audit.export or user permission editing.
  • Use it where you currently compute canExport / canEditPermissions, so the user sees why a button is disabled.
  1. Gradual adoption across domains
  • Over time, update additional domain pages in domains/shared/pages/**, domains/hod/**, domains/employee/** to:
    • Use PageErrorState for data-loading failures.
    • Use handleApiError / useErrorHandler where they currently manually log or ignore errors.
  • This keeps the legendary folder tree intact while giving you consistent, discoverable UX for errors and permissions.

Data & permission flow overview

mermaid
flowchart TD
  user[User] --> ui[React UI]
  ui --> roleBasedRouter[RoleBasedRouter]
  roleBasedRouter -->|auth & permissions| mainLayout[MainLayout]
  mainLayout --> appRoutes[AppRoutes + PermissionGuard]
  appRoutes --> domainPages[Domains (shared/hod/employee)]

  domainPages --> apiCalls[API services + TanStack queries]
  apiCalls -->|errors| handleApiError[handleApiError]
  handleApiError --> toasts[Guided toasts / redirects]

  appRoutes --> unauthorized[/UnauthorizedPage/]
  roleBasedRouter --> permissionsError[/PermissionsError/]

  mainLayout --> appErrorBoundary[AppErrorBoundary]
  appErrorBoundary --> globalError[GlobalErrorPage]

  domainPages --> pageErrorState[PageErrorState]
  pageErrorState --> inlineGuidance[Inline retry/guidance]

This plan keeps your existing routing and RBAC architecture intact, centralizes error and permission UX into shared components, and wires them at the top-level so any page can benefit from consistent, easy-to-understand user guidance for permission issues and backend/data/API errors.