From 6e2c58329adc507e3cf5809d3930ce7f791fcf60 Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Thu, 11 Sep 2025 13:58:25 +0530 Subject: [PATCH 01/45] feat: add cache logic --- src/context/globalInfo.tsx | 93 +++++++++++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 6 deletions(-) diff --git a/src/context/globalInfo.tsx b/src/context/globalInfo.tsx index 6f3cf8cd6..6919ef0c5 100644 --- a/src/context/globalInfo.tsx +++ b/src/context/globalInfo.tsx @@ -1,6 +1,24 @@ import React, { createContext, useContext, useState } from "react"; import { AlertSnackbarProps } from "../components/ui/AlertSnackbar"; import { WhereWhatPair } from "maxun-core"; +import { QueryClient, QueryClientProvider, useQuery, useQueryClient } from '@tanstack/react-query'; +import { getStoredRuns, getStoredRecordings } from "../api/storage"; + +const createDataCacheClient = () => new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30 * 1000, + gcTime: 5 * 60 * 1000, + retry: 2, + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), + } + } +}); + +const dataCacheKeys = { + runs: ['cached-runs'] as const, + recordings: ['cached-recordings'] as const, +} as const; interface RobotMeta { name: string; @@ -164,6 +182,65 @@ const globalInfoContext = createContext(globalInfoStore as GlobalInf export const useGlobalInfoStore = () => useContext(globalInfoContext); +export const useCachedRuns = () => { + return useQuery({ + queryKey: dataCacheKeys.runs, + queryFn: async () => { + const runs = await getStoredRuns(); + if (!runs) throw new Error('Failed to fetch runs data'); + return runs.map((run: any, index: number) => ({ id: index, ...run })); + }, + staleTime: 30 * 1000, + gcTime: 5 * 60 * 1000, + retry: 2, + }); +}; + +export const useCacheInvalidation = () => { + const queryClient = useQueryClient(); + + const invalidateRuns = () => { + queryClient.invalidateQueries({ queryKey: dataCacheKeys.runs }); + }; + + const invalidateRecordings = () => { + queryClient.invalidateQueries({ queryKey: dataCacheKeys.recordings }); + }; + + const addOptimisticRun = (newRun: any) => { + queryClient.setQueryData(dataCacheKeys.runs, (oldData: any) => { + if (!oldData) return [{ id: 0, ...newRun }]; + return [{ id: oldData.length, ...newRun }, ...oldData]; + }); + }; + + const invalidateAllCache = () => { + invalidateRuns(); + invalidateRecordings(); + }; + + return { + invalidateRuns, + invalidateRecordings, + addOptimisticRun, + invalidateAllCache + }; +}; + +export const useCachedRecordings = () => { + return useQuery({ + queryKey: dataCacheKeys.recordings, + queryFn: async () => { + const recordings = await getStoredRecordings(); + if (!recordings) throw new Error('Failed to fetch recordings data'); + return recordings; + }, + staleTime: 30 * 1000, + gcTime: 5 * 60 * 1000, + retry: 2, + }); +}; + export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => { const [browserId, setBrowserId] = useState(globalInfoStore.browserId); const [lastAction, setLastAction] = useState(globalInfoStore.lastAction); @@ -221,9 +298,12 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => { } } + const [dataCacheClient] = useState(() => createDataCacheClient()); + return ( - + { currentSnapshot, setCurrentSnapshot, updateDOMMode, - }} - > - {children} - + }} + > + {children} + + ); }; From dad2fd0442af7225abebe4b62670ce60604028ad Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Sat, 13 Sep 2025 18:35:47 +0530 Subject: [PATCH 02/45] fix: limit instruction highlighting --- src/components/recorder/RightSidePanel.tsx | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/components/recorder/RightSidePanel.tsx b/src/components/recorder/RightSidePanel.tsx index d4ffbb708..d504c537b 100644 --- a/src/components/recorder/RightSidePanel.tsx +++ b/src/components/recorder/RightSidePanel.tsx @@ -940,17 +940,15 @@ export const RightSidePanel: React.FC = ({ onFinishCapture {showLimitOptions && ( - - - {t('right_panel.limit.title')} - - + + {t('right_panel.limit.title')} + updateLimitType(e.target.value as LimitType)} @@ -1191,4 +1189,4 @@ export const RightSidePanel: React.FC = ({ onFinishCapture ); -}; \ No newline at end of file +}; From 6d6ef68a03efba6fb82c75252cf5b59d7d4ad59a Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Tue, 16 Sep 2025 11:19:05 +0530 Subject: [PATCH 03/45] fix: limit instruct highlighting --- src/components/recorder/RightSidePanel.tsx | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/components/recorder/RightSidePanel.tsx b/src/components/recorder/RightSidePanel.tsx index d4ffbb708..d19c54c7a 100644 --- a/src/components/recorder/RightSidePanel.tsx +++ b/src/components/recorder/RightSidePanel.tsx @@ -940,17 +940,15 @@ export const RightSidePanel: React.FC = ({ onFinishCapture {showLimitOptions && ( - - - {t('right_panel.limit.title')} - - + + {t('right_panel.limit.title')} + updateLimitType(e.target.value as LimitType)} From ad2830a9b62bdc0675f8684d8a60c0f0a9cacd3e Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Tue, 16 Sep 2025 22:48:15 +0530 Subject: [PATCH 04/45] fix: disable inline images --- server/src/browser-management/classes/RemoteBrowser.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts index 7e5839c85..a3796cbeb 100644 --- a/server/src/browser-management/classes/RemoteBrowser.ts +++ b/server/src/browser-management/classes/RemoteBrowser.ts @@ -380,6 +380,7 @@ export class RemoteBrowser { ); await this.currentPage.mouse.wheel(data.deltaX, data.deltaY); + await this.currentPage.waitForLoadState("networkidle", { timeout: 5000 }); const scrollInfo = await this.currentPage.evaluate(() => ({ x: window.scrollX, @@ -1590,7 +1591,7 @@ export class RemoteBrowser { } return window.rrwebSnapshot.snapshot(document, { - inlineImages: true, + inlineImages: false, collectFonts: true, }); }); From 2a1b1783d511fc2f5f3c1c968ee446c491a527d1 Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Tue, 16 Sep 2025 23:00:20 +0530 Subject: [PATCH 05/45] fix: container parent filtering --- src/helpers/clientSelectorGenerator.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/helpers/clientSelectorGenerator.ts b/src/helpers/clientSelectorGenerator.ts index 184a199b8..d1e0314e9 100644 --- a/src/helpers/clientSelectorGenerator.ts +++ b/src/helpers/clientSelectorGenerator.ts @@ -957,19 +957,10 @@ class ClientSelectorGenerator { ); if (groupedElementsAtPoint.length > 0) { - const hasAnchorTag = groupedElementsAtPoint.some( - (el) => el.tagName === "A" + let filteredElements = this.filterParentChildGroupedElements( + groupedElementsAtPoint ); - let filteredElements = groupedElementsAtPoint; - - if (hasAnchorTag) { - // Apply parent-child filtering when anchor tags are present - filteredElements = this.filterParentChildGroupedElements( - groupedElementsAtPoint - ); - } - // Sort by DOM depth (deeper elements first for more specificity) filteredElements.sort((a, b) => { const aDepth = this.getElementDepth(a); From 0e32ff0dd9b9eddcac90c842eb7350bb9019e19e Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 19 Sep 2025 00:39:52 +0530 Subject: [PATCH 06/45] feat: use onCancel --- src/components/robot/pages/RobotIntegrationPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/robot/pages/RobotIntegrationPage.tsx b/src/components/robot/pages/RobotIntegrationPage.tsx index 9b941a3d4..e3d07ec0a 100644 --- a/src/components/robot/pages/RobotIntegrationPage.tsx +++ b/src/components/robot/pages/RobotIntegrationPage.tsx @@ -694,7 +694,7 @@ export const RobotIntegrationPage = ({ return ( Date: Fri, 19 Sep 2025 00:40:36 +0530 Subject: [PATCH 07/45] fix: !use onArrowBack --- src/components/robot/pages/RobotIntegrationPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/robot/pages/RobotIntegrationPage.tsx b/src/components/robot/pages/RobotIntegrationPage.tsx index e3d07ec0a..3c8425901 100644 --- a/src/components/robot/pages/RobotIntegrationPage.tsx +++ b/src/components/robot/pages/RobotIntegrationPage.tsx @@ -698,7 +698,7 @@ export const RobotIntegrationPage = ({ cancelButtonText={t("buttons.cancel")} showSaveButton={false} // onBackToSelection={handleBack} - onArrowBack={handleBack} + // onArrowBack={handleBack} showCancelButton={false} backToSelectionText={t("buttons.back_arrow")} > From 1bbd2b7816cef1f23ab029b215419d4f3dee3da1 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 19 Sep 2025 00:51:31 +0530 Subject: [PATCH 08/45] fix: redirect to /integrate/googleSheets on auth --- src/components/robot/Recordings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/robot/Recordings.tsx b/src/components/robot/Recordings.tsx index 6f463ba46..44dc4df6a 100644 --- a/src/components/robot/Recordings.tsx +++ b/src/components/robot/Recordings.tsx @@ -79,7 +79,7 @@ export const Recordings = ({ } else if (authStatus === "success" && robotId) { console.log("Google Auth Status:", authStatus); notify(authStatus, t("recordingtable.notifications.auth_success")); - handleNavigate(`/robots/${robotId}/integrate/google`, robotId, "", []); + handleNavigate(`/robots/${robotId}/integrate/googleSheets`, robotId, "", []); } }, []); From 76ce58fc406c08f1f2cf54044002dedf40573b60 Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Fri, 19 Sep 2025 01:01:24 +0530 Subject: [PATCH 09/45] fix: redirect to /robots on tab click --- src/components/dashboard/MainMenu.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/dashboard/MainMenu.tsx b/src/components/dashboard/MainMenu.tsx index 9df2a6d29..8f0fba782 100644 --- a/src/components/dashboard/MainMenu.tsx +++ b/src/components/dashboard/MainMenu.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import Tabs from '@mui/material/Tabs'; import Tab from '@mui/material/Tab'; import Box from '@mui/material/Box'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useLocation } from 'react-router-dom'; import { Paper, Button, useTheme, Modal, Typography, Stack, TextField, InputAdornment, IconButton } from "@mui/material"; import { AutoAwesome, FormatListBulleted, VpnKey, Usb, CloudQueue, Description, Favorite, ContentCopy, SlowMotionVideo } from "@mui/icons-material"; import { useTranslation } from 'react-i18next'; @@ -17,6 +17,7 @@ export const MainMenu = ({ value = 'robots', handleChangeContent }: MainMenuProp const theme = useTheme(); const { t } = useTranslation(); const navigate = useNavigate(); + const location = useLocation(); const { notify } = useGlobalInfoStore(); const [cloudModalOpen, setCloudModalOpen] = useState(false); @@ -29,6 +30,13 @@ export const MainMenu = ({ value = 'robots', handleChangeContent }: MainMenuProp handleChangeContent(newValue); }; + const handleRobotsClick = () => { + if (location.pathname !== '/robots') { + navigate('/robots'); + handleChangeContent('robots'); + } + }; + const copyDiscountCode = () => { navigator.clipboard.writeText(ossDiscountCode).then(() => { notify("success", "Discount code copied to clipboard!"); @@ -81,7 +89,7 @@ export const MainMenu = ({ value = 'robots', handleChangeContent }: MainMenuProp orientation="vertical" sx={{ alignItems: 'flex-start' }} > - } iconPosition="start" sx={{ justifyContent: 'flex-start', textAlign: 'left', fontSize: 'medium' }} /> + } iconPosition="start" sx={{ justifyContent: 'flex-start', textAlign: 'left', fontSize: 'medium' }} onClick={handleRobotsClick} /> } iconPosition="start" sx={{ justifyContent: 'flex-start', textAlign: 'left', fontSize: 'medium' }} /> } iconPosition="start" sx={{ justifyContent: 'flex-start', textAlign: 'left', fontSize: 'medium' }} /> } iconPosition="start" sx={{ justifyContent: 'flex-start', textAlign: 'left', fontSize: 'medium' }} /> From f885269688efea5e82a6589bd36619ffaa84cf2d Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Fri, 19 Sep 2025 11:18:07 +0530 Subject: [PATCH 10/45] feat: persist recording id --- src/context/globalInfo.tsx | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/context/globalInfo.tsx b/src/context/globalInfo.tsx index 6919ef0c5..a9b20bfa7 100644 --- a/src/context/globalInfo.tsx +++ b/src/context/globalInfo.tsx @@ -249,7 +249,29 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => { const [rerenderRuns, setRerenderRuns] = useState(globalInfoStore.rerenderRuns); const [rerenderRobots, setRerenderRobots] = useState(globalInfoStore.rerenderRobots); const [recordingLength, setRecordingLength] = useState(globalInfoStore.recordingLength); - const [recordingId, setRecordingId] = useState(globalInfoStore.recordingId); + // const [recordingId, setRecordingId] = useState(globalInfoStore.recordingId); + const [recordingId, setRecordingId] = useState(() => { + try { + const stored = sessionStorage.getItem('recordingId'); + return stored ? JSON.parse(stored) : globalInfoStore.recordingId; + } catch { + return globalInfoStore.recordingId; + } + }); + + // Create a wrapped setter that persists to sessionStorage + const setPersistedRecordingId = (newRecordingId: string | null) => { + setRecordingId(newRecordingId); + try { + if (newRecordingId) { + sessionStorage.setItem('recordingId', JSON.stringify(newRecordingId)); + } else { + sessionStorage.removeItem('recordingId'); + } + } catch (error) { + console.warn('Failed to persist recordingId to sessionStorage:', error); + } + }; const [retrainRobotId, setRetrainRobotId] = useState(globalInfoStore.retrainRobotId); const [recordingName, setRecordingName] = useState(globalInfoStore.recordingName); const [isLogin, setIsLogin] = useState(globalInfoStore.isLogin); @@ -320,7 +342,7 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => { recordingLength, setRecordingLength, recordingId, - setRecordingId, + setRecordingId: setPersistedRecordingId, retrainRobotId, setRetrainRobotId, recordingName, From 883922606c8ce6aa0181de287d61c1ec85fca27e Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Fri, 19 Sep 2025 11:24:23 +0530 Subject: [PATCH 11/45] feat: cache robot and run fetch data --- src/components/robot/RecordingsTable.tsx | 85 +++++++++++------------- src/components/run/RunsTable.tsx | 58 ++++------------ 2 files changed, 51 insertions(+), 92 deletions(-) diff --git a/src/components/robot/RecordingsTable.tsx b/src/components/robot/RecordingsTable.tsx index 42c5971b7..188b3165c 100644 --- a/src/components/robot/RecordingsTable.tsx +++ b/src/components/robot/RecordingsTable.tsx @@ -36,8 +36,8 @@ import { MoreHoriz, Refresh } from "@mui/icons-material"; -import { useGlobalInfoStore } from "../../context/globalInfo"; -import { checkRunsForRecording, deleteRecordingFromStorage, getStoredRecordings } from "../../api/storage"; +import { useGlobalInfoStore, useCachedRecordings } from "../../context/globalInfo"; +import { checkRunsForRecording, deleteRecordingFromStorage } from "../../api/storage"; import { Add } from "@mui/icons-material"; import { useNavigate } from 'react-router-dom'; import { canCreateBrowserInState, getActiveBrowserId, stopRecording } from "../../api/recording"; @@ -150,12 +150,11 @@ export const RecordingsTable = ({ const { t } = useTranslation(); const [page, setPage] = React.useState(0); const [rowsPerPage, setRowsPerPage] = React.useState(10); - const [rows, setRows] = React.useState([]); + const { data: recordingsData = [], isLoading: isFetching, error, refetch } = useCachedRecordings(); const [isModalOpen, setModalOpen] = React.useState(false); const [searchTerm, setSearchTerm] = React.useState(''); const [isWarningModalOpen, setWarningModalOpen] = React.useState(false); const [activeBrowserId, setActiveBrowserId] = React.useState(''); - const [isFetching, setIsFetching] = React.useState(true); const columns = useMemo(() => [ { id: 'interpret', label: t('recordingtable.run'), minWidth: 80 }, @@ -238,44 +237,42 @@ export const RecordingsTable = ({ if (dateStr.includes('PM') || dateStr.includes('AM')) { return new Date(dateStr); } - + return new Date(dateStr.replace(/(\d+)\/(\d+)\//, '$2/$1/')) } catch { return new Date(0); } }; - const fetchRecordings = useCallback(async () => { - try { - const recordings = await getStoredRecordings(); - if (recordings) { - const parsedRows = recordings - .map((recording: any, index: number) => { - if (recording?.recording_meta) { - const parsedDate = parseDateString(recording.recording_meta.updatedAt); - - return { - id: index, - ...recording.recording_meta, - content: recording.recording, - parsedDate - }; - } - return null; - }) - .filter(Boolean) - .sort((a, b) => b.parsedDate.getTime() - a.parsedDate.getTime()); - - setRecordings(parsedRows.map((recording) => recording.name)); - setRows(parsedRows); - } - } catch (error) { - console.error('Error fetching recordings:', error); - notify('error', t('recordingtable.notifications.fetch_error')); - } finally { - setIsFetching(false); + const rows = useMemo(() => { + if (!recordingsData) return []; + + const parsedRows = recordingsData + .map((recording: any, index: number) => { + if (recording?.recording_meta) { + const parsedDate = parseDateString(recording.recording_meta.updatedAt); + + return { + id: index, + ...recording.recording_meta, + content: recording.recording, + parsedDate + }; + } + return null; + }) + .filter(Boolean) + .sort((a, b) => b.parsedDate.getTime() - a.parsedDate.getTime()); + + return parsedRows; + }, [recordingsData]); + + useEffect(() => { + if (rows.length > 0) { + setRecordings(rows.map((recording) => recording.name)); } - }, [setRecordings, notify, t]); + }, [rows, setRecordings]); + const handleNewRecording = useCallback(async () => { const canCreateRecording = await canCreateBrowserInState("recording"); @@ -331,7 +328,7 @@ export const RecordingsTable = ({ if (lastPair?.what) { if (Array.isArray(lastPair.what)) { - const gotoAction = lastPair.what.find(action => + const gotoAction = lastPair.what.find((action: any) => action && typeof action === 'object' && 'action' in action && action.action === "goto" ) as any; @@ -408,17 +405,12 @@ export const RecordingsTable = ({ window.sessionStorage.setItem('initialUrl', event.target.value); } - useEffect(() => { - fetchRecordings(); - }, [fetchRecordings]); - useEffect(() => { if (rerenderRobots) { - fetchRecordings().then(() => { - setRerenderRobots(false); - }); + refetch(); + setRerenderRobots(false); } - }, [rerenderRobots, fetchRecordings, setRerenderRobots]); + }, [rerenderRobots, setRerenderRobots, refetch]); function useDebounce(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = React.useState(value); @@ -468,12 +460,11 @@ export const RecordingsTable = ({ const success = await deleteRecordingFromStorage(id); if (success) { - setRows([]); notify('success', t('recordingtable.notifications.delete_success')); - fetchRecordings(); + refetch(); } } - }), [handleRunRecording, handleScheduleRecording, handleIntegrateRecording, handleSettingsRecording, handleEditRobot, handleDuplicateRobot, handleRetrainRobot, notify, t]); + }), [handleRunRecording, handleScheduleRecording, handleIntegrateRecording, handleSettingsRecording, handleEditRobot, handleDuplicateRobot, handleRetrainRobot, notify, t, refetch]); return ( diff --git a/src/components/run/RunsTable.tsx b/src/components/run/RunsTable.tsx index cfcb05d49..4bd3a40c7 100644 --- a/src/components/run/RunsTable.tsx +++ b/src/components/run/RunsTable.tsx @@ -13,8 +13,7 @@ import { Accordion, AccordionSummary, AccordionDetails, Typography, Box, TextFie import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import SearchIcon from '@mui/icons-material/Search'; import { useLocation, useNavigate } from 'react-router-dom'; -import { useGlobalInfoStore } from "../../context/globalInfo"; -import { getStoredRuns } from "../../api/storage"; +import { useGlobalInfoStore, useCachedRuns } from "../../context/globalInfo"; import { RunSettings } from "./RunSettings"; import { CollapsibleRow } from "./ColapsibleRow"; import { ArrowDownward, ArrowUpward, UnfoldMore } from '@mui/icons-material'; @@ -132,16 +131,14 @@ export const RunsTable: React.FC = ({ [t] ); - const [rows, setRows] = useState([]); - const [searchTerm, setSearchTerm] = useState(''); - const [isFetching, setIsFetching] = useState(true); + const { notify, rerenderRuns, setRerenderRuns } = useGlobalInfoStore(); + const { data: rows = [], isLoading: isFetching, error, refetch } = useCachedRuns(); + const [searchTerm, setSearchTerm] = useState(''); const [paginationStates, setPaginationStates] = useState({}); const [expandedRows, setExpandedRows] = useState>(new Set()); const [expandedAccordions, setExpandedAccordions] = useState>(new Set()); - const { notify, rerenderRuns, setRerenderRuns } = useGlobalInfoStore(); - const handleAccordionChange = useCallback((robotMetaId: string, isExpanded: boolean) => { setExpandedAccordions(prev => { const newSet = new Set(prev); @@ -278,47 +275,18 @@ export const RunsTable: React.FC = ({ debouncedSetSearch(event.target.value); }, [debouncedSearch]); - const fetchRuns = useCallback(async () => { - try { - const runs = await getStoredRuns(); - if (runs) { - const parsedRows: Data[] = runs.map((run: any, index: number) => ({ - id: index, - ...run, - })); - setRows(parsedRows); - } else { - notify('error', t('runstable.notifications.no_runs')); - } - } catch (error) { - notify('error', t('runstable.notifications.fetch_error')); - } finally { - setIsFetching(false); - } - }, [notify, t]); useEffect(() => { - let mounted = true; - - if (rows.length === 0 || rerenderRuns) { - setIsFetching(true); - fetchRuns().then(() => { - if (mounted) { - setRerenderRuns(false); - } - }); + if (rerenderRuns) { + refetch(); + setRerenderRuns(false); } - - return () => { - mounted = false; - }; - }, [rerenderRuns, rows.length, setRerenderRuns, fetchRuns]); + }, [rerenderRuns, setRerenderRuns, refetch]); const handleDelete = useCallback(() => { - setRows([]); notify('success', t('runstable.notifications.delete_success')); - fetchRuns(); - }, [notify, t, fetchRuns]); + refetch(); + }, [notify, t, refetch]); // Filter rows based on search term const filteredRows = useMemo(() => { @@ -350,15 +318,15 @@ export const RunsTable: React.FC = ({ }, {} as Record); Object.keys(groupedData).forEach(robotId => { - groupedData[robotId].sort((a, b) => + groupedData[robotId].sort((a: any, b: any) => parseDateString(b.startedAt).getTime() - parseDateString(a.startedAt).getTime() ); }); const robotEntries = Object.entries(groupedData).map(([robotId, runs]) => ({ robotId, - runs, - latestRunDate: parseDateString(runs[0].startedAt).getTime() + runs: runs as Data[], + latestRunDate: parseDateString((runs as Data[])[0].startedAt).getTime() })); robotEntries.sort((a, b) => b.latestRunDate - a.latestRunDate); From 6d25f265946e16a1c8e4242d66d4a607cde0ef5f Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Fri, 19 Sep 2025 11:25:31 +0530 Subject: [PATCH 12/45] feat: invalidate runs data --- src/pages/MainPage.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx index c71478b32..7f2b5b3b4 100644 --- a/src/pages/MainPage.tsx +++ b/src/pages/MainPage.tsx @@ -6,7 +6,7 @@ import { Recordings } from "../components/robot/Recordings"; import { Runs } from "../components/run/Runs"; import ProxyForm from '../components/proxy/ProxyForm'; import ApiKey from '../components/api/ApiKey'; -import { useGlobalInfoStore } from "../context/globalInfo"; +import { useGlobalInfoStore, useCacheInvalidation } from "../context/globalInfo"; import { createAndRunRecording, createRunForStoredRecording, CreateRunResponseWithQueue, interpretStoredRecording, notifyAboutAbort, scheduleStoredRecording } from "../api/storage"; import { io, Socket } from "socket.io-client"; import { stopRecording } from "../api/recording"; @@ -50,6 +50,7 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) let aborted = false; const { notify, setRerenderRuns, setRecordingId } = useGlobalInfoStore(); + const { invalidateRuns } = useCacheInvalidation(); const navigate = useNavigate(); const { state } = useContext(AuthContext); @@ -66,12 +67,14 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) if (!response.success) { notify('error', t('main_page.notifications.abort_failed', { name: robotName })); setRerenderRuns(true); + invalidateRuns(); return; } if (response.isQueued) { setRerenderRuns(true); - + invalidateRuns(); + notify('success', t('main_page.notifications.abort_success', { name: robotName })); setQueuedRuns(prev => { @@ -92,6 +95,7 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) if (abortData.runId === runId) { notify('success', t('main_page.notifications.abort_success', { name: abortData.robotName || robotName })); setRerenderRuns(true); + invalidateRuns(); abortSocket.disconnect(); } }); @@ -100,6 +104,7 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) console.log('Abort socket connection error:', error); notify('error', t('main_page.notifications.abort_failed', { name: robotName })); setRerenderRuns(true); + invalidateRuns(); abortSocket.disconnect(); }); }); @@ -125,8 +130,9 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) setRunningRecordingName(''); setCurrentInterpretationLog(''); setRerenderRuns(true); + invalidateRuns(); }) - }, [runningRecordingName, aborted, currentInterpretationLog, notify, setRerenderRuns]); + }, [runningRecordingName, aborted, currentInterpretationLog, notify, setRerenderRuns, invalidateRuns]); const debugMessageHandler = useCallback((msg: string) => { setCurrentInterpretationLog((prevState) => @@ -156,6 +162,7 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) setRunningRecordingName(''); setCurrentInterpretationLog(''); setRerenderRuns(true); + invalidateRuns(); const robotName = data.robotName; @@ -193,7 +200,7 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) socket.off('connect_error'); socket.off('disconnect'); } - }, [runningRecordingName, sockets, ids, debugMessageHandler, user?.id, t, notify, setRerenderRuns, setQueuedRuns, navigate, setContent, setIds]); + }, [runningRecordingName, sockets, ids, debugMessageHandler, user?.id, t, notify, setRerenderRuns, setQueuedRuns, navigate, setContent, setIds, invalidateRuns]); const handleScheduleRecording = async (settings: ScheduleSettings) => { const { message, runId }: ScheduleRunResponse = await scheduleStoredRecording(runningRecordingId, settings); @@ -209,6 +216,7 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) if (user?.id) { const handleRunCompleted = (completionData: any) => { setRerenderRuns(true); + invalidateRuns(); if (queuedRuns.has(completionData.runId)) { setQueuedRuns(prev => { @@ -233,7 +241,7 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) disconnectQueueSocket(); }; } - }, [user?.id, connectToQueueSocket, disconnectQueueSocket, t, setRerenderRuns, queuedRuns, setQueuedRuns]); + }, [user?.id, connectToQueueSocket, disconnectQueueSocket, t, setRerenderRuns, queuedRuns, setQueuedRuns, invalidateRuns]); const DisplayContent = () => { switch (content) { From 47eb3fc7b4f25cc19d8888ad62cb9c1b64825da4 Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Fri, 19 Sep 2025 11:26:04 +0530 Subject: [PATCH 13/45] chore: add tanstack react query --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 439370c42..d7daee756 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@mui/lab": "^5.0.0-alpha.80", "@mui/material": "^5.6.2", "@react-oauth/google": "^0.12.1", + "@tanstack/react-query": "^5.89.0", "@testing-library/react": "^13.1.1", "@testing-library/user-event": "^13.5.0", "@types/bcrypt": "^5.0.2", From 6c1d6612a418aa59d7d79bfeb0e4549c1b0e4215 Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Sun, 21 Sep 2025 12:45:27 +0530 Subject: [PATCH 14/45] feat: invalidate runs during robot run --- src/pages/MainPage.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx index 7f2b5b3b4..2067fc885 100644 --- a/src/pages/MainPage.tsx +++ b/src/pages/MainPage.tsx @@ -142,8 +142,9 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) const handleRunRecording = useCallback((settings: RunSettings) => { createAndRunRecording(runningRecordingId, settings).then((response: CreateRunResponseWithQueue) => { const { browserId, runId, robotMetaId, queued } = response; - + setIds({ browserId, runId, robotMetaId }); + invalidateRuns(); navigate(`/runs/${robotMetaId}/run/${runId}`); if (queued) { From a0e6fe8481b8528975c2ede144931f5b2757e9a2 Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Tue, 23 Sep 2025 20:21:24 +0530 Subject: [PATCH 15/45] fix: instant discard redirect --- src/components/browser/BrowserRecordingSave.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/browser/BrowserRecordingSave.tsx b/src/components/browser/BrowserRecordingSave.tsx index 2adfcd79d..32d0fabad 100644 --- a/src/components/browser/BrowserRecordingSave.tsx +++ b/src/components/browser/BrowserRecordingSave.tsx @@ -41,8 +41,6 @@ const BrowserRecordingSave = () => { const goToMainMenu = async () => { if (browserId) { - await stopRecording(browserId); - const notificationData = { type: 'warning', message: t('browser_recording.notifications.terminated'), @@ -65,6 +63,10 @@ const BrowserRecordingSave = () => { setBrowserId(null); window.close(); + + stopRecording(browserId).catch((error) => { + console.warn('Background cleanup failed:', error); + }); } }; @@ -242,4 +244,4 @@ const modalStyle = { height: 'fit-content', display: 'block', padding: '20px', -}; \ No newline at end of file +}; From e6d5a34ca28e12ea4b9c41d4811cd529223c8750 Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Wed, 24 Sep 2025 16:05:24 +0530 Subject: [PATCH 16/45] fix: prioritize dialog elem sorting --- src/helpers/clientSelectorGenerator.ts | 42 +++++++++++++++++++++----- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/src/helpers/clientSelectorGenerator.ts b/src/helpers/clientSelectorGenerator.ts index 184a199b8..0bf460abc 100644 --- a/src/helpers/clientSelectorGenerator.ts +++ b/src/helpers/clientSelectorGenerator.ts @@ -413,7 +413,7 @@ class ClientSelectorGenerator { // Only switch to dialog-focused analysis if dialogs have substantial content if (dialogContentElements.length > 5) { - allElements = dialogContentElements; + allElements = [...dialogContentElements, ...allElements]; } } } @@ -972,6 +972,13 @@ class ClientSelectorGenerator { // Sort by DOM depth (deeper elements first for more specificity) filteredElements.sort((a, b) => { + const aDialog = this.isDialogElement(a) ? 1 : 0; + const bDialog = this.isDialogElement(b) ? 1 : 0; + + if (aDialog !== bDialog) { + return bDialog - aDialog; + } + const aDepth = this.getElementDepth(a); const bDepth = this.getElementDepth(b); return bDepth - aDepth; @@ -3908,6 +3915,24 @@ class ClientSelectorGenerator { let elements = iframeDoc.elementsFromPoint(x, y) as HTMLElement[]; if (!elements.length) return null; + const dialogElement = this.findDialogElement(elements); + if (dialogElement) { + const dialogRect = dialogElement.getBoundingClientRect(); + const isClickInsideDialog = x >= dialogRect.left && x <= dialogRect.right && + y >= dialogRect.top && y <= dialogRect.bottom; + + if (isClickInsideDialog) { + const dialogElements = elements.filter( + (el) => el === dialogElement || dialogElement.contains(el) + ); + + const deepestInDialog = this.findDeepestInDialog(dialogElements, dialogElement); + if (deepestInDialog) { + return deepestInDialog; + } + } + } + const filteredElements = this.filterLogicalElements(elements, x, y); const targetElements = filteredElements.length > 0 ? filteredElements : elements; @@ -4111,6 +4136,13 @@ class ClientSelectorGenerator { return deepestElement; } + /** + * Check if an element is a dialog + */ + private isDialogElement(el: HTMLElement): boolean { + return !!el.closest('dialog, [role="dialog"]'); + } + /** * Find all dialog elements in the document */ @@ -4119,14 +4151,8 @@ class ClientSelectorGenerator { const allElements = Array.from(doc.querySelectorAll("*")) as HTMLElement[]; for (const element of allElements) { - if (element.getAttribute("role") === "dialog") { + if (this.isDialogElement(element)) { dialogElements.push(element); - continue; - } - - if (element.tagName.toLowerCase() === "dialog") { - dialogElements.push(element); - continue; } } From 903c318fd682e9942583ccf3c8d7e1f1a0aff97c Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Fri, 26 Sep 2025 16:43:19 +0530 Subject: [PATCH 17/45] feat: use h5 variant --- src/components/robot/pages/RobotConfigPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/robot/pages/RobotConfigPage.tsx b/src/components/robot/pages/RobotConfigPage.tsx index 902518eb0..9e70b43e4 100644 --- a/src/components/robot/pages/RobotConfigPage.tsx +++ b/src/components/robot/pages/RobotConfigPage.tsx @@ -107,7 +107,7 @@ export const RobotConfigPage: React.FC = ({ )} = ({ )} ); -}; \ No newline at end of file +}; From 24a78f868238823fefd3f790966725fffbcb8d35 Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Fri, 26 Sep 2025 16:43:38 +0530 Subject: [PATCH 18/45] fix: remove font weight --- src/components/robot/pages/RobotConfigPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/robot/pages/RobotConfigPage.tsx b/src/components/robot/pages/RobotConfigPage.tsx index 9e70b43e4..53ae90abe 100644 --- a/src/components/robot/pages/RobotConfigPage.tsx +++ b/src/components/robot/pages/RobotConfigPage.tsx @@ -109,7 +109,6 @@ export const RobotConfigPage: React.FC = ({ Date: Sun, 28 Sep 2025 16:11:48 +0530 Subject: [PATCH 19/45] fix: cors navigation iframe rendering --- .../recorder/DOMBrowserRenderer.tsx | 161 ++++++------------ 1 file changed, 48 insertions(+), 113 deletions(-) diff --git a/src/components/recorder/DOMBrowserRenderer.tsx b/src/components/recorder/DOMBrowserRenderer.tsx index 7e9098542..7fcafdeb4 100644 --- a/src/components/recorder/DOMBrowserRenderer.tsx +++ b/src/components/recorder/DOMBrowserRenderer.tsx @@ -167,7 +167,6 @@ export const DOMBrowserRenderer: React.FC = ({ const containerRef = useRef(null); const iframeRef = useRef(null); const [isRendered, setIsRendered] = useState(false); - const [renderError, setRenderError] = useState(null); const [lastMousePosition, setLastMousePosition] = useState({ x: 0, y: 0 }); const [currentHighlight, setCurrentHighlight] = useState<{ element: Element; @@ -342,7 +341,10 @@ export const DOMBrowserRenderer: React.FC = ({ const existingHandlers = (iframeDoc as any)._domRendererHandlers; if (existingHandlers) { Object.entries(existingHandlers).forEach(([event, handler]) => { - iframeDoc.removeEventListener(event, handler as EventListener, false); // Changed to false + const options: boolean | AddEventListenerOptions = ['wheel', 'touchstart', 'touchmove'].includes(event) + ? { passive: false } + : false; + iframeDoc.removeEventListener(event, handler as EventListener, options); }); } @@ -700,7 +702,11 @@ export const DOMBrowserRenderer: React.FC = ({ return; } - e.preventDefault(); + if (isInCaptureMode) { + e.preventDefault(); + e.stopPropagation(); + return; + } if (!isInCaptureMode) { const wheelEvent = e as WheelEvent; @@ -752,7 +758,10 @@ export const DOMBrowserRenderer: React.FC = ({ handlers.beforeunload = preventDefaults; Object.entries(handlers).forEach(([event, handler]) => { - iframeDoc.addEventListener(event, handler, false); + const options: boolean | AddEventListenerOptions = ['wheel', 'touchstart', 'touchmove'].includes(event) + ? { passive: false } + : false; + iframeDoc.addEventListener(event, handler, options); }); // Store handlers for cleanup @@ -795,11 +804,39 @@ export const DOMBrowserRenderer: React.FC = ({ } try { - setRenderError(null); setIsRendered(false); const iframe = iframeRef.current!; - const iframeDoc = iframe.contentDocument!; + let iframeDoc: Document; + + try { + iframeDoc = iframe.contentDocument!; + if (!iframeDoc) { + throw new Error("Cannot access iframe document"); + } + } catch (crossOriginError) { + console.warn("Cross-origin iframe access blocked, recreating iframe"); + + const newIframe = document.createElement('iframe'); + newIframe.style.cssText = iframe.style.cssText; + newIframe.sandbox = iframe.sandbox.value; + newIframe.title = iframe.title; + newIframe.tabIndex = iframe.tabIndex; + newIframe.id = iframe.id; + + iframe.parentNode?.replaceChild(newIframe, iframe); + Object.defineProperty(iframeRef, 'current', { + value: newIframe, + writable: false, + enumerable: true, + configurable: true + }); + + iframeDoc = newIframe.contentDocument!; + if (!iframeDoc) { + throw new Error("Cannot access new iframe document"); + } + } const styleTags = Array.from( document.querySelectorAll('link[rel="stylesheet"], style') @@ -897,8 +934,6 @@ export const DOMBrowserRenderer: React.FC = ({ setupIframeInteractions(iframeDoc); } catch (error) { console.error("Error rendering rrweb snapshot:", error); - setRenderError(error instanceof Error ? error.message : String(error)); - showErrorInIframe(error); } }, [setupIframeInteractions, isInCaptureMode, isCachingChildSelectors] @@ -919,89 +954,6 @@ export const DOMBrowserRenderer: React.FC = ({ } }, [getText, getList, listSelector, isRendered, setupIframeInteractions]); - /** - * Show error message in iframe - */ - const showErrorInIframe = (error: any) => { - if (!iframeRef.current) return; - - const iframe = iframeRef.current; - const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document; - - if (iframeDoc) { - try { - iframeDoc.open(); - iframeDoc.write(` - - - - - -
-

Error Loading DOM Content

-

Failed to render the page in DOM mode.

-

Common causes:

-
    -
  • Page is still loading or navigating
  • -
  • Resource proxy timeouts or failures
  • -
  • Network connectivity issues
  • -
  • Invalid HTML structure
  • -
-

Solutions:

-
    -
  • Try switching back to Screenshot mode
  • -
  • Wait for the page to fully load and try again
  • -
  • Check your network connection
  • -
  • Refresh the browser page
  • -
- -
- Technical details -
${error.toString()}
-
-
- - - `); - iframeDoc.close(); - - window.addEventListener("message", (event) => { - if (event.data === "retry-dom-mode") { - if (socket) { - socket.emit("enable-dom-streaming"); - } - } - }); - } catch (e) { - console.error("Failed to write error message to iframe:", e); - } - } - }; - useEffect(() => { return () => { if (iframeRef.current) { @@ -1010,10 +962,13 @@ export const DOMBrowserRenderer: React.FC = ({ const handlers = (iframeDoc as any)._domRendererHandlers; if (handlers) { Object.entries(handlers).forEach(([event, handler]) => { + const options: boolean | AddEventListenerOptions = ['wheel', 'touchstart', 'touchmove'].includes(event) + ? { passive: false } + : false; iframeDoc.removeEventListener( event, handler as EventListener, - true + options ); }); } @@ -1051,7 +1006,7 @@ export const DOMBrowserRenderer: React.FC = ({ /> {/* Loading indicator */} - {!isRendered && !renderError && ( + {!isRendered && (
= ({
)} - {/* Error indicator */} - {renderError && ( -
- RENDER ERROR -
- )} - {/* Capture mode overlay */} {isInCaptureMode && (
Date: Sun, 28 Sep 2025 22:34:31 +0530 Subject: [PATCH 20/45] feat: add integration error handling --- server/src/api/record.ts | 4 ++-- server/src/pgboss-worker.ts | 4 ++-- server/src/workflow-management/scheduler/index.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/server/src/api/record.ts b/server/src/api/record.ts index cdaf89f93..5d9a68cd6 100644 --- a/server/src/api/record.ts +++ b/server/src/api/record.ts @@ -710,8 +710,8 @@ async function executeRun(id: string, userId: string) { retries: 5, }; - processAirtableUpdates(); - processGoogleSheetUpdates(); + processAirtableUpdates().catch(err => logger.log('error', `Airtable update error: ${err.message}`)); + processGoogleSheetUpdates().catch(err => logger.log('error', `Google Sheets update error: ${err.message}`)); } catch (err: any) { logger.log('error', `Failed to update Google Sheet for run: ${plainRun.runId}: ${err.message}`); } diff --git a/server/src/pgboss-worker.ts b/server/src/pgboss-worker.ts index ccc931316..b501f6c96 100644 --- a/server/src/pgboss-worker.ts +++ b/server/src/pgboss-worker.ts @@ -102,8 +102,8 @@ async function triggerIntegrationUpdates(runId: string, robotMetaId: string): Pr retries: 5, }; - processAirtableUpdates(); - processGoogleSheetUpdates(); + processAirtableUpdates().catch(err => logger.log('error', `Airtable update error: ${err.message}`)); + processGoogleSheetUpdates().catch(err => logger.log('error', `Google Sheets update error: ${err.message}`)); } catch (err: any) { logger.log('error', `Failed to update integrations for run: ${runId}: ${err.message}`); } diff --git a/server/src/workflow-management/scheduler/index.ts b/server/src/workflow-management/scheduler/index.ts index 195e1888d..ce272689a 100644 --- a/server/src/workflow-management/scheduler/index.ts +++ b/server/src/workflow-management/scheduler/index.ts @@ -277,8 +277,8 @@ async function executeRun(id: string, userId: string) { retries: 5, }; - processAirtableUpdates(); - processGoogleSheetUpdates(); + processAirtableUpdates().catch(err => logger.log('error', `Airtable update error: ${err.message}`)); + processGoogleSheetUpdates().catch(err => logger.log('error', `Google Sheets update error: ${err.message}`)); } catch (err: any) { logger.log('error', `Failed to update Google Sheet for run: ${plainRun.runId}: ${err.message}`); } From 0aeb8ad6ca3b86fbf509309f37483f995c9a01fd Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Sun, 28 Sep 2025 22:36:43 +0530 Subject: [PATCH 21/45] feat: map cleanup, process duration --- .../integrations/airtable.ts | 27 +++++++++++-------- .../integrations/gsheet.ts | 15 ++++++++--- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/server/src/workflow-management/integrations/airtable.ts b/server/src/workflow-management/integrations/airtable.ts index 5f72c8363..e1f27264d 100644 --- a/server/src/workflow-management/integrations/airtable.ts +++ b/server/src/workflow-management/integrations/airtable.ts @@ -454,33 +454,36 @@ function isValidUrl(str: string): boolean { } export const processAirtableUpdates = async () => { - while (true) { + const maxProcessingTime = 60000; + const startTime = Date.now(); + + while (Date.now() - startTime < maxProcessingTime) { let hasPendingTasks = false; - + for (const runId in airtableUpdateTasks) { const task = airtableUpdateTasks[runId]; - + if (task.status === 'pending') { hasPendingTasks = true; console.log(`Processing Airtable update for run: ${runId}`); - + try { await updateAirtable(task.robotId, task.runId); console.log(`Successfully updated Airtable for runId: ${runId}`); - airtableUpdateTasks[runId].status = 'completed'; - delete airtableUpdateTasks[runId]; + delete airtableUpdateTasks[runId]; } catch (error: any) { console.error(`Failed to update Airtable for run ${task.runId}:`, error); - + if (task.retries < MAX_RETRIES) { airtableUpdateTasks[runId].retries += 1; console.log(`Retrying task for runId: ${runId}, attempt: ${task.retries + 1}`); } else { - airtableUpdateTasks[runId].status = 'failed'; - console.log(`Max retries reached for runId: ${runId}. Marking task as failed.`); - logger.log('error', `Permanent failure for run ${runId}: ${error.message}`); + console.log(`Max retries reached for runId: ${runId}. Removing task.`); + delete airtableUpdateTasks[runId]; } } + } else if (task.status === 'completed' || task.status === 'failed') { + delete airtableUpdateTasks[runId]; } } @@ -488,8 +491,10 @@ export const processAirtableUpdates = async () => { console.log('No pending Airtable update tasks, exiting processor'); break; } - + console.log('Waiting for 5 seconds before checking again...'); await new Promise(resolve => setTimeout(resolve, 5000)); } + + console.log('Airtable processing completed or timed out'); }; \ No newline at end of file diff --git a/server/src/workflow-management/integrations/gsheet.ts b/server/src/workflow-management/integrations/gsheet.ts index 2a29bdcc2..fcf9b95c8 100644 --- a/server/src/workflow-management/integrations/gsheet.ts +++ b/server/src/workflow-management/integrations/gsheet.ts @@ -286,8 +286,12 @@ export async function writeDataToSheet( } export const processGoogleSheetUpdates = async () => { - while (true) { + const maxProcessingTime = 60000; + const startTime = Date.now(); + + while (Date.now() - startTime < maxProcessingTime) { let hasPendingTasks = false; + for (const runId in googleSheetUpdateTasks) { const task = googleSheetUpdateTasks[runId]; console.log(`Processing task for runId: ${runId}, status: ${task.status}`); @@ -297,7 +301,6 @@ export const processGoogleSheetUpdates = async () => { try { await updateGoogleSheet(task.robotId, task.runId); console.log(`Successfully updated Google Sheet for runId: ${runId}`); - googleSheetUpdateTasks[runId].status = 'completed'; delete googleSheetUpdateTasks[runId]; } catch (error: any) { console.error(`Failed to update Google Sheets for run ${task.runId}:`, error); @@ -305,10 +308,12 @@ export const processGoogleSheetUpdates = async () => { googleSheetUpdateTasks[runId].retries += 1; console.log(`Retrying task for runId: ${runId}, attempt: ${task.retries}`); } else { - googleSheetUpdateTasks[runId].status = 'failed'; - console.log(`Max retries reached for runId: ${runId}. Marking task as failed.`); + console.log(`Max retries reached for runId: ${runId}. Removing task.`); + delete googleSheetUpdateTasks[runId]; } } + } else if (task.status === 'completed' || task.status === 'failed') { + delete googleSheetUpdateTasks[runId]; } } @@ -320,4 +325,6 @@ export const processGoogleSheetUpdates = async () => { console.log('Waiting for 5 seconds before checking again...'); await new Promise(resolve => setTimeout(resolve, 5000)); } + + console.log('Google Sheets processing completed or timed out'); }; \ No newline at end of file From c7ec3cf58ad16a21ff01c8166385de5aaef3c355 Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Sun, 28 Sep 2025 22:37:30 +0530 Subject: [PATCH 22/45] fix: server uncaught err handling --- server/src/server.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/server/src/server.ts b/server/src/server.ts index c49b367b1..2b13dfdb6 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -37,6 +37,12 @@ const pool = new Pool({ database: process.env.DB_NAME, password: process.env.DB_PASSWORD, port: process.env.DB_PORT ? parseInt(process.env.DB_PORT, 10) : undefined, + max: 50, // Increased from 20 to handle batched operations + min: 5, // Minimum connections to maintain + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 10000, // Increased from 5000 to 10000 + maxUses: 7500, + allowExitOnIdle: true }); const PgSession = connectPgSimple(session); @@ -215,6 +221,22 @@ if (require.main === module) { }); } +process.on('unhandledRejection', (reason, promise) => { + logger.log('error', `Unhandled promise rejection at: ${promise}, reason: ${reason}`); + console.error('Unhandled promise rejection:', reason); +}); + +process.on('uncaughtException', (error) => { + logger.log('error', `Uncaught exception: ${error.message}`, { stack: error.stack }); + console.error('Uncaught exception:', error); + + if (process.env.NODE_ENV === 'production') { + setTimeout(() => { + process.exit(1); + }, 5000); + } +}); + if (require.main === module) { process.on('SIGINT', async () => { console.log('Main app shutting down...'); From 24af62c026ca96c0f6f7d573cf811a995575d19f Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Sun, 28 Sep 2025 22:39:39 +0530 Subject: [PATCH 23/45] fix: socket cleanup, err handling --- server/src/browser-management/controller.ts | 39 ++++++++++++++++--- server/src/server.ts | 6 +-- .../classes/Interpreter.ts | 2 +- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/server/src/browser-management/controller.ts b/server/src/browser-management/controller.ts index 1c6ecb5cc..a6db615e4 100644 --- a/server/src/browser-management/controller.ts +++ b/server/src/browser-management/controller.ts @@ -77,7 +77,11 @@ export const createRemoteBrowserForRun = (userId: string): string => { logger.log('info', `createRemoteBrowserForRun: Reserved slot ${id} for user ${userId}`); - initializeBrowserAsync(id, userId); + initializeBrowserAsync(id, userId) + .catch((error: any) => { + logger.log('error', `Unhandled error in initializeBrowserAsync for browser ${id}: ${error.message}`); + browserPool.failBrowserSlot(id); + }); return id; }; @@ -110,7 +114,16 @@ export const destroyRemoteBrowser = async (id: string, userId: string): Promise< } catch (switchOffError) { logger.log('warn', `Error switching off browser ${id}: ${switchOffError}`); } - + + try { + const namespace = io.of(id); + namespace.removeAllListeners(); + namespace.disconnectSockets(true); + logger.log('debug', `Cleaned up socket namespace for browser ${id}`); + } catch (namespaceCleanupError: any) { + logger.log('warn', `Error cleaning up socket namespace for browser ${id}: ${namespaceCleanupError.message}`); + } + return browserPool.deleteRemoteBrowser(id); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); @@ -273,11 +286,27 @@ const initializeBrowserAsync = async (id: string, userId: string) => { } logger.log('debug', `Starting browser initialization for ${id}`); - await browserSession.initialize(userId); - logger.log('debug', `Browser initialization completed for ${id}`); - + + try { + await browserSession.initialize(userId); + logger.log('debug', `Browser initialization completed for ${id}`); + } catch (initError: any) { + try { + await browserSession.switchOff(); + logger.log('info', `Cleaned up failed browser initialization for ${id}`); + } catch (cleanupError: any) { + logger.log('error', `Failed to cleanup browser ${id}: ${cleanupError.message}`); + } + throw initError; + } + const upgraded = browserPool.upgradeBrowserSlot(id, browserSession); if (!upgraded) { + try { + await browserSession.switchOff(); + } catch (cleanupError: any) { + logger.log('error', `Failed to cleanup browser after slot upgrade failure: ${cleanupError.message}`); + } throw new Error('Failed to upgrade reserved browser slot'); } diff --git a/server/src/server.ts b/server/src/server.ts index 2b13dfdb6..5aa8efeda 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -37,10 +37,10 @@ const pool = new Pool({ database: process.env.DB_NAME, password: process.env.DB_PASSWORD, port: process.env.DB_PORT ? parseInt(process.env.DB_PORT, 10) : undefined, - max: 50, // Increased from 20 to handle batched operations - min: 5, // Minimum connections to maintain + max: 50, + min: 5, idleTimeoutMillis: 30000, - connectionTimeoutMillis: 10000, // Increased from 5000 to 10000 + connectionTimeoutMillis: 10000, maxUses: 7500, allowExitOnIdle: true }); diff --git a/server/src/workflow-management/classes/Interpreter.ts b/server/src/workflow-management/classes/Interpreter.ts index 0ed19f19d..9338f5062 100644 --- a/server/src/workflow-management/classes/Interpreter.ts +++ b/server/src/workflow-management/classes/Interpreter.ts @@ -309,7 +309,7 @@ export class WorkflowInterpreter { } }; - private clearState = () => { + public clearState = () => { this.debugMessages = []; this.interpretationIsPaused = false; this.activeId = null; From e75a10dcfed98f4b46ac448f7d31da8be54d24a1 Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Sun, 28 Sep 2025 22:53:52 +0530 Subject: [PATCH 24/45] feat: add batch persistence logic --- .../classes/Interpreter.ts | 193 +++++++++++++----- 1 file changed, 144 insertions(+), 49 deletions(-) diff --git a/server/src/workflow-management/classes/Interpreter.ts b/server/src/workflow-management/classes/Interpreter.ts index 9338f5062..79d2242cc 100644 --- a/server/src/workflow-management/classes/Interpreter.ts +++ b/server/src/workflow-management/classes/Interpreter.ts @@ -118,6 +118,22 @@ export class WorkflowInterpreter { */ private currentRunId: string | null = null; + /** + * Batched persistence system for performance optimization + */ + private persistenceBuffer: Array<{ + actionType: string; + data: any; + listIndex?: number; + timestamp: number; + creditValidated: boolean; + }> = []; + + private persistenceTimer: NodeJS.Timeout | null = null; + private readonly BATCH_SIZE = 5; // Process every 5 items + private readonly BATCH_TIMEOUT = 3000; // Or every 3 seconds + private persistenceInProgress = false; + /** * An array of id's of the pairs from the workflow that are about to be paused. * As "breakpoints". @@ -303,13 +319,27 @@ export class WorkflowInterpreter { await this.interpreter.stop(); this.socket.emit('log', '----- The interpretation has been stopped -----', false); - this.clearState(); + await this.clearState(); } else { logger.log('error', 'Cannot stop: No active interpretation.'); } }; - public clearState = () => { + public clearState = async (): Promise => { + if (this.persistenceBuffer.length > 0) { + try { + await this.flushPersistenceBuffer(); + logger.log('debug', 'Successfully flushed final persistence buffer during cleanup'); + } catch (error: any) { + logger.log('error', `Failed to flush final persistence buffer: ${error.message}`); + } + } + + if (this.persistenceTimer) { + clearTimeout(this.persistenceTimer); + this.persistenceTimer = null; + } + this.debugMessages = []; this.interpretationIsPaused = false; this.activeId = null; @@ -324,6 +354,8 @@ export class WorkflowInterpreter { this.binaryData = []; this.currentScrapeListIndex = 0; this.currentRunId = null; + this.persistenceBuffer = []; + this.persistenceInProgress = false; } /** @@ -336,61 +368,22 @@ export class WorkflowInterpreter { }; /** - * Persists data to database in real-time during interpretation + * Persists extracted data to database with intelligent batching for performance + * Falls back to immediate persistence for critical operations * @private */ private persistDataToDatabase = async (actionType: string, data: any, listIndex?: number): Promise => { if (!this.currentRunId) { - logger.log('debug', 'No run ID available for real-time persistence'); + logger.log('debug', 'No run ID available for persistence'); return; } - try { - const run = await Run.findOne({ where: { runId: this.currentRunId } }); - - if (!run) { - logger.log('warn', `Run not found for real-time persistence: ${this.currentRunId}`); - return; - } - - const currentSerializableOutput = run.serializableOutput ? - JSON.parse(JSON.stringify(run.serializableOutput)) : - { scrapeSchema: [], scrapeList: [] }; - - if (actionType === 'scrapeSchema') { - const newSchemaData = Array.isArray(data) ? data : [data]; - const updatedOutput = { - ...currentSerializableOutput, - scrapeSchema: newSchemaData - }; - - await run.update({ - serializableOutput: updatedOutput - }); - - logger.log('debug', `Persisted scrapeSchema data for run ${this.currentRunId}: ${newSchemaData.length} items`); - - } else if (actionType === 'scrapeList' && typeof listIndex === 'number') { - if (!Array.isArray(currentSerializableOutput.scrapeList)) { - currentSerializableOutput.scrapeList = []; - } - - const updatedList = [...currentSerializableOutput.scrapeList]; - updatedList[listIndex] = data; - - const updatedOutput = { - ...currentSerializableOutput, - scrapeList: updatedList - }; + this.addToPersistenceBatch(actionType, data, listIndex, true); - await run.update({ - serializableOutput: updatedOutput - }); - - logger.log('debug', `Persisted scrapeList data for run ${this.currentRunId} at index ${listIndex}: ${Array.isArray(data) ? data.length : 'N/A'} items`); - } - } catch (error: any) { - logger.log('error', `Failed to persist data in real-time for run ${this.currentRunId}: ${error.message}`); + if (actionType === 'scrapeSchema' || this.persistenceBuffer.length >= this.BATCH_SIZE) { + await this.flushPersistenceBuffer(); + } else { + this.scheduleBatchFlush(); } }; @@ -569,4 +562,106 @@ export class WorkflowInterpreter { this.socket = socket; this.subscribeToPausing(); }; + + /** + * Adds data to persistence buffer for batched processing + * @private + */ + private addToPersistenceBatch(actionType: string, data: any, listIndex?: number, creditValidated: boolean = false): void { + this.persistenceBuffer.push({ + actionType, + data, + listIndex, + timestamp: Date.now(), + creditValidated + }); + + logger.log('debug', `Added ${actionType} to persistence buffer (${this.persistenceBuffer.length} items)`); + } + + /** + * Schedules a batched flush if not already scheduled + * @private + */ + private scheduleBatchFlush(): void { + if (!this.persistenceTimer && !this.persistenceInProgress) { + this.persistenceTimer = setTimeout(async () => { + await this.flushPersistenceBuffer(); + }, this.BATCH_TIMEOUT); + } + } + + /** + * Flushes persistence buffer to database in a single transaction + * @private + */ + private async flushPersistenceBuffer(): Promise { + if (this.persistenceBuffer.length === 0 || this.persistenceInProgress || !this.currentRunId) { + return; + } + + if (this.persistenceTimer) { + clearTimeout(this.persistenceTimer); + this.persistenceTimer = null; + } + + this.persistenceInProgress = true; + const batchToProcess = [...this.persistenceBuffer]; + this.persistenceBuffer = []; + + try { + const sequelize = require('../../storage/db').default; + await sequelize.transaction(async (transaction: any) => { + const { Run } = require('../../models'); + const run = await Run.findOne({ + where: { runId: this.currentRunId! }, + transaction + }); + + if (!run) { + logger.log('warn', `Run not found for batched persistence: ${this.currentRunId}`); + return; + } + + const currentSerializableOutput = run.serializableOutput ? + JSON.parse(JSON.stringify(run.serializableOutput)) : + { scrapeSchema: [], scrapeList: [] }; + + let hasUpdates = false; + + for (const item of batchToProcess) { + if (item.actionType === 'scrapeSchema') { + const newSchemaData = Array.isArray(item.data) ? item.data : [item.data]; + currentSerializableOutput.scrapeSchema = newSchemaData; + hasUpdates = true; + } else if (item.actionType === 'scrapeList' && typeof item.listIndex === 'number') { + if (!Array.isArray(currentSerializableOutput.scrapeList)) { + currentSerializableOutput.scrapeList = []; + } + currentSerializableOutput.scrapeList[item.listIndex] = item.data; + hasUpdates = true; + } + } + + if (hasUpdates) { + await run.update({ + serializableOutput: currentSerializableOutput + }, { transaction }); + + logger.log('debug', `Batched persistence: Updated run ${this.currentRunId} with ${batchToProcess.length} items`); + } + }); + + } catch (error: any) { + logger.log('error', `Failed to flush persistence buffer for run ${this.currentRunId}: ${error.message}`); + + this.persistenceBuffer.unshift(...batchToProcess); + + setTimeout(async () => { + await this.flushPersistenceBuffer(); + }, 5000); + } finally { + this.persistenceInProgress = false; + } + }; } From f77f42a929d957bdc29eb85ef775fd3224bc8f07 Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Sun, 28 Sep 2025 23:00:24 +0530 Subject: [PATCH 25/45] feat: add abort state getter --- maxun-core/src/interpret.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/maxun-core/src/interpret.ts b/maxun-core/src/interpret.ts index ae224b9e0..98dd34cc2 100644 --- a/maxun-core/src/interpret.ts +++ b/maxun-core/src/interpret.ts @@ -123,6 +123,13 @@ export default class Interpreter extends EventEmitter { this.isAborted = true; } + /** + * Returns the current abort status + */ + public getIsAborted(): boolean { + return this.isAborted; + } + private async applyAdBlocker(page: Page): Promise { if (this.blocker) { try { From a83b69cfc667251e82fdf764bfe53e504e2d067c Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Sun, 28 Sep 2025 23:01:23 +0530 Subject: [PATCH 26/45] fix: add process retry count logic --- .../classes/Interpreter.ts | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/server/src/workflow-management/classes/Interpreter.ts b/server/src/workflow-management/classes/Interpreter.ts index 79d2242cc..8c1f7e5e6 100644 --- a/server/src/workflow-management/classes/Interpreter.ts +++ b/server/src/workflow-management/classes/Interpreter.ts @@ -130,9 +130,10 @@ export class WorkflowInterpreter { }> = []; private persistenceTimer: NodeJS.Timeout | null = null; - private readonly BATCH_SIZE = 5; // Process every 5 items - private readonly BATCH_TIMEOUT = 3000; // Or every 3 seconds + private readonly BATCH_SIZE = 5; + private readonly BATCH_TIMEOUT = 3000; private persistenceInProgress = false; + private persistenceRetryCount = 0; /** * An array of id's of the pairs from the workflow that are about to be paused. @@ -340,6 +341,18 @@ export class WorkflowInterpreter { this.persistenceTimer = null; } + if (this.interpreter) { + try { + if (!this.interpreter.getIsAborted()) { + this.interpreter.abort(); + } + await this.interpreter.stop(); + logger.log('debug', 'mx-cloud interpreter properly stopped during cleanup'); + } catch (error: any) { + logger.log('warn', `Error stopping mx-cloud interpreter during cleanup: ${error.message}`); + } + } + this.debugMessages = []; this.interpretationIsPaused = false; this.activeId = null; @@ -356,6 +369,7 @@ export class WorkflowInterpreter { this.currentRunId = null; this.persistenceBuffer = []; this.persistenceInProgress = false; + this.persistenceRetryCount = 0; } /** @@ -541,7 +555,6 @@ export class WorkflowInterpreter { } logger.log('debug', `Interpretation finished`); - this.clearState(); return result; } @@ -652,14 +665,29 @@ export class WorkflowInterpreter { } }); + this.persistenceRetryCount = 0; + } catch (error: any) { logger.log('error', `Failed to flush persistence buffer for run ${this.currentRunId}: ${error.message}`); - this.persistenceBuffer.unshift(...batchToProcess); + if (!this.persistenceRetryCount) { + this.persistenceRetryCount = 0; + } + + if (this.persistenceRetryCount < 3) { + this.persistenceBuffer.unshift(...batchToProcess); + this.persistenceRetryCount++; - setTimeout(async () => { - await this.flushPersistenceBuffer(); - }, 5000); + const backoffDelay = Math.min(5000 * Math.pow(2, this.persistenceRetryCount), 30000); + setTimeout(async () => { + await this.flushPersistenceBuffer(); + }, backoffDelay); + + logger.log('warn', `Scheduling persistence retry ${this.persistenceRetryCount}/3 in ${backoffDelay}ms`); + } else { + logger.log('error', `Max persistence retries exceeded for run ${this.currentRunId}, dropping ${batchToProcess.length} items`); + this.persistenceRetryCount = 0; + } } finally { this.persistenceInProgress = false; } From 47d1b24de3a59b673cca7251ea021c8e86f1bfaa Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Sun, 28 Sep 2025 23:25:32 +0530 Subject: [PATCH 27/45] fix: timeout mechanism, revamp working button logic --- maxun-core/src/interpret.ts | 132 +++++++++++++++++++++++++++++++----- 1 file changed, 114 insertions(+), 18 deletions(-) diff --git a/maxun-core/src/interpret.ts b/maxun-core/src/interpret.ts index 98dd34cc2..2937c40b6 100644 --- a/maxun-core/src/interpret.ts +++ b/maxun-core/src/interpret.ts @@ -617,6 +617,13 @@ export default class Interpreter extends EventEmitter { if (methodName === 'waitForLoadState') { try { + let args = step.args; + + if (Array.isArray(args) && args.length === 1) { + args = [args[0], { timeout: 30000 }]; + } else if (!Array.isArray(args)) { + args = [args, { timeout: 30000 }]; + } await executeAction(invokee, methodName, step.args); } catch (error) { await executeAction(invokee, methodName, 'domcontentloaded'); @@ -677,7 +684,19 @@ export default class Interpreter extends EventEmitter { return; } - const results = await page.evaluate((cfg) => window.scrapeList(cfg), config); + const evaluationPromise = page.evaluate((cfg) => window.scrapeList(cfg), config); + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Page evaluation timeout')), 10000) + ); + + let results; + try { + results = await Promise.race([evaluationPromise, timeoutPromise]); + } catch (error) { + debugLog(`Page evaluation failed: ${error.message}`); + return; + } + const newResults = results.filter(item => { const uniqueKey = JSON.stringify(item); if (scrapedItems.has(uniqueKey)) return false; @@ -698,43 +717,94 @@ export default class Interpreter extends EventEmitter { return false; }; + // Helper function to detect if a selector is XPath + const isXPathSelector = (selector: string): boolean => { + return selector.startsWith('//') || + selector.startsWith('/') || + selector.startsWith('./') || + selector.includes('contains(@') || + selector.includes('[count(') || + selector.includes('@class=') || + selector.includes('@id=') || + selector.includes(' and ') || + selector.includes(' or '); + }; + + // Helper function to wait for selector (CSS or XPath) + const waitForSelectorUniversal = async (selector: string, options: any = {}): Promise => { + try { + if (isXPathSelector(selector)) { + // Use XPath locator + const locator = page.locator(`xpath=${selector}`); + await locator.waitFor({ + state: 'attached', + timeout: options.timeout || 10000 + }); + return await locator.elementHandle(); + } else { + // Use CSS selector + return await page.waitForSelector(selector, { + state: 'attached', + timeout: options.timeout || 10000 + }); + } + } catch (error) { + return null; + } + }; + // Enhanced button finder with retry mechanism - const findWorkingButton = async (selectors: string[]): Promise<{ - button: ElementHandle | null, + const findWorkingButton = async (selectors: string[]): Promise<{ + button: ElementHandle | null, workingSelector: string | null, updatedSelectors: string[] }> => { - let updatedSelectors = [...selectors]; - + const startTime = Date.now(); + const MAX_BUTTON_SEARCH_TIME = 15000; + let updatedSelectors = [...selectors]; + for (let i = 0; i < selectors.length; i++) { + if (Date.now() - startTime > MAX_BUTTON_SEARCH_TIME) { + debugLog(`Button search timeout reached (${MAX_BUTTON_SEARCH_TIME}ms), aborting`); + break; + } const selector = selectors[i]; let retryCount = 0; let selectorSuccess = false; while (retryCount < MAX_RETRIES && !selectorSuccess) { try { - const button = await page.waitForSelector(selector, { - state: 'attached', - timeout: 10000 - }); - + const button = await waitForSelectorUniversal(selector, { timeout: 2000 }); + if (button) { debugLog('Found working selector:', selector); - return { - button, + return { + button, workingSelector: selector, - updatedSelectors + updatedSelectors }; + } else { + retryCount++; + debugLog(`Selector "${selector}" not found: attempt ${retryCount}/${MAX_RETRIES}`); + + if (retryCount < MAX_RETRIES) { + await page.waitForTimeout(RETRY_DELAY); + } else { + debugLog(`Removing failed selector "${selector}" after ${MAX_RETRIES} attempts`); + updatedSelectors = updatedSelectors.filter(s => s !== selector); + selectorSuccess = true; + } } } catch (error) { retryCount++; - debugLog(`Selector "${selector}" failed: attempt ${retryCount}/${MAX_RETRIES}`); - + debugLog(`Selector "${selector}" error: attempt ${retryCount}/${MAX_RETRIES} - ${error.message}`); + if (retryCount < MAX_RETRIES) { await page.waitForTimeout(RETRY_DELAY); } else { debugLog(`Removing failed selector "${selector}" after ${MAX_RETRIES} attempts`); updatedSelectors = updatedSelectors.filter(s => s !== selector); + selectorSuccess = true; } } } @@ -1354,9 +1424,35 @@ export default class Interpreter extends EventEmitter { } private async ensureScriptsLoaded(page: Page) { - const isScriptLoaded = await page.evaluate(() => typeof window.scrape === 'function' && typeof window.scrapeSchema === 'function' && typeof window.scrapeList === 'function' && typeof window.scrapeListAuto === 'function' && typeof window.scrollDown === 'function' && typeof window.scrollUp === 'function'); - if (!isScriptLoaded) { - await page.addInitScript({ path: path.join(__dirname, 'browserSide', 'scraper.js') }); + try { + const evaluationPromise = page.evaluate(() => + typeof window.scrape === 'function' && + typeof window.scrapeSchema === 'function' && + typeof window.scrapeList === 'function' && + typeof window.scrapeListAuto === 'function' && + typeof window.scrollDown === 'function' && + typeof window.scrollUp === 'function' + ); + + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Script check timeout')), 3000) + ); + + const isScriptLoaded = await Promise.race([ + evaluationPromise, + timeoutPromise + ]); + + if (!isScriptLoaded) { + await page.addInitScript({ path: path.join(__dirname, 'browserSide', 'scraper.js') }); + } + } catch (error) { + this.log(`Script check failed, adding script anyway: ${error.message}`, Level.WARN); + try { + await page.addInitScript({ path: path.join(__dirname, 'browserSide', 'scraper.js') }); + } catch (scriptError) { + this.log(`Failed to add script: ${scriptError.message}`, Level.ERROR); + } } } From d98531b8381714e15c99dd43db9fd5b6837ff782 Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Mon, 29 Sep 2025 16:14:16 +0530 Subject: [PATCH 28/45] feat: move robot create modal to page --- src/components/robot/RecordingsTable.tsx | 16 +- src/components/robot/pages/RobotCreate.tsx | 349 +++++++++++++++++++++ src/pages/PageWrapper.tsx | 2 + 3 files changed, 353 insertions(+), 14 deletions(-) create mode 100644 src/components/robot/pages/RobotCreate.tsx diff --git a/src/components/robot/RecordingsTable.tsx b/src/components/robot/RecordingsTable.tsx index 42c5971b7..7055c9fa4 100644 --- a/src/components/robot/RecordingsTable.tsx +++ b/src/components/robot/RecordingsTable.tsx @@ -278,20 +278,8 @@ export const RecordingsTable = ({ }, [setRecordings, notify, t]); const handleNewRecording = useCallback(async () => { - const canCreateRecording = await canCreateBrowserInState("recording"); - - if (!canCreateRecording) { - const activeBrowserId = await getActiveBrowserId(); - if (activeBrowserId) { - setActiveBrowserId(activeBrowserId); - setWarningModalOpen(true); - } else { - notify('warning', t('recordingtable.notifications.browser_limit_warning')); - } - } else { - setModalOpen(true); - } - }, []); + navigate('/robots/create'); + }, [navigate]); const notifyRecordingTabsToClose = (browserId: string) => { const closeMessage = { diff --git a/src/components/robot/pages/RobotCreate.tsx b/src/components/robot/pages/RobotCreate.tsx new file mode 100644 index 000000000..70058642c --- /dev/null +++ b/src/components/robot/pages/RobotCreate.tsx @@ -0,0 +1,349 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { + Box, + Typography, + TextField, + Button, + FormControlLabel, + Checkbox, + IconButton, + Grid, + Card, + CircularProgress, + Container, + CardContent +} from '@mui/material'; +import { ArrowBack, PlayCircleOutline, Article } from '@mui/icons-material'; +import { useGlobalInfoStore } from '../../../context/globalInfo'; +import { canCreateBrowserInState, getActiveBrowserId, stopRecording } from '../../../api/recording'; +import { AuthContext } from '../../../context/auth'; +import { GenericModal } from '../../ui/GenericModal'; + + +const RobotCreate: React.FC = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { setBrowserId, setRecordingUrl, notify, setRecordingId } = useGlobalInfoStore(); + + const [url, setUrl] = useState(''); + const [needsLogin, setNeedsLogin] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [isWarningModalOpen, setWarningModalOpen] = useState(false); + const [activeBrowserId, setActiveBrowserId] = useState(''); + + const { state } = React.useContext(AuthContext); + const { user } = state; + + + const handleStartRecording = async () => { + if (!url.trim()) { + notify('error', 'Please enter a valid URL'); + return; + } + + setIsLoading(true); + + try { + const canCreateRecording = await canCreateBrowserInState("recording"); + + if (!canCreateRecording) { + const activeBrowser = await getActiveBrowserId(); + if (activeBrowser) { + setActiveBrowserId(activeBrowser); + setWarningModalOpen(true); + } else { + notify('warning', t('recordingtable.notifications.browser_limit_warning')); + } + setIsLoading(false); + return; + } + + setBrowserId('new-recording'); + setRecordingUrl(url); + + window.sessionStorage.setItem('browserId', 'new-recording'); + window.sessionStorage.setItem('recordingUrl', url); + window.sessionStorage.setItem('initialUrl', url); + window.sessionStorage.setItem('needsLogin', needsLogin.toString()); + + const sessionId = Date.now().toString(); + window.sessionStorage.setItem('recordingSessionId', sessionId); + + window.open(`/recording-setup?session=${sessionId}`, '_blank'); + window.sessionStorage.setItem('nextTabIsRecording', 'true'); + + // Reset loading state immediately after opening new tab + setIsLoading(false); + navigate('/robots'); + } catch (error) { + console.error('Error starting recording:', error); + notify('error', 'Failed to start recording. Please try again.'); + setIsLoading(false); + } + }; + + const handleDiscardAndCreate = async () => { + if (activeBrowserId) { + await stopRecording(activeBrowserId); + notify('warning', t('browser_recording.notifications.terminated')); + } + + setWarningModalOpen(false); + setIsLoading(false); + + // Continue with the original Recording logic + setBrowserId('new-recording'); + setRecordingUrl(url); + + window.sessionStorage.setItem('browserId', 'new-recording'); + window.sessionStorage.setItem('recordingUrl', url); + window.sessionStorage.setItem('initialUrl', url); + window.sessionStorage.setItem('needsLogin', needsLogin.toString()); + + const sessionId = Date.now().toString(); + window.sessionStorage.setItem('recordingSessionId', sessionId); + + window.open(`/recording-setup?session=${sessionId}`, '_blank'); + window.sessionStorage.setItem('nextTabIsRecording', 'true'); + + navigate('/robots'); + }; + + + + + + + return ( + + + + navigate('/robots')} + sx={{ + ml: -1, + mr: 1, + color: theme => theme.palette.text.primary, + backgroundColor: 'transparent !important', + '&:hover': { + backgroundColor: 'transparent !important', + }, + '&:active': { + backgroundColor: 'transparent !important', + }, + '&:focus': { + backgroundColor: 'transparent !important', + }, + '&:focus-visible': { + backgroundColor: 'transparent !important', + }, + }} + disableRipple + aria-label="Go back" + > + + + + New Data Extraction Robot + + + + + + {/* Logo (kept as original) */} + Maxun Logo + + {/* Origin URL Input */} + + setUrl(e.target.value)} + /> + + + {/* Checkbox */} + + setNeedsLogin(e.target.checked)} + color="primary" + /> + } + label="This website needs logging in." + /> + + + {/* Button */} + + + + + + + + + First time creating a robot? + + + Get help and learn how to use Maxun effectively. + + + + + {/* YouTube Tutorials */} + + window.open("https://www.youtube.com/@MaxunOSS/videos", "_blank")} + > + + theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.54)' : '', + }} + > + + + + + Video Tutorials + + + Watch step-by-step guides + + + + + + + {/* Documentation */} + + window.open("https://docs.maxun.dev", "_blank")} + > + + theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.54)' : '', + }} + > +
+ + + + Documentation + + + Explore detailed guides + + + + + + + + + + + { + setWarningModalOpen(false); + setIsLoading(false); + }} modalStyle={modalStyle}> +
+ {t('recordingtable.warning_modal.title')} + + {t('recordingtable.warning_modal.message')} + + + + + + +
+
+ + + + ); +}; + +export default RobotCreate; + +const modalStyle = { + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: '30%', + backgroundColor: 'background.paper', + p: 4, + height: 'fit-content', + display: 'block', + padding: '20px', +}; \ No newline at end of file diff --git a/src/pages/PageWrapper.tsx b/src/pages/PageWrapper.tsx index e7361b2f5..e257367bf 100644 --- a/src/pages/PageWrapper.tsx +++ b/src/pages/PageWrapper.tsx @@ -12,6 +12,7 @@ import Register from './Register'; import UserRoute from '../routes/userRoute'; import { Routes, Route, useNavigate, Navigate } from 'react-router-dom'; import { NotFoundPage } from '../components/dashboard/NotFound'; +import RobotCreate from '../components/robot/pages/RobotCreate'; export const PageWrapper = () => { const [open, setOpen] = useState(false); @@ -94,6 +95,7 @@ export const PageWrapper = () => { }> } /> + } /> } /> } /> } /> From ca65cbe68b363f6dc5d7af3a2e336773d10424c3 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Mon, 29 Sep 2025 16:55:56 +0530 Subject: [PATCH 29/45] chore: install tanstack react query --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d7daee756..e5655a690 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "@mui/lab": "^5.0.0-alpha.80", "@mui/material": "^5.6.2", "@react-oauth/google": "^0.12.1", - "@tanstack/react-query": "^5.89.0", + "@tanstack/react-query": "^5.90.2", "@testing-library/react": "^13.1.1", "@testing-library/user-event": "^13.5.0", "@types/bcrypt": "^5.0.2", From 2a1cde5a513ab0f986488691d4280b3ca85035fa Mon Sep 17 00:00:00 2001 From: amhsirak Date: Mon, 29 Sep 2025 16:58:54 +0530 Subject: [PATCH 30/45] chore: core v0.0.24 --- maxun-core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maxun-core/package.json b/maxun-core/package.json index a9eba3ba7..b0aaf4c0a 100644 --- a/maxun-core/package.json +++ b/maxun-core/package.json @@ -1,6 +1,6 @@ { "name": "maxun-core", - "version": "0.0.23", + "version": "0.0.24", "description": "Core package for Maxun, responsible for data extraction", "main": "build/index.js", "typings": "build/index.d.ts", From 0ced4ccd93eeba2ca6febc8618fa56126b165eb4 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Mon, 29 Sep 2025 16:59:40 +0530 Subject: [PATCH 31/45] chore: maxun v0.0.24 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e5655a690..f96ae012d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "maxun", - "version": "0.0.23", + "version": "0.0.24", "author": "Maxun", "license": "AGPL-3.0-or-later", "dependencies": { From b1c3032897fa82ac15e792856c44f9738bad509d Mon Sep 17 00:00:00 2001 From: amhsirak Date: Mon, 29 Sep 2025 16:59:59 +0530 Subject: [PATCH 32/45] chore: use maxun-core v0.0.24 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f96ae012d..a5eab10c6 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "lodash": "^4.17.21", "loglevel": "^1.8.0", "loglevel-plugin-remote": "^0.6.8", - "maxun-core": "^0.0.23", + "maxun-core": "^0.0.24", "minio": "^8.0.1", "moment-timezone": "^0.5.45", "node-cron": "^3.0.3", From 7155b74bc2f65c1bd2cf4fb80f248d9d86d7cad4 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Mon, 29 Sep 2025 17:03:38 +0530 Subject: [PATCH 33/45] chore: remove discount code --- src/components/dashboard/MainMenu.tsx | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/components/dashboard/MainMenu.tsx b/src/components/dashboard/MainMenu.tsx index 8f0fba782..ea6dac71d 100644 --- a/src/components/dashboard/MainMenu.tsx +++ b/src/components/dashboard/MainMenu.tsx @@ -120,28 +120,6 @@ export const MainMenu = ({ value = 'robots', handleChangeContent }: MainMenuProp Unlock reliable web data extraction. Maxun Cloud ensures you bypass blocks and scale with ease. - - As a thank-you to open source users, enjoy 8% off your subscription! - - - Use the discount code - - - - - - - ), - }} - sx={{ mb: 2, fontSize: 13 }} - /> - From e3e735b47e0304e8979212af44d3b2afa9f01c8e Mon Sep 17 00:00:00 2001 From: amhsirak Date: Mon, 29 Sep 2025 17:09:14 +0530 Subject: [PATCH 34/45] feat: wrap video tutorials and docs inside one modal --- src/components/dashboard/MainMenu.tsx | 33 ++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/components/dashboard/MainMenu.tsx b/src/components/dashboard/MainMenu.tsx index ea6dac71d..679ab8a44 100644 --- a/src/components/dashboard/MainMenu.tsx +++ b/src/components/dashboard/MainMenu.tsx @@ -22,6 +22,7 @@ export const MainMenu = ({ value = 'robots', handleChangeContent }: MainMenuProp const [cloudModalOpen, setCloudModalOpen] = useState(false); const [sponsorModalOpen, setSponsorModalOpen] = useState(false); + const [docModalOpen, setDocModalOpen] = useState(false); const ossDiscountCode = "MAXUNOSS8"; @@ -96,9 +97,39 @@ export const MainMenu = ({ value = 'robots', handleChangeContent }: MainMenuProp
- + setDocModalOpen(false)}> + + + + + + + From 8badf0818867164be1974492c8809ecd90ed4944 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Mon, 29 Sep 2025 17:10:07 +0530 Subject: [PATCH 35/45] fix: remove tutorials tab --- src/components/dashboard/MainMenu.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/dashboard/MainMenu.tsx b/src/components/dashboard/MainMenu.tsx index 679ab8a44..6b13df311 100644 --- a/src/components/dashboard/MainMenu.tsx +++ b/src/components/dashboard/MainMenu.tsx @@ -130,9 +130,6 @@ export const MainMenu = ({ value = 'robots', handleChangeContent }: MainMenuProp - From 3fbe644c9716a0312a9a729b9da3d98958d35cfe Mon Sep 17 00:00:00 2001 From: amhsirak Date: Mon, 29 Sep 2025 17:10:39 +0530 Subject: [PATCH 36/45] fix: remove cloud modal --- src/components/dashboard/MainMenu.tsx | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/components/dashboard/MainMenu.tsx b/src/components/dashboard/MainMenu.tsx index 6b13df311..1b92c0bd9 100644 --- a/src/components/dashboard/MainMenu.tsx +++ b/src/components/dashboard/MainMenu.tsx @@ -139,21 +139,6 @@ export const MainMenu = ({ value = 'robots', handleChangeContent }: MainMenuProp - - setCloudModalOpen(false)}> - - - Join Maxun Cloud - - - Unlock reliable web data extraction. Maxun Cloud ensures you bypass blocks and scale with ease. - - - - - setSponsorModalOpen(false)}> From 5bb2b87d8b86085ff5413a0a89c46adf9ec53f32 Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Mon, 29 Sep 2025 17:11:13 +0530 Subject: [PATCH 37/45] fix: rm deepest elem dialog filtering --- src/helpers/clientSelectorGenerator.ts | 84 -------------------------- 1 file changed, 84 deletions(-) diff --git a/src/helpers/clientSelectorGenerator.ts b/src/helpers/clientSelectorGenerator.ts index dac49d022..385aa8ecb 100644 --- a/src/helpers/clientSelectorGenerator.ts +++ b/src/helpers/clientSelectorGenerator.ts @@ -3906,24 +3906,6 @@ class ClientSelectorGenerator { let elements = iframeDoc.elementsFromPoint(x, y) as HTMLElement[]; if (!elements.length) return null; - const dialogElement = this.findDialogElement(elements); - if (dialogElement) { - const dialogRect = dialogElement.getBoundingClientRect(); - const isClickInsideDialog = x >= dialogRect.left && x <= dialogRect.right && - y >= dialogRect.top && y <= dialogRect.bottom; - - if (isClickInsideDialog) { - const dialogElements = elements.filter( - (el) => el === dialogElement || dialogElement.contains(el) - ); - - const deepestInDialog = this.findDeepestInDialog(dialogElements, dialogElement); - if (deepestInDialog) { - return deepestInDialog; - } - } - } - const filteredElements = this.filterLogicalElements(elements, x, y); const targetElements = filteredElements.length > 0 ? filteredElements : elements; @@ -4061,72 +4043,6 @@ class ClientSelectorGenerator { return depth; } - /** - * Find dialog element in the elements array - */ - private findDialogElement(elements: HTMLElement[]): HTMLElement | null { - let dialogElement = elements.find((el) => el.getAttribute("role") === "dialog"); - - if (!dialogElement) { - dialogElement = elements.find((el) => el.tagName.toLowerCase() === "dialog"); - } - - if (!dialogElement) { - dialogElement = elements.find((el) => { - const classList = el.classList.toString().toLowerCase(); - const id = (el.id || "").toLowerCase(); - - return ( - classList.includes("modal") || - classList.includes("dialog") || - classList.includes("popup") || - classList.includes("overlay") || - id.includes("modal") || - id.includes("dialog") || - id.includes("popup") - ); - }); - } - - return dialogElement || null; - } - - /** - * Find the deepest element within a dialog - */ - private findDeepestInDialog( - dialogElements: HTMLElement[], - dialogElement: HTMLElement - ): HTMLElement | null { - if (!dialogElements.length) return null; - if (dialogElements.length === 1) return dialogElements[0]; - - let deepestElement = dialogElements[0]; - let maxDepth = 0; - - for (const element of dialogElements) { - let depth = 0; - let current = element; - - // Calculate depth within the dialog context - while ( - current && - current.parentElement && - current !== dialogElement.parentElement - ) { - depth++; - current = current.parentElement; - } - - if (depth > maxDepth) { - maxDepth = depth; - deepestElement = element; - } - } - - return deepestElement; - } - /** * Check if an element is a dialog */ From b5e1277ae9e7c8635798ddf063bee1a6a330da60 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Mon, 29 Sep 2025 17:11:53 +0530 Subject: [PATCH 38/45] fix: remove cloud modal --- src/components/dashboard/MainMenu.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/dashboard/MainMenu.tsx b/src/components/dashboard/MainMenu.tsx index 1b92c0bd9..8e3b8fa94 100644 --- a/src/components/dashboard/MainMenu.tsx +++ b/src/components/dashboard/MainMenu.tsx @@ -130,7 +130,11 @@ export const MainMenu = ({ value = 'robots', handleChangeContent }: MainMenuProp - setDocModalOpen(false)}> - - - - + + + + -