The AuthInfoChanged handler was calling StateHasChanged() during
OnInitializedAsync (fired by JwtTokenRefresher when refreshing the
token during LoadPeople). This conflicted with Blazor's initial
render cycle, leaving the component stuck in a loading state when
the result was empty.
Changes:
- Remove StateHasChanged() from AuthInfoChanged handler (token
update only, consistent with CosplayerDetail pattern)
- Extract named event handler methods for proper unsubscribe
- Implement IDisposable to unsubscribe from singleton events
- Always assign GetAllAsync result (including empty) to people
- CosplayerDetail.razor: add InvariantCulture to @z (missed in prior fix)
- ProgressBar.razor: add InvariantCulture to seg.Width and Value in width% styles
- Remove SendWithRefreshAsync method from AuthServiceBase (now dead code)
- Replace all 15 SendWithRefreshAsync call sites in 7 services with
direct Client.XxxAsync calls
- Remove unused using System.Net from AuthServiceBase
- Update AGENTS.md to document JwtTokenRefresher as the sole token
refresh mechanism
- Move AuthServiceBase from ServiceBase.cs into its own file
- Replace FetchLoggedUserAsync retry loop with re-authentication on
failure, then single recursive retry
- Fix override keyword order in App.razor OnInitializedAsync
- Clean up MainLayout.razor inject (unqualified type name)
- Users: changed from blanket Forbid to ownership check (can update own albums)
- Curators: changed from ownership check to full access (can update any album)
- PersonForm and AlbumForm now call lockBodyScroll/unlockBodyScroll via JS
when Show state changes, preventing the body scroll context from
interfering with position:fixed on mobile browsers
- added minimal mobile CSS: align-items:flex-start, padding-top, and
reduced max-height:85vh on modal-content for small viewports
- safe disposal: unlocks body scroll on DisposeAsync if modal was open
fix(AlbumCard): add null check for Album.Person before rendering link
The ::deep combinator in CosplayerDetail.razor.css cannot target
.modal-overlay rendered by the PersonForm child component because
Blazor's CSS isolation applies the child's scope attribute to its
root, and ::deep only strips the parent's scope — it doesn't match
elements bearing only the child's scope.
- #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
d-none d-sm-flex hides the progress bar but also changes its
display from inline to flex on desktop, breaking visibility.
Remove the hide-class — the progress bar is always shown.
SettingBox base now uses JS interop to call showToast() which
renders a Bootstrap Toast at the bottom-right corner. Removed
all SaveIndicator RenderFragments and IsSaving/IsSaved state
from the four child components. Toast auto-hides after 2s.
Remove d-flex justify-content-between from all card bodies.
Description always sits above the input/control row, and the save
indicator sits inline on the same row as the control. Removed
mobile-specific stacking workaround and save-indicator CSS class
since layout shift is impossible with this structure.
Always render the save indicator container with a fixed 5rem min-width,
only toggle the inner text/color. Previously the conditional Rendering
caused the element to appear-disappear-resize, shifting nearby inputs.
Add .save-indicator CSS class with fixed min-width (4rem) and
text-align:right. Apply to all four setting box types so
'Saving...'→'Saved' text change doesn't shift adjacent
controls.
On ≤576px viewports, switch card body from side-by-side to
stacked layout: description text on top, input/slider
control below. Prevents text clipping and control overflow.
Hide non-essential columns (child summary, dates/progress bar,
elapsed time) on mobile using d-none d-sm-inline/d-sm-flex.
Add flex-wrap to main row for overflow safety on very
narrow viewports.
Use Bootstrap's recommended pattern: .row.g-2 container with col-* grid
classes for item widths, Masonry.js with percentPosition:true for
vertical positioning. Extracted infinite scroll observers into separate
infiniteScroll.js. Removed custom column-width JS, imagesLoaded CDN,
and the old masonryObserver.js.
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.
- Pass PollInterval from Jobs page to JobTree so children refresh at the
same rate as parent jobs (instead of hardcoded 1s)
- Add @key on JobRow (by job Id) and group divs so Blazor reuses DOM
nodes across polls instead of tearing down and rebuilding
- Redirect non-admin users to / on Jobs and Settings pages
- Fix date formatting in User page
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)
Backend (Lactose):
- Dispose DI scopes in JobManager.CreateJob to fix memory leak
- Check t.IsFaulted in Job.ContinueWith so unhandled exceptions surface as
failed jobs instead of being silently treated as completed
- Batch asset processing in ThumbnailJob (200 per batch) with paginated DB
queries instead of loading all 1.3M+ assets into memory at once
- Replace busy-wait with Thread.Sleep(100) to avoid CPU spinning
Frontend (MilkStream.Client):
- Auto-refresh child jobs every 1s in JobTree when a parent is expanded
and still active (Queued/Running/Waiting), stopping on collapse or
terminal status
fix(frontend): Fixes time being interpreted as local time instead of UTC time for job cards.
feat(frontend): Adds more job types buttons on the job page