From f48b308d487fff6a7d2c188472198025258884e2 Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 28 Jul 2025 21:55:16 +0530 Subject: [PATCH 01/39] feat: bypass validation check shadow dom --- src/components/browser/BrowserWindow.tsx | 27 ++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/components/browser/BrowserWindow.tsx b/src/components/browser/BrowserWindow.tsx index 0ad16099b..5bb84101d 100644 --- a/src/components/browser/BrowserWindow.tsx +++ b/src/components/browser/BrowserWindow.tsx @@ -336,7 +336,7 @@ export const BrowserWindow = () => { const listElements = evaluateXPathAllWithShadowSupport( iframeElement.contentDocument!, listSelector, - listSelector.includes('>>') || listSelector.startsWith('//') + listSelector.includes(">>") || listSelector.startsWith("//") ).slice(0, 10); if (listElements.length < 2) { @@ -346,13 +346,36 @@ export const BrowserWindow = () => { const validSelectors: string[] = []; for (const selector of selectors) { + // First, try to access the element directly + try { + const testElement = iframeElement.contentDocument!.evaluate( + selector, + iframeElement.contentDocument!, + null, + XPathResult.FIRST_ORDERED_NODE_TYPE, + null + ).singleNodeValue; + + // If we can't access the element, it's likely in shadow DOM - include it + if (!testElement) { + console.log(`Including potentially shadow DOM selector: ${selector}`); + validSelectors.push(selector); + continue; + } + } catch (accessError) { + // If there's an error accessing, assume shadow DOM and include it + console.log(`Including selector due to access error: ${selector}`); + validSelectors.push(selector); + continue; + } + let occurrenceCount = 0; // Get all elements that match this child selector const childElements = evaluateXPathAllWithShadowSupport( iframeElement.contentDocument!, selector, - selector.includes('>>') || selector.startsWith('//') + selector.includes(">>") || selector.startsWith("//") ); // Check how many of these child elements are contained within our list elements From b98422800339447201ebacc881ac367a69c01e82 Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 28 Jul 2025 22:10:30 +0530 Subject: [PATCH 02/39] feat: add turkish lang support --- src/components/dashboard/NavBar.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index c6ea84d85..42a533f82 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -470,6 +470,14 @@ export const NavBar: React.FC = ({ > Deutsch + { + changeLanguage("tr"); + handleMenuClose(); + }} + > + Türkçe + { window.open('https://docs.maxun.dev/development/i18n', '_blank'); From f7d17c9e907f68d285235eb42fefddb408e7adc0 Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 28 Jul 2025 22:21:06 +0530 Subject: [PATCH 03/39] feat: full support for turkish lang --- src/components/dashboard/NavBar.tsx | 8 ++++++++ src/i18n.ts | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index 42a533f82..2b2bd3cc0 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -574,6 +574,14 @@ export const NavBar: React.FC = ({ > Deutsch + { + changeLanguage("tr"); + handleMenuClose(); + }} + > + Türkçe + { window.open('https://docs.maxun.dev/development/i18n', '_blank'); diff --git a/src/i18n.ts b/src/i18n.ts index c5e84364e..96a84a6b0 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -10,7 +10,7 @@ i18n .init({ fallbackLng: 'en', debug: import.meta.env.DEV, - supportedLngs: ['en', 'es', 'ja', 'zh','de'], + supportedLngs: ['en', 'es', 'ja', 'zh','de', 'tr'], interpolation: { escapeValue: false, // React already escapes }, From f3d08949be3668265151cd66e31bcf9e39ef7d4f Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 31 Jul 2025 23:15:57 +0530 Subject: [PATCH 04/39] feat: add rm integration option sheet selection --- .../integration/IntegrationSettings.tsx | 52 +++++++++++++------ 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/src/components/integration/IntegrationSettings.tsx b/src/components/integration/IntegrationSettings.tsx index cf550443a..6d0b892c6 100644 --- a/src/components/integration/IntegrationSettings.tsx +++ b/src/components/integration/IntegrationSettings.tsx @@ -815,14 +815,24 @@ export const IntegrationSettingsModal = ({ ) : error ? ( {error} ) : spreadsheets.length === 0 ? ( - + + + + ) : ( <> {error} ) : airtableBases.length === 0 ? ( - + + + + ) : ( <> Date: Thu, 31 Jul 2025 23:19:05 +0530 Subject: [PATCH 05/39] feat: sponsor & cloud --- src/components/dashboard/MainMenu.tsx | 166 +++++++++++++------------- 1 file changed, 81 insertions(+), 85 deletions(-) diff --git a/src/components/dashboard/MainMenu.tsx b/src/components/dashboard/MainMenu.tsx index 999b9a834..4de2edd52 100644 --- a/src/components/dashboard/MainMenu.tsx +++ b/src/components/dashboard/MainMenu.tsx @@ -1,12 +1,11 @@ -import React from 'react'; +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 { Paper, Button, useTheme } from "@mui/material"; -import { AutoAwesome, FormatListBulleted, VpnKey, Usb, CloudQueue, Description } from "@mui/icons-material"; +import { Paper, Button, useTheme, Modal, Typography, Stack } from "@mui/material"; +import { AutoAwesome, FormatListBulleted, VpnKey, Usb, CloudQueue, Description, Favorite } from "@mui/icons-material"; import { useTranslation } from 'react-i18next'; -import i18n from '../../i18n'; interface MainMenuProps { value: string; @@ -18,19 +17,20 @@ export const MainMenu = ({ value = 'robots', handleChangeContent }: MainMenuProp const { t } = useTranslation(); const navigate = useNavigate(); + const [cloudModalOpen, setCloudModalOpen] = useState(false); + const [sponsorModalOpen, setSponsorModalOpen] = useState(false); + const handleChange = (event: React.SyntheticEvent, newValue: string) => { navigate(`/${newValue}`); handleChangeContent(newValue); }; - // Define colors based on theme mode const defaultcolor = theme.palette.mode === 'light' ? 'black' : 'white'; const buttonStyles = { justifyContent: 'flex-start', textAlign: 'left', fontSize: '17px', - letterSpacing: '0.02857em', padding: '20px 16px 20px 22px', minHeight: '48px', minWidth: '100%', @@ -39,91 +39,87 @@ export const MainMenu = ({ value = 'robots', handleChangeContent }: MainMenuProp textTransform: 'none', color: theme.palette.mode === 'light' ? '#6C6C6C' : 'inherit', '&:hover': { - color: theme.palette.mode === 'light' ? '#6C6C6C' : 'inherit', backgroundColor: theme.palette.mode === 'light' ? '#f5f5f5' : 'inherit', }, }; - return ( - - - - } - iconPosition="start" - /> - } - iconPosition="start" - /> - } - iconPosition="start" - /> - } - iconPosition="start" - /> - -
- - {/* */} - - + + + +
+
+ + {/* Maxun Cloud Modal */} + setCloudModalOpen(false)}> + + + Upgrade to Maxun Cloud + + + Hosting, scaling, and support — all done for you. No setup, no stress. + As a thank-you, OSS users get 5% off. + + - - + + + {/* Sponsor Modal */} + setSponsorModalOpen(false)}> + + + Support Maxun Open Source + + + Maxun is built with love and maintained by a small team. Help us keep it alive and growing 💙 + + + + + + + + ); }; From 72f01652b62cb74fb94e42385c1089c3600f7e5f Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 31 Jul 2025 23:21:50 +0530 Subject: [PATCH 06/39] fix: text --- src/components/dashboard/MainMenu.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/dashboard/MainMenu.tsx b/src/components/dashboard/MainMenu.tsx index 4de2edd52..cf105ad21 100644 --- a/src/components/dashboard/MainMenu.tsx +++ b/src/components/dashboard/MainMenu.tsx @@ -76,7 +76,7 @@ export const MainMenu = ({ value = 'robots', handleChangeContent }: MainMenuProp Documentation - From 09870107ea5335cdb58bca2d996d00af9d5eb4c5 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 31 Jul 2025 23:31:29 +0530 Subject: [PATCH 08/39] feat: oss sponsor text --- src/components/dashboard/MainMenu.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/dashboard/MainMenu.tsx b/src/components/dashboard/MainMenu.tsx index 6708e5109..9b0d50b83 100644 --- a/src/components/dashboard/MainMenu.tsx +++ b/src/components/dashboard/MainMenu.tsx @@ -106,7 +106,10 @@ export const MainMenu = ({ value = 'robots', handleChangeContent }: MainMenuProp Support Maxun Open Source - Maxun is built with love and maintained by a small team. Help us keep it alive and growing 💙 + Maxun is built by a small, full-time team. Your donations directly contribute to making it better. +
+
+ Thank you for your support! 💙
From c8da160ec78741de19e6df592a18fd98f6e6507f Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 31 Jul 2025 23:40:22 +0530 Subject: [PATCH 13/39] feat: 8% --- src/components/dashboard/MainMenu.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/dashboard/MainMenu.tsx b/src/components/dashboard/MainMenu.tsx index fc7dcb3e3..395b4c9d5 100644 --- a/src/components/dashboard/MainMenu.tsx +++ b/src/components/dashboard/MainMenu.tsx @@ -92,9 +92,9 @@ export const MainMenu = ({ value = 'robots', handleChangeContent }: MainMenuProp Extract web data without getting blocked on Maxun Cloud. - As a thank-you, OSS users get 5% off. + As a thank-you, OSS users get 8% off. - From 297e8463faeaf4c1b47236362f44432cfa1f763b Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 31 Jul 2025 23:48:09 +0530 Subject: [PATCH 14/39] feat: text --- src/components/dashboard/MainMenu.tsx | 49 +++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/src/components/dashboard/MainMenu.tsx b/src/components/dashboard/MainMenu.tsx index 395b4c9d5..dcf328c1c 100644 --- a/src/components/dashboard/MainMenu.tsx +++ b/src/components/dashboard/MainMenu.tsx @@ -3,9 +3,10 @@ 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 { Paper, Button, useTheme, Modal, Typography, Stack } from "@mui/material"; -import { AutoAwesome, FormatListBulleted, VpnKey, Usb, CloudQueue, Description, Favorite } from "@mui/icons-material"; +import { Paper, Button, useTheme, Modal, Typography, Stack, TextField, InputAdornment, IconButton } from "@mui/material"; // Added TextField, InputAdornment, IconButton +import { AutoAwesome, FormatListBulleted, VpnKey, Usb, CloudQueue, Description, Favorite, ContentCopy } from "@mui/icons-material"; // Added ContentCopy import { useTranslation } from 'react-i18next'; +import { useGlobalInfoStore } from "../../context/globalInfo"; interface MainMenuProps { value: string; @@ -16,15 +17,27 @@ export const MainMenu = ({ value = 'robots', handleChangeContent }: MainMenuProp const theme = useTheme(); const { t } = useTranslation(); const navigate = useNavigate(); - + const { notify } = useGlobalInfoStore(); + const [cloudModalOpen, setCloudModalOpen] = useState(false); const [sponsorModalOpen, setSponsorModalOpen] = useState(false); + const ossDiscountCode = "MAXUNOSS8"; + const handleChange = (event: React.SyntheticEvent, newValue: string) => { navigate(`/${newValue}`); handleChangeContent(newValue); }; + const copyDiscountCode = () => { + navigator.clipboard.writeText(ossDiscountCode).then(() => { + notify("success", "Discount code copied to clipboard!"); + }).catch(err => { + console.error('Failed to copy text: ', err); + notify("error", "Failed to copy discount code."); + }); + }; + const defaultcolor = theme.palette.mode === 'light' ? 'black' : 'white'; const buttonStyles = { @@ -92,9 +105,31 @@ export const MainMenu = ({ value = 'robots', handleChangeContent }: MainMenuProp Extract web data without getting blocked on Maxun Cloud. - As a thank-you, OSS users get 8% off. - @@ -109,7 +144,7 @@ export const MainMenu = ({ value = 'robots', handleChangeContent }: MainMenuProp Maxun is built by a small, full-time team. Your donations directly contribute to making it better.

- Thank you for your support! 💙 + Thank you for your support! 💙 - From 805e1095dc7b989a66f223597107ad3df5be694c Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 1 Aug 2025 00:04:09 +0530 Subject: [PATCH 21/39] fix: remove caps --- src/components/dashboard/MainMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dashboard/MainMenu.tsx b/src/components/dashboard/MainMenu.tsx index 686c61597..22734fb12 100644 --- a/src/components/dashboard/MainMenu.tsx +++ b/src/components/dashboard/MainMenu.tsx @@ -107,7 +107,7 @@ 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! + As a thank-you to open source users, enjoy 8% off your subscription! Use the discount code From 26de22ade055e23cb2753a34f85a8495dd640308 Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Fri, 1 Aug 2025 00:39:31 +0530 Subject: [PATCH 22/39] fix: add rel noopener noreferrer --- src/components/dashboard/MainMenu.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/dashboard/MainMenu.tsx b/src/components/dashboard/MainMenu.tsx index 22734fb12..3dcf34d84 100644 --- a/src/components/dashboard/MainMenu.tsx +++ b/src/components/dashboard/MainMenu.tsx @@ -146,10 +146,10 @@ export const MainMenu = ({ value = 'robots', handleChangeContent }: MainMenuProp Thank you for your support! 💙 - - @@ -157,4 +157,4 @@ export const MainMenu = ({ value = 'robots', handleChangeContent }: MainMenuProp ); -}; \ No newline at end of file +}; From 89c7184efcb1efa76243b141660eeb3d389dbba7 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 5 Aug 2025 00:19:17 +0530 Subject: [PATCH 23/39] feat: add restart until manually stopped --- docker-compose.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index e6995c06c..67621344c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,7 @@ services: postgres: image: postgres:13 + restart: unless-stopped environment: POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASSWORD} @@ -17,6 +18,7 @@ services: minio: image: minio/minio + restart: unless-stopped environment: MINIO_ROOT_USER: ${MINIO_ACCESS_KEY} MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY} @@ -32,6 +34,7 @@ services: #context: . #dockerfile: server/Dockerfile image: getmaxun/maxun-backend:latest + restart: unless-stopped ports: - "${BACKEND_PORT:-8080}:${BACKEND_PORT:-8080}" env_file: .env @@ -58,6 +61,7 @@ services: #context: . #dockerfile: Dockerfile image: getmaxun/maxun-frontend:latest + restart: unless-stopped ports: - "${FRONTEND_PORT:-5173}:${FRONTEND_PORT:-5173}" env_file: .env From 47fc16806dc1fb8b3cc54deed102fbb6d951f5ad Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 5 Aug 2025 01:22:51 +0530 Subject: [PATCH 24/39] feat: continue other job execution on fail --- maxun-core/src/utils/concurrency.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/maxun-core/src/utils/concurrency.ts b/maxun-core/src/utils/concurrency.ts index 56c15fd9f..41fc10475 100644 --- a/maxun-core/src/utils/concurrency.ts +++ b/maxun-core/src/utils/concurrency.ts @@ -41,6 +41,10 @@ export default class Concurrency { job().then(() => { // console.debug("Job finished, running the next waiting job..."); this.runNextJob(); + }).catch((error) => { + console.error(`Job failed with error: ${error.message}`); + // Continue processing other jobs even if one fails + this.runNextJob(); }); } else { // console.debug("No waiting job found!"); From 780b18b9f4faae63a75d1af9b6ab24d40531c22d Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 5 Aug 2025 01:24:18 +0530 Subject: [PATCH 25/39] feat: page validity, continue if click fails --- maxun-core/src/interpret.ts | 61 ++++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/maxun-core/src/interpret.ts b/maxun-core/src/interpret.ts index c367c16d6..5ed6fb12b 100644 --- a/maxun-core/src/interpret.ts +++ b/maxun-core/src/interpret.ts @@ -108,7 +108,9 @@ export default class Interpreter extends EventEmitter { PlaywrightBlocker.fromLists(fetch, ['https://easylist.to/easylist/easylist.txt']).then(blocker => { this.blocker = blocker; }).catch(err => { - this.log(`Failed to initialize ad-blocker:`, Level.ERROR); + this.log(`Failed to initialize ad-blocker: ${err.message}`, Level.ERROR); + // Continue without ad-blocker rather than crashing + this.blocker = null; }) } @@ -522,11 +524,16 @@ export default class Interpreter extends EventEmitter { this.options.debugChannel.setActionType('script'); } - const AsyncFunction: FunctionConstructor = Object.getPrototypeOf( - async () => { }, - ).constructor; - const x = new AsyncFunction('page', 'log', code); - await x(page, this.log); + try { + const AsyncFunction: FunctionConstructor = Object.getPrototypeOf( + async () => { }, + ).constructor; + const x = new AsyncFunction('page', 'log', code); + await x(page, this.log); + } catch (error) { + this.log(`Script execution failed: ${error.message}`, Level.ERROR); + throw new Error(`Script execution error: ${error.message}`); + } }, flag: async () => new Promise((res) => { @@ -590,11 +597,18 @@ export default class Interpreter extends EventEmitter { try{ await executeAction(invokee, methodName, [step.args[0], { force: true }]); } catch (error) { - continue + this.log(`Click action failed: ${error.message}`, Level.WARN); + continue; } } } else { - await executeAction(invokee, methodName, step.args); + try { + await executeAction(invokee, methodName, step.args); + } catch (error) { + this.log(`Action ${methodName} failed: ${error.message}`, Level.ERROR); + // Continue with next action instead of crashing + continue; + } } } @@ -1132,7 +1146,16 @@ export default class Interpreter extends EventEmitter { }); /* eslint no-constant-condition: ["warn", { "checkLoops": false }] */ + let loopIterations = 0; + const MAX_LOOP_ITERATIONS = 1000; // Circuit breaker + while (true) { + // Circuit breaker to prevent infinite loops + if (++loopIterations > MAX_LOOP_ITERATIONS) { + this.log('Maximum loop iterations reached, terminating to prevent infinite loop', Level.ERROR); + return; + } + // Checks whether the page was closed from outside, // or the workflow execution has been stopped via `interpreter.stop()` if (p.isClosed() || !this.stopper) { @@ -1147,14 +1170,25 @@ export default class Interpreter extends EventEmitter { } let pageState = {}; - let getStateTest = "Hello"; try { + // Check if page is still valid before accessing state + if (p.isClosed()) { + this.log('Page was closed during execution', Level.WARN); + return; + } + pageState = await this.getState(p, workflowCopy, selectors); selectors = []; console.log("Empty selectors:", selectors) } catch (e: any) { - this.log('The browser has been closed.'); - return; + this.log(`Failed to get page state: ${e.message}`, Level.ERROR); + // If state access fails, attempt graceful recovery + if (p.isClosed()) { + this.log('Browser has been closed, terminating workflow', Level.WARN); + return; + } + // For other errors, continue with empty state to avoid complete failure + pageState = { url: p.url(), selectors: [], cookies: {} }; } if (this.options.debug) { @@ -1207,8 +1241,13 @@ export default class Interpreter extends EventEmitter { selectors.push(selector); } }); + + // Reset loop iteration counter on successful action + loopIterations = 0; } catch (e) { this.log(e, Level.ERROR); + // Don't crash on individual action failures - continue with next iteration + continue; } } else { //await this.disableAdBlocker(p); From 6dac0827b032470ebb6669860a814db9070bf9e1 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 5 Aug 2025 01:25:05 +0530 Subject: [PATCH 26/39] feat: null checks for doc and iframe,frame --- maxun-core/src/browserSide/scraper.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/maxun-core/src/browserSide/scraper.js b/maxun-core/src/browserSide/scraper.js index ba688c474..fdf1ff9c9 100644 --- a/maxun-core/src/browserSide/scraper.js +++ b/maxun-core/src/browserSide/scraper.js @@ -537,6 +537,11 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3, const evaluateXPath = (document, xpath, isShadow = false) => { try { + if (!document || !xpath) { + console.warn('Invalid document or xpath provided to evaluateXPath'); + return null; + } + const result = document.evaluate( xpath, document, @@ -632,6 +637,7 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3, return null; } catch (err) { console.error("Critical XPath failure:", xpath, err); + // Return null instead of throwing to prevent crashes return null; } }; @@ -694,16 +700,25 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3, for (let i = 0; i < parts.length; i++) { if (!currentElement) return null; - // Handle iframe and frame traversal + // Handle iframe and frame traversal with enhanced safety if ( currentElement.tagName === "IFRAME" || currentElement.tagName === "FRAME" ) { try { + // Check if frame is accessible + if (!currentElement.contentDocument && !currentElement.contentWindow) { + console.warn('Frame is not accessible (cross-origin or unloaded)'); + return null; + } + const frameDoc = currentElement.contentDocument || - currentElement.contentWindow.document; - if (!frameDoc) return null; + currentElement.contentWindow?.document; + if (!frameDoc) { + console.warn('Frame document is not available'); + return null; + } if (isXPathSelector(parts[i])) { currentElement = evaluateXPath(frameDoc, parts[i]); From 50fc73746dd1b9df3d0de553a911094a10389058 Mon Sep 17 00:00:00 2001 From: iamdoubz <4871781+iamdoubz@users.noreply.github.com> Date: Tue, 5 Aug 2025 10:18:41 -0500 Subject: [PATCH 27/39] Create self-hosting-docker.md --- docs/self-hosting-docker.md | 134 ++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 docs/self-hosting-docker.md diff --git a/docs/self-hosting-docker.md b/docs/self-hosting-docker.md new file mode 100644 index 000000000..93419a167 --- /dev/null +++ b/docs/self-hosting-docker.md @@ -0,0 +1,134 @@ +# Self hosting docker guide + +So you want to create a bot? Let's get you started! + +## Requirements (not covered) +- Webserver (Apache2, nginx, etc.) +- SSL Certificates (letsencrypt, zerossl, etc) +- A sub-domain to host maxun i.e. maxun.my.domain +- Docker +- Docker compose +- Probably others... + +## Guide +For this guide, we assume that before you start, you have a dedicated docker folder to house config files and everything else we need for persistence between docker container reboots and updates. The path in this guide is `/home/$USER/Docker/maxun`. +1. Change directory into your docker folder `cd /home/$USER/Docker/` +2. Create a new directory for maxun and all the required sub-folders for our docker services `mkdir -p maxun/{db,minio,redis}` +3. Change directory to enter the newly created folder `cd maxun` +4. Create an environment file to save your variables `nano .env` with the following contents: +``` +NODE_ENV=production +JWT_SECRET=openssl rand -base64 48 +DB_NAME=maxun +DB_USER=postgres +DB_PASSWORD=openssl rand -base64 24 +DB_HOST=postgres +DB_PORT=5432 +ENCRYPTION_KEY=openssl rand -base64 64 +SESSION_SECRET=openssl rand -base64 48 +MINIO_ENDPOINT=minio +MINIO_PORT=9000 +MINIO_CONSOLE_PORT=9001 +MINIO_ACCESS_KEY=minio +MINIO_SECRET_KEY=openssl rand -base64 24 +REDIS_HOST=maxun-redis +REDIS_PORT=6379 +REDIS_PASSWORD= +BACKEND_PORT=8080 +FRONTEND_PORT=5173 +BACKEND_URL=https://maxun.my.domain +PUBLIC_URL=https://maxun.my.domain +VITE_BACKEND_URL=https://maxun.my.domain +VITE_PUBLIC_URL=https://maxun.my.domain +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GOOGLE_REDIRECT_URI= +AIRTABLE_CLIENT_ID= +AIRTABLE_REDIRECT_URI= +MAXUN_TELEMETRY=true +``` +5. Ctrl + x, Y, Enter will save your changes +6. Please be sure to READ this file and change the variables to match your environment!!! i.e. BACKEND_PORT=30000 +7. Create a file for docker compose `nano docker-compose.yml` with the following contents: +```yml +services: + postgres: + image: postgres:17 + container_name: maxun-postgres + mem_limit: 512M + environment: + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_NAME} + volumes: + - /home/$USER/Docker/maxun/db:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + + redis: + image: docker.io/library/redis:7 + container_name: maxun-redis + restart: always + mem_limit: 128M + volumes: + - /home/$USER/Docker/maxun/redis:/data + + minio: + image: minio/minio + container_name: maxun-minio + mem_limit: 512M + environment: + MINIO_ROOT_USER: ${MINIO_ACCESS_KEY} + MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY} + command: server /data --console-address :${MINIO_CONSOLE_PORT:-9001} + volumes: + - /home/$USER/Docker/maxun/minio:/data + + backend: + image: getmaxun/maxun-backend:latest + container_name: maxun-backend + ports: + - "127.0.0.1:${BACKEND_PORT:-8080}:${BACKEND_PORT:-8080}" + env_file: .env + environment: + BACKEND_URL: ${BACKEND_URL} + PLAYWRIGHT_BROWSERS_PATH: /ms-playwright + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 0 + # DEBUG: pw:api + # PWDEBUG: 1 # Enables debugging + CHROMIUM_FLAGS: '--disable-gpu --no-sandbox --headless=new' + security_opt: + - seccomp=unconfined # This might help with browser sandbox issues + shm_size: '2gb' + mem_limit: 4g + depends_on: + - postgres + - minio + volumes: + - /var/run/dbus:/var/run/dbus + + frontend: + image: getmaxun/maxun-frontend:latest + container_name: maxun-frontend + mem_limit: 512M + ports: + - "127.0.0.1:${FRONTEND_PORT:-5173}:5173" + env_file: .env + environment: + PUBLIC_URL: ${PUBLIC_URL} + BACKEND_URL: ${BACKEND_URL} + depends_on: + - backend +``` +8. Ctrl + x, Y, Enter will save your changes +9. This particular setup is "production ready" meaning that maxun is only accessible from localhost. You must configure a reverse proxy to access it! +10. Start maxun `sudo docker compose up -d` or `sudo docker-compose up -d` +11. Wait 30 seconds for everything to come up +12. Access your maxun instance at http://localhost:5173 if using defaults + +## Next steps +You will want to configure a reverse proxy. Click on a link below to check out some examples. +- [Nginx](nginx.conf) From 737b7ad081a206f59fe83a3a9a76f538b282ec09 Mon Sep 17 00:00:00 2001 From: iamdoubz <4871781+iamdoubz@users.noreply.github.com> Date: Tue, 5 Aug 2025 10:25:23 -0500 Subject: [PATCH 28/39] Create nginx.conf --- docs/nginx.conf | 92 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 docs/nginx.conf diff --git a/docs/nginx.conf b/docs/nginx.conf new file mode 100644 index 000000000..d5cf2ba8e --- /dev/null +++ b/docs/nginx.conf @@ -0,0 +1,92 @@ +# Robust maxun nginx config file +# DO NOT uncomment commented lines unless YOU know what they mean and YOU know what YOU are doing! +### HTTP server block ### +server { + server_name maxun.my.domain; + root /usr/share/nginx/html; + listen 80; + server_tokens off; + return 301 https://$server_name$request_uri; +} +### HTTPS server block ### +server { +### Default config ### + server_name maxun.my.domain; + root /usr/share/nginx/html; + access_log /var/log/nginx/maxun_access.log; + error_log /var/log/nginx/maxun_error.log info; + listen 443 ssl; + http2 on; + server_tokens off; +### SSL config ### + ssl_certificate /etc/letsencrypt/live/my.domain/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/my.domain/privkey.pem; + ssl_trusted_certificate /etc/letsencrypt/live/my.domain/chain.pem; + ssl_protocols TLSv1.2 TLSv1.3; + #ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1:secp384r1; + ssl_ecdh_curve X25519:prime256v1:secp384r1; + ssl_prefer_server_ciphers off; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305; + ssl_stapling off; + ssl_stapling_verify off; + ssl_session_cache shared:MozSSL:10m; + ssl_session_tickets off; + ssl_session_timeout 1d; + ssl_dhparam dh.pem; + #ssl_conf_command Options KTLS; +### Performance tuning config ### + client_max_body_size 512M; + client_body_timeout 300s; + client_body_buffer_size 256k; + #pagespeed off; +### Compression ### + ## gzip ## + gzip on; + gzip_vary on; + gzip_comp_level 5; + gzip_min_length 256; + gzip_disable msie6; + gzip_proxied expired no-cache no-store private no_last_modified no_etag auth; + gzip_buffers 16 8k; + gzip_types application/atom+xml text/javascript application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/wasm application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy; + ## brotli: enable only if you have compiled nginx with brotli support!!! ## + #brotli on; + #brotli_static on; + #brotli_comp_level 6; + #brotli_types application/atom+xml application/javascript application/json application/rss+xml + # application/vnd.ms-fontobject application/x-font-opentype application/x-font-truetype + # application/x-font-ttf application/x-javascript application/xhtml+xml application/xml + # font/eot font/opentype font/otf font/truetype image/svg+xml image/vnd.microsoft.icon + # image/x-icon image/x-win-bitmap text/css text/javascript text/plain text/xml; +### Default headers ### + add_header Referrer-Policy "no-referrer" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Permitted-Cross-Domain-Policies "none" always; + add_header X-Robots-Tag "noindex, nofollow" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Permissions-Policy "geolocation=(self), midi=(self), sync-xhr=(self), microphone=(self), camera=(self), magnetometer=(self), gyroscope=(self), fullscreen=(self), payment=(self), interest-cohort=()"; +### Proxy rules ### + # Backend web traffic and websockets + location ~ ^/(auth|storage|record|workflow|robot|proxy|api-docs|api|webhook|socket.io)(/|$) { + proxy_pass http://localhost:8080; #Change the port number to match .env file BACKEND_PORT variable + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + # Frontend web traffic + location / { + proxy_pass http://localhost:5173; #Change the port number to match .env file FRONTEND_PORT variable + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} From c62390b0d8014c9b4c629f6ebb0dc595ae777eb6 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 5 Aug 2025 23:16:10 +0530 Subject: [PATCH 29/39] feat: always group table rows --- src/helpers/clientSelectorGenerator.ts | 174 +++++++++++++------------ 1 file changed, 92 insertions(+), 82 deletions(-) diff --git a/src/helpers/clientSelectorGenerator.ts b/src/helpers/clientSelectorGenerator.ts index face7524f..cb782d444 100644 --- a/src/helpers/clientSelectorGenerator.ts +++ b/src/helpers/clientSelectorGenerator.ts @@ -185,7 +185,6 @@ class ClientSelectorGenerator { if (element.nodeType !== Node.ELEMENT_NODE) return null; const tagName = element.tagName.toLowerCase(); - const isCustomElement = tagName.includes("-"); const standardExcludeSelectors = [ @@ -203,38 +202,55 @@ class ClientSelectorGenerator { if (this.groupingConfig.excludeSelectors.includes(tagName)) return null; const children = Array.from(element.children); - const childrenStructure = children.map((child) => ({ - tag: child.tagName.toLowerCase(), - classes: this.normalizeClasses(child.classList), - hasText: (child.textContent ?? "").trim().length > 0, - })); + let childrenStructureString: string; + + if (tagName === 'table') { + // For tables, the fingerprint is based on the header or first row's structure. + const thead = element.querySelector('thead'); + const representativeRow = thead ? thead.querySelector('tr') : element.querySelector('tr'); + + if (representativeRow) { + const structure = Array.from(representativeRow.children).map(child => ({ + tag: child.tagName.toLowerCase(), + classes: this.normalizeClasses(child.classList), + })); + childrenStructureString = JSON.stringify(structure); + } else { + childrenStructureString = JSON.stringify([]); + } + } else if (tagName === 'tr') { + // For rows, the fingerprint is based on the cell structure, ignoring the cell's inner content. + const structure = children.map((child) => ({ + tag: child.tagName.toLowerCase(), + classes: this.normalizeClasses(child.classList), + })); + childrenStructureString = JSON.stringify(structure); + } else { + // Original logic for all other elements. + const structure = children.map((child) => ({ + tag: child.tagName.toLowerCase(), + classes: this.normalizeClasses(child.classList), + hasText: (child.textContent ?? "").trim().length > 0, + })); + childrenStructureString = JSON.stringify(structure); + } const normalizedClasses = this.normalizeClasses(element.classList); const relevantAttributes = Array.from(element.attributes) .filter((attr) => { if (isCustomElement) { - return ![ - "id", - "style", - "data-reactid", - "data-react-checksum", - ].includes(attr.name.toLowerCase()); + return !["id", "style", "data-reactid", "data-react-checksum"].includes(attr.name.toLowerCase()); } else { return ( - !["id", "style", "data-reactid", "data-react-checksum"].includes( - attr.name.toLowerCase() - ) && - (!attr.name.startsWith("data-") || - attr.name === "data-type" || - attr.name === "data-role") + !["id", "style", "data-reactid", "data-react-checksum"].includes(attr.name.toLowerCase()) && + (!attr.name.startsWith("data-") || attr.name === "data-type" || attr.name === "data-role") ); } }) .map((attr) => `${attr.name}=${attr.value}`) .sort(); - // Calculate element depth let depth = 0; let parent = element.parentElement; while (parent && depth < 20) { @@ -242,27 +258,22 @@ class ClientSelectorGenerator { parent = parent.parentElement; } - // Get text content characteristics const textContent = (element.textContent ?? "").trim(); const textCharacteristics = { hasText: textContent.length > 0, textLength: Math.floor(textContent.length / 20) * 20, hasLinks: element.querySelectorAll("a").length, hasImages: element.querySelectorAll("img").length, - hasButtons: element.querySelectorAll( - 'button, input[type="button"], input[type="submit"]' - ).length, + hasButtons: element.querySelectorAll('button, input[type="button"], input[type="submit"]').length, }; - const signature = `${tagName}::${normalizedClasses}::${ - children.length - }::${JSON.stringify(childrenStructure)}::${relevantAttributes.join("|")}`; + const signature = `${tagName}::${normalizedClasses}::${children.length}::${childrenStructureString}::${relevantAttributes.join("|")}`; return { tagName, normalizedClasses, childrenCount: children.length, - childrenStructure: JSON.stringify(childrenStructure), + childrenStructure: childrenStructureString, attributes: relevantAttributes.join("|"), depth, textCharacteristics, @@ -379,87 +390,86 @@ class ClientSelectorGenerator { ) { return; } - + // Clear previous analysis this.elementGroups.clear(); this.groupedElements.clear(); this.lastAnalyzedDocument = iframeDoc; - + // Get all visible elements INCLUDING shadow DOM const allElements = this.getAllVisibleElementsWithShadow(iframeDoc); - - // Create fingerprints for all elements + const processedInTables = new Set(); + + // 1. Specifically find and group rows within each table, bypassing normal similarity checks. + const tables = allElements.filter(el => el.tagName === 'TABLE'); + + tables.forEach(table => { + const rows = Array.from(table.querySelectorAll('tbody > tr')).filter(row => { + const parent = row.parentElement; + if (!parent || !table.contains(parent)) return false; // Ensure row belongs to this table + + const rect = row.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; + }) as HTMLElement[]; + + // If the table has enough rows, force them into a single group. + if (rows.length >= this.groupingConfig.minGroupSize) { + const representativeFingerprint = this.getStructuralFingerprint(rows[0]); + if (!representativeFingerprint) return; + + const group: ElementGroup = { + elements: rows, + fingerprint: representativeFingerprint, + representative: rows[0], + }; + + rows.forEach(row => { + this.elementGroups.set(row, group); + this.groupedElements.add(row); + processedInTables.add(row); + }); + } + }); + + // 2. Group all other elements, excluding table rows that were already grouped. + const remainingElements = allElements.filter(el => !processedInTables.has(el)); const elementFingerprints = new Map(); - - allElements.forEach((element) => { + remainingElements.forEach((element) => { const fingerprint = this.getStructuralFingerprint(element); if (fingerprint) { elementFingerprints.set(element, fingerprint); } }); - - // Find similar groups using similarity scoring - const similarGroups: ElementGroup[] = []; + const processedElements = new Set(); - elementFingerprints.forEach((fingerprint, element) => { if (processedElements.has(element)) return; - + const currentGroup = [element]; processedElements.add(element); - - // Find similar elements + elementFingerprints.forEach((otherFingerprint, otherElement) => { if (processedElements.has(otherElement)) return; - - const similarity = this.calculateSimilarity( - fingerprint, - otherFingerprint - ); - + + const similarity = this.calculateSimilarity(fingerprint, otherFingerprint); if (similarity >= this.groupingConfig.similarityThreshold) { currentGroup.push(otherElement); processedElements.add(otherElement); } }); - - // Add group if it has enough members AND has meaningful children - if (currentGroup.length >= this.groupingConfig.minGroupSize) { - // Check if the representative element has meaningful children - const hasChildren = this.hasAnyMeaningfulChildren(element); - - if (hasChildren) { - const group: ElementGroup = { - elements: currentGroup, - fingerprint, - representative: element, - }; - similarGroups.push(group); - - // Map each element to its group - currentGroup.forEach((el) => { - this.elementGroups.set(el, group); - this.groupedElements.add(el); - }); - } + + if (currentGroup.length >= this.groupingConfig.minGroupSize && this.hasAnyMeaningfulChildren(element)) { + const group: ElementGroup = { + elements: currentGroup, + fingerprint, + representative: element, + }; + currentGroup.forEach((el) => { + this.elementGroups.set(el, group); + this.groupedElements.add(el); + }); } }); - - // Sort groups by size and relevance - similarGroups.sort((a, b) => { - // Prioritize by size first - if (b.elements.length !== a.elements.length) - return b.elements.length - a.elements.length; - - // Then by element size - const aSize = - a.representative.getBoundingClientRect().width * - a.representative.getBoundingClientRect().height; - const bSize = - b.representative.getBoundingClientRect().width * - b.representative.getBoundingClientRect().height; - return bSize - aSize; - }); } /** From 2656476c4401f22ccc3d6ecbf927cc813720bc45 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 5 Aug 2025 23:20:02 +0530 Subject: [PATCH 30/39] feat: increase job execution duration to 23 hours --- server/src/pgboss-worker.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/pgboss-worker.ts b/server/src/pgboss-worker.ts index bf4cbc1ea..2c3ae1ac1 100644 --- a/server/src/pgboss-worker.ts +++ b/server/src/pgboss-worker.ts @@ -56,7 +56,10 @@ interface AbortRunData { runId: string; } -const pgBoss = new PgBoss({connectionString: pgBossConnectionString }); +const pgBoss = new PgBoss({ + connectionString: pgBossConnectionString, + expireInHours: 23 +}); /** * Extract data safely from a job (single job or job array) From 8287a81e02be9409e42c5fcbadeab7e448d56cbb Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 6 Aug 2025 22:46:27 +0530 Subject: [PATCH 31/39] feat: sync translations for all langs --- public/locales/de.json | 16 ++++++++++++++++ public/locales/en.json | 3 +++ public/locales/es.json | 33 +++++++++++++++++++++++++++++++++ public/locales/ja.json | 15 +++++++++++++++ public/locales/tr.json | 3 +++ public/locales/zh.json | 15 +++++++++++++++ 6 files changed, 85 insertions(+) diff --git a/public/locales/de.json b/public/locales/de.json index eef6ace50..19b37c7bf 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -58,6 +58,13 @@ "edit": "Bearbeiten", "delete": "Löschen", "duplicate": "Duplizieren", + "search": "Roboter suchen...", + "warning_modal": { + "title": "Aktiver Browser erkannt", + "message": "Es läuft bereits eine Browser-Aufzeichnungssitzung. Möchten Sie sie verwerfen und eine neue Aufzeichnung erstellen?", + "discard_and_create": "Verwerfen & Neu erstellen", + "cancel": "Abbrechen" + }, "notifications": { "delete_warning": "Der Roboter hat zugehörige Ausführungen. Löschen Sie zuerst die Ausführungen, um den Roboter zu löschen", "delete_success": "Roboter erfolgreich gelöscht", @@ -171,6 +178,11 @@ "pagination": "Wählen Sie aus, wie der Roboter den Rest der Liste erfassen kann", "limit": "Wählen Sie die Anzahl der zu extrahierenden Elemente", "complete": "Erfassung ist abgeschlossen" + }, + "actions": { + "text": "Text erfassen", + "list": "Liste erfassen", + "screenshot": "Screenshot erfassen" } }, "right_panel": { @@ -183,6 +195,7 @@ "confirm_capture": "Erfassung bestätigen", "confirm_pagination": "Bestätigen", "confirm_limit": "Bestätigen", + "confirm_reset": "Bestätigen", "finish_capture": "Erfassung abschließen", "back": "Zurück", "reset": "Starten Sie die Aufnahme neu", @@ -301,6 +314,9 @@ "use_previous": "Möchten Sie Ihre vorherige Auswahl als Bedingung für diese Aktion verwenden?", "previous_action": "Ihre vorherige Aktion war: ", "element_text": "auf einem Element mit Text " + }, + "notifications": { + "reset_success": "Ausgabevorschau erfolgreich zurückgesetzt" } }, "recording_page": { diff --git a/public/locales/en.json b/public/locales/en.json index 7721231eb..51aada6b4 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -314,6 +314,9 @@ "use_previous": "Do you want to use your previous selection as a condition for performing this action?", "previous_action": "Your previous action was: ", "element_text": "on an element with text " + }, + "notifications": { + "reset_success": "Output Preview reset successfully" } }, "recording_page": { diff --git a/public/locales/es.json b/public/locales/es.json index c98b2c8b5..92bf7fe36 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -59,6 +59,12 @@ "delete": "Eliminar", "duplicate": "Duplicar", "search": "Buscar robots...", + "warning_modal": { + "title": "Navegador Activo Detectado", + "message": "Ya hay una sesión de grabación del navegador en ejecución. ¿Le gustaría descartarla y crear una nueva grabación?", + "discard_and_create": "Descartar y Crear Nueva", + "cancel": "Cancelar" + }, "notifications": { "delete_warning": "El robot tiene ejecuciones asociadas. Primero elimine las ejecuciones para eliminar el robot", "delete_success": "Robot eliminado exitosamente", @@ -172,6 +178,11 @@ "pagination": "Seleccione cómo puede el robot capturar el resto de la lista", "limit": "Elija el número de elementos a extraer", "complete": "Captura completada" + }, + "actions": { + "text": "Capturar Texto", + "list": "Capturar Lista", + "screenshot": "Capturar Pantalla" } }, "right_panel": { @@ -184,6 +195,7 @@ "confirm_capture": "Confirmar Captura", "confirm_pagination": "Confirmar", "confirm_limit": "Confirmar", + "confirm_reset": "Confirmar", "finish_capture": "Finalizar Captura", "back": "Atrás", "reset": "Reiniciar", @@ -304,6 +316,27 @@ "reset_success": "Vista previa restablecida correctamente" } }, + "interpretation_log": { + "titles": { + "output_preview": "Vista Previa de Datos de Salida", + "screenshot": "Captura de pantalla" + }, + "messages": { + "additional_rows": "Se extraerán filas adicionales de datos una vez que termine la grabación.", + "successful_training": "¡Has entrenado exitosamente al robot para realizar acciones! Haz clic en el botón de abajo para obtener una vista previa de los datos que tu robot extraerá.", + "no_selection": "Parece que aún no has seleccionado nada para extraer. Una vez que lo hagas, el robot mostrará una vista previa de tus selecciones aquí." + }, + "data_sections": { + "binary_received": "---------- Datos binarios de salida recibidos ----------", + "serializable_received": "---------- Datos serializables de salida recibidos ----------", + "mimetype": "tipo MIME: ", + "image_below": "La imagen se muestra a continuación:", + "separator": "--------------------------------------------------" + }, + "notifications": { + "reset_success": "Vista previa restablecida correctamente" + } + }, "recording_page": { "loader": { "browser_startup": "Iniciando el navegador...Mantener apretado" diff --git a/public/locales/ja.json b/public/locales/ja.json index 32ca06547..421bdecfe 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -59,6 +59,12 @@ "delete": "削除", "duplicate": "複製", "search": "ロボットを検索...", + "warning_modal": { + "title": "アクティブなブラウザが検出されました", + "message": "既にブラウザ録画セッションが実行されています。破棄して新しい録画を作成しますか?", + "discard_and_create": "破棄して新規作成", + "cancel": "キャンセル" + }, "notifications": { "delete_warning": "ロボットには関連する実行があります。ロボットを削除するには、まず実行を削除してください", "delete_success": "ロボットが正常に削除されました", @@ -172,6 +178,11 @@ "pagination": "ロボットがリストの残りをどのように取得するか選択してください", "limit": "抽出するアイテムの数を選択してください", "complete": "取得が完了しました" + }, + "actions": { + "text": "テキストを取得", + "list": "リストを取得", + "screenshot": "スクリーンショットを取得" } }, "right_panel": { @@ -184,6 +195,7 @@ "confirm_capture": "取得を確認", "confirm_pagination": "確認", "confirm_limit": "確認", + "confirm_reset": "確認", "finish_capture": "取得を完了", "back": "戻る", "reset": "リセット", @@ -302,6 +314,9 @@ "use_previous": "この操作の条件として前回の選択を使用しますか?", "previous_action": "前回の操作: ", "element_text": "テキスト要素 " + }, + "notifications": { + "reset_success": "出力プレビューが正常にリセットされました" } }, "recording_page": { diff --git a/public/locales/tr.json b/public/locales/tr.json index 6c714fe05..b4118ed00 100644 --- a/public/locales/tr.json +++ b/public/locales/tr.json @@ -314,6 +314,9 @@ "use_previous": "Bu işlem için önceki seçiminizi koşul olarak kullanmak ister misiniz?", "previous_action": "Önceki işleminiz:", "element_text": " metnine sahip öğe" + }, + "notifications": { + "reset_success": "Önizleme başarıyla sıfırlandı" } }, "recording_page": { diff --git a/public/locales/zh.json b/public/locales/zh.json index 6bb6e9b3c..c1e32760f 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -59,6 +59,12 @@ "delete": "删除", "duplicate": "复制", "search": "搜索机器人...", + "warning_modal": { + "title": "检测到活跃浏览器", + "message": "已经有一个浏览器录制会话正在运行。您想要放弃它并创建新的录制吗?", + "discard_and_create": "放弃并创建新的", + "cancel": "取消" + }, "notifications": { "delete_warning": "该机器人有关联的运行记录。请先删除运行记录才能删除机器人", "delete_success": "机器人删除成功", @@ -172,6 +178,11 @@ "pagination": "选择机器人如何捕获列表的其余部分", "limit": "选择要提取的项目数量", "complete": "捕获完成" + }, + "actions": { + "text": "捕获文本", + "list": "捕获列表", + "screenshot": "捕获截图" } }, "right_panel": { @@ -184,6 +195,7 @@ "confirm_capture": "确认捕获", "confirm_pagination": "确认", "confirm_limit": "确认", + "confirm_reset": "确认", "finish_capture": "完成捕获", "back": "返回", "reset": "重置", @@ -302,6 +314,9 @@ "use_previous": "您要将之前的选择用作执行此操作的条件吗?", "previous_action": "您之前的操作是:", "element_text": "在文本元素上 " + }, + "notifications": { + "reset_success": "输出预览已成功重置" } }, "recording_page": { From fbeaa9d8b85b01dfeb1b0da8d0228cf9985579db Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 7 Aug 2025 19:12:22 +0530 Subject: [PATCH 32/39] feat: focused element typing --- .../recorder/DOMBrowserRenderer.tsx | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/components/recorder/DOMBrowserRenderer.tsx b/src/components/recorder/DOMBrowserRenderer.tsx index 09df5a34e..e409ff640 100644 --- a/src/components/recorder/DOMBrowserRenderer.tsx +++ b/src/components/recorder/DOMBrowserRenderer.tsx @@ -611,19 +611,34 @@ export const DOMBrowserRenderer: React.FC = ({ if (!isInCaptureMode && socket && snapshot?.baseUrl) { const iframe = iframeRef.current; if (iframe) { - const iframeRect = iframe.getBoundingClientRect(); - const iframeX = lastMousePosition.x - iframeRect.left; - const iframeY = lastMousePosition.y - iframeRect.top; + const focusedElement = iframeDoc.activeElement as HTMLElement; + let coordinates = { x: 0, y: 0 }; + + if (focusedElement && focusedElement !== iframeDoc.body) { + // Get coordinates from the focused element + const rect = focusedElement.getBoundingClientRect(); + coordinates = { + x: rect.left + rect.width / 2, + y: rect.top + rect.height / 2 + }; + } else { + // Fallback to last mouse position if no focused element + const iframeRect = iframe.getBoundingClientRect(); + coordinates = { + x: lastMousePosition.x - iframeRect.left, + y: lastMousePosition.y - iframeRect.top + }; + } const selector = clientSelectorGenerator.generateSelector( iframeDoc, - { x: iframeX, y: iframeY }, + coordinates, ActionType.Keydown ); const elementInfo = clientSelectorGenerator.getElementInformation( iframeDoc, - { x: iframeX, y: iframeY }, + coordinates, clientSelectorGenerator.getCurrentState().listSelector, clientSelectorGenerator.getCurrentState().getList ); From 7e7b1efc528487879c0a242393b09b4fad809fb4 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 7 Aug 2025 19:23:22 +0530 Subject: [PATCH 33/39] feat: add key press action backend --- server/src/browser-management/inputHandlers.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/src/browser-management/inputHandlers.ts b/server/src/browser-management/inputHandlers.ts index 602807aed..da48f4fd9 100644 --- a/server/src/browser-management/inputHandlers.ts +++ b/server/src/browser-management/inputHandlers.ts @@ -718,6 +718,8 @@ const handleKeyboardAction = async ( } const generator = activeBrowser.generator; + + await page.press(data.selector, data.key); await generator.onDOMKeyboardAction(page, data); logger.log( "debug", From 956e8a6eeee4c52b39a5dab239898cda30b4a4b1 Mon Sep 17 00:00:00 2001 From: Rohit Date: Fri, 8 Aug 2025 14:27:23 +0530 Subject: [PATCH 34/39] feat: add async schedule recording --- src/pages/MainPage.tsx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx index 9aa8d08ec..ed223ba14 100644 --- a/src/pages/MainPage.tsx +++ b/src/pages/MainPage.tsx @@ -235,15 +235,14 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) } }, [runningRecordingName, sockets, ids, debugMessageHandler, user?.id, t, notify, setRerenderRuns, setQueuedRuns, navigate, setContent, setIds]); - const handleScheduleRecording = (settings: ScheduleSettings) => { - scheduleStoredRecording(runningRecordingId, settings) - .then(({ message, runId }: ScheduleRunResponse) => { - if (message === 'success') { - notify('success', t('main_page.notifications.schedule_success', { name: runningRecordingName })); - } else { - notify('error', t('main_page.notifications.schedule_failed', { name: runningRecordingName })); - } - }); + const handleScheduleRecording = async (settings: ScheduleSettings) => { + const { message, runId }: ScheduleRunResponse = await scheduleStoredRecording(runningRecordingId, settings); + if (message === 'success') { + notify('success', t('main_page.notifications.schedule_success', { name: runningRecordingName })); + } else { + notify('error', t('main_page.notifications.schedule_failed', { name: runningRecordingName })); + } + return message === 'success'; } const DisplayContent = () => { From e0707df62f1ce4db2ba6f5a38a2c87e8057265d9 Mon Sep 17 00:00:00 2001 From: Rohit Date: Fri, 8 Aug 2025 14:30:24 +0530 Subject: [PATCH 35/39] feat: update schedule on success --- src/components/robot/ScheduleSettings.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/robot/ScheduleSettings.tsx b/src/components/robot/ScheduleSettings.tsx index 71951646f..7a28d2dce 100644 --- a/src/components/robot/ScheduleSettings.tsx +++ b/src/components/robot/ScheduleSettings.tsx @@ -10,7 +10,7 @@ import { getSchedule, deleteSchedule } from '../../api/storage'; interface ScheduleSettingsProps { isOpen: boolean; - handleStart: (settings: ScheduleSettings) => void; + handleStart: (settings: ScheduleSettings) => Promise; handleClose: () => void; initialSettings?: ScheduleSettings | null; } @@ -272,7 +272,12 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia -