diff --git a/Makefile b/Makefile index fd3c6db..e3aa3a1 100644 --- a/Makefile +++ b/Makefile @@ -75,16 +75,6 @@ clean: @docker compose down -v --rmi all @docker system prune -f -test: - @echo "Running backend tests in container..." - @docker compose exec admin-backend npm test - @echo "\nRunning frontend tests in container..." - @docker run --rm -v $(PWD)/frontend:/app -w /app node:20-alpine sh -lc 'npm install && npm test' - -test-frontend: - @echo "Running frontend tests in container..." - @docker run --rm -v $(PWD)/frontend:/app -w /app node:20-alpine sh -lc 'npm install && npm test' - logs: @docker compose logs -f @@ -168,57 +158,6 @@ health-check-all: @echo " Discovered Services: $$(curl -s http://localhost: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')" -# Enhanced monitoring commands for Phase 2 -metrics: - @echo "📊 Prometheus Metrics Collection:" - @echo "" - @echo "Traefik Metrics:" - @curl -s http://localhost:8080/metrics | grep "traefik_" | head -5 || echo "Metrics not available" - @echo "" - @echo "Service Response Times (last 5min):" - @curl -s http://localhost:8080/metrics | grep "traefik_service_request_duration" | head -3 || echo "No duration metrics yet" - -service-auth-test: - @echo "🔐 Service-to-Service Authentication Test:" - @echo "" - @echo "Testing platform API authentication..." - @echo " Vehicles API: $$(curl -k -s -o /dev/null -w '%{http_code}' -H 'X-API-Key: mvp-platform-vehicles-secret-key' https://admin.motovaultpro.com/api/platform/vehicles/health 2>/dev/null || echo 'FAIL')" - @echo " Tenants API: $$(curl -k -s -o /dev/null -w '%{http_code}' -H 'X-API-Key: mvp-platform-tenants-secret-key' https://admin.motovaultpro.com/api/platform/tenants/health 2>/dev/null || echo 'FAIL')" - -middleware-test: - @echo "🛡️ Middleware Security Test:" - @echo "" - @echo "Testing security headers..." - @curl -k -s -I https://admin.motovaultpro.com/ | grep -E "(X-Frame-Options|X-Content-Type-Options|Strict-Transport-Security)" || echo "Security headers not applied" - @echo "" - @echo "Testing rate limiting..." - @for i in $$(seq 1 3); do curl -k -s -o /dev/null -w "Request $$i: %{http_code}\n" https://admin.motovaultpro.com/; done - -network-security-test: - @echo "🔒 Network Security Isolation Test:" - @echo "" - @echo "Testing network isolation:" - @docker network inspect motovaultpro_backend motovaultpro_database motovaultpro_platform | jq '.[].Options."com.docker.network.bridge.enable_icc"' | head -3 | sed 's/^/ Network ICC: /' - @echo "" - @echo "Internal network test:" - @echo " Backend → Platform: $$(docker compose exec admin-backend nc -zv mvp-platform-vehicles-api 8000 2>&1 | grep -q 'open' && echo 'CONNECTED' || echo 'ISOLATED')" - -# Mobile Testing Support -mobile-setup: - @echo "📱 Mobile Testing Setup (K8s-Ready Architecture):" - @echo "" - @echo "1. Connect mobile device to same network as development machine" - @echo "2. Development machine IP: $$(hostname -I | awk '{print $$1}' 2>/dev/null || echo 'unknown')" - @echo "3. Add to mobile device DNS/hosts (if rooted):" - @echo " $$(hostname -I | awk '{print $$1}' 2>/dev/null) motovaultpro.com" - @echo " $$(hostname -I | awk '{print $$1}' 2>/dev/null) admin.motovaultpro.com" - @echo "4. Install and trust certificate from: https://$$(hostname -I | awk '{print $$1}' 2>/dev/null)/certs/motovaultpro.com.crt" - @echo "5. Access applications:" - @echo " 🌐 Landing: https://motovaultpro.com" - @echo " 📱 Admin App: https://admin.motovaultpro.com" - @echo "" - @echo "Certificate Generation (if needed): make generate-certs" - # SSL Certificate Generation generate-certs: @echo "Generating multi-domain SSL certificate for mobile compatibility..." @@ -230,67 +169,6 @@ generate-certs: -extensions SAN @echo "✅ Certificate generated with SAN for mobile compatibility (includes $(shell hostname -I | awk '{print $$1}'))" -# Configuration Management Commands (Phase 3) -config-validate: - @echo "🔍 K8s-Equivalent Configuration Validation:" - @./scripts/config-validator.sh - -config-setup: - @echo "📝 Setting up K8s-equivalent configuration and secrets:" - @./scripts/config-validator.sh --generate-templates - @echo "" - @echo "Next steps:" - @echo " 1. Update secret values: edit files in secrets/app/ and secrets/platform/" - @echo " 2. Validate configuration: make config-validate" - @echo " 3. Deploy with new config: make deploy-with-config" - -config-status: - @echo "📊 Configuration Management Status:" - @echo "" - @echo "ConfigMaps (K8s equivalent):" - @find config -name "*.yml" -exec echo " ✅ {}" \; 2>/dev/null || echo " ❌ No config files found" - @echo "" - @echo "Secrets (K8s equivalent):" - @find secrets -name "*.txt" | grep -v example | wc -l | sed 's/^/ 📁 Secret files: /' - @echo "" - @echo "Docker Compose mounts:" - @grep -c "config.*yml\|/run/secrets" docker-compose.yml | sed 's/^/ 🔗 Configuration mounts: /' || echo " ❌ No configuration mounts found" - -deploy-with-config: - @echo "🚀 Deploying with K8s-equivalent configuration management:" - @echo "1. Validating configuration..." - @./scripts/config-validator.sh - @echo "" - @echo "2. Stopping existing services..." - @docker compose down - @echo "" - @echo "3. Starting services with file-based configuration..." - @docker compose up -d --build - @echo "" - @echo "4. Verifying configuration loading..." - @sleep 10 - @make health-check-all - -config-reload: - @echo "🔄 Hot-reloading configuration (K8s ConfigMap equivalent):" - @echo "Restarting services that support configuration hot-reload..." - @docker compose restart traefik - @echo "✅ Configuration reloaded for supported services" - @echo "⚠️ Note: Some services may require full restart for config changes" - -config-backup: - @echo "💾 Backing up current configuration:" - @mkdir -p backups/config-$$(date +%Y%m%d-%H%M%S) - @cp -r config secrets backups/config-$$(date +%Y%m%d-%H%M%S)/ - @echo "✅ Configuration backed up to backups/config-$$(date +%Y%m%d-%H%M%S)/" - -config-diff: - @echo "🔍 Configuration diff from defaults:" - @echo "App configuration changes:" - @diff -u config/app/production.yml.example config/app/production.yml || echo " (No example file to compare)" - @echo "" - @echo "Secret files status:" - @ls -la secrets/app/*.txt | grep -v example || echo " No secrets found" # Enhanced log commands with filtering logs-traefik: @@ -302,75 +180,5 @@ logs-platform: logs-backend-full: @docker compose logs -f admin-backend admin-postgres admin-redis admin-minio -# Phase 4: Optimization & Monitoring Commands -resource-optimization: - @echo "🔧 Resource Optimization Analysis:" - @echo "" - @echo "Current Resource Usage:" - @docker stats --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}" | head -15 - @echo "" - @echo "Resource Recommendations:" - @echo " 🔍 Checking for over-allocated services..." - @docker stats --no-stream | awk 'NR>1 {if ($$3 ~ /%/ && $$3+0 < 50) print " ⬇️ "$1" can reduce CPU allocation (using "$3")"}' | head -5 - @docker stats --no-stream | awk 'NR>1 {if ($$7 ~ /%/ && $$7+0 < 50) print " ⬇️ "$1" can reduce memory allocation (using "$7")"}' | head -5 - -performance-baseline: - @echo "📊 Performance Baseline Measurement:" - @echo "" - @echo "Service Response Times:" - @curl -k -s -o /dev/null -w "Admin Frontend: %{time_total}s\n" https://admin.motovaultpro.com/ - @curl -k -s -o /dev/null -w "Platform Landing: %{time_total}s\n" https://motovaultpro.com/ - @curl -k -s -H "X-API-Key: mvp-platform-vehicles-secret-key" -o /dev/null -w "Vehicles API: %{time_total}s\n" https://admin.motovaultpro.com/api/platform/vehicles/health - @curl -k -s -H "X-API-Key: mvp-platform-tenants-secret-key" -o /dev/null -w "Tenants API: %{time_total}s\n" https://admin.motovaultpro.com/api/platform/tenants/health - @echo "" - @echo "Database Connections:" - @docker compose exec admin-postgres psql -U postgres -d motovaultpro -c "SELECT count(*) as active_connections FROM pg_stat_activity WHERE state = 'active';" -t 2>/dev/null || echo " Admin DB: Connection check failed" - @docker compose exec platform-postgres psql -U platform_user -d platform -c "SELECT count(*) as active_connections FROM pg_stat_activity WHERE state = 'active';" -t 2>/dev/null || echo " Platform DB: Connection check failed" - -monitoring-setup: - @echo "📈 Setting up enhanced monitoring configuration..." - @echo "Creating monitoring directory structure..." - @mkdir -p config/monitoring/alerts logs/monitoring - @echo "✅ Monitoring configuration created" - @echo "" - @echo "To enable full monitoring:" - @echo " 1. Review config/monitoring/prometheus.yml" - @echo " 2. Deploy with: make deploy-with-monitoring" - @echo " 3. Access metrics: make metrics-dashboard" - -deploy-with-monitoring: - @echo "🚀 Deploying with enhanced monitoring..." - @echo "1. Validating configuration..." - @./scripts/config-validator.sh - @echo "" - @echo "2. Restarting services with monitoring configuration..." - @docker compose up -d --build --remove-orphans - @echo "" - @echo "3. Verifying monitoring setup..." - @sleep 10 - @make health-check-all - @echo "" - @echo "✅ Monitoring deployment complete!" - -metrics-dashboard: - @echo "📊 Metrics Dashboard Access:" - @echo "" - @echo "Available metrics endpoints:" - @echo " 🔧 Traefik metrics: http://localhost:8080/metrics" - @echo " 📈 Service discovery: http://localhost:8080/api" - @echo "" - @echo "Sample Traefik metrics:" - @curl -s http://localhost:8080/metrics | grep "traefik_" | head -5 || echo " Metrics not available yet" - -capacity-planning: - @echo "🎯 Capacity Planning Analysis:" - @echo "" - @echo "Current Deployment Footprint:" - @echo " Services: $$(docker compose ps --format '{{.Service}}' | wc -l) containers" - @echo " Networks: $$(docker network ls --filter name=motovaultpro | wc -l) isolated networks" - @echo " Memory Allocation: $$(docker stats --no-stream --format '{{.MemUsage}}' | sed 's/MiB.*//' | awk '{sum+=$$1} END {print sum "MiB total"}' 2>/dev/null || echo 'calculating...')" - @echo "" - @echo "Resource Efficiency:" - @docker stats --no-stream --format "{{.Container}}" | wc -l | awk '{print " Running containers: " $$1}' - @echo " Docker Storage:" - @docker system df | grep -v REPOSITORY +logs-clear: + @sudo sh -c "truncate -s 0 /var/lib/docker/containers/**/*-json.log" \ No newline at end of file diff --git a/backend/src/features/fuel-logs/migrations/004_relax_odometer_and_trip_precision.sql b/backend/src/features/fuel-logs/migrations/004_relax_odometer_and_trip_precision.sql new file mode 100644 index 0000000..cb3346b --- /dev/null +++ b/backend/src/features/fuel-logs/migrations/004_relax_odometer_and_trip_precision.sql @@ -0,0 +1,31 @@ +-- Migration: 004_relax_odometer_and_trip_precision.sql +-- Purpose: Align schema with enhanced API allowing trip-only entries +-- Changes: +-- - Make odometer nullable (trip-only logs permitted) +-- - Change trip_distance to DECIMAL(10,3) to allow fractional distances + +BEGIN; + +-- Allow trip-only entries by making odometer nullable +ALTER TABLE fuel_logs ALTER COLUMN odometer DROP NOT NULL; + +-- Allow fractional trip distances +ALTER TABLE fuel_logs + ALTER COLUMN trip_distance TYPE DECIMAL(10,3) + USING CASE WHEN trip_distance IS NULL THEN NULL ELSE trip_distance::DECIMAL(10,3) END; + +-- Ensure the distance_required_check still exists; recreate defensively with correct semantics +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'distance_required_check' + ) THEN + ALTER TABLE fuel_logs DROP CONSTRAINT distance_required_check; + END IF; + ALTER TABLE fuel_logs ADD CONSTRAINT distance_required_check + CHECK ((trip_distance IS NOT NULL AND trip_distance > 0) OR + (odometer IS NOT NULL AND odometer > 0)); +END $$; + +COMMIT; + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c1fe8cb..96671aa 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -44,6 +44,189 @@ import { useDataSync } from './core/hooks/useDataSync'; import { MobileDebugPanel } from './core/debug/MobileDebugPanel'; import { MobileErrorBoundary } from './core/error-boundaries/MobileErrorBoundary'; +// Hoisted mobile screen components to stabilize identity and prevent remounts +const DashboardScreen: React.FC = () => ( +
+ +
+

