The pricing page had no objection handling and a half-empty page below the
cards. Added a PricingFaq disclosure section (native <details>) covering the
six recurring questions: how the free trial works, whether a card is needed,
cancellation, refunds, changing plan/brands, and payment security — all
grounded in actual product behaviour (30-day Full trial, no card, period-end
access on cancel, downgrade, Stripe PCI-DSS / KVKK).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extracted CredibilityStrip out of the subscription route module into
@/components/credibility-strip so the public pricing page can reuse it
without pulling in the heavy dashboard/Stripe/Remotion bundle. Subscription
now imports it (dropping the now-unused Boxes/Hash/Layers/Zap icons); pricing
renders it below the plan cards to justify price with real product facts
(50+ marka, 1M+ parça, OEM, saniyede sonuç, KVKK·SSL). Test import updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bring the public pricing cards in line with the in-app subscription page and
strengthen the CTA:
- Move the "Popüler" badge from 2 Marka to Full Paket (one consistent story
across both surfaces).
- CTA "Başla" -> "Ücretsiz Başla" (leans on the free trial the flow grants).
- Yearly line now uses a consistent ₺ format ("veya ₺2.000,00/yıl") plus a
"· 2 ay bedava" savings cue instead of the bare, differently-formatted
"veya 2.000,00 TL/yıl".
- Plan-decision helper above the grid ("Hangi planı seçmeliyim? …").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The pricing hero was purely procedural ("Fiyatlandırma" / "İhtiyacınıza
uygun planı seçin") with no value prop and no mention of the free trial —
the strongest conversion lever, present in the meta description and header
CTA but absent from the page body. Reworked into an eyebrow + outcome
headline ("Doğru parçayı saniyede bul") + supporting line, plus a trust
row: 30 gün ücretsiz · kredi kartı gerekmez · istediğin zaman iptal ·
7 gün iade garantisi (reusing existing subscription trust keys).
Also sorted pricing.tsx imports to satisfy biome.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Blog and contact (and about/pricing/legal) pages rendered a stripped-down
header (logo + auth buttons only) with no nav — the site nav "disappeared".
Extracted the landing header into a shared <SiteHeader> (logo, nav, theme
toggle, auth-aware actions, mobile menu) used on every public page for
consistent UX.
- new components/site-header.tsx (nav links work from any route via /#…)
- landing nav gains Blog + İletişim so all pages share the same 5-link nav
- about, contact, pricing, blog, blog/$slug, kvkk, privacy, terms → SiteHeader
- demo keeps its purpose-built demo header (sticky + theme + CTA)
typecheck + biome clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generated for the blog_posts schema added in the previous commit. Applied on
deploy by start.sh (db:migrate). CREATE TABLE + 3 indexes only; no changes to
existing tables.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Makes the blog publishable from the Süper Panel content pipeline (via n8n).
Backend mirrors the changelog module; frontend keeps the existing hand-authored
posts and merges in API-backed ones (no content migration, no regression).
API (apps/api):
- blog_posts Drizzle table (slug unique, title, meta_description, body_markdown,
tags, cover_image, status, source, published_at)
- blog module: GET /blog/posts (public list), GET /blog/posts/:slug (public),
POST /blog/posts/internal (Bearer BLOG_AUTOMATION_TOKEN, mirrors
changelog/internal) — returns { ...post, url }
- BlogService: Drizzle + Redis cache, defensive Turkish-aware slugify +
uniqueness; registered in app.module
Web (apps/web):
- use-blog hooks (list + by-slug, react-query, 30m staleTime)
- blog list: static + API merged, dedup by slug, newest first
- blog detail: static post renders as before; API post renders body_markdown
via react-markdown (remark-gfm) styled to match existing prose
- add react-markdown + remark-gfm
Deploy: set BLOG_AUTOMATION_TOKEN (and optional PUBLIC_WEB_URL) on the api,
run drizzle-kit db:push to create blog_posts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The parts-catalogs HTTP client only retried request timeouts and 401/403;
undici network errors ("TypeError: fetch failed" — a dropped/reset
DataImpulse proxy connection) were thrown on the first attempt. During the
cold-JWT window (outside 09:00-19:00 Istanbul) the proxy drops frequently,
so a single click would surface an empty/slow category that returns full
parts on a manual retry — and could even mark a populated parent group
unavailable when child discovery hit the blip.
Retry transient transport errors (timeout + undici network failures) with a
short backoff, re-acquiring a session each attempt. Definitive HTTP
responses (re-thrown as `Error: HTTP <code> …`, e.g. 400 "list of parts is
empty") are still thrown immediately — they are real answers, not blips.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>