Commit Graph

496 Commits

Author SHA1 Message Date
d22662acb3 perf(pcat): demote e-acca.com to the end of the JWT site round-robin
PCAT auth pool warm-up always started with e-acca.com, which doesn't
load through the new DataImpulse rotating proxy — page.goto sits on
the 30s navigation timeout and only then falls through to the next
candidate. The remaining sites (alkatalog, auto-komplekt, autotrade,
…) reach the upstream widget in 3-10s through the same proxy, so an
on-demand acquireSession() that lands during a cold start spends 30s
blocked on the dead first hop before any retry can succeed.

Move e-acca to the end of the list. New cold-pool capture finishes
in ~4s on the first reachable site (alkatalog) instead of 30s+9s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 10:32:32 +03:00
85d0d791ed feat(internal-admin): refund + generalize subscription extend — Phase E
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Refund (Stripe API)
- StripeService.refundPayment({ paymentId, amount?, reason, founderId })
  is a new public method that wraps stripe.refunds.create:
  * Requires the payment to have a stripePaymentIntentId (post-Iyzico).
  * Refuses payments not in completed/partially_refunded status.
  * Partial refund: amount must be in 1..payment.amount (kuruş).
  * Sends panel_* metadata to Stripe for the founder/reason audit trail.
  * Flips payments.status to refunded / partially_refunded.
  * Appends a dated reason line to payments.admin_note.
  * Captures a `payment_refunded` PostHog event (via:'super_panel').
  * Does NOT cancel the subscription — that's a separate decision.
- New endpoint POST /internal/admin/payments/:id/refund behind the
  InternalTokenGuard, body { amount?, reason, founderId }.
- Wired through PaymentsAdminController in InternalAdminModule;
  StripeModule imported.

Extend (goodwill / bonus time)
- BillingService.extendTrial now accepts both trial AND active
  subscriptions (was trial-only). Same end-date semantics
  (base = max(now, current endDate)). Response now also returns
  subscriptionStatus so the panel can surface the right copy.
