# Live Trip Tracking (WebSocket)

Real-time GPS streaming for Flutter driver + admin/manager live map using **Socket.IO** on namespace `/tracking`.

REST batch sync (`POST /api/travel-requests/:id/route-points/batch`) remains the **fallback** when WebSocket is disabled or unavailable.

## Feature flag

| Source | Field |
|--------|--------|
| `GET /api/config/client` | `liveTrackingWsEnabled`, `liveTrackingUseWebSocket` |
| Env | `LIVE_TRACKING_WS_ENABLED=true` (default) |

When `liveTrackingWsEnabled` is `false`, Flutter should use HTTP polling + batch upload only.

## Connection

```
URL: http://<host>:3000/tracking
Auth: handshake.auth.token = "<JWT access token>"
      OR Authorization: Bearer <token>
      OR x-access-token header
```

Ping/pong is handled by Socket.IO (`pingInterval` 25s). On reconnect, re-authenticate and re-emit `tracking.join` for each active trip room.

## Events

### Client → Server

#### `tracking.join`
```json
{ "tripId": "<uuid>" }
```
**Response:** `{ "ok": true, "tripId": "..." }` or `{ "ok": false, "error": "..." }`

- **Driver:** own active trip only.
- **Admin/Manager:** any trip they can read (operations RBAC).

#### `tracking.leave`
```json
{ "tripId": "<uuid>" }
```

#### `tracking.location.update`
```json
{
  "requestId": "<uuid, same as tripId for display>",
  "tripId": "<uuid>",
  "latitude": 12.9716,
  "longitude": 77.5946,
  "timestamp": "2026-06-16T09:00:00.000Z",
  "speed": 12.5,
  "heading": 180,
  "accuracy": 8,
  "address": "Optional reverse-geocode",
  "pointId": "client-uuid-for-idempotency"
}
```
**Response:** `{ "ok": true, "persisted": true }` | `{ "ok": true, "persisted": false, "reason": "rate_limited" }`

Server ignores points when movement &lt; **3 m** within **2 s** (configurable via `GPS_LIVE_MIN_DISTANCE_METERS`, `GPS_LIVE_MIN_INTERVAL_MS`).

Points are persisted to the same `gps_points` collection as REST batch ingest (dedup by `tripId` + `clientPointId` / `pointId`).

### Server → Client

#### `trip.location.updated`
```json
{
  "requestId": "<uuid>",
  "tripId": "<uuid>",
  "userId": "<mongo user id>",
  "latitude": 12.9716,
  "longitude": 77.5946,
  "timestamp": "2026-06-16T09:00:00.000Z",
  "speed": 12.5,
  "heading": 180,
  "accuracy": 8,
  "address": "...",
  "pointId": "..."
}
```

#### `trip.tracking.status`
```json
{ "tripId": "<uuid>", "status": "started" | "paused" | "resumed" | "ended" }
```

| Trip event | `status` |
|------------|----------|
| Departure / TRAVELLING | `started` |
| Arrival / meeting | `paused` |
| Return leg | `resumed` |
| Completed / cancelled | `ended` |

## Lifecycle

```mermaid
sequenceDiagram
  participant Driver as Flutter Driver
  participant WS as /tracking Gateway
  participant DB as MongoDB gps_points
  participant Viewer as Admin Live Map

  Driver->>WS: connect (JWT)
  Driver->>WS: tracking.join { tripId }
  loop every 2-5s while travelling
    Driver->>WS: tracking.location.update
    WS->>DB: upsert point
    WS->>Viewer: trip.location.updated
  end
  Driver->>WS: tracking.location.update (final)
  Driver->>WS: tracking.leave
  Driver->>WS: disconnect
```

1. **Start trip** — `POST .../departure` → receive `trip.tracking.status: started`.
2. **Stream** — connect socket, join room, emit location every 2–5 s from background GPS.
3. **Arrival / end** — emit final point, `tracking.leave`, disconnect if no active trip.
4. **Viewer** — join same `tripId` room(s), update marker + polyline on `trip.location.updated`.

## Reconnect + backfill (viewer & driver)

On socket reconnect:

1. Re-connect with JWT.
2. Re-emit `tracking.join` for each `tripId`.
3. **Backfill missed points:**
   ```
   GET /api/travel-requests/{requestId}/route-points
   ```
   Merge by `timestamp` / `pointId` into local polyline (key map by `tripId` / `requestId`).

## Flutter integration sketch

### Driver (`TripTrackingSocketService`)

```dart
// Pseudocode — implement in your Flutter repo
class TripTrackingSocketService {
  IO.Socket? _socket;
  final _joinedTrips = <String>{};

  Future<void> connect(String accessToken) async {
    _socket = IO.io('$baseUrl/tracking', IO.OptionBuilder()
      .setTransports(['websocket'])
      .setAuth({'token': accessToken})
      .enableReconnection()
      .setReconnectionDelay(1000)
      .setReconnectionDelayMax(10000)
      .build());
    _socket!.onReconnect((_) => _rejoinAll());
  }

  void joinTrip(String tripId) {
    _joinedTrips.add(tripId);
    _socket?.emit('tracking.join', {'tripId': tripId});
  }

  void emitLocation(Map<String, dynamic> payload) {
    _socket?.emit('tracking.location.update', payload);
  }

  void _rejoinAll() {
    for (final id in _joinedTrips) {
      _socket?.emit('tracking.join', {'tripId': id});
    }
  }
}
```

**Fallback:** if `GET /api/config/client` → `liveTrackingWsEnabled == false` or socket errors, buffer points locally and `POST /api/travel-requests/:id/route-points/batch` on interval.

### Admin live map

```dart
_socket.on('trip.location.updated', (data) {
  final tripId = data['tripId'];
  markers[tripId] = LatLng(data['latitude'], data['longitude']);
  polylines[tripId]?.add(LatLng(...));
});
```

After reconnect, call `GET /api/travel-requests/{requestId}/route-points` and merge.

## Data contracts (unchanged)

- `requestId` — primary display id (equals `tripId` UUID).
- `tripId` — backend trip identifier.
- Trip detail still exposes `tripLegs`, `punches`, `routePoints`, `canMarkArrival`, `hasDeparted`.

## Legacy namespaces

Older namespaces `/admin`, `/hod`, `/employee` with events `join:trip`, `gps:batch` remain for backward compatibility. New Flutter builds should use `/tracking` and the events above.

## Env vars

```env
LIVE_TRACKING_WS_ENABLED=true
GPS_LIVE_MIN_INTERVAL_MS=2000
GPS_LIVE_MIN_DISTANCE_METERS=3
```
