From 594fe7a977b78684761d6168c8c5745f22b7da5d Mon Sep 17 00:00:00 2001 From: REDCODE Date: Sat, 22 Aug 2026 00:41:36 +0200 Subject: [PATCH 01/11] feat(Butter): replace asset DeletedAt update field with IsDeleted flag --- Butter/Dtos/Asset/AssetUpdateDto.cs | 6 ++++-- Butter/Dtos/Asset/BulkRestoreResultDto.cs | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 Butter/Dtos/Asset/BulkRestoreResultDto.cs diff --git a/Butter/Dtos/Asset/AssetUpdateDto.cs b/Butter/Dtos/Asset/AssetUpdateDto.cs index d83f04d..8813319 100644 --- a/Butter/Dtos/Asset/AssetUpdateDto.cs +++ b/Butter/Dtos/Asset/AssetUpdateDto.cs @@ -11,9 +11,11 @@ public class AssetUpdateDto { /// public Guid? UploadedBy { get; set; } /// - /// Gets or sets the deletion date to set on the asset. + /// Gets or sets the soft-delete state to apply to the asset. + /// soft-deletes the asset, restores it, + /// and leaves the current state unchanged. /// - public DateTime? DeletedAt { get; set; } + public bool? IsDeleted { get; set; } /// /// Gets or sets the visibility level. /// diff --git a/Butter/Dtos/Asset/BulkRestoreResultDto.cs b/Butter/Dtos/Asset/BulkRestoreResultDto.cs new file mode 100644 index 0000000..f44f943 --- /dev/null +++ b/Butter/Dtos/Asset/BulkRestoreResultDto.cs @@ -0,0 +1,15 @@ +namespace Butter.Dtos.Asset; + +/// +/// Reports the outcome of a bulk restore operation on soft-deleted assets. +/// +public class BulkRestoreResultDto { + /// + /// Gets or sets the IDs of assets that were successfully restored. + /// + public List Restored { get; set; } = []; + /// + /// Gets or sets the IDs of assets that were skipped because their source file no longer exists on disk. + /// + public List SkippedMissingFile { get; set; } = []; +} From a8122ebc392727ef02a1dcf5cae9a698c1e9d3e5 Mon Sep 17 00:00:00 2001 From: REDCODE Date: Sat, 22 Aug 2026 00:43:31 +0200 Subject: [PATCH 02/11] feat(Lactose): support restoring soft-deleted assets via update endpoints --- Lactose/Controllers/AssetController.cs | 55 ++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/Lactose/Controllers/AssetController.cs b/Lactose/Controllers/AssetController.cs index 5cd94c8..f0317c6 100644 --- a/Lactose/Controllers/AssetController.cs +++ b/Lactose/Controllers/AssetController.cs @@ -239,8 +239,18 @@ public class AssetController( asset.Visibility = dto.Visibility ?? asset.Visibility; log += $"Visibility: {asset.Visibility} -> {dto.Visibility ?? asset.Visibility}\n"; - asset.DeletedAt = dto.DeletedAt ?? asset.DeletedAt; - log += $"DeletedAt: {asset.DeletedAt} -> {dto.DeletedAt ?? asset.DeletedAt}\n"; + if (dto.IsDeleted.HasValue) { + // Restoring a soft-deleted asset requires its source file to still exist on disk; + // otherwise the crawl/integrity jobs would silently re-delete it on their next run. + if (!dto.IsDeleted.Value && asset.DeletedAt != null && !System.IO.File.Exists(asset.OriginalPath)) { + logger.LogWarning($"Cannot restore asset {id}: source file '{asset.OriginalPath}' does not exist on disk!"); + return Conflict(); + } + + DateTime? newDeletedAt = dto.IsDeleted.Value ? DateTime.UtcNow : null; + log += $"DeletedAt: {asset.DeletedAt} -> {newDeletedAt}\n"; + asset.DeletedAt = newDeletedAt; + } if (accesslevel == EAccessLevel.Admin) { asset.UploadedBy = dto.UploadedBy ?? asset.UploadedBy; @@ -286,10 +296,17 @@ public class AssetController( ? await FilterForMaintainerAsync(enumerable, uid.Value, cancellationToken) : enumerable.Where(x => x.UploadedBy == uid).ToList(); + if (bulkDto.Data.IsDeleted == false) { + var result = ApplyBulkRestore(enumerable, bulkDto.Data); + assetRepository.UpdateBulk(enumerable); + await assetRepository.SaveAsync(cancellationToken); + return Ok(result); + } + enumerable.ForEach( x => { x.Visibility = bulkDto.Data.Visibility ?? x.Visibility; - x.DeletedAt = bulkDto.Data.DeletedAt ?? x.DeletedAt; + x.DeletedAt = bulkDto.Data.IsDeleted == true ? DateTime.UtcNow : x.DeletedAt; if (accesslevel == EAccessLevel.Admin) { x.UploadedBy = bulkDto.Data.UploadedBy ?? x.UploadedBy; @@ -302,6 +319,38 @@ public class AssetController( return Ok(); } + /// + /// Applies a restore request (IsDeleted = ) to the given assets. + /// Assets whose source file no longer exists on disk are skipped and reported instead of restored, + /// since the crawl/integrity jobs would re-delete them otherwise. All other update fields are still + /// applied to the assets that are not skipped. + /// + /// The assets selected for the bulk update. + /// The shared update values. + /// The outcome listing restored IDs and IDs skipped due to missing files. + private BulkRestoreResultDto ApplyBulkRestore(List assets, AssetUpdateDto data) { + BulkRestoreResultDto result = new(); + + foreach (var x in assets) { + if (x.DeletedAt != null && !System.IO.File.Exists(x.OriginalPath)) { + result.SkippedMissingFile.Add(x.Id); + continue; + } + + bool wasRestored = x.DeletedAt != null; + + x.Visibility = data.Visibility ?? x.Visibility; + x.DeletedAt = null; + + if (wasRestored) result.Restored.Add(x.Id); + } + + if (result.SkippedMissingFile.Count > 0) + logger.LogWarning($"Bulk restore: {result.SkippedMissingFile.Count} asset(s) skipped because their source files are missing on disk"); + + return result; + } + /// /// Soft-deletes an asset by its ID. /// From c95728b097edeffc3284ea3777925c2ef0b6130f Mon Sep 17 00:00:00 2001 From: REDCODE Date: Sat, 22 Aug 2026 00:45:25 +0200 Subject: [PATCH 03/11] feat(Lactose): add deleted-only and album/person filters to asset search --- Butter/Dtos/Asset/AssetSearchOptionsDto.cs | 13 +++++++++++++ Lactose/Controllers/AssetController.cs | 3 ++- Lactose/Repositories/AssetRepository.cs | 8 +++++++- Lactose/Repositories/IAssetRepository.cs | 5 ++++- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/Butter/Dtos/Asset/AssetSearchOptionsDto.cs b/Butter/Dtos/Asset/AssetSearchOptionsDto.cs index 511163c..b7ad336 100644 --- a/Butter/Dtos/Asset/AssetSearchOptionsDto.cs +++ b/Butter/Dtos/Asset/AssetSearchOptionsDto.cs @@ -39,6 +39,19 @@ public class AssetSearchOptionsDto: PagedSearchParametersDto { /// public Guid? UploadedBy { get; set; } /// + /// Gets or sets whether to filter exclusively for soft-deleted assets. + /// Results remain scoped by the requester's access level. + /// + public bool DeletedOnly { get; set; } + /// + /// Gets or sets the album ID to filter assets belonging to that album. + /// + public Guid? AlbumId { get; set; } + /// + /// Gets or sets the person ID to filter assets in albums owned by that person. + /// + public Guid? PersonId { get; set; } + /// /// Gets or sets whether the total matching count should be computed and returned in the X-Total-Count header. /// Skipping it avoids a full count query over the filtered result set. /// diff --git a/Lactose/Controllers/AssetController.cs b/Lactose/Controllers/AssetController.cs index f0317c6..89ab4ab 100644 --- a/Lactose/Controllers/AssetController.cs +++ b/Lactose/Controllers/AssetController.cs @@ -104,7 +104,8 @@ public class AssetController( searchOptionsDto.Page, searchOptionsDto.PageSize, uid, accessLevel, searchOptionsDto.Unlinked, searchOptionsDto.FolderId, searchOptionsDto.UploadedBy, searchOptionsDto.Search, - searchOptionsDto.IncludeCount, cancellationToken + searchOptionsDto.IncludeCount, searchOptionsDto.DeletedOnly, searchOptionsDto.AlbumId, searchOptionsDto.PersonId, + cancellationToken ); logger.LogTrace( diff --git a/Lactose/Repositories/AssetRepository.cs b/Lactose/Repositories/AssetRepository.cs index 95596f8..da3aace 100644 --- a/Lactose/Repositories/AssetRepository.cs +++ b/Lactose/Repositories/AssetRepository.cs @@ -271,7 +271,7 @@ public class AssetRepository(LactoseDbContext context, ILogger } /// - public async Task<(List Assets, int Total)> GetAssetsAsync(EAssetType? type, DateTime? from, DateTime? to, bool orderRandomly, Guid? seed, int pageNumber, int pageSize, Guid? userId, EAccessLevel accessLevel, bool unlinked, Guid? folderId, Guid? uploadedBy, string? search, bool includeCount, CancellationToken cancellationToken) { + public async Task<(List Assets, int Total)> GetAssetsAsync(EAssetType? type, DateTime? from, DateTime? to, bool orderRandomly, Guid? seed, int pageNumber, int pageSize, Guid? userId, EAccessLevel accessLevel, bool unlinked, Guid? folderId, Guid? uploadedBy, string? search, bool includeCount, bool deletedOnly, Guid? albumId, Guid? personId, CancellationToken cancellationToken) { var query = context.Assets.AsNoTracking().AsQueryable(); if (type.HasValue) query = query.Where(a => a.Type == type.Value); @@ -281,6 +281,12 @@ public class AssetRepository(LactoseDbContext context, ILogger query = query.Where(a => a.UpdatedAt <= to.Value); if (unlinked) query = query.Where(a => a.Albums!.Count == 0); + if (deletedOnly) + query = query.Where(a => a.DeletedAt != null); + if (albumId.HasValue) + query = query.Where(a => a.Albums!.Any(al => al.Id == albumId.Value)); + if (personId.HasValue) + query = query.Where(a => a.Albums!.Any(al => al.PersonOwnerId == personId.Value)); if (folderId.HasValue) query = query.Where(a => a.FolderId == folderId.Value); if (uploadedBy.HasValue) diff --git a/Lactose/Repositories/IAssetRepository.cs b/Lactose/Repositories/IAssetRepository.cs index aa926f1..2fc4f0e 100644 --- a/Lactose/Repositories/IAssetRepository.cs +++ b/Lactose/Repositories/IAssetRepository.cs @@ -282,9 +282,12 @@ public interface IAssetRepository : IDisposable { /// Optional uploader user ID to filter assets by their uploader. /// Optional search term for ILike matching against OriginalFilename. /// If true (default), computes the total matching count. When false, the returned total is zero and the count query is skipped. + /// If true, filters exclusively for soft-deleted assets. Results remain scoped by the requester's access level. + /// Optional album ID to filter assets belonging to that album. + /// Optional person ID to filter assets in albums owned by that person. /// Token to cancel the operation. /// A paginated collection of asset previews matching the filters, and the total matching count. - Task<(List Assets, int Total)> GetAssetsAsync(EAssetType? type, DateTime? from, DateTime? to, bool orderRandomly, Guid? seed, int pageNumber, int pageSize, Guid? userId, EAccessLevel accessLevel, bool unlinked, Guid? folderId, Guid? uploadedBy, string? search, bool includeCount, CancellationToken cancellationToken); + Task<(List Assets, int Total)> GetAssetsAsync(EAssetType? type, DateTime? from, DateTime? to, bool orderRandomly, Guid? seed, int pageNumber, int pageSize, Guid? userId, EAccessLevel accessLevel, bool unlinked, Guid? folderId, Guid? uploadedBy, string? search, bool includeCount, bool deletedOnly, Guid? albumId, Guid? personId, CancellationToken cancellationToken); /// /// Returns groups of unlinked assets (not assigned to any album), grouped by folder for drill-down browsing. From 453507a3ed98a46e306b4c9c96181e65d3385a18 Mon Sep 17 00:00:00 2001 From: REDCODE Date: Sat, 22 Aug 2026 00:47:30 +0200 Subject: [PATCH 04/11] test(Lactose): cover asset restore flows in REST Client suite --- Lactose/WepApiTest.http | 144 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/Lactose/WepApiTest.http b/Lactose/WepApiTest.http index 651112f..57d71fc 100644 --- a/Lactose/WepApiTest.http +++ b/Lactose/WepApiTest.http @@ -2178,6 +2178,150 @@ GET {{WepApiTest_HostAddress}}/api/asset/{{testAssetId}} } %} +# --------------------------------------------------------------------------- +# Restore soft-deleted assets (issue #198) +# --------------------------------------------------------------------------- + +### 131.1 Admin soft-deletes the test asset (IsDeleted = true) +POST {{WepApiTest_HostAddress}}/api/asset +Authorization: Bearer {{admin_token}} +Content-Type: application/json + +{ + "ids": ["{{testAssetId}}"], + "data": { "isDeleted": true } +} + +> {% + var hasAssets = client.global.get("hasAssets"); + if (hasAssets === "true") { + client.test("Soft-delete asset via IsDeleted flag", function () { + client.assert(response.status === 200) + }); + } else { + client.test("No assets — skipping soft-delete test", function () { + client.assert(true) + }); + } +%} + +### 131.2 Admin: deletedOnly search lists the soft-deleted asset +GET {{WepApiTest_HostAddress}}/api/asset?deletedOnly=true&page=0&pageSize=50&includeCount=true +Authorization: Bearer {{admin_token}} + +> {% + var hasAssets = client.global.get("hasAssets"); + if (hasAssets === "true") { + client.test("Admin sees deleted assets in deletedOnly search", function () { + client.assert(response.status === 200); + var total = parseInt(response.headers.valueOf("X-Total-Count")); + client.assert(total >= 1, "Expected at least one deleted asset but got total " + total); + }); + } else { + client.test("No assets — skipping deletedOnly search test", function () { + client.assert(true) + }); + } +%} + +### 131.3 Regular user: deletedOnly search returns nothing (R7 ? users never see deleted) +GET {{WepApiTest_HostAddress}}/api/asset?deletedOnly=true&page=0&pageSize=50&includeCount=true +Authorization: Bearer {{user_token}} + +> {% + client.test("Regular user gets empty deletedOnly results", function () { + client.assert(response.status === 200); + client.assert(response.body.length === 0, "Expected no deleted assets for regular user"); + }); +%} + +### 131.4 Curator: deletedOnly search succeeds (scoped to own uploads by R7) +GET {{WepApiTest_HostAddress}}/api/asset?deletedOnly=true&page=0&pageSize=50&includeCount=true +Authorization: Bearer {{curator_token}} + +> {% + client.test("Curator can query deletedOnly search", function () { + client.assert(response.status === 200) + }); +%} + +### 131.5 Anonymous: restore attempt should 401 +POST {{WepApiTest_HostAddress}}/api/asset/{{testAssetId}} +Content-Type: application/json + +{ + "isDeleted": false +} + +> {% + client.test("Anonymous restore attempt is rejected", function () { + client.assert([401, 403].indexOf(response.status) !== -1, "Expected 401/403 but got " + response.status); + }); +%} + +### 131.6 Regular user: restore attempt should 403 +POST {{WepApiTest_HostAddress}}/api/asset/{{testAssetId}} +Authorization: Bearer {{user_token}} +Content-Type: application/json + +{ + "isDeleted": false +} + +> {% + client.test("Regular user restore attempt is rejected", function () { + client.assert(response.status === 403, "Expected 403 but got " + response.status); + }); +%} + +### 131.7 Admin: bulk restore via update endpoint (IsDeleted = false) +# Assets whose source files are missing on disk are reported in skippedMissingFile +# instead of being restored; a bare 200 therefore always indicates a valid outcome. +POST {{WepApiTest_HostAddress}}/api/asset +Authorization: Bearer {{admin_token}} +Content-Type: application/json + +{ + "ids": ["{{testAssetId}}"], + "data": { "isDeleted": false } +} + +> {% + var hasAssets = client.global.get("hasAssets"); + if (hasAssets === "true") { + client.test("Bulk restore reports restored/skipped lists", function () { + client.assert(response.status === 200); + client.assert(jsonPath(response.body, "$.restored") != null, "Missing 'restored' list"); + client.assert(jsonPath(response.body, "$.skippedMissingFile") != null, "Missing 'skippedMissingFile' list"); + }); + var restored = response.body.restored; + client.global.set("assetRestored", restored != null && restored.indexOf(client.global.get("testAssetId")) !== -1 ? "true" : "false"); + } else { + client.global.set("assetRestored", "false"); + client.test("No assets — skipping bulk restore test", function () { + client.assert(true) + }); + } +%} + +### 131.8 Verify restored asset is live again (skipped when its file was missing on disk) +GET {{WepApiTest_HostAddress}}/api/asset/{{testAssetId}} +Authorization: Bearer {{admin_token}} + +> {% + var assetRestored = client.global.get("assetRestored"); + if (assetRestored === "true") { + client.test("Restored asset no longer reports DeletedAt", function () { + client.assert(response.status === 200); + client.assert(response.body.deletedAt === null, "deletedAt should be cleared after restore"); + }); + } else { + client.test("Asset not restored (missing file) — skipping verification", function () { + client.assert(true) + }); + } +%} + # ============================================================================= # CLEANUP # ============================================================================= From c644717808dc0796dd772534e9c8c1376113c44d Mon Sep 17 00:00:00 2001 From: REDCODE Date: Sat, 22 Aug 2026 00:48:38 +0200 Subject: [PATCH 05/11] feat(MilkStream.Client): add deleted-only listing and asset restore to AssetService --- MilkStream.Client/Services/AssetService.cs | 25 +++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/MilkStream.Client/Services/AssetService.cs b/MilkStream.Client/Services/AssetService.cs index a792d64..56a1b95 100644 --- a/MilkStream.Client/Services/AssetService.cs +++ b/MilkStream.Client/Services/AssetService.cs @@ -27,10 +27,14 @@ public sealed class AssetService( /// Optional uploader user ID to filter assets by their uploader. /// Optional search term for filename matching. /// If true, the API also computes the total matching count. Skipping it avoids a full count query. + /// If true, filters exclusively for soft-deleted assets (scoped by access level server-side). + /// Optional album ID to filter assets belonging to that album. + /// Optional person ID to filter assets in albums owned by that person. /// A list of asset previews, or null if the request failed. public Task?> GetAssetsAsync( EAssetType? type = null, bool random = false, Guid? seed = null, int page = 0, int pageSize = PagedParametersDto.MaxPageSize, - bool unlinked = false, Guid? folderId = null, Guid? uploadedBy = null, string? search = null, bool includeCount = true + bool unlinked = false, Guid? folderId = null, Guid? uploadedBy = null, string? search = null, bool includeCount = true, + bool deletedOnly = false, Guid? albumId = null, Guid? personId = null ) { var url = $"/api/asset?page={page}&pageSize={pageSize}&random={random}"; if (type.HasValue) @@ -39,10 +43,16 @@ public sealed class AssetService( url += $"&seed={seed.Value}"; if (unlinked) url += "&unlinked=true"; + if (deletedOnly) + url += "&deletedOnly=true"; if (folderId.HasValue) url += $"&folderId={folderId.Value}"; if (uploadedBy.HasValue) url += $"&uploadedBy={uploadedBy.Value}"; + if (albumId.HasValue) + url += $"&albumId={albumId.Value}"; + if (personId.HasValue) + url += $"&personId={personId.Value}"; if (!string.IsNullOrEmpty(search)) url += $"&search={Uri.EscapeDataString(search)}"; if (!includeCount) @@ -119,4 +129,17 @@ public sealed class AssetService( var response = await Client.SendAsync(request); return response.IsSuccessStatusCode; } + + /// + /// Restores soft-deleted assets by clearing their deletion timestamp. + /// Assets whose source files no longer exist on disk are skipped server-side and reported. + /// + /// The asset IDs to restore. + /// The restore outcome listing restored and skipped IDs, or null if the request failed. + public async Task RestoreAssetsAsync(List ids) { + var payload = new BulkDto { Ids = ids, Data = new AssetUpdateDto { IsDeleted = false } }; + var response = await Client.PostAsJsonAsync("/api/asset", payload); + if (!response.IsSuccessStatusCode) return null; + return await response.Content.ReadFromJsonAsync(); + } } From 4228ab6ca52c53388e2b83eb2f2fd3bc999cf246 Mon Sep 17 00:00:00 2001 From: REDCODE Date: Sat, 22 Aug 2026 00:51:30 +0200 Subject: [PATCH 06/11] feat(MilkStream.Client): restore deleted assets from album edit mode --- .../Components/Pages/AlbumDetail.razor | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/MilkStream.Client/Components/Pages/AlbumDetail.razor b/MilkStream.Client/Components/Pages/AlbumDetail.razor index e2d91be..fe7805a 100644 --- a/MilkStream.Client/Components/Pages/AlbumDetail.razor +++ b/MilkStream.Client/Components/Pages/AlbumDetail.razor @@ -50,6 +50,10 @@ @if (selectMode) { + @if (selectedDeletedCount > 0) { + + } + + @if (showAssetPicker && album != null) { selectedImages = new(); SelectionRange assetSelectionRange = new(); @@ -185,6 +198,19 @@ bool allSelected => album != null && selectedImages.IsAllSelected(album.Images); + // Deleted tiles are only selectable in edit mode; this counts how many of the + // currently selected assets are soft-deleted and thus eligible for restore. + int selectedDeletedCount { + get { + if (album == null || selectedImages.Count == 0) return 0; + var count = 0; + for (var i = 0; i < album.Images.Count && i < album.AssetPreviews.Count; i++) + if (selectedImages.Contains(album.Images[i]) && album.AssetPreviews[i].DeletedAt != null) + count++; + return count; + } + } + void ToggleSelectAll() { if (album == null) return; selectedImages.ToggleAll(album.Images); @@ -337,6 +363,28 @@ await LoadAlbum(); } + void RestoreSelectedImages() { + var ids = album?.AssetPreviews + .Where(p => p.DeletedAt != null && selectedImages.Contains(p.Id)) + .Select(p => p.Id) + .ToList(); + if (ids == null || ids.Count == 0) return; + restoreConfirm.Confirm( + $"Restore {ids.Count} selected asset{(ids.Count == 1 ? "" : "s")}?", + () => ExecuteRestoreSelectedImages(ids) + ); + } + + async Task ExecuteRestoreSelectedImages(List ids) { + if (album == null) return; + await assetService.RestoreAssetsAsync(ids); + selectMode = false; + selectedImages.Clear(); + await LoadAlbum(); + } + + async Task ExecuteRestorePending() => await restoreConfirm.ExecuteAsync(); + void OpenVisibilityModal() => visibility.Open(); async Task ApplyVisibility() { From 9c5bc6ccf7c692f6526cd94175bb3b45f5839728 Mon Sep 17 00:00:00 2001 From: REDCODE Date: Sat, 22 Aug 2026 00:53:38 +0200 Subject: [PATCH 07/11] feat(MilkStream.Client): pass date-range filters through asset search --- MilkStream.Client/Services/AssetService.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/MilkStream.Client/Services/AssetService.cs b/MilkStream.Client/Services/AssetService.cs index 56a1b95..17a53a4 100644 --- a/MilkStream.Client/Services/AssetService.cs +++ b/MilkStream.Client/Services/AssetService.cs @@ -30,11 +30,14 @@ public sealed class AssetService( /// If true, filters exclusively for soft-deleted assets (scoped by access level server-side). /// Optional album ID to filter assets belonging to that album. /// Optional person ID to filter assets in albums owned by that person. + /// Optional start date string (server-side StartDate filter on CreatedAt). + /// Optional end date string (server-side EndDate filter on UpdatedAt). /// A list of asset previews, or null if the request failed. public Task?> GetAssetsAsync( EAssetType? type = null, bool random = false, Guid? seed = null, int page = 0, int pageSize = PagedParametersDto.MaxPageSize, bool unlinked = false, Guid? folderId = null, Guid? uploadedBy = null, string? search = null, bool includeCount = true, - bool deletedOnly = false, Guid? albumId = null, Guid? personId = null + bool deletedOnly = false, Guid? albumId = null, Guid? personId = null, + string? startDate = null, string? endDate = null ) { var url = $"/api/asset?page={page}&pageSize={pageSize}&random={random}"; if (type.HasValue) @@ -53,6 +56,10 @@ public sealed class AssetService( url += $"&albumId={albumId.Value}"; if (personId.HasValue) url += $"&personId={personId.Value}"; + if (!string.IsNullOrEmpty(startDate)) + url += $"&startDate={Uri.EscapeDataString(startDate)}"; + if (!string.IsNullOrEmpty(endDate)) + url += $"&endDate={Uri.EscapeDataString(endDate)}"; if (!string.IsNullOrEmpty(search)) url += $"&search={Uri.EscapeDataString(search)}"; if (!includeCount) From 559e7f1a4647205f36718f4aeff8daa25274a0ec Mon Sep 17 00:00:00 2001 From: REDCODE Date: Sat, 22 Aug 2026 00:56:25 +0200 Subject: [PATCH 08/11] feat(MilkStream.Client): add deleted-assets tab to maintenance page --- .../Components/Pages/Maintenance.razor | 9 +- .../Components/Shared/DeletedAssetList.razor | 280 ++++++++++++++++++ .../Shared/DeletedAssetList.razor.css | 17 ++ 3 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 MilkStream.Client/Components/Shared/DeletedAssetList.razor create mode 100644 MilkStream.Client/Components/Shared/DeletedAssetList.razor.css diff --git a/MilkStream.Client/Components/Pages/Maintenance.razor b/MilkStream.Client/Components/Pages/Maintenance.razor index 8cb65c8..eea6e1e 100644 --- a/MilkStream.Client/Components/Pages/Maintenance.razor +++ b/MilkStream.Client/Components/Pages/Maintenance.razor @@ -16,6 +16,11 @@ Broken Assets +