From 093b75d775d5c29774458d1e3c0f4733a65985a7 Mon Sep 17 00:00:00 2001 From: Millaguie Date: Fri, 28 Nov 2025 16:00:49 +0100 Subject: [PATCH] fix: bearing maths --- .gitea/workflows/release.yml | 5 +- docker-compose.prod.yml | 143 +++++++++++++----------------- frontend/src/App.jsx | 4 +- frontend/src/hooks/useTimeline.js | 37 ++++++-- frontend/src/hooks/useTrains.js | 15 ++-- 5 files changed, 108 insertions(+), 96 deletions(-) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index c1350ac..2c3b95c 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -52,10 +52,11 @@ jobs: uses: docker/build-push-action@v5 with: context: ./frontend + target: production push: true build-args: | - VITE_API_URL=${{ secrets.PROD_API_URL }} - VITE_WS_URL=${{ secrets.PROD_WS_URL }} + VITE_API_URL=${{ secrets.PROD_API_URL || 'https://trenes.millaguie.net/api' }} + VITE_WS_URL=${{ secrets.PROD_WS_URL || 'https://trenes.millaguie.net' }} APP_VERSION=${{ steps.version.outputs.version }} BUILD_DATE=${{ steps.date.outputs.date }} GIT_COMMIT=${{ github.sha }} diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index ebb7cd9..586fd70 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,17 +1,15 @@ -version: '3.8' - # Docker Compose para producción -# Uso: docker compose -f docker-compose.prod.yml up -d --build +# Uso: docker compose -f docker-compose.prod.yml up -d # # Requisitos previos: # 1. Configurar .env con valores de producción (ver .env.example) -# 2. Configurar certificados SSL en ./nginx/ssl/ o usar certbot +# 2. Login al registry: docker login tea.millaguie.net # 3. Configurar nginx/prod.conf con tu dominio services: # Base de datos PostgreSQL con extensión PostGIS postgres: - image: postgis/postgis:16-3.4-alpine # IMPORTANTE: usar versión 16 para compatibilidad + image: postgis/postgis:16-3.4-alpine container_name: trenes-postgres restart: unless-stopped environment: @@ -68,17 +66,42 @@ services: condition: service_healthy networks: - trenes-network - profiles: - - migration + + # API Backend + api: + image: tea.millaguie.net/millaguie/trenes-backend:${IMAGE_TAG:-latest} + container_name: trenes-api + restart: unless-stopped + command: ["node", "src/api/server.js"] + environment: + NODE_ENV: production + PORT: 3000 + DATABASE_URL: postgresql://${POSTGRES_USER:-trenes}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-trenes} + REDIS_URL: redis://redis:6379 + CORS_ORIGIN: ${CORS_ORIGINS:-https://localhost} + JWT_SECRET: ${JWT_SECRET:-change_me_in_production} + LOG_LEVEL: info + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + flyway: + condition: service_completed_successfully + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/health"] + interval: 30s + timeout: 10s + retries: 3 + networks: + - trenes-network # Worker para polling GTFS-RT Vehicle Positions worker: - build: - context: ./backend - dockerfile: Dockerfile - target: worker + image: tea.millaguie.net/millaguie/trenes-backend:${IMAGE_TAG:-latest} container_name: trenes-worker restart: unless-stopped + command: ["node", "src/worker/gtfs-poller.js"] environment: NODE_ENV: production DATABASE_URL: postgresql://${POSTGRES_USER:-trenes}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-trenes} @@ -91,24 +114,23 @@ services: condition: service_healthy redis: condition: service_healthy + flyway: + condition: service_completed_successfully networks: - trenes-network # Worker para sincronización GTFS Static gtfs-static-syncer: - build: - context: ./backend - dockerfile: Dockerfile - target: worker + image: tea.millaguie.net/millaguie/trenes-backend:${IMAGE_TAG:-latest} container_name: trenes-gtfs-static-syncer restart: unless-stopped - command: node src/worker/gtfs-static-syncer.js + command: ["node", "src/worker/gtfs-static-syncer.js"] environment: NODE_ENV: production DATABASE_URL: postgresql://${POSTGRES_USER:-trenes}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-trenes} REDIS_URL: redis://redis:6379 GTFS_STATIC_URL: https://data.renfe.com/dataset/horarios-trenes-largo-recorrido-ave/resource/horarios-trenes-largo-recorrido-ave-gtfs.zip - SYNC_SCHEDULE: 0 3 * * * + SYNC_SCHEDULE: "0 3 * * *" LOG_LEVEL: info volumes: - gtfs_static_data:/tmp/gtfs @@ -117,18 +139,17 @@ services: condition: service_healthy redis: condition: service_healthy + flyway: + condition: service_completed_successfully networks: - trenes-network # Worker para polling GTFS-RT Trip Updates trip-updates-poller: - build: - context: ./backend - dockerfile: Dockerfile - target: worker + image: tea.millaguie.net/millaguie/trenes-backend:${IMAGE_TAG:-latest} container_name: trenes-trip-updates-poller restart: unless-stopped - command: node src/worker/trip-updates-poller.js + command: ["node", "src/worker/trip-updates-poller.js"] environment: NODE_ENV: production DATABASE_URL: postgresql://${POSTGRES_USER:-trenes}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-trenes} @@ -141,18 +162,17 @@ services: condition: service_healthy redis: condition: service_healthy + flyway: + condition: service_completed_successfully networks: - trenes-network # Worker para polling GTFS-RT Service Alerts alerts-poller: - build: - context: ./backend - dockerfile: Dockerfile - target: worker + image: tea.millaguie.net/millaguie/trenes-backend:${IMAGE_TAG:-latest} container_name: trenes-alerts-poller restart: unless-stopped - command: node src/worker/alerts-poller.js + command: ["node", "src/worker/alerts-poller.js"] environment: NODE_ENV: production DATABASE_URL: postgresql://${POSTGRES_USER:-trenes}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-trenes} @@ -165,18 +185,17 @@ services: condition: service_healthy redis: condition: service_healthy + flyway: + condition: service_completed_successfully networks: - trenes-network # Worker para datos de flota Renfe renfe-fleet-poller: - build: - context: ./backend - dockerfile: Dockerfile - target: worker + image: tea.millaguie.net/millaguie/trenes-backend:${IMAGE_TAG:-latest} container_name: trenes-renfe-fleet-poller restart: unless-stopped - command: node src/worker/renfe-fleet-poller.js + command: ["node", "src/worker/renfe-fleet-poller.js"] environment: NODE_ENV: production DATABASE_URL: postgresql://${POSTGRES_USER:-trenes}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-trenes} @@ -187,18 +206,17 @@ services: condition: service_healthy redis: condition: service_healthy + flyway: + condition: service_completed_successfully networks: - trenes-network # Worker para refrescar vistas de analytics analytics-refresher: - build: - context: ./backend - dockerfile: Dockerfile - target: worker + image: tea.millaguie.net/millaguie/trenes-backend:${IMAGE_TAG:-latest} container_name: trenes-analytics-refresher restart: unless-stopped - command: node src/worker/analytics-refresher.js + command: ["node", "src/worker/analytics-refresher.js"] environment: NODE_ENV: production DATABASE_URL: postgresql://${POSTGRES_USER:-trenes}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-trenes} @@ -210,49 +228,14 @@ services: condition: service_healthy redis: condition: service_healthy - networks: - - trenes-network - - # API Backend - api: - build: - context: ./backend - dockerfile: Dockerfile - target: api - container_name: trenes-api - restart: unless-stopped - environment: - NODE_ENV: production - PORT: 3000 - DATABASE_URL: postgresql://${POSTGRES_USER:-trenes}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-trenes} - REDIS_URL: redis://redis:6379 - CORS_ORIGIN: ${CORS_ORIGINS:-https://localhost} - JWT_SECRET: ${JWT_SECRET} - LOG_LEVEL: info - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - healthcheck: - test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/health"] - interval: 30s - timeout: 10s - retries: 3 + flyway: + condition: service_completed_successfully networks: - trenes-network # Frontend - # IMPORTANTE: Las variables VITE_* deben pasarse como build args, no como environment - # ya que se procesan en tiempo de compilación, no en runtime frontend: - build: - context: ./frontend - dockerfile: Dockerfile - target: production - args: - VITE_API_URL: ${VITE_API_URL} - VITE_WS_URL: ${VITE_WS_URL} + image: tea.millaguie.net/millaguie/trenes-frontend:${IMAGE_TAG:-latest} container_name: trenes-frontend restart: unless-stopped depends_on: @@ -267,7 +250,7 @@ services: restart: unless-stopped volumes: - ./nginx/prod.conf:/etc/nginx/conf.d/default.conf:ro - - letsencrypt_certs:/etc/letsencrypt:ro + - certbot_certs:/etc/letsencrypt:ro - certbot_webroot:/var/www/certbot:ro ports: - "80:80" @@ -288,7 +271,7 @@ services: image: certbot/certbot container_name: trenes-certbot volumes: - - letsencrypt_certs:/etc/letsencrypt + - certbot_certs:/etc/letsencrypt - certbot_webroot:/var/www/certbot entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" networks: @@ -301,10 +284,10 @@ volumes: driver: local gtfs_static_data: driver: local - letsencrypt_certs: - driver: local + certbot_certs: + external: true certbot_webroot: - driver: local + external: true networks: trenes-network: diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index efe5bc9..a93ead8 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -68,8 +68,8 @@ function App() { {activeView === 'dashboard' ? 'Dashboard de Trenes' : isTimelineMode - ? 'Reproduccion Historica - Espana' - : 'Trenes en Tiempo Real - Espana'} + ? 'Reproduccion Historica - España' + : 'Trenes en Tiempo Real - España'} diff --git a/frontend/src/hooks/useTimeline.js b/frontend/src/hooks/useTimeline.js index 797fc29..eec2aff 100644 --- a/frontend/src/hooks/useTimeline.js +++ b/frontend/src/hooks/useTimeline.js @@ -4,7 +4,8 @@ import { calculateBearing, calculateDistance, MIN_DISTANCE_FOR_BEARING } from '. const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000'; /** - * Pre-calculate bearings for historical data based on consecutive positions + * Pre-calculate bearings for historical data based on the last different position + * Searches backwards to find a position with significant distance for accurate bearing * @param {Array} positions - Array of positions sorted by timestamp ASC * @returns {Array} - Positions with calculated bearings */ @@ -26,19 +27,43 @@ function calculateHistoricalBearings(positions) { // Sort by timestamp (should already be sorted, but ensure) trainPos.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp)); + let lastSignificantPos = null; // Track last position with significant movement + let lastBearing = null; // Track last calculated bearing + for (let i = 0; i < trainPos.length; i++) { const current = trainPos[i]; let bearing = current.bearing; // Use existing bearing if available if (bearing === null || bearing === undefined) { - // Calculate bearing from previous position - if (i > 0) { - const prev = trainPos[i - 1]; - const distance = calculateDistance(prev.latitude, prev.longitude, current.latitude, current.longitude); + // Search backwards for the last position with significant distance + if (lastSignificantPos) { + const distance = calculateDistance( + lastSignificantPos.latitude, + lastSignificantPos.longitude, + current.latitude, + current.longitude + ); if (distance >= MIN_DISTANCE_FOR_BEARING) { - bearing = calculateBearing(prev.latitude, prev.longitude, current.latitude, current.longitude); + bearing = calculateBearing( + lastSignificantPos.latitude, + lastSignificantPos.longitude, + current.latitude, + current.longitude + ); + lastSignificantPos = current; + lastBearing = bearing; + } else { + // No significant movement, keep the last known bearing + bearing = lastBearing; } + } else { + // First position for this train + lastSignificantPos = current; } + } else { + // Has bearing from API, update tracking + lastSignificantPos = current; + lastBearing = bearing; } result.push({ diff --git a/frontend/src/hooks/useTrains.js b/frontend/src/hooks/useTrains.js index d954531..d97d997 100644 --- a/frontend/src/hooks/useTrains.js +++ b/frontend/src/hooks/useTrains.js @@ -6,28 +6,31 @@ const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000'; const WS_URL = import.meta.env.VITE_WS_URL || 'http://localhost:3000'; // Calculate bearing for trains based on previous positions +// Only updates the stored position when there's significant movement function addCalculatedBearings(newTrains, previousPositions) { return newTrains.map(train => { // If train already has bearing from API, use it if (train.bearing !== null && train.bearing !== undefined) { - previousPositions.set(train.train_id, { lat: train.latitude, lon: train.longitude }); + previousPositions.set(train.train_id, { lat: train.latitude, lon: train.longitude, bearing: train.bearing }); return train; } const prevPos = previousPositions.get(train.train_id); - let calculatedBearing = null; + let calculatedBearing = prevPos?.bearing ?? null; if (prevPos) { const distance = calculateDistance(prevPos.lat, prevPos.lon, train.latitude, train.longitude); - // Only calculate bearing if the train moved enough + // Only calculate bearing and update position if the train moved enough if (distance >= MIN_DISTANCE_FOR_BEARING) { calculatedBearing = calculateBearing(prevPos.lat, prevPos.lon, train.latitude, train.longitude); + // Only update stored position when there's significant movement + previousPositions.set(train.train_id, { lat: train.latitude, lon: train.longitude, bearing: calculatedBearing }); } + } else { + // First time seeing this train, store position without bearing + previousPositions.set(train.train_id, { lat: train.latitude, lon: train.longitude, bearing: null }); } - // Update previous position - previousPositions.set(train.train_id, { lat: train.latitude, lon: train.longitude }); - return { ...train, bearing: calculatedBearing,