113 lines
4.6 KiB
TypeScript
113 lines
4.6 KiB
TypeScript
/**
|
|
* Where a rider is right now, and how their phone is doing.
|
|
*
|
|
* ── This is a different backend, and that is the whole point ────────────────
|
|
*
|
|
* `jupiter.nearle.app` — the platform's older API, which the xpress console
|
|
* runs against. Fiesta has no equivalent: `getriderperiodiclogs` does not exist
|
|
* there under any prefix, and `partners/getriderlogs`, the closest-looking
|
|
* Fiesta endpoint, is a heartbeat log that repeats one fixed coordinate per
|
|
* rider all day (see `riderShifts`). So this is the ONLY live rider position
|
|
* on the platform, and it was missed once already by checking Fiesta alone.
|
|
*
|
|
* Verified 2026-09-10: rider 852 polled thirty seconds apart moved about 140 m,
|
|
* with battery, connection and accuracy all changing. Genuinely live.
|
|
*
|
|
* Same database as Fiesta underneath — `getallriders` on jupiter and
|
|
* `getriders` on Fiesta return identical rosters and identical on-duty state —
|
|
* so a userid from one is a userid in the other.
|
|
*
|
|
* ── One rider per call ──────────────────────────────────────────────────────
|
|
*
|
|
* There is no fleet form: `?userid=N` answers for that rider, and omitting it
|
|
* answers for whichever rider reported most recently, which is not useful. A
|
|
* fleet view therefore fans out, which is fine at the sizes involved — a
|
|
* merchant's round is a handful of riders.
|
|
*
|
|
* ── Freshness is the caller's problem, and must be shown ────────────────────
|
|
*
|
|
* The endpoint always answers, and it answers with the LAST known fix however
|
|
* old. Riders 883, 897 and 1111 come back with positions from 5, 3 and 12 days
|
|
* ago and nothing in the payload flags them as stale. A map that draws those
|
|
* next to a live one is lying, so `logdate` is parsed here and every consumer
|
|
* is handed an age rather than a bare position.
|
|
*/
|
|
|
|
const JUPITER_BASE = (
|
|
import.meta.env?.['VITE_JUPITER_BASE'] ?? 'https://jupiter.nearle.app'
|
|
)
|
|
.trim()
|
|
.replace(/\/+$/, '');
|
|
|
|
/** One rider's live snapshot, exactly as jupiter sends it. */
|
|
export interface RiderSnapshot {
|
|
userid: number;
|
|
username?: string;
|
|
latitude?: string;
|
|
longitude?: string;
|
|
/** `2026-09-10 11:25:11` — server local time, no zone. */
|
|
logdate?: string;
|
|
/** `"95%"`, with the sign. */
|
|
battery?: string;
|
|
/** `mobile`, `wifi`, `none`. */
|
|
connection?: string;
|
|
/** Metres of GPS uncertainty, as a string. 100.0 is a poor fix. */
|
|
accuracy?: string;
|
|
/** Km/h and degrees, both as strings. */
|
|
speed?: string;
|
|
heading?: string;
|
|
/** `idle`, `active`, and whatever else the app decides to send. */
|
|
status?: string;
|
|
/** The order they are on, when they are on one. */
|
|
orderid?: string;
|
|
is_charging?: boolean;
|
|
is_background?: boolean;
|
|
/** `enabled` / `disabled` — a disabled one explains a stale position. */
|
|
location_service?: string;
|
|
}
|
|
|
|
export class TelemetryError extends Error {
|
|
constructor(message: string) {
|
|
super(message);
|
|
this.name = 'TelemetryError';
|
|
}
|
|
}
|
|
|
|
export const telemetryApi = {
|
|
/**
|
|
* One rider's latest reported position and phone state.
|
|
*
|
|
* Never throws for "this rider has never reported" — that answers 200 with an
|
|
* empty-ish body, and the caller wants to draw the rider as unreachable
|
|
* rather than show an error. It throws only when jupiter itself cannot be
|
|
* reached, which is a different thing and worth saying out loud: jupiter can
|
|
* be down while Fiesta is fine, and vice versa.
|
|
*/
|
|
rider: async (userid: number, signal?: AbortSignal): Promise<RiderSnapshot | null> => {
|
|
let response: Response;
|
|
try {
|
|
response = await fetch(
|
|
`${JUPITER_BASE}/live/api/v1/utils/getriderperiodiclogs?userid=${userid}`,
|
|
{ headers: { Accept: 'application/json' }, signal },
|
|
);
|
|
} catch (error) {
|
|
if ((error as Error)?.name === 'AbortError') throw error;
|
|
throw new TelemetryError(
|
|
'Could not reach the live rider service. It is a separate backend from the rest of the console, so everything else keeps working.',
|
|
);
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new TelemetryError(`The live rider service answered ${response.status}.`);
|
|
}
|
|
|
|
const payload = (await response.json().catch(() => null)) as
|
|
| { data?: RiderSnapshot }
|
|
| null;
|
|
const data = payload?.data;
|
|
// A rider who has never opened the app comes back without coordinates.
|
|
// Null rather than an empty object, so "no fix" is one check everywhere.
|
|
return data && (data.latitude || data.longitude) ? data : null;
|
|
},
|
|
};
|