Dashboard

+

Coming soon - Vehicle insights and analytics

+
+
+
+); + +const LogFuelScreen: React.FC = () => { + const queryClient = useQueryClient(); + const [editingLog, setEditingLog] = useState(null); + const { goBack, canGoBack, navigateToScreen } = useNavigationStore(); + useEffect(() => { + console.log('[LogFuelScreen] Mounted'); + return () => console.log('[LogFuelScreen] Unmounted'); + }, []); + + let fuelLogs: FuelLogResponse[] | undefined, isLoading: boolean | undefined, error: any; + try { + const hookResult = useFuelLogs(); + fuelLogs = hookResult.fuelLogs; + isLoading = hookResult.isLoading; + error = hookResult.error; + } catch (hookError) { + console.error('[LogFuelScreen] Hook error:', hookError); + error = hookError; + } + + const handleEdit = (log: FuelLogResponse) => { + if (!log || !log.id) { + console.error('[LogFuelScreen] Invalid log data for edit:', log); + return; + } + try { + setEditingLog(log); + } catch (error) { + console.error('[LogFuelScreen] Error setting editing log:', error); + } + }; + + const handleDelete = async (_logId: string) => { + try { + queryClient.invalidateQueries({ queryKey: ['fuelLogs'] }); + queryClient.invalidateQueries({ queryKey: ['fuelLogsStats'] }); + } catch (error) { + console.error('Failed to refresh fuel logs after delete:', error); + } + }; + + const handleSaveEdit = async (id: string, data: UpdateFuelLogRequest) => { + try { + await fuelLogsApi.update(id, data); + queryClient.invalidateQueries({ queryKey: ['fuelLogs'] }); + queryClient.invalidateQueries({ queryKey: ['fuelLogsStats'] }); + setEditingLog(null); + } catch (error) { + console.error('Failed to update fuel log:', error); + throw error; + } + }; + + const handleCloseEdit = () => setEditingLog(null); + + if (error) { + return ( +
+ +
+

Failed to load fuel logs

+ +
+
+
+ ); + } + + if (isLoading === undefined) { + return ( +
+ +
+ Initializing fuel logs... +
+
+
+ ); + } + + return ( +
+ + { + // Refresh dependent data + try { + queryClient.invalidateQueries({ queryKey: ['fuelLogs'] }); + queryClient.invalidateQueries({ queryKey: ['fuelLogsStats'] }); + } catch {} + // Navigate back if we have history; otherwise go to Vehicles + if (canGoBack()) { + goBack(); + } else { + navigateToScreen('Vehicles', { source: 'fuel-log-added' }); + } + }} /> + + + +
+ {isLoading ? ( +
+ Loading fuel logs... +
+ ) : ( + + )} +
+
+
+ + + + +
+ ); +}; + +interface AddVehicleScreenProps { + onBack: () => void; + onAdded: () => void; +} + +const AddVehicleScreen: React.FC = ({ onBack, onAdded }) => { + const { optimisticCreateVehicle } = useOptimisticVehicles([]); + + const handleCreateVehicle = async (data: CreateVehicleRequest) => { + try { + await optimisticCreateVehicle(data); + onAdded(); + } catch (error) { + console.error('Failed to create vehicle:', error); + } + }; + + return ( +
+ +
+
+

Add Vehicle

+ +
+ +
+
+
+ ); +}; function App() { const { isLoading, isAuthenticated, loginWithRedirect, user } = useAuth0(); @@ -152,184 +335,9 @@ function App() { ); - // Placeholder screens for mobile - const DashboardScreen = () => ( -
- -
-

Dashboard

-

Coming soon - Vehicle insights and analytics

-
-
-
- ); - - const LogFuelScreen = () => { - const queryClient = useQueryClient(); - const [editingLog, setEditingLog] = useState(null); - - // Safe hook usage with error boundary protection - let fuelLogs, isLoading, error; - - try { - const hookResult = useFuelLogs(); - fuelLogs = hookResult.fuelLogs; - isLoading = hookResult.isLoading; - error = hookResult.error; - } catch (hookError) { - console.error('[LogFuelScreen] Hook error:', hookError); - error = hookError; - } - - const handleEdit = (log: FuelLogResponse) => { - // Defensive validation before setting editing log - if (!log || !log.id) { - console.error('[LogFuelScreen] Invalid log data for edit:', log); - return; - } - - try { - setEditingLog(log); - } catch (error) { - console.error('[LogFuelScreen] Error setting editing log:', error); - } - }; - - const handleDelete = async (_logId: string) => { - try { - // Invalidate queries to refresh the data - queryClient.invalidateQueries({ queryKey: ['fuelLogs'] }); - queryClient.invalidateQueries({ queryKey: ['fuelLogsStats'] }); - } catch (error) { - console.error('Failed to refresh fuel logs after delete:', error); - } - }; - - const handleSaveEdit = async (id: string, data: UpdateFuelLogRequest) => { - try { - await fuelLogsApi.update(id, data); - // Invalidate queries to refresh the data - queryClient.invalidateQueries({ queryKey: ['fuelLogs'] }); - queryClient.invalidateQueries({ queryKey: ['fuelLogsStats'] }); - setEditingLog(null); - } catch (error) { - console.error('Failed to update fuel log:', error); - throw error; // Re-throw to let the dialog handle the error - } - }; - - const handleCloseEdit = () => { - setEditingLog(null); - }; - - if (error) { - return ( -
- -
-

Failed to load fuel logs

- -
-
-
- ); - } - - // Add loading state for component initialization - if (isLoading === undefined) { - return ( -
- -
- Initializing fuel logs... -
-
-
- ); - } - - return ( -
- - - - - -
- {isLoading ? ( -
- Loading fuel logs... -
- ) : ( - - )} -
-
-
- - {/* Edit Dialog */} - - - -
- ); - }; - // Mobile settings now uses the dedicated MobileSettingsScreen component const SettingsScreen = MobileSettingsScreen; - const AddVehicleScreen = () => { - // Vehicle creation logic - const { optimisticCreateVehicle } = useOptimisticVehicles([]); - - const handleCreateVehicle = async (data: CreateVehicleRequest) => { - try { - await optimisticCreateVehicle(data); - // Success - navigate back to list - handleVehicleAdded(); - } catch (error) { - console.error('Failed to create vehicle:', error); - // Error handling is done by the useOptimisticVehicles hook via toast - } - }; - - return ( -
- -
-
-

