From 13fbcc2d43e5daadda61b199f0f3a0c580647e8f Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Wed, 17 Jun 2026 14:13:34 -0400 Subject: [PATCH] fix: restore waveform status coherence, drop dead GetGenreSummaries, restore track info tooltip --- .../Pages/Tracks/CmsAlbumBrowser.razor | 43 +++++++++++++ .../Pages/Tracks/CmsAllReleasesGrid.razor | 20 +++++- .../Pages/Tracks/CmsCutBrowser.razor | 21 +++++- .../Pages/Tracks/CmsMixBrowser.razor | 19 +++++- .../Pages/Tracks/CmsSessionBrowser.razor | 19 +++++- .../Components/Pages/Tracks/Releases.razor | 64 +++++++++++++------ DeepDrftManager/Services/CmsTrackService.cs | 44 ------------- DeepDrftManager/Services/ICmsTrackService.cs | 3 - 8 files changed, 159 insertions(+), 74 deletions(-) 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. ///