Commit Graph

590 Commits

Author SHA1 Message Date
cd4f282537 Merge pull request 'fix(notifications): correct subscription CTA path' (#96) from fix/subscription-cta-path into main
Reviewed-on: #96
2026-06-04 09:44:16 +00:00
b670d39d8f Merge pull request 'fix(jobs): gate lifecycle-email cron to prod host only' (#97) from fix/lifecycle-cron-prod-only into main
Reviewed-on: #97
2026-06-04 09:44:02 +00:00
semih
7e5e0ee446 fix(jobs): gate lifecycle-email cron behind prod-host check
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
dev.sase.tr was running the daily lifecycle-email cron (trial-ending,
win-back) against sase_dev, which is a seeded copy of the prod DB —
real customers were getting duplicate mails (one from prod, one from
dev). Mirror the isCatalogBackfillEnabled() gate so the lifecycle cron
only registers on the canonical prod host (COOLIFY_FQDN=sase.tr or
BETTER_AUTH_URL=https://sase.tr). LIFECYCLE_EMAIL_ENABLED env can force-
enable for staging testing. The else branch removes any stale scheduler
from Redis so a previously-registered cron stops firing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 12:38:10 +03:00
semih
eecc173ecc fix(notifications): correct subscription CTA path to /dashboard/subscription
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
The trial-ending / payment-success / payment-failed emails were pointing
to /dashboard/settings?tab=subscription which does not exist; the correct
path is /dashboard/subscription. Referral paths are unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 12:31:50 +03:00
306e8eb691 Merge pull request 'fix(vin): Q5 WMI brand map + Q6 PL24 nissan/van routing (RCA, verified)' (#95) from fix/vin-wmi-routing into main 2026-06-03 21:10:00 +00:00
83b2693af2 fix(vin): Q5 WMI brand map + Q6 PL24 nissan/van routing (RCA, verified)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Second batch from undecoded-vin-rca.md. Every entry verified (authoritative
WMI registry + prod sibling + VDS fingerprint + live PL24 probe) before adding
— a wrong brand label or a dead PL24 route both have real downside.

Q5 — WMI_BRAND_MAP (packages/shared, UX/telemetry only; does NOT change which
catalog decodes). Adds 9: W1V=Mercedes-Benz, VXF=Fiat, YAR=Toyota, NL1=Hyundai,
KPA=SsangYong, PL1=Proton, LSV=Volkswagen, LVV=Chery, ZCF=Iveco. Resolves the
RCA conflicts: VXF=Fiat (not Opel), YAR=Toyota ProAce (not Opel); NL1=Hyundai
(not TOGG — MY char predates TOGG). Deliberately NOT added: WF1/WAA/W0W (1-char
corruptions of WF0/WAU/WVW — mapping them would mislabel typo classes), VE1
(unresolved, deferred).

Q6 — PL24_WMI_SERVICE_MAP (real decode routing). Both target services are
proven live, so these only append WMIs to working routes (no dead-route →
isDecodeable()=true → 25s-budget/circuit-breaker hazard):
- SJN/VSK/MNT → nissan_parts (Sunderland/Spain/Thailand) — regression restore,
  dropped in the 757905f refactor; nissan_parts proven (JN1 decodes in prod).
- W1V → mercedesvans_parts (Sprinter/Vito) — retires the RCA "never tried" flag:
  mercedesvans_parts decodes WDF44770513186937 live today via the same P5 path.

Tests: shared getBrandFromWmi (adds + corruption-skips return null), pl24 route
map. Full api suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 23:55:22 +03:00
a361c12312 Merge pull request 'fix/vin-decode-rca-bugs' (#94) from fix/vin-decode-rca-bugs into main
Reviewed-on: #94
2026-06-03 20:07:35 +00:00
26fae98246 fix(emex): coerce proxy port env to number (boot-crash on real range)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
ConfigService.get<number>("EMEX_PROXY_PORT_START") returns the raw env
STRING; the port-pick arithmetic then string-concatenated it
(45 + "10001" = "4510001"), producing an out-of-range port that made
undici's `new URL` throw "Invalid URL" at EmexService construction —
crashing the entire API on boot.

A single-port range (823) happened to concat to a still-parseable "0823",
which masked the bug for months. It surfaced the moment the prod
EMEX_PROXY_PORT range was widened (823 -> 10001-10099) to let Q1's
per-request port rotation work: prod crash-looped until the env was
reverted. Coerce to a validated integer port (1-65535) with default
fallback so a real range is safe.

Regression test: constructing EmexService with string port env over a
real range must not throw.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 23:04:49 +03:00
a3ae90ec29 fix(vin-decode): 4 RCA-confirmed decode-chain bugs
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Root-cause analysis live re-decoded all 124 historically-undecoded prod
VINs; 38 already decode now. These 4 fixes target confirmed code bugs
that drop or mask real decodes (see undecoded-vin-rca.md):

Q2 — PL24 circuit breaker now only counts transient transport faults. A
definitive upstream negative (NotFound/BadRequest) no longer trips the
global 30s breaker that was starving PL24 for every subsequent VIN
(the sibling-VIN inconsistency in the report). Live-proven on VR7.

Q3 — previewVin / multi-candidate path no longer returns an empty
success: the pcat/emex candidate branches fill brandName (from catalogId
/ WMI), fixing the 6 "HTTP 200 with null brand+model" cases.

Q1 — EMEX fetch retries transient proxy failures with a FRESH ProxyAgent
per attempt (rotates the DataImpulse port; ~42% blip rate observed),
plus an opt-in direct fallback (EMEX_DIRECT_FALLBACK). HTTP answers are
never retried.

Q4 — VIN resolve cache keys namespaced by DECODE_CHAIN_VERSION and the
negative TTL drops 6h -> 30m, so a decode-chain fix self-heals stale
negatives on deploy instead of masking phantom-undecoded VINs for hours.
The admin cache-buster uses the same key builder.

Tests: 179 passed (+ new Q2/Q3/Q4 specs). typecheck + biome clean.

Deploy note: prod EMEX_PROXY_PORT_START/END are both 823 (single port);
widen to a real range (e.g. 10001-10099) in Coolify so Q1's port
rotation takes full effect.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 22:37:11 +03:00
9611d1c255 Merge pull request 'dev' (#92) from dev into main
Reviewed-on: #92
2026-06-03 17:22:06 +00:00
bb60a95ec3 perf(backfill): Phase-1 residue exclusion — stop re-picking dead vehicles (Tier 3)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Phase-1 of the backfill scan selects all zero-parts decoded vehicles every wave.
Genuine residue (VINs with no catalog data anywhere — model-indexed HKN,
EMEX-uncovered, etc.) stays zero-parts forever, so it filled the batch every
wave, re-attempting dead vehicles and starving the Phase-2 rolling rescan (its
cursor was stuck for a week).

Track a per-vehicle no-result counter (prefetch:noresult:<id>) incremented when a
prefetch attempt finishes with the vehicle still at zero parts (0 categories in
init, or 0 parts after the whole chain). tryPick skips vehicles past
PREFETCH_NORESULT_MAX (default 2) attempts; the counter has a TTL
(PREFETCH_NORESULT_TTL_DAYS, default 7) so a later catalog fix re-fills them.
Frees capacity for fillable vehicles and lets Phase-2 run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 19:15:57 +03:00
48aea65b5c perf(backfill): raise prefetch worker throughput, env-tunable (Tier 2)
After Tier 1 removed the self-throttle, the 5 jobs/min limiter + concurrency 1
became the bottleneck. Make throughput env-tunable so prod can ramp while
watching the fail rate:
- concurrency 1 -> 3 (PREFETCH_CONCURRENCY): parallelises emex/pl24 so a slow
  parts-catalogs job no longer head-of-line-blocks the queue.
- rate ceiling 5 -> 20 jobs/min (PREFETCH_RATE_MAX).
- parts-catalogs: drop the pathological cumulative index*20s enqueue delay (the
  Nth leaf of a vehicle waited N*20s); keep one per-job pace (PCAT_PACE_MS,
  default 15s, 0 to disable).
- PL24 09-18 scrape window now env-tunable (PREFETCH_PL24_START / _END).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 18:43:10 +03:00
d1ad33c26a Merge pull request 'perf(backfill): stop the prefetch worker from throttling itself (Tier 1)' (#88) from dev into main
Reviewed-on: #88
2026-06-03 15:29:06 +00:00
1e3953417e perf(backfill): stop the prefetch worker from throttling itself (Tier 1)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
The worker's own upstream fetches called touchActivity(), setting the
prefetch:activity:<source> cooldown key (TTL 300s) that checkCooldown then
honoured — so after each fetch the worker paused itself for up to ~5 minutes
(re-checking every 60s, ~5 empty cycles per key). At ~1 fetch / 5 min the
3387-job backlog needed ~6 days to drain.

- Wrap each worker job in an AsyncLocalStorage backfill context; touchActivity
  skips the cooldown key when invoked from the worker, so the cooldown reflects
  only real user requests (worker yields to users, never to itself).
- Cooldown TTL 300s -> 90s (a request 5 min ago isn't "active").
- checkCooldown pauses for the key's actual remaining TTL (one wait) instead of
  a fixed 60s re-check loop.

No extra upstream load — only removes the worker's self-imposed idle time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 18:23:54 +03:00
68433bc5f1 Merge pull request 'feat(analytics): thread category/vehicle names on remaining SchemaViewer routes' (#87) from dev into main
Reviewed-on: #87
2026-06-03 14:42:55 +00:00
d694bc8c35 feat(analytics): thread category/vehicle names on remaining SchemaViewer routes
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Completes the empty-catalog enrichment so parts_panel_viewed carries
human-readable names across ALL catalog flows, not just the decoded-vehicle
route. Now the '0 parca - kategoriye gore' breakdown gets named rows from
catalog-brand, emex, pcat and demo browsing too:
- emex: categoryName = data.group.name
- pcat: categoryName = activeSchema.name
- catalog-brand: categoryName = data.name, vehicleLabel = decoded brandName
- demo: categoryName = data.name, vehicleLabel (already computed)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 17:35:55 +03:00
c5cc757166 Merge pull request 'dev' (#86) from dev into main
Reviewed-on: #86
2026-06-03 14:20:40 +00:00
b56ea7c0fb chore(infra): wire POSTHOG_API_KEY/POSTHOG_HOST into api+worker compose env
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Server-side PostHog was disabled in prod (POSTHOG_API_KEY never reached the
container — it wasn't referenced in the compose environment blocks, and per
prior Coolify experience env only injects when referenced as ${VAR}). So all
server-side events (payment_success/failed/refunded, subscription_activated)
silently no-op'd. Reference the vars in both api and worker environment blocks
so a Coolify-set value reaches the container. POSTHOG_HOST defaults to the EU
ingestion host; empty key keeps it disabled (e.g. on dev, to avoid polluting
the prod project).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 16:10:21 +03:00
3bfefe6196 feat(analytics): canonical subscription_activated revenue event ()
No PostHog event carried realized revenue, and EFT/havale activations fired
nothing at all — so total paid revenue / MRR was unmeasurable (a Stripe DWH
connector alone would also miss EFT). activateSubscription is the shared
chokepoint for both Stripe (stripe.service) and EFT/manual (billing.service)
activation, so emit one canonical subscription_activated there with PostHog
revenue props: $revenue (major TRY), currency, mrr (yearly amortised /12),
plan, plan_id, brand_count, billing_period, method (looked up from the latest
payment row), referral_credit_days. Funnel steps keep their kuruş 'amount' but
intentionally carry no $revenue, so revenue isn't double-counted.

Unblocks trial->paid, MRR/ARPU and revenue-by-plan/channel across ALL payment
methods. Injected PostHogService (PostHogModule is @Global).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 16:02:30 +03:00
5d40626a55 Merge pull request 'fix(catalog): case-insensitive PL24 group-wid drill check' (#85) from dev into main
Reviewed-on: #85
2026-06-03 12:49:21 +00:00
c5f8288d71 fix(catalog): case-insensitive PL24 group-wid drill check
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
PL24 sub-group nav nodes were classified by linkWid.includes("Group")
(case-sensitive). That matched capitalised wids (subGroupsTable) but
missed lowercase ones — groupReferenceTable, groupTable, groupsTable
(~1157 leaf nodes in prod) — so those skipped the group-drill branch in
getCategoryWithPartsInner and the reference-resolution descent, falling
to the parts path (a wasted upstream fetch; the generic drill-on-empty
fallback then re-drilled them). Lowercasing the check routes these nav
nodes straight to children/loadError like their capitalised siblings.

Empty-catalog audit (2026-06-03) showed PL24 drives 61% of user-seen
'0 parça' views; pcat fake-leaves are effectively solved (1 case).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 15:40:39 +03:00
0decda8845 Merge pull request 'feat(analytics): enrich empty-catalog signal with vehicle/category names' (#84) from dev into main
Reviewed-on: #84
2026-06-03 11:19:26 +00:00
c7fad362a5 feat(analytics): enrich empty-catalog signal with vehicle/category names
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
parts_panel_viewed carried only opaque vehicle_id/category_id, so the
'0 parça' empty-catalog breakdown (and its rate-spike alert) couldn't say
WHICH vehicle/category was empty — useless for prioritising catalog backfill.

- parts-panel: add vehicle_label + category_name to parts_panel_viewed, and
  fire a new empty_catalog_cta_clicked (with the same context) from the
  empty-state button — a demand/abandonment signal per vehicle+category.
- schema-viewer: forward the two optional props to PartsPanel.
- vehicle category route: pass the existing vehicleLabel + data.name.

Best-effort labels (undefined on routes not yet threaded); parts_count
unchanged so existing insight/alert keep working.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:41:34 +03:00
f233080623 Merge pull request 'fix(analytics): stop orphaning anonymous person on load, breaking signup attribution' (#83) from dev into main
Reviewed-on: #83
2026-06-03 08:05:00 +00:00
fa6937bb5f fix(analytics): stop orphaning anonymous person on load, breaking signup attribution
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
__root identify effect called posthog.reset() whenever `user` was falsy —
which includes the brief window while the session resolves on every page
load. After the signup hard-redirect this rotated the anonymous distinct_id,
orphaning the pre-signup anonymous person that carries `user_signed_up` and
first-touch `$initial_utm_*`. The later identify() then merged a fresh, empty
anon id, so signups never linked to trial/payment (only ~16% stitched) and
channel attribution read "(none)" for 100% of signups.

- __root.tsx: gate the effect on isLoading and only reset() on a real
  identified -> anonymous transition (logout), tracked via a ref. Logout
  still resets via dashboard handleSignOut.
- register.tsx: identify() within the still-active anonymous session before
  firing user_signed_up, so the anon->identified merge carries $initial_utm_*
  and attributes the signup (email flow; Google is handled on OAuth return).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:54:08 +03:00
ca50a3e176 Merge pull request 'dev' (#82) from dev into main
Reviewed-on: #82
2026-06-03 00:03:13 +00:00
2a26890d33 feat(web): escape Instagram/FB in-app browsers to the system browser
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Ad traffic from Instagram/FB lands in their in-app WebView, where Google
OAuth (signIn.social google) returns 403 disallowed_useragent — so the
30-day-trial signup with Google is broken for paid traffic. Also costs
password autofill, persistent session, and clean attribution.

- Android: hard-escape via intent:// (no package= → default browser),
  preserving the full URL so UTM/fbclid survive the hop.
- iOS: Apple allows no programmatic escape — dismissible banner guiding
  "••• → Safari'de Aç" with a copy-link fallback.

Mounted in __root so it runs on the landing page. Pure UA/intent helpers
unit-tested (in-app detection + URL preservation + no false positives).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 02:55:46 +03:00
8551cef2e1 chore(web): drop iabjs:// in-app browser noise via denyUrls
Android Instagram in-app browser injects scripts under the iabjs:// scheme
(navigation_performance_logger_android) that throw bridge errors like
"Java object is gone" during beforeunload. denyUrls on /iabjs:\/\// drops
the whole family at the source, message-independent. Also added the message
to ignoreErrors as a fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 02:48:21 +03:00
1a2369a5ec chore(web): ignore Instagram in-app browser window.webkit noise in Sentry
iOS Instagram/Meta in-app browsers inject their own JS that probes the
WKWebView bridge (window.webkit.messageHandlers) and throws when absent.
It surfaces via our global onerror but is not our code — pure noise that
scales with Instagram ad traffic. Added to Sentry ignoreErrors. The
existing issue (124551981) was muted in the Sentry UI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 02:41:00 +03:00
88beae4930 Merge pull request 'dev' (#81) from dev into main
Reviewed-on: #81
2026-06-02 22:49:49 +00:00
5d8e4023a5 fix(web): stop dropping UTM params from PostHog pageviews
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
capturePageView sent `$current_url: origin + path`, stripping the query
string — so every Meta-ad UTM was lost and all paid traffic was
mis-bucketed as "direct" (PostHog saw 9 facebook visitors vs Meta's
~1450 LPV over 21 days). Send the full window.location.href so PostHog
parses $utm_* / $initial_utm_*. Also flip person_profiles to "always"
so anonymous ad visitors get a first-touch profile (volume ~34k/mo, well
within the 1M free tier). Adds a regression test guarding the query string.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 01:44:50 +03:00
b7dface50c style(web): make "bk. tablo" reference read clearly as a link
Always-underlined, semibold, primary, larger text + arrow, with a "Tabloya
git:" verb so the cross-reference reads unmistakably as a clickable link
instead of muted inline text.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 20:29:03 +03:00
642139de9c fix(pl24): detect "bakınız tablo, konum:" reference phrasing too
PL24 translates "see table" two ways — "bk. tablo:" and "bakınız tablo,
konum:". Detection only matched the first, so the latter rows (e.g. evaporator
housing → 820-020) stayed dead. Broaden the name regex to match either, and
strip both phrasings from the displayed label. The code-in-remark gate still
prevents flagging real parts that merely mention "tablo".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 20:20:30 +03:00
c440252aa3 feat(pl24): on-demand drill to resolve unseeded "bk. tablo:" references
When a reference's target illustration isn't seeded yet (load-time index
miss → categoryId null), clicking it now calls a new resolve endpoint that
drills the relevant main-group root (its external_id = the code's first
digit; the illustration is a direct child) and re-resolves. One PL24 call in
the common case, bounded + cached; falls back to pre-filled search if not
found. UI shows a spinner on the button while drilling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 19:41:02 +03:00
232c9ccc7a feat(pl24): make "bk. tablo:" BOM cross-references navigable
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>
2026-06-02 18:55:42 +03:00
83033a160d Merge pull request 'dev' (#80) from dev into main
Reviewed-on: #80
2026-06-02 12:32:58 +00:00
6bcb7ac2a5 fix(web): instant loading feedback on category node clicks (#73)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Drilling the category tree gave a 0–300 ms feedback gap between click and
the next render — no disabled state, no visible change — so users re-tapped
(one real session: 9 of 22 clicks were rage clicks, 158 s, zero results).

Add an immediate, prominent pending state on all three category views:

- grid: track the clicked card in `pendingId`; the button goes
  disabled + aria-busy and a 28px centred spinner overlays the card (both
  card variants) until the route swap unmounts the grid.
- tree: disable the expand button + aria-busy while children fetch; grow
  the chevron-slot spinner 14px → 20px and tint it primary.
- columns: disable the row + aria-busy while the child column lazy-fetches;
  same 20px primary spinner.

Tests: grid click paints disabled+aria-busy on the tapped card only;
tree/columns rows lock + show a spinner while the fetch is in flight.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 15:15:04 +03:00
e73b899e11 fix(web): parts table spills below the footer on mobile schema page (#76)
The parts-panel wrapper used max-h-[500px] on mobile, but a max-height parent
does not give the panel's `h-full` a definite height to resolve against, so
the panel resolved to the table's full intrinsic height (~2500px) and, because
the wrapper's overflow is visible, spilled out the bottom — rendering the whole
parts list a second time below the signup CTA and the page footer (reported
"ürün kodları → kaydol kartı → footer → ürün kodları" broken structure).

Drop the mobile height cap so the parts list flows in normal document order and
the page scrolls through it (the intended mobile pattern; also resolves the
triple-scroll-trap noted in #76). Desktop is unchanged: the 40% column still
sits inside the md:h-[700px] row with its own inner overflow scroll.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 14:55:30 +03:00
e4b13c6abb fix(web): mobile schema page traps users above the parts list (#76)
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>
2026-06-02 12:34:47 +03:00
ed01ee44d2 Merge pull request 'dev' (#79) from dev into main
Reviewed-on: #79
2026-06-01 23:37:52 +00:00
00f7edbc15 fix(pl24): Volvo vin-image-board BOM parts + decode subgroup name entities
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Volvo's vin-image-board.action ships its BOM inline as partno= tc-data-row
rows (no pncHierCode / json-vin-bom-detail), so the Ford VIN-BOM parser
returned 0 parts. Fall back to parsePsaBomParts (partno= rows) when no pnc
rows are found. Also decode HTML entities (&Ouml; &quot; …) in scraped Volvo
subgroup names. Completes the Volvo chain: group1→group2→illustration→parts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 02:30:54 +03:00
c46695d320 fix(pl24): Volvo drill — keep openVinDialog=false links + image-board leaves
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Two bugs in the vin-group.action subgroup scrape: (1) the filter dropped any
href containing "openVinDialog", but the real sub-group links carry
openVinDialog=false (only the VIN-dialog crumb is =true) — so every child was
discarded; (2) the deepest group level lists its illustration leaves as
vin-image-board.action links, which weren't extracted. Now match both deeper
vin-group.action?groupN= and vin-image-board.action anchors, and only drop the
openVinDialog=true crumb. Completes Volvo group1→group2→illustration→parts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 02:24:59 +03:00
f5c3198a6e fix(pl24): demo-page retry in fetchP4Page + Volvo drill diagnostics
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
PL24 serves a stripped NOT_LOGGED_IN_DEMO page (no groups/parts) when the
service token is stale. decodeVinForService retries on this, but drill paths
(fetchSubGroupsByPath/fetchPartsByPath) reach upstream only via fetchP4Page,
which didn't — so Volvo subgroup drilling parsed empty demo pages. Retry once
with fresh auth on a demo page. Adds a Volvo-drill diagnostic log.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 02:18:04 +03:00
7208069a80 fix(pl24): prefix basePath for relative P4 hrefs in fetchP4Page
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Volvo vin-group.action categories store hrefs relative to the catalog dir
(e.g. "vin-group.action?group1=…"). fetchP4Page did `${baseUrl}${url}`,
collapsing to "partslink24.comvin-group.action" → ENOTFOUND. Prefix the
service basePath when the path is relative. Fixes Volvo subgroup drilling
for existing (relative) stored linkPaths without a re-decode.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 02:12:59 +03:00
497aba24ab fix(ui): repair Turkish labels + hero preview model name
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Two cosmetic fixes the founder flagged in the same browse session:

* parts-panel.tsx — the catalog parts table was missing Turkish
  diacritics on every static label, while the actual part names rendered
  correctly. Looked like an encoding bug; was actually hard-coded ASCII
  strings. Repaired: Parcalar→Parçalar, parca listeleniyor→parça
  listeleniyor, Bu kategori icin parca bulunamadi→Bu kategori için
  parça bulunamadı, Yukleniyor→Yükleniyor, Parca Adi→Parça Adı,
  Geri don→Geri dön. OEM Kodu was already correct.

* index.tsx hero preview — pl24 ships model names with trailing
  duplicates ("Golf 1,6 GOLF"), and the card stitched year right next
  to the engine "(2003 4 silindirli…)" with no clear separator. Now:
  cleanModelName collapses the dup ("Golf 1,6"), year wraps in
  parentheses as a softer secondary element, and the visual flex gap
  guarantees breathing room before the engine pill.
2026-06-02 02:11:35 +03:00
10dcf2d6ae fix(parts): dedupe ingest path + clean ~108k duplicate rows
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Backfill + reactive prefetch can re-drill the same category multiple
times, and the parts insert path had no dedupe guard. Result: 8.2%
duplicate rows on pl24, 14.5% on parts-catalogs, and 33.7% on emex —
~108k extra rows across 7,571 categories on 224 vehicles. Every drilled
catalog page rendered each part twice (the Tampon example: 32 rows for
19 distinct OEMs).

* Migration 0010 — phase 1 deletes existing dupes preserving the
  oldest row per (vehicle_id, category_id, oem_code, name, position)
  group; phase 2 adds a UNIQUE INDEX over the same tuple with NULLS NOT
  DISTINCT (PG 15+) so null position/vehicle_id collapse like equal
  values rather than each counting as its own "distinct" row. Idempotent
  CREATE UNIQUE INDEX IF NOT EXISTS so the runner is safe to re-apply.
* All five insert(parts).values(...).returning() call sites
  (parts.service, categories.service ×3, catalog.service) get
  .onConflictDoNothing() so future re-drills no-op instead of erroring
  on the new constraint. `.returning()` continues to surface only the
  newly-inserted rows; existing logs read `Stored N parts` as actual
  net insertions, which is what we want.

Dry-run on dev DB: 524,540 → 425,186 parts (99,354 dupes deleted), index
created cleanly. Same delta expected on prod (~108k drop).

drizzle-orm 0.41 doesn't expose .nullsNotDistinct() on the index builder
so the constraint is owned by raw SQL — see the inline comment in the
parts schema and the migration file. Future schema generators should NOT
try to drop or rewrite this index.
2026-06-02 02:08:57 +03:00
d792bf0efb fix(pl24): Volvo subgroup drilling via vin-group.action HTML
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Volvo's VIN catalog is a 3-level vin-group.action?group1=…[&group2=…] HTML
tree; PL24's json-vin-*-group.action JSON endpoints now 404. Add HTML
subgroup extraction (keep links one group-level deeper than the current
path) in ford-legacy fetchSubGroupsByPath, and route vin-group.action?group1=
nodes through getChildren (drill-first, fall back to parts) in
getCategoryWithParts. Recovers Volvo group1→group2 navigation. Leaf parts
handled separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 02:08:12 +03:00
316a5e014c feat(backfill): widen scrape window for pcat + double batch size
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Backfill was sweeping only 20 vehicles/hour and skipping pcat outside
09:00–19:00 Istanbul, leaving the catalog backlog (115 pl24 + 63 pcat +
27 emex zero-parts vehicles) crawling forward at ~528 parts/day and the
biggest user vehicle (Toyota Corolla 2026) unchanged across 24h.

Two related changes — both unblocked by the Redis-persisted warm JWT pool
(aa4d055 + dcf7e06) which keeps pcat captures alive 24/7 instead of
needing the 09:00–19:00 office-hours assumption:

* isWithinTimeWindow: parts-catalogs no longer gated — the warm pool +
  Redis hydration cover the cold-start case the old office-hours rule
  was working around. PL24 keeps its 09:00–18:00 window because the
  upstream rate-limit is still tighter outside it.
* BACKFILL_BATCH_SIZE 20 → 40 — twice as many vehicles per wave, still
  protected by MAX_BACKLOG=1000 self-throttle and per-source cooldown
  (prefetch:activity:<source>) so live-user traffic still gets priority.

Combined effect: pcat goes from ~10h/day to 24h/day, batch doubles —
roughly 3× backfill throughput. Worst-case proxy spend tracked by the
DataImpulse daily cap; if a wave saturates upstream the rate-limit
handler (c7e59b9) defers without burning attempts.
2026-06-02 02:00:16 +03:00
0f647b3931 fix(pl24): recover Volvo categories — stop vin-group.action?group1= shadowing
Volvo (VIN-indexed legacy catalog) ships its real top groups as
vin-group.action?group1=… in the vin-group HTML. PL24's
json-vin-main-group.action endpoint now 404s, so decode falls back to the
HTML scrape — but parseP4NavigationCategories AND the seed-time NAV_CRUMB
filter both blanket-exclude vin-group.action, dropping every real Volvo
group → 0 categories. Exclude vin-group.action as a crumb only when it
lacks group1=. Confirmed upstream: 7 real groups (Frenler, Elektrik
sistemi, …) present in the HTML for YV1AS7050A1118639.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 01:51:19 +03:00
dad2060bec Merge pull request 'dev' (#78) from dev into main
Reviewed-on: #78
2026-06-01 22:41:51 +00:00