getCategoryTree cached the built tree for 3600s unconditionally, and the
read guard `if (cached)` treats an empty array as a hit. So a single
transient decode/proxy failure (emex via the flaky DataImpulse pool) seeded
0 categories and poisoned the catalog with an empty tree for a full hour,
even after the source recovered. Cache an empty tree for only 60s — it
self-heals on the next request post-recovery while still throttling
re-decode during a genuine outage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
emex catalogs were seeded from the flat QuickGroups taxonomy (~351
unsearchable leaves on one page). Switch the seeder to the Vehicle.aspx
OEM-microfiche tree (top groups -> sub groups -> Unit.aspx leaves), which
also reaches units that have no QuickGroups gid (e.g. the torque
converter, OEM 5205547 on the Fiesta).
- emex-tree.parser.ts: parseVehicleTree / parseUnitLeaves — pure,
GET-based (per-href ssd, no browser/postback). Unit-tested vs fixtures.
- emex.service: fetchVehicleTree + drillVehicleNode; all 3 decode methods
now populate categoryTree; Unit.aspx leaves fetch parts directly.
- categories.service: revive the dormant tree-insert branch with group
markers (linkWid="emex-group" / hasSubgroups), add an emex getChildren
lazy-drill branch + emex-group parent guard, tighten leaf detection,
disambiguate colliding sub-group names.
- prefetch: emex Vehicle.aspx = parent to drill; Unit/QuickDetails = leaf.
No DDL and no frontend changes — the generic tree UI renders it like
pl24/pcat. Existing flat emex categories re-seed lazily once deleted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PSA (Peugeot/Citroën/DS) VIN decode was systemically broken: the catalog
vin-group page returns all families unfiltered, so decode fell back to the first
family/salesType (a manual base variant) — yielding "{Brand} {VIN}" model names,
empty transmission, wrong model year, and manual-only parts trees (automatic
gearbox parts missing). Reported for a 1999 Peugeot 106 automatic shown as a 2029
manual with no automatic parts.
- New self-contained PL24PsaService: consumes PL24's FI flow (vin.action →
hintstoken → FI page → json-vin-main-groups → json-vin-illustrations →
vin-image-board). Reads model/year/transmission from the FI identification
table; builds the VIN-indexed parts tree (correct per actual VIN). Does not
touch Ford/Volvo/Nissan/Opel/Hyundai-Kia/Fiat.
- Orchestrator + categories.service route PSA VIN decode/drill to the new service.
- Cycle-correct extractModelYear in @sase/shared (X→1999, not 2029): resolve the
30-yr VIN year code to the most-recent plausible year (≤ now+1); dedupe 6 copies.
Validated live against 13 already-decoded PSA VINs: 12/13 full trees with real
model/year/transmission; automatics correctly detected (106 BVA, 206 AL4, 3008 BVA8).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
Easier to see [source-db hit pcat/emex] lines while dev verifies coverage.
Can be downgraded back to debug once we've measured prod hit rates.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Initial design routed emex lookups through vehicles.rawData.ssd → dump vehicles
→ vehicle_parts. Smoke test against prod ssd values: 0 / 10 matched. EMEX
regenerates the SSD on every decode session, so sase's stored SSD never
matches the SSD the dump scraper recorded for the same physical vehicle.
Pivot to a catalog-wide bridge that actually works:
catalogs.code ↔ vehicles.rawData.catalogCode (e.g. "RENAULT201910")
part_groups.group_id ↔ categories.externalId (e.g. "11754")
→ parts via vehicle_parts.group_id (dump's parts.group_id is 100% NULL)
Verified coverage on prod's 8287 unique (catalogCode, gid) pairs: 25/26
catalog codes resolve, 7178 pairs hit a part_group (87%), 5919 of those
return actual parts via vehicle_parts (~71% net). Tradeoff: returns all
parts in the (catalog, group) across every variant in the catalog, so the
result is slightly noisier than the live per-vehicle scrape. Acceptable —
parts overlap heavily and the upstream-call savings outweigh the noise.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an optional local-dump lookup layer in front of the live PartsCatalogs and
EMEX scrapes. When enabled, getCategoryWithPartsInner queries a Postgres
(pcat) or MariaDB (emex) dump for the requested schema/group's parts and
hotspots; on miss it falls through to the existing upstream call unchanged.
Hits avoid the live API, its cooldown, and its rate-limits — direct DB latency.
- New CatalogSourceDbModule with PcatSourceDbService + EmexSourceDbService
(raw SQL, no Drizzle schema modeling — dump shapes are frozen snapshots).
- pcat lookup keys on schema_images.schema_ext_id (the dump's column that
matches sase's pcat groupId; observed ~7% hit rate on prod's 6596 unique
groupIds, of which ~10% have schema_parts → ~3-5% net parts coverage).
Joins schema_parts → parts directly; the dump's part_groups+part_group_items
linkage covers 0 of our hits, so we skip that path entirely.
- emex lookup uses (catalog_id, ssd) → vehicles.id then (vehicle_id, group_id)
→ vehicle_parts → parts + part_images. The ssd is already persisted into
vehicles.rawData.ssd by the existing emex.mapper, no extra capture needed.
Gated behind CATALOG_SOURCE_DB_ENABLED + PCAT_SOURCE_DB_URL / EMEX_SOURCE_DB_URL.
All three default unset, so this commit is a no-op until prod env is configured.
Adds mysql2 dep for the MariaDB client.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The vehicle page search previously only filtered category names at the
currently rendered level. Add a server-side cross-tree search over what's
already drilled into the DB.
New GET /categories/search/:vehicleId?q= returns two sections:
- categories: name-matched leaves UNION the leaf categories that contain a
matching part (with hit count). "fren balatası" matches no leaf by name —
the pads are parts under leaves like "Disk freni" — so the union surfaces
the right leaves.
- parts: parts matching every token on name, or the raw query on oem_code,
with OEM + leaf + breadcrumb.
Pure DB read (no upstream drill); a treeIncomplete hint is returned when the
vehicle's tree looks barely drilled. Frontend adds a debounced search box on
the vehicle page that hides the normal browse while active.
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>
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>
parts-catalogs (pcat) catalogs are deep trees, but every pcat node was
stored as an apparent leaf (pcat: linkPath, no children) until drilled.
Opening an intermediate group called parts2, which returns HTTP 400
("The list of parts is empty"); the drill-to-children fallback only fired
on that exact error string, so any other failure (cold JWT capture,
timeout, 5xx) left both parts and children empty → the UI rendered
"0 parça / bulunamadı" on a node whose parts live 1-3 levels deeper.
#1 Persist the groups2 hasSubgroups/hasParts flags on categories
(new nullable columns, migration 0007) and use them to classify:
- getCategoryWithParts routes a known parent (hasSubgroups, !hasParts)
straight to getChildren, never calling parts2.
- enrichWithSchemaImages treats flagged parents as expandable, flagged
parts-leaves as leaves, and only falls back to the "any pcat: is a
leaf" heuristic for pre-migration rows (flags null).
#2 Make discovery resilient: drill to sub-groups whenever the parts fetch
produces nothing (empty 200 OR a thrown error), not only on HTTP 400.
Mark a node unavailable only on a definitive HTTP 400 with no
sub-groups; transient failures are left for a later retry.
Verified live on prod (VW Passat, "Süspansiyon Çarpanlar"): full path is
Süspansiyon Çarpanlar → Süspansiyon → Süspansiyon 1…5 → 74 parts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PCAT was missing parity with EMEX in two places:
- Part `description` (notice) was stored raw English alongside translated `name`.
Now batched into translateMany so users see Turkish notices.
- Vehicle body/engine/transmission attrs from VIN decode were raw upstream
values. Wire up the existing emex.mapper dictionaries on the PCAT
single-car, PCAT resolveById, and EMEX single-vehicle result paths.
- emex.mapper translateToTurkish now falls back to the original term on
dictionary miss instead of null — upstream values are heterogeneous
(engine codes, multi-word descriptors); losing them was worse than
leaving them untranslated.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
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>
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>
- CATALOG_MAP'e ~40 yeni WMI eklendi (Mitsubishi, Nissan, Volvo, vb.)
- brandFromCatalogCode listesi 55 DB kataloğuna genişletildi
- brand="UNKNOWN" olsa bile catalogCode varsa EMEX sonucu kabul edilir
- emex-catalog: linkPath'li kategorilerde parçalar DB'den yüklenir (scraper bug fix)
- VIN cache: emex_vehicle_vins tablosu ile tekrar VIN'lerde emexdwc.ae atlanır
- getCategoryTree Step 0: VIN cache lookup (~10ms)
- Match sonrası otomatik VIN cache doldurma
- Proaktif cache: ilk decode sonrası matchByName + saveVinCache
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When EMEX DB has no vehicle match (Ford EU, Mercedes etc.), instead of
falling back to full on-demand VIN decode (~8s), use the VIN-decode SSD
directly with QuickGroups.aspx?c={catalog}&vid=0&ssd={vinSsd} (~2-3s).
Tested: Ford Focus CB4 VIN SSD returns 376 categories via QuickGroups.
New step 3.5 in the cascade:
1. pathData → matchByName (DB-only, ~50ms)
2. emexVehicleName → matchByName (DB-only, ~50ms)
3. matchBySsd wizard walk (DB, ~2-3s)
3.5. VIN SSD → QuickGroups shortcut (~2-3s) ← NEW
4. Fallback → full on-demand scrape (~8s)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Redesign emex_* tables from vehicle-centric to catalog-centric structure with
junction tables (emex_vehicle_group_links, emex_vehicle_part_links). Migrate
92M+ rows from emex DB via postgres_fdw. Add EmexCatalogService for browse
and VIN pre-scraped matching, with Redis caching. Frontend catalog page now
has PL24/Emex tabs with full drill-down routes (brand → vehicle → group → parts).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Integrate external parts catalogs API with auth service, add BullMQ-based
catalog prefetch worker for background data caching, expand vehicles service
with shared vehicle support, and add vehicle select modal to frontend.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 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>
- Move trial creation from auth hook to dedicated /subscriptions/trial endpoint
with eligibility checks (3-day Full Paket trial)
- Add animated onboarding progress (Remotion) with confetti on completion
- Register redirects to subscription page with welcome flow
- Add Google social login/signup support with config injection
- Improve hotspot overlay visibility in dark mode
- Show "Panele Git" on landing page when authenticated
- Turkish translations for API error messages
- Add toast utility wrapper, UUID generation for auth IDs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add PL24 Ford legacy service for fordt_parts architecture
- Refactor EMEX to use persistent browser pool instead of per-call instances
- Make vehicle decode resilient: fallback to PL24 when Corgi doesn't recognize VIN
- Add collapsible sidebar with persistent user preference
- Improve brand access guard and categories service
- Add debug/test scripts for VIN e2e testing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Enrich category children with batch-fetched schema images and leaf detection
on the API side. Display thumbnails in grid cards with shimmer loading
placeholders and background prefetching. Add TanStack Query caching for
category children. Fix insert race condition with onConflictDoNothing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace browser-based Puppeteer scraper with standalone scraper wrapper.
Add EMEX as fallback source for categories when PL24 is unavailable.
Update vehicle decoding to use new synchronous EMEX API.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add WV1/WV2/WV3 WMI codes for Volkswagen commercial vehicles (Ticari)
mapped to vn_parts service. Skip BOM links in category traversal since
they are leaf categories returning parts, not subgroups.
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>
Add 11 new test files covering roles guard, brand-access guard, users,
brands, plans, payments, vehicles, categories, parts, referrals, and
admin services. Total test count increases from ~52 to 164, all passing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>