Repository navigation
Frontend Architecture Overview
| Attribute | Value |
|---|---|
| Name | Waste Plus |
| Stack | React 19 · TypeScript 5.9 · Vite 7 · TanStack Query 5 · TanStack Router 1 |
| UI Kit | Carbon Design System (IBM) |
| Auth | AWS Amplify 6 + Cognito (IDIR / BCeID) |
| State | React Context + notification event pipeline + TanStack Query server cache |
| Testing | Vitest (unit) · Playwright (E2E / a11y) |
| PWA | vite-plugin-pwa + Workbox (scaffolded, with lifecycle notifications wired) |
- Feature Flags: shared guidance for rollout strategy, operational safeguards, and lifecycle governance of toggled behavior.
graph TD
Root["main.tsx — StrictMode"]
Root --> AuthProv["AuthProvider"]
AuthProv --> QCP["QueryClientProvider"]
QCP --> PrefProv["PreferenceProvider"]
PrefProv --> ThemeProv["ThemeProvider"]
ThemeProv --> NotifProv["NotificationProvider"]
NotifProv --> PageTitleProv["PageTitleProvider"]
PageTitleProv --> App["App"]
App --> RP["RouterProvider (router singleton from router.tsx)"]
App --> RQDevtools["ReactQueryDevtools"]
RP --> RootRoute["__root — RootLayout (title sync, devtools in DEV)"]
RootRoute --> AuthLayout["_authenticated — auth/status gate + one Layout shell"]
AuthLayout --> TierLayouts["_admin / _client-access — role-tier layouts"]
TierLayouts --> Leaf["Page Content (leaf route component)"]
AuthLayout --> Header["LayoutHeader"]
AuthLayout --> SideNav["LayoutSideNav"]
Application notifications flow through NotificationProvider, not through a
global DOM event bus. The provider exposes toast display, event subscription,
and scoped banner delivery through context-backed hooks.
graph LR
QueryHooks["Shared Query Hooks"] --> Provider["NotificationProvider"]
ServiceWorker["registerServiceWorker.ts"] --> Bridge["eventBridge.ts"]
Bridge --> Provider
Provider --> Toasts["Global Toast Notifications"]
Provider --> ScopedHooks["useScopedNotification"]
ScopedHooks --> Pages["WasteSearchPage / MyClientListPage"]
The routing system uses @tanstack/react-router with file-based routing: every URL is a file under src/routes/, and the TanStack Router Vite plugin generates src/routeTree.gen.ts (committed, because tsc runs before vite build). The router is created once at module load in src/router.tsx as a singleton — no runtime route tree switching. Access control is structural: the pathless _authenticated layout gates auth/status, the _admin / _client-access layouts gate roles, and feature flags are checked in each route's beforeLoad.
graph LR
subgraph "Top-level routes"
Landing["/ — LandingPage (public-only)"]
Dashboard["/dashboard — DashboardRedirect"]
NoRole["/no-role — NoRolePage"]
Unauth["/unauthorized — RoleErrorPage"]
end
subgraph "_authenticated — auth + status gate, one Layout shell"
Clients["/clients — MyClientListPage"]
Search["/search — WasteSearchPage"]
RUDetails["/reporting-units/$ruId — ReportingUnitDetailsPage"]
RUBlock["/reporting-units/$ruId/$blockId — ReportingUnitBlockDetailsPage"]
Config["/configuration/** — admin tier"]
CatchAll["$/ catch-all — NotFoundPage"]
end
AuthLayout["_authenticated.tsx layout"]
AuthLayout -->|validates| UserAccessValidation["computeAuthRedirect → getUserAccessStatus"]
RUDetails -->|loader| RULoader["reportingUnitLoader"]
RUBlock -->|query hook| RUDetailsQuery["useReportingUnitDetailsQuery"]
RUDetails -->|renders blocks list| RUBlocksList["ReportingUnitBlocksList"]
-
2026-10-22: Replaced the HOC-guard routing diagram with the file-based router layout: top-level routes, the
_authenticatedgate shell, role-tier layouts, thebeforeLoadflag gates, and the catch-all (file-based router migration). -
2026-10-01: Renamed the block details route to
/reporting-units/$ruId/$blockId — ReportingUnitBlockDetailsPage(a block is a direct child of its reporting unit, mirroring the backendAttachmentControllermapping) and added aReportingUnitBlocksListrender edge fromReportingUnitDetailsPage(issue #1369, decision D8). -
2026-10-01: Added
/reporting-units/$ruId/blocks/$blockId — ReportingUnitBlockDetailsPageroute with auseReportingUnitDetailsQueryquery edge (no loader, no feature flag; issue #1369).useReportingUnitDetailsQuerywas already listed in the shared query hooks. -
2026-05-19: Added
/reporting-units/$ruId — ReportingUnitDetailsPageroute withreportingUnitLoaderloader edge. - 2026-04-01: Initial routing diagram published.
Feature Flag Controlled The
/reporting-units/$ruIdroute is controlled by thereporting-unit-details-enabledfeature flag. The route'sbeforeLoadthrows a router-level 404 when the flag is off, before any component renders (fail-closed: disabled means the URL is not a route). Check the current flag value and rollout status in Feature Flags.
Route lifecycle:
- The Vite plugin generates the route tree from
src/routes/intosrc/routeTree.gen.ts;router.tsxcreates the router once at module load. -
beforeLoadhooks run first: feature-flag gates throw a router-levelnotFound()when their flag is off. - Routes with a
loaderresolve data before the component mounts.reportingUnitLoaderreceives{ context, params }and callscontext.queryClient.ensureQueryData()to pre-fetch and cache reporting unit data; the page component reads the result viauseLoaderData({ strict: false }). The loader converts 404/403 API responses tonotFound(); flag checks stay inbeforeLoad. - React renders the match chain:
__root(title sync) →_authenticated(auth/status gate, one<Layout>shell) → role-tier layout where the URL belongs (_admin,_client-access). -
DashboardRedirecton/dashboardresolves post-login redirect targets after the OAuth callback. - Errors bubble to the
Layout-wrapped error components on__rootand_authenticated; the router-leveldefaultErrorComponentstays bare as the last-resort fallback so no error renders two shells.
-
App.tsxmounts<RouterProvider router={router} />directly, which handles async initialization internally. - No explicit
React.lazy()is used at the component level currently — the router'sautoCodeSplittingis the primary code-split boundary. - Manual chunks are configured in Vite's
rollupOptions.manualChunksfor vendor splitting.
- Route-level: the
__rootand_authenticatedroutes supplyLayout-wrapped error components (<Layout><GlobalErrorPage /></Layout>) so an error renders exactly one application shell; the router-leveldefaultErrorComponentis a bareGlobalErrorPagekept as the last-resort fallback. -
GlobalErrorPagedisplays the error message from theerrorprop forwarded by the router. - No component-level
ErrorBoundarywrappers are present; errors bubble to the route boundary.
Configured in src/config/react-query/config.ts:
| Option | Value | Rationale |
|---|---|---|
staleTime |
3 hours | Data changes infrequently; aggressive caching reduces server load |
gcTime |
3 hours | Matches stale time for consistent cache lifetime |
refetchOnMount |
false |
Prevents redundant fetches on re-renders |
refetchOnWindowFocus |
false |
Avoids user-surprising background refreshes |
retry |
noRetry (always false) |
Errors surface immediately; retry is handled at the UI layer if needed |
The QueryClient is instantiated once in main.tsx and provided via <QueryClientProvider>.
- A centralized query hook layer lives in
src/config/react-query/hooks.ts, providing shared hooks:useSearchReportingUnitsQuery,useCodesQuery,useWasteSearchFilterOptionsQueries,useDistrictOptionsQuery,useForestClientsByNumbersQuery,useMyForestClientsQuery,useReportingUnitDetailsQuery,useReportingUnitExpandQuery,useClientLookupQuery. - Query keys are centralized in
src/config/react-query/queryKeys.tsusing a typed factory object (queryKeys) organized by domain, eliminating key duplication and preventing collisions. - Shared query input contracts are exported from the key factory (for example
ReportingUnitsQueryParams) and consumed directly by hooks, preventing key-builder and hook parameter drift. - Components consume the shared hooks rather than calling
useQuerydirectly. - Shared query hooks can emit user-facing error notifications through a
notificationTargetoption, keeping error-to-notification translation out of feature components. - Scoped query hooks deduplicate inline error notifications by tracking React Query
errorUpdatedAt, preventing repeated banner emission while the same error persists across renders. - Services return
CancelablePromise<T>which is consumed by the shared hooks. - Waste Search filter reference data is loaded through a single
useQueriesbatch, while still preserving separate cache entries per code resource. - The full request pipeline from component to backend:
sequenceDiagram
participant C as Component
participant H as Shared Hook (hooks.ts)
participant RQ as TanStack Query
participant S as Service (e.g. SearchService)
participant A as Axios (request.ts)
participant MW as Middleware Stack
participant BE as Backend API
C->>H: useSearchReportingUnitsQuery(input)
H->>RQ: useQuery({ queryKey, queryFn })
RQ->>S: service.method(args)
S->>A: doRequest(config, options)
A->>MW: Apply request middleware
MW->>BE: HTTP request
BE-->>MW: HTTP response
MW-->>A: Apply response/failure middleware
A-->>S: Resolved data / ProblemDetails error
S-->>RQ: CancelablePromise resolves
RQ-->>H: data / error / isLoading
H-->>C: data / error / isLoading
sequenceDiagram
participant C as Component (row action)
participant RQ as TanStack Query
participant S as UserService
participant A as Axios
participant MW as Middleware Stack
participant BE as Backend API
C->>RQ: user clicks bookmark (optimistic UI)
RQ->>S: API.user.setUserBookmarkedRu(ruId)
S->>A: PUT /api/users/bookmarks/:ruId
A->>MW: Apply middleware
MW->>BE: HTTP request
BE-->>MW: HTTP response
MW-->>A: Apply response middleware
A-->>S: resolves
RQ-->>C: mutation settled, refetch invoked
- The cache is global to the app (single
QueryClient). - No explicit cache partitioning per route or feature.
- The long stale time (3h) means navigating between pages reuses cached data.
- No explicit
queryClient.invalidateQueries()calls detected in the current codebase. - The no-retry, long-stale-time approach relies on fresh fetches only when the user navigates to a page for the first time in a session.
All HTTP communication is routed through a custom HttpClient base class (defined in src/config/api/types.ts) which wraps Axios. Each service extends HttpClient:
| Service | File | Endpoints |
|---|---|---|
UserService |
users.service.ts |
Preferences CRUD |
SearchService |
search.service.ts |
Reporting unit search, expand, user lookup |
CodesService |
codes.service.ts |
Sampling options, districts, assessment area statuses |
ForestClientService |
forestclient.service.ts |
Client lookup, search, autocomplete |
ReportingUnitService |
reportingunit.service.ts |
Reporting unit details (Zod-validated) |
All services are instantiated once in src/services/APIs.ts and share a single BackendApiConfig.
The request pipeline supports a chain of ApiMiddleware objects, each optionally providing:
-
request(config)— pre-flight config transform -
response(response)— post-success response transform -
failure(error)— error normalization / offline fallback
Active middleware:
| Middleware | Purpose |
|---|---|
problemDetailsMiddleware |
Normalizes all backend errors to RFC 7807 ProblemDetails
|
failureNotificationMiddleware |
Emits toast notifications for unscoped API failures and suppresses duplicates for scoped requests using notificationTarget / suppressFailureNotification metadata |
offlineDataMiddleware |
Caches GET responses in IndexedDB for offline retrieval |
offlineMutationMiddleware |
Queues POST/PUT/PATCH/DELETE in IndexedDB when offline |
problemDetailsMiddleware and failureNotificationMiddleware are registered globally through BackendApiConfig.MIDDLEWARE in src/services/APIs.ts. Service methods no longer attach problemDetailsMiddleware() per request unless a call needs a truly request-specific middleware chain.
All errors are converted to ProblemDetails format:
type ProblemDetails = {
type?: string; // URI reference ("about:blank" default)
title: string; // Human-readable summary
status: number; // HTTP status (0 for network errors)
detail?: string; // Occurrence-specific explanation
instance?: string;
};Axios error codes (ERR_NETWORK, ECONNABORTED, etc.) are mapped to user-friendly titles and descriptions.
- Auth token is retrieved asynchronously from Cognito cookies via
getUserTokenFromCookie(). -
BackendApiConfig.TOKENis initialized toundefinedand resolved at request time via an async resolver; no synchronous token snapshot is taken at startup. - Token is injected as
Bearerheader in every request through thegetHeaders()pipeline. - Token refresh is handled by
AuthProviderwhich re-fetches the session every 3 minutes.
-
No automatic retry —
noRetryalways returnsfalse. - No exponential backoff is implemented.
- This is a deliberate design choice; the UI surfaces errors immediately.
Types are organized by domain and co-located with their service:
| Domain | Type File |
|---|---|
| Auth | src/context/auth/types.ts |
| Search | src/services/search.types.ts |
| Forest Client | src/services/forestclient.types.ts |
| Pagination | src/services/pagination.types.ts |
| API Layer | src/config/api/types.ts |
| PWA | src/config/pwa/types.ts |
| Preferences | src/context/preference/types.ts |
| Reporting Unit | src/services/reportingUnit.types.ts |
- Service methods are fully typed:
CancelablePromise<PageableResponse<T>>. -
PageableResponse<T>provides generic pagination. -
ProblemDetailsis the standard error shape.
-
strict: truein all tsconfig files. -
noUnusedLocals,noUnusedParameters,noFallthroughCasesInSwitchenabled. -
verbatimModuleSyntaxenforced for explicit import/export types. - Role enums and const assertions (
as const) reduce string-literal drift.
- Some
anyusage inrequest.ts(eslint-disabled) for Axios generic compatibility. - New feature flags must be added manually to the
featureFlagsSchemainenv.ts.
graph LR
Vite --> React["@vitejs/plugin-react"]
Vite --> TSPaths["vite-tsconfig-paths"]
Vite --> PWA["vite-plugin-pwa (VitePWA)"]
PWA --> Workbox["workbox (injectManifest)"]
| Alias | Target |
|---|---|
@/ |
./src/ |
Resolved consistently in vite.config.ts, tsconfig.app.json, and eslint.config.js.
Two-layer approach:
-
Build-time: Vite
import.meta.envvariables (prefixedVITE_). -
Runtime:
config.jsandparams.jscan extendwindow.configat deploy time. -
src/env.tsvalidates runtime string config keys before merging them intoenv. -
src/env.tsvalidatesVITE_FEATURE_FLAGSseparately and exposes typed booleans throughfeatureFlags.
This allows container-based deployments to override env vars without rebuilding.
src/env.ts is the single boundary for deploy-time frontend configuration.
| Input | Validation | Output |
|---|---|---|
import.meta.env string values |
Filtered to string entries | env |
window.config string config |
Strict allowlist via Zod | env |
VITE_FEATURE_FLAGS from Vite or window.config
|
Strict typed object via Zod | featureFlags |
Rules:
- Unknown
window.configkeys are rejected at startup. - Runtime config values must match the expected type exactly.
- Feature flags are not free-form; each flag must be declared in
featureFlagsSchema. - Deployment overrides must extend
window.configrather than writing frontend flags tolocalStorage.
| Chunk | Contents |
|---|---|
react-vendor |
react, react-dom |
carbon-charts |
@carbon/charts |
carbon |
@carbon/* |
aws |
aws-amplify |
query |
@tanstack/* |
router |
react-router* |
utils |
axios, lodash, luxon, idb |
Output directory: build/.
-
optimizeDeps.includepre-bundles large dependencies for faster cold starts. -
chunkSizeWarningLimit: 1024 KB. -
maximumFileSizeToCacheInBytes: 6 MB for PWA precaching.
-
Config: Embedded in
vite.config.tsundertest. -
Environment:
jsdom. -
Pattern:
*.unit.test.{ts,tsx}files co-located alongside source. -
Setup:
setup-env.tsmocks AWS Amplify, APIs, fetch, and browser APIs (ResizeObserver, matchMedia). -
Custom matchers:
toContainText(),toBeEmptyDOMElement(). - Coverage: v8 provider, 80% threshold for lines/functions/branches/statements.
- Reporters: default + JUnit XML.
-
Config:
playwright.config.ts. - Projects: chromium, firefox, webkit, mobile-chromium, mobile-safari, a11y-chromium.
- Auth: Mock JWT cookies for test isolation; optional real login flow.
-
API mocking:
mockApi(),mockApiResponses(),mockApiResponsesWithStub()utilities. -
Accessibility:
@axe-core/playwrightwith configurable impact thresholds. -
E2E patterns: Files co-located per page (
*.e2e.test.tsx).
| Layer | Framework | Location |
|---|---|---|
| Pure logic, hooks, utils | Vitest |
*.unit.test.ts next to source |
| Component render + interaction | Vitest + Testing Library |
*.unit.test.tsx next to source |
| Page flows, navigation, integration | Playwright |
*.e2e.test.tsx in src/pages/
|
| Accessibility | Playwright + Axe |
*.a11y.test.tsx in src/pages/
|
| Artifact | Status |
|---|---|
vite-plugin-pwa configured |
Done |
manifest with icons, screenshots |
Done |
service-worker.ts with Workbox |
Done |
registerServiceWorker.ts |
Done |
| IndexedDB offline cache middleware | Done |
| Offline mutation queue middleware | Done |
| Background sync handlers | Placeholder |
| Periodic sync handlers | Placeholder |
| Online/offline status hook | Done |
| Feature flag for offline mode | Scaffolded |
stateDiagram-v2
[*] --> Online
Online --> Offline: Network lost
Offline --> Online: Network restored
state Online {
[*] --> FetchFromAPI
FetchFromAPI --> CacheInIDB: offlineDataMiddleware
CacheInIDB --> RenderData
}
state Offline {
[*] --> ReadFromIDB
ReadFromIDB --> RenderCachedData
MutationQueued: offlineMutationMiddleware queues
RenderCachedData --> MutationQueued: User action
}
Online --> BackgroundSync: Replay queued mutations
BackgroundSync --> Online
- Enable the offline mode feature flag in
onlineStatusStore.ts. - Apply
offlineDataMiddlewareandofflineMutationMiddlewareto all service endpoints. - Implement
syncMutations()insyncHandlers.tsto replay queued mutations. - Implement
periodicRefresh()insyncHandlers.tsto keep cached data fresh. - Add offline-aware UI indicators (connection status banner, sync progress).
- Test SW registration and update flow in production-like environments.
-
Static assets: Precache via Workbox (
globPatterns). - API responses: Stale-while-revalidate for read-heavy endpoints (search, codes).
- Mutations: Queue-and-replay with conflict resolution.
-
Manifest: Already configured with icons, screenshots, and
display: standalone.
graph TD
subgraph "Server State (TanStack Query)"
QC[QueryClient Cache]
QC -->|staleTime: 3h| Data
end
subgraph "Client State (React Context)"
Auth[AuthContext — user, login, logout]
Layout[LayoutContext — sidenav, header panel]
Theme[ThemeContext — Carbon theme]
Pref[PreferenceContext — user prefs]
Notif[NotificationContext — toasts]
PT[PageTitleContext — page title]
end
subgraph "URL State"
SP["/search URL — route-owned via validateSearch"]
SP -->|Zod schema in searchParams.ts validates| Filters
Filters -->|typed navigation writes| SP
end
subgraph "Local State"
Comp[Component useState/useReducer]
end
Auth -->|token| QC
Pref -->|theme pref| Theme
SP -->|filter hydration| Comp
graph LR
Source["TypeScript + SCSS + Assets"]
Source --> TSC["tsc -b (type check)"]
TSC --> ViteBuild["vite build"]
ViteBuild --> Rollup["Rollup (manual chunks)"]
Rollup --> Minify["Terser minification"]
Minify --> PWAPlugin["vite-plugin-pwa"]
PWAPlugin --> SW["Service Worker (injectManifest)"]
PWAPlugin --> Manifest["manifest.webmanifest"]
SW --> BuildDir["build/"]
Manifest --> BuildDir
Rollup --> BuildDir
BuildDir --> Docker["Docker (Caddy)"]
Docker --> OpenShift["OpenShift (deploy)"]
| Layer | Technology |
|---|---|
| Web server | Caddy (via Caddyfile) |
| Container | Docker (via Dockerfile) |
| Orchestration | OpenShift (via openshift.deploy.yml) |
| Runtime config |
public/data/config.js injected at deploy time |
This wiki documents NR Waste Plus for users and developers. Verify security and deployment guidance against the current application configuration before applying it elsewhere.
- Report an issue or ask a question in the GitHub repository.
- Follow the Contributing Guidelines to propose an update.
- Read the Code of Conduct.
Collection indexes support human browsing and machine retrieval. Keep both current when adding or moving wiki pages.
NR Waste Plus Wiki | GitHub Repository