Mirror BatchUpload.OnMediumChanged: switching to Session/Mix trims the
track list to one row and hides the add-track affordance, enforcing the
§9.3 single-track invariant on the edit path. Cut releases unchanged.
Session/Mix browsers share base (load/state/thumb) and a shared table shell carrying the per-row Edit link to BatchEdit; subclasses supply only their medium action.
Renames Genre tab to Release Archive with switch-free medium card group
(Enum.GetValues-driven). Adds MediumFields single dispatch + CutFields/SessionFields/
MixFields per-medium sections embedded by all five upload/edit forms. BatchUpload
enforces single-track invariant for Session/Mix. Adds CmsSessionBrowser (hero-image
upload) and CmsMixBrowser (waveform status + per-row Generate trigger).
ICmsReleaseService/CmsReleaseService wraps api/release endpoints.
Note: medium selector is forward-compat only — API write path pending.
- Add CmsTrackBrowserViewModel.Invalidate(); called from TrackEdit/BatchEdit on save or delete so album/genre cache is invalidated and re-fetches on next mode switch
- CmsAlbumBrowser now handles 0-track releases: confirm dialog + DeleteReleaseAsync instead of early return; partial-failure path also fires OnReleasesChanged to trigger cache invalidation
- TrackList.OnAlbumsChanged now calls VM.Invalidate() so genres stay fresh after any album delete
- UnifiedTrackService.DeleteAsync cascades release soft-delete when last live track is removed (non-fatal; logs on failure)
- New DELETE api/track/release/{id} endpoint (ApiKeyAuthorize) for direct release soft-delete
- EF migration SoftDeleteOrphanedReleases backfills existing orphaned release rows via raw SQL (data-only, no schema change)
GET api/track/waveform-status and POST api/track/{id}/waveform (ApiKey);
CmsTrackService methods; TrackPreProcessing page with per-row and
sequential bulk generation; nav links from TrackList and Index.
AddAuthBlocks installs JwtBearer as the default challenge scheme; the
authorization middleware 401s unauthenticated nav requests before the
Blazor router runs. Tokens live in localStorage and are only readable
via JS interop after the SignalR circuit is live.
- Program.cs: MapRazorComponents .AllowAnonymous() so nav reaches the
Blazor router; API surfaces (MapAuthBlocks, MapControllers) still
enforce JWT. Fix middleware order to UseAuthentication -> UseAntiforgery
-> UseAuthorization per Blazor Web App template.
- App.razor: InteractiveServerRenderMode(prerender:false) on Routes and
HeadOutlet so AuthorizeRouteView evaluates after JS interop is ready;
extract to static field (was two inline allocations per render cycle).
- CmsLayout/Pages: drop conflicting per-component @rendermode directives
(parent now owns the render mode).
- Routes.razor: break authenticated-but-wrong-role redirect loop; split
NotAuthorized into unauthenticated -> RedirectToLogin and
authenticated-wrong-role -> RedirectToAccessDenied (new component).
- Pages/Index.razor: deleted — NavigateTo('/cms') was unreachable for
unauthenticated users and racey for authorized ones.
TrackCard, TracksGallery, DDIcons, DeepDrftPalettes (Default+Cms), DeepDrftFontLinks,
and palette CSS tokens extracted. Both hosts and DeepDrftCms reference the shared RCL.
InteractiveServer only, full AuthBlocks, no WASM. Controllers scaffolded for future
CMS controller migration. CmsStealthRoutingHandler omitted by design (subdomain topology).