- 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)
- LoginService.InitializeAsync: after fetching LoggedUser, check IsBanned
and DeletedAt. If banned/disabled, set ForceLogoutReason, call Logout
to clear state, and return false
- App.razor: after InitializeAsync completes, check ForceLogoutReason
and navigate to /login if set. This handles the timing issue where
MainLayout isnt created yet when the event fires on page load
Refs #45
- AuthController: fix HTTP 100 Continue → 403 Forbidden for banned/disabled
users in both Login and RefreshToken endpoints
- LoginService.Login: return error message from AuthResultDto on failure
instead of a generic message
- LoginService.Reauthenticate: parse AuthResultDto on refresh failure;
if "User is banned" or "User is disabled", trigger force logout via
new ForceLogout event, setting ForceLogoutReason property
- MainLayout: subscribe to ForceLogout event, redirect to /login
- Login.razor: display specific error messages (ban reason, force-logout
reason) instead of generic "Login failed"
Refs #45
- Add AdminUsers.razor page at /Users with user listing, ban/unban toggle,
access level change, delete, and create user functionality
- Extend UserService with GetAllUsersAsync, GetUserByIdAsync,
UpdateUserAsync, DeleteUserAsync, CreateUserAsync methods
- Add Users link to NavMenu dropdown for admin users
- Fix ban/unban bug in UserController where BannedAt was only updated
if the user had been previously banned
Closes#45
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.
- JwtTokenRefresher: remove ForceLogout that was clearing auth on refresh
failure and redirecting to login. Instead return a synthetic 401 so the
service-layer handling can retry.
- AuthServiceBase: add LoginService property and SendWithRefreshAsync
helper — wraps any HTTP call, catches 401, refreshes token via
Reauthenticate(), updates DefaultRequestHeaders, retries once.
- JobsService: remove local SendWithRefreshAsync, use base class version.
- SettingsService, FoldersService: wrap GET calls with
SendWithRefreshAsync.
- LoginService: fix Logout to also clear LoggedUser.
Replace two separate settings pages with a single unified page featuring:
- Sidebar navigation with 6 categories (General, Scanning, Upload, Paths, Jobs, Folders)
- Auto-save with 500ms debounce and visual feedback per setting
- Inline folder management as a dedicated tab
- Removed duplicate BeginSave event subscription bug in SettingBox components
Breaking: SettingsService.BeginSave event and OnBeginSave() removed
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)
The JwtTokenRefresher DelegatingHandler was not reliably intercepting
401 responses in Blazor WASM. Add a SendWithRefreshAsync helper to
JobsService that catches 401, calls Reauthenticate() to get a fresh
token, updates DefaultRequestHeaders, and retries once. This ensures
the Jobs page transparently recovers from token expiry without
showing an error.
- Inject NavigationManager and ILocalStorageService into JwtTokenRefresher
- Extract TryRefresh() helper to avoid repeating refresh logic
- Add ForceLogout() to LoginService that clears both AuthInfo and LoggedUser,
firing AuthInfoChanged and LoggedUserChanged events
- When an auth refresh fails (both pre-emptive and reactive paths), call
ForceLogout(), clear persisted auth from localStorage, and redirect to /
with a full reload so the user sees the login page
- Fire AuthInfoChanged event after successful Reauthenticate() so all
AuthServiceBase instances update DefaultRequestHeaders with the new
token, preventing cascading failure on the next poll cycle
- Wrap RefreshJobs() poll body in try-catch so transient errors don't
crash the component