chore: bump task worker version comment
Force new git SHA to avoid CI scientific notation bug. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
179
backend/scripts/test-all-brands-scroll.ts
Normal file
179
backend/scripts/test-all-brands-scroll.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Loading ALL brands from https://shop.bestdispensary.com/brands');
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', (req) => {
|
||||
if (['image', 'font', 'media'].includes(req.resourceType())) {
|
||||
req.abort();
|
||||
} else {
|
||||
req.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('https://shop.bestdispensary.com/brands', {
|
||||
waitUntil: 'networkidle2',
|
||||
timeout: 60000
|
||||
});
|
||||
await sleep(3000);
|
||||
|
||||
// Bypass age gate
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
console.log('Age gate detected, bypassing...');
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(2000);
|
||||
}
|
||||
|
||||
console.log('Current URL:', page.url());
|
||||
|
||||
// Get initial brand count
|
||||
let brandCount = await page.evaluate(() => {
|
||||
const seen = new Set<string>();
|
||||
document.querySelectorAll('a[href*="/brand/"]').forEach((a: Element) => {
|
||||
const href = a.getAttribute('href');
|
||||
if (href) seen.add(href);
|
||||
});
|
||||
return seen.size;
|
||||
});
|
||||
console.log(`Initial brand count: ${brandCount}`);
|
||||
|
||||
// Aggressive scrolling
|
||||
console.log('\nScrolling to load ALL brands...');
|
||||
let previousCount = 0;
|
||||
let sameCount = 0;
|
||||
|
||||
for (let i = 0; i < 50; i++) {
|
||||
// Scroll to bottom
|
||||
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
||||
await sleep(1000);
|
||||
|
||||
brandCount = await page.evaluate(() => {
|
||||
const seen = new Set<string>();
|
||||
document.querySelectorAll('a[href*="/brand/"]').forEach((a: Element) => {
|
||||
const href = a.getAttribute('href');
|
||||
if (href) seen.add(href);
|
||||
});
|
||||
return seen.size;
|
||||
});
|
||||
|
||||
if (brandCount === previousCount) {
|
||||
sameCount++;
|
||||
if (sameCount >= 5) {
|
||||
console.log(` Scroll ${i+1}: ${brandCount} brands (stopping - no change)`);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
sameCount = 0;
|
||||
console.log(` Scroll ${i+1}: ${brandCount} brands`);
|
||||
}
|
||||
previousCount = brandCount;
|
||||
}
|
||||
|
||||
// Get all unique brands
|
||||
const brands = await page.evaluate(() => {
|
||||
const results: { name: string; href: string }[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
document.querySelectorAll('a[href*="/brand/"]').forEach((a: Element) => {
|
||||
const href = a.getAttribute('href') || '';
|
||||
const normalizedHref = href.toLowerCase();
|
||||
if (seen.has(normalizedHref)) return;
|
||||
seen.add(normalizedHref);
|
||||
|
||||
// Get brand name
|
||||
let name = '';
|
||||
const heading = a.querySelector('h3, h4, h5, [class*="name"]');
|
||||
if (heading) {
|
||||
name = heading.textContent?.trim() || '';
|
||||
}
|
||||
if (!name) {
|
||||
name = a.textContent?.trim().split('\n')[0] || '';
|
||||
}
|
||||
if (!name) {
|
||||
name = href.split('/brand/')[1]?.replace(/-/g, ' ') || '';
|
||||
}
|
||||
|
||||
results.push({ name: name.slice(0, 50), href });
|
||||
});
|
||||
|
||||
return results.sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('TOTAL BRANDS FOUND: ' + brands.length);
|
||||
console.log('='.repeat(60));
|
||||
|
||||
brands.forEach((b, i) => {
|
||||
const num = (i + 1).toString().padStart(3, ' ');
|
||||
console.log(`${num}. ${b.name} (${b.href})`);
|
||||
});
|
||||
|
||||
// Now visit each brand page and count products
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('PRODUCTS PER BRAND');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
const brandProducts: { brand: string; products: number }[] = [];
|
||||
|
||||
for (let i = 0; i < brands.length; i++) {
|
||||
const brand = brands[i];
|
||||
try {
|
||||
const brandUrl = brand.href.startsWith('http')
|
||||
? brand.href
|
||||
: `https://shop.bestdispensary.com${brand.href}`;
|
||||
|
||||
await page.goto(brandUrl, { waitUntil: 'networkidle2', timeout: 30000 });
|
||||
await sleep(1500);
|
||||
|
||||
// Scroll to load products
|
||||
for (let j = 0; j < 10; j++) {
|
||||
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
||||
await sleep(800);
|
||||
}
|
||||
|
||||
const productCount = await page.evaluate(() => {
|
||||
const seen = new Set<string>();
|
||||
document.querySelectorAll('a[href*="/product/"]').forEach((a: Element) => {
|
||||
const img = a.querySelector('img');
|
||||
const name = img?.getAttribute('alt') || a.textContent?.trim() || '';
|
||||
if (name) seen.add(name);
|
||||
});
|
||||
return seen.size;
|
||||
});
|
||||
|
||||
brandProducts.push({ brand: brand.name, products: productCount });
|
||||
console.log(`${(i+1).toString().padStart(3)}. ${brand.name}: ${productCount} products`);
|
||||
|
||||
} catch (err: any) {
|
||||
console.log(`${(i+1).toString().padStart(3)}. ${brand.name}: ERROR - ${err.message?.slice(0, 30)}`);
|
||||
brandProducts.push({ brand: brand.name, products: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
const totalProducts = brandProducts.reduce((sum, b) => sum + b.products, 0);
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('SUMMARY');
|
||||
console.log('='.repeat(60));
|
||||
console.log(`Total brands: ${brands.length}`);
|
||||
console.log(`Total products: ${totalProducts}`);
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
92
backend/scripts/test-bestdispensary-brands.ts
Normal file
92
backend/scripts/test-bestdispensary-brands.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Navigating to https://shop.bestdispensary.com/brands');
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', (req) => {
|
||||
if (['image', 'font', 'media'].includes(req.resourceType())) {
|
||||
req.abort();
|
||||
} else {
|
||||
req.continue();
|
||||
}
|
||||
});
|
||||
|
||||
// Go directly to the brands page
|
||||
await page.goto('https://shop.bestdispensary.com/brands', {
|
||||
waitUntil: 'networkidle2',
|
||||
timeout: 60000
|
||||
});
|
||||
await sleep(3000);
|
||||
|
||||
// Bypass age gate if present
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
console.log('Age gate detected, bypassing...');
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(2000);
|
||||
}
|
||||
|
||||
console.log('Current URL:', page.url());
|
||||
|
||||
// Scroll to load all content
|
||||
console.log('\nScrolling to load all brands...');
|
||||
let previousHeight = 0;
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
||||
await sleep(1500);
|
||||
|
||||
const currentHeight = await page.evaluate(() => document.body.scrollHeight);
|
||||
if (currentHeight === previousHeight) {
|
||||
console.log(` Scroll ${i+1}: No new content`);
|
||||
break;
|
||||
}
|
||||
previousHeight = currentHeight;
|
||||
|
||||
const brandCount = await page.evaluate(() =>
|
||||
document.querySelectorAll('a[href*="/brand/"]').length
|
||||
);
|
||||
console.log(` Scroll ${i+1}: height=${currentHeight}, brand links=${brandCount}`);
|
||||
}
|
||||
|
||||
// Get all brand links
|
||||
const brands = await page.evaluate(() => {
|
||||
const results: { name: string; href: string }[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
document.querySelectorAll('a[href*="/brand/"]').forEach((a: Element) => {
|
||||
const href = a.getAttribute('href') || '';
|
||||
if (seen.has(href)) return;
|
||||
seen.add(href);
|
||||
|
||||
const name = a.textContent?.trim() || href.split('/brand/')[1] || '';
|
||||
results.push({ name, href });
|
||||
});
|
||||
|
||||
return results;
|
||||
});
|
||||
|
||||
console.log(`\nFound ${brands.length} brands:`);
|
||||
brands.forEach(b => console.log(` - ${b.name} (${b.href})`));
|
||||
|
||||
// Take screenshot
|
||||
await page.screenshot({ path: '/tmp/bestdispensary-brands.png', fullPage: true });
|
||||
console.log('\nScreenshot saved to /tmp/bestdispensary-brands.png');
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
108
backend/scripts/test-brands-debug.ts
Normal file
108
backend/scripts/test-brands-debug.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', (req) => {
|
||||
if (['image', 'font', 'media'].includes(req.resourceType())) {
|
||||
req.abort();
|
||||
} else {
|
||||
req.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('https://shop.bestdispensary.com/brands', {
|
||||
waitUntil: 'networkidle2',
|
||||
timeout: 60000
|
||||
});
|
||||
await sleep(3000);
|
||||
|
||||
// Bypass age gate
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(2000);
|
||||
}
|
||||
|
||||
// Check Load More button
|
||||
const btnInfo = await page.evaluate(() => {
|
||||
const btn = document.querySelector('button.collection__load-more');
|
||||
if (!btn) return { found: false };
|
||||
|
||||
const rect = btn.getBoundingClientRect();
|
||||
return {
|
||||
found: true,
|
||||
text: btn.textContent?.trim(),
|
||||
visible: rect.width > 0 && rect.height > 0,
|
||||
top: rect.top,
|
||||
disabled: (btn as HTMLButtonElement).disabled,
|
||||
class: btn.className,
|
||||
};
|
||||
});
|
||||
|
||||
console.log('Load More button:', btnInfo);
|
||||
|
||||
// Scroll to button and click
|
||||
console.log('\nScrolling to button and clicking...');
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const btn = await page.$('button.collection__load-more');
|
||||
if (!btn) {
|
||||
console.log('Button not found');
|
||||
break;
|
||||
}
|
||||
|
||||
// Scroll button into view
|
||||
await page.evaluate((b) => b.scrollIntoView({ behavior: 'smooth', block: 'center' }), btn);
|
||||
await sleep(500);
|
||||
|
||||
// Check if button is still there and clickable
|
||||
const stillThere = await page.evaluate(() => {
|
||||
const b = document.querySelector('button.collection__load-more');
|
||||
return b ? b.textContent?.trim() : null;
|
||||
});
|
||||
|
||||
if (!stillThere) {
|
||||
console.log('Button disappeared - all loaded');
|
||||
break;
|
||||
}
|
||||
|
||||
// Click it
|
||||
await btn.click();
|
||||
console.log(`Click ${i+1}...`);
|
||||
await sleep(2000);
|
||||
|
||||
const count = await page.evaluate(() =>
|
||||
document.querySelectorAll('.brands-page__list a[href*="/brand/"]').length
|
||||
);
|
||||
console.log(` Brands: ${count}`);
|
||||
}
|
||||
|
||||
// Final count
|
||||
const brands = await page.evaluate(() => {
|
||||
const list: string[] = [];
|
||||
document.querySelectorAll('.brands-page__list a[href*="/brand/"]').forEach((a: Element) => {
|
||||
list.push(a.textContent?.trim() || '');
|
||||
});
|
||||
return list;
|
||||
});
|
||||
|
||||
console.log(`\nTotal brands: ${brands.length}`);
|
||||
console.log(brands.join(', '));
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
157
backend/scripts/test-brands-load-all.ts
Normal file
157
backend/scripts/test-brands-load-all.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', (req) => {
|
||||
if (['image', 'font', 'media'].includes(req.resourceType())) {
|
||||
req.abort();
|
||||
} else {
|
||||
req.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('https://shop.bestdispensary.com/brands', {
|
||||
waitUntil: 'networkidle2',
|
||||
timeout: 60000
|
||||
});
|
||||
await sleep(3000);
|
||||
|
||||
// Bypass age gate
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
console.log('Bypassing age gate...');
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(2000);
|
||||
}
|
||||
|
||||
// Click "LOAD MORE" until all brands are loaded
|
||||
console.log('Loading all brands...\n');
|
||||
|
||||
let loadMoreClicks = 0;
|
||||
while (true) {
|
||||
const loadMoreBtn = await page.$('button.collection__load-more');
|
||||
if (!loadMoreBtn) {
|
||||
console.log('No more "Load More" button - all brands loaded!');
|
||||
break;
|
||||
}
|
||||
|
||||
const isVisible = await page.evaluate((btn) => {
|
||||
const rect = btn.getBoundingClientRect();
|
||||
return rect.width > 0 && rect.height > 0;
|
||||
}, loadMoreBtn);
|
||||
|
||||
if (!isVisible) {
|
||||
console.log('Load More button not visible - all brands loaded!');
|
||||
break;
|
||||
}
|
||||
|
||||
await loadMoreBtn.click();
|
||||
loadMoreClicks++;
|
||||
await sleep(1500);
|
||||
|
||||
const brandCount = await page.evaluate(() =>
|
||||
document.querySelectorAll('.brands-page__list a[href*="/brand/"]').length
|
||||
);
|
||||
console.log(` Click ${loadMoreClicks}: ${brandCount} brands loaded`);
|
||||
|
||||
if (loadMoreClicks > 20) break; // Safety limit
|
||||
}
|
||||
|
||||
// Get all brands
|
||||
const brands = await page.evaluate(() => {
|
||||
const results: { name: string; href: string }[] = [];
|
||||
document.querySelectorAll('.brands-page__list a[href*="/brand/"]').forEach((a: Element) => {
|
||||
const href = a.getAttribute('href') || '';
|
||||
const name = a.textContent?.trim() || '';
|
||||
if (name && href) {
|
||||
results.push({ name, href });
|
||||
}
|
||||
});
|
||||
return results;
|
||||
});
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log(`TOTAL BRANDS: ${brands.length}`);
|
||||
console.log('='.repeat(60));
|
||||
|
||||
// Visit each brand and count products
|
||||
console.log('\nCounting products per brand...\n');
|
||||
|
||||
const results: { brand: string; products: number }[] = [];
|
||||
|
||||
for (let i = 0; i < brands.length; i++) {
|
||||
const brand = brands[i];
|
||||
const brandUrl = `https://shop.bestdispensary.com${brand.href}`;
|
||||
|
||||
try {
|
||||
await page.goto(brandUrl, { waitUntil: 'networkidle2', timeout: 30000 });
|
||||
await sleep(1000);
|
||||
|
||||
// Click load more on brand page too
|
||||
for (let j = 0; j < 10; j++) {
|
||||
const loadMore = await page.$('button.collection__load-more');
|
||||
if (!loadMore) break;
|
||||
|
||||
const isVisible = await page.evaluate((btn) => {
|
||||
const rect = btn.getBoundingClientRect();
|
||||
return rect.width > 0 && rect.height > 0;
|
||||
}, loadMore);
|
||||
|
||||
if (!isVisible) break;
|
||||
await loadMore.click();
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
const productCount = await page.evaluate(() => {
|
||||
const seen = new Set<string>();
|
||||
document.querySelectorAll('a[href*="/product/"]').forEach((a: Element) => {
|
||||
const href = a.getAttribute('href');
|
||||
if (href) seen.add(href);
|
||||
});
|
||||
return seen.size;
|
||||
});
|
||||
|
||||
results.push({ brand: brand.name, products: productCount });
|
||||
console.log(`${(i+1).toString().padStart(3)}. ${brand.name}: ${productCount} products`);
|
||||
|
||||
} catch (err: any) {
|
||||
console.log(`${(i+1).toString().padStart(3)}. ${brand.name}: ERROR`);
|
||||
results.push({ brand: brand.name, products: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
const totalProducts = results.reduce((sum, r) => sum + r.products, 0);
|
||||
const brandsWithProducts = results.filter(r => r.products > 0).length;
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('SUMMARY');
|
||||
console.log('='.repeat(60));
|
||||
console.log(`Total brands: ${brands.length}`);
|
||||
console.log(`Brands with products: ${brandsWithProducts}`);
|
||||
console.log(`Total products: ${totalProducts}`);
|
||||
|
||||
// Top brands by product count
|
||||
console.log('\nTop 20 brands by product count:');
|
||||
results
|
||||
.sort((a, b) => b.products - a.products)
|
||||
.slice(0, 20)
|
||||
.forEach((r, i) => console.log(` ${i+1}. ${r.brand}: ${r.products}`));
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
108
backend/scripts/test-brands-products.ts
Normal file
108
backend/scripts/test-brands-products.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
await page.goto('https://shop.bestdispensary.com/brands', {
|
||||
waitUntil: 'networkidle2',
|
||||
timeout: 60000
|
||||
});
|
||||
await sleep(3000);
|
||||
|
||||
// Bypass age gate
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(2000);
|
||||
}
|
||||
|
||||
// Try clicking Load More multiple times with JS
|
||||
console.log('Loading all brands...');
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const clicked = await page.evaluate(() => {
|
||||
const btn = document.querySelector('button.collection__load-more') as HTMLButtonElement;
|
||||
if (btn) { btn.click(); return true; }
|
||||
return false;
|
||||
});
|
||||
if (!clicked) break;
|
||||
await sleep(2000);
|
||||
}
|
||||
|
||||
// Get all brands
|
||||
const brands = await page.evaluate(() => {
|
||||
const list: { name: string; href: string }[] = [];
|
||||
document.querySelectorAll('.brands-page__list a[href*="/brand/"]').forEach((a: Element) => {
|
||||
list.push({
|
||||
name: a.textContent?.trim() || '',
|
||||
href: a.getAttribute('href') || '',
|
||||
});
|
||||
});
|
||||
return list;
|
||||
});
|
||||
|
||||
console.log('Total brands found: ' + brands.length + '\n');
|
||||
console.log('PRODUCTS PER BRAND');
|
||||
console.log('==================\n');
|
||||
|
||||
const results: { brand: string; products: number }[] = [];
|
||||
|
||||
for (let i = 0; i < brands.length; i++) {
|
||||
const brand = brands[i];
|
||||
const url = 'https://shop.bestdispensary.com' + brand.href;
|
||||
|
||||
try {
|
||||
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
|
||||
await sleep(1000);
|
||||
|
||||
// Click load more on brand page
|
||||
for (let j = 0; j < 20; j++) {
|
||||
const clicked = await page.evaluate(() => {
|
||||
const btn = document.querySelector('button.collection__load-more') as HTMLButtonElement;
|
||||
if (btn) { btn.click(); return true; }
|
||||
return false;
|
||||
});
|
||||
if (!clicked) break;
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
const productCount = await page.evaluate(() => {
|
||||
const seen = new Set<string>();
|
||||
document.querySelectorAll('a[href*="/product/"]').forEach((a: Element) => {
|
||||
const href = a.getAttribute('href');
|
||||
if (href) seen.add(href);
|
||||
});
|
||||
return seen.size;
|
||||
});
|
||||
|
||||
results.push({ brand: brand.name, products: productCount });
|
||||
const num = (i + 1).toString().padStart(2, ' ');
|
||||
console.log(num + '. ' + brand.name + ': ' + productCount);
|
||||
|
||||
} catch (err) {
|
||||
results.push({ brand: brand.name, products: 0 });
|
||||
const num = (i + 1).toString().padStart(2, ' ');
|
||||
console.log(num + '. ' + brand.name + ': ERROR');
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
const total = results.reduce((s, r) => s + r.products, 0);
|
||||
console.log('\n==================');
|
||||
console.log('TOTAL: ' + brands.length + ' brands, ' + total + ' products');
|
||||
console.log('==================');
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
130
backend/scripts/test-brands-selector.ts
Normal file
130
backend/scripts/test-brands-selector.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', (req) => {
|
||||
if (['image', 'font', 'media'].includes(req.resourceType())) {
|
||||
req.abort();
|
||||
} else {
|
||||
req.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('https://shop.bestdispensary.com/brands', {
|
||||
waitUntil: 'networkidle2',
|
||||
timeout: 60000
|
||||
});
|
||||
await sleep(3000);
|
||||
|
||||
// Bypass age gate
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(2000);
|
||||
}
|
||||
|
||||
// Use the selector hint: /html/body/main/section
|
||||
console.log('Looking at main > section structure...\n');
|
||||
|
||||
const sectionInfo = await page.evaluate(() => {
|
||||
const main = document.querySelector('main');
|
||||
if (!main) return { error: 'No main element' };
|
||||
|
||||
const sections = main.querySelectorAll('section');
|
||||
const results: any[] = [];
|
||||
|
||||
sections.forEach((section, i) => {
|
||||
const children = section.children;
|
||||
const childInfo: string[] = [];
|
||||
|
||||
for (let j = 0; j < Math.min(children.length, 10); j++) {
|
||||
const child = children[j];
|
||||
childInfo.push(child.tagName + '.' + (child.className?.slice(0, 30) || ''));
|
||||
}
|
||||
|
||||
results.push({
|
||||
index: i,
|
||||
class: section.className?.slice(0, 50),
|
||||
childCount: children.length,
|
||||
sampleChildren: childInfo,
|
||||
});
|
||||
});
|
||||
|
||||
return results;
|
||||
});
|
||||
|
||||
console.log('Sections in main:');
|
||||
console.log(JSON.stringify(sectionInfo, null, 2));
|
||||
|
||||
// Look for brand cards within the section
|
||||
console.log('\nLooking for brand cards in main > section...');
|
||||
|
||||
const brandCards = await page.evaluate(() => {
|
||||
const section = document.querySelector('main > section');
|
||||
if (!section) return [];
|
||||
|
||||
// Get all child elements that might be brand cards
|
||||
const cards: { tag: string; text: string; href: string }[] = [];
|
||||
|
||||
section.querySelectorAll('a').forEach((a: Element) => {
|
||||
const href = a.getAttribute('href') || '';
|
||||
const text = a.textContent?.trim().slice(0, 50) || '';
|
||||
cards.push({ tag: 'a', text, href });
|
||||
});
|
||||
|
||||
return cards;
|
||||
});
|
||||
|
||||
console.log(`Found ${brandCards.length} links in section:`);
|
||||
brandCards.slice(0, 30).forEach(c => console.log(` ${c.text} -> ${c.href}`));
|
||||
|
||||
// Get the grid of brand cards
|
||||
console.log('\nLooking for grid container...');
|
||||
|
||||
const gridCards = await page.evaluate(() => {
|
||||
// Look for grid-like containers
|
||||
const grids = document.querySelectorAll('[class*="grid"], [class*="Grid"], main section > div');
|
||||
const results: any[] = [];
|
||||
|
||||
grids.forEach((grid, i) => {
|
||||
const links = grid.querySelectorAll('a[href*="/brand/"]');
|
||||
if (links.length > 5) {
|
||||
const brands: string[] = [];
|
||||
links.forEach((a: Element) => {
|
||||
const text = a.textContent?.trim().split('\n')[0] || '';
|
||||
if (text && !brands.includes(text)) brands.push(text);
|
||||
});
|
||||
results.push({
|
||||
class: grid.className?.slice(0, 40),
|
||||
brandCount: brands.length,
|
||||
brands: brands.slice(0, 50),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
});
|
||||
|
||||
console.log('Grid containers with brands:');
|
||||
gridCards.forEach(g => {
|
||||
console.log(`\n[${g.brandCount} brands] class="${g.class}"`);
|
||||
g.brands.forEach((b: string, i: number) => console.log(` ${i+1}. ${b}`));
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
138
backend/scripts/test-treez-all-endpoints.ts
Normal file
138
backend/scripts/test-treez-all-endpoints.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
// Capture ALL requests to treez.io
|
||||
const treezRequests: any[] = [];
|
||||
|
||||
page.on('request', (req) => {
|
||||
const url = req.url();
|
||||
if (url.includes('treez.io') && !url.includes('.js') && !url.includes('.css')) {
|
||||
treezRequests.push({
|
||||
url: url,
|
||||
method: req.method(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Also intercept and capture ES API responses
|
||||
page.on('response', async (res) => {
|
||||
const url = res.url();
|
||||
if (url.includes('gapcommerceapi.com') && res.status() === 200) {
|
||||
try {
|
||||
const json = await res.json();
|
||||
const total = json.hits?.total?.value;
|
||||
const count = json.hits?.hits?.length;
|
||||
if (total || count) {
|
||||
console.log('\nES Response: total=' + total + ', returned=' + count);
|
||||
if (json.hits?.hits?.[0]?._source) {
|
||||
const src = json.hits.hits[0]._source;
|
||||
console.log('First product fields: ' + Object.keys(src).slice(0, 20).join(', '));
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Loading /shop page...\n');
|
||||
|
||||
await page.goto('https://shop.bestdispensary.com/shop', {
|
||||
waitUntil: 'networkidle2',
|
||||
timeout: 60000
|
||||
});
|
||||
await sleep(3000);
|
||||
|
||||
// Bypass age gate
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(2000);
|
||||
}
|
||||
|
||||
// Click load more several times
|
||||
console.log('\nClicking Load More...');
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const btn = await page.$('button.collection__load-more');
|
||||
if (!btn) break;
|
||||
await btn.click();
|
||||
await sleep(2000);
|
||||
}
|
||||
|
||||
console.log('\n=== TREEZ API ENDPOINTS CALLED ===\n');
|
||||
const uniqueUrls = [...new Set(treezRequests.map(r => r.url.split('?')[0]))];
|
||||
uniqueUrls.forEach(url => console.log(url));
|
||||
|
||||
// Now intercept the ES response data by making a request from browser context
|
||||
console.log('\n=== FETCHING ALL PRODUCTS VIA BROWSER ===\n');
|
||||
|
||||
const allProducts = await page.evaluate(async () => {
|
||||
const apiKey = 'V3jHL9dFzi3Gj4UISM4lr38Nm0GSxcps5OBz1PbS';
|
||||
const url = 'https://search-kyrok9udlk.gapcommerceapi.com/product/search';
|
||||
|
||||
const query = {
|
||||
from: 0,
|
||||
size: 1000,
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{ bool: { filter: { range: { customMinPrice: { gte: 0.01, lte: 500000 }}}}},
|
||||
{ bool: { should: [{ match: { isAboveThreshold: true }}]}},
|
||||
{ bool: { should: [{ match: { isHideFromMenu: false }}]}}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': apiKey,
|
||||
},
|
||||
body: JSON.stringify(query),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
total: data.hits?.total?.value,
|
||||
count: data.hits?.hits?.length,
|
||||
sample: data.hits?.hits?.[0]?._source,
|
||||
allProducts: data.hits?.hits?.map((h: any) => h._source),
|
||||
};
|
||||
} catch (err: any) {
|
||||
return { error: err.message };
|
||||
}
|
||||
});
|
||||
|
||||
if (allProducts.error) {
|
||||
console.log('Error: ' + allProducts.error);
|
||||
} else {
|
||||
console.log('Total products: ' + allProducts.total);
|
||||
console.log('Returned: ' + allProducts.count);
|
||||
|
||||
if (allProducts.sample) {
|
||||
console.log('\n=== PRODUCT FIELDS ===\n');
|
||||
console.log(Object.keys(allProducts.sample).sort().join('\n'));
|
||||
|
||||
console.log('\n=== SAMPLE PRODUCT ===\n');
|
||||
console.log(JSON.stringify(allProducts.sample, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main();
|
||||
203
backend/scripts/test-treez-all-products.ts
Normal file
203
backend/scripts/test-treez-all-products.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* Extract ALL product elements and find unique products
|
||||
*/
|
||||
|
||||
import puppeteer, { Page } from 'puppeteer';
|
||||
|
||||
const STORE_ID = 'best';
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function bypassAgeGate(page: Page): Promise<void> {
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(2000);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('='.repeat(60));
|
||||
console.log('Extracting ALL product elements');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', (req) => {
|
||||
if (['image', 'font', 'media'].includes(req.resourceType())) {
|
||||
req.abort();
|
||||
} else {
|
||||
req.continue();
|
||||
}
|
||||
});
|
||||
|
||||
const url = `https://${STORE_ID}.treez.io/onlinemenu/brands?customerType=ADULT`;
|
||||
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
|
||||
await sleep(3000);
|
||||
await bypassAgeGate(page);
|
||||
await sleep(2000);
|
||||
|
||||
// Get ALL elements with product_product__ class
|
||||
console.log('\n[1] Counting all product_product__ elements...');
|
||||
|
||||
const elementAnalysis = await page.evaluate(() => {
|
||||
const all = document.querySelectorAll('[class*="product_product__"]');
|
||||
const byTag: Record<string, number> = {};
|
||||
const anchorHrefs: string[] = [];
|
||||
const imgAlts: string[] = [];
|
||||
|
||||
all.forEach(el => {
|
||||
const tag = el.tagName;
|
||||
byTag[tag] = (byTag[tag] || 0) + 1;
|
||||
|
||||
if (tag === 'A') {
|
||||
const href = el.getAttribute('href');
|
||||
if (href && href.includes('/product/')) {
|
||||
anchorHrefs.push(href);
|
||||
}
|
||||
}
|
||||
|
||||
if (tag === 'IMG') {
|
||||
const alt = el.getAttribute('alt');
|
||||
if (alt) imgAlts.push(alt);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
total: all.length,
|
||||
byTag,
|
||||
anchorHrefs: anchorHrefs.slice(0, 20),
|
||||
uniqueAnchors: new Set(anchorHrefs).size,
|
||||
imgAlts: imgAlts.slice(0, 20),
|
||||
uniqueImgAlts: new Set(imgAlts).size,
|
||||
};
|
||||
});
|
||||
|
||||
console.log(`Total elements: ${elementAnalysis.total}`);
|
||||
console.log(`By tag:`, elementAnalysis.byTag);
|
||||
console.log(`Unique anchor hrefs: ${elementAnalysis.uniqueAnchors}`);
|
||||
console.log(`Unique image alts: ${elementAnalysis.uniqueImgAlts}`);
|
||||
console.log(`\nSample anchor hrefs:`, elementAnalysis.anchorHrefs.slice(0, 5));
|
||||
console.log(`Sample image alts:`, elementAnalysis.imgAlts.slice(0, 5));
|
||||
|
||||
// Try to extract using different approaches
|
||||
console.log('\n[2] Testing extraction approaches...');
|
||||
|
||||
const approaches = await page.evaluate(() => {
|
||||
const results: Record<string, { count: number; unique: number; sample: string[] }> = {};
|
||||
|
||||
// Approach 1: Anchor elements with product links
|
||||
const anchors = document.querySelectorAll('a[href*="/product/"]');
|
||||
const anchorNames = new Set<string>();
|
||||
anchors.forEach(a => {
|
||||
const img = a.querySelector('img');
|
||||
const name = img?.getAttribute('alt') || a.textContent?.trim().split('\n')[0] || '';
|
||||
if (name) anchorNames.add(name);
|
||||
});
|
||||
results['a[href*="/product/"]'] = {
|
||||
count: anchors.length,
|
||||
unique: anchorNames.size,
|
||||
sample: Array.from(anchorNames).slice(0, 5),
|
||||
};
|
||||
|
||||
// Approach 2: Images with alt text inside product areas
|
||||
const productImgs = document.querySelectorAll('[class*="product_product__"] img[alt]');
|
||||
const imgNames = new Set<string>();
|
||||
productImgs.forEach(img => {
|
||||
const alt = img.getAttribute('alt');
|
||||
if (alt && alt.length > 2) imgNames.add(alt);
|
||||
});
|
||||
results['[class*="product_product__"] img[alt]'] = {
|
||||
count: productImgs.length,
|
||||
unique: imgNames.size,
|
||||
sample: Array.from(imgNames).slice(0, 5),
|
||||
};
|
||||
|
||||
// Approach 3: H5 elements (product names)
|
||||
const h5s = document.querySelectorAll('h5.product_product__name__JcEk0, h5[class*="product__name"]');
|
||||
const h5Names = new Set<string>();
|
||||
h5s.forEach(h5 => {
|
||||
const text = h5.textContent?.trim();
|
||||
if (text) h5Names.add(text);
|
||||
});
|
||||
results['h5[class*="product__name"]'] = {
|
||||
count: h5s.length,
|
||||
unique: h5Names.size,
|
||||
sample: Array.from(h5Names).slice(0, 5),
|
||||
};
|
||||
|
||||
// Approach 4: Link class with product_product__
|
||||
const links = document.querySelectorAll('a.product_product__ERWtJ, a[class*="product_product__"][class*="link"]');
|
||||
const linkNames = new Set<string>();
|
||||
links.forEach(link => {
|
||||
const h5 = link.querySelector('h5');
|
||||
const img = link.querySelector('img');
|
||||
const name = h5?.textContent?.trim() || img?.getAttribute('alt') || '';
|
||||
if (name) linkNames.add(name);
|
||||
});
|
||||
results['a.product_product__ERWtJ'] = {
|
||||
count: links.length,
|
||||
unique: linkNames.size,
|
||||
sample: Array.from(linkNames).slice(0, 5),
|
||||
};
|
||||
|
||||
return results;
|
||||
});
|
||||
|
||||
Object.entries(approaches).forEach(([sel, data]) => {
|
||||
console.log(`\n${sel}:`);
|
||||
console.log(` Count: ${data.count}, Unique: ${data.unique}`);
|
||||
console.log(` Sample: ${data.sample.join(', ')}`);
|
||||
});
|
||||
|
||||
// The best approach: use images with alt as the source of truth
|
||||
console.log('\n[3] Full product extraction using img[alt] approach...');
|
||||
|
||||
const products = await page.evaluate(() => {
|
||||
const seen = new Set<string>();
|
||||
const products: { name: string; href: string; price: string }[] = [];
|
||||
|
||||
// Get all product links
|
||||
document.querySelectorAll('a[href*="/product/"]').forEach(a => {
|
||||
const img = a.querySelector('img');
|
||||
const name = img?.getAttribute('alt') || '';
|
||||
|
||||
if (!name || seen.has(name)) return;
|
||||
seen.add(name);
|
||||
|
||||
const href = a.getAttribute('href') || '';
|
||||
|
||||
// Get price from within the link or parent
|
||||
let price = '';
|
||||
const priceEl = a.querySelector('[class*="price"]');
|
||||
if (priceEl) {
|
||||
const priceMatch = priceEl.textContent?.match(/\$(\d+(?:\.\d{2})?)/);
|
||||
price = priceMatch ? priceMatch[1] : '';
|
||||
}
|
||||
|
||||
products.push({ name, href, price });
|
||||
});
|
||||
|
||||
return products;
|
||||
});
|
||||
|
||||
console.log(`Extracted ${products.length} unique products`);
|
||||
console.log('\nSample products:');
|
||||
products.slice(0, 10).forEach(p => {
|
||||
console.log(` - ${p.name} | ${p.price ? '$' + p.price : 'N/A'} | ${p.href.slice(0, 40)}...`);
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
52
backend/scripts/test-treez-api.ts
Normal file
52
backend/scripts/test-treez-api.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import axios from 'axios';
|
||||
|
||||
async function main() {
|
||||
const url = 'https://search-kyrok9udlk.gapcommerceapi.com/product/search';
|
||||
|
||||
const query = {
|
||||
from: 0,
|
||||
size: 500,
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{ bool: { filter: { range: { customMinPrice: { gte: 0.01, lte: 500000 }}}}},
|
||||
{ bool: { should: [{ match: { isAboveThreshold: true }}]}},
|
||||
{ bool: { should: [{ match: { isHideFromMenu: false }}]}}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
console.log('Querying Treez Elasticsearch API...\n');
|
||||
|
||||
try {
|
||||
const response = await axios.post(url, query, {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
const data = response.data;
|
||||
const total = data.hits?.total?.value || data.hits?.total;
|
||||
const products = data.hits?.hits || [];
|
||||
|
||||
console.log('Total products: ' + total);
|
||||
console.log('Products returned: ' + products.length + '\n');
|
||||
|
||||
if (products.length > 0) {
|
||||
const first = products[0]._source;
|
||||
console.log('=== PRODUCT FIELDS AVAILABLE ===\n');
|
||||
console.log(Object.keys(first).sort().join('\n'));
|
||||
|
||||
console.log('\n=== SAMPLE PRODUCT ===\n');
|
||||
console.log(JSON.stringify(first, null, 2));
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
console.log('Error: ' + err.message);
|
||||
if (err.response) {
|
||||
console.log('Status: ' + err.response.status);
|
||||
console.log('Data: ' + JSON.stringify(err.response.data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
97
backend/scripts/test-treez-auth-api.ts
Normal file
97
backend/scripts/test-treez-auth-api.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import axios from 'axios';
|
||||
|
||||
async function main() {
|
||||
// Test Elasticsearch API with API key
|
||||
console.log('=== ELASTICSEARCH API ===\n');
|
||||
|
||||
const esUrl = 'https://search-kyrok9udlk.gapcommerceapi.com/product/search';
|
||||
const apiKey = 'V3jHL9dFzi3Gj4UISM4lr38Nm0GSxcps5OBz1PbS';
|
||||
|
||||
const query = {
|
||||
from: 0,
|
||||
size: 1000,
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{ bool: { filter: { range: { customMinPrice: { gte: 0.01, lte: 500000 }}}}},
|
||||
{ bool: { should: [{ match: { isAboveThreshold: true }}]}},
|
||||
{ bool: { should: [{ match: { isHideFromMenu: false }}]}}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios.post(esUrl, query, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': apiKey,
|
||||
'Origin': 'https://shop.bestdispensary.com',
|
||||
'Referer': 'https://shop.bestdispensary.com/',
|
||||
},
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
const data = response.data;
|
||||
const total = data.hits?.total?.value || data.hits?.total;
|
||||
const products = data.hits?.hits || [];
|
||||
|
||||
console.log('Total products: ' + total);
|
||||
console.log('Products returned: ' + products.length);
|
||||
|
||||
if (products.length > 0) {
|
||||
const first = products[0]._source;
|
||||
console.log('\n=== PRODUCT FIELDS ===\n');
|
||||
console.log(Object.keys(first).sort().join('\n'));
|
||||
|
||||
console.log('\n=== SAMPLE PRODUCT ===\n');
|
||||
console.log(JSON.stringify(first, null, 2));
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
console.log('Elasticsearch Error: ' + err.message);
|
||||
if (err.response) {
|
||||
console.log('Status: ' + err.response.status);
|
||||
}
|
||||
}
|
||||
|
||||
// Test Treez Headless API
|
||||
console.log('\n\n=== TREEZ HEADLESS API ===\n');
|
||||
|
||||
const treezUrl = 'https://headless.treez.io/v2.0/dispensary/best/ecommerce/discounts?excludeInactive=true&hideUnset=true&includeProdInfo=true';
|
||||
|
||||
try {
|
||||
const response = await axios.get(treezUrl, {
|
||||
headers: {
|
||||
'client_id': '29dce682258145c6b1cf71027282d083',
|
||||
'client_secret': 'A57bB49AfD7F4233B1750a0B501B4E16',
|
||||
'cache-control': 'max-age=0, no-cache, must-revalidate, proxy-revalidate',
|
||||
'Origin': 'https://shop.bestdispensary.com',
|
||||
'Referer': 'https://shop.bestdispensary.com/',
|
||||
},
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
const data = response.data;
|
||||
console.log('Response type: ' + typeof data);
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
console.log('Array length: ' + data.length);
|
||||
if (data.length > 0) {
|
||||
console.log('First item: ' + JSON.stringify(data[0], null, 2).slice(0, 1000));
|
||||
}
|
||||
} else {
|
||||
console.log('Keys: ' + Object.keys(data).join(', '));
|
||||
console.log('Data: ' + JSON.stringify(data, null, 2).slice(0, 2000));
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
console.log('Treez Error: ' + err.message);
|
||||
if (err.response) {
|
||||
console.log('Status: ' + err.response.status);
|
||||
console.log('Data: ' + JSON.stringify(err.response.data).slice(0, 500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
243
backend/scripts/test-treez-brand-products.ts
Normal file
243
backend/scripts/test-treez-brand-products.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* Visit each brand page and extract products
|
||||
*/
|
||||
|
||||
import puppeteer, { Page } from 'puppeteer';
|
||||
|
||||
const STORE_ID = 'best';
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function bypassAgeGate(page: Page): Promise<void> {
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(2000);
|
||||
}
|
||||
}
|
||||
|
||||
async function scrollToLoadAll(page: Page): Promise<void> {
|
||||
let previousHeight = 0;
|
||||
let sameCount = 0;
|
||||
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const currentHeight = await page.evaluate(() => document.body.scrollHeight);
|
||||
if (currentHeight === previousHeight) {
|
||||
sameCount++;
|
||||
if (sameCount >= 3) break;
|
||||
} else {
|
||||
sameCount = 0;
|
||||
}
|
||||
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
||||
await sleep(1000);
|
||||
previousHeight = currentHeight;
|
||||
}
|
||||
}
|
||||
|
||||
async function extractProducts(page: Page): Promise<{ name: string; price: string; href: string }[]> {
|
||||
return page.evaluate(() => {
|
||||
const products: { name: string; price: string; href: string }[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
document.querySelectorAll('a[href*="/product/"]').forEach(a => {
|
||||
const href = a.getAttribute('href') || '';
|
||||
const img = a.querySelector('img');
|
||||
const h5 = a.querySelector('h5');
|
||||
const name = img?.getAttribute('alt') || h5?.textContent?.trim() || '';
|
||||
|
||||
if (!name || seen.has(name)) return;
|
||||
seen.add(name);
|
||||
|
||||
const priceEl = a.querySelector('[class*="price"]');
|
||||
const priceMatch = priceEl?.textContent?.match(/\$(\d+(?:\.\d{2})?)/);
|
||||
const price = priceMatch ? priceMatch[1] : '';
|
||||
|
||||
products.push({ name, price, href });
|
||||
});
|
||||
|
||||
return products;
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('='.repeat(60));
|
||||
console.log('Extracting Products from All Brands');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', (req) => {
|
||||
if (['image', 'font', 'media'].includes(req.resourceType())) {
|
||||
req.abort();
|
||||
} else {
|
||||
req.continue();
|
||||
}
|
||||
});
|
||||
|
||||
// Go to brands page and get all brand links
|
||||
const brandsUrl = `https://${STORE_ID}.treez.io/onlinemenu/brands?customerType=ADULT`;
|
||||
console.log(`\n[1] Getting brand list from ${brandsUrl}`);
|
||||
|
||||
await page.goto(brandsUrl, { waitUntil: 'networkidle2', timeout: 60000 });
|
||||
await sleep(3000);
|
||||
await bypassAgeGate(page);
|
||||
await sleep(2000);
|
||||
|
||||
// The 142 items on /brands ARE brands (shown as product cards with brand info)
|
||||
// Get the brand names from the product hrefs (they contain brand name in URL)
|
||||
const brandInfo = await page.evaluate(() => {
|
||||
const brands: { name: string; slug: string }[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
// Extract brand info from product URLs
|
||||
// URL pattern: /product/{brand}-{product}-{details}
|
||||
document.querySelectorAll('a[href*="/product/"]').forEach(a => {
|
||||
const href = a.getAttribute('href') || '';
|
||||
// Try to extract brand from URL - first segment before product name
|
||||
const match = href.match(/\/product\/([^-]+(?:-[^-]+)?)-/);
|
||||
if (match) {
|
||||
const slug = match[1];
|
||||
if (!seen.has(slug)) {
|
||||
seen.add(slug);
|
||||
// Also look for brand text in the card
|
||||
const brandEl = a.querySelector('[class*="brand"], [class*="Brand"]');
|
||||
const name = brandEl?.textContent?.trim() || slug;
|
||||
brands.push({ name, slug });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return brands;
|
||||
});
|
||||
|
||||
console.log(`Found ${brandInfo.length} potential brands from product URLs`);
|
||||
console.log('Sample:', brandInfo.slice(0, 5));
|
||||
|
||||
// Actually, let's look for brand page links directly
|
||||
console.log('\n[2] Looking for brand page links...');
|
||||
|
||||
const brandLinks = await page.evaluate(() => {
|
||||
const links: { name: string; href: string }[] = [];
|
||||
|
||||
// Look for links to /brand/ pages
|
||||
document.querySelectorAll('a[href*="/brand/"]').forEach(a => {
|
||||
const href = a.getAttribute('href') || '';
|
||||
const text = a.textContent?.trim() || '';
|
||||
if (href && !links.some(l => l.href === href)) {
|
||||
links.push({ name: text, href });
|
||||
}
|
||||
});
|
||||
|
||||
return links;
|
||||
});
|
||||
|
||||
console.log(`Found ${brandLinks.length} brand page links`);
|
||||
if (brandLinks.length > 0) {
|
||||
console.log('Sample:', brandLinks.slice(0, 10));
|
||||
}
|
||||
|
||||
// If no brand links, try to find them in section headers
|
||||
console.log('\n[3] Looking for brand sections...');
|
||||
|
||||
const brandSections = await page.evaluate(() => {
|
||||
const sections: { brandName: string; sampleProduct: string }[] = [];
|
||||
|
||||
document.querySelectorAll('[class*="products_product__section"]').forEach(section => {
|
||||
const header = section.querySelector('h2, h3, [class*="heading"]');
|
||||
const brandName = header?.textContent?.trim() || '';
|
||||
const firstProduct = section.querySelector('a[href*="/product/"]');
|
||||
const productName = firstProduct?.querySelector('h5')?.textContent?.trim() ||
|
||||
firstProduct?.querySelector('img')?.getAttribute('alt') || '';
|
||||
|
||||
if (brandName) {
|
||||
sections.push({ brandName, sampleProduct: productName });
|
||||
}
|
||||
});
|
||||
|
||||
return sections;
|
||||
});
|
||||
|
||||
console.log(`Found ${brandSections.length} brand sections`);
|
||||
brandSections.slice(0, 10).forEach(s => {
|
||||
console.log(` - Brand: "${s.brandName}" | Sample: "${s.sampleProduct}"`);
|
||||
});
|
||||
|
||||
// Try visiting a brand page directly using the section name
|
||||
if (brandSections.length > 0) {
|
||||
console.log('\n[4] Testing brand page URLs...');
|
||||
|
||||
// Try different URL patterns for first brand
|
||||
const testBrand = brandSections[0].brandName;
|
||||
const testSlug = testBrand.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
||||
|
||||
const urlPatterns = [
|
||||
`/onlinemenu/brand/${encodeURIComponent(testBrand)}`,
|
||||
`/onlinemenu/brand/${testSlug}`,
|
||||
`/brand/${encodeURIComponent(testBrand)}`,
|
||||
`/brand/${testSlug}`,
|
||||
];
|
||||
|
||||
for (const path of urlPatterns) {
|
||||
const testUrl = `https://${STORE_ID}.treez.io${path}?customerType=ADULT`;
|
||||
try {
|
||||
console.log(` Trying: ${testUrl}`);
|
||||
await page.goto(testUrl, { waitUntil: 'networkidle2', timeout: 15000 });
|
||||
await sleep(2000);
|
||||
|
||||
const products = await extractProducts(page);
|
||||
console.log(` Products found: ${products.length}`);
|
||||
|
||||
if (products.length > 0) {
|
||||
console.log(` ✓ Working URL pattern: ${path}`);
|
||||
break;
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.log(` Error: ${e.message.slice(0, 50)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if clicking on a brand section leads to a brand page
|
||||
console.log('\n[5] Checking if brand sections have clickable headers...');
|
||||
|
||||
await page.goto(brandsUrl, { waitUntil: 'networkidle2', timeout: 60000 });
|
||||
await sleep(3000);
|
||||
|
||||
const clickableHeaders = await page.evaluate(() => {
|
||||
const results: { text: string; tag: string; href: string; clickable: boolean }[] = [];
|
||||
|
||||
document.querySelectorAll('[class*="products_product__section"] h2, [class*="products_product__section"] h3').forEach(header => {
|
||||
const link = header.closest('a') || header.querySelector('a');
|
||||
const text = header.textContent?.trim() || '';
|
||||
const href = link?.getAttribute('href') || '';
|
||||
|
||||
results.push({
|
||||
text,
|
||||
tag: header.tagName,
|
||||
href,
|
||||
clickable: !!link,
|
||||
});
|
||||
});
|
||||
|
||||
return results;
|
||||
});
|
||||
|
||||
console.log('Section headers:');
|
||||
clickableHeaders.slice(0, 10).forEach(h => {
|
||||
console.log(` [${h.tag}] "${h.text}" - ${h.clickable ? `Link: ${h.href}` : 'Not clickable'}`);
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
183
backend/scripts/test-treez-brands-detailed.ts
Normal file
183
backend/scripts/test-treez-brands-detailed.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* Detailed brand section analysis
|
||||
*/
|
||||
|
||||
import puppeteer, { Page } from 'puppeteer';
|
||||
|
||||
const STORE_ID = 'best';
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function bypassAgeGate(page: Page): Promise<void> {
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
console.log(' Age gate detected, bypassing...');
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(2000);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('='.repeat(60));
|
||||
console.log('Detailed Brand Section Analysis');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', (req) => {
|
||||
if (['image', 'font', 'media'].includes(req.resourceType())) {
|
||||
req.abort();
|
||||
} else {
|
||||
req.continue();
|
||||
}
|
||||
});
|
||||
|
||||
const url = `https://${STORE_ID}.treez.io/onlinemenu/brands?customerType=ADULT`;
|
||||
console.log(`\nNavigating to ${url}`);
|
||||
|
||||
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
|
||||
await sleep(3000);
|
||||
await bypassAgeGate(page);
|
||||
await sleep(2000);
|
||||
|
||||
// Scroll multiple times to load all content
|
||||
console.log('\n[1] Scrolling to load all content...');
|
||||
let previousHeight = 0;
|
||||
let scrollCount = 0;
|
||||
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
||||
await sleep(1500);
|
||||
|
||||
const currentHeight = await page.evaluate(() => document.body.scrollHeight);
|
||||
const productCount = await page.evaluate(() =>
|
||||
document.querySelectorAll('a[href*="/product/"]').length
|
||||
);
|
||||
|
||||
console.log(` Scroll ${i + 1}: height=${currentHeight}, products=${productCount}`);
|
||||
|
||||
if (currentHeight === previousHeight) {
|
||||
scrollCount++;
|
||||
if (scrollCount >= 3) break;
|
||||
} else {
|
||||
scrollCount = 0;
|
||||
}
|
||||
previousHeight = currentHeight;
|
||||
}
|
||||
|
||||
// Look at ALL h2/h3 headers on page
|
||||
console.log('\n[2] Finding ALL h2/h3 headers on page...');
|
||||
|
||||
const headers = await page.evaluate(() => {
|
||||
const results: { tag: string; text: string; parentClass: string }[] = [];
|
||||
|
||||
document.querySelectorAll('h2, h3').forEach((el: Element) => {
|
||||
results.push({
|
||||
tag: el.tagName,
|
||||
text: el.textContent?.trim().slice(0, 80) || '',
|
||||
parentClass: el.parentElement?.className?.slice(0, 50) || '',
|
||||
});
|
||||
});
|
||||
|
||||
return results;
|
||||
});
|
||||
|
||||
console.log(`Found ${headers.length} headers:`);
|
||||
headers.forEach((h: { tag: string; text: string }) =>
|
||||
console.log(` [${h.tag}] "${h.text}"`)
|
||||
);
|
||||
|
||||
// Get products grouped by their section heading
|
||||
console.log('\n[3] Getting products per section...');
|
||||
|
||||
const sectionProducts = await page.evaluate(() => {
|
||||
const results: { heading: string; products: number }[] = [];
|
||||
|
||||
// Find all sections that contain products
|
||||
document.querySelectorAll('[class*="products_product__section"]').forEach((section: Element) => {
|
||||
const heading = section.querySelector('h2, h3');
|
||||
const headingText = heading?.textContent?.trim() || 'Unknown';
|
||||
const products = section.querySelectorAll('a[href*="/product/"]');
|
||||
|
||||
results.push({
|
||||
heading: headingText,
|
||||
products: products.length,
|
||||
});
|
||||
});
|
||||
|
||||
return results;
|
||||
});
|
||||
|
||||
console.log(`Found ${sectionProducts.length} brand sections:`);
|
||||
let totalProducts = 0;
|
||||
sectionProducts.forEach((s: { heading: string; products: number }) => {
|
||||
console.log(` ${s.heading}: ${s.products} products`);
|
||||
totalProducts += s.products;
|
||||
});
|
||||
console.log(`\nTotal products across all sections: ${totalProducts}`);
|
||||
|
||||
// Also extract brand from each product's URL/card
|
||||
console.log('\n[4] Extracting brand from product URLs/cards...');
|
||||
|
||||
const brandCounts = await page.evaluate(() => {
|
||||
const byBrand: Record<string, number> = {};
|
||||
const seen = new Set<string>();
|
||||
|
||||
document.querySelectorAll('a[href*="/product/"]').forEach((a: Element) => {
|
||||
const href = a.getAttribute('href') || '';
|
||||
const img = a.querySelector('img');
|
||||
const name = img?.getAttribute('alt') || '';
|
||||
|
||||
if (!name || seen.has(name)) return;
|
||||
seen.add(name);
|
||||
|
||||
// Try to find brand from the card
|
||||
const brandEl = a.querySelector('[class*="brand"], [class*="Brand"], span, p');
|
||||
let brand = '';
|
||||
|
||||
// Try various methods to find brand
|
||||
const allSpans = a.querySelectorAll('span, p');
|
||||
allSpans.forEach((span: Element) => {
|
||||
const text = span.textContent?.trim() || '';
|
||||
if (text && text.length < 50 && text !== name && !text.includes('$')) {
|
||||
if (!brand) brand = text;
|
||||
}
|
||||
});
|
||||
|
||||
// Fallback: get brand from parent section heading
|
||||
if (!brand) {
|
||||
const section = a.closest('[class*="products_product__section"]');
|
||||
const heading = section?.querySelector('h2, h3');
|
||||
brand = heading?.textContent?.trim() || 'Unknown';
|
||||
}
|
||||
|
||||
byBrand[brand] = (byBrand[brand] || 0) + 1;
|
||||
});
|
||||
|
||||
return byBrand;
|
||||
});
|
||||
|
||||
console.log('Products by brand:');
|
||||
Object.entries(brandCounts)
|
||||
.sort((a, b) => (b[1] as number) - (a[1] as number))
|
||||
.forEach(([brand, count]) => {
|
||||
console.log(` ${brand}: ${count}`);
|
||||
});
|
||||
|
||||
const uniqueTotal = Object.values(brandCounts).reduce((sum: number, c) => sum + (c as number), 0);
|
||||
console.log(`\nTotal unique products: ${uniqueTotal}`);
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
113
backend/scripts/test-treez-capture-auth.ts
Normal file
113
backend/scripts/test-treez-capture-auth.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
// Capture request headers for API calls
|
||||
const apiRequests: any[] = [];
|
||||
|
||||
page.on('request', (req) => {
|
||||
const url = req.url();
|
||||
if (url.includes('treez.io') || url.includes('gapcommerce')) {
|
||||
apiRequests.push({
|
||||
url: url,
|
||||
method: req.method(),
|
||||
headers: req.headers(),
|
||||
postData: req.postData(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Loading page to capture API auth headers...\n');
|
||||
|
||||
await page.goto('https://shop.bestdispensary.com/shop', {
|
||||
waitUntil: 'networkidle2',
|
||||
timeout: 60000
|
||||
});
|
||||
await sleep(3000);
|
||||
|
||||
// Bypass age gate
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(2000);
|
||||
}
|
||||
|
||||
console.log('=== API REQUESTS WITH HEADERS ===\n');
|
||||
|
||||
apiRequests.forEach((req, i) => {
|
||||
console.log((i+1) + '. ' + req.method + ' ' + req.url.slice(0, 100));
|
||||
console.log(' Headers:');
|
||||
Object.entries(req.headers).forEach(([k, v]) => {
|
||||
if (k.toLowerCase().includes('auth') ||
|
||||
k.toLowerCase().includes('token') ||
|
||||
k.toLowerCase().includes('key') ||
|
||||
k.toLowerCase().includes('api') ||
|
||||
k.toLowerCase() === 'authorization' ||
|
||||
k.toLowerCase() === 'x-api-key') {
|
||||
console.log(' >>> ' + k + ': ' + v);
|
||||
}
|
||||
});
|
||||
// Show all headers for treez.io requests
|
||||
if (req.url.includes('headless.treez.io')) {
|
||||
console.log(' ALL HEADERS:');
|
||||
Object.entries(req.headers).forEach(([k, v]) => {
|
||||
console.log(' ' + k + ': ' + String(v).slice(0, 80));
|
||||
});
|
||||
}
|
||||
console.log('');
|
||||
});
|
||||
|
||||
// Also check for API keys in page scripts
|
||||
console.log('=== CHECKING FOR API KEYS IN PAGE ===\n');
|
||||
|
||||
const pageData = await page.evaluate(() => {
|
||||
const data: any = {};
|
||||
|
||||
// Check window object for API keys
|
||||
const win = window as any;
|
||||
if (win.__NEXT_DATA__) {
|
||||
data.nextData = win.__NEXT_DATA__;
|
||||
}
|
||||
|
||||
// Check for any global config
|
||||
if (win.config || win.CONFIG) {
|
||||
data.config = win.config || win.CONFIG;
|
||||
}
|
||||
|
||||
// Look for treez-related globals
|
||||
Object.keys(win).forEach(key => {
|
||||
if (key.toLowerCase().includes('treez') ||
|
||||
key.toLowerCase().includes('api') ||
|
||||
key.toLowerCase().includes('config')) {
|
||||
try {
|
||||
data[key] = JSON.stringify(win[key]).slice(0, 500);
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
if (pageData.nextData?.props?.pageProps) {
|
||||
console.log('Next.js pageProps keys: ' + Object.keys(pageData.nextData.props.pageProps).join(', '));
|
||||
}
|
||||
if (pageData.nextData?.runtimeConfig) {
|
||||
console.log('Runtime config: ' + JSON.stringify(pageData.nextData.runtimeConfig).slice(0, 500));
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main();
|
||||
100
backend/scripts/test-treez-capture-response.ts
Normal file
100
backend/scripts/test-treez-capture-response.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
import fs from 'fs';
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
// Capture ES API responses
|
||||
let allProductData: any[] = [];
|
||||
|
||||
page.on('response', async (res) => {
|
||||
const url = res.url();
|
||||
if (url.includes('gapcommerceapi.com/product/search') && res.status() === 200) {
|
||||
try {
|
||||
const json = await res.json();
|
||||
const products = json.hits?.hits?.map((h: any) => h._source) || [];
|
||||
allProductData = allProductData.concat(products);
|
||||
console.log('Captured ' + products.length + ' products (total: ' + allProductData.length + ')');
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Loading /shop page to capture product data...\n');
|
||||
|
||||
await page.goto('https://shop.bestdispensary.com/shop', {
|
||||
waitUntil: 'networkidle2',
|
||||
timeout: 60000
|
||||
});
|
||||
await sleep(3000);
|
||||
|
||||
// Bypass age gate
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(2000);
|
||||
}
|
||||
|
||||
// Click load more many times to get all products
|
||||
console.log('\nClicking Load More to capture all products...');
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const btn = await page.$('button.collection__load-more');
|
||||
if (!btn) {
|
||||
console.log('No more Load More button');
|
||||
break;
|
||||
}
|
||||
|
||||
const isVisible = await page.evaluate((b) => {
|
||||
const rect = b.getBoundingClientRect();
|
||||
return rect.width > 0 && rect.height > 0;
|
||||
}, btn);
|
||||
|
||||
if (!isVisible) {
|
||||
console.log('Load More not visible');
|
||||
break;
|
||||
}
|
||||
|
||||
await btn.click();
|
||||
await sleep(1500);
|
||||
console.log('Click ' + (i+1) + ': ' + allProductData.length + ' total products');
|
||||
}
|
||||
|
||||
console.log('\n=== RESULTS ===\n');
|
||||
console.log('Total products captured: ' + allProductData.length);
|
||||
|
||||
if (allProductData.length > 0) {
|
||||
// Dedupe by some ID
|
||||
const seen = new Set();
|
||||
const unique = allProductData.filter(p => {
|
||||
const id = p.id || p.productId || p.name;
|
||||
if (seen.has(id)) return false;
|
||||
seen.add(id);
|
||||
return true;
|
||||
});
|
||||
|
||||
console.log('Unique products: ' + unique.length);
|
||||
console.log('\n=== PRODUCT FIELDS ===\n');
|
||||
console.log(Object.keys(unique[0]).sort().join('\n'));
|
||||
|
||||
console.log('\n=== SAMPLE PRODUCT ===\n');
|
||||
console.log(JSON.stringify(unique[0], null, 2));
|
||||
|
||||
// Save to file
|
||||
fs.writeFileSync('/tmp/treez-products.json', JSON.stringify(unique, null, 2));
|
||||
console.log('\nSaved to /tmp/treez-products.json');
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main();
|
||||
88
backend/scripts/test-treez-capture-text.ts
Normal file
88
backend/scripts/test-treez-capture-text.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
import fs from 'fs';
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
// Capture ES API responses as text
|
||||
let allProducts: any[] = [];
|
||||
|
||||
page.on('response', async (res) => {
|
||||
const url = res.url();
|
||||
if (url.includes('gapcommerceapi.com/product/search')) {
|
||||
console.log('ES Response: status=' + res.status());
|
||||
if (res.status() === 200) {
|
||||
try {
|
||||
const text = await res.text();
|
||||
console.log('Response length: ' + text.length);
|
||||
const json = JSON.parse(text);
|
||||
const products = json.hits?.hits?.map((h: any) => h._source) || [];
|
||||
allProducts = allProducts.concat(products);
|
||||
console.log('Got ' + products.length + ' products (total: ' + allProducts.length + ')');
|
||||
} catch (err: any) {
|
||||
console.log('Parse error: ' + err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Loading page...\n');
|
||||
|
||||
await page.goto('https://shop.bestdispensary.com/shop', {
|
||||
waitUntil: 'networkidle2',
|
||||
timeout: 60000
|
||||
});
|
||||
await sleep(5000);
|
||||
|
||||
// Bypass age gate
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
console.log('Bypassing age gate...');
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(3000);
|
||||
}
|
||||
|
||||
// Wait for initial products to load
|
||||
await sleep(3000);
|
||||
|
||||
console.log('\nInitial products captured: ' + allProducts.length);
|
||||
|
||||
// Try scrolling to trigger more loads
|
||||
console.log('\nScrolling...');
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
||||
await sleep(1500);
|
||||
|
||||
// Also click load more if present
|
||||
try {
|
||||
await page.click('button.collection__load-more');
|
||||
console.log('Clicked load more');
|
||||
} catch {}
|
||||
}
|
||||
|
||||
console.log('\n=== FINAL RESULTS ===\n');
|
||||
console.log('Total products: ' + allProducts.length);
|
||||
|
||||
if (allProducts.length > 0) {
|
||||
console.log('\nFields: ' + Object.keys(allProducts[0]).sort().join(', '));
|
||||
console.log('\nSample:\n' + JSON.stringify(allProducts[0], null, 2));
|
||||
|
||||
fs.writeFileSync('/tmp/treez-products.json', JSON.stringify(allProducts, null, 2));
|
||||
console.log('\nSaved to /tmp/treez-products.json');
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main();
|
||||
192
backend/scripts/test-treez-categories.ts
Normal file
192
backend/scripts/test-treez-categories.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Navigate to each category page and count products
|
||||
*/
|
||||
|
||||
import puppeteer, { Page } from 'puppeteer';
|
||||
|
||||
const STORE_ID = 'best';
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function bypassAgeGate(page: Page): Promise<void> {
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(2000);
|
||||
}
|
||||
}
|
||||
|
||||
async function scrollToLoadAll(page: Page): Promise<void> {
|
||||
let previousHeight = 0;
|
||||
let scrollCount = 0;
|
||||
let sameCount = 0;
|
||||
|
||||
while (scrollCount < 50) {
|
||||
const currentHeight = await page.evaluate(() => document.body.scrollHeight);
|
||||
if (currentHeight === previousHeight) {
|
||||
sameCount++;
|
||||
if (sameCount >= 3) break;
|
||||
} else {
|
||||
sameCount = 0;
|
||||
}
|
||||
|
||||
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
||||
await sleep(1500);
|
||||
previousHeight = currentHeight;
|
||||
scrollCount++;
|
||||
}
|
||||
}
|
||||
|
||||
async function countProducts(page: Page): Promise<number> {
|
||||
return page.evaluate(() => {
|
||||
const seen = new Set<string>();
|
||||
document.querySelectorAll('a[href*="/product/"]').forEach(a => {
|
||||
const img = a.querySelector('img');
|
||||
const name = img?.getAttribute('alt') || a.querySelector('h5')?.textContent?.trim() || '';
|
||||
if (name) seen.add(name);
|
||||
});
|
||||
return seen.size;
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('='.repeat(60));
|
||||
console.log('Testing Treez Category Pages');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', (req) => {
|
||||
if (['image', 'font', 'media'].includes(req.resourceType())) {
|
||||
req.abort();
|
||||
} else {
|
||||
req.continue();
|
||||
}
|
||||
});
|
||||
|
||||
// Categories from the nav menu
|
||||
const categories = [
|
||||
'cartridges',
|
||||
'flower',
|
||||
'pre-rolls',
|
||||
'edibles',
|
||||
'extracts',
|
||||
'tinctures',
|
||||
'capsules',
|
||||
'topicals',
|
||||
'accessories',
|
||||
'drink',
|
||||
];
|
||||
|
||||
const results: { category: string; products: number }[] = [];
|
||||
let ageGateBypassed = false;
|
||||
|
||||
for (const category of categories) {
|
||||
// Try different URL patterns
|
||||
const urls = [
|
||||
`https://${STORE_ID}.treez.io/onlinemenu/${category}?customerType=ADULT`,
|
||||
`https://${STORE_ID}.treez.io/onlinemenu/category/${category}?customerType=ADULT`,
|
||||
`https://${STORE_ID}.treez.io/${category}?customerType=ADULT`,
|
||||
];
|
||||
|
||||
for (const url of urls) {
|
||||
try {
|
||||
console.log(`\nTrying: ${url}`);
|
||||
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
|
||||
await sleep(2000);
|
||||
|
||||
if (!ageGateBypassed) {
|
||||
await bypassAgeGate(page);
|
||||
ageGateBypassed = true;
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
const initialCount = await countProducts(page);
|
||||
if (initialCount > 0) {
|
||||
console.log(` Initial: ${initialCount} products`);
|
||||
|
||||
await scrollToLoadAll(page);
|
||||
const finalCount = await countProducts(page);
|
||||
console.log(` After scroll: ${finalCount} products`);
|
||||
|
||||
results.push({ category, products: finalCount });
|
||||
break; // Found working URL, move to next category
|
||||
} else {
|
||||
console.log(` No products found`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log(` Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also try the main shop page
|
||||
console.log('\nTrying main shop page...');
|
||||
try {
|
||||
const shopUrl = `https://${STORE_ID}.treez.io/onlinemenu/shop?customerType=ADULT`;
|
||||
await page.goto(shopUrl, { waitUntil: 'networkidle2', timeout: 30000 });
|
||||
await sleep(2000);
|
||||
|
||||
const initialCount = await countProducts(page);
|
||||
console.log(`Shop page initial: ${initialCount} products`);
|
||||
|
||||
if (initialCount > 0) {
|
||||
await scrollToLoadAll(page);
|
||||
const finalCount = await countProducts(page);
|
||||
console.log(`Shop page after scroll: ${finalCount} products`);
|
||||
results.push({ category: 'shop', products: finalCount });
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log(`Shop page error: ${error.message}`);
|
||||
}
|
||||
|
||||
// Try to find and click on category links from the nav
|
||||
console.log('\n[Alternative] Trying to find nav category links...');
|
||||
|
||||
const homeUrl = `https://${STORE_ID}.treez.io/onlinemenu/?customerType=ADULT`;
|
||||
await page.goto(homeUrl, { waitUntil: 'networkidle2', timeout: 30000 });
|
||||
await sleep(3000);
|
||||
await bypassAgeGate(page);
|
||||
await sleep(1000);
|
||||
|
||||
const navLinks = await page.evaluate(() => {
|
||||
const links: { text: string; href: string }[] = [];
|
||||
document.querySelectorAll('nav a, [class*="nav"] a').forEach(a => {
|
||||
const text = a.textContent?.trim() || '';
|
||||
const href = a.getAttribute('href') || '';
|
||||
if (href && text && !links.some(l => l.href === href)) {
|
||||
links.push({ text, href });
|
||||
}
|
||||
});
|
||||
return links;
|
||||
});
|
||||
|
||||
console.log('Nav links found:');
|
||||
navLinks.forEach(l => console.log(` - "${l.text}" → ${l.href}`));
|
||||
|
||||
// Summary
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('Summary');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
let total = 0;
|
||||
results.forEach(r => {
|
||||
console.log(`${r.category}: ${r.products} products`);
|
||||
total += r.products;
|
||||
});
|
||||
console.log(`\nTotal across categories: ${total} products`);
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
160
backend/scripts/test-treez-containers.ts
Normal file
160
backend/scripts/test-treez-containers.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Find the correct product card container selector
|
||||
*/
|
||||
|
||||
import puppeteer, { Page } from 'puppeteer';
|
||||
|
||||
const STORE_ID = 'best';
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function bypassAgeGate(page: Page): Promise<void> {
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(2000);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Finding Treez product card containers...\n');
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', (req) => {
|
||||
if (['image', 'font', 'media'].includes(req.resourceType())) {
|
||||
req.abort();
|
||||
} else {
|
||||
req.continue();
|
||||
}
|
||||
});
|
||||
|
||||
const url = `https://${STORE_ID}.treez.io/onlinemenu/brands?customerType=ADULT`;
|
||||
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
|
||||
await sleep(3000);
|
||||
await bypassAgeGate(page);
|
||||
await sleep(2000);
|
||||
|
||||
// Find product card containers by looking for elements that contain both name AND price
|
||||
const analysis = await page.evaluate(() => {
|
||||
// Strategy: find all H5 elements (which contain names), then get their parent containers
|
||||
const nameElements = document.querySelectorAll('h5.product_product__name__JcEk0');
|
||||
const containers: Map<string, { count: number; sample: string }> = new Map();
|
||||
|
||||
nameElements.forEach(nameEl => {
|
||||
// Walk up to find the product card container
|
||||
let current = nameEl.parentElement;
|
||||
let depth = 0;
|
||||
|
||||
while (current && depth < 10) {
|
||||
const className = current.className?.toString?.() || '';
|
||||
|
||||
// Look for ProductCard in the class name
|
||||
if (className.includes('ProductCard')) {
|
||||
const key = className.slice(0, 100);
|
||||
const existing = containers.get(key) || { count: 0, sample: '' };
|
||||
existing.count++;
|
||||
if (!existing.sample) {
|
||||
existing.sample = current.outerHTML.slice(0, 300);
|
||||
}
|
||||
containers.set(key, existing);
|
||||
break;
|
||||
}
|
||||
|
||||
current = current.parentElement;
|
||||
depth++;
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(containers.entries()).map(([cls, data]) => ({
|
||||
class: cls,
|
||||
count: data.count,
|
||||
sample: data.sample,
|
||||
}));
|
||||
});
|
||||
|
||||
console.log('Product card containers found:');
|
||||
analysis.forEach(({ class: cls, count, sample }) => {
|
||||
console.log(`\n[${count}x] ${cls}`);
|
||||
console.log(`Sample: ${sample.slice(0, 200)}...`);
|
||||
});
|
||||
|
||||
// Now test various container selectors
|
||||
console.log('\n\n--- Testing container selectors ---');
|
||||
|
||||
const selectorTests = await page.evaluate(() => {
|
||||
const tests: Record<string, { total: number; withName: number; withPrice: number }> = {};
|
||||
|
||||
const selectors = [
|
||||
'[class*="ProductCardWithBtn"]',
|
||||
'[class*="ProductCard_product"]',
|
||||
'[class*="ProductCard__"]',
|
||||
'article[class*="product"]',
|
||||
'div[class*="ProductCard"]',
|
||||
'a[class*="ProductCard"]',
|
||||
'[class*="product_product__"][class*="link"]',
|
||||
'article',
|
||||
];
|
||||
|
||||
selectors.forEach(sel => {
|
||||
const elements = document.querySelectorAll(sel);
|
||||
let withName = 0;
|
||||
let withPrice = 0;
|
||||
|
||||
elements.forEach(el => {
|
||||
if (el.querySelector('h5, [class*="product__name"]')) withName++;
|
||||
if (el.querySelector('[class*="price"]')) withPrice++;
|
||||
});
|
||||
|
||||
tests[sel] = { total: elements.length, withName, withPrice };
|
||||
});
|
||||
|
||||
return tests;
|
||||
});
|
||||
|
||||
Object.entries(selectorTests).forEach(([sel, { total, withName, withPrice }]) => {
|
||||
console.log(`${sel}: ${total} total, ${withName} with name, ${withPrice} with price`);
|
||||
});
|
||||
|
||||
// Get the actual product card class pattern
|
||||
console.log('\n\n--- Finding exact product card class ---');
|
||||
|
||||
const exactClasses = await page.evaluate(() => {
|
||||
// Find elements that have both h5 name AND price child
|
||||
const allElements = document.querySelectorAll('*');
|
||||
const matches: { tag: string; class: string }[] = [];
|
||||
|
||||
allElements.forEach(el => {
|
||||
const hasName = el.querySelector('h5.product_product__name__JcEk0');
|
||||
const hasPrice = el.querySelector('[class*="price__ins"], [class*="price__"]');
|
||||
|
||||
if (hasName && hasPrice) {
|
||||
const className = el.className?.toString?.() || '';
|
||||
if (className && !matches.some(m => m.class === className)) {
|
||||
matches.push({ tag: el.tagName, class: className.slice(0, 150) });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return matches;
|
||||
});
|
||||
|
||||
console.log('Elements containing both name and price:');
|
||||
exactClasses.forEach(({ tag, class: cls }) => {
|
||||
console.log(` [${tag}] ${cls}`);
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
191
backend/scripts/test-treez-find-brands.ts
Normal file
191
backend/scripts/test-treez-find-brands.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Find actual brand elements on /brands page
|
||||
*/
|
||||
|
||||
import puppeteer, { Page } from 'puppeteer';
|
||||
|
||||
const STORE_ID = 'best';
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function bypassAgeGate(page: Page): Promise<void> {
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
console.log(' Age gate detected, bypassing...');
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(2000);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('='.repeat(60));
|
||||
console.log('Finding Brand Elements on /brands page');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', (req) => {
|
||||
if (['image', 'font', 'media'].includes(req.resourceType())) {
|
||||
req.abort();
|
||||
} else {
|
||||
req.continue();
|
||||
}
|
||||
});
|
||||
|
||||
const url = `https://${STORE_ID}.treez.io/onlinemenu/brands?customerType=ADULT`;
|
||||
console.log(`\nNavigating to ${url}`);
|
||||
|
||||
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
|
||||
await sleep(3000);
|
||||
await bypassAgeGate(page);
|
||||
await sleep(2000);
|
||||
|
||||
// Check current URL
|
||||
const currentUrl = page.url();
|
||||
console.log(`\nCurrent URL: ${currentUrl}`);
|
||||
|
||||
// Look for ANY links on the page that might be brand links
|
||||
console.log('\n[1] Looking for all anchor links with "brand" in href or class...');
|
||||
|
||||
const brandLinks = await page.evaluate(() => {
|
||||
const links: { href: string; text: string }[] = [];
|
||||
|
||||
document.querySelectorAll('a').forEach((a: Element) => {
|
||||
const href = a.getAttribute('href') || '';
|
||||
const text = a.textContent?.trim().slice(0, 50) || '';
|
||||
const className = a.className || '';
|
||||
|
||||
if (href.includes('brand') || href.includes('Brand') ||
|
||||
className.includes('brand') || className.includes('Brand')) {
|
||||
links.push({ href, text });
|
||||
}
|
||||
});
|
||||
|
||||
return links;
|
||||
});
|
||||
|
||||
console.log(`Found ${brandLinks.length} brand-related links:`);
|
||||
brandLinks.slice(0, 30).forEach(l => console.log(` "${l.text}" → ${l.href}`));
|
||||
|
||||
// Look for the navigation/dropdown
|
||||
console.log('\n[2] Looking at navigation structure...');
|
||||
|
||||
const navItems = await page.evaluate(() => {
|
||||
const items: string[] = [];
|
||||
|
||||
document.querySelectorAll('nav a, [class*="nav"] a, header a').forEach((a: Element) => {
|
||||
const text = a.textContent?.trim();
|
||||
const href = a.getAttribute('href') || '';
|
||||
if (text && text.length < 30) {
|
||||
items.push(`${text} (${href})`);
|
||||
}
|
||||
});
|
||||
|
||||
return [...new Set(items)];
|
||||
});
|
||||
|
||||
console.log('Navigation items:');
|
||||
navItems.forEach(item => console.log(` - ${item}`));
|
||||
|
||||
// Look for grid containers that might hold brand cards
|
||||
console.log('\n[3] Looking for brand card containers...');
|
||||
|
||||
const containers = await page.evaluate(() => {
|
||||
const results: { selector: string; count: number; sample: string }[] = [];
|
||||
|
||||
// Try various selectors for brand cards
|
||||
const selectors = [
|
||||
'[class*="brand_brand"]',
|
||||
'[class*="brands_brand"]',
|
||||
'[class*="brand-card"]',
|
||||
'[class*="brandCard"]',
|
||||
'[class*="BrandCard"]',
|
||||
'a[href*="/brand/"]',
|
||||
'[data-testid*="brand"]',
|
||||
];
|
||||
|
||||
selectors.forEach(sel => {
|
||||
const els = document.querySelectorAll(sel);
|
||||
if (els.length > 0) {
|
||||
const first = els[0];
|
||||
results.push({
|
||||
selector: sel,
|
||||
count: els.length,
|
||||
sample: first.textContent?.trim().slice(0, 50) || '',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
});
|
||||
|
||||
console.log('Brand containers found:');
|
||||
containers.forEach(c => console.log(` ${c.selector}: ${c.count} elements, sample: "${c.sample}"`));
|
||||
|
||||
// Get ALL unique hrefs that contain /brand/
|
||||
console.log('\n[4] All links containing "/brand/" in href...');
|
||||
|
||||
const brandHrefs = await page.evaluate(() => {
|
||||
const hrefs: string[] = [];
|
||||
|
||||
document.querySelectorAll('a[href*="/brand/"]').forEach((a: Element) => {
|
||||
const href = a.getAttribute('href');
|
||||
if (href && !hrefs.includes(href)) {
|
||||
hrefs.push(href);
|
||||
}
|
||||
});
|
||||
|
||||
return hrefs;
|
||||
});
|
||||
|
||||
console.log(`Found ${brandHrefs.length} unique brand hrefs:`);
|
||||
brandHrefs.forEach(href => console.log(` ${href}`));
|
||||
|
||||
// Take screenshot
|
||||
await page.screenshot({ path: '/tmp/treez-brands-page.png', fullPage: false });
|
||||
console.log('\n[5] Screenshot saved to /tmp/treez-brands-page.png');
|
||||
|
||||
// Scroll and see if more brands load
|
||||
console.log('\n[6] Scrolling to load more brands...');
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
||||
await sleep(1500);
|
||||
|
||||
const brandCount = await page.evaluate(() =>
|
||||
document.querySelectorAll('a[href*="/brand/"]').length
|
||||
);
|
||||
const productCount = await page.evaluate(() =>
|
||||
document.querySelectorAll('a[href*="/product/"]').length
|
||||
);
|
||||
|
||||
console.log(` Scroll ${i + 1}: brand links=${brandCount}, product links=${productCount}`);
|
||||
}
|
||||
|
||||
// Final brand href list
|
||||
const finalBrandHrefs = await page.evaluate(() => {
|
||||
const hrefs: string[] = [];
|
||||
document.querySelectorAll('a[href*="/brand/"]').forEach((a: Element) => {
|
||||
const href = a.getAttribute('href');
|
||||
if (href && !hrefs.includes(href)) hrefs.push(href);
|
||||
});
|
||||
return hrefs;
|
||||
});
|
||||
|
||||
console.log(`\n[7] Final brand href list (${finalBrandHrefs.length} brands):`);
|
||||
finalBrandHrefs.forEach(href => console.log(` ${href}`));
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
221
backend/scripts/test-treez-full-crawl.ts
Normal file
221
backend/scripts/test-treez-full-crawl.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* Full crawl: Visit each brand page and aggregate all products
|
||||
*/
|
||||
|
||||
import puppeteer, { Page } from 'puppeteer';
|
||||
|
||||
const STORE_ID = 'best';
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function bypassAgeGate(page: Page): Promise<void> {
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(2000);
|
||||
}
|
||||
}
|
||||
|
||||
async function scrollToLoadAll(page: Page): Promise<void> {
|
||||
let previousHeight = 0;
|
||||
let sameCount = 0;
|
||||
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const currentHeight = await page.evaluate(() => document.body.scrollHeight);
|
||||
if (currentHeight === previousHeight) {
|
||||
sameCount++;
|
||||
if (sameCount >= 3) break;
|
||||
} else {
|
||||
sameCount = 0;
|
||||
}
|
||||
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
||||
await sleep(1000);
|
||||
previousHeight = currentHeight;
|
||||
}
|
||||
}
|
||||
|
||||
async function extractProducts(page: Page): Promise<{ name: string; brand: string; price: string; href: string }[]> {
|
||||
return page.evaluate(() => {
|
||||
const products: { name: string; brand: string; price: string; href: string }[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
document.querySelectorAll('a[href*="/product/"]').forEach(a => {
|
||||
const href = a.getAttribute('href') || '';
|
||||
const img = a.querySelector('img');
|
||||
const h5 = a.querySelector('h5');
|
||||
const name = img?.getAttribute('alt') || h5?.textContent?.trim() || '';
|
||||
|
||||
if (!name || seen.has(href)) return;
|
||||
seen.add(href);
|
||||
|
||||
// Extract brand from href pattern: /product/{brand}-{product}
|
||||
const brandMatch = href.match(/\/product\/([^\/]+)/);
|
||||
const productSlug = brandMatch ? brandMatch[1] : '';
|
||||
|
||||
const priceEl = a.querySelector('[class*="price"]');
|
||||
const priceMatch = priceEl?.textContent?.match(/\$(\d+(?:\.\d{2})?)/);
|
||||
const price = priceMatch ? priceMatch[1] : '';
|
||||
|
||||
products.push({ name, brand: productSlug.split('-')[0] || '', price, href });
|
||||
});
|
||||
|
||||
return products;
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('='.repeat(60));
|
||||
console.log('Full Treez Crawl - All Brands');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', (req) => {
|
||||
if (['image', 'font', 'media'].includes(req.resourceType())) {
|
||||
req.abort();
|
||||
} else {
|
||||
req.continue();
|
||||
}
|
||||
});
|
||||
|
||||
// Step 1: Go to brands page and extract all brand links
|
||||
const brandsUrl = `https://${STORE_ID}.treez.io/onlinemenu/brands?customerType=ADULT`;
|
||||
console.log(`\n[1] Getting brand list...`);
|
||||
|
||||
await page.goto(brandsUrl, { waitUntil: 'networkidle2', timeout: 60000 });
|
||||
await sleep(3000);
|
||||
await bypassAgeGate(page);
|
||||
await sleep(2000);
|
||||
|
||||
// Get all brand links from the page
|
||||
const brandLinks = await page.evaluate(() => {
|
||||
const links: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
// Get all /brand/ links
|
||||
document.querySelectorAll('a[href*="/brand/"]').forEach(a => {
|
||||
const href = a.getAttribute('href') || '';
|
||||
if (href && !seen.has(href)) {
|
||||
seen.add(href);
|
||||
links.push(href);
|
||||
}
|
||||
});
|
||||
|
||||
return links;
|
||||
});
|
||||
|
||||
console.log(`Found ${brandLinks.length} brand links: ${brandLinks.join(', ')}`);
|
||||
|
||||
// Step 2: Also extract unique brands from product URLs
|
||||
const productBrands = await page.evaluate(() => {
|
||||
const brands = new Set<string>();
|
||||
|
||||
document.querySelectorAll('a[href*="/product/"]').forEach(a => {
|
||||
const href = a.getAttribute('href') || '';
|
||||
// Pattern: /product/{brand}-{product}-...
|
||||
// Extract first part before first hyphen that looks like brand
|
||||
const match = href.match(/\/product\/([a-z0-9]+(?:-[a-z0-9]+)?)-/i);
|
||||
if (match) {
|
||||
brands.add(match[1].toLowerCase());
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(brands);
|
||||
});
|
||||
|
||||
console.log(`Found ${productBrands.length} brands from product URLs`);
|
||||
|
||||
// Step 3: Build full brand URL list
|
||||
const allBrandUrls = new Set<string>();
|
||||
|
||||
// Add direct brand links
|
||||
brandLinks.forEach(link => {
|
||||
if (link.startsWith('/')) {
|
||||
allBrandUrls.add(`https://${STORE_ID}.treez.io${link}`);
|
||||
} else {
|
||||
allBrandUrls.add(link);
|
||||
}
|
||||
});
|
||||
|
||||
// Add brand URLs from product slugs
|
||||
productBrands.forEach(brand => {
|
||||
allBrandUrls.add(`https://${STORE_ID}.treez.io/brand/${encodeURIComponent(brand)}`);
|
||||
});
|
||||
|
||||
console.log(`Total brand URLs to visit: ${allBrandUrls.size}`);
|
||||
|
||||
// Step 4: Visit each brand page and collect products
|
||||
const allProducts = new Map<string, { name: string; brand: string; price: string; href: string }>();
|
||||
let visitedBrands = 0;
|
||||
|
||||
for (const brandUrl of allBrandUrls) {
|
||||
try {
|
||||
const fullUrl = brandUrl.includes('customerType') ? brandUrl : `${brandUrl}?customerType=ADULT`;
|
||||
console.log(`\n[${++visitedBrands}/${allBrandUrls.size}] Visiting: ${fullUrl}`);
|
||||
|
||||
await page.goto(fullUrl, { waitUntil: 'networkidle2', timeout: 30000 });
|
||||
await sleep(1500);
|
||||
|
||||
// Scroll to load all
|
||||
await scrollToLoadAll(page);
|
||||
|
||||
const products = await extractProducts(page);
|
||||
console.log(` Found ${products.length} products`);
|
||||
|
||||
products.forEach(p => {
|
||||
if (!allProducts.has(p.href)) {
|
||||
allProducts.set(p.href, p);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(` Total unique so far: ${allProducts.size}`);
|
||||
|
||||
} catch (error: any) {
|
||||
console.log(` Error: ${error.message.slice(0, 50)}`);
|
||||
}
|
||||
|
||||
// Small delay between requests
|
||||
await sleep(500);
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('SUMMARY');
|
||||
console.log('='.repeat(60));
|
||||
console.log(`Brands visited: ${visitedBrands}`);
|
||||
console.log(`Total unique products: ${allProducts.size}`);
|
||||
|
||||
// Count by brand
|
||||
const brandCounts: Record<string, number> = {};
|
||||
allProducts.forEach(p => {
|
||||
brandCounts[p.brand] = (brandCounts[p.brand] || 0) + 1;
|
||||
});
|
||||
|
||||
console.log('\nProducts by brand:');
|
||||
Object.entries(brandCounts)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 20)
|
||||
.forEach(([brand, count]) => {
|
||||
console.log(` ${brand}: ${count}`);
|
||||
});
|
||||
|
||||
// Sample products
|
||||
console.log('\nSample products:');
|
||||
Array.from(allProducts.values()).slice(0, 10).forEach(p => {
|
||||
console.log(` - ${p.name} | ${p.brand} | $${p.price || 'N/A'}`);
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
57
backend/scripts/test-treez-headless-api.ts
Normal file
57
backend/scripts/test-treez-headless-api.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import axios from 'axios';
|
||||
|
||||
async function main() {
|
||||
const storeId = 'best';
|
||||
const baseUrl = `https://headless.treez.io/v2.0/dispensary/${storeId}`;
|
||||
|
||||
// Try various endpoints
|
||||
const endpoints = [
|
||||
'/ecommerce/discounts?excludeInactive=true&hideUnset=true&includeProdInfo=true',
|
||||
'/ecommerce/products',
|
||||
'/products',
|
||||
'/menu',
|
||||
'/inventory',
|
||||
'/catalog',
|
||||
];
|
||||
|
||||
console.log('Testing Treez Headless API endpoints...\n');
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
const url = baseUrl + endpoint;
|
||||
console.log('GET ' + url);
|
||||
|
||||
try {
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
console.log(' Status: ' + response.status);
|
||||
const data = response.data;
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
console.log(' Array length: ' + data.length);
|
||||
if (data.length > 0) {
|
||||
console.log(' First item keys: ' + Object.keys(data[0]).join(', '));
|
||||
console.log(' Sample: ' + JSON.stringify(data[0]).slice(0, 300));
|
||||
}
|
||||
} else if (typeof data === 'object') {
|
||||
console.log(' Keys: ' + Object.keys(data).join(', '));
|
||||
console.log(' Sample: ' + JSON.stringify(data).slice(0, 500));
|
||||
}
|
||||
console.log('');
|
||||
|
||||
} catch (err: any) {
|
||||
console.log(' Error: ' + (err.response?.status || err.message));
|
||||
if (err.response?.data) {
|
||||
console.log(' Data: ' + JSON.stringify(err.response.data).slice(0, 200));
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
166
backend/scripts/test-treez-inventory.ts
Normal file
166
backend/scripts/test-treez-inventory.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
// Go to a product detail page
|
||||
await page.goto('https://shop.bestdispensary.com/brand/dime', {
|
||||
waitUntil: 'networkidle2',
|
||||
timeout: 60000
|
||||
});
|
||||
await sleep(3000);
|
||||
|
||||
// Bypass age gate
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(2000);
|
||||
}
|
||||
|
||||
// Get first product URL
|
||||
const productUrl = await page.evaluate(() => {
|
||||
const a = document.querySelector('a[href*="/product/"]');
|
||||
return a ? 'https://shop.bestdispensary.com' + a.getAttribute('href') : null;
|
||||
});
|
||||
|
||||
if (!productUrl) {
|
||||
console.log('No product found');
|
||||
await browser.close();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Checking product: ' + productUrl + '\n');
|
||||
await page.goto(productUrl, { waitUntil: 'networkidle2', timeout: 30000 });
|
||||
await sleep(2000);
|
||||
|
||||
// Look for inventory/stock info
|
||||
const inventoryData = await page.evaluate(() => {
|
||||
const data: any = {};
|
||||
|
||||
// Check for stock/inventory elements
|
||||
const stockSelectors = [
|
||||
'[class*="stock"]',
|
||||
'[class*="Stock"]',
|
||||
'[class*="inventory"]',
|
||||
'[class*="Inventory"]',
|
||||
'[class*="quantity"]',
|
||||
'[class*="Quantity"]',
|
||||
'[class*="available"]',
|
||||
'[class*="Available"]',
|
||||
'[class*="in-stock"]',
|
||||
'[class*="out-of-stock"]',
|
||||
'[data-stock]',
|
||||
'[data-quantity]',
|
||||
'[data-inventory]',
|
||||
];
|
||||
|
||||
data.stockElements = [];
|
||||
stockSelectors.forEach(sel => {
|
||||
document.querySelectorAll(sel).forEach(el => {
|
||||
data.stockElements.push({
|
||||
selector: sel,
|
||||
text: el.textContent?.trim().slice(0, 100),
|
||||
dataAttrs: Object.keys((el as HTMLElement).dataset || {}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Check for "Add to cart" button state (disabled = out of stock)
|
||||
const addToCartBtn = document.querySelector('button[class*="add"], button[class*="cart"]');
|
||||
data.addToCartBtn = {
|
||||
found: !!addToCartBtn,
|
||||
disabled: (addToCartBtn as HTMLButtonElement)?.disabled,
|
||||
text: addToCartBtn?.textContent?.trim(),
|
||||
};
|
||||
|
||||
// Check page source for inventory keywords
|
||||
const bodyText = document.body.innerText;
|
||||
data.hasStockText = bodyText.includes('stock') || bodyText.includes('Stock');
|
||||
data.hasInventoryText = bodyText.includes('inventory') || bodyText.includes('Inventory');
|
||||
data.hasQuantityText = bodyText.includes('quantity') || bodyText.includes('Quantity');
|
||||
data.hasAvailableText = bodyText.includes('available') || bodyText.includes('Available');
|
||||
|
||||
// Get all data attributes on the page
|
||||
data.allDataAttrs = [];
|
||||
document.querySelectorAll('[data-product-id], [data-sku], [data-variant]').forEach(el => {
|
||||
const attrs: any = {};
|
||||
Object.entries((el as HTMLElement).dataset).forEach(([k, v]) => {
|
||||
attrs[k] = v;
|
||||
});
|
||||
if (Object.keys(attrs).length > 0) {
|
||||
data.allDataAttrs.push(attrs);
|
||||
}
|
||||
});
|
||||
|
||||
// Check for JSON-LD or schema data
|
||||
const scripts = document.querySelectorAll('script[type="application/ld+json"]');
|
||||
data.jsonLd = [];
|
||||
scripts.forEach(s => {
|
||||
try {
|
||||
const json = JSON.parse(s.textContent || '');
|
||||
data.jsonLd.push(json);
|
||||
} catch {}
|
||||
});
|
||||
|
||||
// Check Next.js data
|
||||
const nextData = document.getElementById('__NEXT_DATA__');
|
||||
if (nextData) {
|
||||
try {
|
||||
const json = JSON.parse(nextData.textContent || '');
|
||||
data.hasNextData = true;
|
||||
data.nextDataKeys = Object.keys(json);
|
||||
// Look for product data in props
|
||||
if (json.props?.pageProps?.product) {
|
||||
data.productFromNext = json.props.pageProps.product;
|
||||
}
|
||||
if (json.props?.pageProps) {
|
||||
data.pagePropsKeys = Object.keys(json.props.pageProps);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
console.log('Inventory Analysis:\n');
|
||||
console.log('Stock elements found: ' + inventoryData.stockElements.length);
|
||||
inventoryData.stockElements.forEach((s: any) => {
|
||||
console.log(' - ' + s.selector + ': "' + s.text + '"');
|
||||
});
|
||||
|
||||
console.log('\nAdd to Cart button: ' + JSON.stringify(inventoryData.addToCartBtn));
|
||||
|
||||
console.log('\nText checks:');
|
||||
console.log(' Has "stock": ' + inventoryData.hasStockText);
|
||||
console.log(' Has "inventory": ' + inventoryData.hasInventoryText);
|
||||
console.log(' Has "quantity": ' + inventoryData.hasQuantityText);
|
||||
console.log(' Has "available": ' + inventoryData.hasAvailableText);
|
||||
|
||||
console.log('\nData attributes: ' + JSON.stringify(inventoryData.allDataAttrs));
|
||||
console.log('\nJSON-LD: ' + JSON.stringify(inventoryData.jsonLd, null, 2));
|
||||
|
||||
if (inventoryData.hasNextData) {
|
||||
console.log('\nNext.js data found!');
|
||||
console.log(' Keys: ' + inventoryData.nextDataKeys);
|
||||
console.log(' Page props keys: ' + inventoryData.pagePropsKeys);
|
||||
if (inventoryData.productFromNext) {
|
||||
console.log('\n Product data from Next.js:');
|
||||
console.log(JSON.stringify(inventoryData.productFromNext, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
211
backend/scripts/test-treez-load-all.ts
Normal file
211
backend/scripts/test-treez-load-all.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* Find and interact with "load more brands" selector
|
||||
*/
|
||||
|
||||
import puppeteer, { Page } from 'puppeteer';
|
||||
|
||||
const STORE_ID = 'best';
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function bypassAgeGate(page: Page): Promise<void> {
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
console.log(' Age gate detected, bypassing...');
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(2000);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('='.repeat(60));
|
||||
console.log('Finding "Load More Brands" control');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
// Don't block stylesheets - might affect layout
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', (req) => {
|
||||
if (['image', 'font', 'media'].includes(req.resourceType())) {
|
||||
req.abort();
|
||||
} else {
|
||||
req.continue();
|
||||
}
|
||||
});
|
||||
|
||||
const url = `https://${STORE_ID}.treez.io/onlinemenu/brands?customerType=ADULT`;
|
||||
console.log(`\nNavigating to ${url}`);
|
||||
|
||||
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
|
||||
await sleep(3000);
|
||||
await bypassAgeGate(page);
|
||||
await sleep(2000);
|
||||
|
||||
// Find all selects and dropdowns
|
||||
console.log('\n[1] Looking for select elements...');
|
||||
|
||||
const selectInfo = await page.evaluate(() => {
|
||||
const results: any[] = [];
|
||||
|
||||
// Native select elements
|
||||
document.querySelectorAll('select').forEach((sel, i) => {
|
||||
const options = Array.from(sel.options).map(o => ({ value: o.value, text: o.text }));
|
||||
results.push({
|
||||
type: 'select',
|
||||
id: sel.id || `select-${i}`,
|
||||
class: sel.className,
|
||||
options: options.slice(0, 10),
|
||||
totalOptions: sel.options.length,
|
||||
});
|
||||
});
|
||||
|
||||
return results;
|
||||
});
|
||||
|
||||
console.log('Native selects found:', JSON.stringify(selectInfo, null, 2));
|
||||
|
||||
// Look for custom dropdown buttons
|
||||
console.log('\n[2] Looking for dropdown/button elements...');
|
||||
|
||||
const dropdownInfo = await page.evaluate(() => {
|
||||
const results: any[] = [];
|
||||
|
||||
// Look for common dropdown patterns
|
||||
const selectors = [
|
||||
'[class*="dropdown"]',
|
||||
'[class*="Dropdown"]',
|
||||
'[class*="select"]',
|
||||
'[class*="Select"]',
|
||||
'[class*="picker"]',
|
||||
'[class*="Picker"]',
|
||||
'[role="listbox"]',
|
||||
'[role="combobox"]',
|
||||
'button[aria-haspopup]',
|
||||
'[class*="brand"] button',
|
||||
'[class*="Brand"] button',
|
||||
'[class*="filter"]',
|
||||
'[class*="Filter"]',
|
||||
];
|
||||
|
||||
selectors.forEach(sel => {
|
||||
document.querySelectorAll(sel).forEach((el, i) => {
|
||||
const text = el.textContent?.trim().slice(0, 100) || '';
|
||||
const className = el.className?.toString?.().slice(0, 100) || '';
|
||||
if (text.toLowerCase().includes('brand') || text.toLowerCase().includes('more') || text.toLowerCase().includes('all')) {
|
||||
results.push({
|
||||
selector: sel,
|
||||
tag: el.tagName,
|
||||
class: className,
|
||||
text: text.slice(0, 50),
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return results;
|
||||
});
|
||||
|
||||
console.log('Dropdown-like elements:', JSON.stringify(dropdownInfo.slice(0, 10), null, 2));
|
||||
|
||||
// Look for any element containing "brand" text
|
||||
console.log('\n[3] Looking for elements with "brand" or "more" text...');
|
||||
|
||||
const brandTextElements = await page.evaluate(() => {
|
||||
const results: any[] = [];
|
||||
const textContent = ['brand', 'more', 'load', 'view all', 'show all'];
|
||||
|
||||
document.querySelectorAll('button, a, [role="button"], select, [class*="select"]').forEach(el => {
|
||||
const text = el.textContent?.toLowerCase() || '';
|
||||
if (textContent.some(t => text.includes(t))) {
|
||||
results.push({
|
||||
tag: el.tagName,
|
||||
class: el.className?.toString?.().slice(0, 80) || '',
|
||||
text: el.textContent?.trim().slice(0, 100) || '',
|
||||
href: el.getAttribute('href') || '',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
});
|
||||
|
||||
console.log('Elements with brand/more text:', JSON.stringify(brandTextElements.slice(0, 15), null, 2));
|
||||
|
||||
// Count current brand sections
|
||||
console.log('\n[4] Counting brand sections...');
|
||||
|
||||
const brandSections = await page.evaluate(() => {
|
||||
// Look for brand section headers or containers
|
||||
const sections: { title: string; productCount: number }[] = [];
|
||||
|
||||
document.querySelectorAll('[class*="products_product__section"]').forEach(section => {
|
||||
const header = section.querySelector('h2, h3, [class*="heading"]');
|
||||
const title = header?.textContent?.trim() || 'Unknown';
|
||||
const products = section.querySelectorAll('a[class*="product_product__"]');
|
||||
sections.push({ title, productCount: products.length });
|
||||
});
|
||||
|
||||
return sections;
|
||||
});
|
||||
|
||||
console.log(`Found ${brandSections.length} brand sections:`);
|
||||
brandSections.slice(0, 20).forEach(s => console.log(` - ${s.title}: ${s.productCount} products`));
|
||||
|
||||
// Take a screenshot
|
||||
await page.screenshot({ path: '/tmp/treez-brands-full.png', fullPage: true });
|
||||
console.log('\n[5] Full page screenshot saved to /tmp/treez-brands-full.png');
|
||||
|
||||
// Try scrolling to bottom to trigger any lazy loading
|
||||
console.log('\n[6] Scrolling to load more content...');
|
||||
|
||||
let previousHeight = 0;
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
||||
await sleep(1500);
|
||||
|
||||
const currentHeight = await page.evaluate(() => document.body.scrollHeight);
|
||||
const sectionCount = await page.evaluate(() =>
|
||||
document.querySelectorAll('[class*="products_product__section"]').length
|
||||
);
|
||||
|
||||
console.log(` Scroll ${i + 1}: height=${currentHeight}, sections=${sectionCount}`);
|
||||
|
||||
if (currentHeight === previousHeight) {
|
||||
console.log(' No new content, stopping');
|
||||
break;
|
||||
}
|
||||
previousHeight = currentHeight;
|
||||
}
|
||||
|
||||
// Final count
|
||||
const finalSections = await page.evaluate(() => {
|
||||
const sections: { title: string; productCount: number }[] = [];
|
||||
document.querySelectorAll('[class*="products_product__section"]').forEach(section => {
|
||||
const header = section.querySelector('h2, h3, [class*="heading"]');
|
||||
const title = header?.textContent?.trim() || 'Unknown';
|
||||
const products = section.querySelectorAll('a[class*="product_product__"]');
|
||||
sections.push({ title, productCount: products.length });
|
||||
});
|
||||
return sections;
|
||||
});
|
||||
|
||||
console.log(`\n[7] After scrolling: ${finalSections.length} brand sections`);
|
||||
finalSections.forEach(s => console.log(` - ${s.title}: ${s.productCount} products`));
|
||||
|
||||
const totalProducts = finalSections.reduce((sum, s) => sum + s.productCount, 0);
|
||||
console.log(`\nTotal products across all sections: ${totalProducts}`);
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
104
backend/scripts/test-treez-network.ts
Normal file
104
backend/scripts/test-treez-network.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
// Capture all network requests
|
||||
const requests: any[] = [];
|
||||
const responses: any[] = [];
|
||||
|
||||
page.on('request', (req) => {
|
||||
const url = req.url();
|
||||
if (url.includes('api') || url.includes('graphql') ||
|
||||
url.includes('product') || url.includes('menu') ||
|
||||
url.includes('treez') || url.includes('inventory')) {
|
||||
requests.push({
|
||||
url: url.slice(0, 150),
|
||||
method: req.method(),
|
||||
headers: req.headers(),
|
||||
postData: req.postData()?.slice(0, 500),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
page.on('response', async (res) => {
|
||||
const url = res.url();
|
||||
if (url.includes('api') || url.includes('graphql') ||
|
||||
url.includes('product') || url.includes('menu') ||
|
||||
url.includes('inventory')) {
|
||||
try {
|
||||
const contentType = res.headers()['content-type'] || '';
|
||||
if (contentType.includes('json')) {
|
||||
const body = await res.text();
|
||||
responses.push({
|
||||
url: url.slice(0, 150),
|
||||
status: res.status(),
|
||||
bodyPreview: body.slice(0, 1000),
|
||||
});
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Loading page and capturing network requests...\n');
|
||||
|
||||
await page.goto('https://shop.bestdispensary.com/brands', {
|
||||
waitUntil: 'networkidle2',
|
||||
timeout: 60000
|
||||
});
|
||||
await sleep(3000);
|
||||
|
||||
// Bypass age gate
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(2000);
|
||||
}
|
||||
|
||||
// Click load more to trigger more API calls
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const btn = await page.$('button.collection__load-more');
|
||||
if (btn) {
|
||||
await btn.click();
|
||||
await sleep(2000);
|
||||
}
|
||||
}
|
||||
|
||||
// Also visit a product page
|
||||
console.log('\nVisiting a product page...\n');
|
||||
await page.goto('https://shop.bestdispensary.com/product/dime-sour-grapes-2g-disposable-cartridge-2-grams', {
|
||||
waitUntil: 'networkidle2',
|
||||
timeout: 30000
|
||||
});
|
||||
await sleep(2000);
|
||||
|
||||
console.log('=== API REQUESTS FOUND ===\n');
|
||||
requests.forEach((r, i) => {
|
||||
console.log((i+1) + '. ' + r.method + ' ' + r.url);
|
||||
if (r.postData) {
|
||||
console.log(' POST data: ' + r.postData);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\n=== JSON RESPONSES ===\n');
|
||||
responses.forEach((r, i) => {
|
||||
console.log((i+1) + '. ' + r.url);
|
||||
console.log(' Status: ' + r.status);
|
||||
console.log(' Body: ' + r.bodyPreview.slice(0, 300) + '...\n');
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
110
backend/scripts/test-treez-page-fetch.ts
Normal file
110
backend/scripts/test-treez-page-fetch.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
import fs from 'fs';
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
console.log('Loading page first to establish session...\n');
|
||||
|
||||
await page.goto('https://shop.bestdispensary.com/shop', {
|
||||
waitUntil: 'networkidle2',
|
||||
timeout: 60000
|
||||
});
|
||||
await sleep(3000);
|
||||
|
||||
// Bypass age gate
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
console.log('Bypassing age gate...');
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(3000);
|
||||
}
|
||||
|
||||
// Wait for page to fully load
|
||||
await sleep(2000);
|
||||
|
||||
console.log('\nMaking fetch request from page context...\n');
|
||||
|
||||
// Try to make the ES request from within page context
|
||||
const result = await page.evaluate(async () => {
|
||||
const url = 'https://search-kyrok9udlk.gapcommerceapi.com/product/search';
|
||||
const apiKey = 'V3jHL9dFzi3Gj4UISM4lr38Nm0GSxcps5OBz1PbS';
|
||||
|
||||
const query = {
|
||||
from: 0,
|
||||
size: 1000,
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{ bool: { filter: { range: { customMinPrice: { gte: 0.01, lte: 500000 }}}}},
|
||||
{ bool: { should: [{ match: { isAboveThreshold: true }}]}},
|
||||
{ bool: { should: [{ match: { isHideFromMenu: false }}]}}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': apiKey,
|
||||
},
|
||||
body: JSON.stringify(query),
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return { error: 'HTTP ' + response.status, statusText: response.statusText };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
total: data.hits?.total?.value,
|
||||
count: data.hits?.hits?.length,
|
||||
firstProduct: data.hits?.hits?.[0]?._source,
|
||||
products: data.hits?.hits?.map((h: any) => h._source),
|
||||
};
|
||||
} catch (err: any) {
|
||||
return { error: err.message };
|
||||
}
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
console.log('Error: ' + result.error);
|
||||
if (result.statusText) console.log('Status: ' + result.statusText);
|
||||
} else {
|
||||
console.log('Total products in ES: ' + result.total);
|
||||
console.log('Products returned: ' + result.count);
|
||||
|
||||
if (result.firstProduct) {
|
||||
console.log('\n=== PRODUCT FIELDS ===\n');
|
||||
console.log(Object.keys(result.firstProduct).sort().join('\n'));
|
||||
|
||||
console.log('\n=== SAMPLE PRODUCT ===\n');
|
||||
console.log(JSON.stringify(result.firstProduct, null, 2));
|
||||
|
||||
// Save all products
|
||||
if (result.products) {
|
||||
fs.writeFileSync('/tmp/treez-all-products.json', JSON.stringify(result.products, null, 2));
|
||||
console.log('\nSaved ' + result.products.length + ' products to /tmp/treez-all-products.json');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main();
|
||||
171
backend/scripts/test-treez-page-state.ts
Normal file
171
backend/scripts/test-treez-page-state.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
import fs from 'fs';
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
console.log('Loading page...\n');
|
||||
|
||||
await page.goto('https://shop.bestdispensary.com/shop', {
|
||||
waitUntil: 'networkidle2',
|
||||
timeout: 60000
|
||||
});
|
||||
await sleep(3000);
|
||||
|
||||
// Bypass age gate
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
console.log('Bypassing age gate...');
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(3000);
|
||||
}
|
||||
|
||||
// Extract __NEXT_DATA__
|
||||
console.log('\n=== NEXT.JS DATA ===\n');
|
||||
|
||||
const nextData = await page.evaluate(() => {
|
||||
const script = document.getElementById('__NEXT_DATA__');
|
||||
if (script) {
|
||||
try {
|
||||
return JSON.parse(script.textContent || '');
|
||||
} catch { return null; }
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (nextData) {
|
||||
console.log('Top keys: ' + Object.keys(nextData).join(', '));
|
||||
if (nextData.props?.pageProps) {
|
||||
console.log('pageProps keys: ' + Object.keys(nextData.props.pageProps).join(', '));
|
||||
|
||||
// Look for products
|
||||
const pp = nextData.props.pageProps;
|
||||
if (pp.products) {
|
||||
console.log('\nFound products: ' + pp.products.length);
|
||||
if (pp.products[0]) {
|
||||
console.log('Product fields: ' + Object.keys(pp.products[0]).join(', '));
|
||||
console.log('\nSample:\n' + JSON.stringify(pp.products[0], null, 2));
|
||||
}
|
||||
}
|
||||
if (pp.initialProducts) {
|
||||
console.log('\nFound initialProducts: ' + pp.initialProducts.length);
|
||||
}
|
||||
if (pp.data) {
|
||||
console.log('\nFound data: ' + (Array.isArray(pp.data) ? pp.data.length + ' items' : typeof pp.data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check window object
|
||||
console.log('\n=== WINDOW GLOBALS ===\n');
|
||||
|
||||
const windowData = await page.evaluate(() => {
|
||||
const win = window as any;
|
||||
const result: any = {};
|
||||
|
||||
// Common patterns for storing product data
|
||||
const patterns = ['products', 'items', 'data', 'state', 'store', 'redux', 'apollo'];
|
||||
|
||||
Object.keys(win).forEach(key => {
|
||||
const lowerKey = key.toLowerCase();
|
||||
if (patterns.some(p => lowerKey.includes(p))) {
|
||||
try {
|
||||
const val = win[key];
|
||||
if (typeof val === 'object' && val !== null) {
|
||||
result[key] = {
|
||||
type: Array.isArray(val) ? 'array' : 'object',
|
||||
keys: Object.keys(val).slice(0, 10),
|
||||
length: Array.isArray(val) ? val.length : undefined,
|
||||
};
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
console.log('Window globals with data-like names:');
|
||||
Object.entries(windowData).forEach(([k, v]: [string, any]) => {
|
||||
console.log(' ' + k + ': ' + v.type + (v.length ? ' (' + v.length + ')' : '') + ' - keys: ' + v.keys?.join(', '));
|
||||
});
|
||||
|
||||
// Try to find React state
|
||||
console.log('\n=== EXTRACTING FROM DOM ===\n');
|
||||
|
||||
const domProducts = await page.evaluate(() => {
|
||||
const products: any[] = [];
|
||||
|
||||
document.querySelectorAll('a[href*="/product/"]').forEach((card: Element) => {
|
||||
const product: any = {};
|
||||
|
||||
product.href = card.getAttribute('href');
|
||||
product.name = card.querySelector('h3, h4, h5')?.textContent?.trim();
|
||||
|
||||
// Get all text
|
||||
const allText = card.textContent || '';
|
||||
|
||||
// Extract THC %
|
||||
const thcMatch = allText.match(/(\d+(?:\.\d+)?)\s*%/);
|
||||
if (thcMatch) product.thc = thcMatch[1];
|
||||
|
||||
// Extract price
|
||||
const priceMatch = allText.match(/\$(\d+(?:\.\d+)?)/);
|
||||
if (priceMatch) product.price = priceMatch[1];
|
||||
|
||||
// Extract weight
|
||||
const weightMatch = allText.match(/(\d+(?:\.\d+)?)\s*[gG]/);
|
||||
if (weightMatch) product.weight = weightMatch[1] + 'g';
|
||||
|
||||
// Get brand from card
|
||||
const brandEl = card.querySelector('[class*="brand"]');
|
||||
product.brand = brandEl?.textContent?.trim();
|
||||
|
||||
// Get strain type
|
||||
const strainTypes = ['Indica', 'Sativa', 'Hybrid', 'I/S', 'S/I', 'CBD'];
|
||||
strainTypes.forEach(st => {
|
||||
if (allText.includes(st)) product.strainType = st;
|
||||
});
|
||||
|
||||
// Get image
|
||||
const img = card.querySelector('img');
|
||||
product.image = img?.getAttribute('src');
|
||||
|
||||
products.push(product);
|
||||
});
|
||||
|
||||
return products;
|
||||
});
|
||||
|
||||
console.log('Products from DOM: ' + domProducts.length);
|
||||
if (domProducts.length > 0) {
|
||||
console.log('\nSample:\n' + JSON.stringify(domProducts[0], null, 2));
|
||||
|
||||
// Show variety
|
||||
console.log('\n=== DATA QUALITY ===');
|
||||
const withThc = domProducts.filter(p => p.thc).length;
|
||||
const withPrice = domProducts.filter(p => p.price).length;
|
||||
const withBrand = domProducts.filter(p => p.brand).length;
|
||||
const withStrain = domProducts.filter(p => p.strainType).length;
|
||||
|
||||
console.log('With THC%: ' + withThc + '/' + domProducts.length);
|
||||
console.log('With Price: ' + withPrice + '/' + domProducts.length);
|
||||
console.log('With Brand: ' + withBrand + '/' + domProducts.length);
|
||||
console.log('With Strain: ' + withStrain + '/' + domProducts.length);
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main();
|
||||
89
backend/scripts/test-treez-product-api.ts
Normal file
89
backend/scripts/test-treez-product-api.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import axios from 'axios';
|
||||
|
||||
async function main() {
|
||||
const clientId = '29dce682258145c6b1cf71027282d083';
|
||||
const clientSecret = 'A57bB49AfD7F4233B1750a0B501B4E16';
|
||||
const storeId = 'best';
|
||||
|
||||
// Try various Treez API endpoints for products
|
||||
const endpoints = [
|
||||
'https://headless.treez.io/v2.0/dispensary/' + storeId + '/ecommerce/products',
|
||||
'https://headless.treez.io/v2.0/dispensary/' + storeId + '/ecommerce/menu',
|
||||
'https://headless.treez.io/v2.0/dispensary/' + storeId + '/ecommerce/inventory',
|
||||
'https://headless.treez.io/v2.0/dispensary/' + storeId + '/ecommerce/catalog',
|
||||
'https://headless.treez.io/v2.0/dispensary/' + storeId + '/menu',
|
||||
'https://headless.treez.io/v2.0/dispensary/' + storeId + '/products',
|
||||
'https://api.treez.io/v2.0/dispensary/' + storeId + '/ecommerce/products',
|
||||
'https://api.treez.io/v2.0/dispensary/' + storeId + '/products',
|
||||
'https://selltreez.com/api/dispensary/' + storeId + '/products',
|
||||
];
|
||||
|
||||
console.log('Testing Treez product endpoints...\n');
|
||||
|
||||
for (const url of endpoints) {
|
||||
console.log('GET ' + url);
|
||||
try {
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
'client_id': clientId,
|
||||
'client_secret': clientSecret,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
console.log(' Status: ' + response.status);
|
||||
const data = response.data;
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
console.log(' Array: ' + data.length + ' items');
|
||||
if (data[0]) console.log(' Keys: ' + Object.keys(data[0]).slice(0, 10).join(', '));
|
||||
} else if (data?.data && Array.isArray(data.data)) {
|
||||
console.log(' data[]: ' + data.data.length + ' items');
|
||||
if (data.data[0]) console.log(' Keys: ' + Object.keys(data.data[0]).slice(0, 10).join(', '));
|
||||
} else {
|
||||
console.log(' Type: ' + typeof data);
|
||||
console.log(' Keys: ' + (typeof data === 'object' ? Object.keys(data).join(', ') : 'N/A'));
|
||||
}
|
||||
console.log('');
|
||||
|
||||
} catch (err: any) {
|
||||
const status = err.response?.status || 'network error';
|
||||
console.log(' Error: ' + status + '\n');
|
||||
}
|
||||
}
|
||||
|
||||
// Also check the working discounts endpoint for clues
|
||||
console.log('\n=== CHECKING DISCOUNTS FOR PRODUCT REFERENCES ===\n');
|
||||
|
||||
try {
|
||||
const response = await axios.get(
|
||||
'https://headless.treez.io/v2.0/dispensary/' + storeId + '/ecommerce/discounts?excludeInactive=true&hideUnset=true&includeProdInfo=true',
|
||||
{
|
||||
headers: {
|
||||
'client_id': clientId,
|
||||
'client_secret': clientSecret,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const data = response.data?.data || response.data;
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
console.log('Discounts: ' + data.length);
|
||||
console.log('First discount keys: ' + Object.keys(data[0]).join(', '));
|
||||
|
||||
// Check if it has product info
|
||||
if (data[0].products) {
|
||||
console.log('\nHas products array: ' + data[0].products.length);
|
||||
if (data[0].products[0]) {
|
||||
console.log('Product keys: ' + Object.keys(data[0].products[0]).join(', '));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.log('Error: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
174
backend/scripts/test-treez-product-data.ts
Normal file
174
backend/scripts/test-treez-product-data.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
// Go to a brand page with products
|
||||
await page.goto('https://shop.bestdispensary.com/brand/best', {
|
||||
waitUntil: 'networkidle2',
|
||||
timeout: 60000
|
||||
});
|
||||
await sleep(3000);
|
||||
|
||||
// Bypass age gate
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(2000);
|
||||
}
|
||||
|
||||
// Get detailed product card structure
|
||||
console.log('Analyzing product card structure...\n');
|
||||
|
||||
const productData = await page.evaluate(() => {
|
||||
const products: any[] = [];
|
||||
|
||||
document.querySelectorAll('a[href*="/product/"]').forEach((card: Element) => {
|
||||
const product: any = {};
|
||||
|
||||
// URL/slug
|
||||
product.href = card.getAttribute('href');
|
||||
product.slug = product.href?.split('/product/')[1];
|
||||
|
||||
// Image
|
||||
const img = card.querySelector('img');
|
||||
product.imageUrl = img?.getAttribute('src');
|
||||
product.imageAlt = img?.getAttribute('alt');
|
||||
|
||||
// Name (usually in h3 or similar)
|
||||
const nameEl = card.querySelector('h3, h4, h5, [class*="name"], [class*="title"]');
|
||||
product.name = nameEl?.textContent?.trim();
|
||||
|
||||
// Brand
|
||||
const brandEl = card.querySelector('[class*="brand"], [class*="Brand"]');
|
||||
product.brand = brandEl?.textContent?.trim();
|
||||
|
||||
// Price
|
||||
const priceEl = card.querySelector('[class*="price"], [class*="Price"]');
|
||||
product.priceText = priceEl?.textContent?.trim();
|
||||
|
||||
// Category/Type badges
|
||||
const badges: string[] = [];
|
||||
card.querySelectorAll('[class*="badge"], [class*="tag"], [class*="label"]').forEach((b: Element) => {
|
||||
const text = b.textContent?.trim();
|
||||
if (text) badges.push(text);
|
||||
});
|
||||
product.badges = badges;
|
||||
|
||||
// THC/CBD info
|
||||
const thcEl = card.querySelector('[class*="thc"], [class*="THC"]');
|
||||
const cbdEl = card.querySelector('[class*="cbd"], [class*="CBD"]');
|
||||
product.thc = thcEl?.textContent?.trim();
|
||||
product.cbd = cbdEl?.textContent?.trim();
|
||||
|
||||
// Weight/size
|
||||
const weightEl = card.querySelector('[class*="weight"], [class*="size"], [class*="gram"]');
|
||||
product.weight = weightEl?.textContent?.trim();
|
||||
|
||||
// Get all text content for analysis
|
||||
product.allText = card.textContent?.replace(/\s+/g, ' ').trim().slice(0, 200);
|
||||
|
||||
// Get all classes on the card
|
||||
product.cardClasses = card.className;
|
||||
|
||||
products.push(product);
|
||||
});
|
||||
|
||||
return products;
|
||||
});
|
||||
|
||||
console.log('Found ' + productData.length + ' products\n');
|
||||
console.log('Sample product data:\n');
|
||||
|
||||
// Show first 3 products in detail
|
||||
productData.slice(0, 3).forEach((p: any, i: number) => {
|
||||
console.log('Product ' + (i+1) + ':');
|
||||
console.log(' Name: ' + p.name);
|
||||
console.log(' Brand: ' + p.brand);
|
||||
console.log(' Slug: ' + p.slug);
|
||||
console.log(' Price: ' + p.priceText);
|
||||
console.log(' THC: ' + p.thc);
|
||||
console.log(' CBD: ' + p.cbd);
|
||||
console.log(' Weight: ' + p.weight);
|
||||
console.log(' Badges: ' + JSON.stringify(p.badges));
|
||||
console.log(' Image: ' + (p.imageUrl ? p.imageUrl.slice(0, 60) + '...' : 'none'));
|
||||
console.log(' All Text: ' + p.allText);
|
||||
console.log('');
|
||||
});
|
||||
|
||||
// Now visit a product detail page
|
||||
if (productData.length > 0) {
|
||||
const productUrl = 'https://shop.bestdispensary.com' + productData[0].href;
|
||||
console.log('\n=== PRODUCT DETAIL PAGE ===');
|
||||
console.log('Visiting: ' + productUrl + '\n');
|
||||
|
||||
await page.goto(productUrl, { waitUntil: 'networkidle2', timeout: 30000 });
|
||||
await sleep(2000);
|
||||
|
||||
const detailData = await page.evaluate(() => {
|
||||
const data: any = {};
|
||||
|
||||
// Get all text elements
|
||||
data.h1 = document.querySelector('h1')?.textContent?.trim();
|
||||
data.h2s = Array.from(document.querySelectorAll('h2')).map(h => h.textContent?.trim());
|
||||
|
||||
// Price
|
||||
const priceEls = document.querySelectorAll('[class*="price"], [class*="Price"]');
|
||||
data.prices = Array.from(priceEls).map(p => p.textContent?.trim());
|
||||
|
||||
// Description
|
||||
const descEl = document.querySelector('[class*="description"], [class*="Description"], p');
|
||||
data.description = descEl?.textContent?.trim().slice(0, 300);
|
||||
|
||||
// THC/CBD
|
||||
data.cannabinoids = [];
|
||||
document.querySelectorAll('[class*="thc"], [class*="THC"], [class*="cbd"], [class*="CBD"], [class*="cannabinoid"]').forEach(el => {
|
||||
data.cannabinoids.push(el.textContent?.trim());
|
||||
});
|
||||
|
||||
// Category/strain type
|
||||
const typeEls = document.querySelectorAll('[class*="strain"], [class*="type"], [class*="category"]');
|
||||
data.types = Array.from(typeEls).map(t => t.textContent?.trim());
|
||||
|
||||
// Weight options
|
||||
const weightEls = document.querySelectorAll('[class*="weight"], [class*="size"], [class*="option"]');
|
||||
data.weights = Array.from(weightEls).map(w => w.textContent?.trim()).filter(w => w && w.length < 30);
|
||||
|
||||
// Images
|
||||
const imgs = document.querySelectorAll('img[src*="product"], img[src*="menu"]');
|
||||
data.images = Array.from(imgs).map(img => img.getAttribute('src')).slice(0, 3);
|
||||
|
||||
// Get body text for analysis
|
||||
const main = document.querySelector('main');
|
||||
data.mainText = main?.textContent?.replace(/\s+/g, ' ').trim().slice(0, 500);
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
console.log('Product Detail:');
|
||||
console.log(' H1: ' + detailData.h1);
|
||||
console.log(' H2s: ' + JSON.stringify(detailData.h2s));
|
||||
console.log(' Prices: ' + JSON.stringify(detailData.prices));
|
||||
console.log(' Description: ' + (detailData.description || 'none'));
|
||||
console.log(' Cannabinoids: ' + JSON.stringify(detailData.cannabinoids));
|
||||
console.log(' Types: ' + JSON.stringify(detailData.types));
|
||||
console.log(' Weights: ' + JSON.stringify(detailData.weights));
|
||||
console.log(' Images: ' + JSON.stringify(detailData.images));
|
||||
console.log('\n Main text sample: ' + detailData.mainText);
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
143
backend/scripts/test-treez-scroll.ts
Normal file
143
backend/scripts/test-treez-scroll.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Test aggressive scrolling to load all products
|
||||
*/
|
||||
|
||||
import puppeteer from 'puppeteer-extra';
|
||||
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
|
||||
|
||||
puppeteer.use(StealthPlugin());
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise(r => setTimeout(r, ms));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
const capturedProducts: any[] = [];
|
||||
|
||||
// CDP interception
|
||||
const client = await page.target().createCDPSession();
|
||||
await client.send('Network.enable');
|
||||
|
||||
client.on('Network.responseReceived', async (event: any) => {
|
||||
if (event.response.url.includes('gapcommerceapi.com/product/search') && event.response.status === 200) {
|
||||
try {
|
||||
const response = await client.send('Network.getResponseBody', { requestId: event.requestId });
|
||||
const body = response.base64Encoded ? Buffer.from(response.body, 'base64').toString('utf8') : response.body;
|
||||
const json = JSON.parse(body);
|
||||
const products = json.hits?.hits?.map((h: any) => h._source) || [];
|
||||
capturedProducts.push(...products);
|
||||
console.log('Captured ' + products.length + ' (total: ' + capturedProducts.length + ')');
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
|
||||
// Try direct treez.io URL - may have more products
|
||||
const url = process.argv[2] || 'https://best.treez.io/onlinemenu/';
|
||||
console.log('Loading ' + url);
|
||||
|
||||
try {
|
||||
await page.goto(url, { waitUntil: 'networkidle0', timeout: 60000 });
|
||||
} catch (e: any) {
|
||||
console.log('Navigation warning: ' + e.message);
|
||||
}
|
||||
|
||||
await sleep(5000);
|
||||
console.log('Current URL: ' + page.url());
|
||||
|
||||
// Age gate
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
console.log('Bypassing age gate...');
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(3000);
|
||||
}
|
||||
|
||||
console.log('After initial load: ' + capturedProducts.length + ' products');
|
||||
|
||||
// Aggressive scrolling and clicking
|
||||
let lastCount = 0;
|
||||
let staleCount = 0;
|
||||
|
||||
for (let i = 0; i < 60; i++) {
|
||||
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
||||
await sleep(800);
|
||||
|
||||
try {
|
||||
const btn = await page.$('button.collection__load-more');
|
||||
if (btn) {
|
||||
const visible = await page.evaluate((b: Element) => {
|
||||
const rect = b.getBoundingClientRect();
|
||||
return rect.width > 0 && rect.height > 0;
|
||||
}, btn);
|
||||
|
||||
if (visible) {
|
||||
await page.evaluate((b: Element) => b.scrollIntoView({ block: 'center' }), btn);
|
||||
await sleep(300);
|
||||
await btn.click();
|
||||
await sleep(2000);
|
||||
console.log('Clicked Load More at scroll ' + (i+1) + ' - ' + capturedProducts.length + ' products');
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Check for stale data
|
||||
if (capturedProducts.length === lastCount) {
|
||||
staleCount++;
|
||||
if (staleCount >= 5) {
|
||||
console.log('No new products for 5 iterations, stopping');
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
staleCount = 0;
|
||||
}
|
||||
lastCount = capturedProducts.length;
|
||||
}
|
||||
|
||||
console.log('\nFinal count: ' + capturedProducts.length + ' products');
|
||||
|
||||
// Dedupe
|
||||
const seen = new Set<string>();
|
||||
const unique = capturedProducts.filter(p => {
|
||||
if (!p.id || seen.has(p.id)) return false;
|
||||
seen.add(p.id);
|
||||
return true;
|
||||
});
|
||||
|
||||
console.log('Unique: ' + unique.length);
|
||||
|
||||
// Categories
|
||||
const cats: Record<string, number> = {};
|
||||
unique.forEach(p => {
|
||||
cats[p.category] = (cats[p.category] || 0) + 1;
|
||||
});
|
||||
console.log('\nCategories:');
|
||||
Object.entries(cats).sort((a, b) => b[1] - a[1]).forEach(([c, n]) => console.log(' ' + c + ': ' + n));
|
||||
|
||||
// Sample cannabis product
|
||||
const cannabis = unique.find(p => p.category === 'FLOWER' || p.category === 'VAPE');
|
||||
if (cannabis) {
|
||||
console.log('\nSample cannabis product:');
|
||||
console.log(JSON.stringify({
|
||||
id: cannabis.id,
|
||||
name: cannabis.name,
|
||||
brand: cannabis.brand,
|
||||
category: cannabis.category,
|
||||
subtype: cannabis.subtype,
|
||||
availableUnits: cannabis.availableUnits,
|
||||
customMinPrice: cannabis.customMinPrice,
|
||||
}, null, 2));
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main();
|
||||
178
backend/scripts/test-treez-selectors.ts
Normal file
178
backend/scripts/test-treez-selectors.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Analyze all product element structures to find all selector patterns
|
||||
*/
|
||||
|
||||
import puppeteer, { Page } from 'puppeteer';
|
||||
|
||||
const STORE_ID = 'best';
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function bypassAgeGate(page: Page): Promise<void> {
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(2000);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Analyzing Treez product selectors...\n');
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', (req) => {
|
||||
if (['image', 'font', 'media'].includes(req.resourceType())) {
|
||||
req.abort();
|
||||
} else {
|
||||
req.continue();
|
||||
}
|
||||
});
|
||||
|
||||
const url = `https://${STORE_ID}.treez.io/onlinemenu/brands?customerType=ADULT`;
|
||||
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
|
||||
await sleep(3000);
|
||||
await bypassAgeGate(page);
|
||||
await sleep(2000);
|
||||
|
||||
// Analyze product elements
|
||||
const analysis = await page.evaluate(() => {
|
||||
const products = document.querySelectorAll('[class*="product_product__"]');
|
||||
const results: {
|
||||
hasName: number;
|
||||
hasPrice: number;
|
||||
noName: number;
|
||||
noPrice: number;
|
||||
nameClasses: Record<string, number>;
|
||||
priceClasses: Record<string, number>;
|
||||
sampleNoNameHTML: string[];
|
||||
sampleWithNameHTML: string[];
|
||||
h5Count: number;
|
||||
h4Count: number;
|
||||
h3Count: number;
|
||||
allHeadingsWithName: number;
|
||||
} = {
|
||||
hasName: 0,
|
||||
hasPrice: 0,
|
||||
noName: 0,
|
||||
noPrice: 0,
|
||||
nameClasses: {},
|
||||
priceClasses: {},
|
||||
sampleNoNameHTML: [],
|
||||
sampleWithNameHTML: [],
|
||||
h5Count: 0,
|
||||
h4Count: 0,
|
||||
h3Count: 0,
|
||||
allHeadingsWithName: 0,
|
||||
};
|
||||
|
||||
products.forEach((el, i) => {
|
||||
// Check current selectors
|
||||
const nameEl = el.querySelector('[class*="product__name"], [class*="name__"]');
|
||||
const priceEl = el.querySelector('[class*="price"]');
|
||||
|
||||
if (nameEl) {
|
||||
results.hasName++;
|
||||
const cls = nameEl.className?.toString?.() || '';
|
||||
results.nameClasses[cls] = (results.nameClasses[cls] || 0) + 1;
|
||||
} else {
|
||||
results.noName++;
|
||||
if (results.sampleNoNameHTML.length < 3) {
|
||||
results.sampleNoNameHTML.push(el.innerHTML.slice(0, 500));
|
||||
}
|
||||
}
|
||||
|
||||
if (priceEl) {
|
||||
results.hasPrice++;
|
||||
} else {
|
||||
results.noPrice++;
|
||||
}
|
||||
|
||||
// Check for headings that might contain names
|
||||
const h5 = el.querySelector('h5');
|
||||
const h4 = el.querySelector('h4');
|
||||
const h3 = el.querySelector('h3');
|
||||
if (h5) results.h5Count++;
|
||||
if (h4) results.h4Count++;
|
||||
if (h3) results.h3Count++;
|
||||
|
||||
// Any heading with text
|
||||
const anyHeading = el.querySelector('h1, h2, h3, h4, h5, h6');
|
||||
if (anyHeading?.textContent?.trim()) {
|
||||
results.allHeadingsWithName++;
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
});
|
||||
|
||||
console.log('Product Analysis:');
|
||||
console.log(`Total products: ${analysis.hasName + analysis.noName}`);
|
||||
console.log(`With name (current selector): ${analysis.hasName}`);
|
||||
console.log(`Without name (current selector): ${analysis.noName}`);
|
||||
console.log(`With price: ${analysis.hasPrice}`);
|
||||
console.log(`\nHeading counts:`);
|
||||
console.log(` H5: ${analysis.h5Count}`);
|
||||
console.log(` H4: ${analysis.h4Count}`);
|
||||
console.log(` H3: ${analysis.h3Count}`);
|
||||
console.log(` Any heading with text: ${analysis.allHeadingsWithName}`);
|
||||
|
||||
console.log('\nName classes found:');
|
||||
Object.entries(analysis.nameClasses).forEach(([cls, count]) => {
|
||||
console.log(` (${count}x) ${cls.slice(0, 80)}`);
|
||||
});
|
||||
|
||||
console.log('\n--- Sample products WITHOUT name selector ---');
|
||||
analysis.sampleNoNameHTML.forEach((html, i) => {
|
||||
console.log(`\n[Sample ${i + 1}]:`);
|
||||
console.log(html);
|
||||
});
|
||||
|
||||
// Try different selectors
|
||||
console.log('\n\n--- Testing alternative selectors ---');
|
||||
|
||||
const altResults = await page.evaluate(() => {
|
||||
const products = document.querySelectorAll('[class*="product_product__"]');
|
||||
const tests: Record<string, number> = {};
|
||||
|
||||
const selectors = [
|
||||
'h5',
|
||||
'h4',
|
||||
'h3',
|
||||
'[class*="heading"]',
|
||||
'[class*="title"]',
|
||||
'[class*="name"]',
|
||||
'a[href*="/product/"]',
|
||||
'.product_product__name__JcEk0',
|
||||
'[class*="ProductCard"]',
|
||||
];
|
||||
|
||||
selectors.forEach(sel => {
|
||||
let count = 0;
|
||||
products.forEach(el => {
|
||||
if (el.querySelector(sel)) count++;
|
||||
});
|
||||
tests[sel] = count;
|
||||
});
|
||||
|
||||
return tests;
|
||||
});
|
||||
|
||||
Object.entries(altResults).forEach(([sel, count]) => {
|
||||
console.log(` ${sel}: ${count} products`);
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
116
backend/scripts/test-treez-stealth.ts
Normal file
116
backend/scripts/test-treez-stealth.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import puppeteer from 'puppeteer-extra';
|
||||
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
|
||||
import fs from 'fs';
|
||||
|
||||
puppeteer.use(StealthPlugin());
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
const capturedProducts: any[] = [];
|
||||
|
||||
// Use CDP to intercept responses
|
||||
const client = await page.target().createCDPSession();
|
||||
await client.send('Network.enable');
|
||||
|
||||
client.on('Network.responseReceived', async (event) => {
|
||||
const url = event.response.url;
|
||||
if (url.includes('gapcommerceapi.com/product/search') && event.response.status === 200) {
|
||||
try {
|
||||
const response = await client.send('Network.getResponseBody', {
|
||||
requestId: event.requestId,
|
||||
});
|
||||
|
||||
const body = response.base64Encoded
|
||||
? Buffer.from(response.body, 'base64').toString('utf8')
|
||||
: response.body;
|
||||
|
||||
const json = JSON.parse(body);
|
||||
const products = json.hits?.hits?.map((h: any) => h._source) || [];
|
||||
capturedProducts.push(...products);
|
||||
console.log('Captured ' + products.length + ' products (total: ' + capturedProducts.length + ')');
|
||||
|
||||
} catch (err: any) {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Loading page with Stealth plugin...\n');
|
||||
|
||||
await page.goto('https://shop.bestdispensary.com/shop', {
|
||||
waitUntil: 'networkidle2',
|
||||
timeout: 60000
|
||||
});
|
||||
await sleep(3000);
|
||||
|
||||
// Bypass age gate
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
console.log('Bypassing age gate...');
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(3000);
|
||||
}
|
||||
|
||||
// Wait for API calls
|
||||
await sleep(5000);
|
||||
|
||||
console.log('Initial capture: ' + capturedProducts.length + ' products');
|
||||
|
||||
// Scroll and click load more
|
||||
console.log('\nScrolling and clicking Load More...');
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
||||
await sleep(1000);
|
||||
|
||||
try {
|
||||
const btn = await page.$('button.collection__load-more');
|
||||
if (btn) {
|
||||
await btn.click();
|
||||
await sleep(2000);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (i % 5 === 0) {
|
||||
console.log('Progress: ' + capturedProducts.length + ' products');
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== RESULTS ===\n');
|
||||
|
||||
if (capturedProducts.length > 0) {
|
||||
const seen = new Set();
|
||||
const unique = capturedProducts.filter(p => {
|
||||
const id = p.id || p.productId;
|
||||
if (!id || seen.has(id)) return false;
|
||||
seen.add(id);
|
||||
return true;
|
||||
});
|
||||
|
||||
console.log('Total captured: ' + capturedProducts.length);
|
||||
console.log('Unique products: ' + unique.length);
|
||||
console.log('\nFields available:');
|
||||
console.log(Object.keys(unique[0]).sort().join('\n'));
|
||||
console.log('\nSample product:\n' + JSON.stringify(unique[0], null, 2));
|
||||
|
||||
fs.writeFileSync('/tmp/treez-products.json', JSON.stringify(unique, null, 2));
|
||||
console.log('\nSaved to /tmp/treez-products.json');
|
||||
} else {
|
||||
console.log('No products captured - API still blocking');
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main();
|
||||
117
backend/scripts/test-treez-xhr-intercept.ts
Normal file
117
backend/scripts/test-treez-xhr-intercept.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
import fs from 'fs';
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Enable request interception but don't modify requests
|
||||
await page.setRequestInterception(true);
|
||||
|
||||
const capturedProducts: any[] = [];
|
||||
|
||||
page.on('request', (request) => {
|
||||
request.continue();
|
||||
});
|
||||
|
||||
// Use CDP to intercept responses
|
||||
const client = await page.target().createCDPSession();
|
||||
await client.send('Network.enable');
|
||||
|
||||
client.on('Network.responseReceived', async (event) => {
|
||||
const url = event.response.url;
|
||||
if (url.includes('gapcommerceapi.com/product/search')) {
|
||||
console.log('ES Response detected: ' + event.response.status);
|
||||
|
||||
try {
|
||||
const response = await client.send('Network.getResponseBody', {
|
||||
requestId: event.requestId,
|
||||
});
|
||||
|
||||
const body = response.base64Encoded
|
||||
? Buffer.from(response.body, 'base64').toString('utf8')
|
||||
: response.body;
|
||||
|
||||
const json = JSON.parse(body);
|
||||
const products = json.hits?.hits?.map((h: any) => h._source) || [];
|
||||
capturedProducts.push(...products);
|
||||
console.log('Captured ' + products.length + ' products (total: ' + capturedProducts.length + ')');
|
||||
|
||||
} catch (err: any) {
|
||||
console.log('Could not get response body: ' + err.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Loading page with CDP interception...\n');
|
||||
|
||||
await page.goto('https://shop.bestdispensary.com/shop', {
|
||||
waitUntil: 'networkidle2',
|
||||
timeout: 60000
|
||||
});
|
||||
await sleep(3000);
|
||||
|
||||
// Bypass age gate
|
||||
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
||||
if (ageGate) {
|
||||
console.log('Bypassing age gate...');
|
||||
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
||||
if (btn) await btn.click();
|
||||
await sleep(3000);
|
||||
}
|
||||
|
||||
// Click load more many times
|
||||
console.log('\nClicking Load More...');
|
||||
for (let i = 0; i < 30; i++) {
|
||||
try {
|
||||
const btn = await page.$('button.collection__load-more');
|
||||
if (!btn) break;
|
||||
|
||||
const visible = await page.evaluate((b) => {
|
||||
const rect = b.getBoundingClientRect();
|
||||
return rect.width > 0 && rect.height > 0;
|
||||
}, btn);
|
||||
|
||||
if (!visible) break;
|
||||
|
||||
await btn.click();
|
||||
await sleep(1500);
|
||||
console.log('Click ' + (i+1) + ': ' + capturedProducts.length + ' products');
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== RESULTS ===\n');
|
||||
console.log('Total captured: ' + capturedProducts.length);
|
||||
|
||||
if (capturedProducts.length > 0) {
|
||||
// Dedupe
|
||||
const seen = new Set();
|
||||
const unique = capturedProducts.filter(p => {
|
||||
const id = p.id || p.productId;
|
||||
if (!id || seen.has(id)) return false;
|
||||
seen.add(id);
|
||||
return true;
|
||||
});
|
||||
|
||||
console.log('Unique products: ' + unique.length);
|
||||
console.log('\nFields: ' + Object.keys(unique[0]).sort().join('\n'));
|
||||
console.log('\nSample:\n' + JSON.stringify(unique[0], null, 2));
|
||||
|
||||
fs.writeFileSync('/tmp/treez-products.json', JSON.stringify(unique, null, 2));
|
||||
console.log('\nSaved to /tmp/treez-products.json');
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -92,6 +92,10 @@ router.get('/', async (req: Request, res: Response) => {
|
||||
filter.worker_id = req.query.worker_id as string;
|
||||
}
|
||||
|
||||
if (req.query.pool_id) {
|
||||
filter.pool_id = parseInt(req.query.pool_id as string, 10);
|
||||
}
|
||||
|
||||
if (req.query.limit) {
|
||||
filter.limit = parseInt(req.query.limit as string, 10);
|
||||
}
|
||||
@@ -122,6 +126,31 @@ router.get('/counts', async (_req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/tasks/counts/by-state
|
||||
* Get pending task counts grouped by state
|
||||
*/
|
||||
router.get('/counts/by-state', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
d.state as state_code,
|
||||
COUNT(*) FILTER (WHERE t.status = 'pending') as pending,
|
||||
COUNT(*) FILTER (WHERE t.status IN ('claimed', 'running')) as active,
|
||||
COUNT(*) as total
|
||||
FROM worker_tasks t
|
||||
JOIN dispensaries d ON t.dispensary_id = d.id
|
||||
WHERE t.status IN ('pending', 'claimed', 'running')
|
||||
GROUP BY d.state
|
||||
ORDER BY COUNT(*) DESC
|
||||
`);
|
||||
res.json({ states: result.rows });
|
||||
} catch (error: unknown) {
|
||||
console.error('Error getting task counts by state:', error);
|
||||
res.status(500).json({ error: 'Failed to get task counts by state' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/tasks/capacity
|
||||
* Get capacity metrics for all roles
|
||||
@@ -1638,4 +1667,145 @@ router.post('/pool/resume', async (_req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// GEO TASK POOLS - View pools and their contents
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* GET /api/tasks/pools/summary
|
||||
* Quick summary of all pools for dashboard
|
||||
* NOTE: Must be defined BEFORE /pools/:id to avoid route conflict
|
||||
*/
|
||||
router.get('/pools/summary', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const { rows } = await pool.query(`
|
||||
SELECT
|
||||
COUNT(DISTINCT tp.id) as total_pools,
|
||||
COUNT(DISTINCT tp.id) FILTER (WHERE tp.is_active) as active_pools,
|
||||
COUNT(DISTINCT d.id) as total_stores,
|
||||
COUNT(DISTINCT d.id) FILTER (WHERE d.pool_id IS NOT NULL) as assigned_stores,
|
||||
COUNT(DISTINCT t.id) FILTER (WHERE t.status = 'pending') as pending_tasks,
|
||||
COUNT(DISTINCT t.id) FILTER (WHERE t.status = 'running') as running_tasks
|
||||
FROM task_pools tp
|
||||
LEFT JOIN dispensaries d ON d.pool_id = tp.id
|
||||
LEFT JOIN worker_tasks t ON t.dispensary_id = d.id
|
||||
`);
|
||||
|
||||
const poolStatus = await getTaskPoolStatus();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
...rows[0],
|
||||
pool_open: poolStatus.open,
|
||||
});
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ success: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/tasks/pools
|
||||
* List all geo task pools with their stats
|
||||
*/
|
||||
router.get('/pools', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const { rows } = await pool.query(`
|
||||
SELECT
|
||||
tp.id,
|
||||
tp.name,
|
||||
tp.display_name,
|
||||
tp.state_code,
|
||||
tp.city,
|
||||
tp.timezone,
|
||||
tp.radius_miles,
|
||||
tp.is_active,
|
||||
COUNT(DISTINCT d.id) as store_count,
|
||||
COUNT(DISTINCT t.id) FILTER (WHERE t.status = 'pending') as pending_tasks,
|
||||
COUNT(DISTINCT t.id) FILTER (WHERE t.status = 'running') as running_tasks,
|
||||
COUNT(DISTINCT t.id) FILTER (WHERE t.status = 'completed') as completed_tasks,
|
||||
COUNT(DISTINCT wr.worker_id) FILTER (WHERE wr.current_pool_id = tp.id) as active_workers
|
||||
FROM task_pools tp
|
||||
LEFT JOIN dispensaries d ON d.pool_id = tp.id
|
||||
LEFT JOIN worker_tasks t ON t.dispensary_id = d.id
|
||||
LEFT JOIN worker_registry wr ON wr.current_pool_id = tp.id
|
||||
GROUP BY tp.id
|
||||
ORDER BY COUNT(DISTINCT t.id) FILTER (WHERE t.status = 'pending') DESC, tp.display_name
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
pools: rows,
|
||||
total: rows.length,
|
||||
});
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ success: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/tasks/pools/:id
|
||||
* Get a single pool with its stores and tasks
|
||||
*/
|
||||
router.get('/pools/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const poolId = parseInt(req.params.id);
|
||||
|
||||
// Get pool info
|
||||
const { rows: poolRows } = await pool.query(`
|
||||
SELECT * FROM task_pools WHERE id = $1
|
||||
`, [poolId]);
|
||||
|
||||
if (poolRows.length === 0) {
|
||||
return res.status(404).json({ success: false, error: 'Pool not found' });
|
||||
}
|
||||
|
||||
// Get stores in this pool
|
||||
const { rows: stores } = await pool.query(`
|
||||
SELECT
|
||||
d.id,
|
||||
d.name,
|
||||
d.city,
|
||||
d.state,
|
||||
d.latitude,
|
||||
d.longitude,
|
||||
COUNT(t.id) FILTER (WHERE t.status = 'pending') as pending_tasks,
|
||||
COUNT(t.id) FILTER (WHERE t.status = 'running') as running_tasks
|
||||
FROM dispensaries d
|
||||
LEFT JOIN worker_tasks t ON t.dispensary_id = d.id
|
||||
WHERE d.pool_id = $1
|
||||
GROUP BY d.id
|
||||
ORDER BY COUNT(t.id) FILTER (WHERE t.status = 'pending') DESC, d.name
|
||||
`, [poolId]);
|
||||
|
||||
// Get active workers for this pool
|
||||
const { rows: workers } = await pool.query(`
|
||||
SELECT
|
||||
worker_id,
|
||||
friendly_name,
|
||||
current_state,
|
||||
current_city,
|
||||
http_ip as proxy_ip,
|
||||
pool_stores_visited,
|
||||
pool_max_stores
|
||||
FROM worker_registry
|
||||
WHERE current_pool_id = $1
|
||||
`, [poolId]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
pool: poolRows[0],
|
||||
stores,
|
||||
workers,
|
||||
stats: {
|
||||
store_count: stores.length,
|
||||
worker_count: workers.length,
|
||||
pending_tasks: stores.reduce((sum, s) => sum + parseInt(s.pending_tasks || '0'), 0),
|
||||
running_tasks: stores.reduce((sum, s) => sum + parseInt(s.running_tasks || '0'), 0),
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ success: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
import { pool } from '../db/pool';
|
||||
import { buildEvomiProxyUrl, getEvomiConfig } from './crawl-rotator';
|
||||
import { isTaskPoolOpen } from '../tasks/task-pool-state';
|
||||
|
||||
export interface TaskPool {
|
||||
pool_id: number;
|
||||
@@ -46,12 +47,19 @@ export interface ClaimedPoolResult {
|
||||
|
||||
/**
|
||||
* Get all pools that have pending tasks
|
||||
* Returns empty if global pool is closed
|
||||
*/
|
||||
export async function getPoolsWithPendingTasks(): Promise<PoolWithPendingTasks[]> {
|
||||
const { rows } = await pool.query<PoolWithPendingTasks>(
|
||||
// Check global pool toggle first
|
||||
const poolOpen = await isTaskPoolOpen();
|
||||
if (!poolOpen) {
|
||||
return []; // Global pool is closed, no pools available
|
||||
}
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`SELECT * FROM get_pools_with_pending_tasks()`
|
||||
);
|
||||
return rows;
|
||||
return rows as PoolWithPendingTasks[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,7 +71,7 @@ export async function claimPool(
|
||||
poolId?: number
|
||||
): Promise<ClaimedPoolResult | null> {
|
||||
// Claim pool in database
|
||||
const { rows } = await pool.query<TaskPool>(
|
||||
const { rows } = await pool.query(
|
||||
`SELECT * FROM worker_claim_pool($1, $2)`,
|
||||
[workerId, poolId || null]
|
||||
);
|
||||
@@ -73,7 +81,7 @@ export async function claimPool(
|
||||
return null;
|
||||
}
|
||||
|
||||
const claimedPool = rows[0];
|
||||
const claimedPool = rows[0] as TaskPool;
|
||||
|
||||
// Build Evomi proxy URL for this pool's geo
|
||||
const evomiConfig = getEvomiConfig();
|
||||
@@ -107,17 +115,18 @@ export async function pullTasksFromPool(
|
||||
workerId: string,
|
||||
maxStores: number = 6
|
||||
): Promise<PoolTask[]> {
|
||||
const { rows } = await pool.query<PoolTask>(
|
||||
const { rows } = await pool.query(
|
||||
`SELECT * FROM pull_tasks_from_pool($1, $2)`,
|
||||
[workerId, maxStores]
|
||||
);
|
||||
|
||||
if (rows.length > 0) {
|
||||
const storeIds = [...new Set(rows.map(t => t.dispensary_id))];
|
||||
console.log(`[TaskPool] ${workerId} pulled ${rows.length} tasks for ${storeIds.length} stores`);
|
||||
const tasks = rows as PoolTask[];
|
||||
if (tasks.length > 0) {
|
||||
const storeIds = [...new Set(tasks.map(t => t.dispensary_id))];
|
||||
console.log(`[TaskPool] ${workerId} pulled ${tasks.length} tasks for ${storeIds.length} stores`);
|
||||
}
|
||||
|
||||
return rows;
|
||||
return tasks;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,7 +157,7 @@ export async function isWorkerExhausted(workerId: string): Promise<boolean> {
|
||||
* Get worker's current pool info
|
||||
*/
|
||||
export async function getWorkerPool(workerId: string): Promise<TaskPool | null> {
|
||||
const { rows } = await pool.query<TaskPool>(
|
||||
const { rows } = await pool.query(
|
||||
`SELECT tp.id as pool_id, tp.name as pool_name, tp.display_name,
|
||||
tp.state_code, tp.city, tp.latitude, tp.longitude, tp.timezone
|
||||
FROM worker_registry wr
|
||||
@@ -156,7 +165,7 @@ export async function getWorkerPool(workerId: string): Promise<TaskPool | null>
|
||||
WHERE wr.worker_id = $1`,
|
||||
[workerId]
|
||||
);
|
||||
return rows[0] || null;
|
||||
return (rows[0] as TaskPool) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -111,6 +111,7 @@ export interface TaskFilter {
|
||||
status?: TaskStatus | TaskStatus[];
|
||||
dispensary_id?: number;
|
||||
worker_id?: string;
|
||||
pool_id?: number;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
@@ -431,6 +432,11 @@ class TaskService {
|
||||
params.push(filter.worker_id);
|
||||
}
|
||||
|
||||
if (filter.pool_id) {
|
||||
conditions.push(`d.pool_id = $${paramIndex++}`);
|
||||
params.push(filter.pool_id);
|
||||
}
|
||||
|
||||
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
const limit = filter.limit ?? 100;
|
||||
const offset = filter.offset ?? 0;
|
||||
@@ -440,9 +446,12 @@ class TaskService {
|
||||
t.*,
|
||||
d.name as dispensary_name,
|
||||
d.slug as dispensary_slug,
|
||||
d.pool_id as pool_id,
|
||||
tp.display_name as pool_name,
|
||||
w.friendly_name as worker_name
|
||||
FROM worker_tasks t
|
||||
LEFT JOIN dispensaries d ON d.id = t.dispensary_id
|
||||
LEFT JOIN task_pools tp ON tp.id = d.pool_id
|
||||
LEFT JOIN worker_registry w ON w.worker_id = t.worker_id
|
||||
${whereClause}
|
||||
ORDER BY t.created_at DESC
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Task Worker
|
||||
* Task Worker (v2.1)
|
||||
*
|
||||
* A unified worker that pulls tasks from the worker_tasks queue.
|
||||
* Workers register on startup, get a friendly name, and pull tasks.
|
||||
@@ -2043,7 +2043,7 @@ export class TaskWorker {
|
||||
*/
|
||||
private async initStealthOnly(): Promise<boolean> {
|
||||
try {
|
||||
await this.ensureStealthReady();
|
||||
await this.initializeStealth();
|
||||
this.stealthInitialized = true;
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -51,8 +51,8 @@ const MINIO_ACCESS_KEY = process.env.MINIO_ACCESS_KEY;
|
||||
const MINIO_SECRET_KEY = process.env.MINIO_SECRET_KEY;
|
||||
const MINIO_BUCKET = process.env.MINIO_BUCKET || 'cannaiq';
|
||||
|
||||
// Check if MinIO is configured
|
||||
const useMinIO = !!(MINIO_ENDPOINT && MINIO_ACCESS_KEY && MINIO_SECRET_KEY);
|
||||
// Check if MinIO is configured (endpoint required, keys optional for IP-based auth)
|
||||
const useMinIO = !!MINIO_ENDPOINT;
|
||||
|
||||
let minioClient: Minio.Client | null = null;
|
||||
|
||||
@@ -62,8 +62,8 @@ function getMinioClient(): Minio.Client {
|
||||
endPoint: MINIO_ENDPOINT!,
|
||||
port: MINIO_PORT,
|
||||
useSSL: MINIO_USE_SSL,
|
||||
accessKey: MINIO_ACCESS_KEY!,
|
||||
secretKey: MINIO_SECRET_KEY!,
|
||||
accessKey: MINIO_ACCESS_KEY || '', // Empty for IP-based auth
|
||||
secretKey: MINIO_SECRET_KEY || '', // Empty for IP-based auth
|
||||
});
|
||||
}
|
||||
return minioClient!;
|
||||
|
||||
98
backend/treez-intercept.js
Normal file
98
backend/treez-intercept.js
Normal file
@@ -0,0 +1,98 @@
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
(async () => {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Collect all API requests
|
||||
const apiRequests = [];
|
||||
|
||||
page.on('request', (request) => {
|
||||
const url = request.url();
|
||||
// Focus on API/data requests
|
||||
if (url.includes('api') || url.includes('graphql') ||
|
||||
url.includes('.json') || request.resourceType() === 'xhr' ||
|
||||
request.resourceType() === 'fetch') {
|
||||
apiRequests.push({
|
||||
url: url,
|
||||
method: request.method(),
|
||||
headers: request.headers(),
|
||||
type: request.resourceType()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
page.on('response', async (response) => {
|
||||
const url = response.url();
|
||||
const status = response.status();
|
||||
|
||||
// Log API responses with content type
|
||||
if (url.includes('api') || url.includes('graphql') ||
|
||||
url.includes('.json') || url.includes('product')) {
|
||||
const contentType = response.headers()['content-type'] || '';
|
||||
console.log('[' + status + '] ' + url.substring(0, 120));
|
||||
|
||||
// Try to get JSON responses
|
||||
if (contentType.includes('json') && status === 200) {
|
||||
try {
|
||||
const text = await response.text();
|
||||
const preview = text.substring(0, 500);
|
||||
console.log(' Preview: ' + preview);
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Loading https://best.treez.io/onlinemenu/...');
|
||||
|
||||
await page.goto('https://best.treez.io/onlinemenu/', {
|
||||
waitUntil: 'networkidle2',
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
// Wait for any lazy-loaded content
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
// Try scrolling to trigger more loads
|
||||
await page.evaluate(() => {
|
||||
window.scrollTo(0, document.body.scrollHeight / 2);
|
||||
});
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
console.log('\n=== ALL API REQUESTS ===');
|
||||
apiRequests.forEach(req => {
|
||||
console.log(req.method + ' ' + req.url);
|
||||
if (req.headers['authorization']) {
|
||||
console.log(' Auth: ' + req.headers['authorization'].substring(0, 50) + '...');
|
||||
}
|
||||
});
|
||||
|
||||
// Check page content
|
||||
const content = await page.content();
|
||||
console.log('\n=== PAGE TITLE ===');
|
||||
console.log(await page.title());
|
||||
|
||||
// Look for product data in page
|
||||
const productData = await page.evaluate(() => {
|
||||
// Check for React state or window variables
|
||||
const windowKeys = Object.keys(window).filter(k =>
|
||||
k.includes('store') || k.includes('product') || k.includes('__')
|
||||
);
|
||||
return {
|
||||
windowKeys: windowKeys.slice(0, 20),
|
||||
bodyText: document.body.innerText.substring(0, 1000)
|
||||
};
|
||||
});
|
||||
|
||||
console.log('\n=== WINDOW KEYS ===');
|
||||
console.log(productData.windowKeys);
|
||||
|
||||
console.log('\n=== PAGE TEXT PREVIEW ===');
|
||||
console.log(productData.bodyText);
|
||||
|
||||
await browser.close();
|
||||
})();
|
||||
Reference in New Issue
Block a user