Rpartstore currently throws an immediate hard launch-error modal ("Ошибка
запуска каталога" / title "Renault Rpartstore") within ~8-10s of every open —
a server-side/entitlement failure that reopening never fixes. The old code
misclassified it: the error dialog's title matched rpartstoreOpen so
ensureRpartstore returned true, but it lacked the rpartstoreLoaded content
markers, so acquireLoadedRpartstore judged it a born-stuck spinner and burned
all 3 reopens (~54s) before falling back to Dialogys — which then ran out of
the 150s decode budget → not_found. Dialogys itself decodes correctly (~25s).
Fix (detect-and-bail on the FIRST open, no wasted reopens):
- constants: add VINPIN_COORDS.rpartstoreLaunchErrorOk (868,530) + the
VINPIN_OCR.rpartstoreLaunchError pattern (matches the real Cyrillic AND its
stable eng-OCR transliteration "Owwnbka 3anycka KaTanora").
- acquireLoadedRpartstore step 2: poll now stops on loaded OR launch-error and
classifies via the returned OCR text; a launch error dismisses the modal and
returns false immediately → straight to Dialogys (reopen loop untouched for
genuine spinners).
- ensureRpartstore: submenu-open poll also stops fast on the launch error
instead of dead-waiting afterRenaultCatalogOpen.
- bump VINPIN_DECODE_BUDGET_MS 150s→180s (cheap safety margin).
- tests: launch-error → false after ONE open (no reopens, dismiss clicked);
genuine spinner still reopens; OCR-pattern matches Cyrillic + transliteration.
Never-throw contract, sessionPoisoned, budget teardown, cold/Dialogys
fallbacks, the fcc0298 spinner-guard, the 61b5769 establish/teardown fix and
the Fiat ePER path are all intact. Skipped the warm-path Dialogys field-clear
tweak — runDialogysSearch is shared with the cold path and switching its clear
step would change cold behavior.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause of warm-establish failures was resume-into-dirty-session +
broken window-close + no backoff, not OCR/detection. Four composing fixes:
A. Clean teardown (cleanTeardown): before the browser close(), close each
open catalog window via the corrected tab-✕ then click "Çıkış yap" logout
to END the RDS session, so the next warm-up starts from a fresh login/grid
instead of resuming into the last-open 3-window desktop. Wired into
teardownWarm() and both failed-warmUp exits. Bounded + never-throw.
B. Fix close coords + tab-✕ primary. catalogTabClose {135,45}→{93,45}
(validated live). In ensureBrandGrid/returnToBrandGrid the tab-✕ is now the
PRIMARY close; the windowClose {1298,14} click (which opens the HTML-Access
language dropdown) is no longer used there. Escape pressed after each close
to dismiss an accidental dropdown before re-OCR. After N failed closes,
ensureBrandGrid escalates to logout+relogin (one-shot, no recursion) instead
of limping into the ePER-open loop.
C. Realistic grid wait. afterLogin 20_000→45_000 (real grid render ~32-46s).
D. Backoff between re-warm attempts. A failed warmUp sets a cooldown (60s,
exponential to 5min) that BOTH reconcile() and decode()'s warm-on-demand
honour; a successful warm resets it — so a failing seat is no longer
hammered every ~60s leaving fresh dirty windows.
Safety nets preserved: never-throw contract, VINPIN_DECODE_BUDGET_MS,
sessionPoisoned, cold/Dialogys fallbacks; fcc0298 Rpartstore spinner-guard,
Fiat ePER path, and Russian-dialog dismissal (746,454) untouched. Cannot be
exercised in dev (single seat on prod) — needs prod validation on a rested seat.
Tests: +8 unit tests (clean-teardown ordering, tab-✕ primary + relogin
escalation, warm-up backoff respected by reconcile + warm-on-demand + reset).
81 vinpin tests green; tsc + biome clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- inject signed unsubscribeUrl into every optional-workflow Novu payload
(templates render it via {{#if unsubscribeUrl}} footer)
- add 'conversion' campaign workflow to OPTIONAL_WORKFLOWS + email_marketing
category so its one-click tokens validate and opt-outs suppress it
- declare UNSUBSCRIBE_SECRET/URL_BASE/EMAIL in env schema (""→undefined
preprocess against the url().optional() boot-crash trap), .env.example and
both compose env blocks
- fix confirmation-page settings link (?tab=notifications)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Kullanıcı düzeltmesi: kokpit tur ham araç-marka etiketlerini "yansanayi"
sayıyordu — yanlış. FORD/GM/BMW/Mercedes/Volkswagen/Renault… OE etiketidir.
Orijinal tespiti artık kokpit tur='orjinal' setine değil, OE aile üyeliğine
dayanır: OE_SUPPLY_LABELS OE_FAMILIES'ten TÜRETİLİR (+ jenerik ORJINAL), böylece
OE-tespiti ile aile-filtresi drift etmez ve tüm araç markaları orijinal sayılır.
Gerçek yan sanayi PARÇA markaları (Bosch/Febi/Valeo/TRW) hiçbir ailede yok →
orijinal değil. GM Opel ailesine eklendi (TR'de Opel OE'si). Redis v3→v4 (eski
"miss" değerleri bayat). kokpit'e DOKUNULMADI.
Ölçülen etki (dev): grafikli kod %0,77→%0,84; artış sase kataloğu ile takip
beslemesinin bu markalardaki kod kesişimiyle sınırlı (ör. takip'te 4.859 Ford-
stokta koddan 335'i sase kataloğunda).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A freshly-opened Rpartstore instance sometimes gets "born stuck" on an
infinite spinner (survives raise/maximize). The only reliable fix, proven
in a live spike, is to CLOSE the stuck instance and reopen a FRESH one.
Replace the old "raise Rpartstore → if not focusable reopen → else fall to
Dialogys" with a bounded load-verify-else-reopen loop applied to BOTH the
warm-daemon Renault path (warmRenaultDecode) and the cold per-decode path
(runRenaultFlow):
- acquireLoadedRpartstore(): bring a Rpartstore window forward, OCR-verify it
actually rendered its search-home landing markers (new rpartstoreLoaded set —
content tokens the spinner lacks), and if still spinning close the tab and
reopen a fresh instance. Retries up to VINPIN_RPARTSTORE.maxOpens (3) times.
- Bounded inside the decode wall-clock budget AND a tighter acquireBudgetMs
(90s) sub-cap, so a permanently-stuck Rpartstore still leaves headroom to
fall back to Dialogys — never a livelock.
- Rpartstore stays PRIMARY (richer Turkish catalog); Dialogys only as a
last resort once reopen attempts are exhausted or it genuinely misses.
- Fiat ePER path unchanged; sessionPoisoned / never-throw contract preserved.
Adds unit tests for reuse / spinner→reopen→loaded / exhausted→false /
budget-bail / Rpartstore-primary-on-hit / exhausted→Dialogys-fallback.
Could not live-test (single Vinpin seat is held on prod) — needs prod
validation after promote.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Aynı OEM koduna farklı OE dağıtıcıları düşebiliyor (Stellantis: PSA ₺5.531 vs
OPAR ₺3.459) → OE-only havuz bile iki dağıtıcıyı medyanlayıp hayalet fiyat
veriyordu. Artık kart, kodun kendi araç markasına göre tek OE ailesine süzer.
- part-prices.logic: marka→OE-dağıtıcı-ailesi haritası (OE_FAMILIES: PSA=
Peugeot/Citroen/DS, FIAT=Fiat/OPAR/Tofaş, RENAULT=Renault/Dacia/MAIS, ...).
filterOffersForBrand: araç markası → yalnız o ailenin orijinal teklifleri
(jenerik ORJINAL aile-belirsiz olduğu için elenir); yan sanayi markası çipi
→ yalnız etiket-uyumlular; markasız → tüm OE aileleri (taban).
- Web: OEM kartı kodun araç markasını oem-vehicles'tan (en çok geçen brandName)
türetip geçirir; başlık "Orijinal (OE) fiyat analizi · <Marka>". Kart marka
çözümü için oem-vehicles'ı bekler.
- vehiclesByOem NORMALIZE eşleşmeye geçti + parts_oem_code_norm_idx functional
index (migration 0033): PSA kodları boşluklu saklandığından ("9827 622 780")
exact match onları kaçırıyordu — marka bu yüzden çözülemiyordu. Reverse
"kataloğunuzda" bölümü de artık reformatlı kodları buluyor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the per-decode "launch browser + login + open catalog" model with a
persistent warm Vinpin seat that eliminates cold-start, brand-switch cost and the
seat livelock. Additive + fail-safe: every warm operation degrades to the proven
cold per-decode path, so behaviour never regresses.
Warm daemon (VinpinDaemonService, worker-process singleton):
- Scheduler warms the seat at 08:00 and tears it down at 21:00 Europe/Istanbul
(proper TZ via Intl, no hardcoded offset); warms on worker start if inside hours;
reconcile() every 60s with a reentrancy guard.
- Keepalive nudges the RDS session (mouse.move) every ~75s while warm+idle; it
SKIPS during any active seat op (busy flag) and never takes a lock that blocks a
decode.
- decode(vin): inside hours ensure warm (warm-on-demand once) then delegate to the
driver; off-hours delegate straight to the cold path. Never throws.
Driver warm path (VinpinDriverService):
- warmUp() launches+logs in ONCE and opens Fiat ePER + Renault Rpartstore + Renault
Dialogys windows without closing each other, then OCR-binds each taskbar button
(order read via OCR, not hardcoded; raise self-heals by probing slots + OCR
verify). isWarm()/teardownWarm()/keepalivePing() added.
- warmDecode(): taskbar-raise the brand window (Fiat→ePER, Renault→Rpartstore w/
Dialogys fallback), run the EXISTING in-catalog decode on the warm window, OCR
the modal, parse, then Escape to ready the field for the next VIN. Runs under the
wall-clock budget; a hang still aborts.
- Health-recovery: a dropped seat (Disconnected/no-free-sessions OCR marker) →
teardown + re-warm ONCE, then retry the decode once.
- Refactored runDialogys into openDialogysSubmenu + runDialogysSearch so the warm
path searches without a window-closing reopen; cold Dialogys flow unchanged.
Safety nets retained: VINPIN_DECODE_BUDGET_MS + sessionPoisoned breaker (warm
budget abort → teardown+poison+null), single-seat serialization (runExclusive),
decode() never throws. Gated by VINPIN_ENABLED; VINPIN_WARM_DAEMON=false forces the
legacy cold path (kill-switch). Widened VINPIN_MODAL_REGION to ~900px.
Wiring: processor calls getVinpinDaemon().decode(); worker starts the daemon on
boot and stops it (releasing the seat) on shutdown. BullMQ concurrency 1 +
attempts:1 unchanged.
Tests: business-hours warm/teardown scheduling (injected clock/TZ), brand→taskbar
routing, taskbar OCR-order binding, keepalive-skips-during-decode, and
session-drop→re-warm→retry recovery. All existing vinpin/queue tests stay green.
NOTE: un-dev-testable (prod holds the single seat) — pixel/taskbar coords are
OCR-verified + marked TUNE and need live prod validation; warm falls back to cold
until confirmed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A bad/unresolvable VIN could leave a catalog window open on the shared Vinpin
seat; the next decode's ensureBrandGrid found it "resumed open" and looped
(close→brand-grid-not-confirmed→ePER-open→browser-disconnect→relaunch) forever.
BullMQ attempts:2 + 30s backoff auto-re-fed every failure straight back into the
stuck seat, starving all real decodes for minutes.
- queue: attempts:1, drop the 30s exponential backoff (extract VINPIN_DECODE_JOB_
OPTIONS). The driver already runs its own bounded internal retries; a BullMQ
retry on top is what compounded the livelock. Null decode still persists as
not_found; a hard infra throw stays user-retriable (failed).
- driver: hard per-decode wall-clock budget (VINPIN_DECODE_BUDGET_MS, 150s) via
withDeadline() racing each attempt; on abort → close() + poison seat + return
null (no retry into the stuck state).
- driver: sessionPoisoned flag — set at nav-loop exhaustion (ensureBrandGrid /
establishSession), budget abort, and failed post-decode cleanup; the NEXT
decode forces a full cold re-establish instead of reconnecting to the resumed
desktop. Cleared on any confirmed-clean grid/catalogue.
- driver: finally-cleanup after every decode — on failure/not-found return the
seat to a clean brand grid; if that can't reach the grid, poison + tear down.
- driver: basic VIN sanity (17 alphanumerics) before touching the seat.
Healthy Fiat/Renault happy paths are byte-identical when nothing is stuck.
Cannot be live-tested (seat is on prod) — needs prod validation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
(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>
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>