Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
db5d796
Add sonar issues in code quality findings docs
devpro Jul 26, 2026
58686f8
Bump version 2.4.1
devpro Jul 26, 2026
372de12
Fix sonar warnings S2365, ASP0025 ×2, CA1862 ×2, CA1859 ×3, JS S2486
devpro Jul 27, 2026
b5c9768
Add list/thumbnail view toggle to inventory list pages
devpro Jul 27, 2026
27f1aef
Add Playwright smoke tests for the three import pages
devpro Jul 27, 2026
49b261a
Add Playwright smoke test for the reference-data admin page
devpro Jul 27, 2026
d180759
Add Playwright smoke test for the account/preferences page
devpro Jul 27, 2026
ba43822
Refine thumbnail grid: side margins + shape-aware column widths
devpro Jul 27, 2026
62b246e
Add bottom padding below the thumbnail grid
devpro Jul 27, 2026
64e7f1a
Fix time entry issue on mobile view
devpro Jul 27, 2026
4409313
Persist the list/thumbnail view as a per-device preference
devpro Jul 27, 2026
ba92c7b
Add the list/thumbnail view toggle to Wishlist and Watch Next
devpro Jul 28, 2026
064d096
Add generic import
devpro Jul 28, 2026
520c98e
Fix UI
devpro Jul 28, 2026
06e5477
Fix button on thumbnails
devpro Jul 28, 2026
1b3c728
Improve video game detail
devpro Jul 28, 2026
620cfaa
Share media items with another user
devpro Jul 28, 2026
c89eae2
Share car/house/health
devpro Jul 28, 2026
29e4825
Improve shared personal lists
devpro Jul 28, 2026
9a3aa40
Share collectibles and gears
devpro Jul 28, 2026
25798c9
Order tabs in sharing view
devpro Jul 28, 2026
fbfb328
Fix custom image display in wishlist
devpro Jul 29, 2026
1fd6848
Add test
devpro Jul 29, 2026
14fd026
UI title refresh improvement
devpro Jul 29, 2026
5fc8e8d
Fix 'RemoteNavigationManager' has not been initialized error
devpro Jul 29, 2026
42ecee2
Add Playwright tests
devpro Jul 29, 2026
8c69a84
Add playwright test for collection sharing
devpro Jul 29, 2026
cf96098
Remove tv show want to watch
devpro Jul 29, 2026
32d1201
Optimize tv show in watch next
devpro Jul 29, 2026
c40758b
Scoped change to the Watch Next thumbnail (grid) view for the TV show…
devpro Jul 29, 2026
df3df02
Update tv show status to current if new episodes appear
devpro Jul 29, 2026
a039397
Add movie rating from tmdb
devpro Jul 29, 2026
4e61434
Add ratings for other media types
devpro Jul 29, 2026
ceab4d7
Use openlibrary to get book rating
devpro Jul 30, 2026
f8d8569
Add doc for reference ratings
devpro Jul 30, 2026
9381be3
Admin select reference rating source
devpro Jul 30, 2026
bee098a
Increment 2 adding IMDB
devpro Jul 30, 2026
894f8be
Add explore for tv shows and movies
devpro Jul 30, 2026
a5a4d63
Add explore for video games and fix dismissal design
devpro Jul 31, 2026
0d03dd7
Clean db when tests run
devpro Jul 31, 2026
c38be22
Update health check endpoint to healthz
devpro Jul 31, 2026
6c1c2f5
Order import
devpro Aug 1, 2026
035b63a
Cosmetic code change
devpro Aug 1, 2026
146e858
Update min metacritic to 60 (instead of 70)
devpro Aug 1, 2026
3e42a63
Format md files with neatmd
devpro Aug 2, 2026
547207c
Improve md files
devpro Aug 2, 2026
d1abc19
Improve md
devpro Aug 2, 2026
22b13b0
Fix test file format
devpro Aug 2, 2026
5c553d9
Fix
devpro Aug 2, 2026
0e02237
FIx test with new health check endpoint
devpro Aug 2, 2026
de9a2ca
Explore fetch in database
devpro Aug 2, 2026
50bd3f6
Fix FindExternalIdsAsync
devpro Aug 2, 2026
fecf869
Enforce OMDb's daily quota with a replica-shared budget
devpro Aug 2, 2026
674183c
Refresh the stalest reference documents first, bounded per pass
devpro Aug 2, 2026
fc77e4b
Stamp the rating source on tenant items so recompute can do nothing
devpro Aug 3, 2026
fa6572f
Improve Playwright tests
devpro Aug 3, 2026
3a50920
Attemps scale and recompute
devpro Aug 3, 2026
3d90bba
Deleting a TV show now cascades to its episodes
devpro Aug 3, 2026
0e790b5
Add Playwright coverage for the Explore feature
devpro Aug 3, 2026
28f8089
Fix invalid md
devpro Aug 3, 2026
a8fdf40
Add IGDB as the default video game reference provider
devpro Aug 3, 2026
10434ec
Move to optional force to sync now and fix book issue
devpro Aug 4, 2026
63da11f
Fix line length
devpro Aug 4, 2026
fafbccb
Upsert on import based on ref id
devpro Aug 4, 2026
b6d5734
Fix/implement 404
devpro Aug 4, 2026
8d599f3
Fix invalid order
devpro Aug 4, 2026
c56549d
Add env var in ci for integration tests
devpro Aug 4, 2026
5496562
Improve reference import
devpro Aug 4, 2026
872f700
Update CI
devpro Aug 4, 2026
c9f39a7
Fix test depending on Open Library for book ref
devpro Aug 4, 2026
362c1d3
Search books by ISBN through every provider, and report provider fail…
devpro Aug 5, 2026
13a3c57
Stop stamping a reference as enriched when the spent OMDb quota skipp…
devpro Aug 5, 2026
df64aac
Let RAWG overwrite its own cover art again
devpro Aug 5, 2026
5e17d5e
Make Explore recognise the video games the owner already tracks again
devpro Aug 5, 2026
5d9a3aa
Stop Discogs' free-text search from burying the album being searched for
devpro Aug 5, 2026
a9f0045
Fix format in test file
devpro Aug 5, 2026
5cf7702
Let an Explore card open the title at the provider it was rated by
devpro Aug 6, 2026
d4ce0f6
Reach the video games a provider's own search can't find
devpro Aug 6, 2026
0ba5208
Rename env var for mobile test
devpro Aug 6, 2026
6ddad5a
Diagnose a failed e2e run, and stop the tests leaving data behind
devpro Aug 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 6 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,12 @@ jobs:
dotnet-test-args: "--report-xunit-trx --coverage --coverage-output-format cobertura"
extra-vars: |
AllowedOrigins__0=5207
AllowedOrigins__1=7042
Features__IsScalarEnabled=true
Features__IsHttpsRedirectionEnabled=false
Infrastructure__MongoDB__ConnectionString=mongodb://localhost:27017
Infrastructure__MongoDB__DatabaseName=keeptrack_ci
Logging__LogLevel__Keeptrack=Debug
fossa-enabled: true
sonar-enabled: true
sonar-cpd-exclusions: "**/*.g.cs,**/*.generated.cs,**/src/WebApi.Contracts/Dto/*.cs,**/src/Domain/Models/*.cs,**/src/Infrastructure.MongoDb/Entities/*.cs"
Expand All @@ -89,6 +91,10 @@ jobs:
Tmdb__ApiKey=${{ secrets.TMDB_APIKEY }}
Rawg__ApiKey=${{ secrets.RAWG_APIKEY }}
Discogs__Token=${{ secrets.DISCOGS_TOKEN }}
GoogleBooks__ApiKey=${{ secrets.GOOGLEBOOKS_APIKEY }}
Omdb__ApiKey=${{ secrets.OMDB_APIKEY }}
Igdb__ClientId=${{ secrets.IGDB_CLIENTID }}
Igdb__ClientSecret=${{ secrets.IGDB_CLIENTSECRET }}
FIREBASE_APIKEY=${{ secrets.FIREBASE_APIKEY }}
FIREBASE_USERNAME=${{ secrets.FIREBASE_TESTUSERNAME }}
FIREBASE_PASSWORD=${{ secrets.FIREBASE_TESTPASSWORD }}
Expand Down
1,810 changes: 896 additions & 914 deletions CLAUDE.md

