Use Bootstrap's recommended pattern: .row.g-2 container with col-* grid
classes for item widths, Masonry.js with percentPosition:true for
vertical positioning. Extracted infinite scroll observers into separate
infiniteScroll.js. Removed custom column-width JS, imagesLoaded CDN,
and the old masonryObserver.js.
- Hide Email and Created columns below md breakpoint (d-none d-md-table-cell)
- Hide Last Login below lg breakpoint (d-none d-lg-table-cell)
- Replace action btn-group with dropdown menu on mobile (d-sm-none)
- Reduces table from 7 visible columns to 4 on mobile
- Settings: stack sidebar vertically on mobile, fix grid minmax 380->280px, use dvh
- AlbumDetail: add mobile breakpoints for grid, restructure header actions
- ImagePreview: switch info bar to column layout on mobile
- User profile: fix col-2/col-2 to responsive col-5/col-7 breakpoints
- AdminUsers: wrap inline editing controls with flex-wrap on mobile
- Jobs: collapse 8 action buttons into dropdown on mobile
- Albums: add flex-wrap to header, hide 'New Album' text on mobile
- NavMenu: responsive authenticated area (column on mobile, btn-group-sm)
- Empty-state messages: m-5 -> m-2 m-sm-5
- Footer: hide on mobile (d-none d-sm-flex)
- Login: remove orphaned 'Remember me' checkbox
- 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
- LoginService.InitializeAsync: after fetching LoggedUser, check IsBanned
and DeletedAt. If banned/disabled, set ForceLogoutReason, call Logout
to clear state, and return false
- App.razor: after InitializeAsync completes, check ForceLogoutReason
and navigate to /login if set. This handles the timing issue where
MainLayout isnt created yet when the event fires on page load
Refs #45
- 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
- Moved error/success alerts inside the main content block (else branch)
so validation errors (empty email, username, etc.) do not hide the table
- Added dismissible error alert with ClearError method
- Added clearError method to code-behind
- Fixed broken @if(_showCreateForm) guard for create form
Refs #45
- Username and email columns now have pencil-to-edit with confirm/cancel
- Actions column gains a key button to change password inline
- Validation: empty username/email/password rejected with error message
- Unified CancelFieldEdit clears all edit modes at once
Refs #45
- onfocusout on the wrapper span was firing when focus moved to child buttons,
cancelling the edit before the button onclick handlers ran
- Confirm/cancel now works via explicit button clicks only
- Starting a new edit on a different row implicitly cancels the previous one
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
- Role dropdown now uses explicit confirm (checkmark) / cancel (X) buttons
instead of auto-committing on selection
- Clicking outside the dropdown cancels the edit (onfocusout)
- Selecting the same role as current is a no-op
- Admins cannot demote themselves below Admin level
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
- JS masonry sets position/width/left/top/height as inline styles on tiles
- Grid CSS uses !important to override inline styles for proper layout
- Force StateHasChanged on mode switch to ensure re-render
- Button group in header: masonry (columns), grid (square tiles), list (rows)
- CSS-driven mode switching via album-grid.masonry/.grid/.list classes
- List mode: horizontal card with thumbnail left, info right
- New justifiedLayout.apply JS function: uniform row heights, automatic widths
- Reads aspect-ratio from tile inline style, fills each row to container width
- AlbumDetail calls justifiedLayout in OnAfterRenderAsync
- Albums page stays on masonryLayout
- Add masonryLayout.apply JS function using shortest-column-next algorithm
- Both Albums and AlbumDetail call masonryLayout on each render
- Items flow left-to-right in alphabetical order, each keeping its own aspect-ratio height
- Remove CSS column-count in favor of absolute positioning by JS
- Home.DisposeAsync now unlocks body scroll so navigation to other pages works
- Albums and AlbumDetail unlock body scroll on init as defense
- Reorder items with column-major index mapping so CSS columns display left-to-right
- Switch both pages back to column-count CSS for reliable masonry
- Add pageLayout.columnCount JS helper matching media query breakpoints
- CSS Grid forces uniform row heights, cropped landscape images next to portraits
- Flexbox with align-items:flex-start preserves each tile's own aspect-ratio height
- align-content:flex-start ensures container height grows for scrolling
- Replace column-count CSS masonry with flex-wrap layout
- Tiles flow left-to-right first, then wrap to next row
- Responsive column counts: 2/3/4/5 per row
- 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
- Use ::deep in ImagePreview.razor.css so preview-info-cosplayer/album styles pierce CSS isolation boundary into ChildContent
- Add missing @ prefix to Filename parameters so they evaluate as C# expressions instead of literal strings
- 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