Files
cannaiq/findagram/frontend/src/hooks/useGeolocation.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

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;