Large diffs are not rendered by default.

145 changes: 111 additions & 34 deletions CONTRIBUTING.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<PropertyGroup>
<!-- edit this value to change the current MAJOR.MINOR.PATCH version -->
<VersionPrefix>2.4.0</VersionPrefix>
<VersionPrefix>2.4.1</VersionPrefix>
</PropertyGroup>

<Choose>
Expand Down
4 changes: 4 additions & 0 deletions Keeptrack.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@
<File Path="docs/automapper-removal-plan.md" />
<File Path="docs/backlog.md" />
<File Path="docs/code-quality-findings.md" />
<File Path="docs/igdb-api-notes.md" />
<File Path="docs/operations.md" />
<File Path="docs/plan-quick-add.md" />
<File Path="docs/playwright-e2e-tests-plan.md" />
<File Path="docs/prerender-flash-fix.md" />
<File Path="docs/reference-ratings-plan.md" />
<File Path="docs/share-collections-plan.md" />
<File Path="docs/testing-assessment.md" />
</Folder>
<Folder Name="/Solution Items/pipelines/" />
<Folder Name="/Solution Items/pipelines/azure-devops/">
Expand Down
465 changes: 463 additions & 2 deletions docs/code-quality-findings.md

Large diffs are not rendered by default.

126 changes: 126 additions & 0 deletions docs/igdb-api-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# IGDB API notes

