diff --git a/backend/app/agent/factory/browser.py b/backend/app/agent/factory/browser.py index 414d1103..e7aebf93 100644 --- a/backend/app/agent/factory/browser.py +++ b/backend/app/agent/factory/browser.py @@ -36,6 +36,11 @@ from app.service.task import Agents from app.utils.file_utils import get_working_directory +def _get_browser_port(browser: dict) -> int: + """Extract port from a browser config dict, with fallback to env default.""" + return int(browser.get("port", env("browser_port", "9222"))) + + class CdpBrowserPoolManager: """Manages CDP browser pool occupation to ensure parallel tasks use different browsers.""" @@ -87,6 +92,13 @@ class CdpBrowserPoolManager: f"{list(self._occupied_ports.keys())}" ) + def clear_all(self): + """Force-clear all occupied ports (safety net for task cleanup).""" + with self._lock: + count = len(self._occupied_ports) + self._occupied_ports.clear() + return count + def get_occupied_ports(self) -> list[int]: """Get list of currently occupied ports.""" with self._lock: @@ -119,9 +131,7 @@ def browser_agent(options: Chat): options.cdp_browsers, toolkit_session_id ) if selected_browser: - selected_port = selected_browser.get( - "port", env("browser_port", "9222") - ) + selected_port = _get_browser_port(selected_browser) selected_is_external = selected_browser.get("isExternal", False) logger.info( f"Acquired CDP browser from pool (initial): " @@ -129,9 +139,7 @@ def browser_agent(options: Chat): f"session_id={toolkit_session_id}" ) else: - selected_port = options.cdp_browsers[0].get( - "port", env("browser_port", "9222") - ) + selected_port = _get_browser_port(options.cdp_browsers[0]) selected_is_external = options.cdp_browsers[0].get( "isExternal", False ) @@ -259,12 +267,10 @@ def browser_agent(options: Chat): options.cdp_browsers, session_id ) if selected: - agent_instance._cdp_port = selected.get( - "port", env("browser_port", "9222") - ) + agent_instance._cdp_port = _get_browser_port(selected) else: - agent_instance._cdp_port = options.cdp_browsers[0].get( - "port", env("browser_port", "9222") + agent_instance._cdp_port = _get_browser_port( + options.cdp_browsers[0] ) agent_instance._cdp_session_id = session_id logger.info( diff --git a/backend/app/agent/listen_chat_agent.py b/backend/app/agent/listen_chat_agent.py index c3b7da7c..e8da8e0b 100644 --- a/backend/app/agent/listen_chat_agent.py +++ b/backend/app/agent/listen_chat_agent.py @@ -15,6 +15,7 @@ import asyncio import json import logging +import threading from collections.abc import Callable from threading import Event from typing import Any @@ -52,6 +53,8 @@ logger = logging.getLogger("agent") class ListenChatAgent(ChatAgent): + _cdp_clone_lock = threading.Lock() # Protects CDP URL mutation during clone + def __init__( self, api_task_id: str, @@ -700,10 +703,12 @@ class ListenChatAgent(ChatAgent): getattr(self, "_cdp_acquire_callback", None) ) + need_cdp_clone = False if has_cdp and hasattr(self, "_cdp_options"): options = self._cdp_options cdp_browsers = getattr(options, "cdp_browsers", []) if cdp_browsers and hasattr(self, "_browser_toolkit"): + need_cdp_clone = True import uuid as _uuid from app.agent.factory.browser import _cdp_pool_manager @@ -712,32 +717,40 @@ class ListenChatAgent(ChatAgent): selected = _cdp_pool_manager.acquire_browser( cdp_browsers, new_cdp_session ) - from app.component.environment import env + from app.agent.factory.browser import _get_browser_port if selected: - new_cdp_port = selected.get( - "port", env("browser_port", "9222") - ) + new_cdp_port = _get_browser_port(selected) else: - new_cdp_port = cdp_browsers[0].get( - "port", env("browser_port", "9222") - ) + new_cdp_port = _get_browser_port(cdp_browsers[0]) - # Temporarily override the browser toolkit's CDP URL - toolkit = self._browser_toolkit + if need_cdp_clone: + # Temporarily override the browser toolkit's CDP URL. + # Lock prevents concurrent clones from clobbering each + # other's cdp_url on the shared parent toolkit. + toolkit = self._browser_toolkit + with ListenChatAgent._cdp_clone_lock: original_cdp_url = ( toolkit.config_loader.get_browser_config().cdp_url ) toolkit.config_loader.get_browser_config().cdp_url = ( f"http://localhost:{new_cdp_port}" ) - - # Clone tools and collect toolkits that need registration - cloned_tools, toolkits_to_register = self._clone_tools() - - # Restore original CDP URL in parent toolkit - if new_cdp_port is not None and hasattr(self, "_browser_toolkit"): - self._browser_toolkit.config_loader.get_browser_config().cdp_url = original_cdp_url + try: + cloned_tools, toolkits_to_register = ( + self._clone_tools() + ) + except Exception: + _cdp_pool_manager.release_browser( + new_cdp_port, new_cdp_session + ) + raise + finally: + toolkit.config_loader.get_browser_config().cdp_url = ( + original_cdp_url + ) + else: + cloned_tools, toolkits_to_register = self._clone_tools() new_agent = ListenChatAgent( api_task_id=self.api_task_id, diff --git a/backend/app/agent/toolkit/hybrid_browser_toolkit.py b/backend/app/agent/toolkit/hybrid_browser_toolkit.py index 0b510cec..988893d9 100644 --- a/backend/app/agent/toolkit/hybrid_browser_toolkit.py +++ b/backend/app/agent/toolkit/hybrid_browser_toolkit.py @@ -456,7 +456,7 @@ class HybridBrowserToolkit(BaseHybridBrowserToolkit, AbstractToolkit): page_stability_timeout: int | None = None, dom_content_loaded_timeout: int | None = None, viewport_limit: bool = False, - connect_over_cdp: bool = True, + connect_over_cdp: bool = True, # Deprecated: auto-set to True when cdp_url is provided, kept for compatibility cdp_url: str | None = "http://localhost:9222", cdp_keep_current_page: bool = False, full_visual_mode: bool = False, diff --git a/backend/app/utils/single_agent_worker.py b/backend/app/utils/single_agent_worker.py index 6b7de2e4..dd96b832 100644 --- a/backend/app/utils/single_agent_worker.py +++ b/backend/app/utils/single_agent_worker.py @@ -92,7 +92,7 @@ class SingleAgentWorker(BaseSingleAgentWorker): if len(task.content) > 100 else task.content ) - logger.info( + logger.debug( f"[TASK REQUEST] Requesting agent for task_id={task.id}, content_preview='{task_content_preview}'" ) diff --git a/backend/app/utils/workforce.py b/backend/app/utils/workforce.py index b4bf6764..251b6b52 100644 --- a/backend/app/utils/workforce.py +++ b/backend/app/utils/workforce.py @@ -968,7 +968,7 @@ class Workforce(BaseWorkforce): try: from app.agent.factory.browser import _cdp_pool_manager - _cdp_pool_manager._occupied_ports.clear() + _cdp_pool_manager.clear_all() except Exception as e: logger.error(f"[WF-CLEANUP] Error clearing CDP pool: {e}") diff --git a/electron/main/index.ts b/electron/main/index.ts index abca22fd..a757c3ad 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -100,6 +100,19 @@ let cdp_browser_pool: CdpBrowser[] = []; let cdp_browser_processes: Map = new Map(); +/** Remove a non-external browser from the pool by port (used on process error/exit). */ +function removeFromPoolByPort(port: number, reason: string): void { + const idx = cdp_browser_pool.findIndex( + (b) => b.port === port && !b.isExternal + ); + if (idx !== -1) { + const removed = cdp_browser_pool.splice(idx, 1)[0]; + log.warn( + `[CDP POOL] Auto-removed port=${port} (${reason}), id=${removed.id}, pool_size=${cdp_browser_pool.length}` + ); + } +} + // Protocol URL queue for handling URLs before window is ready let protocolUrlQueue: string[] = []; let isWindowReady = false; @@ -424,56 +437,25 @@ function registerIpcHandlers() { // Get all browsers in the pool ipcMain.handle('get-cdp-browsers', () => { - log.info(`[CDP POOL GET] ========================================`); - log.info( - `[CDP POOL GET] Getting CDP browser pool at ${new Date().toISOString()}` - ); - log.info(`[CDP POOL GET] Pool size: ${cdp_browser_pool.length}`); - - if (cdp_browser_pool.length > 0) { - cdp_browser_pool.forEach((b, idx) => { - log.info( - `[CDP POOL GET] Browser ${idx + 1}: port=${b.port}, isExternal=${b.isExternal}, name="${b.name}", id=${b.id}` - ); - }); - } else { - log.warn(`[CDP POOL GET] ⚠️ Pool is EMPTY - no browsers configured`); - } - - log.info(`[CDP POOL GET] ========================================`); + log.debug(`[CDP POOL] GET pool (size=${cdp_browser_pool.length})`); return cdp_browser_pool; }); // Get running browser processes ipcMain.handle('get-running-browser-ports', () => { - const runningPorts = Array.from(cdp_browser_processes.keys()); - log.info(`Getting running browser ports: ${runningPorts.join(', ')}`); - return runningPorts; + return Array.from(cdp_browser_processes.keys()); }); // Add browser to pool ipcMain.handle( 'add-cdp-browser', (event, port: number, isExternal: boolean, name?: string) => { - log.info(`[CDP POOL ADD] ========================================`); - log.info( - `[CDP POOL ADD] Request to add browser at ${new Date().toISOString()}` - ); - log.info(`[CDP POOL ADD] Port: ${port}`); - log.info(`[CDP POOL ADD] Is External: ${isExternal}`); - log.info(`[CDP POOL ADD] Name: "${name}"`); - log.info(`[CDP POOL ADD] Current pool size: ${cdp_browser_pool.length}`); - // Check if browser with this port already exists const existing = cdp_browser_pool.find((b) => b.port === port); if (existing) { log.warn( - `[CDP POOL ADD] ❌ REJECTED - Browser with port ${port} already exists in pool` + `[CDP POOL] ADD rejected: port ${port} already exists (id=${existing.id})` ); - log.warn( - `[CDP POOL ADD] Existing browser: id=${existing.id}, name="${existing.name}"` - ); - log.info(`[CDP POOL ADD] ========================================`); return { success: false, error: 'Browser with this port already exists', @@ -489,13 +471,9 @@ function registerIpcHandlers() { }; cdp_browser_pool.push(newBrowser); - log.info(`[CDP POOL ADD] ✅ SUCCESS - Browser added to pool`); - log.info(`[CDP POOL ADD] Browser ID: ${newBrowser.id}`); - log.info(`[CDP POOL ADD] New pool size: ${cdp_browser_pool.length}`); log.info( - `[CDP POOL ADD] All ports in pool: [${cdp_browser_pool.map((b) => b.port).join(', ')}]` + `[CDP POOL] ADD: port=${port}, isExternal=${isExternal}, id=${newBrowser.id}, pool_size=${cdp_browser_pool.length}` ); - log.info(`[CDP POOL ADD] ========================================`); return { success: true, browser: newBrowser }; } @@ -503,43 +481,30 @@ function registerIpcHandlers() { // Remove browser from pool ipcMain.handle('remove-cdp-browser', (event, browserId: string) => { - log.info(`[CDP POOL REMOVE] ========================================`); - log.info(`[CDP POOL REMOVE] Request to remove browser: ${browserId}`); - const index = cdp_browser_pool.findIndex((b) => b.id === browserId); if (index === -1) { - log.warn(`[CDP POOL REMOVE] ❌ Browser not found: ${browserId}`); - log.info(`[CDP POOL REMOVE] ========================================`); + log.warn(`[CDP POOL] REMOVE: browser not found: ${browserId}`); return { success: false, error: 'Browser not found' }; } const removed = cdp_browser_pool.splice(index, 1)[0]; - log.info( - `[CDP POOL REMOVE] Removed browser: port=${removed.port}, name="${removed.name}"` - ); // If it's a launched browser, kill the process if (!removed.isExternal && cdp_browser_processes.has(removed.port)) { - log.info( - `[CDP POOL REMOVE] Killing launched browser process on port ${removed.port}` - ); try { const process = cdp_browser_processes.get(removed.port); process?.kill(); cdp_browser_processes.delete(removed.port); - log.info(`[CDP POOL REMOVE] Browser process killed successfully`); } catch (error) { log.warn( - `[CDP POOL REMOVE] Failed to kill browser process on port ${removed.port}: ${error}` + `[CDP POOL] Failed to kill browser process on port ${removed.port}: ${error}` ); } } log.info( - `[CDP POOL REMOVE] ✅ SUCCESS - Remaining pool size: ${cdp_browser_pool.length}` + `[CDP POOL] REMOVE: port=${removed.port}, id=${removed.id}, pool_size=${cdp_browser_pool.length}` ); - log.info(`[CDP POOL REMOVE] ========================================`); - return { success: true, browser: removed }; }); @@ -595,11 +560,7 @@ function registerIpcHandlers() { // Launch CDP browser with custom port ipcMain.handle('launch-cdp-browser', async (event, port: number) => { - log.info(`[CDP LAUNCH] ========================================`); - log.info( - `[CDP LAUNCH] Request to launch browser at ${new Date().toISOString()}` - ); - log.info(`[CDP LAUNCH] Target port: ${port}`); + log.info(`[CDP LAUNCH] Launching browser on port ${port}`); try { const platform = process.platform; @@ -782,10 +743,7 @@ function registerIpcHandlers() { // Check if browser on this port is already running if (cdp_browser_processes.has(port)) { - log.warn( - `[CDP LAUNCH] ❌ Browser process already exists on port ${port}` - ); - log.info(`[CDP LAUNCH] ========================================`); + log.warn(`[CDP LAUNCH] Browser process already exists on port ${port}`); return { success: false, error: `Browser already running on port ${port}`, @@ -802,9 +760,7 @@ function registerIpcHandlers() { 'about:blank', ]; - log.info(`[CDP LAUNCH] Spawning Chrome process...`); - log.info(`[CDP LAUNCH] Executable: ${chromeExecutable}`); - log.info(`[CDP LAUNCH] Args: ${args.join(' ')}`); + log.info(`[CDP LAUNCH] Spawning: ${chromeExecutable} on port ${port}`); // Spawn Chrome process const browserProcess = spawn(chromeExecutable, args, { @@ -817,21 +773,7 @@ function registerIpcHandlers() { `[CDP LAUNCH] Browser process error on port ${port}: ${error}` ); cdp_browser_processes.delete(port); - - // Also remove from pool if it was added - const browserInPool = cdp_browser_pool.find( - (b) => b.port === port && !b.isExternal - ); - if (browserInPool) { - const index = cdp_browser_pool.indexOf(browserInPool); - cdp_browser_pool.splice(index, 1); - log.warn( - `[CDP POOL AUTO-REMOVE] Browser on port ${port} removed from pool due to process error` - ); - log.info( - `[CDP POOL AUTO-REMOVE] New pool size: ${cdp_browser_pool.length}` - ); - } + removeFromPoolByPort(port, 'process error'); }); browserProcess.on('exit', (code) => { @@ -839,30 +781,7 @@ function registerIpcHandlers() { `[CDP LAUNCH] Browser process on port ${port} exited with code ${code}` ); cdp_browser_processes.delete(port); - - // Also remove from pool if it was added - const browserInPool = cdp_browser_pool.find( - (b) => b.port === port && !b.isExternal - ); - if (browserInPool) { - const index = cdp_browser_pool.indexOf(browserInPool); - cdp_browser_pool.splice(index, 1); - log.warn( - `[CDP POOL AUTO-REMOVE] Browser on port ${port} removed from pool due to process exit` - ); - log.info(`[CDP POOL AUTO-REMOVE] Exited with code: ${code}`); - log.info( - `[CDP POOL AUTO-REMOVE] Browser ID: ${browserInPool.id}, Name: "${browserInPool.name}"` - ); - log.info( - `[CDP POOL AUTO-REMOVE] New pool size: ${cdp_browser_pool.length}` - ); - if (cdp_browser_pool.length > 0) { - log.info( - `[CDP POOL AUTO-REMOVE] Remaining ports: [${cdp_browser_pool.map((b) => b.port).join(', ')}]` - ); - } - } + removeFromPoolByPort(port, `exit code ${code}`); }); // Store the process in the Map @@ -905,10 +824,6 @@ function registerIpcHandlers() { log.info( `[CDP LAUNCH] ⚠️ NOTE: Browser launched but NOT added to pool yet` ); - log.info( - `[CDP LAUNCH] ⚠️ The UI must call 'add-cdp-browser' to add it to the pool` - ); - log.info(`[CDP LAUNCH] ========================================`); // This is our own launched browser, not external use_external_cdp = false; return { @@ -932,21 +847,22 @@ function registerIpcHandlers() { } // If we get here, browser didn't respond within max wait time + // Kill the orphaned process to avoid resource leak + const proc = cdp_browser_processes.get(port); + if (proc) { + proc.kill(); + cdp_browser_processes.delete(port); + } const totalTime = Date.now() - startTime; log.warn( - `[CDP LAUNCH] ❌ Verification failed after ${totalTime}ms (${attempt} attempts)` + `[CDP LAUNCH] Verification failed after ${totalTime}ms (${attempt} attempts), last error: ${lastError?.code || lastError?.message || 'Unknown'}` ); - log.warn( - `[CDP LAUNCH] Last error: ${lastError?.code || lastError?.message || 'Unknown'}` - ); - log.info(`[CDP LAUNCH] ========================================`); return { success: false, error: `Browser launched but not responding on CDP port after ${totalTime}ms`, }; } catch (error: any) { - log.error(`[CDP LAUNCH] ❌ FAILED to launch browser: ${error}`); - log.info(`[CDP LAUNCH] ========================================`); + log.error(`[CDP LAUNCH] Failed to launch browser: ${error}`); return { success: false, error: error.message, diff --git a/src/pages/Dashboard/Browser.tsx b/src/pages/Dashboard/Browser.tsx index c71e3d18..8507d756 100644 --- a/src/pages/Dashboard/Browser.tsx +++ b/src/pages/Dashboard/Browser.tsx @@ -1,781 +1,849 @@ -import { useState, useEffect } from "react"; -import { Button } from "@/components/ui/button"; -import { Globe, Cookie, Trash2, RefreshCw, RotateCw, Plus, EllipsisVertical, CheckCircle2, XCircle, Loader2 } from "lucide-react"; -import { fetchPost, fetchGet, fetchDelete } from "@/api/http"; -import { useTranslation } from "react-i18next"; -import { toast } from "sonner"; -import AlertDialog from "@/components/ui/alertDialog"; -import { Input } from "@/components/ui/input"; +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +import { fetchDelete, fetchGet, fetchPost } from '@/api/http'; +import AlertDialog from '@/components/ui/alertDialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { + CheckCircle2, + Cookie, + Globe, + Loader2, + Plus, + RefreshCw, + Trash2, + XCircle, +} from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { toast } from 'sonner'; interface CookieDomain { - domain: string; - cookie_count: number; - last_access: string; + domain: string; + cookie_count: number; + last_access: string; } interface GroupedDomain { - mainDomain: string; - subdomains: CookieDomain[]; - totalCookies: number; + mainDomain: string; + subdomains: CookieDomain[]; + totalCookies: number; } interface CdpPortStatus { - checking: boolean; - available: boolean | null; - error?: string; - data?: any; + checking: boolean; + available: boolean | null; + error?: string; + data?: any; } interface CdpBrowser { - id: string; - port: number; - isExternal: boolean; - name?: string; - addedAt: number; + id: string; + port: number; + isExternal: boolean; + name?: string; + addedAt: number; } export default function Browser() { - const { t } = useTranslation(); - const [loginLoading, setLoginLoading] = useState(false); - const [cookiesLoading, setCookiesLoading] = useState(false); - const [cookieDomains, setCookieDomains] = useState([]); - const [deletingDomain, setDeletingDomain] = useState(null); - const [deletingAll, setDeletingAll] = useState(false); - const [showRestartDialog, setShowRestartDialog] = useState(false); - const [cookiesBeforeBrowser, setCookiesBeforeBrowser] = useState(0); - const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + const { t } = useTranslation(); + const [loginLoading, setLoginLoading] = useState(false); + const [cookiesLoading, setCookiesLoading] = useState(false); + const [cookieDomains, setCookieDomains] = useState([]); + const [deletingDomain, setDeletingDomain] = useState(null); + const [deletingAll, setDeletingAll] = useState(false); + const [showRestartDialog, setShowRestartDialog] = useState(false); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); - // CDP port configuration - const [cdpPort, setCdpPort] = useState(9223); - const [customPort, setCustomPort] = useState("9223"); - const [portStatus, setPortStatus] = useState({ - checking: false, - available: null, - }); + // CDP port configuration + const [cdpPort, setCdpPort] = useState(9223); + const [customPort, setCustomPort] = useState('9223'); + const [portStatus, setPortStatus] = useState({ + checking: false, + available: null, + }); - // Dialog states - const [showUseExistingDialog, setShowUseExistingDialog] = useState(false); - const [showLaunchNewDialog, setShowLaunchNewDialog] = useState(false); - const [pendingPort, setPendingPort] = useState(null); + // Dialog states + const [showUseExistingDialog, setShowUseExistingDialog] = useState(false); + const [showLaunchNewDialog, setShowLaunchNewDialog] = useState(false); + const [pendingPort, setPendingPort] = useState(null); - // CDP Browser Pool - const [cdpBrowsers, setCdpBrowsers] = useState([]); - const [deletingBrowser, setDeletingBrowser] = useState(null); - const [runningPorts, setRunningPorts] = useState([]); + // CDP Browser Pool + const [cdpBrowsers, setCdpBrowsers] = useState([]); + const [deletingBrowser, setDeletingBrowser] = useState(null); + const [runningPorts, setRunningPorts] = useState([]); - // Extract main domain (e.g., "aa.bb.cc" -> "bb.cc", "www.google.com" -> "google.com") - const getMainDomain = (domain: string): string => { - // Remove leading dot if present - const cleanDomain = domain.startsWith('.') ? domain.substring(1) : domain; - const parts = cleanDomain.split('.'); + // Extract main domain (e.g., "aa.bb.cc" -> "bb.cc", "www.google.com" -> "google.com") + const getMainDomain = (domain: string): string => { + // Remove leading dot if present + const cleanDomain = domain.startsWith('.') ? domain.substring(1) : domain; + const parts = cleanDomain.split('.'); - // For domains with 2 or fewer parts, return as is - if (parts.length <= 2) { - return cleanDomain; - } + // For domains with 2 or fewer parts, return as is + if (parts.length <= 2) { + return cleanDomain; + } - // For domains with more parts, return last 2 parts (main domain) - return parts.slice(-2).join('.'); - }; + // For domains with more parts, return last 2 parts (main domain) + return parts.slice(-2).join('.'); + }; - // Group domains by main domain - const groupDomainsByMain = (domains: CookieDomain[]): GroupedDomain[] => { - const grouped = new Map(); + // Group domains by main domain + const groupDomainsByMain = (domains: CookieDomain[]): GroupedDomain[] => { + const grouped = new Map(); - domains.forEach(item => { - const mainDomain = getMainDomain(item.domain); - if (!grouped.has(mainDomain)) { - grouped.set(mainDomain, []); - } - grouped.get(mainDomain)!.push(item); - }); + domains.forEach((item) => { + const mainDomain = getMainDomain(item.domain); + if (!grouped.has(mainDomain)) { + grouped.set(mainDomain, []); + } + grouped.get(mainDomain)!.push(item); + }); - return Array.from(grouped.entries()).map(([mainDomain, subdomains]) => ({ - mainDomain, - subdomains, - totalCookies: subdomains.reduce((sum, item) => sum + item.cookie_count, 0) - })).sort((a, b) => a.mainDomain.localeCompare(b.mainDomain)); - }; + return Array.from(grouped.entries()) + .map(([mainDomain, subdomains]) => ({ + mainDomain, + subdomains, + totalCookies: subdomains.reduce( + (sum, item) => sum + item.cookie_count, + 0 + ), + })) + .sort((a, b) => a.mainDomain.localeCompare(b.mainDomain)); + }; - // Auto-load cookies on component mount - useEffect(() => { - handleLoadCookies(); - // Load current browser port on mount - loadCurrentBrowserPort(); - // Load CDP browser pool - loadCdpBrowsers(); - }, []); + // Auto-load cookies on component mount + useEffect(() => { + handleLoadCookies(); + // Load current browser port on mount + loadCurrentBrowserPort(); + // Load CDP browser pool + loadCdpBrowsers(); + }, []); - const loadCurrentBrowserPort = async () => { - if (window.ipcRenderer) { - const port = await window.ipcRenderer.invoke('get-browser-port'); - setCdpPort(port); - setCustomPort(String(port)); - } - }; + const loadCurrentBrowserPort = async () => { + if (window.ipcRenderer) { + const port = await window.ipcRenderer.invoke('get-browser-port'); + setCdpPort(port); + setCustomPort(String(port)); + } + }; - const loadCdpBrowsers = async () => { - if (window.electronAPI?.getCdpBrowsers) { - try { - console.log('[FRONTEND CDP LOAD] Loading CDP browser pool...'); - const browsers = await window.electronAPI.getCdpBrowsers(); - console.log('[FRONTEND CDP LOAD] Loaded browsers:', browsers); - console.log(`[FRONTEND CDP LOAD] Pool size: ${browsers.length}`); - setCdpBrowsers(browsers); + const loadCdpBrowsers = async () => { + if (window.electronAPI?.getCdpBrowsers) { + try { + const browsers = await window.electronAPI.getCdpBrowsers(); + setCdpBrowsers(browsers); - // Also load running browser ports - if (window.electronAPI?.getRunningBrowserPorts) { - const ports = await window.electronAPI.getRunningBrowserPorts(); - console.log('[FRONTEND CDP LOAD] Running browser ports:', ports); - setRunningPorts(ports); - } - } catch (error) { - console.error("[FRONTEND CDP LOAD] Failed to load CDP browsers:", error); - } - } - }; + // Also load running browser ports + if (window.electronAPI?.getRunningBrowserPorts) { + const ports = await window.electronAPI.getRunningBrowserPorts(); + setRunningPorts(ports); + } + } catch (error) { + console.error('Failed to load CDP browsers:', error); + } + } + }; - // Periodically refresh running browser ports - useEffect(() => { - const interval = setInterval(async () => { - if (window.electronAPI?.getRunningBrowserPorts) { - try { - const ports = await window.electronAPI.getRunningBrowserPorts(); - setRunningPorts(ports); - } catch (error) { - console.error("Failed to refresh running ports:", error); - } - } - }, 3000); // Refresh every 3 seconds + // Periodically refresh running browser ports + useEffect(() => { + const interval = setInterval(async () => { + if (window.electronAPI?.getRunningBrowserPorts) { + try { + const ports = await window.electronAPI.getRunningBrowserPorts(); + setRunningPorts(ports); + } catch (error) { + console.error('Failed to refresh running ports:', error); + } + } + }, 3000); // Refresh every 3 seconds - return () => clearInterval(interval); - }, []); + return () => clearInterval(interval); + }, []); - const handleCheckPort = async () => { - const portNumber = parseInt(customPort); + const handleCheckPort = async () => { + const portNumber = parseInt(customPort); - if (isNaN(portNumber) || portNumber < 1 || portNumber > 65535) { - toast.error("Please enter a valid port number (1-65535)"); - return; - } + if (isNaN(portNumber) || portNumber < 1 || portNumber > 65535) { + toast.error('Please enter a valid port number (1-65535)'); + return; + } - setPortStatus({ checking: true, available: null }); + setPortStatus({ checking: true, available: null }); - try { - if (!window.electronAPI?.checkCdpPort) { - toast.error("CDP port check not available"); - setPortStatus({ checking: false, available: false, error: "Not available" }); - return; - } + try { + if (!window.electronAPI?.checkCdpPort) { + toast.error('CDP port check not available'); + setPortStatus({ + checking: false, + available: false, + error: 'Not available', + }); + return; + } - const result = await window.electronAPI.checkCdpPort(portNumber); + const result = await window.electronAPI.checkCdpPort(portNumber); - if (result.available) { - setPortStatus({ - checking: false, - available: true, - data: result.data, - }); - // Browser exists, ask if user wants to use it - setPendingPort(portNumber); - setShowUseExistingDialog(true); - } else { - setPortStatus({ - checking: false, - available: false, - error: result.error, - }); - // No browser on this port, ask if user wants to launch one - setPendingPort(portNumber); - setShowLaunchNewDialog(true); - } - } catch (error: any) { - setPortStatus({ - checking: false, - available: false, - error: error.message, - }); - toast.error(error.message || "Failed to check port"); - } - }; + if (result.available) { + setPortStatus({ + checking: false, + available: true, + data: result.data, + }); + // Browser exists, ask if user wants to use it + setPendingPort(portNumber); + setShowUseExistingDialog(true); + } else { + setPortStatus({ + checking: false, + available: false, + error: result.error, + }); + // No browser on this port, ask if user wants to launch one + setPendingPort(portNumber); + setShowLaunchNewDialog(true); + } + } catch (error: any) { + setPortStatus({ + checking: false, + available: false, + error: error.message, + }); + toast.error(error.message || 'Failed to check port'); + } + }; - const handleUseExistingBrowser = async () => { - setShowUseExistingDialog(false); - if (pendingPort) { - try { - console.log(`[FRONTEND CDP ADD] Attempting to add external browser on port ${pendingPort}`); - // Add browser to pool - if (window.electronAPI?.addCdpBrowser) { - const result = await window.electronAPI.addCdpBrowser(pendingPort, true, `External Browser (${pendingPort})`); - console.log(`[FRONTEND CDP ADD] Result:`, result); - if (result.success) { - console.log(`[FRONTEND CDP ADD] ✅ Successfully added browser ${result.browser.id} on port ${pendingPort}`); - toast.success(`Added external browser on port ${pendingPort} to pool`); - await loadCdpBrowsers(); - } else { - console.error(`[FRONTEND CDP ADD] ❌ Failed to add browser:`, result.error); - toast.error(result.error || "Failed to add browser to pool"); - } - } - } catch (error: any) { - console.error(`[FRONTEND CDP ADD] ❌ Exception:`, error); - toast.error(error.message || "Failed to add browser to pool"); - } - } - setPendingPort(null); - }; + const handleUseExistingBrowser = async () => { + setShowUseExistingDialog(false); + if (pendingPort) { + try { + if (window.electronAPI?.addCdpBrowser) { + const result = await window.electronAPI.addCdpBrowser( + pendingPort, + true, + `External Browser (${pendingPort})` + ); + if (result.success) { + toast.success( + `Added external browser on port ${pendingPort} to pool` + ); + await loadCdpBrowsers(); + } else { + toast.error(result.error || 'Failed to add browser to pool'); + } + } + } catch (error: any) { + toast.error(error.message || 'Failed to add browser to pool'); + } + } + setPendingPort(null); + }; - const handleLaunchNewBrowser = async () => { - setShowLaunchNewDialog(false); + const handleLaunchNewBrowser = async () => { + setShowLaunchNewDialog(false); - if (!pendingPort) { - return; - } + if (!pendingPort) { + return; + } - const port = pendingPort; - setPendingPort(null); + const port = pendingPort; + setPendingPort(null); - try { - if (!window.electronAPI?.launchCdpBrowser) { - toast.error("Launch CDP browser not available"); - return; - } + try { + if (!window.electronAPI?.launchCdpBrowser) { + toast.error('Launch CDP browser not available'); + return; + } - console.log(`[FRONTEND CDP LAUNCH] Launching browser on port ${port}...`); - toast.loading(`Launching browser on port ${port}...`, { id: 'launch-browser' }); + toast.loading(`Launching browser on port ${port}...`, { + id: 'launch-browser', + }); - const result = await window.electronAPI.launchCdpBrowser(port); - console.log(`[FRONTEND CDP LAUNCH] Launch result:`, result); + const result = await window.electronAPI.launchCdpBrowser(port); - if (result.success) { - console.log(`[FRONTEND CDP LAUNCH] ✅ Browser launched successfully on port ${port}`); - toast.success(`Browser launched successfully on port ${port}`, { id: 'launch-browser' }); + if (result.success) { + toast.success(`Browser launched successfully on port ${port}`, { + id: 'launch-browser', + }); - // Add launched browser to pool - console.log(`[FRONTEND CDP LAUNCH] Adding launched browser to pool...`); - if (window.electronAPI?.addCdpBrowser) { - const addResult = await window.electronAPI.addCdpBrowser(port, false, `Launched Browser (${port})`); - console.log(`[FRONTEND CDP LAUNCH] Add to pool result:`, addResult); - if (addResult.success) { - console.log(`[FRONTEND CDP LAUNCH] ✅ Browser added to pool: ${addResult.browser.id}`); - await loadCdpBrowsers(); - } else { - console.error(`[FRONTEND CDP LAUNCH] ❌ Failed to add to pool:`, addResult.error); - toast.error(addResult.error || "Failed to add browser to pool"); - } - } + // Add launched browser to pool + if (window.electronAPI?.addCdpBrowser) { + const addResult = await window.electronAPI.addCdpBrowser( + port, + false, + `Launched Browser (${port})` + ); + if (addResult.success) { + await loadCdpBrowsers(); + } else { + toast.error(addResult.error || 'Failed to add browser to pool'); + } + } - // Update port status - setPortStatus({ - checking: false, - available: true, - data: result.data, - }); - } else { - console.error(`[FRONTEND CDP LAUNCH] ❌ Launch failed:`, result.error); - toast.error(result.error || "Failed to launch browser", { id: 'launch-browser' }); - } - } catch (error: any) { - console.error(`[FRONTEND CDP LAUNCH] ❌ Exception:`, error); - toast.error(error.message || "Failed to launch browser", { id: 'launch-browser' }); - } - }; + // Update port status + setPortStatus({ + checking: false, + available: true, + data: result.data, + }); + } else { + toast.error(result.error || 'Failed to launch browser', { + id: 'launch-browser', + }); + } + } catch (error: any) { + toast.error(error.message || 'Failed to launch browser', { + id: 'launch-browser', + }); + } + }; - const handleRemoveBrowser = async (browserId: string) => { - setDeletingBrowser(browserId); - try { - if (window.electronAPI?.removeCdpBrowser) { - const result = await window.electronAPI.removeCdpBrowser(browserId); - if (result.success) { - toast.success("Browser removed from pool"); - await loadCdpBrowsers(); - } else { - toast.error(result.error || "Failed to remove browser"); - } - } - } catch (error: any) { - toast.error(error.message || "Failed to remove browser"); - } finally { - setDeletingBrowser(null); - } - }; + const handleRemoveBrowser = async (browserId: string) => { + setDeletingBrowser(browserId); + try { + if (window.electronAPI?.removeCdpBrowser) { + const result = await window.electronAPI.removeCdpBrowser(browserId); + if (result.success) { + toast.success('Browser removed from pool'); + await loadCdpBrowsers(); + } else { + toast.error(result.error || 'Failed to remove browser'); + } + } + } catch (error: any) { + toast.error(error.message || 'Failed to remove browser'); + } finally { + setDeletingBrowser(null); + } + }; - const handleBrowserLogin = async () => { - setLoginLoading(true); - try { - // Record current cookie count before opening browser - const currentCookieCount = cookieDomains.reduce((sum, item) => sum + item.cookie_count, 0); - setCookiesBeforeBrowser(currentCookieCount); + const handleBrowserLogin = async () => { + setLoginLoading(true); + try { + const response = await fetchPost('/browser/login'); + if (response) { + toast.success('Browser opened successfully for login'); + // Listen for browser close event to reload cookies + const checkInterval = setInterval(async () => { + try { + // Check if browser is still open by making a request + // When browser closes, reload cookies + const statusResponse = await fetchGet('/browser/status'); + if (!statusResponse || !statusResponse.is_open) { + clearInterval(checkInterval); + await handleLoadCookies(); + // Check if cookies changed + const newResponse = await fetchGet('/browser/cookies'); + if (newResponse && newResponse.success) { + const newDomains = newResponse.domains || []; + const newCookieCount = newDomains.reduce( + (sum: number, item: CookieDomain) => sum + item.cookie_count, + 0 + ); - const response = await fetchPost("/browser/login"); - if (response) { - toast.success("Browser opened successfully for login"); - // Listen for browser close event to reload cookies - const checkInterval = setInterval(async () => { - try { - // Check if browser is still open by making a request - // When browser closes, reload cookies - const statusResponse = await fetchGet("/browser/status"); - if (!statusResponse || !statusResponse.is_open) { - clearInterval(checkInterval); - await handleLoadCookies(); - // Check if cookies changed - const newResponse = await fetchGet("/browser/cookies"); - if (newResponse && newResponse.success) { - const newDomains = newResponse.domains || []; - const newCookieCount = newDomains.reduce((sum: number, item: CookieDomain) => sum + item.cookie_count, 0); + if (newCookieCount > currentCookieCount) { + // Cookies were added, show success toast and restart dialog + const addedCount = newCookieCount - currentCookieCount; + toast.success( + `Added ${addedCount} cookie${addedCount !== 1 ? 's' : ''}` + ); + setHasUnsavedChanges(true); + setShowRestartDialog(true); + } else if (newCookieCount < currentCookieCount) { + // Cookies were deleted (shouldn't happen here, but handle it) + setHasUnsavedChanges(true); + setShowRestartDialog(true); + } + } + } + } catch (error) { + // Browser might be closed + clearInterval(checkInterval); + await handleLoadCookies(); + } + }, 500); // Check every 2 seconds + } + } catch (error: any) { + toast.error(error?.message || 'Failed to open browser'); + } finally { + setLoginLoading(false); + } + }; - if (newCookieCount > currentCookieCount) { - // Cookies were added, show success toast and restart dialog - const addedCount = newCookieCount - currentCookieCount; - toast.success(`Added ${addedCount} cookie${addedCount !== 1 ? 's' : ''}`); - setHasUnsavedChanges(true); - setShowRestartDialog(true); - } else if (newCookieCount < currentCookieCount) { - // Cookies were deleted (shouldn't happen here, but handle it) - setHasUnsavedChanges(true); - setShowRestartDialog(true); - } - } - } - } catch (error) { - // Browser might be closed - clearInterval(checkInterval); - await handleLoadCookies(); - } - }, 500); // Check every 2 seconds - } - } catch (error: any) { - toast.error(error?.message || "Failed to open browser"); - } finally { - setLoginLoading(false); - } - }; + const handleLoadCookies = async () => { + setCookiesLoading(true); + try { + const response = await fetchGet('/browser/cookies'); + if (response && response.success) { + const domains = response.domains || []; + setCookieDomains(domains); + } else { + setCookieDomains([]); + } + } catch (error: any) { + toast.error(error?.message || 'Failed to load cookies'); + setCookieDomains([]); + } finally { + setCookiesLoading(false); + } + }; - const handleLoadCookies = async () => { - setCookiesLoading(true); - try { - const response = await fetchGet("/browser/cookies"); - if (response && response.success) { - const domains = response.domains || []; - setCookieDomains(domains); - } else { - setCookieDomains([]); - } - } catch (error: any) { - toast.error(error?.message || "Failed to load cookies"); - setCookieDomains([]); - } finally { - setCookiesLoading(false); - } - }; + const handleDeleteMainDomain = async ( + mainDomain: string, + subdomains: CookieDomain[] + ) => { + setDeletingDomain(mainDomain); + try { + // Delete all subdomains under this main domain + const deletePromises = subdomains.map((item) => + fetchDelete(`/browser/cookies/${encodeURIComponent(item.domain)}`) + ); + await Promise.all(deletePromises); - const handleDeleteMainDomain = async (mainDomain: string, subdomains: CookieDomain[]) => { - setDeletingDomain(mainDomain); - try { - // Delete all subdomains under this main domain - const deletePromises = subdomains.map(item => - fetchDelete(`/browser/cookies/${encodeURIComponent(item.domain)}`) - ); - await Promise.all(deletePromises); + toast.success(`Deleted cookies for ${mainDomain} and all subdomains`); + // Remove from local state + const domainsToRemove = new Set(subdomains.map((item) => item.domain)); + setCookieDomains((prev) => + prev.filter((item) => !domainsToRemove.has(item.domain)) + ); - toast.success(`Deleted cookies for ${mainDomain} and all subdomains`); - // Remove from local state - const domainsToRemove = new Set(subdomains.map(item => item.domain)); - setCookieDomains(prev => prev.filter(item => !domainsToRemove.has(item.domain))); + // Mark as having unsaved changes + setHasUnsavedChanges(true); + // Show restart dialog after successful deletion + setShowRestartDialog(true); + } catch (error: any) { + toast.error( + error?.message || `Failed to delete cookies for ${mainDomain}` + ); + } finally { + setDeletingDomain(null); + } + }; - // Mark as having unsaved changes - setHasUnsavedChanges(true); - // Show restart dialog after successful deletion - setShowRestartDialog(true); - } catch (error: any) { - toast.error(error?.message || `Failed to delete cookies for ${mainDomain}`); - } finally { - setDeletingDomain(null); - } - }; -4 - const handleDeleteAll = async () => { - setDeletingAll(true); - try { - await fetchDelete("/browser/cookies"); - toast.success("Deleted all cookies"); - setCookieDomains([]); + const handleDeleteAll = async () => { + setDeletingAll(true); + try { + await fetchDelete('/browser/cookies'); + toast.success('Deleted all cookies'); + setCookieDomains([]); - // Mark as having unsaved changes - setHasUnsavedChanges(true); - // Show restart dialog after successful deletion - setShowRestartDialog(true); - } catch (error: any) { - toast.error(error?.message || "Failed to delete all cookies"); - } finally { - setDeletingAll(false); - } - }; + // Mark as having unsaved changes + setHasUnsavedChanges(true); + // Show restart dialog after successful deletion + setShowRestartDialog(true); + } catch (error: any) { + toast.error(error?.message || 'Failed to delete all cookies'); + } finally { + setDeletingAll(false); + } + }; - const handleRestartApp = () => { - if (window.electronAPI && window.electronAPI.restartApp) { - window.electronAPI.restartApp(); - } else { - toast.error("Restart function not available"); - } - }; + const handleRestartApp = () => { + if (window.electronAPI && window.electronAPI.restartApp) { + window.electronAPI.restartApp(); + } else { + toast.error('Restart function not available'); + } + }; - const handleConfirmRestart = () => { - setShowRestartDialog(false); - handleRestartApp(); - }; + const handleConfirmRestart = () => { + setShowRestartDialog(false); + handleRestartApp(); + }; - return ( -
- {/* Restart Dialog */} - setShowRestartDialog(false)} - onConfirm={handleConfirmRestart} - title="Cookies Updated" - message="Cookies have been updated. Would you like to restart the application to use the new cookies?" - confirmText="Yes, Restart" - cancelText="No, Add More" - confirmVariant="information" - /> + return ( +
+ {/* Restart Dialog */} + setShowRestartDialog(false)} + onConfirm={handleConfirmRestart} + title="Cookies Updated" + message="Cookies have been updated. Would you like to restart the application to use the new cookies?" + confirmText="Yes, Restart" + cancelText="No, Add More" + confirmVariant="information" + /> - {/* Use Existing Browser Dialog */} - { - setShowUseExistingDialog(false); - setPendingPort(null); - }} - onConfirm={handleUseExistingBrowser} - title="Browser Found" - message={`A browser is running on port ${pendingPort}. Would you like to use it for browser operations?`} - confirmText="Yes, Use This Browser" - cancelText="Cancel" - confirmVariant="information" - /> + {/* Use Existing Browser Dialog */} + { + setShowUseExistingDialog(false); + setPendingPort(null); + }} + onConfirm={handleUseExistingBrowser} + title="Browser Found" + message={`A browser is running on port ${pendingPort}. Would you like to use it for browser operations?`} + confirmText="Yes, Use This Browser" + cancelText="Cancel" + confirmVariant="information" + /> - {/* Launch New Browser Dialog */} - { - setShowLaunchNewDialog(false); - setPendingPort(null); - }} - onConfirm={handleLaunchNewBrowser} - title="No Browser Found" - message={`No browser is running on port ${pendingPort}. Would you like to launch a new Chrome browser with CDP enabled on this port?`} - confirmText="Yes, Launch Browser" - cancelText="Cancel" - confirmVariant="information" - /> + {/* Launch New Browser Dialog */} + { + setShowLaunchNewDialog(false); + setPendingPort(null); + }} + onConfirm={handleLaunchNewBrowser} + title="No Browser Found" + message={`No browser is running on port ${pendingPort}. Would you like to launch a new Chrome browser with CDP enabled on this port?`} + confirmText="Yes, Launch Browser" + cancelText="Cancel" + confirmVariant="information" + /> - {/* Header Section */} -
-
-
-
-
{t("layout.browser-management")}
-

