Register Phone Number
@@ -541,7 +575,7 @@ export function AdminTools() {
className="flex-1 px-3 py-2 bg-background text-foreground border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
/>
)}
+ {/* CAPTCHA Challenge Modal */}
+ {showCaptcha && (
+
+
+
+
CAPTCHA Verification Required
+
+
+ Signal requires CAPTCHA verification to register this number. Follow these steps:
+
+
+ -
+
+ Open the CAPTCHA page
+
+
+ - Solve the CAPTCHA puzzle
+ - When the "Open Signal" button appears, right-click it
+ - Select "Copy link address" or "Copy Link"
+ - Paste the full link below (starts with
signalcaptcha://)
+
+
+
setCaptchaToken(e.target.value)}
+ placeholder="signalcaptcha://signal-hcaptcha..."
+ className="w-full px-3 py-2 bg-background text-foreground border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary font-mono text-sm"
+ />
+
+
+
+
+
+
+ )}
+
{/* Verify Code */}
{showVerify && (
diff --git a/frontend/src/pages/CommandCenter.tsx b/frontend/src/pages/CommandCenter.tsx
index f3eae70..3550a86 100644
--- a/frontend/src/pages/CommandCenter.tsx
+++ b/frontend/src/pages/CommandCenter.tsx
@@ -153,6 +153,18 @@ export function CommandCenter() {
},
});
+ // Compute awaiting confirmation BEFORE any conditional returns (for hooks)
+ const now = currentTime;
+ const awaitingConfirmation = (events || []).filter((event) => {
+ if (event.status !== 'SCHEDULED' || event.type !== 'TRANSPORT') return false;
+ const start = new Date(event.startTime);
+ return start <= now;
+ });
+
+ // Check which awaiting events have driver responses since the event started
+ // MUST be called before any conditional returns to satisfy React's rules of hooks
+ const { data: respondedEventIds } = useDriverResponseCheck(awaitingConfirmation);
+
// Update clock every second
useEffect(() => {
const clockInterval = setInterval(() => {
@@ -242,7 +254,6 @@ export function CommandCenter() {
return ;
}
- const now = currentTime;
const fifteenMinutes = new Date(now.getTime() + 15 * 60 * 1000);
const thirtyMinutes = new Date(now.getTime() + 30 * 60 * 1000);
const twoHoursLater = new Date(now.getTime() + 2 * 60 * 60 * 1000);
@@ -253,17 +264,6 @@ export function CommandCenter() {
(event) => event.status === 'IN_PROGRESS' && event.type === 'TRANSPORT'
);
- // Trips that SHOULD be active (past start time but still SCHEDULED)
- // These are awaiting driver confirmation
- const awaitingConfirmation = events.filter((event) => {
- if (event.status !== 'SCHEDULED' || event.type !== 'TRANSPORT') return false;
- const start = new Date(event.startTime);
- return start <= now;
- });
-
- // Check which awaiting events have driver responses since the event started
- const { data: respondedEventIds } = useDriverResponseCheck(awaitingConfirmation);
-
// Upcoming trips in next 2 hours
const upcomingTrips = events
.filter((event) => {
diff --git a/frontend/src/pages/DriverProfile.tsx b/frontend/src/pages/DriverProfile.tsx
index ef8ceac..3f65378 100644
--- a/frontend/src/pages/DriverProfile.tsx
+++ b/frontend/src/pages/DriverProfile.tsx
@@ -2,8 +2,23 @@ import { useState, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/api';
import { Loading } from '@/components/Loading';
-import { User, Phone, Save, CheckCircle, AlertCircle } from 'lucide-react';
+import {
+ User,
+ Phone,
+ Save,
+ CheckCircle,
+ AlertCircle,
+ MapPin,
+ Navigation,
+ Route,
+ Gauge,
+ Car,
+ Clock,
+ Shield,
+} from 'lucide-react';
import toast from 'react-hot-toast';
+import { useMyGpsStatus, useMyGpsStats, useUpdateGpsConsent } from '@/hooks/useGps';
+import { formatDistanceToNow } from 'date-fns';
interface DriverProfileData {
id: string;
@@ -221,6 +236,182 @@ export function DriverProfile() {
Trip start confirmation request
+
+ {/* GPS Tracking Section */}
+
+
+ );
+}
+
+function GpsStatsSection() {
+ const { data: gpsStatus, isLoading: statusLoading } = useMyGpsStatus();
+ const { data: gpsStats, isLoading: statsLoading } = useMyGpsStats();
+ const updateConsent = useUpdateGpsConsent();
+
+ if (statusLoading) {
+ return (
+
+
+
+
+
+
GPS Tracking
+
+
+
+ {gpsStatus.isActive ? 'Active' : 'Inactive'}
+
+
+
+ {gpsStatus.lastActive && (
+
+ Last seen: {formatDistanceToNow(new Date(gpsStatus.lastActive), { addSuffix: true })}
+
+ )}
+
+
+ {/* Stats Grid */}
+ {statsLoading ? (
+
+
+
+ ) : gpsStats ? (
+
+
+ Stats for the last 7 days ({new Date(gpsStats.period.from).toLocaleDateString()} - {new Date(gpsStats.period.to).toLocaleDateString()})
+
+
+
+
+
+
{gpsStats.stats.totalMiles}
+
Miles Driven
+
+
+
+
+
{gpsStats.stats.topSpeedMph}
+
Top Speed (mph)
+
+
+
+
+
{gpsStats.stats.averageSpeedMph}
+
Avg Speed (mph)
+
+
+
+
+
{gpsStats.stats.totalTrips}
+
Total Trips
+
+
+
+ {gpsStats.stats.topSpeedTimestamp && (
+
+ Top speed recorded on {new Date(gpsStats.stats.topSpeedTimestamp).toLocaleString()}
+
+ )}
+
+ ) : (
+
+
+
No driving data available yet
+
Start driving to see your stats!
+
+ )}
+
+ {/* Revoke Consent Option */}
+
+
+
);
}
diff --git a/frontend/src/pages/EventList.tsx b/frontend/src/pages/EventList.tsx
index 66477b2..afbe5c1 100644
--- a/frontend/src/pages/EventList.tsx
+++ b/frontend/src/pages/EventList.tsx
@@ -208,15 +208,15 @@ export function EventList() {
return sorted;
}, [events, activeFilter, searchQuery, sortField, sortDirection]);
- const filterTabs: { label: string; value: ActivityFilter; count: number }[] = useMemo(() => {
- if (!events) return [];
+ const filterTabs = useMemo(() => {
+ if (!events) return [] as { label: string; value: ActivityFilter; count: number }[];
return [
- { label: 'All', value: 'ALL', count: events.length },
- { label: 'Transport', value: 'TRANSPORT', count: events.filter(e => e.type === 'TRANSPORT').length },
- { label: 'Meals', value: 'MEAL', count: events.filter(e => e.type === 'MEAL').length },
- { label: 'Events', value: 'EVENT', count: events.filter(e => e.type === 'EVENT').length },
- { label: 'Meetings', value: 'MEETING', count: events.filter(e => e.type === 'MEETING').length },
- { label: 'Accommodation', value: 'ACCOMMODATION', count: events.filter(e => e.type === 'ACCOMMODATION').length },
+ { label: 'All', value: 'ALL' as ActivityFilter, count: events.length },
+ { label: 'Transport', value: 'TRANSPORT' as ActivityFilter, count: events.filter(e => e.type === 'TRANSPORT').length },
+ { label: 'Meals', value: 'MEAL' as ActivityFilter, count: events.filter(e => e.type === 'MEAL').length },
+ { label: 'Events', value: 'EVENT' as ActivityFilter, count: events.filter(e => e.type === 'EVENT').length },
+ { label: 'Meetings', value: 'MEETING' as ActivityFilter, count: events.filter(e => e.type === 'MEETING').length },
+ { label: 'Accommodation', value: 'ACCOMMODATION' as ActivityFilter, count: events.filter(e => e.type === 'ACCOMMODATION').length },
];
}, [events]);
diff --git a/frontend/src/types/gps.ts b/frontend/src/types/gps.ts
new file mode 100644
index 0000000..2ae3d7b
--- /dev/null
+++ b/frontend/src/types/gps.ts
@@ -0,0 +1,100 @@
+export interface LocationData {
+ latitude: number;
+ longitude: number;
+ altitude: number | null;
+ speed: number | null; // mph
+ course: number | null;
+ accuracy: number | null;
+ battery: number | null;
+ timestamp: string;
+}
+
+export interface DriverLocation {
+ driverId: string;
+ driverName: string;
+ driverPhone: string | null;
+ deviceIdentifier: string;
+ isActive: boolean;
+ lastActive: string | null;
+ location: LocationData | null;
+}
+
+export interface GpsDevice {
+ id: string;
+ driverId: string;
+ traccarDeviceId: number;
+ deviceIdentifier: string;
+ enrolledAt: string;
+ consentGiven: boolean;
+ consentGivenAt: string | null;
+ lastActive: string | null;
+ isActive: boolean;
+ driver: {
+ id: string;
+ name: string;
+ phone: string | null;
+ };
+}
+
+export interface DriverStats {
+ driverId: string;
+ driverName: string;
+ period: {
+ from: string;
+ to: string;
+ };
+ stats: {
+ totalMiles: number;
+ topSpeedMph: number;
+ topSpeedTimestamp: string | null;
+ averageSpeedMph: number;
+ totalTrips: number;
+ totalDrivingMinutes: number;
+ };
+ recentLocations: LocationData[];
+}
+
+export interface GpsStatus {
+ traccarAvailable: boolean;
+ traccarVersion: string | null;
+ enrolledDrivers: number;
+ activeDrivers: number;
+ settings: {
+ updateIntervalSeconds: number;
+ shiftStartTime: string;
+ shiftEndTime: string;
+ retentionDays: number;
+ };
+}
+
+export interface GpsSettings {
+ id: string;
+ updateIntervalSeconds: number;
+ shiftStartHour: number;
+ shiftStartMinute: number;
+ shiftEndHour: number;
+ shiftEndMinute: number;
+ retentionDays: number;
+ traccarAdminUser: string;
+ traccarAdminPassword: string | null;
+}
+
+export interface EnrollmentResponse {
+ success: boolean;
+ deviceIdentifier: string;
+ serverUrl: string;
+ port: number;
+ instructions: string;
+ signalMessageSent?: boolean;
+}
+
+export interface MyGpsStatus {
+ enrolled: boolean;
+ driverId?: string;
+ deviceIdentifier?: string;
+ consentGiven?: boolean;
+ consentGivenAt?: string;
+ isActive?: boolean;
+ lastActive?: string;
+ message?: string;
+}
diff --git a/frontend/src/types/settings.ts b/frontend/src/types/settings.ts
index 130a158..980c0c0 100644
--- a/frontend/src/types/settings.ts
+++ b/frontend/src/types/settings.ts
@@ -25,6 +25,7 @@ export interface PdfSettings {
showTimestamp: boolean;
showAppUrl: boolean;
pageSize: PageSize;
+ timezone: string;
// Content Toggles
showFlightInfo: boolean;
@@ -60,6 +61,7 @@ export interface UpdatePdfSettingsDto {
showTimestamp?: boolean;
showAppUrl?: boolean;
pageSize?: PageSize;
+ timezone?: string;
// Content Toggles
showFlightInfo?: boolean;
diff --git a/frontend/tests/production.spec.ts b/frontend/tests/production.spec.ts
new file mode 100644
index 0000000..fca219e
--- /dev/null
+++ b/frontend/tests/production.spec.ts
@@ -0,0 +1,39 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Production Site Tests', () => {
+ test('should load production homepage', async ({ page }) => {
+ console.log('Testing: https://vip.madeamess.online');
+
+ await page.goto('https://vip.madeamess.online', {
+ waitUntil: 'networkidle',
+ timeout: 30000
+ });
+
+ // Check title
+ await expect(page).toHaveTitle(/VIP Coordinator/i);
+ console.log('✅ Page title correct');
+
+ // Take screenshot
+ await page.screenshot({ path: 'production-screenshot.png', fullPage: true });
+ console.log('✅ Screenshot saved');
+ });
+
+ test('should have working API', async ({ request }) => {
+ const response = await request.get('https://vip.madeamess.online/api/v1/health');
+ expect(response.ok()).toBeTruthy();
+
+ const data = await response.json();
+ expect(data.status).toBe('ok');
+ expect(data.environment).toBe('production');
+ console.log('✅ API health check passed:', data);
+ });
+
+ test('should load without errors', async ({ page }) => {
+ await page.goto('https://vip.madeamess.online');
+
+ // Wait for React to render
+ await page.waitForLoadState('networkidle');
+
+ console.log('✅ Page loaded successfully');
+ });
+});
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index c20738e..8b33780 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -12,8 +12,8 @@
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
diff --git a/scripts/setup-auth0-traccar.js b/scripts/setup-auth0-traccar.js
new file mode 100644
index 0000000..3aadb94
--- /dev/null
+++ b/scripts/setup-auth0-traccar.js
@@ -0,0 +1,382 @@
+#!/usr/bin/env node
+/**
+ * Auth0 Setup Script for Traccar GPS Integration
+ *
+ * This script sets up Auth0 roles and actions needed for Traccar GPS tracking
+ * to work with OpenID Connect authentication.
+ *
+ * Usage:
+ * node setup-auth0-traccar.js --token=