Files
sase.tr/scripts/pl24-catalog-explorer.js
Sase Dev 72c0de65de docs: update INDEX.md + add catalog module, Ford/PSA legacy catalog, shared vehicles
- CatalogModule: VIN-less PL24 catalog browser (brands, models, categories, parts)
  - Supports P5 Modern (REST) and P4 Legacy (Ford, PSA) catalog architectures
  - Ford variant selector (model-year/engine/gearbox), PSA variant selector (body/engine/gearbox)
  - New API endpoints: ford-config, psa-bodies, psa-engines, psa-gearboxes, brands/:name/catalogs
- Shared vehicles: vehicles table decoupled from users via userVehicles junction table
- PL24 Ford Legacy service: comprehensive HTML-scraping for Ford/PSA/Hyundai/Kia/Nissan/Opel/Volvo
- PL24 types and service updated for P4 Legacy brand support
- Categories/parts service updated for dual FK (vehicleId + catalogVehicleId) pattern
- Catalog browser frontend routes and components
- docs/INDEX.md: updated with all new endpoints, components, hooks, routes (2026-03-02)
- docs/pl24-catalog/: per-brand catalog exploration docs
- scripts/migration-shared-vehicles.sql, pl24-catalog-explorer.js, posthog-dashboards.sh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 00:31:15 +00:00

