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>
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>
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>
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>
'Ç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>
The two things only the browser can add on top of the backend catalog-degradation
reporting:
- Replay-on-failure: when the catalog UI renders empty-tree (decoded vehicle, no
categories) or a drill loadError, capture a browser Sentry warning and flush the
Session Replay → you can WATCH the user hit the dead-end (serkan's session,
reproducible). Per-session deduped (one replay/session covers the whole journey).
- In-app feedback: a "Çalışmadı mı? Bildir" button on the empty-parts, empty-tree
and loadError states opens the Sentry feedback dialog pre-tagged with the
vehicle/category (+ session replay) — turns a parts shop's complaint into a
structured, triageable report instead of an email.
Browser events are fingerprinted source="browser" so they form their own
"what users actually saw" issues (carrying replays) next to the server-side
detections. tsc + biome + web build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
After the user picks the last meaningful dimension, the next step is determined
— there is no decision left to make. Yet the Ford and PSA selectors stopped at
that point and waited for a "Kataloga Git" / "Proceed" button click. For Ford
specifically this was the worst case: LEGACY_FORD's upstream returns no engines
or gearboxes, so picking a year/catCode IS the whole flow — the user had to
click twice for one decision.
Auto-fire onSelect when the last available dimension is filled in:
- **Ford**: when year picked and (!hasEngines && !hasGearboxes); when engine
picked and !hasGearboxes; when gearbox picked. Also: when config returns
empty (no variants at all), fire onSelect("_nor_", "_nor_", "_nor_") via a
one-shot useEffect so the selector skips itself rather than dead-ending on
a "no config" notice with no actionable button.
- **PSA**: when gearbox is picked. Body→engine→gearbox is always 3 steps,
picking gearbox is unambiguous intent. "Show All" buttons already auto-
advanced — this just removes the asymmetry on the explicit-pick path.
Drop the now-dead Proceed button + handleProceed handler in both. P5
restriction selector already auto-advances on `isFinal` and stays untouched.
i18n keys catalog.fordVariant.proceed / catalog.psaVariant.proceed are kept
in case we ever re-introduce a confirmation button.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Fiat catalog browse returned 0 models because the generic single-endpoint P5
model-list flow can't express Fiat's two-level hierarchy and the drill defaulted
to the tr account (Fiat is licensed only on de-708171).
- fetchVehicleList: dispatch fiatp_parts/fiatt_parts to new fetchFiatVehicleList,
which expands modelOverview (34 families) → models?modelFamily=N (model codes +
year ranges) into flat catalog vehicles whose catalogPath is the maingroups
endpoint. Verified live: 34 families → 123 models, end-to-end drill to parts+image.
- fetchMainGroups / fetchP5Restrictions: account-aware (resolveAccount → de + DE
proxy for Fiat; tr unchanged for every other P5 brand) so browse maingroups no
longer hit the tr demo/empty page.
- web: case-insensitive "/maingroup" gate so Fiat (lowercase /mdl/maingroups,
already a maingroups endpoint) skips the empty restriction selector and loads
categories directly.
- formatFiatYear: "(2016,2020)" → "2016-2020"; +unit tests.
Subgroups/parts/images already resolve account→de for Fiat (unchanged).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a horizontal scrollable "Son ziyaret ettiklerin" strip above the main
brand grid. Renders only when the user has actually opened at least one
brand detail page before — silent in cold-start state.
Why localStorage, not a backend endpoint
- This is a behavioural shortcut, not authoritative state. Adding a
user_recent_brands table for data we don't have yet is premature.
- Keying by `sase-recent-brands-${userId}` mirrors the trial-banner
scoping pattern; a second user on the same browser doesn't inherit the
first user's list.
- localStorage failures (private mode, quota) silently degrade — the
strip just stays hidden, never throws.
How a brand gets added
- Tracked at the destination (`/catalog/:brandName` visit), not on the
link click. A click that never resolves into a real visit (auth gate,
slow nav cancel) shouldn't be a "recently used" signal.
- 12 entries stored, 8 surfaced. Headroom for future ranking (e.g.
weight by frequency × recency) without re-recording history.
Plan-lock awareness
- The strip cross-references the `/catalog/brands` access map, so a
brand the user opened while on Full and then lost on a downgrade
shows the same lock chip + amber upgrade route used by the main grid.
- New PostHog event: `catalog_recent_brand_clicked`. Locked recent
chips reuse `catalog_locked_brand_upgrade_clicked` with
`surface: "recents"` for funnel distinction.
i18n: `catalog.recentSection` (TR: "Son ziyaret ettiklerin" / EN:
"Recently visited").
Three drift-prone catalog surfaces (main /catalog, /catalog/$brandName, and
the legacy CategoryViewToggle) each carried their own inline view-mode
toggle with subtly different aria labels, sizing, icon paths and even a
"Sutun" typo. New surfaces will inherit a single primitive.
- ViewModeToggle: grid/tree/columns radio-group with i18n labels driven by
`catalog.view.*` keys; fan-out by passing `groupLabelKey`.
- useViewMode(key): centralises read/write of `brandViewMode`/`modelViewMode`
in user-settings — page just owns its current value.
- CatalogHeader: title + subtitle + optional breadcrumb trail + back button +
trailing actions slot, so every catalog drill page renders the same shell.
i18n: adds catalog.subtitle, catalog.view.{groupLabel,grid,list,columns},
catalog.{brandSearch,modelSearch}{Placeholder,NoMatch}, catalog.{hideLocked,
showLocked,lockedSection,lockedSectionHint,inPlanSection,goToModels,
selectBrandHint,columnsBrowseHint,sortLabel}, catalog.sort.{newest,oldest,
alphabetical}.
These primitives stand alone in this commit — the main /catalog and
brand-detail pages migrate to them in 10B and 10C.