Skip to main content
This page documents the custom React hooks that power data fetching, SSE connections, and state management for the Fleet Dashboard.

useUnifiedFleetData.ts

The primary data hook that aggregates real-time fleet data from three sources: drivers, vehicles, and trailers.

Purpose

  • Fetch and manage data from three separate API sources
  • Maintain SSE connections for real-time vehicle and trailer updates
  • Calculate fleet statistics (utilization, counts, averages)
  • Provide unified data structure for dashboard components

Import

import { useUnifiedFleetData } from "@/components/hooks/useUnifiedFleetData";

Dependencies

ImportPurpose
sonnertoastError notifications displayed to user
safeParseSSEJSONSafely parses SSE event data (handles malformed JSON)
Error handling: All fetch and SSE errors are displayed via toast.error(). Check the browser console and toast notifications when debugging connection issues.

Return Type

interface UseUnifiedFleetDataResult {
  data: UnifiedFleetData;
  statistics: FleetStatistics;
  connectionState: FleetConnectionState;
  isLoading: boolean;
  refresh: () => Promise<void>;
  topDrivers: DriverSummary[];
  topVehicles: TractorSummary[];
  topTrailers: TrailerSummary[];
}
PropertyTypeDescription
dataUnifiedFleetDataCombined data from all three sources
statisticsFleetStatisticsComputed statistics for KPIs
connectionStateFleetConnectionStateSSE connection status for each source
isLoadingbooleanInitial loading state
refresh() => Promise<void>Manual refresh function
topDriversDriverSummary[]Top 10 active drivers
topVehiclesTractorSummary[]Top 10 vehicles with drivers
topTrailersTrailerSummary[]Top 10 moving trailers

UnifiedFleetData Structure

The data object contains raw arrays plus metadata:
interface UnifiedFleetData {
  drivers: Driver[];
  vehicles: VehicleLocation[];
  trailers: TrailerLocation[];
  driversCount: number;
  vehiclesCount: number;
  trailersCount: number;
  driversPolledAt: string | null;   // ISO timestamp
  vehiclesPolledAt: string | null;  // ISO timestamp
  trailersPolledAt: string | null;  // ISO timestamp
}
Timestamps: The *PolledAt fields indicate when each data source was last fetched/updated. Useful for showing “Last updated X seconds ago” in the UI.

API Endpoints

SourceREST EndpointSSE Endpoint
Drivers/api/drivers/currentNone (polling only)
Vehicles/locations/current/stream/locations
Trailers/api/trailers/current/api/trailers/stream
Base URL: All endpoints use NEXT_PUBLIC_TRACKING_BACKEND_URL environment variable.

Data Flow

1. Component mounts

2. fetchAllData() - Parallel REST calls
   ├── fetchDrivers()  → /api/drivers/current
   ├── fetchVehicles() → /locations/current
   └── fetchTrailers() → /api/trailers/current

3. SSE connections established
   ├── connectVehicleSSE() → /stream/locations
   └── connectTrailerSSE() → /api/trailers/stream

4. Real-time updates via SSE events
   └── "location_update" events update state

5. Statistics recalculated (useMemo)

Internal Functions

Fetches driver data from REST API.Endpoint: GET /api/drivers/currentResponse handling:
  • Supports both old (drivers[]) and new (items[]) API structures
  • Extracts drivers_count or falls back to items.length
  • Extracts polled_at or maxLastUpdatedAt or current timestamp
Fallback chain:
const drivers = rawData.drivers || rawData.items || [];
const driversCount = rawData.drivers_count || rawData.items?.length || 0;
const polledAt = rawData.polled_at || rawData.maxLastUpdatedAt || new Date().toISOString();
State updates:
  • drivers array
  • driversCount
  • driversPolledAt
  • connectionState.drivers
API compatibility: This fallback chain allows the hook to work with different API versions without code changes.
Fetches vehicle data from REST API with structure transformation.Endpoint: GET /locations/currentResponse handling:
  • Supports both old (vehicles[]) and new (items[]) API structures
  • Transforms flat structure to VehicleLocation format if needed
Transformation logic:
// If item has location/address, use as-is
// Otherwise, transform flat structure:
{
  vehicle_id, tractor_number, mcleod_tractor_id, driver_id,
  provider: item.provider || item.last_location_provider || 'unknown',
  location: item.location || { latitude: item.latitude, longitude: item.longitude },
  address: item.address || { formatted: `${lat}, ${lng}` },
  // ... other fields
}
Engine hours conversion: The API may return engine_hours (pre-processed) or engine_runtime (raw). This hook prefers engine_hours.
Fetches trailer data from REST API.Endpoint: GET /api/trailers/currentResponse type: TrailerLocationBatchState updates:
  • trailers array
  • trailersCount
  • trailersPolledAt
  • connectionState.trailers
