36 Commits

Author SHA1 Message Date
d4b12fdd69 fix(bikegear): pass proxy test when product listing is readable 2026-08-01 18:43:26 +05:30
91356cd6ac fix(bikegear): avoid false Cloudflare challenge in proxy test 2026-08-01 18:39:24 +05:30
78bc7c5140 chore(bikegear): add scrape-only cache command 2026-07-31 01:19:46 +05:30
6891ebf4bf chore(bikegear): add Playwright proxy diagnostic 2026-07-31 01:13:19 +05:30
2b8cf2c6fc fix(bikegear): support Webshare proxy config and cache-only mode 2026-07-31 01:06:37 +05:30
19fd7f16ea fix(bikegear): add proxy support to Playwright browser context
BIKEGEAR_PROXY_URL now applies to the browser-mode fetcher too.
The VPS IP is permanently blocked at Cloudflare network level — a residential
proxy is the only remaining solution. Set BIKEGEAR_PROXY_URL=http://user:pass@host:port.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-29 07:34:32 +05:30
76bd69a92f fix(bikegear): switch to persistent browser profile + stealth script to beat Cloudflare detection
Previous approach (newContext per run) was still detected via navigator.plugins=0
and window.chrome=undefined. Two fixes:
1. launchPersistentContext writes a real browser profile to disk — cookies,
   cache and fingerprint survive restarts so Cloudflare trusts the same "user"
2. addInitScript patches plugins, chrome runtime, webdriver and languages on
   every page, hiding remaining headless Chromium signals

recycleBrowserContext now just closes stale pages (profile stays on disk).
closeBrowser just closes the browser process — profile already persisted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-29 07:25:35 +05:30
e4b3292533 fix(bikegear): always delay between brands and tighten anti-ban settings
- Between-brand delay now fires for skipped brands too (was only firing for
  brands with products), fixing rapid-fire requests that caused 403 on detail pages
- Default request delay increased 1500ms -> 2500ms
- Browser context recycle interval tightened 20 -> 10 brands

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-24 17:02:57 +05:30
46d018c56e fix(bikegear): recycle browser context every 20 brands to prevent memory OOM restart
Running Playwright Chromium for 2+ hours with one shared context accumulates
memory until PM2/OOM kills the process with no error. Fix: save cookies and
close/reopen the context every 20 brands so Chromium memory is freed mid-run.
Cookie persistence means the new context resumes with the cf_clearance session.
Override interval with BIKEGEAR_RECYCLE_EVERY env var.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-24 16:27:00 +05:30
a8adf00732 fix(bikegear): reduce browser concurrency to 1 and add 1.5s delay between requests
Cloudflare 429s when running 2+ concurrent browser pages against bikegear.in.
Browser mode now runs 1 page at a time with a 1.5s polite delay after each load.
HTTP mode keeps its original concurrency (3 listing / 2 detail).
Override with BIKEGEAR_LISTING_CONCURRENCY / BIKEGEAR_DETAIL_CONCURRENCY / BIKEGEAR_REQUEST_DELAY.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-22 18:13:29 +05:30
0fbe75dd3d fix(bikegear): use Playwright browser mode to bypass Cloudflare on VPS IPs
HTTP requests from datacenter IPs get 403 from bikegear.in Cloudflare WAF.
Adds BIKEGEAR_FETCH_MODE=browser (default) which runs a headless Chromium via
Playwright — already installed — and navigates pages like a real user, bypassing
the JS challenge without needing a paid proxy.

