## 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>
315 lines
8.9 KiB
JavaScript
315 lines
8.9 KiB
JavaScript
import { useState, useEffect, useCallback } from 'react';
|
|
|
|
// Default location: Phoenix, AZ (fallback if all else fails)
|
|
const DEFAULT_LOCATION = {
|
|
lat: 33.4484,
|
|
lng: -112.0740,
|
|
city: 'Phoenix',
|
|
state: 'AZ'
|
|
};
|
|
|
|
const LOCATION_STORAGE_KEY = 'findagram_location';
|
|
const SESSION_ID_KEY = 'findagram_session_id';
|
|
const API_BASE_URL = process.env.REACT_APP_API_URL || '';
|
|
|
|
/**
|
|
* Get or create session ID
|
|
*/
|
|
function getSessionId() {
|
|
let sessionId = sessionStorage.getItem(SESSION_ID_KEY);
|
|
if (!sessionId) {
|
|
sessionId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
sessionStorage.setItem(SESSION_ID_KEY, sessionId);
|
|
}
|
|
return sessionId;
|
|
}
|
|
|
|
/**
|
|
* Get cached location from sessionStorage
|
|
*/
|
|
function getCachedLocation() {
|
|
try {
|
|
const cached = sessionStorage.getItem(LOCATION_STORAGE_KEY);
|
|
if (cached) {
|
|
return JSON.parse(cached);
|
|
}
|
|
} catch (err) {
|
|
console.error('Error reading cached location:', err);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Save location to sessionStorage
|
|
*/
|
|
function cacheLocation(location) {
|
|
try {
|
|
sessionStorage.setItem(LOCATION_STORAGE_KEY, JSON.stringify(location));
|
|
} catch (err) {
|
|
console.error('Error caching location:', err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Track visitor and get location from our backend API
|
|
* This logs the visit for analytics and returns location from IP
|
|
*/
|
|
async function trackVisitorAndGetLocation() {
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/api/v1/visitor/track`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
domain: 'findagram.co',
|
|
page_path: window.location.pathname,
|
|
session_id: getSessionId(),
|
|
referrer: document.referrer || null,
|
|
}),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.location) {
|
|
return {
|
|
lat: data.location.lat,
|
|
lng: data.location.lng,
|
|
city: data.location.city,
|
|
state: data.location.state,
|
|
stateCode: data.location.stateCode,
|
|
source: 'api'
|
|
};
|
|
}
|
|
} catch (err) {
|
|
console.error('Visitor tracking error:', err);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Custom hook for getting user's geolocation
|
|
*
|
|
* @param {Object} options
|
|
* @param {boolean} options.autoRequest - Whether to request location automatically on mount
|
|
* @param {boolean} options.useIPFallback - Whether to use IP geolocation as fallback (default: true)
|
|
* @param {Object} options.defaultLocation - Default location if all methods fail
|
|
* @returns {Object} { location, loading, error, requestLocation, hasPermission }
|
|
*/
|
|
export function useGeolocation(options = {}) {
|
|
const {
|
|
autoRequest = false,
|
|
useIPFallback = true,
|
|
defaultLocation = DEFAULT_LOCATION
|
|
} = options;
|
|
|
|
const [location, setLocation] = useState(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState(null);
|
|
const [hasPermission, setHasPermission] = useState(null);
|
|
const [locationSource, setLocationSource] = useState(null); // 'gps', 'ip', or 'default'
|
|
|
|
// Try IP geolocation first (no permission needed)
|
|
const getIPLocation = useCallback(async () => {
|
|
if (!useIPFallback) return null;
|
|
|
|
const ipLoc = await getLocationFromIP();
|
|
if (ipLoc) {
|
|
setLocation(ipLoc);
|
|
setLocationSource('ip');
|
|
return ipLoc;
|
|
}
|
|
return null;
|
|
}, [useIPFallback]);
|
|
|
|
// Request precise GPS location (requires permission)
|
|
const requestLocation = useCallback(async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
// First try browser geolocation
|
|
if (navigator.geolocation) {
|
|
return new Promise(async (resolve) => {
|
|
navigator.geolocation.getCurrentPosition(
|
|
(position) => {
|
|
const { latitude, longitude } = position.coords;
|
|
const loc = { lat: latitude, lng: longitude, source: 'gps' };
|
|
setLocation(loc);
|
|
setLocationSource('gps');
|
|
setHasPermission(true);
|
|
setLoading(false);
|
|
resolve(loc);
|
|
},
|
|
async (err) => {
|
|
console.error('Geolocation error:', err);
|
|
|
|
if (err.code === err.PERMISSION_DENIED) {
|
|
setHasPermission(false);
|
|
}
|
|
|
|
// Fall back to IP geolocation
|
|
if (useIPFallback) {
|
|
const ipLoc = await getLocationFromIP();
|
|
if (ipLoc) {
|
|
setLocation(ipLoc);
|
|
setLocationSource('ip');
|
|
setLoading(false);
|
|
resolve(ipLoc);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Last resort: default location
|
|
setError('Unable to determine location');
|
|
setLocation(defaultLocation);
|
|
setLocationSource('default');
|
|
setLoading(false);
|
|
resolve(defaultLocation);
|
|
},
|
|
{
|
|
enableHighAccuracy: false,
|
|
timeout: 5000,
|
|
maximumAge: 600000 // Cache for 10 minutes
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
// No browser geolocation, try IP
|
|
if (useIPFallback) {
|
|
const ipLoc = await getLocationFromIP();
|
|
if (ipLoc) {
|
|
setLocation(ipLoc);
|
|
setLocationSource('ip');
|
|
setLoading(false);
|
|
return ipLoc;
|
|
}
|
|
}
|
|
|
|
// Fallback to default
|
|
setLocation(defaultLocation);
|
|
setLocationSource('default');
|
|
setLoading(false);
|
|
return defaultLocation;
|
|
}, [defaultLocation, useIPFallback]);
|
|
|
|
// Auto-request location on mount if enabled
|
|
useEffect(() => {
|
|
if (autoRequest) {
|
|
const init = async () => {
|
|
// Check for cached location first
|
|
const cached = getCachedLocation();
|
|
if (cached) {
|
|
setLocation(cached);
|
|
setLocationSource(cached.source || 'api');
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
|
|
// Track visitor and get location from our backend API
|
|
const apiLoc = await trackVisitorAndGetLocation();
|
|
if (apiLoc) {
|
|
setLocation(apiLoc);
|
|
setLocationSource('api');
|
|
cacheLocation(apiLoc); // Save for session
|
|
} else {
|
|
// Fallback to default
|
|
setLocation(defaultLocation);
|
|
setLocationSource('default');
|
|
}
|
|
|
|
setLoading(false);
|
|
};
|
|
|
|
init();
|
|
}
|
|
}, [autoRequest, defaultLocation]);
|
|
|
|
return {
|
|
location,
|
|
loading,
|
|
error,
|
|
requestLocation,
|
|
hasPermission,
|
|
locationSource,
|
|
isDefault: locationSource === 'default',
|
|
isFromIP: locationSource === 'ip',
|
|
isFromGPS: locationSource === 'gps'
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Calculate distance between two points using Haversine formula
|
|
*
|
|
* @param {number} lat1 - First point latitude
|
|
* @param {number} lng1 - First point longitude
|
|
* @param {number} lat2 - Second point latitude
|
|
* @param {number} lng2 - Second point longitude
|
|
* @returns {number} Distance in miles
|
|
*/
|
|
export function calculateDistance(lat1, lng1, lat2, lng2) {
|
|
const R = 3959; // Earth's radius in miles
|
|
const dLat = toRad(lat2 - lat1);
|
|
const dLng = toRad(lng2 - lng1);
|
|
const a =
|
|
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
|
|
Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
return R * c;
|
|
}
|
|
|
|
function toRad(deg) {
|
|
return deg * (Math.PI / 180);
|
|
}
|
|
|
|
/**
|
|
* Sort items by distance from a location
|
|
*
|
|
* @param {Array} items - Array of items with location data
|
|
* @param {Object} userLocation - User's location { lat, lng }
|
|
* @param {Function} getItemLocation - Function to extract lat/lng from item
|
|
* @returns {Array} Items sorted by distance with distance property added
|
|
*/
|
|
export function sortByDistance(items, userLocation, getItemLocation = (item) => item.location) {
|
|
if (!userLocation || !items?.length) return items;
|
|
|
|
return items
|
|
.map(item => {
|
|
const itemLoc = getItemLocation(item);
|
|
if (!itemLoc?.latitude || !itemLoc?.longitude) {
|
|
return { ...item, distance: null };
|
|
}
|
|
const distance = calculateDistance(
|
|
userLocation.lat,
|
|
userLocation.lng,
|
|
itemLoc.latitude,
|
|
itemLoc.longitude
|
|
);
|
|
return { ...item, distance: Math.round(distance * 10) / 10 };
|
|
})
|
|
.sort((a, b) => {
|
|
if (a.distance === null) return 1;
|
|
if (b.distance === null) return -1;
|
|
return a.distance - b.distance;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Filter items within a radius from user location
|
|
*
|
|
* @param {Array} items - Array of items with location data
|
|
* @param {Object} userLocation - User's location { lat, lng }
|
|
* @param {number} radiusMiles - Max distance in miles
|
|
* @param {Function} getItemLocation - Function to extract lat/lng from item
|
|
* @returns {Array} Items within radius, sorted by distance
|
|
*/
|
|
export function filterByRadius(items, userLocation, radiusMiles = 50, getItemLocation = (item) => item.location) {
|
|
const sorted = sortByDistance(items, userLocation, getItemLocation);
|
|
return sorted.filter(item => item.distance !== null && item.distance <= radiusMiles);
|
|
}
|
|
|
|
export default useGeolocation;
|