Commit Graph

782 Commits

Author SHA1 Message Date
97c1dbcece Merge pull request 'fix(pcat): robust year extraction across key spelling + date format' (#138) from dev into main
Reviewed-on: #138
2026-06-12 13:11:22 +00:00
d5362a5390 fix(pcat): robust year extraction across key spelling + date format
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
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>
2026-06-12 16:09:14 +03:00
7571438a6b Merge pull request 'dev' (#137) from dev into main 2026-06-12 13:08:00 +00:00
e57a9968b2 fix(pcat): extract real Year param, not the year=All filter axis
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
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>
2026-06-12 16:07:11 +03:00
6c6b74a8e5 feat(payments): recurring billing — Stripe Checkout switches to subscription mode
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
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>
2026-06-12 15:55:44 +03:00
4879f7cdef fix(subscriptions): keep trial alive until payment actually lands
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>
2026-06-12 15:15:47 +03:00
f394897a08 fix(part-prices): fiyat kimliği marka+kod oldu — kısa kodlarda marka karışması giderildi
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>
2026-06-12 14:55:44 +03:00
51d8104cdc polish(part-prices): dialog'da çift başlık kaldırıldı, 'son güncelleme' → 'son fiyat değişimi'
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:45:58 +03:00
500738fcd4 chore(deploy): SUPPLIER_PRICE_DB env'leri sonrası dev redeploy tetikleyici
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:41:05 +03:00
532e1ad9ef feat(part-prices): parça kodu bazlı tedarikçi fiyat geçmişi + OEM sayfasında p50/p95/p99 grafiği
- pg: part_price_tracks + part_price_daily (0018) — (kod, kaynak, gün) başına
  stoktaki tekliflerin p50/p95/p99 + teklif sayısı; source='supplier' şimdilik,
  perakende ileride aynı tabloya 'retail' olarak girer. Tedarikçi kimliği yok.
- API: GET /part-prices/series (ilk istekte takip history'sinden lazy-backfill,
  sonrası salt-pg + Redis) ve POST /part-prices/current-batch (sayfadaki kodlar
  için canlı güncel istatistik). P-servisi sözleşmesi: asla throw yok, fail-open.
- Worker: part-price-refresh cron'u 19:30 Europe/Istanbul (takip sync'i 19:05'te
  bitiyor) — izlenen kodlara bugünün satırını upsert eder, sku_map'i artımlı
  bakar, Redis cache düşürür. SUPPLIER_PRICE_DB_* yoksa sessiz no-op.
- Kaynak köprüsü: takip.sku_map (code_norm → product_id; tam sku / ilk-boşluk /
  ilk-tire sonrası normalize adayları) vmi MySQL'inde kurulu; 6,8M satır.
- Web: OEM detayında "Tedarikçi fiyat analizi" kartı (güncel medyan + P95/P99 +
  teklif sayısı + 30g delta, 30G/90G/Tümü aralıklı step grafik, recharts) ve
  article/muadil/OE satırlarında fiyat çipi → dialog'da tam geçmiş.
- Fix(p): td snapshot'ında gerçek üretici kodu articles.name'de (article_number
  %96 upstream sayısal ID) — sayfa artık kopyalanabilir gerçek kodu gösteriyor.
- compose: SUPPLIER_PRICE_DB_ENABLED/URL api+worker bloklarına eklendi.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:33:37 +03:00
6a54e63c0f Merge pull request 'fix(backfill): in-process scan timer (cron marker buried behind 444k backlog)' (#136) from fix/backfill-inprocess-scan into main 2026-06-12 09:14:25 +00:00
7d10107f4b fix(backfill): run hourly scan on an in-process timer (cron marker gets buried)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
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>
2026-06-12 12:14:09 +03:00
a304420456 Merge pull request 'promote: sezon kazananları admin'e maillenir (otomatik üyelik uzatma kaldırıldı)' (#135) from dev into main 2026-06-12 08:57:03 +00:00
2c848df97d change(gamification): sezon ödülleri otomatik tanımlanmaz — kazananlar admin'e maillenir
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
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>
2026-06-12 11:53:02 +03:00
33e59c9554 Merge pull request 'fix(backfill): nest-build spec typing (unblocks #133 deploy)' (#134) from fix/backfill-lifo-fast-lane into main 2026-06-12 08:47:25 +00:00
a7678acbe1 fix(backfill): type spec queue mock so production nest build compiles it
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
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>
2026-06-12 11:45:28 +03:00
cbdff3311f Merge pull request 'perf(backfill): fast-lane Phase-1 via lifo + run scan despite deep backlog' (#133) from fix/backfill-lifo-fast-lane into main
Reviewed-on: #133
2026-06-12 08:39:41 +00:00
137d0c07a6 Merge pull request 'promote: Parça Uzmanları programı (flag-gated) + revert düzeltmesi' (#132) from promote-expert-program into main 2026-06-12 08:25:35 +00:00
8c1d247c45 Merge remote-tracking branch 'origin/dev'
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
2026-06-12 11:16:18 +03:00
579ee8d275 tweak(gamification): uzman programı kriteri 2/2 → 5/5
En az 5 FARKLI başarılı VIN decode VE en az 5 FARKLI OEM kodu kopyası.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 11:16:01 +03:00
4df06ab3a1 Merge remote-tracking branch 'origin/dev' 2026-06-12 11:05:41 +03:00
58a0faeb99 Reapply "Merge pull request 'dev' (#130) from dev into main"
This reverts commit 5c7803e12a.
2026-06-12 11:05:41 +03:00
6c414f9a59 feat(gamification): Parça Uzmanları programı feature-flag + kullanım kriteri kapısı
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>
2026-06-12 11:04:20 +03:00
fe35bbd826 feat(gamification): aylık uzman sezonları — ilk 3'e otomatik üyelik uzatması
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>
2026-06-12 10:48:36 +03:00
55894bec0e polish(web): oy butonlarından sayaçları kaldır
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>
2026-06-12 09:34:44 +03:00
4068091a21 Merge pull request 'Revert PR #130 (erken prod merge geri alındı — dev'de kalacak, sonra promote)' (#131) from revert-pr130 into main 2026-06-11 21:50:14 +00:00
5c7803e12a Revert "Merge pull request 'dev' (#130) from dev into main"
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
This reverts commit eccecf73e6, reversing
changes made to 4bcac424e0.
2026-06-12 00:48:58 +03:00
24e668ea01 polish(web): oy kartı header'a — OEM kodunun sağ simetriğine, araç+parça bağlamıyla
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>
2026-06-12 00:47:06 +03:00
eccecf73e6 Merge pull request 'dev' (#130) from dev into main
Reviewed-on: #130
2026-06-11 21:45:35 +00:00
6575c611e8 polish(web): puan kuralları yalnız Uzmanlar sayfası altında, oyunlaştırılmış dille
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Ü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>
2026-06-12 00:33:37 +03:00
4072c6e736 feat(catalog): OEM oyları detay sayfasına taşındı + topluluk muadil önerileri
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>
2026-06-12 00:26:20 +03:00
0de6bd3faa perf(backfill): fast-lane Phase-1 via lifo + run scan despite deep backlog
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
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>
2026-06-12 00:18:37 +03:00
d922fb7a02 test(catalog): cover P4 VIN group-node gate (children + loadError)
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>
2026-06-12 00:02:50 +03:00
8fddd09087 feat(catalog): OEM uyumluluk oylaması + Parça Uzmanları liderlik tablosu
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>
2026-06-12 00:00:51 +03:00
09a9487564 remove changelog page and sidebar link
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>
2026-06-11 23:51:54 +03:00
e6c8c9c32e fix(catalog): drill P4 VIN group nodes on live click (Ford/Nissan/Opel parent-gate gap)
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>
2026-06-11 23:48:04 +03:00
4bcac424e0 Merge pull request 'dev' (#126) from dev into main
Reviewed-on: #126
2026-06-11 14:34:37 +00:00
e91159003e chore(web): remove 'click to reopen' hint under recent searches
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 17:33:49 +03:00
45f7e3f5a4 feat(proxy): proxy_logs telemetry — per-attempt provider/ban/latency tracking
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
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>
2026-06-11 17:27:48 +03:00
9f47d15312 feat(web): hide Chatwoot launcher bubble on mobile
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
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>
2026-06-11 17:26:04 +03:00
3c9245bd8b feat(web): keep sidebar on blog & contact for logged-in users
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
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>
2026-06-11 17:25:14 +03:00
ffcd417939 Merge pull request 'hotfix(pcat): widen JWT capture for slow residential IPs' (#129) from hotfix-pcat-capture into main 2026-06-11 13:59:37 +00:00
75d5b11475 fix(pcat): widen JWT capture for slow residential IPs (domcontentloaded + 12s poll)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
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>
2026-06-11 16:59:34 +03:00
4a6e673af1 fix(pcat): widen JWT capture for slow residential IPs (domcontentloaded + 12s poll)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
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>
2026-06-11 16:59:17 +03:00
d82bcb560b Merge pull request 'hotfix(proxy): rotate Floxy creds (402 balance) — restore pcat' (#128) from hotfix-floxy-creds into main 2026-06-11 13:49:58 +00:00
46e4b7cfcb fix(proxy): rotate Floxy creds (prior account hit HTTP 402 / balance exhausted)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
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>
2026-06-11 16:49:56 +03:00
2e6c8ac751 fix(proxy): rotate Floxy creds (prior account hit HTTP 402 / balance exhausted)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
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>
2026-06-11 16:49:37 +03:00
2f6a0e3158 Merge pull request 'Merge dev → main: surveys, P/OEM page, trial upsell, catalog-issue report, pcat timings' (#127) from merge-dev-to-main into main 2026-06-11 13:28:15 +00:00
b6e4deb7bd Merge dev into main: catalog-issue report + pcat skip/transient timings
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Resolves the recurring promote-by-cherry-pick divergence (decode/pcat/emex live
on both branches with different SHAs — all auto-merged). Net-new to main:
- fix(decode) fd78957: mark pcat skip/transient in timings (stop false catalog-gap reads)
- feat(catalog) c972a17: one-click report on empty/error catalog states
- compose: P_DB_ENABLED/P_DB_URL env (inert until set in Coolify)

Conflict: docker-compose.coolify.yml — both sides added env at the same spot;
took dev's superset (main's Directus/Blog vars + dev's P_DB vars).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

# Conflicts:
#	docker-compose.coolify.yml
2026-06-11 16:25:57 +03:00
c972a17670 feat(catalog): one-click report on empty/error catalog states — no form
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
'Ç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>
2026-06-11 16:00:23 +03:00