Set BIKEGEAR_FETCH_MODE=http to keep the old axios path (for local/residential IPs).
Browser instance is shared across the full scrape run and closed cleanly on finish.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-22 18:05:24 +05:30
846e467b85 fix(bikegear): route scraper through residential proxy to bypass Cloudflare 403 on server IPs
Datacenter IPs are blocked by bikegear.in Cloudflare WAF.
Switches fetchHtml from native fetch to axios with BIKEGEAR_PROXY_URL support.
Set BIKEGEAR_PROXY_URL=http://user:pass@host:port in server .env to enable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-14 01:17:29 +05:30
bf207e48a1 fix(bikegear): fix 2 scraper issues found during test run
1. parseProductJsonLd: remove sku requirement — out-of-stock products have
   Product JSON-LD but no sku field, causing false "No product JSON-LD found" errors.
   Now picks block with offers.price or description instead.

2. extractProductImages: add fallback for non-standard image paths — products
   with images at /catalog/KTM/... or other custom paths were falling through
   to JSON-LD fallback (1 image). Now also scans the product-img anchor HTML.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-13 18:18:35 +05:30
0c66b4b7ee feat(bikegear): expand brand list to all 130 brands from bikegear.in, pipeline test passed 100%
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-13 18:03:39 +05:30
c2e037310d feat: add bikegear.in import source (Alpinestars as first brand)
Adds a new OpenCart-based source (bikegear.in) to the import pipeline.
2-phase scraper: listing pages collect product URLs, detail pages extract
JSON-LD (name, price in INR, brand, SKU) + description table HTML + gallery images.

- sources/bikegear/brands.js  — brand list (Alpinestars active, 40+ commented)
- sources/bikegear/scraper.js — HTML scraper with full Sec-Fetch browser headers
- sources/bikegear/converter.js — Shopify-ready converter (record.scraped || record pattern)
- sources/bikegear/index.js   — 24h cache, zero-success guard, brands[] export
- sources/index.js             — register bikegear source

Set BIKEGEAR_CACHE_HOURS=0 to force re-scrape.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-13 17:27:06 +05:30
4b5b7cab9f feat: expose brands list per source via /pipeline/sources API
Each source module now exports a brands[] array:
- motousher: 44 brands (full list from brands.js)
- dirtstreet: 5 brands (SC Project, Evotech, etc.)
- retrorides: 10 bike brands (BMW, Ducati, Aprilia, etc.)
- kyt: KYT
- brocks-performance: Brock's Performance
- yuasa: Yuasa

listSources() passes brands[] through in the API response so the
frontend can render the per-source brand list without a separate call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 13:58:05 +05:30
07fff6ac5a feat(motousher): expand brand list from 12 to 44 brands (2026-07-07)
Added 32 new brands from motousher.com/pages/partner-brands:
ACE Bikes, Baja Designs, Barkbusters, Bumot, Cliff Top - Chargers,
Domino, Doubletake Mirrors, Flo Tool, Giant Loop Moto, HAD Originals,
Hepco Becker, HexEzCan, IntAct Batteries, Key Smart, Motone,
MotoSolutions, Nebo Tools, NGK Plugs, Nite Ize, NoNoise,
Own Your Adventure, Posi-Products, Quad Lock, Quixx, Rok Straps,
Rubba Tech, S100 and A1, SBV Tools, sPOD, True Utility, Vertex, Victor

Also corrected Maxima Racing Oils collection URL:
/maxima-racing-oils -> /maxima-oils (actual slug used on site)

Each entry commented with "already added" or "added 2026-07-07".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 13:47:27 +05:30
9c70fcb86c fix: don't cache or process all-failed Yuasa scrape results
- Cache is now skipped if every product in the cached file has scrapeError
  (prevents a 403-failed run from poisoning the next 24h of imports)
- If all scrapes fail, throw an error so the pipeline stops at step 1
  instead of silently creating 13 empty Shopify products with no images/price
- Only successfully scraped batteries are included in allProducts passed
  to the converter/download/upload/upsert stages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 19:08:20 +05:30
