Commit Graph
9 Commits
Author SHA1 Message Date
Kevin L'Hereec 54c5e37766 fix: distinguish GPU model suffixes (Ti/Super/XT) and seed RTX 5060 series
Build & Deploy / build (push) Failing after 3s
Build & Deploy / docker (push) Skipped
RTX 5060 investigation: confirmed via pc-builder.io's own server-side
search (search_query=RTX 5060 against the full 6222-product GPU catalog,
0 results) that it simply isn't in pc-builder.io's catalog yet - not a
scraping bug, the source data doesn't have it. However, while checking
whether the other merchants had it in stock, found a real matching bug:

labelMatchesModelToken bug: extractModelToken("...RTX 5060 Ti...") only
returned "5060", identical to extractModelToken("...RTX 5060...") without
Ti. Every merchant lookup for either card matched on plain "5060" and
could silently return the *other* card's price (confirmed live: LDLC and
Grosbill returned the exact same product/price for both "RTX 5060" and
"RTX 5060 Ti" queries before this fix).

Fixed by:
- extractModelToken now appends a trailing suffix token (Ti, Super, XT,
  XTX) when it immediately follows the model number, so "5060 Ti" and
  "5060" are extracted as distinct tokens
- New labelMatchesModelToken() replaces the naive
  label.includes(modelToken) check everywhere: a bare token ("5060") no
  longer matches a label that actually has a suffix right after it
  ("5060 Ti"), while a suffixed token ("5060 Ti") only matches labels
  carrying that exact suffix
- 9 new unit tests covering extraction and matching, including the
  regression case (RTX 5060 must not match "RTX 5060 Ti" labels)

Verified fix live: LDLC/Grosbill now return correctly distinct prices for
"RTX 5060" (369.95/364.99) vs "RTX 5060 Ti" (489.95/329.99).

New manual_part_seeder_service.ts: since pc-builder.io's catalog lags
behind real product releases, this seeds a small manually-tracked list of
recent parts it doesn't carry yet (currently RTX 5060 8GB and 5060 Ti
16GB), resolving their price live from the already-supported merchants
rather than hardcoding a number that would go stale. `scrape:parts` now
runs this after the regular catalog scrape. Skips an entry rather than
inserting a guessed price if no merchant has it in stock.

Also fixes a real bug hit while testing the seeder: scraped_parts and
components.purchase_url were varchar(255), which truncates/rejects
Grosbill's long tracking-redirect URLs (500+ characters with encoded
analytics params) - both widened to TEXT via a new migration.
2026-07-23 16:51:29 +02:00
Kevin L'Hereec 6053290501 feat: paginate pc-builder.io scraping and always keep the best known price
Build & Deploy / build (push) Failing after 5s
Build & Deploy / docker (push) Skipped
The RTX 5070 case that prompted this: only 100 GPUs out of pc-builder.io's
actual 6222 were being scraped (its category pages only render the first
page server-side), so cheaper RTX 5070 variants existed on the site but
were invisible to our search. Investigating further also surfaced that
even with full pagination, most recent GPU listings (RTX 5070 included)
have no fixed Amazon price on pc-builder.io at all - just a generic
"search on Amazon" link - so pagination alone doesn't fully solve price
coverage either.

Pagination (part_scraper_service.ts):
- Reverse-engineered pc-builder.io's /products/data endpoint, which powers
  the client-side DataTables widget behind every category page. Requires
  a session cookie + CSRF token scraped from a fresh category page load,
  and an exact field shape (columns[].data must be the literal strings
  "Name"/"Add to Build", not numeric indices - a naive DataTables-shaped
  request 500s otherwise).
- CATEGORY_CONFIG now tracks each category's real pc-builder.io
  category_id and a per-category maxProducts depth, chosen by actual
  catalog size and price-sensitivity: GPU near-full coverage (6000),
  smaller/less price-volatile categories capped lower (300-800) to keep a
  full `scrape:parts` run (all categories) in a reasonable time.
- ensureFreshCategory (triggered inline by a user's search when the cache
  is stale) uses a much shallower QUICK_REFRESH_MAX_PRODUCTS=200 cap
  instead of the category's full depth, since it runs inside the
  request/response cycle of a search - full-depth coverage is only done
  by the standalone `scrape:parts` command.

Always keep the best known price:
- scraped_parts.price is now nullable (migration): parts with no fixed
  pc-builder.io offer are kept and searchable instead of being silently
  skipped, so at least the model is discoverable even before a price is
  resolved.
- New price_source / price_checked_at columns track where the currently
  stored price came from ("pc-builder.io" or a merchant name from
  merchant_price_service.ts).
- ScrapedPartsController#compare now persists the cheapest offer found
  (via the new updatePriceIfCheaper()) when it beats the currently stored
  price - so comparing prices benefits every future search/import for
  that part, not just the user who triggered it. Never regresses the
  price: a comparison finding a higher price than what's stored is
  ignored.
- scrapeComponentType's catalog refresh applies the same "keep whichever
  price is lower" rule per row, so a routine pc-builder.io re-scrape can
  never overwrite a better price a comparison had already found and
  persisted with a worse (or missing) one.
- Search results (GET /scraped-parts) are now ordered priced-cheapest-first,
  then parts with no known price last (`ORDER BY price IS NULL, price ASC`),
  so unpriced parts stay visible without pushing priced results down.
- NewPartSearch.vue displays the catalog price inline (or "No price yet"),
  and the existing "Compare prices" flow now works for those unpriced
  parts the same way it does for priced ones.

