Pre-web changes

This commit is contained in:
Eric Gullickson
2025-11-05 11:04:48 -06:00
parent 45fea0f307
commit 0c3ed01f4b
25 changed files with 257 additions and 3538 deletions

View File

@@ -1,20 +0,0 @@
# Development Environment Variables
# This file is for local development only - NOT for production k8s deployment
# In k8s, these values come from ConfigMaps and Secrets
# Frontend Vite Configuration (build-time only)
VITE_AUTH0_DOMAIN=motovaultpro.us.auth0.com
VITE_AUTH0_CLIENT_ID=yspR8zdnSxmV8wFIghHynQ08iXAPoQJ3
VITE_AUTH0_AUDIENCE=https://api.motovaultpro.com
VITE_API_BASE_URL=/api
# Docker Compose Development Configuration
# These variables are used by docker-compose for container build args only
AUTH0_DOMAIN=motovaultpro.us.auth0.com
AUTH0_CLIENT_ID=yspR8zdnSxmV8wFIghHynQ08iXAPoQJ3
AUTH0_AUDIENCE=https://api.motovaultpro.com
# NOTE: Backend services no longer use this file
# Backend configuration comes from:
# - /app/config/production.yml (non-sensitive config)
# - /run/secrets/ (sensitive secrets)

View File

@@ -278,12 +278,12 @@ Per CLAUDE.md, all code must pass:
```bash ```bash
# Development # Development
make setup # Build + start + migrate make setup # Build + start + migrate
make rebuild # Rebuild containers make rebuild # Rebuild containers
make logs # Tail all logs make logs # Tail all logs
make test # Run all tests docker compose exec mvp-backend npm test # Run backend test suite inside container
make start # Start services make start # Start services
make migrate # Run migrations make migrate # Run migrations
# Testing # Testing
cd backend && npm test -- features/stations cd backend && npm test -- features/stations
@@ -299,7 +299,7 @@ echo "YOUR_API_KEY" > ./secrets/app/google-maps-api-key.txt
# Verification # Verification
docker compose exec mvp-frontend cat /usr/share/nginx/html/config.js docker compose exec mvp-frontend cat /usr/share/nginx/html/config.js
curl -H "Authorization: Bearer $TOKEN" http://localhost:3001/api/stations/saved curl -H "Authorization: Bearer $TOKEN" https://motovaultpro.com/api/stations/saved
``` ```
## Final Notes ## Final Notes

View File

@@ -1,4 +1,4 @@
.PHONY: help setup start stop clean test test-frontend logs shell-backend shell-frontend migrate rebuild traefik-dashboard traefik-logs service-discovery network-inspect health-check-all mobile-setup db-shell-app .PHONY: help setup start stop clean logs shell-backend shell-frontend migrate rebuild traefik-dashboard traefik-logs service-discovery network-inspect health-check-all mobile-setup db-shell-app
help: help:
@echo "MotoVaultPro - Simplified 5-Container Architecture" @echo "MotoVaultPro - Simplified 5-Container Architecture"
@@ -46,7 +46,7 @@ setup:
@echo "" @echo ""
@echo "K8s-ready setup complete!" @echo "K8s-ready setup complete!"
@echo "Access application at: https://motovaultpro.com" @echo "Access application at: https://motovaultpro.com"
@echo "Traefik dashboard at: http://localhost:8080" @echo "Traefik dashboard at: https://motovaultpro.com:8080"
@echo "" @echo ""
@echo "Network Architecture:" @echo "Network Architecture:"
@echo " - 3-tier isolation: frontend, backend, database" @echo " - 3-tier isolation: frontend, backend, database"
@@ -101,11 +101,11 @@ db-shell-app:
# K8s-Ready Architecture Commands # K8s-Ready Architecture Commands
traefik-dashboard: traefik-dashboard:
@echo "Traefik Service Discovery Dashboard:" @echo "Traefik Service Discovery Dashboard:"
@echo " Dashboard: http://localhost:8080" @echo " Dashboard: https://motovaultpro.com:8080"
@echo " API: http://localhost:8080/api" @echo " API: https://motovaultpro.com:8080/api"
@echo "" @echo ""
@echo "Available routes:" @echo "Available routes:"
@curl -s http://localhost:8080/api/http/routers 2>/dev/null | jq -r '.[].name' | grep -v internal | sed 's/^/ - /' || echo " (Traefik not ready yet)" @curl -ks https://motovaultpro.com:8080/api/http/routers 2>/dev/null | jq -r '.[].name' | grep -v internal | sed 's/^/ - /' || echo " (Traefik not ready yet)"
traefik-logs: traefik-logs:
@echo "Traefik access and error logs:" @echo "Traefik access and error logs:"
@@ -116,10 +116,10 @@ service-discovery:
@echo "Service Discovery Status:" @echo "Service Discovery Status:"
@echo "" @echo ""
@echo "Discovered Services:" @echo "Discovered Services:"
@curl -s http://localhost:8080/api/http/services 2>/dev/null | jq -r '.[].name' | grep -v internal | sed 's/^/ - /' || echo " Traefik not ready yet" @curl -ks https://motovaultpro.com:8080/api/http/services 2>/dev/null | jq -r '.[].name' | grep -v internal | sed 's/^/ - /' || echo " Traefik not ready yet"
@echo "" @echo ""
@echo "Active Routes:" @echo "Active Routes:"
@curl -s http://localhost:8080/api/http/routers 2>/dev/null | jq -r '.[].name' | grep -v internal | sed 's/^/ -> /' || echo " No routes discovered yet" @curl -ks https://motovaultpro.com:8080/api/http/routers 2>/dev/null | jq -r '.[].name' | grep -v internal | sed 's/^/ -> /' || echo " No routes discovered yet"
network-inspect: network-inspect:
@echo "K8s-Ready Network Architecture:" @echo "K8s-Ready Network Architecture:"
@@ -137,11 +137,11 @@ health-check-all:
@docker compose ps --format "table {{.Service}}\t{{.Status}}\t{{.Health}}" @docker compose ps --format "table {{.Service}}\t{{.Status}}\t{{.Health}}"
@echo "" @echo ""
@echo "Network Connectivity Test:" @echo "Network Connectivity Test:"
@echo " Traefik API: $$(curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/api/http/services 2>/dev/null || echo 'FAIL')" @echo " Traefik API: $$(curl -ks -o /dev/null -w '%{http_code}' https://motovaultpro.com:8080/api/http/services 2>/dev/null || echo 'FAIL')"
@echo "" @echo ""
@echo "Service Discovery Status:" @echo "Service Discovery Status:"
@echo " Discovered Services: $$(curl -s http://localhost:8080/api/http/services 2>/dev/null | jq '. | length' || echo '0')" @echo " Discovered Services: $$(curl -ks https://motovaultpro.com:8080/api/http/services 2>/dev/null | jq '. | length' || echo '0')"
@echo " Active Routes: $$(curl -s http://localhost:8080/api/http/routers 2>/dev/null | jq '. | length' || echo '0')" @echo " Active Routes: $$(curl -ks https://motovaultpro.com:8080/api/http/routers 2>/dev/null | jq '. | length' || echo '0')"
# SSL Certificate Generation # SSL Certificate Generation
generate-certs: generate-certs:

View File

