diff --git a/DeepDrftManager/Components/Pages/Index.razor b/DeepDrftManager/Components/Pages/Index.razor index 5e78f50..2746a65 100644 --- a/DeepDrftManager/Components/Pages/Index.razor +++ b/DeepDrftManager/Components/Pages/Index.razor @@ -1,13 +1,125 @@ @page "/" +@using DeepDrftManager.Services @attribute [Authorize] @layout Layout.CmsLayout @inject NavigationManager Nav +@inject ICmsTrackService CmsTrackService +@inject ILogger Logger DeepDrft CMS + + Catalogue + + + + @SummaryCard("Tracks", Icons.Material.Filled.LibraryMusic, Color.Primary, _tracksLoading, _trackCount) + + + @SummaryCard("Albums", Icons.Material.Filled.Album, Color.Secondary, _albumsLoading, _albumCount) + + + @SummaryCard("Genres", Icons.Material.Filled.Category, Color.Tertiary, _genresLoading, _genreCount) + + + + @code { - protected override void OnInitialized() + private bool _tracksLoading = true; + private bool _albumsLoading = true; + private bool _genresLoading = true; + + private int? _trackCount; + private int? _albumCount; + private int? _genreCount; + + protected override async Task OnInitializedAsync() { - Nav.NavigateTo("/tracks"); + // Three independent reads run concurrently. Each loader calls StateHasChanged in its + // finally block so its card updates as soon as its own fetch returns. + await Task.WhenAll(LoadTrackCount(), LoadAlbumCount(), LoadGenreCount()); } + + private async Task LoadTrackCount() + { + try + { + var result = await CmsTrackService.GetTrackCountAsync(); + _trackCount = result.Success ? result.Value : null; + if (!result.Success) + { + Logger.LogWarning("Dashboard track count failed: {Error}", + result.Messages.FirstOrDefault()?.Message ?? "Unknown error"); + } + } + finally + { + _tracksLoading = false; + StateHasChanged(); + } + } + + private async Task LoadAlbumCount() + { + try + { + var result = await CmsTrackService.GetAlbumSummariesAsync(); + _albumCount = result.Success && result.Value is not null ? result.Value.Count : null; + if (!result.Success) + { + Logger.LogWarning("Dashboard album summaries failed: {Error}", + result.Messages.FirstOrDefault()?.Message ?? "Unknown error"); + } + } + finally + { + _albumsLoading = false; + StateHasChanged(); + } + } + + private async Task LoadGenreCount() + { + try + { + var result = await CmsTrackService.GetGenreSummariesAsync(); + _genreCount = result.Success && result.Value is not null ? result.Value.Count : null; + if (!result.Success) + { + Logger.LogWarning("Dashboard genre summaries failed: {Error}", + result.Messages.FirstOrDefault()?.Message ?? "Unknown error"); + } + } + finally + { + _genresLoading = false; + StateHasChanged(); + } + } + + private RenderFragment SummaryCard(string label, string icon, Color color, bool loading, int? count) => __builder => + { + + + + + @if (loading) + { + + } + else + { + @(count?.ToString() ?? "—") + } + @label + + + + + View + + + + }; } diff --git a/DeepDrftManager/Services/CmsTrackService.cs b/DeepDrftManager/Services/CmsTrackService.cs index c6fb909..d7c1513 100644 --- a/DeepDrftManager/Services/CmsTrackService.cs +++ b/DeepDrftManager/Services/CmsTrackService.cs @@ -440,4 +440,106 @@ public class CmsTrackService : ICmsTrackService return Result.CreateFailResult("Failed to generate waveform profile."); } } + + public async Task>> GetAlbumSummariesAsync(CancellationToken ct = default) + { + var client = _httpClientFactory.CreateClient(ContentCmsClientName); + + HttpResponseMessage response; + try + { + response = await client.GetAsync("api/track/albums", ct); + } + catch (Exception ex) + { + _logger.LogError(ex, "Content API call failed for album summaries"); + return ResultContainer>.CreateFailResult("Content API is unreachable."); + } + + using (response) + { + if (!response.IsSuccessStatusCode) + { + _logger.LogError("Content API album summaries failed: {Status}", (int)response.StatusCode); + return ResultContainer>.CreateFailResult("Failed to load albums."); + } + + List? albums; + try + { + albums = await response.Content.ReadFromJsonAsync>(ct); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to deserialize album summaries from Content API response"); + return ResultContainer>.CreateFailResult("Content API returned an unexpected response."); + } + + if (albums is null) + { + _logger.LogError("Content API returned a null album summaries list"); + return ResultContainer>.CreateFailResult("Content API returned an empty response."); + } + + return ResultContainer>.CreatePassResult(albums); + } + } + + public async Task>> GetGenreSummariesAsync(CancellationToken ct = default) + { + var client = _httpClientFactory.CreateClient(ContentCmsClientName); + + HttpResponseMessage response; + try + { + response = await client.GetAsync("api/track/genres", ct); + } + catch (Exception ex) + { + _logger.LogError(ex, "Content API call failed for genre summaries"); + return ResultContainer>.CreateFailResult("Content API is unreachable."); + } + + using (response) + { + if (!response.IsSuccessStatusCode) + { + _logger.LogError("Content API genre summaries failed: {Status}", (int)response.StatusCode); + return ResultContainer>.CreateFailResult("Failed to load genres."); + } + + List? genres; + try + { + genres = await response.Content.ReadFromJsonAsync>(ct); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to deserialize genre summaries from Content API response"); + return ResultContainer>.CreateFailResult("Content API returned an unexpected response."); + } + + if (genres is null) + { + _logger.LogError("Content API returned a null genre summaries list"); + return ResultContainer>.CreateFailResult("Content API returned an empty response."); + } + + return ResultContainer>.CreatePassResult(genres); + } + } + + public async Task> GetTrackCountAsync(CancellationToken ct = default) + { + // Re-use the paged endpoint: a single-item page carries the full TotalCount, so no + // dedicated count endpoint is needed. + var paged = await GetPagedAsync(page: 1, pageSize: 1, sortColumn: null, sortDescending: false, ct); + if (!paged.Success || paged.Value is null) + { + var error = paged.Messages.FirstOrDefault()?.Message ?? "Failed to load track count."; + return ResultContainer.CreateFailResult(error); + } + + return ResultContainer.CreatePassResult(paged.Value.TotalCount); + } } diff --git a/DeepDrftManager/Services/ICmsTrackService.cs b/DeepDrftManager/Services/ICmsTrackService.cs index e98cff8..2ce2058 100644 --- a/DeepDrftManager/Services/ICmsTrackService.cs +++ b/DeepDrftManager/Services/ICmsTrackService.cs @@ -82,4 +82,15 @@ public interface ICmsTrackService /// POST api/track/{entryKey}/waveform. Maps a 404 to a "Track audio not found." failure. /// Task GenerateWaveformProfileAsync(string entryKey, CancellationToken ct = default); + + /// Returns all distinct albums with track counts from GET api/track/albums. + Task>> GetAlbumSummariesAsync(CancellationToken ct = default); + + /// Returns all distinct genres with track counts from GET api/track/genres. + Task>> GetGenreSummariesAsync(CancellationToken ct = default); + + /// + /// Returns the total track count by calling GET api/track/page with pageSize=1 and reading TotalCount. + /// + Task> GetTrackCountAsync(CancellationToken ct = default); }