Tüm sub-90 markaların global eşleşmeyen adlarından net olanlar (çoğu İngilizce,
markalar arası): diagnostic/entertainment/bilgisayar/elektrigi→elektrik,
heater/ventilation→sogutma, braking→fren, sunroof/exterior/locking→dis-kaporta,
restraint/occupant/konfor→ic-doseme, mekanik→motor, yapistirici→yaglar. 24/24.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
BMW (~%81): elektronik terimleri → elektrik (audio/navigasyon/elektronik/
iletisim/multimedya/cruise), "ışık"→aydınlatma; servis/doküman sayfaları →
junk (teknik doküman/servis ve onarım/value parts). Ford (~%74): trans
varyantları speed/spd→sanziman, motor varyantları diesel/dohc→motor, gövde→
panjur. 23/23 test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
^[cd]\d{3,} fazla genişti — "D765300(22H), LS 7 F, MB SERVO DİREKSİYON" gibi
parça bilgisi taşıyan düğümleri yapısal yapıp Direksiyon eşleşmesini bozuyordu
(Mercedes ham kapsama %79→%49). $ ile sabitle → sadece çıplak kod (C442,
C105011). Açıklamalı düğümler keyword'le eşleşmeye devam eder. 19/19 test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Opel gerçek parça-kat kapsaması zaten %85; %38 model-kodu köklerin (yapısal)
denominatörde olmasından. Lexicon: sanzuman/disli→sanziman, suspensiyon/dingil→
suspansiyon (Opel yazımları). Serving: groupCategoriesByCanonical yapısal/çöp
düğümleri "Diğer"e sokmadan atlar (Opel model kökleri Birleşik'te görünmez).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Kendi adı eşleşmeyen kategori (Renault P5 X1318_ plaka kodları, figür
illustration'ları) parent'ının kanonik kovasını miras alır ("Alternatör"→
Elektrik). Materyalizasyondan sonra birkaç seviye kaskad. Re-scrape gerektirmez.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Upstream doğrulandı: illustration satırında td[0]=figür kodu ("20-201A .1"),
td[1]=açıklama ("SUB ENGINE ASSY") — parser ilk td'yi (kod) alıp açıklamayı
düşürüyordu. Artık ilk td figür-koduysa (\d\d-\d\d\d[A-Z]?) açıklamalı sonraki
td eklenir → "20-201A .1 SUB ENGINE ASSY", canonical eşleşir (engine→motor).
Diğer markalar etkilenmez (yalnız kod-only ilk td'de tetiklenir). 10/10 test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pl24 Asyalı markaları (Hyundai/Kia/Nissan/Toyota) ve çevrilmemiş emex/pcat
yaprakları İngilizce kategori adı taşıyor (ENGINE/BODY/BRAKE/TRANSMISSION…) →
%0 kapsama. Sistem kelimeleri strong (engine→motor, brake→fren, transmission→
sanziman, electrical→elektrik…), konum kelimeleri weak (body/chassis/floor/axle)
ki "BODY ELECTRICAL"→elektrik olsun. 17/17 test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
migration 0025: 2 yeni kanonik kova (jant-lastik #19, aksesuar #20).
lexicon: jant/lastik/tekerlek/göbek→jant-lastik, aksesuar→aksesuar (catch-all,
en düşük öncelik). Kullanıcı kararları: Hava Sistemi→Soğutma (hava weak),
Bakım parçaları→Filtreler (bakim weak). "hava yastığı"→direksiyon için yastigi
strong'a alındı. 16/16 test. Yorumlar 18→20 kova güncellendi.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Araç sayfasında browse varsayılanı "canonical" (Birleşik) oldu; toggle
"Birleşik | Orijinal Katalog" — Orijinal Katalog seçilince ham OEM ağacı
gösterilir. Kullanıcı isteği: birleşik katalog default, orijinal opsiyonel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Kök tam da kanonik başlıkla anılınca ("Süspansiyon, şasi", "Elektrik",
"Aydınlatma") lexicon'da o kelime yoktu → "Diğer"e düşüyordu. suspansiyon/
elektrik/aydinlatma kendi kovalarına strong olarak eklendi. Serving on-the-fly
fallback aynı lexicon'u kullandığı için taze araçlarda backfill'siz düzelir.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backend: groupCategoriesByCanonical'a requireParts opsiyonu; her iki serving
metodu artık parça-içeren yaprak yoksa (taze/drill-edilmemiş araç) kök
kategorileri kovalayarak boş dönmüyor.
Frontend: araç sayfasına "Katalog | Birleşik" toggle'ı; Birleşik seçilince
CanonicalView aracın kataloğunu 18 kanonik başlık altında gösterir, her kova
CategoryGrid ile normal drill'e bağlanır. canonical_view_opened event'i.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GET /api/catalog/vehicles/:id/canonical → catalog_vehicle_id akışının canonical
karşılığı; pl24 markaları (VW/Opel/Renault) emex/pcat ile aynı 18 başlıktan
gezilir. Parça sayısı category_id ile join'lenir (pl24 parçaları çoğu zaman
vehicle_id taşır, catalog_vehicle_id değil). Gruplama mantığı iki servisin
paylaştığı canonical-grouping.ts helper'ına çıkarıldı (categories.service da
bunu kullanacak şekilde refactor edildi).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GET /categories/canonical → 18 kanonik üst kova (browse landing).
GET /categories/canonical/:vehicleId → aracın parça-içeren leaf kategorilerini
18 kova altında gruplar; VW ve Opel gibi kökten farklı OEM ağaçları tek/aynı
başlık setinden gezilir. Kovalama materyalize canonical_category_id'yi
kullanır, son backfill'den sonra decode edilmiş kategoriler için ortak
canonical-lexicon ile on-the-fly fallback yapar; kovasız olanlar "Diğer"e düşer.
foldName() NORMALIZE_SQL'in JS karşılığı olarak eklendi.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
reset:true → non-manual keyword katmanını sıfırdan kurar: kapsamdaki
materyalize id'leri NULL'lar + stale non-manual map satırlarını siler,
sonra yeniden eşler. Manuel satırlar korunur. Sadece GLOBAL run'da (marka
scope'suz) geçerli — map isim-düzeyi/marka-bağımsız olduğu için marka-scope'lu
silme başka markaların paylaşılan eşlemelerini bozardı. Böylece lexicon/
sınıflandırıcı değişince eski eşlemeler dangling kalmaz.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gerçek VW parça adları geçerlilik-tarihi marker'ı taşıyor ("pencere camlari
d >> - 14.07.2019"); bare ">>" onları yanlışlıkla yapısal işaretliyordu (VW
kapsamı düşüyordu). Gerçek VIN düğümlerini zaten VIN_LIKE yakalıyor.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Önceki commit'te (7b5e678) src/worker.ts git add kapsamı dışında kaldı
(apps/api/src/jobs/ eklendi ama worker.ts src/ altında). Queue/processor
deploy oldu ama tüketici Worker yoktu → enqueue edilen job'lar 'waiting'de
takıldı. Bu commit Worker instance'ını ekler.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Kaynak/marka-bağımsız tek parça taksonomisi. Ham OEM ağaçları (categories.parent_id
+ pl24/emex/pcat aynaları) dokunulmadan kalır; kanonik omurga onlara dik.
- migration 0024: canonical_categories (18 üst kova seed) + category_canonical_map
(isim-düzeyi eşleme) + categories.canonical_category_id kolonu
- canonical-lexicon.ts: iki-aşamalı eşleme (classifyNode: parça/yapısal/çöp;
mapToCanonical: 18-kova keyword). VW+Opel pilotunda kanıtlandı — Opel ağacının
%87'si model-seçici düğüm, bunlar kovaya girmez.
- canonical-backfill BullMQ job'ı (on-demand): distinct foldlanmış adları
sınıflandırır, category_canonical_map'e upsert eder (is_manual korumalı),
categories.canonical_category_id'yi join ile materyalize eder. brands/sources/
dryRun parametreli.
- 8 unit test (classifyNode/mapToCanonical)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Süper Panel'den tam anahtar yönetimi için:
- GET /internal/admin/api-keys artık userId/email'siz TÜM anahtarları
kullanıcı e-postasıyla birlikte döner
- GET /internal/admin/api-keys/usage — günlük decode kotasının anlık
Redis durumu (limit/used/remaining), ids ile filtrelenebilir
- PATCH /internal/admin/api-keys/:id — name, dailyDecodeLimit(null=varsayılan),
domains (yalnız publishable), rateLimitMax(null=plugin varsayılanı),
expiresInDays(null=süresiz)
- PublicApiQuotaService internal-admin'e provider olarak eklendi
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
JWT sitelerinin (parts-catalogs v3 widget'ını gömen partner mağazalar) sağlık
incelemesinden çıkan iki iyileştirme:
1) Direct-capture fallback (PCAT_CAPTURE_ALLOW_DIRECT, default açık): proxied
capture tünel/connect hatasıyla ölürse (Floxy bakiye 402 / tünel down,
DataImpulse portları ölü — 2026-06-11 ve 2026-07-03'te yaşandı) aynı siteyi
sunucu IP'sinden DIRECT yeniden dener. Güvenli: TWS- token IP-bound DEĞİL +
capture düşük frekans (≤MAX_POOL_SIZE refresh/~8.5dk). Doğrulandı: Floxy down
iken direct 1.4s'de gerçek TWS- token yakaladı → warm pool artık tam proxy
kesintisinde bile açlığa girmez.
2) Sert havuz üst-sınırı (enforcePoolCap): refresh yolundaki slot-muhasebe
yarışı havuzu MAX_POOL_SIZE=5'in üstüne şişiriyordu (prod'da 32-39 gözlendi
→ gereksiz capture yükü + partner-site ban riski). Her push'tan sonra en eski
slot(lar) çıkarılıp cap uygulanıyor, timer'ları temizleniyor.
6 JWT sitesinin hepsi canlı (direct 1.3-2s) — liste budanmadı. Birim testler +
canlı Floxy-down doğal deneyiyle doğrulandı.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Kategori-detay parçalarında iç alanlar (source=upstream sağlayıcı adı,
presel, ham price/currency, catalogVehicleId...) public API'den sızıyordu —
decode kontratı disipliniyle beyaz-liste: id/name/description/oemCode/
position/quantity/remark/unavailable/categoryId
- Widget parça tablosu positionNumber yerine gerçek alanı (position ??
hotspotIndex) okuyor
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
String dönüş {success,data} JSON'una sarılıyordu; Content-Type JS ama gövde
JSON olunca tarayıcı ERR_BLOCKED_BY_ORB ile script'i blokladı. @Res ile ham
send'e geçildi (shell zaten öyleydi).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
30 günlük pilot anahtarı üretimi 'expiresIn is larger than the predefined
maximum value' hatasıyla düşüyordu — plugin expiresIn'i saniye bekliyor,
gün→ms çevriliyordu.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add lib/google-ads.ts (lazy gtag init, gclid capture, Enhanced Conversions,
trackGoogleAdsConversion). Fire sign-up conversion on email registration and
purchase conversion on Stripe success. Gated on VITE_GOOGLE_ADS_* env; no-op
when unset. Mirrors the Meta Pixel/CAPI pattern.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Shorten the free trial from 30 to 7 days: backend source of truth
(subscriptions.service TRIAL_DAYS), in-app banner rescale to the 7-day
window, tr/en copy (17 keys each), hardcoded CTAs, and the trial-progress
test. Existing trials keep their stored endDate — new sign-ups only.
Clean 3-way merge with the discount-code billing feature: trial and promo
changes touched disjoint regions of subscription/index.tsx and the message
catalogs. Post-merge verified — web+api typecheck clean, web tests 92/93
(the 1 red is the pre-existing landing index.test.tsx, unrelated to trial).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The permanent (paid) Vinpin seat does NOT auto-launch VinPower after login —
it lands on the Horizon HTML-Access app launcher ("Available: VINPIN"), so the
old brand-grid→Fiat flow failed with "Fiat ePER did not open". Add an
OCR-gated ensureBrandGrid step before the ePER-open loop that:
- detects the launcher (Available / Running / No Running Items) and clicks the
VINPIN app tile to launch VinPower;
- satisfies VinPower's own secondary login dialog (username pre-filled,
password blank) via the webclient-frame DOM password field, with a
coordinate fallback;
- dismisses a stray Horizon "Disconnected" dialog / waits through the blank
connecting screen;
- no-ops when the brand grid is already up (trial-style auto-launch), keeping
it backward-compatible.
New coords/waits tuned for the permanent seat live in vinpin.constants.ts.
Never throws (failures fall through to the caller's retry → null). Verified
live on the permanent account: NM426300006A69963 → DOBLO, NM435600006H43436 →
TIPO-EGEA.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Backend source of truth (subscriptions.service.startFullTrial) now grants a
7-day trial via a named TRIAL_DAYS constant. Existing trials keep their stored
endDate — only new sign-ups are affected, so no migration is needed.
In-app conversion UX rescaled to the 7-day window, preserving the two-banner
no-overlap design:
- urgency banner: visible the last 3 days (soft d3 / urgent d2 / critical d1)
- value-upsell: owns days > 3 (the opening days), still proven-value gated
- progress bars now derive the total span from real start->end dates, so both
7-day and any remaining legacy 30-day trials render an accurate bar
Subscription-page thresholds (<=3 / >3) and the analytics gate updated to match.
Copy: 17 trial strings each in tr.json/en.json plus hardcoded CTAs in
site-header, _auth, register and demo-footer-cta -> "7 gun / 7 Gun / 7-Day".
Lifecycle emails need no change: trial-ending fires 3-4 days before endDate
and win-back after expiry (both already relative to endDate), and templates
use the dynamic {{daysLeft}} variable (no hardcoded 30).
Test: trial-progress-banner spec updated to 7-day scenarios.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the brittle clipboard/fixed-coord extraction with an OCR vision loop:
screenshot -> detect state (login/grid/portal/eper/modal via tesseract keywords)
-> act -> verify -> retry/recover. Adds portal->catalogue detection (the seat
non-deterministically opens a multi-brand portal with no VIN field), not-found
handling, and ffmpeg crop+upscale -> tesseract.js for the decode modal. Only the
model token + year are needed (matcher-tolerant), so OCR garbling is harmless.
Validated live against a trial seat: 6/6 of the ePER-present Fiat VINs decoded to
the correct model (EGEA/DOBLO); 2 genuine "vehicle not found" coverage gaps
reported as null. 8/8 correct outcomes. Still flag-gated off by default; tesseract
is dynamically imported only inside the worker. Warm-session persistence under
swiftshader is the remaining hardening item (currently re-logins per VIN).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When PL24/pcat/emex can't decode a Fiat VIN, decode it via the Vinpin ePER
web catalog (warm-session Playwright worker, single seat, BullMQ concurrency 1),
cache the exact vehicle in vinpin_decodes, match it to PL24's existing
catalog_vehicle for that model, and serve the parts from there. Vinpin = decode
oracle only; PL24 already holds the parts (e.g. Egea/Linea/Doblo).
Strictly gated behind VINPIN_ENABLED (default false) + a Fiat-only brand
allowlist: with the flag off, decodeVin behaviour is byte-identical and the
queue is never touched (covered by tests). Coordinates/selectors in
vinpin.constants.ts are marked TUNE-AGAINST-LIVE-PAID-SEAT.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pcat kuyruğun %67'si (~26.5k) ve en yavaş drenajı. Proxy = DataImpulse rotating
gateway (74.81.81.81:10000-10999, ~1000 port-rotated çıkış IP — 2026-06-25
doğrulandı: 5 port→5 farklı Alman residential IP), per-IP ban yükü dağılıyor →
8/dk shared cap gereksiz muhafazakâr. Adım 1: 20/dk + pace 5s. (Coolify env yolu
duplike-girdi yüzünden container'a inmedi; kod-default güvenilir yol.) Env override
hâlâ mümkün (PREFETCH_RATE_PCAT/_PCAT_DELAY_MS). 429/403/tunnel izlenecek.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Yearly upfront (₺9.990 Full) is the top checkout-abandonment point, so an
annual commitment shouldn't be the default selection. New users now land on
monthly; the yearly tab keeps its +2-month gift badge, and an existing/
pending subscriber's or marketing-preselected period still wins.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a VIN can't be decoded by any source but its WMI brand is known and
has a browse-able catalog (SERVICE_TO_BRAND), return a structured
{ noCatalog: { brandName, display }, vin } 200 response instead of
dead-ending. The web surfaces a "kataloğunu modelden incele" CTA that
deep-links into the existing /dashboard/catalog browse, where the parts
usually exist (Fiat Egea NM4356 -> PL24 TIPO-EGEA; old Renault VF1 -> 147k
emex parts) but aren't reachable by the specific VIN's index entry.
Brands with no browse catalog (Honda, Maserati, Alfa, ...) keep the
existing informative dead-end. New analytics: vin_decode_no_catalog +
vin_no_catalog_browse_clicked (this case no longer emits vin_decode_error).
Tests: 2 api (browseable -> fallback, non-browseable -> throws) + 1 web
(CTA renders, no error banner). RCA writeup:
/home/s/ss/katalogsiz-vin-rca-2026-06-23.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tespit: generic-model (model==brand) sadece Opel'i değil PSA'yı (Peugeot/Citroën)
da kapsıyor; ama PSA generic-model PATLAMIYOR (~200 kat) ve PARÇA ALIYOR (512/290).
Skip onları yanlışlıkla poison'lar + prefetch'i keserdi. Tek doğru guard =
CATEGORY_CAP (marka-bağımsız; yalnız gerçek patlamada >3000 tetiklenir → Opel'i
durdurur, küçük PSA'yı korur). isGenericModel + processInit gate kaldırıldı,
spec sadeleşti (cap + scan-skip kaldı).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>