feat: Stealth worker system with mandatory proxy rotation
## Worker System - Role-agnostic workers that can handle any task type - Pod-based architecture with StatefulSet (5-15 pods, 5 workers each) - Custom pod names (Aethelgard, Xylos, Kryll, etc.) - Worker registry with friendly names and resource monitoring - Hub-and-spoke visualization on JobQueue page ## Stealth & Anti-Detection (REQUIRED) - Proxies are MANDATORY - workers fail to start without active proxies - CrawlRotator initializes on worker startup - Loads proxies from `proxies` table - Auto-rotates proxy + fingerprint on 403 errors - 12 browser fingerprints (Chrome, Firefox, Safari, Edge) - Locale/timezone matching for geographic consistency ## Task System - Renamed product_resync → product_refresh - Task chaining: store_discovery → entry_point → product_discovery - Priority-based claiming with FOR UPDATE SKIP LOCKED - Heartbeat and stale task recovery ## UI Updates - JobQueue: Pod visualization, resource monitoring on hover - WorkersDashboard: Simplified worker list - Removed unused filters from task list ## Other - IP2Location service for visitor analytics - Findagram consumer features scaffolding - Documentation updates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -288,3 +288,89 @@ export async function getStates() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// PRODUCTS
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Fetch products for a specific dispensary
|
||||
* @param {number} dispensaryId - Dispensary ID
|
||||
* @param {Object} params - Query parameters
|
||||
* @returns {Promise<{products: Array, pagination: Object}>}
|
||||
*/
|
||||
export async function getDispensaryProducts(dispensaryId, params = {}) {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (params.category) queryParams.append('category', params.category);
|
||||
if (params.brand) queryParams.append('brand', params.brand);
|
||||
if (params.search) queryParams.append('search', params.search);
|
||||
if (params.inStockOnly) queryParams.append('in_stock_only', 'true');
|
||||
if (params.limit) queryParams.append('limit', params.limit);
|
||||
if (params.offset) queryParams.append('offset', params.offset);
|
||||
|
||||
const queryString = queryParams.toString();
|
||||
const endpoint = `/api/v1/dispensaries/${dispensaryId}/products${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
return apiRequest(endpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get categories available at a dispensary
|
||||
* @param {number} dispensaryId - Dispensary ID
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
export async function getDispensaryCategories(dispensaryId) {
|
||||
return apiRequest(`/api/v1/dispensaries/${dispensaryId}/categories`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get brands available at a dispensary
|
||||
* @param {number} dispensaryId - Dispensary ID
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
export async function getDispensaryBrands(dispensaryId) {
|
||||
return apiRequest(`/api/v1/dispensaries/${dispensaryId}/brands`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map API product to UI format
|
||||
* @param {Object} apiProduct - Product from API
|
||||
* @returns {Object} - Product formatted for UI
|
||||
*/
|
||||
export function mapProductForUI(apiProduct) {
|
||||
const p = apiProduct;
|
||||
|
||||
// Parse price from string or number
|
||||
const parsePrice = (val) => {
|
||||
if (val === null || val === undefined) return null;
|
||||
const num = typeof val === 'string' ? parseFloat(val) : val;
|
||||
return isNaN(num) ? null : num;
|
||||
};
|
||||
|
||||
return {
|
||||
id: p.id,
|
||||
name: p.name || p.name_raw,
|
||||
brand: p.brand || p.brand_name || p.brand_name_raw,
|
||||
category: p.category || p.type || p.category_raw,
|
||||
subcategory: p.subcategory || p.subcategory_raw,
|
||||
strainType: p.strain_type,
|
||||
image: p.image_url || p.primary_image_url,
|
||||
thc: p.thc || p.thc_percent || p.thc_percentage,
|
||||
cbd: p.cbd || p.cbd_percent || p.cbd_percentage,
|
||||
price: parsePrice(p.price_rec) || parsePrice(p.regular_price) || parsePrice(p.price),
|
||||
salePrice: parsePrice(p.price_rec_special) || parsePrice(p.sale_price),
|
||||
inStock: p.in_stock !== undefined ? p.in_stock : p.stock_status === 'in_stock',
|
||||
stockStatus: p.stock_status,
|
||||
onSale: p.on_special || p.special || false,
|
||||
updatedAt: p.updated_at || p.snapshot_at,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get aggregate stats (product count, brand count, dispensary count)
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function getStats() {
|
||||
return apiRequest('/api/v1/stats');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user