- 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
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.
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.
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.
Same batch processing pattern as ThumbnailJob:
- Paginated DB queries (200 per batch) via Count + Skip/Take methods
- Atomic counters with Interlocked.Increment
- Thread.Sleep(100) polling instead of busy-wait
- Added failedAssets tracking to MetadataJob (was missing)
- Refactor PHashJob to match ThumbnailJob's master/sub-job pattern:
- return after JobStatus.Fail() so failures stay Failed
- track failedAssets counter, use CompleteWithErrors
- descriptive sub-job names (pHash for {path})
- lock-based sub-jobs list for thread safety
- Fix JobTree.razor: re-fetch children when parent still active
- OnExpanded invalidates _childrenCache if job is Queued/Running/Waiting
- Fix scope leaks in IntegrityCheckJob and JobScheduler.FetchSettings
- Increase JobRecord.Name to VARCHAR(2048) to prevent truncation
- Add debug logging for sub-job completion tracking
- Use Interlocked.Increment with Volatile.Read to fix cross-thread
visibility of the processed count
- Add await Task.Delay(100) in the wait loop to yield the thread
- Fix inverted cancellation check
Achieves 100% XML doc coverage on non-trivial types with CS1591
enforcement via Directory.Build.props.
Coverage by project:
- Butter: from 6.5% to 100% (DTOs, enums, MIME types)
- Lactose: from ~28% to 100% (controllers, services, repos, jobs, models)
- MilkStream.Client: from ~29% to 100% (all frontend services)
Uses <inheritdoc /> on repository implementations (interfaces
already documented) and full <summary>/<param>/<returns> tags
elsewhere. Enums include member-level docs.
- Split /api/jobs into three endpoints: active roots, paginated past,
children fetched on expand — reduces response from ~1MB to ~5KB
- Persist completed jobs to PostgreSQL via JobRecord entity + migration
- Add configurable job retention cleanup (hourly timer, default 30 days)
- Fix PHashJob master not persisting (missing Complete() call)
- Add PastJobsResponse DTO to fix case-sensitive JsonElement deserialization
- Refactor JobTree/JobRow for lazy-loaded children via FetchChildren callback
- Handle 401 responses in JwtTokenRefresher with token refresh + retry
- Remove JobChildrenSummary component (replaced by inline parent summary)