Design System
What is Eos.Web?
Eos.Web (frontend/src/eos-web) is the Operations frontend — a React +
TypeScript single-page application built with Vite. It's a login page followed by
a logged-in shell:
- A collapsible sidebar with a favorites section and a searchable full menu
tree below it, loaded from
GET /api/operations/menuand/menu/favorites. When collapsed, it switches to an icon-rail + hover-flyout mode. - A top bar with the company logo, the current branch (with a country flag), and a user pill with a Settings/Logout dropdown.
- A dashboard body that's intentionally empty until module pages exist.
Routing, API calls, and auth
- Routing is
react-router-dom:/loginis public,/is gated behindRequireAuth. - API calls go through an
axiosinstance (src/api/client.ts) whosebaseURLcomes from the Vite env varVITE_API_BASE_URL— this is never hardcoded. - JWT storage: the access token lives only in an httpOnly/
Securecookie set by the backend'sAuthCookieWriter— never inlocalStorageor anywhere else JS can read it.client.tssetswithCredentials: trueso the browser sends/receives it automatically; there is no client-side JWT decode.AuthContexthydrates itsCurrentUserstate fromGET api/auth/meinstead (on mount and after login) — the server independently validates the token and derives identity from its own claims. A single in-flight refresh promise (refreshAccessToken()) is shared across concurrent 401s so N simultaneous failing requests trigger exactly onePOST api/auth/refresh, not N; on refresh failure the app forces a redirect to/login.
Passwords are never logged. This section previously described localStorage +
client-side jwt-decode — that was replaced by the httpOnly-cookie model above;
if you find lingering references to tokenStorage.ts/jwt-decode elsewhere,
they're stale.
Api.External is a separate backend — do not call it with apiClient
apiClient targets Logiswift.Eos.Api (the internal Operations API) and
authenticates via the httpOnly cookie above — no Authorization header is set.
Logiswift.Eos.Api.External is a separate ASP.NET Core project with its own base
URL and an OAuth2 bearer-token auth model; Eos.Web does not currently call it.
If a future feature needs to, build a separate Axios instance with OAuth2 token
handling — copying src/api/*.ts patterns as-is would silently produce
unauthenticated requests against the wrong auth model.
Design system — vendored, not Ant Design, for the shell/login
The shell (AppShell/TopBar/SideNav/AppBreadcrumb) and the login page are
styled with a vendored CSS design system (src/styles/tokens.css +
app.css), not Ant Design — ported from a sister project (LSCoreNext) that
built the same system against the same legacy visual language.
antd is not a dependency of this project — don't add it without a
deliberate decision. Every page, including the ~12 Reference CRUD pages' tables/
forms (Cities, Countries, Branches, Companies, Clients, Warehouses, Departments,
Equipment, ForumTypes, Queues, TrCodes, Addresses), is styled with the same
vendored system and custom components (DataGrid, Field, RightPanel,
ConfirmDialog).
:::note Previously used antd — since ported off it
This page previously described antd as a real dependency still used by the 12
CRUD pages. That's no longer true — those pages have since been ported onto the
same vendored DataGrid/Field components as the shell. If you find antd
imports remaining anywhere, that's drift to flag, not the current standard.
:::
:::danger Rule: style with the real classes already in app.css/tokens.css — never invent a parallel one The source project learned this the hard way. Components built by guessing class names before actually reading the CSS produced zero real styling and had to be rewritten wholesale — one entire CSS file was deleted as fully superseded once the real, already-working design system was actually read. Before naming a new component-scoped class, check whether one already exists. :::
Re-vendoring
If the design system changes upstream, re-copy tokens.css/app.css
deliberately. This is a vendored copy, not a fork with its own drifting values.
Grids load only the requested page from the server
table.grid has no built-in limit — unlike antd's Table, which defaults to
10 rows/page. This is the standing default for every grid going forward: build
new grids this way unless explicitly told otherwise for that specific case.
:::caution Real incident: CitiesPage hard-crashed the browser
CitiesPage initially shipped fetching the full MAS_CITY table and slicing
it client-side. Because antd's Table had always paginated at 10 rows/page by
default, nothing had ever actually rendered a whole large table into the DOM
before — removing antd removed that protection without anyone deciding to.
Against real (world-cities-sized) data, this hard-crashed the browser tab.
The fix was later upgraded from "paginate client-side" to real server-side pagination: the API returns only the requested page, and every page/page-size/ sort change is a new network request, not a re-slice of an already-fetched array. :::
The full shape of this pattern — PagedResult<T>, usePagedList, and the
server-driven DataGrid with its page-size selector — is documented on the
Pagination page under Backend Patterns.
Shared components
src/components/ holds a set of UI-kit-agnostic shared components — reuse these
rather than building a one-off styled element:
ButtonPageHeaderModalConfirmDialogEmptyStateLoadingSkeletonErrorBoundaryStatusBadgeAppBreadcrumbMenuIconToast/ToastProvider
MenuIcon always falls back to a generic icon today — Eos's menu API has no
IconName column yet (a future backend enhancement, not built), so don't wire up
per-item icons until that data exists.
Data fetching: plain useEffect/useState by default, TanStack Query for order-tracking pages
The shell, login page, and every standard CRUD page (list + create/edit + delete)
use the existing axios + useEffect/useState pattern — this stays the
default. Don't add TanStack Query to a CRUD page without a specific reason.
MasterOrderDetailPage/MasterOrdersPage/TransportOrderDetailPage/
TransportOrdersPage are the one deliberate exception: they're on
@tanstack/react-query (QueryClientProvider wraps App.tsx,
src/lib/queryClient.ts holds the shared QueryClient), adopted once the pain of
hand-rolled polling + no cross-tab staleness handling became real. No SignalR/
live-push exists yet, so these pages poll instead:
refetchInterval: OPERATIONAL_POLL_INTERVAL_MS(pollingIntervals.ts, 45s) is the shared poll cadence on both detail pages and onusePagedListQuery(src/lib/usePagedListQuery.ts, the TanStack counterpart tousePagedListused only by these two grids) — don't pick a different number per page.refetchOnWindowFocusis also enabled globally, so switching back to a stale tab revalidates immediately instead of waiting for the next interval tick.- A background poll/focus-refetch failure stays silent —
isErrorflips true on any failed fetch, including a background refetch that still has good cacheddata, so every consumer gatesnotFound/errorondata === undefinedrather than onisErroralone. A real first-load failure (datastill undefined) still surfaces normally. useBoundedPollingstill exists and is still used internally byusePagedListfor every other CRUD grid's optionalpollIntervalMs— not dead code.
This is not an app-wide TanStack Query adoption — weigh the same trigger (real polling/staleness pain, not just "this page fetches data") before adding it elsewhere.
Route-level code splitting
Every page component in App.tsx (~30 pages) is wrapped in React.lazy(), with
Routes wrapped in a single Suspense using the existing LoadingSkeleton as
fallback — each page now emits its own build chunk instead of bundling into one
index.js. Layout/provider components (AppShell, AuthProvider, RequireAuth,
RouteErrorBoundary) stay eager since they're needed on every route. Add new
page components the same way — lazy(() => import('./pages/NewPage')) — rather
than a plain static import.
Shared DTOs come from eos-contracts, not src/types
API-boundary DTO types live in the eos-contracts package (repo root, its own
package.json/tsconfig) rather than src/types/ — consumed today via a plain
file: npm dependency, matching this convention for any new shared DTO rather
than adding a fresh type file under src/types/. eos-contracts is also
published to an Azure Artifacts npm feed for potential out-of-repo consumers, but
eos-web deliberately stays on the file: reference until a real out-of-repo
consumer exists — don't switch it to the published version without a reason to.
Branch-level date/number formatting
Order pages format dates/numbers using the current order's own branch locale
(MAS_BRANCH's DateFormat/CultureCode), not the browser's locale or a
hardcoded format — real branches genuinely diverge (see branch/format.ts,
branch/useBranchFormat.ts, branch/BranchContext.tsx, and the
BranchDateField component). Shipped so far on the MasterOrder pages (detail,
form, create wizard, attachments panel) and the TransportOrder/Scenario-log
pages that touch order dates — not yet extended to the rest of Eos.Web.
Follow the same useBranchFormat/BranchDateField pattern when a new page needs
to render an order-scoped date/number rather than inventing a parallel
formatter.