Field-level notes about IGDB that aren't needed to read `IgdbClient.cs`, kept here rather than in `CLAUDE.md` so they cost nothing until someone actually needs them.
The behavioural findings that *do* shape the code (Twitch token auth, the 4 req/s ceiling, Apicalypse POST bodies, no Metacritic score) live in `CLAUDE.md`'s per-provider findings section.

Everything below was confirmed against the live API on 2026-08-03 unless explicitly marked otherwise.

## Query shapes in use

All three are POST bodies to `https://api.igdb.com/v4/games` with `Client-ID` and `Authorization: Bearer` headers.

Purpose | Body
--------------|-----
Details by id | `fields name,summary,first_release_date,genres.name,platforms.name,cover.image_id,rating,rating_count,aggregated_rating,aggregated_rating_count; where id = 1020;`
Search | `fields name,first_release_date,cover.image_id; search "Half-Life 2"; limit 5;`
Ranked page | `fields name,first_release_date,cover.image_id,rating,rating_count,aggregated_rating,aggregated_rating_count; where aggregated_rating_count >= 5; sort aggregated_rating desc; limit 500; offset 0;`

Notes:

- `first_release_date` is a unix timestamp in **seconds**.
- Images: see the dedicated section below - IGDB exposes three kinds and picking the wrong one is a visible regression, not a subtlety.
- `search` is relevance-ordered and genuinely noisy: "Half-Life 2" returns three MMod variants above the canonical game.
It also cannot be combined with `sort`, which is one reason the search path applies no year filter (the other being that an edition's year routinely differs from the one a tenant typed).
- Critic counts run an order of magnitude below user counts - 8 to 27 for top titles, against thousands of user ratings for the same games - which is why `MinCriticRatingCount` and `MinUserRatingCount` are so far apart.

## Images: `cover` vs `artworks` vs `screenshots`

IGDB exposes three separate image collections per game, and they are **different shapes**:

Field | What it is | Shape
--------------|---------------------|------
`cover` | Box art / packshot | Portrait, 3:4
`artworks` | Promotional key art | Landscape, usually 16:9
`screenshots` | In-game captures | Landscape, usually 16:9

**The app stores a landscape screenshot**, falling back to the cover when a game has none.

That is dictated by the UI, not by preference: `VideoGameDetail.razor` renders the stored image full width capped at 320px tall, and `VideoGames.razor` passes `ItemImageShape="wide"` for a 16:9 tile - both with `object-fit: cover`.
Those layouts were built around RAWG's `background_image`, which was landscape key art.
Hand them portrait box art and the crop takes a horizontal band out of the middle and loses the title - confirmed on "Red Dead Redemption 2" and "Baldur's Gate", which is what prompted this.
Movies and TV are the opposite case and keep portrait posters.
**The shape a domain stores follows the layout that displays it, not the provider's preference.**

### `artworks` is a trap - use `screenshots`

`artworks` looks like the obvious choice for a landscape hero and is not.
It is **contributed, not curated**, and unreliable in both aspect ratio and quality.
Inspected directly:

Game | `artworks[0]` | `screenshots[0]`
----------------------|----------------------------------------------------|-----------------
Red Dead Redemption 2 | 720x720 (**square**), posterised, dithered fan art | 1280x720, clean in-game frame
Baldur's Gate III | 1274x720, a bare logo on a black background | 1280x720, clean in-game frame

A square artwork in a 16:9 crop is mangled, and a logo makes a poor hero image.
Screenshots were 1280x720 in every sample and are actual game footage - the closest equivalent to RAWG's `background_image`.
Their one downside is that in-game HUD is sometimes visible (Baldur's Gate III's has a minimap and hotbar), which is the accepted trade.

**Do not "improve" this by preferring `artworks`** - that was tried first, and it is what made the detail page look worse than RAWG's, not better.

Two places deliberately use the cover instead:

- **Admin search results**: the candidate list renders a small 60x90 portrait thumb, which box art fits.
- **The top-rated ranking query** requests **no** landscape image at all.
Explore's cards are portrait (shared with movies and TV, which use posters), so box art is the shape that actually suits them - and it keeps the one query that returns 500
rows down to a single image id per game.

### Size tokens

Measured on the live CDN against a real cover id.
All cover tokens preserve the 3:4 aspect (IGDB scales to fit rather than letterboxing), so for a given image the token is a pure resolution choice - but the *source* image's own aspect is what
decides portrait vs landscape.

Token | Cover (3:4 source) | Weight
-----------------|--------------------|-------
`t_cover_small` | 90x120 | 3 KB
`t_cover_big` | 264x352 | 16 KB
`t_cover_big_2x` | 528x704 | 52 KB
`t_720p` | 540x720 | 55 KB
`t_1080p` | 810x1080 | 107 KB

The app uses `t_720p` for screenshots (1280x720 from a 16:9 source) and `t_cover_big_2x` for the cover fallback.
`t_cover_big_2x` rather than plain `t_cover_big` because TMDB posters arrive as `w500`, and 264px renders visibly soft beside them in the same list.

### Possible future change: more than one image per reference

`VideoGameReferenceModel` holds a single `ImageUrl`, which is what every media type does and has been fine so far.
Storing both a cover and a screenshot would let each surface use the shape that suits it - portrait box art on Explore cards and in the admin picker, a landscape screenshot on the detail banner -
instead of one image being cropped by one of them.
That is a model/mapper/DTO change across all three layers plus the detail and list templates, so it is worth doing only if the single-image compromise starts to grate.
The data is already there: `screenshots.image_id` and `cover.image_id` come back in the same request at no extra cost.

## `game_type` (formerly `category`)

`game_type` classifies what kind of release a record is: a parent title, a DLC, a remaster, a port, and so on.
It **replaced** `category`, which older documentation and most third-party examples still describe.

Two things make this worth writing down:

- **`category` no longer exists, and IGDB does not say so.** Requesting it returns nothing at all (it is silently dropped from the response), and `where category = 0` parses fine and matches **zero** documents.
A stale field name in a `where` therefore fails silently.
In the Explore refresh pass this is especially quiet, because a pass that returns nothing deliberately keeps the previous catalogue rather than emptying it - so the ranking simply stops updating, with no error anywhere.
Confirmed: `fields name,category,game_type; where id = 1020;` (Grand Theft Auto V) returns only `name` and `game_type: 0`.
- **`game_type = 0` means "main game".** Confirmed for GTA V. Filtering on it removes "Elden Ring: Shadow of the Erdtree" (DLC) and "The Last of Us Remastered" (a re-release) from the top of the critic ranking.

**Explore deliberately does not filter on it.** A DLC or a remaster is a first-class thing to track in Keeptrack, so a well-reviewed expansion is a legitimate suggestion rather than noise beside its parent, and both are freely searchable
and linkable either way (`SearchGamesAsync` applies no `where` clause at all).
The field is recorded here for a future advanced search - "main games only", "exclude ports", "show me expansions of things I own" are all one `where` clause once someone wants them.

### Enumerating the values

The ids are data, not a fixed enum, so read them from the API rather than trusting a list copied from a blog:

```bash
curl -s -X POST https://api.igdb.com/v4/game_types "${H[@]}" -d 'fields id,type; limit 50; sort id asc;'
```

Only `0` = main game is confirmed here.
The remaining ids are documented by IGDB as covering DLC, expansions, bundles, standalone expansions, mods, episodes, seasons, remakes, remasters, expanded games, ports, forks, packs and updates -
but the exact id-to-name mapping has not been checked against the live API, so run the command above before relying on a specific number.

## Things not yet explored

- **Multi-query** (`https://api.igdb.com/v4/multiquery`) and `where id = (1,2,3)` batching would let the nightly sync refresh a whole 500-document page in one request instead of 500.
Deliberately out of scope of the IGDB migration - it reshapes `ReferenceSyncService.SyncDomainAsync`'s shared contract, so it belongs in its own change.
- `involved_companies`, `franchises`, `collections` and `similar_games` are all available and unused; `similar_games` in particular could back a per-item "more like this" without any of Explore's ranking machinery.
47 changes: 26 additions & 21 deletions docs/plan-quick-add.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ Three extraction refactors come first — they remove exactly the duplication **
4. `IOwnedCopyDto` + `OwnedVersionFields`

- New `src/WebApi.Contracts/Dto/IOwnedCopyDto.cs`:
CopyType, Price, AcquiredAt, Vendor, Reference. Implemented by OwnedVersionDto and VideoGamePlatformDto (identical existing members; precedent: IReferenceLinkedDto; no Mapperly impact — mappers map members, not interfaces).
CopyType, Price, AcquiredAt, Vendor, Reference.
Implemented by OwnedVersionDto and VideoGamePlatformDto (identical existing members; precedent: IReferenceLinkedDto; no Mapperly impact — mappers map members, not interfaces).
- New `src/BlazorApp/Components/Inventory/Shared/OwnedVersionFields.razor`:
params required IOwnedCopyDto Copy, optional EventCallback OnChanged (default no-op for Quick Add's nothing-persists-until-Save flow).
Body = Physical/Digital button pair + the four fields (invariant-culture decimal parsing included), keeping data-testid="version-*-input".
Expand All @@ -79,7 +80,9 @@ Three extraction refactors come first — they remove exactly the duplication **
Movie + TV show always visible; the other six tiles AND their forms inside `<AuthorizeView Policy="MemberOnly" Context="memberContext">` with the preview-account note in `<NotAuthorized>` (hiding is UX; the API enforces).
- Media forms (per-type markup local to the page, same convention as the list FormTemplates):
- Movie: Title, Year, "Watched on" (FirstSeenAt, default today for the "just saw it" scenario — visible and clearable, since prefilling marks it Seen), Rating.
- TV show: Title, Year. Book: Title, Author, Year, FirstReadAt default today. Album: Title, Artist, Year (Author/Artist feed Open Library/Discogs auto-resolution).
- TV show: Title, Year.
Book: Title, Author, Year, FirstReadAt default today.
Album: Title, Artist, Year (Author/Artist feed Open Library/Discogs auto-resolution).
- Video game: Title, Year, Platform `<select>` over VideoGames.VideoGamePlatforms; choosing a platform adds a VideoGamePlatformDto draft rendered via `<OwnedVersionFields Copy="...">` — no platform = unowned game.
- Movie/TvShow/Book/Album: "I own a copy" toggle revealing `<OwnedVersionFields>`; on save if toggled, `dto.OwnedVersions = [_ownedDraft]` (a bare Physical copy is legitimate).
Single POST carries the copy — no follow-up PUT.
Expand All @@ -88,28 +91,30 @@ Three extraction refactors come first — they remove exactly the duplication **

6. Quick Add page — record types

- Parent fetch on type selection via `CarApiClient/HouseApiClient/HealthProfileApiClient.GetAsync("", 1, 100)`, guarded by a per-type loaded flag.
- 0 parents → empty-state note linking to /cars / /houses / /health to create one; 1 → preselected silently (the common "my car" case);N → segmented button row.
- Below it: `<CarHistoryForm Entry=... ShowFuel/ShowElectric from the selected car's EnergyType/>`, `<HouseHistoryForm/>`, `<HealthRecordForm/>`. Selecting a parent stamps the Entry's parent id; Save disabled until a parent is selected.
- SaveRecordAsync → child client AddAsync → navigate /cars/{carId} / /houses/{houseId} / /health/{profileId}.
- Parent fetch on type selection via `CarApiClient/HouseApiClient/HealthProfileApiClient.GetAsync("", 1, 100)`, guarded by a per-type loaded flag.
- 0 parents → empty-state note linking to /cars / /houses / /health to create one; 1 → preselected silently (the common "my car" case);N → segmented button row.
- Below it: `<CarHistoryForm Entry=... ShowFuel/ShowElectric from the selected car's EnergyType/>`, `<HouseHistoryForm/>`, `<HealthRecordForm/>`.
Selecting a parent stamps the Entry's parent id; Save disabled until a parent is selected.
- SaveRecordAsync → child client AddAsync → navigate /cars/{carId} / /houses/{houseId} / /health/{profileId}.

1. Entry points + CSS
7. Entry points + CSS

- NavMenu.razor: first item inside `<AuthorizeView><Authorized>` (directly below Home): `<NavLink class="nav-link" href="add"><span class="nav-icon">+</span> Quick add</NavLink>`.
Verify + (U+FF0B) has default text presentation per the emoji gotcha; plain ASCII + is the safe fallback.
- Home.razor: stats variant gets a kt-home-cta "+ Quick add" primary button above the stat grid; the empty-state variant makes Quick Add the primary CTA, "Go to my movies" secondary.
- CSS: mostly reuse (.kt-stat-grid/.kt-stat-tile, .kt-form-card, row g-3 with col-6/col-12 mobile splits, segmented buttons).
New app.css is minimal: a .kt-quickadd block forcing the picker to 2 columns under 767px and a full-width Save button on mobile. Forms live in .kt-form-card on a page, not a modal.
No sticky save bar in v1 (forms are short) — note as follow-up.
- NavMenu.razor: first item inside `<AuthorizeView><Authorized>` (directly below Home): `<NavLink class="nav-link" href="add"><span class="nav-icon">+</span> Quick add</NavLink>`.
Verify + (U+FF0B) has default text presentation per the emoji gotcha; plain ASCII + is the safe fallback.
- Home.razor: stats variant gets a kt-home-cta "+ Quick add" primary button above the stat grid; the empty-state variant makes Quick Add the primary CTA, "Go to my movies" secondary.
- CSS: mostly reuse (.kt-stat-grid/.kt-stat-tile, .kt-form-card, row g-3 with col-6/col-12 mobile splits, segmented buttons).
New app.css is minimal: a .kt-quickadd block forcing the picker to 2 columns under 767px and a full-width Save button on mobile.
Forms live in .kt-form-card on a page, not a modal.
No sticky save bar in v1 (forms are short) — note as follow-up.

1. Playwright coverage
8. Playwright coverage

- Pages/QuickAddPage.cs page object + OpenQuickAddAsync() on Pages/PageBase.cs.
- QuickAddSmokeTest (E2E gate, cleanup via E2eFixture.ApiHttpClient):
a. Movie with owned copy — GUID-suffixed title, toggle copy + price, Save, assert landing on /movies/{id} with the copy in the Ownership section.
b. Car refuel — seed a car via API, single car auto-preselected, mileage/fuel/cost, Save, assert landing on /cars/{id} with the refuel row.
- MobileScreenshotTest: add /add (picker) and /add?type=movie (form) to the 390×844 capture list — the mobile-first acceptance check.
- Regression safety for the extractions: the existing Car/House/Health/Ownership/VideoGamePlatform smoke tests already cover the refactored surfaces — run them after each extraction step.
- Pages/QuickAddPage.cs page object + OpenQuickAddAsync() on Pages/PageBase.cs.
- QuickAddSmokeTest (E2E gate, cleanup via E2eFixture.ApiHttpClient):
a. Movie with owned copy — GUID-suffixed title, toggle copy + price, Save, assert landing on /movies/{id} with the copy in the Ownership section.
b. Car refuel — seed a car via API, single car auto-preselected, mileage/fuel/cost, Save, assert landing on /cars/{id} with the refuel row.
- MobileScreenshotTest: add /add (picker) and /add?type=movie (form) to the 390×844 capture list — the mobile-first acceptance check.
- Regression safety for the extractions: the existing Car/House/Health/Ownership/VideoGamePlatform smoke tests already cover the refactored surfaces — run them after each extraction step.

## Gotchas to respect

Expand All @@ -122,7 +127,7 @@ Three extraction refactors come first — they remove exactly the duplication **

- `dotnet build`; `dotnet test` (needs local MongoDB + `Local.runsettings`; Playwright self-skips without `E2E_ENABLED`).
E2E: `E2E_ENABLED=true dotnet test test/BlazorApp.PlaywrightTests/BlazorApp.PlaywrightTests.csproj` per the e2e-local-run-recipe memory / `CONTRIBUTING.md`.
Mobile review: `E2E_SCREENSHOTS=true` + `E2E_SHOTS_DIR` screenshot run, plus a manual pass of <https://localhost:5021/add> at 390px width.
Mobile review: `E2E_MOBILE_CHECK=true` + `E2E_MOBILE_DIR` screenshot run, plus a manual pass of <https://localhost:5021/add> at 390px width.
Manual regression: the three detail pages' add/edit modals and the game platform cards must behave identically after the extractions (same fields, date/time proxies, balance preview).

## Ready-to-use implementation prompt
Expand Down
Loading
Loading