The Vinpin catalog matcher could route a European Renault VIN
(VF1RFE00653633190, decoded "KADJAR") onto the China-market catalog
"KADJAR ÇİN" (source XZH, 22 categories) instead of the correct European
"KADJAR" (XFE, 44 categories), serving wrong parts. Root cause: the
Turkish "ÇİN" lost its Ç/İ to the ASCII token strip and collapsed to a
dropped 1-char "N", so "KADJAR ÇİN" tokenized identically to "KADJAR" —
the market qualifier was invisible to the scorer.
Fixes:
- modelTokens now folds diacritics (NFD + combining-mark strip) so
"ÇİN" survives as the ASCII token "CIN".
- Scorer prefers an EXACT normalized model match (no extra tokens) over
a superset, via a bonus kept smaller than the year-range swing so
multi-generation routing (2022 TIPO-EGEA → MCA) is unaffected.
- New MARKET_QUALIFIERS set (ÇİN/CIN, CHINA, CHINE, RUSYA, ... grounded
in the real Renault/Dacia catalog rows) heavily penalizes candidates
whose EXTRA tokens are region qualifiers; generation tokens are not
penalized, so generations stay matchable.
- Richer-catalog (categoryCount) tiebreak among equal-score candidates;
match() query selects the category count via a correlated subquery.
Adds unit tests incl. the KADJAR case, CLIO/DUSTER generation cases, and
diacritic-folding. Fiat behavior unchanged; all 39 vinpin tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two Vinpin decode-flow UX bugs in the search page:
1. Poll window (VINPIN_MAX_POLLS 6→30, still 3s interval) now covers a
cold Vinpin VDI decode (~60-100s) instead of bailing at 18s. On
poll-exhaust we no longer fall through to the no-catalog dead-end;
instead a reassuring brand-neutral "still working, ready shortly"
card (decodePendingHint) — the decode caches server-side so a later
re-submit returns instantly.
2. The poll-exhaust fallback previously forced brandName:"Fiat" onto the
no-catalog prompt, so a Renault VIN read "Fiat". Removed. The
no-catalog copy is brand-aware via the API's noCatalog.brandName, with
brand-neutral fallback keys (noCatalogHintNeutral / browseCatalogCtaNeutral)
when no brand is available. A Renault VIN never says "Fiat".
Web-only, no flag. New i18n keys added to tr.json + en.json. No API change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Latency optimizations in the flag-gated (VINPIN_ENABLED) Vinpin decode driver.
Every change keeps the existing fixed-wait value as a fallback cap, so
worst-case behaviour and robustness are unchanged — only the common case is
faster. No live seat session was run (prod holds the single Horizon seat).
#2 Renault→Fiat symmetric swap: ensureReady no longer tears the whole browser
down on a Renault→Fiat flow switch (was a ~60-80s cold restart). It now keeps
the authenticated Horizon/VinPower session, returns to the brand grid via the
existing ensureBrandGrid (which closes a resumed foreign catalog window) and
reopens Fiat via the normal tile flow — symmetric with the Fiat→Renault
direction. Guarded fallback: if the in-session swap can't reach the grid /
throws, it falls back to close()+cold re-establish.
#3 OCR-poll-until-ready: replaced big fixed post-action sleeps whose completion
is OCR-detectable with a capped poll (screenshot→ocrRegion→regex every ~700ms,
return on match, cap == old fixed wait). Converted: afterFiatOpen(12s),
afterVinSubmit(7s, Fiat modal), afterRpartstoreSubmit(8s),
afterRenaultCatalogOpen(12s, ×2), plus cold-path afterLogin(20s) and
afterVinPowerLogin(18s). Left afterVinpinLaunch(14s) fixed — the VinPower login
dialog has no reliable OCR marker (detected via DOM), and it doubles as a
generic connecting-screen settle. Left afterDialogysSubmit fixed (out of scope).
#4 Micro-trims: field-clear Backspace burst 40→5 (VINPIN_FIELD_CLEAR_BACKSPACES),
key-type delay 45-50ms→20ms (VINPIN_TYPE_DELAY_MS, 17-char VIN ~850→~340ms),
afterAlertDismiss 700→250ms.
New pure/injectable pollForText helper in vinpin.ocr.ts (unit-tested:
early-return, cap-never-exceeded, first-read match). typecheck + biome clean;
31 vinpin unit tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
VINPIN köprüsüne Fiat'ın yanına Renault/Dacia desteği eklendi.
- Marka yönlendirme: selectVinpinBrandFlow(vin) WMI'den akışı seçer
(Renault/Dacia → Rpartstore/Dialogys, geri kalan → mevcut Fiat ePER,
Fiat davranışı byte-identical). isVinpinBrandAllowed allowlist'i
fiat + renault + dacia'ya genişletildi.
- Renault akışı durum-toleranslı: koltuk son katalogu (Rpartstore)
sunucu-taraflı hatırlayıp açık resume ediyor; ensureRpartstore grid /
submenu / açık-pencere / yükleniyor durumlarını tanıyıp kendini
toparlıyor. Rpartstore ana akış, Dialogys best-effort fallback.
- Ortak ensureBrandGrid artık resume olmuş yabancı katalog penceresini
(Rpartstore/Dialogys/ePER) kapatıp grid'e dönüyor → Renault↔Fiat
ardışık decode'ları (tek koltuk) artık kırılmıyor.
- parseRenaultHeader/isUsableRenaultParse: OCR başlığından model+marka
(RENAULT/DACIA) + yıl; bilinen-token allowlist ile contiguous model
koşusu. Matcher + processor kararlaştırılan markaya göre PL24
catalog_vehicle eşliyor (Fiat varsayılan korunur).
- Testler: Renault parser + marka-yönlendirme/allowlist birim testleri.
Flag-gated (VINPIN_ENABLED). Canlı doğrulama: 5/5 Renault/Dacia VIN
(Kadjar, Clio II, Clio IV, Dacia Duster, Latitude) doğru decode; Fiat
Egea spot-check korunuyor.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Correction to #216: "Birleşik" must fold the catalog into OUR canonical 20-bucket
structure (pcat + eper), not show parts-catalogs' raw tree. Only "Orijinal
Katalog" swaps to the native ePER tree for Fiat.
- treeSource → eper-pekidi for Fiat (Orijinal); canonical tab renders CanonicalView
- reverts #216's "canonical tab → pcat source tree"
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Simplify the previous change: instead of a third source-switcher pill, reuse the
two tabs that are already there. For an eper (Fiat) vehicle:
"Orijinal Katalog" → the native ePER tree (default)
"Birleşik" → parts-catalogs
Non-eper vehicles keep the original source/canonical tabs untouched.
- web: derive treeSource from browseMode when the vehicle has eper; canonical
tab renders the pcat source tree (not CanonicalView) for eper vehicles
- removes the ePER/Diğer switcher, SOURCE_LABELS, and the activeSource state
Net −27 lines.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fiat dealers know the native ePER tree; parts-catalogs' broader-but-lazy folders
are where they hit gaps (a motor OEM missing under "Motor" — the reason we
crawled pekidi ePER). So for a multi-source decoded vehicle, default the
"Orijinal Katalog" view to eper-pekidi — fully populated with parts + diagrams —
instead of showing every source's roots mixed together.
- categories.service.getCategoryTree(vehicleId, source?): prefer eper-pekidi when
present; ?source= overrides; cache keyed per source
- categories: getCatalogSources(vehicleId) → sources present, default (eper) first
- controller: tree/:id/sources endpoint + ?source query
- web: source switcher pill (ePER ⇄ Diğer) in source mode when >1 source;
defaults to the first (eper). pcat never hidden, just one tap away.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A failed ePER VIN lookup ("vehicle not found") whose Cyrillic "не найден" alert
OCRs to garbage ("He HaaeHs") slipped the not-found check; the parser then
grabbed the operator's last-browsed catalog from the breadcrumb
("FIAT » TIPO-EGEA") and false-mapped it to a catalog — NM4131 landed on the
ancient "Tipo 1100-1370-1580 (1987-1993)", plus 2× PANDA. Every genuine decode
carries a SINCOM; the breadcrumb false positives never do.
- parser: isUsableParse now requires a SINCOM (a model alone is not a decode)
- matcher: modelTokens keeps 4-digit engine displacements (1100/1370/1580) —
only 1950-2039 count as years — so ancient catalogs no longer collapse to
["TIPO"] and win the fewest-extra-tokens tiebreak
- matcher: recency tiebreak on equal score — never fall back to an ancient
generation for a bare / year-less model
15 unit tests (parser + matcher) pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Panelin 'Bonus süre ekle (goodwill)' aksiyonu 360 gün gibi süreler
gerektiriyor; 90 gün sınırı iş ihtiyacını karşılamıyordu. Üst sınır
3650 (10 yıl) parmak-kayması koruması olarak kalıyor. Panel tarafı
(sp) aynı limite güncellendi.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fast worker canlıydı ama pcat'in 90/dk penceresini ana worker yiyordu → fast
işler sürekli 18s source-rate defer (BMW init'i slot bulamadı). checkSourceRate
artık şerit-bazlı: fast = total/3 (pcat 30), main = kalan (60); toplam
değişmedi → upstream yükü aynı. Cooldown pause'u da doğru worker'ı duraklatıyor.
Spec fast-queue davranışına güncellendi (17/17).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Kök neden: ana kuyruk saatlerce üretim>tüketim rejiminde (children patlaması);
lifo'da sonradan eklenen HER iş bir öncekini gömer → taze araç init'leri bile
açlığa düşer (BMW 320i 58dk 0 parça). Fix: catalog-prefetch-fast kuyruğu +
ikinci worker (conc=4, 120/dk) — decode-tetiği ve fast:true işler (children
zinciri dahil) buraya akar; per-source redis rate gate iki worker'ı da ortak
sınırlar (upstream korunur). vehicles.service fast kuyruğa bağlandı.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
/dashboard/catalog/:brand/:modelId sayfasında birleşik görünüm yoktu — backend
endpoint'i (/catalog/vehicles/:id/canonical) hazırdı ama web'e hiç bağlanmamıştı.
CanonicalView'a endpoint/catalogMode/brandName/variantSearch prop'ları eklendi
(varyant qs dahil); sayfaya vehicles/$id ile aynı toggle. Varsayılan Orijinal.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(1) Placeholder'lar subgroup içinde olduğundan başlık "0 kategori" diyordu —
sayaç artık flat+subgroup birleşik unique. (2) Kalabalık markalar (pl24 BMW
elektrik 537 entry) görünümü boğuyordu — kova başına en-paylaşılan 40 entry
(vehicle_count desc).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Kullanıcının orijinal tasarımı: aynı (kaynak, marka)da kategori yapısı birebir
aynı — değişen yalnız parça kodları/şemalar. Önceki şablon sadece kova/alt-yaprak
BAŞLIKLARINI overlay ediyordu → taze araçta "Aydınlatma — 0 kategori". Yeni:
canonical_template_entries (migration 0031) — markanın paylaşılan kaynak-kategori
ADLARI (≥2 araç); backfill'de rebuild. overlayTemplate eksik adları disabled
placeholder olarak basar (unavailable=true, CategoryGrid zaten soluk+tıklanamaz
render ediyor) → decode anında her kova tam adlarıyla dolu; drill tamamlanınca
placeholder'lar gerçek satıra dönüşür.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"Fotoğraf okunamadı" genel metni gerçek nedeni (örn. 503 "görselden şase
okuma şu anda kullanılamıyor", 400 "desteklenmeyen tür") yutuyordu — dev'de
OPENROUTER_API_KEY boş kaldığında teşhisi zorlaştırdı. ApiError mesajı artık
toast açıklamasında; 429 için Türkçe "çok fazla deneme" metni; PostHog
reason artık http_<status> taşıyor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dashboard şase kutusunun sağına kamera butonu: kullanıcı fotoğraf yükler
(mobilde kamera), görsel istemcide ~1800px'e küçültülüp JPEG'e çevrilir,
POST /vehicles/decode/ocr OpenRouter üzerinden vision modeline gönderir
(varsayılan google/gemini-2.5-flash; bulamazsa openai/gpt-5.4-nano yedeği),
dönen VIN kutuya yazılır. Decode HER ZAMAN kullanıcının "Şase Çöz"
tıklamasıyla başlar — OCR yalnızca yazma zahmetini alır.
Model seçimi 2026-07-09 canlı bake-off'una dayanır (Commons'tan gerçek VIN
fotoğrafları): flash plaka/etiketleri doğru okudu; flash-lite rakam bozdu,
Haiku 4.5 olmayan VIN uydurdu, OCR.space damgalı şasede 8→S hatası verdi.
Sunucu tarafı katı doğrulama (17 karakter, I→1 O→0 Q→9, aksan ayrıştırma)
"regex'ten geçen ama yanlış" VIN'leri keser; okunaksızsa vin:null döner.
- throttle: kullanıcı başına 10 istek/dk; görsel sınırı 5MB (mevcut multipart
middleware'iyle uyumlu), sadece JPEG/PNG/WebP
- compose: OPENROUTER_API_KEY api VE worker bloklarına eklendi (worker çeviri
işçisi bu anahtarı bekliyordu ama compose'a hiç enjekte edilmemişti),
VIN_OCR_MODEL / VIN_OCR_FALLBACK_MODEL api bloğuna eklendi
- analitik: vin_ocr_used eventi (success/reason/confidence/duration_ms),
query_source="ocr"
- i18n: search.ocr.* (tr+en)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Widget artık /categories varsayılan kanonik cevabını render eder (view=raw
kaldırıldı): kaynak-bağımsız kova → alt-grup → yaprak kategori → parça gezinme.
Frame-tabanlı breadcrumb (her seviye kendi render'ını taşır), ortak gridEl/grid
çizici, leafItem drill'i /categories/:id ile parça+şema. Dashboard'daki
'Birleşik' varsayılanıyla aynı deneyim.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/v1/vehicles/:id/categories artık varsayılan kanonik kova görünümü döndüğü
için, ham ağaç şeklini render eden widget shell'i açıkça view=raw ister —
yoksa {canonical:{name},categories[]} şeklini {name,partCount,id} sanıp
kırılırdı. Kanonik-kova görünümünü widget'a getirmek ayrı iş.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Faz 2 — decode edilen yeni araçların kategorilerini materyalize tutar:
- Processor'a incremental mod: step-2 yalnız HENÜZ map satırı olmayan yeni
isimleri sınıflandırır (tüm ~113k değil). Materialize (step-5) differential +
inheritance NULL-only olduğundan, adı zaten map'li yeni kategori de id'sini
alır — hiçbir şey atlanmaz. is_manual korunur. reset varsa yok sayılır
(reset tam yeniden-eşleme demek).
- Gecelik cron: 05:00 İstanbul, { incremental: true }. Dev+prod'da güvenli
(harici yan-etki yok, kendi DB'sine yazar). Lexicon iyileştirmesi sonrası
tam yeniden-eşleme hâlâ ELLE { reset:true } global run.
- Dokümante edilen 'backfill trigger yok' boşluğunu kapatır; serving zaten
anlık lexicon fallback yapıyordu, bu cron materyalizasyonu güncel tutar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Faz 1 — public API kategori ucunu kanonik taksonomiye bağlar:
- GET /v1/vehicles/:id/categories artık VARSAYILAN olarak birleşik (kanonik)
görünüm döner (kaynak-bağımsız 20 kova + alt-kategoriler,
getCanonicalTree; materyalize %96,9 L1 + anlık lexicon fallback).
- ?view=raw ile ham kaynak ağacı (kaçış kapısı — plaka-kodu yapraklı yeni
PSA/Renault kataloglarında birleşik zayıfsa).
- GÜVENLİK: iki görünüm de public-api.sanitize ile temizlenir — 'source'
(upstream sağlayıcı: parts-catalogs/emex/pl24) ve 'linkWid' (upstream widget
id'si) ARTIK SIZMAZ. Önceki ham-ağaç ucu bunları sızdırıyordu (Faz 1 widget
işinde açılmış mevcut sızıntı, burada kapatıldı).
- Swagger: view param + iki görünümün şeması dokümante.
- 6 birim test (sanitizer beyaz-liste + source-drop).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Önceki hibrit sadece derin parça-yapraklarını + kök/dal girişlerini gösteriyordu;
kısmen drill edilmiş kovada ("Fren sistemi" subtree'de 39 parça) sistem-kökü
skip ediliyor + 0-parçalı kardeşleri düşüyordu → Fren'de 14 yerine 2. Yeni model
(buildCanonicalInput): her kovanın "entry container" köklerinin AYNI-KOVA
çocuklarını SUBTREE parça sayısıyla göster (drilled olsun olmasın). Audi Fren →
11 alt-kategori (Disk frenler/Fren Kaliyeri/El fren…), orijinal katalog gibi.
On-the-fly bucketing (materialize edilmemiş taze kategoriler için).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(1) Serving: "Fren sistemi" gibi sistem-container dalını çocuklarıyla değiştir
(Fren Kaliyeri/Disk frenler/Balata…) → kova doğrudan alt-parçaları içerir.
(2) Frontend: CanonicalView artık subgroups'u (2. seviye: Balata/Disk/Kaliper)
render ediyor — önce sadece flat categories gösteriyordu, alt-seviye işi hiç
görünmüyordu. Böylece Fren'e tıkla → alt-kategoriler doğrudan gelir.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Debriyaj sistemi / İç Aydınlatma Sistemi drill-edilmemiş YAPRAK (0 çocuk) —
önceki "çocuğu olmalı" şartı bunları dışlıyordu → hâlâ 0. Şart: mapped + kök-
değil + 0-parça + entry-point (parent farklı kova). Yaprak/dal fark etmez.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sorun: aracın "Debriyaj sistemi"/"Aydınlatma Sistemi" kategorisi var + doğru
kovaya map olmuş ama parçası drill edilmemiş → hibrit sadece parça-yaprak +
drill-edilmemiş KÖK gösterdiğinden bu DALLAR düşüyordu → "0 kategori". Fix:
bucket'a map olan, çocuğu olan ama subtree'de parçası olmayan dal-girişlerini de
input'a ekle (parent farklı kova = entry point). Böylece kova boş görünmez,
tıklanınca on-demand drill olur (prefetch beklemez).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sorun: yeni decode edilen araç prefetch'i normal öncelikle (5dk delay, fast yok)
enqueue ediliyordu → 82k'lık deep-drill backlog'unun ARKASINA düşüp hiç
işlenmiyordu → taze araçlarda kanonik view boş kalıyordu. Fix: fast:true + lifo
→ taze araç saniyeler içinde drill olur, eski children backlog'unu atlar.
Per-source rate gate upstream'i hâlâ korur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
canonical_structure_template(source, brand, bucket, sub, vehicle_count): drill
edilmiş araçlardan her (kaynak, marka) için görülen kova/alt-yaprak kümesi.
Backfill sonunda yeniden üretilir (global run). Serving overlayTemplate ile
aracın ağacına markanın bilinen ama bu araçta henüz drill edilmemiş kova/alt-
yapraklarını BOŞ ekler → VIN decode olur olmaz FULL kanonik yapı görünür;
parçalar backfill ile dolar. migration 0029. vehicle + catalog akışı.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sorun: bir araçta sadece TEK bir kategori (drill edilmiş olan) görünüyordu —
"herhangi parça varsa leaf-mode" mantığı drill-edilmemiş kökleri gizliyordu.
Fix: parça-tutan yapraklar + drill-edilmemiş kökler (subtree'sinde parça yok)
birlikte gösterilir. Kök yapı decode'da oluştuğundan tüm 20 kova ANINDA görünür;
parçalar backfill deep-drill ile dolar. vehicle + catalog akışı.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(1) Araç sayfası varsayılanı "Orijinal Katalog" (source) — canonical view'ın
edge-case'leri (duplike/drill-edilmemiş kökler) çözülene kadar.
(2) canonical grouping ad-bazlı dedupe: bazı decode'lar kökü iki kez üretiyor
("Motor, Motor"; Seat Ateca'da her kök 2×) → aynı adı en zengin kopyayla
(parça, sonra çocuk) birleştir. Önceki boş-kök filtresi GERİ ALINDI (drill-
edilmemiş kökleri de siliyordu = "2 kategori" regresyonu).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
emex bazen aynı kökü iki kez decode ediyor (biri boş) → "Motor → Motor, Motor"
gibi redundant başlık. Root-fallback artık sadece çocuğu VEYA parçası olan
kökleri gösteriyor. vehicle + catalog akışlarının ikisinde de.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>