d-none d-sm-flex hides the progress bar but also changes its
display from inline to flex on desktop, breaking visibility.
Remove the hide-class — the progress bar is always shown.
SettingBox base now uses JS interop to call showToast() which
renders a Bootstrap Toast at the bottom-right corner. Removed
all SaveIndicator RenderFragments and IsSaving/IsSaved state
from the four child components. Toast auto-hides after 2s.
Remove d-flex justify-content-between from all card bodies.
Description always sits above the input/control row, and the save
indicator sits inline on the same row as the control. Removed
mobile-specific stacking workaround and save-indicator CSS class
since layout shift is impossible with this structure.
Always render the save indicator container with a fixed 5rem min-width,
only toggle the inner text/color. Previously the conditional Rendering
caused the element to appear-disappear-resize, shifting nearby inputs.
Add .save-indicator CSS class with fixed min-width (4rem) and
text-align:right. Apply to all four setting box types so
'Saving...'→'Saved' text change doesn't shift adjacent
controls.
On ≤576px viewports, switch card body from side-by-side to
stacked layout: description text on top, input/slider
control below. Prevents text clipping and control overflow.
Hide non-essential columns (child summary, dates/progress bar,
elapsed time) on mobile using d-none d-sm-inline/d-sm-flex.
Add flex-wrap to main row for overflow safety on very
narrow viewports.
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