diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 7ac876a..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,14 +0,0 @@ -# These are supported funding model platforms - -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: -open_collective: # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username -issuehunt: # Replace with a single IssueHunt username -lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry -polar: # Replace with a single Polar username -buy_me_a_coffee: # Replace with a single Buy Me a Coffee username -custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 3cc77ef..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,171 +0,0 @@ -name: CI - -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] - -env: - DOCKER_BUILDKIT: 1 - COMPOSE_DOCKER_CLI_BUILD: 1 - COMPOSE_FILE: docker-compose.yml - COMPOSE_PROJECT_NAME: ci - -jobs: - backend-tests: - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Cache buildx layers - uses: actions/cache@v4 - with: - path: ~/.cache/buildx - key: ${{ runner.os }}-buildx-${{ hashFiles('backend/package.json', 'frontend/package.json', 'package-lock.json') }} - restore-keys: | - ${{ runner.os }}-buildx- - - - name: Hydrate secrets and backend env - shell: bash - env: - POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} - AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }} - GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }} - run: | - set -euo pipefail - - mkdir -p .ci/secrets - - : "${POSTGRES_PASSWORD:=$(cat secrets/app/postgres-password.txt 2>/dev/null || echo 'localdev123')}" - : "${AUTH0_CLIENT_SECRET:=$(cat secrets/app/auth0-client-secret.txt 2>/dev/null || echo 'ci-auth0-secret')}" - : "${GOOGLE_MAPS_API_KEY:=$(cat secrets/app/google-maps-api-key.txt 2>/dev/null || echo 'ci-google-maps-key')}" - - mkdir -p secrets/app - printf '%s' "$POSTGRES_PASSWORD" > secrets/app/postgres-password.txt - printf '%s' "$AUTH0_CLIENT_SECRET" > secrets/app/auth0-client-secret.txt - printf '%s' "$GOOGLE_MAPS_API_KEY" > secrets/app/google-maps-api-key.txt - - printf '%s' "$POSTGRES_PASSWORD" > .ci/secrets/postgres-password - printf '%s' "$AUTH0_CLIENT_SECRET" > .ci/secrets/auth0-client-secret - printf '%s' "$GOOGLE_MAPS_API_KEY" > .ci/secrets/google-maps-api-key - - cat < .ci/backend.env - NODE_ENV=test - CI=true - CONFIG_PATH=/ci/config/app/ci.yml - SECRETS_DIR=/ci/secrets - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@mvp-postgres:5432/motovaultpro - DB_HOST=mvp-postgres - DB_USER=postgres - DB_PASSWORD=${POSTGRES_PASSWORD} - DB_DATABASE=motovaultpro - DB_PORT=5432 - REDIS_HOST=mvp-redis - REDIS_URL=redis://mvp-redis:6379 - MINIO_ENDPOINT=http://minio:9000 - GOOGLE_MAPS_API_KEY=${GOOGLE_MAPS_API_KEY} - AUTH0_CLIENT_SECRET=${AUTH0_CLIENT_SECRET} - EOF_ENV - - - name: Build application images - run: docker compose build mvp-backend mvp-frontend - - - name: Start database dependencies - run: docker compose up -d mvp-postgres mvp-redis - - - name: Wait for dependencies - shell: bash - run: | - set -euo pipefail - for _ in $(seq 1 20); do - if docker compose exec -T mvp-postgres pg_isready -U postgres >/dev/null 2>&1; then - break - fi - sleep 3 - done - docker compose exec -T mvp-postgres pg_isready -U postgres - docker compose exec -T mvp-redis redis-cli ping - - - name: Build backend builder image - run: docker build --target builder -t motovaultpro-backend-builder backend - - - name: Lint backend - run: | - docker run --rm \ - --network ${COMPOSE_PROJECT_NAME}_default \ - --env-file .ci/backend.env \ - -v ${{ github.workspace }}/config/app/ci.yml:/ci/config/app/ci.yml:ro \ - -v ${{ github.workspace }}/.ci/secrets:/ci/secrets:ro \ - motovaultpro-backend-builder npm run lint - - - name: Run backend tests - env: - CI: true - run: | - docker run --rm \ - --network ${COMPOSE_PROJECT_NAME}_default \ - --env-file .ci/backend.env \ - -v ${{ github.workspace }}/config/app/ci.yml:/ci/config/app/ci.yml:ro \ - -v ${{ github.workspace }}/.ci/secrets:/ci/secrets:ro \ - motovaultpro-backend-builder npm test -- --runInBand - - - name: Tear down containers - if: always() - run: docker compose down -v - - frontend-tests: - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Cache buildx layers - uses: actions/cache@v4 - with: - path: ~/.cache/buildx - key: ${{ runner.os }}-buildx-${{ hashFiles('frontend/package.json', 'package-lock.json') }} - restore-keys: | - ${{ runner.os }}-buildx- - - - name: Prepare frontend env - shell: bash - run: | - set -euo pipefail - mkdir -p .ci - cat < .ci/frontend.env - CI=true - VITE_AUTH0_DOMAIN=motovaultpro.us.auth0.com - VITE_AUTH0_CLIENT_ID=yspR8zdnSxmV8wFIghHynQ08iXAPoQJ3 - VITE_AUTH0_AUDIENCE=https://api.motovaultpro.com - VITE_API_BASE_URL=/api - EOF_ENV - - - name: Build frontend image - run: docker compose build mvp-frontend - - - name: Build frontend dependencies image - run: docker build --target deps -t motovaultpro-frontend-deps frontend - - - name: Lint frontend - run: | - docker run --rm \ - --env-file .ci/frontend.env \ - motovaultpro-frontend-deps npm run lint - - - name: Run frontend tests - env: - CI: true - run: | - docker run --rm \ - --env-file .ci/frontend.env \ - motovaultpro-frontend-deps npm test -- --runInBand diff --git a/backend/src/features/vehicles/domain/vehicles.service.ts b/backend/src/features/vehicles/domain/vehicles.service.ts index de38190..d009470 100644 --- a/backend/src/features/vehicles/domain/vehicles.service.ts +++ b/backend/src/features/vehicles/domain/vehicles.service.ts @@ -13,6 +13,9 @@ import { } from './vehicles.types'; import { logger } from '../../../core/logging/logger'; import { cacheService } from '../../../core/config/redis'; +import { getStorageService } from '../../../core/storage/storage.service'; +import * as fs from 'fs/promises'; +import * as path from 'path'; import { isValidVIN } from '../../../shared-minimal/utils/validators'; import { normalizeMakeName, normalizeModelName } from './name-normalizer'; import { getVehicleDataService, getPool } from '../../platform'; @@ -133,6 +136,59 @@ export class VehiclesService { throw new Error('Unauthorized'); } + logger.info('Deleting vehicle', { + vehicleId: id, + userId, + hasImageKey: !!existing.imageStorageKey, + hasImageBucket: !!existing.imageStorageBucket, + imageStorageKey: existing.imageStorageKey, + imageStorageBucket: existing.imageStorageBucket + }); + + // Delete associated image from storage if present + if (existing.imageStorageKey && existing.imageStorageBucket) { + try { + const storage = getStorageService(); + logger.info('Attempting to delete vehicle image', { + vehicleId: id, + bucket: existing.imageStorageBucket, + key: existing.imageStorageKey + }); + await storage.deleteObject(existing.imageStorageBucket, existing.imageStorageKey); + logger.info('Successfully deleted vehicle image on vehicle deletion', { + vehicleId: id, + bucket: existing.imageStorageBucket, + key: existing.imageStorageKey + }); + + // Clean up empty vehicle directory + // Key format: vehicle-images/{userId}/{vehicleId}/{filename} + const basePath = '/app/data/documents'; + const vehicleDir = path.join(basePath, path.dirname(existing.imageStorageKey)); + try { + await fs.rmdir(vehicleDir); + logger.info('Removed empty vehicle image directory', { vehicleId: id, directory: vehicleDir }); + } catch (dirError) { + // Directory might not be empty or might not exist, ignore + logger.debug('Could not remove vehicle image directory', { + vehicleId: id, + directory: vehicleDir, + error: dirError instanceof Error ? dirError.message : String(dirError) + }); + } + } catch (error) { + // Log warning but don't block vehicle deletion on storage failure + logger.warn('Failed to delete vehicle image on vehicle deletion', { + vehicleId: id, + bucket: existing.imageStorageBucket, + key: existing.imageStorageKey, + error: error instanceof Error ? error.message : String(error) + }); + } + } else { + logger.info('No image to delete for vehicle', { vehicleId: id }); + } + // Soft delete await this.repository.softDelete(id); diff --git a/data/documents/vehicle-images/auth0|68964ab96c80a91461764375/140f97bd-30e2-4f04-8fea-a91d9b530ff9/ab9855b7f51521c8ab71547af6cd1cbae4eaa43f2850149962e8c4e802a75c0e.jpg b/data/documents/vehicle-images/auth0|68964ab96c80a91461764375/140f97bd-30e2-4f04-8fea-a91d9b530ff9/ab9855b7f51521c8ab71547af6cd1cbae4eaa43f2850149962e8c4e802a75c0e.jpg deleted file mode 100644 index 68216b4..0000000 Binary files a/data/documents/vehicle-images/auth0|68964ab96c80a91461764375/140f97bd-30e2-4f04-8fea-a91d9b530ff9/ab9855b7f51521c8ab71547af6cd1cbae4eaa43f2850149962e8c4e802a75c0e.jpg and /dev/null differ diff --git a/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/84f8806f-1ff4-4365-befa-2e7346a1f6f1/c1b7bbabf0b4e000c3f4946c236536c97bd174cd5a29ae34842797ff3bed41a8.jpg b/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/84f8806f-1ff4-4365-befa-2e7346a1f6f1/c1b7bbabf0b4e000c3f4946c236536c97bd174cd5a29ae34842797ff3bed41a8.jpg deleted file mode 100644 index fe7205c..0000000 Binary files a/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/84f8806f-1ff4-4365-befa-2e7346a1f6f1/c1b7bbabf0b4e000c3f4946c236536c97bd174cd5a29ae34842797ff3bed41a8.jpg and /dev/null differ diff --git a/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/84f8806f-1ff4-4365-befa-2e7346a1f6f1/c1b7bbabf0b4e000c3f4946c236536c97bd174cd5a29ae34842797ff3bed41a8.jpg.meta.json b/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/84f8806f-1ff4-4365-befa-2e7346a1f6f1/c1b7bbabf0b4e000c3f4946c236536c97bd174cd5a29ae34842797ff3bed41a8.jpg.meta.json deleted file mode 100644 index c30680b..0000000 --- a/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/84f8806f-1ff4-4365-befa-2e7346a1f6f1/c1b7bbabf0b4e000c3f4946c236536c97bd174cd5a29ae34842797ff3bed41a8.jpg.meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "x-original-filename": "C7Z06-BlackRose.jpg", - "content-type": "image/jpeg" -} \ No newline at end of file diff --git a/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/88f16b0e-ea32-4d99-aa46-218c1e8759fa/5b8d83465e38b0ef4628bbcbcf21072618190280b3b7206dfcdc70b0a18a32c2.jpg b/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/88f16b0e-ea32-4d99-aa46-218c1e8759fa/5b8d83465e38b0ef4628bbcbcf21072618190280b3b7206dfcdc70b0a18a32c2.jpg deleted file mode 100644 index 68216b4..0000000 Binary files a/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/88f16b0e-ea32-4d99-aa46-218c1e8759fa/5b8d83465e38b0ef4628bbcbcf21072618190280b3b7206dfcdc70b0a18a32c2.jpg and /dev/null differ diff --git a/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/ad726296-fe49-4e0b-af99-166700b45417/f4a5599f6debeb5c9d68f20537a10106bf6eb9ef9fb5662b3976780fc2deb499.jpg b/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/ad726296-fe49-4e0b-af99-166700b45417/f4a5599f6debeb5c9d68f20537a10106bf6eb9ef9fb5662b3976780fc2deb499.jpg deleted file mode 100644 index a5c52d3..0000000 Binary files a/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/ad726296-fe49-4e0b-af99-166700b45417/f4a5599f6debeb5c9d68f20537a10106bf6eb9ef9fb5662b3976780fc2deb499.jpg and /dev/null differ diff --git a/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/af19f539-659f-41e0-955b-b225c9f4b87b/d8ff055833dcaa878f85adb9806d4d0d9a03094a3353ada6d21976f1d6c91aed.jpg b/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/af19f539-659f-41e0-955b-b225c9f4b87b/d8ff055833dcaa878f85adb9806d4d0d9a03094a3353ada6d21976f1d6c91aed.jpg deleted file mode 100644 index fe7205c..0000000 Binary files a/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/af19f539-659f-41e0-955b-b225c9f4b87b/d8ff055833dcaa878f85adb9806d4d0d9a03094a3353ada6d21976f1d6c91aed.jpg and /dev/null differ diff --git a/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/af19f539-659f-41e0-955b-b225c9f4b87b/d8ff055833dcaa878f85adb9806d4d0d9a03094a3353ada6d21976f1d6c91aed.jpg.meta.json b/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/af19f539-659f-41e0-955b-b225c9f4b87b/d8ff055833dcaa878f85adb9806d4d0d9a03094a3353ada6d21976f1d6c91aed.jpg.meta.json deleted file mode 100644 index c30680b..0000000 --- a/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/af19f539-659f-41e0-955b-b225c9f4b87b/d8ff055833dcaa878f85adb9806d4d0d9a03094a3353ada6d21976f1d6c91aed.jpg.meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "x-original-filename": "C7Z06-BlackRose.jpg", - "content-type": "image/jpeg" -} \ No newline at end of file diff --git a/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/c617e845-e7ea-4e3a-a54a-66d8f2f873f9/1acf34d8b6912197b5726f086cdc7750b6464fb0926069190f4908a01ed6b7c6.jpg b/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/c617e845-e7ea-4e3a-a54a-66d8f2f873f9/1acf34d8b6912197b5726f086cdc7750b6464fb0926069190f4908a01ed6b7c6.jpg deleted file mode 100644 index 68216b4..0000000 Binary files a/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/c617e845-e7ea-4e3a-a54a-66d8f2f873f9/1acf34d8b6912197b5726f086cdc7750b6464fb0926069190f4908a01ed6b7c6.jpg and /dev/null differ diff --git a/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/dc8a7440-d697-4c37-aa63-32a723357c5f/25abf470668a46bfe3992c08559c8756a35d7999b16bcb4c51d3498c5e876104.jpg b/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/dc8a7440-d697-4c37-aa63-32a723357c5f/25abf470668a46bfe3992c08559c8756a35d7999b16bcb4c51d3498c5e876104.jpg deleted file mode 100644 index a278904..0000000 Binary files a/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/dc8a7440-d697-4c37-aa63-32a723357c5f/25abf470668a46bfe3992c08559c8756a35d7999b16bcb4c51d3498c5e876104.jpg and /dev/null differ diff --git a/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/efef478c-26a5-4cfa-ac15-80b189dea3be/d3d0841be49031421051ecc7947c98e71fab122c74c34cb3ccd641b906858e60.jpg b/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/efef478c-26a5-4cfa-ac15-80b189dea3be/d3d0841be49031421051ecc7947c98e71fab122c74c34cb3ccd641b906858e60.jpg deleted file mode 100644 index fe7205c..0000000 Binary files a/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/efef478c-26a5-4cfa-ac15-80b189dea3be/d3d0841be49031421051ecc7947c98e71fab122c74c34cb3ccd641b906858e60.jpg and /dev/null differ diff --git a/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/efef478c-26a5-4cfa-ac15-80b189dea3be/d3d0841be49031421051ecc7947c98e71fab122c74c34cb3ccd641b906858e60.jpg.meta.json b/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/efef478c-26a5-4cfa-ac15-80b189dea3be/d3d0841be49031421051ecc7947c98e71fab122c74c34cb3ccd641b906858e60.jpg.meta.json deleted file mode 100644 index c30680b..0000000 --- a/data/documents/vehicle-images/auth0|690b785932aca535dabc8d96/efef478c-26a5-4cfa-ac15-80b189dea3be/d3d0841be49031421051ecc7947c98e71fab122c74c34cb3ccd641b906858e60.jpg.meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "x-original-filename": "C7Z06-BlackRose.jpg", - "content-type": "image/jpeg" -} \ No newline at end of file diff --git a/frontend/src/features/stations/pages/StationsPage.tsx b/frontend/src/features/stations/pages/StationsPage.tsx index 46803cd..a115423 100644 --- a/frontend/src/features/stations/pages/StationsPage.tsx +++ b/frontend/src/features/stations/pages/StationsPage.tsx @@ -146,6 +146,20 @@ export const StationsPage: React.FC = () => { }; }, [savedStations]); + // Compute which stations to display on map based on active tab + const mapStations = useMemo(() => { + if (tabValue === 1) { + // Saved tab: show saved stations with valid coordinates + return savedStations.filter( + (station) => + station.latitude !== undefined && + station.longitude !== undefined + ) as Station[]; + } + // Results tab: show search results + return searchResults; + }, [tabValue, savedStations, searchResults]); + // Handle search const handleSearch = (request: StationSearchRequest) => { setCurrentLocation({ latitude: request.latitude, longitude: request.longitude }); @@ -246,7 +260,7 @@ export const StationsPage: React.FC = () => { { = ({ }} > {items.map(({ key, label, icon }) => ( - ))}