613
613parts an APC associate store
POSITRACE INTEGRATION · v0.8.0
PosiTrace · already in the trucks

your trackers feed
our dispatch dashboard.

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.

Source
PosiTrace API
Cache TTL
30s
Code added
~190 lines
Frontend changes
0
Fallback
Phone GPS
Cost
$0/mo new
What changed

data source swap. zero ui changes.

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.

Before · phone GPS

driver tablet pings every 60s

  • Driver phone navigator.geolocation API
  • POST /api/v1/driver/location with lat/lng
  • Worker writes to driver-locations/{date}/{driver_id}.json
  • Battery drain on the tablet
  • Stops if driver hasn't logged in
  • Spotty in dead zones
After · PosiTrace

worker pulls from positrace api

  • Hardware tracker installed in each truck
  • Worker calls PosiTrace API on dispatch refresh
  • Worker writes to same driver-locations/{date}/{branch}.json
  • No tablet battery cost
  • Works whether driver is logged in or not
  • PosiTrace has its own LTE for redundant connectivity
New data flow

positrace api → worker → r2 → dashboard.

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.

EVERY 30s
DISPATCH PWA
Polls every 15s
GET /api/v1/dispatch/status
WORKER
Cache check
lastRefreshAt > 30s? Refresh.
POSITRACE API
GET /fleet/vehicles
Returns lat/lng/driver per truck
WORKER
Map + write
PosiTrace ID → branch · write to R2
R2 STORAGE
driver-locations/{date}/{branch}.json
Same key as before · drop-in replacement
SAME PATH
WORKER
handleDispatchStatus()
Reads R2 · aggregates locations + routes + events
DISPATCH PWA
Renders map
Truck pins move every 15s

FALLBACK · IF POSITRACE_API_URL/KEY/VEHICLES NOT SET → REVERTS TO PHONE GPS PINGS · NO CODE CHANGE NEEDED

What to ask PosiTrace

three things, one email.

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.

Send the email above to PosiTrace support

Or call them directly if you have a rep — most fleet vendors prefer phone for credential setup. Mention you have Pro tier already.

When they respond, set the three Worker secrets

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

Verify the placeholder API path matches their docs

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.

Verify the response shape matches our parser

Our code expects { vehicles: [{ id, position: { latitude, longitude, captured_at } }] }. If theirs differs, adjust the type definition in positrace.ts. Probably 5-10 lines.

Deploy + verify on dispatch dashboard

npm run deploy the worker. Open the dispatch dashboard. Truck pins should now reflect PosiTrace data, no driver login required.

(Optional) Remove location ping from driver app

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.

The code · already written

~190 lines. one new file. two small edits.

If PosiTrace credentials aren't configured, everything falls back to phone GPS pings automatically. Set the three secrets when you're ready to switch.

worker/src/lib/positrace.ts
excerpt · location refresh
// 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 };
}
worker/src/handlers/dispatch.ts
excerpt · cache + refresh on demand
// 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(...), ...]);
If PosiTrace recommends Forge instead

webhook in. same data, different direction.

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.

worker/src/handlers/positrace-webhook.ts
if Forge path · alternative architecture
// 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.

Changeset

files added or changed.

worker/ ├── wrangler.toml # EDIT · documented 3 new optional secrets └── src/ ├── types.ts # EDIT · added 3 PosiTrace fields to Env (all optional) ├── handlers/dispatch.ts # EDIT · maybeRefreshPosiTrace() before status build └── lib/ └── positrace.ts NEW# API client + R2 writer · 145 lines
1 new file 3 edits ~190 lines added 0 frontend changes 0 new deps