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:
314
findagram/frontend/src/hooks/useGeolocation.js
Normal file
314
findagram/frontend/src/hooks/useGeolocation.js
Normal file
@@ -0,0 +1,314 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user