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 @@