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
RoleBasedRouterin[apps/web/src/router/RoleBasedRouter.tsx](apps/web/src/router/RoleBasedRouter.tsx):- Checks auth and
permissionsLoadFailedviauseAuth. - When permissions cannot be loaded, it renders
PermissionsError. - Otherwise it wraps the main app in
MainLayoutand delegates feature routing toAppRoutes.
- Checks auth and
AppRoutesin[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+getRouteAccessto protect routes. - Shared authenticated pages like
/profile,/settings,/security,/sessions,/notificationsare unguarded but still require auth via the outerRoleBasedRouter.
- Defines feature-first routes (
- Permission and role logic
RouteAccessConfigandROUTE_ACCESSin[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).PermissionGuardin[apps/web/src/router/guards/PermissionGuard.tsx](apps/web/src/router/guards/PermissionGuard.tsx):- Enforces login, treats
super_adminas bypass. - When the user has explicit permissions from the API, it checks
requiredPermissions/requiredAnyPermissions; otherwise it falls back toallowedRoles/minimumRole. - On failure it renders a
fallback(defaults to<Navigate to="/unauthorized" />).
- Enforces login, treats
RoleGuardinRoleBasedRouter.tsxsupports additional role/permission checks for non-config-based scenarios, using the samehasPermission/hasMinimumRoleLevelhelpers.
- Sidebar and navigation
Sidebarin[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
Systemsection where each item (/system/monitor,/system/logs,/system/audit,/system/settings) is shown based onhasPermission(user, permission)unless the user issuper_admin. - This means nav visibility is permission-aware and already aligned with
ROUTE_ACCESS.
- Uses
- Existing error handling primitives
PermissionsErrorin[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.UnauthorizedPagein[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.handleApiErroranduseErrorHandlerin[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
isErrorchecks and render inlineCard-based error UIs with minimal guidance.
Target state
- Unified permission failure UX
- All permission denials (from route-level
PermissionGuard,RoleGuard, and backendApiErrorcodes) 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.
- The
- All permission denials (from route-level
- Unified data/API error UX
- All page-level data fetching hooks (
useSystemMonitoringQuery,useSystemLogsQuery,useSystemAuditQuery, and other TanStack queries indomains/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
handleApiErrorlogic for toasts/redirects where appropriate.
- All page-level data fetching hooks (
- Top-level catch-all surface
- A global app error boundary or layout-level error wrapper plugged into
MainLayoutorRoleBasedRouterthat:- Prevents ugly React error boundaries from leaking to the user.
- Shows a friendly general error page for unexpected runtime errors.
- A global app error boundary or layout-level error wrapper plugged into
Proposed components & wiring
**AppErrorBoundaryfor 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-boundarywrapper) that catches render errors underMainLayout. - 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.errornow, later can integrate with your workers audit/logging pipeline).
- Implements a standard React error boundary (class or
- Integration:
- Wrap the
MainLayout+Routesportion inRoleBasedRouterwithAppErrorBoundaryso all protected routes are covered.
- Wrap the
**GlobalErrorPagecomponent (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 underdomains/shared/pages/errorsto keep with the domains/shared convention. - Behavior:
- Accepts props like
title,description,primaryAction,secondaryAction,variant('runtime' | 'network' | 'permissions' | 'notFound'), optionalicon. - Can be composed to mirror
UnauthorizedPageandPermissionsErroras pre-configured variants, or reuse their visual style.
- Accepts props like
- Usage:
- Used by
AppErrorBoundaryfor runtime errors. - Can later be used by route-level fallbacks or deep domain pages for full-page error states.
- Used by
- Refine permission-denied routing behavior
- Route-level (navigation to restricted pages):
- Keep
PermissionGuardandROUTE_ACCESSas-is for central RBAC logic. - Standardize its
fallbackto always go through**UnauthorizedPage** (either viaNavigateto/unauthorizedas now, or by renderingUnauthorizedPagedirectly) to keep UX consistent. - Optionally, extend
UnauthorizedPageto accept areasonquery param or internal state (e.g.?reason=missing-permission&feature=systemAudit) and display a contextual message.
- Keep
- Action-level (API denies permission):
- Rely on
handleApiErrormappings forFORBIDDEN,ERR_PERMISSION,INSUFFICIENT_PERMISSIONSto 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.tsxand take props likerequiredPermissionandcontext.
- Rely on
**PageErrorState/DataErrorStatecomponent for query-based pages**
- Location:
[apps/web/src/domains/shared/components/PageErrorState.tsx](apps/web/src/domains/shared/components/PageErrorState.tsx)(or withindomains/shared/components) to be reused across pages like system monitor/logs/audit, maintenance, users, etc. - Behavior:
- Props:
title,description,onRetry, optionalerror, and flags likeisInitialLoad. - Renders a
Cardwith icon (e.g.AlertCircle), friendly copy, and an optional retry button. - Optionally uses
handleApiErrorunder the hood when anApiErroris passed, to ensure codes are handled centrally while still showing a local card.
- Props:
- Usage:
- Replace repeated inline error card logic in:
SystemMonitorPage(isError || !datablock).SystemLogsPage((isError || error)block).SystemAuditPage((isError || error)block).- Similar list/detail pages under
domains/shared/pages/users,maintenance, etc. whereisErroranderrorare checked.
- Maintain the existing feature layouts, just swap the error block to the shared component for consistency.
- Replace repeated inline error card logic in:
- 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 sameGlobalErrorPageso 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/routeras 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
handleApiErrorwhere it is (src/lib/error/handler.ts) and simply consume it from new shared components as needed.
- Keep RBAC and routing files under
- Minimal changes to core primitives
- Avoid changing the semantics of
PermissionGuardorROUTE_ACCESSto avoid regressions; only:- Optionally normalize their
fallbackusage to route consistently to/unauthorized. - Optionally add some telemetry hooks later.
- Optionally normalize their
- Avoid changing the semantics of
Step-by-step implementation plan
- Introduce
GlobalErrorPage(shared full-page error)
- Create
domains/shared/pages/errors/GlobalErrorPage.tsxwith 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 sameCard/Buttoncomponents and spacing).
- Create
AppErrorBoundaryand wrapMainLayoutroutes
- Add
router/AppErrorBoundary.tsximplementing a standard React error boundary that rendersGlobalErrorPage. - In
RoleBasedRouter, wrap the<MainLayout> ... </MainLayout>block withAppErrorBoundaryso all authenticated routes share this safety net.
- Create
PageErrorStatecomponent 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 | unknownvalue and internally callhandleApiErrorto reuse central mapping, while still rendering the card.
- Refactor key shared/system pages to use
PageErrorState
- Update
SystemMonitorPage,SystemLogsPage,SystemAuditPageto:- Delegate their error blocks (
isError || !data,(isError || error)) toPageErrorStatewith appropriate copy andonRetry.
- Delegate their error blocks (
- 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.
- Standardize permission-denied UX
- Ensure
PermissionGuarduses a consistentfallback:- Either keep
Navigateto/unauthorizedand ensure that page is the single source of truth for permission-denied messaging. - Or, in specific cases, render a full-page
GlobalErrorPagepermission variant when you don’t want a location change.
- Either keep
- Optionally extend
UnauthorizedPageto read an optionalreason(via query string or state) to surface more context without changing routing logic.
- Optional: inline permission notices for critical actions
- Add a small shared
PermissionNoticecomponent underdomains/shared/componentsfor cases likeaudit.exportor user permission editing. - Use it where you currently compute
canExport/canEditPermissions, so the user sees why a button is disabled.
- Gradual adoption across domains
- Over time, update additional domain pages in
domains/shared/pages/**,domains/hod/**,domains/employee/**to:- Use
PageErrorStatefor data-loading failures. - Use
handleApiError/useErrorHandlerwhere they currently manually log or ignore errors.
- Use
- 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.