- WMI_BRAND_MAP + getBrandFromWmi packages/shared'e taşındı (tek kaynak);
corgi.service artık buradan import ediyor (davranış aynı, testler geçiyor)
- VinBrandIcon: VIN'in WMI'ı bilinen markaya denk gelince büyüteç yerine
marka logosu pop animasyonuyla görünür (landing + search VIN textbox)
- WMI haritasına prod DB'de decode edilmiş VIN'lerden 23 eksik WMI eklendi
(Türkiye fabrikaları NM4/NMT/NLA/NLH/NMB dahil; tüm DB WMI'ları artık tanınıyor)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The DataImpulse proxy has dead ports that stall ~10s on connect, and undici's
ProxyAgent connect.timeout does NOT bound the proxy connection (verified: runtime
still 10s despite the Phase-2 setting). With the 30s request timeout, 2-3 dead
ports blow the 25s decode budget → false misses + 12-32s p95 (prod). /car/info
answers in <1s on a healthy proxy, so bound it at the fetch level instead: a 6s
per-call AbortSignal + 4 retries (both env-tunable: PCAT_DECODE_TIMEOUT_MS /
PCAT_DECODE_MAX_RETRIES) make a stuck port abort fast and rotate to a live one
within budget. Helps every PCAT decode, not just Renault.
Band-aid for proxy flakiness; proper fix is proxy port health-tracking (follow-up).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PL24 has suspended Renault VIN identification ("...belirsiz bir süre için mevcut
olmayacağını üzülerek bildiririz") — renault_parts authorizes but every decode
throws that message. In the race that's a wasted ~1s call AND, worse, it counts
as a PL24 failure: 3 strikes opens the PL24 circuit breaker, which then skips
PL24 for ALL brands (Opel/Ford/PSA…) for 30s. Prod data: VF1 (Renault) was the
single biggest decode-failure WMI, much of it repeat hits on a poisoned cache.
Comment out the renault_parts WMIs (VF1/VF6/VNE) so PL24 isn't tried for Renault;
PCAT + EMEX cover it. Re-enable when PL24 restores Renault VIN decode.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PL24 account JWTs were in-memory only, so every (re)deploy forced a fresh login
on the next decode, and api + worker each logged in independently — fighting over
PL24's one-session-per-account (squeezeOut). Persist account tokens to Redis
(keyed by account, TTL = JWT exp) and hydrate from Redis before logging in.
Best-effort: any Redis error falls through to a normal login, so auth never
depends on Redis. Service tokens stay in-memory (warmed by the boot pre-warm).
Complements the OnModuleInit pre-warm — now even the base login survives restarts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PL24 auth tokens (account JWT + per-service tokens) are cached in-memory, so a
(re)deploy clears them and the first VIN decode of each service-family pays the
~10s login + service-authorize handshake on the request path. Measured on dev: a
cold opel_parts decode took 12s vs ~2s once the auth was warm.
Add OnModuleInit to PL24AuthService that warms, in the background (fire-and-forget,
never blocking boot), both base account logins (the shared dominant cost) plus the
common legacy + top Turkish-market service tokens. allSettled throughout so a
slow/down PL24 degrades gracefully; gated on credential presence; PL24_PREWARM=false
disables. The pre-decode (search page) already hides this from users mid-session —
this closes the one remaining gap: the first dealer right after a deploy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prod decode failures clustered on French/Stellantis VINs that no source even
tried: the post-merger WMIs VR3/VR7 (Peugeot), VR1 (Citroën/DS) and W0V (Opel)
were missing from every map, so getServiceName/isSupported returned null and the
VIN was rejected as "Marka desteklenmiyor" before any scrape ran.
Add them to all three maps, matching the existing VF3/VF7/W0L routing:
- Corgi WMI_DATABASE (display brand)
- PL24_WMI_SERVICE_MAP (peugeot_parts / citroen_parts / opel_parts)
- EMEX CATALOG_MAP (PSA -> PEUGEOT00, Opel -> GM_OP201809)
Lets EMEX scrape and PL24 fall back for these mass-market TR brands instead of
hard-failing at the routing step. Phase 4 of 4 on decode reliability.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The PartsCatalogs ProxyAgent had no connect timeout, so a dead DataImpulse proxy
port stalled for undici's 10s default before the retry rotated to a fresh port
(prod: "ConnectTimeoutError ... 74.81.81.81:10628, timeout: 10000ms"). Three such
stalls exceed the caller's 25s decode budget, turning a transient bad port into a
hard decode failure.
Set an explicit 6s proxy connect timeout so retries rotate to a live port within
budget, and make the request timeout / retry count / connect timeout env-tunable
(PCAT_REQUEST_TIMEOUT_MS, PCAT_MAX_RETRIES, PCAT_PROXY_CONNECT_TIMEOUT_MS) with the
current values as defaults. Phase 2 of 4 on decode reliability.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A decode that failed from a transient proxy/network blip (PartsCatalogs
DataImpulse ConnectTimeout, EMEX timeout, PL24 transport error) was written to
the 6h negative cache identically to a genuine "brand not supported" miss. A
user who hit a blip then got "Şase tanınamadı" for 6h with no way to retry out —
and prod analytics showed this hitting SUPPORTED brands (Fiat/Toyota/Hyundai
decode fine once the proxy responds).
Thread a `transient` outcome flag through resolveVin -> doResolveVin:
- PartsCatalogs.decodeVin sets it when it swallows a transport error (vs a clean
"no cars" miss), via a new optional outcome param.
- EMEX surfaces it via its existing {type:"error"} result.
- PL24 sets it on transport/timeout errors in the fallback catch.
resolveVin then skips the negative cache when transient=true; genuine misses
still cache for 6h. Phase 1 of 4 on decode reliability.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The parts-catalogs HTTP client only retried request timeouts and 401/403;
undici network errors ("TypeError: fetch failed" — a dropped/reset
DataImpulse proxy connection) were thrown on the first attempt. During the
cold-JWT window (outside 09:00-19:00 Istanbul) the proxy drops frequently,
so a single click would surface an empty/slow category that returns full
parts on a manual retry — and could even mark a populated parent group
unavailable when child discovery hit the blip.
Retry transient transport errors (timeout + undici network failures) with a
short backoff, re-acquiring a session each attempt. Definitive HTTP
responses (re-thrown as `Error: HTTP <code> …`, e.g. 400 "list of parts is
empty") are still thrown immediately — they are real answers, not blips.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PCAT was missing parity with EMEX in two places:
- Part `description` (notice) was stored raw English alongside translated `name`.
Now batched into translateMany so users see Turkish notices.
- Vehicle body/engine/transmission attrs from VIN decode were raw upstream
values. Wire up the existing emex.mapper dictionaries on the PCAT
single-car, PCAT resolveById, and EMEX single-vehicle result paths.
- emex.mapper translateToTurkish now falls back to the original term on
dictionary miss instead of null — upstream values are heterogeneous
(engine codes, multi-word descriptors); losing them was worse than
leaving them untranslated.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
MAN truck (p5man) main-vin and sub-vin endpoints return human-readable
names in values.mainGroupDescription and values.groupDescription. Our
parsers only checked the standard caption/description fields used by
VW/Renault/etc., so MAN categories were stored with raw numeric codes
("0", "1", ..., layoutIds like "2884606") as names.
After this change, the user sees proper Turkish names like
"MOTOR, SOĞUTMA SİSTEMLERİ" and "ANTEN". Existing brands fall through
the same field chain as before — captionMatch[2] takes precedence,
keeping their behavior unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PCAT auth pool warm-up always started with e-acca.com, which doesn't
load through the new DataImpulse rotating proxy — page.goto sits on
the 30s navigation timeout and only then falls through to the next
candidate. The remaining sites (alkatalog, auto-komplekt, autotrade,
…) reach the upstream widget in 3-10s through the same proxy, so an
on-demand acquireSession() that lands during a cold start spends 30s
blocked on the dead first hop before any retry can succeed.
Move e-acca to the end of the list. New cold-pool capture finishes
in ~4s on the first reachable site (alkatalog) instead of 30s+9s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Probe (scripts/dev) confirmed that everything PartsPanel reads from an
emexdwc.ae leaf — parts table, hotspot coordinates, and the schema image
URL — is fully server-rendered in Unit.aspx HTML. Parts arrive as
`<tr name>` rows with `td[name=c_oem|c_pnc|c_name]`; hotspots are
`<div class="dragger g_highlight" name=N style="margin-top:Ypx; margin-left:Xpx; ...">`
with the coords already in image-natural pixel space; the image URL is
in `<img class="dragger" src=...laximo...>` and its native dims can be
read from the first 24 bytes of the GIF/PNG via a Range GET.
Behaviour:
1. fetchCategoryParts now tries fetchCategoryPartsViaHttp first — two
sequential GETs (QuickDetails → Unit) + a Range GET for image dims.
2. If the HTML yields ≥1 part, we return it.
3. If the HTML returns no Unit.aspx anchor, or 0 parts, we fall back to
the existing Playwright scraper (same code path as before).
The plain-HTTP path skips the ~1-2s browser launch, sidesteps the
3-page semaphore in EmexBrowserService (concurrency cap was throttling
prefetch fan-out), and uses no chromium memory. Measured on dev with 5
fresh-ssd Renault Espace IV leaves: 4.9-5.5s wall per leaf (vs 6-7s on
the Tier 1 browser path, vs 12-14s pre-Tier-1). The 6th sample
(stale-ssd Fren Kaliyeri) failed both paths identically — confirms the
plain-HTTP path doesn't introduce new failure modes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EMEX-decoded vehicles surfaced empty category pages on prod because two
runtime prerequisites were absent from the production image:
1. `scripts/emex-vin-scraper.js` was never copied — the build stage's
`COPY . .` brings it in but the production stage only cherry-picks
`apps/api/dist`, `drizzle`, and `start.sh`. Every EMEX leaf hit
therefore failed with "Scraper file not found at: /app/scripts/…"
and the category page rendered "Bu kategori icin parca bulunamadi."
2. `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser` was
exported in the Dockerfile but never reached `chromium.launch()` —
that env var is consumed by `playwright install`, not at runtime.
Playwright fell back to its bundled headless-shell cache path
(`/root/.cache/ms-playwright/chromium_headless_shell-*/…`) which
does not exist on the alpine image, so even with the scraper file
present the browser pool init would have kept failing.
Fix:
- Dockerfile: `COPY --from=build /app/scripts ./scripts`.
- `emex.browser.ts` + `parts-catalogs-auth.service.ts`: read
`process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` and pass it as
`executablePath` to `chromium.launch()` when set.
Verified on prod container: `ls /app/scripts` → missing pre-fix; the
binary at `/usr/bin/chromium-browser` exists, so the env-var hand-off
will resolve cleanly once the new image lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fetchFordVinBomParts was forwarding the relative HTML jsonUrl with
isFullUrl=true, which made undici try to parse \`/ford/fordp_parts/…\`
as an absolute URL and throw "Failed to parse URL" for every PNC row.
The Hyundai BOM expander next door calls fetchP4Page with the default
isFullUrl=false (relative path) — match that.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
VIN-decoded Ford P/T and Volvo legacy vehicles used to land on an empty
category page: the .action HTML the decoder scraped only carried header
breadcrumbs (Portal / Model seçimi / VIN). The real catalog hangs off
three JSON endpoints that the partslink24 UI calls in the background
once a VIN session is established. None of them need mode/upds/JSESSIONID
beyond the standard PL24TOKEN cookie. Plumb the whole chain so a user
who decoded a VIN sees real Turkish part categories and OEM part
numbers in the user-vehicle flow:
json-vin-main-group.action → real top-level groups (8 for Mondeo)
json-vin-sub-group.action → 58 leaf subgroups (filters subheaders)
vin-image-board.action → BOM table with pncHierCode + jsonUrl
json-vin-bom-detail.action → final OEM partno entries (per variant)
Schema image fetch reuses the existing image-ticket extractor since the
ticket URL lives in the same jsIlluData payload as Hyundai/Opel/Volvo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
VIN-group HTML for Ford/Volvo legacy services exposes the header
breadcrumb links (Portal / Model seçimi / current VIN) via the same
.action pattern the parser relied on, so they were being persisted as
"part categories" — users saw three useless rows instead of real groups.
The PSA flow already documented this trap and bypassed rawData; extend
the same defense to the rest of P4 legacy (parser-side + categories
insert fallback).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diagnostic served its purpose: confirmed that PL24 bomOverviewlist
URLs (e.g. illustrationId=4336125, "Alternatöre genel bakış" with
PR-code filters) return only a records array with no images field —
upstream genuinely has no schema illustration for these overview
tables. Frontend UX for this case is handled separately.
Production p99 was 18min for failed decodes and 15min for PL24 successes;
same VIN could trigger N parallel 10+ min decodes. Three layered fixes:
- In-flight dedup via Redis SETNX (vin🔒*); concurrent same-VIN requests
poll the resolve cache instead of re-firing the upstream chain.
- 24h positive cache (vin:resolve:*) and 6h negative cache
(vin:resolve:neg:*); previously 5min positive / no negative.
- 25s hard abort budget via AbortController; PCAT gets the signal natively
(AbortSignal.any), PL24/EMEX wrapped with raceWithSignal at the boundary.
Aborted decodes don't poison the negative cache.
- PCAT/EMEX real race: first definitive single-result wins; the slower
source is skipped (previously PCAT was always awaited first).
- PL24 circuit breaker: 3 consecutive failures opens a 30s cooldown
(pl24:cb:cooldown_until); successes reset the counter.
- Stage-level timings in query_logs.timings (jsonb): pcat/emex/pl24/
lock_wait/cache_hit/aborted. Failed source now logged as "none" or
"aborted" instead of misleading "corgi".
Verified locally with 3 parallel decodes of a fresh VIN: 1 real decode
(3.59s), 2 lock-waits (3.53s) sharing the result, 4th request 23ms cache
hit. Previously this would have been 3 separate 10+ min PL24 decodes.
Migration 0002 adds query_logs.timings jsonb (NULL default). Must be
applied manually before deploy (deploy.sh does not run db:push).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The biome safe-fix sweep in 3184e4c rewrote `import { X }` to
`import type { X }` for several NestJS providers (ConfigService,
Reflector, EmailService, Database, etc.). Type-only imports are
erased at compile time, but Nest's reflect-metadata DI reads
constructor parameter types at runtime, so every affected provider
threw "Nest can't resolve dependencies (?)" and the API crash-
looped under PM2 (377+ restarts).
Convert all `import type {` to `import {` in apps/api/src so the
type tokens survive into the emitted JS for runtime DI.
Note: biome's useImportType rule does not understand the
reflect-metadata pattern. Worth disabling for apps/api/ in a
follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Run 'biome check --fix' on apps/api/src and apps/web/src to clear
the safe-fixable lint backlog (151 files: parseInt → Number.parseInt,
isNaN → Number.isNaN, organize imports, etc.). 769 errors remain
that require manual changes (mostly noExplicitAny).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- CATALOG_MAP'e ~40 yeni WMI eklendi (Mitsubishi, Nissan, Volvo, vb.)
- brandFromCatalogCode listesi 55 DB kataloğuna genişletildi
- brand="UNKNOWN" olsa bile catalogCode varsa EMEX sonucu kabul edilir
- emex-catalog: linkPath'li kategorilerde parçalar DB'den yüklenir (scraper bug fix)
- VIN cache: emex_vehicle_vins tablosu ile tekrar VIN'lerde emexdwc.ae atlanır
- getCategoryTree Step 0: VIN cache lookup (~10ms)
- Match sonrası otomatik VIN cache doldurma
- Proaktif cache: ilk decode sonrası matchByName + saveVinCache
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add CarBrandLogo component with SVG assets for 11 brands (Alpine, Citroën,
Cupra, Dacia, MAN, Opel, Peugeot, Renault, SEAT, Škoda, Suzuki); falls back
to logoUrl prop then initial-letter avatar
- Use CarBrandLogo across brand-selector, catalog index, subscription page,
dashboard home, and vehicle detail page
- PL24: pre-download schema image buffer while auth headers are fresh to avoid
token expiry on cached URLs; strip buffer before Redis caching
- PL24: fetchVehicleList tries de account first, falls back to main token
- DB: widen catalogVehicles unique index to include serviceName column
- CSS: add Tailwind v4 dark variant declaration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- vehicles.service.ts: remove Corgi from resolveVin chain; go straight
to PartsCatalogs → PL24 → EMEX without offline WMI lookup
- End condition simplified: return null only when EMEX also fails (no
corgiKnown fallback)
- Add scripts/test-emex-http.mjs: proxy timing test for EMEX HTTP decode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When EMEX DB has no vehicle match (Ford EU, Mercedes etc.), instead of
falling back to full on-demand VIN decode (~8s), use the VIN-decode SSD
directly with QuickGroups.aspx?c={catalog}&vid=0&ssd={vinSsd} (~2-3s).
Tested: Ford Focus CB4 VIN SSD returns 376 categories via QuickGroups.
New step 3.5 in the cascade:
1. pathData → matchByName (DB-only, ~50ms)
2. emexVehicleName → matchByName (DB-only, ~50ms)
3. matchBySsd wizard walk (DB, ~2-3s)
3.5. VIN SSD → QuickGroups shortcut (~2-3s) ← NEW
4. Fallback → full on-demand scrape (~8s)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ConfigService returns empty string (not the default) when env var
exists but is empty. Add || fallbacks in code and set proper defaults
in docker-compose to prevent invalid proxy URL like http://user:pass@:10000.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Node's global fetch doesn't support the dispatcher option. Use
undici's exported fetch when proxy is configured, which properly
handles ProxyAgent as a dispatcher.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EMEX server blocks direct server IP requests (geo/bot protection),
causing all HTTP-based VIN decodes to return empty results. Add proxy
support to fetchEmexHtml() using undici ProxyAgent, reusing the same
EMEX_USE_PROXY / EMEX_PROXY_* env vars as the browser service.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace Playwright browser with pure HTTP for EMEX VIN decode:
- Vehicles.aspx, QuickGroups.aspx, QuickDetails.aspx all work
without authentication — no session/browser needed
- VIN decode drops from ~10-20s (Playwright) to ~1-5s (HTTP)
- Playwright browser kept as fallback if HTTP fails
Add multi-candidate selection for EMEX (mirrors PartsCatalogs flow):
- EmexCandidate interface matches PcatCar shape so VehicleSelectModal
renders without changes
- decodeVinOrCandidates(): single HTTP fetch returns vehicle OR
candidates (avoids double Vehicles.aspx request)
- decodeVinByIndex(): resolves user-selected candidate by index
- vehicles.service: emexCandidates in VinResolveResult, emexCarIndex
param in decodeVin/resolveVin, resolveEmexCarByIndex() method
- vehicles.controller: @Body("emexCarIndex") accepted
- search.tsx: candidateSource state, handleCandidateSelect sends
emexCarIndex or pcatCarId based on source
Test VIN NM417800006410193 (Fiat SIENA) now shows 2-option modal.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Integrate external parts catalogs API with auth service, add BullMQ-based
catalog prefetch worker for background data caching, expand vehicles service
with shared vehicle support, and add vehicle select modal to frontend.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add unavailable/remark/modelCodes/presel fields to categories and parts DB schema
- Pass PL24 unavailable flag through pipeline instead of filtering out records
- Show unavailable categories/parts at reduced opacity in grid, tree, and parts panel
- Display part remark and model codes as secondary info in parts table
- Move Google sign-in button above email form on login/register pages with branded icon
- Add public email existence check endpoint for better login error messages
- Update INDEX.md documentation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add PL24 Ford legacy service for fordt_parts architecture
- Refactor EMEX to use persistent browser pool instead of per-call instances
- Make vehicle decode resilient: fallback to PL24 when Corgi doesn't recognize VIN
- Add collapsible sidebar with persistent user preference
- Improve brand access guard and categories service
- Add debug/test scripts for VIN e2e testing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace browser-based Puppeteer scraper with standalone scraper wrapper.
Add EMEX as fallback source for categories when PL24 is unavailable.
Update vehicle decoding to use new synchronous EMEX API.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add WV1/WV2/WV3 WMI codes for Volkswagen commercial vehicles (Ticari)
mapped to vn_parts service. Skip BOM links in category traversal since
they are leaf categories returning parts, not subgroups.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add POST /admin/users endpoint with password hashing and role support
- Add user creation dialog to admin users page
- Migrate web from Next.js to Vite + TanStack Router
- Fix Dialog component positioning for Tailwind CSS v4
- Add @source directive for @sase/ui package scanning
- Add pl24 integration parsers and vehicle decode flow
- Backup old Next.js app to apps/web-nj
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>