Add Vehicle

- -
- -
-
-
- ); - }; - if (isLoading) { if (mobileMode) { return ( @@ -425,7 +433,7 @@ function App() { > {vehicleSubScreen === 'add' || showAddVehicle ? ( - + ) : selectedVehicle && (vehicleSubScreen === 'detail') ? ( { if (this.isOnline) { + const state = useNavigationStore.getState(); + console.log('[DataSync] Tick', { + at: new Date().toISOString(), + activeScreen: state.activeScreen, + selectedVehicleId: state.selectedVehicleId, + }); this.performBackgroundSync(); } }, this.config.syncInterval); @@ -135,14 +141,19 @@ export class DataSyncManager { // If on vehicles screen, refresh vehicles data if (navigationState.activeScreen === 'Vehicles') { + console.log('[DataSync] Invalidating query', { key: ['vehicles'], reason: 'background-sync:vehicles-screen' }); await this.queryClient.invalidateQueries({ queryKey: ['vehicles'] }); } - // If viewing specific vehicle, refresh its data - if (navigationState.selectedVehicleId) { - await this.queryClient.invalidateQueries({ - queryKey: ['vehicles', navigationState.selectedVehicleId] - }); + // If viewing specific vehicle details, refresh that vehicle's data + if ( + navigationState.activeScreen === 'Vehicles' && + navigationState.vehicleSubScreen === 'detail' && + navigationState.selectedVehicleId + ) { + const key = ['vehicles', navigationState.selectedVehicleId]; + console.log('[DataSync] Invalidating query', { key, reason: 'background-sync:selected-vehicle:vehicles-detail' }); + await this.queryClient.invalidateQueries({ queryKey: key }); } console.log('DataSync: Background sync completed'); @@ -241,4 +252,4 @@ export class DataSyncManager { window.removeEventListener('online', this.handleOnline); window.removeEventListener('offline', this.handleOffline); } -} \ No newline at end of file +} diff --git a/frontend/src/features/fuel-logs/components/FuelLogForm.tsx b/frontend/src/features/fuel-logs/components/FuelLogForm.tsx index f906b9d..42aad88 100644 --- a/frontend/src/features/fuel-logs/components/FuelLogForm.tsx +++ b/frontend/src/features/fuel-logs/components/FuelLogForm.tsx @@ -36,7 +36,7 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial const [useOdometer, setUseOdometer] = useState(false); const formInitialized = useRef(false); - const { control, handleSubmit, watch, setValue, formState: { errors, isValid } } = useForm({ + const { control, handleSubmit, watch, setValue, reset, formState: { errors, isValid } } = useForm({ resolver: zodResolver(schema), mode: 'onChange', defaultValues: { @@ -47,14 +47,22 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial }); // DEBUG: Log component renders and form state - console.log('[FuelLogForm] Render - formInitialized:', formInitialized.current, 'isLoading:', isLoading); + console.log('[FuelLogForm] Render', { + at: new Date().toISOString(), + formInitialized: formInitialized.current, + isLoading, + }); // Prevent form reset after initial load useEffect(() => { + console.log('[FuelLogForm] Mounted'); if (!formInitialized.current) { formInitialized.current = true; console.log('[FuelLogForm] Form initialized'); } + return () => { + console.log('[FuelLogForm] Unmounted'); + }; }, []); // DEBUG: Watch for form value changes @@ -63,6 +71,16 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial console.log('[FuelLogForm] Vehicle ID changed:', vehicleId); }, [vehicleId]); + // DEBUG: Track dirty-state changes to detect resets + useEffect(() => { + const subscription = watch((_, info) => { + if (info.name) { + console.log('[FuelLogForm] Field change', { name: info.name, type: info.type }); + } + }); + return () => subscription.unsubscribe(); + }, [watch]); + const watched = watch(['fuelUnits', 'costPerUnit']); const [fuelUnitsRaw, costPerUnitRaw] = watched as [string | number | undefined, string | number | undefined]; @@ -83,6 +101,19 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial tripDistance: useOdometer ? undefined : data.tripDistance, }; await createFuelLog(payload); + // Reset form to initial defaults after successful create + reset({ + vehicleId: undefined as any, + dateTime: new Date().toISOString().slice(0, 16), + odometerReading: undefined as any, + tripDistance: undefined as any, + fuelType: FuelType.GASOLINE, + fuelGrade: undefined as any, + fuelUnits: undefined as any, + costPerUnit: undefined as any, + locationData: undefined as any, + notes: undefined as any, + } as any); onSuccess?.(); }; @@ -178,4 +209,3 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial }; export const FuelLogForm = memo(FuelLogFormComponent); -