fix(prefetch): recurse parts-catalogs trees to full depth
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled

isLeafLinkPath flagged EVERY pcat node as a leaf (any pcat: linkPath), so the
backfill worker fetched a mid-group folder's "parts" — a 1-level drill that
only revealed its sub-groups — and never recursed. Deep pcat trees (whose top
level is ~100% folders) were left one level shallow: no parts seeded, so the
new tree part-counts stayed 0 and the cross-tree catalog search (DB-only) found
nothing on a freshly decoded vehicle. Exactly why serkan's Ford Mondeo looked
like an empty catalog.

Use the captured hasSubgroups flag: a pcat node is a parts leaf only when it is
NOT a known parent group. Folders now queue a children job and the existing
recursion (queueCategoryJob → processChildren → getChildren) drills to full
depth (MAX_DEPTH=5), seeding parts at every leaf. The rolling backfill rescan
warms existing shallow vehicles wave by wave; new decodes warm deep via the
reactive schedulePrefetch. Rate limiter + PCAT_PACE + backlog guard keep it
bounded (all env-tunable to ramp).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 17:12:33 +03:00
parent 3482bb86e3
commit 79bbd1f1b3

View File

@@ -247,7 +247,7 @@ export class PrefetchWorkerService implements OnModuleInit, OnModuleDestroy {
await this.queueCategoryJob(child, vehicleId, source, 1);
queued++;
}
} else if (this.isLeafLinkPath(cat.linkPath, cat.source)) {
} else if (this.isLeafLinkPath(cat.linkPath, cat.source, cat.hasSubgroups)) {
// Leaf — check if parts already fetched
const [partCheck] = await this.db
.select({ id: parts.id })
@@ -488,14 +488,20 @@ export class PrefetchWorkerService implements OnModuleInit, OnModuleDestroy {
// ==================== Helpers ====================
private async queueCategoryJob(
cat: { id: string; linkPath: string | null; source: string; unavailable: boolean },
cat: {
id: string;
linkPath: string | null;
source: string;
unavailable: boolean;
hasSubgroups?: boolean | null;
},
vehicleId: string,
source: string,
depth: number,
): Promise<void> {
if (cat.unavailable) return;
if (this.isLeafLinkPath(cat.linkPath, cat.source)) {
if (this.isLeafLinkPath(cat.linkPath, cat.source, cat.hasSubgroups)) {
// Leaf — check if already has parts
const [partCheck] = await this.db
.select({ id: parts.id })
@@ -543,12 +549,25 @@ export class PrefetchWorkerService implements OnModuleInit, OnModuleDestroy {
}
}
private isLeafLinkPath(linkPath: string | null, source: string): boolean {
private isLeafLinkPath(
linkPath: string | null,
source: string,
hasSubgroups?: boolean | null,
): boolean {
if (!linkPath) return false;
// EMEX: Vehicle.aspx group nodes are parents to drill; Unit.aspx (hierarchical
// tree) and QuickDetails.aspx (legacy flat) leaves carry parts.
if (source === "emex") return !linkPath.includes("Vehicle.aspx");
if (source === "parts-catalogs") return linkPath.startsWith("pcat:"); // pcat: prefix = leaf
// parts-catalogs: EVERY node has a pcat: linkPath, so the old "pcat: prefix =
// leaf" rule mis-flagged every mid-group folder as a leaf — the worker fetched
// its "parts" (a 1-level drill that just revealed sub-groups) and never
// recursed, leaving deep pcat trees a single level shallow (no parts → empty
// part counts, dead cross-tree search). Use the captured hasSubgroups flag
// instead: a node is a parts leaf only when it is NOT a known parent group, so
// folders queue a children job and the recursion drills to full depth. Unknown
// flag (null, rare pre-migration rows) → treated as leaf, preserving the old
// 1-level behaviour for those.
if (source === "parts-catalogs") return hasSubgroups !== true;
// PL24 leaf indicators
return (
linkPath.includes("/bom/") ||