Commit Graph
181 Commits
Author SHA1 Message Date
REDCODE df8b5a2cf2 feat(Lactose): convert FolderRepository to async 2026-08-21 17:30:36 +02:00
REDCODE ae62213b76 refactor(Lactose): await settings lookups in StatsRepository 2026-08-21 17:04:10 +02:00
REDCODE 64ec5d46a8 feat(Lactose): convert JobRecordRepository to async with tuple page result 2026-08-21 17:04:00 +02:00
REDCODE b1bcab67e0 feat(Lactose): convert SettingsRepository to async 2026-08-21 17:04:00 +02:00
REDCODE b43f85e7c9 feat(Lactose): convert TagRepository to async 2026-08-21 17:04:00 +02:00
REDCODE a7d2e88993 feat(Lactose): convert UserRepository to async 2026-08-21 16:27:05 +02:00
REDCODE b6386aad98 refactor(asset): rename fs-browse to directory and remove the DB-path browse endpoint
- Rename the filesystem browse route from /api/asset/fs-browse to /api/asset/directory
  (controller route, client URL, REST tests, and service log label).
- Remove the old /api/asset/browse endpoint and its BrowseAssets/GetDirectoryNames
  repository code, plus the client BrowseAsync method. Repoint AlbumAssetPicker's
  admin/curator folder browse to the /directory endpoint.
- Drop the now-unused pg_catalog.split_part DbFunction mapping.
- Refresh .env GIT_VERSION.
2026-08-21 01:00:38 +02:00
REDCODE 5b4e2b9afc feat(media): repurpose GIFs as video and serve converted mp4 when available
- Classify .gif as EAssetType.Video via MimeTypes (moved image/gif to the Video registry)
- Add MediaRepository.GetConvertedData, served by the existing media/{id} endpoint
  when a converted file exists on disk (with a fallback + warning log if missing)
- Expose the converted MIME type on AssetPreviewDto/AlbumAssetPreviewDto
  so the frontend can detect video assets from a single MimeType field
2026-08-20 23:58:48 +02:00
REDCODE 7fd294043e refactor(asset): convert bulk visibility to ExecuteUpdate, translate directory-name query to LINQ, and mark read-only queries AsNoTracking 2026-08-20 20:10:06 +02:00
REDCODE ba66000748 merge: integrate develop (incremental scans, visibility cascade, view-mode search) 2026-08-20 18:03:58 +02:00
REDCODE a149b01690 feat: show broken asset count on statistics page
Adds a TotalBrokenAssets stat aggregated from the existing asset query and
surfaces it on the stats page as a danger card linked to the maintenance
page.

Refs #180
2026-08-18 16:54:35 +02:00
REDCODE 61b7c10967 feat: track broken assets to stop requeueing known failures
Add ProcessFailedAt/ProcessErrorMessage to Asset plus a filtered index.
All 'missing' queries now exclude broken assets so failed images are not
re-processed indefinitely. Thumbnail/Preview/PHash/Metadata jobs mark the
asset broken on failure via a shared helper. AssetPreviewDto now carries
the broken state for the maintenance report.

Refs #180
2026-08-18 11:04:23 +02:00
REDCODE f8db265ffd perf: use semi-join raw SQL for asset cascade updates 2026-08-18 02:17:31 +02:00
REDCODE a680f6363d feat: add bulk visibility update by person IDs (ExecuteUpdate) 2026-08-18 01:47:52 +02:00
REDCODE 0098ec3a65 chore: remove unused FindByAlbumIds 2026-08-18 01:04:45 +02:00
REDCODE fbd7f5ff00 fix: run bulk cascade within a repository transaction 2026-08-18 01:04:38 +02:00
REDCODE c05082ad13 Merge branch 'develop' into feature/cascade-visibility-skip-deleted 2026-08-17 22:40:36 +00:00
REDCODE f9385e2d9d refactor(stats): move command timeout to connection string
Removes the per-request SetCommandTimeout mutation on the shared scoped
DbContext (flagged in review as a minor smell). The 120s command timeout
is now configured once on the Npgsql connection string, which also
covers other potentially slow maintenance/scan queries. The no-album /
no-person anti-joins no longer need to temporarily mutate and restore
the context's timeout.

