Commit Graph
943 Commits
Author SHA1 Message Date
REDCODE d2daed27a8 Merge pull request 'Global "Search Anything" typeahead dropdown in navbar' (#91) from feature/global-search-dropdown into develop
Reviewed-on: #91
Reviewed-by: Samuele Lorefice <aironenerowork@gmail.com>
Reviewed-by: Fastwind <fastwind@noreply.localhost>
2026-07-13 15:18:40 +00:00
REDCODE 87b69d5ee8 revert: remove AllowAnonymous from AlbumController.Search and PersonController.GetAll 2026-07-13 17:03:53 +02:00
REDCODE e0d46aa87b refactor: replace hardcoded 250 page size limit with PagedParametersDto.MaxPageSize 2026-07-13 17:02:13 +02:00
REDCODE dbe55be63f refactor(albums): extract visibility-aware AssetCount into AlbumMapper overload
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.
2026-07-12 21:15:00 +02:00
REDCODE 28f9e7faae fix: replace untranslatable Tags navigation in TopTags stats query with direct join table query 2026-07-12 21:02:50 +02:00
REDCODE 820d3b1c8b fix: replace untranslatable Album navigation in stats queries with direct join table queries 2026-07-12 21:02:33 +02:00
REDCODE 278648aae6 Merge branch 'develop' into feature/global-search-dropdown 2026-07-12 21:00:42 +02:00
REDCODE 9d9491253b refactor: clean up imports, simplify checks, and add SearchDropdown component
- 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
2026-07-12 20:47:24 +02:00
REDCODE 499c6375f6 Merge branch 'develop' into feature/global-search-dropdown
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
2026-07-12 20:28:59 +02:00
REDCODE 2b150060f4 Merge pull request 'Add searchbar to cosplayer detail page for filtering albums' (#100) from feat/cosplayer-detail-search into develop
Reviewed-on: #100
Reviewed-by: Samuele Lorefice <aironenerowork@gmail.com>
2026-07-12 18:23:51 +00:00
REDCODE b93cb73e0a fix(cosplayer-detail): remove search debounce, instant inline reload
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.
2026-07-12 20:21:27 +02:00
REDCODE d2d584d509 fix(cosplayer-detail): debounce search, keep page intact on filter changes
- Remove OnFilterChanged from SortFilterBar to prevent per-keystroke reloads
- Debounce search input with 300ms CancellationTokenSource delay
- Sort changes (dropdown/toggle) trigger immediate ReloadAlbums
- ReloadAlbums no longer sets isLoading — updates album grid in-place
  without unmounting the page DOM, preserving focus on the search box
- Dispose searchCts on component disposal
2026-07-12 20:19:30 +02:00
REDCODE 8bcb408a5b fix(cosplayer-detail): prevent banner image shuffle on album search/sort
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.
2026-07-12 20:15:09 +02:00
REDCODE 445b030160 feat(cosplayer-detail): add SortFilterBar for album search and sort
- 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
2026-07-12 20:11:06 +02:00
REDCODE 8195808500 docs: update AGENTS.md with missing conventions and project info
- Add Lactose.Analyzers as 5th project (DateTime.UtcNow error MS001)
- Document zero-based pagination standard with validation rules
- Add SharedWith.Any() EF Core translation gotcha
- Document pg_trgm GIN indexes and pgvector extensions
- Fix test count (66+ → 67), add env file reference
2026-07-12 19:41:10 +02:00
REDCODE 393ad136f6 fix: restore PageSize < 1 validation with upper limit of 250 2026-07-12 19:31:23 +02:00
REDCODE 00acfef28c fix: only reject Page < 0, keep original PageSize validation
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.
2026-07-12 19:27:10 +02:00
REDCODE efeed2b668 test: add page=0&pageSize=5 to paginated list endpoints
Person (tests 23-26), Album (tests 45-48), and User (test 12) list
endpoints now explicitly request the first page instead of the full set.
2026-07-12 19:17:15 +02:00
REDCODE efc6379bb3 test: add cleanup of regular test user in REST Client suite 2026-07-12 19:11:11 +02:00
REDCODE 20a456591d fix: move SharedWith visibility check to client side for translatable queries
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
2026-07-12 19:07:38 +02:00
REDCODE 1b301ff2c0 fix: standardize pagination to zero-based across all layers
- 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
2026-07-12 19:03:09 +02:00
REDCODE 50c58e22ff fix: restore ToAlbumPreviewDto mapper method per review feedback #91 2026-07-12 18:50:03 +02:00
REDCODE ad2f08698d perf: simplify ORDER BY to total count and add trigram GIN indexes
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
2026-07-12 18:24:55 +02:00
REDCODE e05ab3beaf docs: document REST Client tests in AGENTS.md 2026-07-12 18:05:55 +02:00
REDCODE 48b6d4f149 perf: add partial index on Assets for visibility-aware search queries
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
2026-07-12 18:04:53 +02:00
REDCODE 89edf290ff refactor: project AlbumPreviewDto directly in repository with visibility-aware asset count
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
2026-07-12 17:41:24 +02:00
REDCODE c77096b0f9 refactor: project PersonPreviewDto directly in repository instead of [NotMapped] entity property
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.
2026-07-12 17:18:17 +02:00
REDCODE e333d0e9db fix: filter album count by user visibility in person search
- 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
2026-07-12 16:51:29 +02:00
REDCODE 324c1b1a17 fix: prevent duplicate @key errors across all list renderings
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
2026-07-12 14:47:12 +02:00
REDCODE ee3511c09a fix: paginate PersonRepository.SearchQuery by ID before loading albums
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.
2026-07-11 20:47:44 +02:00
REDCODE 41c262d157 fix: restore Include(p => p.Albums) in PersonRepository queries
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.
2026-07-11 20:39:14 +02:00
REDCODE 837491fa4a fix: guard ILIKE filter in AlbumRepository.SearchQuery against null/empty query 2026-07-11 20:36:33 +02:00
REDCODE 5a2f61d261 perf: push visibility filter into SQL for PersonRepository queries
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.
2026-07-11 20:35:56 +02:00
REDCODE 851cdfa924 test: restructure WepApiTest.http with three clearly separated users
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).
2026-07-11 19:02:58 +02:00
REDCODE 8dd5e7158f test: fix curator user access level from Admin (2) to Curator (1)
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.
2026-07-11 19:00:53 +02:00
REDCODE 9e52cc2351 fix: use zero-based page numbers in AssetController to match API convention
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.
2026-07-11 18:55:15 +02:00
REDCODE ca8119f356 fix: add [FromQuery] to TagController.GetAll parameter
Complex type PagedSearchParametersDto needs [FromQuery] for GET
requests, otherwise ASP.NET Core tries body binding and returns 415.
2026-07-11 18:54:05 +02:00
REDCODE bef29f5b22 test: add user id to profile update request body
UserUpdateDto.Id is required. The test was sending only username,
causing 400 Bad Request from model binding failure.
2026-07-11 18:52:54 +02:00
REDCODE e27d63cf32 fix: restrict user list to admin only
UserController.GetAll was missing [Authorize(Roles = Admin)],
allowing any authenticated user to list all users.
2026-07-11 18:51:26 +02:00
REDCODE fd46faec21 test: set personId from admin search to ensure it's always populated
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.
2026-07-11 18:50:17 +02:00
REDCODE 648531fcc4 test: handle empty person search results gracefully
The person search may return an empty array when the database has no
publicly visible people. Only set personId when results exist.
2026-07-11 18:48:50 +02:00
REDCODE dda062d959 fix: materialize person search query before client-side visibility filter
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.
2026-07-11 18:46:10 +02:00
REDCODE 02c62d0dd8 test: add anonymous search tests for persons and albums 2026-07-11 18:42:13 +02:00
REDCODE 2249e0fb2e fix: allow anonymous search for persons and albums
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.
2026-07-11 18:42:10 +02:00
REDCODE f626228c2f fix: return 401 Unauthorized when user data is null in PersonController
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.
2026-07-11 18:39:52 +02:00
REDCODE b9fd5e172c test: add endpoint tests for refresh token, albums, curator auth levels
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
2026-07-11 18:33:51 +02:00
REDCODE 3b05061a31 fix: override Bootstrap dropdown-menu display:none
Bootstrap's .dropdown-menu sets display:none. Add display:block to
.search-dropdown-menu to make the search dropdown visible.
2026-07-11 18:15:04 +02:00
REDCODE 7ea72e13ac style: use Bootstrap dropdown-menu and dropdown-item classes
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.
2026-07-11 18:09:57 +02:00
REDCODE faa8e93d55 fix: use case-insensitive ILike search for persons and albums
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.
2026-07-11 18:03:17 +02:00
REDCODE c5ba90202d style: add chevron-right indicator to search dropdown headers
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.
2026-07-11 17:44:13 +02:00