Add fixed-window rate limiting to Lactose with 6 configurable buckets:
- Global (100 req/60s) for all generic calls
- Auth (10 req/60s) strict anti-brute-force
- MediaOriginal (50 req/60s) for full-size media
- MediaThumb (200 req/60s) for thumbnails
- MediaPreview (200 req/60s) for previews
- Jobs (30 req/10s) quick refresh for long-polling
All buckets configurable via appsettings.json RateLimiting section.
Rejected requests return HTTP 429 with JSON body and Retry-After header.
REST Client only processes the first request per ### block.
Split 99f into 99f/99f1 and 99i2 into 99i2/99i2b so dynamic
variables set by response handlers are available in subsequent tests.
POST /api/album/merge — Curator/Admin, validates DestinationId not in SourceIds
POST /api/person/merge — Admin/Curator, validates DestinationId not in SourceIds
FolderBox now delegates deletion to parent via OnDeleteRequested
callback instead of deleting directly. Settings.razor shows a
ConfirmDialog before removing the folder.
Adds confirmation before deleting a cosplayer or bulk-deleting selected
albums. Uses a single ConfirmDialog with dynamic message and stored
callback for either action.
Adds confirmation before deleting an album or bulk-deleting selected
assets. Uses a single ConfirmDialog with dynamic message and stored
callback for either action.
A shared confirmation dialog wrapping ModalFrame for delete operations.
Supports plain text Message or custom Body RenderFragment, configurable
confirm button text and styling, and OnConfirm/OnCancel event callbacks.
Adds a shared LinkToCosplayerModal component with person typeahead
search and wires it into the albums list page's multiselect topbar.
The backend already supports bulk person assignment via POST /api/album
with BulkDto<AlbumUpdateDto> { Data.Person = ... }, so no backend
changes are needed.
Also adds API tests for the bulk album person assignment flow.
Part of #128, closes#122
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
Replaces the hardcoded <a href="/albums"> back link with a button
that calls history.back() via JS interop, falling back to NavigateTo("/albums")
when there is no browser history. Fixes#133.
- Seed SystemUploaderId in DefaultSettings.json (empty = no default owner)
- Read setting once per crawl and apply UploadedBy on new assets
- Backfill UploadedBy on existing assets that are missing it
- Add UserSelect display type for user-picker setting
- Create SettingUserSelect component with search dropdown
- Add setting to Scanning group in Settings UI
- CosplayerDetail: label desktop Delete button and mobile dropdown as
'Delete Cosplayer' (was 'Delete' / 'Delete person')
- ProfileCropper: move instruction text into modal footer to prevent
overlap with Cancel/Save buttons; darken overlay background to
rgba(0,0,0,0.85) instead of semi-transparent rgba(0,0,0,0.6)
- fix: @for closure bug in Home.razor causing preview to show only previous arrow
- guard NavigatePreview against flatList rebuilds during infinite scroll
- fix IntersectionObserver crash when sentinel not in DOM during reload (CosplayerGrid, AlbumGrid)
- align cascade checkbox labels with actual cascade logic (skips private/deleted)
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.
- Restore New Cosplayer button on desktop
- Fix ToggleSelectAll via CosplayerGrid.GetCurrentPersonIds()
- Fix ApplyVisibility wiping data — fetch person first, include all fields
- Add GetCurrentPersonIds() to CosplayerGrid for Select All support
- Visibility modal with cascade to albums and assets
- Desktop inline + mobile dropdown action buttons
- Select All / Deselect All button
- Uses PersonService single-update for visibility, AlbumService cascade
- Add CascadeVisibilityToAssetsAsync to AlbumService (skips private/deleted)
- Replace duplicated loop+filter in Albums, CosplayerDetail, PersonForm with single call
- Update labels to describe skip-private/deleted behavior
- VisibilityModal: add optional CascadeLabel + CascadeEnabled for checkbox
- Albums page: checkbox to cascade album visibility to non-deleted assets
(lower-only: never raise asset visibility)
- PersonForm: cascade checkboxes when editing cosplayer visibility
- Cascade to albums (lower only)
- Cascade to assets in every album (only if album cascade enabled)
- Inject AssetService in Albums page and PersonForm
- Create MilkStream.Client/Components/Shared/VisibilityModal.razor
- Replace duplicated modal markup in AlbumDetail, CosplayerDetail, Albums
- Add Select All/Deselect All to Albums via AlbumGrid ref
- Add Delete, Visibility modal to Albums select mode
- Add mobile dropdown to Albums action buttons
- Add GetCurrentAlbumIds() to AlbumGrid for Select All support