7 new/updated functional tests: nullable price is searchable and sorted
last, and 3 tests for updatePriceIfCheaper (cheaper offer replaces price,
pricier offer is ignored, first-ever price is accepted for a previously
unpriced part).
2026-07-23 16:12:26 +02:00
Kevin L'Hereec 4174d92711 feat: track purchase site URL for new components
Build & Deploy / build (push) Failing after 4s
Build & Deploy / docker (push) Skipped
- New nullable purchase_url column on components
- Store/update validators accept an optional purchaseUrl (must be a valid
  absolute URL with a protocol when provided)
- components/create.vue: NewPartSearch import now also fills purchaseUrl
  from the scraped part's source URL (pc-builder.io product page); the
  field remains manually editable for second-hand parts entered by hand
- components/edit.vue: purchaseUrl is editable after creation
- components/show.vue: displays the purchase site as a clickable link
  when present
- 4 new functional tests: create with/without a purchase URL, invalid URL
  rejected (asserts the Inertia inputErrorsBag flash, not a raw 422 -
  content negotiation only returns JSON for XHR/API-style requests), and
  updating an existing component's purchase URL
2026-07-23 14:10:54 +02:00
Kevin L'Hereec 5854ff8ed1 feat: search and import new parts scraped from pc-builder.io
Build & Deploy / build (push) Failing after 4s
Build & Deploy / docker (push) Skipped
Idealo and PCPartPicker were evaluated and ruled out:
- idealo.fr search pages return 503 (Cloudflare)
- the internal Idealo GraphQL endpoint used by a public scraper
  (app.idealo.de/app-backend/api) is protected by Akamai Bot Manager and
  silently returns empty data even with mobile app headers
- PCPartPicker's product list/search relies on a session-bound CSRF token
  refreshed per page load, against a scraping-hostile ToS

pc-builder.io category pages are server-rendered, include name/price/image
for ~100 products per category, and have no bot protection, so the feature
is built on top of that source only.

- New scraped_parts cache table (type_id, name, price, image_url, source,
  source_url, source_ref unique per source) populated by a batch job
  rather than scraped live per search, since only ~100 first results per
  category are available without reverse-engineering pc-builder.io's
  DataTables/CSRF pagination flow
- part_scraper_service.ts: maps our ComponentType names to pc-builder.io
  category slugs (SSD -> storage, since pc-builder.io has no separate HDD
  category) and parses each category page with cheerio, skipping rows
  without a resolved price (generic 'search on Amazon' links)
- node ace scrape:parts: refreshes the cache for every mapped category
  (upsert on source+source_ref), runnable manually or later via cron
- GET /scraped-parts (authenticated): search the cache by name (LOWER()
  LIKE, avoids a MySQL utf8_bin/utf8mb4 collation mismatch from Lucid's
  whereLike) and optional typeId filter
- NewPartSearch.vue: debounced search widget wired into
  components/create.vue; clicking 'Import' pre-fills the existing
  component creation form (name, price, type, purchase date = today)
  instead of creating the Component directly, so the user still reviews
  and saves it like any manually entered part
- 3 functional tests covering search by name, filter by typeId, and
  unauthenticated access being redirected to login
- Initial scrape run: 989 parts across 10 categories
2026-07-23 12:10:36 +02:00
Kevin L'Hereec da71d33450 feat: add public read-only sharing for computers
Build & Deploy / build (push) Failing after 5s
Build & Deploy / docker (push) Skipped
Adds the ability for an owner to share a computer build via a public,
unauthenticated link, inspired by a feature request from the original
pcbuilder repo (no such feature actually existed there — implemented
from scratch here).

- New columns on computers: share_token (unique, nullable), share_enabled,
  share_show_sell_price, share_show_cost
- computer_share_service.ts: generates a non-guessable share token
  (crypto.randomBytes, base64url), enables/disables sharing, and supports
  rotating the token to invalidate previously distributed links
- ComputerShareController (authenticated, owner-scoped like the rest of
  the app): PUT /computers/:id/share to update settings, POST
  /computers/:id/share/rotate to rotate the token
- PublicComputerController + GET /share/:token (outside any auth/guest
  group, reachable anonymously): resolves only computers with
  share_enabled = true, 404 otherwise
- ComputerPublicTransformer: dedicated read-only projection that never
  leaks userId, and only includes sellPrice/cost when the owner opted in
  via share_show_sell_price / share_show_cost — everything else (title,
  scores, state, components, pictures) is always visible on a shared link
- ComputerShareModal.vue: owner-facing UI (toggle sharing, choose which
  prices to expose, copy the public link, revoke/rotate it) wired into
  computers/show.vue via a new 'Share' button
- share/show.vue: public page rendered with EmptyLayout (no authenticated
  header/sidebar)
- Boolean columns use a  cast since MySQL returns tinyint(1),
  not native booleans, through Lucid
- 6 new functional tests covering: enabling sharing, price visibility
  toggles, disabling sharing revokes the link, cross-user protection on
  the share settings endpoint (404), token rotation invalidating the old
  link, and anonymous access to a shared computer
2026-07-23 11:43:38 +02:00
Kevin L'Hereec ec6734f491 chore: clean up dead UI code and regenerate schema
- Remove non-functional search bar, notifications and settings buttons from header
- Remove leftover console.log and unused useAttrs import in sidebar
- Remove unused scoped CSS in computers/index.vue (Tailwind-only template)
- Regenerate database/schema.ts after running migrations locally
- Ignore .serena/ local tooling directory
- Refresh package-lock.json after npm install
2026-07-23 10:21:51 +02:00
Kevin d98e08f144 fix: TS errors, serialize calls, unused imports, pre-commit hook 2026-07-07 20:49:52 +02:00
Kevin 5b27625ead first fixes 2026-06-29 22:48:59 +02:00
Kevin 648d0af538 Initial commit: PC Builder AdonisJS project 2026-06-28 10:41:51 +02:00