From 9b28a09253910bb79abb9d973b21b9e2a4e44cc3 Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Tue, 12 May 2026 00:18:47 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BF=D0=BE=D0=B4=D1=80=D0=BE=D0=B1?= =?UTF-8?q?=D0=BD=D0=BE=D0=B5=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0=D0=B6?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE=D0=BA?= =?UTF-8?q?=20=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE=D0=BD=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DTOs/Sync/SyncDtos.cs | 8 ++- .../ExternalServices/ModeusApiClient.cs | 20 +++++++- .../Services/ScheduleSyncService.cs | 49 +++++++++++++++++- frontend/src/api/types.ts | 1 + .../src/views/admin/AdminLecturesView.vue | 50 ++++++++++++++++++- 5 files changed, 122 insertions(+), 6 deletions(-) diff --git a/backend/UniVerse.Application/DTOs/Sync/SyncDtos.cs b/backend/UniVerse.Application/DTOs/Sync/SyncDtos.cs index d1a738e..b8dd58a 100644 --- a/backend/UniVerse.Application/DTOs/Sync/SyncDtos.cs +++ b/backend/UniVerse.Application/DTOs/Sync/SyncDtos.cs @@ -7,7 +7,13 @@ public record SyncScheduleRequest( IReadOnlyList? TypeId ); -public record SyncResultDto(int Created, int Updated, int Skipped, string? Error); +public record SyncResultDto( + int Created, + int Updated, + int Skipped, + string? Error, + IReadOnlyList? Details = null +); public record SyncStatusDto(DateTime? LastSyncAt, string Status, SyncResultDto? LastResult); diff --git a/backend/UniVerse.Infrastructure/ExternalServices/ModeusApiClient.cs b/backend/UniVerse.Infrastructure/ExternalServices/ModeusApiClient.cs index 3a739f3..07e5031 100644 --- a/backend/UniVerse.Infrastructure/ExternalServices/ModeusApiClient.cs +++ b/backend/UniVerse.Infrastructure/ExternalServices/ModeusApiClient.cs @@ -23,7 +23,8 @@ public class ModeusApiClient : IModeusApiClient { var body = new { specialtyCode = request.SpecialtyCode, timeMin = request.TimeMin, timeMax = request.TimeMax, typeId = request.TypeId }; var response = await _http.PostAsJsonAsync("/api/proxy/events/search", body); - response.EnsureSuccessStatusCode(); + await EnsureSuccessAsync(response, "Modeus events search", + $"specialtyCode={request.SpecialtyCode ?? ""}, timeMin={request.TimeMin:O}, timeMax={request.TimeMax:O}, typeId=[{string.Join(", ", request.TypeId ?? [])}]"); return await response.Content.ReadFromJsonAsync() ?? new(new()); } @@ -46,7 +47,8 @@ public class ModeusApiClient : IModeusApiClient }; var response = await _http.PostAsJsonAsync("/api/proxy/rooms/search", body); - response.EnsureSuccessStatusCode(); + await EnsureSuccessAsync(response, "Modeus rooms search", + $"name=, sort=+building.name,+name, size={pageSize}, page={page}, deleted=false"); var payload = await response.Content.ReadFromJsonAsync() ?? new ModeusRoomsResponse(); allRooms.AddRange(payload.RoomItems); @@ -59,6 +61,20 @@ public class ModeusApiClient : IModeusApiClient return new ModeusRoomsResponse { Rooms = allRooms }; } + private static async Task EnsureSuccessAsync(HttpResponseMessage response, string operation, string requestSummary) + { + if (response.IsSuccessStatusCode) return; + + var responseBody = await response.Content.ReadAsStringAsync(); + if (responseBody.Length > 2000) + responseBody = string.Concat(responseBody.AsSpan(0, 2000), "..."); + + throw new HttpRequestException( + $"{operation} failed with HTTP {(int)response.StatusCode} {response.ReasonPhrase}. Request: {requestSummary}. Response body: {responseBody}", + null, + response.StatusCode); + } + public async Task> SearchEmployeeAsync(string fullname) { var response = await _http.GetFromJsonAsync>( diff --git a/backend/UniVerse.Infrastructure/Services/ScheduleSyncService.cs b/backend/UniVerse.Infrastructure/Services/ScheduleSyncService.cs index ee113e5..35c3aad 100644 --- a/backend/UniVerse.Infrastructure/Services/ScheduleSyncService.cs +++ b/backend/UniVerse.Infrastructure/Services/ScheduleSyncService.cs @@ -21,6 +21,7 @@ public class ScheduleSyncService : IScheduleSyncService public async Task SyncScheduleAsync(SyncScheduleRequest request) { + const string stage = "schedule"; int created = 0, updated = 0, skipped = 0; try { @@ -45,7 +46,18 @@ public class ScheduleSyncService : IScheduleSyncService catch (Exception ex) { _logger.LogError(ex, "Schedule sync failed"); - var result = new SyncResultDto(created, updated, skipped, ex.Message); + var result = new SyncResultDto(created, updated, skipped, ex.Message, BuildErrorDetails( + ex, + stage, + created, + updated, + skipped, + [ + $"specialtyCode={request.SpecialtyCode ?? ""}", + $"timeMin={request.TimeMin:O}", + $"timeMax={request.TimeMax:O}", + $"typeId=[{string.Join(", ", request.TypeId ?? [])}]" + ])); _lastStatus = new SyncStatusDto(DateTime.UtcNow, "failed", result); return result; } @@ -53,6 +65,7 @@ public class ScheduleSyncService : IScheduleSyncService public async Task SyncRoomsAsync() { + const string stage = "rooms"; int created = 0, updated = 0, skipped = 0; try { @@ -96,7 +109,13 @@ public class ScheduleSyncService : IScheduleSyncService catch (Exception ex) { _logger.LogError(ex, "Rooms sync failed"); - var result = new SyncResultDto(created, updated, skipped, ex.Message); + var result = new SyncResultDto(created, updated, skipped, ex.Message, BuildErrorDetails( + ex, + stage, + created, + updated, + skipped, + ["request=name:, sort:+building.name,+name, deleted:false, page size:100"])); _lastStatus = new SyncStatusDto(DateTime.UtcNow, "failed", result); return result; } @@ -109,4 +128,30 @@ public class ScheduleSyncService : IScheduleSyncService } public Task GetLastSyncStatusAsync() => Task.FromResult(_lastStatus); + + private static IReadOnlyList BuildErrorDetails( + Exception exception, + string stage, + int created, + int updated, + int skipped, + IReadOnlyList context) + { + var details = new List + { + $"stage={stage}", + $"exceptionType={exception.GetType().FullName}", + $"message={exception.Message}", + $"partialResult=created:{created}, updated:{updated}, skipped:{skipped}" + }; + + if (exception is HttpRequestException httpException && httpException.StatusCode.HasValue) + details.Add($"httpStatus={(int)httpException.StatusCode.Value} {httpException.StatusCode.Value}"); + + if (exception.InnerException != null) + details.Add($"innerException={exception.InnerException.GetType().FullName}: {exception.InnerException.Message}"); + + details.AddRange(context); + return details; + } } diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index 16056c6..accd091 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -166,6 +166,7 @@ export interface SyncResultDto { updated: number skipped: number error?: string | null + details?: string[] | null } export interface UserAchievementDto { diff --git a/frontend/src/views/admin/AdminLecturesView.vue b/frontend/src/views/admin/AdminLecturesView.vue index e584675..1aa281f 100644 --- a/frontend/src/views/admin/AdminLecturesView.vue +++ b/frontend/src/views/admin/AdminLecturesView.vue @@ -1,6 +1,7 @@