Replace the fixed GUID-based ordering (OrderBy(a => a.Id)) with a
client-generated seed that produces a truly different random order on
each page refresh. The seed is passed to the server and used to
deterministically shuffle all asset IDs via a hash function, ensuring
consistent pagination with no duplicates across pages within a session.
Also fix LoadPreviousPage showing a spinner when already at page 1 by
adding loadedMinPage <= 1 to the guard clause.
Replace manual childJobs.ForEach(j => j.Cancel()) with linked
CancellationTokenSources. Master jobs now call LinkParentToken(token)
on each child, so when the master's CTS fires, all children (running
and queued) receive cancellation automatically via their linked token.
Cancelling a master job now:
- Immediately signals all Running children via their linked CTS
- Ensures queued children, when started by JobManager, receive an
already-cancelled token and bail without processing
- Eliminates the collection-modification-during-ForEach race condition
Affects: PHashJob, ThumbnailJob, PreviewJob, MetadataJob,
FileSystemCrawlJob.
The snapshotFirstTileTop/restoreScrollAfterPrepend dance caused a double
visual correction (content shifts down then scroll snaps back) making
scroll-up feel janky compared to scroll-down. Removing it lets new content
naturally push existing tiles down when prepended, matching the expected
scroll-up behavior where content above fills in seamlessly.
- Replace single bottom observer with observeBottom/observeTop dual observers
- Track loadedMinPage/loadedMaxPage to know which pages are in memory
- hasMoreUp flag controls whether scrolling up should load pages
- LoadPreviousPage prepends pages at index 0 with scroll position preservation
using snapshotFirstTileTop/restoreScrollAfterPrepend JS helpers
- Eviction balanced: scroll down evicts from top, scroll up evicts from bottom
- Both observers registered on first render, methods return early when idle
Previously the download link just navigated cross-origin to the media URL.
Now uses JS interop to fetch the file as a blob, create an object URL,
and trigger a browser download via a hidden anchor element.
- Add 'View full size' (opens original in new tab) and 'Download' buttons
- Positioned opposite the cosplayer/album names in the preview info bar
- Info bar always visible so buttons are accessible even without metadata
- Uses original media endpoint with JWT token for authenticated access
- Add hover overlay on masonry tiles showing cosplayer and album names
- Add info bar in full-screen preview overlay with same metadata
- Hide overlays entirely when no data is available (jobs not yet run)
- CSS: gradient overlay at tile bottom with smooth fade-in on hover
- CSS: matching info bar at preview bottom
- Add AlbumNames and CosplayerNames fields to AssetPreviewDto
- Include Albums and PersonOwner in AssetRepository.GetAssets() query
- Populate new fields in AssetsMapper.ToAssetPreviewDto()
UsersMapper.ToGetUsersDto() was not copying LastLogin, BannedAt, or
UpdatedAt from the User entity to the UserInfoDto, causing the client
to always receive null for these fields. Added the missing mappings.
Also added null fallback ('Never') for LastLogin display on the User
page to prevent NullReferenceException when the value has not been set.
Job.Cancel() now handles queued jobs (cts null) by setting Canceled status and firing events directly. ContinueWith checks t.IsCanceled to catch ThrowIfCancellationRequested patterns. cts is now disposed after job completion. CreatePersonsJob, CreateAlbumsJob, and IntegrityCheckJob now catch OperationCanceledException and set proper Canceled state. IntegrityCheckJob also gained cancellation checks between each step and within iteration loops.
Parent jobs with fetched sub-jobs now show a multi-color stacked
progress bar instead of the single parent-progress bar. Segments
represent sub-job status distribution: completed (success),
running (primary), waiting (info), failed (danger), with-errors
(warning), and queued (primary for visibility in dark theme).
ProgressBar gets an optional Segments parameter for stacked mode.
JobRow accepts Children and computes status-weighted segments.
JobTree passes cached children to JobRow.
MetadataJob, ThumbnailJob, PreviewJob, and PHashJob previously only
canceled the master job's own token, leaving already-enqueued sub-jobs
running indefinitely. Added child-job tracking with proper cancellation
propagation at both master cancellation exit points, matching the
existing pattern in FileSystemCrawlJob.
- Add PreviewJob (WebP) — same master/sub-job pattern as ThumbnailJob
- Convert ThumbnailJob from JPEG to WebP with configurable quality
- Add ThumbnailQuality/PreviewQuality settings (configurable in settings UI)
- Add PreviewSize, ThumbnailFormat, PreviewFormat fields to Asset model
- Add format-aware repo queries for auto-regeneration of stale JPEGs
- Fix MIME types in MediaRepository/MediaController to return image/webp
- Wire PreviewJob into JobScheduler, JobMapper, JobsController
- Expose quality settings on Settings page (Jobs group, range sliders)
- Add 'Make Previews' button on Jobs page, reorder to scan/metadata/thumb/preview/phash/integrity
- Bump all NuGet packages to 10.0.x (ASP.NET, EF Core, Npgsql, Swashbuckle)
- Bump Pgvector.EntityFrameworkCore to 0.3.0, Microsoft.IdentityModel.JsonWebTokens to 8.19.1
- Pin Microsoft.OpenApi to 2.7.5, strip security scheme from SwaggerGen (API removed in v2)
- Docker images already at 10.0
Replace broken one-to-many User.AssetId shadow FK with proper
AssetUser join table using bidirectional navigation:
HasMany(e => e.SharedWith).WithMany(e => e.SharedAssets)
Join table columns: SharedAssetsId (FK to Assets) + SharedWithId (FK to Users).
Adds SharedAssets navigation to User model.
Generated via dotnet ef migrations add.
The native Dart binary at runtimes/linux-x64/src/dart is not
published into the runtime container, causing a crash when the
SassCompilerHostedService tries to start at runtime.
SCSS compilation now must be done manually with the CLI
or by temporarily re-adding the package. Instructions added
to AGENTS.md.
- MediaController: accept ?token=<jwt> query param as alt auth for browser img requests,
validating against signing key via JwtSecurityTokenHandler
- Home.razor: append current JWT token to all thumbnail/preview URLs,
update on AuthInfoChanged, empty when not logged in
- SettingRange.razor: add MinPowerValue/MaxPowerValue params; component computes
log2 exponents internally when set, falls back to MinValue/MaxValue for
non-Po2W sliders (TimePicker)
- Settings.razor: Po2W case passes pixel values via MinPowerValue/MaxPowerValue
instead of exponents; move Thumbnail Size and Preview Size to Jobs tab
- DefaultSettings.json: Thumbnail 64-1024 px, Preview 256-4096 px (split ranges)
- Migration: ConvertPo2WOptionsToPixelValues — converts existing Po2W Options
from exponents to actual pixel values via POWER(2,n), guarded by < 32 check
to skip already-migrated rows