Since you already have PosiTrace tracking the trucks, the cleanest move is to drop the driver-phone-GPS pings entirely and pull live truck positions from PosiTrace's API. Dispatch dashboard reads from the same R2 path it always did — the data source just changed underneath. Code is written. You need three things from PosiTrace before it goes live.
The dispatch dashboard reads location data from driver-locations/{date}/{branch}.json in R2. That path now gets populated by PosiTrace instead of phone GPS — same shape, same key. Dashboard doesn't know the difference.
Worker uses a 30-second in-memory cache. Dashboard hits us every 15s; we hit PosiTrace at most every 30s. Total upstream load: ~120 PosiTrace API calls per hour during business hours, well within their rate limits.
FALLBACK · IF POSITRACE_API_URL/KEY/VEHICLES NOT SET → REVERTS TO PHONE GPS PINGS · NO CODE CHANGE NEEDED
PosiTrace doesn't publish API docs publicly — you need to email their support team. Use the template below; they typically respond within a business day.
Or call them directly if you have a rep — most fleet vendors prefer phone for credential setup. Mention you have Pro tier already.
npx wrangler secret put POSITRACE_API_URL
npx wrangler secret put POSITRACE_API_KEY
npx wrangler secret put POSITRACE_VEHICLES
For POSITRACE_VEHICLES, format as VEHICLE_ID:branch comma-separated:
VEHICLE_4567:belleville,VEHICLE_8901:kingston
The Worker code currently uses GET /fleet/vehicles?include=position,driver as a placeholder. Update positrace.ts → fetchFleetLocations with the real path from their docs. Likely 1-line change.
Our code expects { vehicles: [{ id, position: { latitude, longitude, captured_at } }] }. If theirs differs, adjust the type definition in positrace.ts. Probably 5-10 lines.
npm run deploy the worker. Open the dispatch dashboard. Truck pins should now reflect PosiTrace data, no driver login required.
Once PosiTrace is producing reliable data, remove the 60s GPS ping loop from driver-app/src/App.tsx. Saves tablet battery. The phone-ping fallback can stay if you want belt-and-suspenders.
If PosiTrace credentials aren't configured, everything falls back to phone GPS pings automatically. Set the three secrets when you're ready to switch.
// High-level: refresh all known vehicles' locations in R2.
// Called by dispatch.ts when cached locations are stale.
export async function refreshLocationsFromPosiTrace(
config: PosiTraceConfig,
catalogBucket: R2Bucket,
): Promise<{ updated: number; errors: string[] }> {
const today = new Date().toISOString().slice(0, 10);
let vehicles: PosiTraceVehicle[];
try {
vehicles = await fetchFleetLocations(config);
} catch (e: any) {
return { updated: 0, errors: [`PosiTrace fetch failed: ${e.message}`] };
}
const byPositraceId = new Map(vehicles.map(v => [v.id, v]));
let updated = 0;
const errors: string[] = [];
for (const mapping of config.vehicles) {
const vehicle = byPositraceId.get(mapping.positrace_id);
if (!vehicle) {
errors.push(`Vehicle ${mapping.positrace_id} not in fleet response`);
continue;
}
const location = toDriverLocation(vehicle, mapping);
const key = `driver-locations/${today}/${mapping.internal_id}.json`;
await catalogBucket.put(key, JSON.stringify(location));
updated++;
}
return { updated, errors };
}
// PosiTrace cache: only re-fetch if last refresh was >30s ago
const LOCATION_REFRESH_TTL_MS = 30_000;
let lastRefreshAt = 0;
async function maybeRefreshPosiTrace(env: DispatchEnv): Promise<void> {
if (!env.POSITRACE_API_URL || !env.POSITRACE_API_KEY || !env.POSITRACE_VEHICLES) {
return; // Not configured · use phone GPS fallback
}
const now = Date.now();
if (now - lastRefreshAt < LOCATION_REFRESH_TTL_MS) {
return; // Cache still warm
}
try {
const vehicles = parseVehicleMapping(env.POSITRACE_VEHICLES);
await refreshLocationsFromPosiTrace(
{ apiUrl: env.POSITRACE_API_URL, apiKey: env.POSITRACE_API_KEY, vehicles },
env.CATALOG_BUCKET,
);
lastRefreshAt = now;
} catch (e) {
console.error('[dispatch] PosiTrace refresh failed:', e);
// Don't update lastRefreshAt — retry on the next request
}
}
// Then in handleDispatchStatus():
// await maybeRefreshPosiTrace(env); ← runs before reading R2
// const [bv, kgn] = await Promise.all([buildBranchStatus(...), ...]);
PosiTrace's Forge integration platform pushes data OUT to other systems. If they recommend Forge over direct API access, the model flips: instead of us polling them, they POST location updates to a Worker webhook on every change. We just receive + write to R2.
// Alternative architecture if PosiTrace's Forge platform is the recommended path.
// PosiTrace pushes location updates to us via webhook on every position change.
// No polling needed; effectively zero latency.
export async function handlePosiTraceWebhook(request: Request, env: Env): Promise<Response> {
// Verify webhook signature (Forge typically uses HMAC-SHA256 in a header)
const signature = request.headers.get('x-positrace-signature');
const body = await request.text();
if (!verifySignature(body, signature, env.POSITRACE_WEBHOOK_SECRET)) {
return new Response('invalid signature', { status: 401 });
}
const event = JSON.parse(body) as PosiTraceWebhookEvent;
if (event.type !== 'position.updated') return new Response('ok', { status: 200 });
const today = new Date().toISOString().slice(0, 10);
const mapping = findVehicleMapping(event.vehicle_id, env);
if (!mapping) return new Response('unknown vehicle', { status: 200 });
const location = {
driver_id: event.driver_id ?? mapping.branch,
branch: mapping.branch,
lat: event.position.lat,
lng: event.position.lng,
accuracy: event.position.accuracy ?? null,
timestamp: event.timestamp,
received_at: new Date().toISOString(),
};
await env.CATALOG_BUCKET.put(
`driver-locations/${today}/${mapping.internal_id}.json`,
JSON.stringify(location),
);
return new Response('ok', { status: 200 });
}
Either path works — direct API polling (current implementation) is simpler to set up and the latency difference is invisible at our 15-second dashboard refresh cadence. If PosiTrace's reply mentions Forge as the path forward, swap to the webhook handler. The R2 → dashboard half stays identical either way.