diff --git a/DeepDrftManager/Components/Pages/Tracks/AlbumHeaderFields.razor b/DeepDrftManager/Components/Pages/Tracks/AlbumHeaderFields.razor
index 89c70e9..113c85d 100644
--- a/DeepDrftManager/Components/Pages/Tracks/AlbumHeaderFields.razor
+++ b/DeepDrftManager/Components/Pages/Tracks/AlbumHeaderFields.razor
@@ -66,6 +66,9 @@
@@ -85,6 +88,15 @@
[Parameter] public IBrowserFile? SelectedImageFile { get; set; }
[Parameter] public EventCallback SelectedImageFileChanged { get; set; }
+ // Session-only — the held hero-image file, threaded through MediumFields to SessionFields.
+ // Ignored for Cut/Mix media. The parent (BatchUpload) owns it and uploads it after create.
+ [Parameter] public IBrowserFile? HeroImageFile { get; set; }
+ [Parameter] public EventCallback HeroImageFileChanged { get; set; }
+
+ // Gates the hero file picker in SessionFields (threaded to MediumFields → SessionFields).
+ // Set true only on the BatchUpload create path; leave false/absent on all edit paths.
+ [Parameter] public bool AllowHeroUpload { get; set; }
+
// BatchEdit only: when set (and no new file picked), preview the release's current cover.
// The parent nulls this to drop the preview when the admin clears the existing cover.
[Parameter] public string? ExistingImagePath { get; set; }
diff --git a/DeepDrftManager/Components/Pages/Tracks/BatchUpload.razor b/DeepDrftManager/Components/Pages/Tracks/BatchUpload.razor
index 76ce17d..66ee1ea 100644
--- a/DeepDrftManager/Components/Pages/Tracks/BatchUpload.razor
+++ b/DeepDrftManager/Components/Pages/Tracks/BatchUpload.razor
@@ -26,6 +26,8 @@
Medium="_medium"
MediumChanged="OnMediumChanged"
@bind-SelectedImageFile="_selectedImageFile"
+ @bind-HeroImageFile="_heroImageFile"
+ AllowHeroUpload="true"
Disabled="_uploading" />
@if (_medium == ReleaseMedium.Cut)
@@ -71,6 +73,11 @@
@_errorMessage
}
+ @if (!string.IsNullOrEmpty(_warningMessage))
+ {
+ @_warningMessage
+ }
+
0)
@@ -336,6 +364,43 @@
}
}
+ // Session hero image is resource-addressed, so it is uploaded here — after the
+ // release exists and we have its id — within the same submit gesture. Non-blocking:
+ // the Session is persisted; a failed hero upload is recoverable from the Sessions
+ // browser's per-row Set/Replace hero action.
+ if (_medium == ReleaseMedium.Session
+ && _heroImageFile is { } heroFile
+ && result.Value.ReleaseId is { } sessionReleaseId)
+ {
+ try
+ {
+ await using var heroStream = heroFile.OpenReadStream(maxAllowedSize: 50_000_000);
+ var heroResult = await CmsReleaseService.UploadSessionHeroImageAsync(
+ sessionReleaseId, heroStream, heroFile.Name, heroFile.ContentType);
+ if (!heroResult.Success)
+ {
+ var heroError = heroResult.Messages.FirstOrDefault()?.Message ?? "Unknown error";
+ Logger.LogWarning("Batch upload: hero image upload failed for release {ReleaseId} ('{TrackName}'): {Error}",
+ sessionReleaseId, row.TrackName, heroError);
+ Snackbar.Add("Session uploaded, but the hero image failed. Set it from the Sessions browser.", Severity.Warning);
+ }
+ }
+ catch (Exception heroEx)
+ {
+ Logger.LogError(heroEx, "Batch upload: exception uploading hero image for release {ReleaseId}", sessionReleaseId);
+ Snackbar.Add("Session uploaded, but the hero image failed. Set it from the Sessions browser.", Severity.Warning);
+ }
+ }
+ else if (_medium == ReleaseMedium.Session && _heroImageFile is not null)
+ {
+ // ReleaseId was null on a Session track result — internal inconsistency.
+ // Hero file is held but cannot be uploaded without a release id; log and
+ // surface so the admin can set it from the Sessions browser.
+ Logger.LogWarning("Batch upload: Session track '{TrackName}' (id={Id}) has no ReleaseId — hero image dropped",
+ row.TrackName, result.Value.Id);
+ Snackbar.Add("Session uploaded, but the hero image could not be linked (no release id). Set it from the Sessions browser.", Severity.Warning);
+ }
+
// Mix uploads fire the server-side high-res waveform trigger (§3.4). The CMS
// computes nothing — the API derives the datum from the audio it just stored.
// Non-blocking: the track is persisted; a failed trigger is recoverable from
diff --git a/DeepDrftManager/Components/Pages/Tracks/MediumFields.razor b/DeepDrftManager/Components/Pages/Tracks/MediumFields.razor
index 45a5cde..6e24668 100644
--- a/DeepDrftManager/Components/Pages/Tracks/MediumFields.razor
+++ b/DeepDrftManager/Components/Pages/Tracks/MediumFields.razor
@@ -1,4 +1,5 @@
@using DeepDrftModels.Enums
+@using Microsoft.AspNetCore.Components.Forms
@* The single dispatch point for medium-conditional form fields. All five upload/edit forms embed this
one component; the @switch below is the ONLY place medium-specific form shape is decided. Adding a
@@ -26,7 +27,10 @@
Disabled="Disabled" />
break;
case ReleaseMedium.Session:
-
+
break;
case ReleaseMedium.Mix:
@@ -42,5 +46,13 @@
[Parameter] public ReleaseType ReleaseType { get; set; } = ReleaseType.Single;
[Parameter] public EventCallback ReleaseTypeChanged { get; set; }
+ // Session-only — the held hero-image file, uploaded after create. Ignored for Cut/Mix.
+ [Parameter] public IBrowserFile? HeroImageFile { get; set; }
+ [Parameter] public EventCallback HeroImageFileChanged { get; set; }
+
+ // Gates the hero file picker in SessionFields. True on the BatchUpload create path;
+ // false/absent on all edit paths so SessionFields falls back to the guidance alert.
+ [Parameter] public bool AllowHeroUpload { get; set; }
+
[Parameter] public bool Disabled { get; set; }
}
diff --git a/DeepDrftManager/Components/Pages/Tracks/SessionFields.razor b/DeepDrftManager/Components/Pages/Tracks/SessionFields.razor
index 227dc7c..cee4777 100644
--- a/DeepDrftManager/Components/Pages/Tracks/SessionFields.razor
+++ b/DeepDrftManager/Components/Pages/Tracks/SessionFields.razor
@@ -1,10 +1,68 @@
-@* Session-medium fields. The hero image is resource-addressed (POST api/release/{id}/session/hero-image)
- and therefore set after the release exists — managed per-row in the Sessions browser, not at create
- time when no release id is yet assigned. This section states that contract so the admin knows where
- the hero image is managed; it carries no input of its own. *@
-
-
- Sessions are single-track live releases. After upload, set the hero image from the
- Release Archive → Sessions browser.
-
-
+@using Microsoft.AspNetCore.Components.Forms
+
+@* Session-medium fields. When AllowHeroUpload is true (BatchUpload create path), the hero image
+ file picker is shown — the file is held by the parent and POSTed after the release is created.
+ When false (edit paths: BatchEdit, TrackEdit, TrackNew), the original guidance alert is rendered
+ instead, directing the admin to the Sessions browser per-row replace action. This gate prevents
+ a dead/inert control on edit forms that do not wire the hero callbacks. *@
+@if (AllowHeroUpload)
+{
+
+
+
+
+ Sessions are single-track live releases. The hero image is the session's primary visual identity.
+
+
+ @if (HeroImageFile is { } selectedHero)
+ {
+
+ Selected: @selectedHero.Name
+
+
+ }
+ else
+ {
+ No hero image — optional, but recommended.
+ }
+
+
+ @if (HeroImageFile is not null)
+ {
+ Will upload on submit.
+ }
+
+
+
+}
+else
+{
+
+
+ Sessions are single-track live releases. After upload, set the hero image from the
+ Release Archive → Sessions browser.
+
+
+}
+
+@code {
+ [Parameter] public IBrowserFile? HeroImageFile { get; set; }
+ [Parameter] public EventCallback HeroImageFileChanged { get; set; }
+ [Parameter] public bool Disabled { get; set; }
+
+ // When true (BatchUpload create path), render the hero file picker.
+ // When false/absent (edit paths), render the guidance alert directing the admin to the
+ // Sessions browser — no dead control where callbacks are unwired.
+ [Parameter] public bool AllowHeroUpload { get; set; }
+
+ private Task HandleHeroFileSelected(InputFileChangeEventArgs e) =>
+ HeroImageFileChanged.InvokeAsync(e.File);
+
+ private Task ClearHeroFile() =>
+ HeroImageFileChanged.InvokeAsync(null);
+}