166 lines
8.7 KiB
Markdown
166 lines
8.7 KiB
Markdown
# 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 `<Dispatch>`. `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`.
|