- {t("layout.browser-management-description")}.

-
-
-
-
- - {/* Content Section */} -
-
+ {/* Header Section */} +
+
+
+
+
+ {t('layout.browser-management')} +
+

+ {t('layout.browser-management-description')}. +

+
+
+
+
-
-
- -
-
{t("layout.browser-cookies")}
-

{t("layout.browser-cookies-description")} -

+ {/* Content Section */} +
+
+
+
+ +
+
+ {t('layout.browser-cookies')} +
+

+ {t('layout.browser-cookies-description')} +

- {/* CDP Port Configuration Section */} -
-
-
-
- CDP Browser Connection -
-

- Connect to a Chrome browser with remote debugging enabled -

-
-
+ {/* CDP Port Configuration Section */} +
+
+
+
+ CDP Browser Connection +
+

+ Connect to a Chrome browser with remote debugging enabled +

+
+
-
-
-
- Current Port: {cdpPort} -
-

- Check if a browser is available on a specific port -

-
+
+
+
+ Current Port:{' '} + + {cdpPort} + +
+

+ Check if a browser is available on a specific port +

+
-
- setCustomPort(e.target.value)} - className="flex-1" - min={1} - max={65535} - /> - -
+
+ setCustomPort(e.target.value)} + className="flex-1" + min={1} + max={65535} + /> + +
- {portStatus.available !== null && ( -
- {portStatus.available ? ( - <> - -
-
- Browser Available -
- {portStatus.data && ( -
- {portStatus.data['Browser']} - {portStatus.data['User-Agent']?.split(' ')[0]} -
- )} -
- - ) : ( - <> - -
-
- Browser Not Available -
-
- {portStatus.error} -
-
- - )} -
- )} -
-
+ {portStatus.available !== null && ( +
+ {portStatus.available ? ( + <> + +
+
+ Browser Available +
+ {portStatus.data && ( +
+ {portStatus.data['Browser']} -{' '} + {portStatus.data['User-Agent']?.split(' ')[0]} +
+ )} +
+ + ) : ( + <> + +
+
+ Browser Not Available +
+
+ {portStatus.error} +
+
+ + )} +
+ )} +
+
- {/* CDP Browser Pool Section */} -
-
-
-
-
- CDP Browser Pool -
- - {runningPorts.length} / {cdpBrowsers.length} Running - -
-

