Routes & stops
Schedule as minute-of-day
Overview
A route has a name, a color, an active flag, and an ordered array of stops. Each stop has a name, lat/lng, scheduled minute-of-day, plus brand-rich fields — tagline (one prose sentence), houseStyle (a wine-style line), hoursText (free-form opening hours), and heroImageUrl with attribution. The scheduledMinuteOfDay field is the secret sauce.
How it works
Route insertion: `POST /admin/routes` accepts the route fields plus an array of stops. The API generates IDs, computes the order from array position, and writes both in a single transaction.
ETAs derived from minute-of-day: the rider's Today page combines scheduledMinuteOfDay with today's date in the route's timezone to build a real timestamp, which feeds the ETA banner.
Live vehicle progress: as the vehicle pings positions, we compute the nearest un-passed stop by haversine distance plus the route's polyline projection. The 'next stop' becomes the basis for the ETA.
Stop edits propagate immediately — there is no caching layer between the API and the catalog. Rider apps pick up changes on next screen focus via React Query's staleTime of 60000.
Inactive routes are filtered out of the assignments picker and the rider catalog, but their historical reservations stay intact (we soft-archive, not hard-delete).
Key decisions
Minute-of-day, not a timestamp
A route that runs every day at 9am isn't tied to any specific date. Storing a full timestamp would force us to materialize one row per day, or do tricky modulo math. An integer 0–1439 makes the recurrence trivial — combine with today's date when you need a real timestamp.
Stops own prose, not just coordinates
A wine-country shuttle stop is a destination, not a coordinate. The tagline, houseStyle, hoursText, and heroImageUrl fields let each stop tell a story on the rider's StopDetailSheet. The data is heavier than a stop on a city bus would be — and that's the whole point.