54b2ad381c fix: add Sec-Fetch browser headers to Yuasa scraper to bypass 403
Production server IP was being rejected by yuasabatteries.com as a bot.
Added full Chrome Sec-Fetch-* and Sec-CH-UA header suite (same fix applied
to retrorides earlier) so the server appears as a real browser navigation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 19:00:55 +05:30
95e100c9b4 feat: add Yuasa Batteries as new import pipeline source
- scraper.js: fetches product detail pages from yuasabatteries.com for a configurable battery ID list; auto-discovers and downloads the current MSRP xlsx from the Yuasa MSRP page to build a battery-type → price map using the xlsx package; extracts specs, images, and vehicle fitment from HTML tables
- converter.js: converts scraped battery data to Shopify-ready format (title, SKU, price from MSRP, description from specs, images)
- index.js: standard pipeline integration with 24h cache (YUASA_CACHE_HOURS=0 to bypass); battery ID list configurable via YUASA_BATTERY_IDS env var (comma-separated)
- sources/index.js: registers yuasa as the 6th import source

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 18:57:09 +05:30
ccc1179fa2 Fix retrorides 409 on production — add full Sec-Fetch browser headers
Production server IP triggered mod_security 409 even on sitemap fetch.
- Added Sec-Fetch-Dest/Mode/Site/User and Sec-CH-UA headers that
  mod_security requires to recognise a real browser request
- Separate SITEMAP_HEADERS set for XML sitemap request (Accept: application/xml)
- fetchText now accepts optional headers override

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 19:46:22 +05:30
6938c113ae Fix retrorides 409 errors — treat as rate-limit, drop concurrency to 1
HTTP 409 Conflict from the site WAF when requests run in parallel.
- 409 now retried with backoff (same as 429), up to 5 attempts at 5s increments
- Default concurrency changed from 3 to 1 (sequential fetches)
- RETRORIDES_CONCURRENCY env var to override if needed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 19:36:59 +05:30
f48bc39751 Update package-lock.json
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 18:57:28 +05:30
4b2288187c Add Retro Rides source — sitemap scraper + converter + pipeline integration
- New source: retrorides (retrorides.co.in, 1168 products)
- Scraper reads all product URLs from WordPress sitemap in one call
- Extracts title/SKU/price/stock from JSON-LD, gallery images from data-large_image
- Bike brand/model from breadcrumb (case-insensitive BIKE_MAKES normalisation)
- Converts Jetpack CDN image URLs to direct retrorides.co.in URLs
- 24h cache with RETRORIDES_CACHE_HOURS env var, concurrency=3 default
- Converter follows same record.scraped pattern as motousher/dirtstreet
- Registered in sources/index.js alongside kyt, brocks-performance, motousher, dirtstreet

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 18:37:47 +05:30
08f21d9bc9 Add job cancellation — backend pipeline + cancel API route
pipelineJobs.js:
- cancelJob(jobId): marks job as cancelled=true, status=cancelling
- isJobCancelled(jobId): checked by the pipeline between stages

runSourcePipeline.js:
- PipelineCancelledError class
- checkCancelled() called before each of the 6 pipeline stages
- Accepts options.isCancelled() callback from the job runner

runKytPipelineJob.js:
- Passes isCancelled: () => isJobCancelled(job.id) into pipeline
- Catches PipelineCancelledError separately, sets status=cancelled

routes/pipeline.js:
- POST /pipeline/cancel/:jobId — marks job for cancellation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 16:57:35 +05:30
4e536f08b3 Add 24h cache to motousher and dirtstreet fetchWebsiteData
Subsequent pipeline runs within 24 hours reuse the existing
01_products_aggregated.json instead of re-scraping all brands,
eliminating redundant HTTP requests and 429 rate-limit retries.

Cache lifetime controlled per source:
  MOTOUSHER_CACHE_HOURS=0  → always re-scrape
  DIRTSTREET_CACHE_HOURS=0 → always re-scrape
  (default: 24h)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 16:44:38 +05:30
