Fixed mobile form
This commit is contained in:
196
Makefile
196
Makefile
@@ -75,16 +75,6 @@ clean:
|
|||||||
@docker compose down -v --rmi all
|
@docker compose down -v --rmi all
|
||||||
@docker system prune -f
|
@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:
|
logs:
|
||||||
@docker compose logs -f
|
@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 " 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')"
|
@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
|
# SSL Certificate Generation
|
||||||
generate-certs:
|
generate-certs:
|
||||||
@echo "Generating multi-domain SSL certificate for mobile compatibility..."
|
@echo "Generating multi-domain SSL certificate for mobile compatibility..."
|
||||||
@@ -230,67 +169,6 @@ generate-certs:
|
|||||||
-extensions SAN
|
-extensions SAN
|
||||||
@echo "✅ Certificate generated with SAN for mobile compatibility (includes $(shell hostname -I | awk '{print $$1}'))"
|
@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
|
# Enhanced log commands with filtering
|
||||||
logs-traefik:
|
logs-traefik:
|
||||||
@@ -302,75 +180,5 @@ logs-platform:
|
|||||||
logs-backend-full:
|
logs-backend-full:
|
||||||
@docker compose logs -f admin-backend admin-postgres admin-redis admin-minio
|
@docker compose logs -f admin-backend admin-postgres admin-redis admin-minio
|
||||||
|
|
||||||
# Phase 4: Optimization & Monitoring Commands
|
logs-clear:
|
||||||
resource-optimization:
|
@sudo sh -c "truncate -s 0 /var/lib/docker/containers/**/*-json.log"
|
||||||
@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
|
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -44,6 +44,189 @@ import { useDataSync } from './core/hooks/useDataSync';
|
|||||||
import { MobileDebugPanel } from './core/debug/MobileDebugPanel';
|
import { MobileDebugPanel } from './core/debug/MobileDebugPanel';
|
||||||
import { MobileErrorBoundary } from './core/error-boundaries/MobileErrorBoundary';
|
import { MobileErrorBoundary } from './core/error-boundaries/MobileErrorBoundary';
|
||||||
|
|
||||||
|
// Hoisted mobile screen components to stabilize identity and prevent remounts
|
||||||
|
const DashboardScreen: React.FC = () => (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<GlassCard>
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<h2 className="text-lg font-semibold text-slate-800 mb-2">Dashboard</h2>
|
||||||
|
<p className="text-slate-500">Coming soon - Vehicle insights and analytics</p>
|
||||||
|
</div>
|
||||||
|
</GlassCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const LogFuelScreen: React.FC = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [editingLog, setEditingLog] = useState<FuelLogResponse | null>(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 (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<GlassCard>
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<p className="text-red-600 mb-4">Failed to load fuel logs</p>
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg"
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</GlassCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading === undefined) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<GlassCard>
|
||||||
|
<div className="text-center py-8 text-slate-500">
|
||||||
|
Initializing fuel logs...
|
||||||
|
</div>
|
||||||
|
</GlassCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<MobileErrorBoundary screenName="FuelLogForm" key="fuel-form">
|
||||||
|
<FuelLogForm onSuccess={() => {
|
||||||
|
// 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' });
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
</MobileErrorBoundary>
|
||||||
|
<MobileErrorBoundary screenName="FuelLogsSection" key="fuel-section">
|
||||||
|
<GlassCard>
|
||||||
|
<div className="py-2">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="text-center py-8 text-slate-500">
|
||||||
|
Loading fuel logs...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<FuelLogsList
|
||||||
|
logs={fuelLogs || []}
|
||||||
|
onEdit={handleEdit}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</GlassCard>
|
||||||
|
</MobileErrorBoundary>
|
||||||
|
|
||||||
|
<MobileErrorBoundary screenName="FuelLogEditDialog" key="fuel-edit-dialog">
|
||||||
|
<FuelLogEditDialog
|
||||||
|
open={!!editingLog}
|
||||||
|
log={editingLog}
|
||||||
|
onClose={handleCloseEdit}
|
||||||
|
onSave={handleSaveEdit}
|
||||||
|
/>
|
||||||
|
</MobileErrorBoundary>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AddVehicleScreenProps {
|
||||||
|
onBack: () => void;
|
||||||
|
onAdded: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddVehicleScreen: React.FC<AddVehicleScreenProps> = ({ 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 (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<GlassCard>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-lg font-semibold text-slate-800">Add Vehicle</h2>
|
||||||
|
<button
|
||||||
|
onClick={onBack}
|
||||||
|
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<VehicleForm
|
||||||
|
onSubmit={handleCreateVehicle}
|
||||||
|
onCancel={onBack}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</GlassCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { isLoading, isAuthenticated, loginWithRedirect, user } = useAuth0();
|
const { isLoading, isAuthenticated, loginWithRedirect, user } = useAuth0();
|
||||||
@@ -152,184 +335,9 @@ function App() {
|
|||||||
<MobileDebugPanel visible={import.meta.env.MODE === 'development'} />
|
<MobileDebugPanel visible={import.meta.env.MODE === 'development'} />
|
||||||
);
|
);
|
||||||
|
|
||||||
// Placeholder screens for mobile
|
|
||||||
const DashboardScreen = () => (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<GlassCard>
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<h2 className="text-lg font-semibold text-slate-800 mb-2">Dashboard</h2>
|
|
||||||
<p className="text-slate-500">Coming soon - Vehicle insights and analytics</p>
|
|
||||||
</div>
|
|
||||||
</GlassCard>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const LogFuelScreen = () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const [editingLog, setEditingLog] = useState<FuelLogResponse | null>(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 (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<GlassCard>
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<p className="text-red-600 mb-4">Failed to load fuel logs</p>
|
|
||||||
<button
|
|
||||||
onClick={() => window.location.reload()}
|
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg"
|
|
||||||
>
|
|
||||||
Retry
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</GlassCard>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add loading state for component initialization
|
|
||||||
if (isLoading === undefined) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<GlassCard>
|
|
||||||
<div className="text-center py-8 text-slate-500">
|
|
||||||
Initializing fuel logs...
|
|
||||||
</div>
|
|
||||||
</GlassCard>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<MobileErrorBoundary screenName="FuelLogForm" key="fuel-form">
|
|
||||||
<FuelLogForm />
|
|
||||||
</MobileErrorBoundary>
|
|
||||||
<MobileErrorBoundary screenName="FuelLogsSection" key="fuel-section">
|
|
||||||
<GlassCard>
|
|
||||||
<div className="py-2">
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="text-center py-8 text-slate-500">
|
|
||||||
Loading fuel logs...
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<FuelLogsList
|
|
||||||
logs={fuelLogs || []}
|
|
||||||
onEdit={handleEdit}
|
|
||||||
onDelete={handleDelete}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</GlassCard>
|
|
||||||
</MobileErrorBoundary>
|
|
||||||
|
|
||||||
{/* Edit Dialog */}
|
|
||||||
<MobileErrorBoundary screenName="FuelLogEditDialog" key="fuel-edit-dialog">
|
|
||||||
<FuelLogEditDialog
|
|
||||||
open={!!editingLog}
|
|
||||||
log={editingLog}
|
|
||||||
onClose={handleCloseEdit}
|
|
||||||
onSave={handleSaveEdit}
|
|
||||||
/>
|
|
||||||
</MobileErrorBoundary>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mobile settings now uses the dedicated MobileSettingsScreen component
|
// Mobile settings now uses the dedicated MobileSettingsScreen component
|
||||||
const SettingsScreen = MobileSettingsScreen;
|
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 (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<GlassCard>
|
|
||||||
<div className="p-4">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<h2 className="text-lg font-semibold text-slate-800">Add Vehicle</h2>
|
|
||||||
<button
|
|
||||||
onClick={handleBackToList}
|
|
||||||
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<VehicleForm
|
|
||||||
onSubmit={handleCreateVehicle}
|
|
||||||
onCancel={handleBackToList}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</GlassCard>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
if (mobileMode) {
|
if (mobileMode) {
|
||||||
return (
|
return (
|
||||||
@@ -425,7 +433,7 @@ function App() {
|
|||||||
>
|
>
|
||||||
<MobileErrorBoundary screenName="Vehicles">
|
<MobileErrorBoundary screenName="Vehicles">
|
||||||
{vehicleSubScreen === 'add' || showAddVehicle ? (
|
{vehicleSubScreen === 'add' || showAddVehicle ? (
|
||||||
<AddVehicleScreen />
|
<AddVehicleScreen onBack={handleBackToList} onAdded={handleVehicleAdded} />
|
||||||
) : selectedVehicle && (vehicleSubScreen === 'detail') ? (
|
) : selectedVehicle && (vehicleSubScreen === 'detail') ? (
|
||||||
<VehicleDetailMobile
|
<VehicleDetailMobile
|
||||||
vehicle={selectedVehicle}
|
vehicle={selectedVehicle}
|
||||||
|
|||||||
@@ -115,6 +115,12 @@ export class DataSyncManager {
|
|||||||
private startBackgroundSync() {
|
private startBackgroundSync() {
|
||||||
this.syncInterval = setInterval(() => {
|
this.syncInterval = setInterval(() => {
|
||||||
if (this.isOnline) {
|
if (this.isOnline) {
|
||||||
|
const state = useNavigationStore.getState();
|
||||||
|
console.log('[DataSync] Tick', {
|
||||||
|
at: new Date().toISOString(),
|
||||||
|
activeScreen: state.activeScreen,
|
||||||
|
selectedVehicleId: state.selectedVehicleId,
|
||||||
|
});
|
||||||
this.performBackgroundSync();
|
this.performBackgroundSync();
|
||||||
}
|
}
|
||||||
}, this.config.syncInterval);
|
}, this.config.syncInterval);
|
||||||
@@ -135,14 +141,19 @@ export class DataSyncManager {
|
|||||||
|
|
||||||
// If on vehicles screen, refresh vehicles data
|
// If on vehicles screen, refresh vehicles data
|
||||||
if (navigationState.activeScreen === 'Vehicles') {
|
if (navigationState.activeScreen === 'Vehicles') {
|
||||||
|
console.log('[DataSync] Invalidating query', { key: ['vehicles'], reason: 'background-sync:vehicles-screen' });
|
||||||
await this.queryClient.invalidateQueries({ queryKey: ['vehicles'] });
|
await this.queryClient.invalidateQueries({ queryKey: ['vehicles'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
// If viewing specific vehicle, refresh its data
|
// If viewing specific vehicle details, refresh that vehicle's data
|
||||||
if (navigationState.selectedVehicleId) {
|
if (
|
||||||
await this.queryClient.invalidateQueries({
|
navigationState.activeScreen === 'Vehicles' &&
|
||||||
queryKey: ['vehicles', navigationState.selectedVehicleId]
|
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');
|
console.log('DataSync: Background sync completed');
|
||||||
@@ -241,4 +252,4 @@ export class DataSyncManager {
|
|||||||
window.removeEventListener('online', this.handleOnline);
|
window.removeEventListener('online', this.handleOnline);
|
||||||
window.removeEventListener('offline', this.handleOffline);
|
window.removeEventListener('offline', this.handleOffline);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial
|
|||||||
const [useOdometer, setUseOdometer] = useState(false);
|
const [useOdometer, setUseOdometer] = useState(false);
|
||||||
const formInitialized = useRef(false);
|
const formInitialized = useRef(false);
|
||||||
|
|
||||||
const { control, handleSubmit, watch, setValue, formState: { errors, isValid } } = useForm<CreateFuelLogRequest>({
|
const { control, handleSubmit, watch, setValue, reset, formState: { errors, isValid } } = useForm<CreateFuelLogRequest>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -47,14 +47,22 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial
|
|||||||
});
|
});
|
||||||
|
|
||||||
// DEBUG: Log component renders and form state
|
// 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
|
// Prevent form reset after initial load
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log('[FuelLogForm] Mounted');
|
||||||
if (!formInitialized.current) {
|
if (!formInitialized.current) {
|
||||||
formInitialized.current = true;
|
formInitialized.current = true;
|
||||||
console.log('[FuelLogForm] Form initialized');
|
console.log('[FuelLogForm] Form initialized');
|
||||||
}
|
}
|
||||||
|
return () => {
|
||||||
|
console.log('[FuelLogForm] Unmounted');
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// DEBUG: Watch for form value changes
|
// 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);
|
console.log('[FuelLogForm] Vehicle ID changed:', vehicleId);
|
||||||
}, [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 watched = watch(['fuelUnits', 'costPerUnit']);
|
||||||
const [fuelUnitsRaw, costPerUnitRaw] = watched as [string | number | undefined, string | number | undefined];
|
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,
|
tripDistance: useOdometer ? undefined : data.tripDistance,
|
||||||
};
|
};
|
||||||
await createFuelLog(payload);
|
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?.();
|
onSuccess?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -178,4 +209,3 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const FuelLogForm = memo(FuelLogFormComponent);
|
export const FuelLogForm = memo(FuelLogFormComponent);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user