Refs #173
2026-08-18 00:16:58 +02:00
REDCODE e995388af0 fix(stats): correct no-album/person counts and refresh charts on reload
Addresses review findings on PR #178:

1. Backend: AssetsWithNoAlbum / AssetsWithNoPerson now exclude
   soft-deleted assets. The COUNT(DISTINCT AlbumAsset.AssetsId) previously
   included deleted assets, which (since TotalAssets only counts live
   assets) inflated the in-album count and undercounted orphans. The
   distinct count is now restricted to live asset IDs via an
   Id IN (SELECT Id FROM Assets WHERE DeletedAt IS NULL) predicate.

2. Frontend: StatChart now re-renders when its data parameters change
   instead of only on first render, so the Refresh button updates the
   four Chart.js charts alongside the tables/cards.

Refs #173, #177
2026-08-18 00:12:13 +02:00
REDCODE 965caadc93 fix(stats): compute no-album/person counts as differences
AssetsWithNoAlbum / AssetsWithNoPerson still exceeded the 30s command
timeout even after querying the AlbumAsset join table directly, because
a count(*) anti-join against the full Assets table is inherently
O(assets).

Rewrite both as computed differences from the already-known TotalAssets:
- AssetsWithNoAlbum   = TotalAssets - COUNT(DISTINCT AlbumAsset.AssetsId)
- AssetsWithNoPerson  = TotalAssets - COUNT(DISTINCT AlbumAsset.AssetsId
                        WHERE AlbumsId in person-owned albums)

These are single-table aggregate scans over the indexed join table
(no anti-join against Assets), clamped to >= 0. A 120s command timeout
is applied around these two queries as a safety net and restored
afterwards so other repos are unaffected.

Also adds OrderBy to the grouped FirstOrDefaultAsync to silence the
FirstWithoutOrderByAndFilter warning.

Refs #173
2026-08-17 23:40:02 +02:00
REDCODE 8a1b39347d fix(stats): use indexed join-table anti-joins for no-album/person
AssetsWithNoAlbum / AssetsWithNoPerson previously anti-joined through
Albums -> AlbumAsset -> Assets, which generated a large correlated join
that exceeded the 30s DB command timeout at scale.

Rewrite both to anti-join against the AlbumAsset join table directly,
using its existing AssetsId index:
- assets with no album: Assets.Id NOT IN (SELECT AlbumAsset.AssetsId)
- assets with no person: Assets.Id NOT IN (SELECT AlbumAsset.AssetsId
  WHERE AlbumsId in person-owned albums)

This avoids touching the full Assets table in the subquery and lets the
planner use a hash anti-join instead of a nested loop.

Refs #173
2026-08-17 23:33:21 +02:00
REDCODE 5e7c053218 feat(stats): cache aggregated results in-memory
Caches the assembled StatsDto in IMemoryCache with a 60s TTL. The cache
key is fingerprinted with the ThumbnailSize/PreviewSize setting values so
changing those settings immediately invalidates the stale counts on the
next request. Registers AddMemoryCache() in Program.cs.

Refs #175
2026-08-17 22:53:24 +02:00
REDCODE e1bb7d3330 refactor(stats): consolidate and async-ify stats queries
Rewrites StatsRepository to be fully async and collapses the ~30
synchronous round-trips into a bounded set of aggregated queries:

- Single grouped pass over non-deleted assets computes totals, storage,
  visibility breakdown, orphans, 7/30-day activity, and all
  data-completeness counts (missing metadata/thumbnail/preview/phash).
- AssetsByType + StorageByType come from one GROUP BY on Type.
- UsersByAccessLevel, TotalUsers, and users registered in 30 days come
  from one GROUP BY on AccessLevel.
- MIME breakdown and resolution buckets are now GROUP BY'd in SQL; the
  resolution bucket key is a translatable CASE expression on the max
  dimension, so no more loading every asset's dimensions into memory.
