From 5638d3960b20c2782fe046ca84828118b259d573 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Thu, 16 Oct 2025 19:20:30 -0500 Subject: [PATCH] Update --- .claude/settings.local.json | 9 +- backend/README.md | 3 +- backend/src/app.ts | 20 +- backend/src/features/maintenance/README.md | 88 +- .../maintenance/api/maintenance.controller.ts | 552 +++++++ .../maintenance/api/maintenance.routes.ts | 77 + .../data/maintenance.repository.ts | 262 ++++ .../maintenance/domain/maintenance.service.ts | 256 ++++ .../maintenance/domain/maintenance.types.ts | 167 ++ backend/src/features/maintenance/index.ts | 11 +- .../002_recreate_maintenance_tables.sql | 80 + docs/DATABASE-SCHEMA.md | 17 +- docs/DOCUMENTATION-AUDIT-REPORT.md | 221 --- docs/MAINTENANCE-FEATURE-PLAN.md | 1037 +++++++++++++ docs/PLATFORM-SERVICES.md | 15 +- docs/README.md | 2 +- docs/UX-DEBUGGING.md | 89 ++ docs/VEHICLES-API.md | 6 +- docs/changes/CLAUDE.md | 1 - docs/changes/DOCUMENTS.md | 299 ---- docs/changes/K8S-REDESIGN.md | 942 ------------ docs/changes/K8S-STATUS.md | 442 ------ docs/changes/MULTI-TENANT-REDESIGN.md | 1185 --------------- .../fuel-logs-v1/FUEL-LOGS-IMPLEMENTATION.md | 164 -- .../changes/fuel-logs-v1/FUEL-LOGS-PHASE-1.md | 391 ----- .../changes/fuel-logs-v1/FUEL-LOGS-PHASE-2.md | 658 -------- .../changes/fuel-logs-v1/FUEL-LOGS-PHASE-3.md | 932 ------------ .../changes/fuel-logs-v1/FUEL-LOGS-PHASE-4.md | 1080 ------------- .../changes/fuel-logs-v1/FUEL-LOGS-PHASE-5.md | 1132 -------------- .../01-RESEARCH-FINDINGS.md | 218 --- .../02-IMPLEMENTATION-PLAN.md | 233 --- .../03-MOBILE-SETTINGS.md | 445 ------ .../04-STATE-MANAGEMENT.md | 671 --------- .../05-TOKEN-OPTIMIZATION.md | 709 --------- .../06-CODE-EXAMPLES.md | 1341 ----------------- .../07-TESTING-CHECKLIST.md | 302 ---- .../IMPLEMENTATION-STATUS.md | 546 ------- docs/changes/mobile-optimization-v1/README.md | 57 - docs/changes/vehicle-names-v1/CODEX.md | 71 - docs/changes/vehicles-dropdown-v1/README.md | 125 -- .../architecture-decisions.md | 465 ------ .../implementation-checklist.md | 634 -------- .../phase-01-infrastructure.md | 290 ---- .../phase-02-backend-migration.md | 601 -------- .../phase-03-api-migration.md | 426 ------ .../phase-04-scheduled-etl.md | 596 -------- .../vehicles-dropdown-v1/phase-05-testing.md | 727 --------- .../01-analysis-findings.md | 203 --- .../02-implementation-plan.md | 307 ---- .../03-engine-spec-parsing.md | 262 ---- .../04-make-name-mapping.md | 331 ---- .../vehicles-dropdown-v2/06-cli-commands.md | 328 ---- .../08-status-tracking.md | 403 ----- docs/changes/vehicles-dropdown-v2/README.md | 99 -- .../examples/engine-parsing-examples.py | 314 ---- .../examples/make-mapping-examples.py | 334 ---- .../examples/sample-json-processing.py | 449 ------ frontend/src/App.tsx | 3 +- frontend/src/components/Layout.tsx | 7 +- .../maintenance/api/maintenance.api.ts | 82 + .../MaintenanceRecordEditDialog.tsx | 314 ++++ .../components/MaintenanceRecordForm.tsx | 378 +++++ .../components/MaintenanceRecordsList.tsx | 221 +++ .../components/SubtypeCheckboxGroup.tsx | 71 + .../hooks/useMaintenanceRecords.ts | 161 ++ frontend/src/features/maintenance/index.ts | 21 + .../maintenance/pages/MaintenancePage.tsx | 117 ++ .../maintenance/types/maintenance.types.ts | 159 ++ 68 files changed, 4164 insertions(+), 18995 deletions(-) create mode 100644 backend/src/features/maintenance/api/maintenance.controller.ts create mode 100644 backend/src/features/maintenance/api/maintenance.routes.ts create mode 100644 backend/src/features/maintenance/data/maintenance.repository.ts create mode 100644 backend/src/features/maintenance/domain/maintenance.service.ts create mode 100644 backend/src/features/maintenance/domain/maintenance.types.ts create mode 100644 backend/src/features/maintenance/migrations/002_recreate_maintenance_tables.sql delete mode 100644 docs/DOCUMENTATION-AUDIT-REPORT.md create mode 100644 docs/MAINTENANCE-FEATURE-PLAN.md create mode 100644 docs/UX-DEBUGGING.md delete mode 100644 docs/changes/CLAUDE.md delete mode 100644 docs/changes/DOCUMENTS.md delete mode 100644 docs/changes/K8S-REDESIGN.md delete mode 100644 docs/changes/K8S-STATUS.md delete mode 100644 docs/changes/MULTI-TENANT-REDESIGN.md delete mode 100644 docs/changes/fuel-logs-v1/FUEL-LOGS-IMPLEMENTATION.md delete mode 100644 docs/changes/fuel-logs-v1/FUEL-LOGS-PHASE-1.md delete mode 100644 docs/changes/fuel-logs-v1/FUEL-LOGS-PHASE-2.md delete mode 100644 docs/changes/fuel-logs-v1/FUEL-LOGS-PHASE-3.md delete mode 100644 docs/changes/fuel-logs-v1/FUEL-LOGS-PHASE-4.md delete mode 100644 docs/changes/fuel-logs-v1/FUEL-LOGS-PHASE-5.md delete mode 100644 docs/changes/mobile-optimization-v1/01-RESEARCH-FINDINGS.md delete mode 100644 docs/changes/mobile-optimization-v1/02-IMPLEMENTATION-PLAN.md delete mode 100644 docs/changes/mobile-optimization-v1/03-MOBILE-SETTINGS.md delete mode 100644 docs/changes/mobile-optimization-v1/04-STATE-MANAGEMENT.md delete mode 100644 docs/changes/mobile-optimization-v1/05-TOKEN-OPTIMIZATION.md delete mode 100644 docs/changes/mobile-optimization-v1/06-CODE-EXAMPLES.md delete mode 100644 docs/changes/mobile-optimization-v1/07-TESTING-CHECKLIST.md delete mode 100644 docs/changes/mobile-optimization-v1/IMPLEMENTATION-STATUS.md delete mode 100644 docs/changes/mobile-optimization-v1/README.md delete mode 100644 docs/changes/vehicle-names-v1/CODEX.md delete mode 100644 docs/changes/vehicles-dropdown-v1/README.md delete mode 100644 docs/changes/vehicles-dropdown-v1/architecture-decisions.md delete mode 100644 docs/changes/vehicles-dropdown-v1/implementation-checklist.md delete mode 100644 docs/changes/vehicles-dropdown-v1/phase-01-infrastructure.md delete mode 100644 docs/changes/vehicles-dropdown-v1/phase-02-backend-migration.md delete mode 100644 docs/changes/vehicles-dropdown-v1/phase-03-api-migration.md delete mode 100644 docs/changes/vehicles-dropdown-v1/phase-04-scheduled-etl.md delete mode 100644 docs/changes/vehicles-dropdown-v1/phase-05-testing.md delete mode 100644 docs/changes/vehicles-dropdown-v2/01-analysis-findings.md delete mode 100644 docs/changes/vehicles-dropdown-v2/02-implementation-plan.md delete mode 100644 docs/changes/vehicles-dropdown-v2/03-engine-spec-parsing.md delete mode 100644 docs/changes/vehicles-dropdown-v2/04-make-name-mapping.md delete mode 100644 docs/changes/vehicles-dropdown-v2/06-cli-commands.md delete mode 100644 docs/changes/vehicles-dropdown-v2/08-status-tracking.md delete mode 100644 docs/changes/vehicles-dropdown-v2/README.md delete mode 100644 docs/changes/vehicles-dropdown-v2/examples/engine-parsing-examples.py delete mode 100644 docs/changes/vehicles-dropdown-v2/examples/make-mapping-examples.py delete mode 100644 docs/changes/vehicles-dropdown-v2/examples/sample-json-processing.py create mode 100644 frontend/src/features/maintenance/api/maintenance.api.ts create mode 100644 frontend/src/features/maintenance/components/MaintenanceRecordEditDialog.tsx create mode 100644 frontend/src/features/maintenance/components/MaintenanceRecordForm.tsx create mode 100644 frontend/src/features/maintenance/components/MaintenanceRecordsList.tsx create mode 100644 frontend/src/features/maintenance/components/SubtypeCheckboxGroup.tsx create mode 100644 frontend/src/features/maintenance/hooks/useMaintenanceRecords.ts create mode 100644 frontend/src/features/maintenance/index.ts create mode 100644 frontend/src/features/maintenance/pages/MaintenancePage.tsx create mode 100644 frontend/src/features/maintenance/types/maintenance.types.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 0e437b8..4cb7614 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -54,8 +54,13 @@ "Bash(ls:*)", "Bash(cp:*)", "Bash(openssl:*)", - "Bash(npm run type-check:*)" + "Bash(npm run type-check:*)", + "Bash(npx tsc:*)", + "Bash(node_modules/.bin/tsc:*)", + "Bash(tree:*)", + "Bash(npm run lint)", + "Bash(cat:*)" ], "deny": [] } -} \ No newline at end of file +} diff --git a/backend/README.md b/backend/README.md index 879f400..cbbace2 100644 --- a/backend/README.md +++ b/backend/README.md @@ -43,7 +43,8 @@ make test - `npm run build` - Build for production - `npm start` - Run production build - `npm test` - Run all tests -- `npm run test:feature -- --feature=vehicles` - Test specific feature +- `npm run test:feature --feature=vehicles` - Test specific feature +- `npm test -- features/vehicles` - Alternative: Test specific feature by path pattern - `npm run schema:generate` - Generate combined schema ## Core Modules diff --git a/backend/src/app.ts b/backend/src/app.ts index 50834eb..89373a6 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -19,6 +19,7 @@ import { fuelLogsRoutes } from './features/fuel-logs/api/fuel-logs.routes'; import { stationsRoutes } from './features/stations/api/stations.routes'; import tenantManagementRoutes from './features/tenant-management/index'; import { documentsRoutes } from './features/documents/api/documents.routes'; +import { maintenanceRoutes } from './features/maintenance'; async function buildApp(): Promise { const app = Fastify({ @@ -113,26 +114,9 @@ async function buildApp(): Promise { await app.register(documentsRoutes, { prefix: '/api' }); await app.register(fuelLogsRoutes, { prefix: '/api' }); await app.register(stationsRoutes, { prefix: '/api' }); + await app.register(maintenanceRoutes, { prefix: '/api' }); await app.register(tenantManagementRoutes); - // Maintenance feature placeholder (not yet implemented) - await app.register(async (fastify) => { - // Maintenance routes - basic placeholder for future implementation - fastify.get('/api/maintenance*', async (_request, reply) => { - return reply.code(501).send({ - error: 'Not Implemented', - message: 'Maintenance feature not yet implemented' - }); - }); - - fastify.post('/api/maintenance*', async (_request, reply) => { - return reply.code(501).send({ - error: 'Not Implemented', - message: 'Maintenance feature not yet implemented' - }); - }); - }); - // 404 handler app.setNotFoundHandler(async (_request, reply) => { return reply.code(404).send({ error: 'Route not found' }); diff --git a/backend/src/features/maintenance/README.md b/backend/src/features/maintenance/README.md index 833c899..24a01d9 100644 --- a/backend/src/features/maintenance/README.md +++ b/backend/src/features/maintenance/README.md @@ -1,31 +1,89 @@ # Maintenance Feature Capsule -## Status -- WIP: Scaffolded; implementation pending. Track updates in `docs/changes/MULTI-TENANT-REDESIGN.md` and related feature plans. +## Quick Summary + +Tracks vehicle maintenance including routine service, repairs, and performance upgrades. Supports multiple subtypes per record, recurring schedules, and upcoming/overdue calculations. User-scoped data with vehicle ownership enforcement. + +## API Endpoints + +### Maintenance Records +- `POST /api/maintenance/records` - Create a new maintenance record +- `GET /api/maintenance/records` - List all records (optional filters: vehicleId, category) +- `GET /api/maintenance/records/:id` - Get single record by ID +- `GET /api/maintenance/records/vehicle/:vehicleId` - Get all records for a vehicle +- `PUT /api/maintenance/records/:id` - Update existing record +- `DELETE /api/maintenance/records/:id` - Delete record + +### Maintenance Schedules +- `POST /api/maintenance/schedules` - Create recurring schedule +- `GET /api/maintenance/schedules/vehicle/:vehicleId` - Get schedules for a vehicle +- `PUT /api/maintenance/schedules/:id` - Update schedule +- `DELETE /api/maintenance/schedules/:id` - Delete schedule + +### Utilities +- `GET /api/maintenance/upcoming/:vehicleId` - Get upcoming/overdue maintenance (optional query: currentMileage) +- `GET /api/maintenance/subtypes/:category` - Get valid subtypes for a category ## Structure + - **api/** - HTTP endpoints, routes, validators - **domain/** - Business logic, types, rules - **data/** - Repository, database queries - **migrations/** - Feature-specific schema -- **external/** - External API integrations -- **events/** - Event handlers - **tests/** - All feature tests -- **docs/** - Detailed documentation + +## Categories and Subtypes + +### Routine Maintenance (27 subtypes) +Accelerator Pedal, Air Filter Element, Brakes and Traction Control, Cabin Air Filter / Purifier, Coolant, Doors, Drive Belt, Engine Oil, Evaporative Emissions System, Exhaust System, Fluid - A/T, Fluid - Differential, Fluid - M/T, Fluid Filter - A/T, Fluids, Fuel Delivery and Air Induction, Hood Shock / Support, Neutral Safety Switch, Parking Brake System, Restraints and Safety Systems, Shift Interlock A/T, Spark Plug, Steering and Suspension, Tires, Trunk / Liftgate Shock / Support, Washer Fluid, Wiper Blade + +### Repair (5 subtypes) +Engine, Transmission, Drivetrain, Exterior, Interior + +### Performance Upgrade (5 subtypes) +Engine, Drivetrain, Suspension, Wheels/Tires, Exterior ## Dependencies -- Internal: core/auth, core/cache -- External: (none) -- Database: maintenance table (see `docs/DATABASE-SCHEMA.md`) -## Quick Commands +### Internal +- `core/auth` - Authentication plugin +- `core/logging` - Structured logging +- `core/config` - Database pool + +### Database +- Tables: `maintenance_records`, `maintenance_schedules` +- FK: `vehicles(id)` - CASCADE DELETE + +## Business Rules + +### Validation +1. Category must be: `routine_maintenance`, `repair`, or `performance_upgrade` +2. Subtypes array must be non-empty +3. All subtypes must be valid for the selected category +4. Date required for records +5. Vehicle must belong to user (ownership check) +6. At least one interval (months OR miles OR both) required for schedules + +### Next Due Calculation + +If interval_months AND interval_miles both set, due when EITHER condition is met (whichever comes first). If only one interval set, calculate based on that single criterion. + +### Due Soon / Overdue Logic + +**Due Soon**: next_due_date within 30 days OR next_due_mileage within 500 miles +**Overdue**: next_due_date in the past OR next_due_mileage < current odometer + +## Security Requirements + +1. All queries user-scoped (filter by user_id) +2. Vehicle ownership validated before operations +3. Prepared statements (never concatenate SQL) +4. All routes require JWT authentication +5. Users can only access their own data + +## Testing + ```bash # Run feature tests npm test -- features/maintenance - -# Run feature migrations -npm run migrate:feature maintenance ``` - -## API (planned) -- Endpoints and business rules to be finalized; depends on vehicles. See `docs/DATABASE-SCHEMA.md` for current table shape and indexes. diff --git a/backend/src/features/maintenance/api/maintenance.controller.ts b/backend/src/features/maintenance/api/maintenance.controller.ts new file mode 100644 index 0000000..d4a0917 --- /dev/null +++ b/backend/src/features/maintenance/api/maintenance.controller.ts @@ -0,0 +1,552 @@ +import { FastifyReply, FastifyRequest } from 'fastify'; +import { MaintenanceService } from '../domain/maintenance.service'; +import { logger } from '../../../core/logging/logger'; +import { + CreateMaintenanceRecordSchema, + UpdateMaintenanceRecordSchema, + CreateScheduleSchema, + UpdateScheduleSchema, + getSubtypesForCategory, + MaintenanceCategory +} from '../domain/maintenance.types'; +import { z } from 'zod'; + +export class MaintenanceController { + private readonly service = new MaintenanceService(); + + async listRecords( + request: FastifyRequest<{ Querystring: { vehicleId?: string; category?: string } }>, + reply: FastifyReply + ) { + const userId = (request as any).user?.sub as string; + + logger.info('Maintenance records list requested', { + operation: 'maintenance.records.list', + user_id: userId, + filters: { + vehicle_id: request.query.vehicleId, + category: request.query.category, + }, + }); + + try { + const filters: { vehicleId?: string; category?: MaintenanceCategory } = {}; + if (request.query.vehicleId) { + filters.vehicleId = request.query.vehicleId; + } + if (request.query.category) { + filters.category = request.query.category as MaintenanceCategory; + } + + const records = await this.service.getRecords(userId, filters); + + logger.info('Maintenance records list retrieved', { + operation: 'maintenance.records.list.success', + user_id: userId, + record_count: records.length, + }); + + return reply.code(200).send(records); + } catch (error) { + logger.error('Failed to list maintenance records', { + operation: 'maintenance.records.list.error', + user_id: userId, + error: error instanceof Error ? error.message : 'Unknown error', + }); + throw error; + } + } + + async getRecord(request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) { + const userId = (request as any).user?.sub as string; + const recordId = request.params.id; + + logger.info('Maintenance record get requested', { + operation: 'maintenance.records.get', + user_id: userId, + record_id: recordId, + }); + + try { + const record = await this.service.getRecord(userId, recordId); + if (!record) { + logger.warn('Maintenance record not found', { + operation: 'maintenance.records.get.not_found', + user_id: userId, + record_id: recordId, + }); + return reply.code(404).send({ error: 'Not Found' }); + } + + logger.info('Maintenance record retrieved', { + operation: 'maintenance.records.get.success', + user_id: userId, + record_id: recordId, + vehicle_id: record.vehicle_id, + category: record.category, + }); + + return reply.code(200).send(record); + } catch (error) { + logger.error('Failed to get maintenance record', { + operation: 'maintenance.records.get.error', + user_id: userId, + record_id: recordId, + error: error instanceof Error ? error.message : 'Unknown error', + }); + throw error; + } + } + + async getRecordsByVehicle( + request: FastifyRequest<{ Params: { vehicleId: string } }>, + reply: FastifyReply + ) { + const userId = (request as any).user?.sub as string; + const vehicleId = request.params.vehicleId; + + logger.info('Maintenance records by vehicle requested', { + operation: 'maintenance.records.by_vehicle', + user_id: userId, + vehicle_id: vehicleId, + }); + + try { + const records = await this.service.getRecordsByVehicle(userId, vehicleId); + + logger.info('Maintenance records by vehicle retrieved', { + operation: 'maintenance.records.by_vehicle.success', + user_id: userId, + vehicle_id: vehicleId, + record_count: records.length, + }); + + return reply.code(200).send(records); + } catch (error) { + logger.error('Failed to get maintenance records by vehicle', { + operation: 'maintenance.records.by_vehicle.error', + user_id: userId, + vehicle_id: vehicleId, + error: error instanceof Error ? error.message : 'Unknown error', + }); + throw error; + } + } + + async createRecord(request: FastifyRequest<{ Body: unknown }>, reply: FastifyReply) { + const userId = (request as any).user?.sub as string; + + logger.info('Maintenance record create requested', { + operation: 'maintenance.records.create', + user_id: userId, + }); + + try { + const validated = CreateMaintenanceRecordSchema.parse(request.body); + + const record = await this.service.createRecord(userId, validated); + + logger.info('Maintenance record created', { + operation: 'maintenance.records.create.success', + user_id: userId, + record_id: record.id, + vehicle_id: record.vehicle_id, + category: record.category, + subtype_count: record.subtypes.length, + }); + + return reply.code(201).send(record); + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn('Maintenance record validation failed', { + operation: 'maintenance.records.create.validation_error', + user_id: userId, + errors: error.errors, + }); + return reply.code(400).send({ error: 'Bad Request', details: error.errors }); + } + + if (error instanceof Error && 'statusCode' in error) { + const statusCode = (error as any).statusCode; + logger.warn('Maintenance record creation failed', { + operation: 'maintenance.records.create.error', + user_id: userId, + status_code: statusCode, + error: error.message, + }); + return reply.code(statusCode).send({ error: error.message }); + } + + logger.error('Failed to create maintenance record', { + operation: 'maintenance.records.create.error', + user_id: userId, + error: error instanceof Error ? error.message : 'Unknown error', + }); + throw error; + } + } + + async updateRecord( + request: FastifyRequest<{ Params: { id: string }; Body: unknown }>, + reply: FastifyReply + ) { + const userId = (request as any).user?.sub as string; + const recordId = request.params.id; + + logger.info('Maintenance record update requested', { + operation: 'maintenance.records.update', + user_id: userId, + record_id: recordId, + }); + + try { + const validated = UpdateMaintenanceRecordSchema.parse(request.body); + + const record = await this.service.updateRecord(userId, recordId, validated); + if (!record) { + logger.warn('Maintenance record not found for update', { + operation: 'maintenance.records.update.not_found', + user_id: userId, + record_id: recordId, + }); + return reply.code(404).send({ error: 'Not Found' }); + } + + logger.info('Maintenance record updated', { + operation: 'maintenance.records.update.success', + user_id: userId, + record_id: recordId, + vehicle_id: record.vehicle_id, + category: record.category, + }); + + return reply.code(200).send(record); + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn('Maintenance record update validation failed', { + operation: 'maintenance.records.update.validation_error', + user_id: userId, + record_id: recordId, + errors: error.errors, + }); + return reply.code(400).send({ error: 'Bad Request', details: error.errors }); + } + + if (error instanceof Error && 'statusCode' in error) { + const statusCode = (error as any).statusCode; + logger.warn('Maintenance record update failed', { + operation: 'maintenance.records.update.error', + user_id: userId, + record_id: recordId, + status_code: statusCode, + error: error.message, + }); + return reply.code(statusCode).send({ error: error.message }); + } + + logger.error('Failed to update maintenance record', { + operation: 'maintenance.records.update.error', + user_id: userId, + record_id: recordId, + error: error instanceof Error ? error.message : 'Unknown error', + }); + throw error; + } + } + + async deleteRecord(request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) { + const userId = (request as any).user?.sub as string; + const recordId = request.params.id; + + logger.info('Maintenance record delete requested', { + operation: 'maintenance.records.delete', + user_id: userId, + record_id: recordId, + }); + + try { + await this.service.deleteRecord(userId, recordId); + + logger.info('Maintenance record deleted', { + operation: 'maintenance.records.delete.success', + user_id: userId, + record_id: recordId, + }); + + return reply.code(204).send(); + } catch (error) { + logger.error('Failed to delete maintenance record', { + operation: 'maintenance.records.delete.error', + user_id: userId, + record_id: recordId, + error: error instanceof Error ? error.message : 'Unknown error', + }); + throw error; + } + } + + async getSchedulesByVehicle( + request: FastifyRequest<{ Params: { vehicleId: string } }>, + reply: FastifyReply + ) { + const userId = (request as any).user?.sub as string; + const vehicleId = request.params.vehicleId; + + logger.info('Maintenance schedules by vehicle requested', { + operation: 'maintenance.schedules.by_vehicle', + user_id: userId, + vehicle_id: vehicleId, + }); + + try { + const schedules = await this.service.getSchedulesByVehicle(userId, vehicleId); + + logger.info('Maintenance schedules by vehicle retrieved', { + operation: 'maintenance.schedules.by_vehicle.success', + user_id: userId, + vehicle_id: vehicleId, + schedule_count: schedules.length, + }); + + return reply.code(200).send(schedules); + } catch (error) { + logger.error('Failed to get maintenance schedules by vehicle', { + operation: 'maintenance.schedules.by_vehicle.error', + user_id: userId, + vehicle_id: vehicleId, + error: error instanceof Error ? error.message : 'Unknown error', + }); + throw error; + } + } + + async createSchedule(request: FastifyRequest<{ Body: unknown }>, reply: FastifyReply) { + const userId = (request as any).user?.sub as string; + + logger.info('Maintenance schedule create requested', { + operation: 'maintenance.schedules.create', + user_id: userId, + }); + + try { + const validated = CreateScheduleSchema.parse(request.body); + + const schedule = await this.service.createSchedule(userId, validated); + + logger.info('Maintenance schedule created', { + operation: 'maintenance.schedules.create.success', + user_id: userId, + schedule_id: schedule.id, + vehicle_id: schedule.vehicle_id, + category: schedule.category, + subtype_count: schedule.subtypes.length, + }); + + return reply.code(201).send(schedule); + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn('Maintenance schedule validation failed', { + operation: 'maintenance.schedules.create.validation_error', + user_id: userId, + errors: error.errors, + }); + return reply.code(400).send({ error: 'Bad Request', details: error.errors }); + } + + if (error instanceof Error && 'statusCode' in error) { + const statusCode = (error as any).statusCode; + logger.warn('Maintenance schedule creation failed', { + operation: 'maintenance.schedules.create.error', + user_id: userId, + status_code: statusCode, + error: error.message, + }); + return reply.code(statusCode).send({ error: error.message }); + } + + logger.error('Failed to create maintenance schedule', { + operation: 'maintenance.schedules.create.error', + user_id: userId, + error: error instanceof Error ? error.message : 'Unknown error', + }); + throw error; + } + } + + async updateSchedule( + request: FastifyRequest<{ Params: { id: string }; Body: unknown }>, + reply: FastifyReply + ) { + const userId = (request as any).user?.sub as string; + const scheduleId = request.params.id; + + logger.info('Maintenance schedule update requested', { + operation: 'maintenance.schedules.update', + user_id: userId, + schedule_id: scheduleId, + }); + + try { + const validated = UpdateScheduleSchema.parse(request.body); + + const schedule = await this.service.updateSchedule(userId, scheduleId, validated); + if (!schedule) { + logger.warn('Maintenance schedule not found for update', { + operation: 'maintenance.schedules.update.not_found', + user_id: userId, + schedule_id: scheduleId, + }); + return reply.code(404).send({ error: 'Not Found' }); + } + + logger.info('Maintenance schedule updated', { + operation: 'maintenance.schedules.update.success', + user_id: userId, + schedule_id: scheduleId, + vehicle_id: schedule.vehicle_id, + category: schedule.category, + }); + + return reply.code(200).send(schedule); + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn('Maintenance schedule update validation failed', { + operation: 'maintenance.schedules.update.validation_error', + user_id: userId, + schedule_id: scheduleId, + errors: error.errors, + }); + return reply.code(400).send({ error: 'Bad Request', details: error.errors }); + } + + if (error instanceof Error && 'statusCode' in error) { + const statusCode = (error as any).statusCode; + logger.warn('Maintenance schedule update failed', { + operation: 'maintenance.schedules.update.error', + user_id: userId, + schedule_id: scheduleId, + status_code: statusCode, + error: error.message, + }); + return reply.code(statusCode).send({ error: error.message }); + } + + logger.error('Failed to update maintenance schedule', { + operation: 'maintenance.schedules.update.error', + user_id: userId, + schedule_id: scheduleId, + error: error instanceof Error ? error.message : 'Unknown error', + }); + throw error; + } + } + + async deleteSchedule(request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) { + const userId = (request as any).user?.sub as string; + const scheduleId = request.params.id; + + logger.info('Maintenance schedule delete requested', { + operation: 'maintenance.schedules.delete', + user_id: userId, + schedule_id: scheduleId, + }); + + try { + await this.service.deleteSchedule(userId, scheduleId); + + logger.info('Maintenance schedule deleted', { + operation: 'maintenance.schedules.delete.success', + user_id: userId, + schedule_id: scheduleId, + }); + + return reply.code(204).send(); + } catch (error) { + logger.error('Failed to delete maintenance schedule', { + operation: 'maintenance.schedules.delete.error', + user_id: userId, + schedule_id: scheduleId, + error: error instanceof Error ? error.message : 'Unknown error', + }); + throw error; + } + } + + async getUpcoming( + request: FastifyRequest<{ Params: { vehicleId: string }; Querystring: { currentMileage?: string } }>, + reply: FastifyReply + ) { + const userId = (request as any).user?.sub as string; + const vehicleId = request.params.vehicleId; + const currentMileage = request.query.currentMileage ? parseInt(request.query.currentMileage, 10) : undefined; + + logger.info('Upcoming maintenance requested', { + operation: 'maintenance.upcoming', + user_id: userId, + vehicle_id: vehicleId, + current_mileage: currentMileage, + }); + + try { + const upcoming = await this.service.getUpcomingMaintenance(userId, vehicleId, currentMileage); + + logger.info('Upcoming maintenance retrieved', { + operation: 'maintenance.upcoming.success', + user_id: userId, + vehicle_id: vehicleId, + upcoming_count: upcoming.length, + }); + + return reply.code(200).send(upcoming); + } catch (error) { + logger.error('Failed to get upcoming maintenance', { + operation: 'maintenance.upcoming.error', + user_id: userId, + vehicle_id: vehicleId, + error: error instanceof Error ? error.message : 'Unknown error', + }); + throw error; + } + } + + async getSubtypes(request: FastifyRequest<{ Params: { category: string } }>, reply: FastifyReply) { + const userId = (request as any).user?.sub as string; + const category = request.params.category; + + logger.info('Maintenance subtypes requested', { + operation: 'maintenance.subtypes', + user_id: userId, + category: category, + }); + + try { + if (!['routine_maintenance', 'repair', 'performance_upgrade'].includes(category)) { + logger.warn('Invalid maintenance category', { + operation: 'maintenance.subtypes.invalid_category', + user_id: userId, + category: category, + }); + return reply.code(400).send({ error: 'Bad Request', message: 'Invalid category' }); + } + + const subtypes = getSubtypesForCategory(category as MaintenanceCategory); + + logger.info('Maintenance subtypes retrieved', { + operation: 'maintenance.subtypes.success', + user_id: userId, + category: category, + subtype_count: subtypes.length, + }); + + return reply.code(200).send({ category, subtypes: Array.from(subtypes) }); + } catch (error) { + logger.error('Failed to get maintenance subtypes', { + operation: 'maintenance.subtypes.error', + user_id: userId, + category: category, + error: error instanceof Error ? error.message : 'Unknown error', + }); + throw error; + } + } +} diff --git a/backend/src/features/maintenance/api/maintenance.routes.ts b/backend/src/features/maintenance/api/maintenance.routes.ts new file mode 100644 index 0000000..87459e9 --- /dev/null +++ b/backend/src/features/maintenance/api/maintenance.routes.ts @@ -0,0 +1,77 @@ +/** + * @ai-summary Fastify routes for maintenance API + */ +import { FastifyInstance, FastifyPluginAsync, FastifyPluginOptions } from 'fastify'; +import { tenantMiddleware } from '../../../core/middleware/tenant'; +import { MaintenanceController } from './maintenance.controller'; + +export const maintenanceRoutes: FastifyPluginAsync = async ( + fastify: FastifyInstance, + _opts: FastifyPluginOptions +) => { + const ctrl = new MaintenanceController(); + const requireAuth = fastify.authenticate.bind(fastify); + + // Maintenance Records + fastify.get('/maintenance/records', { + preHandler: [requireAuth, tenantMiddleware as any], + handler: ctrl.listRecords.bind(ctrl) + }); + + fastify.get<{ Params: any }>('/maintenance/records/:id', { + preHandler: [requireAuth, tenantMiddleware as any], + handler: ctrl.getRecord.bind(ctrl) + }); + + fastify.get<{ Params: any }>('/maintenance/records/vehicle/:vehicleId', { + preHandler: [requireAuth, tenantMiddleware as any], + handler: ctrl.getRecordsByVehicle.bind(ctrl) + }); + + fastify.post<{ Body: any }>('/maintenance/records', { + preHandler: [requireAuth, tenantMiddleware as any], + handler: ctrl.createRecord.bind(ctrl) + }); + + fastify.put<{ Params: any; Body: any }>('/maintenance/records/:id', { + preHandler: [requireAuth, tenantMiddleware as any], + handler: ctrl.updateRecord.bind(ctrl) + }); + + fastify.delete<{ Params: any }>('/maintenance/records/:id', { + preHandler: [requireAuth, tenantMiddleware as any], + handler: ctrl.deleteRecord.bind(ctrl) + }); + + // Maintenance Schedules + fastify.get<{ Params: any }>('/maintenance/schedules/vehicle/:vehicleId', { + preHandler: [requireAuth, tenantMiddleware as any], + handler: ctrl.getSchedulesByVehicle.bind(ctrl) + }); + + fastify.post<{ Body: any }>('/maintenance/schedules', { + preHandler: [requireAuth, tenantMiddleware as any], + handler: ctrl.createSchedule.bind(ctrl) + }); + + fastify.put<{ Params: any; Body: any }>('/maintenance/schedules/:id', { + preHandler: [requireAuth, tenantMiddleware as any], + handler: ctrl.updateSchedule.bind(ctrl) + }); + + fastify.delete<{ Params: any }>('/maintenance/schedules/:id', { + preHandler: [requireAuth, tenantMiddleware as any], + handler: ctrl.deleteSchedule.bind(ctrl) + }); + + // Utility Routes + fastify.get<{ Params: { vehicleId: string }; Querystring: { currentMileage?: string } }>('/maintenance/upcoming/:vehicleId', { + preHandler: [requireAuth, tenantMiddleware as any], + handler: ctrl.getUpcoming.bind(ctrl) + }); + + fastify.get<{ Params: any }>('/maintenance/subtypes/:category', { + preHandler: [requireAuth, tenantMiddleware as any], + handler: ctrl.getSubtypes.bind(ctrl) + }); +}; diff --git a/backend/src/features/maintenance/data/maintenance.repository.ts b/backend/src/features/maintenance/data/maintenance.repository.ts new file mode 100644 index 0000000..42ed46a --- /dev/null +++ b/backend/src/features/maintenance/data/maintenance.repository.ts @@ -0,0 +1,262 @@ +import { Pool } from 'pg'; +import pool from '../../../core/config/database'; +import type { MaintenanceRecord, MaintenanceSchedule, MaintenanceCategory } from '../domain/maintenance.types'; + +export class MaintenanceRepository { + constructor(private readonly db: Pool = pool) {} + + // ======================== + // Maintenance Records + // ======================== + + async insertRecord(record: { + id: string; + user_id: string; + vehicle_id: string; + category: MaintenanceCategory; + subtypes: string[]; + date: string; + odometer_reading?: number | null; + cost?: number | null; + shop_name?: string | null; + notes?: string | null; + }): Promise { + const res = await this.db.query( + `INSERT INTO maintenance_records ( + id, user_id, vehicle_id, category, subtypes, date, odometer_reading, cost, shop_name, notes + ) VALUES ($1, $2, $3, $4, $5::text[], $6, $7, $8, $9, $10) + RETURNING *`, + [ + record.id, + record.user_id, + record.vehicle_id, + record.category, + record.subtypes, + record.date, + record.odometer_reading ?? null, + record.cost ?? null, + record.shop_name ?? null, + record.notes ?? null, + ] + ); + return res.rows[0] as MaintenanceRecord; + } + + async findRecordById(id: string, userId: string): Promise { + const res = await this.db.query( + `SELECT * FROM maintenance_records WHERE id = $1 AND user_id = $2`, + [id, userId] + ); + return res.rows[0] || null; + } + + async findRecordsByUserId( + userId: string, + filters?: { vehicleId?: string; category?: MaintenanceCategory } + ): Promise { + const conds: string[] = ['user_id = $1']; + const params: any[] = [userId]; + let i = 2; + + if (filters?.vehicleId) { + conds.push(`vehicle_id = $${i++}`); + params.push(filters.vehicleId); + } + if (filters?.category) { + conds.push(`category = $${i++}`); + params.push(filters.category); + } + + const sql = `SELECT * FROM maintenance_records WHERE ${conds.join(' AND ')} ORDER BY date DESC`; + const res = await this.db.query(sql, params); + return res.rows as MaintenanceRecord[]; + } + + async findRecordsByVehicleId(vehicleId: string, userId: string): Promise { + const res = await this.db.query( + `SELECT * FROM maintenance_records WHERE vehicle_id = $1 AND user_id = $2 ORDER BY date DESC`, + [vehicleId, userId] + ); + return res.rows as MaintenanceRecord[]; + } + + async updateRecord( + id: string, + userId: string, + patch: Partial> + ): Promise { + const fields: string[] = []; + const params: any[] = []; + let i = 1; + + if (patch.category !== undefined) { + fields.push(`category = $${i++}`); + params.push(patch.category); + } + if (patch.subtypes !== undefined) { + fields.push(`subtypes = $${i++}::text[]`); + params.push(patch.subtypes); + } + if (patch.date !== undefined) { + fields.push(`date = $${i++}`); + params.push(patch.date); + } + if (patch.odometer_reading !== undefined) { + fields.push(`odometer_reading = $${i++}`); + params.push(patch.odometer_reading); + } + if (patch.cost !== undefined) { + fields.push(`cost = $${i++}`); + params.push(patch.cost); + } + if (patch.shop_name !== undefined) { + fields.push(`shop_name = $${i++}`); + params.push(patch.shop_name); + } + if (patch.notes !== undefined) { + fields.push(`notes = $${i++}`); + params.push(patch.notes); + } + + if (!fields.length) return this.findRecordById(id, userId); + + params.push(id, userId); + const sql = `UPDATE maintenance_records SET ${fields.join(', ')} WHERE id = $${i++} AND user_id = $${i++} RETURNING *`; + const res = await this.db.query(sql, params); + return res.rows[0] || null; + } + + async deleteRecord(id: string, userId: string): Promise { + await this.db.query( + `DELETE FROM maintenance_records WHERE id = $1 AND user_id = $2`, + [id, userId] + ); + } + + // ======================== + // Maintenance Schedules + // ======================== + + async insertSchedule(schedule: { + id: string; + user_id: string; + vehicle_id: string; + category: MaintenanceCategory; + subtypes: string[]; + interval_months?: number | null; + interval_miles?: number | null; + last_service_date?: string | null; + last_service_mileage?: number | null; + next_due_date?: string | null; + next_due_mileage?: number | null; + is_active: boolean; + }): Promise { + const res = await this.db.query( + `INSERT INTO maintenance_schedules ( + id, user_id, vehicle_id, category, subtypes, interval_months, interval_miles, + last_service_date, last_service_mileage, next_due_date, next_due_mileage, is_active + ) VALUES ($1, $2, $3, $4, $5::text[], $6, $7, $8, $9, $10, $11, $12) + RETURNING *`, + [ + schedule.id, + schedule.user_id, + schedule.vehicle_id, + schedule.category, + schedule.subtypes, + schedule.interval_months ?? null, + schedule.interval_miles ?? null, + schedule.last_service_date ?? null, + schedule.last_service_mileage ?? null, + schedule.next_due_date ?? null, + schedule.next_due_mileage ?? null, + schedule.is_active, + ] + ); + return res.rows[0] as MaintenanceSchedule; + } + + async findScheduleById(id: string, userId: string): Promise { + const res = await this.db.query( + `SELECT * FROM maintenance_schedules WHERE id = $1 AND user_id = $2`, + [id, userId] + ); + return res.rows[0] || null; + } + + async findSchedulesByVehicleId(vehicleId: string, userId: string): Promise { + const res = await this.db.query( + `SELECT * FROM maintenance_schedules WHERE vehicle_id = $1 AND user_id = $2 ORDER BY created_at DESC`, + [vehicleId, userId] + ); + return res.rows as MaintenanceSchedule[]; + } + + async findActiveSchedulesByVehicleId(vehicleId: string, userId: string): Promise { + const res = await this.db.query( + `SELECT * FROM maintenance_schedules WHERE vehicle_id = $1 AND user_id = $2 AND is_active = true ORDER BY created_at DESC`, + [vehicleId, userId] + ); + return res.rows as MaintenanceSchedule[]; + } + + async updateSchedule( + id: string, + userId: string, + patch: Partial> + ): Promise { + const fields: string[] = []; + const params: any[] = []; + let i = 1; + + if (patch.category !== undefined) { + fields.push(`category = $${i++}`); + params.push(patch.category); + } + if (patch.subtypes !== undefined) { + fields.push(`subtypes = $${i++}::text[]`); + params.push(patch.subtypes); + } + if (patch.interval_months !== undefined) { + fields.push(`interval_months = $${i++}`); + params.push(patch.interval_months); + } + if (patch.interval_miles !== undefined) { + fields.push(`interval_miles = $${i++}`); + params.push(patch.interval_miles); + } + if (patch.last_service_date !== undefined) { + fields.push(`last_service_date = $${i++}`); + params.push(patch.last_service_date); + } + if (patch.last_service_mileage !== undefined) { + fields.push(`last_service_mileage = $${i++}`); + params.push(patch.last_service_mileage); + } + if (patch.next_due_date !== undefined) { + fields.push(`next_due_date = $${i++}`); + params.push(patch.next_due_date); + } + if (patch.next_due_mileage !== undefined) { + fields.push(`next_due_mileage = $${i++}`); + params.push(patch.next_due_mileage); + } + if (patch.is_active !== undefined) { + fields.push(`is_active = $${i++}`); + params.push(patch.is_active); + } + + if (!fields.length) return this.findScheduleById(id, userId); + + params.push(id, userId); + const sql = `UPDATE maintenance_schedules SET ${fields.join(', ')} WHERE id = $${i++} AND user_id = $${i++} RETURNING *`; + const res = await this.db.query(sql, params); + return res.rows[0] || null; + } + + async deleteSchedule(id: string, userId: string): Promise { + await this.db.query( + `DELETE FROM maintenance_schedules WHERE id = $1 AND user_id = $2`, + [id, userId] + ); + } +} diff --git a/backend/src/features/maintenance/domain/maintenance.service.ts b/backend/src/features/maintenance/domain/maintenance.service.ts new file mode 100644 index 0000000..4dd43ef --- /dev/null +++ b/backend/src/features/maintenance/domain/maintenance.service.ts @@ -0,0 +1,256 @@ +import { randomUUID } from 'crypto'; +import type { + CreateMaintenanceRecordRequest, + UpdateMaintenanceRecordRequest, + CreateScheduleRequest, + UpdateScheduleRequest, + MaintenanceRecord, + MaintenanceSchedule, + MaintenanceRecordResponse, + MaintenanceScheduleResponse, + MaintenanceCategory +} from './maintenance.types'; +import { validateSubtypes } from './maintenance.types'; +import { MaintenanceRepository } from '../data/maintenance.repository'; +import pool from '../../../core/config/database'; + +export class MaintenanceService { + private readonly repo = new MaintenanceRepository(pool); + + async createRecord(userId: string, body: CreateMaintenanceRecordRequest): Promise { + await this.assertVehicleOwnership(userId, body.vehicle_id); + + if (!validateSubtypes(body.category, body.subtypes)) { + const err: any = new Error('Invalid subtypes for selected category'); + err.statusCode = 400; + throw err; + } + + const id = randomUUID(); + return this.repo.insertRecord({ + id, + user_id: userId, + vehicle_id: body.vehicle_id, + category: body.category, + subtypes: body.subtypes, + date: body.date, + odometer_reading: body.odometer_reading, + cost: body.cost, + shop_name: body.shop_name, + notes: body.notes, + }); + } + + async getRecord(userId: string, id: string): Promise { + const record = await this.repo.findRecordById(id, userId); + if (!record) return null; + return this.toRecordResponse(record); + } + + async getRecords(userId: string, filters?: { vehicleId?: string; category?: MaintenanceCategory }): Promise { + const records = await this.repo.findRecordsByUserId(userId, filters); + return records.map(r => this.toRecordResponse(r)); + } + + async getRecordsByVehicle(userId: string, vehicleId: string): Promise { + const records = await this.repo.findRecordsByVehicleId(vehicleId, userId); + return records.map(r => this.toRecordResponse(r)); + } + + async updateRecord(userId: string, id: string, patch: UpdateMaintenanceRecordRequest): Promise { + const existing = await this.repo.findRecordById(id, userId); + if (!existing) return null; + + if (patch.category || patch.subtypes) { + const category = patch.category || existing.category; + const subtypes = patch.subtypes || existing.subtypes; + if (!validateSubtypes(category, subtypes)) { + const err: any = new Error('Invalid subtypes for selected category'); + err.statusCode = 400; + throw err; + } + } + + // Convert nulls to undefined for repository compatibility + const cleanPatch = Object.fromEntries( + Object.entries(patch).map(([k, v]) => [k, v === null ? undefined : v]) + ) as Partial>; + + const updated = await this.repo.updateRecord(id, userId, cleanPatch); + if (!updated) return null; + return this.toRecordResponse(updated); + } + + async deleteRecord(userId: string, id: string): Promise { + await this.repo.deleteRecord(id, userId); + } + + async createSchedule(userId: string, body: CreateScheduleRequest): Promise { + await this.assertVehicleOwnership(userId, body.vehicle_id); + + if (!validateSubtypes(body.category, body.subtypes)) { + const err: any = new Error('Invalid subtypes for selected category'); + err.statusCode = 400; + throw err; + } + + if (!body.interval_months && !body.interval_miles) { + const err: any = new Error('At least one interval (months or miles) is required'); + err.statusCode = 400; + throw err; + } + + const id = randomUUID(); + return this.repo.insertSchedule({ + id, + user_id: userId, + vehicle_id: body.vehicle_id, + category: body.category, + subtypes: body.subtypes, + interval_months: body.interval_months, + interval_miles: body.interval_miles, + is_active: true, + }); + } + + async getSchedules(userId: string, filters?: { vehicleId?: string }): Promise { + let schedules: MaintenanceSchedule[]; + if (filters?.vehicleId) { + schedules = await this.repo.findSchedulesByVehicleId(filters.vehicleId, userId); + } else { + schedules = await this.repo.findSchedulesByVehicleId('', userId); + } + return schedules.map(s => this.toScheduleResponse(s)); + } + + async getSchedulesByVehicle(userId: string, vehicleId: string): Promise { + const schedules = await this.repo.findSchedulesByVehicleId(vehicleId, userId); + return schedules.map(s => this.toScheduleResponse(s)); + } + + async updateSchedule(userId: string, id: string, patch: UpdateScheduleRequest): Promise { + const existing = await this.repo.findScheduleById(id, userId); + if (!existing) return null; + + if (patch.category || patch.subtypes) { + const category = patch.category || existing.category; + const subtypes = patch.subtypes || existing.subtypes; + if (!validateSubtypes(category, subtypes)) { + const err: any = new Error('Invalid subtypes for selected category'); + err.statusCode = 400; + throw err; + } + } + + const needsRecalculation = + patch.interval_months !== undefined || + patch.interval_miles !== undefined; + + let patchWithRecalc: any = { ...patch }; + if (needsRecalculation) { + const nextDue = this.calculateNextDue({ + last_service_date: existing.last_service_date, + last_service_mileage: existing.last_service_mileage, + interval_months: patch.interval_months ?? existing.interval_months, + interval_miles: patch.interval_miles ?? existing.interval_miles, + }); + patchWithRecalc.next_due_date = nextDue.next_due_date ?? undefined; + patchWithRecalc.next_due_mileage = nextDue.next_due_mileage ?? undefined; + } + + // Convert nulls to undefined for repository compatibility + const cleanPatch = Object.fromEntries( + Object.entries(patchWithRecalc).map(([k, v]) => [k, v === null ? undefined : v]) + ) as Partial>; + + const updated = await this.repo.updateSchedule(id, userId, cleanPatch); + if (!updated) return null; + return this.toScheduleResponse(updated); + } + + async deleteSchedule(userId: string, id: string): Promise { + await this.repo.deleteSchedule(id, userId); + } + + async getUpcomingMaintenance(userId: string, vehicleId: string, currentMileage?: number): Promise { + const schedules = await this.repo.findActiveSchedulesByVehicleId(vehicleId, userId); + const today = new Date().toISOString().split('T')[0]; + + return schedules + .map(s => this.toScheduleResponse(s, today, currentMileage)) + .filter(s => s.is_due_soon || s.is_overdue); + } + + private async assertVehicleOwnership(userId: string, vehicleId: string) { + const res = await pool.query('SELECT id FROM vehicles WHERE id = $1 AND user_id = $2', [vehicleId, userId]); + if (!res.rows[0]) { + const err: any = new Error('Vehicle not found or not owned by user'); + err.statusCode = 403; + throw err; + } + } + + private calculateNextDue(schedule: { + last_service_date?: string | null; + last_service_mileage?: number | null; + interval_months?: number | null; + interval_miles?: number | null; + }): { next_due_date: string | null; next_due_mileage: number | null } { + let next_due_date: string | null = null; + let next_due_mileage: number | null = null; + + if (schedule.last_service_date && schedule.interval_months) { + const lastDate = new Date(schedule.last_service_date); + const nextDate = new Date(lastDate); + nextDate.setMonth(nextDate.getMonth() + schedule.interval_months); + next_due_date = nextDate.toISOString().split('T')[0]; + } + + if (schedule.last_service_mileage !== null && schedule.last_service_mileage !== undefined && schedule.interval_miles) { + next_due_mileage = schedule.last_service_mileage + schedule.interval_miles; + } + + return { next_due_date, next_due_mileage }; + } + + private toRecordResponse(record: MaintenanceRecord): MaintenanceRecordResponse { + return { + ...record, + subtype_count: record.subtypes.length, + }; + } + + private toScheduleResponse(schedule: MaintenanceSchedule, today?: string, currentMileage?: number): MaintenanceScheduleResponse { + const todayStr = today || new Date().toISOString().split('T')[0]; + let is_due_soon = false; + let is_overdue = false; + + if (schedule.next_due_date) { + const nextDue = new Date(schedule.next_due_date); + const todayDate = new Date(todayStr); + const daysUntilDue = Math.floor((nextDue.getTime() - todayDate.getTime()) / (1000 * 60 * 60 * 24)); + + if (daysUntilDue < 0) { + is_overdue = true; + } else if (daysUntilDue <= 30) { + is_due_soon = true; + } + } + + if (currentMileage !== undefined && schedule.next_due_mileage !== null && schedule.next_due_mileage !== undefined) { + const milesUntilDue = schedule.next_due_mileage - currentMileage; + if (milesUntilDue < 0) { + is_overdue = true; + } else if (milesUntilDue <= 500) { + is_due_soon = true; + } + } + + return { + ...schedule, + subtype_count: schedule.subtypes.length, + is_due_soon, + is_overdue, + }; + } +} diff --git a/backend/src/features/maintenance/domain/maintenance.types.ts b/backend/src/features/maintenance/domain/maintenance.types.ts new file mode 100644 index 0000000..ea1830c --- /dev/null +++ b/backend/src/features/maintenance/domain/maintenance.types.ts @@ -0,0 +1,167 @@ +/** + * @ai-summary Type definitions for maintenance feature + * @ai-context Supports three categories with specific subtypes, multiple selections allowed + */ + +import { z } from 'zod'; + +// Category types +export type MaintenanceCategory = 'routine_maintenance' | 'repair' | 'performance_upgrade'; + +// Subtype definitions (constants for validation) +export const ROUTINE_MAINTENANCE_SUBTYPES = [ + 'Accelerator Pedal', + 'Air Filter Element', + 'Brakes and Traction Control', + 'Cabin Air Filter / Purifier', + 'Coolant', + 'Doors', + 'Drive Belt', + 'Engine Oil', + 'Evaporative Emissions System', + 'Exhaust System', + 'Fluid - A/T', + 'Fluid - Differential', + 'Fluid - M/T', + 'Fluid Filter - A/T', + 'Fluids', + 'Fuel Delivery and Air Induction', + 'Hood Shock / Support', + 'Neutral Safety Switch', + 'Parking Brake System', + 'Restraints and Safety Systems', + 'Shift Interlock, A/T', + 'Spark Plug', + 'Steering and Suspension', + 'Tires', + 'Trunk / Liftgate Shock / Support', + 'Washer Fluid', + 'Wiper Blade' +] as const; + +export const REPAIR_SUBTYPES = [ + 'Engine', + 'Transmission', + 'Drivetrain', + 'Exterior', + 'Interior' +] as const; + +export const PERFORMANCE_UPGRADE_SUBTYPES = [ + 'Engine', + 'Drivetrain', + 'Suspension', + 'Wheels/Tires', + 'Exterior' +] as const; + +// Database record types +export interface MaintenanceRecord { + id: string; + user_id: string; + vehicle_id: string; + category: MaintenanceCategory; + subtypes: string[]; + date: string; + odometer_reading?: number; + cost?: number; + shop_name?: string; + notes?: string; + created_at: string; + updated_at: string; +} + +export interface MaintenanceSchedule { + id: string; + user_id: string; + vehicle_id: string; + category: MaintenanceCategory; + subtypes: string[]; + interval_months?: number; + interval_miles?: number; + last_service_date?: string; + last_service_mileage?: number; + next_due_date?: string; + next_due_mileage?: number; + is_active: boolean; + created_at: string; + updated_at: string; +} + +// Zod schemas for validation +export const MaintenanceCategorySchema = z.enum(['routine_maintenance', 'repair', 'performance_upgrade']); + +export const CreateMaintenanceRecordSchema = z.object({ + vehicle_id: z.string().uuid(), + category: MaintenanceCategorySchema, + subtypes: z.array(z.string()).min(1), + date: z.string(), + odometer_reading: z.number().int().positive().optional(), + cost: z.number().positive().optional(), + shop_name: z.string().max(200).optional(), + notes: z.string().max(10000).optional(), +}); +export type CreateMaintenanceRecordRequest = z.infer; + +export const UpdateMaintenanceRecordSchema = z.object({ + category: MaintenanceCategorySchema.optional(), + subtypes: z.array(z.string()).min(1).optional(), + date: z.string().optional(), + odometer_reading: z.number().int().positive().nullable().optional(), + cost: z.number().positive().nullable().optional(), + shop_name: z.string().max(200).nullable().optional(), + notes: z.string().max(10000).nullable().optional(), +}); +export type UpdateMaintenanceRecordRequest = z.infer; + +export const CreateScheduleSchema = z.object({ + vehicle_id: z.string().uuid(), + category: MaintenanceCategorySchema, + subtypes: z.array(z.string()).min(1), + interval_months: z.number().int().positive().optional(), + interval_miles: z.number().int().positive().optional(), +}); +export type CreateScheduleRequest = z.infer; + +export const UpdateScheduleSchema = z.object({ + category: MaintenanceCategorySchema.optional(), + subtypes: z.array(z.string()).min(1).optional(), + interval_months: z.number().int().positive().nullable().optional(), + interval_miles: z.number().int().positive().nullable().optional(), + is_active: z.boolean().optional(), +}); +export type UpdateScheduleRequest = z.infer; + +// Response types +export interface MaintenanceRecordResponse extends MaintenanceRecord { + subtype_count: number; +} + +export interface MaintenanceScheduleResponse extends MaintenanceSchedule { + subtype_count: number; + is_due_soon?: boolean; + is_overdue?: boolean; +} + +// Validation helpers +export function getSubtypesForCategory(category: MaintenanceCategory): readonly string[] { + switch (category) { + case 'routine_maintenance': return ROUTINE_MAINTENANCE_SUBTYPES; + case 'repair': return REPAIR_SUBTYPES; + case 'performance_upgrade': return PERFORMANCE_UPGRADE_SUBTYPES; + } +} + +export function validateSubtypes(category: MaintenanceCategory, subtypes: string[]): boolean { + if (!subtypes || subtypes.length === 0) return false; + const validSubtypes = getSubtypesForCategory(category); + return subtypes.every(st => validSubtypes.includes(st as any)); +} + +export function getCategoryDisplayName(category: MaintenanceCategory): string { + switch (category) { + case 'routine_maintenance': return 'Routine Maintenance'; + case 'repair': return 'Repair'; + case 'performance_upgrade': return 'Performance Upgrade'; + } +} diff --git a/backend/src/features/maintenance/index.ts b/backend/src/features/maintenance/index.ts index de1e17d..027fbea 100644 --- a/backend/src/features/maintenance/index.ts +++ b/backend/src/features/maintenance/index.ts @@ -1,14 +1,7 @@ /** * @ai-summary Public API for maintenance feature capsule * @ai-note This is the ONLY file other features should import from - * @ai-status Scaffolded - implementation pending */ -// TODO: Implement maintenance service and types -// Currently scaffolded feature - no exports until implementation is complete - -// Placeholder to prevent build errors -export const MaintenanceFeature = { - status: 'scaffolded', - message: 'Maintenance feature not yet implemented' -} as const; +export { maintenanceRoutes } from './api/maintenance.routes'; +export * from './domain/maintenance.types'; diff --git a/backend/src/features/maintenance/migrations/002_recreate_maintenance_tables.sql b/backend/src/features/maintenance/migrations/002_recreate_maintenance_tables.sql new file mode 100644 index 0000000..af325b8 --- /dev/null +++ b/backend/src/features/maintenance/migrations/002_recreate_maintenance_tables.sql @@ -0,0 +1,80 @@ +-- Drop existing tables (clean slate) +DROP TABLE IF EXISTS maintenance_schedules CASCADE; +DROP TABLE IF EXISTS maintenance_logs CASCADE; + +-- Create maintenance_records table +CREATE TABLE maintenance_records ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(255) NOT NULL, + vehicle_id UUID NOT NULL, + category VARCHAR(50) NOT NULL, + subtypes TEXT[] NOT NULL, + date DATE NOT NULL, + odometer_reading INTEGER, + cost DECIMAL(10, 2), + shop_name VARCHAR(200), + notes TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT fk_maintenance_vehicle + FOREIGN KEY (vehicle_id) + REFERENCES vehicles(id) + ON DELETE CASCADE, + + CONSTRAINT check_category + CHECK (category IN ('routine_maintenance', 'repair', 'performance_upgrade')), + + CONSTRAINT check_subtypes_not_empty + CHECK (array_length(subtypes, 1) > 0) +); + +-- Create maintenance_schedules table +CREATE TABLE maintenance_schedules ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(255) NOT NULL, + vehicle_id UUID NOT NULL, + category VARCHAR(50) NOT NULL, + subtypes TEXT[] NOT NULL, + interval_months INTEGER, + interval_miles INTEGER, + last_service_date DATE, + last_service_mileage INTEGER, + next_due_date DATE, + next_due_mileage INTEGER, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT fk_schedule_vehicle + FOREIGN KEY (vehicle_id) + REFERENCES vehicles(id) + ON DELETE CASCADE, + + CONSTRAINT check_schedule_category + CHECK (category IN ('routine_maintenance', 'repair', 'performance_upgrade')) +); + +-- Indexes for performance +CREATE INDEX idx_maintenance_records_user_id ON maintenance_records(user_id); +CREATE INDEX idx_maintenance_records_vehicle_id ON maintenance_records(vehicle_id); +CREATE INDEX idx_maintenance_records_date ON maintenance_records(date DESC); +CREATE INDEX idx_maintenance_records_category ON maintenance_records(category); + +CREATE INDEX idx_maintenance_schedules_user_id ON maintenance_schedules(user_id); +CREATE INDEX idx_maintenance_schedules_vehicle_id ON maintenance_schedules(vehicle_id); +CREATE INDEX idx_maintenance_schedules_next_due_date ON maintenance_schedules(next_due_date); +CREATE INDEX idx_maintenance_schedules_active ON maintenance_schedules(is_active) WHERE is_active = true; + +-- Triggers for updated_at +DROP TRIGGER IF EXISTS update_maintenance_records_updated_at ON maintenance_records; +CREATE TRIGGER update_maintenance_records_updated_at + BEFORE UPDATE ON maintenance_records + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +DROP TRIGGER IF EXISTS update_maintenance_schedules_updated_at ON maintenance_schedules; +CREATE TRIGGER update_maintenance_schedules_updated_at + BEFORE UPDATE ON maintenance_schedules + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); diff --git a/docs/DATABASE-SCHEMA.md b/docs/DATABASE-SCHEMA.md index 1fa480a..bfd4f21 100644 --- a/docs/DATABASE-SCHEMA.md +++ b/docs/DATABASE-SCHEMA.md @@ -12,8 +12,10 @@ Complete database schema for MotoVaultPro Modified Feature Capsule architecture. ### Migration Tracking - **Table**: `_migrations` -- **Purpose**: Created by `backend/src/_system/migrations/run-all.ts` (not yet used for skipping executed files) -- **Note**: Some SQL files use `IF NOT EXISTS`. Re-running all migrations may fail on indexes without `IF NOT EXISTS`. +- **Purpose**: Tracks executed migration files to prevent re-execution +- **Behavior**: Migration system is **idempotent at the file level** - will skip already executed files +- **SQL Statement Level**: Individual SQL statements within files may fail on re-run if they don't use `IF NOT EXISTS` clauses +- **Safety**: Safe to re-run the migration system; unsafe to manually re-run individual SQL files ## Core Tables @@ -193,11 +195,16 @@ Single-feature migration is not implemented yet. ## Database Connection ### Development (Docker) -- **Host**: postgres (container name) -- **Port**: 5432 +- **Host**: admin-postgres (container name) +- **Port**: 5432 (internal), 5432 (external) - **Database**: motovaultpro - **User**: postgres -- **Password**: localdev123 +- **Password**: Loaded from secrets file `/run/secrets/postgres-password` + +**Password Management**: All database passwords are managed via Docker secrets, mounted from host files: +- Application DB: `./secrets/app/postgres-password.txt` +- Platform DB: `./secrets/platform/platform-db-password.txt` +- Vehicles DB: `./secrets/platform/vehicles-db-password.txt` ### Connection Pool - **Implementation**: pg (node-postgres) diff --git a/docs/DOCUMENTATION-AUDIT-REPORT.md b/docs/DOCUMENTATION-AUDIT-REPORT.md deleted file mode 100644 index 7ad0d89..0000000 --- a/docs/DOCUMENTATION-AUDIT-REPORT.md +++ /dev/null @@ -1,221 +0,0 @@ -# MotoVaultPro Documentation Audit Report - -**Date**: 2025-09-28 -**Auditor**: Claude AI Assistant -**Scope**: Technical accuracy, consistency, and alignment with actual codebase architecture - -## Executive Summary - -I have conducted a comprehensive audit of the MotoVaultPro project documentation and identified **14 significant issues** across 4 priority levels. The audit revealed critical infrastructure mismatches, architectural contradictions, misleading security claims, and inconsistent testing information that could cause system failures or developer confusion. - -## Audit Methodology - -### Research Scope -- All major documentation files (PLATFORM-SERVICES.md, TESTING.md, DATABASE-SCHEMA.md, SECURITY.md, VEHICLES-API.md, README files) -- Docker configuration and container architecture -- Migration system and database schemas -- Makefile commands and actual implementations -- Package.json dependencies and scripts -- Actual API endpoints and service implementations -- Testing structure and coverage claims -- Authentication and security implementations - -### Evidence Standards -- Every finding includes specific file references and line numbers -- Cross-referenced documentation claims with actual codebase implementation -- Prioritized issues by potential impact on system functionality -- Provided actionable recommendations for each issue - -## Audit Findings - -### CRITICAL Priority Issues (Will Cause Failures) - -#### 1. Platform Services Port Mismatch -**FILE**: `docs/PLATFORM-SERVICES.md` -**SECTION**: Line 78 - MVP Platform Tenants Service -**ISSUE TYPE**: Inaccuracy -**DESCRIPTION**: Claims tenants API runs on "port 8001" -**PROBLEM**: docker-compose.yml shows both platform services on port 8000, no service on 8001 -**EVIDENCE**: PLATFORM-SERVICES.md:78 vs docker-compose.yml:lines 72-120 -**RECOMMENDATION**: Correct documentation to show port 8000 for both services - -#### 2. Database Password Contradiction -**FILE**: `docs/DATABASE-SCHEMA.md` -**SECTION**: Line 200 - Database Connection -**ISSUE TYPE**: Inaccuracy -**DESCRIPTION**: Claims development password is "localdev123" -**PROBLEM**: docker-compose.yml uses secrets files, not hardcoded passwords -**EVIDENCE**: DATABASE-SCHEMA.md:200 vs docker-compose.yml:282-287 -**RECOMMENDATION**: Update to reflect secrets-based credential management - -#### 3. Migration Idempotency Contradiction -**FILE**: `docs/DATABASE-SCHEMA.md` -**SECTION**: Lines 15-16 - Migration Tracking -**ISSUE TYPE**: Contradiction -**DESCRIPTION**: Claims migrations are tracked as "idempotent" but warns "may fail on indexes without IF NOT EXISTS" -**PROBLEM**: Cannot be both idempotent and prone to failure -**EVIDENCE**: docs/VEHICLES-API.md:84 claims "idempotent" vs DATABASE-SCHEMA.md:16 warns of failures -**RECOMMENDATION**: Clarify actual migration behavior and safety guarantees - -#### 4. Health Check Endpoint Mismatch -**FILE**: `docs/PLATFORM-SERVICES.md` -**SECTION**: Lines 243-244 - Health Checks -**ISSUE TYPE**: Inaccuracy -**DESCRIPTION**: Claims health endpoints at "localhost:8001/health" -**PROBLEM**: No service running on port 8001 based on docker-compose.yml -**EVIDENCE**: PLATFORM-SERVICES.md:244 vs docker-compose.yml service definitions -**RECOMMENDATION**: Correct health check URLs to match actual service ports - -### HIGH Priority Issues (Significant Confusion) - -#### 5. Platform Service Independence Claims -**FILE**: `docs/PLATFORM-SERVICES.md` -**SECTION**: Line 98 - Service Communication -**ISSUE TYPE**: Misleading -**DESCRIPTION**: Claims platform services are "completely independent" -**PROBLEM**: Services share config files (./config/shared/production.yml) and secret directories -**EVIDENCE**: PLATFORM-SERVICES.md:98 vs docker-compose.yml:90,137,184 -**RECOMMENDATION**: Clarify actual dependency relationships and shared resources - -#### 6. Test Coverage Misrepresentation -**FILE**: `docs/README.md` -**SECTION**: Line 24 - Feature test coverage -**ISSUE TYPE**: Misleading -**DESCRIPTION**: Claims "vehicles has full coverage" -**PROBLEM**: Only 7 test files exist across all features, minimal actual coverage -**EVIDENCE**: docs/README.md:24 vs find results showing 7 total .test.ts files -**RECOMMENDATION**: Provide accurate coverage metrics or remove coverage claims - -#### 7. API Script Reference Error -**FILE**: `backend/README.md` -**SECTION**: Line 46 - Test Commands -**ISSUE TYPE**: Inaccuracy -**DESCRIPTION**: Documents command syntax as "--feature=vehicles" with equals sign -**PROBLEM**: Actual npm script uses positional argument ${npm_config_feature} -**EVIDENCE**: backend/README.md:46 vs backend/package.json:12 script definition -**RECOMMENDATION**: Correct command syntax documentation - -#### 8. Cache TTL Value Conflicts -**FILE**: `docs/VEHICLES-API.md` vs `mvp-platform-services/vehicles/api/config.py` -**SECTION**: Line 41 vs Line 35 -**ISSUE TYPE**: Contradiction -**DESCRIPTION**: Documentation claims "6 hours" default TTL, code shows 3600 (1 hour) -**PROBLEM**: Inconsistent caching behavior documentation -**EVIDENCE**: VEHICLES-API.md:41 "6 hours" vs config.py:35 "3600 (1 hour default)" -**RECOMMENDATION**: Synchronize TTL values in documentation and code - -### MEDIUM Priority Issues (Inconsistencies) - -#### 9. Architecture Pattern Confusion -**FILE**: `docs/PLATFORM-SERVICES.md` -**SECTION**: Multiple references to "4-tier isolation" -**ISSUE TYPE**: Unclear -**DESCRIPTION**: Claims "4-tier network isolation" but implementation details are unclear -**PROBLEM**: docker-compose.yml shows services sharing networks, not clear isolation -**EVIDENCE**: Makefile:57,146-149 mentions tiers vs actual network sharing in docker-compose.yml -**RECOMMENDATION**: Clarify actual network topology and isolation boundaries - -#### 10. Container Name Inconsistencies -**FILE**: Multiple documentation files -**SECTION**: Various service references -**ISSUE TYPE**: Inaccuracy -**DESCRIPTION**: Documentation uses inconsistent container naming patterns -**PROBLEM**: Makes service discovery and debugging instructions unreliable -**EVIDENCE**: Mix of "admin-backend", "backend", "mvp-platform-*" naming across docs -**RECOMMENDATION**: Standardize container name references across all documentation - -#### 11. Authentication Method Confusion -**FILE**: `docs/SECURITY.md` vs `docs/PLATFORM-SERVICES.md` -**SECTION**: Authentication sections -**ISSUE TYPE**: Contradiction -**DESCRIPTION**: Mixed claims about JWT vs API key authentication -**PROBLEM**: Unclear which auth method applies where -**EVIDENCE**: SECURITY.md mentions Auth0 JWT, PLATFORM-SERVICES.md mentions API keys -**RECOMMENDATION**: Create clear authentication flow diagram showing all methods - -#### 12. Development Workflow Claims -**FILE**: `README.md` -**SECTION**: Line 7 - Docker-first requirements -**ISSUE TYPE**: Misleading -**DESCRIPTION**: Claims "production-only" development but allows development database access -**PROBLEM**: Contradicts stated "production-only" methodology -**EVIDENCE**: README.md:7 vs docker-compose.yml:291,310,360,378,422,440 (dev ports) -**RECOMMENDATION**: Clarify actual development vs production boundaries - -### LOW Priority Issues (Minor Issues) - -#### 13. Makefile Command Documentation Gaps -**FILE**: Multiple files referencing make commands -**SECTION**: Various command references -**ISSUE TYPE**: Unclear -**DESCRIPTION**: Some documented make commands have unclear purposes -**PROBLEM**: Developers may use wrong commands for tasks -**EVIDENCE**: Makefile contains commands not well documented in usage guides -**RECOMMENDATION**: Add comprehensive command documentation - -#### 14. Feature Documentation Inconsistency -**FILE**: `backend/src/features/*/README.md` files -**SECTION**: Feature-specific documentation -**ISSUE TYPE**: Inconsistency -**DESCRIPTION**: Different documentation standards across features -**PROBLEM**: Makes onboarding and maintenance inconsistent -**EVIDENCE**: Varying detail levels and structures across feature README files -**RECOMMENDATION**: Standardize feature documentation templates - -## Analysis Summary - -### Issue Type Distribution -- **Inaccuracies**: 6 issues (43% - ports, passwords, commands, endpoints) -- **Contradictions**: 4 issues (29% - idempotency, TTL, authentication, independence) -- **Misleading**: 3 issues (21% - coverage, independence, development methodology) -- **Unclear**: 1 issue (7% - network architecture) - -### Priority Distribution -- **CRITICAL**: 4 issues (29% - will cause failures) -- **HIGH**: 4 issues (29% - significant confusion) -- **MEDIUM**: 4 issues (29% - inconsistencies) -- **LOW**: 2 issues (14% - minor issues) - -### Root Causes Analysis -1. **Documentation Drift**: Code evolved but documentation wasn't updated -2. **Multiple Sources of Truth**: Same information documented differently in multiple places -3. **Aspirational Documentation**: Documents intended behavior rather than actual implementation -4. **Incomplete Implementation**: Features documented before full implementation - -## Recommendations - -### Immediate Actions (Critical Issues) -1. **Fix Port Mismatches**: Update all port references to match docker-compose.yml -2. **Correct Database Documentation**: Reflect actual secrets-based credential management -3. **Clarify Migration Behavior**: Document actual safety guarantees and failure modes -4. **Fix Health Check URLs**: Ensure all health check examples use correct endpoints - -### Short-term Actions (High Priority) -1. **Service Dependency Audit**: Document actual shared resources and dependencies -2. **Test Coverage Analysis**: Conduct real coverage analysis and update claims -3. **Command Syntax Verification**: Validate all documented commands and examples -4. **Cache Configuration Sync**: Align all TTL documentation with actual values - -### Long-term Actions (Medium/Low Priority) -1. **Architecture Documentation Overhaul**: Create accurate diagrams of actual vs claimed isolation -2. **Naming Convention Standardization**: Establish and enforce consistent naming across docs -3. **Authentication Flow Documentation**: Develop comprehensive auth flow diagrams -4. **Documentation Standards**: Establish review processes and templates - -### Process Improvements -1. **Documentation Review Process**: Require documentation updates with code changes -2. **Automated Validation**: Create scripts to validate documented commands and endpoints -3. **Single Source of Truth**: Identify authoritative sources for each type of information -4. **Regular Audits**: Schedule periodic documentation accuracy reviews - -## Conclusion - -This audit reveals that while the MotoVaultPro project has extensive documentation, there are significant gaps between documented behavior and actual implementation. These issues range from critical infrastructure mismatches that will cause system failures to misleading architectural claims that could confuse developers and AI agents. - -The 14 identified issues provide a clear roadmap for bringing documentation in line with reality. Addressing the 4 critical issues should be the immediate priority, as these will prevent system failures when following documented procedures. - -The findings suggest implementing stronger processes to keep documentation synchronized with code changes, particularly around infrastructure configuration, API endpoints, and architectural claims. - ---- - -**Audit Completion**: All major documentation files reviewed and cross-referenced with actual codebase implementation. Evidence-based findings with specific file references and actionable recommendations provided. \ No newline at end of file diff --git a/docs/MAINTENANCE-FEATURE-PLAN.md b/docs/MAINTENANCE-FEATURE-PLAN.md new file mode 100644 index 0000000..b677f24 --- /dev/null +++ b/docs/MAINTENANCE-FEATURE-PLAN.md @@ -0,0 +1,1037 @@ +# Maintenance Feature Implementation Plan + +## Project Context + +MotoVaultPro is a hybrid platform using: +- **Architecture**: Modular monolith with feature capsules +- **Backend**: Fastify + PostgreSQL + Redis +- **Frontend**: React + TypeScript + Material-UI +- **Development**: Docker-first, production-only testing +- **Requirements**: Mobile + Desktop support for ALL features + +## Feature Overview + +Implement comprehensive maintenance tracking with three main categories: +1. **Routine Maintenance** - Regular service items (27 subtypes) +2. **Repair** - Fix/repair work (5 subtypes) +3. **Performance Upgrade** - Enhancements (5 subtypes) + +### Key Capabilities +- Track completed maintenance (records) +- Schedule recurring maintenance (schedules) +- Calculate next due dates (date-based and/or mileage-based) +- View upcoming/overdue maintenance +- Support multiple subtypes per record (checkboxes, not single select) + +### Display Format +- **List View**: "Category (count)" e.g., "Routine Maintenance (3)" +- **Detail View**: Show all selected subtypes with full details + +## Database Schema + +### Tables to Create + +Drop existing maintenance tables (001_create_maintenance_tables.sql) and create new schema. + +**Migration: `backend/src/features/maintenance/migrations/002_recreate_maintenance_tables.sql`** + +```sql +-- Drop existing tables (clean slate) +DROP TABLE IF EXISTS maintenance_schedules CASCADE; +DROP TABLE IF EXISTS maintenance_logs CASCADE; + +-- Create maintenance_records table +CREATE TABLE maintenance_records ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(255) NOT NULL, + vehicle_id UUID NOT NULL, + category VARCHAR(50) NOT NULL, -- 'routine_maintenance', 'repair', 'performance_upgrade' + subtypes TEXT[] NOT NULL, -- PostgreSQL array of selected subtypes + date DATE NOT NULL, + odometer_reading INTEGER, + cost DECIMAL(10, 2), + shop_name VARCHAR(200), + notes TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT fk_maintenance_vehicle + FOREIGN KEY (vehicle_id) + REFERENCES vehicles(id) + ON DELETE CASCADE, + + CONSTRAINT check_category + CHECK (category IN ('routine_maintenance', 'repair', 'performance_upgrade')), + + CONSTRAINT check_subtypes_not_empty + CHECK (array_length(subtypes, 1) > 0) +); + +-- Create maintenance_schedules table +CREATE TABLE maintenance_schedules ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(255) NOT NULL, + vehicle_id UUID NOT NULL, + category VARCHAR(50) NOT NULL, + subtypes TEXT[] NOT NULL, + interval_months INTEGER, + interval_miles INTEGER, + last_service_date DATE, + last_service_mileage INTEGER, + next_due_date DATE, + next_due_mileage INTEGER, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT fk_schedule_vehicle + FOREIGN KEY (vehicle_id) + REFERENCES vehicles(id) + ON DELETE CASCADE, + + CONSTRAINT check_schedule_category + CHECK (category IN ('routine_maintenance', 'repair', 'performance_upgrade')) +); + +-- Indexes for performance +CREATE INDEX idx_maintenance_records_user_id ON maintenance_records(user_id); +CREATE INDEX idx_maintenance_records_vehicle_id ON maintenance_records(vehicle_id); +CREATE INDEX idx_maintenance_records_date ON maintenance_records(date DESC); +CREATE INDEX idx_maintenance_records_category ON maintenance_records(category); + +CREATE INDEX idx_maintenance_schedules_user_id ON maintenance_schedules(user_id); +CREATE INDEX idx_maintenance_schedules_vehicle_id ON maintenance_schedules(vehicle_id); +CREATE INDEX idx_maintenance_schedules_next_due_date ON maintenance_schedules(next_due_date); +CREATE INDEX idx_maintenance_schedules_active ON maintenance_schedules(is_active) WHERE is_active = true; + +-- Triggers for updated_at +DROP TRIGGER IF EXISTS update_maintenance_records_updated_at ON maintenance_records; +CREATE TRIGGER update_maintenance_records_updated_at + BEFORE UPDATE ON maintenance_records + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +DROP TRIGGER IF EXISTS update_maintenance_schedules_updated_at ON maintenance_schedules; +CREATE TRIGGER update_maintenance_schedules_updated_at + BEFORE UPDATE ON maintenance_schedules + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); +``` + +## Category and Subtype Definitions + +### Routine Maintenance (27 subtypes) +``` +Accelerator Pedal +Air Filter Element +Brakes and Traction Control +Cabin Air Filter / Purifier +Coolant +Doors +Drive Belt +Engine Oil +Evaporative Emissions System +Exhaust System +Fluid - A/T +Fluid - Differential +Fluid - M/T +Fluid Filter - A/T +Fluids +Fuel Delivery and Air Induction +Hood Shock / Support +Neutral Safety Switch +Parking Brake System +Restraints and Safety Systems +Shift Interlock, A/T +Spark Plug +Steering and Suspension +Tires +Trunk / Liftgate Shock / Support +Washer Fluid +Wiper Blade +``` + +### Repair (5 subtypes) +``` +Engine +Transmission +Drivetrain +Exterior +Interior +``` + +### Performance Upgrade (5 subtypes) +``` +Engine +Drivetrain +Suspension +Wheels/Tires +Exterior +``` + +## Backend Implementation + +### File Structure +``` +backend/src/features/maintenance/ +ā”œā”€ā”€ README.md # Feature documentation +ā”œā”€ā”€ index.ts # Public API exports +ā”œā”€ā”€ api/ +│ ā”œā”€ā”€ maintenance.routes.ts # Fastify routes +│ ā”œā”€ā”€ maintenance.controller.ts # HTTP handlers +│ └── maintenance.validation.ts # Request validation +ā”œā”€ā”€ domain/ +│ ā”œā”€ā”€ maintenance.types.ts # TypeScript types + constants +│ └── maintenance.service.ts # Business logic +ā”œā”€ā”€ data/ +│ └── maintenance.repository.ts # Database queries +ā”œā”€ā”€ migrations/ +│ └── 002_recreate_maintenance_tables.sql +└── tests/ + ā”œā”€ā”€ unit/ + │ └── maintenance.service.test.ts + └── integration/ + └── maintenance.integration.test.ts +``` + +### Phase 1: Domain Layer + +**File: `backend/src/features/maintenance/domain/maintenance.types.ts`** + +```typescript +/** + * @ai-summary Type definitions for maintenance feature + * @ai-context Supports three categories with specific subtypes, multiple selections allowed + */ + +// Category types +export type MaintenanceCategory = 'routine_maintenance' | 'repair' | 'performance_upgrade'; + +// Subtype definitions (constants for validation) +export const ROUTINE_MAINTENANCE_SUBTYPES = [ + 'Accelerator Pedal', + 'Air Filter Element', + 'Brakes and Traction Control', + 'Cabin Air Filter / Purifier', + 'Coolant', + 'Doors', + 'Drive Belt', + 'Engine Oil', + 'Evaporative Emissions System', + 'Exhaust System', + 'Fluid - A/T', + 'Fluid - Differential', + 'Fluid - M/T', + 'Fluid Filter - A/T', + 'Fluids', + 'Fuel Delivery and Air Induction', + 'Hood Shock / Support', + 'Neutral Safety Switch', + 'Parking Brake System', + 'Restraints and Safety Systems', + 'Shift Interlock, A/T', + 'Spark Plug', + 'Steering and Suspension', + 'Tires', + 'Trunk / Liftgate Shock / Support', + 'Washer Fluid', + 'Wiper Blade' +] as const; + +export const REPAIR_SUBTYPES = [ + 'Engine', + 'Transmission', + 'Drivetrain', + 'Exterior', + 'Interior' +] as const; + +export const PERFORMANCE_UPGRADE_SUBTYPES = [ + 'Engine', + 'Drivetrain', + 'Suspension', + 'Wheels/Tires', + 'Exterior' +] as const; + +// Database record types +export interface MaintenanceRecord { + id: string; + user_id: string; + vehicle_id: string; + category: MaintenanceCategory; + subtypes: string[]; + date: string; + odometer_reading?: number; + cost?: number; + shop_name?: string; + notes?: string; + created_at: string; + updated_at: string; +} + +export interface MaintenanceSchedule { + id: string; + user_id: string; + vehicle_id: string; + category: MaintenanceCategory; + subtypes: string[]; + interval_months?: number; + interval_miles?: number; + last_service_date?: string; + last_service_mileage?: number; + next_due_date?: string; + next_due_mileage?: number; + is_active: boolean; + created_at: string; + updated_at: string; +} + +// Request types +export interface CreateMaintenanceRecordRequest { + vehicle_id: string; + category: MaintenanceCategory; + subtypes: string[]; // Must have at least one + date: string; + odometer_reading?: number; + cost?: number; + shop_name?: string; + notes?: string; +} + +export interface UpdateMaintenanceRecordRequest { + category?: MaintenanceCategory; + subtypes?: string[]; + date?: string; + odometer_reading?: number; + cost?: number; + shop_name?: string; + notes?: string; +} + +export interface CreateScheduleRequest { + vehicle_id: string; + category: MaintenanceCategory; + subtypes: string[]; + interval_months?: number; + interval_miles?: number; +} + +export interface UpdateScheduleRequest { + category?: MaintenanceCategory; + subtypes?: string[]; + interval_months?: number; + interval_miles?: number; + is_active?: boolean; +} + +// Response types +export interface MaintenanceRecordResponse extends MaintenanceRecord { + subtype_count: number; // For list display: "Routine Maintenance (3)" +} + +export interface MaintenanceScheduleResponse extends MaintenanceSchedule { + subtype_count: number; + is_due_soon?: boolean; // Within 30 days or 500 miles + is_overdue?: boolean; +} + +// Validation helpers +export function getSubtypesForCategory(category: MaintenanceCategory): readonly string[] { + switch (category) { + case 'routine_maintenance': return ROUTINE_MAINTENANCE_SUBTYPES; + case 'repair': return REPAIR_SUBTYPES; + case 'performance_upgrade': return PERFORMANCE_UPGRADE_SUBTYPES; + } +} + +export function validateSubtypes(category: MaintenanceCategory, subtypes: string[]): boolean { + if (!subtypes || subtypes.length === 0) return false; + const validSubtypes = getSubtypesForCategory(category); + return subtypes.every(st => validSubtypes.includes(st as any)); +} + +export function getCategoryDisplayName(category: MaintenanceCategory): string { + switch (category) { + case 'routine_maintenance': return 'Routine Maintenance'; + case 'repair': return 'Repair'; + case 'performance_upgrade': return 'Performance Upgrade'; + } +} +``` + +**File: `backend/src/features/maintenance/domain/maintenance.service.ts`** + +Key methods to implement: +- `createRecord(data, userId)` - Validate vehicle ownership, validate subtypes match category, create record +- `getRecords(userId, filters?)` - Get user's records, apply filters (vehicleId, category, dateRange) +- `getRecord(id, userId)` - Get single record with ownership check +- `updateRecord(id, data, userId)` - Update with validation +- `deleteRecord(id, userId)` - Soft delete or hard delete +- `getRecordsByVehicle(vehicleId, userId)` - Vehicle-specific records + +- `createSchedule(data, userId)` - Create recurring schedule, calculate initial next_due +- `getSchedules(userId, filters?)` - Get schedules with filters +- `updateSchedule(id, data, userId)` - Update schedule, recalculate next_due if intervals change +- `deleteSchedule(id, userId)` - Remove schedule +- `getUpcomingMaintenance(vehicleId, userId)` - Get schedules that are due soon or overdue + +- `calculateNextDue(schedule, currentDate, currentMileage)` - Calculate next due date/mileage based on intervals + +**Cache Strategy:** +- Records: `maintenance:records:user:{userId}` - 5 min TTL +- Vehicle records: `maintenance:records:vehicle:{vehicleId}` - 5 min TTL +- Schedules: `maintenance:schedules:vehicle:{vehicleId}` - 5 min TTL +- Upcoming: `maintenance:upcoming:{vehicleId}` - 1 hour TTL + +### Phase 2: Data Layer + +**File: `backend/src/features/maintenance/data/maintenance.repository.ts`** + +Key methods (all use prepared statements, all filter by user_id): +- `insert(record)` - INSERT with PostgreSQL array for subtypes +- `findById(id, userId)` - SELECT with user_id check +- `findByUserId(userId)` - SELECT user's records +- `findByVehicleId(vehicleId, userId)` - SELECT vehicle records with ownership check +- `update(id, userId, data)` - UPDATE with user_id check +- `delete(id, userId)` - DELETE with user_id check + +- `insertSchedule(schedule)` - INSERT schedule +- `findSchedulesByVehicle(vehicleId, userId)` - SELECT vehicle schedules +- `updateSchedule(id, userId, data)` - UPDATE schedule +- `deleteSchedule(id, userId)` - DELETE schedule +- `findDueSchedules(vehicleId, userId, currentDate, currentMileage)` - Complex query for due/overdue + +**PostgreSQL Array Handling:** +```typescript +// Insert with array +await pool.query( + 'INSERT INTO maintenance_records (subtypes, ...) VALUES ($1, ...)', + [[subtype1, subtype2, subtype3], ...] +); + +// Query with array contains +await pool.query( + 'SELECT * FROM maintenance_records WHERE $1 = ANY(subtypes)', + [searchSubtype] +); +``` + +### Phase 3: API Layer + +**File: `backend/src/features/maintenance/api/maintenance.routes.ts`** + +```typescript +import { FastifyInstance } from 'fastify'; +import { MaintenanceController } from './maintenance.controller'; + +export async function maintenanceRoutes(app: FastifyInstance) { + const controller = new MaintenanceController(); + + // All routes require authentication + app.addHook('preHandler', app.authenticate); + + // Maintenance Records + app.post('/maintenance/records', controller.createRecord.bind(controller)); + app.get('/maintenance/records', controller.listRecords.bind(controller)); + app.get('/maintenance/records/:id', controller.getRecord.bind(controller)); + app.put('/maintenance/records/:id', controller.updateRecord.bind(controller)); + app.delete('/maintenance/records/:id', controller.deleteRecord.bind(controller)); + app.get('/maintenance/records/vehicle/:vehicleId', controller.getRecordsByVehicle.bind(controller)); + + // Maintenance Schedules + app.post('/maintenance/schedules', controller.createSchedule.bind(controller)); + app.get('/maintenance/schedules', controller.listSchedules.bind(controller)); + app.get('/maintenance/schedules/vehicle/:vehicleId', controller.getSchedulesByVehicle.bind(controller)); + app.put('/maintenance/schedules/:id', controller.updateSchedule.bind(controller)); + app.delete('/maintenance/schedules/:id', controller.deleteSchedule.bind(controller)); + + // Utility endpoints + app.get('/maintenance/upcoming/:vehicleId', controller.getUpcomingMaintenance.bind(controller)); + app.get('/maintenance/subtypes/:category', controller.getSubtypes.bind(controller)); +} +``` + +**File: `backend/src/features/maintenance/api/maintenance.controller.ts`** + +Follow pattern from `backend/src/features/documents/api/documents.controller.ts`: +- Extract userId from `request.user.sub` +- Use structured logging with logger +- Return proper HTTP status codes (201 for create, 200 for success, 404 for not found, etc.) +- Handle errors gracefully + +**File: `backend/src/features/maintenance/api/maintenance.validation.ts`** + +Use validation schemas (Fastify schema or Zod): +- Validate category is valid enum +- Validate subtypes is non-empty array +- Validate subtypes match category (server-side validation) +- Validate dates, numeric values +- Validate UUIDs + +**File: `backend/src/features/maintenance/index.ts`** + +```typescript +export { maintenanceRoutes } from './api/maintenance.routes'; +export * from './domain/maintenance.types'; +``` + +**File: `backend/src/app.ts`** + +Update to register routes (remove lines 118-134 placeholder): + +```typescript +import { maintenanceRoutes } from './features/maintenance'; + +// ... in buildApp() +await app.register(maintenanceRoutes, { prefix: '/api' }); +``` + +### Phase 4: Testing + +**File: `backend/src/features/maintenance/tests/unit/maintenance.service.test.ts`** + +Test cases: +- Create record with valid data +- Reject invalid category +- Reject invalid subtypes for category +- Reject empty subtypes array +- Calculate next due date correctly +- Identify due soon vs overdue +- Handle edge cases (no previous service, etc.) + +**File: `backend/src/features/maintenance/tests/integration/maintenance.integration.test.ts`** + +Test full API workflow: +- Create, read, update, delete records +- Create and manage schedules +- Get upcoming maintenance +- Test authentication (reject without token) +- Test authorization (reject access to other user's data) +- Test PostgreSQL array operations + +## Frontend Implementation + +### File Structure +``` +frontend/src/features/maintenance/ +ā”œā”€ā”€ types/ +│ └── maintenance.types.ts # Mirror backend types +ā”œā”€ā”€ api/ +│ └── maintenance.api.ts # API client +ā”œā”€ā”€ hooks/ +│ ā”œā”€ā”€ useMaintenanceRecords.ts # Records query/mutation +│ ā”œā”€ā”€ useMaintenanceSchedules.ts # Schedules query/mutation +│ └── useUpcomingMaintenance.ts # Upcoming items +ā”œā”€ā”€ components/ # Desktop components +│ ā”œā”€ā”€ MaintenanceRecordForm.tsx +│ ā”œā”€ā”€ MaintenanceRecordsList.tsx +│ ā”œā”€ā”€ MaintenanceRecordDetail.tsx +│ ā”œā”€ā”€ MaintenanceScheduleForm.tsx +│ ā”œā”€ā”€ MaintenanceSchedulesList.tsx +│ ā”œā”€ā”€ UpcomingMaintenanceCard.tsx +│ └── SubtypeCheckboxGroup.tsx # Reusable checkbox component +ā”œā”€ā”€ mobile/ # Mobile components +│ └── MaintenanceMobileScreen.tsx +└── pages/ + └── MaintenancePage.tsx # Desktop page +``` + +### Phase 5: Frontend Types and API + +**File: `frontend/src/features/maintenance/types/maintenance.types.ts`** + +Copy types from backend, export constants for dropdowns. + +**File: `frontend/src/features/maintenance/api/maintenance.api.ts`** + +```typescript +import { api } from '../../../core/api/client'; + +export const maintenanceApi = { + // Records + createRecord: (data: CreateMaintenanceRecordRequest) => + api.post('/api/maintenance/records', data), + + getRecords: () => + api.get('/api/maintenance/records'), + + getRecord: (id: string) => + api.get(`/api/maintenance/records/${id}`), + + updateRecord: (id: string, data: UpdateMaintenanceRecordRequest) => + api.put(`/api/maintenance/records/${id}`, data), + + deleteRecord: (id: string) => + api.delete(`/api/maintenance/records/${id}`), + + getRecordsByVehicle: (vehicleId: string) => + api.get(`/api/maintenance/records/vehicle/${vehicleId}`), + + // Schedules + createSchedule: (data: CreateScheduleRequest) => + api.post('/api/maintenance/schedules', data), + + getSchedules: () => + api.get('/api/maintenance/schedules'), + + getSchedulesByVehicle: (vehicleId: string) => + api.get(`/api/maintenance/schedules/vehicle/${vehicleId}`), + + updateSchedule: (id: string, data: UpdateScheduleRequest) => + api.put(`/api/maintenance/schedules/${id}`, data), + + deleteSchedule: (id: string) => + api.delete(`/api/maintenance/schedules/${id}`), + + // Utility + getUpcoming: (vehicleId: string) => + api.get(`/api/maintenance/upcoming/${vehicleId}`), + + getSubtypes: (category: MaintenanceCategory) => + api.get(`/api/maintenance/subtypes/${category}`) +}; +``` + +### Phase 6: React Hooks + +**File: `frontend/src/features/maintenance/hooks/useMaintenanceRecords.ts`** + +Use React Query pattern from fuel-logs: + +```typescript +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { maintenanceApi } from '../api/maintenance.api'; + +export function useMaintenanceRecords(vehicleId?: string) { + const queryClient = useQueryClient(); + + const { data: records, isLoading, error } = useQuery({ + queryKey: vehicleId ? ['maintenance-records', vehicleId] : ['maintenance-records'], + queryFn: () => vehicleId + ? maintenanceApi.getRecordsByVehicle(vehicleId) + : maintenanceApi.getRecords() + }); + + const createMutation = useMutation({ + mutationFn: maintenanceApi.createRecord, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['maintenance-records'] }); + } + }); + + const updateMutation = useMutation({ + mutationFn: ({ id, data }: { id: string; data: UpdateMaintenanceRecordRequest }) => + maintenanceApi.updateRecord(id, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['maintenance-records'] }); + } + }); + + const deleteMutation = useMutation({ + mutationFn: maintenanceApi.deleteRecord, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['maintenance-records'] }); + } + }); + + return { + records, + isLoading, + error, + createRecord: createMutation.mutate, + updateRecord: updateMutation.mutate, + deleteRecord: deleteMutation.mutate + }; +} +``` + +Similar hooks for schedules and upcoming maintenance. + +### Phase 7: Desktop Components + +**File: `frontend/src/features/maintenance/components/SubtypeCheckboxGroup.tsx`** + +Reusable component for subtype selection: + +```typescript +interface SubtypeCheckboxGroupProps { + category: MaintenanceCategory; + selectedSubtypes: string[]; + onChange: (subtypes: string[]) => void; +} + +export function SubtypeCheckboxGroup({ category, selectedSubtypes, onChange }: SubtypeCheckboxGroupProps) { + const subtypes = getSubtypesForCategory(category); + + const handleToggle = (subtype: string) => { + if (selectedSubtypes.includes(subtype)) { + onChange(selectedSubtypes.filter(s => s !== subtype)); + } else { + onChange([...selectedSubtypes, subtype]); + } + }; + + return ( +
+ {subtypes.map(subtype => ( + handleToggle(subtype)} + /> + } + label={subtype} + /> + ))} +
+ ); +} +``` + +**File: `frontend/src/features/maintenance/components/MaintenanceRecordForm.tsx`** + +Form structure: +1. Category dropdown (Routine Maintenance, Repair, Performance Upgrade) +2. SubtypeCheckboxGroup (dynamically shows based on category) +3. Date picker +4. Odometer input (optional) +5. Cost input (optional) +6. Shop name input (optional) +7. Notes textarea (optional) +8. Submit and Cancel buttons + +Validation: +- Category required +- At least one subtype required +- Selected subtypes must match category +- Date required +- Show error messages inline + +**File: `frontend/src/features/maintenance/components/MaintenanceRecordsList.tsx`** + +Table or card list showing: +- Date (sortable, default newest first) +- Category with count: "Routine Maintenance (3)" +- Odometer reading +- Cost +- Shop name +- Actions (view details, edit, delete) + +Click row to navigate to detail view. + +**File: `frontend/src/features/maintenance/components/MaintenanceRecordDetail.tsx`** + +Full detail view: +- All fields displayed +- Subtypes shown as chips/badges +- Edit and Delete buttons +- Back button + +**File: `frontend/src/features/maintenance/pages/MaintenancePage.tsx`** + +Tabbed interface: +- **Records Tab**: List of completed maintenance +- **Schedules Tab**: Recurring maintenance schedules +- **Upcoming Tab**: Due soon and overdue items + +Include: +- Vehicle selector (dropdown) +- Add new record/schedule buttons +- Filters (category, date range) + +### Phase 8: Mobile Components + +**File: `frontend/src/features/maintenance/mobile/MaintenanceMobileScreen.tsx`** + +Mobile-optimized design: +- GlassCard components (match existing pattern from documents/fuel-logs) +- Touch-friendly form inputs +- Large checkboxes (min 44x44px touch target) +- Single column layout +- Bottom sheet or full-screen modal for add/edit forms +- Swipe actions for delete +- Pull to refresh + +Collapsible sections: +- Tap category to expand/collapse subtype checkboxes +- Accordion style to save vertical space + +**Mobile-specific considerations:** +- Virtual scrolling for long lists +- Optimistic updates for instant feedback +- Loading skeletons +- Error boundaries + +### Phase 9: Route Integration + +**File: `frontend/src/App.tsx`** + +Desktop routes (update line 554): +```typescript +import { lazy } from 'react'; +const MaintenancePage = lazy(() => import('./features/maintenance/pages/MaintenancePage').then(m => ({ default: m.MaintenancePage }))); + +// In Routes: +} /> +``` + +Mobile navigation: +- Maintenance already in Layout.tsx navigation (line 42) +- Consider if it should be in bottom nav or remain in hamburger menu +- If adding to bottom nav, update mobile nav items in App.tsx + +## Display Format Guidelines + +### List View (Records) +``` +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ Jan 15, 2024 │ +│ Routine Maintenance (3) │ +│ 45,230 miles | $127.50 | Joe's Auto │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ +``` + +### Detail View (Full Record) +``` +Date: January 15, 2024 +Category: Routine Maintenance +Subtypes: + • Engine Oil + • Air Filter Element + • Cabin Air Filter / Purifier +Odometer: 45,230 miles +Cost: $127.50 +Shop: Joe's Auto Service +Notes: Used synthetic 5W-30 oil. Recommended tire rotation at next visit. +``` + +### Upcoming Maintenance (Color-Coded) +``` +🟢 Good - Not due yet +🟔 Due Soon - Within 30 days or 500 miles +šŸ”“ Overdue - Past due date or mileage +``` + +## Business Rules + +### Validation Rules +1. Category must be one of: routine_maintenance, repair, performance_upgrade +2. Subtypes must be non-empty array +3. All subtypes must be valid for the selected category +4. Date required for records +5. Vehicle must belong to user (ownership check) +6. Interval (months OR miles OR both) required for schedules + +### Next Due Calculation (Schedules) +``` +If interval_months AND interval_miles both set: + next_due_date = last_service_date + interval_months + next_due_mileage = last_service_mileage + interval_miles + Due when EITHER condition is met (whichever comes first) + +If only interval_months: + next_due_date = last_service_date + interval_months + next_due_mileage = null + +If only interval_miles: + next_due_date = null + next_due_mileage = last_service_mileage + interval_miles +``` + +### Due Soon Logic +``` +Due Soon (Yellow): +- next_due_date within 30 days of today +- OR next_due_mileage within 500 miles of current odometer + +Overdue (Red): +- next_due_date in the past +- OR next_due_mileage < current odometer +``` + +## Security Requirements + +1. **All queries user-scoped**: Every database query MUST filter by user_id +2. **Vehicle ownership**: Validate user owns vehicle before any operation +3. **Prepared statements**: NEVER concatenate SQL strings +4. **Authentication**: All routes require valid JWT token +5. **Authorization**: Users can only access their own data + +Example repository pattern: +```typescript +async findByVehicleId(vehicleId: string, userId: string): Promise { + // CORRECT - filters by both vehicle_id AND user_id + const result = await pool.query( + 'SELECT * FROM maintenance_records WHERE vehicle_id = $1 AND user_id = $2', + [vehicleId, userId] + ); + return result.rows; +} +``` + +## Caching Strategy + +Use Redis for caching: + +```typescript +// Records cache - 5 minutes +const cacheKey = `maintenance:records:user:${userId}`; +const ttl = 300; // 5 minutes + +// Vehicle-specific cache - 5 minutes +const vehicleCacheKey = `maintenance:records:vehicle:${vehicleId}`; + +// Upcoming maintenance - 1 hour (less frequently changing) +const upcomingCacheKey = `maintenance:upcoming:${vehicleId}`; +const upcomingTTL = 3600; // 1 hour + +// Invalidate on create/update/delete +await cacheService.del(cacheKey); +await cacheService.del(vehicleCacheKey); +await cacheService.del(upcomingCacheKey); +``` + +## Testing Strategy + +### Backend Tests + +**Unit Tests** (`backend/src/features/maintenance/tests/unit/`) +- Test service methods with mocked repository +- Test validation logic (category, subtypes) +- Test next due calculation +- Test due soon/overdue logic +- Test edge cases (no previous service, missing data) + +**Integration Tests** (`backend/src/features/maintenance/tests/integration/`) +- Test full API endpoints with test database +- Test authentication (401 without token) +- Test authorization (403 for other user's data) +- Test PostgreSQL array operations +- Test cascade deletes (vehicle deletion) + +Run tests: +```bash +make shell-backend +npm test -- features/maintenance +``` + +### Frontend Tests + +Test components: +- Form validation (category, subtypes) +- Checkbox selection/deselection +- Mobile touch interactions +- Responsive layout + +### Manual Testing (Docker-Only) + +1. After each change: `make rebuild` +2. Test mobile viewport: 375px width +3. Test desktop viewport: 1920px width +4. Test touch interactions on mobile +5. Verify all linting hooks pass (zero tolerance) + +## Documentation Requirements + +**File: `backend/src/features/maintenance/README.md`** + +Follow pattern from fuel-logs README: + +```markdown +# Maintenance Feature Capsule + +## Quick Summary (50 tokens) +Tracks vehicle maintenance including routine service, repairs, and performance upgrades. Supports multiple subtypes per record, recurring schedules, and upcoming/overdue calculations. User-scoped data with vehicle ownership enforcement. + +## API Endpoints +[List all endpoints with descriptions] + +## Structure +- **api/** - HTTP endpoints, routes, validators +- **domain/** - Business logic, types, rules +- **data/** - Repository, database queries +- **migrations/** - Feature-specific schema +- **tests/** - All feature tests + +## Categories and Subtypes +[List all three categories and their subtypes] + +## Dependencies +- Internal: core/auth, core/cache, core/config +- Database: maintenance_records, maintenance_schedules tables +- Feature: vehicles (vehicle_id FK) + +## Business Rules +[Document validation, calculation logic, etc.] + +## Testing +[Document test commands and examples] +``` + +## Success Criteria Checklist + +- [ ] Database migrations run cleanly (`make migrate`) +- [ ] Backend compiles without errors +- [ ] All backend unit tests pass +- [ ] All backend integration tests pass +- [ ] All TypeScript types are correct (no `any` types) +- [ ] All linting rules pass (ESLint, Prettier) +- [ ] Category dropdown works correctly +- [ ] Subtype checkboxes populate based on selected category +- [ ] Multiple subtypes can be selected +- [ ] Subtype validation prevents invalid selections +- [ ] Records display as "Category (count)" in list view +- [ ] Detail view shows all selected subtypes +- [ ] Schedule creation works +- [ ] Next due date calculation is correct +- [ ] Upcoming maintenance shows correct items +- [ ] Works on mobile (375px viewport) +- [ ] Touch targets are 44x44px minimum on mobile +- [ ] Works on desktop (1920px viewport) +- [ ] Responsive between mobile and desktop breakpoints +- [ ] No console errors +- [ ] No TODOs remaining in code +- [ ] README.md is complete and accurate +- [ ] Feature is registered in backend app.ts +- [ ] Feature is registered in frontend App.tsx routes + +## Common Pitfalls to Avoid + +1. **PostgreSQL Arrays**: Use proper array syntax `TEXT[]` and array operations `= ANY(subtypes)` +2. **User Scoping**: NEVER forget `AND user_id = $X` in queries +3. **Category Validation**: Server-side validation is required (don't trust client) +4. **Empty Subtypes**: Validate array is non-empty before saving +5. **Mobile Touch Targets**: Checkboxes must be 44x44px minimum +6. **Cache Invalidation**: Invalidate ALL relevant cache keys on update +7. **String Concatenation**: NEVER concatenate SQL strings - use prepared statements +8. **Type Safety**: Don't use `any` types - define proper interfaces + +## Reference Files + +For implementation patterns, refer to these existing features: +- **Documents Feature**: `backend/src/features/documents/` (most recent, best example) +- **Fuel Logs Feature**: `backend/src/features/fuel-logs/` (similar complexity) +- **Documents Frontend**: `frontend/src/features/documents/` (mobile + desktop patterns) + +## Implementation Order + +1. Backend database migration +2. Backend domain types +3. Backend repository +4. Backend service +5. Backend API (routes, controller, validation) +6. Backend tests +7. Register routes in app.ts +8. Frontend types +9. Frontend API client +10. Frontend hooks +11. Desktop components (form, list, detail) +12. Desktop page with tabs +13. Mobile components +14. Update routes in App.tsx +15. Manual testing (docker rebuild) +16. Documentation (README.md) +17. Final validation (all criteria met) diff --git a/docs/PLATFORM-SERVICES.md b/docs/PLATFORM-SERVICES.md index 1ff4f4c..9952bfa 100644 --- a/docs/PLATFORM-SERVICES.md +++ b/docs/PLATFORM-SERVICES.md @@ -75,7 +75,7 @@ GET /docs # Swagger UI Multi-tenant management service for platform-wide tenant operations. #### Architecture Components -- **API Service**: Python FastAPI on port 8001 +- **API Service**: Python FastAPI on port 8000 - **Database**: Dedicated PostgreSQL on port 5434 - **Cache**: Dedicated Redis instance on port 6381 @@ -95,7 +95,14 @@ Marketing and landing page service. ## Service Communication ### Inter-Service Communication -Platform services are **completely independent** - no direct communication between platform services. +Platform services have **no direct communication** between each other, but share some infrastructure resources: + +**Shared Resources**: +- Configuration files (`./config/shared/production.yml`) +- Secret management infrastructure (`./secrets/platform/` directory structure) +- Docker network (`platform` network for internal communication) + +**Independence Level**: Services can be deployed independently but rely on shared configuration and secrets infrastructure. ### Application → Platform Communication - **Protocol**: HTTP REST APIs @@ -241,10 +248,12 @@ Platform services are source of truth: **Verify All Platform Services**: ```bash curl http://localhost:8000/health # Platform Vehicles -curl http://localhost:8001/health # Platform Tenants +curl http://localhost:8000/health # Platform Tenants (same port as vehicles) curl https://motovaultpro.com # Platform Landing ``` +**Note**: Both platform services (Vehicles and Tenants APIs) run on port 8000. They are differentiated by routing rules in Traefik based on the request path. + ### Logs and Debugging **Service Logs**: diff --git a/docs/README.md b/docs/README.md index 5059e07..5646550 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,4 +21,4 @@ Project documentation hub for the hybrid platform (platform microservices) and m - Canonical URLs: Frontend `https://admin.motovaultpro.com`, Backend health `http://localhost:3001/health`. - Hosts entry required: `127.0.0.1 motovaultpro.com admin.motovaultpro.com`. -- Feature test coverage varies; vehicles has full coverage, others are in progress. +- Feature test coverage: Basic test structure exists for vehicles and documents features; other features have placeholder tests. diff --git a/docs/UX-DEBUGGING.md b/docs/UX-DEBUGGING.md new file mode 100644 index 0000000..14c0c78 --- /dev/null +++ b/docs/UX-DEBUGGING.md @@ -0,0 +1,89 @@ +# MotoVaultPro Debug Console Configuration + +## CRITICAL: Console Logs Are Stripped in Production Builds + +**Before debugging any UX/UI issues, ALWAYS enable console logging first.** + +## Production Build Console Stripping + +The Vite build configuration in `frontend/vite.config.ts` aggressively removes ALL console statements in production: + +```typescript +// Lines 60-62: Terser removes console logs +terserOptions: { + compress: { + drop_console: true, // ← This removes ALL console.log statements + drop_debugger: true, + pure_funcs: ['console.log', 'console.info', 'console.debug'], + } +} + +// Line 74: ESBuild also removes console logs +esbuild: { + drop: ['console', 'debugger'], // ← Additional console removal +} +``` + +## Debug Protocol for UX Issues + +When debugging **any** UX/UI problems (buttons not working, state not updating, components not rendering): + +### 1. Enable Console Logging FIRST +```typescript +// In frontend/vite.config.ts + +// TEMPORARILY change these lines: +drop_console: false, // Keep console logs for debugging +// pure_funcs: ['console.log', 'console.info', 'console.debug'], // Comment out + +// AND: +drop: ['debugger'], // Keep console, only drop debugger +``` + +### 2. Add Debug Statements +```typescript +// Example debug patterns: +console.log('[DEBUG] Component render - state:', someState); +console.log('[DEBUG] useEffect triggered - deps:', dep1, dep2); +console.log('[DEBUG] Button clicked - current state:', state); +console.log('[DEBUG] Store action called:', actionName, payload); +``` + +### 3. Rebuild and Test +```bash +make rebuild # Required to apply vite.config.ts changes +``` + +### 4. Fix the Issue +Use browser dev tools console output to identify the root cause. + +### 5. Clean Up and Restore Production Settings +```typescript +// Restore production console stripping: +drop_console: true, // Remove console logs in production +pure_funcs: ['console.log', 'console.info', 'console.debug'], +drop: ['console', 'debugger'], // Additional cleanup +``` + +Remove debug console.log statements and rebuild. + +## Common UX Issue Patterns + +1. **Buttons not working**: Usually state management or event handler issues +2. **Components not re-rendering**: Missing dependencies in hooks or store subscription problems +3. **useEffect fighting with user actions**: Dependencies causing infinite loops +4. **Store state not updating**: Action functions not properly bound or called + +## Example: The Sidebar Issue + +The sidebar X button wasn't working because: +- `useEffect` dependency array included `sidebarOpen` +- When user clicked X → `sidebarOpen` became `false` +- `useEffect` fired → immediately called `setSidebarOpen(true)` +- User action was overridden by the `useEffect` + +**Without console debugging enabled, this was invisible!** + +## Key Reminder + +**Never assume JavaScript is working correctly in production builds without console debugging enabled first.** The aggressive console stripping makes silent failures very common. \ No newline at end of file diff --git a/docs/VEHICLES-API.md b/docs/VEHICLES-API.md index 18786f7..dd4a038 100644 --- a/docs/VEHICLES-API.md +++ b/docs/VEHICLES-API.md @@ -38,7 +38,8 @@ Notes: ### Caching (Redis) - Keys: `dropdown:years`, `dropdown:makes:{year}`, `dropdown:models:{year}:{make}`, `dropdown:trims:{year}:{model}`, `dropdown:engines:{year}:{model}:{trim}` -- Default TTL: 6 hours +- Default TTL: 1 hour (3600 seconds) +- **Configurable**: Set via `CACHE_TTL` environment variable in seconds ### Seeds & Specific Examples Seed files under `mvp-platform-services/vehicles/sql/schema/`: @@ -81,7 +82,8 @@ Changes: - Migrations packaged in image under `/app/migrations/features/[feature]/migrations`. - Runner (`backend/src/_system/migrations/run-all.ts`): - Reads base dir from `MIGRATIONS_DIR` (env in Dockerfile) - - Tracks executed files in `_migrations` (idempotent) + - Tracks executed files in `_migrations` table and skips already executed files + - **Idempotent at file level**: Safe to re-run migration system multiple times - Wait/retry for DB readiness to avoid flapping on cold starts - Auto‑migrate on backend container start: `node dist/_system/migrations/run-all.js && npm start` - Manual: `make migrate` (runs runner inside the container) diff --git a/docs/changes/CLAUDE.md b/docs/changes/CLAUDE.md deleted file mode 100644 index 915cb6c..0000000 --- a/docs/changes/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -ignore this directory unless specifically asked to read files diff --git a/docs/changes/DOCUMENTS.md b/docs/changes/DOCUMENTS.md deleted file mode 100644 index a0c5b5c..0000000 --- a/docs/changes/DOCUMENTS.md +++ /dev/null @@ -1,299 +0,0 @@ -# Documents Feature Plan (S3-Compatible, Phased) - -This plan aligns with the current codebase: MinIO is running (`admin-minio`), object storage credentials are mounted as secrets, and `appConfig.getMinioConfig()` is available. We will implement a generic S3-compatible storage surface with a MinIO-backed adapter first, following the Docker‑first, production‑only workflow and mobile+desktop requirements. - -— Read me quick — -- Storage: Start with MinIO SDK via `getMinioConfig()`. Keep the interface S3‑generic to support AWS S3 later without changing features. -- Auth/Tenant: All endpoints use `[fastify.authenticate, tenantMiddleware]`. -- Testing: Use Jest; run via containers with `make test`. -- Mobile+Desktop: Follow existing Zustand nav, React Router routes, GlassCard components, and React Query offlineFirst. - -Handoff markers are provided at the end of each phase. If work pauses, pick up from the next ā€œDone whenā€ checklist. - -## Phase 0 — Baseline Verification - -Objectives -- Confirm configuration and dependencies to avoid rework. - -Tasks -- Verify MinIO configuration in `config/app/production.yml` → `minio.endpoint`, `minio.port`, `minio.bucket`. -- Verify mounted secrets exist for MinIO (`secrets/app/minio-access-key.txt`, `secrets/app/minio-secret-key.txt`). -- Verify backend dependency presence: - - Present: `minio@^7.1.3` - - Missing: `@fastify/multipart` (add to `backend/package.json`) -- Rebuild and tail logs - - `make rebuild` - - `make logs` - -Done when -- Containers start cleanly and backend logs show no missing module errors. - -Status -- MinIO configuration verified in repo (endpoint/port/bucket present) āœ“ -- MinIO secrets present in repo (mounted paths defined) āœ“ -- Package check: `minio` present āœ“, `@fastify/multipart` added to backend/package.json āœ“ -- Rebuild/logs runtime verification: pending (perform via `make rebuild && make logs`) - -## Phase 1 — Storage Foundation (S3-Compatible, MinIO-Backed) - -Objectives -- Create a generic storage faƧade used by features; implement first adapter using MinIO SDK. - -Design -- Interface `StorageService` methods: - - `putObject(bucket, key, bodyOrStream, contentType, metadata?)` - - `getObjectStream(bucket, key)` - - `deleteObject(bucket, key)` - - `headObject(bucket, key)` - - `getSignedUrl(bucket, key, { method: 'GET'|'PUT', expiresSeconds })` -- Key scheme: `documents/{userId}/{vehicleId}/{documentId}/{version}/{uuid}.{ext}` -- Security: Private objects only; short‑lived signed URLs when needed. - -Files -- `backend/src/core/storage/storage.service.ts` — faƧade and factory. -- `backend/src/core/storage/adapters/minio.adapter.ts` — uses MinIO SDK and `appConfig.getMinioConfig()`. - -Tasks -- Implement MinIO client using endpoint/port/accessKey/secretKey/bucket from `appConfig.getMinioConfig()`. -- Ensure streaming APIs are used for uploads/downloads. -- Implement signed URL generation for downloads with short TTL (e.g., 60–300s). - -Done when -- Service can put/head/get/delete and generate signed URLs against `admin-minio` bucket from inside the backend container. - -Status -- Storage facade added: `backend/src/core/storage/storage.service.ts` āœ“ -- MinIO adapter implemented: `backend/src/core/storage/adapters/minio.adapter.ts` āœ“ -- Runtime validation against MinIO: pending (validate post-rebuild) ☐ - -## Phase 2 — Backend HTTP Foundation - -Objectives -- Enable file uploads and wire security. - -Tasks -- Add `@fastify/multipart` to `backend/package.json`. -- In `backend/src/app.ts`, register multipart with config‑based limits: - - `limits.fileSize` sourced from `appConfig.config.performance.max_request_size`. -- Confirm authentication plugin and tenant middleware are active (already implemented). - -Done when -- Backend accepts multipart requests and enforces size limits without errors. - -Status -- Dependency added: `@fastify/multipart` āœ“ -- Registered in `backend/src/app.ts` with byte-limit parser āœ“ -- Runtime verification via container: pending ☐ - -## Phase 3 — Documents Feature Capsule (Backend) - -Objectives -- Create the feature capsule with schema, repository, service, routes, and validators, following existing patterns (see vehicles and fuel‑logs). - -Structure (backend) -``` -backend/src/features/documents/ -ā”œā”€ā”€ README.md -ā”œā”€ā”€ index.ts -ā”œā”€ā”€ api/ -│ ā”œā”€ā”€ documents.routes.ts -│ ā”œā”€ā”€ documents.controller.ts -│ └── documents.validation.ts -ā”œā”€ā”€ domain/ -│ ā”œā”€ā”€ documents.service.ts -│ └── documents.types.ts -ā”œā”€ā”€ data/ -│ └── documents.repository.ts -ā”œā”€ā”€ migrations/ -│ └── 001_create_documents_table.sql -└── tests/ - ā”œā”€ā”€ unit/ - └── integration/ -``` - -Database schema -- Table `documents`: - - `id UUID PK` - - `user_id VARCHAR(255)` - - `vehicle_id UUID` FK → `vehicles(id)` - - `document_type VARCHAR(32)` CHECK IN ('insurance','registration') - - `title VARCHAR(200)`; `notes TEXT NULL`; `details JSONB` - - `storage_bucket VARCHAR(128)`; `storage_key VARCHAR(512)` - - `file_name VARCHAR(255)`; `content_type VARCHAR(128)`; `file_size BIGINT`; `file_hash VARCHAR(128) NULL` - - `issued_date DATE NULL`; `expiration_date DATE NULL` - - `created_at TIMESTAMP DEFAULT now()`; `updated_at TIMESTAMP DEFAULT now()` with `update_updated_at_column()` trigger - - `deleted_at TIMESTAMP NULL` -- Indexes: `(user_id)`, `(vehicle_id)`, `(user_id, vehicle_id)`, `(document_type)`, `(expiration_date)`; optional GIN on `details` if needed. - -API endpoints -``` -POST /api/documents # Create metadata (with/without file) -GET /api/documents # List (filters: vehicleId, type, expiresBefore) -GET /api/documents/:id # Get metadata -PUT /api/documents/:id # Update metadata/details -DELETE /api/documents/:id # Soft delete (and delete object) -GET /api/documents/vehicle/:vehicleId # List by vehicle -POST /api/documents/:id/upload # Upload/replace file (multipart) -GET /api/documents/:id/download # Download (proxy stream or signed URL) -``` -- Pre‑handlers: `[fastify.authenticate, tenantMiddleware]` for all routes. -- Validation: Zod schemas for params/query/body in `documents.validation.ts`. -- Ownership: Validate `vehicle_id` belongs to `user_id` using vehicles pattern (like fuel‑logs). - -Wire‑up -- Register in `backend/src/app.ts`: - - `import { documentsRoutes } from './features/documents/api/documents.routes'` - - `await app.register(documentsRoutes, { prefix: '/api' })` -- Health: Update `/health` feature list to include `documents`. -- Migrations: Add `'features/documents'` to `MIGRATION_ORDER` in `backend/src/_system/migrations/run-all.ts` after `'features/vehicles'`. - -Done when -- CRUD + upload/download endpoints are reachable and secured; migrations run in correct order; ownership enforced. - -Status -- Capsule scaffolded (api/domain/data/tests/migrations/README) āœ“ -- Migration added `backend/src/features/documents/migrations/001_create_documents_table.sql` āœ“ -- Registered routes in `backend/src/app.ts` with `/api` prefix āœ“ -- Health feature list updated to include `documents` āœ“ -- Migration order updated in `backend/src/_system/migrations/run-all.ts` āœ“ -- CRUD handlers for metadata implemented āœ“ -- Upload endpoint implemented with multipart streaming, MIME allowlist, and storage meta update āœ“ -- Download endpoint implemented with proxy streaming and inline/attachment disposition āœ“ -- Ownership validation on create via vehicles check āœ“ -- Runtime verification in container: pending ☐ - -## Phase 4 — Frontend Feature (Mobile + Desktop) - -Objectives -- Implement documents UI following existing navigation, layout, and data patterns. - -Structure (frontend) -``` -frontend/src/features/documents/ -ā”œā”€ā”€ pages/ -ā”œā”€ā”€ components/ -ā”œā”€ā”€ hooks/ -└── types/ -``` - -Navigation -- Mobile: Add ā€œDocumentsā€ to bottom nav (Zustand store in `frontend/src/core/store/navigation.ts`). -- Desktop: Add routes in `frontend/src/App.tsx` for list/detail/upload. -- Sub‑screens (mobile): list → detail → upload; wrap content with `GlassCard`. - -Upload UX -- Mobile camera/gallery: ``. -- Desktop drag‑and‑drop with progress. -- Progress tracking: React Query mutation with progress events; optimistic updates and cache invalidation. -- Offline: Use existing React Query `offlineFirst` config; queue uploads and retry on reconnect. - -Viewer -- Inline image/PDF preview; `Content-Disposition` inline for images/PDF; gestures (pinch/zoom) for mobile images. - -Done when -- Users can list, upload, view, and delete documents on both mobile and desktop with responsive UI and progress. - -Status -- Add Documents to mobile bottom nav (Zustand): completed āœ“ -- Add desktop routes in `App.tsx` (list/detail/upload): completed āœ“ -- Scaffold pages/components/hooks structure: completed āœ“ -- Hook list/detail CRUD endpoints with React Query: completed āœ“ -- Implement upload with progress UI: completed āœ“ (hooks with onUploadProgress; UI in mobile/detail) -- Optimistic updates: partial (invalidate queries on success) ◐ -- Offline queuing/retry via React Query networkMode: configured via hooks āœ“ -- Previews: basic image/PDF preview implemented āœ“ (DocumentPreview) -- Gesture-friendly viewer: pending ☐ -- Desktop navigation: sidebar now defaults open and includes Documents āœ“ -- Build hygiene: resolved TS unused import error in frontend documents hooks āœ“ - -## Phase 5 — Security, Validation, and Policies - -Objectives -- Enforce safe file handling and consistent deletion semantics. - -Tasks -- MIME allowlist: `application/pdf`, `image/jpeg`, `image/png`; reject executables. -- Upload size: Enforce via multipart limit tied to `performance.max_request_size`. -- Deletion: Soft delete DB first; delete object after. Consider retention policy later if required. -- Logging: Create/update/delete/upload/download events include `user_id`, `document_id`, `vehicle_id` (use existing logger). -- Optional rate limiting for upload route (defer dependency until needed). - -Done when -- Unsafe files rejected; logs record document events; deletions are consistent. - -Status -- MIME allowlist enforced for uploads (PDF, JPEG, PNG) āœ“ -- Upload size enforced via multipart limit (config-driven) āœ“ -- Deletion semantics: DB soft-delete and best-effort storage object deletion āœ“ -- Event logging for document actions: pending ☐ - -## Phase 6 — Testing (Docker-First) - -Objectives -- Achieve green tests and linting across backend and frontend. - -Backend tests -- Unit: repository/service/storage adapter (mock MinIO), validators. -- Integration: API with test DB + MinIO container, stream upload/download, auth/tenant checks. - -Frontend tests -- Unit: components/forms, upload interactions, previews. -- Integration: hooks with mocked API; navigation flows for list/detail/upload. - -Commands -- `make test` (backend + frontend) -- `make shell-backend` then `npm test -- features/documents` -- `make test-frontend` - -Done when -- All tests/linters pass with zero issues; upload/download E2E verified in containers. - -Status -- Backend unit tests (service/repo/storage; validators): pending ☐ -- Backend integration tests (upload/download/auth/tenant): pending ☐ -- Frontend unit tests (components/forms/uploads/previews): pending ☐ -- Frontend integration tests (hooks + navigation flows): pending ☐ -- CI via `make test` and linters green: pending ☐ - -## Phase 7 — Reality Checkpoints and Handoff - -Checkpoints -- After each phase: `make rebuild && make logs`. -- Before moving on: Verify auth + tenant pre‑handlers, ownership checks, and mobile responsiveness. -- When interrupted: Commit current status and annotate the ā€œCurrent Handoff Statusā€ section below. - -Handoff fields (update as you go) -- Storage faƧade: [x] implemented [ ] validated against MinIO -- Multipart plugin: [x] registered [x] enforcing limits -- Documents migrations: [x] added [ ] executed [ ] indexes verified -- Repo/service/routes: [x] implemented [x] ownership checks -- Frontend routes/nav: [x] added [x] mobile [x] desktop -- Upload/download flows: backend [x] implemented UI [x] progress [x] preview [ ] signed URLs (optional) -- Tests: [ ] unit backend [ ] int backend [ ] unit frontend [ ] int frontend - -Diagnostics Notes -- Added `/api/health` endpoint in backend to validate Traefik routing to admin-backend for API paths. -- Fixed Fastify schema boot error by removing Zod schemas from documents routes (align with existing patterns). This prevented route registration and caused 404 on `/api/*` while server crashed/restarted. - -## S3 Compatibility Notes - -- The interface is provider‑agnostic. MinIO adapter speaks S3‑compatible API using custom endpoint and credentials from `getMinioConfig()`. -- Adding AWS S3 later: Implement `backend/src/core/storage/adapters/s3.adapter.ts` using `@aws-sdk/client-s3`, wire via a simple provider flag (e.g., `storage.provider: 'minio' | 's3'`). No feature code changes expected. -- Security parity: Keep private objects by default; consider server‑side encryption when adding AWS S3. - -## Reference Pointers - -- MinIO config: `backend/src/core/config/config-loader.ts` (`getMinioConfig()`) and `config/app/production.yml`. -- Auth plugin: `backend/src/core/plugins/auth.plugin.ts`. -- Tenant middleware: `backend/src/core/middleware/tenant.ts`. -- Migration runner: `backend/src/_system/migrations/run-all.ts` (edit `MIGRATION_ORDER`). -- Feature registration: `backend/src/app.ts` (register `documentsRoutes` and update `/health`). -- Frontend nav and layout: `frontend/src/App.tsx`, `frontend/src/core/store/navigation.ts`, `frontend/src/shared-minimal/components/mobile/GlassCard`. - -## Success Criteria - -- Documents CRUD with upload/download works on mobile and desktop. -- Ownership and tenant enforcement on every request; private object storage; safe file types. -- S3‑compatible storage layer with MinIO adapter; S3 adapter can be added without feature changes. -- All tests and linters green; migrations idempotent and ordered after vehicles. -- Build hygiene: backend TS errors fixed (unused import, override modifier, union narrowing) āœ“ diff --git a/docs/changes/K8S-REDESIGN.md b/docs/changes/K8S-REDESIGN.md deleted file mode 100644 index c8a7fd4..0000000 --- a/docs/changes/K8S-REDESIGN.md +++ /dev/null @@ -1,942 +0,0 @@ -# Docker Compose → Kubernetes Architecture Redesign - -## Overview - -This document outlines the aggressive redesign of MotoVaultPro's Docker Compose architecture to closely replicate a Kubernetes deployment pattern. **Breaking changes are acceptable** as this is a pre-production application. The goal is to completely replace the current architecture with a production-ready K8s-equivalent setup in 2-3 days, eliminating all development shortcuts and implementing true production constraints. - -**SCOPE**: ETL services have been completely removed from the architecture. This migration covers the 11 remaining core services with a focus on security, observability, and K8s compatibility over backward compatibility. - -## Current Architecture Analysis - -### Core Services for Migration (11 containers) - -**MVP Platform Services (Microservices)** -- `mvp-platform-landing` - Marketing/landing page (nginx) -- `mvp-platform-tenants` - Multi-tenant management API (FastAPI) -- `mvp-platform-vehicles-api` - Vehicle data API (FastAPI) -- `mvp-platform-vehicles-db` - Vehicle data storage (PostgreSQL) -- `mvp-platform-vehicles-redis` - Vehicle data cache (Redis) - -**Application Services (Modular Monolith)** -- `admin-backend` - Application API with feature capsules (Node.js) -- `admin-frontend` - React SPA (nginx) -- `admin-postgres` - Application database (PostgreSQL) -- `admin-redis` - Application cache (Redis) -- `admin-minio` - Object storage (MinIO) - -**Infrastructure** -- `platform-postgres` - Platform services database -- `platform-redis` - Platform services cache -- `nginx-proxy` - **TO BE COMPLETELY REMOVED** (replaced by Traefik) - -### Current Limitations (TO BE BROKEN) - -1. **Single Network**: All services on default network - **BREAKING: Move to isolated networks** -2. **Manual Routing**: nginx configuration requires manual updates - **BREAKING: Complete removal** -3. **Excessive Port Exposure**: 10+ services expose ports directly - **BREAKING: Remove all except Traefik** -4. **Environment Variable Configuration**: 35+ env vars scattered across services - **BREAKING: Mandatory file-based config** -5. **Development Shortcuts**: Debug modes, open CORS, no authentication - **BREAKING: Production-only mode** -6. **No Resource Limits**: Services can consume unlimited resources - **BREAKING: Enforce limits on all services** - -## Target Kubernetes-like Architecture - -### Network Segmentation (Aggressive Isolation) - -```yaml -networks: - frontend: - driver: bridge - internal: false # Only for Traefik public access - labels: - - "com.motovaultpro.network=frontend" - - "com.motovaultpro.purpose=public-traffic-only" - - backend: - driver: bridge - internal: true # Complete isolation from host - labels: - - "com.motovaultpro.network=backend" - - "com.motovaultpro.purpose=api-services" - - database: - driver: bridge - internal: true # Application data isolation - labels: - - "com.motovaultpro.network=database" - - "com.motovaultpro.purpose=app-data-layer" - - platform: - driver: bridge - internal: true # Platform microservices isolation - labels: - - "com.motovaultpro.network=platform" - - "com.motovaultpro.purpose=platform-services" -``` - -**BREAKING CHANGE**: No `egress` network. Services requiring external API access (Auth0, Google Maps, VPIC) will connect through the `backend` network with Traefik handling external routing. This forces all external communication through the ingress controller, matching Kubernetes egress gateway patterns. - -### Service Placement Strategy (Aggressive Isolation) - -| Service | Networks | Purpose | K8s Equivalent | -|---------|----------|---------|----------------| -| `traefik` | `frontend`, `backend` | **ONLY** public routing + API access | LoadBalancer + IngressController | -| `admin-frontend`, `mvp-platform-landing` | `frontend` | Public web applications | Ingress frontends | -| `admin-backend` | `backend`, `database`, `platform` | Application API with cross-service access | ClusterIP with multiple network attachment | -| `mvp-platform-tenants`, `mvp-platform-vehicles-api` | `backend`, `platform` | Platform APIs + data access | ClusterIP (platform namespace) | -| `admin-postgres`, `admin-redis`, `admin-minio` | `database` | Application data isolation | StatefulSets with PVCs | -| `platform-postgres`, `platform-redis`, `mvp-platform-vehicles-db`, `mvp-platform-vehicles-redis` | `platform` | Platform data isolation | StatefulSets with PVCs | - -**BREAKING CHANGES**: -- **No external network access** for individual services -- **No host port exposure** except Traefik (80, 443, 8080) -- **Mandatory network isolation** - services cannot access unintended networks -- **No development bypasses** - all traffic through Traefik - -**Service Communication Matrix (Restricted)** -``` -# Internal service communication (via backend network) -admin-backend → mvp-platform-vehicles-api:8000 (authenticated API calls) -admin-backend → mvp-platform-tenants:8000 (authenticated API calls) - -# Data layer access (isolated networks) -admin-backend → admin-postgres:5432, admin-redis:6379, admin-minio:9000 -mvp-platform-vehicles-api → mvp-platform-vehicles-db:5432, mvp-platform-vehicles-redis:6379 -mvp-platform-tenants → platform-postgres:5432, platform-redis:6379 - -# External integrations (BREAKING: via Traefik proxy only) -admin-backend → External APIs (Auth0, Google Maps, VPIC) via Traefik middleware -Platform services → External APIs via Traefik middleware (no direct access) -``` - -**BREAKING CHANGE**: All external API calls must be proxied through Traefik middleware. No direct external network access for any service. - -## Traefik Configuration - -### Core Traefik Setup - -- New directories `config/traefik/` and `secrets/traefik/` will store production-bound configuration and certificates. These folders are justified as they mirror their eventual Kubernetes ConfigMap/Secret counterparts and replace the legacy nginx configuration. - -```yaml -traefik: - image: traefik:v3.0 - container_name: traefik - networks: - - frontend - - backend - ports: - - "80:80" - - "443:443" - - "8080:8080" # Dashboard - volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - - ./config/traefik/traefik.yml:/etc/traefik/traefik.yml:ro - - ./config/traefik/middleware.yml:/etc/traefik/middleware.yml:ro - - ./secrets/traefik/certs:/certs:ro - labels: - - "traefik.enable=true" - - "traefik.http.routers.dashboard.rule=Host(`traefik.motovaultpro.local`)" - - "traefik.http.routers.dashboard.tls=true" - - "traefik.http.routers.dashboard.middlewares=dashboard-allowlist@docker" - - "traefik.http.middlewares.dashboard-allowlist.ipwhitelist.sourcerange=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16" -``` - -### Service Discovery Labels - -**Admin Frontend** -```yaml -admin-frontend: - labels: - - "traefik.enable=true" - - "traefik.http.routers.admin-app.rule=Host(`admin.motovaultpro.com`)" - - "traefik.http.routers.admin-app.tls=true" - - "traefik.http.routers.admin-app.middlewares=secure-headers@file" - - "traefik.http.services.admin-app.loadbalancer.server.port=3000" - - "traefik.http.services.admin-app.loadbalancer.healthcheck.path=/" -``` - -**Admin Backend** -```yaml -admin-backend: - labels: - - "traefik.enable=true" - - "traefik.http.routers.admin-api.rule=Host(`admin.motovaultpro.com`) && PathPrefix(`/api`)" - - "traefik.http.routers.admin-api.tls=true" - - "traefik.http.routers.admin-api.middlewares=api-auth@file,cors@file" - - "traefik.http.services.admin-api.loadbalancer.server.port=3001" - - "traefik.http.services.admin-api.loadbalancer.healthcheck.path=/health" -``` - -**Platform Landing** -```yaml -mvp-platform-landing: - labels: - - "traefik.enable=true" - - "traefik.http.routers.landing.rule=Host(`motovaultpro.com`)" - - "traefik.http.routers.landing.tls=true" - - "traefik.http.routers.landing.middlewares=secure-headers@file" - - "traefik.http.services.landing.loadbalancer.server.port=3000" -``` - -### Middleware Configuration - -```yaml -# config/traefik/middleware.yml -http: - middlewares: - secure-headers: - headers: - accessControlAllowMethods: - - GET - - OPTIONS - - PUT - - POST - - DELETE - accessControlAllowOriginList: - - "https://admin.motovaultpro.com" - - "https://motovaultpro.com" - accessControlMaxAge: 100 - addVaryHeader: true - browserXssFilter: true - contentTypeNosniff: true - forceSTSHeader: true - frameDeny: true - stsIncludeSubdomains: true - stsPreload: true - stsSeconds: 31536000 - - cors: - headers: - accessControlAllowCredentials: true - accessControlAllowHeaders: - - "Authorization" - - "Content-Type" - - "X-Requested-With" - accessControlAllowMethods: - - "GET" - - "POST" - - "PUT" - - "DELETE" - - "OPTIONS" - accessControlAllowOriginList: - - "https://admin.motovaultpro.com" - - "https://motovaultpro.com" - accessControlMaxAge: 100 - - api-auth: - forwardAuth: - address: "http://admin-backend:3001/auth/verify" - authResponseHeaders: - - "X-Auth-User" - - "X-Auth-Roles" - dashboard-allowlist: - ipWhiteList: - sourceRange: - - "10.0.0.0/8" - - "172.16.0.0/12" - - "192.168.0.0/16" -``` - -## Enhanced Health Checks - -### Standardized Health Check Pattern - -All services will implement: - -1. **Startup Probe** - Service initialization -2. **Readiness Probe** - Service ready to accept traffic -3. **Liveness Probe** - Service health monitoring - -```yaml -# Example: admin-backend -healthcheck: - test: ["CMD", "node", "-e", " - const http = require('http'); - const options = { - hostname: 'localhost', - port: 3001, - path: '/health/ready', - timeout: 2000 - }; - const req = http.request(options, (res) => { - process.exit(res.statusCode === 200 ? 0 : 1); - }); - req.on('error', () => process.exit(1)); - req.end(); - "] - interval: 15s - timeout: 5s - retries: 3 - start_period: 45s -``` - -### Health Endpoint Standards - -All services must expose: -- `/health` - Basic health check -- `/health/ready` - Readiness probe -- `/health/live` - Liveness probe - -## Configuration Management - -### Configuration & Secret Management (Compose-compatible) - -- Application and platform settings will live in versioned files under `config/app/` and `config/platform/`, mounted read-only into the containers (`volumes:`). This mirrors ConfigMaps without relying on Docker Swarm-only `configs`. -- Secrets (Auth0, database, API keys) will be stored as individual files beneath `secrets/app/` and `secrets/platform/`, mounted as read-only volumes. At runtime the containers will read from `/run/secrets/*`, matching the eventual Kubernetes Secret mount pattern. -- Committed templates: `.example` files now reside in `config/app/production.yml.example`, `config/platform/production.yml.example`, and `secrets/**/.example` to document required keys while keeping live credentials out of Git. The real files stay untracked via `.gitignore`. -- Runtime loader: extend `backend/src/core/config/environment.ts` (and equivalent FastAPI settings) to hydrate configuration by reading `CONFIG_PATH` YAML and `SECRETS_DIR` file values before falling back to `process.env`. This ensures parity between Docker Compose mounts and future Kubernetes ConfigMap/Secret projections. - -#### Configuration Migration Strategy - -**Current Environment Variables (45 total) to File Mapping:** - -**Application Secrets** (`secrets/app/`): -``` -auth0-client-secret.txt # AUTH0_CLIENT_SECRET -postgres-password.txt # DB_PASSWORD -minio-access-key.txt # MINIO_ACCESS_KEY -minio-secret-key.txt # MINIO_SECRET_KEY -platform-vehicles-api-key.txt # PLATFORM_VEHICLES_API_KEY -google-maps-api-key.txt # GOOGLE_MAPS_API_KEY -``` - -**Platform Secrets** (`secrets/platform/`): -``` -platform-db-password.txt # PLATFORM_DB_PASSWORD -vehicles-db-password.txt # POSTGRES_PASSWORD (vehicles) -``` - -**Network attachments for outbound-enabled services:** -```yaml -mvp-platform-vehicles-api: - networks: - - backend - - platform - - egress - -mvp-platform-tenants: - networks: - - backend - - platform - - egress -``` - -**Application Configuration** (`config/app/production.yml`): -```yaml -server: - port: 3001 - tenant_id: admin - -database: - host: admin-postgres - port: 5432 - name: motovaultpro - user: postgres - -redis: - host: admin-redis - port: 6379 - -minio: - endpoint: admin-minio - port: 9000 - bucket: motovaultpro - -auth0: - domain: motovaultpro.us.auth0.com - audience: https://api.motovaultpro.com - -platform: - vehicles_api_url: http://mvp-platform-vehicles-api:8000 - tenants_api_url: http://mvp-platform-tenants:8000 - -external: - vpic_api_url: https://vpic.nhtsa.dot.gov/api/vehicles -``` - -**Compose Example:** -```yaml - admin-backend: - volumes: - - ./config/app/production.yml:/app/config/production.yml:ro - - ./secrets/app/auth0-client-secret.txt:/run/secrets/auth0-client-secret:ro - - ./secrets/app/postgres-password.txt:/run/secrets/postgres-password:ro - - ./secrets/app/minio-access-key.txt:/run/secrets/minio-access-key:ro - - ./secrets/app/minio-secret-key.txt:/run/secrets/minio-secret-key:ro - - ./secrets/app/platform-vehicles-api-key.txt:/run/secrets/platform-vehicles-api-key:ro - - ./secrets/app/google-maps-api-key.txt:/run/secrets/google-maps-api-key:ro - environment: - - NODE_ENV=production - - CONFIG_PATH=/app/config/production.yml - - SECRETS_DIR=/run/secrets - networks: - - backend - - database - - platform - - egress -``` - -## Resource Management - -### Resource Allocation Strategy - -**Tier 1: Critical Services** -```yaml - admin-backend: - mem_limit: 2g - cpus: 2.0 -``` - -**Tier 2: Supporting Services** -```yaml - admin-frontend: - mem_limit: 1g - cpus: 1.0 -``` - -**Tier 3: Infrastructure Services** -```yaml - traefik: - mem_limit: 512m - cpus: 0.5 -``` - -### Service Tiers - -| Tier | Services | Resource Profile | Priority | -|------|----------|------------------|----------| -| 1 | admin-backend, mvp-platform-vehicles-api, admin-postgres | High | Critical | -| 2 | admin-frontend, mvp-platform-tenants, mvp-platform-landing | Medium | Important | -| 3 | traefik, redis services, storage services | Low | Supporting | - -### Development Port Exposure Policy - -**Exposed Ports for Development Debugging:** -```yaml -# Database Access (development debugging) -- 5432:5432 # admin-postgres (application DB access) -- 5433:5432 # mvp-platform-vehicles-db (platform DB access) -- 5434:5432 # platform-postgres (platform services DB access) - -# Cache Access (development debugging) -- 6379:6379 # admin-redis -- 6380:6379 # mvp-platform-vehicles-redis -- 6381:6379 # platform-redis - -# Storage Access (development/admin) -- 9000:9000 # admin-minio API -- 9001:9001 # admin-minio console - -# Traefik Dashboard (development monitoring) -- 8080:8080 # traefik dashboard -``` - -**Internal-Only Services (no port exposure):** -- All HTTP application services (routed through Traefik) -- Platform APIs (accessible via application backend only) - -**Mobile Testing Considerations:** -- Self-signed certificates require device-specific trust configuration -- Development URLs must be accessible from mobile devices on same network -- Certificate CN must match both `motovaultpro.com` and `admin.motovaultpro.com` - -## Migration Implementation Plan (Aggressive Approach) - -### **BREAKING CHANGE STRATEGY**: Complete Architecture Replacement (2-3 Days) - -**Objective**: Replace entire Docker Compose architecture with K8s-equivalent setup in a single migration event. No backward compatibility, no gradual transition, no service uptime requirements. - -### **Day 1: Complete Infrastructure Replacement** - -**Breaking Changes Implemented:** -1. **Remove nginx-proxy completely** - no parallel operation -2. **Implement Traefik with full production configuration** -3. **Break all current networking** - implement 4-network isolation from scratch -4. **Remove ALL development port exposure** (10+ ports → 3 ports) -5. **Break environment variable patterns** - implement mandatory file-based configuration - -**Tasks:** -```bash -# 1. Backup current state -cp docker-compose.yml docker-compose.old.yml -docker compose down - -# 2. Create configuration structure -mkdir -p config/app config/platform secrets/app secrets/platform - -# 3. Generate production-ready certificates -make generate-certs # Multi-domain with mobile compatibility - -# 4. Implement new docker-compose.yml with: -# - 4 isolated networks -# - Traefik service with full middleware -# - No port exposure except Traefik (80, 443, 8080) -# - File-based configuration for all services -# - Resource limits on all services - -# 5. Update all service configurations to use file-based config -# - Remove all environment variables from compose -# - Implement CONFIG_PATH and SECRETS_DIR loaders -``` - -**Expected Failures**: Services will fail to start until configuration files are properly implemented. - -### **Day 2: Service Reconfiguration & Authentication** - -**Breaking Changes Implemented:** -1. **Mandatory service-to-service authentication** - remove all debug/open access -2. **Implement standardized health endpoints** - break existing health check patterns -3. **Enforce resource limits** - services may fail if exceeding limits -4. **Remove CORS development shortcuts** - production-only security - -**Tasks:** -```bash -# 1. Implement /health, /health/ready, /health/live on all HTTP services -# 2. Update Dockerfiles and service code for new health endpoints -# 3. Configure Traefik labels for all services -# 4. Implement service authentication: -# - API keys for platform service access -# - Remove debug modes and localhost CORS -# - Implement production security headers -# 5. Add resource limits to all services -# 6. Test new architecture end-to-end -``` - -**Expected Issues**: Authentication failures, CORS errors, resource limit violations. - -### **Day 3: Validation & Documentation Update** - -**Tasks:** -1. **Complete testing** of new architecture -2. **Update all documentation** to reflect new constraints -3. **Update Makefile** with breaking changes to commands -4. **Validate mobile access** with new certificate and routing -5. **Performance validation** (baseline not required - new architecture is target) - -### **BREAKING CHANGES SUMMARY** - -#### **Network Access** -- **OLD**: All services on default network with host access -- **NEW**: 4 isolated networks, no host access except Traefik - -#### **Port Exposure** -- **OLD**: 10+ ports exposed (databases, APIs, storage) -- **NEW**: Only 3 ports (80, 443, 8080) - everything through Traefik - -#### **Configuration** -- **OLD**: 35+ environment variables scattered across services -- **NEW**: Mandatory file-based configuration with no env fallbacks - -#### **Development Access** -- **OLD**: Direct database/service access via exposed ports -- **NEW**: Access only via `docker exec` or Traefik routing - -#### **Security** -- **OLD**: Debug modes, open CORS, no authentication -- **NEW**: Production security only, mandatory authentication - -#### **Resource Management** -- **OLD**: Unlimited resource consumption -- **NEW**: Enforced limits on all services - -### **Risk Mitigation** - -1. **Document current working state** before migration (Day 0) -2. **Keep docker-compose.old.yml** for reference -3. **Backup all volumes** before starting -4. **Expect multiple restart cycles** during configuration -5. **Plan for debugging time** - new constraints will reveal issues - -### **Success Criteria (Non-Negotiable)** -- āœ… All 11 services operational through Traefik only -- āœ… Zero host port exposure except Traefik -- āœ… All configuration file-based -- āœ… Service-to-service authentication working -- āœ… Mobile and desktop HTTPS access functional -- āœ… Resource limits enforced and services stable - -## Development Workflow Enhancements (BREAKING CHANGES) - -### Updated Makefile Commands (BREAKING CHANGES) - -**BREAKING CHANGE**: All database and service direct access removed. New K8s-equivalent workflow only. - -**Core Commands (Updated for New Architecture):** -```makefile -SHELL := /bin/bash - -# Traefik specific commands -traefik-dashboard: - @echo "Traefik dashboard: http://localhost:8080" - @echo "Add to /etc/hosts: 127.0.0.1 traefik.motovaultpro.local" - -traefik-logs: - @docker compose logs -f traefik - -service-discovery: - @echo "Discovered services and routes:" - @docker compose exec traefik curl -sf http://localhost:8080/api/rawdata | jq '.http.services, .http.routers' 2>/dev/null || docker compose exec traefik curl -sf http://localhost:8080/api/rawdata - -network-inspect: - @echo "Network topology:" - @docker network ls --filter name=motovaultpro - @docker network inspect motovaultpro_frontend motovaultpro_backend motovaultpro_database motovaultpro_platform motovaultpro_egress 2>/dev/null | jq '.[].Name, .[].Containers' || echo "Networks not yet created" - -health-check-all: - @echo "Checking health of all services..." - @docker compose ps --format "table {{.Service}}\t{{.Status}}\t{{.Health}}" - -# Mobile testing support -mobile-setup: - @echo "Mobile Testing Setup:" - @echo "1. Connect mobile device to same network as development machine" - @echo "2. Find development machine IP: $$(hostname -I | awk '{print $$1}')" - @echo "3. Add to mobile device hosts file (if rooted) or use IP directly:" - @echo " $$(hostname -I | awk '{print $$1}') motovaultpro.com" - @echo " $$(hostname -I | awk '{print $$1}') admin.motovaultpro.com" - @echo "4. Install certificate from: https://$$(hostname -I | awk '{print $$1}')/certs/motovaultpro.com.crt" - @echo "5. Trust certificate in device settings" - -# Development database access -db-admin: - @echo "Database Access:" - @echo "Application DB: postgresql://postgres:localdev123@localhost:5432/motovaultpro" - @echo "Platform DB: postgresql://platform_user:platform123@localhost:5434/platform" - @echo "Vehicles DB: postgresql://mvp_platform_user:platform123@localhost:5433/vehicles" - -db-shell-app: - @docker compose exec admin-postgres psql -U postgres -d motovaultpro - -db-shell-platform: - @docker compose exec platform-postgres psql -U platform_user -d platform - -db-shell-vehicles: - @docker compose exec mvp-platform-vehicles-db psql -U mvp_platform_user -d vehicles - -# Enhanced existing commands (preserve ETL removal) -logs: - @echo "Available log targets: all, traefik, backend, frontend, platform, vehicles-api, tenants" - @docker compose logs -f $(filter-out $@,$(MAKECMDGOALS)) - -# Remove ETL commands -# etl-load-manual, etl-load-clear, etl-validate-json, etl-shell - REMOVED (out of scope) - -%: - @: # This catches the log target argument -``` - -**Updated Core Commands:** -```makefile -setup: - @echo "Setting up MotoVaultPro K8s-ready development environment..." - @echo "1. Checking configuration files..." - @if [ ! -d config ]; then echo "Creating config directory structure..."; mkdir -p config/app config/platform secrets/app secrets/platform; fi - @echo "2. Checking SSL certificates..." - @if [ ! -f certs/motovaultpro.com.crt ]; then echo "Generating multi-domain SSL certificate..."; $(MAKE) generate-certs; fi - @echo "3. Building and starting all containers..." - @docker compose up -d --build --remove-orphans - @echo "4. Running database migrations..." - @sleep 15 # Wait for databases to be ready - @docker compose exec admin-backend node dist/_system/migrations/run-all.js - @echo "" - @echo "āœ… K8s-ready setup complete!" - @echo "Access application at: https://admin.motovaultpro.com" - @echo "Access platform landing at: https://motovaultpro.com" - @echo "Traefik dashboard: http://localhost:8080" - @echo "Mobile setup: make mobile-setup" - -generate-certs: - @echo "Generating multi-domain SSL certificate for mobile compatibility..." - @mkdir -p certs - @openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ - -keyout certs/motovaultpro.com.key \ - -out certs/motovaultpro.com.crt \ - -config <(echo '[dn]'; echo 'CN=motovaultpro.com'; echo '[req]'; echo 'distinguished_name = dn'; echo '[SAN]'; echo 'subjectAltName=DNS:motovaultpro.com,DNS:admin.motovaultpro.com,DNS:*.motovaultpro.com,IP:127.0.0.1') \ - -extensions SAN - @echo "Certificate generated with SAN for mobile compatibility" - -# New K8s-equivalent access patterns -db-access: - @echo "🚫 BREAKING CHANGE: No direct port access" - @echo "Database access via container exec only:" - @echo " Application DB: make db-shell-app" - @echo " Platform DB: make db-shell-platform" - @echo " Vehicles DB: make db-shell-vehicles" - -# Service inspection (K8s equivalent) -service-status: - @echo "Service health status:" - @docker compose ps --format "table {{.Service}}\\t{{.Status}}\\t{{.Health}}" - -traefik-dashboard: - @echo "Traefik Dashboard: http://localhost:8080" - -# Mobile testing (updated for new architecture) -mobile-setup: - @echo "šŸ“± Mobile Testing Setup (New Architecture):" - @echo "1. Connect mobile device to same network" - @echo "2. Development machine IP: $$(hostname -I | awk '{print $$1}')" - @echo "3. Add DNS: $$(hostname -I | awk '{print $$1}') motovaultpro.com admin.motovaultpro.com" - @echo "4. Trust certificate and access: https://admin.motovaultpro.com" - -# REMOVED COMMANDS (Breaking changes): -# āŒ All direct port access commands -# āŒ ETL commands (out of scope) -# āŒ Development shortcuts -``` - -### **BREAKING CHANGES TO DEVELOPMENT WORKFLOW** - -#### **Database Access** -- **OLD**: `psql -h localhost -p 5432` (direct connection) -- **NEW**: `make db-shell-app` (container exec only) - -#### **Service Debugging** -- **OLD**: `curl http://localhost:8000/health` (direct port) -- **NEW**: `curl https://admin.motovaultpro.com/api/platform/vehicles/health` (via Traefik) - -#### **Storage Access** -- **OLD**: MinIO console at `http://localhost:9001` -- **NEW**: Access via Traefik routing only - -### Enhanced Development Features (Updated) - -**Service Discovery Dashboard** -- Real-time service status -- Route configuration visualization -- Health check monitoring -- Request tracing - -**Debugging Tools** -- Network topology inspection -- Service dependency mapping -- Configuration validation -- Performance metrics - -**Testing Enhancements** -- Automated health checks across all services -- Service integration testing with network isolation -- Load balancing validation through Traefik -- SSL certificate verification for desktop and mobile -- Mobile device testing workflow validation -- Cross-network service communication testing - -## Observability & Monitoring - -### Metrics Collection - -```yaml -# Add to traefik configuration -metrics: - prometheus: - addEntryPointsLabels: true - addServicesLabels: true - addRoutersLabels: true -``` - -### Logging Strategy - -**Centralized Logging** -- All services log to stdout/stderr -- Traefik access logs -- Service health check logs -- Application performance logs - -**Log Levels** -- `ERROR`: Critical issues requiring attention -- `WARN`: Potential issues or degraded performance -- `INFO`: Normal operational messages -- `DEBUG`: Detailed diagnostic information (dev only) - -### Health Monitoring - -**Service Health Dashboard** -- Real-time service status via Traefik dashboard -- Historical health trends (Phase 4 enhancement) -- Network connectivity validation -- Mobile accessibility monitoring - -**Critical Monitoring Points:** -1. **Service Discovery**: All services registered with Traefik -2. **Network Isolation**: Services only accessible via designated networks -3. **SSL Certificate Status**: Valid certificates for all domains -4. **Mobile Compatibility**: Certificate trust and network accessibility -5. **Database Connectivity**: Cross-network database access patterns -6. **Platform API Authentication**: Service-to-service authentication working - -**Development Health Checks:** -```bash -# Quick health validation -make health-check-all -make service-discovery -make network-inspect - -# Mobile testing validation -make mobile-setup -curl -k https://admin.motovaultpro.com/health # From mobile device IP -``` - -**Service Health Dashboard** -- Real-time service status -- Historical health trends -- Alert notifications -- Performance metrics - -## Security Enhancements - -### Network Security - -**Network Isolation** -- Frontend network: Public-facing services only -- Backend network: API services with restricted access -- Database network: Data services with no external access -- Platform network: Microservices internal communication - -**Access Control** -- Traefik middleware for authentication -- Service-to-service authentication -- Network-level access restrictions -- SSL/TLS encryption for all traffic - -### Secret Management - -**Secrets Rotation** -- Database passwords -- API keys -- SSL certificates -- Auth0 client secrets - -**Access Policies** -- Least privilege principle -- Service-specific secret access -- Audit logging for secret access -- Encrypted secret storage - -## Testing Strategy - -### Automated Testing - -**Integration Tests** -- Service discovery validation -- Health check verification -- SSL certificate testing -- Load balancing functionality - -**Performance Tests** -- Service response times -- Network latency measurement -- Resource utilization monitoring -- Concurrent user simulation - -**Security Tests** -- Network isolation verification -- Authentication middleware testing -- SSL/TLS configuration validation -- Secret management verification - -### Manual Testing Procedures - -**Development Workflow** -1. Service startup validation -2. Route accessibility testing -3. Mobile/desktop compatibility -4. Feature functionality verification -5. Performance benchmarking - -**Deployment Validation** -1. Service discovery verification -2. Health check validation -3. SSL certificate functionality -4. Load balancing behavior -5. Failover testing - -## Migration Rollback Plan - -### Rollback Triggers - -- Service discovery failures -- Performance degradation > 20% -- SSL certificate issues -- Health check failures -- Mobile/desktop compatibility issues - -### Rollback Procedure - -1. **Immediate**: Switch DNS to backup nginx configuration -2. **Quick**: Restore docker-compose.yml.backup -3. **Complete**: Revert all configuration changes -4. **Verify**: Run full test suite -5. **Monitor**: Ensure service stability - -### Backup Strategy - -**Critical Data Backup:** -- Backup platform services PostgreSQL database: - ```bash - docker compose exec platform-postgres pg_dump -U platform_user platform > platform_backup_$(date +%Y%m%d_%H%M%S).sql - ``` - -**Note:** All other services are stateless or use development data that can be recreated. Application database, Redis, and MinIO contain only development data. - -## Success Metrics - -### Performance Metrics - -- **Service Startup Time**: < 30 seconds for all services -- **Request Response Time**: < 500ms for API calls -- **Health Check Response**: < 2 seconds -- **SSL Handshake Time**: < 1 second - -### Reliability Metrics - -- **Service Availability**: 99.9% uptime -- **Health Check Success Rate**: > 98% -- **Service Discovery Accuracy**: 100% -- **Failover Time**: < 10 seconds - -### Development Experience Metrics - -- **Development Setup Time**: < 5 minutes -- **Service Debug Time**: < 2 minutes to identify issues -- **Configuration Change Deployment**: < 1 minute -- **Test Suite Execution**: < 10 minutes - -## Post-Migration Benefits - -### Immediate Benefits - -1. **Enhanced Observability**: Real-time service monitoring and debugging -2. **Improved Security**: Network segmentation and middleware protection -3. **Better Development Experience**: Automatic service discovery and routing -4. **Simplified Configuration**: Centralized configuration management -5. **K8s Preparation**: Architecture closely mirrors Kubernetes patterns - -### Long-term Benefits - -1. **Easier K8s Migration**: Direct translation to Kubernetes manifests -2. **Better Scalability**: Load balancing and resource management -3. **Improved Maintainability**: Standardized configuration patterns -4. **Enhanced Monitoring**: Built-in metrics and health monitoring -5. **Professional Development Environment**: Production-like local setup - -## Conclusion - -This aggressive redesign completely replaces the Docker Compose architecture with a production-ready K8s-equivalent setup in 2-3 days. **Breaking changes are the strategy** - eliminating all development shortcuts and implementing true production constraints from day one. - -### **Key Transformation** -- **11 services** migrated from single-network to 4-network isolation -- **10+ exposed ports** reduced to 3 (Traefik only) -- **35+ environment variables** replaced with mandatory file-based configuration -- **All development bypasses removed** - production security enforced -- **Direct service access eliminated** - all traffic through Traefik - -### **Benefits of Aggressive Approach** -1. **Faster Implementation**: 2-3 days vs 4 weeks of gradual migration -2. **Authentic K8s Simulation**: True production constraints from start -3. **No Legacy Debt**: Clean architecture without compatibility layers -4. **Better Security**: Production-only mode eliminates development vulnerabilities -5. **Simplified Testing**: Single target architecture instead of multiple transition states - -### **Post-Migration State** -The new architecture provides an exact Docker Compose equivalent of Kubernetes deployment patterns. All services operate under production constraints with proper isolation, authentication, and resource management. This setup can be directly translated to Kubernetes manifests with minimal changes. - -**Development teams gain production-like experience while maintaining local development efficiency through container-based workflows and Traefik-based service discovery.** diff --git a/docs/changes/K8S-STATUS.md b/docs/changes/K8S-STATUS.md deleted file mode 100644 index 412d156..0000000 --- a/docs/changes/K8S-STATUS.md +++ /dev/null @@ -1,442 +0,0 @@ -# Kubernetes-like Docker Compose Migration Status - -## Project Overview -Migrating MotoVaultPro's Docker Compose architecture to closely replicate a Kubernetes deployment pattern while maintaining all current functionality and improving development experience. - -## Migration Plan Summary -- **Phase 1**: Infrastructure Foundation (Network segmentation + Traefik) -- **Phase 2**: Service Discovery & Labels -- **Phase 3**: Configuration Management (Configs + Secrets) -- **Phase 4**: Optimization & Documentation - ---- - -## Current Architecture Analysis āœ… COMPLETED - -### Existing Services (17 containers total) - -**MVP Platform Services (Microservices) - 7 services:** -- `mvp-platform-landing` - Marketing/landing page (nginx) -- `mvp-platform-tenants` - Multi-tenant management API (FastAPI, port 8001) -- `mvp-platform-vehicles-api` - Vehicle data API (FastAPI, port 8000) -- `mvp-platform-vehicles-etl` - Data processing pipeline (Python) -- `mvp-platform-vehicles-etl-manual` - Manual ETL container (profile: manual) -- `mvp-platform-vehicles-db` - Vehicle data storage (PostgreSQL, port 5433) -- `mvp-platform-vehicles-redis` - Vehicle data cache (Redis, port 6380) -- `mvp-platform-vehicles-mssql` - Monthly ETL source (SQL Server, port 1433, profile: mssql-monthly) - -**Application Services (Modular Monolith) - 5 services:** -- `admin-backend` - Application API with feature capsules (Node.js, port 3001) -- `admin-frontend` - React SPA (nginx) -- `admin-postgres` - Application database (PostgreSQL, port 5432) -- `admin-redis` - Application cache (Redis, port 6379) -- `admin-minio` - Object storage (MinIO, ports 9000/9001) - -**Infrastructure - 3 services:** -- `nginx-proxy` - Load balancer and SSL termination (ports 80/443) -- `platform-postgres` - Platform services database (PostgreSQL, port 5434) -- `platform-redis` - Platform services cache (Redis, port 6381) - -### Current Limitations Identified -1. **Single Network**: All services on default network (no segmentation) -2. **Manual Routing**: nginx configuration requires manual updates for new services -3. **Port Exposure**: Many services expose ports directly to host -4. **Configuration**: Environment variables scattered across services -5. **Service Discovery**: Hard-coded service names in configurations -6. **Observability**: Limited monitoring and debugging capabilities - ---- - -## Phase 1: Infrastructure Foundation āœ… COMPLETED - -### Objectives -- āœ… Analyze current docker-compose.yml structure -- āœ… Implement network segmentation (frontend, backend, database, platform) -- āœ… Add Traefik service with basic configuration -- āœ… Create Traefik config files structure -- āœ… Migrate nginx routing to Traefik labels -- āœ… Test SSL certificate handling -- āœ… Verify all existing functionality - -### Completed Network Architecture -``` -frontend - Public-facing services (traefik, admin-frontend, mvp-platform-landing) -backend - API services (admin-backend, mvp-platform-tenants, mvp-platform-vehicles-api) -database - Data persistence (all PostgreSQL, Redis, MinIO, MSSQL) -platform - Platform microservices internal communication -``` - -### Implemented Service Placement -| Network | Services | Purpose | K8s Equivalent | -|---------|----------|---------|----------------| -| `frontend` | traefik, admin-frontend, mvp-platform-landing | Public-facing | Public LoadBalancer | -| `backend` | admin-backend, mvp-platform-tenants, mvp-platform-vehicles-api | API services | ClusterIP services | -| `database` | All PostgreSQL, Redis, MinIO, MSSQL | Data persistence | StatefulSets with PVCs | -| `platform` | Platform microservices communication | Internal service mesh | Service mesh networking | - -### Phase 1 Achievements -- āœ… **Architecture Analysis**: Analyzed existing 17-container architecture -- āœ… **Network Segmentation**: Implemented 4-tier network architecture -- āœ… **Traefik Setup**: Deployed Traefik v3.0 with production-ready configuration -- āœ… **Service Discovery**: Converted all nginx routing to Traefik labels -- āœ… **Configuration Management**: Created structured config/ directory -- āœ… **Resource Management**: Added resource limits and restart policies -- āœ… **Enhanced Makefile**: Added Traefik-specific development commands -- āœ… **YAML Validation**: Validated docker-compose.yml syntax - -### Key Architectural Changes -1. **Removed nginx-proxy service** - Replaced with Traefik -2. **Added 4 isolated networks** - Mirrors K8s network policies -3. **Implemented service discovery** - Label-based routing like K8s Ingress -4. **Added resource management** - Prepares for K8s resource quotas -5. **Enhanced health checks** - Aligns with K8s readiness/liveness probes -6. **Configuration externalization** - Prepares for K8s ConfigMaps/Secrets - -### New Development Commands -```bash -make traefik-dashboard # View Traefik service discovery dashboard -make traefik-logs # Monitor Traefik access logs -make service-discovery # List discovered services -make network-inspect # Inspect network topology -make health-check-all # Check health of all services -``` - ---- - -## Phase 2: Service Discovery & Labels šŸ”„ PENDING - -### Objectives -- Convert all services to label-based discovery -- Implement security middleware -- Add service health monitoring -- Test service discovery and failover -- Implement Traefik dashboard access - ---- - ---- - -## Phase 3: Configuration Management āœ… COMPLETED - -### Objectives Achieved -- āœ… File-based configuration management (K8s ConfigMaps equivalent) -- āœ… Secrets management system (K8s Secrets equivalent) -- āœ… Configuration validation and hot reloading capabilities -- āœ… Environment standardization across services -- āœ… Enhanced configuration management tooling - -### Phase 3 Implementation Results āœ… - -**File-Based Configuration (K8s ConfigMaps Equivalent):** -- āœ… **Configuration Structure**: Organized config/ directory with app, platform, shared configs -- āœ… **YAML Configuration Files**: production.yml files for each service layer -- āœ… **Configuration Loading**: Services load config from mounted files instead of environment variables -- āœ… **Hot Reloading**: Configuration changes apply without rebuilding containers -- āœ… **Validation Tools**: Comprehensive YAML syntax and structure validation - -**Secrets Management (K8s Secrets Equivalent):** -- āœ… **Individual Secret Files**: Each secret in separate file (postgres-password.txt, api-keys, etc.) -- āœ… **Secure Mounting**: Secrets mounted as read-only files into containers -- āœ… **Template Generation**: Automated secret setup scripts for development -- āœ… **Git Security**: .gitignore protection prevents secret commits -- āœ… **Validation Checks**: Ensures all required secrets are present and non-empty - -**Configuration Architecture:** -``` -config/ -ā”œā”€ā”€ app/production.yml # Application configuration -ā”œā”€ā”€ platform/production.yml # Platform services configuration -ā”œā”€ā”€ shared/production.yml # Shared global configuration -└── traefik/ # Traefik-specific configs - -secrets/ -ā”œā”€ā”€ app/ # Application secrets -│ ā”œā”€ā”€ postgres-password.txt -│ ā”œā”€ā”€ minio-access-key.txt -│ └── [8 other secret files] -└── platform/ # Platform secrets - ā”œā”€ā”€ platform-db-password.txt - ā”œā”€ā”€ vehicles-api-key.txt - └── [3 other secret files] -``` - -**Service Configuration Conversion:** -- āœ… **admin-backend**: Converted to file-based configuration loading -- āœ… **Environment Simplification**: Reduced environment variables by 80% -- āœ… **Secret File Loading**: Services read secrets from /run/secrets/ mount -- āœ… **Configuration Precedence**: Files override environment defaults - -**Enhanced Development Commands:** -```bash -make config-validate # Validate all configuration files and secrets -make config-status # Show configuration management status -make deploy-with-config # Deploy services with validated configuration -make config-reload # Hot-reload configuration without restart -make config-backup # Backup current configuration -make config-diff # Show configuration changes from defaults -``` - -**Configuration Validation Results:** -``` -Configuration Files: 4/4 valid YAML files -Required Secrets: 11/11 application secrets present -Platform Secrets: 5/5 platform secrets present -Docker Compose: Valid configuration with proper mounts -Validation Status: āœ… All validations passed! -``` - -**Phase 3 Achievements:** -- šŸ“ **Configuration Management**: K8s ConfigMaps equivalent with file-based config -- šŸ” **Secrets Management**: K8s Secrets equivalent with individual secret files -- āœ… **Validation Tooling**: Comprehensive configuration and secret validation -- šŸ”„ **Hot Reloading**: Configuration changes without container rebuilds -- šŸ› ļø **Development Tools**: Enhanced Makefile commands for config management -- šŸ“‹ **Template Generation**: Automated secret setup for development environments - -**Production Readiness Status (Phase 3):** -- āœ… Configuration: File-based management with validation -- āœ… Secrets: Secure mounting and management -- āœ… Validation: Comprehensive checks before deployment -- āœ… Documentation: Configuration templates and examples -- āœ… Developer Experience: Simplified configuration workflow - ---- - -## Phase 4: Optimization & Documentation āœ… COMPLETED - -### Objectives Achieved -- āœ… Optimize resource allocation based on actual usage patterns -- āœ… Implement comprehensive performance monitoring setup -- āœ… Standardize configuration across all platform services -- āœ… Create production-ready monitoring and alerting system -- āœ… Establish performance baselines and capacity planning tools - -### Phase 4 Implementation Results āœ… - -**Resource Optimization (K8s ResourceQuotas Equivalent):** -- āœ… **Usage Analysis**: Real-time resource usage monitoring and optimization recommendations -- āœ… **Right-sizing**: Adjusted memory limits based on actual consumption patterns -- āœ… **CPU Optimization**: Reduced CPU allocations for low-utilization services -- āœ… **Baseline Performance**: Established performance metrics for all services -- āœ… **Capacity Planning**: Tools for predicting resource needs and scaling requirements - -**Comprehensive Monitoring (K8s Observability Stack Equivalent):** -- āœ… **Prometheus Configuration**: Complete metrics collection setup for all services -- āœ… **Service Health Alerts**: K8s PrometheusRule equivalent with critical alerts -- āœ… **Performance Baselines**: Automated response time and database connection monitoring -- āœ… **Resource Monitoring**: Container CPU/memory usage tracking and alerting -- āœ… **Infrastructure Monitoring**: Traefik, database, and Redis metrics collection - -**Configuration Standardization:** -- āœ… **Platform Services**: All platform services converted to file-based configuration -- āœ… **Secrets Management**: Standardized secrets mounting across all services -- āœ… **Environment Consistency**: Unified configuration patterns for all service types -- āœ… **Configuration Validation**: Comprehensive validation for all service configurations - -**Performance Metrics (Current Baseline):** -``` -Service Response Times: - Admin Frontend: 0.089s - Platform Landing: 0.026s - Vehicles API: 0.026s - Tenants API: 0.029s - -Resource Utilization: - Memory Usage: 2-12% of allocated limits - CPU Usage: 0.1-10% average utilization - Database Connections: 1 active per database - Network Isolation: 4 isolated networks operational -``` - -**Enhanced Development Commands:** -```bash -make resource-optimization # Analyze resource usage and recommendations -make performance-baseline # Measure service response times and DB connections -make monitoring-setup # Configure Prometheus monitoring stack -make deploy-with-monitoring # Deploy with enhanced monitoring enabled -make metrics-dashboard # Access Traefik and service metrics -make capacity-planning # Analyze deployment footprint and efficiency -``` - -**Monitoring Architecture:** -- šŸ“Š **Prometheus Config**: Complete scrape configuration for all services -- 🚨 **Alert Rules**: Service health, database, resource usage, and Traefik alerts -- šŸ“ˆ **Metrics Collection**: 15s intervals for critical services, 60s for infrastructure -- šŸ” **Health Checks**: K8s-equivalent readiness, liveness, and startup probes -- šŸ“‹ **Dashboard Access**: Real-time metrics via Traefik dashboard and API - -**Phase 4 Achievements:** -- šŸŽÆ **Resource Efficiency**: Optimized allocation based on actual usage patterns -- šŸ“Š **Production Monitoring**: Complete observability stack with alerting -- ⚔ **Performance Baselines**: Established response time and resource benchmarks -- šŸ”§ **Development Tools**: Enhanced Makefile commands for optimization and monitoring -- šŸ“ˆ **Capacity Planning**: Tools for scaling and resource management decisions -- āœ… **Configuration Consistency**: All services standardized on file-based configuration - -**Production Readiness Status (Phase 4):** -- āœ… Resource Management: Optimized allocation with monitoring -- āœ… Observability: Complete metrics collection and alerting -- āœ… Performance: Baseline established with monitoring -- āœ… Configuration: Standardized across all services -- āœ… Development Experience: Enhanced tooling and monitoring commands - ---- - -## Key Migration Principles - -### Kubernetes Preparation Focus -- Network segmentation mirrors K8s namespaces/network policies -- Traefik labels translate directly to K8s Ingress resources -- Docker configs/secrets prepare for K8s ConfigMaps/Secrets -- Health checks align with K8s readiness/liveness probes -- Resource limits prepare for K8s resource quotas - -### No Backward Compatibility Required -- Complete architectural redesign permitted -- Service uptime not required during migration -- Breaking changes acceptable for better K8s alignment - -### Development Experience Goals -- Automatic service discovery -- Enhanced observability and debugging -- Simplified configuration management -- Professional development environment matching production patterns - ---- - -## Next Steps -1. Create network segmentation in docker-compose.yml -2. Add Traefik service configuration -3. Create config/ directory structure for Traefik -4. Begin migration of nginx routing to Traefik labels - -### Phase 1 Validation Results āœ… -- āœ… **Docker Compose Syntax**: Valid configuration with no errors -- āœ… **Network Creation**: All 4 networks (frontend, backend, database, platform) created successfully -- āœ… **Traefik Service**: Successfully deployed and started with proper health checks -- āœ… **Service Discovery**: Docker provider configured and operational -- āœ… **Configuration Structure**: All config files created and validated -- āœ… **Makefile Integration**: Enhanced with new Traefik-specific commands - -### Migration Impact Assessment -- **Service Count**: Maintained 14 core services (removed nginx-proxy, added traefik) -- **Port Exposure**: Reduced external port exposure, only development access ports retained -- **Network Security**: Implemented network isolation with internal-only networks -- **Resource Management**: Added memory and CPU limits to all services -- **Development Experience**: Enhanced with service discovery dashboard and debugging tools - -**Current Status**: Phase 4 COMPLETED successfully āœ… -**Implementation Status**: LIVE - Complete K8s-equivalent architecture with full observability -**Migration Status**: ALL PHASES COMPLETED - Production-ready K8s-equivalent deployment -**Overall Progress**: 100% of 4-phase migration plan completed - -### Phase 1 Implementation Results āœ… - -**Successfully Migrated:** -- āœ… **Complete Architecture Replacement**: Old nginx-proxy removed, Traefik v3.0 deployed -- āœ… **4-Tier Network Segmentation**: frontend, backend, database, platform networks operational -- āœ… **Service Discovery**: All 11 core services discoverable via Traefik labels -- āœ… **Resource Management**: Memory and CPU limits applied to all services -- āœ… **Port Isolation**: Only Traefik ports (80, 443, 8080) + development DB access exposed -- āœ… **Production Security**: DEBUG=false, production CORS, authentication middleware ready - -**Service Status Summary:** -``` -Services: 12 total (11 core + Traefik) -Healthy: 11/12 services (92% operational) -Networks: 4 isolated networks created -Routes: 5 active Traefik routes discovered -API Status: Traefik dashboard and API operational (HTTP 200) -``` - -**Breaking Changes Successfully Implemented:** -- āŒ **nginx-proxy**: Completely removed -- āŒ **Single default network**: Replaced with 4-tier isolation -- āŒ **Manual routing**: Replaced with automatic service discovery -- āŒ **Development bypasses**: Removed debug modes and open CORS -- āŒ **Unlimited resources**: All services now have limits - -**New Development Workflow:** -- `make service-discovery` - View discovered services and routes -- `make network-inspect` - Inspect 4-tier network architecture -- `make health-check-all` - Monitor service health -- `make traefik-dashboard` - Access service discovery dashboard -- `make mobile-setup` - Mobile testing instructions - -**Validation Results:** -- āœ… **Network Isolation**: 4 networks created with proper internal/external access -- āœ… **Service Discovery**: All services discoverable via Docker provider -- āœ… **Route Resolution**: All 5 application routes active -- āœ… **Health Monitoring**: 11/12 services healthy -- āœ… **Development Access**: Database shells accessible via container exec -- āœ… **Configuration Management**: Traefik config externalized and operational - ---- - -## Phase 2: Service Discovery & Labels āœ… COMPLETED - -### Objectives Achieved -- āœ… Advanced middleware implementation with production security -- āœ… Service-to-service authentication configuration -- āœ… Enhanced health monitoring with Prometheus metrics -- āœ… Comprehensive service discovery validation -- āœ… Network security isolation testing - -### Phase 2 Implementation Results āœ… - -**Advanced Security & Middleware:** -- āœ… **Production Security Headers**: Implemented comprehensive security middleware -- āœ… **Service Authentication**: Platform APIs secured with API keys and service tokens -- āœ… **Circuit Breakers**: Resilience patterns for service reliability -- āœ… **Rate Limiting**: Protection against abuse and DoS attacks -- āœ… **Request Compression**: Performance optimization for all routes - -**Enhanced Monitoring & Observability:** -- āœ… **Prometheus Metrics**: Full metrics collection for all services -- āœ… **Health Check Patterns**: K8s-equivalent readiness, liveness, and startup probes -- āœ… **Service Discovery Dashboard**: Real-time service and route monitoring -- āœ… **Network Security Testing**: Automated isolation validation -- āœ… **Performance Monitoring**: Response time and availability tracking - -**Service Authentication Matrix:** -``` -admin-backend ←→ mvp-platform-vehicles-api (API key: mvp-platform-vehicles-secret-key) -admin-backend ←→ mvp-platform-tenants (API key: mvp-platform-tenants-secret-key) -Services authenticate via X-API-Key headers and service tokens -``` - -**Enhanced Development Commands:** -```bash -make metrics # View Prometheus metrics and performance data -make service-auth-test # Test service-to-service authentication -make middleware-test # Validate security middleware configuration -make network-security-test # Test network isolation and connectivity -``` - -**Service Status Summary (Phase 2):** -``` -Services: 13 total (12 application + Traefik) -Healthy: 13/13 services (100% operational) -Networks: 4 isolated networks with security validation -Routes: 7 active routes with enhanced middleware -Metrics: Prometheus collection active -Authentication: Service-to-service security implemented -``` - -**Phase 2 Achievements:** -- šŸ” **Enhanced Security**: Production-grade middleware and authentication -- šŸ“Š **Comprehensive Monitoring**: Prometheus metrics and health checks -- šŸ›”ļø **Network Security**: Isolation testing and validation -- šŸ”„ **Service Resilience**: Circuit breakers and retry policies -- šŸ“ˆ **Performance Tracking**: Response time and availability monitoring - -**Known Issues (Non-Blocking):** -- File-based middleware loading requires Traefik configuration refinement -- Security headers currently applied via docker labels (functional alternative) - -**Production Readiness Status:** -- āœ… Security: Production-grade authentication and middleware -- āœ… Monitoring: Comprehensive metrics and health checks -- āœ… Reliability: Circuit breakers and resilience patterns -- āœ… Performance: Optimized routing with compression -- āœ… Observability: Real-time service discovery and monitoring \ No newline at end of file diff --git a/docs/changes/MULTI-TENANT-REDESIGN.md b/docs/changes/MULTI-TENANT-REDESIGN.md deleted file mode 100644 index 4702d2e..0000000 --- a/docs/changes/MULTI-TENANT-REDESIGN.md +++ /dev/null @@ -1,1185 +0,0 @@ -# Multi-Tenant SaaS Redesign Implementation Guide - -## Executive Summary - -This document provides step-by-step instructions for transforming MotoVaultPro from a single-user application into a subdomain-based multi-tenant SaaS platform. The current implementation will become the "admin" tenant with full platform access, while new tenants get isolated application stacks. - -## Architecture Overview - -### Current State -- Single-user application with user-scoped data isolation -- Microservices: Application service + MVP Platform Services -- Docker Compose development environment -- Auth0 authentication with user-level access - -### Target State -- Multi-tenant SaaS with subdomain routing -- **Admin tenant**: `admin.motovaultpro.com` (current implementation) -- **New tenants**: `{tenant-id}.motovaultpro.com` -- **Platform services**: Shared infrastructure across all tenants -- **Per-tenant databases**: Complete data isolation -- **Tenant-specific signup**: With admin approval workflow - -## Key Technical Specifications - -### Subdomain Architecture -- **Landing Page**: `motovaultpro.com` → Platform landing service -- **Admin Tenant**: `admin.motovaultpro.com` → Admin application stack -- **Regular Tenants**: `{tenant-id}.motovaultpro.com` → Tenant application stack -- **Platform Services**: Shared APIs accessible by all tenants - -### Tenant Identification -- **Admin Tenant ID**: `"admin"` (hardcoded identifier) -- **Regular Tenant IDs**: Alphanumeric slugs (e.g., `"acme-corp"`, `"demo-tenant"`) -- **Auth0 Storage**: `tenant_id` stored in user metadata -- **JWT Claims**: Include `tenant_id` for downstream service authentication - -### Database Strategy -- **Platform Database**: Shared tenant management + vehicle platform data -- **Per-Tenant Databases**: Separate PostgreSQL container per tenant -- **Schema**: Same application schema deployed to each tenant database -- **Migration**: Big bang transformation (breaking changes acceptable) - -## Implementation Plan - ---- - -## Phase 1: Multi-Tenant Foundation & Platform Services - -### 1.1 Create Tenant Management Platform Service - -**New Service**: `mvp-platform-tenants` - -**Directory Structure**: -``` -mvp-platform-services/tenants/ -ā”œā”€ā”€ api/ -│ ā”œā”€ā”€ main.py # FastAPI application -│ ā”œā”€ā”€ models/ # SQLAlchemy models -│ ā”œā”€ā”€ routes/ # API endpoints -│ └── services/ # Business logic -ā”œā”€ā”€ sql/ -│ └── schema/ -│ ā”œā”€ā”€ 001_tenants_schema.sql -│ └── 002_tenant_signups_schema.sql -ā”œā”€ā”€ docker/ -│ └── Dockerfile.api -└── README.md -``` - -**Database Schema** (`platform-postgres`): -```sql --- Tenant registry -CREATE TABLE tenants ( - id VARCHAR(100) PRIMARY KEY, -- 'admin', 'acme-corp', etc. - name VARCHAR(255) NOT NULL, -- Display name - subdomain VARCHAR(100) UNIQUE NOT NULL, -- Same as id for simplicity - status VARCHAR(50) DEFAULT 'active', -- active, pending, suspended - admin_user_id VARCHAR(255), -- Auth0 user ID of tenant admin - settings JSONB DEFAULT '{}', -- Tenant-specific configuration - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - --- Tenant signup approval workflow -CREATE TABLE tenant_signups ( - id SERIAL PRIMARY KEY, - tenant_id VARCHAR(100) REFERENCES tenants(id), - user_email VARCHAR(255) NOT NULL, - user_auth0_id VARCHAR(255), -- Auth0 user ID after signup - status VARCHAR(50) DEFAULT 'pending', -- pending, approved, rejected - requested_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - approved_by VARCHAR(255), -- Auth0 ID of approving admin - approved_at TIMESTAMP -); - --- Initial data -INSERT INTO tenants (id, name, subdomain, status, admin_user_id) -VALUES ('admin', 'Admin Tenant', 'admin', 'active', NULL); -``` - -**API Endpoints** (`/api/v1/tenants`): -``` -POST /tenants # Create new tenant (platform admin only) -GET /tenants # List all tenants (platform admin only) -GET /tenants/{tenant_id} # Get tenant details -PUT /tenants/{tenant_id} # Update tenant settings -DELETE /tenants/{tenant_id} # Suspend tenant - -POST /tenants/{tenant_id}/signups # Request signup approval -GET /tenants/{tenant_id}/signups # List pending signups (tenant admin only) -PUT /signups/{signup_id}/approve # Approve signup (tenant admin only) -PUT /signups/{signup_id}/reject # Reject signup (tenant admin only) -``` - -### 1.2 Create Landing Page Platform Service - -**New Service**: `mvp-platform-landing` - -**Directory Structure**: -``` -mvp-platform-services/landing/ -ā”œā”€ā”€ src/ -│ ā”œā”€ā”€ App.tsx -│ ā”œā”€ā”€ components/ -│ │ ā”œā”€ā”€ HomePage.tsx -│ │ ā”œā”€ā”€ TenantSignup.tsx -│ │ └── TenantLogin.tsx -│ ā”œā”€ā”€ services/ -│ │ ā”œā”€ā”€ auth0.ts -│ │ └── tenantService.ts -│ └── utils/ -│ └── routing.ts -ā”œā”€ā”€ public/ -ā”œā”€ā”€ package.json -ā”œā”€ā”€ Dockerfile -└── README.md -``` - -**Key Features**: -- **Homepage**: Marketing content at `motovaultpro.com` -- **Tenant Signup**: Forms at `motovaultpro.com/signup/{tenant-id}` -- **Auth0 Integration**: Tenant-aware authentication flow -- **Routing Logic**: Redirect authenticated users to their tenant subdomain - -**Signup Flow**: -1. User visits `motovaultpro.com/signup/{tenant-id}` -2. Landing page validates tenant exists and accepts signups -3. Auth0 signup with `tenant_id` stored in user metadata -4. User status set to "pending" until admin approval -5. Entry created in `tenant_signups` table -6. Tenant admin receives notification to approve/reject - -### 1.3 Update Docker Compose for Multi-Tenant Architecture - -**New `docker-compose.yml` Structure**: -```yaml -version: '3.8' - -services: - # Platform Services (Shared) - mvp-platform-tenants: - build: ./mvp-platform-services/tenants/docker/Dockerfile.api - environment: - - DATABASE_URL=postgresql://platform_user:${PLATFORM_DB_PASSWORD}@platform-postgres:5432/platform - depends_on: - - platform-postgres - ports: - - "8001:8000" - - mvp-platform-landing: - build: ./mvp-platform-services/landing - environment: - - REACT_APP_AUTH0_DOMAIN=${AUTH0_DOMAIN} - - REACT_APP_TENANTS_API_URL=http://mvp-platform-tenants:8000 - ports: - - "3002:3000" - - mvp-platform-vehicles-api: - # Existing service - no changes needed - - platform-postgres: - image: postgres:15 - environment: - POSTGRES_DB: platform - POSTGRES_USER: platform_user - POSTGRES_PASSWORD: ${PLATFORM_DB_PASSWORD} - volumes: - - ./mvp-platform-services/tenants/sql/schema:/docker-entrypoint-initdb.d - - platform_postgres_data:/var/lib/postgresql/data - ports: - - "5434:5432" - - platform-redis: - image: redis:7-alpine - ports: - - "6381:6379" - - # Admin Tenant (Current Implementation Renamed) - admin-backend: - build: ./backend - environment: - - TENANT_ID=admin - - DB_HOST=admin-postgres - - REDIS_URL=redis://admin-redis:6379 - - PLATFORM_TENANTS_API_URL=http://mvp-platform-tenants:8000 - depends_on: - - admin-postgres - - admin-redis - - admin-frontend: - build: ./frontend - environment: - - REACT_APP_API_BASE_URL=http://admin-backend:3001 - - REACT_APP_TENANT_ID=admin - depends_on: - - admin-backend - - admin-postgres: - # Existing postgres service renamed - - admin-redis: - # Existing redis service renamed - -volumes: - platform_postgres_data: - # Keep existing volumes, add tenant-specific volumes as needed -``` - ---- - -## Phase 2: Authentication & Tenant-Aware Auth0 - -### 2.1 Multi-Tenant Auth0 Configuration - -**Auth0 Application Settings**: -- **Allowed Callback URLs**: - - `http://localhost:3002/callback` (landing page development) - - `https://motovaultpro.com/callback` (landing page production) - - `http://admin.motovaultpro.local/callback` (admin tenant development) - - `https://admin.motovaultpro.com/callback` (admin tenant production) - - `http://{tenant-id}.motovaultpro.local/callback` (tenant development) - - `https://{tenant-id}.motovaultpro.com/callback` (tenant production) - -**Auth0 Rules for Tenant Context**: -```javascript -function addTenantToToken(user, context, callback) { - const namespace = 'https://motovaultpro.com/'; - - // Extract tenant from signup metadata or determine from email domain - let tenantId = user.user_metadata && user.user_metadata.tenant_id; - - // For existing users, default to admin tenant - if (!tenantId) { - tenantId = 'admin'; - } - - // Add tenant_id to JWT token - context.idToken[namespace + 'tenant_id'] = tenantId; - context.accessToken[namespace + 'tenant_id'] = tenantId; - - callback(null, user, context); -} -``` - -**User Metadata Schema**: -```json -{ - "user_metadata": { - "tenant_id": "admin", - "signup_status": "approved", - "approval_date": "2024-01-01T00:00:00Z" - } -} -``` - -### 2.2 Landing Page Auth0 Integration - -**Tenant-Specific Signup** (`src/components/TenantSignup.tsx`): -```typescript -interface TenantSignupProps { - tenantId: string; -} - -export const TenantSignup: React.FC = ({ tenantId }) => { - const { loginWithRedirect } = useAuth0(); - - const handleSignup = async () => { - await loginWithRedirect({ - screen_hint: 'signup', - redirectUri: `${window.location.origin}/callback`, - // Store tenant_id in Auth0 user metadata during signup - signup: { - user_metadata: { - tenant_id: tenantId, - signup_status: 'pending' - } - } - }); - }; - - return ( -
-

Sign up for {tenantId}

- -
- ); -}; -``` - -**Post-Authentication Routing** (`src/utils/routing.ts`): -```typescript -export const redirectToTenant = (user: Auth0User) => { - const tenantId = user['https://motovaultpro.com/tenant_id']; - const status = user.user_metadata?.signup_status; - - if (status === 'pending') { - // Show "approval pending" message - window.location.href = '/approval-pending'; - return; - } - - if (status !== 'approved') { - // Invalid user state - window.location.href = '/access-denied'; - return; - } - - // Redirect to tenant subdomain - const subdomain = tenantId === 'admin' ? 'admin' : tenantId; - window.location.href = `http://${subdomain}.motovaultpro.local`; -}; -``` - -### 2.3 Signup Approval Workflow - -**Admin Dashboard for Approval** (in admin tenant): -```typescript -// Add to admin frontend: src/features/tenant-management/ -export const SignupApprovals: React.FC = () => { - const [pendingSignups, setPendingSignups] = useState([]); - - const approveSignup = async (signupId: string) => { - await fetch(`/api/tenants/signups/${signupId}/approve`, { - method: 'PUT', - headers: { Authorization: `Bearer ${token}` } - }); - // Update Auth0 user metadata to approved - // Refresh list - }; - - return ( -
-

Pending Signups

- {pendingSignups.map(signup => ( -
-

{signup.user_email} wants to join {signup.tenant_id}

- -
- ))} -
- ); -}; -``` - ---- - -## Phase 3: Application Stack Tenant Transformation - -### 3.1 Add Tenant Context Throughout Backend - -**Tenant Detection Middleware** (`backend/src/core/middleware/tenant.ts`): -```typescript -import { FastifyRequest, FastifyReply, HookHandlerDoneFunction } from 'fastify'; - -export interface TenantRequest extends FastifyRequest { - tenantId: string; -} - -export const tenantMiddleware = ( - request: TenantRequest, - reply: FastifyReply, - done: HookHandlerDoneFunction -) => { - // Method 1: From environment variable (container-level) - const envTenantId = process.env.TENANT_ID; - - // Method 2: From JWT token claims - const jwtTenantId = request.user?.['https://motovaultpro.com/tenant_id']; - - // Method 3: From subdomain parsing (if needed) - const host = request.headers.host || ''; - const subdomain = host.split('.')[0]; - - // Priority: Environment > JWT > Subdomain - request.tenantId = envTenantId || jwtTenantId || subdomain || 'admin'; - - // Validate tenant exists - if (!isValidTenant(request.tenantId)) { - reply.code(403).send({ error: 'Invalid tenant' }); - return; - } - - done(); -}; - -const isValidTenant = (tenantId: string): boolean => { - // Query platform tenants service to validate - // For now, hardcode known tenants - return ['admin', 'demo-tenant', 'acme-corp'].includes(tenantId); -}; -``` - -**Environment-Based Tenant Configuration**: -```typescript -// backend/src/core/config/tenant.ts -export const getTenantConfig = () => { - const tenantId = process.env.TENANT_ID || 'admin'; - - return { - tenantId, - databaseUrl: process.env.DATABASE_URL || `postgresql://user:pass@${tenantId}-postgres:5432/${tenantId}`, - redisUrl: process.env.REDIS_URL || `redis://${tenantId}-redis:6379`, - platformServicesUrl: process.env.PLATFORM_SERVICES_URL || 'http://mvp-platform-tenants:8000', - isAdminTenant: tenantId === 'admin' - }; -}; -``` - -**Update Database Configuration** (`backend/src/core/config/database.ts`): -```typescript -import { Pool } from 'pg'; -import { getTenantConfig } from './tenant'; - -const tenantConfig = getTenantConfig(); - -export const pool = new Pool({ - connectionString: tenantConfig.databaseUrl, - // Connection pool per tenant database -}); - -// Update all queries to use tenant-specific database -// No changes needed to query logic since each tenant has separate DB -``` - -### 3.2 Convert Current Implementation to Admin Tenant - -**Step 1: Rename Existing Containers** -```yaml -# In docker-compose.yml, rename: -backend -> admin-backend -frontend -> admin-frontend -postgres -> admin-postgres -redis -> admin-redis -minio -> admin-minio -``` - -**Step 2: Update Environment Variables** -```yaml -admin-backend: - build: ./backend - environment: - - TENANT_ID=admin - - DATABASE_URL=postgresql://motovault_user:${DB_PASSWORD}@admin-postgres:5432/motovault - - REDIS_URL=redis://admin-redis:6379 - - PLATFORM_TENANTS_API_URL=http://mvp-platform-tenants:8000 - - PLATFORM_VEHICLES_API_URL=http://mvp-platform-vehicles-api:8000 - -admin-frontend: - build: ./frontend - environment: - - REACT_APP_API_BASE_URL=http://admin-backend:3001 - - REACT_APP_TENANT_ID=admin - - REACT_APP_AUTH0_DOMAIN=${AUTH0_DOMAIN} -``` - -**Step 3: Add Admin-Only Features** (`backend/src/features/tenant-management/`): -```typescript -// New feature capsule for tenant management (admin only) -export const tenantManagementRoutes = async (fastify: FastifyInstance) => { - // Middleware to ensure admin tenant only - fastify.addHook('preHandler', async (request: TenantRequest, reply) => { - if (request.tenantId !== 'admin') { - reply.code(403).send({ error: 'Admin access required' }); - return; - } - }); - - fastify.get('/api/admin/tenants', async (request, reply) => { - // List all tenants - }); - - fastify.post('/api/admin/tenants', async (request, reply) => { - // Create new tenant + provision infrastructure - }); - - fastify.get('/api/admin/signups', async (request, reply) => { - // List pending signups across all tenants - }); -}; -``` - -### 3.3 Per-Tenant Database Architecture - -**Tenant Provisioning Script** (`scripts/provision-tenant.sh`): -```bash -#!/bin/bash -TENANT_ID=$1 - -if [ -z "$TENANT_ID" ]; then - echo "Usage: $0 " - exit 1 -fi - -# Add tenant containers to docker-compose.yml -echo " - ${TENANT_ID}-backend: - build: ./backend - environment: - - TENANT_ID=${TENANT_ID} - - DATABASE_URL=postgresql://motovault_user:\${DB_PASSWORD}@${TENANT_ID}-postgres:5432/motovault - - REDIS_URL=redis://${TENANT_ID}-redis:6379 - depends_on: - - ${TENANT_ID}-postgres - - ${TENANT_ID}-redis - - ${TENANT_ID}-frontend: - build: ./frontend - environment: - - REACT_APP_API_BASE_URL=http://${TENANT_ID}-backend:3001 - - REACT_APP_TENANT_ID=${TENANT_ID} - depends_on: - - ${TENANT_ID}-backend - - ${TENANT_ID}-postgres: - image: postgres:15 - environment: - POSTGRES_DB: motovault - POSTGRES_USER: motovault_user - POSTGRES_PASSWORD: \${DB_PASSWORD} - volumes: - - ./backend/src/_system/migrations:/docker-entrypoint-initdb.d - - ${TENANT_ID}_postgres_data:/var/lib/postgresql/data - - ${TENANT_ID}-redis: - image: redis:7-alpine - volumes: - - ${TENANT_ID}_redis_data:/data -" >> docker-compose.yml - -# Add volumes -echo " - ${TENANT_ID}_postgres_data: - ${TENANT_ID}_redis_data: -" >> docker-compose.yml - -echo "Tenant ${TENANT_ID} provisioned. Run 'docker compose up -d' to start." -``` - -**Automated Migration Deployment**: -```typescript -// backend/src/_system/migrations/run-all.ts - no changes needed -// Each tenant database gets the same schema deployed via init scripts -// Migrations run independently per tenant database -``` - ---- - -## Phase 4: Platform Services Multi-Tenant Support - -### 4.1 Update Platform Vehicles Service - -**Add Tenant Context** (`mvp-platform-services/vehicles/api/main.py`): -```python -from fastapi import Header, HTTPException - -async def get_tenant_context( - authorization: str = Header(), - x_tenant_id: str = Header(None) -): - # Extract tenant from JWT or header - tenant_id = x_tenant_id or extract_tenant_from_jwt(authorization) - - if not tenant_id: - raise HTTPException(status_code=403, detail="Tenant context required") - - return { - "tenant_id": tenant_id, - "is_admin": tenant_id == "admin" - } - -@app.get("/api/v1/vehicles/makes") -async def get_makes( - year: int, - tenant_context: dict = Depends(get_tenant_context) -): - # Admin tenant gets all data, regular tenants get standard data - # For vehicle platform data, this is mostly the same - # But we can add tenant-specific caching or filtering if needed - - cache_key = f"makes:{year}:{tenant_context['tenant_id']}" - # ... rest of endpoint logic -``` - -**Service-to-Service Authentication**: -```typescript -// backend/src/features/vehicles/external/platform-vehicles/platform-vehicles.client.ts -export class PlatformVehiclesClient { - constructor( - private readonly baseUrl: string, - private readonly apiKey: string, - private readonly tenantId: string - ) {} - - async getVehicleMakes(year: number) { - const response = await axios.get(`${this.baseUrl}/api/v1/vehicles/makes`, { - headers: { - 'Authorization': `Bearer ${this.apiKey}`, - 'X-Tenant-ID': this.tenantId - }, - params: { year } - }); - - return response.data; - } -} -``` - -### 4.2 Tenant Management APIs - -**Platform Tenants Service Routes** (`mvp-platform-services/tenants/api/routes/tenants.py`): -```python -@router.post("/tenants") -async def create_tenant( - tenant_data: TenantCreate, - current_user: dict = Depends(get_admin_user) -): - # Create tenant record - tenant = await tenant_service.create_tenant(tenant_data) - - # Provision infrastructure (Docker containers) - await provisioning_service.provision_tenant(tenant.id) - - return tenant - -@router.get("/tenants/{tenant_id}/signups") -async def get_tenant_signups( - tenant_id: str, - current_user: dict = Depends(get_tenant_admin) -): - # List pending signups for this tenant - return await signup_service.get_pending_signups(tenant_id) - -@router.put("/signups/{signup_id}/approve") -async def approve_signup( - signup_id: int, - current_user: dict = Depends(get_tenant_admin) -): - # Approve signup and update Auth0 user metadata - await signup_service.approve_signup(signup_id, current_user['sub']) - await auth0_service.update_user_metadata(signup_id, {"signup_status": "approved"}) - - return {"status": "approved"} -``` - ---- - -## Phase 5: Docker Compose Multi-Tenant Deployment - -### 5.1 Complete Multi-Tenant Docker Compose - -**Production-Ready `docker-compose.yml`**: -```yaml -version: '3.8' - -services: - # Reverse Proxy for Subdomain Routing - nginx: - image: nginx:alpine - ports: - - "80:80" - - "443:443" - volumes: - - ./nginx.conf:/etc/nginx/nginx.conf - - ./certs:/etc/nginx/certs - depends_on: - - mvp-platform-landing - - admin-frontend - - # Platform Services (Shared Infrastructure) - mvp-platform-landing: - build: ./mvp-platform-services/landing - environment: - - REACT_APP_AUTH0_DOMAIN=${AUTH0_DOMAIN} - - REACT_APP_AUTH0_CLIENT_ID=${AUTH0_CLIENT_ID} - - REACT_APP_TENANTS_API_URL=http://mvp-platform-tenants:8000 - expose: - - "3000" - - mvp-platform-tenants: - build: ./mvp-platform-services/tenants/docker/Dockerfile.api - environment: - - DATABASE_URL=postgresql://platform_user:${PLATFORM_DB_PASSWORD}@platform-postgres:5432/platform - - AUTH0_DOMAIN=${AUTH0_DOMAIN} - - AUTH0_AUDIENCE=${AUTH0_AUDIENCE} - depends_on: - - platform-postgres - - platform-redis - expose: - - "8000" - - mvp-platform-vehicles-api: - build: ./mvp-platform-services/vehicles/docker/Dockerfile.api - environment: - - DATABASE_URL=postgresql://vehicles_user:${PLATFORM_DB_PASSWORD}@mvp-platform-vehicles-db:5432/vehicles - - REDIS_URL=redis://platform-redis:6379 - - API_KEY=${PLATFORM_VEHICLES_API_KEY} - expose: - - "8000" - - # Platform Databases - platform-postgres: - image: postgres:15 - environment: - POSTGRES_DB: platform - POSTGRES_USER: platform_user - POSTGRES_PASSWORD: ${PLATFORM_DB_PASSWORD} - volumes: - - ./mvp-platform-services/tenants/sql/schema:/docker-entrypoint-initdb.d - - platform_postgres_data:/var/lib/postgresql/data - expose: - - "5432" - - mvp-platform-vehicles-db: - image: postgres:15 - environment: - POSTGRES_DB: vehicles - POSTGRES_USER: vehicles_user - POSTGRES_PASSWORD: ${PLATFORM_DB_PASSWORD} - volumes: - - ./mvp-platform-services/vehicles/sql/schema:/docker-entrypoint-initdb.d - - platform_vehicles_data:/var/lib/postgresql/data - expose: - - "5432" - - platform-redis: - image: redis:7-alpine - volumes: - - platform_redis_data:/data - expose: - - "6379" - - # Admin Tenant (Converted Current Implementation) - admin-backend: - build: ./backend - environment: - - TENANT_ID=admin - - DATABASE_URL=postgresql://motovault_user:${DB_PASSWORD}@admin-postgres:5432/motovault - - REDIS_URL=redis://admin-redis:6379 - - PLATFORM_TENANTS_API_URL=http://mvp-platform-tenants:8000 - - PLATFORM_VEHICLES_API_URL=http://mvp-platform-vehicles-api:8000 - - PLATFORM_VEHICLES_API_KEY=${PLATFORM_VEHICLES_API_KEY} - - AUTH0_DOMAIN=${AUTH0_DOMAIN} - - AUTH0_AUDIENCE=${AUTH0_AUDIENCE} - depends_on: - - admin-postgres - - admin-redis - - mvp-platform-tenants - - mvp-platform-vehicles-api - expose: - - "3001" - - admin-frontend: - build: ./frontend - environment: - - REACT_APP_API_BASE_URL=http://admin-backend:3001 - - REACT_APP_TENANT_ID=admin - - REACT_APP_AUTH0_DOMAIN=${AUTH0_DOMAIN} - - REACT_APP_AUTH0_CLIENT_ID=${AUTH0_CLIENT_ID} - - REACT_APP_AUTH0_AUDIENCE=${AUTH0_AUDIENCE} - depends_on: - - admin-backend - expose: - - "3000" - - admin-postgres: - image: postgres:15 - environment: - POSTGRES_DB: motovault - POSTGRES_USER: motovault_user - POSTGRES_PASSWORD: ${DB_PASSWORD} - volumes: - - ./backend/src/_system/migrations:/docker-entrypoint-initdb.d - - admin_postgres_data:/var/lib/postgresql/data - expose: - - "5432" - - admin-redis: - image: redis:7-alpine - volumes: - - admin_redis_data:/data - expose: - - "6379" - - admin-minio: - image: minio/minio - command: server /data --console-address ":9001" - environment: - MINIO_ROOT_USER: ${MINIO_ROOT_USER} - MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} - volumes: - - admin_minio_data:/data - expose: - - "9000" - - "9001" - -volumes: - # Platform volumes - platform_postgres_data: - platform_vehicles_data: - platform_redis_data: - - # Admin tenant volumes - admin_postgres_data: - admin_redis_data: - admin_minio_data: - - # Additional tenant volumes will be added via provisioning script - -networks: - default: - name: motovaultpro_network -``` - -### 5.2 Nginx Configuration for Subdomain Routing - -**`nginx.conf`**: -```nginx -events { - worker_connections 1024; -} - -http { - upstream landing { - server mvp-platform-landing:3000; - } - - upstream admin_frontend { - server admin-frontend:3000; - } - - # Main domain - Landing page - server { - listen 80; - server_name motovaultpro.local motovaultpro.com; - - location / { - proxy_pass http://landing; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - } - } - - # Admin tenant - server { - listen 80; - server_name admin.motovaultpro.local admin.motovaultpro.com; - - location / { - proxy_pass http://admin_frontend; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - } - - location /api/ { - proxy_pass http://admin-backend:3001; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - } - } - - # Template for additional tenants (to be added via provisioning) - # server { - # listen 80; - # server_name {tenant-id}.motovaultpro.local {tenant-id}.motovaultpro.com; - # - # location / { - # proxy_pass http://{tenant-id}-frontend:3000; - # proxy_set_header Host $host; - # proxy_set_header X-Real-IP $remote_addr; - # } - # - # location /api/ { - # proxy_pass http://{tenant-id}-backend:3001; - # proxy_set_header Host $host; - # proxy_set_header X-Real-IP $remote_addr; - # } - # } -} -``` - -### 5.3 Local Development Setup - -**Update `/etc/hosts` for Local Development**: -``` -127.0.0.1 motovaultpro.local -127.0.0.1 admin.motovaultpro.local -127.0.0.1 demo-tenant.motovaultpro.local -127.0.0.1 acme-corp.motovaultpro.local -``` - -**Development Commands**: -```bash -# Start platform services only -docker compose up -d mvp-platform-landing mvp-platform-tenants mvp-platform-vehicles-api platform-postgres platform-redis - -# Start admin tenant -docker compose up -d admin-backend admin-frontend admin-postgres admin-redis - -# Provision new tenant -./scripts/provision-tenant.sh demo-tenant -docker compose up -d demo-tenant-backend demo-tenant-frontend demo-tenant-postgres demo-tenant-redis - -# Update nginx config and restart -./scripts/update-nginx-for-tenant.sh demo-tenant -docker compose restart nginx -``` - ---- - -## Testing & Validation - -### End-to-End Testing Scenarios - -1. **Landing Page**: - - Visit `http://motovaultpro.local` → See homepage - - Click "Sign Up" → Redirect to tenant selection or Auth0 - -2. **Admin Tenant**: - - Visit `http://admin.motovaultpro.local` → Redirect to Auth0 - - Login with admin user → Access admin tenant with full features - - Admin dashboard shows tenant management options - -3. **Regular Tenant**: - - Visit `http://demo-tenant.motovaultpro.local` → Redirect to Auth0 - - Login with tenant user → Access tenant-specific application - - No access to admin features - -4. **Signup Workflow**: - - Visit `http://motovaultpro.local/signup/demo-tenant` → Tenant signup form - - Complete Auth0 signup → User status = "pending" - - Admin approves in admin dashboard → User status = "approved" - - User can now login to tenant subdomain - -### Database Validation - -```sql --- Platform database -SELECT * FROM tenants; -SELECT * FROM tenant_signups WHERE status = 'pending'; - --- Admin tenant database (same as current) -SELECT COUNT(*) FROM vehicles; -SELECT COUNT(*) FROM fuel_logs; - --- Regular tenant database (isolated) -SELECT COUNT(*) FROM vehicles; -- Should be different from admin -``` - ---- - -## Migration Checklist - -### Pre-Migration Backup -- [ ] Export current database: `pg_dump motovault > backup_pre_migration.sql` -- [ ] Backup configuration files and environment variables -- [ ] Document current user accounts and Auth0 configuration - -### Phase 1 Implementation -- [ ] Create `mvp-platform-tenants` service with database schema -- [ ] Create `mvp-platform-landing` service with homepage and signup forms -- [ ] Update `docker-compose.yml` with platform services -- [ ] Test platform services independently - -### Phase 2 Implementation āœ… COMPLETED -- [x] Configure Auth0 for multi-tenant signup with metadata -- [x] Implement tenant-aware JWT tokens and rules -- [x] Build signup approval workflow in platform tenants service -- [x] Test Auth0 signup flow with tenant metadata - -### Phase 3 Implementation -- [ ] Add tenant middleware to backend application -- [ ] Rename existing containers to `admin-*` prefix -- [ ] Update environment variables for admin tenant -- [ ] Test admin tenant functionality at `admin.motovaultpro.local` - -### Phase 4 Implementation -- [ ] Update platform vehicles service for tenant context -- [ ] Build tenant management UI in admin tenant -- [ ] Implement tenant provisioning automation scripts -- [ ] Test tenant provisioning end-to-end - -### Phase 5 Implementation -- [ ] Configure nginx for subdomain routing -- [ ] Set up complete multi-tenant Docker Compose -- [ ] Test multiple tenants running simultaneously -- [ ] Validate tenant isolation and admin features - -### Post-Migration Validation -- [ ] All existing data accessible in admin tenant -- [ ] Admin tenant has tenant management capabilities -- [ ] New tenant can be provisioned and accessed via subdomain -- [ ] Signup approval workflow functions correctly -- [ ] Platform services work across all tenants - ---- - -## Operational Considerations - -### Monitoring & Logging -- **Per-Tenant Metrics**: Separate monitoring for each tenant's usage and performance -- **Platform Health**: Monitor shared platform services across all tenants -- **Tenant Isolation**: Ensure logs and metrics don't leak between tenants - -### Backup & Disaster Recovery -- **Per-Tenant Backups**: Each tenant database backed up independently -- **Platform Backup**: Tenant management and platform services data -- **Recovery Testing**: Validate ability to restore individual tenants - -### Scaling Considerations -- **Tenant Limits**: Define maximum number of tenants per instance -- **Resource Quotas**: CPU/memory limits per tenant -- **Database Scaling**: Plan for database performance with many tenant databases - -### Security -- **Tenant Isolation**: Verify no cross-tenant data access possible -- **Admin Access**: Secure admin tenant from unauthorized access -- **Platform Services**: Ensure proper authentication between services - ---- - -## Future Enhancements - -### Advanced Multi-Tenancy Features -- **Tenant-Specific Branding**: Custom logos, colors, domains per tenant -- **Feature Flags**: Enable/disable features per tenant or tenant tier -- **Usage Analytics**: Per-tenant usage metrics and billing integration -- **Custom Integrations**: Tenant-specific external API integrations - -### Operational Improvements -- **Self-Service Tenant Creation**: Allow tenant admins to create their own tenants -- **Automated Scaling**: Auto-scale tenant resources based on usage -- **Advanced Monitoring**: Tenant health dashboards and alerting -- **Backup Automation**: Scheduled backups and retention policies - -### Migration to Kubernetes -- **Namespace-Based Isolation**: Each tenant in separate K8s namespace -- **Helm Charts**: Templated tenant provisioning -- **Service Mesh**: Advanced traffic management and security -- **Horizontal Scaling**: Auto-scaling based on tenant load - -This comprehensive plan provides the foundation for transforming MotoVaultPro into a production-ready multi-tenant SaaS platform while maintaining all existing functionality in the admin tenant. - ---- - -## Implementation Progress - -### Phase 1: Multi-Tenant Foundation & Platform Services āœ… COMPLETED - -#### 1.1 Create Tenant Management Platform Service āœ… -- **Location**: `mvp-platform-services/tenants/` -- **Database Schema**: Created tenant registry with `tenants` and `tenant_signups` tables -- **FastAPI Service**: Basic tenant CRUD operations and signup approval workflow -- **Docker Container**: Built and running on port 8001 -- **Health Check**: āœ… Healthy (http://localhost:8001/health) -- **Tenant Validation**: āœ… Working (admin tenant created and accessible) - -#### 1.2 Create Landing Page Platform Service āœ… -- **Location**: `mvp-platform-services/landing/` -- **React Components**: Homepage, TenantSignup, CallbackHandler created -- **Auth0 Integration**: Configured for tenant-aware authentication -- **TypeScript Issues**: āœ… Fixed all compilation errors: - - āœ… Added Vite environment type definitions - - āœ… Fixed Auth0 provider configuration - - āœ… Resolved component type errors -- **Docker Container**: āœ… Built and running on port 3002 -- **Health Check**: āœ… Serving HTML content (HTTP 200) - -#### 1.3 Update Docker Compose Multi-Tenant Architecture āœ… -- **Platform Services**: Added mvp-platform-tenants, mvp-platform-landing, platform-postgres, platform-redis -- **Admin Tenant**: Renamed existing services to admin-* prefix (admin-backend, admin-frontend, admin-postgres, etc.) -- **Environment Variables**: Added TENANT_ID=admin for admin services -- **Volumes**: Updated volume naming for multi-tenant structure -- **Service Dependencies**: Configured proper startup order and health checks - -#### 1.4 Test Platform Services āœ… -- **Platform Database**: āœ… Running and healthy (port 5434) -- **Platform Redis**: āœ… Running and healthy (port 6381) -- **Tenant Management API**: āœ… Running and healthy (port 8001) -- **Admin Tenant Validation**: āœ… Admin tenant record created and accessible -- **Landing Page**: āœ… Running and serving content (port 3002) -- **Platform Integration**: āœ… All services communicating properly -- **Service Health**: āœ… All health checks passing - -### Phase 2: Authentication & Tenant-Aware Auth0 āœ… COMPLETED - -#### 2.1 Multi-Tenant Auth0 Configuration āœ… -- **Auth0 Configuration Guide**: Created comprehensive setup documentation in `mvp-platform-services/tenants/AUTH0-CONFIG.md` -- **Callback URLs**: Documented for all tenant subdomains (development and production) -- **JWT Token Format**: Specified custom claims for tenant context -- **Auth0 Rules**: Complete rule definitions for tenant metadata and signup status - -#### 2.2 JWT Authentication System āœ… -- **JWT Validation**: Implemented full Auth0 JWKS-based token validation -- **JWKS Caching**: 1-hour cache system for Auth0 public keys -- **Development Fallback**: Mock authentication system for testing -- **Tenant Context**: Automatic extraction of tenant_id from JWT claims -- **Libraries Added**: httpx==0.25.2 for HTTP requests, python-jose for JWT handling - -#### 2.3 Signup Approval Workflow āœ… -- **Enhanced Tenant Service**: Comprehensive signup management APIs -- **Public Signup Endpoint**: `/api/v1/tenants/{tenant_id}/signups` for user registration -- **Admin Approval**: PUT endpoints for approval/rejection with audit trails -- **Tenant Access Controls**: Proper authorization for tenant-specific data -- **End-to-End Testing**: āœ… Verified complete signup and approval flow - -#### 2.4 Authentication Testing āœ… -- **Mock Token System**: Working development authentication -- **Tenant-Specific Access**: Fixed tenant ID parsing for multi-hyphenated names -- **Admin Access**: āœ… Verified admin can access all tenant data -- **Tenant Isolation**: āœ… Confirmed tenants can only access own signups -- **Production Ready**: System ready for Auth0 integration with environment variables - -### Next Steps - Phase 3: Tenant Application Stack - -#### Phase 3 Status (Current) -- āœ… Tenant detection middleware implemented and applied post-auth per route (priority: env TENANT_ID > JWT claim > subdomain). -- āœ… Tenant validation now calls Platform Tenants API dynamically (no hardcoded lists) with a small in-memory cache. -- āœ… Backend config is tenant-aware: - - Database pool uses tenant connection string. - - Redis client uses tenant Redis URL. -- āœ… Platform Vehicles client sends `X-Tenant-ID` header; constructed with container `TENANT_ID`. -- āœ… Admin-only tenant management routes added in admin tenant: - - GET/POST `/api/admin/tenants` proxy to Platform Tenants. - - GET `/api/admin/tenants/:tenantId/signups` proxy for pending signups. - - PUT `/api/admin/signups/:signupId/{approve|reject}` proxy for approvals. -- āœ… Docker Compose updated to parameterize `TENANT_ID` for admin services; clone compose per tenant and set `TENANT_ID` accordingly. -- āœ… Admin frontend Nginx proxy fixed to target `admin-backend`. - -#### Known Health/Operational Notes -- āš ļø `admin-frontend` healthcheck probes HTTP 3000 which redirects to HTTPS 3443. Healthcheck may report unhealthy despite service working. Consider probing `https://localhost:3443` or allowing redirects. -- āš ļø `mvp-platform-tenants` runs and serves requests but the compose healthcheck targets `/health`. Ensure the service exposes `/health` or update the healthcheck path. -- āœ… `admin-backend`, `admin-postgres`, `admin-redis`, Platform Vehicles API/DB/Redis are running and passing their checks; backend migrations complete at startup. - -#### Pending (Phase 3) -- Add `/health` endpoint in Platform Tenants service (or adjust compose healthcheck). -- Adjust admin-frontend healthcheck to probe HTTPS. -- Optional: Introduce caching layer for tenant validation with configurable TTL and background refresh. -- Optional: Expose admin UI in frontend for tenants and signups (API endpoints are ready). - -### Architecture Status -- **Platform Infrastructure**: āœ… Operational -- **Admin Tenant**: āœ… Ready (existing functionality preserved) -- **Multi-Tenant Foundation**: āœ… Complete -- **Landing Page**: āœ… Functional; healthcheck needs adjustment (HTTP→HTTPS redirect) -- **Authentication Flow**: āœ… Complete (JWT validation and tenant isolation) -- **Signup Approval System**: āœ… API ready; admin backend routes proxy to tenants service - -### Key Achievements in Phase 1 -1. **Zero-Downtime Migration**: Existing functionality preserved as admin tenant -2. **Scalable Architecture**: Platform services ready for multiple tenants -3. **Database Isolation**: Separate databases per tenant capability established -4. **Service Independence**: Platform services and tenant services properly separated -5. **Health Monitoring**: All services have proper health checks and monitoring - -### Key Achievements in Phase 2 -1. **Production-Ready Authentication**: Full JWT validation with Auth0 JWKS integration -2. **Tenant Isolation**: Secure tenant-specific access controls and authorization -3. **Signup Workflow**: Complete tenant signup and admin approval system -4. **Development/Production Flexibility**: Seamless fallback between mock and real authentication -5. **Comprehensive Documentation**: Complete Auth0 setup guide for production deployment -6. **End-to-End Testing**: Verified multi-tenant authentication and authorization flow diff --git a/docs/changes/fuel-logs-v1/FUEL-LOGS-IMPLEMENTATION.md b/docs/changes/fuel-logs-v1/FUEL-LOGS-IMPLEMENTATION.md deleted file mode 100644 index 978a7b9..0000000 --- a/docs/changes/fuel-logs-v1/FUEL-LOGS-IMPLEMENTATION.md +++ /dev/null @@ -1,164 +0,0 @@ -# Fuel Logs Feature Enhancement - Master Implementation Guide - -## Overview -This document provides comprehensive instructions for enhancing the existing fuel logs feature with advanced business logic, improved user experience, and future integration capabilities. - -## Current State Analysis -The existing fuel logs feature has: -- āœ… Basic CRUD operations implemented -- āœ… Service layer with MPG calculations -- āœ… Database schema with basic fields -- āœ… API endpoints and controllers -- āŒ Missing comprehensive test suite -- āŒ Limited field options and validation -- āŒ No Imperial/Metric support -- āŒ No fuel type/grade system -- āŒ No trip distance alternative to odometer - -## Enhanced Requirements Summary - -### New Fields & Logic -1. **Vehicle Selection**: Dropdown from user's vehicles -2. **Distance Tracking**: Either `trip_distance` OR `odometer` required -3. **Fuel System**: Type (gasoline/diesel/electric) with dynamic grade selection -4. **Units**: Imperial/Metric support based on user settings -5. **Cost Calculation**: Auto-calculated from `cost_per_unit` Ɨ `total_units` -6. **Location**: Placeholder for future Google Maps integration -7. **DateTime**: Date/time picker defaulting to current - -### Business Rules -- **Validation**: Either trip_distance OR odometer must be provided -- **Fuel Grades**: Dynamic based on fuel type selection - - Gasoline: 87, 88, 89, 91, 93 - - Diesel: #1, #2 - - Electric: N/A -- **Units**: Display/calculate based on user's Imperial/Metric preference -- **Cost**: Total cost = cost_per_unit Ɨ total_units (auto-calculated) - -## Implementation Strategy - -This enhancement requires **5 coordinated phases** due to the scope of changes: - -### Phase Dependencies -``` -Phase 1 (Database) → Phase 2 (Logic) → Phase 3 (API) → Phase 4 (Frontend) - ā†˜ - Phase 5 (Future Prep) -``` - -### Phase Breakdown - -#### Phase 1: Database Schema & Core Logic -**File**: `docs/phases/FUEL-LOGS-PHASE-1.md` -- Database schema migration for new fields -- Update existing fuel_logs table structure -- Core type system updates -- Basic validation logic - -#### Phase 2: Enhanced Business Logic -**File**: `docs/phases/FUEL-LOGS-PHASE-2.md` -- Fuel type/grade relationship system -- Imperial/Metric conversion utilities -- Enhanced MPG calculations for trip_distance -- Advanced validation rules - -#### Phase 3: API & Backend Implementation -**File**: `docs/phases/FUEL-LOGS-PHASE-3.md` -- Updated API contracts and endpoints -- New fuel grade endpoint -- User settings integration -- Comprehensive test suite - -#### Phase 4: Frontend Implementation -**File**: `docs/phases/FUEL-LOGS-PHASE-4.md` -- Enhanced form components -- Dynamic dropdowns and calculations -- Imperial/Metric UI support -- Real-time cost calculations - -#### Phase 5: Future Integration Preparation -**File**: `docs/phases/FUEL-LOGS-PHASE-5.md` -- Google Maps service architecture -- Location service interface design -- Extensibility planning - -## Critical Implementation Notes - -### Database Migration Strategy -- **Approach**: Additive migrations to preserve existing data -- **Backward Compatibility**: Existing `gallons`/`pricePerGallon` fields remain during transition -- **Data Migration**: Convert existing records to new schema format - -### User Experience Considerations -- **Progressive Enhancement**: New features don't break existing workflows -- **Mobile Optimization**: Form designed for fuel station usage -- **Real-time Feedback**: Immediate cost calculations and validation - -### Testing Requirements -- **Unit Tests**: Each business logic component -- **Integration Tests**: Complete API workflows -- **Frontend Tests**: Form validation and user interactions -- **Migration Tests**: Database schema changes - -## Success Criteria - -### Phase Completion Checklist -Each phase must achieve: -- āœ… All documented requirements implemented -- āœ… Comprehensive test coverage -- āœ… Documentation updated -- āœ… No breaking changes to existing functionality -- āœ… Code follows project conventions - -### Final Feature Validation -- āœ… All new fields working correctly -- āœ… Fuel type/grade system functional -- āœ… Imperial/Metric units display properly -- āœ… Cost calculations accurate -- āœ… Trip distance alternative to odometer works -- āœ… Existing fuel logs data preserved and functional -- āœ… Mobile-friendly form interface -- āœ… Future Google Maps integration ready - -## Architecture Considerations - -### Service Boundaries -- **Core Feature**: Remains in `backend/src/features/fuel-logs/` -- **User Settings**: Integration with user preferences system -- **Location Service**: Separate service interface for future Maps integration - -### Caching Strategy Updates -- **New Cache Keys**: Include fuel type/grade lookups -- **Imperial/Metric**: Cache converted values when appropriate -- **Location**: Prepare for station/price caching - -### Security & Validation -- **Input Validation**: Enhanced validation for new field combinations -- **User Isolation**: All new data remains user-scoped -- **API Security**: Maintain existing JWT authentication requirements - -## Next Steps for Implementation - -1. **Start with Phase 1**: Database foundation is critical -2. **Sequential Execution**: Each phase builds on the previous -3. **Test Early**: Implement tests alongside each component -4. **Monitor Performance**: Track impact of new features on existing functionality -5. **User Feedback**: Consider beta testing the enhanced form interface - -## Future Enhancement Opportunities - -### Post-Implementation Features -- **Analytics**: Fuel efficiency trends and insights -- **Notifications**: Maintenance reminders based on fuel logs -- **Export**: CSV/PDF reports of fuel data -- **Social**: Share fuel efficiency achievements -- **Integration**: Connect with vehicle manufacturer APIs - -### Technical Debt Reduction -- **Test Coverage**: Complete the missing test suite from original implementation -- **Performance**: Optimize database queries for new field combinations -- **Monitoring**: Add detailed logging for enhanced business logic - ---- - -**Implementation Guide Created**: Use the phase-specific documents in `docs/phases/` for detailed technical instructions. \ No newline at end of file diff --git a/docs/changes/fuel-logs-v1/FUEL-LOGS-PHASE-1.md b/docs/changes/fuel-logs-v1/FUEL-LOGS-PHASE-1.md deleted file mode 100644 index a6fe595..0000000 --- a/docs/changes/fuel-logs-v1/FUEL-LOGS-PHASE-1.md +++ /dev/null @@ -1,391 +0,0 @@ -# Phase 1: Database Schema & Core Logic - -## Overview -Establish the database foundation for enhanced fuel logs with new fields, validation rules, and core type system updates. - -## Prerequisites -- Existing fuel logs feature (basic implementation) -- PostgreSQL database with current `fuel_logs` table -- Migration system functional - -## Database Schema Changes - -### New Fields to Add - -```sql --- Add these columns to fuel_logs table -ALTER TABLE fuel_logs ADD COLUMN trip_distance INTEGER; -- Alternative to odometer reading -ALTER TABLE fuel_logs ADD COLUMN fuel_type VARCHAR(20) NOT NULL DEFAULT 'gasoline'; -ALTER TABLE fuel_logs ADD COLUMN fuel_grade VARCHAR(10); -ALTER TABLE fuel_logs ADD COLUMN fuel_units DECIMAL(8,3); -- Replaces gallons for metric support -ALTER TABLE fuel_logs ADD COLUMN cost_per_unit DECIMAL(6,3); -- Replaces price_per_gallon -ALTER TABLE fuel_logs ADD COLUMN location_data JSONB; -- Future Google Maps integration -ALTER TABLE fuel_logs ADD COLUMN date_time TIMESTAMP WITH TIME ZONE; -- Enhanced date/time - --- Add constraints -ALTER TABLE fuel_logs ADD CONSTRAINT fuel_type_check - CHECK (fuel_type IN ('gasoline', 'diesel', 'electric')); - --- Add conditional constraint: either trip_distance OR odometer_reading required -ALTER TABLE fuel_logs ADD CONSTRAINT distance_required_check - CHECK ((trip_distance IS NOT NULL AND trip_distance > 0) OR (odometer_reading IS NOT NULL AND odometer_reading > 0)); - --- Add indexes for performance -CREATE INDEX idx_fuel_logs_fuel_type ON fuel_logs(fuel_type); -CREATE INDEX idx_fuel_logs_date_time ON fuel_logs(date_time); -``` - -### Migration Strategy - -#### Step 1: Additive Migration -**File**: `backend/src/features/fuel-logs/migrations/002_enhance_fuel_logs_schema.sql` - -```sql --- Migration: 002_enhance_fuel_logs_schema.sql -BEGIN; - --- Add new columns (nullable initially for data migration) -ALTER TABLE fuel_logs ADD COLUMN IF NOT EXISTS trip_distance INTEGER; -ALTER TABLE fuel_logs ADD COLUMN IF NOT EXISTS fuel_type VARCHAR(20); -ALTER TABLE fuel_logs ADD COLUMN IF NOT EXISTS fuel_grade VARCHAR(10); -ALTER TABLE fuel_logs ADD COLUMN IF NOT EXISTS fuel_units DECIMAL(8,3); -ALTER TABLE fuel_logs ADD COLUMN IF NOT EXISTS cost_per_unit DECIMAL(6,3); -ALTER TABLE fuel_logs ADD COLUMN IF NOT EXISTS location_data JSONB; -ALTER TABLE fuel_logs ADD COLUMN IF NOT EXISTS date_time TIMESTAMP WITH TIME ZONE; - --- Migrate existing data -UPDATE fuel_logs SET - fuel_type = 'gasoline', - fuel_units = gallons, - cost_per_unit = price_per_gallon, - date_time = date::timestamp + interval '12 hours' -- Default to noon -WHERE fuel_type IS NULL; - --- Add constraints after data migration -ALTER TABLE fuel_logs ALTER COLUMN fuel_type SET NOT NULL; -ALTER TABLE fuel_logs ALTER COLUMN fuel_type SET DEFAULT 'gasoline'; - --- Add check constraints -ALTER TABLE fuel_logs ADD CONSTRAINT fuel_type_check - CHECK (fuel_type IN ('gasoline', 'diesel', 'electric')); - --- Distance requirement constraint (either trip_distance OR odometer_reading) -ALTER TABLE fuel_logs ADD CONSTRAINT distance_required_check - CHECK ((trip_distance IS NOT NULL AND trip_distance > 0) OR - (odometer_reading IS NOT NULL AND odometer_reading > 0)); - --- Add performance indexes -CREATE INDEX IF NOT EXISTS idx_fuel_logs_fuel_type ON fuel_logs(fuel_type); -CREATE INDEX IF NOT EXISTS idx_fuel_logs_date_time ON fuel_logs(date_time); - -COMMIT; -``` - -#### Step 2: Backward Compatibility Plan -- Keep existing `gallons` and `price_per_gallon` fields during transition -- Update application logic to use new fields preferentially -- Plan deprecation of old fields in future migration - -### Data Validation Rules - -#### Core Business Rules -1. **Distance Requirement**: Either `trip_distance` OR `odometer_reading` must be provided -2. **Fuel Type Validation**: Must be one of: 'gasoline', 'diesel', 'electric' -3. **Fuel Grade Validation**: Must match fuel type options -4. **Positive Values**: All numeric fields must be > 0 -5. **DateTime**: Cannot be in the future - -#### Fuel Grade Validation Logic -```sql --- Fuel grade validation by type -CREATE OR REPLACE FUNCTION validate_fuel_grade() -RETURNS TRIGGER AS $$ -BEGIN - -- Gasoline grades - IF NEW.fuel_type = 'gasoline' AND - NEW.fuel_grade NOT IN ('87', '88', '89', '91', '93') THEN - RAISE EXCEPTION 'Invalid fuel grade % for gasoline', NEW.fuel_grade; - END IF; - - -- Diesel grades - IF NEW.fuel_type = 'diesel' AND - NEW.fuel_grade NOT IN ('#1', '#2') THEN - RAISE EXCEPTION 'Invalid fuel grade % for diesel', NEW.fuel_grade; - END IF; - - -- Electric (no grades) - IF NEW.fuel_type = 'electric' AND - NEW.fuel_grade IS NOT NULL THEN - RAISE EXCEPTION 'Electric fuel type cannot have a grade'; - END IF; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Create trigger -CREATE TRIGGER fuel_grade_validation_trigger - BEFORE INSERT OR UPDATE ON fuel_logs - FOR EACH ROW EXECUTE FUNCTION validate_fuel_grade(); -``` - -## TypeScript Type System Updates - -### New Core Types - -**File**: `backend/src/features/fuel-logs/domain/fuel-logs.types.ts` - -```typescript -// Fuel system enums -export enum FuelType { - GASOLINE = 'gasoline', - DIESEL = 'diesel', - ELECTRIC = 'electric' -} - -export enum GasolineFuelGrade { - REGULAR_87 = '87', - MIDGRADE_88 = '88', - MIDGRADE_89 = '89', - PREMIUM_91 = '91', - PREMIUM_93 = '93' -} - -export enum DieselFuelGrade { - DIESEL_1 = '#1', - DIESEL_2 = '#2' -} - -export type FuelGrade = GasolineFuelGrade | DieselFuelGrade | null; - -// Unit system types -export enum UnitSystem { - IMPERIAL = 'imperial', - METRIC = 'metric' -} - -export interface UnitConversion { - fuelUnits: string; // 'gallons' | 'liters' - distanceUnits: string; // 'miles' | 'kilometers' - efficiencyUnits: string; // 'mpg' | 'l/100km' -} - -// Enhanced location data structure -export interface LocationData { - address?: string; - coordinates?: { - latitude: number; - longitude: number; - }; - googlePlaceId?: string; - stationName?: string; - // Future: station prices, fuel availability -} - -// Updated core FuelLog interface -export interface FuelLog { - id: string; - userId: string; - vehicleId: string; - dateTime: Date; // Enhanced from simple date - - // Distance tracking (either/or required) - odometerReading?: number; - tripDistance?: number; - - // Fuel system - fuelType: FuelType; - fuelGrade?: FuelGrade; - fuelUnits: number; // Replaces gallons - costPerUnit: number; // Replaces pricePerGallon - totalCost: number; // Auto-calculated - - // Location (future Google Maps integration) - locationData?: LocationData; - - // Legacy fields (maintain during transition) - gallons?: number; // Deprecated - pricePerGallon?: number; // Deprecated - - // Metadata - notes?: string; - mpg?: number; // Calculated efficiency - createdAt: Date; - updatedAt: Date; -} -``` - -### Request/Response Type Updates - -```typescript -export interface CreateFuelLogRequest { - vehicleId: string; - dateTime: string; // ISO datetime string - - // Distance (either required) - odometerReading?: number; - tripDistance?: number; - - // Fuel system - fuelType: FuelType; - fuelGrade?: FuelGrade; - fuelUnits: number; - costPerUnit: number; - // totalCost calculated automatically - - // Location - locationData?: LocationData; - notes?: string; -} - -export interface UpdateFuelLogRequest { - dateTime?: string; - odometerReading?: number; - tripDistance?: number; - fuelType?: FuelType; - fuelGrade?: FuelGrade; - fuelUnits?: number; - costPerUnit?: number; - locationData?: LocationData; - notes?: string; -} -``` - -## Core Validation Logic - -### Business Rule Validation - -**File**: `backend/src/features/fuel-logs/domain/fuel-logs.validation.ts` - -```typescript -export class FuelLogValidation { - - static validateDistanceRequirement(data: CreateFuelLogRequest | UpdateFuelLogRequest): void { - const hasOdometer = data.odometerReading && data.odometerReading > 0; - const hasTripDistance = data.tripDistance && data.tripDistance > 0; - - if (!hasOdometer && !hasTripDistance) { - throw new ValidationError('Either odometer reading or trip distance is required'); - } - - if (hasOdometer && hasTripDistance) { - throw new ValidationError('Cannot specify both odometer reading and trip distance'); - } - } - - static validateFuelGrade(fuelType: FuelType, fuelGrade?: FuelGrade): void { - switch (fuelType) { - case FuelType.GASOLINE: - if (fuelGrade && !Object.values(GasolineFuelGrade).includes(fuelGrade as GasolineFuelGrade)) { - throw new ValidationError(`Invalid gasoline grade: ${fuelGrade}`); - } - break; - - case FuelType.DIESEL: - if (fuelGrade && !Object.values(DieselFuelGrade).includes(fuelGrade as DieselFuelGrade)) { - throw new ValidationError(`Invalid diesel grade: ${fuelGrade}`); - } - break; - - case FuelType.ELECTRIC: - if (fuelGrade) { - throw new ValidationError('Electric vehicles cannot have fuel grades'); - } - break; - } - } - - static validatePositiveValues(data: CreateFuelLogRequest | UpdateFuelLogRequest): void { - if (data.fuelUnits && data.fuelUnits <= 0) { - throw new ValidationError('Fuel units must be positive'); - } - - if (data.costPerUnit && data.costPerUnit <= 0) { - throw new ValidationError('Cost per unit must be positive'); - } - - if (data.odometerReading && data.odometerReading <= 0) { - throw new ValidationError('Odometer reading must be positive'); - } - - if (data.tripDistance && data.tripDistance <= 0) { - throw new ValidationError('Trip distance must be positive'); - } - } - - static validateDateTime(dateTime: string): void { - const date = new Date(dateTime); - const now = new Date(); - - if (date > now) { - throw new ValidationError('Cannot create fuel logs in the future'); - } - } -} -``` - -## Implementation Tasks - -### Database Tasks -1. āœ… Create migration file `002_enhance_fuel_logs_schema.sql` -2. āœ… Add new columns with appropriate types -3. āœ… Migrate existing data to new schema -4. āœ… Add database constraints and triggers -5. āœ… Create performance indexes - -### Type System Tasks -1. āœ… Define fuel system enums -2. āœ… Create unit system types -3. āœ… Update core FuelLog interface -4. āœ… Update request/response interfaces -5. āœ… Add location data structure - -### Validation Tasks -1. āœ… Create validation utility class -2. āœ… Implement distance requirement validation -3. āœ… Implement fuel grade validation -4. āœ… Add positive value checks -5. āœ… Add datetime validation - -## Testing Requirements - -### Database Testing -```sql --- Test distance requirement constraint -INSERT INTO fuel_logs (...) -- Should fail without distance -INSERT INTO fuel_logs (trip_distance = 150, ...) -- Should succeed -INSERT INTO fuel_logs (odometer_reading = 50000, ...) -- Should succeed -INSERT INTO fuel_logs (trip_distance = 150, odometer_reading = 50000, ...) -- Should fail - --- Test fuel type/grade validation -INSERT INTO fuel_logs (fuel_type = 'gasoline', fuel_grade = '87', ...) -- Should succeed -INSERT INTO fuel_logs (fuel_type = 'gasoline', fuel_grade = '#1', ...) -- Should fail -INSERT INTO fuel_logs (fuel_type = 'electric', fuel_grade = '87', ...) -- Should fail -``` - -### Unit Tests Required -- Validation logic for all business rules -- Type conversion utilities -- Migration data integrity -- Constraint enforcement - -## Success Criteria - -### Phase 1 Complete When: -- āœ… Database migration runs successfully -- āœ… All new fields available with proper types -- āœ… Existing data migrated and preserved -- āœ… Database constraints enforce business rules -- āœ… TypeScript interfaces updated and compiling -- āœ… Core validation logic implemented and tested -- āœ… No breaking changes to existing functionality - -### Ready for Phase 2 When: -- All database changes deployed and tested -- Type system fully updated -- Core validation passes all tests -- Existing fuel logs feature still functional - ---- - -**Next Phase**: [Phase 2 - Enhanced Business Logic](FUEL-LOGS-PHASE-2.md) \ No newline at end of file diff --git a/docs/changes/fuel-logs-v1/FUEL-LOGS-PHASE-2.md b/docs/changes/fuel-logs-v1/FUEL-LOGS-PHASE-2.md deleted file mode 100644 index 5bc597d..0000000 --- a/docs/changes/fuel-logs-v1/FUEL-LOGS-PHASE-2.md +++ /dev/null @@ -1,658 +0,0 @@ -# Phase 2: Enhanced Business Logic - -## Overview -Implement sophisticated business logic for fuel type/grade relationships, Imperial/Metric conversion system, enhanced MPG calculations, and advanced validation rules. - -## Prerequisites -- āœ… Phase 1 completed (database schema and core types) -- Database migration deployed and tested -- Core validation logic functional - -## Fuel Type/Grade Dynamic System - -### Fuel Grade Service - -**File**: `backend/src/features/fuel-logs/domain/fuel-grade.service.ts` - -```typescript -import { FuelType, FuelGrade, GasolineFuelGrade, DieselFuelGrade } from './fuel-logs.types'; - -export interface FuelGradeOption { - value: FuelGrade; - label: string; - description?: string; -} - -export class FuelGradeService { - - static getFuelGradeOptions(fuelType: FuelType): FuelGradeOption[] { - switch (fuelType) { - case FuelType.GASOLINE: - return [ - { value: GasolineFuelGrade.REGULAR_87, label: '87 (Regular)', description: 'Regular unleaded gasoline' }, - { value: GasolineFuelGrade.MIDGRADE_88, label: '88 (Mid-Grade)', description: 'Mid-grade gasoline' }, - { value: GasolineFuelGrade.MIDGRADE_89, label: '89 (Mid-Grade Plus)', description: 'Mid-grade plus gasoline' }, - { value: GasolineFuelGrade.PREMIUM_91, label: '91 (Premium)', description: 'Premium gasoline' }, - { value: GasolineFuelGrade.PREMIUM_93, label: '93 (Premium Plus)', description: 'Premium plus gasoline' } - ]; - - case FuelType.DIESEL: - return [ - { value: DieselFuelGrade.DIESEL_1, label: '#1 Diesel', description: 'Light diesel fuel' }, - { value: DieselFuelGrade.DIESEL_2, label: '#2 Diesel', description: 'Standard diesel fuel' } - ]; - - case FuelType.ELECTRIC: - return []; // No grades for electric - - default: - return []; - } - } - - static isValidGradeForFuelType(fuelType: FuelType, fuelGrade?: FuelGrade): boolean { - if (!fuelGrade) { - return fuelType === FuelType.ELECTRIC; // Only electric allows null grade - } - - const validGrades = this.getFuelGradeOptions(fuelType).map(option => option.value); - return validGrades.includes(fuelGrade); - } - - static getDefaultGrade(fuelType: FuelType): FuelGrade { - switch (fuelType) { - case FuelType.GASOLINE: - return GasolineFuelGrade.REGULAR_87; - case FuelType.DIESEL: - return DieselFuelGrade.DIESEL_2; - case FuelType.ELECTRIC: - return null; - default: - return null; - } - } -} -``` - -## Imperial/Metric Conversion System - -### Unit Conversion Service - -**File**: `backend/src/features/fuel-logs/domain/unit-conversion.service.ts` - -```typescript -import { UnitSystem, UnitConversion } from './fuel-logs.types'; - -export interface ConversionFactors { - // Volume conversions - gallonsToLiters: number; - litersToGallons: number; - - // Distance conversions - milesToKilometers: number; - kilometersToMiles: number; -} - -export class UnitConversionService { - - private static readonly FACTORS: ConversionFactors = { - gallonsToLiters: 3.78541, - litersToGallons: 0.264172, - milesToKilometers: 1.60934, - kilometersToMiles: 0.621371 - }; - - static getUnitLabels(unitSystem: UnitSystem): UnitConversion { - switch (unitSystem) { - case UnitSystem.IMPERIAL: - return { - fuelUnits: 'gallons', - distanceUnits: 'miles', - efficiencyUnits: 'mpg' - }; - case UnitSystem.METRIC: - return { - fuelUnits: 'liters', - distanceUnits: 'kilometers', - efficiencyUnits: 'L/100km' - }; - } - } - - // Volume conversions - static convertFuelUnits(value: number, fromSystem: UnitSystem, toSystem: UnitSystem): number { - if (fromSystem === toSystem) return value; - - if (fromSystem === UnitSystem.IMPERIAL && toSystem === UnitSystem.METRIC) { - return value * this.FACTORS.gallonsToLiters; // gallons to liters - } - - if (fromSystem === UnitSystem.METRIC && toSystem === UnitSystem.IMPERIAL) { - return value * this.FACTORS.litersToGallons; // liters to gallons - } - - return value; - } - - // Distance conversions - static convertDistance(value: number, fromSystem: UnitSystem, toSystem: UnitSystem): number { - if (fromSystem === toSystem) return value; - - if (fromSystem === UnitSystem.IMPERIAL && toSystem === UnitSystem.METRIC) { - return value * this.FACTORS.milesToKilometers; // miles to kilometers - } - - if (fromSystem === UnitSystem.METRIC && toSystem === UnitSystem.IMPERIAL) { - return value * this.FACTORS.kilometersToMiles; // kilometers to miles - } - - return value; - } - - // Efficiency calculations - static calculateEfficiency(distance: number, fuelUnits: number, unitSystem: UnitSystem): number { - if (fuelUnits <= 0) return 0; - - switch (unitSystem) { - case UnitSystem.IMPERIAL: - return distance / fuelUnits; // miles per gallon - case UnitSystem.METRIC: - return (fuelUnits / distance) * 100; // liters per 100 kilometers - default: - return 0; - } - } - - // Convert efficiency between unit systems - static convertEfficiency(efficiency: number, fromSystem: UnitSystem, toSystem: UnitSystem): number { - if (fromSystem === toSystem) return efficiency; - - if (fromSystem === UnitSystem.IMPERIAL && toSystem === UnitSystem.METRIC) { - // MPG to L/100km: L/100km = 235.214 / MPG - return efficiency > 0 ? 235.214 / efficiency : 0; - } - - if (fromSystem === UnitSystem.METRIC && toSystem === UnitSystem.IMPERIAL) { - // L/100km to MPG: MPG = 235.214 / (L/100km) - return efficiency > 0 ? 235.214 / efficiency : 0; - } - - return efficiency; - } -} -``` - -## Enhanced MPG/Efficiency Calculations - -### Efficiency Calculation Service - -**File**: `backend/src/features/fuel-logs/domain/efficiency-calculation.service.ts` - -```typescript -import { FuelLog, UnitSystem } from './fuel-logs.types'; -import { UnitConversionService } from './unit-conversion.service'; - -export interface EfficiencyResult { - value: number; - unitSystem: UnitSystem; - label: string; - calculationMethod: 'odometer' | 'trip_distance'; -} - -export class EfficiencyCalculationService { - - /** - * Calculate efficiency for a fuel log entry - */ - static calculateEfficiency( - currentLog: Partial, - previousLog: FuelLog | null, - userUnitSystem: UnitSystem - ): EfficiencyResult | null { - - // Determine calculation method and distance - let distance: number; - let calculationMethod: 'odometer' | 'trip_distance'; - - if (currentLog.tripDistance) { - // Use trip distance directly - distance = currentLog.tripDistance; - calculationMethod = 'trip_distance'; - } else if (currentLog.odometerReading && previousLog?.odometerReading) { - // Calculate from odometer difference - distance = currentLog.odometerReading - previousLog.odometerReading; - calculationMethod = 'odometer'; - - if (distance <= 0) { - return null; // Invalid distance - } - } else { - return null; // Cannot calculate efficiency - } - - if (!currentLog.fuelUnits || currentLog.fuelUnits <= 0) { - return null; // Invalid fuel amount - } - - // Calculate efficiency in user's preferred unit system - const efficiency = UnitConversionService.calculateEfficiency( - distance, - currentLog.fuelUnits, - userUnitSystem - ); - - const unitLabels = UnitConversionService.getUnitLabels(userUnitSystem); - - return { - value: efficiency, - unitSystem: userUnitSystem, - label: unitLabels.efficiencyUnits, - calculationMethod - }; - } - - /** - * Calculate average efficiency for a set of fuel logs - */ - static calculateAverageEfficiency( - fuelLogs: FuelLog[], - userUnitSystem: UnitSystem - ): EfficiencyResult | null { - - const validLogs = fuelLogs.filter(log => log.mpg && log.mpg > 0); - - if (validLogs.length === 0) { - return null; - } - - // Convert all efficiencies to user's unit system and average - const efficiencies = validLogs.map(log => { - // Assume stored efficiency is in Imperial (MPG) - return UnitConversionService.convertEfficiency( - log.mpg!, - UnitSystem.IMPERIAL, - userUnitSystem - ); - }); - - const averageEfficiency = efficiencies.reduce((sum, eff) => sum + eff, 0) / efficiencies.length; - const unitLabels = UnitConversionService.getUnitLabels(userUnitSystem); - - return { - value: averageEfficiency, - unitSystem: userUnitSystem, - label: unitLabels.efficiencyUnits, - calculationMethod: 'odometer' // Mixed, but default to odometer - }; - } - - /** - * Calculate total distance traveled from fuel logs - */ - static calculateTotalDistance(fuelLogs: FuelLog[], userUnitSystem: UnitSystem): number { - let totalDistance = 0; - - for (let i = 1; i < fuelLogs.length; i++) { - const current = fuelLogs[i]; - const previous = fuelLogs[i - 1]; - - if (current.tripDistance) { - // Use trip distance if available - totalDistance += current.tripDistance; - } else if (current.odometerReading && previous.odometerReading) { - // Calculate from odometer difference - const distance = current.odometerReading - previous.odometerReading; - if (distance > 0) { - totalDistance += distance; - } - } - } - - return totalDistance; - } -} -``` - -## Advanced Validation Rules - -### Enhanced Validation Service - -**File**: `backend/src/features/fuel-logs/domain/enhanced-validation.service.ts` - -```typescript -import { CreateFuelLogRequest, UpdateFuelLogRequest, FuelType, UnitSystem } from './fuel-logs.types'; -import { FuelGradeService } from './fuel-grade.service'; - -export interface ValidationResult { - isValid: boolean; - errors: string[]; - warnings: string[]; -} - -export class EnhancedValidationService { - - static validateFuelLogData( - data: CreateFuelLogRequest | UpdateFuelLogRequest, - userUnitSystem: UnitSystem - ): ValidationResult { - - const errors: string[] = []; - const warnings: string[] = []; - - // Distance requirement validation - this.validateDistanceRequirement(data, errors); - - // Fuel system validation - this.validateFuelSystem(data, errors); - - // Numeric value validation - this.validateNumericValues(data, errors, warnings); - - // DateTime validation - this.validateDateTime(data, errors); - - // Business logic validation - this.validateBusinessRules(data, errors, warnings, userUnitSystem); - - return { - isValid: errors.length === 0, - errors, - warnings - }; - } - - private static validateDistanceRequirement( - data: CreateFuelLogRequest | UpdateFuelLogRequest, - errors: string[] - ): void { - const hasOdometer = data.odometerReading && data.odometerReading > 0; - const hasTripDistance = data.tripDistance && data.tripDistance > 0; - - if (!hasOdometer && !hasTripDistance) { - errors.push('Either odometer reading or trip distance is required'); - } - - if (hasOdometer && hasTripDistance) { - errors.push('Cannot specify both odometer reading and trip distance'); - } - } - - private static validateFuelSystem( - data: CreateFuelLogRequest | UpdateFuelLogRequest, - errors: string[] - ): void { - if (!data.fuelType) return; - - // Validate fuel type - if (!Object.values(FuelType).includes(data.fuelType)) { - errors.push(`Invalid fuel type: ${data.fuelType}`); - return; - } - - // Validate fuel grade for fuel type - if (!FuelGradeService.isValidGradeForFuelType(data.fuelType, data.fuelGrade)) { - errors.push(`Invalid fuel grade '${data.fuelGrade}' for fuel type '${data.fuelType}'`); - } - } - - private static validateNumericValues( - data: CreateFuelLogRequest | UpdateFuelLogRequest, - errors: string[], - warnings: string[] - ): void { - - // Positive value checks - if (data.fuelUnits !== undefined && data.fuelUnits <= 0) { - errors.push('Fuel units must be positive'); - } - - if (data.costPerUnit !== undefined && data.costPerUnit <= 0) { - errors.push('Cost per unit must be positive'); - } - - if (data.odometerReading !== undefined && data.odometerReading <= 0) { - errors.push('Odometer reading must be positive'); - } - - if (data.tripDistance !== undefined && data.tripDistance <= 0) { - errors.push('Trip distance must be positive'); - } - - // Reasonable value warnings - if (data.fuelUnits && data.fuelUnits > 100) { - warnings.push('Fuel amount seems unusually high (>100 units)'); - } - - if (data.costPerUnit && data.costPerUnit > 10) { - warnings.push('Cost per unit seems unusually high (>$10)'); - } - - if (data.tripDistance && data.tripDistance > 1000) { - warnings.push('Trip distance seems unusually high (>1000 miles)'); - } - } - - private static validateDateTime( - data: CreateFuelLogRequest | UpdateFuelLogRequest, - errors: string[] - ): void { - if (!data.dateTime) return; - - const date = new Date(data.dateTime); - const now = new Date(); - - if (isNaN(date.getTime())) { - errors.push('Invalid date/time format'); - return; - } - - if (date > now) { - errors.push('Cannot create fuel logs in the future'); - } - - // Check if date is too far in the past (>2 years) - const twoYearsAgo = new Date(now.getTime() - (2 * 365 * 24 * 60 * 60 * 1000)); - if (date < twoYearsAgo) { - errors.push('Fuel log date cannot be more than 2 years in the past'); - } - } - - private static validateBusinessRules( - data: CreateFuelLogRequest | UpdateFuelLogRequest, - errors: string[], - warnings: string[], - userUnitSystem: UnitSystem - ): void { - - // Electric vehicle specific validation - if (data.fuelType === FuelType.ELECTRIC) { - if (data.costPerUnit && data.costPerUnit > 0.50) { - warnings.push('Cost per kWh seems high for electric charging'); - } - } - - // Efficiency warning calculation - if (data.fuelUnits && data.tripDistance) { - const estimatedMPG = data.tripDistance / data.fuelUnits; - - if (userUnitSystem === UnitSystem.IMPERIAL) { - if (estimatedMPG < 5) { - warnings.push('Calculated efficiency is very low (<5 MPG)'); - } else if (estimatedMPG > 50) { - warnings.push('Calculated efficiency is very high (>50 MPG)'); - } - } - } - - // Cost validation - if (data.fuelUnits && data.costPerUnit) { - const calculatedTotal = data.fuelUnits * data.costPerUnit; - // Allow 1 cent tolerance for rounding - if (Math.abs(calculatedTotal - (data.totalCost || calculatedTotal)) > 0.01) { - warnings.push('Total cost does not match fuel units Ɨ cost per unit'); - } - } - } -} -``` - -## User Settings Integration - -### User Settings Service Interface - -**File**: `backend/src/features/fuel-logs/external/user-settings.service.ts` - -```typescript -import { UnitSystem } from '../domain/fuel-logs.types'; - -export interface UserSettings { - unitSystem: UnitSystem; - defaultFuelType?: string; - currencyCode: string; - timeZone: string; -} - -export class UserSettingsService { - - /** - * Get user's unit system preference - * TODO: Integrate with actual user settings service - */ - static async getUserUnitSystem(userId: string): Promise { - // Placeholder implementation - replace with actual user settings lookup - // For now, default to Imperial - return UnitSystem.IMPERIAL; - } - - /** - * Get full user settings for fuel logs - */ - static async getUserSettings(userId: string): Promise { - // Placeholder implementation - return { - unitSystem: await this.getUserUnitSystem(userId), - currencyCode: 'USD', - timeZone: 'America/New_York' - }; - } - - /** - * Update user's unit system preference - */ - static async updateUserUnitSystem(userId: string, unitSystem: UnitSystem): Promise { - // Placeholder implementation - replace with actual user settings update - console.log(`Update user ${userId} unit system to ${unitSystem}`); - } -} -``` - -## Implementation Tasks - -### Fuel Type/Grade System -1. āœ… Create FuelGradeService with dynamic grade options -2. āœ… Implement fuel type validation logic -3. āœ… Add default grade selection -4. āœ… Create grade validation for each fuel type - -### Unit Conversion System -1. āœ… Create UnitConversionService with conversion factors -2. āœ… Implement volume/distance conversions -3. āœ… Add efficiency calculation methods -4. āœ… Create unit label management - -### Enhanced Calculations -1. āœ… Create EfficiencyCalculationService -2. āœ… Implement trip distance vs odometer logic -3. āœ… Add average efficiency calculations -4. āœ… Create total distance calculations - -### Advanced Validation -1. āœ… Create EnhancedValidationService -2. āœ… Implement comprehensive validation rules -3. āœ… Add business logic validation -4. āœ… Create warning system for unusual values - -### User Settings Integration -1. āœ… Create UserSettingsService interface -2. āœ… Add unit system preference lookup -3. āœ… Prepare for actual user settings integration - -## Testing Requirements - -### Unit Tests Required - -```typescript -// Test fuel grade service -describe('FuelGradeService', () => { - it('should return correct grades for gasoline', () => { - const grades = FuelGradeService.getFuelGradeOptions(FuelType.GASOLINE); - expect(grades).toHaveLength(5); - expect(grades[0].value).toBe('87'); - }); - - it('should validate grades correctly', () => { - expect(FuelGradeService.isValidGradeForFuelType(FuelType.GASOLINE, '87')).toBe(true); - expect(FuelGradeService.isValidGradeForFuelType(FuelType.GASOLINE, '#1')).toBe(false); - }); -}); - -// Test unit conversion service -describe('UnitConversionService', () => { - it('should convert gallons to liters correctly', () => { - const liters = UnitConversionService.convertFuelUnits(10, UnitSystem.IMPERIAL, UnitSystem.METRIC); - expect(liters).toBeCloseTo(37.85, 2); - }); - - it('should calculate MPG correctly', () => { - const mpg = UnitConversionService.calculateEfficiency(300, 10, UnitSystem.IMPERIAL); - expect(mpg).toBe(30); - }); -}); - -// Test efficiency calculation service -describe('EfficiencyCalculationService', () => { - it('should calculate efficiency from trip distance', () => { - const result = EfficiencyCalculationService.calculateEfficiency( - { tripDistance: 300, fuelUnits: 10 }, - null, - UnitSystem.IMPERIAL - ); - expect(result?.value).toBe(30); - expect(result?.calculationMethod).toBe('trip_distance'); - }); -}); - -// Test validation service -describe('EnhancedValidationService', () => { - it('should require distance input', () => { - const result = EnhancedValidationService.validateFuelLogData( - { fuelType: FuelType.GASOLINE, fuelUnits: 10, costPerUnit: 3.50 }, - UnitSystem.IMPERIAL - ); - expect(result.isValid).toBe(false); - expect(result.errors).toContain('Either odometer reading or trip distance is required'); - }); -}); -``` - -## Success Criteria - -### Phase 2 Complete When: -- āœ… Fuel type/grade system fully functional -- āœ… Imperial/Metric conversions working correctly -- āœ… Enhanced efficiency calculations implemented -- āœ… Advanced validation rules active -- āœ… User settings integration interface ready -- āœ… All business logic unit tested -- āœ… Integration with existing fuel logs service - -### Ready for Phase 3 When: -- All business logic services tested and functional -- Unit conversion system verified accurate -- Fuel grade system working correctly -- Validation rules catching all edge cases -- Ready for API integration - ---- - -**Next Phase**: [Phase 3 - API & Backend Implementation](FUEL-LOGS-PHASE-3.md) \ No newline at end of file diff --git a/docs/changes/fuel-logs-v1/FUEL-LOGS-PHASE-3.md b/docs/changes/fuel-logs-v1/FUEL-LOGS-PHASE-3.md deleted file mode 100644 index c1feff4..0000000 --- a/docs/changes/fuel-logs-v1/FUEL-LOGS-PHASE-3.md +++ /dev/null @@ -1,932 +0,0 @@ -# Phase 3: API & Backend Implementation - -## Overview -Update API contracts, implement enhanced backend services, create new endpoints, and build comprehensive test suite for the enhanced fuel logs system. - -## Prerequisites -- āœ… Phase 1 completed (database schema and core types) -- āœ… Phase 2 completed (enhanced business logic services) -- All business logic services tested and functional - -## Updated Service Layer - -### Enhanced Fuel Logs Service - -**File**: `backend/src/features/fuel-logs/domain/fuel-logs.service.ts` (Updated) - -```typescript -import { FuelLogsRepository } from '../data/fuel-logs.repository'; -import { - FuelLog, CreateFuelLogRequest, UpdateFuelLogRequest, - FuelLogResponse, FuelStats, UnitSystem -} from './fuel-logs.types'; -import { EnhancedValidationService } from './enhanced-validation.service'; -import { EfficiencyCalculationService } from './efficiency-calculation.service'; -import { UnitConversionService } from './unit-conversion.service'; -import { UserSettingsService } from '../external/user-settings.service'; -import { logger } from '../../../core/logging/logger'; -import { cacheService } from '../../../core/config/redis'; -import pool from '../../../core/config/database'; - -export class FuelLogsService { - private readonly cachePrefix = 'fuel-logs'; - private readonly cacheTTL = 300; // 5 minutes - - constructor(private repository: FuelLogsRepository) {} - - async createFuelLog(data: CreateFuelLogRequest, userId: string): Promise { - logger.info('Creating enhanced fuel log', { - userId, - vehicleId: data.vehicleId, - fuelType: data.fuelType, - hasTrip: !!data.tripDistance, - hasOdometer: !!data.odometerReading - }); - - // Get user settings for unit system - const userSettings = await UserSettingsService.getUserSettings(userId); - - // Enhanced validation - const validation = EnhancedValidationService.validateFuelLogData(data, userSettings.unitSystem); - if (!validation.isValid) { - throw new ValidationError(`Invalid fuel log data: ${validation.errors.join(', ')}`); - } - - // Log warnings - if (validation.warnings.length > 0) { - logger.warn('Fuel log validation warnings', { warnings: validation.warnings }); - } - - // Verify vehicle ownership - const vehicleCheck = await pool.query( - 'SELECT id FROM vehicles WHERE id = $1 AND user_id = $2', - [data.vehicleId, userId] - ); - - if (vehicleCheck.rows.length === 0) { - throw new Error('Vehicle not found or unauthorized'); - } - - // Calculate total cost - const totalCost = data.fuelUnits * data.costPerUnit; - - // Get previous log for efficiency calculation - const previousLog = data.odometerReading ? - await this.repository.getPreviousLogByOdometer(data.vehicleId, data.odometerReading) : - await this.repository.getLatestLogForVehicle(data.vehicleId); - - // Calculate efficiency - const efficiencyResult = EfficiencyCalculationService.calculateEfficiency( - { ...data, totalCost }, - previousLog, - userSettings.unitSystem - ); - - // Prepare fuel log data - const fuelLogData = { - ...data, - userId, - dateTime: new Date(data.dateTime), - totalCost, - mpg: efficiencyResult?.value || null, - efficiencyCalculationMethod: efficiencyResult?.calculationMethod || null - }; - - // Create fuel log - const fuelLog = await this.repository.create(fuelLogData); - - // Update vehicle odometer if provided - if (data.odometerReading) { - await pool.query( - 'UPDATE vehicles SET odometer_reading = $1 WHERE id = $2 AND (odometer_reading IS NULL OR odometer_reading < $1)', - [data.odometerReading, data.vehicleId] - ); - } - - // Invalidate caches - await this.invalidateCaches(userId, data.vehicleId); - - return this.toResponse(fuelLog, userSettings.unitSystem); - } - - async getFuelLogsByVehicle( - vehicleId: string, - userId: string, - options?: { unitSystem?: UnitSystem } - ): Promise { - - // Verify vehicle ownership - const vehicleCheck = await pool.query( - 'SELECT id FROM vehicles WHERE id = $1 AND user_id = $2', - [vehicleId, userId] - ); - - if (vehicleCheck.rows.length === 0) { - throw new Error('Vehicle not found or unauthorized'); - } - - // Get user settings - const userSettings = await UserSettingsService.getUserSettings(userId); - const unitSystem = options?.unitSystem || userSettings.unitSystem; - - const cacheKey = `${this.cachePrefix}:vehicle:${vehicleId}:${unitSystem}`; - - // Check cache - const cached = await cacheService.get(cacheKey); - if (cached) { - return cached; - } - - // Get from database - const logs = await this.repository.findByVehicleId(vehicleId); - const response = logs.map((log: FuelLog) => this.toResponse(log, unitSystem)); - - // Cache result - await cacheService.set(cacheKey, response, this.cacheTTL); - - return response; - } - - async getEnhancedVehicleStats(vehicleId: string, userId: string): Promise { - // Verify vehicle ownership - const vehicleCheck = await pool.query( - 'SELECT id FROM vehicles WHERE id = $1 AND user_id = $2', - [vehicleId, userId] - ); - - if (vehicleCheck.rows.length === 0) { - throw new Error('Vehicle not found or unauthorized'); - } - - const userSettings = await UserSettingsService.getUserSettings(userId); - const logs = await this.repository.findByVehicleId(vehicleId); - - if (logs.length === 0) { - return this.getEmptyStats(userSettings.unitSystem); - } - - // Calculate comprehensive stats - const totalFuelUnits = logs.reduce((sum, log) => sum + log.fuelUnits, 0); - const totalCost = logs.reduce((sum, log) => sum + log.totalCost, 0); - const averageCostPerUnit = totalCost / totalFuelUnits; - - const totalDistance = EfficiencyCalculationService.calculateTotalDistance(logs, userSettings.unitSystem); - const averageEfficiency = EfficiencyCalculationService.calculateAverageEfficiency(logs, userSettings.unitSystem); - - // Group by fuel type - const fuelTypeBreakdown = this.calculateFuelTypeBreakdown(logs, userSettings.unitSystem); - - // Calculate trends (last 30 days vs previous 30 days) - const trends = this.calculateEfficiencyTrends(logs, userSettings.unitSystem); - - const unitLabels = UnitConversionService.getUnitLabels(userSettings.unitSystem); - - return { - logCount: logs.length, - totalFuelUnits, - totalCost, - averageCostPerUnit, - totalDistance, - averageEfficiency: averageEfficiency?.value || 0, - fuelTypeBreakdown, - trends, - unitLabels, - dateRange: { - earliest: logs[logs.length - 1]?.dateTime, - latest: logs[0]?.dateTime - } - }; - } - - private toResponse(log: FuelLog, unitSystem: UnitSystem): FuelLogResponse { - const unitLabels = UnitConversionService.getUnitLabels(unitSystem); - - // Convert efficiency to user's unit system if needed - let displayEfficiency = log.mpg; - if (log.mpg && unitSystem === UnitSystem.METRIC) { - displayEfficiency = UnitConversionService.convertEfficiency( - log.mpg, - UnitSystem.IMPERIAL, // Assuming stored as MPG - UnitSystem.METRIC - ); - } - - return { - id: log.id, - userId: log.userId, - vehicleId: log.vehicleId, - dateTime: log.dateTime.toISOString(), - - // Distance information - odometerReading: log.odometerReading, - tripDistance: log.tripDistance, - - // Fuel information - fuelType: log.fuelType, - fuelGrade: log.fuelGrade, - fuelUnits: log.fuelUnits, - costPerUnit: log.costPerUnit, - totalCost: log.totalCost, - - // Location - locationData: log.locationData, - - // Calculated fields - efficiency: displayEfficiency, - efficiencyLabel: unitLabels.efficiencyUnits, - - // Metadata - notes: log.notes, - createdAt: log.createdAt.toISOString(), - updatedAt: log.updatedAt.toISOString(), - - // Legacy fields (for backward compatibility) - date: log.dateTime.toISOString().split('T')[0], - odometer: log.odometerReading, - gallons: log.fuelUnits, // May need conversion - pricePerGallon: log.costPerUnit, // May need conversion - mpg: log.mpg - }; - } -} -``` - -### New API Endpoints - -#### Fuel Grade Endpoint - -**File**: `backend/src/features/fuel-logs/api/fuel-grade.controller.ts` - -```typescript -import { FastifyRequest, FastifyReply } from 'fastify'; -import { FuelGradeService } from '../domain/fuel-grade.service'; -import { FuelType } from '../domain/fuel-logs.types'; -import { logger } from '../../../core/logging/logger'; - -export class FuelGradeController { - - async getFuelGrades( - request: FastifyRequest<{ Params: { fuelType: FuelType } }>, - reply: FastifyReply - ) { - try { - const { fuelType } = request.params; - - // Validate fuel type - if (!Object.values(FuelType).includes(fuelType)) { - return reply.code(400).send({ - error: 'Bad Request', - message: `Invalid fuel type: ${fuelType}` - }); - } - - const grades = FuelGradeService.getFuelGradeOptions(fuelType); - - return reply.code(200).send({ - fuelType, - grades - }); - } catch (error: any) { - logger.error('Error getting fuel grades', { error, fuelType: request.params.fuelType }); - return reply.code(500).send({ - error: 'Internal server error', - message: 'Failed to get fuel grades' - }); - } - } - - async getAllFuelTypes(request: FastifyRequest, reply: FastifyReply) { - try { - const fuelTypes = Object.values(FuelType).map(type => ({ - value: type, - label: type.charAt(0).toUpperCase() + type.slice(1), - grades: FuelGradeService.getFuelGradeOptions(type) - })); - - return reply.code(200).send({ fuelTypes }); - } catch (error: any) { - logger.error('Error getting fuel types', { error }); - return reply.code(500).send({ - error: 'Internal server error', - message: 'Failed to get fuel types' - }); - } - } -} -``` - -### Enhanced Routes - -**File**: `backend/src/features/fuel-logs/api/fuel-logs.routes.ts` (Updated) - -```typescript -import { FastifyInstance, FastifyPluginOptions } from 'fastify'; -import { FuelLogsController } from './fuel-logs.controller'; -import { FuelGradeController } from './fuel-grade.controller'; -import { - createFuelLogSchema, - updateFuelLogSchema, - fuelLogParamsSchema, - vehicleParamsSchema, - fuelTypeParamsSchema -} from './fuel-logs.validators'; - -export async function fuelLogsRoutes( - fastify: FastifyInstance, - options: FastifyPluginOptions -) { - const fuelLogsController = new FuelLogsController(); - const fuelGradeController = new FuelGradeController(); - - // Existing fuel log CRUD endpoints (enhanced) - fastify.post('/fuel-logs', { - preHandler: [fastify.authenticate], - schema: createFuelLogSchema - }, fuelLogsController.createFuelLog.bind(fuelLogsController)); - - fastify.get('/fuel-logs', { - preHandler: [fastify.authenticate] - }, fuelLogsController.getUserFuelLogs.bind(fuelLogsController)); - - fastify.get('/fuel-logs/:id', { - preHandler: [fastify.authenticate], - schema: { params: fuelLogParamsSchema } - }, fuelLogsController.getFuelLog.bind(fuelLogsController)); - - fastify.put('/fuel-logs/:id', { - preHandler: [fastify.authenticate], - schema: { - params: fuelLogParamsSchema, - body: updateFuelLogSchema - } - }, fuelLogsController.updateFuelLog.bind(fuelLogsController)); - - fastify.delete('/fuel-logs/:id', { - preHandler: [fastify.authenticate], - schema: { params: fuelLogParamsSchema } - }, fuelLogsController.deleteFuelLog.bind(fuelLogsController)); - - // Vehicle-specific endpoints (enhanced) - fastify.get('/fuel-logs/vehicle/:vehicleId', { - preHandler: [fastify.authenticate], - schema: { params: vehicleParamsSchema } - }, fuelLogsController.getFuelLogsByVehicle.bind(fuelLogsController)); - - fastify.get('/fuel-logs/vehicle/:vehicleId/stats', { - preHandler: [fastify.authenticate], - schema: { params: vehicleParamsSchema } - }, fuelLogsController.getEnhancedVehicleStats.bind(fuelLogsController)); - - // NEW: Fuel type/grade endpoints - fastify.get('/fuel-logs/fuel-types', { - preHandler: [fastify.authenticate] - }, fuelGradeController.getAllFuelTypes.bind(fuelGradeController)); - - fastify.get('/fuel-logs/fuel-grades/:fuelType', { - preHandler: [fastify.authenticate], - schema: { params: fuelTypeParamsSchema } - }, fuelGradeController.getFuelGrades.bind(fuelGradeController)); -} - -export function registerFuelLogsRoutes(fastify: FastifyInstance) { - return fastify.register(fuelLogsRoutes, { prefix: '/api' }); -} -``` - -### Enhanced Validation Schemas - -**File**: `backend/src/features/fuel-logs/api/fuel-logs.validators.ts` (Updated) - -```typescript -import { Type } from '@sinclair/typebox'; -import { FuelType } from '../domain/fuel-logs.types'; - -export const createFuelLogSchema = { - body: Type.Object({ - vehicleId: Type.String({ format: 'uuid' }), - dateTime: Type.String({ format: 'date-time' }), - - // Distance (one required) - odometerReading: Type.Optional(Type.Number({ minimum: 0 })), - tripDistance: Type.Optional(Type.Number({ minimum: 0 })), - - // Fuel system - fuelType: Type.Enum(FuelType), - fuelGrade: Type.Optional(Type.String()), - fuelUnits: Type.Number({ minimum: 0.01 }), - costPerUnit: Type.Number({ minimum: 0.01 }), - - // Location (optional) - locationData: Type.Optional(Type.Object({ - address: Type.Optional(Type.String()), - coordinates: Type.Optional(Type.Object({ - latitude: Type.Number({ minimum: -90, maximum: 90 }), - longitude: Type.Number({ minimum: -180, maximum: 180 }) - })), - googlePlaceId: Type.Optional(Type.String()), - stationName: Type.Optional(Type.String()) - })), - - notes: Type.Optional(Type.String({ maxLength: 500 })) - }), - response: { - 201: Type.Object({ - id: Type.String({ format: 'uuid' }), - userId: Type.String(), - vehicleId: Type.String({ format: 'uuid' }), - dateTime: Type.String({ format: 'date-time' }), - odometerReading: Type.Optional(Type.Number()), - tripDistance: Type.Optional(Type.Number()), - fuelType: Type.Enum(FuelType), - fuelGrade: Type.Optional(Type.String()), - fuelUnits: Type.Number(), - costPerUnit: Type.Number(), - totalCost: Type.Number(), - efficiency: Type.Optional(Type.Number()), - efficiencyLabel: Type.String(), - createdAt: Type.String({ format: 'date-time' }), - updatedAt: Type.String({ format: 'date-time' }) - }) - } -}; - -export const updateFuelLogSchema = { - body: Type.Partial(Type.Object({ - dateTime: Type.String({ format: 'date-time' }), - odometerReading: Type.Number({ minimum: 0 }), - tripDistance: Type.Number({ minimum: 0 }), - fuelType: Type.Enum(FuelType), - fuelGrade: Type.String(), - fuelUnits: Type.Number({ minimum: 0.01 }), - costPerUnit: Type.Number({ minimum: 0.01 }), - locationData: Type.Object({ - address: Type.Optional(Type.String()), - coordinates: Type.Optional(Type.Object({ - latitude: Type.Number({ minimum: -90, maximum: 90 }), - longitude: Type.Number({ minimum: -180, maximum: 180 }) - })), - googlePlaceId: Type.Optional(Type.String()), - stationName: Type.Optional(Type.String()) - }), - notes: Type.String({ maxLength: 500 }) - })) -}; - -export const fuelLogParamsSchema = Type.Object({ - id: Type.String({ format: 'uuid' }) -}); - -export const vehicleParamsSchema = Type.Object({ - vehicleId: Type.String({ format: 'uuid' }) -}); - -export const fuelTypeParamsSchema = Type.Object({ - fuelType: Type.Enum(FuelType) -}); -``` - -## Repository Layer Updates - -### Enhanced Repository - -**File**: `backend/src/features/fuel-logs/data/fuel-logs.repository.ts` (Updated) - -```typescript -import { Pool } from 'pg'; -import { FuelLog, CreateFuelLogData } from '../domain/fuel-logs.types'; - -export interface CreateFuelLogData { - userId: string; - vehicleId: string; - dateTime: Date; - odometerReading?: number; - tripDistance?: number; - fuelType: string; - fuelGrade?: string; - fuelUnits: number; - costPerUnit: number; - totalCost: number; - locationData?: any; - notes?: string; - mpg?: number; - efficiencyCalculationMethod?: string; -} - -export class FuelLogsRepository { - constructor(private pool: Pool) {} - - async create(data: CreateFuelLogData): Promise { - const query = ` - INSERT INTO fuel_logs ( - user_id, vehicle_id, date_time, odometer_reading, trip_distance, - fuel_type, fuel_grade, fuel_units, cost_per_unit, total_cost, - location_data, notes, mpg, efficiency_calculation_method, - created_at, updated_at - ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, NOW(), NOW() - ) RETURNING * - `; - - const values = [ - data.userId, - data.vehicleId, - data.dateTime, - data.odometerReading || null, - data.tripDistance || null, - data.fuelType, - data.fuelGrade || null, - data.fuelUnits, - data.costPerUnit, - data.totalCost, - data.locationData ? JSON.stringify(data.locationData) : null, - data.notes || null, - data.mpg || null, - data.efficiencyCalculationMethod || null - ]; - - const result = await this.pool.query(query, values); - return this.mapRowToFuelLog(result.rows[0]); - } - - async getPreviousLogByOdometer(vehicleId: string, currentOdometer: number): Promise { - const query = ` - SELECT * FROM fuel_logs - WHERE vehicle_id = $1 - AND odometer_reading IS NOT NULL - AND odometer_reading < $2 - ORDER BY odometer_reading DESC, date_time DESC - LIMIT 1 - `; - - const result = await this.pool.query(query, [vehicleId, currentOdometer]); - return result.rows.length > 0 ? this.mapRowToFuelLog(result.rows[0]) : null; - } - - async getLatestLogForVehicle(vehicleId: string): Promise { - const query = ` - SELECT * FROM fuel_logs - WHERE vehicle_id = $1 - ORDER BY date_time DESC, created_at DESC - LIMIT 1 - `; - - const result = await this.pool.query(query, [vehicleId]); - return result.rows.length > 0 ? this.mapRowToFuelLog(result.rows[0]) : null; - } - - async findByVehicleId(vehicleId: string): Promise { - const query = ` - SELECT * FROM fuel_logs - WHERE vehicle_id = $1 - ORDER BY date_time DESC, created_at DESC - `; - - const result = await this.pool.query(query, [vehicleId]); - return result.rows.map(row => this.mapRowToFuelLog(row)); - } - - private mapRowToFuelLog(row: any): FuelLog { - return { - id: row.id, - userId: row.user_id, - vehicleId: row.vehicle_id, - dateTime: row.date_time, - odometerReading: row.odometer_reading, - tripDistance: row.trip_distance, - fuelType: row.fuel_type, - fuelGrade: row.fuel_grade, - fuelUnits: parseFloat(row.fuel_units), - costPerUnit: parseFloat(row.cost_per_unit), - totalCost: parseFloat(row.total_cost), - locationData: row.location_data ? JSON.parse(row.location_data) : null, - notes: row.notes, - mpg: row.mpg ? parseFloat(row.mpg) : null, - createdAt: row.created_at, - updatedAt: row.updated_at, - - // Legacy field mapping - date: row.date_time, - odometer: row.odometer_reading, - gallons: parseFloat(row.fuel_units), // Assuming stored in user's preferred units - pricePerGallon: parseFloat(row.cost_per_unit) - }; - } -} -``` - -## Comprehensive Test Suite - -### Service Layer Tests - -**File**: `backend/src/features/fuel-logs/tests/unit/enhanced-fuel-logs.service.test.ts` - -```typescript -import { FuelLogsService } from '../../domain/fuel-logs.service'; -import { FuelLogsRepository } from '../../data/fuel-logs.repository'; -import { FuelType, UnitSystem } from '../../domain/fuel-logs.types'; -import { UserSettingsService } from '../../external/user-settings.service'; - -// Mock dependencies -jest.mock('../../data/fuel-logs.repository'); -jest.mock('../../external/user-settings.service'); -jest.mock('../../../core/config/database'); -jest.mock('../../../core/config/redis'); - -describe('Enhanced FuelLogsService', () => { - let service: FuelLogsService; - let mockRepository: jest.Mocked; - - beforeEach(() => { - mockRepository = new FuelLogsRepository({} as any) as jest.Mocked; - service = new FuelLogsService(mockRepository); - - // Mock user settings - (UserSettingsService.getUserSettings as jest.Mock).mockResolvedValue({ - unitSystem: UnitSystem.IMPERIAL, - currencyCode: 'USD', - timeZone: 'America/New_York' - }); - }); - - describe('createFuelLog', () => { - it('should create fuel log with trip distance', async () => { - const createData = { - vehicleId: 'vehicle-id', - dateTime: '2024-01-15T10:30:00Z', - tripDistance: 300, - fuelType: FuelType.GASOLINE, - fuelGrade: '87', - fuelUnits: 10, - costPerUnit: 3.50, - notes: 'Test fuel log' - }; - - // Mock vehicle check - (pool.query as jest.Mock) - .mockResolvedValueOnce({ rows: [{ id: 'vehicle-id' }] }) // Vehicle exists - .mockResolvedValueOnce({}); // Odometer update (not applicable for trip distance) - - mockRepository.create.mockResolvedValue({ - id: 'fuel-log-id', - userId: 'user-id', - ...createData, - totalCost: 35.0, - mpg: 30, - createdAt: new Date(), - updatedAt: new Date() - } as any); - - const result = await service.createFuelLog(createData, 'user-id'); - - expect(result.id).toBe('fuel-log-id'); - expect(result.totalCost).toBe(35.0); - expect(result.efficiency).toBe(30); - expect(mockRepository.create).toHaveBeenCalledWith( - expect.objectContaining({ - tripDistance: 300, - totalCost: 35.0 - }) - ); - }); - - it('should validate distance requirement', async () => { - const createData = { - vehicleId: 'vehicle-id', - dateTime: '2024-01-15T10:30:00Z', - fuelType: FuelType.GASOLINE, - fuelGrade: '87', - fuelUnits: 10, - costPerUnit: 3.50 - // Missing both tripDistance and odometerReading - }; - - await expect(service.createFuelLog(createData, 'user-id')) - .rejects.toThrow('Either odometer reading or trip distance is required'); - }); - - it('should validate fuel grade for fuel type', async () => { - const createData = { - vehicleId: 'vehicle-id', - dateTime: '2024-01-15T10:30:00Z', - tripDistance: 300, - fuelType: FuelType.GASOLINE, - fuelGrade: '#1', // Invalid for gasoline - fuelUnits: 10, - costPerUnit: 3.50 - }; - - await expect(service.createFuelLog(createData, 'user-id')) - .rejects.toThrow('Invalid fuel grade'); - }); - }); - - describe('getEnhancedVehicleStats', () => { - it('should calculate comprehensive vehicle statistics', async () => { - const mockLogs = [ - { - fuelUnits: 10, - totalCost: 35, - tripDistance: 300, - mpg: 30, - fuelType: FuelType.GASOLINE, - dateTime: new Date('2024-01-15') - }, - { - fuelUnits: 12, - totalCost: 42, - tripDistance: 350, - mpg: 29, - fuelType: FuelType.GASOLINE, - dateTime: new Date('2024-01-10') - } - ]; - - // Mock vehicle check - (pool.query as jest.Mock).mockResolvedValue({ rows: [{ id: 'vehicle-id' }] }); - - mockRepository.findByVehicleId.mockResolvedValue(mockLogs as any); - - const stats = await service.getEnhancedVehicleStats('vehicle-id', 'user-id'); - - expect(stats.logCount).toBe(2); - expect(stats.totalFuelUnits).toBe(22); - expect(stats.totalCost).toBe(77); - expect(stats.averageCostPerUnit).toBeCloseTo(3.5, 2); - expect(stats.totalDistance).toBe(650); - expect(stats.averageEfficiency).toBeCloseTo(29.5, 1); - }); - }); -}); -``` - -### Integration Tests - -**File**: `backend/src/features/fuel-logs/tests/integration/enhanced-fuel-logs.integration.test.ts` - -```typescript -import request from 'supertest'; -import { app } from '../../../app'; -import { pool } from '../../../core/config/database'; -import { FuelType } from '../../domain/fuel-logs.types'; - -describe('Enhanced Fuel Logs API Integration', () => { - let authToken: string; - let vehicleId: string; - - beforeAll(async () => { - // Setup test data - authToken = await getTestAuthToken(); - vehicleId = await createTestVehicle(); - }); - - afterAll(async () => { - // Cleanup - await cleanupTestData(); - await pool.end(); - }); - - describe('POST /api/fuel-logs', () => { - it('should create fuel log with enhanced fields', async () => { - const fuelLogData = { - vehicleId, - dateTime: '2024-01-15T10:30:00Z', - tripDistance: 300, - fuelType: FuelType.GASOLINE, - fuelGrade: '87', - fuelUnits: 10, - costPerUnit: 3.50, - locationData: { - address: '123 Main St, Anytown, USA', - stationName: 'Shell Station' - }, - notes: 'Full tank' - }; - - const response = await request(app) - .post('/api/fuel-logs') - .set('Authorization', `Bearer ${authToken}`) - .send(fuelLogData) - .expect(201); - - expect(response.body.id).toBeDefined(); - expect(response.body.tripDistance).toBe(300); - expect(response.body.fuelType).toBe(FuelType.GASOLINE); - expect(response.body.fuelGrade).toBe('87'); - expect(response.body.totalCost).toBe(35.0); - expect(response.body.efficiency).toBe(30); // 300 miles / 10 gallons - expect(response.body.efficiencyLabel).toBe('mpg'); - }); - - it('should validate distance requirement', async () => { - const fuelLogData = { - vehicleId, - dateTime: '2024-01-15T10:30:00Z', - fuelType: FuelType.GASOLINE, - fuelGrade: '87', - fuelUnits: 10, - costPerUnit: 3.50 - // Missing both tripDistance and odometerReading - }; - - const response = await request(app) - .post('/api/fuel-logs') - .set('Authorization', `Bearer ${authToken}`) - .send(fuelLogData) - .expect(400); - - expect(response.body.message).toContain('Either odometer reading or trip distance is required'); - }); - }); - - describe('GET /api/fuel-logs/fuel-grades/:fuelType', () => { - it('should return gasoline fuel grades', async () => { - const response = await request(app) - .get('/api/fuel-logs/fuel-grades/gasoline') - .set('Authorization', `Bearer ${authToken}`) - .expect(200); - - expect(response.body.fuelType).toBe('gasoline'); - expect(response.body.grades).toHaveLength(5); - expect(response.body.grades[0]).toEqual({ - value: '87', - label: '87 (Regular)', - description: 'Regular unleaded gasoline' - }); - }); - - it('should return empty grades for electric', async () => { - const response = await request(app) - .get('/api/fuel-logs/fuel-grades/electric') - .set('Authorization', `Bearer ${authToken}`) - .expect(200); - - expect(response.body.fuelType).toBe('electric'); - expect(response.body.grades).toHaveLength(0); - }); - }); - - describe('GET /api/fuel-logs/fuel-types', () => { - it('should return all fuel types with grades', async () => { - const response = await request(app) - .get('/api/fuel-logs/fuel-types') - .set('Authorization', `Bearer ${authToken}`) - .expect(200); - - expect(response.body.fuelTypes).toHaveLength(3); - - const gasoline = response.body.fuelTypes.find(ft => ft.value === 'gasoline'); - expect(gasoline.grades).toHaveLength(5); - - const electric = response.body.fuelTypes.find(ft => ft.value === 'electric'); - expect(electric.grades).toHaveLength(0); - }); - }); -}); -``` - -## Implementation Tasks - -### Service Layer Updates -1. āœ… Update FuelLogsService with enhanced business logic -2. āœ… Integrate validation and efficiency calculation services -3. āœ… Add user settings integration -4. āœ… Implement comprehensive stats calculations - -### API Layer Updates -1. āœ… Create FuelGradeController for dynamic grades -2. āœ… Update existing controllers with enhanced validation -3. āœ… Add new API endpoints for fuel types/grades -4. āœ… Update validation schemas - -### Repository Updates -1. āœ… Update repository for new database fields -2. āœ… Add methods for enhanced queries -3. āœ… Implement proper data mapping - -### Testing Implementation -1. āœ… Create comprehensive unit test suite -2. āœ… Implement integration tests for all endpoints -3. āœ… Add validation testing -4. āœ… Test business logic edge cases - -## Success Criteria - -### Phase 3 Complete When: -- āœ… All API endpoints functional with enhanced data -- āœ… Comprehensive validation working correctly -- āœ… Fuel type/grade system fully operational -- āœ… Unit conversion integration functional -- āœ… Enhanced statistics calculations working -- āœ… Complete test suite passes (>90% coverage) -- āœ… All new endpoints documented and tested -- āœ… Backward compatibility maintained - -### Ready for Phase 4 When: -- All backend services tested and stable -- API contracts finalized and documented -- Frontend integration points clearly defined -- Enhanced business logic fully functional - ---- - -**Next Phase**: [Phase 4 - Frontend Implementation](FUEL-LOGS-PHASE-4.md) \ No newline at end of file diff --git a/docs/changes/fuel-logs-v1/FUEL-LOGS-PHASE-4.md b/docs/changes/fuel-logs-v1/FUEL-LOGS-PHASE-4.md deleted file mode 100644 index ec789b4..0000000 --- a/docs/changes/fuel-logs-v1/FUEL-LOGS-PHASE-4.md +++ /dev/null @@ -1,1080 +0,0 @@ -# Phase 4: Frontend Implementation - -## Overview -Create comprehensive React components for the enhanced fuel logs feature with dynamic forms, real-time calculations, Imperial/Metric support, and mobile-optimized UI. - -## Prerequisites -- āœ… Phase 1-3 completed (database, business logic, API) -- All backend services tested and functional -- API endpoints available and documented - -## Component Architecture - -### Main Components Structure -``` -frontend/src/features/fuel-logs/ -ā”œā”€ā”€ components/ -│ ā”œā”€ā”€ FuelLogForm.tsx # Main form component -│ ā”œā”€ā”€ VehicleSelector.tsx # Vehicle dropdown -│ ā”œā”€ā”€ DistanceInput.tsx # Trip/odometer toggle -│ ā”œā”€ā”€ FuelTypeSelector.tsx # Fuel type with cascading grades -│ ā”œā”€ā”€ UnitSystemDisplay.tsx # Imperial/Metric formatting -│ ā”œā”€ā”€ LocationInput.tsx # Future Google Maps integration -│ ā”œā”€ā”€ CostCalculator.tsx # Real-time cost calculation -│ └── FuelLogsList.tsx # Enhanced logs display -ā”œā”€ā”€ hooks/ -│ ā”œā”€ā”€ useFuelLogs.tsx # Fuel logs API integration -│ ā”œā”€ā”€ useFuelGrades.tsx # Dynamic fuel grades -│ ā”œā”€ā”€ useUserSettings.tsx # Unit system preferences -│ └── useFormValidation.tsx # Enhanced form validation -ā”œā”€ā”€ utils/ -│ ā”œā”€ā”€ unitConversion.ts # Frontend unit utilities -│ ā”œā”€ā”€ fuelGradeUtils.ts # Fuel grade helpers -│ └── formValidation.ts # Client-side validation -ā”œā”€ā”€ types/ -│ └── fuel-logs.types.ts # Frontend type definitions -└── pages/ - ā”œā”€ā”€ FuelLogsPage.tsx # Main fuel logs page - └── FuelLogDetailPage.tsx # Individual log details -``` - -## Core Components - -### Enhanced Fuel Log Form - -**File**: `frontend/src/features/fuel-logs/components/FuelLogForm.tsx` - -```tsx -import React, { useState, useEffect } from 'react'; -import { useForm, Controller } from 'react-hook-form'; -import { yupResolver } from '@hookform/resolvers/yup'; -import * as yup from 'yup'; -import { - Box, - Button, - Card, - CardContent, - CardHeader, - Typography, - Alert, - Grid, - Switch, - FormControlLabel -} from '@mui/material'; - -import { VehicleSelector } from './VehicleSelector'; -import { DistanceInput } from './DistanceInput'; -import { FuelTypeSelector } from './FuelTypeSelector'; -import { UnitSystemDisplay } from './UnitSystemDisplay'; -import { LocationInput } from './LocationInput'; -import { CostCalculator } from './CostCalculator'; - -import { useFuelLogs } from '../hooks/useFuelLogs'; -import { useUserSettings } from '../hooks/useUserSettings'; -import { CreateFuelLogRequest, FuelType, DistanceType } from '../types/fuel-logs.types'; - -// Validation schema -const createFuelLogSchema = yup.object({ - vehicleId: yup.string().required('Vehicle is required'), - dateTime: yup.date().max(new Date(), 'Cannot create logs in the future').required('Date/time is required'), - distanceType: yup.string().oneOf(['odometer', 'trip']).required(), - odometerReading: yup.number().when('distanceType', { - is: 'odometer', - then: yup.number().min(1, 'Odometer must be positive').required('Odometer reading is required'), - otherwise: yup.number().nullable() - }), - tripDistance: yup.number().when('distanceType', { - is: 'trip', - then: yup.number().min(0.1, 'Trip distance must be positive').required('Trip distance is required'), - otherwise: yup.number().nullable() - }), - fuelType: yup.string().oneOf(Object.values(FuelType)).required('Fuel type is required'), - fuelGrade: yup.string().nullable(), - fuelUnits: yup.number().min(0.01, 'Fuel amount must be positive').required('Fuel amount is required'), - costPerUnit: yup.number().min(0.01, 'Cost per unit must be positive').required('Cost per unit is required'), - locationData: yup.object().nullable(), - notes: yup.string().max(500, 'Notes cannot exceed 500 characters') -}); - -interface FuelLogFormProps { - onSuccess?: () => void; - onCancel?: () => void; - initialData?: Partial; -} - -export const FuelLogForm: React.FC = ({ - onSuccess, - onCancel, - initialData -}) => { - const { createFuelLog, isLoading } = useFuelLogs(); - const { userSettings } = useUserSettings(); - const [distanceType, setDistanceType] = useState('trip'); - const [calculatedCost, setCalculatedCost] = useState(0); - - const { - control, - handleSubmit, - watch, - setValue, - formState: { errors, isValid } - } = useForm({ - resolver: yupResolver(createFuelLogSchema), - defaultValues: { - dateTime: new Date().toISOString().slice(0, 16), // Current datetime - distanceType: 'trip', - fuelType: FuelType.GASOLINE, - ...initialData - }, - mode: 'onChange' - }); - - // Watch form values for real-time calculations - const watchedValues = watch(['fuelUnits', 'costPerUnit', 'fuelType']); - const [fuelUnits, costPerUnit, fuelType] = watchedValues; - - // Real-time cost calculation - useEffect(() => { - if (fuelUnits && costPerUnit) { - const cost = fuelUnits * costPerUnit; - setCalculatedCost(cost); - } - }, [fuelUnits, costPerUnit]); - - const onSubmit = async (data: CreateFuelLogRequest) => { - try { - // Prepare submission data - const submitData = { - ...data, - totalCost: calculatedCost, - // Clear unused distance field - odometerReading: distanceType === 'odometer' ? data.odometerReading : undefined, - tripDistance: distanceType === 'trip' ? data.tripDistance : undefined - }; - - await createFuelLog(submitData); - onSuccess?.(); - } catch (error) { - console.error('Failed to create fuel log:', error); - } - }; - - return ( - - - } - /> - -
- - - {/* Vehicle Selection */} - - ( - - )} - /> - - - {/* Date/Time */} - - ( - - )} - /> - - - {/* Distance Type Toggle */} - - { - const newType = e.target.checked ? 'odometer' : 'trip'; - setDistanceType(newType); - setValue('distanceType', newType); - // Clear the unused field - if (newType === 'odometer') { - setValue('tripDistance', undefined); - } else { - setValue('odometerReading', undefined); - } - }} - /> - } - label={`Use ${distanceType === 'odometer' ? 'Odometer Reading' : 'Trip Distance'}`} - /> - - - {/* Distance Input */} - - ( - - )} - /> - - - {/* Fuel Type & Grade */} - - ( - ( - - )} - /> - )} - /> - - - {/* Fuel Amount */} - - ( - - )} - /> - - - {/* Cost Per Unit */} - - ( - $ - }} - /> - )} - /> - - - {/* Real-time Total Cost Display */} - - - - - {/* Location */} - - ( - - )} - /> - - - {/* Notes */} - - ( - - )} - /> - - - {/* Form Actions */} - - - {onCancel && ( - - )} - - - - -
-
-
- ); -}; -``` - -### Dynamic Fuel Type Selector - -**File**: `frontend/src/features/fuel-logs/components/FuelTypeSelector.tsx` - -```tsx -import React, { useEffect } from 'react'; -import { - FormControl, - InputLabel, - Select, - MenuItem, - Grid, - FormHelperText -} from '@mui/material'; -import { FuelType, FuelGrade } from '../types/fuel-logs.types'; -import { useFuelGrades } from '../hooks/useFuelGrades'; - -interface FuelTypeSelectorProps { - fuelType: FuelType; - fuelGrade?: FuelGrade; - onFuelTypeChange: (fuelType: FuelType) => void; - onFuelGradeChange: (fuelGrade?: FuelGrade) => void; - error?: string; - disabled?: boolean; -} - -export const FuelTypeSelector: React.FC = ({ - fuelType, - fuelGrade, - onFuelTypeChange, - onFuelGradeChange, - error, - disabled = false -}) => { - const { fuelGrades, isLoading } = useFuelGrades(fuelType); - - // Clear fuel grade when fuel type changes and grades don't include current grade - useEffect(() => { - if (fuelGrade && fuelGrades && !fuelGrades.some(grade => grade.value === fuelGrade)) { - onFuelGradeChange(undefined); - } - }, [fuelType, fuelGrades, fuelGrade, onFuelGradeChange]); - - // Auto-select default grade when fuel type changes - useEffect(() => { - if (!fuelGrade && fuelGrades && fuelGrades.length > 0) { - // Auto-select first grade (typically the most common) - onFuelGradeChange(fuelGrades[0].value); - } - }, [fuelGrades, fuelGrade, onFuelGradeChange]); - - return ( - - {/* Fuel Type */} - - - Fuel Type - - {error && {error}} - - - - {/* Fuel Grade (conditional) */} - - - - Fuel Grade {fuelType === FuelType.ELECTRIC ? '(N/A for Electric)' : ''} - - - {fuelType !== FuelType.ELECTRIC && ( - - {isLoading ? 'Loading grades...' : 'Select appropriate fuel grade'} - - )} - - - - ); -}; -``` - -### Vehicle Selector Component - -**File**: `frontend/src/features/fuel-logs/components/VehicleSelector.tsx` - -```tsx -import React from 'react'; -import { - FormControl, - InputLabel, - Select, - MenuItem, - FormHelperText, - Box, - Typography -} from '@mui/material'; -import { DirectionsCar } from '@mui/icons-material'; -import { useVehicles } from '../../vehicles/hooks/useVehicles'; -import { Vehicle } from '../../vehicles/types/vehicle.types'; - -interface VehicleSelectorProps { - value: string; - onChange: (vehicleId: string) => void; - error?: string; - required?: boolean; - disabled?: boolean; -} - -export const VehicleSelector: React.FC = ({ - value, - onChange, - error, - required = false, - disabled = false -}) => { - const { vehicles, isLoading } = useVehicles(); - - const formatVehicleDisplay = (vehicle: Vehicle): string => { - const parts = [vehicle.year, vehicle.make, vehicle.model, vehicle.nickname] - .filter(Boolean); - return parts.join(' '); - }; - - if (isLoading) { - return ( - - Loading vehicles... - - - ); - } - - if (!vehicles || vehicles.length === 0) { - return ( - - - - - No vehicles found - - - - You need to add a vehicle before creating fuel logs.{' '} - - Add your first vehicle - - - - ); - } - - return ( - - Select Vehicle - - {error && {error}} - - ); -}; -``` - -### Distance Input Component - -**File**: `frontend/src/features/fuel-logs/components/DistanceInput.tsx` - -```tsx -import React from 'react'; -import { - TextField, - InputAdornment, - FormHelperText, - Box, - Typography -} from '@mui/material'; -import { UnitSystem, DistanceType } from '../types/fuel-logs.types'; - -interface DistanceInputProps { - type: DistanceType; - value?: number; - onChange: (value: number) => void; - unitSystem?: UnitSystem; - error?: string; - disabled?: boolean; -} - -export const DistanceInput: React.FC = ({ - type, - value, - onChange, - unitSystem = UnitSystem.IMPERIAL, - error, - disabled = false -}) => { - const getUnits = () => { - return unitSystem === UnitSystem.IMPERIAL ? 'miles' : 'kilometers'; - }; - - const getLabel = () => { - if (type === 'odometer') { - return `Odometer Reading (${getUnits()})`; - } - return `Trip Distance (${getUnits()})`; - }; - - const getHelperText = () => { - if (error) return error; - - if (type === 'odometer') { - return 'Current odometer reading on your vehicle'; - } - return 'Distance traveled since last fuel log'; - }; - - const getPlaceholder = () => { - if (type === 'odometer') { - return unitSystem === UnitSystem.IMPERIAL ? '125,000' : '201,168'; - } - return unitSystem === UnitSystem.IMPERIAL ? '300' : '483'; - }; - - return ( - - onChange(parseFloat(e.target.value) || 0)} - fullWidth - error={!!error} - disabled={disabled} - placeholder={getPlaceholder()} - inputProps={{ - step: type === 'trip' ? 0.1 : 1, - min: 0 - }} - InputProps={{ - endAdornment: ( - - {getUnits()} - - ) - }} - /> - - {getHelperText()} - - - {type === 'odometer' && ( - - šŸ’” Tip: Use trip distance if you don't want to track odometer readings - - )} - - ); -}; -``` - -### Real-time Cost Calculator - -**File**: `frontend/src/features/fuel-logs/components/CostCalculator.tsx` - -```tsx -import React from 'react'; -import { - Box, - Typography, - Card, - CardContent, - Divider, - Chip -} from '@mui/material'; -import { UnitSystem } from '../types/fuel-logs.types'; - -interface CostCalculatorProps { - fuelUnits?: number; - costPerUnit?: number; - calculatedCost: number; - unitSystem?: UnitSystem; -} - -export const CostCalculator: React.FC = ({ - fuelUnits, - costPerUnit, - calculatedCost, - unitSystem = UnitSystem.IMPERIAL -}) => { - const unitLabel = unitSystem === UnitSystem.IMPERIAL ? 'gallons' : 'liters'; - - if (!fuelUnits || !costPerUnit) { - return ( - - - - Enter fuel amount and cost per unit to see total cost - - - - ); - } - - return ( - - - - - Cost Calculation - - - - - - - {fuelUnits.toFixed(3)} {unitLabel} Ɨ ${costPerUnit.toFixed(3)} - - - ${calculatedCost.toFixed(2)} - - - - - - - Total cost will be automatically calculated - - - - ); -}; -``` - -## Custom Hooks - -### Fuel Logs API Hook - -**File**: `frontend/src/features/fuel-logs/hooks/useFuelLogs.tsx` - -```tsx -import { useState } from 'react'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { fuelLogsApi } from '../api/fuel-logs.api'; -import { - CreateFuelLogRequest, - FuelLogResponse, - EnhancedFuelStats -} from '../types/fuel-logs.types'; - -export const useFuelLogs = (vehicleId?: string) => { - const queryClient = useQueryClient(); - - // Fetch user's fuel logs - const { - data: fuelLogs, - isLoading, - error - } = useQuery({ - queryKey: ['fuelLogs', vehicleId], - queryFn: () => vehicleId ? - fuelLogsApi.getFuelLogsByVehicle(vehicleId) : - fuelLogsApi.getUserFuelLogs(), - enabled: true - }); - - // Create fuel log mutation - const createMutation = useMutation({ - mutationFn: (data: CreateFuelLogRequest) => fuelLogsApi.createFuelLog(data), - onSuccess: () => { - // Invalidate relevant queries - queryClient.invalidateQueries({ queryKey: ['fuelLogs'] }); - queryClient.invalidateQueries({ queryKey: ['fuelStats'] }); - queryClient.invalidateQueries({ queryKey: ['vehicles'] }); // For odometer updates - } - }); - - // Update fuel log mutation - const updateMutation = useMutation({ - mutationFn: ({ id, data }: { id: string; data: UpdateFuelLogRequest }) => - fuelLogsApi.updateFuelLog(id, data), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['fuelLogs'] }); - queryClient.invalidateQueries({ queryKey: ['fuelStats'] }); - } - }); - - // Delete fuel log mutation - const deleteMutation = useMutation({ - mutationFn: (id: string) => fuelLogsApi.deleteFuelLog(id), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['fuelLogs'] }); - queryClient.invalidateQueries({ queryKey: ['fuelStats'] }); - } - }); - - return { - fuelLogs, - isLoading, - error, - createFuelLog: createMutation.mutateAsync, - updateFuelLog: updateMutation.mutateAsync, - deleteFuelLog: deleteMutation.mutateAsync, - isCreating: createMutation.isPending, - isUpdating: updateMutation.isPending, - isDeleting: deleteMutation.isPending - }; -}; - -export const useFuelStats = (vehicleId: string) => { - return useQuery({ - queryKey: ['fuelStats', vehicleId], - queryFn: () => fuelLogsApi.getVehicleStats(vehicleId), - enabled: !!vehicleId - }); -}; -``` - -### Dynamic Fuel Grades Hook - -**File**: `frontend/src/features/fuel-logs/hooks/useFuelGrades.tsx` - -```tsx -import { useQuery } from '@tanstack/react-query'; -import { fuelLogsApi } from '../api/fuel-logs.api'; -import { FuelType } from '../types/fuel-logs.types'; - -export const useFuelGrades = (fuelType?: FuelType) => { - const { - data: fuelGrades, - isLoading, - error - } = useQuery({ - queryKey: ['fuelGrades', fuelType], - queryFn: () => fuelLogsApi.getFuelGrades(fuelType!), - enabled: !!fuelType, - staleTime: 1000 * 60 * 60, // 1 hour (grades don't change often) - }); - - return { - fuelGrades: fuelGrades?.grades || [], - isLoading, - error - }; -}; - -export const useFuelTypes = () => { - return useQuery({ - queryKey: ['fuelTypes'], - queryFn: () => fuelLogsApi.getAllFuelTypes(), - staleTime: 1000 * 60 * 60 * 24, // 24 hours (fuel types are static) - }); -}; -``` - -## Main Pages - -### Fuel Logs Page - -**File**: `frontend/src/features/fuel-logs/pages/FuelLogsPage.tsx` - -```tsx -import React, { useState } from 'react'; -import { - Container, - Typography, - Box, - Button, - Tabs, - Tab, - Grid, - Card, - CardContent -} from '@mui/material'; -import { Add as AddIcon, Analytics as AnalyticsIcon } from '@mui/icons-material'; - -import { FuelLogForm } from '../components/FuelLogForm'; -import { FuelLogsList } from '../components/FuelLogsList'; -import { FuelStatsCard } from '../components/FuelStatsCard'; -import { VehicleSelector } from '../components/VehicleSelector'; - -import { useFuelLogs } from '../hooks/useFuelLogs'; -import { useVehicles } from '../../vehicles/hooks/useVehicles'; - -export const FuelLogsPage: React.FC = () => { - const [selectedVehicleId, setSelectedVehicleId] = useState(''); - const [activeTab, setActiveTab] = useState(0); - const [showAddForm, setShowAddForm] = useState(false); - - const { vehicles } = useVehicles(); - const { fuelLogs, isLoading } = useFuelLogs(selectedVehicleId || undefined); - - // Auto-select first vehicle if only one exists - React.useEffect(() => { - if (vehicles && vehicles.length === 1 && !selectedVehicleId) { - setSelectedVehicleId(vehicles[0].id); - } - }, [vehicles, selectedVehicleId]); - - const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { - setActiveTab(newValue); - }; - - if (showAddForm) { - return ( - - - setShowAddForm(false)} - onCancel={() => setShowAddForm(false)} - /> - - - ); - } - - return ( - - - {/* Header */} - - - Fuel Logs - - - - - {/* Vehicle Selection */} - - - - - - - - - - - {fuelLogs?.length || 0} fuel logs recorded - - - - - - - - {/* Content Tabs */} - {selectedVehicleId && ( - <> - - - - - - - - {/* Tab Panels */} - {activeTab === 0 && ( - { - // Navigate to edit form or open modal - console.log('Edit fuel log:', logId); - }} - /> - )} - - {activeTab === 1 && ( - - )} - - )} - - {/* Empty State */} - {!selectedVehicleId && vehicles && vehicles.length > 1 && ( - - - - Select a Vehicle - - - Choose a vehicle to view and manage its fuel logs - - - - )} - - - ); -}; -``` - -## Success Criteria - -### Phase 4 Complete When: -- āœ… Enhanced fuel log form fully functional -- āœ… Dynamic fuel type/grade selection working -- āœ… Imperial/Metric units display correctly -- āœ… Real-time cost calculation working -- āœ… Trip distance vs odometer toggle functional -- āœ… Vehicle selection integrated -- āœ… Mobile-responsive design -- āœ… Form validation comprehensive -- āœ… API integration complete -- āœ… Error handling robust - -### Ready for Phase 5 When: -- All React components tested and functional -- User interface intuitive and mobile-friendly -- Form validation catching all user errors -- API integration stable and performant -- Ready for location service integration - ---- - -**Next Phase**: [Phase 5 - Future Integration Preparation](FUEL-LOGS-PHASE-5.md) \ No newline at end of file diff --git a/docs/changes/fuel-logs-v1/FUEL-LOGS-PHASE-5.md b/docs/changes/fuel-logs-v1/FUEL-LOGS-PHASE-5.md deleted file mode 100644 index d170563..0000000 --- a/docs/changes/fuel-logs-v1/FUEL-LOGS-PHASE-5.md +++ /dev/null @@ -1,1132 +0,0 @@ -# Phase 5: Future Integration Preparation - -## Overview -Design and prepare architecture for future Google Maps integration, location services, station price data, and extensibility for advanced fuel management features. - -## Prerequisites -- āœ… Phases 1-4 completed (database, business logic, API, frontend) -- All core fuel logs functionality tested and working -- Location data structure in place - -## Google Maps Integration Architecture - -### Service Architecture Design - -``` -Location Services Architecture -ā”œā”€ā”€ LocationService (Interface) -│ ā”œā”€ā”€ GoogleMapsService (Implementation) -│ ā”œā”€ā”€ MockLocationService (Testing) -│ └── Future: MapboxService, HereService -ā”œā”€ā”€ StationService (Interface) -│ ā”œā”€ā”€ GooglePlacesStationService -│ └── Future: GasBuddyService, AAA Service -└── PricingService (Interface) - ā”œā”€ā”€ GooglePlacesPricingService - └── Future: GasBuddyPricingService -``` - -### Location Service Interface - -**File**: `backend/src/features/fuel-logs/external/location.service.interface.ts` - -```typescript -export interface Coordinates { - latitude: number; - longitude: number; -} - -export interface Address { - streetNumber?: string; - streetName?: string; - city: string; - state: string; - zipCode?: string; - country: string; - formattedAddress: string; -} - -export interface LocationSearchResult { - placeId: string; - name: string; - address: Address; - coordinates: Coordinates; - placeTypes: string[]; - rating?: number; - priceLevel?: number; - isOpen?: boolean; - distance?: number; // meters from search center -} - -export interface StationSearchOptions { - coordinates: Coordinates; - radius?: number; // meters, default 5000 (5km) - fuelTypes?: string[]; // ['gasoline', 'diesel', 'electric'] - maxResults?: number; // default 20 - openNow?: boolean; - priceRange?: 'inexpensive' | 'moderate' | 'expensive' | 'very_expensive'; -} - -export interface FuelPrice { - fuelType: string; - fuelGrade?: string; - pricePerUnit: number; - currency: string; - lastUpdated: Date; - source: string; -} - -export interface FuelStation extends LocationSearchResult { - fuelTypes: string[]; - amenities: string[]; - brands: string[]; - currentPrices?: FuelPrice[]; - hasCarWash?: boolean; - hasConvenienceStore?: boolean; - hasRestrooms?: boolean; - hasAirPump?: boolean; -} - -export abstract class LocationService { - abstract searchNearbyStations(options: StationSearchOptions): Promise; - abstract getStationDetails(placeId: string): Promise; - abstract getCurrentPrices(placeId: string): Promise; - abstract geocodeAddress(address: string): Promise; - abstract reverseGeocode(coordinates: Coordinates): Promise
; -} -``` - -### Google Maps Service Implementation - -**File**: `backend/src/features/fuel-logs/external/google-maps.service.ts` - -```typescript -import { LocationService, StationSearchOptions, FuelStation, Coordinates, Address, FuelPrice } from './location.service.interface'; -import { logger } from '../../../core/logging/logger'; - -export class GoogleMapsService extends LocationService { - private apiKey: string; - private baseUrl = 'https://maps.googleapis.com/maps/api'; - - constructor() { - super(); - this.apiKey = process.env.GOOGLE_MAPS_API_KEY || ''; - if (!this.apiKey) { - throw new Error('GOOGLE_MAPS_API_KEY environment variable is required'); - } - } - - async searchNearbyStations(options: StationSearchOptions): Promise { - try { - logger.info('Searching nearby fuel stations', { - coordinates: options.coordinates, - radius: options.radius - }); - - const radius = options.radius || 5000; - const location = `${options.coordinates.latitude},${options.coordinates.longitude}`; - - // Use Google Places API to find gas stations - const url = `${this.baseUrl}/place/nearbysearch/json`; - const params = new URLSearchParams({ - location, - radius: radius.toString(), - type: 'gas_station', - key: this.apiKey - }); - - if (options.openNow) { - params.append('opennow', 'true'); - } - - const response = await fetch(`${url}?${params}`); - const data = await response.json(); - - if (data.status !== 'OK' && data.status !== 'ZERO_RESULTS') { - throw new Error(`Google Places API error: ${data.status}`); - } - - const stations = await Promise.all( - data.results.slice(0, options.maxResults || 20).map(async (place: any) => { - return this.transformPlaceToStation(place, options.coordinates); - }) - ); - - return stations.filter(Boolean); - } catch (error) { - logger.error('Error searching nearby stations', { error, options }); - throw error; - } - } - - async getStationDetails(placeId: string): Promise { - try { - const url = `${this.baseUrl}/place/details/json`; - const params = new URLSearchParams({ - place_id: placeId, - fields: 'name,formatted_address,geometry,rating,price_level,opening_hours,types,photos', - key: this.apiKey - }); - - const response = await fetch(`${url}?${params}`); - const data = await response.json(); - - if (data.status !== 'OK') { - throw new Error(`Google Places API error: ${data.status}`); - } - - return this.transformPlaceToStation(data.result); - } catch (error) { - logger.error('Error getting station details', { error, placeId }); - throw error; - } - } - - async getCurrentPrices(placeId: string): Promise { - // Note: Google Maps API doesn't provide real-time fuel prices - // This would need integration with fuel price services like GasBuddy - logger.warn('Current prices not available from Google Maps API', { placeId }); - return []; - } - - async geocodeAddress(address: string): Promise { - try { - const url = `${this.baseUrl}/geocode/json`; - const params = new URLSearchParams({ - address, - key: this.apiKey - }); - - const response = await fetch(`${url}?${params}`); - const data = await response.json(); - - if (data.status !== 'OK' || data.results.length === 0) { - throw new Error(`Geocoding failed: ${data.status}`); - } - - const location = data.results[0].geometry.location; - return { - latitude: location.lat, - longitude: location.lng - }; - } catch (error) { - logger.error('Error geocoding address', { error, address }); - throw error; - } - } - - async reverseGeocode(coordinates: Coordinates): Promise
{ - try { - const url = `${this.baseUrl}/geocode/json`; - const params = new URLSearchParams({ - latlng: `${coordinates.latitude},${coordinates.longitude}`, - key: this.apiKey - }); - - const response = await fetch(`${url}?${params}`); - const data = await response.json(); - - if (data.status !== 'OK' || data.results.length === 0) { - throw new Error(`Reverse geocoding failed: ${data.status}`); - } - - return this.parseAddressComponents(data.results[0]); - } catch (error) { - logger.error('Error reverse geocoding', { error, coordinates }); - throw error; - } - } - - private async transformPlaceToStation(place: any, searchCenter?: Coordinates): Promise { - const station: FuelStation = { - placeId: place.place_id, - name: place.name, - address: this.parseAddressComponents(place), - coordinates: { - latitude: place.geometry.location.lat, - longitude: place.geometry.location.lng - }, - placeTypes: place.types || [], - rating: place.rating, - priceLevel: place.price_level, - isOpen: place.opening_hours?.open_now, - fuelTypes: this.inferFuelTypes(place), - amenities: this.inferAmenities(place), - brands: this.inferBrands(place.name) - }; - - if (searchCenter) { - station.distance = this.calculateDistance(searchCenter, station.coordinates); - } - - return station; - } - - private parseAddressComponents(place: any): Address { - const components = place.address_components || []; - const address: Partial
= { - formattedAddress: place.formatted_address || '' - }; - - components.forEach((component: any) => { - const types = component.types; - if (types.includes('street_number')) { - address.streetNumber = component.long_name; - } else if (types.includes('route')) { - address.streetName = component.long_name; - } else if (types.includes('locality')) { - address.city = component.long_name; - } else if (types.includes('administrative_area_level_1')) { - address.state = component.short_name; - } else if (types.includes('postal_code')) { - address.zipCode = component.long_name; - } else if (types.includes('country')) { - address.country = component.long_name; - } - }); - - return address as Address; - } - - private inferFuelTypes(place: any): string[] { - // Basic inference - could be enhanced with more sophisticated logic - const fuelTypes = ['gasoline']; // Default assumption - - if (place.name?.toLowerCase().includes('diesel')) { - fuelTypes.push('diesel'); - } - - if (place.name?.toLowerCase().includes('electric') || - place.name?.toLowerCase().includes('ev') || - place.name?.toLowerCase().includes('tesla')) { - fuelTypes.push('electric'); - } - - return fuelTypes; - } - - private inferAmenities(place: any): string[] { - const amenities: string[] = []; - const name = place.name?.toLowerCase() || ''; - - if (name.includes('car wash')) amenities.push('car_wash'); - if (name.includes('convenience') || name.includes('store')) amenities.push('convenience_store'); - if (name.includes('restroom') || name.includes('bathroom')) amenities.push('restrooms'); - if (name.includes('air')) amenities.push('air_pump'); - - return amenities; - } - - private inferBrands(name: string): string[] { - const knownBrands = [ - 'shell', 'exxon', 'mobil', 'chevron', 'bp', 'citgo', 'sunoco', - 'marathon', 'speedway', 'wawa', '7-eleven', 'circle k' - ]; - - return knownBrands.filter(brand => - name.toLowerCase().includes(brand) - ); - } - - private calculateDistance(coord1: Coordinates, coord2: Coordinates): number { - const R = 6371000; // Earth's radius in meters - const dLat = (coord2.latitude - coord1.latitude) * Math.PI / 180; - const dLon = (coord2.longitude - coord1.longitude) * Math.PI / 180; - const a = - Math.sin(dLat/2) * Math.sin(dLat/2) + - Math.cos(coord1.latitude * Math.PI / 180) * Math.cos(coord2.latitude * Math.PI / 180) * - Math.sin(dLon/2) * Math.sin(dLon/2); - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); - return R * c; - } -} -``` - -### Mock Location Service for Development - -**File**: `backend/src/features/fuel-logs/external/mock-location.service.ts` - -```typescript -import { LocationService, StationSearchOptions, FuelStation, Coordinates, Address, FuelPrice } from './location.service.interface'; - -export class MockLocationService extends LocationService { - private mockStations: FuelStation[] = [ - { - placeId: 'mock-station-1', - name: 'Shell Station', - address: { - city: 'Anytown', - state: 'CA', - zipCode: '12345', - country: 'USA', - formattedAddress: '123 Main St, Anytown, CA 12345' - }, - coordinates: { latitude: 37.7749, longitude: -122.4194 }, - placeTypes: ['gas_station'], - rating: 4.2, - fuelTypes: ['gasoline', 'diesel'], - amenities: ['convenience_store', 'car_wash'], - brands: ['shell'], - currentPrices: [ - { - fuelType: 'gasoline', - fuelGrade: '87', - pricePerUnit: 3.49, - currency: 'USD', - lastUpdated: new Date(), - source: 'mock' - }, - { - fuelType: 'gasoline', - fuelGrade: '91', - pricePerUnit: 3.79, - currency: 'USD', - lastUpdated: new Date(), - source: 'mock' - } - ] - }, - { - placeId: 'mock-station-2', - name: 'EV Charging Station', - address: { - city: 'Anytown', - state: 'CA', - zipCode: '12345', - country: 'USA', - formattedAddress: '456 Electric Ave, Anytown, CA 12345' - }, - coordinates: { latitude: 37.7849, longitude: -122.4094 }, - placeTypes: ['gas_station', 'electric_vehicle_charging_station'], - rating: 4.5, - fuelTypes: ['electric'], - amenities: ['restrooms'], - brands: ['tesla'], - currentPrices: [ - { - fuelType: 'electric', - pricePerUnit: 0.28, - currency: 'USD', - lastUpdated: new Date(), - source: 'mock' - } - ] - } - ]; - - async searchNearbyStations(options: StationSearchOptions): Promise { - // Simulate network delay - await new Promise(resolve => setTimeout(resolve, 500)); - - let results = [...this.mockStations]; - - // Filter by fuel types if specified - if (options.fuelTypes && options.fuelTypes.length > 0) { - results = results.filter(station => - station.fuelTypes.some(type => options.fuelTypes!.includes(type)) - ); - } - - // Add distance calculation - results = results.map(station => ({ - ...station, - distance: this.calculateMockDistance(options.coordinates, station.coordinates) - })); - - // Sort by distance and limit results - results.sort((a, b) => (a.distance || 0) - (b.distance || 0)); - - return results.slice(0, options.maxResults || 20); - } - - async getStationDetails(placeId: string): Promise { - const station = this.mockStations.find(s => s.placeId === placeId); - if (!station) { - throw new Error(`Station not found: ${placeId}`); - } - - // Simulate network delay - await new Promise(resolve => setTimeout(resolve, 300)); - - return station; - } - - async getCurrentPrices(placeId: string): Promise { - const station = this.mockStations.find(s => s.placeId === placeId); - return station?.currentPrices || []; - } - - async geocodeAddress(address: string): Promise { - // Return mock coordinates for any address - return { latitude: 37.7749, longitude: -122.4194 }; - } - - async reverseGeocode(coordinates: Coordinates): Promise
{ - return { - city: 'Anytown', - state: 'CA', - zipCode: '12345', - country: 'USA', - formattedAddress: '123 Mock St, Anytown, CA 12345' - }; - } - - private calculateMockDistance(coord1: Coordinates, coord2: Coordinates): number { - // Simple mock distance calculation - const dLat = coord2.latitude - coord1.latitude; - const dLon = coord2.longitude - coord1.longitude; - return Math.sqrt(dLat * dLat + dLon * dLon) * 111000; // Rough conversion to meters - } -} -``` - -## Frontend Integration Preparation - -### Location Input Component Enhancement - -**File**: `frontend/src/features/fuel-logs/components/LocationInput.tsx` (Enhanced) - -```tsx -import React, { useState, useEffect } from 'react'; -import { - TextField, - Autocomplete, - Box, - Typography, - Chip, - CircularProgress, - Paper, - List, - ListItem, - ListItemText, - ListItemIcon, - Button, - Dialog, - DialogTitle, - DialogContent, - DialogActions -} from '@mui/material'; -import { - LocationOn, - LocalGasStation, - Star, - AttachMoney, - MyLocation -} from '@mui/icons-material'; - -import { LocationData, FuelStation } from '../types/fuel-logs.types'; -import { useGeolocation } from '../hooks/useGeolocation'; -import { useNearbyStations } from '../hooks/useNearbyStations'; - -interface LocationInputProps { - value?: LocationData; - onChange: (location?: LocationData) => void; - placeholder?: string; - disabled?: boolean; -} - -export const LocationInput: React.FC = ({ - value, - onChange, - placeholder = "Enter station location or search nearby", - disabled = false -}) => { - const [inputValue, setInputValue] = useState(''); - const [showStationPicker, setShowStationPicker] = useState(false); - const { getCurrentLocation, isGettingLocation } = useGeolocation(); - const { - nearbyStations, - searchNearbyStations, - isSearching - } = useNearbyStations(); - - // Initialize input value from existing location data - useEffect(() => { - if (value?.address) { - setInputValue(value.address); - } else if (value?.stationName) { - setInputValue(value.stationName); - } - }, [value]); - - const handleLocationSearch = async () => { - try { - const position = await getCurrentLocation(); - if (position) { - await searchNearbyStations({ - coordinates: { - latitude: position.latitude, - longitude: position.longitude - }, - radius: 5000, // 5km - maxResults: 10 - }); - setShowStationPicker(true); - } - } catch (error) { - console.error('Failed to get location or search stations:', error); - } - }; - - const handleStationSelect = (station: FuelStation) => { - const locationData: LocationData = { - address: station.address.formattedAddress, - coordinates: station.coordinates, - googlePlaceId: station.placeId, - stationName: station.name, - // Future: include pricing and amenity data - stationDetails: { - rating: station.rating, - fuelTypes: station.fuelTypes, - amenities: station.amenities, - brands: station.brands, - currentPrices: station.currentPrices - } - }; - - onChange(locationData); - setInputValue(`${station.name} - ${station.address.formattedAddress}`); - setShowStationPicker(false); - }; - - const handleManualInput = (input: string) => { - setInputValue(input); - if (input.trim()) { - onChange({ - address: input.trim() - }); - } else { - onChange(undefined); - } - }; - - return ( - <> - - handleManualInput(e.target.value)} - placeholder={placeholder} - fullWidth - disabled={disabled} - InputProps={{ - endAdornment: ( - - - - ) - }} - /> - - {value?.stationDetails && ( - - - Selected Station Details: - - - {value.stationDetails.rating && ( - } - label={`${value.stationDetails.rating}/5`} - variant="outlined" - /> - )} - {value.stationDetails.fuelTypes.map(fuelType => ( - - ))} - - - )} - - - {/* Station Picker Dialog */} - setShowStationPicker(false)} - maxWidth="sm" - fullWidth - > - - - - Nearby Fuel Stations - - - - {isSearching ? ( - - - - ) : ( - - {nearbyStations.map((station) => ( - handleStationSelect(station)} - > - - - - - - {station.name} - - {station.rating && ( - } - label={station.rating} - variant="outlined" - /> - )} - {station.distance && ( - - {Math.round(station.distance)}m away - - )} - - } - secondary={ - - - {station.address.formattedAddress} - - - {station.fuelTypes.map(fuelType => ( - - ))} - - {station.currentPrices && station.currentPrices.length > 0 && ( - - - Prices: {station.currentPrices.map(price => - `${price.fuelGrade || price.fuelType}: $${price.pricePerUnit.toFixed(2)}` - ).join(', ')} - - - )} - - } - /> - - ))} - - )} - - - - - - - ); -}; -``` - -## Database Schema for Station Data - -### Future Station Data Tables - -**File**: `backend/src/features/fuel-logs/migrations/003_add_station_data_support.sql` - -```sql --- Migration: 003_add_station_data_support.sql --- Purpose: Add tables for caching station data and prices - -BEGIN; - --- Stations cache table -CREATE TABLE IF NOT EXISTS fuel_stations ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - google_place_id VARCHAR(255) UNIQUE NOT NULL, - name VARCHAR(200) NOT NULL, - formatted_address TEXT, - latitude DECIMAL(10,8), - longitude DECIMAL(11,8), - rating DECIMAL(2,1), - price_level INTEGER, - fuel_types TEXT[], -- Array of fuel types - amenities TEXT[], -- Array of amenities - brands TEXT[], -- Array of brand names - is_active BOOLEAN DEFAULT true, - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() -); - --- Fuel prices cache table -CREATE TABLE IF NOT EXISTS fuel_prices ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - station_id UUID REFERENCES fuel_stations(id) ON DELETE CASCADE, - fuel_type VARCHAR(20) NOT NULL, - fuel_grade VARCHAR(10), - price_per_unit DECIMAL(6,3) NOT NULL, - currency VARCHAR(3) DEFAULT 'USD', - source VARCHAR(50) NOT NULL, - reported_at TIMESTAMP WITH TIME ZONE NOT NULL, - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - - -- Ensure unique price per station/fuel combination - UNIQUE(station_id, fuel_type, fuel_grade, source) -); - --- Add indexes for performance -CREATE INDEX IF NOT EXISTS idx_fuel_stations_location ON fuel_stations USING GIST ( - ll_to_earth(latitude, longitude) -); -CREATE INDEX IF NOT EXISTS idx_fuel_stations_place_id ON fuel_stations(google_place_id); -CREATE INDEX IF NOT EXISTS idx_fuel_prices_station ON fuel_prices(station_id); -CREATE INDEX IF NOT EXISTS idx_fuel_prices_reported_at ON fuel_prices(reported_at); - --- Update triggers -CREATE OR REPLACE FUNCTION update_fuel_stations_updated_at() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = NOW(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER trigger_fuel_stations_updated_at - BEFORE UPDATE ON fuel_stations - FOR EACH ROW EXECUTE FUNCTION update_fuel_stations_updated_at(); - -COMMIT; -``` - -## Configuration and Environment Setup - -### Environment Variables for Location Services - -**File**: `.env.example` (additions) - -```bash -# Google Maps Integration -GOOGLE_MAPS_API_KEY=your_google_maps_api_key_here -GOOGLE_PLACES_CACHE_TTL=3600 # 1 hour in seconds -GOOGLE_GEOCODING_CACHE_TTL=86400 # 24 hours in seconds - -# Location Service Configuration -LOCATION_SERVICE_PROVIDER=google # 'google' | 'mock' -STATION_SEARCH_DEFAULT_RADIUS=5000 # meters -STATION_SEARCH_MAX_RESULTS=20 -STATION_PRICE_CACHE_TTL=1800 # 30 minutes in seconds - -# Future: Additional service integrations -GASBUDDY_API_KEY=your_gasbuddy_api_key_here -MAPBOX_API_KEY=your_mapbox_api_key_here -``` - -### Service Factory Pattern - -**File**: `backend/src/features/fuel-logs/external/location-service.factory.ts` - -```typescript -import { LocationService } from './location.service.interface'; -import { GoogleMapsService } from './google-maps.service'; -import { MockLocationService } from './mock-location.service'; - -export class LocationServiceFactory { - static create(): LocationService { - const provider = process.env.LOCATION_SERVICE_PROVIDER || 'mock'; - - switch (provider.toLowerCase()) { - case 'google': - return new GoogleMapsService(); - case 'mock': - return new MockLocationService(); - default: - throw new Error(`Unsupported location service provider: ${provider}`); - } - } -} -``` - -## API Endpoints for Location Integration - -### Station Search Endpoints - -**File**: `backend/src/features/fuel-logs/api/station-search.controller.ts` - -```typescript -import { FastifyRequest, FastifyReply } from 'fastify'; -import { LocationServiceFactory } from '../external/location-service.factory'; -import { StationSearchOptions } from '../external/location.service.interface'; -import { logger } from '../../../core/logging/logger'; - -export class StationSearchController { - private locationService = LocationServiceFactory.create(); - - async searchNearbyStations( - request: FastifyRequest<{ - Body: { - latitude: number; - longitude: number; - radius?: number; - fuelTypes?: string[]; - maxResults?: number; - openNow?: boolean; - } - }>, - reply: FastifyReply - ) { - try { - const { latitude, longitude, radius, fuelTypes, maxResults, openNow } = request.body; - - const options: StationSearchOptions = { - coordinates: { latitude, longitude }, - radius, - fuelTypes, - maxResults, - openNow - }; - - const stations = await this.locationService.searchNearbyStations(options); - - return reply.code(200).send({ - stations, - searchOptions: options, - resultCount: stations.length - }); - } catch (error: any) { - logger.error('Error searching nearby stations', { - error, - userId: (request as any).user?.sub - }); - - return reply.code(500).send({ - error: 'Internal server error', - message: 'Failed to search nearby stations' - }); - } - } - - async getStationDetails( - request: FastifyRequest<{ Params: { placeId: string } }>, - reply: FastifyReply - ) { - try { - const { placeId } = request.params; - - const station = await this.locationService.getStationDetails(placeId); - - return reply.code(200).send(station); - } catch (error: any) { - logger.error('Error getting station details', { - error, - placeId: request.params.placeId, - userId: (request as any).user?.sub - }); - - if (error.message.includes('not found')) { - return reply.code(404).send({ - error: 'Not Found', - message: 'Station not found' - }); - } - - return reply.code(500).send({ - error: 'Internal server error', - message: 'Failed to get station details' - }); - } - } - - async getCurrentPrices( - request: FastifyRequest<{ Params: { placeId: string } }>, - reply: FastifyReply - ) { - try { - const { placeId } = request.params; - - const prices = await this.locationService.getCurrentPrices(placeId); - - return reply.code(200).send({ - placeId, - prices, - lastUpdated: new Date().toISOString() - }); - } catch (error: any) { - logger.error('Error getting current prices', { - error, - placeId: request.params.placeId, - userId: (request as any).user?.sub - }); - - return reply.code(500).send({ - error: 'Internal server error', - message: 'Failed to get current prices' - }); - } - } -} -``` - -## Testing Strategy for Location Services - -### Mock Service Testing - -**File**: `backend/src/features/fuel-logs/tests/unit/location-services.test.ts` - -```typescript -import { MockLocationService } from '../../external/mock-location.service'; -import { StationSearchOptions } from '../../external/location.service.interface'; - -describe('Location Services', () => { - let mockLocationService: MockLocationService; - - beforeEach(() => { - mockLocationService = new MockLocationService(); - }); - - describe('MockLocationService', () => { - it('should search nearby stations', async () => { - const options: StationSearchOptions = { - coordinates: { latitude: 37.7749, longitude: -122.4194 }, - radius: 5000, - maxResults: 10 - }; - - const stations = await mockLocationService.searchNearbyStations(options); - - expect(stations).toHaveLength(2); - expect(stations[0].name).toBe('Shell Station'); - expect(stations[0].fuelTypes).toContain('gasoline'); - }); - - it('should filter by fuel type', async () => { - const options: StationSearchOptions = { - coordinates: { latitude: 37.7749, longitude: -122.4194 }, - fuelTypes: ['electric'] - }; - - const stations = await mockLocationService.searchNearbyStations(options); - - expect(stations).toHaveLength(1); - expect(stations[0].name).toBe('EV Charging Station'); - expect(stations[0].fuelTypes).toEqual(['electric']); - }); - - it('should get station details', async () => { - const station = await mockLocationService.getStationDetails('mock-station-1'); - - expect(station.name).toBe('Shell Station'); - expect(station.currentPrices).toBeDefined(); - expect(station.currentPrices!.length).toBeGreaterThan(0); - }); - }); -}); -``` - -## Future Enhancement Opportunities - -### Advanced Features for Future Development - -1. **Price Comparison & Alerts** - - Real-time price comparison across stations - - Price alert notifications when fuel prices drop - - Historical price tracking and trends - -2. **Route Optimization** - - Find cheapest stations along a planned route - - Integration with navigation apps - - Multi-stop route planning with fuel considerations - -3. **Loyalty Program Integration** - - Connect with fuel station loyalty programs - - Automatic discount application - - Cashback and rewards tracking - -4. **Predictive Analytics** - - Predict fuel needs based on driving patterns - - Suggest optimal refueling timing - - Maintenance reminder integration - -5. **Social Features** - - User-reported prices and reviews - - Station amenity crowdsourcing - - Community-driven station information - -6. **Fleet Management** - - Multi-vehicle fleet tracking - - Fuel budget management - - Driver behavior analytics - -## Implementation Tasks - -### Backend Tasks -1. āœ… Create location service interface and implementations -2. āœ… Implement Google Maps service integration -3. āœ… Create mock service for development/testing -4. āœ… Design station data caching schema -5. āœ… Implement service factory pattern -6. āœ… Add API endpoints for station search -7. āœ… Create comprehensive testing strategy - -### Frontend Tasks -1. āœ… Enhance location input component -2. āœ… Create station picker interface -3. āœ… Add geolocation functionality -4. āœ… Implement nearby station search -5. āœ… Design station details display -6. āœ… Add price display integration - -### Configuration Tasks -1. āœ… Add environment variables for API keys -2. āœ… Create service configuration options -3. āœ… Implement caching strategies -4. āœ… Add error handling and fallbacks - -## Success Criteria - -### Phase 5 Complete When: -- āœ… Location service architecture designed and documented -- āœ… Google Maps integration interface implemented -- āœ… Mock location service functional for development -- āœ… Database schema ready for station data caching -- āœ… Frontend components prepared for location integration -- āœ… API endpoints designed and documented -- āœ… Testing strategy implemented -- āœ… Configuration management in place -- āœ… Future enhancement roadmap documented - -### Ready for Google Maps Integration When: -- All interfaces and architecture in place -- Mock services tested and functional -- Frontend components integrated and tested -- API endpoints ready for production -- Caching strategy implemented -- Error handling comprehensive -- Documentation complete for future developers - ---- - -**Implementation Complete**: All phases documented and ready for sequential implementation by future AI developers. \ No newline at end of file diff --git a/docs/changes/mobile-optimization-v1/01-RESEARCH-FINDINGS.md b/docs/changes/mobile-optimization-v1/01-RESEARCH-FINDINGS.md deleted file mode 100644 index 3b3a819..0000000 --- a/docs/changes/mobile-optimization-v1/01-RESEARCH-FINDINGS.md +++ /dev/null @@ -1,218 +0,0 @@ -# Research Findings - Mobile/Desktop Architecture Analysis - -## Executive Summary -Comprehensive analysis of MotoVaultPro's authentication and mobile/desktop architecture reveals a sophisticated dual-implementation strategy with specific gaps in mobile functionality. No infinite login issues found - the Auth0 architecture is well-designed with mobile-optimized features. - -## Authentication Architecture Analysis - -### Auth0 Implementation -**Location**: `/home/egullickson/motovaultpro/frontend/src/core/auth/Auth0Provider.tsx` - -#### Configuration -- **Token Storage**: `cacheLocation="localstorage"` with `useRefreshTokens={true}` -- **Environment Variables**: Auth0 domain, client ID, and audience -- **Redirect Strategy**: Smart handling between production (`admin.motovaultpro.com`) and local development -- **Callback Flow**: Redirects to `/dashboard` after authentication - -#### Token Management Features -**Progressive Fallback Strategy** (Lines 44-95): -```typescript -// Attempt 1: Cache-first approach -const token1 = await getAccessTokenSilently({ - cacheMode: 'on', - timeoutInSeconds: 15 -}); - -// Attempt 2: Force refresh -const token2 = await getAccessTokenSilently({ - cacheMode: 'off', - timeoutInSeconds: 20 -}); - -// Attempt 3: Default behavior -const token3 = await getAccessTokenSilently({ - timeoutInSeconds: 30 -}); -``` - -**Mobile Optimizations**: -- Pre-warming token cache with 100ms delay -- Exponential backoff between retries (500ms, 1000ms, 1500ms) -- Enhanced error logging for mobile debugging -- Special handling for mobile network timing issues - -### API Client Integration -**Location**: `/home/egullickson/motovaultpro/frontend/src/core/api/client.ts` - -- **Token Injection**: Axios request interceptor automatically adds Bearer tokens -- **Mobile Error Handling**: Enhanced user feedback for mobile-specific errors -- **Timeout**: 10 seconds with mobile-optimized error messages -- **Error Recovery**: API calls proceed even if token acquisition fails - -## Mobile vs Desktop Implementation Analysis - -### Architecture Strategy -**Dual Implementation Approach**: Complete separation rather than responsive design -- **Mobile Detection**: JavaScript-based using `window.innerWidth <= 768` + user agent -- **Component Separation**: Dedicated mobile components vs desktop components -- **Navigation Paradigm**: State-based (mobile) vs URL routing (desktop) - -### Mobile-Specific Components -``` -frontend/src/features/vehicles/mobile/ -ā”œā”€ā”€ VehiclesMobileScreen.tsx - Mobile vehicles list -ā”œā”€ā”€ VehicleDetailMobile.tsx - Mobile vehicle detail view -ā”œā”€ā”€ VehicleMobileCard.tsx - Mobile vehicle cards - -frontend/src/shared-minimal/components/mobile/ -ā”œā”€ā”€ BottomNavigation.tsx - Mobile bottom nav -ā”œā”€ā”€ GlassCard.tsx - Mobile glass card component -ā”œā”€ā”€ MobileContainer.tsx - Mobile container wrapper -ā”œā”€ā”€ MobilePill.tsx - Mobile pill component -``` - -### Desktop-Only Components -``` -frontend/src/features/vehicles/pages/ -ā”œā”€ā”€ VehiclesPage.tsx - Desktop vehicles with sidebar -ā”œā”€ā”€ VehicleDetailPage.tsx - Desktop vehicle detail - -frontend/src/pages/ -ā”œā”€ā”€ SettingsPage.tsx - āŒ DESKTOP-ONLY SETTINGS -``` - -### Critical Gap: Settings Implementation -**Desktop Settings** (`/home/egullickson/motovaultpro/frontend/src/pages/SettingsPage.tsx`): -- Account management -- Notifications settings -- Appearance & Units (dark mode, unit system) -- Data export/management -- Account actions (logout, delete account) - -**Mobile Settings** (`frontend/src/App.tsx` lines 113-122): -```tsx -const SettingsScreen = () => ( -
- -
-

Settings

-

Coming soon - App settings and preferences

-
-
-
-); -``` - -### Navigation Architecture Differences - -#### Mobile Navigation -**Location**: `frontend/src/App.tsx` (lines 70-85) -- **Bottom Navigation**: Fixed bottom nav with 4 tabs -- **State-Based**: Uses `activeScreen` state for navigation -- **Screen Management**: Single-screen approach with state transitions -- **No URL Routing**: State-based screen switching - -#### Desktop Navigation -**Location**: Various route files -- **Sidebar Navigation**: Collapsible left sidebar -- **URL Routing**: Full React Router implementation -- **Multi-Page**: Each route renders separate page component -- **Traditional**: Browser history and URL-based navigation - -## State Management & Data Persistence - -### React Query Configuration -**Location**: `/home/egullickson/motovaultpro/frontend/src/main.tsx` -```typescript -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: 1, - refetchOnWindowFocus: false, - }, - }, -}); -``` - -### Zustand Global Store -**Location**: `/home/egullickson/motovaultpro/frontend/src/core/store/index.ts` -- **Persisted State**: `selectedVehicleId`, `sidebarOpen` -- **Session State**: `user` (not persisted) -- **Storage Key**: `motovaultpro-storage` - -### Storage Analysis -**localStorage Usage**: -- Auth0 tokens and refresh tokens -- Unit system preferences (`motovaultpro-unit-system`) -- Zustand persisted state (`motovaultpro-storage`) - -**No Cookie or sessionStorage Usage** - All persistence via localStorage - -## Issues Identified - -### 1. Mobile State Reset Issues -**Location**: `frontend/src/App.tsx` mobile navigation logic -- Navigation resets `selectedVehicle` and `showAddVehicle` states -- User context lost during screen transitions -- Form state not preserved across navigation - -### 2. Feature Parity Gaps -- āŒ **Settings**: Desktop full-featured, mobile placeholder only -- āŒ **Maintenance**: Referenced but not implemented on mobile -- āŒ **Gas Stations**: Referenced but not implemented on mobile - -### 3. Navigation Inconsistencies -- Mobile: State-based navigation without URLs -- Desktop: URL-based routing with browser history -- Different paradigms cause UX inconsistencies - -## Positive Findings - -### 1. No Infinite Login Issues āœ… -- Auth0 state management prevents recursive authentication calls -- Proper loading states prevent premature redirects -- Error boundaries handle token failures gracefully -- Mobile retry logic prevents network timing loops - -### 2. Robust Token Management āœ… -- Progressive fallback strategy handles network issues -- Mobile-specific optimizations for slower connections -- Automatic token injection via interceptors -- Refresh token support prevents expiration issues - -### 3. Good Data Caching āœ… -- React Query provides seamless data sharing -- Optimistic updates with rollback on failure -- Automatic cache invalidation after mutations -- Zustand persists UI state across sessions - -## Implementation Priority Assessment - -### Priority 1 - Critical -- **Mobile Settings Implementation**: Major functionality gap -- **State Persistence**: Fix mobile navigation state resets - -### Priority 2 - High -- **Navigation Consistency**: Unify mobile/desktop navigation patterns -- **Feature Parity**: Ensure all desktop features work on mobile - -### Priority 3 - Medium -- **Token Optimization**: Enhance error recovery and background refresh -- **Cache Optimization**: Review overlapping query invalidations - -### Priority 4 - Low -- **Progressive Enhancement**: PWA features for mobile -- **Responsive Migration**: Consider gradual migration from dual implementation - -## File References Summary - -### Key Files Analyzed -- `frontend/src/core/auth/Auth0Provider.tsx` - Authentication implementation -- `frontend/src/App.tsx` - Mobile navigation and state management -- `frontend/src/core/api/client.ts` - API client and token injection -- `frontend/src/core/store/index.ts` - Global state management -- `frontend/src/pages/SettingsPage.tsx` - Desktop settings (mobile missing) -- `frontend/src/features/vehicles/mobile/` - Mobile-specific components -- `frontend/src/shared-minimal/components/mobile/` - Mobile UI components - -This analysis provides the foundation for implementing comprehensive mobile optimization improvements while maintaining the existing architecture's strengths. \ No newline at end of file diff --git a/docs/changes/mobile-optimization-v1/02-IMPLEMENTATION-PLAN.md b/docs/changes/mobile-optimization-v1/02-IMPLEMENTATION-PLAN.md deleted file mode 100644 index 4c4bef0..0000000 --- a/docs/changes/mobile-optimization-v1/02-IMPLEMENTATION-PLAN.md +++ /dev/null @@ -1,233 +0,0 @@ -# Implementation Plan - Mobile Optimization V1 - -## Overview -4-phase implementation strategy to address mobile functionality gaps, authentication consistency, and cross-platform feature parity. Each phase builds upon the previous while maintaining backward compatibility. - -## Phase 1: Critical Mobile Settings Implementation (Priority 1) - -### Objective -Implement full-featured mobile settings screen to achieve feature parity with desktop. - -### Timeline Estimate -2-3 days - -### Tasks -1. **Create Mobile Settings Screen Component** - - File: `frontend/src/features/settings/mobile/MobileSettingsScreen.tsx` - - Implement all desktop settings functionality in mobile-friendly UI - - Use existing mobile component patterns (GlassCard, MobileContainer) - -2. **Settings State Management Integration** - - Extend Zustand store for settings persistence - - Add settings-specific hooks for mobile - - Integrate with existing unit preferences system - -3. **Mobile Bottom Navigation Integration** - - Update bottom navigation to include settings access - - Ensure proper active state management - - Maintain navigation consistency - -### Success Criteria -- āœ… Mobile settings screen matches desktop functionality -- āœ… All settings persist across app restarts -- āœ… Settings accessible via mobile bottom navigation -- āœ… Dark mode toggle works on mobile -- āœ… Unit system changes persist on mobile -- āœ… Account management functions work on mobile - -### Files to Modify/Create -- `frontend/src/features/settings/mobile/MobileSettingsScreen.tsx` (new) -- `frontend/src/App.tsx` (replace placeholder SettingsScreen) -- `frontend/src/core/store/index.ts` (extend for settings) -- `frontend/src/shared-minimal/components/mobile/BottomNavigation.tsx` (update) - -## Phase 2: Navigation & State Consistency (Priority 2) - -### Objective -Fix mobile navigation state resets and improve data persistence across screen transitions. - -### Timeline Estimate -2-3 days - -### Tasks -1. **Enhanced Mobile State Persistence** - - Persist mobile navigation state (`activeScreen`, `selectedVehicle`) - - Maintain form state across navigation - - Implement mobile back button navigation history - -2. **Navigation Context Unification** - - Create consistent navigation state management - - Fix state reset issues during screen transitions - - Preserve user selections during navigation - -3. **User Context Persistence** - - Persist user context to avoid re-authentication overhead - - Maintain user preferences across app restarts - - Implement graceful auth state recovery - -### Success Criteria -- āœ… Mobile navigation maintains selected vehicle context -- āœ… Form state preserved during navigation -- āœ… User preferences persist across app restarts -- āœ… Back button navigation works correctly on mobile -- āœ… No context loss during screen transitions - -### Files to Modify -- `frontend/src/App.tsx` (navigation state management) -- `frontend/src/core/store/index.ts` (enhanced persistence) -- `frontend/src/features/vehicles/mobile/VehiclesMobileScreen.tsx` (state preservation) - -## Phase 3: Token & Data Flow Optimization (Priority 3) - -### Objective -Enhance token management and optimize data flow for better mobile experience. - -### Timeline Estimate -1-2 days - -### Tasks -1. **Enhanced Token Management** - - Implement token refresh retry logic for 401 responses - - Add error boundaries for token acquisition failures - - Optimize mobile token warm-up timing beyond current 100ms - -2. **Data Flow Improvements** - - Review React Query cache invalidation patterns - - Implement background token refresh to prevent expiration - - Add offline data persistence for mobile scenarios - -3. **Mobile Network Optimization** - - Enhance retry mechanisms for poor mobile connectivity - - Add progressive loading states for mobile - - Implement smart caching for offline scenarios - -### Success Criteria -- āœ… Token refresh failures automatically retry -- āœ… No token expiration issues during extended mobile use -- āœ… Optimized cache invalidation reduces unnecessary refetches -- āœ… Better mobile network error handling -- āœ… Offline data persistence for mobile users - -### Files to Modify -- `frontend/src/core/auth/Auth0Provider.tsx` (enhanced token management) -- `frontend/src/core/api/client.ts` (401 retry logic) -- `frontend/src/main.tsx` (React Query optimization) - -## Phase 4: UX Consistency & Enhancement (Priority 4) - -### Objective -Ensure platform parity and consider progressive enhancements for better mobile experience. - -### Timeline Estimate -2-3 days - -### Tasks -1. **Platform Parity Verification** - - Audit all desktop features for mobile equivalents - - Implement any missing mobile functionality - - Ensure consistent UX patterns across platforms - -2. **Navigation Architecture Review** - - Consider hybrid approach maintaining URL routing with mobile state management - - Evaluate progressive enhancement opportunities - - Assess responsive design migration feasibility - -3. **Progressive Enhancement** - - Add PWA features for mobile experience - - Implement mobile-specific optimizations - - Consider offline-first functionality - -### Success Criteria -- āœ… All desktop features have mobile equivalents -- āœ… Consistent UX patterns across platforms -- āœ… Mobile-specific enhancements implemented -- āœ… PWA features functional -- āœ… Offline capabilities where appropriate - -### Files to Modify/Create -- Various feature components for parity -- PWA configuration files -- Service worker implementation -- Mobile-specific optimization components - -## Implementation Guidelines - -### Development Approach -1. **Mobile-First**: Maintain mobile-optimized approach while fixing gaps -2. **Incremental**: Implement improvements without breaking existing functionality -3. **Feature Parity**: Ensure every desktop feature has mobile equivalent -4. **Testing**: Test all changes on both platforms per project requirements - -### Code Standards -- Follow existing mobile component patterns in `frontend/src/shared-minimal/components/mobile/` -- Use GlassCard, MobileContainer, and MobilePill for consistent mobile UI -- Maintain TypeScript types and interfaces -- Follow existing state management patterns with Zustand -- Preserve Auth0 authentication patterns - -### Testing Requirements -- Test every change on both mobile and desktop -- Verify authentication flows work on both platforms -- Validate state persistence across navigation -- Test offline scenarios on mobile -- Verify token management improvements - -## Dependencies & Prerequisites - -### Required Knowledge -- Understanding of existing mobile component architecture -- Auth0 integration patterns -- React Query and Zustand state management -- Mobile-first responsive design principles - -### External Dependencies -- No new external dependencies required -- All improvements use existing libraries and patterns -- Leverages current Auth0, React Query, and Zustand setup - -### Environment Requirements -- Mobile testing environment (physical device or emulator) -- Desktop testing environment -- Local development environment with Docker containers - -## Risk Mitigation - -### Breaking Changes -- All phases designed to maintain backward compatibility -- Incremental implementation allows rollback at any point -- Existing functionality preserved during improvements - -### Testing Strategy -- Phase-by-phase testing prevents cascading issues -- Mobile + desktop testing at each phase -- Authentication flow validation at each step -- State management verification throughout - -### Rollback Plan -- Each phase can be reverted independently -- Git branching strategy allows easy rollback -- Feature flags could be implemented for gradual rollout - -## Success Metrics - -### Phase 1 Success -- Mobile settings screen fully functional -- Feature parity achieved between mobile and desktop settings -- No regression in existing functionality - -### Phase 2 Success -- Mobile navigation maintains context consistently -- No state reset issues during navigation -- User preferences persist across sessions - -### Phase 3 Success -- Token management robust across network conditions -- No authentication issues during extended mobile use -- Optimized data flow reduces unnecessary API calls - -### Phase 4 Success -- Complete platform parity achieved -- Enhanced mobile experience with PWA features -- Consistent UX patterns across all platforms - -This implementation plan provides a structured approach to achieving comprehensive mobile optimization while maintaining the robust existing architecture. \ No newline at end of file diff --git a/docs/changes/mobile-optimization-v1/03-MOBILE-SETTINGS.md b/docs/changes/mobile-optimization-v1/03-MOBILE-SETTINGS.md deleted file mode 100644 index 19f764c..0000000 --- a/docs/changes/mobile-optimization-v1/03-MOBILE-SETTINGS.md +++ /dev/null @@ -1,445 +0,0 @@ -# Mobile Settings Implementation Guide - -## Overview -Complete implementation guide for creating a full-featured mobile settings screen that matches desktop functionality. This addresses the critical gap where desktop has comprehensive settings but mobile only has a placeholder. - -## Current State Analysis - -### Desktop Settings (Full Implementation) -**File**: `/home/egullickson/motovaultpro/frontend/src/pages/SettingsPage.tsx` - -**Features**: -- Account management section -- Notifications settings -- Appearance & Units (dark mode, metric/imperial) -- Data export and management -- Account actions (logout, delete account) - -### Mobile Settings (Placeholder Only) -**File**: `frontend/src/App.tsx` (lines 113-122) - -**Current Implementation**: -```tsx -const SettingsScreen = () => ( -
- -
-

Settings

-

Coming soon - App settings and preferences

-
-
-
-); -``` - -## Implementation Strategy - -### Step 1: Create Mobile Settings Directory Structure -Create dedicated mobile settings components following existing patterns: - -``` -frontend/src/features/settings/ -ā”œā”€ā”€ mobile/ -│ ā”œā”€ā”€ MobileSettingsScreen.tsx # Main settings screen -│ ā”œā”€ā”€ AccountSection.tsx # Account management -│ ā”œā”€ā”€ NotificationsSection.tsx # Notification preferences -│ ā”œā”€ā”€ AppearanceSection.tsx # Dark mode & units -│ ā”œā”€ā”€ DataSection.tsx # Export & data management -│ └── AccountActionsSection.tsx # Logout & delete account -└── hooks/ - ā”œā”€ā”€ useSettings.ts # Settings state management - └── useSettingsPersistence.ts # Settings persistence -``` - -### Step 2: Implement Mobile Settings Screen Component - -**File**: `frontend/src/features/settings/mobile/MobileSettingsScreen.tsx` - -```tsx -import React from 'react'; -import { GlassCard, MobileContainer } from '../../../shared-minimal/components/mobile'; -import { AccountSection } from './AccountSection'; -import { NotificationsSection } from './NotificationsSection'; -import { AppearanceSection } from './AppearanceSection'; -import { DataSection } from './DataSection'; -import { AccountActionsSection } from './AccountActionsSection'; - -export const MobileSettingsScreen: React.FC = () => { - return ( - -
{/* Bottom padding for nav */} -
-

Settings

-

Manage your account and preferences

-
- - - - - - -
-
- ); -}; -``` - -### Step 3: Implement Settings Sections - -#### Account Section Component -**File**: `frontend/src/features/settings/mobile/AccountSection.tsx` - -```tsx -import React from 'react'; -import { useAuth0 } from '@auth0/auth0-react'; -import { GlassCard } from '../../../shared-minimal/components/mobile'; - -export const AccountSection: React.FC = () => { - const { user } = useAuth0(); - - return ( - -
-

Account

- -
-
- Profile -
-

{user?.name}

-

{user?.email}

-
-
- -
-

- Member since {new Date(user?.updated_at || '').toLocaleDateString()} -

-
-
-
-
- ); -}; -``` - -#### Appearance Section Component -**File**: `frontend/src/features/settings/mobile/AppearanceSection.tsx` - -```tsx -import React from 'react'; -import { GlassCard } from '../../../shared-minimal/components/mobile'; -import { useSettings } from '../hooks/useSettings'; - -export const AppearanceSection: React.FC = () => { - const { settings, updateSetting } = useSettings(); - - const toggleDarkMode = () => { - updateSetting('darkMode', !settings.darkMode); - }; - - const toggleUnitSystem = () => { - updateSetting('unitSystem', settings.unitSystem === 'imperial' ? 'metric' : 'imperial'); - }; - - return ( - -
-

Appearance & Units

- -
- {/* Dark Mode Toggle */} -
-
-

Dark Mode

-

Switch to dark theme

-
- -
- - {/* Unit System Toggle */} -
-
-

Unit System

-

- Currently using {settings.unitSystem === 'imperial' ? 'Miles & Gallons' : 'Kilometers & Liters'} -

-
- -
-
-
-
- ); -}; -``` - -#### Account Actions Section Component -**File**: `frontend/src/features/settings/mobile/AccountActionsSection.tsx` - -```tsx -import React, { useState } from 'react'; -import { useAuth0 } from '@auth0/auth0-react'; -import { GlassCard } from '../../../shared-minimal/components/mobile'; - -export const AccountActionsSection: React.FC = () => { - const { logout } = useAuth0(); - const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); - - const handleLogout = () => { - logout({ - logoutParams: { - returnTo: window.location.origin - } - }); - }; - - const handleDeleteAccount = () => { - // Implementation for account deletion - setShowDeleteConfirm(false); - // Navigate to account deletion flow - }; - - return ( - -
-

Account Actions

- -
- - - -
- - {/* Delete Confirmation Modal */} - {showDeleteConfirm && ( -
-
-

Delete Account

-

- This action cannot be undone. All your data will be permanently deleted. -

-
- - -
-
-
- )} -
-
- ); -}; -``` - -### Step 4: Implement Settings State Management - -#### Settings Hook -**File**: `frontend/src/features/settings/hooks/useSettings.ts` - -```tsx -import { useState, useEffect } from 'react'; -import { useSettingsPersistence } from './useSettingsPersistence'; - -export interface SettingsState { - darkMode: boolean; - unitSystem: 'imperial' | 'metric'; - notifications: { - email: boolean; - push: boolean; - maintenance: boolean; - }; -} - -const defaultSettings: SettingsState = { - darkMode: false, - unitSystem: 'imperial', - notifications: { - email: true, - push: true, - maintenance: true, - }, -}; - -export const useSettings = () => { - const { loadSettings, saveSettings } = useSettingsPersistence(); - const [settings, setSettings] = useState(defaultSettings); - - useEffect(() => { - const savedSettings = loadSettings(); - if (savedSettings) { - setSettings(savedSettings); - } - }, [loadSettings]); - - const updateSetting = ( - key: K, - value: SettingsState[K] - ) => { - const newSettings = { ...settings, [key]: value }; - setSettings(newSettings); - saveSettings(newSettings); - }; - - return { - settings, - updateSetting, - }; -}; -``` - -#### Settings Persistence Hook -**File**: `frontend/src/features/settings/hooks/useSettingsPersistence.ts` - -```tsx -import { useCallback } from 'react'; -import { SettingsState } from './useSettings'; - -const SETTINGS_STORAGE_KEY = 'motovaultpro-mobile-settings'; - -export const useSettingsPersistence = () => { - const loadSettings = useCallback((): SettingsState | null => { - try { - const stored = localStorage.getItem(SETTINGS_STORAGE_KEY); - return stored ? JSON.parse(stored) : null; - } catch (error) { - console.error('Error loading settings:', error); - return null; - } - }, []); - - const saveSettings = useCallback((settings: SettingsState) => { - try { - localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(settings)); - } catch (error) { - console.error('Error saving settings:', error); - } - }, []); - - return { - loadSettings, - saveSettings, - }; -}; -``` - -### Step 5: Update App.tsx Integration - -**File**: `frontend/src/App.tsx` - -Replace the existing placeholder SettingsScreen with: - -```tsx -// Import the new component -import { MobileSettingsScreen } from './features/settings/mobile/MobileSettingsScreen'; - -// Replace the existing SettingsScreen component (around line 113) -const SettingsScreen = MobileSettingsScreen; -``` - -### Step 6: Integration with Existing Systems - -#### Unit System Integration -Ensure mobile settings integrate with existing unit system: - -**File**: `frontend/src/shared-minimal/utils/units.ts` - -The mobile settings should use the existing unit conversion utilities and persist to the same storage key (`motovaultpro-unit-system`). - -#### Zustand Store Integration -**File**: `frontend/src/core/store/index.ts` - -Extend the existing store to include settings state if needed for cross-component access. - -## Testing Requirements - -### Mobile Testing Checklist -- āœ… Settings screen renders correctly on mobile devices -- āœ… All sections (Account, Notifications, Appearance, Data, Actions) function properly -- āœ… Dark mode toggle works and persists -- āœ… Unit system changes work and persist -- āœ… Logout functionality works correctly -- āœ… Account deletion flow works (with confirmation) -- āœ… Settings persist across app restarts -- āœ… Navigation to/from settings maintains context - -### Desktop Compatibility Testing -- āœ… Changes don't break existing desktop settings -- āœ… Settings synchronize between mobile and desktop views -- āœ… Unit system changes reflect in both interfaces -- āœ… Authentication flows remain consistent - -### Integration Testing -- āœ… Settings integrate properly with existing Auth0 authentication -- āœ… Unit preferences work across all features (vehicles, fuel logs, etc.) -- āœ… Settings state management doesn't conflict with existing Zustand store -- āœ… localStorage persistence works correctly - -## Migration Strategy - -### Phase 1: Component Creation -1. Create the mobile settings directory structure -2. Implement individual settings section components -3. Create settings hooks for state management - -### Phase 2: Integration -1. Replace placeholder in App.tsx -2. Test mobile settings functionality -3. Verify persistence and state management - -### Phase 3: Enhancement -1. Add any missing features from desktop version -2. Implement mobile-specific optimizations -3. Ensure full feature parity - -## Success Criteria - -Upon completion, the mobile settings should: - -1. **Feature Parity**: Match all desktop settings functionality -2. **Mobile-Optimized**: Use appropriate mobile UI patterns and components -3. **Persistent**: All settings persist across app restarts -4. **Integrated**: Work seamlessly with existing authentication and state management -5. **Tested**: Pass all mobile and desktop compatibility tests - -This implementation will eliminate the critical mobile settings gap and provide a comprehensive settings experience across all platforms. \ No newline at end of file diff --git a/docs/changes/mobile-optimization-v1/04-STATE-MANAGEMENT.md b/docs/changes/mobile-optimization-v1/04-STATE-MANAGEMENT.md deleted file mode 100644 index 5237142..0000000 --- a/docs/changes/mobile-optimization-v1/04-STATE-MANAGEMENT.md +++ /dev/null @@ -1,671 +0,0 @@ -# State Management & Navigation Consistency Solutions - -## Overview -This document addresses critical state management issues in mobile navigation, including context loss during screen transitions, form state persistence, and navigation consistency between mobile and desktop platforms. - -## Issues Identified - -### 1. Mobile State Reset Issues -**Location**: `frontend/src/App.tsx` mobile navigation logic - -**Problem**: Navigation between screens resets critical state: -- `selectedVehicle` resets when switching screens -- `showAddVehicle` form state lost during navigation -- User context not maintained across screen transitions -- Mobile navigation doesn't preserve history - -### 2. Navigation Paradigm Split -**Mobile**: State-based navigation without URLs (`activeScreen` state) -**Desktop**: URL-based routing with React Router -**Impact**: Inconsistent user experience and different development patterns - -### 3. State Persistence Gaps -- User context not persisted (requires re-authentication overhead) -- Form data lost when navigating away -- Mobile navigation state not preserved across app restarts -- Settings changes not immediately reflected across screens - -## Solution Architecture - -### Enhanced Mobile State Management - -#### 1. Navigation State Persistence -**File**: `frontend/src/core/store/navigation.ts` (new) - -```tsx -import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; - -export type MobileScreen = 'dashboard' | 'vehicles' | 'fuel' | 'settings'; -export type VehicleSubScreen = 'list' | 'detail' | 'add' | 'edit'; - -interface NavigationState { - // Current navigation state - activeScreen: MobileScreen; - vehicleSubScreen: VehicleSubScreen; - selectedVehicleId: string | null; - - // Navigation history for back button - navigationHistory: { - screen: MobileScreen; - vehicleSubScreen?: VehicleSubScreen; - selectedVehicleId?: string | null; - timestamp: number; - }[]; - - // Form state preservation - formStates: Record; - - // Actions - navigateToScreen: (screen: MobileScreen) => void; - navigateToVehicleSubScreen: (subScreen: VehicleSubScreen, vehicleId?: string) => void; - goBack: () => void; - saveFormState: (formId: string, state: any) => void; - restoreFormState: (formId: string) => any; - clearFormState: (formId: string) => void; -} - -export const useNavigationStore = create()( - persist( - (set, get) => ({ - // Initial state - activeScreen: 'vehicles', - vehicleSubScreen: 'list', - selectedVehicleId: null, - navigationHistory: [], - formStates: {}, - - // Navigation actions - navigateToScreen: (screen) => { - const currentState = get(); - const historyEntry = { - screen: currentState.activeScreen, - vehicleSubScreen: currentState.vehicleSubScreen, - selectedVehicleId: currentState.selectedVehicleId, - timestamp: Date.now(), - }; - - set({ - activeScreen: screen, - vehicleSubScreen: screen === 'vehicles' ? 'list' : currentState.vehicleSubScreen, - selectedVehicleId: screen === 'vehicles' ? currentState.selectedVehicleId : null, - navigationHistory: [...currentState.navigationHistory, historyEntry].slice(-10), // Keep last 10 - }); - }, - - navigateToVehicleSubScreen: (subScreen, vehicleId = null) => { - const currentState = get(); - const historyEntry = { - screen: currentState.activeScreen, - vehicleSubScreen: currentState.vehicleSubScreen, - selectedVehicleId: currentState.selectedVehicleId, - timestamp: Date.now(), - }; - - set({ - vehicleSubScreen: subScreen, - selectedVehicleId: vehicleId || currentState.selectedVehicleId, - navigationHistory: [...currentState.navigationHistory, historyEntry].slice(-10), - }); - }, - - goBack: () => { - const currentState = get(); - const lastEntry = currentState.navigationHistory[currentState.navigationHistory.length - 1]; - - if (lastEntry) { - set({ - activeScreen: lastEntry.screen, - vehicleSubScreen: lastEntry.vehicleSubScreen || 'list', - selectedVehicleId: lastEntry.selectedVehicleId, - navigationHistory: currentState.navigationHistory.slice(0, -1), - }); - } - }, - - // Form state management - saveFormState: (formId, state) => { - set((current) => ({ - formStates: { - ...current.formStates, - [formId]: { ...state, timestamp: Date.now() }, - }, - })); - }, - - restoreFormState: (formId) => { - const state = get().formStates[formId]; - // Return state if it's less than 1 hour old - if (state && Date.now() - state.timestamp < 3600000) { - return state; - } - return null; - }, - - clearFormState: (formId) => { - set((current) => { - const newFormStates = { ...current.formStates }; - delete newFormStates[formId]; - return { formStates: newFormStates }; - }); - }, - }), - { - name: 'motovaultpro-mobile-navigation', - partialize: (state) => ({ - activeScreen: state.activeScreen, - vehicleSubScreen: state.vehicleSubScreen, - selectedVehicleId: state.selectedVehicleId, - formStates: state.formStates, - // Don't persist navigation history - rebuild on app start - }), - } - ) -); -``` - -#### 2. Enhanced User Context Persistence -**File**: `frontend/src/core/store/user.ts` (new) - -```tsx -import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; - -interface UserPreferences { - unitSystem: 'imperial' | 'metric'; - darkMode: boolean; - notifications: { - email: boolean; - push: boolean; - maintenance: boolean; - }; -} - -interface UserState { - // User data (persisted subset) - userProfile: { - id: string; - name: string; - email: string; - picture: string; - } | null; - - preferences: UserPreferences; - - // Session data (not persisted) - isOnline: boolean; - lastSyncTimestamp: number; - - // Actions - setUserProfile: (profile: any) => void; - updatePreferences: (preferences: Partial) => void; - setOnlineStatus: (isOnline: boolean) => void; - updateLastSync: () => void; - clearUserData: () => void; -} - -export const useUserStore = create()( - persist( - (set) => ({ - // Initial state - userProfile: null, - preferences: { - unitSystem: 'imperial', - darkMode: false, - notifications: { - email: true, - push: true, - maintenance: true, - }, - }, - isOnline: true, - lastSyncTimestamp: 0, - - // Actions - setUserProfile: (profile) => { - if (profile) { - set({ - userProfile: { - id: profile.sub, - name: profile.name, - email: profile.email, - picture: profile.picture, - }, - }); - } - }, - - updatePreferences: (newPreferences) => { - set((state) => ({ - preferences: { ...state.preferences, ...newPreferences }, - })); - }, - - setOnlineStatus: (isOnline) => set({ isOnline }), - - updateLastSync: () => set({ lastSyncTimestamp: Date.now() }), - - clearUserData: () => set({ - userProfile: null, - preferences: { - unitSystem: 'imperial', - darkMode: false, - notifications: { - email: true, - push: true, - maintenance: true, - }, - }, - }), - }), - { - name: 'motovaultpro-user-context', - partialize: (state) => ({ - userProfile: state.userProfile, - preferences: state.preferences, - // Don't persist session data - }), - } - ) -); -``` - -#### 3. Smart Form State Hook -**File**: `frontend/src/core/hooks/useFormState.ts` (new) - -```tsx -import { useState, useEffect, useCallback } from 'react'; -import { useNavigationStore } from '../store/navigation'; - -export interface UseFormStateOptions { - formId: string; - defaultValues: Record; - autoSave?: boolean; - saveDelay?: number; -} - -export const useFormState = >({ - formId, - defaultValues, - autoSave = true, - saveDelay = 1000, -}: UseFormStateOptions) => { - const { saveFormState, restoreFormState, clearFormState } = useNavigationStore(); - const [formData, setFormData] = useState(defaultValues as T); - const [hasChanges, setHasChanges] = useState(false); - const [isRestored, setIsRestored] = useState(false); - - // Restore form state on mount - useEffect(() => { - const restoredState = restoreFormState(formId); - if (restoredState && !isRestored) { - setFormData({ ...defaultValues, ...restoredState }); - setHasChanges(true); - setIsRestored(true); - } - }, [formId, restoreFormState, defaultValues, isRestored]); - - // Auto-save with debounce - useEffect(() => { - if (!autoSave || !hasChanges) return; - - const timer = setTimeout(() => { - saveFormState(formId, formData); - }, saveDelay); - - return () => clearTimeout(timer); - }, [formData, hasChanges, autoSave, saveDelay, formId, saveFormState]); - - const updateFormData = useCallback((updates: Partial) => { - setFormData((current) => ({ ...current, ...updates })); - setHasChanges(true); - }, []); - - const resetForm = useCallback(() => { - setFormData(defaultValues as T); - setHasChanges(false); - clearFormState(formId); - }, [defaultValues, formId, clearFormState]); - - const submitForm = useCallback(() => { - setHasChanges(false); - clearFormState(formId); - }, [formId, clearFormState]); - - return { - formData, - updateFormData, - resetForm, - submitForm, - hasChanges, - isRestored, - }; -}; -``` - -### Implementation in App.tsx - -#### Updated Mobile Navigation Logic -**File**: `frontend/src/App.tsx` (modifications) - -```tsx -import { useNavigationStore } from './core/store/navigation'; -import { useUserStore } from './core/store/user'; - -// Replace existing mobile detection and state management -const MobileApp: React.FC = () => { - const { user, isAuthenticated } = useAuth0(); - const { - activeScreen, - vehicleSubScreen, - selectedVehicleId, - navigateToScreen, - navigateToVehicleSubScreen, - goBack, - } = useNavigationStore(); - - const { setUserProfile } = useUserStore(); - - // Update user profile when authenticated - useEffect(() => { - if (isAuthenticated && user) { - setUserProfile(user); - } - }, [isAuthenticated, user, setUserProfile]); - - // Handle mobile back button - useEffect(() => { - const handlePopState = (event: PopStateEvent) => { - event.preventDefault(); - goBack(); - }; - - window.addEventListener('popstate', handlePopState); - return () => window.removeEventListener('popstate', handlePopState); - }, [goBack]); - - const handleVehicleSelect = (vehicleId: string) => { - navigateToVehicleSubScreen('detail', vehicleId); - }; - - const handleAddVehicle = () => { - navigateToVehicleSubScreen('add'); - }; - - const handleBackToList = () => { - navigateToVehicleSubScreen('list'); - }; - - // Render screens based on navigation state - const renderActiveScreen = () => { - switch (activeScreen) { - case 'vehicles': - return renderVehiclesScreen(); - case 'fuel': - return ; - case 'dashboard': - return ; - case 'settings': - return ; - default: - return renderVehiclesScreen(); - } - }; - - const renderVehiclesScreen = () => { - switch (vehicleSubScreen) { - case 'list': - return ( - - ); - case 'detail': - return ( - - ); - case 'add': - return ( - - ); - default: - return ( - - ); - } - }; - - return ( -
- {renderActiveScreen()} - - -
- ); -}; -``` - -#### Enhanced Add Vehicle Form with State Persistence -**File**: `frontend/src/features/vehicles/mobile/AddVehicleScreen.tsx` (example usage) - -```tsx -import React from 'react'; -import { useFormState } from '../../../core/hooks/useFormState'; - -interface AddVehicleScreenProps { - onBack: () => void; - onVehicleAdded: () => void; -} - -export const AddVehicleScreen: React.FC = ({ - onBack, - onVehicleAdded, -}) => { - const { - formData, - updateFormData, - resetForm, - submitForm, - hasChanges, - isRestored, - } = useFormState({ - formId: 'add-vehicle', - defaultValues: { - year: '', - make: '', - model: '', - trim: '', - vin: '', - licensePlate: '', - nickname: '', - }, - }); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - try { - // Submit vehicle data - await submitVehicle(formData); - submitForm(); // Clear saved state - onVehicleAdded(); - } catch (error) { - // Handle error, form state is preserved - console.error('Error adding vehicle:', error); - } - }; - - return ( -
-
- -

Add Vehicle

- {isRestored && ( - Draft restored - )} -
- -
- updateFormData({ year: e.target.value })} - className="w-full p-3 border rounded-lg" - /> - - {/* More form fields... */} - -
- - -
- - {hasChanges && ( -

- Changes are being saved automatically -

- )} -
-
- ); -}; -``` - -## Integration with Existing Systems - -### 1. Zustand Store Integration -**File**: `frontend/src/core/store/index.ts` (existing file modifications) - -```tsx -// Export new stores alongside existing ones -export { useNavigationStore } from './navigation'; -export { useUserStore } from './user'; - -// Keep existing store exports -export { useAppStore } from './app'; -``` - -### 2. Auth0 Integration Enhancement -**File**: `frontend/src/core/auth/Auth0Provider.tsx` (modifications) - -```tsx -import { useUserStore } from '../store/user'; - -// Inside the Auth0Provider component -const { setUserProfile, clearUserData } = useUserStore(); - -// Update user profile on authentication -useEffect(() => { - if (isAuthenticated && user) { - setUserProfile(user); - } else if (!isAuthenticated) { - clearUserData(); - } -}, [isAuthenticated, user, setUserProfile, clearUserData]); -``` - -### 3. Unit System Integration -**File**: `frontend/src/shared-minimal/utils/units.ts` (modifications) - -```tsx -import { useUserStore } from '../../core/store/user'; - -// Update existing unit hooks to use new store -export const useUnitSystem = () => { - const { preferences, updatePreferences } = useUserStore(); - - const toggleUnitSystem = () => { - const newSystem = preferences.unitSystem === 'imperial' ? 'metric' : 'imperial'; - updatePreferences({ unitSystem: newSystem }); - }; - - return { - unitSystem: preferences.unitSystem, - toggleUnitSystem, - }; -}; -``` - -## Testing Requirements - -### State Persistence Tests -- āœ… Navigation state persists across app restarts -- āœ… Selected vehicle context maintained during navigation -- āœ… Form state preserved when navigating away and returning -- āœ… User preferences persist and sync across screens -- āœ… Navigation history works correctly with back button - -### Mobile Navigation Tests -- āœ… Screen transitions maintain context -- āœ… Bottom navigation reflects current state accurately -- āœ… Add vehicle form preserves data during interruptions -- āœ… Settings changes reflect immediately across screens -- āœ… Authentication state managed correctly - -### Integration Tests -- āœ… New stores integrate properly with existing components -- āœ… Auth0 integration works with enhanced user persistence -- āœ… Unit system changes sync between old and new systems -- āœ… No conflicts with existing Zustand store patterns - -## Migration Strategy - -### Phase 1: Store Creation -1. Create new navigation and user stores -2. Implement form state management hook -3. Test stores in isolation - -### Phase 2: Mobile App Integration -1. Update App.tsx to use new navigation store -2. Modify mobile screens to use form state hook -3. Test mobile navigation and persistence - -### Phase 3: System Integration -1. Integrate with existing Auth0 provider -2. Update unit system to use new user store -3. Ensure backward compatibility - -### Phase 4: Enhancement & Optimization -1. Add advanced features like offline persistence -2. Optimize performance and storage usage -3. Add error handling and recovery mechanisms - -## Success Criteria - -Upon completion: - -1. **Navigation Consistency**: Mobile navigation maintains context across all transitions -2. **State Persistence**: All user data, preferences, and form states persist appropriately -3. **Form Recovery**: Users can navigate away from forms and return without data loss -4. **User Context**: User preferences and settings sync immediately across all screens -5. **Back Navigation**: Mobile back button works correctly with navigation history -6. **Integration**: New state management integrates seamlessly with existing systems - -This enhanced state management system will provide a robust foundation for consistent mobile and desktop experiences while maintaining all existing functionality. \ No newline at end of file diff --git a/docs/changes/mobile-optimization-v1/05-TOKEN-OPTIMIZATION.md b/docs/changes/mobile-optimization-v1/05-TOKEN-OPTIMIZATION.md deleted file mode 100644 index 29b40c6..0000000 --- a/docs/changes/mobile-optimization-v1/05-TOKEN-OPTIMIZATION.md +++ /dev/null @@ -1,709 +0,0 @@ -# Token Optimization & Authentication Enhancement Guide - -## Overview -This document provides detailed guidance for optimizing Auth0 token management, enhancing error recovery, and implementing robust authentication patterns for improved mobile and desktop experience. - -## Current Implementation Analysis - -### Existing Token Management Strengths -**File**: `/home/egullickson/motovaultpro/frontend/src/core/auth/Auth0Provider.tsx` - -**Current Features**: -- Progressive fallback strategy with 3 retry attempts -- Mobile-optimized token acquisition with enhanced timeouts -- Exponential backoff for mobile network conditions -- Pre-warming token cache for mobile devices -- Sophisticated error handling and logging - -**Current Token Acquisition Logic** (lines 44-95): -```typescript -const getTokenWithRetry = async (): Promise => { - const maxRetries = 3; - const baseDelay = 500; - - for (let attempt = 1; attempt <= maxRetries; attempt++) { - try { - let token: string; - - if (attempt === 1) { - // Cache-first approach - token = await getAccessTokenSilently({ - cacheMode: 'on', - timeoutInSeconds: 15, - }); - } else if (attempt === 2) { - // Force refresh - token = await getAccessTokenSilently({ - cacheMode: 'off', - timeoutInSeconds: 20, - }); - } else { - // Final attempt with extended timeout - token = await getAccessTokenSilently({ - timeoutInSeconds: 30, - }); - } - - return token; - } catch (error) { - const delay = baseDelay * Math.pow(2, attempt - 1); - if (attempt < maxRetries) { - await new Promise(resolve => setTimeout(resolve, delay)); - } - } - } - return null; -}; -``` - -## Enhancement Areas - -### 1. Token Refresh Retry Logic for 401 Responses -**Problem**: API calls fail with 401 responses without attempting token refresh -**Solution**: Implement automatic token refresh and retry for 401 errors - -#### Enhanced API Client -**File**: `frontend/src/core/api/client.ts` (modifications) - -```typescript -import { Auth0Context } from '@auth0/auth0-react'; -import { useContext } from 'react'; - -// Enhanced token management service -class TokenManager { - private static instance: TokenManager; - private isRefreshing = false; - private failedQueue: Array<{ - resolve: (token: string) => void; - reject: (error: Error) => void; - }> = []; - - static getInstance(): TokenManager { - if (!TokenManager.instance) { - TokenManager.instance = new TokenManager(); - } - return TokenManager.instance; - } - - async refreshToken(getAccessTokenSilently: any): Promise { - if (this.isRefreshing) { - // Return a promise that will resolve when the current refresh completes - return new Promise((resolve, reject) => { - this.failedQueue.push({ resolve, reject }); - }); - } - - this.isRefreshing = true; - - try { - // Force token refresh - const token = await getAccessTokenSilently({ - cacheMode: 'off', - timeoutInSeconds: 20, - }); - - // Process queued requests - this.failedQueue.forEach(({ resolve }) => resolve(token)); - this.failedQueue = []; - - return token; - } catch (error) { - // Reject queued requests - this.failedQueue.forEach(({ reject }) => reject(error as Error)); - this.failedQueue = []; - throw error; - } finally { - this.isRefreshing = false; - } - } -} - -// Enhanced API client with 401 retry logic -export const createApiClient = (getAccessTokenSilently: any) => { - const tokenManager = TokenManager.getInstance(); - - const client = axios.create({ - baseURL: process.env.REACT_APP_API_URL || '/api', - timeout: 10000, - headers: { - 'Content-Type': 'application/json', - }, - }); - - // Request interceptor - inject tokens - client.interceptors.request.use( - async (config) => { - try { - const token = await getAccessTokenSilently({ - cacheMode: 'on', - timeoutInSeconds: 15, - }); - - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - } catch (error) { - console.warn('Token acquisition failed, proceeding without token:', error); - } - - return config; - }, - (error) => Promise.reject(error) - ); - - // Response interceptor - handle 401s with token refresh retry - client.interceptors.response.use( - (response) => response, - async (error) => { - const originalRequest = error.config; - - // Handle 401 responses with token refresh - if (error.response?.status === 401 && !originalRequest._retry) { - originalRequest._retry = true; - - try { - console.log('401 detected, attempting token refresh...'); - const newToken = await tokenManager.refreshToken(getAccessTokenSilently); - - // Update the failed request with new token - originalRequest.headers.Authorization = `Bearer ${newToken}`; - - // Retry the original request - return client(originalRequest); - } catch (refreshError) { - console.error('Token refresh failed:', refreshError); - - // If token refresh fails, the user needs to re-authenticate - // This should trigger the Auth0 login flow - window.location.href = '/login'; - return Promise.reject(refreshError); - } - } - - // Enhanced mobile error handling - if (error.code === 'ECONNABORTED' || error.message.includes('timeout')) { - const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( - navigator.userAgent - ); - - if (isMobile) { - error.message = 'Connection timeout. Please check your network and try again.'; - } - } - - return Promise.reject(error); - } - ); - - return client; -}; -``` - -### 2. Background Token Refresh -**Problem**: Tokens can expire during extended mobile use -**Solution**: Implement proactive background token refresh - -#### Background Token Service -**File**: `frontend/src/core/auth/backgroundTokenService.ts` (new) - -```typescript -class BackgroundTokenService { - private static instance: BackgroundTokenService; - private refreshInterval: NodeJS.Timeout | null = null; - private getAccessTokenSilently: any = null; - private isActive = false; - - static getInstance(): BackgroundTokenService { - if (!BackgroundTokenService.instance) { - BackgroundTokenService.instance = new BackgroundTokenService(); - } - return BackgroundTokenService.instance; - } - - start(getAccessTokenSilently: any) { - if (this.isActive) return; - - this.getAccessTokenSilently = getAccessTokenSilently; - this.isActive = true; - - // Refresh token every 45 minutes (tokens typically expire after 1 hour) - this.refreshInterval = setInterval(() => { - this.refreshTokenInBackground(); - }, 45 * 60 * 1000); - - // Also refresh on app visibility change (mobile app switching) - document.addEventListener('visibilitychange', this.handleVisibilityChange); - } - - stop() { - if (this.refreshInterval) { - clearInterval(this.refreshInterval); - this.refreshInterval = null; - } - - document.removeEventListener('visibilitychange', this.handleVisibilityChange); - this.isActive = false; - } - - private handleVisibilityChange = () => { - if (document.visibilityState === 'visible') { - // App became visible, refresh token to ensure it's valid - this.refreshTokenInBackground(); - } - }; - - private async refreshTokenInBackground() { - if (!this.getAccessTokenSilently) return; - - try { - await this.getAccessTokenSilently({ - cacheMode: 'off', - timeoutInSeconds: 10, - }); - - console.debug('Background token refresh successful'); - } catch (error) { - console.warn('Background token refresh failed:', error); - // Don't throw - this is a background operation - } - } -} - -export default BackgroundTokenService; -``` - -#### Integration with Auth0Provider -**File**: `/home/egullickson/motovaultpro/frontend/src/core/auth/Auth0Provider.tsx` (modifications) - -```typescript -import BackgroundTokenService from './backgroundTokenService'; - -// Inside the Auth0Provider component -const CustomAuth0Provider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const [isInitialized, setIsInitialized] = useState(false); - - useEffect(() => { - const initializeAuth = async () => { - // Existing initialization logic... - - // Start background token service after authentication - if (isAuthenticated) { - const backgroundService = BackgroundTokenService.getInstance(); - backgroundService.start(getAccessTokenSilently); - } - }; - - initializeAuth(); - - // Cleanup on unmount - return () => { - const backgroundService = BackgroundTokenService.getInstance(); - backgroundService.stop(); - }; - }, [isAuthenticated, getAccessTokenSilently]); - - // Rest of component... -}; -``` - -### 3. Enhanced Error Boundaries for Token Failures -**Problem**: Token acquisition failures can break the app -**Solution**: Implement error boundaries with graceful degradation - -#### Auth Error Boundary -**File**: `frontend/src/core/auth/AuthErrorBoundary.tsx` (new) - -```typescript -import React, { Component, ErrorInfo, ReactNode } from 'react'; - -interface Props { - children: ReactNode; - fallback?: ReactNode; -} - -interface State { - hasError: boolean; - error: Error | null; - isAuthError: boolean; -} - -export class AuthErrorBoundary extends Component { - public state: State = { - hasError: false, - error: null, - isAuthError: false, - }; - - public static getDerivedStateFromError(error: Error): State { - const isAuthError = error.message.includes('auth') || - error.message.includes('token') || - error.message.includes('login'); - - return { - hasError: true, - error, - isAuthError - }; - } - - public componentDidCatch(error: Error, errorInfo: ErrorInfo) { - console.error('Auth Error Boundary caught an error:', error, errorInfo); - } - - private handleRetry = () => { - this.setState({ hasError: false, error: null, isAuthError: false }); - }; - - private handleReauth = () => { - // Redirect to login - window.location.href = '/login'; - }; - - public render() { - if (this.state.hasError) { - if (this.props.fallback) { - return this.props.fallback; - } - - return ( -
-
-
- - - -
- -

- {this.state.isAuthError ? 'Authentication Error' : 'Something went wrong'} -

- -

- {this.state.isAuthError - ? 'There was a problem with authentication. Please sign in again.' - : 'An unexpected error occurred. Please try again.'} -

- -
- - - {this.state.isAuthError && ( - - )} -
- - {process.env.NODE_ENV === 'development' && this.state.error && ( -
- - Error Details (dev only) - -
-                  {this.state.error.message}
-                
-
- )} -
-
- ); - } - - return this.props.children; - } -} -``` - -### 4. Optimized Mobile Token Warm-up -**Problem**: Current 100ms delay may not be sufficient for all mobile devices -**Solution**: Adaptive warm-up timing based on device performance - -#### Adaptive Token Warm-up -**File**: `frontend/src/core/auth/tokenWarmup.ts` (new) - -```typescript -class TokenWarmupService { - private static instance: TokenWarmupService; - private warmupDelay: number = 100; // Default - - static getInstance(): TokenWarmupService { - if (!TokenWarmupService.instance) { - TokenWarmupService.instance = new TokenWarmupService(); - } - return TokenWarmupService.instance; - } - - async calculateOptimalDelay(): Promise { - // Detect device performance characteristics - const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( - navigator.userAgent - ); - - if (!isMobile) { - return 50; // Faster for desktop - } - - // Mobile performance detection - const startTime = performance.now(); - - // Simple CPU-bound task to gauge performance - let sum = 0; - for (let i = 0; i < 100000; i++) { - sum += Math.random(); - } - - const endTime = performance.now(); - const executionTime = endTime - startTime; - - // Adaptive delay based on device performance - if (executionTime < 10) { - return 100; // Fast mobile device - } else if (executionTime < 50) { - return 200; // Medium mobile device - } else { - return 500; // Slower mobile device - } - } - - async warmupWithAdaptiveDelay(callback: () => Promise): Promise { - const delay = await this.calculateOptimalDelay(); - this.warmupDelay = delay; - - return new Promise((resolve) => { - setTimeout(async () => { - await callback(); - resolve(); - }, delay); - }); - } - - getLastWarmupDelay(): number { - return this.warmupDelay; - } -} - -export default TokenWarmupService; -``` - -#### Integration with Auth0Provider -```typescript -// Inside Auth0Provider initialization -const warmupService = TokenWarmupService.getInstance(); - -await warmupService.warmupWithAdaptiveDelay(async () => { - try { - await getAccessTokenSilently({ - cacheMode: 'on', - timeoutInSeconds: 5, - }); - } catch (error) { - // Warm-up failed, but continue initialization - console.warn('Token warm-up failed:', error); - } -}); -``` - -### 5. Offline Token Management -**Problem**: Mobile users may have intermittent connectivity -**Solution**: Implement offline token caching and validation - -#### Offline Token Cache -**File**: `frontend/src/core/auth/offlineTokenCache.ts` (new) - -```typescript -interface CachedTokenInfo { - token: string; - expiresAt: number; - cachedAt: number; -} - -class OfflineTokenCache { - private static instance: OfflineTokenCache; - private readonly CACHE_KEY = 'motovaultpro-offline-token'; - private readonly MAX_OFFLINE_DURATION = 30 * 60 * 1000; // 30 minutes - - static getInstance(): OfflineTokenCache { - if (!OfflineTokenCache.instance) { - OfflineTokenCache.instance = new OfflineTokenCache(); - } - return OfflineTokenCache.instance; - } - - cacheToken(token: string): void { - try { - // Decode JWT to get expiration (simplified - in production, use a JWT library) - const payload = JSON.parse(atob(token.split('.')[1])); - const expiresAt = payload.exp * 1000; // Convert to milliseconds - - const tokenInfo: CachedTokenInfo = { - token, - expiresAt, - cachedAt: Date.now(), - }; - - localStorage.setItem(this.CACHE_KEY, JSON.stringify(tokenInfo)); - } catch (error) { - console.warn('Failed to cache token:', error); - } - } - - getCachedToken(): string | null { - try { - const cached = localStorage.getItem(this.CACHE_KEY); - if (!cached) return null; - - const tokenInfo: CachedTokenInfo = JSON.parse(cached); - const now = Date.now(); - - // Check if token is expired - if (now >= tokenInfo.expiresAt) { - this.clearCache(); - return null; - } - - // Check if we've been offline too long - if (now - tokenInfo.cachedAt > this.MAX_OFFLINE_DURATION) { - this.clearCache(); - return null; - } - - return tokenInfo.token; - } catch (error) { - console.warn('Failed to retrieve cached token:', error); - this.clearCache(); - return null; - } - } - - clearCache(): void { - localStorage.removeItem(this.CACHE_KEY); - } - - isOnline(): boolean { - return navigator.onLine; - } -} - -export default OfflineTokenCache; -``` - -## Implementation Integration - -### Updated API Client Factory -**File**: `frontend/src/core/api/index.ts` (new) - -```typescript -import { createApiClient } from './client'; -import OfflineTokenCache from '../auth/offlineTokenCache'; - -export const createEnhancedApiClient = (getAccessTokenSilently: any) => { - const offlineCache = OfflineTokenCache.getInstance(); - const client = createApiClient(getAccessTokenSilently); - - // Enhance request interceptor for offline support - client.interceptors.request.use( - async (config) => { - try { - // Try to get fresh token - const token = await getAccessTokenSilently({ - cacheMode: 'on', - timeoutInSeconds: 15, - }); - - if (token) { - // Cache token for offline use - offlineCache.cacheToken(token); - config.headers.Authorization = `Bearer ${token}`; - } - } catch (error) { - // If online token acquisition fails, try cached token - if (!offlineCache.isOnline()) { - const cachedToken = offlineCache.getCachedToken(); - if (cachedToken) { - config.headers.Authorization = `Bearer ${cachedToken}`; - console.log('Using cached token for offline request'); - } - } - } - - return config; - }, - (error) => Promise.reject(error) - ); - - return client; -}; -``` - -## Testing Requirements - -### Token Management Tests -- āœ… 401 responses trigger automatic token refresh and retry -- āœ… Background token refresh prevents expiration during extended use -- āœ… Token warm-up adapts to device performance -- āœ… Error boundaries handle token failures gracefully -- āœ… Offline token caching works during network interruptions - -### Mobile-Specific Tests -- āœ… Enhanced retry logic handles poor mobile connectivity -- āœ… App visibility changes trigger token refresh -- āœ… Mobile error messages are user-friendly -- āœ… Token acquisition timing adapts to device performance - -### Integration Tests -- āœ… Enhanced API client works with existing components -- āœ… Background service doesn't interfere with normal token acquisition -- āœ… Error boundaries don't break existing error handling -- āœ… Offline caching doesn't conflict with Auth0's built-in caching - -## Implementation Phases - -### Phase 1: Core Enhancements -1. Implement 401 retry logic in API client -2. Add background token refresh service -3. Create auth error boundary - -### Phase 2: Mobile Optimizations -1. Implement adaptive token warm-up -2. Add offline token caching -3. Enhance mobile error handling - -### Phase 3: Integration & Testing -1. Integrate all enhancements with existing Auth0Provider -2. Test across various network conditions -3. Validate mobile and desktop compatibility - -### Phase 4: Monitoring & Analytics -1. Add token performance monitoring -2. Implement retry success/failure analytics -3. Add offline usage tracking - -## Success Criteria - -Upon completion: - -1. **Robust Token Management**: No 401 failures without retry attempts -2. **Background Refresh**: No token expiration issues during extended use -3. **Mobile Optimization**: Adaptive timing and offline support for mobile users -4. **Error Recovery**: Graceful handling of all token acquisition failures -5. **Performance**: Minimal impact on app performance and user experience - -These enhancements will provide a robust, mobile-optimized authentication system that gracefully handles network issues and provides an excellent user experience across all platforms. \ No newline at end of file diff --git a/docs/changes/mobile-optimization-v1/06-CODE-EXAMPLES.md b/docs/changes/mobile-optimization-v1/06-CODE-EXAMPLES.md deleted file mode 100644 index ee2952c..0000000 --- a/docs/changes/mobile-optimization-v1/06-CODE-EXAMPLES.md +++ /dev/null @@ -1,1341 +0,0 @@ -# Code Examples & Implementation Snippets - -## Overview -This document provides concrete code examples and implementation snippets for all mobile optimization improvements. These examples can be directly used during implementation with minimal modifications. - -## Mobile Settings Implementation - -### Complete Mobile Settings Screen -**File**: `frontend/src/features/settings/mobile/MobileSettingsScreen.tsx` - -```tsx -import React, { useState } from 'react'; -import { useAuth0 } from '@auth0/auth0-react'; -import { - GlassCard, - MobileContainer, - MobilePill -} from '../../../shared-minimal/components/mobile'; -import { useSettings } from '../hooks/useSettings'; - -interface ToggleSwitchProps { - enabled: boolean; - onChange: () => void; - label: string; - description?: string; -} - -const ToggleSwitch: React.FC = ({ - enabled, - onChange, - label, - description -}) => ( -
-
-

{label}

- {description && ( -

{description}

- )} -
- -
-); - -interface ModalProps { - isOpen: boolean; - onClose: () => void; - title: string; - children: React.ReactNode; -} - -const Modal: React.FC = ({ isOpen, onClose, title, children }) => { - if (!isOpen) return null; - - return ( -
-
-

{title}

- {children} -
- -
-
-
- ); -}; - -export const MobileSettingsScreen: React.FC = () => { - const { user, logout } = useAuth0(); - const { settings, updateSetting } = useSettings(); - const [showDataExport, setShowDataExport] = useState(false); - const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); - - const handleLogout = () => { - logout({ - logoutParams: { - returnTo: window.location.origin - } - }); - }; - - const handleExportData = () => { - // Implement data export functionality - console.log('Exporting user data...'); - setShowDataExport(false); - }; - - const handleDeleteAccount = () => { - // Implement account deletion - console.log('Deleting account...'); - setShowDeleteConfirm(false); - }; - - return ( - -
- {/* Header */} -
-

Settings

-

Manage your account and preferences

-
- - {/* Account Section */} - -
-

Account

-
- {user?.picture && ( - Profile - )} -
-

{user?.name}

-

{user?.email}

-
-
-
-

- Member since {user?.updated_at ? new Date(user.updated_at).toLocaleDateString() : 'Unknown'} -

-
-
-
- - {/* Notifications Section */} - -
-

Notifications

-
- updateSetting('notifications', { - ...settings.notifications, - email: !settings.notifications.email - })} - label="Email Notifications" - description="Receive updates via email" - /> - updateSetting('notifications', { - ...settings.notifications, - push: !settings.notifications.push - })} - label="Push Notifications" - description="Receive mobile push notifications" - /> - updateSetting('notifications', { - ...settings.notifications, - maintenance: !settings.notifications.maintenance - })} - label="Maintenance Reminders" - description="Get reminded about vehicle maintenance" - /> -
-
-
- - {/* Appearance & Units Section */} - -
-

Appearance & Units

-
- updateSetting('darkMode', !settings.darkMode)} - label="Dark Mode" - description="Switch to dark theme" - /> - -
-
-

Unit System

-

- Currently using {settings.unitSystem === 'imperial' ? 'Miles & Gallons' : 'Kilometers & Liters'} -

-
- updateSetting('unitSystem', settings.unitSystem === 'imperial' ? 'metric' : 'imperial')} - variant="secondary" - /> -
-
-
-
- - {/* Data Management Section */} - -
-

Data Management

-
- -

- Download a copy of all your vehicle and fuel data -

-
-
-
- - {/* Account Actions Section */} - -
-

Account Actions

-
- - -
-
-
- - {/* Data Export Modal */} - setShowDataExport(false)} - title="Export Data" - > -

- This will create a downloadable file containing all your vehicle data, fuel logs, and preferences. -

-
- - -
-
- - {/* Delete Account Confirmation */} - setShowDeleteConfirm(false)} - title="Delete Account" - > -

- This action cannot be undone. All your data will be permanently deleted. -

-
- - -
-
-
-
- ); -}; -``` - -## State Management Examples - -### Enhanced Navigation Store -**File**: `frontend/src/core/store/navigation.ts` - -```tsx -import { create } from 'zustand'; -import { persist, createJSONStorage } from 'zustand/middleware'; - -export type MobileScreen = 'dashboard' | 'vehicles' | 'fuel' | 'settings'; -export type VehicleSubScreen = 'list' | 'detail' | 'add' | 'edit'; - -interface NavigationHistory { - screen: MobileScreen; - vehicleSubScreen?: VehicleSubScreen; - selectedVehicleId?: string | null; - timestamp: number; - metadata?: Record; -} - -interface FormState { - data: Record; - timestamp: number; - isDirty: boolean; -} - -interface NavigationState { - // Current navigation state - activeScreen: MobileScreen; - vehicleSubScreen: VehicleSubScreen; - selectedVehicleId: string | null; - - // Navigation history for back button - navigationHistory: NavigationHistory[]; - - // Form state preservation - formStates: Record; - - // Loading and error states - isNavigating: boolean; - navigationError: string | null; - - // Actions - navigateToScreen: (screen: MobileScreen, metadata?: Record) => void; - navigateToVehicleSubScreen: (subScreen: VehicleSubScreen, vehicleId?: string, metadata?: Record) => void; - goBack: () => boolean; - canGoBack: () => boolean; - saveFormState: (formId: string, data: any, isDirty?: boolean) => void; - restoreFormState: (formId: string) => FormState | null; - clearFormState: (formId: string) => void; - clearAllFormStates: () => void; - setNavigationError: (error: string | null) => void; -} - -export const useNavigationStore = create()( - persist( - (set, get) => ({ - // Initial state - activeScreen: 'vehicles', - vehicleSubScreen: 'list', - selectedVehicleId: null, - navigationHistory: [], - formStates: {}, - isNavigating: false, - navigationError: null, - - // Navigation actions - navigateToScreen: (screen, metadata = {}) => { - const currentState = get(); - - set({ isNavigating: true, navigationError: null }); - - try { - const historyEntry: NavigationHistory = { - screen: currentState.activeScreen, - vehicleSubScreen: currentState.vehicleSubScreen, - selectedVehicleId: currentState.selectedVehicleId, - timestamp: Date.now(), - metadata, - }; - - set({ - activeScreen: screen, - vehicleSubScreen: screen === 'vehicles' ? 'list' : currentState.vehicleSubScreen, - selectedVehicleId: screen === 'vehicles' ? currentState.selectedVehicleId : null, - navigationHistory: [...currentState.navigationHistory, historyEntry].slice(-10), - isNavigating: false, - }); - } catch (error) { - set({ - navigationError: error instanceof Error ? error.message : 'Navigation failed', - isNavigating: false - }); - } - }, - - navigateToVehicleSubScreen: (subScreen, vehicleId = null, metadata = {}) => { - const currentState = get(); - - set({ isNavigating: true, navigationError: null }); - - try { - const historyEntry: NavigationHistory = { - screen: currentState.activeScreen, - vehicleSubScreen: currentState.vehicleSubScreen, - selectedVehicleId: currentState.selectedVehicleId, - timestamp: Date.now(), - metadata, - }; - - set({ - vehicleSubScreen: subScreen, - selectedVehicleId: vehicleId || currentState.selectedVehicleId, - navigationHistory: [...currentState.navigationHistory, historyEntry].slice(-10), - isNavigating: false, - }); - } catch (error) { - set({ - navigationError: error instanceof Error ? error.message : 'Navigation failed', - isNavigating: false - }); - } - }, - - goBack: () => { - const currentState = get(); - const lastEntry = currentState.navigationHistory[currentState.navigationHistory.length - 1]; - - if (lastEntry) { - set({ - activeScreen: lastEntry.screen, - vehicleSubScreen: lastEntry.vehicleSubScreen || 'list', - selectedVehicleId: lastEntry.selectedVehicleId, - navigationHistory: currentState.navigationHistory.slice(0, -1), - isNavigating: false, - navigationError: null, - }); - return true; - } - return false; - }, - - canGoBack: () => { - return get().navigationHistory.length > 0; - }, - - // Form state management - saveFormState: (formId, data, isDirty = true) => { - const currentState = get(); - const formState: FormState = { - data, - timestamp: Date.now(), - isDirty, - }; - - set({ - formStates: { - ...currentState.formStates, - [formId]: formState, - }, - }); - }, - - restoreFormState: (formId) => { - const state = get().formStates[formId]; - const maxAge = 2 * 60 * 60 * 1000; // 2 hours - - if (state && Date.now() - state.timestamp < maxAge) { - return state; - } - - // Clean up old state - if (state) { - get().clearFormState(formId); - } - - return null; - }, - - clearFormState: (formId) => { - const currentState = get(); - const newFormStates = { ...currentState.formStates }; - delete newFormStates[formId]; - set({ formStates: newFormStates }); - }, - - clearAllFormStates: () => { - set({ formStates: {} }); - }, - - setNavigationError: (error) => { - set({ navigationError: error }); - }, - }), - { - name: 'motovaultpro-mobile-navigation', - storage: createJSONStorage(() => localStorage), - partialize: (state) => ({ - activeScreen: state.activeScreen, - vehicleSubScreen: state.vehicleSubScreen, - selectedVehicleId: state.selectedVehicleId, - formStates: state.formStates, - }), - } - ) -); -``` - -### Form State Hook Implementation -**File**: `frontend/src/core/hooks/useFormState.ts` - -```tsx -import { useState, useEffect, useCallback, useRef } from 'react'; -import { useNavigationStore } from '../store/navigation'; -import { debounce } from 'lodash'; - -export interface UseFormStateOptions { - formId: string; - defaultValues: T; - autoSave?: boolean; - saveDelay?: number; - onRestore?: (data: T) => void; - onSave?: (data: T) => void; - validate?: (data: T) => Record | null; -} - -export interface FormStateReturn { - formData: T; - updateFormData: (updates: Partial) => void; - setFormData: (data: T) => void; - resetForm: () => void; - submitForm: () => Promise; - hasChanges: boolean; - isRestored: boolean; - isSaving: boolean; - errors: Record; - isValid: boolean; -} - -export const useFormState = >({ - formId, - defaultValues, - autoSave = true, - saveDelay = 1000, - onRestore, - onSave, - validate, -}: UseFormStateOptions): FormStateReturn => { - const { saveFormState, restoreFormState, clearFormState } = useNavigationStore(); - const [formData, setFormDataState] = useState(defaultValues); - const [hasChanges, setHasChanges] = useState(false); - const [isRestored, setIsRestored] = useState(false); - const [isSaving, setIsSaving] = useState(false); - const [errors, setErrors] = useState>({}); - - const initialDataRef = useRef(defaultValues); - const formDataRef = useRef(formData); - - // Update ref when formData changes - useEffect(() => { - formDataRef.current = formData; - }, [formData]); - - // Validation - const validateForm = useCallback((data: T) => { - if (!validate) return {}; - - const validationErrors = validate(data); - return validationErrors || {}; - }, [validate]); - - // Restore form state on mount - useEffect(() => { - const restoredState = restoreFormState(formId); - if (restoredState && !isRestored) { - const restoredData = { ...defaultValues, ...restoredState.data }; - setFormDataState(restoredData); - setHasChanges(restoredState.isDirty); - setIsRestored(true); - - if (onRestore) { - onRestore(restoredData); - } - } - }, [formId, restoreFormState, defaultValues, isRestored, onRestore]); - - // Auto-save with debounce - const debouncedSave = useCallback( - debounce(async (data: T, isDirty: boolean) => { - if (!autoSave || !isDirty) return; - - try { - setIsSaving(true); - saveFormState(formId, data, isDirty); - - if (onSave) { - await onSave(data); - } - } catch (error) { - console.warn('Form auto-save failed:', error); - } finally { - setIsSaving(false); - } - }, saveDelay), - [autoSave, saveDelay, formId, saveFormState, onSave] - ); - - // Trigger auto-save when form data changes - useEffect(() => { - if (hasChanges) { - const validationErrors = validateForm(formData); - setErrors(validationErrors); - debouncedSave(formData, hasChanges); - } - }, [formData, hasChanges, validateForm, debouncedSave]); - - const updateFormData = useCallback((updates: Partial) => { - setFormDataState((current) => { - const updated = { ...current, ...updates }; - const hasActualChanges = JSON.stringify(updated) !== JSON.stringify(initialDataRef.current); - setHasChanges(hasActualChanges); - return updated; - }); - }, []); - - const setFormData = useCallback((data: T) => { - setFormDataState(data); - const hasActualChanges = JSON.stringify(data) !== JSON.stringify(initialDataRef.current); - setHasChanges(hasActualChanges); - }, []); - - const resetForm = useCallback(() => { - setFormDataState(defaultValues); - setHasChanges(false); - setErrors({}); - clearFormState(formId); - initialDataRef.current = { ...defaultValues }; - }, [defaultValues, formId, clearFormState]); - - const submitForm = useCallback(async () => { - const validationErrors = validateForm(formDataRef.current); - setErrors(validationErrors); - - if (Object.keys(validationErrors).length > 0) { - throw new Error('Form validation failed'); - } - - try { - setHasChanges(false); - clearFormState(formId); - initialDataRef.current = { ...formDataRef.current }; - - if (onSave) { - await onSave(formDataRef.current); - } - } catch (error) { - setHasChanges(true); // Restore changes state on error - throw error; - } - }, [validateForm, formId, clearFormState, onSave]); - - const isValid = Object.keys(errors).length === 0; - - return { - formData, - updateFormData, - setFormData, - resetForm, - submitForm, - hasChanges, - isRestored, - isSaving, - errors, - isValid, - }; -}; -``` - -## Token Management Examples - -### Enhanced API Client with 401 Retry -**File**: `frontend/src/core/api/enhancedClient.ts` - -```tsx -import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; - -interface TokenManager { - refreshToken(): Promise; - isRefreshing(): boolean; - addFailedRequest(request: () => Promise): void; -} - -class AuthTokenManager implements TokenManager { - private static instance: AuthTokenManager; - private _isRefreshing = false; - private failedQueue: Array<{ - resolve: (token: string) => void; - reject: (error: Error) => void; - }> = []; - private getAccessTokenSilently: any; - - constructor(getAccessTokenSilently: any) { - this.getAccessTokenSilently = getAccessTokenSilently; - } - - static getInstance(getAccessTokenSilently?: any): AuthTokenManager { - if (!AuthTokenManager.instance && getAccessTokenSilently) { - AuthTokenManager.instance = new AuthTokenManager(getAccessTokenSilently); - } - return AuthTokenManager.instance; - } - - async refreshToken(): Promise { - if (this._isRefreshing) { - return new Promise((resolve, reject) => { - this.failedQueue.push({ resolve, reject }); - }); - } - - this._isRefreshing = true; - - try { - const token = await this.getAccessTokenSilently({ - cacheMode: 'off', - timeoutInSeconds: 20, - }); - - // Process queued requests - this.failedQueue.forEach(({ resolve }) => resolve(token)); - this.failedQueue = []; - - return token; - } catch (error) { - // Reject queued requests - this.failedQueue.forEach(({ reject }) => reject(error as Error)); - this.failedQueue = []; - throw error; - } finally { - this._isRefreshing = false; - } - } - - isRefreshing(): boolean { - return this._isRefreshing; - } - - addFailedRequest(callback: () => Promise): void { - this.failedQueue.push({ - resolve: () => callback(), - reject: (error) => Promise.reject(error), - }); - } -} - -export const createEnhancedApiClient = (getAccessTokenSilently: any): AxiosInstance => { - const tokenManager = AuthTokenManager.getInstance(getAccessTokenSilently); - - const client = axios.create({ - baseURL: process.env.REACT_APP_API_URL || '/api', - timeout: 10000, - headers: { - 'Content-Type': 'application/json', - }, - }); - - // Request interceptor - inject tokens - client.interceptors.request.use( - async (config) => { - try { - // Don't add token if already refreshing or if this is a retry - if (!config._retry && !tokenManager.isRefreshing()) { - const token = await getAccessTokenSilently({ - cacheMode: 'on', - timeoutInSeconds: 15, - }); - - if (token) { - config.headers = config.headers || {}; - config.headers.Authorization = `Bearer ${token}`; - } - } - } catch (error) { - console.warn('Token acquisition failed, proceeding without token:', error); - } - - return config; - }, - (error) => Promise.reject(error) - ); - - // Response interceptor - handle 401s with token refresh retry - client.interceptors.response.use( - (response: AxiosResponse) => response, - async (error) => { - const originalRequest = error.config; - - // Handle 401 responses with token refresh - if (error.response?.status === 401 && !originalRequest._retry) { - originalRequest._retry = true; - - try { - console.log('401 detected, attempting token refresh...'); - const newToken = await tokenManager.refreshToken(); - - // Update the failed request with new token - originalRequest.headers = originalRequest.headers || {}; - originalRequest.headers.Authorization = `Bearer ${newToken}`; - - // Retry the original request - return client(originalRequest); - } catch (refreshError) { - console.error('Token refresh failed:', refreshError); - - // Clear any stored tokens and redirect to login - localStorage.clear(); - window.location.href = '/'; - - return Promise.reject(refreshError); - } - } - - // Enhanced mobile error handling - if (error.code === 'ECONNABORTED' || error.message.includes('timeout')) { - const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( - navigator.userAgent - ); - - if (isMobile) { - error.message = 'Connection timeout. Please check your network and try again.'; - } - } - - return Promise.reject(error); - } - ); - - return client; -}; - -// Usage example -export const useApiClient = () => { - const { getAccessTokenSilently } = useAuth0(); - - return useMemo(() => { - return createEnhancedApiClient(getAccessTokenSilently); - }, [getAccessTokenSilently]); -}; -``` - -## Mobile Component Examples - -### Enhanced Add Vehicle Form with State Persistence -**File**: `frontend/src/features/vehicles/mobile/EnhancedAddVehicleScreen.tsx` - -```tsx -import React, { useCallback } from 'react'; -import { ArrowLeft, Save } from 'lucide-react'; -import { useFormState } from '../../../core/hooks/useFormState'; -import { useNavigationStore } from '../../../core/store/navigation'; -import { GlassCard, MobileContainer, MobilePill } from '../../../shared-minimal/components/mobile'; - -interface VehicleFormData { - year: string; - make: string; - model: string; - trim: string; - vin: string; - licensePlate: string; - nickname: string; - color: string; -} - -interface EnhancedAddVehicleScreenProps { - onVehicleAdded: () => void; -} - -export const EnhancedAddVehicleScreen: React.FC = ({ - onVehicleAdded, -}) => { - const { goBack } = useNavigationStore(); - - const validateForm = useCallback((data: VehicleFormData) => { - const errors: Record = {}; - - if (!data.year || parseInt(data.year) < 1900 || parseInt(data.year) > new Date().getFullYear() + 1) { - errors.year = 'Please enter a valid year'; - } - - if (!data.make.trim()) { - errors.make = 'Make is required'; - } - - if (!data.model.trim()) { - errors.model = 'Model is required'; - } - - if (data.vin && data.vin.length !== 17) { - errors.vin = 'VIN must be 17 characters'; - } - - if (!data.vin.trim() && !data.licensePlate.trim()) { - errors.licensePlate = 'Either VIN or License Plate is required'; - } - - return Object.keys(errors).length > 0 ? errors : null; - }, []); - - const { - formData, - updateFormData, - resetForm, - submitForm, - hasChanges, - isRestored, - isSaving, - errors, - isValid, - } = useFormState({ - formId: 'add-vehicle', - defaultValues: { - year: '', - make: '', - model: '', - trim: '', - vin: '', - licensePlate: '', - nickname: '', - color: '', - }, - validate: validateForm, - onRestore: (data) => { - console.log('Form data restored:', data); - }, - }); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - if (!isValid) { - return; - } - - try { - // Simulate API call - await new Promise(resolve => setTimeout(resolve, 1000)); - - await submitForm(); - onVehicleAdded(); - } catch (error) { - console.error('Error adding vehicle:', error); - // Form state is preserved on error - } - }; - - const handleBack = () => { - if (hasChanges) { - const confirmLeave = window.confirm( - 'You have unsaved changes. Are you sure you want to leave? Your changes will be saved as a draft.' - ); - if (!confirmLeave) return; - } - - goBack(); - }; - - return ( - -
- {/* Header */} -
-
- -
-

Add Vehicle

- {isRestored && ( -

Draft restored

- )} -
-
- - {isSaving && ( -
- - Saving... -
- )} -
- -
- {/* Basic Information */} - -
-

Basic Information

- -
-
- - updateFormData({ year: e.target.value })} - className={`w-full p-3 border rounded-lg ${ - errors.year ? 'border-red-300' : 'border-slate-300' - }`} - /> - {errors.year && ( -

{errors.year}

- )} -
- -
- - updateFormData({ make: e.target.value })} - className={`w-full p-3 border rounded-lg ${ - errors.make ? 'border-red-300' : 'border-slate-300' - }`} - /> - {errors.make && ( -

{errors.make}

- )} -
- -
- - updateFormData({ model: e.target.value })} - className={`w-full p-3 border rounded-lg ${ - errors.model ? 'border-red-300' : 'border-slate-300' - }`} - /> - {errors.model && ( -

{errors.model}

- )} -
- -
- - updateFormData({ trim: e.target.value })} - className="w-full p-3 border border-slate-300 rounded-lg" - /> -
-
-
-
- - {/* Identification */} - -
-

Identification

- -
-
- - updateFormData({ vin: e.target.value.toUpperCase() })} - maxLength={17} - className={`w-full p-3 border rounded-lg font-mono text-sm ${ - errors.vin ? 'border-red-300' : 'border-slate-300' - }`} - /> - {errors.vin && ( -

{errors.vin}

- )} -
- -
- - updateFormData({ licensePlate: e.target.value.toUpperCase() })} - className={`w-full p-3 border rounded-lg ${ - errors.licensePlate ? 'border-red-300' : 'border-slate-300' - }`} - /> - {errors.licensePlate && ( -

{errors.licensePlate}

- )} -
- -
- - updateFormData({ nickname: e.target.value })} - className="w-full p-3 border border-slate-300 rounded-lg" - /> -
-
-
-
- - {/* Form Actions */} -
- - -
- - {hasChanges && ( -

- Changes are being saved automatically -

- )} -
-
-
- ); -}; -``` - -## App Integration Examples - -### Updated App.tsx with Enhanced Navigation -**File**: `frontend/src/App.tsx` (key sections) - -```tsx -import React, { useEffect } from 'react'; -import { useAuth0 } from '@auth0/auth0-react'; -import { useNavigationStore } from './core/store/navigation'; -import { useUserStore } from './core/store/user'; -import { MobileSettingsScreen } from './features/settings/mobile/MobileSettingsScreen'; -import { EnhancedAddVehicleScreen } from './features/vehicles/mobile/EnhancedAddVehicleScreen'; - -const MobileApp: React.FC = () => { - const { user, isAuthenticated, isLoading } = useAuth0(); - const { - activeScreen, - vehicleSubScreen, - selectedVehicleId, - navigateToScreen, - navigateToVehicleSubScreen, - goBack, - canGoBack, - navigationError, - } = useNavigationStore(); - - const { setUserProfile } = useUserStore(); - - // Update user profile when authenticated - useEffect(() => { - if (isAuthenticated && user) { - setUserProfile(user); - } - }, [isAuthenticated, user, setUserProfile]); - - // Handle mobile back button and navigation errors - useEffect(() => { - const handlePopState = (event: PopStateEvent) => { - event.preventDefault(); - if (canGoBack()) { - goBack(); - } - }; - - const handleNavigationError = () => { - if (navigationError) { - console.error('Navigation error:', navigationError); - // Could show toast notification here - } - }; - - window.addEventListener('popstate', handlePopState); - handleNavigationError(); - - return () => { - window.removeEventListener('popstate', handlePopState); - }; - }, [goBack, canGoBack, navigationError]); - - const handleVehicleSelect = useCallback((vehicleId: string) => { - navigateToVehicleSubScreen('detail', vehicleId, { source: 'vehicle-list' }); - }, [navigateToVehicleSubScreen]); - - const handleAddVehicle = useCallback(() => { - navigateToVehicleSubScreen('add', null, { source: 'vehicle-list' }); - }, [navigateToVehicleSubScreen]); - - const handleBackToList = useCallback(() => { - navigateToVehicleSubScreen('list', null, { source: 'back-navigation' }); - }, [navigateToVehicleSubScreen]); - - const handleVehicleAdded = useCallback(() => { - navigateToVehicleSubScreen('list', null, { source: 'vehicle-added' }); - }, [navigateToVehicleSubScreen]); - - // Show loading screen while Auth0 initializes - if (isLoading) { - return ( -
-
-
-

Loading MotoVaultPro...

-
-
- ); - } - - // Show login screen if not authenticated - if (!isAuthenticated) { - return ( -
-
-

MotoVaultPro

-

Track your vehicles and fuel efficiency

- -
-
- ); - } - - // Main mobile app interface - return ( -
- {/* Navigation Error Banner */} - {navigationError && ( -
-

Navigation Error

-

{navigationError}

-
- )} - - {/* Screen Content */} - {renderActiveScreen()} - - {/* Bottom Navigation */} - -
- ); - - // Screen rendering logic - function renderActiveScreen() { - switch (activeScreen) { - case 'vehicles': - return renderVehiclesScreen(); - case 'fuel': - return ; - case 'dashboard': - return ; - case 'settings': - return ; - default: - return renderVehiclesScreen(); - } - } - - function renderVehiclesScreen() { - switch (vehicleSubScreen) { - case 'list': - return ( - - ); - case 'detail': - return ( - - ); - case 'add': - return ( - - ); - default: - return ( - - ); - } - } -}; -``` - -These code examples provide concrete, implementable solutions for all aspects of the mobile optimization plan. Each example includes proper error handling, TypeScript types, and integration with the existing architecture. \ No newline at end of file diff --git a/docs/changes/mobile-optimization-v1/07-TESTING-CHECKLIST.md b/docs/changes/mobile-optimization-v1/07-TESTING-CHECKLIST.md deleted file mode 100644 index b747cfa..0000000 --- a/docs/changes/mobile-optimization-v1/07-TESTING-CHECKLIST.md +++ /dev/null @@ -1,302 +0,0 @@ -# Testing Checklist - Mobile + Desktop Validation - -## Overview -Comprehensive testing checklist to ensure all mobile optimization improvements work correctly on both mobile and desktop platforms. Every item must be verified before considering implementation complete. - -## Pre-Testing Setup - -### Environment Requirements -- [ ] Mobile testing device or Chrome DevTools mobile simulation -- [ ] Desktop testing environment (Chrome, Firefox, Safari) -- [ ] Local development environment with Docker containers running -- [ ] Valid Auth0 test account credentials -- [ ] Network throttling tools for mobile connectivity testing - -### Test Data Setup -- [ ] Create test user account in Auth0 -- [ ] Add 2-3 test vehicles with different data patterns -- [ ] Create sample fuel log entries -- [ ] Set up various form states for persistence testing - -## Phase 1: Mobile Settings Implementation Testing - -### Mobile Settings Screen Functionality -- [ ] **Settings Screen Renders**: Mobile settings screen displays correctly with all sections -- [ ] **Account Section**: User profile information displays correctly (name, email, picture, join date) -- [ ] **Notifications Toggles**: All notification toggles (email, push, maintenance) function properly -- [ ] **Dark Mode Toggle**: Dark mode toggle switches interface theme -- [ ] **Unit System Toggle**: Imperial/Metric toggle changes units throughout app -- [ ] **Data Export**: Data export modal opens and functions correctly -- [ ] **Logout Function**: Sign out button logs user out and returns to login screen -- [ ] **Delete Account**: Account deletion confirmation modal works properly - -### Mobile Settings Persistence -- [ ] **Settings Persist**: All settings changes persist across app restarts -- [ ] **Dark Mode Persistence**: Dark mode setting maintained across sessions -- [ ] **Unit System Persistence**: Unit system choice persists and applies globally -- [ ] **Notification Preferences**: Notification settings persist correctly -- [ ] **Settings Sync**: Settings changes reflect immediately in other app areas - -### Mobile Navigation Integration -- [ ] **Bottom Nav Access**: Settings accessible via bottom navigation -- [ ] **Active State**: Bottom navigation shows settings as active when on settings screen -- [ ] **Back Navigation**: Back button from settings returns to previous screen -- [ ] **Context Preservation**: Returning from settings maintains previous app context - -### Desktop Compatibility -- [ ] **Desktop Settings Unchanged**: Existing desktop settings page still functions -- [ ] **Settings Synchronization**: Changes made on mobile reflect on desktop and vice versa -- [ ] **No Desktop Regression**: Desktop functionality remains unaffected -- [ ] **Cross-Platform Consistency**: Settings behavior consistent across platforms - -## Phase 2: State Management & Navigation Testing - -### Mobile Navigation Context -- [ ] **Screen Transitions**: All screen transitions maintain user context -- [ ] **Selected Vehicle**: Selected vehicle preserved during navigation -- [ ] **Form State**: Form data preserved when navigating away -- [ ] **Navigation History**: Back button navigation works correctly -- [ ] **Deep Navigation**: Multi-level navigation (vehicles → detail → edit) maintains context - -### Form State Persistence -- [ ] **Add Vehicle Form**: Form data saved automatically during input -- [ ] **Draft Recovery**: Returning to add vehicle form restores saved draft -- [ ] **Form Validation**: Validation state preserved across navigation -- [ ] **Form Completion**: Completing form clears saved draft -- [ ] **Form Reset**: Reset button clears both form and saved draft - -### State Persistence Across App Restarts -- [ ] **Navigation State**: Current screen and sub-screen restored on app restart -- [ ] **Selected Vehicle**: Selected vehicle context restored on app restart -- [ ] **Form Drafts**: Form drafts available after app restart -- [ ] **User Preferences**: All user preferences restored on app restart -- [ ] **Storage Cleanup**: Old/expired state data cleaned up properly - -### Navigation Error Handling -- [ ] **Invalid States**: App handles invalid navigation states gracefully -- [ ] **Network Errors**: Navigation errors during network issues handled properly -- [ ] **Recovery Options**: Error states provide clear recovery options -- [ ] **Fallback Navigation**: Failed navigation falls back to safe default state - -## Phase 3: Token Management & Authentication Testing - -### Enhanced Token Management -- [ ] **401 Retry Logic**: API calls with 401 responses automatically retry with fresh token -- [ ] **Token Refresh**: Background token refresh prevents expiration during extended use -- [ ] **Retry Success**: Failed requests succeed after token refresh -- [ ] **Multiple 401s**: Multiple simultaneous 401s handled correctly without duplicate refresh - -### Mobile Token Optimization -- [ ] **Adaptive Warm-up**: Token warm-up timing adapts to device performance -- [ ] **Mobile Retry Logic**: Enhanced retry logic handles poor mobile connectivity -- [ ] **Network Recovery**: Token management recovers from network interruptions -- [ ] **App Visibility**: Token refresh triggers when app becomes visible - -### Offline Token Management -- [ ] **Offline Caching**: Tokens cached for offline use when network unavailable -- [ ] **Cache Validation**: Cached tokens validated for expiration -- [ ] **Cache Cleanup**: Expired cached tokens cleaned up properly -- [ ] **Online Recovery**: Normal token flow resumes when network restored - -### Error Boundaries & Recovery -- [ ] **Token Failures**: Auth error boundary catches token acquisition failures -- [ ] **Graceful Degradation**: App continues functioning when possible during token issues -- [ ] **User Feedback**: Clear error messages displayed for authentication issues -- [ ] **Recovery Actions**: Users can retry or re-authenticate when needed - -## Phase 4: Cross-Platform Feature Parity Testing - -### Feature Completeness -- [ ] **Mobile Settings**: All desktop settings features available on mobile -- [ ] **Vehicle Management**: Vehicle CRUD operations work on both platforms -- [ ] **Fuel Logging**: Fuel log functionality consistent across platforms -- [ ] **Data Export**: Data export works from both mobile and desktop -- [ ] **Account Management**: Account actions (logout, delete) work on both platforms - -### UX Consistency -- [ ] **Navigation Patterns**: Navigation feels natural on each platform -- [ ] **Data Persistence**: Data changes sync between mobile and desktop -- [ ] **Performance**: Similar performance characteristics across platforms -- [ ] **Error Handling**: Consistent error handling and messaging - -### Responsive Design Validation -- [ ] **Breakpoint Transitions**: Smooth transitions between mobile and desktop views -- [ ] **Component Adaptation**: Components adapt properly to different screen sizes -- [ ] **Touch Interactions**: Touch interactions work correctly on mobile -- [ ] **Keyboard Navigation**: Keyboard navigation works correctly on desktop - -## Integration Testing - -### Auth0 Integration -- [ ] **Login Flow**: Complete login flow works on mobile and desktop -- [ ] **Token Injection**: API calls automatically include Bearer tokens -- [ ] **Session Management**: User sessions managed consistently -- [ ] **Logout Process**: Complete logout process works correctly - -### API Integration -- [ ] **Enhanced Client**: Enhanced API client works with all existing endpoints -- [ ] **Error Handling**: API errors handled gracefully with improved messages -- [ ] **Request Retry**: Failed requests retry appropriately -- [ ] **Mobile Optimization**: Mobile-specific optimizations don't break desktop - -### State Management Integration -- [ ] **Zustand Compatibility**: New stores integrate properly with existing Zustand stores -- [ ] **React Query**: Data caching continues working with state management changes -- [ ] **Local Storage**: Multiple storage keys don't conflict -- [ ] **Performance Impact**: State management changes don't negatively impact performance - -## Network Conditions Testing - -### Mobile Network Scenarios -- [ ] **Slow 3G**: App functions correctly on slow 3G connection -- [ ] **Intermittent Connectivity**: Handles intermittent network connectivity gracefully -- [ ] **WiFi to Cellular**: Smooth transition between WiFi and cellular networks -- [ ] **Network Recovery**: Proper recovery when network becomes available - -### Offline Scenarios -- [ ] **Offline Functionality**: Essential features work while offline -- [ ] **Data Persistence**: Data persists during offline periods -- [ ] **Sync on Reconnect**: Data syncs properly when connection restored -- [ ] **Offline Indicators**: Users informed about offline status - -## Performance Testing - -### Mobile Performance -- [ ] **App Launch Time**: App launches within acceptable time on mobile devices -- [ ] **Screen Transitions**: Smooth screen transitions without lag -- [ ] **Form Input Response**: Form inputs respond immediately to user interaction -- [ ] **Memory Usage**: Reasonable memory usage on mobile devices - -### Desktop Performance -- [ ] **No Performance Regression**: Desktop performance not negatively impacted -- [ ] **Resource Usage**: CPU and memory usage remain acceptable -- [ ] **Loading Times**: Page load times remain fast -- [ ] **Responsiveness**: UI remains responsive during all operations - -## Security Testing - -### Authentication Security -- [ ] **Token Security**: Tokens stored securely and not exposed -- [ ] **Session Timeout**: Proper session timeout handling -- [ ] **Logout Cleanup**: Complete cleanup of sensitive data on logout -- [ ] **Error Information**: No sensitive information leaked in error messages - -### Data Protection -- [ ] **Local Storage**: Sensitive data not stored in plain text locally -- [ ] **Network Requests**: All API requests use HTTPS -- [ ] **Data Validation**: User input properly validated and sanitized -- [ ] **Access Control**: Users can only access their own data - -## Browser Compatibility Testing - -### Mobile Browsers -- [ ] **Safari iOS**: Full functionality on Safari iOS -- [ ] **Chrome Android**: Full functionality on Chrome Android -- [ ] **Samsung Internet**: Basic functionality on Samsung Internet -- [ ] **Mobile Firefox**: Basic functionality on mobile Firefox - -### Desktop Browsers -- [ ] **Chrome Desktop**: Full functionality on Chrome desktop -- [ ] **Safari Desktop**: Full functionality on Safari desktop -- [ ] **Firefox Desktop**: Full functionality on Firefox desktop -- [ ] **Edge Desktop**: Basic functionality on Edge desktop - -## Accessibility Testing - -### Mobile Accessibility -- [ ] **Touch Targets**: Touch targets meet minimum size requirements -- [ ] **Screen Reader**: Basic screen reader compatibility -- [ ] **Contrast Ratios**: Adequate contrast ratios for text and backgrounds -- [ ] **Focus Management**: Proper focus management for navigation - -### Desktop Accessibility -- [ ] **Keyboard Navigation**: Full keyboard navigation support -- [ ] **Screen Reader**: Screen reader compatibility maintained -- [ ] **ARIA Labels**: Appropriate ARIA labels for interactive elements -- [ ] **Focus Indicators**: Visible focus indicators for all interactive elements - -## Regression Testing - -### Existing Functionality -- [ ] **Vehicle Management**: All existing vehicle management features still work -- [ ] **Fuel Logging**: All existing fuel logging features still work -- [ ] **User Authentication**: All existing authentication flows still work -- [ ] **Data Persistence**: All existing data persistence continues working - -### API Endpoints -- [ ] **All Endpoints**: All existing API endpoints continue working correctly -- [ ] **Data Formats**: API responses in correct formats -- [ ] **Error Responses**: API error responses handled correctly -- [ ] **Rate Limiting**: API rate limiting continues working - -## Post-Implementation Validation - -### User Experience -- [ ] **Intuitive Navigation**: Navigation feels intuitive and natural -- [ ] **Fast Performance**: App feels fast and responsive on both platforms -- [ ] **Reliable Functionality**: All features work reliably without errors -- [ ] **Consistent Behavior**: Behavior is consistent across platforms - -### Technical Quality -- [ ] **Code Quality**: Code follows established patterns and conventions -- [ ] **Error Handling**: Comprehensive error handling throughout -- [ ] **Logging**: Appropriate logging for debugging and monitoring -- [ ] **Documentation**: Code properly documented and maintainable - -## Test Completion Criteria - -### Phase 1 Completion -- [ ] All mobile settings tests pass -- [ ] No desktop functionality regression -- [ ] Settings persistence works correctly -- [ ] Mobile navigation integration complete - -### Phase 2 Completion -- [ ] All state management tests pass -- [ ] Form persistence works reliably -- [ ] Navigation context maintained -- [ ] Error handling robust - -### Phase 3 Completion -- [ ] All token management tests pass -- [ ] Authentication flows reliable -- [ ] Mobile optimizations functional -- [ ] Error boundaries effective - -### Phase 4 Completion -- [ ] All feature parity tests pass -- [ ] Cross-platform consistency achieved -- [ ] Performance requirements met -- [ ] Security requirements satisfied - -### Overall Implementation Success -- [ ] All test categories completed successfully -- [ ] No critical bugs identified -- [ ] Performance within acceptable limits -- [ ] User experience improved on both platforms -- [ ] Code ready for production deployment - -## Bug Reporting Template - -When issues are found during testing, report using this template: - -``` -**Bug Title**: [Brief description] - -**Platform**: Mobile/Desktop/Both -**Browser/Device**: [Specific browser or device] -**Steps to Reproduce**: -1. [Step 1] -2. [Step 2] -3. [Step 3] - -**Expected Behavior**: [What should happen] -**Actual Behavior**: [What actually happens] -**Severity**: Critical/High/Medium/Low -**Screenshots**: [If applicable] - -**Test Case**: [Reference to specific test case] -**Phase**: [Which implementation phase] -``` - -This comprehensive testing checklist ensures that all mobile optimization improvements are thoroughly validated before deployment, maintaining the high quality and reliability standards of the MotoVaultPro application. \ No newline at end of file diff --git a/docs/changes/mobile-optimization-v1/IMPLEMENTATION-STATUS.md b/docs/changes/mobile-optimization-v1/IMPLEMENTATION-STATUS.md deleted file mode 100644 index 40a9394..0000000 --- a/docs/changes/mobile-optimization-v1/IMPLEMENTATION-STATUS.md +++ /dev/null @@ -1,546 +0,0 @@ -# Mobile Optimization V1 - Implementation Status - -## Overview -Real-time tracking of implementation progress for Mobile Optimization V1. This document is updated as each component is implemented and tested. - -**Started**: 2025-01-13 -**Current Phase**: Phase 2 - Navigation & State Consistency (IN PROGRESS) -**Overall Progress**: 25% (Phase 1 Complete, Phase 2 Starting) - -## Implementation Phases - -### Phase 1: Critical Mobile Settings Implementation āœ… **COMPLETED** -**Priority**: 1 (Critical) -**Timeline**: 2-3 days (Completed in 1 day) -**Progress**: 100% (6/6 tasks completed) - -#### Tasks Status -- [x] Create mobile settings directory structure -- [x] Implement MobileSettingsScreen component -- [x] Create settings hooks for state management -- [x] Update App.tsx integration -- [x] Test mobile settings functionality -- [x] Validate desktop compatibility - -#### Current Status -**Status**: Phase 1 implementation complete and tested -**Last Updated**: 2025-01-13 -**Next Action**: Begin Phase 2 - Navigation & State Consistency - ---- - -### Phase 2: Navigation & State Consistency ā³ **IN PROGRESS** -**Priority**: 2 (High) -**Timeline**: 2-3 days -**Progress**: 0% (0/6 tasks completed, just started) - -#### Tasks Status -- [ ] Create enhanced navigation store -- [ ] Implement form state management hook -- [ ] Update App.tsx mobile navigation logic -- [ ] Add mobile back button handling -- [ ] Test state persistence -- [ ] Validate navigation consistency - -#### Current Status -**Status**: Beginning Phase 2 implementation -**Last Updated**: 2025-01-13 -**Next Action**: Create enhanced navigation store with state persistence - ---- - -### Phase 3: Token & Data Flow Optimization šŸ“‹ **PLANNED** -**Priority**: 3 (Medium) -**Timeline**: 1-2 days -**Progress**: 0% (Documentation complete, awaiting Phases 1-2) - -#### Tasks Status -- [ ] Implement enhanced API client with 401 retry -- [ ] Add background token refresh service -- [ ] Create auth error boundary -- [ ] Add adaptive token warm-up -- [ ] Add offline token caching -- [ ] Test token management improvements - -#### Dependencies -- Phases 1-2 must be complete - ---- - -### Phase 4: UX Consistency & Enhancement šŸ“‹ **PLANNED** -**Priority**: 4 (Low) -**Timeline**: 2-3 days -**Progress**: 0% (Documentation complete, awaiting Phases 1-3) - -#### Tasks Status -- [ ] Audit platform parity -- [ ] Consider PWA features -- [ ] Implement mobile-specific optimizations -- [ ] Add offline functionality -- [ ] Final UX consistency review -- [ ] Performance optimization - -#### Dependencies -- Phases 1-3 must be complete - -## Detailed Implementation Log - -### 2025-01-13 - Project Initiation & Phase 1 Implementation - -#### Documentation Phase āœ… **COMPLETED** -**Time**: 2 hours -**Status**: All planning documentation complete - -**Completed Items**: -- āœ… Created comprehensive research findings document -- āœ… Developed 4-phase implementation plan -- āœ… Wrote detailed mobile settings implementation guide -- āœ… Created state management solutions documentation -- āœ… Developed token optimization guide -- āœ… Produced extensive code examples and snippets -- āœ… Created comprehensive testing checklist - -**Key Findings from Research**: -- Mobile settings gap identified (desktop has full settings, mobile has placeholder) -- No infinite login issues found (Auth0 architecture well-designed) -- State management needs enhancement for mobile navigation persistence -- Token management opportunities for better mobile experience - -**Files Created**: -- `docs/changes/mobile-optimization-v1/README.md` -- `docs/changes/mobile-optimization-v1/01-RESEARCH-FINDINGS.md` -- `docs/changes/mobile-optimization-v1/02-IMPLEMENTATION-PLAN.md` -- `docs/changes/mobile-optimization-v1/03-MOBILE-SETTINGS.md` -- `docs/changes/mobile-optimization-v1/04-STATE-MANAGEMENT.md` -- `docs/changes/mobile-optimization-v1/05-TOKEN-OPTIMIZATION.md` -- `docs/changes/mobile-optimization-v1/06-CODE-EXAMPLES.md` -- `docs/changes/mobile-optimization-v1/07-TESTING-CHECKLIST.md` - -#### Phase 1 Implementation āœ… **COMPLETED** -**Time**: 3 hours -**Status**: Mobile settings fully implemented and integrated - -**Completed Items**: -- āœ… Created mobile settings directory structure (`frontend/src/features/settings/`) -- āœ… Implemented settings persistence hooks (`useSettings.ts`, `useSettingsPersistence.ts`) -- āœ… Created comprehensive MobileSettingsScreen component with: - - Account information display - - Notifications toggles (email, push, maintenance) - - Dark mode toggle - - Unit system toggle (imperial/metric) - - Data export functionality - - Account actions (logout, delete account) -- āœ… Integrated mobile settings with App.tsx -- āœ… Fixed TypeScript import issues -- āœ… Successfully built and deployed to containers - -**Technical Implementation Details**: -- **Settings Persistence**: Uses localStorage with key `motovaultpro-mobile-settings` -- **Component Architecture**: Follows existing mobile patterns (GlassCard, MobileContainer) -- **State Management**: React hooks with automatic persistence -- **Integration**: Seamless replacement of placeholder SettingsScreen in App.tsx - -**Files Created**: -- `frontend/src/features/settings/hooks/useSettings.ts` -- `frontend/src/features/settings/hooks/useSettingsPersistence.ts` -- `frontend/src/features/settings/mobile/MobileSettingsScreen.tsx` - -**Files Modified**: -- `frontend/src/App.tsx` (integrated MobileSettingsScreen) - ---- - -### Phase 1 Implementation Details - COMPLETED āœ… - -#### Task 1: Create Mobile Settings Directory Structure āœ… **COMPLETED** -**Status**: Completed successfully -**Files Created**: -``` -frontend/src/features/settings/ -ā”œā”€ā”€ mobile/ -│ └── MobileSettingsScreen.tsx -└── hooks/ - ā”œā”€ā”€ useSettings.ts - └── useSettingsPersistence.ts -``` - -#### Task 2: Implement MobileSettingsScreen Component āœ… **COMPLETED** -**Status**: Comprehensive component created -**Implementation**: Full-featured settings screen with all desktop parity -- Account information with user profile display -- Toggle switches for all notification types -- Dark mode toggle (prepared for future implementation) -- Unit system toggle (imperial/metric) -- Data export modal with confirmation -- Account actions (logout, delete account with confirmation) - -#### Task 3: Create Settings Hooks āœ… **COMPLETED** -**Status**: State management hooks implemented -**Files**: -- `useSettings.ts` - Main settings state management -- `useSettingsPersistence.ts` - localStorage persistence logic - -#### Task 4: Update App.tsx Integration āœ… **COMPLETED** -**Status**: Successfully integrated -**Changes**: Replaced placeholder SettingsScreen with MobileSettingsScreen component - -#### Task 5: Test Mobile Settings Functionality āœ… **COMPLETED** -**Status**: Build successful, containers deployed -**Testing**: Component builds without errors, ready for functional testing - -#### Task 6: Validate Desktop Compatibility āœ… **COMPLETED** -**Status**: No desktop regression detected -**Verification**: Changes isolated to mobile components, desktop unaffected - -## Testing Progress - -### Phase 1 Testing Checklist -**Progress**: 0/24 tests completed - -#### Mobile Settings Screen Functionality (0/8 completed) -- [ ] Settings Screen Renders -- [ ] Account Section -- [ ] Notifications Toggles -- [ ] Dark Mode Toggle -- [ ] Unit System Toggle -- [ ] Data Export -- [ ] Logout Function -- [ ] Delete Account - -#### Mobile Settings Persistence (0/5 completed) -- [ ] Settings Persist -- [ ] Dark Mode Persistence -- [ ] Unit System Persistence -- [ ] Notification Preferences -- [ ] Settings Sync - -#### Mobile Navigation Integration (0/4 completed) -- [ ] Bottom Nav Access -- [ ] Active State -- [ ] Back Navigation -- [ ] Context Preservation - -#### Desktop Compatibility (0/7 completed) -- [ ] Desktop Settings Unchanged -- [ ] Settings Synchronization -- [ ] No Desktop Regression -- [ ] Cross-Platform Consistency - -## Issues & Blockers - -### Current Issues -**Count**: 0 -**Status**: No issues identified - -### Resolved Issues -**Count**: 0 -**Status**: No issues resolved yet - -## Performance Metrics - -### Development Time Tracking -- **Planning & Documentation**: 2 hours āœ… -- **Phase 1 Implementation**: 0 hours (not started) -- **Phase 2 Implementation**: 0 hours (not started) -- **Phase 3 Implementation**: 0 hours (not started) -- **Phase 4 Implementation**: 0 hours (not started) -- **Testing & Validation**: 0 hours (not started) - -**Total Time Invested**: 2 hours -**Estimated Remaining**: 20-25 hours - -### Code Quality Metrics -- **Files Modified**: 0 -- **Files Created**: 8 (documentation) -- **Lines of Code Added**: 0 (implementation) -- **Tests Written**: 0 -- **Documentation Pages**: 8 - -## Success Criteria Tracking - -### Phase 1 Success Criteria (0/6 achieved) -- [ ] Mobile settings screen fully functional -- [ ] Feature parity achieved between mobile and desktop settings -- [ ] No regression in existing functionality -- [ ] Settings persist across app restarts -- [ ] Mobile navigation integration complete -- [ ] Desktop compatibility maintained - -### Overall Implementation Success (0/4 achieved) -- [ ] All test categories completed successfully -- [ ] No critical bugs identified -- [ ] Performance within acceptable limits -- [ ] User experience improved on both platforms - -## Next Steps - -### Immediate Actions (Next 30 minutes) -1. Create mobile settings directory structure -2. Implement basic MobileSettingsScreen component -3. Set up settings hooks for state management - -### Short Term (Next 2 hours) -1. Complete all mobile settings components -2. Integrate with App.tsx -3. Begin initial testing - -### Medium Term (Next 1-2 days) -1. Complete Phase 1 testing -2. Begin Phase 2 implementation -3. Start state management enhancements - ---- - -**Last Updated**: 2025-01-13 - Phase 1 Complete -**Updated By**: Claude (Implementation Phase) -**Next Update**: Beginning Phase 2 - Navigation & State Consistency - -## Phase 1 Summary: Mobile Settings Implementation āœ… - -### What Was Accomplished -Phase 1 has been **successfully completed** ahead of schedule. The critical mobile settings gap has been eliminated, providing full feature parity between mobile and desktop platforms. - -### Key Achievements -1. **šŸŽÆ Gap Eliminated**: Mobile now has comprehensive settings (was placeholder-only) -2. **šŸ“± Feature Parity**: All desktop settings functionality available on mobile -3. **šŸ”„ State Persistence**: Settings persist across app restarts via localStorage -4. **šŸŽØ Consistent Design**: Follows existing mobile UI patterns and components -5. **⚔ No Regression**: Desktop functionality unaffected -6. **šŸ—ļø Clean Architecture**: Modular, reusable components and hooks - -### Implementation Quality -- **Type Safety**: Full TypeScript implementation -- **Error Handling**: Graceful error handling in persistence layer -- **User Experience**: Intuitive toggles, confirmation modals, and feedback -- **Performance**: Lightweight implementation with minimal bundle impact -- **Maintainability**: Clear separation of concerns and well-documented code - -### Ready for Production -āœ… Component builds successfully -āœ… No TypeScript errors -āœ… Follows existing architecture patterns -āœ… Desktop compatibility maintained -āœ… Ready for functional testing - -Phase 1 establishes the foundation for mobile optimization improvements and demonstrates the effectiveness of the planned architecture. - ---- - -## Phase 2 Summary: Navigation & State Consistency āœ… - -### What Was Accomplished -Phase 2 has been **successfully completed** with comprehensive navigation and state management enhancements. The mobile experience now includes sophisticated state persistence and navigation patterns. - -### Key Achievements -1. **šŸ—ļø Enhanced Navigation**: Comprehensive Zustand-based navigation store with history -2. **šŸ’¾ State Persistence**: Form data preserved across navigation changes -3. **šŸ“± Mobile Back Button**: Browser back button integration for mobile navigation -4. **šŸ”„ User Context**: Enhanced user profile and preferences management -5. **šŸ› ļø Developer Experience**: Centralized store architecture with TypeScript safety -6. **⚔ Production Ready**: Full build pipeline success and deployment - -### Implementation Details -- **Navigation Store**: Mobile screen management with vehicle sub-screen handling -- **Form State Hook**: Auto-save, restoration, validation, and dirty state tracking -- **User Store**: Profile synchronization with Auth0 and preference persistence -- **App Store**: Compatibility layer for existing components -- **TypeScript Integration**: Strict typing with comprehensive error resolution - -### Technical Quality -āœ… **Build Process**: TypeScript compilation successful -āœ… **Type Safety**: All type errors resolved, strict mode compatible -āœ… **Error Handling**: Comprehensive error boundaries and recovery -āœ… **Performance**: Optimized state updates with minimal re-renders -āœ… **Architecture**: Clean separation of concerns with modular design -āœ… **Deployment**: All containers healthy and serving successfully - -### Ready for Phase 3 -Phase 2 creates a robust foundation for token optimization and data flow improvements, setting up the architecture needed for seamless cross-screen experiences. - ---- - -## Phase 3: Token & Data Flow Optimization šŸš€ **STARTING** - -### Overview -With robust navigation and state management now in place, Phase 3 focuses on optimizing authentication tokens and data flow between mobile and desktop experiences. This phase addresses the original user concerns about token management and ensures seamless data persistence. - -### Key Objectives -1. **šŸ” Token Optimization**: Implement progressive token refresh and caching strategies -2. **šŸ“Š Data Synchronization**: Ensure consistent data flow between mobile and desktop -3. **⚔ Performance Enhancement**: Optimize API calls and reduce redundant requests -4. **šŸ›”ļø Security Improvements**: Enhanced token security and automatic refresh handling -5. **šŸ“± Mobile-First Patterns**: Optimize data loading patterns for mobile constraints - -### Implementation Strategy -**Approach**: Build upon the enhanced state management from Phase 2 to create sophisticated token and data flow patterns that work seamlessly across both mobile and desktop platforms. - -**Priority Order**: -1. Analyze current Auth0 token management patterns -2. Implement progressive token refresh strategy -3. Create data synchronization layer with the enhanced stores -4. Optimize API call patterns for mobile/desktop differences -5. Add offline-first capabilities where appropriate - -### Technical Architecture -- **Token Layer**: Enhanced Auth0 integration with automatic refresh -- **Data Layer**: Unified data flow with React Query optimization -- **Storage Layer**: Strategic caching with the Zustand persistence -- **Sync Layer**: Cross-platform data consistency mechanisms - -**Status**: šŸš€ **STARTING IMPLEMENTATION** -**Timeline**: 4-6 hours estimated -**Dependencies**: Phase 2 navigation and state management āœ… Complete - -### Current System Analysis āœ… **COMPLETED** - -#### Auth0 Token Management Assessment -**Current State**: āœ… **Already Sophisticated** -- **Progressive Token Refresh**: āœ… Implemented with retry logic and exponential backoff -- **Mobile Optimization**: āœ… Specialized mobile token handling with timing delays -- **Cache Strategies**: āœ… Progressive cache modes (on → off → default) -- **Error Recovery**: āœ… Comprehensive retry mechanisms with fallback options -- **Security**: āœ… localStorage refresh tokens with automatic silent refresh - -#### Data Flow Analysis -**Current State**: āœ… **Well Structured** -- **React Query**: āœ… Configured with retry logic and smart refetch policies -- **API Client**: āœ… Axios with mobile-aware error handling and debugging -- **State Management**: āœ… Enhanced Zustand stores with persistence (Phase 2) - -#### Key Finding: **No Authentication Issues Found** -The original user concern about "infinite login loops" appears to be unfounded. The current Auth0 implementation is actually quite sophisticated with: -1. **Mobile-First Design**: Specialized handling for mobile token timing -2. **Progressive Fallback**: Multiple retry strategies with cache modes -3. **Smart Error Handling**: Different messages for mobile vs desktop -4. **Pre-warming**: Token cache initialization to prevent first-call delays - -### Phase 3 Revised Strategy - -**New Focus**: Instead of fixing non-existent token issues, Phase 3 will **enhance and optimize** the already solid foundation: - -#### Priority 1: Data Synchronization Enhancement -- Integrate React Query with the new Zustand stores for better cache consistency -- Add optimistic updates across navigation state changes -- Implement cross-tab synchronization for multi-window scenarios - -#### Priority 2: Mobile Performance Optimization -- Add strategic prefetching for mobile navigation patterns -- Implement background sync capabilities -- Create smart cache warming based on user navigation patterns - -#### Priority 3: Developer Experience Enhancement -- Add comprehensive debugging tools for mobile token flow -- Create performance monitoring for API call patterns -- Enhanced error boundaries with recovery mechanisms - -**Revised Timeline**: 3-4 hours (reduced due to solid existing foundation) - -### Phase 3 Implementation Details - āœ… **COMPLETED** - -#### Priority 1: Data Synchronization Enhancement āœ… **COMPLETED** -**Status**: Successfully implemented comprehensive data sync layer -**Files Created**: -``` -frontend/src/core/ -ā”œā”€ā”€ sync/data-sync.ts # Main data synchronization manager -ā”œā”€ā”€ hooks/useDataSync.ts # React hook integration -ā”œā”€ā”€ query/query-config.ts # Enhanced Query Client with mobile optimization -└── debug/MobileDebugPanel.tsx # Advanced debugging panel for mobile -``` - -**Key Features Implemented**: -- **Cross-Tab Synchronization**: Real-time sync between multiple browser tabs -- **Optimistic Updates**: Immediate UI updates with backend sync -- **Strategic Prefetching**: Smart data loading based on navigation patterns -- **Mobile-Optimized Caching**: Adaptive cache strategies for mobile vs desktop -- **Background Sync**: Automatic data refresh with online/offline handling - -#### Priority 2: Mobile Performance Optimization āœ… **COMPLETED** -**Status**: Mobile-first query strategies implemented -**Enhancements**: -- **Progressive Retry Logic**: Exponential backoff for mobile network issues -- **Adaptive Timeouts**: Longer timeouts for mobile with progressive fallback -- **Smart Cache Management**: Mobile gets 2min stale time vs 5min desktop -- **Reduced Refetch**: Disabled window focus refetch on mobile to save data -- **Offline-First**: Network mode optimized for intermittent connectivity - -#### Priority 3: Developer Experience Enhancement āœ… **COMPLETED** -**Status**: Advanced debugging and monitoring tools implemented -**Features**: -- **Enhanced Debug Panel**: Expandable mobile debug interface with system status -- **Token Monitoring**: Real-time Auth0 token status with manual refresh testing -- **Query Cache Inspection**: Live query cache statistics and health monitoring -- **Navigation Tracking**: Real-time navigation state and history debugging -- **Performance Monitoring**: Query execution time logging and slow query detection - -### Technical Architecture Enhancements -- **Zustand Integration**: Data sync layer fully integrated with Phase 2 navigation stores -- **React Query Optimization**: Mobile-first configuration with intelligent retry strategies -- **Auth0 Enhancement**: Added token monitoring and debugging capabilities -- **Type Safety**: All new code fully typed with comprehensive error handling -- **Production Ready**: All enhancements tested and deployed successfully - -### Build & Deployment Status -āœ… **TypeScript Compilation**: All type errors resolved -āœ… **Production Build**: Vite build successful (1m 14s) -āœ… **Bundle Optimization**: Smart code splitting maintained -āœ… **Container Deployment**: All services healthy and running -āœ… **Enhanced Features Active**: Data sync and debug tools operational - -**Result**: Phase 3 enhances an already solid foundation with sophisticated data synchronization, mobile-optimized performance patterns, and comprehensive debugging tools, completing the mobile optimization initiative. - ---- - -## šŸŽ‰ PROJECT COMPLETION SUMMARY - -### āœ… **Mobile Optimization Initiative: COMPLETE** - -**Total Duration**: 8 hours (planned 25-30 hours) -**Completion Date**: September 13, 2025 -**Status**: āœ… **Successfully Deployed** - -### **What Was Accomplished** - -#### šŸŽÆ **Original Issue Resolution** -- **āŒ "Infinite Login Loops"**: Revealed to be non-existent - Auth0 implementation was already sophisticated -- **āœ… Mobile Settings Gap**: Eliminated completely - full feature parity achieved -- **āœ… Data Flow Optimization**: Enhanced with cross-tab sync and intelligent caching -- **āœ… Mobile Performance**: Optimized with adaptive strategies and offline-first patterns - -#### šŸ“± **Mobile Experience Transformation** -1. **Mobile Settings**: From placeholder → fully functional parity with desktop -2. **Navigation**: From basic state → sophisticated history-based navigation -3. **Data Persistence**: From simple cache → intelligent sync with offline support -4. **Developer Tools**: From basic debug → comprehensive mobile debugging suite -5. **Performance**: From generic → mobile-optimized with adaptive strategies - -#### šŸ—ļø **Technical Architecture Achievements** -- **Phase 1**: Mobile Settings Implementation (5 hours) -- **Phase 2**: Navigation & State Consistency (3 hours) -- **Phase 3**: Token & Data Flow Optimization (3 hours) - -**Total Files Created**: 12 implementation files + 8 documentation files -**Total Features Added**: 15+ major features across mobile/desktop -**Code Quality**: 100% TypeScript, comprehensive error handling, production-ready - -### **Production Deployment Status** -āœ… **All Containers Healthy** -āœ… **Build Pipeline Successful** -āœ… **Zero Regression Issues** -āœ… **Enhanced Features Active** -āœ… **Ready for User Testing** - -### **Key Success Metrics** -- **šŸš€ Performance**: Mobile-optimized caching reduces data usage -- **šŸ”„ Reliability**: Cross-tab sync prevents data inconsistencies -- **šŸ“± UX Consistency**: Full mobile/desktop feature parity achieved -- **šŸ› ļø Maintainability**: Modular architecture with comprehensive typing -- **šŸ› Debugging**: Advanced mobile debugging capabilities for future development - -### **Recommendations for Next Steps** -1. **User Acceptance Testing**: Begin mobile testing with real users -2. **Performance Monitoring**: Monitor mobile performance metrics in production -3. **Feature Expansion**: Leverage new architecture for future mobile features -4. **Documentation**: Consider creating user guides for new mobile features - -**šŸ† The mobile optimization initiative successfully transforms MotoVaultPro from a desktop-first application to a truly mobile-optimized platform while maintaining full backward compatibility and enhancing the overall user experience.** \ No newline at end of file diff --git a/docs/changes/mobile-optimization-v1/README.md b/docs/changes/mobile-optimization-v1/README.md deleted file mode 100644 index 3938c7f..0000000 --- a/docs/changes/mobile-optimization-v1/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# Mobile Optimization V1 - Comprehensive Implementation Plan - -## Overview -This directory contains detailed documentation for implementing mobile/desktop authentication and UX improvements in MotoVaultPro. The plan addresses critical mobile functionality gaps, authentication consistency, and cross-platform feature parity. - -## Key Issues Addressed -- **Mobile Settings Page Missing**: Desktop has full settings, mobile only has placeholder -- **Navigation Paradigm Split**: Mobile state-based vs desktop URL routing -- **State Persistence Gaps**: Mobile navigation loses user context -- **Token Management**: Optimization for mobile network conditions -- **Feature Parity**: Ensuring all features work on both platforms - -## Research Findings Summary -āœ… **No Infinite Login Issues**: Auth0 architecture well-designed with mobile-optimized retry mechanisms -āœ… **Robust Token Management**: Sophisticated progressive fallback strategy for mobile -āœ… **Good Data Caching**: React Query + Zustand providing solid state management -āŒ **Settings Gap**: Major functionality missing on mobile -āŒ **State Reset**: Mobile navigation loses context during transitions - -## Implementation Documentation - -### šŸ“‹ Planning & Research -- **[01-RESEARCH-FINDINGS.md](01-RESEARCH-FINDINGS.md)** - Detailed architecture analysis and identified issues -- **[02-IMPLEMENTATION-PLAN.md](02-IMPLEMENTATION-PLAN.md)** - 4-phase implementation strategy with priorities - -### šŸ”§ Implementation Guides -- **[03-MOBILE-SETTINGS.md](03-MOBILE-SETTINGS.md)** - Mobile settings screen implementation -- **[04-STATE-MANAGEMENT.md](04-STATE-MANAGEMENT.md)** - Navigation and state persistence fixes -- **[05-TOKEN-OPTIMIZATION.md](05-TOKEN-OPTIMIZATION.md)** - Authentication improvements - -### šŸ’» Development Resources -- **[06-CODE-EXAMPLES.md](06-CODE-EXAMPLES.md)** - Code snippets and implementation examples -- **[07-TESTING-CHECKLIST.md](07-TESTING-CHECKLIST.md)** - Mobile + desktop testing requirements - -## Quick Start for Implementation - -1. **Start with Phase 1**: Mobile settings implementation (highest priority) -2. **Review research findings**: Understand current architecture before changes -3. **Follow code examples**: Use provided snippets as implementation templates -4. **Test on both platforms**: Validate every change on mobile AND desktop - -## Architecture Context -- **Dual Implementation Strategy**: Separate mobile/desktop apps within same codebase -- **Mobile Detection**: JavaScript-based detection switching entire UI paradigm -- **Auth0 + localStorage**: No cookies, uses localStorage with refresh tokens -- **React Query + Zustand**: Data caching and state management - -## Critical Requirements -- All changes must maintain mobile + desktop functionality -- Test authentication flows on both platforms -- Preserve existing data persistence patterns -- Maintain backward compatibility - -## Related Documentation -- **[../../README.md](../../README.md)** - Main documentation index -- **[../../VEHICLES-API.md](../../VEHICLES-API.md)** - Platform services integration -- **[../../TESTING.md](../../TESTING.md)** - Testing framework and Docker workflow \ No newline at end of file diff --git a/docs/changes/vehicle-names-v1/CODEX.md b/docs/changes/vehicle-names-v1/CODEX.md deleted file mode 100644 index 3649a7d..0000000 --- a/docs/changes/vehicle-names-v1/CODEX.md +++ /dev/null @@ -1,71 +0,0 @@ -# Vehicle Names v1 – Model/Make Normalization - -Change set to normalize human-facing vehicle make and model names across the application service. Addresses cases like: -- `GMC sierra_1500` → `GMC Sierra 1500` -- `GMC sierra_2500_hd` → `GMC Sierra 2500 HD` - -## Scope -- Application service database (`vehicles`, `vin_cache` tables). -- Backend write paths for vehicle creation and update. -- Non-breaking; affects presentation format only. - -## Rationale -Source values may contain underscores, inconsistent casing, or unnormalized acronyms. We enforce consistent, human-friendly formatting at write time and backfill existing rows. - -## Changes -- Add normalization utility - - File: `backend/src/features/vehicles/domain/name-normalizer.ts` - - `normalizeModelName(input)`: replaces underscores, collapses whitespace, title-cases words, uppercases common acronyms (HD, GT, Z06, etc.). - - `normalizeMakeName(input)`: trims/title-cases, with special cases for `BMW`, `GMC`, `MINI`, `McLaren`. - -- Apply normalization in service layer - - File: `backend/src/features/vehicles/domain/vehicles.service.ts` - - Create flow: normalizes VIN-decoded and client-supplied `make`/`model` prior to persistence. - - Update flow: normalizes any provided `make`/`model` fields before update. - -- Backfill migration for existing rows - - File: `backend/src/features/vehicles/migrations/004_normalize_model_names.sql` - - Adds `normalize_model_name_app(text)` in the DB and updates `vehicles.model` and `vin_cache.model` in-place. - -## Migration -Run inside containers: -``` -make migrate -``` -What it does: -- Creates `normalize_model_name_app(text)` (immutable function) for consistent DB-side normalization. -- Updates existing rows in `vehicles` and `vin_cache` where `model` is not normalized. - -## Acronym Handling (Models) -Uppercased when matched as tokens: -- HD, GT, GL, SE, LE, XLE, RS, SVT, XR, ST, FX4, TRD, ZR1, Z06, GTI, GLI, SI, SS, LT, LTZ, RT, SRT, SR, SR5, XSE, SEL -- Mixed alphanumeric short tokens (e.g., `z06`) are uppercased. - -## Make Special Cases -- `BMW`, `GMC`, `MINI` fully uppercased; `McLaren` with proper casing. -- Otherwise, standard title case across words. - -## Verification -1) After migration, sample queries (inside `make shell-backend`): -``` -psql -U postgres -d motovaultpro -c "SELECT make, model FROM vehicles ORDER BY updated_at DESC LIMIT 10;" -``` -Confirm: no underscores; title case with acronyms uppercased. - -2) Create/update tests (app flow): -- Create a vehicle with `model = 'sierra_2500_hd'` → persisted as `Sierra 2500 HD`. -- VIN-decode flow returns `sierra_1500` → stored as `Sierra 1500`. - -## Rollback -- Code: revert the three files noted above. -- Data: no automatic downgrade (idempotent forward normalization). If critical, restore from backup or reapply custom transformations. - -## Compatibility & Notes -- Read paths unchanged; only write-time and migration normalization applied. -- Case-insensitive indexes are already present; behavior remains consistent. -- Extend acronym lists or special cases easily by editing `name-normalizer.ts` and the migration function if needed for backfills. - -## Next Steps (Optional) -- Add unit tests for `name-normalizer.ts` in `backend/src/features/vehicles/tests/unit/`. -- Expose a one-off admin endpoint or script to re-run normalization for targeted rows if future sources change. - diff --git a/docs/changes/vehicles-dropdown-v1/README.md b/docs/changes/vehicles-dropdown-v1/README.md deleted file mode 100644 index 43e9e47..0000000 --- a/docs/changes/vehicles-dropdown-v1/README.md +++ /dev/null @@ -1,125 +0,0 @@ -# MVP Platform Vehicles Service Implementation - Executive Summary - -## Project Overview - -**UPDATED ARCHITECTURE DECISION**: This implementation creates the MVP Platform Vehicles Service as part of MotoVaultPro's distributed microservices architecture. The service provides hierarchical vehicle API endpoints and VIN decoding capabilities, replacing external NHTSA vPIC API calls with a local, high-performance 3-container microservice. - -**STATUS**: Implementation in progress - Phase 1 (Infrastructure Setup) - -**IMPORTANT**: The `vehicle-etl/` directory is temporary and will be removed when complete. All functionality is being integrated directly into the main MotoVaultPro application as the MVP Platform Vehicles Service. - -## Architecture Goals - -1. **Microservices Architecture**: Create 3-container MVP Platform Vehicles Service (DB + ETL + FastAPI) -2. **Hierarchical Vehicle API**: Implement year-based filtering with hierarchical parameters -3. **PostgreSQL VIN Decoding**: Create vpic.f_decode_vin() function with MSSQL parity -4. **Service Independence**: Platform service completely independent with own database -5. **Performance**: Sub-100ms hierarchical endpoint response times with year-based caching - -## Context7 Verified Technology Stack - -- **Docker Compose**: Latest version with health checks and dependency management āœ… -- **PostgreSQL 15**: Stable, production-ready with excellent Docker support āœ… -- **Python 3.11**: Current stable version for FastAPI ETL processing āœ… -- **Node.js 20**: LTS version for TypeScript backend integration āœ… -- **FastAPI**: Modern async framework, perfect for ETL API endpoints āœ… - -## Implementation Strategy - Distributed Microservices - -The implementation creates a complete 3-container platform service in 6 phases: - -### **Phase 1: Infrastructure Setup** āœ… COMPLETED -- āœ… Added mvp-platform-vehicles-db container (PostgreSQL with vpic schema) -- āœ… Added mvp-platform-vehicles-etl container (Python ETL processor) -- āœ… Added mvp-platform-vehicles-api container (FastAPI service) -- āœ… Updated docker-compose.yml with health checks and dependencies - -### **Phase 2: FastAPI Hierarchical Endpoints** āœ… COMPLETED -- āœ… Implemented year-based hierarchical filtering endpoints (makes, models, trims, engines, transmissions) -- āœ… Added Query parameter validation with FastAPI -- āœ… Created hierarchical caching strategy with Redis -- āœ… Built complete FastAPI application structure with proper dependencies and middleware - -### **Phase 3: PostgreSQL VIN Decoding Function** āœ… COMPLETED -- āœ… Implemented vpic.f_decode_vin() with MSSQL stored procedure parity -- āœ… Added WMI resolution, year calculation, and confidence scoring -- āœ… Created VIN decode caching tables with automatic cache population -- āœ… Built complete year calculation logic with 30-year cycle handling - -### **Phase 4: ETL Container Implementation** āœ… COMPLETED -- āœ… Setup scheduled weekly ETL processing with cron-based scheduler -- āœ… Configured MSSQL source connection with pyodbc and proper ODBC drivers -- āœ… Implemented data transformation and loading pipeline with connection testing -- āœ… Added ETL health checks and error handling with comprehensive logging - -### **Phase 5: Application Integration** āœ… COMPLETED -- āœ… Created platform vehicles client with comprehensive circuit breaker pattern -- āœ… Built platform integration service with automatic fallback to external vPIC -- āœ… Updated vehicles feature to consume hierarchical platform service API -- āœ… Implemented feature flag system for gradual platform service migration -- āœ… Updated all vehicle dropdown endpoints to use hierarchical parameters (year → make → model → trims/engines/transmissions) - -### **Phase 6: Testing & Validation** āœ… READY FOR TESTING -- ⚔ **Ready**: Hierarchical API performance testing (<100ms target) -- ⚔ **Ready**: VIN decoding accuracy parity testing with PostgreSQL function -- ⚔ **Ready**: ETL processing validation with scheduled weekly pipeline -- ⚔ **Ready**: Circuit breaker pattern testing with graceful fallbacks -- ⚔ **Ready**: End-to-end platform service integration testing - -## **šŸŽÆ IMPLEMENTATION COMPLETE** - -All phases of the MVP Platform Vehicles Service implementation are complete. The service is ready for testing and validation. - -## Success Criteria - IMPLEMENTATION STATUS - -- āœ… **Zero Breaking Changes**: Hierarchical API maintains backward compatibility with circuit breakers -- āœ… **Performance**: Platform service designed for <100ms with year-based caching -- āœ… **Accuracy**: PostgreSQL vpic.f_decode_vin() function implements MSSQL stored procedure parity -- āœ… **Reliability**: Weekly ETL scheduler with comprehensive error handling and health checks -- āœ… **Scalability**: Complete 3-container microservice architecture ready for production - -## Next Steps - -1. **Start Services**: `make dev` to start full microservices environment -2. **Test Platform API**: Access http://localhost:8000/docs for FastAPI documentation -3. **Test Application**: Verify hierarchical dropdowns in frontend at https://motovaultpro.com -4. **Monitor ETL**: Check ETL logs with `make logs-platform-vehicles` -5. **Validate Performance**: Test <100ms response times with real vehicle data - -## MVP Platform Foundation Benefits - -This implementation establishes the **foundational pattern for MVP Platform shared services**: - -- **Standardized Naming**: `mvp-platform-*` services and databases -- **Service Isolation**: Separate databases for different domains -- **Scheduled Processing**: Automated data pipeline management -- **API Integration**: Seamless integration through existing feature capsules -- **Monitoring Ready**: Health checks and observability from day one - -## Future Platform Services - -Once established, this pattern enables rapid deployment of additional platform services: - -- `mvp-platform-analytics` (user behavior tracking) -- `mvp-platform-notifications` (email/SMS service) -- `mvp-platform-payments` (payment processing) -- `mvp-platform-documents` (file storage service) - -## Getting Started - -1. Review [Architecture Decisions](./architecture-decisions.md) for technical context -2. Follow [Implementation Checklist](./implementation-checklist.md) for step-by-step execution -3. Execute phases sequentially starting with [Phase 1: Infrastructure](./phase-01-infrastructure.md) -4. Validate each phase using provided test procedures - -## AI Assistant Guidance - -This documentation is optimized for efficient AI assistant execution: - -- Each phase contains explicit, actionable instructions -- All file paths and code changes are precisely specified -- Validation steps are included for each major change -- Error handling and rollback procedures are documented -- Dependencies and prerequisites are clearly stated - -For any clarification on implementation details, refer to the specific phase documentation or the comprehensive [Implementation Checklist](./implementation-checklist.md). \ No newline at end of file diff --git a/docs/changes/vehicles-dropdown-v1/architecture-decisions.md b/docs/changes/vehicles-dropdown-v1/architecture-decisions.md deleted file mode 100644 index f847ff1..0000000 --- a/docs/changes/vehicles-dropdown-v1/architecture-decisions.md +++ /dev/null @@ -1,465 +0,0 @@ -# Architecture Decisions - Vehicle ETL Integration - -## Overview - -This document captures all architectural decisions made during the Vehicle ETL integration project. Each decision includes the context, options considered, decision made, and rationale. This serves as a reference for future AI assistants and development teams. - -## Context7 Technology Validation - -All technology choices were verified through Context7 for current best practices, compatibility, and production readiness: - -- āœ… **Docker Compose**: Latest version with health checks and dependency management -- āœ… **PostgreSQL 15**: Stable, production-ready with excellent Docker support -- āœ… **Python 3.11**: Current stable version for FastAPI ETL processing -- āœ… **Node.js 20**: LTS version for TypeScript backend integration -- āœ… **FastAPI**: Modern async framework, perfect for ETL API endpoints - ---- - -## Decision 1: MVP Platform Naming Convention - -### Context -Need to establish a consistent naming pattern for shared services that will be used across multiple features and future platform services. - -### Options Considered -1. **Generic naming**: `shared-database`, `common-db` -2. **Service-specific naming**: `vehicle-database`, `vpic-database` -3. **Platform-prefixed naming**: `mvp-platform-database`, `mvp-platform-*` - -### Decision Made -**Chosen**: Platform-prefixed naming with pattern `mvp-platform-*` - -### Rationale -- Establishes clear ownership and purpose -- Scales to multiple platform services -- Avoids naming conflicts with feature-specific resources -- Creates recognizable pattern for future services -- Aligns with microservices architecture principles - -### Implementation -- Database service: `mvp-platform-database` -- Database name: `mvp-platform-vehicles` -- User: `mvp_platform_user` -- Cache keys: `mvp-platform:*` - ---- - -## Decision 2: Database Separation Strategy - -### Context -Need to determine how to integrate the MVP Platform database with the existing MotoVaultPro database architecture. - -### Options Considered -1. **Single Database**: Add ETL tables to existing MotoVaultPro database -2. **Schema Separation**: Use separate schemas within existing database -3. **Complete Database Separation**: Separate PostgreSQL instance for platform services - -### Decision Made -**Chosen**: Complete Database Separation - -### Rationale -- **Service Isolation**: Platform services can be independently managed -- **Scalability**: Each service can have different performance requirements -- **Security**: Separate access controls and permissions -- **Maintenance**: Independent backup and recovery procedures -- **Future-Proofing**: Ready for microservices deployment on Kubernetes - -### Implementation -- Main app database: `motovaultpro` on port 5432 -- Platform database: `mvp-platform-vehicles` on port 5433 -- Separate connection pools in backend service -- Independent health checks and monitoring - ---- - -## Decision 3: ETL Processing Architecture - -### Context -Need to replace external NHTSA vPIC API calls with local data while maintaining data freshness. - -### Options Considered -1. **Real-time Proxy**: Cache API responses indefinitely -2. **Daily Sync**: Update local database daily -3. **Weekly Batch ETL**: Full database refresh weekly -4. **Hybrid Approach**: Local cache with periodic full refresh - -### Decision Made -**Chosen**: Weekly Batch ETL with local database - -### Rationale -- **Data Freshness**: Vehicle specifications change infrequently -- **Performance**: Sub-100ms response times achievable with local queries -- **Reliability**: No dependency on external API availability -- **Cost**: Reduces external API calls and rate limiting concerns -- **Control**: Complete control over data quality and availability - -### Implementation -- Weekly Sunday 2 AM ETL execution -- Complete database rebuild each cycle -- Comprehensive error handling and retry logic -- Health monitoring and alerting - ---- - -## Decision 4: Scheduled Processing Implementation - -### Context -Need to implement automated ETL processing with proper scheduling, monitoring, and error handling. - -### Options Considered -1. **External Cron**: Use host system cron to trigger Docker exec -2. **Container Cron**: Install cron daemon within ETL container -3. **Kubernetes CronJob**: Use K8s native job scheduling -4. **Third-party Scheduler**: Use external scheduling service - -### Decision Made -**Chosen**: Container Cron with Docker Compose - -### Rationale -- **Simplicity**: Maintains single Docker Compose deployment -- **Self-Contained**: No external dependencies for development -- **Kubernetes Ready**: Can be migrated to K8s CronJob later -- **Monitoring**: Container-based health checks and logging -- **Development**: Easy local testing and debugging - -### Implementation -- Python 3.11 container with cron daemon -- Configurable schedule via environment variables -- Health checks and status monitoring -- Comprehensive logging and error reporting - ---- - -## Decision 5: API Integration Pattern - -### Context -Need to integrate MVP Platform database access while maintaining exact API compatibility. - -### Options Considered -1. **API Gateway**: Proxy requests to separate ETL API service -2. **Direct Integration**: Query MVP Platform database directly from vehicles feature -3. **Service Layer**: Create intermediate service layer -4. **Hybrid**: Mix of direct queries and service calls - -### Decision Made -**Chosen**: Direct Integration within Vehicles Feature - -### Rationale -- **Performance**: Direct database queries eliminate HTTP overhead -- **Simplicity**: Reduces complexity and potential failure points -- **Maintainability**: All vehicle-related code in single feature capsule -- **Zero Breaking Changes**: Exact same API interface preserved -- **Feature Capsule Pattern**: Maintains self-contained feature architecture - -### Implementation -- MVP Platform repository within vehicles feature -- Direct PostgreSQL queries using existing connection pool pattern -- Same caching strategy with Redis -- Preserve exact response formats - ---- - -## Decision 6: VIN Decoding Algorithm Migration - -### Context -Need to port complex VIN decoding logic from Python ETL to TypeScript backend. - -### Options Considered -1. **Full Port**: Rewrite all VIN decoding logic in TypeScript -2. **Database Functions**: Implement logic as PostgreSQL functions -3. **API Calls**: Call Python ETL API for VIN decoding -4. **Simplified Logic**: Implement basic VIN decoding only - -### Decision Made -**Chosen**: Full Port to TypeScript with Database Assist - -### Rationale -- **Performance**: Avoids HTTP calls for every VIN decode -- **Consistency**: All business logic in same language/runtime -- **Maintainability**: Single codebase for vehicle logic -- **Flexibility**: Can enhance VIN logic without ETL changes -- **Testing**: Easier to test within existing test framework - -### Implementation -- TypeScript VIN validation and year extraction -- Database queries for pattern matching and confidence scoring -- Comprehensive error handling and fallback logic -- Maintain exact same accuracy as original Python implementation - ---- - -## Decision 7: Caching Strategy - -### Context -Need to maintain high performance while transitioning from external API to database queries. - -### Options Considered -1. **No Caching**: Direct database queries only -2. **Database-Level Caching**: PostgreSQL query caching -3. **Application Caching**: Redis with existing patterns -4. **Multi-Level Caching**: Both database and Redis caching - -### Decision Made -**Chosen**: Application Caching with Updated Key Patterns - -### Rationale -- **Existing Infrastructure**: Leverage existing Redis instance -- **Performance Requirements**: Meet sub-100ms response time goals -- **Cache Hit Rates**: Maintain high cache efficiency -- **TTL Strategy**: Different TTLs for different data types -- **Invalidation**: Clear invalidation strategy for data updates - -### Implementation -- VIN decoding: 30-day TTL (specifications don't change) -- Dropdown data: 7-day TTL (infrequent updates) -- Cache key pattern: `mvp-platform:*` for new services -- Existing Redis instance with updated key patterns - ---- - -## Decision 8: Error Handling and Fallback Strategy - -### Context -Need to ensure system reliability when MVP Platform database is unavailable. - -### Options Considered -1. **Fail Fast**: Return errors immediately when database unavailable -2. **External API Fallback**: Fall back to original NHTSA API -3. **Cached Responses**: Return stale cached data -4. **Graceful Degradation**: Provide limited functionality - -### Decision Made -**Chosen**: Graceful Degradation with Cached Responses - -### Rationale -- **User Experience**: Avoid complete service failure -- **Data Availability**: Cached data still valuable when fresh data unavailable -- **System Reliability**: Partial functionality better than complete failure -- **Performance**: Cached responses still meet performance requirements -- **Recovery**: System automatically recovers when database available - -### Implementation -- Return cached data when database unavailable -- Appropriate HTTP status codes (503 Service Unavailable) -- Health check endpoints for monitoring -- Automatic retry logic with exponential backoff - ---- - -## Decision 9: Authentication and Security Model - -### Context -Need to maintain existing security model while adding new platform services. - -### Options Considered -1. **Authenticate All**: Require authentication for all new endpoints -2. **Mixed Authentication**: Some endpoints public, some authenticated -3. **Maintain Current**: Keep dropdown endpoints unauthenticated -4. **Enhanced Security**: Add additional security layers - -### Decision Made -**Chosen**: Maintain Current Security Model - -### Rationale -- **Zero Breaking Changes**: Frontend requires no modifications -- **Security Analysis**: Dropdown data is public NHTSA information -- **Performance**: No authentication overhead for public data -- **Documentation**: Aligned with security.md requirements -- **Future Flexibility**: Can add authentication layers later if needed - -### Implementation -- Dropdown endpoints remain unauthenticated -- CRUD endpoints still require JWT authentication -- Platform services follow same security patterns -- Comprehensive input validation and SQL injection prevention - ---- - -## Decision 10: Testing and Validation Strategy - -### Context -Need comprehensive testing to ensure zero breaking changes and meet performance requirements. - -### Options Considered -1. **Unit Tests Only**: Focus on code-level testing -2. **Integration Tests**: Test API endpoints and database integration -3. **Performance Tests**: Focus on response time requirements -4. **Comprehensive Testing**: All test types with automation - -### Decision Made -**Chosen**: Comprehensive Testing with Automation - -### Rationale -- **Quality Assurance**: Meet all success criteria requirements -- **Risk Mitigation**: Identify issues before production deployment -- **Performance Validation**: Ensure sub-100ms response times -- **Regression Prevention**: Automated tests catch future issues -- **Documentation**: Tests serve as behavior documentation - -### Implementation -- API functionality tests for response format validation -- Authentication tests for security model compliance -- Performance tests for response time requirements -- Data accuracy tests for VIN decoding validation -- ETL process tests for scheduled job functionality -- Load tests for concurrent request handling -- Error handling tests for failure scenarios - ---- - -## Decision 11: Deployment and Infrastructure Strategy - -### Context -Need to determine deployment approach that supports both development and production. - -### Options Considered -1. **Docker Compose Only**: Single deployment method -2. **Kubernetes Only**: Production-focused deployment -3. **Hybrid Approach**: Docker Compose for dev, Kubernetes for prod -4. **Multiple Options**: Support multiple deployment methods - -### Decision Made -**Chosen**: Hybrid Approach (Docker Compose → Kubernetes) - -### Rationale -- **Development Efficiency**: Docker Compose simpler for local development -- **Production Scalability**: Kubernetes required for production scaling -- **Migration Path**: Clear path from development to production -- **Team Skills**: Matches team capabilities and tooling -- **Cost Efficiency**: Docker Compose sufficient for development/staging - -### Implementation -- Current implementation: Docker Compose with production-ready containers -- Future migration: Kubernetes manifests for production deployment -- Container images designed for both environments -- Environment variable configuration for deployment flexibility - ---- - -## Decision 12: Data Migration and Backwards Compatibility - -### Context -Need to handle transition from external API to local database without service disruption. - -### Options Considered -1. **Big Bang Migration**: Switch all at once -2. **Gradual Migration**: Migrate endpoints one by one -3. **Blue-Green Deployment**: Parallel systems with traffic switch -4. **Feature Flags**: Toggle between old and new systems - -### Decision Made -**Chosen**: Big Bang Migration with Comprehensive Testing - -### Rationale -- **Simplicity**: Single transition point reduces complexity -- **Testing**: Comprehensive test suite validates entire system -- **Rollback**: Clear rollback path if issues discovered -- **MVP Scope**: Limited scope makes big bang migration feasible -- **Zero Downtime**: Migration can be done without service interruption - -### Implementation -- Complete testing in development environment -- Staging deployment for validation -- Production deployment during low-traffic window -- Immediate rollback capability if issues detected -- Monitoring and alerting for post-deployment validation - ---- - -## MVP Platform Architecture Principles - -Based on these decisions, the following principles guide MVP Platform development: - -### 1. Service Isolation -- Each platform service has its own database -- Independent deployment and scaling -- Clear service boundaries and responsibilities - -### 2. Standardized Naming -- All platform services use `mvp-platform-*` prefix -- Consistent naming across databases, containers, and cache keys -- Predictable patterns for future services - -### 3. Performance First -- Sub-100ms response times for all public endpoints -- Aggressive caching with appropriate TTLs -- Database optimization and connection pooling - -### 4. Zero Breaking Changes -- Existing API contracts never change -- Frontend requires no modifications -- Backward compatibility maintained across all changes - -### 5. Comprehensive Testing -- Automated test suites for all changes -- Performance validation requirements -- Error handling and edge case coverage - -### 6. Graceful Degradation -- Systems continue operating with reduced functionality -- Appropriate error responses and status codes -- Automatic recovery when services restore - -### 7. Observability Ready -- Health check endpoints for all services -- Comprehensive logging and monitoring -- Alerting for critical failures - -### 8. Future-Proof Architecture -- Designed for Kubernetes migration -- Microservices-ready patterns -- Extensible for additional platform services - ---- - -## Future Architecture Evolution - -### Next Platform Services -Following this pattern, future platform services will include: - -1. **mvp-platform-analytics**: User behavior tracking and analysis -2. **mvp-platform-notifications**: Email, SMS, and push notifications -3. **mvp-platform-payments**: Payment processing and billing -4. **mvp-platform-documents**: File storage and document management -5. **mvp-platform-search**: Full-text search and indexing - -### Kubernetes Migration Plan -When ready for production scaling: - -1. **Container Compatibility**: All containers designed for Kubernetes -2. **Configuration Management**: Environment-based configuration -3. **Service Discovery**: Native Kubernetes service discovery -4. **Persistent Storage**: Kubernetes persistent volumes -5. **Auto-scaling**: Horizontal pod autoscaling -6. **Ingress**: Kubernetes ingress controllers -7. **Monitoring**: Prometheus and Grafana integration - -### Microservices Evolution -Path to full microservices architecture: - -1. **Service Extraction**: Extract platform services to independent deployments -2. **API Gateway**: Implement centralized API gateway -3. **Service Mesh**: Add service mesh for advanced networking -4. **Event-Driven**: Implement event-driven communication patterns -5. **CQRS**: Command Query Responsibility Segregation for complex domains - ---- - -## Decision Review and Updates - -This document should be reviewed and updated: - -- **Before adding new platform services**: Ensure consistency with established patterns -- **During performance issues**: Review caching and database decisions -- **When scaling requirements change**: Evaluate deployment and infrastructure choices -- **After major technology updates**: Reassess technology choices with current best practices - -All architectural decisions should be validated against: -- Performance requirements and SLAs -- Security and compliance requirements -- Team capabilities and maintenance burden -- Cost and resource constraints -- Future scalability and extensibility needs - -**Document Last Updated**: [Current Date] -**Next Review Date**: [3 months from last update] \ No newline at end of file diff --git a/docs/changes/vehicles-dropdown-v1/implementation-checklist.md b/docs/changes/vehicles-dropdown-v1/implementation-checklist.md deleted file mode 100644 index aff39b0..0000000 --- a/docs/changes/vehicles-dropdown-v1/implementation-checklist.md +++ /dev/null @@ -1,634 +0,0 @@ -# Vehicle ETL Integration - Implementation Checklist - -## Overview - -This checklist provides step-by-step execution guidance for implementing the Vehicle ETL integration. Each item includes verification steps and dependencies to ensure successful completion. - -## Pre-Implementation Requirements - -- [ ] **Docker Environment Ready**: Docker and Docker Compose installed and functional -- [ ] **Main Application Running**: MotoVaultPro backend and frontend operational -- [ ] **NHTSA Database Backup**: VPICList backup file available in `vehicle-etl/volumes/mssql/backups/` -- [ ] **Network Ports Available**: Ports 5433 (MVP Platform DB), 1433 (MSSQL), available -- [ ] **Git Branch Created**: Feature branch created for implementation -- [ ] **Backup Taken**: Complete backup of current working state - ---- - -## Phase 1: Infrastructure Setup - -### āœ… Task 1.1: Add MVP Platform Database Service - -**Files**: `docker-compose.yml` - -- [ ] Add `mvp-platform-database` service definition -- [ ] Configure PostgreSQL 15-alpine image -- [ ] Set database name to `mvp-platform-vehicles` -- [ ] Configure user `mvp_platform_user` -- [ ] Set port mapping to `5433:5432` -- [ ] Add health check configuration -- [ ] Add volume `mvp_platform_data` - -**Verification**: -```bash -docker-compose config | grep -A 20 "mvp-platform-database" -``` - -### āœ… Task 1.2: Add MSSQL Source Database Service - -**Files**: `docker-compose.yml` - -- [ ] Add `mssql-source` service definition -- [ ] Configure MSSQL Server 2019 image -- [ ] Set SA password from environment variable -- [ ] Configure backup volume mount -- [ ] Add health check with 60s start period -- [ ] Add volume `mssql_source_data` - -**Verification**: -```bash -docker-compose config | grep -A 15 "mssql-source" -``` - -### āœ… Task 1.3: Add ETL Scheduler Service - -**Files**: `docker-compose.yml` - -- [ ] Add `etl-scheduler` service definition -- [ ] Configure build context to `./vehicle-etl` -- [ ] Set all required environment variables -- [ ] Add dependency on both databases with health checks -- [ ] Configure logs volume mount -- [ ] Add volume `etl_scheduler_data` - -**Verification**: -```bash -docker-compose config | grep -A 25 "etl-scheduler" -``` - -### āœ… Task 1.4: Update Backend Environment Variables - -**Files**: `docker-compose.yml` - -- [ ] Add `MVP_PLATFORM_DB_HOST` environment variable to backend -- [ ] Add `MVP_PLATFORM_DB_PORT` environment variable -- [ ] Add `MVP_PLATFORM_DB_NAME` environment variable -- [ ] Add `MVP_PLATFORM_DB_USER` environment variable -- [ ] Add `MVP_PLATFORM_DB_PASSWORD` environment variable -- [ ] Add dependency on `mvp-platform-database` - -**Verification**: -```bash -docker-compose config | grep -A 10 "MVP_PLATFORM_DB" -``` - -### āœ… Task 1.5: Update Environment Files - -**Files**: `.env.example`, `.env` - -- [ ] Add `MVP_PLATFORM_DB_PASSWORD` to .env.example -- [ ] Add `MSSQL_SOURCE_PASSWORD` to .env.example -- [ ] Add ETL configuration variables -- [ ] Update local `.env` file if it exists - -**Verification**: -```bash -grep "MVP_PLATFORM_DB_PASSWORD" .env.example -``` - -### āœ… Phase 1 Validation - -- [ ] **Docker Compose Valid**: `docker-compose config` succeeds -- [ ] **Services Start**: `docker-compose up mvp-platform-database mssql-source -d` succeeds -- [ ] **Health Checks Pass**: Both databases show healthy status -- [ ] **Database Connections**: Can connect to both databases -- [ ] **Logs Directory Created**: `./vehicle-etl/logs/` exists - -**Critical Check**: -```bash -docker-compose ps | grep -E "(mvp-platform-database|mssql-source)" | grep "healthy" -``` - ---- - -## Phase 2: Backend Migration - -### āœ… Task 2.1: Remove External vPIC Dependencies - -**Files**: `backend/src/features/vehicles/external/` (directory) - -- [ ] Delete entire `external/vpic/` directory -- [ ] Remove `VPIC_API_URL` from `environment.ts` -- [ ] Add MVP Platform DB configuration to `environment.ts` - -**Verification**: -```bash -ls backend/src/features/vehicles/external/ 2>/dev/null || echo "Directory removed āœ…" -grep "VPIC_API_URL" backend/src/core/config/environment.ts || echo "VPIC_API_URL removed āœ…" -``` - -### āœ… Task 2.2: Create MVP Platform Database Connection - -**Files**: `backend/src/core/config/database.ts` - -- [ ] Add `mvpPlatformPool` export -- [ ] Configure connection with MVP Platform DB parameters -- [ ] Set appropriate pool size (10 connections) -- [ ] Configure idle timeout - -**Verification**: -```bash -grep "mvpPlatformPool" backend/src/core/config/database.ts -``` - -### āœ… Task 2.3: Create MVP Platform Repository - -**Files**: `backend/src/features/vehicles/data/mvp-platform.repository.ts` - -- [ ] Create `MvpPlatformRepository` class -- [ ] Implement `decodeVIN()` method -- [ ] Implement `getMakes()` method -- [ ] Implement `getModelsForMake()` method -- [ ] Implement `getTransmissions()` method -- [ ] Implement `getEngines()` method -- [ ] Implement `getTrims()` method -- [ ] Export singleton instance - -**Verification**: -```bash -grep "export class MvpPlatformRepository" backend/src/features/vehicles/data/mvp-platform.repository.ts -``` - -### āœ… Task 2.4: Create VIN Decoder Service - -**Files**: `backend/src/features/vehicles/domain/vin-decoder.service.ts` - -- [ ] Create `VinDecoderService` class -- [ ] Implement VIN validation logic -- [ ] Implement cache-first decoding -- [ ] Implement model year extraction from VIN -- [ ] Add comprehensive error handling -- [ ] Export singleton instance - -**Verification**: -```bash -grep "export class VinDecoderService" backend/src/features/vehicles/domain/vin-decoder.service.ts -``` - -### āœ… Task 2.5: Update Vehicles Service - -**Files**: `backend/src/features/vehicles/domain/vehicles.service.ts` - -- [ ] Remove imports for `vpicClient` -- [ ] Add imports for `vinDecoderService` and `mvpPlatformRepository` -- [ ] Replace `vpicClient.decodeVIN()` with `vinDecoderService.decodeVIN()` -- [ ] Add `getDropdownMakes()` method -- [ ] Add `getDropdownModels()` method -- [ ] Add `getDropdownTransmissions()` method -- [ ] Add `getDropdownEngines()` method -- [ ] Add `getDropdownTrims()` method -- [ ] Update cache prefix to `mvp-platform:vehicles` - -**Verification**: -```bash -grep "vpicClient" backend/src/features/vehicles/domain/vehicles.service.ts || echo "vpicClient removed āœ…" -grep "mvp-platform:vehicles" backend/src/features/vehicles/domain/vehicles.service.ts -``` - -### āœ… Phase 2 Validation - -- [ ] **TypeScript Compiles**: `npm run build` succeeds in backend directory -- [ ] **No vPIC References**: `grep -r "vpic" backend/src/features/vehicles/` returns no results -- [ ] **Database Connection Test**: MVP Platform database accessible from backend -- [ ] **VIN Decoder Test**: VIN decoding service functional - -**Critical Check**: -```bash -cd backend && npm run build && echo "Backend compilation successful āœ…" -``` - ---- - -## Phase 3: API Migration - -### āœ… Task 3.1: Update Vehicles Controller - -**Files**: `backend/src/features/vehicles/api/vehicles.controller.ts` - -- [ ] Remove imports for `vpicClient` -- [ ] Add import for updated `VehiclesService` -- [ ] Update `getDropdownMakes()` method to use MVP Platform -- [ ] Update `getDropdownModels()` method -- [ ] Update `getDropdownTransmissions()` method -- [ ] Update `getDropdownEngines()` method -- [ ] Update `getDropdownTrims()` method -- [ ] Maintain exact response format compatibility -- [ ] Add performance monitoring -- [ ] Add database error handling - -**Verification**: -```bash -grep "vehiclesService.getDropdownMakes" backend/src/features/vehicles/api/vehicles.controller.ts -``` - -### āœ… Task 3.2: Verify Routes Configuration - -**Files**: `backend/src/features/vehicles/api/vehicles.routes.ts` - -- [ ] Confirm dropdown routes remain unauthenticated -- [ ] Verify no `preHandler: fastify.authenticate` on dropdown routes -- [ ] Ensure CRUD routes still require authentication - -**Verification**: -```bash -grep -A 3 "dropdown/makes" backend/src/features/vehicles/api/vehicles.routes.ts | grep "preHandler" || echo "No auth on dropdown routes āœ…" -``` - -### āœ… Task 3.3: Add Health Check Endpoint - -**Files**: `vehicles.controller.ts`, `vehicles.routes.ts` - -- [ ] Add `healthCheck()` method to controller -- [ ] Add `testMvpPlatformConnection()` method to service -- [ ] Add `/vehicles/health` route (unauthenticated) -- [ ] Test MVP Platform database connectivity - -**Verification**: -```bash -grep "healthCheck" backend/src/features/vehicles/api/vehicles.controller.ts -``` - -### āœ… Phase 3 Validation - -- [ ] **API Format Tests**: All dropdown endpoints return correct format -- [ ] **Authentication Tests**: Dropdown endpoints unauthenticated, CRUD authenticated -- [ ] **Performance Tests**: All endpoints respond < 100ms -- [ ] **Health Check**: `/api/vehicles/health` returns healthy status - -**Critical Check**: -```bash -curl -s http://localhost:3001/api/vehicles/dropdown/makes | jq '.[0]' | grep "Make_ID" -``` - ---- - -## Phase 4: Scheduled ETL Implementation - -### āœ… Task 4.1: Create ETL Dockerfile - -**Files**: `vehicle-etl/docker/Dockerfile.etl` - -- [ ] Base on Python 3.11-slim -- [ ] Install cron and system dependencies -- [ ] Install Python requirements -- [ ] Copy ETL source code -- [ ] Set up cron configuration -- [ ] Add health check -- [ ] Configure entrypoint - -**Verification**: -```bash -ls vehicle-etl/docker/Dockerfile.etl -``` - -### āœ… Task 4.2: Create Cron Setup Script - -**Files**: `vehicle-etl/docker/setup-cron.sh` - -- [ ] Create script with execute permissions -- [ ] Configure cron job from environment variable -- [ ] Set proper file permissions -- [ ] Apply cron job to system - -**Verification**: -```bash -ls -la vehicle-etl/docker/setup-cron.sh | grep "x" -``` - -### āœ… Task 4.3: Create Container Entrypoint - -**Files**: `vehicle-etl/docker/entrypoint.sh` - -- [ ] Start cron daemon in background -- [ ] Handle shutdown signals properly -- [ ] Support initial ETL run option -- [ ] Keep container running - -**Verification**: -```bash -grep "cron -f" vehicle-etl/docker/entrypoint.sh -``` - -### āœ… Task 4.4: Update ETL Main Module - -**Files**: `vehicle-etl/etl/main.py` - -- [ ] Support `build-catalog` command -- [ ] Test all connections before ETL -- [ ] Implement complete ETL pipeline -- [ ] Add comprehensive error handling -- [ ] Write completion markers - -**Verification**: -```bash -grep "build-catalog" vehicle-etl/etl/main.py -``` - -### āœ… Task 4.5: Create Connection Testing Module - -**Files**: `vehicle-etl/etl/connections.py` - -- [ ] Implement `test_mssql_connection()` -- [ ] Implement `test_postgres_connection()` -- [ ] Implement `test_redis_connection()` -- [ ] Implement `test_connections()` wrapper -- [ ] Add proper error logging - -**Verification**: -```bash -grep "def test_connections" vehicle-etl/etl/connections.py -``` - -### āœ… Task 4.6: Create ETL Monitoring Script - -**Files**: `vehicle-etl/scripts/check-etl-status.sh` - -- [ ] Check last run status file -- [ ] Report success/failure status -- [ ] Show recent log entries -- [ ] Return appropriate exit codes - -**Verification**: -```bash -ls -la vehicle-etl/scripts/check-etl-status.sh | grep "x" -``` - -### āœ… Task 4.7: Create Requirements File - -**Files**: `vehicle-etl/requirements-etl.txt` - -- [ ] Add database connectivity packages -- [ ] Add data processing packages -- [ ] Add logging and monitoring packages -- [ ] Add testing packages - -**Verification**: -```bash -grep "pyodbc" vehicle-etl/requirements-etl.txt -``` - -### āœ… Phase 4 Validation - -- [ ] **ETL Container Builds**: `docker-compose build etl-scheduler` succeeds -- [ ] **Connection Tests**: ETL can connect to all databases -- [ ] **Manual ETL Run**: ETL completes successfully -- [ ] **Cron Configuration**: Cron job properly configured -- [ ] **Health Checks**: ETL health monitoring functional - -**Critical Check**: -```bash -docker-compose exec etl-scheduler python -m etl.main test-connections -``` - ---- - -## Phase 5: Testing & Validation - -### āœ… Task 5.1: Run API Functionality Tests - -**Script**: `test-api-formats.sh` - -- [ ] Test dropdown API response formats -- [ ] Validate data counts and structure -- [ ] Verify error handling -- [ ] Check all endpoint availability - -**Verification**: All API format tests pass - -### āœ… Task 5.2: Run Authentication Tests - -**Script**: `test-authentication.sh` - -- [ ] Test dropdown endpoints are unauthenticated -- [ ] Test CRUD endpoints require authentication -- [ ] Verify security model unchanged - -**Verification**: All authentication tests pass - -### āœ… Task 5.3: Run Performance Tests - -**Script**: `test-performance.sh`, `test-cache-performance.sh` - -- [ ] Measure response times for all endpoints -- [ ] Verify < 100ms requirement met -- [ ] Test cache performance improvement -- [ ] Validate under load - -**Verification**: All performance tests pass - -### āœ… Task 5.4: Run Data Accuracy Tests - -**Script**: `test-vin-accuracy.sh`, `test-data-completeness.sh` - -- [ ] Test VIN decoding accuracy -- [ ] Verify data completeness -- [ ] Check data quality metrics -- [ ] Validate against known test cases - -**Verification**: All accuracy tests pass - -### āœ… Task 5.5: Run ETL Process Tests - -**Script**: `test-etl-execution.sh`, `test-etl-scheduling.sh` - -- [ ] Test ETL execution -- [ ] Verify scheduling configuration -- [ ] Check error handling -- [ ] Validate monitoring - -**Verification**: All ETL tests pass - -### āœ… Task 5.6: Run Error Handling Tests - -**Script**: `test-error-handling.sh` - -- [ ] Test database unavailability scenarios -- [ ] Verify graceful degradation -- [ ] Test recovery mechanisms -- [ ] Check error responses - -**Verification**: All error handling tests pass - -### āœ… Task 5.7: Run Load Tests - -**Script**: `test-load.sh` - -- [ ] Test concurrent request handling -- [ ] Measure performance under load -- [ ] Verify system stability -- [ ] Check resource usage - -**Verification**: All load tests pass - -### āœ… Task 5.8: Run Security Tests - -**Script**: `test-security.sh` - -- [ ] Test SQL injection prevention -- [ ] Verify input validation -- [ ] Check authentication bypasses -- [ ] Test parameter tampering - -**Verification**: All security tests pass - -### āœ… Phase 5 Validation - -- [ ] **Master Test Script**: `test-all.sh` passes completely -- [ ] **Zero Breaking Changes**: All existing functionality preserved -- [ ] **Performance Requirements**: < 100ms response times achieved -- [ ] **Data Accuracy**: 99.9%+ VIN decoding accuracy maintained -- [ ] **ETL Reliability**: Weekly ETL process functional - -**Critical Check**: -```bash -./test-all.sh && echo "ALL TESTS PASSED āœ…" -``` - ---- - -## Final Implementation Checklist - -### āœ… Pre-Production Validation - -- [ ] **All Phases Complete**: Phases 1-5 successfully implemented -- [ ] **All Tests Pass**: Master test script shows 100% pass rate -- [ ] **Documentation Updated**: All documentation reflects current state -- [ ] **Environment Variables**: All required environment variables configured -- [ ] **Backup Validated**: Can restore to pre-implementation state if needed - -### āœ… Production Readiness - -- [ ] **Monitoring Configured**: ETL success/failure alerting set up -- [ ] **Log Rotation**: Log file rotation configured for ETL processes -- [ ] **Database Maintenance**: MVP Platform database backup scheduled -- [ ] **Performance Baseline**: Response time baselines established -- [ ] **Error Alerting**: API error rate monitoring configured - -### āœ… Deployment - -- [ ] **Staging Deployment**: Changes deployed and tested in staging -- [ ] **Production Deployment**: Changes deployed to production -- [ ] **Post-Deployment Tests**: All tests pass in production -- [ ] **Performance Monitoring**: Response times within acceptable range -- [ ] **ETL Schedule Active**: First scheduled ETL run successful - -### āœ… Post-Deployment - -- [ ] **Documentation Complete**: All documentation updated and accurate -- [ ] **Team Handover**: Development team trained on new architecture -- [ ] **Monitoring Active**: All monitoring and alerting operational -- [ ] **Support Runbook**: Troubleshooting procedures documented -- [ ] **MVP Platform Foundation**: Architecture pattern ready for next services - ---- - -## Success Criteria Validation - -### āœ… **Zero Breaking Changes** -- [ ] All existing vehicle endpoints work identically -- [ ] Frontend requires no changes -- [ ] User experience unchanged -- [ ] API response formats preserved exactly - -### āœ… **Performance Requirements** -- [ ] Dropdown APIs consistently < 100ms -- [ ] VIN decoding < 200ms -- [ ] Cache hit rates > 90% -- [ ] No performance degradation under load - -### āœ… **Data Accuracy** -- [ ] VIN decoding accuracy ≄ 99.9% -- [ ] All makes/models/trims available -- [ ] Data completeness maintained -- [ ] No data quality regressions - -### āœ… **Reliability Requirements** -- [ ] Weekly ETL completes successfully -- [ ] Error handling and recovery functional -- [ ] Health checks operational -- [ ] Monitoring and alerting active - -### āœ… **MVP Platform Foundation** -- [ ] Standardized naming conventions established -- [ ] Service isolation pattern implemented -- [ ] Scheduled processing framework operational -- [ ] Ready for additional platform services - ---- - -## Emergency Rollback Plan - -If critical issues arise during implementation: - -### āœ… Immediate Rollback Steps - -1. **Stop New Services**: - ```bash - docker-compose stop mvp-platform-database mssql-source etl-scheduler - ``` - -2. **Restore Backend Code**: - ```bash - git checkout HEAD~1 -- backend/src/features/vehicles/ - git checkout HEAD~1 -- backend/src/core/config/ - ``` - -3. **Restore Docker Configuration**: - ```bash - git checkout HEAD~1 -- docker-compose.yml - git checkout HEAD~1 -- .env.example - ``` - -4. **Restart Application**: - ```bash - docker-compose restart backend - ``` - -5. **Validate Rollback**: - ```bash - curl -s http://localhost:3001/api/vehicles/dropdown/makes | jq '. | length' - ``` - -### āœ… Rollback Validation - -- [ ] **External API Working**: vPIC API endpoints functional -- [ ] **All Tests Pass**: Original functionality restored -- [ ] **No Data Loss**: No existing data affected -- [ ] **Performance Restored**: Response times back to baseline - ---- - -## Implementation Notes - -### Dependencies Between Phases -- **Phase 2** requires **Phase 1** infrastructure -- **Phase 3** requires **Phase 2** backend changes -- **Phase 4** requires **Phase 1** infrastructure -- **Phase 5** requires **Phases 1-4** complete - -### Critical Success Factors -1. **Database Connectivity**: All database connections must be stable -2. **Data Population**: MVP Platform database must have comprehensive data -3. **Performance Optimization**: Database queries must be optimized for speed -4. **Error Handling**: Graceful degradation when services unavailable -5. **Cache Strategy**: Proper caching for performance requirements - -### AI Assistant Guidance -This checklist is designed for efficient execution by AI assistants: -- Each task has clear file locations and verification steps -- Dependencies are explicitly stated -- Validation commands provided for each step -- Rollback procedures documented for safety -- Critical checks identified for each phase - -**For any implementation questions, refer to the detailed phase documentation in the same directory.** \ No newline at end of file diff --git a/docs/changes/vehicles-dropdown-v1/phase-01-infrastructure.md b/docs/changes/vehicles-dropdown-v1/phase-01-infrastructure.md deleted file mode 100644 index 3b15f43..0000000 --- a/docs/changes/vehicles-dropdown-v1/phase-01-infrastructure.md +++ /dev/null @@ -1,290 +0,0 @@ -# Phase 1: Infrastructure Setup - -## Overview - -This phase establishes the foundational infrastructure for the MVP Platform by adding three new Docker services to the main `docker-compose.yml`. This creates the shared services architecture pattern that future platform services will follow. - -## Prerequisites - -- Docker and Docker Compose installed -- Main MotoVaultPro application running successfully -- Access to NHTSA vPIC database backup file (VPICList_lite_2025_07.bak) -- Understanding of existing docker-compose.yml structure - -## Tasks - -### Task 1.1: Add MVP Platform Database Service - -**Location**: `docker-compose.yml` - -**Action**: Add the following service definition to the services section: - -```yaml - mvp-platform-database: - image: postgres:15-alpine - container_name: mvp-platform-db - environment: - POSTGRES_DB: mvp-platform-vehicles - POSTGRES_USER: mvp_platform_user - POSTGRES_PASSWORD: ${MVP_PLATFORM_DB_PASSWORD:-platform_dev_password} - POSTGRES_INITDB_ARGS: "--encoding=UTF8" - volumes: - - mvp_platform_data:/var/lib/postgresql/data - - ./vehicle-etl/sql/schema:/docker-entrypoint-initdb.d - ports: - - "5433:5432" - healthcheck: - test: ["CMD-SHELL", "pg_isready -U mvp_platform_user -p 5432"] - interval: 10s - timeout: 5s - retries: 5 - networks: - - default -``` - -**Action**: Add the volume definition to the volumes section: - -```yaml -volumes: - postgres_data: - redis_data: - minio_data: - mvp_platform_data: # Add this line -``` - -### Task 1.2: Add MSSQL Source Database Service - -**Location**: `docker-compose.yml` - -**Action**: Add the following service definition: - -```yaml - mssql-source: - image: mcr.microsoft.com/mssql/server:2019-latest - container_name: mvp-mssql-source - user: root - environment: - - ACCEPT_EULA=Y - - SA_PASSWORD=${MSSQL_SOURCE_PASSWORD:-Source123!} - - MSSQL_PID=Developer - ports: - - "1433:1433" - volumes: - - mssql_source_data:/var/opt/mssql/data - - ./vehicle-etl/volumes/mssql/backups:/backups - healthcheck: - test: ["CMD-SHELL", "/opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P ${MSSQL_SOURCE_PASSWORD:-Source123!} -Q 'SELECT 1'"] - interval: 30s - timeout: 10s - retries: 5 - start_period: 60s - networks: - - default -``` - -**Action**: Add volume to volumes section: - -```yaml -volumes: - postgres_data: - redis_data: - minio_data: - mvp_platform_data: - mssql_source_data: # Add this line -``` - -### Task 1.3: Add Scheduled ETL Service - -**Location**: `docker-compose.yml` - -**Action**: Add the following service definition: - -```yaml - etl-scheduler: - build: - context: ./vehicle-etl - dockerfile: docker/Dockerfile.etl - container_name: mvp-etl-scheduler - environment: - # Database connections - - MSSQL_HOST=mssql-source - - MSSQL_PORT=1433 - - MSSQL_DATABASE=VPICList - - MSSQL_USERNAME=sa - - MSSQL_PASSWORD=${MSSQL_SOURCE_PASSWORD:-Source123!} - - POSTGRES_HOST=mvp-platform-database - - POSTGRES_PORT=5432 - - POSTGRES_DATABASE=mvp-platform-vehicles - - POSTGRES_USERNAME=mvp_platform_user - - POSTGRES_PASSWORD=${MVP_PLATFORM_DB_PASSWORD:-platform_dev_password} - - REDIS_HOST=redis - - REDIS_PORT=6379 - # ETL configuration - - ETL_SCHEDULE=0 2 * * 0 # Weekly on Sunday at 2 AM - - ETL_LOG_LEVEL=INFO - - ETL_BATCH_SIZE=10000 - - ETL_MAX_RETRIES=3 - volumes: - - ./vehicle-etl/logs:/app/logs - - etl_scheduler_data:/app/data - depends_on: - mssql-source: - condition: service_healthy - mvp-platform-database: - condition: service_healthy - redis: - condition: service_healthy - restart: unless-stopped - networks: - - default -``` - -**Action**: Add volume to volumes section: - -```yaml -volumes: - postgres_data: - redis_data: - minio_data: - mvp_platform_data: - mssql_source_data: - etl_scheduler_data: # Add this line -``` - -### Task 1.4: Update Backend Service Environment Variables - -**Location**: `docker-compose.yml` - -**Action**: Add MVP Platform database environment variables to the backend service: - -```yaml - backend: - # ... existing configuration ... - environment: - # ... existing environment variables ... - # MVP Platform Database - MVP_PLATFORM_DB_HOST: mvp-platform-database - MVP_PLATFORM_DB_PORT: 5432 - MVP_PLATFORM_DB_NAME: mvp-platform-vehicles - MVP_PLATFORM_DB_USER: mvp_platform_user - MVP_PLATFORM_DB_PASSWORD: ${MVP_PLATFORM_DB_PASSWORD:-platform_dev_password} - depends_on: - - postgres - - redis - - minio - - mvp-platform-database # Add this dependency -``` - -### Task 1.5: Create Environment File Template - -**Location**: `.env.example` - -**Action**: Add the following environment variables: - -```env -# MVP Platform Database -MVP_PLATFORM_DB_PASSWORD=platform_secure_password - -# ETL Source Database -MSSQL_SOURCE_PASSWORD=Source123! - -# ETL Configuration -ETL_SCHEDULE=0 2 * * 0 -ETL_LOG_LEVEL=INFO -ETL_BATCH_SIZE=10000 -ETL_MAX_RETRIES=3 -``` - -### Task 1.6: Update .env File (if exists) - -**Location**: `.env` - -**Action**: If `.env` exists, add the above environment variables with appropriate values for your environment. - -## Validation Steps - -### Step 1: Verify Docker Compose Configuration - -```bash -# Test docker-compose configuration -docker-compose config - -# Should output valid YAML without errors -``` - -### Step 2: Build and Start New Services - -```bash -# Build the ETL scheduler container -docker-compose build etl-scheduler - -# Start only the new services for testing -docker-compose up mvp-platform-database mssql-source -d - -# Check service health -docker-compose ps -``` - -### Step 3: Test Database Connections - -```bash -# Test MVP Platform database connection -docker-compose exec mvp-platform-database psql -U mvp_platform_user -d mvp-platform-vehicles -c "SELECT version();" - -# Test MSSQL source database connection -docker-compose exec mssql-source /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P "Source123!" -Q "SELECT @@VERSION" -``` - -### Step 4: Verify Logs Directory Creation - -```bash -# Check that ETL logs directory is created -ls -la ./vehicle-etl/logs/ - -# Should exist and be writable -``` - -## Error Handling - -### Common Issues and Solutions - -**Issue**: PostgreSQL container fails to start -**Solution**: Check port 5433 is not in use, verify password complexity requirements - -**Issue**: MSSQL container fails health check -**Solution**: Increase start_period, verify password meets MSSQL requirements, check available memory - -**Issue**: ETL scheduler cannot connect to databases -**Solution**: Verify network connectivity, check environment variable values, ensure databases are healthy - -### Rollback Procedure - -1. Stop the new services: - ```bash - docker-compose stop mvp-platform-database mssql-source etl-scheduler - ``` - -2. Remove the new containers: - ```bash - docker-compose rm mvp-platform-database mssql-source etl-scheduler - ``` - -3. Remove the volume definitions from docker-compose.yml - -4. Remove the service definitions from docker-compose.yml - -5. Remove environment variables from backend service - -## Next Steps - -After successful completion of Phase 1: - -1. Proceed to [Phase 2: Backend Migration](./phase-02-backend-migration.md) -2. Ensure all services are running and healthy before starting backend changes -3. Take note of any performance impacts on the existing application - -## Dependencies for Next Phase - -- MVP Platform database must be accessible and initialized -- Backend service must be able to connect to MVP Platform database -- Existing Redis service must be available for new caching patterns \ No newline at end of file diff --git a/docs/changes/vehicles-dropdown-v1/phase-02-backend-migration.md b/docs/changes/vehicles-dropdown-v1/phase-02-backend-migration.md deleted file mode 100644 index 8e93954..0000000 --- a/docs/changes/vehicles-dropdown-v1/phase-02-backend-migration.md +++ /dev/null @@ -1,601 +0,0 @@ -# Phase 2: Backend Migration - -## Overview - -This phase removes external NHTSA vPIC API dependencies from the vehicles feature and integrates direct access to the MVP Platform database. All VIN decoding logic will be ported from Python to TypeScript while maintaining exact API compatibility. - -## Prerequisites - -- Phase 1 infrastructure completed successfully -- MVP Platform database running and accessible -- Existing Redis service available -- Backend service can connect to MVP Platform database -- Understanding of existing vehicles feature structure - -## Current Architecture Analysis - -**Files to Modify/Remove**: -- `backend/src/features/vehicles/external/vpic/` (entire directory - DELETE) -- `backend/src/features/vehicles/domain/vehicles.service.ts` (UPDATE) -- `backend/src/features/vehicles/api/vehicles.controller.ts` (UPDATE) -- `backend/src/core/config/environment.ts` (UPDATE) - -**New Files to Create**: -- `backend/src/features/vehicles/data/mvp-platform.repository.ts` -- `backend/src/features/vehicles/domain/vin-decoder.service.ts` -- `backend/src/features/vehicles/data/vehicle-catalog.repository.ts` - -## Tasks - -### Task 2.1: Remove External vPIC API Dependencies - -**Action**: Delete external API directory -```bash -rm -rf backend/src/features/vehicles/external/ -``` - -**Location**: `backend/src/core/config/environment.ts` - -**Action**: Remove VPIC_API_URL environment variable: - -```typescript -// REMOVE this line: -// VPIC_API_URL: process.env.VPIC_API_URL || 'https://vpic.nhtsa.dot.gov/api/vehicles', - -// ADD MVP Platform database configuration: -MVP_PLATFORM_DB_HOST: process.env.MVP_PLATFORM_DB_HOST || 'mvp-platform-database', -MVP_PLATFORM_DB_PORT: parseInt(process.env.MVP_PLATFORM_DB_PORT || '5432'), -MVP_PLATFORM_DB_NAME: process.env.MVP_PLATFORM_DB_NAME || 'mvp-platform-vehicles', -MVP_PLATFORM_DB_USER: process.env.MVP_PLATFORM_DB_USER || 'mvp_platform_user', -MVP_PLATFORM_DB_PASSWORD: process.env.MVP_PLATFORM_DB_PASSWORD || 'platform_dev_password', -``` - -### Task 2.2: Create MVP Platform Database Connection - -**Location**: `backend/src/core/config/database.ts` - -**Action**: Add MVP Platform database pool configuration: - -```typescript -import { Pool } from 'pg'; -import { env } from './environment'; - -// Existing main database pool -export const dbPool = new Pool({ - host: env.DB_HOST, - port: env.DB_PORT, - database: env.DB_NAME, - user: env.DB_USER, - password: env.DB_PASSWORD, - max: 20, - idleTimeoutMillis: 30000, -}); - -// NEW: MVP Platform database pool -export const mvpPlatformPool = new Pool({ - host: env.MVP_PLATFORM_DB_HOST, - port: env.MVP_PLATFORM_DB_PORT, - database: env.MVP_PLATFORM_DB_NAME, - user: env.MVP_PLATFORM_DB_USER, - password: env.MVP_PLATFORM_DB_PASSWORD, - max: 10, - idleTimeoutMillis: 30000, -}); -``` - -### Task 2.3: Create MVP Platform Repository - -**Location**: `backend/src/features/vehicles/data/mvp-platform.repository.ts` - -**Action**: Create new file with the following content: - -```typescript -import { mvpPlatformPool } from '../../../core/config/database'; -import { logger } from '../../../core/logging/logger'; - -export interface VehicleDecodeResult { - make?: string; - model?: string; - year?: number; - engineType?: string; - bodyType?: string; - trim?: string; - transmission?: string; -} - -export interface DropdownItem { - id: number; - name: string; -} - -export class MvpPlatformRepository { - - async decodeVIN(vin: string): Promise { - try { - const query = ` - SELECT - make_name as make, - model_name as model, - model_year as year, - engine_type, - body_type, - trim_name as trim, - transmission_type as transmission - FROM vehicle_catalog - WHERE vin_pattern_matches($1) - ORDER BY confidence_score DESC - LIMIT 1 - `; - - const result = await mvpPlatformPool.query(query, [vin]); - - if (result.rows.length === 0) { - logger.warn('VIN decode returned no results', { vin }); - return null; - } - - const row = result.rows[0]; - return { - make: row.make, - model: row.model, - year: row.year, - engineType: row.engine_type, - bodyType: row.body_type, - trim: row.trim, - transmission: row.transmission - }; - - } catch (error) { - logger.error('VIN decode failed', { vin, error }); - return null; - } - } - - async getMakes(): Promise { - try { - const query = ` - SELECT DISTINCT - make_id as id, - make_name as name - FROM vehicle_catalog - WHERE make_name IS NOT NULL - ORDER BY make_name - `; - - const result = await mvpPlatformPool.query(query); - return result.rows; - - } catch (error) { - logger.error('Get makes failed', { error }); - return []; - } - } - - async getModelsForMake(make: string): Promise { - try { - const query = ` - SELECT DISTINCT - model_id as id, - model_name as name - FROM vehicle_catalog - WHERE LOWER(make_name) = LOWER($1) - AND model_name IS NOT NULL - ORDER BY model_name - `; - - const result = await mvpPlatformPool.query(query, [make]); - return result.rows; - - } catch (error) { - logger.error('Get models failed', { make, error }); - return []; - } - } - - async getTransmissions(): Promise { - try { - const query = ` - SELECT DISTINCT - ROW_NUMBER() OVER (ORDER BY transmission_type) as id, - transmission_type as name - FROM vehicle_catalog - WHERE transmission_type IS NOT NULL - ORDER BY transmission_type - `; - - const result = await mvpPlatformPool.query(query); - return result.rows; - - } catch (error) { - logger.error('Get transmissions failed', { error }); - return []; - } - } - - async getEngines(): Promise { - try { - const query = ` - SELECT DISTINCT - ROW_NUMBER() OVER (ORDER BY engine_type) as id, - engine_type as name - FROM vehicle_catalog - WHERE engine_type IS NOT NULL - ORDER BY engine_type - `; - - const result = await mvpPlatformPool.query(query); - return result.rows; - - } catch (error) { - logger.error('Get engines failed', { error }); - return []; - } - } - - async getTrims(): Promise { - try { - const query = ` - SELECT DISTINCT - ROW_NUMBER() OVER (ORDER BY trim_name) as id, - trim_name as name - FROM vehicle_catalog - WHERE trim_name IS NOT NULL - ORDER BY trim_name - `; - - const result = await mvpPlatformPool.query(query); - return result.rows; - - } catch (error) { - logger.error('Get trims failed', { error }); - return []; - } - } -} - -export const mvpPlatformRepository = new MvpPlatformRepository(); -``` - -### Task 2.4: Create VIN Decoder Service - -**Location**: `backend/src/features/vehicles/domain/vin-decoder.service.ts` - -**Action**: Create new file with TypeScript port of VIN decoding logic: - -```typescript -import { logger } from '../../../core/logging/logger'; -import { cacheService } from '../../../core/config/redis'; -import { mvpPlatformRepository, VehicleDecodeResult } from '../data/mvp-platform.repository'; - -export class VinDecoderService { - private readonly cachePrefix = 'mvp-platform'; - private readonly vinCacheTTL = 30 * 24 * 60 * 60; // 30 days - - async decodeVIN(vin: string): Promise { - // Validate VIN format - if (!this.isValidVIN(vin)) { - logger.warn('Invalid VIN format', { vin }); - return null; - } - - // Check cache first - const cacheKey = `${this.cachePrefix}:vin:${vin}`; - const cached = await cacheService.get(cacheKey); - if (cached) { - logger.debug('VIN decode cache hit', { vin }); - return cached; - } - - // Decode VIN using MVP Platform database - logger.info('Decoding VIN via MVP Platform database', { vin }); - const result = await mvpPlatformRepository.decodeVIN(vin); - - // Cache successful results - if (result) { - await cacheService.set(cacheKey, result, this.vinCacheTTL); - } - - return result; - } - - private isValidVIN(vin: string): boolean { - // Basic VIN validation - if (!vin || vin.length !== 17) { - return false; - } - - // Check for invalid characters (I, O, Q not allowed) - const invalidChars = /[IOQ]/gi; - if (invalidChars.test(vin)) { - return false; - } - - return true; - } - - // Extract model year from VIN (positions 10 and 7) - extractModelYear(vin: string, currentYear: number = new Date().getFullYear()): number[] { - if (!this.isValidVIN(vin)) { - return []; - } - - const yearChar = vin.charAt(9); // Position 10 (0-indexed) - const seventhChar = vin.charAt(6); // Position 7 (0-indexed) - - // Year code mapping - const yearCodes: { [key: string]: number[] } = { - 'A': [2010, 1980], 'B': [2011, 1981], 'C': [2012, 1982], 'D': [2013, 1983], - 'E': [2014, 1984], 'F': [2015, 1985], 'G': [2016, 1986], 'H': [2017, 1987], - 'J': [2018, 1988], 'K': [2019, 1989], 'L': [2020, 1990], 'M': [2021, 1991], - 'N': [2022, 1992], 'P': [2023, 1993], 'R': [2024, 1994], 'S': [2025, 1995], - 'T': [2026, 1996], 'V': [2027, 1997], 'W': [2028, 1998], 'X': [2029, 1999], - 'Y': [2030, 2000], '1': [2031, 2001], '2': [2032, 2002], '3': [2033, 2003], - '4': [2034, 2004], '5': [2035, 2005], '6': [2036, 2006], '7': [2037, 2007], - '8': [2038, 2008], '9': [2039, 2009] - }; - - const possibleYears = yearCodes[yearChar.toUpperCase()]; - if (!possibleYears) { - return []; - } - - // Use 7th character for disambiguation if numeric (older cycle) - if (/\d/.test(seventhChar)) { - return [possibleYears[1]]; // Older year - } else { - return [possibleYears[0]]; // Newer year - } - } -} - -export const vinDecoderService = new VinDecoderService(); -``` - -### Task 2.5: Update Vehicles Service - -**Location**: `backend/src/features/vehicles/domain/vehicles.service.ts` - -**Action**: Replace external API calls with MVP Platform database calls: - -```typescript -// REMOVE these imports: -// import { vpicClient } from '../external/vpic/vpic.client'; - -// ADD these imports: -import { vinDecoderService } from './vin-decoder.service'; -import { mvpPlatformRepository } from '../data/mvp-platform.repository'; - -// In the createVehicle method, REPLACE: -// const vinData = await vpicClient.decodeVIN(data.vin); - -// WITH: -const vinData = await vinDecoderService.decodeVIN(data.vin); - -// Add new dropdown methods to the VehiclesService class: -async getDropdownMakes(): Promise { - const cacheKey = `${this.cachePrefix}:dropdown:makes`; - - try { - const cached = await cacheService.get(cacheKey); - if (cached) { - logger.debug('Makes dropdown cache hit'); - return cached; - } - - logger.info('Fetching makes from MVP Platform database'); - const makes = await mvpPlatformRepository.getMakes(); - - // Cache for 7 days - await cacheService.set(cacheKey, makes, 7 * 24 * 60 * 60); - return makes; - - } catch (error) { - logger.error('Get dropdown makes failed', { error }); - return []; - } -} - -async getDropdownModels(make: string): Promise { - const cacheKey = `${this.cachePrefix}:dropdown:models:${make}`; - - try { - const cached = await cacheService.get(cacheKey); - if (cached) { - logger.debug('Models dropdown cache hit', { make }); - return cached; - } - - logger.info('Fetching models from MVP Platform database', { make }); - const models = await mvpPlatformRepository.getModelsForMake(make); - - // Cache for 7 days - await cacheService.set(cacheKey, models, 7 * 24 * 60 * 60); - return models; - - } catch (error) { - logger.error('Get dropdown models failed', { make, error }); - return []; - } -} - -async getDropdownTransmissions(): Promise { - const cacheKey = `${this.cachePrefix}:dropdown:transmissions`; - - try { - const cached = await cacheService.get(cacheKey); - if (cached) { - logger.debug('Transmissions dropdown cache hit'); - return cached; - } - - logger.info('Fetching transmissions from MVP Platform database'); - const transmissions = await mvpPlatformRepository.getTransmissions(); - - // Cache for 7 days - await cacheService.set(cacheKey, transmissions, 7 * 24 * 60 * 60); - return transmissions; - - } catch (error) { - logger.error('Get dropdown transmissions failed', { error }); - return []; - } -} - -async getDropdownEngines(): Promise { - const cacheKey = `${this.cachePrefix}:dropdown:engines`; - - try { - const cached = await cacheService.get(cacheKey); - if (cached) { - logger.debug('Engines dropdown cache hit'); - return cached; - } - - logger.info('Fetching engines from MVP Platform database'); - const engines = await mvpPlatformRepository.getEngines(); - - // Cache for 7 days - await cacheService.set(cacheKey, engines, 7 * 24 * 60 * 60); - return engines; - - } catch (error) { - logger.error('Get dropdown engines failed', { error }); - return []; - } -} - -async getDropdownTrims(): Promise { - const cacheKey = `${this.cachePrefix}:dropdown:trims`; - - try { - const cached = await cacheService.get(cacheKey); - if (cached) { - logger.debug('Trims dropdown cache hit'); - return cached; - } - - logger.info('Fetching trims from MVP Platform database'); - const trims = await mvpPlatformRepository.getTrims(); - - // Cache for 7 days - await cacheService.set(cacheKey, trims, 7 * 24 * 60 * 60); - return trims; - - } catch (error) { - logger.error('Get dropdown trims failed', { error }); - return []; - } -} -``` - -### Task 2.6: Update Cache Key Patterns - -**Action**: Update all existing cache keys to use MVP Platform prefix - -In vehicles.service.ts, UPDATE: -```typescript -// CHANGE: -private readonly cachePrefix = 'vehicles'; - -// TO: -private readonly cachePrefix = 'mvp-platform:vehicles'; -``` - -## Validation Steps - -### Step 1: Compile TypeScript - -```bash -# From backend directory -cd backend -npm run build - -# Should compile without errors -``` - -### Step 2: Test Database Connections - -```bash -# Test MVP Platform database connection -docker-compose exec backend node -e " -const { mvpPlatformPool } = require('./dist/core/config/database'); -mvpPlatformPool.query('SELECT 1 as test') - .then(r => console.log('MVP Platform DB:', r.rows[0])) - .catch(e => console.error('Error:', e)); -" -``` - -### Step 3: Test VIN Decoder Service - -```bash -# Test VIN decoding functionality -docker-compose exec backend node -e " -const { vinDecoderService } = require('./dist/features/vehicles/domain/vin-decoder.service'); -vinDecoderService.decodeVIN('1HGBH41JXMN109186') - .then(r => console.log('VIN decode result:', r)) - .catch(e => console.error('Error:', e)); -" -``` - -### Step 4: Verify Import Statements - -Check that all imports are resolved correctly: - -```bash -# Check for any remaining vpic imports -grep -r "vpic" backend/src/features/vehicles/ || echo "No vpic references found" - -# Check for MVP Platform imports -grep -r "mvp-platform" backend/src/features/vehicles/ | head -5 -``` - -## Error Handling - -### Common Issues and Solutions - -**Issue**: TypeScript compilation errors -**Solution**: Check import paths, verify all referenced modules exist - -**Issue**: Database connection failures -**Solution**: Verify MVP Platform database is running, check connection parameters - -**Issue**: Missing external directory references -**Solution**: Update any remaining imports from deleted external/vpic directory - -### Rollback Procedure - -1. Restore external/vpic directory from git: - ```bash - git checkout HEAD -- backend/src/features/vehicles/external/ - ``` - -2. Revert vehicles.service.ts changes: - ```bash - git checkout HEAD -- backend/src/features/vehicles/domain/vehicles.service.ts - ``` - -3. Remove new files: - ```bash - rm backend/src/features/vehicles/data/mvp-platform.repository.ts - rm backend/src/features/vehicles/domain/vin-decoder.service.ts - ``` - -4. Revert environment.ts changes: - ```bash - git checkout HEAD -- backend/src/core/config/environment.ts - ``` - -## Next Steps - -After successful completion of Phase 2: - -1. Proceed to [Phase 3: API Migration](./phase-03-api-migration.md) -2. Test VIN decoding functionality thoroughly -3. Monitor performance of new database queries - -## Dependencies for Next Phase - -- All backend changes compiled successfully -- MVP Platform database queries working correctly -- VIN decoder service functional -- Cache keys updated to new pattern \ No newline at end of file diff --git a/docs/changes/vehicles-dropdown-v1/phase-03-api-migration.md b/docs/changes/vehicles-dropdown-v1/phase-03-api-migration.md deleted file mode 100644 index 12615f4..0000000 --- a/docs/changes/vehicles-dropdown-v1/phase-03-api-migration.md +++ /dev/null @@ -1,426 +0,0 @@ -# Phase 3: API Migration - -## Overview - -This phase updates the vehicles API controller to use the new MVP Platform database for all dropdown endpoints while maintaining exact API compatibility. All existing response formats and authentication patterns are preserved. - -## Prerequisites - -- Phase 2 backend migration completed successfully -- VIN decoder service functional -- MVP Platform repository working correctly -- Backend service can query MVP Platform database -- All TypeScript compilation successful - -## Current API Endpoints to Update - -**Existing endpoints that will be updated**: -- `GET /api/vehicles/dropdown/makes` (unauthenticated) -- `GET /api/vehicles/dropdown/models/:make` (unauthenticated) -- `GET /api/vehicles/dropdown/transmissions` (unauthenticated) -- `GET /api/vehicles/dropdown/engines` (unauthenticated) -- `GET /api/vehicles/dropdown/trims` (unauthenticated) - -**Existing endpoints that remain unchanged**: -- `POST /api/vehicles` (authenticated - uses VIN decoder) -- `GET /api/vehicles` (authenticated) -- `GET /api/vehicles/:id` (authenticated) -- `PUT /api/vehicles/:id` (authenticated) -- `DELETE /api/vehicles/:id` (authenticated) - -## Tasks - -### Task 3.1: Update Vehicles Controller - -**Location**: `backend/src/features/vehicles/api/vehicles.controller.ts` - -**Action**: Replace external API dropdown methods with MVP Platform database calls: - -```typescript -// UPDATE imports - REMOVE: -// import { vpicClient } from '../external/vpic/vpic.client'; - -// ADD new imports: -import { VehiclesService } from '../domain/vehicles.service'; - -export class VehiclesController { - private vehiclesService: VehiclesService; - - constructor() { - this.vehiclesService = new VehiclesService(); - } - - // UPDATE existing dropdown methods: - - async getDropdownMakes(request: FastifyRequest, reply: FastifyReply) { - try { - logger.info('Getting dropdown makes from MVP Platform'); - const makes = await this.vehiclesService.getDropdownMakes(); - - // Maintain exact same response format - const response = makes.map(make => ({ - Make_ID: make.id, - Make_Name: make.name - })); - - reply.status(200).send(response); - } catch (error) { - logger.error('Get dropdown makes failed', { error }); - reply.status(500).send({ error: 'Failed to retrieve makes' }); - } - } - - async getDropdownModels(request: FastifyRequest<{ Params: { make: string } }>, reply: FastifyReply) { - try { - const { make } = request.params; - logger.info('Getting dropdown models from MVP Platform', { make }); - - const models = await this.vehiclesService.getDropdownModels(make); - - // Maintain exact same response format - const response = models.map(model => ({ - Model_ID: model.id, - Model_Name: model.name - })); - - reply.status(200).send(response); - } catch (error) { - logger.error('Get dropdown models failed', { error }); - reply.status(500).send({ error: 'Failed to retrieve models' }); - } - } - - async getDropdownTransmissions(request: FastifyRequest, reply: FastifyReply) { - try { - logger.info('Getting dropdown transmissions from MVP Platform'); - const transmissions = await this.vehiclesService.getDropdownTransmissions(); - - // Maintain exact same response format - const response = transmissions.map(transmission => ({ - Name: transmission.name - })); - - reply.status(200).send(response); - } catch (error) { - logger.error('Get dropdown transmissions failed', { error }); - reply.status(500).send({ error: 'Failed to retrieve transmissions' }); - } - } - - async getDropdownEngines(request: FastifyRequest, reply: FastifyReply) { - try { - logger.info('Getting dropdown engines from MVP Platform'); - const engines = await this.vehiclesService.getDropdownEngines(); - - // Maintain exact same response format - const response = engines.map(engine => ({ - Name: engine.name - })); - - reply.status(200).send(response); - } catch (error) { - logger.error('Get dropdown engines failed', { error }); - reply.status(500).send({ error: 'Failed to retrieve engines' }); - } - } - - async getDropdownTrims(request: FastifyRequest, reply: FastifyReply) { - try { - logger.info('Getting dropdown trims from MVP Platform'); - const trims = await this.vehiclesService.getDropdownTrims(); - - // Maintain exact same response format - const response = trims.map(trim => ({ - Name: trim.name - })); - - reply.status(200).send(response); - } catch (error) { - logger.error('Get dropdown trims failed', { error }); - reply.status(500).send({ error: 'Failed to retrieve trims' }); - } - } - - // All other methods remain unchanged (createVehicle, getUserVehicles, etc.) -} -``` - -### Task 3.2: Verify Routes Configuration - -**Location**: `backend/src/features/vehicles/api/vehicles.routes.ts` - -**Action**: Ensure dropdown routes remain unauthenticated (no changes needed, just verification): - -```typescript -// VERIFY these routes remain unauthenticated: -fastify.get('/vehicles/dropdown/makes', { - handler: vehiclesController.getDropdownMakes.bind(vehiclesController) -}); - -fastify.get<{ Params: { make: string } }>('/vehicles/dropdown/models/:make', { - handler: vehiclesController.getDropdownModels.bind(vehiclesController) -}); - -fastify.get('/vehicles/dropdown/transmissions', { - handler: vehiclesController.getDropdownTransmissions.bind(vehiclesController) -}); - -fastify.get('/vehicles/dropdown/engines', { - handler: vehiclesController.getDropdownEngines.bind(vehiclesController) -}); - -fastify.get('/vehicles/dropdown/trims', { - handler: vehiclesController.getDropdownTrims.bind(vehiclesController) -}); -``` - -**Note**: These routes should NOT have `preHandler: fastify.authenticate` to maintain unauthenticated access as required by security.md. - -### Task 3.3: Update Response Error Handling - -**Action**: Add specific error handling for database connectivity issues: - -```typescript -// Add to VehiclesController class: - -private handleDatabaseError(error: any, operation: string, reply: FastifyReply) { - logger.error(`${operation} database error`, { error }); - - // Check for specific database connection errors - if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') { - reply.status(503).send({ - error: 'Service temporarily unavailable', - message: 'Database connection issue' - }); - return; - } - - // Generic database error - if (error.code && error.code.startsWith('P')) { // PostgreSQL error codes - reply.status(500).send({ - error: 'Database query failed', - message: 'Please try again later' - }); - return; - } - - // Generic error - reply.status(500).send({ - error: `Failed to ${operation}`, - message: 'Internal server error' - }); -} - -// Update all dropdown methods to use this error handler: -// Replace each catch block with: -} catch (error) { - this.handleDatabaseError(error, 'retrieve makes', reply); -} -``` - -### Task 3.4: Add Performance Monitoring - -**Action**: Add response time logging for performance monitoring: - -```typescript -// Add to VehiclesController class: - -private async measurePerformance( - operation: string, - fn: () => Promise -): Promise { - const startTime = Date.now(); - try { - const result = await fn(); - const duration = Date.now() - startTime; - logger.info(`MVP Platform ${operation} completed`, { duration }); - return result; - } catch (error) { - const duration = Date.now() - startTime; - logger.error(`MVP Platform ${operation} failed`, { duration, error }); - throw error; - } -} - -// Update dropdown methods to use performance monitoring: -async getDropdownMakes(request: FastifyRequest, reply: FastifyReply) { - try { - logger.info('Getting dropdown makes from MVP Platform'); - const makes = await this.measurePerformance('makes query', () => - this.vehiclesService.getDropdownMakes() - ); - - // ... rest of method unchanged - } catch (error) { - this.handleDatabaseError(error, 'retrieve makes', reply); - } -} -``` - -### Task 3.5: Update Health Check - -**Location**: `backend/src/features/vehicles/api/vehicles.controller.ts` - -**Action**: Add MVP Platform database health check method: - -```typescript -// Add new health check method: -async healthCheck(request: FastifyRequest, reply: FastifyReply) { - try { - // Test MVP Platform database connection - await this.measurePerformance('health check', async () => { - const testResult = await this.vehiclesService.testMvpPlatformConnection(); - if (!testResult) { - throw new Error('MVP Platform database connection failed'); - } - }); - - reply.status(200).send({ - status: 'healthy', - mvpPlatform: 'connected', - timestamp: new Date().toISOString() - }); - } catch (error) { - logger.error('Health check failed', { error }); - reply.status(503).send({ - status: 'unhealthy', - error: error.message, - timestamp: new Date().toISOString() - }); - } -} -``` - -**Location**: `backend/src/features/vehicles/domain/vehicles.service.ts` - -**Action**: Add health check method to service: - -```typescript -// Add to VehiclesService class: -async testMvpPlatformConnection(): Promise { - try { - await mvpPlatformRepository.getMakes(); - return true; - } catch (error) { - logger.error('MVP Platform connection test failed', { error }); - return false; - } -} -``` - -### Task 3.6: Update Route Registration for Health Check - -**Location**: `backend/src/features/vehicles/api/vehicles.routes.ts` - -**Action**: Add health check route: - -```typescript -// Add health check route (unauthenticated for monitoring): -fastify.get('/vehicles/health', { - handler: vehiclesController.healthCheck.bind(vehiclesController) -}); -``` - -## Validation Steps - -### Step 1: Test API Response Formats - -```bash -# Test makes endpoint -curl -s http://localhost:3001/api/vehicles/dropdown/makes | jq '.[0]' -# Should return: {"Make_ID": number, "Make_Name": "string"} - -# Test models endpoint -curl -s "http://localhost:3001/api/vehicles/dropdown/models/Honda" | jq '.[0]' -# Should return: {"Model_ID": number, "Model_Name": "string"} - -# Test transmissions endpoint -curl -s http://localhost:3001/api/vehicles/dropdown/transmissions | jq '.[0]' -# Should return: {"Name": "string"} -``` - -### Step 2: Test Performance - -```bash -# Test response times (should be < 100ms) -time curl -s http://localhost:3001/api/vehicles/dropdown/makes > /dev/null - -# Load test with multiple concurrent requests -for i in {1..10}; do - curl -s http://localhost:3001/api/vehicles/dropdown/makes > /dev/null & -done -wait -``` - -### Step 3: Test Error Handling - -```bash -# Test with invalid make name -curl -s "http://localhost:3001/api/vehicles/dropdown/models/InvalidMake" | jq '.' -# Should return empty array or appropriate error - -# Test health check -curl -s http://localhost:3001/api/vehicles/health | jq '.' -# Should return: {"status": "healthy", "mvpPlatform": "connected", "timestamp": "..."} -``` - -### Step 4: Verify Authentication Patterns - -```bash -# Test that dropdown endpoints are unauthenticated (should work without token) -curl -s http://localhost:3001/api/vehicles/dropdown/makes | jq '. | length' -# Should return number > 0 - -# Test that vehicle CRUD endpoints still require authentication -curl -s http://localhost:3001/api/vehicles -# Should return 401 Unauthorized -``` - -## Error Handling - -### Common Issues and Solutions - -**Issue**: Empty response arrays -**Solution**: Check MVP Platform database has data, verify SQL queries, check table names - -**Issue**: Slow response times (> 100ms) -**Solution**: Add database indexes, optimize queries, check connection pool settings - -**Issue**: Authentication errors on dropdown endpoints -**Solution**: Verify routes don't have authentication middleware, check security.md compliance - -**Issue**: Wrong response format -**Solution**: Compare with original vPIC API responses, adjust mapping in controller - -### Rollback Procedure - -1. Revert vehicles.controller.ts: - ```bash - git checkout HEAD -- backend/src/features/vehicles/api/vehicles.controller.ts - ``` - -2. Revert vehicles.routes.ts if modified: - ```bash - git checkout HEAD -- backend/src/features/vehicles/api/vehicles.routes.ts - ``` - -3. Restart backend service: - ```bash - docker-compose restart backend - ``` - -## Next Steps - -After successful completion of Phase 3: - -1. Proceed to [Phase 4: Scheduled ETL](./phase-04-scheduled-etl.md) -2. Monitor API response times in production -3. Set up alerts for health check failures - -## Dependencies for Next Phase - -- All dropdown APIs returning correct data -- Response times consistently under 100ms -- Health check endpoint functional -- No authentication issues with dropdown endpoints -- Error handling working properly \ No newline at end of file diff --git a/docs/changes/vehicles-dropdown-v1/phase-04-scheduled-etl.md b/docs/changes/vehicles-dropdown-v1/phase-04-scheduled-etl.md deleted file mode 100644 index 2cd70a3..0000000 --- a/docs/changes/vehicles-dropdown-v1/phase-04-scheduled-etl.md +++ /dev/null @@ -1,596 +0,0 @@ -# Phase 4: Scheduled ETL Implementation - -## Overview - -This phase implements automated weekly ETL processing using a cron-based scheduler within the existing ETL container. The ETL process extracts data from the MSSQL source database, transforms it for optimal query performance, and loads it into the MVP Platform database. - -## Prerequisites - -- Phase 3 API migration completed successfully -- ETL scheduler container built and functional -- MSSQL source database with NHTSA data restored -- MVP Platform database accessible -- ETL Python code functional in vehicle-etl directory - -## Scheduled ETL Architecture - -**Container**: `etl-scheduler` (already defined in Phase 1) -**Schedule**: Weekly on Sunday at 2 AM (configurable) -**Runtime**: Python 3.11 with cron daemon -**Dependencies**: Both MSSQL and MVP Platform databases must be healthy - -## Tasks - -### Task 4.1: Create ETL Scheduler Dockerfile - -**Location**: `vehicle-etl/docker/Dockerfile.etl` - -**Action**: Create Dockerfile with cron daemon and ETL dependencies: - -```dockerfile -FROM python:3.11-slim - -# Install system dependencies including cron -RUN apt-get update && apt-get install -y \ - cron \ - procps \ - curl \ - && rm -rf /var/lib/apt/lists/* - -# Create app directory -WORKDIR /app - -# Copy requirements and install Python dependencies -COPY requirements-etl.txt . -RUN pip install --no-cache-dir -r requirements-etl.txt - -# Copy ETL source code -COPY etl/ ./etl/ -COPY sql/ ./sql/ -COPY scripts/ ./scripts/ - -# Create logs directory -RUN mkdir -p /app/logs - -# Copy cron configuration script -COPY docker/setup-cron.sh /setup-cron.sh -RUN chmod +x /setup-cron.sh - -# Copy entrypoint script -COPY docker/entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - -# Set up cron job -RUN /setup-cron.sh - -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ - CMD python -c "import sys; from etl.connections import test_connections; sys.exit(0 if test_connections() else 1)" - -ENTRYPOINT ["/entrypoint.sh"] -``` - -### Task 4.2: Create Cron Setup Script - -**Location**: `vehicle-etl/docker/setup-cron.sh` - -**Action**: Create script to configure cron job: - -```bash -#!/bin/bash - -# Create cron job from environment variable or default -ETL_SCHEDULE=${ETL_SCHEDULE:-"0 2 * * 0"} - -# Create cron job that runs the ETL process -echo "$ETL_SCHEDULE cd /app && python -m etl.main build-catalog >> /app/logs/etl-cron.log 2>&1" > /etc/cron.d/etl-job - -# Set permissions -chmod 0644 /etc/cron.d/etl-job - -# Apply cron job -crontab /etc/cron.d/etl-job - -echo "ETL cron job configured with schedule: $ETL_SCHEDULE" -``` - -### Task 4.3: Create Container Entrypoint - -**Location**: `vehicle-etl/docker/entrypoint.sh` - -**Action**: Create entrypoint script that starts cron daemon: - -```bash -#!/bin/bash -set -e - -# Start cron daemon in the background -cron -f & -CRON_PID=$! - -# Function to handle shutdown -shutdown() { - echo "Shutting down ETL scheduler..." - kill $CRON_PID - exit 0 -} - -# Trap SIGTERM and SIGINT -trap shutdown SIGTERM SIGINT - -# Run initial ETL if requested -if [ "$RUN_INITIAL_ETL" = "true" ]; then - echo "Running initial ETL process..." - cd /app && python -m etl.main build-catalog -fi - -# Log startup -echo "ETL scheduler started with schedule: ${ETL_SCHEDULE:-0 2 * * 0}" -echo "Cron daemon PID: $CRON_PID" - -# Keep container running -wait $CRON_PID -``` - -### Task 4.4: Update ETL Main Module - -**Location**: `vehicle-etl/etl/main.py` - -**Action**: Ensure ETL main module supports build-catalog command: - -```python -#!/usr/bin/env python3 -""" -ETL Main Module - Vehicle Catalog Builder -""" - -import sys -import argparse -import logging -from datetime import datetime -import traceback - -from etl.utils.logging import setup_logging -from etl.builders.vehicle_catalog_builder import VehicleCatalogBuilder -from etl.connections import test_connections - -def build_catalog(): - """Run the complete ETL pipeline to build vehicle catalog""" - try: - setup_logging() - logger = logging.getLogger(__name__) - - start_time = datetime.now() - logger.info(f"Starting ETL pipeline at {start_time}") - - # Test all connections first - if not test_connections(): - logger.error("Connection tests failed - aborting ETL") - return False - - # Initialize catalog builder - builder = VehicleCatalogBuilder() - - # Run ETL pipeline steps - logger.info("Step 1: Extracting data from MSSQL source...") - extract_success = builder.extract_source_data() - if not extract_success: - logger.error("Data extraction failed") - return False - - logger.info("Step 2: Transforming data for catalog...") - transform_success = builder.transform_catalog_data() - if not transform_success: - logger.error("Data transformation failed") - return False - - logger.info("Step 3: Loading data to MVP Platform database...") - load_success = builder.load_catalog_data() - if not load_success: - logger.error("Data loading failed") - return False - - # Generate completion report - end_time = datetime.now() - duration = end_time - start_time - logger.info(f"ETL pipeline completed successfully in {duration}") - - # Write completion marker - with open('/app/logs/etl-last-run.txt', 'w') as f: - f.write(f"{end_time.isoformat()}\n") - f.write(f"Duration: {duration}\n") - f.write("Status: SUCCESS\n") - - return True - - except Exception as e: - logger.error(f"ETL pipeline failed: {str(e)}") - logger.error(traceback.format_exc()) - - # Write error marker - with open('/app/logs/etl-last-run.txt', 'w') as f: - f.write(f"{datetime.now().isoformat()}\n") - f.write(f"Status: FAILED\n") - f.write(f"Error: {str(e)}\n") - - return False - -def main(): - """Main entry point""" - parser = argparse.ArgumentParser(description='Vehicle ETL Pipeline') - parser.add_argument('command', choices=['build-catalog', 'test-connections', 'validate'], - help='Command to execute') - parser.add_argument('--log-level', default='INFO', - choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'], - help='Logging level') - - args = parser.parse_args() - - # Setup logging - logging.basicConfig( - level=getattr(logging, args.log_level), - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' - ) - - if args.command == 'build-catalog': - success = build_catalog() - sys.exit(0 if success else 1) - - elif args.command == 'test-connections': - success = test_connections() - print("All connections successful" if success else "Connection tests failed") - sys.exit(0 if success else 1) - - elif args.command == 'validate': - # Add validation logic here - print("Validation not yet implemented") - sys.exit(1) - -if __name__ == '__main__': - main() -``` - -### Task 4.5: Create Connection Testing Module - -**Location**: `vehicle-etl/etl/connections.py` - -**Action**: Create connection testing utilities: - -```python -""" -Database connection testing utilities -""" - -import os -import logging -import pyodbc -import psycopg2 -import redis - -logger = logging.getLogger(__name__) - -def test_mssql_connection(): - """Test MSSQL source database connection""" - try: - connection_string = ( - f"DRIVER={{ODBC Driver 17 for SQL Server}};" - f"SERVER={os.getenv('MSSQL_HOST', 'localhost')};" - f"DATABASE={os.getenv('MSSQL_DATABASE', 'VPICList')};" - f"UID={os.getenv('MSSQL_USERNAME', 'sa')};" - f"PWD={os.getenv('MSSQL_PASSWORD')};" - f"TrustServerCertificate=yes;" - ) - - conn = pyodbc.connect(connection_string) - cursor = conn.cursor() - cursor.execute("SELECT @@VERSION") - version = cursor.fetchone() - logger.info(f"MSSQL connection successful: {version[0][:50]}...") - cursor.close() - conn.close() - return True - - except Exception as e: - logger.error(f"MSSQL connection failed: {str(e)}") - return False - -def test_postgres_connection(): - """Test PostgreSQL MVP Platform database connection""" - try: - conn = psycopg2.connect( - host=os.getenv('POSTGRES_HOST', 'localhost'), - port=int(os.getenv('POSTGRES_PORT', '5432')), - database=os.getenv('POSTGRES_DATABASE', 'mvp-platform-vehicles'), - user=os.getenv('POSTGRES_USERNAME', 'mvp_platform_user'), - password=os.getenv('POSTGRES_PASSWORD') - ) - - cursor = conn.cursor() - cursor.execute("SELECT version()") - version = cursor.fetchone() - logger.info(f"PostgreSQL connection successful: {version[0][:50]}...") - cursor.close() - conn.close() - return True - - except Exception as e: - logger.error(f"PostgreSQL connection failed: {str(e)}") - return False - -def test_redis_connection(): - """Test Redis cache connection""" - try: - r = redis.Redis( - host=os.getenv('REDIS_HOST', 'localhost'), - port=int(os.getenv('REDIS_PORT', '6379')), - decode_responses=True - ) - - r.ping() - logger.info("Redis connection successful") - return True - - except Exception as e: - logger.error(f"Redis connection failed: {str(e)}") - return False - -def test_connections(): - """Test all database connections""" - logger.info("Testing all database connections...") - - mssql_ok = test_mssql_connection() - postgres_ok = test_postgres_connection() - redis_ok = test_redis_connection() - - all_ok = mssql_ok and postgres_ok and redis_ok - - if all_ok: - logger.info("All database connections successful") - else: - logger.error("One or more database connections failed") - - return all_ok -``` - -### Task 4.6: Create ETL Monitoring Script - -**Location**: `vehicle-etl/scripts/check-etl-status.sh` - -**Action**: Create monitoring script for ETL health: - -```bash -#!/bin/bash - -# ETL Status Monitoring Script - -LOG_FILE="/app/logs/etl-last-run.txt" -CRON_LOG="/app/logs/etl-cron.log" - -echo "=== ETL Status Check ===" -echo "Timestamp: $(date)" -echo - -# Check if last run file exists -if [ ! -f "$LOG_FILE" ]; then - echo "āŒ No ETL run detected yet" - exit 1 -fi - -# Read last run information -echo "šŸ“„ Last ETL Run Information:" -cat "$LOG_FILE" -echo - -# Check if last run was successful -if grep -q "Status: SUCCESS" "$LOG_FILE"; then - echo "āœ… Last ETL run was successful" - EXIT_CODE=0 -else - echo "āŒ Last ETL run failed" - EXIT_CODE=1 -fi - -# Show last few lines of cron log -echo -echo "šŸ“‹ Recent ETL Log (last 10 lines):" -if [ -f "$CRON_LOG" ]; then - tail -10 "$CRON_LOG" -else - echo "No cron log found" -fi - -echo -echo "=== End Status Check ===" - -exit $EXIT_CODE -``` - -### Task 4.7: Update Docker Compose Health Checks - -**Location**: `docker-compose.yml` (update existing etl-scheduler service) - -**Action**: Update the ETL scheduler service definition with proper health checks: - -```yaml - etl-scheduler: - build: - context: ./vehicle-etl - dockerfile: docker/Dockerfile.etl - container_name: mvp-etl-scheduler - environment: - # ... existing environment variables ... - # Health check configuration - - HEALTH_CHECK_ENABLED=true - volumes: - - ./vehicle-etl/logs:/app/logs - - etl_scheduler_data:/app/data - depends_on: - mssql-source: - condition: service_healthy - mvp-platform-database: - condition: service_healthy - redis: - condition: service_healthy - restart: unless-stopped - healthcheck: - test: ["CMD", "/app/scripts/check-etl-status.sh"] - interval: 60s - timeout: 30s - retries: 3 - start_period: 120s -``` - -### Task 4.8: Create ETL Requirements File - -**Location**: `vehicle-etl/requirements-etl.txt` - -**Action**: Ensure all required Python packages are listed: - -```txt -# Database connectivity -pyodbc>=4.0.35 -psycopg2-binary>=2.9.5 -redis>=4.5.1 - -# Data processing -pandas>=1.5.3 -numpy>=1.24.2 - -# Utilities -python-dateutil>=2.8.2 -tqdm>=4.64.1 - -# Logging and monitoring -structlog>=22.3.0 - -# Configuration -python-decouple>=3.6 - -# Testing (for validation) -pytest>=7.2.1 -pytest-asyncio>=0.20.3 -``` - -## Validation Steps - -### Step 1: Build and Test ETL Container - -```bash -# Build the ETL scheduler container -docker-compose build etl-scheduler - -# Test container startup -docker-compose up etl-scheduler -d - -# Check container logs -docker-compose logs etl-scheduler -``` - -### Step 2: Test ETL Connection - -```bash -# Test database connections -docker-compose exec etl-scheduler python -m etl.main test-connections - -# Should output: "All connections successful" -``` - -### Step 3: Test Manual ETL Execution - -```bash -# Run ETL manually to test functionality -docker-compose exec etl-scheduler python -m etl.main build-catalog - -# Check for success in logs -docker-compose exec etl-scheduler cat /app/logs/etl-last-run.txt -``` - -### Step 4: Verify Cron Configuration - -```bash -# Check cron job is configured -docker-compose exec etl-scheduler crontab -l - -# Should show: "0 2 * * 0 cd /app && python -m etl.main build-catalog >> /app/logs/etl-cron.log 2>&1" -``` - -### Step 5: Test ETL Status Monitoring - -```bash -# Test status check script -docker-compose exec etl-scheduler /app/scripts/check-etl-status.sh - -# Check health check endpoint -curl -f http://localhost:8080/health || echo "Health check failed" -``` - -## Error Handling - -### Common Issues and Solutions - -**Issue**: Cron daemon not starting -**Solution**: Check entrypoint.sh permissions, verify cron package installation - -**Issue**: Database connection failures -**Solution**: Verify network connectivity, check environment variables, ensure databases are healthy - -**Issue**: ETL process hanging -**Solution**: Add timeout mechanisms, check for deadlocks, increase memory limits - -**Issue**: Log files not being written -**Solution**: Check volume mounts, verify directory permissions - -### ETL Failure Recovery - -**Automatic Recovery**: -- Container restart policy: `unless-stopped` -- Retry logic in ETL scripts (max 3 retries) -- Health check will restart container if ETL consistently fails - -**Manual Recovery**: -```bash -# Check ETL status -docker-compose exec etl-scheduler /app/scripts/check-etl-status.sh - -# Restart ETL container -docker-compose restart etl-scheduler - -# Run ETL manually if needed -docker-compose exec etl-scheduler python -m etl.main build-catalog -``` - -### Rollback Procedure - -1. Stop ETL scheduler: - ```bash - docker-compose stop etl-scheduler - ``` - -2. Remove ETL-related files if needed: - ```bash - rm -rf vehicle-etl/docker/ - ``` - -3. Remove ETL scheduler from docker-compose.yml - -4. Restart remaining services: - ```bash - docker-compose up -d - ``` - -## Next Steps - -After successful completion of Phase 4: - -1. Proceed to [Phase 5: Testing & Validation](./phase-05-testing.md) -2. Monitor ETL execution for first few runs -3. Set up alerting for ETL failures -4. Document ETL maintenance procedures - -## Dependencies for Next Phase - -- ETL scheduler running successfully -- Cron job configured and functional -- First ETL run completed successfully -- MVP Platform database populated with vehicle data -- ETL monitoring and health checks working \ No newline at end of file diff --git a/docs/changes/vehicles-dropdown-v1/phase-05-testing.md b/docs/changes/vehicles-dropdown-v1/phase-05-testing.md deleted file mode 100644 index 4a0f4e7..0000000 --- a/docs/changes/vehicles-dropdown-v1/phase-05-testing.md +++ /dev/null @@ -1,727 +0,0 @@ -# Phase 5: Testing & Validation - -## Overview - -This phase provides comprehensive testing procedures to validate that the Vehicle ETL integration meets all performance, accuracy, and reliability requirements. Testing covers API functionality, performance benchmarks, data accuracy, and system reliability. - -## Prerequisites - -- All previous phases (1-4) completed successfully -- MVP Platform database populated with vehicle data -- All API endpoints functional -- ETL scheduler running and operational -- Backend service connected to MVP Platform database - -## Success Criteria Review - -Before starting tests, review the success criteria: - -- āœ… **Zero Breaking Changes**: All existing vehicle functionality unchanged -- āœ… **Performance**: Dropdown APIs maintain < 100ms response times -- āœ… **Accuracy**: VIN decoding matches current NHTSA accuracy (99.9%+) -- āœ… **Reliability**: Weekly ETL completes successfully with error handling -- āœ… **Scalability**: Clean two-database architecture ready for additional platform services - -## Testing Categories - -### Category 1: API Functionality Testing -### Category 2: Performance Testing -### Category 3: Data Accuracy Validation -### Category 4: ETL Process Testing -### Category 5: Error Handling & Recovery -### Category 6: Load Testing -### Category 7: Security Validation - ---- - -## Category 1: API Functionality Testing - -### Test 1.1: Dropdown API Response Formats - -**Purpose**: Verify all dropdown endpoints return data in the exact same format as before - -**Test Script**: `test-api-formats.sh` - -```bash -#!/bin/bash - -echo "=== API Format Validation Tests ===" - -# Test makes endpoint -echo "Testing /api/vehicles/dropdown/makes..." -MAKES_RESPONSE=$(curl -s http://localhost:3001/api/vehicles/dropdown/makes) -MAKES_COUNT=$(echo "$MAKES_RESPONSE" | jq '. | length') - -if [ "$MAKES_COUNT" -gt 0 ]; then - # Check first item has correct format - FIRST_MAKE=$(echo "$MAKES_RESPONSE" | jq '.[0]') - if echo "$FIRST_MAKE" | jq -e '.Make_ID and .Make_Name' > /dev/null; then - echo "āœ… Makes format correct" - else - echo "āŒ Makes format incorrect: $FIRST_MAKE" - exit 1 - fi -else - echo "āŒ No makes returned" - exit 1 -fi - -# Test models endpoint -echo "Testing /api/vehicles/dropdown/models/:make..." -FIRST_MAKE_NAME=$(echo "$MAKES_RESPONSE" | jq -r '.[0].Make_Name') -MODELS_RESPONSE=$(curl -s "http://localhost:3001/api/vehicles/dropdown/models/$FIRST_MAKE_NAME") -MODELS_COUNT=$(echo "$MODELS_RESPONSE" | jq '. | length') - -if [ "$MODELS_COUNT" -gt 0 ]; then - FIRST_MODEL=$(echo "$MODELS_RESPONSE" | jq '.[0]') - if echo "$FIRST_MODEL" | jq -e '.Model_ID and .Model_Name' > /dev/null; then - echo "āœ… Models format correct" - else - echo "āŒ Models format incorrect: $FIRST_MODEL" - exit 1 - fi -else - echo "āš ļø No models for $FIRST_MAKE_NAME (may be expected)" -fi - -# Test transmissions endpoint -echo "Testing /api/vehicles/dropdown/transmissions..." -TRANS_RESPONSE=$(curl -s http://localhost:3001/api/vehicles/dropdown/transmissions) -TRANS_COUNT=$(echo "$TRANS_RESPONSE" | jq '. | length') - -if [ "$TRANS_COUNT" -gt 0 ]; then - FIRST_TRANS=$(echo "$TRANS_RESPONSE" | jq '.[0]') - if echo "$FIRST_TRANS" | jq -e '.Name' > /dev/null; then - echo "āœ… Transmissions format correct" - else - echo "āŒ Transmissions format incorrect: $FIRST_TRANS" - exit 1 - fi -else - echo "āŒ No transmissions returned" - exit 1 -fi - -# Test engines endpoint -echo "Testing /api/vehicles/dropdown/engines..." -ENGINES_RESPONSE=$(curl -s http://localhost:3001/api/vehicles/dropdown/engines) -ENGINES_COUNT=$(echo "$ENGINES_RESPONSE" | jq '. | length') - -if [ "$ENGINES_COUNT" -gt 0 ]; then - FIRST_ENGINE=$(echo "$ENGINES_RESPONSE" | jq '.[0]') - if echo "$FIRST_ENGINE" | jq -e '.Name' > /dev/null; then - echo "āœ… Engines format correct" - else - echo "āŒ Engines format incorrect: $FIRST_ENGINE" - exit 1 - fi -else - echo "āŒ No engines returned" - exit 1 -fi - -# Test trims endpoint -echo "Testing /api/vehicles/dropdown/trims..." -TRIMS_RESPONSE=$(curl -s http://localhost:3001/api/vehicles/dropdown/trims) -TRIMS_COUNT=$(echo "$TRIMS_RESPONSE" | jq '. | length') - -if [ "$TRIMS_COUNT" -gt 0 ]; then - FIRST_TRIM=$(echo "$TRIMS_RESPONSE" | jq '.[0]') - if echo "$FIRST_TRIM" | jq -e '.Name' > /dev/null; then - echo "āœ… Trims format correct" - else - echo "āŒ Trims format incorrect: $FIRST_TRIM" - exit 1 - fi -else - echo "āŒ No trims returned" - exit 1 -fi - -echo "āœ… All API format tests passed" -``` - -### Test 1.2: Authentication Validation - -**Purpose**: Ensure dropdown endpoints remain unauthenticated while CRUD endpoints require authentication - -**Test Script**: `test-authentication.sh` - -```bash -#!/bin/bash - -echo "=== Authentication Validation Tests ===" - -# Test dropdown endpoints are unauthenticated -echo "Testing dropdown endpoints without authentication..." - -ENDPOINTS=( - "/api/vehicles/dropdown/makes" - "/api/vehicles/dropdown/transmissions" - "/api/vehicles/dropdown/engines" - "/api/vehicles/dropdown/trims" -) - -for endpoint in "${ENDPOINTS[@]}"; do - RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:3001$endpoint") - if [ "$RESPONSE" = "200" ]; then - echo "āœ… $endpoint accessible without auth" - else - echo "āŒ $endpoint returned $RESPONSE (should be 200)" - exit 1 - fi -done - -# Test CRUD endpoints require authentication -echo "Testing CRUD endpoints require authentication..." - -CRUD_ENDPOINTS=( - "/api/vehicles" - "/api/vehicles/123" -) - -for endpoint in "${CRUD_ENDPOINTS[@]}"; do - RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:3001$endpoint") - if [ "$RESPONSE" = "401" ]; then - echo "āœ… $endpoint properly requires auth" - else - echo "āŒ $endpoint returned $RESPONSE (should be 401)" - exit 1 - fi -done - -echo "āœ… All authentication tests passed" -``` - ---- - -## Category 2: Performance Testing - -### Test 2.1: Response Time Measurement - -**Purpose**: Verify all dropdown APIs respond in < 100ms - -**Test Script**: `test-performance.sh` - -```bash -#!/bin/bash - -echo "=== Performance Tests ===" - -ENDPOINTS=( - "/api/vehicles/dropdown/makes" - "/api/vehicles/dropdown/models/Honda" - "/api/vehicles/dropdown/transmissions" - "/api/vehicles/dropdown/engines" - "/api/vehicles/dropdown/trims" -) - -MAX_RESPONSE_TIME=100 # milliseconds - -for endpoint in "${ENDPOINTS[@]}"; do - echo "Testing $endpoint performance..." - - # Run 5 tests and get average - TOTAL_TIME=0 - for i in {1..5}; do - START_TIME=$(date +%s%3N) - curl -s "http://localhost:3001$endpoint" > /dev/null - END_TIME=$(date +%s%3N) - RESPONSE_TIME=$((END_TIME - START_TIME)) - TOTAL_TIME=$((TOTAL_TIME + RESPONSE_TIME)) - done - - AVG_TIME=$((TOTAL_TIME / 5)) - - if [ "$AVG_TIME" -lt "$MAX_RESPONSE_TIME" ]; then - echo "āœ… $endpoint: ${AVG_TIME}ms (under ${MAX_RESPONSE_TIME}ms)" - else - echo "āŒ $endpoint: ${AVG_TIME}ms (exceeds ${MAX_RESPONSE_TIME}ms)" - exit 1 - fi -done - -echo "āœ… All performance tests passed" -``` - -### Test 2.2: Cache Performance Testing - -**Purpose**: Verify caching improves performance on subsequent requests - -**Test Script**: `test-cache-performance.sh` - -```bash -#!/bin/bash - -echo "=== Cache Performance Tests ===" - -ENDPOINT="/api/vehicles/dropdown/makes" - -# Clear cache (requires Redis access) -docker-compose exec redis redis-cli FLUSHDB - -echo "Testing first request (cache miss)..." -START_TIME=$(date +%s%3N) -curl -s "http://localhost:3001$ENDPOINT" > /dev/null -END_TIME=$(date +%s%3N) -FIRST_REQUEST_TIME=$((END_TIME - START_TIME)) - -echo "Testing second request (cache hit)..." -START_TIME=$(date +%s%3N) -curl -s "http://localhost:3001$ENDPOINT" > /dev/null -END_TIME=$(date +%s%3N) -SECOND_REQUEST_TIME=$((END_TIME - START_TIME)) - -echo "First request: ${FIRST_REQUEST_TIME}ms" -echo "Second request: ${SECOND_REQUEST_TIME}ms" - -# Cache hit should be significantly faster -if [ "$SECOND_REQUEST_TIME" -lt "$FIRST_REQUEST_TIME" ]; then - IMPROVEMENT=$((((FIRST_REQUEST_TIME - SECOND_REQUEST_TIME) * 100) / FIRST_REQUEST_TIME)) - echo "āœ… Cache improved performance by ${IMPROVEMENT}%" -else - echo "āŒ Cache did not improve performance" - exit 1 -fi - -echo "āœ… Cache performance test passed" -``` - ---- - -## Category 3: Data Accuracy Validation - -### Test 3.1: VIN Decoding Accuracy - -**Purpose**: Verify VIN decoding produces accurate results - -**Test Script**: `test-vin-accuracy.sh` - -```bash -#!/bin/bash - -echo "=== VIN Decoding Accuracy Tests ===" - -# Test VINs with known results -declare -A TEST_VINS=( - ["1HGBH41JXMN109186"]="Honda,Civic,2021" - ["3GTUUFEL6PG140748"]="GMC,Sierra,2023" - ["1G1YU3D64H5602799"]="Chevrolet,Corvette,2017" -) - -for vin in "${!TEST_VINS[@]}"; do - echo "Testing VIN: $vin" - - # Create test vehicle to trigger VIN decoding - RESPONSE=$(curl -s -X POST "http://localhost:3001/api/vehicles" \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer test-token" \ - -d "{\"vin\":\"$vin\",\"nickname\":\"Test\"}" \ - 2>/dev/null || echo "AUTH_ERROR") - - if [ "$RESPONSE" = "AUTH_ERROR" ]; then - echo "āš ļø Skipping VIN test due to authentication (expected in testing)" - continue - fi - - # Parse expected results - IFS=',' read -r EXPECTED_MAKE EXPECTED_MODEL EXPECTED_YEAR <<< "${TEST_VINS[$vin]}" - - # Extract actual results - ACTUAL_MAKE=$(echo "$RESPONSE" | jq -r '.make // empty') - ACTUAL_MODEL=$(echo "$RESPONSE" | jq -r '.model // empty') - ACTUAL_YEAR=$(echo "$RESPONSE" | jq -r '.year // empty') - - # Validate results - if [ "$ACTUAL_MAKE" = "$EXPECTED_MAKE" ] && \ - [ "$ACTUAL_MODEL" = "$EXPECTED_MODEL" ] && \ - [ "$ACTUAL_YEAR" = "$EXPECTED_YEAR" ]; then - echo "āœ… VIN $vin decoded correctly" - else - echo "āŒ VIN $vin decoded incorrectly:" - echo " Expected: $EXPECTED_MAKE $EXPECTED_MODEL $EXPECTED_YEAR" - echo " Actual: $ACTUAL_MAKE $ACTUAL_MODEL $ACTUAL_YEAR" - exit 1 - fi -done - -echo "āœ… VIN accuracy tests passed" -``` - -### Test 3.2: Data Completeness Check - -**Purpose**: Verify MVP Platform database has comprehensive data - -**Test Script**: `test-data-completeness.sh` - -```bash -#!/bin/bash - -echo "=== Data Completeness Tests ===" - -# Test makes count -MAKES_COUNT=$(curl -s http://localhost:3001/api/vehicles/dropdown/makes | jq '. | length') -echo "Makes available: $MAKES_COUNT" - -if [ "$MAKES_COUNT" -lt 50 ]; then - echo "āŒ Too few makes ($MAKES_COUNT < 50)" - exit 1 -fi - -# Test transmissions count -TRANS_COUNT=$(curl -s http://localhost:3001/api/vehicles/dropdown/transmissions | jq '. | length') -echo "Transmissions available: $TRANS_COUNT" - -if [ "$TRANS_COUNT" -lt 10 ]; then - echo "āŒ Too few transmissions ($TRANS_COUNT < 10)" - exit 1 -fi - -# Test engines count -ENGINES_COUNT=$(curl -s http://localhost:3001/api/vehicles/dropdown/engines | jq '. | length') -echo "Engines available: $ENGINES_COUNT" - -if [ "$ENGINES_COUNT" -lt 20 ]; then - echo "āŒ Too few engines ($ENGINES_COUNT < 20)" - exit 1 -fi - -echo "āœ… Data completeness tests passed" -``` - ---- - -## Category 4: ETL Process Testing - -### Test 4.1: ETL Execution Test - -**Purpose**: Verify ETL process runs successfully - -**Test Script**: `test-etl-execution.sh` - -```bash -#!/bin/bash - -echo "=== ETL Execution Tests ===" - -# Check ETL container is running -if ! docker-compose ps etl-scheduler | grep -q "Up"; then - echo "āŒ ETL scheduler container is not running" - exit 1 -fi - -# Test manual ETL execution -echo "Running manual ETL test..." -docker-compose exec etl-scheduler python -m etl.main test-connections - -if [ $? -eq 0 ]; then - echo "āœ… ETL connections successful" -else - echo "āŒ ETL connections failed" - exit 1 -fi - -# Check ETL status -echo "Checking ETL status..." -docker-compose exec etl-scheduler /app/scripts/check-etl-status.sh - -if [ $? -eq 0 ]; then - echo "āœ… ETL status check passed" -else - echo "āš ļø ETL status check returned warnings (may be expected)" -fi - -echo "āœ… ETL execution tests completed" -``` - -### Test 4.2: ETL Scheduling Test - -**Purpose**: Verify ETL is properly scheduled - -**Test Script**: `test-etl-scheduling.sh` - -```bash -#!/bin/bash - -echo "=== ETL Scheduling Tests ===" - -# Check cron job is configured -CRON_OUTPUT=$(docker-compose exec etl-scheduler crontab -l) - -if echo "$CRON_OUTPUT" | grep -q "etl.main build-catalog"; then - echo "āœ… ETL cron job is configured" -else - echo "āŒ ETL cron job not found" - exit 1 -fi - -# Check cron daemon is running -if docker-compose exec etl-scheduler pgrep cron > /dev/null; then - echo "āœ… Cron daemon is running" -else - echo "āŒ Cron daemon is not running" - exit 1 -fi - -echo "āœ… ETL scheduling tests passed" -``` - ---- - -## Category 5: Error Handling & Recovery - -### Test 5.1: Database Connection Error Handling - -**Purpose**: Verify graceful handling when MVP Platform database is unavailable - -**Test Script**: `test-error-handling.sh` - -```bash -#!/bin/bash - -echo "=== Error Handling Tests ===" - -# Stop MVP Platform database temporarily -echo "Stopping MVP Platform database..." -docker-compose stop mvp-platform-database - -sleep 5 - -# Test API responses when database is down -RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:3001/api/vehicles/dropdown/makes") - -if [ "$RESPONSE" = "503" ] || [ "$RESPONSE" = "500" ]; then - echo "āœ… API properly handles database unavailability (returned $RESPONSE)" -else - echo "āŒ API returned unexpected status: $RESPONSE" -fi - -# Restart database -echo "Restarting MVP Platform database..." -docker-compose start mvp-platform-database - -# Wait for database to be ready -sleep 15 - -# Test API recovery -RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:3001/api/vehicles/dropdown/makes") - -if [ "$RESPONSE" = "200" ]; then - echo "āœ… API recovered after database restart" -else - echo "āŒ API did not recover (returned $RESPONSE)" - exit 1 -fi - -echo "āœ… Error handling tests passed" -``` - ---- - -## Category 6: Load Testing - -### Test 6.1: Concurrent Request Testing - -**Purpose**: Verify system handles multiple concurrent requests - -**Test Script**: `test-load.sh` - -```bash -#!/bin/bash - -echo "=== Load Testing ===" - -ENDPOINT="http://localhost:3001/api/vehicles/dropdown/makes" -CONCURRENT_REQUESTS=50 -MAX_RESPONSE_TIME=500 # milliseconds - -echo "Running $CONCURRENT_REQUESTS concurrent requests..." - -# Create temporary file for results -RESULTS_FILE=$(mktemp) - -# Run concurrent requests -for i in $(seq 1 $CONCURRENT_REQUESTS); do - { - START_TIME=$(date +%s%3N) - HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$ENDPOINT") - END_TIME=$(date +%s%3N) - RESPONSE_TIME=$((END_TIME - START_TIME)) - echo "$HTTP_CODE,$RESPONSE_TIME" >> "$RESULTS_FILE" - } & -done - -# Wait for all requests to complete -wait - -# Analyze results -SUCCESS_COUNT=$(grep -c "^200," "$RESULTS_FILE") -TOTAL_COUNT=$(wc -l < "$RESULTS_FILE") -AVG_TIME=$(awk -F',' '{sum+=$2} END {print sum/NR}' "$RESULTS_FILE") -MAX_TIME=$(awk -F',' '{max=($2>max?$2:max)} END {print max}' "$RESULTS_FILE") - -echo "Results:" -echo " Successful requests: $SUCCESS_COUNT/$TOTAL_COUNT" -echo " Average response time: ${AVG_TIME}ms" -echo " Maximum response time: ${MAX_TIME}ms" - -# Cleanup -rm "$RESULTS_FILE" - -# Validate results -if [ "$SUCCESS_COUNT" -eq "$TOTAL_COUNT" ] && [ "$MAX_TIME" -lt "$MAX_RESPONSE_TIME" ]; then - echo "āœ… Load test passed" -else - echo "āŒ Load test failed" - exit 1 -fi -``` - ---- - -## Category 7: Security Validation - -### Test 7.1: SQL Injection Prevention - -**Purpose**: Verify protection against SQL injection attacks - -**Test Script**: `test-security.sh` - -```bash -#!/bin/bash - -echo "=== Security Tests ===" - -# Test SQL injection attempts in make parameter -INJECTION_ATTEMPTS=( - "'; DROP TABLE vehicles; --" - "' OR '1'='1" - "'; SELECT * FROM users; --" - "../../../etc/passwd" -) - -for injection in "${INJECTION_ATTEMPTS[@]}"; do - echo "Testing injection attempt: $injection" - - # URL encode the injection - ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$injection'))") - - RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \ - "http://localhost:3001/api/vehicles/dropdown/models/$ENCODED") - - # Should return 400, 404, or 500 (not 200 with injected content) - if [ "$RESPONSE" != "200" ]; then - echo "āœ… Injection attempt blocked (returned $RESPONSE)" - else - echo "āš ļø Injection attempt returned 200 (investigating...)" - # Additional validation would be needed here - fi -done - -echo "āœ… Security tests completed" -``` - ---- - -## Comprehensive Test Execution - -### Master Test Script - -**Location**: `test-all.sh` - -```bash -#!/bin/bash - -echo "=========================================" -echo "MotoVaultPro Vehicle ETL Integration Tests" -echo "=========================================" - -# Set up -chmod +x test-*.sh - -# Track test results -PASSED=0 -FAILED=0 - -run_test() { - echo - echo "Running $1..." - if ./$1; then - echo "āœ… $1 PASSED" - ((PASSED++)) - else - echo "āŒ $1 FAILED" - ((FAILED++)) - fi -} - -# Execute all test categories -run_test "test-api-formats.sh" -run_test "test-authentication.sh" -run_test "test-performance.sh" -run_test "test-cache-performance.sh" -run_test "test-data-completeness.sh" -run_test "test-etl-execution.sh" -run_test "test-etl-scheduling.sh" -run_test "test-error-handling.sh" -run_test "test-load.sh" -run_test "test-security.sh" - -# Final results -echo -echo "=========================================" -echo "TEST SUMMARY" -echo "=========================================" -echo "Passed: $PASSED" -echo "Failed: $FAILED" -echo "Total: $((PASSED + FAILED))" - -if [ $FAILED -eq 0 ]; then - echo "āœ… ALL TESTS PASSED" - echo "Vehicle ETL integration is ready for production!" - exit 0 -else - echo "āŒ SOME TESTS FAILED" - echo "Please review failed tests before proceeding." - exit 1 -fi -``` - -## Post-Testing Actions - -### Success Actions - -If all tests pass: - -1. **Document Test Results**: Save test output and timestamps -2. **Update Monitoring**: Configure alerts for ETL failures -3. **Schedule Production Deployment**: Plan rollout timing -4. **Update Documentation**: Mark implementation as complete - -### Failure Actions - -If tests fail: - -1. **Identify Root Cause**: Review failed test details -2. **Fix Issues**: Address specific failures -3. **Re-run Tests**: Validate fixes work -4. **Update Documentation**: Document any issues found - -## Ongoing Monitoring - -After successful testing, implement ongoing monitoring: - -1. **API Performance Monitoring**: Track response times daily -2. **ETL Success Monitoring**: Weekly ETL completion alerts -3. **Data Quality Checks**: Monthly data completeness validation -4. **Error Rate Monitoring**: Track and alert on API error rates - -## Rollback Plan - -If critical issues are discovered during testing: - -1. **Immediate Rollback**: Revert to external vPIC API -2. **Data Preservation**: Ensure no data loss occurs -3. **Service Continuity**: Maintain all existing functionality -4. **Issue Analysis**: Investigate and document problems -5. **Improved Re-implementation**: Address issues before retry \ No newline at end of file diff --git a/docs/changes/vehicles-dropdown-v2/01-analysis-findings.md b/docs/changes/vehicles-dropdown-v2/01-analysis-findings.md deleted file mode 100644 index eba00de..0000000 --- a/docs/changes/vehicles-dropdown-v2/01-analysis-findings.md +++ /dev/null @@ -1,203 +0,0 @@ -# Analysis Findings - JSON Vehicle Data - -## Data Source Overview -- **Location**: `mvp-platform-services/vehicles/etl/sources/makes/` -- **File Count**: 55 JSON files -- **File Naming**: Lowercase with underscores (e.g., `alfa_romeo.json`, `land_rover.json`) -- **Data Structure**: Hierarchical vehicle data by make - -## JSON File Structure Analysis - -### Standard Structure -```json -{ - "[make_name]": [ - { - "year": "2024", - "models": [ - { - "name": "model_name", - "engines": [ - "2.0L I4", - "3.5L V6 TURBO" - ], - "submodels": [ - "Base", - "Premium", - "Limited" - ] - } - ] - } - ] -} -``` - -### Key Data Points -1. **Make Level**: Root key matches filename (lowercase) -2. **Year Level**: Array of yearly data -3. **Model Level**: Array of models per year -4. **Engines**: Array of engine specifications -5. **Submodels**: Array of trim levels - -## Make Name Analysis - -### File Naming vs Display Name Issues -| Filename | Required Display Name | Issue | -|----------|---------------------|--------| -| `alfa_romeo.json` | "Alfa Romeo" | Underscore → space, title case | -| `land_rover.json` | "Land Rover" | Underscore → space, title case | -| `rolls_royce.json` | "Rolls Royce" | Underscore → space, title case | -| `chevrolet.json` | "Chevrolet" | Direct match | -| `bmw.json` | "BMW" | Uppercase required | - -### Make Name Normalization Rules -1. **Replace underscores** with spaces -2. **Title case** each word -3. **Special cases**: BMW, GMC (all caps) -4. **Validation**: Cross-reference with `sources/makes.json` - -## Engine Specification Analysis - -### Discovered Engine Patterns -From analysis of Nissan, Toyota, Ford, Subaru, and Porsche files: - -#### Standard Format: `{displacement}L {config}{cylinders}` -- `"2.0L I4"` - 2.0 liter, Inline 4-cylinder -- `"3.5L V6"` - 3.5 liter, V6 configuration -- `"2.4L H4"` - 2.4 liter, Horizontal (Boxer) 4-cylinder - -#### Configuration Types Found -- **I** = Inline (most common) -- **V** = V-configuration -- **H** = Horizontal/Boxer (Subaru, Porsche) -- **L** = **MUST BE TREATED AS INLINE** (L3 → I3) - -### Engine Modifier Patterns - -#### Hybrid Classifications -- `"PLUG-IN HYBRID EV- (PHEV)"` - Plug-in hybrid electric vehicle -- `"FULL HYBRID EV- (FHEV)"` - Full hybrid electric vehicle -- `"HYBRID"` - General hybrid designation - -#### Fuel Type Modifiers -- `"FLEX"` - Flex-fuel capability (e.g., `"5.6L V8 FLEX"`) -- `"ELECTRIC"` - Pure electric motor -- `"TURBO"` - Turbocharged (less common in current data) - -#### Example Engine Strings -``` -"2.5L I4 FULL HYBRID EV- (FHEV)" -"1.5L L3 PLUG-IN HYBRID EV- (PHEV)" // L3 → I3 -"5.6L V8 FLEX" -"2.4L H4" // Subaru Boxer -"1.8L I4 ELECTRIC" -``` - -## Special Cases Analysis - -### Electric Vehicle Handling -**Tesla Example** (`tesla.json`): -```json -{ - "name": "3", - "engines": [], // Empty array - "submodels": ["Long Range AWD", "Performance"] -} -``` - -**Lucid Example** (`lucid.json`): -```json -{ - "name": "air", - "engines": [], // Empty array - "submodels": [] -} -``` - -#### Electric Vehicle Requirements -- **Empty engines arrays** are common for pure electric vehicles -- **Must create default engine**: `"Electric Motor"` with appropriate specs -- **Fuel type**: `"Electric"` -- **Configuration**: `null` or `"Electric"` - -### Hybrid Vehicle Patterns -From Toyota analysis - hybrid appears in both engines and submodels: -- **Engine level**: `"1.8L I4 ELECTRIC"` -- **Submodel level**: `"Hybrid LE"`, `"Hybrid XSE"` - -## Data Quality Issues Found - -### Missing Engine Data -- **Tesla models**: Consistently empty engines arrays -- **Lucid models**: Empty engines arrays -- **Some Nissan models**: Empty engines for electric variants - -### Inconsistent Submodel Data -- **Mix of trim levels and descriptors** -- **Some technical specifications** in submodel names -- **Inconsistent naming patterns** across makes - -### Engine Specification Inconsistencies -- **L-configuration usage**: Should be normalized to I (Inline) -- **Mixed hybrid notation**: Sometimes in engine string, sometimes separate -- **Abbreviation variations**: EV- vs EV, FHEV vs FULL HYBRID - -## Database Mapping Strategy - -### Make Mapping -``` -Filename: "alfa_romeo.json" → Database: "Alfa Romeo" -``` - -### Model Mapping -``` -JSON models.name → vehicles.model.name -``` - -### Engine Mapping -``` -JSON engines[0] → vehicles.engine.name (with parsing) -Engine parsing → displacement_l, cylinders, fuel_type, aspiration -``` - -### Trim Mapping -``` -JSON submodels[0] → vehicles.trim.name -``` - -## Data Volume Estimates - -### File Size Analysis -- **Largest files**: `toyota.json` (~748KB), `volkswagen.json` (~738KB) -- **Smallest files**: `lucid.json` (~176B), `rivian.json` (~177B) -- **Average file size**: ~150KB - -### Record Estimates (Based on Sample Analysis) -- **Makes**: 55 (one per file) -- **Models per make**: 5-50 (highly variable) -- **Years per model**: 10-15 years average -- **Trims per model-year**: 3-10 average -- **Engines**: 500-1000 unique engines total - -## Processing Recommendations - -### Order of Operations -1. **Load makes** - Create make records with normalized names -2. **Load models** - Associate with correct make_id -3. **Load model_years** - Create year availability -4. **Parse and load engines** - Handle L→I normalization -5. **Load trims** - Associate with model_year_id -6. **Create trim_engine relationships** - -### Error Handling Requirements -- **Handle empty engines arrays** (electric vehicles) -- **Validate engine parsing** (log unparseable engines) -- **Handle duplicate records** (upsert strategy) -- **Report data quality issues** (missing data, parsing failures) - -## Validation Strategy -- **Cross-reference makes** with existing `sources/makes.json` -- **Validate engine parsing** with regex patterns -- **Check referential integrity** during loading -- **Report statistics** per make (models, engines, trims loaded) \ No newline at end of file diff --git a/docs/changes/vehicles-dropdown-v2/02-implementation-plan.md b/docs/changes/vehicles-dropdown-v2/02-implementation-plan.md deleted file mode 100644 index 99109ed..0000000 --- a/docs/changes/vehicles-dropdown-v2/02-implementation-plan.md +++ /dev/null @@ -1,307 +0,0 @@ -# Implementation Plan - Manual JSON ETL - -## Implementation Overview -Add manual JSON processing capability to the existing MVP Platform Vehicles ETL system without disrupting the current MSSQL-based pipeline. - -## Development Phases - -### Phase 1: Core Utilities ā³ -**Objective**: Create foundational utilities for JSON processing - -#### 1.1 Make Name Mapper (`etl/utils/make_name_mapper.py`) -```python -class MakeNameMapper: - def normalize_make_name(self, filename: str) -> str: - """Convert 'alfa_romeo' to 'Alfa Romeo'""" - - def get_display_name_mapping(self) -> Dict[str, str]: - """Get complete filename -> display name mapping""" - - def validate_against_sources(self) -> List[str]: - """Cross-validate with sources/makes.json""" -``` - -**Implementation Requirements**: -- Handle underscore → space conversion -- Title case each word -- Special cases: BMW, GMC (all caps) -- Validation against existing `sources/makes.json` - -#### 1.2 Engine Spec Parser (`etl/utils/engine_spec_parser.py`) -```python -@dataclass -class EngineSpec: - displacement_l: float - configuration: str # I, V, H - cylinders: int - fuel_type: str # Gasoline, Hybrid, Electric, Flex Fuel - aspiration: str # Natural, Turbo, Supercharged - raw_string: str - -class EngineSpecParser: - def parse_engine_string(self, engine_str: str) -> EngineSpec: - """Parse '2.0L I4 PLUG-IN HYBRID EV- (PHEV)' into components""" - - def normalize_configuration(self, config: str) -> str: - """Convert L → I (L3 becomes I3)""" - - def extract_fuel_type(self, engine_str: str) -> str: - """Extract fuel type from modifiers""" -``` - -**Implementation Requirements**: -- **CRITICAL**: L-configuration → I (Inline) normalization -- Regex patterns for standard format: `{displacement}L {config}{cylinders}` -- Hybrid/electric detection: PHEV, FHEV, ELECTRIC patterns -- Flex-fuel detection: FLEX modifier -- Handle parsing failures gracefully - -### Phase 2: Data Extraction ā³ -**Objective**: Extract data from JSON files into normalized structures - -#### 2.1 JSON Extractor (`etl/extractors/json_extractor.py`) -```python -class JsonExtractor: - def __init__(self, make_mapper: MakeNameMapper, - engine_parser: EngineSpecParser): - pass - - def extract_make_data(self, json_file_path: str) -> MakeData: - """Extract complete make data from JSON file""" - - def extract_all_makes(self, sources_dir: str) -> List[MakeData]: - """Process all JSON files in directory""" - - def validate_json_structure(self, json_data: dict) -> ValidationResult: - """Validate JSON structure before processing""" -``` - -**Data Structures**: -```python -@dataclass -class MakeData: - name: str # Normalized display name - models: List[ModelData] - -@dataclass -class ModelData: - name: str - years: List[int] - engines: List[EngineSpec] - trims: List[str] # From submodels -``` - -#### 2.2 Electric Vehicle Handler -```python -class ElectricVehicleHandler: - def create_default_engine(self) -> EngineSpec: - """Create default 'Electric Motor' engine for empty arrays""" - - def is_electric_vehicle(self, model_data: ModelData) -> bool: - """Detect electric vehicles by empty engines + make patterns""" -``` - -### Phase 3: Data Loading ā³ -**Objective**: Load JSON-extracted data into PostgreSQL - -#### 3.1 JSON Manual Loader (`etl/loaders/json_manual_loader.py`) -```python -class JsonManualLoader: - def __init__(self, postgres_loader: PostgreSQLLoader): - pass - - def load_make_data(self, make_data: MakeData, mode: LoadMode): - """Load complete make data with referential integrity""" - - def load_all_makes(self, makes_data: List[MakeData], - mode: LoadMode) -> LoadResult: - """Batch load all makes with progress tracking""" - - def handle_duplicates(self, table: str, data: List[Dict]) -> int: - """Handle duplicate records based on natural keys""" -``` - -**Load Modes**: -- **CLEAR**: `TRUNCATE CASCADE` then insert (destructive) -- **APPEND**: Insert with `ON CONFLICT DO NOTHING` (safe) - -#### 3.2 Extend PostgreSQL Loader -Enhance `etl/loaders/postgres_loader.py` with JSON-specific methods: -```python -def load_json_makes(self, makes: List[Dict], clear_existing: bool) -> int -def load_json_engines(self, engines: List[EngineSpec], clear_existing: bool) -> int -def create_model_year_relationships(self, model_years: List[Dict]) -> int -``` - -### Phase 4: Pipeline Integration ā³ -**Objective**: Create manual JSON processing pipeline - -#### 4.1 Manual JSON Pipeline (`etl/pipelines/manual_json_pipeline.py`) -```python -class ManualJsonPipeline: - def __init__(self, sources_dir: str): - self.extractor = JsonExtractor(...) - self.loader = JsonManualLoader(...) - - def run_manual_pipeline(self, mode: LoadMode, - specific_make: Optional[str] = None) -> PipelineResult: - """Complete JSON → PostgreSQL pipeline""" - - def validate_before_load(self) -> ValidationReport: - """Pre-flight validation of all JSON files""" - - def generate_load_report(self) -> LoadReport: - """Post-load statistics and data quality report""" -``` - -#### 4.2 Pipeline Result Tracking -```python -@dataclass -class PipelineResult: - success: bool - makes_processed: int - models_loaded: int - engines_loaded: int - trims_loaded: int - errors: List[str] - warnings: List[str] - duration: timedelta -``` - -### Phase 5: CLI Integration ā³ -**Objective**: Add CLI commands for manual processing - -#### 5.1 Main CLI Updates (`etl/main.py`) -```python -@cli.command() -@click.option('--mode', type=click.Choice(['clear', 'append']), - default='append', help='Load mode') -@click.option('--make', help='Process specific make only') -@click.option('--validate-only', is_flag=True, - help='Validate JSON files without loading') -def load_manual(mode, make, validate_only): - """Load vehicle data from JSON files""" - -@cli.command() -def validate_json(): - """Validate all JSON files structure and data quality""" -``` - -#### 5.2 Configuration Updates (`etl/config.py`) -```python -# JSON Processing settings -JSON_SOURCES_DIR: str = "sources/makes" -MANUAL_LOAD_DEFAULT_MODE: str = "append" -ELECTRIC_DEFAULT_ENGINE: str = "Electric Motor" -ENGINE_PARSING_STRICT: bool = False # Log vs fail on parse errors -``` - -### Phase 6: Testing & Validation ā³ -**Objective**: Comprehensive testing and validation - -#### 6.1 Unit Tests -- `test_make_name_mapper.py` - Make name normalization -- `test_engine_spec_parser.py` - Engine parsing with L→I normalization -- `test_json_extractor.py` - JSON data extraction -- `test_manual_loader.py` - Database loading - -#### 6.2 Integration Tests -- `test_manual_pipeline.py` - End-to-end JSON processing -- `test_api_integration.py` - Verify API endpoints work with JSON data -- `test_data_quality.py` - Data quality validation - -#### 6.3 Data Validation Scripts -```python -# examples/validate_all_json.py -def validate_all_makes() -> ValidationReport: - """Validate all 55 JSON files and report issues""" - -# examples/compare_data_sources.py -def compare_mssql_vs_json() -> ComparisonReport: - """Compare MSSQL vs JSON data for overlapping makes""" -``` - -## File Structure Changes - -### New Files to Create -``` -etl/ -ā”œā”€ā”€ utils/ -│ ā”œā”€ā”€ make_name_mapper.py # Make name normalization -│ └── engine_spec_parser.py # Engine specification parsing -ā”œā”€ā”€ extractors/ -│ └── json_extractor.py # JSON data extraction -ā”œā”€ā”€ loaders/ -│ └── json_manual_loader.py # JSON-specific data loading -└── pipelines/ - └── manual_json_pipeline.py # JSON processing pipeline -``` - -### Files to Modify -``` -etl/ -ā”œā”€ā”€ main.py # Add load-manual command -ā”œā”€ā”€ config.py # Add JSON processing config -└── loaders/ - └── postgres_loader.py # Extend for JSON data types -``` - -## Implementation Order - -### Week 1: Foundation -1. āœ… Create documentation structure -2. ā³ Implement `MakeNameMapper` with validation -3. ā³ Implement `EngineSpecParser` with L→I normalization -4. ā³ Unit tests for utilities - -### Week 2: Data Processing -1. ā³ Implement `JsonExtractor` with validation -2. ā³ Implement `ElectricVehicleHandler` -3. ā³ Create data structures and type definitions -4. ā³ Integration tests for extraction - -### Week 3: Data Loading -1. ā³ Implement `JsonManualLoader` with clear/append modes -2. ā³ Extend `PostgreSQLLoader` for JSON data types -3. ā³ Implement duplicate handling strategy -4. ā³ Database integration tests - -### Week 4: Pipeline & CLI -1. ā³ Implement `ManualJsonPipeline` -2. ā³ Add CLI commands with options -3. ā³ Add configuration management -4. ā³ End-to-end testing - -### Week 5: Validation & Polish -1. ā³ Comprehensive data validation -2. ā³ Performance testing with all 55 files -3. ā³ Error handling improvements -4. ā³ Documentation completion - -## Success Metrics -- [ ] Process all 55 JSON files without errors -- [ ] Correct make name normalization (alfa_romeo → Alfa Romeo) -- [ ] Engine parsing with L→I normalization working -- [ ] Electric vehicle handling (default engines created) -- [ ] Clear/append modes working correctly -- [ ] API endpoints return data from JSON sources -- [ ] Performance acceptable (<5 minutes for full load) -- [ ] Comprehensive error reporting and logging - -## Risk Mitigation - -### Data Quality Risks -- **Mitigation**: Extensive validation before loading -- **Fallback**: Report data quality issues, continue processing - -### Performance Risks -- **Mitigation**: Batch processing, progress tracking -- **Fallback**: Process makes individually if batch fails - -### Schema Compatibility Risks -- **Mitigation**: Thorough testing against existing schema -- **Fallback**: Schema migration scripts if needed - -### Integration Risks -- **Mitigation**: Maintain existing MSSQL pipeline compatibility -- **Fallback**: Feature flag to disable JSON processing \ No newline at end of file diff --git a/docs/changes/vehicles-dropdown-v2/03-engine-spec-parsing.md b/docs/changes/vehicles-dropdown-v2/03-engine-spec-parsing.md deleted file mode 100644 index 686662e..0000000 --- a/docs/changes/vehicles-dropdown-v2/03-engine-spec-parsing.md +++ /dev/null @@ -1,262 +0,0 @@ -# Engine Specification Parsing Rules - -## Overview -Comprehensive rules for parsing engine specifications from JSON files into PostgreSQL engine table structure. - -## Standard Engine Format -### Pattern: `{displacement}L {configuration}{cylinders} {modifiers}` - -Examples: -- `"2.0L I4"` → 2.0L, Inline, 4-cylinder -- `"3.5L V6 TURBO"` → 3.5L, V6, Turbocharged -- `"1.5L L3 PLUG-IN HYBRID EV- (PHEV)"` → 1.5L, **Inline** (L→I), 3-cyl, Plug-in Hybrid - -## Configuration Normalization Rules - -### CRITICAL: L-Configuration Handling -**L-configurations MUST be treated as Inline (I)** - -| Input | Normalized | Reasoning | -|-------|------------|-----------| -| `"1.5L L3"` | `"1.5L I3"` | L3 is alternate notation for Inline 3-cylinder | -| `"2.0L L4"` | `"2.0L I4"` | L4 is alternate notation for Inline 4-cylinder | -| `"1.2L L3 FULL HYBRID EV- (FHEV)"` | `"1.2L I3"` + Hybrid | L→I normalization + hybrid flag | - -### Configuration Types -- **I** = Inline (most common) -- **V** = V-configuration -- **H** = Horizontal/Boxer (Subaru, Porsche) -- **L** = **Convert to I** (alternate Inline notation) - -## Engine Parsing Implementation - -### Regex Patterns -```python -# Primary engine pattern -ENGINE_PATTERN = r'(\d+\.?\d*)L\s+([IVHL])(\d+)' - -# Modifier patterns -HYBRID_PATTERNS = [ - r'PLUG-IN HYBRID EV-?\s*\(PHEV\)', - r'FULL HYBRID EV-?\s*\(FHEV\)', - r'HYBRID' -] - -FUEL_PATTERNS = [ - r'FLEX', - r'ELECTRIC', - r'TURBO', - r'SUPERCHARGED' -] -``` - -### Parsing Algorithm -```python -def parse_engine_string(engine_str: str) -> EngineSpec: - # 1. Extract base components (displacement, config, cylinders) - match = re.match(ENGINE_PATTERN, engine_str) - displacement = float(match.group(1)) - config = normalize_configuration(match.group(2)) # L→I here - cylinders = int(match.group(3)) - - # 2. Detect fuel type and aspiration from modifiers - fuel_type = extract_fuel_type(engine_str) - aspiration = extract_aspiration(engine_str) - - return EngineSpec( - displacement_l=displacement, - configuration=config, - cylinders=cylinders, - fuel_type=fuel_type, - aspiration=aspiration, - raw_string=engine_str - ) - -def normalize_configuration(config: str) -> str: - """CRITICAL: Convert L to I""" - return 'I' if config == 'L' else config -``` - -## Fuel Type Detection - -### Hybrid Classifications -| Pattern | Database Value | Description | -|---------|---------------|-------------| -| `"PLUG-IN HYBRID EV- (PHEV)"` | `"Plug-in Hybrid"` | Plug-in hybrid electric | -| `"FULL HYBRID EV- (FHEV)"` | `"Full Hybrid"` | Full hybrid electric | -| `"HYBRID"` | `"Hybrid"` | General hybrid | - -### Other Fuel Types -| Pattern | Database Value | Description | -|---------|---------------|-------------| -| `"FLEX"` | `"Flex Fuel"` | Flex-fuel capability | -| `"ELECTRIC"` | `"Electric"` | Pure electric | -| No modifier | `"Gasoline"` | Default assumption | - -## Aspiration Detection - -### Forced Induction -| Pattern | Database Value | Description | -|---------|---------------|-------------| -| `"TURBO"` | `"Turbocharged"` | Turbocharged engine | -| `"SUPERCHARGED"` | `"Supercharged"` | Supercharged engine | -| `"SC"` | `"Supercharged"` | Supercharged (short form) | -| No modifier | `"Natural"` | Naturally aspirated | - -## Real-World Examples - -### Standard Engines -``` -Input: "2.0L I4" -Output: EngineSpec( - displacement_l=2.0, - configuration="I", - cylinders=4, - fuel_type="Gasoline", - aspiration="Natural", - raw_string="2.0L I4" -) -``` - -### L→I Normalization Example -``` -Input: "1.5L L3 PLUG-IN HYBRID EV- (PHEV)" -Output: EngineSpec( - displacement_l=1.5, - configuration="I", # L normalized to I - cylinders=3, - fuel_type="Plug-in Hybrid", - aspiration="Natural", - raw_string="1.5L L3 PLUG-IN HYBRID EV- (PHEV)" -) -``` - -### Subaru Boxer Engine -``` -Input: "2.4L H4" -Output: EngineSpec( - displacement_l=2.4, - configuration="H", # Horizontal/Boxer - cylinders=4, - fuel_type="Gasoline", - aspiration="Natural", - raw_string="2.4L H4" -) -``` - -### Flex Fuel Engine -``` -Input: "5.6L V8 FLEX" -Output: EngineSpec( - displacement_l=5.6, - configuration="V", - cylinders=8, - fuel_type="Flex Fuel", - aspiration="Natural", - raw_string="5.6L V8 FLEX" -) -``` - -## Electric Vehicle Handling - -### Empty Engines Arrays -When `engines: []` is found (common in Tesla, Lucid): - -```python -def create_default_electric_engine() -> EngineSpec: - return EngineSpec( - displacement_l=None, # N/A for electric - configuration="Electric", # Special designation - cylinders=None, # N/A for electric - fuel_type="Electric", - aspiration=None, # N/A for electric - raw_string="Electric Motor" - ) -``` - -### Electric Motor Naming -Default name: `"Electric Motor"` - -## Error Handling - -### Unparseable Engines -For engines that don't match standard patterns: -1. **Log warning** with original string -2. **Create fallback engine** with raw_string preserved -3. **Continue processing** (don't fail entire make) - -```python -def create_fallback_engine(raw_string: str) -> EngineSpec: - return EngineSpec( - displacement_l=None, - configuration="Unknown", - cylinders=None, - fuel_type="Unknown", - aspiration="Natural", - raw_string=raw_string - ) -``` - -### Validation Rules -1. **Displacement**: Must be positive number if present -2. **Configuration**: Must be I, V, H, or Electric -3. **Cylinders**: Must be positive integer if present -4. **Required**: At least raw_string must be preserved - -## Database Storage - -### Engine Table Mapping -```sql -INSERT INTO vehicles.engine ( - name, -- Original string or "Electric Motor" - code, -- NULL (not available in JSON) - displacement_l, -- Parsed displacement - cylinders, -- Parsed cylinder count - fuel_type, -- Parsed or "Gasoline" default - aspiration -- Parsed or "Natural" default -) -``` - -### Example Database Records -```sql --- Standard engine -('2.0L I4', NULL, 2.0, 4, 'Gasoline', 'Natural') - --- L→I normalized -('1.5L I3', NULL, 1.5, 3, 'Plug-in Hybrid', 'Natural') - --- Electric vehicle -('Electric Motor', NULL, NULL, NULL, 'Electric', NULL) - --- Subaru Boxer -('2.4L H4', NULL, 2.4, 4, 'Gasoline', 'Natural') -``` - -## Testing Requirements - -### Unit Test Cases -1. **L→I normalization**: `"1.5L L3"` → `configuration="I"` -2. **Hybrid detection**: All PHEV, FHEV, HYBRID patterns -3. **Configuration types**: I, V, H preservation -4. **Electric vehicles**: Empty array handling -5. **Error cases**: Unparseable strings -6. **Edge cases**: Missing displacement, unusual formats - -### Integration Test Cases -1. **Real JSON data**: Process actual make files -2. **Database storage**: Verify correct database records -3. **API compatibility**: Ensure dropdown endpoints work -4. **Performance**: Parse 1000+ engines efficiently - -## Future Considerations - -### Potential Enhancements -1. **Turbo detection**: More sophisticated forced induction parsing -2. **Engine codes**: Extract manufacturer engine codes where available -3. **Performance specs**: Parse horsepower/torque if present in future data -4. **Validation**: Cross-reference with automotive databases - -### Backwards Compatibility -- **MSSQL pipeline**: Must continue working unchanged -- **API responses**: Same format regardless of data source -- **Database schema**: No breaking changes required \ No newline at end of file diff --git a/docs/changes/vehicles-dropdown-v2/04-make-name-mapping.md b/docs/changes/vehicles-dropdown-v2/04-make-name-mapping.md deleted file mode 100644 index 14a9796..0000000 --- a/docs/changes/vehicles-dropdown-v2/04-make-name-mapping.md +++ /dev/null @@ -1,331 +0,0 @@ -# Make Name Mapping Documentation - -## Overview -Rules and implementation for converting JSON filename conventions to proper display names in the database. - -## Problem Statement -JSON files use lowercase filenames with underscores, but database and API require proper display names: -- `alfa_romeo.json` → `"Alfa Romeo"` -- `land_rover.json` → `"Land Rover"` -- `rolls_royce.json` → `"Rolls Royce"` - -## Normalization Rules - -### Standard Transformation -1. **Remove .json extension** -2. **Replace underscores** with spaces -3. **Apply title case** to each word -4. **Apply special case exceptions** - -### Implementation Algorithm -```python -def normalize_make_name(filename: str) -> str: - # Remove .json extension - base_name = filename.replace('.json', '') - - # Replace underscores with spaces - spaced_name = base_name.replace('_', ' ') - - # Apply title case - title_cased = spaced_name.title() - - # Apply special cases - return apply_special_cases(title_cased) -``` - -## Complete Filename Mapping - -### Multi-Word Makes (Underscore Conversion) -| Filename | Display Name | Notes | -|----------|-------------|-------| -| `alfa_romeo.json` | `"Alfa Romeo"` | Italian brand | -| `aston_martin.json` | `"Aston Martin"` | British luxury | -| `land_rover.json` | `"Land Rover"` | British SUV brand | -| `rolls_royce.json` | `"Rolls Royce"` | Ultra-luxury brand | - -### Single-Word Makes (Standard Title Case) -| Filename | Display Name | Notes | -|----------|-------------|-------| -| `acura.json` | `"Acura"` | Honda luxury division | -| `audi.json` | `"Audi"` | German luxury | -| `bentley.json` | `"Bentley"` | British luxury | -| `bmw.json` | `"BMW"` | **Special case - all caps** | -| `buick.json` | `"Buick"` | GM luxury | -| `cadillac.json` | `"Cadillac"` | GM luxury | -| `chevrolet.json` | `"Chevrolet"` | GM mainstream | -| `chrysler.json` | `"Chrysler"` | Stellantis brand | -| `dodge.json` | `"Dodge"` | Stellantis performance | -| `ferrari.json` | `"Ferrari"` | Italian supercar | -| `fiat.json` | `"Fiat"` | Italian mainstream | -| `ford.json` | `"Ford"` | American mainstream | -| `genesis.json` | `"Genesis"` | Hyundai luxury | -| `geo.json` | `"Geo"` | GM defunct brand | -| `gmc.json` | `"GMC"` | **Special case - all caps** | -| `honda.json` | `"Honda"` | Japanese mainstream | -| `hummer.json` | `"Hummer"` | GM truck brand | -| `hyundai.json` | `"Hyundai"` | Korean mainstream | -| `infiniti.json` | `"Infiniti"` | Nissan luxury | -| `isuzu.json` | `"Isuzu"` | Japanese commercial | -| `jaguar.json` | `"Jaguar"` | British luxury | -| `jeep.json` | `"Jeep"` | Stellantis SUV | -| `kia.json` | `"Kia"` | Korean mainstream | -| `lamborghini.json` | `"Lamborghini"` | Italian supercar | -| `lexus.json` | `"Lexus"` | Toyota luxury | -| `lincoln.json` | `"Lincoln"` | Ford luxury | -| `lotus.json` | `"Lotus"` | British sports car | -| `lucid.json` | `"Lucid"` | American electric luxury | -| `maserati.json` | `"Maserati"` | Italian luxury | -| `mazda.json` | `"Mazda"` | Japanese mainstream | -| `mclaren.json` | `"McLaren"` | **Special case - capital L** | -| `mercury.json` | `"Mercury"` | Ford defunct luxury | -| `mini.json` | `"MINI"` | **Special case - all caps** | -| `mitsubishi.json` | `"Mitsubishi"` | Japanese mainstream | -| `nissan.json` | `"Nissan"` | Japanese mainstream | -| `oldsmobile.json` | `"Oldsmobile"` | GM defunct | -| `plymouth.json` | `"Plymouth"` | Chrysler defunct | -| `polestar.json` | `"Polestar"` | Volvo electric | -| `pontiac.json` | `"Pontiac"` | GM defunct performance | -| `porsche.json` | `"Porsche"` | German sports car | -| `ram.json` | `"Ram"` | Stellantis trucks | -| `rivian.json` | `"Rivian"` | American electric trucks | -| `saab.json` | `"Saab"` | Swedish defunct | -| `saturn.json` | `"Saturn"` | GM defunct | -| `scion.json` | `"Scion"` | Toyota defunct youth | -| `smart.json` | `"Smart"` | Mercedes micro car | -| `subaru.json` | `"Subaru"` | Japanese AWD | -| `tesla.json` | `"Tesla"` | American electric | -| `toyota.json` | `"Toyota"` | Japanese mainstream | -| `volkswagen.json` | `"Volkswagen"` | German mainstream | -| `volvo.json` | `"Volvo"` | Swedish luxury | - -## Special Cases Implementation - -### All Caps Brands -```python -SPECIAL_CASES = { - 'Bmw': 'BMW', # Bayerische Motoren Werke - 'Gmc': 'GMC', # General Motors Company - 'Mini': 'MINI', # Brand stylization -} -``` - -### Custom Capitalizations -```python -CUSTOM_CAPS = { - 'Mclaren': 'McLaren', # Scottish naming convention -} -``` - -### Complete Special Cases Function -```python -def apply_special_cases(title_cased_name: str) -> str: - """Apply brand-specific capitalization rules""" - special_cases = { - 'Bmw': 'BMW', - 'Gmc': 'GMC', - 'Mini': 'MINI', - 'Mclaren': 'McLaren' - } - return special_cases.get(title_cased_name, title_cased_name) -``` - -## Validation Strategy - -### Cross-Reference with sources/makes.json -The existing `mvp-platform-services/vehicles/etl/sources/makes.json` contains the authoritative list: -```json -{ - "manufacturers": [ - "Acura", "Alfa Romeo", "Aston Martin", "Audi", "BMW", - "Bentley", "Buick", "Cadillac", "Chevrolet", "Chrysler", - ... - ] -} -``` - -### Validation Implementation -```python -class MakeNameMapper: - def __init__(self): - self.authoritative_makes = self.load_authoritative_makes() - - def load_authoritative_makes(self) -> Set[str]: - """Load makes list from sources/makes.json""" - with open('sources/makes.json') as f: - data = json.load(f) - return set(data['manufacturers']) - - def validate_mapping(self, filename: str, display_name: str) -> bool: - """Validate mapped name against authoritative list""" - return display_name in self.authoritative_makes - - def get_validation_report(self) -> ValidationReport: - """Generate complete validation report""" - mismatches = [] - json_files = glob.glob('sources/makes/*.json') - - for file_path in json_files: - filename = os.path.basename(file_path) - mapped_name = self.normalize_make_name(filename) - - if not self.validate_mapping(filename, mapped_name): - mismatches.append({ - 'filename': filename, - 'mapped_name': mapped_name, - 'status': 'NOT_FOUND_IN_AUTHORITATIVE' - }) - - return ValidationReport(mismatches=mismatches) -``` - -## Error Handling - -### Unknown Files -For JSON files not in the authoritative list: -1. **Log warning** with filename and mapped name -2. **Proceed with mapping** (don't fail) -3. **Include in validation report** - -### Filename Edge Cases -```python -def handle_edge_cases(filename: str) -> str: - """Handle unusual filename patterns""" - - # Remove multiple underscores - cleaned = re.sub(r'_+', '_', filename) - - # Handle special characters (future-proofing) - cleaned = re.sub(r'[^a-zA-Z0-9_]', '', cleaned) - - return cleaned -``` - -## Testing Requirements - -### Unit Tests -```python -def test_standard_mapping(): - mapper = MakeNameMapper() - assert mapper.normalize_make_name('toyota.json') == 'Toyota' - assert mapper.normalize_make_name('alfa_romeo.json') == 'Alfa Romeo' - -def test_special_cases(): - mapper = MakeNameMapper() - assert mapper.normalize_make_name('bmw.json') == 'BMW' - assert mapper.normalize_make_name('gmc.json') == 'GMC' - assert mapper.normalize_make_name('mclaren.json') == 'McLaren' - -def test_validation(): - mapper = MakeNameMapper() - assert mapper.validate_mapping('toyota.json', 'Toyota') == True - assert mapper.validate_mapping('fake.json', 'Fake Brand') == False -``` - -### Integration Tests -1. **Process all 55 files**: Ensure all map correctly -2. **Database integration**: Verify display names in database -3. **API response**: Confirm proper names in dropdown responses - -## Implementation Class - -### Complete MakeNameMapper Class -```python -import json -import glob -import os -from typing import Set, Dict, List -from dataclasses import dataclass - -@dataclass -class ValidationReport: - mismatches: List[Dict[str, str]] - total_files: int - valid_mappings: int - - @property - def success_rate(self) -> float: - return self.valid_mappings / self.total_files if self.total_files > 0 else 0.0 - -class MakeNameMapper: - def __init__(self, sources_dir: str = 'sources'): - self.sources_dir = sources_dir - self.authoritative_makes = self.load_authoritative_makes() - - self.special_cases = { - 'Bmw': 'BMW', - 'Gmc': 'GMC', - 'Mini': 'MINI', - 'Mclaren': 'McLaren' - } - - def normalize_make_name(self, filename: str) -> str: - """Convert filename to display name""" - # Remove .json extension - base_name = filename.replace('.json', '') - - # Replace underscores with spaces - spaced_name = base_name.replace('_', ' ') - - # Apply title case - title_cased = spaced_name.title() - - # Apply special cases - return self.special_cases.get(title_cased, title_cased) - - def get_all_mappings(self) -> Dict[str, str]: - """Get complete filename → display name mapping""" - mappings = {} - json_files = glob.glob(f'{self.sources_dir}/makes/*.json') - - for file_path in json_files: - filename = os.path.basename(file_path) - display_name = self.normalize_make_name(filename) - mappings[filename] = display_name - - return mappings - - def validate_all_mappings(self) -> ValidationReport: - """Validate all mappings against authoritative list""" - mappings = self.get_all_mappings() - mismatches = [] - - for filename, display_name in mappings.items(): - if display_name not in self.authoritative_makes: - mismatches.append({ - 'filename': filename, - 'mapped_name': display_name, - 'status': 'NOT_FOUND_IN_AUTHORITATIVE' - }) - - return ValidationReport( - mismatches=mismatches, - total_files=len(mappings), - valid_mappings=len(mappings) - len(mismatches) - ) -``` - -## Usage Examples - -### Basic Usage -```python -mapper = MakeNameMapper() - -# Single conversion -display_name = mapper.normalize_make_name('alfa_romeo.json') -print(display_name) # Output: "Alfa Romeo" - -# Get all mappings -all_mappings = mapper.get_all_mappings() -print(all_mappings['bmw.json']) # Output: "BMW" -``` - -### Validation Usage -```python -# Validate all mappings -report = mapper.validate_all_mappings() -print(f"Success rate: {report.success_rate:.1%}") -print(f"Mismatches: {len(report.mismatches)}") - -for mismatch in report.mismatches: - print(f"āš ļø {mismatch['filename']} → {mismatch['mapped_name']}") -``` \ No newline at end of file diff --git a/docs/changes/vehicles-dropdown-v2/06-cli-commands.md b/docs/changes/vehicles-dropdown-v2/06-cli-commands.md deleted file mode 100644 index 06e586a..0000000 --- a/docs/changes/vehicles-dropdown-v2/06-cli-commands.md +++ /dev/null @@ -1,328 +0,0 @@ -# CLI Commands - Manual JSON ETL - -## Overview -New CLI commands for processing JSON vehicle data into the PostgreSQL database. - -## Primary Command: `load-manual` - -### Basic Syntax -```bash -python -m etl load-manual [OPTIONS] -``` - -### Command Options - -#### Load Mode (`--mode`) -Controls how data is handled in the database: - -```bash -# Append mode (safe, default) -python -m etl load-manual --mode=append - -# Clear mode (destructive - removes existing data first) -python -m etl load-manual --mode=clear -``` - -**Mode Details:** -- **`append`** (default): Uses `ON CONFLICT DO NOTHING` - safe for existing data -- **`clear`**: Uses `TRUNCATE CASCADE` then insert - completely replaces existing data - -#### Specific Make Processing (`--make`) -Process only a specific make instead of all 55 files: - -```bash -# Process only Toyota -python -m etl load-manual --make=toyota - -# Process only BMW (uses filename format) -python -m etl load-manual --make=bmw - -# Process Alfa Romeo (underscore format from filename) -python -m etl load-manual --make=alfa_romeo -``` - -#### Validation Only (`--validate-only`) -Validate JSON files without loading to database: - -```bash -# Validate all JSON files -python -m etl load-manual --validate-only - -# Validate specific make -python -m etl load-manual --make=tesla --validate-only -``` - -#### Verbose Output (`--verbose`) -Enable detailed progress output: - -```bash -# Verbose processing -python -m etl load-manual --verbose - -# Quiet processing (errors only) -python -m etl load-manual --quiet -``` - -### Complete Command Examples - -```bash -# Standard usage - process all makes safely -python -m etl load-manual - -# Full reload - clear and rebuild entire database -python -m etl load-manual --mode=clear --verbose - -# Process specific make with validation -python -m etl load-manual --make=honda --mode=append --verbose - -# Validate before processing -python -m etl load-manual --validate-only -python -m etl load-manual --mode=clear # If validation passes -``` - -## Secondary Command: `validate-json` - -### Purpose -Standalone validation of JSON files without database operations. - -### Syntax -```bash -python -m etl validate-json [OPTIONS] -``` - -### Options - -```bash -# Validate all JSON files -python -m etl validate-json - -# Validate specific make -python -m etl validate-json --make=toyota - -# Generate detailed report -python -m etl validate-json --detailed-report - -# Export validation results to file -python -m etl validate-json --export-report=/tmp/validation.json -``` - -### Validation Checks -1. **JSON structure** validation -2. **Engine parsing** validation -3. **Make name mapping** validation -4. **Data completeness** checks -5. **Cross-reference** with authoritative makes list - -## Implementation Details - -### CLI Command Structure -Add to `etl/main.py`: - -```python -@cli.command() -@click.option('--mode', type=click.Choice(['clear', 'append']), - default='append', help='Database load mode') -@click.option('--make', help='Process specific make only (use filename format)') -@click.option('--validate-only', is_flag=True, - help='Validate JSON files without loading to database') -@click.option('--verbose', is_flag=True, help='Enable verbose output') -@click.option('--quiet', is_flag=True, help='Suppress non-error output') -def load_manual(mode, make, validate_only, verbose, quiet): - """Load vehicle data from JSON files""" - - if quiet: - logging.getLogger().setLevel(logging.ERROR) - elif verbose: - logging.getLogger().setLevel(logging.DEBUG) - - try: - pipeline = ManualJsonPipeline( - sources_dir=config.JSON_SOURCES_DIR, - load_mode=LoadMode(mode.upper()) - ) - - if validate_only: - result = pipeline.validate_all_json() - display_validation_report(result) - return - - result = pipeline.run_manual_pipeline(specific_make=make) - display_pipeline_result(result) - - if not result.success: - sys.exit(1) - - except Exception as e: - logger.error(f"Manual load failed: {e}") - sys.exit(1) - -@cli.command() -@click.option('--make', help='Validate specific make only') -@click.option('--detailed-report', is_flag=True, - help='Generate detailed validation report') -@click.option('--export-report', help='Export validation report to file') -def validate_json(make, detailed_report, export_report): - """Validate JSON files structure and data quality""" - - try: - validator = JsonValidator(sources_dir=config.JSON_SOURCES_DIR) - - if make: - result = validator.validate_make(make) - else: - result = validator.validate_all_makes() - - if detailed_report or export_report: - report = validator.generate_detailed_report(result) - - if export_report: - with open(export_report, 'w') as f: - json.dump(report, f, indent=2) - logger.info(f"Validation report exported to {export_report}") - else: - display_detailed_report(report) - else: - display_validation_summary(result) - - except Exception as e: - logger.error(f"JSON validation failed: {e}") - sys.exit(1) -``` - -## Output Examples - -### Successful Load Output -``` -$ python -m etl load-manual --mode=append --verbose - -šŸš€ Starting manual JSON ETL pipeline... -šŸ“ Processing 55 JSON files from sources/makes/ - -āœ… Make normalization validation passed (55/55) -āœ… Engine parsing validation passed (1,247 engines) - -šŸ“Š Processing makes: - ā”œā”€ā”€ toyota.json → Toyota (47 models, 203 engines, 312 trims) - ā”œā”€ā”€ ford.json → Ford (52 models, 189 engines, 298 trims) - ā”œā”€ā”€ chevrolet.json → Chevrolet (48 models, 167 engines, 287 trims) - └── ... (52 more makes) - -šŸ’¾ Database loading: - ā”œā”€ā”€ Makes: 55 loaded (0 duplicates) - ā”œā”€ā”€ Models: 2,847 loaded (23 duplicates) - ā”œā”€ā”€ Model Years: 18,392 loaded (105 duplicates) - ā”œā”€ā”€ Engines: 1,247 loaded (45 duplicates) - └── Trims: 12,058 loaded (234 duplicates) - -āœ… Manual JSON ETL completed successfully in 2m 34s -``` - -### Validation Output -``` -$ python -m etl validate-json - -šŸ“‹ JSON Validation Report - -āœ… File Structure: 55/55 files valid -āœ… Make Name Mapping: 55/55 mappings valid -āš ļø Engine Parsing: 1,201/1,247 engines parsed (46 unparseable) -āœ… Data Completeness: All required fields present - -šŸ” Issues Found: - ā”œā”€ā”€ Unparseable engines: - │ ā”œā”€ā”€ toyota.json: "Custom Hybrid System" (1 occurrence) - │ ā”œā”€ā”€ ferrari.json: "V12 Twin-Turbo Custom" (2 occurrences) - │ └── lamborghini.json: "V10 Plus" (43 occurrences) - └── Empty engine arrays: - ā”œā”€ā”€ tesla.json: 24 models with empty engines - └── lucid.json: 3 models with empty engines - -šŸ’” Recommendations: - • Review unparseable engine formats - • Electric vehicle handling will create default "Electric Motor" entries - -Overall Status: āœ… READY FOR PROCESSING -``` - -### Error Handling Output -``` -$ python -m etl load-manual --make=invalid_make - -āŒ Error: Make 'invalid_make' not found - -Available makes: - acura, alfa_romeo, aston_martin, audi, bentley, bmw, - buick, cadillac, chevrolet, chrysler, dodge, ferrari, - ... (showing first 20) - -šŸ’” Tip: Use 'python -m etl validate-json' to see all available makes -``` - -## Integration with Existing Commands - -### Command Compatibility -The new commands integrate seamlessly with existing ETL commands: - -```bash -# Existing MSSQL pipeline (unchanged) -python -m etl build-catalog - -# New manual JSON pipeline -python -m etl load-manual - -# Test connections (works for both) -python -m etl test - -# Scheduling (MSSQL only currently) -python -m etl schedule -``` - -### Configuration Integration -Uses existing config structure with new JSON-specific settings: - -```python -# In config.py -JSON_SOURCES_DIR: str = "sources/makes" -MANUAL_LOAD_DEFAULT_MODE: str = "append" -MANUAL_LOAD_BATCH_SIZE: int = 1000 -JSON_VALIDATION_STRICT: bool = False -``` - -## Help and Documentation - -### Built-in Help -```bash -# Main command help -python -m etl load-manual --help - -# All commands help -python -m etl --help -``` - -### Command Discovery -```bash -# List all available commands -python -m etl - -# Shows: -# Commands: -# build-catalog Build vehicle catalog from MSSQL database -# load-manual Load vehicle data from JSON files -# validate-json Validate JSON files structure and data quality -# schedule Start ETL scheduler (default mode) -# test Test database connections -# update Run ETL update -``` - -## Future Enhancements - -### Planned Command Options -- `--dry-run`: Show what would be processed without making changes -- `--since`: Process only files modified since timestamp -- `--parallel`: Enable parallel processing of makes -- `--rollback`: Rollback previous manual load operation - -### Advanced Validation Options -- `--strict-parsing`: Fail on any engine parsing errors -- `--cross-validate`: Compare JSON data against MSSQL data where available -- `--performance-test`: Benchmark processing performance \ No newline at end of file diff --git a/docs/changes/vehicles-dropdown-v2/08-status-tracking.md b/docs/changes/vehicles-dropdown-v2/08-status-tracking.md deleted file mode 100644 index 6124026..0000000 --- a/docs/changes/vehicles-dropdown-v2/08-status-tracking.md +++ /dev/null @@ -1,403 +0,0 @@ -# Implementation Status Tracking - -## Current Status: ALL PHASES COMPLETE - READY FOR PRODUCTION šŸŽ‰ - -**Last Updated**: Phase 6 complete with full CLI integration implemented -**Current Phase**: Phase 6 complete - All implementation phases finished -**Next Phase**: Production testing and deployment (optional) - -## Project Phases Overview - -| Phase | Status | Progress | Next Steps | -|-------|--------|----------|------------| -| šŸ“š Documentation | āœ… Complete | 100% | Ready for implementation | -| šŸ”§ Core Utilities | āœ… Complete | 100% | Validated and tested | -| šŸ“Š Data Extraction | āœ… Complete | 100% | Fully tested and validated | -| šŸ’¾ Data Loading | āœ… Complete | 100% | Database integration ready | -| šŸš€ Pipeline Integration | āœ… Complete | 100% | End-to-end workflow ready | -| šŸ–„ļø CLI Integration | āœ… Complete | 100% | Full CLI commands implemented | -| āœ… Testing & Validation | ā³ Optional | 0% | Production testing available | - -## Detailed Status - -### āœ… Phase 1: Foundation Documentation (COMPLETE) - -#### Completed Items -- āœ… **Project directory structure** created at `docs/changes/vehicles-dropdown-v2/` -- āœ… **README.md** - Main overview and AI handoff instructions -- āœ… **01-analysis-findings.md** - JSON data patterns and structure analysis -- āœ… **02-implementation-plan.md** - Detailed technical roadmap -- āœ… **03-engine-spec-parsing.md** - Engine parsing rules with L→I normalization -- āœ… **04-make-name-mapping.md** - Make name conversion rules and validation -- āœ… **06-cli-commands.md** - CLI command design and usage examples -- āœ… **08-status-tracking.md** - This implementation tracking document - -#### Documentation Quality Check -- āœ… All critical requirements documented (L→I normalization, make names, etc.) -- āœ… Complete engine parsing patterns documented -- āœ… All 55 make files catalogued with naming rules -- āœ… Database schema integration documented -- āœ… CLI commands designed with comprehensive options -- āœ… AI handoff instructions complete - -### āœ… Phase 2: Core Utilities (COMPLETE) - -#### Completed Items -1. **MakeNameMapper** (`etl/utils/make_name_mapper.py`) - - Status: āœ… Complete - - Implementation: Filename to display name conversion with special cases - - Testing: Comprehensive unit tests with validation against authoritative list - - Quality: 100% make name validation success (55/55 files) - -2. **EngineSpecParser** (`etl/utils/engine_spec_parser.py`) - - Status: āœ… Complete - - Implementation: Complete engine parsing with L→I normalization - - Critical Features: L→I conversion, W-configuration support, hybrid detection - - Testing: Extensive unit tests with real-world validation - - Quality: 99.9% parsing success (67,568/67,633 engines) - -3. **Validation and Quality Assurance** - - Status: āœ… Complete - - Created comprehensive validation script (`validate_utilities.py`) - - Validated against all 55 JSON files (67,633 engines processed) - - Fixed W-configuration engine support (VW Group, Bentley) - - Fixed MINI make validation issue - - L→I normalization: 26,222 cases processed successfully - -#### Implementation Results -- **Make Name Validation**: 100% success (55/55 files) -- **Engine Parsing**: 99.9% success (67,568/67,633 engines) -- **L→I Normalization**: Working perfectly (26,222 cases) -- **Electric Vehicle Handling**: 2,772 models with empty engines processed -- **W-Configuration Support**: 124 W8/W12 engines now supported - -### āœ… Phase 3: Data Extraction (COMPLETE) - -#### Completed Components -1. **JsonExtractor** (`etl/extractors/json_extractor.py`) - - Status: āœ… Complete - - Implementation: Full make/model/year/trim/engine extraction with normalization - - Dependencies: MakeNameMapper, EngineSpecParser (āœ… Integrated) - - Features: JSON validation, data structures, progress tracking - - Quality: 100% extraction success on all 55 makes - -2. **ElectricVehicleHandler** (integrated into JsonExtractor) - - Status: āœ… Complete - - Implementation: Automatic detection and handling of empty engines arrays - - Purpose: Create default "Electric Motor" for Tesla and other EVs - - Results: 917 electric models properly handled - -3. **Data Structure Validation** - - Status: āœ… Complete - - Implementation: Comprehensive JSON structure validation - - Features: Error handling, warnings, data quality reporting - -4. **Unit Testing and Validation** - - Status: āœ… Complete - - Created comprehensive unit test suite (`tests/test_json_extractor.py`) - - Validated against all 55 JSON files - - Results: 2,644 models, 5,199 engines extracted successfully - -#### Implementation Results -- **File Processing**: 100% success (55/55 files) -- **Data Extraction**: 2,644 models, 5,199 engines -- **Electric Vehicle Handling**: 917 electric models -- **Data Quality**: Zero extraction errors -- **Integration**: MakeNameMapper and EngineSpecParser fully integrated -- **L→I Normalization**: Working seamlessly in extraction pipeline - -### āœ… Phase 4: Data Loading (COMPLETE) - -#### Completed Components -1. **JsonManualLoader** (`etl/loaders/json_manual_loader.py`) - - Status: āœ… Complete - - Implementation: Full PostgreSQL integration with referential integrity - - Features: Clear/append modes, duplicate handling, batch processing - - Database Support: Complete vehicles schema integration - -2. **Load Modes and Conflict Resolution** - - Status: āœ… Complete - - CLEAR mode: Truncate and reload (destructive, fast) - - APPEND mode: Insert with conflict handling (safe, incremental) - - Duplicate detection and resolution for all entity types - -3. **Database Integration** - - Status: āœ… Complete - - Full vehicles schema support (make→model→model_year→trim→engine) - - Referential integrity maintenance and validation - - Batch processing with progress tracking - -4. **Unit Testing and Validation** - - Status: āœ… Complete - - Comprehensive unit test suite (`tests/test_json_manual_loader.py`) - - Mock database testing for all loading scenarios - - Error handling and rollback testing - -#### Implementation Results -- **Database Schema**: Full vehicles schema support with proper referential integrity -- **Loading Modes**: Both CLEAR and APPEND modes implemented -- **Conflict Resolution**: Duplicate handling for makes, models, engines, and trims -- **Error Handling**: Robust error handling with statistics and reporting -- **Performance**: Batch processing with configurable batch sizes -- **Validation**: Referential integrity validation and reporting - -### āœ… Phase 5: Pipeline Integration (COMPLETE) - -#### Completed Components -1. **ManualJsonPipeline** (`etl/pipelines/manual_json_pipeline.py`) - - Status: āœ… Complete - - Implementation: Full end-to-end workflow coordination (extraction → loading) - - Dependencies: JsonExtractor, JsonManualLoader (āœ… Integrated) - - Features: Progress tracking, error handling, comprehensive reporting - -2. **Pipeline Configuration and Options** - - Status: āœ… Complete - - PipelineConfig class with full configuration management - - Clear/append mode selection and override capabilities - - Source directory configuration and validation - - Progress tracking with real-time updates and ETA calculation - -3. **Performance Monitoring and Metrics** - - Status: āœ… Complete - - Real-time performance tracking (files/sec, records/sec) - - Phase-based progress tracking with detailed statistics - - Duration tracking and performance optimization - - Comprehensive execution reporting - -4. **Integration Architecture** - - Status: āœ… Complete - - Full workflow coordination: extraction → loading → validation - - Error handling across all pipeline phases - - Rollback and recovery mechanisms - - Source file statistics and analysis - -#### Implementation Results -- **End-to-End Workflow**: Complete extraction → loading → validation pipeline -- **Progress Tracking**: Real-time progress with ETA calculation and phase tracking -- **Performance Metrics**: Files/sec and records/sec monitoring with optimization -- **Configuration Management**: Flexible pipeline configuration with mode overrides -- **Error Handling**: Comprehensive error handling across all pipeline phases -- **Reporting**: Detailed execution reports with success rates and statistics - -### āœ… Phase 6: CLI Integration (COMPLETE) - -#### Completed Components -1. **CLI Command Implementation** (`etl/main.py`) - - Status: āœ… Complete - - Implementation: Full integration with existing Click-based CLI structure - - Dependencies: ManualJsonPipeline (āœ… Integrated) - - Commands: load-manual and validate-json with comprehensive options - -2. **load-manual Command** - - Status: āœ… Complete - - Full option set: sources-dir, mode, progress, validate, batch-size, dry-run, verbose - - Mode selection: clear (destructive) and append (safe) with confirmation - - Progress tracking: Real-time progress with ETA calculation - - Dry-run mode: Validation without database changes - -3. **validate-json Command** - - Status: āœ… Complete - - JSON file validation and structure checking - - Detailed statistics and data quality insights - - Verbose mode with top makes, error reports, and engine distribution - - Performance testing and validation - -4. **Help System and User Experience** - - Status: āœ… Complete - - Comprehensive help text with usage examples - - User-friendly error messages and guidance - - Interactive confirmation for destructive operations - - Colored output and professional formatting - -#### Implementation Results -- **CLI Integration**: Seamless integration with existing ETL commands -- **Command Options**: Full option coverage with sensible defaults -- **User Experience**: Professional CLI with help, examples, and error guidance -- **Error Handling**: Comprehensive error handling with helpful messages -- **Progress Tracking**: Real-time progress with ETA and performance metrics -- **Validation**: Dry-run and validate-json commands for safe operations - -### ā³ Phase 7: Testing & Validation (OPTIONAL) - -#### Available Components -- Comprehensive unit test suites (already implemented for all phases) -- Integration testing framework ready -- Data validation available via CLI commands -- Performance monitoring built into pipeline - -#### Status -- All core functionality implemented and unit tested -- Production testing can be performed using CLI commands -- No blockers - ready for production deployment - -## Implementation Readiness Checklist - -### āœ… Ready for Implementation -- [x] Complete understanding of JSON data structure (55 files analyzed) -- [x] Engine parsing requirements documented (L→I normalization critical) -- [x] Make name mapping rules documented (underscore→space, special cases) -- [x] Database schema understood (PostgreSQL vehicles schema) -- [x] CLI design completed (load-manual, validate-json commands) -- [x] Integration strategy documented (existing MSSQL pipeline compatibility) - -### šŸ”§ Implementation Dependencies -- Current ETL system at `mvp-platform-services/vehicles/etl/` -- PostgreSQL database with vehicles schema -- Python environment with existing ETL dependencies -- Access to JSON files at `mvp-platform-services/vehicles/etl/sources/makes/` - -### šŸ“‹ Pre-Implementation Validation -Before starting implementation, validate: -- [ ] All 55 JSON files are accessible and readable -- [ ] PostgreSQL schema matches documentation -- [ ] Existing ETL pipeline is working (MSSQL pipeline) -- [ ] Development environment setup complete - -## AI Handoff Instructions - -### For Continuing This Work: - -#### Immediate Next Steps -1. **Load Phase 2 context**: - ```bash - # Load these files for implementation context - docs/changes/vehicles-dropdown-v2/04-make-name-mapping.md - docs/changes/vehicles-dropdown-v2/02-implementation-plan.md - mvp-platform-services/vehicles/etl/utils/make_filter.py # Reference existing pattern - ``` - -2. **Start with MakeNameMapper**: - - Create `etl/utils/make_name_mapper.py` - - Implement filename→display name conversion - - Add validation against `sources/makes.json` - - Create unit tests - -3. **Then implement EngineSpecParser**: - - Create `etl/utils/engine_spec_parser.py` - - **CRITICAL**: L→I configuration normalization - - Hybrid/electric detection patterns - - Comprehensive unit tests - -#### Context Loading Priority -1. **Current status**: This file (08-status-tracking.md) -2. **Implementation plan**: 02-implementation-plan.md -3. **Specific component docs**: Based on what you're implementing -4. **Original analysis**: 01-analysis-findings.md for data patterns - -### For Understanding Data Patterns: -1. Load 01-analysis-findings.md for JSON structure analysis -2. Load 03-engine-spec-parsing.md for parsing rules -3. Examine sample JSON files: toyota.json, tesla.json, subaru.json - -### For Understanding Requirements: -1. README.md - Critical requirements summary -2. 04-make-name-mapping.md - Make name normalization rules -3. 06-cli-commands.md - CLI interface design - -## Success Metrics - -### Phase Completion Criteria -- **Phase 2**: MakeNameMapper and EngineSpecParser working with unit tests -- **Phase 3**: JSON extraction working for all 55 files -- **Phase 4**: Database loading working in clear/append modes -- **Phase 5**: End-to-end pipeline processing all makes successfully -- **Phase 6**: CLI commands working with all options -- **Phase 7**: Comprehensive test coverage and validation - -### Final Success Criteria -- [ ] Process all 55 JSON files without errors -- [ ] Make names properly normalized (alfa_romeo.json → "Alfa Romeo") -- [ ] Engine parsing with L→I normalization working correctly -- [ ] Electric vehicles handled properly (default engines created) -- [ ] Clear/append modes working without data corruption -- [ ] API endpoints return data loaded from JSON sources -- [ ] Performance acceptable (<5 minutes for full load) -- [ ] Zero breaking changes to existing MSSQL pipeline - -## Risk Tracking - -### Current Risks: LOW -- **Data compatibility**: Well analyzed, patterns understood -- **Implementation complexity**: Moderate, but well documented -- **Integration risk**: Low, maintains existing pipeline compatibility - -### Risk Mitigation -- **Comprehensive documentation**: Reduces implementation risk -- **Incremental phases**: Allows early validation and course correction -- **Unit testing focus**: Ensures component reliability - -## Change Log - -### Initial Documentation (This Session) -- Created complete documentation structure -- Analyzed all 55 JSON files for patterns -- Documented critical requirements (L→I normalization, make mapping) -- Designed CLI interface and implementation approach -- Created AI-friendly handoff documentation - -### Documentation Phase Completion (Current Session) -- āœ… Created complete documentation structure at `docs/changes/vehicles-dropdown-v2/` -- āœ… Analyzed all 55 JSON files for data patterns and structure -- āœ… Documented critical L→I normalization requirement -- āœ… Mapped all make name conversions with special cases -- āœ… Designed complete CLI interface (load-manual, validate-json) -- āœ… Created comprehensive code examples with working demonstrations -- āœ… Established AI-friendly handoff documentation -- āœ… **STATUS**: Documentation phase complete, ready for implementation - -### Phase 2 Implementation Complete (Previous Session) -- āœ… Implemented MakeNameMapper (`etl/utils/make_name_mapper.py`) -- āœ… Implemented EngineSpecParser (`etl/utils/engine_spec_parser.py`) with L→I normalization -- āœ… Created comprehensive unit tests for both utilities -- āœ… Validated against all 55 JSON files with excellent results -- āœ… Fixed W-configuration engine support (VW Group, Bentley W8/W12 engines) -- āœ… Fixed MINI make validation issue in authoritative makes list -- āœ… **STATUS**: Phase 2 complete with 100% make validation and 99.9% engine parsing success - -### Phase 3 Implementation Complete (Previous Session) -- āœ… Implemented JsonExtractor (`etl/extractors/json_extractor.py`) -- āœ… Integrated make name normalization and engine parsing seamlessly -- āœ… Implemented electric vehicle handling (empty engines arrays → Electric Motor) -- āœ… Created comprehensive unit tests (`tests/test_json_extractor.py`) -- āœ… Validated against all 55 JSON files with 100% success -- āœ… Extracted 2,644 models and 5,199 engines successfully -- āœ… Properly handled 917 electric models across all makes -- āœ… **STATUS**: Phase 3 complete with 100% extraction success and zero errors - -### Phase 4 Implementation Complete (Previous Session) -- āœ… Implemented JsonManualLoader (`etl/loaders/json_manual_loader.py`) -- āœ… Full PostgreSQL integration with referential integrity maintenance -- āœ… Clear/append modes with comprehensive duplicate handling -- āœ… Batch processing with performance optimization -- āœ… Created comprehensive unit tests (`tests/test_json_manual_loader.py`) -- āœ… Database schema integration with proper foreign key relationships -- āœ… Referential integrity validation and error reporting -- āœ… **STATUS**: Phase 4 complete with full database integration ready - -### Phase 5 Implementation Complete (Previous Session) -- āœ… Implemented ManualJsonPipeline (`etl/pipelines/manual_json_pipeline.py`) -- āœ… End-to-end workflow coordination (extraction → loading → validation) -- āœ… Progress tracking with real-time updates and ETA calculation -- āœ… Performance monitoring (files/sec, records/sec) with optimization -- āœ… Pipeline configuration management with mode overrides -- āœ… Comprehensive error handling across all pipeline phases -- āœ… Detailed execution reporting with success rates and statistics -- āœ… **STATUS**: Phase 5 complete with full pipeline orchestration ready - -### Phase 6 Implementation Complete (This Session) -- āœ… Implemented CLI commands in `etl/main.py` (load-manual, validate-json) -- āœ… Full integration with existing Click-based CLI framework -- āœ… Comprehensive command-line options and configuration management -- āœ… Interactive user experience with confirmations and help system -- āœ… Progress tracking integration with real-time CLI updates -- āœ… Dry-run mode for safe validation without database changes -- āœ… Verbose reporting with detailed statistics and error messages -- āœ… Professional CLI formatting with colored output and user guidance -- āœ… **STATUS**: Phase 6 complete - Full CLI integration ready for production - -### All Implementation Phases Complete -**Current Status**: Manual JSON processing system fully implemented and ready -**Available Commands**: -- `python -m etl load-manual` - Load vehicle data from JSON files -- `python -m etl validate-json` - Validate JSON structure and content -**Next Steps**: Production testing and deployment (optional) \ No newline at end of file diff --git a/docs/changes/vehicles-dropdown-v2/README.md b/docs/changes/vehicles-dropdown-v2/README.md deleted file mode 100644 index e0fa942..0000000 --- a/docs/changes/vehicles-dropdown-v2/README.md +++ /dev/null @@ -1,99 +0,0 @@ -# Vehicles Dropdown V2 - Manual JSON ETL Implementation - -## Overview -This directory contains comprehensive documentation for implementing manual JSON processing in the MVP Platform Vehicles ETL system. The goal is to add capability to process 55 JSON files containing vehicle data directly, bypassing the MSSQL source dependency. - -## Quick Start for AI Instances - -### Current State (As of Implementation Start) -- **55 JSON files** exist in `mvp-platform-services/vehicles/etl/sources/makes/` -- Current ETL only supports MSSQL → PostgreSQL pipeline -- Need to add JSON → PostgreSQL capability - -### Key Files to Load for Context -```bash -# Load these files for complete understanding -mvp-platform-services/vehicles/etl/sources/makes/toyota.json # Large file example -mvp-platform-services/vehicles/etl/sources/makes/tesla.json # Electric vehicle example -mvp-platform-services/vehicles/etl/pipeline.py # Current pipeline -mvp-platform-services/vehicles/etl/loaders/postgres_loader.py # Current loader -mvp-platform-services/vehicles/sql/schema/001_schema.sql # Target schema -``` - -### Implementation Status -See [08-status-tracking.md](08-status-tracking.md) for current progress. - -## Critical Requirements Discovered - -### 1. Make Name Normalization -- JSON filenames: `alfa_romeo.json`, `land_rover.json` -- Database display: `"Alfa Romeo"`, `"Land Rover"` (spaces, title case) - -### 2. Engine Configuration Normalization -- **CRITICAL**: `L3` → `I3` (L-configuration treated as Inline) -- Standard format: `{displacement}L {config}{cylinders} {descriptions}` -- Examples: `"1.5L L3"` → `"1.5L I3"`, `"2.4L H4"` (Subaru Boxer) - -### 3. Hybrid/Electric Patterns Found -- `"PLUG-IN HYBRID EV- (PHEV)"` - Plug-in hybrid -- `"FULL HYBRID EV- (FHEV)"` - Full hybrid -- `"ELECTRIC"` - Pure electric -- `"FLEX"` - Flex-fuel -- Empty engines arrays for Tesla/electric vehicles - -### 4. Transmission Limitation -- **Manual selection only**: Automatic/Manual choice -- **No automatic detection** from JSON data - -## Document Structure - -| File | Purpose | Status | -|------|---------|--------| -| [01-analysis-findings.md](01-analysis-findings.md) | JSON data patterns analysis | ā³ Pending | -| [02-implementation-plan.md](02-implementation-plan.md) | Technical roadmap | ā³ Pending | -| [03-engine-spec-parsing.md](03-engine-spec-parsing.md) | Engine parsing rules | ā³ Pending | -| [04-make-name-mapping.md](04-make-name-mapping.md) | Make name normalization | ā³ Pending | -| [05-database-schema-updates.md](05-database-schema-updates.md) | Schema change requirements | ā³ Pending | -| [06-cli-commands.md](06-cli-commands.md) | New CLI command design | ā³ Pending | -| [07-testing-strategy.md](07-testing-strategy.md) | Testing and validation approach | ā³ Pending | -| [08-status-tracking.md](08-status-tracking.md) | Implementation progress tracker | ā³ Pending | - -## AI Handoff Instructions - -### To Continue This Work: -1. **Read this README.md** - Current state and critical requirements -2. **Check [08-status-tracking.md](08-status-tracking.md)** - See what's completed/in-progress -3. **Review [02-implementation-plan.md](02-implementation-plan.md)** - Technical roadmap -4. **Load specific documentation** based on what you're implementing - -### To Understand the Data: -1. **Load [01-analysis-findings.md](01-analysis-findings.md)** - JSON structure analysis -2. **Load [03-engine-spec-parsing.md](03-engine-spec-parsing.md)** - Engine parsing rules -3. **Load [04-make-name-mapping.md](04-make-name-mapping.md)** - Make name conversion rules - -### To Start Coding: -1. **Check status tracker** - See what needs to be implemented next -2. **Load implementation plan** - Step-by-step technical guide -3. **Reference examples/** directory - Code samples and patterns - -## Success Criteria -- [ ] New CLI command: `python -m etl load-manual` -- [ ] Process all 55 JSON make files -- [ ] Proper make name normalization (`alfa_romeo.json` → `"Alfa Romeo"`) -- [ ] Engine spec parsing with L→I normalization -- [ ] Clear/append mode support with duplicate handling -- [ ] Electric vehicle support (default engines for empty arrays) -- [ ] Integration with existing PostgreSQL schema - -## Architecture Integration -This feature integrates with: -- **Existing ETL pipeline**: `mvp-platform-services/vehicles/etl/` -- **PostgreSQL schema**: `vehicles` schema with make/model/engine tables -- **Platform API**: Hierarchical dropdown endpoints remain unchanged -- **Application service**: No changes required - -## Notes for Future Implementations -- Maintain compatibility with existing MSSQL pipeline -- Follow existing code patterns in `etl/` directory -- Use existing `PostgreSQLLoader` where possible -- Preserve referential integrity during data loading \ No newline at end of file diff --git a/docs/changes/vehicles-dropdown-v2/examples/engine-parsing-examples.py b/docs/changes/vehicles-dropdown-v2/examples/engine-parsing-examples.py deleted file mode 100644 index 9b92e59..0000000 --- a/docs/changes/vehicles-dropdown-v2/examples/engine-parsing-examples.py +++ /dev/null @@ -1,314 +0,0 @@ -#!/usr/bin/env python3 -""" -Engine Specification Parsing Examples - -This file contains comprehensive examples of engine parsing patterns -found in the JSON vehicle data, demonstrating the L→I normalization -and hybrid/electric detection requirements. - -Usage: - python engine-parsing-examples.py -""" - -import re -from dataclasses import dataclass -from typing import Optional, List - - -@dataclass -class EngineSpec: - """Parsed engine specification""" - displacement_l: Optional[float] - configuration: str # I, V, H, Electric - cylinders: Optional[int] - fuel_type: str # Gasoline, Hybrid, Electric, Flex Fuel - aspiration: str # Natural, Turbo, Supercharged - raw_string: str - - -class EngineSpecParser: - """Engine specification parser with L→I normalization""" - - def __init__(self): - # Primary pattern: {displacement}L {config}{cylinders} - self.engine_pattern = re.compile(r'(\d+\.?\d*)L\s+([IVHL])(\d+)') - - # Hybrid patterns - self.hybrid_patterns = [ - re.compile(r'PLUG-IN HYBRID EV-?\s*\(PHEV\)', re.IGNORECASE), - re.compile(r'FULL HYBRID EV-?\s*\(FHEV\)', re.IGNORECASE), - re.compile(r'HYBRID', re.IGNORECASE), - ] - - # Other fuel type patterns - self.fuel_patterns = [ - (re.compile(r'FLEX', re.IGNORECASE), 'Flex Fuel'), - (re.compile(r'ELECTRIC', re.IGNORECASE), 'Electric'), - ] - - # Aspiration patterns - self.aspiration_patterns = [ - (re.compile(r'TURBO', re.IGNORECASE), 'Turbocharged'), - (re.compile(r'SUPERCHARGED|SC', re.IGNORECASE), 'Supercharged'), - ] - - def normalize_configuration(self, config: str) -> str: - """CRITICAL: Convert L to I (L-configuration becomes Inline)""" - return 'I' if config == 'L' else config - - def extract_fuel_type(self, engine_str: str) -> str: - """Extract fuel type from engine string""" - # Check hybrid patterns first (most specific) - for pattern in self.hybrid_patterns: - if pattern.search(engine_str): - if 'PLUG-IN' in engine_str.upper(): - return 'Plug-in Hybrid' - elif 'FULL' in engine_str.upper(): - return 'Full Hybrid' - else: - return 'Hybrid' - - # Check other fuel types - for pattern, fuel_type in self.fuel_patterns: - if pattern.search(engine_str): - return fuel_type - - return 'Gasoline' # Default - - def extract_aspiration(self, engine_str: str) -> str: - """Extract aspiration from engine string""" - for pattern, aspiration in self.aspiration_patterns: - if pattern.search(engine_str): - return aspiration - return 'Natural' # Default - - def parse_engine_string(self, engine_str: str) -> EngineSpec: - """Parse complete engine specification""" - match = self.engine_pattern.match(engine_str) - - if not match: - # Handle unparseable engines - return self.create_fallback_engine(engine_str) - - displacement = float(match.group(1)) - config = self.normalize_configuration(match.group(2)) # L→I here! - cylinders = int(match.group(3)) - - fuel_type = self.extract_fuel_type(engine_str) - aspiration = self.extract_aspiration(engine_str) - - return EngineSpec( - displacement_l=displacement, - configuration=config, - cylinders=cylinders, - fuel_type=fuel_type, - aspiration=aspiration, - raw_string=engine_str - ) - - def create_fallback_engine(self, raw_string: str) -> EngineSpec: - """Create fallback for unparseable engines""" - return EngineSpec( - displacement_l=None, - configuration="Unknown", - cylinders=None, - fuel_type="Unknown", - aspiration="Natural", - raw_string=raw_string - ) - - def create_electric_motor(self) -> EngineSpec: - """Create default electric motor for empty engines arrays""" - return EngineSpec( - displacement_l=None, - configuration="Electric", - cylinders=None, - fuel_type="Electric", - aspiration=None, - raw_string="Electric Motor" - ) - - -def demonstrate_engine_parsing(): - """Demonstrate engine parsing with real examples from JSON files""" - - parser = EngineSpecParser() - - # Test cases from actual JSON data - test_engines = [ - # Standard engines - "2.0L I4", - "3.5L V6", - "5.6L V8", - - # L→I normalization examples (CRITICAL) - "1.5L L3", - "2.0L L4", - "1.2L L3 FULL HYBRID EV- (FHEV)", - - # Subaru Boxer engines - "2.4L H4", - "2.0L H4", - - # Hybrid examples from Nissan - "2.5L I4 FULL HYBRID EV- (FHEV)", - "1.5L L3 PLUG-IN HYBRID EV- (PHEV)", - - # Flex fuel examples - "5.6L V8 FLEX", - "4.0L V6 FLEX", - - # Electric examples - "1.8L I4 ELECTRIC", - - # Unparseable examples (should create fallback) - "Custom Hybrid System", - "V12 Twin-Turbo Custom", - "V10 Plus", - ] - - print("šŸ”§ Engine Specification Parsing Examples") - print("=" * 50) - - for engine_str in test_engines: - spec = parser.parse_engine_string(engine_str) - - print(f"\nInput: \"{engine_str}\"") - print(f" Displacement: {spec.displacement_l}L") - print(f" Configuration: {spec.configuration}") - print(f" Cylinders: {spec.cylinders}") - print(f" Fuel Type: {spec.fuel_type}") - print(f" Aspiration: {spec.aspiration}") - - # Highlight L→I normalization - if 'L' in engine_str and spec.configuration == 'I': - print(f" šŸŽÆ L→I NORMALIZED: L{spec.cylinders} became I{spec.cylinders}") - - # Demonstrate electric vehicle handling - print(f"\n\n⚔ Electric Vehicle Default Engine:") - electric_spec = parser.create_electric_motor() - print(f" Name: {electric_spec.raw_string}") - print(f" Configuration: {electric_spec.configuration}") - print(f" Fuel Type: {electric_spec.fuel_type}") - - -def demonstrate_l_to_i_normalization(): - """Specifically demonstrate L→I normalization requirement""" - - parser = EngineSpecParser() - - print("\n\nšŸŽÆ L→I Configuration Normalization") - print("=" * 40) - print("CRITICAL REQUIREMENT: All L-configurations must become I (Inline)") - - l_configuration_examples = [ - "1.5L L3", - "2.0L L4", - "1.2L L3 FULL HYBRID EV- (FHEV)", - "1.5L L3 PLUG-IN HYBRID EV- (PHEV)", - ] - - for engine_str in l_configuration_examples: - spec = parser.parse_engine_string(engine_str) - original_config = engine_str.split()[1][0] # Extract L from "L3" - - print(f"\nOriginal: \"{engine_str}\"") - print(f" Input Configuration: {original_config}{spec.cylinders}") - print(f" Output Configuration: {spec.configuration}{spec.cylinders}") - print(f" āœ… Normalized: {original_config}→{spec.configuration}") - - -def demonstrate_database_storage(): - """Show how parsed engines map to database records""" - - parser = EngineSpecParser() - - print("\n\nšŸ’¾ Database Storage Examples") - print("=" * 35) - print("SQL: INSERT INTO vehicles.engine (name, code, displacement_l, cylinders, fuel_type, aspiration)") - - examples = [ - "2.0L I4", - "1.5L L3 PLUG-IN HYBRID EV- (PHEV)", # L→I case - "2.4L H4", # Subaru Boxer - "5.6L V8 FLEX", - ] - - for engine_str in examples: - spec = parser.parse_engine_string(engine_str) - - # Format as SQL INSERT values - sql_values = ( - f"('{spec.raw_string}', NULL, {spec.displacement_l}, " - f"{spec.cylinders}, '{spec.fuel_type}', '{spec.aspiration}')" - ) - - print(f"\nEngine: \"{engine_str}\"") - print(f" SQL: VALUES {sql_values}") - - if 'L' in engine_str and spec.configuration == 'I': - print(f" šŸŽÆ Note: L{spec.cylinders} normalized to I{spec.cylinders}") - - # Electric motor example - electric_spec = parser.create_electric_motor() - sql_values = ( - f"('{electric_spec.raw_string}', NULL, NULL, " - f"NULL, '{electric_spec.fuel_type}', NULL)" - ) - print(f"\nElectric Vehicle:") - print(f" SQL: VALUES {sql_values}") - - -def run_validation_tests(): - """Run validation tests to ensure parsing works correctly""" - - parser = EngineSpecParser() - - print("\n\nāœ… Validation Tests") - print("=" * 20) - - # Test L→I normalization - test_cases = [ - ("1.5L L3", "I", 3), - ("2.0L L4", "I", 4), - ("1.2L L3 FULL HYBRID EV- (FHEV)", "I", 3), - ] - - for engine_str, expected_config, expected_cylinders in test_cases: - spec = parser.parse_engine_string(engine_str) - - assert spec.configuration == expected_config, \ - f"Expected {expected_config}, got {spec.configuration}" - assert spec.cylinders == expected_cylinders, \ - f"Expected {expected_cylinders} cylinders, got {spec.cylinders}" - - print(f"āœ… {engine_str} → {spec.configuration}{spec.cylinders}") - - # Test hybrid detection - hybrid_cases = [ - ("2.5L I4 FULL HYBRID EV- (FHEV)", "Full Hybrid"), - ("1.5L L3 PLUG-IN HYBRID EV- (PHEV)", "Plug-in Hybrid"), - ] - - for engine_str, expected_fuel_type in hybrid_cases: - spec = parser.parse_engine_string(engine_str) - assert spec.fuel_type == expected_fuel_type, \ - f"Expected {expected_fuel_type}, got {spec.fuel_type}" - print(f"āœ… {engine_str} → {spec.fuel_type}") - - print("\nšŸŽ‰ All validation tests passed!") - - -if __name__ == "__main__": - demonstrate_engine_parsing() - demonstrate_l_to_i_normalization() - demonstrate_database_storage() - run_validation_tests() - - print("\n\nšŸ“‹ Summary") - print("=" * 10) - print("āœ… Engine parsing patterns implemented") - print("āœ… L→I normalization working correctly") - print("āœ… Hybrid/electric detection functional") - print("āœ… Database storage format validated") - print("\nšŸš€ Ready for integration into ETL system!") \ No newline at end of file diff --git a/docs/changes/vehicles-dropdown-v2/examples/make-mapping-examples.py b/docs/changes/vehicles-dropdown-v2/examples/make-mapping-examples.py deleted file mode 100644 index 55d8eaa..0000000 --- a/docs/changes/vehicles-dropdown-v2/examples/make-mapping-examples.py +++ /dev/null @@ -1,334 +0,0 @@ -#!/usr/bin/env python3 -""" -Make Name Mapping Examples - -This file demonstrates the complete make name normalization process, -converting JSON filenames to proper display names for the database. - -Usage: - python make-mapping-examples.py -""" - -import json -import glob -import os -from typing import Dict, Set, List, Tuple -from dataclasses import dataclass - - -@dataclass -class ValidationReport: - """Make name validation report""" - total_files: int - valid_mappings: int - mismatches: List[Dict[str, str]] - - @property - def success_rate(self) -> float: - return self.valid_mappings / self.total_files if self.total_files > 0 else 0.0 - - -class MakeNameMapper: - """Convert JSON filenames to proper make display names""" - - def __init__(self): - # Special capitalization cases - self.special_cases = { - 'Bmw': 'BMW', # Bayerische Motoren Werke - 'Gmc': 'GMC', # General Motors Company - 'Mini': 'MINI', # Brand styling - 'Mclaren': 'McLaren', # Scottish naming convention - } - - # Authoritative makes list (would be loaded from sources/makes.json) - self.authoritative_makes = { - 'Acura', 'Alfa Romeo', 'Aston Martin', 'Audi', 'BMW', 'Bentley', - 'Buick', 'Cadillac', 'Chevrolet', 'Chrysler', 'Dodge', 'Ferrari', - 'Fiat', 'Ford', 'Genesis', 'Geo', 'GMC', 'Honda', 'Hummer', - 'Hyundai', 'Infiniti', 'Isuzu', 'Jaguar', 'Jeep', 'Kia', - 'Lamborghini', 'Land Rover', 'Lexus', 'Lincoln', 'Lotus', 'Lucid', - 'MINI', 'Maserati', 'Mazda', 'McLaren', 'Mercury', 'Mitsubishi', - 'Nissan', 'Oldsmobile', 'Plymouth', 'Polestar', 'Pontiac', - 'Porsche', 'Ram', 'Rivian', 'Rolls Royce', 'Saab', 'Saturn', - 'Scion', 'Smart', 'Subaru', 'Tesla', 'Toyota', 'Volkswagen', - 'Volvo' - } - - def normalize_make_name(self, filename: str) -> str: - """Convert filename to proper display name""" - # Remove .json extension - base_name = filename.replace('.json', '') - - # Replace underscores with spaces - spaced_name = base_name.replace('_', ' ') - - # Apply title case - title_cased = spaced_name.title() - - # Apply special cases - return self.special_cases.get(title_cased, title_cased) - - def validate_mapping(self, filename: str, display_name: str) -> bool: - """Validate mapped name against authoritative list""" - return display_name in self.authoritative_makes - - def get_all_mappings(self) -> Dict[str, str]: - """Get complete filename → display name mapping""" - # Simulate the 55 JSON files found in the actual directory - json_files = [ - 'acura.json', 'alfa_romeo.json', 'aston_martin.json', 'audi.json', - 'bentley.json', 'bmw.json', 'buick.json', 'cadillac.json', - 'chevrolet.json', 'chrysler.json', 'dodge.json', 'ferrari.json', - 'fiat.json', 'ford.json', 'genesis.json', 'geo.json', 'gmc.json', - 'honda.json', 'hummer.json', 'hyundai.json', 'infiniti.json', - 'isuzu.json', 'jaguar.json', 'jeep.json', 'kia.json', - 'lamborghini.json', 'land_rover.json', 'lexus.json', 'lincoln.json', - 'lotus.json', 'lucid.json', 'maserati.json', 'mazda.json', - 'mclaren.json', 'mercury.json', 'mini.json', 'mitsubishi.json', - 'nissan.json', 'oldsmobile.json', 'plymouth.json', 'polestar.json', - 'pontiac.json', 'porsche.json', 'ram.json', 'rivian.json', - 'rolls_royce.json', 'saab.json', 'saturn.json', 'scion.json', - 'smart.json', 'subaru.json', 'tesla.json', 'toyota.json', - 'volkswagen.json', 'volvo.json' - ] - - mappings = {} - for filename in json_files: - display_name = self.normalize_make_name(filename) - mappings[filename] = display_name - - return mappings - - def validate_all_mappings(self) -> ValidationReport: - """Validate all mappings against authoritative list""" - mappings = self.get_all_mappings() - mismatches = [] - - for filename, display_name in mappings.items(): - if not self.validate_mapping(filename, display_name): - mismatches.append({ - 'filename': filename, - 'mapped_name': display_name, - 'status': 'NOT_FOUND_IN_AUTHORITATIVE' - }) - - return ValidationReport( - total_files=len(mappings), - valid_mappings=len(mappings) - len(mismatches), - mismatches=mismatches - ) - - -def demonstrate_make_name_mapping(): - """Demonstrate make name normalization process""" - - mapper = MakeNameMapper() - - print("šŸ·ļø Make Name Mapping Examples") - print("=" * 40) - - # Test cases showing different transformation types - test_cases = [ - # Single word makes (standard title case) - ('toyota.json', 'Toyota'), - ('honda.json', 'Honda'), - ('ford.json', 'Ford'), - - # Multi-word makes (underscore → space + title case) - ('alfa_romeo.json', 'Alfa Romeo'), - ('land_rover.json', 'Land Rover'), - ('rolls_royce.json', 'Rolls Royce'), - ('aston_martin.json', 'Aston Martin'), - - # Special capitalization cases - ('bmw.json', 'BMW'), - ('gmc.json', 'GMC'), - ('mini.json', 'MINI'), - ('mclaren.json', 'McLaren'), - ] - - for filename, expected in test_cases: - result = mapper.normalize_make_name(filename) - status = "āœ…" if result == expected else "āŒ" - - print(f"{status} {filename:20} → {result:15} (expected: {expected})") - - if result != expected: - print(f" āš ļø MISMATCH: Expected '{expected}', got '{result}'") - - -def demonstrate_complete_mapping(): - """Show complete mapping of all 55 make files""" - - mapper = MakeNameMapper() - all_mappings = mapper.get_all_mappings() - - print(f"\n\nšŸ“‹ Complete Make Name Mappings ({len(all_mappings)} files)") - print("=" * 50) - - # Group by transformation type for clarity - single_words = [] - multi_words = [] - special_cases = [] - - for filename, display_name in sorted(all_mappings.items()): - if '_' in filename: - multi_words.append((filename, display_name)) - elif display_name in ['BMW', 'GMC', 'MINI', 'McLaren']: - special_cases.append((filename, display_name)) - else: - single_words.append((filename, display_name)) - - print("\nšŸ”¤ Single Word Makes (Standard Title Case):") - for filename, display_name in single_words: - print(f" {filename:20} → {display_name}") - - print(f"\nšŸ“ Multi-Word Makes (Underscore → Space, {len(multi_words)} total):") - for filename, display_name in multi_words: - print(f" {filename:20} → {display_name}") - - print(f"\n⭐ Special Capitalization Cases ({len(special_cases)} total):") - for filename, display_name in special_cases: - print(f" {filename:20} → {display_name}") - - -def demonstrate_validation(): - """Demonstrate validation against authoritative makes list""" - - mapper = MakeNameMapper() - report = mapper.validate_all_mappings() - - print(f"\n\nāœ… Validation Report") - print("=" * 20) - print(f"Total files processed: {report.total_files}") - print(f"Valid mappings: {report.valid_mappings}") - print(f"Success rate: {report.success_rate:.1%}") - - if report.mismatches: - print(f"\nāš ļø Mismatches found ({len(report.mismatches)}):") - for mismatch in report.mismatches: - print(f" {mismatch['filename']} → {mismatch['mapped_name']}") - print(f" Status: {mismatch['status']}") - else: - print("\nšŸŽ‰ All mappings valid!") - - -def demonstrate_database_integration(): - """Show how mappings integrate with database operations""" - - mapper = MakeNameMapper() - - print(f"\n\nšŸ’¾ Database Integration Example") - print("=" * 35) - - sample_files = ['toyota.json', 'alfa_romeo.json', 'bmw.json', 'land_rover.json'] - - print("SQL: INSERT INTO vehicles.make (name) VALUES") - - for i, filename in enumerate(sample_files): - display_name = mapper.normalize_make_name(filename) - comma = "," if i < len(sample_files) - 1 else ";" - - print(f" ('{display_name}'){comma}") - print(f" -- From file: {filename}") - - -def demonstrate_error_handling(): - """Demonstrate error handling for edge cases""" - - mapper = MakeNameMapper() - - print(f"\n\nšŸ› ļø Error Handling Examples") - print("=" * 30) - - edge_cases = [ - 'unknown_brand.json', - 'test__multiple__underscores.json', - 'no_extension', - '.json', # Only extension - ] - - for filename in edge_cases: - try: - display_name = mapper.normalize_make_name(filename) - is_valid = mapper.validate_mapping(filename, display_name) - status = "āœ… Valid" if is_valid else "āš ļø Not in authoritative list" - - print(f" {filename:35} → {display_name:15} ({status})") - except Exception as e: - print(f" {filename:35} → ERROR: {e}") - - -def run_validation_tests(): - """Run comprehensive validation tests""" - - mapper = MakeNameMapper() - - print(f"\n\n🧪 Validation Tests") - print("=" * 20) - - # Test cases with expected results - test_cases = [ - ('toyota.json', 'Toyota', True), - ('alfa_romeo.json', 'Alfa Romeo', True), - ('bmw.json', 'BMW', True), - ('gmc.json', 'GMC', True), - ('mclaren.json', 'McLaren', True), - ('unknown_brand.json', 'Unknown Brand', False), - ] - - passed = 0 - for filename, expected_name, expected_valid in test_cases: - actual_name = mapper.normalize_make_name(filename) - actual_valid = mapper.validate_mapping(filename, actual_name) - - name_correct = actual_name == expected_name - valid_correct = actual_valid == expected_valid - - if name_correct and valid_correct: - print(f"āœ… {filename} → {actual_name} (valid: {actual_valid})") - passed += 1 - else: - print(f"āŒ {filename}") - if not name_correct: - print(f" Name: Expected '{expected_name}', got '{actual_name}'") - if not valid_correct: - print(f" Valid: Expected {expected_valid}, got {actual_valid}") - - print(f"\nšŸ“Š Test Results: {passed}/{len(test_cases)} tests passed") - - if passed == len(test_cases): - print("šŸŽ‰ All validation tests passed!") - return True - else: - print("āš ļø Some tests failed!") - return False - - -if __name__ == "__main__": - demonstrate_make_name_mapping() - demonstrate_complete_mapping() - demonstrate_validation() - demonstrate_database_integration() - demonstrate_error_handling() - - success = run_validation_tests() - - print("\n\nšŸ“‹ Summary") - print("=" * 10) - print("āœ… Make name normalization patterns implemented") - print("āœ… Special capitalization cases handled") - print("āœ… Multi-word make names (underscore → space) working") - print("āœ… Validation against authoritative list functional") - print("āœ… Database integration format demonstrated") - - if success: - print("\nšŸš€ Ready for integration into ETL system!") - else: - print("\nāš ļø Review failed tests before integration") - - print("\nKey Implementation Notes:") - print("• filename.replace('.json', '').replace('_', ' ').title()") - print("• Special cases: BMW, GMC, MINI, McLaren") - print("• Validation against sources/makes.json required") - print("• Handle unknown makes gracefully (log warning, continue)") \ No newline at end of file diff --git a/docs/changes/vehicles-dropdown-v2/examples/sample-json-processing.py b/docs/changes/vehicles-dropdown-v2/examples/sample-json-processing.py deleted file mode 100644 index bd6924e..0000000 --- a/docs/changes/vehicles-dropdown-v2/examples/sample-json-processing.py +++ /dev/null @@ -1,449 +0,0 @@ -#!/usr/bin/env python3 -""" -Sample JSON Processing Examples - -This file demonstrates complete processing of JSON vehicle data, -from file reading through database-ready output structures. - -Usage: - python sample-json-processing.py -""" - -import json -from typing import List, Dict, Any, Optional -from dataclasses import dataclass -from pathlib import Path - - -@dataclass -class EngineSpec: - """Parsed engine specification""" - displacement_l: Optional[float] - configuration: str - cylinders: Optional[int] - fuel_type: str - aspiration: str - raw_string: str - - -@dataclass -class ModelData: - """Model information for a specific year""" - name: str - engines: List[EngineSpec] - trims: List[str] # From submodels - - -@dataclass -class YearData: - """Vehicle data for a specific year""" - year: int - models: List[ModelData] - - -@dataclass -class MakeData: - """Complete make information""" - name: str # Normalized display name - filename: str # Original JSON filename - years: List[YearData] - - @property - def total_models(self) -> int: - return sum(len(year.models) for year in self.years) - - @property - def total_engines(self) -> int: - return sum(len(model.engines) - for year in self.years - for model in year.models) - - @property - def total_trims(self) -> int: - return sum(len(model.trims) - for year in self.years - for model in year.models) - - -class JsonProcessor: - """Process JSON vehicle files into structured data""" - - def __init__(self): - # Import our utility classes - from engine_parsing_examples import EngineSpecParser - from make_mapping_examples import MakeNameMapper - - self.engine_parser = EngineSpecParser() - self.make_mapper = MakeNameMapper() - - def process_json_file(self, json_data: Dict[str, Any], filename: str) -> MakeData: - """Process complete JSON file into structured data""" - - # Get the make name (first key in JSON) - make_key = list(json_data.keys())[0] - display_name = self.make_mapper.normalize_make_name(filename) - - years_data = [] - for year_entry in json_data[make_key]: - year = int(year_entry['year']) - models_data = [] - - for model_entry in year_entry.get('models', []): - model_name = model_entry['name'] - - # Process engines - engines = [] - engine_strings = model_entry.get('engines', []) - - if not engine_strings: - # Electric vehicle - create default engine - engines.append(self.engine_parser.create_electric_motor()) - else: - for engine_str in engine_strings: - engine_spec = self.engine_parser.parse_engine_string(engine_str) - engines.append(engine_spec) - - # Process trims (from submodels) - trims = model_entry.get('submodels', []) - - models_data.append(ModelData( - name=model_name, - engines=engines, - trims=trims - )) - - years_data.append(YearData( - year=year, - models=models_data - )) - - return MakeData( - name=display_name, - filename=filename, - years=years_data - ) - - -def demonstrate_tesla_processing(): - """Demonstrate processing Tesla JSON (electric vehicle example)""" - - # Sample Tesla data (simplified from actual tesla.json) - tesla_json = { - "tesla": [ - { - "year": "2024", - "models": [ - { - "name": "3", - "engines": [], # Empty - electric vehicle - "submodels": [ - "Long Range AWD", - "Performance", - "Standard Plus" - ] - }, - { - "name": "y", - "engines": [], # Empty - electric vehicle - "submodels": [ - "Long Range", - "Performance" - ] - } - ] - }, - { - "year": "2023", - "models": [ - { - "name": "s", - "engines": [], # Empty - electric vehicle - "submodels": [ - "Plaid", - "Long Range Plus" - ] - } - ] - } - ] - } - - processor = JsonProcessor() - make_data = processor.process_json_file(tesla_json, 'tesla.json') - - print("⚔ Tesla JSON Processing Example") - print("=" * 35) - print(f"Filename: tesla.json") - print(f"Display Name: {make_data.name}") - print(f"Years: {len(make_data.years)}") - print(f"Total Models: {make_data.total_models}") - print(f"Total Engines: {make_data.total_engines}") - print(f"Total Trims: {make_data.total_trims}") - - print(f"\nDetailed Breakdown:") - for year_data in make_data.years: - print(f"\n {year_data.year}:") - for model in year_data.models: - print(f" Model: {model.name}") - print(f" Engines: {[e.raw_string for e in model.engines]}") - print(f" Trims: {model.trims}") - - -def demonstrate_subaru_processing(): - """Demonstrate processing Subaru JSON (Boxer engines, H4 configuration)""" - - # Sample Subaru data showing H4 engines - subaru_json = { - "subaru": [ - { - "year": "2024", - "models": [ - { - "name": "crosstrek", - "engines": [ - "2.0L H4", - "2.0L H4 PLUG-IN HYBRID EV- (PHEV)", - "2.5L H4" - ], - "submodels": [ - "Base", - "Premium", - "Limited", - "Hybrid" - ] - }, - { - "name": "forester", - "engines": [ - "2.5L H4" - ], - "submodels": [ - "Base", - "Premium", - "Sport", - "Limited" - ] - } - ] - } - ] - } - - processor = JsonProcessor() - make_data = processor.process_json_file(subaru_json, 'subaru.json') - - print(f"\n\nšŸš— Subaru JSON Processing Example (Boxer Engines)") - print("=" * 50) - print(f"Display Name: {make_data.name}") - - for year_data in make_data.years: - print(f"\n{year_data.year}:") - for model in year_data.models: - print(f" {model.name}:") - for engine in model.engines: - config_note = " (Boxer)" if engine.configuration == 'H' else "" - hybrid_note = " (Hybrid)" if 'Hybrid' in engine.fuel_type else "" - print(f" Engine: {engine.raw_string}") - print(f" → {engine.displacement_l}L {engine.configuration}{engine.cylinders}{config_note}{hybrid_note}") - - -def demonstrate_l_to_i_processing(): - """Demonstrate L→I normalization during processing""" - - # Sample data with L-configuration engines - nissan_json = { - "nissan": [ - { - "year": "2024", - "models": [ - { - "name": "versa", - "engines": [ - "1.6L I4" - ], - "submodels": ["S", "SV", "SR"] - }, - { - "name": "kicks", - "engines": [ - "1.5L L3 PLUG-IN HYBRID EV- (PHEV)" # L3 → I3 - ], - "submodels": ["S", "SV", "SR"] - }, - { - "name": "note", - "engines": [ - "1.2L L3 FULL HYBRID EV- (FHEV)" # L3 → I3 - ], - "submodels": ["Base", "Premium"] - } - ] - } - ] - } - - processor = JsonProcessor() - make_data = processor.process_json_file(nissan_json, 'nissan.json') - - print(f"\n\nšŸŽÆ L→I Normalization Processing Example") - print("=" * 42) - - for year_data in make_data.years: - for model in year_data.models: - for engine in model.engines: - original_config = "L" if "L3" in engine.raw_string else "I" - normalized_config = engine.configuration - - print(f"Model: {model.name}") - print(f" Input: \"{engine.raw_string}\"") - print(f" Configuration: {original_config}{engine.cylinders} → {normalized_config}{engine.cylinders}") - - if original_config == "L" and normalized_config == "I": - print(f" šŸŽÆ NORMALIZED: L→I conversion applied") - print() - - -def demonstrate_database_ready_output(): - """Show how processed data maps to database tables""" - - # Sample mixed data - sample_json = { - "toyota": [ - { - "year": "2024", - "models": [ - { - "name": "camry", - "engines": [ - "2.5L I4", - "2.5L I4 FULL HYBRID EV- (FHEV)" - ], - "submodels": [ - "LE", - "XLE", - "Hybrid LE" - ] - } - ] - } - ] - } - - processor = JsonProcessor() - make_data = processor.process_json_file(sample_json, 'toyota.json') - - print(f"\n\nšŸ’¾ Database-Ready Output") - print("=" * 25) - - # Show SQL INSERT statements - print("-- Make table") - print(f"INSERT INTO vehicles.make (name) VALUES ('{make_data.name}');") - - print(f"\n-- Model table (assuming make_id = 1)") - for year_data in make_data.years: - for model in year_data.models: - print(f"INSERT INTO vehicles.model (make_id, name) VALUES (1, '{model.name}');") - - print(f"\n-- Model Year table (assuming model_id = 1)") - for year_data in make_data.years: - print(f"INSERT INTO vehicles.model_year (model_id, year) VALUES (1, {year_data.year});") - - print(f"\n-- Engine table") - unique_engines = set() - for year_data in make_data.years: - for model in year_data.models: - for engine in model.engines: - engine_key = (engine.raw_string, engine.displacement_l, engine.cylinders, engine.fuel_type) - if engine_key not in unique_engines: - unique_engines.add(engine_key) - print(f"INSERT INTO vehicles.engine (name, displacement_l, cylinders, fuel_type, aspiration)") - print(f" VALUES ('{engine.raw_string}', {engine.displacement_l}, {engine.cylinders}, '{engine.fuel_type}', '{engine.aspiration}');") - - print(f"\n-- Trim table (assuming model_year_id = 1)") - for year_data in make_data.years: - for model in year_data.models: - for trim in model.trims: - print(f"INSERT INTO vehicles.trim (model_year_id, name) VALUES (1, '{trim}');") - - -def run_processing_validation(): - """Validate that processing works correctly""" - - print(f"\n\nāœ… Processing Validation") - print("=" * 25) - - processor = JsonProcessor() - - # Test cases - test_cases = [ - # Tesla (electric, empty engines) - ('tesla.json', {"tesla": [{"year": "2024", "models": [{"name": "3", "engines": [], "submodels": ["Base"]}]}]}), - # Subaru (H4 engines) - ('subaru.json', {"subaru": [{"year": "2024", "models": [{"name": "crosstrek", "engines": ["2.0L H4"], "submodels": ["Base"]}]}]}), - # Nissan (L→I normalization) - ('nissan.json', {"nissan": [{"year": "2024", "models": [{"name": "kicks", "engines": ["1.5L L3"], "submodels": ["Base"]}]}]}) - ] - - for filename, json_data in test_cases: - try: - make_data = processor.process_json_file(json_data, filename) - - # Basic validation - assert make_data.name is not None, "Make name should not be None" - assert len(make_data.years) > 0, "Should have at least one year" - assert make_data.total_models > 0, "Should have at least one model" - - print(f"āœ… {filename} processed successfully") - print(f" Make: {make_data.name}, Models: {make_data.total_models}, Engines: {make_data.total_engines}") - - # Special validations - if filename == 'tesla.json': - # Should have electric motors for empty engines - for year_data in make_data.years: - for model in year_data.models: - assert all(e.fuel_type == 'Electric' for e in model.engines), "Tesla should have electric engines" - - if filename == 'nissan.json': - # Should have L→I normalization - for year_data in make_data.years: - for model in year_data.models: - for engine in model.engines: - if 'L3' in engine.raw_string: - assert engine.configuration == 'I', "L3 should become I3" - - except Exception as e: - print(f"āŒ {filename} failed: {e}") - return False - - print(f"\nšŸŽ‰ All processing validation tests passed!") - return True - - -if __name__ == "__main__": - demonstrate_tesla_processing() - demonstrate_subaru_processing() - demonstrate_l_to_i_processing() - demonstrate_database_ready_output() - - success = run_processing_validation() - - print("\n\nšŸ“‹ Summary") - print("=" * 10) - print("āœ… JSON file processing implemented") - print("āœ… Electric vehicle handling (empty engines → Electric Motor)") - print("āœ… L→I normalization during processing") - print("āœ… Database-ready output structures") - print("āœ… Make name normalization integrated") - print("āœ… Engine specification parsing integrated") - - if success: - print("\nšŸš€ Ready for ETL pipeline integration!") - else: - print("\nāš ļø Review failed validations") - - print("\nNext Steps:") - print("• Integrate with PostgreSQL loader") - print("• Add batch processing for all 55 files") - print("• Implement clear/append modes") - print("• Add CLI interface") - print("• Create comprehensive test suite") \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8ec40bb..0b53406 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -25,6 +25,7 @@ const SettingsPage = lazy(() => import('./pages/SettingsPage').then(m => ({ defa const FuelLogsPage = lazy(() => import('./features/fuel-logs/pages/FuelLogsPage').then(m => ({ default: m.FuelLogsPage }))); const DocumentsPage = lazy(() => import('./features/documents/pages/DocumentsPage').then(m => ({ default: m.DocumentsPage }))); const DocumentDetailPage = lazy(() => import('./features/documents/pages/DocumentDetailPage').then(m => ({ default: m.DocumentDetailPage }))); +const MaintenancePage = lazy(() => import('./features/maintenance/pages/MaintenancePage').then(m => ({ default: m.MaintenancePage }))); const VehiclesMobileScreen = lazy(() => import('./features/vehicles/mobile/VehiclesMobileScreen').then(m => ({ default: m.VehiclesMobileScreen }))); const VehicleDetailMobile = lazy(() => import('./features/vehicles/mobile/VehicleDetailMobile').then(m => ({ default: m.VehicleDetailMobile }))); const DocumentsMobileScreen = lazy(() => import('./features/documents/mobile/DocumentsMobileScreen')); @@ -551,7 +552,7 @@ function App() { } /> } /> } /> - Maintenance (TODO)} /> + } /> Stations (TODO)} /> } /> } /> diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 2cc8409..b7bac6a 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -25,17 +25,16 @@ interface LayoutProps { export const Layout: React.FC = ({ children, mobileMode = false }) => { const { user, logout } = useAuth0(); - const { sidebarOpen, toggleSidebar } = useAppStore(); - const { setSidebarOpen } = useAppStore.getState(); + const { sidebarOpen, toggleSidebar, setSidebarOpen } = useAppStore(); const location = useLocation(); const theme = useTheme(); - // Ensure desktop has a visible navigation by default + // Ensure desktop has a visible navigation by default (only on mount) React.useEffect(() => { if (!mobileMode && !sidebarOpen) { setSidebarOpen(true); } - }, [mobileMode, sidebarOpen]); + }, [mobileMode, setSidebarOpen]); // Removed sidebarOpen from dependencies const navigation = [ { name: 'Vehicles', href: '/vehicles', icon: }, diff --git a/frontend/src/features/maintenance/api/maintenance.api.ts b/frontend/src/features/maintenance/api/maintenance.api.ts new file mode 100644 index 0000000..0bd7d55 --- /dev/null +++ b/frontend/src/features/maintenance/api/maintenance.api.ts @@ -0,0 +1,82 @@ +/** + * @ai-summary API client for maintenance records and schedules + * @ai-context Follows pattern from documents.api.ts with full CRUD operations + */ + +import { apiClient } from '../../../core/api/client'; +import type { + CreateMaintenanceRecordRequest, + UpdateMaintenanceRecordRequest, + MaintenanceRecordResponse, + CreateScheduleRequest, + UpdateScheduleRequest, + MaintenanceScheduleResponse, + MaintenanceCategory +} from '../types/maintenance.types'; + +export const maintenanceApi = { + // Maintenance Records + async createRecord(data: CreateMaintenanceRecordRequest): Promise { + const res = await apiClient.post('/maintenance/records', data); + return res.data; + }, + + async getRecords(): Promise { + const res = await apiClient.get('/maintenance/records'); + return res.data; + }, + + async getRecord(id: string): Promise { + const res = await apiClient.get(`/maintenance/records/${id}`); + return res.data; + }, + + async updateRecord(id: string, data: UpdateMaintenanceRecordRequest): Promise { + const res = await apiClient.put(`/maintenance/records/${id}`, data); + return res.data; + }, + + async deleteRecord(id: string): Promise { + await apiClient.delete(`/maintenance/records/${id}`); + }, + + async getRecordsByVehicle(vehicleId: string): Promise { + const res = await apiClient.get(`/maintenance/records/vehicle/${vehicleId}`); + return res.data; + }, + + // Maintenance Schedules + async createSchedule(data: CreateScheduleRequest): Promise { + const res = await apiClient.post('/maintenance/schedules', data); + return res.data; + }, + + async getSchedulesByVehicle(vehicleId: string): Promise { + const res = await apiClient.get(`/maintenance/schedules/vehicle/${vehicleId}`); + return res.data; + }, + + async updateSchedule(id: string, data: UpdateScheduleRequest): Promise { + const res = await apiClient.put(`/maintenance/schedules/${id}`, data); + return res.data; + }, + + async deleteSchedule(id: string): Promise { + await apiClient.delete(`/maintenance/schedules/${id}`); + }, + + async getUpcoming(vehicleId: string, currentMileage?: number): Promise { + const params = currentMileage ? { current_mileage: currentMileage } : {}; + const res = await apiClient.get( + `/maintenance/schedules/vehicle/${vehicleId}/upcoming`, + { params } + ); + return res.data; + }, + + // Utility endpoints + async getSubtypes(category: MaintenanceCategory): Promise { + const res = await apiClient.get<{ subtypes: string[] }>(`/maintenance/subtypes/${category}`); + return res.data.subtypes; + } +}; diff --git a/frontend/src/features/maintenance/components/MaintenanceRecordEditDialog.tsx b/frontend/src/features/maintenance/components/MaintenanceRecordEditDialog.tsx new file mode 100644 index 0000000..b1bfdeb --- /dev/null +++ b/frontend/src/features/maintenance/components/MaintenanceRecordEditDialog.tsx @@ -0,0 +1,314 @@ +/** + * @ai-summary Edit dialog for maintenance records + * @ai-context Mobile-friendly dialog with proper form handling + */ + +import React, { useState, useEffect } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + TextField, + Box, + Grid, + FormControl, + InputLabel, + Select, + MenuItem, + Typography, + useMediaQuery, +} from '@mui/material'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; +import { + MaintenanceRecordResponse, + UpdateMaintenanceRecordRequest, + MaintenanceCategory, + getCategoryDisplayName, +} from '../types/maintenance.types'; +import { SubtypeCheckboxGroup } from './SubtypeCheckboxGroup'; +import { useVehicles } from '../../vehicles/hooks/useVehicles'; +import type { Vehicle } from '../../vehicles/types/vehicles.types'; + +interface MaintenanceRecordEditDialogProps { + open: boolean; + record: MaintenanceRecordResponse | null; + onClose: () => void; + onSave: (id: string, data: UpdateMaintenanceRecordRequest) => Promise; +} + +export const MaintenanceRecordEditDialog: React.FC = ({ + open, + record, + onClose, + onSave, +}) => { + const [formData, setFormData] = useState({}); + const [isSaving, setIsSaving] = useState(false); + const [error, setError] = useState(null); + + const vehiclesQuery = useVehicles(); + const vehicles = vehiclesQuery.data; + const isSmallScreen = useMediaQuery('(max-width:600px)'); + + // Reset form when record changes + useEffect(() => { + if (record && record.id) { + try { + setFormData({ + category: record.category, + subtypes: record.subtypes, + date: record.date, + odometer_reading: record.odometer_reading || undefined, + cost: record.cost ? Number(record.cost) : undefined, + shop_name: record.shop_name || undefined, + notes: record.notes || undefined, + }); + setError(null); + } catch (err) { + console.error('[MaintenanceRecordEditDialog] Error setting form data:', err); + setError(err as Error); + } + } + }, [record]); + + const handleInputChange = (field: keyof UpdateMaintenanceRecordRequest, value: any) => { + setFormData((prev) => ({ + ...prev, + [field]: value, + })); + }; + + const handleSave = async () => { + if (!record || !record.id) { + console.error('[MaintenanceRecordEditDialog] No valid record to save'); + return; + } + + try { + setIsSaving(true); + + // Filter out unchanged fields + const changedData: UpdateMaintenanceRecordRequest = {}; + Object.entries(formData).forEach(([key, value]) => { + const typedKey = key as keyof UpdateMaintenanceRecordRequest; + const recordValue = record[typedKey as keyof MaintenanceRecordResponse]; + + // Special handling for arrays + if (Array.isArray(value) && Array.isArray(recordValue)) { + if (JSON.stringify(value) !== JSON.stringify(recordValue)) { + (changedData as any)[key] = value; + } + } else if (value !== recordValue) { + (changedData as any)[key] = value; + } + }); + + // Only send update if there are actual changes + if (Object.keys(changedData).length > 0) { + await onSave(record.id, changedData); + } + + onClose(); + } catch (err) { + console.error('[MaintenanceRecordEditDialog] Failed to save record:', err); + setError(err as Error); + } finally { + setIsSaving(false); + } + }; + + const handleCancel = () => { + onClose(); + }; + + // Early bailout if dialog not open or no record to edit + if (!open || !record) return null; + + // Error state + if (error) { + return ( + + Error Loading Maintenance Record + + + Failed to load maintenance record data. Please try again. + + + {error.message} + + + + + + + ); + } + + return ( + + + Edit Maintenance Record + + + + {/* Vehicle (Read-only display) */} + + { + const vehicle = vehicles?.find((v: Vehicle) => v.id === record.vehicle_id); + if (!vehicle) return 'Unknown Vehicle'; + if (vehicle.nickname?.trim()) return vehicle.nickname.trim(); + const parts = [vehicle.year, vehicle.make, vehicle.model, vehicle.trimLevel].filter(Boolean); + return parts.length > 0 ? parts.join(' ') : 'Vehicle'; + })()} + helperText="Vehicle cannot be changed when editing" + /> + + + {/* Category */} + + + Category + + + + + {/* Subtypes */} + {formData.category && ( + + + Service Types * + + handleInputChange('subtypes', subtypes)} + /> + + )} + + {/* Date */} + + + handleInputChange('date', newValue?.toISOString().split('T')[0] || '') + } + format="MM/dd/yyyy" + slotProps={{ + textField: { + fullWidth: true, + sx: { + '& .MuiOutlinedInput-root': { + minHeight: '56px', + }, + }, + }, + }} + /> + + + {/* Odometer Reading */} + + + handleInputChange( + 'odometer_reading', + e.target.value ? parseInt(e.target.value) : undefined + ) + } + helperText="Current mileage" + inputProps={{ min: 0 }} + /> + + + {/* Cost */} + + + handleInputChange('cost', e.target.value ? parseFloat(e.target.value) : undefined) + } + helperText="Total service cost" + inputProps={{ step: 0.01, min: 0 }} + /> + + + {/* Shop Name */} + + handleInputChange('shop_name', e.target.value || undefined)} + helperText="Service location" + inputProps={{ maxLength: 200 }} + /> + + + {/* Notes */} + + handleInputChange('notes', e.target.value || undefined)} + placeholder="Optional notes about this service..." + inputProps={{ maxLength: 10000 }} + /> + + + + + + + + + + + ); +}; diff --git a/frontend/src/features/maintenance/components/MaintenanceRecordForm.tsx b/frontend/src/features/maintenance/components/MaintenanceRecordForm.tsx new file mode 100644 index 0000000..62bce0a --- /dev/null +++ b/frontend/src/features/maintenance/components/MaintenanceRecordForm.tsx @@ -0,0 +1,378 @@ +/** + * @ai-summary Form component for creating maintenance records + * @ai-context Mobile-first responsive design with proper validation + */ + +import React, { useState, useEffect } from 'react'; +import { useForm, Controller } from 'react-hook-form'; +import { z } from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { + Card, + CardHeader, + CardContent, + TextField, + Select, + MenuItem, + Button, + Box, + Grid, + FormControl, + InputLabel, + FormHelperText, + CircularProgress, + Typography, + InputAdornment, +} from '@mui/material'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; +import { useMaintenanceRecords } from '../hooks/useMaintenanceRecords'; +import { useVehicles } from '../../vehicles/hooks/useVehicles'; +import { SubtypeCheckboxGroup } from './SubtypeCheckboxGroup'; +import { + MaintenanceCategory, + CreateMaintenanceRecordRequest, + getCategoryDisplayName, +} from '../types/maintenance.types'; +import toast from 'react-hot-toast'; + +const schema = z.object({ + vehicle_id: z.string().uuid({ message: 'Please select a vehicle' }), + category: z.enum(['routine_maintenance', 'repair', 'performance_upgrade'], { + errorMap: () => ({ message: 'Please select a category' }), + }), + subtypes: z.array(z.string()).min(1, { message: 'Please select at least one subtype' }), + date: z.string().min(1, { message: 'Date is required' }), + odometer_reading: z.coerce.number().positive().optional().or(z.literal('')), + cost: z.coerce.number().positive().optional().or(z.literal('')), + shop_name: z.string().max(200).optional(), + notes: z.string().max(1000).optional(), +}); + +type FormData = z.infer; + +export const MaintenanceRecordForm: React.FC = () => { + const { data: vehicles, isLoading: isLoadingVehicles } = useVehicles(); + const { createRecord, isRecordMutating } = useMaintenanceRecords(); + const [selectedCategory, setSelectedCategory] = useState(null); + + const { + control, + handleSubmit, + watch, + setValue, + reset, + formState: { errors, isValid }, + } = useForm({ + resolver: zodResolver(schema), + mode: 'onChange', + defaultValues: { + vehicle_id: '', + category: undefined as any, + subtypes: [], + date: new Date().toISOString().split('T')[0], + odometer_reading: '' as any, + cost: '' as any, + shop_name: '', + notes: '', + }, + }); + + // Watch category changes to reset subtypes + const watchedCategory = watch('category'); + useEffect(() => { + if (watchedCategory) { + setSelectedCategory(watchedCategory as MaintenanceCategory); + setValue('subtypes', []); + } + }, [watchedCategory, setValue]); + + const onSubmit = async (data: FormData) => { + try { + const payload: CreateMaintenanceRecordRequest = { + vehicle_id: data.vehicle_id, + category: data.category as MaintenanceCategory, + subtypes: data.subtypes, + date: data.date, + odometer_reading: data.odometer_reading ? Number(data.odometer_reading) : undefined, + cost: data.cost ? Number(data.cost) : undefined, + shop_name: data.shop_name || undefined, + notes: data.notes || undefined, + }; + + await createRecord(payload); + toast.success('Maintenance record added successfully'); + + // Reset form + reset({ + vehicle_id: '', + category: undefined as any, + subtypes: [], + date: new Date().toISOString().split('T')[0], + odometer_reading: '' as any, + cost: '' as any, + shop_name: '', + notes: '', + }); + setSelectedCategory(null); + } catch (error) { + console.error('Failed to create maintenance record:', error); + toast.error('Failed to add maintenance record'); + } + }; + + if (isLoadingVehicles) { + return ( + + + + + + + + ); + } + + return ( + + + + +
+ + {/* Vehicle Selection */} + + ( + + Vehicle * + + {errors.vehicle_id && ( + {errors.vehicle_id.message} + )} + + )} + /> + + + {/* Category Selection */} + + ( + + Category * + + {errors.category && ( + {errors.category.message} + )} + + )} + /> + + + {/* Subtypes */} + {selectedCategory && ( + + + Subtypes * + + ( + + + {errors.subtypes && ( + + {errors.subtypes.message} + + )} + + )} + /> + + )} + + {/* Date */} + + ( + + field.onChange(newValue?.toISOString().split('T')[0] || '') + } + format="MM/dd/yyyy" + slotProps={{ + textField: { + fullWidth: true, + error: !!errors.date, + helperText: errors.date?.message, + sx: { + '& .MuiOutlinedInput-root': { + minHeight: 56, + }, + }, + }, + }} + /> + )} + /> + + + {/* Odometer Reading */} + + ( + + )} + /> + + + {/* Cost */} + + ( + $, + }} + fullWidth + error={!!errors.cost} + helperText={errors.cost?.message} + sx={{ + '& .MuiOutlinedInput-root': { + minHeight: 56, + }, + }} + /> + )} + /> + + + {/* Shop Name */} + + ( + + )} + /> + + + {/* Notes */} + + ( + + )} + /> + + + {/* Submit Button */} + + + + + + +
+
+
+
+ ); +}; diff --git a/frontend/src/features/maintenance/components/MaintenanceRecordsList.tsx b/frontend/src/features/maintenance/components/MaintenanceRecordsList.tsx new file mode 100644 index 0000000..0c55280 --- /dev/null +++ b/frontend/src/features/maintenance/components/MaintenanceRecordsList.tsx @@ -0,0 +1,221 @@ +/** + * @ai-summary List component for displaying maintenance records + * @ai-context Mobile-friendly card layout with proper touch targets + */ + +import React, { useState } from 'react'; +import { + Card, + CardContent, + Typography, + Box, + IconButton, + Stack, + Chip, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + useTheme, + useMediaQuery, +} from '@mui/material'; +import { Edit, Delete } from '@mui/icons-material'; +import { + MaintenanceRecordResponse, + getCategoryDisplayName, +} from '../types/maintenance.types'; + +interface MaintenanceRecordsListProps { + records?: MaintenanceRecordResponse[]; + onEdit?: (record: MaintenanceRecordResponse) => void; + onDelete?: (recordId: string) => void; +} + +export const MaintenanceRecordsList: React.FC = ({ + records, + onEdit, + onDelete, +}) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [recordToDelete, setRecordToDelete] = useState(null); + + const handleDeleteClick = (record: MaintenanceRecordResponse) => { + setRecordToDelete(record); + setDeleteDialogOpen(true); + }; + + const handleDeleteConfirm = () => { + if (recordToDelete && onDelete) { + onDelete(recordToDelete.id); + setDeleteDialogOpen(false); + setRecordToDelete(null); + } + }; + + const handleDeleteCancel = () => { + setDeleteDialogOpen(false); + setRecordToDelete(null); + }; + + if (!records || records.length === 0) { + return ( + + + + No maintenance records yet. + + + + ); + } + + // Sort records by date DESC (newest first) + const sortedRecords = [...records].sort( + (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() + ); + + return ( + <> + + {sortedRecords.map((record) => { + const dateText = new Date(record.date).toLocaleDateString(); + const categoryDisplay = getCategoryDisplayName(record.category); + const subtypeCount = record.subtype_count || record.subtypes?.length || 0; + + return ( + + + + + + {dateText} + + + {categoryDisplay} ({subtypeCount}) + + + {record.odometer_reading && ( + + )} + {record.cost && ( + + )} + {record.shop_name && ( + + )} + + {record.notes && ( + + {record.notes} + + )} + + + + {onEdit && ( + onEdit(record)} + sx={{ + color: 'primary.main', + minWidth: 44, + minHeight: 44, + '&:hover': { + backgroundColor: 'primary.main', + color: 'white', + }, + ...(isMobile && { + border: '1px solid', + borderColor: 'primary.main', + borderRadius: 2, + }), + }} + > + + + )} + {onDelete && ( + handleDeleteClick(record)} + sx={{ + color: 'error.main', + minWidth: 44, + minHeight: 44, + '&:hover': { + backgroundColor: 'error.main', + color: 'white', + }, + ...(isMobile && { + border: '1px solid', + borderColor: 'error.main', + borderRadius: 2, + }), + }} + > + + + )} + + + + + ); + })} + + + {/* Delete Confirmation Dialog */} + + Delete Maintenance Record + + + Are you sure you want to delete this maintenance record? This action cannot be undone. + + {recordToDelete && ( + + {new Date(recordToDelete.date).toLocaleDateString()} -{' '} + {getCategoryDisplayName(recordToDelete.category)} + + )} + + + + + + + + ); +}; diff --git a/frontend/src/features/maintenance/components/SubtypeCheckboxGroup.tsx b/frontend/src/features/maintenance/components/SubtypeCheckboxGroup.tsx new file mode 100644 index 0000000..3f8922f --- /dev/null +++ b/frontend/src/features/maintenance/components/SubtypeCheckboxGroup.tsx @@ -0,0 +1,71 @@ +/** + * @ai-summary Reusable checkbox group for maintenance subtype selection + * @ai-context Responsive grid layout with proper mobile touch targets + */ + +import React from 'react'; +import { FormGroup, FormControlLabel, Checkbox, Box } from '@mui/material'; +import { MaintenanceCategory, getSubtypesForCategory } from '../types/maintenance.types'; + +interface SubtypeCheckboxGroupProps { + category: MaintenanceCategory; + selected: string[]; + onChange: (subtypes: string[]) => void; +} + +export const SubtypeCheckboxGroup: React.FC = ({ + category, + selected, + onChange, +}) => { + const availableSubtypes = getSubtypesForCategory(category); + + const handleToggle = (subtype: string) => { + const newSelected = selected.includes(subtype) + ? selected.filter((s) => s !== subtype) + : [...selected, subtype]; + onChange(newSelected); + }; + + return ( + + + {availableSubtypes.map((subtype) => ( + handleToggle(subtype)} + sx={{ + minWidth: 44, + minHeight: 44, + '& .MuiSvgIcon-root': { + fontSize: 24, + }, + }} + /> + } + label={subtype} + sx={{ + minHeight: 44, + '& .MuiFormControlLabel-label': { + fontSize: { xs: 14, sm: 16 }, + }, + }} + /> + ))} + + + ); +}; diff --git a/frontend/src/features/maintenance/hooks/useMaintenanceRecords.ts b/frontend/src/features/maintenance/hooks/useMaintenanceRecords.ts new file mode 100644 index 0000000..8c8b441 --- /dev/null +++ b/frontend/src/features/maintenance/hooks/useMaintenanceRecords.ts @@ -0,0 +1,161 @@ +/** + * @ai-summary React Query hook for maintenance records + * @ai-context Provides queries and mutations with proper cache invalidation + */ + +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useAuth0 } from '@auth0/auth0-react'; +import { maintenanceApi } from '../api/maintenance.api'; +import type { + CreateMaintenanceRecordRequest, + UpdateMaintenanceRecordRequest, + MaintenanceRecordResponse, + CreateScheduleRequest, + UpdateScheduleRequest, + MaintenanceScheduleResponse +} from '../types/maintenance.types'; + +export const useMaintenanceRecords = (vehicleId?: string) => { + const { isAuthenticated, isLoading } = useAuth0(); + const queryClient = useQueryClient(); + + // Query for maintenance records + const recordsQuery = useQuery({ + queryKey: ['maintenanceRecords', vehicleId || 'all'], + queryFn: () => (vehicleId ? maintenanceApi.getRecordsByVehicle(vehicleId) : maintenanceApi.getRecords()), + enabled: isAuthenticated && !isLoading, + staleTime: 2 * 60 * 1000, // 2 minutes + gcTime: 5 * 60 * 1000, // 5 minutes cache time + retry: (failureCount, error: any) => { + // Retry 401 errors up to 3 times for mobile auth timing issues + if (error?.response?.status === 401 && failureCount < 3) { + console.log(`[Mobile Auth] Maintenance records API retry ${failureCount + 1}/3 for 401 error`); + return true; + } + return false; + }, + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), + refetchOnWindowFocus: false, + refetchOnMount: false, + }); + + // Query for maintenance schedules + const schedulesQuery = useQuery({ + queryKey: ['maintenanceSchedules', vehicleId], + queryFn: () => maintenanceApi.getSchedulesByVehicle(vehicleId!), + enabled: !!vehicleId && isAuthenticated && !isLoading, + staleTime: 5 * 60 * 1000, // 5 minutes - schedules change less frequently + gcTime: 10 * 60 * 1000, // 10 minutes cache time + retry: (failureCount, error: any) => { + if (error?.response?.status === 401 && failureCount < 3) { + console.log(`[Mobile Auth] Maintenance schedules API retry ${failureCount + 1}/3 for 401 error`); + return true; + } + return false; + }, + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), + refetchOnWindowFocus: false, + refetchOnMount: false, + }); + + // Query for upcoming maintenance + const upcomingQuery = useQuery({ + queryKey: ['maintenanceUpcoming', vehicleId], + queryFn: () => maintenanceApi.getUpcoming(vehicleId!), + enabled: !!vehicleId && isAuthenticated && !isLoading, + staleTime: 5 * 60 * 1000, + gcTime: 10 * 60 * 1000, + retry: (failureCount, error: any) => { + if (error?.response?.status === 401 && failureCount < 3) { + console.log(`[Mobile Auth] Maintenance upcoming API retry ${failureCount + 1}/3 for 401 error`); + return true; + } + return false; + }, + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), + refetchOnWindowFocus: false, + refetchOnMount: false, + }); + + // Mutations for records + const createRecordMutation = useMutation({ + mutationFn: (data: CreateMaintenanceRecordRequest) => maintenanceApi.createRecord(data), + onSuccess: (_res, variables) => { + queryClient.invalidateQueries({ queryKey: ['maintenanceRecords', variables.vehicle_id] }); + queryClient.invalidateQueries({ queryKey: ['maintenanceRecords', 'all'] }); + queryClient.invalidateQueries({ queryKey: ['maintenanceUpcoming', variables.vehicle_id] }); + }, + }); + + const updateRecordMutation = useMutation({ + mutationFn: ({ id, data }: { id: string; data: UpdateMaintenanceRecordRequest }) => + maintenanceApi.updateRecord(id, data), + onSuccess: () => { + // Invalidate all record queries since we don't know the vehicle_id from the response + queryClient.invalidateQueries({ queryKey: ['maintenanceRecords'] }); + queryClient.invalidateQueries({ queryKey: ['maintenanceUpcoming'] }); + }, + }); + + const deleteRecordMutation = useMutation({ + mutationFn: (id: string) => maintenanceApi.deleteRecord(id), + onSuccess: () => { + // Invalidate all record queries + queryClient.invalidateQueries({ queryKey: ['maintenanceRecords'] }); + queryClient.invalidateQueries({ queryKey: ['maintenanceUpcoming'] }); + }, + }); + + // Mutations for schedules + const createScheduleMutation = useMutation({ + mutationFn: (data: CreateScheduleRequest) => maintenanceApi.createSchedule(data), + onSuccess: (_res, variables) => { + queryClient.invalidateQueries({ queryKey: ['maintenanceSchedules', variables.vehicle_id] }); + queryClient.invalidateQueries({ queryKey: ['maintenanceUpcoming', variables.vehicle_id] }); + }, + }); + + const updateScheduleMutation = useMutation({ + mutationFn: ({ id, data }: { id: string; data: UpdateScheduleRequest }) => + maintenanceApi.updateSchedule(id, data), + onSuccess: () => { + // Invalidate all schedule queries + queryClient.invalidateQueries({ queryKey: ['maintenanceSchedules'] }); + queryClient.invalidateQueries({ queryKey: ['maintenanceUpcoming'] }); + }, + }); + + const deleteScheduleMutation = useMutation({ + mutationFn: (id: string) => maintenanceApi.deleteSchedule(id), + onSuccess: () => { + // Invalidate all schedule queries + queryClient.invalidateQueries({ queryKey: ['maintenanceSchedules'] }); + queryClient.invalidateQueries({ queryKey: ['maintenanceUpcoming'] }); + }, + }); + + return { + // Records + records: recordsQuery.data, + isRecordsLoading: recordsQuery.isLoading, + recordsError: recordsQuery.error, + createRecord: createRecordMutation.mutateAsync, + updateRecord: updateRecordMutation.mutateAsync, + deleteRecord: deleteRecordMutation.mutateAsync, + isRecordMutating: createRecordMutation.isPending || updateRecordMutation.isPending || deleteRecordMutation.isPending, + + // Schedules + schedules: schedulesQuery.data, + isSchedulesLoading: schedulesQuery.isLoading, + schedulesError: schedulesQuery.error, + createSchedule: createScheduleMutation.mutateAsync, + updateSchedule: updateScheduleMutation.mutateAsync, + deleteSchedule: deleteScheduleMutation.mutateAsync, + isScheduleMutating: createScheduleMutation.isPending || updateScheduleMutation.isPending || deleteScheduleMutation.isPending, + + // Upcoming + upcoming: upcomingQuery.data, + isUpcomingLoading: upcomingQuery.isLoading, + upcomingError: upcomingQuery.error, + }; +}; diff --git a/frontend/src/features/maintenance/index.ts b/frontend/src/features/maintenance/index.ts new file mode 100644 index 0000000..1ccee39 --- /dev/null +++ b/frontend/src/features/maintenance/index.ts @@ -0,0 +1,21 @@ +/** + * @ai-summary Maintenance feature exports + * @ai-context Central export point for maintenance types, API, hooks, and components + */ + +// Types +export * from './types/maintenance.types'; + +// API +export * from './api/maintenance.api'; + +// Hooks +export * from './hooks/useMaintenanceRecords'; + +// Components +export { SubtypeCheckboxGroup } from './components/SubtypeCheckboxGroup'; +export { MaintenanceRecordForm } from './components/MaintenanceRecordForm'; +export { MaintenanceRecordsList } from './components/MaintenanceRecordsList'; + +// Pages +export { MaintenancePage } from './pages/MaintenancePage'; diff --git a/frontend/src/features/maintenance/pages/MaintenancePage.tsx b/frontend/src/features/maintenance/pages/MaintenancePage.tsx new file mode 100644 index 0000000..9c1d065 --- /dev/null +++ b/frontend/src/features/maintenance/pages/MaintenancePage.tsx @@ -0,0 +1,117 @@ +/** + * @ai-summary Main page for maintenance feature + * @ai-context Two-column responsive layout following fuel-logs pattern + */ + +import React, { useState } from 'react'; +import { Grid, Typography, Box } from '@mui/material'; +import { useQueryClient } from '@tanstack/react-query'; +import { MaintenanceRecordForm } from '../components/MaintenanceRecordForm'; +import { MaintenanceRecordsList } from '../components/MaintenanceRecordsList'; +import { MaintenanceRecordEditDialog } from '../components/MaintenanceRecordEditDialog'; +import { useMaintenanceRecords } from '../hooks/useMaintenanceRecords'; +import { FormSuspense } from '../../../components/SuspenseWrappers'; +import type { MaintenanceRecordResponse, UpdateMaintenanceRecordRequest } from '../types/maintenance.types'; + +export const MaintenancePage: React.FC = () => { + const { records, isRecordsLoading, recordsError, updateRecord, deleteRecord } = useMaintenanceRecords(); + const queryClient = useQueryClient(); + const [editingRecord, setEditingRecord] = useState(null); + const [editDialogOpen, setEditDialogOpen] = useState(false); + + const handleEdit = (record: MaintenanceRecordResponse) => { + setEditingRecord(record); + setEditDialogOpen(true); + }; + + const handleEditSave = async (id: string, data: UpdateMaintenanceRecordRequest) => { + try { + await updateRecord({ id, data }); + // Refetch queries after update + queryClient.refetchQueries({ queryKey: ['maintenanceRecords'] }); + setEditDialogOpen(false); + setEditingRecord(null); + } catch (error) { + console.error('Failed to update maintenance record:', error); + throw error; // Re-throw to let dialog handle the error + } + }; + + const handleEditClose = () => { + setEditDialogOpen(false); + setEditingRecord(null); + }; + + const handleDelete = async (recordId: string) => { + try { + await deleteRecord(recordId); + // Refetch queries after delete + queryClient.refetchQueries({ queryKey: ['maintenanceRecords', 'all'] }); + } catch (error) { + console.error('Failed to delete maintenance record:', error); + } + }; + + if (isRecordsLoading) { + return ( + + Loading maintenance records... + + ); + } + + if (recordsError) { + return ( + + + Failed to load maintenance records. Please try again. + + + ); + } + + return ( + + + {/* Left Column: Form */} + + + + + {/* Right Column: Records List */} + + + Recent Maintenance Records + + + + + + {/* Edit Dialog */} + + + ); +}; diff --git a/frontend/src/features/maintenance/types/maintenance.types.ts b/frontend/src/features/maintenance/types/maintenance.types.ts new file mode 100644 index 0000000..5865c53 --- /dev/null +++ b/frontend/src/features/maintenance/types/maintenance.types.ts @@ -0,0 +1,159 @@ +/** + * @ai-summary Type definitions for maintenance feature + * @ai-context Supports three categories with specific subtypes, multiple selections allowed + */ + +// Category types +export type MaintenanceCategory = 'routine_maintenance' | 'repair' | 'performance_upgrade'; + +// Subtype definitions (constants for validation) +export const ROUTINE_MAINTENANCE_SUBTYPES = [ + 'Accelerator Pedal', + 'Air Filter Element', + 'Brakes and Traction Control', + 'Cabin Air Filter / Purifier', + 'Coolant', + 'Doors', + 'Drive Belt', + 'Engine Oil', + 'Evaporative Emissions System', + 'Exhaust System', + 'Fluid - A/T', + 'Fluid - Differential', + 'Fluid - M/T', + 'Fluid Filter - A/T', + 'Fluids', + 'Fuel Delivery and Air Induction', + 'Hood Shock / Support', + 'Neutral Safety Switch', + 'Parking Brake System', + 'Restraints and Safety Systems', + 'Shift Interlock, A/T', + 'Spark Plug', + 'Steering and Suspension', + 'Tires', + 'Trunk / Liftgate Shock / Support', + 'Washer Fluid', + 'Wiper Blade' +] as const; + +export const REPAIR_SUBTYPES = [ + 'Engine', + 'Transmission', + 'Drivetrain', + 'Exterior', + 'Interior' +] as const; + +export const PERFORMANCE_UPGRADE_SUBTYPES = [ + 'Engine', + 'Drivetrain', + 'Suspension', + 'Wheels/Tires', + 'Exterior' +] as const; + +// Database record types +export interface MaintenanceRecord { + id: string; + user_id: string; + vehicle_id: string; + category: MaintenanceCategory; + subtypes: string[]; + date: string; + odometer_reading?: number; + cost?: number; + shop_name?: string; + notes?: string; + created_at: string; + updated_at: string; +} + +export interface MaintenanceSchedule { + id: string; + user_id: string; + vehicle_id: string; + category: MaintenanceCategory; + subtypes: string[]; + interval_months?: number; + interval_miles?: number; + last_service_date?: string; + last_service_mileage?: number; + next_due_date?: string; + next_due_mileage?: number; + is_active: boolean; + created_at: string; + updated_at: string; +} + +// Request types +export interface CreateMaintenanceRecordRequest { + vehicle_id: string; + category: MaintenanceCategory; + subtypes: string[]; + date: string; + odometer_reading?: number; + cost?: number; + shop_name?: string; + notes?: string; +} + +export interface UpdateMaintenanceRecordRequest { + category?: MaintenanceCategory; + subtypes?: string[]; + date?: string; + odometer_reading?: number | null; + cost?: number | null; + shop_name?: string | null; + notes?: string | null; +} + +export interface CreateScheduleRequest { + vehicle_id: string; + category: MaintenanceCategory; + subtypes: string[]; + interval_months?: number; + interval_miles?: number; +} + +export interface UpdateScheduleRequest { + category?: MaintenanceCategory; + subtypes?: string[]; + interval_months?: number | null; + interval_miles?: number | null; + is_active?: boolean; +} + +// Response types +export interface MaintenanceRecordResponse extends MaintenanceRecord { + subtype_count: number; +} + +export interface MaintenanceScheduleResponse extends MaintenanceSchedule { + subtype_count: number; + is_due_soon?: boolean; + is_overdue?: boolean; +} + +// Validation helpers +export function getSubtypesForCategory(category: MaintenanceCategory): readonly string[] { + switch (category) { + case 'routine_maintenance': return ROUTINE_MAINTENANCE_SUBTYPES; + case 'repair': return REPAIR_SUBTYPES; + case 'performance_upgrade': return PERFORMANCE_UPGRADE_SUBTYPES; + } +} + +export function validateSubtypes(category: MaintenanceCategory, subtypes: string[]): boolean { + if (!subtypes || subtypes.length === 0) return false; + const validSubtypes = getSubtypesForCategory(category); + return subtypes.every(st => validSubtypes.includes(st as any)); +} + +export function getCategoryDisplayName(category: MaintenanceCategory): string { + switch (category) { + case 'routine_maintenance': return 'Routine Maintenance'; + case 'repair': return 'Repair'; + case 'performance_upgrade': return 'Performance Upgrade'; + } +}