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/TrackEdit.razor b/DeepDrftCms/Pages/Tracks/TrackEdit.razor
new file mode 100644
index 0000000..1757f37
--- /dev/null
+++ b/DeepDrftCms/Pages/Tracks/TrackEdit.razor
@@ -0,0 +1,230 @@
+@page "/cms/tracks/{Id:int}"
+@rendermode InteractiveServer
+@using AuthBlocksWeb.HierarchicalAuthorize
+@using AuthBlocksWeb.Services
+@using DeepDrftWeb.Services
+@using System.Net.Http.Headers
+@using System.Net.Http.Json
+@attribute [HierarchicalRoleAuthorize("Admin")]
+@inject ITrackService TrackService
+@inject IHttpClientFactory HttpClientFactory
+@inject ITokenService TokenService
+@inject ISnackbar Snackbar
+@inject IDialogService DialogService
+@inject NavigationManager Nav
+
+Edit Track — DeepDrft CMS
+
+
+
+ Back to tracks
+
+
+ @if (_loading)
+ {
+
+ }
+ else if (_track is null)
+ {
+
+ Track not found.
+
+ }
+ else
+ {
+ Edit Track
+
+
+
+
+ @_track.EntryKey
+
+ Vault reference — not editable.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Delete
+
+
+
+ Save Changes
+
+
+
+
+ }
+
+
+@code {
+ [Parameter] public int Id { get; set; }
+
+ private TrackEntity? _track;
+ private TrackEditForm _form = new();
+ private bool _loading = true;
+ private bool _busy;
+
+ private bool CanSave =>
+ !string.IsNullOrWhiteSpace(_form.TrackName)
+ && !string.IsNullOrWhiteSpace(_form.Artist);
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadAsync();
+ }
+
+ private async Task LoadAsync()
+ {
+ _loading = true;
+ var result = await TrackService.GetById(Id);
+ _track = result.Success ? result.Value : null;
+ if (_track is not null)
+ {
+ _form = TrackEditForm.From(_track);
+ }
+ _loading = false;
+ }
+
+ private async Task SaveAsync()
+ {
+ if (_track is null || !CanSave) return;
+
+ _busy = true;
+ try
+ {
+ var http = HttpClientFactory.CreateClient("DeepDrft.API");
+ await AttachBearerAsync(http);
+
+ var payload = new
+ {
+ TrackName = _form.TrackName,
+ Artist = _form.Artist,
+ Album = string.IsNullOrWhiteSpace(_form.Album) ? null : _form.Album,
+ Genre = string.IsNullOrWhiteSpace(_form.Genre) ? null : _form.Genre,
+ ReleaseDate = _form.ReleaseDate is { } d ? DateOnly.FromDateTime(d) : (DateOnly?)null
+ };
+
+ var response = await http.PutAsJsonAsync($"api/cms/track/{Id}", payload);
+ if (response.IsSuccessStatusCode)
+ {
+ Snackbar.Add("Track updated.", Severity.Success);
+ await LoadAsync();
+ }
+ else
+ {
+ Snackbar.Add($"Save failed: {(int)response.StatusCode} {response.ReasonPhrase}", Severity.Error);
+ }
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"Save failed: {ex.Message}", Severity.Error);
+ }
+ finally
+ {
+ _busy = false;
+ }
+ }
+
+ private async Task ConfirmDelete()
+ {
+ if (_track is null) return;
+
+ var confirmed = await DialogService.ShowMessageBox(
+ "Delete track",
+ $"Permanently delete \"{_track.TrackName}\" by {_track.Artist}? This cannot be undone.",
+ yesText: "Delete",
+ cancelText: "Cancel");
+
+ if (confirmed != true) return;
+
+ _busy = true;
+ try
+ {
+ var http = HttpClientFactory.CreateClient("DeepDrft.API");
+ await AttachBearerAsync(http);
+
+ var response = await http.DeleteAsync($"api/cms/track/{Id}");
+ if (response.IsSuccessStatusCode)
+ {
+ Snackbar.Add("Track deleted.", Severity.Success);
+ Nav.NavigateTo("/cms/tracks");
+ }
+ else
+ {
+ Snackbar.Add($"Delete failed: {(int)response.StatusCode} {response.ReasonPhrase}", Severity.Error);
+ _busy = false;
+ }
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"Delete failed: {ex.Message}", Severity.Error);
+ _busy = false;
+ }
+ }
+
+ private async Task AttachBearerAsync(HttpClient http)
+ {
+ var token = await TokenService.GetAccessTokenAsync();
+ if (!string.IsNullOrEmpty(token))
+ {
+ http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
+ }
+ }
+
+ private sealed class TrackEditForm
+ {
+ public string TrackName { get; set; } = string.Empty;
+ public string Artist { get; set; } = string.Empty;
+ public string? Album { get; set; }
+ public string? Genre { get; set; }
+ public DateTime? ReleaseDate { get; set; }
+
+ public static TrackEditForm From(TrackEntity track) => new()
+ {
+ TrackName = track.TrackName,
+ Artist = track.Artist,
+ Album = track.Album,
+ Genre = track.Genre,
+ ReleaseDate = track.ReleaseDate is { } d
+ ? d.ToDateTime(TimeOnly.MinValue)
+ : null
+ };
+ }
+}
diff --git a/DeepDrftWeb/Controllers/CmsEditController.cs b/DeepDrftWeb/Controllers/CmsEditController.cs
new file mode 100644
index 0000000..d6f2ec2
--- /dev/null
+++ b/DeepDrftWeb/Controllers/CmsEditController.cs
@@ -0,0 +1,58 @@
+using DeepDrftModels.Entities;
+using DeepDrftWeb.Services;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using NetBlocks.Models;
+
+namespace DeepDrftWeb.Controllers;
+
+[ApiController]
+[Authorize(Roles = "Admin")]
+[Route("api/cms/track")]
+public class CmsEditController : ControllerBase
+{
+ private readonly ITrackService _trackService;
+
+ public CmsEditController(ITrackService trackService)
+ {
+ _trackService = trackService;
+ }
+
+ // Metadata-only update. EntryKey is immutable in Wave 1 — audio replacement
+ // is a separate Wave 2 operation that touches the vault.
+ [HttpPut("{id:int}")]
+ public async Task>> Update(int id, [FromBody] CmsTrackUpdateRequest request)
+ {
+ var existing = await _trackService.GetById(id);
+ if (!existing.Success)
+ {
+ var failure = ApiResult.CreateFailResult(existing.GetMessage());
+ return StatusCode(500, new ApiResultDto(failure));
+ }
+
+ if (existing.Value is null)
+ {
+ return NotFound();
+ }
+
+ var track = existing.Value;
+ track.TrackName = request.TrackName;
+ track.Artist = request.Artist;
+ track.Album = request.Album;
+ track.Genre = request.Genre;
+ track.ReleaseDate = request.ReleaseDate;
+
+ var updated = await _trackService.Update(track);
+ var apiResult = ApiResult.From(updated);
+ var dto = new ApiResultDto(apiResult);
+
+ return updated.Success ? Ok(dto) : StatusCode(500, dto);
+ }
+}
+
+public record CmsTrackUpdateRequest(
+ string TrackName,
+ string Artist,
+ string? Album,
+ string? Genre,
+ DateOnly? ReleaseDate);