@@ -7,6 +7,7 @@ import pool from '../../core/config/database';
import { cacheService } from '../../core/config/redis'; import { cacheService } from '../../core/config/redis';
import { VINDecodeService } from './domain/vin-decode.service'; import { VINDecodeService } from './domain/vin-decode.service';
import { PlatformCacheService } from './domain/platform-cache.service'; import { PlatformCacheService } from './domain/platform-cache.service';
import { VehicleDataService } from './domain/vehicle-data.service';
export { platformRoutes } from './api/platform.routes'; export { platformRoutes } from './api/platform.routes';
export { PlatformController } from './api/platform.controller'; export { PlatformController } from './api/platform.controller';
@@ -18,6 +19,7 @@ export * from './models/responses';
// Singleton VIN decode service for use by other features // Singleton VIN decode service for use by other features
let vinDecodeServiceInstance: VINDecodeService | null = null; let vinDecodeServiceInstance: VINDecodeService | null = null;
let vehicleDataServiceInstance: VehicleDataService | null = null;
export function getVINDecodeService(): VINDecodeService { export function getVINDecodeService(): VINDecodeService {
if (!vinDecodeServiceInstance) { if (!vinDecodeServiceInstance) {
@@ -27,6 +29,14 @@ export function getVINDecodeService(): VINDecodeService {
return vinDecodeServiceInstance; return vinDecodeServiceInstance;
} }
export function getVehicleDataService(): VehicleDataService {
if (!vehicleDataServiceInstance) {
const platformCache = new PlatformCacheService(cacheService);
vehicleDataServiceInstance = new VehicleDataService(platformCache);
}
return vehicleDataServiceInstance;
}
// Helper to get pool for VIN decode service // Helper to get pool for VIN decode service
export function getPool(): Pool { export function getPool(): Pool {
return pool; return pool;

View File

@@ -203,7 +203,7 @@ make setup
make logs make logs
# Verify # Verify
curl http://localhost:3001/health curl https://motovaultpro.com/api/health
``` ```
### Verification ### Verification
@@ -215,7 +215,7 @@ docker compose exec mvp-frontend cat /run/secrets/google-maps-api-key
docker compose exec mvp-frontend cat /usr/share/nginx/html/config.js docker compose exec mvp-frontend cat /usr/share/nginx/html/config.js
# Test API endpoint # Test API endpoint
curl -H "Authorization: Bearer $TOKEN" http://localhost:3001/api/stations/saved curl -H "Authorization: Bearer $TOKEN" https://motovaultpro.com/api/stations/saved
``` ```
## Next Steps (Phases 6-11) ## Next Steps (Phases 6-11)

View File

@@ -19,7 +19,7 @@ Authorization: Bearer {jwt_token}
## Base URL ## Base URL
**Development**: `http://localhost:3001/api/stations` **Development**: `https://motovaultpro.com/api/stations`
**Production**: `https://motovaultpro.com/api/stations` **Production**: `https://motovaultpro.com/api/stations`
@@ -574,7 +574,7 @@ TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
```bash ```bash
# 1. Search for stations # 1. Search for stations
curl -X POST http://localhost:3001/api/stations/search \ curl -X POST https://motovaultpro.com/api/stations/search \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
@@ -585,7 +585,7 @@ curl -X POST http://localhost:3001/api/stations/search \
# 2. Save a station from search results # 2. Save a station from search results
PLACE_ID="ChIJN1t_tDeuEmsRUsoyG83frY4" PLACE_ID="ChIJN1t_tDeuEmsRUsoyG83frY4"
curl -X POST http://localhost:3001/api/stations/save \ curl -X POST https://motovaultpro.com/api/stations/save \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "{ -d "{
@@ -595,11 +595,11 @@ curl -X POST http://localhost:3001/api/stations/save \
}" | jq }" | jq
# 3. Get all saved stations # 3. Get all saved stations
curl -X GET http://localhost:3001/api/stations/saved \ curl -X GET https://motovaultpro.com/api/stations/saved \
-H "Authorization: Bearer $TOKEN" | jq -H "Authorization: Bearer $TOKEN" | jq
# 4. Update saved station # 4. Update saved station
curl -X PATCH http://localhost:3001/api/stations/saved/$PLACE_ID \ curl -X PATCH https://motovaultpro.com/api/stations/saved/$PLACE_ID \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
@@ -607,7 +607,7 @@ curl -X PATCH http://localhost:3001/api/stations/saved/$PLACE_ID \
}' | jq }' | jq
# 5. Delete saved station # 5. Delete saved station
curl -X DELETE http://localhost:3001/api/stations/saved/$PLACE_ID \ curl -X DELETE https://motovaultpro.com/api/stations/saved/$PLACE_ID \
-H "Authorization: Bearer $TOKEN" -H "Authorization: Bearer $TOKEN"
``` ```
@@ -644,7 +644,7 @@ Google Maps API key is loaded from `/run/secrets/google-maps-api-key` at contain
API endpoints are configured to accept requests from: API endpoints are configured to accept requests from:
- https://motovaultpro.com (production) - https://motovaultpro.com (production)
- http://localhost:3000 (development) - https://motovaultpro.com (development)
## Versioning ## Versioning

View File

@@ -92,7 +92,7 @@ The Gas Stations feature requires two Google Maps APIs:
- Add allowed domains: - Add allowed domains:
``` ```
https://motovaultpro.com/* https://motovaultpro.com/*
http://localhost:3000/* # Development only https://motovaultpro.com/* # Development only
``` ```
3. Click **Save** 3. Click **Save**
@@ -273,7 +273,7 @@ Cost:
```bash ```bash
# Development # Development
secrets/app/google-maps-api-key.txt → Development key (restricted to localhost) secrets/app/google-maps-api-key.txt → Development key (restricted to motovaultpro.com)
# Production # Production
Kubernetes secret → Production key (restricted to production IPs) Kubernetes secret → Production key (restricted to production IPs)
@@ -421,7 +421,7 @@ After setup, verify everything works:
- [ ] Billing alerts configured - [ ] Billing alerts configured
- [ ] Key tested in development: - [ ] Key tested in development:
```bash ```bash
curl -X POST http://localhost:3001/api/stations/search \ curl -X POST https://motovaultpro.com/api/stations/search \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"latitude": 37.7749, "longitude": -122.4194}' -d '{"latitude": 37.7749, "longitude": -122.4194}'

View File

@@ -275,7 +275,7 @@ docker compose ps
# All should show "Up" status # All should show "Up" status
# Check backend health # Check backend health
curl -s http://localhost:3001/health | jq curl -s https://motovaultpro.com/api/health | jq
# Should return: {"status":"ok","features":["stations",...]} # Should return: {"status":"ok","features":["stations",...]}
# Check frontend loads # Check frontend loads
@@ -292,11 +292,11 @@ Run comprehensive health checks to verify deployment:
**Backend Health**: **Backend Health**:
```bash ```bash
# Overall health check # Overall health check
curl http://localhost:3001/health curl https://motovaultpro.com/api/health
# Stations feature health (implicit in API availability) # Stations feature health (implicit in API availability)
curl -H "Authorization: Bearer $TEST_TOKEN" \ curl -H "Authorization: Bearer $TEST_TOKEN" \
http://localhost:3001/api/stations/saved https://motovaultpro.com/api/stations/saved
``` ```
**Frontend Health**: **Frontend Health**:
@@ -343,7 +343,7 @@ export TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
**Test Search Endpoint**: **Test Search Endpoint**:
```bash ```bash
curl -X POST http://localhost:3001/api/stations/search \ curl -X POST https://motovaultpro.com/api/stations/search \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
@@ -379,14 +379,14 @@ curl -X POST http://localhost:3001/api/stations/search \
**Test Save Endpoint**: **Test Save Endpoint**:
```bash ```bash
# First search to populate cache # First search to populate cache
PLACE_ID=$(curl -X POST http://localhost:3001/api/stations/search \ PLACE_ID=$(curl -X POST https://motovaultpro.com/api/stations/search \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"latitude": 37.7749, "longitude": -122.4194}' | \ -d '{"latitude": 37.7749, "longitude": -122.4194}' | \
jq -r '.stations[0].placeId') jq -r '.stations[0].placeId')
# Then save station # Then save station
curl -X POST http://localhost:3001/api/stations/save \ curl -X POST https://motovaultpro.com/api/stations/save \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "{ -d "{
@@ -398,13 +398,13 @@ curl -X POST http://localhost:3001/api/stations/save \
**Test Get Saved Endpoint**: **Test Get Saved Endpoint**:
```bash ```bash
curl -X GET http://localhost:3001/api/stations/saved \ curl -X GET https://motovaultpro.com/api/stations/saved \
-H "Authorization: Bearer $TOKEN" | jq -H "Authorization: Bearer $TOKEN" | jq
``` ```
**Test Delete Endpoint**: **Test Delete Endpoint**:
```bash ```bash
curl -X DELETE http://localhost:3001/api/stations/saved/$PLACE_ID \ curl -X DELETE https://motovaultpro.com/api/stations/saved/$PLACE_ID \
-H "Authorization: Bearer $TOKEN" -H "Authorization: Bearer $TOKEN"
# Should return 204 No Content # Should return 204 No Content
``` ```
@@ -437,20 +437,20 @@ Verify users can only access their own data:
```bash ```bash
# User 1 saves a station # User 1 saves a station
USER1_TOKEN="..." USER1_TOKEN="..."
PLACE_ID=$(curl -X POST http://localhost:3001/api/stations/search \ PLACE_ID=$(curl -X POST https://motovaultpro.com/api/stations/search \
-H "Authorization: Bearer $USER1_TOKEN" \ -H "Authorization: Bearer $USER1_TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"latitude": 37.7749, "longitude": -122.4194}' | \ -d '{"latitude": 37.7749, "longitude": -122.4194}' | \
jq -r '.stations[0].placeId') jq -r '.stations[0].placeId')
curl -X POST http://localhost:3001/api/stations/save \ curl -X POST https://motovaultpro.com/api/stations/save \
-H "Authorization: Bearer $USER1_TOKEN" \ -H "Authorization: Bearer $USER1_TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "{\"placeId\": \"$PLACE_ID\"}" -d "{\"placeId\": \"$PLACE_ID\"}"
# User 2 should NOT see User 1's saved station # User 2 should NOT see User 1's saved station
USER2_TOKEN="..." USER2_TOKEN="..."
curl -X GET http://localhost:3001/api/stations/saved \ curl -X GET https://motovaultpro.com/api/stations/saved \
-H "Authorization: Bearer $USER2_TOKEN" | jq -H "Authorization: Bearer $USER2_TOKEN" | jq
# Should return: [] (empty array) # Should return: [] (empty array)
``` ```
@@ -461,13 +461,13 @@ Test response times meet requirements:
```bash ```bash
# Search endpoint (should be < 1500ms) # Search endpoint (should be < 1500ms)
time curl -X POST http://localhost:3001/api/stations/search \ time curl -X POST https://motovaultpro.com/api/stations/search \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"latitude": 37.7749, "longitude": -122.4194}' -d '{"latitude": 37.7749, "longitude": -122.4194}'
# Saved stations endpoint (should be < 100ms) # Saved stations endpoint (should be < 100ms)
time curl -X GET http://localhost:3001/api/stations/saved \ time curl -X GET https://motovaultpro.com/api/stations/saved \
-H "Authorization: Bearer $TOKEN" -H "Authorization: Bearer $TOKEN"
``` ```
@@ -532,7 +532,7 @@ docker compose exec postgres psql -U postgres -d motovaultpro < backup_YYYYMMDD_
docker compose up -d docker compose up -d
# Verify rollback successful # Verify rollback successful
curl http://localhost:3001/health curl https://motovaultpro.com/api/health
``` ```
### Partial Rollback (Disable Feature) ### Partial Rollback (Disable Feature)
@@ -547,7 +547,7 @@ If only stations feature needs to be disabled:
docker compose up -d --build mvp-backend docker compose up -d --build mvp-backend
# Verify other features still work # Verify other features still work
curl http://localhost:3001/health curl https://motovaultpro.com/api/health
``` ```
### Database Rollback Only ### Database Rollback Only
@@ -655,7 +655,7 @@ docker compose logs mvp-backend | grep -i migration
- [ ] Check container health: `docker compose ps` - [ ] Check container health: `docker compose ps`
- [ ] Review error logs: `docker compose logs | grep -i error` - [ ] Review error logs: `docker compose logs | grep -i error`
- [ ] Verify API responding: `curl http://localhost:3001/health` - [ ] Verify API responding: `curl https://motovaultpro.com/api/health`
### Weekly Checks ### Weekly Checks

View File

@@ -10,7 +10,7 @@ Run this single command for immediate status:
```bash ```bash
# All-in-one health check # All-in-one health check
curl -s http://localhost:3001/health | jq curl -s https://motovaultpro.com/api/health | jq
``` ```
**Expected Output**: **Expected Output**:
@@ -39,7 +39,7 @@ export TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
#### Test Search Endpoint #### Test Search Endpoint
```bash ```bash
curl -X POST http://localhost:3001/api/stations/search \ curl -X POST https://motovaultpro.com/api/stations/search \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
@@ -61,14 +61,14 @@ curl -X POST http://localhost:3001/api/stations/search \
```bash ```bash
# First search to get a place ID # First search to get a place ID
PLACE_ID=$(curl -s -X POST http://localhost:3001/api/stations/search \ PLACE_ID=$(curl -s -X POST https://motovaultpro.com/api/stations/search \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"latitude": 37.7749, "longitude": -122.4194}' \ -d '{"latitude": 37.7749, "longitude": -122.4194}' \
| jq -r '.stations[0].placeId') | jq -r '.stations[0].placeId')
# Then save station # Then save station
curl -X POST http://localhost:3001/api/stations/save \ curl -X POST https://motovaultpro.com/api/stations/save \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "{\"placeId\": \"$PLACE_ID\", \"nickname\": \"Health Check Station\"}" \ -d "{\"placeId\": \"$PLACE_ID\", \"nickname\": \"Health Check Station\"}" \
@@ -84,7 +84,7 @@ curl -X POST http://localhost:3001/api/stations/save \
#### Test Get Saved Endpoint #### Test Get Saved Endpoint
```bash ```bash
curl -X GET http://localhost:3001/api/stations/saved \ curl -X GET https://motovaultpro.com/api/stations/saved \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
-w "\nHTTP Status: %{http_code}\nTime: %{time_total}s\n" \ -w "\nHTTP Status: %{http_code}\nTime: %{time_total}s\n" \
| jq | jq
@@ -98,7 +98,7 @@ curl -X GET http://localhost:3001/api/stations/saved \
#### Test Delete Endpoint #### Test Delete Endpoint
```bash ```bash
curl -X DELETE http://localhost:3001/api/stations/saved/$PLACE_ID \ curl -X DELETE https://motovaultpro.com/api/stations/saved/$PLACE_ID \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
-w "\nHTTP Status: %{http_code}\nTime: %{time_total}s\n" -w "\nHTTP Status: %{http_code}\nTime: %{time_total}s\n"
``` ```
@@ -316,13 +316,13 @@ Verify users can only access their own data:
USER1_TOKEN="..." USER1_TOKEN="..."
# User 1 saves a station # User 1 saves a station
PLACE_ID=$(curl -s -X POST http://localhost:3001/api/stations/search \ PLACE_ID=$(curl -s -X POST https://motovaultpro.com/api/stations/search \
-H "Authorization: Bearer $USER1_TOKEN" \ -H "Authorization: Bearer $USER1_TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"latitude": 37.7749, "longitude": -122.4194}' \ -d '{"latitude": 37.7749, "longitude": -122.4194}' \
| jq -r '.stations[0].placeId') | jq -r '.stations[0].placeId')
curl -s -X POST http://localhost:3001/api/stations/save \ curl -s -X POST https://motovaultpro.com/api/stations/save \
-H "Authorization: Bearer $USER1_TOKEN" \ -H "Authorization: Bearer $USER1_TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "{\"placeId\": \"$PLACE_ID\"}" > /dev/null -d "{\"placeId\": \"$PLACE_ID\"}" > /dev/null
@@ -331,7 +331,7 @@ curl -s -X POST http://localhost:3001/api/stations/save \
USER2_TOKEN="..." USER2_TOKEN="..."
# User 2 tries to access User 1's saved stations # User 2 tries to access User 1's saved stations
curl -s -X GET http://localhost:3001/api/stations/saved \ curl -s -X GET https://motovaultpro.com/api/stations/saved \
-H "Authorization: Bearer $USER2_TOKEN" \ -H "Authorization: Bearer $USER2_TOKEN" \
| jq '. | length' | jq '. | length'
``` ```
@@ -346,7 +346,7 @@ curl -s -X GET http://localhost:3001/api/stations/saved \
# Run 10 searches and measure average time # Run 10 searches and measure average time
for i in {1..10}; do for i in {1..10}; do
curl -s -o /dev/null -w "%{time_total}\n" \ curl -s -o /dev/null -w "%{time_total}\n" \
-X POST http://localhost:3001/api/stations/search \ -X POST https://motovaultpro.com/api/stations/search \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"latitude": 37.7749, "longitude": -122.4194}' -d '{"latitude": 37.7749, "longitude": -122.4194}'
@@ -360,7 +360,7 @@ done | awk '{sum+=$1} END {print "Average:", sum/NR, "seconds"}'
```bash ```bash
# Measure save operation time # Measure save operation time
time curl -s -o /dev/null \ time curl -s -o /dev/null \
-X POST http://localhost:3001/api/stations/save \ -X POST https://motovaultpro.com/api/stations/save \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "{\"placeId\": \"$PLACE_ID\"}" -d "{\"placeId\": \"$PLACE_ID\"}"
@@ -379,7 +379,7 @@ sudo apt-get install apache2-utils
# Run load test (100 requests, 10 concurrent) # Run load test (100 requests, 10 concurrent)
ab -n 100 -c 10 -T 'application/json' -H "Authorization: Bearer $TOKEN" \ ab -n 100 -c 10 -T 'application/json' -H "Authorization: Bearer $TOKEN" \
-p search_payload.json \ -p search_payload.json \
http://localhost:3001/api/stations/search https://motovaultpro.com/api/stations/search
``` ```
**Expected**: **Expected**:
@@ -415,7 +415,7 @@ if [ -z "$TOKEN" ]; then
fi fi
echo "1. Backend Health Check..." echo "1. Backend Health Check..."
if curl -s http://localhost:3001/health | jq -e '.status == "ok"' > /dev/null; then if curl -s https://motovaultpro.com/api/health | jq -e '.status == "ok"' > /dev/null; then
echo "${GREEN}✓ Backend healthy${NC}" echo "${GREEN}✓ Backend healthy${NC}"
else else
echo "${RED}✗ Backend unhealthy${NC}" echo "${RED}✗ Backend unhealthy${NC}"
@@ -444,7 +444,7 @@ fi
echo "" echo ""
echo "4. Search API Check..." echo "4. Search API Check..."
if curl -s -X POST http://localhost:3001/api/stations/search \ if curl -s -X POST https://motovaultpro.com/api/stations/search \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"latitude": 37.7749, "longitude": -122.4194}' \ -d '{"latitude": 37.7749, "longitude": -122.4194}' \
@@ -492,7 +492,7 @@ JWT_TOKEN="your_token_here" ./health-check.sh
- [ ] Backend container running: `docker compose ps mvp-backend` - [ ] Backend container running: `docker compose ps mvp-backend`
- [ ] Frontend container running: `docker compose ps mvp-frontend` - [ ] Frontend container running: `docker compose ps mvp-frontend`
- [ ] No errors in logs: `docker compose logs --tail=100 | grep -i error` - [ ] No errors in logs: `docker compose logs --tail=100 | grep -i error`
- [ ] Health endpoint responding: `curl http://localhost:3001/health` - [ ] Health endpoint responding: `curl https://motovaultpro.com/api/health`
### Weekly Monitoring ### Weekly Monitoring

View File

@@ -116,7 +116,7 @@ Complete production readiness checklist for the Gas Stations feature. This docum
#### Immediate Validation (within 5 minutes) #### Immediate Validation (within 5 minutes)
- [ ] Health endpoint responding: `curl http://localhost:3001/health` - [ ] Health endpoint responding: `curl https://motovaultpro.com/api/health`
- [ ] Backend logs show no errors: `docker compose logs mvp-backend | grep -i error` - [ ] Backend logs show no errors: `docker compose logs mvp-backend | grep -i error`
- [ ] Frontend logs show no errors: `docker compose logs mvp-frontend | grep -i error` - [ ] Frontend logs show no errors: `docker compose logs mvp-frontend | grep -i error`
- [ ] Database tables exist: `\dt station*` - [ ] Database tables exist: `\dt station*`

View File

@@ -140,7 +140,7 @@ docker compose logs mvp-frontend | grep -i config
**Check config.js is served**: **Check config.js is served**:
```bash ```bash
curl -s http://localhost:3000/config.js curl -s https://motovaultpro.com/config.js
``` ```
**Expected**: JavaScript file with `window.CONFIG = {...}` **Expected**: JavaScript file with `window.CONFIG = {...}`
@@ -214,7 +214,7 @@ Results: 20 stations
export TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." export TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
# Search for stations # Search for stations
curl -X POST http://localhost:3001/api/stations/search \ curl -X POST https://motovaultpro.com/api/stations/search \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
@@ -400,7 +400,7 @@ docker compose exec mvp-frontend cat /usr/share/nginx/html/config.js
2. **Check config.js content**: 2. **Check config.js content**:
```bash ```bash
curl http://localhost:3000/config.js curl https://motovaultpro.com/config.js
``` ```
3. **Check index.html loads config.js**: 3. **Check index.html loads config.js**:

View File

@@ -13,16 +13,17 @@ Primary entity for vehicle management consuming MVP Platform Vehicles Service. H
- `DELETE /api/vehicles/:id` - Soft delete vehicle - `DELETE /api/vehicles/:id` - Soft delete vehicle
### Hierarchical Vehicle Dropdowns ### Hierarchical Vehicle Dropdowns
**Status**: Dropdown methods are TODO stubs in vehicles service. Frontend directly consumes platform module endpoints. **Status**: Vehicles service now proxies the platform vehicle catalog to provide fully dynamic dropdowns. Each selection step filters the next list, ensuring only valid combinations are shown.
Frontend consumes (via `/platform` module, not vehicles feature): Sequence:
- `GET /api/platform/years` - Get all years 1. `GET /api/vehicles/dropdown/years``[number]` (latest to oldest).
- `GET /api/platform/makes?year={year}` - Get makes for year 2. `GET /api/vehicles/dropdown/makes?year={year}` `{ id, name }[]` (only makes produced in the selected year).
- `GET /api/platform/models?year={year}&make_id={make_id}` - Get models for make/year 3. `GET /api/vehicles/dropdown/models?year={year}&make_id={id}` `{ id, name }[]` (models offered for that year/make).
- `GET /api/platform/trims?year={year}&make_id={make_id}&model_id={model_id}` - Get trims 4. `GET /api/vehicles/dropdown/trims?year={year}&make_id={id}&model_id={id}``{ id, name }[]` (valid trims for the chosen combination).
- `GET /api/platform/engines?year={year}&make_id={make_id}&model_id={model_id}&trim_id={trim_id}` - Get engines 5. `GET /api/vehicles/dropdown/engines?year={year}&make_id={id}&model_id={id}&trim_id={id}` `{ id, name }[]` (engines tied to the trim).
- `GET /api/platform/transmissions?year={year}&make_id={make_id}&model_id={model_id}` - Get transmissions 6. `GET /api/vehicles/dropdown/transmissions?year={year}&make_id={id}&model_id={id}``{ id, name }[]` (static options: Automatic, Manual).
- `GET /api/platform/vehicle?vin={vin}` - Decode VIN
All dropdown endpoints call `Platform VehicleDataService` behind the scenes, reuse Redis caching, and return normalized `{ id, name }` payloads ready for the frontend.
## Authentication ## Authentication
- All vehicles endpoints (including dropdowns) require a valid JWT (Auth0). - All vehicles endpoints (including dropdowns) require a valid JWT (Auth0).

View File

@@ -4,7 +4,7 @@
*/ */
import { VehiclesRepository } from '../data/vehicles.repository'; import { VehiclesRepository } from '../data/vehicles.repository';
import { getVINDecodeService, getPool } from '../../platform'; import { getVINDecodeService, getVehicleDataService, getPool } from '../../platform';
import { import {
Vehicle, Vehicle,
CreateVehicleRequest, CreateVehicleRequest,
@@ -162,52 +162,52 @@ export class VehiclesService {
await cacheService.del(cacheKey); await cacheService.del(cacheKey);
} }
async getDropdownMakes(_year: number): Promise<{ id: number; name: string }[]> { async getDropdownMakes(year: number): Promise<{ id: number; name: string }[]> {
// TODO: Implement using platform VehicleDataService const vehicleDataService = getVehicleDataService();
// For now, return empty array to allow migration to complete const pool = getPool();
logger.warn('Dropdown makes not yet implemented via platform feature');
return []; logger.info('Fetching dropdown makes via platform module', { year });
return vehicleDataService.getMakes(pool, year);
} }
async getDropdownModels(_year: number, _makeId: number): Promise<{ id: number; name: string }[]> { async getDropdownModels(year: number, makeId: number): Promise<{ id: number; name: string }[]> {
// TODO: Implement using platform VehicleDataService const vehicleDataService = getVehicleDataService();
logger.warn('Dropdown models not yet implemented via platform feature'); const pool = getPool();
return [];
logger.info('Fetching dropdown models via platform module', { year, makeId });
return vehicleDataService.getModels(pool, year, makeId);
} }
async getDropdownTransmissions(_year: number, _makeId: number, _modelId: number): Promise<{ name: string }[]> { async getDropdownTransmissions(_year: number, _makeId: number, _modelId: number): Promise<{ id: number; name: string }[]> {
// TODO: Implement using platform VehicleDataService logger.info('Providing dropdown transmissions from static list');
logger.warn('Dropdown transmissions not yet implemented via platform feature'); return [
return []; { id: 1, name: 'Automatic' },
{ id: 2, name: 'Manual' }
];
} }
async getDropdownEngines(_year: number, _makeId: number, _modelId: number, _trimId: number): Promise<{ name: string }[]> { async getDropdownEngines(year: number, makeId: number, modelId: number, trimId: number): Promise<{ id: number; name: string }[]> {
// TODO: Implement using platform VehicleDataService const vehicleDataService = getVehicleDataService();
logger.warn('Dropdown engines not yet implemented via platform feature'); const pool = getPool();
return [];
logger.info('Fetching dropdown engines via platform module', { year, makeId, modelId, trimId });
return vehicleDataService.getEngines(pool, year, modelId, trimId);
} }
async getDropdownTrims(_year: number, _makeId: number, _modelId: number): Promise<{ name: string }[]> { async getDropdownTrims(year: number, makeId: number, modelId: number): Promise<{ id: number; name: string }[]> {
// TODO: Implement using platform VehicleDataService const vehicleDataService = getVehicleDataService();
logger.warn('Dropdown trims not yet implemented via platform feature'); const pool = getPool();
return [];
logger.info('Fetching dropdown trims via platform module', { year, makeId, modelId });
return vehicleDataService.getTrims(pool, year, modelId);
} }
async getDropdownYears(): Promise<number[]> { async getDropdownYears(): Promise<number[]> {
try { const vehicleDataService = getVehicleDataService();
logger.info('Getting dropdown years'); const pool = getPool();
// Fallback: generate recent years
const currentYear = new Date().getFullYear(); logger.info('Fetching dropdown years via platform module');
const years: number[] = []; return vehicleDataService.getYears(pool);
for (let y = currentYear + 1; y >= 1980; y--) years.push(y);
return years;
} catch (error) {
logger.error('Failed to get dropdown years', { error });
const currentYear = new Date().getFullYear();
const years: number[] = [];
for (let y = currentYear + 1; y >= 1980; y--) years.push(y);
return years;
}
} }
async decodeVIN(vin: string): Promise<{ async decodeVIN(vin: string): Promise<{

View File

@@ -12,20 +12,42 @@ import * as platformModule from '../../../platform';
jest.mock('../../data/vehicles.repository'); jest.mock('../../data/vehicles.repository');
jest.mock('../../../../core/config/redis'); jest.mock('../../../../core/config/redis');
jest.mock('../../../platform', () => ({ jest.mock('../../../platform', () => ({
getVINDecodeService: jest.fn() getVINDecodeService: jest.fn(),
getVehicleDataService: jest.fn(),
getPool: jest.fn()
})); }));
const mockRepository = jest.mocked(VehiclesRepository); const mockRepository = jest.mocked(VehiclesRepository);
const mockCacheService = jest.mocked(cacheService); const mockCacheService = jest.mocked(cacheService);
const mockGetVINDecodeService = jest.mocked(platformModule.getVINDecodeService); const mockGetVINDecodeService = jest.mocked(platformModule.getVINDecodeService);
const mockGetVehicleDataService = jest.mocked(platformModule.getVehicleDataService);
const mockGetPool = jest.mocked(platformModule.getPool);
describe('VehiclesService', () => { describe('VehiclesService', () => {
let service: VehiclesService; let service: VehiclesService;
let repositoryInstance: jest.Mocked<VehiclesRepository>; let repositoryInstance: jest.Mocked<VehiclesRepository>;
let vehicleDataServiceMock: {
getYears: jest.Mock;
getMakes: jest.Mock;
getModels: jest.Mock;
getTrims: jest.Mock;
getEngines: jest.Mock;
};
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
vehicleDataServiceMock = {
getYears: jest.fn(),
getMakes: jest.fn(),
getModels: jest.fn(),
getTrims: jest.fn(),
getEngines: jest.fn(),
};
mockGetVehicleDataService.mockReturnValue(vehicleDataServiceMock as any);
mockGetPool.mockReturnValue('mock-pool' as any);
repositoryInstance = { repositoryInstance = {
create: jest.fn(), create: jest.fn(),
findByUserId: jest.fn(), findByUserId: jest.fn(),
@@ -39,6 +61,63 @@ describe('VehiclesService', () => {
service = new VehiclesService(repositoryInstance); service = new VehiclesService(repositoryInstance);
}); });
describe('dropdown data integration', () => {
it('retrieves years from platform service', async () => {
vehicleDataServiceMock.getYears.mockResolvedValue([2024, 2023]);
const result = await service.getDropdownYears();
expect(mockGetVehicleDataService).toHaveBeenCalled();
expect(vehicleDataServiceMock.getYears).toHaveBeenCalledWith('mock-pool');
expect(result).toEqual([2024, 2023]);
});
it('retrieves makes scoped to year', async () => {
vehicleDataServiceMock.getMakes.mockResolvedValue([{ id: 10, name: 'Honda' }]);
const result = await service.getDropdownMakes(2024);
expect(vehicleDataServiceMock.getMakes).toHaveBeenCalledWith('mock-pool', 2024);
expect(result).toEqual([{ id: 10, name: 'Honda' }]);
});
it('retrieves models scoped to year and make', async () => {
vehicleDataServiceMock.getModels.mockResolvedValue([{ id: 20, name: 'Civic' }]);
const result = await service.getDropdownModels(2024, 10);
expect(vehicleDataServiceMock.getModels).toHaveBeenCalledWith('mock-pool', 2024, 10);
expect(result).toEqual([{ id: 20, name: 'Civic' }]);
});
it('retrieves trims scoped to year, make, and model', async () => {
vehicleDataServiceMock.getTrims.mockResolvedValue([{ id: 30, name: 'Sport' }]);
const result = await service.getDropdownTrims(2024, 10, 20);
expect(vehicleDataServiceMock.getTrims).toHaveBeenCalledWith('mock-pool', 2024, 20);
expect(result).toEqual([{ id: 30, name: 'Sport' }]);
});
it('retrieves engines scoped to selection', async () => {
vehicleDataServiceMock.getEngines.mockResolvedValue([{ id: 40, name: '2.0L Turbo' }]);
const result = await service.getDropdownEngines(2024, 10, 20, 30);
expect(vehicleDataServiceMock.getEngines).toHaveBeenCalledWith('mock-pool', 2024, 20, 30);
expect(result).toEqual([{ id: 40, name: '2.0L Turbo' }]);
});
it('returns static transmission options', async () => {
const result = await service.getDropdownTransmissions(2024, 10, 20);
expect(result).toEqual([
{ id: 1, name: 'Automatic' },
{ id: 2, name: 'Manual' }
]);
});
});
});
describe('createVehicle', () => { describe('createVehicle', () => {
const mockVehicleData = { const mockVehicleData = {
vin: '1HGBH41JXMN109186', vin: '1HGBH41JXMN109186',
@@ -94,7 +173,7 @@ describe('VehiclesService', () => {
const result = await service.createVehicle(mockVehicleData, 'user-123'); const result = await service.createVehicle(mockVehicleData, 'user-123');
expect(repositoryInstance.findByUserAndVIN).toHaveBeenCalledWith('user-123', '1HGBH41JXMN109186'); expect(repositoryInstance.findByUserAndVIN).toHaveBeenCalledWith('user-123', '1HGBH41JXMN109186');
expect(mockVinDecodeService.decodeVIN).toHaveBeenCalledWith('1HGBH41JXMN109186'); expect(mockVinDecodeService.decodeVIN).toHaveBeenCalledWith('mock-pool', '1HGBH41JXMN109186');
expect(repositoryInstance.create).toHaveBeenCalledWith({ expect(repositoryInstance.create).toHaveBeenCalledWith({
...mockVehicleData, ...mockVehicleData,
userId: 'user-123', userId: 'user-123',
@@ -319,4 +398,4 @@ describe('VehiclesService', () => {
await expect(service.deleteVehicle('vehicle-id-123', 'user-123')).rejects.toThrow('Unauthorized'); await expect(service.deleteVehicle('vehicle-id-123', 'user-123')).rejects.toThrow('Unauthorized');
}); });
}); });
}); });

View File

@@ -137,7 +137,7 @@ MotoVaultPro is a single-tenant vehicle management application built with a **6-
- **Port**: 3000 (internal) - **Port**: 3000 (internal)
- **Networks**: frontend - **Networks**: frontend
- **Dependencies**: Backend (API calls) - **Dependencies**: Backend (API calls)
- **Health Check**: `curl http://localhost:3000` (30s interval) - **Health Check**: `curl https://motovaultpro.com` (30s interval)
- **Environment Variables**: - **Environment Variables**:
- `VITE_AUTH0_DOMAIN` - Auth0 tenant - `VITE_AUTH0_DOMAIN` - Auth0 tenant
- `VITE_AUTH0_CLIENT_ID` - Auth0 application ID - `VITE_AUTH0_CLIENT_ID` - Auth0 application ID
@@ -151,7 +151,7 @@ MotoVaultPro is a single-tenant vehicle management application built with a **6-
- **Port**: 3001 (internal) - **Port**: 3001 (internal)
- **Networks**: backend, database - **Networks**: backend, database
- **Dependencies**: PostgreSQL, Redis, Platform - **Dependencies**: PostgreSQL, Redis, Platform
- **Health Check**: `http://localhost:3001/health` (30s interval) - **Health Check**: `https://motovaultpro.com/api/health` (30s interval)
- **Configuration**: - **Configuration**:
- `/app/config/production.yml` - Main config - `/app/config/production.yml` - Main config
- `/app/config/shared.yml` - Shared config - `/app/config/shared.yml` - Shared config
@@ -402,7 +402,7 @@ docker logs mvp-postgres -f
curl https://motovaultpro.com/api/health # Backend (includes platform module) curl https://motovaultpro.com/api/health # Backend (includes platform module)
# Or from within backend container # Or from within backend container
docker compose exec mvp-backend curl http://localhost:3001/health docker compose exec mvp-backend curl https://motovaultpro.com/api/health
``` ```
### Database Access ### Database Access

File diff suppressed because it is too large Load Diff

View File

@@ -1,295 +0,0 @@
# Gas Stations Feature - Testing Implementation Report
## Overview
Comprehensive test suite implemented for Phase 8 of the Gas Stations feature. All critical paths covered with unit, integration, and end-to-end tests.
## Test Files Created
### Backend Tests
#### Unit Tests (3 files)
1. **`backend/src/features/stations/tests/unit/stations.service.test.ts`**
- Test Coverage: StationsService business logic
- Tests:
- searchNearbyStations (happy path, sorting, metadata, default radius)
- saveStation (success, not found, with metadata)
- getUserSavedStations (returns all, empty array)
- removeSavedStation (success, error handling, user isolation)
- Total: 10 test cases
2. **`backend/src/features/stations/tests/unit/google-maps.client.test.ts`**
- Test Coverage: Google Maps API client
- Tests:
- searchNearbyStations (success, API errors, zero results, network errors)
- Distance calculation accuracy
- Result formatting
- Custom radius parameter
- Caching behavior (multiple calls, different coordinates)
- Total: 10 test cases
3. **`backend/src/features/stations/tests/fixtures/mock-stations.ts`** (existing, updated)
- Mock data for all tests
- Properly typed Station and SavedStation objects
- Test coordinates for major cities
#### Integration Tests (1 file)
4. **`backend/src/features/stations/tests/integration/stations.api.test.ts`**
- Test Coverage: Complete API endpoint testing
- Tests:
- POST /api/stations/search (valid search, missing coordinates, auth required, coordinate validation)
- POST /api/stations/save (save success, invalid placeId, station not in cache, user isolation)
- GET /api/stations/saved (returns user stations, empty array, includes metadata)
- DELETE /api/stations/saved/:placeId (delete success, 404 not found, ownership verification)
- Error handling (Google Maps API errors, schema validation, authentication)
- Total: 15 test cases
### Frontend Tests
#### Component Tests (1 file)
5. **`frontend/src/features/stations/__tests__/components/StationCard.test.tsx`**
- Test Coverage: StationCard component
- Tests:
- Rendering (name, address, photo, rating, distance)
- Save/delete actions
- Directions link (Google Maps integration)
- Touch targets (44px minimum)
- Card selection
- Total: 10 test cases
#### Hook Tests (1 file)
6. **`frontend/src/features/stations/__tests__/hooks/useStationsSearch.test.ts`**
- Test Coverage: useStationsSearch React Query hook
- Tests:
- Search execution (basic, custom radius)
- Loading states (pending, clearing after success)
- Error handling (API errors, onError callback)
- Success callback
- Total: 6 test cases
#### API Client Tests (1 file)
7. **`frontend/src/features/stations/__tests__/api/stations.api.test.ts`**
- Test Coverage: Stations API client
- Tests:
- searchStations (valid request, without radius, error handling, 401/500 errors)
- saveStation (with metadata, without optional fields, error handling)
- getSavedStations (fetch all, empty array, error handling)
- deleteSavedStation (delete success, 404 handling, error handling)
- URL construction validation
- Request payload validation
- Response parsing
- Total: 18 test cases
#### E2E Tests (1 file - Template)
8. **`frontend/cypress/e2e/stations.cy.ts`**
- Test Coverage: Complete user workflows
- Tests:
- Search for nearby stations (current location, manual coordinates, error handling, loading states)
- View stations on map (markers, info windows, auto-fit)
- Save station to favorites (save action, nickname/notes, prevent duplicates)
- View saved stations list (display all, empty state, custom nicknames)
- Delete saved station (delete action, optimistic removal, error handling)
- Mobile navigation flow (tab switching, touch targets)
- Error recovery (network errors, authentication errors)
- Integration with fuel logs
- Total: 20+ test scenarios
## Test Statistics
### Backend Tests
- **Total Test Files**: 4 (3 unit + 1 integration)
- **Total Test Cases**: ~35
- **Coverage Target**: >80%
- **Framework**: Jest with ts-jest
- **Mocking**: jest.mock() for external dependencies
### Frontend Tests
- **Total Test Files**: 3 (1 component + 1 hook + 1 API)
- **Total Test Cases**: ~34
- **E2E Template**: 1 file with 20+ scenarios
- **Framework**: React Testing Library, Jest
- **E2E Framework**: Cypress
### Combined
- **Total Test Files**: 8
- **Total Test Cases**: ~69 (excluding E2E)
- **E2E Scenarios**: 20+
- **Total Lines of Test Code**: ~2,500+
## Test Standards Applied
### Code Quality
- Zero TypeScript errors
- Zero lint warnings
- Proper type safety with strict null checks
- Clear test descriptions
- Proper setup/teardown
### Testing Best Practices
- Arrange-Act-Assert pattern
- Mocking external dependencies (Google Maps API, database)
- Test isolation (beforeEach cleanup)
- Meaningful test names
- Edge case coverage
### Coverage Areas
1. **Happy Paths**: All successful user flows
2. **Error Handling**: API failures, network errors, validation errors
3. **Edge Cases**: Empty results, missing data, null values
4. **User Isolation**: Data segregation by user_id
5. **Authentication**: JWT requirements
6. **Performance**: Response times, caching behavior
7. **Mobile**: Touch targets, responsive design
## Key Fixes Applied
### TypeScript Strict Mode Compliance
1. Fixed array access with optional chaining (`array[0]?.prop`)
2. Fixed SavedStation type (uses `stationId` not `placeId`)
3. Fixed Station optional properties (only set if defined)
4. Fixed import paths (`buildApp` from `app.ts`)
5. Removed unused imports
### Test Implementation Corrections
1. Aligned test mocks with actual repository methods
2. Corrected method names (`getUserSavedStations` vs `getSavedStations`)
3. Fixed return type expectations
4. Added proper error assertions
## Running Tests
### Backend Tests
```bash
cd backend
# Run all stations tests
npm test -- stations
# Run with coverage
npm test -- stations --coverage
# Run specific test file
npm test -- stations.service.test.ts
```
### Frontend Tests
```bash
cd frontend
# Run all stations tests
npm test -- stations
# Run with coverage
npm test -- stations --coverage
# Run E2E tests
npm run e2e
# or
npx cypress run --spec "cypress/e2e/stations.cy.ts"
```
### Docker Container Tests
```bash
# Backend tests in container
make shell-backend
npm test -- stations
# Run from host
docker compose exec mvp-backend npm test -- stations
```
## Test Results (Expected)
### Unit Tests
- All 10 service tests: PASS
- All 10 Google Maps client tests: PASS
- Mock data: Valid and type-safe
### Integration Tests
- All 15 API endpoint tests: PASS (requires database)
- User isolation verified
- Error handling confirmed
### Frontend Tests
- Component tests: PASS
- Hook tests: PASS
- API client tests: PASS
### E2E Tests
- Template created for manual execution
- Requires Google Maps API key
- Requires Auth0 test user
## Coverage Report
Expected coverage after full test run:
```
Feature: stations
-------------------------------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
-------------------------------|---------|----------|---------|---------|
stations.service.ts | 85.7 | 80.0 | 100.0 | 85.7 |
stations.repository.ts | 75.0 | 66.7 | 90.0 | 75.0 |
google-maps.client.ts | 90.0 | 85.7 | 100.0 | 90.0 |
stations.controller.ts | 80.0 | 75.0 | 100.0 | 80.0 |
-------------------------------|---------|----------|---------|---------|
All files | 82.5 | 77.2 | 97.5 | 82.5 |
-------------------------------|---------|----------|---------|---------|
```
## Next Steps
### Phase 9: Documentation
- API documentation with examples
- Setup instructions
- Troubleshooting guide
### Phase 10: Validation & Polish
- Run all tests in Docker
- Fix any remaining linting issues
- Manual testing (desktop + mobile)
- Performance validation
### Phase 11: Deployment
- Verify secrets configuration
- Run migrations
- Final smoke tests
- Production checklist
## Notes
### Test Dependencies
- Backend: Jest, Supertest, ts-jest, @types/jest
- Frontend: @testing-library/react, @testing-library/jest-dom, @testing-library/user-event
- E2E: Cypress (installed separately)
### Known Limitations
1. Integration tests require database connection
2. Google Maps API tests use mocks (not real API)
3. E2E tests require manual execution (not in CI yet)
4. Some tests may need Auth0 test credentials
### Future Improvements
1. Add CI/CD pipeline integration
2. Add snapshot testing for components
3. Add performance benchmarks
4. Add accessibility testing
5. Add visual regression testing
## Conclusion
Comprehensive testing suite implemented covering:
- 100% of backend service methods
- 100% of API endpoints
- All critical frontend components
- All React Query hooks
- All API client methods
- Complete E2E user workflows
All tests follow MotoVaultPro testing standards and are ready for integration into the continuous integration pipeline.
---
**Testing Complete**: Phase 8 ✅
**Report Generated**: 2025-11-04
**Author**: Feature Capsule Agent

View File

@@ -1,648 +0,0 @@
Gas Stations Feature - Complete Implementation Plan (K8s-Aligned Secrets)
Overview
Implement complete Gas Stations feature with full integration, map visualization, backend improvements, and comprehensive testing. Uses K8s-aligned runtime
secrets pattern for frontend Google Maps API key.
Phase 1: Frontend Secrets Infrastructure (K8s-Aligned)
1.1 Create Frontend Config Loader
Create frontend/scripts/load-config.sh:
- Bash script that runs as container entrypoint
- Reads /run/secrets/google-maps-api-key
- Generates /usr/share/nginx/html/config.js with: window.CONFIG = { googleMapsApiKey: '...' }
- Falls back to empty string if secret not found
- Logs success/failure for debugging
1.2 Update Frontend Dockerfile
Modify frontend/Dockerfile:
- Copy load-config.sh into image
- Make script executable
- Add SECRETS_DIR environment variable (default: /run/secrets)
- Update CMD to run script before nginx: ["sh", "-c", "/app/load-config.sh && nginx -g 'daemon off;'"]
1.3 Update Frontend index.html
Modify frontend/index.html:
- Add <script src="/config.js"></script> before app bundle
- Config loads before React app initializes
- Add TypeScript types for window.CONFIG
1.4 Create Frontend Config Types
Create frontend/src/core/config/config.types.ts:
- Interface for window.CONFIG
- Export typed accessor: getConfig()
- Runtime validation (throw if required values missing)
1.5 Update docker-compose.yml
Add to mvp-frontend service:
volumes:
- ./secrets/app/google-maps-api-key.txt:/run/secrets/google-maps-api-key:ro
environment:
SECRETS_DIR: /run/secrets
1.6 Document Pattern
Create frontend/docs/RUNTIME-CONFIG.md:
- Explain runtime config pattern
- How to add new runtime secrets
- K8s deployment notes
- Local development setup
Phase 2: Backend Improvements
2.1 Add Circuit Breaker Pattern
- Install opossum package: backend/package.json
- Create backend/src/features/stations/external/google-maps/google-maps.circuit-breaker.ts
- Update google-maps.client.ts to wrap API calls
- Follow platform feature pattern (reference: backend/src/features/platform/)
- Config: 10s timeout, 50% threshold, 30s reset
2.2 Backend Unit Tests
Create backend/src/features/stations/tests/unit/:
- stations.service.test.ts - Business logic, user isolation
- stations.repository.test.ts - Database operations, SQL queries
- google-maps.client.test.ts - API mocking, distance calculation
- Use test fixtures for sample data
2.3 Backend Integration Tests
Create backend/src/features/stations/tests/integration/:
- stations.api.test.ts - Full endpoint testing with real database
- saved-stations.flow.test.ts - Save, retrieve, delete workflow
- Use test database (configured in test setup)
2.4 Test Fixtures
Create backend/src/features/stations/tests/fixtures/:
- mock-stations.ts - Sample station objects
- mock-google-response.ts - Mock Google Places API responses
- test-coordinates.ts - Test location data
2.5 Cache Cleanup Job
Create backend/src/features/stations/jobs/cache-cleanup.job.ts:
- Scheduled job (daily at 2 AM)
- Delete station_cache entries older than 24 hours
- Log cleanup metrics (rows deleted)
- Register in main app scheduler
Phase 3: Frontend Foundation
3.1 Type Definitions
Create frontend/src/features/stations/types/stations.types.ts:
- Station interface (id, name, address, location, rating, distance, photoUrl, prices)
- SavedStation interface (extends Station, adds nickname, notes, isFavorite)
- SearchRequest interface (latitude, longitude, radius?, fuelType?)
- SearchResponse interface
- MapMarker interface for map pins
3.2 API Client
Create frontend/src/features/stations/api/stations.api.ts:
- searchStations(request: SearchRequest): Promise<Station[]>
- saveStation(placeId: string, data: SaveStationData): Promise<SavedStation>
- getSavedStations(): Promise<SavedStation[]>
- deleteSavedStation(placeId: string): Promise<void>
- Use axios with proper error handling
- Add request/response logging
3.3 React Query Hooks
Create frontend/src/features/stations/hooks/:
useStationsSearch.ts:
- Mutation hook for search (not cached by default)
- Accepts SearchRequest
- Returns Station array
- Integrates with useGeolocation
useSavedStations.ts:
- Query hook with React Query caching
- Auto-refetch on window focus
- Invalidate on save/delete mutations
useSaveStation.ts:
- Mutation hook
- Optimistic updates to saved list
- Invalidates saved stations cache on success
useDeleteStation.ts:
- Mutation hook
- Optimistic removal from list
- Invalidates cache on success
useGeolocation.ts:
- Browser Geolocation API wrapper
- Permission handling
- Error states (permission denied, unavailable, timeout)
- Returns current position
3.4 Google Maps Integration
Create frontend/src/features/stations/utils/maps-loader.ts:
- Load Google Maps JavaScript API dynamically
- Use runtime config: getConfig().googleMapsApiKey
- Promise-based API
- Singleton pattern (load once)
- TypeScript types for Google Maps objects
Phase 4: Frontend Components
4.1 Core Components
Create frontend/src/features/stations/components/:
StationCard.tsx:
- Material-UI Card component
- Props: station, onSave, onDelete, isSaved
- Display: name (Typography h6), address (Typography body2), distance (Chip)
- Rating stars (Rating component)
- Price display (if available)
- Photo thumbnail (if available)
- Save/Unsave IconButton (BookmarkIcon / BookmarkBorderIcon)
- Directions link (opens Google Maps)
- Mobile: 44px min height, touch targets
- Desktop: hover effects
StationsList.tsx:
- Container for search results
- Props: stations[], loading, error, onSaveStation
- Grid layout (responsive: 1 col mobile, 2 cols tablet, 3 cols desktop)
- Loading skeleton (Skeleton components)
- Empty state: "No stations found. Try adjusting your search."
- Error state with retry button
SavedStationsList.tsx:
- User's favorites display
- Props: savedStations[], onDelete, onSelect
- List layout (vertical)
- Show nickname if set, otherwise station name
- Notes preview (truncated)
- Distance from current location (optional)
- Delete IconButton
- Empty state: "No saved stations yet. Save stations from search results."
StationsSearchForm.tsx:
- Form with search parameters
- Current Location button (uses useGeolocation)
- Manual lat/lng inputs (number fields)
- Radius slider (FormControl, Slider: 1-25 miles, default 5)
- Fuel type select (optional filter)
- Search button (LoadingButton)
- Loading states on button
- Validation: require location (current OR manual)
- Error display for geolocation failures
StationMap.tsx:
- Google Maps embed component
- Props: stations[], center, zoom, onMarkerClick
- Load map using maps-loader utility
- Station markers (color-coded: blue=normal, gold=saved)
- Current location marker (red pin)
- Info windows on marker click (station details + save button)
- Directions button in info window
- Zoom controls, pan controls
- Responsive height (300px mobile, 500px desktop)
4.2 UI Utilities
Create frontend/src/features/stations/utils/:
distance.ts:
- formatDistance(meters: number): string - "1.2 mi" or "0.3 mi"
- calculateDistance(lat1, lng1, lat2, lng2): number - Haversine formula
location.ts:
- getCurrentPosition(): Promise<GeolocationPosition>
- requestLocationPermission(): Promise<boolean>
- Error handling helpers
map-utils.ts:
- createStationMarker(station, map, isSaved): google.maps.Marker
- createInfoWindow(station): google.maps.InfoWindow
- fitBoundsToMarkers(map, markers): void
Phase 5: Desktop Implementation
5.1 Desktop Page
Create frontend/src/features/stations/pages/StationsPage.tsx:
- Layout: Grid container
- Left column (60%): StationMap (full height)
- Right column (40%):
- StationsSearchForm at top
- Tabs: "Search Results" | "Saved Stations"
- Tab 1: StationsList
- Tab 2: SavedStationsList
- State management:
- searchResults (from search mutation)
- savedStations (from query)
- selectedStation (for map focus)
- Effects:
- Load saved stations on mount
- Update map when search results change
- Mobile breakpoint: Stack vertically (map on top)
5.2 Desktop Routing
Update frontend/src/App.tsx (line 556):
- Remove: <Route path="/stations" element={<div>Stations (TODO)</div>} />
- Add: <Route path="/stations" element={<StationsPage />} />
- Import: import { StationsPage } from './features/stations/pages/StationsPage'
- Ensure route is inside ProtectedRoute wrapper
Phase 6: Mobile Implementation
6.1 Mobile Screen
Create frontend/src/features/stations/pages/StationsMobileScreen.tsx:
- BottomNavigation with 3 tabs: Search, Saved, Map
- Tab 0 (Search):
- StationsSearchForm (compact mode)
- StationsList (vertical scroll)
- Pull-to-refresh support
- Tab 1 (Saved):
- SavedStationsList (full screen)
- Swipe to delete gestures
- Tab 2 (Map):
- StationMap (full screen, 100vh)
- Floating search button (FAB) to go back to search tab
- Bottom sheet for station details (SwipeableDrawer)
- Touch targets: 44px minimum
- Safe area insets for notched devices
6.2 Mobile Routing
Update frontend/src/App.tsx:
- Add mobile route: <Route path="/m/stations" element={<StationsMobileScreen />} />
- Update mobile navigation items (add stations)
- Ensure icon is interactive (onClick navigation)
Phase 7: Fuel Logs Integration
7.1 Station Picker Component
Create frontend/src/features/fuel-logs/components/StationPicker.tsx:
- Autocomplete component (Material-UI)
- Props: value, onChange, userLocation?
- Options:
- Saved stations (shown first, grouped)
- Nearby stations (if location available)
- Manual text input (freeSolo mode)
- Option rendering:
- Station name (primary)
- Distance + address (secondary)
- Bookmark icon if saved
- Debounced search (300ms)
- Loading indicator while searching
- Fallback to text input if API fails
7.2 Update Fuel Log Form
Modify frontend/src/features/fuel-logs/components/FuelLogForm.tsx:
- Replace LocationInput with StationPicker
- Props: pass user location from geolocation
- Value binding to stationName field
- Maintain backward compatibility (accepts text string)
- Show nearby stations on field focus
- Optional: "Save this station" checkbox when new station entered
7.3 Integration Features
Optional enhancements:
- In FuelLogCard, link station name to station details
- In StationsPage, show "Recent fuel logs at this station"
- Suggest saving station after logging fuel at new location
Phase 8: Testing
8.1 Backend Tests Execution
cd backend
npm test -- features/stations
- Verify all unit tests pass
- Verify all integration tests pass
- Check coverage report (aim for >80%)
- Fix any failing tests
8.2 Frontend Component Tests
Create frontend/src/features/stations/__tests__/components/:
- StationCard.test.tsx - Rendering, save/delete actions
- StationsList.test.tsx - List rendering, empty/error states
- SavedStationsList.test.tsx - Saved list, delete action
- StationsSearchForm.test.tsx - Form validation, submission
- StationMap.test.tsx - Map initialization (with mocks)
- Use React Testing Library
- Mock API calls with MSW (Mock Service Worker)
8.3 Frontend Page Tests
Create frontend/src/features/stations/__tests__/pages/:
- StationsPage.test.tsx - Desktop page integration
- StationsMobileScreen.test.tsx - Mobile navigation, tabs
- Test user workflows: search, save, delete
- Test error handling and loading states
8.4 API Client Tests
Create frontend/src/features/stations/api/__tests__/:
- stations.api.test.ts - Test all API methods
- Mock axios responses
- Test error handling (network, 401, 500)
- Verify request payloads
8.5 React Query Hook Tests
Create frontend/src/features/stations/hooks/__tests__/:
- Test each hook in isolation
- Mock React Query
- Test loading, success, error states
- Test cache invalidation logic
- Test optimistic updates
8.6 E2E Tests
Create frontend/cypress/e2e/stations.cy.ts (or similar E2E framework):
- Test: Search for nearby stations
- Test: Save a station to favorites
- Test: View saved stations list
- Test: Delete a saved station
- Test: Use station in fuel log entry
- Test: Mobile navigation flow
- Test: Map marker click and info window
- Use real backend API in test environment
Phase 9: Documentation
9.1 Backend Feature Docs
Create backend/src/features/stations/docs/:
ARCHITECTURE.md:
- System design overview
- Data flow diagrams
- External dependencies (Google Maps API)
- Caching strategy
- Database schema
- Circuit breaker pattern
API.md:
- All endpoint documentation
- Request/response examples (curl commands)
- Authentication requirements
- Rate limits and quotas
- Error responses
TESTING.md:
- How to run tests
- Test database setup
- Writing new tests
- Coverage goals
GOOGLE-MAPS-SETUP.md:
- API key creation in Google Cloud Console
- Required APIs to enable
- Quota management
- Cost estimation
- Security best practices
9.2 Frontend Feature Docs
Create frontend/src/features/stations/README.md:
- Feature overview
- Component hierarchy
- Hook usage examples
- Adding new functionality
- Runtime config pattern
- Testing guide
9.3 Runtime Config Documentation
Already created in Phase 1.6: frontend/docs/RUNTIME-CONFIG.md
9.4 Update Main Documentation
Update docs/README.md:
- Add "Gas Stations" to features list
- Link to backend and frontend docs
- Mention Google Maps integration
Update backend/src/features/stations/README.md:
- Complete feature overview
- Configuration requirements
- API endpoints summary
- Testing instructions
- Deployment notes
Phase 10: Validation & Polish
10.1 Docker Build & Test
make rebuild # Rebuild all containers
make logs # Watch for errors
make migrate # Run database migrations
make test # Run all tests
Verify:
- Frontend config.js is generated at startup
- Backend loads Google Maps API key from secret
- All containers start without errors
- Database migrations create tables
- Tests pass in container environment
10.2 Linting & Formatting
# Backend
cd backend && npm run lint && npm run type-check
# Frontend
cd frontend && npm run lint && npm run type-check
Fix all issues (MUST be 100% green per CLAUDE.md):
- Zero TypeScript errors
- Zero ESLint warnings
- Zero Prettier formatting issues
10.3 Manual Testing - Desktop
- Open https://motovaultpro.com/stations in browser
- Test geolocation permission flow
- Search for stations near current location
- Verify map loads with markers
- Click marker, verify info window
- Save a station
- View saved stations list
- Delete a saved station
- Test manual lat/lng search
- Test with no results (remote location)
- Test error handling (API failure)
10.4 Manual Testing - Mobile
- Open mobile nav, navigate to Stations
- Test all 3 tabs (Search, Saved, Map)
- Verify 44px touch targets
- Test swipe gestures
- Test bottom sheet interactions
- Verify responsive layout
- Test on actual mobile device (if possible)
10.5 Fuel Logs Integration Testing
- Navigate to Fuel Logs
- Create new fuel log entry
- Test StationPicker autocomplete
- Select a saved station
- Enter new station name
- Submit fuel log
- Verify station name is saved
10.6 Cross-Browser Testing
Test on:
- Chrome (latest)
- Safari (latest)
- Firefox (latest)
- Mobile Safari (iOS)
- Mobile Chrome (Android)
10.7 Performance Validation
- Check map load time (<2s)
- Verify API response times (<500ms)
- Check Redis cache hits (backend logs)
- Monitor Google Maps API quota usage
- Verify no memory leaks (React DevTools Profiler)
Phase 11: Deployment Preparation
11.1 Configuration Checklist
Backend:
- Secret file exists: ./secrets/app/google-maps-api-key.txt
- Docker compose mounts secret to /run/secrets/google-maps-api-key
- Backend config loader reads from SECRETS_DIR
Frontend:
- Secret file mounted: /run/secrets/google-maps-api-key
- Entrypoint script generates /usr/share/nginx/html/config.js
- index.html loads config.js before app
- App accesses via getConfig().googleMapsApiKey
11.2 Database Migration
make migrate
Verify:
- Migration 001_create_stations_tables.sql runs successfully
- Tables created: station_cache, saved_stations
- Indexes created on both tables
- Test queries work
11.3 Secrets Verification
# Verify backend can read secret
docker compose exec mvp-backend cat /run/secrets/google-maps-api-key
# Verify frontend generates config
docker compose exec mvp-frontend cat /usr/share/nginx/html/config.js
# Should show: window.CONFIG = { googleMapsApiKey: "..." }
11.4 Health Checks
- Verify /health endpoint includes stations feature
- Test stations API endpoints with curl:
# Get JWT token first
TOKEN="your-jwt-token"
# Search stations
curl -X POST http://localhost:3001/api/stations/search \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"latitude": 37.7749, "longitude": -122.4194}'
# Get saved stations
curl http://localhost:3001/api/stations/saved \
-H "Authorization: Bearer $TOKEN"
11.5 Production Readiness
Update config/app/production.yml.example:
- Document Google Maps API key requirement
- Add example configuration
Create deployment checklist:
- Google Maps API key in secrets
- Frontend container mounts secret
- Backend container mounts secret
- Database migrations run
- All tests pass
- Linters pass
- Manual testing complete
- Performance acceptable
- Documentation complete
Success Criteria (per CLAUDE.md)
- All linters pass with zero issues
- All tests pass (backend + frontend)
- Feature works end-to-end on desktop
- Feature works end-to-end on mobile
- Fuel logs integration functional
- Map visualization working
- Google Maps API integration live via runtime secrets
- K8s-aligned secrets pattern implemented
- Documentation complete
- Old code deleted (no orphaned files)
K8s Secrets Pattern Summary
Backend (already implemented):
- Secret mounted: /run/secrets/google-maps-api-key
- Read at runtime by config-loader.ts
- Never in environment variables or code
Frontend (new implementation):
- Secret mounted: /run/secrets/google-maps-api-key
- Read at container startup by entrypoint script
- Injected into config.js served with static files
- App loads config at runtime via window.CONFIG
- Can be updated by restarting container (no rebuild)
This mirrors K8s deployment where:
- Secrets are mounted as volumes
- Applications read them at runtime
- Secrets can be rotated without rebuilding images
Estimated Timeline
- Phase 1 (Frontend Secrets): 1 day
- Phase 2 (Backend): 1 day
- Phase 3 (Foundation): 0.5 day
- Phase 4 (Components): 1 day
- Phase 5 (Desktop): 0.5 day
- Phase 6 (Mobile): 0.5 day
- Phase 7 (Integration): 0.5 day
- Phase 8 (Testing): 1.5 days
- Phase 9 (Docs): 0.5 day
- Phase 10 (Validation): 0.5 day
- Phase 11 (Deployment): 0.5 day
Total: 8 days (full implementation with K8s-aligned secrets)
Reference Implementations
- Secrets pattern: Backend config-loader.ts and docker-compose.yml
- Feature structure: frontend/src/features/vehicles/
- Component patterns: Vehicles components
- Testing: Vehicles test suite
- Mobile + Desktop: Vehicles pages

File diff suppressed because it is too large Load Diff

View File

@@ -1,260 +1 @@
*** CRITICAL ***: Never read this file. Stop and move on. *** CRITICAL ***: Never read this file. Stop and move on.
*** STARTING PROMPT ***
## Agent Team (Specialized Development)
MotoVaultPro uses a 4-agent team for optimal development velocity and quality enforcement.
### When to Use Agents
**Feature Capsule Agent** - Backend feature development
- Implementing new features in `backend/src/features/{feature}/`
- Creating API endpoints, business logic, database access
- Platform service integration
- Backend tests and migrations
**Platform Feature Agent** - Integrated platform module
- Enhancing `backend/src/features/platform/`
- Vehicle lookup + VIN decoding logic
- Redis/SQL performance tuning for platform endpoints
- Platform module tests and documentation
**Mobile-First Frontend Agent** - Responsive UI/UX
- React components in `frontend/src/features/{feature}/`
- Mobile + desktop responsive design (NON-NEGOTIABLE)
- Forms, validation, and React Query integration
- Frontend tests and accessibility
**Quality Enforcer Agent** - Quality assurance
- Running complete test suites
- Validating linting and type checking
- Enforcing "all green" policy (ZERO TOLERANCE)
- Mobile + desktop validation
### Agent Spawning Examples
```
# Backend feature development
Task: "Implement {feature} backend following feature capsule pattern.
Read backend/src/features/{feature}/README.md and implement API, domain, data layers with tests."
Agent: Feature Capsule Agent
# Frontend development
Task: "Build responsive UI for {feature}. Read backend API docs and implement mobile-first.
Test on 320px and 1920px viewports."
Agent: Mobile-First Frontend Agent
# Platform module work
Task: "Enhance platform feature capsule (backend/src/features/platform).
Implement API/domain/data changes with accompanying tests."
Agent: Platform Feature Agent
# Quality validation
Task: "Validate {feature} quality gates. Run all tests, check linting, verify mobile + desktop.
Report pass/fail with details."
Agent: Quality Enforcer Agent
```
### Agent Coordination Workflow
1. Feature Capsule Agent → Implements backend
2. Mobile-First Frontend Agent → Implements UI (parallel)
3. Quality Enforcer Agent → Validates everything
4. Expert Software Architect → Reviews and approves
### When Coordinator Handles Directly
- Quick bug fixes (single file)
- Documentation updates
- Configuration changes
- Simple code reviews
- Answering questions
## Key Commands
- Start: `make start`
- Rebuild: `make rebuild`
- Logs: `make logs`
- Test: `make test`
- Migrate: `make migrate`
- Shell (backend): `make shell-backend`
- Shell (frontend): `make shell-frontend`
## Development Rules
1. NEVER use emojis in code or documentation
2. Every feature MUST be responsive (mobile + desktop) - NON-NEGOTIABLE
3. Testing and debugging can be done locally
4. All testing and debugging needs to be verified in containers
5. Each backend feature is self-contained in `backend/src/features/{name}/`
6. Delete old code when replacing (no commented code)
7. Use meaningful variable names (`userID` not `id`)
8. ALL quality gates must pass (all green policy)
9. Feature capsules are self-contained modules
## Making Changes
### Frontend Changes (React)
**Agent**: Mobile-First Frontend Agent
- Components: `frontend/src/features/{feature}/components/`
- Types: `frontend/src/features/{feature}/types/`
- After changes: `make rebuild` then test at https://admin.motovaultpro.com
- MUST test on mobile (320px) AND desktop (1920px)
### Backend Changes (Node.js)
**Agent**: Feature Capsule Agent
- API: `backend/src/features/{feature}/api/`
- Business logic: `backend/src/features/{feature}/domain/`
- Database: `backend/src/features/{feature}/data/`
- After changes: `make rebuild` then check logs
### Platform Module Changes (TypeScript)
**Agent**: Platform Feature Agent
- Feature capsule: `backend/src/features/platform/`
- API routes: `backend/src/features/platform/api/`
- Domain/data: `backend/src/features/platform/domain/` and `data/`
- After changes: `make rebuild` then verify platform endpoints via backend logs/tests
### Database Changes
- Add migration: `backend/src/features/{feature}/migrations/00X_description.sql`
- Run: `make migrate`
- Validate: Check logs and test affected features
### Adding NPM Packages
- Edit `package.json` (frontend or backend)
- Run `make rebuild` (no local npm install)
- Containers handle dependency installation
## Common Tasks
### Add a New Feature (Full Stack)
1. Spawn Feature Capsule Agent for backend
2. Spawn Mobile-First Frontend Agent for UI (parallel)
3. Feature Capsule Agent: API + domain + data + tests
4. Mobile-First Agent: Components + forms + tests
5. Spawn Quality Enforcer Agent for validation
6. Review and approve
### Add a Form Field
1. Update types in frontend/backend
2. Add to database migration if needed
3. Update React form component (Mobile-First Agent)
4. Update backend validation (Feature Capsule Agent)
5. Test with `make rebuild`
6. Validate with Quality Enforcer Agent
### Add New API Endpoint
**Agent**: Feature Capsule Agent
1. Create route in `backend/src/features/{feature}/api/`
2. Add service method in `domain/`
3. Add repository method in `data/`
4. Write unit and integration tests
5. Test with `make rebuild`
### Fix UI Responsiveness
**Agent**: Mobile-First Frontend Agent
1. Use Tailwind classes: `sm:`, `md:`, `lg:`
2. Test on mobile viewport (320px, 375px, 768px)
3. Test on desktop viewport (1024px, 1920px)
4. Ensure touch targets are 44px minimum
5. Validate keyboard navigation on desktop
### Add Platform Integration
**Agents**: Platform Feature Agent + Feature Capsule Agent
1. Platform Feature Agent: Implement/update platform endpoint logic
2. Feature Capsule Agent: Update consuming feature client (e.g. `external/platform-vehicles/`)
3. Feature Capsule Agent: Adjust caching/circuit breaker strategies as needed
4. Joint testing: run targeted unit/integration suites
5. Quality Enforcer Agent: Validate end-to-end
### Run Quality Checks
**Agent**: Quality Enforcer Agent
1. Run all tests: `make test`
2. Check linting: `npm run lint` (backend container)
3. Check types: `npm run type-check` (backend container)
4. Validate mobile + desktop
5. Report pass/fail with details
## Quality Gates (MANDATORY)
Code is complete when:
- All linters pass with zero issues
- All tests pass (100% green)
- Feature works end-to-end
- Mobile + desktop validated (for frontend)
- Old code is deleted
- Documentation updated
- Test coverage >= 80% for new code
## Architecture Quick Reference
### Application Stack
- **Backend Feature Capsules**: Modular monolith in `backend/src/features/`
- **Platform Module**: Vehicle data + VIN decoding in `backend/src/features/platform/`
- **Frontend**: React SPA in `frontend/src/`
### Feature Capsule Pattern
Each feature is self-contained:
```
backend/src/features/{feature}/
├── README.md # Complete feature documentation
├── api/ # HTTP layer
├── domain/ # Business logic
├── data/ # Database access
├── migrations/ # Schema changes
├── external/ # Platform service clients
└── tests/ # Unit + integration tests
```
## Important Context
- **Auth**: Frontend uses Auth0, backend validates JWTs
- **Database**: PostgreSQL with user-isolated data (user_id scoping)
- **Platform APIs**: Exposed via `/api/platform/*`, secured with Auth0 JWTs
- **Caching**: Redis with feature-specific TTL strategies
- **Testing**: Jest (backend + frontend)
- **Docker-First**: All development in containers (production-only)
## Agent Coordination Rules
### Clear Ownership
- Feature Capsule Agent: Backend application features
- Platform Feature Agent: Platform capsule inside backend
- Mobile-First Frontend Agent: All UI/UX code
- Quality Enforcer Agent: Testing and validation only
### Handoff Protocol
1. Development agent completes work
2. Development agent hands off to Quality Enforcer
3. Quality Enforcer validates all quality gates
4. Quality Enforcer reports pass/fail
5. If fail: Development agent fixes issues
6. If pass: Expert Software Architect approves
### Parallel Development
- Feature Capsule + Mobile-First work simultaneously
- Both agents have clear boundaries
- Both hand off to Quality Enforcer when ready
- Quality Enforcer validates complete feature
## Current Task
[Describe your specific task here - e.g., "Add notes field to vehicle form", "Create maintenance reminders feature", "Integrate new platform service"]
**Recommended Agent**: [Which agent should handle this task]
**Steps**:
1. [Step 1]
2. [Step 2]
3. [Step 3]
## References
- Agent Definitions: `.claude/agents/`
- Architecture: `docs/PLATFORM-SERVICES.md`
- Testing: `docs/TESTING.md`
- Context Loading: `.ai/context.json`
- Development Guidelines: `CLAUDE.md`
- Feature Documentation: `backend/src/features/{feature}/README.md`

View File

@@ -136,7 +136,7 @@ npm test -- vehicles.service.test.ts
npm test -- --testNamePattern="VIN validation" npm test -- --testNamePattern="VIN validation"
# Frontend tests (Jest) # Frontend tests (Jest)
make test-frontend docker compose exec mvp-frontend npm test
``` ```
### Coverage Reports ### Coverage Reports
@@ -271,7 +271,7 @@ docker compose exec mvp-frontend npm test
### Feature Development Workflow ### Feature Development Workflow
1. **Write tests first**: TDD approach preferred 1. **Write tests first**: TDD approach preferred
2. **Run tests continuously**: Use `npm run test:watch` 2. **Run tests continuously**: Use `npm run test:watch`
3. **Test in containers**: Always verify with `make test` 3. **Test in containers**: Always verify with `docker compose exec mvp-backend npm test`
4. **Check coverage**: Ensure new code is covered 4. **Check coverage**: Ensure new code is covered
5. **Integration last**: Run full test suite before PR 5. **Integration last**: Run full test suite before PR

View File

@@ -19,27 +19,30 @@ This document explains the endtoend Vehicles API architecture after the pl
Idempotent constraints/indexes added where applicable (e.g., unique lower(name), unique(model_id, year), guarded `CREATE INDEX IF NOT EXISTS`, guarded trigger). Idempotent constraints/indexes added where applicable (e.g., unique lower(name), unique(model_id, year), guarded `CREATE INDEX IF NOT EXISTS`, guarded trigger).
### API Endpoints (Bearer auth required) ### Dropdown API (Bearer auth required)
Prefix: `/api/platform` Prefix: `/api/vehicles/dropdown`
- `GET /api/platform/years``[number]` distinct years (desc) - `GET /years``[number]` (latest to oldest model years)
- `GET /api/platform/makes?year={year}``{ makes: { id, name }[] }` - `GET /makes?year={year}``{ id, name }[]` (makes available in that year)
- `GET /api/platform/models?year={year}&make_id={make_id}``{ models: { id, name }[] }` - `GET /models?year={year}&make_id={make}``{ id, name }[]` (models filtered by year + make)
- `GET /api/platform/trims?year={year}&make_id={make_id}&model_id={model_id}``{ trims: { id, name }[] }` - `GET /trims?year={year}&make_id={make}&model_id={model}``{ id, name }[]` (trims for the selection)
- `GET /api/platform/engines?year={year}&make_id={make_id}&model_id={model_id}&trim_id={trim_id}``{ engines: { id, name }[] }` - `GET /engines?year={year}&make_id={make}&model_id={model}&trim_id={trim}``{ id, name }[]` (engines for the trim)
- `GET /transmissions?year={year}&make_id={make}&model_id={model}``{ id, name }[]` (`Automatic`, `Manual`)
Notes: Selection order is enforced: each endpoint requires the IDs returned by the previous stage, so users never see invalid combinations (e.g., 2023 Chevrolet excludes the S-10, 1999 GMC trims exclude AT4X, etc.).
- `make_id` is maintained for a consistent query chain, but engines are enforced by `(year, model_id, trim_id)`.
- Trims/engines include `id` to enable the next hop in the UI. ### Platform module (internal bridge)
Prefix: `/api/platform`
- Same hierarchical endpoints as above, but responses are wrapped (`{ makes: [...] }`, `{ models: [...] }`) for service-to-service integrations.
- VIN decode endpoint: `GET /api/platform/vehicle?vin={vin}`
### Authentication ### Authentication
- Auth0 JWT via `Authorization: Bearer ${JWT_TOKEN}` (required for all platform endpoints) - Auth0 JWT via `Authorization: Bearer ${JWT_TOKEN}` (required for both `/api/vehicles/*` and `/api/platform/*`)
- Configured in backend: `src/core/plugins/auth.plugin.ts` with JWKS validation - Configured in backend: `src/core/plugins/auth.plugin.ts` with JWKS validation
### Caching (Redis) ### Caching (Redis)
- Keys: `dropdown:years`, `dropdown:makes:{year}`, `dropdown:models:{year}:{make}`, `dropdown:trims:{year}:{model}`, `dropdown:engines:{year}:{model}:{trim}` - Keys: `platform:years`, `platform:vehicle-data:makes:{year}`, `platform:vehicle-data:models:{year}:{make}`, `platform:vehicle-data:trims:{year}:{model}`, `platform:vehicle-data:engines:{year}:{model}:{trim}`
- Dropdown data TTL: 6 hours (21600 seconds) - Dropdown TTL: 6 hours (`21600s`)
- VIN decode cache TTL: 7 days (604800 seconds) - VIN decode TTL: 7 days (`604800s`) via `platform:vin-decode:{vin}`
- Cache key format for VIN decodes: `vin:decode:{vin}`
- Implementation: `backend/src/features/platform/domain/platform-cache.service.ts` - Implementation: `backend/src/features/platform/domain/platform-cache.service.ts`
### Seeds & Specific Examples ### Seeds & Specific Examples
@@ -56,22 +59,12 @@ Clear platform cache:
## MotoVaultPro Backend (Application Service) ## MotoVaultPro Backend (Application Service)
### Proxy Dropdown Endpoints ### Proxy Dropdown Endpoints
Prefix: `/api/vehicles/dropdown` Vehicles service simply re-emits the data returned by `Platform VehicleDataService`, normalizing it to `{ id, name }[]` arrays for frontend consumption. Redis caching and Postgres lookups all remain centralized in the platform module.
- `GET /years``[number]` (calls platform `/years`)
- `GET /makes?year=YYYY``{ id, name }[]`
- `GET /models?year=YYYY&make_id=ID``{ id, name }[]`
- `GET /trims?year=YYYY&make_id=ID&model_id=ID``{ id, name }[]`
- `GET /engines?year=YYYY&make_id=ID&model_id=ID&trim_id=ID``{ id, name }[]`
Changes: ### Platform Integration
- Engines route now requires `trim_id`. - `VehiclesService` lazily instantiates `VehicleDataService` via `getVehicleDataService()`, guaranteeing a shared cache.
- New `/years` route for UI bootstrap. - Each dropdown call passes the shared Postgres pool (`getPool()`) and propagates selection context (year/make/model/trim).
- Transmission dropdown is intentionally static (`Automatic`, `Manual`) until richer metadata is available.
### Platform Client & Integration
- `PlatformVehiclesClient`:
- Added `getYears()`
- `getEngines(year, makeId, modelId, trimId)` to pass trim id
- `PlatformIntegrationService` consumed by `VehiclesService` updated accordingly.
### Authentication (App) ### Authentication (App)
- Auth0 JWT enforced via Fastify + JWKS. No mock users. - Auth0 JWT enforced via Fastify + JWKS. No mock users.
@@ -94,6 +87,7 @@ Changes:
- APIs used: - APIs used:
- `/api/vehicles/dropdown/years` - `/api/vehicles/dropdown/years`
- `/api/vehicles/dropdown/makes|models|trims|engines` - `/api/vehicles/dropdown/makes|models|trims|engines`
- `/api/vehicles/dropdown/transmissions`
## Add Vehicle Form Change/Add/Modify/Delete Fields (Fast Track) ## Add Vehicle Form Change/Add/Modify/Delete Fields (Fast Track)
@@ -135,7 +129,7 @@ VIN/License rule
- Backend (includes platform module): `docker compose up -d --build backend` - Backend (includes platform module): `docker compose up -d --build backend`
### Logs & Health ### Logs & Health
- Backend: `/health` shows status/feature list, including platform readiness - Backend: `https://motovaultpro.com/api/health` shows status/feature list, including platform readiness
- Logs: `make logs-backend`, `make logs-frontend` - Logs: `make logs-backend`, `make logs-frontend`
### Common Reset Sequences ### Common Reset Sequences

View File

@@ -890,7 +890,7 @@ const sortedStations = useMemo(() => {
**Solutions**: **Solutions**:
1. Check browser permissions (user must allow) 1. Check browser permissions (user must allow)
2. Requires HTTPS in production (not localhost) 2. Requires HTTPS in production (use https://motovaultpro.com)
3. Some browsers block geolocation in iframes 3. Some browsers block geolocation in iframes
4. Fallback to manual coordinates 4. Fallback to manual coordinates

View File

@@ -32,34 +32,34 @@ export const vehiclesApi = {
await apiClient.delete(`/vehicles/${id}`); await apiClient.delete(`/vehicles/${id}`);
}, },
// Dropdown API methods (authenticated) - using unified platform endpoints // Dropdown API methods (authenticated) - proxied through vehicles capsule
getYears: async (): Promise<number[]> => { getYears: async (): Promise<number[]> => {
const response = await apiClient.get('/platform/years'); const response = await apiClient.get('/vehicles/dropdown/years');
return response.data; return response.data;
}, },
getMakes: async (year: number): Promise<DropdownOption[]> => { getMakes: async (year: number): Promise<DropdownOption[]> => {
const response = await apiClient.get(`/platform/makes?year=${year}`); const response = await apiClient.get(`/vehicles/dropdown/makes?year=${year}`);
return response.data; return response.data;
}, },
getModels: async (year: number, makeId: number): Promise<DropdownOption[]> => { getModels: async (year: number, makeId: number): Promise<DropdownOption[]> => {
const response = await apiClient.get(`/platform/models?year=${year}&make_id=${makeId}`); const response = await apiClient.get(`/vehicles/dropdown/models?year=${year}&make_id=${makeId}`);
return response.data; return response.data;
}, },
getTransmissions: async (year: number, makeId: number, modelId: number): Promise<DropdownOption[]> => { getTransmissions: async (year: number, makeId: number, modelId: number): Promise<DropdownOption[]> => {
const response = await apiClient.get(`/platform/transmissions?year=${year}&make_id=${makeId}&model_id=${modelId}`); const response = await apiClient.get(`/vehicles/dropdown/transmissions?year=${year}&make_id=${makeId}&model_id=${modelId}`);
return response.data; return response.data;
}, },
getEngines: async (year: number, makeId: number, modelId: number, trimId: number): Promise<DropdownOption[]> => { getEngines: async (year: number, makeId: number, modelId: number, trimId: number): Promise<DropdownOption[]> => {
const response = await apiClient.get(`/platform/engines?year=${year}&make_id=${makeId}&model_id=${modelId}&trim_id=${trimId}`); const response = await apiClient.get(`/vehicles/dropdown/engines?year=${year}&make_id=${makeId}&model_id=${modelId}&trim_id=${trimId}`);
return response.data; return response.data;
}, },
getTrims: async (year: number, makeId: number, modelId: number): Promise<DropdownOption[]> => { getTrims: async (year: number, makeId: number, modelId: number): Promise<DropdownOption[]> => {
const response = await apiClient.get(`/platform/trims?year=${year}&make_id=${makeId}&model_id=${modelId}`); const response = await apiClient.get(`/vehicles/dropdown/trims?year=${year}&make_id=${makeId}&model_id=${modelId}`);
return response.data; return response.data;
}, },

View File

@@ -72,6 +72,7 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
const [models, setModels] = useState<DropdownOption[]>([]); const [models, setModels] = useState<DropdownOption[]>([]);
const [engines, setEngines] = useState<DropdownOption[]>([]); const [engines, setEngines] = useState<DropdownOption[]>([]);
const [trims, setTrims] = useState<DropdownOption[]>([]); const [trims, setTrims] = useState<DropdownOption[]>([]);
const [transmissions, setTransmissions] = useState<DropdownOption[]>([]);
const [selectedYear, setSelectedYear] = useState<number | undefined>(); const [selectedYear, setSelectedYear] = useState<number | undefined>();
const [selectedMake, setSelectedMake] = useState<DropdownOption | undefined>(); const [selectedMake, setSelectedMake] = useState<DropdownOption | undefined>();
const [selectedModel, setSelectedModel] = useState<DropdownOption | undefined>(); const [selectedModel, setSelectedModel] = useState<DropdownOption | undefined>();
@@ -155,6 +156,7 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
setModels([]); setModels([]);
setEngines([]); setEngines([]);
setTrims([]); setTrims([]);
setTransmissions([]);
setSelectedMake(undefined); setSelectedMake(undefined);
setSelectedModel(undefined); setSelectedModel(undefined);
setValue('make', ''); setValue('make', '');
@@ -189,6 +191,7 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
// Clear dependent selections // Clear dependent selections
setEngines([]); setEngines([]);
setTrims([]); setTrims([]);
setTransmissions([]);
setSelectedModel(undefined); setSelectedModel(undefined);
setValue('model', ''); setValue('model', '');
setValue('transmission', ''); setValue('transmission', '');
@@ -215,8 +218,12 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
const loadTrims = async () => { const loadTrims = async () => {
setLoadingDropdowns(true); setLoadingDropdowns(true);
try { try {
const trimsData = await vehiclesApi.getTrims(watchedYear, selectedMake.id, modelOption.id); const [trimsData, transmissionsData] = await Promise.all([
vehiclesApi.getTrims(watchedYear, selectedMake.id, modelOption.id),
vehiclesApi.getTransmissions(watchedYear, selectedMake.id, modelOption.id)
]);
setTrims(trimsData); setTrims(trimsData);
setTransmissions(transmissionsData);
setSelectedModel(modelOption); setSelectedModel(modelOption);
// Clear deeper selections // Clear deeper selections
setEngines([]); setEngines([]);
@@ -228,6 +235,7 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
console.error('Failed to load detailed data:', error); console.error('Failed to load detailed data:', error);
setTrims([]); setTrims([]);
setEngines([]); setEngines([]);
setTransmissions([]);
} finally { } finally {
setLoadingDropdowns(false); setLoadingDropdowns(false);
} }
@@ -397,7 +405,7 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
</select> </select>
</div> </div>
{/* Transmission (right, static options) */} {/* Transmission (right) */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Transmission Transmission
@@ -405,11 +413,15 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
<select <select
{...register('transmission')} {...register('transmission')}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500 min-h-[44px]" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500 min-h-[44px]"
disabled={loadingDropdowns || transmissions.length === 0}
style={{ fontSize: '16px' }} style={{ fontSize: '16px' }}
> >
<option value="">Select Transmission</option> <option value="">Select Transmission</option>
<option value="Automatic">Automatic</option> {transmissions.map((transmission) => (
<option value="Manual">Manual</option> <option key={transmission.id} value={transmission.name}>
{transmission.name}
</option>
))}
</select> </select>
</div> </div>
</div> </div>