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:
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;"]
|
||||||
|
|||||||
@@ -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,56 +806,54 @@ 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 */}
|
||||||
<div>
|
<details className="bg-muted/30 rounded-lg border border-border">
|
||||||
<label className="text-xs text-muted-foreground">Device ID</label>
|
<summary className="p-3 text-sm font-medium cursor-pointer hover:bg-muted/50 transition-colors">
|
||||||
<div className="flex items-center gap-2">
|
Manual Setup (if QR doesn't work)
|
||||||
<code className="flex-1 bg-muted p-2 rounded text-sm font-mono">
|
</summary>
|
||||||
{enrollmentResult.deviceIdentifier}
|
<div className="px-3 pb-3 space-y-2">
|
||||||
</code>
|
<div>
|
||||||
<button
|
<label className="text-xs text-muted-foreground">Device ID</label>
|
||||||
onClick={() => copyToClipboard(enrollmentResult.deviceIdentifier)}
|
<div className="flex items-center gap-2">
|
||||||
className="p-2 hover:bg-muted rounded transition-colors"
|
<code className="flex-1 bg-muted p-2 rounded text-sm font-mono">
|
||||||
>
|
{enrollmentResult.deviceIdentifier}
|
||||||
<Copy className="h-4 w-4" />
|
</code>
|
||||||
</button>
|
<button
|
||||||
|
onClick={() => copyToClipboard(enrollmentResult.deviceIdentifier)}
|
||||||
|
className="p-2 hover:bg-muted rounded transition-colors"
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<div>
|
<label className="text-xs text-muted-foreground">Server URL</label>
|
||||||
<label className="text-xs text-muted-foreground">Server URL</label>
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<code className="flex-1 bg-muted p-2 rounded text-sm font-mono text-xs">
|
||||||
<code className="flex-1 bg-muted p-2 rounded text-sm font-mono text-xs">
|
{enrollmentResult.serverUrl}
|
||||||
{enrollmentResult.serverUrl}
|
</code>
|
||||||
</code>
|
<button
|
||||||
<button
|
onClick={() => copyToClipboard(enrollmentResult.serverUrl)}
|
||||||
onClick={() => copyToClipboard(enrollmentResult.serverUrl)}
|
className="p-2 hover:bg-muted rounded transition-colors"
|
||||||
className="p-2 hover:bg-muted rounded transition-colors"
|
>
|
||||||
>
|
<Copy className="h-4 w-4" />
|
||||||
<Copy className="h-4 w-4" />
|
</button>
|
||||||
</button>
|
</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>
|
||||||
|
<li>Set frequency to {settings?.updateIntervalSeconds || 60} seconds</li>
|
||||||
|
<li>Tap "Service Status" to start tracking</li>
|
||||||
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</details>
|
||||||
|
|
||||||
<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>Tap "Service Status" to start tracking</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => { setShowEnrollModal(false); setEnrollmentResult(null); }}
|
onClick={() => { setShowEnrollModal(false); setEnrollmentResult(null); }}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user