Floxy residential proxy bitince/çökünce (402 bakiye veya tünel reddi —
ERR_TUNNEL_CONNECTION_FAILED), EMEX + pcat decode istekleri ölü proxy'ye
çarpıp decode başarı oranını çökertiyordu (2026-06-13: ~17k tünel hatası/24s,
decode %78→%45). Önceki "DataImpulse last-ditch fallback" tarayıcı yolunda hiç
yoktu ve HTTP yolunda her istekte 2 ölü Floxy denemesi ziyan ediyordu.
Paylaşılan ProxyHealthService kapısı (in-memory cooldown): herhangi bir tüketici
bir Floxy bağlantı hatası görünce kapıyı tetikler; cooldown boyunca TÜM tüketiciler
DataImpulse'a düşer. Floxy'den ilk başarılı yanıt veya cooldown bitişi kapıyı
temizler (kendi kendini iyileştirir, periyodik yeniden-deneme). FLOXY_FAILOVER_COOLDOWN_MS
ile ayarlanır (varsayılan 180s).
Bağlanan tüketiciler:
- EMEX HTTP (fetchEmexHtml): kapı açıkken DataImpulse-öncelikli zamanlama
- EMEX tarayıcı (Playwright): launch'ta dinamik sağlayıcı seçimi + ensureSession
health-gate'i (Floxy ölünce DataImpulse'a relaunch, kapı temizlenince Floxy'i
yeniden dene); scrape-içi tünel ölümünde tripFloxyFailover
- pcat (buildProxy): hem call hem capture (Playwright JWT) bacakları DataImpulse'a düşer
isProxyConnectFailure(): yalnız gerçek bağlantı hatalarında tetikler — yavaş-ama-
canlı exit'in nav timeout'u failover'ı tetiklemez.
Test: ProxyHealthService + isProxyConnectFailure birim testleri; tüm api suite (303) yeşil.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reklam engelleyici / gizlilik eklentisi challenges.cloudflare.com'u
kestiğinde widget sessizce kayboluyordu; form token'sız gidince
kullanıcı yanıltıcı 'E-posta veya şifre hatalı' görüyordu. Artık
script yüklenemediğinde veya widget error-callback verdiğinde
widget'ın altında açıklayıcı bir uyarı çıkıyor (login/register/iletişim).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Broadened extractYearFromPcatCar after auditing 176 null-year prod pcat
vehicles: the build date sits under "production date" (space) far more often
than "production_date" (underscore), and values come both year-first
(2006/02/27) and day-first (08/03/2016). Match the date key by spelling-agnostic
substring and pull the 4-digit year via regex so parseInt no longer reads the
day. Concrete Year param still wins over build date.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pcat decode emits a faceting axis param key="year" value="All" BEFORE the
concrete per-car key="Year" value="2021". extractYearFromPcatCar used
find(includes("year")) which grabbed the axis → parseInt("All")=NaN → null,
so pcat-sourced vehicles (e.g. Opel ASTRA-J W0VPD5EC1MG063839) saved with a
blank year despite the year being present in raw_data. Scan all year-ish
params for a plausible value, then fall back to production_date.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Monthly/yearly purchases were one-time charges: our "subscription" was just
an end_date stamp, access silently died at period end and no renewal
machinery existed (no auto-charge, no reminder) — every paying customer had
to notice the lockout and re-buy by hand.
- Checkout now mode:"subscription" with inline recurring price_data; the
Stripe customer is stored on first purchase and reused (saved card +
invoice history on one record, with a stale-customer retry guard)
- invoice.paid webhook: extends end_date to the billing-line period end,
records a completed payment (deduped on stripe_invoice_id against webhook
retries), captures subscription_renewed with $revenue, mails the receipt;
late dunning recovery re-activates the row and re-grants Full-plan brands
- invoice.payment_failed webhook: dunning mail with Stripe's next retry
date; access is NOT cut — end_date governs and the nightly cron closes it
if every retry fails. Product rule: mail on success, mail on failure,
never a pre-charge reminder
- customer.subscription.deleted: stamps cancelledAt; renewals stop and
access runs out at end_date naturally
- cancel()/resume() sync cancel_at_period_end to Stripe (forwardRef pair) —
an in-app cancel that leaves the card being charged was unacceptable
- subscription_create invoices only enrich the checkout's payment row
(payment intent + invoice id for receipts/panel refunds); activation,
revenue and the receipt stay on checkout.session.completed
- migration 0020: users.stripe_customer_id,
user_subscriptions.stripe_subscription_id (+idx),
payments.stripe_invoice_id (+idx)
Legacy one-time subs (3 live payers) are untouched: they expire at their
end_date as before and board recurring on their next manual checkout.
Promote checklist: add invoice.paid / invoice.payment_failed /
customer.subscription.deleted to the prod webhook endpoint; verify Stripe
"Customer emails" upcoming-renewal reminders stay OFF.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Clicking "Öde" ran create(), which expired the user's live trial BEFORE
any money moved. Anyone who then abandoned the Stripe page lost their
remaining trial days and was fully locked out (brand-access requires
active|trial) — every abandoned checkout became silent churn (3 real
victims on prod: 06-02 and 2x 06-10, incl. a 9.990 TRY yearly intent).
- create(): no longer touches the trial; only stale pending rows are
expired at checkout start
- activateSubscription(): supersedes the trial here instead, once the
payment has actually landed (single legitimate early-end point;
idempotency guard unaffected)
- getMySubscription(): status-priority pick (active > pending > trial >
cancelled > rest) so the expired remains of an abandoned checkout —
always the newest row — no longer eclipse a live trial on the
subscription page
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Kısa sayısal kodlar markalar arası çakışıyor ve FARKLI fiziksel parçaların
fiyatları tek havuzda karışıyordu (vaka: FEBI 27155 ₺160 iken çip GROS+İBRAŞ+
NIFEA karışımı ₺566 medyan gösterdi).
- sku_map'e brand_norm eklendi (takip p10 kolonunun marka kısmı; yoksa sku
öneki) — vmi'de yeniden kuruldu (6,8M satır, %99,4 markalı)
- filterOffersForBrand: önek/sıralı-altdizi marka eşleme (BCH→BOSCH,
BLP→BLUEPRINT, BRA→İBRAŞ); markasız sorgu yalnızca uzun/benzersiz kodlarda
(≥8 ya da ≥5+harf+rakam) tüm teklifleri kullanır; kısa salt-sayısal kod
uyumsuzsa boş döner (yanlış veri göstermek yok)
- pg kimliği (code_norm, brand_norm) — 0019 drop+recreate (yalnız 3 test kodu
vardı, lazy-backfill yeniden doldurur)
- API: series?code&brand; current-batch artık { parts: [{code, brand}] } alır,
cevap "CODE::BRAND" anahtarlı; Redis anahtarları v2
- Web: satır çipleri ve dialog markayı geçirir; ana OEM kodu markasız kalır
(uzun-kod kuralıyla eşleşir); OE çiplerinde dağıtıcı etiketi fallback'i
(MAIS taşıyan tekliflere RENAULT isteği uzun kodda izinli)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The BullMQ cron scan never fired on prod (ic frozen at 28 for days, 0
scans completed) even after the lifo change. Root cause, confirmed live:
a cron scheduler enqueues a DELAYED marker, and BullMQ promotes delayed
jobs to the wait-list HEAD via LPUSH — `promoteDelayedJobs` ignores lifo —
while the worker pops from the tail. Behind the ~444k deep-drill backlog
the scan marker sat at wait position 1 (head) with ~444k jobs ahead of it
→ unreachable for weeks. So scheduler-level lifo cannot work; the marker
must not go through the queue at all.
Run the scan on an in-process hourly timer in PrefetchWorkerService
instead (kickoff 60s after boot, then hourly), prod-host gated and
single-fired via a Redis NX lock. The scan's Phase-1 still enqueues
NON-delayed `lifo` init jobs, which DO jump the wait list (RPUSH tail →
popped next), so newly-decoded / zero-parts vehicles get onboarded ahead
of the backlog while it drains. jobs.module now removes the old cron
scheduler so stale markers stop being produced; the backfill-scan job
handler stays so any already-queued legacy marker runs harmlessly.
Tested: spec covers the NX lock (run when acquired / skip when held) plus
the existing lifo + Phase-1/Phase-2 gating; full api suite 272 passed,
`pnpm build` + biome green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
expert-rewards cron'u artık üyelik uzatmıyor: ay kapanışında ilk 3'ü
hesaplayıp admin@sase.tr'ye Postal üzerinden kazanan listesini yollar
(sıra, ad, e-posta, puan, ödül 30/15/7 gün, kullanıcı ID) ve
oem_expert_rewards'a denetim satırı yazar; günleri admin elle tanımlar.
Dev DB'den atılan mailler "[dev]" konu önekiyle ayrışır. Mail-önce-satır-
sonra sırası retry'da mail kaybını önler (nadir çift mail kabul edilir).
Abonelik/kredi dokunuşu tamamen kaldırıldı.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The prod docker build runs `nest build`, which compiles spec files too —
the untyped `vi.fn(async () => undefined)` mock made `queue.add.mock.calls[i]`
a zero-length tuple, so the destructures tripped TS2493/TS2352 and failed
`pnpm build` (tsc --noEmit had skipped the spec). Type the mock args as
unknown[]. No behaviour change; tests still 7/7, `pnpm build` green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Program (oylama + muadil önerisi + liderlik) artık iki katmanlı kapının
arkasında: PostHog "oem-expert-program" ana şalteri (local-eval, fail-open)
VE kullanım kriteri — en az 2 FARKLI başarılı VIN decode VE en az 2 FARKLI
OEM kodu kopyası. Karar sunucuda tek noktada (ExpertAccessService.check);
web GET /oem-votes/access ile bir kez sorar, kriterler istemciye sızmaz.
Kapalıyken: sidebar nav linki görünmez, OEM detayındaki oy kartı + öneri
bölümü render edilmez, /dashboard/uzmanlar kademeli-açılış mesajı gösterir
ve liderlik sorgusu atılmaz. Yazma uçları (oy, öneri) sunucu tarafında da
ForbiddenException ile korunur — UI gizlemek tek başına güven sınırı değil.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Parça Uzmanları sıralaması artık aylık sezon: liderlik tablosu içinde
bulunulan TR-ayının (Europe/Istanbul) puanlarını gösterir ve her ayın
1'i 00:00 TR'de kendiliğinden sıfırlanır. expert-rewards cron'u
(BullMQ scheduler, "0 0 1 * *" tz=Europe/Istanbul) aynı anda biten
sezonu kapatır ve ilk 3 oylayıcıya üyelik uzatması verir:
1. → 30 gün, 2. → 15 gün, 3. → 7 gün (EXPERT_REWARD_LADDER).
Ödül mekaniği referral'la birebir: canlı active/trial abonelik endDate
+gün uzar, yoksa günler users.referral_credit_days'e bankalanır (sonraki
trial/aktivasyonda tüketilir). oem_expert_rewards (migration 0017,
period+rank UNIQUE) hem denetim kaydı hem run-once garantisi — retry ya
da elle tetik çift ödül veremez. Sıralama ölçütü job ve leaderboard'da
birebir aynı (puan desc, eşitlikte puana erken ulaşan önde).
Web: uzmanlar sayfasına sezon şeridi ("Haziran 2026 sezonu" + 🥇1 ay ·
🥈15 gün · 🥉7 gün rozetleri); alt küçük-punto kural satırına aylık
sıfırlama + ödül notu eklendi. Leaderboard cevabına periodStart eklendi.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Uyumlu/Uyumsuz butonlarında oy sayıları artık görünmüyor (çoğunluğu ele
vermek oyu yönlendiriyordu); özet yine çekilir — kullanıcının kendi oyu
butonda işaretli kalır.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Topluluk oyu kartının koyu dolgusu kalktı (sayfa arkaplanıyla aynı, yalnız
çerçeve); desktop'ta başlıkta OEM kodunun sağına yerleşti, mobilde kodun
altına iner. Katalogdan gelişte araç etiketi + parça adı search-param'la
taşınır (v/p/vid) ve kartta butonların üstünde "araç · parça" satırı olarak
gösterilir — neyle neyin uyumlu olduğu tek alanda. vid oy kaydına analitik
bağlam olarak geri eklendi; doğrudan ziyarette genel soru metnine düşülür.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Üstteki üç-kartlı kural bloğu ve oy kartındaki kural cümlesi kaldırıldı;
kurallar tek satır küçük puntoyla sayfanın en altında ("Oy ver +1 ·
çoğunluğu tuttur +2 bonus · kodu ilk değerlendiren 3 puanı kapar").
Başlık ve boş-durum metinleri de oyunlaştırıldı; oy kartında yalnız
"Her oy puan kazandırır · Parça Uzmanları sıralaması" linki kaldı.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Parça tablosu sadeleşti: Uyum ve Adet kolonları kalktı, liste açılışındaki
toplu istekler (/oem-votes/lookup ve /p/matched) tamamen kaldırıldı. Her OEM
kodu artık koşulsuz /dashboard/oem/$code'a linklenir — P eşleşmesi olmayan
kodda da sayfa dolu: topluluk oyu kartı, muadil önerileri ve ters katalog.
OEM detay sayfası: OemVoteCard (uyumlu/uyumsuz, sayaçlar, puan toast'ı,
puanlama özeti + Parça Uzmanları linki) ve OemSuggestionsSection — eşleşme
bulunamayan kodlar için kullanıcıdan marka + parça kodu önerisi toplar.
Öneriler oem_suggestions tablosunda (kullanıcı+kod+normalize öneri başına
tek satır, ON CONFLICT yutulur), markaya+normalize koda göre gruplanıp
"× N kullanıcı" rozetiyle listelenir; önerilen kod kendi detayına linklenir.
Şimdilik öneri puan kazandırmaz; status kolonu moderasyon kancası.
API: oem-suggestions modülü (POST 10/dk throttle, GET ?code=), migration
0016_oem_suggestions. Eski tablo-içi OemVoteButtons bileşeni silindi.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hourly catalog-backfill scan had gone effectively dead on prod: its
job sat behind a ~430k-deep wait list and, even when it ran, self-skipped
because the queue backlog (431k) was far over the maxBacklog ceiling
(1000). Net result: newly-decoded / zero-parts vehicles were never
onboarded — they starved behind the deep-drill backlog (ic stuck at 28
for days, 0 backfill-scan jobs ever completed).
Root constraint (verified against bullmq 5.68 lua): moveToActive drains
the `wait` list (RPOPLPUSH from the tail) BEFORE the `prioritized` ZSET,
so a `priority` job is starved behind an already-deep wait queue — the
opposite of what's wanted. The lever that works is `lifo`: it RPUSHes to
the tail, where the very next RPOPLPUSH picks it, ahead of the FIFO
backlog. addJobFromScheduler honours lifo too, so the scan job itself can
jump the queue.
Changes:
- Thread a `fast` flag through the init→children→parts chain; fast jobs
are enqueued with `lifo:true` so the whole chain jumps the backlog.
- Backfill scan: Phase-1 (zero-parts vehicles) now runs every wave in the
fast lane even when the backlog is over the ceiling; only Phase-2 (the
rolling rescan that piles on) is suspended while the queue is deep.
- Register the hourly scan job with `lifo:true` so it fires on the next
tick instead of being buried for days.
Tested: new prefetch-worker.service.spec (lifo wiring + Phase-1/Phase-2
gating), full api suite green (248 passed), typecheck + biome clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirrors the PSA VIN-scope tests for the new …group.action parent gate: a Ford
json-vin-sub-group.action node with a positional link_wid routes to children
(not a silent empty leaf), and an empty drill surfaces loadError.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Parça satırlarına topluluk oyu eklendi: her OEM kodu için uyumlu/uyumsuz
ikilisi (👍/👎, sayaçlı). Oylar oem_votes'a (kullanıcı+kod başına tek oy,
fikir değişikliği günceller, puan üretmez), ödüller oem_vote_points
ledger'ına yazılır: oy +1, kesin çoğunlukla aynı yönde +2 (kodu ilk
oylayan her zaman 3 alır); ödüller oy anında kesinleşir, çoğunluk sonra
dönse de geri alınmaz. Aynı koda eşzamanlı oylar advisory lock ile
sıralanır.
/dashboard/uzmanlar: Trophy Gamification UI Kit'ten (ui.trophy.so, MIT)
uyarlanan kürsü + sıralama + puan rozetiyle "Parça Uzmanları" liderlik
sayfası; adlar KVKK-maskeli (S*** Y***), cevap kullanıcı id sızdırmaz.
Not: sidebar nav linki, tr/en i18n anahtarları ve routeTree 09a9487'de
gitmişti; bu commit eksik kalan rota/bileşen/API dosyalarını tamamlayarak
dev build'ini düzeltir. Migration: 0015_oem_votes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop /dashboard/changelog route, ChangelogTab component, useChangelog
hook, the sidebar nav item, and the related tr/en translation keys.
API-side changelog schemas are untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
getCategoryWithPartsInner had parent gates for PSA (psa::/json-vin-main-groups),
pcat (hasSubgroups), pl24 link_wid *Group* tables, Volvo (vin-group.action) and
emex — but P4 VIN/legacy group nodes whose link_wid is a positional code
("1","2","CAP1"…) and whose path is a *…group.action* drill (Ford
json-vin-sub-group, Nissan/Opel json-sub-group, etc.) matched none of them. They
fell through to the leaf parts path, fetched no parts and rendered a silent empty
panel — the dominant current '0 parça' cluster (mekanik/kaporta/Elektrikli aksam…
across many vehicles; PostHog-confirmed). Add a gate that drills any pl24
…group.action node (excluding Volvo's own vin-group.action branch) → subgroups or
retryable loadError. Lazy-heals existing flag-less rows, no reseed.
Also set has_subgroups at seed time: PSA scopes → true (always parents);
P4-legacy cats → true when linkPath is a …group.action drill. Fixes folder
signposting + lets prefetch pre-drill them instead of treating them as leaves.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One row per proxied upstream attempt (pcat call/capture/validate, emex http)
written fire-and-forget by the new ProxyTelemetryService (buffered, capped,
errors swallowed — telemetry can never hurt the request path).
- banned = upstream 403/429 (IP-block signal), distinct from auth/data errors
- sticky legs (pcat capture, emex floxy) carry a session_key; pcat capture
also resolves the actual residential exit IP via a parallel ipify probe
through the same sticky session → concrete banned-IP tracking
- rotating legs log provider + outcome (ban *rate* instead of per-IP)
- 30-day retention piggybacked on the query-cleanup job
Feeds the Süper Panel /analytics/proxy page (provider grading + banned IPs).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Floating launcher covered content on small screens. Bubble is hidden
below 768px via injected CSS; programmatic openChatwoot() callers
(subscription page, onboarding modal) still open the fullscreen widget.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sidebar linked /blog and /contact pointed at public marketing pages, so
clicking them dropped the user out of the dashboard shell. Extract page
content into shared components (blog-content, contact-content) and add
dashboard-wrapped routes /dashboard/blog, /dashboard/blog/$slug and
/dashboard/contact; sidebar now links to those. Public SEO pages stay
unchanged and remain the canonical URLs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
After the Floxy account rotation, the new residential exit IPs are slower:
measured DOM-ready ~7s and the widget's /v3/api/proxy token call firing ~11.8s —
past the 10s PAGE_TIMEOUT + networkidle, so every capture timed out ("No token
after 12s") and the warm pool stayed empty (pcat dead despite valid creds).
- waitUntil networkidle → domcontentloaded (reliable ~7s; networkidle often never
settles on JS catalog sites through a slow proxy → goto times out pre-token).
- PAGE_TIMEOUT 10s → 25s (env PCAT_PAGE_TIMEOUT_MS); token fired at ~11.8s.
- CAPTURE_POLL_AFTER_OK 5s → 12s (env PCAT_CAPTURE_POLL_OK) to cover the ~5s
gap between DOM-ready and the token call. Capture is background (warm pool,
cold-pool fast-fail never blocks users), so the longer cap is free insurance.
Verified via standalone Playwright probe through the new Floxy: token captured at
11842ms (DOM 7023ms). typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hardcoded Floxy account (d739255e819b) ran out of balance → 402 Payment
Required on every CONNECT → pcat decode dead on prod since ~07:16 UTC 2026-06-11
(pcat has no proxy fallback; EMEX limped on its DataImpulse fallback). New funded
account cac4b0d96a80 verified working (call leg 401-reachable, capture leg 200).
Updates the hardcoded defaults in all 3 Floxy consumers (pcat auth, emex http,
emex browser); env overrides PCAT_FLOXY_USER/PASS + EMEX_FLOXY_USER/PASS unchanged.
Follow-up: wire FLOXY_USER/PASS into docker-compose env so future rotations are
Coolify-env-only (no code change / rebuild).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'Çalışmadı mı? Bildir' opened the Sentry feedback dialog; most users
bail at a form. Now a single click captures a catalog_issue_reported
PostHog event with the vehicle/category context (vin, vehicle_label,
category_name, source, page) and flips to 'Bildirildi — teşekkürler'.
The identified person already carries the reporter's email; the Süper
Panel lists these under /analytics/reported-categories. The automatic
Sentry degradation report (with session replay) still fires via
useReportCatalogDegradation — unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cold-pool fast-fail (and transport blips) make pcat return null without
ever evaluating the VIN; EMEX then identifies the car and the row lands
as 'No catalog — identified as X' (pcat sub-timing 1-2ms). Downstream
catalog-gap detection can't tell these from true coverage gaps and
flags covered brands (Audi, 2026-06-11, 7 retries by one user).
- timings.pcat_skipped=1 on cold-pool fast-fail
- timings.pcat_transient=1 on transport failure (circuit-open skips
already carry pcat_circuit_open)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
EMEX egress was DataImpulse-first: fetchEmexHtml tried 3 dead-prone DataImpulse
ports before falling to the Floxy fallback, and the browser scraper launched on a
random DataImpulse port. With ~50% of DataImpulse ports dead, this burned the 25s
decode budget before reaching Floxy (observed: a Ford VIN where pcat correctly
returned null fast, then EMEX's DataImpulse retries ate the budget).
ssd tokens are replayed from emex's own HTML and are NOT strictly IP-bound on the
.aspx endpoints (the prior random-port-per-call primary proved that), so a sticky
Floxy IP across the flow is strictly safer.
- EMEX_PROXY_PROVIDER (default "floxy" | "dataimpulse" | "none"). floxy →
fetchEmexHtml schedule = [floxy, floxy, dataimpulse] (Floxy sticky primary,
rolls IP on transport failure, single DataImpulse last-ditch). dataimpulse →
legacy [di,di,di,floxy,floxy]. Default HTTP agent (image-dims) follows provider.
- EmexBrowserService launches chromium on a sticky Floxy session (one residential
exit IP for the browser lifetime; fresh id per relaunch) under the same flag.
- Reuses the existing EMEX_FLOXY_* config (same Floxy account pcat now uses).
Rollback: EMEX_PROXY_PROVIDER=dataimpulse. typecheck+biome+11 emex tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Prod decode was dominated by the DataImpulse proxy: ~50% of ports dead at any
moment, 57% of captured tokens discarded by validation, the dead-port slot
never evicted (invalidateSession only fired on 401/403, never on transport
timeout), and a cold pool blocked the user inline on a multi-second capture.
pcat sub-timing was p50 8.3s / p90 56s, blowing the 25s decode budget.
Verified on prod: the TWS- token is NOT request-time IP-bound (0 auth 401/403
across thousands of calls; a token captured on one IP replays 200 through
arbitrary fresh residential IPs). So the proxy can rotate freely.
- Transport swap to Floxy residential (PCAT_PROXY_PROVIDER=floxy|dataimpulse|none).
Call/validate leg = a fresh exit IP per request (max IP diversity → min per-IP
ban); capture leg = a sticky session per Playwright attempt. DataImpulse +
direct kept as env fallbacks. Token decoupled from the slot (proxyPort removed).
- Prune 3 zero-yield JWT sites (knkauto/autodo.kz/flynestauto — 0 captures, ~80%
of attempts); add PCAT_JWT_SITES env override.
- Lower catalog-scrape timeout 30s→10s (env-tunable) so a slow IP aborts within
budget and the retry rotates to a fresh IP.
- Cold-pool fast-fail on the decode race: warm in background, let EMEX/PL24
answer instead of blocking inline; treated as a transient miss (no neg-cache).
- Evict a token after N consecutive transport failures on rotating IPs (the
dead-slot bug); reset on any 2xx.
- PCAT circuit breaker mirroring PL24 (transient-only; cold-skip neutral; skip
marks outcome.transient so a pcat-only VIN isn't negative-cached 30m).
Typecheck + Biome + unit tests (categories/vehicles) green. Default flips pcat
to Floxy on next deploy; instant rollback via PCAT_PROXY_PROVIDER.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New "Bu kod kataloğunuzda" section lists the user's decoded vehicles
whose parts include the queried OEM code, each linking to the schema
page that shows it. Pure sase data (parts.oem_code → vehicles, indexed
exact match) — no TecDoc / vehicle-structure dependency.
GET /parts/oem-vehicles?code=.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Customer entered VXKUPHNKSP4032040 (2023 Corsa F) and got a bare
'Marka desteklenmiyor' — VXK was missing from all three WMI maps, so
identifyBasic could not even name the brand. PL24's opel_parts catalog
ends at CORSA-E (2015-2019, verified via the model-picker page), so the
catalog itself stays a gap, but the user now gets the honest
'Opel 2023 olarak tanındı, katalog henüz mevcut değil' message and the
miss is logged for the coverage backlog.
- WMI_BRAND_MAP + PL24_WMI_SERVICE_MAP + EMEX CATALOG_MAP: VXK -> Opel
- DECODE_CHAIN_VERSION 2 -> 3 (brand-mapping change; invalidates the
stale negative cache for the customer's VIN)
- vin-cache-keys.spec: lock-key assertion checked the bare version char,
which now appears inside the test VIN — assert the ✌️ segment instead
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
POST /vehicles/decode returned the full vehicles row while every consumer
(search, landing, service-test) only navigates by id and re-fetches via
GET /vehicles/:id. That shipped the raw upstream decode payload to the
client on every decode — rawData is 19KB avg / 127KB p95 / 207KB max in
prod — plus the provider name, for nothing.
The response is now { id, brandName, model, year } on both the DB-hit and
fresh-decode paths. Frontend drops the (now absent) source property from
the vin_decode_success event; decode source remains queryable server-side
via query_logs.source.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The multi-candidate decode response leaked provider internals (source name,
pcat car ids, EMEX _ssd/_vid/_quickGroupsUrl/catalogId) and made the client
carry them between requests: the frontend stored candidateSource and echoed
pcatCarId/emexCarIndex back on selection.
Now the candidate list returned to the client carries only display fields
(name, description, parameters) plus an opaque key, and the provider mapping
is stashed in Redis (vin:candidates:*, 30m TTL, resolve-cache fallback). The
pick request sends just { vin, candidate }. Legacy pcatCarId/emexCarIndex
body params still work for already-loaded bundles.
Also drops `source` from the public /vehicles/preview response — no consumer
used it, and provider names must never be public (same policy as
teaser-stats).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Polish pass on the survey popover:
- Entrance: slide-up + fade on mount (300ms ease-out, respects
prefers-reduced-motion via motion-reduce:transition-none).
- Step progress: "1 / N" indicator on multi-question surveys so users know
how much is left.
- Esc closes the card (counts as a dismissal); listener attached once with a
dismissRef so it captures current answers, not a stale first-render closure.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design review of the live survey popover surfaced three issues:
1. NPS 0–10 wrapped to two rows (flex-wrap + flex-1 made the 8/9/10 row
stretch absurdly wide) — now a single equal-width grid row (grid-cols-N
by scale; static map so Tailwind keeps the classes).
2. Single-question ratings forced a redundant "Devam" tap — tapping a score
now auto-advances (450ms debounce to allow changing the pick) and the
submit button is hidden on rating questions.
3. On mobile the card crowded the Chatwoot launcher — bumped to bottom-28
(sm:bottom-24 keeps desktop unchanged).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Remove the EAN line from the matched-article cards and stop fetching/
shipping eans in /p/oem (its only consumer was that line). Snapshot
still holds the data; the endpoint just no longer queries it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
OEM detail "slow load" was two things, not the query (DB 135ms / API
13-120ms):
1. The parts-panel link opened a NEW TAB → full SPA cold boot every
click. Switch to in-app client navigation on plain click (real href
kept, so ctrl/cmd/middle-click still opens a new tab).
2. The /p/oem response shipped each article's oeNumbers + compatible
lists (up to 200 each × 60 articles) that the UI never renders — 96%
of a 370 KB payload. Ship lean articles; aggregates already carry
the cross-refs. 60-article code: 370 KB → ~12 KB.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
trial_urgency_banner_viewed fires ~1s into the page load and raced the lazy
surveys extension — events captured before its hook exists can never activate
an event-triggered survey (affects real users, not just e2e). capture() now
awaits surveysReceiverReady() (onSurveysLoaded, 3s safety timeout) and runs the
eligibility check after the event has gone through.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>