Files
cannaiq/backend/src/scripts/estimate-bandwidth.ts
Kelly 2f483b3084 feat: SEO template library, discovery pipeline, and orchestrator enhancements
## SEO Template Library
- Add complete template library with 7 page types (state, city, category, brand, product, search, regeneration)
- Add Template Library tab in SEO Orchestrator with accordion-based editors
- Add template preview, validation, and variable injection engine
- Add API endpoints: /api/seo/templates, preview, validate, generate, regenerate

## Discovery Pipeline
- Add promotion.ts for discovery location validation and promotion
- Add discover-all-states.ts script for multi-state discovery
- Add promotion log migration (067)
- Enhance discovery routes and types

## Orchestrator & Admin
- Add crawl_enabled filter to stores page
- Add API permissions page
- Add job queue management
- Add price analytics routes
- Add markets and intelligence routes
- Enhance dashboard and worker monitoring

## Infrastructure
- Add migrations for worker definitions, SEO settings, field alignment
- Add canonical pipeline for scraper v2
- Update hydration and sync orchestrator
- Enhance multi-state query service

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 00:05:34 -07:00

174 lines
5.5 KiB
TypeScript

import axios from 'axios';
import { Pool } from 'pg';
const DUTCHIE_GRAPHQL_URL = 'https://dutchie.com/graphql';
const MENU_PRODUCTS_QUERY = `
query FilteredProducts($productsFilter: ProductFilterInput!) {
filteredProducts(productsFilter: $productsFilter) {
products {
id
name
brand
category
subcategory
strainType
description
image
images {
id
url
}
posId
potencyCbd {
formatted
range
unit
}
potencyThc {
formatted
range
unit
}
variants {
id
option
price
priceMed
priceRec
quantity
specialPrice
}
status
}
}
}
`;
function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
async function measureRequest(dispensaryId: string, mode: 'A' | 'B') {
const variables: any = {
productsFilter: {
dispensaryId,
pricingType: 'rec',
Status: mode === 'A' ? 'Active' : null,
}
};
const requestBody = JSON.stringify({
query: MENU_PRODUCTS_QUERY,
variables,
});
const requestSize = Buffer.byteLength(requestBody, 'utf8');
try {
const response = await axios.post(DUTCHIE_GRAPHQL_URL, requestBody, {
headers: {
'Content-Type': 'application/json',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Origin': 'https://dutchie.com',
},
timeout: 30000,
});
const responseSize = Buffer.byteLength(JSON.stringify(response.data), 'utf8');
const productCount = response.data?.data?.filteredProducts?.products?.length || 0;
// Debug: show what we got
if (productCount === 0) {
console.log(` Response preview: ${JSON.stringify(response.data).slice(0, 300)}...`);
}
return { requestSize, responseSize, productCount };
} catch (error: any) {
console.error(` Error: ${error.message}`);
if (error.response) {
console.error(` Status: ${error.response.status}`);
console.error(` Data: ${JSON.stringify(error.response.data).slice(0, 200)}`);
}
return { requestSize, responseSize: 0, productCount: 0, error: error.message };
}
}
async function main() {
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
// Get one store with products (use a known good ID)
const { rows } = await pool.query(`
SELECT d.platform_dispensary_id, d.name, COUNT(sp.id) as product_count
FROM dispensaries d
LEFT JOIN store_products sp ON d.id = sp.dispensary_id
WHERE d.platform_dispensary_id IS NOT NULL
GROUP BY d.id
ORDER BY product_count DESC
LIMIT 1
`);
if (rows.length === 0) {
console.log('No crawlable stores found');
await pool.end();
return;
}
const store = rows[0];
console.log('=== Dutchie GraphQL Bandwidth for One Store ===\n');
console.log(`Store: ${store.name}`);
console.log(`Platform ID: ${store.platform_dispensary_id}`);
console.log(`Products in DB: ${store.product_count || 'unknown'}\n`);
// Mode A (Active products with pricing)
console.log('Fetching Mode A (Active products)...');
const modeA = await measureRequest(store.platform_dispensary_id, 'A');
// Mode B (All products)
console.log('Fetching Mode B (All products)...');
const modeB = await measureRequest(store.platform_dispensary_id, 'B');
console.log('\n=== Results for ONE STORE ===');
console.log('\nMode A (Active products with pricing):');
console.log(` Request size: ${formatBytes(modeA.requestSize)}`);
console.log(` Response size: ${formatBytes(modeA.responseSize)}`);
console.log(` Products: ${modeA.productCount}`);
if (modeA.productCount > 0) {
console.log(` Per product: ${formatBytes(modeA.responseSize / modeA.productCount)}`);
}
console.log('\nMode B (All products incl. OOS):');
console.log(` Request size: ${formatBytes(modeB.requestSize)}`);
console.log(` Response size: ${formatBytes(modeB.responseSize)}`);
console.log(` Products: ${modeB.productCount}`);
if (modeB.productCount > 0) {
console.log(` Per product: ${formatBytes(modeB.responseSize / modeB.productCount)}`);
}
console.log('\nDual-Mode Crawl (what we actually do):');
const totalRequest = modeA.requestSize + modeB.requestSize;
const totalResponse = modeA.responseSize + modeB.responseSize;
const totalBandwidth = totalRequest + totalResponse;
console.log(` Total request: ${formatBytes(totalRequest)}`);
console.log(` Total response: ${formatBytes(totalResponse)}`);
console.log(` TOTAL BANDWIDTH: ${formatBytes(totalBandwidth)}`);
// Per-product average
const avgProducts = Math.max(modeA.productCount, modeB.productCount);
const bytesPerProduct = avgProducts > 0 ? totalResponse / avgProducts : 0;
console.log('\n=== Quick Reference ===');
console.log(`Average bytes per product: ~${formatBytes(bytesPerProduct)}`);
console.log(`\nTypical store sizes:`);
console.log(` Small (100 products): ~${formatBytes(bytesPerProduct * 100 + totalRequest)}`);
console.log(` Medium (300 products): ~${formatBytes(bytesPerProduct * 300 + totalRequest)}`);
console.log(` Large (500 products): ~${formatBytes(bytesPerProduct * 500 + totalRequest)}`);
await pool.end();
}
main().catch(console.error);