Files
cannaiq/findagram/frontend/src/api/consumer.js
Kelly 56cc171287 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>
2025-12-10 00:44:59 -07:00

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;