feat: add QR code to enrollment screen for Traccar Client setup

Generate a QR code URL containing device ID, server URL, and update
interval that the Traccar Client app can scan to auto-configure.
The enrollment modal now shows the QR prominently with manual setup
collapsed as a fallback. Also pins Traccar to 6.11 and fixes Docker
health checks (IPv6/curl issues).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 20:54:59 +01:00
parent 1e162b4f7c
commit a0d0cbc8f6
5 changed files with 84 additions and 49 deletions

View File

@@ -5,6 +5,7 @@ import {
BadRequestException, BadRequestException,
OnModuleInit, OnModuleInit,
} from '@nestjs/common'; } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Cron, CronExpression } from '@nestjs/schedule'; import { Cron, CronExpression } from '@nestjs/schedule';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { SignalService } from '../signal/signal.service'; import { SignalService } from '../signal/signal.service';
@@ -27,6 +28,7 @@ export class GpsService implements OnModuleInit {
private prisma: PrismaService, private prisma: PrismaService,
private traccarClient: TraccarClientService, private traccarClient: TraccarClientService,
private signalService: SignalService, private signalService: SignalService,
private configService: ConfigService,
) {} ) {}
async onModuleInit() { async onModuleInit() {
@@ -141,6 +143,7 @@ export class GpsService implements OnModuleInit {
success: boolean; success: boolean;
deviceIdentifier: string; deviceIdentifier: string;
serverUrl: string; serverUrl: string;
qrCodeUrl: string;
instructions: string; instructions: string;
signalMessageSent?: boolean; signalMessageSent?: boolean;
}> { }> {
@@ -192,6 +195,19 @@ export class GpsService implements OnModuleInit {
const serverUrl = this.traccarClient.getDeviceServerUrl(); const serverUrl = this.traccarClient.getDeviceServerUrl();
const settings = await this.getSettings(); const settings = await this.getSettings();
// Build QR code URL for Traccar Client app
// Format: https://server:5055?id=DEVICE_ID&interval=SECONDS
// The Traccar Client app parses this as: server URL (origin) + query params (id, interval, etc.)
const devicePort = this.configService.get<number>('TRACCAR_DEVICE_PORT') || 5055;
const traccarPublicUrl = this.traccarClient.getTraccarUrl();
const qrUrl = new URL(traccarPublicUrl);
qrUrl.port = String(devicePort);
qrUrl.searchParams.set('id', actualDeviceId);
qrUrl.searchParams.set('interval', String(settings.updateIntervalSeconds));
const qrCodeUrl = qrUrl.toString();
this.logger.log(`QR code URL for driver: ${qrCodeUrl}`);
const instructions = ` const instructions = `
GPS Tracking Setup Instructions for ${driver.name}: GPS Tracking Setup Instructions for ${driver.name}:
@@ -234,6 +250,7 @@ Note: GPS tracking is only active during shift hours (${settings.shiftStartHour}
success: true, success: true,
deviceIdentifier: actualDeviceId, // Return what Traccar actually stored deviceIdentifier: actualDeviceId, // Return what Traccar actually stored
serverUrl, serverUrl,
qrCodeUrl,
instructions, instructions,
signalMessageSent, signalMessageSent,
}; };

View File

@@ -52,7 +52,7 @@ services:
# Traccar GPS Tracking Server # Traccar GPS Tracking Server
traccar: traccar:
image: traccar/traccar:latest image: traccar/traccar:6.11
container_name: vip-traccar container_name: vip-traccar
ports: ports:
- "8082:8082" # Web UI & API - "8082:8082" # Web UI & API

View File

@@ -46,7 +46,7 @@ EXPOSE 80
# Health check # Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1 CMD wget --no-verbose --tries=1 --spider http://127.0.0.1/ || exit 1
# Start nginx # Start nginx
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]

View File

@@ -768,18 +768,37 @@ export function GpsTracking() {
</div> </div>
)} )}
{/* Manual Configuration - Traccar Client doesn't reliably scan QR codes */} {/* QR Code - scan with Traccar Client app to auto-configure */}
<div className="bg-blue-50 dark:bg-blue-950/30 p-4 rounded-lg border border-blue-200 dark:border-blue-800"> <div className="bg-white dark:bg-gray-900 p-4 rounded-lg border border-border text-center">
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center justify-center gap-2 mb-3">
<Smartphone className="h-5 w-5 text-blue-600" /> <QrCode className="h-5 w-5 text-primary" />
<span className="text-sm font-medium">Scan with Traccar Client</span>
</div>
<div className="flex justify-center mb-3">
<QRCodeSVG
value={enrollmentResult.qrCodeUrl}
size={200}
level="M"
includeMargin
/>
</div>
<p className="text-xs text-muted-foreground">
Open Traccar Client app tap the QR icon scan this code
</p>
</div>
{/* Download links */}
<div className="bg-blue-50 dark:bg-blue-950/30 p-3 rounded-lg border border-blue-200 dark:border-blue-800">
<div className="flex items-center gap-2 mb-2">
<Smartphone className="h-4 w-4 text-blue-600" />
<span className="text-sm font-medium text-blue-800 dark:text-blue-200">Download Traccar Client</span> <span className="text-sm font-medium text-blue-800 dark:text-blue-200">Download Traccar Client</span>
</div> </div>
<div className="flex gap-2 justify-center mb-3"> <div className="flex gap-2 justify-center">
<a <a
href="https://apps.apple.com/app/traccar-client/id843156974" href="https://apps.apple.com/app/traccar-client/id843156974"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="px-3 py-1 bg-blue-600 text-white text-sm rounded hover:bg-blue-700" className="px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700"
> >
iOS App Store iOS App Store
</a> </a>
@@ -787,17 +806,19 @@ export function GpsTracking() {
href="https://play.google.com/store/apps/details?id=org.traccar.client" href="https://play.google.com/store/apps/details?id=org.traccar.client"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="px-3 py-1 bg-green-600 text-white text-sm rounded hover:bg-green-700" className="px-3 py-1 bg-green-600 text-white text-xs rounded hover:bg-green-700"
> >
Google Play Google Play
</a> </a>
</div> </div>
<p className="text-xs text-center text-blue-700 dark:text-blue-300">
Must use official Traccar Client app (not other GPS apps)
</p>
</div> </div>
<div className="space-y-3"> {/* Manual fallback - collapsible */}
<details className="bg-muted/30 rounded-lg border border-border">
<summary className="p-3 text-sm font-medium cursor-pointer hover:bg-muted/50 transition-colors">
Manual Setup (if QR doesn't work)
</summary>
<div className="px-3 pb-3 space-y-2">
<div> <div>
<label className="text-xs text-muted-foreground">Device ID</label> <label className="text-xs text-muted-foreground">Device ID</label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -826,17 +847,13 @@ export function GpsTracking() {
</button> </button>
</div> </div>
</div> </div>
</div> <ol className="text-xs space-y-1 list-decimal list-inside text-muted-foreground mt-2">
<li>Open Traccar Client and enter Device ID and Server URL above</li>
<div className="bg-muted/50 p-3 rounded-lg">
<p className="text-sm font-medium mb-2">Driver Instructions:</p>
<ol className="text-sm space-y-1 list-decimal list-inside text-muted-foreground">
<li>Download "Traccar Client" from App Store or Play Store</li>
<li>Open app and enter the Device ID and Server URL</li>
<li>Set frequency to {settings?.updateIntervalSeconds || 60} seconds</li> <li>Set frequency to {settings?.updateIntervalSeconds || 60} seconds</li>
<li>Tap "Service Status" to start tracking</li> <li>Tap "Service Status" to start tracking</li>
</ol> </ol>
</div> </div>
</details>
<button <button
onClick={() => { setShowEnrollModal(false); setEnrollmentResult(null); }} onClick={() => { setShowEnrollModal(false); setEnrollmentResult(null); }}

View File

@@ -83,6 +83,7 @@ export interface EnrollmentResponse {
success: boolean; success: boolean;
deviceIdentifier: string; deviceIdentifier: string;
serverUrl: string; serverUrl: string;
qrCodeUrl: string;
instructions: string; instructions: string;
signalMessageSent?: boolean; signalMessageSent?: boolean;
} }