live search 2/2
This commit is contained in:
parent
46e25ccd48
commit
46184a1281
@ -41,7 +41,17 @@ export const CompetitorsGridView: React.FC<CompetitorsGridViewProps> = ({ compet
|
||||
const [sortModel, setSortModel] = useState<GridSortModel>(() => paramsToSortModel(searchParams));
|
||||
|
||||
const [columnVisibilityModel, setColumnVisibilityModel] = useState<GridColumnVisibilityModel>(() => paramsToVisibilityModel(searchParams));
|
||||
const filteredCompetitors = competitors;
|
||||
// Deduplicate and filter rows that have no usable ID (livesearch streams may have placeId instead of place_id, or missing IDs)
|
||||
const filteredCompetitors = React.useMemo(() => {
|
||||
const seen = new Set<string>();
|
||||
return competitors.filter(c => {
|
||||
const id = c.place_id || (c as any).placeId || (c as any).id;
|
||||
if (!id) return false;
|
||||
if (seen.has(id)) return false;
|
||||
seen.add(id);
|
||||
return true;
|
||||
});
|
||||
}, [competitors]);
|
||||
// Column Widths state
|
||||
const [columnWidths, setColumnWidths] = useState<Record<string, number>>(() => {
|
||||
try {
|
||||
@ -220,7 +230,7 @@ export const CompetitorsGridView: React.FC<CompetitorsGridViewProps> = ({ compet
|
||||
<DataGrid
|
||||
rows={filteredCompetitors}
|
||||
columns={orderedColumns}
|
||||
getRowId={(row) => row.place_id}
|
||||
getRowId={(row) => row.place_id || row.placeId || row.id}
|
||||
loading={loading}
|
||||
filterModel={filterModel}
|
||||
onFilterModelChange={handleFilterModelChange}
|
||||
|
||||
@ -603,9 +603,9 @@ export const CompetitorsMapView: React.FC<CompetitorsMapViewProps> = ({ competit
|
||||
<SimulatorLayers
|
||||
map={map.current}
|
||||
isDarkStyle={mapStyleKey === 'dark'}
|
||||
simulatorData={simulatorData}
|
||||
simulatorPath={simulatorPath}
|
||||
simulatorScanner={simulatorScanner || liveScanner}
|
||||
simulatorData={liveAreas.length > 0 ? null : simulatorData}
|
||||
simulatorPath={liveAreas.length > 0 ? null : simulatorPath}
|
||||
simulatorScanner={liveScanner || simulatorScanner}
|
||||
/>
|
||||
<RegionLayers
|
||||
map={map.current}
|
||||
|
||||
@ -5,76 +5,61 @@ export function LiveSearchLayers({ map, liveAreas, liveRadii, liveNodes, isDarkS
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
|
||||
// Add source if not present
|
||||
if (!map.getSource('live-areas')) {
|
||||
map.addSource('live-areas', {
|
||||
type: 'geojson',
|
||||
data: { type: 'FeatureCollection', features: [] }
|
||||
});
|
||||
map.addLayer({
|
||||
id: 'live-areas-fill',
|
||||
type: 'fill',
|
||||
source: 'live-areas',
|
||||
paint: {
|
||||
'fill-color': '#eab308', // yellow-500
|
||||
'fill-opacity': 0.15
|
||||
}
|
||||
});
|
||||
map.addLayer({
|
||||
id: 'live-areas-line',
|
||||
type: 'line',
|
||||
source: 'live-areas',
|
||||
paint: {
|
||||
'line-color': '#eab308',
|
||||
'line-width': 2,
|
||||
'line-dasharray': [2, 2]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!map.getSource('live-radii')) {
|
||||
map.addSource('live-radii', {
|
||||
type: 'geojson',
|
||||
data: { type: 'FeatureCollection', features: [] }
|
||||
});
|
||||
map.addLayer({
|
||||
id: 'live-radii-fill',
|
||||
type: 'fill',
|
||||
source: 'live-radii',
|
||||
paint: {
|
||||
'fill-color': '#3b82f6', // blue-500
|
||||
'fill-opacity': 0.1
|
||||
}
|
||||
});
|
||||
map.addLayer({
|
||||
id: 'live-radii-line',
|
||||
type: 'line',
|
||||
source: 'live-radii',
|
||||
paint: {
|
||||
'line-color': '#3b82f6',
|
||||
'line-width': 2
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!map.getSource('live-nodes')) {
|
||||
map.addSource('live-nodes', {
|
||||
type: 'geojson',
|
||||
data: { type: 'FeatureCollection', features: [] }
|
||||
});
|
||||
map.addLayer({
|
||||
id: 'live-nodes-circle',
|
||||
type: 'circle',
|
||||
source: 'live-nodes',
|
||||
paint: {
|
||||
'circle-radius': 4,
|
||||
'circle-color': '#f43f5e', // rose-500
|
||||
'circle-stroke-width': 1,
|
||||
'circle-stroke-color': '#ffffff'
|
||||
}
|
||||
});
|
||||
const initSources = () => {
|
||||
if (!map.getSource('live-areas')) {
|
||||
map.addSource('live-areas', {
|
||||
type: 'geojson',
|
||||
data: { type: 'FeatureCollection', features: [] }
|
||||
});
|
||||
map.addLayer({
|
||||
id: 'live-areas-fill',
|
||||
type: 'fill',
|
||||
source: 'live-areas',
|
||||
paint: {
|
||||
'fill-color': '#eab308',
|
||||
'fill-opacity': 0.15
|
||||
}
|
||||
});
|
||||
map.addLayer({
|
||||
id: 'live-areas-line',
|
||||
type: 'line',
|
||||
source: 'live-areas',
|
||||
paint: {
|
||||
'line-color': '#eab308',
|
||||
'line-width': 2,
|
||||
'line-dasharray': [2, 2]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!map.getSource('live-nodes')) {
|
||||
map.addSource('live-nodes', {
|
||||
type: 'geojson',
|
||||
data: { type: 'FeatureCollection', features: [] }
|
||||
});
|
||||
map.addLayer({
|
||||
id: 'live-nodes-circle',
|
||||
type: 'circle',
|
||||
source: 'live-nodes',
|
||||
paint: {
|
||||
'circle-radius': 4,
|
||||
'circle-color': '#f43f5e',
|
||||
'circle-stroke-width': 1,
|
||||
'circle-stroke-color': '#ffffff'
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (map.isStyleLoaded()) {
|
||||
initSources();
|
||||
} else {
|
||||
map.once('style.load', initSources);
|
||||
}
|
||||
|
||||
return () => {
|
||||
map.off('style.load', initSources);
|
||||
};
|
||||
}, [map]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -99,62 +84,32 @@ export function LiveSearchLayers({ map, liveAreas, liveRadii, liveNodes, isDarkS
|
||||
});
|
||||
}, [map, liveAreas]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!map || !map.getSource('live-radii')) return;
|
||||
|
||||
// Radii usually come as center + radius
|
||||
// Let's create a rough circle polygon using basic math if there is no boundary
|
||||
const features = liveRadii.map(r => {
|
||||
const center = r.location?.location || r.center || r;
|
||||
const radiusInKm = r.radius || r.radius_km;
|
||||
if (center && radiusInKm) {
|
||||
const lng = center.lng || center.lon;
|
||||
if (lng === undefined || center.lat === undefined) return null;
|
||||
|
||||
// Return a circle polygon
|
||||
const points = 64;
|
||||
const coords = [];
|
||||
const distanceX = radiusInKm / (111.320 * Math.cos((center.lat * Math.PI) / 180));
|
||||
const distanceY = radiusInKm / 110.574;
|
||||
|
||||
for (let i = 0; i < points; i++) {
|
||||
const theta = (i / points) * (2 * Math.PI);
|
||||
const x = distanceX * Math.cos(theta);
|
||||
const y = distanceY * Math.sin(theta);
|
||||
coords.push([lng + x, center.lat + y]);
|
||||
}
|
||||
coords.push(coords[0]); // close polygon
|
||||
|
||||
return {
|
||||
type: 'Feature',
|
||||
properties: { name: r.name || 'Radius' },
|
||||
geometry: {
|
||||
type: 'Polygon',
|
||||
coordinates: [coords]
|
||||
}
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}).filter(Boolean);
|
||||
|
||||
(map.getSource('live-radii') as maplibregl.GeoJSONSource).setData({
|
||||
type: 'FeatureCollection',
|
||||
features: features as any
|
||||
});
|
||||
}, [map, liveRadii]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!map || !map.getSource('live-nodes')) return;
|
||||
|
||||
const features = liveNodes.map(n => {
|
||||
const loc = n.location || n;
|
||||
if (loc && typeof loc.lat === 'number' && typeof loc.lng === 'number') {
|
||||
// Resolve coordinates from multiple possible formats
|
||||
let lat: number | undefined;
|
||||
let lng: number | undefined;
|
||||
|
||||
if (n.gps_coordinates) {
|
||||
lat = n.gps_coordinates.latitude;
|
||||
lng = n.gps_coordinates.longitude;
|
||||
} else if (n.location && typeof n.location.lat === 'number') {
|
||||
lat = n.location.lat;
|
||||
lng = n.location.lng || n.location.lon;
|
||||
} else if (typeof n.lat === 'number') {
|
||||
lat = n.lat;
|
||||
lng = n.lng || n.lon;
|
||||
}
|
||||
|
||||
if (lat !== undefined && lng !== undefined && isFinite(lat) && isFinite(lng)) {
|
||||
return {
|
||||
type: 'Feature',
|
||||
properties: { name: n.name || 'Node' },
|
||||
properties: { name: n.title || n.name || 'Node', type: n.type },
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [loc.lng, loc.lat]
|
||||
coordinates: [lng, lat]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -69,6 +69,16 @@ export function GridSearchResults({ jobId, competitors, excludedTypes, updateExc
|
||||
<div className="flex items-center gap-3 p-1 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm bg-white dark:bg-gray-800">
|
||||
<span className="text-xs text-gray-500 px-2">
|
||||
{competitors.length} results
|
||||
{(() => {
|
||||
const emailCount = competitors.filter(c => {
|
||||
const e = c as any;
|
||||
const emails = e.emails || e.email;
|
||||
return emails && (Array.isArray(emails) ? emails.length > 0 : !!emails);
|
||||
}).length;
|
||||
return emailCount > 0 ? (
|
||||
<span className="ml-2 text-emerald-600 dark:text-emerald-400">📧 {emailCount}</span>
|
||||
) : null;
|
||||
})()}
|
||||
</span>
|
||||
<div className="h-4 w-px bg-gray-200 dark:bg-gray-700" />
|
||||
<div className="flex">
|
||||
|
||||
@ -2,6 +2,8 @@ import React, { createContext, useContext, useState, useEffect, ReactNode } from
|
||||
import { getPlacesGridSearchStreamUrl } from '../client-gridsearch';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
|
||||
export type StreamPhase = 'idle' | 'grid' | 'searching' | 'enriching' | 'complete' | 'failed';
|
||||
|
||||
export interface GridSearchStreamState {
|
||||
streaming: boolean;
|
||||
connected: boolean;
|
||||
@ -12,11 +14,15 @@ export interface GridSearchStreamState {
|
||||
liveNodes: any[];
|
||||
stats: {
|
||||
completedWaypoints: number;
|
||||
totalWaypoints: number;
|
||||
totalResults: number;
|
||||
apiCalls: number;
|
||||
totalLocationsEnriched: number;
|
||||
errors: number;
|
||||
phase: StreamPhase;
|
||||
};
|
||||
liveScanner: any;
|
||||
jobResult: any;
|
||||
}
|
||||
|
||||
const initialState: GridSearchStreamState = {
|
||||
@ -29,26 +35,65 @@ const initialState: GridSearchStreamState = {
|
||||
liveNodes: [],
|
||||
stats: {
|
||||
completedWaypoints: 0,
|
||||
totalWaypoints: 0,
|
||||
totalResults: 0,
|
||||
apiCalls: 0,
|
||||
totalLocationsEnriched: 0,
|
||||
errors: 0,
|
||||
phase: 'idle',
|
||||
},
|
||||
liveScanner: null
|
||||
liveScanner: null,
|
||||
jobResult: null,
|
||||
};
|
||||
|
||||
// ── pure helpers ──
|
||||
|
||||
function buildScannerGeoJSON(data: any): any | null {
|
||||
const loc = data.location?.location || data.center || data;
|
||||
const lng = loc.lng || loc.lon;
|
||||
if (!loc.lat || !lng) return null;
|
||||
function extractCoords(data: any): { lat: number; lng: number } | null {
|
||||
// waypoint-start: data.location.location or data.center
|
||||
const loc = data.location?.location || data.center;
|
||||
if (loc) {
|
||||
const lng = loc.lng || loc.lon;
|
||||
if (loc.lat && lng) return { lat: loc.lat, lng };
|
||||
}
|
||||
// node event: gps_coordinates from places table
|
||||
if (data.gps_coordinates) {
|
||||
return { lat: data.gps_coordinates.latitude, lng: data.gps_coordinates.longitude };
|
||||
}
|
||||
// fallback
|
||||
if (typeof data.lat === 'number') {
|
||||
return { lat: data.lat, lng: data.lng || data.lon };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function bearingBetween(lat1: number, lng1: number, lat2: number, lng2: number): number {
|
||||
const toRad = (d: number) => (d * Math.PI) / 180;
|
||||
const toDeg = (r: number) => (r * 180) / Math.PI;
|
||||
const dLng = toRad(lng2 - lng1);
|
||||
const y = Math.sin(dLng) * Math.cos(toRad(lat2));
|
||||
const x = Math.cos(toRad(lat1)) * Math.sin(toRad(lat2)) -
|
||||
Math.sin(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.cos(dLng);
|
||||
return (toDeg(Math.atan2(y, x)) + 360) % 360;
|
||||
}
|
||||
|
||||
function buildScannerGeoJSON(
|
||||
lat: number, lng: number,
|
||||
prevLat?: number, prevLng?: number,
|
||||
mouthOpen = true
|
||||
): any {
|
||||
const bearing = (prevLat !== undefined && prevLng !== undefined)
|
||||
? bearingBetween(prevLat, prevLng, lat, lng)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
type: 'FeatureCollection',
|
||||
features: [{
|
||||
type: 'Feature',
|
||||
properties: { icon_state: 'pacman-open', bearing: 0, radius: data.radius_km || 0.5 },
|
||||
geometry: { type: 'Point', coordinates: [lng, loc.lat] }
|
||||
properties: {
|
||||
icon_state: mouthOpen ? 'pacman-open' : 'pacman-closed',
|
||||
bearing,
|
||||
},
|
||||
geometry: { type: 'Point', coordinates: [lng, lat] }
|
||||
}]
|
||||
};
|
||||
}
|
||||
@ -81,16 +126,46 @@ function applyEventToState(
|
||||
const stats = parsed.stats || data?.stats;
|
||||
if (stats) next.stats = { ...next.stats, ...stats };
|
||||
|
||||
console.log(`SSE ${type}`, parsed);
|
||||
switch (type) {
|
||||
case 'grid-ready':
|
||||
next.stats = { ...next.stats, totalWaypoints: data?.totalWaypoints || data?.waypoints?.length || 0, phase: 'searching' };
|
||||
next.statusMessage = `Grid ready — ${next.stats.totalWaypoints} waypoints`;
|
||||
break;
|
||||
|
||||
case 'area-start':
|
||||
next.liveAreas = [...next.liveAreas, data];
|
||||
next.statusMessage = `Searching area: ${data?.name || '...'}`;
|
||||
break;
|
||||
|
||||
case 'area-finish':
|
||||
next.statusMessage = `Finished area: ${data?.area_gid || '...'}`;
|
||||
break;
|
||||
|
||||
case 'radius-start':
|
||||
case 'waypoint-start':
|
||||
next.liveRadii = [...next.liveRadii, data];
|
||||
next.liveScanner = buildScannerGeoJSON(data) ?? next.liveScanner;
|
||||
case 'waypoint-start': {
|
||||
// No longer accumulate liveRadii (blue circles removed)
|
||||
const wCoords = extractCoords(data);
|
||||
if (wCoords) {
|
||||
const prevPos = (next as any)._lastScannerPos;
|
||||
const mouth = !((next as any)._pacmanOpen ?? true); // toggle
|
||||
next.liveScanner = buildScannerGeoJSON(
|
||||
wCoords.lat, wCoords.lng,
|
||||
prevPos?.lat, prevPos?.lng,
|
||||
mouth
|
||||
);
|
||||
(next as any)._lastScannerPos = wCoords;
|
||||
(next as any)._pacmanOpen = mouth;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'waypoint-finish':
|
||||
next.stats = {
|
||||
...next.stats,
|
||||
completedWaypoints: next.stats.completedWaypoints + 1,
|
||||
apiCalls: data?.apiCalls ?? next.stats.apiCalls,
|
||||
};
|
||||
break;
|
||||
|
||||
case 'node-start':
|
||||
@ -101,10 +176,62 @@ function applyEventToState(
|
||||
case 'node': {
|
||||
const comp = normalizeLocation(type, data);
|
||||
next.competitors = [...next.competitors, comp];
|
||||
// Also add to liveNodes for map pins — replace any node-start placeholder
|
||||
const pid = data?.place_id || data?.placeId;
|
||||
if (pid) {
|
||||
next.liveNodes = [
|
||||
...next.liveNodes.filter((n: any) => (n.place_id || n.placeId) !== pid),
|
||||
data
|
||||
];
|
||||
} else {
|
||||
next.liveNodes = [...next.liveNodes, data];
|
||||
}
|
||||
// Move pacman to the found location
|
||||
const nCoords = extractCoords(data);
|
||||
if (nCoords) {
|
||||
const prevPos = (next as any)._lastScannerPos;
|
||||
const mouth = !((next as any)._pacmanOpen ?? true);
|
||||
next.liveScanner = buildScannerGeoJSON(
|
||||
nCoords.lat, nCoords.lng,
|
||||
prevPos?.lat, prevPos?.lng,
|
||||
mouth
|
||||
);
|
||||
(next as any)._lastScannerPos = nCoords;
|
||||
(next as any)._pacmanOpen = mouth;
|
||||
}
|
||||
next.stats = { ...next.stats, totalResults: next.competitors.length };
|
||||
if (type === 'node') next.stats = { ...next.stats, totalLocationsEnriched: next.stats.totalLocationsEnriched + 1 };
|
||||
next.statusMessage = `Found: ${comp?.title || comp?.name || 'Location'}`;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'enrich-start':
|
||||
next.stats = { ...next.stats, phase: 'enriching' };
|
||||
next.statusMessage = `Enriching ${data?.locationCount || ''} locations...`;
|
||||
break;
|
||||
|
||||
case 'node-error':
|
||||
case 'nodePage':
|
||||
next.stats = { ...next.stats, errors: next.stats.errors + 1 };
|
||||
break;
|
||||
|
||||
case 'job_result':
|
||||
next.jobResult = data;
|
||||
next.stats = {
|
||||
...next.stats,
|
||||
phase: 'complete',
|
||||
totalResults: data?.totalResults ?? next.stats.totalResults,
|
||||
apiCalls: data?.freshApiCalls ?? next.stats.apiCalls,
|
||||
};
|
||||
next.statusMessage = 'Search Complete';
|
||||
break;
|
||||
|
||||
case 'failed':
|
||||
next.streaming = false;
|
||||
next.stats = { ...next.stats, phase: 'failed' };
|
||||
next.statusMessage = data?.error || 'Job failed';
|
||||
break;
|
||||
|
||||
case 'log':
|
||||
break; // cpp logging forward – ignored
|
||||
}
|
||||
@ -117,7 +244,8 @@ function applyEventToState(
|
||||
const STREAM_EVENTS = [
|
||||
'progress', 'area-start', 'area-finish', 'waypoint-start',
|
||||
'waypoint-finish', 'radius-start', 'node-start', 'location',
|
||||
'node', 'log', 'job_result', 'stats'
|
||||
'node', 'node-error', 'nodePage', 'log', 'job_result', 'stats',
|
||||
'grid-ready', 'enrich-start', 'failed'
|
||||
] as const;
|
||||
|
||||
const GridSearchStreamContext = createContext<GridSearchStreamState>(initialState);
|
||||
@ -150,7 +278,6 @@ export function GridSearchStreamProvider({
|
||||
if (!isActive) return;
|
||||
try {
|
||||
const { type, parsed, data } = parseStreamEvent(eventType, e.data);
|
||||
console.log('sse - uds stream', parsed);
|
||||
setState(prev => applyEventToState(prev, type, parsed, data));
|
||||
} catch (err) {
|
||||
console.error('Failed parsing stream event', eventType, err);
|
||||
@ -173,7 +300,16 @@ export function GridSearchStreamProvider({
|
||||
|
||||
es.addEventListener('complete', () => {
|
||||
if (!isActive) return;
|
||||
setState(prev => ({ ...prev, streaming: false, connected: false, statusMessage: 'Search Complete' }));
|
||||
setState(prev => ({ ...prev, streaming: false, connected: false, statusMessage: 'Search Complete', stats: { ...prev.stats, phase: 'complete' } }));
|
||||
es?.close();
|
||||
});
|
||||
|
||||
es.addEventListener('failed', (e: MessageEvent) => {
|
||||
if (!isActive) return;
|
||||
try {
|
||||
const parsed = JSON.parse(e.data);
|
||||
setState(prev => ({ ...prev, streaming: false, connected: false, statusMessage: parsed?.error || 'Job failed', stats: { ...prev.stats, phase: 'failed' } }));
|
||||
} catch { /* ignore */ }
|
||||
es?.close();
|
||||
});
|
||||
|
||||
|
||||
@ -40,6 +40,7 @@ export function JobViewer({ jobId }: { jobId: string }) {
|
||||
const searchSettings = restoredState?.run?.request?.guided?.settings || null;
|
||||
|
||||
const [retrying, setRetrying] = useState(false);
|
||||
const [reloadKey, setReloadKey] = useState(0);
|
||||
|
||||
// For CompetitorsGridView props
|
||||
const [excludedTypes, setExcludedTypes] = useState<string[]>([]);
|
||||
@ -54,16 +55,13 @@ export function JobViewer({ jobId }: { jobId: string }) {
|
||||
const data = await fetchPlacesGridSearchById(jobId);
|
||||
if (!active) return;
|
||||
|
||||
// Handle nested wrapper if present (sometimes { data: { ... } } or just the object)
|
||||
// Backend always returns { request, result, areas, status }
|
||||
const payload = data?.data ? data.data : data;
|
||||
setJobData(payload);
|
||||
|
||||
if (payload && payload.result && Array.isArray(payload.result.enrichResults)) {
|
||||
setCompetitors(payload.result.enrichResults);
|
||||
} else if (Array.isArray(payload)) {
|
||||
setCompetitors(payload);
|
||||
} else if (payload && Array.isArray(payload.enrichResults)) {
|
||||
setCompetitors(payload.enrichResults);
|
||||
const enrichResults = payload?.result?.enrichResults;
|
||||
if (Array.isArray(enrichResults)) {
|
||||
setCompetitors(enrichResults);
|
||||
} else {
|
||||
setCompetitors([]);
|
||||
}
|
||||
@ -78,7 +76,7 @@ export function JobViewer({ jobId }: { jobId: string }) {
|
||||
load();
|
||||
|
||||
return () => { active = false; };
|
||||
}, [jobId]);
|
||||
}, [jobId, reloadKey]);
|
||||
|
||||
const dummyUpdateExcluded = async (types: string[]) => setExcludedTypes(types);
|
||||
|
||||
@ -86,7 +84,8 @@ export function JobViewer({ jobId }: { jobId: string }) {
|
||||
try {
|
||||
setRetrying(true);
|
||||
await retryPlacesGridSearchJob(jobId);
|
||||
window.location.reload();
|
||||
// Bust the dedup cache and re-trigger the useEffect instead of hard-reloading
|
||||
setReloadKey(k => k + 1);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
setError(e.message || 'Failed to retry job');
|
||||
@ -124,8 +123,8 @@ export function JobViewer({ jobId }: { jobId: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
if (jobData?.status === 'failed' || jobData?.run?.status === 'failed') {
|
||||
const errorMsg = jobData.result?.error || jobData?.run?.result?.error || 'Unknown error during job execution. The backend worker may have crashed or timed out.';
|
||||
if (jobData?.status === 'failed') {
|
||||
const errorMsg = jobData.result?.error || 'Unknown error during job execution. The backend worker may have crashed or timed out.';
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center p-6 flex-1 w-full box-border max-w-4xl mx-auto min-h-0">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-xl w-full p-10 text-center space-y-6">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user