From e0e86d28308d477b3fd88a007a83fff8468682b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Guti=C3=A9rrez?= <35082514+alezmad@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:47:01 +0000 Subject: [PATCH] feat: Persist jobs to localStorage and reset search after launch - Reset search fields after job is successfully launched - Allow user to immediately start another scrape - Save active jobs to localStorage for persistence across refresh - Restore jobs from localStorage on page load - Resume polling for non-terminal jobs (pending/running) - Filter out jobs older than 24 hours - Add remove button (X) to each job card - Clean up localStorage when jobs are removed Co-Authored-By: Claude Opus 4.5 --- web/components/ScraperTest.tsx | 121 +++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/web/components/ScraperTest.tsx b/web/components/ScraperTest.tsx index 314d0d7..a7bc647 100644 --- a/web/components/ScraperTest.tsx +++ b/web/components/ScraperTest.tsx @@ -135,6 +135,114 @@ export default function ScraperTest({ onJobsChange, onSelectReviews }: ScraperTe const pollingIntervals = useRef>(new Map()); const abortControllerRef = useRef(null); + // localStorage key for persisting jobs + const JOBS_STORAGE_KEY = 'reviewiq_active_jobs'; + + // Reset search state after job launch + const resetSearchState = () => { + setBusinessNameQuery(''); + // Keep location as it's auto-detected + setSearchedQuery(''); + setHasReviews(null); + setAvailableReviewCount(null); + setBusinessName(null); + setBusinessAddress(null); + setBusinessRating(null); + setBusinessImage(null); + setBusinessCategory(null); + }; + + // Persist jobs to localStorage whenever they change + useEffect(() => { + if (jobs.size > 0) { + const jobsArray = Array.from(jobs.values()); + localStorage.setItem(JOBS_STORAGE_KEY, JSON.stringify(jobsArray)); + } + }, [jobs]); + + // Restore jobs from localStorage on mount + useEffect(() => { + const restoreJobs = async () => { + try { + const stored = localStorage.getItem(JOBS_STORAGE_KEY); + if (!stored) return; + + const storedJobs: JobStatus[] = JSON.parse(stored); + if (!Array.isArray(storedJobs) || storedJobs.length === 0) return; + + // Filter out jobs older than 24 hours + const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000; + const recentJobs = storedJobs.filter(job => { + const createdAt = new Date(job.created_at).getTime(); + return createdAt > oneDayAgo; + }); + + if (recentJobs.length === 0) { + localStorage.removeItem(JOBS_STORAGE_KEY); + return; + } + + // Fetch current status for each job + const jobsMap = new Map(); + for (const job of recentJobs) { + try { + const response = await fetch(`/api/jobs/${job.job_id}`); + if (response.ok) { + const freshJob = await response.json(); + jobsMap.set(job.job_id, freshJob); + + // Resume polling for non-terminal jobs + if (freshJob.status === 'pending' || freshJob.status === 'running') { + startPolling(job.job_id); + } + } + } catch (err) { + console.error('Failed to restore job', job.job_id, err); + } + } + + if (jobsMap.size > 0) { + setJobs(jobsMap); + } + } catch (err) { + console.error('Failed to restore jobs from localStorage', err); + } + }; + + restoreJobs(); + }, []); + + // Remove a job from the list and localStorage + const removeJob = (jobId: string) => { + // Stop polling if running + const interval = pollingIntervals.current.get(jobId); + if (interval) { + clearInterval(interval); + pollingIntervals.current.delete(jobId); + } + + // Remove from state + setJobs(prev => { + const newMap = new Map(prev); + newMap.delete(jobId); + + // Update localStorage + if (newMap.size === 0) { + localStorage.removeItem(JOBS_STORAGE_KEY); + } else { + localStorage.setItem(JOBS_STORAGE_KEY, JSON.stringify(Array.from(newMap.values()))); + } + + return newMap; + }); + + // Clear active job if it was removed + if (activeJobId === jobId) { + setActiveJobId(null); + setReviews([]); + } + }; + // Build full search query from business name + location const buildSearchQuery = () => { const name = businessNameQuery.trim(); @@ -401,6 +509,9 @@ export default function ScraperTest({ onJobsChange, onSelectReviews }: ScraperTe setActiveJobId(data.job_id); startPolling(data.job_id); + // Reset search state to allow starting another job + resetSearchState(); + } catch (err) { setError(err instanceof Error ? err.message : 'Failed to submit job'); } finally { @@ -870,6 +981,16 @@ export default function ScraperTest({ onJobsChange, onSelectReviews }: ScraperTe

{job.job_id}

{job.url}

+ {/* Remove job button */} + {/* Progress Bar for Running Jobs */}