- Show percentage of total assets on missing metadata/thumbnail/preview/phash cards
- Add stale counts for thumbnails and previews (generated at wrong resolution vs current settings)
- Add h-100 to all completeness cards for consistent row heights
- AuthController: fix HTTP 100 Continue → 403 Forbidden for banned/disabled
users in both Login and RefreshToken endpoints
- LoginService.Login: return error message from AuthResultDto on failure
instead of a generic message
- LoginService.Reauthenticate: parse AuthResultDto on refresh failure;
if "User is banned" or "User is disabled", trigger force logout via
new ForceLogout event, setting ForceLogoutReason property
- MainLayout: subscribe to ForceLogout event, redirect to /login
- Login.razor: display specific error messages (ban reason, force-logout
reason) instead of generic "Login failed"
Refs #45
- UserController.Create was assigning userCreateDto.Password directly without
hashing via IPasswordHasher, unlike the Update method and AuthController
- Moved duplicate email/username checks before User object instantiation
Refs #45
- Add AdminUsers.razor page at /Users with user listing, ban/unban toggle,
access level change, delete, and create user functionality
- Extend UserService with GetAllUsersAsync, GetUserByIdAsync,
UpdateUserAsync, DeleteUserAsync, CreateUserAsync methods
- Add Users link to NavMenu dropdown for admin users
- Fix ban/unban bug in UserController where BannedAt was only updated
if the user had been previously banned
Closes#45
Album search for User role used asset.IsPubliclyShared and
SharedWith checks without filtering soft-deleted assets. A deleted
public asset would cause its album to appear in search results.
Added asset.DeletedAt == null checks to both filter paths.
Refs: #11
The User-role asset search filter had two bugs:
1. DeletedAt != null was negated — shared non-deleted assets were
excluded, while shared deleted assets passed the filter.
2. DeletedAt check was applied only to the shared leg — public
deleted assets leaked through the filter.
Corrected to: not-deleted AND (public OR shared), matching the
logic used by GET /api/asset/{id}.
Refs: #11
- AlbumController GET {id}: exclude soft-deleted assets for User role
(public/owned and shared paths) and Curator role (viewed all assets)
- AssetController GET: add explicit parentheses to DeletedAt operator
grouping in search filter to match intent
- MediaController: deny Curators direct file access to deleted assets;
extract CanAccessAssetDirectly helper method
Refs: #11
- Add AlbumIds parallel list to AssetPreviewDto
- Populate AlbumIds in AssetsMapper
- Home tile hover: album names link to /albums/{id} with onclick:stopPropagation
- ImagePreview ChildContent: album names link to /albums/{id}
- CSS: tile-info-album and preview-info-album support <a> tags with hover underline
- Add one-time migration SQL to set CoverAssetId for existing albums
- AlbumController.Create auto-sets cover from first asset if not specified
- CreateAlbumsJob assigns cover images after asset assignment phase
- Stats: add AlbumsMissingCover and CosplayersMissingCover counters
- Create ImagePreview.razor component with self-contained url generation, keyboard handling, and download
- Add Filename to AssetPreviewDto and AlbumAssetPreviewDto DTOs
- Update AssetsMapper and AlbumMapper to populate FileName
- Remove duplicate preview overlay HTML/CSS/code from Home.razor and AlbumDetail.razor
- Fix AssetDto.FileName hiding inherited member from AssetPreviewDto
- Add missing Save() calls to AlbumController Update and Delete
- Wire CoverAssetId in AlbumController Create/Update/BulkUpdate
- Sort album assets by OriginalFilename in Get endpoint
- Implement PersonController.GetAll returning non-deleted people
- Guard MediaController GetThumb/GetPreview against empty paths (return 404)
- Add CoverAssetId FK and CoverAsset navigation to Album model
- Add AlbumAssetPreviewDto with resolution/thumbnail info
- Extend AlbumPreviewDto/FullDto with PersonName, CoverAssetId, AssetCount
- Add CoverAssetId to AlbumCreateDto and AlbumUpdateDto
- Update AlbumMapper to populate new fields from eager-loaded relations
- Add EF migration for CoverAssetId column
Replace the fixed GUID-based ordering (OrderBy(a => a.Id)) with a
client-generated seed that produces a truly different random order on
each page refresh. The seed is passed to the server and used to
deterministically shuffle all asset IDs via a hash function, ensuring
consistent pagination with no duplicates across pages within a session.
Also fix LoadPreviousPage showing a spinner when already at page 1 by
adding loadedMinPage <= 1 to the guard clause.
Replace manual childJobs.ForEach(j => j.Cancel()) with linked
CancellationTokenSources. Master jobs now call LinkParentToken(token)
on each child, so when the master's CTS fires, all children (running
and queued) receive cancellation automatically via their linked token.
Cancelling a master job now:
- Immediately signals all Running children via their linked CTS
- Ensures queued children, when started by JobManager, receive an
already-cancelled token and bail without processing
- Eliminates the collection-modification-during-ForEach race condition
Affects: PHashJob, ThumbnailJob, PreviewJob, MetadataJob,
FileSystemCrawlJob.
- Add AlbumNames and CosplayerNames fields to AssetPreviewDto
- Include Albums and PersonOwner in AssetRepository.GetAssets() query
- Populate new fields in AssetsMapper.ToAssetPreviewDto()
UsersMapper.ToGetUsersDto() was not copying LastLogin, BannedAt, or
UpdatedAt from the User entity to the UserInfoDto, causing the client
to always receive null for these fields. Added the missing mappings.
Also added null fallback ('Never') for LastLogin display on the User
page to prevent NullReferenceException when the value has not been set.
Job.Cancel() now handles queued jobs (cts null) by setting Canceled status and firing events directly. ContinueWith checks t.IsCanceled to catch ThrowIfCancellationRequested patterns. cts is now disposed after job completion. CreatePersonsJob, CreateAlbumsJob, and IntegrityCheckJob now catch OperationCanceledException and set proper Canceled state. IntegrityCheckJob also gained cancellation checks between each step and within iteration loops.
MetadataJob, ThumbnailJob, PreviewJob, and PHashJob previously only
canceled the master job's own token, leaving already-enqueued sub-jobs
running indefinitely. Added child-job tracking with proper cancellation
propagation at both master cancellation exit points, matching the
existing pattern in FileSystemCrawlJob.