diff --git a/DeepDrftManager/Components/Pages/Tracks/CmsAlbumBrowser.razor b/DeepDrftManager/Components/Pages/Tracks/CmsAlbumBrowser.razor
index c6b2e71..fdcc781 100644
--- a/DeepDrftManager/Components/Pages/Tracks/CmsAlbumBrowser.razor
+++ b/DeepDrftManager/Components/Pages/Tracks/CmsAlbumBrowser.razor
@@ -120,6 +120,9 @@ else
live here. *@
Profile
High-res
+ @* Info column: per-track EntryKey + OriginalFileName tooltip (migrated
+ from the retired CmsTrackGrid's .cms-track-info monospace block). *@
+
@track.TrackNumber
@@ -156,6 +159,24 @@ else
}
+ @* Per-track info tooltip (restored from the retired CmsTrackGrid's
+ .cms-track-info monospace block): EntryKey + OriginalFileName. *@
+
+
+
+ @track.EntryKey
+ @if (!string.IsNullOrWhiteSpace(track.OriginalFileName))
+ {
+ @track.OriginalFileName
+ }
+
+
+
+
+
+
}
@@ -171,6 +192,26 @@ else
[Parameter] public bool IsLoading { get; set; }
[Parameter] public EventCallback OnReleasesChanged { get; set; }
+ ///
+ /// Fires after any per-row waveform generate (profile or high-res) succeeds. The parent page
+ /// wires this to its own RefreshWaveformStatusAsync so its missing-count badges stay
+ /// current after an individual-row generate inside an expanded album row.
+ ///
+ [Parameter] public EventCallback OnWaveformGenerated { get; set; }
+
+ ///
+ /// Clears the cached per-track waveform status so the next row expand re-fetches fresh data
+ /// from the API. Called by the parent page after a catalogue-wide bulk run so already-expanded
+ /// rows reflect the new state on the next expand interaction.
+ ///
+ public Task InvalidateWaveformStatusAsync()
+ {
+ _profileStatus = null;
+ _highResStatus = null;
+ StateHasChanged();
+ return Task.CompletedTask;
+ }
+
// Zero or more dedicated, header-labelled special-action columns (Session hero upload, Mix waveform
// generate), each rendered as its own header cell + per-row cell between the Tracks and Actions
// columns. The ALL and Cut tabs leave this empty and render exactly as before — only the standard
@@ -265,6 +306,7 @@ else
{
(_profileStatus ??= new())[track.EntryKey] = true;
Snackbar.Add($"Generated profile for '{track.TrackName}'.", Severity.Success);
+ await OnWaveformGenerated.InvokeAsync();
}
else
{
@@ -295,6 +337,7 @@ else
{
(_highResStatus ??= new())[track.EntryKey] = true;
Snackbar.Add($"Generated high-res datum for '{track.TrackName}'.", Severity.Success);
+ await OnWaveformGenerated.InvokeAsync();
}
else
{
diff --git a/DeepDrftManager/Components/Pages/Tracks/CmsAllReleasesGrid.razor b/DeepDrftManager/Components/Pages/Tracks/CmsAllReleasesGrid.razor
index 5e37b0b..a92c7a8 100644
--- a/DeepDrftManager/Components/Pages/Tracks/CmsAllReleasesGrid.razor
+++ b/DeepDrftManager/Components/Pages/Tracks/CmsAllReleasesGrid.razor
@@ -8,9 +8,11 @@
own data load so a host (TrackList today, the 8.A tab strip later) renders it with no parameters and
no VM plumbing. Re-loads on first render and re-fetches after a row mutation so the list stays in
sync with the catalogue. *@
-
+ OnReleasesChanged="OnGridReleasesChanged"
+ OnWaveformGenerated="OnWaveformGenerated" />
@code {
// Fires after a row mutation (delete) so a host can invalidate sibling caches derived from the same
@@ -18,9 +20,23 @@
// notification, not the data source. Optional: an embed that has no sibling state leaves it unset.
[Parameter] public EventCallback OnReleasesChanged { get; set; }
+ ///
+ /// Forwarded from the inner : fires after any per-row waveform
+ /// generate succeeds so the parent page can refresh its catalogue-wide missing-count badges.
+ ///
+ [Parameter] public EventCallback OnWaveformGenerated { get; set; }
+
+ private CmsAlbumBrowser? _albumBrowser;
private IReadOnlyList _releases = Array.Empty();
private bool _loading = true;
+ ///
+ /// Clears the inner grid's cached per-track waveform status so the next row expand re-fetches.
+ /// Called by the parent page after a catalogue-wide bulk run.
+ ///
+ public Task InvalidateWaveformStatusAsync() =>
+ _albumBrowser?.InvalidateWaveformStatusAsync() ?? Task.CompletedTask;
+
protected override Task OnInitializedAsync() => ReloadAsync();
private async Task OnGridReleasesChanged()
diff --git a/DeepDrftManager/Components/Pages/Tracks/CmsCutBrowser.razor b/DeepDrftManager/Components/Pages/Tracks/CmsCutBrowser.razor
index b2c23db..cd295b3 100644
--- a/DeepDrftManager/Components/Pages/Tracks/CmsCutBrowser.razor
+++ b/DeepDrftManager/Components/Pages/Tracks/CmsCutBrowser.razor
@@ -6,17 +6,34 @@
tab carries expand-tracks, delete, the Type chip, and per-row edit identically to the ALL tab — no
forked grid. Cuts have no medium-specific action, so no SpecialColumns are supplied; the grid renders
its shared edit/delete only. Embedded as tab content only; no standalone @page route. *@
-
+ OnReleasesChanged="ReloadAsync"
+ OnWaveformGenerated="OnWaveformGenerated" />
@code {
+ ///
+ /// Forwarded from the inner : fires after any per-row waveform
+ /// generate succeeds so the parent page can refresh its catalogue-wide missing-count badges.
+ ///
+ [Parameter] public EventCallback OnWaveformGenerated { get; set; }
+
+ private CmsAlbumBrowser? _albumBrowser;
+
protected override ReleaseMedium Medium => ReleaseMedium.Cut;
protected override string MediumNoun => "cuts";
protected override CutRow ToRow(ReleaseDto release) => new() { Release = release };
protected override ReleaseDto ReleaseOf(CutRow row) => row.Release;
+ ///
+ /// Clears the inner grid's cached per-track waveform status so the next row expand re-fetches.
+ /// Called by the parent page after a catalogue-wide bulk run.
+ ///
+ public Task InvalidateWaveformStatusAsync() =>
+ _albumBrowser?.InvalidateWaveformStatusAsync() ?? Task.CompletedTask;
+
public sealed class CutRow
{
public required ReleaseDto Release { get; set; }
diff --git a/DeepDrftManager/Components/Pages/Tracks/CmsMixBrowser.razor b/DeepDrftManager/Components/Pages/Tracks/CmsMixBrowser.razor
index 53e35b2..04faaca 100644
--- a/DeepDrftManager/Components/Pages/Tracks/CmsMixBrowser.razor
+++ b/DeepDrftManager/Components/Pages/Tracks/CmsMixBrowser.razor
@@ -41,9 +41,24 @@ else
///
[Parameter] public bool Embedded { get; set; }
+ ///
+ /// Forwarded from the inner : fires after any per-row waveform
+ /// generate succeeds so the parent page can refresh its catalogue-wide missing-count badges.
+ ///
+ [Parameter] public EventCallback OnWaveformGenerated { get; set; }
+
+ private CmsAlbumBrowser? _albumBrowser;
+
protected override ReleaseMedium Medium => ReleaseMedium.Mix;
protected override string MediumNoun => "mixes";
+ ///
+ /// Clears the inner grid's cached per-track waveform status so the next row expand re-fetches.
+ /// Called by the parent page after a catalogue-wide bulk run.
+ ///
+ public Task InvalidateWaveformStatusAsync() =>
+ _albumBrowser?.InvalidateWaveformStatusAsync() ?? Task.CompletedTask;
+
protected override MixRow ToRow(ReleaseDto release) => new()
{
Release = release,
@@ -56,9 +71,11 @@ else
// both branches above render the same markup without duplication. The Mix declares one dedicated
// "Waveform" special-action column; the grid renders it between Tracks and Actions, handing the cell
// each release, and RowFor recovers the matching MixRow's generate state.
- private RenderFragment GridContent => @ @;
// Allocated once per component instance in OnInitialized (field initializers cannot reference
diff --git a/DeepDrftManager/Components/Pages/Tracks/CmsSessionBrowser.razor b/DeepDrftManager/Components/Pages/Tracks/CmsSessionBrowser.razor
index d336d02..c0eb71c 100644
--- a/DeepDrftManager/Components/Pages/Tracks/CmsSessionBrowser.razor
+++ b/DeepDrftManager/Components/Pages/Tracks/CmsSessionBrowser.razor
@@ -42,9 +42,24 @@ else
///
[Parameter] public bool Embedded { get; set; }
+ ///
+ /// Forwarded from the inner : fires after any per-row waveform
+ /// generate succeeds so the parent page can refresh its catalogue-wide missing-count badges.
+ ///
+ [Parameter] public EventCallback OnWaveformGenerated { get; set; }
+
+ private CmsAlbumBrowser? _albumBrowser;
+
protected override ReleaseMedium Medium => ReleaseMedium.Session;
protected override string MediumNoun => "sessions";
+ ///
+ /// Clears the inner grid's cached per-track waveform status so the next row expand re-fetches.
+ /// Called by the parent page after a catalogue-wide bulk run.
+ ///
+ public Task InvalidateWaveformStatusAsync() =>
+ _albumBrowser?.InvalidateWaveformStatusAsync() ?? Task.CompletedTask;
+
protected override SessionRow ToRow(ReleaseDto release) => new()
{
Release = release,
@@ -57,9 +72,11 @@ else
// both branches above render the same markup without duplication. The Session declares one dedicated
// "Hero" special-action column; the grid renders it between Tracks and Actions, handing the cell each
// release, and RowFor recovers the matching SessionRow's upload state.
- private RenderFragment GridContent => @ @;
// Allocated once per component instance in OnInitialized (field initializers cannot reference
diff --git a/DeepDrftManager/Components/Pages/Tracks/Releases.razor b/DeepDrftManager/Components/Pages/Tracks/Releases.razor
index a2c7df8..851e045 100644
--- a/DeepDrftManager/Components/Pages/Tracks/Releases.razor
+++ b/DeepDrftManager/Components/Pages/Tracks/Releases.razor
@@ -73,14 +73,23 @@
-
+
+
+
+
+
+
+
+
+
+
- @foreach (var medium in Enum.GetValues())
- {
-
- @MediumGrid(medium)
-
- }
@@ -104,9 +113,8 @@
private static string AddTrackHref(ReleaseMedium medium) =>
$"/tracks/upload?medium={medium.ToString().ToLowerInvariant()}";
- // Medium → tab label. The one place medium display text lives for the tab strip; a future medium adds
- // one entry here and surfaces a tab automatically. The ALL tab is rendered separately (it is not a
- // medium).
+ // Medium → tab label. The one place medium display text lives for the tab strip. The ALL tab is
+ // rendered separately (it is not a medium). Tabs are explicit markup so @ref captures work.
private static readonly IReadOnlyDictionary MediumTabLabels =
new Dictionary
{
@@ -115,17 +123,14 @@
[ReleaseMedium.Mix] = "MIXES",
};
- // Medium → embedded grid. Each medium's grid is its own component (Cut has no per-row action; Session
- // carries hero upload; Mix carries waveform generation), so the content dispatch is a per-medium
- // mapping by nature — but it is a single switch returning a fragment, not a markup fork. The browsers
- // render Embedded so their standalone page chrome (container, title, back button) is suppressed here.
- private RenderFragment MediumGrid(ReleaseMedium medium) => medium switch
- {
- ReleaseMedium.Cut => @,
- ReleaseMedium.Session => @,
- ReleaseMedium.Mix => @,
- _ => @No grid for this medium.
- };
+ // @ref handles for the per-tab grids. Used to (a) invalidate their cached per-track waveform status
+ // after a page-level bulk run, and (b) to wire OnWaveformGenerated so per-row generates bubble up
+ // and refresh the page-level missing-count badges. Tabs are now explicit markup rather than the
+ // former enum-driven MediumGrid() switch so @ref captures are possible.
+ private CmsAllReleasesGrid? _allGrid;
+ private CmsCutBrowser? _cutBrowser;
+ private CmsSessionBrowser? _sessionBrowser;
+ private CmsMixBrowser? _mixBrowser;
// EntryKey → HasProfile / HasHighRes, loaded once on init so the bulk buttons can show accurate missing
// counts without depending on any rendered grid. Re-fetched after each bulk run so the counts settle.
@@ -167,6 +172,21 @@
StateHasChanged();
}
+ // Invalidates the cached per-track waveform status on all embedded grids so the next row expand
+ // re-fetches fresh data. Called after each catalogue-wide bulk run so already-expanded rows
+ // reflect the new waveform state on the next expand interaction.
+ private async Task InvalidateAllGridsAsync()
+ {
+ var tasks = new[]
+ {
+ _allGrid?.InvalidateWaveformStatusAsync() ?? Task.CompletedTask,
+ _cutBrowser?.InvalidateWaveformStatusAsync() ?? Task.CompletedTask,
+ _sessionBrowser?.InvalidateWaveformStatusAsync() ?? Task.CompletedTask,
+ _mixBrowser?.InvalidateWaveformStatusAsync() ?? Task.CompletedTask,
+ };
+ await Task.WhenAll(tasks);
+ }
+
///
/// Backfill every track missing a waveform profile, one request at a time so a large backfill does not
/// flood the API with concurrent WAV decodes. On completion, re-reads the status map so the missing
@@ -206,6 +226,7 @@
_bulkRunning = false;
await RefreshWaveformStatusAsync();
+ await InvalidateAllGridsAsync();
var succeeded = missing.Count - failures;
if (failures == 0)
@@ -257,6 +278,7 @@
_highResBulkRunning = false;
await RefreshWaveformStatusAsync();
+ await InvalidateAllGridsAsync();
var succeeded = missing.Count - failures;
if (failures == 0)
diff --git a/DeepDrftManager/Services/CmsTrackService.cs b/DeepDrftManager/Services/CmsTrackService.cs
index a84c971..3e64e10 100644
--- a/DeepDrftManager/Services/CmsTrackService.cs
+++ b/DeepDrftManager/Services/CmsTrackService.cs
@@ -661,50 +661,6 @@ public class CmsTrackService : ICmsTrackService
}
}
- 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
diff --git a/DeepDrftManager/Services/ICmsTrackService.cs b/DeepDrftManager/Services/ICmsTrackService.cs
index 66246d0..d666fa5 100644
--- a/DeepDrftManager/Services/ICmsTrackService.cs
+++ b/DeepDrftManager/Services/ICmsTrackService.cs
@@ -122,9 +122,6 @@ public interface ICmsTrackService
/// Returns all releases with track counts from GET api/track/albums.
Task>> GetReleasesAsync(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.
///