743 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* PL24 Katalog Yapısı Keşif Script'i
*
* PL24 web arayüzüne (brandMenu.do) Playwright ile bağlanır,
* sol sidebar'daki her markayı tıklar ve arka planda yapılan
* XHR/fetch isteklerini intercept ederek API endpoint yapısını keşfeder.
* Her marka için docs/pl24-catalog/{brandSlug}.md oluşturur.
*
* Kullanım:
* node scripts/pl24-catalog-explorer.js # tüm markalar
* node scripts/pl24-catalog-explorer.js --brand VW # tek marka
* node scripts/pl24-catalog-explorer.js --headed # tarayıcı görünür
* node scripts/pl24-catalog-explorer.js --force # mevcut dosyaları yenile
* node scripts/pl24-catalog-explorer.js --delay 10000
*/
const { chromium } = require("playwright");
const fs = require("fs");
const path = require("path");
// PL24 .env'den kimlik bilgilerini yükle (dotenv bağımlılığı olmadan)
(function loadEnv() {
const envPath = path.join(__dirname, "../apps/api/.env");
if (!fs.existsSync(envPath)) return;
const lines = fs.readFileSync(envPath, "utf-8").split("\n");
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const eqIdx = trimmed.indexOf("=");
if (eqIdx < 1) continue;
const key = trimmed.substring(0, eqIdx).trim();
const val = trimmed.substring(eqIdx + 1).trim().replace(/^["']|["']$/g, "");
if (!process.env[key]) process.env[key] = val;
}
})();
// ── Config ──────────────────────────────────────────────────────────────────
const CONFIG = {
pl24BaseUrl: process.env.PL24_BASE_URL || "https://www.partslink24.com",
companyCode: process.env.PL24_COMPANY_CODE || "",
username: process.env.PL24_USERNAME || "",
password: process.env.PL24_PASSWORD || "",
brandMenuUrl: "/partslink24/user/brandMenu.do",
headless: true,
delayBetweenBrands: 8000, // ms — rate limit koruması
delayBetweenClicks: 3000, // ms — marka içi tıklamalar arası
navigationTimeout: 60000,
actionTimeout: 30000,
networkIdleTimeout: 5000, // API isteklerinin gelmesi için bekleme
outputDir: path.join(__dirname, "../docs/pl24-catalog"),
force: false,
filterBrand: null,
};
// ── CLI Args ─────────────────────────────────────────────────────────────────
function parseArgs() {
const args = process.argv.slice(2);
const opts = { ...CONFIG };
for (let i = 0; i < args.length; i++) {
if (args[i] === "--headed") opts.headless = false;
if (args[i] === "--headless") opts.headless = true;
if (args[i] === "--force") opts.force = true;
if (args[i] === "--brand" && args[i + 1]) opts.filterBrand = args[++i].toUpperCase();
if (args[i] === "--delay" && args[i + 1]) opts.delayBetweenBrands = parseInt(args[++i], 10);
}
return opts;
}
// ── Helpers ──────────────────────────────────────────────────────────────────
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
function ts() {
return new Date().toISOString().replace("T", " ").substring(0, 19);
}
function log(msg) {
console.log(`[${ts()}] ${msg}`);
}
function logSection(title) {
console.log(`\n${"═".repeat(70)}\n ${title}\n${"═".repeat(70)}`);
}
function slugify(name) {
return name
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
}
function isApiRequest(url) {
// Sadece JSON API isteklerini filtrele
const u = url.toLowerCase();
return (
u.includes("/extern/") ||
u.includes("/p5vwag/") ||
u.includes("/p5bmw/") ||
u.includes("/p5daimler/") ||
u.includes("/p5toyota/") ||
u.includes("/p5jlr/") ||
u.includes("/p5renault/") ||
u.includes("/p5stellantis/") ||
u.includes("/p5hyundai/") ||
u.includes("/p5nissan/") ||
u.includes("/p5volvo/") ||
u.includes("/p5opel/") ||
u.includes("/ford/") ||
u.includes("/pl24-appgtw/") ||
u.includes("/pl24-manufacturer/") ||
u.includes("/auth/ext/") ||
(u.includes("partslink24.com") && (u.includes(".json") || u.includes("/api/")))
);
}
// ── Keşif Notları Yardımcısı ─────────────────────────────────────────────────
class BrandExplorer {
constructor(brandName) {
this.brandName = brandName;
this.requests = [];
this.notes = [];
this.startedAt = new Date().toISOString();
}
addRequest({ method, url, status, responseBody, phase }) {
this.requests.push({ method, url, status, responseBody, phase });
}
addNote(note) {
this.notes.push(note);
log(` NOTE: ${note}`);
}
generateMd() {
// Unique URL'leri grupla
const byPath = {};
for (const r of this.requests) {
try {
const u = new URL(r.url);
const pathKey = u.pathname;
if (!byPath[pathKey]) byPath[pathKey] = { ...r, count: 0, params: [] };
byPath[pathKey].count++;
if (u.search) byPath[pathKey].params.push(u.search);
} catch {
// geçersiz URL
}
}
const endpointList = Object.entries(byPath)
.map(([p, r]) => {
const exampleUrl = r.url.length > 120 ? r.url.substring(0, 120) + "..." : r.url;
const bodyPreview = r.responseBody
? r.responseBody.substring(0, 300).replace(/\n/g, " ")
: "(yanıt yok)";
return `### ${p}\n- **Method:** ${r.method}\n- **Status:** ${r.status || "?"}\n- **Faz:** ${r.phase}\n- **Örnek URL:**\n \`${exampleUrl}\`\n- **Yanıt özeti:**\n \`${bodyPreview}\`\n- **Çağrı sayısı:** ${r.count}\n`;
})
.join("\n");
const requestTable = this.requests
.map((r) => {
const shortUrl = r.url.length > 80 ? r.url.substring(0, 80) + "..." : r.url;
const body = r.responseBody
? r.responseBody.substring(0, 100).replace(/\|/g, "\\|").replace(/\n/g, " ")
: "";
return `| ${r.method} | \`${shortUrl}\` | ${r.status || "?"} | ${r.phase} | \`${body}\` |`;
})
.join("\n");
const notesList = this.notes.map((n) => `- ${n}`).join("\n") || "- Özel not yok";
return `# ${this.brandName} — PL24 Katalog Yapısı
## Keşif Tarihi
${this.startedAt}
## Tespit Edilen Endpoint'ler (Unique Path'ler)
${endpointList || "_Hiç API isteği yakalanmadı._"}
## Ham Yakalanan İstekler
| Method | URL | Status | Faz | Response (ilk 100 char) |
|--------|-----|--------|-----|------------------------|
${requestTable || "| — | — | — | — | — |"}
## Notlar
${notesList}
`;
}
}
// ── Login ────────────────────────────────────────────────────────────────────
/**
* PL24 REST API üzerinden login olup session cookie'yi Playwright context'e enjekte eder.
* Web formu yerine doğrudan /pl24-appgtw/ext/api/1.0/login endpoint'i kullanılır.
*/
async function loginToPL24(page, opts) {
log("PL24 REST API login başlıyor...");
// 1. REST API ile login — web form bypass
const loginBody = {
authentication: {
account: opts.companyCode,
user: opts.username,
pwd: opts.password,
},
device: {
id: "0",
os: "Windows 10",
offset: "0",
lang: "en-US",
"os-version": "0",
},
"app-version": "",
squeezeOut: true,
};
const loginResp = await fetch(
`${opts.pl24BaseUrl}/pl24-appgtw/ext/api/1.0/login`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
},
body: JSON.stringify(loginBody),
signal: AbortSignal.timeout(opts.actionTimeout),
},
);
if (!loginResp.ok) {
throw new Error(`REST login HTTP ${loginResp.status}`);
}
const loginData = await loginResp.json();
if (!loginData.token?.access_token) {
throw new Error(`REST login başarısız: ${loginData.status}${loginData.message || "token yok"}`);
}
const accessToken = loginData.token.access_token;
log(`REST login başarılı. Token: ${accessToken.substring(0, 30)}...`);
// Session cookie
const setCookie = loginResp.headers.get("set-cookie") || "";
const pl24TokenMatch = setCookie.match(/PL24TOKEN=([^;]+)/);
const jsessionMatch = setCookie.match(/JSESSIONID=([^;]+)/);
const pl24Token = pl24TokenMatch ? `PL24TOKEN=${pl24TokenMatch[1]}` : "";
const jsession = jsessionMatch ? `JSESSIONID=${jsessionMatch[1]}` : "";
log(`Cookie: ${pl24Token || jsession || "(yok)"}`);
// 2. Cookie'leri Playwright context'e enjekte et
const context = page.context();
const cookiesToSet = [];
if (pl24TokenMatch) {
cookiesToSet.push({ name: "PL24TOKEN", value: pl24TokenMatch[1], domain: "www.partslink24.com", path: "/" });
}
if (jsessionMatch) {
cookiesToSet.push({ name: "JSESSIONID", value: jsessionMatch[1], domain: "www.partslink24.com", path: "/" });
}
if (cookiesToSet.length > 0) {
await context.addCookies(cookiesToSet);
log(`${cookiesToSet.length} cookie enjekte edildi`);
}
// 3. Önce ana sayfayı ziyaret et (session kurmak için)
log("Ana sayfa ziyaret ediliyor...");
await page.goto(`${opts.pl24BaseUrl}/`, {
waitUntil: "networkidle",
timeout: opts.navigationTimeout,
});
// 4. Formu JWT token ile doldurarak login yap
const currentUrl = page.url();
log(`URL: ${currentUrl}`);
if (currentUrl.includes("brandMenu")) {
log("Giriş yapılmış (cookie çalıştı)");
return;
}
if (currentUrl.includes("login")) {
// Form görünüyor — doldur ve submit et
log("Login formu dolduruluyor...");
await page.fill("#login-id", opts.companyCode);
await page.fill("#login-name", opts.username);
await page.fill("#inputPassword", opts.password);
// JavaScript ile doLoginAjax çağır
log("doLoginAjax çağrılıyor...");
await page.evaluate(() => {
if (typeof doLoginAjax === "function") {
doLoginAjax(false);
}
});
// Redirect bekle
try {
await page.waitForURL(/brandMenu|portal|welcome/, { timeout: 30000 });
log("Login başarılı — redirect oldu");
} catch {
await sleep(3000);
const url2 = page.url();
log(`Login sonrası URL: ${url2}`);
if (url2.includes("login")) {
// HTML hata mesajını kaydet
const html = await page.content();
require("fs").writeFileSync("/tmp/pl24-login-error.html", html);
throw new Error(`Login başarısız: ${url2} (HTML → /tmp/pl24-login-error.html)`);
}
}
}
// 5. brandMenu.do'ya git
log("brandMenu.do'ya gidiliyor...");
await page.goto(`${opts.pl24BaseUrl}/partslink24/user/brandMenu.do`, {
waitUntil: "networkidle",
timeout: opts.navigationTimeout,
});
const finalUrl = page.url();
log(`brandMenu URL: ${finalUrl}`);
if (finalUrl.includes("login")) {
throw new Error(`brandMenu'ya erişilemedi, login gerekiyor: ${finalUrl}`);
}
log("PL24'e başarıyla giriş yapıldı!");
}
// ── Marka Listesi ─────────────────────────────────────────────────────────────
async function getBrandList(page) {
log("Marka listesi okunuyor...");
// brandMenu.do'da markalar: <a class="brand-logo" href="/partslink24/launchCatalog.do?service=vw_parts&t=..." title="Volkswagen">
try {
const count = await page.locator("a.brand-logo").count();
if (count > 0) {
log(` a.brand-logo selector: ${count} marka bulundu`);
const brands = await page.$$eval("a.brand-logo", (els) =>
els.map((el) => {
const href = el.getAttribute("href") || "";
// service adını URL'den çıkar: ?service=vw_parts&...
const serviceMatch = href.match(/[?&]service=([^&]+)/);
return {
name: el.getAttribute("title") || el.textContent?.trim() || "",
href,
service: serviceMatch ? serviceMatch[1] : "",
onclick: el.getAttribute("onclick") || "",
};
}).filter((b) => b.name.length > 0)
);
// Servis adına göre deduplicate (brandMenu.do her markayı 2 kez gösteriyor)
const seen = new Set();
const unique = brands.filter((b) => {
if (!b.service || seen.has(b.service)) return false;
seen.add(b.service);
return true;
});
log(` ${unique.length} marka listelendi (${brands.length - unique.length} duplicate atlandı):`);
for (const b of unique) {
log(` ${b.name} (${b.service}) → ${b.href.substring(0, 60)}`);
}
return unique;
}
} catch (err) {
log(` a.brand-logo selector hatası: ${err.message}`);
}
// Fallback: launchCatalog.do içeren linkleri ara
log(" Fallback: launchCatalog.do linkleri aranıyor...");
try {
const brands = await page.$$eval("a[href*='launchCatalog']", (els) =>
els.map((el) => {
const href = el.getAttribute("href") || "";
const serviceMatch = href.match(/[?&]service=([^&]+)/);
const title = el.getAttribute("title") || el.textContent?.trim() || "";
return {
name: title,
href,
service: serviceMatch ? serviceMatch[1] : "",
onclick: el.getAttribute("onclick") || "",
};
}).filter((b) => b.name.length > 0)
);
if (brands.length > 0) {
log(` ${brands.length} marka (launchCatalog fallback):`);
for (const b of brands) {
log(` ${b.name} (${b.service})`);
}
return brands;
}
} catch { /* devam */ }
// Son çare: sayfa kaynağını kaydet ve hata ver
log(" UYARI: Marka listesi bulunamadı, sayfa HTML kaydediliyor...");
const bodyHtml = await page.evaluate(() => document.body?.innerHTML?.substring(0, 5000) || "");
log(" Sayfa HTML (ilk 5000 char):");
console.log(bodyHtml);
return [];
}
// ── Tek Marka Keşfi ───────────────────────────────────────────────────────────
async function exploreBrand(page, brand, opts) {
const explorer = new BrandExplorer(brand.name);
log(`\nKeşif başlıyor: ${brand.name}`);
// Network intercept'i başlat
const requestLog = [];
const onRequest = (req) => {
const url = req.url();
if (isApiRequest(url)) {
requestLog.push({ method: req.method(), url, phase: "request", status: null, responseBody: null });
}
};
const onResponse = async (res) => {
const url = res.url();
if (!isApiRequest(url)) return;
const status = res.status();
let responseBody = null;
try {
const ct = res.headers()["content-type"] || "";
if (ct.includes("json") || ct.includes("text")) {
const text = await res.text();
responseBody = text.substring(0, 1000);
}
} catch { /* body alınamadı */ }
// Mevcut request'i güncelle veya yeni ekle
const existing = requestLog.find((r) => r.url === url && r.status === null);
if (existing) {
existing.status = status;
existing.responseBody = responseBody;
} else {
requestLog.push({ method: res.request().method(), url, phase: "response", status, responseBody });
}
};
page.on("request", onRequest);
page.on("response", onResponse);
try {
// ── Faz 1: Markayı tıkla ──────────────────────────────────────────────
log(` Faz 1: Marka tıklanıyor — ${brand.name}`);
if (brand.href && brand.href !== "#" && brand.href !== "") {
const fullUrl = brand.href.startsWith("http")
? brand.href
: `${opts.pl24BaseUrl}${brand.href}`;
await page.goto(fullUrl, { waitUntil: "domcontentloaded", timeout: opts.navigationTimeout });
} else if (brand.onclick) {
await page.evaluate((onclick) => eval(onclick), brand.onclick);
} else {
// Metin ile bul ve tıkla
const el = page.locator(`a:has-text("${brand.name}")`).first();
await el.click();
}
// API isteklerinin gelmesi için bekle
await sleep(opts.networkIdleTimeout);
// Sayfanın yapısını incele
const pageTitle = await page.title().catch(() => "");
log(` Sayfa başlığı: ${pageTitle}`);
explorer.addNote(`Sayfa başlığı: ${pageTitle}`);
explorer.addNote(`URL: ${page.url()}`);
// Faz 1 isteklerini kaydet
const phase1Requests = [...requestLog];
for (const r of phase1Requests) {
explorer.addRequest({ ...r, phase: "marka-ana-sayfa" });
}
requestLog.length = 0;
log(` Faz 1: ${phase1Requests.length} API isteği yakalandı`);
// ── Faz 2: İlk model/araç'ı tıkla ──────────────────────────────────────
log(" Faz 2: İlk model aranıyor...");
const modelSelectors = [
".model-list li:first-child a",
".vehicleList li:first-child a",
".modelSeries li:first-child a",
"table.models tr:nth-child(2) td:first-child a",
".content a:first-child",
"ul li a:first-child",
];
let modelClicked = false;
for (const sel of modelSelectors) {
try {
const el = page.locator(sel).first();
if (await el.isVisible({ timeout: 2000 })) {
const modelName = await el.textContent();
log(` İlk model tıklanıyor: "${modelName?.trim()}" (${sel})`);
await el.click();
await sleep(opts.delayBetweenClicks);
modelClicked = true;
explorer.addNote(`İlk model tıklandı: "${modelName?.trim()}"`);
break;
}
} catch { /* devam */ }
}
if (!modelClicked) {
explorer.addNote("İlk model tıklanamadı — selector bulunamadı");
log(" Faz 2: Model selector bulunamadı, atlanıyor");
}
// Faz 2 isteklerini kaydet
const phase2Requests = [...requestLog];
for (const r of phase2Requests) {
explorer.addRequest({ ...r, phase: "model-secim" });
}
requestLog.length = 0;
log(` Faz 2: ${phase2Requests.length} ek API isteği yakalandı`);
// ── Faz 3: Kategoriler ───────────────────────────────────────────────────
log(" Faz 3: Kategori yapısı inceleniyor...");
const categorySelectors = [
".mainGroup li:first-child a",
".categoryList li:first-child a",
".groups li:first-child a",
"ul.mainGroups li:first-child a",
];
for (const sel of categorySelectors) {
try {
const el = page.locator(sel).first();
if (await el.isVisible({ timeout: 2000 })) {
const catName = await el.textContent();
log(` İlk kategori tıklanıyor: "${catName?.trim()}" (${sel})`);
await el.click();
await sleep(opts.delayBetweenClicks);
explorer.addNote(`İlk kategori tıklandı: "${catName?.trim()}"`);
break;
}
} catch { /* devam */ }
}
const phase3Requests = [...requestLog];
for (const r of phase3Requests) {
explorer.addRequest({ ...r, phase: "kategori" });
}
requestLog.length = 0;
log(` Faz 3: ${phase3Requests.length} ek API isteği yakalandı`);
} catch (err) {
explorer.addNote(`HATA: ${err.message}`);
log(` HATA: ${err.message}`);
} finally {
page.off("request", onRequest);
page.off("response", onResponse);
}
return explorer;
}
// ── Ana Akış ─────────────────────────────────────────────────────────────────
async function main() {
const opts = parseArgs();
if (!opts.companyCode || !opts.username || !opts.password) {
console.error("HATA: PL24 kimlik bilgileri eksik. apps/api/.env dosyasını kontrol edin.");
process.exit(1);
}
// Çıktı dizinini oluştur
if (!fs.existsSync(opts.outputDir)) {
fs.mkdirSync(opts.outputDir, { recursive: true });
}
logSection("PL24 KATALOG KEŞİF ARACI");
log(`PL24 URL: ${opts.pl24BaseUrl}`);
log(`Headless: ${opts.headless}, Delay: ${opts.delayBetweenBrands}ms, Force: ${opts.force}`);
if (opts.filterBrand) log(`Marka filtresi: ${opts.filterBrand}`);
// Tarayıcı başlat
const browser = await chromium.launch({
headless: opts.headless,
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
const context = await browser.newContext({
viewport: { width: 1440, height: 900 },
ignoreHTTPSErrors: true,
userAgent:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
});
context.setDefaultTimeout(opts.actionTimeout);
context.setDefaultNavigationTimeout(opts.navigationTimeout);
const page = await context.newPage();
try {
// Login
logSection("LOGIN");
await loginToPL24(page, opts);
// Screenshot: brandMenu
await page.screenshot({
path: path.join(opts.outputDir, "_brandmenu-screenshot.png"),
});
log("brandMenu.do ekran görüntüsü kaydedildi");
// Marka listesi
logSection("MARKA LİSTESİ");
let brands = await getBrandList(page);
if (brands.length === 0) {
log("UYARI: Marka listesi boş — sayfa yapısı analiz ediliyor...");
// Sayfa kaynağını kaydet
const html = await page.content();
fs.writeFileSync(path.join(opts.outputDir, "_brandmenu-source.html"), html);
log("Sayfa kaynağı _brandmenu-source.html olarak kaydedildi");
}
log(`Toplam ${brands.length} marka bulundu`);
// Filtrele (hem marka adına hem service adına bak)
if (opts.filterBrand) {
brands = brands.filter((b) =>
b.name.toUpperCase().includes(opts.filterBrand) ||
(b.service || "").toUpperCase().includes(opts.filterBrand)
);
log(`Filtre sonrası: ${brands.length} marka`);
}
if (brands.length === 0) {
log("İşlenecek marka yok. Çıkılıyor.");
return;
}
// Özet için marka bilgilerini tut
const summaryData = [];
// ── Her Marka ─────────────────────────────────────────────────────────
for (let i = 0; i < brands.length; i++) {
const brand = brands[i];
const slug = slugify(brand.name);
const outFile = path.join(opts.outputDir, `${slug}.md`);
logSection(`[${i + 1}/${brands.length}] ${brand.name}`);
// Resume: zaten keşfedildiyse atla
if (!opts.force && fs.existsSync(outFile)) {
log(`Atlanıyor (zaten mevcut): ${outFile}`);
summaryData.push({ name: brand.name, slug, status: "atlandı (mevcut)" });
continue;
}
// brandMenu.do'ya dön (her marka keşfinden önce)
if (i > 0) {
try {
await page.goto(`${opts.pl24BaseUrl}${opts.brandMenuUrl}`, {
waitUntil: "domcontentloaded",
timeout: opts.navigationTimeout,
});
await sleep(2000);
} catch (err) {
log(`brandMenu.do'ya dönülemedi: ${err.message}`);
}
}
// Keşfet
const explorer = await exploreBrand(page, brand, opts);
const md = explorer.generateMd();
// Kaydet
fs.writeFileSync(outFile, md, "utf-8");
log(`Kaydedildi: ${outFile}`);
summaryData.push({
name: brand.name,
slug,
status: "tamamlandı",
requestCount: explorer.requests.length,
});
// Rate limit
if (i < brands.length - 1) {
log(`Bekleniyor: ${opts.delayBetweenBrands}ms...`);
await sleep(opts.delayBetweenBrands);
}
}
// ── Özet Dosyası ──────────────────────────────────────────────────────
logSection("ÖZET");
const summaryLines = summaryData.map((s) =>
`| ${s.name} | ${s.slug} | ${s.status} | ${s.requestCount ?? "-"} |`
).join("\n");
const summaryMd = `# PL24 Katalog Keşif Özeti
## Keşif Tarihi
${new Date().toISOString()}
## Sonuçlar
| Marka | Slug | Durum | API İstek Sayısı |
|-------|------|-------|-----------------|
${summaryLines}
## Notlar
- Bu dosya otomatik olarak oluşturulmuştur
- Her marka için ayrıntı: \`{slug}.md\`
`;
fs.writeFileSync(path.join(opts.outputDir, "_summary.md"), summaryMd, "utf-8");
log(`Özet kaydedildi: ${path.join(opts.outputDir, "_summary.md")}`);
log(`\nKeşif tamamlandı. ${summaryData.length} marka işlendi.`);
log(`Çıktı dizini: ${opts.outputDir}`);
} finally {
await browser.close();
}
}
main().catch((err) => {
console.error(`FATAL: ${err.message}`);
console.error(err.stack);
process.exit(1);
});