Establishes SSE connection for real-time vehicle updates.Endpoint: GET /stream/locations (EventSource)Event handlers:
EventAction
connectedUpdate connection state to “connected”
location_updateMerge new vehicle data with existing (preserves mcleod_tractor_id, tractor_number)
onerrorSet connection state to “disconnected”
Data merging: SSE updates may not include all fields. The hook preserves mcleod_tractor_id and tractor_number from existing data when merging updates.
Merge logic:
const updatedVehicles = data.vehicles.map((newVehicle) => {
  const existing = existingMap.get(newVehicle.vehicle_id);
  return {
    ...newVehicle,
    mcleod_tractor_id: newVehicle.mcleod_tractor_id ?? existing?.mcleod_tractor_id ?? null,
    tractor_number: newVehicle.tractor_number ?? existing?.tractor_number ?? null,
  };
});
The ?? (nullish coalescing) means only null or undefined triggers fallback. Empty string "" is preserved from the new data.
Establishes SSE connection for real-time trailer updates.Endpoint: GET /api/trailers/stream (EventSource)Event handlers:
EventAction
connectedUpdate connection state to “connected”
location_updateReplace trailer array with new data
onerrorSet connection state to “disconnected”

Statistics Calculation

The statistics object is computed via useMemo and recalculates when data changes.

Driver Statistics

StatCalculation
totaldrivers.length
activeCount where status === "active"
inactiveCount where status === "inactive"
onDuty0 (requires additional HOS data)
offDuty0 (requires additional HOS data)
hosWarnings0 (requires additional HOS data)

Vehicle Statistics

StatCalculation
totalvehicles.length
engineOnCount where engine_state === "on"
engineOffCount where engine_state === "off"
withDriversCount where driver_id exists (truthy)
availableCount where driver_id is falsy
lowFuelCount where fuel.primary_percentage < 25
avgSpeedAverage speed of “active” vehicles
avgSpeed calculation: Only includes vehicles where:
  • engine_state === "on" AND
  • speed !== null AND
  • speed !== undefined
This ensures we don’t skew averages with parked vehicles or missing data.

Trailer Statistics

StatCalculation
totaltrailers.length
movingCount where converted speed ≥ 5 mph
stationaryCount where converted speed < 5 mph
recentUpdatesCount updated within last 5 minutes
staleDataCount not updated in 10+ minutes
avgSpeedAverage speed of moving trailers
providerCountsRecord<string, number> by provider
Speed conversion for trailers:
const isMotive = t.provider.toLowerCase() === "motive";
const speedInMph = t.speed ? (isMotive ? t.speed * 0.621371 : t.speed) : 0;
  • Motive provider: Speed is in km/h → converted to mph
  • Other providers: Speed assumed to already be in mph (used as-is)
  • Null speed: Treated as 0
Inconsistency note: The LiveTrailerMap component has a different implementation that converts ALL providers the same way. The hook’s logic (shown above) is the correct behavior.

Top Items

The hook provides pre-filtered “top” lists for compact dashboard displays:
PropertyFilterLimit
topDriversstatus === "active"10
topVehiclesdriver_id is truthy and not empty string10
topTrailersConverted speed ≥ 5 mph10
topVehicles filter: The actual check is:
v.driver_id !== null && v.driver_id !== undefined && v.driver_id !== ""
This excludes vehicles with empty string driver IDs, which may occur with some data sources.

Usage Example

export function UnifiedFleetDashboard() {
  const {
    data,
    statistics,
    connectionState,
    isLoading,
    refresh,
    topDrivers,
    topVehicles,
    topTrailers,
  } = useUnifiedFleetData();

  // Access combined data
  const { drivers, vehicles, trailers } = data;

  // Check connection status
  const allConnected = 
    connectionState.drivers.status === "connected" &&
    connectionState.vehicles.status === "connected" &&
    connectionState.trailers.status === "connected";

  // Display KPIs
  return (
    <div>
      <p>Tractor Utilization: {statistics.vehicles.withDrivers}/{statistics.vehicles.total}</p>
      <p>Active Drivers: {statistics.drivers.active}</p>
      <p>Moving Trailers: {statistics.trailers.moving}</p>
    </div>
  );
}

useVehicleLocations.ts

A focused hook for vehicle location data only, used internally by LiveVehicleMap when no filtered data is provided.

Purpose

  • Fetch vehicle locations via REST API
  • Maintain SSE connection for real-time updates
  • Track cache status for debugging
  • Provide simpler interface than useUnifiedFleetData

Import

import { useVehicleLocations } from "@/components/hooks/useVehicleLocations";

Dependencies

ImportPurpose
sonnertoastError notifications
safeParseSSEJSONSafely parses SSE event data

Return Type

