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
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
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.
SQL-level search by name, sorting (name, created, albums), and pagination.
Access-level filtering for regular users pushed to SQL via subquery.
Replaces the in-memory GetAllVisible pattern.
Person had a DeletedAt property and all queries filtered by it, but the
controller was hard-deleting via context.People.Remove(). This removes
the DeletedAt property, drops the column via migration, and cleans up
all stale DeletedAt filters in queries.
- Add ChildrenResponse DTO (Children + Since) in Butter.Dtos.Jobs
- JobManager.GetChildren returns List<JobStatusDto> only (no tuple),
active children filtered via LINQ over activeJobs.Values
- JobsController generates Since timestamp, returns ChildrenResponse
instead of setting x-delta-timestamp header
- JobsService deserializes ChildrenResponse instead of parsing headers
EF Core cannot translate three-level nested Any through many-to-many
navigation (Person→Albums→Assets→SharedWith). Load data with Include
chain and filter visible persons in-memory instead.
- IPersonRepository.GetAllVisible filters persons by asset visibility
- For User level: only returns persons that have at least one album
with a non-deleted asset that is publicly shared, owned by the user,
or explicitly shared with the user
- For Admin/Curator: returns all persons (unchanged behavior)
- PersonController.GetAll extracts user data and uses GetAllVisible
- JobManager.GetChildren now accepts optional 'since' parameter
- Returns only children with LastChange/ModifiedAt > since
- Includes seenIds dedup between active and past children
- Returns response timestamp for next poll
- Controller sets x-delta-timestamp header
- Add ModifiedAt (DateTime?) to JobRecord
- Auto-set on Insert/Update in repository
- Add GetChildrenModifiedSince to interface + implementation
- EF Core migration AddModifiedAtToJobRecord
The Interlocked.Increment(ref failedAssets) for Failed subjobs
caused double-counting: a subjob with N/12 failed assets would
add 1 (failed subjob) + 12 (individual assets) = 13, exceeding
the total asset count. Removed the Increment since subjob's
failedAssets is already the accurate per-asset count.
- Users: changed from blanket Forbid to ownership check (can update own albums)
- Curators: changed from ownership check to full access (can update any album)
- #61: wrap standalone fallback div in cosplayer-card-img-wrap on
Cosplayers page so placeholder icon gets proper sizing/positioning
- #62: add mobile breakpoint rule for modal-overlay so edit form
appears centered on small screens instead of at page bottom
- #66: make cosplayer name in ImagePreview, AlbumDetail, AlbumCard,
and Home asset tiles a clickable link to /cosplayer/{id}
- add CosplayerIds to AssetPreviewDto and populate via mapper for
linked navigation from the home page image previewer
- Remove EnableLegacyTimestampBehavior switch from Program.cs
- Add ConfigureConventions to DbContext pinning DateTime → timestamptz
- Add migration to convert all 17 DateTime columns across 5 tables
(Albums, Assets, Users, People, JobRecords) with safe AT TIME ZONE 'UTC'
- Add Roslyn analyzer (MS001-MS004) enforcing UTC-only DateTime at compile time
with code fix provider for auto-replacement
- Fix pre-existing DateTime.Now in AuthController.cs:119
- Add AllowMissingPrunePackageData to work around .NET 10 SDK issue