refactor(decode): keep decode-source details server-side, opaque candidate keys
The multi-candidate decode response leaked provider internals (source name,
pcat car ids, EMEX _ssd/_vid/_quickGroupsUrl/catalogId) and made the client
carry them between requests: the frontend stored candidateSource and echoed
pcatCarId/emexCarIndex back on selection.
Now the candidate list returned to the client carries only display fields
(name, description, parameters) plus an opaque key, and the provider mapping
is stashed in Redis (vin:candidates:*, 30m TTL, resolve-cache fallback). The
pick request sends just { vin, candidate }. Legacy pcatCarId/emexCarIndex
body params still work for already-loaded bundles.
Also drops `source` from the public /vehicles/preview response — no consumer
used it, and provider names must never be public (same policy as
teaser-stats).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 2077a9724a)
This commit is contained in:
@@ -142,10 +142,10 @@ function SearchPage() {
|
||||
const pendingVinRef = useRef<string | null>(null);
|
||||
const didInitFromUrlRef = useRef(false);
|
||||
|
||||
// Vehicle candidate selection (PartsCatalogs or EMEX multi-result)
|
||||
// Vehicle candidate selection (multi-result decode). Candidates carry only
|
||||
// display fields + an opaque key; the decode source stays server-side.
|
||||
const [candidates, setCandidates] = useState<any[] | null>(null);
|
||||
const [candidateVin, setCandidateVin] = useState("");
|
||||
const [candidateSource, setCandidateSource] = useState<"parts-catalogs" | "emex" | null>(null);
|
||||
const [selectLoading, setSelectLoading] = useState(false);
|
||||
|
||||
// Live preview state
|
||||
@@ -285,12 +285,10 @@ function SearchPage() {
|
||||
if (data.candidates && Array.isArray(data.candidates)) {
|
||||
setCandidates(data.candidates);
|
||||
setCandidateVin(cleanVin);
|
||||
setCandidateSource(data.source ?? "parts-catalogs");
|
||||
candidatesShownAtRef.current = performance.now();
|
||||
capture("vin_decode_candidates", {
|
||||
vin: cleanVin,
|
||||
count: data.candidates.length,
|
||||
source: data.source,
|
||||
response_time_ms: responseTimeMs,
|
||||
query_source: querySourceRef.current,
|
||||
});
|
||||
@@ -461,16 +459,14 @@ function SearchPage() {
|
||||
? Math.round(performance.now() - candidatesShownAtRef.current)
|
||||
: null;
|
||||
try {
|
||||
const payload: Record<string, unknown> = { vin: candidateVin };
|
||||
if (candidateSource === "emex") {
|
||||
payload.emexCarIndex = Number.parseInt(carId, 10);
|
||||
} else {
|
||||
payload.pcatCarId = carId;
|
||||
}
|
||||
const data = await api.post<any>("/vehicles/decode", payload);
|
||||
// The carId is the opaque candidate key from the decode response — the
|
||||
// server maps it back to the provider-specific selection.
|
||||
const data = await api.post<any>("/vehicles/decode", {
|
||||
vin: candidateVin,
|
||||
candidate: carId,
|
||||
});
|
||||
capture("vin_decode_candidate_selected", {
|
||||
vin: candidateVin,
|
||||
source: candidateSource,
|
||||
carId,
|
||||
vehicle_id: data.id,
|
||||
selected_index: selectedIndex,
|
||||
@@ -479,7 +475,6 @@ function SearchPage() {
|
||||
});
|
||||
candidatesShownAtRef.current = null;
|
||||
setCandidates(null);
|
||||
setCandidateSource(null);
|
||||
navigate({
|
||||
to: "/dashboard/vehicles/$id",
|
||||
params: { id: data.id },
|
||||
@@ -488,7 +483,6 @@ function SearchPage() {
|
||||
const message = err instanceof ApiError ? err.message : t("search.errorGeneric");
|
||||
setError(message);
|
||||
setCandidates(null);
|
||||
setCandidateSource(null);
|
||||
toast.error(t("search.candidateSelectFailed"));
|
||||
} finally {
|
||||
setSelectLoading(false);
|
||||
|
||||
@@ -160,7 +160,7 @@ function ServiceTestPage() {
|
||||
try {
|
||||
const data = await api.post<any>("/vehicles/decode", {
|
||||
vin: candidateVin,
|
||||
emexCarIndex: Number.parseInt(carId, 10),
|
||||
candidate: carId,
|
||||
});
|
||||
setCandidates(null);
|
||||
if (data.id) {
|
||||
|
||||
Reference in New Issue
Block a user