- Enable PostHog heatmaps (coordinate-only; autocapture stays off so the
PII-heavy logged-in dashboard never ships VIN/OEM text to PostHog).
- Add reusable feature-flag plumbing: subscribeFeatureFlag in lib/posthog
and a useFeatureFlag React hook (works for boolean flags + experiments).
- Wire experiment `demo-cta-copy` into DemoFooterCta: control = current
"sınırsız sorgulama" framing, benefit = B2B OEM value hook. Variant is
tagged on demo_to_register_click; exposure tracked via $feature_flag_called.
PostHog experiment 82775 created in draft (primary metric: demo→user_signed_up,
test accounts excluded). Launches after this reaches prod.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PL24 BOM emits "see table NNN-NNN" reference rows (oem N/A, target code in
remark) with NO upstream link. Resolve the code against the vehicle's
illustration index (codes live in category names as {NNN-NNN}) and render
jump links. Unresolved targets (branch not seeded yet) deep-link a pre-filled
catalog search via ?q=.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On mobile (iOS Safari, 390px), the schema viewport's one-finger touchmove
handler called setPan() unconditionally — even at zoom 1 with nothing to
pan — which makes iOS suppress the native page scroll. Users couldn't reach
the parts list below the fold (real trial session: 16 rage clicks, 0 OEM
codes copied, no purchase).
- use-schema-interaction: gate one-finger pan on zoom > 1 so the gesture
falls through to native page scroll when not zoomed in.
- schema-viewer: set touch-action (pan-y pinch-zoom at zoom 1, none when
zoomed) on the viewport; shrink mobile schema height 400px -> 280px so the
first parts rows peek below the fold and signal "more below".
- parts-panel: after a hotspot tap, scrollIntoView uses block:"start" on
mobile (panel is below the fold) and block:"center" on desktop, so the
selection is actually visible.
Tests: new Vitest hook test asserts no pan at zoom<=1, pans at zoom>1, and
pinch-zoom still works; new parts-panel scroll test asserts mobile vs desktop
block target. Also fixes a pre-existing typo in the empty-state test ("Geri
don" -> "Geri dön").
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the old marketing "guided tour" /demo with a real, fully-functional
catalog browsing experience for the pre-warmed example vehicle. No auth
required, no upstream calls — entirely served from prod DB.
Backend (apps/api/src/demo):
* New @Public() controller exposing five endpoints under /api/demo:
- GET /vehicle → demo vehicle metadata
- GET /categories/tree → top-level category tree
- GET /categories/search?q= → cross-tree search
- GET /categories/:id → getCategoryWithParts (parts+schema+hotspots)
- GET /categories/:id/children → drill children
* DemoService validates every category id against DEMO_VEHICLE_ID before any
downstream service call — the public surface can't be used to read an
arbitrary vehicle's catalog (1-row SELECT, NotFound on miss or wrong owner).
* Vehicle id is env-driven (DEMO_VEHICLE_ID, defaults to the pre-warmed
WVWZZZ1JZ3W597935 — VW Golf 2003 with 277 cats / 9841 parts / 178 schemas
fully drilled in prod).
* Wires CategoriesModule (already exports CategoriesService) — zero new
business logic, just a thin public façade.
Frontend (apps/web):
* /demo (replaces old marketing page): vehicle header + top categories grid
reading /api/demo/* + sticky DemoBanner with sign-up CTA.
* /demo/categories/$categoryId: drill page rendering either a children grid
(parent) or the existing SchemaViewer + parts panel (leaf) — same shape
the dashboard uses, so hotspot overlay, breadcrumb trail, retry on
upstream loadError all just work.
* DemoBanner: sticky top, "Örnek araç: {label} — Kayıt Ol" CTA. The
"Yeni VIN sorgula" explicit paywall trigger lands in a follow-up task.
* PostHog events: demo_loaded (source query-param-aware),
demo_category_clicked, demo_category_detail_viewed, demo_to_register_click
(banner / footer / category_footer placements).
* usePageMeta gains an opt-in `noindex` flag — demo sets it to noindex,follow
for the first 4-6 weeks per spec; cleaned up on unmount so SPA navigation
doesn't carry it to the next route.
Follow-up to the landing fix: the credibility strip (subscription/checkout),
meta description, auth-layout badge and blog copy still claimed "50+
marka" while the catalog supports 27. Align all to 27 for consistency.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The vehicle category flow had two competing navigation models: the grid
drilled in place (no URL change, own breadcrumb) while the category route
had a separate breadcrumb reconstructed from the cached tree plus a back
button with different semantics. The route breadcrumb collapsed on
refresh/deep-link and for lazily-fetched deep nodes, and browser-back from
an in-grid drill ejected the user out of the whole flow.
Make the URL + a server-provided ancestor trail the single source of truth:
- API: getCategoryWithParts now returns `ancestors[]` (root→parent) via a
recursive CTE, so breadcrumbs are complete regardless of client cache.
- New shared <CategoryBreadcrumb> (Arama → vehicle → ancestors → current)
used by both the vehicle and category pages.
- Category page back button derives the parent from `ancestors` (no more
cache-path race); cache reconstruction (findCategoryPath) removed.
- CategoryGrid drills via route navigation per level (each level a URL +
history entry), seeding the query cache for instant render. Browser-back
now goes up one level and deep levels are shareable/refreshable.
- Tree & Columns intentionally keep their in-place paradigm.
- a11y: aria-labels on back buttons and the breadcrumb nav.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The pricing/subscription credibility strip stated 50+ marka / 1M+ parça
(the current real figures) while the rest of the site still showed the
outdated 27+ marka / 243K parça — so the same visitor saw "1M+ parts" on
pricing and "243K parts" on register. Updated every catalog-scale claim
(SEO/OG/Twitter meta, page-meta default, homepage stat cards + feature
cards + CTA list, auth promo panel, blog prose) to the consistent
50+ marka / 1M+ parça. Also sorted blog imports for biome.
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>
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>
Squash-recovery of fusion/fn-188 onto dev — original branch was based on
main (dccd4fa) due to baseBranch=None drift in Fusion settings; rebase
onto dev surfaced unrelated main-only commits (EMEX/PL24/Corgi) as false
conflicts. This commit applies only FN-188's 23-file changelog patch.
- apps/api: ChangelogModule (controller, service, DTO, spec, schema)
- apps/web: ChangelogTab (timeline + accordion), useChangelog hook, i18n
- packages/shared: changelog Zod schemas + types
- packages/ui: Accordion component + Badge stage variant
- docs/INDEX.md: changelog feature documented
Lint, typecheck, all 169 api tests + 2 web tests pass.
- Add unavailable/remark/modelCodes/presel fields to categories and parts DB schema
- Pass PL24 unavailable flag through pipeline instead of filtering out records
- Show unavailable categories/parts at reduced opacity in grid, tree, and parts panel
- Display part remark and model codes as secondary info in parts table
- Move Google sign-in button above email form on login/register pages with branded icon
- Add public email existence check endpoint for better login error messages
- Update INDEX.md documentation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add oem_code_copies table and analytics module for tracking part code copies
- Integrate PostHog for frontend product analytics (VIN decode, OEM copy events)
- Switch email service from stub to Postal API with proper error handling
- Add copy button to parts panel with clipboard + server-side logging
- Add admin copy-logs page with filtering and top-copied-codes view
- Add VIN report endpoint for users to flag unrecognized chassis numbers
- Add Postal email env vars to config schema
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add POST /admin/users endpoint with password hashing and role support
- Add user creation dialog to admin users page
- Migrate web from Next.js to Vite + TanStack Router
- Fix Dialog component positioning for Tailwind CSS v4
- Add @source directive for @sase/ui package scanning
- Add pl24 integration parsers and vehicle decode flow
- Backup old Next.js app to apps/web-nj
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>