- Rename the filesystem browse route from /api/asset/fs-browse to /api/asset/directory
(controller route, client URL, REST tests, and service log label).
- Remove the old /api/asset/browse endpoint and its BrowseAssets/GetDirectoryNames
repository code, plus the client BrowseAsync method. Repoint AlbumAssetPicker's
admin/curator folder browse to the /directory endpoint.
- Drop the now-unused pg_catalog.split_part DbFunction mapping.
- Refresh .env GIT_VERSION.
- Classify .gif as EAssetType.Video via MimeTypes (moved image/gif to the Video registry)
- Add MediaRepository.GetConvertedData, served by the existing media/{id} endpoint
when a converted file exists on disk (with a fallback + warning log if missing)
- Expose the converted MIME type on AssetPreviewDto/AlbumAssetPreviewDto
so the frontend can detect video assets from a single MimeType field
Adds a TotalBrokenAssets stat aggregated from the existing asset query and
surfaces it on the stats page as a danger card linked to the maintenance
page.
Refs #180
Add ProcessFailedAt/ProcessErrorMessage to Asset plus a filtered index.
All 'missing' queries now exclude broken assets so failed images are not
re-processed indefinitely. Thumbnail/Preview/PHash/Metadata jobs mark the
asset broken on failure via a shared helper. AssetPreviewDto now carries
the broken state for the maintenance report.
Refs #180
Removes the per-request SetCommandTimeout mutation on the shared scoped
DbContext (flagged in review as a minor smell). The 120s command timeout
is now configured once on the Npgsql connection string, which also
covers other potentially slow maintenance/scan queries. The no-album /
no-person anti-joins no longer need to temporarily mutate and restore
the context's timeout.
Refs #173
Addresses review findings on PR #178:
1. Backend: AssetsWithNoAlbum / AssetsWithNoPerson now exclude
soft-deleted assets. The COUNT(DISTINCT AlbumAsset.AssetsId) previously
included deleted assets, which (since TotalAssets only counts live
assets) inflated the in-album count and undercounted orphans. The
distinct count is now restricted to live asset IDs via an
Id IN (SELECT Id FROM Assets WHERE DeletedAt IS NULL) predicate.
2. Frontend: StatChart now re-renders when its data parameters change
instead of only on first render, so the Refresh button updates the
four Chart.js charts alongside the tables/cards.
Refs #173, #177
AssetsWithNoAlbum / AssetsWithNoPerson still exceeded the 30s command
timeout even after querying the AlbumAsset join table directly, because
a count(*) anti-join against the full Assets table is inherently
O(assets).
Rewrite both as computed differences from the already-known TotalAssets:
- AssetsWithNoAlbum = TotalAssets - COUNT(DISTINCT AlbumAsset.AssetsId)
- AssetsWithNoPerson = TotalAssets - COUNT(DISTINCT AlbumAsset.AssetsId
WHERE AlbumsId in person-owned albums)
These are single-table aggregate scans over the indexed join table
(no anti-join against Assets), clamped to >= 0. A 120s command timeout
is applied around these two queries as a safety net and restored
afterwards so other repos are unaffected.
Also adds OrderBy to the grouped FirstOrDefaultAsync to silence the
FirstWithoutOrderByAndFilter warning.
Refs #173
AssetsWithNoAlbum / AssetsWithNoPerson previously anti-joined through
Albums -> AlbumAsset -> Assets, which generated a large correlated join
that exceeded the 30s DB command timeout at scale.
Rewrite both to anti-join against the AlbumAsset join table directly,
using its existing AssetsId index:
- assets with no album: Assets.Id NOT IN (SELECT AlbumAsset.AssetsId)
- assets with no person: Assets.Id NOT IN (SELECT AlbumAsset.AssetsId
WHERE AlbumsId in person-owned albums)
This avoids touching the full Assets table in the subquery and lets the
planner use a hash anti-join instead of a nested loop.
Refs #173
Caches the assembled StatsDto in IMemoryCache with a 60s TTL. The cache
key is fingerprinted with the ThumbnailSize/PreviewSize setting values so
changing those settings immediately invalidates the stale counts on the
next request. Registers AddMemoryCache() in Program.cs.
Refs #175
Rewrites StatsRepository to be fully async and collapses the ~30
synchronous round-trips into a bounded set of aggregated queries:
- Single grouped pass over non-deleted assets computes totals, storage,
visibility breakdown, orphans, 7/30-day activity, and all
data-completeness counts (missing metadata/thumbnail/preview/phash).
- AssetsByType + StorageByType come from one GROUP BY on Type.
- UsersByAccessLevel, TotalUsers, and users registered in 30 days come
from one GROUP BY on AccessLevel.
- MIME breakdown and resolution buckets are now GROUP BY'd in SQL; the
resolution bucket key is a translatable CASE expression on the max
dimension, so no more loading every asset's dimensions into memory.
- AssetsWithNoAlbum / AssetsWithNoPerson use set-based NOT IN anti-joins
over the Album.Assets navigation instead of per-row NOT EXISTS.
- TopTags uses the Tag.Assets navigation (join table indexed on TagsId).
- Drops IDisposable/Dispose() so the transient repo no longer disposes
the shared scoped DbContext.
- StatsController action is now async.
StatsDto shape is unchanged, so the existing .http tests remain valid.
Refs #173
Random ordering now uses a pg_catalog.md5-based deterministic shuffle in SQL
instead of materializing every matching asset ID in memory. The listing
projects directly to AssetPreviewDto (no full entity transfer) with album and
cosplayer names via one grouped query. includeCount=false skips the count
query entirely.
FindVisible filters and projects assets at the database level instead of
loading full entities then filtering/sorting in memory. Find no longer
eager-loads assets; FindWithAssets covers the update path. SearchQuery gains
personOwnerId filter and AsNoTracking.
No longer loads the person's full album/asset graph into memory; uses ID-first
paging, ILike search against the trgm GIN index, and DB-level counts. Slims
Find() to a PK lookup for write paths.
- Extract PathFromGuid to shared PathUtils.cs (PreviewJob + ThumbnailJob)
- Consolidate RegexHighlighter duplicate methods via delegation
- Fix default port 3306 -> 5432 in appsettings.json + docker-compose.yml
- Remove port gotcha section from AGENTS.md
- Fix ITagRepository XML docs after parameter rename (id -> tag)
- Remove 16 unused using System; imports from migration files (implicit usings available)
- Fix 6 empty catch blocks with proper exception logging
- Remove unused import (false positive flagged as wontfix)
- Skip test coverage, orphaned, and stale exclude issues as false positives
- Add missing namespace to IPersonRepository.cs
- Rename IFolderRepository Create/Delete to Insert/Remove for CRUD consistency
- Fix ITagRepository.Delete parameter name from 'id' to 'tag'
- Add missing IDisposable to IMediaRepository
- Rename SettingsExtensions to SettingsExtension for naming consistency
- Rename PagedParametersDTO.cs to PagedParametersDto.cs
- Add .desloppify/ to .gitignore
Add AlbumTotalCount [NotMapped] to Person model, set before pagination in FindVisible, read in PersonMapper instead of paginated Albums.Count. Also apply inline aspect-ratio only in masonry mode so grid mode uses CSS aspect-ratio: 1 for square crops.
UserController.Delete was calling userRepository.Delete(user) which performed
a hard delete (context.Users.Remove), violating the project's soft-delete
convention. Changed to set user.DeletedAt = DateTime.UtcNow, matching the
pattern used in AssetController.Delete.
Also removed the now-unused Delete method from IUserRepository and
UserRepository for consistency with AssetRepository (which also has no
Delete method).
Extended REST tests 103-104 to verify deletedAt is set after deletion.
Closes#130
Adds userId.HasValue guard to the private-visibility condition in both
AssetRepository and MediaRepository, preventing the null==null match
when an anonymous visitor (userId=null) encounters an asset with null
UploadedBy.
Backend:
- Add DeletedAt to AssetPreviewDto, AlbumAssetPreviewDto, AlbumPreviewDto
- Remove redundant DeletedAt from AssetDto (now inherited from base)
- Add viewerId parameter to ToAssetPreviewDto, ToAlbumPreviewDto,
ToAlbumFullDto, ToPersonDetailedDto mappers
- Conditionally send DeletedAt only when viewer is Admin or asset uploader
- Pass uid/viewerId from all controller/repository call sites
Frontend:
- AlbumCard: show orange (not public) / red (deleted) border in select
mode for admins/curators
- AlbumDetail: same border logic in GetTileClass for asset tiles
- Borders only appear in select mode per REDCODE's feedback