dev #112

Merged
root merged 7 commits from dev into main 2026-06-09 11:03:28 +03:00

7 Commits

Author SHA1 Message Date
7bdbe3541b feat(flags): decouple analytics capture from flag evaluation (POSTHOG_CAPTURE_ENABLED)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
So a non-prod env can hold the (public) project key and evaluate flags - kill
switches, rollout - WITHOUT shipping analytics events to the shared prod
PostHog project. The posthog-node client (which powers both capture and local
flag eval) now initializes whenever the project key is present; capture() is
gated on POSTHOG_CAPTURE_ENABLED (default on; set "false" on dev). Wired into
the api + worker compose blocks.

This lets dev.sase.tr exercise the kill switches end-to-end while keeping the
prod analytics project clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 01:17:00 +03:00
ed45021d71 feat(flags): server-side feature flags + upstream kill switches + live backfill config
Add server-side PostHog feature-flag evaluation to the API and wire three
high-leverage uses. The flags live in PostHog (project 127747), dormant and
fail-open, so this commit is a no-op until POSTHOG_PERSONAL_API_KEY is set and
a switch is flipped.

Phase 1 - upstream kill switches:
  PostHogService gains local flag evaluation (onlyEvaluateLocally +
  sendFeatureFlagEvents:false -> zero decode latency, no VIN leak) and
  isSourceLive(). Guards at each source's decode entry (parts-catalogs, emex,
  pl24 whole-source + per-brand via LEGACY_ARCH_SOURCE_TAG) let a flailing
  upstream be disabled from the PostHog UI in ~5s instead of a
  code-fix -> dev -> prod redeploy. Fail-open: any unresolved flag keeps the
  source live, so a PostHog outage can never black out decoding.

Phase 2 - guarded rollout primitive:
  isEnabled()/variant() with VIN bucketing, ready to ramp a new decode/parser
  path 0->100% behind a decode-*-v2 flag (recipe in feature-flags-strategy.md).

Phase 4 - remote-config ops tuning:
  prefetch-worker reads cfg-backfill-tuning to retune backfill
  batchSize/maxBacklog/businessHoursOnly live; malformed/missing -> the
  compiled-in constants.

POSTHOG_PERSONAL_API_KEY wired into the api + worker compose blocks (empty ->
flags inert, no added latency). Tests updated for the new constructor params.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 00:21:52 +03:00
6daf59fe8a fix(catalog): make columns view height dynamic — drop hardcoded 320/420 caps
Both the catalog flow ($brandName/$modelId) and the VIN-decode flow
(vehicles/$id) render the same CategoryColumns. It was stuck inside a fixed
visual window: container min-height 320px, each column max-height 420px. With
20+ root categories the user saw an internal scrollbar inside an otherwise
short rectangle even though there was plenty of page room.

Make it flex naturally:
- Outer container: min-h-[24rem] keeps a solid floor for empty/cold state,
  max-h-[calc(100dvh-220px)] keeps a 200-leaf catalog from running off the
  bottom of the page. Within that band, height tracks the tallest column's
  natural content (flex stretch — siblings share the height).
- Inner column panels: drop the 420px max-height. Each column flexes to the
  shared height; overflow-y-auto only kicks in when the outer viewport cap
  compresses the row.

Net result: small catalogs render a short, tight columns view; large
catalogs grow to fill the available height up to the viewport cap, then
the columns scroll individually. Same component fixes /catalog/.../$modelId,
/vehicles/$id, and the nested /categories/$categoryId drill.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 10:03:09 +03:00
88aa9271a3 fix(catalog): auto-advance variant selectors instead of forcing an extra "Kataloga Git" click
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>
2026-06-06 03:39:13 +03:00
9ccf0863f0 fix(catalog/ford): disambiguate identical catCode buttons + relabel "Model Yılı" → "Varyant"
Two visible defects when a user picks a Ford model (variant selector step):

