Commit Graph
3 Commits
Author SHA1 Message Date
Kevin L'Hereec 62ec796e05 fix: repair broken CSP and compare live merchant prices for new parts
Build & Deploy / build (push) Failing after 4s
Build & Deploy / docker (push) Skipped
CSP fix:
- The previous shield.ts config used '@viteDevUrl'/'@viteHmrUrl' CSP
  keywords that don't exist in this version of @adonisjs/shield (only
  '@viteUrl' is registered by @adonisjs/vite, and it resolves to an empty
  string here since Vite assets are proxied through AdonisJS on the same
  origin). Both placeholders were sent to the browser literally, breaking
  Google Fonts, the Vite HMR websocket, and pc-builder.io/favicon images.
- Replaced with concrete directives: 'self' covers same-origin Vite
  assets, ws://localhost:{VITE_HMR_PORT|24678} is added only in dev for
  the HMR socket, and explicit allowlists for Google Fonts
  (fonts.googleapis.com/fonts.gstatic.com), pc-builder.io product images,
  and Google's favicon service (www.google.com) used by the merchant
  comparison UI

Real purchase links + live price comparison:
- part_scraper_service.ts: ScrapedPart.sourceUrl now stores the actual
  Amazon.fr purchase link scraped from pc-builder.io's category pages
  (https://www.amazon.fr/dp/{ASIN}?tag=... or a search fallback), instead
  of the pc-builder.io product page, which isn't a place anyone can buy
  anything from
- New merchant_price_service.ts: on-demand live price lookup across
  Amazon, LDLC, Grosbill and TopAchat. Of 6 requested merchants,
  Materiel.net (search requires JS/Turbo, no results via plain GET) and
  Cdiscount/PCComponentes (empty body / 403) were dropped as not scrapable
  without a headless browser or paid API
- Matching strategy: pc-builder.io's full product names (e.g. "AMD Ryzen 3
  3200G 3.6 GHz Quad-Core Processor") return zero results on every
  merchant's search engine, so we search on a short brand+series query
  and then pick the specific result whose label contains the extracted
  model token (e.g. "3200G"). No token match means no offer returned,
  rather than risk quoting the price of a different, unrelated product
- Fixed price parsing for LDLC/Grosbill, which render prices as nested
  price/sup elements for whole euros vs cents (and LDLC additionally
  wraps that in a duplicate outer .price div) - naive text concatenation
  was misreading "64,95" as "6495"
- New GET /scraped-parts/compare endpoint (authenticated), called on
  demand per search result rather than for every row, since it makes
  several outbound requests and can take a few seconds
- NewPartSearch.vue: "Compare prices" button per result shows the
  cheapest merchant with its favicon and links to the actual offer;
  importing a part now prefers the live cheapest price/link over
  pc-builder.io's potentially stale catalog price (confirmed stale in
  practice: pc-builder.io listed a Ryzen 3 3200G at 128 euros while
  Amazon sells it for 66.89 euros)
- 3 new functional tests for the compare endpoint (auth required, 404 on
  unknown part, 422 on invalid id)
2026-07-23 14:58:33 +02:00
Kevin L'Hereec 497ae797ca feat: add 24h cache freshness with auto-refresh on search
Build & Deploy / build (push) Failing after 5s
Build & Deploy / docker (push) Skipped
- part_scraper_service.ts: new CACHE_TTL_HOURS (24), isCategoryStale()
  checks the most recent scrapedAt for a given ComponentType's mapped
  category, ensureFreshCategory() re-scrapes it synchronously if missing
  or stale (errors are logged, not thrown, so search still falls back to
  whatever is already cached)
- ScrapedPartsController#index: when a typeId filter is provided, the
  corresponding category cache is refreshed eagerly if stale before
  querying results. Free-text searches without a typeId skip this to
  avoid re-scraping every mapped category on every keystroke -
  'node ace scrape:parts' still refreshes everything at once on demand
- 4 new tests covering staleness detection (missing cache, <24h, >24h)
  and that unmapped types never trigger a scrape/network call
2026-07-23 13:58:37 +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