- AssetsWithNoAlbum / AssetsWithNoPerson use set-based NOT IN anti-joins
  over the Album.Assets navigation instead of per-row NOT EXISTS.
- TopTags uses the Tag.Assets navigation (join table indexed on TagsId).
- Drops IDisposable/Dispose() so the transient repo no longer disposes
  the shared scoped DbContext.
- StatsController action is now async.

StatsDto shape is unchanged, so the existing .http tests remain valid.

Refs #173
2026-08-17 22:52:27 +02:00
REDCODE f75940cd20 feat: add bulk asset visibility update via ExecuteUpdateAsync 2026-08-17 22:32:25 +02:00
REDCODE 4189e975a1 feat: add bulk descendant queries for visibility cascade 2026-08-17 21:48:48 +02:00
REDCODE 63e3413141 perf(asset): server-side seeded shuffle, DTO projection, opt-in count
Random ordering now uses a pg_catalog.md5-based deterministic shuffle in SQL
instead of materializing every matching asset ID in memory. The listing
projects directly to AssetPreviewDto (no full entity transfer) with album and
cosplayer names via one grouped query. includeCount=false skips the count
query entirely.
2026-08-11 13:10:16 +02:00
REDCODE b83b97934c perf(album): project assets to DTO in SQL and slim Find paths
FindVisible filters and projects assets at the database level instead of
loading full entities then filtering/sorting in memory. Find no longer
eager-loads assets; FindWithAssets covers the update path. SearchQuery gains
personOwnerId filter and AsNoTracking.
2026-08-11 13:09:48 +02:00
REDCODE 6bf627e429 perf(person): push album filter/search/sort/pagination to SQL in FindVisible
No longer loads the person's full album/asset graph into memory; uses ID-first
paging, ILike search against the trgm GIN index, and DB-level counts. Slims
Find() to a PK lookup for write paths.
2026-08-11 13:09:44 +02:00
REDCODE 0480b00766 fix: desloppify review fixes - PathFromGuid extraction, RegexHighlighter dedup, port default fix
- Extract PathFromGuid to shared PathUtils.cs (PreviewJob + ThumbnailJob)
- Consolidate RegexHighlighter duplicate methods via delegation
- Fix default port 3306 -> 5432 in appsettings.json + docker-compose.yml
- Remove port gotcha section from AGENTS.md
- Fix ITagRepository XML docs after parameter rename (id -> tag)
2026-07-29 20:12:58 +02:00
REDCODE 26674e05e5 refactor: quality improvements from desloppify scan
- Remove 16 unused using System; imports from migration files (implicit usings available)
- Fix 6 empty catch blocks with proper exception logging
- Remove unused import (false positive flagged as wontfix)
- Skip test coverage, orphaned, and stale exclude issues as false positives
- Add missing namespace to IPersonRepository.cs
- Rename IFolderRepository Create/Delete to Insert/Remove for CRUD consistency
- Fix ITagRepository.Delete parameter name from 'id' to 'tag'
- Add missing IDisposable to IMediaRepository
- Rename SettingsExtensions to SettingsExtension for naming consistency
- Rename PagedParametersDTO.cs to PagedParametersDto.cs
- Add .desloppify/ to .gitignore
2026-07-29 19:52:49 +02:00
REDCODE 79252d363d fix: sort album assets by OriginalFilename instead of CreatedAt (#144) 2026-07-21 15:55:58 +02:00
REDCODE 8ff21fa56b fix: use pre-pagination album count (#143)
Add AlbumTotalCount [NotMapped] to Person model, set before pagination in FindVisible, read in PersonMapper instead of paginated Albums.Count. Also apply inline aspect-ratio only in masonry mode so grid mode uses CSS aspect-ratio: 1 for square crops.
2026-07-21 15:47:21 +02:00
REDCODE 439df56f2b fix: use per-card aspect-ratio from cover dimensions for stable masonry layout (#146) 2026-07-21 15:05:02 +02:00
REDCODE 9965a13f40 fix: order album assets by CreatedAt in album detail (#144) 2026-07-21 14:26:29 +02:00
REDCODE d89f6f0a21 fix: address PR #142 review issues
- Fix timer leak: implement IDisposable on AlbumAssetPicker
- Remove unnecessary full-table COUNT in GetFolderGroups
- Remove dead DirectoryEntryDto.AssetCount
- Remove unused ExistingAssetIds parameter
- Remove redundant .Distinct() after .Union()
- Extract hardcoded page size 150 to named constant
- Rename AssetSearchOptionsDto.Unassigned to Unlinked for consistency
- BrowseAssets returns 404 for missing folderId instead of 200 empty
2026-07-17 00:00:37 +02:00
REDCODE 8589a85625 refactor: rename UnlinkedAssetGroupDto to AssetGroupDto 2026-07-16 23:14:14 +02:00
REDCODE 983cd8baef refactor: add Embedded mode, UnlinkedOnly param, rename methods from Unlinked to generic names 2026-07-16 23:13:18 +02:00
REDCODE eae6152738 fix: filter directory names to only paths containing a slash after prefix, excluding filenames 2026-07-16 22:56:17 +02:00
REDCODE c4788e1572 fix: use parameterized SqlQueryRaw instead of unquoted string.Format to prevent SQL error 2026-07-16 22:51:41 +02:00
REDCODE 1dfa9eaf41 perf: replace in-memory path parsing with SELECT DISTINCT SQL, remove dir count badges 2026-07-16 22:44:20 +02:00
REDCODE 50b5a28841 perf: replace in-memory path parsing with SQL SPLIT_PART aggregation and double-LIKE filter 2026-07-16 22:15:11 +02:00
REDCODE 5d9aa21987 feat: add file-browser BrowseUnlinkedAssets endpoint with directory tree from OriginalPath 2026-07-16 21:43:44 +02:00
REDCODE 90c81c2d20 fix: use Albums.Count==0 instead of !Any() for unlinked filter, add trace logging 2026-07-16 21:06:18 +02:00
REDCODE 22706a09cd feat: extend AssetRepository with unassigned, folderId, uploadedBy, search filters and GetUnlinkedGroups 2026-07-16 20:51:34 +02:00
REDCODE 1e2bc83b20 fix: guard against null PersonOwnerId in MergePeople albums query
Replace PersonOwnerId ?? Guid.Empty with explicit null check to
avoid matching albums with no owner if sourceIds contained Guid.Empty.
2026-07-16 18:30:23 +02:00
REDCODE 656e6fa9bf feat: add MergePeople to person repository
Reassigns albums (PersonOwnerId), faces (PersonId), and maintainers
from source people to destination, then hard-deletes source people.
2026-07-16 17:56:24 +02:00
REDCODE 5e1306e466 feat: add MergeAlbums to album repository
Loads destination + source albums with assets, moves all assets
from sources to destination (deduplicated), then hard-deletes sources.
2026-07-16 17:55:46 +02:00
REDCODE 7db1738678 fix: soft-delete users in UserController.Delete instead of hard-delete
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
2026-07-16 02:31:52 +02:00
REDCODE 3bf98cbfed fix: prevent private assets with null UploadedBy leaking to anonymous users
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.
2026-07-15 23:33:08 +02:00
REDCODE 86f95d7461 feat: add role-visible colored borders on albums and assets based on item state (#78)
Backend:
- Add DeletedAt to AssetPreviewDto, AlbumAssetPreviewDto, AlbumPreviewDto
- Remove redundant DeletedAt from AssetDto (now inherited from base)
- Add viewerId parameter to ToAssetPreviewDto, ToAlbumPreviewDto,
  ToAlbumFullDto, ToPersonDetailedDto mappers
- Conditionally send DeletedAt only when viewer is Admin or asset uploader
- Pass uid/viewerId from all controller/repository call sites

Frontend:
- AlbumCard: show orange (not public) / red (deleted) border in select
  mode for admins/curators
- AlbumDetail: same border logic in GetTileClass for asset tiles
- Borders only appear in select mode per REDCODE's feedback
2026-07-15 20:48:00 +02:00