add passenger telemetry

This commit is contained in:
2026-05-21 00:09:11 +03:00
parent 66c84ecdeb
commit e57fea321f
15 changed files with 1411 additions and 6 deletions

View File

@@ -1013,6 +1013,8 @@
## ETA
Текущая ETA-логика в коде остается schedule-based: расписание + `time_offset_minutes` + `historical_delays`. Realtime-позиции, включая `source: "crowdsourced"`, публикуются через realtime API как дополнительный сигнал, но низкоуверенные passenger clusters не ухудшают schedule fallback. TODO для будущего realtime ETA: использовать official telemetry как основной источник, а crowdsourced virtual vehicles только как fallback/secondary signal с учетом `confidence`.
### [POST] /api/v1/eta/calculate
- Описание: считает ETA для одной пары `routeId + stopId`
@@ -1320,7 +1322,8 @@
### [POST] /api/v1/telemetry/ingest
- Описание: принимает GPS/телеметрию, при необходимости автоматически создает транспорт по `vehicleNumber`, обновляет `vehicle_live_state` и пушит событие в realtime-канал маршрута
- Описание: принимает официальную GPS/телеметрию транспорта, при необходимости автоматически создает транспорт по `vehicleNumber`, обновляет `vehicle_live_state` и пушит событие в realtime-канал маршрута
- Важно: этот endpoint предназначен только для водительского приложения, GPS-трекера или официальной транспортной телеметрии. Пассажирское приложение не должно отправлять сюда пользовательские геосигналы.
- Авторизация: `Authorization: Bearer <JWT>` или `X-API-Key: <key>`
- Параметры path: нет
- Query-параметры: нет
@@ -1369,9 +1372,57 @@
}
```
### [POST] /api/v1/passenger-telemetry/ingest
- Описание: принимает обезличенный пассажирский геосигнал только при явном opt-in пользователя. Сигналы сохраняются только во временной in-memory очереди с TTL около 10 минут и используются для агрегированных approximate realtime-позиций.
- Авторизация: `Authorization: Bearer <JWT>` или `X-API-Key: <key>`
- Важно по приватности:
- не принимает и не хранит `user id`, `account id`, телефон, device id или постоянные идентификаторы;
- `session_id` трактуется как временный анонимный идентификатор и в памяти хранится только в хешированном виде;
- сырые пассажирские точки не отдаются через публичный REST API и WebSocket;
- пассажирская точка никогда не показывается напрямую как транспорт;
- realtime может отдать только агрегированную виртуальную позицию `source: "crowdsourced"` / `is_estimated: true`, если достаточно уверенности.
- Rate limit: отдельный лимит поверх общего API rate limit по `session_id`, IP и API key.
- Request Body:
- `session_id: string` — временный анонимный id сессии
- `latitude: number`
- `longitude: number`
- `accuracy_m?: number | null`
- `speed_kmh?: number | null`
- `heading?: number | null`
- `observed_at?: string (ISO datetime)`
- `route_id?: number | string | null` — optional hint, сервер проверяет существование и матчинг к маршруту
- `stop_id?: number | string | null`
- `app_state?: 'foreground' | 'background'`
- Response 202:
- `{ accepted: true }`
- Ошибки:
- `400` — невалидные координаты, `accuracy_m`, `speed_kmh`, `heading`, `observed_at`, `route_id`, `stop_id`, `app_state`
- `404` — `Route not found`, если переданный `route_id` не существует
- `429` — превышен passenger telemetry rate limit
- `500` — `Failed to ingest passenger telemetry`
- Где найдено в коде: `backend/src/routes/api.js` -> `passengerTelemetryController.ingest`, `backend/src/controllers/passengerTelemetryController.js`, `backend/src/services/passengerTelemetryAggregator.js`
Пример запроса:
```json
{
"session_id": "temporary-anonymous-id",
"latitude": 55.485,
"longitude": 28.768,
"accuracy_m": 18.5,
"speed_kmh": 32.0,
"heading": 91.0,
"observed_at": "2026-05-20T12:30:00Z",
"route_id": "12",
"stop_id": "123",
"app_state": "foreground"
}
```
### [GET] /api/v1/realtime/routes/:routeId/vehicles
- Описание: отдает snapshot живых транспортных единиц по маршруту
- Описание: отдает snapshot живых транспортных единиц по маршруту. Ответ может включать official live vehicles и агрегированные approximate crowdsourced virtual vehicles, если official telemetry рядом отсутствует и passenger confidence достаточный.
- Авторизация: `Authorization: Bearer <JWT>` или `X-API-Key: <key>`
- Параметры path: `routeId`
- Query-параметры:
@@ -1380,6 +1431,14 @@
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: `LiveVehicleState[]`
- Backward-compatible optional fields:
- `source?: 'official' | 'crowdsourced'`
- `confidence?: number`
- `confidence_label?: 'LOW' | 'MEDIUM' | 'HIGH'`
- `accuracy_m?: number`
- `is_estimated?: boolean`
- Для crowdsourced транспорта `vehicle_id` имеет synthetic формат `crowd:{routeId}:{clusterId}` и не содержит `session_id`.
- Сырые passenger positions в этом endpoint не публикуются.
- Ошибки:
- `400` — `routeId must be a positive integer`
- `500` — `Failed to fetch route live vehicles`
@@ -1454,8 +1513,27 @@
"latitude": 55.4869,
"longitude": 28.7856,
"speed_kmh": 24,
"source": "official",
"confidence": 1,
"accuracy_m": 5,
"is_estimated": false,
"is_online": true,
"seconds_since_update": 3
},
{
"vehicle_id": "crowd:7:ab12cd34ef56",
"vehicle_number": null,
"route_id": 7,
"latitude": 55.4871,
"longitude": 28.786,
"speed_kmh": 22,
"source": "crowdsourced",
"confidence": 0.63,
"confidence_label": "MEDIUM",
"accuracy_m": 27,
"is_estimated": true,
"is_online": true,
"seconds_since_update": 5
}
]
}

View File

@@ -23,6 +23,7 @@ tags:
- name: Alerts
- name: Users
- name: Telemetry
- name: Passenger Telemetry
- name: Realtime
paths:
/health:
@@ -1694,7 +1695,8 @@ paths:
/api/v1/telemetry/ingest:
post:
tags: [Telemetry]
summary: Ingest GPS telemetry
summary: Ingest official vehicle GPS telemetry
description: For driver apps, GPS trackers, and official telemetry only. Passenger apps must use /api/v1/passenger-telemetry/ingest instead.
security:
- BearerAuth: []
- ApiKeyAuth: []
@@ -1733,10 +1735,58 @@ paths:
$ref: '#/components/responses/TooManyRequests'
'500':
$ref: '#/components/responses/InternalError'
/api/v1/passenger-telemetry/ingest:
post:
tags: [Passenger Telemetry]
summary: Ingest anonymized passenger telemetry
description: |
Accepts opt-in, temporary, anonymous passenger geo-signals for short-lived aggregation.
Raw passenger points are not exposed through public REST APIs or WebSocket messages.
The server may emit only aggregate approximate virtual vehicles with source=crowdsourced.
security:
- BearerAuth: []
- ApiKeyAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PassengerTelemetryIngestRequest'
example:
session_id: temporary-anonymous-id
latitude: 55.485
longitude: 28.768
accuracy_m: 18.5
speed_kmh: 32
heading: 91
observed_at: '2026-05-20T12:30:00Z'
route_id: '12'
stop_id: '123'
app_state: foreground
responses:
'202':
description: Passenger signal accepted for aggregation
content:
application/json:
schema:
$ref: '#/components/schemas/PassengerTelemetryIngestResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/TooManyRequests'
'500':
$ref: '#/components/responses/InternalError'
/api/v1/realtime/routes/{routeId}/vehicles:
get:
tags: [Realtime]
summary: Live vehicles snapshot for route
description: Returns official live vehicles plus aggregate approximate crowdsourced virtual vehicles when confidence is sufficient. Raw passenger points are never returned.
security:
- BearerAuth: []
- ApiKeyAuth: []
@@ -2879,11 +2929,66 @@ components:
sourceRef:
type: string
nullable: true
PassengerTelemetryIngestRequest:
type: object
required: [session_id, latitude, longitude]
additionalProperties: false
properties:
session_id:
type: string
minLength: 1
maxLength: 128
description: Temporary anonymous session id; not a user, account, phone, or device id.
latitude:
type: number
minimum: -90
maximum: 90
longitude:
type: number
minimum: -180
maximum: 180
accuracy_m:
type: number
minimum: 0
maximum: 1000
nullable: true
speed_kmh:
type: number
minimum: 0
maximum: 120
nullable: true
heading:
type: number
minimum: 0
maximum: 360
nullable: true
observed_at:
type: string
format: date-time
nullable: true
route_id:
oneOf:
- type: integer
- type: string
nullable: true
description: Optional client hint; server verifies it before aggregation.
stop_id:
oneOf:
- type: integer
- type: string
nullable: true
app_state:
type: string
enum: [foreground, background]
nullable: true
LiveVehicleState:
type: object
properties:
vehicle_id:
type: integer
oneOf:
- type: integer
- type: string
description: Official vehicles use numeric ids; crowdsourced virtual vehicles use crowd:{routeId}:{clusterId}.
vehicle_number:
type: string
nullable: true
@@ -2917,12 +3022,31 @@ components:
accuracy_meters:
type: number
nullable: true
accuracy_m:
type: number
nullable: true
source_type:
type: string
nullable: true
source_ref:
type: string
nullable: true
source:
type: string
enum: [official, crowdsourced]
nullable: true
confidence:
type: number
minimum: 0
maximum: 1
nullable: true
confidence_label:
type: string
enum: [LOW, MEDIUM, HIGH]
nullable: true
is_estimated:
type: boolean
nullable: true
last_seen_at:
type: string
format: date-time
@@ -2942,3 +3066,10 @@ components:
type: string
vehicle:
$ref: '#/components/schemas/LiveVehicleState'
PassengerTelemetryIngestResponse:
type: object
required: [accepted]
properties:
accepted:
type: boolean
example: true