Files
cannaiq/findagram/frontend/src/lib/storage.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

364 lines
9.4 KiB
JavaScript

/**
* localStorage helpers for user data persistence
*
* Manages favorites, price alerts, and saved searches without requiring authentication.
* All data is stored locally in the browser.
*/
const STORAGE_KEYS = {
FAVORITES: 'findagram_favorites',
ALERTS: 'findagram_alerts',
SAVED_SEARCHES: 'findagram_saved_searches',
};
// ============================================================
// FAVORITES
// ============================================================
/**
* Get all favorite product IDs
* @returns {number[]} Array of product IDs
*/
export function getFavorites() {
try {
const data = localStorage.getItem(STORAGE_KEYS.FAVORITES);
return data ? JSON.parse(data) : [];
} catch (e) {
console.error('Error reading favorites:', e);
return [];
}
}
/**
* Check if a product is favorited
* @param {number} productId
* @returns {boolean}
*/
export function isFavorite(productId) {
const favorites = getFavorites();
return favorites.includes(productId);
}
/**
* Add a product to favorites
* @param {number} productId
*/
export function addFavorite(productId) {
const favorites = getFavorites();
if (!favorites.includes(productId)) {
favorites.push(productId);
localStorage.setItem(STORAGE_KEYS.FAVORITES, JSON.stringify(favorites));
}
}
/**
* Remove a product from favorites
* @param {number} productId
*/
export function removeFavorite(productId) {
const favorites = getFavorites();
const updated = favorites.filter(id => id !== productId);
localStorage.setItem(STORAGE_KEYS.FAVORITES, JSON.stringify(updated));
}
/**
* Toggle a product's favorite status
* @param {number} productId
* @returns {boolean} New favorite status
*/
export function toggleFavorite(productId) {
if (isFavorite(productId)) {
removeFavorite(productId);
return false;
} else {
addFavorite(productId);
return true;
}
}
/**
* Clear all favorites
*/
export function clearFavorites() {
localStorage.setItem(STORAGE_KEYS.FAVORITES, JSON.stringify([]));
}
// ============================================================
// PRICE ALERTS
// ============================================================
/**
* @typedef {Object} PriceAlert
* @property {string} id - Unique alert ID
* @property {number} productId - Product ID to track
* @property {string} productName - Product name (for display when offline)
* @property {string} productImage - Product image URL
* @property {string} brandName - Brand name
* @property {number} targetPrice - Target price to alert at
* @property {number} originalPrice - Price when alert was created
* @property {boolean} active - Whether alert is active
* @property {string} createdAt - ISO date string
*/
/**
* Get all price alerts
* @returns {PriceAlert[]}
*/
export function getAlerts() {
try {
const data = localStorage.getItem(STORAGE_KEYS.ALERTS);
return data ? JSON.parse(data) : [];
} catch (e) {
console.error('Error reading alerts:', e);
return [];
}
}
/**
* Get alert for a specific product
* @param {number} productId
* @returns {PriceAlert|null}
*/
export function getAlertForProduct(productId) {
const alerts = getAlerts();
return alerts.find(a => a.productId === productId) || null;
}
/**
* Create a new price alert
* @param {Object} params
* @param {number} params.productId
* @param {string} params.productName
* @param {string} params.productImage
* @param {string} params.brandName
* @param {number} params.targetPrice
* @param {number} params.originalPrice
* @returns {PriceAlert}
*/
export function createAlert({ productId, productName, productImage, brandName, targetPrice, originalPrice }) {
const alerts = getAlerts();
// Check if alert already exists for this product
const existingIndex = alerts.findIndex(a => a.productId === productId);
const alert = {
id: existingIndex >= 0 ? alerts[existingIndex].id : `alert_${Date.now()}`,
productId,
productName,
productImage,
brandName,
targetPrice,
originalPrice,
active: true,
createdAt: existingIndex >= 0 ? alerts[existingIndex].createdAt : new Date().toISOString(),
};
if (existingIndex >= 0) {
alerts[existingIndex] = alert;
} else {
alerts.push(alert);
}
localStorage.setItem(STORAGE_KEYS.ALERTS, JSON.stringify(alerts));
return alert;
}
/**
* Update an existing alert
* @param {string} alertId
* @param {Partial<PriceAlert>} updates
*/
export function updateAlert(alertId, updates) {
const alerts = getAlerts();
const index = alerts.findIndex(a => a.id === alertId);
if (index >= 0) {
alerts[index] = { ...alerts[index], ...updates };
localStorage.setItem(STORAGE_KEYS.ALERTS, JSON.stringify(alerts));
}
}
/**
* Toggle alert active status
* @param {string} alertId
* @returns {boolean} New active status
*/
export function toggleAlertActive(alertId) {
const alerts = getAlerts();
const alert = alerts.find(a => a.id === alertId);
if (alert) {
alert.active = !alert.active;
localStorage.setItem(STORAGE_KEYS.ALERTS, JSON.stringify(alerts));
return alert.active;
}
return false;
}
/**
* Delete an alert
* @param {string} alertId
*/
export function deleteAlert(alertId) {
const alerts = getAlerts();
const updated = alerts.filter(a => a.id !== alertId);
localStorage.setItem(STORAGE_KEYS.ALERTS, JSON.stringify(updated));
}
/**
* Clear all alerts
*/
export function clearAlerts() {
localStorage.setItem(STORAGE_KEYS.ALERTS, JSON.stringify([]));
}
// ============================================================
// SAVED SEARCHES
// ============================================================
/**
* @typedef {Object} SavedSearch
* @property {string} id - Unique search ID
* @property {string} name - User-defined name for the search
* @property {Object} filters - Search filter parameters
* @property {string} [filters.search] - Search term
* @property {string} [filters.type] - Category type
* @property {string} [filters.brandName] - Brand filter
* @property {string} [filters.strainType] - Strain type filter
* @property {number} [filters.priceMax] - Max price filter
* @property {number} [filters.thcMin] - Min THC filter
* @property {string} createdAt - ISO date string
*/
/**
* Get all saved searches
* @returns {SavedSearch[]}
*/
export function getSavedSearches() {
try {
const data = localStorage.getItem(STORAGE_KEYS.SAVED_SEARCHES);
return data ? JSON.parse(data) : [];
} catch (e) {
console.error('Error reading saved searches:', e);
return [];
}
}
/**
* Create a new saved search
* @param {Object} params
* @param {string} params.name - Display name for the search
* @param {Object} params.filters - Search filters
* @returns {SavedSearch}
*/
export function createSavedSearch({ name, filters }) {
const searches = getSavedSearches();
const search = {
id: `search_${Date.now()}`,
name,
filters,
createdAt: new Date().toISOString(),
};
searches.push(search);
localStorage.setItem(STORAGE_KEYS.SAVED_SEARCHES, JSON.stringify(searches));
return search;
}
/**
* Update a saved search
* @param {string} searchId
* @param {Partial<SavedSearch>} updates
*/
export function updateSavedSearch(searchId, updates) {
const searches = getSavedSearches();
const index = searches.findIndex(s => s.id === searchId);
if (index >= 0) {
searches[index] = { ...searches[index], ...updates };
localStorage.setItem(STORAGE_KEYS.SAVED_SEARCHES, JSON.stringify(searches));
}
}
/**
* Delete a saved search
* @param {string} searchId
*/
export function deleteSavedSearch(searchId) {
const searches = getSavedSearches();
const updated = searches.filter(s => s.id !== searchId);
localStorage.setItem(STORAGE_KEYS.SAVED_SEARCHES, JSON.stringify(updated));
}
/**
* Clear all saved searches
*/
export function clearSavedSearches() {
localStorage.setItem(STORAGE_KEYS.SAVED_SEARCHES, JSON.stringify([]));
}
// ============================================================
// UTILITY FUNCTIONS
// ============================================================
/**
* Build a URL with search params from filters
* @param {Object} filters
* @returns {string}
*/
export function buildSearchUrl(filters) {
const params = new URLSearchParams();
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
params.set(key, value);
}
});
return `/products?${params.toString()}`;
}
/**
* Generate a name for a search based on its filters
* @param {Object} filters
* @returns {string}
*/
export function generateSearchName(filters) {
const parts = [];
if (filters.search) parts.push(`"${filters.search}"`);
if (filters.type) parts.push(filters.type);
if (filters.brandName) parts.push(filters.brandName);
if (filters.strainType) parts.push(filters.strainType);
if (filters.priceMax) parts.push(`Under $${filters.priceMax}`);
if (filters.thcMin) parts.push(`${filters.thcMin}%+ THC`);
return parts.length > 0 ? parts.join(' - ') : 'All Products';
}
// Default export
const storage = {
// Favorites
getFavorites,
isFavorite,
addFavorite,
removeFavorite,
toggleFavorite,
clearFavorites,
// Alerts
getAlerts,
getAlertForProduct,
createAlert,
updateAlert,
toggleAlertActive,
deleteAlert,
clearAlerts,
// Saved Searches
getSavedSearches,
createSavedSearch,
updateSavedSearch,
deleteSavedSearch,
clearSavedSearches,
// Utilities
buildSearchUrl,
generateSearchName,
};
export default storage;