interface UseVehicleLocationsResult {
  vehicles: VehicleLocation[];
  connectionState: SSEConnectionState;
  vehiclesCount: number;
  polledAt: string | null;
  cacheStatus: "hit" | "miss" | null;
  refresh: () => Promise<void>;
  isLoading: boolean;
}
PropertyTypeDescription
vehiclesVehicleLocation[]Array of vehicle location objects
connectionStateSSEConnectionStateSSE connection status
vehiclesCountnumberTotal count from API
polledAtstring | nullISO timestamp of last poll
cacheStatus"hit" | "miss" | nullBackend cache status
refresh() => Promise<void>Manual refresh function
isLoadingbooleanInitial loading state

API Endpoints

TypeEndpoint
RESTGET /locations/current
SSEGET /stream/locations

SSE Event Types

EventDescriptionData
connectedConnection established{ message, timestamp, cached_data? }
location_updateNew vehicle dataVehicleLocationsResponse
heartbeatKeep-alive ping{ message? }
Cached data on connect: The SSE connected event may include cached_data with the latest vehicle locations, allowing immediate display without waiting for the first update.

Internal Functions

Fetches vehicle data via REST before SSE connection.Endpoint: GET /locations/currentResponse type: VehicleLocationsResponseUpdates state:
  • vehicles
  • vehiclesCount
  • polledAt
  • cacheStatus
  • connectionState.lastUpdate
Establishes SSE connection for real-time updates.Endpoint: GET /stream/locationsEvent handlers:
EventHandler
connectedSet status to “connected”, optionally load cached data
location_updateReplace vehicles array, update timestamps
heartbeatLog heartbeat (no state change)
onerrorSet status to “disconnected”, show toast error
onmessageFallback for untyped messages (logged only)
The browser’s EventSource automatically handles reconnection on network errors.
Manual refresh that calls fetchInitialData().
const refresh = useCallback(async () => {
  await fetchInitialData();
}, [fetchInitialData]);
Calling refresh() only fetches REST data. SSE connection is not reset.

Lifecycle

useEffect (on mount)
├── fetchInitialData()
│   └── GET /locations/current
├── .then() → connectSSE()
│   └── new EventSource(/stream/locations)
└── return cleanup
    ├── eventSource.close()
    └── clearTimeout(reconnectTimeout)
Note: The reconnectTimeoutRef is defined but not actively used in the current implementation. The browser’s native EventSource auto-reconnect handles reconnection. This ref may be for future custom reconnection logic.

Difference from useUnifiedFleetData

AspectuseVehicleLocationsuseUnifiedFleetData
Data sourcesVehicles onlyDrivers + Vehicles + Trailers
SSE connections12
StatisticsNoneComputed statistics
Top itemsNonetopDrivers, topVehicles, topTrailers
Cache statusTrackedNot tracked
Data transformationNone (expects VehicleLocationsResponse)Transforms flat API structures
Use caseStandalone map componentFull dashboard
Important difference: useVehicleLocations expects the API to return data in VehicleLocationsResponse format directly. It does NOT perform the structure transformation that useUnifiedFleetData does.If the backend returns a flat structure (with latitude/longitude at root level instead of nested location object), use useUnifiedFleetData instead.

Usage Example

export function LiveVehicleMap({ filteredVehicles }: Props) {
  // Only use hook if no filtered data provided
  const { 
    vehicles: allVehicles, 
    connectionState,
    vehiclesCount,
    polledAt,
    isLoading,
    refresh
  } = useVehicleLocations();
  
  // Use filtered vehicles if provided, else all vehicles
  const vehicles = filteredVehicles || allVehicles;
  
  const isConnected = connectionState.status === "connected";
  
  return (
    <div>
      <Badge>{isConnected ? "Live" : "Disconnected"}</Badge>
      <span>{vehiclesCount} vehicles</span>
      <span>Updated: {polledAt}</span>
      <Button onClick={refresh}>Refresh</Button>
    </div>
  );
}

SSE Connection States

Both hooks use similar connection state management:
interface SSEConnectionState {
  status: "connecting" | "connected" | "disconnected" | "error";
  lastUpdate: string | null;  // ISO timestamp
  error: string | null;       // Error message
}

State Transitions

Initial: "connecting"

┌───────────────────────────┐
│  SSE "connected" event → "connected"     │
│                                           │
│  SSE "location_update" → "connected"     │
│          (stays connected)                │
│                                           │
│  SSE error → "disconnected"              │
│        (browser auto-reconnects)          │
│                                           │
│  Fetch error → "error"                   │
│        (requires manual refresh)          │
└───────────────────────────┘
“disconnected” vs “error”:
  • disconnected = SSE connection lost (temporary, auto-reconnects)
  • error = REST fetch failed (permanent, requires user action)

Environment Variables

Both hooks require:
NEXT_PUBLIC_TRACKING_BACKEND_URL=http://localhost:8000
This URL should point to the Tracking Backend service. In production, this would be the deployed backend URL.