- Manage multiple CDP browsers for task execution -

-
-
+ {/* CDP Browser Pool Section */} +
+
+
+
+
+ CDP Browser Pool +
+ + {runningPorts.length} / {cdpBrowsers.length} Running + +
+

+ Manage multiple CDP browsers for task execution +

+
+
- {cdpBrowsers.length > 0 ? ( -
- {cdpBrowsers.map((browser) => ( -
-
-
- - {browser.name || `Browser ${browser.port}`} - - - {browser.isExternal ? 'External' : 'Launched'} - - {/* Running status indicator */} - {runningPorts.includes(browser.port) ? ( - - - Running - - ) : ( - !browser.isExternal && ( - - - Stopped - - ) - )} -
- - Port: {browser.port} - -
- -
- ))} -
- ) : ( -
- -
- No browsers in pool -
-

- Add browsers using the check port tool above -

-
- )} -
+ {cdpBrowsers.length > 0 ? ( +
+ {cdpBrowsers.map((browser) => ( +
+
+
+ + {browser.name || `Browser ${browser.port}`} + + + {browser.isExternal ? 'External' : 'Launched'} + + {/* Running status indicator */} + {runningPorts.includes(browser.port) ? ( + + + Running + + ) : ( + !browser.isExternal && ( + + + Stopped + + ) + )} +
+ + Port: {browser.port} + +
+ +
+ ))} +
+ ) : ( +
+ +
+ No browsers in pool +
+

+ Add browsers using the check port tool above +

+
+ )} +
- {/* Cookies Section */} -
+ {/* Cookies Section */} +
+
+
+
+ {t('layout.cookie-domains')} +
+ {cookieDomains.length > 0 && ( +
+ {groupDomainsByMain(cookieDomains).length} +
+ )} +
-
-
-
- {t("layout.cookie-domains")} -
- {cookieDomains.length > 0 && ( -
- {groupDomainsByMain(cookieDomains).length} -
- )} -
+
+ {cookieDomains.length > 0 && ( + + )} + + +
+
-
- {cookieDomains.length > 0 && ( - - )} - - -
-
- - {cookieDomains.length > 0 ? ( -
- {groupDomainsByMain(cookieDomains).map((group, index) => ( -
-
- - {group.mainDomain} - - - {group.totalCookies} Cookie{group.totalCookies !== 1 ? 's' : ''} - -
- -
- ))} -
- ) : ( -
- -
- {t("layout.no-cookies-saved-yet")} -
-

- {t("layout.no-cookies-saved-yet-description")} -

-
- )} -
-
- -
- For more information, check out our - {t("layout.privacy-policy")} + {cookieDomains.length > 0 ? ( +
+ {groupDomainsByMain(cookieDomains).map((group, index) => ( +
+
+ + {group.mainDomain} + + + {group.totalCookies} Cookie + {group.totalCookies !== 1 ? 's' : ''} + +
+ +
+ ))} +
+ ) : ( +
+ +
+ {t('layout.no-cookies-saved-yet')} +
+

+ {t('layout.no-cookies-saved-yet-description')} +

+
+ )} +
-
-
-
- ); +
+ For more information, check out our + + {t('layout.privacy-policy')} + +
+
+
+
+ ); }