1. **N identical buttons.** Ford's `modelFamilyToModelList` gives every sub-catCode
   the same caption — just the family name. parseFordCatCodesForFamily returned
   `[{code:CBV,name:Kuga},{code:CBS,name:Kuga},{code:CTD,name:Kuga}]` for Kuga
   2012-2020. The UI rendered three indistinguishable "Kuga" pills with no way
   for the user to tell them apart. Same on Galaxy ("Galaxy"/"Galaxy"), every
   multi-catCode family.

   Fix: detect duplicate baseNames per family and graft a disambiguator —
   year first (friendliest), catCode as a last resort. Result:
   "Kuga (2013-2016)" / "Kuga (2016-2020)" / "Kuga (CTD)" — or, when year is
   present even for unique entries, always include it for a uniform look.

2. **Section title was lying.** UI label was `catalog.fordVariant.modelYear`
   = "Model Yılı" ("Model Year") but for Ford LEGACY these are catCodes
   (generation identifiers), not years. Rename the key value to "Varyant" /
   "Variant" — semantically correct for both Ford catCodes and Volvo years
   (Volvo flows through the same selector and its codes ARE years; "Varyant"
   covers both).

Cleared the affected Redis cache (`pl24:ford:config:*`) on dev so the next
hit fetches fresh disambiguated data; prod cache will roll over on its own
TTL after promote.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 03:12:20 +03:00
37aa4dfd02 fix(catalog): kill "Modeller" empty-card on vehicle page + unify drill headers
The PL24 vehicle page rendered a Card titled `t("catalog.models")` ("Modeller")
whose content was the *selected vehicle's* specs (motor/kasa/vites/pazar). When
all four spec fields were null — common for sparse decodes — the user saw an
empty card with a bogus "Modeller" heading just below the model name.

Replace the misleading card with a flat spec-chip row (icon + label : value),
hidden entirely when no specs exist. Use the shared CatalogHeader so brand →
vehicle has a proper breadcrumb back to "/catalog/$brandName" and uses the same
back-button affordance as the rest of the catalog surface. Use the shared
ViewModeToggle so view-mode buttons stop having hardcoded TR title attrs
("Izgara", "Agac", "Sutun") and inherit the i18n + a11y from the component.

Same cleanup for pcat/$catalogId_/$modelId and emex/$catalogCode_/$vehicleId:
both rolled their own header — both now use CatalogHeader with full crumbs and
i18n'd search/count/empty-state strings (catalog.pcat.*, catalog.emex.*).

Why now: catalog flow audit caught the empty-card bug ("model seçilince
yukarıda boş bir alan kalıyor"), plus drift between drill levels (some pages
used CatalogHeader, vehicle/pcat/emex did not). One unified pattern across
brands → models → vehicle → categories.

i18n: +catalog.categoryCount, +catalog.vehicleSpecs.{engine,body,transmission,
market}, +catalog.pcat.{searchPlaceholder,vehicleCount,schemaCount},
+catalog.emex.{searchPlaceholder,optionCount,variantCount,noResults,
noPartsTitle,noPartsHint,loadError}.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 03:01:09 +03:00
2040686e4d fix(catalog): Hyundai/Kia/Nissan schema images (image-board ticket URL)
These three brands rendered parts with NO schema illustration (and no hotspots):
schema_pics=0 vs 56/29/19 parts on dev. Root cause: extractPsaImageTicketUrl only
read imageViewerParamsUrl from the id="jsinitparams" data-params attribute (where
Ford/PSA/Opel/Volvo put it). Hyundai/Kia/Nissan's jsinitparams carries only a
localization dict ({"commonTxt":{"ok":"TAMAM"}}); their imageViewerParamsUrl lives
in a separate <script> JSON blob → extractor returned null → image pipeline skipped.

Fix: when the jsinitparams-attribute parse yields no URL, fall back to a whole-HTML
scan for "imageViewerParamsUrl":"…" (decoding &). Brand-agnostic and additive;
Ford/PSA/Opel/Volvo unchanged (attribute path still wins). Proven live via the de
client: Hyundai 600x820/3 hotspots, Kia 600x820/11, Nissan 1024x560/13 — all download OK.

Also: drop a pre-existing noParameterAssign in fetchP4Page (derive `account` const
from `accountParam` instead of reassigning the param). +3 extractor unit tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:19:19 +03:00