feat: Initial commit - Train tracking system
Some checks failed
Auto Tag on Merge to Main / auto-tag (push) Successful in 27s
CI - Lint and Build / lint-backend (push) Failing after 30s
CI - Lint and Build / lint-frontend (push) Failing after 2s
CI - Lint and Build / build-frontend (push) Has been skipped
CI - Lint and Build / docker-build-test (push) Has been skipped

Complete real-time train tracking system for Spanish railways (Renfe/Cercanías):

- Backend API (Node.js/Express) with GTFS-RT polling workers
- Frontend dashboard (React/Vite) with Leaflet maps
- Real-time updates via Socket.io WebSocket
- PostgreSQL/PostGIS database with Flyway migrations
- Redis caching layer
- Docker Compose configuration for development and production
- Gitea CI/CD workflows (lint, auto-tag, release)
- Production deployment with nginx + Let's Encrypt SSL

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Millaguie
2025-11-28 00:21:15 +01:00
commit 34c0cb50c7
64 changed files with 15577 additions and 0 deletions

958
FASE3-ANALYTICS.md Normal file
View File

@@ -0,0 +1,958 @@
# Fase 3: Analytics y Exploración Avanzada
## Estado: ✅ IMPLEMENTADO (Backend)
La Fase 3 añade capacidades avanzadas de análisis, exploración de rutas, planificación de viajes y exportación de datos.
---
## ✨ Características Implementadas
### Backend
#### Base de Datos
- ✅ Migración V6: Vistas materializadas para analytics
- ✅ Funciones para heatmaps, estadísticas y análisis
- ✅ Tablas para cache de exportaciones
- ✅ Vistas de sistema: traffic_by_hour, traffic_by_route, daily_statistics, route_performance
#### Workers
- ✅ Analytics Refresher: Refresco automático de vistas materializadas cada 15 minutos
#### API REST
- ✅ Analytics API (`/analytics`) - Heatmaps, estadísticas, performance
- ✅ Explorer API (`/explorer`) - Explorador de rutas, planificador de viajes, búsqueda
### Frontend
- ⏳ Componentes de Analytics (pendiente)
- ⏳ Heatmap de tráfico (pendiente)
- ⏳ Dashboard de estadísticas (pendiente)
- ⏳ Planificador de viajes UI (pendiente)
---
## 📁 Nuevos Archivos Phase 3
### Base de Datos
```
database/migrations/
└── V6__analytics_and_statistics.sql # Vistas y funciones de analytics
```
### Backend Workers
```
backend/src/worker/
└── analytics-refresher.js # Refresco de vistas materializadas
```
### Backend API
```
backend/src/api/routes/
├── analytics.js # Endpoints de analytics y exportación
└── explorer.js # Explorador de rutas y planificador
```
---
## 🚀 Ejecutar con Fase 3
### Usando Docker Compose
```bash
# 1. Ejecutar migraciones (incluye V6)
make migrate
# 2. Iniciar todos los servicios (incluye analytics-refresher)
make start
# El worker analytics-refresher se inicia automáticamente
# y refresca las vistas cada 15 minutos
```
### Desarrollo Local
```bash
cd backend
# Terminal 1: Analytics Refresher
npm run dev:analytics
# Terminal 2: API Server
npm run dev
```
---
## 📡 Nuevos Endpoints API
### Analytics - Traffic
#### GET /analytics/traffic/heatmap
Obtener datos de heatmap de tráfico.
**Query Parameters:**
- `start_date` (opcional): Fecha inicio (ISO 8601)
- `end_date` (opcional): Fecha fin (ISO 8601)
- `grid_size` (opcional): Tamaño de celda en grados (default: 0.1 ≈ 11km)
**Ejemplo:**
```bash
# Heatmap últimos 7 días
curl http://localhost:3000/analytics/traffic/heatmap
# Heatmap con grid más fino
curl http://localhost:3000/analytics/traffic/heatmap?grid_size=0.05
# Rango personalizado
curl "http://localhost:3000/analytics/traffic/heatmap?start_date=2025-11-20T00:00:00Z&end_date=2025-11-27T23:59:59Z"
```
**Respuesta:**
```json
[
{
"lat": 40.4,
"lon": -3.7,
"intensity": 125,
"avgSpeed": 78.5
},
{
"lat": 41.3,
"lon": 2.1,
"intensity": 98,
"avgSpeed": 65.3
}
]
```
#### GET /analytics/traffic/hourly
Obtener patrón de tráfico por hora del día.
**Query Parameters:**
- `days` (opcional): Número de días para analizar (default: 7)
**Ejemplo:**
```bash
curl http://localhost:3000/analytics/traffic/hourly?days=30
```
**Respuesta:**
```json
[
{
"hour_of_day": 0,
"avg_trains": 15.3,
"avg_speed": 45.2,
"total_observations": 45230
},
{
"hour_of_day": 7,
"avg_trains": 87.5,
"avg_speed": 68.7,
"total_observations": 125890
}
]
```
#### GET /analytics/traffic/by-hour
Obtener estadísticas de tráfico agregadas por hora.
**Query Parameters:**
- `limit` (opcional): Número de horas (default: 24)
**Ejemplo:**
```bash
curl http://localhost:3000/analytics/traffic/by-hour?limit=48
```
#### GET /analytics/traffic/by-route
Obtener estadísticas de tráfico por ruta.
**Ejemplo:**
```bash
curl http://localhost:3000/analytics/traffic/by-route
```
**Respuesta:**
```json
[
{
"route_id": "AVE-MAD-BCN",
"route_name": "Madrid - Barcelona",
"route_type": "HIGH_SPEED",
"total_trains": 234,
"active_days": 30,
"avg_speed": 185.4,
"total_positions": 125890
}
]
```
---
### Analytics - Statistics
#### GET /analytics/statistics/daily
Obtener estadísticas diarias del sistema.
**Query Parameters:**
- `days` (opcional): Número de días (default: 30)
**Ejemplo:**
```bash
curl http://localhost:3000/analytics/statistics/daily?days=90
```
**Respuesta:**
```json
[
{
"date": "2025-11-27",
"unique_trains": 456,
"total_positions": 65432,
"avg_speed": 72.3,
"stopped_count": 12890,
"moving_count": 52542
}
]
```
#### GET /analytics/statistics/system
Obtener estado actual del sistema.
**Ejemplo:**
```bash
curl http://localhost:3000/analytics/statistics/system
```
**Respuesta:**
```json
{
"active_trains": 234,
"active_alerts": 5,
"delayed_trips": 12,
"avg_delay_seconds": 450,
"active_routes": 45,
"last_update": "2025-11-27T15:30:00Z"
}
```
---
### Analytics - Performance
#### GET /analytics/performance/routes
Obtener métricas de rendimiento de rutas (puntualidad, retrasos).
**Query Parameters:**
- `limit` (opcional): Número de rutas (default: 20)
**Ejemplo:**
```bash
curl http://localhost:3000/analytics/performance/routes?limit=10
```
**Respuesta:**
```json
[
{
"route_id": "AVE-MAD-BCN",
"route_name": "Madrid - Barcelona",
"total_trips": 345,
"delayed_trips": 23,
"on_time_trips": 322,
"avg_delay_seconds": 180,
"median_delay_seconds": 120,
"max_delay_seconds": 1800,
"punctuality_percentage": 93.33
}
]
```
#### GET /analytics/performance/route/:routeId
Obtener estadísticas detalladas de una ruta específica.
**Query Parameters:**
- `days` (opcional): Número de días para analizar (default: 7)
**Ejemplo:**
```bash
curl http://localhost:3000/analytics/performance/route/AVE-MAD-BCN?days=30
```
**Respuesta:**
```json
{
"total_trips": 234,
"unique_trains": 45,
"avg_speed": 185.4,
"max_speed": 298.7,
"total_distance_km": 98765.4,
"avg_delay_seconds": 120,
"on_time_percentage": 92.5
}
```
---
### Analytics - Delays
#### GET /analytics/delays/top-routes
Obtener rutas con más retrasos.
**Ejemplo:**
```bash
curl http://localhost:3000/analytics/delays/top-routes
```
**Respuesta:**
```json
[
{
"route_id": "MD-MAD-VAL",
"route_name": "Madrid - Valencia",
"delayed_count": 45,
"avg_delay": 600,
"max_delay": 2400
}
]
```
---
### Analytics - Stations
#### GET /analytics/stations/busiest
Obtener estaciones más transitadas.
**Query Parameters:**
- `limit` (opcional): Número de estaciones (default: 20)
**Ejemplo:**
```bash
curl http://localhost:3000/analytics/stations/busiest?limit=10
```
**Respuesta:**
```json
[
{
"stop_id": "MADRID-PUERTA-DE-ATOCHA",
"stop_name": "Madrid Puerta de Atocha",
"daily_trips": 456,
"routes_count": 34
}
]
```
#### GET /analytics/stations/:stationId/statistics
Obtener estadísticas de una estación específica.
**Query Parameters:**
- `days` (opcional): Número de días (default: 7)
**Ejemplo:**
```bash
curl http://localhost:3000/analytics/stations/MADRID-PUERTA-DE-ATOCHA/statistics?days=30
```
**Respuesta:**
```json
{
"total_departures": 3450,
"total_arrivals": 3420,
"unique_routes": 45,
"avg_delay_minutes": 2.5,
"busiest_hour": 8
}
```
---
### Analytics - Trains
#### GET /analytics/trains/:trainId/distance
Calcular distancia recorrida por un tren.
**Query Parameters:**
- `start_time` (opcional): Timestamp inicio
- `end_time` (opcional): Timestamp fin
**Ejemplo:**
```bash
curl "http://localhost:3000/analytics/trains/12345/distance?start_time=2025-11-27T00:00:00Z&end_time=2025-11-27T23:59:59Z"
```
**Respuesta:**
```json
{
"train_id": "12345",
"start_time": "2025-11-27T00:00:00Z",
"end_time": "2025-11-27T23:59:59Z",
"distance_km": 1234.56
}
```
---
### Analytics - Export
#### GET /analytics/export
Exportar datos en diferentes formatos.
**Query Parameters:**
- `table` (requerido): Tabla a exportar
- `format` (opcional): json, csv, geojson (default: json)
- `start_date` (opcional): Fecha inicio
- `end_date` (opcional): Fecha fin
- `limit` (opcional): Límite de registros (default: 1000)
**Tablas permitidas:**
- train_positions
- trains
- routes
- stations
- alerts
- trip_updates
- traffic_by_hour
- daily_statistics
**Ejemplo JSON:**
```bash
curl "http://localhost:3000/analytics/export?table=trains&format=json"
```
**Ejemplo CSV:**
```bash
curl "http://localhost:3000/analytics/export?table=train_positions&format=csv&limit=5000" > positions.csv
```
**Ejemplo GeoJSON:**
```bash
curl "http://localhost:3000/analytics/export?table=stations&format=geojson" > stations.geojson
```
---
### Analytics - Refresh
#### POST /analytics/refresh
Refrescar manualmente las vistas materializadas.
**Ejemplo:**
```bash
curl -X POST http://localhost:3000/analytics/refresh
```
**Respuesta:**
```json
{
"success": true,
"message": "Analytics views refreshed successfully",
"timestamp": "2025-11-27T15:30:00Z"
}
```
---
## 📍 Explorer - Route Explorer
### GET /explorer/routes/:routeId
Obtener información completa de una ruta (trips, stops, shape).
**Ejemplo:**
```bash
curl http://localhost:3000/explorer/routes/AVE-MAD-BCN
```
**Respuesta:**
```json
{
"route": {
"route_id": "AVE-MAD-BCN",
"route_name": "Madrid - Barcelona",
"route_type": "HIGH_SPEED"
},
"trips": [
{
"trip_id": "trip_001",
"trip_headsign": "Barcelona Sants",
"direction_id": 0
}
],
"stops": [
{
"stop_id": "MADRID-PUERTA-DE-ATOCHA",
"stop_name": "Madrid Puerta de Atocha",
"stop_lat": 40.4067,
"stop_lon": -3.6906
}
],
"shape": {
"shape_id": "shape_001",
"points": [
{
"lat": 40.4067,
"lon": -3.6906,
"sequence": 1,
"distance": 0
}
]
},
"total_trips": 45,
"total_stops": 8
}
```
### GET /explorer/trips/:tripId/schedule
Obtener horario completo de un viaje.
**Ejemplo:**
```bash
curl http://localhost:3000/explorer/trips/trip_12345/schedule
```
**Respuesta:**
```json
[
{
"stop_id": "MADRID-PUERTA-DE-ATOCHA",
"stop_name": "Madrid Puerta de Atocha",
"arrival_time": "08:00:00",
"departure_time": "08:00:00",
"stop_sequence": 1
},
{
"stop_id": "BARCELONA-SANTS",
"stop_name": "Barcelona Sants",
"arrival_time": "10:45:00",
"departure_time": "10:45:00",
"stop_sequence": 3
}
]
```
---
## 🚉 Explorer - Stations
### GET /explorer/stations/:stationId
Obtener información completa de una estación.
**Ejemplo:**
```bash
curl http://localhost:3000/explorer/stations/MADRID-PUERTA-DE-ATOCHA
```
**Respuesta:**
```json
{
"station": {
"stop_id": "MADRID-PUERTA-DE-ATOCHA",
"stop_name": "Madrid Puerta de Atocha",
"stop_lat": 40.4067,
"stop_lon": -3.6906
},
"next_departures": [
{
"trip_id": "trip_001",
"route_id": "AVE-MAD-BCN",
"route_name": "Madrid - Barcelona",
"headsign": "Barcelona Sants",
"scheduled_departure": "15:00:00",
"estimated_delay": 180,
"status": "DELAYED"
}
],
"routes": [
{
"route_id": "AVE-MAD-BCN",
"route_name": "Madrid - Barcelona"
}
],
"statistics": {
"total_departures": 456,
"total_arrivals": 450,
"unique_routes": 34,
"avg_delay_minutes": 2.5,
"busiest_hour": 8
}
}
```
### GET /explorer/stations/:stationId/nearby
Obtener estaciones cercanas.
**Query Parameters:**
- `radius` (opcional): Radio en km (default: 5)
**Ejemplo:**
```bash
curl "http://localhost:3000/explorer/stations/MADRID-PUERTA-DE-ATOCHA/nearby?radius=10"
```
**Respuesta:**
```json
[
{
"stop_id": "MADRID-CHAMARTIN",
"stop_name": "Madrid Chamartín",
"stop_lat": 40.4728,
"stop_lon": -3.6797,
"distance_km": 7.8
}
]
```
---
## 🗺️ Explorer - Trip Planner
### GET /explorer/planner
Planificador de viajes entre dos estaciones.
**Query Parameters:**
- `origin` (requerido): ID de estación origen
- `destination` (requerido): ID de estación destino
- `time` (opcional): Hora de salida (HH:MM:SS)
- `date` (opcional): Fecha (YYYY-MM-DD)
**Ejemplo:**
```bash
curl "http://localhost:3000/explorer/planner?origin=MADRID-PUERTA-DE-ATOCHA&destination=BARCELONA-SANTS&time=08:00:00"
```
**Respuesta:**
```json
{
"origin": "MADRID-PUERTA-DE-ATOCHA",
"destination": "BARCELONA-SANTS",
"requested_time": "08:00:00",
"requested_date": "today",
"direct_trips": [
{
"trip_id": "trip_001",
"route_id": "AVE-MAD-BCN",
"route_name": "Madrid - Barcelona",
"trip_headsign": "Barcelona Sants",
"origin_departure": "08:00:00",
"destination_arrival": "10:45:00",
"duration_minutes": 165,
"delay": {
"delay_seconds": 180,
"schedule_relationship": "SCHEDULED"
}
}
],
"trips_with_transfer": [
{
"trip1_id": "trip_002",
"route1_name": "Madrid - Zaragoza",
"trip2_id": "trip_003",
"route2_name": "Zaragoza - Barcelona",
"origin_departure": "08:30:00",
"transfer_arrival": "09:50:00",
"transfer_departure": "10:10:00",
"destination_arrival": "11:30:00",
"transfer_station": "ZARAGOZA-DELICIAS",
"transfer_station_name": "Zaragoza Delicias",
"total_duration_minutes": 180
}
],
"total_options": 3
}
```
### GET /explorer/routes/between
Encontrar rutas que conectan dos estaciones.
**Query Parameters:**
- `origin` (requerido): ID de estación origen
- `destination` (requerido): ID de estación destino
**Ejemplo:**
```bash
curl "http://localhost:3000/explorer/routes/between?origin=MADRID-PUERTA-DE-ATOCHA&destination=BARCELONA-SANTS"
```
**Respuesta:**
```json
[
{
"route_id": "AVE-MAD-BCN",
"route_name": "Madrid - Barcelona",
"route_type": "HIGH_SPEED",
"route_color": "FF6600",
"daily_trips": 25
}
]
```
---
## 🔍 Explorer - Search
### GET /explorer/search
Buscar estaciones por nombre.
**Query Parameters:**
- `query` (requerido): Término de búsqueda (mínimo 2 caracteres)
- `limit` (opcional): Número de resultados (default: 10)
**Ejemplo:**
```bash
curl "http://localhost:3000/explorer/search?query=madrid&limit=5"
```
**Respuesta:**
```json
[
{
"stop_id": "MADRID-PUERTA-DE-ATOCHA",
"stop_name": "Madrid Puerta de Atocha",
"stop_lat": 40.4067,
"stop_lon": -3.6906,
"location_type": 1,
"parent_station": null
},
{
"stop_id": "MADRID-CHAMARTIN",
"stop_name": "Madrid Chamartín",
"stop_lat": 40.4728,
"stop_lon": -3.6797,
"location_type": 1,
"parent_station": null
}
]
```
---
## 🗄️ Vistas Materializadas
### traffic_by_hour
Tráfico agregado por hora (últimos 30 días).
**Columnas:**
- hour: Hora (timestamp)
- active_trains: Número de trenes activos
- total_positions: Total de posiciones registradas
- avg_speed: Velocidad promedio
- median_speed: Velocidad mediana
- max_speed: Velocidad máxima
### traffic_by_route
Tráfico por ruta (últimos 30 días).
**Columnas:**
- route_id, route_name, route_type
- total_trains: Total de trenes diferentes
- active_days: Días con actividad
- avg_speed: Velocidad promedio
- total_positions: Posiciones registradas
### daily_statistics
Estadísticas diarias del sistema (últimos 90 días).
**Columnas:**
- date: Fecha
- unique_trains: Trenes únicos
- total_positions: Posiciones totales
- avg_speed: Velocidad promedio
- stopped_count: Posiciones paradas
- moving_count: Posiciones en movimiento
### route_performance
Rendimiento y puntualidad por ruta (últimos 30 días).
**Columnas:**
- route_id, route_name
- total_trips: Total de viajes
- delayed_trips: Viajes retrasados
- on_time_trips: Viajes puntuales
- avg_delay_seconds: Retraso promedio
- median_delay_seconds: Retraso mediano
- max_delay_seconds: Retraso máximo
- punctuality_percentage: % de puntualidad
**Refresco:** Cada 15 minutos automáticamente via analytics-refresher worker.
---
## 🧪 Probar Phase 3
### 1. Verificar Analytics Refresher
```bash
# Ver logs del worker
docker-compose logs -f analytics-refresher
# Deberías ver cada 15 minutos:
# "Starting analytics views refresh..."
# "Analytics views refreshed successfully"
```
### 2. Probar Analytics API
```bash
# Sistema general
curl http://localhost:3000/analytics/statistics/system | jq
# Heatmap
curl http://localhost:3000/analytics/traffic/heatmap | jq
# Performance de rutas
curl http://localhost:3000/analytics/performance/routes | jq
# Rutas más retrasadas
curl http://localhost:3000/analytics/delays/top-routes | jq
# Estaciones más transitadas
curl http://localhost:3000/analytics/stations/busiest | jq
```
### 3. Probar Explorer API
```bash
# Buscar estación
curl "http://localhost:3000/explorer/search?query=madrid" | jq
# Info de estación
curl http://localhost:3000/explorer/stations/MADRID-PUERTA-DE-ATOCHA | jq
# Info de ruta
curl http://localhost:3000/explorer/routes/AVE-MAD-BCN | jq
# Planificar viaje
curl "http://localhost:3000/explorer/planner?origin=MADRID-PUERTA-DE-ATOCHA&destination=BARCELONA-SANTS" | jq
```
### 4. Probar Exportación
```bash
# Exportar a CSV
curl "http://localhost:3000/analytics/export?table=routes&format=csv" > routes.csv
# Exportar a GeoJSON
curl "http://localhost:3000/analytics/export?table=stations&format=geojson" > stations.geojson
# Exportar posiciones filtradas
curl "http://localhost:3000/analytics/export?table=train_positions&start_date=2025-11-27T00:00:00Z&limit=1000" > positions.json
```
---
## 📊 Use Cases
### Dashboard de Control
```bash
# Obtener datos para dashboard
curl http://localhost:3000/analytics/statistics/system
curl http://localhost:3000/analytics/traffic/by-route
curl http://localhost:3000/analytics/delays/top-routes
curl http://localhost:3000/analytics/stations/busiest?limit=5
```
### Análisis de Rendimiento
```bash
# Análisis de una ruta específica
curl http://localhost:3000/analytics/performance/route/AVE-MAD-BCN?days=30
# Estadísticas diarias últimos 30 días
curl http://localhost:3000/analytics/statistics/daily?days=30
# Distancia recorrida por tren
curl "http://localhost:3000/analytics/trains/12345/distance?start_time=2025-11-01T00:00:00Z"
```
### Planificación de Viajes
```bash
# Buscar estación
curl "http://localhost:3000/explorer/search?query=barcelona"
# Ver info completa de estación
curl http://localhost:3000/explorer/stations/BARCELONA-SANTS
# Planificar viaje
curl "http://localhost:3000/explorer/planner?origin=MADRID-PUERTA-DE-ATOCHA&destination=BARCELONA-SANTS&time=08:00:00"
# Ver rutas que conectan dos puntos
curl "http://localhost:3000/explorer/routes/between?origin=MADRID-PUERTA-DE-ATOCHA&destination=BARCELONA-SANTS"
```
---
## 🐛 Troubleshooting Phase 3
### Las vistas materializadas están vacías
**Causa**: No hay suficientes datos históricos o no se han refrescado.
**Solución**:
```bash
# Refrescar manualmente
curl -X POST http://localhost:3000/analytics/refresh
# Verificar en PostgreSQL
make psql
> SELECT COUNT(*) FROM traffic_by_hour;
> SELECT COUNT(*) FROM traffic_by_route;
```
### El planificador no encuentra viajes
**Causa**: Datos GTFS Static no cargados o formato de parámetros incorrecto.
**Solución**:
```bash
# Verificar que hay trips y stop_times
make psql
> SELECT COUNT(*) FROM trips;
> SELECT COUNT(*) FROM stop_times;
# Verificar formato de parámetros
curl "http://localhost:3000/explorer/planner?origin=STOP_ID_ORIGIN&destination=STOP_ID_DEST"
```
### Export devuelve error
**Causa**: Tabla no permitida o parámetros inválidos.
**Solución**:
```bash
# Ver tablas permitidas
curl "http://localhost:3000/analytics/export" | jq '.allowed_tables'
# Usar tabla válida
curl "http://localhost:3000/analytics/export?table=routes&format=json"
```
---
## 📝 Próximos Pasos
Funcionalidades pendientes de Phase 3:
- [ ] Frontend: Heatmap de tráfico en mapa
- [ ] Frontend: Dashboard de estadísticas con gráficos
- [ ] Frontend: UI del planificador de viajes
- [ ] Frontend: Explorador de rutas interactivo
- [ ] WebSocket para updates de analytics en tiempo real
- [ ] Overlay de infraestructura ADIF (WMS)
- [ ] Predicciones ML (Fase 4)
---
## 📚 Documentación Relacionada
- [Arquitectura Completa](arquitectura-sistema-tracking-trenes.md)
- [Fase 1 - MVP](FASE1-MVP.md)
- [Fase 2 - Enriquecimiento](FASE2-ENRIQUECIMIENTO.md)
- [Fuentes de Datos](FUENTES_DATOS.md)
- [README Principal](README.md)
---
**Estado**: Fase 3 - Backend Completo, Frontend Pendiente
**Fecha**: 27 noviembre 2025
**Próxima Fase**: Frontend Phase 3 + Fase 4 (ML y Predicciones)