fix: improve GPS position sync reliability and add route trails (#21)

Backend:
- Increase sync overlap buffer from 5s to 30s to catch late-arriving positions
- Add position history endpoint GET /gps/locations/:driverId/history
- Add logging for position sync counts (returned vs inserted)

Frontend:
- Add useDriverLocationHistory hook for fetching position trails
- Draw Polyline route trails on GPS map for each tracked driver
- Historical positions shown as semi-transparent paths behind live markers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 16:42:41 +01:00
parent 3bc9cd0bca
commit 4dbb899409
4 changed files with 160 additions and 11 deletions

View File

@@ -130,6 +130,21 @@ export class GpsController {
return location;
}
/**
* Get a driver's location history (for route trail display)
*/
@Get('locations/:driverId/history')
@Roles(Role.ADMINISTRATOR)
async getDriverLocationHistory(
@Param('driverId') driverId: string,
@Query('from') fromStr?: string,
@Query('to') toStr?: string,
) {
const from = fromStr ? new Date(fromStr) : undefined;
const to = toStr ? new Date(toStr) : undefined;
return this.gpsService.getDriverLocationHistory(driverId, from, to);
}
/**
* Get a driver's stats (Admin viewing any driver)
*/

View File

@@ -471,6 +471,49 @@ Note: GPS tracking is only active during shift hours (${settings.shiftStartHour}
};
}
/**
* Get driver location history (for route trail display)
*/
async getDriverLocationHistory(
driverId: string,
fromDate?: Date,
toDate?: Date,
): Promise<LocationDataDto[]> {
const device = await this.prisma.gpsDevice.findUnique({
where: { driverId },
});
if (!device) {
throw new NotFoundException('Driver is not enrolled for GPS tracking');
}
// Default to last 4 hours if no date range specified
const to = toDate || new Date();
const from = fromDate || new Date(to.getTime() - 4 * 60 * 60 * 1000);
const locations = await this.prisma.gpsLocationHistory.findMany({
where: {
deviceId: device.id,
timestamp: {
gte: from,
lte: to,
},
},
orderBy: { timestamp: 'asc' },
});
return locations.map((loc) => ({
latitude: loc.latitude,
longitude: loc.longitude,
altitude: loc.altitude,
speed: loc.speed,
course: loc.course,
accuracy: loc.accuracy,
battery: loc.battery,
timestamp: loc.timestamp,
}));
}
/**
* Get driver's own stats (for driver self-view)
*/
@@ -605,28 +648,37 @@ Note: GPS tracking is only active during shift hours (${settings.shiftStartHour}
},
});
if (devices.length === 0) return;
if (devices.length === 0) {
this.logger.debug('[GPS Sync] No active devices to sync');
return;
}
const now = new Date();
this.logger.log(`[GPS Sync] Starting sync for ${devices.length} active devices`);
for (const device of devices) {
try {
// Calculate "since" from device's last active time with 5s overlap buffer
// Calculate "since" from device's last active time with 30s overlap buffer
// (increased from 5s to catch late-arriving positions)
// Falls back to 2 minutes ago if no lastActive
const since = device.lastActive
? new Date(device.lastActive.getTime() - 5000)
? new Date(device.lastActive.getTime() - 30000)
: new Date(now.getTime() - 120000);
this.logger.debug(`[GPS Sync] Fetching positions for device ${device.traccarDeviceId} since ${since.toISOString()}`);
const positions = await this.traccarClient.getPositionHistory(
device.traccarDeviceId,
since,
now,
);
this.logger.log(`[GPS Sync] Device ${device.traccarDeviceId}: Retrieved ${positions.length} positions from Traccar`);
if (positions.length === 0) continue;
// Batch insert with skipDuplicates (unique constraint on deviceId+timestamp)
await this.prisma.gpsLocationHistory.createMany({
const insertResult = await this.prisma.gpsLocationHistory.createMany({
data: positions.map((p) => ({
deviceId: device.id,
latitude: p.latitude,
@@ -641,6 +693,13 @@ Note: GPS tracking is only active during shift hours (${settings.shiftStartHour}
skipDuplicates: true,
});
const inserted = insertResult.count;
const skipped = positions.length - inserted;
this.logger.log(
`[GPS Sync] Device ${device.traccarDeviceId}: ` +
`Inserted ${inserted} new positions, skipped ${skipped} duplicates`
);
// Update lastActive to the latest position timestamp
const latestPosition = positions.reduce((latest, p) =>
new Date(p.deviceTime) > new Date(latest.deviceTime) ? p : latest
@@ -650,9 +709,11 @@ Note: GPS tracking is only active during shift hours (${settings.shiftStartHour}
data: { lastActive: new Date(latestPosition.deviceTime) },
});
} catch (error) {
this.logger.error(`Failed to sync positions for device ${device.traccarDeviceId}: ${error}`);
this.logger.error(`[GPS Sync] Failed to sync positions for device ${device.traccarDeviceId}: ${error}`);
}
}
this.logger.log('[GPS Sync] Sync completed');
}
/**