Replace inline AlbumPreviewDto construction in AlbumRepository.SearchQuery
with a call to the new ToAlbumPreviewDto(album, userId, accessLevel) mapper
overload, making the visibility logic reusable and the repository more compact.
- Remove unused using directives across C# and Razor files
- Remove unused IServiceProvider from SettingsRepository
- Simplify null/empty string checks in StatsRepository
- Add null-safe navigation for Albums/Tags in stats queries
- Initialize Asset.Hash default to prevent null refs
- Deduplicate AssetIds in AssetPicker
- Add OnStartedWaiting/OnFinishedWaiting/OnProgressChanged to Job
- Add global SearchDropdown component with keyboard nav
- Fix XML doc param mismatches
Resolved merge conflict in CosplayerDetail.razor:
- Kept SortFilterBar with instant search/sort (from feat/cosplayer-detail-search)
- Adapted allAlbums references to allAlbumsDict (from feature/global-search-dropdown)
- Kept updated PersonController.cs and PersonService.cs from both branches
Search now fires ReloadAlbums immediately on every keystroke, matching
the same instant behavior as Albums and Cosplayers pages. Since
ReloadAlbums only swaps the album grid content without touching
isLoading, focus is preserved and the page stays intact.
Add updateBanner parameter to LoadPerson so ReloadAlbums (triggered
by search/sort filter changes) does not regenerate the random banner
cover URLs, keeping the header fixed during filtering.
- Backend: accept PagedSearchParametersDto on GET /api/person/{id}
with in-memory search (case-insensitive title Contains) and sort
(name/created/updated/assets) before pagination
- Frontend service: add search, sortBy, sortAsc params to GetByIdAsync
- Frontend page: replace inline action buttons with reusable SortFilterBar
component, wire search/sort state into initial load and infinite scroll
Per review feedback: only Page being negative is truly invalid.
PageSize validation reverted to original < 0 (allowing 0).
PersonController keeps minimal Page < 0 check since it had none before.
JobsController aligned for consistency.
EF Core cannot translate SharedWith.Any() through the many-to-many
AlbumAsset join table inside a SQL WHERE clause. Moved the visibility
filter to client-side after loading the page's records with Include.
PersonRepository.SearchQuery and AlbumRepository.SearchQuery now:
1. Get paginated IDs without visibility filter (translatable SQL)
2. Load those records with Include chains
3. Filter visibility in C# (SharedWith.Any(), etc.)
4. Project to DTOs
- AssetRepository: all Skip() formulas changed from (page-1)*size to page*size
- JobRecordRepository: same formula change for GetPastRootJobs
- AssetController: removed Page+1 bridge conversion, passes Page directly
- JobsController: GetPast default changed from 1 to 0, added validation
- AlbumController, TagController, PersonController: consistent Page<0/PageSize<1 validation
- IAssetRepository XML doc: 1-based → zero-based
- Home.razor: shifted all internal page state from 1-based to 0-based
Closes#95
Simplify the 'assets'/'albums' sort to use total count instead of
visibility-filtered count, eliminating the expensive correlated COUNT
subquery from ORDER BY (which evaluated 25k times for every album).
Add pg_trgm extension and GIN trigram indexes on People.Name and
Albums.Title for efficient ILike %%query%% searches.
The SELECT projection still computes visibility-filtered counts for
accuracy, but only for the paginated subset (30 rows).
Migration: 20260712162406_AddTrigramIndexes
Create IX_Assets_VisibleForSearch on (IsPubliclyShared, OwnerId)
WHERE "DeletedAt" IS NULL to accelerate the correlated subqueries
used in both AlbumRepository and PersonRepository for filtering,
sorting by asset/album count, and projecting AssetCount/TotalAlbums.
- Fluent API in LactoseDbContext.OnModelCreating
- Generated migration via dotnet ef migrations add
Align AlbumRepository.SearchQuery with the PersonRepository pattern:
return AlbumPreviewDto directly with access-level filtering at the
query level, avoiding loading full entity graphs and filtering
in-memory in the controller.
- Add userId/accessLevel params to SearchQuery
- Push visibility filter into the query for regular users
- Compute AssetCount with same visibility logic in the projection
- Sort by 'assets' uses visible count for regular users
- Remove unused AlbumMapper.ToAlbumPreviewDto extension
- Simplify AlbumController.Search to just return repo results
Replace the TempData-like TotalVisibleAlbums [NotMapped] property on
Person with a direct DTO projection in PersonRepository.SearchQuery.
The repository now returns IEnumerable<PersonPreviewDto>, computing
the visibility-aware album count in the EF Core subquery and projecting
only the needed columns. This avoids polluting the entity model with a
context-dependent property.
- Add Person.TotalVisibleAlbums [NotMapped] populated by repository
- Use visibility-aware album count in PersonMapper
- Restructure PersonRepository.SearchQuery with two-step pagination
for correct ordering and per-user album visibility filtering
Switch accumulating list storage to Dictionary<Guid, T> to guarantee
unique Blazor @key values when paginated data overlaps or races occur
during sort/filter changes.
- CosplayerGrid: Dictionary<Guid, PersonPreviewDto> for people list
- CosplayerDetail: Dictionary<Guid, AlbumPreviewDto> for albums
- AlbumDetail: deduplicate album.Images at load time via dict
- Home: deduplicate flatList rebuild via dict (keeps first occurrence)
- AssetPicker: Distinct() on parameter list
Include + Skip/Take on a collection navigation can produce duplicate people when sorted by album count, because pagination operates on joined rows. Fix by selecting paged IDs first, then loading the full entities with Include for those IDs.
The visibility subquery doesn't load the Albums navigation, so TotalAlbums in the mapper was always 0. Added back a lightweight Include(p => p.Albums) (without Assets/SharedWith chain) to both SearchQuery and GetAllVisible.
Replace client-side visibility filtering with EF Core Any() subqueries in SearchQuery and GetAllVisible. This eliminates the cartesian product from the Include chain (Person -> Albums -> Assets -> SharedWith) and lets the database short-circuit the auth check with indexes. Adds null-forgiving operators (!) on navigation access in the query predicates.
The test file now creates three distinct users at the start:
- admin (seeded, admin/admin)
- curator (created by admin, accessLevel=1)
- user (registered via /api/auth/register, accessLevel=0)
Each user has its own token variable (admin_token, curator_token,
user_token) and ID variable (admin_id, curator_id, user_id).
Sections are organized: Setup → Auth → User CRUD → Person →
Album → Tag → Asset → Stats → Settings → Cleanup.
All endpoints are tested at each appropriate authorization level:
admin (full access), curator (elevated access), user (own data only),
and anonymous (public data only).
EAccessLevel enum: User=0, Curator=1, Admin=2. The test created the
curator user with accessLevel=2 (Admin), making curator_token an
admin token, which broke the 'Create user as curator should 403' test.
AssetController.GetAll validated page >= 1 (one-based), inconsistent
with PersonController and AlbumController which use zero-based pages.
Adjust validation to page >= 0 and pass page + 1 to the repository
which internally uses one-based.
Rng user may see no public people, leaving personId unset. Admin
sees all people including test-created ones, so setting personId from
the admin response ensures downstream tests have a valid reference.
The nested Any() through SharedWith navigation cannot be translated
to SQL by EF Core. Apply the same pattern as GetAllVisible and
AlbumController.Search: materialize server-side (search, sort,
pagination), then filter by visibility in memory.
GET /api/person and GET /api/album should not require authentication.
Anonymous users are treated as EAccessLevel.User, so they only see
publicly shared content. PersonController.GetAll uses uid ?? default
instead of uid!.Value to handle null uid safely.
PersonController.GetAll used uid!.Value which throws NRE if
authService.GetUserData returns null (e.g. claims present but
user not found). Return 401 Unauthorized instead of crashing.
Add HTTP endpoint tests covering:
- Refresh token endpoint (empty body, valid, auth failures)
- Album GET by ID (rng user, admin, non-existent)
- Album search (rng user, admin)
- User get all and profile update (rng user, admin)
- Stats endpoint (admin, curator, rng user 403)
- Tag search and create (admin, rng user 403)
- Asset search (rng user, anonymous)
- Curator-level CRUD permissions (person, album, stats succeed;
user creation and settings access 403)
- Cleanup of curator test user
Match styling of the user profile dropdown in the navbar by using
Bootstrap's native .dropdown-menu and .dropdown-item classes instead
of custom classes. Keep only custom positioning, thumbnail sizing,
and the clickable header styles.
Replace EF Core Contains() with EF.Functions.ILike() which
translates to PostgreSQL ILIKE for case-insensitive matching.
Searching 'alice', 'Alice', or 'ALICE' now returns the same results.
Shows a right-pointing arrow on section headers (Cosplayers/Albums)
to visually signal they are clickable and navigate to the list page.
The arrow subtly moves right on hover for extra affordance.