# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Commands ```bash npm run start:staging # dev server (loads .env.staging via env-cmd) npm run build:staging # production build — USE THIS, see "Environment" below npm test # CRA/Jest, watch mode CI=true npx react-scripts test --watchAll=false # single run CI=true npx react-scripts test --watchAll=false -t "step ordering" # one test by name CI=true npx react-scripts test --watchAll=false --testPathPattern=api # one file ``` Lint is not wired to a script. ESLint is deliberately disabled in the webpack pipeline (`config-overrides.js` filters out `ESLintWebpackPlugin`; `.env` sets `DISABLE_ESLINT_PLUGIN`), so **lint errors never fail a build** — run ESLint explicitly if you need it. ## Environment — read this before building `REACT_APP_URL` (v1) and `REACT_APP_URL2` (v2) are the Jupiter API bases. They live in `.env`, `.env.development`, and `.env.staging`. **All three files are gitignored**, so a fresh clone or a CI/build host has none of them. `npm run build` does *not* load the env-cmd files — only `build:dev` / `build:staging` do. CRA auto-loads `.env` alone. A build missing `REACT_APP_URL` does not fail; it inlines `undefined`, every request becomes the *relative* path `"undefined/orders/..."`, and those silently resolve against whatever origin the console is served from. This shipped to production once and looked like "the API base URL changed to the front-end's own host". `config-overrides.js` now hard-fails a production build when `REACT_APP_URL`/`REACT_APP_URL2` are missing. Add any new required var to `REQUIRED_PROD_ENV` there. **`REACT_APP_URL3` is referenced but defined nowhere** (`Dispatch.js`, the `getdeliverylogs` fetch). Actual-GPS trails and all Compare deltas silently render nothing as a result. Whether that endpoint is v1 or v2 is unconfirmed. ## Architecture CRA + `react-app-rewired` on the **Mantis** MUI template. The template's own pages/theme are mostly inert scaffolding — all real work lives in `src/pages/nearle/`. Data fetching is TanStack Query v5; maps are Leaflet + OSRM. Redux is vestigial — `store/reducers/index.js` combines only `menu` and `snackbar` (the `auth.js` reducer file is not registered), so no domain data flows through it. Server state belongs in TanStack Query. ### Auth is localStorage, and it is not enforced `login.js` writes `tenantid`, `applocationid`, `locationid`, `userid`, `authname` etc. to localStorage; everything else reads them from there. `AuthGuard`/`GuestGuard` exist but are **commented out in both route files**, so every route is publicly reachable. `App.js` does a soft redirect to `/login` when `authname` is absent and clears localStorage after a 1-hour session window. ### The API layer has a module-load trap `pages/nearle/api/api.js` reads `const tenid = localStorage.getItem('tenantid')` **at module scope**. It is captured once on first import, so a login that happens afterwards leaves every `tenid`-using query pinned to the stale value until a full reload. Prefer reading localStorage inside the query function for anything new. Several endpoints bypass the env vars and hardcode absolute hosts — the solver calls (`routes.workolik.com`, `routemate.workolik.com`), `finalCreatedeliveries`, and `fetchRidersLogs`. `finalCreatedeliveries` hardcodes the **live** Jupiter host, so a staging build still commits deliveries to production. ### Dispatch.js has two personalities `pages/nearle/dispatch/Dispatch.js` (~6k lines, plus a ~10k-line CSS file) is one component serving two modes, switched by whether a `data` prop is passed: | | standalone `/nearle/dispatch` | embedded (`data` passed) | |---|---|---| | source | live API polling | a solver response | | `shouldFetchLive` | `true` | `false` | | default `viewMode` | `riders` | `zones` | | header / batch / date bar | shown | hidden | Both paths converge on the same shape: `{ zones: [{ zone_name, riders: [{ rider_id, rider_name, orders[] }] }], zone_summary[], details[] }`. The live path builds it in the `liveData` memo by re-bucketing flat delivery rows by `deliverysuburb`; the embedded path receives it already shaped. **Anything that produces data for Dispatch must match that shape** — that is what `normaliseAssignResponse` in `api.js` exists to guarantee. View modes: By Location / By Zone / By Rider / Active / Rider Info. "Active" is the live-ops view — only riders whose latest GPS log is `active`/`pending`, collapsed to their single in-progress delivery, polling deliveries every 15s. Orders bucket into three fixed **batches** (Morning <08:00, Afternoon 09:00–12:30, Evening 16:00–19:00) on `assigntime`. Gaps between them are intentional. Layout persists under `dispatch.slots.v8`; bump the key and add the old one to `LEGACY_SLOTS_STORAGE_KEYS` when the shape changes. The operator-editable slot editor and the status-wise time-field dropdown are both commented out in the JSX, not deleted. Shared constants and pure helpers live in `dispatchShared.js` specifically to avoid a circular import between `Dispatch.js` and its children (`CompareDataPanel`, `ActiveSection`). ### Assign flow: two clicks, two endpoints ``` /nearle/orders "Assign Orders (N)" → POST https://routes.workolik.com/api/v1/optimization/nagercoil/riderassign (fixed-rider Nagercoil solver; body {deliveries:[...]}, or {} to let the server pull created orders itself. Empty result → info snackbar, no navigation.) → navigate('/nearle/dispatch/preview', { state: { dispatchPreviewData, ... } }) /nearle/dispatch/preview "Assign Orders" ← this is the commit → POST https://jupiter.nearle.app/live/api/v1/deliveries/createdeliveries → navigate('/nearle/deliveries') ``` The first click writes nothing — it only produces a preview. `Preview.js` deliberately wipes `history.state.usr` after consuming the response so a reload cannot resurrect a stale plan (it bounces to `/nearle/orders` instead). Two de-duplication rules matter here and were both written to fix real duplicate-delivery bugs: `extractRiders` de-dupes by `orderid` across zones (one rider legitimately appears once per delivery suburb), and `applyReconcileResponse` wipes a rider's orders from *every* zone before re-placing them. `normaliseAssignResponse` assigns `step` **per rider across the whole response**, before zone bucketing. Numbering within each zone instead would restart the sequence at every suburb and misrepresent the solver's ordering on map pins and route lines. ## Dead code that looks live Several features are fully implemented but unreachable. Do not assume a feature works because the code exists: - **Compare mode** (`CompareDataPanel`, compare map, delta engine, Kalman/OSRM track snapping) — `compareOpen` only becomes `true` via `pendingCompareRef.current`, which nothing ever sets. There is no Compare button in the JSX. - **Analysis view** (`#dispatch-analysis`, batch efficiency) — `setTopView` is never called, so `topView` is permanently `'live'`. Its tenant is also hardcoded (`ANALYSIS_TENANT_ID = 916`). - **Preview.js reconcile / re-optimise / CSV export / tuning** — `handleReconcile`, `handleCreateDelivery`, `tabValue`, `csvExportData`, `tuningTypes` and their imports each appear exactly once (their own declaration). The render is now just a header plus an embedded ``. `reconcileSteps` and `createOptimisationDeliveries` remain wired in `api.js` but nothing invokes them. - **`/nearle/orders/preview`** (`OrdersPreview.js`, ~770 lines) — routed, but nothing navigates to it. Superseded by the dispatch Preview. - **Rider push notification** — `Preview.js` only calls `notifyRider` when `stateData.rider?.userfcmtoken` exists, and `orders.js` never passes a `rider` key. The assigned rider is never notified. ## Tests Two suites, 67 tests: - `src/pages/nearle/api/api.test.js` — `normaliseAssignResponse` across every response shape, `createNagercoilDeliveries` request wiring, and the `!data?.details?.length` contract that `orders.js` branches on. `axios` is mocked with a module factory (CRA automock breaks on `response.data`). - `src/utils/route-guard/authState.test.js` — session validity, which gates every protected route. Covers the 1-hour boundary, a missing start time (accepts, rather than logging the user out) and storage throwing (fails closed). Component tests do not exist. The large page components have heavy provider requirements (react-query, notistack, router, theme), so prefer testing extracted pure logic in `api.js` / `dispatchShared.js` over mounting them. ## Conventions Prettier: single quotes, no trailing comma, 140 print width, 2-space indent. Files use CRLF — multi-line string matching in scripts needs `\r?\n`.