feat: CarBrandLogo component + PL24 image pre-download + catalog index fix
- Add CarBrandLogo component with SVG assets for 11 brands (Alpine, Citroën, Cupra, Dacia, MAN, Opel, Peugeot, Renault, SEAT, Škoda, Suzuki); falls back to logoUrl prop then initial-letter avatar - Use CarBrandLogo across brand-selector, catalog index, subscription page, dashboard home, and vehicle detail page - PL24: pre-download schema image buffer while auth headers are fresh to avoid token expiry on cached URLs; strip buffer before Redis caching - PL24: fetchVehicleList tries de account first, falls back to main token - DB: widen catalogVehicles unique index to include serviceName column - CSS: add Tailwind v4 dark variant declaration Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -256,7 +256,7 @@ export const catalogVehicles = pgTable(
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("catalog_vehicles_source_vid_idx").on(table.source, table.serviceVehicleId),
|
||||
uniqueIndex("catalog_vehicles_source_svc_vid_idx").on(table.source, table.serviceName, table.serviceVehicleId),
|
||||
index("catalog_vehicles_brand_name_idx").on(table.brandName),
|
||||
index("catalog_vehicles_service_name_idx").on(table.serviceName),
|
||||
],
|
||||
|
||||
@@ -315,6 +315,9 @@ export class PL24Service {
|
||||
const groupName = crumbs[crumbs.length - 1]?.name || "";
|
||||
const illustrationId =
|
||||
linkPath.match(/illustrationId=(\d+)/)?.[1] || "";
|
||||
// Temporary: log raw images field to diagnose 404 issue
|
||||
const rawImages = (data?.data as any)?.images || data?.images;
|
||||
if (rawImages) this.logger.log(`[imgdbg] images field: ${JSON.stringify(rawImages).substring(0, 500)}`);
|
||||
const imageData = this.extractImageData(data);
|
||||
|
||||
let parts = this.parsePartsResponse(data);
|
||||
@@ -340,7 +343,19 @@ export class PL24Service {
|
||||
hotspots: imageData.hotspots,
|
||||
};
|
||||
|
||||
await this.redis.setJson(cacheKey, result, 3600);
|
||||
// Pre-download image buffer while auth headers are fresh (avoids token expiry on cached URL)
|
||||
if (result.schemaImageUrl) {
|
||||
const downloaded = await this.tryDownloadImageBuffer(result.schemaImageUrl, headers);
|
||||
if (downloaded) {
|
||||
result.schemaImageBuffer = downloaded.buffer;
|
||||
result.schemaImageContentType = downloaded.contentType;
|
||||
result.schemaImageUrl = undefined; // buffer takes precedence
|
||||
}
|
||||
}
|
||||
|
||||
// Cache without buffer (binary data not suited for Redis)
|
||||
const { schemaImageBuffer: _buf, ...toCache } = result as any;
|
||||
await this.redis.setJson(cacheKey, toCache, 3600);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
@@ -1295,6 +1310,36 @@ export class PL24Service {
|
||||
|
||||
// ==================== PRIVATE: Image extraction ====================
|
||||
|
||||
private async tryDownloadImageBuffer(
|
||||
imageUrl: string,
|
||||
headers: Record<string, string>,
|
||||
): Promise<{ buffer: Buffer; contentType: string } | null> {
|
||||
this.logger.log(`[imgdl] Downloading image: ${imageUrl}`);
|
||||
try {
|
||||
const response = await fetch(imageUrl, {
|
||||
method: "GET",
|
||||
headers,
|
||||
signal: AbortSignal.timeout(this.timeout),
|
||||
});
|
||||
if (!response.ok) {
|
||||
this.logger.warn(`Image pre-download failed: HTTP ${response.status}`);
|
||||
return null;
|
||||
}
|
||||
const contentType = response.headers.get("content-type") || "image/png";
|
||||
if (contentType.includes("application/json")) {
|
||||
const json = (await response.json()) as Record<string, any>;
|
||||
if (json.image && typeof json.image === "string") {
|
||||
return { buffer: Buffer.from(json.image, "base64"), contentType: "image/png" };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return { buffer: Buffer.from(await response.arrayBuffer()), contentType };
|
||||
} catch (err) {
|
||||
this.logger.warn(`Image pre-download error: ${(err as Error).message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private extractIllustrationUrl(response: unknown): string | null {
|
||||
const responseData = response as Record<string, unknown>;
|
||||
const data =
|
||||
@@ -1816,13 +1861,18 @@ export class PL24Service {
|
||||
return this.fordLegacyService.fetchVehicleListForVolvo(serviceName);
|
||||
}
|
||||
|
||||
const cacheKey = `${PL24_DEFAULTS.CACHE_PREFIX}vehicle_list:${serviceName}`;
|
||||
const cached = await this.redis.getJson<any[]>(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
try {
|
||||
await this.authService.authorizeService(serviceName);
|
||||
const headers = await this.authService.buildAuthHeaders(serviceName);
|
||||
// Try de account; fall back to main token (demo mode) if that also fails
|
||||
let headers: Record<string, string>;
|
||||
try {
|
||||
await this.authService.authorizeServiceForAccount(serviceName, "de");
|
||||
headers = await this.authService.buildAuthHeadersForAccount("de", serviceName);
|
||||
} catch {
|
||||
this.logger.warn(
|
||||
`fetchVehicleList: de auth failed for ${serviceName}, using main token`,
|
||||
);
|
||||
headers = await this.authService.buildAuthHeaders();
|
||||
}
|
||||
|
||||
const catalogBase = getServiceApiPath(serviceName);
|
||||
|
||||
@@ -1885,7 +1935,6 @@ export class PL24Service {
|
||||
}
|
||||
|
||||
if (vehicles.length > 0) {
|
||||
await this.redis.setJson(cacheKey, vehicles, 86400); // 24h
|
||||
return vehicles;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user