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/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..a7f66d4
--- /dev/null
+++ b/DeepDrftCms/Pages/Tracks/TrackList.razor
@@ -0,0 +1,141 @@
+@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
+
+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, cancellationToken);
+
+ 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 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)
+ {
+ 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)
+ {
+ 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 cf3d836..edc98a0 100644
--- a/DeepDrftCms/_Imports.razor
+++ b/DeepDrftCms/_Imports.razor
@@ -5,7 +5,9 @@
@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
+@using DeepDrftWeb.Services
@using MudBlazor
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)