From 77e6637a9e3fb9f63c77b422b5ad8cedc7bd8c61 Mon Sep 17 00:00:00 2001 From: Daniel Harvey Date: Mon, 18 May 2026 14:49:20 -0400 Subject: [PATCH 1/3] cms-w3-t1: add /cms/tracks admin track list with edit/delete affordances --- DeepDrftCms/DeepDrftCms.csproj | 1 + DeepDrftCms/Pages/Tracks/TrackList.razor | 135 +++++++++++++++++++++++ DeepDrftCms/_Imports.razor | 1 + 3 files changed, 137 insertions(+) create mode 100644 DeepDrftCms/Pages/Tracks/TrackList.razor diff --git a/DeepDrftCms/DeepDrftCms.csproj b/DeepDrftCms/DeepDrftCms.csproj index 65b0747..c484fdc 100644 --- a/DeepDrftCms/DeepDrftCms.csproj +++ b/DeepDrftCms/DeepDrftCms.csproj @@ -21,6 +21,7 @@ + diff --git a/DeepDrftCms/Pages/Tracks/TrackList.razor b/DeepDrftCms/Pages/Tracks/TrackList.razor new file mode 100644 index 0000000..e63ba33 --- /dev/null +++ b/DeepDrftCms/Pages/Tracks/TrackList.razor @@ -0,0 +1,135 @@ +@page "/cms/tracks" +@rendermode InteractiveServer +@using System.Net +@using AuthBlocksWeb.HierarchicalAuthorize +@using DeepDrftModels.Models +@attribute [HierarchicalRoleAuthorize("Admin")] +@inject ITrackService TrackService +@inject IHttpClientFactory HttpClientFactory +@inject IDialogService DialogService +@inject ISnackbar Snackbar +@inject NavigationManager Navigation + +Tracks — DeepDrft CMS + + + + Tracks + + Add Track + + + + + + No tracks found. + + + Loading tracks… + + + Track Name + Artist + Album + Genre + Release Date + Entry Key + Actions + + + @context.TrackName + @context.Artist + @(context.Album ?? "—") + @(context.Genre ?? "—") + @(context.ReleaseDate?.ToString("yyyy-MM-dd") ?? "—") + @context.EntryKey + + + + + + + + + + + + + + + +@code { + private MudTable? _table; + + private async Task> LoadServerData(TableState state, CancellationToken cancellationToken) + { + var pageNumber = state.Page + 1; // MudTable is 0-based, service is 1-based. + var sortColumn = string.IsNullOrEmpty(state.SortLabel) ? "TrackName" : state.SortLabel; + var sortDescending = state.SortDirection == SortDirection.Descending; + + var result = await TrackService.GetPaged(pageNumber, state.PageSize, sortColumn, sortDescending); + + if (!result.Success || result.Value is null) + { + var errorText = result.Messages.FirstOrDefault()?.Message ?? "Unknown error"; + Snackbar.Add($"Failed to load tracks: {errorText}", Severity.Error); + return new TableData { Items = Array.Empty(), TotalItems = 0 }; + } + + var page = result.Value; + return new TableData + { + Items = page.Items, + TotalItems = page.TotalCount + }; + } + + private async Task ConfirmAndDelete(TrackEntity track) + { + var confirmed = await DialogService.ShowMessageBox( + title: "Delete track", + markupMessage: new MarkupString($"Delete {WebUtility.HtmlEncode(track.TrackName)} by {WebUtility.HtmlEncode(track.Artist)}? This removes both the metadata row and the underlying audio entry."), + yesText: "Delete", + cancelText: "Cancel"); + + if (confirmed != true) return; + + try + { + var client = HttpClientFactory.CreateClient("DeepDrft.API"); + var response = await client.DeleteAsync($"api/cms/track/{track.Id}"); + + if (response.IsSuccessStatusCode) + { + Snackbar.Add($"Deleted '{track.TrackName}'.", Severity.Success); + if (_table is not null) await _table.ReloadServerData(); + } + else + { + Snackbar.Add($"Delete failed ({(int)response.StatusCode} {response.ReasonPhrase}).", Severity.Error); + } + } + catch (Exception ex) + { + Snackbar.Add($"Delete failed: {ex.Message}", Severity.Error); + } + } +} diff --git a/DeepDrftCms/_Imports.razor b/DeepDrftCms/_Imports.razor index cf3d836..d0b266a 100644 --- a/DeepDrftCms/_Imports.razor +++ b/DeepDrftCms/_Imports.razor @@ -8,4 +8,5 @@ @using Microsoft.JSInterop @using DeepDrftCms @using DeepDrftModels.Entities +@using DeepDrftWeb.Services @using MudBlazor From 88c94b24cf5090475d906155bd31a78fa4fa4102 Mon Sep 17 00:00:00 2001 From: Daniel Harvey Date: Mon, 18 May 2026 14:58:36 -0400 Subject: [PATCH 2/3] Fix W3-T1 review: forward CancellationToken in GetPaged, scrub ex.Message from snackbar, drop unused Navigation inject, annotate ITrackService wiring in CmsStartup --- DeepDrftCms/CmsStartup.cs | 5 +++++ DeepDrftCms/Pages/Tracks/TrackList.razor | 7 ++++--- DeepDrftCms/_Imports.razor | 1 + DeepDrftWeb.Services/ITrackService.cs | 2 +- DeepDrftWeb.Services/Repositories/TrackRepository.cs | 6 +++--- DeepDrftWeb.Services/TrackService.cs | 4 ++-- 6 files changed, 16 insertions(+), 9 deletions(-) diff --git a/DeepDrftCms/CmsStartup.cs b/DeepDrftCms/CmsStartup.cs index 44861e1..1b80271 100644 --- a/DeepDrftCms/CmsStartup.cs +++ b/DeepDrftCms/CmsStartup.cs @@ -7,6 +7,11 @@ public static class CmsStartup public static IServiceCollection AddCmsServices(this IServiceCollection services) { // CMS-specific services registered here as implementation waves land. + // + // Note: ITrackService (and its dependencies: TrackRepository, DeepDrftContext) is registered + // by DeepDrftWeb.Startup.ConfigureDomainServices, not here. The CMS RCL runs inside the + // DeepDrftWeb host, which wires those up before this method is called. A standalone CMS + // host would need to register ITrackService separately. return services; } } diff --git a/DeepDrftCms/Pages/Tracks/TrackList.razor b/DeepDrftCms/Pages/Tracks/TrackList.razor index e63ba33..f6892c6 100644 --- a/DeepDrftCms/Pages/Tracks/TrackList.razor +++ b/DeepDrftCms/Pages/Tracks/TrackList.razor @@ -8,7 +8,7 @@ @inject IHttpClientFactory HttpClientFactory @inject IDialogService DialogService @inject ISnackbar Snackbar -@inject NavigationManager Navigation +@inject ILogger Logger Tracks — DeepDrft CMS @@ -85,7 +85,7 @@ var sortColumn = string.IsNullOrEmpty(state.SortLabel) ? "TrackName" : state.SortLabel; var sortDescending = state.SortDirection == SortDirection.Descending; - var result = await TrackService.GetPaged(pageNumber, state.PageSize, sortColumn, sortDescending); + var result = await TrackService.GetPaged(pageNumber, state.PageSize, sortColumn, sortDescending, cancellationToken); if (!result.Success || result.Value is null) { @@ -129,7 +129,8 @@ } catch (Exception ex) { - Snackbar.Add($"Delete failed: {ex.Message}", Severity.Error); + Logger.LogError(ex, "Delete failed for track {TrackId}", track.Id); + Snackbar.Add("Delete failed — please try again.", Severity.Error); } } } diff --git a/DeepDrftCms/_Imports.razor b/DeepDrftCms/_Imports.razor index d0b266a..edc98a0 100644 --- a/DeepDrftCms/_Imports.razor +++ b/DeepDrftCms/_Imports.razor @@ -5,6 +5,7 @@ @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web.Virtualization @using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.Extensions.Logging @using Microsoft.JSInterop @using DeepDrftCms @using DeepDrftModels.Entities diff --git a/DeepDrftWeb.Services/ITrackService.cs b/DeepDrftWeb.Services/ITrackService.cs index 93184a5..6d387ef 100644 --- a/DeepDrftWeb.Services/ITrackService.cs +++ b/DeepDrftWeb.Services/ITrackService.cs @@ -8,7 +8,7 @@ public interface ITrackService { Task> GetById(long id); Task>> GetAll(); - Task>> GetPaged(int pageNumber, int pageSize, string? sortColumn, bool sortDescending); + Task>> GetPaged(int pageNumber, int pageSize, string? sortColumn, bool sortDescending, CancellationToken cancellationToken = default); Task> Create(TrackEntity newTrack); Task> Update(TrackEntity track); Task Delete(long id); diff --git a/DeepDrftWeb.Services/Repositories/TrackRepository.cs b/DeepDrftWeb.Services/Repositories/TrackRepository.cs index d4cb123..8399d4e 100644 --- a/DeepDrftWeb.Services/Repositories/TrackRepository.cs +++ b/DeepDrftWeb.Services/Repositories/TrackRepository.cs @@ -24,12 +24,12 @@ public class TrackRepository return await _db.Tracks.ToListAsync(); } - public async Task> GetPage(PagingParameters pageParameters) + public async Task> GetPage(PagingParameters pageParameters, CancellationToken cancellationToken = default) { // Two separate queries with no transaction: count and page can be momentarily inconsistent // under concurrent writes. Acceptable — write volume is low and the UI is read-only. // If filtering is added, the count query must be updated to apply the same filter. - var count = await _db.Tracks.CountAsync(); + var count = await _db.Tracks.CountAsync(cancellationToken); var orderBy = pageParameters.OrderBy ?? (t => t.Id); var ordered = pageParameters.IsDescending @@ -39,7 +39,7 @@ public class TrackRepository var page = await ordered .Skip(pageParameters.Skip) .Take(pageParameters.PageSize) - .ToListAsync(); + .ToListAsync(cancellationToken); return new PagedResult(page, count, pageParameters.Page, pageParameters.PageSize); } diff --git a/DeepDrftWeb.Services/TrackService.cs b/DeepDrftWeb.Services/TrackService.cs index fe6015d..e1cd6ad 100644 --- a/DeepDrftWeb.Services/TrackService.cs +++ b/DeepDrftWeb.Services/TrackService.cs @@ -41,7 +41,7 @@ public class TrackService : ITrackService } } - public async Task>> GetPaged(int pageNumber, int pageSize, string? sortColumn, bool sortDescending) + public async Task>> GetPaged(int pageNumber, int pageSize, string? sortColumn, bool sortDescending, CancellationToken cancellationToken = default) { try { @@ -75,7 +75,7 @@ public class TrackService : ITrackService } } - var page = await _repository.GetPage(parameters); + var page = await _repository.GetPage(parameters, cancellationToken); return ResultContainer>.CreatePassResult(page); } catch (Exception e) From b6715e495aed4c100dab330428cdc17d5c69413a Mon Sep 17 00:00:00 2001 From: Daniel Harvey Date: Mon, 18 May 2026 15:38:24 -0400 Subject: [PATCH 3/3] Fix 401 on track delete: attach JWT bearer token in TrackList.ConfirmAndDelete --- DeepDrftCms/Pages/Tracks/TrackList.razor | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/DeepDrftCms/Pages/Tracks/TrackList.razor b/DeepDrftCms/Pages/Tracks/TrackList.razor index f6892c6..a7f66d4 100644 --- a/DeepDrftCms/Pages/Tracks/TrackList.razor +++ b/DeepDrftCms/Pages/Tracks/TrackList.razor @@ -1,11 +1,13 @@ @page "/cms/tracks" @rendermode InteractiveServer @using System.Net +@using System.Net.Http.Headers @using AuthBlocksWeb.HierarchicalAuthorize @using DeepDrftModels.Models @attribute [HierarchicalRoleAuthorize("Admin")] @inject ITrackService TrackService @inject IHttpClientFactory HttpClientFactory +@inject AuthBlocksWeb.Services.ITokenService TokenService @inject IDialogService DialogService @inject ISnackbar Snackbar @inject ILogger Logger @@ -115,6 +117,9 @@ try { var client = HttpClientFactory.CreateClient("DeepDrft.API"); + var token = await TokenService.GetAccessTokenAsync(); + if (!string.IsNullOrEmpty(token)) + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var response = await client.DeleteAsync($"api/cms/track/{track.Id}"); if (response.IsSuccessStatusCode)