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:
Kelly
2025-12-10 00:44:59 -07:00
parent 0295637ed6
commit 56cc171287
61 changed files with 8591 additions and 2076 deletions

View File

@@ -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');
}