From 36b80da631f16238765385973eabaed2bc2c081d Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 2 Jan 2026 11:18:23 +0530 Subject: [PATCH 1/6] feat(core): real time progress update for runs --- maxun-core/src/interpret.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/maxun-core/src/interpret.ts b/maxun-core/src/interpret.ts index b909376a..e6bd62f9 100644 --- a/maxun-core/src/interpret.ts +++ b/maxun-core/src/interpret.ts @@ -48,6 +48,7 @@ interface InterpreterOptions { debugMessage: (msg: string) => void, setActionType: (type: string) => void, incrementScrapeListIndex: () => void, + progressUpdate: (current: number, total: number, percentage: number) => void, }> } @@ -84,6 +85,10 @@ export default class Interpreter extends EventEmitter { private scrapeListCounter: number = 0; + private totalActions: number = 0; + + private executedActions: number = 0; + constructor(workflow: WorkflowFile, options?: Partial) { super(); this.workflow = workflow.workflow; @@ -1596,6 +1601,17 @@ export default class Interpreter extends EventEmitter { workflowCopy.splice(actionId, 1); console.log(`Action with ID ${action.id} removed from the workflow copy.`); + + this.executedActions++; + const percentage = Math.round((this.executedActions / this.totalActions) * 100); + + if (this.options.debugChannel?.progressUpdate) { + this.options.debugChannel.progressUpdate( + this.executedActions, + this.totalActions, + percentage + ); + } // const newSelectors = this.getPreviousSelectors(workflow, actionId); // const newSelectors = this.getSelectors(workflowCopy); @@ -1686,6 +1702,13 @@ export default class Interpreter extends EventEmitter { */ this.initializedWorkflow = Preprocessor.initWorkflow(this.workflow, params); + this.totalActions = this.initializedWorkflow.length; + this.executedActions = 0; + + if (this.options.debugChannel?.progressUpdate) { + this.options.debugChannel.progressUpdate(0, this.totalActions, 0); + } + await this.ensureScriptsLoaded(page); this.stopper = () => { From 2f42914cc422a2f05ebecdffbc83481ece539884 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 2 Jan 2026 11:35:32 +0530 Subject: [PATCH 2/6] feat: real time progress update for runs --- server/src/workflow-management/classes/Interpreter.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/src/workflow-management/classes/Interpreter.ts b/server/src/workflow-management/classes/Interpreter.ts index 03e9ef87..1bd6efe5 100644 --- a/server/src/workflow-management/classes/Interpreter.ts +++ b/server/src/workflow-management/classes/Interpreter.ts @@ -580,6 +580,13 @@ export class WorkflowInterpreter { setActionName: (name: string) => { this.currentActionName = name; }, + progressUpdate: (current: number, total: number, percentage: number) => { + this.socket.nsp.emit('workflowProgress', { + current, + total, + percentage + }); + }, }, serializableCallback: async (data: any) => { try { From 0e3d430f41bfa0d3ee1514b71246dc9c0b14d7be Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 2 Jan 2026 11:38:16 +0530 Subject: [PATCH 3/6] chore: remove scrollToLogBottom() --- src/components/run/ColapsibleRow.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/components/run/ColapsibleRow.tsx b/src/components/run/ColapsibleRow.tsx index 67e82bf0..bf5116e5 100644 --- a/src/components/run/ColapsibleRow.tsx +++ b/src/components/run/ColapsibleRow.tsx @@ -54,12 +54,6 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, onToggleExpanded, cu const logEndRef = useRef(null); - const scrollToLogBottom = () => { - if (logEndRef.current) { - logEndRef.current.scrollIntoView({ behavior: "smooth" }); - } - } - const handleAbort = () => { abortRunHandler(row.runId, row.name, row.browserId); } @@ -67,12 +61,7 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, onToggleExpanded, cu const handleRowExpand = () => { const newOpen = !isOpen; onToggleExpanded(newOpen); - //scrollToLogBottom(); }; - - // useEffect(() => { - // scrollToLogBottom(); - // }, [currentLog]) useEffect(() => { const fetchUserEmail = async () => { From 8a32c0b2d2d0c7b245175ccbd17af603f8304a57 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 2 Jan 2026 11:42:32 +0530 Subject: [PATCH 4/6] feat: real time progress update for runs --- src/components/run/ColapsibleRow.tsx | 89 +++++++++++++++++++++++++++- src/components/run/RunContent.tsx | 34 ++++++++++- 2 files changed, 119 insertions(+), 4 deletions(-) diff --git a/src/components/run/ColapsibleRow.tsx b/src/components/run/ColapsibleRow.tsx index bf5116e5..7307985e 100644 --- a/src/components/run/ColapsibleRow.tsx +++ b/src/components/run/ColapsibleRow.tsx @@ -12,6 +12,45 @@ import { GenericModal } from "../ui/GenericModal"; import { getUserById } from "../../api/auth"; import { useTranslation } from "react-i18next"; import { useTheme } from "@mui/material/styles"; +import { io, Socket } from "socket.io-client"; +import { remoteBrowserApiUrl } from "../../apiConfig"; + +const socketCache = new Map(); +const progressCallbacks = new Map void>>(); + +function getOrCreateSocket(browserId: string): Socket { + if (socketCache.has(browserId)) { + return socketCache.get(browserId)!; + } + + const socket = io(`${remoteBrowserApiUrl}/${browserId}`, { + transports: ["websocket"], + rejectUnauthorized: false + }); + + socket.on('workflowProgress', (data: any) => { + const callbacks = progressCallbacks.get(browserId); + if (callbacks) { + callbacks.forEach(cb => cb(data)); + } + }); + + socketCache.set(browserId, socket); + return socket; +} + +function cleanupSocketIfUnused(browserId: string) { + const callbacks = progressCallbacks.get(browserId); + + if (!callbacks || callbacks.size === 0) { + const socket = socketCache.get(browserId); + if (socket) { + socket.disconnect(); + socketCache.delete(browserId); + progressCallbacks.delete(browserId); + } + } +} interface RunTypeChipProps { runByUserId?: string; @@ -54,6 +93,53 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, onToggleExpanded, cu const logEndRef = useRef(null); + const [workflowProgress, setWorkflowProgress] = useState<{ + current: number; + total: number; + percentage: number; + } | null>(null); + + // Subscribe to progress updates using module-level socket cache + useEffect(() => { + if (!row.browserId) return; + + // Get or create socket (from module cache) + getOrCreateSocket(row.browserId); + + // Register callback + if (!progressCallbacks.has(row.browserId)) { + progressCallbacks.set(row.browserId, new Set()); + } + + const callback = (data: any) => { + setWorkflowProgress(data); + }; + + progressCallbacks.get(row.browserId)!.add(callback); + + // Cleanup: remove callback and cleanup socket if no callbacks remain + return () => { + const callbacks = progressCallbacks.get(row.browserId); + if (callbacks) { + callbacks.delete(callback); + // Cleanup socket if this was the last callback + cleanupSocketIfUnused(row.browserId); + } + }; + }, [row.browserId]); + + // Clear progress UI when run completes and trigger socket cleanup + useEffect(() => { + if (row.status !== 'running' && row.status !== 'queued') { + setWorkflowProgress(null); + // Attempt to cleanup socket when run completes + // (will only cleanup if no other callbacks exist) + if (row.browserId) { + cleanupSocketIfUnused(row.browserId); + } + } + }, [row.status, row.browserId]); + const handleAbort = () => { abortRunHandler(row.runId, row.name, row.browserId); } @@ -185,7 +271,8 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, onToggleExpanded, cu + logEndRef={logEndRef} interpretationInProgress={runningRecordingName === row.name} + workflowProgress={workflowProgress} /> diff --git a/src/components/run/RunContent.tsx b/src/components/run/RunContent.tsx index 0cb3e71a..6a50b098 100644 --- a/src/components/run/RunContent.tsx +++ b/src/components/run/RunContent.tsx @@ -23,6 +23,8 @@ import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; import { useTranslation } from "react-i18next"; import { useThemeMode } from "../../context/theme-provider"; +import { remoteBrowserApiUrl } from "../../apiConfig"; +import { io } from "socket.io-client"; interface RunContentProps { row: Data, @@ -30,10 +32,14 @@ interface RunContentProps { interpretationInProgress: boolean, logEndRef: React.RefObject, abortRunHandler: () => void, + workflowProgress: { + current: number; + total: number; + percentage: number; + } | null, } -export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRef, abortRunHandler }: RunContentProps) => { - const { t } = useTranslation(); +export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRef, abortRunHandler, workflowProgress }: RunContentProps) => { const { t } = useTranslation(); const [tab, setTab] = React.useState('output'); const [markdownContent, setMarkdownContent] = useState(''); const [htmlContent, setHtmlContent] = useState(''); @@ -63,6 +69,15 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe setTab(tab); }, [interpretationInProgress]); + const getProgressMessage = (percentage: number): string => { + if (percentage === 0) return 'Initializing workflow...'; + if (percentage < 25) return 'Starting execution...'; + if (percentage < 50) return 'Processing actions...'; + if (percentage < 75) return 'Extracting data...'; + if (percentage < 100) return 'Finalizing results...'; + return 'Completing...'; + }; + useEffect(() => { setMarkdownContent(''); setHtmlContent(''); @@ -810,7 +825,20 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe {row.status === 'running' || row.status === 'queued' ? ( <> - + {workflowProgress ? ( + <> + + {getProgressMessage(workflowProgress.percentage)} + + ) : ( + <> + + {t('run_content.loading')} + + )} {t('run_content.loading')}