openapi: 3.0.3 info: title: Polotsk Transit API version: 1.0.0-draft description: | Черновик OpenAPI 3.0, восстановленный по реальному коду проекта. В проекте нет встроенного swagger/openapi генератора, поэтому часть схем описана по фактическим SQL-запросам и runtime-поведению контроллеров. servers: - url: http://localhost:3000 description: Local backend tags: - name: System - name: Auth - name: Upload - name: Routes - name: Route Stops - name: Schedules - name: Holidays - name: Stops - name: ETA - name: Sync - name: Alerts - name: Users - name: Telemetry - name: Realtime paths: /health: get: tags: [System] summary: Health check security: [] responses: '200': description: Service status content: application/json: schema: type: object required: [status, timestamp, version] properties: status: type: string example: healthy timestamp: type: string format: date-time version: type: string example: 1.0.0 /api/v1/auth/login: post: tags: [Auth] summary: Login security: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/LoginRequest' responses: '200': description: Authenticated content: application/json: schema: $ref: '#/components/schemas/LoginResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' /api/v1/auth/refresh: post: tags: [Auth] summary: Refresh access token security: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RefreshRequest' responses: '200': description: New access token content: application/json: schema: $ref: '#/components/schemas/RefreshResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' /api/v1/auth/logout: post: tags: [Auth] summary: Logout by refresh token security: [] requestBody: required: false content: application/json: schema: $ref: '#/components/schemas/LogoutRequest' responses: '200': description: Logged out content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' /api/v1/auth/me: get: tags: [Auth] summary: Current user security: - BearerAuth: [] responses: '200': description: Current authenticated user content: application/json: schema: $ref: '#/components/schemas/UserMe' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' /api/v1/auth/change-password: post: tags: [Auth] summary: Change password security: - BearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ChangePasswordRequest' responses: '200': description: Password changed content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' /api/v1/upload/avatar: post: tags: [Upload] summary: Upload or replace current user avatar security: - BearerAuth: [] requestBody: required: true content: multipart/form-data: schema: type: object required: [avatar] properties: avatar: type: string format: binary responses: '200': description: Avatar uploaded content: application/json: schema: $ref: '#/components/schemas/AvatarUploadResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' delete: tags: [Upload] summary: Delete current user avatar security: - BearerAuth: [] responses: '200': description: Avatar deleted content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '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/upload/avatar/{userId}: delete: tags: [Upload] summary: Admin deletes any user avatar security: - BearerAuth: [] parameters: - $ref: '#/components/parameters/UserId' responses: '200': description: Avatar deleted content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '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/routes: get: tags: [Routes] summary: List active routes security: - BearerAuth: [] - ApiKeyAuth: [] responses: '200': description: Route list content: application/json: schema: type: array items: $ref: '#/components/schemas/RouteSummary' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' post: tags: [Routes] summary: Create route security: - BearerAuth: [] - ApiKeyAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateRouteRequest' responses: '201': description: Route created content: application/json: schema: $ref: '#/components/schemas/Route' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' /api/v1/routes/{id}: get: tags: [Routes] summary: Get route detail with stops security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - $ref: '#/components/parameters/Id' responses: '200': description: Route detail content: application/json: schema: $ref: '#/components/schemas/RouteDetail' '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' put: tags: [Routes] summary: Update route security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - $ref: '#/components/parameters/Id' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateRouteRequest' responses: '200': description: Updated route content: application/json: schema: $ref: '#/components/schemas/Route' '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' delete: tags: [Routes] summary: Delete route security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - $ref: '#/components/parameters/Id' responses: '200': description: Route deleted content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '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/routes/{routeId}/stops: get: tags: [Route Stops] summary: Get ordered stops for route security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - $ref: '#/components/parameters/RouteId' responses: '200': description: Route stop list content: application/json: schema: type: array items: $ref: '#/components/schemas/RouteStopView' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' post: tags: [Route Stops] summary: Add stop to route security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - $ref: '#/components/parameters/RouteId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AddRouteStopRequest' responses: '201': description: Route stop created content: application/json: schema: $ref: '#/components/schemas/RouteStopRecord' '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/routes/{routeId}/available-stops: get: tags: [Route Stops] summary: Stops not yet assigned to route security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - $ref: '#/components/parameters/RouteId' responses: '200': description: Available stops content: application/json: schema: type: array items: $ref: '#/components/schemas/AvailableStop' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' /api/v1/routes/{routeId}/stops/{routeStopId}: put: tags: [Route Stops] summary: Update route stop position or offset security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - $ref: '#/components/parameters/RouteId' - $ref: '#/components/parameters/RouteStopId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateRouteStopRequest' responses: '200': description: Updated route stop content: application/json: schema: $ref: '#/components/schemas/RouteStopRecord' '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' delete: tags: [Route Stops] summary: Remove stop from route security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - $ref: '#/components/parameters/RouteId' - $ref: '#/components/parameters/RouteStopId' responses: '200': description: Route stop deleted content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '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/routes/{routeId}/stops-reorder: put: tags: [Route Stops] summary: Bulk reorder route stops security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - $ref: '#/components/parameters/RouteId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ReorderRouteStopsRequest' responses: '200': description: Route stops reordered content: application/json: schema: type: object required: [message, count] properties: message: type: string count: type: integer '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' /api/v1/routes/{routeId}/schedules: get: tags: [Schedules] summary: List route schedules security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - $ref: '#/components/parameters/RouteId' responses: '200': description: Schedules for route content: application/json: schema: type: array items: $ref: '#/components/schemas/ScheduleWithRoute' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' /api/v1/routes/{routeId}/schedule/today: get: tags: [Schedules] summary: Get active schedule for date security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - $ref: '#/components/parameters/RouteId' - in: query name: date schema: type: string format: date required: false responses: '200': description: Schedule for date content: application/json: schema: $ref: '#/components/schemas/TodayScheduleResponse' '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/routes/{routeId}/schedules/copy: post: tags: [Schedules] summary: Copy schedule from one day type to another security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - $ref: '#/components/parameters/RouteId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CopyScheduleRequest' responses: '200': description: Existing schedule updated content: application/json: schema: $ref: '#/components/schemas/Schedule' '201': description: New schedule created content: application/json: schema: $ref: '#/components/schemas/Schedule' '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/schedules: post: tags: [Schedules] summary: Create schedule security: - BearerAuth: [] - ApiKeyAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateScheduleRequest' responses: '201': description: Schedule created content: application/json: schema: $ref: '#/components/schemas/Schedule' '400': $ref: '#/components/responses/BadRequest' '409': description: Schedule already exists content: application/json: schema: type: object properties: error: type: string message: type: string existingId: type: integer '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' /api/v1/schedules/{id}: get: tags: [Schedules] summary: Get schedule by id security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - $ref: '#/components/parameters/Id' responses: '200': description: Schedule detail content: application/json: schema: $ref: '#/components/schemas/ScheduleWithRoute' '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' put: tags: [Schedules] summary: Update schedule security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - $ref: '#/components/parameters/Id' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateScheduleRequest' responses: '200': description: Schedule updated content: application/json: schema: $ref: '#/components/schemas/Schedule' '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' delete: tags: [Schedules] summary: Delete schedule security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - $ref: '#/components/parameters/Id' responses: '200': description: Schedule deleted content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '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/holidays: get: tags: [Holidays] summary: List holidays security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - in: query name: year schema: type: integer required: false responses: '200': description: Holiday list content: application/json: schema: type: array items: $ref: '#/components/schemas/Holiday' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' post: tags: [Holidays] summary: Create holiday security: - BearerAuth: [] - ApiKeyAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateHolidayRequest' responses: '201': description: Holiday created content: application/json: schema: $ref: '#/components/schemas/Holiday' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' /api/v1/holidays/upcoming: get: tags: [Holidays] summary: Get upcoming holidays security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - in: query name: limit schema: type: integer required: false responses: '200': description: Upcoming holidays content: application/json: schema: type: array items: $ref: '#/components/schemas/UpcomingHoliday' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' /api/v1/holidays/year/{year}: get: tags: [Holidays] summary: Get holidays for year with resolved recurring dates security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - in: path name: year required: true schema: type: integer responses: '200': description: Holidays for year content: application/json: schema: type: array items: $ref: '#/components/schemas/HolidayForYear' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' /api/v1/holidays/check/{date}: get: tags: [Holidays] summary: Check if date is holiday security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - in: path name: date required: true schema: type: string format: date responses: '200': description: Holiday check content: application/json: schema: type: object required: [date, isHoliday, holidayName] properties: date: type: string format: date isHoliday: type: boolean holidayName: type: string nullable: true '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' /api/v1/holidays/{id}: put: tags: [Holidays] summary: Update holiday security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - $ref: '#/components/parameters/Id' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateHolidayRequest' responses: '200': description: Holiday updated content: application/json: schema: $ref: '#/components/schemas/Holiday' '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' delete: tags: [Holidays] summary: Delete holiday security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - $ref: '#/components/parameters/Id' responses: '200': description: Holiday deleted content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '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/stops: get: tags: [Stops] summary: List active stops security: - BearerAuth: [] - ApiKeyAuth: [] responses: '200': description: Stop list content: application/json: schema: type: array items: $ref: '#/components/schemas/StopSummary' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' post: tags: [Stops] summary: Create stop security: - BearerAuth: [] - ApiKeyAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateStopRequest' responses: '201': description: Stop created content: application/json: schema: $ref: '#/components/schemas/Stop' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' /api/v1/stops/nearby: get: tags: [Stops] summary: Find nearby stops security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - in: query name: lat required: true schema: type: number - in: query name: lon required: true schema: type: number - in: query name: radius required: false schema: type: number default: 500 responses: '200': description: Nearby stops content: application/json: schema: type: array items: allOf: - $ref: '#/components/schemas/Stop' - type: object properties: distance_meters: type: number '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' /api/v1/stops/{id}: get: tags: [Stops] summary: Get stop detail security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - $ref: '#/components/parameters/Id' responses: '200': description: Stop detail content: application/json: schema: $ref: '#/components/schemas/StopDetail' '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' put: tags: [Stops] summary: Update stop security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - $ref: '#/components/parameters/Id' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateStopRequest' responses: '200': description: Stop updated content: application/json: schema: $ref: '#/components/schemas/Stop' '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' delete: tags: [Stops] summary: Delete stop security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - $ref: '#/components/parameters/Id' responses: '200': description: Stop deleted content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '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/eta/calculate: post: tags: [ETA] summary: Calculate ETA for route+stop security: - BearerAuth: [] - ApiKeyAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CalculateEtaRequest' responses: '200': description: ETA calculated content: application/json: schema: oneOf: - $ref: '#/components/schemas/CalculateEtaResponse' - type: object properties: message: type: string nextDay: type: string '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/eta/stop/{stopId}: get: tags: [ETA] summary: ETA for all routes at stop security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - in: path name: stopId required: true schema: type: integer responses: '200': description: ETA list content: application/json: schema: $ref: '#/components/schemas/StopEtasResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' /api/v1/sync: get: tags: [Sync] summary: Full or incremental sync security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - in: query name: lastSync required: false schema: type: string format: date-time responses: '200': description: Sync payload content: application/json: schema: oneOf: - $ref: '#/components/schemas/FullSyncResponse' - $ref: '#/components/schemas/IncrementalSyncResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' /api/v1/sync/status: get: tags: [Sync] summary: Sync status security: - BearerAuth: [] - ApiKeyAuth: [] responses: '200': description: Status payload content: application/json: schema: $ref: '#/components/schemas/SyncStatusResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' /api/v1/alerts: get: tags: [Alerts] summary: List alerts security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - in: query name: route_id required: false schema: type: integer - in: query name: is_active required: false schema: type: boolean - in: query name: alert_type required: false schema: $ref: '#/components/schemas/AlertType' responses: '200': description: Alert list content: application/json: schema: type: array items: $ref: '#/components/schemas/AlertRead' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' post: tags: [Alerts] summary: Create alert security: - BearerAuth: [] - ApiKeyAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateAlertRequest' responses: '201': description: Alert created content: application/json: schema: $ref: '#/components/schemas/Alert' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' /api/v1/alerts/active: get: tags: [Alerts] summary: List currently active alerts security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - in: query name: route_id required: false schema: type: integer responses: '200': description: Active alerts content: application/json: schema: type: array items: $ref: '#/components/schemas/AlertRead' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' /api/v1/alerts/cleanup: post: tags: [Alerts] summary: Deactivate expired alerts security: - BearerAuth: [] - ApiKeyAuth: [] responses: '200': description: Cleanup stats content: application/json: schema: type: object required: [message, deactivated_count] properties: message: type: string deactivated_count: type: integer '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' /api/v1/alerts/{id}: get: tags: [Alerts] summary: Get alert by id security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - $ref: '#/components/parameters/Id' responses: '200': description: Alert detail content: application/json: schema: $ref: '#/components/schemas/AlertRead' '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' put: tags: [Alerts] summary: Update alert security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - $ref: '#/components/parameters/Id' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateAlertRequest' responses: '200': description: Alert updated content: application/json: schema: $ref: '#/components/schemas/Alert' '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' delete: tags: [Alerts] summary: Delete alert security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - $ref: '#/components/parameters/Id' responses: '200': description: Alert deleted content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '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/users: get: tags: [Users] summary: List users security: - BearerAuth: [] responses: '200': description: User list content: application/json: schema: type: array items: $ref: '#/components/schemas/UserAdminView' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' post: tags: [Users] summary: Create user security: - BearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateUserRequest' responses: '201': description: User created content: application/json: schema: $ref: '#/components/schemas/UserCreated' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' /api/v1/users/{id}: get: tags: [Users] summary: Get user by id security: - BearerAuth: [] parameters: - $ref: '#/components/parameters/Id' responses: '200': description: User detail content: application/json: schema: $ref: '#/components/schemas/UserAdminView' '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' put: tags: [Users] summary: Update user security: - BearerAuth: [] parameters: - $ref: '#/components/parameters/Id' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateUserRequest' responses: '200': description: User updated content: application/json: schema: $ref: '#/components/schemas/UserUpdated' '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' delete: tags: [Users] summary: Delete user security: - BearerAuth: [] parameters: - $ref: '#/components/parameters/Id' responses: '200': description: User deleted content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '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/users/{id}/reset-password: post: tags: [Users] summary: Reset user password security: - BearerAuth: [] parameters: - $ref: '#/components/parameters/Id' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ResetPasswordRequest' responses: '200': description: Password reset content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '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/telemetry/ingest: post: tags: [Telemetry] summary: Ingest GPS telemetry security: - BearerAuth: [] - ApiKeyAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TelemetryIngestRequest' example: routeId: 7 vehicleNumber: DEMO-7A vehicleType: bus lat: 55.4869 lon: 28.7856 speedKmh: 24 heading: 90 accuracyMeters: 5 recordedAt: '2026-04-16T10:30:00.000Z' responses: '201': description: Telemetry accepted content: application/json: schema: $ref: '#/components/schemas/TelemetryIngestResponse' '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 security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - in: path name: routeId required: true schema: type: integer - in: query name: includeStale schema: type: boolean - in: query name: staleAfterSeconds schema: type: integer default: 180 responses: '200': description: Route live vehicles content: application/json: schema: type: array items: $ref: '#/components/schemas/LiveVehicleState' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalError' /api/v1/realtime/vehicles/{vehicleId}: get: tags: [Realtime] summary: Live vehicle state security: - BearerAuth: [] - ApiKeyAuth: [] parameters: - in: path name: vehicleId required: true schema: type: integer - in: query name: staleAfterSeconds schema: type: integer default: 180 responses: '200': description: Vehicle live state content: application/json: schema: $ref: '#/components/schemas/LiveVehicleState' '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' components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key BearerAuth: type: http scheme: bearer bearerFormat: JWT parameters: Id: in: path name: id required: true schema: type: integer RouteId: in: path name: routeId required: true schema: type: integer RouteStopId: in: path name: routeStopId required: true schema: type: integer UserId: in: path name: userId required: true schema: type: integer responses: BadRequest: description: Bad request content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' Unauthorized: description: Authentication required or invalid credentials content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' Forbidden: description: Forbidden content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' NotFound: description: Not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' TooManyRequests: description: Too many requests content: application/json: schema: type: object properties: error: type: string message: type: string retryAfter: type: integer InternalError: description: Internal server error content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' schemas: ErrorResponse: type: object properties: error: type: string message: type: string MessageResponse: type: object required: [message] properties: message: type: string RouteType: type: string enum: [bus, minibus, trolleybus, tram] ScheduleDayType: type: string enum: [weekday, saturday, sunday, holiday] AlertType: type: string enum: [delay, cancellation, detour, info] UserRole: type: string enum: [admin, user] Route: type: object properties: id: type: integer route_number: type: string name: type: string type: $ref: '#/components/schemas/RouteType' color: type: string is_active: type: boolean description: type: string nullable: true created_at: type: string format: date-time updated_at: type: string format: date-time RouteSummary: allOf: - $ref: '#/components/schemas/Route' - type: object properties: stops_count: oneOf: - type: integer - type: string Stop: type: object properties: id: type: integer name: type: string location: description: Raw PostGIS geography serialization; exact shape is not guaranteed nullable: true latitude: type: number longitude: type: number address: type: string nullable: true description: type: string nullable: true is_active: type: boolean created_at: type: string format: date-time updated_at: type: string format: date-time StopSummary: allOf: - $ref: '#/components/schemas/Stop' - type: object properties: routes_count: oneOf: - type: integer - type: string StopOnRoute: allOf: - $ref: '#/components/schemas/Stop' - type: object properties: sequence: type: integer time_offset_minutes: type: integer RouteDetail: allOf: - $ref: '#/components/schemas/Route' - type: object properties: stops: type: array items: $ref: '#/components/schemas/StopOnRoute' RouteAtStop: allOf: - $ref: '#/components/schemas/Route' - type: object properties: sequence: type: integer time_offset_minutes: type: integer StopDetail: allOf: - $ref: '#/components/schemas/Stop' - type: object properties: routes: type: array items: $ref: '#/components/schemas/RouteAtStop' RouteStopRecord: type: object properties: id: type: integer route_id: type: integer stop_id: type: integer sequence: type: integer time_offset_minutes: type: integer RouteStopView: type: object properties: route_stop_id: type: integer sequence: type: integer time_offset_minutes: type: integer stop_id: type: integer name: type: string address: type: string nullable: true latitude: type: number longitude: type: number AvailableStop: type: object properties: id: type: integer name: type: string address: type: string nullable: true latitude: type: number longitude: type: number Schedule: type: object properties: id: type: integer route_id: type: integer day_type: $ref: '#/components/schemas/ScheduleDayType' departure_times: type: array items: type: string valid_from: type: string format: date nullable: true valid_until: type: string format: date nullable: true created_at: type: string format: date-time updated_at: type: string format: date-time ScheduleWithRoute: allOf: - $ref: '#/components/schemas/Schedule' - type: object properties: route_number: type: string route_name: type: string TodayScheduleResponse: type: object properties: date: type: string format: date scheduleType: $ref: '#/components/schemas/ScheduleDayType' isHoliday: type: boolean holidayName: type: string nullable: true schedule: $ref: '#/components/schemas/ScheduleWithRoute' Holiday: type: object properties: id: type: integer date: type: string format: date name: type: string is_recurring: type: boolean recurring_month: type: integer nullable: true recurring_day: type: integer nullable: true created_at: type: string format: date-time HolidayForYear: type: object properties: id: type: integer date: type: string format: date name: type: string is_recurring: type: boolean UpcomingHoliday: type: object properties: id: type: integer name: type: string next_date: type: string format: date is_recurring: type: boolean Alert: type: object properties: id: type: integer route_id: type: integer nullable: true alert_type: $ref: '#/components/schemas/AlertType' title: type: string message: type: string start_time: type: string format: date-time end_time: type: string format: date-time nullable: true is_active: type: boolean created_at: type: string format: date-time AlertRead: allOf: - $ref: '#/components/schemas/Alert' - type: object properties: route_name: type: string nullable: true route_number: type: string nullable: true color: type: string nullable: true UserLoginPayload: type: object properties: id: type: integer username: type: string full_name: type: string nullable: true email: type: string nullable: true role: $ref: '#/components/schemas/UserRole' is_active: type: boolean last_login: type: string format: date-time nullable: true created_at: type: string format: date-time updated_at: type: string format: date-time avatar_url: type: string nullable: true UserMe: type: object properties: id: type: integer username: type: string full_name: type: string nullable: true email: type: string nullable: true role: $ref: '#/components/schemas/UserRole' avatar_url: type: string nullable: true last_login: type: string format: date-time nullable: true created_at: type: string format: date-time UserAdminView: type: object properties: id: type: integer username: type: string full_name: type: string nullable: true email: type: string nullable: true role: $ref: '#/components/schemas/UserRole' is_active: type: boolean last_login: type: string format: date-time nullable: true created_at: type: string format: date-time UserCreated: type: object properties: id: type: integer username: type: string full_name: type: string nullable: true email: type: string nullable: true role: $ref: '#/components/schemas/UserRole' is_active: type: boolean created_at: type: string format: date-time UserUpdated: type: object properties: id: type: integer username: type: string full_name: type: string nullable: true email: type: string nullable: true role: $ref: '#/components/schemas/UserRole' is_active: type: boolean Arrival: type: object properties: scheduledTime: type: string estimatedTime: type: string minutesUntil: type: integer delay: type: integer CalculateEtaResponse: type: object properties: routeId: type: integer stopId: type: integer currentTime: type: string dayType: $ref: '#/components/schemas/ScheduleDayType' arrivals: type: array items: $ref: '#/components/schemas/Arrival' StopEtasRoute: type: object properties: id: type: integer route_number: type: string name: type: string type: $ref: '#/components/schemas/RouteType' color: type: string arrivals: type: array items: $ref: '#/components/schemas/Arrival' error: type: string nullable: true StopEtasResponse: type: object properties: stopId: oneOf: - type: integer - type: string routes: type: array items: $ref: '#/components/schemas/StopEtasRoute' FullSyncResponse: type: object properties: syncType: type: string enum: [full] timestamp: type: string format: date-time version: type: string data: type: object properties: routes: type: array items: $ref: '#/components/schemas/Route' stops: type: array items: $ref: '#/components/schemas/Stop' routeStops: type: array items: $ref: '#/components/schemas/RouteStopRecord' schedules: type: array items: $ref: '#/components/schemas/Schedule' alerts: type: array items: $ref: '#/components/schemas/Alert' holidays: type: array items: $ref: '#/components/schemas/Holiday' metadata: type: object properties: routesCount: type: integer stopsCount: type: integer schedulesCount: type: integer holidaysCount: type: integer IncrementalSyncResponse: type: object properties: syncType: type: string enum: [incremental] timestamp: type: string format: date-time lastSync: type: string format: date-time changes: type: object properties: routes: $ref: '#/components/schemas/SyncChangeBucketRoutes' stops: $ref: '#/components/schemas/SyncChangeBucketStops' routeStops: $ref: '#/components/schemas/SyncChangeBucketRouteStops' schedules: $ref: '#/components/schemas/SyncChangeBucketSchedules' alerts: type: array items: $ref: '#/components/schemas/Alert' metadata: type: object properties: changesCount: type: integer SyncChangeBucketRoutes: type: object properties: updated: type: array items: $ref: '#/components/schemas/Route' deleted: type: array items: type: integer SyncChangeBucketStops: type: object properties: updated: type: array items: $ref: '#/components/schemas/Stop' deleted: type: array items: type: integer SyncChangeBucketRouteStops: type: object properties: updated: type: array items: $ref: '#/components/schemas/RouteStopRecord' deleted: type: array items: type: integer SyncChangeBucketSchedules: type: object properties: updated: type: array items: $ref: '#/components/schemas/Schedule' deleted: type: array items: type: integer SyncStatusResponse: type: object properties: status: type: string timestamp: type: string format: date-time database: type: object properties: routes_count: oneOf: - type: integer - type: string stops_count: oneOf: - type: integer - type: string schedules_count: oneOf: - type: integer - type: string last_change: type: string format: date-time nullable: true LoginRequest: type: object required: [username, password] properties: username: type: string password: type: string LoginResponse: type: object properties: user: $ref: '#/components/schemas/UserLoginPayload' accessToken: type: string refreshToken: type: string RefreshRequest: type: object required: [refreshToken] properties: refreshToken: type: string RefreshResponse: type: object required: [accessToken] properties: accessToken: type: string LogoutRequest: type: object properties: refreshToken: type: string ChangePasswordRequest: type: object required: [currentPassword, newPassword] properties: currentPassword: type: string newPassword: type: string minLength: 6 refreshToken: type: string CreateRouteRequest: type: object required: [route_number, name, type] properties: route_number: type: string name: type: string type: $ref: '#/components/schemas/RouteType' color: type: string default: '#0066CC' description: type: string UpdateRouteRequest: type: object properties: route_number: type: string name: type: string type: $ref: '#/components/schemas/RouteType' color: type: string description: type: string is_active: type: boolean CreateStopRequest: type: object required: [name, latitude, longitude] properties: name: type: string latitude: type: number longitude: type: number address: type: string description: type: string UpdateStopRequest: type: object properties: name: type: string latitude: type: number longitude: type: number address: type: string description: type: string is_active: type: boolean AddRouteStopRequest: type: object required: [stopId] properties: stopId: type: integer sequence: type: integer timeOffsetMinutes: type: integer default: 0 UpdateRouteStopRequest: type: object properties: sequence: type: integer timeOffsetMinutes: type: integer ReorderRouteStopsRequest: type: object required: [stops] properties: stops: type: array items: type: object required: [stopId, sequence] properties: stopId: type: integer sequence: type: integer timeOffsetMinutes: type: integer CreateScheduleRequest: type: object required: [routeId, dayType, departureTimes] properties: routeId: type: integer dayType: $ref: '#/components/schemas/ScheduleDayType' departureTimes: type: array items: type: string validFrom: type: string format: date nullable: true validUntil: type: string format: date nullable: true UpdateScheduleRequest: type: object properties: departureTimes: type: array items: type: string validFrom: type: string format: date nullable: true validUntil: type: string format: date nullable: true CopyScheduleRequest: type: object required: [fromDayType, toDayType] properties: fromDayType: $ref: '#/components/schemas/ScheduleDayType' toDayType: $ref: '#/components/schemas/ScheduleDayType' CreateHolidayRequest: type: object required: [date, name] properties: date: type: string format: date name: type: string isRecurring: type: boolean UpdateHolidayRequest: type: object properties: date: type: string format: date name: type: string isRecurring: type: boolean CreateAlertRequest: type: object required: [alert_type, title, message, start_time] properties: route_id: type: integer nullable: true alert_type: $ref: '#/components/schemas/AlertType' title: type: string message: type: string start_time: type: string format: date-time end_time: type: string format: date-time nullable: true is_active: type: boolean UpdateAlertRequest: type: object properties: route_id: type: integer nullable: true alert_type: $ref: '#/components/schemas/AlertType' title: type: string message: type: string start_time: type: string format: date-time end_time: type: string format: date-time nullable: true is_active: type: boolean CreateUserRequest: type: object required: [username, password] properties: username: type: string password: type: string minLength: 6 full_name: type: string email: type: string format: email role: $ref: '#/components/schemas/UserRole' UpdateUserRequest: type: object properties: full_name: type: string email: type: string format: email role: $ref: '#/components/schemas/UserRole' is_active: type: boolean ResetPasswordRequest: type: object required: [newPassword] properties: newPassword: type: string minLength: 6 AvatarUploadResponse: type: object properties: message: type: string avatar_url: type: string user: type: object properties: id: type: integer username: type: string avatar_url: type: string nullable: true CalculateEtaRequest: type: object required: [stopId, routeId] properties: stopId: type: integer routeId: type: integer TelemetryIngestRequest: type: object required: [lat, lon] properties: vehicleId: type: integer nullable: true vehicleNumber: type: string nullable: true routeId: type: integer nullable: true vehicleType: $ref: '#/components/schemas/RouteType' lat: type: number minimum: -90 maximum: 90 lon: type: number minimum: -180 maximum: 180 speedKmh: type: number nullable: true heading: type: number minimum: 0 maximum: 360 nullable: true accuracyMeters: type: number nullable: true recordedAt: type: string format: date-time nullable: true sourceType: type: string nullable: true sourceRef: type: string nullable: true LiveVehicleState: type: object properties: vehicle_id: type: integer vehicle_number: type: string nullable: true registration: type: string nullable: true vehicle_type: $ref: '#/components/schemas/RouteType' capacity: type: integer nullable: true route_id: type: integer route_number: type: string route_name: type: string route_color: type: string nullable: true latitude: type: number longitude: type: number speed_kmh: type: number nullable: true heading: type: number nullable: true accuracy_meters: type: number nullable: true source_type: type: string nullable: true source_ref: type: string nullable: true last_seen_at: type: string format: date-time updated_at: type: string format: date-time is_online: type: boolean seconds_since_update: type: integer is_moving: type: boolean TelemetryIngestResponse: type: object properties: message: type: string vehicle: $ref: '#/components/schemas/LiveVehicleState'