Commit Graph

691 Commits

Author SHA1 Message Date
94f6ed5dd9 Merge pull request 'dev' (#49) from dev into main
Reviewed-on: #49
2026-05-25 12:56:12 +00:00
8fe42d729a feat(web): relabel Stripe pay button to "Kredi Kartı ile Öde"
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
The checkout button read "{amount} ile öde"; per request it now reads
"Kredi Kartı ile Öde" (EN: "Pay with Credit Card"), clarifying the payment
method. The amount stays visible in the order summary directly above the
button, so no pricing context is lost.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:53:47 +03:00
85f4252f7e feat(web): default to yearly billing, make Full Paket the popular plan
Per request: shift the "Popüler" emphasis from the monthly-anchored 2 Marka
to Full Paket, default the billing toggle to yearly, and put a "+2 ay hediye"
gift badge on the yearly tab (replacing the abstract -17%). Removed the
now-redundant "2 ay bedava" note under the toggle since the badge carries
that message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:49:56 +03:00
a631380e74 fix(web): stop mobile layout viewport expanding past device width
On mobile the dashboard opened "zoomed in" — the layout viewport reported
~548px instead of the device's 390px, so users had to pinch-zoom out to
fit. Root cause: the dashboard main column (flex flex-1 flex-col) is a flex
item with the default min-width:auto, so it refused to shrink below its
content's intrinsic width. The subscription feature-comparison table
(~514px: a w-48 label column + 4 plan columns) therefore dragged the whole
column — and the mobile layout viewport — wider than the screen, despite
the table sitting in an overflow-x-auto wrapper.

Adding min-w-0 lets the column shrink to the viewport; the wide table now
scrolls inside its own container. Verified with iPhone-class emulation
(390px): innerWidth/scrollWidth = 390 with no horizontal overflow across
/dashboard, /dashboard/search and /dashboard/subscription. Fix lives in the
shared dashboard layout, so it repairs every dashboard page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:43:33 +03:00
4ba06615a1 style(web): satisfy biome on subscription page (format + a11y)
Run biome --write formatting on the subscription page + new tests, and
swap the trial-progress banner's role="status" for aria-live="polite"
(keeps the polite live-region announcement without tripping
lint/a11y/useSemanticElements).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:31:08 +03:00
0bae337c69 feat(web): show persistent trial-progress banner for days 8-30
Trial users previously got no reminder of remaining days until the urgent
(<=7d) amber banner appeared, leaving most of the 30-day window with no
conversion nudge. Added a calm, brand-toned TrialProgressBanner shown when
more than 7 days remain: days left, a progress bar (elapsed share of the
30-day trial), and a "Planını seç" CTA that scrolls to checkout and fires
trial_progress_cta_clicked. The urgent banner still owns the final 7 days.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:29:42 +03:00
2912dc6eb2 feat(web): add credibility strip to subscription page
The page had no credibility signal at the point of conversion. Added a
CredibilityStrip under the header (shown only to converting users:
no-sub / trial / expired / pending) surfacing real, verifiable product
facts — 50+ marka, 1M+ parça, OEM + alternatif kodlar, saniyede sonuç,
KVKK uyumlu · SSL. Deliberately no fabricated customer counts or
testimonials.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:27:43 +03:00
daaf616335 feat(web): add plan decision helper + trial value hook
The brand-count plan model leaves users unsure whether they need 1/2/3
brands. Added a PlanHelperHint above the grid ("Hangi planı seçmeliyim? …
4+ marka için Full Paket marka başına daha avantajlı"). Also added an
outcome-focused hook line to the trial card ("Şase numarasını gir, doğru
parçayı saniyede bul …") above its feature list, selling the result rather
than only listing features.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:25:55 +03:00
ef4ebec025 feat(web): reinforce value on subscription plan selection
Three CRO additions at the point of decision:
- Outcome-focused page subtitle ("Doğru parçayı saniyede bul …") replacing
  the purely procedural copy.
- Per-day price anchor on every plan card (≈ ₺X/gün), reframing the cost as
  a small daily number. Computed from the displayed monthly/yearly price.
- "2 ay bedava" note under the billing toggle when yearly is selected,
  making the otherwise-abstract -17% concrete (yearly = 10× monthly).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:23:21 +03:00
02ea505af7 feat(web): add order summary to subscription payment step
At the payment step the user previously saw only the pay button with no
recap of what they were buying — a trust + friction gap right before
checkout. Added an OrderSummary block (plan, selected brands, billing
period, total) above the Stripe button, wired through the already-defined
but unused orderSummary* i18n keys. Brand names resolve from the cached
/brands query; the full plan shows "Tümü seçildi".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:19:37 +03:00
b32c777951 fix(web): render plan names + price in subscription feature matrix headers
The comparison table's column headers were empty <th/> cells (aria-label
only), so sighted users could not tell which column belonged to which plan.
Headers now show plan name + monthly price + the Popüler badge. Added a
"Marka sayısı" row exposing the brand limit (1/2/3/Tümü) — the attribute
that actually differentiates the brand1/brand2/brand3 tiers, which the
matrix previously omitted entirely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:16:57 +03:00
d459995292 Merge pull request 'feat(referrals): rework reward engine + email-verified landing' (#48) from dev into main
Reviewed-on: #48
2026-05-25 11:13:35 +00:00
3926e276fd feat(referrals): rework reward engine + email-verified landing
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Reward engine:
- Recurring milestones (every 3 → +7d, every 5 → +14d) instead of one-time
  tiers capped at 5; idempotent + transactional grants serialised per
  referrer so concurrent qualifications can't double-count.
- Rewards now gated on the referred user's email verification
  (afterEmailVerification hook); already-verified referees (OAuth) qualify
  at apply time.
- Reward days banked as users.referral_credit_days when the referrer has no
  live subscription, consumed on next trial start / activation (no more
  silently lost rewards).
- Accurate cumulative rewardDays in stats; getMyReferrals returns referee
  name/masked email/status.

Hardening / cleanup:
- onConflictDoNothing makes apply idempotent (no unhandled unique violation).
- Anti-fraud: normalizeEmail blocks self-referral via gmail dot/+tag aliases.
- Collision-safe referral code generation at signup.
- Single apply path (welcome onboarding modal); removed duplicate calls in
  register + subscription pages. Input validation on the apply code.

Email verification UX:
- Verification link now lands on a dedicated /email-verified confirmation
  page instead of the deep-linked VIN/search page.

Schema: referrals.status + qualified_at, users.referral_credit_days (0009).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 13:28:32 +03:00
52644ec51c Merge pull request 'dev' (#47) from dev into main
Reviewed-on: #47
2026-05-25 09:27:32 +00:00
125c6fc431 feat(web): show vehicle-shaped shimmer while decoding instead of a frozen button
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
When Şase Çöz is clicked and the decode (reused pre-decode or fresh) is still in
flight, the search form is replaced by a vehicle-page-shaped skeleton with a
sweeping shimmer, so the wait reads as "the page is loading" rather than an
unresponsive button. On success we navigate; candidates/errors fall back to the
form. When the pre-decode already resolved, the skeleton is near-instant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 11:57:12 +03:00
e96f311651 fix(web): reuse speculative pre-decode on Şase Çöz instead of decoding twice
The 17-char speculative pre-decode fired a POST /vehicles/decode and discarded
the result, then the button click fired a second identical decode — two
requests per VIN. Keep the pre-decode promise and have runDecode reuse it on the
first attempt (awaiting it if still in flight), so the click navigates off the
already-running decode. Retries and the sub-350ms "clicked before warm" case
fall back to a fresh decode; a failed warm retries once.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 11:45:23 +03:00
78f2f788d7 Merge pull request 'dev' (#46) from dev into main
Reviewed-on: #46
2026-05-25 01:49:04 +00:00
df85ebad83 feat(web): show retry prompt when a category fails to load from source
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
When getCategoryWithParts reports loadError (the PL24 catalog fetch failed
rather than the category being genuinely empty), the leaf view now renders a
"Katalog şu an yüklenemedi — tekrar dene" alert with a retry button instead of
the misleading "Bu kategori için parça bulunamadı" empty state. Genuine empty
leaves are unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 04:36:46 +03:00
ab5ed62f52 feat(api): signal loadError when a PL24 catalog fetch fails
A category whose PL24 drill/parts fetch errors (e.g. a broken catalog snapshot
returning HTTP 500) was indistinguishable from a genuinely empty leaf — both
came back with empty parts, so the UI showed a misleading "no parts found".

getCategoryWithParts now: (1) for a pl24 group node (link_wid marks a groups
table) with no children, drills via getChildren — returns the subgroups if PL24
serves them, otherwise flags loadError (these group nodes are never legitimately
empty); (2) sets loadError when the leaf parts fetch throws. loadError is only
true when the lists are actually empty, so genuine empty leaves stay clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 04:35:28 +03:00
77947cd162 feat(web): clean repeated tokens from model names
Catalog sources double up tokens in model names — "Laguna (Laguna)",
"Golf 1,6 GOLF", "Golf TDI Variant GOLF". Add a shared cleanModelName helper
that drops a parenthetical repeating surrounding text and exact case-insensitive
duplicate tokens (order preserved, conservative on ambiguous cases). Applied to
the vehicle page h2, the collapsed summary, and the category breadcrumb. Unit
tested.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:55:56 +03:00
0bd9165e66 feat(web): show engine code in summary for all decode sources
The collapsed summary's "Motor kodu" read only PL24's rawData.engineCode, so it
was blank for emex/pcat/vin-api. Resolve it per source: PL24 engineCode, emex
parsedOptions.engine_type, vin-api engineModel, or the pcat "engine" parameter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:48:10 +03:00
8fe731b7e0 feat(web): add emex + vin-api vehicle attribute extraction
Extend the source dispatcher to the remaining decode sources. emex surfaces its
parsedOptions codes (engine_type/gearbox_type/vehicle_type → Motor tipi /
Şanzıman tipi / Araç tipi), led by any readable DB columns (Vites, Kasa).
vin-api surfaces its flat fields (bodyClass, engineModel, transmissionStyle,
plantCountry → Kasa / Motor / Vites / Üretim ülkesi). Both replace the bare
3-field DB fallback these vehicles showed before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:47:42 +03:00
1549c14a09 feat(web): source-aware vehicle attributes — surface pcat parameters
Vehicle info only ever read PL24's vinfoBasic segment, so emex/pcat/vin-api
vehicles fell back to 3-4 sparse DB columns. Introduce a getDisplayAttributes
dispatcher keyed on the decode source. First source added: parts-catalogs,
which ships a structured pcatCar.parameters list (Year, Region, Steering,
Transmission type/code, Engine, Type, Configuration…) — sorted by sortOrder
with English names mapped to Turkish. A shared header-dedup drops the
brand/model/year rows that already live in the h2 and the collapsed summary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:47:01 +03:00
dd95641497 feat(web): surface decoded equipment/PR-code list in vehicle card
The PL24 VIN decode already ships a readable description for every PR code in
the prNr segment (187 here for the VW Golf) — surface it instead of guessing a
dictionary. The expanded accordion now shows a "Donanım kodları" section: each
row pairs the code as a mono badge with its decoded Turkish description (paint
and color entries included), in a scrollable two-column list. Raw \r\n and \-
escapes are cleaned.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:28:52 +03:00
2075631c65 feat(web): make vehicle info card a collapsible accordion
Collapse the Araç Bilgileri card by default. The always-visible header now
shows an at-a-glance summary (Model, Model yılı, Motor kodu); expanding reveals
the full attribute grid, which also gains the decoded drive type (Çekiş /
"Önden çekiş") from rawData. The loading skeleton now matches the collapsed
header so there's no layout jump when data lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:27:55 +03:00
531ab35a89 polish(web): tidy dashboard shell — emoji, dead bell, footer links, dvh
Remove the emoji from the header greeting and the duplicate avatar/name block
(the sidebar already shows the user). Remove the bell button that had no handler
or notification system behind it. Turn the footer's plain "Gizlilik Politikası,
Kullanım Koşulları" text into real /privacy and /terms links (matching the
landing footer). Switch min-h-screen to min-h-[100dvh] so the layout doesn't
jump on mobile Safari.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:13:54 +03:00
301e7e50a8 feat(web): add per-level search filter to category grid
Deep category levels are hard to scan — Elektrik has 62 children, Kaporta 58.
Add a name filter input that appears when a level has more than 8 items and
filters the current drill level live (Turkish-aware lowercasing for İ/ı), with a
clear button and a "no match" message. Resets automatically when navigating
between levels. Benefits the vehicle, catalog, and category sub-page flows since
they all share CategoryGrid.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:12:27 +03:00
136c947c31 feat(web): stop truncating category names + add keyboard focus ring
Root-level category grid cards clipped their names to one line ("Yakıt, egzoz
gaz…", "Ön aks, direksiy…"). Switch the icon card title from truncate to
line-clamp-2 so two-line names stay legible (matching the image card). Also add
a focus-visible ring to the grid category buttons so keyboard users can see
where they are — the tree view already had one, the grid did not.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:10:13 +03:00
08aa3ac88f feat(web): add error/not-found state + safe back nav to vehicle page
The vehicle detail page had no error handling: a failed /vehicles/:id (404 or
network) rendered a broken header ("  ()") with no retry, while only the
categories sub-page had a proper error block. Now a failed load shows a
not-found vs. generic-error alert with retry (mirroring the category page), and
404s skip retries so the state appears immediately. The back button now uses
the router history and falls back to /dashboard/search on deep links instead of
window.history.back() leaving the site.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:09:20 +03:00
45ac5783b5 feat(web): make vehicle info card readable instead of raw VIN codes
The Araç Bilgileri card showed only raw PL24 VIN-decoded codes (Satış tipi
1J10E4, Donanım 82, Motor kodu BCB…) and fully suppressed the friendlier DB
fields. Now it leads with the readable engine description + body type, then the
decoded codes; drops the Model/Model-yılı rows that duplicate the h2 header;
maps the known mojibake labels to clean Turkish; drops the uppercase transform
(which corrupted the Turkish dotted/dotless i); and lays out 2 cols on mobile
with long values spanning the full row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:07:38 +03:00
0bad420ab3 Merge pull request 'dev' (#45) from dev into main
Reviewed-on: #45
2026-05-24 23:40:27 +00:00
1c187847c4 perf(api): short per-call timeout + more retries for PartsCatalogs VIN decode
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
The DataImpulse proxy has dead ports that stall ~10s on connect, and undici's
ProxyAgent connect.timeout does NOT bound the proxy connection (verified: runtime
still 10s despite the Phase-2 setting). With the 30s request timeout, 2-3 dead
ports blow the 25s decode budget → false misses + 12-32s p95 (prod). /car/info
answers in <1s on a healthy proxy, so bound it at the fetch level instead: a 6s
per-call AbortSignal + 4 retries (both env-tunable: PCAT_DECODE_TIMEOUT_MS /
PCAT_DECODE_MAX_RETRIES) make a stuck port abort fast and rotate to a live one
within budget. Helps every PCAT decode, not just Renault.

Band-aid for proxy flakiness; proper fix is proxy port health-tracking (follow-up).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 02:19:18 +03:00
497f9cbc41 fix(api): stop routing Renault to PL24 (PL24 disabled Renault VIN decode)
PL24 has suspended Renault VIN identification ("...belirsiz bir süre için mevcut
olmayacağını üzülerek bildiririz") — renault_parts authorizes but every decode
throws that message. In the race that's a wasted ~1s call AND, worse, it counts
as a PL24 failure: 3 strikes opens the PL24 circuit breaker, which then skips
PL24 for ALL brands (Opel/Ford/PSA…) for 30s. Prod data: VF1 (Renault) was the
single biggest decode-failure WMI, much of it repeat hits on a poisoned cache.

Comment out the renault_parts WMIs (VF1/VF6/VNE) so PL24 isn't tried for Renault;
PCAT + EMEX cover it. Re-enable when PL24 restores Renault VIN decode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 02:05:30 +03:00
f9cbcfb620 perf(api): Redis-back PL24 account tokens (survive restarts, shared api+worker)
PL24 account JWTs were in-memory only, so every (re)deploy forced a fresh login
on the next decode, and api + worker each logged in independently — fighting over
PL24's one-session-per-account (squeezeOut). Persist account tokens to Redis
(keyed by account, TTL = JWT exp) and hydrate from Redis before logging in.
Best-effort: any Redis error falls through to a normal login, so auth never
depends on Redis. Service tokens stay in-memory (warmed by the boot pre-warm).
Complements the OnModuleInit pre-warm — now even the base login survives restarts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 01:47:36 +03:00
59e40d6ccd feat(api): identify-but-no-catalog fallback instead of a bare "unsupported"
When no catalog source can decode a VIN, the decode dead-ended with "Şase
numarası tanınamadı. Marka desteklenmiyor." even for valid, recognizable cars.
Now, before that error, run a best-effort identification (offline Corgi WMI +
NHTSA); if it yields a brand/model/year, tell the dealer what the car is and that
the request was logged ("Bu araç X olarak tanındı, ancak ... katalog henüz yok.
Talebiniz kaydedildi."). Logged distinctly (identified_no_catalog) so the coverage
backlog can be mined. Skipped on budget-abort.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 01:47:36 +03:00
518ac7c291 Merge pull request 'dev' (#44) from dev into main
Reviewed-on: #44
2026-05-24 22:31:27 +00:00
afc68fa2d7 perf(api): pre-warm PL24 auth on boot to kill the post-deploy cold-start
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
PL24 auth tokens (account JWT + per-service tokens) are cached in-memory, so a
(re)deploy clears them and the first VIN decode of each service-family pays the
~10s login + service-authorize handshake on the request path. Measured on dev: a
cold opel_parts decode took 12s vs ~2s once the auth was warm.

Add OnModuleInit to PL24AuthService that warms, in the background (fire-and-forget,
never blocking boot), both base account logins (the shared dominant cost) plus the
common legacy + top Turkish-market service tokens. allSettled throughout so a
slow/down PL24 degrades gracefully; gated on credential presence; PL24_PREWARM=false
disables. The pre-decode (search page) already hides this from users mid-session —
this closes the one remaining gap: the first dealer right after a deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 01:16:59 +03:00
c2960013f0 feat(web): speculative pre-decode on a valid VIN (faster counter lookups)
The moment the VIN reaches 17 valid chars, warm the full decode in the background
(350ms debounce, deduped per VIN, fire-and-forget) so the "Şase Çöz" click hits
the backend lock / 24h positive cache and returns near-instantly. If the warm is
still in flight when the dealer clicks, the button's decode lock-waits on it and
navigates the moment it lands.

No analytics/state/navigation from the pre-decode — vin_decoded and the
success/candidates/error events still fire only from the button path, so the CRO
funnel stays = real intent. Raw fetch + AbortController (api.post takes no signal),
mirroring the existing 17-char preview effect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 00:54:22 +03:00
f5e4de2aab feat(api): fastest-source-wins decode race (PCAT+EMEX parallel, PL24 on PCAT miss)
Replaces the capped-EMEX-then-sequential-PL24 fallback (incl. the Phase-3 8s EMEX
cap) with a dynamic first-definitive race. PCAT + EMEX fire in parallel; the
instant PCAT settles without a single car, PL24 joins the race (a definitive EMEX
would already have returned). Whichever source yields a definitive result FIRST
wins — no artificial wait. At the parts counter the dealer gets the OEM match as
fast as any one source can answer.

- Removes EMEX_RACE_MS (reverts the 8s cap from 9b13c4a).
- PL24 still gated by isDecodeable + circuit breaker, raced against the 25s budget.
- Preserves Phase-1 transient tagging (no negative-cache poisoning), ctx timings,
  and the candidate fallback.
- Adds race tests: PCAT-1-car wins (no PL24); PCAT-miss + EMEX vehicle;
  PCAT-miss + EMEX-miss -> PL24 wins; all-miss -> throws.

Supersedes the Phase-3 EMEX-cap approach.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 00:54:16 +03:00
8e9439450b feat(api): add newer Peugeot/Citroën/DS/Opel WMIs (VR1/VR3/VR7/W0V)
Prod decode failures clustered on French/Stellantis VINs that no source even
tried: the post-merger WMIs VR3/VR7 (Peugeot), VR1 (Citroën/DS) and W0V (Opel)
were missing from every map, so getServiceName/isSupported returned null and the
VIN was rejected as "Marka desteklenmiyor" before any scrape ran.

Add them to all three maps, matching the existing VF3/VF7/W0L routing:
- Corgi WMI_DATABASE (display brand)
- PL24_WMI_SERVICE_MAP (peugeot_parts / citroen_parts / opel_parts)
- EMEX CATALOG_MAP (PSA -> PEUGEOT00, Opel -> GM_OP201809)

Lets EMEX scrape and PL24 fall back for these mass-market TR brands instead of
hard-failing at the routing step. Phase 4 of 4 on decode reliability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 00:13:43 +03:00
9b13c4a21f fix(api): give EMEX a realistic race window in the decode chain (3s -> 8s)
EMEX HTTP scrapes typically answer in ~4-5s, but the decode race capped EMEX at
3s (EMEX_RACE_MS) and fell back to PL24 — which doesn't cover EMEX-only brands.
Prod logs show the exact loss: "PL24 fallback triggered (EMEX: timeout)" at the
3s mark, immediately followed by "EMEX HTTP: found 334 categories". The win was
thrown away.

Raise the default cap to 8s and make it env-tunable (EMEX_RACE_MS), still well
inside the 25s decode budget. Only adds latency on the harder VINs where
PartsCatalogs didn't already resolve. Phase 3 of 4 on decode reliability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 00:10:41 +03:00
c331dca9fe fix(api): fail fast on dead PartsCatalogs proxy ports + tunable timeouts
The PartsCatalogs ProxyAgent had no connect timeout, so a dead DataImpulse proxy
port stalled for undici's 10s default before the retry rotated to a fresh port
(prod: "ConnectTimeoutError ... 74.81.81.81:10628, timeout: 10000ms"). Three such
stalls exceed the caller's 25s decode budget, turning a transient bad port into a
hard decode failure.

Set an explicit 6s proxy connect timeout so retries rotate to a live port within
budget, and make the request timeout / retry count / connect timeout env-tunable
(PCAT_REQUEST_TIMEOUT_MS, PCAT_MAX_RETRIES, PCAT_PROXY_CONNECT_TIMEOUT_MS) with the
current values as defaults. Phase 2 of 4 on decode reliability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 00:09:27 +03:00
257052162e fix(api): don't poison the VIN negative cache on transient source failures
A decode that failed from a transient proxy/network blip (PartsCatalogs
DataImpulse ConnectTimeout, EMEX timeout, PL24 transport error) was written to
the 6h negative cache identically to a genuine "brand not supported" miss. A
user who hit a blip then got "Şase tanınamadı" for 6h with no way to retry out —
and prod analytics showed this hitting SUPPORTED brands (Fiat/Toyota/Hyundai
decode fine once the proxy responds).

Thread a `transient` outcome flag through resolveVin -> doResolveVin:
- PartsCatalogs.decodeVin sets it when it swallows a transport error (vs a clean
  "no cars" miss), via a new optional outcome param.
- EMEX surfaces it via its existing {type:"error"} result.
- PL24 sets it on transport/timeout errors in the fallback catch.
resolveVin then skips the negative cache when transient=true; genuine misses
still cache for 6h. Phase 1 of 4 on decode reliability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 00:08:08 +03:00
bef147d787 Merge pull request 'dev' (#43) from dev into main
Reviewed-on: #43
2026-05-24 20:40:01 +00:00
6283b66502 docs: refresh INDEX.md to match live code
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Surgical refresh of the drifted project index: corrected the repo path,
documented the internal-admin "Supe Panel", payments now Stripe-only (EFT
history preserved as deprecated), CI/CD -> qa-gate + Coolify(dev)/PM2(prod),
added blog/changelog/posthog modules + Sentry entry points, route count
36 -> 42, and replaced the fictional docs list with the files that exist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:34:49 +03:00
70ee3d637f fix(web): fire checkout_started when the payment step is reached (all plans)
checkout_started only fired for the "full" plan (plus a mostly-unreachable
sticky-CTA branch), so non-full-plan users reached payment without it ever
firing — PostHog showed fewer checkout_started (1 person) than the downstream
payment_initiated (3), which is impossible in a clean funnel.

Move it to a step-driven effect that fires once when the payment step is
reached, via any path (plan card, sticky CTA, or a prefilled pending plan),
re-arming if the user returns to plan selection. Fixes the Monetization and
Ödeme funnels at the checkout step and keeps plan -> checkout meaningful
(brand-selection drop-off now shows up instead of a trivial 100% step).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:34:43 +03:00
195b3204c3 feat(web): land new signups on VIN search, activation as a modal
New signups were redirected to /dashboard/subscription (the paywall):
91% of them hit it and 78% bounced without ever searching a VIN (PostHog,
30d). Acquisition is strong (homepage -> register) but activation was the
cliff — only 13% of new users ever reached the search page.

- register: redirect signups to /dashboard/search?welcome=1 (value-first),
  carrying the landing-page VIN + referral code across.
- search: read welcome/vin/ref params; prefill a carried VIN and auto-decode
  it once the activation modal is dismissed (no re-typing); strip params after.
- WelcomeOnboardingModal: the old full-page provisioning view is now a modal
  over the search page. It applies the referral (covers the Google OAuth
  signup path), starts the trial, plays the Remotion onboarding motion, then
  steps aside so the user lands on their vehicle's parts.
- trial_started now fires for every eligible new user (was a rarely-clicked
  button) — this populates the Onboarding funnel, which previously read 0.
- tests: add useSearch to the router stub; refresh the stale retry-CTA tests
  to the shipped submit-doubles-as-retry behavior (retry CTA dropped in a33deef).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:24:53 +03:00
5bb7a0a914 Merge pull request 'dev' (#42) from dev into main
Reviewed-on: #42
2026-05-24 17:44:02 +00:00
49891a8df6 chore(web): contact address label 'Hak Sahibi' → 'Firma Adı'
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:04:20 +03:00
ea96e7fc25 chore(web): update contact page address to THINXTRA LLC
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 19:58:13 +03:00