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>
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>
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>
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>
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>
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>
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>