- Endpoint URL kept as /trial/extend for backward compatibility; the
  panel decides the user-facing label ("Trial uzat" vs "Bonus süre
  ekle / Goodwill") based on current status.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 10:03:03 +03:00
d2fb64484c perf(emex): plain-HTTP fast path with Playwright fallback (Tier 3-A)
Probe (scripts/dev) confirmed that everything PartsPanel reads from an
emexdwc.ae leaf — parts table, hotspot coordinates, and the schema image
URL — is fully server-rendered in Unit.aspx HTML. Parts arrive as
`<tr name>` rows with `td[name=c_oem|c_pnc|c_name]`; hotspots are
`<div class="dragger g_highlight" name=N style="margin-top:Ypx; margin-left:Xpx; ...">`
with the coords already in image-natural pixel space; the image URL is
in `<img class="dragger" src=...laximo...>` and its native dims can be
read from the first 24 bytes of the GIF/PNG via a Range GET.

Behaviour:
1. fetchCategoryParts now tries fetchCategoryPartsViaHttp first — two
   sequential GETs (QuickDetails → Unit) + a Range GET for image dims.
2. If the HTML yields ≥1 part, we return it.
3. If the HTML returns no Unit.aspx anchor, or 0 parts, we fall back to
   the existing Playwright scraper (same code path as before).

The plain-HTTP path skips the ~1-2s browser launch, sidesteps the
3-page semaphore in EmexBrowserService (concurrency cap was throttling
prefetch fan-out), and uses no chromium memory. Measured on dev with 5
fresh-ssd Renault Espace IV leaves: 4.9-5.5s wall per leaf (vs 6-7s on
the Tier 1 browser path, vs 12-14s pre-Tier-1). The 6th sample
(stale-ssd Fren Kaliyeri) failed both paths identically — confirms the
plain-HTTP path doesn't introduce new failure modes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 09:56:53 +03:00
37a5e8f0f7 feat(internal-admin): plan change + cancel/resume — Phase D
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Three new endpoints on /internal/admin/subscriptions/:id complete the
Süper Panel billing surface.

POST .../change-plan  { newPlanId, reason, founderId }
- Only for active or trial subscriptions.
- Refuses no-op (already on that plan) and inactive plans.
- Updates planId; leaves userBrands intact so the founder can decide.
- Response includes brandReassignmentNeeded flag when the new plan's
  brandCount diverges from the current user-brand count (the panel
  surfaces a warning so the founder reaches out).

POST .../cancel       { reason, founderId }
- Active or trial → cancelled (sets cancelledAt = now).
- Refuses already-cancelled or expired.

POST .../resume       { reason, founderId }
- Cancelled → active (clears cancelledAt).
- All other states rejected.

Controller cleanup: factored requireFounder + requireReason guards so
every endpoint enforces the same validation contract uniformly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 09:43:19 +03:00
0e83353f4e feat(internal-admin): trial extend + manual activate — Phase C
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Two new endpoints on /internal/admin/subscriptions/:id behind the
InternalTokenGuard.

POST .../trial/extend     { days, reason, founderId }
- Only operates on status='trial' subscriptions.
- 1..90 day clamp; new endDate = max(now, current endDate) + days
  (never shrinks the trial window).
- Returns previous/new endDate + daysAdded.

POST .../activate         { reason, founderId }
- Wraps SubscriptionsService.activateSubscription which handles
  status transition, startDate/endDate by billing period, and
  brand auto-assignment for Full plan.
- Refuses already-active, cancelled, or expired subscriptions.

Wiring
- BillingService + BillingController added to InternalAdminModule.
- SubscriptionsModule imported so we can call activateSubscription.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 09:39:18 +03:00
96a9d11015 feat(internal-admin): user lifecycle — suspend / reactivate / ban
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Süper Panel Phase 7 — Phase B. Founder can suspend, reactivate, or ban a
Sase user from the panel. Status enforced in the AuthGuard so blocked
users can no longer make authenticated requests.

Schema (migration 0006)
- users.status varchar(20) default 'active' — active|suspended|banned
- users.status_reason text — free-text reason set on transition
- users.status_changed_at, status_changed_by uuid — audit metadata
- users_status_idx

Auth
- AuthGuard rejects 'suspended' / 'banned' with TR-localized message.
- auth.ts: declared `status` as a Better Auth additionalField so the
  session.user object exposes it (matches how `role` is wired).

Endpoints (InternalTokenGuard)
- POST /internal/admin/users/:id/suspend     { reason, founderId }
- POST /internal/admin/users/:id/reactivate  { founderId }
- POST /internal/admin/users/:id/ban         { reason, founderId }

Service
- LifecycleService.setStatus():
  - refuses to touch admin-role users
  - refuses no-op transitions (already in target state)
  - refuses suspended→banned→suspended downgrade path (must reactivate first)
  - on suspend/ban: deletes all sessions for the user (immediate sign-out)
  - returns { from, to, sessionsKilled, changedAt }

Wiring
- LifecycleService + LifecycleController added to InternalAdminModule.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 09:34:52 +03:00
a1e80c042f fix(emex): ship runtime scripts/ in prod image and forward chromium executable path
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
EMEX-decoded vehicles surfaced empty category pages on prod because two
runtime prerequisites were absent from the production image:

1. `scripts/emex-vin-scraper.js` was never copied — the build stage's
   `COPY . .` brings it in but the production stage only cherry-picks
   `apps/api/dist`, `drizzle`, and `start.sh`. Every EMEX leaf hit
   therefore failed with "Scraper file not found at: /app/scripts/…"
   and the category page rendered "Bu kategori icin parca bulunamadi."
2. `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser` was
   exported in the Dockerfile but never reached `chromium.launch()` —
   that env var is consumed by `playwright install`, not at runtime.
   Playwright fell back to its bundled headless-shell cache path
   (`/root/.cache/ms-playwright/chromium_headless_shell-*/…`) which
   does not exist on the alpine image, so even with the scraper file
   present the browser pool init would have kept failing.

Fix:
- Dockerfile: `COPY --from=build /app/scripts ./scripts`.
- `emex.browser.ts` + `parts-catalogs-auth.service.ts`: read
  `process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` and pass it as
  `executablePath` to `chromium.launch()` when set.

Verified on prod container: `ls /app/scripts` → missing pre-fix; the
binary at `/usr/bin/chromium-browser` exists, so the env-var hand-off
will resolve cleanly once the new image lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 08:45:18 +03:00
2583b781ec feat(internal-admin): readonly impersonation for Süper Panel
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Lets the founder open a target user's session in a new tab from the
panel for debugging. Read-only enforced server-side — any non-GET
request from an impersonated session returns 403.

Schema
- sessions.impersonated_by (uuid, nullable) — founder Better Auth user id
- sessions.impersonation_readonly (bool, default false)
- index on impersonated_by

Service
- ImpersonationService.createReadonlySession({ targetUserId, founderId,
  ttlMinutes, reason, ipAddress, userAgent }):
  - Random sessionId + token (32 bytes hex each)
  - TTL clamped 1..60 min, default 15
  - Refuses to impersonate admin users
  - Inserts sessions row; signs cookie value with HMAC-SHA256(BETTER_AUTH_SECRET)
    matching better-call's signCookieValue format
  - Returns { cookieName, cookieValue, expiresAt, sessionId }

Guard
- ImpersonationReadonlyGuard runs after AuthGuard, before RolesGuard.
- GET/HEAD/OPTIONS pass through.
- For other methods: looks up sessions.impersonated_by + impersonation_readonly
  by request.session.id; throws ForbiddenException if both truthy.

Endpoints (InternalAdminModule)
- POST /internal/admin/users/:id/impersonate-readonly [InternalTokenGuard]
  body: { ttlMinutes, reason, founderId }
  returns: { redirectUrl, expiresAt, sessionIdPrefix }
  Hand-off is via signed consume URL (cross-origin Set-Cookie limitations).
- GET /admin/impersonate/consume?t=<signed> [@Public]
  Verifies HMAC-signed payload (<=60s validity), sets the Better Auth session
  cookie on sase.tr, redirects to /. One-shot.

Wiring
- InternalAdminModule imported in AppModule.
- ImpersonationReadonlyGuard registered as APP_GUARD between Auth and Roles.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 00:48:39 +03:00
d1c78f7b89 feat(internal-admin): add InternalTokenGuard for Süper Panel admin calls
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Foundation guard for /internal/admin/* endpoints called from Süper Panel
(sp.semih.ai) over Coolify internal network. Verifies X-Internal-Token
header against INTERNAL_API_TOKEN env with constant-time compare.

Not yet wired to any endpoint — internal-admin module/controllers will
land in follow-up commits as panel-side mutation features ship.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 00:31:05 +03:00
Fusion
5118c8e5e9 feat(FN-415): add locator-stability + hit-target verification tests
Commits merged:
- feat(FN-415): add locator-stability + hit-target verification tests

Files changed:
apps/api/src/telemetry/__tests__/telemetry.spec.ts |  8 +++
 .../src/routes/__tests__/dashboard-search.test.tsx | 67 ++++++++++++++++++++++
 2 files changed, 75 insertions(+)

Fusion-Task-Id: FN-415

Fusion-Task-Lineage: 01e6f3d2-cd2e-4023-b538-ea54926b4f87
2026-05-17 06:17:51 +00:00
Fusion
78ab45d512 feat(FN-411): Merge pull request 'dev' (#3) from dev into main (+16 more)
Commits merged:
- docs(FN-411): scoping output — spawned FN-415 (FN-367 mechanical verification) and FN-416 (FN-368 mechanical verification)
- Merge pull request #21 — feat(FN-403): parts panel manifest
- Merge pull request #20 — feat(FN-401): blocker note
- Merge pull request #19 — feat(FN-400): selector manifest
- Merge pull request #18 — feat(FN-399): PostHog cross-session funnel audit
- Merge pull request #17 — chore(payments): Stripe-only
- Merge pull request #16 — feat(FN-395): P1 candidate shortlist
- Merge pull request #15 — broaden leaf detection
- fix(categories): broaden leaf detection to all /extern/*/{vin,mdl}_items endpoints
- Merge pull request #14 — fix(pl24): Ford VIN flow
- Merge pull request 'dev' (#13) from dev into main
- Merge pull request 'feat(FN-367): add inline retry affordance after VIN decode failure (FN-367, gitea #11)' (#12) from dev into main
- Merge pull request 'dev' (#9) from dev into main
- Merge pull request 'dev' (#8) from dev into main
- Merge pull request 'docs(readme): spacing tweak — measure cache-enabled dev deploy speed' (#7) from dev into main
- Merge pull request 'docs(readme): add staging URL — gitea deploy flow smoke test' (#4) from dev into main
- Merge pull request 'dev' (#3) from dev into main

Files changed:
SCOPING-FN-411.md                             | 85 +++++++++++++++++++++++++++
 apps/api/src/catalog/catalog.service.ts       |  8 ++-
 apps/api/src/categories/categories.service.ts | 23 +++++---
 3 files changed, 107 insertions(+), 9 deletions(-)

Fusion-Task-Id: FN-411
2026-05-16 23:54:44 +00:00
Fusion
65daf99b2e chore(payments): remove EFT/bank-transfer method, keep Stripe only
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Removes the EFT (havale) payment surface across API, web, i18n, and docs.
Card payment via Stripe is now the only checkout path.

API (apps/api/src):
- Delete payments/bank-accounts/ module (controller + service + module)
- payments.service: drop EFT methods (createEftPayment, uploadEftReceipt,
  approveEft, rejectEft, getActiveBankInfo, getPendingEftPayments) and the
  BankAccountsService dependency
- payments.controller: drop /payments/eft, /payments/eft/:id/{receipt,approve,reject},
  /payments/bank-info, /payments/pending; keep /payments/me
- payments.module: drop BankAccountsModule import
- admin.service: drop pending EFT counter from getDashboardStats; drop
  getPendingPayments (admin EFT approval list)
- admin.controller: drop /admin/payments/pending endpoint
- admin.service.spec: drop the getPendingPayments + pendingPayments assertions

Web (apps/web/src):
- Delete components/payment/bank-transfer-card.tsx
- Delete routes/dashboard/admin/payments.tsx (EFT approval page); regenerate
  routeTree.gen.ts
- subscription/index.tsx: drop EFT tab from PaymentMethodSection, drop the
  paymentMethod state + handleEftCompleted, simplify ConfirmationCard to the
  Stripe-only path, drop unused imports (Tabs, BankTransferCard, Building2)
- billing.tsx: drop "eft" from method filter chips; keep methodLabels.eft and
  the legacy receipt download so historical EFT records still display
- dashboard.tsx + admin/index.tsx: drop /dashboard/admin/payments from the
  sidebar + admin quick-link grid; trim unused icon imports; KEYS_6 → KEYS_5
  for the now-5-card admin stats skeleton
- messages/{tr,en}.json: strip every EFT-only key under payment.* (eftTransfer,
  eftConfirmationDescription, eftPaid, eftStatus, bank.*, uploadReceipt* etc.)

Schema (apps/api/src/database/schema):
- bankAccounts table + payments.bankAccountId column are kept as-is: legacy
  EFT payments remain visible on the billing page and the schema preserves
  historical records.

Docs:
- README.md: "Stripe (kart) + EFT" → "Stripe (kart)"
- CLAUDE.md: stack table + PaymentsModule row + payments table note + route
  list updated; admin EFT approval route removed

Verification:
- pnpm typecheck: green (api + web + shared + config + ui)
- pnpm lint: green
- pnpm test: 20 web tests pass, 173 api tests pass
2026-05-16 11:03:30 +03:00
33605ead25 fix(categories): broaden leaf detection to all /extern/*/{vin,mdl}_items endpoints
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
The leaf-path checks only matched /servicepart/vin_items literally, but
PL24's P5 modern catalog ships the same shape under /chemicals/vin_items,
/accessories/vin_items, /chemicals/mdl_items, etc. When user clicked a
"Rötuşkalemseti" (touch-up paint set) category whose linkPath was
/p5vwag/extern/chemicals/vin_items, the code drilled in, treated each
paint chemical's per-part URL (?partno=LLSMAX010) as a sub-category,
and inserted 228 ghost rows under it. Replace the literal substring
match with a regex that covers the whole /extern/{kind}/(vin|mdl)_items
pattern; apply to both the user-vehicle (categories.service) and the
catalog (catalog.service) flows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 10:30:46 +03:00
af4142077f fix(categories): treat image-board.action linkPaths as leaves in user-vehicle flow
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
getChildren had a leaf-detection whitelist that covered BOM/partinfo paths
but missed PL24's image-board.action (and the VIN variant). When users
clicked into a vin-image-board.action leaf, fetchSubGroupsByPath ran on
its HTML, parsed the BOM rows' jsonUrl="...json-vin-bom-detail.action..."
attributes as if they were sub-groups, and inserted 10+ ghost category
rows under the leaf — each pointing to a per-part endpoint that's not
a sub-group at all. Drilling into one of those ghosts then surfaced the
"0 parça listeleniyor" empty state instead of the real BOM table.

Add both image-board.action and json-vin-bom-detail.action to the leaf
detection list so the flow short-circuits and getCategoryWithParts
handles them via fetchPartsByPath as designed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 10:03:59 +03:00
2bf5e1622c fix(pl24): pass Ford VIN BOM detail URL through fetchP4Page baseUrl prefix
fetchFordVinBomParts was forwarding the relative HTML jsonUrl with
isFullUrl=true, which made undici try to parse \`/ford/fordp_parts/…\`
as an absolute URL and throw "Failed to parse URL" for every PNC row.
The Hyundai BOM expander next door calls fetchP4Page with the default
isFullUrl=false (relative path) — match that.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 10:03:59 +03:00
be3fb2a4b5 feat(pl24): wire Ford/Volvo VIN-decode flow end-to-end via json-vin endpoints
VIN-decoded Ford P/T and Volvo legacy vehicles used to land on an empty
category page: the .action HTML the decoder scraped only carried header
breadcrumbs (Portal / Model seçimi / VIN). The real catalog hangs off
three JSON endpoints that the partslink24 UI calls in the background
once a VIN session is established. None of them need mode/upds/JSESSIONID
beyond the standard PL24TOKEN cookie. Plumb the whole chain so a user
who decoded a VIN sees real Turkish part categories and OEM part
numbers in the user-vehicle flow:

  json-vin-main-group.action   → real top-level groups (8 for Mondeo)
  json-vin-sub-group.action    → 58 leaf subgroups (filters subheaders)
  vin-image-board.action       → BOM table with pncHierCode + jsonUrl
  json-vin-bom-detail.action   → final OEM partno entries (per variant)

Schema image fetch reuses the existing image-ticket extractor since the
ticket URL lives in the same jsIlluData payload as Hyundai/Opel/Volvo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 10:03:59 +03:00
032217716e fix(pl24): drop P4 nav-crumb links from decoded categories
VIN-group HTML for Ford/Volvo legacy services exposes the header
breadcrumb links (Portal / Model seçimi / current VIN) via the same
.action pattern the parser relied on, so they were being persisted as
"part categories" — users saw three useless rows instead of real groups.
The PSA flow already documented this trap and bypassed rawData; extend
the same defense to the rest of P4 legacy (parser-side + categories
insert fallback).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 10:03:59 +03:00
Semih
de84d8d267 fix(schema-viewer): drop empty schema panel for image-less BOM overviews
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
PL24's bomOverviewlist widget (PR-code-filtered overview tables like
"Alternatöre genel bakış · Start-Stop modu bulunan araçlar için
[PR:7L6,7L8]") returns parts but no illustration — upstream genuinely
has no schema for these. Previously the 60% left panel rendered the
text "Sema goruntusu bulunamadi", which reads like a failure and
wastes most of the layout for what is actually a complete result.

When schemaPic is null, skip the schema panel entirely and let the
parts list take the full container width. The parts panel already
handles its own header/empty state, so no other adjustment needed.
2026-05-14 20:47:43 +00:00
Semih
7ffff32cac chore(pl24): remove [imgdbg] diagnostic log
Diagnostic served its purpose: confirmed that PL24 bomOverviewlist
URLs (e.g. illustrationId=4336125, "Alternatöre genel bakış" with
PR-code filters) return only a records array with no images field —
upstream genuinely has no schema illustration for these overview
tables. Frontend UX for this case is handled separately.
2026-05-14 20:47:03 +00:00
Semih
7f0f17580e chore(pl24): log full response shape when images field is missing (temp diagnostic) 2026-05-14 20:33:24 +00:00
Semih
33e3a7a7d6 fix(vehicles): make decoded vehicles readable by any authenticated user
Decoded vehicle data is shared across the platform — once any user
decodes a VIN, the vehicle, its categories, and its parts should be
visible to every authenticated user. The previous getById enforced a
user↔vehicle junction via inner join, returning 404 "Araç bulunamadı"
when a user tried to view a vehicle they hadn't decoded themselves.
This surfaced as "Veriler yüklenirken bir hata oluştu" on the category
detail page for any vehicle the current user wasn't linked to.

Drop the ownership filter from getById; the userVehicles junction is
now used only for per-user history listing and delete operations.
Verified with Playwright: GET /api/vehicles/.../categories/... was
returning 404 from the ownership check for non-owner users.
2026-05-14 20:17:38 +00:00
Semih
47f76e47d2 fix(FN-373): stop stack reset on every render in CategoryGrid
useTranslation returns a fresh `t` each render, so including it in the
useEffect deps caused the effect to re-fire continuously, resetting the
drill-down stack immediately after handleSelect pushed a child level.
Result: clicking a parent category in grid view did nothing — no API
call, no UI change, no navigation. Reproduced via Playwright on
dev.sase.tr.

Drop `t` from the dep array (key is static; locale changes mid-session
are rare and acceptable to render stale until the next prop change).
2026-05-14 20:02:14 +00:00
Fusion
6c28688283 feat(FN-373): grid view drill-down for parent categories (+1 more)
Commits merged:
- chore(FN-373): verify grid drill-down lint + typecheck clean
- fix(FN-373): grid view drill-down for parent categories

Files changed:
.../src/components/categories/category-grid.tsx    | 175 ++++++++++++++++-----
 apps/web/src/messages/en.json                      |   3 +
 apps/web/src/messages/tr.json                      |   3 +
 3 files changed, 138 insertions(+), 43 deletions(-)

Fusion-Task-Id: FN-373
2026-05-14 18:56:08 +00:00
Fusion
0ce68a7b99 feat(FN-368): parts panel loading state, inert unavailable rows, escape hatch (gitea #10)
Commits merged:
- fix(FN-368): parts panel loading state, inert unavailable rows, escape hatch (gitea #10)

Files changed:
.../schema/__tests__/parts-panel.test.tsx          | 151 ++++++++++++++
 apps/web/src/components/schema/parts-panel.tsx     | 230 ++++++++++++---------
 apps/web/src/components/schema/schema-viewer.tsx   |   7 +-
 3 files changed, 291 insertions(+), 97 deletions(-)

Fusion-Task-Id: FN-368
2026-05-14 18:43:20 +00:00
Fusion
e274e02c23 feat(FN-367): add inline retry affordance after VIN decode failure (FN-367, gitea #11)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Commits merged:
- fix(web): add inline retry affordance after VIN decode failure (FN-367, gitea #11)

Files changed:
apps/api/src/telemetry/__tests__/telemetry.spec.ts |   2 +-
 .../src/routes/__tests__/dashboard-search.test.tsx | 172 +++++++++++++++++++++
 apps/web/src/routes/dashboard/search.tsx           | 145 ++++++++++-------
 3 files changed, 266 insertions(+), 53 deletions(-)

Fusion-Task-Id: FN-367

Fusion-Task-Lineage: 5dccf49f-5c77-4fe4-ae2d-0273016d517d
2026-05-14 13:55:36 +00:00
Fusion
a0deefd4b2 chore(remotion): acknowledge Remotion license on Player instances
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Suppresses the 'license required' console warning on the homepage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 10:40:57 +00:00
Fusion
09901899d1 fix(csp): allow data: media for Remotion player audio
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-14 10:31:12 +00:00
Fusion
36d041d514 fix(csp,faro): allow t.sase.tr in CSP and proxy /collect/ to Grafana Cloud
CSP was blocking PostHog scripts/connections (t.sase.tr) and the theme-FOUC
inline script in index.html. Faro /collect/ requests were aborting because
the bare-metal nginx route disappeared during the Coolify migration.

- Add https://t.sase.tr to scriptSrc + connectSrc
- Add sha256 hash for the theme-FOUC inline script in index.html
- Exclude /collect/* from the /api global prefix
- Add FaroCollectController that forwards POST /collect/:id to
  faro-collector-prod-eu-west-2.grafana.net (overridable via FARO_UPSTREAM)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 10:08:18 +00:00
Fusion
5bd5325367 feat(FN-356): add checkout_started PostHog event to subscription checkout flow (+1 more)
Commits merged:
- chore(FN-356): verify checkout_started event is in place
- feat(FN-356): add checkout_started PostHog event to subscription checkout flow

Files changed:
apps/web/src/routes/dashboard/subscription/index.tsx | 4 ++++
 1 file changed, 4 insertions(+)

Fusion-Task-Id: FN-356
2026-05-14 10:00:40 +00:00
Fusion
906eef148d feat(FN-346): add CLS regression test for skeleton plan grid (+1 more)
Commits merged:
- feat(FN-346): add grid-parity integration test for skeleton CLS regression
- feat(FN-346): add CLS regression test for skeleton plan grid

Files changed:
.../subscription/__tests__/cls-regression.test.tsx | 128 +++++++++++++++++++
 .../subscription/__tests__/grid-parity.test.tsx    | 141 +++++++++++++++++++++
 .../src/routes/dashboard/subscription/index.tsx    |   4 +-
 3 files changed, 271 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-346
2026-05-14 05:09:47 +00:00
Fusion
aaf2e763cb feat(FN-345): fix skeleton layout shift, dialog overflow, and safe-area padding (+1 more)
Commits merged:
- fix(FN-345): add mobile-safe max-width to cancel confirm dialog
- fix(FN-345): fix skeleton layout shift, dialog overflow, and safe-area padding

Files changed:
apps/web/src/routes/dashboard/subscription/index.tsx | 15 +++++++++------
 1 file changed, 9 insertions(+), 6 deletions(-)

Fusion-Task-Id: FN-345
2026-05-14 03:05:45 +00:00
Fusion
561b02b3d2 feat(FN-343): remove lingering iyzico references after Stripe migration (+1 more)
Commits merged:
- chore(FN-343): remove lingering iyzico references from docs, config, and scripts
- feat(FN-343): remove lingering iyzico references after Stripe migration

Files changed:
CLAUDE.md                                    | 10 +++++-----
 README.md                                    |  2 +-
 apps/api/src/database/schema/core.ts         |  1 +
 apps/web/src/messages/en.json                |  1 -
 apps/web/src/messages/tr.json                |  1 -
 apps/web/src/routes/dashboard/billing.tsx    |  6 +++---
 docker-compose.coolify.yml                   |  5 ++---
 docs/INDEX.md                                | 27 ++++++++++++-------------
 knowledge.md                                 | 30 ++++++++++++++--------------
 packages/shared/src/constants/error-codes.ts |  1 -
 packages/shared/src/index.ts                 |  1 -
 packages/shared/src/types/payment.ts         | 12 +----------
 scripts/fn342-pw-verify.mjs                  |  2 +-
 scripts/validate-env.sh                      |  3 +--
 14 files changed, 43 insertions(+), 59 deletions(-)

Fusion-Task-Id: FN-343
2026-05-14 02:31:42 +00:00
Sase Dev
02bf7342a1 fix(subscription): recover stepper when a pending checkout is abandoned
Some checks failed
Deploy / Deploy to Production (push) Has been cancelled
A user who picked a plan + brands, clicked the checkout button to
create the pending subscription, then closed the tab without paying
came back to a frozen "Mevcut Plan · Bekliyor" card with no payment
button and no way to edit. Status was "pending", which my conditional
treated as an active subscription and routed away from the stepper.

Frontend
- hasActiveSub narrowed to active|cancelled only. Pending users now
  see the stepper again.
- New effect pre-fills selectedPlanKey, selectedBrandIds,
  billingPeriod from the pending row on first render and jumps
  straight to the payment step.
- New PendingPaymentBanner at the top of the stepper with
  "Odemeye devam et" (scrolls + sets step=payment) and "Vazgec"
  (calls cancel-pending then resets selection).

Backend
- subscriptions.service.create() auto-expires existing pending
  rows for the user before inserting a new one — abandoned checkouts
  no longer accumulate and a fresh attempt with a different plan
  doesn't 409.
- New PATCH /subscriptions/cancel-pending + service method for
  the banner's explicit cancel action.

i18n: subscription.pendingBanner.* (tr/en).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:57:13 +00:00
Sase Dev
9c05a05a7a fix(drizzle): register 0004_hot_quicksilver in migration journal
The 0004 migration file shipped in 6a0b36a but _journal.json did not
include its entry, so the hash-based migrate runner saw "0 applied,
4 already applied" on the previous deploy. Append idx 4 so the next
deploy picks it up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:46:54 +00:00
Sase Dev
6a0b36a7a0 feat: replace Iyzico with Stripe + single-page subscription stepper
Subscription/checkout flow rewritten end-to-end. The plan-card "Devam Et"
button silently wiped brand selection on re-click and the actual proceed
button lived offscreen — fixed by collapsing plan/brands/payment/confirm
into a single vertical stepper with one sticky CTA.

Backend
- Stripe Hosted Checkout (`/payments/stripe/checkout`) + webhook
  (`/payments/stripe/webhook`, raw body) replacing the stubbed Iyzico
  module. Webhook activates subscription on `checkout.session.completed`,
  expires the pending subscription on cancel/expire so users can retry.
- New `bank_accounts` table — multiple rows, single `is_active` enforced
  by a partial unique index. Admin CRUD under `/admin/bank-accounts`
  with multipart QR upload to MinIO; atomic `/activate` swap in a
  transaction; `GET /payments/bank-info` returns the active row.
- `payments` gains `stripe_session_id`, `stripe_payment_intent_id`,
  `bank_account_id`. EFT flow now reads the active bank account at
  payment time and stores the FK for reconciliation.
- Env: `IYZICO_*` removed, `STRIPE_*` added (validated by zod schema).
- `main.ts` `rawBody: true` for Stripe signature verification.
- Drizzle 0003 snapshot id collision fixed (VIEW-only migration shared
  prevId with 0002, blocking new generates).

Frontend
- `/dashboard/subscription` rewritten as a 4-step vertical stepper with
  step-aware sticky bottom CTA; plan re-selection is idempotent and
  preserves brand state. `/dashboard/subscription/pay` deleted; Stripe
  returns to the same page via `?stripe=success|cancelled` and the UI
  polls `/subscriptions/me` until the webhook activates the row.
- New components: `bank-transfer-card.tsx` (DB-driven IBAN + Kolay Adres
  + uploaded QR image + receipt upload) and `stripe-checkout-button.tsx`.
- Active subscription view, trial onboarding/urgency banner, downgrade
  and cancel dialogs preserved.
- TR/EN i18n: new `subscription.steps.*`, `subscription.stickyCta.*`,
  `payment.stripe.*`, `payment.bank.*`; provider label updated.

PostHog: `method: "iyzico"` → `"stripe"`; new events `iban_copied`,
`kolay_adres_copied`, `qr_viewed`, `eft_initiated`,
`stripe_redirect_returned`.

Deploy runs `db:migrate` which applies 0004_hot_quicksilver
(additive: new table + nullable columns; safe to apply on prod).
Operator must add `STRIPE_SECRET_KEY`, `STRIPE_PUBLISHABLE_KEY`,
`STRIPE_WEBHOOK_SECRET` to env and create the first
`bank_accounts` row via the admin endpoint before the bank tab works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:42:28 +00:00
Sase Dev
0b6d4bb0a7 feat(analytics): enrich PostHog VIN/parts/subscription events (SASE-PH-001 Faz 0/1 additive subset)
Per sase-posthog-events-prd.md, additive-only pass (no renames, no dead-feature events):

- search.tsx: add search_input_focused, search_input_validation_failed,
  search_paste_detected, search_history_item_selected; track query_source
  (manual|paste|history) and propagate to vin_decoded/success/error; add
  response_time_ms, source, status_code on success/error; add selected_index,
  candidates_count, time_to_select_ms to vin_decode_candidate_selected.
- parts-panel.tsx: add parts_panel_viewed with vehicle_id, category_id,
  parts_count, has_prices, available_groups_count (one-shot per mount).
- subscription/index.tsx: enrich plan_selected (period, from_plan, status),
  checkout_started (from_plan, brands_count, trial_available, status),
  subscription_cancelled (from_plan, billing_period, status_before_cancel,
  days_since_start, had_downgrade_offer).
2026-05-13 22:05:50 +00:00
Sase Dev
5b9af5a6a5 fix(web): hoist hooks above early returns in TrialUrgencyBanner
useEffect and useCallback were declared after three `return null` guards,
so when a user's subscription transitioned into the trial+≤7-days window
React saw more hooks than the previous render and crashed with #310.
Hoisted all hooks above the single guard return; render and dismiss
behavior unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 21:41:16 +00:00
Fusion
c8aac14d56 feat(FN-306): change trial banner threshold to ≤7 days and rename PostHog events
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Deploy / Deploy to Production (push) Has been cancelled
Commits merged:
- feat(FN-306): complete Step 1 — change trial banner threshold to ≤7 days and rename PostHog events

Files changed:
apps/web/src/components/trial-urgency-banner.tsx | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

Fusion-Task-Id: FN-306
2026-05-13 09:00:22 +00:00
Fusion
4cb715f1b4 feat(FN-300): add trial urgency banner to subscription page
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged:
- feat(FN-300): complete Step 1 — add trial urgency banner to subscription page

Files changed:
.../src/routes/dashboard/subscription/index.tsx    | 107 +++++++++++++++++++++
 1 file changed, 107 insertions(+)

Fusion-Task-Id: FN-300
2026-05-13 08:13:26 +00:00
Fusion
3dd4dc2f77 feat(FN-297): add social proof impression tracking to landing page testimonials (+2 more)
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged:
- docs(FN-297): update FN-275 audit with social proof event locations
- fix(FN-297): format IntersectionObserver mock in test file
- feat(FN-297): add social proof impression tracking to landing page testimonials

Files changed:
apps/web/src/routes/__tests__/index.test.tsx |   7 ++
 apps/web/src/routes/index.tsx                | 111 +++++++++++++++++++++++++--
 docs/product/funnel-audit-p1-cro-2026-05.md  |  19 ++++-
 3 files changed, 127 insertions(+), 10 deletions(-)

Fusion-Task-Id: FN-297
2026-05-13 07:28:34 +00:00
Fusion
80596a4ceb feat(FN-285): fix EMEX parallel decode race — always await second promise
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged:
- fix(FN-285): fix EMEX parallel decode race — always await second promise

Files changed:
apps/api/src/vehicles/vehicles.service.spec.ts | 63 ++++++++++++++++++++++++++
 apps/api/src/vehicles/vehicles.service.ts      |  5 +-
 2 files changed, 66 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-285
2026-05-13 06:56:18 +00:00
Fusion
6162a8f787 feat(FN-283): add PostHog events to TrialUrgencyBanner
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged:
- feat(FN-283): add PostHog events to TrialUrgencyBanner

Files changed:
apps/web/src/components/trial-urgency-banner.tsx | 34 ++++++++++++++++++++++--
 1 file changed, 32 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-283
2026-05-13 06:41:04 +00:00
Fusion
c0fef5921b feat(FN-282): add social proof impression tracking to testimonial and trust components
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged:
- feat(FN-282): add social proof impression tracking to testimonial and trust components

Files changed:
apps/web/src/routes/_auth.tsx                      | 42 +++++++++++-
 .../src/routes/dashboard/subscription/index.tsx    | 80 ++++++++++++++++++++--
 2 files changed, 117 insertions(+), 5 deletions(-)

Fusion-Task-Id: FN-282
2026-05-13 06:36:16 +00:00
Fusion
d855b564bf feat(FN-281): add payment_success and payment_failed PostHog events (+1 more)
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged:
- fix(FN-281): make result search param truly optional to fix typecheck
- feat(FN-281): add payment_success and payment_failed PostHog events

Files changed:
apps/api/package.json                              |    1 +
 apps/api/src/app.module.ts                         |    2 +
 apps/api/src/payments/payments.service.spec.ts     |   99 +-
 apps/api/src/payments/payments.service.ts          |   40 +
 apps/api/src/posthog/posthog.module.ts             |    9 +
 apps/api/src/posthog/posthog.service.ts            |   64 ++
 .../web/src/components/payment/payment-content.tsx |   24 +-
 apps/web/src/routeTree.gen.ts                      | 1110 ++++++++++----------
 apps/web/src/routes/dashboard/subscription/pay.tsx |   24 +-
 packages/config/src/index.ts                       |    4 +
 pnpm-lock.yaml                                     |   30 +
 11 files changed, 827 insertions(+), 580 deletions(-)

Fusion-Task-Id: FN-281
2026-05-13 06:30:58 +00:00
Fusion
7707443be6 feat(FN-278): add yearly_toggle_clicked PostHog events and 4-col skeleton grid
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
- Track yearly_toggle_clicked PostHog event on billing period toggle (monthly/yearly)
- Update skeleton grid from 2-col to 4-col layout with additional skeleton cards
- Add regression tests verifying analytics tracking and skeleton grid rendering

Fusion-Task-Id: FN-278
2026-05-13 05:55:25 +00:00
Fusion
cc2bfbe2b0 feat(FN-211): normalize typography on subscription page
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged:
- fix(FN-211): normalize typography on subscription page

Files changed:
apps/web/src/routes/dashboard/subscription/index.tsx | 20 +++++++++++---------
 1 file changed, 11 insertions(+), 9 deletions(-)

Fusion-Task-Id: FN-211
2026-05-13 05:30:19 +00:00
Fusion
fa49c20d09 feat(FN-263): normalize typography on subscription page (+2 more)
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged:
- feat(FN-263): normalize typography on subscription page
- chore(FN-263): apply Biome formatting fix for long className lines
- feat(FN-263): normalize typography on subscription page

Files changed:
.../src/routes/dashboard/subscription/index.tsx    | 116 +++++++++++----------
 1 file changed, 62 insertions(+), 54 deletions(-)

Fusion-Task-Id: FN-263
2026-05-13 04:51:56 +00:00
Fusion
f0bfe6ec8b feat(FN-280): add cancel-save flow PostHog events — cancel_flow_viewed and cancel_save_clicked
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged:
- feat(FN-280): add cancel-save flow PostHog events — cancel_flow_viewed and cancel_save_clicked

Files changed:
apps/web/src/routes/dashboard/subscription/index.tsx | 17 ++++++++++++++++-
 1 file changed, 16 insertions(+), 1 deletion(-)

Fusion-Task-Id: FN-280
2026-05-13 03:58:39 +00:00
Fusion
fb3fa6dff4 feat(FN-277): add @sase/shared resolve alias to vitest configs for both api and web (+1 more)
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged:
- docs(FN-277): update Pre-Merge QA Gate policy with explicit sub-bullets per spec
- fix(FN-277): add @sase/shared resolve alias to vitest configs for both api and web

Files changed:
.fusion/memory/MEMORY.md  | 14 +++++++++-----
 apps/api/vitest.config.ts |  6 ++++++
 apps/web/vitest.config.ts |  1 +
 3 files changed, 16 insertions(+), 5 deletions(-)

Fusion-Task-Id: FN-277
2026-05-13 02:07:02 +00:00
Fusion
6ae26e7a62 feat(FN-213): a11y audit — fix plan card double-focus trap, trust bar ARIA, color contrast
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged:
- feat(FN-213): a11y audit — fix plan card double-focus trap, trust bar ARIA, color contrast

Files changed:
apps/web/src/routes/dashboard/subscription/__tests__/index.test.tsx | 1 +
 1 file changed, 1 insertion(+)

Fusion-Task-Id: FN-213
2026-05-13 01:54:30 +00:00