6eac0b92ed Fix converter reading nested scraped data and brand priority
Both motousher and dirtstreet converters were reading product fields
(title, sku, price, images) directly from the aggregated record, but
those fields live inside record.scraped after fetchWebsiteData wraps
them. Results were: Untitled Product, missing images, SKU=variant-1.

Also fixed brand priority: per-product brand (e.g. Evans Coolant,
SC Project) now takes precedence over the global SHOPIFY_BRAND env
var (KYT), which was incorrectly overriding all products from the
new sources.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 16:39:54 +05:30
c0132ab0aa Add motousher and dirtstreet as active import pipeline sources
Both sources are now registered in sources/index.js and fully wired
into the 6-stage pipeline (fetch → download → watermark → upload →
convert → upsert). The frontend will automatically show them as tabs
via GET /pipeline/sources without any frontend changes needed.

motousher/ (Shopify JSON API — 12 brands, ~2,446 products):
- scraper.js: fetches /collections/{slug}/products.json + /products/{handle}.json
- converter.js: maps scraped products to standard pipeline format
- index.js: fetchWebsiteData() loops all brands, normalises to
  productSummary.img format for shared download/upload utilities
- Supports MOTOUSHER_BRANDS env var to filter brands on a run

dirtstreet/ (WooCommerce HTML + JSON-LD — 5 brands, ~1,087 products):
- scraper.js: pure fetch, paginates /brand/{slug}/page/N/,
  extracts price from offers.priceSpecification[0].price,
  stock from JSON-LD availability field
- converter.js: maps scraped products to standard pipeline format,
  builds descriptionHtml from body + short desc + attributes table
- index.js: fetchWebsiteData() loops all brands, normalises to
  productSummary.img format
- Supports DIRTSTREET_BRANDS env var to filter brands on a run

sources/index.js: registered all 4 sources (kyt, brocks-performance,
motousher, dirtstreet). GET /pipeline/sources now returns all 4.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 12:25:31 +05:30
b8d9478afa Add test_source scrapers for motousher.com and dirtstreet.in
Adds two new experimental product scrapers under test_source/, isolated
from the active pipeline until verified and ready to promote.

motousher/ (Shopify store — Shopify JSON API):
- Scrapes 12 brands: All Balls Racing, DID Chains, EBC Brakes, Esjot
  Sprockets, Evans Coolant, Grip Puppies, HiFlo Filters, JT Sprockets,
  Maxima Racing Oils, Putoline, Ram Mount, Wunderlich
- 2,446 products total scraped and verified
- Uses /collections/{slug}/products.json + /products/{handle}.json
- Parallel fetch (concurrency 3), paginated collection listing

dirtstreet/ (WooCommerce store — HTML + JSON-LD):
- Scrapes 5 brands: SC Project, Evotech Performance, DNA Air Filters,
  WRS, Zero Gravity Racing
- 1,087 products total scraped and verified
- Pure fetch with JSON-LD schema.org extraction (no browser)
- Handles paginated /brand/{slug}/page/N/ archives
- Price extracted from offers.priceSpecification[0].price
- Stock status derived from JSON-LD availability field

Both scrapers are standalone (node index.js), support --brand and
--limit flags, save per-brand JSON files and a combined.json.
Scraped data lives in data/sources/test_source/ (gitignored).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 12:17:23 +05:30
1d254a9009 Install Playwright browser for backend 2026-05-15 00:25:40 +05:30
68949f124e Add multi-source import pipeline 2026-05-14 23:57:27 +05:30
bef07eff10 Refactor code structure for improved readability and maintainability 2026-05-14 23:56:13 +05:30
2320d1d5c3 Fix typo in health check service name 2026-04-14 14:23:00 +05:30
33ad269821 Fix health check service name in response 2026-04-14 14:22:25 +05:30
9480832478 Add concurrency handling and logging enhancements to KYT pipeline 2026-04-14 13:26:56 +05:30
e87bd907ea first commit 2026-04-13 17:31:26 +05:30