From a539bb6fdf2115c5f1ae8482469234209c7fc379 Mon Sep 17 00:00:00 2001 From: hamz2a Date: Tue, 31 Dec 2024 16:08:58 +0100 Subject: [PATCH] editoast: endpoints to retrieve stdcm payloads Signed-off-by: hamz2a --- editoast/editoast_common/src/tracing.rs | 3 +- editoast/openapi.yaml | 61 ++++++ editoast/src/main.rs | 7 +- editoast/src/models/stdcm_log.rs | 33 ++- editoast/src/views/mod.rs | 2 + editoast/src/views/projects.rs | 2 +- editoast/src/views/stdcm_logs.rs | 206 +++++++++++++++++++ editoast/src/views/test_app.rs | 14 +- editoast/src/views/timetable/stdcm.rs | 30 ++- front/public/locales/en/errors.json | 3 + front/public/locales/fr/errors.json | 3 + front/src/common/api/generatedEditoastApi.ts | 146 ++++++++++--- 12 files changed, 452 insertions(+), 58 deletions(-) create mode 100644 editoast/src/views/stdcm_logs.rs diff --git a/editoast/editoast_common/src/tracing.rs b/editoast/editoast_common/src/tracing.rs index 0054b8ab576..0ff3e4d960b 100644 --- a/editoast/editoast_common/src/tracing.rs +++ b/editoast/editoast_common/src/tracing.rs @@ -31,11 +31,12 @@ pub struct TracingConfig { pub fn create_tracing_subscriber( tracing_config: TracingConfig, + log_level: tracing_subscriber::filter::LevelFilter, exporter: T, ) -> impl tracing::Subscriber { let env_filter_layer = tracing_subscriber::EnvFilter::builder() // Set the default log level to 'info' - .with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into()) + .with_default_directive(log_level.into()) .from_env_lossy(); let fmt_layer = tracing_subscriber::fmt::layer() .pretty() diff --git a/editoast/openapi.yaml b/editoast/openapi.yaml index df21a9065db..aef8b6e4216 100644 --- a/editoast/openapi.yaml +++ b/editoast/openapi.yaml @@ -2590,6 +2590,42 @@ paths: application/json: schema: $ref: '#/components/schemas/StdcmSearchEnvironment' + /stdcm_logs: + get: + tags: + - stdcm_log + responses: + '200': + description: The list of Stdcm Logs + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/StdcmLog' + /stdcm_logs/{trace_id}: + get: + tags: + - stdcm_log + parameters: + - name: trace_id + in: path + required: true + schema: + type: string + responses: + '200': + description: The stdcm log + content: + application/json: + schema: + $ref: '#/components/schemas/StdcmLog' + '404': + description: The stdcm log was not found + content: + application/json: + schema: + $ref: '#/components/schemas/InternalError' /temporary_speed_limit_group: post: tags: @@ -4741,6 +4777,7 @@ components: - $ref: '#/components/schemas/EditoastStdcmErrorRollingStockNotFound' - $ref: '#/components/schemas/EditoastStdcmErrorTimetableNotFound' - $ref: '#/components/schemas/EditoastStdcmErrorTowedRollingStockNotFound' + - $ref: '#/components/schemas/EditoastStdcmLogErrorNotFound' - $ref: '#/components/schemas/EditoastStudyErrorNotFound' - $ref: '#/components/schemas/EditoastStudyErrorStartDateAfterEndDate' - $ref: '#/components/schemas/EditoastTemporarySpeedLimitErrorNameAlreadyUsed' @@ -5960,6 +5997,30 @@ components: type: string enum: - editoast:stdcm_v2:TowedRollingStockNotFound + EditoastStdcmLogErrorNotFound: + type: object + required: + - type + - status + - message + properties: + context: + type: object + required: + - trace_id + properties: + trace_id: + type: string + message: + type: string + status: + type: integer + enum: + - 404 + type: + type: string + enum: + - editoast:stdcm_log:NotFound EditoastStudyErrorNotFound: type: object required: diff --git a/editoast/src/main.rs b/editoast/src/main.rs index 754c6999d67..022bc5c88c4 100644 --- a/editoast/src/main.rs +++ b/editoast/src/main.rs @@ -115,7 +115,12 @@ async fn run() -> Result<(), Box> { stream: EditoastMode::from_client(&client).into(), telemetry, }; - create_tracing_subscriber(tracing_config, exporter).init(); + create_tracing_subscriber( + tracing_config, + tracing_subscriber::filter::LevelFilter::INFO, + exporter, + ) + .init(); let pg_config = client.postgres_config; let db_pool = diff --git a/editoast/src/models/stdcm_log.rs b/editoast/src/models/stdcm_log.rs index ae77062bd6c..04e8e48d97f 100644 --- a/editoast/src/models/stdcm_log.rs +++ b/editoast/src/models/stdcm_log.rs @@ -1,11 +1,15 @@ +use std::ops::DerefMut; + use chrono::DateTime; use chrono::Utc; +use diesel::ExpressionMethods; +use diesel::OptionalExtension; +use diesel::QueryDsl; +use diesel_async::RunQueryDsl; use editoast_derive::Model; use editoast_models::DbConnection; -use opentelemetry::trace::TraceContextExt; use serde::Deserialize; use serde::Serialize; -use tracing_opentelemetry::OpenTelemetrySpanExt; use utoipa::ToSchema; use crate::core::stdcm::Request; @@ -18,7 +22,7 @@ editoast_common::schemas! { #[derive(Clone, Debug, Serialize, Deserialize, Model, ToSchema)] #[model(table = editoast_models::tables::stdcm_logs)] -#[model(gen(ops = c))] +#[model(gen(ops = cd, list))] pub struct StdcmLog { pub id: i64, pub trace_id: String, @@ -33,17 +37,13 @@ pub struct StdcmLog { impl StdcmLog { pub async fn log( mut conn: DbConnection, + trace_id: String, request: Request, response: Response, user_id: Option, ) { - let trace_id = tracing::Span::current() - .context() - .span() - .span_context() - .trace_id(); let stdcm_log_changeset = StdcmLog::changeset() - .trace_id(trace_id.to_string()) + .trace_id(trace_id) .request(request) .response(response.clone()) .user_id(user_id); @@ -51,4 +51,19 @@ impl StdcmLog { tracing::error!("Failed during log operation: {e}"); } } + + pub async fn find_by_trace_id( + conn: &mut DbConnection, + trace_id: &str, + ) -> crate::error::Result> { + use editoast_models::tables::stdcm_logs::dsl; + + dsl::stdcm_logs + .filter(dsl::trace_id.eq(trace_id)) + .first::(conn.write().await.deref_mut()) + .await + .map(Into::into) + .optional() + .map_err(Into::into) + } } diff --git a/editoast/src/views/mod.rs b/editoast/src/views/mod.rs index e82231dd5bb..1441072c30f 100644 --- a/editoast/src/views/mod.rs +++ b/editoast/src/views/mod.rs @@ -14,6 +14,7 @@ pub mod scenario; pub mod search; pub mod speed_limit_tags; pub mod sprites; +pub mod stdcm_logs; pub mod stdcm_search_environment; pub mod study; pub mod temporary_speed_limits; @@ -111,6 +112,7 @@ crate::routes! { &train_schedule, &timetable, &path, + &stdcm_logs, &scenario, } diff --git a/editoast/src/views/projects.rs b/editoast/src/views/projects.rs index 9bda30c0ba3..1e16fceb593 100644 --- a/editoast/src/views/projects.rs +++ b/editoast/src/views/projects.rs @@ -415,7 +415,7 @@ pub mod tests { .roles(HashSet::from([BuiltinRole::OpsRead])) .build(); - let request = app.post("/projects").with_user(user).json(&json!({ + let request = app.post("/projects").with_user(&user).json(&json!({ "name": "test_project_failed", "description": "", "objectives": "", diff --git a/editoast/src/views/stdcm_logs.rs b/editoast/src/views/stdcm_logs.rs new file mode 100644 index 00000000000..65a6aad1b60 --- /dev/null +++ b/editoast/src/views/stdcm_logs.rs @@ -0,0 +1,206 @@ +use axum::extract::Path; +use axum::extract::State; +use axum::Extension; +use axum::Json; +use editoast_authz::BuiltinRole; +use editoast_derive::EditoastError; +use editoast_models::DbConnectionPoolV2; +use thiserror::Error; +use utoipa::IntoParams; + +use crate::error::Result; +use crate::models::stdcm_log::StdcmLog; +use crate::List; +use crate::SelectionSettings; + +use super::AuthenticationExt; +use super::AuthorizationError; + +crate::routes! { + "/stdcm_logs" => { + list_stdcm_logs, + "/{trace_id}" => { + stdcm_log_by_trace_id, + }, + }, +} + +#[derive(Debug, Error, EditoastError)] +#[editoast_error(base_id = "stdcm_log")] +pub enum StdcmLogError { + #[error("StdcmLog '{trace_id}', could not be found")] + #[editoast_error(status = 404)] + NotFound { trace_id: String }, +} + +#[utoipa::path( + get, path = "", + tag = "stdcm_log", + responses( + (status = 200, body = Vec, description = "The list of Stdcm Logs"), + ) +)] +async fn list_stdcm_logs( + State(db_pool): State, + Extension(auth): AuthenticationExt, +) -> Result>> { + let authorized = auth + .check_roles([BuiltinRole::Superuser].into()) + .await + .map_err(AuthorizationError::AuthError)?; + if !authorized { + return Err(AuthorizationError::Forbidden.into()); + } + + let conn = &mut db_pool.get().await?; + + let stdcm_logs = StdcmLog::list(conn, SelectionSettings::new()).await?; + + Ok(Json(stdcm_logs)) +} + +#[derive(IntoParams)] +#[allow(unused)] +pub struct StdcmLogTraceIdParam { + trace_id: String, +} + +#[utoipa::path( + get, path = "", + params(StdcmLogTraceIdParam), + tag = "stdcm_log", + responses( + (status = 200, body = StdcmLog, description = "The stdcm log"), + (status = 404, body = InternalError, description = "The stdcm log was not found"), + ) +)] +async fn stdcm_log_by_trace_id( + State(db_pool): State, + Extension(auth): AuthenticationExt, + Path(trace_id): Path, +) -> Result> { + let authorized = auth + .check_roles([BuiltinRole::Superuser].into()) + .await + .map_err(AuthorizationError::AuthError)?; + if !authorized { + return Err(AuthorizationError::Forbidden.into()); + } + + let conn = &mut db_pool.get().await?; + + match StdcmLog::find_by_trace_id(conn, &trace_id).await { + Ok(Some(stdcm_log)) => Ok(Json(stdcm_log)), + Ok(None) => Err(StdcmLogError::NotFound { trace_id }.into()), + Err(e) => Err(e), + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + use axum::http::StatusCode; + use editoast_authz::authorizer::UserInfo; + use editoast_authz::BuiltinRole; + use editoast_models::DbConnectionPoolV2; + use pretty_assertions::assert_eq; + use rstest::rstest; + use serde_json::json; + use uuid::Uuid; + + use crate::models::fixtures::create_fast_rolling_stock; + use crate::models::fixtures::create_small_infra; + use crate::models::fixtures::create_timetable; + use crate::models::stdcm_log::StdcmLog; + use crate::views::test_app::TestApp; + use crate::views::test_app::TestAppBuilder; + use crate::views::test_app::TestRequestExt; + use crate::views::timetable::stdcm::tests::core_mocking_client; + use crate::views::timetable::stdcm::tests::pathfinding_result_success; + use crate::views::timetable::stdcm::tests::simulation_response; + use crate::views::timetable::stdcm::tests::stdcm_payload; + + async fn prepare_test_context(disable_superuser: bool) -> (TestApp, UserInfo, String) { + let db_pool = DbConnectionPoolV2::for_tests(); + let mut core = core_mocking_client(); + core.stub("/v2/stdcm") + .method(reqwest::Method::POST) + .response(StatusCode::OK) + .json(json!({ + "status": "success", + "simulation": serde_json::to_value(simulation_response()).unwrap(), + "path": serde_json::to_value(pathfinding_result_success()).unwrap(), + "departure_time": "2024-01-02T00:00:00Z" + })) + .finish(); + + let user = UserInfo { + identity: "superuser_id".to_owned(), + name: "superuser_name".to_owned(), + }; + + let mut roles = HashSet::from([BuiltinRole::Stdcm, BuiltinRole::Superuser]); + + if disable_superuser { + roles.remove(&BuiltinRole::Superuser); + } + + let app = TestAppBuilder::new() + .db_pool(db_pool.clone()) + .core_client(core.into()) + .enable_authorization(true) + .user(user.clone()) + .roles(roles) + .build(); + + let small_infra = create_small_infra(&mut db_pool.get_ok()).await; + let timetable = create_timetable(&mut db_pool.get_ok()).await; + let rolling_stock = + create_fast_rolling_stock(&mut db_pool.get_ok(), &Uuid::new_v4().to_string()).await; + + let trace_id = "cd62312a1bd0df0a612ff9ade3d03635".to_string(); + let request = app + .post(format!("/timetable/{}/stdcm?infra={}", timetable.id, small_infra.id).as_str()) + .json(&stdcm_payload(rolling_stock.id)) + .add_header("traceparent", format!("00-{trace_id}-18ae0228cf2d63b0-01")) + .with_user(&user); + app.fetch(request).assert_status(StatusCode::OK); + (app, user, trace_id) + } + + #[rstest] + async fn list_stdcm_logs_return_success() { + let (app, user, trace_id) = prepare_test_context(false).await; + let request = app.get("/stdcm_logs").with_user(&user); + let stdcm_logs: Vec = + app.fetch(request).assert_status(StatusCode::OK).json_into(); + assert_eq!(stdcm_logs.len(), 1); + assert_eq!( + stdcm_logs + .first() + .expect("expected at least one log entry, but found none") + .trace_id, + trace_id + ); + } + + #[rstest] + async fn get_stdcm_log_by_trace_id_return_success() { + let (app, user, trace_id) = prepare_test_context(false).await; + let request = app + .get(format!("/stdcm_logs/{trace_id}").as_str()) + .with_user(&user); + let stdcm_log: StdcmLog = app.fetch(request).assert_status(StatusCode::OK).json_into(); + assert_eq!(stdcm_log.trace_id, trace_id); + } + + #[rstest] + async fn get_stdcm_log_by_trace_id_return_unauthorized() { + let (app, user, trace_id) = prepare_test_context(true).await; + let request = app + .get(format!("/stdcm_logs/{trace_id}").as_str()) + .with_user(&user); + app.fetch(request).assert_status(StatusCode::FORBIDDEN); + } +} diff --git a/editoast/src/views/test_app.rs b/editoast/src/views/test_app.rs index 2ca93547513..79fdc07b49e 100644 --- a/editoast/src/views/test_app.rs +++ b/editoast/src/views/test_app.rs @@ -159,7 +159,11 @@ impl TestAppBuilder { endpoint: Url::parse("http://localhost:4317").unwrap(), }), }; - let sub = create_tracing_subscriber(tracing_config, NoopSpanExporter); + let sub = create_tracing_subscriber( + tracing_config, + tracing_subscriber::filter::LevelFilter::DEBUG, + NoopSpanExporter, + ); let tracing_guard = tracing::subscriber::set_default(sub); // Config valkey @@ -293,13 +297,13 @@ impl TestApp { } pub trait TestRequestExt { - fn with_user(self, user: UserInfo) -> Self; + fn with_user(self, user: &UserInfo) -> Self; } impl TestRequestExt for TestRequest { - fn with_user(self, user: UserInfo) -> Self { - self.add_header("x-remote-user-identity", user.identity) - .add_header("x-remote-user-name", user.name) + fn with_user(self, user: &UserInfo) -> Self { + self.add_header("x-remote-user-identity", user.identity.clone()) + .add_header("x-remote-user-name", user.name.clone()) } } diff --git a/editoast/src/views/timetable/stdcm.rs b/editoast/src/views/timetable/stdcm.rs index f63488d4229..c3738bfac4b 100644 --- a/editoast/src/views/timetable/stdcm.rs +++ b/editoast/src/views/timetable/stdcm.rs @@ -6,8 +6,9 @@ use axum::extract::Path; use axum::extract::Query; use axum::extract::State; use axum::Extension; +use chrono::DateTime; +use chrono::Duration; use chrono::Utc; -use chrono::{DateTime, Duration}; use editoast_authz::BuiltinRole; use editoast_derive::EditoastError; use editoast_models::DbConnectionPoolV2; @@ -17,6 +18,7 @@ use editoast_schemas::train_schedule::Margins; use editoast_schemas::train_schedule::ReceptionSignal; use editoast_schemas::train_schedule::ScheduleItem; use failure_handler::SimulationFailureHandler; +use opentelemetry::trace::TraceContextExt; use request::convert_steps; use request::Request; use serde::Deserialize; @@ -25,6 +27,7 @@ use std::collections::HashMap; use std::sync::Arc; use thiserror::Error; use tracing::Instrument; +use tracing_opentelemetry::OpenTelemetrySpanExt; use utoipa::IntoParams; use utoipa::ToSchema; use validator::Validate; @@ -141,6 +144,12 @@ async fn stdcm( return Err(AuthorizationError::Forbidden.into()); } + let trace_id = tracing::Span::current() + .context() + .span() + .span_context() + .trace_id(); + stdcm_request.validate()?; let mut conn = db_pool.get().await?; @@ -273,7 +282,14 @@ async fn stdcm( // We just don't await the creation of the log entry since we want // the endpoint to return as soon as possible, and because failing // to persist a log entry is not a very important error here. - StdcmLog::log(conn, stdcm_request, stdcm_response.clone(), user_id).in_current_span(), + StdcmLog::log( + conn, + trace_id.to_string(), + stdcm_request, + stdcm_response.clone(), + user_id, + ) + .in_current_span(), ) .await; } @@ -474,7 +490,7 @@ fn build_single_margin(margin: Option) -> Margins { } #[cfg(test)] -mod tests { +pub mod tests { use axum::http::StatusCode; use chrono::DateTime; use editoast_models::DbConnectionPoolV2; @@ -650,7 +666,7 @@ mod tests { assert_eq!(physics_consist.max_speed, 20_f64); } - fn pathfinding_result_success() -> PathfindingResult { + pub fn pathfinding_result_success() -> PathfindingResult { PathfindingResult::Success(PathfindingResultSuccess { blocks: vec![], routes: vec![], @@ -660,7 +676,7 @@ mod tests { }) } - fn simulation_response() -> SimulationResponse { + pub fn simulation_response() -> SimulationResponse { SimulationResponse::Success { base: ReportTrain { positions: vec![], @@ -700,7 +716,7 @@ mod tests { } } - fn stdcm_payload(rolling_stock_id: i64) -> serde_json::Value { + pub fn stdcm_payload(rolling_stock_id: i64) -> serde_json::Value { json!({ "comfort": "STANDARD", "margin": "4.5min/100km", @@ -723,7 +739,7 @@ mod tests { }) } - fn core_mocking_client() -> MockingClient { + pub fn core_mocking_client() -> MockingClient { let mut core = MockingClient::new(); core.stub("/v2/pathfinding/blocks") .method(reqwest::Method::POST) diff --git a/front/public/locales/en/errors.json b/front/public/locales/en/errors.json index 8b6dd34e806..d7d28237860 100644 --- a/front/public/locales/en/errors.json +++ b/front/public/locales/en/errors.json @@ -186,6 +186,9 @@ "stdcm": { "InfraNotFound": "Infrastructure '{{infra_id}}' does not exist" }, + "stdcm_log": { + "NotFound": "" + }, "stdcm_v2": { "InfraNotFound": "Infrastructure '{{infra_id}}' does not exist", "InvalidPathItems": "Invalid waypoint(s) {{items}}", diff --git a/front/public/locales/fr/errors.json b/front/public/locales/fr/errors.json index 9f41174a6aa..c7ec765e034 100644 --- a/front/public/locales/fr/errors.json +++ b/front/public/locales/fr/errors.json @@ -188,6 +188,9 @@ "stdcm": { "InfraNotFound": "Infrastructure {{infra_id}} non trouvée" }, + "stdcm_log": { + "NotFound": "" + }, "stdcm_v2": { "InfraNotFound": "Infrastructure '{{infra_id}}' non trouvée", "InvalidPathItems": "Point(s) de passage {{items}} invalide(s)", diff --git a/front/src/common/api/generatedEditoastApi.ts b/front/src/common/api/generatedEditoastApi.ts index bbbaf449098..5ce182d957f 100644 --- a/front/src/common/api/generatedEditoastApi.ts +++ b/front/src/common/api/generatedEditoastApi.ts @@ -18,6 +18,7 @@ export const addTagTypes = [ 'speed_limit_tags', 'sprites', 'stdcm_search_environment', + 'stdcm_log', 'temporary_speed_limits', 'timetable', 'stdcm', @@ -769,6 +770,17 @@ const injectedRtkApi = api }), invalidatesTags: ['stdcm_search_environment'], }), + getStdcmLogs: build.query({ + query: () => ({ url: `/stdcm_logs` }), + providesTags: ['stdcm_log'], + }), + getStdcmLogsByTraceId: build.query< + GetStdcmLogsByTraceIdApiResponse, + GetStdcmLogsByTraceIdApiArg + >({ + query: (queryArg) => ({ url: `/stdcm_logs/${queryArg.traceId}` }), + providesTags: ['stdcm_log'], + }), postTemporarySpeedLimitGroup: build.mutation< PostTemporarySpeedLimitGroupApiResponse, PostTemporarySpeedLimitGroupApiArg @@ -1636,6 +1648,12 @@ export type PostStdcmSearchEnvironmentApiResponse = /** status 201 */ StdcmSear export type PostStdcmSearchEnvironmentApiArg = { stdcmSearchEnvironmentCreateForm: StdcmSearchEnvironmentCreateForm; }; +export type GetStdcmLogsApiResponse = /** status 200 The list of Stdcm Logs */ StdcmLog[]; +export type GetStdcmLogsApiArg = void; +export type GetStdcmLogsByTraceIdApiResponse = /** status 200 The stdcm log */ StdcmLog; +export type GetStdcmLogsByTraceIdApiArg = { + traceId: string; +}; export type PostTemporarySpeedLimitGroupApiResponse = /** status 201 The id of the created temporary speed limit group. */ { group_id: number; @@ -3276,30 +3294,57 @@ export type StdcmSearchEnvironmentCreateForm = { timetable_id: number; work_schedule_group_id?: number | null; }; -export type TimetableResult = { - timetable_id: number; -}; -export type TimetableDetailedResult = { - timetable_id: number; - train_ids: number[]; +export type StepTimingData = { + /** Time at which the train should arrive at the location */ + arrival_time: string; + /** The train may arrive up to this duration after the expected arrival time */ + arrival_time_tolerance_after: number; + /** The train may arrive up to this duration before the expected arrival time */ + arrival_time_tolerance_before: number; }; -export type ConflictRequirement = { - end_time: string; - start_time: string; - zone: string; +export type PathfindingItem = { + /** The stop duration in milliseconds, None if the train does not stop. */ + duration?: number | null; + location: PathItemLocation; + timing_data?: StepTimingData | null; }; -export type Conflict = { - conflict_type: 'Spacing' | 'Routing'; - /** Datetime of the end of the conflict */ - end_time: string; - /** List of requirements causing the conflict */ - requirements: ConflictRequirement[]; - /** Datetime of the start of the conflict */ - start_time: string; - /** List of train ids involved in the conflict */ - train_ids: number[]; - /** List of work schedule ids involved in the conflict */ - work_schedule_ids: number[]; +export type Request = { + comfort: Comfort; + electrical_profile_set_id?: number | null; + loading_gauge_type?: LoadingGaugeType | null; + /** Can be a percentage `X%`, a time in minutes per 100 kilometer `Xmin/100km` */ + margin?: string | null; + /** Maximum speed of the consist in km/h */ + max_speed?: number | null; + /** By how long we can shift the departure time in milliseconds + Deprecated, first step data should be used instead */ + maximum_departure_delay?: number | null; + /** Specifies how long the total run time can be in milliseconds + Deprecated, first step data should be used instead */ + maximum_run_time?: number | null; + rolling_stock_id: number; + /** Train categories for speed limits */ + speed_limit_tags?: string | null; + /** Deprecated, first step arrival time should be used instead */ + start_time?: string | null; + steps: PathfindingItem[]; + temporary_speed_limit_group_id?: number | null; + /** Margin after the train passage in milliseconds + + Enforces that the path used by the train should be free and + available at least that many milliseconds after its passage. */ + time_gap_after?: number; + /** Margin before the train passage in seconds + + Enforces that the path used by the train should be free and + available at least that many milliseconds before its passage. */ + time_gap_before?: number; + /** Total length of the consist in meters */ + total_length?: number | null; + /** Total mass of the consist in kg */ + total_mass?: number | null; + towed_rolling_stock_id?: number | null; + work_schedule_group_id?: number | null; }; export type ReportTrain = { /** Total energy consumption */ @@ -3411,19 +3456,52 @@ export type SimulationResponse = core_error: InternalError; status: 'simulation_failed'; }; -export type StepTimingData = { - /** Time at which the train should arrive at the location */ - arrival_time: string; - /** The train may arrive up to this duration after the expected arrival time */ - arrival_time_tolerance_after: number; - /** The train may arrive up to this duration before the expected arrival time */ - arrival_time_tolerance_before: number; +export type Response = + | { + departure_time: string; + path: PathfindingResultSuccess; + simulation: SimulationResponse; + status: 'success'; + } + | { + status: 'path_not_found'; + } + | { + error: SimulationResponse; + status: 'preprocessing_simulation_error'; + }; +export type StdcmLog = { + created: string; + id: number; + request: Request; + response: Response; + trace_id: string; + user_id?: number | null; }; -export type PathfindingItem = { - /** The stop duration in milliseconds, None if the train does not stop. */ - duration?: number | null; - location: PathItemLocation; - timing_data?: StepTimingData | null; +export type TimetableResult = { + timetable_id: number; +}; +export type TimetableDetailedResult = { + timetable_id: number; + train_ids: number[]; +}; +export type ConflictRequirement = { + end_time: string; + start_time: string; + zone: string; +}; +export type Conflict = { + conflict_type: 'Spacing' | 'Routing'; + /** Datetime of the end of the conflict */ + end_time: string; + /** List of requirements causing the conflict */ + requirements: ConflictRequirement[]; + /** Datetime of the start of the conflict */ + start_time: string; + /** List of train ids involved in the conflict */ + train_ids: number[]; + /** List of work schedule ids involved in the conflict */ + work_schedule_ids: number[]; }; export type Distribution = 'STANDARD' | 'MARECO'; export type TrainScheduleBase = {