-
Notifications
You must be signed in to change notification settings - Fork 1
Frontend
The TrackHub portal is a React 19 + TypeScript 7 single-page application built with Vite 8 and tested with Vitest 4. It is served as static files by nginx and talks to every backend service directly.
npm run typecheck && npm test && npm run build is the gate. tsc is the TypeScript lint gate — typescript-eslint is disabled until it supports TypeScript 7.
Related pages: Technology · Inter-Service Communication
graph TB
subgraph App["TrackHub Web"]
router["React Router<br/><i>routes.tsx</i>"]
auth_mod["Auth Context<br/><i>PKCE + token refresh mutex</i>"]
subgraph Pages["Screens"]
dash["Dashboard<br/><i>Live map + positions</i>"]
trips["Trip Manager<br/><i>Dispatch board</i>"]
geofence["Geofence Manager<br/><i>Polygons + circles</i>"]
reports["Reports<br/><i>Preview + Excel/PDF</i>"]
admin["Account Management<br/><i>Users, units, documents, drivers, alerts</i>"]
sysadmin["System Administration<br/><i>Accounts, clients, features</i>"]
status["Status<br/><i>public</i>"]
profile["Profile"]
end
help["Contextual Help<br/><i>Help button / F1, EN + ES</i>"]
subgraph Data["Data access"]
queries["src/queries/<br/><i>TanStack Query hooks</i>"]
api["src/api/<backend>/<br/><i>typed functions</i>"]
core["src/api/core/<br/><i>graphqlClient, restClient,<br/>tokenStore, endpoints, errors</i>"]
end
end
router --> Pages
Pages --> queries --> api --> core
Pages --- help
auth_mod -->|Bearer token| core
core -->|GraphQL / REST over HTTPS| backend["Backend Services"]
Components never touch the network. Every call goes through three layers, in this order:
| Layer | File | Responsibility |
|---|---|---|
| 1 | src/api/<backend>/<domain>Operations.ts |
GraphQL documents, written with the generated graphql() tag |
| 2 | src/api/<backend>/<domain>.ts |
Plain typed async functions that throw ApiError
|
| 3 | src/queries/<domain>.ts |
TanStack Query hooks owning cache keys and invalidation-after-mutation |
The shared core lives in src/api/core/:
| File | Role |
|---|---|
endpoints.ts |
All endpoint environment reads — nowhere else |
tokenStore.ts |
Framework-agnostic token holder with a single refresh mutex; AuthContext is the writer |
graphqlClient.ts |
executeGraphQL(backend, document, variables) |
restClient.ts |
Authenticated multipart uploads and downloads, same mutex |
errors.ts |
ApiError, and notifyApiError → an 'app-error' CustomEvent → the toast |
Rules that matter:
-
Values travel as GraphQL variables ONLY. The old
formatValueinterpolation and escaping is gone; never build a document by string concatenation. - Partial success is tolerated — errors alongside usable data do not discard the data.
- Global
QueryCache/MutationCacheonErrorroutes failures to the toast; mutations are never retried. - A query may set
meta: { silent: true }to opt out of the global error toast. Used by surfaces that display failure as their content — so the status page does not fire a toast every 60 s while calmly reporting an outage.
Both live in src/api/, are reached only through src/queries/platformStatus.ts, and are documented in their own file headers. The public status page must work with no token at all:
-
api/core/healthProbe.ts— anonymousfetchof each service's/health -
getVisibleAnnouncements— anonymousaxiosGET that returns[]instead of throwing, so a Manager outage removes the banner rather than the page
The contract test suite exports each producer's SDL to TrackHub/schemas/<service>.graphql. Then:
npm run codegenruns graphql-codegen (client preset, per backend; scalars UUID/DateTime → string, Long → number) and validates every portal operation against those SDLs. Backend drift becomes a compile error rather than a runtime surprise.
The workflow after any backend GraphQL change is therefore:
- run the contract tests (which re-export the SDLs);
- run
npm run codegen; - run
npm run typecheck && npm test && npm run build.
Reporting is REST-only (api/BasicReports), and the Manager documents REST base is ~/documents — no api/ prefix.
src/ is 100% TypeScript. allowJs is off and an ESLint guard errors on any new .js or .jsx file under src/.
- The vendored Argon layer (
components,controls,theme) exports real prop types. Import and use them directly — never re-introduce local prop-slice interfaces oras unknown asboundary casts at call sites. If a control genuinely lacks a prop you must pass, widen the control's exported prop type. - Argon theme extensions are a single MUI module augmentation in
src/types/mui-theme.d.ts(boxShadows, borders, functions, palette groups,typography.size,text.main), consumed directly by the styled*Rootfiles — not by structural helper types. - No
as unknown asboundary casts remain in hand-written code. The survivors are codegen output, dynamic palette and i18n-key lookups, and documented data-shape casts. - Evaluated and deferred:
noUncheckedIndexedAccess(139 errors, mostly in vendored interiors) andexactOptionalPropertyTypes(73).
vite.config.ts defines process.env.REACT_APP_*. This is a decision, not debt: all reads centralize in api/core/endpoints.ts, and keeping the CRA environment-variable convention means existing .env files and deployment documentation stay valid. It is disabled in test mode, because suites assign env at runtime.
English and Spanish, through i18next. Keys are compile-checked (src/types/i18next.d.ts, strictKeyChecks against en.json), so a key must be added to both locales/en.json and locales/es.json. Dynamic keys cast at the key expression only.
User documentation lives in this repository, not in the wiki:
TrackHub/public/help/{en,es}/<topic-id>.md
One GitHub-flavoured Markdown topic per screen or feature, with YAML frontmatter carrying screens: (route-key mapping), category, related and an optional featureKey. It ships as static assets with each portal build, so what a user reads always matches the screens in front of them.
scripts/build-help.mjs runs on predev and prebuild (and standalone as npm run help:check). It validates the authoring contract:
- language parity between
enandes - topic id equals filename
-
screens:↔routes.tsxin both directions -
topic:link targets resolve - no raw HTML
- referenced assets exist
It emits the gitignored public/help/manifest.json, whose per-topic content hashes act as fetch cache-busters — the SPA never parses frontmatter itself.
HelpProvider (src/context/help/, mounted in App, F1 and Shift+? shortcuts) plus the navbar Help button and HelpDialog (src/controls/Help/, react-markdown + remark-gfm, skipHtml, with topic: links navigating in-modal behind a back stack).
TanStack queries fetch topics lazily with staleTime: Infinity, detect the nginx SPA index.html fallback by content (so fetched HTML is never rendered), and fall back to English, then to the index view.
Index and search filtering by role and feature reuses routeAllowed and the account's features. That is UX only — help content is a public static asset by decision.
The nginx.frontend.conf /help/ location (max-age 300, try_files $uri =404) is optional hardening; the client-side fallback detection must stay regardless.
/status is a public route in the SPA, not a separate site. It renders without authentication and outside the signed-in shell, because the scenario it exists for is "nobody can sign in — is it us or the platform?"
Three things make that work:
- the SPA is static files served by nginx with no auth;
- every service maps an anonymous
UseHealthChecks("/health"); -
UseCorsruns before it, so the portal origin can probe each service cross-origin with no token.
That CORS-before-health ordering is load-bearing. AuthorityServer originally had it inverted, which made the Sign-in tile permanently unreadable.
If nginx or the frontend itself is down, nothing can self-report. That is accepted.
RouteDefinition.public marks a route reachable by every principal type. Omitting principalTypes defaults to [User], so "public" had to be explicit — without the flag, a Driver or public-link principal was redirected to /dashboard, which is itself User-only, and the two bounced forever. App.tsx's routeAllowed and the Sidenav filter both honour it.
| Tier | Sees | Gate |
|---|---|---|
| Anonymous | Service tiles + active announcements | — |
| Manager or Administrator | Additionally the GPS-synchronisation (SyncWorker) tile | OperatorSyncRuns/Read |
| Administrator | Additionally the background-jobs table and announcement CRUD |
Administrative/Read, Administrative/Write
|
The page resolves its own isAdmin / isManager state (both silent) rather than taking props, because it is rendered for signed-out visitors too.
Service state is probed from the browser, not from a server rollup: GET {base}/health per backend, with a 6 s AbortController budget, using URLs built only from HEALTH_ENDPOINTS in api/core/endpoints.ts (lazy getters, so tests can stub the env). Probes never reject — a total outage still renders the page, degraded per tile.
- ErrorBoundary and NotificationProvider at the app root
- 30 s request timeout (60 s for file transfers)
- Console output suppressed in production
- Security meta headers in
index.html -
Map popups escape their input. Leaflet
bindPopup/bindTooltipand the Google InfoWindow assign their argument viainnerHTML, so every dynamic value passes throughescapeHtml(src/utils/htmlUtils.ts). Transporter names are account-editable free text and addresses come from a third-party geocoder. -
datetime-local⇄ UTC goes throughtoDateTimeLocalInput/fromDateTimeLocalInput(src/utils/dateUtils.ts). The control holds local wall time, so the instant shifts by the viewer's offset in both directions. A helper that skips the shift round-trips correctly only underTZ=UTC— which is what dev boxes and CI run. Assert the round-trip property, never a literal.
All reads go through src/api/core/endpoints.ts. The CRA REACT_APP_ convention is retained deliberately.
| Variable | Purpose |
|---|---|
REACT_APP_CLIENT_ID |
OAuth client id (web_client) |
REACT_APP_AUTHORIZATION_ENDPOINT |
/Identity/authorize |
REACT_APP_TOKEN_ENDPOINT |
/Identity/token |
REACT_APP_CALLBACK_ENDPOINT |
OAuth redirect target |
REACT_APP_REVOKE_TOKEN_ENDPOINT |
/Identity/revoke |
REACT_APP_LOGOUT_ENDPOINT |
/Identity/logout |
REACT_APP_MANAGER_ENDPOINT |
Manager GraphQL |
REACT_APP_SECURITY_ENDPOINT |
Security GraphQL |
REACT_APP_ROUTER_ENDPOINT |
Router GraphQL |
REACT_APP_GEOFENCING_ENDPOINT |
Geofencing GraphQL |
REACT_APP_TELEMETRY_ENDPOINT |
Telemetry GraphQL |
REACT_APP_TRIPMANAGEMENT_ENDPOINT |
TripManagement GraphQL |
REACT_APP_REPORTING_ENDPOINT |
Reporting REST base |
REACT_APP_DEFAULT_LAT / REACT_APP_DEFAULT_LNG
|
Map centre when the browser denies location permission |
Platform
- Architecture
- Technology
- Inter-Service Communication
- Database
- Security and Identity
- User Permissions Overview
Services
- Manager
- Telemetry
- Router
- Adding a Provider
- Geofencing
- Trip Management
- Reporting
- Common Library
- Frontend
Engineering
User docs
- User Guide (ships in the app)