## 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>
303 lines
7.9 KiB
JavaScript
303 lines
7.9 KiB
JavaScript
/**
|
|
* Consumer API Client for Findagram
|
|
*
|
|
* Handles authenticated requests for:
|
|
* - Favorites
|
|
* - Price Alerts
|
|
* - Saved Searches
|
|
*
|
|
* All methods require auth token (use with AuthContext's authFetch)
|
|
*/
|
|
|
|
// ============================================================
|
|
// FAVORITES
|
|
// ============================================================
|
|
|
|
/**
|
|
* Get all user's favorites
|
|
* @param {Function} authFetch - Authenticated fetch from AuthContext
|
|
*/
|
|
export async function getFavorites(authFetch) {
|
|
return authFetch('/api/consumer/favorites');
|
|
}
|
|
|
|
/**
|
|
* Add product to favorites
|
|
* @param {Function} authFetch
|
|
* @param {number} productId
|
|
* @param {number} [dispensaryId] - Optional dispensary context
|
|
*/
|
|
export async function addFavorite(authFetch, productId, dispensaryId = null) {
|
|
return authFetch('/api/consumer/favorites', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ productId, dispensaryId }),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Remove favorite by favorite ID
|
|
* @param {Function} authFetch
|
|
* @param {number} favoriteId
|
|
*/
|
|
export async function removeFavorite(authFetch, favoriteId) {
|
|
return authFetch(`/api/consumer/favorites/${favoriteId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Remove favorite by product ID
|
|
* @param {Function} authFetch
|
|
* @param {number} productId
|
|
*/
|
|
export async function removeFavoriteByProduct(authFetch, productId) {
|
|
return authFetch(`/api/consumer/favorites/product/${productId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if product is favorited
|
|
* @param {Function} authFetch
|
|
* @param {number} productId
|
|
* @returns {Promise<{isFavorited: boolean}>}
|
|
*/
|
|
export async function checkFavorite(authFetch, productId) {
|
|
return authFetch(`/api/consumer/favorites/check/product/${productId}`);
|
|
}
|
|
|
|
// ============================================================
|
|
// ALERTS
|
|
// ============================================================
|
|
|
|
/**
|
|
* Get all user's alerts
|
|
* @param {Function} authFetch
|
|
*/
|
|
export async function getAlerts(authFetch) {
|
|
return authFetch('/api/consumer/alerts');
|
|
}
|
|
|
|
/**
|
|
* Get alert statistics
|
|
* @param {Function} authFetch
|
|
*/
|
|
export async function getAlertStats(authFetch) {
|
|
return authFetch('/api/consumer/alerts/stats');
|
|
}
|
|
|
|
/**
|
|
* Create a price drop alert
|
|
* @param {Function} authFetch
|
|
* @param {Object} params
|
|
* @param {number} params.productId - Product to track
|
|
* @param {number} params.targetPrice - Price to alert at
|
|
* @param {number} [params.dispensaryId] - Optional dispensary context
|
|
*/
|
|
export async function createPriceAlert(authFetch, { productId, targetPrice, dispensaryId }) {
|
|
return authFetch('/api/consumer/alerts', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
alertType: 'price_drop',
|
|
productId,
|
|
targetPrice,
|
|
dispensaryId,
|
|
}),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a back-in-stock alert
|
|
* @param {Function} authFetch
|
|
* @param {Object} params
|
|
* @param {number} params.productId - Product to track
|
|
* @param {number} [params.dispensaryId] - Optional dispensary context
|
|
*/
|
|
export async function createStockAlert(authFetch, { productId, dispensaryId }) {
|
|
return authFetch('/api/consumer/alerts', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
alertType: 'back_in_stock',
|
|
productId,
|
|
dispensaryId,
|
|
}),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a brand/category alert
|
|
* @param {Function} authFetch
|
|
* @param {Object} params
|
|
* @param {string} [params.brand] - Brand to track
|
|
* @param {string} [params.category] - Category to track
|
|
*/
|
|
export async function createBrandCategoryAlert(authFetch, { brand, category }) {
|
|
return authFetch('/api/consumer/alerts', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
alertType: 'product_on_special',
|
|
brand,
|
|
category,
|
|
}),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update an alert
|
|
* @param {Function} authFetch
|
|
* @param {number} alertId
|
|
* @param {Object} updates
|
|
* @param {boolean} [updates.isActive]
|
|
* @param {number} [updates.targetPrice]
|
|
*/
|
|
export async function updateAlert(authFetch, alertId, updates) {
|
|
return authFetch(`/api/consumer/alerts/${alertId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(updates),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Toggle alert active status
|
|
* @param {Function} authFetch
|
|
* @param {number} alertId
|
|
*/
|
|
export async function toggleAlert(authFetch, alertId) {
|
|
return authFetch(`/api/consumer/alerts/${alertId}/toggle`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Delete an alert
|
|
* @param {Function} authFetch
|
|
* @param {number} alertId
|
|
*/
|
|
export async function deleteAlert(authFetch, alertId) {
|
|
return authFetch(`/api/consumer/alerts/${alertId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
// ============================================================
|
|
// SAVED SEARCHES
|
|
// ============================================================
|
|
|
|
/**
|
|
* Get all user's saved searches
|
|
* @param {Function} authFetch
|
|
*/
|
|
export async function getSavedSearches(authFetch) {
|
|
return authFetch('/api/consumer/saved-searches');
|
|
}
|
|
|
|
/**
|
|
* Create a saved search
|
|
* @param {Function} authFetch
|
|
* @param {Object} params
|
|
* @param {string} params.name - Display name
|
|
* @param {string} [params.query] - Search query
|
|
* @param {string} [params.category] - Category filter
|
|
* @param {string} [params.brand] - Brand filter
|
|
* @param {string} [params.strainType] - Strain type filter
|
|
* @param {number} [params.minPrice] - Min price filter
|
|
* @param {number} [params.maxPrice] - Max price filter
|
|
* @param {number} [params.minThc] - Min THC filter
|
|
* @param {number} [params.maxThc] - Max THC filter
|
|
* @param {boolean} [params.notifyOnNew] - Notify on new products
|
|
* @param {boolean} [params.notifyOnPriceDrop] - Notify on price drops
|
|
*/
|
|
export async function createSavedSearch(authFetch, params) {
|
|
return authFetch('/api/consumer/saved-searches', {
|
|
method: 'POST',
|
|
body: JSON.stringify(params),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update a saved search
|
|
* @param {Function} authFetch
|
|
* @param {number} searchId
|
|
* @param {Object} updates
|
|
*/
|
|
export async function updateSavedSearch(authFetch, searchId, updates) {
|
|
return authFetch(`/api/consumer/saved-searches/${searchId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(updates),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Delete a saved search
|
|
* @param {Function} authFetch
|
|
* @param {number} searchId
|
|
*/
|
|
export async function deleteSavedSearch(authFetch, searchId) {
|
|
return authFetch(`/api/consumer/saved-searches/${searchId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Run a saved search (get search params)
|
|
* @param {Function} authFetch
|
|
* @param {number} searchId
|
|
* @returns {Promise<{searchParams: Object, searchUrl: string}>}
|
|
*/
|
|
export async function runSavedSearch(authFetch, searchId) {
|
|
return authFetch(`/api/consumer/saved-searches/${searchId}/run`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
// ============================================================
|
|
// HELPER: Generate search name from filters
|
|
// ============================================================
|
|
|
|
/**
|
|
* Generate a display name for a search based on filters
|
|
* @param {Object} filters
|
|
* @returns {string}
|
|
*/
|
|
export function generateSearchName(filters) {
|
|
const parts = [];
|
|
|
|
if (filters.query || filters.search) parts.push(`"${filters.query || filters.search}"`);
|
|
if (filters.category || filters.type) parts.push(filters.category || filters.type);
|
|
if (filters.brand || filters.brandName) parts.push(filters.brand || filters.brandName);
|
|
if (filters.strainType) parts.push(filters.strainType);
|
|
if (filters.maxPrice) parts.push(`Under $${filters.maxPrice}`);
|
|
if (filters.minThc) parts.push(`${filters.minThc}%+ THC`);
|
|
|
|
return parts.length > 0 ? parts.join(' - ') : 'All Products';
|
|
}
|
|
|
|
// Default export
|
|
const consumerApi = {
|
|
// Favorites
|
|
getFavorites,
|
|
addFavorite,
|
|
removeFavorite,
|
|
removeFavoriteByProduct,
|
|
checkFavorite,
|
|
// Alerts
|
|
getAlerts,
|
|
getAlertStats,
|
|
createPriceAlert,
|
|
createStockAlert,
|
|
createBrandCategoryAlert,
|
|
updateAlert,
|
|
toggleAlert,
|
|
deleteAlert,
|
|
// Saved Searches
|
|
getSavedSearches,
|
|
createSavedSearch,
|
|
updateSavedSearch,
|
|
deleteSavedSearch,
|
|
runSavedSearch,
|
|
// Helpers
|
|
generateSearchName,
|
|
};
|
|
|
|
export default consumerApi;
|