Skip to content

V2.4 UI improvements, gears and collectibles, books api improvements#467

Open
devpro wants to merge 28 commits into
mainfrom
feature/amazon-gdpr
Open

V2.4 UI improvements, gears and collectibles, books api improvements#467
devpro wants to merge 28 commits into
mainfrom
feature/amazon-gdpr

Conversation

@devpro

@devpro devpro commented Jul 23, 2026

Copy link
Copy Markdown
Owner

No description provided.

devpro added 22 commits July 19, 2026 16:55
[PersistentState] is fully reverted in TvShowDetail.razor — Show/Reference/Episodes are back to private fields _show/_reference/_episodes, OnParametersSetAsync always calls LoadAsync (no restore-skip path), matching how Watch Next handles it. The _loading/_loaded flash fix stays untouched. Build passes. I also updated docs/prerender-flash-fix.md to record why (unbounded embedded Episodes list hit the same 32 KB SignalR ceiling as Watch Next's aggregate list).
… and Album, reproducing Book's exact pattern end-to-end

Backend: added CustomImageUrl to VideoGameModel/AlbumModel (Domain), VideoGame/Album entities (custom_image_url BSON field), VideoGameDto/AlbumDto (with matching doc comments), and updated VideoGameController/AlbumController.OnListMappedAsync to apply the override after reference-cover hydration, same as BookController. No mapper changes needed — it's a plain matching-name scalar, auto-mapped by Mapperly.

UI: both detail pages now compute coverUrl with the same precedence (CustomImageUrl if set, else the linked reference's cover), and have a "Cover image URL" text input with the same conditional placeholder and @onchange save pattern as BookDetail.razor.

Tests: added *ResourceList_CustomImageUrlOverridesTheLinkedReferencesCover integration tests for both types (mirroring Book's), plus CustomImageUrl fuzzing in the existing full-cycle round-trip tests. All 12 Book/VideoGame/Album integration tests pass against real MongoDB.

Visual verification: extended MobileScreenshotTest (the project's established visual-review harness) to seed a custom cover URL on the seeded Album/VideoGame and capture their detail pages by title. Ran it with E2E_SCREENSHOTS=true — screenshots confirm both pages render the custom cover (overriding the real RAWG/Discogs art) and the populated input field, matching Book's UX exactly.
What it does: a new "generic video game transaction import" (/import/video-games, POST /api/import/video-games/preview|commit) that parses a CSV (Transaction Date, Game Name, Product Name, Platform, Vendor, Transaction Id, Order Id, Final Price (€)) into review rows and commits selected ones as VideoGame items — same review-then-commit UX as the existing Amazon import, merging same-titled rows into one game with multiple Platforms entries.

Key changes:
- Shared engine extracted: the title-merge/dedup logic (ComputeCommitPlan/FindImportedReferences) was pulled out of AmazonImportMergeService into a new OwnedItemImportMergeService, since it was already fully generic. Amazon's own code/tests were only touched for the resulting rename, no behavior change.
- New VideoGamePlatformModel.ProductName field (store's specific edition/product text), now editable on VideoGameDetail.razor via a new ExtraFields slot added to the shared OwnedVersionFields component.
- Bug found and fixed: a PSN transaction can bundle several different products under one shared Transaction Id/Order Id (confirmed against your real export — three "Far Cry 4" DLC packs, one transaction). The dedup Reference was colliding across those lines and silently dropping platforms. Fixed by disambiguating with Product Name, mirroring how Amazon's import already uses ASIN for the same purpose.
- Reconciliation reporting: RowsImported/SkippedRowTitles were added so the import result screen shows "X of Y selected rows imported" plus a named list of anything actually skipped, since the created/merged-item counts alone can look short even when nothing was lost (rows sharing a title consolidate into one item).
Reuses the plain-scalar shape already established for these three types
(no reference-linking, so no override/precedence logic needed): shown as
a thumbnail on the list page and editable on the detail page header.
Mirrors the list pages' existing URL-state pattern (navigate, don't
mutate) so browser back/forward and page reloads restore the tab that
was showing, instead of always resetting to the first one.
…bum)

Mirrors VideoGamePlatformModel.ProductName, generalized onto the one
shared owned-copy shape so it's per-copy for free: a season 6 Blu-ray,
a season 1 digital copy, and an old season 1 DVD can each carry their
own store listing text. Rendered through OwnedVersionFields' existing
ExtraFields slot in OwnedVersionsEditor, shared by all four types.
Scaffolded by direct analogy to Album (CLAUDE.md's "Adding a new
trackable item type" recipe), minus reference-linking since neither
type has an external data provider. Both are member-only, minimal
(title, brand, year, notes, optional cover image, favorite, owned
versions) and share the OwnedVersionsEditor/OwnedVersionFields
components used by every other owned-copy type.
The new API clients were never registered in BlazorApp's
AddWebApiHttpClient, so opening Collectibles or Gear threw
InvalidOperationException at render time - a registration this
codebase keeps in a separate file from where the client classes live,
which the earlier implementation missed entirely.

Also replaces the cramped ItemThumb-next-to-title treatment on detail
pages (Car/House/HealthProfile/Collectible/Gear) with the same
full-width banner-image pattern VideoGameDetail/AlbumDetail already
use. On Car specifically, the banner and URL field now sit below the
energy type buttons rather than above the other fields.
…here

List pages (Cars/Houses/Collectibles/Gear) now pass ItemImageShape="wide"
like VideoGames.razor, instead of the default portrait shape that badly
cropped landscape photos of cars/houses/objects. HealthProfile keeps the
portrait default since its subject is a person.

Detail pages (Car/House/HealthProfile) now render the cover image as its
own standalone kt-form-card p-0 banner, matching what Collectible/Gear
already do (and what VideoGameDetail/AlbumDetail established) - not
crammed inside the multi-field card after the last text field.
- Moved SetFieldAsync (fill + blur, for @onchange-bound fields) off BookDetailPage onto the shared DetailPageBase — it was never Book-specific, and I'd compounded the existing wart by copying BookDetailPage.SetFieldAsync into five new unrelated tests. Also fixed the three pre-existing misuses (BookSmokeTest, OwnershipSmokeTest, QuickAddSmokeTest) while I was in there.
- Found ReferenceableDetailPageBase already had a CoverImage locator (role=img, alt ending in "cover"/"poster") for the reference-linked types. Promoted it to DetailPageBase instead of reinventing it, added ImageUrlInput alongside it, and fixed Car/House/HealthProfile's alt text (was just @name, now @name cover) so they actually match that convention.
- Rewrote all five smoke tests (Car, House, Health, Collectible, Gear) to use detail.ImageUrlInput / detail.CoverImage / DetailPageBase.SetFieldAsync instead of raw inline selectors.
Reproduced the failure directly (ran the smoke test 5+ times against the real Google Books API) and found two separate, real issues:

1. Google Books genuinely is having a rough day — retries already existed (3 retries via AddStandardResilienceHandler), but weren't always enough to ride out sustained 500/503s. Fixed: added AddBookProviderResilienceHandler (ProviderResilienceExtensions.cs) with 5 retries / shorter backoff / wider total timeout, applied to Google Books + Open Library + BnF in Program.cs. A run after this change took 32s (visibly working through retries) and passed.
2. Google Books responses were needlessly heavy — confirmed with curl: an unfiltered search response was ~75KB vs TMDB's ~14KB for a comparable query, explaining the "always slower than other providers" feel independent of today's outage. Fixed: added Google's fields= partial-response parameter to both the search and details calls in GoogleBooksClient.cs, restricted to exactly the fields the app actually reads.
3. Bonus UI bug found while reproducing: InlineReferenceLinker.razor showed "No results" and the raw HttpRequestException message simultaneously on a search failure, and the raw exception text wasn't user-friendly. Fixed both.
Added Gear and Collectible as importable types in the Amazon order-history CSV import feature, mirroring the existing Movie/TvShow handling exactly (both use the same OwnedVersions shape):

- AmazonImportMediaType — added Gear, Collectible enum members.
- AmazonImportCommitResultDto — added Gear*/Collectibles* created/merged/skipped counts.
- AmazonImportController — injected IGearRepository/ICollectibleRepository; extended Preview (already-imported check) and Commit (create/merge block) with two more cases.
- AmazonImportPage.razor — added the two options to the type dropdown and two lines to the result summary.
- Tests — extended AmazonFixtureCsvBuilder with Gear/Collectible fixture rows, and AmazonImportResourceTest with full round-trip assertions (preview, commit, re-import dedup, cleanup).
- Updated a couple of doc comments (AmazonImportController, AmazonOwnedItemImportRequestItem) that used to enumerate only Book/Movie/TvShow/VideoGame.
Backend (5 domains: TvShow, Movie, Book, VideoGame, Album):
- Added DeleteAsync(string id) to each I<X>ReferenceRepository + Mongo implementation — the shared reference collections had no delete path before.
- Added Unlink<X>ReferenceAsync(model) to ReferenceEnrichmentService's partials — clears the tenant's ReferenceId and unconditionally deletes the shared reference document.
- Added POST /api/<x>/{id}/unlink-reference to each controller, gated by a method-level [Authorize(Policy = "AdminOnly")] on top of the controller's own policy.

Frontend:
- Added UnlinkReferenceAsync to each <X>ApiClient.
- Added an admin-only trash-icon button next to the ↻ refresh icon on all five detail pages, behind a ConfirmModal (since it's destructive/irreversible). On confirm, it unlinks, reloads the item, and shows an inline toast — InlineReferenceLinker reappears automatically since it's already conditioned on an empty ReferenceId, so the admin can immediately search and pick the correct match.

Tests: new UnlinkReferenceResourceTest.cs (5 integration tests, verifying the link is cleared and the reference document is actually deleted) and UnlinkReferenceAuthorizationTest.cs (reflection guard confirming the AdminOnly policy can't be silently dropped, since the shared Firebase test account is itself an admin and can't exercise a 403 over HTTP).
- New user-preferences pattern: UserPreferencesModel/Repository/StorageMapper (Domain + Infrastructure.MongoDb), UserPreferencesDto/DtoMapper/UserPreferencesController (WebApi, GET/PUT /api/user-preferences), a unique owner_id index, and a UserPreferencesApiClient on the Blazor side — designed so a future toggle is just one more named boolean, no new plumbing.
- /account/manage: now has a "Preferences" section with a "Show chasse-aux-livres.fr link on book pages" toggle that persists immediately.
- BookDetail.razor: the ISBN field is now an input-group; when an ISBN is set and the preference is on, a ↗ button opens https://www.chasse-aux-livres.fr/<isbn> in a new tab.
try
{
#pragma warning disable S5693
await using var stream = e.File.OpenReadStream(MaxFileSize);
try
{
#pragma warning disable S5693
await using var stream = e.File.OpenReadStream(MaxFileSize);
devpro and others added 2 commits July 24, 2026 00:12
…es, date parsing, regex timeouts, cognitive complexity

- Replace throw-on-first-match foreach loops with FirstOrDefault+throw (S1751)
- Extract repeated string literals to constants (S1192)
- Simplify Count-1 indexing and a synchronous filter loop to LINQ (S6608, S3267)
- Deduplicate the identical refresh-reference nested ternary across five detail
  pages into ReferenceRefreshMessage.Compute, and the identical DateOnly
  HTML-input parsing across eight sites into DateOnlyInput.Parse - both fix
  S3358/S6580 while removing the underlying duplication
- Add a match timeout to every Regex construction/replace missing one (S6444)
- Extract per-row/per-item helper methods in HealthImportService and
  OwnedItemImportMergeService to bring cognitive complexity back under
  threshold (S3776), verified against existing unit tests plus a new
  DateOnlyInputTest

S8970 (null-forgiving operator removal) and the S5332 http:// BnfClient XML
namespace URIs were investigated and left alone - both are Sonar false
positives confirmed by a real compiler error and by XML namespace semantics
respectively. S107/S6960 (parameter counts, "split this controller") are
architectural, matching this codebase's documented DI-heavy/one-controller-
per-feature conventions, so left as-is.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E91RS1SmtWz2CBqk2YbEby
The commit button and its warning text now also account for selected video-game rows with no platform chosen, mirroring the existing "pick a type" guard, so the 400 case can no longer be triggered from the UI.
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
4.9% Duplication on New Code (required ≤ 3%)
C Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

devpro and others added 4 commits July 24, 2026 00:53
SaveAuthorAsync/SaveArtistAsync had copied the Title field's
never-allow-empty guard, so clearing the input silently skipped the
API call and the old value came back on reload. Only Title (and
Name-equivalent identity fields on other types) should require a
non-empty value.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
One reusable component (SuggestInput.razor in Components/Inventory/Shared/) instead of two half-solutions:
- A real text input — filters suggestions as you type (Contains, capped to 8 matches, so a long category list never dumps in full), click one to fill it, or keep typing to enter something new. No "click and it's stuck" behavior — the input stays live.
- Suggestions render as a small dark-themed dropdown menu (.kt-autocomplete-menu) built from the app's own CSS variables, not a native <datalist> popup, so it actually respects the dark theme and sits in normal flow instead of floating over other fields.

List page: the earlier <select> was the actual bug behind "pushed to another line for nothing" — Bootstrap's .form-select forces width: 100%, and inside a flex row that percentage resolves against the row itself, which is what forced the wrap even with room to spare. SuggestInput sizes its wrapper to a fixed 150px flex item (.kt-category-select, same non-growing/non-shrinking shape as the existing .kt-sort-select), so it behaves predictably and sits inline with Favorites/Owned like it should.

Detail page: replaced the wall of chips with the same component — type to filter, pick a match or type a new category, no dead popup overlapping Notes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants