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>
15 KiB
Fase 2: Enriquecimiento - GTFS Static, Trip Updates y Service Alerts
Estado: 🚧 EN DESARROLLO
La Fase 2 añade enriquecimiento de datos mediante GTFS Static, actualizaciones de viajes en tiempo real (Trip Updates) y alertas de servicio (Service Alerts).
✨ Características Implementadas
Backend
Base de Datos
- ✅ Migración V5: Tablas GTFS Static (trips, stop_times, calendar, shapes)
- ✅ Tablas para Trip Updates y Stop Time Updates
- ✅ Vistas: active_trips_today, delayed_trips
- ✅ Funciones: get_trip_schedule, get_next_departures
Workers
- ✅ GTFS Static Syncer: Sincronización diaria de datos estáticos
- ✅ Trip Updates Poller: Polling de retrasos y actualizaciones de viajes
- ✅ Service Alerts Poller: Polling de alertas e incidencias
API REST
- ✅ Endpoints de Alertas (
/alerts) - ✅ Endpoints de Trips y Delays (
/trips)
Frontend
- ⏳ Componente de Alertas (pendiente)
- ⏳ Monitor de Puntualidad (pendiente)
- ⏳ Timeline Funcional (pendiente)
📁 Nuevos Archivos Phase 2
Base de Datos
database/migrations/
└── V5__gtfs_static_tables.sql # Tablas GTFS Static + Trip Updates
Backend Workers
backend/src/worker/
├── gtfs-static-syncer.js # Sincronización GTFS Static
├── trip-updates-poller.js # Polling Trip Updates
└── alerts-poller.js # Polling Service Alerts
Backend API
backend/src/api/routes/
├── alerts.js # Endpoints de alertas
└── trips.js # Endpoints de trips y delays
🚀 Ejecutar con Fase 2
Usando Docker Compose
# 1. Ejecutar migraciones (incluye V5)
make migrate
# 2. Iniciar todos los servicios (incluye nuevos workers)
make start
# Los nuevos workers se inician automáticamente:
# - gtfs-static-syncer (sincronización diaria a las 3 AM)
# - trip-updates-poller (polling cada 30s)
# - alerts-poller (polling cada 30s)
Desarrollo Local
cd backend
# Terminal 1: GTFS Static Syncer
npm run dev:gtfs-static
# Terminal 2: Trip Updates Poller
npm run dev:trip-updates
# Terminal 3: Service Alerts Poller
npm run dev:alerts
# Terminal 4: API Server
npm run dev
📡 Nuevos Endpoints API
Alertas
GET /alerts
Obtener todas las alertas activas con filtros opcionales.
Query Parameters:
route_id(opcional): Filtrar por rutaseverity(opcional): Filtrar por severidad (LOW, MEDIUM, HIGH, CRITICAL)type(opcional): Filtrar por tipo (DELAY, CANCELLATION, INCIDENT, etc.)
Ejemplo:
# Todas las alertas activas
curl http://localhost:3000/alerts
# Alertas de una ruta específica
curl http://localhost:3000/alerts?route_id=AVE-MAD-BCN
# Alertas críticas
curl http://localhost:3000/alerts?severity=CRITICAL
# Alertas de cancelaciones
curl http://localhost:3000/alerts?type=CANCELLATION
Respuesta:
[
{
"alert_id": 1,
"alert_type": "DELAY",
"severity": "MEDIUM",
"cause": "TECHNICAL_PROBLEM",
"effect": "SIGNIFICANT_DELAYS",
"header_text": "Retraso en AVE 03055",
"description_text": "Retraso de 15 minutos debido a problemas técnicos",
"url": null,
"route_id": "AVE-MAD-BCN",
"trip_id": "trip_12345",
"train_id": null,
"start_time": "2025-11-27T10:00:00Z",
"end_time": null,
"created_at": "2025-11-27T10:05:00Z",
"updated_at": "2025-11-27T10:05:00Z"
}
]
GET /alerts/:id
Obtener una alerta específica.
Ejemplo:
curl http://localhost:3000/alerts/1
GET /alerts/route/:routeId
Obtener todas las alertas activas de una ruta.
Ejemplo:
curl http://localhost:3000/alerts/route/AVE-MAD-BCN
GET /alerts/train/:trainId
Obtener todas las alertas activas de un tren.
Ejemplo:
curl http://localhost:3000/alerts/train/12345
Trips y Delays
GET /trips
Obtener todos los viajes activos del día.
Query Parameters:
route_id(opcional): Filtrar por rutaservice_id(opcional): Filtrar por servicio
Ejemplo:
# Todos los viajes activos hoy
curl http://localhost:3000/trips
# Viajes de una ruta específica
curl http://localhost:3000/trips?route_id=AVE-MAD-BCN
Respuesta:
[
{
"trip_id": "trip_12345",
"route_id": "AVE-MAD-BCN",
"service_id": "weekday",
"trip_headsign": "Barcelona Sants",
"direction_id": 0,
"block_id": null,
"shape_id": "shape_001"
}
]
GET /trips/:id
Obtener detalles completos de un viaje incluyendo su horario.
Ejemplo:
curl http://localhost:3000/trips/trip_12345
Respuesta:
{
"trip_id": "trip_12345",
"route_id": "AVE-MAD-BCN",
"service_id": "weekday",
"trip_headsign": "Barcelona Sants",
"direction_id": 0,
"schedule": [
{
"stop_id": "MADRID-PUERTA-DE-ATOCHA",
"stop_sequence": 1,
"arrival_time": "08:00:00",
"departure_time": "08:00:00",
"stop_headsign": null
},
{
"stop_id": "ZARAGOZA-DELICIAS",
"stop_sequence": 2,
"arrival_time": "09:25:00",
"departure_time": "09:27:00",
"stop_headsign": null
},
{
"stop_id": "BARCELONA-SANTS",
"stop_sequence": 3,
"arrival_time": "10:45:00",
"departure_time": "10:45:00",
"stop_headsign": null
}
]
}
GET /trips/:id/updates
Obtener actualizaciones en tiempo real de un viaje (retrasos, cancelaciones).
Ejemplo:
curl http://localhost:3000/trips/trip_12345/updates
Respuesta:
{
"trip_id": "trip_12345",
"has_updates": true,
"update_id": 1,
"delay_seconds": 900,
"schedule_relationship": "SCHEDULED",
"start_date": "20251127",
"received_at": "2025-11-27T10:15:00Z",
"stop_time_updates": [
{
"stop_sequence": 2,
"stop_id": "ZARAGOZA-DELICIAS",
"arrival_delay": 900,
"departure_delay": 900,
"schedule_relationship": "SCHEDULED"
},
{
"stop_sequence": 3,
"stop_id": "BARCELONA-SANTS",
"arrival_delay": 900,
"departure_delay": null,
"schedule_relationship": "SCHEDULED"
}
]
}
GET /trips/:id/delays
Obtener información resumida de retrasos de un viaje.
Ejemplo:
curl http://localhost:3000/trips/trip_12345/delays
Respuesta:
{
"trip_id": "trip_12345",
"delay_status": "MODERATE_DELAY",
"delay_seconds": 900,
"delay_formatted": "15 min 0 s",
"schedule_relationship": "SCHEDULED",
"received_at": "2025-11-27T10:15:00Z"
}
Estados de Delay:
NO_DATA: Sin información de retrasosON_TIME: Puntual (0 segundos)MINOR_DELAY: Retraso menor (1-5 minutos)MODERATE_DELAY: Retraso moderado (5-15 minutos)MAJOR_DELAY: Retraso mayor (>15 minutos)EARLY: Adelantado
GET /trips/route/:routeId
Obtener todos los viajes de una ruta.
Ejemplo:
curl http://localhost:3000/trips/route/AVE-MAD-BCN
GET /trips/delayed/all
Obtener todos los viajes actualmente retrasados.
Query Parameters:
min_delay(opcional): Retraso mínimo en segundos (default: 0)
Ejemplo:
# Todos los viajes retrasados
curl http://localhost:3000/trips/delayed/all
# Solo retrasos mayores a 5 minutos
curl http://localhost:3000/trips/delayed/all?min_delay=300
Respuesta:
[
{
"trip_id": "trip_12345",
"route_id": "AVE-MAD-BCN",
"trip_headsign": "Barcelona Sants",
"delay_seconds": 900,
"schedule_relationship": "SCHEDULED",
"received_at": "2025-11-27T10:15:00Z"
}
]
🔌 Nuevos WebSocket Events (Planeados)
Servidor → Cliente
// Nueva alerta creada
socket.on('alert:new', (alert) => {
console.log('Nueva alerta:', alert);
});
// Alerta actualizada
socket.on('alert:update', (alert) => {
console.log('Alerta actualizada:', alert);
});
// Retraso detectado
socket.on('trip:delay', (delayInfo) => {
console.log('Retraso en viaje:', delayInfo);
});
// Cancelación de viaje
socket.on('trip:cancelled', (tripInfo) => {
console.log('Viaje cancelado:', tripInfo);
});
🗄️ Estructura de Datos
Tablas GTFS Static
trips
Información de viajes planificados.
CREATE TABLE trips (
trip_id VARCHAR(100) PRIMARY KEY,
route_id VARCHAR(50),
service_id VARCHAR(50),
trip_headsign VARCHAR(200),
trip_short_name VARCHAR(50),
direction_id INTEGER,
block_id VARCHAR(50),
shape_id VARCHAR(100),
wheelchair_accessible INTEGER,
bikes_allowed INTEGER
);
stop_times
Horarios de parada de cada viaje.
CREATE TABLE stop_times (
trip_id VARCHAR(100),
arrival_time TIME,
departure_time TIME,
stop_id VARCHAR(100),
stop_sequence INTEGER,
stop_headsign VARCHAR(200),
pickup_type INTEGER,
drop_off_type INTEGER,
shape_dist_traveled FLOAT,
PRIMARY KEY (trip_id, stop_sequence)
);
calendar
Calendario de servicio (días de operación).
CREATE TABLE calendar (
service_id VARCHAR(50) PRIMARY KEY,
monday BOOLEAN,
tuesday BOOLEAN,
wednesday BOOLEAN,
thursday BOOLEAN,
friday BOOLEAN,
saturday BOOLEAN,
sunday BOOLEAN,
start_date DATE,
end_date DATE
);
shapes
Geometría de las rutas (trayectorias).
CREATE TABLE shapes (
shape_id VARCHAR(100),
shape_pt_lat DOUBLE PRECISION,
shape_pt_lon DOUBLE PRECISION,
shape_pt_sequence INTEGER,
shape_dist_traveled FLOAT,
PRIMARY KEY (shape_id, shape_pt_sequence)
);
Tablas de Actualizaciones en Tiempo Real
trip_updates
Actualizaciones de viajes (retrasos, cancelaciones).
CREATE TABLE trip_updates (
update_id SERIAL PRIMARY KEY,
trip_id VARCHAR(100),
route_id VARCHAR(50),
start_date VARCHAR(10),
schedule_relationship VARCHAR(20),
delay_seconds INTEGER,
received_at TIMESTAMP DEFAULT NOW()
);
stop_time_updates
Actualizaciones de paradas específicas.
CREATE TABLE stop_time_updates (
update_id INTEGER REFERENCES trip_updates(update_id),
stop_sequence INTEGER,
stop_id VARCHAR(100),
arrival_delay INTEGER,
departure_delay INTEGER,
schedule_relationship VARCHAR(20),
PRIMARY KEY (update_id, stop_sequence)
);
🧪 Probar Phase 2
1. Verificar GTFS Static Sync
# Ver logs del syncer
docker-compose logs -f gtfs-static-syncer
# Deberías ver:
# "Starting GTFS Static synchronization..."
# "GTFS data downloaded successfully"
# "Imported X routes, Y trips, Z stops"
# Verificar datos en PostgreSQL
make psql
> SELECT COUNT(*) FROM trips;
> SELECT COUNT(*) FROM stop_times;
> SELECT * FROM active_trips_today LIMIT 5;
2. Verificar Trip Updates
# Ver logs del poller
docker-compose logs -f trip-updates-poller
# Deberías ver:
# "Polling Trip Updates..."
# "Processed X trip updates"
# Verificar en PostgreSQL
make psql
> SELECT * FROM delayed_trips;
> SELECT * FROM trip_updates ORDER BY received_at DESC LIMIT 5;
3. Verificar Service Alerts
# Ver logs del poller
docker-compose logs -f alerts-poller
# Deberías ver:
# "Polling Service Alerts..."
# "Processed X alerts"
# Probar API
curl http://localhost:3000/alerts | jq
curl http://localhost:3000/alerts?severity=HIGH | jq
4. Verificar Trip Delays API
# Obtener viajes retrasados
curl http://localhost:3000/trips/delayed/all | jq
# Obtener delay de un viaje específico
curl http://localhost:3000/trips/trip_12345/delays | jq
# Obtener schedule completo
curl http://localhost:3000/trips/trip_12345 | jq
🐛 Troubleshooting Phase 2
No hay datos de GTFS Static
Causa: El syncer no se ha ejecutado o el ZIP no está disponible.
Solución:
# Ejecutar sync manual
docker-compose exec gtfs-static-syncer node src/worker/gtfs-static-syncer.js
# Verificar logs
docker-compose logs gtfs-static-syncer
# Verificar conectividad
curl -I https://data.renfe.com/dataset/horarios-trenes-largo-recorrido-ave/resource/horarios-trenes-largo-recorrido-ave-gtfs.zip
No se reciben Trip Updates
Causa: Feed GTFS-RT no disponible o URL incorrecta.
Solución:
# Verificar logs
docker-compose logs trip-updates-poller
# Probar feed manualmente
curl https://gtfsrt.renfe.com/trip_updates.pb > /tmp/test.pb
file /tmp/test.pb # Debe ser "data" (Protocol Buffer)
# Verificar variables de entorno
docker-compose exec trip-updates-poller env | grep GTFS
Alerts no aparecen
Causa: No hay alertas activas o el poller no está corriendo.
Solución:
# Verificar worker
docker-compose ps alerts-poller
# Ver logs
docker-compose logs alerts-poller
# Verificar en BD
make psql
> SELECT COUNT(*) FROM alerts WHERE end_time IS NULL OR end_time > NOW();
📊 Monitorización Phase 2
Logs de Workers
# Todos los workers Phase 2
docker-compose logs -f gtfs-static-syncer trip-updates-poller alerts-poller
# Worker específico
docker-compose logs -f trip-updates-poller
Estadísticas en Redis
make redis-cli
# Viajes retrasados
> SMEMBERS trips:delayed
# Alertas activas por ruta
> SMEMBERS alerts:route:AVE-MAD-BCN
# Última sincronización GTFS
> GET gtfs:last_sync
Métricas en PostgreSQL
-- Total de viajes del día
SELECT COUNT(*) FROM active_trips_today;
-- Viajes retrasados
SELECT COUNT(*) FROM delayed_trips;
-- Alertas activas
SELECT alert_type, COUNT(*)
FROM alerts
WHERE end_time IS NULL OR end_time > NOW()
GROUP BY alert_type;
-- Retraso promedio
SELECT AVG(delay_seconds) / 60 as avg_delay_minutes
FROM trip_updates
WHERE received_at > NOW() - INTERVAL '1 hour';
🎯 Próximos Pasos
Funcionalidades pendientes de Phase 2:
- WebSocket events para alertas y delays en tiempo real
- Frontend: Componente de alertas
- Frontend: Monitor de puntualidad
- Frontend: Timeline funcional con reproducción histórica
- Frontend: Panel de incidencias
- Notificaciones push (opcional)
- Exportar reportes de puntualidad (opcional)
📝 Notas Técnicas
Fuentes de Datos Phase 2
- GTFS Static: https://data.renfe.com/dataset/horarios-trenes-largo-recorrido-ave/resource/horarios-trenes-largo-recorrido-ave-gtfs.zip
- Trip Updates: https://gtfsrt.renfe.com/trip_updates.pb
- Service Alerts: https://gtfsrt.renfe.com/service_alerts.pb
Frecuencias
- GTFS Static Sync: Diariamente a las 3 AM (configurable via
SYNC_SCHEDULE) - Trip Updates: Cada 30 segundos
- Service Alerts: Cada 30 segundos
Retención de Datos
- Trip Updates: 7 días (usar función
cleanup_old_trip_updates()) - Alerts: Se mantienen hasta
end_time+ 7 días - GTFS Static: Se sobrescribe en cada sync
📚 Documentación Relacionada
Estado: Fase 2 - Backend Completo, Frontend Pendiente Fecha: 27 noviembre 2025 Próxima Fase: Frontend Phase 2 + Fase 3 (Analytics)