Opening dispatch showed "Pick one to see its stops" over an empty panel.
That wastes the first look: the whole day IS the answer to "where is my
work", and an operator usually only wants to narrow after seeing it.
Both views now open on the map, drawn from every stop in range. Picking a
rider or a shop filters it; it no longer summons it. The customer view keeps
its placeholder — one customer's drops are all at one address, so there is
no shape to open onto.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYEsb8PNZ19G9R8gUjTU7n
The deploy died on `npm ci` with "Missing: @emnapi/core@1.11.3 from lock
file". Nothing was wrong with the code — the lock really was incomplete,
and the Dockerfile was right to refuse it.
I broke it. Adding leaflet ran `npm install` under npm 11.6.2; the builder
is node:22-alpine, which ships npm 10.9.8. npm 11 prunes optional platform
packages that npm 10 still validates, and it dropped `@emnapi/core` and
`@emnapi/runtime` — transitive optional deps of
`@tailwindcss/oxide-wasm32-wasi`. Both were present in the lock at b760c1a
and absent from 34bf798 onward.
Regenerated with `npx npm@10.9.8 install --package-lock-only`, which
restores both entries and keeps leaflet 1.9.4 / @types/leaflet 1.9.22.
Verified by running the builder's exact command in a scratch directory:
the old lock reproduces the failure, the new one gives "added 212 packages"
under npm 10.9.8 AND under npm 11.6.2 — so it holds whichever npm the image
ships. No Dockerfile change was needed for that.
The Dockerfile comment did need one. It said the fix was "`npm install`
locally and COMMIT the updated package-lock.json", which is exactly the
step that caused this. It now says to prove the lock against npm 10.9.8 in
a scratch directory before pushing, and how to regenerate it if it fails.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYEsb8PNZ19G9R8gUjTU7n
The separate Map tab is gone. "By rider" and "By store" now draw the map
where the stops table was, so picking a rider from the rail shows their
round on a map and picking a branch shows its day.
A round and a shop's day are both shapes — where the work is, in what order,
how far apart — and a table of addresses shows neither. You can read twenty
rows and still not see that a rider crossed the city twice.
Two views keep the table, on purpose: the waiting queue, whose rows are
ticked to assign them (a checkbox cannot live on a map pin, and nothing
there has been worked yet), and "By customer", where one customer's drops
are all at one address.
The map draws all three places a delivery has, not just the rider:
shop a square, one per branch — pickuplat/pickuplon, 500/500 rows
drop a circle per stop, by status — droplat/droplon, 500/500 rows
rider a hollow ring — riderslat/riderslon, 461/500 and 324/500
Shop and drop are on every row, so the map is never empty on a day with
work in it. The rider fix is real: on delivered rows it sits a median 134 m
from the drop, a reading taken at the customer's door.
The endpoints spell the columns differently — `pickuplon` on getdeliveries
against `pickuplong` on getorders, one letter, and reading the wrong one
puts every shop off the coast of Ghana. `droplat` is filled on deliveries
and empty on orders. `coordOf` takes every spelling so a waiting order maps
the same as a delivered one.
Also fixes the rail itself: it named each group from its first stop's
`ridername`, and that column holds a delivery status on more rows than a
name for two riders in five. The board was listing a phantom rider called
"delivered" carrying 100 of the day's stops, next to the real riders.
Grouping was never wrong — that is on userid — only the label. `isRealName`
now excludes the status vocabulary and both the rail and the map take the
most common name that survives.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYEsb8PNZ19G9R8gUjTU7n
Three of the features the old console had and ours did not, built on what
the data can actually support rather than on what the column names imply.
Two findings changed the shape of the work:
`riderlogs` is not a GPS trail. Every ping a rider sends carries the SAME
coordinate — one rider's 2,404 pings on 14 August all read 11.052998,
76.929958, and the same holds on every day and region checked. Distance,
speed and "time moving" cannot come from it. The Fleet page therefore
reports presence only: who was online and for how long, inferred from the
gaps between check-ins, because `login`, `logout` and `workhours` are empty
on all 320,132 August rows. It says out loud that it cannot tell a rider
parked all day from one who crossed the city.
The delivery ladder is not the order its columns are in. `starttime` is
later than `arrivaltime` on 316 of 316 rows, which looks corrupt and is not:
`starttime` is per-DROP, stamped when the rider sets off for that address
having finished the last one. Read as assign -> arrive -> pickup -> start ->
deliver, every duration is positive. On tenant 916 that shows the bottleneck
is not the riding: a median 69.5 minutes passes between handing an order to
a rider and that rider reaching the shop, against 0.6 minutes at the counter.
- Map tab on dispatch, fed by `deliveries.riderslat/lon` — the only rider
positions that move (353 distinct across 461 rows). Tenant-scoped, so a
shop sees its own rounds. The line joins stops in worked order and says
it is not a route.
- Plan vs actual tab: promised against delivered, and a step breakdown of
where the hours go. `actualkms` is excluded — it equals the planned `kms`
to the decimal on every delivered row, so it is a copy, not a measurement.
- Fleet page in the platform console: a presence gantt and a map of where
each rider is registered.
- `ridername` holds a delivery status on more rows than it holds a name for
two riders in five, so names are resolved by excluding the status
vocabulary first.
- leaflet, wrapped directly rather than via react-leaflet, lazy-loaded so
only the pages with a map pay for it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYEsb8PNZ19G9R8gUjTU7n