- Add backend stale process monitoring API (/api/stale-processes) - Add users management route - Add frontend landing page and stale process monitor UI on /scraper-tools - Move old development scripts to backend/archive/ - Update frontend build with new features 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
191 lines
5.9 KiB
TypeScript
191 lines
5.9 KiB
TypeScript
import { firefox } from 'playwright';
|
|
import { getRandomProxy } from './src/utils/proxyManager.js';
|
|
|
|
async function testStockStatus() {
|
|
const proxy = await getRandomProxy();
|
|
if (!proxy) {
|
|
console.log('❌ No proxy available');
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`🔐 Using proxy: ${proxy.server}`);
|
|
|
|
const browser = await firefox.launch({ headless: true });
|
|
|
|
const context = await browser.newContext({
|
|
viewport: { width: 1920, height: 1080 },
|
|
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
|
proxy: {
|
|
server: proxy.server,
|
|
username: proxy.username,
|
|
password: proxy.password
|
|
}
|
|
});
|
|
|
|
const page = await context.newPage();
|
|
|
|
// Test multiple dispensaries
|
|
const dispensaries = [
|
|
'AZ-Deeply-Rooted',
|
|
'CA-Sol-Flower-Blythe',
|
|
'CA-Cookies-Santa-Ana',
|
|
'CO-Lemonnade-Denver'
|
|
];
|
|
|
|
let totalFoundWithStock = 0;
|
|
|
|
for (const dispensary of dispensaries) {
|
|
try {
|
|
const menuUrl = `https://dutchie.com/embedded-menu/${dispensary}`;
|
|
console.log(`\n📍 Loading menu: ${menuUrl}...`);
|
|
|
|
await page.goto(menuUrl, {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: 45000
|
|
});
|
|
|
|
await page.waitForTimeout(5000);
|
|
|
|
// Scroll to load more products
|
|
console.log('📜 Scrolling to load products...');
|
|
for (let i = 0; i < 5; i++) {
|
|
await page.evaluate(() => window.scrollBy(0, window.innerHeight));
|
|
await page.waitForTimeout(2000);
|
|
}
|
|
|
|
// Analyze stock status text across all product cards
|
|
const stockAnalysis = await page.evaluate(() => {
|
|
const productCards = Array.from(document.querySelectorAll('a[href*="/product/"]'));
|
|
console.log(`Found ${productCards.length} product cards`);
|
|
|
|
const stockStatuses: {
|
|
allText: string;
|
|
hasOutOfStock: boolean;
|
|
hasLowStock: boolean;
|
|
hasExpired: boolean;
|
|
hasPreOrder: boolean;
|
|
hasQuantity: boolean;
|
|
stockRelatedText: string[];
|
|
}[] = [];
|
|
|
|
const stockKeywords = [
|
|
'out of stock',
|
|
'low stock',
|
|
'limited',
|
|
'expired',
|
|
'pre-order',
|
|
'preorder',
|
|
'call for',
|
|
'only',
|
|
'left',
|
|
'available',
|
|
'in stock',
|
|
'unavailable'
|
|
];
|
|
|
|
productCards.forEach((card) => {
|
|
const allText = (card.textContent || '').toLowerCase();
|
|
|
|
// Find any stock-related text
|
|
const foundKeywords = stockKeywords.filter(keyword =>
|
|
allText.includes(keyword)
|
|
);
|
|
|
|
if (foundKeywords.length > 0) {
|
|
stockStatuses.push({
|
|
allText: allText.substring(0, 200),
|
|
hasOutOfStock: allText.includes('out of stock'),
|
|
hasLowStock: allText.includes('low stock') || allText.includes('limited'),
|
|
hasExpired: allText.includes('expired'),
|
|
hasPreOrder: allText.includes('pre-order') || allText.includes('preorder'),
|
|
hasQuantity: /only \d+ left/.test(allText) || /\d+ left/.test(allText),
|
|
stockRelatedText: foundKeywords
|
|
});
|
|
}
|
|
});
|
|
|
|
return {
|
|
totalProducts: productCards.length,
|
|
productsWithStockInfo: stockStatuses.length,
|
|
stockStatuses: stockStatuses.slice(0, 20) // Sample first 20
|
|
};
|
|
});
|
|
|
|
console.log(`\n📊 Stock Status Analysis:`);
|
|
console.log(` Total products found: ${stockAnalysis.totalProducts}`);
|
|
console.log(` Products with stock info: ${stockAnalysis.productsWithStockInfo}`);
|
|
|
|
if (stockAnalysis.productsWithStockInfo > 0) {
|
|
console.log(`\n Stock Status Samples:`);
|
|
|
|
const summary = {
|
|
outOfStock: 0,
|
|
lowStock: 0,
|
|
expired: 0,
|
|
preOrder: 0,
|
|
quantity: 0
|
|
};
|
|
|
|
stockAnalysis.stockStatuses.forEach((status, i) => {
|
|
console.log(`\n Product ${i + 1}:`);
|
|
console.log(` Keywords found: ${status.stockRelatedText.join(', ')}`);
|
|
console.log(` Text preview: "${status.allText.substring(0, 150)}..."`);
|
|
|
|
if (status.hasOutOfStock) {
|
|
summary.outOfStock++;
|
|
console.log(` ⚠️ OUT OF STOCK`);
|
|
}
|
|
if (status.hasLowStock) {
|
|
summary.lowStock++;
|
|
console.log(` ⚠️ LOW STOCK / LIMITED`);
|
|
}
|
|
if (status.hasExpired) {
|
|
summary.expired++;
|
|
console.log(` ⚠️ EXPIRED`);
|
|
}
|
|
if (status.hasPreOrder) {
|
|
summary.preOrder++;
|
|
console.log(` ⚠️ PRE-ORDER`);
|
|
}
|
|
if (status.hasQuantity) {
|
|
summary.quantity++;
|
|
console.log(` ⚠️ QUANTITY LIMITED`);
|
|
}
|
|
});
|
|
|
|
console.log(`\n Summary (from sampled products):`);
|
|
console.log(` Out of stock: ${summary.outOfStock}`);
|
|
console.log(` Low stock: ${summary.lowStock}`);
|
|
console.log(` Expired: ${summary.expired}`);
|
|
console.log(` Pre-order: ${summary.preOrder}`);
|
|
console.log(` Quantity limited: ${summary.quantity}`);
|
|
} else {
|
|
console.log(`\n ✅ No stock status indicators found (all products appear in stock)`);
|
|
}
|
|
|
|
if (stockAnalysis.productsWithStockInfo > 0) {
|
|
totalFoundWithStock += stockAnalysis.productsWithStockInfo;
|
|
// Take a screenshot if we found stock info
|
|
await page.screenshot({ path: `/tmp/stock-${dispensary}.png`, fullPage: true });
|
|
console.log(`\n📸 Screenshot saved to /tmp/stock-${dispensary}.png`);
|
|
}
|
|
|
|
} catch (error: any) {
|
|
console.error(` ❌ Error loading ${dispensary}:`, error.message);
|
|
}
|
|
|
|
// Wait between dispensaries
|
|
await page.waitForTimeout(2000);
|
|
}
|
|
|
|
console.log(`\n${'='.repeat(60)}`);
|
|
console.log(`✅ SEARCH COMPLETE`);
|
|
console.log(` Dispensaries checked: ${dispensaries.length}`);
|
|
console.log(` Total products with stock info found: ${totalFoundWithStock}`);
|
|
console.log(`${'='.repeat(60)}\n`);
|
|
|
|
await browser.close();
|
|
}
|
|
|
|
testStockStatus();
|