[
  {
    "path": "README.md",
    "content": "# Enabling cyber investigations from a browser\n**A prototype tool for mapping cyber crime**  \n\nCreated by **@UK_Daniel_Card**  \n\nThe experimental version is the live, bleeeding edge version (Live Demo is largely where I'm storing a relatively stable version... but this is all DEV!)\n\nIF YOU WANT TO REALLY TEST THIS, PLEASE RUN IT LOCALLY WITH A LOCAL CORS PROXY (There is one provided in this repo, an online CORS proxy is planned for the future!)\n\n🔗 [Experimental Version](https://mr-r3b00t.github.io/crime-mapper/experimental_mapper.html)\n\n🔗 [IPInfo Enrichment to CSV](https://mr-r3b00t.github.io/crime-mapper/ipinfo_to_csv.html) \n\n🔗 [Shodan Nrich to CSV](https://mr-r3b00t.github.io/crime-mapper/nrich.html) \n\n✉️ [Email Analysis Tool - Experimental](https://mr-r3b00t.github.io/crime-mapper/header_analysis.html)\n\n✉️ [Email Time Detla Analysis Tool - Experimental](https://mr-r3b00t.github.io/crime-mapper/email_time_delta.html)\n\n🔗 [Live Demo](https://mr-r3b00t.github.io/crime-mapper/crimemapper.html)  \n \n\n### Features  \n- Import and Export to JSON\n- FREE Enrichments with GOOGLE, SHODAN INTERNETDB, HUDSON ROCK (more to come!)\n- Runs locally or via GitHub Pages\n- Includes a POC local CORS proxy - you need to run this using node and run the html file locally as well!\n- Powered by an external JS library and third-party API keys (*BYOK - Bring Your Own Keys*)\n- Be realistic with performance and scale assumptions/expectations - this is an in browser tool, it's purpose is for analysing phishing sites, scamming sites and small attack surfaces. Realistically above 1000 entities and you may have performance issues, the constraints are on the host machine specifications but even on a high end rig you likely will have issues above 4000 entities.\n\n### Built With  \n- GROK3  \n- ChatGPT  \n\n⚠️ **Status:** Very Alpha - use at your own risk!  \n\n### Support the Project  \n☕ [Buy Me a Coffee](https://buymeacoffee.com/mrr3b00t)  \n\n© Xservus Limited  \n\n---\n\n## Explore More Tools  \n- [Gephi](https://gephi.org/features/) - Advanced graph visualization  \n- [Graphviz](https://graphviz.org) - Graph visualization software  \n"
  },
  {
    "path": "analysis.md",
    "content": "# Analysis of experimental_mapper.html\n\n## Overview\n\nThis document provides an assessment of the current state of `experimental_mapper.html` in the Experimental Mapper application, evaluating it against good practices in web development. The analysis focuses on structure, maintainability, performance, and adherence to modern standards.\n\n## Assessment\n\n### 1. Structure and Organization\n- **Current State**: `experimental_mapper.html` is a single monolithic file that combines HTML, CSS, and JavaScript. It serves as the main entry point for the application, containing all UI elements, styles, and logic in one place.\n- **Good Practices**: Modern web development advocates for separation of concerns, where HTML (structure), CSS (presentation), and JavaScript (behavior) are kept in separate files or modules. This enhances readability, maintainability, and scalability.\n- **Assessment**: The current structure does not follow good practices due to the lack of separation. This monolithic approach makes it difficult to manage and update the codebase as the application grows.\n\n### 2. Maintainability\n- **Current State**: All functionality, including UI rendering, data handling, and event logic, is embedded within `experimental_mapper.html`. There are no clear boundaries between different components or concerns.\n- **Good Practices**: Maintainable codebases use modular architectures (e.g., MVC, component-based frameworks) to isolate different functionalities. This allows for easier debugging, testing, and updates.\n- **Assessment**: The file's maintainability is poor. Without modularization, making changes or fixing bugs requires navigating through a large, intertwined codebase, increasing the risk of introducing errors.\n\n### 3. Performance\n- **Current State**: The file includes inline styles and scripts, and it likely performs DOM manipulations directly within the HTML file. There are no apparent optimizations like lazy loading or batch updates mentioned.\n- **Good Practices**: Performance optimization includes minimizing DOM operations, using external stylesheets and scripts, and implementing techniques like debouncing for event handlers. CDNs for libraries can help, but they should be managed efficiently.\n- **Assessment**: Performance is likely suboptimal due to inline content and lack of optimization strategies. Direct DOM manipulation without batching can lead to frequent reflows and repaints, especially in a graph visualization application like this.\n\n### 4. Scalability\n- **Current State**: As a single file, `experimental_mapper.html` handles all aspects of the application, from UI to data processing.\n- **Good Practices**: Scalable applications are built with modular components that can be extended or replaced without affecting the entire system. Using ES6 modules or a framework can facilitate this.\n- **Assessment**: The current state is not scalable. Adding new features or integrating with additional APIs would further complicate the file, making it unwieldy.\n\n### 5. Testability\n- **Current State**: There is no mention of a testing framework or structure within the file. All logic is inline, making it hard to isolate units for testing.\n- **Good Practices**: Code should be structured to allow unit testing, integration testing, and end-to-end testing. Frameworks like QUnit or Jest can be used for JavaScript testing.\n- **Assessment**: Testability is very low. Without separation, it's nearly impossible to write automated tests for individual components or functions.\n\n### 6. Adherence to Standards\n- **Current State**: The file uses HTML, CSS, and JavaScript but does not follow modern standards like semantic HTML or modular JavaScript (ES6 modules).\n- **Good Practices**: Use semantic HTML5 for better accessibility and SEO, CSS preprocessors or methodologies (like BEM) for styling, and ES6+ features for JavaScript.\n- **Assessment**: The adherence to modern web standards is minimal. Updating to use semantic elements and modular JavaScript would improve the codebase significantly.\n\n### 7. Error Handling and Debugging\n- **Current State**: There is no centralized error handling or logging mechanism apparent in the description of the file.\n- **Good Practices**: Implement centralized error handling and logging to track issues and provide meaningful feedback to developers and users.\n- **Assessment**: Error handling and debugging capabilities are likely inadequate, making it harder to diagnose issues in production or development.\n\n## Recommendations\n\n1. **Separation of Concerns**: Break down `experimental_mapper.html` into separate files for HTML, CSS, and JavaScript. Consider adopting an MVC or component-based architecture to organize the codebase.\n2. **Modularization**: Use ES6 modules to split JavaScript logic into manageable pieces. Create components for UI elements to improve reusability and maintainability.\n3. **Performance Optimization**: Implement batch updates for DOM manipulations, use external stylesheets, and consider debouncing for event handlers to reduce performance bottlenecks.\n4. **Testing Framework**: Introduce a testing framework like QUnit to enable unit and integration testing. Structure code to allow mocking of dependencies.\n5. **Modern Standards**: Update HTML to use semantic elements, organize CSS with a methodology like BEM, and leverage modern JavaScript features.\n6. **Error Handling**: Add a centralized error handling mechanism to log errors and provide user feedback, improving debugging and user experience.\n7. **Documentation**: Document the codebase structure, functionality, and usage to aid future development and onboarding.\n\n## Conclusion\n\nThe current state of `experimental_mapper.html` does not align with good practices in web development due to its monolithic structure, lack of separation of concerns, and absence of modern standards and optimizations. Implementing the recommendations above will significantly improve maintainability, scalability, and performance, aligning the Experimental Mapper application with industry best practices. "
  },
  {
    "path": "bugs.md",
    "content": "# Bug Tracking for Experimental Mapper Application\n\n## Overview\n\nThis document is used to track bugs and issues encountered during the development and testing of the Experimental Mapper application. Each bug will be listed with a description, status, and any relevant notes or steps to reproduce.\n\n## Bugs\n\n### Bug 1: UI Not Visible on Loading index.html\n- **Date Reported**: [Current Date]\n- **Status**: Open\n- **Description**: When loading `index.html`, the UI frame is visible, but there are no contents displayed within the frame.\n- **Steps to Reproduce**:\n  1. Open `index.html` in a browser (preferably through a local web server to handle CORS issues).\n  2. Observe that the basic structure or frame of the application loads, but no UI components (like controls, network visualization, etc.) are visible.\n- **Possible Causes**:\n  - JavaScript modules might not be loading due to CORS restrictions if not served through a local server.\n  - Initialization of UI components in `main.js` or related scripts might be failing.\n  - Missing or incorrect DOM elements required by the application components.\n- **Root Cause Analysis**:\n  - After reviewing the console output, all initialization steps in `main.js` are completing successfully, including importing modules, initializing error handler, configuration, model, and controller.\n  - UI components are being initialized as 'placeholders' according to logs (e.g., 'TopBarComponent initialized'), but no content is rendered into the DOM.\n  - The most likely root cause is that the UI components, although initialized, lack the necessary rendering logic to populate the DOM elements (like `#top-bar`, `#controls-panel`, etc.) with content as was done in the original `experimental_mapper.html`.\n  - Additionally, if rendering depends on events like `Controller:StateUpdated`, these events are not being listened to or triggered (as seen with 'EventBus: No listeners for Controller:StateUpdated'), preventing UI updates.\n- **Recommended Actions**:\n  1. Verify that each UI component (`TopBarComponent.js`, `ControlsComponent.js`, etc.) has the necessary logic to render content into their respective DOM elements. If they are placeholders, implement the rendering logic to populate the DOM with UI elements as per the original application.\n  2. Update each component's initialization or rendering method in `AppController.js` or individual component files to log when they attempt to manipulate the DOM, confirming if rendering is attempted and if it fails.\n  3. Ensure that `AppController.js` triggers an initial rendering or state update event (e.g., `Controller:StateUpdated`) after all components are initialized to prompt UI rendering.\n  4. Use browser developer tools to inspect the DOM after page load to check if any content has been added to placeholder elements (`#top-bar`, `#controls-panel`, etc.). If not, confirm the issue is with rendering logic.\n  5. Ensure the application is served through a local web server to handle CORS issues with ES6 modules (e.g., use `python3 -m http.server 8000` and access via `http://localhost:8000/index.html`).\n- **Notes**:\n  - Ensure the file is served via a local web server (e.g., `http-server`, `live-server`, or `python -m http.server 8000`) to handle CORS issues with ES6 modules.\n  - Check browser console for any JavaScript errors that might indicate issues with module loading or initialization.\n  - Console output confirms initialization steps complete, but UI rendering does not occur, pointing to incomplete component rendering logic.\n- **Assigned To**: [To be assigned]\n- **Priority**: High "
  },
  {
    "path": "compare_strings.html",
    "content": "<script type=\"text/javascript\">\n        var gk_isXlsx = false;\n        var gk_xlsxFileLookup = {};\n        var gk_fileData = {};\n        function loadFileData(filename) {\n        if (gk_isXlsx && gk_xlsxFileLookup[filename]) {\n            try {\n                var workbook = XLSX.read(gk_fileData[filename], { type: 'base64' });\n                var firstSheetName = workbook.SheetNames[0];\n                var worksheet = workbook.Sheets[firstSheetName];\n\n                // Convert sheet to JSON to filter blank rows\n                var jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, blankrows: false, defval: '' });\n                // Filter out blank rows (rows where all cells are empty, null, or undefined)\n                var filteredData = jsonData.filter(row =>\n                    row.some(cell => cell !== '' && cell !== null && cell !== undefined)\n                );\n\n                // Convert filtered JSON back to CSV\n                var csv = XLSX.utils.aoa_to_sheet(filteredData); // Create a new sheet from filtered array of arrays\n                csv = XLSX.utils.sheet_to_csv(csv, { header: 1 });\n                return csv;\n            } catch (e) {\n                console.error(e);\n                return \"\";\n            }\n        }\n        return gk_fileData[filename] || \"\";\n        }\n        </script><!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Input Comparison Tool</title>\n    <style>\n        body {\n            font-family: 'Inter', sans-serif;\n            background-color: #f3f4f6;\n            display: flex;\n            justify-content: center;\n            align-items: center;\n            min-height: 100vh;\n            margin: 0;\n            color: #1f2937;\n        }\n        .container {\n            background: white;\n            padding: 2rem;\n            border-radius: 0.5rem;\n            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n            width: 100%;\n            max-width: 400px;\n        }\n        h1 {\n            font-size: 1.5rem;\n            font-weight: 600;\n            margin-bottom: 1.5rem;\n            text-align: center;\n        }\n        .input-group {\n            margin-bottom: 1rem;\n        }\n        label {\n            display: block;\n            font-size: 0.875rem;\n            font-weight: 500;\n            margin-bottom: 0.5rem;\n        }\n        input {\n            width: 100%;\n            padding: 0.75rem;\n            border: 1px solid #d1d5db;\n            border-radius: 0.375rem;\n            font-size: 1rem;\n            transition: border-color 0.2s;\n        }\n        input:focus {\n            outline: none;\n            border-color: #3b82f6;\n            box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);\n        }\n        button {\n            width: 100%;\n            padding: 0.75rem;\n            background-color: #3b82f6;\n            color: white;\n            border: none;\n            border-radius: 0.375rem;\n            font-size: 1rem;\n            font-weight: 500;\n            cursor: pointer;\n            transition: background-color 0.2s;\n        }\n        button:hover {\n            background-color: #2563eb;\n        }\n        #result {\n            margin-top: 1.5rem;\n            text-align: center;\n            font-size: 1rem;\n            font-weight: 500;\n        }\n        .same {\n            color: #16a34a;\n        }\n        .different {\n            color: #dc2626;\n        }\n    </style>\n</head>\n<body>\n    <div class=\"container\">\n        <h1>Compare Inputs</h1>\n        <div class=\"input-group\">\n            <label for=\"input1\">Input 1</label>\n            <input type=\"text\" id=\"input1\" placeholder=\"Enter first input\">\n        </div>\n        <div class=\"input-group\">\n            <label for=\"input2\">Input 2</label>\n            <input type=\"text\" id=\"input2\" placeholder=\"Enter second input\">\n        </div>\n        <button onclick=\"compareInputs()\">Compare</button>\n        <div id=\"result\"></div>\n    </div>\n\n    <script>\n        function compareInputs() {\n            const input1 = document.getElementById('input1').value;\n            const input2 = document.getElementById('input2').value;\n            const resultDiv = document.getElementById('result');\n\n            if (input1 === input2) {\n                resultDiv.textContent = 'Inputs are the same!';\n                resultDiv.className = 'same';\n            } else {\n                resultDiv.textContent = 'Inputs are different!';\n                resultDiv.className = 'different';\n            }\n        }\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "cors_proxy_server.js",
    "content": "const express = require('express');\nconst axios = require('axios');\nconst https = require('https');\nconst url = require('url');\nconst cors = require('cors');\nconst querystring = require('querystring');\n\nconst app = express();\nconst port = 3000;\n\nconst allowList = [\n    'api.shodan.io',\n    'ipinfo.io',\n    'safebrowsing.googleapis.com',\n    'dns.google.com',\n    'api.hudsonrock.com',\n    'cavalier.hudsonrock.com',\n    'internetdb.shodan.io',\n    'api.greynoise.io',\n    'urlscan.io',\n    'api.securitytrails.com',\n    'urlhaus-api.abuse.ch',\n    'api.any.run',\n    'dns.google'\n];\n\napp.use(express.urlencoded({ extended: true }));\napp.use(express.json());\n\n// Use default query parser but log for debugging\napp.set('query parser', (str) => {\n    const parsed = querystring.parse(str);\n    console.log(`[Query Parser] Input: ${str}, Parsed: ${JSON.stringify(parsed)}`);\n    return parsed;\n});\n\napp.use(cors({\n    origin: (origin, callback) => {\n        const allowed = !origin || origin === 'null' || origin === 'http://localhost:3000';\n        console.log(`[CORS] Origin: ${origin} - ${allowed ? 'Allowed' : 'Rejected'}`);\n        if (allowed) {\n            callback(null, true);\n        } else {\n            callback(new Error('Origin not allowed by CORS policy'));\n        }\n    },\n    optionsSuccessStatus: 200\n}));\n\n// Status endpoint\napp.get('/status', (req, res) => {\n    console.log('[Status] Request received');\n    res.json({\n        status: 'running',\n        version: 'fixed-2025-04-12-v6',\n        port: port,\n        allowList: allowList,\n        timestamp: new Date().toISOString()\n    });\n});\n\nfunction validateTargetUrl(req, res, next) {\n    console.log('[Validate] Full Request Details:', {\n        method: req.method,\n        url: req.originalUrl,\n        headers: req.headers,\n        query: req.query,\n        body: req.body,\n        timestamp: new Date().toISOString()\n    });\n\n    // Accurate raw query string\n    const rawQuery = req.originalUrl.includes('?') ? req.originalUrl.split('?')[1] : 'none';\n    console.log(`[Validate] Raw query string: ${rawQuery}`);\n\n    // Reconstruct full URL from req.query\n    let targetUrl = req.query.url;\n    if (!targetUrl) {\n        console.log('[Validate] Rejected: Missing url parameter');\n        return res.status(400).json({ error: 'Missing url parameter' });\n    }\n\n    // Append additional query parameters (e.g., type=MX)\n    const additionalParams = Object.keys(req.query)\n        .filter(key => key !== 'url')\n        .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(req.query[key])}`)\n        .join('&');\n    if (additionalParams) {\n        targetUrl += (targetUrl.includes('?') ? '&' : '?') + additionalParams;\n    }\n\n    console.log(`[Validate] Reconstructed target URL: ${targetUrl}`);\n\n    let parsedUrl;\n    try {\n        parsedUrl = new url.URL(targetUrl);\n    } catch (error) {\n        console.log(`[Validate] Rejected: Invalid URL format - ${targetUrl} - Error: ${error.message}`);\n        return res.status(400).json({ error: 'Invalid URL format' });\n    }\n\n    const hostname = parsedUrl.hostname;\n    if (!allowList.includes(hostname)) {\n        console.log(`[Validate] Rejected: Domain not allowed - ${hostname}`);\n        return res.status(403).json({ error: 'Target domain not in allow list' });\n    }\n\n    req.targetUrl = targetUrl;\n    console.log(`[Validate] Validated: ${targetUrl} (${req.method})`);\n    next();\n}\n\nasync function proxyRequest(req, res) {\n    console.log(`[Proxy] Processing request for: ${req.targetUrl}`);\n    try {\n        const axiosConfig = {\n            method: req.method.toLowerCase(),\n            url: req.targetUrl,\n            headers: {\n                'User-Agent': req.headers['user-agent'] || 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3.1 Safari/605.1.15',\n                'Content-Type': req.headers['content-type'] || 'application/x-www-form-urlencoded',\n                'Accept': req.headers['accept'] || 'application/json'\n            },\n            httpsAgent: new https.Agent({\n                rejectUnauthorized: false\n            }),\n            responseType: 'stream'\n        };\n\n        if (req.method === 'POST' && req.body) {\n            axiosConfig.data = req.body;\n            console.log(`[Proxy] POST data: ${JSON.stringify(req.body)}`);\n        }\n\n        ['api-key', 'key', 'apikey', 'Auth-Key'].forEach(header => {\n            if (req.headers[header.toLowerCase()]) {\n                axiosConfig.headers[header] = req.headers[header.toLowerCase()];\n                console.log(`[Proxy] Forwarding header: ${header}`);\n            }\n        });\n\n        console.log(`[Proxy] Forwarding request to: ${req.targetUrl} with config:`, {\n            method: axiosConfig.method,\n            url: axiosConfig.url,\n            headers: axiosConfig.headers\n        });\n\n        const response = await axios(axiosConfig);\n\n        Object.keys(response.headers).forEach(key => {\n            res.setHeader(key, response.headers[key]);\n        });\n\n        response.data.pipe(res);\n\n        console.log(`[Proxy] Response from ${req.targetUrl}:`, {\n            status: response.status,\n            statusText: response.statusText,\n            headers: response.headers,\n            timestamp: new Date().toISOString()\n        });\n    } catch (error) {\n        console.error(`[Proxy] Error for ${req.targetUrl}:`, error);\n        if (error.response) {\n            console.log(`[Proxy] Error Response from ${req.targetUrl}:`, {\n                status: error.response.status,\n                statusText: error.response.statusText,\n                headers: error.response.headers,\n                timestamp: new Date().toISOString()\n            });\n            error.response.data.pipe(res);\n        } else {\n            console.error(`[Proxy] Non-HTTP error: ${error.message}`);\n            res.status(500).json({\n                error: 'Proxy error',\n                message: error.message,\n                stack: error.stack,\n                request: {\n                    url: req.targetUrl,\n                    method: req.method\n                }\n            });\n        }\n    }\n}\n\napp.all('/proxy', validateTargetUrl, proxyRequest);\n\napp.use((err, req, res, next) => {\n    console.error(`[Server] Error:`, {\n        message: err.message,\n        stack: err.stack,\n        request: {\n            url: req.originalUrl,\n            method: req.method,\n            headers: req.headers,\n            body: req.body\n        },\n        timestamp: new Date().toISOString()\n    });\n    res.status(500).json({\n        error: 'Internal server error',\n        message: err.message,\n        stack: err.stack,\n        request: {\n            url: req.originalUrl,\n            method: req.method,\n            headers: req.headers,\n            body: req.body\n        }\n    });\n});\n\napp.listen(port, () => {\n    console.log(`[Server] CORS Proxy Server running on http://localhost:${port}`);\n    console.log('[Server] Usage:');\n    console.log('[Server]   GET:  http://localhost:3000/proxy?url=<target-url>');\n    console.log('[Server] Allowed domains:', allowList.join(', '));\n});\n"
  },
  {
    "path": "crimemapper.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Baddie Mapper - Experimental</title>\n    <style>\n\n/* Target mx-node class for vis.js nodes */\n.vis-network .vis-node.mx-node {\n    background-color: #60a5fa; /* Blue */\n    border-color: #1e88e5; /* Slightly darker blue border for contrast */\n}\n\n.modal {\n    display: none;\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    background: rgba(0, 0, 0, 0.5);\n    z-index: 2000;\n    overflow: auto;\n}\n\n.modal-content {\n    position: relative;\n    margin: 50px auto;\n    padding: 20px;\n    width: 80%;\n    max-width: 800px;\n    max-height: 60vh; /* Shorter height: 60% of viewport */\n    overflow-y: auto; /* Vertical scroll for content */\n    border-radius: 8px;\n    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);\n    background: var(--modal-bg, #fff);\n    color: var(--modal-color, #1f2a44);\n    border: 1px solid var(--modal-border, #d1d5db);\n    display: flex;\n    flex-direction: column; /* Stack content vertically */\n}\n\n.dark .modal-content {\n    background: #1f2a44;\n    color: #e2e8f0;\n    border-color: #4b5563;\n}\n\n.close-modal {\n    position: absolute;\n    top: 10px;\n    right: 15px;\n    font-size: 24px;\n    cursor: pointer;\n    color: inherit;\n}\n\n.close-modal:hover {\n    color: #ef4444;\n}\n\n#riskTableContainer {\n    overflow-x: auto; /* Horizontal scroll for wide tables */\n    flex-grow: 1; /* Allow table to take available space */\n}\n\ntable {\n    width: 100%;\n    border-collapse: collapse;\n    margin-bottom: 20px;\n}\n\nth, td {\n    padding: 10px;\n    text-align: left;\n    border-bottom: 1px solid var(--table-border, #d1d5db);\n    word-wrap: break-word;\n    max-width: 250px;\n}\n\n.dark th, .dark td {\n    border-bottom-color: #4b5563;\n}\n\nth {\n    background: var(--th-bg, #f1f5f9);\n}\n\n.dark th {\n    background: #2d3748;\n}\n\n.print-button {\n    padding: 10px 20px;\n    background: #22c55e;\n    color: #fff;\n    border: none;\n    border-radius: 4px;\n    cursor: pointer;\n    margin-top: 10px; /* Space above button */\n    align-self: center; /* Center button horizontally */\n}\n\n.print-button:hover {\n    background: #16a34a;\n}\n\n/* CSS Variables */\n:root {\n    --transition: 0.3s;\n    --shadow-light: 0 2px 10px rgba(0, 0, 0, 0.1);\n    --shadow-dark: 0 2px 10px rgba(0, 0, 0, 0.3);\n    --border-light: #d1d5db;\n    --border-dark: #4b5563;\n    --top-bar-height: 40px;\n}\n\n/* Base Styles */\nbody {\n    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\n    margin: 0;\n    padding: 0;\n    display: flex;\n    height: calc(100vh - var(--top-bar-height));\n    font-size: 12px;\n    transition: background-color var(--transition), color var(--transition);\n    margin-top: var(--top-bar-height);\n}\n\nbody.light-mode {\n    background-color: #f0f2f5;\n    color: #1f2a44;\n}\n\nbody.dark-mode {\n    background-color: #1e293b;\n    color: #e2e8f0;\n}\n\n/* Top Bar */\n#top-bar {\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: var(--top-bar-height);\n    background-color: #2d3748;\n    color: #e2e8f0;\n    display: flex;\n    justify-content: space-between; /* Explicitly separate version and links */\n    align-items: center;\n    padding: 0 20px;\n    z-index: 1000;\n    box-shadow: var(--shadow-dark);\n    box-sizing: border-box;\n}\n\n.light-mode #top-bar {\n    background-color: #f0f2f5;\n    color: #1f2a44;\n    box-shadow: var(--shadow-light);\n}\n\n#version {\n    font-size: 14px;\n    font-weight: 500;\n    flex-shrink: 0; /* Prevent shrinking */\n    margin-right: 20px; /* Add space to separate from links */\n}\n\n#top-bar-links {\n    display: flex;\n    align-items: center;\n    gap: 30px; /* Increased spacing between links */\n    /* Fallback absolute positioning */\n    position: absolute;\n    right: 20px;\n    top: 50%;\n    transform: translateY(-50%);\n    /* Diagnostic border to confirm application */\n    border: 1px solid red !important;\n}\n\n#cyberchef-link,\n#github-link {\n    font-size: 14px;\n    color: #60a5fa;\n    text-decoration: none;\n    transition: color var(--transition);\n}\n\n#cyberchef-link:hover,\n#github-link:hover {\n    color: #3b82f6;\n}\n\n.light-mode #cyberchef-link,\n.light-mode #github-link {\n    color: #3b82f6;\n}\n\n.light-mode #cyberchef-link:hover,\n.light-mode #github-link:hover {\n    color: #2563eb;\n}\n/* Controls Panel */\n#controls {\n    position: fixed;\n    top: var(--top-bar-height);\n    left: 0;\n    height: calc(100vh - var(--top-bar-height));\n    z-index: 1000;\n    display: flex;\n    flex-direction: column;\n    overflow-y: auto;\n    transition: width var(--transition);\n    background-color: #fff;\n}\n\n.light-mode #controls {\n    background-color: #fff;\n    box-shadow: var(--shadow-light);\n}\n\n.dark-mode #controls {\n    background-color: #2d3748;\n    box-shadow: var(--shadow-dark);\n    color: #e2e8f0;\n}\n\n#controls.collapsed {\n    width: 50px;\n}\n\n#controls:not(.collapsed) {\n    width: 350px;\n}\n\n#controls-header {\n    padding: 10px;\n    display: flex;\n    flex-direction: column;\n    gap: 10px;\n}\n\n#controls-footer {\n    text-align: center;\n    padding: 10px;\n    font-size: 10px;\n    margin-top: auto;\n    transition: color var(--transition);\n}\n\n.light-mode #controls-footer {\n    color: #6b7280;\n}\n\n.dark-mode #controls-footer {\n    color: #94a3b8;\n}\n\n/* Menu Toggle Button */\n#menu-toggle {\n    padding: 6px 12px;\n    background-color: #3b82f6;\n    color: white;\n    border: none;\n    border-radius: 4px;\n    font-size: 12px;\n    cursor: pointer;\n    transition: background-color var(--transition);\n    width: 100%;\n    box-sizing: border-box;\n}\n\n#controls.collapsed #menu-toggle {\n    width: 100%;\n    display: block;\n}\n\n.light-mode #menu-toggle {\n    background-color: #3b82f6;\n}\n\n#menu-toggle:hover {\n    background-color: #2563eb;\n}\n\n.dark-mode #menu-toggle:hover {\n    background-color: #3b82f6;\n}\n\n/* Top Buttons */\n#mode-toggle,\n#pause-toggle,\n#reset-layout,\n#summary-button {\n    padding: 6px 12px;\n    background-color: #6b7280;\n    color: white;\n    border: none;\n    border-radius: 4px;\n    font-size: 12px;\n    cursor: pointer;\n    transition: background-color var(--transition);\n    width: 100%;\n    box-sizing: border-box;\n}\n\n#mode-toggle:hover,\n#pause-toggle:hover,\n#reset-layout:hover,\n#summary-button:hover {\n    background-color: #4b5563;\n}\n\n.dark-mode #mode-toggle,\n.dark-mode #pause-toggle,\n.dark-mode #reset-layout,\n.dark-mode #summary-button {\n    background-color: #9ca3af;\n}\n\n#pause-toggle.paused {\n    background-color: #ef4444;\n}\n\n#controls.collapsed #mode-toggle,\n#controls.collapsed #pause-toggle,\n#controls.collapsed #reset-layout,\n#controls.collapsed #summary-button,\n#controls.collapsed .tab-buttons,\n#controls.collapsed .tab-content,\n#controls.collapsed #controls-footer {\n    display: none;\n}\n\n/* Tab Navigation */\n.tab-buttons {\n    display: flex;\n    flex-wrap: wrap;\n    border-bottom: 1px solid var(--border-light);\n    transition: border-color var(--transition);\n    padding: 10px 0;\n}\n\n.dark-mode .tab-buttons {\n    border-bottom: 1px solid var(--border-dark);\n}\n\n.tab-button {\n    flex: 1 0 14.28%;\n    padding: 10px;\n    text-align: center;\n    border: none;\n    cursor: pointer;\n    transition: background-color var(--transition), color var(--transition);\n    font-size: 10px;\n}\n\n.light-mode .tab-button {\n    background-color: #f9fafb;\n    color: #1f2a44;\n}\n\n.dark-mode .tab-button {\n    background-color: #374151;\n    color: #e2e8f0;\n}\n\n.tab-button.active {\n    font-weight: bold;\n}\n\n.light-mode .tab-button.active {\n    background-color: #fff;\n    border-bottom: 2px solid #3b82f6;\n}\n\n.dark-mode .tab-button.active {\n    background-color: #2d3748;\n    border-bottom: 2px solid #60a5fa;\n}\n\n.light-mode .tab-button:hover:not(.active) {\n    background-color: #e5e7eb;\n}\n\n.dark-mode .tab-button:hover:not(.active) {\n    background-color: #4b5563;\n}\n\n/* Tab Content */\n.tab-content {\n    padding: 15px;\n    display: none;\n    flex-grow: 1;\n}\n\n.tab-content.active {\n    display: block;\n}\n\n.input-group {\n    margin: 15px 0;\n    padding: 10px;\n    border-radius: 6px;\n    transition: background-color var(--transition);\n}\n\n.light-mode .input-group {\n    background-color: #f9fafb;\n}\n\n.dark-mode .input-group {\n    background-color: #374151;\n}\n\n.input-group h3 {\n    margin: 0 0 8px 0;\n    font-size: 14px;\n}\n\n.light-mode .input-group h3 {\n    color: #1f2a44;\n}\n\n.dark-mode .input-group h3 {\n    color: #e2e8f0;\n}\n\n/* Inputs and Buttons */\ninput,\nselect,\ntextarea {\n    margin: 4px 0;\n    padding: 6px 10px;\n    border-radius: 4px;\n    font-size: 12px;\n    transition: border-color var(--transition), background-color var(--transition), color var(--transition);\n    width: 100%;\n    box-sizing: border-box;\n}\n\n.light-mode input,\n.light-mode select,\n.light-mode textarea {\n    border: 1px solid var(--border-light);\n    background-color: #fff;\n    color: #1f2a44;\n}\n\n.dark-mode input,\n.dark-mode select,\n.dark-mode textarea {\n    border: 1px solid var(--border-dark);\n    background-color: #4b5563;\n    color: #e2e8f0;\n}\n\ntextarea {\n    height: 100px;\n    resize: vertical;\n}\n\nbutton {\n    padding: 6px 12px;\n    border: none;\n    border-radius: 4px;\n    font-size: 12px;\n    cursor: pointer;\n    transition: background-color var(--transition), color var(--transition);\n    width: 100%;\n    margin: 4px 0;\n}\n\n.light-mode button {\n    background-color: #3b82f6;\n    color: white;\n}\n\n.dark-mode button {\n    background-color: #60a5fa;\n    color: #1e293b;\n}\n\n/* Network Container */\n#myNetwork {\n    flex-grow: 1;\n    height: calc(100vh - var(--top-bar-height));\n    border-radius: 0 8px 8px 0;\n    box-shadow: var(--shadow-light);\n    transition: margin-left var(--transition), margin-right var(--transition);\n    position: relative;\n}\n\n.light-mode #myNetwork {\n    background-color: #fff;\n}\n\n.dark-mode #myNetwork {\n    background-color: #334155;\n    box-shadow: var(--shadow-dark);\n}\n\n#controls:not(.collapsed) ~ #myNetwork {\n    margin-left: 300px;\n}\n\n#controls.collapsed ~ #myNetwork {\n    margin-left: 50px;\n}\n\n/* Properties Panel */\n#properties-panel {\n    position: fixed;\n    top: var(--top-bar-height);\n    right: -350px; /* Ensure it's fully off-screen */\n    width: 350px;\n    height: calc(100vh - var(--top-bar-height));\n    background-color: #fff;\n    box-shadow: -2px 0 10px rgba(0, 0, 0, 0.2);\n    z-index: 2000;\n    padding: 20px;\n    overflow-y: auto;\n    transition: right var(--transition);\n    display: none; /* Hidden by default */\n    box-sizing: border-box;\n}\n\n#properties-panel.active {\n    right: 0;\n    display: block; /* Show when active */\n}\n\n.dark-mode #properties-panel {\n    background-color: #2d3748;\n    box-shadow: -2px 0 10px rgba(0, 0, 0, 0.4);\n    color: #e2e8f0;\n}\n\n\n#properties-panel .close-button {\n    position: absolute;\n    top: 10px;\n    right: 10px;\n    background: none;\n    border: none;\n    font-size: 18px;\n    cursor: pointer;\n    color: #1f2a44;\n    transition: color var(--transition);\n}\n\n.dark-mode #properties-panel .close-button {\n    color: #e2e8f0;\n}\n\n#properties-panel.active ~ #myNetwork {\n    margin-right: 300px;\n}\n\n#properties-panel.active ~ #controls:not(.collapsed) ~ #myNetwork {\n    margin-left: 300px;\n    margin-right: 300px;\n}\n\n#properties-panel.active ~ #controls.collapsed ~ #myNetwork {\n    margin-left: 50px;\n    margin-right: 300px;\n}\n\n/* Notes Modal */\n#notes-modal {\n    display: none;\n    position: fixed;\n    top: 50%;\n    left: 50%;\n    transform: translate(-50%, -50%);\n    background-color: #fff;\n    padding: 20px;\n    border-radius: 8px;\n    box-shadow: var(--shadow-light);\n    z-index: 2000;\n    width: 500px;\n    max-width: 90vw;\n    max-height: 80vh;\n    overflow-y: auto;\n    box-sizing: border-box;\n}\n\n.dark-mode #notes-modal {\n    background-color: #2d3748;\n    box-shadow: var(--shadow-dark);\n    color: #e2e8f0;\n}\n\n#modal-overlay {\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100vw;\n    height: 100vh;\n    background: rgba(0, 0, 0, 0.5);\n    z-index: 1999;\n}\n\n#notes-modal h3 {\n    margin: 0 0 15px 0;\n    font-size: 16px;\n}\n\n#notes-textarea {\n    width: 100%;\n    height: 300px;\n    padding: 10px;\n    border-radius: 4px;\n    border: 1px solid var(--border-light);\n    background-color: #fff;\n    color: #1f2a44;\n    font-size: 12px;\n    resize: vertical;\n    box-sizing: border-box;\n    overflow-y: auto;\n}\n\n.dark-mode #notes-textarea {\n    border: 1px solid var(--border-dark);\n    background-color: #4b5563;\n    color: #e2e8f0;\n}\n\n#notes-modal .button-container {\n    margin-top: 15px;\n    display: flex;\n    justify-content: flex-end;\n    gap: 10px;\n}\n\n#notes-modal button {\n    padding: 6px 12px;\n    border-radius: 4px;\n    border: none;\n    cursor: pointer;\n    font-size: 12px;\n    transition: background-color var(--transition);\n}\n\n#notes-save {\n    background-color: #3b82f6;\n    color: white;\n}\n\n#notes-cancel {\n    background-color: #6b7280;\n    color: white;\n}\n\n#notes-save:hover {\n    background-color: #2563eb;\n}\n\n#notes-cancel:hover {\n    background-color: #4b5563;\n}\n\n.dark-mode #notes-save {\n    background-color: #60a5fa;\n    color: #1e293b;\n}\n\n.dark-mode #notes-cancel {\n    background-color: #9ca3af;\n}\n\n/* Summary Modal */\n#summary-modal {\n    display: none;\n    position: fixed;\n    top: 50%;\n    left: 50%;\n    transform: translate(-50%, -50%);\n    background-color: #fff;\n    padding: 20px;\n    border-radius: 8px;\n    box-shadow: var(--shadow-light);\n    z-index: 2000;\n    max-width: 500px;\n    width: 90%;\n    max-height: 80vh;\n    overflow-y: auto;\n}\n\n.toastify {\n    top: calc(var(--top-bar-height) + 10px) !important;\n    z-index: 3000 !important;\n}\n\n.dark-mode #summary-modal {\n    background-color: #2d3748;\n    box-shadow: var(--shadow-dark);\n    color: #e2e8f0;\n}\n\n#summary-modal table {\n    width: 100%;\n    border-collapse: collapse;\n    margin-top: 10px;\n}\n\n#summary-modal th,\n#summary-modal td {\n    padding: 8px;\n    text-align: left;\n    border-bottom: 1px solid var(--border-light);\n}\n\n.dark-mode #summary-modal th,\n.dark-mode #summary-modal td {\n    border-bottom: 1px solid var(--border-dark);\n}\n\n#summary-modal th {\n    background-color: #f9fafb;\n}\n\n.dark-mode #summary-modal th {\n    background-color: #374151;\n}\n\n#summary-modal .close-button {\n    float: right;\n    background: none;\n    border: none;\n    font-size: 16px;\n    cursor: pointer;\n    color: #1f2a44;\n}\n\n.dark-mode #summary-modal .close-button {\n    color: #e2e8f0;\n}\n\n/* Progress Bar */\n#progress-bar {\n    position: fixed;\n    top: var(--top-bar-height);\n    left: 0;\n    width: 100%;\n    padding: 10px;\n    text-align: center;\n    color: white;\n    font-size: 14px;\n    font-weight: bold;\n    z-index: 2000;\n    transition: opacity 0.5s ease-in-out;\n}\n\n.progress-active {\n    background-color: #dc2626;\n}\n\n.progress-complete {\n    background-color: #22c55e;\n}\n\n.progress-hidden {\n    opacity: 0;\n    pointer-events: none;\n}\n\n/* Context Menus */\n#contextMenu,\n#edgeContextMenu {\n    position: absolute;\n    background-color: #fff;\n    border: 1px solid #ccc;\n    box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);\n    z-index: 2000;\n    padding: 5px 0;\n}\n\n#contextMenu button,\n#edgeContextMenu button {\n    display: block;\n    width: 100%;\n    text-align: left;\n    padding: 5px 10px;\n    background: none;\n    border: none;\n    cursor: pointer;\n    color: #1f2a44;\n}\n\n#contextMenu button:hover,\n#edgeContextMenu button:hover {\n    background-color: #f0f0f0;\n}\n\n.dark-mode #contextMenu,\n.dark-mode #edgeContextMenu {\n    background-color: #2d3748;\n    border: 1px solid var(--border-dark);\n}\n\n.dark-mode #contextMenu button,\n.dark-mode #edgeContextMenu button {\n    color: #e2e8f0;\n}\n\n.dark-mode #contextMenu button:hover,\n.dark-mode #edgeContextMenu button:hover {\n    background-color: #4b5563;\n}\n\n/* Network Visualization */\n#myNetwork .vis-network canvas {\n    overflow: visible; /* Change from hidden to visible */\n}\n\n.vis-network .vis-label {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    text-align: center;\n    padding: 0;\n    margin: 0;\n    overflow: hidden;\n    white-space: normal;\n    max-width: 100%;\n}\n\n.vis-network .vis-node {\n    min-width: 40px;\n    min-height: 40px;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n}\n\n/* Password Toggle */\n.password-container {\n    display: flex;\n    flex-direction: column; /* Stack children vertically */\n    width: 100%;\n    margin-bottom: 8px;\n    gap: 4px; /* Add space between input and button */\n}\n\n/* Inputs - Remove padding-right */\n.password-container input {\n    padding: 6px 10px; /* Adjusted from padding-right: 60px to standard padding */\n}\n\n/* Password Toggle */\n.toggle-password {\n    /* Remove absolute positioning */\n    padding: 2px 8px;\n    background-color: #6b7280;\n    color: white;\n    border: none;\n    border-radius: 4px;\n    font-size: 10px;\n    cursor: pointer;\n    width: auto; /* Let it size naturally */\n    align-self: flex-start; /* Align to the left */\n}\n\n.light-mode .toggle-password {\n    background-color: #6b7280;\n}\n\n.dark-mode .toggle-password {\n    background-color: #9ca3af;\n}\n\n.toggle-password:hover {\n    background-color: #4b5563;\n}\n/* Stop Task Button */\n#stop-task {\n    padding: 6px 12px;\n    background-color: #dc2626;\n    color: white;\n    border: none;\n    border-radius: 4px;\n    font-size: 12px;\n    cursor: pointer;\n    transition: background-color var(--transition);\n    width: auto;\n    margin: 0 auto;\n    display: inline-block;\n}\n\n#stop-task:hover {\n    background-color: #b91c1c;\n}\n\n#stop-task:disabled {\n    background-color: #9ca3af;\n    cursor: not-allowed;\n}\n\n/* Search Input */\n#search-input {\n    padding: 6px 10px;\n    border-radius: 4px;\n    border: 1px solid var(--border-light);\n    background-color: #fff;\n    color: #1f2a44;\n    transition: border-color var(--transition);\n}\n\n.dark-mode #search-input {\n    border: 1px solid var(--border-dark);\n    background-color: #4b5563;\n    color: #e2e8f0;\n}\n\n#search-input:focus {\n    outline: none;\n    border-color: #3b82f6;\n}\n\n/* Checkbox Labels */\n.checkbox-label {\n    display: flex;\n    align-items: center;\n    margin: 4px 0;\n    font-size: 12px;\n}\n\n.light-mode .checkbox-label {\n    color: #1f2a44;\n}\n\n.dark-mode .checkbox-label {\n    color: #e2e8f0;\n}\n\ninput[type=\"checkbox\"] {\n    margin-right: 8px;\n    width: auto;\n}\n\n/* Media Queries */\n@media (max-width: 768px) {\n    #controls:not(.collapsed) {\n        width: 100%;\n    }\n    \n    #controls:not(.collapsed) ~ #myNetwork {\n        margin-left: 0;\n        display: none;\n    }\n    \n    #controls.collapsed ~ #myNetwork {\n        margin-left: 50px;\n        display: block;\n    }\n    \n    #properties-panel.active ~ #myNetwork {\n        margin-right: 300px;\n    }\n}\n\n#controls.collapsed #manual-save,\n#controls.collapsed #buy-me-a-coffee-container,\n#controls.collapsed #stop-task-container {\n    display: none;\n}\n</style>\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/toastify-js/1.12.0/toastify.min.css\">\n    <script src=\"https://cdn.jsdelivr.net/npm/vis-network@9.1.9/dist/vis-network.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/toastify-js/1.12.0/toastify.min.js\"></script>  \n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.3/jspdf.plugin.autotable.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js\"></script>\n</head>\n<body class=\"dark-mode\">\n    <div id=\"top-bar\">\n        <span id=\"version\">Baddie Mapper Experimental</span>\n        <a href=\"https://github.com/mr-r3b00t/crime-mapper\" target=\"_blank\" id=\"github-link\">GitHub Repo</a>\n        <a href=\"https://gchq.github.io/CyberChef\" target=\"_blank\" id=\"cyberchef-link\">GCHQ CyberChef</a>\n    </div>\n    <div id=\"notes-modal\">\n        <h3>Edit Node Notes</h3>\n        <textarea id=\"notes-textarea\" placeholder=\"Enter notes here (max 1000 characters)\"></textarea>\n        <div class=\"button-container\">\n            <button id=\"notes-save\" onclick=\"saveNodeNotes()\">Save</button>\n            <button id=\"notes-cancel\" onclick=\"hideNotesModal()\">Cancel</button>\n        </div>\n    </div>\n        <div id=\"progress-bar\" class=\"progress-hidden\">Task in progress...</div>\n        <!-- Rest of your HTML -->\n\n      \n\n    <div id=\"controls\">\n        <button id=\"menu-toggle\" onclick=\"toggleMenu()\" title=\"Toggle Menu\">></button>\n        <button id=\"mode-toggle\" onclick=\"toggleMode()\">Switch to Light Mode</button>\n        <button id=\"pause-toggle\" onclick=\"togglePhysics()\">Pause Physics</button>\n        <button id=\"reset-layout\" onclick=\"resetLayout()\">Reset Layout</button>\n        <div class=\"tab-buttons\">\n            <button class=\"tab-button\" onclick=\"showTab('object-management')\">Object Management</button>\n            <button class=\"tab-button\" onclick=\"showTab('link-management')\">Link Management</button>\n            <button class=\"tab-button\" onclick=\"showTab('import-export')\">Import/Export</button>\n            <button class=\"tab-button\" onclick=\"showTab('api-keys')\">Config</button>\n            <button class=\"tab-button\" onclick=\"showTab('enrichment')\">Enrichment</button>\n            <button class=\"tab-button active\" onclick=\"showTab('import-iocs')\">Import IOCs</button>\n            <button class=\"tab-button\" onclick=\"showTab('layouts')\">Layouts</button>\n            <button class=\"tab-button\" onclick=\"showTab('search')\">Search</button>\n        </div>\n        <div id=\"object-management\" class=\"tab-content\">\n            <div class=\"input-group\">\n                <h3>Add Entity</h3>\n                <select id=\"addEntityType\">\n                    <option value=\"contact\">Contact</option>\n                    <option value=\"ip\">IP Address</option>\n                    <option value=\"domain\">Domain</option>\n                    <option value=\"organization\">Organization</option>\n                    <option value=\"port\">Port</option>\n                    <option value=\"wallet\">Wallet</option>\n                    <option value=\"bank\">Bank Account</option>\n                    <option value=\"technology\">Technology</option>\n                    <option value=\"device\">Device</option>\n                    <option value=\"malware\">Malware</option>\n                    <option value=\"vulnerability\">Vulnerability</option>\n                    <option value=\"subnet\">Subnet</option> <!-- New option -->\n                </select>\n                <input type=\"text\" id=\"addVulnNameInput\" placeholder=\"Vulnerability Name\" style=\"display: none;\">\n                <input type=\"text\" id=\"addVulnCVEInput\" placeholder=\"CVE (optional)\" style=\"display: none;\">\n                <input type=\"text\" id=\"addVulnUrlInput\" placeholder=\"URL (optional)\" style=\"display: none;\">\n                <input type=\"text\" id=\"addNameInput\" placeholder=\"Name\">\n                <input type=\"email\" id=\"addEmailInput\" placeholder=\"Email (optional)\">\n                <input type=\"text\" id=\"addIpInput\" placeholder=\"IP Address\" style=\"display: none;\">\n                <input type=\"text\" id=\"addDomainInput\" placeholder=\"Domain\" style=\"display: none;\">\n                <input type=\"text\" id=\"addOrgInput\" placeholder=\"Organization Name\" style=\"display: none;\">\n                <input type=\"text\" id=\"addSubnetInput\" placeholder=\"Subnet (e.g., 192.168.1.0/24)\" style=\"display: none;\">\n                <input type=\"text\" id=\"addPortNumInput\" placeholder=\"Port Number\" style=\"display: none;\">\n                <select id=\"addPortType\" style=\"display: none;\">\n                    <option value=\"TCP\">TCP</option>\n                    <option value=\"UDP\">UDP</option>\n                </select>\n                <input type=\"text\" id=\"addWalletAddressInput\" placeholder=\"Wallet Address\" style=\"display: none;\">\n                <input type=\"text\" id=\"addAccountNumberInput\" placeholder=\"Account Number\" style=\"display: none;\">\n                <input type=\"text\" id=\"addSortCodeInput\" placeholder=\"Sort Code\" style=\"display: none;\">\n                <input type=\"text\" id=\"addTechNameInput\" placeholder=\"Technology Name\" style=\"display: none;\">\n                <input type=\"text\" id=\"addTechVersionInput\" placeholder=\"Version\" style=\"display: none;\">\n                <select id=\"addDeviceCategory\" style=\"display: none;\">\n                    <option value=\"Server\">Server</option>\n                    <option value=\"PC\">PC</option>\n                    <option value=\"Laptop\">Laptop</option>\n                    <option value=\"MAC\">MAC</option>\n                    <option value=\"SmartPhone\">SmartPhone</option>\n                    <option value=\"IOT\">IOT</option>\n                    <option value=\"Router\">Router</option>\n                    <option value=\"Switch\">Switch</option>\n                    <option value=\"Wireless Access Point\">Wireless Access Point</option>\n                    <option value=\"Other\">Other</option>\n                </select>\n                <input type=\"text\" id=\"addDeviceNameInput\" placeholder=\"Device Name\" style=\"display: none;\">\n                <input type=\"text\" id=\"addMalwareNameInput\" placeholder=\"Malware Name\" style=\"display: none;\">\n                <select id=\"addMalwareType\" style=\"display: none;\">\n                    <option value=\"Wiper\">Wiper</option>\n                    <option value=\"RAT\">RAT</option>\n                    <option value=\"Encryptor\">Encryptor</option>\n                    <option value=\"Stealer\">Stealer</option>\n                    <option value=\"Other\">Other</option>\n                </select>\n                <button onclick=\"addNode()\">Add Entity</button>\n            </div>\n            <div class=\"input-group\">\n                <h3>Edit Entity</h3>\n                <select id=\"editNodeSelect\" onchange=\"loadNodeForEdit()\"></select>\n                <select id=\"editEntityType\" disabled>\n                    <option value=\"contact\">Contact</option>\n                    <option value=\"ip\">IP Address</option>\n                    <option value=\"domain\">Domain</option>\n                    <option value=\"organization\">Organization</option>\n                    <option value=\"port\">Port</option>\n                    <option value=\"wallet\">Wallet</option>\n                    <option value=\"bank\">Bank Account</option>\n                    <option value=\"technology\">Technology</option>\n                    <option value=\"device\">Device</option>\n                    <option value=\"malware\">Malware</option>\n                    <option value=\"vulnerability\">Vulnerability</option>\n                </select>\n                <select id=\"editEntityType\" disabled>\n                    <!-- Existing options -->\n                    <option value=\"subnet\">Subnet</option>\n                </select>\n                <input type=\"text\" id=\"editSubnetInput\" placeholder=\"Subnet (e.g., 192.168.1.0/24)\" style=\"display: none;\">\n                <input type=\"text\" id=\"editVulnNameInput\" placeholder=\"Vulnerability Name\" style=\"display: none;\">\n                <input type=\"text\" id=\"editVulnCVEInput\" placeholder=\"CVE (optional)\" style=\"display: none;\">\n                <input type=\"text\" id=\"editVulnUrlInput\" placeholder=\"URL (optional)\" style=\"display: none;\">\n                <input type=\"text\" id=\"editNameInput\" placeholder=\"Name\">\n                <input type=\"email\" id=\"editEmailInput\" placeholder=\"Email (optional)\">\n                <input type=\"text\" id=\"editIpInput\" placeholder=\"IP Address\" style=\"display: none;\">\n                <input type=\"text\" id=\"editDomainInput\" placeholder=\"Domain\" style=\"display: none;\">\n                <input type=\"text\" id=\"editOrgInput\" placeholder=\"Organization Name\" style=\"display: none;\">\n                <input type=\"text\" id=\"editPortNumInput\" placeholder=\"Port Number\" style=\"display: none;\">\n                <select id=\"editPortType\" style=\"display: none;\">\n                    <option value=\"TCP\">TCP</option>\n                    <option value=\"UDP\">UDP</option>\n                </select>\n                <input type=\"text\" id=\"editWalletAddressInput\" placeholder=\"Wallet Address\" style=\"display: none;\">\n                <input type=\"text\" id=\"editAccountNumberInput\" placeholder=\"Account Number\" style=\"display: none;\">\n                <input type=\"text\" id=\"editSortCodeInput\" placeholder=\"Sort Code\" style=\"display: none;\">\n                <input type=\"text\" id=\"editTechNameInput\" placeholder=\"Technology Name\" style=\"display: none;\">\n                <input type=\"text\" id=\"editTechVersionInput\" placeholder=\"Version\" style=\"display: none;\">\n                <select id=\"editDeviceCategory\" style=\"display: none;\">\n                    <option value=\"Server\">Server</option>\n                    <option value=\"PC\">PC</option>\n                    <option value=\"Laptop\">Laptop</option>\n                    <option value=\"MAC\">MAC</option>\n                    <option value=\"SmartPhone\">SmartPhone</option>\n                    <option value=\"IOT\">IOT</option>\n                    <option value=\"Router\">Router</option>\n                    <option value=\"Switch\">Switch</option>\n                    <option value=\"Wireless Access Point\">Wireless Access Point</option>\n                    <option value=\"Other\">Other</option>\n                </select>\n                <input type=\"text\" id=\"editDeviceNameInput\" placeholder=\"Device Name\" style=\"display: none;\">\n                <input type=\"text\" id=\"editMalwareNameInput\" placeholder=\"Malware Name\" style=\"display: none;\">\n                <select id=\"editMalwareType\" style=\"display: none;\">\n                    <option value=\"Wiper\">Wiper</option>\n                    <option value=\"RAT\">RAT</option>\n                    <option value=\"Encryptor\">Encryptor</option>\n                    <option value=\"Stealer\">Stealer</option>\n                    <option value=\"Other\">Other</option>\n                </select>\n                <button onclick=\"editNode()\">Save Changes</button>\n            </div>\n            <div class=\"input-group\">\n                <h3>Remove Entity</h3>\n                <select id=\"removeNode\"></select>\n                <button onclick=\"removeNode()\">Remove Entity</button>\n            </div>\n        </div>\n        <div id=\"link-management\" class=\"tab-content\">\n            <div class=\"input-group\">\n                <h3>Create Link</h3>\n                <select id=\"fromNode\"></select>\n                <select id=\"toNode\"></select>\n                <input type=\"text\" id=\"edgeLabel\" placeholder=\"Link Label\">\n                <button onclick=\"addEdge()\">Add Link</button>\n            </div>\n            <div class=\"input-group\">\n                <h3>Remove Link</h3>\n                <select id=\"removeEdge\"></select>\n                <button onclick=\"removeEdge()\">Remove Link</button>\n            </div>\n        </div>\n        <div id=\"import-export\" class=\"tab-content\">\n            <div class=\"input-group\">\n                <h3>Export/Import</h3>\n                <button onclick=\"exportGraph()\">Export to JSON</button>\n                <button onclick=\"exportVisibleGraph()\">Export Visible to JSON</button>\n                <!-- Removed: <input type=\"file\" id=\"importFile\" accept=\".json\"> -->\n                <button onclick=\"importGraph()\">Import from JSON</button>\n                <button onclick=\"clearGraph()\">Clear Graph</button>\n                <button id=\"summary-button\" onclick=\"showGraphSummary()\">Graph Summary</button>\n                <button onclick=\"exportToPNG()\">Export to PNG</button>\n                <button onclick=\"exportToPDF()\">Export to PDF</button>\n                <button onclick=\"exportConfigBackup()\">Backup Config</button> <!-- New Button -->\n                <button onclick=\"importConfig()\">Import Config</button> <!-- New Button -->\n                <button onclick=\"importNMAP()\">Import NMAP</button> <!-- New Button -->\n            </div>\n        </div>\n        <div id=\"api-keys\" class=\"tab-content\">\n            <div class=\"input-group\">\n                <h3>IPINFO API Key</h3>\n                <div class=\"password-container\">\n                    <input type=\"password\" id=\"ipinfoApiKey\" placeholder=\"Enter IPinfo API Key\">\n                    <button type=\"button\" class=\"toggle-password\" data-target=\"ipinfoApiKey\">Show</button>\n                </div>\n                <label class=\"checkbox-label\">\n                    <input type=\"checkbox\" id=\"storeIpinfoKey\"> Store in local storage\n                </label>\n                <button onclick=\"saveIpinfoApiKey()\">Save IPinfo API Key</button>\n            </div>\n            <div class=\"input-group\">\n                <h3>Shodan API Key</h3>\n                <div class=\"password-container\">\n                    <input type=\"password\" id=\"shodanApiKey\" placeholder=\"Enter Shodan API Key\">\n                    <button type=\"button\" class=\"toggle-password\" data-target=\"shodanApiKey\">Show</button>\n                </div>\n                <label class=\"checkbox-label\">\n                    <input type=\"checkbox\" id=\"storeShodanKey\"> Store in local storage\n                </label>\n                <button onclick=\"saveShodanApiKey()\">Save Shodan API Key</button>\n            </div>\n            <div class=\"input-group\">\n                <h3>GreyNoise API Key</h3>\n                <div class=\"password-container\">\n                    <input type=\"password\" id=\"greynoiseApiKey\" placeholder=\"Enter GreyNoise API Key\">\n                    <button type=\"button\" class=\"toggle-password\" data-target=\"greynoiseApiKey\">Show</button>\n                </div>\n                <label class=\"checkbox-label\">\n                    <input type=\"checkbox\" id=\"storeGreynoiseKey\"> Store in local storage\n                </label>\n                <button onclick=\"saveGreynoiseApiKey()\">Save GreyNoise API Key</button>\n            </div>\n            <div class=\"input-group\">\n                <h3>URLscan.io API Key</h3>\n                <div class=\"password-container\">\n                    <input type=\"password\" id=\"urlscanApiKey\" placeholder=\"Enter URLscan.io API Key\">\n                    <button type=\"button\" class=\"toggle-password\" data-target=\"urlscanApiKey\">Show</button>\n                </div>\n                <label class=\"checkbox-label\">\n                    <input type=\"checkbox\" id=\"storeUrlscanKey\"> Store in local storage\n                </label>\n                <button onclick=\"saveUrlscanApiKey()\">Save URLscan.io API Key</button>\n            </div>\n            <div class=\"input-group\">\n                <h3>SecurityTrails API Key</h3>\n                <div class=\"password-container\">\n                    <input type=\"password\" id=\"securitytrailsApiKey\" placeholder=\"Enter SecurityTrails API Key\">\n                    <button type=\"button\" class=\"toggle-password\" data-target=\"securitytrailsApiKey\">Show</button>\n                </div>\n                <label class=\"checkbox-label\">\n                    <input type=\"checkbox\" id=\"storeSecuritytrailsKey\"> Store in local storage\n                </label>\n                <button onclick=\"saveSecuritytrailsApiKey()\">Save SecurityTrails API Key</button>\n                </div>\n                <div class=\"input-group\">\n                    <h3>URLhaus API Key</h3>\n                    <div class=\"password-container\">\n                        <input type=\"password\" id=\"urlhausApiKey\" placeholder=\"Enter URLhaus API Key\">\n                        <button type=\"button\" class=\"toggle-password\" data-target=\"urlhausApiKey\">Show</button>\n                    </div>\n                    <label class=\"checkbox-label\">\n                        <input type=\"checkbox\" id=\"storeUrlhausKey\"> Store in local storage\n                    </label>\n                    <button onclick=\"saveUrlhausApiKey()\">Save URLhaus API Key</button>\n                </div>\n            <div class=\"input-group\">\n                <h3>CORS Proxy URL</h3>\n                <div class=\"password-container\">\n                    <input type=\"password\" id=\"corsProxyUrl\" placeholder=\"Enter CORS Proxy URL\" value=\"http://localhost:3000/proxy?url=\">\n                    <button type=\"button\" class=\"toggle-password\" data-target=\"corsProxyUrl\">Show</button>\n                </div>\n                <label class=\"checkbox-label\">\n                    <input type=\"checkbox\" id=\"storeCorsProxy\" checked> Store in local storage\n                </label>\n                <label class=\"checkbox-label\">\n                    <input type=\"checkbox\" id=\"routeViaProxy\"> Route all traffic via proxy\n                </label>\n                <label class=\"checkbox-label\">\n                    <input type=\"checkbox\" id=\"ignoreApiKeysViaProxy\"> Ignore API keys when using proxy\n                </label>\n                <button onclick=\"saveCorsProxyUrl()\">Save CORS Proxy URL</button>\n            </div>\n            <div class=\"input-group\">\n                <h3>Test Functions</h3>\n                <button onclick=\"runAllTests()\">Run Test Functions</button>\n            </div>\n        </div>\n        <div id=\"enrichment\" class=\"tab-content\">\n            <div class=\"input-group\">\n                <h3>Bulk Enrichment</h3>\n                <button onclick=\"enrichAllIpinfo()\">Enrich All IPs with IPinfo</button>\n                <button onclick=\"enrichAllShodan()\">Enrich All IPs with Shodan</button>\n                <button onclick=\"enrichAllInternetDB()\">Enrich All IPs with InternetDB</button>\n                <button onclick=\"enrichAllGoogleDNS()\">Enrich All Domains with Google DNS</button>\n                <button onclick=\"enrichAllGoogleDNSMX()\">Enrich All Domains with Google DNS (MX)</button>\n                <button onclick=\"enrichAllGoogleDNSTXT()\">Enrich All Domains with Google DNS (TXT)</button>\n                <button onclick=\"enrichAllHudsonRockEmails()\">Enrich All Emails with Hudson Rock</button>\n                <button onclick=\"enrichAllHudsonRockDomains()\">Enrich All Domains with Hudson Rock</button>\n                <button onclick=\"enrichAllGreyNoise()\">Enrich All IPs with GreyNoise</button>\n                <button onclick=\"enrichAllURLscan()\">Enrich All URLs with URLscan.io</button>\n                <button onclick=\"enrichAllSecurityTrails()\">Enrich All Domains with SecurityTrails Subdomains</button>\n                <button onclick=\"enrichAllURLhaus()\">Enrich All URLs with URLhaus</button>\n            </div>\n        </div>\n        <div id=\"import-iocs\" class=\"tab-content active\">\n            <div class=\"input-group\">\n                <h3>Import IOCs</h3>\n                <textarea id=\"iocText\" placeholder=\"Paste IOC text here (IPs, domains, emails)\"></textarea>\n                <button onclick=\"importIOCsFromText()\">Import from Text</button>\n                <input type=\"file\" id=\"iocFile\" accept=\".txt\">\n                <button onclick=\"importIOCsFromFile()\">Import from File</button>\n            </div>\n        </div>\n        <div id=\"search\" class=\"tab-content\">\n            <div class=\"input-group\" style=\"margin: 10px 0;\">\n                <input type=\"text\" id=\"search-input\" placeholder=\"Search graph...\" style=\"width: 100%; margin-bottom: 5px;\">\n                <button onclick=\"searchGraph()\" style=\"background-color: #10b981;\">Search</button>\n            </div>\n            <button onclick=\"riskAnalysis()\">Risk Analysis</button> <!-- New Button -->\n        </div>\n        <div id=\"riskModal\" class=\"modal\">\n            <div class=\"modal-content\">\n                <span class=\"close-modal\">&times;</span>\n                <h2>Risk Analysis</h2>\n                <div id=\"riskTableContainer\"></div>\n            </div>\n        </div>\n        <div id=\"layouts\" class=\"tab-content\">\n            <div class=\"input-group\">\n                <h3>Graph Layouts</h3>\n                <!-- Existing layout buttons remain here -->\n                <button onclick=\"setOrganicLayout()\">Organic</button>\n                <button onclick=\"setCircularLayout()\">Circular</button>\n                <button onclick=\"setOrthogonalLayout()\">Orthogonal</button>\n                <button onclick=\"setTreeLayout()\">Tree</button>\n                <button onclick=\"setHierarchicalLayout()\">Hierarchical</button>\n            </div>\n            <!-- New section for label visibility -->\n            <div class=\"input-group\">\n                <h3>Label Visibility</h3>\n                <label class=\"checkbox-label\">\n                    <input type=\"checkbox\" id=\"showNodeLabels\" checked onchange=\"toggleNodeLabels()\">\n                    Show Node Labels\n                </label>\n                <label class=\"checkbox-label\">\n                    <input type=\"checkbox\" id=\"showEdgeLabels\" checked onchange=\"toggleEdgeLabels()\">\n                    Show Edge Labels\n                </label>\n                <label class=\"checkbox-label\">\n                    <input type=\"checkbox\" id=\"hideIsolatedNodes\" onchange=\"toggleIsolatedNodes()\">\n                    Hide Nodes Without Links\n                </label>\n            </div>\n            <div class=\"input-group\">\n                <h3>Node Size Layouts</h3>\n                <button onclick=\"setNodeSizeLayout('incoming')\">Size by Incoming Links</button>\n                <button onclick=\"setNodeSizeLayout('outgoing')\">Size by Outgoing Links</button>\n                <button onclick=\"setNodeSizeLayout('both')\">Size by All Links</button>\n            </div>\n            <div class=\"input-group\">\n                <h3>Filter Display</h3>\n                <button onclick=\"filterIpAndDomains()\">Show Only IPs & Domains</button>\n                <button onclick=\"showAllNodes()\">Show All Nodes</button>\n            </div>\n        </div>\n         <!-- Buy Me a Coffee Button -->\n         <div id=\"buy-me-a-coffee-container\" style=\"text-align: center; padding: 10px;\">\n            <script type=\"text/javascript\" src=\"https://cdnjs.buymeacoffee.com/1.0.0/button.prod.min.js\" \n                data-name=\"bmc-button\" \n                data-slug=\"mrr3b00t\" \n                data-color=\"#FFDD00\" \n                data-emoji=\"\"  \n                data-font=\"Cookie\" \n                data-text=\"Buy me a coffee\" \n                data-outline-color=\"#000000\" \n                data-font-color=\"#000000\" \n                data-coffee-color=\"#ffffff\">\n            </script>\n        </div>\n\n        <button id=\"manual-save\" onclick=\"saveStateAfterOperation()\">Save Now</button>\n\n        <div id=\"stop-task-container\" style=\"text-align: center; padding: 10px;\">\n            <button id=\"stop-task\" onclick=\"stopActiveTask()\">Stop Active Task</button>\n        </div>\n\n        <!-- Suggested -->\n        <footer id=\"controls-footer\"> \n    <p>Created by mrr3b00t (@UK_Daniel_Card)</p>\n    <p>© Xservus Limited - v0.289 demo</p>\n</footer>\n    </div>\n    <div id=\"myNetwork\"></div>\n    <div id=\"summary-modal\">\n        <button class=\"close-button\" onclick=\"hideGraphSummary()\">×</button>\n        <h3>Graph Summary</h3>\n        <table id=\"summary-table\">\n            <thead>\n                <tr>\n                    <th>Type</th>\n                    <th>Count</th>\n                </tr>\n            </thead>\n            <tbody></tbody>\n        </table>\n    </div>\n    <div id=\"properties-panel\">\n        <button class=\"close-button\" onclick=\"hidePropertiesPanel()\">×</button>\n        <h3>Node Properties</h3>\n        <table id=\"properties-table\">\n            <thead>\n                <tr>\n                    <th>Property</th>\n                    <th>Value</th>\n                </tr>\n            </thead>\n            <tbody></tbody>\n        </table>\n    </div>\n    <div id=\"contextMenu\" style=\"display: none;\"></div>\n    <div id=\"edgeContextMenu\" style=\"display: none;\"></div>\n\n<script>\n        let nodes = new vis.DataSet([]);\n        let edges = new vis.DataSet([]);\n        let nextId = 1;\n        let isDarkMode = true;\n        let ipinfoApiKey;\n        let shodanApiKey;\n        let urlhausApiKey = localStorage.getItem('urlhausApiKey') || '';\n        let securitytrailsApiKey = localStorage.getItem('securitytrailsApiKey') || ''; //security tails api key variable\n        let greynoiseApiKey = localStorage.getItem('greynoiseApiKey') || '';\n        let urlscanApiKey = localStorage.getItem('urlscanApiKey') || '';\n        let corsProxyUrl;\n        let routeViaProxy;\n        let ignoreApiKeysViaProxy;\n        let isPhysicsPaused = false;\n        let lastRequestTime = 0;\n        let activeTaskController = null;\n        let nodeLabelsVisible = true;\n        let edgeLabelsVisible = true;\n        let currentEditingNodeId = null;\n        const RATE_LIMIT_MS = 500;\n        const SHODAN_RATE_LIMIT_MS = 1000; // Specific 1-second delay for Shodan\n        const SECURITYTRAILS_RATE_LIMIT_MS = 2000; // 1 second delay for SecurityTrails API\n        const ipRegex = { ipv4: /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/, ipv6: /^([0-9a-fA-F]{0,4}:){7}[0-9a-fA-F]{0,4}$/ };\n        const domainRegex = /^(?:[a-zA-Z0-9-_]+\\.)*[a-zA-Z0-9-_]+\\.[a-zA-Z]{2,}$/;\n        const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/g;\n\n        let selectedNodes = new Set(); // To track multiple selected nodes\n    \n        initializeApiKeys();\n\n\n\n        // Define updateTheme first\nfunction updateTheme() {\n    if (isDarkMode) {\n        document.body.classList.remove('light-mode');\n        document.body.classList.add('dark-mode');\n        options.nodes.font = {\n            size: nodeLabelsVisible ? 12 : 0,\n            color: '#e2e8f0',  // Light color for dark mode\n            multi: true,\n            align: 'center',\n            vadjust: 0,\n            strokeWidth: 0\n        };\n        options.edges.font = {\n            size: edgeLabelsVisible ? 12 : 0,\n            color: '#e2e8f0',  // Light color for dark mode\n            strokeWidth: 0,\n            strokeColor: 'transparent',\n            align: 'middle',\n            multi: true\n        };\n    } else {\n        document.body.classList.remove('dark-mode');\n        document.body.classList.add('light-mode');\n        options.nodes.font = {\n            size: nodeLabelsVisible ? 12 : 0,\n            color: '#1f2a44',  // Dark color for light mode\n            multi: true,\n            align: 'center',\n            vadjust: 0,\n            strokeWidth: 0\n        };\n        options.edges.font = {\n            size: edgeLabelsVisible ? 12 : 0,\n            color: '#1f2a44',  // Dark color for light mode\n            strokeWidth: 0,\n            strokeColor: 'transparent',\n            align: 'middle',\n            multi: true\n        };\n    }\n\n    // Update all existing nodes and edges with the new font settings\n    nodes.forEach(node => {\n        nodes.update({\n            id: node.id,\n            font: options.nodes.font\n        });\n    });\n    \n    edges.forEach(edge => {\n        edges.update({\n            id: edge.id,\n            font: options.edges.font\n        });\n    });\n\n    // Apply the options to the network and force a redraw\n    network.setOptions(options);\n    updateLabelVisibility();\n    //ensureInteractionSettings();\n    network.setData({ nodes: nodes, edges: edges });  // Force data refresh\n    network.redraw();\n}\n\nfunction searchGraph() {\n        const searchTerm = document.getElementById('search-input').value.trim().toLowerCase();\n        if (!searchTerm) {\n            showToast('Please enter a search term', 'error');\n            resetNodeHighlights();\n            return;\n        }\n\n    // Reset previous highlights\n    resetNodeHighlights();\n\n    // Find matching nodes\n    const matchingNodes = nodes.get().filter(node => {\n        // Search in label, and type-specific fields\n        const searchableText = [\n            node.label || '',\n            node.ip || '',\n            node.domain || '',\n            node.email || '',\n            node.name || '',\n            node.organization || '',\n            node.portNumber || '',\n            node.address || '',\n            node.accountNumber || '',\n            node.techName || '',\n            node.deviceName || '',\n            node.malwareName || '',\n            node.vulnName || '',\n            node.cve || '',\n            node.hash || '',\n            node.asn || '',\n            node.city || '',\n            node.country || '',\n            node.os || '',\n            node.product || ''\n        ].join(' ').toLowerCase();\n\n        return searchableText.includes(searchTerm);\n    });\n\n    if (matchingNodes.length === 0) {\n        showToast('No matches found', 'info');\n        return;\n    }\n\n    // Highlight matching nodes\n    matchingNodes.forEach(node => {\n        nodes.update({\n            id: node.id,\n            color: {\n                background: '#ffeb3b', // Bright yellow for highlight\n                border: '#f44336',     // Red border for visibility\n                highlight: {\n                    background: '#ffeb3b',\n                    border: '#f44336'\n                }\n            },\n            font: {\n                size: 14,  // Slightly larger font for visibility\n                color: '#000000'  // Black text for contrast\n            }\n        });\n    });\n\n    // Zoom to the first matching node\n    const firstMatchId = matchingNodes[0].id;\n    network.focus(firstMatchId, {\n        scale: 1.5,  // Zoom in\n        animation: {\n            duration: 1000,\n            easingFunction: 'easeInOutQuad'\n        }\n    });\n    ensureInteractionSettings(); // Ensure panning is enabled after focus\n    showToast(`Found ${matchingNodes.length} matching nodes`, 'success');\n}\n\nfunction resetNodeHighlights() {\n    nodes.forEach(node => {\n        // Restore original colors based on node type\n        const originalColor = getNodeColorByType(node.type);\n        nodes.update({\n            id: node.id,\n            color: {\n                background: originalColor.background,\n                border: isDarkMode ? '#94a3b8' : '#6b7280',\n                highlight: {\n                    background: originalColor.background,\n                    border: '#60a5fa'\n                }\n            },\n            font: {\n                size: nodeLabelsVisible ? 12 : 0,\n                color: isDarkMode ? '#e2e8f0' : '#1f2a44'\n            }\n        });\n    });\n    network.fit({\n        animation: {\n            duration: 500,\n            easingFunction: 'easeInOutQuad'\n        }\n    });\n}\n\nfunction exportToPNG() {\n    network.setOptions({ physics: { enabled: false } });\n    network.stabilize(100);\n    network.fit(); // Zoom to fit all nodes\n    \n    setTimeout(() => {\n        try {\n            const canvas = document.querySelector('#myNetwork .vis-network canvas');\n            if (!canvas) throw new Error('Canvas not found');\n            \n            const dataURL = canvas.toDataURL('image/png');\n            const link = document.createElement('a');\n            link.href = dataURL;\n            link.download = `network_graph_${new Date().toISOString().replace(/[:.]/g, '-')}.png`;\n            document.body.appendChild(link);\n            link.click();\n            document.body.removeChild(link);\n            \n            showToast('Graph exported as PNG', 'success');\n        } catch (error) {\n            console.error('Error exporting to PNG:', error);\n            showToast('Failed to export graph as PNG: ' + error.message, 'error');\n        } finally {\n            network.setOptions({ physics: { enabled: !isPhysicsPaused } });\n        }\n    }, 500);\n}\n\n\nfunction filterIpAndDomains() {\n    // Disable physics during filtering\n    network.setOptions({ physics: { enabled: false } });\n    \n    // Update each node's hidden property based on type\n    nodes.forEach(node => {\n        const shouldHide = node.type !== 'ip' && node.type !== 'domain';\n        nodes.update({\n            id: node.id,\n            hidden: shouldHide\n        });\n    });\n    \n    // Hide edges connected to hidden nodes\n    edges.forEach(edge => {\n        const fromNode = nodes.get(edge.from);\n        const toNode = nodes.get(edge.to);\n        const shouldHide = fromNode.hidden || toNode.hidden;\n        edges.update({\n            id: edge.id,\n            hidden: shouldHide\n        });\n    });\n    \n    // Stabilize and fit the network\n    stabilizeNetwork().then(() => {\n        network.fit({\n            animation: {\n                duration: 300,\n                easingFunction: 'easeInOutQuad'\n            }\n        });\n        showToast('Showing only IP addresses and domains', 'success');\n        saveStateAfterOperation();\n    });\n}\n\nfunction showAllNodes() {\n    // Disable physics during filtering\n    network.setOptions({ physics: { enabled: false } });\n    \n    // Show all nodes and edges\n    nodes.forEach(node => {\n        nodes.update({\n            id: node.id,\n            hidden: false\n        });\n    });\n    \n    edges.forEach(edge => {\n        edges.update({\n            id: edge.id,\n            hidden: false\n        });\n    });\n    \n    // Stabilize and fit the network\n    stabilizeNetwork().then(() => {\n        network.fit({\n            animation: {\n                duration: 300,\n                easingFunction: 'easeInOutQuad'\n            }\n        });\n        showToast('Showing all nodes and edges', 'success');\n        saveStateAfterOperation();\n    });\n}\n\n// Helper function to get original colors by node type\nfunction getNodeColorByType(type) {\n    const colorMap = {\n        'ip': '#f87171',\n        'domain': '#60a5fa',\n        'contact': '#4ade80',\n        'organization': '#facc15',\n        'port': '#a78bfa',\n        'wallet': '#fb923c',\n        'bank': '#10b981',\n        'technology': '#ec4899',\n        'device': '#14b8a6',\n        'malware': '#ef4444',\n        'vulnerability': '#dc2626',\n        'favicon': '#22d3ee',\n        'http_hash': '#f97316',\n        'html_hash': '#f59e0b',\n        'ssl_hash': '#8b5cf6',\n        'asn': '#a3e635',\n        'city': '#f97316',\n        'country': '#34d399',\n        'os': '#10b981',\n        'product': '#ec4899',\n        'http_title': '#3b82f6',\n        'vpn': '#9333ea',\n        'proxy': '#f43f5e',\n        'tor': '#64748b',\n        'relay': '#eab308',\n        'hosting': '#14b8a6',\n        'tag': '#6d28d9',\n        'cpe': '#0d9488',\n        'mx': '#34d399', // Green for MX\n        'txt': '#f59e0b'  // Orange for TXT\n    };\n    return { background: colorMap[type] || '#ffffff' };\n}\n\n// Add this with your other event listeners\ndocument.getElementById('search-input').addEventListener('keypress', function(event) {\n    if (event.key === 'Enter') {\n        event.preventDefault();\n        searchGraph();\n    }\n});\n\n\n        async function enrichAllIpinfo() {\n    if (!ipinfoApiKey && !ignoreApiKeysViaProxy) { \n        showToast('Please set your IPinfo API key in the \"API Keys\" tab first.', 'error'); \n        return; \n    }\n    \n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const ipNodes = nodes.get({ filter: n => n.type === 'ip' && n.ip });\n    const totalIPs = ipNodes.length;\n    let successfulEnrichments = 0;\n    \n    const batchSize = 50;\n    const delayBetweenBatches = 200;\n    const totalBatches = Math.ceil(totalIPs / batchSize);\n    const assumedRequestTimeMs = 100;\n    const timePerBatchMs = assumedRequestTimeMs;\n    const totalBatchDelays = (totalBatches - 1) * delayBetweenBatches;\n    const estimatedTimeMs = (timePerBatchMs * totalBatches) + totalBatchDelays + 1000;\n    \n    const estimatedSeconds = Math.ceil(estimatedTimeMs / 1000);\n    const estimatedMinutes = Math.floor(estimatedSeconds / 60);\n    const remainingSeconds = estimatedSeconds % 60;\n    const timeEstimateStr = estimatedMinutes > 0 \n        ? `${estimatedMinutes}m ${remainingSeconds}s` \n        : `${estimatedSeconds}s`;\n    \n    showToast(`Estimated time for IPinfo enrichment: ~${timeEstimateStr}`, 'info');\n    document.getElementById('progress-bar').textContent = `IPinfo Enrichment: 0/${totalIPs} IPs (0%) - Est. ${timeEstimateStr}`;\n    \n    const newNodes = [];\n    const newEdges = [];\n    let existingAsns = new Map(nodes.get({ filter: n => n.type === 'asn' }).map(n => [n.asn, n.id]));\n    let existingCities = new Map(nodes.get({ filter: n => n.type === 'city' }).map(n => [n.city, n.id]));\n    let existingOrgs = new Map(nodes.get({ filter: n => n.type === 'organization' }).map(n => [n.organization, n.id]));\n    let existingCountries = new Map(nodes.get({ filter: n => n.type === 'country' }).map(n => [n.country, n.id]));\n    let existingPrivacyTypes = new Map([\n        ['vpn', null], ['proxy', null], ['tor', null], ['relay', null], ['hosting', null]\n    ].map(([type]) => {\n        const existing = nodes.get({ filter: n => n.type === type })[0];\n        return [type, existing ? existing.id : null];\n    }));\n\n    const privacyTypes = [\n        { key: 'vpn', label: 'VPN', color: '#9333ea' },\n        { key: 'proxy', label: 'Proxy', color: '#f43f5e' },\n        { key: 'tor', label: 'Tor', color: '#64748b' },\n        { key: 'relay', label: 'Relay', color: '#eab308' },\n        { key: 'hosting', label: 'Hosting', color: '#14b8a6' }\n    ];\n\n    async function processBatch(batch) {\n        const promises = batch.map(node => {\n            if (activeTaskController && activeTaskController.signal.aborted) {\n                return Promise.resolve(null);\n            }\n            const baseUrl = ignoreApiKeysViaProxy ? \n                `https://ipinfo.io/${node.ip}/json` : \n                `https://ipinfo.io/${node.ip}/json?token=${ipinfoApiKey}`;\n            const url = constructUrl(baseUrl, !ignoreApiKeysViaProxy);\n            return fetch(url)\n                .then(response => {\n                    if (!response.ok) throw new Error('Failed to fetch IPinfo data');\n                    return response.json();\n                })\n                .then(data => {\n                    const ipNodeId = node.id;\n                    const asn = data.asn?.asn || 'Unknown ASN';\n                    const city = data.city || 'Unknown City';\n                    const companyName = data.company?.name || 'Unknown Company';\n                    const country = data.country || 'Unknown Country';\n                    const privacy = data.privacy || { vpn: false, proxy: false, tor: false, relay: false, hosting: false };\n\n                    // ASN\n                    let asnId = existingAsns.get(asn);\n                    if (!asnId) {\n                        asnId = nextId++;\n                        newNodes.push({ \n                            id: asnId, \n                            type: 'asn', \n                            label: `ASN: ${asn}`, \n                            title: `ASN: ${asn}`, \n                            color: { background: '#a3e635' }, \n                            asn \n                        });\n                        existingAsns.set(asn, asnId);\n                    }\n                    const asnEdgeId = `${ipNodeId}-${asnId}-AssignedTo`;\n                    if (!edges.get(asnEdgeId) && !newEdges.some(e => e.id === asnEdgeId)) {\n                        newEdges.push({ id: asnEdgeId, from: ipNodeId, to: asnId, label: 'Assigned to' });\n                    }\n\n                    // City\n                    let cityId = existingCities.get(city);\n                    if (!cityId) {\n                        cityId = nextId++;\n                        newNodes.push({ \n                            id: cityId, \n                            type: 'city', \n                            label: `City: ${city}`, \n                            title: `City: ${city}`, \n                            color: { background: '#f97316' }, \n                            city \n                        });\n                        existingCities.set(city, cityId);\n                    }\n                    const cityEdgeId = `${ipNodeId}-${cityId}-LocatedIn`;\n                    if (!edges.get(cityEdgeId) && !newEdges.some(e => e.id === cityEdgeId)) {\n                        newEdges.push({ id: cityEdgeId, from: ipNodeId, to: cityId, label: 'Located in' });\n                    }\n\n                    // Organization\n                    let orgId = existingOrgs.get(companyName);\n                    if (!orgId) {\n                        orgId = nextId++;\n                        newNodes.push({ \n                            id: orgId, \n                            type: 'organization', \n                            label: `Organization: ${companyName}`, \n                            title: `Company: ${companyName}`, \n                            color: { background: '#facc15' }, \n                            organization: companyName \n                        });\n                        existingOrgs.set(companyName, orgId);\n                    }\n                    const orgEdgeId = `${ipNodeId}-${orgId}-BelongsTo`;\n                    if (!edges.get(orgEdgeId) && !newEdges.some(e => e.id === orgEdgeId)) {\n                        newEdges.push({ id: orgEdgeId, from: ipNodeId, to: orgId, label: 'Belongs to' });\n                    }\n\n                    // Country\n                    let countryId = existingCountries.get(country);\n                    if (!countryId) {\n                        countryId = nextId++;\n                        newNodes.push({ \n                            id: countryId, \n                            type: 'country', \n                            label: `Country: ${country}`, \n                            title: `Country: ${country}`, \n                            color: { background: '#34d399' }, \n                            country \n                        });\n                        existingCountries.set(country, countryId);\n                    }\n                    const countryEdgeId = `${ipNodeId}-${countryId}-LocatedIn`;\n                    if (!edges.get(countryEdgeId) && !newEdges.some(e => e.id === countryEdgeId)) {\n                        newEdges.push({ id: countryEdgeId, from: ipNodeId, to: countryId, label: 'Located in' });\n                    }\n\n                    // Privacy Types\n                    privacyTypes.forEach(privacyType => {\n                        if (privacy[privacyType.key]) {\n                            let privacyNodeId = existingPrivacyTypes.get(privacyType.key);\n                            if (!privacyNodeId) {\n                                privacyNodeId = nextId++;\n                                newNodes.push({ \n                                    id: privacyNodeId, \n                                    type: privacyType.key, \n                                    label: privacyType.label, \n                                    title: privacyType.label, \n                                    color: { background: privacyType.color }\n                                });\n                                existingPrivacyTypes.set(privacyType.key, privacyNodeId);\n                            }\n                            const privacyEdgeId = `${ipNodeId}-${privacyNodeId}-Uses`;\n                            if (!edges.get(privacyEdgeId) && !newEdges.some(e => e.id === privacyEdgeId)) {\n                                newEdges.push({ id: privacyEdgeId, from: ipNodeId, to: privacyNodeId, label: 'Uses' });\n                            }\n                        }\n                    });\n\n                    successfulEnrichments++;\n                })\n                .catch(error => {\n                    console.error(`Failed to enrich IP ${node.ip}: ${error.message}`);\n                    showToast(`Failed to enrich IP ${node.ip}: ${error.message}`, 'error');\n                    return null;\n                });\n        });\n        await Promise.all(promises);\n    }\n    \n    let lastProgressUpdate = 0;\n    const progressUpdateInterval = 1000;\n    const startTime = Date.now();\n    \n    for (let i = 0; i < totalIPs; i += batchSize) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('IPinfo enrichment stopped', 'info');\n            break;\n        }\n        \n        const batch = ipNodes.slice(i, Math.min(i + batchSize, totalIPs));\n        await processBatch(batch);\n        \n        if (newNodes.length > 0) {\n            nodes.add(newNodes);\n            newNodes.length = 0;\n        }\n        if (newEdges.length > 0) {\n            edges.add(newEdges);\n            newEdges.length = 0;\n        }\n        \n        const currentTime = Date.now();\n        if (currentTime - lastProgressUpdate >= progressUpdateInterval) {\n            const processedIPs = Math.min(i + batchSize, totalIPs);\n            const progress = ((processedIPs / totalIPs) * 100).toFixed(1);\n            const remainingIPs = totalIPs - processedIPs;\n            const remainingTimeMs = Math.max(0, remainingIPs * assumedRequestTimeMs);\n            const remainingSeconds = Math.ceil(remainingTimeMs / 1000);\n            const remainingMinutes = Math.floor(remainingSeconds / 60);\n            const remainingSecondsPart = remainingSeconds % 60;\n            const remainingTimeStr = remainingMinutes > 0 \n                ? `${remainingMinutes}m ${remainingSecondsPart}s` \n                : `${remainingSeconds}s`;\n            \n            document.getElementById('progress-bar').textContent = \n                `IPinfo Enrichment: ${successfulEnrichments}/${totalIPs} IPs (${progress}%) - Est. ${remainingTimeStr} remaining`;\n            lastProgressUpdate = currentTime;\n            updateSelectOptions();\n        }\n        \n        await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));\n    }\n    \n    if (newNodes.length > 0) nodes.add(newNodes);\n    if (newEdges.length > 0) edges.add(newEdges);\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    //ensureInteractionSettings();\n    completeProgressBar();\n    showToast(`IPinfo enrichment completed: ${successfulEnrichments}/${totalIPs} IPs enriched`, 'success');\n    \n    if (window.innerWidth <= 768) {\n        const controls = document.getElementById('controls');\n        controls.classList.add('collapsed');\n        document.getElementById('myNetwork').style.display = 'block';\n        network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });\n    }\n}\n\n\n//Export to PDF\n\n\nfunction exportToPDF() {\n    network.setOptions({ physics: { enabled: false } });\n    network.stabilize(100);\n    network.fit();\n    \n    setTimeout(() => {\n        try {\n            const canvas = document.querySelector('#myNetwork .vis-network canvas');\n            if (!canvas) throw new Error('Canvas not found');\n\n            const { jsPDF } = window.jspdf;\n            const pdf = new jsPDF({\n                orientation: 'landscape',\n                unit: 'px',\n                format: [canvas.width, canvas.height]\n            });\n\n            // Add title\n            pdf.setFontSize(16);\n            pdf.text('Network Graph Export', 40, 20);\n            \n            const imgData = canvas.toDataURL('image/png');\n            pdf.addImage(imgData, 'PNG', 0, 40, canvas.width, canvas.height - 40); // Offset for title\n            pdf.save(`network_graph_${new Date().toISOString().replace(/[:.]/g, '-')}.pdf`);\n            \n            showToast('Graph exported as PDF', 'success');\n        } catch (error) {\n            console.error('Error exporting to PDF:', error);\n            showToast('Failed to export graph as PDF: ' + error.message, 'error');\n        } finally {\n            network.setOptions({ physics: { enabled: !isPhysicsPaused } });\n        }\n    }, 500);\n}\n\n// Function to trigger save after key operations\nfunction saveStateAfterOperation() {\n    saveState();\n    showToast('Progress saved', 'success');\n}\n\nwindow.onload = function() {\n    const stateLoaded = loadState();\n    if (!stateLoaded) {\n        nodes = new vis.DataSet([]);\n        edges = new vis.DataSet([]);\n        nextId = 1;\n    }\n\n    document.getElementById('ipinfoApiKey').value = ipinfoApiKey;\n    document.getElementById('shodanApiKey').value = shodanApiKey;\n    document.getElementById('corsProxyUrl').value = corsProxyUrl;\n    document.getElementById('routeViaProxy').checked = routeViaProxy;\n    document.getElementById('ignoreApiKeysViaProxy').checked = ignoreApiKeysViaProxy;\n    document.getElementById('storeIpinfoKey').checked = !!localStorage.getItem('ipinfoApiKey');\n    document.getElementById('storeShodanKey').checked = !!localStorage.getItem('shodanApiKey');\n    document.getElementById('storeCorsProxy').checked = !!localStorage.getItem('corsProxyUrl');\n\n    updateSelectOptions();\n    updateTheme();\n    \n    // Ensure interaction settings with dragView\n    network.setOptions({\n        interaction: {\n            ...baseInteractionOptions,\n            dragView: true,  // Explicitly enable dragging\n            zoomView: true,\n            zoomSpeed: 0.5\n        }\n    });\n\n    document.getElementById('menu-toggle').textContent = '<';  // Set to \"<\" since menu starts expanded\n    ensureInteractionSettings();\n    \n    container.removeEventListener('mousedown', handleMouseDown);\n    container.removeEventListener('mousemove', handleMouseMove);\n    container.removeEventListener('mouseup', handleMouseUp);\n    container.addEventListener('wheel', handleWheel, { passive: false });\n\n    if (window.innerWidth <= 768) {\n        document.getElementById('controls').classList.add('collapsed');\n    }\n\n    stabilizeNetwork().then(() => {\n        network.fit({ animation: { duration: 300 } });\n    });\n\n    let hasChanges = false;\n    nodes.on('*', () => hasChanges = true);\n    edges.on('*', () => hasChanges = true);\n    setInterval(() => {\n        if (hasChanges) {\n            saveState();\n            hasChanges = false;\n            showToast('Auto-saved', 'info');\n        }\n    }, 60 * 1000);\n};\n\n\nfunction loadState() {\n    const savedState = localStorage.getItem('networkGraphState');\n    if (!savedState) {\n        console.log('No saved state found in localStorage');\n        return false;\n    }\n\n    try {\n        const state = JSON.parse(savedState);\n        if (!state.nodes || !state.edges) return false;\n\n        nodes.clear();\n        edges.clear();\n        \n        state.nodes.forEach(node => nodes.add(node));\n        state.edges.forEach(edge => edges.add(edge));\n        \n        nextId = state.nextId || 1;\n        isDarkMode = state.isDarkMode !== undefined ? state.isDarkMode : true;\n        isPhysicsPaused = state.isPhysicsPaused || false;\n        nodeLabelsVisible = state.nodeLabelsVisible !== undefined ? state.nodeLabelsVisible : true;\n        edgeLabelsVisible = state.edgeLabelsVisible !== undefined ? state.edgeLabelsVisible : true;\n        \n        document.getElementById('showNodeLabels').checked = nodeLabelsVisible;\n        document.getElementById('showEdgeLabels').checked = edgeLabelsVisible;\n        \n        updateTheme();\n        const pauseButton = document.getElementById('pause-toggle');\n        pauseButton.textContent = isPhysicsPaused ? 'Resume Physics' : 'Pause Physics';\n        pauseButton.classList.toggle('paused', isPhysicsPaused);\n        \n         // Ensure interaction settings with dragView\n    network.setOptions({\n        interaction: {\n            ...baseInteractionOptions,\n            dragView: true,  // Explicitly enable dragging\n            zoomView: true,\n            zoomSpeed: 0.5\n        }\n    });\n\n        \n        // Re-apply wheel event listener (from previous zoom fix)\n        container.removeEventListener('wheel', handleWheel);\n        container.addEventListener('wheel', handleWheel, { passive: false });\n        \n        console.log('Loaded state with dragView enabled');\n        return true;\n    } catch (e) {\n        console.error('Error loading state:', e);\n        showToast('Failed to load saved state: ' + e.message, 'error');\n        localStorage.removeItem('networkGraphState');\n        return false;\n    }\n}\n\n\n// Separate wheel handler function for reusability //removed this as it broke pan\nfunction handleWheel(event) {\n    // event.preventDefault();\n    // const scale = event.deltaY > 0 ? 0.9 : 1.1;\n    // const currentScale = network.getScale();\n    // network.moveTo({\n    //     scale: currentScale * scale,\n    //     animation: { duration: 100 }\n    // });\n}\n\nfunction throttleRequest(fn) {\n            return async function(...args) {\n                const now = Date.now();\n                const timeSinceLast = now - lastRequestTime;\n                if (timeSinceLast < RATE_LIMIT_MS) await new Promise(resolve => setTimeout(resolve, RATE_LIMIT_MS - timeSinceLast));\n                lastRequestTime = now;\n                return fn(...args);\n            };\n        }\n\n\nconst baseInteractionOptions = {\n    dragNodes: true,\n    dragView: true,    // Explicitly enable panning\n    zoomView: true,    // Enable zooming\n    selectable: true,\n    multiselect: true,\n    hover: true,\n    zoomSpeed: 0.5\n};\n\n        let container = document.getElementById('myNetwork');\n        let data = { nodes: nodes, edges: edges };\n\n        let options = {\n    nodes: {\n        shape: 'dot',\n        size: 10,\n        font: { \n            size: nodeLabelsVisible ? 12 : 0,\n            color: '#e2e8f0',\n            multi: true,\n            align: 'center',\n            vadjust: 0\n        },\n        scaling: {\n            min: 10,\n            max: 125,\n            label: { enabled: false }\n        },\n        fixed: {\n            x: false,\n            y: false\n        },\n        shapeProperties: {\n            useBorderWithImage: false\n        },\n        chosen: {\n            label: function(values, id, selected, hovering) {\n                values.size = nodeLabelsVisible ? 12 : 0;\n            }\n        },\n        color: {\n            hover: {\n                border: '#60a5fa',\n                background: '#4b5563'\n            },\n            highlight: {\n                border: '#60a5fa',\n                background: '#4b5563'\n            }\n        }\n    },\n    edges: { \n        arrows: { to: { enabled: true, scaleFactor: 0.5 } },\n        font: { \n            size: edgeLabelsVisible ? 12 : 0,\n            color: '#e2e8f0',\n            strokeWidth: 0,\n            strokeColor: 'transparent'\n        },\n        smooth: true,\n        width: 1,\n        chosen: {\n            label: function(values, id, selected, hovering) {\n                values.size = edgeLabelsVisible ? 12 : 0;\n            }\n        },\n        color: { \n            color: '#94a3b8',\n            highlight: '#60a5fa'\n        }\n    },\n    physics: { \n        enabled: true,\n        stabilization: {\n            enabled: true,\n            iterations: 100,\n            updateInterval: 25\n        },\n        barnesHut: { \n            gravitationalConstant: -8000,\n            centralGravity: 0.1,\n            springLength: 200,\n            springConstant: 0.04,\n            damping: 0.9,\n            avoidOverlap: 0.5\n        },\n        maxVelocity: 25,\n        minVelocity: 0.1,\n        solver: 'barnesHut'\n    },\n    interaction: {\n        dragNodes: true,\n        dragView: true,    // Enable view dragging (panning)\n        zoomView: true,    // Enable zooming\n        hover: true,\n        multiselect: true,\n        selectable: true,\n        zoomSpeed: 0.5\n    },\n    layout: { improvedLayout: true }\n};\n\n        let network = new vis.Network(container, data, options);\n\nnetwork.on('selectNode', function(params) {\n    selectedNodes = new Set(params.nodes); // Sync with vis.js selection\n    console.log('Nodes selected:', [...selectedNodes]);\n});\n\nnetwork.on('deselectNode', function(params) {\n    selectedNodes = new Set(params.nodes); // Sync with vis.js selection\n    console.log('Nodes deselected, remaining:', [...selectedNodes]);\n});\n\nfunction ensureInteractionSettings() {\n    network.setOptions({\n        interaction: { \n            dragNodes: true,\n            dragView: true,    // Explicitly enable panning\n            zoomView: true,\n            selectable: true,\n            multiselect: true,\n            hover: true,\n            zoomSpeed: 0.5\n        },\n        physics: {\n            enabled: !isPhysicsPaused,\n            stabilization: { enabled: false },\n            barnesHut: {\n                gravitationalConstant: -8000,\n                centralGravity: 0.1,\n                springLength: 200,\n                springConstant: 0.04,\n                damping: 0.9,\n                avoidOverlap: 0.5\n            },\n            maxVelocity: 25,\n            minVelocity: 0.1\n        },\n        nodes: {\n            fixed: { x: false, y: false } // Ensure nodes remain movable unless explicitly fixed\n        }\n    });\n    console.log('Interaction settings reapplied with dragView enabled');\n}\n\n\nnetwork.on('dragStart', function(params) {\n    if (params.nodes.length) {\n        // Dragging a node\n        console.log('Dragging node:', params.nodes);\n    } else {\n        // Dragging background (panning)\n        console.log('Starting pan');\n        network.setOptions({\n            interaction: { dragView: true }\n        });\n    }\n});\n\nnetwork.on('dragEnd', function(params) {\n    if (params.nodes.length > 0) {\n        // Node dragging ended\n        params.nodes.forEach(nodeId => {\n            nodes.update({\n                id: nodeId,\n                fixed: { x: false, y: false }\n            });\n        });\n        console.log('Node drag ended, position updated');\n        //saveState(); // Save silently without toast\n    } else {\n        // View panning ended\n        console.log('View pan ended, no save needed');\n    }\n    ensureInteractionSettings(); // Reapply interaction settings regardless\n});\n\nfunction stabilizeNetwork(skipFit = false) {\n    return new Promise(resolve => {\n        network.setOptions({\n            physics: {\n                enabled: true,\n                stabilization: { enabled: true, iterations: 200, updateInterval: 50 }\n            }\n        });\n        network.stabilize(200);\n        let resolved = false;\n        network.once('stabilizationIterationsDone', () => {\n            if (!resolved) {\n                resolved = true;\n                finishStabilization(resolve, skipFit);\n            }\n        });\n        setTimeout(() => {\n            if (!resolved) {\n                resolved = true;\n                finishStabilization(resolve, skipFit);\n            }\n        }, 5000);\n    });\n}\n\nfunction finishStabilization(resolve, skipFit) {\n    network.setOptions({ \n        physics: { enabled: !isPhysicsPaused, stabilization: { enabled: false } }\n    });\n    ensureInteractionSettings(); // Reapply interaction settings after stabilization\n    if (!skipFit) network.fit({ animation: { duration: 500, easingFunction: 'easeInOutQuad' } });\n    resolve();\n}\n   \n\nnetwork.on('init', function() {\n            container.addEventListener('wheel', function(event) {\n                event.preventDefault();\n                const scale = event.deltaY > 0 ? 0.9 : 1.1;\n                const currentScale = network.getScale();\n                network.moveTo({\n                    scale: currentScale * scale,\n                    animation: { duration: 100 }\n                });\n            }, { passive: false });\n}\n\n);\n\nnetwork.on('oncontext', function(params) {\n    params.event.preventDefault();\n    const nodeId = this.getNodeAt(params.pointer.DOM);\n    const edgeId = this.getEdgeAt(params.pointer.DOM);\n\n    // Don't modify selectedNodes here unless it's a deliberate action\n    if (selectedNodes.size > 0) {\n        const firstNode = nodes.get([...selectedNodes][0]);\n        let value;\n        switch (firstNode.type) {\n            case 'ip': value = firstNode.ip; break;\n            case 'domain': value = firstNode.domain; break;\n            default: value = firstNode.label;\n        }\n        showContextMenu(params.pointer.DOM.x, params.pointer.DOM.y, value, [...selectedNodes], firstNode.type);\n    } else if (nodeId) {\n        // Fallback for single node if no prior selection\n        const node = nodes.get(nodeId);\n        let value;\n        switch (node.type) {\n            case 'ip': value = node.ip; break;\n            case 'domain': value = node.domain; break;\n            default: value = node.label;\n        }\n        showContextMenu(params.pointer.DOM.x, params.pointer.DOM.y, value, [nodeId], node.type);\n    } else if (edgeId) {\n        showEdgeContextMenu(params.pointer.DOM.x, params.pointer.DOM.y, edgeId);\n    }\n});\n\nnetwork.on('click', function(params) {\n\n    if (params.nodes.length === 0) {\n        // Empty space clicked, prevent default zooming\n        params.event.preventDefault();\n        console.log('Empty space clicked, no action taken');\n        return;\n    }\n    // Only proceed if exactly one node is selected and not dragging\n    if (params.nodes.length !== 1 || params.event.type === 'drag') {\n        console.log('Click event ignored:', params);\n        return;\n    }\n\n    const nodeId = params.nodes[0];\n    const node = nodes.get(nodeId);\n\n    // Verify node exists\n    if (!node) {\n        console.error('Node not found for ID:', nodeId);\n        showToast('Node not found', 'error');\n        return;\n    }\n\n    // Log the node for debugging\n    console.log('Clicked node:', JSON.stringify(node, null, 2));\n\n    // Determine the value to copy based on node type\n    let valueToCopy;\n    switch (node.type) {\n        case 'ip':\n            valueToCopy = node.ip;\n            break;\n        case 'domain':\n            valueToCopy = node.domain;\n            break;\n        case 'url':\n            valueToCopy = node.url;\n            break;\n        case 'contact':\n            valueToCopy = node.email || node.name;\n            break;\n        case 'organization':\n            valueToCopy = node.organization;\n            break;\n        case 'port':\n            valueToCopy = `${node.portType}/${node.portNumber}`;\n            break;\n        case 'wallet':\n            valueToCopy = node.address;\n            break;\n        case 'bank':\n            valueToCopy = node.accountNumber;\n            break;\n        case 'technology':\n            valueToCopy = node.techName;\n            break;\n        case 'device':\n            valueToCopy = node.deviceName;\n            break;\n        case 'malware':\n            valueToCopy = node.malwareName;\n            break;\n        case 'vulnerability':\n            valueToCopy = node.cve || node.vulnName;\n            break;\n        case 'favicon':\n        case 'http_hash':\n        case 'html_hash':\n        case 'ssl_hash':\n        case 'hash': // New case for file hashes\n            valueToCopy = node.hash; // Use full hash value\n            break;\n        case 'asn':\n            valueToCopy = node.asn;\n            break;\n        case 'city':\n            valueToCopy = node.city;\n            break;\n        case 'country':\n            valueToCopy = node.country;\n            break;\n        case 'os':\n            valueToCopy = node.os;\n            break;\n        case 'product':\n            valueToCopy = node.product;\n            break;\n        case 'http_title':\n            valueToCopy = node.title;\n            break;\n        case 'vpn':\n        case 'proxy':\n        case 'tor':\n        case 'relay':\n        case 'hosting':\n            valueToCopy = node.value;\n            break;\n        case 'hash': // Add this case for file hashes\n            if (!node.hash) return;\n            nodeData.hash = node.hash;\n            nodeData.hashType = node.hashType || 'Unknown';\n            nodeData.label = `${node.hashType}: ${node.hash.substring(0, 8)}...`;\n            nodeData.title = `File Hash\\nType: ${node.hashType}\\nValue: ${node.hash}${node.notes ? '\\nNotes: ' + node.notes : ''}`;\n            nodeData.color.background = node.color?.background || '#f97316';\n            break;\n        case 'txt': // New case for TXT records\n            valueToCopy = node.text;\n            break;\n        default:\n            valueToCopy = node.label.split('\\n')[0].split(': ')[1] || node.label.split('\\n')[0];\n            console.warn(`Unhandled node type \"${node.type}\", using fallback value:`, valueToCopy);\n    }\n\n    // Check if a valid value was found\n    if (!valueToCopy) {\n        console.warn('No valid value to copy for node:', node);\n        showToast(`No value available to copy for ${node.type}`, 'warning');\n        return;\n    }\n\n    // Log the value being copied for debugging\n    console.log('Value to copy:', valueToCopy);\n\n    // Attempt to copy to clipboard\n    navigator.clipboard.writeText(valueToCopy)\n        .then(() => {\n            showToast(`Copied \"${valueToCopy}\" to clipboard`, 'success');\n        })\n        .catch(err => {\n            console.error('Failed to copy to clipboard:', err);\n            showToast(`Failed to copy: ${err.message}`, 'error');\n        });\n});\n\nfunction showEdgeContextMenu(x, y, edgeId) {\n    const menu = document.getElementById('edgeContextMenu');\n    const edge = edges.get(edgeId);\n    if (!edge) return;\n    \n    const menuHtml = `\n        <button onclick=\"editEdgeLabel('${edgeId}')\">Edit Label</button>\n        <button onclick=\"removeEdgeDirect('${edgeId}')\">Delete Edge</button>\n    `;\n    \n    menu.innerHTML = menuHtml;\n    const canvasOffset = container.getBoundingClientRect();\n    menu.style.left = `${x + canvasOffset.left}px`;\n    menu.style.top = `${y + canvasOffset.top}px`;\n    menu.style.display = 'block';\n    document.addEventListener('click', hideEdgeContextMenu);\n}\n\nfunction hideEdgeContextMenu() {\n    document.getElementById('edgeContextMenu').style.display = 'none';\n    document.removeEventListener('click', hideEdgeContextMenu);\n}\n\nfunction editEdgeLabel(edgeId) {\n    const edge = edges.get(edgeId);\n    if (!edge) {\n        showToast('Edge not found', 'error');\n        return;\n    }\n    \n    const currentLabel = edge.label || '';\n    const newLabel = prompt('Enter new edge label (leave empty to remove):', currentLabel);\n    \n    if (newLabel !== null) { // null means user cancelled\n        edges.update({\n            id: edgeId,\n            label: newLabel.trim() === '' ? undefined : newLabel.trim()\n            \n        });\n        updateEdgeSelectOptions();\n        stabilizeNetwork();\n        saveStateAfterOperation();\n        showToast('Edge label updated', 'success');\n    }\n    \n    hideEdgeContextMenu();\n}\n\nfunction removeEdgeDirect(edgeId) {\n    const edge = edges.get(edgeId);\n    if (!edge) {\n        showToast('Edge not found', 'error');\n        return;\n    }\n    \n    edges.remove({ id: edgeId });\n    updateNodeSizes();\n    updateEdgeSelectOptions();\n    stabilizeNetwork();\n    saveStateAfterOperation();\n    showToast('Edge removed', 'success');\n    hideEdgeContextMenu();\n}\n\n// show context menu on right click\nfunction showContextMenu(x, y, value, nodeIds, type) {\n    const nodesArray = Array.isArray(nodeIds) ? nodeIds : [nodeIds];\n    const menu = document.getElementById('contextMenu');\n    const isMultiple = nodesArray.length > 1;\n    \n    let menuHtml = `\n        <button onclick=\"deleteNodes([${nodesArray.join(',')}])\">Delete ${isMultiple ? 'Selected Nodes' : 'Node'}</button>\n        ${isMultiple ? '' : `<button onclick=\"startLinkCreation(${nodesArray[0]})\">Create Link From Here</button>`}\n        ${isMultiple ? '' : `<button onclick=\"showPropertiesPanel(${nodesArray[0]}); hideContextMenu();\">View Node Properties</button>`}\n        ${isMultiple ? '' : `<button onclick=\"editNodeNotes(${nodesArray[0]}); hideContextMenu();\">Add/Edit Notes</button>`}\n    `;\n    \n    // Check if all selected nodes are of the same type\n    const allSameType = nodesArray.every(id => {\n        const node = nodes.get(id);\n        return node && node.type === type;\n    });\n    \n    if (allSameType) {\n        if (type === 'ip') {\n            menuHtml += `\n                <button onclick=\"throttledEnrichIPMultiple([${nodesArray.map(id => `'${nodes.get(id).ip}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])\">Enrich via IPINFO</button>\n                <button onclick=\"throttledEnrichShodanMultiple([${nodesArray.map(id => `'${nodes.get(id).ip}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])\">Enrich via Shodan</button>\n                <button onclick=\"throttledEnrichInternetDBMultiple([${nodesArray.map(id => `'${nodes.get(id).ip}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])\">Enrich via InternetDB</button>\n                <button onclick=\"throttledEnrichGreyNoiseMultiple([${nodesArray.map(id => `'${nodes.get(id).ip}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])\">Enrich via GreyNoise</button>\n                <button onclick=\"throttledSendHttpsRequestMultiple([${nodesArray.map(id => `'${nodes.get(id).ip}'`).join(',')}], 'ip', 'https')\">Send HTTPS Request</button>\n                <button onclick=\"throttledSendHttpsRequestMultiple([${nodesArray.map(id => `'${nodes.get(id).ip}'`).join(',')}], 'ip', 'http')\">Send HTTP Request</button>\n                <button onclick=\"throttledEnrichURLscan('${nodes.get(nodesArray[0]).ip}', ${nodesArray[0]})\">Enrich via URLscan.io</button>\n            `;\n        } else if (type === 'domain') {\n            menuHtml += `\n                <button onclick=\"throttledEnrichGoogleDNSMultiple([${nodesArray.map(id => `'${nodes.get(id).domain}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])\">Enrich via Google DNS</button>\n                <button onclick=\"throttledEnrichGoogleDNSMXMultiple([${nodesArray.map(id => `'${nodes.get(id).domain}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])\">Enrich via Google DNS (MX)</button>\n                <button onclick=\"throttledEnrichGoogleDNSTXTMultiple([${nodesArray.map(id => `'${nodes.get(id).domain}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])\">Enrich via Google DNS (TXT)</button>\n                <button onclick=\"throttledEnrichShodanMultiple([${nodesArray.map(id => `'${nodes.get(id).domain}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])\">Enrich via Shodan</button>\n                <button onclick=\"throttledEnrichHudsonRockDomainMultiple([${nodesArray.map(id => `'${nodes.get(id).domain}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])\">Enrich via Hudson Rock</button>\n                <button onclick=\"throttledSendHttpsRequestMultiple([${nodesArray.map(id => `'${nodes.get(id).domain}'`).join(',')}], 'domain', 'https')\">Send HTTPS Request</button>\n                <button onclick=\"throttledSendHttpsRequestMultiple([${nodesArray.map(id => `'${nodes.get(id).domain}'`).join(',')}], 'domain', 'http')\">Send HTTP Request</button>\n                <button onclick=\"throttledEnrichURLscan('https://${nodes.get(nodesArray[0]).domain}', ${nodesArray[0]})\">Enrich via URLscan.io</button>\n                <button onclick=\"throttledEnrichSecurityTrailsDomainMultiple([${nodesArray.map(id => `'${nodes.get(id).domain}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])\">Enrich via SecurityTrails</button>\n            `;\n        } else if (type === 'html_hash') {\n            menuHtml += `\n                <button onclick=\"throttledSearchShodanHtmlHash('${nodesArray[0]}', '${nodes.get(nodesArray[0]).hash}')\">Search Shodan for IPs with this HTML Hash</button>\n            `;\n        } else if (type === 'contact') {\n            menuHtml += `\n                <button onclick=\"throttledEnrichHudsonRockMultiple([${nodesArray.map(id => `'${nodes.get(id).email}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])\">Enrich via Hudson Rock</button>\n            `;\n        }\n    } else if (type === 'url') {\n            menuHtml += `\n                <button onclick=\"throttledEnrichURLhausMultiple([${nodesArray.map(id => `'${nodes.get(id).url}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])\">Enrich via URLhaus</button>\n            `;\n        \n    }\n\n    menu.innerHTML = menuHtml;\n    const canvasOffset = container.getBoundingClientRect();\n    menu.style.left = `${x + canvasOffset.left}px`;\n    menu.style.top = `${y + canvasOffset.top}px`;\n    menu.style.display = 'block';\n    document.addEventListener('click', hideContextMenu);\n}\n\n\n// why is this here?\nlet linkFromNode = null;\n// set variable above - move to top\n\nfunction saveState() {\n    try {\n        const state = {\n            nodes: nodes.get().map(node => {\n                // Base properties common to all nodes\n                const nodeData = {\n                    id: node.id,\n                    type: node.type,\n                    label: node.label,\n                    title: node.title,\n                    color: node.color,\n                    x: node.x,\n                    y: node.y,\n                    size: node.size,\n                    originalLabel: node.originalLabel,\n                    notes: node.notes // Include notes for all types\n                };\n                if (node.type === 'subnet') {\n                nodeData.subnet = node.subnet;\n                nodeData.isPrivate = node.isPrivate;\n            }\n                // Add type-specific properties\n                switch (node.type) {\n                    case 'contact':\n                        nodeData.name = node.name;\n                        nodeData.email = node.email;\n                        break;\n                    case 'ip':\n                        nodeData.ip = node.ip;\n                        break;\n                    case 'domain':\n                        nodeData.domain = node.domain;\n                        break;\n                    case 'organization':\n                        nodeData.organization = node.organization;\n                        break;\n                    case 'port':\n                        nodeData.portType = node.portType;\n                        nodeData.portNumber = node.portNumber;\n                        break;\n                    case 'wallet':\n                        nodeData.address = node.address;\n                        break;\n                    case 'bank':\n                        nodeData.accountNumber = node.accountNumber;\n                        nodeData.sortCode = node.sortCode;\n                        break;\n                    case 'technology':\n                        nodeData.techName = node.techName;\n                        nodeData.techVersion = node.techVersion;\n                        break;\n                    case 'device':\n                        nodeData.deviceCategory = node.deviceCategory;\n                        nodeData.deviceName = node.deviceName;\n                        break;\n                    case 'malware':\n                        nodeData.malwareName = node.malwareName;\n                        nodeData.malwareType = node.malwareType;\n                        break;\n                    case 'vulnerability':\n                        nodeData.vulnName = node.vulnName;\n                        nodeData.cve = node.cve;\n                        nodeData.url = node.url;\n                        break;\n                    case 'favicon':\n                        nodeData.hash = node.hash;\n                        nodeData.location = node.location;\n                        break;\n                    case 'http_hash':\n                    case 'html_hash':\n                    case 'ssl_hash':\n                        nodeData.hash = node.hash;\n                        break;\n                    case 'asn':\n                        nodeData.asn = node.asn;\n                        break;\n                    case 'city':\n                        nodeData.city = node.city;\n                        break;\n                    case 'country':\n                        nodeData.country = node.country;\n                        break;\n                    case 'os':\n                        nodeData.os = node.os;\n                        break;\n                    case 'product':\n                        nodeData.product = node.product;\n                        break;\n                    case 'http_title':\n                        nodeData.title = node.title; // Already in base properties, but explicit for clarity\n                        break;\n                    case 'vpn':\n                    case 'proxy':\n                    case 'tor':\n                    case 'relay':\n                    case 'hosting':\n                        nodeData.value = node.value;\n                        break;\n                    case 'tag':\n                        nodeData.tag = node.tag;\n                        break;\n                    case 'cpe':\n                        nodeData.cpe = node.cpe;\n                        break;\n                    case 'service':\n                        nodeData.name = node.name;\n                        break;\n                    case 'timestamp':\n                        nodeData.timestamp = node.timestamp;\n                        break;\n                    case 'url':\n                        nodeData.url = node.url;\n                        break;\n                    case 'port_title':\n                        nodeData.portType = node.portType;\n                        nodeData.portNumber = node.portNumber;\n                        nodeData.title = node.title;\n                        break;\n                    case 'hash': // Add this case for file hashes\n                        nodeData.hash = node.hash;\n                        nodeData.hashType = node.hashType;\n                        break;\n                    case 'txt':\n                        nodeData.text = node.text;\n                        break;\n                    default:\n                        console.warn(`Unhandled node type in saveState: ${node.type}`);\n                }\n\n                return nodeData;\n            }),\n            edges: edges.get().map(edge => ({\n                id: edge.id,\n                from: edge.from,\n                to: edge.to,\n                label: edge.label,\n                originalLabel: edge.originalLabel\n            })),\n            nextId: nextId,\n            isDarkMode: isDarkMode,\n            isPhysicsPaused: isPhysicsPaused,\n            nodeLabelsVisible: nodeLabelsVisible,\n            edgeLabelsVisible: edgeLabelsVisible\n        };\n\n        localStorage.setItem('networkGraphState', JSON.stringify(state));\n        console.log('State saved successfully');\n    } catch (e) {\n        console.error('Failed to save state:', e);\n        showToast('Failed to save state: ' + e.message, 'error');\n    }\n}\n\n\n\nconst throttledSearchShodanHtmlHash = throttleRequest(async function searchShodanHtmlHash(htmlHashNodeId, htmlHash, signal) {\n    if (!shodanApiKey) {\n        showToast('Please set your Shodan API key in the \"API Keys\" tab first.', 'error');\n        return;\n    }\n\n    // Validate htmlHash\n    const hashNum = parseInt(htmlHash);\n    if (!Number.isInteger(hashNum)) {\n        showToast(`Invalid HTML Hash: ${htmlHash}. Must be an integer.`, 'error');\n        return;\n    }\n\n    network.setOptions({ physics: { enabled: false } });\n    showProgressBar();\n    document.getElementById('progress-bar').textContent = `Searching Shodan for IPs with HTML Hash ${htmlHash.substring(0, 8)}...`;\n\n    try {\n        // Construct the URL to match the Bash script exactly\n        const query = `http.html_hash:${hashNum}`; // No encoding here yet\n        const baseUrl = `https://api.shodan.io/shodan/host/search?key=${shodanApiKey}&query=${query}`;\n        const url = routeViaProxy ? `${corsProxyUrl}/${baseUrl}` : baseUrl;\n\n        console.log('Search Shodan Base URL (before proxy):', baseUrl);\n        console.log('Search Shodan Final URL:', url);\n        console.log('routeViaProxy:', routeViaProxy, 'corsProxyUrl:', corsProxyUrl);\n\n        const response = await fetch(url, {\n            method: 'GET',\n            signal: signal,\n            headers: {\n                'Accept': 'application/json' // Ensure JSON response\n            }\n        });\n\n        if (!response.ok) {\n            const errorText = await response.text();\n            console.log('Search Shodan Status:', response.status, response.statusText);\n            console.log('Search Shodan Response Body:', errorText);\n            console.log('Request Headers:', Object.fromEntries(response.headers.entries()));\n            throw new Error(`Failed to fetch Shodan data: ${response.statusText} - ${errorText}`);\n        }\n\n        const data = await response.json();\n        console.log('Shodan Response:', data);\n\n        if (!data.matches || data.matches.length === 0) {\n            showToast(`No IPs found on Shodan with HTML Hash ${htmlHash}`, 'info');\n            completeProgressBar();\n            await stabilizeNetwork();\n            return;\n        }\n\n        // Process the matches\n        const newNodes = [];\n        const newEdges = [];\n        let successfulAdditions = 0;\n        const totalMatches = data.matches.length;\n\n        const existingIPs = new Map(nodes.get({ filter: n => n.type === 'ip' }).map(n => [n.ip, n.id]));\n\n        for (const match of data.matches) {\n            if (activeTaskController && activeTaskController.signal.aborted) {\n                showToast('Shodan HTML Hash search stopped', 'info');\n                break;\n            }\n\n            const ip = match.ip_str;\n            let ipId = existingIPs.get(ip);\n\n            if (!ipId) {\n                ipId = nextId++;\n                newNodes.push({\n                    id: ipId,\n                    type: 'ip',\n                    label: `IP: ${ip}`,\n                    title: `IP Address: ${ip}\\nFrom Shodan HTML Hash Search`,\n                    color: { background: '#f87171' },\n                    ip: ip,\n                    size: 20\n                });\n                existingIPs.set(ip, ipId);\n            }\n\n            const edgeId = `${ipId}-${htmlHashNodeId}-SharesHash`;\n            if (!edges.get(edgeId) && !newEdges.some(e => e.id === edgeId)) {\n                newEdges.push({\n                    id: edgeId,\n                    from: ipId,\n                    to: htmlHashNodeId,\n                    label: 'Shares HTML Hash'\n                });\n            }\n\n            successfulAdditions++;\n            const progress = ((successfulAdditions / totalMatches) * 100).toFixed(1);\n            document.getElementById('progress-bar').textContent = `Shodan HTML Hash Search: ${successfulAdditions}/${totalMatches} IPs (${progress}%)`;\n        }\n\n        if (newNodes.length > 0) nodes.add(newNodes);\n        if (newEdges.length > 0) edges.add(newEdges);\n\n        updateNodeSizes();\n        updateSelectOptions();\n        await stabilizeNetwork();\n        //ensureInteractionSettings();\n\n        completeProgressBar();\n        showToast(`Found and added ${successfulAdditions} IPs with HTML Hash ${htmlHash.substring(0, 8)}...`, 'success');\n    } catch (error) {\n        if (error.name === 'AbortError') {\n            showToast('Shodan HTML Hash search was cancelled', 'info');\n        } else {\n            console.error(`Error searching Shodan for HTML Hash ${htmlHash}: ${error.message}`);\n            showToast(`Error searching Shodan: ${error.message}. Check CORS proxy in Config tab.`, 'error');\n        }\n        completeProgressBar();\n        await stabilizeNetwork();\n    }\n}, SHODAN_RATE_LIMIT_MS);\n\n\n\nfunction cancelLinkCreation() {\n    if (linkFromNode) {\n        // Reset visual feedback\n        const originalNode = nodes.get(linkFromNode);\n        nodes.update({\n            id: linkFromNode,\n            color: { border: isDarkMode ? '#94a3b8' : '#6b7280', background: originalNode.color.background }\n        });\n        \n        showToast('Link creation cancelled', 'info');\n        linkFromNode = null;\n        \n        // Remove temporary listeners\n        network.off('oncontext', handleLinkDestination);\n        network.off('click', cancelLinkCreation);\n    }\n}\n\n\n        function hideContextMenu() {\n            document.getElementById('contextMenu').style.display = 'none';\n            document.removeEventListener('click', hideContextMenu);\n        }\n\n        const throttledSendHttpsRequest = throttleRequest(async function sendHttpsRequest(target, type) {\n    network.setOptions({ physics: { enabled: false } });\n    let url = type === 'ip' ? \n        `https://${target}` : \n        `https://${target}`;\n    url = constructUrl(url);\n    let message;\n\n    try {\n        const response = await fetch(url, {\n            method: 'GET',\n            headers: {\n                'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',\n                'Origin': window.location.origin\n            },\n            mode: 'cors',\n            credentials: 'omit'\n        });\n\n        const status = response.status;\n        const statusText = response.statusText;\n        const headers = {};\n        response.headers.forEach((value, key) => {\n            headers[key] = value;\n        });\n\n        let body = '';\n        const contentType = headers['content-type'] || '';\n        if (contentType.includes('text') || contentType.includes('html') || contentType.includes('json')) {\n            body = await response.text();\n            if (body.length > 500) {\n                body = body.substring(0, 500) + '... (truncated)';\n            }\n        } else {\n            body = '(Binary or unsupported content type)';\n        }\n\n        message = `\n            HTTPS Request to https://${target}\n            Status: ${status} ${statusText}\n            Headers: ${JSON.stringify(headers, null, 2)}\n            Body: ${body}\n        `.trim();\n        showToast(message, 'success');\n    } catch (error) {\n        message = `\n            HTTPS Request to https://${target}\n            Error: ${error.message}\n            Note: Ensure the CORS proxy (${corsProxyUrl}) is active and correctly configured\n        `.trim();\n        showToast(message, 'error');\n    } finally {\n        await stabilizeNetwork();\n    }\n});\n\n        function saveCorsProxyUrl() {\n    corsProxyUrl = document.getElementById('corsProxyUrl').value.trim();\n    const storeProxy = document.getElementById('storeCorsProxy').checked;\n    routeViaProxy = document.getElementById('routeViaProxy').checked;\n    ignoreApiKeysViaProxy = document.getElementById('ignoreApiKeysViaProxy').checked;\n\n    if (corsProxyUrl) {\n        if (storeProxy) {\n            localStorage.setItem('corsProxyUrl', corsProxyUrl);\n            localStorage.setItem('routeViaProxy', routeViaProxy);\n            localStorage.setItem('ignoreApiKeysViaProxy', ignoreApiKeysViaProxy);\n            showToast('CORS Proxy settings saved successfully!', 'success');\n        } else {\n            localStorage.removeItem('corsProxyUrl');\n            localStorage.removeItem('routeViaProxy');\n            localStorage.removeItem('ignoreApiKeysViaProxy');\n            showToast('CORS Proxy settings set for this session only', 'success');\n        }\n    } else {\n        corsProxyUrl = 'http://localhost:3000/proxy?url=';\n        routeViaProxy = false;\n        ignoreApiKeysViaProxy = false;\n        localStorage.removeItem('corsProxyUrl');\n        localStorage.removeItem('routeViaProxy');\n        localStorage.removeItem('ignoreApiKeysViaProxy');\n        document.getElementById('corsProxyUrl').value = corsProxyUrl;\n        document.getElementById('routeViaProxy').checked = false;\n        document.getElementById('ignoreApiKeysViaProxy').checked = false;\n        showToast('CORS Proxy settings reset to default', 'info');\n    }\n}\n\nfunction constructUrl(baseUrl, useApiKey = true) {\n    if (routeViaProxy) {\n        // Ensure corsProxyUrl doesn't end with a slash and baseUrl doesn't start with one\n        const cleanProxyUrl = corsProxyUrl.replace(/\\/+$/, ''); // Remove trailing slashes\n        const cleanBaseUrl = baseUrl.replace(/^\\/+/, '');      // Remove leading slashes\n        return `${cleanProxyUrl}${cleanBaseUrl}`;\n    }\n    return useApiKey ? baseUrl : baseUrl.split('?')[0]; // Remove query params if ignoring API keys\n}\n\n function showToast(message, type = 'info') {\n    console.log('Showing toast:', message, type);\n    const topBarHeight = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--top-bar-height')) || 40;\n    const buffer = 20;\n    const toastOptions = {\n        text: message,\n        duration: 6500,\n        position: \"center\",\n        style: {\n            background: isDarkMode ? '#2d3748' : '#fff',\n            color: isDarkMode ? '#e2e8f0' : '#1f2a44',\n            border: `1px solid ${isDarkMode ? '#4b5563' : '#d1d5db'}`,\n            boxShadow: isDarkMode ? '0 2px 10px rgba(0, 0, 0, 0.3)' : '0 2px 10px rgba(0, 0, 0, 0.1)',\n            position: 'fixed',\n            top: `${topBarHeight + buffer}px`,\n            left: '50%',\n            transform: 'translateX(-50%)',\n            width: 'auto',\n            maxWidth: '80%',\n            zIndex: 3000\n        }\n    };\n    switch (type) {\n        case 'success': toastOptions.style.background = isDarkMode ? '#166534' : '#22c55e'; toastOptions.style.color = '#fff'; break;\n        case 'error': toastOptions.style.background = isDarkMode ? '#991b1b' : '#ef4444'; toastOptions.style.color = '#fff'; break;\n    }\n    const toast = Toastify(toastOptions);\n    console.log('Toast instance created:', toast);\n    setTimeout(() => {\n        toast.showToast();\n        console.log('Toast should now be visible');\n    }, 100); // Small delay to ensure rendering\n}\n\nasync function stabilizeNetwork(skipFit = false) {\n    return new Promise(resolve => {\n        network.setOptions({\n            physics: {\n                enabled: true,\n                stabilization: {\n                    enabled: true,\n                    iterations: 200,\n                    updateInterval: 50\n                },\n                barnesHut: {\n                    gravitationalConstant: -8000,\n                    centralGravity: 0.3,\n                    springLength: 150,\n                    avoidOverlap: 1.0,\n                    damping: 0.9\n                }\n            },\n            // Explicitly preserve interaction settings\n            interaction: { \n                ...baseInteractionOptions,\n                zoomView: true,  // Ensure zooming is enabled\n                //dragView: true,\n                zoomSpeed: 0.5\n            }\n        });\n\n        network.stabilize(200);\n        network.once('stabilizationIterationsDone', () => {\n            network.setOptions({\n                physics: {\n                    enabled: !isPhysicsPaused,\n                    stabilization: { enabled: false }\n                },\n                interaction: { \n                    ...baseInteractionOptions,\n                    zoomView: true,\n                    //dragView: true,\n                    zoomSpeed: 0.5\n                }\n            });\n\n            if (!skipFit) {\n                network.fit({\n                    animation: {\n                        duration: 500,\n                        easingFunction: 'easeInOutQuad'\n                    }\n                });\n            }\n\n            setTimeout(() => {\n                const boundingBox = network.getBoundingBox();\n                if (boundingBox && nodes.length > 0) {\n                    const margin = 50;\n                    nodes.forEach(node => {\n                        const { x, y } = network.getPositions([node.id])[node.id] || { x: 0, y: 0 };\n                        nodes.update({\n                            id: node.id,\n                            x: Math.max(boundingBox.left + margin, Math.min(boundingBox.right - margin, x)),\n                            y: Math.max(boundingBox.top + margin, Math.min(boundingBox.bottom - margin, y))\n                        });\n                    });\n                }\n                resolve();\n                \n            }, skipFit ? 0 : 550);\n            ensureInteractionSettings(); // Ensure panning is enabled\n        });\n    });\n}\n\n\n\nconst throttledEnrichInternetDB = throttleRequest(async function enrichInternetDB(ip, ipNodeId, isBulk = false) {\n    network.setOptions({ physics: { enabled: false } });\n    try {\n        const url = constructUrl(`https://internetdb.shodan.io/${ip}`);\n        const response = await fetch(url);\n        if (!response.ok) throw new Error('Failed to fetch InternetDB data');\n        const data = await response.json();\n\n        // Deduplication maps\n        const existingPorts = new Map(nodes.get({ filter: n => n.type === 'port' }).map(n => [`${n.portType}/${n.portNumber}`, n.id]));\n        const existingDomains = new Map(nodes.get({ filter: n => n.type === 'domain' }).map(n => [n.domain, n.id]));\n        const existingVulns = new Map(nodes.get({ filter: n => n.type === 'vulnerability' }).map(n => [n.cve, n.id]));\n        const existingTags = new Map(nodes.get({ filter: n => n.type === 'tag' }).map(n => [n.tag, n.id]));\n        const existingCPEs = new Map(nodes.get({ filter: n => n.type === 'cpe' }).map(n => [n.cpe, n.id]));\n\n        const newNodes = [];\n        const newEdges = [];\n\n        // Ports (existing logic with deduplication)\n        if (data.ports && data.ports.length > 0) {\n            data.ports.forEach(port => {\n                const portKey = `TCP/${port}`;\n                let portId = existingPorts.get(portKey);\n                if (!portId) {\n                    portId = nextId++;\n                    newNodes.push({ \n                        id: portId, \n                        type: 'port', \n                        label: `TCP/${port}`, \n                        title: `Port\\nType: TCP\\nNumber: ${port}`, \n                        color: { background: '#a78bfa' }, \n                        portType: 'TCP', \n                        portNumber: port.toString() \n                    });\n                    existingPorts.set(portKey, portId);\n                }\n                const edgeId = `${ipNodeId}-${portId}-Exposes`;\n                if (!edges.get(edgeId) && !newEdges.some(e => e.id === edgeId)) {\n                    newEdges.push({ id: edgeId, from: ipNodeId, to: portId, label: 'Exposes' });\n                }\n            });\n        }\n\n        // Hostnames (existing logic with deduplication)\n        if (data.hostnames && data.hostnames.length > 0) {\n            data.hostnames.forEach(hostname => {\n                let domainId = existingDomains.get(hostname);\n                if (!domainId) {\n                    domainId = nextId++;\n                    newNodes.push({ \n                        id: domainId, \n                        type: 'domain', \n                        label: hostname, \n                        title: `Domain: ${hostname}`, \n                        color: { background: '#60a5fa' }, \n                        domain: hostname \n                    });\n                    existingDomains.set(hostname, domainId);\n                }\n                const edgeId = `${ipNodeId}-${domainId}-ResolvesTo`;\n                if (!edges.get(edgeId) && !newEdges.some(e => e.id === edgeId)) {\n                    newEdges.push({ id: edgeId, from: ipNodeId, to: domainId, label: 'Resolves to' });\n                }\n            });\n        }\n\n        // Vulnerabilities (existing logic with deduplication)\n        if (data.cves && data.cves.length > 0) {\n            data.cves.forEach(cve => {\n                let cveId = existingVulns.get(cve);\n                if (!cveId) {\n                    cveId = nextId++;\n                    newNodes.push({ \n                        id: cveId, \n                        type: 'vulnerability', \n                        label: `Vulnerability: ${cve}`, \n                        title: `Vulnerability\\nName: ${cve}\\nCVE: ${cve}`, \n                        color: { background: '#dc2626' }, \n                        vulnName: cve, \n                        cve: cve,\n                        url: `https://nvd.nist.gov/vuln/detail/${cve}`\n                    });\n                    existingVulns.set(cve, cveId);\n                }\n                const edgeId = `${ipNodeId}-${cveId}-Has`;\n                if (!edges.get(edgeId) && !newEdges.some(e => e.id === edgeId)) {\n                    newEdges.push({ id: edgeId, from: ipNodeId, to: cveId, label: 'Has' });\n                }\n            });\n        }\n\n        // Tags (new)\n        if (data.tags && data.tags.length > 0) {\n            data.tags.forEach(tag => {\n                let tagId = existingTags.get(tag);\n                if (!tagId) {\n                    tagId = nextId++;\n                    newNodes.push({\n                        id: tagId,\n                        type: 'tag',\n                        label: `Tag: ${tag}`,\n                        title: `Tag: ${tag}`,\n                        color: { background: '#6d28d9' },\n                        tag: tag\n                    });\n                    existingTags.set(tag, tagId);\n                }\n                const edgeId = `${ipNodeId}-${tagId}-Tagged`;\n                if (!edges.get(edgeId) && !newEdges.some(e => e.id === edgeId)) {\n                    newEdges.push({ id: edgeId, from: ipNodeId, to: tagId, label: 'Tagged' });\n                }\n            });\n        }\n\n        // CPEs (new)\n        if (data.cpes && data.cpes.length > 0) {\n            data.cpes.forEach(cpe => {\n                let cpeId = existingCPEs.get(cpe);\n                if (!cpeId) {\n                    cpeId = nextId++;\n                    newNodes.push({\n                        id: cpeId,\n                        type: 'cpe',\n                        label: `CPE: ${cpe.split(':')[3] || cpe}`,\n                        title: `CPE: ${cpe}`,\n                        color: { background: '#0d9488' },\n                        cpe: cpe\n                    });\n                    existingCPEs.set(cpe, cpeId);\n                }\n                const edgeId = `${ipNodeId}-${cpeId}-Runs`;\n                if (!edges.get(edgeId) && !newEdges.some(e => e.id === edgeId)) {\n                    newEdges.push({ id: edgeId, from: ipNodeId, to: cpeId, label: 'Runs' });\n                }\n            });\n        }\n\n        // Batch update\n        if (newNodes.length > 0) nodes.add(newNodes);\n        if (newEdges.length > 0) edges.add(newEdges);\n\n        updateNodeSizes();\n        updateSelectOptions();\n        await stabilizeNetwork();\n        //ensureInteractionSettings();\n        if (!isBulk) showToast(`IP ${ip} enrichment completed using InternetDB`, 'success');\n    } catch (error) {\n        console.error(`Error enriching IP ${ip} with InternetDB: ${error.message}`);\n        showToast(`Error enriching IP ${ip} with InternetDB: ${error.message}`, 'error');\n        await stabilizeNetwork();\n    }\n});\n\n \nasync function enrichAllShodan() {\n    if (!shodanApiKey && !ignoreApiKeysViaProxy) { \n        showToast('Please set your Shodan API key in the \"API Keys\" tab first.', 'error'); \n        return; \n    }\n    \n    showProgressBar();\n    const progressBar = document.getElementById('progress-bar');\n    network.setOptions({ physics: { enabled: false } });\n    const ipNodes = nodes.get({ filter: n => n.type === 'ip' && n.ip });\n    const totalIPs = ipNodes.length;\n    let successfulEnrichments = 0;\n    \n    if (totalIPs === 0) {\n        showToast('No IP nodes found to enrich', 'info');\n        completeProgressBar();\n        return;\n    }\n    \n    console.log(`Found ${totalIPs} IP nodes to enrich with Shodan`);\n    showToast(`Starting Shodan enrichment for ${totalIPs} IPs`, 'info');\n    \n    const batchSize = 5; // Smaller batch size to respect Shodan rate limits\n    const delayBetweenBatches = 100; // Small delay between batches\n    const shodanDelayMs = SHODAN_RATE_LIMIT_MS; // 1-second delay per request\n    const totalBatches = Math.ceil(totalIPs / batchSize);\n    const timePerBatchMs = shodanDelayMs * batchSize;\n    const totalBatchDelays = (totalBatches - 1) * delayBetweenBatches;\n    const estimatedTimeMs = (timePerBatchMs * totalBatches) + totalBatchDelays + 1000;\n    \n    const estimatedSeconds = Math.ceil(estimatedTimeMs / 1000);\n    const estimatedMinutes = Math.floor(estimatedSeconds / 60);\n    const remainingSeconds = estimatedSeconds % 60;\n    const timeEstimateStr = estimatedMinutes > 0 \n        ? `${estimatedMinutes}m ${remainingSeconds}s` \n        : `${estimatedSeconds}s`;\n    \n    showToast(`Estimated time for Shodan enrichment: ~${timeEstimateStr}`, 'info');\n    progressBar.textContent = `Shodan Enrichment: 0/${totalIPs} IPs (0%) - Est. ${timeEstimateStr}`;\n    \n    async function processBatch(batch) {\n        for (const node of batch) {\n            if (activeTaskController && activeTaskController.signal.aborted) {\n                return false;\n            }\n            try {\n                const baseUrl = ignoreApiKeysViaProxy ?\n                    `https://api.shodan.io/shodan/host/${node.ip}` :\n                    `https://api.shodan.io/shodan/host/${node.ip}?key=${shodanApiKey}`;\n                const url = constructUrl(baseUrl, !ignoreApiKeysViaProxy);\n                const response = await fetch(url, { signal: activeTaskController?.signal });\n                if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n                const data = await response.json();\n                console.log(`Shodan response for ${node.ip}:`, JSON.stringify(data, null, 2));\n\n                // Use the same processing logic as right-click enrichment\n                await processShodanData(node.id, data);\n                \n                successfulEnrichments++;\n            } catch (error) {\n                if (error.name === 'AbortError') {\n                    return false;\n                }\n                console.error(`Failed to enrich IP ${node.ip}: ${error.message}`);\n                showToast(`Failed to enrich IP ${node.ip}: ${error.message}`, 'error');\n            }\n            await new Promise(resolve => setTimeout(resolve, shodanDelayMs)); // Respect Shodan rate limit\n        }\n        return true;\n    }\n    \n    let lastProgressUpdate = 0;\n    const progressUpdateInterval = 500;\n    const startTime = Date.now();\n\n    try {\n        for (let i = 0; i < totalIPs; i += batchSize) {\n            if (activeTaskController && activeTaskController.signal.aborted) {\n                showToast('Shodan enrichment stopped', 'info');\n                progressBar.textContent = `Shodan Enrichment: Stopped at ${successfulEnrichments}/${totalIPs} IPs`;\n                break;\n            }\n            \n            const batch = ipNodes.slice(i, Math.min(i + batchSize, totalIPs));\n            console.log(`Processing batch ${Math.floor(i / batchSize) + 1} of ${totalBatches}, IPs ${i} to ${Math.min(i + batchSize - 1, totalIPs - 1)}`);\n            \n            const batchSuccess = await processBatch(batch);\n            \n            if (batchSuccess) {\n                updateNodeSizes();\n                updateSelectOptions();\n            }\n            \n            const currentTime = Date.now();\n            const processedIPs = Math.min(i + batchSize, totalIPs);\n            const progress = ((processedIPs / totalIPs) * 100).toFixed(1);\n            \n            if (currentTime - lastProgressUpdate >= progressUpdateInterval || processedIPs === totalIPs) {\n                const elapsedTimeMs = currentTime - startTime;\n                const timePerIp = successfulEnrichments > 0 ? elapsedTimeMs / successfulEnrichments : shodanDelayMs;\n                const remainingIPs = totalIPs - successfulEnrichments;\n                const remainingTimeMs = remainingIPs * timePerIp;\n                const remainingSeconds = Math.ceil(remainingTimeMs / 1000);\n                const remainingMinutes = Math.floor(remainingSeconds / 60);\n                const remainingSecondsPart = remainingSeconds % 60;\n                const remainingTimeStr = remainingMinutes > 0 \n                    ? `${remainingMinutes}m ${remainingSecondsPart}s` \n                    : `${remainingSeconds}s`;\n                \n                progressBar.textContent = `Shodan Enrichment: ${successfulEnrichments}/${totalIPs} IPs (${progress}%) - Est. ${remainingTimeStr} remaining`;\n                lastProgressUpdate = currentTime;\n            }\n            \n            await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));\n        }\n    } finally {\n        updateNodeSizes();\n        updateSelectOptions();\n        updateTheme();\n        \n        await stabilizeNetwork();\n        //ensureInteractionSettings();\n\n        if (!(activeTaskController && activeTaskController.signal.aborted)) {\n            completeProgressBar();\n            showToast(`Shodan enrichment completed: ${successfulEnrichments}/${totalIPs} IPs enriched`, 'success');\n        } else {\n            showToast(`Shodan enrichment stopped: ${successfulEnrichments}/${totalIPs} IPs processed`, 'info');\n        }\n        \n        if (window.innerWidth <= 768) {\n            const controls = document.getElementById('controls');\n            controls.classList.add('collapsed');\n            document.getElementById('myNetwork').style.display = 'block';\n            network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });\n        }\n    }\n}\n\nasync function enrichAllInternetDB() {\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const ipNodes = nodes.get({ filter: n => n.type === 'ip' && n.ip });\n    const totalIPs = ipNodes.length;\n    let successfulEnrichments = 0;\n    \n    console.log(`Found ${totalIPs} IP nodes to enrich with InternetDB`);\n    showToast(`Starting InternetDB enrichment for ${totalIPs} IPs`, 'info');\n    \n    const batchSize = 50;\n    const delayBetweenBatches = 200;\n    const totalBatches = Math.ceil(totalIPs / batchSize);\n    const assumedRequestTimeMs = 100;\n    const timePerBatchMs = assumedRequestTimeMs;\n    const totalBatchDelays = (totalBatches - 1) * delayBetweenBatches;\n    const estimatedTimeMs = (timePerBatchMs * totalBatches) + totalBatchDelays + 1000;\n    \n    const estimatedSeconds = Math.ceil(estimatedTimeMs / 1000);\n    const estimatedMinutes = Math.floor(estimatedSeconds / 60);\n    const remainingSeconds = estimatedSeconds % 60;\n    const timeEstimateStr = estimatedMinutes > 0 \n        ? `${estimatedMinutes}m ${remainingSeconds}s` \n        : `${estimatedSeconds}s`;\n    \n    showToast(`Estimated time for InternetDB enrichment: ~${timeEstimateStr}`, 'info');\n    document.getElementById('progress-bar').textContent = `InternetDB Enrichment: 0/${totalIPs} IPs (0%) - Est. ${timeEstimateStr}`;\n    \n    // Deduplication maps\n    const existingPorts = new Map(nodes.get({ filter: n => n.type === 'port' }).map(n => [`${n.portType}/${n.portNumber}`, n.id]));\n    const existingDomains = new Map(nodes.get({ filter: n => n.type === 'domain' }).map(n => [n.domain, n.id]));\n    const existingVulns = new Map(nodes.get({ filter: n => n.type === 'vulnerability' }).map(n => [n.cve, n.id]));\n    const existingTags = new Map(nodes.get({ filter: n => n.type === 'tag' }).map(n => [n.tag, n.id]));\n    const existingCPEs = new Map(nodes.get({ filter: n => n.type === 'cpe' }).map(n => [n.cpe, n.id]));\n    \n    const newNodes = [];\n    const newEdges = [];\n    \n    async function processBatch(batch) {\n        const promises = batch.map(node => {\n            if (activeTaskController && activeTaskController.signal.aborted) {\n                return Promise.resolve(null);\n            }\n            const url = constructUrl(`https://internetdb.shodan.io/${node.ip}`);\n            return fetch(url)\n                .then(response => {\n                    if (!response.ok) throw new Error('Failed to fetch InternetDB data');\n                    return response.json();\n                })\n                .then(data => {\n                    // Ports\n                    if (data.ports && data.ports.length > 0) {\n                        data.ports.forEach(port => {\n                            const portKey = `TCP/${port}`;\n                            let portId = existingPorts.get(portKey);\n                            if (!portId) {\n                                portId = nextId++;\n                                newNodes.push({\n                                    id: portId,\n                                    type: 'port',\n                                    label: `TCP/${port}`,\n                                    title: `Port\\nType: TCP\\nNumber: ${port}`,\n                                    color: { background: '#a78bfa' },\n                                    portType: 'TCP',\n                                    portNumber: port.toString()\n                                });\n                                existingPorts.set(portKey, portId);\n                            }\n                            const edgeId = `${node.id}-${portId}-Exposes`;\n                            if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                                newEdges.push({ id: edgeId, from: node.id, to: portId, label: 'Exposes' });\n                            }\n                        });\n                    }\n\n                    // Hostnames\n                    if (data.hostnames && data.hostnames.length > 0) {\n                        data.hostnames.forEach(hostname => {\n                            let domainId = existingDomains.get(hostname);\n                            if (!domainId) {\n                                domainId = nextId++;\n                                newNodes.push({\n                                    id: domainId,\n                                    type: 'domain',\n                                    label: `Domain: ${hostname}`,\n                                    title: `Domain: ${hostname}`,\n                                    color: { background: '#60a5fa' },\n                                    domain: hostname\n                                });\n                                existingDomains.set(hostname, domainId);\n                            }\n                            const edgeId = `${node.id}-${domainId}-ResolvesTo`;\n                            if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                                newEdges.push({ id: edgeId, from: node.id, to: domainId, label: 'Resolves to' });\n                            }\n                        });\n                    }\n\n                    // Vulnerabilities\n                    if (data.cves && data.cves.length > 0) {\n                        data.cves.forEach(cve => {\n                            let cveId = existingVulns.get(cve);\n                            if (!cveId) {\n                                cveId = nextId++;\n                                newNodes.push({\n                                    id: cveId,\n                                    type: 'vulnerability',\n                                    label: `Vulnerability: ${cve}`,\n                                    title: `Vulnerability\\nName: ${cve}\\nCVE: ${cve}`,\n                                    color: { background: '#dc2626' },\n                                    vulnName: cve,\n                                    cve: cve,\n                                    url: `https://nvd.nist.gov/vuln/detail/${cve}`\n                                });\n                                existingVulns.set(cve, cveId);\n                            }\n                            const edgeId = `${node.id}-${cveId}-Has`;\n                            if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                                newEdges.push({ id: edgeId, from: node.id, to: cveId, label: 'Has' });\n                            }\n                        });\n                    }\n\n                    // Tags\n                    if (data.tags && data.tags.length > 0) {\n                        data.tags.forEach(tag => {\n                            let tagId = existingTags.get(tag);\n                            if (!tagId) {\n                                tagId = nextId++;\n                                newNodes.push({\n                                    id: tagId,\n                                    type: 'tag',\n                                    label: `Tag: ${tag}`,\n                                    title: `Tag: ${tag}`,\n                                    color: { background: '#6d28d9' },\n                                    tag: tag\n                                });\n                                existingTags.set(tag, tagId);\n                            }\n                            const edgeId = `${node.id}-${tagId}-Tagged`;\n                            if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                                newEdges.push({ id: edgeId, from: node.id, to: tagId, label: 'Tagged' });\n                            }\n                        });\n                    }\n\n                    // CPEs\n                    if (data.cpes && data.cpes.length > 0) {\n                        data.cpes.forEach(cpe => {\n                            let cpeId = existingCPEs.get(cpe);\n                            if (!cpeId) {\n                                cpeId = nextId++;\n                                newNodes.push({\n                                    id: cpeId,\n                                    type: 'cpe',\n                                    label: `CPE: ${cpe.split(':')[3] || cpe}`,\n                                    title: `CPE: ${cpe}`,\n                                    color: { background: '#0d9488' },\n                                    cpe: cpe\n                                });\n                                existingCPEs.set(cpe, cpeId);\n                            }\n                            const edgeId = `${node.id}-${cpeId}-Runs`;\n                            if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                                newEdges.push({ id: edgeId, from: node.id, to: cpeId, label: 'Runs' });\n                            }\n                        });\n                    }\n\n                    successfulEnrichments++;\n                })\n                .catch(error => {\n                    console.error(`Failed to enrich IP ${node.ip}: ${error.message}`);\n                    showToast(`Failed to enrich IP ${node.ip}: ${error.message}`, 'error');\n                    return null;\n                });\n        });\n        await Promise.all(promises);\n    }\n    \n    let lastProgressUpdate = 0;\n    const progressUpdateInterval = 1000;\n    const startTime = Date.now();\n    \n    for (let i = 0; i < totalIPs; i += batchSize) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('InternetDB enrichment stopped', 'info');\n            break;\n        }\n        \n        const batch = ipNodes.slice(i, Math.min(i + batchSize, totalIPs));\n        console.log(`Processing batch ${Math.floor(i / batchSize) + 1} of ${totalBatches}, IPs ${i} to ${Math.min(i + batchSize - 1, totalIPs - 1)}`);\n        \n        await processBatch(batch);\n        \n        if (newNodes.length > 0) {\n            nodes.add(newNodes);\n            newNodes.length = 0;\n        }\n        if (newEdges.length > 0) {\n            edges.add(newEdges);\n            newEdges.length = 0;\n        }\n        \n        const currentTime = Date.now();\n        if (currentTime - lastProgressUpdate >= progressUpdateInterval) {\n            const processedIPs = Math.min(i + batchSize, totalIPs);\n            const progress = ((processedIPs / totalIPs) * 100).toFixed(1);\n            const remainingIPs = totalIPs - processedIPs;\n            const remainingTimeMs = Math.max(0, remainingIPs * assumedRequestTimeMs);\n            const remainingSeconds = Math.ceil(remainingTimeMs / 1000);\n            const remainingMinutes = Math.floor(remainingSeconds / 60);\n            const remainingSecondsPart = remainingSeconds % 60;\n            const remainingTimeStr = remainingMinutes > 0 \n                ? `${remainingMinutes}m ${remainingSecondsPart}s` \n                : `${remainingSeconds}s`;\n            \n            document.getElementById('progress-bar').textContent = \n                `InternetDB Enrichment: ${successfulEnrichments}/${totalIPs} IPs (${progress}%) - Est. ${remainingTimeStr} remaining`;\n            lastProgressUpdate = currentTime;\n            updateSelectOptions();\n        }\n        \n        await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));\n    }\n    \n    if (newNodes.length > 0) nodes.add(newNodes);\n    if (newEdges.length > 0) edges.add(newEdges);\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    //ensureInteractionSettings();\n    completeProgressBar();\n    showToast(`InternetDB enrichment completed: ${successfulEnrichments}/${totalIPs} IPs enriched`, 'success');\n    \n    if (window.innerWidth <= 768) {\n        const controls = document.getElementById('controls');\n        controls.classList.add('collapsed');\n        document.getElementById('myNetwork').style.display = 'block';\n        network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });\n    }\n}\n\nconst throttledEnrichIP = throttleRequest(async function enrichIP(ip, ipNodeId, isBulk = false, signal) {\n    if (!ipinfoApiKey && !ignoreApiKeysViaProxy) { \n        showToast('Please set your IPinfo API key in the \"API Keys\" tab first.', 'error'); \n        return; \n    }\n    network.setOptions({ physics: { enabled: false } });\n    try {\n        const baseUrl = ignoreApiKeysViaProxy ? \n            `https://ipinfo.io/${ip}/json` : \n            `https://ipinfo.io/${ip}/json?token=${ipinfoApiKey}`;\n        const url = constructUrl(baseUrl, !ignoreApiKeysViaProxy);\n        const response = await fetch(url, { signal });\n        if (!response.ok) throw new Error('Failed to fetch IP info');\n        const data = await response.json();\n        \n        const asn = data.asn?.asn || 'Unknown ASN';\n        const city = data.city || 'Unknown City';\n        const companyName = data.company?.name || 'Unknown Company';\n        const country = data.country || 'Unknown Country';\n        const privacy = data.privacy || { vpn: false, proxy: false, tor: false, relay: false, hosting: false };\n\n        const newNodes = [];\n        const newEdges = [];\n        \n        // Pre-collect existing nodes for deduplication\n        let existingAsns = new Map(nodes.get({ filter: n => n.type === 'asn' }).map(n => [n.asn, n.id]));\n        let existingCities = new Map(nodes.get({ filter: n => n.type === 'city' }).map(n => [n.city, n.id]));\n        let existingOrgs = new Map(nodes.get({ filter: n => n.type === 'organization' }).map(n => [n.organization, n.id]));\n        let existingCountries = new Map(nodes.get({ filter: n => n.type === 'country' }).map(n => [n.country, n.id]));\n        let existingPrivacyTypes = new Map([\n            ['vpn', null], ['proxy', null], ['tor', null], ['relay', null], ['hosting', null]\n        ].map(([type]) => {\n            const existing = nodes.get({ filter: n => n.type === type })[0];\n            return [type, existing ? existing.id : null];\n        }));\n\n        const privacyTypes = [\n            { key: 'vpn', label: 'VPN', color: '#9333ea' },\n            { key: 'proxy', label: 'Proxy', color: '#f43f5e' },\n            { key: 'tor', label: 'Tor', color: '#64748b' },\n            { key: 'relay', label: 'Relay', color: '#eab308' },\n            { key: 'hosting', label: 'Hosting', color: '#14b8a6' }\n        ];\n\n        // Helper function to add node and edge with unique ID\n        const addNodeAndEdge = (type, key, value, labelPrefix, title, color, edgeLabel) => {\n            let targetId = existingAsns.get(value) || existingCities.get(value) || existingOrgs.get(value) || existingCountries.get(value);\n            if (!targetId) {\n                targetId = nextId++;\n                newNodes.push({ \n                    id: targetId, \n                    type: type, \n                    label: `${labelPrefix}: ${value}`, \n                    title: title, \n                    color: { background: color }, \n                    [key]: value \n                });\n                if (type === 'asn') existingAsns.set(value, targetId);\n                else if (type === 'city') existingCities.set(value, targetId);\n                else if (type === 'organization') existingOrgs.set(value, targetId);\n                else if (type === 'country') existingCountries.set(value, targetId);\n            }\n            const edgeId = `${ipNodeId}-${targetId}-${edgeLabel}`;\n            if (!edges.get(edgeId) && !newEdges.some(e => e.id === edgeId)) {\n                newEdges.push({ id: edgeId, from: ipNodeId, to: targetId, label: edgeLabel });\n            }\n        };\n\n        // Add nodes and edges\n        addNodeAndEdge('asn', 'asn', asn, 'ASN', `ASN: ${asn}`, '#a3e635', 'Assigned to');\n        addNodeAndEdge('city', 'city', city, 'City', `City: ${city}`, '#f97316', 'Located in');\n        addNodeAndEdge('organization', 'organization', companyName, 'Organization', `Company: ${companyName}`, '#facc15', 'Belongs to');\n        addNodeAndEdge('country', 'country', country, 'Country', `Country: ${country}`, '#34d399', 'Located in');\n\n        // Privacy Types\n        privacyTypes.forEach(privacyType => {\n            if (privacy[privacyType.key]) {\n                let privacyNodeId = existingPrivacyTypes.get(privacyType.key);\n                if (!privacyNodeId) {\n                    privacyNodeId = nextId++;\n                    newNodes.push({ \n                        id: privacyNodeId, \n                        type: privacyType.key, \n                        label: privacyType.label, \n                        title: privacyType.label, \n                        color: { background: privacyType.color }\n                    });\n                    existingPrivacyTypes.set(privacyType.key, privacyNodeId);\n                }\n                const edgeId = `${ipNodeId}-${privacyNodeId}-Uses`;\n                if (!edges.get(edgeId) && !newEdges.some(e => e.id === edgeId)) {\n                    newEdges.push({ id: edgeId, from: ipNodeId, to: privacyNodeId, label: 'Uses' });\n                }\n            }\n        });\n\n        // Batch update\n        if (newNodes.length > 0) nodes.add(newNodes);\n        if (newEdges.length > 0) edges.add(newEdges);\n\n        updateNodeSizes();\n        updateSelectOptions();\n        await stabilizeNetwork();\n        if (!isBulk) showToast(`IP ${ip} enrichment completed using IPinfo`, 'success');\n    } catch (error) {\n        if (error.name === 'AbortError') {\n            showToast(`Enrichment of IP ${ip} was cancelled`, 'info');\n            return;\n        }\n        console.error(`Error enriching IP ${ip}: ${error.message}`);\n        showToast(`Error enriching IP ${ip}: ${error.message}`, 'error');\n        await stabilizeNetwork();\n    }\n}, RATE_LIMIT_MS);\n\n\nconst throttledEnrichShodan = throttleRequest(async function enrichShodan(ip, ipNodeId, isBulk = false, signal) {\n    if (!shodanApiKey && !ignoreApiKeysViaProxy) { \n        showToast('Please set your Shodan API key in the \"API Keys\" tab first.', 'error'); \n        return; \n    }\n    \n    if (!isBulk) network.setOptions({ physics: { enabled: false } });\n    \n    try {\n        const baseUrl = ignoreApiKeysViaProxy ? \n            `https://api.shodan.io/shodan/host/${ip}` : \n            `https://api.shodan.io/shodan/host/${ip}?key=${shodanApiKey}`;\n        const url = constructUrl(baseUrl, !ignoreApiKeysViaProxy);\n        const response = await fetch(url, { signal });\n        if (!response.ok) throw new Error(`Failed to fetch Shodan data: ${response.statusText}`);\n        const data = await response.json();\n\n        // Process Shodan data using the shared helper\n        await processShodanData(ipNodeId, data);\n\n        updateNodeSizes();\n        updateSelectOptions();\n        if (!isBulk) {\n            await stabilizeNetwork();\n            updateTheme();\n            showToast(`IP ${ip} enrichment completed using Shodan`, 'success');\n        }\n    } catch (error) {\n        if (error.name === 'AbortError') {\n            showToast(`Enrichment of IP ${ip} was cancelled`, 'info');\n            return;\n        }\n        console.error(`Error enriching IP ${ip} with Shodan: ${error.message}`);\n        showToast(`Error enriching IP ${ip} with Shodan: ${error.message}`, 'error');\n        if (!isBulk) await stabilizeNetwork();\n    }\n}, SHODAN_RATE_LIMIT_MS);\n\n\n\nasync function importIOCsFromText() {\n    let text = document.getElementById('iocText').value.trim();\n    if (!text) { \n        showToast('Please enter some text containing IOCs', 'error'); \n        return; \n    }\n    // Apply comprehensive refanging\n    text = refangText(text);\n    await processIOCs(text);\n    document.getElementById('iocText').value = '';\n    saveStateAfterOperation();\n    showToast('IOCs (including emails) import completed', 'success');\n}\n\nasync function importIOCsFromFile() {\n    const fileInput = document.getElementById('iocFile');\n    const file = fileInput.files[0];\n    if (!file) { \n        showToast('Please select a text file containing IOCs', 'error'); \n        return; \n    }\n    const reader = new FileReader();\n    reader.onload = async function(event) { \n        // Apply comprehensive refanging\n        let text = refangText(event.target.result);\n        await processIOCs(text); \n        fileInput.value = ''; \n        saveStateAfterOperation();\n        showToast('IOCs (including emails) import completed', 'success');\n    };\n    reader.readAsText(file);\n}\n\n\n\n\nfunction toggleMode() {\n    isDarkMode = !isDarkMode;\n    updateTheme();\n    document.getElementById('mode-toggle').textContent = isDarkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode';\n}\n\n        function togglePhysics() {\n    isPhysicsPaused = !isPhysicsPaused;\n    const pauseButton = document.getElementById('pause-toggle');\n    network.setOptions({ \n        physics: { \n            enabled: !isPhysicsPaused,\n            barnesHut: {\n                // Ensure consistent physics settings\n                gravitationalConstant: -8000,\n                centralGravity: 0.1,\n                springLength: 200,\n                springConstant: 0.04,\n                damping: 0.9,\n                avoidOverlap: 0.5\n            },\n            maxVelocity: 25,\n            minVelocity: 0.1\n        },\n        interaction: { ...baseInteractionOptions }\n    });\n    pauseButton.textContent = isPhysicsPaused ? 'Resume Physics' : 'Pause Physics';\n    pauseButton.classList.toggle('paused', isPhysicsPaused);\n    if (!isPhysicsPaused) {\n        // Trigger stabilization when resuming physics\n        stabilizeNetwork();\n    }\n    //ensureInteractionSettings();\n    }\n\n        function resetLayout() {\n            nodes.forEach(node => nodes.update({ id: node.id, x: undefined, y: undefined }));\n            stabilizeNetwork(false);\n        }\n\n        function setOrganicLayout() {\n    network.setOptions({\n        physics: {\n            enabled: true,\n            stabilization: {\n                enabled: true,\n                iterations: 200\n            },\n            barnesHut: {\n                gravitationalConstant: -8000,\n                centralGravity: 0.3,\n                springLength: 150,\n                springConstant: 0.05,\n                damping: 0.9,\n                avoidOverlap: 1.0\n            },\n            maxVelocity: 50,\n            minVelocity: 0.1\n        },\n        layout: { \n            hierarchical: false,\n            improvedLayout: false\n        },\n        interaction: { ...baseInteractionOptions }\n    });\n\n    // Reset node positions\n    nodes.forEach(node => {\n        nodes.update({ \n            id: node.id, \n            x: undefined, \n            y: undefined,\n            fixed: { x: false, y: false }\n        });\n    });\n\n    stabilizeNetwork().then(() => {\n        isPhysicsPaused = true;\n        network.setOptions({ physics: { enabled: false } });\n        const pauseButton = document.getElementById('pause-toggle');\n        pauseButton.textContent = 'Resume Physics';\n        pauseButton.classList.add('paused');\n        //ensureInteractionSettings();\n    });\n}\n\nfunction setCircularLayout() {\n    network.setOptions({\n        physics: { enabled: false },\n        layout: { hierarchical: false },\n        interaction: { ...baseInteractionOptions }\n    });\n\n    const containerRect = container.getBoundingClientRect();\n    const radius = Math.min(containerRect.width, containerRect.height) / 2 - 100;\n    const nodeCount = nodes.length;\n    const angleStep = (2 * Math.PI) / nodeCount;\n    const centerX = containerRect.width / 2;\n    const centerY = containerRect.height / 2;\n\n    nodes.forEach((node, i) => {\n        const x = centerX + radius * Math.cos(angleStep * i);\n        const y = centerY + radius * Math.sin(angleStep * i);\n        nodes.update({\n            id: node.id,\n            x: x,\n            y: y,\n            fixed: { x: true, y: true }\n        });\n    });\n\n    network.fit({\n        animation: {\n            duration: 500,\n            easingFunction: 'easeInOutQuad'\n        }\n    });\n}\n\nfunction setOrthogonalLayout() {\n    network.setOptions({\n        physics: { enabled: false },\n        layout: { hierarchical: false },\n        interaction: { ...baseInteractionOptions }\n    });\n\n    const containerRect = container.getBoundingClientRect();\n    const gridSize = Math.ceil(Math.sqrt(nodes.length));\n    const stepX = containerRect.width / (gridSize + 1);\n    const stepY = containerRect.height / (gridSize + 1);\n    let i = 0;\n\n    nodes.forEach(node => {\n        const x = (i % gridSize + 0.5) * stepX;\n        const y = (Math.floor(i / gridSize) + 0.5) * stepY;\n        nodes.update({\n            id: node.id,\n            x: x,\n            y: y,\n            fixed: { x: true, y: true }\n        });\n        i++;\n    });\n\n    network.fit({\n        animation: {\n            duration: 500,\n            easingFunction: 'easeInOutQuad'\n        }\n    });\n}\n\nfunction setTreeLayout() {\n    network.setOptions({\n        physics: { enabled: false },\n        layout: {\n            hierarchical: {\n                enabled: true,\n                levelSeparation: 200,\n                nodeSpacing: 150,\n                treeSpacing: 200,\n                direction: 'UD',\n                sortMethod: 'hubsize',\n                shakeTowards: 'leaves'\n            }\n        },\n        edges: {\n            smooth: {\n                enabled: true,\n                type: 'cubicBezier'\n            }\n        },\n        interaction: { ...baseInteractionOptions }\n    });\n\n    // Reset positions before applying layout\n    nodes.forEach(node => {\n        nodes.update({\n            id: node.id,\n            x: undefined,\n            y: undefined,\n            fixed: { x: false, y: false }\n        });\n    });\n\n    network.fit({\n        animation: {\n            duration: 500,\n            easingFunction: 'easeInOutQuad'\n        }\n    });\n}\n\nfunction setHierarchicalLayout() {\n    network.setOptions({\n        physics: { enabled: false },\n        layout: {\n            hierarchical: {\n                enabled: true,\n                levelSeparation: 200,\n                nodeSpacing: 150,\n                treeSpacing: 200,\n                direction: 'UD',\n                sortMethod: 'directed',\n                shakeTowards: 'roots'\n            }\n        },\n        edges: {\n            smooth: {\n                enabled: true,\n                type: 'cubicBezier'\n            }\n        },\n        interaction: { ...baseInteractionOptions }\n    });\n\n    // Reset positions before applying layout\n    nodes.forEach(node => {\n        nodes.update({\n            id: node.id,\n            x: undefined,\n            y: undefined,\n            fixed: { x: false, y: false }\n        });\n    });\n\n    network.fit({\n        animation: {\n            duration: 500,\n            easingFunction: 'easeInOutQuad'\n        }\n    });\n}\n\n        function showTab(tabId) {\n            document.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active'));\n            document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));\n            document.getElementById(tabId).classList.add('active');\n            document.querySelector(`.tab-button[onclick=\"showTab('${tabId}')\"]`).classList.add('active');\n        }\n\n        document.getElementById('addEntityType').addEventListener('change', function() {\n            let type = this.value;\n            document.getElementById('addVulnNameInput').style.display = type === 'vulnerability' ? 'block' : 'none';\n            document.getElementById('addVulnCVEInput').style.display = type === 'vulnerability' ? 'block' : 'none';\n            document.getElementById('addVulnUrlInput').style.display = type === 'vulnerability' ? 'block' : 'none';\n            document.getElementById('addNameInput').style.display = type === 'contact' ? 'block' : 'none';\n            document.getElementById('addEmailInput').style.display = type === 'contact' ? 'block' : 'none';\n            document.getElementById('addIpInput').style.display = type === 'ip' ? 'block' : 'none';\n            document.getElementById('addDomainInput').style.display = type === 'domain' ? 'block' : 'none';\n            document.getElementById('addOrgInput').style.display = type === 'organization' ? 'block' : 'none';\n            document.getElementById('addPortNumInput').style.display = type === 'port' ? 'block' : 'none';\n            document.getElementById('addPortType').style.display = type === 'port' ? 'block' : 'none';\n            document.getElementById('addWalletAddressInput').style.display = type === 'wallet' ? 'block' : 'none';\n            document.getElementById('addAccountNumberInput').style.display = type === 'bank' ? 'block' : 'none';\n            document.getElementById('addSortCodeInput').style.display = type === 'bank' ? 'block' : 'none';\n            document.getElementById('addTechNameInput').style.display = type === 'technology' ? 'block' : 'none';\n            document.getElementById('addTechVersionInput').style.display = type === 'technology' ? 'block' : 'none';\n            document.getElementById('addDeviceCategory').style.display = type === 'device' ? 'block' : 'none';\n            document.getElementById('addDeviceNameInput').style.display = type === 'device' ? 'block' : 'none';\n            document.getElementById('addMalwareNameInput').style.display = type === 'malware' ? 'block' : 'none';\n            document.getElementById('addMalwareType').style.display = type === 'malware' ? 'block' : 'none';\n            document.getElementById('addSubnetInput').style.display = type === 'subnet' ? 'block' : 'none';\n        });\n\n        function createNodeData(type, values) {\n    const nodeData = { \n        id: nextId++, \n        size: 10,\n        type,\n        widthConstraint: false,\n        heightConstraint: false\n    };\n    \n    // Define configs for all node types\n    const configs = {\n        vulnerability: { \n            fields: ['vulnName'], \n            optionalFields: ['cve', 'url', 'notes'], \n            color: '#dc2626',\n            label: v => `Vulnerability: ${v.vulnName}${v.cve ? '\\nCVE: ' + v.cve : ''}${v.url ? '\\nURL: ' + v.url : ''}`,\n            title: v => `Vulnerability\\nName: ${v.vulnName}${v.cve ? '\\nCVE: ' + v.cve : ''}${v.url ? '\\nURL: ' + v.url : ''}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        contact: { \n            fields: ['name'], \n            optionalFields: ['email', 'notes'], \n            color: '#4ade80', \n            label: v => `Contact: ${v.name}${v.email ? '\\n' + v.email : ''}`, \n            title: v => `Contact\\nName: ${v.name}${v.email ? '\\nEmail: ' + v.email : ''}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        ip: { \n            fields: ['ip'], \n            optionalFields: ['notes'],\n            color: '#f87171', \n            label: v => `IP: ${v.ip}`, \n            title: v => `IP Address: ${v.ip}${v.notes ? '\\nNotes: ' + v.notes : ''}`,\n            validate: v => {\n                const ipRegex = {\n                    ipv4: /^(?!0\\d)(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?!0\\d)(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?!0\\d)(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?!0\\d)(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,\n                    ipv6: /^((([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4})|(([0-9a-fA-F]{1,4}:){1,7}:)|(::([0-9a-fA-F]{1,4}:){0,5}[0-9a-fA-F]{1,4})|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|::)$/i\n                };\n                return ipRegex.ipv4.test(v.ip) || ipRegex.ipv6.test(v.ip);\n            }\n        },\n        domain: { \n            fields: ['domain'], \n            optionalFields: ['notes'],\n            color: '#60a5fa', \n            label: v => `Domain: ${v.domain}`, \n            title: v => `Domain: ${v.domain}${v.notes ? '\\nNotes: ' + v.notes : ''}` \n        },\n        organization: { \n            fields: ['organization'], \n            optionalFields: ['notes'],\n            color: '#facc15', \n            label: v => `Organization: ${v.organization}`, \n            title: v => `Organization: ${v.organization}${v.notes ? '\\nNotes: ' + v.notes : ''}` \n        },\n        port: { \n            fields: ['portNumber', 'portType'], \n            optionalFields: ['notes'],\n            color: '#a78bfa', \n            label: v => `Port: ${v.portType}/${v.portNumber}`, \n            title: v => `Port\\nType: ${v.portType}\\nNumber: ${v.portNumber}${v.notes ? '\\nNotes: ' + v.notes : ''}` \n        },\n        wallet: { \n            fields: ['address'], \n            optionalFields: ['notes'],\n            color: '#fb923c', \n            label: v => `Wallet: ${v.address}`, \n            title: v => `Wallet\\nAddress: ${v.address}${v.notes ? '\\nNotes: ' + v.notes : ''}` \n        },\n        bank: { \n            fields: ['accountNumber', 'sortCode'], \n            optionalFields: ['notes'],\n            color: '#10b981', \n            label: v => `Bank: ${v.accountNumber}\\nSort Code: ${v.sortCode}`, \n            title: v => `Bank Account\\nAccount Number: ${v.accountNumber}\\nSort Code: ${v.sortCode}${v.notes ? '\\nNotes: ' + v.notes : ''}` \n        },\n        technology: { \n            fields: ['techName'], \n            optionalFields: ['techVersion', 'notes'], \n            color: '#ec4899', \n            label: v => `Technology: ${v.techName}${v.techVersion ? '\\nVersion: ' + v.techVersion : ''}`, \n            title: v => `Technology\\nName: ${v.techName}${v.techVersion ? '\\nVersion: ' + v.techVersion : ''}${v.notes ? '\\nNotes: ' + v.notes : ''}` \n        },\n        device: { \n            fields: ['deviceCategory', 'deviceName'], \n            optionalFields: ['notes'],\n            color: '#14b8a6', \n            label: v => `Device: ${v.deviceName}\\nCategory: ${v.deviceCategory}`, \n            title: v => `Device\\nName: ${v.deviceName}\\nCategory: ${v.deviceCategory}${v.notes ? '\\nNotes: ' + v.notes : ''}` \n        },\n        malware: { \n            fields: ['malwareName', 'malwareType'], \n            optionalFields: ['notes'],\n            color: '#ef4444', \n            label: v => `Malware: ${v.malwareName}\\nType: ${v.malwareType}`, \n            title: v => `Malware\\nName: ${v.malwareName}\\nType: ${v.malwareType}${v.notes ? '\\nNotes: ' + v.notes : ''}` \n        },\n        favicon: { \n            fields: ['hash'], \n            optionalFields: ['location', 'notes'], \n            color: '#22d3ee',\n            label: v => `Favicon: ${v.hash.substring(0, 8)}...`, \n            title: v => `Favicon\\nHash: ${v.hash}${v.location ? '\\nPath: ' + v.location : ''}${v.notes ? '\\nNotes: ' + v.notes : ''}` \n        },\n        subnet: { \n            fields: ['subnet'], \n            optionalFields: ['notes'],\n            color: '#9333ea',\n            validate: v => {\n                const cidrRegex = /^(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})\\/(\\d{1,2})$/;\n                if (!cidrRegex.test(v.subnet)) return false;\n                const [ip, mask] = v.subnet.split('/');\n                const octets = ip.split('.').map(Number);\n                const maskNum = Number(mask);\n                return octets.every(o => o >= 0 && o <= 255) && maskNum >= 0 && maskNum <= 32;\n            }\n        },\n        mx: {\n            fields: ['hostname'],\n            optionalFields: ['notes'],\n            color: '#34d399', // Green for MX records\n            label: v => `MX: ${v.hostname.length > 30 ? v.hostname.substring(0, 27) + '...' : v.hostname}`,\n            title: v => `Mail Exchanger\\nHostname: ${v.hostname}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        txt: {\n            fields: ['text'],\n            optionalFields: ['notes'],\n            color: '#f59e0b', // Orange for TXT records\n            label: v => `TXT: ${v.text.length > 30 ? v.text.substring(0, 27) + '...' : v.text}`,\n            title: v => `TXT Record\\nValue: ${v.text}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        }\n    };\n\n    const config = configs[type];\n    if (!config || config.fields.some(f => !values[f])) { \n        showToast(`Please enter all required fields for ${type}`, 'error'); \n        return null; \n    }\n    if (config.validate && !config.validate(values)) { \n        showToast(`Invalid ${type} format`, 'error'); \n        return null; \n    }\n    \n    const allValues = { ...values };\n    if (config.optionalFields) config.optionalFields.forEach(f => { if (values[f]) allValues[f] = values[f]; });\n    \n    // Handle subnet type separately to set label and title directly\n    if (type === 'subnet') {\n        const [ip] = values.subnet.split('/');\n        const isPrivate = isPrivateIP(ip);\n        allValues.isPrivate = isPrivate;\n        nodeData.isPrivate = isPrivate;\n        nodeData.label = `Subnet: ${values.subnet} (${isPrivate ? 'Private' : 'Public'})`;\n        nodeData.title = `Subnet: ${values.subnet}\\nType: ${isPrivate ? 'Private' : 'Public'}${values.notes ? '\\nNotes: ' + values.notes : ''}`;\n        console.log(`Subnet ${values.subnet} classified as ${isPrivate ? 'Private' : 'Public'}`);\n        console.log(`Label set to: ${nodeData.label}`);\n        console.log(`Title set to: ${nodeData.title}`);\n    } else {\n        // For other types, use the config's label and title functions\n        nodeData.label = config.label ? config.label(allValues) : undefined;\n        nodeData.title = config.title ? config.title(allValues) : undefined;\n    }\n\n    nodeData.color = { background: config.color };\n    Object.assign(nodeData, allValues);\n\n    console.log(`Final nodeData: ${JSON.stringify(nodeData)}`);\n    return nodeData;\n}\n\n// Define isPrivateIP if not already defined elsewhere\nfunction isPrivateIP(ip) {\n    console.log(`isPrivateIP called with: ${ip}`);\n    const octets = ip.split('.').map(Number);\n    console.log(`Parsed octets: ${octets}`);\n    \n    if (octets.length !== 4 || octets.some(o => o < 0 || o > 255)) {\n        console.log(`Invalid IP format: ${ip}`);\n        return false;\n    }\n\n    const isPrivate = (\n        (octets[0] === 10) || // 10.0.0.0/8\n        (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) || // 172.16.0.0/12\n        (octets[0] === 192 && octets[1] === 168) // 192.168.0.0/16\n    );\n    \n    console.log(`IP ${ip} is ${isPrivate ? 'private' : 'public'}`);\n    return isPrivate;\n}\n\n\nfunction addNode() {\n    const type = document.getElementById('addEntityType').value;\n    \n    // Collect inputs based on entity type\n    const inputs = {\n        vulnerability: { \n            vulnName: document.getElementById('addVulnNameInput').value.trim(),\n            cve: document.getElementById('addVulnCVEInput').value.trim(),\n            url: document.getElementById('addVulnUrlInput').value.trim()\n        },\n        contact: { \n            name: document.getElementById('addNameInput').value.trim(),\n            email: document.getElementById('addEmailInput').value.trim()\n        },\n        ip: { \n            ip: document.getElementById('addIpInput').value.trim()\n        },\n        subnet: { \n            subnet: document.getElementById('addSubnetInput').value.trim()\n        },\n        domain: { \n            domain: document.getElementById('addDomainInput').value.trim()\n        },\n        organization: { \n            organization: document.getElementById('addOrgInput').value.trim()\n        },\n        port: { \n            portNumber: document.getElementById('addPortNumInput').value.trim(),\n            portType: document.getElementById('addPortType').value\n        },\n        wallet: { \n            address: document.getElementById('addWalletAddressInput').value.trim()\n        },\n        bank: { \n            accountNumber: document.getElementById('addAccountNumberInput').value.trim(),\n            sortCode: document.getElementById('addSortCodeInput').value.trim()\n        },\n        technology: { \n            techName: document.getElementById('addTechNameInput').value.trim(),\n            techVersion: document.getElementById('addTechVersionInput').value.trim()\n        },\n        device: { \n            deviceCategory: document.getElementById('addDeviceCategory').value,\n            deviceName: document.getElementById('addDeviceNameInput').value.trim()\n        },\n        malware: { \n            malwareName: document.getElementById('addMalwareNameInput').value.trim(),\n            malwareType: document.getElementById('addMalwareType').value\n        }\n    };\n\n    const nodeData = createNodeData(type, inputs[type]);\n    if (!nodeData) {\n        return;\n    }\n\n    // Check for duplicates based on key fields with corrected syntax\n    const existingNodes = nodes.get({\n        filter: n => n.type === type && (\n            (type === 'contact' && n.name === inputs[type].name && (!inputs[type].email || n.email === inputs[type].email)) ||\n            (type === 'ip' && n.ip === inputs[type].ip) ||\n            (type === 'domain' && n.domain === inputs[type].domain) ||\n            (type === 'organization' && n.organization === inputs[type].organization) ||\n            (type === 'port' && n.portNumber === inputs[type].portNumber && n.portType === inputs[type].portType) ||\n            (type === 'wallet' && n.address === inputs[type].address) ||\n            (type === 'bank' && n.accountNumber === inputs[type].accountNumber && n.sortCode === inputs[type].sortCode) ||\n            (type === 'technology' && n.techName === inputs[type].techName && n.techVersion === inputs[type].techVersion) ||\n            (type === 'device' && n.deviceCategory === inputs[type].deviceCategory && n.deviceName === inputs[type].deviceName) ||\n            (type === 'malware' && n.malwareName === inputs[type].malwareName && n.malwareType === inputs[type].malwareType) ||\n            (type === 'vulnerability' && n.vulnName === inputs[type].vulnName && \n             (!inputs[type].cve || n.cve === inputs[type].cve) && \n             (!inputs[type].url || n.url === inputs[type].url))\n        )\n    });\n\n    if (existingNodes.length > 0) {\n        showToast(`${type} already exists`, 'error');\n        return;\n    }\n\n    nodes.add({\n        ...nodeData,\n        size: 10,\n        widthConstraint: false,\n        heightConstraint: false\n    });\n\n    updateSelectOptions();\n    clearAddInputs();\n    updateNodeSizes();\n    stabilizeNetwork();\n    saveStateAfterOperation();\n    showToast(`${type} added successfully`, 'success');\n}\n\n\n        function editNode() {\n            const nodeId = document.getElementById('editNodeSelect').value;\n            if (!nodeId) { showToast('Please select a node to edit', 'error'); return; }\n            const node = nodes.get(parseInt(nodeId));\n            if (!node) { showToast('Selected node not found', 'error'); return; }\n\n            const type = node.type;\n            const inputs = {\n                contact: { name: document.getElementById('editNameInput').value, email: document.getElementById('editEmailInput').value },\n                ip: { ip: document.getElementById('editIpInput').value.trim() },\n                domain: { domain: document.getElementById('editDomainInput').value },\n                organization: { organization: document.getElementById('editOrgInput').value },\n                port: { portNumber: document.getElementById('editPortNumInput').value, portType: document.getElementById('editPortType').value },\n                wallet: { address: document.getElementById('editWalletAddressInput').value },\n                bank: { accountNumber: document.getElementById('editAccountNumberInput').value, sortCode: document.getElementById('editSortCodeInput').value },\n                technology: { techName: document.getElementById('editTechNameInput').value, techVersion: document.getElementById('editTechVersionInput').value },\n                device: { deviceCategory: document.getElementById('editDeviceCategory').value, deviceName: document.getElementById('editDeviceNameInput').value },\n                malware: { malwareName: document.getElementById('editMalwareNameInput').value, malwareType: document.getElementById('editMalwareType').value },\n                vulnerability: {vulnName: document.getElementById('editVulnNameInput').value.trim(),\n            cve: document.getElementById('editVulnCVEInput').value.trim(),\n            subnet: { subnet: document.getElementById('editSubnetInput').value.trim() },\n            url: document.getElementById('editVulnUrlInput').value.trim()\n            }\n            };\n            const updatedNodeData = createNodeData(type, inputs[type]);\n            if (updatedNodeData) {\n                const existingNode = nodes.get({ filter: n => n.id !== parseInt(nodeId) && n.type === type && Object.keys(inputs[type]).every(key => n[key] === inputs[type][key]) });\n                if (existingNode) { showToast(`Another ${type} with these values already exists`, 'error'); return; }\n                updatedNodeData.id = node.id;\n                nodes.update(updatedNodeData);\n                updateSelectOptions();\n                clearEditInputs();\n                stabilizeNetwork();\n                saveStateAfterOperation(); \n            }\n        }\n\n        function loadNodeForEdit() {\n            const nodeId = document.getElementById('editNodeSelect').value;\n            clearEditInputs();\n            if (!nodeId) return;\n\n            const node = nodes.get(parseInt(nodeId));\n            if (!node) return;\n\n            document.getElementById('editEntityType').value = node.type;\n            switch (node.type) {\n                case 'subnet':\n            document.getElementById('editSubnetInput').value = node.subnet || '';\n                    break;\n                case 'contact':\n                    document.getElementById('editNameInput').value = node.name || '';\n                    document.getElementById('editEmailInput').value = node.email || '';\n                    break;\n                case 'ip':\n                    document.getElementById('editIpInput').value = node.ip || '';\n                    break;\n                case 'domain':\n                    document.getElementById('editDomainInput').value = node.domain || '';\n                    break;\n                case 'organization':\n                    document.getElementById('editOrgInput').value = node.organization || '';\n                    break;\n                case 'port':\n                    document.getElementById('editPortNumInput').value = node.portNumber || '';\n                    document.getElementById('editPortType').value = node.portType || 'TCP';\n                    break;\n                case 'wallet':\n                    document.getElementById('editWalletAddressInput').value = node.address || '';\n                    break;\n                case 'bank':\n                    document.getElementById('editAccountNumberInput').value = node.accountNumber || '';\n                    document.getElementById('editSortCodeInput').value = node.sortCode || '';\n                    break;\n                case 'technology':\n                    document.getElementById('editTechNameInput').value = node.techName || '';\n                    document.getElementById('editTechVersionInput').value = node.techVersion || '';\n                    break;\n                case 'device':\n                    document.getElementById('editDeviceCategory').value = node.deviceCategory || 'Server';\n                    document.getElementById('editDeviceNameInput').value = node.deviceName || '';\n                    break;\n                case 'malware':\n                    document.getElementById('editMalwareNameInput').value = node.malwareName || '';\n                    document.getElementById('editMalwareType').value = node.malwareType || 'Wiper';\n                    break;\n                case 'vulnerability':\n                    document.getElementById('editVulnNameInput').value = node.vulnName || '';\n                    document.getElementById('editVulnCVEInput').value = node.cve || '';\n                    document.getElementById('editVulnUrlInput').value = node.url || '';\n                    break;\n            }\n            document.getElementById('editNameInput').style.display = node.type === 'contact' ? 'block' : 'none';\n            document.getElementById('editEmailInput').style.display = node.type === 'contact' ? 'block' : 'none';\n            document.getElementById('editIpInput').style.display = node.type === 'ip' ? 'block' : 'none';\n            document.getElementById('editDomainInput').style.display = node.type === 'domain' ? 'block' : 'none';\n            document.getElementById('editOrgInput').style.display = node.type === 'organization' ? 'block' : 'none';\n            document.getElementById('editPortNumInput').style.display = node.type === 'port' ? 'block' : 'none';\n            document.getElementById('editPortType').style.display = node.type === 'port' ? 'block' : 'none';\n            document.getElementById('editWalletAddressInput').style.display = node.type === 'wallet' ? 'block' : 'none';\n            document.getElementById('editAccountNumberInput').style.display = node.type === 'bank' ? 'block' : 'none';\n            document.getElementById('editSortCodeInput').style.display = node.type === 'bank' ? 'block' : 'none';\n            document.getElementById('editTechNameInput').style.display = node.type === 'technology' ? 'block' : 'none';\n            document.getElementById('editTechVersionInput').style.display = node.type === 'technology' ? 'block' : 'none';\n            document.getElementById('editDeviceCategory').style.display = node.type === 'device' ? 'block' : 'none';\n            document.getElementById('editDeviceNameInput').style.display = node.type === 'device' ? 'block' : 'none';\n            document.getElementById('editMalwareNameInput').style.display = node.type === 'malware' ? 'block' : 'none';\n            document.getElementById('editMalwareType').style.display = node.type === 'malware' ? 'block' : 'none';\n            document.getElementById('editVulnNameInput').style.display = node.type === 'vulnerability' ? 'block' : 'none';\n            document.getElementById('editVulnCVEInput').style.display = node.type === 'vulnerability' ? 'block' : 'none';\n            document.getElementById('editVulnUrlInput').style.display = node.type === 'vulnerability' ? 'block' : 'none';\n            document.getElementById('editSubnetInput').style.display = node.type === 'subnet' ? 'block' : 'none';\n        }\n\n        function clearAddInputs() {\n            document.querySelectorAll('#object-management .input-group:first-child input').forEach(input => input.value = '');\n            document.getElementById('addDeviceCategory').value = 'Server';\n            document.getElementById('addMalwareType').value = 'Wiper';\n            document.getElementById('addPortType').value = 'TCP';\n            document.getElementById('addVulnNameInput').value = '';\n            document.getElementById('addVulnCVEInput').value = '';\n            document.getElementById('addVulnUrlInput').value = '';\n        }\n\n        function clearEditInputs() {\n            document.querySelectorAll('#object-management .input-group:nth-child(2) input').forEach(input => input.value = '');\n            document.getElementById('editDeviceCategory').value = 'Server';\n            document.getElementById('editMalwareType').value = 'Wiper';\n            document.getElementById('editPortType').value = 'TCP';\n            document.getElementById('editVulnNameInput').value = '';\n            document.getElementById('editVulnCVEInput').value = '';\n            document.getElementById('editVulnUrlInput').value = '';\n            document.getElementById('editSubnetInput').value = '';\n        }\n\n        \n        function addEdge() {\n            let from = document.getElementById('fromNode').value; \n            let to = document.getElementById('toNode').value; \n            let label = document.getElementById('edgeLabel').value;\n            if (!from || !to || from === to) return showToast('Please select different nodes', 'error');\n            edges.add({ \n        id: `edge_${from}_${to}_${Date.now()}`, \n        from: parseInt(from), \n        to: parseInt(to), \n        label: label || '',\n        originalLabel: label || ''  // Add this line\n    });\n            updateNodeSizes(); \n            updateEdgeSelectOptions(); \n            document.getElementById('edgeLabel').value = ''; \n            stabilizeNetwork();\n            saveStateAfterOperation();\n        }\n\n        function removeNode() {\n            let nodeId = document.getElementById('removeNode').value;\n            if (!nodeId) return showToast('Please select a node', 'error');\n            edges.remove(edges.get({ filter: edge => edge.from === parseInt(nodeId) || edge.to === parseInt(nodeId) }));\n            nodes.remove({ id: parseInt(nodeId) });\n            updateNodeSizes(); \n            updateSelectOptions(); \n            stabilizeNetwork();\n            saveStateAfterOperation();\n        }\n\n        function removeEdge() {\n            let edgeId = document.getElementById('removeEdge').value;\n            if (!edgeId) return showToast('Please select an edge', 'error');\n            edges.remove({ id: edgeId });\n            updateNodeSizes(); \n            updateEdgeSelectOptions(); \n            document.getElementById('removeEdge').value = ''; \n            stabilizeNetwork();\n            saveStateAfterOperation();\n        }\n\n        function clearGraph() {\n    if (!confirm('Are you sure you want to clear the entire graph? This action cannot be undone.')) return;\n    nodes.clear();\n    edges.clear();\n    nextId = 1;\n    updateSelectOptions();\n    updateEdgeSelectOptions();\n    clearAddInputs();\n    clearEditInputs();\n    stabilizeNetwork();\n    saveStateAfterOperation();\n    showToast('Graph cleared', 'success');\n}\n\nfunction updateNodeSizes(affectedNodeIds = null) {\n    const nodesToUpdate = affectedNodeIds ? nodes.get(affectedNodeIds) : nodes.get();\n    nodesToUpdate.forEach(node => {\n        const connections = edges.get({ filter: edge => edge.from === node.id || edge.to === node.id }).length;\n        const newSize = Math.min(15 + connections * 10, 120);\n        nodes.update({ id: node.id, size: newSize, widthConstraint: false, heightConstraint: false });\n    });\n}\n\n\n\n\n        function updateSelectOptions() {\n            ['editNodeSelect', 'fromNode', 'toNode', 'removeNode'].forEach(id => {\n                let select = document.getElementById(id);\n                select.innerHTML = '<option value=\"\">Select</option>';\n                nodes.forEach(node => { \n                    let option = document.createElement('option'); \n                    option.value = node.id; \n                    option.text = node.label.split('\\n')[0]; \n                    select.appendChild(option); \n                });\n            });\n            updateEdgeSelectOptions();\n        }\n\n        function updateEdgeSelectOptions() {\n            let select = document.getElementById('removeEdge');\n            select.innerHTML = '<option value=\"\">Select Edge</option>';\n            edges.forEach(edge => {\n                let fromNode = nodes.get(edge.from); \n                let toNode = nodes.get(edge.to);\n                if (fromNode && toNode) {\n                    let option = document.createElement('option');\n                    option.value = edge.id; \n                    option.text = `${fromNode.label.split('\\n')[0]} -> ${toNode.label.split('\\n')[0]}${edge.label ? ' (' + edge.label + ')' : ''}`;\n                    select.appendChild(option);\n                }\n            });\n        }\n\n        function exportGraph() {\n            const exportData = { \n                nodes: nodes.get().map(node => ({ \n                    id: node.id, type: node.type, name: node.name, email: node.email, ip: node.ip, domain: node.domain, \n                    organization: node.organization, portType: node.portType, portNumber: node.portNumber, address: node.address, \n                    accountNumber: node.accountNumber, sortCode: node.sortCode, techName: node.techName, techVersion: node.techVersion, \n                    deviceCategory: node.deviceCategory, deviceName: node.deviceName, malwareName: node.malwareName, malwareType: node.malwareType, \n                    country: node.country, asn: node.asn, city: node.city, value: node.value, vulnName: node.vulnName, cve: node.cve, url: node.url \n                })), \n                edges: edges.get().map(edge => ({ id: edge.id, from: edge.from, to: edge.to, label: edge.label })) \n            };\n            const json = JSON.stringify(exportData, null, 2);\n            const blob = new Blob([json], { type: 'application/json' });\n            const url = URL.createObjectURL(blob);\n            const a = document.createElement('a'); \n            a.href = url; \n            a.download = 'network_graph.json'; \n            a.click(); \n            URL.revokeObjectURL(url);\n        }\n\n// Process IOCs\nasync function processIOCs(text) {\n    network.setOptions({ physics: { enabled: false } });\n\n    // Regex patterns\n    const urlRegex = /^(https?:\\/\\/)([^\\s/:]+)(?::(\\d{1,5}))?(\\/.*)?$/i;\n    const domainRegex = /^(?:[a-zA-Z0-9-_]+\\.)*[a-zA-Z0-9-_]+\\.[a-zA-Z]{2,}(?<!\\.(png|jpg|jpeg|gif|bmp|tif|tiff|pdf|doc|docx|xls|xlsx|ppt|pptx|txt|csv|zip|rar|7z|exe|dll|sys|bat|sh|mp3|mp4|avi|mkv|mov|wmv|flv|wav|html|css|js|php|asp|aspx|jsp|sql|db|bak|log|tar|gz|tgz))\\.?$/i;\n    const ipRegex = { \n        ipv4: /^(?!0\\d)(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?!0\\d)(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?!0\\d)(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?!0\\d)(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,\n        ipv6: /^((([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4})|(([0-9a-fA-F]{1,4}:){1,7}:)|(::([0-9a-fA-F]{1,4}:){0,5}[0-9a-fA-F]{1,4})|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|::)$/i\n    };\n    const subnetRegex = /^(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})\\/(\\d{1,2})$/;\n    const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/g;\n    const hashRegex = {\n        sha256: /^[0-9a-fA-F]{64}$/,\n        md5: /^[0-9a-fA-F]{32}$/,\n        sha1: /^[0-9a-fA-F]{40}$/\n    };\n    const hashPattern = /[0-9a-fA-F]{32,64}/g;\n    const timestampRegex = /^\\d{2}:\\d{2}:\\d{2}$/;\n\n    // Common file extensions to filter out\n    const commonFileExtensions = new Set([\n        'png', 'jpg', 'jpeg', 'gif', 'bmp', 'tif', 'tiff', 'pdf',\n        'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'csv',\n        'zip', 'rar', '7z', 'exe', 'dll', 'sys', 'bat', 'sh',\n        'mp3', 'mp4', 'avi', 'mkv', 'mov', 'wmv', 'flv', 'wav',\n        'html', 'css', 'js', 'php', 'asp', 'aspx', 'jsp', 'sql',\n        'db', 'bak', 'log', 'tar', 'gz', 'tgz'\n    ]);\n\n    // Split text into tokens\n    const tokens = text.split(/[\\s,\\n]+/).filter(token => token.trim().length > 0);\n    console.log('Tokens:', tokens);\n\n    // Extract matches\n    const urlMatches = new Set();\n    const ipMatches = new Set();\n    const subnetMatches = new Set();\n    const domainMatches = new Set();\n    const emailMatches = new Set(tokens.map(token => token.match(emailRegex)?.[0]).filter(Boolean));\n    const hashCandidates = new Set(tokens.map(token => token.match(hashPattern)?.[0]).filter(Boolean));\n\n    // Process each token\n    for (const token of tokens) {\n        // Skip timestamps\n        if (timestampRegex.test(token)) {\n            console.log(`Ignoring timestamp: ${token}`);\n            continue;\n        }\n\n        const urlMatch = token.match(urlRegex);\n        if (urlMatch) {\n            const [, protocol, host, port, path] = urlMatch;\n            urlMatches.add(token);\n            console.log(`URL Match: ${token}, Protocol: ${protocol}, Host: ${host}, Port: ${port || 'none'}, Path: ${path || 'none'}`);\n\n            // Normalize host by removing trailing dot\n            const normalizedHost = host.replace(/\\.$/, '');\n            if (subnetRegex.test(normalizedHost)) {\n                subnetMatches.add(normalizedHost);\n            } else if (ipRegex.ipv4.test(normalizedHost) || ipRegex.ipv6.test(normalizedHost)) {\n                ipMatches.add(normalizedHost);\n            } else if (domainRegex.test(normalizedHost)) {\n                domainMatches.add(normalizedHost);\n            }\n            continue;\n        }\n\n        // Check for standalone subnets\n        if (subnetRegex.test(token)) {\n            subnetMatches.add(token);\n            continue;\n        }\n\n        // Check for standalone IPs\n        if (ipRegex.ipv4.test(token) || ipRegex.ipv6.test(token)) {\n            ipMatches.add(token);\n            continue;\n        }\n\n        // Check for standalone domains, excluding file extensions, and normalize\n        const normalizedToken = token.replace(/\\.$/, '');\n        const tokenLower = normalizedToken.toLowerCase();\n        const extension = tokenLower.split('.').pop();\n        if (domainRegex.test(normalizedToken) && !urlMatches.has(token) && !commonFileExtensions.has(extension)) {\n            domainMatches.add(normalizedToken);\n            continue;\n        }\n    }\n\n    // Filter hash candidates\n    const hashMatches = new Set();\n    hashCandidates.forEach(hash => {\n        if (hashRegex.sha256.test(hash)) hashMatches.add({ value: hash, type: 'sha256' });\n        else if (hashRegex.md5.test(hash)) hashMatches.add({ value: hash, type: 'md5' });\n        else if (hashRegex.sha1.test(hash)) hashMatches.add({ value: hash, type: 'sha1' });\n    });\n\n    const totalItems = urlMatches.size + ipMatches.size + subnetMatches.size + domainMatches.size + emailMatches.size + hashMatches.size;\n    if (totalItems === 0) {\n        showToast('No IOCs found in text', 'info');\n        return;\n    }\n\n    console.log('URL Matches:', Array.from(urlMatches));\n    console.log('IP Matches:', Array.from(ipMatches));\n    console.log('Subnet Matches:', Array.from(subnetMatches));\n    console.log('Domain Matches:', Array.from(domainMatches));\n    console.log('Email Matches:', Array.from(emailMatches));\n    console.log('Hash Matches:', Array.from(hashMatches));\n\n    // Maps for deduplication\n    const existingNodes = new Map();\n    nodes.forEach(node => {\n        if (node.type === 'url' && node.url) existingNodes.set(`url:${node.url}`, node);\n        if (node.type === 'ip' && node.ip) existingNodes.set(`ip:${node.ip}`, node);\n        if (node.type === 'subnet' && node.subnet) existingNodes.set(`subnet:${node.subnet}`, node);\n        if (node.type === 'domain' && node.domain) existingNodes.set(`domain:${node.domain}`, node);\n        if (node.type === 'contact' && node.email) existingNodes.set(`email:${node.email}`, node);\n        if (node.type === 'hash' && node.hash) existingNodes.set(`hash:${node.hash}`, node);\n        if (node.type === 'port' && node.portNumber) existingNodes.set(`port:${node.portType}/${node.portNumber}`, node);\n    });\n\n    const newNodes = [];\n    const newEdges = [];\n    let processedCount = 0;\n    const batchSize = 50;\n\n    // Set to track edges by from-to-label\n    const edgeSet = new Set();\n    edges.forEach(edge => {\n        edgeSet.add(`${edge.from}_${edge.to}_${edge.label}`);\n    });\n\n    // Helper function to extract apex domain\n    function getApexDomain(fullDomain) {\n        const parts = fullDomain.toLowerCase().split('.');\n        if (parts.length < 2) return fullDomain;\n\n        const multiPartTlds = ['co.uk', 'gov.uk', 'ac.uk', 'org.uk', 'com.au', 'co.jp', 'ne.jp', 'go.jp'];\n        for (const tld of multiPartTlds) {\n            const tldParts = tld.split('.');\n            if (parts.slice(-tldParts.length).join('.') === tld) {\n                if (parts.length > tldParts.length) {\n                    return parts.slice(-(tldParts.length + 1)).join('.');\n                }\n                return fullDomain;\n            }\n        }\n        return parts.slice(-2).join('.');\n    }\n\n    // Helper function to get all domain levels\n    function getDomainLevels(domain) {\n        const levels = [];\n        let current = domain;\n        while (current.includes('.')) {\n            levels.push(current);\n            const parts = current.split('.');\n            current = parts.slice(1).join('.');\n            // Stop at apex domain to prevent splitting multi-part TLDs\n            if (current === getApexDomain(domain)) {\n                levels.push(current);\n                break;\n            }\n        }\n        return levels; // Highest level (subdomain) first\n    }\n\n    // Process URLs\n    for (const url of urlMatches) {\n        const urlMatch = url.match(urlRegex);\n        if (!urlMatch) continue;\n        const [, protocol, host, port, path] = urlMatch;\n\n        if (!existingNodes.has(`url:${url}`)) {\n            const urlNodeId = nextId++;\n            const label = `URL: ${url.length > 30 ? url.substring(0, 27) + '...' : url}`;\n            newNodes.push({\n                id: urlNodeId,\n                type: 'url',\n                label,\n                title: `Full URL: ${url}`,\n                color: { background: '#3b82f6' },\n                url,\n                size: 15\n            });\n            existingNodes.set(`url:${url}`, { id: urlNodeId });\n            console.log(`Added URL node: ${url}, ID: ${urlNodeId}`);\n\n            // Handle host (IP, subnet, or domain)\n            const normalizedHost = host.replace(/\\.$/, '');\n            if (subnetRegex.test(normalizedHost)) {\n                if (!existingNodes.has(`subnet:${normalizedHost}`)) {\n                    const subnetNodeData = createNodeData('subnet', { subnet: normalizedHost });\n                    if (subnetNodeData) {\n                        subnetNodeData.id = nextId++;\n                        newNodes.push(subnetNodeData);\n                        existingNodes.set(`subnet:${normalizedHost}`, { id: subnetNodeData.id });\n                        const edgeKey = `${urlNodeId}_${subnetNodeData.id}_Resolves to`;\n                        if (!edgeSet.has(edgeKey)) {\n                            newEdges.push({\n                                id: `edge_${urlNodeId}_${subnetNodeData.id}_${Date.now()}`,\n                                from: urlNodeId,\n                                to: subnetNodeData.id,\n                                label: 'Resolves to'\n                            });\n                            edgeSet.add(edgeKey);\n                            console.log(`Added Subnet node: ${normalizedHost}, ID: ${subnetNodeData.id}`);\n                        }\n                    }\n                } else {\n                    const existingSubnetNode = existingNodes.get(`subnet:${normalizedHost}`);\n                    const edgeKey = `${urlNodeId}_${existingSubnetNode.id}_Resolves to`;\n                    if (!edgeSet.has(edgeKey)) {\n                        newEdges.push({\n                            id: `edge_${urlNodeId}_${existingSubnetNode.id}_${Date.now()}`,\n                            from: urlNodeId,\n                            to: existingSubnetNode.id,\n                            label: 'Resolves to'\n                        });\n                        edgeSet.add(edgeKey);\n                    }\n                }\n            } else if (ipRegex.ipv4.test(normalizedHost) || ipRegex.ipv6.test(normalizedHost)) {\n                if (!existingNodes.has(`ip:${normalizedHost}`)) {\n                    const ipNodeId = nextId++;\n                    newNodes.push({\n                        id: ipNodeId,\n                        type: 'ip',\n                        label: `IP: ${normalizedHost}`,\n                        title: `IP Address: ${normalizedHost}`,\n                        color: { background: '#f87171' },\n                        ip: normalizedHost,\n                        size: 20\n                    });\n                    existingNodes.set(`ip:${normalizedHost}`, { id: ipNodeId });\n                    const edgeKey = `${urlNodeId}_${ipNodeId}_Resolves to`;\n                    if (!edgeSet.has(edgeKey)) {\n                        newEdges.push({\n                            id: `edge_${urlNodeId}_${ipNodeId}_${Date.now()}`,\n                            from: urlNodeId,\n                            to: ipNodeId,\n                            label: 'Resolves to'\n                        });\n                        edgeSet.add(edgeKey);\n                        console.log(`Added IP node: ${normalizedHost}, ID: ${ipNodeId}`);\n                    }\n                } else {\n                    const existingIpNode = existingNodes.get(`ip:${normalizedHost}`);\n                    const edgeKey = `${urlNodeId}_${existingIpNode.id}_Resolves to`;\n                    if (!edgeSet.has(edgeKey)) {\n                        newEdges.push({\n                            id: `edge_${urlNodeId}_${existingIpNode.id}_${Date.now()}`,\n                            from: urlNodeId,\n                            to: existingIpNode.id,\n                            label: 'Resolves to'\n                        });\n                        edgeSet.add(edgeKey);\n                    }\n                }\n            } else if (domainRegex.test(normalizedHost)) {\n                const domainLevels = getDomainLevels(normalizedHost);\n                const domainNodes = [];\n\n                // Create or reuse nodes for each domain level\n                for (const level of domainLevels) {\n                    let nodeId;\n                    if (!existingNodes.has(`domain:${level}`)) {\n                        nodeId = nextId++;\n                        const isApex = level === getApexDomain(normalizedHost);\n                        const node = {\n                            id: nodeId,\n                            type: 'domain',\n                            label: isApex ? `Apex: ${level}` : `Domain: ${level}`,\n                            title: isApex ? `Apex Domain: ${level}` : `Domain: ${level}`,\n                            color: { background: '#60a5fa' },\n                            domain: level,\n                            size: 20\n                        };\n                        newNodes.push(node);\n                        existingNodes.set(`domain:${level}`, { id: nodeId });\n                        domainNodes.push(node);\n                        console.log(`Added ${isApex ? 'Apex' : 'Domain'} node: ${level}, ID: ${nodeId}`);\n                    } else {\n                        nodeId = existingNodes.get(`domain:${level}`).id;\n                        domainNodes.push({ id: nodeId, domain: level });\n                    }\n                }\n\n                // Create edges for subdomains\n                for (let i = 0; i < domainNodes.length - 1; i++) {\n                    const fromId = domainNodes[i].id; // Higher level (e.g., test.co.uk)\n                    const toId = domainNodes[i + 1].id; // Lower level (e.g., co.uk)\n                    const edgeKey = `${fromId}_${toId}_Subdomain of`;\n                    if (!edgeSet.has(edgeKey)) {\n                        newEdges.push({\n                            id: `edge_${fromId}_${toId}_subdomain_${Date.now()}`,\n                            from: fromId,\n                            to: toId,\n                            label: 'Subdomain of'\n                        });\n                        edgeSet.add(edgeKey);\n                        console.log(`Linked Domain ${fromId} to Parent: ${toId}`);\n                    }\n                }\n\n                // Link URL to deepest subdomain (highest level)\n                if (domainNodes.length > 0) {\n                    const deepestNode = domainNodes[0];\n                    const edgeKey = `${urlNodeId}_${deepestNode.id}_Belongs to`;\n                    if (!edgeSet.has(edgeKey)) {\n                        newEdges.push({\n                            id: `edge_${urlNodeId}_${deepestNode.id}_belongs_${Date.now()}`,\n                            from: urlNodeId,\n                            to: deepestNode.id,\n                            label: 'Belongs to'\n                        });\n                        edgeSet.add(edgeKey);\n                        console.log(`Linked URL ${urlNodeId} to Domain: ${deepestNode.id}`);\n                    }\n                }\n            }\n\n            // Handle port\n            if (port && parseInt(port) >= 1 && parseInt(port) <= 65535) {\n                const portKey = `port:TCP/${port}`;\n                if (!existingNodes.has(portKey)) {\n                    const portNodeId = nextId++;\n                    newNodes.push({\n                        id: portNodeId,\n                        type: 'port',\n                        label: `TCP/${port}`,\n                        title: `Port\\nType: TCP\\nNumber: ${port}`,\n                        color: { background: '#a78bfa' },\n                        portType: 'TCP',\n                        portNumber: port,\n                        size: 10\n                    });\n                    existingNodes.set(portKey, { id: portNodeId });\n                    const edgeKey = `${urlNodeId}_${portNodeId}_Uses port`;\n                    if (!edgeSet.has(edgeKey)) {\n                        newEdges.push({\n                            id: `edge_${urlNodeId}_${portNodeId}_${Date.now()}`,\n                            from: urlNodeId,\n                            to: portNodeId,\n                            label: 'Uses port'\n                        });\n                        edgeSet.add(edgeKey);\n                        console.log(`Added Port node: TCP/${port}, ID: ${portNodeId}`);\n                    }\n                } else {\n                    const existingPortNode = existingNodes.get(portKey);\n                    const edgeKey = `${urlNodeId}_${existingPortNode.id}_Uses port`;\n                    if (!edgeSet.has(edgeKey)) {\n                        newEdges.push({\n                            id: `edge_${urlNodeId}_${existingPortNode.id}_${Date.now()}`,\n                            from: urlNodeId,\n                            to: existingPortNode.id,\n                            label: 'Uses port'\n                        });\n                        edgeSet.add(edgeKey);\n                    }\n                }\n            }\n        }\n        processedCount++;\n        if (processedCount % batchSize === 0) {\n            nodes.add(newNodes);\n            edges.add(newEdges);\n            newNodes.length = 0;\n            newEdges.length = 0;\n            await new Promise(resolve => setTimeout(resolve, 0)).catch(err => console.error('Batch processing error:', err));\n        }\n    }\n\n    // Process standalone Subnets\n    for (const subnet of subnetMatches) {\n        if (subnetRegex.test(subnet) && !existingNodes.has(`subnet:${subnet}`)) {\n            const subnetNodeData = createNodeData('subnet', { subnet });\n            if (subnetNodeData) {\n                subnetNodeData.id = nextId++;\n                newNodes.push(subnetNodeData);\n                existingNodes.set(`subnet:${subnet}`, { id: subnetNodeData.id });\n                console.log(`Added standalone Subnet node: ${subnet}, ID: ${subnetNodeData.id}`);\n            }\n        }\n        processedCount++;\n        if (processedCount % batchSize === 0) {\n            nodes.add(newNodes);\n            edges.add(newEdges);\n            newNodes.length = 0;\n            newEdges.length = 0;\n            await new Promise(resolve => setTimeout(resolve, 0)).catch(err => console.error('Batch processing error:', err));\n        }\n    }\n\n    // Process standalone IPs\n    for (const ip of ipMatches) {\n        if ((ipRegex.ipv4.test(ip) || ipRegex.ipv6.test(ip)) && !existingNodes.has(`ip:${ip}`)) {\n            const nodeId = nextId++;\n            newNodes.push({\n                id: nodeId,\n                type: 'ip',\n                label: `IP: ${ip}`,\n                title: `IP Address: ${ip}`,\n                color: { background: '#f87171' },\n                ip,\n                size: 20\n            });\n            existingNodes.set(`ip:${ip}`, { id: nodeId });\n            console.log(`Added standalone IP node: ${ip}, ID: ${nodeId}`);\n        }\n        processedCount++;\n        if (processedCount % batchSize === 0) {\n            nodes.add(newNodes);\n            edges.add(newEdges);\n            newNodes.length = 0;\n            newEdges.length = 0;\n            await new Promise(resolve => setTimeout(resolve, 0)).catch(err => console.error('Batch processing error:', err));\n        }\n    }\n\n    // Process standalone Domains\n    for (const domain of domainMatches) {\n        const domainLevels = getDomainLevels(domain);\n        const domainNodes = [];\n\n        // Create or reuse nodes for each domain level\n        for (const level of domainLevels) {\n            let nodeId;\n            if (!existingNodes.has(`domain:${level}`)) {\n                nodeId = nextId++;\n                const isApex = level === getApexDomain(domain);\n                const isMX = domain.endsWith('.'); // Heuristic for MX domains\n            const node = {\n                id: nodeId,\n                type: isMX ? 'mx' : 'domain', // Set type to mx if detected\n                label: isApex ? `Apex: ${level}` : `Domain: ${level}`,\n                title: isApex ? `Apex Domain: ${level}` : `Domain: ${level}`,\n                color: { background: isMX ? '#60a5fa' : '#60a5fa' }, // Blue for MX and domains\n                className: isMX ? 'mx-node' : 'domain-node', // Assign className\n                domain: level,\n                size: 20\n            };\n                newNodes.push(node);\n                existingNodes.set(`domain:${level}`, { id: nodeId });\n                domainNodes.push(node);\n                console.log(`Added ${isApex ? 'Apex' : 'Domain'} node: ${level}, ID: ${nodeId}`);\n            } else {\n                nodeId = existingNodes.get(`domain:${level}`).id;\n                domainNodes.push({ id: nodeId, domain: level });\n            }\n        }\n\n        // Create edges for subdomains\n        for (let i = 0; i < domainNodes.length - 1; i++) {\n            const fromId = domainNodes[i].id; // Higher level (e.g., test.co.uk)\n            const toId = domainNodes[i + 1].id; // Lower level (e.g., co.uk)\n            const edgeKey = `${fromId}_${toId}_Subdomain of`;\n            if (!edgeSet.has(edgeKey)) {\n                newEdges.push({\n                    id: `edge_${fromId}_${toId}_subdomain_${Date.now()}`,\n                    from: fromId,\n                    to: toId,\n                    label: 'Subdomain of'\n                });\n                edgeSet.add(edgeKey);\n                console.log(`Linked Domain ${fromId} to Parent: ${toId}`);\n            }\n        }\n        processedCount++;\n        if (processedCount % batchSize === 0) {\n            nodes.add(newNodes);\n            edges.add(newEdges);\n            newNodes.length = 0;\n            newEdges.length = 0;\n            await new Promise(resolve => setTimeout(resolve, 0)).catch(err => console.error('Batch processing error:', err));\n        }\n    }\n\n    // Process Emails\n    for (const email of emailMatches) {\n        if (!existingNodes.has(`email:${email}`)) {\n            const emailNodeId = nextId++;\n            const emailDomain = email.split('@')[1].replace(/\\.$/, '');\n            newNodes.push({\n                id: emailNodeId,\n                type: 'contact',\n                label: `Contact: ${email}`,\n                title: `Contact\\nEmail: ${email}`,\n                color: { background: '#4ade80' },\n                email,\n                name: email.split('@')[0],\n                size: 10\n            });\n            existingNodes.set(`email:${email}`, { id: emailNodeId });\n\n            if (domainRegex.test(emailDomain)) {\n                // Create or reuse node for the full email domain\n                let emailDomainNodeId;\n                if (!existingNodes.has(`domain:${emailDomain}`)) {\n                    emailDomainNodeId = nextId++;\n                    newNodes.push({\n                        id: emailDomainNodeId,\n                        type: 'domain',\n                        label: `Domain: ${emailDomain}`,\n                        title: `Domain: ${emailDomain}`,\n                        color: { background: '#60a5fa' },\n                        domain: emailDomain,\n                        size: 20\n                    });\n                    existingNodes.set(`domain:${emailDomain}`, { id: emailDomainNodeId });\n                    console.log(`Added Domain node: ${emailDomain}, ID: ${emailDomainNodeId}`);\n                } else {\n                    emailDomainNodeId = existingNodes.get(`domain:${emailDomain}`).id;\n                }\n\n                // Link email to the full email domain\n                const edgeKeyContact = `${emailNodeId}_${emailDomainNodeId}_Registered with`;\n                if (!edgeSet.has(edgeKeyContact)) {\n                    newEdges.push({\n                        id: `edge_${emailNodeId}_${emailDomainNodeId}_registered_${Date.now()}`,\n                        from: emailNodeId,\n                        to: emailDomainNodeId,\n                        label: 'is asociated with: '\n                    });\n                    edgeSet.add(edgeKeyContact);\n                    console.log(`Linked Email ${emailNodeId} to Domain: ${emailDomainNodeId}`);\n                }\n\n                // Create hierarchy for email domain (subdomains and apex)\n                const domainLevels = getDomainLevels(emailDomain);\n                const domainNodes = [];\n\n                // Create or reuse nodes for each domain level\n                for (const level of domainLevels) {\n                    let nodeId;\n                    if (!existingNodes.has(`domain:${level}`)) {\n                        nodeId = nextId++;\n                        const isApex = level === getApexDomain(emailDomain);\n                        const node = {\n                            id: nodeId,\n                            type: 'domain',\n                            label: isApex ? `Apex: ${level}` : `Domain: ${level}`,\n                            title: isApex ? `Apex Domain: ${level}` : `Domain: ${level}`,\n                            color: { background: '#60a5fa' },\n                            domain: level,\n                            size: 20\n                        };\n                        newNodes.push(node);\n                        existingNodes.set(`domain:${level}`, { id: nodeId });\n                        domainNodes.push(node);\n                        console.log(`Added ${isApex ? 'Apex' : 'Domain'} node: ${level}, ID: ${nodeId}`);\n                    } else {\n                        nodeId = existingNodes.get(`domain:${level}`).id;\n                        domainNodes.push({ id: nodeId, domain: level });\n                    }\n                }\n\n                // Create edges for subdomains\n                for (let i = 0; i < domainNodes.length - 1; i++) {\n                    const fromId = domainNodes[i].id; // Higher level (e.g., test.co.uk)\n                    const toId = domainNodes[i + 1].id; // Lower level (e.g., co.uk)\n                    const edgeKey = `${fromId}_${toId}_Subdomain of`;\n                    if (!edgeSet.has(edgeKey)) {\n                        newEdges.push({\n                            id: `edge_${fromId}_${toId}_subdomain_${Date.now()}`,\n                            from: fromId,\n                            to: toId,\n                            label: 'Subdomain of'\n                        });\n                        edgeSet.add(edgeKey);\n                        console.log(`Linked Domain ${fromId} to Parent: ${toId}`);\n                    }\n                }\n            }\n        }\n        processedCount++;\n        if (processedCount % batchSize === 0) {\n            nodes.add(newNodes);\n            edges.add(newEdges);\n            newNodes.length = 0;\n            newEdges.length = 0;\n            await new Promise(resolve => setTimeout(resolve, 0)).catch(err => console.error('Batch processing error:', err));\n        }\n    }\n\n    // Process Hashes\n    for (const hashObj of hashMatches) {\n        const hash = hashObj.value;\n        if (!existingNodes.has(`hash:${hash}`)) {\n            const hashNodeId = nextId++;\n            const hashType = hashObj.type.toUpperCase();\n            newNodes.push({\n                id: hashNodeId,\n                type: 'hash',\n                label: `${hashType}: ${hash.substring(0, 8)}...`,\n                title: `File Hash\\nType: ${hashType}\\nValue: ${hash}`,\n                color: { background: '#f97316' },\n                hash,\n                hashType,\n                size: 15\n            });\n            existingNodes.set(`hash:${hash}`, { id: hashNodeId });\n            console.log(`Added Hash node: ${hash}, Type: ${hashType}, ID: ${hashNodeId}`);\n        }\n        processedCount++;\n        if (processedCount % batchSize === 0) {\n            nodes.add(newNodes);\n            edges.add(newEdges);\n            newNodes.length = 0;\n            newEdges.length = 0;\n            await new Promise(resolve => setTimeout(resolve, 0)).catch(err => console.error('Batch processing error:', err));\n        }\n    }\n\n    // Add remaining items\n    if (newNodes.length > 0) {\n        console.log('Final batch processing:', { newNodes, newEdges });\n        nodes.add(newNodes);\n        edges.add(newEdges);\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork().catch(err => console.error('Error stabilizing network:', err));\n    showToast(`Imported ${urlMatches.size} URLs, ${ipMatches.size} IPs, ${subnetMatches.size} subnets, ${domainMatches.size} domains, ${emailMatches.size} emails, ${hashMatches.size} hashes`, 'success');\n}\n//End of Process IOCs\n\n\ndocument.addEventListener('keypress', event => {\n            if (event.key === 'Enter') {\n                event.preventDefault();\n                const activeElement = document.activeElement;\n                if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) {\n                    const start = activeElement.selectionStart; \n                    const end = activeElement.selectionEnd; \n                    const value = activeElement.value;\n                    activeElement.value = value.substring(0, start) + '\\r\\n' + value.substring(end);\n                    activeElement.selectionStart = activeElement.selectionEnd = start + 2;\n                }\n            }\n        });\n\n   \n        function toggleNodeLabels() {\n    nodeLabelsVisible = document.getElementById('showNodeLabels').checked;\n    \n    // Update each node individually while preserving original labels\n    nodes.forEach(node => {\n        // Ensure original label is stored\n        if (!node.hasOwnProperty('originalLabel')) {\n            node.originalLabel = node.label || '';\n        }\n        \n        nodes.update({\n            id: node.id,\n            label: nodeLabelsVisible ? node.originalLabel : '',  // Toggle between original and empty\n            font: {\n                size: nodeLabelsVisible ? 12 : 0,\n                color: isDarkMode ? '#e2e8f0' : '#1f2a44',\n                multi: true,\n                align: 'center',\n                vadjust: 0,\n                strokeWidth: 0\n            }\n        });\n    });\n    \n    // Update global network options\n    network.setOptions({\n        nodes: {\n            font: {\n                size: nodeLabelsVisible ? 12 : 0,\n                color: isDarkMode ? '#e2e8f0' : '#1f2a44',\n                multi: true,\n                align: 'center',\n                vadjust: 0,\n                strokeWidth: 0\n            },\n            chosen: {\n                label: function(values, id, selected, hovering) {\n                    values.size = nodeLabelsVisible ? 12 : 0;\n                }\n            }\n        }\n    });\n    \n    // Force a full network refresh\n    network.setData({ nodes: nodes, edges: edges });  // Reset data to force update\n    network.stabilize(100);\n    network.redraw();\n    saveStateAfterOperation();\n}\n\n  \n\nfunction toggleEdgeLabels() {\n    edgeLabelsVisible = document.getElementById('showEdgeLabels').checked;\n    \n    // Update each edge individually while preserving original labels\n    edges.forEach(edge => {\n        // Ensure original label is stored\n        if (!edge.hasOwnProperty('originalLabel')) {\n            edge.originalLabel = edge.label || '';\n        }\n        \n        edges.update({\n            id: edge.id,\n            label: edgeLabelsVisible ? edge.originalLabel : '',  // Toggle between original and empty\n            font: {\n                size: edgeLabelsVisible ? 12 : 0,\n                color: isDarkMode ? '#e2e8f0' : '#1f2a44',\n                strokeWidth: 0,\n                strokeColor: 'transparent',\n                align: 'middle',\n                multi: true\n            }\n        });\n    });\n    \n    // Update global network options\n    network.setOptions({\n        edges: {\n            font: {\n                size: edgeLabelsVisible ? 12 : 0,\n                color: isDarkMode ? '#e2e8f0' : '#1f2a44',\n                strokeWidth: 0,\n                strokeColor: 'transparent',\n                align: 'middle',\n                multi: true\n            },\n            chosen: {\n                label: function(values, id, selected, hovering) {\n                    values.size = edgeLabelsVisible ? 12 : 0;\n                }\n            }\n        }\n    });\n    \n    // Force a full network refresh\n    network.setData({ nodes: nodes, edges: edges });  // Reset data to force update\n    network.stabilize(100);\n    network.redraw();\n    saveStateAfterOperation();\n}\n\nfunction updateLabelVisibility() {\n    network.setOptions({\n        nodes: {\n            font: {\n                size: nodeLabelsVisible ? 12 : 0,\n                color: isDarkMode ? '#e2e8f0' : '#1f2a44'\n            }\n        },\n        edges: {\n            font: {\n                size: edgeLabelsVisible ? 12 : 0,\n                color: isDarkMode ? '#e2e8f0' : '#1f2a44'\n            }\n        }\n    });\n    network.redraw();\n}\n\nfunction toggleIsolatedNodes() {\n    const hideIsolated = document.getElementById('hideIsolatedNodes').checked;\n    \n    nodes.forEach(node => {\n        const connections = edges.get({\n            filter: edge => edge.from === node.id || edge.to === node.id\n        });\n        \n        nodes.update({\n            id: node.id,\n            hidden: hideIsolated && connections.length === 0\n        });\n    });\n    \n    stabilizeNetwork().then(() => {\n        network.fit({\n            animation: {\n                duration: 300,\n                easingFunction: 'easeInOutQuad'\n            }\n        });\n    });\n    \n    saveStateAfterOperation();\n}\n\n\nasync function processShodanData(ipNodeId, data) {\n    // Ensure nodes and edges are defined (assuming vis.js DataSets)\n    if (!nodes || !edges) {\n        console.error('Nodes or edges DataSet not initialized');\n        return;\n    }\n\n    // Deduplication maps\n    let existingPorts = new Map(nodes.get({ filter: n => n.type === 'port' }).map(n => [`${n.portType}/${n.portNumber}`, n.id]));\n    let existingDomains = new Map(nodes.get({ filter: n => n.type === 'domain' }).map(n => [n.domain, n.id]));\n    let existingSslHashes = new Map(nodes.get({ filter: n => n.type === 'ssl_hash' }).map(n => [n.hash, n.id]));\n    let existingHtmlHashes = new Map(nodes.get({ filter: n => n.type === 'html_hash' }).map(n => [n.hash, n.id]));\n    let existingOs = new Map(nodes.get({ filter: n => n.type === 'os' }).map(n => [n.os, n.id]));\n    let existingProducts = new Map(nodes.get({ filter: n => n.type === 'product' }).map(n => [n.product, n.id]));\n    let existingHttpHashes = new Map(nodes.get({ filter: n => n.type === 'http_hash' }).map(n => [n.hash, n.id]));\n    let existingTitles = new Map(nodes.get({ filter: n => n.type === 'http_title' }).map(n => [n.title, n.id]));\n    let existingFavicons = new Map(nodes.get({ filter: n => n.type === 'favicon' }).map(n => [n.hash, n.id]));\n    let existingPortTitles = new Map(nodes.get({ filter: n => n.type === 'port_title' }).map(n => [`${n.portType}/${n.portNumber}/${n.title}`, n.id]));\n\n    const newNodes = [];\n    const newEdges = [];\n\n    console.log(`Processing Shodan data for IP: ${ipNodeId}`);\n\n    // Top-level domains and hostnames linked to IP\n    if (data.domains && Array.isArray(data.domains)) {\n        data.domains.forEach(domain => {\n            let domainId = existingDomains.get(domain);\n            if (!domainId) {\n                domainId = nextId++;\n                newNodes.push({\n                    id: domainId,\n                    type: 'domain',\n                    label: `Domain: ${domain}`,\n                    title: `Domain: ${domain}`,\n                    color: { background: '#60a5fa' },\n                    domain: domain\n                });\n                existingDomains.set(domain, domainId);\n            }\n            const edgeId = `${ipNodeId}-${domainId}-ResolvesTo`;\n            if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                newEdges.push({ id: edgeId, from: ipNodeId, to: domainId, label: 'Resolves to' });\n            }\n        });\n    }\n\n    if (data.hostnames && Array.isArray(data.hostnames)) {\n        data.hostnames.forEach(hostname => {\n            let domainId = existingDomains.get(hostname);\n            if (!domainId) {\n                domainId = nextId++;\n                newNodes.push({\n                    id: domainId,\n                    type: 'domain',\n                    label: `Hostname: ${hostname}`,\n                    title: `Hostname: ${hostname}`,\n                    color: { background: '#60a5fa' },\n                    domain: hostname\n                });\n                existingDomains.set(hostname, domainId);\n            }\n            const edgeId = `${ipNodeId}-${domainId}-ResolvesTo`;\n            if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                newEdges.push({ id: edgeId, from: ipNodeId, to: domainId, label: 'Resolves to' });\n            }\n        });\n    }\n\n    // Process nested data array for port-specific entities\n    if (data.data && Array.isArray(data.data)) {\n        for (const banner of data.data) {\n            // Port creation\n            let portId = null;\n            if (banner.port && banner.transport) {\n                const portKey = `${banner.transport.toUpperCase()}/${banner.port}`;\n                portId = existingPorts.get(portKey);\n                if (!portId) {\n                    portId = nextId++;\n                    newNodes.push({\n                        id: portId,\n                        type: 'port',\n                        label: `${banner.transport.toUpperCase()}/${banner.port}`,\n                        title: `Port\\nType: ${banner.transport.toUpperCase()}\\nNumber: ${banner.port}`,\n                        color: { background: '#a78bfa' },\n                        portType: banner.transport.toUpperCase(),\n                        portNumber: banner.port.toString()\n                    });\n                    existingPorts.set(portKey, portId);\n                    console.log(`Created port node: ${portKey} with ID: ${portId}`);\n                }\n                const edgeId = `${ipNodeId}-${portId}-Exposes`;\n                if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                    newEdges.push({ id: edgeId, from: ipNodeId, to: portId, label: 'Exposes' });\n                    console.log(`Linked port ${portKey} to IP with edge: ${edgeId}`);\n                }\n            } else {\n                console.warn(`No port or transport found in banner:`, banner);\n            }\n\n            // Operating System (linked to IP)\n            if (banner.os) {\n                let osId = existingOs.get(banner.os);\n                if (!osId) {\n                    osId = nextId++;\n                    newNodes.push({\n                        id: osId,\n                        type: 'os',\n                        label: `OS: ${banner.os}`,\n                        title: `Operating System: ${banner.os}`,\n                        color: { background: '#10b981' },\n                        os: banner.os\n                    });\n                    existingOs.set(banner.os, osId);\n                }\n                const edgeId = `${ipNodeId}-${osId}-Runs`;\n                if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                    newEdges.push({ id: edgeId, from: ipNodeId, to: osId, label: 'Runs' });\n                }\n            }\n\n            // Products (linked to IP)\n            if (banner.product) {\n                let productId = existingProducts.get(banner.product);\n                if (!productId) {\n                    productId = nextId++;\n                    newNodes.push({\n                        id: productId,\n                        type: 'product',\n                        label: `Product: ${banner.product}`,\n                        title: `Product: ${banner.product}`,\n                        color: { background: '#ec4899' },\n                        product: banner.product\n                    });\n                    existingProducts.set(banner.product, productId);\n                }\n                const edgeId = `${ipNodeId}-${productId}-Uses`;\n                if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                    newEdges.push({ id: edgeId, from: ipNodeId, to: productId, label: 'Uses' });\n                }\n            }\n\n            // HTTP Hash (linked to port if available)\n            if (banner.http && banner.http.headers_hash && portId) {\n                const httpHash = banner.http.headers_hash.toString();\n                let httpHashId = existingHttpHashes.get(httpHash);\n                if (!httpHashId) {\n                    httpHashId = nextId++;\n                    newNodes.push({\n                        id: httpHashId,\n                        type: 'http_hash',\n                        label: `HTTP Hash: ${httpHash.substring(0, 8)}...`,\n                        title: `HTTP Headers Hash\\nValue: ${httpHash}`,\n                        color: { background: '#f97316' },\n                        hash: httpHash\n                    });\n                    existingHttpHashes.set(httpHash, httpHashId);\n                }\n                const edgeId = `${portId}-${httpHashId}-Serves`;\n                if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                    newEdges.push({ id: edgeId, from: portId, to: httpHashId, label: 'Serves' });\n                    console.log(`Linked HTTP Hash ${httpHash} to port ${portId} with edge: ${edgeId}`);\n                }\n            }\n\n            // Favicon (linked to port if available)\n            if (banner.http && banner.http.favicon && banner.http.favicon.hash && portId) {\n                const faviconHash = banner.http.favicon.hash.toString();\n                let faviconId = existingFavicons.get(faviconHash);\n                if (!faviconId) {\n                    faviconId = nextId++;\n                    newNodes.push({\n                        id: faviconId,\n                        type: 'favicon',\n                        label: `Favicon: ${faviconHash.substring(0, 8)}...`,\n                        title: `Favicon\\nHash: ${faviconHash}\\nPath: ${banner.http.favicon.location || 'N/A'}`,\n                        color: { background: '#22d3ee' },\n                        hash: faviconHash\n                    });\n                    existingFavicons.set(faviconHash, faviconId);\n                }\n                const edgeId = `${portId}-${faviconId}-Serves`;\n                if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                    newEdges.push({ id: edgeId, from: portId, to: faviconId, label: 'Serves' });\n                    console.log(`Linked Favicon ${faviconHash} to port ${portId} with edge: ${edgeId}`);\n                }\n            }\n\n            // HTTP Title and Port Title (linked to port if available)\n            if (banner.http && banner.http.title && portId) {\n                // HTTP Title\n                let titleId = existingTitles.get(banner.http.title);\n                if (!titleId) {\n                    titleId = nextId++;\n                    newNodes.push({\n                        id: titleId,\n                        type: 'http_title',\n                        label: `Title: ${banner.http.title}`,\n                        title: `HTTP Title: ${banner.http.title}`,\n                        color: { background: '#3b82f6' },\n                        title: banner.http.title\n                    });\n                    existingTitles.set(banner.http.title, titleId);\n                }\n                const titleEdgeId = `${portId}-${titleId}-HasTitle`;\n                if (!newEdges.some(e => e.id === titleEdgeId) && !edges.get(titleEdgeId)) {\n                    newEdges.push({ id: titleEdgeId, from: portId, to: titleId, label: 'Has Title' });\n                    console.log(`Linked HTTP Title \"${banner.http.title}\" to port ${portId} with edge: ${titleEdgeId}`);\n                }\n\n                // Port-Specific Title\n                const portTitleKey = `${banner.transport.toUpperCase()}/${banner.port}/${banner.http.title}`;\n                let portTitleId = existingPortTitles.get(portTitleKey);\n                if (!portTitleId) {\n                    portTitleId = nextId++;\n                    newNodes.push({\n                        id: portTitleId,\n                        type: 'port_title',\n                        label: `Title ${banner.transport.toUpperCase()}/${banner.port}`,\n                        title: `Port Title\\nPort: ${banner.transport.toUpperCase()}/${banner.port}\\nTitle: ${banner.http.title}`,\n                        color: { background: '#4b5e40' },\n                        portType: banner.transport.toUpperCase(),\n                        portNumber: banner.port.toString(),\n                        title: banner.http.title\n                    });\n                    existingPortTitles.set(portTitleKey, portTitleId);\n                }\n                const portTitleEdgeId = `${portId}-${portTitleId}-HasPortTitle`;\n                if (!newEdges.some(e => e.id === portTitleEdgeId) && !edges.get(portTitleEdgeId)) {\n                    newEdges.push({ id: portTitleEdgeId, from: portId, to: portTitleId, label: 'Has Port Title' });\n                    console.log(`Linked Port Title \"${portTitleKey}\" to port ${portId} with edge: ${portTitleEdgeId}`);\n                }\n            }\n\n            // HTML Hash (linked to port if available)\n            if (banner.http && banner.http.html_hash && portId) {\n                const htmlHash = banner.http.html_hash.toString();\n                let htmlId = existingHtmlHashes.get(htmlHash);\n                if (!htmlId) {\n                    htmlId = nextId++;\n                    newNodes.push({\n                        id: htmlId,\n                        type: 'html_hash',\n                        label: `HTML Hash: ${htmlHash.substring(0, 8)}...`,\n                        title: `HTML Hash\\nValue: ${htmlHash}`,\n                        color: { background: '#f59e0b' },\n                        hash: htmlHash\n                    });\n                    existingHtmlHashes.set(htmlHash, htmlId);\n                }\n                const edgeId = `${portId}-${htmlId}-Serves`;\n                if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                    newEdges.push({ id: edgeId, from: portId, to: htmlId, label: 'Serves' });\n                    console.log(`Linked HTML Hash ${htmlHash} to port ${portId} with edge: ${edgeId}`);\n                }\n            }\n\n            // SSL Hash (linked to port if available)\n            if (banner.ssl && banner.ssl.cert && banner.ssl.cert.fingerprint && banner.ssl.cert.fingerprint.sha256 && portId) {\n                const sslHash = banner.ssl.cert.fingerprint.sha256;\n                let sslId = existingSslHashes.get(sslHash);\n                if (!sslId) {\n                    sslId = nextId++;\n                    newNodes.push({\n                        id: sslId,\n                        type: 'ssl_hash',\n                        label: `SSL Hash: ${sslHash.substring(0, 8)}...`,\n                        title: `SSL Certificate Hash\\nSHA256: ${sslHash}`,\n                        color: { background: '#8b5cf6' },\n                        hash: sslHash\n                    });\n                    existingSslHashes.set(sslHash, sslId);\n                }\n                const edgeId = `${portId}-${sslId}-Uses`;\n                if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                    newEdges.push({ id: edgeId, from: portId, to: sslId, label: 'Uses' });\n                    console.log(`Linked SSL Hash ${sslHash} to port ${portId} with edge: ${edgeId}`);\n                }\n            }\n        }\n    }\n\n    console.log('New Nodes:', newNodes);\n    console.log('New Edges:', newEdges);\n\n    if (newNodes.length > 0) {\n        try {\n            nodes.add(newNodes);\n            console.log(`Added ${newNodes.length} nodes`);\n        } catch (error) {\n            console.error('Error adding nodes:', error);\n        }\n    }\n    if (newEdges.length > 0) {\n        try {\n            edges.add(newEdges);\n            console.log(`Added ${newEdges.length} edges`);\n        } catch (error) {\n            console.error('Error adding edges:', error);\n        }\n    }\n}\n\n\n\n\n\n// Password toggle functionality\ndocument.querySelectorAll('.toggle-password').forEach(button => {\n    button.addEventListener('click', function() {\n        const targetId = this.getAttribute('data-target');\n        const input = document.getElementById(targetId);\n        if (input.type === 'password') {\n            input.type = 'text';\n            this.textContent = 'Hide';\n        } else {\n            input.type = 'password';\n            this.textContent = 'Show';\n        }\n    });\n});\n\n\n\nconst stateLoaded = loadState();\n    document.getElementById('stop-task').disabled = true;\n\n    if (!stateLoaded) {\n        console.log('No saved state found or loading failed, applying defaults');\n        updateTheme();\n        //ensureInteractionSettings();\n    } else {\n        // Force UI and network update after successful load\n        updateSelectOptions();\n        updateEdgeSelectOptions();\n        stabilizeNetwork().then(() => {\n            network.fit({ \n                animation: { \n                    duration: 500, \n                    easingFunction: 'easeInOutQuad' \n                } \n            });\n        });\n    }\n\n    // Update UI elements regardless of state\n    updateSelectOptions();\n\n    if (window.innerWidth <= 768) {\n        document.getElementById('controls').classList.add('collapsed');\n    }\n\n    setInterval(saveState, 5 * 60 * 1000);\n\n    // Test Harness for Network Graph Visualization Tool\n    async function runAllTests() {\n        console.log(\"Starting All Tests...\");\n        showToast(\"All Tests Started\", \"info\");\n\n        // Helper function to wait\n        const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));\n\n        // Step 1: Add a Domain Entity (dns.google.com)\n        console.log(\"Step 1: Adding dns.google.com entity\");\n        document.getElementById('addEntityType').value = 'domain';\n        document.getElementById('addDomainInput').value = 'dns.google.com';\n        document.getElementById('addDomainInput').style.display = 'block';\n        addNode();\n        await wait(1000); // Wait for node addition to complete\n\n        // Verify node was added\n        const domainNode = nodes.get({ filter: n => n.type === 'domain' && n.domain === 'dns.google.com' })[0];\n        if (!domainNode) {\n            console.error(\"Failed to add dns.google.com node\");\n            showToast(\"Failed to add dns.google.com\", \"error\");\n            return;\n        }\n        console.log(\"dns.google.com added with ID:\", domainNode.id);\n        showToast(\"Added dns.google.com\", \"success\");\n\n        // Step 2: Run Enrichment Functions\n        console.log(\"Step 2: Running Enrichment Functions\");\n\n        // Google DNS Enrichment (should resolve to IPs like 8.8.8.8)\n        console.log(\"Enriching with Google DNS...\");\n        await throttledEnrichGoogleDNS('dns.google.com', domainNode.id);\n        await wait(2000);\n\n        // Get an IP node to enrich further (e.g., 8.8.8.8)\n        const ipNode = nodes.get({ filter: n => n.type === 'ip' && n.ip })[0];\n        if (ipNode) {\n            console.log(\"Found IP node to enrich:\", ipNode.ip);\n\n            // IPinfo Enrichment\n            console.log(\"Enriching with IPinfo...\");\n            await throttledEnrichIP(ipNode.ip, ipNode.id);\n            await wait(2000);\n\n            // Shodan Enrichment\n            console.log(\"Enriching with Shodan...\");\n            await throttledEnrichShodan(ipNode.ip, ipNode.id);\n            await wait(2000);\n\n            // InternetDB Enrichment\n            console.log(\"Enriching with InternetDB...\");\n            await throttledEnrichInternetDB(ipNode.ip, ipNode.id);\n            await wait(2000);\n        } else {\n            console.warn(\"No IP node found after Google DNS enrichment\");\n            showToast(\"No IP found to enrich further\", \"warning\");\n        }\n\n        // Step 3: Test Bulk Enrichment\n        console.log(\"Step 3: Testing Bulk Enrichment\");\n        await enrichAllIpinfo();\n        await wait(2000);\n        await enrichAllShodan();\n        await wait(2000);\n        await enrichAllInternetDB();\n        await wait(2000);\n        await enrichAllGoogleDNS();\n        await wait(2000);\n\n        // Step 4: Test Layouts\n        console.log(\"Step 4: Testing Layouts\");\n        setOrganicLayout();\n        await wait(1000);\n        setCircularLayout();\n        await wait(1000);\n        setOrthogonalLayout();\n        await wait(1000);\n        setTreeLayout();\n        await wait(1000);\n        setHierarchicalLayout();\n        await wait(1000);\n\n        // Step 5: Test Import/Export\n        console.log(\"Step 5: Testing Import/Export\");\n        exportGraph(); // Export current graph\n        await wait(1000);\n        // Simulate importing IOCs\n        document.getElementById('iocText').value = \"8.8.4.4\\nexample.com\";\n        importIOCsFromText();\n        await wait(1000);\n\n        // Step 6: Test Physics Toggle\n        console.log(\"Step 6: Testing Physics Toggle\");\n        togglePhysics(); // Pause\n        await wait(1000);\n        togglePhysics(); // Resume\n        await wait(1000);\n\n        // Step 7: Test Mode Toggle\n        console.log(\"Step 7: Testing Mode Toggle\");\n        toggleMode(); // Switch to light/dark\n        await wait(1000);\n        toggleMode(); // Switch back\n        await wait(1000);\n\n        // Step 8: Test Label Visibility\n        console.log(\"Step 8: Testing Label Visibility\");\n        document.getElementById('showNodeLabels').checked = false;\n        toggleNodeLabels();\n        await wait(1000);\n        document.getElementById('showNodeLabels').checked = true;\n        toggleNodeLabels();\n        await wait(1000);\n\n        // Step 9: Clean Up - Delete the Original Node\n        console.log(\"Step 9: Cleaning Up\");\n        if (domainNode) {\n            document.getElementById('removeNode').value = domainNode.id;\n            removeNode();\n            await wait(1000);\n            console.log(\"dns.google.com removed\");\n            showToast(\"Removed dns.google.com\", \"success\");\n        }\n\n        console.log(\"All Tests Completed\");\n        showToast(\"All Tests Completed\", \"success\");\n        clearGraph();\n    }\n\n    function showProgressBar() {\n    const progressBar = document.getElementById('progress-bar');\n    progressBar.textContent = 'Task in progress...';\n    progressBar.classList.remove('progress-hidden', 'progress-complete');\n    progressBar.classList.add('progress-active');\n}\n\nfunction completeProgressBar() {\n    const progressBar = document.getElementById('progress-bar');\n    progressBar.textContent = 'Task Complete';\n    progressBar.classList.remove('progress-active');\n    progressBar.classList.add('progress-complete');\n    \n    setTimeout(() => {\n        progressBar.classList.add('progress-hidden');\n    }, 5000); // Hide after 5 seconds\n}\n\nfunction showGraphSummary() {\n    // Get all nodes and count by type\n    const typeCounts = {};\n    nodes.forEach(node => {\n        typeCounts[node.type] = (typeCounts[node.type] || 0) + 1;\n    });\n    \n    // Build the table\n    const tbody = document.querySelector('#summary-table tbody');\n    tbody.innerHTML = ''; // Clear existing content\n    \n    const types = Object.keys(typeCounts).sort();\n    types.forEach(type => {\n        const row = document.createElement('tr');\n        row.innerHTML = `\n            <td>${type.charAt(0).toUpperCase() + type.slice(1)}</td>\n            <td>${typeCounts[type]}</td>\n        `;\n        tbody.appendChild(row);\n    });\n    \n    // Add total row\n    const totalRow = document.createElement('tr');\n    totalRow.innerHTML = `\n        <td><strong>Total</strong></td>\n        <td><strong>${nodes.length}</strong></td>\n    `;\n    tbody.appendChild(totalRow);\n    \n    // Show the modal\n    document.getElementById('summary-modal').style.display = 'block';\n    \n    // Optional: Add an overlay to dim the background\n    if (!document.getElementById('modal-overlay')) {\n        const overlay = document.createElement('div');\n        overlay.id = 'modal-overlay';\n        overlay.style.cssText = `\n            position: fixed;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background: rgba(0, 0, 0, 0.5);\n            z-index: 1999;\n        `;\n        document.body.appendChild(overlay);\n    }\n}\n\nfunction hideGraphSummary() {\n    document.getElementById('summary-modal').style.display = 'none';\n    const overlay = document.getElementById('modal-overlay');\n    if (overlay) overlay.remove();\n}\n\n// Close modal when clicking outside\ndocument.addEventListener('click', (event) => {\n    const modal = document.getElementById('summary-modal');\n    if (modal.style.display === 'block' && !modal.contains(event.target) && event.target.id !== 'summary-button') {\n        hideGraphSummary();\n    }\n});\n\nfunction toggleMenu() {\n    const controls = document.getElementById('controls');\n    const menuToggle = document.getElementById('menu-toggle');\n    const propertiesPanel = document.getElementById('properties-panel');\n    const myNetwork = document.getElementById('myNetwork');\n    \n    controls.classList.toggle('collapsed');\n    \n    if (controls.classList.contains('collapsed')) {\n        menuToggle.textContent = '>'; \n        menuToggle.style.transform = 'rotate(0deg)';\n        myNetwork.style.marginLeft = '50px';\n    } else {\n        menuToggle.textContent = '<'; \n        menuToggle.style.transform = 'rotate(0deg)';\n        myNetwork.style.marginLeft = '300px';\n    }\n\n    myNetwork.style.marginRight = propertiesPanel.classList.contains('active') ? '300px' : '0';\n    \n    network.fit({\n        animation: { duration: 300, easingFunction: 'easeInOutQuad' }\n    });\n}\n\n\n\nconst throttledEnrichIPMultiple = throttleRequest(async function enrichIPMultiple(ips, nodeIds) {\n    if (!Array.isArray(ips) || !Array.isArray(nodeIds) || ips.length !== nodeIds.length) {\n        showToast('Invalid input for multiple IP enrichment', 'error');\n        return;\n    }\n\n    if (!ipinfoApiKey && !ignoreApiKeysViaProxy) {\n        showToast('Please set your IPinfo API key in the \"API Keys\" tab first.', 'error');\n        return;\n    }\n\n    for (let i = 0; i < ips.length; i++) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('IPinfo enrichment stopped', 'info');\n            break;\n        }\n        await throttledEnrichIP(ips[i], nodeIds[i], true); // isBulk = true\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    showToast(`Enriched ${ips.length} IPs with IPinfo`, 'success');\n}, RATE_LIMIT_MS);\n\nconst throttledEnrichShodanMultiple = throttleRequest(async function enrichShodanMultiple(targets, nodeIds) {\n    if (!Array.isArray(targets) || !Array.isArray(nodeIds) || targets.length !== nodeIds.length) {\n        showToast('Invalid input for multiple Shodan enrichment', 'error');\n        return;\n    }\n\n    if (!shodanApiKey && !ignoreApiKeysViaProxy) {\n        showToast('Please set your Shodan API key in the \"API Keys\" tab first.', 'error');\n        return;\n    }\n\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const totalTargets = targets.length;\n    let processedTargets = 0;\n\n    showToast(`Starting Shodan enrichment for ${totalTargets} targets`, 'info');\n\n    const batchSize = 5;\n    const delayBetweenBatches = 100;\n    const totalBatches = Math.ceil(totalTargets / batchSize);\n    const timePerBatchMs = SHODAN_RATE_LIMIT_MS * batchSize;\n    const totalBatchDelays = (totalBatches - 1) * delayBetweenBatches;\n    const estimatedTimeMs = (timePerBatchMs * totalBatches) + totalBatchDelays + 1000;\n\n    const estimatedSeconds = Math.ceil(estimatedTimeMs / 1000);\n    const estimatedMinutes = Math.floor(estimatedSeconds / 60);\n    const remainingSeconds = estimatedSeconds % 60;\n    const timeEstimateStr = estimatedMinutes > 0 \n        ? `${estimatedMinutes}m ${remainingSeconds}s` \n        : `${estimatedSeconds}s`;\n\n    showToast(`Estimated time for Shodan enrichment: ~${timeEstimateStr}`, 'info');\n    document.getElementById('progress-bar').textContent = `Shodan Enrichment: 0/${totalTargets} Targets (0%) - Est. ${timeEstimateStr}`;\n\n    let lastProgressUpdate = 0;\n    const progressUpdateInterval = 500;\n    const startTime = Date.now();\n\n    for (let i = 0; i < totalTargets; i += batchSize) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('Shodan enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = `Shodan Enrichment: Stopped at ${processedTargets}/${totalTargets} Targets`;\n            break;\n        }\n\n        const batchTargets = targets.slice(i, Math.min(i + batchSize, totalTargets));\n        const batchNodeIds = nodeIds.slice(i, Math.min(i + batchSize, totalTargets));\n\n        for (let j = 0; j < batchTargets.length; j++) {\n            await throttledEnrichShodan(batchTargets[j], batchNodeIds[j], true); // isBulk = true\n            processedTargets++;\n        }\n\n        const currentTime = Date.now();\n        if (currentTime - lastProgressUpdate >= progressUpdateInterval || processedTargets === totalTargets) {\n            const progress = ((processedTargets / totalTargets) * 100).toFixed(1);\n            const remainingTargets = totalTargets - processedTargets;\n            const remainingTimeMs = remainingTargets * SHODAN_RATE_LIMIT_MS;\n            const remainingSeconds = Math.ceil(remainingTimeMs / 1000);\n            const remainingMinutes = Math.floor(remainingSeconds / 60);\n            const remainingSecondsPart = remainingSeconds % 60;\n            const remainingTimeStr = remainingMinutes > 0 \n                ? `${remainingMinutes}m ${remainingSecondsPart}s` \n                : `${remainingSeconds}s`;\n\n            document.getElementById('progress-bar').textContent = \n                `Shodan Enrichment: ${processedTargets}/${totalTargets} Targets (${progress}%) - Est. ${remainingTimeStr} remaining`;\n            lastProgressUpdate = currentTime;\n            updateSelectOptions();\n        }\n\n        await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    //ensureInteractionSettings();\n\n    if (!(activeTaskController && activeTaskController.signal.aborted)) {\n        completeProgressBar();\n        showToast(`Shodan enrichment completed: ${processedTargets}/${totalTargets} targets enriched`, 'success');\n    } else {\n        showToast(`Shodan enrichment stopped: ${processedTargets}/${totalTargets} targets processed`, 'info');\n    }\n\n    if (window.innerWidth <= 768) {\n        const controls = document.getElementById('controls');\n        controls.classList.add('collapsed');\n        document.getElementById('myNetwork').style.display = 'block';\n        network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });\n    }\n}, SHODAN_RATE_LIMIT_MS);\n\n\n\n\n\n\n\n\n\n\nconst throttledEnrichInternetDBMultiple = throttleRequest(async function enrichInternetDBMultiple(ips, nodeIds) {\n    if (!Array.isArray(ips) || !Array.isArray(nodeIds) || ips.length !== nodeIds.length) {\n        showToast('Invalid input for multiple InternetDB enrichment', 'error');\n        return;\n    }\n\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const totalIPs = ips.length;\n    let successfulEnrichments = 0;\n\n    showToast(`Starting InternetDB enrichment for ${totalIPs} IPs`, 'info');\n\n    const batchSize = 50; // Consistent with other bulk functions\n    const delayBetweenBatches = 200; // Consistent with other bulk functions\n    const totalBatches = Math.ceil(totalIPs / batchSize);\n    const assumedRequestTimeMs = 100; // Estimated time per request\n    const timePerBatchMs = assumedRequestTimeMs * batchSize;\n    const totalBatchDelays = (totalBatches - 1) * delayBetweenBatches;\n    const estimatedTimeMs = (timePerBatchMs * totalBatches) + totalBatchDelays + 1000;\n\n    const estimatedSeconds = Math.ceil(estimatedTimeMs / 1000);\n    const estimatedMinutes = Math.floor(estimatedSeconds / 60);\n    const remainingSeconds = estimatedSeconds % 60;\n    const timeEstimateStr = estimatedMinutes > 0 \n        ? `${estimatedMinutes}m ${remainingSeconds}s` \n        : `${estimatedSeconds}s`;\n\n    showToast(`Estimated time for InternetDB enrichment: ~${timeEstimateStr}`, 'info');\n    document.getElementById('progress-bar').textContent = `InternetDB Enrichment: 0/${totalIPs} IPs (0%) - Est. ${timeEstimateStr}`;\n\n    let lastProgressUpdate = 0;\n    const progressUpdateInterval = 1000;\n    const startTime = Date.now();\n\n    for (let i = 0; i < totalIPs; i += batchSize) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('InternetDB enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = `InternetDB Enrichment: Stopped at ${successfulEnrichments}/${totalIPs} IPs`;\n            break;\n        }\n\n        const batchIPs = ips.slice(i, Math.min(i + batchSize, totalIPs));\n        const batchNodeIds = nodeIds.slice(i, Math.min(i + batchSize, totalIPs));\n\n        const promises = batchIPs.map((ip, index) => {\n            return throttledEnrichInternetDB(ip, batchNodeIds[index], true) // isBulk = true\n                .then(() => successfulEnrichments++)\n                .catch(error => {\n                    console.error(`Failed to enrich IP ${ip}: ${error.message}`);\n                    showToast(`Failed to enrich IP ${ip}: ${error.message}`, 'error');\n                });\n        });\n\n        await Promise.all(promises);\n\n        const currentTime = Date.now();\n        if (currentTime - lastProgressUpdate >= progressUpdateInterval || i + batchSize >= totalIPs) {\n            const processedIPs = Math.min(i + batchSize, totalIPs);\n            const progress = ((processedIPs / totalIPs) * 100).toFixed(1);\n            const remainingIPs = totalIPs - processedIPs;\n            const remainingTimeMs = Math.max(0, remainingIPs * assumedRequestTimeMs);\n            const remainingSeconds = Math.ceil(remainingTimeMs / 1000);\n            const remainingMinutes = Math.floor(remainingSeconds / 60);\n            const remainingSecondsPart = remainingSeconds % 60;\n            const remainingTimeStr = remainingMinutes > 0 \n                ? `${remainingMinutes}m ${remainingSecondsPart}s` \n                : `${remainingSeconds}s`;\n\n            document.getElementById('progress-bar').textContent = \n                `InternetDB Enrichment: ${successfulEnrichments}/${totalIPs} IPs (${progress}%) - Est. ${remainingTimeStr} remaining`;\n            lastProgressUpdate = currentTime;\n            updateSelectOptions();\n        }\n\n        await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    //ensureInteractionSettings();\n\n    if (!(activeTaskController && activeTaskController.signal.aborted)) {\n        completeProgressBar();\n        showToast(`InternetDB enrichment completed: ${successfulEnrichments}/${totalIPs} IPs enriched`, 'success');\n    } else {\n        showToast(`InternetDB enrichment stopped: ${successfulEnrichments}/${totalIPs} IPs processed`, 'info');\n    }\n\n    if (window.innerWidth <= 768) {\n        const controls = document.getElementById('controls');\n        controls.classList.add('collapsed');\n        document.getElementById('myNetwork').style.display = 'block';\n        network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });\n    }\n}, RATE_LIMIT_MS); // Use default rate limit of 500ms\n\n// Add these functions within your <script> tag, ideally near other layout-related functions like setOrganicLayout()\n\n    function setNodeSizeLayout(mode) {\n    // Disable physics to prevent interference during resizing\n    network.setOptions({ physics: { enabled: false } });\n    \n    // Get all nodes\n    const allNodes = nodes.get();\n    \n    // Calculate sizes based on mode\n    allNodes.forEach(node => {\n        let connectionCount = 0;\n        \n        switch (mode) {\n            case 'incoming':\n                // Count edges where this node is the target (to)\n                connectionCount = edges.get({\n                    filter: edge => edge.to === node.id\n                }).length;\n                break;\n                \n            case 'outgoing':\n                // Count edges where this node is the source (from)\n                connectionCount = edges.get({\n                    filter: edge => edge.from === node.id\n                }).length;\n                break;\n                \n            case 'both':\n                // Count all edges connected to this node\n                connectionCount = edges.get({\n                    filter: edge => edge.from === node.id || edge.to === node.id\n                }).length;\n                break;\n                \n            default:\n                console.warn('Invalid mode for setNodeSizeLayout:', mode);\n                return;\n        }\n        \n        // Calculate new size: minimum 10, increases by 10 per connection, max 120\n        const newSize = Math.min(10 + connectionCount * 10, 120);\n        \n        // Update node with new size\n        nodes.update({\n            id: node.id,\n            size: newSize,\n            widthConstraint: false,  // Remove any width constraints\n            heightConstraint: false  // Remove any height constraints\n        });\n    });\n    \n    // Stabilize and fit the network after resizing\n    stabilizeNetwork().then(() => {\n        network.fit({\n            animation: {\n                duration: 300,\n                easingFunction: 'easeInOutQuad'\n            }\n        });\n        \n        // Save the updated state\n        saveStateAfterOperation();\n        \n        // Show confirmation\n        const modeText = {\n            'incoming': 'Incoming Links',\n            'outgoing': 'Outgoing Links',\n            'both': 'All Links'\n        }[mode];\n        showToast(`Node sizes updated based on ${modeText}`, 'success');\n    });\n}\n\nconst throttledSendHttpsRequestMultiple = throttleRequest(async function sendHttpsRequestMultiple(targets, type, protocol) {\n    if (!Array.isArray(targets)) {\n        showToast('Invalid input for multiple HTTPS requests', 'error');\n        return;\n    }\n\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const totalTargets = targets.length;\n    let successfulRequests = 0;\n\n    showToast(`Starting ${protocol.toUpperCase()} requests for ${totalTargets} ${type}s`, 'info');\n    document.getElementById('progress-bar').textContent = `${protocol.toUpperCase()} Requests: 0/${totalTargets} ${type}s (0%)`;\n\n    for (let i = 0; i < totalTargets; i++) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast(`${protocol.toUpperCase()} requests stopped`, 'info');\n            document.getElementById('progress-bar').textContent = `${protocol.toUpperCase()} Requests: Stopped at ${successfulRequests}/${totalTargets} ${type}s`;\n            break;\n        }\n\n        const target = targets[i];\n        const url = constructUrl(`${protocol}://${target}`);\n        \n        try {\n            const response = await fetch(url, {\n                method: 'GET',\n                headers: {\n                    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',\n                    'Origin': window.location.origin\n                },\n                mode: 'cors',\n                credentials: 'omit',\n                signal: activeTaskController?.signal\n            });\n\n            const status = response.status;\n            const statusText = response.statusText;\n            successfulRequests++;\n\n            const progress = ((successfulRequests / totalTargets) * 100).toFixed(1);\n            document.getElementById('progress-bar').textContent = \n                `${protocol.toUpperCase()} Requests: ${successfulRequests}/${totalTargets} ${type}s (${progress}%)`;\n\n            showToast(`${protocol.toUpperCase()} request to ${target}: ${status} ${statusText}`, 'success');\n        } catch (error) {\n            if (error.name === 'AbortError') {\n                break;\n            }\n            showToast(`${protocol.toUpperCase()} request to ${target} failed: ${error.message}`, 'error');\n        }\n\n        // Small delay between requests to avoid overwhelming the proxy/server\n        await new Promise(resolve => setTimeout(resolve, 200));\n    }\n\n    await stabilizeNetwork();\n    //ensureInteractionSettings();\n\n    if (!(activeTaskController && activeTaskController.signal.aborted)) {\n        completeProgressBar();\n        showToast(`${protocol.toUpperCase()} requests completed: ${successfulRequests}/${totalTargets} successful`, 'success');\n    }\n\n    if (window.innerWidth <= 768) {\n        const controls = document.getElementById('controls');\n        controls.classList.add('collapsed');\n        document.getElementById('myNetwork').style.display = 'block';\n        network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });\n    }\n}, RATE_LIMIT_MS);\n\n\nfunction stopActiveTask() {\n    if (activeTaskController) {\n        activeTaskController.abort();\n        showToast('Active task stopped', 'info');\n        document.getElementById('stop-task').disabled = true;\n        activeTaskController = null;\n        completeProgressBar();\n    } else {\n        showToast('No active task to stop', 'warning');\n    }\n}\nfunction startLinkCreation(nodeId) {\n    linkFromNode = nodeId;\n    nodes.update({ id: nodeId, color: { border: '#ff0000' } }); // Highlight source node\n    showToast('Right-click another node to create a link, or click anywhere to cancel', 'info');\n    network.once('oncontext', handleLinkDestination);\n    network.once('click', cancelLinkCreation);\n}\n\nfunction handleLinkDestination(params) {\n    const toNodeId = network.getNodeAt(params.pointer.DOM);\n    if (toNodeId && toNodeId !== linkFromNode) {\n        const label = prompt('Enter link label (optional):', '');\n        edges.add({\n            id: `edge_${linkFromNode}_${toNodeId}_${Date.now()}`,\n            from: linkFromNode,\n            to: toNodeId,\n            label: label || ''\n        });\n        updateNodeSizes();\n        stabilizeNetwork();\n        showToast('Link created', 'success');\n    }\n    cancelLinkCreation();\n}\n\nconst throttledEnrichHudsonRock = throttleRequest(async function enrichHudsonRock(email, emailNodeId, isBulk = false, signal) {\n    network.setOptions({ physics: { enabled: false } });\n    try {\n        const url = constructUrl(`https://cavalier.hudsonrock.com/api/json/v2/osint-tools/search-by-email?email=${encodeURIComponent(email)}`);\n        const response = await fetch(url, { signal });\n        if (!response.ok) throw new Error(`Failed to fetch Hudson Rock data: ${response.statusText}`);\n        const data = await response.json();\n\n        // Deduplication maps\n        const existingComputers = new Map(nodes.get({ filter: n => n.type === 'device' }).map(n => [n.deviceName, n.id]));\n        const existingIPs = new Map(nodes.get({ filter: n => n.type === 'ip' }).map(n => [n.ip, n.id]));\n        const existingMalwares = new Map(nodes.get({ filter: n => n.type === 'malware' }).map(n => [n.malwareName, n.id]));\n        const existingOS = new Map(nodes.get({ filter: n => n.type === 'os' }).map(n => [n.os, n.id]));\n        const existingAVs = new Map(nodes.get({ filter: n => n.type === 'technology' }).map(n => [n.techName, n.id]));\n\n        const newNodes = [];\n        const newEdges = [];\n\n        // Process each stealer entry\n        if (data.stealers && Array.isArray(data.stealers)) {\n            data.stealers.forEach(stealer => {\n                // Computer (Device)\n                const computerName = stealer.computer_name || 'Unknown Computer';\n                let computerId = existingComputers.get(computerName);\n                if (!computerId) {\n                    computerId = nextId++;\n                    newNodes.push({\n                        id: computerId,\n                        type: 'device',\n                        label: `Device: ${computerName}`,\n                        title: `Device\\nName: ${computerName}\\nCategory: Infected Device`,\n                        color: { background: '#14b8a6' },\n                        deviceCategory: 'Infected Device',\n                        deviceName: computerName\n                    });\n                    existingComputers.set(computerName, computerId);\n                }\n                const emailToComputerEdge = `${emailNodeId}-${computerId}-CompromisedOn`;\n                if (!edges.get(emailToComputerEdge) && !newEdges.some(e => e.id === emailToComputerEdge)) {\n                    newEdges.push({ id: emailToComputerEdge, from: emailNodeId, to: computerId, label: 'Compromised on' });\n                }\n\n                // IP Address\n                const ip = stealer.ip || 'Unknown IP';\n                if (ipRegex.ipv4.test(ip) || ipRegex.ipv6.test(ip)) {\n                    let ipId = existingIPs.get(ip);\n                    if (!ipId) {\n                        ipId = nextId++;\n                        newNodes.push({\n                            id: ipId,\n                            type: 'ip',\n                            label: `IP: ${ip}`,\n                            title: `IP Address: ${ip}`,\n                            color: { background: '#f87171' },\n                            ip: ip\n                        });\n                        existingIPs.set(ip, ipId);\n                    }\n                    const computerToIpEdge = `${computerId}-${ipId}-AssignedTo`;\n                    if (!edges.get(computerToIpEdge) && !newEdges.some(e => e.id === computerToIpEdge)) {\n                        newEdges.push({ id: computerToIpEdge, from: computerId, to: ipId, label: 'Assigned to' });\n                    }\n                }\n\n                // Malware (assuming \"jsc.exe\" is indicative; we’ll use malware_path as a name)\n                const malwareName = stealer.malware_path ? stealer.malware_path.split('\\\\').pop() : 'Unknown Malware';\n                let malwareId = existingMalwares.get(malwareName);\n                if (!malwareId) {\n                    malwareId = nextId++;\n                    newNodes.push({\n                        id: malwareId,\n                        type: 'malware',\n                        label: `Malware: ${malwareName}`,\n                        title: `Malware\\nName: ${malwareName}\\nType: Info-Stealer\\nDate: ${stealer.date_compromised || 'N/A'}`,\n                        color: { background: '#ef4444' },\n                        malwareName: malwareName,\n                        malwareType: 'Info-Stealer'\n                    });\n                    existingMalwares.set(malwareName, malwareId);\n                }\n                const computerToMalwareEdge = `${computerId}-${malwareId}-InfectedBy`;\n                if (!edges.get(computerToMalwareEdge) && !newEdges.some(e => e.id === computerToMalwareEdge)) {\n                    newEdges.push({ id: computerToMalwareEdge, from: computerId, to: malwareId, label: 'Infected by' });\n                }\n\n                // Operating System\n                const os = stealer.operating_system || 'Unknown OS';\n                let osId = existingOS.get(os);\n                if (!osId) {\n                    osId = nextId++;\n                    newNodes.push({\n                        id: osId,\n                        type: 'os',\n                        label: `OS: ${os}`,\n                        title: `Operating System: ${os}`,\n                        color: { background: '#10b981' },\n                        os: os\n                    });\n                    existingOS.set(os, osId);\n                }\n                const computerToOsEdge = `${computerId}-${osId}-Runs`;\n                if (!edges.get(computerToOsEdge) && !newEdges.some(e => e.id === computerToOsEdge)) {\n                    newEdges.push({ id: computerToOsEdge, from: computerId, to: osId, label: 'Runs' });\n                }\n\n                // Antiviruses\n                if (stealer.antiviruses && Array.isArray(stealer.antiviruses)) {\n                    stealer.antiviruses.forEach(av => {\n                        let avId = existingAVs.get(av);\n                        if (!avId) {\n                            avId = nextId++;\n                            newNodes.push({\n                                id: avId,\n                                type: 'technology',\n                                label: `AV: ${av}`,\n                                title: `Technology\\nName: ${av}\\nVersion: N/A`,\n                                color: { background: '#ec4899' },\n                                techName: av,\n                                techVersion: 'N/A'\n                            });\n                            existingAVs.set(av, avId);\n                        }\n                        const computerToAvEdge = `${computerId}-${avId}-ProtectedBy`;\n                        if (!edges.get(computerToAvEdge) && !newEdges.some(e => e.id === computerToAvEdge)) {\n                            newEdges.push({ id: computerToAvEdge, from: computerId, to: avId, label: 'Protected by' });\n                        }\n                    });\n                }\n            });\n        }\n\n        // Batch update\n        if (newNodes.length > 0) nodes.add(newNodes);\n        if (newEdges.length > 0) edges.add(newEdges);\n\n        updateNodeSizes();\n        updateSelectOptions();\n        await stabilizeNetwork();\n        if (!isBulk) showToast(`Email ${email} enrichment completed using Hudson Rock`, 'success');\n    } catch (error) {\n        if (error.name === 'AbortError') {\n            showToast(`Enrichment of email ${email} was cancelled`, 'info');\n            return;\n        }\n        console.error(`Error enriching email ${email} with Hudson Rock: ${error.message}`);\n        showToast(`Error enriching email ${email}: ${error.message}`, 'error');\n        await stabilizeNetwork();\n    }\n}, RATE_LIMIT_MS); // Using the default 500ms rate limit\n\n\nconst throttledEnrichHudsonRockMultiple = throttleRequest(async function enrichHudsonRockMultiple(emails, nodeIds) {\n    if (!Array.isArray(emails) || !Array.isArray(nodeIds) || emails.length !== nodeIds.length) {\n        showToast('Invalid input for multiple Hudson Rock enrichment', 'error');\n        return;\n    }\n\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const totalEmails = emails.length;\n    let successfulEnrichments = 0;\n\n    showToast(`Starting Hudson Rock enrichment for ${totalEmails} emails`, 'info');\n    document.getElementById('progress-bar').textContent = `Hudson Rock Enrichment: 0/${totalEmails} Emails (0%)`;\n\n    for (let i = 0; i < totalEmails; i++) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('Hudson Rock enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = `Hudson Rock Enrichment: Stopped at ${successfulEnrichments}/${totalEmails} Emails`;\n            break;\n        }\n\n        await throttledEnrichHudsonRock(emails[i], nodeIds[i], true); // isBulk = true\n        successfulEnrichments++;\n\n        const progress = ((successfulEnrichments / totalEmails) * 100).toFixed(1);\n        document.getElementById('progress-bar').textContent = \n            `Hudson Rock Enrichment: ${successfulEnrichments}/${totalEmails} Emails (${progress}%)`;\n        \n        // Small delay to respect API limits\n        await new Promise(resolve => setTimeout(resolve, 200));\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    //ensureInteractionSettings();\n\n    if (!(activeTaskController && activeTaskController.signal.aborted)) {\n        completeProgressBar();\n        showToast(`Hudson Rock enrichment completed: ${successfulEnrichments}/${totalEmails} emails enriched`, 'success');\n    }\n}, RATE_LIMIT_MS);\n\n\nconst throttledEnrichHudsonRockDomain = throttleRequest(async function enrichHudsonRockDomain(domain, domainNodeId, isBulk = false, signal) {\n    network.setOptions({ physics: { enabled: false } });\n    try {\n        const url = constructUrl(`https://cavalier.hudsonrock.com/api/json/v2/osint-tools/search-by-domain?domain=${encodeURIComponent(domain)}`);\n        const response = await fetch(url, { signal });\n        if (!response.ok) throw new Error(`Failed to fetch Hudson Rock domain data: ${response.statusText}`);\n        const data = await response.json();\n\n        // Deduplication maps\n        const existingOrganizations = new Map(nodes.get({ filter: n => n.type === 'organization' }).map(n => [n.organization, n.id]));\n        const existingDomains = new Map(nodes.get({ filter: n => n.type === 'domain' }).map(n => [n.domain, n.id]));\n        const existingMalwares = new Map(nodes.get({ filter: n => n.type === 'malware' }).map(n => [n.malwareName, n.id]));\n        const existingTechnologies = new Map(nodes.get({ filter: n => n.type === 'technology' }).map(n => [n.techName, n.id]));\n\n        const newNodes = [];\n        const newEdges = [];\n\n        // Process organization (company name)\n        if (data.data && data.data.company_name) {\n            const companyName = data.data.company_name;\n            let orgId = existingOrganizations.get(companyName);\n            if (!orgId) {\n                orgId = nextId++;\n                newNodes.push({\n                    id: orgId,\n                    type: 'organization',\n                    label: `Organization: ${companyName}`,\n                    title: `Organization: ${companyName}`,\n                    color: { background: '#facc15' },\n                    organization: companyName\n                });\n                existingOrganizations.set(companyName, orgId);\n            }\n            const orgEdgeId = `${domainNodeId}-${orgId}-BelongsTo`;\n            if (!edges.get(orgEdgeId) && !newEdges.some(e => e.id === orgEdgeId)) {\n                newEdges.push({ id: orgEdgeId, from: domainNodeId, to: orgId, label: 'Belongs to' });\n            }\n        }\n\n        // Process third-party domains\n        if (data.thirdPartyDomains && Array.isArray(data.thirdPartyDomains)) {\n            data.thirdPartyDomains.forEach(thirdParty => {\n                if (thirdParty.domain && thirdParty.domain !== null) {\n                    let thirdPartyId = existingDomains.get(thirdParty.domain);\n                    if (!thirdPartyId) {\n                        thirdPartyId = nextId++;\n                        newNodes.push({\n                            id: thirdPartyId,\n                            type: 'domain',\n                            label: `Domain: ${thirdParty.domain}`,\n                            title: `Third-Party Domain: ${thirdParty.domain}\\nOccurrences: ${thirdParty.occurrence}`,\n                            color: { background: '#60a5fa' },\n                            domain: thirdParty.domain\n                        });\n                        existingDomains.set(thirdParty.domain, thirdPartyId);\n                    }\n                    const thirdPartyEdgeId = `${domainNodeId}-${thirdPartyId}-AssociatedWith`;\n                    if (!edges.get(thirdPartyEdgeId) && !newEdges.some(e => e.id === thirdPartyEdgeId)) {\n                        newEdges.push({\n                            id: thirdPartyEdgeId,\n                            from: domainNodeId,\n                            to: thirdPartyId,\n                            label: `Associated with (${thirdParty.occurrence})`\n                        });\n                    }\n                }\n            });\n        }\n\n        // Process stealer families as malware\n        if (data.stealerFamilies && typeof data.stealerFamilies === 'object') {\n            for (const [malwareName, count] of Object.entries(data.stealerFamilies)) {\n                if (malwareName !== 'total' && count > 0) {\n                    let malwareId = existingMalwares.get(malwareName);\n                    if (!malwareId) {\n                        malwareId = nextId++;\n                        newNodes.push({\n                            id: malwareId,\n                            type: 'malware',\n                            label: `Malware: ${malwareName}`,\n                            title: `Malware\\nName: ${malwareName}\\nType: Info-Stealer\\nOccurrences: ${count}`,\n                            color: { background: '#ef4444' },\n                            malwareName: malwareName,\n                            malwareType: 'Info-Stealer'\n                        });\n                        existingMalwares.set(malwareName, malwareId);\n                    }\n                    const malwareEdgeId = `${domainNodeId}-${malwareId}-InfectedBy`;\n                    if (!edges.get(malwareEdgeId) && !newEdges.some(e => e.id === malwareEdgeId)) {\n                        newEdges.push({\n                            id: malwareEdgeId,\n                            from: domainNodeId,\n                            to: malwareId,\n                            label: `Infected by (${count})`\n                        });\n                    }\n                }\n            }\n        }\n\n        // Process antiviruses as technologies\n        if (data.antiviruses && data.antiviruses.list && Array.isArray(data.antiviruses.list)) {\n            data.antiviruses.list.forEach(av => {\n                if (av.name && av.count > 0) {\n                    let avId = existingTechnologies.get(av.name);\n                    if (!avId) {\n                        avId = nextId++;\n                        newNodes.push({\n                            id: avId,\n                            type: 'technology',\n                            label: `AV: ${av.name}`,\n                            title: `Technology\\nName: ${av.name}\\nVersion: N/A\\nOccurrences: ${av.count}`,\n                            color: { background: '#ec4899' },\n                            techName: av.name,\n                            techVersion: 'N/A'\n                        });\n                        existingTechnologies.set(av.name, avId);\n                    }\n                    const avEdgeId = `${domainNodeId}-${avId}-ProtectedBy`;\n                    if (!edges.get(avEdgeId) && !newEdges.some(e => e.id === avEdgeId)) {\n                        newEdges.push({\n                            id: avEdgeId,\n                            from: domainNodeId,\n                            to: avId,\n                            label: `Protected by (${av.count})`\n                        });\n                    }\n                }\n            });\n        }\n\n        // Batch update\n        if (newNodes.length > 0) nodes.add(newNodes);\n        if (newEdges.length > 0) edges.add(newEdges);\n\n        updateNodeSizes();\n        updateSelectOptions();\n        await stabilizeNetwork();\n        if (!isBulk) showToast(`Domain ${domain} enrichment completed using Hudson Rock`, 'success');\n    } catch (error) {\n        if (error.name === 'AbortError') {\n            showToast(`Enrichment of domain ${domain} was cancelled`, 'info');\n            return;\n        }\n        console.error(`Error enriching domain ${domain} with Hudson Rock: ${error.message}`);\n        showToast(`Error enriching domain ${domain}: ${error.message}`, 'error');\n        await stabilizeNetwork();\n    }\n}, RATE_LIMIT_MS);\n\nconst throttledEnrichHudsonRockDomainMultiple = throttleRequest(async function enrichHudsonRockDomainMultiple(domains, nodeIds) {\n    if (!Array.isArray(domains) || !Array.isArray(nodeIds) || domains.length !== nodeIds.length) {\n        showToast('Invalid input for multiple Hudson Rock domain enrichment', 'error');\n        return;\n    }\n\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const totalDomains = domains.length;\n    let successfulEnrichments = 0;\n\n    showToast(`Starting Hudson Rock enrichment for ${totalDomains} domains`, 'info');\n    document.getElementById('progress-bar').textContent = `Hudson Rock Domain Enrichment: 0/${totalDomains} Domains (0%)`;\n\n    for (let i = 0; i < totalDomains; i++) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('Hudson Rock domain enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = `Hudson Rock Domain Enrichment: Stopped at ${successfulEnrichments}/${totalDomains} Domains`;\n            break;\n        }\n\n        await throttledEnrichHudsonRockDomain(domains[i], nodeIds[i], true); // isBulk = true\n        successfulEnrichments++;\n\n        const progress = ((successfulEnrichments / totalDomains) * 100).toFixed(1);\n        document.getElementById('progress-bar').textContent = \n            `Hudson Rock Domain Enrichment: ${successfulEnrichments}/${totalDomains} Domains (${progress}%)`;\n        \n        // Small delay to respect API limits\n        await new Promise(resolve => setTimeout(resolve, 200));\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();xf\n    await stabilizeNetwork();\n    //ensureInteractionSettings();\n\n    if (!(activeTaskController && activeTaskController.signal.aborted)) {\n        completeProgressBar();\n        showToast(`Hudson Rock domain enrichment completed: ${successfulEnrichments}/${totalDomains} domains enriched`, 'success');\n    }\n}, RATE_LIMIT_MS);\n\n\n\nfunction showPropertiesPanel(nodeId) {\n    const node = nodes.get(nodeId);\n    if (!node) {\n        showToast('Node not found', 'error');\n        return;\n    }\n\n    const tbody = document.querySelector('#properties-table tbody');\n    tbody.innerHTML = '';\n\n    const properties = {};\n    for (let key in node) {\n        if (node.hasOwnProperty(key) && \n            !['id', 'x', 'y', 'fixed', 'physics', 'hidden', 'group', 'options', \n              'scaling', 'shadow', 'shapeProperties', 'chosen', 'mass'].includes(key)) {\n            properties[key] = node[key];\n        }\n    }\n\n    Object.entries(properties).forEach(([key, value]) => {\n        if (typeof value === 'object' && value !== null && key !== 'color') {\n            value = JSON.stringify(value, null, 2);\n        } else if (value === null || value === undefined) {\n            value = 'N/A';\n        }\n        const row = document.createElement('tr');\n        row.innerHTML = `<td>${key}</td><td>${value}</td>`;\n        tbody.appendChild(row);\n    });\n\n    const panel = document.getElementById('properties-panel');\n    panel.style.display = 'block';\n    panel.classList.add('active');\n\n    const controls = document.getElementById('controls');\n    const myNetwork = document.getElementById('myNetwork');\n    myNetwork.style.marginRight = '300px';\n    myNetwork.style.marginLeft = controls.classList.contains('collapsed') ? '50px' : '300px';\n\n    network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });\n}\n\nfunction hidePropertiesPanel() {\n    const panel = document.getElementById('properties-panel');\n    if (!panel) return;\n\n    panel.classList.remove('active');\n\n    // Reset layout\n    const controls = document.getElementById('controls');\n    const myNetwork = document.getElementById('myNetwork');\n    myNetwork.style.marginRight = '0';\n    myNetwork.style.marginLeft = controls.classList.contains('collapsed') ? '50px' : '300px';\n\n    // Optional: Hide panel after transition to avoid flicker\n    panel.addEventListener('transitionend', function handler() {\n        if (!panel.classList.contains('active')) {\n            panel.style.display = 'none';\n        }\n        panel.removeEventListener('transitionend', handler);\n    });\n\n    network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });\n}\n\n// Ensure toggleMenu aligns with right-side panel\nfunction toggleMenu() {\n    const controls = document.getElementById('controls');\n    const menuToggle = document.getElementById('menu-toggle');\n    const propertiesPanel = document.getElementById('properties-panel');\n    const myNetwork = document.getElementById('myNetwork');\n    \n    controls.classList.toggle('collapsed');\n    \n    if (controls.classList.contains('collapsed')) {\n        menuToggle.textContent = '>'; \n        menuToggle.style.transform = 'rotate(0deg)';\n        myNetwork.style.marginLeft = '50px';\n    } else {\n        menuToggle.textContent = '<'; \n        menuToggle.style.transform = 'rotate(0deg)';\n        myNetwork.style.marginLeft = '300px';\n    }\n\n    myNetwork.style.marginRight = propertiesPanel.classList.contains('active') ? '300px' : '0';\n    \n    network.fit({\n        animation: { duration: 300, easingFunction: 'easeInOutQuad' }\n    });\n}\n\n// Update window resize handler\nwindow.addEventListener('resize', () => {\n    const propertiesPanel = document.getElementById('properties-panel');\n    const controls = document.getElementById('controls');\n    const myNetwork = document.getElementById('myNetwork');\n    \n    if (propertiesPanel.classList.contains('active')) {\n        myNetwork.style.marginRight = '300px';\n    } else {\n        myNetwork.style.marginRight = '0';\n    }\n    myNetwork.style.marginLeft = controls.classList.contains('collapsed') ? '50px' : '300px';\n});\n\nasync function enrichAllHudsonRockEmails() {\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const emailNodes = nodes.get({ filter: n => n.type === 'contact' && n.email });\n    const totalEmails = emailNodes.length;\n    let successfulEnrichments = 0;\n\n    if (totalEmails === 0) {\n        showToast('No email nodes found to enrich', 'info');\n        completeProgressBar();\n        return;\n    }\n\n    console.log(`Found ${totalEmails} email nodes to enrich with Hudson Rock`);\n    showToast(`Starting Hudson Rock enrichment for ${totalEmails} emails`, 'info');\n\n    const batchSize = 50; // Consistent with other bulk enrichment functions\n    const delayBetweenBatches = 200; // Consistent with other bulk functions\n    const totalBatches = Math.ceil(totalEmails / batchSize);\n    const assumedRequestTimeMs = 100; // Estimated time per request\n    const timePerBatchMs = assumedRequestTimeMs * batchSize;\n    const totalBatchDelays = (totalBatches - 1) * delayBetweenBatches;\n    const estimatedTimeMs = (timePerBatchMs * totalBatches) + totalBatchDelays + 1000;\n\n    const estimatedSeconds = Math.ceil(estimatedTimeMs / 1000);\n    const estimatedMinutes = Math.floor(estimatedSeconds / 60);\n    const remainingSeconds = estimatedSeconds % 60;\n    const timeEstimateStr = estimatedMinutes > 0 \n        ? `${estimatedMinutes}m ${remainingSeconds}s` \n        : `${estimatedSeconds}s`;\n\n    showToast(`Estimated time for Hudson Rock email enrichment: ~${timeEstimateStr}`, 'info');\n    document.getElementById('progress-bar').textContent = `Hudson Rock Email Enrichment: 0/${totalEmails} Emails (0%) - Est. ${timeEstimateStr}`;\n\n    async function processBatch(batch) {\n        const promises = batch.map(node => {\n            if (activeTaskController && activeTaskController.signal.aborted) {\n                return Promise.resolve(null);\n            }\n            return throttledEnrichHudsonRock(node.email, node.id, true) // isBulk = true\n                .then(() => successfulEnrichments++)\n                .catch(error => {\n                    console.error(`Failed to enrich email ${node.email}: ${error.message}`);\n                    showToast(`Failed to enrich email ${node.email}: ${error.message}`, 'error');\n                    return null;\n                });\n        });\n        await Promise.all(promises);\n    }\n\n    let lastProgressUpdate = 0;\n    const progressUpdateInterval = 1000;\n    const startTime = Date.now();\n\n    for (let i = 0; i < totalEmails; i += batchSize) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('Hudson Rock email enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = `Hudson Rock Email Enrichment: Stopped at ${successfulEnrichments}/${totalEmails} Emails`;\n            break;\n        }\n\n        const batch = emailNodes.slice(i, Math.min(i + batchSize, totalEmails));\n        console.log(`Processing batch ${Math.floor(i / batchSize) + 1} of ${totalBatches}, Emails ${i} to ${Math.min(i + batchSize - 1, totalEmails - 1)}`);\n\n        await processBatch(batch);\n\n        const currentTime = Date.now();\n        if (currentTime - lastProgressUpdate >= progressUpdateInterval || i + batchSize >= totalEmails) {\n            const processedEmails = Math.min(i + batchSize, totalEmails);\n            const progress = ((processedEmails / totalEmails) * 100).toFixed(1);\n            const remainingEmails = totalEmails - processedEmails;\n            const remainingTimeMs = Math.max(0, remainingEmails * assumedRequestTimeMs);\n            const remainingSeconds = Math.ceil(remainingTimeMs / 1000);\n            const remainingMinutes = Math.floor(remainingSeconds / 60);\n            const remainingSecondsPart = remainingSeconds % 60;\n            const remainingTimeStr = remainingMinutes > 0 \n                ? `${remainingMinutes}m ${remainingSecondsPart}s` \n                : `${remainingSeconds}s`;\n\n            document.getElementById('progress-bar').textContent = \n                `Hudson Rock Email Enrichment: ${successfulEnrichments}/${totalEmails} Emails (${progress}%) - Est. ${remainingTimeStr} remaining`;\n            lastProgressUpdate = currentTime;\n            updateSelectOptions();\n        }\n\n        await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    //ensureInteractionSettings();\n\n    if (!(activeTaskController && activeTaskController.signal.aborted)) {\n        completeProgressBar();\n        showToast(`Hudson Rock email enrichment completed: ${successfulEnrichments}/${totalEmails} emails enriched`, 'success');\n    } else {\n        showToast(`Hudson Rock email enrichment stopped: ${successfulEnrichments}/${totalEmails} emails processed`, 'info');\n    }\n\n    if (window.innerWidth <= 768) {\n        const controls = document.getElementById('controls');\n        controls.classList.add('collapsed');\n        document.getElementById('myNetwork').style.display = 'block';\n        network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });\n    }\n}\n\nasync function enrichAllHudsonRockDomains() {\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const domainNodes = nodes.get({ filter: n => n.type === 'domain' && n.domain });\n    const totalDomains = domainNodes.length;\n    let successfulEnrichments = 0;\n\n    if (totalDomains === 0) {\n        showToast('No domain nodes found to enrich', 'info');\n        completeProgressBar();\n        return;\n    }\n\n    console.log(`Found ${totalDomains} domain nodes to enrich with Hudson Rock`);\n    showToast(`Starting Hudson Rock enrichment for ${totalDomains} domains`, 'info');\n\n    const batchSize = 50; // Consistent with other bulk enrichment functions\n    const delayBetweenBatches = 200; // Consistent with other bulk functions\n    const totalBatches = Math.ceil(totalDomains / batchSize);\n    const assumedRequestTimeMs = 100; // Estimated time per request\n    const timePerBatchMs = assumedRequestTimeMs * batchSize;\n    const totalBatchDelays = (totalBatches - 1) * delayBetweenBatches;\n    const estimatedTimeMs = (timePerBatchMs * totalBatches) + totalBatchDelays + 1000;\n\n    const estimatedSeconds = Math.ceil(estimatedTimeMs / 1000);\n    const estimatedMinutes = Math.floor(estimatedSeconds / 60);\n    const remainingSeconds = estimatedSeconds % 60;\n    const timeEstimateStr = estimatedMinutes > 0 \n        ? `${estimatedMinutes}m ${remainingSeconds}s` \n        : `${estimatedSeconds}s`;\n\n    showToast(`Estimated time for Hudson Rock domain enrichment: ~${timeEstimateStr}`, 'info');\n    document.getElementById('progress-bar').textContent = `Hudson Rock Domain Enrichment: 0/${totalDomains} Domains (0%) - Est. ${timeEstimateStr}`;\n\n    async function processBatch(batch) {\n        const promises = batch.map(node => {\n            if (activeTaskController && activeTaskController.signal.aborted) {\n                return Promise.resolve(null);\n            }\n            return throttledEnrichHudsonRockDomain(node.domain, node.id, true) // isBulk = true\n                .then(() => successfulEnrichments++)\n                .catch(error => {\n                    console.error(`Failed to enrich domain ${node.domain}: ${error.message}`);\n                    showToast(`Failed to enrich domain ${node.domain}: ${error.message}`, 'error');\n                    return null;\n                });\n        });\n        await Promise.all(promises);\n    }\n\n    let lastProgressUpdate = 0;\n    const progressUpdateInterval = 1000;\n    const startTime = Date.now();\n\n    for (let i = 0; i < totalDomains; i += batchSize) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('Hudson Rock domain enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = `Hudson Rock Domain Enrichment: Stopped at ${successfulEnrichments}/${totalDomains} Domains`;\n            break;\n        }\n\n        const batch = domainNodes.slice(i, Math.min(i + batchSize, totalDomains));\n        console.log(`Processing batch ${Math.floor(i / batchSize) + 1} of ${totalBatches}, Domains ${i} to ${Math.min(i + batchSize - 1, totalDomains - 1)}`);\n\n        await processBatch(batch);\n\n        const currentTime = Date.now();\n        if (currentTime - lastProgressUpdate >= progressUpdateInterval || i + batchSize >= totalDomains) {\n            const processedDomains = Math.min(i + batchSize, totalDomains);\n            const progress = ((processedDomains / totalDomains) * 100).toFixed(1);\n            const remainingDomains = totalDomains - processedDomains;\n            const remainingTimeMs = Math.max(0, remainingDomains * assumedRequestTimeMs);\n            const remainingSeconds = Math.ceil(remainingTimeMs / 1000);\n            const remainingMinutes = Math.floor(remainingSeconds / 60);\n            const remainingSecondsPart = remainingSeconds % 60;\n            const remainingTimeStr = remainingMinutes > 0 \n                ? `${remainingMinutes}m ${remainingSecondsPart}s` \n                : `${remainingSeconds}s`;\n\n            document.getElementById('progress-bar').textContent = \n                `Hudson Rock Domain Enrichment: ${successfulEnrichments}/${totalDomains} Domains (${progress}%) - Est. ${remainingTimeStr} remaining`;\n            lastProgressUpdate = currentTime;\n            updateSelectOptions();\n        }\n\n        await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    //ensureInteractionSettings();\n\n    if (!(activeTaskController && activeTaskController.signal.aborted)) {\n        completeProgressBar();\n        showToast(`Hudson Rock domain enrichment completed: ${successfulEnrichments}/${totalDomains} domains enriched`, 'success');\n    } else {\n        showToast(`Hudson Rock domain enrichment stopped: ${successfulEnrichments}/${totalDomains} domains processed`, 'info');\n    }\n\n    if (window.innerWidth <= 768) {\n        const controls = document.getElementById('controls');\n        controls.classList.add('collapsed');\n        document.getElementById('myNetwork').style.display = 'block';\n        network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });\n    }\n}\n\nfunction initializeApiKeys() {\n    ipinfoApiKey = localStorage.getItem('ipinfoApiKey') || '';\n    shodanApiKey = localStorage.getItem('shodanApiKey') || '';\n    greynoiseApiKey = localStorage.getItem('greynoiseApiKey') || '';\n    urlscanApiKey = localStorage.getItem('urlscanApiKey') || '';\n    securitytrailsApiKey = localStorage.getItem('securitytrailsApiKey') || '';\n    urlhausApiKey = localStorage.getItem('urlhausApiKey') || '';\n    corsProxyUrl = localStorage.getItem('corsProxyUrl') || 'http://localhost:3000/proxy?url=';\n    routeViaProxy = localStorage.getItem('routeViaProxy') === 'true';\n    ignoreApiKeysViaProxy = localStorage.getItem('ignoreApiKeysViaProxy') === 'true';\n\n    document.getElementById('ipinfoApiKey').value = ipinfoApiKey;\n    document.getElementById('shodanApiKey').value = shodanApiKey;\n    document.getElementById('greynoiseApiKey').value = greynoiseApiKey;\n    document.getElementById('urlscanApiKey').value = urlscanApiKey;\n    document.getElementById('securitytrailsApiKey').value = securitytrailsApiKey;\n    document.getElementById('urlhausApiKey').value = urlhausApiKey;\n    document.getElementById('corsProxyUrl').value = corsProxyUrl;\n    document.getElementById('routeViaProxy').checked = routeViaProxy;\n    document.getElementById('ignoreApiKeysViaProxy').checked = ignoreApiKeysViaProxy;\n    document.getElementById('storeIpinfoKey').checked = !!localStorage.getItem('ipinfoApiKey');\n    document.getElementById('storeShodanKey').checked = !!localStorage.getItem('shodanApiKey');\n    document.getElementById('storeGreynoiseKey').checked = !!localStorage.getItem('greynoiseApiKey');\n    document.getElementById('storeUrlscanKey').checked = !!localStorage.getItem('urlscanApiKey');\n    document.getElementById('storeSecuritytrailsKey').checked = !!localStorage.getItem('securitytrailsApiKey');\n    document.getElementById('storeUrlhausKey').checked = !!localStorage.getItem('urlhausApiKey');\n}\n\n\n//save ipinfo key\n\nfunction saveIpinfoApiKey() {\n    ipinfoApiKey = document.getElementById('ipinfoApiKey').value.trim();\n    const storeKey = document.getElementById('storeIpinfoKey').checked;\n    if (ipinfoApiKey) {\n        if (storeKey) {\n            localStorage.setItem('ipinfoApiKey', ipinfoApiKey);\n        } else {\n            localStorage.removeItem('ipinfoApiKey');\n        }\n        alert('IPinfo API key saved successfully!');\n    } else {\n        localStorage.removeItem('ipinfoApiKey');\n        alert('Please enter a valid IPinfo API key.');\n    }\n}\n\n//save shodan key\n\nfunction saveShodanApiKey() {\n    shodanApiKey = document.getElementById('shodanApiKey').value.trim();\n    const storeKey = document.getElementById('storeShodanKey').checked;\n    if (shodanApiKey) {\n        if (storeKey) {\n            localStorage.setItem('shodanApiKey', shodanApiKey);\n        } else {\n            localStorage.removeItem('shodanApiKey');\n        }\n        alert('Shodan API key saved successfully!');\n    } else {\n        localStorage.removeItem('shodanApiKey');\n        alert('Please enter a valid Shodan API key.');\n    }\n}\n\n// function impport graph\n\nfunction importGraph() {\n            const fileInput = document.getElementById('importFile');\n            const file = fileInput.files[0];\n            if (!file) {\n                alert('Please select a JSON file to import');\n                return;\n            }\n\n            const reader = new FileReader();\n            reader.onload = function(event) {\n                try {\n                    const importedData = JSON.parse(event.target.result);\n                    \n                    nodes.clear();\n                    edges.clear();\n\n                    importedData.nodes.forEach(node => {\n                        let nodeData = {\n                            id: node.id,\n                            size: 20,\n                            type: node.type\n                        };\n\n                        if (node.type === 'contact') {\n                            nodeData.label = `${node.name}\\n${node.email}`;\n                            nodeData.title = `Contact\\nName: ${node.name}\\nEmail: ${node.email}`;\n                            nodeData.color = { background: '#4ade80' };\n                            nodeData.name = node.name;\n                            nodeData.email = node.email;\n                        } else if (node.type === 'ip') {\n                            nodeData.label = node.ip;\n                            nodeData.title = `IP Address: ${node.ip}`;\n                            nodeData.color = { background: '#f87171' };\n                            nodeData.ip = node.ip;\n                        } else if (node.type === 'domain') {\n                            nodeData.label = node.domain;\n                            nodeData.title = `Domain: ${node.domain}`;\n                            nodeData.color = { background: '#60a5fa' };\n                            nodeData.domain = node.domain;\n                        } else if (node.type === 'organization') {\n                            nodeData.label = node.organization;\n                            nodeData.title = `Organization: ${node.organization}`;\n                            nodeData.color = { background: '#facc15' };\n                            nodeData.organization = node.organization;\n                        } else if (node.type === 'port') {\n                            nodeData.label = `${node.portType}/${node.portNumber}`;\n                            nodeData.title = `Port\\nType: ${node.portType}\\nNumber: ${node.portNumber}`;\n                            nodeData.color = { background: '#a78bfa' };\n                            nodeData.portType = node.portType;\n                            nodeData.portNumber = node.portNumber;\n                        } else if (node.type === 'wallet') {\n                            nodeData.label = node.address;\n                            nodeData.title = `Wallet\\nAddress: ${node.address}`;\n                            nodeData.color = { background: '#fb923c' };\n                            nodeData.address = node.address;\n                        } else if (node.type === 'bank') {\n                            nodeData.label = `${node.accountNumber}\\n${node.sortCode}`;\n                            nodeData.title = `Bank Account\\nAccount Number: ${node.accountNumber}\\nSort Code: ${node.sortCode}`;\n                            nodeData.color = { background: '#10b981' };\n                            nodeData.accountNumber = node.accountNumber;\n                            nodeData.sortCode = node.sortCode;\n                        } else if (node.type === 'technology') {\n                            nodeData.label = `${node.techName}\\n${node.techVersion}`;\n                            nodeData.title = `Technology\\nName: ${node.techName}\\nVersion: ${node.techVersion}`;\n                            nodeData.color = { background: '#ec4899' };\n                            nodeData.techName = node.techName;\n                            nodeData.techVersion = node.techVersion;\n                        } else if (node.type === 'device') {\n                            nodeData.label = node.deviceCategory;\n                            nodeData.title = `Device\\nCategory: ${node.deviceCategory}`;\n                            nodeData.color = { background: '#14b8a6' };\n                            nodeData.deviceCategory = node.deviceCategory;\n                        }\n\n                        let isDuplicate = false;\n                        if (node.type === 'contact') {\n                            isDuplicate = nodes.get({ filter: n => n.type === 'contact' && n.name === nodeData.name && n.email === nodeData.email }).length > 0;\n                        } else if (node.type === 'ip') {\n                            isDuplicate = nodes.get({ filter: n => n.type === 'ip' && n.ip === nodeData.ip }).length > 0;\n                        } else if (node.type === 'domain') {\n                            isDuplicate = nodes.get({ filter: n => n.type === 'domain' && n.domain === nodeData.domain }).length > 0;\n                        } else if (node.type === 'organization') {\n                            isDuplicate = nodes.get({ filter: n => n.type === 'organization' && n.organization === nodeData.organization }).length > 0;\n                        } else if (node.type === 'port') {\n                            isDuplicate = nodes.get({ filter: n => n.type === 'port' && n.portNumber === nodeData.portNumber && n.portType === nodeData.portType }).length > 0;\n                        } else if (node.type === 'wallet') {\n                            isDuplicate = nodes.get({ filter: n => n.type === 'wallet' && n.address === nodeData.address }).length > 0;\n                        } else if (node.type === 'bank') {\n                            isDuplicate = nodes.get({ filter: n => n.type === 'bank' && n.accountNumber === nodeData.accountNumber && n.sortCode === nodeData.sortCode }).length > 0;\n                        } else if (node.type === 'technology') {\n                            isDuplicate = nodes.get({ filter: n => n.type === 'technology' && n.techName === nodeData.techName && n.techVersion === nodeData.techVersion }).length > 0;\n                        } else if (node.type === 'device') {\n                            isDuplicate = nodes.get({ filter: n => n.type === 'device' && n.deviceCategory === nodeData.deviceCategory }).length > 0;\n                        }\n\n                        if (!isDuplicate) {\n                            nodes.add(nodeData);\n                        }\n                    });\n\n                    importedData.edges.forEach(edge => {\n                        edges.add({ \n                            id: edge.id || `edge_${edge.from}_${edge.to}_${Date.now()}`,\n                            from: edge.from, \n                            to: edge.to,\n                            label: edge.label || undefined\n                        });\n                    });\n\n                    updateNodeSizes();\n                    updateSelectOptions();\n                    updateEdgeSelectOptions();\n                    network.setData({ nodes: nodes, edges: edges });\n                    nextId = Math.max(...importedData.nodes.map(n => n.id)) + 1;\n\n                    fileInput.value = '';\n                } catch (e) {\n                    alert('Error importing JSON: ' + e.message);\n                }\n            };\n            reader.readAsText(file);\n        }\n\n// Save GreyNoise API Key\nfunction saveGreynoiseApiKey() {\n    greynoiseApiKey = document.getElementById('greynoiseApiKey').value.trim();\n    const storeKey = document.getElementById('storeGreynoiseKey').checked;\n    if (greynoiseApiKey) {\n        if (storeKey) {\n            localStorage.setItem('greynoiseApiKey', greynoiseApiKey);\n            showToast('GreyNoise API key saved successfully!', 'success');\n        } else {\n            localStorage.removeItem('greynoiseApiKey');\n            showToast('GreyNoise API key set for this session only', 'success');\n        }\n    } else {\n        localStorage.removeItem('greynoiseApiKey');\n        showToast('Please enter a valid GreyNoise API key.', 'error');\n    }\n}\n\n// Greynoise enrichement\n\n\n\nconst throttledEnrichGreyNoise = throttleRequest(async function enrichGreyNoise(ip, ipNodeId, isBulk = false, signal) {\n    if (!greynoiseApiKey && !ignoreApiKeysViaProxy) {\n        showToast('Please set your GreyNoise API key in the \"Config\" tab first.', 'error');\n        return;\n    }\n    \n    if (!isBulk) network.setOptions({ physics: { enabled: false } });\n    \n    try {\n        const baseUrl = `https://api.greynoise.io/v3/community/${ip}`;\n        const url = routeViaProxy ? `${corsProxyUrl}/${baseUrl}` : baseUrl;\n        const response = await fetch(url, {\n            headers: { 'key': greynoiseApiKey },\n            signal\n        });\n        if (!response.ok) throw new Error(`Failed to fetch GreyNoise data: ${response.statusText}`);\n        const data = await response.json();\n\n        // Only process successful responses\n        if (data.message !== 'Success') {\n            throw new Error('GreyNoise API did not return successful response');\n        }\n\n        // Deduplication maps\n        const existingTags = new Map(nodes.get({ filter: n => n.type === 'tag' }).map(n => [n.tag, n.id]));\n        const existingNames = new Map(nodes.get({ filter: n => n.type === 'service' }).map(n => [n.name, n.id]));\n        const existingDates = new Map(nodes.get({ filter: n => n.type === 'timestamp' }).map(n => [n.timestamp, n.id]));\n\n        const newNodes = [];\n        const newEdges = [];\n\n        // Update IP node title with GreyNoise data\n        nodes.update({\n            id: ipNodeId,\n            title: `IP Address: ${ip}\\nNoise: ${data.noise ? 'Yes' : 'No'}\\nRIOT: ${data.riot ? 'Yes' : 'No'}\\nClassification: ${data.classification}\\nName: ${data.name}\\nLast Seen: ${data.last_seen}\\nGreyNoise Link: ${data.link}`\n        });\n\n        // Classification Tag (if not 'unknown')\n        if (data.classification && data.classification !== 'unknown') {\n            const classificationKey = data.classification;\n            let classId = existingTags.get(classificationKey);\n            if (!classId) {\n                classId = nextId++;\n                newNodes.push({\n                    id: classId,\n                    type: 'tag',\n                    label: `Classification: ${classificationKey}`,\n                    title: `GreyNoise Classification: ${classificationKey}`,\n                    color: { background: '#6d28d9' }, // Purple for tags\n                    tag: classificationKey\n                });\n                existingTags.set(classificationKey, classId);\n            }\n            const classEdgeId = `${ipNodeId}-${classId}-ClassifiedAs`;\n            if (!edges.get(classEdgeId) && !newEdges.some(e => e.id === classEdgeId)) {\n                newEdges.push({ id: classEdgeId, from: ipNodeId, to: classId, label: 'Classified as' });\n            }\n        }\n\n        // Service Name (e.g., Google Public DNS)\n        if (data.name && data.name !== 'Unknown') {\n            let serviceId = existingNames.get(data.name);\n            if (!serviceId) {\n                serviceId = nextId++;\n                newNodes.push({\n                    id: serviceId,\n                    type: 'service', // New type for service names\n                    label: `Service: ${data.name}`,\n                    title: `Service Name: ${data.name}`,\n                    color: { background: '#10b981' }, // Green for services\n                    name: data.name\n                });\n                existingNames.set(data.name, serviceId);\n            }\n            const serviceEdgeId = `${ipNodeId}-${serviceId}-OperatesAs`;\n            if (!edges.get(serviceEdgeId) && !newEdges.some(e => e.id === serviceEdgeId)) {\n                newEdges.push({ id: serviceEdgeId, from: ipNodeId, to: serviceId, label: 'Operates as' });\n            }\n        }\n\n        // Last Seen Timestamp\n        if (data.last_seen) {\n            let timestampId = existingDates.get(data.last_seen);\n            if (!timestampId) {\n                timestampId = nextId++;\n                newNodes.push({\n                    id: timestampId,\n                    type: 'timestamp', // New type for timestamps\n                    label: `Last Seen: ${data.last_seen}`,\n                    title: `Last Seen: ${data.last_seen}`,\n                    color: { background: '#f97316' }, // Orange for timestamps\n                    timestamp: data.last_seen\n                });\n                existingDates.set(data.last_seen, timestampId);\n            }\n            const timestampEdgeId = `${ipNodeId}-${timestampId}-LastSeen`;\n            if (!edges.get(timestampEdgeId) && !newEdges.some(e => e.id === timestampEdgeId)) {\n                newEdges.push({ id: timestampEdgeId, from: ipNodeId, to: timestampId, label: 'Last seen' });\n            }\n        }\n\n        // Noise Status (as a boolean-like tag)\n        const noiseKey = `noise-${data.noise}`;\n        let noiseId = existingTags.get(noiseKey);\n        if (!noiseId) {\n            noiseId = nextId++;\n            newNodes.push({\n                id: noiseId,\n                type: 'tag',\n                label: `Noise: ${data.noise ? 'Yes' : 'No'}`,\n                title: `GreyNoise Noise Status: ${data.noise ? 'Yes' : 'No'}`,\n                color: { background: '#6d28d9' },\n                tag: noiseKey\n            });\n            existingTags.set(noiseKey, noiseId);\n        }\n        const noiseEdgeId = `${ipNodeId}-${noiseId}-HasNoise`;\n        if (!edges.get(noiseEdgeId) && !newEdges.some(e => e.id === noiseEdgeId)) {\n            newEdges.push({ id: noiseEdgeId, from: ipNodeId, to: noiseId, label: 'Has noise status' });\n        }\n\n        // RIOT Status (as a boolean-like tag)\n        const riotKey = `riot-${data.riot}`;\n        let riotId = existingTags.get(riotKey);\n        if (!riotId) {\n            riotId = nextId++;\n            newNodes.push({\n                id: riotId,\n                type: 'tag',\n                label: `RIOT: ${data.riot ? 'Yes' : 'No'}`,\n                title: `GreyNoise RIOT Status: ${data.riot ? 'Yes' : 'No'}`,\n                color: { background: '#6d28d9' },\n                tag: riotKey\n            });\n            existingTags.set(riotKey, riotId);\n        }\n        const riotEdgeId = `${ipNodeId}-${riotId}-HasRIOT`;\n        if (!edges.get(riotEdgeId) && !newEdges.some(e => e.id === riotEdgeId)) {\n            newEdges.push({ id: riotEdgeId, from: ipNodeId, to: riotId, label: 'Has RIOT status' });\n        }\n\n        // Batch update\n        if (newNodes.length > 0) nodes.add(newNodes);\n        if (newEdges.length > 0) edges.add(newEdges);\n\n        updateNodeSizes();\n        updateSelectOptions();\n        if (!isBulk) {\n            await stabilizeNetwork();\n            showToast(`IP ${ip} enriched with GreyNoise Community data`, 'success');\n        }\n    } catch (error) {\n        if (error.name === 'AbortError') {\n            showToast(`Enrichment of IP ${ip} cancelled`, 'info');\n            return;\n        }\n        console.error(`Error enriching IP ${ip} with GreyNoise: ${error.message}`);\n        showToast(`Error enriching IP ${ip}: ${error.message}`, 'error');\n        if (!isBulk) await stabilizeNetwork();\n    }\n}, RATE_LIMIT_MS);\n\n// Multiple IP enrichment\nconst throttledEnrichGreyNoiseMultiple = throttleRequest(async function enrichGreyNoiseMultiple(ips, nodeIds) {\n    if (!Array.isArray(ips) || !Array.isArray(nodeIds) || ips.length !== nodeIds.length) {\n        showToast('Invalid input for multiple GreyNoise enrichment', 'error');\n        return;\n    }\n\n    if (!greynoiseApiKey && !ignoreApiKeysViaProxy) {\n        showToast('Please set your GreyNoise API key in the \"Config\" tab first.', 'error');\n        return;\n    }\n\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const totalIPs = ips.length;\n    let successfulEnrichments = 0;\n\n    showToast(`Starting GreyNoise enrichment for ${totalIPs} IPs`, 'info');\n    document.getElementById('progress-bar').textContent = `GreyNoise Enrichment: 0/${totalIPs} IPs (0%)`;\n\n    for (let i = 0; i < totalIPs; i++) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('GreyNoise enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = \n                `GreyNoise Enrichment: Stopped at ${successfulEnrichments}/${totalIPs} IPs`;\n            break;\n        }\n        await throttledEnrichGreyNoise(ips[i], nodeIds[i], true);\n        successfulEnrichments++;\n        const progress = ((successfulEnrichments / totalIPs) * 100).toFixed(1);\n        document.getElementById('progress-bar').textContent = \n            `GreyNoise Enrichment: ${successfulEnrichments}/${totalIPs} IPs (${progress}%)`;\n        // Small delay to respect API rate limits\n        await new Promise(resolve => setTimeout(resolve, 200));\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    //ensureInteractionSettings();\n\n    if (!(activeTaskController && activeTaskController.signal.aborted)) {\n        completeProgressBar();\n        showToast(`GreyNoise enrichment completed: ${successfulEnrichments}/${totalIPs} IPs enriched`, 'success');\n    }\n}, RATE_LIMIT_MS);\n\n\nasync function enrichAllGreyNoise() {\n    if (!greynoiseApiKey && !ignoreApiKeysViaProxy) {\n        showToast('Please set your GreyNoise API key in the \"Config\" tab first.', 'error');\n        return;\n    }\n\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const ipNodes = nodes.get({ filter: n => n.type === 'ip' && n.ip });\n    const totalIPs = ipNodes.length;\n    let successfulEnrichments = 0;\n\n    if (totalIPs === 0) {\n        showToast('No IP nodes found to enrich', 'info');\n        completeProgressBar();\n        return;\n    }\n\n    const batchSize = 50;\n    const delayBetweenBatches = 200;\n    const totalBatches = Math.ceil(totalIPs / batchSize);\n    const estimatedTimeMs = (totalIPs * RATE_LIMIT_MS) + (totalBatches - 1) * delayBetweenBatches;\n    const estimatedSeconds = Math.ceil(estimatedTimeMs / 1000);\n    const timeEstimateStr = estimatedSeconds > 60 \n        ? `${Math.floor(estimatedSeconds / 60)}m ${estimatedSeconds % 60}s` \n        : `${estimatedSeconds}s`;\n\n    document.getElementById('progress-bar').textContent = \n        `GreyNoise Enrichment: 0/${totalIPs} IPs (0%) - Est. ${timeEstimateStr}`;\n\n    for (let i = 0; i < totalIPs; i += batchSize) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('GreyNoise enrichment stopped', 'info');\n            break;\n        }\n\n        const batch = ipNodes.slice(i, Math.min(i + batchSize, totalIPs));\n        const batchPromises = batch.map(node => \n            throttledEnrichGreyNoise(node.ip, node.id, true)\n                .then(() => successfulEnrichments++)\n                .catch(error => console.error(`Failed to enrich ${node.ip}: ${error.message}`))\n        );\n\n        await Promise.all(batchPromises);\n\n        const progress = ((successfulEnrichments / totalIPs) * 100).toFixed(1);\n        document.getElementById('progress-bar').textContent = \n            `GreyNoise Enrichment: ${successfulEnrichments}/${totalIPs} IPs (${progress}%)`;\n        \n        await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    completeProgressBar();\n    showToast(`GreyNoise enrichment completed: ${successfulEnrichments}/${totalIPs} IPs enriched`, 'success');\n}\n\n// New function to save API key\nfunction saveUrlscanApiKey() {\n    urlscanApiKey = document.getElementById('urlscanApiKey').value.trim();\n    const storeKey = document.getElementById('storeUrlscanKey').checked;\n    if (urlscanApiKey) {\n        if (storeKey) {\n            localStorage.setItem('urlscanApiKey', urlscanApiKey);\n            showToast('URLscan.io API key saved successfully!', 'success');\n        } else {\n            localStorage.removeItem('urlscanApiKey');\n            showToast('URLscan.io API key set for this session only', 'success');\n        }\n    } else {\n        localStorage.removeItem('urlscanApiKey');\n        showToast('Please enter a valid URLscan.io API key.', 'error');\n    }\n}\n\n// Single URL enrichment\nconst throttledEnrichURLscan = throttleRequest(async function enrichURLscan(url, nodeId, isBulk = false, signal) {\n    if (!isBulk) network.setOptions({ physics: { enabled: false } });\n    \n    try {\n        const parsedUrl = url.startsWith('http') ? new URL(url) : { hostname: url };\n        const query = parsedUrl.hostname ? `domain:${parsedUrl.hostname}` : `ip:${url}`;\n        const searchUrl = constructUrl(`https://urlscan.io/api/v1/search/?q=${encodeURIComponent(query)}`);\n        \n        const headers = {\n            'Accept': 'application/json'\n        };\n        if (urlscanApiKey && !ignoreApiKeysViaProxy) {\n            headers['API-Key'] = urlscanApiKey;\n        }\n\n        const response = await fetch(searchUrl, {\n            method: 'GET',\n            headers: headers,\n            signal\n        });\n        \n        if (!response.ok) {\n            const errorText = await response.text();\n            throw new Error(`Failed to fetch URLscan.io data: ${response.statusText} - ${errorText}`);\n        }\n        \n        const data = await response.json();\n\n        if (!data.results || data.results.length === 0) {\n            throw new Error(`No results found for ${query}`);\n        }\n\n        // Process the most recent result\n        const latestResult = data.results[0];\n        const newNodes = [];\n        const newEdges = [];\n\n        // 1. Status (page.status)\n        if (latestResult.page?.status) {\n            const statusId = addOrGetNode('tag', `status-${latestResult.page.status}`, newNodes, {\n                label: `Status: ${latestResult.page.status}`,\n                title: `HTTP Status: ${latestResult.page.status}`,\n                color: { background: '#6d28d9' }\n            });\n            newEdges.push({ id: `${nodeId}-${statusId}-HasStatus`, from: nodeId, to: statusId, label: 'Has status' });\n        }\n\n        // 2. URL (page.url) - Updated to show full URL or meaningful truncation\n        if (latestResult.page?.url) {\n        const decodedUrl = decodeURIComponent(latestResult.page.url);\n        const urlLabel = decodedUrl.length > 30 \n            ? `${decodedUrl.substring(0, 27)}...` \n            : decodedUrl;\n        const urlId = addOrGetNode('url', latestResult.page.url, newNodes, {\n            label: `URL: ${urlLabel}`,\n            title: `Full URL: ${decodedUrl}`,\n            url: decodedUrl, // Explicitly store full decoded URL\n            color: { background: '#3b82f6' }\n        });\n        newEdges.push({ id: `${nodeId}-${urlId}-ScannedAs`, from: nodeId, to: urlId, label: 'Scanned as' });\n    }\n\n\n        // 3. Submitted Date (task.time)\n        if (latestResult.task?.time) {\n            const dateId = addOrGetNode('timestamp', latestResult.task.time, newNodes, {\n                label: `Submitted: ${new Date(latestResult.task.time).toLocaleDateString()}`,\n                title: `Submitted Date: ${latestResult.task.time}`,\n                color: { background: '#f97316' }\n            });\n            newEdges.push({ id: `${nodeId}-${dateId}-SubmittedOn`, from: nodeId, to: dateId, label: 'Submitted on' });\n        }\n\n        // 4. Score (_score)\n        if (latestResult._score !== null && latestResult._score !== undefined) {\n            const scoreId = nextId++;\n            newNodes.push({\n                id: scoreId,\n                type: 'tag',\n                label: `Score: ${latestResult._score}`,\n                title: `URLscan Score: ${latestResult._score}`,\n                color: { background: '#10b981' }\n            });\n            newEdges.push({ id: `${nodeId}-${scoreId}-HasScore`, from: nodeId, to: scoreId, label: 'Has score' });\n        }\n\n        // 5. Total (data.total)\n        if (data.total !== undefined) {\n            const totalId = nextId++;\n            newNodes.push({\n                id: totalId,\n                type: 'tag',\n                label: `Total Scans: ${data.total}`,\n                title: `Total URLscan Results: ${data.total}`,\n                color: { background: '#ec4899' }\n            });\n            newEdges.push({ id: `${nodeId}-${totalId}-ScanCount`, from: nodeId, to: totalId, label: 'Scan count' });\n        }\n\n        // 6. Country (page.country)\n        if (latestResult.page?.country) {\n            const countryId = addOrGetNode('country', latestResult.page.country, newNodes, {\n                label: `Country: ${latestResult.page.country}`,\n                title: `Country: ${latestResult.page.country}`,\n                color: { background: '#34d399' }\n            });\n            newEdges.push({ id: `${nodeId}-${countryId}-LocatedIn`, from: nodeId, to: countryId, label: 'Located in' });\n        }\n\n        // 7. ASN (page.asn)\n        if (latestResult.page?.asn) {\n            const asnId = addOrGetNode('asn', latestResult.page.asn, newNodes, {\n                label: `ASN: ${latestResult.page.asn}`,\n                title: `ASN: ${latestResult.page.asn}\\nName: ${latestResult.page.asnname || 'N/A'}`,\n                color: { background: '#a3e635' }\n            });\n            newEdges.push({ id: `${nodeId}-${asnId}-AssignedTo`, from: nodeId, to: asnId, label: 'Assigned to' });\n        }\n\n        // 8. Apex Domain (page.apexDomain)\n        if (latestResult.page?.apexDomain) {\n            const apexId = addOrGetNode('domain', latestResult.page.apexDomain, newNodes, {\n                label: `Apex: ${latestResult.page.apexDomain}`,\n                title: `Apex Domain: ${latestResult.page.apexDomain}`,\n                color: { background: '#60a5fa' }\n            });\n            newEdges.push({ id: `${nodeId}-${apexId}-ApexDomain`, from: nodeId, to: apexId, label: 'Apex domain' });\n        }\n\n        // 9. TLS Age (page.tlsAgeDays)\n        if (latestResult.page?.tlsAgeDays !== undefined) {\n            const tlsAgeId = nextId++;\n            newNodes.push({\n                id: tlsAgeId,\n                type: 'tag',\n                label: `TLS Age: ${latestResult.page.tlsAgeDays} days`,\n                title: `TLS Certificate Age: ${latestResult.page.tlsAgeDays} days`,\n                color: { background: '#8b5cf6' }\n            });\n            newEdges.push({ id: `${nodeId}-${tlsAgeId}-TlsAge`, from: nodeId, to: tlsAgeId, label: 'TLS age' });\n        }\n\n        // 10. IP (page.ip)\n        if (latestResult.page?.ip) {\n            const ipId = addOrGetNode('ip', latestResult.page.ip, newNodes, {\n                label: `IP: ${latestResult.page.ip}`,\n                title: `IP Address: ${latestResult.page.ip}`,\n                color: { background: '#f87171' }\n            });\n            newEdges.push({ id: `${nodeId}-${ipId}-ResolvedTo`, from: nodeId, to: ipId, label: 'Resolved to' });\n        }\n\n        // Update original node with scan metadata\n        nodes.update({\n            id: nodeId,\n            title: `${nodes.get(nodeId).title || url}\\nLast Scanned: ${latestResult.task?.time || 'N/A'}\\nUUID: ${latestResult._id}\\nScreenshot: ${latestResult.screenshot || 'N/A'}`\n        });\n\n        if (newNodes.length) nodes.add(newNodes);\n        if (newEdges.length) edges.add(newEdges);\n\n        updateNodeSizes();\n        updateSelectOptions();\n        if (!isBulk) {\n            await stabilizeNetwork();\n            showToast(`URL ${url} enriched with URLscan.io search results`, 'success');\n        }\n    } catch (error) {\n        if (error.name === 'AbortError') {\n            showToast(`Enrichment of URL ${url} cancelled`, 'info');\n            return;\n        }\n        console.error(`Error enriching URL ${url}: ${error.message}`);\n        showToast(`Error enriching URL ${url}: ${error.message}${!urlscanApiKey ? ' (API key may improve results)' : ''}`, 'error');\n        if (!isBulk) await stabilizeNetwork();\n    }\n}, RATE_LIMIT_MS);\n\n// Bulk enrichment\nasync function enrichAllURLscan() {\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const urlNodes = nodes.get({ filter: n => (n.type === 'domain' || n.type === 'ip') && (n.domain || n.ip) });\n    const totalURLs = urlNodes.length;\n    let successfulEnrichments = 0;\n\n    if (totalURLs === 0) {\n        showToast('No URLs found to enrich', 'info');\n        completeProgressBar();\n        return;\n    }\n\n    const batchSize = urlscanApiKey ? 5 : 2;\n    const delayBetweenBatches = urlscanApiKey ? 1000 : 2000;\n    const estimatedTimeMs = (totalURLs / batchSize) * delayBetweenBatches;\n    const timeEstimateStr = Math.ceil(estimatedTimeMs / 60000) + 'm';\n\n    document.getElementById('progress-bar').textContent = \n        `URLscan.io Enrichment: 0/${totalURLs} URLs (0%) - Est. ${timeEstimateStr}`;\n\n    if (!urlscanApiKey) {\n        showToast('Running URLscan.io enrichment without API key - limited rate applies', 'warning');\n    }\n\n    for (let i = 0; i < totalURLs; i += batchSize) {\n        if (activeTaskController?.signal.aborted) {\n            showToast('URLscan.io enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = \n                `URLscan.io Enrichment: Stopped at ${successfulEnrichments}/${totalURLs} URLs`;\n            break;\n        }\n\n        const batch = urlNodes.slice(i, Math.min(i + batchSize, totalURLs));\n        const promises = batch.map(node => {\n            const url = node.type === 'domain' ? `https://${node.domain}` : node.ip;\n            return throttledEnrichURLscan(url, node.id, true)\n                .then(() => successfulEnrichments++)\n                .catch(error => {\n                    console.error(`Batch error for ${url}: ${error.message}`);\n                });\n        });\n\n        await Promise.all(promises);\n\n        const progress = ((successfulEnrichments / totalURLs) * 100).toFixed(1);\n        document.getElementById('progress-bar').textContent = \n            `URLscan.io Enrichment: ${successfulEnrichments}/${totalURLs} URLs (${progress}%)`;\n        \n        await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    //ensureInteractionSettings();\n    completeProgressBar();\n    showToast(`URLscan.io enrichment completed: ${successfulEnrichments}/${totalURLs} URLs enriched${!urlscanApiKey ? ' (API key may improve results)' : ''}`, 'success');\n}\n\n// Updated helper function to handle new node types\nfunction addOrGetNode(type, value, newNodes, properties) {\n    const key = type === 'url' ? 'url' : type; // Use 'url' as the key for URL nodes\n    const existing = nodes.get({ filter: n => n.type === type && n[key] === value })[0];\n    if (existing) return existing.id;\n    \n    const nodeId = nextId++;\n    newNodes.push({\n        id: nodeId,\n        type: type,\n        [key]: value,\n        ...properties\n    });\n    return nodeId;\n}\n\n//security trails API key save\nfunction saveSecuritytrailsApiKey() {\n    securitytrailsApiKey = document.getElementById('securitytrailsApiKey').value.trim();\n    const storeKey = document.getElementById('storeSecuritytrailsKey').checked;\n    if (securitytrailsApiKey) {\n        if (storeKey) localStorage.setItem('securitytrailsApiKey', securitytrailsApiKey);\n        else localStorage.removeItem('securitytrailsApiKey');\n        showToast('SecurityTrails API key saved successfully!', 'success');\n    } else {\n        localStorage.removeItem('securitytrailsApiKey');\n        showToast('Please enter a valid SecurityTrails API key.', 'error');\n    }\n}\n\n\n\n//Export visable only\n\nfunction exportVisibleGraph() {\n    // Filter for only visible nodes\n    const visibleNodes = nodes.get().filter(node => !node.hidden).map(node => ({ \n        id: node.id,\n        type: node.type,\n        name: node.name,\n        email: node.email,\n        ip: node.ip,\n        domain: node.domain,\n        organization: node.organization,\n        portType: node.portType,\n        portNumber: node.portNumber,\n        address: node.address,\n        accountNumber: node.accountNumber,\n        sortCode: node.sortCode,\n        techName: node.techName,\n        techVersion: node.techVersion,\n        deviceCategory: node.deviceCategory,\n        deviceName: node.deviceName,\n        malwareName: node.malwareName,\n        malwareType: node.malwareType,\n        country: node.country,\n        asn: node.asn,\n        city: node.city,\n        value: node.value,\n        vulnName: node.vulnName,\n        cve: node.cve,\n        url: node.url,\n        x: node.x,\n        y: node.y,\n        label: node.label,\n        title: node.title,\n        color: node.color,\n        size: node.size\n    }));\n\n    // Get all edges and filter for those connecting visible nodes\n    const visibleNodeIds = new Set(visibleNodes.map(node => node.id));\n    const visibleEdges = edges.get().filter(edge => \n        !edge.hidden && \n        visibleNodeIds.has(edge.from) && \n        visibleNodeIds.has(edge.to)\n    ).map(edge => ({\n        id: edge.id,\n        from: edge.from,\n        to: edge.to,\n        label: edge.label\n    }));\n\n    // Create export data object\n    const exportData = {\n        nodes: visibleNodes,\n        edges: visibleEdges\n    };\n\n    // Convert to JSON and trigger download\n    const json = JSON.stringify(exportData, null, 2);\n    const blob = new Blob([json], { type: 'application/json' });\n    const url = URL.createObjectURL(blob);\n    const a = document.createElement('a');\n    a.href = url;\n    a.download = `visible_network_graph_${new Date().toISOString().replace(/[:.]/g, '-')}.json`;\n    a.click();\n    URL.revokeObjectURL(url);\n    \n    showToast('Visible graph exported to JSON', 'success');\n}\n\n\n// Single Domain Enrichment with SecurityTrails\nconst throttledEnrichSecurityTrailsDomain = throttleRequest(async function enrichSecurityTrailsDomain(domain, domainNodeId, isBulk = false, signal) {\n    if (!securitytrailsApiKey && !ignoreApiKeysViaProxy) {\n        showToast('Please set your SecurityTrails API key in the \"Config\" tab first.', 'error');\n        return;\n    }\n\n    if (!isBulk) network.setOptions({ physics: { enabled: false } });\n\n    try {\n        const baseUrl = `https://api.securitytrails.com/v1/domain/${domain}/subdomains`;\n        const url = constructUrl(baseUrl);\n        console.log('Requesting SecurityTrails URL:', url);\n\n        const response = await fetch(url, {\n            headers: { \n                'apikey': securitytrailsApiKey, // Updated to 'apikey'\n                'Accept': 'application/json' \n            },\n            signal\n        });\n\n        if (!response.ok) {\n            const errorText = await response.text();\n            if (response.status === 404) {\n                showToast(`No subdomains found for domain ${domain} in SecurityTrails`, 'info');\n                return;\n            }\n            throw new Error(`Failed to fetch SecurityTrails data: ${response.statusText} - ${errorText}`);\n        }\n\n        const data = await response.json();\n        console.log('SecurityTrails Response:', data);\n\n        if (!data.subdomains || data.subdomains.length === 0) {\n            showToast(`No subdomains associated with domain ${domain} in SecurityTrails`, 'info');\n            return;\n        }\n\n        const newNodes = [];\n        const newEdges = [];\n        const existingDomains = new Map(nodes.get({ filter: n => n.type === 'domain' }).map(n => [n.domain, n.id]));\n\n        data.subdomains.forEach(subdomain => {\n            const fullSubdomain = `${subdomain}.${domain}`;\n            let subdomainId = existingDomains.get(fullSubdomain);\n            if (!subdomainId) {\n                subdomainId = nextId++;\n                newNodes.push({\n                    id: subdomainId,\n                    type: 'domain',\n                    label: `Subdomain: ${fullSubdomain}`,\n                    title: `Subdomain: ${fullSubdomain}\\nFrom SecurityTrails`,\n                    color: { background: '#60a5fa' },\n                    domain: fullSubdomain\n                });\n                existingDomains.set(fullSubdomain, subdomainId);\n            }\n            const edgeId = `${domainNodeId}-${subdomainId}-SubdomainOf`;\n            if (!edges.get(edgeId) && !newEdges.some(e => e.id === edgeId)) {\n                newEdges.push({ id: edgeId, from: domainNodeId, to: subdomainId, label: 'Subdomain of' });\n            }\n        });\n\n        if (newNodes.length > 0) nodes.add(newNodes);\n        if (newEdges.length > 0) edges.add(newEdges);\n\n        updateNodeSizes();\n        updateSelectOptions();\n        if (!isBulk) {\n            await stabilizeNetwork();\n            showToast(`Domain ${domain} enriched with ${data.subdomains.length} subdomains from SecurityTrails`, 'success');\n        }\n    } catch (error) {\n        if (error.name === 'AbortError') {\n            showToast(`Enrichment of domain ${domain} cancelled`, 'info');\n            return;\n        }\n        console.error(`Error enriching domain ${domain} with SecurityTrails: ${error.message}`);\n        showToast(`Error enriching domain ${domain}: ${error.message}`, 'error');\n        if (!isBulk) await stabilizeNetwork();\n    }\n}, SECURITYTRAILS_RATE_LIMIT_MS);\n\n\nasync function enrichAllSecurityTrails() {\n    if (!securitytrailsApiKey && !ignoreApiKeysViaProxy) {\n        showToast('Please set your SecurityTrails API key in the \"Config\" tab first.', 'error');\n        return;\n    }\n\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const domainNodes = nodes.get({ filter: n => n.type === 'domain' && n.domain });\n    const totalDomains = domainNodes.length;\n    let successfulEnrichments = 0;\n\n    if (totalDomains === 0) {\n        showToast('No domain nodes found to enrich', 'info');\n        completeProgressBar();\n        return;\n    }\n\n    const delayBetweenRequests = SECURITYTRAILS_RATE_LIMIT_MS; // Use the defined rate limit\n    const estimatedTimeMs = totalDomains * delayBetweenRequests;\n    const estimatedSeconds = Math.ceil(estimatedTimeMs / 1000);\n    const timeEstimateStr = estimatedSeconds > 60 \n        ? `${Math.floor(estimatedSeconds / 60)}m ${estimatedSeconds % 60}s` \n        : `${estimatedSeconds}s`;\n\n    showToast(`Starting SecurityTrails enrichment for ${totalDomains} domains`, 'info');\n    document.getElementById('progress-bar').textContent = `SecurityTrails Enrichment: 0/${totalDomains} Domains (0%) - Est. ${timeEstimateStr}`;\n\n    for (let i = 0; i < totalDomains; i++) {\n        if (activeTaskController?.signal.aborted) {\n            showToast('SecurityTrails enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = `SecurityTrails Enrichment: Stopped at ${successfulEnrichments}/${totalDomains} Domains`;\n            break;\n        }\n\n        const node = domainNodes[i];\n        let attempts = 0;\n        const maxAttempts = 3;\n        let success = false;\n\n        while (attempts < maxAttempts && !success) {\n            try {\n                await throttledEnrichSecurityTrailsDomain(node.domain, node.id, true);\n                successfulEnrichments++;\n                success = true;\n            } catch (error) {\n                if (error.message.includes('429')) {\n                    attempts++;\n                    if (attempts < maxAttempts) {\n                        const backoffTime = delayBetweenRequests * attempts; // Exponential backoff: 2s, 4s, 6s\n                        showToast(`Rate limit hit for ${node.domain}, retrying in ${backoffTime / 1000}s (Attempt ${attempts}/${maxAttempts})`, 'warning');\n                        await new Promise(resolve => setTimeout(resolve, backoffTime));\n                    } else {\n                        showToast(`Failed to enrich ${node.domain} after ${maxAttempts} attempts: ${error.message}`, 'error');\n                    }\n                } else {\n                    showToast(`Error enriching ${node.domain}: ${error.message}`, 'error');\n                    break; // Non-429 errors skip retries\n                }\n            }\n        }\n\n        const progress = ((successfulEnrichments / totalDomains) * 100).toFixed(1);\n        document.getElementById('progress-bar').textContent = `SecurityTrails Enrichment: ${successfulEnrichments}/${totalDomains} Domains (${progress}%)`;\n\n        // Wait between requests, even on success, to respect rate limits\n        if (i < totalDomains - 1) { // No delay after the last domain\n            await new Promise(resolve => setTimeout(resolve, delayBetweenRequests));\n        }\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    //ensureInteractionSettings();\n    completeProgressBar();\n    showToast(`SecurityTrails enrichment completed: ${successfulEnrichments}/${totalDomains} domains enriched`, 'success');\n}\n\n\n\n// Multiple Domain Enrichment with SecurityTrails (Context Menu)\nconst throttledEnrichSecurityTrailsDomainMultiple = throttleRequest(async function enrichSecurityTrailsDomainMultiple(domains, nodeIds) {\n    if (!Array.isArray(domains) || !Array.isArray(nodeIds) || domains.length !== nodeIds.length) {\n        showToast('Invalid input for multiple SecurityTrails enrichment', 'error');\n        return;\n    }\n\n    if (!securitytrailsApiKey && !ignoreApiKeysViaProxy) {\n        showToast('Please set your SecurityTrails API key in the \"Config\" tab first.', 'error');\n        return;\n    }\n\n    showProgressBar();\n    let successfulEnrichments = 0;\n    const totalDomains = domains.length;\n\n    document.getElementById('progress-bar').textContent = `SecurityTrails Enrichment: 0/${totalDomains} Domains (0%)`;\n\n    for (let i = 0; i < domains.length; i++) {\n        if (activeTaskController?.signal.aborted) {\n            showToast('SecurityTrails enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = `SecurityTrails Enrichment: Stopped at ${successfulEnrichments}/${totalDomains} Domains`;\n            break;\n        }\n        await throttledEnrichSecurityTrailsDomain(domains[i], nodeIds[i], true)\n            .then(() => successfulEnrichments++)\n            .catch(error => console.error(`Failed to enrich ${domains[i]}: ${error.message}`));\n        \n        const progress = ((successfulEnrichments / totalDomains) * 100).toFixed(1);\n        document.getElementById('progress-bar').textContent = `SecurityTrails Enrichment: ${successfulEnrichments}/${totalDomains} Domains (${progress}%)`;\n        await new Promise(resolve => setTimeout(resolve, 200)); // Small delay between requests\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    completeProgressBar();\n    showToast(`Enriched ${successfulEnrichments}/${domains.length} domains with SecurityTrails subdomains`, 'success');\n}, SECURITYTRAILS_RATE_LIMIT_MS);\n\n//Refang IOCs\n\nfunction refangText(text) {\n    if (typeof text !== 'string') return text;\n\n    let refanged = text;\n\n    // URL-specific defanging patterns\n    refanged = refanged.replace(/hxxps/gi, 'https');          // hxxps → https\n    refanged = refanged.replace(/hxxp/gi, 'http');           // hxxp → http\n    refanged = refanged.replace(/\\[hxxps\\]/gi, 'https');     // [hxxps] → https\n    refanged = refanged.replace(/\\[hxxp\\]/gi, 'http');       // [hxxp] → http\n    refanged = refanged.replace(/\\[https\\]/gi, 'https');     // [https] → https\n    refanged = refanged.replace(/\\[http\\]/gi, 'http');       // [http] → http\n    refanged = refanged.replace(/\\[colon\\]/gi, ':');         // [colon] → :\n    refanged = refanged.replace(/\\[\\/\\]/g, '/');             // [/] → /\n    refanged = refanged.replace(/\\[ \\/\\]/g, '/');            // [ /] → /\n    refanged = refanged.replace(/\\[slash\\]/gi, '/');         // [slash] → /\n    refanged = refanged.replace(/\\[\\.\\]/g, '.');             // [.] → .\n    refanged = refanged.replace(/\\[dot\\]/gi, '.');           // [dot] or [DOT] → .\n    refanged = refanged.replace(/\\[ \\. \\]/g, '.');           // [ . ] → .\n\n    // Email-specific defanging (unchanged)\n    refanged = refanged.replace(/\\[at\\]/gi, '@');            // [at] or [AT] → @\n    refanged = refanged.replace(/\\[ @ \\]/g, '@');            // [ @ ] → @\n    refanged = refanged.replace(/ at /gi, '@');              // \" at \" → @\n\n    // General cleanup\n    refanged = refanged.replace(/\\[\\s*\\]/g, '');             // Empty brackets [] → ''\n    refanged = refanged.replace(/\\[([^\\]]+)\\]/g, '$1');      // [text] → text (non-special cases)\n    refanged = refanged.replace(/\\s+/g, ' ').trim();         // Normalize spaces\n\n    return refanged;\n}\n\n\n\n//notes \nfunction editNodeNotes(nodeId) {\n    console.log('Attempting to open notes for node:', nodeId);\n    const node = nodes.get(nodeId);\n    if (!node) {\n        showToast('Node not found', 'error');\n        return;\n    }\n\n    const modal = document.getElementById('notes-modal');\n    const notesTextarea = document.getElementById('notes-textarea');\n    if (!modal || !notesTextarea) {\n        console.error('Modal or textarea not found');\n        showToast('Notes interface unavailable', 'error');\n        return;\n    }\n\n    // Get the modal title element\n    const modalTitle = modal.querySelector('h3');\n    if (!modalTitle) {\n        console.error('Modal title element not found');\n        showToast('Modal title unavailable', 'error');\n        return;\n    }\n\n    // Determine the entity name\n    let entityName;\n    switch (node.type) {\n        case 'contact':\n            entityName = node.name || node.email || node.label.split('\\n')[0];\n            break;\n        case 'ip':\n            entityName = node.ip || node.label.split('\\n')[0];\n            break;\n        case 'domain':\n            entityName = node.domain || node.label.split('\\n')[0];\n            break;\n        case 'organization':\n            entityName = node.organization || node.label.split('\\n')[0];\n            break;\n        case 'port':\n            entityName = `${node.portType}/${node.portNumber}` || node.label.split('\\n')[0];\n            break;\n        case 'wallet':\n            entityName = node.address || node.label.split('\\n')[0];\n            break;\n        case 'bank':\n            entityName = node.accountNumber || node.label.split('\\n')[0];\n            break;\n        case 'technology':\n            entityName = node.techName || node.label.split('\\n')[0];\n            break;\n        case 'device':\n            entityName = node.deviceName || node.label.split('\\n')[0];\n            break;\n        case 'malware':\n            entityName = node.malwareName || node.label.split('\\n')[0];\n            break;\n        case 'vulnerability':\n            entityName = node.vulnName || node.cve || node.label.split('\\n')[0];\n            break;\n        default:\n            entityName = node.label.split('\\n')[0]; // Fallback to first line of label\n    }\n\n    // Set the modal title\n    modalTitle.textContent = `Edit Notes for ${entityName}`;\n\n    currentEditingNodeId = nodeId;\n    notesTextarea.value = node.notes || '';\n\n    // Remove existing overlay if any\n    let overlay = document.getElementById('modal-overlay');\n    if (overlay) overlay.remove();\n\n    // Create new overlay\n    overlay = document.createElement('div');\n    overlay.id = 'modal-overlay';\n    overlay.style.cssText = `\n        position: fixed;\n        top: 0;\n        left: 0;\n        width: 100vw;\n        height: 100vh;\n        background: rgba(0, 0, 0, 0.5);\n        z-index: 1999;\n    `;\n    document.body.appendChild(overlay);\n\n    // Reset modal styles\n    modal.style.position = 'fixed';\n    modal.style.top = '50%';\n    modal.style.left = '50%';\n    modal.style.transform = 'translate(-50%, -50%)';\n    modal.style.zIndex = '2000';\n    modal.style.display = 'block';\n\n    // Force reflow\n    void modal.offsetWidth;\n    console.log('Modal display set to:', modal.style.display);\n\n    notesTextarea.focus();\n\n    // Remove any existing click handler to prevent accumulation\n    if (window.notesClickHandler) {\n        document.removeEventListener('click', window.notesClickHandler);\n    }\n\n    // Define and add new click handler\n    const clickHandler = (event) => {\n        if (modal.style.display === 'block' && !modal.contains(event.target)) {\n            hideNotesModal();\n        }\n    };\n    setTimeout(() => {\n        document.addEventListener('click', clickHandler);\n        window.notesClickHandler = clickHandler; // Store globally for cleanup\n    }, 100);\n\n    // Remove any existing resize handler\n    if (modal.dataset.resizeHandler) {\n        window.removeEventListener('resize', modal.dataset.resizeHandler);\n    }\n\n    // Add resize handler\n    const resizeHandler = () => {\n        modal.style.top = '50%';\n        modal.style.left = '50%';\n        modal.style.transform = 'translate(-50%, -50%)';\n    };\n    window.addEventListener('resize', resizeHandler);\n    modal.dataset.resizeHandler = resizeHandler;\n}\n\n\n// save notes\n\nfunction saveNodeNotes() {\n    if (currentEditingNodeId === null) return;\n\n    const node = nodes.get(currentEditingNodeId);\n    if (!node) {\n        showToast('Node not found', 'error');\n        hideNotesModal();\n        return;\n    }\n\n    const notesTextarea = document.getElementById('notes-textarea');\n    const newNotes = notesTextarea.value.trim();\n\n    if (newNotes.length > 1000) {\n        showToast('Notes cannot exceed 1000 characters', 'error');\n        return;\n    }\n\n    // Define configs as in createNodeData (or move this to a global scope if reused elsewhere)\n    const configs = {\n        contact: {\n            title: v => `Contact\\nName: ${v.name}${v.email ? '\\nEmail: ' + v.email : ''}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        ip: {\n            title: v => `IP Address: ${v.ip}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        domain: {\n            title: v => `Domain: ${v.domain}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        organization: {\n            title: v => `Organization: ${v.organization}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        port: {\n            title: v => `Port\\nType: ${v.portType}\\nNumber: ${v.portNumber}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        wallet: {\n            title: v => `Wallet\\nAddress: ${v.address}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        bank: {\n            title: v => `Bank Account\\nAccount Number: ${v.accountNumber}\\nSort Code: ${v.sortCode}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        technology: {\n            title: v => `Technology\\nName: ${v.techName}${v.techVersion ? '\\nVersion: ' + v.techVersion : ''}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        device: {\n            title: v => `Device\\nName: ${v.deviceName}\\nCategory: ${v.deviceCategory}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        malware: {\n            title: v => `Malware\\nName: ${v.malwareName}\\nType: ${v.malwareType}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        vulnerability: {\n            title: v => `Vulnerability\\nName: ${v.vulnName}${v.cve ? '\\nCVE: ' + v.cve : ''}${v.url ? '\\nURL: ' + v.url : ''}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        // Add other types as needed\n    };\n\n    const config = configs[node.type];\n    if (!config) {\n        showToast(`Unsupported node type: ${node.type}`, 'error');\n        hideNotesModal();\n        return;\n    }\n\n    const updatedNode = { ...node, notes: newNotes };\n    nodes.update({\n        id: currentEditingNodeId,\n        notes: newNotes,\n        title: config.title(updatedNode) // Call the function directly\n    });\n\n    saveStateAfterOperation();\n    showToast('Notes saved successfully', 'success');\n    hideNotesModal();\n}\n\nfunction hideNotesModal() {\n    const modal = document.getElementById('notes-modal');\n    if (!modal) return;\n\n    modal.style.display = 'none';\n    const overlay = document.getElementById('modal-overlay');\n    if (overlay) overlay.remove();\n\n    // Clean up click handler\n    if (window.notesClickHandler) {\n        document.removeEventListener('click', window.notesClickHandler);\n        delete window.notesClickHandler;\n    }\n\n    // Clean up resize handler\n    const resizeHandler = modal.dataset.resizeHandler;\n    if (resizeHandler) {\n        window.removeEventListener('resize', resizeHandler);\n        delete modal.dataset.resizeHandler;\n    }\n\n    currentEditingNodeId = null;\n}\n\n\n\n\n\n// delete nodes\n\nfunction deleteNodes(nodeIds) {\n    console.log('deleteNodes called with:', nodeIds);\n    \n    if (!nodeIds || !Array.isArray(nodeIds) || nodeIds.length === 0) {\n        showToast('No nodes selected to delete', 'error');\n        return;\n    }\n\n    // Filter out invalid or non-existent node IDs\n    const validNodeIds = nodeIds.filter(id => {\n        const node = nodes.get(id);\n        if (!node) {\n            console.warn(`Node with ID ${id} not found`);\n            return false;\n        }\n        return true;\n    });\n\n    if (validNodeIds.length === 0) {\n        showToast('No valid nodes found to delete', 'error');\n        return;\n    }\n\n    // Remove all edges connected to these nodes\n    edges.forEach(edge => {\n        if (validNodeIds.includes(edge.from) || validNodeIds.includes(edge.to)) {\n            edges.remove({ id: edge.id });\n        }\n    });\n\n    // Remove the nodes\n    nodes.remove(validNodeIds);\n\n    // Update UI and network\n    updateNodeSizes();\n    updateSelectOptions();\n    stabilizeNetwork();\n    saveStateAfterOperation();\n\n    showToast(`${validNodeIds.length} node${validNodeIds.length > 1 ? 's' : ''} deleted`, 'success');\n}\n\n// save URLHAUS keys\n\nfunction saveUrlhausApiKey() {\n    urlhausApiKey = document.getElementById('urlhausApiKey').value.trim();\n    const storeKey = document.getElementById('storeUrlhausKey').checked;\n    if (urlhausApiKey) {\n        if (storeKey) {\n            localStorage.setItem('urlhausApiKey', urlhausApiKey);\n            showToast('URLhaus API key saved successfully!', 'success');\n        } else {\n            localStorage.removeItem('urlhausApiKey');\n            showToast('URLhaus API key set for this session only', 'success');\n        }\n    } else {\n        localStorage.removeItem('urlhausApiKey');\n        showToast('Please enter a valid URLhaus API key.', 'error');\n    }\n}\n\n\n// URL HAUS\n\nconst throttledEnrichURLhaus = throttleRequest(async function enrichURLhaus(url, urlNodeId, isBulk = false, signal) {\n    if (!urlhausApiKey && !ignoreApiKeysViaProxy) {\n        showToast('Please set your URLhaus API key in the \"Config\" tab first.', 'error');\n        return;\n    }\n\n    if (!isBulk) network.setOptions({ physics: { enabled: false } });\n\n    try {\n        const baseUrl = 'https://urlhaus-api.abuse.ch/v1/url/';\n        const urlToQuery = constructUrl(baseUrl);\n        const postData = `url=${encodeURIComponent(url)}`;\n        const headers = {\n            'Content-Type': 'application/x-www-form-urlencoded',\n            'Accept': 'application/json'\n        };\n        if (urlhausApiKey && !ignoreApiKeysViaProxy) {\n            headers['Auth-Key'] = urlhausApiKey;\n        }\n\n        const response = await fetch(urlToQuery, {\n            method: 'POST',\n            headers: headers,\n            body: postData,\n            signal\n        });\n\n        if (!response.ok) {\n            const errorText = await response.text();\n            throw new Error(`Failed to fetch URLhaus data: ${response.statusText} - ${errorText}`);\n        }\n\n        const data = await response.json();\n\n        if (data.query_status !== 'ok') {\n            if (data.query_status === 'no_results') {\n                if (!isBulk) showToast(`No URLhaus data found for ${url}`, 'info');\n                return;\n            }\n            throw new Error(`URLhaus query failed: ${data.query_status}`);\n        }\n\n        const newNodes = [];\n        const newEdges = [];\n        const existingNodes = new Map();\n\n        // Cache existing nodes for deduplication\n        nodes.forEach(node => {\n            if (node.type === 'url' && node.url) existingNodes.set(`url:${node.url}`, node.id);\n            if (node.type === 'tag' && node.tag) existingNodes.set(`tag:${node.tag}`, node.id);\n            if (node.type === 'timestamp' && node.timestamp) existingNodes.set(`timestamp:${node.timestamp}`, node.id);\n            if (node.type === 'threat' && node.threat) existingNodes.set(`threat:${node.threat}`, node.id);\n        });\n\n        // URL Status\n        if (data.url_status) {\n            const statusId = existingNodes.get(`tag:${data.url_status}`) || nextId++;\n            if (!existingNodes.has(`tag:${data.url_status}`)) {\n                newNodes.push({\n                    id: statusId,\n                    type: 'tag',\n                    label: `Status: ${data.url_status}`,\n                    title: `URLhaus Status: ${data.url_status}`,\n                    color: { background: '#6d28d9' },\n                    tag: data.url_status\n                });\n                existingNodes.set(`tag:${data.url_status}`, statusId);\n            }\n            newEdges.push({ id: `${urlNodeId}-${statusId}-Status`, from: urlNodeId, to: statusId, label: 'Status' });\n        }\n\n        // Threat Type\n        if (data.threat) {\n            const threatId = existingNodes.get(`threat:${data.threat}`) || nextId++;\n            if (!existingNodes.has(`threat:${data.threat}`)) {\n                newNodes.push({\n                    id: threatId,\n                    type: 'threat',\n                    label: `Threat: ${data.threat}`,\n                    title: `URLhaus Threat: ${data.threat}`,\n                    color: { background: '#ef4444' },\n                    threat: data.threat\n                });\n                existingNodes.set(`threat:${data.threat}`, threatId);\n            }\n            newEdges.push({ id: `${urlNodeId}-${threatId}-Threat`, from: urlNodeId, to: threatId, label: 'Threat' });\n        }\n\n        // Date Added\n        if (data.date_added) {\n            const dateId = existingNodes.get(`timestamp:${data.date_added}`) || nextId++;\n            if (!existingNodes.has(`timestamp:${data.date_added}`)) {\n                newNodes.push({\n                    id: dateId,\n                    type: 'timestamp',\n                    label: `Added: ${data.date_added}`,\n                    title: `Date Added: ${data.date_added}`,\n                    color: { background: '#f97316' },\n                    timestamp: data.date_added\n                });\n                existingNodes.set(`timestamp:${data.date_added}`, dateId);\n            }\n            newEdges.push({ id: `${urlNodeId}-${dateId}-AddedOn`, from: urlNodeId, to: dateId, label: 'Added on' });\n        }\n\n        // Tags\n        if (data.tags && Array.isArray(data.tags)) {\n            data.tags.forEach(tag => {\n                const tagId = existingNodes.get(`tag:${tag}`) || nextId++;\n                if (!existingNodes.has(`tag:${tag}`)) {\n                    newNodes.push({\n                        id: tagId,\n                        type: 'tag',\n                        label: `Tag: ${tag}`,\n                        title: `URLhaus Tag: ${tag}`,\n                        color: { background: '#6d28d9' },\n                        tag: tag\n                    });\n                    existingNodes.set(`tag:${tag}`, tagId);\n                }\n                newEdges.push({ id: `${urlNodeId}-${tagId}-Tagged`, from: urlNodeId, to: tagId, label: 'Tagged' });\n            });\n        }\n\n        // Update node with URLhaus reference\n        nodes.update({\n            id: urlNodeId,\n            title: `${nodes.get(urlNodeId).title || url}\\nURLhaus Ref: ${data.urlhaus_reference || 'N/A'}`\n        });\n\n        if (newNodes.length) nodes.add(newNodes);\n        if (newEdges.length) edges.add(newEdges);\n\n        updateNodeSizes();\n        updateSelectOptions();\n        if (!isBulk) {\n            await stabilizeNetwork();\n            showToast(`URL ${url} enriched with URLhaus data`, 'success');\n        }\n    } catch (error) {\n        if (error.name === 'AbortError') {\n            showToast(`Enrichment of URL ${url} cancelled`, 'info');\n            return;\n        }\n        console.error(`Error enriching URL ${url} with URLhaus: ${error.message}`);\n        showToast(`Error enriching URL ${url}: ${error.message}`, 'error');\n        if (!isBulk) await stabilizeNetwork();\n    }\n}, RATE_LIMIT_MS);\n\nconst throttledEnrichURLhausMultiple = throttleRequest(async function enrichURLhausMultiple(urls, nodeIds) {\n    if (!Array.isArray(urls) || !Array.isArray(nodeIds) || urls.length !== nodeIds.length) {\n        showToast('Invalid input for multiple URLhaus enrichment', 'error');\n        return;\n    }\n\n    if (!urlhausApiKey && !ignoreApiKeysViaProxy) {\n        showToast('Please set your URLhaus API key in the \"Config\" tab first.', 'error');\n        return;\n    }\n\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const totalURLs = urls.length;\n    let successfulEnrichments = 0;\n\n    document.getElementById('progress-bar').textContent = `URLhaus Enrichment: 0/${totalURLs} URLs (0%)`;\n\n    for (let i = 0; i < totalURLs; i++) {\n        if (activeTaskController?.signal.aborted) {\n            showToast('URLhaus enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = `URLhaus Enrichment: Stopped at ${successfulEnrichments}/${totalURLs} URLs`;\n            break;\n        }\n        await throttledEnrichURLhaus(urls[i], nodeIds[i], true);\n        successfulEnrichments++;\n        const progress = ((successfulEnrichments / totalURLs) * 100).toFixed(1);\n        document.getElementById('progress-bar').textContent = `URLhaus Enrichment: ${successfulEnrichments}/${totalURLs} URLs (${progress}%)`;\n        await new Promise(resolve => setTimeout(resolve, 200)); // Rate limiting\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    completeProgressBar();\n    showToast(`Enriched ${successfulEnrichments}/${totalURLs} URLs with URLhaus`, 'success');\n}, RATE_LIMIT_MS);\n\nasync function enrichAllURLhaus() {\n    if (!urlhausApiKey && !ignoreApiKeysViaProxy) {\n        showToast('Please set your URLhaus API key in the \"Config\" tab first.', 'error');\n        return;\n    }\n\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const urlNodes = nodes.get({ filter: n => n.type === 'url' && n.url });\n    const totalURLs = urlNodes.length;\n    let successfulEnrichments = 0;\n\n    if (totalURLs === 0) {\n        showToast('No URL nodes found to enrich', 'info');\n        completeProgressBar();\n        return;\n    }\n\n    const batchSize = 10;\n    const delayBetweenBatches = 500;\n    const totalBatches = Math.ceil(totalURLs / batchSize);\n    const estimatedTimeMs = totalBatches * delayBetweenBatches;\n    const timeEstimateStr = Math.ceil(estimatedTimeMs / 60000) + 'm';\n\n    document.getElementById('progress-bar').textContent = `URLhaus Enrichment: 0/${totalURLs} URLs (0%) - Est. ${timeEstimateStr}`;\n\n    for (let i = 0; i < totalURLs; i += batchSize) {\n        if (activeTaskController?.signal.aborted) {\n            showToast('URLhaus enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = `URLhaus Enrichment: Stopped at ${successfulEnrichments}/${totalURLs} URLs`;\n            break;\n        }\n\n        const batch = urlNodes.slice(i, Math.min(i + batchSize, totalURLs));\n        const promises = batch.map(node => \n            throttledEnrichURLhaus(node.url, node.id, true)\n                .then(() => successfulEnrichments++)\n                .catch(error => console.error(`Error for ${node.url}: ${error.message}`))\n        );\n\n        await Promise.all(promises);\n\n        const progress = ((successfulEnrichments / totalURLs) * 100).toFixed(1);\n        document.getElementById('progress-bar').textContent = `URLhaus Enrichment: ${successfulEnrichments}/${totalURLs} URLs (${progress}%)`;\n        await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    //ensureInteractionSettings();\n    completeProgressBar();\n    showToast(`URLhaus enrichment completed: ${successfulEnrichments}/${totalURLs} URLs enriched`, 'success');\n}\n\n\n// Replace the existing isPrivateIP function (for completeness)\nfunction isPrivateIP(ip) {\n    console.log(`isPrivateIP called with: ${ip}`);\n    const octets = ip.split('.').map(Number);\n    console.log(`Parsed octets: ${octets}`);\n    \n    if (octets.length !== 4 || octets.some(o => o < 0 || o > 255)) {\n        console.log(`Invalid IP format: ${ip}`);\n        return false;\n    }\n\n    const isPrivate = (\n        (octets[0] === 10) || // 10.0.0.0/8\n        (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) || // 172.16.0.0/12\n        (octets[0] === 192 && octets[1] === 168) // 192.168.0.0/16\n    );\n    \n    console.log(`IP ${ip} is ${isPrivate ? 'private' : 'public'}`);\n    return isPrivate;\n}\n\n\nfunction exportConfigBackup() {\n    // Collect API keys and proxy config from localStorage\n    const configData = {\n        ipinfoApiKey: localStorage.getItem('ipinfoApiKey') || '',\n        shodanApiKey: localStorage.getItem('shodanApiKey') || '',\n        greynoiseApiKey: localStorage.getItem('greynoiseApiKey') || '',\n        urlscanApiKey: localStorage.getItem('urlscanApiKey') || '',\n        securitytrailsApiKey: localStorage.getItem('securitytrailsApiKey') || '',\n        urlhausApiKey: localStorage.getItem('urlhausApiKey') || '',\n        corsProxyUrl: localStorage.getItem('corsProxyUrl') || '',\n        routeViaProxy: localStorage.getItem('routeViaProxy') === 'true',\n        ignoreApiKeysViaProxy: localStorage.getItem('ignoreApiKeysViaProxy') === 'true'\n    };\n\n    // Convert to JSON\n    const json = JSON.stringify(configData, null, 2);\n    const blob = new Blob([json], { type: 'application/json' });\n    const url = URL.createObjectURL(blob);\n    const a = document.createElement('a');\n    a.href = url;\n    a.download = 'configuration_backup.json';\n    document.body.appendChild(a);\n    a.click();\n    document.body.removeChild(a);\n    URL.revokeObjectURL(url);\n\n    showToast('Configuration backup exported successfully', 'success');\n}\n\nfunction importConfig() {\n    // Create a temporary file input element\n    const input = document.createElement('input');\n    input.type = 'file';\n    input.accept = '.json';\n\n    input.onchange = function(event) {\n        const file = event.target.files[0];\n        if (!file) {\n            showToast('No file selected', 'error');\n            return;\n        }\n\n        const reader = new FileReader();\n        reader.onload = function(e) {\n            try {\n                const configData = JSON.parse(e.target.result);\n\n                // Validate the imported data\n                const expectedKeys = [\n                    'ipinfoApiKey', 'shodanApiKey', 'greynoiseApiKey', 'urlscanApiKey', \n                    'securitytrailsApiKey', 'urlhausApiKey', 'corsProxyUrl', \n                    'routeViaProxy', 'ignoreApiKeysViaProxy'\n                ];\n                const hasValidKeys = expectedKeys.every(key => key in configData);\n                if (!hasValidKeys) {\n                    throw new Error('Invalid configuration file: missing required keys');\n                }\n\n                // Update localStorage and global variables\n                ipinfoApiKey = configData.ipinfoApiKey || '';\n                shodanApiKey = configData.shodanApiKey || '';\n                greynoiseApiKey = configData.greynoiseApiKey || '';\n                urlscanApiKey = configData.urlscanApiKey || '';\n                securitytrailsApiKey = configData.securitytrailsApiKey || '';\n                urlhausApiKey = configData.urlhausApiKey || '';\n                corsProxyUrl = configData.corsProxyUrl || 'http://localhost:3000/proxy?url=';\n                routeViaProxy = !!configData.routeViaProxy;\n                ignoreApiKeysViaProxy = !!configData.ignoreApiKeysViaProxy;\n\n                // Persist to localStorage\n                localStorage.setItem('ipinfoApiKey', ipinfoApiKey);\n                localStorage.setItem('shodanApiKey', shodanApiKey);\n                localStorage.setItem('greynoiseApiKey', greynoiseApiKey);\n                localStorage.setItem('urlscanApiKey', urlscanApiKey);\n                localStorage.setItem('securitytrailsApiKey', securitytrailsApiKey);\n                localStorage.setItem('urlhausApiKey', urlhausApiKey);\n                localStorage.setItem('corsProxyUrl', corsProxyUrl);\n                localStorage.setItem('routeViaProxy', routeViaProxy.toString());\n                localStorage.setItem('ignoreApiKeysViaProxy', ignoreApiKeysViaProxy.toString());\n\n                // Update UI elements\n                document.getElementById('ipinfoApiKey').value = ipinfoApiKey;\n                document.getElementById('shodanApiKey').value = shodanApiKey;\n                document.getElementById('greynoiseApiKey').value = greynoiseApiKey;\n                document.getElementById('urlscanApiKey').value = urlscanApiKey;\n                document.getElementById('securitytrailsApiKey').value = securitytrailsApiKey;\n                document.getElementById('urlhausApiKey').value = urlhausApiKey;\n                document.getElementById('corsProxyUrl').value = corsProxyUrl;\n                document.getElementById('routeViaProxy').checked = routeViaProxy;\n                document.getElementById('ignoreApiKeysViaProxy').checked = ignoreApiKeysViaProxy;\n\n                // Update checkbox states based on whether keys are stored\n                document.getElementById('storeIpinfoKey').checked = !!ipinfoApiKey;\n                document.getElementById('storeShodanKey').checked = !!shodanApiKey;\n                document.getElementById('storeGreynoiseKey').checked = !!greynoiseApiKey;\n                document.getElementById('storeUrlscanKey').checked = !!urlscanApiKey;\n                document.getElementById('storeSecuritytrailsKey').checked = !!securitytrailsApiKey;\n                document.getElementById('storeUrlhausKey').checked = !!urlhausApiKey;\n                document.getElementById('storeCorsProxy').checked = !!corsProxyUrl;\n\n                showToast('Configuration imported successfully', 'success');\n            } catch (error) {\n                console.error('Error importing config:', error);\n                showToast(`Failed to import configuration: ${error.message}`, 'error');\n            }\n        };\n        reader.readAsText(file);\n    };\n\n    // Trigger file selection\n    input.click();\n}\n\n// IMport from NMAP XML Function\n\nasync function importNMAP() {\n    // Create a temporary file input element\n    const input = document.createElement('input');\n    input.type = 'file';\n    input.accept = '.xml';\n\n    input.onchange = async function(event) {\n        const file = event.target.files[0];\n        if (!file) {\n            showToast('No file selected', 'error');\n            return;\n        }\n\n        const reader = new FileReader();\n        reader.onload = async function(e) {\n            try {\n                let xmlText = e.target.result;\n                console.log('Raw XML Text:', xmlText); // Log raw input for debugging\n\n                // Extract valid XML content between <?xml and </nmaprun>\n                const xmlStart = xmlText.indexOf('<?xml');\n                const xmlEnd = xmlText.lastIndexOf('</nmaprun>') + '</nmaprun>'.length;\n                if (xmlStart === -1 || xmlEnd === -1) {\n                    throw new Error('Could not find valid XML content in file');\n                }\n                xmlText = xmlText.substring(xmlStart, xmlEnd);\n                console.log('Extracted XML Text:', xmlText); // Log extracted XML\n\n                const parser = new DOMParser();\n                const xmlDoc = parser.parseFromString(xmlText, 'application/xml');\n\n                // Check for XML parsing errors\n                const parserError = xmlDoc.querySelector('parsererror');\n                if (parserError) {\n                    console.error('Parser Error Details:', parserError.textContent);\n                    throw new Error('Invalid Nmap XML file: ' + parserError.textContent);\n                }\n\n                console.log('Parsed XML Document:', xmlDoc); // Log parsed doc for debugging\n\n                // Maps for deduplication\n                const existingNodes = new Map();\n                nodes.forEach(node => {\n                    if (node.type === 'ip' && node.ip) existingNodes.set(`ip:${node.ip}`, node);\n                    if (node.type === 'domain' && node.domain) existingNodes.set(`domain:${node.domain.toLowerCase()}`, node);\n                    if (node.type === 'port' && node.portNumber) existingNodes.set(`port:${node.portType}/${node.portNumber}`, node);\n                    if (node.type === 'service' && node.serviceName) existingNodes.set(`service:${node.serviceName.toLowerCase()}_${node.portType}/${node.portNumber}`, node);\n                    if (node.type === 'os' && node.os) existingNodes.set(`os:${node.os}`, node);\n                    if (node.type === 'cpe' && node.cpe) existingNodes.set(`cpe:${node.cpe}`, node);\n                    if (node.type === 'hop' && node.ipaddr) existingNodes.set(`hop:${node.ipaddr}`, node);\n                    if (node.type === 'mac' && node.mac) existingNodes.set(`mac:${node.mac.toLowerCase()}`, node);\n                });\n\n                const newNodes = [];\n                const newEdges = [];\n                let processedCount = 0;\n                const batchSize = 50;\n\n                // Process each host\n                const hosts = xmlDoc.getElementsByTagName('host');\n                if (hosts.length === 0) {\n                    throw new Error('No hosts found in Nmap XML');\n                }\n\n                for (const host of hosts) {\n                    // Extract IP address\n                    const ipAddr = host.querySelector('address[addrtype=\"ipv4\"]');\n                    const ip = ipAddr ? ipAddr.getAttribute('addr') : null;\n                    if (!ip) continue;\n\n                    let ipNodeId;\n                    if (!existingNodes.has(`ip:${ip}`)) {\n                        ipNodeId = nextId++;\n                        newNodes.push({\n                            id: ipNodeId,\n                            type: 'ip',\n                            label: `IP: ${ip}`,\n                            title: `IP Address: ${ip}`,\n                            color: { background: '#f87171' },\n                            ip: ip,\n                            size: 20\n                        });\n                        existingNodes.set(`ip:${ip}`, { id: ipNodeId });\n                        console.log(`Added IP node: ${ip}, ID: ${ipNodeId}`);\n                    } else {\n                        ipNodeId = existingNodes.get(`ip:${ip}`).id;\n                    }\n\n                    // Extract MAC address\n                    const macAddr = host.querySelector('address[addrtype=\"mac\"]');\n                    const mac = macAddr ? macAddr.getAttribute('addr') : null;\n                    if (mac) {\n                        const macKey = `mac:${mac.toLowerCase()}`;\n                        let macNodeId;\n                        if (!existingNodes.has(macKey)) {\n                            macNodeId = nextId++;\n                            newNodes.push({\n                                id: macNodeId,\n                                type: 'mac',\n                                label: `MAC: ${mac}`,\n                                title: `MAC Address: ${mac}`,\n                                color: { background: '#8b5cf6' },\n                                mac: mac,\n                                size: 15\n                            });\n                            existingNodes.set(macKey, { id: macNodeId });\n                            console.log(`Added MAC node: ${mac}, ID: ${macNodeId}`);\n                        } else {\n                            macNodeId = existingNodes.get(macKey).id;\n                        }\n                        newEdges.push({\n                            id: `edge_${ipNodeId}_${macNodeId}_${Date.now()}`,\n                            from: ipNodeId,\n                            to: macNodeId,\n                            label: 'Has MAC'\n                        });\n                        console.log(`Linked IP ${ipNodeId} to MAC: ${macNodeId}`);\n                    }\n\n                    // Extract hostnames\n                    const hostnames = host.getElementsByTagName('hostname');\n                    for (const hostname of hostnames) {\n                        const domain = hostname.getAttribute('name');\n                        if (domain && !existingNodes.has(`domain:${domain.toLowerCase()}`)) {\n                            const domainNodeId = nextId++;\n                            newNodes.push({\n                                id: domainNodeId,\n                                type: 'domain',\n                                label: `Domain: ${domain}`,\n                                title: `Domain: ${domain}\\nType: ${hostname.getAttribute('type')}`,\n                                color: { background: '#60a5fa' },\n                                domain: domain,\n                                size: 20\n                            });\n                            existingNodes.set(`domain:${domain.toLowerCase()}`, { id: domainNodeId });\n                            newEdges.push({\n                                id: `edge_${ipNodeId}_${domainNodeId}_${Date.now()}`,\n                                from: ipNodeId,\n                                to: domainNodeId,\n                                label: 'Resolves to'\n                            });\n                            console.log(`Added Domain node: ${domain}, ID: ${domainNodeId}, Edge to IP: ${ipNodeId}`);\n                        } else if (domain) {\n                            const existingDomainNode = existingNodes.get(`domain:${domain.toLowerCase()}`);\n                            if (!newEdges.some(e => e.from === ipNodeId && e.to === existingDomainNode.id)) {\n                                newEdges.push({\n                                    id: `edge_${ipNodeId}_${existingDomainNode.id}_${Date.now()}`,\n                                    from: ipNodeId,\n                                    to: existingDomainNode.id,\n                                    label: 'Resolves to'\n                                });\n                                console.log(`Linked IP ${ipNodeId} to existing Domain: ${existingDomainNode.id}`);\n                            }\n                        }\n                    }\n\n                    // Extract ports, services, and certificate domains\n                    const ports = host.getElementsByTagName('port');\n                    for (const port of ports) {\n                        const protocol = port.getAttribute('protocol');\n                        const portNumber = port.getAttribute('portid');\n                        const state = port.querySelector('state')?.getAttribute('state') || 'unknown';\n                        const service = port.querySelector('service');\n                        const serviceName = service?.getAttribute('name') || 'unknown';\n                        const product = service?.getAttribute('product') || '';\n                        const version = service?.getAttribute('version') || '';\n                        const extrainfo = service?.getAttribute('extrainfo') || '';\n\n                        const portKey = `port:${protocol.toUpperCase()}/${portNumber}`;\n                        let portNodeId;\n                        if (!existingNodes.has(portKey)) {\n                            portNodeId = nextId++;\n                            newNodes.push({\n                                id: portNodeId,\n                                type: 'port',\n                                label: `${protocol.toUpperCase()}/${portNumber} (${state})`,\n                                title: `Port\\nType: ${protocol.toUpperCase()}\\nNumber: ${portNumber}\\nState: ${state}`,\n                                color: { background: state === 'open' ? '#a78bfa' : '#d1d5db' },\n                                portType: protocol.toUpperCase(),\n                                portNumber: portNumber,\n                                size: 10\n                            });\n                            existingNodes.set(portKey, { id: portNodeId });\n                            console.log(`Added Port node: ${protocol}/${portNumber}, State: ${state}, ID: ${portNodeId}`);\n                        } else {\n                            portNodeId = existingNodes.get(portKey).id;\n                        }\n\n                        newEdges.push({\n                            id: `edge_${ipNodeId}_${portNodeId}_${Date.now()}`,\n                            from: ipNodeId,\n                            to: portNodeId,\n                            label: 'Has port'\n                        });\n                        console.log(`Linked IP ${ipNodeId} to Port: ${portNodeId}`);\n\n                        // Service entity linked to port\n                        const serviceKey = `service:${serviceName.toLowerCase()}_${protocol.toUpperCase()}/${portNumber}`;\n                        let serviceNodeId;\n                        if (serviceName !== 'unknown' && !existingNodes.has(serviceKey)) {\n                            serviceNodeId = nextId++;\n                            newNodes.push({\n                                id: serviceNodeId,\n                                type: 'service',\n                                label: `Service: ${serviceName}`,\n                                title: `Service\\nName: ${serviceName}${product ? `\\nProduct: ${product}` : ''}${version ? `\\nVersion: ${version}` : ''}${extrainfo ? `\\nExtra Info: ${extrainfo}` : ''}`,\n                                color: { background: '#ec4899' },\n                                serviceName: serviceName,\n                                portType: protocol.toUpperCase(),\n                                portNumber: portNumber,\n                                size: 15\n                            });\n                            existingNodes.set(serviceKey, { id: serviceNodeId });\n                            newEdges.push({\n                                id: `edge_${portNodeId}_${serviceNodeId}_${Date.now()}`,\n                                from: portNodeId,\n                                to: serviceNodeId,\n                                label: 'Runs'\n                            });\n                            console.log(`Added Service node: ${serviceName}, ID: ${serviceNodeId}, Edge to Port: ${portNodeId}`);\n                        } else if (serviceName !== 'unknown') {\n                            serviceNodeId = existingNodes.get(serviceKey).id;\n                            if (!newEdges.some(e => e.from === portNodeId && e.to === serviceNodeId)) {\n                                newEdges.push({\n                                    id: `edge_${portNodeId}_${serviceNodeId}_${Date.now()}`,\n                                    from: portNodeId,\n                                    to: serviceNodeId,\n                                    label: 'Runs'\n                                });\n                                console.log(`Linked Port ${portNodeId} to existing Service: ${serviceNodeId}`);\n                            }\n                        }\n\n                        // CPE entities from service\n                        const cpes = service ? service.getElementsByTagName('cpe') : [];\n                        for (const cpe of cpes) {\n                            const cpeValue = cpe.textContent;\n                            if (cpeValue && !existingNodes.has(`cpe:${cpeValue}`)) {\n                                const cpeNodeId = nextId++;\n                                newNodes.push({\n                                    id: cpeNodeId,\n                                    type: 'cpe',\n                                    label: `CPE: ${cpeValue.split(':').slice(2).join(':')}`,\n                                    title: `CPE: ${cpeValue}`,\n                                    color: { background: '#f59e0b' },\n                                    cpe: cpeValue,\n                                    size: 12\n                                });\n                                existingNodes.set(`cpe:${cpeValue}`, { id: cpeNodeId });\n                                newEdges.push({\n                                    id: `edge_${serviceNodeId || portNodeId}_${cpeNodeId}_${Date.now()}`,\n                                    from: serviceNodeId || portNodeId,\n                                    to: cpeNodeId,\n                                    label: 'Identifies'\n                                });\n                                console.log(`Added CPE node: ${cpeValue}, ID: ${cpeNodeId}, Edge to ${serviceNodeId ? 'Service' : 'Port'}: ${serviceNodeId || portNodeId}`);\n                            }\n                        }\n\n                        // Extract certificate domains (SANs) from ssl-cert script\n                        const sslCertScript = port.querySelector('script[id=\"ssl-cert\"]');\n                        if (sslCertScript) {\n                            const output = sslCertScript.getAttribute('output') || '';\n                            // Match SANs like \"DNS:example.com\" or \"DNS:*.example.com\"\n                            const sanMatches = output.match(/DNS:[^\\s,]+/g) || [];\n                            for (const san of sanMatches) {\n                                const domain = san.replace('DNS:', '').trim();\n                                if (domain && !existingNodes.has(`domain:${domain.toLowerCase()}`)) {\n                                    const domainNodeId = nextId++;\n                                    newNodes.push({\n                                        id: domainNodeId,\n                                        type: 'domain',\n                                        label: `Domain: ${domain}`,\n                                        title: `Domain: ${domain}\\nSource: SSL Certificate SAN`,\n                                        color: { background: '#60a5fa' },\n                                        domain: domain,\n                                        size: 20\n                                    });\n                                    existingNodes.set(`domain:${domain.toLowerCase()}`, { id: domainNodeId });\n                                    newEdges.push({\n                                        id: `edge_${ipNodeId}_${domainNodeId}_${Date.now()}`,\n                                        from: ipNodeId,\n                                        to: domainNodeId,\n                                        label: 'Certificate Domain'\n                                    });\n                                    console.log(`Added SAN Domain node: ${domain}, ID: ${domainNodeId}, Edge to IP: ${ipNodeId}`);\n                                } else if (domain) {\n                                    const existingDomainNode = existingNodes.get(`domain:${domain.toLowerCase()}`);\n                                    if (!newEdges.some(e => e.from === ipNodeId && e.to === existingDomainNode.id && e.label === 'Certificate Domain')) {\n                                        newEdges.push({\n                                            id: `edge_${ipNodeId}_${existingDomainNode.id}_${Date.now()}`,\n                                            from: ipNodeId,\n                                            to: existingDomainNode.id,\n                                            label: 'Certificate Domain'\n                                        });\n                                        console.log(`Linked IP ${ipNodeId} to existing SAN Domain: ${existingDomainNode.id}`);\n                                    }\n                                }\n                            }\n                        }\n                    }\n\n                    // Extract OS\n                    const osMatch = host.querySelector('osmatch');\n                    if (osMatch) {\n                        const osName = osMatch.getAttribute('name');\n                        const osKey = `os:${osName}`;\n                        let osNodeId;\n                        if (!existingNodes.has(osKey)) {\n                            osNodeId = nextId++;\n                            newNodes.push({\n                                id: osNodeId,\n                                type: 'os',\n                                label: `OS: ${osName}`,\n                                title: `Operating System: ${osName}\\nAccuracy: ${osMatch.getAttribute('accuracy')}%`,\n                                color: { background: '#10b981' },\n                                os: osName,\n                                size: 15\n                            });\n                            existingNodes.set(osKey, { id: osNodeId });\n                            newEdges.push({\n                                id: `edge_${ipNodeId}_${osNodeId}_${Date.now()}`,\n                                from: ipNodeId,\n                                to: osNodeId,\n                                label: 'Runs'\n                            });\n                            console.log(`Added OS node: ${osName}, ID: ${osNodeId}, Edge to IP: ${ipNodeId}`);\n                        } else {\n                            osNodeId = existingNodes.get(osKey).id;\n                            if (!newEdges.some(e => e.from === ipNodeId && e.to === osNodeId)) {\n                                newEdges.push({\n                                    id: `edge_${ipNodeId}_${osNodeId}_${Date.now()}`,\n                                    from: ipNodeId,\n                                    to: osNodeId,\n                                    label: 'Runs'\n                                });\n                                console.log(`Linked IP ${ipNodeId} to existing OS: ${osNodeId}`);\n                            }\n                        }\n\n                        // CPE entities from OS\n                        const osCpes = host.querySelectorAll('osclass > cpe');\n                        for (const cpe of osCpes) {\n                            const cpeValue = cpe.textContent;\n                            if (cpeValue && !existingNodes.has(`cpe:${cpeValue}`)) {\n                                const cpeNodeId = nextId++;\n                                newNodes.push({\n                                    id: cpeNodeId,\n                                    type: 'cpe',\n                                    label: `CPE: ${cpeValue.split(':').slice(2).join(':')}`,\n                                    title: `CPE: ${cpeValue}`,\n                                    color: { background: '#f59e0b' },\n                                    cpe: cpeValue,\n                                    size: 12\n                                });\n                                existingNodes.set(`cpe:${cpeValue}`, { id: cpeNodeId });\n                                newEdges.push({\n                                    id: `edge_${osNodeId}_${cpeNodeId}_${Date.now()}`,\n                                    from: osNodeId,\n                                    to: cpeNodeId,\n                                    label: 'Identifies'\n                                });\n                                console.log(`Added CPE node: ${cpeValue}, ID: ${cpeNodeId}, Edge to OS: ${osNodeId}`);\n                            }\n                        }\n                    }\n\n                    // Extract trace hops\n                    const trace = host.querySelector('trace');\n                    if (trace) {\n                        const hops = trace.getElementsByTagName('hop');\n                        for (const hop of hops) {\n                            const hopIp = hop.getAttribute('ipaddr');\n                            if (hopIp && !existingNodes.has(`hop:${hopIp}`)) {\n                                const hopNodeId = nextId++;\n                                newNodes.push({\n                                    id: hopNodeId,\n                                    type: 'hop',\n                                    label: `Hop: ${hopIp}`,\n                                    title: `Trace Hop\\nIP: ${hopIp}\\nTTL: ${hop.getAttribute('ttl')}\\nRTT: ${hop.getAttribute('rtt') || 'unknown'} ms\\nHost: ${hop.getAttribute('host') || 'unknown'}`,\n                                    color: { background: '#14b8a6' },\n                                    ipaddr: hopIp,\n                                    size: 12\n                                });\n                                existingNodes.set(`hop:${hopIp}`, { id: hopNodeId });\n                                newEdges.push({\n                                    id: `edge_${ipNodeId}_${hopNodeId}_${Date.now()}`,\n                                    from: ipNodeId,\n                                    to: hopNodeId,\n                                    label: 'Reachable via'\n                                });\n                                console.log(`Added Hop node: ${hopIp}, ID: ${hopNodeId}, Edge to IP: ${ipNodeId}`);\n                            }\n                        }\n                    }\n\n                    processedCount++;\n                    if (processedCount % batchSize === 0) {\n                        nodes.add(newNodes);\n                        edges.add(newEdges);\n                        newNodes.length = 0;\n                        newEdges.length = 0;\n                        await new Promise(resolve => setTimeout(resolve, 0));\n                    }\n                }\n\n                // Add remaining items\n                if (newNodes.length > 0) {\n                    nodes.add(newNodes);\n                    edges.add(newEdges);\n                }\n\n                updateNodeSizes();\n                updateSelectOptions();\n                await stabilizeNetwork().catch(err => console.error('Error stabilizing network:', err));\n                showToast(`Imported ${hosts.length} hosts from Nmap XML`, 'success');\n            } catch (error) {\n                console.error('Error importing Nmap XML:', error);\n                showToast(`Failed to import Nmap XML: ${error.message}`, 'error');\n            }\n        };\n        reader.readAsText(file);\n    };\n\n    // Trigger file selection\n    input.click();\n}\n\n// Risk analysis\n\nfunction riskAnalysis() {\n    const riskyPorts = {\n        'TCP/3389': 'RDP (Remote Desktop Protocol)',\n        'TCP/5985': 'WinRM (Windows Remote Management)',\n        'TCP/5986': 'WinRM (Windows Remote Management HTTPS)',\n        'TCP/1433': 'MSSQL (Microsoft SQL Server)',\n        'TCP/21': 'FTP (File Transfer Protocol)',\n        'TCP/389': 'LDAP (Lightweight Directory Access Protocol)',\n        'TCP/445': 'SMB (Server Message Block)',\n        'TCP/443': 'HTTPS (Hypertext Transfer Protocol Secure)',\n        'TCP/80': 'HTTP (Hypertext Transfer Protocol)',\n        'TCP/53': 'DNS (Domain Name System)'\n    };\n\n    const risks = [];\n    const ipNodes = new Map();\n\n    // Collect IP nodes\n    nodes.forEach(node => {\n        if ((node.type || '').toLowerCase() === 'ip' && node.ip) {\n            ipNodes.set(node.ip, { id: node.id, domains: new Set(), riskyPorts: new Set() });\n        }\n    });\n\n    // Link domains to IPs\n    edges.forEach(edge => {\n        const label = (edge.label || '').toLowerCase();\n        if (label === 'resolves to' || label === 'certificate domain') {\n            const fromNode = nodes.get(edge.from);\n            const toNode = nodes.get(edge.to);\n            if (fromNode?.type.toLowerCase() === 'ip' && toNode?.type.toLowerCase() === 'domain' && fromNode.ip && toNode.domain) {\n                ipNodes.get(fromNode.ip).domains.add(toNode.domain);\n            }\n        }\n    });\n\n    // Identify risky ports\n    nodes.forEach(node => {\n        if ((node.type || '').toLowerCase() === 'port') {\n            const portType = (node.portType || '').toUpperCase();\n            const portNumber = String(node.portNumber || '');\n            const portKey = `${portType}/${portNumber}`;\n            let state = (node.state || '').toLowerCase();\n            if (!state && node.label) {\n                const stateMatch = node.label.match(/\\((open|closed|filtered)\\)/i);\n                if (stateMatch) state = stateMatch[1].toLowerCase();\n            }\n\n            if (riskyPorts[portKey] && state === 'open') {\n                edges.forEach(edge => {\n                    const edgeLabel = (edge.label || '').toLowerCase();\n                    if (edge.to === node.id && edgeLabel === 'has port') {\n                        const ipNode = nodes.get(edge.from);\n                        if (ipNode?.type.toLowerCase() === 'ip' && ipNode.ip) {\n                            ipNodes.get(ipNode.ip).riskyPorts.add(portKey);\n                        }\n                    }\n                });\n            }\n        }\n    });\n\n    // Build risks\n    ipNodes.forEach((data, ip) => {\n        if (data.riskyPorts.size > 0) {\n            risks.push({\n                ip: ip,\n                domains: Array.from(data.domains),\n                riskyPorts: Array.from(data.riskyPorts).map(port => ({\n                    port: port,\n                    name: riskyPorts[port]\n                }))\n            });\n        }\n    });\n\n    // Display results\n    const modal = document.getElementById('riskModal');\n    if (risks.length === 0) {\n        modal.style.display = 'none';\n        showToast('No risky elements found in the graph', 'info');\n        return;\n    }\n\n    const tableContainer = document.getElementById('riskTableContainer');\n    let tableHtml = `\n        <table>\n            <thead>\n                <tr>\n                    <th>IP Address</th>\n                    <th>Domain Names</th>\n                    <th>Risky Ports</th>\n                </tr>\n            </thead>\n            <tbody>\n    `;\n\n    risks.forEach(risk => {\n        tableHtml += `\n            <tr>\n                <td>${risk.ip}</td>\n                <td>${risk.domains.length > 0 ? risk.domains.join(', ') : 'None'}</td>\n                <td>${risk.riskyPorts.map(p => `${p.name} (${p.port})`).join(', ')}</td>\n            </tr>\n        `;\n    });\n\n    tableHtml += `\n            </tbody>\n        </table>\n        <button class=\"print-button\" onclick=\"printRiskTableToPDF()\">Print to PDF</button>\n        <button class=\"export-button\" onclick=\"exportToExcel()\">Export to Excel</button>\n    `;\n\n    tableContainer.innerHTML = tableHtml;\n    modal.style.display = 'block';\n\n    // Modal close functionality\n    const closeModal = modal.querySelector('.close-modal');\n    closeModal.onclick = () => modal.style.display = 'none';\n    modal.onclick = (e) => {\n        if (e.target === modal) modal.style.display = 'none';\n    };\n}\n\n\n// Add event listener to close modal when clicking outside\nwindow.onclick = function(event) {\n    const modal = document.getElementById('riskModal');\n    if (event.target === modal) {\n        modal.style.display = 'none';\n    }\n};\n\n\n\n// Save Risk Assessment to PDF\nfunction printRiskTableToPDF() {\n    const { jsPDF } = window.jspdf;\n    const doc = new jsPDF();\n    const table = document.querySelector('#riskTableContainer table');\n\n    if (!table) {\n        showToast('No risk table found to print', 'error');\n        return;\n    }\n\n    // Add title\n    doc.setFontSize(16);\n    doc.text('Risk Analysis Report', 10, 10);\n\n    // Convert table to PDF with light mode styling\n    doc.autoTable({\n        html: table,\n        startY: 20,\n        styles: {\n            fillColor: [255, 255, 255], // White background for all cells\n            textColor: [31, 42, 68],    // Dark text (#1f2a44)\n            lineColor: [209, 213, 219], // Light gray border (#d1d5db)\n            lineWidth: 0.1\n        },\n        headStyles: {\n            fillColor: [241, 245, 249], // Light gray header (#f1f5f9)\n            textColor: [31, 42, 68]     // Dark text for header\n        },\n        alternateRowStyles: {\n            fillColor: [255, 255, 255]  // No striping, all rows white\n        }\n    });\n\n    // Save the PDF\n    doc.save('risk_analysis.pdf');\n}\n\nfunction exportToExcel() {\n    // Check if XLSX is available\n    if (typeof XLSX === 'undefined') {\n        showToast('Excel export library not loaded', 'error');\n        return;\n    }\n\n    const table = document.querySelector('#riskTableContainer table');\n    if (!table) {\n        showToast('No risk table found to export', 'error');\n        return;\n    }\n\n    // Prepare data for Excel\n    const data = [];\n    const headers = ['IP Address', 'Domain Names', 'Risky Ports'];\n    data.push(headers);\n\n    const rows = table.querySelectorAll('tbody tr');\n    rows.forEach(row => {\n        const cells = row.querySelectorAll('td');\n        const rowData = [\n            cells[0].textContent, // IP Address\n            cells[1].textContent, // Domain Names\n            cells[2].textContent  // Risky Ports\n        ];\n        data.push(rowData);\n    });\n\n    // Create workbook and worksheet\n    const ws = XLSX.utils.aoa_to_sheet(data); // Array of arrays to sheet\n    const wb = XLSX.utils.book_new();\n    XLSX.utils.book_append_sheet(wb, ws, 'Risk Analysis');\n\n    // Style headers (optional, basic bolding)\n    ws['!cols'] = [{ wch: 15 }, { wch: 30 }, { wch: 50 }]; // Set column widths\n    for (let i = 0; i < headers.length; i++) {\n        const cellRef = XLSX.utils.encode_cell({ r: 0, c: i });\n        ws[cellRef].s = { font: { bold: true } };\n    }\n\n    // Export to file\n    XLSX.writeFile(wb, 'risk_analysis.xlsx');\n}\n\n\n\n//GOOGLE DNS\n\nconst throttledEnrichGoogleDNS = throttleRequest(async function enrichGoogleDNS(domain, domainNodeId, isBulk = false, signal) {\n    if (!isBulk) network.setOptions({ physics: { enabled: false } });\n\n    // ── Validation & cleaning ─────────────────────────────\n    if (!domain || typeof domain !== 'string' || domain.trim() === '') {\n        if (!isBulk) showToast('Skipping: empty or invalid domain', 'warning');\n        return;\n    }\n    const cleanDomain = domain.trim();\n\n    try {\n        // ── Direct URL (bypasses constructUrl/proxy) ───────\n        const url = `https://dns.google/resolve?name=${encodeURIComponent(cleanDomain)}&type=A`;\n        console.log(`Fetching A records for ${cleanDomain}: ${url}`);\n\n        const response = await fetch(url, { signal });\n        if (!response.ok) {\n            const errorText = await response.text();\n            throw new Error(`Failed to fetch DNS data: ${response.status} ${response.statusText} - ${errorText}`);\n        }\n\n        const data = await response.json();\n\n        if (data.Status !== 0) {\n            throw new Error(`Google DNS error: Status ${data.Status}${data.Comment ? ` (${data.Comment})` : ''}`);\n        }\n\n        if (data.Answer) {\n            data.Answer.forEach(answer => {\n                if (answer.type === 1) { // Type 1 is A record (IPv4)\n                    const ip = answer.data.trim();\n                    const existingIP = nodes.get({ filter: n => n.type === 'ip' && n.ip === ip })[0];\n                    const ipId = existingIP ? existingIP.id : nextId++;\n\n                    if (!existingIP) {\n                        nodes.add({\n                            id: ipId,\n                            type: 'ip',\n                            label: `IP: ${ip}`,\n                            title: `IP Address: ${ip}`,\n                            color: { background: '#f87171' },\n                            ip: ip,\n                            size: 20\n                        });\n                    }\n\n                    const edgeId = `${domainNodeId}-${ipId}-ResolvesTo`;\n                    if (!edges.get(edgeId)) {\n                        edges.add({ id: edgeId, from: domainNodeId, to: ipId, label: 'Resolves to' });\n                    }\n                }\n            });\n        } else {\n            if (!isBulk) showToast(`No A records found for ${cleanDomain}`, 'info');\n        }\n\n        updateNodeSizes();\n        updateSelectOptions();\n\n        if (!isBulk) {\n            await stabilizeNetwork();\n            showToast(`Domain ${cleanDomain} enrichment completed using Google DNS`, 'success');\n        }\n    } catch (error) {\n        if (error.name === 'AbortError') {\n            showToast(`A record enrichment of domain ${cleanDomain} cancelled`, 'info');\n            return;\n        }\n        console.error(`Error enriching domain ${cleanDomain} with Google DNS: ${error.message}`);\n        showToast(`Error enriching domain ${cleanDomain} with Google DNS: ${error.message}`, 'error');\n        if (!isBulk) await stabilizeNetwork();\n    }\n}, RATE_LIMIT_MS);\n\n// Multiple Google DNS Node Enrichment\n\nconst throttledEnrichGoogleDNSMultiple = throttleRequest(async function enrichGoogleDNSMultiple(domains, nodeIds) {\n    if (!Array.isArray(domains) || !Array.isArray(nodeIds) || domains.length !== nodeIds.length) {\n        showToast('Invalid input for multiple domain enrichment', 'error');\n        return;\n    }\n\n    for (let i = 0; i < domains.length; i++) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('Google DNS enrichment stopped', 'info');\n            break;\n        }\n        await throttledEnrichGoogleDNS(domains[i], nodeIds[i], true); // isBulk = true\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    showToast(`Enriched ${domains.length} domains with Google DNS`, 'success');\n}, RATE_LIMIT_MS);\n\n\n\n// Google Bulk Enrichment\n\nasync function enrichAllGoogleDNS() {\n            network.setOptions({ physics: { enabled: false } });\n            const domainNodes = nodes.get({ filter: n => n.type === 'domain' });\n            for (const node of domainNodes) await throttledEnrichGoogleDNS(node.domain, node.id, true);\n            await stabilizeNetwork();\n            showToast('Bulk Google DNS enrichment completed', 'success');\n        }\n\n\n// GOOGLE MX and TXT Enrichment\n\n// Single MX Record Enrichment\nconst throttledEnrichGoogleDNSMX = throttleRequest(async function enrichGoogleDNSMX(domain, domainNodeId, isBulk = false, signal) {\n    if (!isBulk) network.setOptions({ physics: { enabled: false } });\n    try {\n        const baseUrl = `https://dns.google/resolve?name=${encodeURIComponent(domain)}&type=MX`;\n        const url = constructUrl(baseUrl);\n        console.log(`Fetching MX records for ${domain}: ${url}`); // Debug log\n        const response = await fetch(url, { signal });\n        if (!response.ok) {\n            const errorText = await response.text();\n            throw new Error(`Failed to fetch MX DNS data: ${response.status} ${response.statusText} - ${errorText}`);\n        }\n        const data = await response.json();\n        \n        if (data.Status !== 0) {\n            throw new Error(`Google DNS error: Status ${data.Status}${data.Comment ? ` (${data.Comment})` : ''}`);\n        }\n        \n        if (data.Answer) {\n            const existingMXNodes = new Map(nodes.get({ filter: n => n.type === 'mx' && n.hostname }).map(n => [n.hostname.toLowerCase(), n.id]));\n            data.Answer.forEach(answer => {\n                if (answer.type === 15) { // Type 15 is MX record\n                    const [priority, hostname] = answer.data.trim().split(' ');\n                    if (hostname && hostname !== '.') {\n                        let mxId = existingMXNodes.get(hostname.toLowerCase());\n                        if (!mxId) {\n                            mxId = nextId++;\n                            nodes.add({\n                                id: mxId,\n                                type: 'domain',\n                                label: `MX: ${hostname.length > 30 ? hostname.substring(0, 27) + '...' : hostname}`,\n                                title: `Mail Exchanger\\nHostname: ${hostname}\\nPriority: ${priority}`,\n                                color: { background: '#1e88e5' },\n                                hostname: hostname,\n                                size: 15\n                            });\n                            existingMXNodes.set(hostname.toLowerCase(), mxId);\n                        }\n                        const edgeId = `${domainNodeId}-${mxId}-MXFor`;\n                        if (!edges.get(edgeId)) {\n                            edges.add({ id: edgeId, from: domainNodeId, to: mxId, label: 'MX for' });\n                        }\n                    }\n                }\n            });\n        } else {\n            if (!isBulk) showToast(`No MX records found for ${domain}`, 'info');\n        }\n        \n        updateNodeSizes();\n        updateSelectOptions();\n        if (!isBulk) {\n            await stabilizeNetwork();\n            showToast(`Domain ${domain} MX enrichment completed using Google DNS`, 'success');\n        }\n    } catch (error) {\n        if (error.name === 'AbortError') {\n            showToast(`MX enrichment of domain ${domain} cancelled`, 'info');\n            return;\n        }\n        console.error(`Error enriching domain ${domain} MX with Google DNS: ${error.message}`);\n        showToast(`Error enriching domain ${domain} MX: ${error.message}`, 'error');\n        if (!isBulk) await stabilizeNetwork();\n    }\n}, RATE_LIMIT_MS);\n\n// Multiple MX Record Enrichment\nconst throttledEnrichGoogleDNSMXMultiple = throttleRequest(async function enrichGoogleDNSMXMultiple(domains, nodeIds) {\n    if (!Array.isArray(domains) || !Array.isArray(nodeIds) || domains.length !== nodeIds.length) {\n        showToast('Invalid input for multiple MX enrichment', 'error');\n        return;\n    }\n\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const totalDomains = domains.length;\n    let successfulEnrichments = 0;\n\n    for (let i = 0; i < totalDomains; i++) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('Google DNS MX enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = `Google DNS MX Enrichment: Stopped at ${successfulEnrichments}/${totalDomains} Domains`;\n            break;\n        }\n        await throttledEnrichGoogleDNSMX(domains[i], nodeIds[i], true);\n        successfulEnrichments++;\n        const progress = ((successfulEnrichments / totalDomains) * 100).toFixed(1);\n        document.getElementById('progress-bar').textContent = `Google DNS MX Enrichment: ${successfulEnrichments}/${totalDomains} Domains (${progress}%)`;\n        await new Promise(resolve => setTimeout(resolve, 200)); // Small delay\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    completeProgressBar();\n    showToast(`Enriched ${successfulEnrichments}/${totalDomains} domains with Google DNS MX records`, 'success');\n}, RATE_LIMIT_MS);\n\n// Bulk MX Enrichment\nasync function enrichAllGoogleDNSMX() {\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const domainNodes = nodes.get({ filter: n => n.type === 'domain' && n.domain });\n    const totalDomains = domainNodes.length;\n    let successfulEnrichments = 0;\n\n    if (totalDomains === 0) {\n        showToast('No domain nodes found to enrich', 'info');\n        completeProgressBar();\n        return;\n    }\n\n    const batchSize = 50;\n    const delayBetweenBatches = 200;\n    const totalBatches = Math.ceil(totalDomains / batchSize);\n    const estimatedTimeMs = (totalDomains * RATE_LIMIT_MS) + (totalBatches - 1) * delayBetweenBatches;\n    const estimatedSeconds = Math.ceil(estimatedTimeMs / 1000);\n    const timeEstimateStr = estimatedSeconds > 60 \n        ? `${Math.floor(estimatedSeconds / 60)}m ${estimatedSeconds % 60}s` \n        : `${estimatedSeconds}s`;\n\n    document.getElementById('progress-bar').textContent = `Google DNS MX Enrichment: 0/${totalDomains} Domains (0%) - Est. ${timeEstimateStr}`;\n\n    for (let i = 0; i < totalDomains; i += batchSize) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('Google DNS MX enrichment stopped', 'info');\n            break;\n        }\n\n        const batch = domainNodes.slice(i, Math.min(i + batchSize, totalDomains));\n        const batchPromises = batch.map(node => \n            throttledEnrichGoogleDNSMX(node.domain, node.id, true)\n                .then(() => successfulEnrichments++)\n                .catch(error => console.error(`Failed to enrich ${node.domain}: ${error.message}`))\n        );\n\n        await Promise.all(batchPromises);\n\n        const progress = ((successfulEnrichments / totalDomains) * 100).toFixed(1);\n        document.getElementById('progress-bar').textContent = \n            `Google DNS MX Enrichment: ${successfulEnrichments}/${totalDomains} Domains (${progress}%)`;\n        \n        await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    completeProgressBar();\n    showToast(`Google DNS MX enrichment completed: ${successfulEnrichments}/${totalDomains} domains enriched`, 'success');\n}\n\n// Single TXT Record Enrichment\n\nconst throttledEnrichGoogleDNSTXT = throttleRequest(async function enrichGoogleDNSTXT(domain, domainNodeId, isBulk = false, signal) {\n    if (!isBulk) network.setOptions({ physics: { enabled: false } });\n    try {\n        const baseUrl = `https://dns.google/resolve?name=${encodeURIComponent(domain)}&type=TXT`;\n        const url = constructUrl(baseUrl);\n        console.log(`Fetching TXT records for ${domain}: ${url}`);\n        const response = await fetch(url, { signal });\n        if (!response.ok) {\n            const errorText = await response.text();\n            throw new Error(`Failed to fetch TXT DNS data: ${response.status} ${response.statusText} - ${errorText}`);\n        }\n        const data = await response.json();\n        \n        if (data.Status !== 0) {\n            throw new Error(`Google DNS error: Status ${data.Status}${data.Comment ? ` (${data.Comment})` : ''}`);\n        }\n        \n        if (data.Answer) {\n            let foundTXT = false;\n            const existingTXTNodes = new Map(nodes.get({ filter: n => n.type === 'txt' && n.text }).map(n => [n.text, n.id]));\n            data.Answer.forEach(answer => {\n                if (answer.type === 16) { // Type 16 is TXT record\n                    foundTXT = true;\n                    const text = answer.data.replace(/^\"|\"$/g, '').trim();\n                    if (text) {\n                        let txtId = existingTXTNodes.get(text);\n                        if (!txtId) {\n                            txtId = nextId++;\n                            nodes.add({\n                                id: txtId,\n                                type: 'txt',\n                                label: `TXT: ${text.length > 30 ? text.substring(0, 27) + '...' : text}`,\n                                title: `TXT Record\\nValue: ${text}`,\n                                color: { background: '#f59e0b' },\n                                text: text,\n                                size: 15\n                            });\n                            existingTXTNodes.set(text, txtId);\n                        }\n                        const edgeId = `${domainNodeId}-${txtId}-TXTFor`;\n                        if (!edges.get(edgeId)) {\n                            edges.add({ id: edgeId, from: domainNodeId, to: txtId, label: 'TXT for' });\n                        }\n                    }\n                }\n            });\n            if (!foundTXT && !isBulk) {\n                showToast(`No TXT records found for ${domain}; received unexpected record types`, 'warning');\n            }\n        } else {\n            if (!isBulk) showToast(`No TXT records found for ${domain}`, 'info');\n        }\n        \n        updateNodeSizes();\n        updateSelectOptions();\n        if (!isBulk) {\n            await stabilizeNetwork();\n            showToast(`Domain ${domain} TXT enrichment completed using Google DNS`, 'success');\n        }\n    } catch (error) {\n        if (error.name === 'AbortError') {\n            showToast(`TXT enrichment of domain ${domain} cancelled`, 'info');\n            return;\n        }\n        console.error(`Error enriching domain ${domain} TXT with Google DNS: ${error.message}`);\n        showToast(`Error enriching domain ${domain} TXT: ${error.message}`, 'error');\n        if (!isBulk) await stabilizeNetwork();\n    }\n}, RATE_LIMIT_MS);\n\n// Multiple TXT Record Enrichment\nconst throttledEnrichGoogleDNSTXTMultiple = throttleRequest(async function enrichGoogleDNSTXTMultiple(domains, nodeIds) {\n    if (!Array.isArray(domains) || !Array.isArray(nodeIds) || domains.length !== nodeIds.length) {\n        showToast('Invalid input for multiple TXT enrichment', 'error');\n        return;\n    }\n\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const totalDomains = domains.length;\n    let successfulEnrichments = 0;\n\n    for (let i = 0; i < totalDomains; i++) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('Google DNS TXT enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = `Google DNS TXT Enrichment: Stopped at ${successfulEnrichments}/${totalDomains} Domains`;\n            break;\n        }\n        await throttledEnrichGoogleDNSTXT(domains[i], nodeIds[i], true);\n        successfulEnrichments++;\n        const progress = ((successfulEnrichments / totalDomains) * 100).toFixed(1);\n        document.getElementById('progress-bar').textContent = `Google DNS TXT Enrichment: ${successfulEnrichments}/${totalDomains} Domains (${progress}%)`;\n        await new Promise(resolve => setTimeout(resolve, 200)); // Small delay\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    completeProgressBar();\n    showToast(`Enriched ${successfulEnrichments}/${totalDomains} domains with Google DNS TXT records`, 'success');\n}, RATE_LIMIT_MS);\n\n// Bulk TXT Enrichment\nasync function enrichAllGoogleDNSTXT() {\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const domainNodes = nodes.get({ filter: n => n.type === 'domain' && n.domain });\n    const totalDomains = domainNodes.length;\n    let successfulEnrichments = 0;\n\n    if (totalDomains === 0) {\n        showToast('No domain nodes found to enrich', 'info');\n        completeProgressBar();\n        return;\n    }\n\n    const batchSize = 50;\n    const delayBetweenBatches = 200;\n    const totalBatches = Math.ceil(totalDomains / batchSize);\n    const estimatedTimeMs = (totalDomains * RATE_LIMIT_MS) + (totalBatches - 1) * delayBetweenBatches;\n    const estimatedSeconds = Math.ceil(estimatedTimeMs / 1000);\n    const timeEstimateStr = estimatedSeconds > 60 \n        ? `${Math.floor(estimatedSeconds / 60)}m ${estimatedSeconds % 60}s` \n        : `${estimatedSeconds}s`;\n\n    document.getElementById('progress-bar').textContent = `Google DNS TXT Enrichment: 0/${totalDomains} Domains (0%) - Est. ${timeEstimateStr}`;\n\n    for (let i = 0; i < totalDomains; i += batchSize) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('Google DNS TXT enrichment stopped', 'info');\n            break;\n        }\n\n        const batch = domainNodes.slice(i, Math.min(i + batchSize, totalDomains));\n        const batchPromises = batch.map(node => \n            throttledEnrichGoogleDNSTXT(node.domain, node.id, true)\n                .then(() => successfulEnrichments++)\n                .catch(error => console.error(`Failed to enrich ${node.domain}: ${error.message}`))\n        );\n\n        await Promise.all(batchPromises);\n\n        const progress = ((successfulEnrichments / totalDomains) * 100).toFixed(1);\n        document.getElementById('progress-bar').textContent = \n            `Google DNS TXT Enrichment: ${successfulEnrichments}/${totalDomains} Domains (${progress}%)`;\n        \n        await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    completeProgressBar();\n    showToast(`Google DNS TXT enrichment completed: ${successfulEnrichments}/${totalDomains} domains enriched`, 'success');\n}\n\n// Add this at the end of the script section\nwindow.addEventListener('beforeunload', function() {\n    try {\n        saveState();\n    } catch (e) {\n        console.error('Before unload save failed:', e);\n    }\n});\n\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "email_time_delta.html",
    "content": "<script type=\"text/javascript\">\n        var gk_isXlsx = false;\n        var gk_xlsxFileLookup = {};\n        var gk_fileData = {};\n        function loadFileData(filename) {\n        if (gk_isXlsx && gk_xlsxFileLookup[filename]) {\n            try {\n                var workbook = XLSX.read(gk_fileData[filename], { type: 'base64' });\n                var firstSheetName = workbook.SheetNames[0];\n                var worksheet = workbook.Sheets[firstSheetName];\n\n                // Convert sheet to JSON to filter blank rows\n                var jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, blankrows: false, defval: '' });\n                // Filter out blank rows (rows where all cells are empty, null, or undefined)\n                var filteredData = jsonData.filter(row =>\n                    row.some(cell => cell !== '' && cell !== null && cell !== undefined)\n                );\n\n                // Convert filtered JSON back to CSV\n                var csv = XLSX.utils.aoa_to_sheet(filteredData); // Create a new sheet from filtered array of arrays\n                csv = XLSX.utils.sheet_to_csv(csv, { header: 1 });\n                return csv;\n            } catch (e) {\n                console.error(e);\n                return \"\";\n            }\n        }\n        return gk_fileData[filename] || \"\";\n        }\n        </script><!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Email Time Delta Analyzer</title>\n    <style>\n        body {\n            font-family: 'Inter', sans-serif;\n            background-color: #f3f4f6;\n            min-height: 100vh;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            padding: 16px;\n            margin: 0;\n        }\n        .container {\n            background-color: #ffffff;\n            border-radius: 16px;\n            box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);\n            padding: 32px;\n            width: 100%;\n            max-width: 1024px;\n        }\n        .title {\n            font-size: 30px;\n            font-weight: 700;\n            color: #1f2937;\n            margin-bottom: 24px;\n        }\n        .label {\n            display: block;\n            font-size: 14px;\n            font-weight: 500;\n            color: #374151;\n            margin-bottom: 8px;\n        }\n        .textarea {\n            width: 100%;\n            height: 160px;\n            padding: 16px;\n            border: 1px solid #d1d5db;\n            border-radius: 8px;\n            resize: vertical;\n            font-size: 14px;\n            color: #374151;\n        }\n        .textarea:focus {\n            outline: none;\n            border-color: #3b82f6;\n            box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5);\n        }\n        .button {\n            width: 100%;\n            background-color: #2563eb;\n            color: #ffffff;\n            padding: 12px;\n            border-radius: 8px;\n            font-size: 16px;\n            font-weight: 600;\n            border: none;\n            cursor: pointer;\n            transition: background-color 0.2s;\n        }\n        .button:hover {\n            background-color: #1d4ed8;\n        }\n        .results {\n            margin-top: 32px;\n        }\n        .section-title {\n            font-size: 20px;\n            font-weight: 600;\n            color: #1f2937;\n            margin-bottom: 16px;\n        }\n        .error-message, .base64-message {\n            padding: 16px;\n            border-radius: 8px;\n            margin-bottom: 16px;\n        }\n        .error-message {\n            background-color: #fee2e2;\n            color: #b91c1c;\n        }\n        .base64-message {\n            background-color: #fef9c3;\n            color: #854d0e;\n        }\n        .hidden {\n            display: none;\n        }\n        .timeline {\n            position: relative;\n            padding-left: 32px;\n        }\n        .timeline-dot {\n            width: 12px;\n            height: 12px;\n            background-color: #3b82f6;\n            border-radius: 50%;\n            position: absolute;\n            left: -6px;\n            top: 50%;\n            transform: translateY(-50%);\n        }\n        .timeline-bar {\n            height: 8px;\n            background-color: #3b82f6;\n            border-radius: 4px;\n            margin-top: 8px;\n            transition: width 0.5s ease-in-out;\n        }\n        .negative-bar {\n            background-color: #ef4444; /* Red for negative deltas */\n        }\n        .stage {\n            margin-bottom: 24px;\n            position: relative;\n        }\n        .stage-title {\n            font-size: 18px;\n            font-weight: 500;\n            color: #374151;\n        }\n        .stage-text {\n            color: #6b7280;\n            font-size: 14px;\n        }\n        .decoded-body {\n            background-color: #f9fafb;\n            padding: 16px;\n            border-radius: 8px;\n            font-size: 14px;\n            color: #374151;\n            white-space: pre-wrap;\n            max-height: 300px;\n            overflow-y: auto;\n        }\n        .info-section {\n            text-align: center;\n            font-size: 10px;\n            color: #6b7280;\n            padding-top: 10px;\n            margin-bottom: 24px;\n        }\n        .info-section a {\n            color: #6b7280;\n            text-decoration: none;\n        }\n        .info-section a:hover {\n            text-decoration: underline;\n        }\n        .info-section img {\n            height: 60px;\n            width: 217px;\n            margin-bottom: 8px;\n        }\n        .info-section p {\n            margin: 4px 0;\n        }\n        .metadata-section {\n            margin-bottom: 24px;\n            padding: 16px;\n            background-color: #f9fafb;\n            border-radius: 8px;\n        }\n        .metadata-item {\n            margin: 8px 0;\n            font-size: 14px;\n            color: #374151;\n        }\n        .metadata-item strong {\n            color: #1f2937;\n            margin-right: 8px;\n        }\n    </style>\n</head>\n<body>\n    <div class=\"container\">\n        <h1 class=\"title\">Email Time Delta Analyzer</h1>\n        \n        <section class=\"info-section\">\n            <a href=\"https://www.buymeacoffee.com/mrr3b00t\" target=\"_blank\">\n                <img src=\"https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png\" alt=\"Buy Me A Coffee\">\n            </a>\n            <p>Copyright © Xservus Limited</p>\n            <p>Experimental - Validate all results manually and/or with another tool</p>\n            <p>Version 0.2</p>\n            <p><a href=\"https://www.pwndefend.com\">https://www.pwndefend.com</a></p>\n        </section>\n        \n        <div class=\"mb-6\">\n            <label for=\"emailInput\" class=\"label\">Paste Email Source</label>\n            <textarea\n                id=\"emailInput\"\n                class=\"textarea\"\n                placeholder=\"Paste the complete email source here...\"\n            ></textarea>\n        </div>\n\n        <button id=\"analyzeButton\" class=\"button\">Analyze Time Deltas</button>\n\n        <div id=\"results\" class=\"results hidden\">\n            <h2 class=\"section-title\">Email Metadata</h2>\n            <div class=\"metadata-section\">\n                <div class=\"metadata-item\"><strong>From:</strong> <span id=\"metadataFrom\"></span></div>\n                <div class=\"metadata-item\"><strong>To:</strong> <span id=\"metadataTo\"></span></div>\n                <div class=\"metadata-item\"><strong>CC:</strong> <span id=\"metadataCC\"></span></div>\n                <div class=\"metadata-item\"><strong>Date:</strong> <span id=\"metadataDate\"></span></div>\n                <div class=\"metadata-item\"><strong>Subject:</strong> <span id=\"metadataSubject\"></span></div>\n            </div>\n\n            <h2 class=\"section-title\">Header Analysis Results</h2>\n            <div id=\"errorMessage\" class=\"error-message hidden\"></div>\n            <div id=\"base64Message\" class=\"base64-message hidden\"></div>\n            <div class=\"timeline\">\n                <div id=\"creationStage\" class=\"stage\">\n                    <div class=\"timeline-dot\"></div>\n                    <h3 class=\"stage-title\">Email Created</h3>\n                    <p id=\"creationTime\" class=\"stage-text\"></p>\n                </div>\n\n                <div id=\"creationToSentStage\" class=\"stage\">\n                    <div class=\"timeline-dot\"></div>\n                    <h3 class=\"stage-title\">Time to Send</h3>\n                    <p id=\"creationToSent\" class=\"stage-text\"></p>\n                    <div id=\"creationToSentBar\" class=\"timeline-bar\"></div>\n                </div>\n\n                <div id=\"sentStage\" class=\"stage\">\n                    <div class=\"timeline-dot\"></div>\n                    <h3 class=\"stage-title\">Email Sent</h3>\n                    <p id=\"sentTime\" class=\"stage-text\"></p>\n                </div>\n\n                <div id=\"sentToDeliveredStage\" class=\"stage\">\n                    <div class=\"timeline-dot\"></div>\n                    <h3 class=\"stage-title\">Time to Deliver</h3>\n                    <p id=\"sentToDelivered\" class=\"stage-text\"></p>\n                    <div id=\"sentToDeliveredBar\" class=\"timeline-bar\"></div>\n                </div>\n\n                <div id=\"deliveredStage\" class=\"stage\">\n                    <div class=\"timeline-dot\"></div>\n                    <h3 class=\"stage-title\">Email Delivered</h3>\n                    <p id=\"deliveredTime\" class=\"stage-text\"></p>\n                </div>\n\n                <div id=\"headerTimestamps\" class=\"timeline\"></div>\n            </div>\n\n            <h2 class=\"section-title mt-8\">Body Analysis Results</h2>\n            <div id=\"bodyResults\" class=\"timeline\"></div>\n            \n            <h2 class=\"section-title mt-8\">Decoded Email Body</h2>\n            <div id=\"decodedBody\" class=\"decoded-body\"></div>\n        </div>\n    </div>\n\n    <script type=\"text/javascript\">\n        var gk_isXlsx = false;\n        var gk_xlsxFileLookup = {};\n        var gk_fileData = {};\n        function loadFileData(filename) {\n            if (gk_isXlsx && gk_xlsxFileLookup[filename]) {\n                try {\n                    var workbook = XLSX.read(gk_fileData[filename], { type: 'base64' });\n                    var firstSheetName = workbook.SheetNames[0];\n                    var worksheet = workbook.Sheets[firstSheetName];\n                    var jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, blankrows: false, defval: '' });\n                    var filteredData = jsonData.filter(row =>\n                        row.some(cell => cell !== '' && cell !== null && cell !== undefined)\n                    );\n                    var csv = XLSX.utils.aoa_to_sheet(filteredData);\n                    csv = XLSX.utils.sheet_to_csv(csv, { header: 1 });\n                    return csv;\n                } catch (e) {\n                    console.error(e);\n                    return \"\";\n                }\n            }\n            return gk_fileData[filename] || \"\";\n        }\n\n        function formatTimeDelta(seconds) {\n            if (seconds === null) return 'Not available';\n            const absSeconds = Math.abs(seconds);\n            let formatted;\n            if (absSeconds >= 86400) {\n                const days = absSeconds / 86400;\n                formatted = `${days.toFixed(2)} days`;\n            } else if (absSeconds >= 3600) {\n                const hours = absSeconds / 3600;\n                formatted = `${hours.toFixed(2)} hours`;\n            } else if (absSeconds >= 60) {\n                const minutes = absSeconds / 60;\n                formatted = `${minutes.toFixed(2)} minutes`;\n            } else {\n                formatted = `${absSeconds.toFixed(2)} seconds`;\n            }\n            return seconds < 0 ? `-${formatted}` : formatted;\n        }\n\n        function calculateBarWidth(seconds) {\n            if (!seconds) return '0%';\n            const maxSeconds = 300;\n            const percentage = Math.min((Math.abs(seconds) / maxSeconds) * 100, 100);\n            return `${percentage}%`;\n        }\n\n        function escapeHtml(unsafe) {\n            return unsafe\n                .replace(/&/g, \"&\")\n                .replace(/</g, \"<\")\n                .replace(/>/g, \">\")\n                .replace(/\"/g, \"&quot;\")\n                .replace(/'/g, \"&#39;\");\n        }\n\n        function parseEmailTimeDelta(emlSource) {\n            const result = {\n                creationToSent: null,\n                sentToDelivered: null,\n                error: null,\n                creationTime: null,\n                sentTime: null,\n                deliveredTime: null,\n                bodyTimestamps: [],\n                headerTimestamps: [],\n                base64DecodeError: null,\n                decodedBody: '',\n                from: null,\n                to: null,\n                cc: null,\n                date: null,\n                subject: null\n            };\n\n            try {\n                const parts = emlSource.split(/(?=(?:^|\\n)--[\\w\\-]+)/m);\n                let headersPart = parts[0];\n                let bodyParts = parts.slice(1);\n\n                if (bodyParts.length === 0) {\n                    const simpleSplit = emlSource.split('\\n\\n', 2);\n                    headersPart = simpleSplit[0];\n                    bodyParts = simpleSplit[1] ? [simpleSplit[1]] : [];\n                }\n\n                const headers = headersPart.split('\\n');\n                let dateHeader = null;\n                let receivedHeaders = [];\n                let createdTime = null;\n                const dateRegex = /\\w{3}, \\d{1,2} \\w{3} \\d{4} \\d{2}:\\d{2}:\\d{2} [+-]\\d{4}|\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:[+-]\\d{4}|Z)?|\\w{3} \\w{3} \\d{1,2} \\d{2}:\\d{2}:\\d{2} \\d{4}/;\n\n                headers.forEach((line, index) => {\n                    let fullLine = line.trim();\n                    let lineIndex = index;\n                    while (lineIndex + 1 < headers.length && headers[lineIndex + 1].match(/^\\s/)) {\n                        fullLine += ' ' + headers[++lineIndex].trim();\n                    }\n\n                    const colonIndex = fullLine.indexOf(':');\n                    if (colonIndex !== -1) {\n                        const headerName = fullLine.substring(0, colonIndex).trim();\n                        const headerValue = fullLine.substring(colonIndex + 1).trim();\n                        const dateMatch = headerValue.match(dateRegex);\n                        if (dateMatch) {\n                            const timestamp = new Date(headerValue);\n                            if (!isNaN(timestamp)) {\n                                result.headerTimestamps.push({\n                                    headerName,\n                                    timestamp: timestamp.toISOString(),\n                                    lineIndex\n                                });\n                            }\n                        }\n\n                        const lowerHeaderName = headerName.toLowerCase();\n                        if (lowerHeaderName === 'from') result.from = headerValue;\n                        if (lowerHeaderName === 'to') result.to = headerValue;\n                        if (lowerHeaderName === 'cc') result.cc = headerValue;\n                        if (lowerHeaderName === 'subject') result.subject = headerValue;\n                        if (lowerHeaderName === 'date') {\n                            result.date = headerValue;\n                            dateHeader = headerValue;\n                        }\n                        if (lowerHeaderName === 'received') {\n                            receivedHeaders.push(headerValue);\n                        }\n                        if (lowerHeaderName === 'x-ms-exchange-crosstenant-originalarrivaltime' ||\n                            lowerHeaderName === 'x-createdtime' ||\n                            lowerHeaderName === 'x-origination-time') {\n                            createdTime = headerValue;\n                        }\n                    }\n                });\n\n                result.headerTimestamps.sort((a, b) => a.lineIndex - b.lineIndex);\n\n                const sentDate = dateHeader ? new Date(dateHeader) : null;\n                const creationDate = createdTime ? new Date(createdTime) : sentDate;\n                let deliveryDate = null;\n                if (receivedHeaders.length > 0) {\n                    const lastReceived = receivedHeaders[0];\n                    const dateMatch = lastReceived.match(/;\\s*(.+)$/);\n                    if (dateMatch) {\n                        deliveryDate = new Date(dateMatch[1].trim());\n                    }\n                }\n\n                if (creationDate && !isNaN(creationDate)) {\n                    result.creationTime = creationDate.toISOString();\n                }\n                if (sentDate && !isNaN(sentDate)) {\n                    result.sentTime = sentDate.toISOString();\n                }\n                if (deliveryDate && !isNaN(deliveryDate)) {\n                    result.deliveredTime = deliveryDate.toISOString();\n                }\n\n                if (creationDate && sentDate && !isNaN(creationDate) && !isNaN(sentDate)) {\n                    result.creationToSent = (sentDate - creationDate) / 1000;\n                }\n                if (sentDate && deliveryDate && !isNaN(sentDate) && !isNaN(deliveryDate)) {\n                    result.sentToDelivered = (deliveryDate - sentDate) / 1000;\n                }\n\n                let decodedBody = '';\n                bodyParts.forEach((part, partIndex) => {\n                    const lines = part.split('\\n');\n                    let contentType = null;\n                    let contentTransferEncoding = null;\n                    let bodyContent = '';\n                    let inBody = false;\n\n                    for (let i = 0; i < lines.length; i++) {\n                        const line = lines[i];\n                        if (!inBody) {\n                            let fullLine = line.trim();\n                            while (i + 1 < lines.length && lines[i + 1].match(/^\\s/)) {\n                                fullLine += ' ' + lines[++i].trim();\n                            }\n\n                            if (fullLine.toLowerCase().startsWith('content-type:')) {\n                                contentType = fullLine.split(':')[1].trim().toLowerCase();\n                            }\n                            if (fullLine.toLowerCase().startsWith('content-transfer-encoding:')) {\n                                contentTransferEncoding = fullLine.split(':')[1].trim().toLowerCase();\n                            }\n                            if (fullLine === '') {\n                                inBody = true;\n                            }\n                        } else {\n                            bodyContent += line + '\\n';\n                        }\n                    }\n\n                    let decodedContent = bodyContent.trim();\n                    if (contentType && contentType.includes('text/plain; charset=utf-8') && contentTransferEncoding === 'base64') {\n                        console.log(`Found base64 encoded text/plain part with UTF-8 charset in part ${partIndex}`);\n                        console.log(`Attempting to decode base64 content in part ${partIndex}`);\n                        console.log(`Base64 content: ${bodyContent}`);\n                        try {\n                            const cleanBase64 = bodyContent.replace(/[\\r\\n]+/g, '').trim();\n                            decodedContent = atob(cleanBase64);\n                            console.log(`Decoded content: ${decodedContent}`);\n                        } catch (err) {\n                            console.log(`Failed to decode base64 content in part ${partIndex}:`, err.message);\n                            result.base64DecodeError = `Failed to decode base64 content in part ${partIndex}: ${err.message}`;\n                            decodedContent = '[Base64 decoding failed]';\n                        }\n                    } else if (contentType && contentTransferEncoding === 'base64' && contentType.includes('text/html')) {\n                        console.log(`Found base64 encoded text/html part in part ${partIndex}`);\n                        console.log(`Attempting to decode base64 content in part ${partIndex}`);\n                        console.log(`Base64 content: ${bodyContent}`);\n                        try {\n                            const cleanBase64 = bodyContent.replace(/[\\r\\n]+/g, '').trim();\n                            decodedContent = atob(cleanBase64);\n                            console.log(`Decoded content: ${decodedContent}`);\n                        } catch (err) {\n                            console.log(`Failed to decode base64 content in part ${partIndex}:`, err.message);\n                            result.base64DecodeError = `Failed to decode base64 content in part ${partIndex}: ${err.message}`;\n                            decodedContent = '[Base64 decoding failed]';\n                        }\n                    }\n\n                    if (contentType && (contentType.includes('text/plain') || contentType.includes('text/html'))) {\n                        decodedBody += decodedContent + '\\n';\n                    }\n                });\n\n                result.decodedBody = decodedBody.trim() || 'No body content available';\n                console.log('Full decoded body for timestamp parsing:', decodedBody);\n\n                const bodyLines = decodedBody.split('\\n').filter(line => line.trim());\n                console.log('Body lines:', bodyLines);\n                const bodyDateRegex = /(?:(?:<b>Sent:<\\/b>\\s*)?(\\d{1,2}\\s+(?:January|February|March|April|May|June|July|August|September|October|November|December)\\s+\\d{4}\\s+\\d{1,2}:\\d{2}(?::\\d{2})?(?:\\s*(?:AM|PM|am|pm))?(?:<br>)?)|(\\d{4}-\\d{2}-\\d{2}[ T]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:[+-]\\d{4}|Z)?)|(On\\s+\\d{1,2}\\s+(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s+\\d{4},\\s+at\\s+\\d{1,2}:\\d{2}(?::\\d{2})?(?:,)?)|(Sent:\\s*(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday),\\s+(?:January|February|March|April|May|June|July|August|September|October|November|December)\\s+\\d{1,2},\\s+\\d{4}\\s+\\d{1,2}:\\d{2}(?::\\d{2})?\\s*(?:AM|PM|am|pm))|(On\\s+(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun),\\s+\\d{1,2}\\s+(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s+\\d{4}\\s+at\\s+\\d{1,2}:\\d{2}(?::\\d{2})?(?:,)?)|(On\\s+\\d{2}\\/\\d{2}\\/\\d{4}\\s+\\d{1,2}:\\d{2}(?::\\d{2})?,?)|(Date:\\s*(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun),\\s+\\d{1,2}\\s+(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s+\\d{4}\\s+at\\s+\\d{1,2}:\\d{2}(?::\\d{2})?(?:,)?)|((?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday),\\s+(?:January|February|March|April|May|June|July|August|September|October|November|December)\\s+\\d{1,2},\\s+\\d{4}\\s+\\d{1,2}:\\d{2}(?::\\d{2})?\\s*(?:AM|PM|am|pm)))/i;\n\n                bodyLines.forEach((line, index) => {\n                    const match = line.match(bodyDateRegex);\n                    if (match) {\n                        console.log(`Matched timestamp in line ${index + 1}: ${line}`);\n                        let timestampStr = match[1] || match[2] || match[3] || match[4] || match[5] || match[6] || match[7] || match[8];\n                        if (timestampStr) {\n                            // Clean up HTML tags and entities for parsing\n                            timestampStr = timestampStr.replace(/<[^>]+>/g, '').replace(/&[a-zA-Z0-9#]+;/g, '').trim();\n                            console.log(`Cleaned timestamp: ${timestampStr}`);\n\n                            // Normalize timestamp\n                            if (timestampStr.startsWith('On') && timestampStr.includes(', at') && !timestampStr.match(/Mon|Tue|Wed|Thu|Fri|Sat|Sun/)) {\n                                timestampStr = timestampStr.replace(/^On\\s+(\\d{1,2}\\s+\\w{3}\\s+\\d{4}),\\s+at\\s+(\\d{1,2}:\\d{2}(?::\\d{2})?)(?:,)?/, '$1 $2');\n                            } else if (timestampStr.startsWith('Sent:')) {\n                                timestampStr = timestampStr.replace(/^Sent:\\s*(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday),\\s+/, '');\n                            } else if (timestampStr.startsWith('On') && timestampStr.includes(' at ') && timestampStr.match(/Mon|Tue|Wed|Thu|Fri|Sat|Sun/)) {\n                                timestampStr = timestampStr.replace(/^On\\s+(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun),\\s+/, '');\n                                timestampStr = timestampStr.replace(/,\\s+at\\s+/, ' ');\n                            } else if (timestampStr.startsWith('On') && timestampStr.match(/\\d{2}\\/\\d{2}\\/\\d{4}/)) {\n                                // Thunderbird\n                            } else if (timestampStr.startsWith('Date:')) {\n                                timestampStr = timestampStr.replace(/^Date:\\s*(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun),\\s+/, '');\n                                timestampStr = timestampStr.replace(/\\s+at\\s+/, ' ');\n                            } else if (timestampStr.match(/^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday),/)) {\n                                timestampStr = timestampStr.replace(/^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday),\\s+/, '');\n                            }\n                            console.log(`Normalized timestamp: ${timestampStr}`);\n\n                            const timestamp = new Date(timestampStr);\n                            if (!isNaN(timestamp)) {\n                                console.log(`Parsed timestamp: ${timestamp.toISOString()}`);\n                                const entry = {\n                                    lineNumber: index + 1,\n                                    timestamp: timestamp.toISOString(),\n                                    line: line.trim().substring(0, 100) + (line.length > 100 ? '...' : '')\n                                };\n                                if (result.creationTime) {\n                                    const creationDate = new Date(result.creationTime);\n                                    entry.deltaFromCreation = (timestamp - creationDate) / 1000;\n                                }\n                                result.bodyTimestamps.push(entry);\n                            } else {\n                                console.log(`Failed to parse timestamp: ${timestampStr}`);\n                            }\n                        }\n                    } else {\n                        console.log(`No timestamp match in line ${index + 1}: ${line}`);\n                    }\n                });\n\n                result.bodyTimestamps.forEach((entry, index) => {\n                    if (index > 0) {\n                        const prevTime = new Date(result.bodyTimestamps[index - 1].timestamp);\n                        const currentTime = new Date(entry.timestamp);\n                        entry.delta = (currentTime - prevTime) / 1000;\n                    }\n                });\n\n                if (result.creationToSent === null && result.sentToDelivered === null && result.bodyTimestamps.length === 0 && !result.decodedBody.trim()) {\n                    result.error = 'Could not parse valid dates from email headers or body, and no valid body content found';\n                } else if (result.creationToSent === null && result.sentToDelivered === null && result.bodyTimestamps.length === 0) {\n                    result.error = 'Could not parse valid dates from email headers or body';\n                }\n\n            } catch (err) {\n                result.error = `Error parsing email: ${err.message}`;\n            }\n\n            return result;\n        }\n\n        document.getElementById('analyzeButton').addEventListener('click', () => {\n            const emailInput = document.getElementById('emailInput').value;\n            const resultsDiv = document.getElementById('results');\n            const errorMessage = document.getElementById('errorMessage');\n            const base64Message = document.getElementById('base64Message');\n            const bodyResults = document.getElementById('bodyResults');\n            const decodedBodyDiv = document.getElementById('decodedBody');\n\n            if (!emailInput.trim()) {\n                errorMessage.textContent = 'Please provide email source text';\n                errorMessage.classList.remove('hidden');\n                base64Message.classList.add('hidden');\n                resultsDiv.classList.add('hidden');\n                return;\n            }\n\n            const result = parseEmailTimeDelta(emailInput);\n\n            errorMessage.classList.add('hidden');\n            base64Message.classList.add('hidden');\n            resultsDiv.classList.remove('hidden');\n\n            // Metadata\n            document.getElementById('metadataFrom').textContent = result.from || 'Not available';\n            document.getElementById('metadataTo').textContent = result.to || 'Not available';\n            document.getElementById('metadataCC').textContent = result.cc || 'Not available';\n            document.getElementById('metadataDate').textContent = result.date ? new Date(result.date).toLocaleString() : 'Not available';\n            document.getElementById('metadataSubject').textContent = result.subject || 'Not available';\n\n            // Header Analysis\n            document.getElementById('creationTime').textContent = result.creationTime \n                ? new Date(result.creationTime).toLocaleString() \n                : 'Not available';\n                \n            document.getElementById('creationToSent').textContent = formatTimeDelta(result.creationToSent);\n            document.getElementById('creationToSentBar').style.width = calculateBarWidth(result.creationToSent);\n            document.getElementById('creationToSentBar').className = 'timeline-bar' + (result.creationToSent < 0 ? ' negative-bar' : '');\n                \n            document.getElementById('sentTime').textContent = result.sentTime \n                ? new Date(result.sentTime).toLocaleString() \n                : 'Not available';\n                \n            document.getElementById('sentToDelivered').textContent = formatTimeDelta(result.sentToDelivered);\n            document.getElementById('sentToDeliveredBar').style.width = calculateBarWidth(result.sentToDelivered);\n            document.getElementById('sentToDeliveredBar').className = 'timeline-bar' + (result.sentToDelivered < 0 ? ' negative-bar' : '');\n                \n            document.getElementById('deliveredTime').textContent = result.deliveredTime \n                ? new Date(result.deliveredTime).toLocaleString() \n                : 'Not available';\n\n            document.getElementById('creationStage').classList.toggle('hidden', !result.creationTime);\n            document.getElementById('creationToSentStage').classList.toggle('hidden', result.creationToSent === null);\n            document.getElementById('sentStage').classList.toggle('hidden', !result.sentTime);\n            document.getElementById('sentToDeliveredStage').classList.toggle('hidden', result.sentToDelivered === null);\n            document.getElementById('deliveredStage').classList.toggle('hidden', !result.deliveredTime);\n\n            const headerTimestampsDiv = document.getElementById('headerTimestamps');\n            headerTimestampsDiv.innerHTML = '';\n            if (result.headerTimestamps.length > 0) {\n                result.headerTimestamps.forEach((entry, index) => {\n                    const stageDiv = document.createElement('div');\n                    stageDiv.className = 'stage';\n                    const prevDelta = index > 0 ? \n                        (new Date(entry.timestamp) - new Date(result.headerTimestamps[index - 1].timestamp)) / 1000 : null;\n                    stageDiv.innerHTML = `\n                        <div class=\"timeline-dot\"></div>\n                        <h3 class=\"stage-title\">${entry.headerName}</h3>\n                        <p class=\"stage-text\">Time: ${new Date(entry.timestamp).toLocaleString()}</p>\n                        ${prevDelta !== null ? `\n                            <p class=\"stage-text\">Time since previous: ${formatTimeDelta(prevDelta)}</p>\n                            <div class=\"timeline-bar ${prevDelta < 0 ? 'negative-bar' : ''}\" style=\"width: ${calculateBarWidth(prevDelta)};\"></div>\n                        ` : ''}\n                    `;\n                    headerTimestampsDiv.appendChild(stageDiv);\n                });\n            }\n\n            if (result.base64DecodeError) {\n                base64Message.textContent = result.base64DecodeError;\n                base64Message.classList.remove('hidden');\n            }\n\n            bodyResults.innerHTML = '';\n            if (result.bodyTimestamps.length === 0) {\n                bodyResults.innerHTML = '<p class=\"stage-text\">No timestamps found in email body</p>';\n            } else {\n                result.bodyTimestamps.forEach((entry, index) => {\n                    const stageDiv = document.createElement('div');\n                    stageDiv.className = 'stage';\n                    const escapedLine = escapeHtml(entry.line.substring(0, 100)) + (entry.line.length > 100 ? '...' : '');\n                    console.log(`Rendering line ${entry.lineNumber}: raw=${entry.line}, escaped=${escapedLine}`);\n                    stageDiv.innerHTML = `\n                        <div class=\"timeline-dot\"></div>\n                        <h3 class=\"stage-title\">Line ${entry.lineNumber}</h3>\n                        <p class=\"stage-text\">Time: ${new Date(entry.timestamp).toLocaleString()}</p>\n                        <p class=\"stage-text\">Content: ${escapedLine}</p>\n                        ${entry.deltaFromCreation !== undefined ? `\n                            <p class=\"stage-text\">Time from creation: ${formatTimeDelta(entry.deltaFromCreation)}</p>\n                            <div class=\"timeline-bar ${entry.deltaFromCreation < 0 ? 'negative-bar' : ''}\" style=\"width: ${calculateBarWidth(entry.deltaFromCreation)};\"></div>\n                        ` : ''}\n                        ${index > 0 ? `\n                            <p class=\"stage-text\">Time since previous: ${formatTimeDelta(entry.delta)}</p>\n                            <div class=\"timeline-bar ${entry.delta < 0 ? 'negative-bar' : ''}\" style=\"width: ${calculateBarWidth(entry.delta)};\"></div>\n                        ` : ''}\n                    `;\n                    bodyResults.appendChild(stageDiv);\n                });\n            }\n\n            decodedBodyDiv.textContent = result.decodedBody;\n\n            if (result.error) {\n                errorMessage.textContent = result.error;\n                errorMessage.classList.remove('hidden');\n            }\n        });\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "example-pwndefend.json",
    "content": "{\n  \"nodes\": [\n    {\n      \"id\": 1,\n      \"type\": \"domain\",\n      \"domain\": \"pwndefend.com\"\n    },\n    {\n      \"id\": 2,\n      \"type\": \"ip\",\n      \"ip\": \"172.67.136.149\"\n    },\n    {\n      \"id\": 3,\n      \"type\": \"ip\",\n      \"ip\": \"104.21.81.6\"\n    },\n    {\n      \"id\": 4,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8080\"\n    },\n    {\n      \"id\": 5,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2082\"\n    },\n    {\n      \"id\": 6,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2083\"\n    },\n    {\n      \"id\": 7,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2052\"\n    },\n    {\n      \"id\": 8,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2086\"\n    },\n    {\n      \"id\": 9,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2087\"\n    },\n    {\n      \"id\": 10,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"80\"\n    },\n    {\n      \"id\": 11,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8880\"\n    },\n    {\n      \"id\": 12,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8443\"\n    },\n    {\n      \"id\": 13,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"443\"\n    },\n    {\n      \"id\": 14,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2053\"\n    },\n    {\n      \"id\": 15,\n      \"type\": \"asn\",\n      \"asn\": \"AS13335\"\n    },\n    {\n      \"id\": 16,\n      \"type\": \"city\",\n      \"city\": \"San Francisco\"\n    },\n    {\n      \"id\": 17,\n      \"type\": \"organization\",\n      \"organization\": \"Cloudflare, Inc.\"\n    },\n    {\n      \"id\": 18,\n      \"type\": \"country\",\n      \"country\": \"US\"\n    },\n    {\n      \"id\": 19,\n      \"type\": \"vpn\"\n    },\n    {\n      \"id\": 20,\n      \"type\": \"hosting\"\n    }\n  ],\n  \"edges\": [\n    {\n      \"id\": \"1-2-ResolvesTo\",\n      \"from\": 1,\n      \"to\": 2,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"1-3-ResolvesTo\",\n      \"from\": 1,\n      \"to\": 3,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"2-4-Exposes\",\n      \"from\": 2,\n      \"to\": 4,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"2-5-Exposes\",\n      \"from\": 2,\n      \"to\": 5,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"2-6-Exposes\",\n      \"from\": 2,\n      \"to\": 6,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"2-7-Exposes\",\n      \"from\": 2,\n      \"to\": 7,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"2-8-Exposes\",\n      \"from\": 2,\n      \"to\": 8,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"2-9-Exposes\",\n      \"from\": 2,\n      \"to\": 9,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"2-10-Exposes\",\n      \"from\": 2,\n      \"to\": 10,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"2-11-Exposes\",\n      \"from\": 2,\n      \"to\": 11,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"2-12-Exposes\",\n      \"from\": 2,\n      \"to\": 12,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"2-13-Exposes\",\n      \"from\": 2,\n      \"to\": 13,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"3-4-Exposes\",\n      \"from\": 3,\n      \"to\": 4,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"3-5-Exposes\",\n      \"from\": 3,\n      \"to\": 5,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"3-6-Exposes\",\n      \"from\": 3,\n      \"to\": 6,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"3-14-Exposes\",\n      \"from\": 3,\n      \"to\": 14,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"3-8-Exposes\",\n      \"from\": 3,\n      \"to\": 8,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"3-9-Exposes\",\n      \"from\": 3,\n      \"to\": 9,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"3-10-Exposes\",\n      \"from\": 3,\n      \"to\": 10,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"3-11-Exposes\",\n      \"from\": 3,\n      \"to\": 11,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"3-12-Exposes\",\n      \"from\": 3,\n      \"to\": 12,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"3-13-Exposes\",\n      \"from\": 3,\n      \"to\": 13,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"2-15-Assigned to\",\n      \"from\": 2,\n      \"to\": 15,\n      \"label\": \"Assigned to\"\n    },\n    {\n      \"id\": \"2-16-Located in\",\n      \"from\": 2,\n      \"to\": 16,\n      \"label\": \"Located in\"\n    },\n    {\n      \"id\": \"2-17-Belongs to\",\n      \"from\": 2,\n      \"to\": 17,\n      \"label\": \"Belongs to\"\n    },\n    {\n      \"id\": \"2-18-Located in\",\n      \"from\": 2,\n      \"to\": 18,\n      \"label\": \"Located in\"\n    },\n    {\n      \"id\": \"2-19-Uses\",\n      \"from\": 2,\n      \"to\": 19,\n      \"label\": \"Uses\"\n    },\n    {\n      \"id\": \"2-20-Uses\",\n      \"from\": 2,\n      \"to\": 20,\n      \"label\": \"Uses\"\n    },\n    {\n      \"id\": \"3-15-Assigned to\",\n      \"from\": 3,\n      \"to\": 15,\n      \"label\": \"Assigned to\"\n    },\n    {\n      \"id\": \"3-16-Located in\",\n      \"from\": 3,\n      \"to\": 16,\n      \"label\": \"Located in\"\n    },\n    {\n      \"id\": \"3-17-Belongs to\",\n      \"from\": 3,\n      \"to\": 17,\n      \"label\": \"Belongs to\"\n    },\n    {\n      \"id\": \"3-18-Located in\",\n      \"from\": 3,\n      \"to\": 18,\n      \"label\": \"Located in\"\n    },\n    {\n      \"id\": \"3-20-Uses\",\n      \"from\": 3,\n      \"to\": 20,\n      \"label\": \"Uses\"\n    }\n  ]\n}"
  },
  {
    "path": "experimental_mapper.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Baddie Mapper - Experimental</title>\n    <style>\n\n/* Target mx-node class for vis.js nodes */\n.vis-network .vis-node.mx-node {\n    background-color: #60a5fa; /* Blue */\n    border-color: #1e88e5; /* Slightly darker blue border for contrast */\n}\n\n.modal {\n    display: none;\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    background: rgba(0, 0, 0, 0.5);\n    z-index: 2000;\n    overflow: auto;\n}\n\n.modal-content {\n    position: relative;\n    margin: 50px auto;\n    padding: 20px;\n    width: 80%;\n    max-width: 800px;\n    max-height: 60vh; /* Shorter height: 60% of viewport */\n    overflow-y: auto; /* Vertical scroll for content */\n    border-radius: 8px;\n    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);\n    background: var(--modal-bg, #fff);\n    color: var(--modal-color, #1f2a44);\n    border: 1px solid var(--modal-border, #d1d5db);\n    display: flex;\n    flex-direction: column; /* Stack content vertically */\n}\n\n.dark .modal-content {\n    background: #1f2a44;\n    color: #e2e8f0;\n    border-color: #4b5563;\n}\n\n.close-modal {\n    position: absolute;\n    top: 10px;\n    right: 15px;\n    font-size: 24px;\n    cursor: pointer;\n    color: inherit;\n}\n\n.close-modal:hover {\n    color: #ef4444;\n}\n\n#riskTableContainer {\n    overflow-x: auto; /* Horizontal scroll for wide tables */\n    flex-grow: 1; /* Allow table to take available space */\n}\n\ntable {\n    width: 100%;\n    border-collapse: collapse;\n    margin-bottom: 20px;\n}\n\nth, td {\n    padding: 10px;\n    text-align: left;\n    border-bottom: 1px solid var(--table-border, #d1d5db);\n    word-wrap: break-word;\n    max-width: 250px;\n}\n\n.dark th, .dark td {\n    border-bottom-color: #4b5563;\n}\n\nth {\n    background: var(--th-bg, #f1f5f9);\n}\n\n.dark th {\n    background: #2d3748;\n}\n\n.print-button {\n    padding: 10px 20px;\n    background: #22c55e;\n    color: #fff;\n    border: none;\n    border-radius: 4px;\n    cursor: pointer;\n    margin-top: 10px; /* Space above button */\n    align-self: center; /* Center button horizontally */\n}\n\n.print-button:hover {\n    background: #16a34a;\n}\n\n/* CSS Variables */\n:root {\n    --transition: 0.3s;\n    --shadow-light: 0 2px 10px rgba(0, 0, 0, 0.1);\n    --shadow-dark: 0 2px 10px rgba(0, 0, 0, 0.3);\n    --border-light: #d1d5db;\n    --border-dark: #4b5563;\n    --top-bar-height: 40px;\n}\n\n/* Base Styles */\nbody {\n    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\n    margin: 0;\n    padding: 0;\n    display: flex;\n    height: calc(100vh - var(--top-bar-height));\n    font-size: 12px;\n    transition: background-color var(--transition), color var(--transition);\n    margin-top: var(--top-bar-height);\n}\n\nbody.light-mode {\n    background-color: #f0f2f5;\n    color: #1f2a44;\n}\n\nbody.dark-mode {\n    background-color: #1e293b;\n    color: #e2e8f0;\n}\n\n/* Top Bar */\n#top-bar {\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: var(--top-bar-height);\n    background-color: #2d3748;\n    color: #e2e8f0;\n    display: flex;\n    justify-content: space-between; /* Explicitly separate version and links */\n    align-items: center;\n    padding: 0 20px;\n    z-index: 1000;\n    box-shadow: var(--shadow-dark);\n    box-sizing: border-box;\n}\n\n.light-mode #top-bar {\n    background-color: #f0f2f5;\n    color: #1f2a44;\n    box-shadow: var(--shadow-light);\n}\n\n#version {\n    font-size: 14px;\n    font-weight: 500;\n    flex-shrink: 0; /* Prevent shrinking */\n    margin-right: 20px; /* Add space to separate from links */\n}\n\n#top-bar-links {\n    display: flex;\n    align-items: center;\n    gap: 30px; /* Increased spacing between links */\n    /* Fallback absolute positioning */\n    position: absolute;\n    right: 20px;\n    top: 50%;\n    transform: translateY(-50%);\n    /* Diagnostic border to confirm application */\n    border: 1px solid red !important;\n}\n\n#cyberchef-link,\n#github-link {\n    font-size: 14px;\n    color: #60a5fa;\n    text-decoration: none;\n    transition: color var(--transition);\n}\n\n#cyberchef-link:hover,\n#github-link:hover {\n    color: #3b82f6;\n}\n\n.light-mode #cyberchef-link,\n.light-mode #github-link {\n    color: #3b82f6;\n}\n\n.light-mode #cyberchef-link:hover,\n.light-mode #github-link:hover {\n    color: #2563eb;\n}\n/* Controls Panel */\n#controls {\n    position: fixed;\n    top: var(--top-bar-height);\n    left: 0;\n    height: calc(100vh - var(--top-bar-height));\n    z-index: 1000;\n    display: flex;\n    flex-direction: column;\n    overflow-y: auto;\n    transition: width var(--transition);\n    background-color: #fff;\n}\n\n.light-mode #controls {\n    background-color: #fff;\n    box-shadow: var(--shadow-light);\n}\n\n.dark-mode #controls {\n    background-color: #2d3748;\n    box-shadow: var(--shadow-dark);\n    color: #e2e8f0;\n}\n\n#controls.collapsed {\n    width: 50px;\n}\n\n#controls:not(.collapsed) {\n    width: 350px;\n}\n\n#controls-header {\n    padding: 10px;\n    display: flex;\n    flex-direction: column;\n    gap: 10px;\n}\n\n#controls-footer {\n    text-align: center;\n    padding: 10px;\n    font-size: 10px;\n    margin-top: auto;\n    transition: color var(--transition);\n}\n\n.light-mode #controls-footer {\n    color: #6b7280;\n}\n\n.dark-mode #controls-footer {\n    color: #94a3b8;\n}\n\n/* Menu Toggle Button */\n#menu-toggle {\n    padding: 6px 12px;\n    background-color: #3b82f6;\n    color: white;\n    border: none;\n    border-radius: 4px;\n    font-size: 12px;\n    cursor: pointer;\n    transition: background-color var(--transition);\n    width: 100%;\n    box-sizing: border-box;\n}\n\n#controls.collapsed #menu-toggle {\n    width: 100%;\n    display: block;\n}\n\n.light-mode #menu-toggle {\n    background-color: #3b82f6;\n}\n\n#menu-toggle:hover {\n    background-color: #2563eb;\n}\n\n.dark-mode #menu-toggle:hover {\n    background-color: #3b82f6;\n}\n\n/* Top Buttons */\n#mode-toggle,\n#pause-toggle,\n#reset-layout,\n#summary-button {\n    padding: 6px 12px;\n    background-color: #6b7280;\n    color: white;\n    border: none;\n    border-radius: 4px;\n    font-size: 12px;\n    cursor: pointer;\n    transition: background-color var(--transition);\n    width: 100%;\n    box-sizing: border-box;\n}\n\n#mode-toggle:hover,\n#pause-toggle:hover,\n#reset-layout:hover,\n#summary-button:hover {\n    background-color: #4b5563;\n}\n\n.dark-mode #mode-toggle,\n.dark-mode #pause-toggle,\n.dark-mode #reset-layout,\n.dark-mode #summary-button {\n    background-color: #9ca3af;\n}\n\n#pause-toggle.paused {\n    background-color: #ef4444;\n}\n\n#controls.collapsed #mode-toggle,\n#controls.collapsed #pause-toggle,\n#controls.collapsed #reset-layout,\n#controls.collapsed #summary-button,\n#controls.collapsed .tab-buttons,\n#controls.collapsed .tab-content,\n#controls.collapsed #controls-footer {\n    display: none;\n}\n\n/* Tab Navigation */\n.tab-buttons {\n    display: flex;\n    flex-wrap: wrap;\n    border-bottom: 1px solid var(--border-light);\n    transition: border-color var(--transition);\n    padding: 10px 0;\n}\n\n.dark-mode .tab-buttons {\n    border-bottom: 1px solid var(--border-dark);\n}\n\n.tab-button {\n    flex: 1 0 14.28%;\n    padding: 10px;\n    text-align: center;\n    border: none;\n    cursor: pointer;\n    transition: background-color var(--transition), color var(--transition);\n    font-size: 10px;\n}\n\n.light-mode .tab-button {\n    background-color: #f9fafb;\n    color: #1f2a44;\n}\n\n.dark-mode .tab-button {\n    background-color: #374151;\n    color: #e2e8f0;\n}\n\n.tab-button.active {\n    font-weight: bold;\n}\n\n.light-mode .tab-button.active {\n    background-color: #fff;\n    border-bottom: 2px solid #3b82f6;\n}\n\n.dark-mode .tab-button.active {\n    background-color: #2d3748;\n    border-bottom: 2px solid #60a5fa;\n}\n\n.light-mode .tab-button:hover:not(.active) {\n    background-color: #e5e7eb;\n}\n\n.dark-mode .tab-button:hover:not(.active) {\n    background-color: #4b5563;\n}\n\n/* Tab Content */\n.tab-content {\n    padding: 15px;\n    display: none;\n    flex-grow: 1;\n}\n\n.tab-content.active {\n    display: block;\n}\n\n.input-group {\n    margin: 15px 0;\n    padding: 10px;\n    border-radius: 6px;\n    transition: background-color var(--transition);\n}\n\n.light-mode .input-group {\n    background-color: #f9fafb;\n}\n\n.dark-mode .input-group {\n    background-color: #374151;\n}\n\n.input-group h3 {\n    margin: 0 0 8px 0;\n    font-size: 14px;\n}\n\n.light-mode .input-group h3 {\n    color: #1f2a44;\n}\n\n.dark-mode .input-group h3 {\n    color: #e2e8f0;\n}\n\n/* Inputs and Buttons */\ninput,\nselect,\ntextarea {\n    margin: 4px 0;\n    padding: 6px 10px;\n    border-radius: 4px;\n    font-size: 12px;\n    transition: border-color var(--transition), background-color var(--transition), color var(--transition);\n    width: 100%;\n    box-sizing: border-box;\n}\n\n.light-mode input,\n.light-mode select,\n.light-mode textarea {\n    border: 1px solid var(--border-light);\n    background-color: #fff;\n    color: #1f2a44;\n}\n\n.dark-mode input,\n.dark-mode select,\n.dark-mode textarea {\n    border: 1px solid var(--border-dark);\n    background-color: #4b5563;\n    color: #e2e8f0;\n}\n\ntextarea {\n    height: 100px;\n    resize: vertical;\n}\n\nbutton {\n    padding: 6px 12px;\n    border: none;\n    border-radius: 4px;\n    font-size: 12px;\n    cursor: pointer;\n    transition: background-color var(--transition), color var(--transition);\n    width: 100%;\n    margin: 4px 0;\n}\n\n.light-mode button {\n    background-color: #3b82f6;\n    color: white;\n}\n\n.dark-mode button {\n    background-color: #60a5fa;\n    color: #1e293b;\n}\n\n/* Network Container */\n#myNetwork {\n    flex-grow: 1;\n    height: calc(100vh - var(--top-bar-height));\n    border-radius: 0 8px 8px 0;\n    box-shadow: var(--shadow-light);\n    transition: margin-left var(--transition), margin-right var(--transition);\n    position: relative;\n}\n\n.light-mode #myNetwork {\n    background-color: #fff;\n}\n\n.dark-mode #myNetwork {\n    background-color: #334155;\n    box-shadow: var(--shadow-dark);\n}\n\n#controls:not(.collapsed) ~ #myNetwork {\n    margin-left: 300px;\n}\n\n#controls.collapsed ~ #myNetwork {\n    margin-left: 50px;\n}\n\n/* Properties Panel */\n#properties-panel {\n    position: fixed;\n    top: var(--top-bar-height);\n    right: -350px; /* Ensure it's fully off-screen */\n    width: 350px;\n    height: calc(100vh - var(--top-bar-height));\n    background-color: #fff;\n    box-shadow: -2px 0 10px rgba(0, 0, 0, 0.2);\n    z-index: 2000;\n    padding: 20px;\n    overflow-y: auto;\n    transition: right var(--transition);\n    display: none; /* Hidden by default */\n    box-sizing: border-box;\n}\n\n#properties-panel.active {\n    right: 0;\n    display: block; /* Show when active */\n}\n\n.dark-mode #properties-panel {\n    background-color: #2d3748;\n    box-shadow: -2px 0 10px rgba(0, 0, 0, 0.4);\n    color: #e2e8f0;\n}\n\n\n#properties-panel .close-button {\n    position: absolute;\n    top: 10px;\n    right: 10px;\n    background: none;\n    border: none;\n    font-size: 18px;\n    cursor: pointer;\n    color: #1f2a44;\n    transition: color var(--transition);\n}\n\n.dark-mode #properties-panel .close-button {\n    color: #e2e8f0;\n}\n\n#properties-panel.active ~ #myNetwork {\n    margin-right: 300px;\n}\n\n#properties-panel.active ~ #controls:not(.collapsed) ~ #myNetwork {\n    margin-left: 300px;\n    margin-right: 300px;\n}\n\n#properties-panel.active ~ #controls.collapsed ~ #myNetwork {\n    margin-left: 50px;\n    margin-right: 300px;\n}\n\n/* Notes Modal */\n#notes-modal {\n    display: none;\n    position: fixed;\n    top: 50%;\n    left: 50%;\n    transform: translate(-50%, -50%);\n    background-color: #fff;\n    padding: 20px;\n    border-radius: 8px;\n    box-shadow: var(--shadow-light);\n    z-index: 2000;\n    width: 500px;\n    max-width: 90vw;\n    max-height: 80vh;\n    overflow-y: auto;\n    box-sizing: border-box;\n}\n\n.dark-mode #notes-modal {\n    background-color: #2d3748;\n    box-shadow: var(--shadow-dark);\n    color: #e2e8f0;\n}\n\n#modal-overlay {\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100vw;\n    height: 100vh;\n    background: rgba(0, 0, 0, 0.5);\n    z-index: 1999;\n}\n\n#notes-modal h3 {\n    margin: 0 0 15px 0;\n    font-size: 16px;\n}\n\n#notes-textarea {\n    width: 100%;\n    height: 300px;\n    padding: 10px;\n    border-radius: 4px;\n    border: 1px solid var(--border-light);\n    background-color: #fff;\n    color: #1f2a44;\n    font-size: 12px;\n    resize: vertical;\n    box-sizing: border-box;\n    overflow-y: auto;\n}\n\n.dark-mode #notes-textarea {\n    border: 1px solid var(--border-dark);\n    background-color: #4b5563;\n    color: #e2e8f0;\n}\n\n#notes-modal .button-container {\n    margin-top: 15px;\n    display: flex;\n    justify-content: flex-end;\n    gap: 10px;\n}\n\n#notes-modal button {\n    padding: 6px 12px;\n    border-radius: 4px;\n    border: none;\n    cursor: pointer;\n    font-size: 12px;\n    transition: background-color var(--transition);\n}\n\n#notes-save {\n    background-color: #3b82f6;\n    color: white;\n}\n\n#notes-cancel {\n    background-color: #6b7280;\n    color: white;\n}\n\n#notes-save:hover {\n    background-color: #2563eb;\n}\n\n#notes-cancel:hover {\n    background-color: #4b5563;\n}\n\n.dark-mode #notes-save {\n    background-color: #60a5fa;\n    color: #1e293b;\n}\n\n.dark-mode #notes-cancel {\n    background-color: #9ca3af;\n}\n\n/* Summary Modal */\n#summary-modal {\n    display: none;\n    position: fixed;\n    top: 50%;\n    left: 50%;\n    transform: translate(-50%, -50%);\n    background-color: #fff;\n    padding: 20px;\n    border-radius: 8px;\n    box-shadow: var(--shadow-light);\n    z-index: 2000;\n    max-width: 500px;\n    width: 90%;\n    max-height: 80vh;\n    overflow-y: auto;\n}\n\n.toastify {\n    top: calc(var(--top-bar-height) + 10px) !important;\n    z-index: 3000 !important;\n}\n\n.dark-mode #summary-modal {\n    background-color: #2d3748;\n    box-shadow: var(--shadow-dark);\n    color: #e2e8f0;\n}\n\n#summary-modal table {\n    width: 100%;\n    border-collapse: collapse;\n    margin-top: 10px;\n}\n\n#summary-modal th,\n#summary-modal td {\n    padding: 8px;\n    text-align: left;\n    border-bottom: 1px solid var(--border-light);\n}\n\n.dark-mode #summary-modal th,\n.dark-mode #summary-modal td {\n    border-bottom: 1px solid var(--border-dark);\n}\n\n#summary-modal th {\n    background-color: #f9fafb;\n}\n\n.dark-mode #summary-modal th {\n    background-color: #374151;\n}\n\n#summary-modal .close-button {\n    float: right;\n    background: none;\n    border: none;\n    font-size: 16px;\n    cursor: pointer;\n    color: #1f2a44;\n}\n\n.dark-mode #summary-modal .close-button {\n    color: #e2e8f0;\n}\n\n/* Progress Bar */\n#progress-bar {\n    position: fixed;\n    top: var(--top-bar-height);\n    left: 0;\n    width: 100%;\n    padding: 10px;\n    text-align: center;\n    color: white;\n    font-size: 14px;\n    font-weight: bold;\n    z-index: 2000;\n    transition: opacity 0.5s ease-in-out;\n}\n\n.progress-active {\n    background-color: #dc2626;\n}\n\n.progress-complete {\n    background-color: #22c55e;\n}\n\n.progress-hidden {\n    opacity: 0;\n    pointer-events: none;\n}\n\n/* Context Menus */\n#contextMenu,\n#edgeContextMenu {\n    position: absolute;\n    background-color: #fff;\n    border: 1px solid #ccc;\n    box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);\n    z-index: 2000;\n    padding: 5px 0;\n}\n\n#contextMenu button,\n#edgeContextMenu button {\n    display: block;\n    width: 100%;\n    text-align: left;\n    padding: 5px 10px;\n    background: none;\n    border: none;\n    cursor: pointer;\n    color: #1f2a44;\n}\n\n#contextMenu button:hover,\n#edgeContextMenu button:hover {\n    background-color: #f0f0f0;\n}\n\n.dark-mode #contextMenu,\n.dark-mode #edgeContextMenu {\n    background-color: #2d3748;\n    border: 1px solid var(--border-dark);\n}\n\n.dark-mode #contextMenu button,\n.dark-mode #edgeContextMenu button {\n    color: #e2e8f0;\n}\n\n.dark-mode #contextMenu button:hover,\n.dark-mode #edgeContextMenu button:hover {\n    background-color: #4b5563;\n}\n\n/* Network Visualization */\n#myNetwork .vis-network canvas {\n    overflow: visible; /* Change from hidden to visible */\n}\n\n.vis-network .vis-label {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    text-align: center;\n    padding: 0;\n    margin: 0;\n    overflow: hidden;\n    white-space: normal;\n    max-width: 100%;\n}\n\n.vis-network .vis-node {\n    min-width: 40px;\n    min-height: 40px;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n}\n\n/* Password Toggle */\n.password-container {\n    display: flex;\n    flex-direction: column; /* Stack children vertically */\n    width: 100%;\n    margin-bottom: 8px;\n    gap: 4px; /* Add space between input and button */\n}\n\n/* Inputs - Remove padding-right */\n.password-container input {\n    padding: 6px 10px; /* Adjusted from padding-right: 60px to standard padding */\n}\n\n/* Password Toggle */\n.toggle-password {\n    /* Remove absolute positioning */\n    padding: 2px 8px;\n    background-color: #6b7280;\n    color: white;\n    border: none;\n    border-radius: 4px;\n    font-size: 10px;\n    cursor: pointer;\n    width: auto; /* Let it size naturally */\n    align-self: flex-start; /* Align to the left */\n}\n\n.light-mode .toggle-password {\n    background-color: #6b7280;\n}\n\n.dark-mode .toggle-password {\n    background-color: #9ca3af;\n}\n\n.toggle-password:hover {\n    background-color: #4b5563;\n}\n/* Stop Task Button */\n#stop-task {\n    padding: 6px 12px;\n    background-color: #dc2626;\n    color: white;\n    border: none;\n    border-radius: 4px;\n    font-size: 12px;\n    cursor: pointer;\n    transition: background-color var(--transition);\n    width: auto;\n    margin: 0 auto;\n    display: inline-block;\n}\n\n#stop-task:hover {\n    background-color: #b91c1c;\n}\n\n#stop-task:disabled {\n    background-color: #9ca3af;\n    cursor: not-allowed;\n}\n\n/* Search Input */\n#search-input {\n    padding: 6px 10px;\n    border-radius: 4px;\n    border: 1px solid var(--border-light);\n    background-color: #fff;\n    color: #1f2a44;\n    transition: border-color var(--transition);\n}\n\n.dark-mode #search-input {\n    border: 1px solid var(--border-dark);\n    background-color: #4b5563;\n    color: #e2e8f0;\n}\n\n#search-input:focus {\n    outline: none;\n    border-color: #3b82f6;\n}\n\n/* Checkbox Labels */\n.checkbox-label {\n    display: flex;\n    align-items: center;\n    margin: 4px 0;\n    font-size: 12px;\n}\n\n.light-mode .checkbox-label {\n    color: #1f2a44;\n}\n\n.dark-mode .checkbox-label {\n    color: #e2e8f0;\n}\n\ninput[type=\"checkbox\"] {\n    margin-right: 8px;\n    width: auto;\n}\n\n/* Media Queries */\n@media (max-width: 768px) {\n    #controls:not(.collapsed) {\n        width: 100%;\n    }\n    \n    #controls:not(.collapsed) ~ #myNetwork {\n        margin-left: 0;\n        display: none;\n    }\n    \n    #controls.collapsed ~ #myNetwork {\n        margin-left: 50px;\n        display: block;\n    }\n    \n    #properties-panel.active ~ #myNetwork {\n        margin-right: 300px;\n    }\n}\n\n#controls.collapsed #manual-save,\n#controls.collapsed #buy-me-a-coffee-container,\n#controls.collapsed #stop-task-container {\n    display: none;\n}\n</style>\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/toastify-js/1.12.0/toastify.min.css\">\n    <script src=\"https://cdn.jsdelivr.net/npm/vis-network@9.1.9/dist/vis-network.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/toastify-js/1.12.0/toastify.min.js\"></script>  \n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.3/jspdf.plugin.autotable.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js\"></script>\n</head>\n<body class=\"dark-mode\">\n    <div id=\"top-bar\">\n        <span id=\"version\">Baddie Mapper Experimental</span>\n        <a href=\"https://github.com/mr-r3b00t/crime-mapper\" target=\"_blank\" id=\"github-link\">GitHub Repo</a>\n        <a href=\"https://gchq.github.io/CyberChef\" target=\"_blank\" id=\"cyberchef-link\">GCHQ CyberChef</a>\n    </div>\n    <div id=\"notes-modal\">\n        <h3>Edit Node Notes</h3>\n        <textarea id=\"notes-textarea\" placeholder=\"Enter notes here (max 1000 characters)\"></textarea>\n        <div class=\"button-container\">\n            <button id=\"notes-save\" onclick=\"saveNodeNotes()\">Save</button>\n            <button id=\"notes-cancel\" onclick=\"hideNotesModal()\">Cancel</button>\n        </div>\n    </div>\n        <div id=\"progress-bar\" class=\"progress-hidden\">Task in progress...</div>\n        <!-- Rest of your HTML -->\n\n      \n\n    <div id=\"controls\">\n        <button id=\"menu-toggle\" onclick=\"toggleMenu()\" title=\"Toggle Menu\">></button>\n        <button id=\"mode-toggle\" onclick=\"toggleMode()\">Switch to Light Mode</button>\n        <button id=\"pause-toggle\" onclick=\"togglePhysics()\">Pause Physics</button>\n        <button id=\"reset-layout\" onclick=\"resetLayout()\">Reset Layout</button>\n        <div class=\"tab-buttons\">\n            <button class=\"tab-button\" onclick=\"showTab('object-management')\">Object Management</button>\n            <button class=\"tab-button\" onclick=\"showTab('link-management')\">Link Management</button>\n            <button class=\"tab-button\" onclick=\"showTab('import-export')\">Import/Export</button>\n            <button class=\"tab-button\" onclick=\"showTab('api-keys')\">Config</button>\n            <button class=\"tab-button\" onclick=\"showTab('enrichment')\">Enrichment</button>\n            <button class=\"tab-button active\" onclick=\"showTab('import-iocs')\">Import IOCs</button>\n            <button class=\"tab-button\" onclick=\"showTab('layouts')\">Layouts</button>\n            <button class=\"tab-button\" onclick=\"showTab('search')\">Search</button>\n        </div>\n        <div id=\"object-management\" class=\"tab-content\">\n            <div class=\"input-group\">\n                <h3>Add Entity</h3>\n                <select id=\"addEntityType\">\n                    <option value=\"contact\">Contact</option>\n                    <option value=\"ip\">IP Address</option>\n                    <option value=\"domain\">Domain</option>\n                    <option value=\"organization\">Organization</option>\n                    <option value=\"port\">Port</option>\n                    <option value=\"wallet\">Wallet</option>\n                    <option value=\"bank\">Bank Account</option>\n                    <option value=\"technology\">Technology</option>\n                    <option value=\"device\">Device</option>\n                    <option value=\"malware\">Malware</option>\n                    <option value=\"vulnerability\">Vulnerability</option>\n                    <option value=\"subnet\">Subnet</option> <!-- New option -->\n                </select>\n                <input type=\"text\" id=\"addVulnNameInput\" placeholder=\"Vulnerability Name\" style=\"display: none;\">\n                <input type=\"text\" id=\"addVulnCVEInput\" placeholder=\"CVE (optional)\" style=\"display: none;\">\n                <input type=\"text\" id=\"addVulnUrlInput\" placeholder=\"URL (optional)\" style=\"display: none;\">\n                <input type=\"text\" id=\"addNameInput\" placeholder=\"Name\">\n                <input type=\"email\" id=\"addEmailInput\" placeholder=\"Email (optional)\">\n                <input type=\"text\" id=\"addIpInput\" placeholder=\"IP Address\" style=\"display: none;\">\n                <input type=\"text\" id=\"addDomainInput\" placeholder=\"Domain\" style=\"display: none;\">\n                <input type=\"text\" id=\"addOrgInput\" placeholder=\"Organization Name\" style=\"display: none;\">\n                <input type=\"text\" id=\"addSubnetInput\" placeholder=\"Subnet (e.g., 192.168.1.0/24)\" style=\"display: none;\">\n                <input type=\"text\" id=\"addPortNumInput\" placeholder=\"Port Number\" style=\"display: none;\">\n                <select id=\"addPortType\" style=\"display: none;\">\n                    <option value=\"TCP\">TCP</option>\n                    <option value=\"UDP\">UDP</option>\n                </select>\n                <input type=\"text\" id=\"addWalletAddressInput\" placeholder=\"Wallet Address\" style=\"display: none;\">\n                <input type=\"text\" id=\"addAccountNumberInput\" placeholder=\"Account Number\" style=\"display: none;\">\n                <input type=\"text\" id=\"addSortCodeInput\" placeholder=\"Sort Code\" style=\"display: none;\">\n                <input type=\"text\" id=\"addTechNameInput\" placeholder=\"Technology Name\" style=\"display: none;\">\n                <input type=\"text\" id=\"addTechVersionInput\" placeholder=\"Version\" style=\"display: none;\">\n                <select id=\"addDeviceCategory\" style=\"display: none;\">\n                    <option value=\"Server\">Server</option>\n                    <option value=\"PC\">PC</option>\n                    <option value=\"Laptop\">Laptop</option>\n                    <option value=\"MAC\">MAC</option>\n                    <option value=\"SmartPhone\">SmartPhone</option>\n                    <option value=\"IOT\">IOT</option>\n                    <option value=\"Router\">Router</option>\n                    <option value=\"Switch\">Switch</option>\n                    <option value=\"Wireless Access Point\">Wireless Access Point</option>\n                    <option value=\"Other\">Other</option>\n                </select>\n                <input type=\"text\" id=\"addDeviceNameInput\" placeholder=\"Device Name\" style=\"display: none;\">\n                <input type=\"text\" id=\"addMalwareNameInput\" placeholder=\"Malware Name\" style=\"display: none;\">\n                <select id=\"addMalwareType\" style=\"display: none;\">\n                    <option value=\"Wiper\">Wiper</option>\n                    <option value=\"RAT\">RAT</option>\n                    <option value=\"Encryptor\">Encryptor</option>\n                    <option value=\"Stealer\">Stealer</option>\n                    <option value=\"Other\">Other</option>\n                </select>\n                <button onclick=\"addNode()\">Add Entity</button>\n            </div>\n            <div class=\"input-group\">\n                <h3>Edit Entity</h3>\n                <select id=\"editNodeSelect\" onchange=\"loadNodeForEdit()\"></select>\n                <select id=\"editEntityType\" disabled>\n                    <option value=\"contact\">Contact</option>\n                    <option value=\"ip\">IP Address</option>\n                    <option value=\"domain\">Domain</option>\n                    <option value=\"organization\">Organization</option>\n                    <option value=\"port\">Port</option>\n                    <option value=\"wallet\">Wallet</option>\n                    <option value=\"bank\">Bank Account</option>\n                    <option value=\"technology\">Technology</option>\n                    <option value=\"device\">Device</option>\n                    <option value=\"malware\">Malware</option>\n                    <option value=\"vulnerability\">Vulnerability</option>\n                </select>\n                <select id=\"editEntityType\" disabled>\n                    <!-- Existing options -->\n                    <option value=\"subnet\">Subnet</option>\n                </select>\n                <input type=\"text\" id=\"editSubnetInput\" placeholder=\"Subnet (e.g., 192.168.1.0/24)\" style=\"display: none;\">\n                <input type=\"text\" id=\"editVulnNameInput\" placeholder=\"Vulnerability Name\" style=\"display: none;\">\n                <input type=\"text\" id=\"editVulnCVEInput\" placeholder=\"CVE (optional)\" style=\"display: none;\">\n                <input type=\"text\" id=\"editVulnUrlInput\" placeholder=\"URL (optional)\" style=\"display: none;\">\n                <input type=\"text\" id=\"editNameInput\" placeholder=\"Name\">\n                <input type=\"email\" id=\"editEmailInput\" placeholder=\"Email (optional)\">\n                <input type=\"text\" id=\"editIpInput\" placeholder=\"IP Address\" style=\"display: none;\">\n                <input type=\"text\" id=\"editDomainInput\" placeholder=\"Domain\" style=\"display: none;\">\n                <input type=\"text\" id=\"editOrgInput\" placeholder=\"Organization Name\" style=\"display: none;\">\n                <input type=\"text\" id=\"editPortNumInput\" placeholder=\"Port Number\" style=\"display: none;\">\n                <select id=\"editPortType\" style=\"display: none;\">\n                    <option value=\"TCP\">TCP</option>\n                    <option value=\"UDP\">UDP</option>\n                </select>\n                <input type=\"text\" id=\"editWalletAddressInput\" placeholder=\"Wallet Address\" style=\"display: none;\">\n                <input type=\"text\" id=\"editAccountNumberInput\" placeholder=\"Account Number\" style=\"display: none;\">\n                <input type=\"text\" id=\"editSortCodeInput\" placeholder=\"Sort Code\" style=\"display: none;\">\n                <input type=\"text\" id=\"editTechNameInput\" placeholder=\"Technology Name\" style=\"display: none;\">\n                <input type=\"text\" id=\"editTechVersionInput\" placeholder=\"Version\" style=\"display: none;\">\n                <select id=\"editDeviceCategory\" style=\"display: none;\">\n                    <option value=\"Server\">Server</option>\n                    <option value=\"PC\">PC</option>\n                    <option value=\"Laptop\">Laptop</option>\n                    <option value=\"MAC\">MAC</option>\n                    <option value=\"SmartPhone\">SmartPhone</option>\n                    <option value=\"IOT\">IOT</option>\n                    <option value=\"Router\">Router</option>\n                    <option value=\"Switch\">Switch</option>\n                    <option value=\"Wireless Access Point\">Wireless Access Point</option>\n                    <option value=\"Other\">Other</option>\n                </select>\n                <input type=\"text\" id=\"editDeviceNameInput\" placeholder=\"Device Name\" style=\"display: none;\">\n                <input type=\"text\" id=\"editMalwareNameInput\" placeholder=\"Malware Name\" style=\"display: none;\">\n                <select id=\"editMalwareType\" style=\"display: none;\">\n                    <option value=\"Wiper\">Wiper</option>\n                    <option value=\"RAT\">RAT</option>\n                    <option value=\"Encryptor\">Encryptor</option>\n                    <option value=\"Stealer\">Stealer</option>\n                    <option value=\"Other\">Other</option>\n                </select>\n                <button onclick=\"editNode()\">Save Changes</button>\n            </div>\n            <div class=\"input-group\">\n                <h3>Remove Entity</h3>\n                <select id=\"removeNode\"></select>\n                <button onclick=\"removeNode()\">Remove Entity</button>\n            </div>\n        </div>\n        <div id=\"link-management\" class=\"tab-content\">\n            <div class=\"input-group\">\n                <h3>Create Link</h3>\n                <select id=\"fromNode\"></select>\n                <select id=\"toNode\"></select>\n                <input type=\"text\" id=\"edgeLabel\" placeholder=\"Link Label\">\n                <button onclick=\"addEdge()\">Add Link</button>\n            </div>\n            <div class=\"input-group\">\n                <h3>Remove Link</h3>\n                <select id=\"removeEdge\"></select>\n                <button onclick=\"removeEdge()\">Remove Link</button>\n            </div>\n        </div>\n        <div id=\"import-export\" class=\"tab-content\">\n            <div class=\"input-group\">\n                <h3>Export/Import</h3>\n                <button onclick=\"exportGraph()\">Export to JSON</button>\n                <button onclick=\"exportVisibleGraph()\">Export Visible to JSON</button>\n                <!-- Removed: <input type=\"file\" id=\"importFile\" accept=\".json\"> -->\n                <button onclick=\"importGraph()\">Import from JSON</button>\n                <button onclick=\"clearGraph()\">Clear Graph</button>\n                <button id=\"summary-button\" onclick=\"showGraphSummary()\">Graph Summary</button>\n                <button onclick=\"exportToPNG()\">Export to PNG</button>\n                <button onclick=\"exportToPDF()\">Export to PDF</button>\n                <button onclick=\"exportConfigBackup()\">Backup Config</button> <!-- New Button -->\n                <button onclick=\"importConfig()\">Import Config</button> <!-- New Button -->\n                <button onclick=\"importNMAP()\">Import NMAP</button> <!-- New Button -->\n            </div>\n        </div>\n        <div id=\"api-keys\" class=\"tab-content\">\n            <div class=\"input-group\">\n                <h3>IPINFO API Key</h3>\n                <div class=\"password-container\">\n                    <input type=\"password\" id=\"ipinfoApiKey\" placeholder=\"Enter IPinfo API Key\">\n                    <button type=\"button\" class=\"toggle-password\" data-target=\"ipinfoApiKey\">Show</button>\n                </div>\n                <label class=\"checkbox-label\">\n                    <input type=\"checkbox\" id=\"storeIpinfoKey\"> Store in local storage\n                </label>\n                <button onclick=\"saveIpinfoApiKey()\">Save IPinfo API Key</button>\n            </div>\n            <div class=\"input-group\">\n                <h3>Shodan API Key</h3>\n                <div class=\"password-container\">\n                    <input type=\"password\" id=\"shodanApiKey\" placeholder=\"Enter Shodan API Key\">\n                    <button type=\"button\" class=\"toggle-password\" data-target=\"shodanApiKey\">Show</button>\n                </div>\n                <label class=\"checkbox-label\">\n                    <input type=\"checkbox\" id=\"storeShodanKey\"> Store in local storage\n                </label>\n                <button onclick=\"saveShodanApiKey()\">Save Shodan API Key</button>\n            </div>\n            <div class=\"input-group\">\n                <h3>GreyNoise API Key</h3>\n                <div class=\"password-container\">\n                    <input type=\"password\" id=\"greynoiseApiKey\" placeholder=\"Enter GreyNoise API Key\">\n                    <button type=\"button\" class=\"toggle-password\" data-target=\"greynoiseApiKey\">Show</button>\n                </div>\n                <label class=\"checkbox-label\">\n                    <input type=\"checkbox\" id=\"storeGreynoiseKey\"> Store in local storage\n                </label>\n                <button onclick=\"saveGreynoiseApiKey()\">Save GreyNoise API Key</button>\n            </div>\n            <div class=\"input-group\">\n                <h3>URLscan.io API Key</h3>\n                <div class=\"password-container\">\n                    <input type=\"password\" id=\"urlscanApiKey\" placeholder=\"Enter URLscan.io API Key\">\n                    <button type=\"button\" class=\"toggle-password\" data-target=\"urlscanApiKey\">Show</button>\n                </div>\n                <label class=\"checkbox-label\">\n                    <input type=\"checkbox\" id=\"storeUrlscanKey\"> Store in local storage\n                </label>\n                <button onclick=\"saveUrlscanApiKey()\">Save URLscan.io API Key</button>\n            </div>\n            <div class=\"input-group\">\n                <h3>SecurityTrails API Key</h3>\n                <div class=\"password-container\">\n                    <input type=\"password\" id=\"securitytrailsApiKey\" placeholder=\"Enter SecurityTrails API Key\">\n                    <button type=\"button\" class=\"toggle-password\" data-target=\"securitytrailsApiKey\">Show</button>\n                </div>\n                <label class=\"checkbox-label\">\n                    <input type=\"checkbox\" id=\"storeSecuritytrailsKey\"> Store in local storage\n                </label>\n                <button onclick=\"saveSecuritytrailsApiKey()\">Save SecurityTrails API Key</button>\n                </div>\n                <div class=\"input-group\">\n                    <h3>URLhaus API Key</h3>\n                    <div class=\"password-container\">\n                        <input type=\"password\" id=\"urlhausApiKey\" placeholder=\"Enter URLhaus API Key\">\n                        <button type=\"button\" class=\"toggle-password\" data-target=\"urlhausApiKey\">Show</button>\n                    </div>\n                    <label class=\"checkbox-label\">\n                        <input type=\"checkbox\" id=\"storeUrlhausKey\"> Store in local storage\n                    </label>\n                    <button onclick=\"saveUrlhausApiKey()\">Save URLhaus API Key</button>\n                </div>\n            <div class=\"input-group\">\n                <h3>CORS Proxy URL</h3>\n                <div class=\"password-container\">\n                    <input type=\"password\" id=\"corsProxyUrl\" placeholder=\"Enter CORS Proxy URL\" value=\"http://localhost:3000/proxy?url=\">\n                    <button type=\"button\" class=\"toggle-password\" data-target=\"corsProxyUrl\">Show</button>\n                </div>\n                <label class=\"checkbox-label\">\n                    <input type=\"checkbox\" id=\"storeCorsProxy\" checked> Store in local storage\n                </label>\n                <label class=\"checkbox-label\">\n                    <input type=\"checkbox\" id=\"routeViaProxy\"> Route all traffic via proxy\n                </label>\n                <label class=\"checkbox-label\">\n                    <input type=\"checkbox\" id=\"ignoreApiKeysViaProxy\"> Ignore API keys when using proxy\n                </label>\n                <button onclick=\"saveCorsProxyUrl()\">Save CORS Proxy URL</button>\n            </div>\n            <div class=\"input-group\">\n                <h3>Test Functions</h3>\n                <button onclick=\"runAllTests()\">Run Test Functions</button>\n            </div>\n        </div>\n        <div id=\"enrichment\" class=\"tab-content\">\n            <div class=\"input-group\">\n                <h3>Bulk Enrichment</h3>\n                <button onclick=\"enrichAllIpinfo()\">Enrich All IPs with IPinfo</button>\n                <button onclick=\"enrichAllShodan()\">Enrich All IPs with Shodan</button>\n                <button onclick=\"enrichAllInternetDB()\">Enrich All IPs with InternetDB</button>\n                <button onclick=\"enrichAllGoogleDNS()\">Enrich All Domains with Google DNS</button>\n                <button onclick=\"enrichAllGoogleDNSMX()\">Enrich All Domains with Google DNS (MX)</button>\n                <button onclick=\"enrichAllGoogleDNSTXT()\">Enrich All Domains with Google DNS (TXT)</button>\n                <button onclick=\"enrichAllHudsonRockEmails()\">Enrich All Emails with Hudson Rock</button>\n                <button onclick=\"enrichAllHudsonRockDomains()\">Enrich All Domains with Hudson Rock</button>\n                <button onclick=\"enrichAllGreyNoise()\">Enrich All IPs with GreyNoise</button>\n                <button onclick=\"enrichAllURLscan()\">Enrich All URLs with URLscan.io</button>\n                <button onclick=\"enrichAllSecurityTrails()\">Enrich All Domains with SecurityTrails Subdomains</button>\n                <button onclick=\"enrichAllURLhaus()\">Enrich All URLs with URLhaus</button>\n            </div>\n        </div>\n        <div id=\"import-iocs\" class=\"tab-content active\">\n            <div class=\"input-group\">\n                <h3>Import IOCs</h3>\n                <textarea id=\"iocText\" placeholder=\"Paste IOC text here (IPs, domains, emails)\"></textarea>\n                <button onclick=\"importIOCsFromText()\">Import from Text</button>\n                <input type=\"file\" id=\"iocFile\" accept=\".txt\">\n                <button onclick=\"importIOCsFromFile()\">Import from File</button>\n            </div>\n        </div>\n        <div id=\"search\" class=\"tab-content\">\n            <div class=\"input-group\" style=\"margin: 10px 0;\">\n                <input type=\"text\" id=\"search-input\" placeholder=\"Search graph...\" style=\"width: 100%; margin-bottom: 5px;\">\n                <button onclick=\"searchGraph()\" style=\"background-color: #10b981;\">Search</button>\n            </div>\n            <button onclick=\"riskAnalysis()\">Risk Analysis</button> <!-- New Button -->\n        </div>\n        <div id=\"riskModal\" class=\"modal\">\n            <div class=\"modal-content\">\n                <span class=\"close-modal\">&times;</span>\n                <h2>Risk Analysis</h2>\n                <div id=\"riskTableContainer\"></div>\n            </div>\n        </div>\n        <div id=\"layouts\" class=\"tab-content\">\n            <div class=\"input-group\">\n                <h3>Graph Layouts</h3>\n                <!-- Existing layout buttons remain here -->\n                <button onclick=\"setOrganicLayout()\">Organic</button>\n                <button onclick=\"setCircularLayout()\">Circular</button>\n                <button onclick=\"setOrthogonalLayout()\">Orthogonal</button>\n                <button onclick=\"setTreeLayout()\">Tree</button>\n                <button onclick=\"setHierarchicalLayout()\">Hierarchical</button>\n            </div>\n            <!-- New section for label visibility -->\n            <div class=\"input-group\">\n                <h3>Label Visibility</h3>\n                <label class=\"checkbox-label\">\n                    <input type=\"checkbox\" id=\"showNodeLabels\" checked onchange=\"toggleNodeLabels()\">\n                    Show Node Labels\n                </label>\n                <label class=\"checkbox-label\">\n                    <input type=\"checkbox\" id=\"showEdgeLabels\" checked onchange=\"toggleEdgeLabels()\">\n                    Show Edge Labels\n                </label>\n                <label class=\"checkbox-label\">\n                    <input type=\"checkbox\" id=\"hideIsolatedNodes\" onchange=\"toggleIsolatedNodes()\">\n                    Hide Nodes Without Links\n                </label>\n            </div>\n            <div class=\"input-group\">\n                <h3>Node Size Layouts</h3>\n                <button onclick=\"setNodeSizeLayout('incoming')\">Size by Incoming Links</button>\n                <button onclick=\"setNodeSizeLayout('outgoing')\">Size by Outgoing Links</button>\n                <button onclick=\"setNodeSizeLayout('both')\">Size by All Links</button>\n            </div>\n            <div class=\"input-group\">\n                <h3>Filter Display</h3>\n                <button onclick=\"filterIpAndDomains()\">Show Only IPs & Domains</button>\n                <button onclick=\"showAllNodes()\">Show All Nodes</button>\n            </div>\n        </div>\n         <!-- Buy Me a Coffee Button -->\n         <div id=\"buy-me-a-coffee-container\" style=\"text-align: center; padding: 10px;\">\n            <script type=\"text/javascript\" src=\"https://cdnjs.buymeacoffee.com/1.0.0/button.prod.min.js\" \n                data-name=\"bmc-button\" \n                data-slug=\"mrr3b00t\" \n                data-color=\"#FFDD00\" \n                data-emoji=\"\"  \n                data-font=\"Cookie\" \n                data-text=\"Buy me a coffee\" \n                data-outline-color=\"#000000\" \n                data-font-color=\"#000000\" \n                data-coffee-color=\"#ffffff\">\n            </script>\n        </div>\n\n        <button id=\"manual-save\" onclick=\"saveStateAfterOperation()\">Save Now</button>\n\n        <div id=\"stop-task-container\" style=\"text-align: center; padding: 10px;\">\n            <button id=\"stop-task\" onclick=\"stopActiveTask()\">Stop Active Task</button>\n        </div>\n\n        <!-- Suggested -->\n        <footer id=\"controls-footer\"> \n    <p>Created by mrr3b00t (@UK_Daniel_Card)</p>\n    <p>© Xservus Limited - v0.288 experimental</p>\n</footer>\n    </div>\n    <div id=\"myNetwork\"></div>\n    <div id=\"summary-modal\">\n        <button class=\"close-button\" onclick=\"hideGraphSummary()\">×</button>\n        <h3>Graph Summary</h3>\n        <table id=\"summary-table\">\n            <thead>\n                <tr>\n                    <th>Type</th>\n                    <th>Count</th>\n                </tr>\n            </thead>\n            <tbody></tbody>\n        </table>\n    </div>\n    <div id=\"properties-panel\">\n        <button class=\"close-button\" onclick=\"hidePropertiesPanel()\">×</button>\n        <h3>Node Properties</h3>\n        <table id=\"properties-table\">\n            <thead>\n                <tr>\n                    <th>Property</th>\n                    <th>Value</th>\n                </tr>\n            </thead>\n            <tbody></tbody>\n        </table>\n    </div>\n    <div id=\"contextMenu\" style=\"display: none;\"></div>\n    <div id=\"edgeContextMenu\" style=\"display: none;\"></div>\n\n<script>\n        let nodes = new vis.DataSet([]);\n        let edges = new vis.DataSet([]);\n        let nextId = 1;\n        let isDarkMode = true;\n        let ipinfoApiKey;\n        let shodanApiKey;\n        let urlhausApiKey = localStorage.getItem('urlhausApiKey') || '';\n        let securitytrailsApiKey = localStorage.getItem('securitytrailsApiKey') || ''; //security tails api key variable\n        let greynoiseApiKey = localStorage.getItem('greynoiseApiKey') || '';\n        let urlscanApiKey = localStorage.getItem('urlscanApiKey') || '';\n        let corsProxyUrl;\n        let routeViaProxy;\n        let ignoreApiKeysViaProxy;\n        let isPhysicsPaused = false;\n        let lastRequestTime = 0;\n        let activeTaskController = null;\n        let nodeLabelsVisible = true;\n        let edgeLabelsVisible = true;\n        let currentEditingNodeId = null;\n        const RATE_LIMIT_MS = 500;\n        const SHODAN_RATE_LIMIT_MS = 1000; // Specific 1-second delay for Shodan\n        const SECURITYTRAILS_RATE_LIMIT_MS = 2000; // 1 second delay for SecurityTrails API\n        const ipRegex = { ipv4: /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/, ipv6: /^([0-9a-fA-F]{0,4}:){7}[0-9a-fA-F]{0,4}$/ };\n        const domainRegex = /^(?:[a-zA-Z0-9-_]+\\.)*[a-zA-Z0-9-_]+\\.[a-zA-Z]{2,}$/;\n        const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/g;\n\n        let selectedNodes = new Set(); // To track multiple selected nodes\n    \n        initializeApiKeys();\n\n\n\n        // Define updateTheme first\nfunction updateTheme() {\n    if (isDarkMode) {\n        document.body.classList.remove('light-mode');\n        document.body.classList.add('dark-mode');\n        options.nodes.font = {\n            size: nodeLabelsVisible ? 12 : 0,\n            color: '#e2e8f0',  // Light color for dark mode\n            multi: true,\n            align: 'center',\n            vadjust: 0,\n            strokeWidth: 0\n        };\n        options.edges.font = {\n            size: edgeLabelsVisible ? 12 : 0,\n            color: '#e2e8f0',  // Light color for dark mode\n            strokeWidth: 0,\n            strokeColor: 'transparent',\n            align: 'middle',\n            multi: true\n        };\n    } else {\n        document.body.classList.remove('dark-mode');\n        document.body.classList.add('light-mode');\n        options.nodes.font = {\n            size: nodeLabelsVisible ? 12 : 0,\n            color: '#1f2a44',  // Dark color for light mode\n            multi: true,\n            align: 'center',\n            vadjust: 0,\n            strokeWidth: 0\n        };\n        options.edges.font = {\n            size: edgeLabelsVisible ? 12 : 0,\n            color: '#1f2a44',  // Dark color for light mode\n            strokeWidth: 0,\n            strokeColor: 'transparent',\n            align: 'middle',\n            multi: true\n        };\n    }\n\n    // Update all existing nodes and edges with the new font settings\n    nodes.forEach(node => {\n        nodes.update({\n            id: node.id,\n            font: options.nodes.font\n        });\n    });\n    \n    edges.forEach(edge => {\n        edges.update({\n            id: edge.id,\n            font: options.edges.font\n        });\n    });\n\n    // Apply the options to the network and force a redraw\n    network.setOptions(options);\n    updateLabelVisibility();\n    //ensureInteractionSettings();\n    network.setData({ nodes: nodes, edges: edges });  // Force data refresh\n    network.redraw();\n}\n\nfunction searchGraph() {\n        const searchTerm = document.getElementById('search-input').value.trim().toLowerCase();\n        if (!searchTerm) {\n            showToast('Please enter a search term', 'error');\n            resetNodeHighlights();\n            return;\n        }\n\n    // Reset previous highlights\n    resetNodeHighlights();\n\n    // Find matching nodes\n    const matchingNodes = nodes.get().filter(node => {\n        // Search in label, and type-specific fields\n        const searchableText = [\n            node.label || '',\n            node.ip || '',\n            node.domain || '',\n            node.email || '',\n            node.name || '',\n            node.organization || '',\n            node.portNumber || '',\n            node.address || '',\n            node.accountNumber || '',\n            node.techName || '',\n            node.deviceName || '',\n            node.malwareName || '',\n            node.vulnName || '',\n            node.cve || '',\n            node.hash || '',\n            node.asn || '',\n            node.city || '',\n            node.country || '',\n            node.os || '',\n            node.product || ''\n        ].join(' ').toLowerCase();\n\n        return searchableText.includes(searchTerm);\n    });\n\n    if (matchingNodes.length === 0) {\n        showToast('No matches found', 'info');\n        return;\n    }\n\n    // Highlight matching nodes\n    matchingNodes.forEach(node => {\n        nodes.update({\n            id: node.id,\n            color: {\n                background: '#ffeb3b', // Bright yellow for highlight\n                border: '#f44336',     // Red border for visibility\n                highlight: {\n                    background: '#ffeb3b',\n                    border: '#f44336'\n                }\n            },\n            font: {\n                size: 14,  // Slightly larger font for visibility\n                color: '#000000'  // Black text for contrast\n            }\n        });\n    });\n\n    // Zoom to the first matching node\n    const firstMatchId = matchingNodes[0].id;\n    network.focus(firstMatchId, {\n        scale: 1.5,  // Zoom in\n        animation: {\n            duration: 1000,\n            easingFunction: 'easeInOutQuad'\n        }\n    });\n    ensureInteractionSettings(); // Ensure panning is enabled after focus\n    showToast(`Found ${matchingNodes.length} matching nodes`, 'success');\n}\n\nfunction resetNodeHighlights() {\n    nodes.forEach(node => {\n        // Restore original colors based on node type\n        const originalColor = getNodeColorByType(node.type);\n        nodes.update({\n            id: node.id,\n            color: {\n                background: originalColor.background,\n                border: isDarkMode ? '#94a3b8' : '#6b7280',\n                highlight: {\n                    background: originalColor.background,\n                    border: '#60a5fa'\n                }\n            },\n            font: {\n                size: nodeLabelsVisible ? 12 : 0,\n                color: isDarkMode ? '#e2e8f0' : '#1f2a44'\n            }\n        });\n    });\n    network.fit({\n        animation: {\n            duration: 500,\n            easingFunction: 'easeInOutQuad'\n        }\n    });\n}\n\nfunction exportToPNG() {\n    network.setOptions({ physics: { enabled: false } });\n    network.stabilize(100);\n    network.fit(); // Zoom to fit all nodes\n    \n    setTimeout(() => {\n        try {\n            const canvas = document.querySelector('#myNetwork .vis-network canvas');\n            if (!canvas) throw new Error('Canvas not found');\n            \n            const dataURL = canvas.toDataURL('image/png');\n            const link = document.createElement('a');\n            link.href = dataURL;\n            link.download = `network_graph_${new Date().toISOString().replace(/[:.]/g, '-')}.png`;\n            document.body.appendChild(link);\n            link.click();\n            document.body.removeChild(link);\n            \n            showToast('Graph exported as PNG', 'success');\n        } catch (error) {\n            console.error('Error exporting to PNG:', error);\n            showToast('Failed to export graph as PNG: ' + error.message, 'error');\n        } finally {\n            network.setOptions({ physics: { enabled: !isPhysicsPaused } });\n        }\n    }, 500);\n}\n\n\nfunction filterIpAndDomains() {\n    // Disable physics during filtering\n    network.setOptions({ physics: { enabled: false } });\n    \n    // Update each node's hidden property based on type\n    nodes.forEach(node => {\n        const shouldHide = node.type !== 'ip' && node.type !== 'domain';\n        nodes.update({\n            id: node.id,\n            hidden: shouldHide\n        });\n    });\n    \n    // Hide edges connected to hidden nodes\n    edges.forEach(edge => {\n        const fromNode = nodes.get(edge.from);\n        const toNode = nodes.get(edge.to);\n        const shouldHide = fromNode.hidden || toNode.hidden;\n        edges.update({\n            id: edge.id,\n            hidden: shouldHide\n        });\n    });\n    \n    // Stabilize and fit the network\n    stabilizeNetwork().then(() => {\n        network.fit({\n            animation: {\n                duration: 300,\n                easingFunction: 'easeInOutQuad'\n            }\n        });\n        showToast('Showing only IP addresses and domains', 'success');\n        saveStateAfterOperation();\n    });\n}\n\nfunction showAllNodes() {\n    // Disable physics during filtering\n    network.setOptions({ physics: { enabled: false } });\n    \n    // Show all nodes and edges\n    nodes.forEach(node => {\n        nodes.update({\n            id: node.id,\n            hidden: false\n        });\n    });\n    \n    edges.forEach(edge => {\n        edges.update({\n            id: edge.id,\n            hidden: false\n        });\n    });\n    \n    // Stabilize and fit the network\n    stabilizeNetwork().then(() => {\n        network.fit({\n            animation: {\n                duration: 300,\n                easingFunction: 'easeInOutQuad'\n            }\n        });\n        showToast('Showing all nodes and edges', 'success');\n        saveStateAfterOperation();\n    });\n}\n\n// Helper function to get original colors by node type\nfunction getNodeColorByType(type) {\n    const colorMap = {\n        'ip': '#f87171',\n        'domain': '#60a5fa',\n        'contact': '#4ade80',\n        'organization': '#facc15',\n        'port': '#a78bfa',\n        'wallet': '#fb923c',\n        'bank': '#10b981',\n        'technology': '#ec4899',\n        'device': '#14b8a6',\n        'malware': '#ef4444',\n        'vulnerability': '#dc2626',\n        'favicon': '#22d3ee',\n        'http_hash': '#f97316',\n        'html_hash': '#f59e0b',\n        'ssl_hash': '#8b5cf6',\n        'asn': '#a3e635',\n        'city': '#f97316',\n        'country': '#34d399',\n        'os': '#10b981',\n        'product': '#ec4899',\n        'http_title': '#3b82f6',\n        'vpn': '#9333ea',\n        'proxy': '#f43f5e',\n        'tor': '#64748b',\n        'relay': '#eab308',\n        'hosting': '#14b8a6',\n        'tag': '#6d28d9',\n        'cpe': '#0d9488',\n        'mx': '#34d399', // Green for MX\n        'txt': '#f59e0b'  // Orange for TXT\n    };\n    return { background: colorMap[type] || '#ffffff' };\n}\n\n// Add this with your other event listeners\ndocument.getElementById('search-input').addEventListener('keypress', function(event) {\n    if (event.key === 'Enter') {\n        event.preventDefault();\n        searchGraph();\n    }\n});\n\n\n        async function enrichAllIpinfo() {\n    if (!ipinfoApiKey && !ignoreApiKeysViaProxy) { \n        showToast('Please set your IPinfo API key in the \"API Keys\" tab first.', 'error'); \n        return; \n    }\n    \n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const ipNodes = nodes.get({ filter: n => n.type === 'ip' && n.ip });\n    const totalIPs = ipNodes.length;\n    let successfulEnrichments = 0;\n    \n    const batchSize = 50;\n    const delayBetweenBatches = 200;\n    const totalBatches = Math.ceil(totalIPs / batchSize);\n    const assumedRequestTimeMs = 100;\n    const timePerBatchMs = assumedRequestTimeMs;\n    const totalBatchDelays = (totalBatches - 1) * delayBetweenBatches;\n    const estimatedTimeMs = (timePerBatchMs * totalBatches) + totalBatchDelays + 1000;\n    \n    const estimatedSeconds = Math.ceil(estimatedTimeMs / 1000);\n    const estimatedMinutes = Math.floor(estimatedSeconds / 60);\n    const remainingSeconds = estimatedSeconds % 60;\n    const timeEstimateStr = estimatedMinutes > 0 \n        ? `${estimatedMinutes}m ${remainingSeconds}s` \n        : `${estimatedSeconds}s`;\n    \n    showToast(`Estimated time for IPinfo enrichment: ~${timeEstimateStr}`, 'info');\n    document.getElementById('progress-bar').textContent = `IPinfo Enrichment: 0/${totalIPs} IPs (0%) - Est. ${timeEstimateStr}`;\n    \n    const newNodes = [];\n    const newEdges = [];\n    let existingAsns = new Map(nodes.get({ filter: n => n.type === 'asn' }).map(n => [n.asn, n.id]));\n    let existingCities = new Map(nodes.get({ filter: n => n.type === 'city' }).map(n => [n.city, n.id]));\n    let existingOrgs = new Map(nodes.get({ filter: n => n.type === 'organization' }).map(n => [n.organization, n.id]));\n    let existingCountries = new Map(nodes.get({ filter: n => n.type === 'country' }).map(n => [n.country, n.id]));\n    let existingPrivacyTypes = new Map([\n        ['vpn', null], ['proxy', null], ['tor', null], ['relay', null], ['hosting', null]\n    ].map(([type]) => {\n        const existing = nodes.get({ filter: n => n.type === type })[0];\n        return [type, existing ? existing.id : null];\n    }));\n\n    const privacyTypes = [\n        { key: 'vpn', label: 'VPN', color: '#9333ea' },\n        { key: 'proxy', label: 'Proxy', color: '#f43f5e' },\n        { key: 'tor', label: 'Tor', color: '#64748b' },\n        { key: 'relay', label: 'Relay', color: '#eab308' },\n        { key: 'hosting', label: 'Hosting', color: '#14b8a6' }\n    ];\n\n    async function processBatch(batch) {\n        const promises = batch.map(node => {\n            if (activeTaskController && activeTaskController.signal.aborted) {\n                return Promise.resolve(null);\n            }\n            const baseUrl = ignoreApiKeysViaProxy ? \n                `https://ipinfo.io/${node.ip}/json` : \n                `https://ipinfo.io/${node.ip}/json?token=${ipinfoApiKey}`;\n            const url = constructUrl(baseUrl, !ignoreApiKeysViaProxy);\n            return fetch(url)\n                .then(response => {\n                    if (!response.ok) throw new Error('Failed to fetch IPinfo data');\n                    return response.json();\n                })\n                .then(data => {\n                    const ipNodeId = node.id;\n                    const asn = data.asn?.asn || 'Unknown ASN';\n                    const city = data.city || 'Unknown City';\n                    const companyName = data.company?.name || 'Unknown Company';\n                    const country = data.country || 'Unknown Country';\n                    const privacy = data.privacy || { vpn: false, proxy: false, tor: false, relay: false, hosting: false };\n\n                    // ASN\n                    let asnId = existingAsns.get(asn);\n                    if (!asnId) {\n                        asnId = nextId++;\n                        newNodes.push({ \n                            id: asnId, \n                            type: 'asn', \n                            label: `ASN: ${asn}`, \n                            title: `ASN: ${asn}`, \n                            color: { background: '#a3e635' }, \n                            asn \n                        });\n                        existingAsns.set(asn, asnId);\n                    }\n                    const asnEdgeId = `${ipNodeId}-${asnId}-AssignedTo`;\n                    if (!edges.get(asnEdgeId) && !newEdges.some(e => e.id === asnEdgeId)) {\n                        newEdges.push({ id: asnEdgeId, from: ipNodeId, to: asnId, label: 'Assigned to' });\n                    }\n\n                    // City\n                    let cityId = existingCities.get(city);\n                    if (!cityId) {\n                        cityId = nextId++;\n                        newNodes.push({ \n                            id: cityId, \n                            type: 'city', \n                            label: `City: ${city}`, \n                            title: `City: ${city}`, \n                            color: { background: '#f97316' }, \n                            city \n                        });\n                        existingCities.set(city, cityId);\n                    }\n                    const cityEdgeId = `${ipNodeId}-${cityId}-LocatedIn`;\n                    if (!edges.get(cityEdgeId) && !newEdges.some(e => e.id === cityEdgeId)) {\n                        newEdges.push({ id: cityEdgeId, from: ipNodeId, to: cityId, label: 'Located in' });\n                    }\n\n                    // Organization\n                    let orgId = existingOrgs.get(companyName);\n                    if (!orgId) {\n                        orgId = nextId++;\n                        newNodes.push({ \n                            id: orgId, \n                            type: 'organization', \n                            label: `Organization: ${companyName}`, \n                            title: `Company: ${companyName}`, \n                            color: { background: '#facc15' }, \n                            organization: companyName \n                        });\n                        existingOrgs.set(companyName, orgId);\n                    }\n                    const orgEdgeId = `${ipNodeId}-${orgId}-BelongsTo`;\n                    if (!edges.get(orgEdgeId) && !newEdges.some(e => e.id === orgEdgeId)) {\n                        newEdges.push({ id: orgEdgeId, from: ipNodeId, to: orgId, label: 'Belongs to' });\n                    }\n\n                    // Country\n                    let countryId = existingCountries.get(country);\n                    if (!countryId) {\n                        countryId = nextId++;\n                        newNodes.push({ \n                            id: countryId, \n                            type: 'country', \n                            label: `Country: ${country}`, \n                            title: `Country: ${country}`, \n                            color: { background: '#34d399' }, \n                            country \n                        });\n                        existingCountries.set(country, countryId);\n                    }\n                    const countryEdgeId = `${ipNodeId}-${countryId}-LocatedIn`;\n                    if (!edges.get(countryEdgeId) && !newEdges.some(e => e.id === countryEdgeId)) {\n                        newEdges.push({ id: countryEdgeId, from: ipNodeId, to: countryId, label: 'Located in' });\n                    }\n\n                    // Privacy Types\n                    privacyTypes.forEach(privacyType => {\n                        if (privacy[privacyType.key]) {\n                            let privacyNodeId = existingPrivacyTypes.get(privacyType.key);\n                            if (!privacyNodeId) {\n                                privacyNodeId = nextId++;\n                                newNodes.push({ \n                                    id: privacyNodeId, \n                                    type: privacyType.key, \n                                    label: privacyType.label, \n                                    title: privacyType.label, \n                                    color: { background: privacyType.color }\n                                });\n                                existingPrivacyTypes.set(privacyType.key, privacyNodeId);\n                            }\n                            const privacyEdgeId = `${ipNodeId}-${privacyNodeId}-Uses`;\n                            if (!edges.get(privacyEdgeId) && !newEdges.some(e => e.id === privacyEdgeId)) {\n                                newEdges.push({ id: privacyEdgeId, from: ipNodeId, to: privacyNodeId, label: 'Uses' });\n                            }\n                        }\n                    });\n\n                    successfulEnrichments++;\n                })\n                .catch(error => {\n                    console.error(`Failed to enrich IP ${node.ip}: ${error.message}`);\n                    showToast(`Failed to enrich IP ${node.ip}: ${error.message}`, 'error');\n                    return null;\n                });\n        });\n        await Promise.all(promises);\n    }\n    \n    let lastProgressUpdate = 0;\n    const progressUpdateInterval = 1000;\n    const startTime = Date.now();\n    \n    for (let i = 0; i < totalIPs; i += batchSize) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('IPinfo enrichment stopped', 'info');\n            break;\n        }\n        \n        const batch = ipNodes.slice(i, Math.min(i + batchSize, totalIPs));\n        await processBatch(batch);\n        \n        if (newNodes.length > 0) {\n            nodes.add(newNodes);\n            newNodes.length = 0;\n        }\n        if (newEdges.length > 0) {\n            edges.add(newEdges);\n            newEdges.length = 0;\n        }\n        \n        const currentTime = Date.now();\n        if (currentTime - lastProgressUpdate >= progressUpdateInterval) {\n            const processedIPs = Math.min(i + batchSize, totalIPs);\n            const progress = ((processedIPs / totalIPs) * 100).toFixed(1);\n            const remainingIPs = totalIPs - processedIPs;\n            const remainingTimeMs = Math.max(0, remainingIPs * assumedRequestTimeMs);\n            const remainingSeconds = Math.ceil(remainingTimeMs / 1000);\n            const remainingMinutes = Math.floor(remainingSeconds / 60);\n            const remainingSecondsPart = remainingSeconds % 60;\n            const remainingTimeStr = remainingMinutes > 0 \n                ? `${remainingMinutes}m ${remainingSecondsPart}s` \n                : `${remainingSeconds}s`;\n            \n            document.getElementById('progress-bar').textContent = \n                `IPinfo Enrichment: ${successfulEnrichments}/${totalIPs} IPs (${progress}%) - Est. ${remainingTimeStr} remaining`;\n            lastProgressUpdate = currentTime;\n            updateSelectOptions();\n        }\n        \n        await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));\n    }\n    \n    if (newNodes.length > 0) nodes.add(newNodes);\n    if (newEdges.length > 0) edges.add(newEdges);\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    //ensureInteractionSettings();\n    completeProgressBar();\n    showToast(`IPinfo enrichment completed: ${successfulEnrichments}/${totalIPs} IPs enriched`, 'success');\n    \n    if (window.innerWidth <= 768) {\n        const controls = document.getElementById('controls');\n        controls.classList.add('collapsed');\n        document.getElementById('myNetwork').style.display = 'block';\n        network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });\n    }\n}\n\n\n//Export to PDF\n\n\nfunction exportToPDF() {\n    network.setOptions({ physics: { enabled: false } });\n    network.stabilize(100);\n    network.fit();\n    \n    setTimeout(() => {\n        try {\n            const canvas = document.querySelector('#myNetwork .vis-network canvas');\n            if (!canvas) throw new Error('Canvas not found');\n\n            const { jsPDF } = window.jspdf;\n            const pdf = new jsPDF({\n                orientation: 'landscape',\n                unit: 'px',\n                format: [canvas.width, canvas.height]\n            });\n\n            // Add title\n            pdf.setFontSize(16);\n            pdf.text('Network Graph Export', 40, 20);\n            \n            const imgData = canvas.toDataURL('image/png');\n            pdf.addImage(imgData, 'PNG', 0, 40, canvas.width, canvas.height - 40); // Offset for title\n            pdf.save(`network_graph_${new Date().toISOString().replace(/[:.]/g, '-')}.pdf`);\n            \n            showToast('Graph exported as PDF', 'success');\n        } catch (error) {\n            console.error('Error exporting to PDF:', error);\n            showToast('Failed to export graph as PDF: ' + error.message, 'error');\n        } finally {\n            network.setOptions({ physics: { enabled: !isPhysicsPaused } });\n        }\n    }, 500);\n}\n\n// Function to trigger save after key operations\nfunction saveStateAfterOperation() {\n    saveState();\n    showToast('Progress saved', 'success');\n}\n\nwindow.onload = function() {\n    const stateLoaded = loadState();\n    if (!stateLoaded) {\n        nodes = new vis.DataSet([]);\n        edges = new vis.DataSet([]);\n        nextId = 1;\n    }\n\n    document.getElementById('ipinfoApiKey').value = ipinfoApiKey;\n    document.getElementById('shodanApiKey').value = shodanApiKey;\n    document.getElementById('corsProxyUrl').value = corsProxyUrl;\n    document.getElementById('routeViaProxy').checked = routeViaProxy;\n    document.getElementById('ignoreApiKeysViaProxy').checked = ignoreApiKeysViaProxy;\n    document.getElementById('storeIpinfoKey').checked = !!localStorage.getItem('ipinfoApiKey');\n    document.getElementById('storeShodanKey').checked = !!localStorage.getItem('shodanApiKey');\n    document.getElementById('storeCorsProxy').checked = !!localStorage.getItem('corsProxyUrl');\n\n    updateSelectOptions();\n    updateTheme();\n    \n    // Ensure interaction settings with dragView\n    network.setOptions({\n        interaction: {\n            ...baseInteractionOptions,\n            dragView: true,  // Explicitly enable dragging\n            zoomView: true,\n            zoomSpeed: 0.5\n        }\n    });\n\n    document.getElementById('menu-toggle').textContent = '<';  // Set to \"<\" since menu starts expanded\n    ensureInteractionSettings();\n    \n    container.removeEventListener('mousedown', handleMouseDown);\n    container.removeEventListener('mousemove', handleMouseMove);\n    container.removeEventListener('mouseup', handleMouseUp);\n    container.addEventListener('wheel', handleWheel, { passive: false });\n\n    if (window.innerWidth <= 768) {\n        document.getElementById('controls').classList.add('collapsed');\n    }\n\n    stabilizeNetwork().then(() => {\n        network.fit({ animation: { duration: 300 } });\n    });\n\n    let hasChanges = false;\n    nodes.on('*', () => hasChanges = true);\n    edges.on('*', () => hasChanges = true);\n    setInterval(() => {\n        if (hasChanges) {\n            saveState();\n            hasChanges = false;\n            showToast('Auto-saved', 'info');\n        }\n    }, 60 * 1000);\n};\n\n\nfunction loadState() {\n    const savedState = localStorage.getItem('networkGraphState');\n    if (!savedState) {\n        console.log('No saved state found in localStorage');\n        return false;\n    }\n\n    try {\n        const state = JSON.parse(savedState);\n        if (!state.nodes || !state.edges) return false;\n\n        nodes.clear();\n        edges.clear();\n        \n        state.nodes.forEach(node => nodes.add(node));\n        state.edges.forEach(edge => edges.add(edge));\n        \n        nextId = state.nextId || 1;\n        isDarkMode = state.isDarkMode !== undefined ? state.isDarkMode : true;\n        isPhysicsPaused = state.isPhysicsPaused || false;\n        nodeLabelsVisible = state.nodeLabelsVisible !== undefined ? state.nodeLabelsVisible : true;\n        edgeLabelsVisible = state.edgeLabelsVisible !== undefined ? state.edgeLabelsVisible : true;\n        \n        document.getElementById('showNodeLabels').checked = nodeLabelsVisible;\n        document.getElementById('showEdgeLabels').checked = edgeLabelsVisible;\n        \n        updateTheme();\n        const pauseButton = document.getElementById('pause-toggle');\n        pauseButton.textContent = isPhysicsPaused ? 'Resume Physics' : 'Pause Physics';\n        pauseButton.classList.toggle('paused', isPhysicsPaused);\n        \n         // Ensure interaction settings with dragView\n    network.setOptions({\n        interaction: {\n            ...baseInteractionOptions,\n            dragView: true,  // Explicitly enable dragging\n            zoomView: true,\n            zoomSpeed: 0.5\n        }\n    });\n\n        \n        // Re-apply wheel event listener (from previous zoom fix)\n        container.removeEventListener('wheel', handleWheel);\n        container.addEventListener('wheel', handleWheel, { passive: false });\n        \n        console.log('Loaded state with dragView enabled');\n        return true;\n    } catch (e) {\n        console.error('Error loading state:', e);\n        showToast('Failed to load saved state: ' + e.message, 'error');\n        localStorage.removeItem('networkGraphState');\n        return false;\n    }\n}\n\n\n// Separate wheel handler function for reusability //removed this as it broke pan\nfunction handleWheel(event) {\n    // event.preventDefault();\n    // const scale = event.deltaY > 0 ? 0.9 : 1.1;\n    // const currentScale = network.getScale();\n    // network.moveTo({\n    //     scale: currentScale * scale,\n    //     animation: { duration: 100 }\n    // });\n}\n\nfunction throttleRequest(fn) {\n            return async function(...args) {\n                const now = Date.now();\n                const timeSinceLast = now - lastRequestTime;\n                if (timeSinceLast < RATE_LIMIT_MS) await new Promise(resolve => setTimeout(resolve, RATE_LIMIT_MS - timeSinceLast));\n                lastRequestTime = now;\n                return fn(...args);\n            };\n        }\n\n\nconst baseInteractionOptions = {\n    dragNodes: true,\n    dragView: true,    // Explicitly enable panning\n    zoomView: true,    // Enable zooming\n    selectable: true,\n    multiselect: true,\n    hover: true,\n    zoomSpeed: 0.5\n};\n\n        let container = document.getElementById('myNetwork');\n        let data = { nodes: nodes, edges: edges };\n\n        let options = {\n    nodes: {\n        shape: 'dot',\n        size: 10,\n        font: { \n            size: nodeLabelsVisible ? 12 : 0,\n            color: '#e2e8f0',\n            multi: true,\n            align: 'center',\n            vadjust: 0\n        },\n        scaling: {\n            min: 10,\n            max: 125,\n            label: { enabled: false }\n        },\n        fixed: {\n            x: false,\n            y: false\n        },\n        shapeProperties: {\n            useBorderWithImage: false\n        },\n        chosen: {\n            label: function(values, id, selected, hovering) {\n                values.size = nodeLabelsVisible ? 12 : 0;\n            }\n        },\n        color: {\n            hover: {\n                border: '#60a5fa',\n                background: '#4b5563'\n            },\n            highlight: {\n                border: '#60a5fa',\n                background: '#4b5563'\n            }\n        }\n    },\n    edges: { \n        arrows: { to: { enabled: true, scaleFactor: 0.5 } },\n        font: { \n            size: edgeLabelsVisible ? 12 : 0,\n            color: '#e2e8f0',\n            strokeWidth: 0,\n            strokeColor: 'transparent'\n        },\n        smooth: true,\n        width: 1,\n        chosen: {\n            label: function(values, id, selected, hovering) {\n                values.size = edgeLabelsVisible ? 12 : 0;\n            }\n        },\n        color: { \n            color: '#94a3b8',\n            highlight: '#60a5fa'\n        }\n    },\n    physics: { \n        enabled: true,\n        stabilization: {\n            enabled: true,\n            iterations: 100,\n            updateInterval: 25\n        },\n        barnesHut: { \n            gravitationalConstant: -8000,\n            centralGravity: 0.1,\n            springLength: 200,\n            springConstant: 0.04,\n            damping: 0.9,\n            avoidOverlap: 0.5\n        },\n        maxVelocity: 25,\n        minVelocity: 0.1,\n        solver: 'barnesHut'\n    },\n    interaction: {\n        dragNodes: true,\n        dragView: true,    // Enable view dragging (panning)\n        zoomView: true,    // Enable zooming\n        hover: true,\n        multiselect: true,\n        selectable: true,\n        zoomSpeed: 0.5\n    },\n    layout: { improvedLayout: true }\n};\n\n        let network = new vis.Network(container, data, options);\n\nnetwork.on('selectNode', function(params) {\n    selectedNodes = new Set(params.nodes); // Sync with vis.js selection\n    console.log('Nodes selected:', [...selectedNodes]);\n});\n\nnetwork.on('deselectNode', function(params) {\n    selectedNodes = new Set(params.nodes); // Sync with vis.js selection\n    console.log('Nodes deselected, remaining:', [...selectedNodes]);\n});\n\nfunction ensureInteractionSettings() {\n    network.setOptions({\n        interaction: { \n            dragNodes: true,\n            dragView: true,    // Explicitly enable panning\n            zoomView: true,\n            selectable: true,\n            multiselect: true,\n            hover: true,\n            zoomSpeed: 0.5\n        },\n        physics: {\n            enabled: !isPhysicsPaused,\n            stabilization: { enabled: false },\n            barnesHut: {\n                gravitationalConstant: -8000,\n                centralGravity: 0.1,\n                springLength: 200,\n                springConstant: 0.04,\n                damping: 0.9,\n                avoidOverlap: 0.5\n            },\n            maxVelocity: 25,\n            minVelocity: 0.1\n        },\n        nodes: {\n            fixed: { x: false, y: false } // Ensure nodes remain movable unless explicitly fixed\n        }\n    });\n    console.log('Interaction settings reapplied with dragView enabled');\n}\n\n\nnetwork.on('dragStart', function(params) {\n    if (params.nodes.length) {\n        // Dragging a node\n        console.log('Dragging node:', params.nodes);\n    } else {\n        // Dragging background (panning)\n        console.log('Starting pan');\n        network.setOptions({\n            interaction: { dragView: true }\n        });\n    }\n});\n\nnetwork.on('dragEnd', function(params) {\n    if (params.nodes.length > 0) {\n        // Node dragging ended\n        params.nodes.forEach(nodeId => {\n            nodes.update({\n                id: nodeId,\n                fixed: { x: false, y: false }\n            });\n        });\n        console.log('Node drag ended, position updated');\n        //saveState(); // Save silently without toast\n    } else {\n        // View panning ended\n        console.log('View pan ended, no save needed');\n    }\n    ensureInteractionSettings(); // Reapply interaction settings regardless\n});\n\nfunction stabilizeNetwork(skipFit = false) {\n    return new Promise(resolve => {\n        network.setOptions({\n            physics: {\n                enabled: true,\n                stabilization: { enabled: true, iterations: 200, updateInterval: 50 }\n            }\n        });\n        network.stabilize(200);\n        let resolved = false;\n        network.once('stabilizationIterationsDone', () => {\n            if (!resolved) {\n                resolved = true;\n                finishStabilization(resolve, skipFit);\n            }\n        });\n        setTimeout(() => {\n            if (!resolved) {\n                resolved = true;\n                finishStabilization(resolve, skipFit);\n            }\n        }, 5000);\n    });\n}\n\nfunction finishStabilization(resolve, skipFit) {\n    network.setOptions({ \n        physics: { enabled: !isPhysicsPaused, stabilization: { enabled: false } }\n    });\n    ensureInteractionSettings(); // Reapply interaction settings after stabilization\n    if (!skipFit) network.fit({ animation: { duration: 500, easingFunction: 'easeInOutQuad' } });\n    resolve();\n}\n   \n\nnetwork.on('init', function() {\n            container.addEventListener('wheel', function(event) {\n                event.preventDefault();\n                const scale = event.deltaY > 0 ? 0.9 : 1.1;\n                const currentScale = network.getScale();\n                network.moveTo({\n                    scale: currentScale * scale,\n                    animation: { duration: 100 }\n                });\n            }, { passive: false });\n}\n\n);\n\nnetwork.on('oncontext', function(params) {\n    params.event.preventDefault();\n    const nodeId = this.getNodeAt(params.pointer.DOM);\n    const edgeId = this.getEdgeAt(params.pointer.DOM);\n\n    // Don't modify selectedNodes here unless it's a deliberate action\n    if (selectedNodes.size > 0) {\n        const firstNode = nodes.get([...selectedNodes][0]);\n        let value;\n        switch (firstNode.type) {\n            case 'ip': value = firstNode.ip; break;\n            case 'domain': value = firstNode.domain; break;\n            default: value = firstNode.label;\n        }\n        showContextMenu(params.pointer.DOM.x, params.pointer.DOM.y, value, [...selectedNodes], firstNode.type);\n    } else if (nodeId) {\n        // Fallback for single node if no prior selection\n        const node = nodes.get(nodeId);\n        let value;\n        switch (node.type) {\n            case 'ip': value = node.ip; break;\n            case 'domain': value = node.domain; break;\n            default: value = node.label;\n        }\n        showContextMenu(params.pointer.DOM.x, params.pointer.DOM.y, value, [nodeId], node.type);\n    } else if (edgeId) {\n        showEdgeContextMenu(params.pointer.DOM.x, params.pointer.DOM.y, edgeId);\n    }\n});\n\nnetwork.on('click', function(params) {\n\n    if (params.nodes.length === 0) {\n        // Empty space clicked, prevent default zooming\n        params.event.preventDefault();\n        console.log('Empty space clicked, no action taken');\n        return;\n    }\n    // Only proceed if exactly one node is selected and not dragging\n    if (params.nodes.length !== 1 || params.event.type === 'drag') {\n        console.log('Click event ignored:', params);\n        return;\n    }\n\n    const nodeId = params.nodes[0];\n    const node = nodes.get(nodeId);\n\n    // Verify node exists\n    if (!node) {\n        console.error('Node not found for ID:', nodeId);\n        showToast('Node not found', 'error');\n        return;\n    }\n\n    // Log the node for debugging\n    console.log('Clicked node:', JSON.stringify(node, null, 2));\n\n    // Determine the value to copy based on node type\n    let valueToCopy;\n    switch (node.type) {\n        case 'ip':\n            valueToCopy = node.ip;\n            break;\n        case 'domain':\n            valueToCopy = node.domain;\n            break;\n        case 'url':\n            valueToCopy = node.url;\n            break;\n        case 'contact':\n            valueToCopy = node.email || node.name;\n            break;\n        case 'organization':\n            valueToCopy = node.organization;\n            break;\n        case 'port':\n            valueToCopy = `${node.portType}/${node.portNumber}`;\n            break;\n        case 'wallet':\n            valueToCopy = node.address;\n            break;\n        case 'bank':\n            valueToCopy = node.accountNumber;\n            break;\n        case 'technology':\n            valueToCopy = node.techName;\n            break;\n        case 'device':\n            valueToCopy = node.deviceName;\n            break;\n        case 'malware':\n            valueToCopy = node.malwareName;\n            break;\n        case 'vulnerability':\n            valueToCopy = node.cve || node.vulnName;\n            break;\n        case 'favicon':\n        case 'http_hash':\n        case 'html_hash':\n        case 'ssl_hash':\n        case 'hash': // New case for file hashes\n            valueToCopy = node.hash; // Use full hash value\n            break;\n        case 'asn':\n            valueToCopy = node.asn;\n            break;\n        case 'city':\n            valueToCopy = node.city;\n            break;\n        case 'country':\n            valueToCopy = node.country;\n            break;\n        case 'os':\n            valueToCopy = node.os;\n            break;\n        case 'product':\n            valueToCopy = node.product;\n            break;\n        case 'http_title':\n            valueToCopy = node.title;\n            break;\n        case 'vpn':\n        case 'proxy':\n        case 'tor':\n        case 'relay':\n        case 'hosting':\n            valueToCopy = node.value;\n            break;\n        case 'hash': // Add this case for file hashes\n            if (!node.hash) return;\n            nodeData.hash = node.hash;\n            nodeData.hashType = node.hashType || 'Unknown';\n            nodeData.label = `${node.hashType}: ${node.hash.substring(0, 8)}...`;\n            nodeData.title = `File Hash\\nType: ${node.hashType}\\nValue: ${node.hash}${node.notes ? '\\nNotes: ' + node.notes : ''}`;\n            nodeData.color.background = node.color?.background || '#f97316';\n            break;\n        case 'txt': // New case for TXT records\n            valueToCopy = node.text;\n            break;\n        default:\n            valueToCopy = node.label.split('\\n')[0].split(': ')[1] || node.label.split('\\n')[0];\n            console.warn(`Unhandled node type \"${node.type}\", using fallback value:`, valueToCopy);\n    }\n\n    // Check if a valid value was found\n    if (!valueToCopy) {\n        console.warn('No valid value to copy for node:', node);\n        showToast(`No value available to copy for ${node.type}`, 'warning');\n        return;\n    }\n\n    // Log the value being copied for debugging\n    console.log('Value to copy:', valueToCopy);\n\n    // Attempt to copy to clipboard\n    navigator.clipboard.writeText(valueToCopy)\n        .then(() => {\n            showToast(`Copied \"${valueToCopy}\" to clipboard`, 'success');\n        })\n        .catch(err => {\n            console.error('Failed to copy to clipboard:', err);\n            showToast(`Failed to copy: ${err.message}`, 'error');\n        });\n});\n\nfunction showEdgeContextMenu(x, y, edgeId) {\n    const menu = document.getElementById('edgeContextMenu');\n    const edge = edges.get(edgeId);\n    if (!edge) return;\n    \n    const menuHtml = `\n        <button onclick=\"editEdgeLabel('${edgeId}')\">Edit Label</button>\n        <button onclick=\"removeEdgeDirect('${edgeId}')\">Delete Edge</button>\n    `;\n    \n    menu.innerHTML = menuHtml;\n    const canvasOffset = container.getBoundingClientRect();\n    menu.style.left = `${x + canvasOffset.left}px`;\n    menu.style.top = `${y + canvasOffset.top}px`;\n    menu.style.display = 'block';\n    document.addEventListener('click', hideEdgeContextMenu);\n}\n\nfunction hideEdgeContextMenu() {\n    document.getElementById('edgeContextMenu').style.display = 'none';\n    document.removeEventListener('click', hideEdgeContextMenu);\n}\n\nfunction editEdgeLabel(edgeId) {\n    const edge = edges.get(edgeId);\n    if (!edge) {\n        showToast('Edge not found', 'error');\n        return;\n    }\n    \n    const currentLabel = edge.label || '';\n    const newLabel = prompt('Enter new edge label (leave empty to remove):', currentLabel);\n    \n    if (newLabel !== null) { // null means user cancelled\n        edges.update({\n            id: edgeId,\n            label: newLabel.trim() === '' ? undefined : newLabel.trim()\n            \n        });\n        updateEdgeSelectOptions();\n        stabilizeNetwork();\n        saveStateAfterOperation();\n        showToast('Edge label updated', 'success');\n    }\n    \n    hideEdgeContextMenu();\n}\n\nfunction removeEdgeDirect(edgeId) {\n    const edge = edges.get(edgeId);\n    if (!edge) {\n        showToast('Edge not found', 'error');\n        return;\n    }\n    \n    edges.remove({ id: edgeId });\n    updateNodeSizes();\n    updateEdgeSelectOptions();\n    stabilizeNetwork();\n    saveStateAfterOperation();\n    showToast('Edge removed', 'success');\n    hideEdgeContextMenu();\n}\n\n// show context menu on right click\nfunction showContextMenu(x, y, value, nodeIds, type) {\n    const nodesArray = Array.isArray(nodeIds) ? nodeIds : [nodeIds];\n    const menu = document.getElementById('contextMenu');\n    const isMultiple = nodesArray.length > 1;\n    \n    let menuHtml = `\n        <button onclick=\"deleteNodes([${nodesArray.join(',')}])\">Delete ${isMultiple ? 'Selected Nodes' : 'Node'}</button>\n        ${isMultiple ? '' : `<button onclick=\"startLinkCreation(${nodesArray[0]})\">Create Link From Here</button>`}\n        ${isMultiple ? '' : `<button onclick=\"showPropertiesPanel(${nodesArray[0]}); hideContextMenu();\">View Node Properties</button>`}\n        ${isMultiple ? '' : `<button onclick=\"editNodeNotes(${nodesArray[0]}); hideContextMenu();\">Add/Edit Notes</button>`}\n    `;\n    \n    // Check if all selected nodes are of the same type\n    const allSameType = nodesArray.every(id => {\n        const node = nodes.get(id);\n        return node && node.type === type;\n    });\n    \n    if (allSameType) {\n        if (type === 'ip') {\n            menuHtml += `\n                <button onclick=\"throttledEnrichIPMultiple([${nodesArray.map(id => `'${nodes.get(id).ip}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])\">Enrich via IPINFO</button>\n                <button onclick=\"throttledEnrichShodanMultiple([${nodesArray.map(id => `'${nodes.get(id).ip}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])\">Enrich via Shodan</button>\n                <button onclick=\"throttledEnrichInternetDBMultiple([${nodesArray.map(id => `'${nodes.get(id).ip}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])\">Enrich via InternetDB</button>\n                <button onclick=\"throttledEnrichGreyNoiseMultiple([${nodesArray.map(id => `'${nodes.get(id).ip}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])\">Enrich via GreyNoise</button>\n                <button onclick=\"throttledSendHttpsRequestMultiple([${nodesArray.map(id => `'${nodes.get(id).ip}'`).join(',')}], 'ip', 'https')\">Send HTTPS Request</button>\n                <button onclick=\"throttledSendHttpsRequestMultiple([${nodesArray.map(id => `'${nodes.get(id).ip}'`).join(',')}], 'ip', 'http')\">Send HTTP Request</button>\n                <button onclick=\"throttledEnrichURLscan('${nodes.get(nodesArray[0]).ip}', ${nodesArray[0]})\">Enrich via URLscan.io</button>\n            `;\n        } else if (type === 'domain') {\n            menuHtml += `\n                <button onclick=\"throttledEnrichGoogleDNSMultiple([${nodesArray.map(id => `'${nodes.get(id).domain}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])\">Enrich via Google DNS</button>\n                <button onclick=\"throttledEnrichGoogleDNSMXMultiple([${nodesArray.map(id => `'${nodes.get(id).domain}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])\">Enrich via Google DNS (MX)</button>\n                <button onclick=\"throttledEnrichGoogleDNSTXTMultiple([${nodesArray.map(id => `'${nodes.get(id).domain}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])\">Enrich via Google DNS (TXT)</button>\n                <button onclick=\"throttledEnrichShodanMultiple([${nodesArray.map(id => `'${nodes.get(id).domain}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])\">Enrich via Shodan</button>\n                <button onclick=\"throttledEnrichHudsonRockDomainMultiple([${nodesArray.map(id => `'${nodes.get(id).domain}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])\">Enrich via Hudson Rock</button>\n                <button onclick=\"throttledSendHttpsRequestMultiple([${nodesArray.map(id => `'${nodes.get(id).domain}'`).join(',')}], 'domain', 'https')\">Send HTTPS Request</button>\n                <button onclick=\"throttledSendHttpsRequestMultiple([${nodesArray.map(id => `'${nodes.get(id).domain}'`).join(',')}], 'domain', 'http')\">Send HTTP Request</button>\n                <button onclick=\"throttledEnrichURLscan('https://${nodes.get(nodesArray[0]).domain}', ${nodesArray[0]})\">Enrich via URLscan.io</button>\n                <button onclick=\"throttledEnrichSecurityTrailsDomainMultiple([${nodesArray.map(id => `'${nodes.get(id).domain}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])\">Enrich via SecurityTrails</button>\n            `;\n        } else if (type === 'html_hash') {\n            menuHtml += `\n                <button onclick=\"throttledSearchShodanHtmlHash('${nodesArray[0]}', '${nodes.get(nodesArray[0]).hash}')\">Search Shodan for IPs with this HTML Hash</button>\n            `;\n        } else if (type === 'contact') {\n            menuHtml += `\n                <button onclick=\"throttledEnrichHudsonRockMultiple([${nodesArray.map(id => `'${nodes.get(id).email}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])\">Enrich via Hudson Rock</button>\n            `;\n        }\n    } else if (type === 'url') {\n            menuHtml += `\n                <button onclick=\"throttledEnrichURLhausMultiple([${nodesArray.map(id => `'${nodes.get(id).url}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])\">Enrich via URLhaus</button>\n            `;\n        \n    }\n\n    menu.innerHTML = menuHtml;\n    const canvasOffset = container.getBoundingClientRect();\n    menu.style.left = `${x + canvasOffset.left}px`;\n    menu.style.top = `${y + canvasOffset.top}px`;\n    menu.style.display = 'block';\n    document.addEventListener('click', hideContextMenu);\n}\n\n\n// why is this here?\nlet linkFromNode = null;\n// set variable above - move to top\n\nfunction saveState() {\n    try {\n        const state = {\n            nodes: nodes.get().map(node => {\n                // Base properties common to all nodes\n                const nodeData = {\n                    id: node.id,\n                    type: node.type,\n                    label: node.label,\n                    title: node.title,\n                    color: node.color,\n                    x: node.x,\n                    y: node.y,\n                    size: node.size,\n                    originalLabel: node.originalLabel,\n                    notes: node.notes // Include notes for all types\n                };\n                if (node.type === 'subnet') {\n                nodeData.subnet = node.subnet;\n                nodeData.isPrivate = node.isPrivate;\n            }\n                // Add type-specific properties\n                switch (node.type) {\n                    case 'contact':\n                        nodeData.name = node.name;\n                        nodeData.email = node.email;\n                        break;\n                    case 'ip':\n                        nodeData.ip = node.ip;\n                        break;\n                    case 'domain':\n                        nodeData.domain = node.domain;\n                        break;\n                    case 'organization':\n                        nodeData.organization = node.organization;\n                        break;\n                    case 'port':\n                        nodeData.portType = node.portType;\n                        nodeData.portNumber = node.portNumber;\n                        break;\n                    case 'wallet':\n                        nodeData.address = node.address;\n                        break;\n                    case 'bank':\n                        nodeData.accountNumber = node.accountNumber;\n                        nodeData.sortCode = node.sortCode;\n                        break;\n                    case 'technology':\n                        nodeData.techName = node.techName;\n                        nodeData.techVersion = node.techVersion;\n                        break;\n                    case 'device':\n                        nodeData.deviceCategory = node.deviceCategory;\n                        nodeData.deviceName = node.deviceName;\n                        break;\n                    case 'malware':\n                        nodeData.malwareName = node.malwareName;\n                        nodeData.malwareType = node.malwareType;\n                        break;\n                    case 'vulnerability':\n                        nodeData.vulnName = node.vulnName;\n                        nodeData.cve = node.cve;\n                        nodeData.url = node.url;\n                        break;\n                    case 'favicon':\n                        nodeData.hash = node.hash;\n                        nodeData.location = node.location;\n                        break;\n                    case 'http_hash':\n                    case 'html_hash':\n                    case 'ssl_hash':\n                        nodeData.hash = node.hash;\n                        break;\n                    case 'asn':\n                        nodeData.asn = node.asn;\n                        break;\n                    case 'city':\n                        nodeData.city = node.city;\n                        break;\n                    case 'country':\n                        nodeData.country = node.country;\n                        break;\n                    case 'os':\n                        nodeData.os = node.os;\n                        break;\n                    case 'product':\n                        nodeData.product = node.product;\n                        break;\n                    case 'http_title':\n                        nodeData.title = node.title; // Already in base properties, but explicit for clarity\n                        break;\n                    case 'vpn':\n                    case 'proxy':\n                    case 'tor':\n                    case 'relay':\n                    case 'hosting':\n                        nodeData.value = node.value;\n                        break;\n                    case 'tag':\n                        nodeData.tag = node.tag;\n                        break;\n                    case 'cpe':\n                        nodeData.cpe = node.cpe;\n                        break;\n                    case 'service':\n                        nodeData.name = node.name;\n                        break;\n                    case 'timestamp':\n                        nodeData.timestamp = node.timestamp;\n                        break;\n                    case 'url':\n                        nodeData.url = node.url;\n                        break;\n                    case 'port_title':\n                        nodeData.portType = node.portType;\n                        nodeData.portNumber = node.portNumber;\n                        nodeData.title = node.title;\n                        break;\n                    case 'hash': // Add this case for file hashes\n                        nodeData.hash = node.hash;\n                        nodeData.hashType = node.hashType;\n                        break;\n                    case 'txt':\n                        nodeData.text = node.text;\n                        break;\n                    default:\n                        console.warn(`Unhandled node type in saveState: ${node.type}`);\n                }\n\n                return nodeData;\n            }),\n            edges: edges.get().map(edge => ({\n                id: edge.id,\n                from: edge.from,\n                to: edge.to,\n                label: edge.label,\n                originalLabel: edge.originalLabel\n            })),\n            nextId: nextId,\n            isDarkMode: isDarkMode,\n            isPhysicsPaused: isPhysicsPaused,\n            nodeLabelsVisible: nodeLabelsVisible,\n            edgeLabelsVisible: edgeLabelsVisible\n        };\n\n        localStorage.setItem('networkGraphState', JSON.stringify(state));\n        console.log('State saved successfully');\n    } catch (e) {\n        console.error('Failed to save state:', e);\n        showToast('Failed to save state: ' + e.message, 'error');\n    }\n}\n\n\n\nconst throttledSearchShodanHtmlHash = throttleRequest(async function searchShodanHtmlHash(htmlHashNodeId, htmlHash, signal) {\n    if (!shodanApiKey) {\n        showToast('Please set your Shodan API key in the \"API Keys\" tab first.', 'error');\n        return;\n    }\n\n    // Validate htmlHash\n    const hashNum = parseInt(htmlHash);\n    if (!Number.isInteger(hashNum)) {\n        showToast(`Invalid HTML Hash: ${htmlHash}. Must be an integer.`, 'error');\n        return;\n    }\n\n    network.setOptions({ physics: { enabled: false } });\n    showProgressBar();\n    document.getElementById('progress-bar').textContent = `Searching Shodan for IPs with HTML Hash ${htmlHash.substring(0, 8)}...`;\n\n    try {\n        // Construct the URL to match the Bash script exactly\n        const query = `http.html_hash:${hashNum}`; // No encoding here yet\n        const baseUrl = `https://api.shodan.io/shodan/host/search?key=${shodanApiKey}&query=${query}`;\n        const url = routeViaProxy ? `${corsProxyUrl}/${baseUrl}` : baseUrl;\n\n        console.log('Search Shodan Base URL (before proxy):', baseUrl);\n        console.log('Search Shodan Final URL:', url);\n        console.log('routeViaProxy:', routeViaProxy, 'corsProxyUrl:', corsProxyUrl);\n\n        const response = await fetch(url, {\n            method: 'GET',\n            signal: signal,\n            headers: {\n                'Accept': 'application/json' // Ensure JSON response\n            }\n        });\n\n        if (!response.ok) {\n            const errorText = await response.text();\n            console.log('Search Shodan Status:', response.status, response.statusText);\n            console.log('Search Shodan Response Body:', errorText);\n            console.log('Request Headers:', Object.fromEntries(response.headers.entries()));\n            throw new Error(`Failed to fetch Shodan data: ${response.statusText} - ${errorText}`);\n        }\n\n        const data = await response.json();\n        console.log('Shodan Response:', data);\n\n        if (!data.matches || data.matches.length === 0) {\n            showToast(`No IPs found on Shodan with HTML Hash ${htmlHash}`, 'info');\n            completeProgressBar();\n            await stabilizeNetwork();\n            return;\n        }\n\n        // Process the matches\n        const newNodes = [];\n        const newEdges = [];\n        let successfulAdditions = 0;\n        const totalMatches = data.matches.length;\n\n        const existingIPs = new Map(nodes.get({ filter: n => n.type === 'ip' }).map(n => [n.ip, n.id]));\n\n        for (const match of data.matches) {\n            if (activeTaskController && activeTaskController.signal.aborted) {\n                showToast('Shodan HTML Hash search stopped', 'info');\n                break;\n            }\n\n            const ip = match.ip_str;\n            let ipId = existingIPs.get(ip);\n\n            if (!ipId) {\n                ipId = nextId++;\n                newNodes.push({\n                    id: ipId,\n                    type: 'ip',\n                    label: `IP: ${ip}`,\n                    title: `IP Address: ${ip}\\nFrom Shodan HTML Hash Search`,\n                    color: { background: '#f87171' },\n                    ip: ip,\n                    size: 20\n                });\n                existingIPs.set(ip, ipId);\n            }\n\n            const edgeId = `${ipId}-${htmlHashNodeId}-SharesHash`;\n            if (!edges.get(edgeId) && !newEdges.some(e => e.id === edgeId)) {\n                newEdges.push({\n                    id: edgeId,\n                    from: ipId,\n                    to: htmlHashNodeId,\n                    label: 'Shares HTML Hash'\n                });\n            }\n\n            successfulAdditions++;\n            const progress = ((successfulAdditions / totalMatches) * 100).toFixed(1);\n            document.getElementById('progress-bar').textContent = `Shodan HTML Hash Search: ${successfulAdditions}/${totalMatches} IPs (${progress}%)`;\n        }\n\n        if (newNodes.length > 0) nodes.add(newNodes);\n        if (newEdges.length > 0) edges.add(newEdges);\n\n        updateNodeSizes();\n        updateSelectOptions();\n        await stabilizeNetwork();\n        //ensureInteractionSettings();\n\n        completeProgressBar();\n        showToast(`Found and added ${successfulAdditions} IPs with HTML Hash ${htmlHash.substring(0, 8)}...`, 'success');\n    } catch (error) {\n        if (error.name === 'AbortError') {\n            showToast('Shodan HTML Hash search was cancelled', 'info');\n        } else {\n            console.error(`Error searching Shodan for HTML Hash ${htmlHash}: ${error.message}`);\n            showToast(`Error searching Shodan: ${error.message}. Check CORS proxy in Config tab.`, 'error');\n        }\n        completeProgressBar();\n        await stabilizeNetwork();\n    }\n}, SHODAN_RATE_LIMIT_MS);\n\n\n\nfunction cancelLinkCreation() {\n    if (linkFromNode) {\n        // Reset visual feedback\n        const originalNode = nodes.get(linkFromNode);\n        nodes.update({\n            id: linkFromNode,\n            color: { border: isDarkMode ? '#94a3b8' : '#6b7280', background: originalNode.color.background }\n        });\n        \n        showToast('Link creation cancelled', 'info');\n        linkFromNode = null;\n        \n        // Remove temporary listeners\n        network.off('oncontext', handleLinkDestination);\n        network.off('click', cancelLinkCreation);\n    }\n}\n\n\n        function hideContextMenu() {\n            document.getElementById('contextMenu').style.display = 'none';\n            document.removeEventListener('click', hideContextMenu);\n        }\n\n        const throttledSendHttpsRequest = throttleRequest(async function sendHttpsRequest(target, type) {\n    network.setOptions({ physics: { enabled: false } });\n    let url = type === 'ip' ? \n        `https://${target}` : \n        `https://${target}`;\n    url = constructUrl(url);\n    let message;\n\n    try {\n        const response = await fetch(url, {\n            method: 'GET',\n            headers: {\n                'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',\n                'Origin': window.location.origin\n            },\n            mode: 'cors',\n            credentials: 'omit'\n        });\n\n        const status = response.status;\n        const statusText = response.statusText;\n        const headers = {};\n        response.headers.forEach((value, key) => {\n            headers[key] = value;\n        });\n\n        let body = '';\n        const contentType = headers['content-type'] || '';\n        if (contentType.includes('text') || contentType.includes('html') || contentType.includes('json')) {\n            body = await response.text();\n            if (body.length > 500) {\n                body = body.substring(0, 500) + '... (truncated)';\n            }\n        } else {\n            body = '(Binary or unsupported content type)';\n        }\n\n        message = `\n            HTTPS Request to https://${target}\n            Status: ${status} ${statusText}\n            Headers: ${JSON.stringify(headers, null, 2)}\n            Body: ${body}\n        `.trim();\n        showToast(message, 'success');\n    } catch (error) {\n        message = `\n            HTTPS Request to https://${target}\n            Error: ${error.message}\n            Note: Ensure the CORS proxy (${corsProxyUrl}) is active and correctly configured\n        `.trim();\n        showToast(message, 'error');\n    } finally {\n        await stabilizeNetwork();\n    }\n});\n\n        function saveCorsProxyUrl() {\n    corsProxyUrl = document.getElementById('corsProxyUrl').value.trim();\n    const storeProxy = document.getElementById('storeCorsProxy').checked;\n    routeViaProxy = document.getElementById('routeViaProxy').checked;\n    ignoreApiKeysViaProxy = document.getElementById('ignoreApiKeysViaProxy').checked;\n\n    if (corsProxyUrl) {\n        if (storeProxy) {\n            localStorage.setItem('corsProxyUrl', corsProxyUrl);\n            localStorage.setItem('routeViaProxy', routeViaProxy);\n            localStorage.setItem('ignoreApiKeysViaProxy', ignoreApiKeysViaProxy);\n            showToast('CORS Proxy settings saved successfully!', 'success');\n        } else {\n            localStorage.removeItem('corsProxyUrl');\n            localStorage.removeItem('routeViaProxy');\n            localStorage.removeItem('ignoreApiKeysViaProxy');\n            showToast('CORS Proxy settings set for this session only', 'success');\n        }\n    } else {\n        corsProxyUrl = 'http://localhost:3000/proxy?url=';\n        routeViaProxy = false;\n        ignoreApiKeysViaProxy = false;\n        localStorage.removeItem('corsProxyUrl');\n        localStorage.removeItem('routeViaProxy');\n        localStorage.removeItem('ignoreApiKeysViaProxy');\n        document.getElementById('corsProxyUrl').value = corsProxyUrl;\n        document.getElementById('routeViaProxy').checked = false;\n        document.getElementById('ignoreApiKeysViaProxy').checked = false;\n        showToast('CORS Proxy settings reset to default', 'info');\n    }\n}\n\nfunction constructUrl(baseUrl, useApiKey = true) {\n    if (routeViaProxy) {\n        // Ensure corsProxyUrl doesn't end with a slash and baseUrl doesn't start with one\n        const cleanProxyUrl = corsProxyUrl.replace(/\\/+$/, ''); // Remove trailing slashes\n        const cleanBaseUrl = baseUrl.replace(/^\\/+/, '');      // Remove leading slashes\n        return `${cleanProxyUrl}${cleanBaseUrl}`;\n    }\n    return useApiKey ? baseUrl : baseUrl.split('?')[0]; // Remove query params if ignoring API keys\n}\n\n function showToast(message, type = 'info') {\n    console.log('Showing toast:', message, type);\n    const topBarHeight = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--top-bar-height')) || 40;\n    const buffer = 20;\n    const toastOptions = {\n        text: message,\n        duration: 6500,\n        position: \"center\",\n        style: {\n            background: isDarkMode ? '#2d3748' : '#fff',\n            color: isDarkMode ? '#e2e8f0' : '#1f2a44',\n            border: `1px solid ${isDarkMode ? '#4b5563' : '#d1d5db'}`,\n            boxShadow: isDarkMode ? '0 2px 10px rgba(0, 0, 0, 0.3)' : '0 2px 10px rgba(0, 0, 0, 0.1)',\n            position: 'fixed',\n            top: `${topBarHeight + buffer}px`,\n            left: '50%',\n            transform: 'translateX(-50%)',\n            width: 'auto',\n            maxWidth: '80%',\n            zIndex: 3000\n        }\n    };\n    switch (type) {\n        case 'success': toastOptions.style.background = isDarkMode ? '#166534' : '#22c55e'; toastOptions.style.color = '#fff'; break;\n        case 'error': toastOptions.style.background = isDarkMode ? '#991b1b' : '#ef4444'; toastOptions.style.color = '#fff'; break;\n    }\n    const toast = Toastify(toastOptions);\n    console.log('Toast instance created:', toast);\n    setTimeout(() => {\n        toast.showToast();\n        console.log('Toast should now be visible');\n    }, 100); // Small delay to ensure rendering\n}\n\nasync function stabilizeNetwork(skipFit = false) {\n    return new Promise(resolve => {\n        network.setOptions({\n            physics: {\n                enabled: true,\n                stabilization: {\n                    enabled: true,\n                    iterations: 200,\n                    updateInterval: 50\n                },\n                barnesHut: {\n                    gravitationalConstant: -8000,\n                    centralGravity: 0.3,\n                    springLength: 150,\n                    avoidOverlap: 1.0,\n                    damping: 0.9\n                }\n            },\n            // Explicitly preserve interaction settings\n            interaction: { \n                ...baseInteractionOptions,\n                zoomView: true,  // Ensure zooming is enabled\n                //dragView: true,\n                zoomSpeed: 0.5\n            }\n        });\n\n        network.stabilize(200);\n        network.once('stabilizationIterationsDone', () => {\n            network.setOptions({\n                physics: {\n                    enabled: !isPhysicsPaused,\n                    stabilization: { enabled: false }\n                },\n                interaction: { \n                    ...baseInteractionOptions,\n                    zoomView: true,\n                    //dragView: true,\n                    zoomSpeed: 0.5\n                }\n            });\n\n            if (!skipFit) {\n                network.fit({\n                    animation: {\n                        duration: 500,\n                        easingFunction: 'easeInOutQuad'\n                    }\n                });\n            }\n\n            setTimeout(() => {\n                const boundingBox = network.getBoundingBox();\n                if (boundingBox && nodes.length > 0) {\n                    const margin = 50;\n                    nodes.forEach(node => {\n                        const { x, y } = network.getPositions([node.id])[node.id] || { x: 0, y: 0 };\n                        nodes.update({\n                            id: node.id,\n                            x: Math.max(boundingBox.left + margin, Math.min(boundingBox.right - margin, x)),\n                            y: Math.max(boundingBox.top + margin, Math.min(boundingBox.bottom - margin, y))\n                        });\n                    });\n                }\n                resolve();\n                \n            }, skipFit ? 0 : 550);\n            ensureInteractionSettings(); // Ensure panning is enabled\n        });\n    });\n}\n\n\n\nconst throttledEnrichInternetDB = throttleRequest(async function enrichInternetDB(ip, ipNodeId, isBulk = false) {\n    network.setOptions({ physics: { enabled: false } });\n    try {\n        const url = constructUrl(`https://internetdb.shodan.io/${ip}`);\n        const response = await fetch(url);\n        if (!response.ok) throw new Error('Failed to fetch InternetDB data');\n        const data = await response.json();\n\n        // Deduplication maps\n        const existingPorts = new Map(nodes.get({ filter: n => n.type === 'port' }).map(n => [`${n.portType}/${n.portNumber}`, n.id]));\n        const existingDomains = new Map(nodes.get({ filter: n => n.type === 'domain' }).map(n => [n.domain, n.id]));\n        const existingVulns = new Map(nodes.get({ filter: n => n.type === 'vulnerability' }).map(n => [n.cve, n.id]));\n        const existingTags = new Map(nodes.get({ filter: n => n.type === 'tag' }).map(n => [n.tag, n.id]));\n        const existingCPEs = new Map(nodes.get({ filter: n => n.type === 'cpe' }).map(n => [n.cpe, n.id]));\n\n        const newNodes = [];\n        const newEdges = [];\n\n        // Ports (existing logic with deduplication)\n        if (data.ports && data.ports.length > 0) {\n            data.ports.forEach(port => {\n                const portKey = `TCP/${port}`;\n                let portId = existingPorts.get(portKey);\n                if (!portId) {\n                    portId = nextId++;\n                    newNodes.push({ \n                        id: portId, \n                        type: 'port', \n                        label: `TCP/${port}`, \n                        title: `Port\\nType: TCP\\nNumber: ${port}`, \n                        color: { background: '#a78bfa' }, \n                        portType: 'TCP', \n                        portNumber: port.toString() \n                    });\n                    existingPorts.set(portKey, portId);\n                }\n                const edgeId = `${ipNodeId}-${portId}-Exposes`;\n                if (!edges.get(edgeId) && !newEdges.some(e => e.id === edgeId)) {\n                    newEdges.push({ id: edgeId, from: ipNodeId, to: portId, label: 'Exposes' });\n                }\n            });\n        }\n\n        // Hostnames (existing logic with deduplication)\n        if (data.hostnames && data.hostnames.length > 0) {\n            data.hostnames.forEach(hostname => {\n                let domainId = existingDomains.get(hostname);\n                if (!domainId) {\n                    domainId = nextId++;\n                    newNodes.push({ \n                        id: domainId, \n                        type: 'domain', \n                        label: hostname, \n                        title: `Domain: ${hostname}`, \n                        color: { background: '#60a5fa' }, \n                        domain: hostname \n                    });\n                    existingDomains.set(hostname, domainId);\n                }\n                const edgeId = `${ipNodeId}-${domainId}-ResolvesTo`;\n                if (!edges.get(edgeId) && !newEdges.some(e => e.id === edgeId)) {\n                    newEdges.push({ id: edgeId, from: ipNodeId, to: domainId, label: 'Resolves to' });\n                }\n            });\n        }\n\n        // Vulnerabilities (existing logic with deduplication)\n        if (data.cves && data.cves.length > 0) {\n            data.cves.forEach(cve => {\n                let cveId = existingVulns.get(cve);\n                if (!cveId) {\n                    cveId = nextId++;\n                    newNodes.push({ \n                        id: cveId, \n                        type: 'vulnerability', \n                        label: `Vulnerability: ${cve}`, \n                        title: `Vulnerability\\nName: ${cve}\\nCVE: ${cve}`, \n                        color: { background: '#dc2626' }, \n                        vulnName: cve, \n                        cve: cve,\n                        url: `https://nvd.nist.gov/vuln/detail/${cve}`\n                    });\n                    existingVulns.set(cve, cveId);\n                }\n                const edgeId = `${ipNodeId}-${cveId}-Has`;\n                if (!edges.get(edgeId) && !newEdges.some(e => e.id === edgeId)) {\n                    newEdges.push({ id: edgeId, from: ipNodeId, to: cveId, label: 'Has' });\n                }\n            });\n        }\n\n        // Tags (new)\n        if (data.tags && data.tags.length > 0) {\n            data.tags.forEach(tag => {\n                let tagId = existingTags.get(tag);\n                if (!tagId) {\n                    tagId = nextId++;\n                    newNodes.push({\n                        id: tagId,\n                        type: 'tag',\n                        label: `Tag: ${tag}`,\n                        title: `Tag: ${tag}`,\n                        color: { background: '#6d28d9' },\n                        tag: tag\n                    });\n                    existingTags.set(tag, tagId);\n                }\n                const edgeId = `${ipNodeId}-${tagId}-Tagged`;\n                if (!edges.get(edgeId) && !newEdges.some(e => e.id === edgeId)) {\n                    newEdges.push({ id: edgeId, from: ipNodeId, to: tagId, label: 'Tagged' });\n                }\n            });\n        }\n\n        // CPEs (new)\n        if (data.cpes && data.cpes.length > 0) {\n            data.cpes.forEach(cpe => {\n                let cpeId = existingCPEs.get(cpe);\n                if (!cpeId) {\n                    cpeId = nextId++;\n                    newNodes.push({\n                        id: cpeId,\n                        type: 'cpe',\n                        label: `CPE: ${cpe.split(':')[3] || cpe}`,\n                        title: `CPE: ${cpe}`,\n                        color: { background: '#0d9488' },\n                        cpe: cpe\n                    });\n                    existingCPEs.set(cpe, cpeId);\n                }\n                const edgeId = `${ipNodeId}-${cpeId}-Runs`;\n                if (!edges.get(edgeId) && !newEdges.some(e => e.id === edgeId)) {\n                    newEdges.push({ id: edgeId, from: ipNodeId, to: cpeId, label: 'Runs' });\n                }\n            });\n        }\n\n        // Batch update\n        if (newNodes.length > 0) nodes.add(newNodes);\n        if (newEdges.length > 0) edges.add(newEdges);\n\n        updateNodeSizes();\n        updateSelectOptions();\n        await stabilizeNetwork();\n        //ensureInteractionSettings();\n        if (!isBulk) showToast(`IP ${ip} enrichment completed using InternetDB`, 'success');\n    } catch (error) {\n        console.error(`Error enriching IP ${ip} with InternetDB: ${error.message}`);\n        showToast(`Error enriching IP ${ip} with InternetDB: ${error.message}`, 'error');\n        await stabilizeNetwork();\n    }\n});\n\n \nasync function enrichAllShodan() {\n    if (!shodanApiKey && !ignoreApiKeysViaProxy) { \n        showToast('Please set your Shodan API key in the \"API Keys\" tab first.', 'error'); \n        return; \n    }\n    \n    showProgressBar();\n    const progressBar = document.getElementById('progress-bar');\n    network.setOptions({ physics: { enabled: false } });\n    const ipNodes = nodes.get({ filter: n => n.type === 'ip' && n.ip });\n    const totalIPs = ipNodes.length;\n    let successfulEnrichments = 0;\n    \n    if (totalIPs === 0) {\n        showToast('No IP nodes found to enrich', 'info');\n        completeProgressBar();\n        return;\n    }\n    \n    console.log(`Found ${totalIPs} IP nodes to enrich with Shodan`);\n    showToast(`Starting Shodan enrichment for ${totalIPs} IPs`, 'info');\n    \n    const batchSize = 5; // Smaller batch size to respect Shodan rate limits\n    const delayBetweenBatches = 100; // Small delay between batches\n    const shodanDelayMs = SHODAN_RATE_LIMIT_MS; // 1-second delay per request\n    const totalBatches = Math.ceil(totalIPs / batchSize);\n    const timePerBatchMs = shodanDelayMs * batchSize;\n    const totalBatchDelays = (totalBatches - 1) * delayBetweenBatches;\n    const estimatedTimeMs = (timePerBatchMs * totalBatches) + totalBatchDelays + 1000;\n    \n    const estimatedSeconds = Math.ceil(estimatedTimeMs / 1000);\n    const estimatedMinutes = Math.floor(estimatedSeconds / 60);\n    const remainingSeconds = estimatedSeconds % 60;\n    const timeEstimateStr = estimatedMinutes > 0 \n        ? `${estimatedMinutes}m ${remainingSeconds}s` \n        : `${estimatedSeconds}s`;\n    \n    showToast(`Estimated time for Shodan enrichment: ~${timeEstimateStr}`, 'info');\n    progressBar.textContent = `Shodan Enrichment: 0/${totalIPs} IPs (0%) - Est. ${timeEstimateStr}`;\n    \n    async function processBatch(batch) {\n        for (const node of batch) {\n            if (activeTaskController && activeTaskController.signal.aborted) {\n                return false;\n            }\n            try {\n                const baseUrl = ignoreApiKeysViaProxy ?\n                    `https://api.shodan.io/shodan/host/${node.ip}` :\n                    `https://api.shodan.io/shodan/host/${node.ip}?key=${shodanApiKey}`;\n                const url = constructUrl(baseUrl, !ignoreApiKeysViaProxy);\n                const response = await fetch(url, { signal: activeTaskController?.signal });\n                if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n                const data = await response.json();\n                console.log(`Shodan response for ${node.ip}:`, JSON.stringify(data, null, 2));\n\n                // Use the same processing logic as right-click enrichment\n                await processShodanData(node.id, data);\n                \n                successfulEnrichments++;\n            } catch (error) {\n                if (error.name === 'AbortError') {\n                    return false;\n                }\n                console.error(`Failed to enrich IP ${node.ip}: ${error.message}`);\n                showToast(`Failed to enrich IP ${node.ip}: ${error.message}`, 'error');\n            }\n            await new Promise(resolve => setTimeout(resolve, shodanDelayMs)); // Respect Shodan rate limit\n        }\n        return true;\n    }\n    \n    let lastProgressUpdate = 0;\n    const progressUpdateInterval = 500;\n    const startTime = Date.now();\n\n    try {\n        for (let i = 0; i < totalIPs; i += batchSize) {\n            if (activeTaskController && activeTaskController.signal.aborted) {\n                showToast('Shodan enrichment stopped', 'info');\n                progressBar.textContent = `Shodan Enrichment: Stopped at ${successfulEnrichments}/${totalIPs} IPs`;\n                break;\n            }\n            \n            const batch = ipNodes.slice(i, Math.min(i + batchSize, totalIPs));\n            console.log(`Processing batch ${Math.floor(i / batchSize) + 1} of ${totalBatches}, IPs ${i} to ${Math.min(i + batchSize - 1, totalIPs - 1)}`);\n            \n            const batchSuccess = await processBatch(batch);\n            \n            if (batchSuccess) {\n                updateNodeSizes();\n                updateSelectOptions();\n            }\n            \n            const currentTime = Date.now();\n            const processedIPs = Math.min(i + batchSize, totalIPs);\n            const progress = ((processedIPs / totalIPs) * 100).toFixed(1);\n            \n            if (currentTime - lastProgressUpdate >= progressUpdateInterval || processedIPs === totalIPs) {\n                const elapsedTimeMs = currentTime - startTime;\n                const timePerIp = successfulEnrichments > 0 ? elapsedTimeMs / successfulEnrichments : shodanDelayMs;\n                const remainingIPs = totalIPs - successfulEnrichments;\n                const remainingTimeMs = remainingIPs * timePerIp;\n                const remainingSeconds = Math.ceil(remainingTimeMs / 1000);\n                const remainingMinutes = Math.floor(remainingSeconds / 60);\n                const remainingSecondsPart = remainingSeconds % 60;\n                const remainingTimeStr = remainingMinutes > 0 \n                    ? `${remainingMinutes}m ${remainingSecondsPart}s` \n                    : `${remainingSeconds}s`;\n                \n                progressBar.textContent = `Shodan Enrichment: ${successfulEnrichments}/${totalIPs} IPs (${progress}%) - Est. ${remainingTimeStr} remaining`;\n                lastProgressUpdate = currentTime;\n            }\n            \n            await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));\n        }\n    } finally {\n        updateNodeSizes();\n        updateSelectOptions();\n        updateTheme();\n        \n        await stabilizeNetwork();\n        //ensureInteractionSettings();\n\n        if (!(activeTaskController && activeTaskController.signal.aborted)) {\n            completeProgressBar();\n            showToast(`Shodan enrichment completed: ${successfulEnrichments}/${totalIPs} IPs enriched`, 'success');\n        } else {\n            showToast(`Shodan enrichment stopped: ${successfulEnrichments}/${totalIPs} IPs processed`, 'info');\n        }\n        \n        if (window.innerWidth <= 768) {\n            const controls = document.getElementById('controls');\n            controls.classList.add('collapsed');\n            document.getElementById('myNetwork').style.display = 'block';\n            network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });\n        }\n    }\n}\n\nasync function enrichAllInternetDB() {\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const ipNodes = nodes.get({ filter: n => n.type === 'ip' && n.ip });\n    const totalIPs = ipNodes.length;\n    let successfulEnrichments = 0;\n    \n    console.log(`Found ${totalIPs} IP nodes to enrich with InternetDB`);\n    showToast(`Starting InternetDB enrichment for ${totalIPs} IPs`, 'info');\n    \n    const batchSize = 50;\n    const delayBetweenBatches = 200;\n    const totalBatches = Math.ceil(totalIPs / batchSize);\n    const assumedRequestTimeMs = 100;\n    const timePerBatchMs = assumedRequestTimeMs;\n    const totalBatchDelays = (totalBatches - 1) * delayBetweenBatches;\n    const estimatedTimeMs = (timePerBatchMs * totalBatches) + totalBatchDelays + 1000;\n    \n    const estimatedSeconds = Math.ceil(estimatedTimeMs / 1000);\n    const estimatedMinutes = Math.floor(estimatedSeconds / 60);\n    const remainingSeconds = estimatedSeconds % 60;\n    const timeEstimateStr = estimatedMinutes > 0 \n        ? `${estimatedMinutes}m ${remainingSeconds}s` \n        : `${estimatedSeconds}s`;\n    \n    showToast(`Estimated time for InternetDB enrichment: ~${timeEstimateStr}`, 'info');\n    document.getElementById('progress-bar').textContent = `InternetDB Enrichment: 0/${totalIPs} IPs (0%) - Est. ${timeEstimateStr}`;\n    \n    // Deduplication maps\n    const existingPorts = new Map(nodes.get({ filter: n => n.type === 'port' }).map(n => [`${n.portType}/${n.portNumber}`, n.id]));\n    const existingDomains = new Map(nodes.get({ filter: n => n.type === 'domain' }).map(n => [n.domain, n.id]));\n    const existingVulns = new Map(nodes.get({ filter: n => n.type === 'vulnerability' }).map(n => [n.cve, n.id]));\n    const existingTags = new Map(nodes.get({ filter: n => n.type === 'tag' }).map(n => [n.tag, n.id]));\n    const existingCPEs = new Map(nodes.get({ filter: n => n.type === 'cpe' }).map(n => [n.cpe, n.id]));\n    \n    const newNodes = [];\n    const newEdges = [];\n    \n    async function processBatch(batch) {\n        const promises = batch.map(node => {\n            if (activeTaskController && activeTaskController.signal.aborted) {\n                return Promise.resolve(null);\n            }\n            const url = constructUrl(`https://internetdb.shodan.io/${node.ip}`);\n            return fetch(url)\n                .then(response => {\n                    if (!response.ok) throw new Error('Failed to fetch InternetDB data');\n                    return response.json();\n                })\n                .then(data => {\n                    // Ports\n                    if (data.ports && data.ports.length > 0) {\n                        data.ports.forEach(port => {\n                            const portKey = `TCP/${port}`;\n                            let portId = existingPorts.get(portKey);\n                            if (!portId) {\n                                portId = nextId++;\n                                newNodes.push({\n                                    id: portId,\n                                    type: 'port',\n                                    label: `TCP/${port}`,\n                                    title: `Port\\nType: TCP\\nNumber: ${port}`,\n                                    color: { background: '#a78bfa' },\n                                    portType: 'TCP',\n                                    portNumber: port.toString()\n                                });\n                                existingPorts.set(portKey, portId);\n                            }\n                            const edgeId = `${node.id}-${portId}-Exposes`;\n                            if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                                newEdges.push({ id: edgeId, from: node.id, to: portId, label: 'Exposes' });\n                            }\n                        });\n                    }\n\n                    // Hostnames\n                    if (data.hostnames && data.hostnames.length > 0) {\n                        data.hostnames.forEach(hostname => {\n                            let domainId = existingDomains.get(hostname);\n                            if (!domainId) {\n                                domainId = nextId++;\n                                newNodes.push({\n                                    id: domainId,\n                                    type: 'domain',\n                                    label: `Domain: ${hostname}`,\n                                    title: `Domain: ${hostname}`,\n                                    color: { background: '#60a5fa' },\n                                    domain: hostname\n                                });\n                                existingDomains.set(hostname, domainId);\n                            }\n                            const edgeId = `${node.id}-${domainId}-ResolvesTo`;\n                            if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                                newEdges.push({ id: edgeId, from: node.id, to: domainId, label: 'Resolves to' });\n                            }\n                        });\n                    }\n\n                    // Vulnerabilities\n                    if (data.cves && data.cves.length > 0) {\n                        data.cves.forEach(cve => {\n                            let cveId = existingVulns.get(cve);\n                            if (!cveId) {\n                                cveId = nextId++;\n                                newNodes.push({\n                                    id: cveId,\n                                    type: 'vulnerability',\n                                    label: `Vulnerability: ${cve}`,\n                                    title: `Vulnerability\\nName: ${cve}\\nCVE: ${cve}`,\n                                    color: { background: '#dc2626' },\n                                    vulnName: cve,\n                                    cve: cve,\n                                    url: `https://nvd.nist.gov/vuln/detail/${cve}`\n                                });\n                                existingVulns.set(cve, cveId);\n                            }\n                            const edgeId = `${node.id}-${cveId}-Has`;\n                            if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                                newEdges.push({ id: edgeId, from: node.id, to: cveId, label: 'Has' });\n                            }\n                        });\n                    }\n\n                    // Tags\n                    if (data.tags && data.tags.length > 0) {\n                        data.tags.forEach(tag => {\n                            let tagId = existingTags.get(tag);\n                            if (!tagId) {\n                                tagId = nextId++;\n                                newNodes.push({\n                                    id: tagId,\n                                    type: 'tag',\n                                    label: `Tag: ${tag}`,\n                                    title: `Tag: ${tag}`,\n                                    color: { background: '#6d28d9' },\n                                    tag: tag\n                                });\n                                existingTags.set(tag, tagId);\n                            }\n                            const edgeId = `${node.id}-${tagId}-Tagged`;\n                            if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                                newEdges.push({ id: edgeId, from: node.id, to: tagId, label: 'Tagged' });\n                            }\n                        });\n                    }\n\n                    // CPEs\n                    if (data.cpes && data.cpes.length > 0) {\n                        data.cpes.forEach(cpe => {\n                            let cpeId = existingCPEs.get(cpe);\n                            if (!cpeId) {\n                                cpeId = nextId++;\n                                newNodes.push({\n                                    id: cpeId,\n                                    type: 'cpe',\n                                    label: `CPE: ${cpe.split(':')[3] || cpe}`,\n                                    title: `CPE: ${cpe}`,\n                                    color: { background: '#0d9488' },\n                                    cpe: cpe\n                                });\n                                existingCPEs.set(cpe, cpeId);\n                            }\n                            const edgeId = `${node.id}-${cpeId}-Runs`;\n                            if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                                newEdges.push({ id: edgeId, from: node.id, to: cpeId, label: 'Runs' });\n                            }\n                        });\n                    }\n\n                    successfulEnrichments++;\n                })\n                .catch(error => {\n                    console.error(`Failed to enrich IP ${node.ip}: ${error.message}`);\n                    showToast(`Failed to enrich IP ${node.ip}: ${error.message}`, 'error');\n                    return null;\n                });\n        });\n        await Promise.all(promises);\n    }\n    \n    let lastProgressUpdate = 0;\n    const progressUpdateInterval = 1000;\n    const startTime = Date.now();\n    \n    for (let i = 0; i < totalIPs; i += batchSize) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('InternetDB enrichment stopped', 'info');\n            break;\n        }\n        \n        const batch = ipNodes.slice(i, Math.min(i + batchSize, totalIPs));\n        console.log(`Processing batch ${Math.floor(i / batchSize) + 1} of ${totalBatches}, IPs ${i} to ${Math.min(i + batchSize - 1, totalIPs - 1)}`);\n        \n        await processBatch(batch);\n        \n        if (newNodes.length > 0) {\n            nodes.add(newNodes);\n            newNodes.length = 0;\n        }\n        if (newEdges.length > 0) {\n            edges.add(newEdges);\n            newEdges.length = 0;\n        }\n        \n        const currentTime = Date.now();\n        if (currentTime - lastProgressUpdate >= progressUpdateInterval) {\n            const processedIPs = Math.min(i + batchSize, totalIPs);\n            const progress = ((processedIPs / totalIPs) * 100).toFixed(1);\n            const remainingIPs = totalIPs - processedIPs;\n            const remainingTimeMs = Math.max(0, remainingIPs * assumedRequestTimeMs);\n            const remainingSeconds = Math.ceil(remainingTimeMs / 1000);\n            const remainingMinutes = Math.floor(remainingSeconds / 60);\n            const remainingSecondsPart = remainingSeconds % 60;\n            const remainingTimeStr = remainingMinutes > 0 \n                ? `${remainingMinutes}m ${remainingSecondsPart}s` \n                : `${remainingSeconds}s`;\n            \n            document.getElementById('progress-bar').textContent = \n                `InternetDB Enrichment: ${successfulEnrichments}/${totalIPs} IPs (${progress}%) - Est. ${remainingTimeStr} remaining`;\n            lastProgressUpdate = currentTime;\n            updateSelectOptions();\n        }\n        \n        await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));\n    }\n    \n    if (newNodes.length > 0) nodes.add(newNodes);\n    if (newEdges.length > 0) edges.add(newEdges);\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    //ensureInteractionSettings();\n    completeProgressBar();\n    showToast(`InternetDB enrichment completed: ${successfulEnrichments}/${totalIPs} IPs enriched`, 'success');\n    \n    if (window.innerWidth <= 768) {\n        const controls = document.getElementById('controls');\n        controls.classList.add('collapsed');\n        document.getElementById('myNetwork').style.display = 'block';\n        network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });\n    }\n}\n\nconst throttledEnrichIP = throttleRequest(async function enrichIP(ip, ipNodeId, isBulk = false, signal) {\n    if (!ipinfoApiKey && !ignoreApiKeysViaProxy) { \n        showToast('Please set your IPinfo API key in the \"API Keys\" tab first.', 'error'); \n        return; \n    }\n    network.setOptions({ physics: { enabled: false } });\n    try {\n        const baseUrl = ignoreApiKeysViaProxy ? \n            `https://ipinfo.io/${ip}/json` : \n            `https://ipinfo.io/${ip}/json?token=${ipinfoApiKey}`;\n        const url = constructUrl(baseUrl, !ignoreApiKeysViaProxy);\n        const response = await fetch(url, { signal });\n        if (!response.ok) throw new Error('Failed to fetch IP info');\n        const data = await response.json();\n        \n        const asn = data.asn?.asn || 'Unknown ASN';\n        const city = data.city || 'Unknown City';\n        const companyName = data.company?.name || 'Unknown Company';\n        const country = data.country || 'Unknown Country';\n        const privacy = data.privacy || { vpn: false, proxy: false, tor: false, relay: false, hosting: false };\n\n        const newNodes = [];\n        const newEdges = [];\n        \n        // Pre-collect existing nodes for deduplication\n        let existingAsns = new Map(nodes.get({ filter: n => n.type === 'asn' }).map(n => [n.asn, n.id]));\n        let existingCities = new Map(nodes.get({ filter: n => n.type === 'city' }).map(n => [n.city, n.id]));\n        let existingOrgs = new Map(nodes.get({ filter: n => n.type === 'organization' }).map(n => [n.organization, n.id]));\n        let existingCountries = new Map(nodes.get({ filter: n => n.type === 'country' }).map(n => [n.country, n.id]));\n        let existingPrivacyTypes = new Map([\n            ['vpn', null], ['proxy', null], ['tor', null], ['relay', null], ['hosting', null]\n        ].map(([type]) => {\n            const existing = nodes.get({ filter: n => n.type === type })[0];\n            return [type, existing ? existing.id : null];\n        }));\n\n        const privacyTypes = [\n            { key: 'vpn', label: 'VPN', color: '#9333ea' },\n            { key: 'proxy', label: 'Proxy', color: '#f43f5e' },\n            { key: 'tor', label: 'Tor', color: '#64748b' },\n            { key: 'relay', label: 'Relay', color: '#eab308' },\n            { key: 'hosting', label: 'Hosting', color: '#14b8a6' }\n        ];\n\n        // Helper function to add node and edge with unique ID\n        const addNodeAndEdge = (type, key, value, labelPrefix, title, color, edgeLabel) => {\n            let targetId = existingAsns.get(value) || existingCities.get(value) || existingOrgs.get(value) || existingCountries.get(value);\n            if (!targetId) {\n                targetId = nextId++;\n                newNodes.push({ \n                    id: targetId, \n                    type: type, \n                    label: `${labelPrefix}: ${value}`, \n                    title: title, \n                    color: { background: color }, \n                    [key]: value \n                });\n                if (type === 'asn') existingAsns.set(value, targetId);\n                else if (type === 'city') existingCities.set(value, targetId);\n                else if (type === 'organization') existingOrgs.set(value, targetId);\n                else if (type === 'country') existingCountries.set(value, targetId);\n            }\n            const edgeId = `${ipNodeId}-${targetId}-${edgeLabel}`;\n            if (!edges.get(edgeId) && !newEdges.some(e => e.id === edgeId)) {\n                newEdges.push({ id: edgeId, from: ipNodeId, to: targetId, label: edgeLabel });\n            }\n        };\n\n        // Add nodes and edges\n        addNodeAndEdge('asn', 'asn', asn, 'ASN', `ASN: ${asn}`, '#a3e635', 'Assigned to');\n        addNodeAndEdge('city', 'city', city, 'City', `City: ${city}`, '#f97316', 'Located in');\n        addNodeAndEdge('organization', 'organization', companyName, 'Organization', `Company: ${companyName}`, '#facc15', 'Belongs to');\n        addNodeAndEdge('country', 'country', country, 'Country', `Country: ${country}`, '#34d399', 'Located in');\n\n        // Privacy Types\n        privacyTypes.forEach(privacyType => {\n            if (privacy[privacyType.key]) {\n                let privacyNodeId = existingPrivacyTypes.get(privacyType.key);\n                if (!privacyNodeId) {\n                    privacyNodeId = nextId++;\n                    newNodes.push({ \n                        id: privacyNodeId, \n                        type: privacyType.key, \n                        label: privacyType.label, \n                        title: privacyType.label, \n                        color: { background: privacyType.color }\n                    });\n                    existingPrivacyTypes.set(privacyType.key, privacyNodeId);\n                }\n                const edgeId = `${ipNodeId}-${privacyNodeId}-Uses`;\n                if (!edges.get(edgeId) && !newEdges.some(e => e.id === edgeId)) {\n                    newEdges.push({ id: edgeId, from: ipNodeId, to: privacyNodeId, label: 'Uses' });\n                }\n            }\n        });\n\n        // Batch update\n        if (newNodes.length > 0) nodes.add(newNodes);\n        if (newEdges.length > 0) edges.add(newEdges);\n\n        updateNodeSizes();\n        updateSelectOptions();\n        await stabilizeNetwork();\n        if (!isBulk) showToast(`IP ${ip} enrichment completed using IPinfo`, 'success');\n    } catch (error) {\n        if (error.name === 'AbortError') {\n            showToast(`Enrichment of IP ${ip} was cancelled`, 'info');\n            return;\n        }\n        console.error(`Error enriching IP ${ip}: ${error.message}`);\n        showToast(`Error enriching IP ${ip}: ${error.message}`, 'error');\n        await stabilizeNetwork();\n    }\n}, RATE_LIMIT_MS);\n\n\nconst throttledEnrichShodan = throttleRequest(async function enrichShodan(ip, ipNodeId, isBulk = false, signal) {\n    if (!shodanApiKey && !ignoreApiKeysViaProxy) { \n        showToast('Please set your Shodan API key in the \"API Keys\" tab first.', 'error'); \n        return; \n    }\n    \n    if (!isBulk) network.setOptions({ physics: { enabled: false } });\n    \n    try {\n        const baseUrl = ignoreApiKeysViaProxy ? \n            `https://api.shodan.io/shodan/host/${ip}` : \n            `https://api.shodan.io/shodan/host/${ip}?key=${shodanApiKey}`;\n        const url = constructUrl(baseUrl, !ignoreApiKeysViaProxy);\n        const response = await fetch(url, { signal });\n        if (!response.ok) throw new Error(`Failed to fetch Shodan data: ${response.statusText}`);\n        const data = await response.json();\n\n        // Process Shodan data using the shared helper\n        await processShodanData(ipNodeId, data);\n\n        updateNodeSizes();\n        updateSelectOptions();\n        if (!isBulk) {\n            await stabilizeNetwork();\n            updateTheme();\n            showToast(`IP ${ip} enrichment completed using Shodan`, 'success');\n        }\n    } catch (error) {\n        if (error.name === 'AbortError') {\n            showToast(`Enrichment of IP ${ip} was cancelled`, 'info');\n            return;\n        }\n        console.error(`Error enriching IP ${ip} with Shodan: ${error.message}`);\n        showToast(`Error enriching IP ${ip} with Shodan: ${error.message}`, 'error');\n        if (!isBulk) await stabilizeNetwork();\n    }\n}, SHODAN_RATE_LIMIT_MS);\n\n\n\nasync function importIOCsFromText() {\n    let text = document.getElementById('iocText').value.trim();\n    if (!text) { \n        showToast('Please enter some text containing IOCs', 'error'); \n        return; \n    }\n    // Apply comprehensive refanging\n    text = refangText(text);\n    await processIOCs(text);\n    document.getElementById('iocText').value = '';\n    saveStateAfterOperation();\n    showToast('IOCs (including emails) import completed', 'success');\n}\n\nasync function importIOCsFromFile() {\n    const fileInput = document.getElementById('iocFile');\n    const file = fileInput.files[0];\n    if (!file) { \n        showToast('Please select a text file containing IOCs', 'error'); \n        return; \n    }\n    const reader = new FileReader();\n    reader.onload = async function(event) { \n        // Apply comprehensive refanging\n        let text = refangText(event.target.result);\n        await processIOCs(text); \n        fileInput.value = ''; \n        saveStateAfterOperation();\n        showToast('IOCs (including emails) import completed', 'success');\n    };\n    reader.readAsText(file);\n}\n\n\n\n\nfunction toggleMode() {\n    isDarkMode = !isDarkMode;\n    updateTheme();\n    document.getElementById('mode-toggle').textContent = isDarkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode';\n}\n\n        function togglePhysics() {\n    isPhysicsPaused = !isPhysicsPaused;\n    const pauseButton = document.getElementById('pause-toggle');\n    network.setOptions({ \n        physics: { \n            enabled: !isPhysicsPaused,\n            barnesHut: {\n                // Ensure consistent physics settings\n                gravitationalConstant: -8000,\n                centralGravity: 0.1,\n                springLength: 200,\n                springConstant: 0.04,\n                damping: 0.9,\n                avoidOverlap: 0.5\n            },\n            maxVelocity: 25,\n            minVelocity: 0.1\n        },\n        interaction: { ...baseInteractionOptions }\n    });\n    pauseButton.textContent = isPhysicsPaused ? 'Resume Physics' : 'Pause Physics';\n    pauseButton.classList.toggle('paused', isPhysicsPaused);\n    if (!isPhysicsPaused) {\n        // Trigger stabilization when resuming physics\n        stabilizeNetwork();\n    }\n    //ensureInteractionSettings();\n    }\n\n        function resetLayout() {\n            nodes.forEach(node => nodes.update({ id: node.id, x: undefined, y: undefined }));\n            stabilizeNetwork(false);\n        }\n\n        function setOrganicLayout() {\n    network.setOptions({\n        physics: {\n            enabled: true,\n            stabilization: {\n                enabled: true,\n                iterations: 200\n            },\n            barnesHut: {\n                gravitationalConstant: -8000,\n                centralGravity: 0.3,\n                springLength: 150,\n                springConstant: 0.05,\n                damping: 0.9,\n                avoidOverlap: 1.0\n            },\n            maxVelocity: 50,\n            minVelocity: 0.1\n        },\n        layout: { \n            hierarchical: false,\n            improvedLayout: false\n        },\n        interaction: { ...baseInteractionOptions }\n    });\n\n    // Reset node positions\n    nodes.forEach(node => {\n        nodes.update({ \n            id: node.id, \n            x: undefined, \n            y: undefined,\n            fixed: { x: false, y: false }\n        });\n    });\n\n    stabilizeNetwork().then(() => {\n        isPhysicsPaused = true;\n        network.setOptions({ physics: { enabled: false } });\n        const pauseButton = document.getElementById('pause-toggle');\n        pauseButton.textContent = 'Resume Physics';\n        pauseButton.classList.add('paused');\n        //ensureInteractionSettings();\n    });\n}\n\nfunction setCircularLayout() {\n    network.setOptions({\n        physics: { enabled: false },\n        layout: { hierarchical: false },\n        interaction: { ...baseInteractionOptions }\n    });\n\n    const containerRect = container.getBoundingClientRect();\n    const radius = Math.min(containerRect.width, containerRect.height) / 2 - 100;\n    const nodeCount = nodes.length;\n    const angleStep = (2 * Math.PI) / nodeCount;\n    const centerX = containerRect.width / 2;\n    const centerY = containerRect.height / 2;\n\n    nodes.forEach((node, i) => {\n        const x = centerX + radius * Math.cos(angleStep * i);\n        const y = centerY + radius * Math.sin(angleStep * i);\n        nodes.update({\n            id: node.id,\n            x: x,\n            y: y,\n            fixed: { x: true, y: true }\n        });\n    });\n\n    network.fit({\n        animation: {\n            duration: 500,\n            easingFunction: 'easeInOutQuad'\n        }\n    });\n}\n\nfunction setOrthogonalLayout() {\n    network.setOptions({\n        physics: { enabled: false },\n        layout: { hierarchical: false },\n        interaction: { ...baseInteractionOptions }\n    });\n\n    const containerRect = container.getBoundingClientRect();\n    const gridSize = Math.ceil(Math.sqrt(nodes.length));\n    const stepX = containerRect.width / (gridSize + 1);\n    const stepY = containerRect.height / (gridSize + 1);\n    let i = 0;\n\n    nodes.forEach(node => {\n        const x = (i % gridSize + 0.5) * stepX;\n        const y = (Math.floor(i / gridSize) + 0.5) * stepY;\n        nodes.update({\n            id: node.id,\n            x: x,\n            y: y,\n            fixed: { x: true, y: true }\n        });\n        i++;\n    });\n\n    network.fit({\n        animation: {\n            duration: 500,\n            easingFunction: 'easeInOutQuad'\n        }\n    });\n}\n\nfunction setTreeLayout() {\n    network.setOptions({\n        physics: { enabled: false },\n        layout: {\n            hierarchical: {\n                enabled: true,\n                levelSeparation: 200,\n                nodeSpacing: 150,\n                treeSpacing: 200,\n                direction: 'UD',\n                sortMethod: 'hubsize',\n                shakeTowards: 'leaves'\n            }\n        },\n        edges: {\n            smooth: {\n                enabled: true,\n                type: 'cubicBezier'\n            }\n        },\n        interaction: { ...baseInteractionOptions }\n    });\n\n    // Reset positions before applying layout\n    nodes.forEach(node => {\n        nodes.update({\n            id: node.id,\n            x: undefined,\n            y: undefined,\n            fixed: { x: false, y: false }\n        });\n    });\n\n    network.fit({\n        animation: {\n            duration: 500,\n            easingFunction: 'easeInOutQuad'\n        }\n    });\n}\n\nfunction setHierarchicalLayout() {\n    network.setOptions({\n        physics: { enabled: false },\n        layout: {\n            hierarchical: {\n                enabled: true,\n                levelSeparation: 200,\n                nodeSpacing: 150,\n                treeSpacing: 200,\n                direction: 'UD',\n                sortMethod: 'directed',\n                shakeTowards: 'roots'\n            }\n        },\n        edges: {\n            smooth: {\n                enabled: true,\n                type: 'cubicBezier'\n            }\n        },\n        interaction: { ...baseInteractionOptions }\n    });\n\n    // Reset positions before applying layout\n    nodes.forEach(node => {\n        nodes.update({\n            id: node.id,\n            x: undefined,\n            y: undefined,\n            fixed: { x: false, y: false }\n        });\n    });\n\n    network.fit({\n        animation: {\n            duration: 500,\n            easingFunction: 'easeInOutQuad'\n        }\n    });\n}\n\n        function showTab(tabId) {\n            document.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active'));\n            document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));\n            document.getElementById(tabId).classList.add('active');\n            document.querySelector(`.tab-button[onclick=\"showTab('${tabId}')\"]`).classList.add('active');\n        }\n\n        document.getElementById('addEntityType').addEventListener('change', function() {\n            let type = this.value;\n            document.getElementById('addVulnNameInput').style.display = type === 'vulnerability' ? 'block' : 'none';\n            document.getElementById('addVulnCVEInput').style.display = type === 'vulnerability' ? 'block' : 'none';\n            document.getElementById('addVulnUrlInput').style.display = type === 'vulnerability' ? 'block' : 'none';\n            document.getElementById('addNameInput').style.display = type === 'contact' ? 'block' : 'none';\n            document.getElementById('addEmailInput').style.display = type === 'contact' ? 'block' : 'none';\n            document.getElementById('addIpInput').style.display = type === 'ip' ? 'block' : 'none';\n            document.getElementById('addDomainInput').style.display = type === 'domain' ? 'block' : 'none';\n            document.getElementById('addOrgInput').style.display = type === 'organization' ? 'block' : 'none';\n            document.getElementById('addPortNumInput').style.display = type === 'port' ? 'block' : 'none';\n            document.getElementById('addPortType').style.display = type === 'port' ? 'block' : 'none';\n            document.getElementById('addWalletAddressInput').style.display = type === 'wallet' ? 'block' : 'none';\n            document.getElementById('addAccountNumberInput').style.display = type === 'bank' ? 'block' : 'none';\n            document.getElementById('addSortCodeInput').style.display = type === 'bank' ? 'block' : 'none';\n            document.getElementById('addTechNameInput').style.display = type === 'technology' ? 'block' : 'none';\n            document.getElementById('addTechVersionInput').style.display = type === 'technology' ? 'block' : 'none';\n            document.getElementById('addDeviceCategory').style.display = type === 'device' ? 'block' : 'none';\n            document.getElementById('addDeviceNameInput').style.display = type === 'device' ? 'block' : 'none';\n            document.getElementById('addMalwareNameInput').style.display = type === 'malware' ? 'block' : 'none';\n            document.getElementById('addMalwareType').style.display = type === 'malware' ? 'block' : 'none';\n            document.getElementById('addSubnetInput').style.display = type === 'subnet' ? 'block' : 'none';\n        });\n\n        function createNodeData(type, values) {\n    const nodeData = { \n        id: nextId++, \n        size: 10,\n        type,\n        widthConstraint: false,\n        heightConstraint: false\n    };\n    \n    // Define configs for all node types\n    const configs = {\n        vulnerability: { \n            fields: ['vulnName'], \n            optionalFields: ['cve', 'url', 'notes'], \n            color: '#dc2626',\n            label: v => `Vulnerability: ${v.vulnName}${v.cve ? '\\nCVE: ' + v.cve : ''}${v.url ? '\\nURL: ' + v.url : ''}`,\n            title: v => `Vulnerability\\nName: ${v.vulnName}${v.cve ? '\\nCVE: ' + v.cve : ''}${v.url ? '\\nURL: ' + v.url : ''}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        contact: { \n            fields: ['name'], \n            optionalFields: ['email', 'notes'], \n            color: '#4ade80', \n            label: v => `Contact: ${v.name}${v.email ? '\\n' + v.email : ''}`, \n            title: v => `Contact\\nName: ${v.name}${v.email ? '\\nEmail: ' + v.email : ''}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        ip: { \n            fields: ['ip'], \n            optionalFields: ['notes'],\n            color: '#f87171', \n            label: v => `IP: ${v.ip}`, \n            title: v => `IP Address: ${v.ip}${v.notes ? '\\nNotes: ' + v.notes : ''}`,\n            validate: v => {\n                const ipRegex = {\n                    ipv4: /^(?!0\\d)(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?!0\\d)(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?!0\\d)(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?!0\\d)(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,\n                    ipv6: /^((([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4})|(([0-9a-fA-F]{1,4}:){1,7}:)|(::([0-9a-fA-F]{1,4}:){0,5}[0-9a-fA-F]{1,4})|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|::)$/i\n                };\n                return ipRegex.ipv4.test(v.ip) || ipRegex.ipv6.test(v.ip);\n            }\n        },\n        domain: { \n            fields: ['domain'], \n            optionalFields: ['notes'],\n            color: '#60a5fa', \n            label: v => `Domain: ${v.domain}`, \n            title: v => `Domain: ${v.domain}${v.notes ? '\\nNotes: ' + v.notes : ''}` \n        },\n        organization: { \n            fields: ['organization'], \n            optionalFields: ['notes'],\n            color: '#facc15', \n            label: v => `Organization: ${v.organization}`, \n            title: v => `Organization: ${v.organization}${v.notes ? '\\nNotes: ' + v.notes : ''}` \n        },\n        port: { \n            fields: ['portNumber', 'portType'], \n            optionalFields: ['notes'],\n            color: '#a78bfa', \n            label: v => `Port: ${v.portType}/${v.portNumber}`, \n            title: v => `Port\\nType: ${v.portType}\\nNumber: ${v.portNumber}${v.notes ? '\\nNotes: ' + v.notes : ''}` \n        },\n        wallet: { \n            fields: ['address'], \n            optionalFields: ['notes'],\n            color: '#fb923c', \n            label: v => `Wallet: ${v.address}`, \n            title: v => `Wallet\\nAddress: ${v.address}${v.notes ? '\\nNotes: ' + v.notes : ''}` \n        },\n        bank: { \n            fields: ['accountNumber', 'sortCode'], \n            optionalFields: ['notes'],\n            color: '#10b981', \n            label: v => `Bank: ${v.accountNumber}\\nSort Code: ${v.sortCode}`, \n            title: v => `Bank Account\\nAccount Number: ${v.accountNumber}\\nSort Code: ${v.sortCode}${v.notes ? '\\nNotes: ' + v.notes : ''}` \n        },\n        technology: { \n            fields: ['techName'], \n            optionalFields: ['techVersion', 'notes'], \n            color: '#ec4899', \n            label: v => `Technology: ${v.techName}${v.techVersion ? '\\nVersion: ' + v.techVersion : ''}`, \n            title: v => `Technology\\nName: ${v.techName}${v.techVersion ? '\\nVersion: ' + v.techVersion : ''}${v.notes ? '\\nNotes: ' + v.notes : ''}` \n        },\n        device: { \n            fields: ['deviceCategory', 'deviceName'], \n            optionalFields: ['notes'],\n            color: '#14b8a6', \n            label: v => `Device: ${v.deviceName}\\nCategory: ${v.deviceCategory}`, \n            title: v => `Device\\nName: ${v.deviceName}\\nCategory: ${v.deviceCategory}${v.notes ? '\\nNotes: ' + v.notes : ''}` \n        },\n        malware: { \n            fields: ['malwareName', 'malwareType'], \n            optionalFields: ['notes'],\n            color: '#ef4444', \n            label: v => `Malware: ${v.malwareName}\\nType: ${v.malwareType}`, \n            title: v => `Malware\\nName: ${v.malwareName}\\nType: ${v.malwareType}${v.notes ? '\\nNotes: ' + v.notes : ''}` \n        },\n        favicon: { \n            fields: ['hash'], \n            optionalFields: ['location', 'notes'], \n            color: '#22d3ee',\n            label: v => `Favicon: ${v.hash.substring(0, 8)}...`, \n            title: v => `Favicon\\nHash: ${v.hash}${v.location ? '\\nPath: ' + v.location : ''}${v.notes ? '\\nNotes: ' + v.notes : ''}` \n        },\n        subnet: { \n            fields: ['subnet'], \n            optionalFields: ['notes'],\n            color: '#9333ea',\n            validate: v => {\n                const cidrRegex = /^(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})\\/(\\d{1,2})$/;\n                if (!cidrRegex.test(v.subnet)) return false;\n                const [ip, mask] = v.subnet.split('/');\n                const octets = ip.split('.').map(Number);\n                const maskNum = Number(mask);\n                return octets.every(o => o >= 0 && o <= 255) && maskNum >= 0 && maskNum <= 32;\n            }\n        },\n        mx: {\n            fields: ['hostname'],\n            optionalFields: ['notes'],\n            color: '#34d399', // Green for MX records\n            label: v => `MX: ${v.hostname.length > 30 ? v.hostname.substring(0, 27) + '...' : v.hostname}`,\n            title: v => `Mail Exchanger\\nHostname: ${v.hostname}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        txt: {\n            fields: ['text'],\n            optionalFields: ['notes'],\n            color: '#f59e0b', // Orange for TXT records\n            label: v => `TXT: ${v.text.length > 30 ? v.text.substring(0, 27) + '...' : v.text}`,\n            title: v => `TXT Record\\nValue: ${v.text}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        }\n    };\n\n    const config = configs[type];\n    if (!config || config.fields.some(f => !values[f])) { \n        showToast(`Please enter all required fields for ${type}`, 'error'); \n        return null; \n    }\n    if (config.validate && !config.validate(values)) { \n        showToast(`Invalid ${type} format`, 'error'); \n        return null; \n    }\n    \n    const allValues = { ...values };\n    if (config.optionalFields) config.optionalFields.forEach(f => { if (values[f]) allValues[f] = values[f]; });\n    \n    // Handle subnet type separately to set label and title directly\n    if (type === 'subnet') {\n        const [ip] = values.subnet.split('/');\n        const isPrivate = isPrivateIP(ip);\n        allValues.isPrivate = isPrivate;\n        nodeData.isPrivate = isPrivate;\n        nodeData.label = `Subnet: ${values.subnet} (${isPrivate ? 'Private' : 'Public'})`;\n        nodeData.title = `Subnet: ${values.subnet}\\nType: ${isPrivate ? 'Private' : 'Public'}${values.notes ? '\\nNotes: ' + values.notes : ''}`;\n        console.log(`Subnet ${values.subnet} classified as ${isPrivate ? 'Private' : 'Public'}`);\n        console.log(`Label set to: ${nodeData.label}`);\n        console.log(`Title set to: ${nodeData.title}`);\n    } else {\n        // For other types, use the config's label and title functions\n        nodeData.label = config.label ? config.label(allValues) : undefined;\n        nodeData.title = config.title ? config.title(allValues) : undefined;\n    }\n\n    nodeData.color = { background: config.color };\n    Object.assign(nodeData, allValues);\n\n    console.log(`Final nodeData: ${JSON.stringify(nodeData)}`);\n    return nodeData;\n}\n\n// Define isPrivateIP if not already defined elsewhere\nfunction isPrivateIP(ip) {\n    console.log(`isPrivateIP called with: ${ip}`);\n    const octets = ip.split('.').map(Number);\n    console.log(`Parsed octets: ${octets}`);\n    \n    if (octets.length !== 4 || octets.some(o => o < 0 || o > 255)) {\n        console.log(`Invalid IP format: ${ip}`);\n        return false;\n    }\n\n    const isPrivate = (\n        (octets[0] === 10) || // 10.0.0.0/8\n        (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) || // 172.16.0.0/12\n        (octets[0] === 192 && octets[1] === 168) // 192.168.0.0/16\n    );\n    \n    console.log(`IP ${ip} is ${isPrivate ? 'private' : 'public'}`);\n    return isPrivate;\n}\n\n\nfunction addNode() {\n    const type = document.getElementById('addEntityType').value;\n    \n    // Collect inputs based on entity type\n    const inputs = {\n        vulnerability: { \n            vulnName: document.getElementById('addVulnNameInput').value.trim(),\n            cve: document.getElementById('addVulnCVEInput').value.trim(),\n            url: document.getElementById('addVulnUrlInput').value.trim()\n        },\n        contact: { \n            name: document.getElementById('addNameInput').value.trim(),\n            email: document.getElementById('addEmailInput').value.trim()\n        },\n        ip: { \n            ip: document.getElementById('addIpInput').value.trim()\n        },\n        subnet: { \n            subnet: document.getElementById('addSubnetInput').value.trim()\n        },\n        domain: { \n            domain: document.getElementById('addDomainInput').value.trim()\n        },\n        organization: { \n            organization: document.getElementById('addOrgInput').value.trim()\n        },\n        port: { \n            portNumber: document.getElementById('addPortNumInput').value.trim(),\n            portType: document.getElementById('addPortType').value\n        },\n        wallet: { \n            address: document.getElementById('addWalletAddressInput').value.trim()\n        },\n        bank: { \n            accountNumber: document.getElementById('addAccountNumberInput').value.trim(),\n            sortCode: document.getElementById('addSortCodeInput').value.trim()\n        },\n        technology: { \n            techName: document.getElementById('addTechNameInput').value.trim(),\n            techVersion: document.getElementById('addTechVersionInput').value.trim()\n        },\n        device: { \n            deviceCategory: document.getElementById('addDeviceCategory').value,\n            deviceName: document.getElementById('addDeviceNameInput').value.trim()\n        },\n        malware: { \n            malwareName: document.getElementById('addMalwareNameInput').value.trim(),\n            malwareType: document.getElementById('addMalwareType').value\n        }\n    };\n\n    const nodeData = createNodeData(type, inputs[type]);\n    if (!nodeData) {\n        return;\n    }\n\n    // Check for duplicates based on key fields with corrected syntax\n    const existingNodes = nodes.get({\n        filter: n => n.type === type && (\n            (type === 'contact' && n.name === inputs[type].name && (!inputs[type].email || n.email === inputs[type].email)) ||\n            (type === 'ip' && n.ip === inputs[type].ip) ||\n            (type === 'domain' && n.domain === inputs[type].domain) ||\n            (type === 'organization' && n.organization === inputs[type].organization) ||\n            (type === 'port' && n.portNumber === inputs[type].portNumber && n.portType === inputs[type].portType) ||\n            (type === 'wallet' && n.address === inputs[type].address) ||\n            (type === 'bank' && n.accountNumber === inputs[type].accountNumber && n.sortCode === inputs[type].sortCode) ||\n            (type === 'technology' && n.techName === inputs[type].techName && n.techVersion === inputs[type].techVersion) ||\n            (type === 'device' && n.deviceCategory === inputs[type].deviceCategory && n.deviceName === inputs[type].deviceName) ||\n            (type === 'malware' && n.malwareName === inputs[type].malwareName && n.malwareType === inputs[type].malwareType) ||\n            (type === 'vulnerability' && n.vulnName === inputs[type].vulnName && \n             (!inputs[type].cve || n.cve === inputs[type].cve) && \n             (!inputs[type].url || n.url === inputs[type].url))\n        )\n    });\n\n    if (existingNodes.length > 0) {\n        showToast(`${type} already exists`, 'error');\n        return;\n    }\n\n    nodes.add({\n        ...nodeData,\n        size: 10,\n        widthConstraint: false,\n        heightConstraint: false\n    });\n\n    updateSelectOptions();\n    clearAddInputs();\n    updateNodeSizes();\n    stabilizeNetwork();\n    saveStateAfterOperation();\n    showToast(`${type} added successfully`, 'success');\n}\n\n\n        function editNode() {\n            const nodeId = document.getElementById('editNodeSelect').value;\n            if (!nodeId) { showToast('Please select a node to edit', 'error'); return; }\n            const node = nodes.get(parseInt(nodeId));\n            if (!node) { showToast('Selected node not found', 'error'); return; }\n\n            const type = node.type;\n            const inputs = {\n                contact: { name: document.getElementById('editNameInput').value, email: document.getElementById('editEmailInput').value },\n                ip: { ip: document.getElementById('editIpInput').value.trim() },\n                domain: { domain: document.getElementById('editDomainInput').value },\n                organization: { organization: document.getElementById('editOrgInput').value },\n                port: { portNumber: document.getElementById('editPortNumInput').value, portType: document.getElementById('editPortType').value },\n                wallet: { address: document.getElementById('editWalletAddressInput').value },\n                bank: { accountNumber: document.getElementById('editAccountNumberInput').value, sortCode: document.getElementById('editSortCodeInput').value },\n                technology: { techName: document.getElementById('editTechNameInput').value, techVersion: document.getElementById('editTechVersionInput').value },\n                device: { deviceCategory: document.getElementById('editDeviceCategory').value, deviceName: document.getElementById('editDeviceNameInput').value },\n                malware: { malwareName: document.getElementById('editMalwareNameInput').value, malwareType: document.getElementById('editMalwareType').value },\n                vulnerability: {vulnName: document.getElementById('editVulnNameInput').value.trim(),\n            cve: document.getElementById('editVulnCVEInput').value.trim(),\n            subnet: { subnet: document.getElementById('editSubnetInput').value.trim() },\n            url: document.getElementById('editVulnUrlInput').value.trim()\n            }\n            };\n            const updatedNodeData = createNodeData(type, inputs[type]);\n            if (updatedNodeData) {\n                const existingNode = nodes.get({ filter: n => n.id !== parseInt(nodeId) && n.type === type && Object.keys(inputs[type]).every(key => n[key] === inputs[type][key]) });\n                if (existingNode) { showToast(`Another ${type} with these values already exists`, 'error'); return; }\n                updatedNodeData.id = node.id;\n                nodes.update(updatedNodeData);\n                updateSelectOptions();\n                clearEditInputs();\n                stabilizeNetwork();\n                saveStateAfterOperation(); \n            }\n        }\n\n        function loadNodeForEdit() {\n            const nodeId = document.getElementById('editNodeSelect').value;\n            clearEditInputs();\n            if (!nodeId) return;\n\n            const node = nodes.get(parseInt(nodeId));\n            if (!node) return;\n\n            document.getElementById('editEntityType').value = node.type;\n            switch (node.type) {\n                case 'subnet':\n            document.getElementById('editSubnetInput').value = node.subnet || '';\n                    break;\n                case 'contact':\n                    document.getElementById('editNameInput').value = node.name || '';\n                    document.getElementById('editEmailInput').value = node.email || '';\n                    break;\n                case 'ip':\n                    document.getElementById('editIpInput').value = node.ip || '';\n                    break;\n                case 'domain':\n                    document.getElementById('editDomainInput').value = node.domain || '';\n                    break;\n                case 'organization':\n                    document.getElementById('editOrgInput').value = node.organization || '';\n                    break;\n                case 'port':\n                    document.getElementById('editPortNumInput').value = node.portNumber || '';\n                    document.getElementById('editPortType').value = node.portType || 'TCP';\n                    break;\n                case 'wallet':\n                    document.getElementById('editWalletAddressInput').value = node.address || '';\n                    break;\n                case 'bank':\n                    document.getElementById('editAccountNumberInput').value = node.accountNumber || '';\n                    document.getElementById('editSortCodeInput').value = node.sortCode || '';\n                    break;\n                case 'technology':\n                    document.getElementById('editTechNameInput').value = node.techName || '';\n                    document.getElementById('editTechVersionInput').value = node.techVersion || '';\n                    break;\n                case 'device':\n                    document.getElementById('editDeviceCategory').value = node.deviceCategory || 'Server';\n                    document.getElementById('editDeviceNameInput').value = node.deviceName || '';\n                    break;\n                case 'malware':\n                    document.getElementById('editMalwareNameInput').value = node.malwareName || '';\n                    document.getElementById('editMalwareType').value = node.malwareType || 'Wiper';\n                    break;\n                case 'vulnerability':\n                    document.getElementById('editVulnNameInput').value = node.vulnName || '';\n                    document.getElementById('editVulnCVEInput').value = node.cve || '';\n                    document.getElementById('editVulnUrlInput').value = node.url || '';\n                    break;\n            }\n            document.getElementById('editNameInput').style.display = node.type === 'contact' ? 'block' : 'none';\n            document.getElementById('editEmailInput').style.display = node.type === 'contact' ? 'block' : 'none';\n            document.getElementById('editIpInput').style.display = node.type === 'ip' ? 'block' : 'none';\n            document.getElementById('editDomainInput').style.display = node.type === 'domain' ? 'block' : 'none';\n            document.getElementById('editOrgInput').style.display = node.type === 'organization' ? 'block' : 'none';\n            document.getElementById('editPortNumInput').style.display = node.type === 'port' ? 'block' : 'none';\n            document.getElementById('editPortType').style.display = node.type === 'port' ? 'block' : 'none';\n            document.getElementById('editWalletAddressInput').style.display = node.type === 'wallet' ? 'block' : 'none';\n            document.getElementById('editAccountNumberInput').style.display = node.type === 'bank' ? 'block' : 'none';\n            document.getElementById('editSortCodeInput').style.display = node.type === 'bank' ? 'block' : 'none';\n            document.getElementById('editTechNameInput').style.display = node.type === 'technology' ? 'block' : 'none';\n            document.getElementById('editTechVersionInput').style.display = node.type === 'technology' ? 'block' : 'none';\n            document.getElementById('editDeviceCategory').style.display = node.type === 'device' ? 'block' : 'none';\n            document.getElementById('editDeviceNameInput').style.display = node.type === 'device' ? 'block' : 'none';\n            document.getElementById('editMalwareNameInput').style.display = node.type === 'malware' ? 'block' : 'none';\n            document.getElementById('editMalwareType').style.display = node.type === 'malware' ? 'block' : 'none';\n            document.getElementById('editVulnNameInput').style.display = node.type === 'vulnerability' ? 'block' : 'none';\n            document.getElementById('editVulnCVEInput').style.display = node.type === 'vulnerability' ? 'block' : 'none';\n            document.getElementById('editVulnUrlInput').style.display = node.type === 'vulnerability' ? 'block' : 'none';\n            document.getElementById('editSubnetInput').style.display = node.type === 'subnet' ? 'block' : 'none';\n        }\n\n        function clearAddInputs() {\n            document.querySelectorAll('#object-management .input-group:first-child input').forEach(input => input.value = '');\n            document.getElementById('addDeviceCategory').value = 'Server';\n            document.getElementById('addMalwareType').value = 'Wiper';\n            document.getElementById('addPortType').value = 'TCP';\n            document.getElementById('addVulnNameInput').value = '';\n            document.getElementById('addVulnCVEInput').value = '';\n            document.getElementById('addVulnUrlInput').value = '';\n        }\n\n        function clearEditInputs() {\n            document.querySelectorAll('#object-management .input-group:nth-child(2) input').forEach(input => input.value = '');\n            document.getElementById('editDeviceCategory').value = 'Server';\n            document.getElementById('editMalwareType').value = 'Wiper';\n            document.getElementById('editPortType').value = 'TCP';\n            document.getElementById('editVulnNameInput').value = '';\n            document.getElementById('editVulnCVEInput').value = '';\n            document.getElementById('editVulnUrlInput').value = '';\n            document.getElementById('editSubnetInput').value = '';\n        }\n\n        \n        function addEdge() {\n            let from = document.getElementById('fromNode').value; \n            let to = document.getElementById('toNode').value; \n            let label = document.getElementById('edgeLabel').value;\n            if (!from || !to || from === to) return showToast('Please select different nodes', 'error');\n            edges.add({ \n        id: `edge_${from}_${to}_${Date.now()}`, \n        from: parseInt(from), \n        to: parseInt(to), \n        label: label || '',\n        originalLabel: label || ''  // Add this line\n    });\n            updateNodeSizes(); \n            updateEdgeSelectOptions(); \n            document.getElementById('edgeLabel').value = ''; \n            stabilizeNetwork();\n            saveStateAfterOperation();\n        }\n\n        function removeNode() {\n            let nodeId = document.getElementById('removeNode').value;\n            if (!nodeId) return showToast('Please select a node', 'error');\n            edges.remove(edges.get({ filter: edge => edge.from === parseInt(nodeId) || edge.to === parseInt(nodeId) }));\n            nodes.remove({ id: parseInt(nodeId) });\n            updateNodeSizes(); \n            updateSelectOptions(); \n            stabilizeNetwork();\n            saveStateAfterOperation();\n        }\n\n        function removeEdge() {\n            let edgeId = document.getElementById('removeEdge').value;\n            if (!edgeId) return showToast('Please select an edge', 'error');\n            edges.remove({ id: edgeId });\n            updateNodeSizes(); \n            updateEdgeSelectOptions(); \n            document.getElementById('removeEdge').value = ''; \n            stabilizeNetwork();\n            saveStateAfterOperation();\n        }\n\n        function clearGraph() {\n    if (!confirm('Are you sure you want to clear the entire graph? This action cannot be undone.')) return;\n    nodes.clear();\n    edges.clear();\n    nextId = 1;\n    updateSelectOptions();\n    updateEdgeSelectOptions();\n    clearAddInputs();\n    clearEditInputs();\n    stabilizeNetwork();\n    saveStateAfterOperation();\n    showToast('Graph cleared', 'success');\n}\n\nfunction updateNodeSizes(affectedNodeIds = null) {\n    const nodesToUpdate = affectedNodeIds ? nodes.get(affectedNodeIds) : nodes.get();\n    nodesToUpdate.forEach(node => {\n        const connections = edges.get({ filter: edge => edge.from === node.id || edge.to === node.id }).length;\n        const newSize = Math.min(15 + connections * 10, 120);\n        nodes.update({ id: node.id, size: newSize, widthConstraint: false, heightConstraint: false });\n    });\n}\n\n\n\n\n        function updateSelectOptions() {\n            ['editNodeSelect', 'fromNode', 'toNode', 'removeNode'].forEach(id => {\n                let select = document.getElementById(id);\n                select.innerHTML = '<option value=\"\">Select</option>';\n                nodes.forEach(node => { \n                    let option = document.createElement('option'); \n                    option.value = node.id; \n                    option.text = node.label.split('\\n')[0]; \n                    select.appendChild(option); \n                });\n            });\n            updateEdgeSelectOptions();\n        }\n\n        function updateEdgeSelectOptions() {\n            let select = document.getElementById('removeEdge');\n            select.innerHTML = '<option value=\"\">Select Edge</option>';\n            edges.forEach(edge => {\n                let fromNode = nodes.get(edge.from); \n                let toNode = nodes.get(edge.to);\n                if (fromNode && toNode) {\n                    let option = document.createElement('option');\n                    option.value = edge.id; \n                    option.text = `${fromNode.label.split('\\n')[0]} -> ${toNode.label.split('\\n')[0]}${edge.label ? ' (' + edge.label + ')' : ''}`;\n                    select.appendChild(option);\n                }\n            });\n        }\n\n        function exportGraph() {\n            const exportData = { \n                nodes: nodes.get().map(node => ({ \n                    id: node.id, type: node.type, name: node.name, email: node.email, ip: node.ip, domain: node.domain, \n                    organization: node.organization, portType: node.portType, portNumber: node.portNumber, address: node.address, \n                    accountNumber: node.accountNumber, sortCode: node.sortCode, techName: node.techName, techVersion: node.techVersion, \n                    deviceCategory: node.deviceCategory, deviceName: node.deviceName, malwareName: node.malwareName, malwareType: node.malwareType, \n                    country: node.country, asn: node.asn, city: node.city, value: node.value, vulnName: node.vulnName, cve: node.cve, url: node.url \n                })), \n                edges: edges.get().map(edge => ({ id: edge.id, from: edge.from, to: edge.to, label: edge.label })) \n            };\n            const json = JSON.stringify(exportData, null, 2);\n            const blob = new Blob([json], { type: 'application/json' });\n            const url = URL.createObjectURL(blob);\n            const a = document.createElement('a'); \n            a.href = url; \n            a.download = 'network_graph.json'; \n            a.click(); \n            URL.revokeObjectURL(url);\n        }\n\n// Process IOCs\nasync function processIOCs(text) {\n    network.setOptions({ physics: { enabled: false } });\n\n    // Regex patterns\n    const urlRegex = /^(https?:\\/\\/)([^\\s/:]+)(?::(\\d{1,5}))?(\\/.*)?$/i;\n    const domainRegex = /^(?:[a-zA-Z0-9-_]+\\.)*[a-zA-Z0-9-_]+\\.[a-zA-Z]{2,}(?<!\\.(png|jpg|jpeg|gif|bmp|tif|tiff|pdf|doc|docx|xls|xlsx|ppt|pptx|txt|csv|zip|rar|7z|exe|dll|sys|bat|sh|mp3|mp4|avi|mkv|mov|wmv|flv|wav|html|css|js|php|asp|aspx|jsp|sql|db|bak|log|tar|gz|tgz))\\.?$/i;\n    const ipRegex = { \n        ipv4: /^(?!0\\d)(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?!0\\d)(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?!0\\d)(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?!0\\d)(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,\n        ipv6: /^((([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4})|(([0-9a-fA-F]{1,4}:){1,7}:)|(::([0-9a-fA-F]{1,4}:){0,5}[0-9a-fA-F]{1,4})|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|::)$/i\n    };\n    const subnetRegex = /^(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})\\/(\\d{1,2})$/;\n    const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/g;\n    const hashRegex = {\n        sha256: /^[0-9a-fA-F]{64}$/,\n        md5: /^[0-9a-fA-F]{32}$/,\n        sha1: /^[0-9a-fA-F]{40}$/\n    };\n    const hashPattern = /[0-9a-fA-F]{32,64}/g;\n    const timestampRegex = /^\\d{2}:\\d{2}:\\d{2}$/;\n\n    // Common file extensions to filter out\n    const commonFileExtensions = new Set([\n        'png', 'jpg', 'jpeg', 'gif', 'bmp', 'tif', 'tiff', 'pdf',\n        'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'csv',\n        'zip', 'rar', '7z', 'exe', 'dll', 'sys', 'bat', 'sh',\n        'mp3', 'mp4', 'avi', 'mkv', 'mov', 'wmv', 'flv', 'wav',\n        'html', 'css', 'js', 'php', 'asp', 'aspx', 'jsp', 'sql',\n        'db', 'bak', 'log', 'tar', 'gz', 'tgz'\n    ]);\n\n    // Split text into tokens\n    const tokens = text.split(/[\\s,\\n]+/).filter(token => token.trim().length > 0);\n    console.log('Tokens:', tokens);\n\n    // Extract matches\n    const urlMatches = new Set();\n    const ipMatches = new Set();\n    const subnetMatches = new Set();\n    const domainMatches = new Set();\n    const emailMatches = new Set(tokens.map(token => token.match(emailRegex)?.[0]).filter(Boolean));\n    const hashCandidates = new Set(tokens.map(token => token.match(hashPattern)?.[0]).filter(Boolean));\n\n    // Process each token\n    for (const token of tokens) {\n        // Skip timestamps\n        if (timestampRegex.test(token)) {\n            console.log(`Ignoring timestamp: ${token}`);\n            continue;\n        }\n\n        const urlMatch = token.match(urlRegex);\n        if (urlMatch) {\n            const [, protocol, host, port, path] = urlMatch;\n            urlMatches.add(token);\n            console.log(`URL Match: ${token}, Protocol: ${protocol}, Host: ${host}, Port: ${port || 'none'}, Path: ${path || 'none'}`);\n\n            // Normalize host by removing trailing dot\n            const normalizedHost = host.replace(/\\.$/, '');\n            if (subnetRegex.test(normalizedHost)) {\n                subnetMatches.add(normalizedHost);\n            } else if (ipRegex.ipv4.test(normalizedHost) || ipRegex.ipv6.test(normalizedHost)) {\n                ipMatches.add(normalizedHost);\n            } else if (domainRegex.test(normalizedHost)) {\n                domainMatches.add(normalizedHost);\n            }\n            continue;\n        }\n\n        // Check for standalone subnets\n        if (subnetRegex.test(token)) {\n            subnetMatches.add(token);\n            continue;\n        }\n\n        // Check for standalone IPs\n        if (ipRegex.ipv4.test(token) || ipRegex.ipv6.test(token)) {\n            ipMatches.add(token);\n            continue;\n        }\n\n        // Check for standalone domains, excluding file extensions, and normalize\n        const normalizedToken = token.replace(/\\.$/, '');\n        const tokenLower = normalizedToken.toLowerCase();\n        const extension = tokenLower.split('.').pop();\n        if (domainRegex.test(normalizedToken) && !urlMatches.has(token) && !commonFileExtensions.has(extension)) {\n            domainMatches.add(normalizedToken);\n            continue;\n        }\n    }\n\n    // Filter hash candidates\n    const hashMatches = new Set();\n    hashCandidates.forEach(hash => {\n        if (hashRegex.sha256.test(hash)) hashMatches.add({ value: hash, type: 'sha256' });\n        else if (hashRegex.md5.test(hash)) hashMatches.add({ value: hash, type: 'md5' });\n        else if (hashRegex.sha1.test(hash)) hashMatches.add({ value: hash, type: 'sha1' });\n    });\n\n    const totalItems = urlMatches.size + ipMatches.size + subnetMatches.size + domainMatches.size + emailMatches.size + hashMatches.size;\n    if (totalItems === 0) {\n        showToast('No IOCs found in text', 'info');\n        return;\n    }\n\n    console.log('URL Matches:', Array.from(urlMatches));\n    console.log('IP Matches:', Array.from(ipMatches));\n    console.log('Subnet Matches:', Array.from(subnetMatches));\n    console.log('Domain Matches:', Array.from(domainMatches));\n    console.log('Email Matches:', Array.from(emailMatches));\n    console.log('Hash Matches:', Array.from(hashMatches));\n\n    // Maps for deduplication\n    const existingNodes = new Map();\n    nodes.forEach(node => {\n        if (node.type === 'url' && node.url) existingNodes.set(`url:${node.url}`, node);\n        if (node.type === 'ip' && node.ip) existingNodes.set(`ip:${node.ip}`, node);\n        if (node.type === 'subnet' && node.subnet) existingNodes.set(`subnet:${node.subnet}`, node);\n        if (node.type === 'domain' && node.domain) existingNodes.set(`domain:${node.domain}`, node);\n        if (node.type === 'contact' && node.email) existingNodes.set(`email:${node.email}`, node);\n        if (node.type === 'hash' && node.hash) existingNodes.set(`hash:${node.hash}`, node);\n        if (node.type === 'port' && node.portNumber) existingNodes.set(`port:${node.portType}/${node.portNumber}`, node);\n    });\n\n    const newNodes = [];\n    const newEdges = [];\n    let processedCount = 0;\n    const batchSize = 50;\n\n    // Set to track edges by from-to-label\n    const edgeSet = new Set();\n    edges.forEach(edge => {\n        edgeSet.add(`${edge.from}_${edge.to}_${edge.label}`);\n    });\n\n    // Helper function to extract apex domain\n    function getApexDomain(fullDomain) {\n        const parts = fullDomain.toLowerCase().split('.');\n        if (parts.length < 2) return fullDomain;\n\n        const multiPartTlds = ['co.uk', 'gov.uk', 'ac.uk', 'org.uk', 'com.au', 'co.jp', 'ne.jp', 'go.jp'];\n        for (const tld of multiPartTlds) {\n            const tldParts = tld.split('.');\n            if (parts.slice(-tldParts.length).join('.') === tld) {\n                if (parts.length > tldParts.length) {\n                    return parts.slice(-(tldParts.length + 1)).join('.');\n                }\n                return fullDomain;\n            }\n        }\n        return parts.slice(-2).join('.');\n    }\n\n    // Helper function to get all domain levels\n    function getDomainLevels(domain) {\n        const levels = [];\n        let current = domain;\n        while (current.includes('.')) {\n            levels.push(current);\n            const parts = current.split('.');\n            current = parts.slice(1).join('.');\n            // Stop at apex domain to prevent splitting multi-part TLDs\n            if (current === getApexDomain(domain)) {\n                levels.push(current);\n                break;\n            }\n        }\n        return levels; // Highest level (subdomain) first\n    }\n\n    // Process URLs\n    for (const url of urlMatches) {\n        const urlMatch = url.match(urlRegex);\n        if (!urlMatch) continue;\n        const [, protocol, host, port, path] = urlMatch;\n\n        if (!existingNodes.has(`url:${url}`)) {\n            const urlNodeId = nextId++;\n            const label = `URL: ${url.length > 30 ? url.substring(0, 27) + '...' : url}`;\n            newNodes.push({\n                id: urlNodeId,\n                type: 'url',\n                label,\n                title: `Full URL: ${url}`,\n                color: { background: '#3b82f6' },\n                url,\n                size: 15\n            });\n            existingNodes.set(`url:${url}`, { id: urlNodeId });\n            console.log(`Added URL node: ${url}, ID: ${urlNodeId}`);\n\n            // Handle host (IP, subnet, or domain)\n            const normalizedHost = host.replace(/\\.$/, '');\n            if (subnetRegex.test(normalizedHost)) {\n                if (!existingNodes.has(`subnet:${normalizedHost}`)) {\n                    const subnetNodeData = createNodeData('subnet', { subnet: normalizedHost });\n                    if (subnetNodeData) {\n                        subnetNodeData.id = nextId++;\n                        newNodes.push(subnetNodeData);\n                        existingNodes.set(`subnet:${normalizedHost}`, { id: subnetNodeData.id });\n                        const edgeKey = `${urlNodeId}_${subnetNodeData.id}_Resolves to`;\n                        if (!edgeSet.has(edgeKey)) {\n                            newEdges.push({\n                                id: `edge_${urlNodeId}_${subnetNodeData.id}_${Date.now()}`,\n                                from: urlNodeId,\n                                to: subnetNodeData.id,\n                                label: 'Resolves to'\n                            });\n                            edgeSet.add(edgeKey);\n                            console.log(`Added Subnet node: ${normalizedHost}, ID: ${subnetNodeData.id}`);\n                        }\n                    }\n                } else {\n                    const existingSubnetNode = existingNodes.get(`subnet:${normalizedHost}`);\n                    const edgeKey = `${urlNodeId}_${existingSubnetNode.id}_Resolves to`;\n                    if (!edgeSet.has(edgeKey)) {\n                        newEdges.push({\n                            id: `edge_${urlNodeId}_${existingSubnetNode.id}_${Date.now()}`,\n                            from: urlNodeId,\n                            to: existingSubnetNode.id,\n                            label: 'Resolves to'\n                        });\n                        edgeSet.add(edgeKey);\n                    }\n                }\n            } else if (ipRegex.ipv4.test(normalizedHost) || ipRegex.ipv6.test(normalizedHost)) {\n                if (!existingNodes.has(`ip:${normalizedHost}`)) {\n                    const ipNodeId = nextId++;\n                    newNodes.push({\n                        id: ipNodeId,\n                        type: 'ip',\n                        label: `IP: ${normalizedHost}`,\n                        title: `IP Address: ${normalizedHost}`,\n                        color: { background: '#f87171' },\n                        ip: normalizedHost,\n                        size: 20\n                    });\n                    existingNodes.set(`ip:${normalizedHost}`, { id: ipNodeId });\n                    const edgeKey = `${urlNodeId}_${ipNodeId}_Resolves to`;\n                    if (!edgeSet.has(edgeKey)) {\n                        newEdges.push({\n                            id: `edge_${urlNodeId}_${ipNodeId}_${Date.now()}`,\n                            from: urlNodeId,\n                            to: ipNodeId,\n                            label: 'Resolves to'\n                        });\n                        edgeSet.add(edgeKey);\n                        console.log(`Added IP node: ${normalizedHost}, ID: ${ipNodeId}`);\n                    }\n                } else {\n                    const existingIpNode = existingNodes.get(`ip:${normalizedHost}`);\n                    const edgeKey = `${urlNodeId}_${existingIpNode.id}_Resolves to`;\n                    if (!edgeSet.has(edgeKey)) {\n                        newEdges.push({\n                            id: `edge_${urlNodeId}_${existingIpNode.id}_${Date.now()}`,\n                            from: urlNodeId,\n                            to: existingIpNode.id,\n                            label: 'Resolves to'\n                        });\n                        edgeSet.add(edgeKey);\n                    }\n                }\n            } else if (domainRegex.test(normalizedHost)) {\n                const domainLevels = getDomainLevels(normalizedHost);\n                const domainNodes = [];\n\n                // Create or reuse nodes for each domain level\n                for (const level of domainLevels) {\n                    let nodeId;\n                    if (!existingNodes.has(`domain:${level}`)) {\n                        nodeId = nextId++;\n                        const isApex = level === getApexDomain(normalizedHost);\n                        const node = {\n                            id: nodeId,\n                            type: 'domain',\n                            label: isApex ? `Apex: ${level}` : `Domain: ${level}`,\n                            title: isApex ? `Apex Domain: ${level}` : `Domain: ${level}`,\n                            color: { background: '#60a5fa' },\n                            domain: level,\n                            size: 20\n                        };\n                        newNodes.push(node);\n                        existingNodes.set(`domain:${level}`, { id: nodeId });\n                        domainNodes.push(node);\n                        console.log(`Added ${isApex ? 'Apex' : 'Domain'} node: ${level}, ID: ${nodeId}`);\n                    } else {\n                        nodeId = existingNodes.get(`domain:${level}`).id;\n                        domainNodes.push({ id: nodeId, domain: level });\n                    }\n                }\n\n                // Create edges for subdomains\n                for (let i = 0; i < domainNodes.length - 1; i++) {\n                    const fromId = domainNodes[i].id; // Higher level (e.g., test.co.uk)\n                    const toId = domainNodes[i + 1].id; // Lower level (e.g., co.uk)\n                    const edgeKey = `${fromId}_${toId}_Subdomain of`;\n                    if (!edgeSet.has(edgeKey)) {\n                        newEdges.push({\n                            id: `edge_${fromId}_${toId}_subdomain_${Date.now()}`,\n                            from: fromId,\n                            to: toId,\n                            label: 'Subdomain of'\n                        });\n                        edgeSet.add(edgeKey);\n                        console.log(`Linked Domain ${fromId} to Parent: ${toId}`);\n                    }\n                }\n\n                // Link URL to deepest subdomain (highest level)\n                if (domainNodes.length > 0) {\n                    const deepestNode = domainNodes[0];\n                    const edgeKey = `${urlNodeId}_${deepestNode.id}_Belongs to`;\n                    if (!edgeSet.has(edgeKey)) {\n                        newEdges.push({\n                            id: `edge_${urlNodeId}_${deepestNode.id}_belongs_${Date.now()}`,\n                            from: urlNodeId,\n                            to: deepestNode.id,\n                            label: 'Belongs to'\n                        });\n                        edgeSet.add(edgeKey);\n                        console.log(`Linked URL ${urlNodeId} to Domain: ${deepestNode.id}`);\n                    }\n                }\n            }\n\n            // Handle port\n            if (port && parseInt(port) >= 1 && parseInt(port) <= 65535) {\n                const portKey = `port:TCP/${port}`;\n                if (!existingNodes.has(portKey)) {\n                    const portNodeId = nextId++;\n                    newNodes.push({\n                        id: portNodeId,\n                        type: 'port',\n                        label: `TCP/${port}`,\n                        title: `Port\\nType: TCP\\nNumber: ${port}`,\n                        color: { background: '#a78bfa' },\n                        portType: 'TCP',\n                        portNumber: port,\n                        size: 10\n                    });\n                    existingNodes.set(portKey, { id: portNodeId });\n                    const edgeKey = `${urlNodeId}_${portNodeId}_Uses port`;\n                    if (!edgeSet.has(edgeKey)) {\n                        newEdges.push({\n                            id: `edge_${urlNodeId}_${portNodeId}_${Date.now()}`,\n                            from: urlNodeId,\n                            to: portNodeId,\n                            label: 'Uses port'\n                        });\n                        edgeSet.add(edgeKey);\n                        console.log(`Added Port node: TCP/${port}, ID: ${portNodeId}`);\n                    }\n                } else {\n                    const existingPortNode = existingNodes.get(portKey);\n                    const edgeKey = `${urlNodeId}_${existingPortNode.id}_Uses port`;\n                    if (!edgeSet.has(edgeKey)) {\n                        newEdges.push({\n                            id: `edge_${urlNodeId}_${existingPortNode.id}_${Date.now()}`,\n                            from: urlNodeId,\n                            to: existingPortNode.id,\n                            label: 'Uses port'\n                        });\n                        edgeSet.add(edgeKey);\n                    }\n                }\n            }\n        }\n        processedCount++;\n        if (processedCount % batchSize === 0) {\n            nodes.add(newNodes);\n            edges.add(newEdges);\n            newNodes.length = 0;\n            newEdges.length = 0;\n            await new Promise(resolve => setTimeout(resolve, 0)).catch(err => console.error('Batch processing error:', err));\n        }\n    }\n\n    // Process standalone Subnets\n    for (const subnet of subnetMatches) {\n        if (subnetRegex.test(subnet) && !existingNodes.has(`subnet:${subnet}`)) {\n            const subnetNodeData = createNodeData('subnet', { subnet });\n            if (subnetNodeData) {\n                subnetNodeData.id = nextId++;\n                newNodes.push(subnetNodeData);\n                existingNodes.set(`subnet:${subnet}`, { id: subnetNodeData.id });\n                console.log(`Added standalone Subnet node: ${subnet}, ID: ${subnetNodeData.id}`);\n            }\n        }\n        processedCount++;\n        if (processedCount % batchSize === 0) {\n            nodes.add(newNodes);\n            edges.add(newEdges);\n            newNodes.length = 0;\n            newEdges.length = 0;\n            await new Promise(resolve => setTimeout(resolve, 0)).catch(err => console.error('Batch processing error:', err));\n        }\n    }\n\n    // Process standalone IPs\n    for (const ip of ipMatches) {\n        if ((ipRegex.ipv4.test(ip) || ipRegex.ipv6.test(ip)) && !existingNodes.has(`ip:${ip}`)) {\n            const nodeId = nextId++;\n            newNodes.push({\n                id: nodeId,\n                type: 'ip',\n                label: `IP: ${ip}`,\n                title: `IP Address: ${ip}`,\n                color: { background: '#f87171' },\n                ip,\n                size: 20\n            });\n            existingNodes.set(`ip:${ip}`, { id: nodeId });\n            console.log(`Added standalone IP node: ${ip}, ID: ${nodeId}`);\n        }\n        processedCount++;\n        if (processedCount % batchSize === 0) {\n            nodes.add(newNodes);\n            edges.add(newEdges);\n            newNodes.length = 0;\n            newEdges.length = 0;\n            await new Promise(resolve => setTimeout(resolve, 0)).catch(err => console.error('Batch processing error:', err));\n        }\n    }\n\n    // Process standalone Domains\n    for (const domain of domainMatches) {\n        const domainLevels = getDomainLevels(domain);\n        const domainNodes = [];\n\n        // Create or reuse nodes for each domain level\n        for (const level of domainLevels) {\n            let nodeId;\n            if (!existingNodes.has(`domain:${level}`)) {\n                nodeId = nextId++;\n                const isApex = level === getApexDomain(domain);\n                const isMX = domain.endsWith('.'); // Heuristic for MX domains\n            const node = {\n                id: nodeId,\n                type: isMX ? 'mx' : 'domain', // Set type to mx if detected\n                label: isApex ? `Apex: ${level}` : `Domain: ${level}`,\n                title: isApex ? `Apex Domain: ${level}` : `Domain: ${level}`,\n                color: { background: isMX ? '#60a5fa' : '#60a5fa' }, // Blue for MX and domains\n                className: isMX ? 'mx-node' : 'domain-node', // Assign className\n                domain: level,\n                size: 20\n            };\n                newNodes.push(node);\n                existingNodes.set(`domain:${level}`, { id: nodeId });\n                domainNodes.push(node);\n                console.log(`Added ${isApex ? 'Apex' : 'Domain'} node: ${level}, ID: ${nodeId}`);\n            } else {\n                nodeId = existingNodes.get(`domain:${level}`).id;\n                domainNodes.push({ id: nodeId, domain: level });\n            }\n        }\n\n        // Create edges for subdomains\n        for (let i = 0; i < domainNodes.length - 1; i++) {\n            const fromId = domainNodes[i].id; // Higher level (e.g., test.co.uk)\n            const toId = domainNodes[i + 1].id; // Lower level (e.g., co.uk)\n            const edgeKey = `${fromId}_${toId}_Subdomain of`;\n            if (!edgeSet.has(edgeKey)) {\n                newEdges.push({\n                    id: `edge_${fromId}_${toId}_subdomain_${Date.now()}`,\n                    from: fromId,\n                    to: toId,\n                    label: 'Subdomain of'\n                });\n                edgeSet.add(edgeKey);\n                console.log(`Linked Domain ${fromId} to Parent: ${toId}`);\n            }\n        }\n        processedCount++;\n        if (processedCount % batchSize === 0) {\n            nodes.add(newNodes);\n            edges.add(newEdges);\n            newNodes.length = 0;\n            newEdges.length = 0;\n            await new Promise(resolve => setTimeout(resolve, 0)).catch(err => console.error('Batch processing error:', err));\n        }\n    }\n\n    // Process Emails\n    for (const email of emailMatches) {\n        if (!existingNodes.has(`email:${email}`)) {\n            const emailNodeId = nextId++;\n            const emailDomain = email.split('@')[1].replace(/\\.$/, '');\n            newNodes.push({\n                id: emailNodeId,\n                type: 'contact',\n                label: `Contact: ${email}`,\n                title: `Contact\\nEmail: ${email}`,\n                color: { background: '#4ade80' },\n                email,\n                name: email.split('@')[0],\n                size: 10\n            });\n            existingNodes.set(`email:${email}`, { id: emailNodeId });\n\n            if (domainRegex.test(emailDomain)) {\n                // Create or reuse node for the full email domain\n                let emailDomainNodeId;\n                if (!existingNodes.has(`domain:${emailDomain}`)) {\n                    emailDomainNodeId = nextId++;\n                    newNodes.push({\n                        id: emailDomainNodeId,\n                        type: 'domain',\n                        label: `Domain: ${emailDomain}`,\n                        title: `Domain: ${emailDomain}`,\n                        color: { background: '#60a5fa' },\n                        domain: emailDomain,\n                        size: 20\n                    });\n                    existingNodes.set(`domain:${emailDomain}`, { id: emailDomainNodeId });\n                    console.log(`Added Domain node: ${emailDomain}, ID: ${emailDomainNodeId}`);\n                } else {\n                    emailDomainNodeId = existingNodes.get(`domain:${emailDomain}`).id;\n                }\n\n                // Link email to the full email domain\n                const edgeKeyContact = `${emailNodeId}_${emailDomainNodeId}_Registered with`;\n                if (!edgeSet.has(edgeKeyContact)) {\n                    newEdges.push({\n                        id: `edge_${emailNodeId}_${emailDomainNodeId}_registered_${Date.now()}`,\n                        from: emailNodeId,\n                        to: emailDomainNodeId,\n                        label: 'is asociated with: '\n                    });\n                    edgeSet.add(edgeKeyContact);\n                    console.log(`Linked Email ${emailNodeId} to Domain: ${emailDomainNodeId}`);\n                }\n\n                // Create hierarchy for email domain (subdomains and apex)\n                const domainLevels = getDomainLevels(emailDomain);\n                const domainNodes = [];\n\n                // Create or reuse nodes for each domain level\n                for (const level of domainLevels) {\n                    let nodeId;\n                    if (!existingNodes.has(`domain:${level}`)) {\n                        nodeId = nextId++;\n                        const isApex = level === getApexDomain(emailDomain);\n                        const node = {\n                            id: nodeId,\n                            type: 'domain',\n                            label: isApex ? `Apex: ${level}` : `Domain: ${level}`,\n                            title: isApex ? `Apex Domain: ${level}` : `Domain: ${level}`,\n                            color: { background: '#60a5fa' },\n                            domain: level,\n                            size: 20\n                        };\n                        newNodes.push(node);\n                        existingNodes.set(`domain:${level}`, { id: nodeId });\n                        domainNodes.push(node);\n                        console.log(`Added ${isApex ? 'Apex' : 'Domain'} node: ${level}, ID: ${nodeId}`);\n                    } else {\n                        nodeId = existingNodes.get(`domain:${level}`).id;\n                        domainNodes.push({ id: nodeId, domain: level });\n                    }\n                }\n\n                // Create edges for subdomains\n                for (let i = 0; i < domainNodes.length - 1; i++) {\n                    const fromId = domainNodes[i].id; // Higher level (e.g., test.co.uk)\n                    const toId = domainNodes[i + 1].id; // Lower level (e.g., co.uk)\n                    const edgeKey = `${fromId}_${toId}_Subdomain of`;\n                    if (!edgeSet.has(edgeKey)) {\n                        newEdges.push({\n                            id: `edge_${fromId}_${toId}_subdomain_${Date.now()}`,\n                            from: fromId,\n                            to: toId,\n                            label: 'Subdomain of'\n                        });\n                        edgeSet.add(edgeKey);\n                        console.log(`Linked Domain ${fromId} to Parent: ${toId}`);\n                    }\n                }\n            }\n        }\n        processedCount++;\n        if (processedCount % batchSize === 0) {\n            nodes.add(newNodes);\n            edges.add(newEdges);\n            newNodes.length = 0;\n            newEdges.length = 0;\n            await new Promise(resolve => setTimeout(resolve, 0)).catch(err => console.error('Batch processing error:', err));\n        }\n    }\n\n    // Process Hashes\n    for (const hashObj of hashMatches) {\n        const hash = hashObj.value;\n        if (!existingNodes.has(`hash:${hash}`)) {\n            const hashNodeId = nextId++;\n            const hashType = hashObj.type.toUpperCase();\n            newNodes.push({\n                id: hashNodeId,\n                type: 'hash',\n                label: `${hashType}: ${hash.substring(0, 8)}...`,\n                title: `File Hash\\nType: ${hashType}\\nValue: ${hash}`,\n                color: { background: '#f97316' },\n                hash,\n                hashType,\n                size: 15\n            });\n            existingNodes.set(`hash:${hash}`, { id: hashNodeId });\n            console.log(`Added Hash node: ${hash}, Type: ${hashType}, ID: ${hashNodeId}`);\n        }\n        processedCount++;\n        if (processedCount % batchSize === 0) {\n            nodes.add(newNodes);\n            edges.add(newEdges);\n            newNodes.length = 0;\n            newEdges.length = 0;\n            await new Promise(resolve => setTimeout(resolve, 0)).catch(err => console.error('Batch processing error:', err));\n        }\n    }\n\n    // Add remaining items\n    if (newNodes.length > 0) {\n        console.log('Final batch processing:', { newNodes, newEdges });\n        nodes.add(newNodes);\n        edges.add(newEdges);\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork().catch(err => console.error('Error stabilizing network:', err));\n    showToast(`Imported ${urlMatches.size} URLs, ${ipMatches.size} IPs, ${subnetMatches.size} subnets, ${domainMatches.size} domains, ${emailMatches.size} emails, ${hashMatches.size} hashes`, 'success');\n}\n//End of Process IOCs\n\n\ndocument.addEventListener('keypress', event => {\n            if (event.key === 'Enter') {\n                event.preventDefault();\n                const activeElement = document.activeElement;\n                if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) {\n                    const start = activeElement.selectionStart; \n                    const end = activeElement.selectionEnd; \n                    const value = activeElement.value;\n                    activeElement.value = value.substring(0, start) + '\\r\\n' + value.substring(end);\n                    activeElement.selectionStart = activeElement.selectionEnd = start + 2;\n                }\n            }\n        });\n\n   \n        function toggleNodeLabels() {\n    nodeLabelsVisible = document.getElementById('showNodeLabels').checked;\n    \n    // Update each node individually while preserving original labels\n    nodes.forEach(node => {\n        // Ensure original label is stored\n        if (!node.hasOwnProperty('originalLabel')) {\n            node.originalLabel = node.label || '';\n        }\n        \n        nodes.update({\n            id: node.id,\n            label: nodeLabelsVisible ? node.originalLabel : '',  // Toggle between original and empty\n            font: {\n                size: nodeLabelsVisible ? 12 : 0,\n                color: isDarkMode ? '#e2e8f0' : '#1f2a44',\n                multi: true,\n                align: 'center',\n                vadjust: 0,\n                strokeWidth: 0\n            }\n        });\n    });\n    \n    // Update global network options\n    network.setOptions({\n        nodes: {\n            font: {\n                size: nodeLabelsVisible ? 12 : 0,\n                color: isDarkMode ? '#e2e8f0' : '#1f2a44',\n                multi: true,\n                align: 'center',\n                vadjust: 0,\n                strokeWidth: 0\n            },\n            chosen: {\n                label: function(values, id, selected, hovering) {\n                    values.size = nodeLabelsVisible ? 12 : 0;\n                }\n            }\n        }\n    });\n    \n    // Force a full network refresh\n    network.setData({ nodes: nodes, edges: edges });  // Reset data to force update\n    network.stabilize(100);\n    network.redraw();\n    saveStateAfterOperation();\n}\n\n  \n\nfunction toggleEdgeLabels() {\n    edgeLabelsVisible = document.getElementById('showEdgeLabels').checked;\n    \n    // Update each edge individually while preserving original labels\n    edges.forEach(edge => {\n        // Ensure original label is stored\n        if (!edge.hasOwnProperty('originalLabel')) {\n            edge.originalLabel = edge.label || '';\n        }\n        \n        edges.update({\n            id: edge.id,\n            label: edgeLabelsVisible ? edge.originalLabel : '',  // Toggle between original and empty\n            font: {\n                size: edgeLabelsVisible ? 12 : 0,\n                color: isDarkMode ? '#e2e8f0' : '#1f2a44',\n                strokeWidth: 0,\n                strokeColor: 'transparent',\n                align: 'middle',\n                multi: true\n            }\n        });\n    });\n    \n    // Update global network options\n    network.setOptions({\n        edges: {\n            font: {\n                size: edgeLabelsVisible ? 12 : 0,\n                color: isDarkMode ? '#e2e8f0' : '#1f2a44',\n                strokeWidth: 0,\n                strokeColor: 'transparent',\n                align: 'middle',\n                multi: true\n            },\n            chosen: {\n                label: function(values, id, selected, hovering) {\n                    values.size = edgeLabelsVisible ? 12 : 0;\n                }\n            }\n        }\n    });\n    \n    // Force a full network refresh\n    network.setData({ nodes: nodes, edges: edges });  // Reset data to force update\n    network.stabilize(100);\n    network.redraw();\n    saveStateAfterOperation();\n}\n\nfunction updateLabelVisibility() {\n    network.setOptions({\n        nodes: {\n            font: {\n                size: nodeLabelsVisible ? 12 : 0,\n                color: isDarkMode ? '#e2e8f0' : '#1f2a44'\n            }\n        },\n        edges: {\n            font: {\n                size: edgeLabelsVisible ? 12 : 0,\n                color: isDarkMode ? '#e2e8f0' : '#1f2a44'\n            }\n        }\n    });\n    network.redraw();\n}\n\nfunction toggleIsolatedNodes() {\n    const hideIsolated = document.getElementById('hideIsolatedNodes').checked;\n    \n    nodes.forEach(node => {\n        const connections = edges.get({\n            filter: edge => edge.from === node.id || edge.to === node.id\n        });\n        \n        nodes.update({\n            id: node.id,\n            hidden: hideIsolated && connections.length === 0\n        });\n    });\n    \n    stabilizeNetwork().then(() => {\n        network.fit({\n            animation: {\n                duration: 300,\n                easingFunction: 'easeInOutQuad'\n            }\n        });\n    });\n    \n    saveStateAfterOperation();\n}\n\n\nasync function processShodanData(ipNodeId, data) {\n    // Ensure nodes and edges are defined (assuming vis.js DataSets)\n    if (!nodes || !edges) {\n        console.error('Nodes or edges DataSet not initialized');\n        return;\n    }\n\n    // Deduplication maps\n    let existingPorts = new Map(nodes.get({ filter: n => n.type === 'port' }).map(n => [`${n.portType}/${n.portNumber}`, n.id]));\n    let existingDomains = new Map(nodes.get({ filter: n => n.type === 'domain' }).map(n => [n.domain, n.id]));\n    let existingSslHashes = new Map(nodes.get({ filter: n => n.type === 'ssl_hash' }).map(n => [n.hash, n.id]));\n    let existingHtmlHashes = new Map(nodes.get({ filter: n => n.type === 'html_hash' }).map(n => [n.hash, n.id]));\n    let existingOs = new Map(nodes.get({ filter: n => n.type === 'os' }).map(n => [n.os, n.id]));\n    let existingProducts = new Map(nodes.get({ filter: n => n.type === 'product' }).map(n => [n.product, n.id]));\n    let existingHttpHashes = new Map(nodes.get({ filter: n => n.type === 'http_hash' }).map(n => [n.hash, n.id]));\n    let existingTitles = new Map(nodes.get({ filter: n => n.type === 'http_title' }).map(n => [n.title, n.id]));\n    let existingFavicons = new Map(nodes.get({ filter: n => n.type === 'favicon' }).map(n => [n.hash, n.id]));\n    let existingPortTitles = new Map(nodes.get({ filter: n => n.type === 'port_title' }).map(n => [`${n.portType}/${n.portNumber}/${n.title}`, n.id]));\n\n    const newNodes = [];\n    const newEdges = [];\n\n    console.log(`Processing Shodan data for IP: ${ipNodeId}`);\n\n    // Top-level domains and hostnames linked to IP\n    if (data.domains && Array.isArray(data.domains)) {\n        data.domains.forEach(domain => {\n            let domainId = existingDomains.get(domain);\n            if (!domainId) {\n                domainId = nextId++;\n                newNodes.push({\n                    id: domainId,\n                    type: 'domain',\n                    label: `Domain: ${domain}`,\n                    title: `Domain: ${domain}`,\n                    color: { background: '#60a5fa' },\n                    domain: domain\n                });\n                existingDomains.set(domain, domainId);\n            }\n            const edgeId = `${ipNodeId}-${domainId}-ResolvesTo`;\n            if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                newEdges.push({ id: edgeId, from: ipNodeId, to: domainId, label: 'Resolves to' });\n            }\n        });\n    }\n\n    if (data.hostnames && Array.isArray(data.hostnames)) {\n        data.hostnames.forEach(hostname => {\n            let domainId = existingDomains.get(hostname);\n            if (!domainId) {\n                domainId = nextId++;\n                newNodes.push({\n                    id: domainId,\n                    type: 'domain',\n                    label: `Hostname: ${hostname}`,\n                    title: `Hostname: ${hostname}`,\n                    color: { background: '#60a5fa' },\n                    domain: hostname\n                });\n                existingDomains.set(hostname, domainId);\n            }\n            const edgeId = `${ipNodeId}-${domainId}-ResolvesTo`;\n            if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                newEdges.push({ id: edgeId, from: ipNodeId, to: domainId, label: 'Resolves to' });\n            }\n        });\n    }\n\n    // Process nested data array for port-specific entities\n    if (data.data && Array.isArray(data.data)) {\n        for (const banner of data.data) {\n            // Port creation\n            let portId = null;\n            if (banner.port && banner.transport) {\n                const portKey = `${banner.transport.toUpperCase()}/${banner.port}`;\n                portId = existingPorts.get(portKey);\n                if (!portId) {\n                    portId = nextId++;\n                    newNodes.push({\n                        id: portId,\n                        type: 'port',\n                        label: `${banner.transport.toUpperCase()}/${banner.port}`,\n                        title: `Port\\nType: ${banner.transport.toUpperCase()}\\nNumber: ${banner.port}`,\n                        color: { background: '#a78bfa' },\n                        portType: banner.transport.toUpperCase(),\n                        portNumber: banner.port.toString()\n                    });\n                    existingPorts.set(portKey, portId);\n                    console.log(`Created port node: ${portKey} with ID: ${portId}`);\n                }\n                const edgeId = `${ipNodeId}-${portId}-Exposes`;\n                if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                    newEdges.push({ id: edgeId, from: ipNodeId, to: portId, label: 'Exposes' });\n                    console.log(`Linked port ${portKey} to IP with edge: ${edgeId}`);\n                }\n            } else {\n                console.warn(`No port or transport found in banner:`, banner);\n            }\n\n            // Operating System (linked to IP)\n            if (banner.os) {\n                let osId = existingOs.get(banner.os);\n                if (!osId) {\n                    osId = nextId++;\n                    newNodes.push({\n                        id: osId,\n                        type: 'os',\n                        label: `OS: ${banner.os}`,\n                        title: `Operating System: ${banner.os}`,\n                        color: { background: '#10b981' },\n                        os: banner.os\n                    });\n                    existingOs.set(banner.os, osId);\n                }\n                const edgeId = `${ipNodeId}-${osId}-Runs`;\n                if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                    newEdges.push({ id: edgeId, from: ipNodeId, to: osId, label: 'Runs' });\n                }\n            }\n\n            // Products (linked to IP)\n            if (banner.product) {\n                let productId = existingProducts.get(banner.product);\n                if (!productId) {\n                    productId = nextId++;\n                    newNodes.push({\n                        id: productId,\n                        type: 'product',\n                        label: `Product: ${banner.product}`,\n                        title: `Product: ${banner.product}`,\n                        color: { background: '#ec4899' },\n                        product: banner.product\n                    });\n                    existingProducts.set(banner.product, productId);\n                }\n                const edgeId = `${ipNodeId}-${productId}-Uses`;\n                if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                    newEdges.push({ id: edgeId, from: ipNodeId, to: productId, label: 'Uses' });\n                }\n            }\n\n            // HTTP Hash (linked to port if available)\n            if (banner.http && banner.http.headers_hash && portId) {\n                const httpHash = banner.http.headers_hash.toString();\n                let httpHashId = existingHttpHashes.get(httpHash);\n                if (!httpHashId) {\n                    httpHashId = nextId++;\n                    newNodes.push({\n                        id: httpHashId,\n                        type: 'http_hash',\n                        label: `HTTP Hash: ${httpHash.substring(0, 8)}...`,\n                        title: `HTTP Headers Hash\\nValue: ${httpHash}`,\n                        color: { background: '#f97316' },\n                        hash: httpHash\n                    });\n                    existingHttpHashes.set(httpHash, httpHashId);\n                }\n                const edgeId = `${portId}-${httpHashId}-Serves`;\n                if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                    newEdges.push({ id: edgeId, from: portId, to: httpHashId, label: 'Serves' });\n                    console.log(`Linked HTTP Hash ${httpHash} to port ${portId} with edge: ${edgeId}`);\n                }\n            }\n\n            // Favicon (linked to port if available)\n            if (banner.http && banner.http.favicon && banner.http.favicon.hash && portId) {\n                const faviconHash = banner.http.favicon.hash.toString();\n                let faviconId = existingFavicons.get(faviconHash);\n                if (!faviconId) {\n                    faviconId = nextId++;\n                    newNodes.push({\n                        id: faviconId,\n                        type: 'favicon',\n                        label: `Favicon: ${faviconHash.substring(0, 8)}...`,\n                        title: `Favicon\\nHash: ${faviconHash}\\nPath: ${banner.http.favicon.location || 'N/A'}`,\n                        color: { background: '#22d3ee' },\n                        hash: faviconHash\n                    });\n                    existingFavicons.set(faviconHash, faviconId);\n                }\n                const edgeId = `${portId}-${faviconId}-Serves`;\n                if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                    newEdges.push({ id: edgeId, from: portId, to: faviconId, label: 'Serves' });\n                    console.log(`Linked Favicon ${faviconHash} to port ${portId} with edge: ${edgeId}`);\n                }\n            }\n\n            // HTTP Title and Port Title (linked to port if available)\n            if (banner.http && banner.http.title && portId) {\n                // HTTP Title\n                let titleId = existingTitles.get(banner.http.title);\n                if (!titleId) {\n                    titleId = nextId++;\n                    newNodes.push({\n                        id: titleId,\n                        type: 'http_title',\n                        label: `Title: ${banner.http.title}`,\n                        title: `HTTP Title: ${banner.http.title}`,\n                        color: { background: '#3b82f6' },\n                        title: banner.http.title\n                    });\n                    existingTitles.set(banner.http.title, titleId);\n                }\n                const titleEdgeId = `${portId}-${titleId}-HasTitle`;\n                if (!newEdges.some(e => e.id === titleEdgeId) && !edges.get(titleEdgeId)) {\n                    newEdges.push({ id: titleEdgeId, from: portId, to: titleId, label: 'Has Title' });\n                    console.log(`Linked HTTP Title \"${banner.http.title}\" to port ${portId} with edge: ${titleEdgeId}`);\n                }\n\n                // Port-Specific Title\n                const portTitleKey = `${banner.transport.toUpperCase()}/${banner.port}/${banner.http.title}`;\n                let portTitleId = existingPortTitles.get(portTitleKey);\n                if (!portTitleId) {\n                    portTitleId = nextId++;\n                    newNodes.push({\n                        id: portTitleId,\n                        type: 'port_title',\n                        label: `Title ${banner.transport.toUpperCase()}/${banner.port}`,\n                        title: `Port Title\\nPort: ${banner.transport.toUpperCase()}/${banner.port}\\nTitle: ${banner.http.title}`,\n                        color: { background: '#4b5e40' },\n                        portType: banner.transport.toUpperCase(),\n                        portNumber: banner.port.toString(),\n                        title: banner.http.title\n                    });\n                    existingPortTitles.set(portTitleKey, portTitleId);\n                }\n                const portTitleEdgeId = `${portId}-${portTitleId}-HasPortTitle`;\n                if (!newEdges.some(e => e.id === portTitleEdgeId) && !edges.get(portTitleEdgeId)) {\n                    newEdges.push({ id: portTitleEdgeId, from: portId, to: portTitleId, label: 'Has Port Title' });\n                    console.log(`Linked Port Title \"${portTitleKey}\" to port ${portId} with edge: ${portTitleEdgeId}`);\n                }\n            }\n\n            // HTML Hash (linked to port if available)\n            if (banner.http && banner.http.html_hash && portId) {\n                const htmlHash = banner.http.html_hash.toString();\n                let htmlId = existingHtmlHashes.get(htmlHash);\n                if (!htmlId) {\n                    htmlId = nextId++;\n                    newNodes.push({\n                        id: htmlId,\n                        type: 'html_hash',\n                        label: `HTML Hash: ${htmlHash.substring(0, 8)}...`,\n                        title: `HTML Hash\\nValue: ${htmlHash}`,\n                        color: { background: '#f59e0b' },\n                        hash: htmlHash\n                    });\n                    existingHtmlHashes.set(htmlHash, htmlId);\n                }\n                const edgeId = `${portId}-${htmlId}-Serves`;\n                if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                    newEdges.push({ id: edgeId, from: portId, to: htmlId, label: 'Serves' });\n                    console.log(`Linked HTML Hash ${htmlHash} to port ${portId} with edge: ${edgeId}`);\n                }\n            }\n\n            // SSL Hash (linked to port if available)\n            if (banner.ssl && banner.ssl.cert && banner.ssl.cert.fingerprint && banner.ssl.cert.fingerprint.sha256 && portId) {\n                const sslHash = banner.ssl.cert.fingerprint.sha256;\n                let sslId = existingSslHashes.get(sslHash);\n                if (!sslId) {\n                    sslId = nextId++;\n                    newNodes.push({\n                        id: sslId,\n                        type: 'ssl_hash',\n                        label: `SSL Hash: ${sslHash.substring(0, 8)}...`,\n                        title: `SSL Certificate Hash\\nSHA256: ${sslHash}`,\n                        color: { background: '#8b5cf6' },\n                        hash: sslHash\n                    });\n                    existingSslHashes.set(sslHash, sslId);\n                }\n                const edgeId = `${portId}-${sslId}-Uses`;\n                if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {\n                    newEdges.push({ id: edgeId, from: portId, to: sslId, label: 'Uses' });\n                    console.log(`Linked SSL Hash ${sslHash} to port ${portId} with edge: ${edgeId}`);\n                }\n            }\n        }\n    }\n\n    console.log('New Nodes:', newNodes);\n    console.log('New Edges:', newEdges);\n\n    if (newNodes.length > 0) {\n        try {\n            nodes.add(newNodes);\n            console.log(`Added ${newNodes.length} nodes`);\n        } catch (error) {\n            console.error('Error adding nodes:', error);\n        }\n    }\n    if (newEdges.length > 0) {\n        try {\n            edges.add(newEdges);\n            console.log(`Added ${newEdges.length} edges`);\n        } catch (error) {\n            console.error('Error adding edges:', error);\n        }\n    }\n}\n\n\n\n\n\n// Password toggle functionality\ndocument.querySelectorAll('.toggle-password').forEach(button => {\n    button.addEventListener('click', function() {\n        const targetId = this.getAttribute('data-target');\n        const input = document.getElementById(targetId);\n        if (input.type === 'password') {\n            input.type = 'text';\n            this.textContent = 'Hide';\n        } else {\n            input.type = 'password';\n            this.textContent = 'Show';\n        }\n    });\n});\n\n\n\nconst stateLoaded = loadState();\n    document.getElementById('stop-task').disabled = true;\n\n    if (!stateLoaded) {\n        console.log('No saved state found or loading failed, applying defaults');\n        updateTheme();\n        //ensureInteractionSettings();\n    } else {\n        // Force UI and network update after successful load\n        updateSelectOptions();\n        updateEdgeSelectOptions();\n        stabilizeNetwork().then(() => {\n            network.fit({ \n                animation: { \n                    duration: 500, \n                    easingFunction: 'easeInOutQuad' \n                } \n            });\n        });\n    }\n\n    // Update UI elements regardless of state\n    updateSelectOptions();\n\n    if (window.innerWidth <= 768) {\n        document.getElementById('controls').classList.add('collapsed');\n    }\n\n    setInterval(saveState, 5 * 60 * 1000);\n\n    // Test Harness for Network Graph Visualization Tool\n    async function runAllTests() {\n        console.log(\"Starting All Tests...\");\n        showToast(\"All Tests Started\", \"info\");\n\n        // Helper function to wait\n        const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));\n\n        // Step 1: Add a Domain Entity (dns.google.com)\n        console.log(\"Step 1: Adding dns.google.com entity\");\n        document.getElementById('addEntityType').value = 'domain';\n        document.getElementById('addDomainInput').value = 'dns.google.com';\n        document.getElementById('addDomainInput').style.display = 'block';\n        addNode();\n        await wait(1000); // Wait for node addition to complete\n\n        // Verify node was added\n        const domainNode = nodes.get({ filter: n => n.type === 'domain' && n.domain === 'dns.google.com' })[0];\n        if (!domainNode) {\n            console.error(\"Failed to add dns.google.com node\");\n            showToast(\"Failed to add dns.google.com\", \"error\");\n            return;\n        }\n        console.log(\"dns.google.com added with ID:\", domainNode.id);\n        showToast(\"Added dns.google.com\", \"success\");\n\n        // Step 2: Run Enrichment Functions\n        console.log(\"Step 2: Running Enrichment Functions\");\n\n        // Google DNS Enrichment (should resolve to IPs like 8.8.8.8)\n        console.log(\"Enriching with Google DNS...\");\n        await throttledEnrichGoogleDNS('dns.google.com', domainNode.id);\n        await wait(2000);\n\n        // Get an IP node to enrich further (e.g., 8.8.8.8)\n        const ipNode = nodes.get({ filter: n => n.type === 'ip' && n.ip })[0];\n        if (ipNode) {\n            console.log(\"Found IP node to enrich:\", ipNode.ip);\n\n            // IPinfo Enrichment\n            console.log(\"Enriching with IPinfo...\");\n            await throttledEnrichIP(ipNode.ip, ipNode.id);\n            await wait(2000);\n\n            // Shodan Enrichment\n            console.log(\"Enriching with Shodan...\");\n            await throttledEnrichShodan(ipNode.ip, ipNode.id);\n            await wait(2000);\n\n            // InternetDB Enrichment\n            console.log(\"Enriching with InternetDB...\");\n            await throttledEnrichInternetDB(ipNode.ip, ipNode.id);\n            await wait(2000);\n        } else {\n            console.warn(\"No IP node found after Google DNS enrichment\");\n            showToast(\"No IP found to enrich further\", \"warning\");\n        }\n\n        // Step 3: Test Bulk Enrichment\n        console.log(\"Step 3: Testing Bulk Enrichment\");\n        await enrichAllIpinfo();\n        await wait(2000);\n        await enrichAllShodan();\n        await wait(2000);\n        await enrichAllInternetDB();\n        await wait(2000);\n        await enrichAllGoogleDNS();\n        await wait(2000);\n\n        // Step 4: Test Layouts\n        console.log(\"Step 4: Testing Layouts\");\n        setOrganicLayout();\n        await wait(1000);\n        setCircularLayout();\n        await wait(1000);\n        setOrthogonalLayout();\n        await wait(1000);\n        setTreeLayout();\n        await wait(1000);\n        setHierarchicalLayout();\n        await wait(1000);\n\n        // Step 5: Test Import/Export\n        console.log(\"Step 5: Testing Import/Export\");\n        exportGraph(); // Export current graph\n        await wait(1000);\n        // Simulate importing IOCs\n        document.getElementById('iocText').value = \"8.8.4.4\\nexample.com\";\n        importIOCsFromText();\n        await wait(1000);\n\n        // Step 6: Test Physics Toggle\n        console.log(\"Step 6: Testing Physics Toggle\");\n        togglePhysics(); // Pause\n        await wait(1000);\n        togglePhysics(); // Resume\n        await wait(1000);\n\n        // Step 7: Test Mode Toggle\n        console.log(\"Step 7: Testing Mode Toggle\");\n        toggleMode(); // Switch to light/dark\n        await wait(1000);\n        toggleMode(); // Switch back\n        await wait(1000);\n\n        // Step 8: Test Label Visibility\n        console.log(\"Step 8: Testing Label Visibility\");\n        document.getElementById('showNodeLabels').checked = false;\n        toggleNodeLabels();\n        await wait(1000);\n        document.getElementById('showNodeLabels').checked = true;\n        toggleNodeLabels();\n        await wait(1000);\n\n        // Step 9: Clean Up - Delete the Original Node\n        console.log(\"Step 9: Cleaning Up\");\n        if (domainNode) {\n            document.getElementById('removeNode').value = domainNode.id;\n            removeNode();\n            await wait(1000);\n            console.log(\"dns.google.com removed\");\n            showToast(\"Removed dns.google.com\", \"success\");\n        }\n\n        console.log(\"All Tests Completed\");\n        showToast(\"All Tests Completed\", \"success\");\n        clearGraph();\n    }\n\n    function showProgressBar() {\n    const progressBar = document.getElementById('progress-bar');\n    progressBar.textContent = 'Task in progress...';\n    progressBar.classList.remove('progress-hidden', 'progress-complete');\n    progressBar.classList.add('progress-active');\n}\n\nfunction completeProgressBar() {\n    const progressBar = document.getElementById('progress-bar');\n    progressBar.textContent = 'Task Complete';\n    progressBar.classList.remove('progress-active');\n    progressBar.classList.add('progress-complete');\n    \n    setTimeout(() => {\n        progressBar.classList.add('progress-hidden');\n    }, 5000); // Hide after 5 seconds\n}\n\nfunction showGraphSummary() {\n    // Get all nodes and count by type\n    const typeCounts = {};\n    nodes.forEach(node => {\n        typeCounts[node.type] = (typeCounts[node.type] || 0) + 1;\n    });\n    \n    // Build the table\n    const tbody = document.querySelector('#summary-table tbody');\n    tbody.innerHTML = ''; // Clear existing content\n    \n    const types = Object.keys(typeCounts).sort();\n    types.forEach(type => {\n        const row = document.createElement('tr');\n        row.innerHTML = `\n            <td>${type.charAt(0).toUpperCase() + type.slice(1)}</td>\n            <td>${typeCounts[type]}</td>\n        `;\n        tbody.appendChild(row);\n    });\n    \n    // Add total row\n    const totalRow = document.createElement('tr');\n    totalRow.innerHTML = `\n        <td><strong>Total</strong></td>\n        <td><strong>${nodes.length}</strong></td>\n    `;\n    tbody.appendChild(totalRow);\n    \n    // Show the modal\n    document.getElementById('summary-modal').style.display = 'block';\n    \n    // Optional: Add an overlay to dim the background\n    if (!document.getElementById('modal-overlay')) {\n        const overlay = document.createElement('div');\n        overlay.id = 'modal-overlay';\n        overlay.style.cssText = `\n            position: fixed;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background: rgba(0, 0, 0, 0.5);\n            z-index: 1999;\n        `;\n        document.body.appendChild(overlay);\n    }\n}\n\nfunction hideGraphSummary() {\n    document.getElementById('summary-modal').style.display = 'none';\n    const overlay = document.getElementById('modal-overlay');\n    if (overlay) overlay.remove();\n}\n\n// Close modal when clicking outside\ndocument.addEventListener('click', (event) => {\n    const modal = document.getElementById('summary-modal');\n    if (modal.style.display === 'block' && !modal.contains(event.target) && event.target.id !== 'summary-button') {\n        hideGraphSummary();\n    }\n});\n\nfunction toggleMenu() {\n    const controls = document.getElementById('controls');\n    const menuToggle = document.getElementById('menu-toggle');\n    const propertiesPanel = document.getElementById('properties-panel');\n    const myNetwork = document.getElementById('myNetwork');\n    \n    controls.classList.toggle('collapsed');\n    \n    if (controls.classList.contains('collapsed')) {\n        menuToggle.textContent = '>'; \n        menuToggle.style.transform = 'rotate(0deg)';\n        myNetwork.style.marginLeft = '50px';\n    } else {\n        menuToggle.textContent = '<'; \n        menuToggle.style.transform = 'rotate(0deg)';\n        myNetwork.style.marginLeft = '300px';\n    }\n\n    myNetwork.style.marginRight = propertiesPanel.classList.contains('active') ? '300px' : '0';\n    \n    network.fit({\n        animation: { duration: 300, easingFunction: 'easeInOutQuad' }\n    });\n}\n\n\n\nconst throttledEnrichIPMultiple = throttleRequest(async function enrichIPMultiple(ips, nodeIds) {\n    if (!Array.isArray(ips) || !Array.isArray(nodeIds) || ips.length !== nodeIds.length) {\n        showToast('Invalid input for multiple IP enrichment', 'error');\n        return;\n    }\n\n    if (!ipinfoApiKey && !ignoreApiKeysViaProxy) {\n        showToast('Please set your IPinfo API key in the \"API Keys\" tab first.', 'error');\n        return;\n    }\n\n    for (let i = 0; i < ips.length; i++) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('IPinfo enrichment stopped', 'info');\n            break;\n        }\n        await throttledEnrichIP(ips[i], nodeIds[i], true); // isBulk = true\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    showToast(`Enriched ${ips.length} IPs with IPinfo`, 'success');\n}, RATE_LIMIT_MS);\n\nconst throttledEnrichShodanMultiple = throttleRequest(async function enrichShodanMultiple(targets, nodeIds) {\n    if (!Array.isArray(targets) || !Array.isArray(nodeIds) || targets.length !== nodeIds.length) {\n        showToast('Invalid input for multiple Shodan enrichment', 'error');\n        return;\n    }\n\n    if (!shodanApiKey && !ignoreApiKeysViaProxy) {\n        showToast('Please set your Shodan API key in the \"API Keys\" tab first.', 'error');\n        return;\n    }\n\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const totalTargets = targets.length;\n    let processedTargets = 0;\n\n    showToast(`Starting Shodan enrichment for ${totalTargets} targets`, 'info');\n\n    const batchSize = 5;\n    const delayBetweenBatches = 100;\n    const totalBatches = Math.ceil(totalTargets / batchSize);\n    const timePerBatchMs = SHODAN_RATE_LIMIT_MS * batchSize;\n    const totalBatchDelays = (totalBatches - 1) * delayBetweenBatches;\n    const estimatedTimeMs = (timePerBatchMs * totalBatches) + totalBatchDelays + 1000;\n\n    const estimatedSeconds = Math.ceil(estimatedTimeMs / 1000);\n    const estimatedMinutes = Math.floor(estimatedSeconds / 60);\n    const remainingSeconds = estimatedSeconds % 60;\n    const timeEstimateStr = estimatedMinutes > 0 \n        ? `${estimatedMinutes}m ${remainingSeconds}s` \n        : `${estimatedSeconds}s`;\n\n    showToast(`Estimated time for Shodan enrichment: ~${timeEstimateStr}`, 'info');\n    document.getElementById('progress-bar').textContent = `Shodan Enrichment: 0/${totalTargets} Targets (0%) - Est. ${timeEstimateStr}`;\n\n    let lastProgressUpdate = 0;\n    const progressUpdateInterval = 500;\n    const startTime = Date.now();\n\n    for (let i = 0; i < totalTargets; i += batchSize) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('Shodan enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = `Shodan Enrichment: Stopped at ${processedTargets}/${totalTargets} Targets`;\n            break;\n        }\n\n        const batchTargets = targets.slice(i, Math.min(i + batchSize, totalTargets));\n        const batchNodeIds = nodeIds.slice(i, Math.min(i + batchSize, totalTargets));\n\n        for (let j = 0; j < batchTargets.length; j++) {\n            await throttledEnrichShodan(batchTargets[j], batchNodeIds[j], true); // isBulk = true\n            processedTargets++;\n        }\n\n        const currentTime = Date.now();\n        if (currentTime - lastProgressUpdate >= progressUpdateInterval || processedTargets === totalTargets) {\n            const progress = ((processedTargets / totalTargets) * 100).toFixed(1);\n            const remainingTargets = totalTargets - processedTargets;\n            const remainingTimeMs = remainingTargets * SHODAN_RATE_LIMIT_MS;\n            const remainingSeconds = Math.ceil(remainingTimeMs / 1000);\n            const remainingMinutes = Math.floor(remainingSeconds / 60);\n            const remainingSecondsPart = remainingSeconds % 60;\n            const remainingTimeStr = remainingMinutes > 0 \n                ? `${remainingMinutes}m ${remainingSecondsPart}s` \n                : `${remainingSeconds}s`;\n\n            document.getElementById('progress-bar').textContent = \n                `Shodan Enrichment: ${processedTargets}/${totalTargets} Targets (${progress}%) - Est. ${remainingTimeStr} remaining`;\n            lastProgressUpdate = currentTime;\n            updateSelectOptions();\n        }\n\n        await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    //ensureInteractionSettings();\n\n    if (!(activeTaskController && activeTaskController.signal.aborted)) {\n        completeProgressBar();\n        showToast(`Shodan enrichment completed: ${processedTargets}/${totalTargets} targets enriched`, 'success');\n    } else {\n        showToast(`Shodan enrichment stopped: ${processedTargets}/${totalTargets} targets processed`, 'info');\n    }\n\n    if (window.innerWidth <= 768) {\n        const controls = document.getElementById('controls');\n        controls.classList.add('collapsed');\n        document.getElementById('myNetwork').style.display = 'block';\n        network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });\n    }\n}, SHODAN_RATE_LIMIT_MS);\n\n\n\n\n\n\n\n\n\n\nconst throttledEnrichInternetDBMultiple = throttleRequest(async function enrichInternetDBMultiple(ips, nodeIds) {\n    if (!Array.isArray(ips) || !Array.isArray(nodeIds) || ips.length !== nodeIds.length) {\n        showToast('Invalid input for multiple InternetDB enrichment', 'error');\n        return;\n    }\n\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const totalIPs = ips.length;\n    let successfulEnrichments = 0;\n\n    showToast(`Starting InternetDB enrichment for ${totalIPs} IPs`, 'info');\n\n    const batchSize = 50; // Consistent with other bulk functions\n    const delayBetweenBatches = 200; // Consistent with other bulk functions\n    const totalBatches = Math.ceil(totalIPs / batchSize);\n    const assumedRequestTimeMs = 100; // Estimated time per request\n    const timePerBatchMs = assumedRequestTimeMs * batchSize;\n    const totalBatchDelays = (totalBatches - 1) * delayBetweenBatches;\n    const estimatedTimeMs = (timePerBatchMs * totalBatches) + totalBatchDelays + 1000;\n\n    const estimatedSeconds = Math.ceil(estimatedTimeMs / 1000);\n    const estimatedMinutes = Math.floor(estimatedSeconds / 60);\n    const remainingSeconds = estimatedSeconds % 60;\n    const timeEstimateStr = estimatedMinutes > 0 \n        ? `${estimatedMinutes}m ${remainingSeconds}s` \n        : `${estimatedSeconds}s`;\n\n    showToast(`Estimated time for InternetDB enrichment: ~${timeEstimateStr}`, 'info');\n    document.getElementById('progress-bar').textContent = `InternetDB Enrichment: 0/${totalIPs} IPs (0%) - Est. ${timeEstimateStr}`;\n\n    let lastProgressUpdate = 0;\n    const progressUpdateInterval = 1000;\n    const startTime = Date.now();\n\n    for (let i = 0; i < totalIPs; i += batchSize) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('InternetDB enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = `InternetDB Enrichment: Stopped at ${successfulEnrichments}/${totalIPs} IPs`;\n            break;\n        }\n\n        const batchIPs = ips.slice(i, Math.min(i + batchSize, totalIPs));\n        const batchNodeIds = nodeIds.slice(i, Math.min(i + batchSize, totalIPs));\n\n        const promises = batchIPs.map((ip, index) => {\n            return throttledEnrichInternetDB(ip, batchNodeIds[index], true) // isBulk = true\n                .then(() => successfulEnrichments++)\n                .catch(error => {\n                    console.error(`Failed to enrich IP ${ip}: ${error.message}`);\n                    showToast(`Failed to enrich IP ${ip}: ${error.message}`, 'error');\n                });\n        });\n\n        await Promise.all(promises);\n\n        const currentTime = Date.now();\n        if (currentTime - lastProgressUpdate >= progressUpdateInterval || i + batchSize >= totalIPs) {\n            const processedIPs = Math.min(i + batchSize, totalIPs);\n            const progress = ((processedIPs / totalIPs) * 100).toFixed(1);\n            const remainingIPs = totalIPs - processedIPs;\n            const remainingTimeMs = Math.max(0, remainingIPs * assumedRequestTimeMs);\n            const remainingSeconds = Math.ceil(remainingTimeMs / 1000);\n            const remainingMinutes = Math.floor(remainingSeconds / 60);\n            const remainingSecondsPart = remainingSeconds % 60;\n            const remainingTimeStr = remainingMinutes > 0 \n                ? `${remainingMinutes}m ${remainingSecondsPart}s` \n                : `${remainingSeconds}s`;\n\n            document.getElementById('progress-bar').textContent = \n                `InternetDB Enrichment: ${successfulEnrichments}/${totalIPs} IPs (${progress}%) - Est. ${remainingTimeStr} remaining`;\n            lastProgressUpdate = currentTime;\n            updateSelectOptions();\n        }\n\n        await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    //ensureInteractionSettings();\n\n    if (!(activeTaskController && activeTaskController.signal.aborted)) {\n        completeProgressBar();\n        showToast(`InternetDB enrichment completed: ${successfulEnrichments}/${totalIPs} IPs enriched`, 'success');\n    } else {\n        showToast(`InternetDB enrichment stopped: ${successfulEnrichments}/${totalIPs} IPs processed`, 'info');\n    }\n\n    if (window.innerWidth <= 768) {\n        const controls = document.getElementById('controls');\n        controls.classList.add('collapsed');\n        document.getElementById('myNetwork').style.display = 'block';\n        network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });\n    }\n}, RATE_LIMIT_MS); // Use default rate limit of 500ms\n\n// Add these functions within your <script> tag, ideally near other layout-related functions like setOrganicLayout()\n\n    function setNodeSizeLayout(mode) {\n    // Disable physics to prevent interference during resizing\n    network.setOptions({ physics: { enabled: false } });\n    \n    // Get all nodes\n    const allNodes = nodes.get();\n    \n    // Calculate sizes based on mode\n    allNodes.forEach(node => {\n        let connectionCount = 0;\n        \n        switch (mode) {\n            case 'incoming':\n                // Count edges where this node is the target (to)\n                connectionCount = edges.get({\n                    filter: edge => edge.to === node.id\n                }).length;\n                break;\n                \n            case 'outgoing':\n                // Count edges where this node is the source (from)\n                connectionCount = edges.get({\n                    filter: edge => edge.from === node.id\n                }).length;\n                break;\n                \n            case 'both':\n                // Count all edges connected to this node\n                connectionCount = edges.get({\n                    filter: edge => edge.from === node.id || edge.to === node.id\n                }).length;\n                break;\n                \n            default:\n                console.warn('Invalid mode for setNodeSizeLayout:', mode);\n                return;\n        }\n        \n        // Calculate new size: minimum 10, increases by 10 per connection, max 120\n        const newSize = Math.min(10 + connectionCount * 10, 120);\n        \n        // Update node with new size\n        nodes.update({\n            id: node.id,\n            size: newSize,\n            widthConstraint: false,  // Remove any width constraints\n            heightConstraint: false  // Remove any height constraints\n        });\n    });\n    \n    // Stabilize and fit the network after resizing\n    stabilizeNetwork().then(() => {\n        network.fit({\n            animation: {\n                duration: 300,\n                easingFunction: 'easeInOutQuad'\n            }\n        });\n        \n        // Save the updated state\n        saveStateAfterOperation();\n        \n        // Show confirmation\n        const modeText = {\n            'incoming': 'Incoming Links',\n            'outgoing': 'Outgoing Links',\n            'both': 'All Links'\n        }[mode];\n        showToast(`Node sizes updated based on ${modeText}`, 'success');\n    });\n}\n\nconst throttledSendHttpsRequestMultiple = throttleRequest(async function sendHttpsRequestMultiple(targets, type, protocol) {\n    if (!Array.isArray(targets)) {\n        showToast('Invalid input for multiple HTTPS requests', 'error');\n        return;\n    }\n\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const totalTargets = targets.length;\n    let successfulRequests = 0;\n\n    showToast(`Starting ${protocol.toUpperCase()} requests for ${totalTargets} ${type}s`, 'info');\n    document.getElementById('progress-bar').textContent = `${protocol.toUpperCase()} Requests: 0/${totalTargets} ${type}s (0%)`;\n\n    for (let i = 0; i < totalTargets; i++) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast(`${protocol.toUpperCase()} requests stopped`, 'info');\n            document.getElementById('progress-bar').textContent = `${protocol.toUpperCase()} Requests: Stopped at ${successfulRequests}/${totalTargets} ${type}s`;\n            break;\n        }\n\n        const target = targets[i];\n        const url = constructUrl(`${protocol}://${target}`);\n        \n        try {\n            const response = await fetch(url, {\n                method: 'GET',\n                headers: {\n                    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',\n                    'Origin': window.location.origin\n                },\n                mode: 'cors',\n                credentials: 'omit',\n                signal: activeTaskController?.signal\n            });\n\n            const status = response.status;\n            const statusText = response.statusText;\n            successfulRequests++;\n\n            const progress = ((successfulRequests / totalTargets) * 100).toFixed(1);\n            document.getElementById('progress-bar').textContent = \n                `${protocol.toUpperCase()} Requests: ${successfulRequests}/${totalTargets} ${type}s (${progress}%)`;\n\n            showToast(`${protocol.toUpperCase()} request to ${target}: ${status} ${statusText}`, 'success');\n        } catch (error) {\n            if (error.name === 'AbortError') {\n                break;\n            }\n            showToast(`${protocol.toUpperCase()} request to ${target} failed: ${error.message}`, 'error');\n        }\n\n        // Small delay between requests to avoid overwhelming the proxy/server\n        await new Promise(resolve => setTimeout(resolve, 200));\n    }\n\n    await stabilizeNetwork();\n    //ensureInteractionSettings();\n\n    if (!(activeTaskController && activeTaskController.signal.aborted)) {\n        completeProgressBar();\n        showToast(`${protocol.toUpperCase()} requests completed: ${successfulRequests}/${totalTargets} successful`, 'success');\n    }\n\n    if (window.innerWidth <= 768) {\n        const controls = document.getElementById('controls');\n        controls.classList.add('collapsed');\n        document.getElementById('myNetwork').style.display = 'block';\n        network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });\n    }\n}, RATE_LIMIT_MS);\n\n\nfunction stopActiveTask() {\n    if (activeTaskController) {\n        activeTaskController.abort();\n        showToast('Active task stopped', 'info');\n        document.getElementById('stop-task').disabled = true;\n        activeTaskController = null;\n        completeProgressBar();\n    } else {\n        showToast('No active task to stop', 'warning');\n    }\n}\nfunction startLinkCreation(nodeId) {\n    linkFromNode = nodeId;\n    nodes.update({ id: nodeId, color: { border: '#ff0000' } }); // Highlight source node\n    showToast('Right-click another node to create a link, or click anywhere to cancel', 'info');\n    network.once('oncontext', handleLinkDestination);\n    network.once('click', cancelLinkCreation);\n}\n\nfunction handleLinkDestination(params) {\n    const toNodeId = network.getNodeAt(params.pointer.DOM);\n    if (toNodeId && toNodeId !== linkFromNode) {\n        const label = prompt('Enter link label (optional):', '');\n        edges.add({\n            id: `edge_${linkFromNode}_${toNodeId}_${Date.now()}`,\n            from: linkFromNode,\n            to: toNodeId,\n            label: label || ''\n        });\n        updateNodeSizes();\n        stabilizeNetwork();\n        showToast('Link created', 'success');\n    }\n    cancelLinkCreation();\n}\n\nconst throttledEnrichHudsonRock = throttleRequest(async function enrichHudsonRock(email, emailNodeId, isBulk = false, signal) {\n    network.setOptions({ physics: { enabled: false } });\n    try {\n        const url = constructUrl(`https://cavalier.hudsonrock.com/api/json/v2/osint-tools/search-by-email?email=${encodeURIComponent(email)}`);\n        const response = await fetch(url, { signal });\n        if (!response.ok) throw new Error(`Failed to fetch Hudson Rock data: ${response.statusText}`);\n        const data = await response.json();\n\n        // Deduplication maps\n        const existingComputers = new Map(nodes.get({ filter: n => n.type === 'device' }).map(n => [n.deviceName, n.id]));\n        const existingIPs = new Map(nodes.get({ filter: n => n.type === 'ip' }).map(n => [n.ip, n.id]));\n        const existingMalwares = new Map(nodes.get({ filter: n => n.type === 'malware' }).map(n => [n.malwareName, n.id]));\n        const existingOS = new Map(nodes.get({ filter: n => n.type === 'os' }).map(n => [n.os, n.id]));\n        const existingAVs = new Map(nodes.get({ filter: n => n.type === 'technology' }).map(n => [n.techName, n.id]));\n\n        const newNodes = [];\n        const newEdges = [];\n\n        // Process each stealer entry\n        if (data.stealers && Array.isArray(data.stealers)) {\n            data.stealers.forEach(stealer => {\n                // Computer (Device)\n                const computerName = stealer.computer_name || 'Unknown Computer';\n                let computerId = existingComputers.get(computerName);\n                if (!computerId) {\n                    computerId = nextId++;\n                    newNodes.push({\n                        id: computerId,\n                        type: 'device',\n                        label: `Device: ${computerName}`,\n                        title: `Device\\nName: ${computerName}\\nCategory: Infected Device`,\n                        color: { background: '#14b8a6' },\n                        deviceCategory: 'Infected Device',\n                        deviceName: computerName\n                    });\n                    existingComputers.set(computerName, computerId);\n                }\n                const emailToComputerEdge = `${emailNodeId}-${computerId}-CompromisedOn`;\n                if (!edges.get(emailToComputerEdge) && !newEdges.some(e => e.id === emailToComputerEdge)) {\n                    newEdges.push({ id: emailToComputerEdge, from: emailNodeId, to: computerId, label: 'Compromised on' });\n                }\n\n                // IP Address\n                const ip = stealer.ip || 'Unknown IP';\n                if (ipRegex.ipv4.test(ip) || ipRegex.ipv6.test(ip)) {\n                    let ipId = existingIPs.get(ip);\n                    if (!ipId) {\n                        ipId = nextId++;\n                        newNodes.push({\n                            id: ipId,\n                            type: 'ip',\n                            label: `IP: ${ip}`,\n                            title: `IP Address: ${ip}`,\n                            color: { background: '#f87171' },\n                            ip: ip\n                        });\n                        existingIPs.set(ip, ipId);\n                    }\n                    const computerToIpEdge = `${computerId}-${ipId}-AssignedTo`;\n                    if (!edges.get(computerToIpEdge) && !newEdges.some(e => e.id === computerToIpEdge)) {\n                        newEdges.push({ id: computerToIpEdge, from: computerId, to: ipId, label: 'Assigned to' });\n                    }\n                }\n\n                // Malware (assuming \"jsc.exe\" is indicative; we’ll use malware_path as a name)\n                const malwareName = stealer.malware_path ? stealer.malware_path.split('\\\\').pop() : 'Unknown Malware';\n                let malwareId = existingMalwares.get(malwareName);\n                if (!malwareId) {\n                    malwareId = nextId++;\n                    newNodes.push({\n                        id: malwareId,\n                        type: 'malware',\n                        label: `Malware: ${malwareName}`,\n                        title: `Malware\\nName: ${malwareName}\\nType: Info-Stealer\\nDate: ${stealer.date_compromised || 'N/A'}`,\n                        color: { background: '#ef4444' },\n                        malwareName: malwareName,\n                        malwareType: 'Info-Stealer'\n                    });\n                    existingMalwares.set(malwareName, malwareId);\n                }\n                const computerToMalwareEdge = `${computerId}-${malwareId}-InfectedBy`;\n                if (!edges.get(computerToMalwareEdge) && !newEdges.some(e => e.id === computerToMalwareEdge)) {\n                    newEdges.push({ id: computerToMalwareEdge, from: computerId, to: malwareId, label: 'Infected by' });\n                }\n\n                // Operating System\n                const os = stealer.operating_system || 'Unknown OS';\n                let osId = existingOS.get(os);\n                if (!osId) {\n                    osId = nextId++;\n                    newNodes.push({\n                        id: osId,\n                        type: 'os',\n                        label: `OS: ${os}`,\n                        title: `Operating System: ${os}`,\n                        color: { background: '#10b981' },\n                        os: os\n                    });\n                    existingOS.set(os, osId);\n                }\n                const computerToOsEdge = `${computerId}-${osId}-Runs`;\n                if (!edges.get(computerToOsEdge) && !newEdges.some(e => e.id === computerToOsEdge)) {\n                    newEdges.push({ id: computerToOsEdge, from: computerId, to: osId, label: 'Runs' });\n                }\n\n                // Antiviruses\n                if (stealer.antiviruses && Array.isArray(stealer.antiviruses)) {\n                    stealer.antiviruses.forEach(av => {\n                        let avId = existingAVs.get(av);\n                        if (!avId) {\n                            avId = nextId++;\n                            newNodes.push({\n                                id: avId,\n                                type: 'technology',\n                                label: `AV: ${av}`,\n                                title: `Technology\\nName: ${av}\\nVersion: N/A`,\n                                color: { background: '#ec4899' },\n                                techName: av,\n                                techVersion: 'N/A'\n                            });\n                            existingAVs.set(av, avId);\n                        }\n                        const computerToAvEdge = `${computerId}-${avId}-ProtectedBy`;\n                        if (!edges.get(computerToAvEdge) && !newEdges.some(e => e.id === computerToAvEdge)) {\n                            newEdges.push({ id: computerToAvEdge, from: computerId, to: avId, label: 'Protected by' });\n                        }\n                    });\n                }\n            });\n        }\n\n        // Batch update\n        if (newNodes.length > 0) nodes.add(newNodes);\n        if (newEdges.length > 0) edges.add(newEdges);\n\n        updateNodeSizes();\n        updateSelectOptions();\n        await stabilizeNetwork();\n        if (!isBulk) showToast(`Email ${email} enrichment completed using Hudson Rock`, 'success');\n    } catch (error) {\n        if (error.name === 'AbortError') {\n            showToast(`Enrichment of email ${email} was cancelled`, 'info');\n            return;\n        }\n        console.error(`Error enriching email ${email} with Hudson Rock: ${error.message}`);\n        showToast(`Error enriching email ${email}: ${error.message}`, 'error');\n        await stabilizeNetwork();\n    }\n}, RATE_LIMIT_MS); // Using the default 500ms rate limit\n\n\nconst throttledEnrichHudsonRockMultiple = throttleRequest(async function enrichHudsonRockMultiple(emails, nodeIds) {\n    if (!Array.isArray(emails) || !Array.isArray(nodeIds) || emails.length !== nodeIds.length) {\n        showToast('Invalid input for multiple Hudson Rock enrichment', 'error');\n        return;\n    }\n\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const totalEmails = emails.length;\n    let successfulEnrichments = 0;\n\n    showToast(`Starting Hudson Rock enrichment for ${totalEmails} emails`, 'info');\n    document.getElementById('progress-bar').textContent = `Hudson Rock Enrichment: 0/${totalEmails} Emails (0%)`;\n\n    for (let i = 0; i < totalEmails; i++) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('Hudson Rock enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = `Hudson Rock Enrichment: Stopped at ${successfulEnrichments}/${totalEmails} Emails`;\n            break;\n        }\n\n        await throttledEnrichHudsonRock(emails[i], nodeIds[i], true); // isBulk = true\n        successfulEnrichments++;\n\n        const progress = ((successfulEnrichments / totalEmails) * 100).toFixed(1);\n        document.getElementById('progress-bar').textContent = \n            `Hudson Rock Enrichment: ${successfulEnrichments}/${totalEmails} Emails (${progress}%)`;\n        \n        // Small delay to respect API limits\n        await new Promise(resolve => setTimeout(resolve, 200));\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    //ensureInteractionSettings();\n\n    if (!(activeTaskController && activeTaskController.signal.aborted)) {\n        completeProgressBar();\n        showToast(`Hudson Rock enrichment completed: ${successfulEnrichments}/${totalEmails} emails enriched`, 'success');\n    }\n}, RATE_LIMIT_MS);\n\n\nconst throttledEnrichHudsonRockDomain = throttleRequest(async function enrichHudsonRockDomain(domain, domainNodeId, isBulk = false, signal) {\n    network.setOptions({ physics: { enabled: false } });\n    try {\n        const url = constructUrl(`https://cavalier.hudsonrock.com/api/json/v2/osint-tools/search-by-domain?domain=${encodeURIComponent(domain)}`);\n        const response = await fetch(url, { signal });\n        if (!response.ok) throw new Error(`Failed to fetch Hudson Rock domain data: ${response.statusText}`);\n        const data = await response.json();\n\n        // Deduplication maps\n        const existingOrganizations = new Map(nodes.get({ filter: n => n.type === 'organization' }).map(n => [n.organization, n.id]));\n        const existingDomains = new Map(nodes.get({ filter: n => n.type === 'domain' }).map(n => [n.domain, n.id]));\n        const existingMalwares = new Map(nodes.get({ filter: n => n.type === 'malware' }).map(n => [n.malwareName, n.id]));\n        const existingTechnologies = new Map(nodes.get({ filter: n => n.type === 'technology' }).map(n => [n.techName, n.id]));\n\n        const newNodes = [];\n        const newEdges = [];\n\n        // Process organization (company name)\n        if (data.data && data.data.company_name) {\n            const companyName = data.data.company_name;\n            let orgId = existingOrganizations.get(companyName);\n            if (!orgId) {\n                orgId = nextId++;\n                newNodes.push({\n                    id: orgId,\n                    type: 'organization',\n                    label: `Organization: ${companyName}`,\n                    title: `Organization: ${companyName}`,\n                    color: { background: '#facc15' },\n                    organization: companyName\n                });\n                existingOrganizations.set(companyName, orgId);\n            }\n            const orgEdgeId = `${domainNodeId}-${orgId}-BelongsTo`;\n            if (!edges.get(orgEdgeId) && !newEdges.some(e => e.id === orgEdgeId)) {\n                newEdges.push({ id: orgEdgeId, from: domainNodeId, to: orgId, label: 'Belongs to' });\n            }\n        }\n\n        // Process third-party domains\n        if (data.thirdPartyDomains && Array.isArray(data.thirdPartyDomains)) {\n            data.thirdPartyDomains.forEach(thirdParty => {\n                if (thirdParty.domain && thirdParty.domain !== null) {\n                    let thirdPartyId = existingDomains.get(thirdParty.domain);\n                    if (!thirdPartyId) {\n                        thirdPartyId = nextId++;\n                        newNodes.push({\n                            id: thirdPartyId,\n                            type: 'domain',\n                            label: `Domain: ${thirdParty.domain}`,\n                            title: `Third-Party Domain: ${thirdParty.domain}\\nOccurrences: ${thirdParty.occurrence}`,\n                            color: { background: '#60a5fa' },\n                            domain: thirdParty.domain\n                        });\n                        existingDomains.set(thirdParty.domain, thirdPartyId);\n                    }\n                    const thirdPartyEdgeId = `${domainNodeId}-${thirdPartyId}-AssociatedWith`;\n                    if (!edges.get(thirdPartyEdgeId) && !newEdges.some(e => e.id === thirdPartyEdgeId)) {\n                        newEdges.push({\n                            id: thirdPartyEdgeId,\n                            from: domainNodeId,\n                            to: thirdPartyId,\n                            label: `Associated with (${thirdParty.occurrence})`\n                        });\n                    }\n                }\n            });\n        }\n\n        // Process stealer families as malware\n        if (data.stealerFamilies && typeof data.stealerFamilies === 'object') {\n            for (const [malwareName, count] of Object.entries(data.stealerFamilies)) {\n                if (malwareName !== 'total' && count > 0) {\n                    let malwareId = existingMalwares.get(malwareName);\n                    if (!malwareId) {\n                        malwareId = nextId++;\n                        newNodes.push({\n                            id: malwareId,\n                            type: 'malware',\n                            label: `Malware: ${malwareName}`,\n                            title: `Malware\\nName: ${malwareName}\\nType: Info-Stealer\\nOccurrences: ${count}`,\n                            color: { background: '#ef4444' },\n                            malwareName: malwareName,\n                            malwareType: 'Info-Stealer'\n                        });\n                        existingMalwares.set(malwareName, malwareId);\n                    }\n                    const malwareEdgeId = `${domainNodeId}-${malwareId}-InfectedBy`;\n                    if (!edges.get(malwareEdgeId) && !newEdges.some(e => e.id === malwareEdgeId)) {\n                        newEdges.push({\n                            id: malwareEdgeId,\n                            from: domainNodeId,\n                            to: malwareId,\n                            label: `Infected by (${count})`\n                        });\n                    }\n                }\n            }\n        }\n\n        // Process antiviruses as technologies\n        if (data.antiviruses && data.antiviruses.list && Array.isArray(data.antiviruses.list)) {\n            data.antiviruses.list.forEach(av => {\n                if (av.name && av.count > 0) {\n                    let avId = existingTechnologies.get(av.name);\n                    if (!avId) {\n                        avId = nextId++;\n                        newNodes.push({\n                            id: avId,\n                            type: 'technology',\n                            label: `AV: ${av.name}`,\n                            title: `Technology\\nName: ${av.name}\\nVersion: N/A\\nOccurrences: ${av.count}`,\n                            color: { background: '#ec4899' },\n                            techName: av.name,\n                            techVersion: 'N/A'\n                        });\n                        existingTechnologies.set(av.name, avId);\n                    }\n                    const avEdgeId = `${domainNodeId}-${avId}-ProtectedBy`;\n                    if (!edges.get(avEdgeId) && !newEdges.some(e => e.id === avEdgeId)) {\n                        newEdges.push({\n                            id: avEdgeId,\n                            from: domainNodeId,\n                            to: avId,\n                            label: `Protected by (${av.count})`\n                        });\n                    }\n                }\n            });\n        }\n\n        // Batch update\n        if (newNodes.length > 0) nodes.add(newNodes);\n        if (newEdges.length > 0) edges.add(newEdges);\n\n        updateNodeSizes();\n        updateSelectOptions();\n        await stabilizeNetwork();\n        if (!isBulk) showToast(`Domain ${domain} enrichment completed using Hudson Rock`, 'success');\n    } catch (error) {\n        if (error.name === 'AbortError') {\n            showToast(`Enrichment of domain ${domain} was cancelled`, 'info');\n            return;\n        }\n        console.error(`Error enriching domain ${domain} with Hudson Rock: ${error.message}`);\n        showToast(`Error enriching domain ${domain}: ${error.message}`, 'error');\n        await stabilizeNetwork();\n    }\n}, RATE_LIMIT_MS);\n\nconst throttledEnrichHudsonRockDomainMultiple = throttleRequest(async function enrichHudsonRockDomainMultiple(domains, nodeIds) {\n    if (!Array.isArray(domains) || !Array.isArray(nodeIds) || domains.length !== nodeIds.length) {\n        showToast('Invalid input for multiple Hudson Rock domain enrichment', 'error');\n        return;\n    }\n\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const totalDomains = domains.length;\n    let successfulEnrichments = 0;\n\n    showToast(`Starting Hudson Rock enrichment for ${totalDomains} domains`, 'info');\n    document.getElementById('progress-bar').textContent = `Hudson Rock Domain Enrichment: 0/${totalDomains} Domains (0%)`;\n\n    for (let i = 0; i < totalDomains; i++) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('Hudson Rock domain enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = `Hudson Rock Domain Enrichment: Stopped at ${successfulEnrichments}/${totalDomains} Domains`;\n            break;\n        }\n\n        await throttledEnrichHudsonRockDomain(domains[i], nodeIds[i], true); // isBulk = true\n        successfulEnrichments++;\n\n        const progress = ((successfulEnrichments / totalDomains) * 100).toFixed(1);\n        document.getElementById('progress-bar').textContent = \n            `Hudson Rock Domain Enrichment: ${successfulEnrichments}/${totalDomains} Domains (${progress}%)`;\n        \n        // Small delay to respect API limits\n        await new Promise(resolve => setTimeout(resolve, 200));\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();xf\n    await stabilizeNetwork();\n    //ensureInteractionSettings();\n\n    if (!(activeTaskController && activeTaskController.signal.aborted)) {\n        completeProgressBar();\n        showToast(`Hudson Rock domain enrichment completed: ${successfulEnrichments}/${totalDomains} domains enriched`, 'success');\n    }\n}, RATE_LIMIT_MS);\n\n\n\nfunction showPropertiesPanel(nodeId) {\n    const node = nodes.get(nodeId);\n    if (!node) {\n        showToast('Node not found', 'error');\n        return;\n    }\n\n    const tbody = document.querySelector('#properties-table tbody');\n    tbody.innerHTML = '';\n\n    const properties = {};\n    for (let key in node) {\n        if (node.hasOwnProperty(key) && \n            !['id', 'x', 'y', 'fixed', 'physics', 'hidden', 'group', 'options', \n              'scaling', 'shadow', 'shapeProperties', 'chosen', 'mass'].includes(key)) {\n            properties[key] = node[key];\n        }\n    }\n\n    Object.entries(properties).forEach(([key, value]) => {\n        if (typeof value === 'object' && value !== null && key !== 'color') {\n            value = JSON.stringify(value, null, 2);\n        } else if (value === null || value === undefined) {\n            value = 'N/A';\n        }\n        const row = document.createElement('tr');\n        row.innerHTML = `<td>${key}</td><td>${value}</td>`;\n        tbody.appendChild(row);\n    });\n\n    const panel = document.getElementById('properties-panel');\n    panel.style.display = 'block';\n    panel.classList.add('active');\n\n    const controls = document.getElementById('controls');\n    const myNetwork = document.getElementById('myNetwork');\n    myNetwork.style.marginRight = '300px';\n    myNetwork.style.marginLeft = controls.classList.contains('collapsed') ? '50px' : '300px';\n\n    network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });\n}\n\nfunction hidePropertiesPanel() {\n    const panel = document.getElementById('properties-panel');\n    if (!panel) return;\n\n    panel.classList.remove('active');\n\n    // Reset layout\n    const controls = document.getElementById('controls');\n    const myNetwork = document.getElementById('myNetwork');\n    myNetwork.style.marginRight = '0';\n    myNetwork.style.marginLeft = controls.classList.contains('collapsed') ? '50px' : '300px';\n\n    // Optional: Hide panel after transition to avoid flicker\n    panel.addEventListener('transitionend', function handler() {\n        if (!panel.classList.contains('active')) {\n            panel.style.display = 'none';\n        }\n        panel.removeEventListener('transitionend', handler);\n    });\n\n    network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });\n}\n\n// Ensure toggleMenu aligns with right-side panel\nfunction toggleMenu() {\n    const controls = document.getElementById('controls');\n    const menuToggle = document.getElementById('menu-toggle');\n    const propertiesPanel = document.getElementById('properties-panel');\n    const myNetwork = document.getElementById('myNetwork');\n    \n    controls.classList.toggle('collapsed');\n    \n    if (controls.classList.contains('collapsed')) {\n        menuToggle.textContent = '>'; \n        menuToggle.style.transform = 'rotate(0deg)';\n        myNetwork.style.marginLeft = '50px';\n    } else {\n        menuToggle.textContent = '<'; \n        menuToggle.style.transform = 'rotate(0deg)';\n        myNetwork.style.marginLeft = '300px';\n    }\n\n    myNetwork.style.marginRight = propertiesPanel.classList.contains('active') ? '300px' : '0';\n    \n    network.fit({\n        animation: { duration: 300, easingFunction: 'easeInOutQuad' }\n    });\n}\n\n// Update window resize handler\nwindow.addEventListener('resize', () => {\n    const propertiesPanel = document.getElementById('properties-panel');\n    const controls = document.getElementById('controls');\n    const myNetwork = document.getElementById('myNetwork');\n    \n    if (propertiesPanel.classList.contains('active')) {\n        myNetwork.style.marginRight = '300px';\n    } else {\n        myNetwork.style.marginRight = '0';\n    }\n    myNetwork.style.marginLeft = controls.classList.contains('collapsed') ? '50px' : '300px';\n});\n\nasync function enrichAllHudsonRockEmails() {\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const emailNodes = nodes.get({ filter: n => n.type === 'contact' && n.email });\n    const totalEmails = emailNodes.length;\n    let successfulEnrichments = 0;\n\n    if (totalEmails === 0) {\n        showToast('No email nodes found to enrich', 'info');\n        completeProgressBar();\n        return;\n    }\n\n    console.log(`Found ${totalEmails} email nodes to enrich with Hudson Rock`);\n    showToast(`Starting Hudson Rock enrichment for ${totalEmails} emails`, 'info');\n\n    const batchSize = 50; // Consistent with other bulk enrichment functions\n    const delayBetweenBatches = 200; // Consistent with other bulk functions\n    const totalBatches = Math.ceil(totalEmails / batchSize);\n    const assumedRequestTimeMs = 100; // Estimated time per request\n    const timePerBatchMs = assumedRequestTimeMs * batchSize;\n    const totalBatchDelays = (totalBatches - 1) * delayBetweenBatches;\n    const estimatedTimeMs = (timePerBatchMs * totalBatches) + totalBatchDelays + 1000;\n\n    const estimatedSeconds = Math.ceil(estimatedTimeMs / 1000);\n    const estimatedMinutes = Math.floor(estimatedSeconds / 60);\n    const remainingSeconds = estimatedSeconds % 60;\n    const timeEstimateStr = estimatedMinutes > 0 \n        ? `${estimatedMinutes}m ${remainingSeconds}s` \n        : `${estimatedSeconds}s`;\n\n    showToast(`Estimated time for Hudson Rock email enrichment: ~${timeEstimateStr}`, 'info');\n    document.getElementById('progress-bar').textContent = `Hudson Rock Email Enrichment: 0/${totalEmails} Emails (0%) - Est. ${timeEstimateStr}`;\n\n    async function processBatch(batch) {\n        const promises = batch.map(node => {\n            if (activeTaskController && activeTaskController.signal.aborted) {\n                return Promise.resolve(null);\n            }\n            return throttledEnrichHudsonRock(node.email, node.id, true) // isBulk = true\n                .then(() => successfulEnrichments++)\n                .catch(error => {\n                    console.error(`Failed to enrich email ${node.email}: ${error.message}`);\n                    showToast(`Failed to enrich email ${node.email}: ${error.message}`, 'error');\n                    return null;\n                });\n        });\n        await Promise.all(promises);\n    }\n\n    let lastProgressUpdate = 0;\n    const progressUpdateInterval = 1000;\n    const startTime = Date.now();\n\n    for (let i = 0; i < totalEmails; i += batchSize) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('Hudson Rock email enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = `Hudson Rock Email Enrichment: Stopped at ${successfulEnrichments}/${totalEmails} Emails`;\n            break;\n        }\n\n        const batch = emailNodes.slice(i, Math.min(i + batchSize, totalEmails));\n        console.log(`Processing batch ${Math.floor(i / batchSize) + 1} of ${totalBatches}, Emails ${i} to ${Math.min(i + batchSize - 1, totalEmails - 1)}`);\n\n        await processBatch(batch);\n\n        const currentTime = Date.now();\n        if (currentTime - lastProgressUpdate >= progressUpdateInterval || i + batchSize >= totalEmails) {\n            const processedEmails = Math.min(i + batchSize, totalEmails);\n            const progress = ((processedEmails / totalEmails) * 100).toFixed(1);\n            const remainingEmails = totalEmails - processedEmails;\n            const remainingTimeMs = Math.max(0, remainingEmails * assumedRequestTimeMs);\n            const remainingSeconds = Math.ceil(remainingTimeMs / 1000);\n            const remainingMinutes = Math.floor(remainingSeconds / 60);\n            const remainingSecondsPart = remainingSeconds % 60;\n            const remainingTimeStr = remainingMinutes > 0 \n                ? `${remainingMinutes}m ${remainingSecondsPart}s` \n                : `${remainingSeconds}s`;\n\n            document.getElementById('progress-bar').textContent = \n                `Hudson Rock Email Enrichment: ${successfulEnrichments}/${totalEmails} Emails (${progress}%) - Est. ${remainingTimeStr} remaining`;\n            lastProgressUpdate = currentTime;\n            updateSelectOptions();\n        }\n\n        await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    //ensureInteractionSettings();\n\n    if (!(activeTaskController && activeTaskController.signal.aborted)) {\n        completeProgressBar();\n        showToast(`Hudson Rock email enrichment completed: ${successfulEnrichments}/${totalEmails} emails enriched`, 'success');\n    } else {\n        showToast(`Hudson Rock email enrichment stopped: ${successfulEnrichments}/${totalEmails} emails processed`, 'info');\n    }\n\n    if (window.innerWidth <= 768) {\n        const controls = document.getElementById('controls');\n        controls.classList.add('collapsed');\n        document.getElementById('myNetwork').style.display = 'block';\n        network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });\n    }\n}\n\nasync function enrichAllHudsonRockDomains() {\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const domainNodes = nodes.get({ filter: n => n.type === 'domain' && n.domain });\n    const totalDomains = domainNodes.length;\n    let successfulEnrichments = 0;\n\n    if (totalDomains === 0) {\n        showToast('No domain nodes found to enrich', 'info');\n        completeProgressBar();\n        return;\n    }\n\n    console.log(`Found ${totalDomains} domain nodes to enrich with Hudson Rock`);\n    showToast(`Starting Hudson Rock enrichment for ${totalDomains} domains`, 'info');\n\n    const batchSize = 50; // Consistent with other bulk enrichment functions\n    const delayBetweenBatches = 200; // Consistent with other bulk functions\n    const totalBatches = Math.ceil(totalDomains / batchSize);\n    const assumedRequestTimeMs = 100; // Estimated time per request\n    const timePerBatchMs = assumedRequestTimeMs * batchSize;\n    const totalBatchDelays = (totalBatches - 1) * delayBetweenBatches;\n    const estimatedTimeMs = (timePerBatchMs * totalBatches) + totalBatchDelays + 1000;\n\n    const estimatedSeconds = Math.ceil(estimatedTimeMs / 1000);\n    const estimatedMinutes = Math.floor(estimatedSeconds / 60);\n    const remainingSeconds = estimatedSeconds % 60;\n    const timeEstimateStr = estimatedMinutes > 0 \n        ? `${estimatedMinutes}m ${remainingSeconds}s` \n        : `${estimatedSeconds}s`;\n\n    showToast(`Estimated time for Hudson Rock domain enrichment: ~${timeEstimateStr}`, 'info');\n    document.getElementById('progress-bar').textContent = `Hudson Rock Domain Enrichment: 0/${totalDomains} Domains (0%) - Est. ${timeEstimateStr}`;\n\n    async function processBatch(batch) {\n        const promises = batch.map(node => {\n            if (activeTaskController && activeTaskController.signal.aborted) {\n                return Promise.resolve(null);\n            }\n            return throttledEnrichHudsonRockDomain(node.domain, node.id, true) // isBulk = true\n                .then(() => successfulEnrichments++)\n                .catch(error => {\n                    console.error(`Failed to enrich domain ${node.domain}: ${error.message}`);\n                    showToast(`Failed to enrich domain ${node.domain}: ${error.message}`, 'error');\n                    return null;\n                });\n        });\n        await Promise.all(promises);\n    }\n\n    let lastProgressUpdate = 0;\n    const progressUpdateInterval = 1000;\n    const startTime = Date.now();\n\n    for (let i = 0; i < totalDomains; i += batchSize) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('Hudson Rock domain enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = `Hudson Rock Domain Enrichment: Stopped at ${successfulEnrichments}/${totalDomains} Domains`;\n            break;\n        }\n\n        const batch = domainNodes.slice(i, Math.min(i + batchSize, totalDomains));\n        console.log(`Processing batch ${Math.floor(i / batchSize) + 1} of ${totalBatches}, Domains ${i} to ${Math.min(i + batchSize - 1, totalDomains - 1)}`);\n\n        await processBatch(batch);\n\n        const currentTime = Date.now();\n        if (currentTime - lastProgressUpdate >= progressUpdateInterval || i + batchSize >= totalDomains) {\n            const processedDomains = Math.min(i + batchSize, totalDomains);\n            const progress = ((processedDomains / totalDomains) * 100).toFixed(1);\n            const remainingDomains = totalDomains - processedDomains;\n            const remainingTimeMs = Math.max(0, remainingDomains * assumedRequestTimeMs);\n            const remainingSeconds = Math.ceil(remainingTimeMs / 1000);\n            const remainingMinutes = Math.floor(remainingSeconds / 60);\n            const remainingSecondsPart = remainingSeconds % 60;\n            const remainingTimeStr = remainingMinutes > 0 \n                ? `${remainingMinutes}m ${remainingSecondsPart}s` \n                : `${remainingSeconds}s`;\n\n            document.getElementById('progress-bar').textContent = \n                `Hudson Rock Domain Enrichment: ${successfulEnrichments}/${totalDomains} Domains (${progress}%) - Est. ${remainingTimeStr} remaining`;\n            lastProgressUpdate = currentTime;\n            updateSelectOptions();\n        }\n\n        await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    //ensureInteractionSettings();\n\n    if (!(activeTaskController && activeTaskController.signal.aborted)) {\n        completeProgressBar();\n        showToast(`Hudson Rock domain enrichment completed: ${successfulEnrichments}/${totalDomains} domains enriched`, 'success');\n    } else {\n        showToast(`Hudson Rock domain enrichment stopped: ${successfulEnrichments}/${totalDomains} domains processed`, 'info');\n    }\n\n    if (window.innerWidth <= 768) {\n        const controls = document.getElementById('controls');\n        controls.classList.add('collapsed');\n        document.getElementById('myNetwork').style.display = 'block';\n        network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });\n    }\n}\n\nfunction initializeApiKeys() {\n    ipinfoApiKey = localStorage.getItem('ipinfoApiKey') || '';\n    shodanApiKey = localStorage.getItem('shodanApiKey') || '';\n    greynoiseApiKey = localStorage.getItem('greynoiseApiKey') || '';\n    urlscanApiKey = localStorage.getItem('urlscanApiKey') || '';\n    securitytrailsApiKey = localStorage.getItem('securitytrailsApiKey') || '';\n    urlhausApiKey = localStorage.getItem('urlhausApiKey') || '';\n    corsProxyUrl = localStorage.getItem('corsProxyUrl') || 'http://localhost:3000/proxy?url=';\n    routeViaProxy = localStorage.getItem('routeViaProxy') === 'true';\n    ignoreApiKeysViaProxy = localStorage.getItem('ignoreApiKeysViaProxy') === 'true';\n\n    document.getElementById('ipinfoApiKey').value = ipinfoApiKey;\n    document.getElementById('shodanApiKey').value = shodanApiKey;\n    document.getElementById('greynoiseApiKey').value = greynoiseApiKey;\n    document.getElementById('urlscanApiKey').value = urlscanApiKey;\n    document.getElementById('securitytrailsApiKey').value = securitytrailsApiKey;\n    document.getElementById('urlhausApiKey').value = urlhausApiKey;\n    document.getElementById('corsProxyUrl').value = corsProxyUrl;\n    document.getElementById('routeViaProxy').checked = routeViaProxy;\n    document.getElementById('ignoreApiKeysViaProxy').checked = ignoreApiKeysViaProxy;\n    document.getElementById('storeIpinfoKey').checked = !!localStorage.getItem('ipinfoApiKey');\n    document.getElementById('storeShodanKey').checked = !!localStorage.getItem('shodanApiKey');\n    document.getElementById('storeGreynoiseKey').checked = !!localStorage.getItem('greynoiseApiKey');\n    document.getElementById('storeUrlscanKey').checked = !!localStorage.getItem('urlscanApiKey');\n    document.getElementById('storeSecuritytrailsKey').checked = !!localStorage.getItem('securitytrailsApiKey');\n    document.getElementById('storeUrlhausKey').checked = !!localStorage.getItem('urlhausApiKey');\n}\n\n\n//save ipinfo key\n\nfunction saveIpinfoApiKey() {\n    ipinfoApiKey = document.getElementById('ipinfoApiKey').value.trim();\n    const storeKey = document.getElementById('storeIpinfoKey').checked;\n    if (ipinfoApiKey) {\n        if (storeKey) {\n            localStorage.setItem('ipinfoApiKey', ipinfoApiKey);\n        } else {\n            localStorage.removeItem('ipinfoApiKey');\n        }\n        alert('IPinfo API key saved successfully!');\n    } else {\n        localStorage.removeItem('ipinfoApiKey');\n        alert('Please enter a valid IPinfo API key.');\n    }\n}\n\n//save shodan key\n\nfunction saveShodanApiKey() {\n    shodanApiKey = document.getElementById('shodanApiKey').value.trim();\n    const storeKey = document.getElementById('storeShodanKey').checked;\n    if (shodanApiKey) {\n        if (storeKey) {\n            localStorage.setItem('shodanApiKey', shodanApiKey);\n        } else {\n            localStorage.removeItem('shodanApiKey');\n        }\n        alert('Shodan API key saved successfully!');\n    } else {\n        localStorage.removeItem('shodanApiKey');\n        alert('Please enter a valid Shodan API key.');\n    }\n}\n\n// function impport graph\n\nfunction importGraph() {\n            const fileInput = document.getElementById('importFile');\n            const file = fileInput.files[0];\n            if (!file) {\n                alert('Please select a JSON file to import');\n                return;\n            }\n\n            const reader = new FileReader();\n            reader.onload = function(event) {\n                try {\n                    const importedData = JSON.parse(event.target.result);\n                    \n                    nodes.clear();\n                    edges.clear();\n\n                    importedData.nodes.forEach(node => {\n                        let nodeData = {\n                            id: node.id,\n                            size: 20,\n                            type: node.type\n                        };\n\n                        if (node.type === 'contact') {\n                            nodeData.label = `${node.name}\\n${node.email}`;\n                            nodeData.title = `Contact\\nName: ${node.name}\\nEmail: ${node.email}`;\n                            nodeData.color = { background: '#4ade80' };\n                            nodeData.name = node.name;\n                            nodeData.email = node.email;\n                        } else if (node.type === 'ip') {\n                            nodeData.label = node.ip;\n                            nodeData.title = `IP Address: ${node.ip}`;\n                            nodeData.color = { background: '#f87171' };\n                            nodeData.ip = node.ip;\n                        } else if (node.type === 'domain') {\n                            nodeData.label = node.domain;\n                            nodeData.title = `Domain: ${node.domain}`;\n                            nodeData.color = { background: '#60a5fa' };\n                            nodeData.domain = node.domain;\n                        } else if (node.type === 'organization') {\n                            nodeData.label = node.organization;\n                            nodeData.title = `Organization: ${node.organization}`;\n                            nodeData.color = { background: '#facc15' };\n                            nodeData.organization = node.organization;\n                        } else if (node.type === 'port') {\n                            nodeData.label = `${node.portType}/${node.portNumber}`;\n                            nodeData.title = `Port\\nType: ${node.portType}\\nNumber: ${node.portNumber}`;\n                            nodeData.color = { background: '#a78bfa' };\n                            nodeData.portType = node.portType;\n                            nodeData.portNumber = node.portNumber;\n                        } else if (node.type === 'wallet') {\n                            nodeData.label = node.address;\n                            nodeData.title = `Wallet\\nAddress: ${node.address}`;\n                            nodeData.color = { background: '#fb923c' };\n                            nodeData.address = node.address;\n                        } else if (node.type === 'bank') {\n                            nodeData.label = `${node.accountNumber}\\n${node.sortCode}`;\n                            nodeData.title = `Bank Account\\nAccount Number: ${node.accountNumber}\\nSort Code: ${node.sortCode}`;\n                            nodeData.color = { background: '#10b981' };\n                            nodeData.accountNumber = node.accountNumber;\n                            nodeData.sortCode = node.sortCode;\n                        } else if (node.type === 'technology') {\n                            nodeData.label = `${node.techName}\\n${node.techVersion}`;\n                            nodeData.title = `Technology\\nName: ${node.techName}\\nVersion: ${node.techVersion}`;\n                            nodeData.color = { background: '#ec4899' };\n                            nodeData.techName = node.techName;\n                            nodeData.techVersion = node.techVersion;\n                        } else if (node.type === 'device') {\n                            nodeData.label = node.deviceCategory;\n                            nodeData.title = `Device\\nCategory: ${node.deviceCategory}`;\n                            nodeData.color = { background: '#14b8a6' };\n                            nodeData.deviceCategory = node.deviceCategory;\n                        }\n\n                        let isDuplicate = false;\n                        if (node.type === 'contact') {\n                            isDuplicate = nodes.get({ filter: n => n.type === 'contact' && n.name === nodeData.name && n.email === nodeData.email }).length > 0;\n                        } else if (node.type === 'ip') {\n                            isDuplicate = nodes.get({ filter: n => n.type === 'ip' && n.ip === nodeData.ip }).length > 0;\n                        } else if (node.type === 'domain') {\n                            isDuplicate = nodes.get({ filter: n => n.type === 'domain' && n.domain === nodeData.domain }).length > 0;\n                        } else if (node.type === 'organization') {\n                            isDuplicate = nodes.get({ filter: n => n.type === 'organization' && n.organization === nodeData.organization }).length > 0;\n                        } else if (node.type === 'port') {\n                            isDuplicate = nodes.get({ filter: n => n.type === 'port' && n.portNumber === nodeData.portNumber && n.portType === nodeData.portType }).length > 0;\n                        } else if (node.type === 'wallet') {\n                            isDuplicate = nodes.get({ filter: n => n.type === 'wallet' && n.address === nodeData.address }).length > 0;\n                        } else if (node.type === 'bank') {\n                            isDuplicate = nodes.get({ filter: n => n.type === 'bank' && n.accountNumber === nodeData.accountNumber && n.sortCode === nodeData.sortCode }).length > 0;\n                        } else if (node.type === 'technology') {\n                            isDuplicate = nodes.get({ filter: n => n.type === 'technology' && n.techName === nodeData.techName && n.techVersion === nodeData.techVersion }).length > 0;\n                        } else if (node.type === 'device') {\n                            isDuplicate = nodes.get({ filter: n => n.type === 'device' && n.deviceCategory === nodeData.deviceCategory }).length > 0;\n                        }\n\n                        if (!isDuplicate) {\n                            nodes.add(nodeData);\n                        }\n                    });\n\n                    importedData.edges.forEach(edge => {\n                        edges.add({ \n                            id: edge.id || `edge_${edge.from}_${edge.to}_${Date.now()}`,\n                            from: edge.from, \n                            to: edge.to,\n                            label: edge.label || undefined\n                        });\n                    });\n\n                    updateNodeSizes();\n                    updateSelectOptions();\n                    updateEdgeSelectOptions();\n                    network.setData({ nodes: nodes, edges: edges });\n                    nextId = Math.max(...importedData.nodes.map(n => n.id)) + 1;\n\n                    fileInput.value = '';\n                } catch (e) {\n                    alert('Error importing JSON: ' + e.message);\n                }\n            };\n            reader.readAsText(file);\n        }\n\n// Save GreyNoise API Key\nfunction saveGreynoiseApiKey() {\n    greynoiseApiKey = document.getElementById('greynoiseApiKey').value.trim();\n    const storeKey = document.getElementById('storeGreynoiseKey').checked;\n    if (greynoiseApiKey) {\n        if (storeKey) {\n            localStorage.setItem('greynoiseApiKey', greynoiseApiKey);\n            showToast('GreyNoise API key saved successfully!', 'success');\n        } else {\n            localStorage.removeItem('greynoiseApiKey');\n            showToast('GreyNoise API key set for this session only', 'success');\n        }\n    } else {\n        localStorage.removeItem('greynoiseApiKey');\n        showToast('Please enter a valid GreyNoise API key.', 'error');\n    }\n}\n\n// Greynoise enrichement\n\n\n\nconst throttledEnrichGreyNoise = throttleRequest(async function enrichGreyNoise(ip, ipNodeId, isBulk = false, signal) {\n    if (!greynoiseApiKey && !ignoreApiKeysViaProxy) {\n        showToast('Please set your GreyNoise API key in the \"Config\" tab first.', 'error');\n        return;\n    }\n    \n    if (!isBulk) network.setOptions({ physics: { enabled: false } });\n    \n    try {\n        const baseUrl = `https://api.greynoise.io/v3/community/${ip}`;\n        const url = routeViaProxy ? `${corsProxyUrl}/${baseUrl}` : baseUrl;\n        const response = await fetch(url, {\n            headers: { 'key': greynoiseApiKey },\n            signal\n        });\n        if (!response.ok) throw new Error(`Failed to fetch GreyNoise data: ${response.statusText}`);\n        const data = await response.json();\n\n        // Only process successful responses\n        if (data.message !== 'Success') {\n            throw new Error('GreyNoise API did not return successful response');\n        }\n\n        // Deduplication maps\n        const existingTags = new Map(nodes.get({ filter: n => n.type === 'tag' }).map(n => [n.tag, n.id]));\n        const existingNames = new Map(nodes.get({ filter: n => n.type === 'service' }).map(n => [n.name, n.id]));\n        const existingDates = new Map(nodes.get({ filter: n => n.type === 'timestamp' }).map(n => [n.timestamp, n.id]));\n\n        const newNodes = [];\n        const newEdges = [];\n\n        // Update IP node title with GreyNoise data\n        nodes.update({\n            id: ipNodeId,\n            title: `IP Address: ${ip}\\nNoise: ${data.noise ? 'Yes' : 'No'}\\nRIOT: ${data.riot ? 'Yes' : 'No'}\\nClassification: ${data.classification}\\nName: ${data.name}\\nLast Seen: ${data.last_seen}\\nGreyNoise Link: ${data.link}`\n        });\n\n        // Classification Tag (if not 'unknown')\n        if (data.classification && data.classification !== 'unknown') {\n            const classificationKey = data.classification;\n            let classId = existingTags.get(classificationKey);\n            if (!classId) {\n                classId = nextId++;\n                newNodes.push({\n                    id: classId,\n                    type: 'tag',\n                    label: `Classification: ${classificationKey}`,\n                    title: `GreyNoise Classification: ${classificationKey}`,\n                    color: { background: '#6d28d9' }, // Purple for tags\n                    tag: classificationKey\n                });\n                existingTags.set(classificationKey, classId);\n            }\n            const classEdgeId = `${ipNodeId}-${classId}-ClassifiedAs`;\n            if (!edges.get(classEdgeId) && !newEdges.some(e => e.id === classEdgeId)) {\n                newEdges.push({ id: classEdgeId, from: ipNodeId, to: classId, label: 'Classified as' });\n            }\n        }\n\n        // Service Name (e.g., Google Public DNS)\n        if (data.name && data.name !== 'Unknown') {\n            let serviceId = existingNames.get(data.name);\n            if (!serviceId) {\n                serviceId = nextId++;\n                newNodes.push({\n                    id: serviceId,\n                    type: 'service', // New type for service names\n                    label: `Service: ${data.name}`,\n                    title: `Service Name: ${data.name}`,\n                    color: { background: '#10b981' }, // Green for services\n                    name: data.name\n                });\n                existingNames.set(data.name, serviceId);\n            }\n            const serviceEdgeId = `${ipNodeId}-${serviceId}-OperatesAs`;\n            if (!edges.get(serviceEdgeId) && !newEdges.some(e => e.id === serviceEdgeId)) {\n                newEdges.push({ id: serviceEdgeId, from: ipNodeId, to: serviceId, label: 'Operates as' });\n            }\n        }\n\n        // Last Seen Timestamp\n        if (data.last_seen) {\n            let timestampId = existingDates.get(data.last_seen);\n            if (!timestampId) {\n                timestampId = nextId++;\n                newNodes.push({\n                    id: timestampId,\n                    type: 'timestamp', // New type for timestamps\n                    label: `Last Seen: ${data.last_seen}`,\n                    title: `Last Seen: ${data.last_seen}`,\n                    color: { background: '#f97316' }, // Orange for timestamps\n                    timestamp: data.last_seen\n                });\n                existingDates.set(data.last_seen, timestampId);\n            }\n            const timestampEdgeId = `${ipNodeId}-${timestampId}-LastSeen`;\n            if (!edges.get(timestampEdgeId) && !newEdges.some(e => e.id === timestampEdgeId)) {\n                newEdges.push({ id: timestampEdgeId, from: ipNodeId, to: timestampId, label: 'Last seen' });\n            }\n        }\n\n        // Noise Status (as a boolean-like tag)\n        const noiseKey = `noise-${data.noise}`;\n        let noiseId = existingTags.get(noiseKey);\n        if (!noiseId) {\n            noiseId = nextId++;\n            newNodes.push({\n                id: noiseId,\n                type: 'tag',\n                label: `Noise: ${data.noise ? 'Yes' : 'No'}`,\n                title: `GreyNoise Noise Status: ${data.noise ? 'Yes' : 'No'}`,\n                color: { background: '#6d28d9' },\n                tag: noiseKey\n            });\n            existingTags.set(noiseKey, noiseId);\n        }\n        const noiseEdgeId = `${ipNodeId}-${noiseId}-HasNoise`;\n        if (!edges.get(noiseEdgeId) && !newEdges.some(e => e.id === noiseEdgeId)) {\n            newEdges.push({ id: noiseEdgeId, from: ipNodeId, to: noiseId, label: 'Has noise status' });\n        }\n\n        // RIOT Status (as a boolean-like tag)\n        const riotKey = `riot-${data.riot}`;\n        let riotId = existingTags.get(riotKey);\n        if (!riotId) {\n            riotId = nextId++;\n            newNodes.push({\n                id: riotId,\n                type: 'tag',\n                label: `RIOT: ${data.riot ? 'Yes' : 'No'}`,\n                title: `GreyNoise RIOT Status: ${data.riot ? 'Yes' : 'No'}`,\n                color: { background: '#6d28d9' },\n                tag: riotKey\n            });\n            existingTags.set(riotKey, riotId);\n        }\n        const riotEdgeId = `${ipNodeId}-${riotId}-HasRIOT`;\n        if (!edges.get(riotEdgeId) && !newEdges.some(e => e.id === riotEdgeId)) {\n            newEdges.push({ id: riotEdgeId, from: ipNodeId, to: riotId, label: 'Has RIOT status' });\n        }\n\n        // Batch update\n        if (newNodes.length > 0) nodes.add(newNodes);\n        if (newEdges.length > 0) edges.add(newEdges);\n\n        updateNodeSizes();\n        updateSelectOptions();\n        if (!isBulk) {\n            await stabilizeNetwork();\n            showToast(`IP ${ip} enriched with GreyNoise Community data`, 'success');\n        }\n    } catch (error) {\n        if (error.name === 'AbortError') {\n            showToast(`Enrichment of IP ${ip} cancelled`, 'info');\n            return;\n        }\n        console.error(`Error enriching IP ${ip} with GreyNoise: ${error.message}`);\n        showToast(`Error enriching IP ${ip}: ${error.message}`, 'error');\n        if (!isBulk) await stabilizeNetwork();\n    }\n}, RATE_LIMIT_MS);\n\n// Multiple IP enrichment\nconst throttledEnrichGreyNoiseMultiple = throttleRequest(async function enrichGreyNoiseMultiple(ips, nodeIds) {\n    if (!Array.isArray(ips) || !Array.isArray(nodeIds) || ips.length !== nodeIds.length) {\n        showToast('Invalid input for multiple GreyNoise enrichment', 'error');\n        return;\n    }\n\n    if (!greynoiseApiKey && !ignoreApiKeysViaProxy) {\n        showToast('Please set your GreyNoise API key in the \"Config\" tab first.', 'error');\n        return;\n    }\n\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const totalIPs = ips.length;\n    let successfulEnrichments = 0;\n\n    showToast(`Starting GreyNoise enrichment for ${totalIPs} IPs`, 'info');\n    document.getElementById('progress-bar').textContent = `GreyNoise Enrichment: 0/${totalIPs} IPs (0%)`;\n\n    for (let i = 0; i < totalIPs; i++) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('GreyNoise enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = \n                `GreyNoise Enrichment: Stopped at ${successfulEnrichments}/${totalIPs} IPs`;\n            break;\n        }\n        await throttledEnrichGreyNoise(ips[i], nodeIds[i], true);\n        successfulEnrichments++;\n        const progress = ((successfulEnrichments / totalIPs) * 100).toFixed(1);\n        document.getElementById('progress-bar').textContent = \n            `GreyNoise Enrichment: ${successfulEnrichments}/${totalIPs} IPs (${progress}%)`;\n        // Small delay to respect API rate limits\n        await new Promise(resolve => setTimeout(resolve, 200));\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    //ensureInteractionSettings();\n\n    if (!(activeTaskController && activeTaskController.signal.aborted)) {\n        completeProgressBar();\n        showToast(`GreyNoise enrichment completed: ${successfulEnrichments}/${totalIPs} IPs enriched`, 'success');\n    }\n}, RATE_LIMIT_MS);\n\n\nasync function enrichAllGreyNoise() {\n    if (!greynoiseApiKey && !ignoreApiKeysViaProxy) {\n        showToast('Please set your GreyNoise API key in the \"Config\" tab first.', 'error');\n        return;\n    }\n\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const ipNodes = nodes.get({ filter: n => n.type === 'ip' && n.ip });\n    const totalIPs = ipNodes.length;\n    let successfulEnrichments = 0;\n\n    if (totalIPs === 0) {\n        showToast('No IP nodes found to enrich', 'info');\n        completeProgressBar();\n        return;\n    }\n\n    const batchSize = 50;\n    const delayBetweenBatches = 200;\n    const totalBatches = Math.ceil(totalIPs / batchSize);\n    const estimatedTimeMs = (totalIPs * RATE_LIMIT_MS) + (totalBatches - 1) * delayBetweenBatches;\n    const estimatedSeconds = Math.ceil(estimatedTimeMs / 1000);\n    const timeEstimateStr = estimatedSeconds > 60 \n        ? `${Math.floor(estimatedSeconds / 60)}m ${estimatedSeconds % 60}s` \n        : `${estimatedSeconds}s`;\n\n    document.getElementById('progress-bar').textContent = \n        `GreyNoise Enrichment: 0/${totalIPs} IPs (0%) - Est. ${timeEstimateStr}`;\n\n    for (let i = 0; i < totalIPs; i += batchSize) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('GreyNoise enrichment stopped', 'info');\n            break;\n        }\n\n        const batch = ipNodes.slice(i, Math.min(i + batchSize, totalIPs));\n        const batchPromises = batch.map(node => \n            throttledEnrichGreyNoise(node.ip, node.id, true)\n                .then(() => successfulEnrichments++)\n                .catch(error => console.error(`Failed to enrich ${node.ip}: ${error.message}`))\n        );\n\n        await Promise.all(batchPromises);\n\n        const progress = ((successfulEnrichments / totalIPs) * 100).toFixed(1);\n        document.getElementById('progress-bar').textContent = \n            `GreyNoise Enrichment: ${successfulEnrichments}/${totalIPs} IPs (${progress}%)`;\n        \n        await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    completeProgressBar();\n    showToast(`GreyNoise enrichment completed: ${successfulEnrichments}/${totalIPs} IPs enriched`, 'success');\n}\n\n// New function to save API key\nfunction saveUrlscanApiKey() {\n    urlscanApiKey = document.getElementById('urlscanApiKey').value.trim();\n    const storeKey = document.getElementById('storeUrlscanKey').checked;\n    if (urlscanApiKey) {\n        if (storeKey) {\n            localStorage.setItem('urlscanApiKey', urlscanApiKey);\n            showToast('URLscan.io API key saved successfully!', 'success');\n        } else {\n            localStorage.removeItem('urlscanApiKey');\n            showToast('URLscan.io API key set for this session only', 'success');\n        }\n    } else {\n        localStorage.removeItem('urlscanApiKey');\n        showToast('Please enter a valid URLscan.io API key.', 'error');\n    }\n}\n\n// Single URL enrichment\nconst throttledEnrichURLscan = throttleRequest(async function enrichURLscan(url, nodeId, isBulk = false, signal) {\n    if (!isBulk) network.setOptions({ physics: { enabled: false } });\n    \n    try {\n        const parsedUrl = url.startsWith('http') ? new URL(url) : { hostname: url };\n        const query = parsedUrl.hostname ? `domain:${parsedUrl.hostname}` : `ip:${url}`;\n        const searchUrl = constructUrl(`https://urlscan.io/api/v1/search/?q=${encodeURIComponent(query)}`);\n        \n        const headers = {\n            'Accept': 'application/json'\n        };\n        if (urlscanApiKey && !ignoreApiKeysViaProxy) {\n            headers['API-Key'] = urlscanApiKey;\n        }\n\n        const response = await fetch(searchUrl, {\n            method: 'GET',\n            headers: headers,\n            signal\n        });\n        \n        if (!response.ok) {\n            const errorText = await response.text();\n            throw new Error(`Failed to fetch URLscan.io data: ${response.statusText} - ${errorText}`);\n        }\n        \n        const data = await response.json();\n\n        if (!data.results || data.results.length === 0) {\n            throw new Error(`No results found for ${query}`);\n        }\n\n        // Process the most recent result\n        const latestResult = data.results[0];\n        const newNodes = [];\n        const newEdges = [];\n\n        // 1. Status (page.status)\n        if (latestResult.page?.status) {\n            const statusId = addOrGetNode('tag', `status-${latestResult.page.status}`, newNodes, {\n                label: `Status: ${latestResult.page.status}`,\n                title: `HTTP Status: ${latestResult.page.status}`,\n                color: { background: '#6d28d9' }\n            });\n            newEdges.push({ id: `${nodeId}-${statusId}-HasStatus`, from: nodeId, to: statusId, label: 'Has status' });\n        }\n\n        // 2. URL (page.url) - Updated to show full URL or meaningful truncation\n        if (latestResult.page?.url) {\n        const decodedUrl = decodeURIComponent(latestResult.page.url);\n        const urlLabel = decodedUrl.length > 30 \n            ? `${decodedUrl.substring(0, 27)}...` \n            : decodedUrl;\n        const urlId = addOrGetNode('url', latestResult.page.url, newNodes, {\n            label: `URL: ${urlLabel}`,\n            title: `Full URL: ${decodedUrl}`,\n            url: decodedUrl, // Explicitly store full decoded URL\n            color: { background: '#3b82f6' }\n        });\n        newEdges.push({ id: `${nodeId}-${urlId}-ScannedAs`, from: nodeId, to: urlId, label: 'Scanned as' });\n    }\n\n\n        // 3. Submitted Date (task.time)\n        if (latestResult.task?.time) {\n            const dateId = addOrGetNode('timestamp', latestResult.task.time, newNodes, {\n                label: `Submitted: ${new Date(latestResult.task.time).toLocaleDateString()}`,\n                title: `Submitted Date: ${latestResult.task.time}`,\n                color: { background: '#f97316' }\n            });\n            newEdges.push({ id: `${nodeId}-${dateId}-SubmittedOn`, from: nodeId, to: dateId, label: 'Submitted on' });\n        }\n\n        // 4. Score (_score)\n        if (latestResult._score !== null && latestResult._score !== undefined) {\n            const scoreId = nextId++;\n            newNodes.push({\n                id: scoreId,\n                type: 'tag',\n                label: `Score: ${latestResult._score}`,\n                title: `URLscan Score: ${latestResult._score}`,\n                color: { background: '#10b981' }\n            });\n            newEdges.push({ id: `${nodeId}-${scoreId}-HasScore`, from: nodeId, to: scoreId, label: 'Has score' });\n        }\n\n        // 5. Total (data.total)\n        if (data.total !== undefined) {\n            const totalId = nextId++;\n            newNodes.push({\n                id: totalId,\n                type: 'tag',\n                label: `Total Scans: ${data.total}`,\n                title: `Total URLscan Results: ${data.total}`,\n                color: { background: '#ec4899' }\n            });\n            newEdges.push({ id: `${nodeId}-${totalId}-ScanCount`, from: nodeId, to: totalId, label: 'Scan count' });\n        }\n\n        // 6. Country (page.country)\n        if (latestResult.page?.country) {\n            const countryId = addOrGetNode('country', latestResult.page.country, newNodes, {\n                label: `Country: ${latestResult.page.country}`,\n                title: `Country: ${latestResult.page.country}`,\n                color: { background: '#34d399' }\n            });\n            newEdges.push({ id: `${nodeId}-${countryId}-LocatedIn`, from: nodeId, to: countryId, label: 'Located in' });\n        }\n\n        // 7. ASN (page.asn)\n        if (latestResult.page?.asn) {\n            const asnId = addOrGetNode('asn', latestResult.page.asn, newNodes, {\n                label: `ASN: ${latestResult.page.asn}`,\n                title: `ASN: ${latestResult.page.asn}\\nName: ${latestResult.page.asnname || 'N/A'}`,\n                color: { background: '#a3e635' }\n            });\n            newEdges.push({ id: `${nodeId}-${asnId}-AssignedTo`, from: nodeId, to: asnId, label: 'Assigned to' });\n        }\n\n        // 8. Apex Domain (page.apexDomain)\n        if (latestResult.page?.apexDomain) {\n            const apexId = addOrGetNode('domain', latestResult.page.apexDomain, newNodes, {\n                label: `Apex: ${latestResult.page.apexDomain}`,\n                title: `Apex Domain: ${latestResult.page.apexDomain}`,\n                color: { background: '#60a5fa' }\n            });\n            newEdges.push({ id: `${nodeId}-${apexId}-ApexDomain`, from: nodeId, to: apexId, label: 'Apex domain' });\n        }\n\n        // 9. TLS Age (page.tlsAgeDays)\n        if (latestResult.page?.tlsAgeDays !== undefined) {\n            const tlsAgeId = nextId++;\n            newNodes.push({\n                id: tlsAgeId,\n                type: 'tag',\n                label: `TLS Age: ${latestResult.page.tlsAgeDays} days`,\n                title: `TLS Certificate Age: ${latestResult.page.tlsAgeDays} days`,\n                color: { background: '#8b5cf6' }\n            });\n            newEdges.push({ id: `${nodeId}-${tlsAgeId}-TlsAge`, from: nodeId, to: tlsAgeId, label: 'TLS age' });\n        }\n\n        // 10. IP (page.ip)\n        if (latestResult.page?.ip) {\n            const ipId = addOrGetNode('ip', latestResult.page.ip, newNodes, {\n                label: `IP: ${latestResult.page.ip}`,\n                title: `IP Address: ${latestResult.page.ip}`,\n                color: { background: '#f87171' }\n            });\n            newEdges.push({ id: `${nodeId}-${ipId}-ResolvedTo`, from: nodeId, to: ipId, label: 'Resolved to' });\n        }\n\n        // Update original node with scan metadata\n        nodes.update({\n            id: nodeId,\n            title: `${nodes.get(nodeId).title || url}\\nLast Scanned: ${latestResult.task?.time || 'N/A'}\\nUUID: ${latestResult._id}\\nScreenshot: ${latestResult.screenshot || 'N/A'}`\n        });\n\n        if (newNodes.length) nodes.add(newNodes);\n        if (newEdges.length) edges.add(newEdges);\n\n        updateNodeSizes();\n        updateSelectOptions();\n        if (!isBulk) {\n            await stabilizeNetwork();\n            showToast(`URL ${url} enriched with URLscan.io search results`, 'success');\n        }\n    } catch (error) {\n        if (error.name === 'AbortError') {\n            showToast(`Enrichment of URL ${url} cancelled`, 'info');\n            return;\n        }\n        console.error(`Error enriching URL ${url}: ${error.message}`);\n        showToast(`Error enriching URL ${url}: ${error.message}${!urlscanApiKey ? ' (API key may improve results)' : ''}`, 'error');\n        if (!isBulk) await stabilizeNetwork();\n    }\n}, RATE_LIMIT_MS);\n\n// Bulk enrichment\nasync function enrichAllURLscan() {\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const urlNodes = nodes.get({ filter: n => (n.type === 'domain' || n.type === 'ip') && (n.domain || n.ip) });\n    const totalURLs = urlNodes.length;\n    let successfulEnrichments = 0;\n\n    if (totalURLs === 0) {\n        showToast('No URLs found to enrich', 'info');\n        completeProgressBar();\n        return;\n    }\n\n    const batchSize = urlscanApiKey ? 5 : 2;\n    const delayBetweenBatches = urlscanApiKey ? 1000 : 2000;\n    const estimatedTimeMs = (totalURLs / batchSize) * delayBetweenBatches;\n    const timeEstimateStr = Math.ceil(estimatedTimeMs / 60000) + 'm';\n\n    document.getElementById('progress-bar').textContent = \n        `URLscan.io Enrichment: 0/${totalURLs} URLs (0%) - Est. ${timeEstimateStr}`;\n\n    if (!urlscanApiKey) {\n        showToast('Running URLscan.io enrichment without API key - limited rate applies', 'warning');\n    }\n\n    for (let i = 0; i < totalURLs; i += batchSize) {\n        if (activeTaskController?.signal.aborted) {\n            showToast('URLscan.io enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = \n                `URLscan.io Enrichment: Stopped at ${successfulEnrichments}/${totalURLs} URLs`;\n            break;\n        }\n\n        const batch = urlNodes.slice(i, Math.min(i + batchSize, totalURLs));\n        const promises = batch.map(node => {\n            const url = node.type === 'domain' ? `https://${node.domain}` : node.ip;\n            return throttledEnrichURLscan(url, node.id, true)\n                .then(() => successfulEnrichments++)\n                .catch(error => {\n                    console.error(`Batch error for ${url}: ${error.message}`);\n                });\n        });\n\n        await Promise.all(promises);\n\n        const progress = ((successfulEnrichments / totalURLs) * 100).toFixed(1);\n        document.getElementById('progress-bar').textContent = \n            `URLscan.io Enrichment: ${successfulEnrichments}/${totalURLs} URLs (${progress}%)`;\n        \n        await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    //ensureInteractionSettings();\n    completeProgressBar();\n    showToast(`URLscan.io enrichment completed: ${successfulEnrichments}/${totalURLs} URLs enriched${!urlscanApiKey ? ' (API key may improve results)' : ''}`, 'success');\n}\n\n// Updated helper function to handle new node types\nfunction addOrGetNode(type, value, newNodes, properties) {\n    const key = type === 'url' ? 'url' : type; // Use 'url' as the key for URL nodes\n    const existing = nodes.get({ filter: n => n.type === type && n[key] === value })[0];\n    if (existing) return existing.id;\n    \n    const nodeId = nextId++;\n    newNodes.push({\n        id: nodeId,\n        type: type,\n        [key]: value,\n        ...properties\n    });\n    return nodeId;\n}\n\n//security trails API key save\nfunction saveSecuritytrailsApiKey() {\n    securitytrailsApiKey = document.getElementById('securitytrailsApiKey').value.trim();\n    const storeKey = document.getElementById('storeSecuritytrailsKey').checked;\n    if (securitytrailsApiKey) {\n        if (storeKey) localStorage.setItem('securitytrailsApiKey', securitytrailsApiKey);\n        else localStorage.removeItem('securitytrailsApiKey');\n        showToast('SecurityTrails API key saved successfully!', 'success');\n    } else {\n        localStorage.removeItem('securitytrailsApiKey');\n        showToast('Please enter a valid SecurityTrails API key.', 'error');\n    }\n}\n\n\n\n//Export visable only\n\nfunction exportVisibleGraph() {\n    // Filter for only visible nodes\n    const visibleNodes = nodes.get().filter(node => !node.hidden).map(node => ({ \n        id: node.id,\n        type: node.type,\n        name: node.name,\n        email: node.email,\n        ip: node.ip,\n        domain: node.domain,\n        organization: node.organization,\n        portType: node.portType,\n        portNumber: node.portNumber,\n        address: node.address,\n        accountNumber: node.accountNumber,\n        sortCode: node.sortCode,\n        techName: node.techName,\n        techVersion: node.techVersion,\n        deviceCategory: node.deviceCategory,\n        deviceName: node.deviceName,\n        malwareName: node.malwareName,\n        malwareType: node.malwareType,\n        country: node.country,\n        asn: node.asn,\n        city: node.city,\n        value: node.value,\n        vulnName: node.vulnName,\n        cve: node.cve,\n        url: node.url,\n        x: node.x,\n        y: node.y,\n        label: node.label,\n        title: node.title,\n        color: node.color,\n        size: node.size\n    }));\n\n    // Get all edges and filter for those connecting visible nodes\n    const visibleNodeIds = new Set(visibleNodes.map(node => node.id));\n    const visibleEdges = edges.get().filter(edge => \n        !edge.hidden && \n        visibleNodeIds.has(edge.from) && \n        visibleNodeIds.has(edge.to)\n    ).map(edge => ({\n        id: edge.id,\n        from: edge.from,\n        to: edge.to,\n        label: edge.label\n    }));\n\n    // Create export data object\n    const exportData = {\n        nodes: visibleNodes,\n        edges: visibleEdges\n    };\n\n    // Convert to JSON and trigger download\n    const json = JSON.stringify(exportData, null, 2);\n    const blob = new Blob([json], { type: 'application/json' });\n    const url = URL.createObjectURL(blob);\n    const a = document.createElement('a');\n    a.href = url;\n    a.download = `visible_network_graph_${new Date().toISOString().replace(/[:.]/g, '-')}.json`;\n    a.click();\n    URL.revokeObjectURL(url);\n    \n    showToast('Visible graph exported to JSON', 'success');\n}\n\n\n// Single Domain Enrichment with SecurityTrails\nconst throttledEnrichSecurityTrailsDomain = throttleRequest(async function enrichSecurityTrailsDomain(domain, domainNodeId, isBulk = false, signal) {\n    if (!securitytrailsApiKey && !ignoreApiKeysViaProxy) {\n        showToast('Please set your SecurityTrails API key in the \"Config\" tab first.', 'error');\n        return;\n    }\n\n    if (!isBulk) network.setOptions({ physics: { enabled: false } });\n\n    try {\n        const baseUrl = `https://api.securitytrails.com/v1/domain/${domain}/subdomains`;\n        const url = constructUrl(baseUrl);\n        console.log('Requesting SecurityTrails URL:', url);\n\n        const response = await fetch(url, {\n            headers: { \n                'apikey': securitytrailsApiKey, // Updated to 'apikey'\n                'Accept': 'application/json' \n            },\n            signal\n        });\n\n        if (!response.ok) {\n            const errorText = await response.text();\n            if (response.status === 404) {\n                showToast(`No subdomains found for domain ${domain} in SecurityTrails`, 'info');\n                return;\n            }\n            throw new Error(`Failed to fetch SecurityTrails data: ${response.statusText} - ${errorText}`);\n        }\n\n        const data = await response.json();\n        console.log('SecurityTrails Response:', data);\n\n        if (!data.subdomains || data.subdomains.length === 0) {\n            showToast(`No subdomains associated with domain ${domain} in SecurityTrails`, 'info');\n            return;\n        }\n\n        const newNodes = [];\n        const newEdges = [];\n        const existingDomains = new Map(nodes.get({ filter: n => n.type === 'domain' }).map(n => [n.domain, n.id]));\n\n        data.subdomains.forEach(subdomain => {\n            const fullSubdomain = `${subdomain}.${domain}`;\n            let subdomainId = existingDomains.get(fullSubdomain);\n            if (!subdomainId) {\n                subdomainId = nextId++;\n                newNodes.push({\n                    id: subdomainId,\n                    type: 'domain',\n                    label: `Subdomain: ${fullSubdomain}`,\n                    title: `Subdomain: ${fullSubdomain}\\nFrom SecurityTrails`,\n                    color: { background: '#60a5fa' },\n                    domain: fullSubdomain\n                });\n                existingDomains.set(fullSubdomain, subdomainId);\n            }\n            const edgeId = `${domainNodeId}-${subdomainId}-SubdomainOf`;\n            if (!edges.get(edgeId) && !newEdges.some(e => e.id === edgeId)) {\n                newEdges.push({ id: edgeId, from: domainNodeId, to: subdomainId, label: 'Subdomain of' });\n            }\n        });\n\n        if (newNodes.length > 0) nodes.add(newNodes);\n        if (newEdges.length > 0) edges.add(newEdges);\n\n        updateNodeSizes();\n        updateSelectOptions();\n        if (!isBulk) {\n            await stabilizeNetwork();\n            showToast(`Domain ${domain} enriched with ${data.subdomains.length} subdomains from SecurityTrails`, 'success');\n        }\n    } catch (error) {\n        if (error.name === 'AbortError') {\n            showToast(`Enrichment of domain ${domain} cancelled`, 'info');\n            return;\n        }\n        console.error(`Error enriching domain ${domain} with SecurityTrails: ${error.message}`);\n        showToast(`Error enriching domain ${domain}: ${error.message}`, 'error');\n        if (!isBulk) await stabilizeNetwork();\n    }\n}, SECURITYTRAILS_RATE_LIMIT_MS);\n\n\nasync function enrichAllSecurityTrails() {\n    if (!securitytrailsApiKey && !ignoreApiKeysViaProxy) {\n        showToast('Please set your SecurityTrails API key in the \"Config\" tab first.', 'error');\n        return;\n    }\n\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const domainNodes = nodes.get({ filter: n => n.type === 'domain' && n.domain });\n    const totalDomains = domainNodes.length;\n    let successfulEnrichments = 0;\n\n    if (totalDomains === 0) {\n        showToast('No domain nodes found to enrich', 'info');\n        completeProgressBar();\n        return;\n    }\n\n    const delayBetweenRequests = SECURITYTRAILS_RATE_LIMIT_MS; // Use the defined rate limit\n    const estimatedTimeMs = totalDomains * delayBetweenRequests;\n    const estimatedSeconds = Math.ceil(estimatedTimeMs / 1000);\n    const timeEstimateStr = estimatedSeconds > 60 \n        ? `${Math.floor(estimatedSeconds / 60)}m ${estimatedSeconds % 60}s` \n        : `${estimatedSeconds}s`;\n\n    showToast(`Starting SecurityTrails enrichment for ${totalDomains} domains`, 'info');\n    document.getElementById('progress-bar').textContent = `SecurityTrails Enrichment: 0/${totalDomains} Domains (0%) - Est. ${timeEstimateStr}`;\n\n    for (let i = 0; i < totalDomains; i++) {\n        if (activeTaskController?.signal.aborted) {\n            showToast('SecurityTrails enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = `SecurityTrails Enrichment: Stopped at ${successfulEnrichments}/${totalDomains} Domains`;\n            break;\n        }\n\n        const node = domainNodes[i];\n        let attempts = 0;\n        const maxAttempts = 3;\n        let success = false;\n\n        while (attempts < maxAttempts && !success) {\n            try {\n                await throttledEnrichSecurityTrailsDomain(node.domain, node.id, true);\n                successfulEnrichments++;\n                success = true;\n            } catch (error) {\n                if (error.message.includes('429')) {\n                    attempts++;\n                    if (attempts < maxAttempts) {\n                        const backoffTime = delayBetweenRequests * attempts; // Exponential backoff: 2s, 4s, 6s\n                        showToast(`Rate limit hit for ${node.domain}, retrying in ${backoffTime / 1000}s (Attempt ${attempts}/${maxAttempts})`, 'warning');\n                        await new Promise(resolve => setTimeout(resolve, backoffTime));\n                    } else {\n                        showToast(`Failed to enrich ${node.domain} after ${maxAttempts} attempts: ${error.message}`, 'error');\n                    }\n                } else {\n                    showToast(`Error enriching ${node.domain}: ${error.message}`, 'error');\n                    break; // Non-429 errors skip retries\n                }\n            }\n        }\n\n        const progress = ((successfulEnrichments / totalDomains) * 100).toFixed(1);\n        document.getElementById('progress-bar').textContent = `SecurityTrails Enrichment: ${successfulEnrichments}/${totalDomains} Domains (${progress}%)`;\n\n        // Wait between requests, even on success, to respect rate limits\n        if (i < totalDomains - 1) { // No delay after the last domain\n            await new Promise(resolve => setTimeout(resolve, delayBetweenRequests));\n        }\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    //ensureInteractionSettings();\n    completeProgressBar();\n    showToast(`SecurityTrails enrichment completed: ${successfulEnrichments}/${totalDomains} domains enriched`, 'success');\n}\n\n\n\n// Multiple Domain Enrichment with SecurityTrails (Context Menu)\nconst throttledEnrichSecurityTrailsDomainMultiple = throttleRequest(async function enrichSecurityTrailsDomainMultiple(domains, nodeIds) {\n    if (!Array.isArray(domains) || !Array.isArray(nodeIds) || domains.length !== nodeIds.length) {\n        showToast('Invalid input for multiple SecurityTrails enrichment', 'error');\n        return;\n    }\n\n    if (!securitytrailsApiKey && !ignoreApiKeysViaProxy) {\n        showToast('Please set your SecurityTrails API key in the \"Config\" tab first.', 'error');\n        return;\n    }\n\n    showProgressBar();\n    let successfulEnrichments = 0;\n    const totalDomains = domains.length;\n\n    document.getElementById('progress-bar').textContent = `SecurityTrails Enrichment: 0/${totalDomains} Domains (0%)`;\n\n    for (let i = 0; i < domains.length; i++) {\n        if (activeTaskController?.signal.aborted) {\n            showToast('SecurityTrails enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = `SecurityTrails Enrichment: Stopped at ${successfulEnrichments}/${totalDomains} Domains`;\n            break;\n        }\n        await throttledEnrichSecurityTrailsDomain(domains[i], nodeIds[i], true)\n            .then(() => successfulEnrichments++)\n            .catch(error => console.error(`Failed to enrich ${domains[i]}: ${error.message}`));\n        \n        const progress = ((successfulEnrichments / totalDomains) * 100).toFixed(1);\n        document.getElementById('progress-bar').textContent = `SecurityTrails Enrichment: ${successfulEnrichments}/${totalDomains} Domains (${progress}%)`;\n        await new Promise(resolve => setTimeout(resolve, 200)); // Small delay between requests\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    completeProgressBar();\n    showToast(`Enriched ${successfulEnrichments}/${domains.length} domains with SecurityTrails subdomains`, 'success');\n}, SECURITYTRAILS_RATE_LIMIT_MS);\n\n//Refang IOCs\n\nfunction refangText(text) {\n    if (typeof text !== 'string') return text;\n\n    let refanged = text;\n\n    // URL-specific defanging patterns\n    refanged = refanged.replace(/hxxps/gi, 'https');          // hxxps → https\n    refanged = refanged.replace(/hxxp/gi, 'http');           // hxxp → http\n    refanged = refanged.replace(/\\[hxxps\\]/gi, 'https');     // [hxxps] → https\n    refanged = refanged.replace(/\\[hxxp\\]/gi, 'http');       // [hxxp] → http\n    refanged = refanged.replace(/\\[https\\]/gi, 'https');     // [https] → https\n    refanged = refanged.replace(/\\[http\\]/gi, 'http');       // [http] → http\n    refanged = refanged.replace(/\\[colon\\]/gi, ':');         // [colon] → :\n    refanged = refanged.replace(/\\[\\/\\]/g, '/');             // [/] → /\n    refanged = refanged.replace(/\\[ \\/\\]/g, '/');            // [ /] → /\n    refanged = refanged.replace(/\\[slash\\]/gi, '/');         // [slash] → /\n    refanged = refanged.replace(/\\[\\.\\]/g, '.');             // [.] → .\n    refanged = refanged.replace(/\\[dot\\]/gi, '.');           // [dot] or [DOT] → .\n    refanged = refanged.replace(/\\[ \\. \\]/g, '.');           // [ . ] → .\n\n    // Email-specific defanging (unchanged)\n    refanged = refanged.replace(/\\[at\\]/gi, '@');            // [at] or [AT] → @\n    refanged = refanged.replace(/\\[ @ \\]/g, '@');            // [ @ ] → @\n    refanged = refanged.replace(/ at /gi, '@');              // \" at \" → @\n\n    // General cleanup\n    refanged = refanged.replace(/\\[\\s*\\]/g, '');             // Empty brackets [] → ''\n    refanged = refanged.replace(/\\[([^\\]]+)\\]/g, '$1');      // [text] → text (non-special cases)\n    refanged = refanged.replace(/\\s+/g, ' ').trim();         // Normalize spaces\n\n    return refanged;\n}\n\n\n\n//notes \nfunction editNodeNotes(nodeId) {\n    console.log('Attempting to open notes for node:', nodeId);\n    const node = nodes.get(nodeId);\n    if (!node) {\n        showToast('Node not found', 'error');\n        return;\n    }\n\n    const modal = document.getElementById('notes-modal');\n    const notesTextarea = document.getElementById('notes-textarea');\n    if (!modal || !notesTextarea) {\n        console.error('Modal or textarea not found');\n        showToast('Notes interface unavailable', 'error');\n        return;\n    }\n\n    // Get the modal title element\n    const modalTitle = modal.querySelector('h3');\n    if (!modalTitle) {\n        console.error('Modal title element not found');\n        showToast('Modal title unavailable', 'error');\n        return;\n    }\n\n    // Determine the entity name\n    let entityName;\n    switch (node.type) {\n        case 'contact':\n            entityName = node.name || node.email || node.label.split('\\n')[0];\n            break;\n        case 'ip':\n            entityName = node.ip || node.label.split('\\n')[0];\n            break;\n        case 'domain':\n            entityName = node.domain || node.label.split('\\n')[0];\n            break;\n        case 'organization':\n            entityName = node.organization || node.label.split('\\n')[0];\n            break;\n        case 'port':\n            entityName = `${node.portType}/${node.portNumber}` || node.label.split('\\n')[0];\n            break;\n        case 'wallet':\n            entityName = node.address || node.label.split('\\n')[0];\n            break;\n        case 'bank':\n            entityName = node.accountNumber || node.label.split('\\n')[0];\n            break;\n        case 'technology':\n            entityName = node.techName || node.label.split('\\n')[0];\n            break;\n        case 'device':\n            entityName = node.deviceName || node.label.split('\\n')[0];\n            break;\n        case 'malware':\n            entityName = node.malwareName || node.label.split('\\n')[0];\n            break;\n        case 'vulnerability':\n            entityName = node.vulnName || node.cve || node.label.split('\\n')[0];\n            break;\n        default:\n            entityName = node.label.split('\\n')[0]; // Fallback to first line of label\n    }\n\n    // Set the modal title\n    modalTitle.textContent = `Edit Notes for ${entityName}`;\n\n    currentEditingNodeId = nodeId;\n    notesTextarea.value = node.notes || '';\n\n    // Remove existing overlay if any\n    let overlay = document.getElementById('modal-overlay');\n    if (overlay) overlay.remove();\n\n    // Create new overlay\n    overlay = document.createElement('div');\n    overlay.id = 'modal-overlay';\n    overlay.style.cssText = `\n        position: fixed;\n        top: 0;\n        left: 0;\n        width: 100vw;\n        height: 100vh;\n        background: rgba(0, 0, 0, 0.5);\n        z-index: 1999;\n    `;\n    document.body.appendChild(overlay);\n\n    // Reset modal styles\n    modal.style.position = 'fixed';\n    modal.style.top = '50%';\n    modal.style.left = '50%';\n    modal.style.transform = 'translate(-50%, -50%)';\n    modal.style.zIndex = '2000';\n    modal.style.display = 'block';\n\n    // Force reflow\n    void modal.offsetWidth;\n    console.log('Modal display set to:', modal.style.display);\n\n    notesTextarea.focus();\n\n    // Remove any existing click handler to prevent accumulation\n    if (window.notesClickHandler) {\n        document.removeEventListener('click', window.notesClickHandler);\n    }\n\n    // Define and add new click handler\n    const clickHandler = (event) => {\n        if (modal.style.display === 'block' && !modal.contains(event.target)) {\n            hideNotesModal();\n        }\n    };\n    setTimeout(() => {\n        document.addEventListener('click', clickHandler);\n        window.notesClickHandler = clickHandler; // Store globally for cleanup\n    }, 100);\n\n    // Remove any existing resize handler\n    if (modal.dataset.resizeHandler) {\n        window.removeEventListener('resize', modal.dataset.resizeHandler);\n    }\n\n    // Add resize handler\n    const resizeHandler = () => {\n        modal.style.top = '50%';\n        modal.style.left = '50%';\n        modal.style.transform = 'translate(-50%, -50%)';\n    };\n    window.addEventListener('resize', resizeHandler);\n    modal.dataset.resizeHandler = resizeHandler;\n}\n\n\n// save notes\n\nfunction saveNodeNotes() {\n    if (currentEditingNodeId === null) return;\n\n    const node = nodes.get(currentEditingNodeId);\n    if (!node) {\n        showToast('Node not found', 'error');\n        hideNotesModal();\n        return;\n    }\n\n    const notesTextarea = document.getElementById('notes-textarea');\n    const newNotes = notesTextarea.value.trim();\n\n    if (newNotes.length > 1000) {\n        showToast('Notes cannot exceed 1000 characters', 'error');\n        return;\n    }\n\n    // Define configs as in createNodeData (or move this to a global scope if reused elsewhere)\n    const configs = {\n        contact: {\n            title: v => `Contact\\nName: ${v.name}${v.email ? '\\nEmail: ' + v.email : ''}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        ip: {\n            title: v => `IP Address: ${v.ip}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        domain: {\n            title: v => `Domain: ${v.domain}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        organization: {\n            title: v => `Organization: ${v.organization}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        port: {\n            title: v => `Port\\nType: ${v.portType}\\nNumber: ${v.portNumber}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        wallet: {\n            title: v => `Wallet\\nAddress: ${v.address}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        bank: {\n            title: v => `Bank Account\\nAccount Number: ${v.accountNumber}\\nSort Code: ${v.sortCode}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        technology: {\n            title: v => `Technology\\nName: ${v.techName}${v.techVersion ? '\\nVersion: ' + v.techVersion : ''}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        device: {\n            title: v => `Device\\nName: ${v.deviceName}\\nCategory: ${v.deviceCategory}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        malware: {\n            title: v => `Malware\\nName: ${v.malwareName}\\nType: ${v.malwareType}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        vulnerability: {\n            title: v => `Vulnerability\\nName: ${v.vulnName}${v.cve ? '\\nCVE: ' + v.cve : ''}${v.url ? '\\nURL: ' + v.url : ''}${v.notes ? '\\nNotes: ' + v.notes : ''}`\n        },\n        // Add other types as needed\n    };\n\n    const config = configs[node.type];\n    if (!config) {\n        showToast(`Unsupported node type: ${node.type}`, 'error');\n        hideNotesModal();\n        return;\n    }\n\n    const updatedNode = { ...node, notes: newNotes };\n    nodes.update({\n        id: currentEditingNodeId,\n        notes: newNotes,\n        title: config.title(updatedNode) // Call the function directly\n    });\n\n    saveStateAfterOperation();\n    showToast('Notes saved successfully', 'success');\n    hideNotesModal();\n}\n\nfunction hideNotesModal() {\n    const modal = document.getElementById('notes-modal');\n    if (!modal) return;\n\n    modal.style.display = 'none';\n    const overlay = document.getElementById('modal-overlay');\n    if (overlay) overlay.remove();\n\n    // Clean up click handler\n    if (window.notesClickHandler) {\n        document.removeEventListener('click', window.notesClickHandler);\n        delete window.notesClickHandler;\n    }\n\n    // Clean up resize handler\n    const resizeHandler = modal.dataset.resizeHandler;\n    if (resizeHandler) {\n        window.removeEventListener('resize', resizeHandler);\n        delete modal.dataset.resizeHandler;\n    }\n\n    currentEditingNodeId = null;\n}\n\n\n\n\n\n// delete nodes\n\nfunction deleteNodes(nodeIds) {\n    console.log('deleteNodes called with:', nodeIds);\n    \n    if (!nodeIds || !Array.isArray(nodeIds) || nodeIds.length === 0) {\n        showToast('No nodes selected to delete', 'error');\n        return;\n    }\n\n    // Filter out invalid or non-existent node IDs\n    const validNodeIds = nodeIds.filter(id => {\n        const node = nodes.get(id);\n        if (!node) {\n            console.warn(`Node with ID ${id} not found`);\n            return false;\n        }\n        return true;\n    });\n\n    if (validNodeIds.length === 0) {\n        showToast('No valid nodes found to delete', 'error');\n        return;\n    }\n\n    // Remove all edges connected to these nodes\n    edges.forEach(edge => {\n        if (validNodeIds.includes(edge.from) || validNodeIds.includes(edge.to)) {\n            edges.remove({ id: edge.id });\n        }\n    });\n\n    // Remove the nodes\n    nodes.remove(validNodeIds);\n\n    // Update UI and network\n    updateNodeSizes();\n    updateSelectOptions();\n    stabilizeNetwork();\n    saveStateAfterOperation();\n\n    showToast(`${validNodeIds.length} node${validNodeIds.length > 1 ? 's' : ''} deleted`, 'success');\n}\n\n// save URLHAUS keys\n\nfunction saveUrlhausApiKey() {\n    urlhausApiKey = document.getElementById('urlhausApiKey').value.trim();\n    const storeKey = document.getElementById('storeUrlhausKey').checked;\n    if (urlhausApiKey) {\n        if (storeKey) {\n            localStorage.setItem('urlhausApiKey', urlhausApiKey);\n            showToast('URLhaus API key saved successfully!', 'success');\n        } else {\n            localStorage.removeItem('urlhausApiKey');\n            showToast('URLhaus API key set for this session only', 'success');\n        }\n    } else {\n        localStorage.removeItem('urlhausApiKey');\n        showToast('Please enter a valid URLhaus API key.', 'error');\n    }\n}\n\n\n// URL HAUS\n\nconst throttledEnrichURLhaus = throttleRequest(async function enrichURLhaus(url, urlNodeId, isBulk = false, signal) {\n    if (!urlhausApiKey && !ignoreApiKeysViaProxy) {\n        showToast('Please set your URLhaus API key in the \"Config\" tab first.', 'error');\n        return;\n    }\n\n    if (!isBulk) network.setOptions({ physics: { enabled: false } });\n\n    try {\n        const baseUrl = 'https://urlhaus-api.abuse.ch/v1/url/';\n        const urlToQuery = constructUrl(baseUrl);\n        const postData = `url=${encodeURIComponent(url)}`;\n        const headers = {\n            'Content-Type': 'application/x-www-form-urlencoded',\n            'Accept': 'application/json'\n        };\n        if (urlhausApiKey && !ignoreApiKeysViaProxy) {\n            headers['Auth-Key'] = urlhausApiKey;\n        }\n\n        const response = await fetch(urlToQuery, {\n            method: 'POST',\n            headers: headers,\n            body: postData,\n            signal\n        });\n\n        if (!response.ok) {\n            const errorText = await response.text();\n            throw new Error(`Failed to fetch URLhaus data: ${response.statusText} - ${errorText}`);\n        }\n\n        const data = await response.json();\n\n        if (data.query_status !== 'ok') {\n            if (data.query_status === 'no_results') {\n                if (!isBulk) showToast(`No URLhaus data found for ${url}`, 'info');\n                return;\n            }\n            throw new Error(`URLhaus query failed: ${data.query_status}`);\n        }\n\n        const newNodes = [];\n        const newEdges = [];\n        const existingNodes = new Map();\n\n        // Cache existing nodes for deduplication\n        nodes.forEach(node => {\n            if (node.type === 'url' && node.url) existingNodes.set(`url:${node.url}`, node.id);\n            if (node.type === 'tag' && node.tag) existingNodes.set(`tag:${node.tag}`, node.id);\n            if (node.type === 'timestamp' && node.timestamp) existingNodes.set(`timestamp:${node.timestamp}`, node.id);\n            if (node.type === 'threat' && node.threat) existingNodes.set(`threat:${node.threat}`, node.id);\n        });\n\n        // URL Status\n        if (data.url_status) {\n            const statusId = existingNodes.get(`tag:${data.url_status}`) || nextId++;\n            if (!existingNodes.has(`tag:${data.url_status}`)) {\n                newNodes.push({\n                    id: statusId,\n                    type: 'tag',\n                    label: `Status: ${data.url_status}`,\n                    title: `URLhaus Status: ${data.url_status}`,\n                    color: { background: '#6d28d9' },\n                    tag: data.url_status\n                });\n                existingNodes.set(`tag:${data.url_status}`, statusId);\n            }\n            newEdges.push({ id: `${urlNodeId}-${statusId}-Status`, from: urlNodeId, to: statusId, label: 'Status' });\n        }\n\n        // Threat Type\n        if (data.threat) {\n            const threatId = existingNodes.get(`threat:${data.threat}`) || nextId++;\n            if (!existingNodes.has(`threat:${data.threat}`)) {\n                newNodes.push({\n                    id: threatId,\n                    type: 'threat',\n                    label: `Threat: ${data.threat}`,\n                    title: `URLhaus Threat: ${data.threat}`,\n                    color: { background: '#ef4444' },\n                    threat: data.threat\n                });\n                existingNodes.set(`threat:${data.threat}`, threatId);\n            }\n            newEdges.push({ id: `${urlNodeId}-${threatId}-Threat`, from: urlNodeId, to: threatId, label: 'Threat' });\n        }\n\n        // Date Added\n        if (data.date_added) {\n            const dateId = existingNodes.get(`timestamp:${data.date_added}`) || nextId++;\n            if (!existingNodes.has(`timestamp:${data.date_added}`)) {\n                newNodes.push({\n                    id: dateId,\n                    type: 'timestamp',\n                    label: `Added: ${data.date_added}`,\n                    title: `Date Added: ${data.date_added}`,\n                    color: { background: '#f97316' },\n                    timestamp: data.date_added\n                });\n                existingNodes.set(`timestamp:${data.date_added}`, dateId);\n            }\n            newEdges.push({ id: `${urlNodeId}-${dateId}-AddedOn`, from: urlNodeId, to: dateId, label: 'Added on' });\n        }\n\n        // Tags\n        if (data.tags && Array.isArray(data.tags)) {\n            data.tags.forEach(tag => {\n                const tagId = existingNodes.get(`tag:${tag}`) || nextId++;\n                if (!existingNodes.has(`tag:${tag}`)) {\n                    newNodes.push({\n                        id: tagId,\n                        type: 'tag',\n                        label: `Tag: ${tag}`,\n                        title: `URLhaus Tag: ${tag}`,\n                        color: { background: '#6d28d9' },\n                        tag: tag\n                    });\n                    existingNodes.set(`tag:${tag}`, tagId);\n                }\n                newEdges.push({ id: `${urlNodeId}-${tagId}-Tagged`, from: urlNodeId, to: tagId, label: 'Tagged' });\n            });\n        }\n\n        // Update node with URLhaus reference\n        nodes.update({\n            id: urlNodeId,\n            title: `${nodes.get(urlNodeId).title || url}\\nURLhaus Ref: ${data.urlhaus_reference || 'N/A'}`\n        });\n\n        if (newNodes.length) nodes.add(newNodes);\n        if (newEdges.length) edges.add(newEdges);\n\n        updateNodeSizes();\n        updateSelectOptions();\n        if (!isBulk) {\n            await stabilizeNetwork();\n            showToast(`URL ${url} enriched with URLhaus data`, 'success');\n        }\n    } catch (error) {\n        if (error.name === 'AbortError') {\n            showToast(`Enrichment of URL ${url} cancelled`, 'info');\n            return;\n        }\n        console.error(`Error enriching URL ${url} with URLhaus: ${error.message}`);\n        showToast(`Error enriching URL ${url}: ${error.message}`, 'error');\n        if (!isBulk) await stabilizeNetwork();\n    }\n}, RATE_LIMIT_MS);\n\nconst throttledEnrichURLhausMultiple = throttleRequest(async function enrichURLhausMultiple(urls, nodeIds) {\n    if (!Array.isArray(urls) || !Array.isArray(nodeIds) || urls.length !== nodeIds.length) {\n        showToast('Invalid input for multiple URLhaus enrichment', 'error');\n        return;\n    }\n\n    if (!urlhausApiKey && !ignoreApiKeysViaProxy) {\n        showToast('Please set your URLhaus API key in the \"Config\" tab first.', 'error');\n        return;\n    }\n\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const totalURLs = urls.length;\n    let successfulEnrichments = 0;\n\n    document.getElementById('progress-bar').textContent = `URLhaus Enrichment: 0/${totalURLs} URLs (0%)`;\n\n    for (let i = 0; i < totalURLs; i++) {\n        if (activeTaskController?.signal.aborted) {\n            showToast('URLhaus enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = `URLhaus Enrichment: Stopped at ${successfulEnrichments}/${totalURLs} URLs`;\n            break;\n        }\n        await throttledEnrichURLhaus(urls[i], nodeIds[i], true);\n        successfulEnrichments++;\n        const progress = ((successfulEnrichments / totalURLs) * 100).toFixed(1);\n        document.getElementById('progress-bar').textContent = `URLhaus Enrichment: ${successfulEnrichments}/${totalURLs} URLs (${progress}%)`;\n        await new Promise(resolve => setTimeout(resolve, 200)); // Rate limiting\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    completeProgressBar();\n    showToast(`Enriched ${successfulEnrichments}/${totalURLs} URLs with URLhaus`, 'success');\n}, RATE_LIMIT_MS);\n\nasync function enrichAllURLhaus() {\n    if (!urlhausApiKey && !ignoreApiKeysViaProxy) {\n        showToast('Please set your URLhaus API key in the \"Config\" tab first.', 'error');\n        return;\n    }\n\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const urlNodes = nodes.get({ filter: n => n.type === 'url' && n.url });\n    const totalURLs = urlNodes.length;\n    let successfulEnrichments = 0;\n\n    if (totalURLs === 0) {\n        showToast('No URL nodes found to enrich', 'info');\n        completeProgressBar();\n        return;\n    }\n\n    const batchSize = 10;\n    const delayBetweenBatches = 500;\n    const totalBatches = Math.ceil(totalURLs / batchSize);\n    const estimatedTimeMs = totalBatches * delayBetweenBatches;\n    const timeEstimateStr = Math.ceil(estimatedTimeMs / 60000) + 'm';\n\n    document.getElementById('progress-bar').textContent = `URLhaus Enrichment: 0/${totalURLs} URLs (0%) - Est. ${timeEstimateStr}`;\n\n    for (let i = 0; i < totalURLs; i += batchSize) {\n        if (activeTaskController?.signal.aborted) {\n            showToast('URLhaus enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = `URLhaus Enrichment: Stopped at ${successfulEnrichments}/${totalURLs} URLs`;\n            break;\n        }\n\n        const batch = urlNodes.slice(i, Math.min(i + batchSize, totalURLs));\n        const promises = batch.map(node => \n            throttledEnrichURLhaus(node.url, node.id, true)\n                .then(() => successfulEnrichments++)\n                .catch(error => console.error(`Error for ${node.url}: ${error.message}`))\n        );\n\n        await Promise.all(promises);\n\n        const progress = ((successfulEnrichments / totalURLs) * 100).toFixed(1);\n        document.getElementById('progress-bar').textContent = `URLhaus Enrichment: ${successfulEnrichments}/${totalURLs} URLs (${progress}%)`;\n        await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    //ensureInteractionSettings();\n    completeProgressBar();\n    showToast(`URLhaus enrichment completed: ${successfulEnrichments}/${totalURLs} URLs enriched`, 'success');\n}\n\n\n// Replace the existing isPrivateIP function (for completeness)\nfunction isPrivateIP(ip) {\n    console.log(`isPrivateIP called with: ${ip}`);\n    const octets = ip.split('.').map(Number);\n    console.log(`Parsed octets: ${octets}`);\n    \n    if (octets.length !== 4 || octets.some(o => o < 0 || o > 255)) {\n        console.log(`Invalid IP format: ${ip}`);\n        return false;\n    }\n\n    const isPrivate = (\n        (octets[0] === 10) || // 10.0.0.0/8\n        (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) || // 172.16.0.0/12\n        (octets[0] === 192 && octets[1] === 168) // 192.168.0.0/16\n    );\n    \n    console.log(`IP ${ip} is ${isPrivate ? 'private' : 'public'}`);\n    return isPrivate;\n}\n\n\nfunction exportConfigBackup() {\n    // Collect API keys and proxy config from localStorage\n    const configData = {\n        ipinfoApiKey: localStorage.getItem('ipinfoApiKey') || '',\n        shodanApiKey: localStorage.getItem('shodanApiKey') || '',\n        greynoiseApiKey: localStorage.getItem('greynoiseApiKey') || '',\n        urlscanApiKey: localStorage.getItem('urlscanApiKey') || '',\n        securitytrailsApiKey: localStorage.getItem('securitytrailsApiKey') || '',\n        urlhausApiKey: localStorage.getItem('urlhausApiKey') || '',\n        corsProxyUrl: localStorage.getItem('corsProxyUrl') || '',\n        routeViaProxy: localStorage.getItem('routeViaProxy') === 'true',\n        ignoreApiKeysViaProxy: localStorage.getItem('ignoreApiKeysViaProxy') === 'true'\n    };\n\n    // Convert to JSON\n    const json = JSON.stringify(configData, null, 2);\n    const blob = new Blob([json], { type: 'application/json' });\n    const url = URL.createObjectURL(blob);\n    const a = document.createElement('a');\n    a.href = url;\n    a.download = 'configuration_backup.json';\n    document.body.appendChild(a);\n    a.click();\n    document.body.removeChild(a);\n    URL.revokeObjectURL(url);\n\n    showToast('Configuration backup exported successfully', 'success');\n}\n\nfunction importConfig() {\n    // Create a temporary file input element\n    const input = document.createElement('input');\n    input.type = 'file';\n    input.accept = '.json';\n\n    input.onchange = function(event) {\n        const file = event.target.files[0];\n        if (!file) {\n            showToast('No file selected', 'error');\n            return;\n        }\n\n        const reader = new FileReader();\n        reader.onload = function(e) {\n            try {\n                const configData = JSON.parse(e.target.result);\n\n                // Validate the imported data\n                const expectedKeys = [\n                    'ipinfoApiKey', 'shodanApiKey', 'greynoiseApiKey', 'urlscanApiKey', \n                    'securitytrailsApiKey', 'urlhausApiKey', 'corsProxyUrl', \n                    'routeViaProxy', 'ignoreApiKeysViaProxy'\n                ];\n                const hasValidKeys = expectedKeys.every(key => key in configData);\n                if (!hasValidKeys) {\n                    throw new Error('Invalid configuration file: missing required keys');\n                }\n\n                // Update localStorage and global variables\n                ipinfoApiKey = configData.ipinfoApiKey || '';\n                shodanApiKey = configData.shodanApiKey || '';\n                greynoiseApiKey = configData.greynoiseApiKey || '';\n                urlscanApiKey = configData.urlscanApiKey || '';\n                securitytrailsApiKey = configData.securitytrailsApiKey || '';\n                urlhausApiKey = configData.urlhausApiKey || '';\n                corsProxyUrl = configData.corsProxyUrl || 'http://localhost:3000/proxy?url=';\n                routeViaProxy = !!configData.routeViaProxy;\n                ignoreApiKeysViaProxy = !!configData.ignoreApiKeysViaProxy;\n\n                // Persist to localStorage\n                localStorage.setItem('ipinfoApiKey', ipinfoApiKey);\n                localStorage.setItem('shodanApiKey', shodanApiKey);\n                localStorage.setItem('greynoiseApiKey', greynoiseApiKey);\n                localStorage.setItem('urlscanApiKey', urlscanApiKey);\n                localStorage.setItem('securitytrailsApiKey', securitytrailsApiKey);\n                localStorage.setItem('urlhausApiKey', urlhausApiKey);\n                localStorage.setItem('corsProxyUrl', corsProxyUrl);\n                localStorage.setItem('routeViaProxy', routeViaProxy.toString());\n                localStorage.setItem('ignoreApiKeysViaProxy', ignoreApiKeysViaProxy.toString());\n\n                // Update UI elements\n                document.getElementById('ipinfoApiKey').value = ipinfoApiKey;\n                document.getElementById('shodanApiKey').value = shodanApiKey;\n                document.getElementById('greynoiseApiKey').value = greynoiseApiKey;\n                document.getElementById('urlscanApiKey').value = urlscanApiKey;\n                document.getElementById('securitytrailsApiKey').value = securitytrailsApiKey;\n                document.getElementById('urlhausApiKey').value = urlhausApiKey;\n                document.getElementById('corsProxyUrl').value = corsProxyUrl;\n                document.getElementById('routeViaProxy').checked = routeViaProxy;\n                document.getElementById('ignoreApiKeysViaProxy').checked = ignoreApiKeysViaProxy;\n\n                // Update checkbox states based on whether keys are stored\n                document.getElementById('storeIpinfoKey').checked = !!ipinfoApiKey;\n                document.getElementById('storeShodanKey').checked = !!shodanApiKey;\n                document.getElementById('storeGreynoiseKey').checked = !!greynoiseApiKey;\n                document.getElementById('storeUrlscanKey').checked = !!urlscanApiKey;\n                document.getElementById('storeSecuritytrailsKey').checked = !!securitytrailsApiKey;\n                document.getElementById('storeUrlhausKey').checked = !!urlhausApiKey;\n                document.getElementById('storeCorsProxy').checked = !!corsProxyUrl;\n\n                showToast('Configuration imported successfully', 'success');\n            } catch (error) {\n                console.error('Error importing config:', error);\n                showToast(`Failed to import configuration: ${error.message}`, 'error');\n            }\n        };\n        reader.readAsText(file);\n    };\n\n    // Trigger file selection\n    input.click();\n}\n\n// IMport from NMAP XML Function\n\nasync function importNMAP() {\n    // Create a temporary file input element\n    const input = document.createElement('input');\n    input.type = 'file';\n    input.accept = '.xml';\n\n    input.onchange = async function(event) {\n        const file = event.target.files[0];\n        if (!file) {\n            showToast('No file selected', 'error');\n            return;\n        }\n\n        const reader = new FileReader();\n        reader.onload = async function(e) {\n            try {\n                let xmlText = e.target.result;\n                console.log('Raw XML Text:', xmlText); // Log raw input for debugging\n\n                // Extract valid XML content between <?xml and </nmaprun>\n                const xmlStart = xmlText.indexOf('<?xml');\n                const xmlEnd = xmlText.lastIndexOf('</nmaprun>') + '</nmaprun>'.length;\n                if (xmlStart === -1 || xmlEnd === -1) {\n                    throw new Error('Could not find valid XML content in file');\n                }\n                xmlText = xmlText.substring(xmlStart, xmlEnd);\n                console.log('Extracted XML Text:', xmlText); // Log extracted XML\n\n                const parser = new DOMParser();\n                const xmlDoc = parser.parseFromString(xmlText, 'application/xml');\n\n                // Check for XML parsing errors\n                const parserError = xmlDoc.querySelector('parsererror');\n                if (parserError) {\n                    console.error('Parser Error Details:', parserError.textContent);\n                    throw new Error('Invalid Nmap XML file: ' + parserError.textContent);\n                }\n\n                console.log('Parsed XML Document:', xmlDoc); // Log parsed doc for debugging\n\n                // Maps for deduplication\n                const existingNodes = new Map();\n                nodes.forEach(node => {\n                    if (node.type === 'ip' && node.ip) existingNodes.set(`ip:${node.ip}`, node);\n                    if (node.type === 'domain' && node.domain) existingNodes.set(`domain:${node.domain.toLowerCase()}`, node);\n                    if (node.type === 'port' && node.portNumber) existingNodes.set(`port:${node.portType}/${node.portNumber}`, node);\n                    if (node.type === 'service' && node.serviceName) existingNodes.set(`service:${node.serviceName.toLowerCase()}_${node.portType}/${node.portNumber}`, node);\n                    if (node.type === 'os' && node.os) existingNodes.set(`os:${node.os}`, node);\n                    if (node.type === 'cpe' && node.cpe) existingNodes.set(`cpe:${node.cpe}`, node);\n                    if (node.type === 'hop' && node.ipaddr) existingNodes.set(`hop:${node.ipaddr}`, node);\n                    if (node.type === 'mac' && node.mac) existingNodes.set(`mac:${node.mac.toLowerCase()}`, node);\n                });\n\n                const newNodes = [];\n                const newEdges = [];\n                let processedCount = 0;\n                const batchSize = 50;\n\n                // Process each host\n                const hosts = xmlDoc.getElementsByTagName('host');\n                if (hosts.length === 0) {\n                    throw new Error('No hosts found in Nmap XML');\n                }\n\n                for (const host of hosts) {\n                    // Extract IP address\n                    const ipAddr = host.querySelector('address[addrtype=\"ipv4\"]');\n                    const ip = ipAddr ? ipAddr.getAttribute('addr') : null;\n                    if (!ip) continue;\n\n                    let ipNodeId;\n                    if (!existingNodes.has(`ip:${ip}`)) {\n                        ipNodeId = nextId++;\n                        newNodes.push({\n                            id: ipNodeId,\n                            type: 'ip',\n                            label: `IP: ${ip}`,\n                            title: `IP Address: ${ip}`,\n                            color: { background: '#f87171' },\n                            ip: ip,\n                            size: 20\n                        });\n                        existingNodes.set(`ip:${ip}`, { id: ipNodeId });\n                        console.log(`Added IP node: ${ip}, ID: ${ipNodeId}`);\n                    } else {\n                        ipNodeId = existingNodes.get(`ip:${ip}`).id;\n                    }\n\n                    // Extract MAC address\n                    const macAddr = host.querySelector('address[addrtype=\"mac\"]');\n                    const mac = macAddr ? macAddr.getAttribute('addr') : null;\n                    if (mac) {\n                        const macKey = `mac:${mac.toLowerCase()}`;\n                        let macNodeId;\n                        if (!existingNodes.has(macKey)) {\n                            macNodeId = nextId++;\n                            newNodes.push({\n                                id: macNodeId,\n                                type: 'mac',\n                                label: `MAC: ${mac}`,\n                                title: `MAC Address: ${mac}`,\n                                color: { background: '#8b5cf6' },\n                                mac: mac,\n                                size: 15\n                            });\n                            existingNodes.set(macKey, { id: macNodeId });\n                            console.log(`Added MAC node: ${mac}, ID: ${macNodeId}`);\n                        } else {\n                            macNodeId = existingNodes.get(macKey).id;\n                        }\n                        newEdges.push({\n                            id: `edge_${ipNodeId}_${macNodeId}_${Date.now()}`,\n                            from: ipNodeId,\n                            to: macNodeId,\n                            label: 'Has MAC'\n                        });\n                        console.log(`Linked IP ${ipNodeId} to MAC: ${macNodeId}`);\n                    }\n\n                    // Extract hostnames\n                    const hostnames = host.getElementsByTagName('hostname');\n                    for (const hostname of hostnames) {\n                        const domain = hostname.getAttribute('name');\n                        if (domain && !existingNodes.has(`domain:${domain.toLowerCase()}`)) {\n                            const domainNodeId = nextId++;\n                            newNodes.push({\n                                id: domainNodeId,\n                                type: 'domain',\n                                label: `Domain: ${domain}`,\n                                title: `Domain: ${domain}\\nType: ${hostname.getAttribute('type')}`,\n                                color: { background: '#60a5fa' },\n                                domain: domain,\n                                size: 20\n                            });\n                            existingNodes.set(`domain:${domain.toLowerCase()}`, { id: domainNodeId });\n                            newEdges.push({\n                                id: `edge_${ipNodeId}_${domainNodeId}_${Date.now()}`,\n                                from: ipNodeId,\n                                to: domainNodeId,\n                                label: 'Resolves to'\n                            });\n                            console.log(`Added Domain node: ${domain}, ID: ${domainNodeId}, Edge to IP: ${ipNodeId}`);\n                        } else if (domain) {\n                            const existingDomainNode = existingNodes.get(`domain:${domain.toLowerCase()}`);\n                            if (!newEdges.some(e => e.from === ipNodeId && e.to === existingDomainNode.id)) {\n                                newEdges.push({\n                                    id: `edge_${ipNodeId}_${existingDomainNode.id}_${Date.now()}`,\n                                    from: ipNodeId,\n                                    to: existingDomainNode.id,\n                                    label: 'Resolves to'\n                                });\n                                console.log(`Linked IP ${ipNodeId} to existing Domain: ${existingDomainNode.id}`);\n                            }\n                        }\n                    }\n\n                    // Extract ports, services, and certificate domains\n                    const ports = host.getElementsByTagName('port');\n                    for (const port of ports) {\n                        const protocol = port.getAttribute('protocol');\n                        const portNumber = port.getAttribute('portid');\n                        const state = port.querySelector('state')?.getAttribute('state') || 'unknown';\n                        const service = port.querySelector('service');\n                        const serviceName = service?.getAttribute('name') || 'unknown';\n                        const product = service?.getAttribute('product') || '';\n                        const version = service?.getAttribute('version') || '';\n                        const extrainfo = service?.getAttribute('extrainfo') || '';\n\n                        const portKey = `port:${protocol.toUpperCase()}/${portNumber}`;\n                        let portNodeId;\n                        if (!existingNodes.has(portKey)) {\n                            portNodeId = nextId++;\n                            newNodes.push({\n                                id: portNodeId,\n                                type: 'port',\n                                label: `${protocol.toUpperCase()}/${portNumber} (${state})`,\n                                title: `Port\\nType: ${protocol.toUpperCase()}\\nNumber: ${portNumber}\\nState: ${state}`,\n                                color: { background: state === 'open' ? '#a78bfa' : '#d1d5db' },\n                                portType: protocol.toUpperCase(),\n                                portNumber: portNumber,\n                                size: 10\n                            });\n                            existingNodes.set(portKey, { id: portNodeId });\n                            console.log(`Added Port node: ${protocol}/${portNumber}, State: ${state}, ID: ${portNodeId}`);\n                        } else {\n                            portNodeId = existingNodes.get(portKey).id;\n                        }\n\n                        newEdges.push({\n                            id: `edge_${ipNodeId}_${portNodeId}_${Date.now()}`,\n                            from: ipNodeId,\n                            to: portNodeId,\n                            label: 'Has port'\n                        });\n                        console.log(`Linked IP ${ipNodeId} to Port: ${portNodeId}`);\n\n                        // Service entity linked to port\n                        const serviceKey = `service:${serviceName.toLowerCase()}_${protocol.toUpperCase()}/${portNumber}`;\n                        let serviceNodeId;\n                        if (serviceName !== 'unknown' && !existingNodes.has(serviceKey)) {\n                            serviceNodeId = nextId++;\n                            newNodes.push({\n                                id: serviceNodeId,\n                                type: 'service',\n                                label: `Service: ${serviceName}`,\n                                title: `Service\\nName: ${serviceName}${product ? `\\nProduct: ${product}` : ''}${version ? `\\nVersion: ${version}` : ''}${extrainfo ? `\\nExtra Info: ${extrainfo}` : ''}`,\n                                color: { background: '#ec4899' },\n                                serviceName: serviceName,\n                                portType: protocol.toUpperCase(),\n                                portNumber: portNumber,\n                                size: 15\n                            });\n                            existingNodes.set(serviceKey, { id: serviceNodeId });\n                            newEdges.push({\n                                id: `edge_${portNodeId}_${serviceNodeId}_${Date.now()}`,\n                                from: portNodeId,\n                                to: serviceNodeId,\n                                label: 'Runs'\n                            });\n                            console.log(`Added Service node: ${serviceName}, ID: ${serviceNodeId}, Edge to Port: ${portNodeId}`);\n                        } else if (serviceName !== 'unknown') {\n                            serviceNodeId = existingNodes.get(serviceKey).id;\n                            if (!newEdges.some(e => e.from === portNodeId && e.to === serviceNodeId)) {\n                                newEdges.push({\n                                    id: `edge_${portNodeId}_${serviceNodeId}_${Date.now()}`,\n                                    from: portNodeId,\n                                    to: serviceNodeId,\n                                    label: 'Runs'\n                                });\n                                console.log(`Linked Port ${portNodeId} to existing Service: ${serviceNodeId}`);\n                            }\n                        }\n\n                        // CPE entities from service\n                        const cpes = service ? service.getElementsByTagName('cpe') : [];\n                        for (const cpe of cpes) {\n                            const cpeValue = cpe.textContent;\n                            if (cpeValue && !existingNodes.has(`cpe:${cpeValue}`)) {\n                                const cpeNodeId = nextId++;\n                                newNodes.push({\n                                    id: cpeNodeId,\n                                    type: 'cpe',\n                                    label: `CPE: ${cpeValue.split(':').slice(2).join(':')}`,\n                                    title: `CPE: ${cpeValue}`,\n                                    color: { background: '#f59e0b' },\n                                    cpe: cpeValue,\n                                    size: 12\n                                });\n                                existingNodes.set(`cpe:${cpeValue}`, { id: cpeNodeId });\n                                newEdges.push({\n                                    id: `edge_${serviceNodeId || portNodeId}_${cpeNodeId}_${Date.now()}`,\n                                    from: serviceNodeId || portNodeId,\n                                    to: cpeNodeId,\n                                    label: 'Identifies'\n                                });\n                                console.log(`Added CPE node: ${cpeValue}, ID: ${cpeNodeId}, Edge to ${serviceNodeId ? 'Service' : 'Port'}: ${serviceNodeId || portNodeId}`);\n                            }\n                        }\n\n                        // Extract certificate domains (SANs) from ssl-cert script\n                        const sslCertScript = port.querySelector('script[id=\"ssl-cert\"]');\n                        if (sslCertScript) {\n                            const output = sslCertScript.getAttribute('output') || '';\n                            // Match SANs like \"DNS:example.com\" or \"DNS:*.example.com\"\n                            const sanMatches = output.match(/DNS:[^\\s,]+/g) || [];\n                            for (const san of sanMatches) {\n                                const domain = san.replace('DNS:', '').trim();\n                                if (domain && !existingNodes.has(`domain:${domain.toLowerCase()}`)) {\n                                    const domainNodeId = nextId++;\n                                    newNodes.push({\n                                        id: domainNodeId,\n                                        type: 'domain',\n                                        label: `Domain: ${domain}`,\n                                        title: `Domain: ${domain}\\nSource: SSL Certificate SAN`,\n                                        color: { background: '#60a5fa' },\n                                        domain: domain,\n                                        size: 20\n                                    });\n                                    existingNodes.set(`domain:${domain.toLowerCase()}`, { id: domainNodeId });\n                                    newEdges.push({\n                                        id: `edge_${ipNodeId}_${domainNodeId}_${Date.now()}`,\n                                        from: ipNodeId,\n                                        to: domainNodeId,\n                                        label: 'Certificate Domain'\n                                    });\n                                    console.log(`Added SAN Domain node: ${domain}, ID: ${domainNodeId}, Edge to IP: ${ipNodeId}`);\n                                } else if (domain) {\n                                    const existingDomainNode = existingNodes.get(`domain:${domain.toLowerCase()}`);\n                                    if (!newEdges.some(e => e.from === ipNodeId && e.to === existingDomainNode.id && e.label === 'Certificate Domain')) {\n                                        newEdges.push({\n                                            id: `edge_${ipNodeId}_${existingDomainNode.id}_${Date.now()}`,\n                                            from: ipNodeId,\n                                            to: existingDomainNode.id,\n                                            label: 'Certificate Domain'\n                                        });\n                                        console.log(`Linked IP ${ipNodeId} to existing SAN Domain: ${existingDomainNode.id}`);\n                                    }\n                                }\n                            }\n                        }\n                    }\n\n                    // Extract OS\n                    const osMatch = host.querySelector('osmatch');\n                    if (osMatch) {\n                        const osName = osMatch.getAttribute('name');\n                        const osKey = `os:${osName}`;\n                        let osNodeId;\n                        if (!existingNodes.has(osKey)) {\n                            osNodeId = nextId++;\n                            newNodes.push({\n                                id: osNodeId,\n                                type: 'os',\n                                label: `OS: ${osName}`,\n                                title: `Operating System: ${osName}\\nAccuracy: ${osMatch.getAttribute('accuracy')}%`,\n                                color: { background: '#10b981' },\n                                os: osName,\n                                size: 15\n                            });\n                            existingNodes.set(osKey, { id: osNodeId });\n                            newEdges.push({\n                                id: `edge_${ipNodeId}_${osNodeId}_${Date.now()}`,\n                                from: ipNodeId,\n                                to: osNodeId,\n                                label: 'Runs'\n                            });\n                            console.log(`Added OS node: ${osName}, ID: ${osNodeId}, Edge to IP: ${ipNodeId}`);\n                        } else {\n                            osNodeId = existingNodes.get(osKey).id;\n                            if (!newEdges.some(e => e.from === ipNodeId && e.to === osNodeId)) {\n                                newEdges.push({\n                                    id: `edge_${ipNodeId}_${osNodeId}_${Date.now()}`,\n                                    from: ipNodeId,\n                                    to: osNodeId,\n                                    label: 'Runs'\n                                });\n                                console.log(`Linked IP ${ipNodeId} to existing OS: ${osNodeId}`);\n                            }\n                        }\n\n                        // CPE entities from OS\n                        const osCpes = host.querySelectorAll('osclass > cpe');\n                        for (const cpe of osCpes) {\n                            const cpeValue = cpe.textContent;\n                            if (cpeValue && !existingNodes.has(`cpe:${cpeValue}`)) {\n                                const cpeNodeId = nextId++;\n                                newNodes.push({\n                                    id: cpeNodeId,\n                                    type: 'cpe',\n                                    label: `CPE: ${cpeValue.split(':').slice(2).join(':')}`,\n                                    title: `CPE: ${cpeValue}`,\n                                    color: { background: '#f59e0b' },\n                                    cpe: cpeValue,\n                                    size: 12\n                                });\n                                existingNodes.set(`cpe:${cpeValue}`, { id: cpeNodeId });\n                                newEdges.push({\n                                    id: `edge_${osNodeId}_${cpeNodeId}_${Date.now()}`,\n                                    from: osNodeId,\n                                    to: cpeNodeId,\n                                    label: 'Identifies'\n                                });\n                                console.log(`Added CPE node: ${cpeValue}, ID: ${cpeNodeId}, Edge to OS: ${osNodeId}`);\n                            }\n                        }\n                    }\n\n                    // Extract trace hops\n                    const trace = host.querySelector('trace');\n                    if (trace) {\n                        const hops = trace.getElementsByTagName('hop');\n                        for (const hop of hops) {\n                            const hopIp = hop.getAttribute('ipaddr');\n                            if (hopIp && !existingNodes.has(`hop:${hopIp}`)) {\n                                const hopNodeId = nextId++;\n                                newNodes.push({\n                                    id: hopNodeId,\n                                    type: 'hop',\n                                    label: `Hop: ${hopIp}`,\n                                    title: `Trace Hop\\nIP: ${hopIp}\\nTTL: ${hop.getAttribute('ttl')}\\nRTT: ${hop.getAttribute('rtt') || 'unknown'} ms\\nHost: ${hop.getAttribute('host') || 'unknown'}`,\n                                    color: { background: '#14b8a6' },\n                                    ipaddr: hopIp,\n                                    size: 12\n                                });\n                                existingNodes.set(`hop:${hopIp}`, { id: hopNodeId });\n                                newEdges.push({\n                                    id: `edge_${ipNodeId}_${hopNodeId}_${Date.now()}`,\n                                    from: ipNodeId,\n                                    to: hopNodeId,\n                                    label: 'Reachable via'\n                                });\n                                console.log(`Added Hop node: ${hopIp}, ID: ${hopNodeId}, Edge to IP: ${ipNodeId}`);\n                            }\n                        }\n                    }\n\n                    processedCount++;\n                    if (processedCount % batchSize === 0) {\n                        nodes.add(newNodes);\n                        edges.add(newEdges);\n                        newNodes.length = 0;\n                        newEdges.length = 0;\n                        await new Promise(resolve => setTimeout(resolve, 0));\n                    }\n                }\n\n                // Add remaining items\n                if (newNodes.length > 0) {\n                    nodes.add(newNodes);\n                    edges.add(newEdges);\n                }\n\n                updateNodeSizes();\n                updateSelectOptions();\n                await stabilizeNetwork().catch(err => console.error('Error stabilizing network:', err));\n                showToast(`Imported ${hosts.length} hosts from Nmap XML`, 'success');\n            } catch (error) {\n                console.error('Error importing Nmap XML:', error);\n                showToast(`Failed to import Nmap XML: ${error.message}`, 'error');\n            }\n        };\n        reader.readAsText(file);\n    };\n\n    // Trigger file selection\n    input.click();\n}\n\n// Risk analysis\n\nfunction riskAnalysis() {\n    const riskyPorts = {\n        'TCP/3389': 'RDP (Remote Desktop Protocol)',\n        'TCP/5985': 'WinRM (Windows Remote Management)',\n        'TCP/5986': 'WinRM (Windows Remote Management HTTPS)',\n        'TCP/1433': 'MSSQL (Microsoft SQL Server)',\n        'TCP/21': 'FTP (File Transfer Protocol)',\n        'TCP/389': 'LDAP (Lightweight Directory Access Protocol)',\n        'TCP/445': 'SMB (Server Message Block)',\n        'TCP/443': 'HTTPS (Hypertext Transfer Protocol Secure)',\n        'TCP/80': 'HTTP (Hypertext Transfer Protocol)',\n        'TCP/53': 'DNS (Domain Name System)'\n    };\n\n    const risks = [];\n    const ipNodes = new Map();\n\n    // Collect IP nodes\n    nodes.forEach(node => {\n        if ((node.type || '').toLowerCase() === 'ip' && node.ip) {\n            ipNodes.set(node.ip, { id: node.id, domains: new Set(), riskyPorts: new Set() });\n        }\n    });\n\n    // Link domains to IPs\n    edges.forEach(edge => {\n        const label = (edge.label || '').toLowerCase();\n        if (label === 'resolves to' || label === 'certificate domain') {\n            const fromNode = nodes.get(edge.from);\n            const toNode = nodes.get(edge.to);\n            if (fromNode?.type.toLowerCase() === 'ip' && toNode?.type.toLowerCase() === 'domain' && fromNode.ip && toNode.domain) {\n                ipNodes.get(fromNode.ip).domains.add(toNode.domain);\n            }\n        }\n    });\n\n    // Identify risky ports\n    nodes.forEach(node => {\n        if ((node.type || '').toLowerCase() === 'port') {\n            const portType = (node.portType || '').toUpperCase();\n            const portNumber = String(node.portNumber || '');\n            const portKey = `${portType}/${portNumber}`;\n            let state = (node.state || '').toLowerCase();\n            if (!state && node.label) {\n                const stateMatch = node.label.match(/\\((open|closed|filtered)\\)/i);\n                if (stateMatch) state = stateMatch[1].toLowerCase();\n            }\n\n            if (riskyPorts[portKey] && state === 'open') {\n                edges.forEach(edge => {\n                    const edgeLabel = (edge.label || '').toLowerCase();\n                    if (edge.to === node.id && edgeLabel === 'has port') {\n                        const ipNode = nodes.get(edge.from);\n                        if (ipNode?.type.toLowerCase() === 'ip' && ipNode.ip) {\n                            ipNodes.get(ipNode.ip).riskyPorts.add(portKey);\n                        }\n                    }\n                });\n            }\n        }\n    });\n\n    // Build risks\n    ipNodes.forEach((data, ip) => {\n        if (data.riskyPorts.size > 0) {\n            risks.push({\n                ip: ip,\n                domains: Array.from(data.domains),\n                riskyPorts: Array.from(data.riskyPorts).map(port => ({\n                    port: port,\n                    name: riskyPorts[port]\n                }))\n            });\n        }\n    });\n\n    // Display results\n    const modal = document.getElementById('riskModal');\n    if (risks.length === 0) {\n        modal.style.display = 'none';\n        showToast('No risky elements found in the graph', 'info');\n        return;\n    }\n\n    const tableContainer = document.getElementById('riskTableContainer');\n    let tableHtml = `\n        <table>\n            <thead>\n                <tr>\n                    <th>IP Address</th>\n                    <th>Domain Names</th>\n                    <th>Risky Ports</th>\n                </tr>\n            </thead>\n            <tbody>\n    `;\n\n    risks.forEach(risk => {\n        tableHtml += `\n            <tr>\n                <td>${risk.ip}</td>\n                <td>${risk.domains.length > 0 ? risk.domains.join(', ') : 'None'}</td>\n                <td>${risk.riskyPorts.map(p => `${p.name} (${p.port})`).join(', ')}</td>\n            </tr>\n        `;\n    });\n\n    tableHtml += `\n            </tbody>\n        </table>\n        <button class=\"print-button\" onclick=\"printRiskTableToPDF()\">Print to PDF</button>\n        <button class=\"export-button\" onclick=\"exportToExcel()\">Export to Excel</button>\n    `;\n\n    tableContainer.innerHTML = tableHtml;\n    modal.style.display = 'block';\n\n    // Modal close functionality\n    const closeModal = modal.querySelector('.close-modal');\n    closeModal.onclick = () => modal.style.display = 'none';\n    modal.onclick = (e) => {\n        if (e.target === modal) modal.style.display = 'none';\n    };\n}\n\n\n// Add event listener to close modal when clicking outside\nwindow.onclick = function(event) {\n    const modal = document.getElementById('riskModal');\n    if (event.target === modal) {\n        modal.style.display = 'none';\n    }\n};\n\n\n\n// Save Risk Assessment to PDF\nfunction printRiskTableToPDF() {\n    const { jsPDF } = window.jspdf;\n    const doc = new jsPDF();\n    const table = document.querySelector('#riskTableContainer table');\n\n    if (!table) {\n        showToast('No risk table found to print', 'error');\n        return;\n    }\n\n    // Add title\n    doc.setFontSize(16);\n    doc.text('Risk Analysis Report', 10, 10);\n\n    // Convert table to PDF with light mode styling\n    doc.autoTable({\n        html: table,\n        startY: 20,\n        styles: {\n            fillColor: [255, 255, 255], // White background for all cells\n            textColor: [31, 42, 68],    // Dark text (#1f2a44)\n            lineColor: [209, 213, 219], // Light gray border (#d1d5db)\n            lineWidth: 0.1\n        },\n        headStyles: {\n            fillColor: [241, 245, 249], // Light gray header (#f1f5f9)\n            textColor: [31, 42, 68]     // Dark text for header\n        },\n        alternateRowStyles: {\n            fillColor: [255, 255, 255]  // No striping, all rows white\n        }\n    });\n\n    // Save the PDF\n    doc.save('risk_analysis.pdf');\n}\n\nfunction exportToExcel() {\n    // Check if XLSX is available\n    if (typeof XLSX === 'undefined') {\n        showToast('Excel export library not loaded', 'error');\n        return;\n    }\n\n    const table = document.querySelector('#riskTableContainer table');\n    if (!table) {\n        showToast('No risk table found to export', 'error');\n        return;\n    }\n\n    // Prepare data for Excel\n    const data = [];\n    const headers = ['IP Address', 'Domain Names', 'Risky Ports'];\n    data.push(headers);\n\n    const rows = table.querySelectorAll('tbody tr');\n    rows.forEach(row => {\n        const cells = row.querySelectorAll('td');\n        const rowData = [\n            cells[0].textContent, // IP Address\n            cells[1].textContent, // Domain Names\n            cells[2].textContent  // Risky Ports\n        ];\n        data.push(rowData);\n    });\n\n    // Create workbook and worksheet\n    const ws = XLSX.utils.aoa_to_sheet(data); // Array of arrays to sheet\n    const wb = XLSX.utils.book_new();\n    XLSX.utils.book_append_sheet(wb, ws, 'Risk Analysis');\n\n    // Style headers (optional, basic bolding)\n    ws['!cols'] = [{ wch: 15 }, { wch: 30 }, { wch: 50 }]; // Set column widths\n    for (let i = 0; i < headers.length; i++) {\n        const cellRef = XLSX.utils.encode_cell({ r: 0, c: i });\n        ws[cellRef].s = { font: { bold: true } };\n    }\n\n    // Export to file\n    XLSX.writeFile(wb, 'risk_analysis.xlsx');\n}\n\n\n\n//GOOGLE DNS\n\nconst throttledEnrichGoogleDNS = throttleRequest(async function enrichGoogleDNS(domain, domainNodeId, isBulk = false, signal) {\n    if (!isBulk) network.setOptions({ physics: { enabled: false } });\n    try {\n        const baseUrl = `https://dns.google/resolve?name=${encodeURIComponent(domain)}&type=A`;\n        const url = constructUrl(baseUrl, false); // No API key needed for Google DNS\n        console.log(`Fetching A records for ${domain}: ${url}`); // Debug log\n        const response = await fetch(url, { signal });\n        if (!response.ok) {\n            const errorText = await response.text();\n            throw new Error(`Failed to fetch DNS data: ${response.status} ${response.statusText} - ${errorText}`);\n        }\n        const data = await response.json();\n        \n        if (data.Status !== 0) {\n            throw new Error(`Google DNS error: Status ${data.Status}${data.Comment ? ` (${data.Comment})` : ''}`);\n        }\n        \n        if (data.Answer) {\n            data.Answer.forEach(answer => {\n                if (answer.type === 1) { // Type 1 is A record (IPv4)\n                    const ip = answer.data;\n                    const existingIP = nodes.get({ filter: n => n.type === 'ip' && n.ip === ip })[0];\n                    const ipId = existingIP ? existingIP.id : nextId++;\n                    if (!existingIP) {\n                        nodes.add({\n                            id: ipId,\n                            type: 'ip',\n                            label: `IP: ${ip}`,\n                            title: `IP Address: ${ip}`,\n                            color: { background: '#f87171' },\n                            ip: ip,\n                            size: 20\n                        });\n                    }\n                    const edgeId = `${domainNodeId}-${ipId}-ResolvesTo`;\n                    if (!edges.get(edgeId)) {\n                        edges.add({ id: edgeId, from: domainNodeId, to: ipId, label: 'Resolves to' });\n                    }\n                }\n            });\n        } else {\n            if (!isBulk) showToast(`No A records found for ${domain}`, 'info');\n        }\n        \n        updateNodeSizes();\n        updateSelectOptions();\n        if (!isBulk) {\n            await stabilizeNetwork();\n            showToast(`Domain ${domain} enrichment completed using Google DNS`, 'success');\n        }\n    } catch (error) {\n        if (error.name === 'AbortError') {\n            showToast(`A record enrichment of domain ${domain} cancelled`, 'info');\n            return;\n        }\n        console.error(`Error enriching domain ${domain} with Google DNS: ${error.message}`);\n        showToast(`Error enriching domain ${domain} with Google DNS: ${error.message}`, 'error');\n        if (!isBulk) await stabilizeNetwork();\n    }\n}, RATE_LIMIT_MS);\n\n// Multiple Google DNS Node Enrichment\n\nconst throttledEnrichGoogleDNSMultiple = throttleRequest(async function enrichGoogleDNSMultiple(domains, nodeIds) {\n    if (!Array.isArray(domains) || !Array.isArray(nodeIds) || domains.length !== nodeIds.length) {\n        showToast('Invalid input for multiple domain enrichment', 'error');\n        return;\n    }\n\n    for (let i = 0; i < domains.length; i++) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('Google DNS enrichment stopped', 'info');\n            break;\n        }\n        await throttledEnrichGoogleDNS(domains[i], nodeIds[i], true); // isBulk = true\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    showToast(`Enriched ${domains.length} domains with Google DNS`, 'success');\n}, RATE_LIMIT_MS);\n\n\n\n// Google Bulk Enrichment\n\nasync function enrichAllGoogleDNS() {\n            network.setOptions({ physics: { enabled: false } });\n            const domainNodes = nodes.get({ filter: n => n.type === 'domain' });\n            for (const node of domainNodes) await throttledEnrichGoogleDNS(node.domain, node.id, true);\n            await stabilizeNetwork();\n            showToast('Bulk Google DNS enrichment completed', 'success');\n        }\n\n\n// GOOGLE MX and TXT Enrichment\n\n// Single MX Record Enrichment\nconst throttledEnrichGoogleDNSMX = throttleRequest(async function enrichGoogleDNSMX(domain, domainNodeId, isBulk = false, signal) {\n    if (!isBulk) network.setOptions({ physics: { enabled: false } });\n    try {\n        const baseUrl = `https://dns.google/resolve?name=${encodeURIComponent(domain)}&type=MX`;\n        const url = constructUrl(baseUrl);\n        console.log(`Fetching MX records for ${domain}: ${url}`); // Debug log\n        const response = await fetch(url, { signal });\n        if (!response.ok) {\n            const errorText = await response.text();\n            throw new Error(`Failed to fetch MX DNS data: ${response.status} ${response.statusText} - ${errorText}`);\n        }\n        const data = await response.json();\n        \n        if (data.Status !== 0) {\n            throw new Error(`Google DNS error: Status ${data.Status}${data.Comment ? ` (${data.Comment})` : ''}`);\n        }\n        \n        if (data.Answer) {\n            const existingMXNodes = new Map(nodes.get({ filter: n => n.type === 'mx' && n.hostname }).map(n => [n.hostname.toLowerCase(), n.id]));\n            data.Answer.forEach(answer => {\n                if (answer.type === 15) { // Type 15 is MX record\n                    const [priority, hostname] = answer.data.trim().split(' ');\n                    if (hostname && hostname !== '.') {\n                        let mxId = existingMXNodes.get(hostname.toLowerCase());\n                        if (!mxId) {\n                            mxId = nextId++;\n                            nodes.add({\n                                id: mxId,\n                                type: 'domain',\n                                label: `MX: ${hostname.length > 30 ? hostname.substring(0, 27) + '...' : hostname}`,\n                                title: `Mail Exchanger\\nHostname: ${hostname}\\nPriority: ${priority}`,\n                                color: { background: '#1e88e5' },\n                                hostname: hostname,\n                                size: 15\n                            });\n                            existingMXNodes.set(hostname.toLowerCase(), mxId);\n                        }\n                        const edgeId = `${domainNodeId}-${mxId}-MXFor`;\n                        if (!edges.get(edgeId)) {\n                            edges.add({ id: edgeId, from: domainNodeId, to: mxId, label: 'MX for' });\n                        }\n                    }\n                }\n            });\n        } else {\n            if (!isBulk) showToast(`No MX records found for ${domain}`, 'info');\n        }\n        \n        updateNodeSizes();\n        updateSelectOptions();\n        if (!isBulk) {\n            await stabilizeNetwork();\n            showToast(`Domain ${domain} MX enrichment completed using Google DNS`, 'success');\n        }\n    } catch (error) {\n        if (error.name === 'AbortError') {\n            showToast(`MX enrichment of domain ${domain} cancelled`, 'info');\n            return;\n        }\n        console.error(`Error enriching domain ${domain} MX with Google DNS: ${error.message}`);\n        showToast(`Error enriching domain ${domain} MX: ${error.message}`, 'error');\n        if (!isBulk) await stabilizeNetwork();\n    }\n}, RATE_LIMIT_MS);\n\n// Multiple MX Record Enrichment\nconst throttledEnrichGoogleDNSMXMultiple = throttleRequest(async function enrichGoogleDNSMXMultiple(domains, nodeIds) {\n    if (!Array.isArray(domains) || !Array.isArray(nodeIds) || domains.length !== nodeIds.length) {\n        showToast('Invalid input for multiple MX enrichment', 'error');\n        return;\n    }\n\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const totalDomains = domains.length;\n    let successfulEnrichments = 0;\n\n    for (let i = 0; i < totalDomains; i++) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('Google DNS MX enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = `Google DNS MX Enrichment: Stopped at ${successfulEnrichments}/${totalDomains} Domains`;\n            break;\n        }\n        await throttledEnrichGoogleDNSMX(domains[i], nodeIds[i], true);\n        successfulEnrichments++;\n        const progress = ((successfulEnrichments / totalDomains) * 100).toFixed(1);\n        document.getElementById('progress-bar').textContent = `Google DNS MX Enrichment: ${successfulEnrichments}/${totalDomains} Domains (${progress}%)`;\n        await new Promise(resolve => setTimeout(resolve, 200)); // Small delay\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    completeProgressBar();\n    showToast(`Enriched ${successfulEnrichments}/${totalDomains} domains with Google DNS MX records`, 'success');\n}, RATE_LIMIT_MS);\n\n// Bulk MX Enrichment\nasync function enrichAllGoogleDNSMX() {\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const domainNodes = nodes.get({ filter: n => n.type === 'domain' && n.domain });\n    const totalDomains = domainNodes.length;\n    let successfulEnrichments = 0;\n\n    if (totalDomains === 0) {\n        showToast('No domain nodes found to enrich', 'info');\n        completeProgressBar();\n        return;\n    }\n\n    const batchSize = 50;\n    const delayBetweenBatches = 200;\n    const totalBatches = Math.ceil(totalDomains / batchSize);\n    const estimatedTimeMs = (totalDomains * RATE_LIMIT_MS) + (totalBatches - 1) * delayBetweenBatches;\n    const estimatedSeconds = Math.ceil(estimatedTimeMs / 1000);\n    const timeEstimateStr = estimatedSeconds > 60 \n        ? `${Math.floor(estimatedSeconds / 60)}m ${estimatedSeconds % 60}s` \n        : `${estimatedSeconds}s`;\n\n    document.getElementById('progress-bar').textContent = `Google DNS MX Enrichment: 0/${totalDomains} Domains (0%) - Est. ${timeEstimateStr}`;\n\n    for (let i = 0; i < totalDomains; i += batchSize) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('Google DNS MX enrichment stopped', 'info');\n            break;\n        }\n\n        const batch = domainNodes.slice(i, Math.min(i + batchSize, totalDomains));\n        const batchPromises = batch.map(node => \n            throttledEnrichGoogleDNSMX(node.domain, node.id, true)\n                .then(() => successfulEnrichments++)\n                .catch(error => console.error(`Failed to enrich ${node.domain}: ${error.message}`))\n        );\n\n        await Promise.all(batchPromises);\n\n        const progress = ((successfulEnrichments / totalDomains) * 100).toFixed(1);\n        document.getElementById('progress-bar').textContent = \n            `Google DNS MX Enrichment: ${successfulEnrichments}/${totalDomains} Domains (${progress}%)`;\n        \n        await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    completeProgressBar();\n    showToast(`Google DNS MX enrichment completed: ${successfulEnrichments}/${totalDomains} domains enriched`, 'success');\n}\n\n// Single TXT Record Enrichment\n\nconst throttledEnrichGoogleDNSTXT = throttleRequest(async function enrichGoogleDNSTXT(domain, domainNodeId, isBulk = false, signal) {\n    if (!isBulk) network.setOptions({ physics: { enabled: false } });\n    try {\n        const baseUrl = `https://dns.google/resolve?name=${encodeURIComponent(domain)}&type=TXT`;\n        const url = constructUrl(baseUrl);\n        console.log(`Fetching TXT records for ${domain}: ${url}`);\n        const response = await fetch(url, { signal });\n        if (!response.ok) {\n            const errorText = await response.text();\n            throw new Error(`Failed to fetch TXT DNS data: ${response.status} ${response.statusText} - ${errorText}`);\n        }\n        const data = await response.json();\n        \n        if (data.Status !== 0) {\n            throw new Error(`Google DNS error: Status ${data.Status}${data.Comment ? ` (${data.Comment})` : ''}`);\n        }\n        \n        if (data.Answer) {\n            let foundTXT = false;\n            const existingTXTNodes = new Map(nodes.get({ filter: n => n.type === 'txt' && n.text }).map(n => [n.text, n.id]));\n            data.Answer.forEach(answer => {\n                if (answer.type === 16) { // Type 16 is TXT record\n                    foundTXT = true;\n                    const text = answer.data.replace(/^\"|\"$/g, '').trim();\n                    if (text) {\n                        let txtId = existingTXTNodes.get(text);\n                        if (!txtId) {\n                            txtId = nextId++;\n                            nodes.add({\n                                id: txtId,\n                                type: 'txt',\n                                label: `TXT: ${text.length > 30 ? text.substring(0, 27) + '...' : text}`,\n                                title: `TXT Record\\nValue: ${text}`,\n                                color: { background: '#f59e0b' },\n                                text: text,\n                                size: 15\n                            });\n                            existingTXTNodes.set(text, txtId);\n                        }\n                        const edgeId = `${domainNodeId}-${txtId}-TXTFor`;\n                        if (!edges.get(edgeId)) {\n                            edges.add({ id: edgeId, from: domainNodeId, to: txtId, label: 'TXT for' });\n                        }\n                    }\n                }\n            });\n            if (!foundTXT && !isBulk) {\n                showToast(`No TXT records found for ${domain}; received unexpected record types`, 'warning');\n            }\n        } else {\n            if (!isBulk) showToast(`No TXT records found for ${domain}`, 'info');\n        }\n        \n        updateNodeSizes();\n        updateSelectOptions();\n        if (!isBulk) {\n            await stabilizeNetwork();\n            showToast(`Domain ${domain} TXT enrichment completed using Google DNS`, 'success');\n        }\n    } catch (error) {\n        if (error.name === 'AbortError') {\n            showToast(`TXT enrichment of domain ${domain} cancelled`, 'info');\n            return;\n        }\n        console.error(`Error enriching domain ${domain} TXT with Google DNS: ${error.message}`);\n        showToast(`Error enriching domain ${domain} TXT: ${error.message}`, 'error');\n        if (!isBulk) await stabilizeNetwork();\n    }\n}, RATE_LIMIT_MS);\n\n// Multiple TXT Record Enrichment\nconst throttledEnrichGoogleDNSTXTMultiple = throttleRequest(async function enrichGoogleDNSTXTMultiple(domains, nodeIds) {\n    if (!Array.isArray(domains) || !Array.isArray(nodeIds) || domains.length !== nodeIds.length) {\n        showToast('Invalid input for multiple TXT enrichment', 'error');\n        return;\n    }\n\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const totalDomains = domains.length;\n    let successfulEnrichments = 0;\n\n    for (let i = 0; i < totalDomains; i++) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('Google DNS TXT enrichment stopped', 'info');\n            document.getElementById('progress-bar').textContent = `Google DNS TXT Enrichment: Stopped at ${successfulEnrichments}/${totalDomains} Domains`;\n            break;\n        }\n        await throttledEnrichGoogleDNSTXT(domains[i], nodeIds[i], true);\n        successfulEnrichments++;\n        const progress = ((successfulEnrichments / totalDomains) * 100).toFixed(1);\n        document.getElementById('progress-bar').textContent = `Google DNS TXT Enrichment: ${successfulEnrichments}/${totalDomains} Domains (${progress}%)`;\n        await new Promise(resolve => setTimeout(resolve, 200)); // Small delay\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    completeProgressBar();\n    showToast(`Enriched ${successfulEnrichments}/${totalDomains} domains with Google DNS TXT records`, 'success');\n}, RATE_LIMIT_MS);\n\n// Bulk TXT Enrichment\nasync function enrichAllGoogleDNSTXT() {\n    showProgressBar();\n    network.setOptions({ physics: { enabled: false } });\n    const domainNodes = nodes.get({ filter: n => n.type === 'domain' && n.domain });\n    const totalDomains = domainNodes.length;\n    let successfulEnrichments = 0;\n\n    if (totalDomains === 0) {\n        showToast('No domain nodes found to enrich', 'info');\n        completeProgressBar();\n        return;\n    }\n\n    const batchSize = 50;\n    const delayBetweenBatches = 200;\n    const totalBatches = Math.ceil(totalDomains / batchSize);\n    const estimatedTimeMs = (totalDomains * RATE_LIMIT_MS) + (totalBatches - 1) * delayBetweenBatches;\n    const estimatedSeconds = Math.ceil(estimatedTimeMs / 1000);\n    const timeEstimateStr = estimatedSeconds > 60 \n        ? `${Math.floor(estimatedSeconds / 60)}m ${estimatedSeconds % 60}s` \n        : `${estimatedSeconds}s`;\n\n    document.getElementById('progress-bar').textContent = `Google DNS TXT Enrichment: 0/${totalDomains} Domains (0%) - Est. ${timeEstimateStr}`;\n\n    for (let i = 0; i < totalDomains; i += batchSize) {\n        if (activeTaskController && activeTaskController.signal.aborted) {\n            showToast('Google DNS TXT enrichment stopped', 'info');\n            break;\n        }\n\n        const batch = domainNodes.slice(i, Math.min(i + batchSize, totalDomains));\n        const batchPromises = batch.map(node => \n            throttledEnrichGoogleDNSTXT(node.domain, node.id, true)\n                .then(() => successfulEnrichments++)\n                .catch(error => console.error(`Failed to enrich ${node.domain}: ${error.message}`))\n        );\n\n        await Promise.all(batchPromises);\n\n        const progress = ((successfulEnrichments / totalDomains) * 100).toFixed(1);\n        document.getElementById('progress-bar').textContent = \n            `Google DNS TXT Enrichment: ${successfulEnrichments}/${totalDomains} Domains (${progress}%)`;\n        \n        await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));\n    }\n\n    updateNodeSizes();\n    updateSelectOptions();\n    await stabilizeNetwork();\n    completeProgressBar();\n    showToast(`Google DNS TXT enrichment completed: ${successfulEnrichments}/${totalDomains} domains enriched`, 'success');\n}\n\n// Add this at the end of the script section\nwindow.addEventListener('beforeunload', function() {\n    try {\n        saveState();\n    } catch (e) {\n        console.error('Before unload save failed:', e);\n    }\n});\n\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "functions.md",
    "content": "# Functions in experimental_mapper.html\n\nThis document lists the JavaScript functions found in `experimental_mapper.html`.\n\n| Function Name                      | Description                                                                 |\n| :--------------------------------- | :-------------------------------------------------------------------------- |\n| `updateTheme()`                    | Toggles between light and dark mode themes and updates the network style.   |\n| `searchGraph()`                    | Searches the network graph for nodes matching the input term.               |\n| `resetNodeHighlights()`            | Resets highlights applied to nodes during a search.                         |\n| `exportToPNG()`                    | Exports the current network view to a PNG image file.                       |\n| `filterIpAndDomains()`             | Hides all nodes except those of type 'ip' or 'domain'.                      |\n| `showAllNodes()`                   | Makes all nodes and edges visible in the graph.                             |\n| `getNodeColorByType(type)`         | Returns the background color for a node based on its type.                  |\n| `enrichAllIpinfo()`                | Performs bulk enrichment of all IP nodes using the IPinfo API.              |\n| `processBatch(batch)`              | (Internal helper for `enrichAllIpinfo`) Processes a batch of IP nodes for enrichment. |\n| `exportToPDF()`                    | Exports the current network view to a PDF document.                         |\n| `saveStateAfterOperation()`        | Saves the current graph state to local storage and shows a toast message.   |\n| `window.onload = function()`       | Initialization function called when the page loads.                         |\n| `loadState()`                      | Loads the saved graph state from local storage.                             |\n| `handleWheel(event)`               | Handles mouse wheel events for zooming (currently commented out).           |\n| `throttleRequest(fn)`              | Creates a throttled version of a function to limit request rates.           |\n| `ensureInteractionSettings()`      | Ensures the correct interaction settings (dragging, zooming) are applied.   |\n| `stabilizeNetwork(skipFit)`        | Stabilizes the network physics and optionally fits the view.                |\n| `finishStabilization(resolve, skipFit)` | (Internal helper for `stabilizeNetwork`) Completes the stabilization process. |\n| `showEdgeContextMenu(x, y, edgeId)` | Displays a context menu for a specific edge.                                |\n| `hideEdgeContextMenu()`            | Hides the edge context menu.                                                |\n| `editEdgeLabel(edgeId)`            | Allows editing the label of a specific edge via a prompt.                   |\n| `removeEdgeDirect(edgeId)`         | Removes a specific edge directly from the graph.                            |\n| `showContextMenu(x, y, value, nodeIds, type)` | Displays a context menu for one or more selected nodes.          |\n| `hideContextMenu()`                | Hides the node context menu.                                                |\n| `saveState()`                      | Saves the current state of the graph (nodes, edges, settings) to local storage. |\n| `searchShodanHtmlHash(htmlHashNodeId, htmlHash, signal)` | (Throttled) Searches Shodan for IPs matching a specific HTML hash. |\n| `enrichIP(ip, nodeId)`             | (Throttled) Enriches a single IP node using IPinfo.                         |\n| `enrichIPMultiple(ips, nodeIds, signal)` | (Throttled) Enriches multiple IP nodes using IPinfo.                      |\n| `enrichShodan(value, nodeId)`      | (Throttled) Enriches a single IP or domain using the Shodan API.            |\n| `enrichShodanMultiple(values, nodeIds, signal)` | (Throttled) Enriches multiple IPs or domains using Shodan.       |\n| `enrichInternetDB(ip, nodeId)`     | (Throttled) Enriches a single IP using the InternetDB API.                  |\n| `enrichInternetDBMultiple(ips, nodeIds, signal)` | (Throttled) Enriches multiple IPs using InternetDB.             |\n| `enrichGoogleDNS(domain, nodeId)`  | (Throttled) Enriches a single domain using Google DNS (A records).          |\n| `enrichGoogleDNSMultiple(domains, nodeIds, signal)` | (Throttled) Enriches multiple domains using Google DNS (A records). |\n| `enrichGoogleDNSMX(domain, nodeId)`| (Throttled) Enriches a single domain using Google DNS (MX records).         |\n| `enrichGoogleDNSMXMultiple(domains, nodeIds, signal)` | (Throttled) Enriches multiple domains using Google DNS (MX records). |\n| `enrichGoogleDNSTXT(domain, nodeId)`| (Throttled) Enriches a single domain using Google DNS (TXT records).        |\n| `enrichGoogleDNSTXTMultiple(domains, nodeIds, signal)` | (Throttled) Enriches multiple domains using Google DNS (TXT records). |\n| `enrichHudsonRock(email, nodeId)`  | (Throttled) Enriches a single email address using Hudson Rock API.          |\n| `enrichHudsonRockMultiple(emails, nodeIds, signal)` | (Throttled) Enriches multiple email addresses using Hudson Rock. |\n| `enrichHudsonRockDomain(domain, nodeId)` | (Throttled) Enriches a single domain using Hudson Rock API.             |\n| `enrichHudsonRockDomainMultiple(domains, nodeIds, signal)` | (Throttled) Enriches multiple domains using Hudson Rock. |\n| `enrichGreyNoise(ip, nodeId)`      | (Throttled) Enriches a single IP using the GreyNoise API.                   |\n| `enrichGreyNoiseMultiple(ips, nodeIds, signal)` | (Throttled) Enriches multiple IPs using GreyNoise.              |\n| `sendHttpsRequest(value, type, protocol, nodeId)` | (Throttled) Sends an HTTP(S) request to an IP or domain.       |\n| `sendHttpsRequestMultiple(values, type, protocol, signal)` | (Throttled) Sends HTTP(S) requests to multiple IPs or domains. |\n| `enrichURLscan(url, nodeId)`       | (Throttled) Enriches a URL/IP/Domain using the URLscan.io API.              |\n| `enrichSecurityTrailsDomain(domain, nodeId)` | (Throttled) Enriches a domain for subdomains using SecurityTrails. |\n| `enrichSecurityTrailsDomainMultiple(domains, nodeIds, signal)` | (Throttled) Enriches multiple domains using SecurityTrails. |\n| `enrichURLhaus(url, nodeId)`       | (Throttled) Enriches a URL using the URLhaus API.                           |\n| `enrichURLhausMultiple(urls, nodeIds, signal)` | (Throttled) Enriches multiple URLs using URLhaus.               |\n| `startLinkCreation(nodeId)`        | Initiates the process of creating a link starting from a specific node.     |\n| `deleteNodes(nodeIds)`             | Deletes one or more specified nodes and their connected edges.              |\n| `hidePropertiesPanel()`            | Hides the node properties panel.                                            |\n| `showPropertiesPanel(nodeId)`      | Shows the properties panel for a specific node.                             |\n| `editNodeNotes(nodeId)`            | Opens the modal to add or edit notes for a specific node.                   |\n| `saveNodeNotes()`                  | Saves the notes entered in the notes modal to the selected node.            |\n| `hideNotesModal()`                 | Hides the node notes editing modal.                                         |\n| `showToast(message, type, duration)` | Displays a temporary notification message (toast).                        |\n| `toggleMenu()`                     | Toggles the visibility (collapse/expand) of the controls panel.             |\n| `toggleMode()`                     | Switches between light and dark UI modes.                                   |\n| `togglePhysics()`                  | Pauses or resumes the physics simulation of the network graph.              |\n| `resetLayout()`                    | Resets the network layout using the default physics settings.               |\n| `showTab(tabId)`                   | Switches the visible tab in the controls panel.                             |\n| `updateAddForm()`                  | Updates the visibility of input fields in the 'Add Entity' form based on type. |\n| `updateEditFormVisibility()`       | Updates the visibility of input fields in the 'Edit Entity' form based on type. |\n| `addNode()`                        | Adds a new node (entity) to the graph based on the 'Add Entity' form.       |\n| `loadNodeForEdit()`                | Loads the data of the selected node into the 'Edit Entity' form.            |\n| `editNode()`                       | Saves the changes made to a node in the 'Edit Entity' form.                 |\n| `removeNode()`                     | Removes the selected node from the 'Remove Entity' dropdown list.           |\n| `addEdge()`                        | Adds a new edge (link) between two selected nodes.                          |\n| `removeEdge()`                     | Removes the selected edge from the 'Remove Link' dropdown list.             |\n| `updateSelectOptions()`            | Updates the options in all node selection dropdown menus.                   |\n| `updateEdgeSelectOptions()`        | Updates the options in the edge removal dropdown menu.                      |\n| `exportGraph()`                    | Exports the entire graph (all nodes and edges) to a JSON file.              |\n| `exportVisibleGraph()`             | Exports only the currently visible nodes and edges to a JSON file.          |\n| `importGraph()`                    | Imports a graph from a selected JSON file.                                  |\n| `clearGraph()`                     | Removes all nodes and edges from the graph.                                 |\n| `showGraphSummary()`               | Displays a modal summarizing the counts of different node types.            |\n| `hideGraphSummary()`               | Hides the graph summary modal.                                              |\n| `saveApiKey(keyName, inputId, storeCheckboxId)` | Saves an API key, optionally storing it in local storage.       |\n| `saveIpinfoApiKey()`               | Saves the IPinfo API key.                                                   |\n| `saveShodanApiKey()`               | Saves the Shodan API key.                                                   |\n| `saveGreynoiseApiKey()`            | Saves the GreyNoise API key.                                                |\n| `saveUrlscanApiKey()`              | Saves the URLscan.io API key.                                               |\n| `saveSecuritytrailsApiKey()`       | Saves the SecurityTrails API key.                                           |\n| `saveUrlhausApiKey()`              | Saves the URLhaus API key.                                                  |\n| `saveCorsProxyUrl()`               | Saves the CORS proxy URL and related settings.                              |\n| `initializeApiKeys()`              | Loads API keys and settings from local storage on startup.                  |\n| `runAllTests()`                    | Runs test functions for all configured enrichment APIs.                     |\n| `testIpinfo()`                     | Tests the IPinfo API connection and key.                                    |\n| `testShodan()`                     | Tests the Shodan API connection and key.                                    |\n| `testInternetDB()`                 | Tests the InternetDB API connection.                                        |\n| `testGoogleDNS()`                  | Tests the Google DNS API connection.                                        |\n| `testHudsonRockEmail()`            | Tests the Hudson Rock Email API connection.                                 |\n| `testHudsonRockDomain()`           | Tests the Hudson Rock Domain API connection.                                |\n| `testGreyNoise()`                  | Tests the GreyNoise API connection and key.                                 |\n| `testURLscan()`                    | Tests the URLscan.io API connection and key.                                |\n| `testSecurityTrails()`             | Tests the SecurityTrails API connection and key.                            |\n| `testURLhaus()`                    | Tests the URLhaus API connection.                                           |\n| `constructUrl(baseUrl, useApiKey)` | Constructs the final URL for an API request, optionally using the proxy.    |\n| `enrichAllShodan()`                | Performs bulk enrichment of all IPs/Domains using the Shodan API.           |\n| `enrichAllInternetDB()`            | Performs bulk enrichment of all IPs using the InternetDB API.               |\n| `enrichAllGoogleDNS()`             | Performs bulk enrichment of all domains using Google DNS (A records).       |\n| `enrichAllGoogleDNSMX()`           | Performs bulk enrichment of all domains using Google DNS (MX records).      |\n| `enrichAllGoogleDNSTXT()`          | Performs bulk enrichment of all domains using Google DNS (TXT records).     |\n| `enrichAllHudsonRockEmails()`      | Performs bulk enrichment of all emails using the Hudson Rock API.           |\n| `enrichAllHudsonRockDomains()`     | Performs bulk enrichment of all domains using the Hudson Rock API.          |\n| `enrichAllGreyNoise()`             | Performs bulk enrichment of all IPs using the GreyNoise API.                |\n| `enrichAllURLscan()`               | Performs bulk enrichment of all URLs/Domains/IPs using URLscan.io.          |\n| `enrichAllSecurityTrails()`        | Performs bulk enrichment of all domains using SecurityTrails.               |\n| `enrichAllURLhaus()`               | Performs bulk enrichment of all URLs using URLhaus.                         |\n| `importIOCsFromText()`             | Imports IOCs (IPs, domains, emails, hashes) from the text area.             |\n| `importIOCsFromFile()`             | Imports IOCs from a selected text file.                                     |\n| `processIOCs(text)`                | Processes a block of text to extract IOCs and add them to the graph.        |\n| `getOrCreateNode(value, type, properties)` | Gets an existing node or creates a new one if it doesn't exist.      |\n| `addNodeToGraph(nodeData)`         | Adds a node object to the graph's dataset.                                  |\n| `addEdgeToGraph(fromId, toId, label)` | Adds an edge object to the graph's dataset.                             |\n| `setOrganicLayout()`               | Applies an organic (force-directed) layout to the graph.                    |\n| `setCircularLayout()`              | Applies a circular layout to the graph.                                     |\n| `setOrthogonalLayout()`            | Applies an orthogonal layout (experimental, may require tuning).            |\n| `setTreeLayout()`                  | Applies a tree layout (experimental, may require tuning).                   |\n| `setHierarchicalLayout()`          | Applies a hierarchical layout to the graph.                                 |\n| `setLayoutOptions(layoutOptions)`  | Applies specific layout options to the network.                             |\n| `toggleNodeLabels()`               | Toggles the visibility of node labels.                                      |\n| `toggleEdgeLabels()`               | Toggles the visibility of edge labels.                                      |\n| `updateLabelVisibility()`          | Updates the font size for nodes and edges based on visibility settings.     |\n| `toggleIsolatedNodes()`            | Toggles the visibility of nodes that have no connections (edges).           |\n| `updateNodeSizes(type)`            | Adjusts node sizes based on the number of incoming/outgoing/total links.    |\n| `showProgressBar()`                | Shows the progress bar for long-running tasks.                              |\n| `completeProgressBar()`            | Hides the progress bar and indicates completion.                            |\n| `stopActiveTask()`                 | Attempts to cancel the currently running asynchronous task (e.g., enrichment).|\n| `exportConfigBackup()`             | Exports API keys and settings to a JSON backup file.                        |\n| `importConfig()`                   | Imports API keys and settings from a JSON backup file.                      |\n| `importNMAP()`                     | Imports data from an NMAP XML scan results file.                            |\n| `parseNmapXml(xmlString)`          | Parses NMAP XML data and adds corresponding nodes/edges to the graph.       |\n| `riskAnalysis()`                   | Performs a risk analysis based on node types and displays results in a modal. |\n| `printRiskTable()`                 | Generates a printable version of the risk analysis table.                   | "
  },
  {
    "path": "header_analysis.html",
    "content": "<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Email Analyser</title>\n    <style>\n        * * {\n    margin: 0;\n    padding: 0;\n    box-sizing: border-box;\n    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\n}\n\nbody {\n    font-size: 12px;\n    display: flex;\n    min-height: 100vh;\n    background-color: #f0f2f5;\n}\n\n.sidebar-container {\n    display: flex;\n    position: relative;\n}\n\n.sidebar {\n    width: 300px;\n    background-color: #ffffff;\n    padding: 20px;\n    box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);\n    display: flex;\n    flex-direction: column;\n    gap: 15px;\n    min-width: 200px;\n    max-width: 500px;\n}\n\n.sidebar h2 {\n    color: #333;\n    font-size: 1.3em;\n    margin-bottom: 10px;\n}\n\n.sidebar textarea {\n    width: 100%;\n    height: 200px;\n    padding: 10px;\n    border: 1px solid #ddd;\n    border-radius: 5px;\n    resize: none;\n    font-size: 14px;\n    background-color: #fafafa;\n}\n\n.sidebar textarea:focus {\n    outline: none;\n    border-color: #007bff;\n    box-shadow: 0 0 5px rgba(0, 123, 255, 0.3);\n}\n\n.sidebar button {\n    padding: 10px;\n    border: none;\n    border-radius: 5px;\n    font-size: 14px;\n    cursor: pointer;\n    transition: background-color 0.3s;\n}\n\n.sidebar button#analyseBtn {\n    background-color: #007bff;\n    color: white;\n}\n\n.sidebar button#analyseBtn:hover {\n    background-color: #0056b3;\n}\n\n.sidebar button#clearBtn {\n    background-color: #dc3545;\n    color: white;\n}\n\n.sidebar button#clearBtn:hover {\n    background-color: #b02a37;\n}\n\n.resize-handle {\n    width: 5px;\n    background-color: #ddd;\n    cursor: col-resize;\n    transition: background-color 0.2s;\n}\n\n.resize-handle:hover {\n    background-color: #bbb;\n}\n\n.main {\n    flex: 1;\n    padding: 20px;\n    overflow-y: auto;\n}\n\n.main h2, .main h3 {\n    color: #333;\n    margin-bottom: 20px;\n}\n\ntable {\n    width: 100%;\n    border-collapse: collapse;\n    background-color: #ffffff;\n    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);\n    margin-bottom: 30px;\n}\n\nth, td {\n    padding: 12px;\n    font-size: 13px;\n    text-align: left;\n    border-bottom: 1px solid #ddd;\n}\n\nth {\n    background-color: #007bff;\n    color: white;\n    cursor: pointer;\n    position: relative;\n    user-select: none;\n}\n\nth:hover {\n    background-color: #0056b3;\n}\n\nth.sort-asc::after {\n    content: '↑';\n    position: absolute;\n    right: 8px;\n}\n\nth.sort-desc::after {\n    content: '↓';\n    position: absolute;\n    right: 8px;\n}\n\ntr:hover {\n    background-color: #f8f9fa;\n}\n\n.body-section {\n    background-color: #ffffff;\n    padding: 20px;\n    border-radius: 5px;\n    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);\n    margin-bottom: 30px;\n}\n\n.body-section pre {\n    white-space: pre-wrap;\n    word-wrap: break-word;\n    margin: 0;\n    font-size: 14px;\n}\n\n.body-section .html-content {\n    border: 1px solid #ddd;\n    padding: 10px;\n    border-radius: 5px;\n}\n\n.body-content-container {\n    display: none;\n    margin-top: 10px;\n}\n\n.body-content-container.expanded {\n    display: block;\n}\n\n.attachments-section {\n    background-color: #ffffff;\n    padding: 20px;\n    border-radius: 5px;\n    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);\n    margin-bottom: 30px;\n}\n\n.attachments-section h3 {\n    color: #333;\n    margin-bottom: 15px;\n}\n\n.attachments-section table {\n    margin-bottom: 0;\n}\n\n.attachments-section .download-btn {\n    display: inline-block;\n    padding: 8px 12px;\n    background-color: #007bff;\n    color: white;\n    text-decoration: none;\n    border-radius: 5px;\n    font-size: 13px;\n    transition: background-color 0.3s;\n}\n\n.attachments-section .download-btn:hover {\n    background-color: #0056b3;\n}\n\n.ioc-section {\n    background-color: #ffffff;\n    padding: 20px;\n    border-radius: 5px;\n    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);\n}\n\n.ioc-section table {\n    margin-bottom: 0;\n}\n\n.spf-section, .dkim-section, .dmarc-section {\n    background-color: #ffffff;\n    padding: 20px;\n    border-radius: 5px;\n    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);\n    margin-bottom: 20px;\n}\n\n.spf-section h3, .dkim-section h3, .dmarc-section h3 {\n    color: #333;\n    margin-bottom: 15px;\n}\n\n.spf-section table, .dkim-section table, .dmarc-section table {\n    margin-bottom: 0;\n}\n\n.spf-status, .dkim-status, .dmarc-status {\n    padding: 10px;\n    margin-bottom: 10px;\n    border-radius: 5px;\n    color: white;\n    font-weight: bold;\n    text-align: center;\n    display: inline-block;\n    width: calc(100% - 30px);\n}\n\n.spf-pass, .dkim-pass, .dmarc-pass {\n    background-color: #28a745;\n}\n\n.spf-fail, .dkim-fail, .dmarc-fail {\n    background-color: #dc3545;\n}\n\n.bcl-section {\n    background-color: #ffffff;\n    padding: 20px;\n    border-radius: 5px;\n    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);\n    margin-bottom: 20px;\n}\n\n.bcl-section h3 {\n    color: #333;\n    margin-bottom: 15px;\n}\n\n.bcl-status {\n    padding: 10px;\n    margin-bottom: 10px;\n    border-radius: 5px;\n    color: white;\n    font-weight: bold;\n    text-align: center;\n    display: inline-block;\n    width: calc(100% - 30px);\n}\n\n.bcl-low {\n    background-color: #28a745;\n}\n\n.bcl-medium {\n    background-color: #ffc107;\n}\n\n.bcl-high, .bcl-na {\n    background-color: #dc3545;\n}\n\n.bcl-details-container {\n    display: none;\n    margin-top: 10px;\n}\n\n.bcl-details-container.expanded {\n    display: block;\n}\n\n.forefront-section {\n    background-color: #ffffff;\n    padding: 20px;\n    border-radius: 5px;\n    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);\n    margin-bottom: 20px;\n}\n\n.forefront-section h3 {\n    color: #333;\n    margin-bottom: 15px;\n}\n\n.forefront-section table {\n    margin-bottom: 0;\n}\n\n.forefront-status {\n    padding: 10px;\n    margin-bottom: 10px;\n    border-radius: 5px;\n    color: white;\n    font-weight: bold;\n    text-align: center;\n    display: inline-block;\n    width: calc(100% - 30px);\n    background-color: #17a2b8;\n}\n\n.forefront-status.na {\n    background-color: #dc3545;\n}\n\n.header-section {\n    overflow-x: auto;\n    background-color: #ffffff;\n    padding: 20px;\n    border-radius: 5px;\n    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);\n    margin-bottom: 20px;\n}\n\n.header-section table {\n    width: 100%;\n    max-width: 100%;\n    table-layout: auto;\n    margin-bottom: 0;\n}\n\n.spf-details, .dkim-details, .dmarc-details, .forefront-details, .bcl-details {\n    white-space: pre-wrap;\n    word-wrap: break-word;\n}\n\n.spf-details-container, .dkim-details-container, .dmarc-details-container, .forefront-details-container {\n    display: none;\n    margin-top: 10px;\n}\n\n.spf-details-container.expanded, .dkim-details-container.expanded, .dmarc-details-container.expanded, .forefront-details-container.expanded {\n    display: block;\n}\n\n.toggle-spf-arrow, .toggle-dkim-arrow, .toggle-dmarc-arrow, .toggle-forefront-arrow, .toggle-header-arrow, .toggle-body-arrow, .toggle-bcl-arrow {\n    cursor: pointer;\n    font-size: 14px;\n    margin-left: 10px;\n    vertical-align: middle;\n    color: #333;\n    transition: color 0.3s;\n}\n\n.toggle-spf-arrow:hover, .toggle-dkim-arrow:hover, .toggle-dmarc-arrow:hover, .toggle-forefront-arrow:hover, .toggle-header-arrow:hover, .toggle-body-arrow:hover, .toggle-bcl-arrow:hover {\n    color: #007bff;\n}\n\n.spf-status-container, .dkim-status-container, .dmarc-status-container, .forefront-status-container, .header-table-container, .body-content-toggle, .bcl-status-container {\n    display: flex;\n    align-items: center;\n    margin-bottom: 10px;\n}\n\n.header-table-toggle {\n    text-align: center;\n    margin-bottom: 10px;\n}\n\n/* Adjust column widths for header table */\n.header-section th:nth-child(1), .header-section td:nth-child(1) { /* Order */\n    width: 10%;\n    min-width: 50px;\n}\n.header-section th:nth-child(2), .header-section td:nth-child(2) { /* Action */\n    width: 12%;\n    min-width: 80px;\n    word-wrap: break-word;\n}\n.header-section th:nth-child(3), .header-section td:nth-child(3) { /* Timestamp */\n    width: 16%;\n    min-width: 120px;\n    word-wrap: break-word;\n}\n.header-section th:nth-child(4), .header-section td:nth-child(4) { /* From Server */\n    width: 15%;\n    min-width: 100px;\n    word-wrap: break-word;\n}\n.header-section th:nth-child(5), .header-section td:nth-child(5) { /* By Server */\n    width: 15%;\n    min-width: 100px;\n    word-wrap: break-word;\n}\n.header-section th:nth-child(6), .header-section td:nth-child(6) { /* IP */\n    width: 12%;\n    min-width: 80px;\n    word-wrap: break-word;\n}\n.header-section th:nth-child(7), .header-section td:nth-child(7) { /* Email/Domain */\n    width: 15%;\n    min-width: 100px;\n    word-wrap: break-word;\n}\n.header-section th:nth-child(8), .header-section td:nth-child(8) { /* Details */\n    width: 15%;\n    min-width: 100px;\n    word-wrap: break-word;\n}\n\n/* Improve text wrapping in header table cells */\n.header-section td, .header-section th {\n    word-break: break-word;\n    overflow-wrap: break-word;\n    padding: 8px;\n}\n\n/* Adjust column widths for IOC, SPF, DKIM, DMARC, Forefront, and Attachments tables */\n.ioc-section th:nth-child(1), .ioc-section td:nth-child(1),\n.spf-section th:nth-child(1), .spf-section td:nth-child(1),\n.dkim-section th:nth-child(1), .dkim-section td:nth-child(1),\n.dmarc-section th:nth-child(1), .dmarc-section td:nth-child(1),\n.forefront-section th:nth-child(1), .forefront-section td:nth-child(1),\n.attachments-section th:nth-child(1), .attachments-section td:nth-child(1) { /* Type/Field/Name */\n    width: 40%;\n}\n.ioc-section th:nth-child(2), .ioc-section td:nth-child(2),\n.spf-section th:nth-child(2), .spf-section td:nth-child(2),\n.dkim-section th:nth-child(2), .dkim-section td:nth-child(2),\n.dmarc-section th:nth-child(2), .dmarc-section td:nth-child(2),\n.forefront-section th:nth-child(2), .forefront-section td:nth-child(2),\n.attachments-section th:nth-child(2), .attachments-section td:nth-child(2) { /* Value/Type */\n    width: 30%;\n}\n.attachments-section th:nth-child(3), .attachments-section td:nth-child(3) { /* Download */\n    width: 30%;\n}\n\n@media (max-width: 768px) {\n    body {\n        flex-direction: column;\n    }\n\n    .sidebar-container {\n        width: 100%;\n    }\n\n    .sidebar {\n        width: 100%;\n        max-width: none;\n        min-width: 0;\n    }\n\n    .resize-handle {\n        display: none;\n    }\n\n    .main {\n        width: 100%;\n    }\n\n    table, thead, tbody, th, td, tr {\n        display: block;\n    }\n    th, td {\n        width: 100% !important;\n        min-width: 0;\n        box-sizing: border-box;\n    }\n    th::before {\n        content: attr(data-label);\n        font-weight: normal;\n        display: inline-block;\n        width: 40%;\n    }\n    td::before {\n        content: attr(data-label);\n        font-weight: normal;\n        display: inline-block;\n        width: 40%;\n    }\n\n    .spf-status, .dkim-status, .dmarc-status, .forefront-status, .bcl-status {\n        width: calc(100% - 20px);\n    }\n\n    .header-section table,\n    .header-section thead,\n    .header-section tbody,\n    .header-section th,\n    .header-section td,\n    .header-section tr {\n        display: block;\n    }\n    .header-section th,\n    .header-section td {\n        width: 100% !important;\n        min-width: 0;\n    }\n}\n\n.auth-results-section {\n    background-color: #ffffff;\n    padding: 20px;\n    border-radius: 5px;\n    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);\n    margin-bottom: 20px;\n}\n\n.auth-results-section h3 {\n    color: #333;\n    margin-bottom: 15px;\n}\n\n.auth-results-section table {\n    margin-bottom: 0;\n}\n\n.auth-results-status {\n    padding: 10px;\n    margin-bottom: 10px;\n    border-radius: 5px;\n    color: white;\n    font-weight: bold;\n    text-align: center;\n    display: inline-block;\n    width: calc(100% - 30px);\n}\n\n.auth-results-pass {\n    background-color: #28a745;\n}\n\n.auth-results-fail {\n    background-color: #dc3545;\n}\n\n.auth-results-details-container {\n    display: none;\n    margin-top: 10px;\n}\n\n.auth-results-details-container.expanded {\n    display: block;\n}\n\n.toggle-auth-results-arrow {\n    cursor: pointer;\n    font-size: 14px;\n    margin-left: 10px;\n    vertical-align: middle;\n    color: #333;\n    transition: color 0.3s;\n}\n\n.toggle-auth-results-arrow:hover {\n    color: #007bff;\n}\n\n.auth-results-status-container {\n    display: flex;\n    align-items: center;\n    margin-bottom: 10px;\n}\n\n.auth-results-details {\n    white-space: pre-wrap;\n    word-wrap: break-word;\n}\n        \n    </style>\n</head>\n<body>\n    <div class=\"sidebar-container\">\n        <div class=\"sidebar\">\n            <h2>Email Analyser</h2>\n            <textarea id=\"headerInput\" placeholder=\"Paste email headers and body here...\"></textarea>\n            <button id=\"analyseBtn\">Analyse</button>\n            <button id=\"clearBtn\">Clear</button>\n            <footer style=\"text-align: center; font-size: 10px; color: #6b7280; padding-top: 10px;\">\n                <a href=\"https://www.buymeacoffee.com/mrr3b00t\" target=\"_blank\">\n                    <img src=\"https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png\" alt=\"Buy Me A Coffee\" style=\"height: 60px !important;width: 217px !important;\">\n                </a>\n                <p>Copyright © Xservus Limited</p>\n                <p>Experimental - Validate all results manaually and/or with another tool</p>\n                <p>Version 0.41</p>\n                <a href=\"https://www.pwndefend.com\">https://www.pwdefend.com</a>\n            </footer>\n        </div>\n        <div class=\"resize-handle\"></div>\n    </div>\n    <div class=\"main\">\n        <h2>Email Details</h2>\n        <div id=\"emailDetails\"></div>\n        <h2>Header Analysis</h2>\n        <div id=\"headerResults\"></div>\n        <h3>Body Content</h3>\n        <div id=\"bodyResults\" class=\"body-section\"></div>\n        <h3>Attachments</h3>\n        <div id=\"attachmentResults\" class=\"attachments-section\"></div>\n        <h2>IOCs</h2>\n        <div id=\"iocResults\" class=\"ioc-section\"></div>\n    </div>\n\n    <script>\n\n    var gk_fileData = {};\n    function loadFileData(filename) {\n      return gk_fileData[filename] || \"\";\n    }\n        // Sidebar resizing logic\n        const sidebar = document.querySelector('.sidebar');\n        const resizeHandle = document.querySelector('.resize-handle');\n        let isResizing = false;\n\n        if (resizeHandle && sidebar) {\n            resizeHandle.addEventListener('mousedown', (e) => {\n                isResizing = true;\n                document.body.style.userSelect = 'none';\n            });\n\n            document.addEventListener('mousemove', (e) => {\n                if (!isResizing) return;\n                const newWidth = e.clientX - sidebar.getBoundingClientRect().left;\n                if (newWidth >= 200 && newWidth <= 500) {\n                    sidebar.style.width = `${newWidth}px`;\n                }\n            });\n\n            document.addEventListener('mouseup', () => {\n                if (isResizing) {\n                    isResizing = false;\n                    document.body.style.userSelect = '';\n                }\n            });\n        }\n\n        // Analysis and sorting logic\n        const analyseBtn = document.getElementById('analyseBtn');\n        const clearBtn = document.getElementById('clearBtn');\n        if (analyseBtn) analyseBtn.addEventListener('click', analyseEmail);\n        if (clearBtn) clearBtn.addEventListener('click', clearResults);\n\n        // Global state\n        let sortState = { column: 'order', direction: 'asc' };\n        let transactions = [];\n        let headerFields = {};\n        let isHeaderTableExpanded = false;\n        let isBodyContentExpanded = false;\n        let isBclDetailsExpanded = false;\n        let lastSpfResult = {\n            status: 'N/A',\n            fields: {\n                Result: '', Receiver: '', ClientIP: '', EnvelopeFrom: '',\n                HELO: '', Mechanism: '', PriorityProtocol: ''\n            },\n            details: ''\n        };\n        let lastBclResult = { status: 'N/A', value: '', message: '', details: '' };\n        let lastForefrontResult = {\n            status: 'N/A',\n            fields: {\n                CIP: '', CTRY: '', LANG: '', SCL: '', SRV: '', IPV: '',\n                SFV: '', H: '', PTR: '', CAT: '', SFS: '', DIR: ''\n            },\n            details: ''\n        };\n        let lastBodyContent = '';\n        let attachments = [];\n        let lastDkimResult = {\n        status: 'N/A',\n        fields: {\n            Result: '', Selector: '', Domain: '', SigningAlgorithm: '',\n            Canonicalization: '', SignatureValue: ''\n        },\n        details: ''\n    };\n\n    let lastAuthResults = {\n    status: 'N/A',\n    fields: {\n        SPFResult: '', DKIMResult: '', DMARCResult: '', Server: '', AuthDetails: ''\n    },\n    details: ''\n};\n\n    let lastDmarcResult = {\n        status: 'N/A',\n        fields: {\n            Result: '', FromDomain: '', Policy: '', SubdomainPolicy: '',\n            AlignmentDKIM: '', AlignmentSPF: ''\n        },\n        details: ''\n    };\n\n //DKIM Analysis\n\n function analyseDKIMRecord(headers) {\n    if (typeof headers !== 'string') {\n        return {\n            status: 'N/A',\n            fields: {\n                Result: '', Selector: '', Domain: '', SigningAlgorithm: '',\n                Canonicalization: ''\n            },\n            details: 'Headers must be a string'\n        };\n    }\n\n    let result = {\n        status: 'N/A',\n        fields: {\n            Result: '', Selector: '', Domain: '', SigningAlgorithm: '',\n            Canonicalization: ''\n        },\n        details: ''\n    };\n\n    // Extract Authentication-Results header\n    const authResultsRegex = /(?:^|\\n)Authentication-Results:[^\\n]*(?:\\n\\s+[^\\n]*)*?(?=\\n[^ \\t\\n]|\\n*$)/gi;\n    const authHeaderMatch = headers.match(authResultsRegex);\n\n    if (authHeaderMatch) {\n        const authHeader = authHeaderMatch[authHeaderMatch.length - 1].replace(/^Authentication-Results:\\s*/, '').trim();\n        result.details = authHeader;\n\n        // Extract explicit DKIM result\n        const dkimResultMatch = authHeader.match(/dkim=([\\w\\s]+?)(?=\\s|;|$)/i);\n        result.fields.Result = dkimResultMatch ? dkimResultMatch[1].trim().toLowerCase() : '';\n        result.status = result.fields.Result === 'pass' ? 'PASS' : \n                        (result.fields.Result === 'fail' ? 'FAIL' : \n                        (result.fields.Result ? 'UNKNOWN' : 'N/A'));\n\n        // Extract additional DKIM fields (optional, informational)\n        result.fields.Domain = (authHeader.match(/header\\.d=([\\w\\s.-]+?)(?=\\s|;|$)/i) || [])[1]?.trim() || '';\n        result.fields.Selector = (authHeader.match(/header\\.s=([\\w\\s.-]+?)(?=\\s|;|$)/i) || [])[1]?.trim() || '';\n        result.fields.SigningAlgorithm = (authHeader.match(/header\\.a=([\\w\\s-]+?)(?=\\s|;|$)/i) || [])[1]?.trim() || '';\n        result.fields.Canonicalization = (authHeader.match(/header\\.c=([\\w\\s\\/]+?)(?=\\s|;|$)/i) || [])[1]?.trim() || '';\n    } else {\n        result.details = 'No Authentication-Results header found.';\n        result.status = 'N/A';\n    }\n\n    return result;\n}\n //DKIM Analyis End\n\n    function analyseDMARCRecord(headers) {\n    // Validate input\n    if (typeof headers !== 'string') {\n        console.debug('analyseDMARCRecord: Invalid input, headers must be a string:', headers);\n        return {\n            status: 'N/A',\n            fields: {\n                Result: 'N/A',\n                FromDomain: 'N/A',\n                Policy: 'N/A',\n                SubdomainPolicy: 'N/A',\n                AlignmentDKIM: 'N/A',\n                AlignmentSPF: 'N/A'\n            },\n            details: 'Headers must be a string'\n        };\n    }\n\n    // Log input for debugging\n    console.debug('analyseDMARCRecord: Input headers:', headers);\n\n    // Normalize whitespace (replace tabs and multiple spaces with single space, preserve newlines)\n    const normalizedHeaders = headers.replace(/\\t+/g, ' ').replace(/ +/g, ' ').trim();\n\n    // Find all Authentication-Results headers\n    const authResultsRegex = /(?:^|\\n)Authentication-Results:[^\\n]*(?:\\n\\s+[^\\n]*)*?(?=\\n[^ \\t\\n]|\\n*$)/gi;\n    const authMatches = normalizedHeaders.match(authResultsRegex) || [];\n\n    if (authMatches.length === 0) {\n        console.debug('analyseDMARCRecord: No Authentication-Results header found in:', normalizedHeaders);\n        return {\n            status: 'N/A',\n            fields: {\n                Result: 'N/A',\n                FromDomain: 'N/A',\n                Policy: 'N/A',\n                SubdomainPolicy: 'N/A',\n                AlignmentDKIM: 'N/A',\n                AlignmentSPF: 'N/A'\n            },\n            details: 'No DMARC record found'\n        };\n    }\n\n    // Use the last Authentication-Results header (closest to receiving server)\n    const dmarcHeader = authMatches[authMatches.length - 1].replace(/^Authentication-Results:\\s*/, '').trim();\n    console.debug('analyseDMARCRecord: Selected header:', dmarcHeader);\n\n    // Extract fields with case-insensitive regex\n    const fields = {\n        Result: (dmarcHeader.match(/dmarc=(\\w+)/i) || [])[1]?.toLowerCase() || 'N/A',\n        FromDomain: (dmarcHeader.match(/header\\.from=([^;\\s]+)/i) || [])[1] || 'N/A',\n        Policy: (dmarcHeader.match(/p=([^;\\s]+)/i) || [])[1] || 'N/A',\n        SubdomainPolicy: (dmarcHeader.match(/sp=([^;\\s]+)/i) || [])[1] || 'N/A',\n        AlignmentDKIM: (dmarcHeader.match(/dkim-alignment=([^;\\s]+)/i) || [])[1] || 'N/A',\n        AlignmentSPF: (dmarcHeader.match(/spf-alignment=([^;\\s]+)/i) || [])[1] || 'N/A'\n    };\n\n    // Determine status based on Result\n    let status = 'N/A';\n    if (fields.Result !== 'N/A') {\n        if (['pass', 'fail', 'none'].includes(fields.Result)) {\n            status = fields.Result === 'pass' ? 'PASS' : 'FAIL';\n        } else {\n            status = 'INVALID';\n        }\n    }\n\n    // Provide detailed feedback\n    let details = `DMARC result: ${fields.Result}`;\n    if (fields.Result === 'N/A') {\n        details = 'No DMARC record found';\n    } else if (fields.Policy !== 'N/A') {\n        details += `, policy: ${fields.Policy}`;\n    }\n\n    console.debug('analyseDMARCRecord: Output:', { status, fields, details });\n\n    return {\n        status,\n        fields,\n        details\n    };\n}\n\n    function toggleDkimDetails() {\n        const dkimDetails = document.getElementById('dkim-details');\n        const toggleArrow = document.querySelector('.toggle-dkim-arrow');\n        if (dkimDetails && toggleArrow) {\n            dkimDetails.classList.toggle('expanded');\n            toggleArrow.textContent = dkimDetails.classList.contains('expanded') ? '▲' : '▼';\n        }\n    }\n\n    function toggleDmarcDetails() {\n        const dmarcDetails = document.getElementById('dmarc-details');\n        const toggleArrow = document.querySelector('.toggle-dmarc-arrow');\n        if (dmarcDetails && toggleArrow) {\n            dmarcDetails.classList.toggle('expanded');\n            toggleArrow.textContent = dmarcDetails.classList.contains('expanded') ? '▲' : '▼';\n        }\n    }\n\n\n        function escapeHtml(text) {\n            if (typeof text !== 'string') return '';\n            const div = document.createElement('div');\n            div.textContent = text;\n            return div.innerHTML;\n        }\n\n        function sanitizeHtml(html) {\n            try {\n                const div = document.createElement('div');\n                div.innerHTML = html;\n\n                ['script', 'iframe', 'object', 'embed'].forEach(tag => {\n                    const elements = div.getElementsByTagName(tag);\n                    while (elements.length) {\n                        elements[0].parentNode.removeChild(elements[0]);\n                    }\n                });\n\n                const elements = div.querySelectorAll('*');\n                elements.forEach(el => {\n                    Array.from(el.attributes).forEach(attr => {\n                        const name = attr.name.toLowerCase();\n                        const value = attr.value.toLowerCase();\n                        if (name.startsWith('on') || value.includes('javascript:') || value.includes('data:')) {\n                            el.removeAttribute(attr.name);\n                        }\n                        if (['src'].includes(name) && value.match(/^(https?:\\/\\/)?[\\w.-]*(mzstatic\\.com|apple\\.com)\\//)) {\n                            // Keep original src\n                        } else if (['src', 'href'].includes(name) && (value.startsWith('http') || value.startsWith('//'))) {\n                            el.setAttribute(attr.name, '#');\n                        }\n                    });\n                });\n\n                return div.innerHTML;\n            } catch (e) {\n                console.error('Error sanitizing HTML:', e);\n                return '<p>Error rendering HTML content.</p>';\n            }\n        }\n\n        function decodeQuotedPrintable(text) {\n            if (typeof text !== 'string') return '';\n            let bytes = [];\n            text = text.replace(/=\\r?\\n/g, '');\n            let i = 0;\n            while (i < text.length) {\n                if (text[i] === '=' && i + 2 < text.length) {\n                    const hex = text.slice(i + 1, i + 3);\n                    if (/[0-9A-F]{2}/i.test(hex)) {\n                        bytes.push(parseInt(hex, 16));\n                        i += 3;\n                    } else {\n                        bytes.push(text.charCodeAt(i));\n                        i++;\n                    }\n                } else {\n                    bytes.push(text.charCodeAt(i));\n                    i++;\n                }\n            }\n            try {\n                return new TextDecoder('utf-8').decode(new Uint8Array(bytes));\n            } catch (e) {\n                console.warn('UTF-8 decoding failed:', e);\n                return text;\n            }\n        }\n\n        function decodeBase64(text) {\n            if (typeof text !== 'string') return '';\n            try {\n                const cleanedText = text.replace(/\\s+/g, '');\n                const base64Regex = /^[A-Za-z0-9+/=]+$/;\n                if (!base64Regex.test(cleanedText)) {\n                    console.warn('Invalid base64 string detected:', cleanedText.substring(0, 50) + '...');\n                    return '';\n                }\n                const paddingNeeded = (4 - (cleanedText.length % 4)) % 4;\n                const paddedText = cleanedText + '='.repeat(paddingNeeded);\n                const decoded = atob(paddedText);\n                return decoded;\n            } catch (e) {\n                console.warn('Failed to decode base64 string:', e.message);\n                return '';\n            }\n        }\n\n        function parseEmailAddress(emailStr) {\n            if (typeof emailStr !== 'string') return '';\n            const match = emailStr.match(/<([^>]+)>|[^\\s<>,;]+@[^\\s<>,;]+/);\n            return match ? match[1] || match[0] : '';\n        }\n\n        function escapeRegex(str) {\n            if (typeof str !== 'string') return '';\n            return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n        }\n\n        function validateIP(ip) {\n            if (typeof ip !== 'string') return '';\n            const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;\n            const ipv6Regex = /^[0-9a-fA-F:]+$/;\n            return ipv4Regex.test(ip) || (ipv6Regex.test(ip) && ip.includes(':')) ? ip : '';\n        }\n\n        function parseDate(dateStr) {\n            if (!dateStr || typeof dateStr !== 'string') return '';\n            let cleanedDate = dateStr.replace(/^;?\\s*|\\s*;?\\s*$/g, '').replace(/\\s+/g, ' ').trim();\n            const parts = cleanedDate.match(/(?:(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun),\\s*)?(\\d{1,2})\\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s+(\\d{4})\\s+(\\d{2}:\\d{2}:\\d{2})\\s*(?:(?:[+-]\\d{4}|GMT|UTC|Z|\\w+)?)?/i);\n            if (!parts) return '';\n\n            let [, day, month, year, time, tz] = parts;\n            const months = {\n                'jan': '01', 'feb': '02', 'mar': '03', 'apr': '04',\n                'may': '05', 'jun': '06', 'jul': '07', 'aug': '08',\n                'sep': '09', 'oct': '10', 'nov': '11', 'dec': '12'\n            };\n\n            day = day.padStart(2, '0');\n            month = months[month.toLowerCase()];\n            if (!month) return '';\n\n            tz = tz ? tz.trim() : '+0000';\n            if (['GMT', 'UTC', 'Z'].includes(tz.toUpperCase())) {\n                tz = '+0000';\n            } else if (!tz.match(/^[+-]\\d{4}$/)) {\n                tz = '+0000';\n            } else {\n                tz = tz.replace(/([+-]\\d{2})(\\d{2})/, '$1:$2');\n            }\n\n            const dateString = `${year}-${month}-${day}T${time}${tz}`;\n            try {\n                const date = new Date(dateString);\n                return isNaN(date.getTime()) ? '' : date.toISOString();\n            } catch (e) {\n                console.error('Date parsing error:', e, dateString);\n                return '';\n            }\n        }\n\n        function analyseSPFRecord(headers) {\n            if (typeof headers !== 'string') {\n                return {\n                    status: 'N/A',\n                    fields: {\n                        Result: '', Receiver: '', ClientIP: '', EnvelopeFrom: '',\n                        HELO: '', Mechanism: '', PriorityProtocol: ''\n                    },\n                    details: 'Headers must be a string'\n                };\n            }\n            const spfRegex = /^Received-SPF:.*?(?=\\n[A-Z-]|$)/gim;\n            const spfMatches = headers.match(spfRegex) || [];\n            if (spfMatches.length === 0) {\n                return {\n                    status: 'N/A',\n                    fields: {\n                        Result: '', Receiver: '', ClientIP: '', EnvelopeFrom: '',\n                        HELO: '', Mechanism: '', PriorityProtocol: ''\n                    },\n                    details: 'SPF header is missing'\n                };\n            }\n\n            const spfHeader = spfMatches[0].trim();\n            const fields = {\n                Result: (spfHeader.match(/^Received-SPF: (\\w+)/i) || [])[1] || '',\n                Receiver: (spfHeader.match(/receiver=([^;]+)/i) || [])[1] || '',\n                ClientIP: (spfHeader.match(/client-ip=([^;]+)/i) || [])[1] || '',\n                EnvelopeFrom: (spfHeader.match(/envelope-from=([^;]+)/i) || \n                              spfHeader.match(/domain of\\s*<?([^;>\\s]+)/i) || [])[1] || '',\n                HELO: (spfHeader.match(/helo=([^;]+)/i) || [])[1] || '',\n                Mechanism: (spfHeader.match(/mechanism=([^;]+)/i) || \n                           spfHeader.match(/problem=([^;]+)/i) || [])[1] || '',\n                PriorityProtocol: (spfHeader.match(/pr=([^;]+)/i) || [])[1] || ''\n            };\n\n            return {\n                status: fields.Result.toLowerCase() === 'pass' ? 'PASS' : 'FAIL',\n                fields,\n                details: spfHeader\n            };\n        }\n\n        function analyseBCLRecord(headers) {\n            if (typeof headers !== 'string') {\n                return {\n                    status: 'N/A',\n                    value: '',\n                    message: 'Invalid headers provided',\n                    details: 'Headers must be a string'\n                };\n            }\n            const bclRegex = /^X-Microsoft-Antispam:.*$/gim;\n            const bclMatches = headers.match(bclRegex) || [];\n            if (bclMatches.length === 0) {\n                return {\n                    status: 'N/A',\n                    value: '',\n                    message: 'No X-Microsoft-Antispam header found',\n                    details: 'X-Microsoft-Antispam header is missing'\n                };\n            }\n\n            const bclHeader = bclMatches[0].replace(/^X-Microsoft-Antispam:\\s*/i, '').trim();\n            const bclValueMatch = bclHeader.match(/BCL:(\\d+)/i);\n            if (!bclValueMatch) {\n                return {\n                    status: 'N/A',\n                    value: '',\n                    message: 'BCL value not found in header',\n                    details: bclHeader\n                };\n            }\n\n            const bclValue = parseInt(bclValueMatch[1], 10);\n            let status, message;\n            if (bclValue <= 3) {\n                status = 'LOW';\n                message = 'Low bulk complaint level - email is unlikely to be spam';\n            } else if (bclValue <= 6) {\n                status = 'MEDIUM';\n                message = 'Medium bulk complaint level - email may be spam';\n            } else {\n                status = 'HIGH';\n                message = 'High bulk complaint level - email is likely spam';\n            }\n\n            return {\n                status,\n                value: bclValue.toString(),\n                message,\n                details: bclHeader\n            };\n        }\n\n        function analyseForefrontAntispam(headers) {\n            if (typeof headers !== 'string') {\n                return {\n                    status: 'N/A',\n                    fields: {\n                        CIP: '', CTRY: '', LANG: '', SCL: '', SRV: '', IPV: '',\n                        SFV: '', H: '', PTR: '', CAT: '', SFS: '', DIR: ''\n                    },\n                    details: 'Headers must be a string'\n                };\n            }\n            const forefrontRegex = /^X-Forefront-Antispam-Report:.*$/gim;\n            const forefrontMatches = headers.match(forefrontRegex) || [];\n            if (forefrontMatches.length === 0) {\n                return {\n                    status: 'N/A',\n                    fields: {\n                        CIP: '', CTRY: '', LANG: '', SCL: '', SRV: '', IPV: '',\n                        SFV: '', H: '', PTR: '', CAT: '', SFS: '', DIR: ''\n                    },\n                    details: 'X-Forefront-Antispam-Report header is missing'\n                };\n            }\n\n            const forefrontHeader = forefrontMatches[0].replace(/^X-Forefront-Antispam-Report:\\s*/i, '').trim();\n            const fields = {\n                CIP: (forefrontHeader.match(/CIP:([^;]+)/i) || [])[1] || '',\n                CTRY: (forefrontHeader.match(/CTRY:([^;]+)/i) || [])[1] || '',\n                LANG: (forefrontHeader.match(/LANG:([^;]+)/i) || [])[1] || '',\n                SCL: (forefrontHeader.match(/SCL:([^;]+)/i) || [])[1] || '',\n                SRV: (forefrontHeader.match(/SRV:([^;]+)/i) || [])[1] || '',\n                IPV: (forefrontHeader.match(/IPV:([^;]+)/i) || [])[1] || '',\n                SFV: (forefrontHeader.match(/SFV:([^;]+)/i) || [])[1] || '',\n                H: (forefrontHeader.match(/H:([^;]+)/i) || [])[1] || '',\n                PTR: (forefrontHeader.match(/PTR:([^;]+)/i) || [])[1] || '',\n                CAT: (forefrontHeader.match(/CAT:([^;]+)/i) || [])[1] || '',\n                SFS: (forefrontHeader.match(/SFS:([^;]+)/i) || [])[1] || '',\n                DIR: (forefrontHeader.match(/DIR:([^;]+)/i) || [])[1] || ''\n            };\n\n            return {\n                status: 'VALID',\n                fields,\n                details: forefrontHeader\n            };\n        }\n\n        function sortTransactions(column, direction) {\n            transactions.sort((a, b) => {\n                let valA = a[column] || '';\n                let valB = b[column] || '';\n\n                if (column === 'order') {\n                    valA = parseInt(valA) || 0;\n                    valB = parseInt(valB) || 0;\n                    return direction === 'asc' ? valA - valB : valB - valA;\n                }\n\n                if (column === 'timestamp') {\n                    valA = valA ? new Date(valA) : new Date(0);\n                    valB = valB ? new Date(valB) : new Date(0);\n                    return direction === 'asc' ? valA - valB : valB - valA;\n                }\n\n                if (!valA && !valB) return 0;\n                if (!valA) return direction === 'asc' ? 1 : -1;\n                if (!valB) return direction === 'asc' ? -1 : 1;\n                return direction === 'asc'\n                    ? valA.localeCompare(valB)\n                    : valB.localeCompare(valA);\n            });\n        }\n\n// Render Header Fields\n\nfunction renderHeaderFields(headerFields, spfResult, dkimResult, dmarcResult, bclResult, forefrontResult, authResult) {\n    let html = '<div class=\"header-fields\" style=\"margin-bottom: 20px;\">';\n    \n    // SPF Section\n    html += `\n        <div class=\"spf-section\">\n            ${spfResult.status === 'N/A' ? \n                '<div class=\"spf-status-container\"><div class=\"spf-status spf-fail\">SPF: N/A</div></div><p>No SPF record found.</p>' : \n                `<div class=\"spf-status-container\">\n                    <div class=\"spf-status ${spfResult.status === 'PASS' ? 'spf-pass' : 'spf-fail'}\">\n                        SPF: ${escapeHtml(spfResult.fields.Result || 'N/A')}\n                    </div>\n                    <span class=\"toggle-spf-arrow\" onclick=\"toggleSpfDetails()\">▼</span>\n                </div>\n                <div class=\"spf-details-container\" id=\"spf-details\">\n                    <h3>SPF Analysis</h3>\n                    <table>\n                        <thead>\n                            <tr>\n                                <th data-label=\"Field\">Field</th>\n                                <th data-label=\"Value\">Value</th>\n                            </tr>\n                        </thead>\n                        <tbody>\n                            <tr><td data-label=\"Result\">Result</td><td data-label=\"Value\">${escapeHtml(spfResult.fields.Result || 'N/A')}</td></tr>\n                            <tr><td data-label=\"Receiver\">Receiver</td><td data-label=\"Value\">${escapeHtml(spfResult.fields.Receiver || 'N/A')}</td></tr>\n                            <tr><td data-label=\"Client IP\">Client IP</td><td data-label=\"Value\">${escapeHtml(spfResult.fields.ClientIP || 'N/A')}</td></tr>\n                            <tr><td data-label=\"Envelope From\">Envelope From</td><td data-label=\"Value\">${escapeHtml(spfResult.fields.EnvelopeFrom || 'N/A')}</td></tr>\n                            <tr><td data-label=\"HELO\">HELO</td><td data-label=\"Value\">${escapeHtml(spfResult.fields.HELO || 'N/A')}</td></tr>\n                            <tr><td data-label=\"Mechanism\">Mechanism</td><td data-label=\"Value\">${escapeHtml(spfResult.fields.Mechanism || 'N/A')}</td></tr>\n                            <tr><td data-label=\"Priority/Protocol\">Priority/Protocol</td><td data-label=\"Value\">${escapeHtml(spfResult.fields.PriorityProtocol || 'N/A')}</td></tr>\n                        </tbody>\n                    </table>\n                    <p style=\"margin-top: 10px;\" class=\"spf-details\"><strong>Full Details:</strong> ${escapeHtml(spfResult.details)}</p>\n                </div>`\n            }\n        </div>\n    `;\n    \n    // DKIM Section\n    html += `\n       <div class=\"dkim-section\">\n    ${dkimResult.status === 'N/A' ? \n        '<div class=\"dkim-status-container\"><div class=\"dkim-status dkim-fail\">DKIM: N/A</div></div><p>No DKIM authentication result found.</p>' : \n        `<div class=\"dkim-status-container\">\n            <div class=\"dkim-status ${dkimResult.status === 'PASS' ? 'dkim-pass' : 'dkim-fail'}\">\n                DKIM: ${escapeHtml(dkimResult.fields.Result.toUpperCase())}\n            </div>\n            <span class=\"toggle-dkim-arrow\" onclick=\"toggleDkimDetails()\">▼</span>\n        </div>\n        <div class=\"dkim-details-container\" id=\"dkim-details\">\n            <h3>DKIM Analysis</h3>\n            <table>\n                <thead>\n                    <tr>\n                        <th data-label=\"Field\">Field</th>\n                        <th data-label=\"Value\">Value</th>\n                    </tr>\n                </thead>\n                <tbody>\n                    <tr><td data-label=\"Result\">Result</td><td data-label=\"Value\">${escapeHtml(dkimResult.fields.Result || 'N/A')}</td></tr>\n                    <tr><td data-label=\"Selector\">Selector</td><td data-label=\"Value\">${escapeHtml(dkimResult.fields.Selector || 'N/A')}</td></tr>\n                    <tr><td data-label=\"Domain\">Domain</td><td data-label=\"Value\">${escapeHtml(dkimResult.fields.Domain || 'N/A')}</td></tr>\n                    <tr><td data-label=\"Signing Algorithm\">Signing Algorithm</td><td data-label=\"Value\">${escapeHtml(dkimResult.fields.SigningAlgorithm || 'N/A')}</td></tr>\n                    <tr><td data-label=\"Canonicalization\">Canonicalization</td><td data-label=\"Value\">${escapeHtml(dkimResult.fields.Canonicalization || 'N/A')}</td></tr>\n                </tbody>\n            </table>\n            <p style=\"margin-top: 10px;\" class=\"dkim-details\"><strong>Full Details:</strong> ${escapeHtml(dkimResult.details)}</p>\n        </div>`\n    }\n</div>\n    `;\n    \n    // DMARC Section\n    html += `\n        <div class=\"dmarc-section\">\n            ${dmarcResult.status === 'N/A' ? \n                '<div class=\"dmarc-status-container\"><div class=\"dmarc-status dmarc-fail\">DMARC: N/A</div></div><p>No DMARC record found.</p>' : \n                `<div class=\"dmarc-status-container\">\n                    <div class=\"dmarc-status ${dmarcResult.status === 'PASS' ? 'dmarc-pass' : 'dmarc-fail'}\">\n                        DMARC: ${escapeHtml(dmarcResult.fields.Result || 'N/A')}\n                    </div>\n                    <span class=\"toggle-dmarc-arrow\" onclick=\"toggleDmarcDetails()\">▼</span>\n                </div>\n                <div class=\"dmarc-details-container\" id=\"dmarc-details\">\n                    <h3>DMARC Analysis</h3>\n                    <table>\n                        <thead>\n                            <tr>\n                                <th data-label=\"Field\">Field</th>\n                                <th data-label=\"Value\">Value</th>\n                            </tr>\n                        </thead>\n                        <tbody>\n                            <tr><td data-label=\"Result\">Result</td><td data-label=\"Value\">${escapeHtml(dmarcResult.fields.Result || 'N/A')}</td></tr>\n                            <tr><td data-label=\"From Domain\">From Domain</td><td data-label=\"Value\">${escapeHtml(dmarcResult.fields.FromDomain || 'N/A')}</td></tr>\n                            <tr><td data-label=\"Policy\">Policy</td><td data-label=\"Value\">${escapeHtml(dmarcResult.fields.Policy || 'N/A')}</td></tr>\n                            <tr><td data-label=\"Subdomain Policy\">Subdomain Policy</td><td data-label=\"Value\">${escapeHtml(dmarcResult.fields.SubdomainPolicy || 'N/A')}</td></tr>\n                            <tr><td data-label=\"DKIM Alignment\">DKIM Alignment</td><td data-label=\"Value\">${escapeHtml(dmarcResult.fields.AlignmentDKIM || 'N/A')}</td></tr>\n                            <tr><td data-label=\"SPF Alignment\">SPF Alignment</td><td data-label=\"Value\">${escapeHtml(dmarcResult.fields.AlignmentSPF || 'N/A')}</td></tr>\n                        </tbody>\n                    </table>\n                    <p style=\"margin-top: 10px;\" class=\"dmarc-details\"><strong>Full Details:</strong> ${escapeHtml(dmarcResult.details)}</p>\n                </div>`\n            }\n        </div>\n    `;\n    \n    // Authentication-Results Section\n    html += `\n        <div class=\"auth-results-section\">\n            ${authResult.status === 'N/A' ? \n                '<div class=\"auth-results-status-container\"><div class=\"auth-results-status auth-results-fail\">Authentication-Results: N/A</div></div><p>No Authentication-Results header found.</p>' : \n                `<div class=\"auth-results-status-container\">\n                    <div class=\"auth-results-status ${authResult.status === 'PASS' ? 'auth-results-pass' : 'auth-results-fail'}\">\n                        Authentication-Results: ${escapeHtml(authResult.status)}\n                    </div>\n                    <span class=\"toggle-auth-results-arrow\" onclick=\"toggleAuthResultsDetails()\">▼</span>\n                </div>\n                <div class=\"auth-results-details-container\" id=\"auth-results-details\">\n                    <h3>Authentication-Results Analysis</h3>\n                    <table>\n                        <thead>\n                            <tr>\n                                <th data-label=\"Field\">Field</th>\n                                <th data-label=\"Value\">Value</th>\n                            </tr>\n                        </thead>\n                        <tbody>\n                            <tr><td data-label=\"SPF Result\">SPF Result</td><td data-label=\"Value\">${escapeHtml(authResult.fields.SPFResult || 'N/A')}</td></tr>\n                            <tr><td data-label=\"DKIM Result\">DKIM Result</td><td data-label=\"Value\">${escapeHtml(authResult.fields.DKIMResult || 'N/A')}</td></tr>\n                            <tr><td data-label=\"DMARC Result\">DMARC Result</td><td data-label=\"Value\">${escapeHtml(authResult.fields.DMARCResult || 'N/A')}</td></tr>\n                            <tr><td data-label=\"Server\">Server</td><td data-label=\"Value\">${escapeHtml(authResult.fields.Server || 'N/A')}</td></tr>\n                            <tr><td data-label=\"Auth Details\">Auth Details</td><td data-label=\"Value\">${escapeHtml(authResult.fields.AuthDetails || 'N/A')}</td></tr>\n                        </tbody>\n                    </table>\n                    <p style=\"margin-top: 10px;\" class=\"auth-results-details\"><strong>Full Details:</strong> ${escapeHtml(authResult.details)}</p>\n                </div>`\n            }\n        </div>\n    `;\n    \n    // BCL Section\n    html += `\n        <div class=\"bcl-section\">\n            ${bclResult.status === 'N/A' ? \n                '<div class=\"bcl-status-container\"><div class=\"bcl-status bcl-na\">X-Microsoft-Antispam: N/A</div></div><p>No X-Microsoft-Antispam header found.</p>' : \n                `<div class=\"bcl-status-container\">\n                    <div class=\"bcl-status ${bclResult.status === 'LOW' ? 'bcl-low' : bclResult.status === 'MEDIUM' ? 'bcl-medium' : 'bcl-high'}\">\n                        X-Microsoft-Antispam: ${escapeHtml(bclResult.status)}\n                    </div>\n                    <span class=\"toggle-bcl-arrow\" onclick=\"toggleBclDetails()\">▼</span>\n                </div>\n                <div class=\"bcl-details-container\" id=\"bcl-details\">\n                    <h3>BCL Analysis</h3>\n                    <table>\n                        <thead>\n                            <tr>\n                                <th data-label=\"Field\">Field</th>\n                                <th data-label=\"Value\">Value</th>\n                            </tr>\n                        </thead>\n                        <tbody>\n                            <tr><td data-label=\"Status\">Status</td><td data-label=\"Value\">${escapeHtml(bclResult.status)}</td></tr>\n                            <tr><td data-label=\"BCL Value\">BCL Value</td><td data-label=\"Value\">${escapeHtml(bclResult.value || 'N/A')}</td></tr>\n                            <tr><td data-label=\"Message\">Message</td><td data-label=\"Value\">${escapeHtml(bclResult.message)}</td></tr>\n                        </tbody>\n                    </table>\n                    <p style=\"margin-top: 10px;\" class=\"bcl-details\"><strong>Full Details:</strong> ${escapeHtml(bclResult.details)}</p>\n                </div>`\n            }\n        </div>\n    `;\n    \n    // Forefront Antispam Section\n    html += `\n        <div class=\"forefront-section\">\n            ${forefrontResult.status === 'N/A' ? \n                '<div class=\"forefront-status-container\"><div class=\"forefront-status na\">X-Forefront-Antispam-Report: N/A</div></div><p>No X-Forefront-Antispam-Report header found.</p>' : \n                `<div class=\"forefront-status-container\">\n                    <div class=\"forefront-status\">\n                        X-Forefront-Antispam-Report: ${escapeHtml(forefrontResult.status)}\n                    </div>\n                    <span class=\"toggle-forefront-arrow\" onclick=\"toggleForefrontDetails()\">▼</span>\n                </div>\n                <div class=\"forefront-details-container\" id=\"forefront-details\">\n                    <h3>X-Forefront-Antispam-Report Analysis</h3>\n                    <table>\n                        <thead>\n                            <tr>\n                                <th data-label=\"Field\">Field</th>\n                                <th data-label=\"Value\">Value</th>\n                            </tr>\n                        </thead>\n                        <tbody>\n                            <tr><td data-label=\"Language\">Language</td><td data-label=\"Value\">${escapeHtml(forefrontResult.fields.LANG || 'N/A')}</td></tr>\n                            <tr><td data-label=\"Spam Confidence Level\">Spam Confidence Level</td><td data-label=\"Value\">${escapeHtml(forefrontResult.fields.SCL || 'N/A')}</td></tr>\n                            <tr><td data-label=\"Spam Filtering Verdict\">Spam Filtering Verdict</td><td data-label=\"Value\">${escapeHtml(forefrontResult.fields.SFV || 'N/A')}</td></tr>\n                            <tr><td data-label=\"IP Filter Verdict\">IP Filter Verdict</td><td data-label=\"Value\">${escapeHtml(forefrontResult.fields.IPV || 'N/A')}</td></tr>\n                            <tr><td data-label=\"HELO/EHLO\">HELO/EHLO</td><td data-label=\"Value\">${escapeHtml(forefrontResult.fields.H || 'N/A')}</td></tr>\n                            <tr><td data-label=\"PTR Record\">PTR Record</td><td data-label=\"Value\">${escapeHtml(forefrontResult.fields.PTR || 'N/A')}</td></tr>\n                            <tr><td data-label=\"Connecting IP Address\">Connecting IP Address</td><td data-label=\"Value\">${escapeHtml(forefrontResult.fields.CIP || 'N/A')}</td></tr>\n                            <tr><td data-label=\"Protection Policy Category\">Protection Policy Category</td><td data-label=\"Value\">${escapeHtml(forefrontResult.fields.CAT || 'N/A')}</td></tr>\n                            <tr><td data-label=\"Spam Rules\">Spam Rules</td><td data-label=\"Value\">${escapeHtml(forefrontResult.fields.SFS || 'N/A')}</td></tr>\n                            <tr><td data-label=\"Source Header\">Source Header</td><td data-label=\"Value\">${escapeHtml(forefrontResult.fields.SRV || 'N/A')}</td></tr>\n                            <tr><td data-label=\"Direction\">Direction</td><td data-label=\"Value\">${escapeHtml(forefrontResult.fields.DIR || 'N/A')}</td></tr>\n                        </tbody>\n                    </table>\n                    <p style=\"margin-top: 10px;\" class=\"forefront-details\"><strong>Full Details:</strong> ${escapeHtml(forefrontResult.details)}</p>\n                </div>`\n            }\n        </div>\n    `;\n    \n    html += '</div>';\n    return html;\n}\n\n\n// ENd of Render Header Fields\n\n        function toggleSpfDetails() {\n            const spfDetails = document.getElementById('spf-details');\n            const toggleArrow = document.querySelector('.toggle-spf-arrow');\n            if (spfDetails && toggleArrow) {\n                spfDetails.classList.toggle('expanded');\n                toggleArrow.textContent = spfDetails.classList.contains('expanded') ? '▲' : '▼';\n            }\n        }\n\n        function toggleForefrontDetails() {\n            const forefrontDetails = document.getElementById('forefront-details');\n            const toggleArrow = document.querySelector('.toggle-forefront-arrow');\n            if (forefrontDetails && toggleArrow) {\n                forefrontDetails.classList.toggle('expanded');\n                toggleArrow.textContent = forefrontDetails.classList.contains('expanded') ? '▲' : '▼';\n            }\n        }\n\n        function toggleBclDetails() {\n            isBclDetailsExpanded = !isBclDetailsExpanded;\n            const bclDetails = document.getElementById('bcl-details');\n            const toggleArrow = document.querySelector('.toggle-bcl-arrow');\n            if (bclDetails && toggleArrow) {\n                bclDetails.classList.toggle('expanded');\n                toggleArrow.textContent = bclDetails.classList.contains('expanded') ? '▲' : '▼';\n            }\n        }\n\n  \n\n        function toggleBodyContent() {\n            isBodyContentExpanded = !isBodyContentExpanded;\n            const bodyResultsDiv = document.getElementById('bodyResults');\n            if (!bodyResultsDiv) return;\n\n            let bodyHtml = '';\n            if (lastBodyContent) {\n                bodyHtml += `<div class=\"body-content-toggle\">\n                    <span class=\"toggle-body-arrow\" onclick=\"toggleBodyContent()\">${isBodyContentExpanded ? '▲' : '▼'}</span>\n                </div>\n                <div class=\"body-content-container${isBodyContentExpanded ? ' expanded' : ''}\" id=\"body-content\">\n                    ${lastBodyContent}\n                </div>`;\n            } else {\n                bodyHtml = '<p>No body content provided.</p>';\n            }\n            bodyResultsDiv.innerHTML = bodyHtml;\n        }\n\n        function renderEmailDetails(headerFields) {\n            let html = '<div class=\"email-details\" style=\"margin-bottom: 20px;\">';\n            html += '<p><strong>From:</strong> ' + (headerFields.from ? escapeHtml(headerFields.from) : 'Not provided') + '</p>';\n            html += '<p><strong>To:</strong> ' + (headerFields.to.length ? escapeHtml(headerFields.to.join(', ')) : 'Not provided') + '</p>';\n            html += '<p><strong>Cc:</strong> ' + (headerFields.cc.length ? escapeHtml(headerFields.cc.join(', ')) : 'Not provided') + '</p>';\n            html += '<p><strong>Subject:</strong> ' + (headerFields.subject ? escapeHtml(headerFields.subject) : 'Not provided') + '</p>';\n            html += '<p><strong>Date:</strong> ' + (headerFields.date ? escapeHtml(headerFields.date) : 'Not provided') + '</p>';\n            html += '</div>';\n            return html;\n        }\n\n\n    // New renderTable function\n\n    function toggleHeaderTable() {\n    isHeaderTableExpanded = !isHeaderTableExpanded;\n    renderTable(lastSpfResult, lastDkimResult, lastDmarcResult, lastBclResult, lastForefrontResult, lastAuthResults);\n}\n\nfunction renderTable(spfResult, dkimResult, dmarcResult, bclResult, forefrontResult, authResult) {\n    const headerResultsDiv = document.getElementById('headerResults');\n    const emailDetailsDiv = document.getElementById('emailDetails');\n    \n    if (!headerResultsDiv || !emailDetailsDiv) {\n        console.error('Header results or email details div not found');\n        return;\n    }\n\n    emailDetailsDiv.innerHTML = renderEmailDetails(headerFields);\n\n    let tableHtml = renderHeaderFields(headerFields, spfResult, dkimResult, dmarcResult, bclResult, forefrontResult, authResult);\n    \n    // Always render the header section, even if no transactions\n    tableHtml += '<div class=\"header-section\">';\n\n    if (transactions.length === 0) {\n        tableHtml += '<p>No headers found.</p></div>';\n        headerResultsDiv.innerHTML = tableHtml;\n        return;\n    }\n\n    tableHtml += `\n        <table>\n            <thead>\n                <tr>\n                    <th data-column=\"order\" class=\"${sortState.column === 'order' ? 'sort-' + sortState.direction : ''}\" onclick=\"sortColumn('order')\">Order</th>\n                    <th data-column=\"action\" class=\"${sortState.column === 'action' ? 'sort-' + sortState.direction : ''}\" onclick=\"sortColumn('action')\">Action</th>\n                    <th data-column=\"timestamp\" class=\"${sortState.column === 'timestamp' ? 'sort-' + sortState.direction : ''}\" onclick=\"sortColumn('timestamp')\">Timestamp</th>\n                    <th data-column=\"fromServer\" class=\"${sortState.column === 'fromServer' ? 'sort-' + sortState.direction : ''}\" onclick=\"sortColumn('fromServer')\">From Server</th>\n                    <th data-column=\"byServer\" class=\"${sortState.column === 'byServer' ? 'sort-' + sortState.direction : ''}\" onclick=\"sortColumn('byServer')\">By Server</th>\n                    <th data-column=\"ip\" class=\"${sortState.column === 'ip' ? 'sort-' + sortState.direction : ''}\" onclick=\"sortColumn('ip')\">IP</th>\n                    <th data-column=\"emailOrDomain\" class=\"${sortState.column === 'emailOrDomain' ? 'sort-' + sortState.direction : ''}\" onclick=\"sortColumn('emailOrDomain')\">Email/Domain</th>\n                    <th data-column=\"details\" class=\"${sortState.column === 'details' ? 'sort-' + sortState.direction : ''}\" onclick=\"sortColumn('details')\">Details</th>\n                </tr>\n            </thead>\n            <tbody>\n    `;\n\n    // Render either all transactions or the first 5 based on isHeaderTableExpanded\n    const displayTransactions = isHeaderTableExpanded ? transactions : transactions.slice(0, 5);\n    displayTransactions.forEach(transaction => {\n        tableHtml += `\n            <tr>\n                <td data-label=\"Order\">${transaction.order || ''}</td>\n                <td data-label=\"Action\">${escapeHtml(transaction.action)}</td>\n                <td data-label=\"Timestamp\">${escapeHtml(transaction.timestamp || '')}</td>\n                <td data-label=\"From Server\">${escapeHtml(transaction.fromServer || '')}</td>\n                <td data-label=\"By Server\">${escapeHtml(transaction.byServer || '')}</td>\n                <td data-label=\"IP\">${escapeHtml(transaction.ip || '')}</td>\n                <td data-label=\"Email/Domain\">${escapeHtml(transaction.emailOrDomain || '')}</td>\n                <td data-label=\"Details\">${escapeHtml(transaction.details)}</td>\n            </tr>\n        `;\n    });\n\n    tableHtml += '</tbody></table>';\n\n    // Always show the toggle if there are more than 5 transactions\n    if (transactions.length > 5) {\n        tableHtml += `\n            <div class=\"header-table-toggle\">\n                <span class=\"toggle-header-arrow\" onclick=\"toggleHeaderTable()\">${isHeaderTableExpanded ? '▲' : '▼'}</span>\n            </div>\n        `;\n    }\n\n    tableHtml += '</div>';\n\n    headerResultsDiv.innerHTML = tableHtml;\n}\n\n\n\n\n        function renderAttachments() {\n            const attachmentResultsDiv = document.getElementById('attachmentResults');\n            if (!attachmentResultsDiv) {\n                console.error('Attachment results div not found');\n                return;\n            }\n\n            if (!Array.isArray(attachments) || attachments.length === 0) {\n                attachmentResultsDiv.innerHTML = '<p>No attachments found.</p>';\n                return;\n            }\n\n            let tableHtml = `\n                <table>\n                    <thead>\n                        <tr>\n                            <th data-label=\"Name\">Name</th>\n                            <th data-label=\"Type\">Type</th>\n                            <th data-label=\"Download\">Download</th>\n                        </tr>\n                    </thead>\n                    <tbody>\n            `;\n\n            attachments.forEach((attachment, index) => {\n                tableHtml += `\n                    <tr>\n                        <td data-label=\"Name\">${escapeHtml(attachment.name)}</td>\n                        <td data-label=\"Type\">${escapeHtml(attachment.type)}</td>\n                        <td data-label=\"Download\"><a href=\"${attachment.dataUrl}\" download=\"${escapeHtml(attachment.name)}\" class=\"download-btn\">Download</a></td>\n                    </tr>\n                `;\n            });\n\n            tableHtml += '</tbody></table>';\n            attachmentResultsDiv.innerHTML = tableHtml;\n        }\n\n       // Update sortColumn function\n       function sortColumn(column) {\n    if (sortState.column === column) {\n        sortState.direction = sortState.direction === 'asc' ? 'desc' : 'asc';\n    } else {\n        sortState.column = column;\n        sortState.direction = 'asc';\n    }\n    sortTransactions(sortState.column, sortState.direction);\n    renderTable(lastSpfResult, lastDkimResult, lastDmarcResult, lastBclResult, lastForefrontResult, lastAuthResults);\n}\n\n        function processIOCs(text, bodyParts) {\n            if (typeof text !== 'string' || !Array.isArray(bodyParts)) return [];\n            const decodedText = decodeQuotedPrintable(text);\n            const patterns = {\n                url: /https?:\\/\\/[^\\s/$.?#].[^\\s]*/gi,\n                ipv4: /\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\b/g,\n                ipv6: /\\b(?:[0-9a-fA-F]{1,4}:){1,7}(?::[0-9a-fA-F]{1,4}|:)\\b|\\b[0-9a-fA-F]{1,4}::[0-9a-fA-F]{1,4}\\b/g,\n                subnet: /\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\/(?:[8-9]|[1-2][0-9]|3[0-2])\\b/g,\n                domain: /(?:[a-zA-Z0-9-_]+\\.)+[a-zA-Z0-9-_]+\\.[a-zA-Z]{2,}/gi,\n                email: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/g,\n                hash: /\\b[0-9a-fA-F]{32,64}\\b/g,\n                timestamp: /\\b(?:[0-1][0-9]|2[0-3]|[0-9]):[0-5][0-9]:[0-5][0-9](?:\\.\\d{1,3})?\\b/g,\n                messageId: /\\b[a-zA-Z0-9-]+\\.\\d+\\.\\d+\\.\\d+\\b/g\n            };\n\n            const hashRegex = {\n                sha256: /^[0-9a-fA-F]{64}$/,\n                md5: /^[0-9a-fA-F]{32}$/,\n                sha1: /^[0-9a-fA-F]{40}$/\n            };\n\n            const commonFileExtensions = new Set([\n                'png', 'jpg', 'jpeg', 'gif', 'bmp', 'tif', 'tiff', 'pdf',\n                'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'csv',\n                'zip', 'rar', '7z', 'exe', 'dll', 'sys', 'bat', 'sh',\n                'mp3', 'mp4', 'avi', 'mkv', 'mov', 'wmv', 'flv', 'wav',\n                'html', 'css', 'js', 'php', 'asp', 'aspx', 'jsp', 'sql',\n                'db', 'bak', 'log', 'tar', 'gz', 'tgz'\n            ]);\n\n            let cleanedText = decodedText.replace(patterns.timestamp, '');\n            const iocs = {\n                url: new Set(),\n                ip: new Set(),\n                subnet: new Set(),\n                domain: new Set(),\n                email: new Set(),\n                hash: new Set()\n            };\n\n            function extractMatches(content, regex, set, transform = v => v) {\n                if (typeof content !== 'string') return;\n                const matches = content.match(regex) || [];\n                matches.forEach(match => {\n                    const transformed = transform(match);\n                    if (transformed) set.add(transformed);\n                });\n            }\n\n            extractMatches(cleanedText, patterns.url, iocs.url);\n            extractMatches(cleanedText, patterns.ipv4, iocs.ip, v => v.replace(/[\\[\\]\\(\\)]/g, ''));\n            extractMatches(cleanedText, patterns.ipv6, iocs.ip, v => v.replace(/[\\[\\]\\(\\)]/g, ''));\n            extractMatches(cleanedText, patterns.subnet, iocs.subnet);\n            extractMatches(cleanedText, patterns.email, iocs.email);\n\n            extractMatches(cleanedText, patterns.domain, iocs.domain, domain => {\n                const normalized = domain.replace(/\\.$/, '');\n                const extension = normalized.toLowerCase().split('.').pop();\n                return (!iocs.ip.has(normalized) && \n                        !iocs.subnet.has(normalized) && \n                        !commonFileExtensions.has(extension)) ? normalized : null;\n            });\n            iocs.domain.delete(null);\n\n            const messageIds = new Set(cleanedText.match(patterns.messageId) || []);\n            extractMatches(cleanedText, patterns.hash, iocs.hash, hash => {\n                if (messageIds.has(hash)) return null;\n                if (hashRegex.sha256.test(hash)) return JSON.stringify({ value: hash, type: 'SHA256' });\n                if (hashRegex.md5.test(hash)) return JSON.stringify({ value: hash, type: 'MD5' });\n                if (hashRegex.sha1.test(hash)) return JSON.stringify({ value: hash, type: 'SHA1' });\n                return null;\n            });\n            iocs.hash.delete(null);\n\n            bodyParts.forEach(part => {\n                let content = part.content;\n                if (part.encoding === 'quoted-printable') {\n                    content = decodeQuotedPrintable(content);\n                } else if (part.encoding === 'base64') {\n                    content = decodeBase64(content);\n                }\n                if (typeof content !== 'string' || content === '') return;\n                content = content.replace(patterns.timestamp, '');\n\n                extractMatches(content, patterns.url, iocs.url);\n                extractMatches(content, patterns.ipv4, iocs.ip, v => v.replace(/[\\[\\]\\(\\)]/g, ''));\n                extractMatches(content, patterns.ipv6, iocs.ip, v => v.replace(/[\\[\\]\\(\\)]/g, ''));\n                extractMatches(content, patterns.email, iocs.email);\n\n                extractMatches(content, patterns.domain, iocs.domain, domain => {\n                    const normalized = domain.replace(/\\.$/, '');\n                    const extension = normalized.toLowerCase().split('.').pop();\n                    return (!iocs.ip.has(normalized) && \n                            !iocs.subnet.has(normalized) && \n                            !commonFileExtensions.has(extension)) ? normalized : null;\n                });\n                iocs.domain.delete(null);\n\n                extractMatches(content, patterns.hash, iocs.hash, hash => {\n                    if (messageIds.has(hash)) return null;\n                    if (hashRegex.sha256.test(hash)) return JSON.stringify({ value: hash, type: 'SHA256' });\n                    if (hashRegex.md5.test(hash)) return JSON.stringify({ value: hash, type: 'MD5' });\n                    if (hashRegex.sha1.test(hash)) return JSON.stringify({ value: hash, type: 'SHA1' });\n                    return null;\n                });\n                iocs.hash.delete(null);\n            });\n\n            const result = [];\n            iocs.url.forEach(value => result.push({ type: 'URL', value }));\n            iocs.ip.forEach(value => result.push({ type: 'IP', value }));\n            iocs.subnet.forEach(value => result.push({ type: 'Subnet', value }));\n            iocs.domain.forEach(value => result.push({ type: 'Domain', value }));\n            iocs.email.forEach(value => result.push({ type: 'Email', value }));\n            iocs.hash.forEach(hash => {\n                const { value, type } = JSON.parse(hash);\n                result.push({ type: `Hash (${type})`, value });\n            });\n\n            return result.sort((a, b) => a.type.localeCompare(b.type));\n        }\n\n        function renderIOCs(iocs) {\n            const iocResultsDiv = document.getElementById('iocResults');\n            if (!iocResultsDiv) {\n                console.error('IOC results div not found');\n                return;\n            }\n\n            if (!Array.isArray(iocs) || iocs.length === 0) {\n                iocResultsDiv.innerHTML = '<p>No IOCs found.</p>';\n                return;\n            }\n\n            let tableHtml = `\n                <table>\n                    <thead>\n                        <tr>\n                            <th data-label=\"Type\">Type</th>\n                            <th data-label=\"Value\">Value</th>\n                        </tr>\n                    </thead>\n                    <tbody>\n            `;\n\n            iocs.forEach(ioc => {\n                tableHtml += `\n                    <tr>\n                        <td data-label=\"Type\">${escapeHtml(ioc.type)}</td>\n                        <td data-label=\"Value\">${escapeHtml(ioc.value)}</td>\n                    </tr>\n                `;\n            });\n\n            tableHtml += '</tbody></table>';\n            iocResultsDiv.innerHTML = tableHtml;\n        }\n\n // Update clearResults function\n function clearResults() {\n        const headerInput = document.getElementById('headerInput');\n        const headerResultsDiv = document.getElementById('headerResults');\n        const emailDetailsDiv = document.getElementById('emailDetails');\n        const bodyResultsDiv = document.getElementById('bodyResults');\n        const attachmentResultsDiv = document.getElementById('attachmentResults');\n        const iocResultsDiv = document.getElementById('iocResults');\n\n        if (headerInput) headerInput.value = '';\n        if (headerResultsDiv) headerResultsDiv.innerHTML = '';\n        if (emailDetailsDiv) emailDetailsDiv.innerHTML = '';\n        if (bodyResultsDiv) bodyResultsDiv.innerHTML = '';\n        if (attachmentResultsDiv) attachmentResultsDiv.innerHTML = '';\n        if (iocResultsDiv) iocResultsDiv.innerHTML = '';\n        transactions = [];\n        headerFields = {};\n        isHeaderTableExpanded = false;\n        isBodyContentExpanded = false;\n        isBclDetailsExpanded = false;\n        lastBodyContent = '';\n        attachments = [];\n        lastSpfResult = {\n            status: 'N/A',\n            fields: {\n                Result: '', Receiver: '', ClientIP: '', EnvelopeFrom: '',\n                HELO: '', Mechanism: '', PriorityProtocol: ''\n            },\n            details: ''\n        };\n        lastDkimResult = {\n            status: 'N/A',\n            fields: {\n                Result: '', Selector: '', Domain: '', SigningAlgorithm: '',\n                Canonicalization: '', SignatureValue: ''\n            },\n            details: ''\n        };\n        lastDmarcResult = {\n            status: 'N/A',\n            fields: {\n                Result: '', FromDomain: '', Policy: '', SubdomainPolicy: '',\n                AlignmentDKIM: '', AlignmentSPF: ''\n            },\n            details: ''\n        };\n        lastBclResult = { status: 'N/A', value: '', message: '', details: '' };\n        lastForefrontResult = {\n            status: 'N/A',\n            fields: {\n                CIP: '', CTRY: '', LANG: '', SCL: '', SRV: '', IPV: '',\n                SFV: '', H: '', PTR: '', CAT: '', SFS: '', DIR: ''\n            },\n            details: ''\n        };\n        lastAuthResults = {\n    status: 'N/A',\n    fields: {\n        SPFResult: '', DKIMResult: '', DMARCResult: '', Server: '', AuthDetails: ''\n    },\n    details: ''\n};\n    }\n\n\n    function analyseEmail() {\n    const headerInput = document.getElementById('headerInput');\n    const headerResultsDiv = document.getElementById('headerResults');\n    const emailDetailsDiv = document.getElementById('emailDetails');\n    const bodyResultsDiv = document.getElementById('bodyResults');\n    const attachmentResultsDiv = document.getElementById('attachmentResults');\n    const iocResultsDiv = document.getElementById('iocResults');\n\n    if (!headerInput || !headerResultsDiv || !emailDetailsDiv || !bodyResultsDiv || !attachmentResultsDiv || !iocResultsDiv) {\n        console.error('Required DOM elements missing');\n        return;\n    }\n\n    const input = headerInput.value.trim();\n    if (!input) {\n        emailDetailsDiv.innerHTML = '<p>Please enter email headers to analyse.</p>';\n        headerResultsDiv.innerHTML = '<p>Please enter email headers to analyse.</p>';\n        bodyResultsDiv.innerHTML = '<p>No body content provided.</p>';\n        attachmentResultsDiv.innerHTML = '<p>No attachments found.</p>';\n        iocResultsDiv.innerHTML = '<p>No IOCs found.</p>';\n        return;\n    }\n\n    try {\n        // Normalize line endings and split headers from body\n        const normalizedInput = input.replace(/\\r\\n/g, '\\n');\n        const headerEndIndex = normalizedInput.indexOf('\\n\\n');\n        const headers = headerEndIndex !== -1 ? normalizedInput.substring(0, headerEndIndex) : normalizedInput;\n        const body = headerEndIndex !== -1 ? normalizedInput.substring(headerEndIndex + 2).trim() : '';\n        transactions = [];\n        headerFields = { from: '', to: [], cc: [], subject: '', date: '' };\n        attachments = [];\n\n        // Perform analyses\n        lastSpfResult = analyseSPFRecord(headers);\n        lastDkimResult = analyseDKIMRecord(headers);\n        lastDmarcResult = analyseDMARCRecord(headers);\n        lastBclResult = analyseBCLRecord(headers);\n        lastForefrontResult = analyseForefrontAntispam(headers);\n\n        // Regular expressions for specific headers\n        const fromRegex = /^From:.*?(?=\\n[A-Z-]|$)/gim;\n        const senderRegex = /^Sender:.*?(?=\\n[A-Z-]|$)/gim;\n        const toRegex = /^To:.*?(?=\\n[A-Z-]|$)/gim;\n        const ccRegex = /^Cc:.*?(?=\\n[A-Z-]|$)/gim;\n        const subjectRegex = /^Subject:.*?(?=\\n[A-Z-]|$)/gim;\n        const dateRegex = /^Date:.*?(?=\\n[A-Z-]|$)/gim;\n        const emailRegex = /[^\\s<>,;]+@[^\\s<>,;]+\\.[^\\s<>,;]+/g;\n        const contentTypeRegex = /Content-Type:.*?(?=\\n[A-Z-]|$)/gis;\n        const contentEncodingRegex = /Content-Transfer-Encoding:.*?(?=\\n[A-Z-]|$)/gis;\n        const contentDispositionRegex = /Content-Disposition:.*?(?=\\n[A-Z-]|$)/gis;\n        const receivedRegex = /^Received:.*?(?=\\n[A-Z-]|$|\\n\\n)(?:\\n\\s+.*?(?=\\n[A-Z-]|$|\\n\\n))*/gim;\n        const receivedDateRegex = /;?\\s*(?:(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun),\\s*)?\\d{1,2}\\s+(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s+\\d{4}\\s+\\d{2}:\\d{2}:\\d{2}\\s*(?:[+-]\\d{4}|GMT)?/i;\n        const serverRegex = /by\\s+([a-zA-Z0-9:.-]+)/i;\n        const fromServerRegex = /from\\s+([a-zA-Z0-9:.-]+)/i;\n        const ipRegex = /\\[(?:(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})|([0-9a-fA-F:]+))\\]/;\n        const boundaryRegex = /boundary=[\"']?([^\"'\\n;]+)[\"']?/i;\n        const forRegex = /for\\s+<?([^\\s>]+@[^\\s>]+)>?/i;\n        const filenameRegex = /filename=[\"']?([^\"'\\n;]+)[\"']?/i;\n\n        // Extract header fields\n        const fromMatches = headers.match(fromRegex) || [];\n        if (fromMatches.length > 0) {\n            headerFields.from = parseEmailAddress(fromMatches[0].replace(/^From:\\s*/i, '').trim()) || fromMatches[0].replace(/^From:\\s*/i, '').trim();\n        }\n\n        const senderMatches = headers.match(senderRegex) || [];\n        if (senderMatches.length > 0 && !headerFields.from) {\n            headerFields.from = parseEmailAddress(senderMatches[0].replace(/^Sender:\\s*/i, '').trim()) || senderMatches[0].replace(/^Sender:\\s*/i, '').trim();\n        }\n\n        const toMatches = headers.match(toRegex) || [];\n        toMatches.forEach(match => {\n            const emails = match.match(emailRegex) || [];\n            headerFields.to.push(...emails);\n        });\n        headerFields.to = [...new Set(headerFields.to)];\n\n        const ccMatches = headers.match(ccRegex) || [];\n        ccMatches.forEach(match => {\n            const emails = match.match(emailRegex) || [];\n            headerFields.cc.push(...emails);\n        });\n        headerFields.cc = [...new Set(headerFields.cc)];\n\n        const subjectMatches = headers.match(subjectRegex) || [];\n        if (subjectMatches.length > 0) {\n            headerFields.subject = decodeQuotedPrintable(subjectMatches[0].replace(/^Subject:\\s*/i, '').trim());\n        }\n\n        const dateMatches = headers.match(dateRegex) || [];\n        if (dateMatches.length > 0) {\n            headerFields.date = parseDate(dateMatches[0].replace(/^Date:\\s*/i, '').trim());\n        }\n\n        // Extract global sender and recipients\n        const globalSender = headerFields.from;\n        const globalRecipients = headerFields.to;\n\n        // Parse all headers\n        let orderCounter = 0;\n        const headerLines = headers.split('\\n');\n        let currentHeader = '';\n        let currentValue = [];\n        const allHeaders = [];\n\n        headerLines.forEach(line => {\n            if (/^[A-Za-z0-9-]+:/.test(line)) {\n                if (currentHeader) {\n                    allHeaders.push({\n                        name: currentHeader,\n                        value: currentValue.join('\\n').trim()\n                    });\n                    currentValue = [];\n                }\n                const [name, ...value] = line.split(':');\n                currentHeader = name.trim();\n                currentValue.push(value.join(':').trim());\n            } else if (currentHeader && /^\\s+/.test(line)) {\n                currentValue.push(line.trim());\n            }\n        });\n\n        if (currentHeader) {\n            allHeaders.push({\n                name: currentHeader,\n                value: currentValue.join('\\n').trim()\n            });\n        }\n\n        allHeaders.forEach(header => {\n            const headerName = header.name;\n            const headerValue = header.value;\n\n            if (headerName.toLowerCase() === 'received') {\n                const rawValue = headerValue;\n                const cleanedValue = rawValue.replace(/\\n\\s+/g, ' ').trim();\n\n                let date = '';\n                const dateMatch = rawValue.match(receivedDateRegex);\n                if (dateMatch) {\n                    date = parseDate(dateMatch[0].replace(/^;\\s*/, '').trim());\n                }\n\n                let fromServer = '';\n                const fromServerMatch = rawValue.match(fromServerRegex);\n                if (fromServerMatch) {\n                    fromServer = fromServerMatch[1].trim();\n                }\n\n                let byServer = '';\n                const byServerMatch = rawValue.match(serverRegex);\n                if (byServerMatch) {\n                    byServer = byServerMatch[1].trim();\n                }\n\n                let ip = '';\n                const ipMatch = rawValue.match(ipRegex);\n                if (ipMatch) {\n                    ip = validateIP(ipMatch[1] || ipMatch[2]);\n                }\n\n                let recipientEmail = '';\n                const forMatch = rawValue.match(forRegex);\n                if (forMatch) {\n                    recipientEmail = parseEmailAddress(forMatch[1]);\n                } else {\n                    recipientEmail = globalRecipients[0] || '';\n                }\n\n                if (fromServer || byServer) {\n                    transactions.push({\n                        order: ++orderCounter,\n                        action: 'Received',\n                        timestamp: date,\n                        fromServer: fromServer,\n                        byServer: byServer,\n                        ip: ip,\n                        emailOrDomain: recipientEmail || globalSender || fromServer || byServer,\n                        details: cleanedValue\n                    });\n                }\n            } else {\n                transactions.push({\n                    order: ++orderCounter,\n                    action: headerName,\n                    timestamp: '',\n                    fromServer: '',\n                    byServer: '',\n                    ip: '',\n                    emailOrDomain: '',\n                    details: headerValue\n                });\n            }\n        });\n\n        // Process Authentiction headers\n        lastAuthResults = analyseAuthResults(headers);\n\n        // Default sort\n        sortState = { column: 'order', direction: 'asc' };\n        sortTransactions(sortState.column, sortState.direction);\n\n        // Process body\n        const bodyParts = [];\n        if (!body) {\n            lastBodyContent = '<p>No body content provided.</p>';\n            attachmentResultsDiv.innerHTML = '<p>No attachments found.</p>';\n        } else {\n            const contentTypeMatch = headers.match(contentTypeRegex) || [];\n            let boundary = '';\n            for (let match of contentTypeMatch) {\n                const boundaryMatch = match.match(boundaryRegex);\n                if (boundaryMatch) {\n                    boundary = boundaryMatch[1];\n                    break;\n                }\n            }\n\n            if (boundary) {\n                const parts = body.split(`--${boundary}`).filter(part => part.trim() && !part.startsWith('--'));\n                lastBodyContent = parts.map((part, index) => {\n                    const lines = part.trim().split('\\n');\n                    let contentType = '';\n                    let encoding = '7bit';\n                    let charset = 'utf-8';\n                    let contentDisposition = '';\n                    let content = '';\n                    let inContent = false;\n                    let filename = '';\n\n                    for (let line of lines) {\n                        if (!inContent && line.match(/^Content-Type:/i)) {\n                            contentType = line.replace(/^Content-Type:\\s*/i, '').trim();\n                            const charsetMatch = contentType.match(/charset=[\"']?([^\"'\\s;]+)[\"']?/i);\n                            if (charsetMatch) {\n                                charset = charsetMatch[1].toLowerCase();\n                            }\n                        } else if (!inContent && line.match(/^Content-Transfer-Encoding:/i)) {\n                            encoding = line.replace(/^Content-Transfer-Encoding:\\s*/i, '').trim().toLowerCase();\n                        } else if (!inContent && line.match(/^Content-Disposition:/i)) {\n                            contentDisposition = line.replace(/^Content-Disposition:\\s*/i, '').trim().toLowerCase();\n                            const filenameMatch = line.match(filenameRegex);\n                            if (filenameMatch) {\n                                filename = filenameMatch[1];\n                            }\n                        } else if (!inContent && line.trim() === '') {\n                            inContent = true;\n                        } else if (inContent) {\n                            content += line + '\\n';\n                        }\n                    }\n\n                    content = content.trim();\n                    const isAttachment = contentDisposition.includes('attachment') ||\n                        contentType.match(/^(application|image|audio|video)\\//i) ||\n                        (filename && !contentType.includes('text/'));\n\n                    if (isAttachment && content && filename) {\n                        let decodedContent = content;\n                        let binaryContent;\n\n                        if (encoding === 'base64') {\n                            decodedContent = decodeBase64(content);\n                            if (decodedContent === '') {\n                                console.warn(`Failed to decode attachment: ${filename}`);\n                                return '';\n                            }\n                            binaryContent = new Uint8Array(decodedContent.split('').map(char => char.charCodeAt(0)));\n                        } else if (encoding === 'quoted-printable') {\n                            decodedContent = decodeQuotedPrintable(content);\n                            binaryContent = new Uint8Array(decodedContent.split('').map(char => char.charCodeAt(0)));\n                        } else {\n                            binaryContent = new Uint8Array(content.split('').map(char => char.charCodeAt(0)));\n                        }\n\n                        try {\n                            const blob = new Blob([binaryContent], { type: contentType.split(';')[0] });\n                            const dataUrl = URL.createObjectURL(blob);\n\n                            attachments.push({\n                                name: filename,\n                                type: contentType.split(';')[0] || 'application/octet-stream',\n                                dataUrl: dataUrl\n                            });\n                        } catch (e) {\n                            console.warn(`Failed to create attachment for ${filename}:`, e);\n                        }\n\n                        return '';\n                    }\n\n                    if (encoding === 'base64') {\n                        content = decodeBase64(content);\n                        if (content === '') {\n                            return `<p>Failed to decode base64 content for part ${index + 1}.</p>`;\n                        }\n                    } else if (encoding === 'quoted-printable') {\n                        content = decodeQuotedPrintable(content);\n                    }\n\n                    bodyParts.push({ content, encoding });\n\n                    if (contentType.includes('text/plain')) {\n                        return `<h4>Plain Text</h4><pre>${escapeHtml(content)}</pre>`;\n                    } else if (contentType.includes('text/html')) {\n                        return `<h4>HTML Content</h4><div class=\"html-content\">${sanitizeHtml(content)}</div>`;\n                    }\n                    return '';\n                }).filter(part => part).join('');\n\n                if (!lastBodyContent) {\n                    lastBodyContent = '<p>No displayable body content provided.</p>';\n                }\n            } else {\n                lastBodyContent = `<h4>Plain Text</h4><pre>${escapeHtml(body)}</pre>`;\n                bodyParts.push({ content: body, encoding: '7bit' });\n            }\n        }\n\n        renderTable(lastSpfResult, lastDkimResult, lastDmarcResult, lastBclResult, lastForefrontResult, lastAuthResults);\n        toggleBodyContent();\n        renderAttachments();\n\n        const iocs = processIOCs(headers, bodyParts);\n        renderIOCs(iocs);\n\n    } catch (e) {\n        console.error('Analysis failed:', e);\n        emailDetailsDiv.innerHTML = '<p>Error analysing email: ' + escapeHtml(e.message) + '</p>';\n        headerResultsDiv.innerHTML = '';\n        bodyResultsDiv.innerHTML = '<p>Error processing body content.</p>';\n        attachmentResultsDiv.innerHTML = '<p>No attachments found.</p>';\n        iocResultsDiv.innerHTML = '<p>No IOCs found.</p>';\n    }\n}\n\nfunction analyseAuthResults(headers) {\n    if (typeof headers !== 'string') {\n        return {\n            status: 'N/A',\n            fields: {\n                SPFResult: '', DKIMResult: '', DMARCResult: '', Server: '', AuthDetails: ''\n            },\n            details: 'Headers must be a string'\n        };\n    }\n\n    // Normalize headers to match analyseDMARCRecord: simplify tabs and multiple spaces\n    const normalizedHeaders = headers.replace(/\\t+/g, ' ').replace(/ +/g, ' ').trim();\n    const authRegex = /(?:^|\\n)Authentication-Results:[^\\n]*(?:\\n\\s+[^\\n]*)*?(?=\\n[^ \\t\\n]|\\n*$)/gi;\n    const authMatches = headers.match(authRegex) || [];\n\n    if (authMatches.length === 0) {\n        return {\n            status: 'N/A',\n            fields: {\n                SPFResult: '', DKIMResult: '', DMARCResult: '', Server: '', AuthDetails: ''\n            },\n            details: 'Authentication-Results header is missing'\n        };\n    }\n\n    // Use the last Authentication-Results header (consistent with analyseDMARCRecord)\n    const authHeader = authMatches[authMatches.length - 1].replace(/^Authentication-Results:\\s*/, '').trim();\n    const fields = {\n        SPFResult: (authHeader.match(/spf=([\\w\\s]+?)(?=\\s|;|$)/i) || [])[1]?.trim() || '',\n        DKIMResult: (authHeader.match(/dkim=([\\w\\s]+?)(?=\\s|;|$)/i) || [])[1]?.trim() || '',\n        DMARCResult: (authHeader.match(/dmarc=([\\w\\s]+?)(?=\\s|;|$)/i) || [])[1]?.trim() || '',\n        Server: (authHeader.match(/^([^;]+)/i) || [])[1]?.trim() || '',\n        AuthDetails: (authHeader.match(/auth=([^;]+)/i) || [])[1] || ''\n    };\n\n    // Determine aggregate status\n    let overallStatus = 'FAIL';\n    if (fields.DMARCResult.toLowerCase() === 'pass') {\n        overallStatus = 'PASS'; // DMARC pass takes precedence\n    } else if (fields.SPFResult.toLowerCase() === 'pass' || fields.DKIMResult.toLowerCase() === 'pass') {\n        overallStatus = 'PARTIAL'; // Either SPF or DKIM pass, but not DMARC\n    } else if (fields.SPFResult || fields.DKIMResult || fields.DMARCResult) {\n        overallStatus = 'FAIL'; // Some results present, but none pass\n    } else {\n        overallStatus = 'N/A'; // No results present\n    }\n\n    return {\n        status: overallStatus,\n        fields,\n        details: authHeader\n    };\n}\n\nfunction toggleAuthResultsDetails() {\n    const authDetails = document.getElementById('auth-results-details');\n    const toggleArrow = document.querySelector('.toggle-auth-results-arrow');\n    if (authDetails && toggleArrow) {\n        authDetails.classList.toggle('expanded');\n        toggleArrow.textContent = authDetails.classList.contains('expanded') ? '▲' : '▼';\n    }\n}\n \n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "ipinfo_to_csv.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n  <title>Network Graph → IP Table</title>\n  <link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH\" crossorigin=\"anonymous\">\n  <link rel=\"stylesheet\" href=\"https://cdn.datatables.net/2.1.8/css/dataTables.bootstrap5.min.css\"/>\n  <style>\n    body { padding: 2rem; background: #f8f9fa; }\n    #drop-zone {\n      border: 3px dashed #6c757d;\n      border-radius: 12px;\n      padding: 3rem;\n      text-align: center;\n      background: white;\n      transition: all 0.2s;\n    }\n    #drop-zone.dragover { border-color: #0d6efd; background: #e7f1ff; }\n    #file-info { margin-top: 1rem; font-weight: bold; }\n    table.dataTable { font-size: 0.95rem; }\n    th, td { vertical-align: middle; }\n    #controls { margin: 1.5rem 0; }\n  </style>\n</head>\n<body>\n\n<div class=\"container\">\n  <h2 class=\"mb-4 text-center\">Network Graph JSON → IP Table</h2>\n\n  <div id=\"drop-zone\" class=\"mb-4\">\n    <p class=\"lead mb-1\">Drop your <code>network_graph-*.json</code> file here</p>\n    <p class=\"text-muted\">or click to select file</p>\n    <input type=\"file\" id=\"fileInput\" accept=\".json\" style=\"display:none\"/>\n    <div id=\"file-info\" class=\"text-primary\"></div>\n  </div>\n\n  <div id=\"result\" class=\"d-none\">\n    <div id=\"controls\" class=\"d-flex justify-content-between align-items-center\">\n      <h5>Extracted IP Records</h5>\n      <button id=\"exportCsvBtn\" class=\"btn btn-success\">Export to CSV</button>\n    </div>\n\n    <table id=\"ipTable\" class=\"table table-striped table-hover table-bordered\">\n      <thead class=\"table-dark\">\n        <tr>\n          <th>IP</th>\n          <th>ASN</th>\n          <th>Country</th>\n          <th>City</th>\n          <th>Organization</th>\n          <th>Special</th>\n        </tr>\n      </thead>\n      <tbody></tbody>\n    </table>\n  </div>\n\n  <div id=\"error\" class=\"alert alert-danger d-none\" role=\"alert\"></div>\n</div>\n\n<script src=\"https://code.jquery.com/jquery-3.7.1.min.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js\"></script>\n<script src=\"https://cdn.datatables.net/2.1.8/js/dataTables.min.js\"></script>\n<script src=\"https://cdn.datatables.net/2.1.8/js/dataTables.bootstrap5.min.js\"></script>\n\n<script>\n// ────────────────────────────────────────────────\nconst dropZone = document.getElementById('drop-zone');\nconst fileInput = document.getElementById('fileInput');\nconst fileInfo  = document.getElementById('file-info');\nconst resultDiv = document.getElementById('result');\nconst errorDiv  = document.getElementById('error');\nconst exportBtn = document.getElementById('exportCsvBtn');\nlet dataTable   = null;\n\n// Click → open file dialog\ndropZone.addEventListener('click', () => fileInput.click());\n\n// File selected via dialog\nfileInput.addEventListener('change', e => {\n  if (e.target.files.length) processFile(e.target.files[0]);\n});\n\n// Drag & drop\ndropZone.addEventListener('dragover', e => {\n  e.preventDefault();\n  dropZone.classList.add('dragover');\n});\ndropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));\ndropZone.addEventListener('drop', e => {\n  e.preventDefault();\n  dropZone.classList.remove('dragover');\n  if (e.dataTransfer.files.length) processFile(e.dataTransfer.files[0]);\n});\n\nfunction showError(msg) {\n  errorDiv.textContent = msg;\n  errorDiv.classList.remove('d-none');\n  resultDiv.classList.add('d-none');\n}\n\nfunction clearError() {\n  errorDiv.classList.add('d-none');\n  errorDiv.textContent = '';\n}\n\nlet allRows = []; // store full data for export\n\nfunction processFile(file) {\n  if (!file.name.endsWith('.json')) {\n    showError(\"Please upload a .json file\");\n    return;\n  }\n\n  fileInfo.textContent = `Processing: ${file.name} (${(file.size/1024).toFixed(1)} KB)`;\n  clearError();\n\n  const reader = new FileReader();\n  reader.onload = e => {\n    try {\n      const data = JSON.parse(e.target.result);\n      buildTable(data);\n    } catch (err) {\n      showError(\"Invalid JSON format: \" + err.message);\n    }\n  };\n  reader.onerror = () => showError(\"Failed to read file\");\n  reader.readAsText(file);\n}\n\nfunction buildTable(json) {\n  // Build lookup maps\n  const nodesById = {};\n  json.nodes.forEach(node => {\n    nodesById[node.id] = node;\n  });\n\n  const edgesFrom = {};\n  json.edges.forEach(edge => {\n    if (!edgesFrom[edge.from]) edgesFrom[edge.from] = [];\n    edgesFrom[edge.from].push(edge);\n  });\n\n  // Collect IP rows\n  allRows = json.nodes\n    .filter(n => n.type === \"ip\")\n    .map(ipNode => {\n      const id = ipNode.id;\n      const conn = (edgesFrom[id] || []);\n\n      const findConnected = (type, labelTest) =>\n        conn.find(e => \n          nodesById[e.to]?.type === type &&\n          labelTest(e.label)\n        )?.to ?? null;\n\n      const asnNodeId     = findConnected(\"asn\",     l => l.includes(\"Assigned\"));\n      const countryNodeId = findConnected(\"country\", l => l.includes(\"Located\"));\n      const cityNodeId    = findConnected(\"city\",    l => l.includes(\"Located\"));\n      const orgNodeId     = findConnected(\"organization\", l => l.includes(\"Belongs\"));\n      const usesNodes     = conn\n        .filter(e => e.label.includes(\"Uses\"))\n        .map(e => nodesById[e.to]?.type)\n        .filter(Boolean);\n\n      return {\n        ip:          ipNode.ip || \"\",\n        asn:         nodesById[asnNodeId]?.asn    || \"\",\n        country:     nodesById[countryNodeId]?.country || \"\",\n        city:        nodesById[cityNodeId]?.city  || \"\",\n        organization:nodesById[orgNodeId]?.organization || \"\",\n        special:     [...new Set(usesNodes)].join(\", \") || \"\"\n      };\n    });\n\n  // Destroy previous table if exists\n  if (dataTable) {\n    dataTable.destroy();\n    dataTable = null;\n    $('#ipTable tbody').empty();\n  }\n\n  // Fill table\n  const tbody = document.querySelector('#ipTable tbody');\n  allRows.forEach(row => {\n    const tr = document.createElement('tr');\n    tr.innerHTML = `\n      <td>${row.ip}</td>\n      <td>${row.asn}</td>\n      <td>${row.country}</td>\n      <td>${row.city}</td>\n      <td>${row.organization}</td>\n      <td>${row.special}</td>\n    `;\n    tbody.appendChild(tr);\n  });\n\n  // Initialize DataTable\n  dataTable = $('#ipTable').DataTable({\n    pageLength: 15,\n    lengthMenu: [10, 15, 25, 50, 100],\n    order: [[0, 'asc']],\n    responsive: true,\n    language: { search: \"Filter:\" }\n  });\n\n  resultDiv.classList.remove('d-none');\n}\n\n// ─── Export CSV ────────────────────────────────────────\nexportBtn.addEventListener('click', () => {\n  if (!allRows.length) {\n    alert(\"No data to export yet.\");\n    return;\n  }\n\n  // Use visible/filtered rows if user filtered the table\n  const visibleRows = dataTable.rows({ search: 'applied' }).data().toArray();\n\n  // If no rows visible (e.g. filter too strict), fall back to all\n  const rowsToExport = visibleRows.length > 0 ? visibleRows : allRows.map(r => [\n    r.ip, r.asn, r.country, r.city, r.organization, r.special\n  ]);\n\n  const headers = [\"IP\",\"ASN\",\"Country\",\"City\",\"Organization\",\"Special\"];\n  const csvContent = [\n    headers.join(\",\"),\n    ...rowsToExport.map(row => \n      row.map(cell => `\"${String(cell).replace(/\"/g, '\"\"')}\"`).join(\",\")\n    )\n  ].join(\"\\n\");\n\n  const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });\n  const url = URL.createObjectURL(blob);\n  const link = document.createElement(\"a\");\n  link.setAttribute(\"href\", url);\n  link.setAttribute(\"download\", \"ip_network_export.csv\");\n  document.body.appendChild(link);\n  link.click();\n  document.body.removeChild(link);\n  URL.revokeObjectURL(url);\n});\n</script>\n</body>\n</html>\n"
  },
  {
    "path": "macos_cors_proxy_install.sh",
    "content": "#!/bin/bash\n\n# Script to install the CORS proxy server from mr-r3b00t/crime-mapper on macOS\n# Ensures Homebrew and npm dependencies are installed\n\n# Exit on any error\nset -e\n\n# Colors for output\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nNC='\\033[0m' # No Color\n\n# Function to print error and exit\nerror_exit() {\n    echo -e \"${RED}Error: $1${NC}\" >&2\n    exit 1\n}\n\n# Function to print success messages\nsuccess() {\n    echo -e \"${GREEN}$1${NC}\"\n}\n\n# Check if running on macOS\necho \"Checking operating system...\"\nif [[ \"$(uname -s)\" != \"Darwin\" ]]; then\n    error_exit \"This script is designed for macOS only.\"\nfi\nsuccess \"Confirmed running on macOS.\"\n\n# Check for Homebrew and install if not present\necho \"Checking for Homebrew...\"\nif ! command -v brew >/dev/null 2>&1; then\n    echo \"Homebrew not found. Installing Homebrew...\"\n    /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\" || error_exit \"Failed to install Homebrew.\"\n    # Add Homebrew to PATH for this session\n    eval \"$(/opt/homebrew/bin/brew shellenv)\" || error_exit \"Failed to set up Homebrew environment.\"\nfi\nsuccess \"Homebrew is installed.\"\n\n# Update Homebrew\necho \"Updating Homebrew...\"\nbrew update || error_exit \"Failed to update Homebrew.\"\nsuccess \"Homebrew updated.\"\n\n# Install Node.js and npm via Homebrew\necho \"Checking for Node.js and npm...\"\nif ! command -v node >/dev/null 2>&1 || ! command -v npm >/dev/null 2>&1; then\n    echo \"Installing Node.js and npm...\"\n    brew install node || error_exit \"Failed to install Node.js and npm.\"\nfi\nsuccess \"Node.js and npm are installed.\"\nnode_version=$(node -v)\nnpm_version=$(npm -v)\necho \"Node.js version: $node_version\"\necho \"npm version: $npm_version\"\n\n# Define installation directory\nINSTALL_DIR=\"$HOME/crime-mapper-cors-proxy\"\necho \"Installation directory: $INSTALL_DIR\"\n\n# Remove existing directory if it exists (optional, comment out if you want to preserve it)\nif [[ -d \"$INSTALL_DIR\" ]]; then\n    echo \"Existing installation found. Removing it...\"\n    rm -rf \"$INSTALL_DIR\" || error_exit \"Failed to remove existing directory.\"\nfi\n\n# Clone the crime-mapper repository\necho \"Cloning crime-mapper repository...\"\ngit clone https://github.com/mr-r3b00t/crime-mapper.git \"$INSTALL_DIR\" || error_exit \"Failed to clone repository.\"\nsuccess \"Repository cloned successfully.\"\n\n# Navigate to the installation directory\ncd \"$INSTALL_DIR\" || error_exit \"Failed to change to installation directory.\"\n\n# Check if cors_proxy_server.js exists\nif [[ ! -f \"cors_proxy_server.js\" ]]; then\n    error_exit \"cors_proxy_server.js not found in the repository.\"\nfi\n\n# Install npm dependencies\necho \"Installing npm dependencies...\"\nnpm install cors express || error_exit \"Failed to install npm dependencies (cors, express).\"\nsuccess \"npm dependencies installed.\"\n\n# Create a launch script for convenience\nLAUNCH_SCRIPT=\"$INSTALL_DIR/start_cors_proxy.sh\"\ncat << EOF > \"$LAUNCH_SCRIPT\"\n#!/bin/bash\ncd \"$INSTALL_DIR\" || exit 1\nnode cors_proxy_server.js\nEOF\nchmod +x \"$LAUNCH_SCRIPT\" || error_exit \"Failed to make launch script executable.\"\nsuccess \"Launch script created at $LAUNCH_SCRIPT.\"\n\n# Start the CORS proxy in the background\necho \"Starting CORS proxy server...\"\n\"$LAUNCH_SCRIPT\" & \nCORS_PID=$!\nsleep 2 # Give it a moment to start\n\n# Check if the server is running\nif ps -p $CORS_PID > /dev/null; then\n    success \"CORS proxy server started successfully (PID: $CORS_PID).\"\n    echo \"You can stop it manually with: kill $CORS_PID\"\nelse\n    error_exit \"CORS proxy server failed to start. Check $INSTALL_DIR for logs or errors.\"\nfi\n\n# Provide instructions\necho\nsuccess \"Installation complete!\"\necho \"To start the CORS proxy manually in the future, run:\"\necho \"  $LAUNCH_SCRIPT\"\necho \"The server runs on port 8081 by default (check cors_proxy_server.js to confirm).\"\necho \"To use with crime-mapper, update the CORS Proxy URL in the Config tab to: http://localhost:8081\"\n"
  },
  {
    "path": "network_graph-4.json",
    "content": "{\n  \"nodes\": [\n    {\n      \"id\": 1,\n      \"type\": \"domain\",\n      \"domain\": \"example.com\"\n    },\n    {\n      \"id\": 2,\n      \"type\": \"contact\",\n      \"name\": \"Attacker\",\n      \"email\": \"attacker@example.com\"\n    },\n    {\n      \"id\": 3,\n      \"type\": \"contact\",\n      \"name\": \"Victim\",\n      \"email\": \"victim@example.com\"\n    },\n    {\n      \"id\": 4,\n      \"type\": \"wallet\",\n      \"address\": \"0xWallet1\"\n    },\n    {\n      \"id\": 5,\n      \"type\": \"wallet\",\n      \"address\": \"0xWallet2\"\n    },\n    {\n      \"id\": 6,\n      \"type\": \"wallet\",\n      \"address\": \"0xWallet3\"\n    },\n    {\n      \"id\": 7,\n      \"type\": \"wallet\",\n      \"address\": \"0xWallet4\"\n    },\n    {\n      \"id\": 8,\n      \"type\": \"wallet\",\n      \"address\": \"0xWallet5\"\n    },\n    {\n      \"id\": 9,\n      \"type\": \"ip\",\n      \"ip\": \"192.168.1.1\"\n    },\n    {\n      \"id\": 10,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"443\"\n    },\n    {\n      \"id\": 11,\n      \"type\": \"ip\",\n      \"ip\": \"192.168.1.2\"\n    },\n    {\n      \"id\": 12,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"443\"\n    },\n    {\n      \"id\": 13,\n      \"type\": \"ip\",\n      \"ip\": \"192.168.1.3\"\n    },\n    {\n      \"id\": 14,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"443\"\n    },\n    {\n      \"id\": 15,\n      \"type\": \"ip\",\n      \"ip\": \"192.168.1.4\"\n    },\n    {\n      \"id\": 16,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"443\"\n    },\n    {\n      \"id\": 17,\n      \"type\": \"ip\",\n      \"ip\": \"192.168.1.5\"\n    },\n    {\n      \"id\": 18,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"443\"\n    },\n    {\n      \"id\": 19,\n      \"type\": \"ip\",\n      \"ip\": \"192.168.1.6\"\n    },\n    {\n      \"id\": 20,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"443\"\n    },\n    {\n      \"id\": 21,\n      \"type\": \"ip\",\n      \"ip\": \"192.168.1.7\"\n    },\n    {\n      \"id\": 22,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"443\"\n    },\n    {\n      \"id\": 23,\n      \"type\": \"ip\",\n      \"ip\": \"192.168.1.8\"\n    },\n    {\n      \"id\": 24,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"443\"\n    },\n    {\n      \"id\": 25,\n      \"type\": \"ip\",\n      \"ip\": \"192.168.1.9\"\n    },\n    {\n      \"id\": 26,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"443\"\n    },\n    {\n      \"id\": 27,\n      \"type\": \"ip\",\n      \"ip\": \"192.168.1.10\"\n    },\n    {\n      \"id\": 28,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"443\"\n    },\n    {\n      \"id\": 29,\n      \"type\": \"ip\",\n      \"ip\": \"192.168.1.11\"\n    },\n    {\n      \"id\": 30,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"443\"\n    },\n    {\n      \"id\": 31,\n      \"type\": \"ip\",\n      \"ip\": \"192.168.1.12\"\n    },\n    {\n      \"id\": 32,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"443\"\n    },\n    {\n      \"id\": 33,\n      \"type\": \"ip\",\n      \"ip\": \"192.168.1.13\"\n    },\n    {\n      \"id\": 34,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"443\"\n    },\n    {\n      \"id\": 35,\n      \"type\": \"ip\",\n      \"ip\": \"192.168.1.14\"\n    },\n    {\n      \"id\": 36,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"443\"\n    },\n    {\n      \"id\": 37,\n      \"type\": \"ip\",\n      \"ip\": \"192.168.1.15\"\n    },\n    {\n      \"id\": 38,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"443\"\n    },\n    {\n      \"id\": 39,\n      \"type\": \"ip\",\n      \"ip\": \"192.168.1.16\"\n    },\n    {\n      \"id\": 40,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"443\"\n    },\n    {\n      \"id\": 41,\n      \"type\": \"ip\",\n      \"ip\": \"192.168.1.17\"\n    },\n    {\n      \"id\": 42,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"443\"\n    },\n    {\n      \"id\": 43,\n      \"type\": \"ip\",\n      \"ip\": \"192.168.1.18\"\n    },\n    {\n      \"id\": 44,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"443\"\n    },\n    {\n      \"id\": 45,\n      \"type\": \"ip\",\n      \"ip\": \"192.168.1.19\"\n    },\n    {\n      \"id\": 46,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"443\"\n    },\n    {\n      \"id\": 47,\n      \"type\": \"ip\",\n      \"ip\": \"192.168.1.20\"\n    },\n    {\n      \"id\": 48,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"443\"\n    }\n  ],\n  \"edges\": [\n    {\n      \"id\": \"edge_2_1_1742037176201\",\n      \"from\": 2,\n      \"to\": 1,\n      \"label\": \"attacks\"\n    },\n    {\n      \"id\": \"edge_1_3_1742037176203\",\n      \"from\": 1,\n      \"to\": 3,\n      \"label\": \"targets\"\n    },\n    {\n      \"id\": \"edge_7_1_1742037176204\",\n      \"from\": 7,\n      \"to\": 1,\n      \"label\": \"owns\"\n    },\n    {\n      \"id\": \"edge_8_1_1742037176204\",\n      \"from\": 8,\n      \"to\": 1,\n      \"label\": \"owns\"\n    },\n    {\n      \"id\": \"edge_9_1_1742037176205\",\n      \"from\": 9,\n      \"to\": 1,\n      \"label\": \"resolves\"\n    },\n    {\n      \"id\": \"edge_9_10_1742037176205\",\n      \"from\": 9,\n      \"to\": 10,\n      \"label\": \"exposes\"\n    },\n    {\n      \"id\": \"edge_11_1_1742037176205\",\n      \"from\": 11,\n      \"to\": 1,\n      \"label\": \"resolves\"\n    },\n    {\n      \"id\": \"edge_11_12_1742037176206\",\n      \"from\": 11,\n      \"to\": 12,\n      \"label\": \"exposes\"\n    },\n    {\n      \"id\": \"edge_13_1_1742037176206\",\n      \"from\": 13,\n      \"to\": 1,\n      \"label\": \"resolves\"\n    },\n    {\n      \"id\": \"edge_13_14_1742037176206\",\n      \"from\": 13,\n      \"to\": 14,\n      \"label\": \"exposes\"\n    },\n    {\n      \"id\": \"edge_15_1_1742037176207\",\n      \"from\": 15,\n      \"to\": 1,\n      \"label\": \"resolves\"\n    },\n    {\n      \"id\": \"edge_15_16_1742037176207\",\n      \"from\": 15,\n      \"to\": 16,\n      \"label\": \"exposes\"\n    },\n    {\n      \"id\": \"edge_17_1_1742037176207\",\n      \"from\": 17,\n      \"to\": 1,\n      \"label\": \"resolves\"\n    },\n    {\n      \"id\": \"edge_17_18_1742037176208\",\n      \"from\": 17,\n      \"to\": 18,\n      \"label\": \"exposes\"\n    },\n    {\n      \"id\": \"edge_19_1_1742037176208\",\n      \"from\": 19,\n      \"to\": 1,\n      \"label\": \"resolves\"\n    },\n    {\n      \"id\": \"edge_19_20_1742037176208\",\n      \"from\": 19,\n      \"to\": 20,\n      \"label\": \"exposes\"\n    },\n    {\n      \"id\": \"edge_21_1_1742037176209\",\n      \"from\": 21,\n      \"to\": 1,\n      \"label\": \"resolves\"\n    },\n    {\n      \"id\": \"edge_21_22_1742037176209\",\n      \"from\": 21,\n      \"to\": 22,\n      \"label\": \"exposes\"\n    },\n    {\n      \"id\": \"edge_23_1_1742037176209\",\n      \"from\": 23,\n      \"to\": 1,\n      \"label\": \"resolves\"\n    },\n    {\n      \"id\": \"edge_23_24_1742037176210\",\n      \"from\": 23,\n      \"to\": 24,\n      \"label\": \"exposes\"\n    },\n    {\n      \"id\": \"edge_25_1_1742037176210\",\n      \"from\": 25,\n      \"to\": 1,\n      \"label\": \"resolves\"\n    },\n    {\n      \"id\": \"edge_25_26_1742037176211\",\n      \"from\": 25,\n      \"to\": 26,\n      \"label\": \"exposes\"\n    },\n    {\n      \"id\": \"edge_27_1_1742037176211\",\n      \"from\": 27,\n      \"to\": 1,\n      \"label\": \"resolves\"\n    },\n    {\n      \"id\": \"edge_27_28_1742037176211\",\n      \"from\": 27,\n      \"to\": 28,\n      \"label\": \"exposes\"\n    },\n    {\n      \"id\": \"edge_29_1_1742037176212\",\n      \"from\": 29,\n      \"to\": 1,\n      \"label\": \"resolves\"\n    },\n    {\n      \"id\": \"edge_29_30_1742037176212\",\n      \"from\": 29,\n      \"to\": 30,\n      \"label\": \"exposes\"\n    },\n    {\n      \"id\": \"edge_31_1_1742037176212\",\n      \"from\": 31,\n      \"to\": 1,\n      \"label\": \"resolves\"\n    },\n    {\n      \"id\": \"edge_31_32_1742037176213\",\n      \"from\": 31,\n      \"to\": 32,\n      \"label\": \"exposes\"\n    },\n    {\n      \"id\": \"edge_33_1_1742037176213\",\n      \"from\": 33,\n      \"to\": 1,\n      \"label\": \"resolves\"\n    },\n    {\n      \"id\": \"edge_33_34_1742037176214\",\n      \"from\": 33,\n      \"to\": 34,\n      \"label\": \"exposes\"\n    },\n    {\n      \"id\": \"edge_35_1_1742037176214\",\n      \"from\": 35,\n      \"to\": 1,\n      \"label\": \"resolves\"\n    },\n    {\n      \"id\": \"edge_35_36_1742037176214\",\n      \"from\": 35,\n      \"to\": 36,\n      \"label\": \"exposes\"\n    },\n    {\n      \"id\": \"edge_37_1_1742037176215\",\n      \"from\": 37,\n      \"to\": 1,\n      \"label\": \"resolves\"\n    },\n    {\n      \"id\": \"edge_37_38_1742037176216\",\n      \"from\": 37,\n      \"to\": 38,\n      \"label\": \"exposes\"\n    },\n    {\n      \"id\": \"edge_39_1_1742037176216\",\n      \"from\": 39,\n      \"to\": 1,\n      \"label\": \"resolves\"\n    },\n    {\n      \"id\": \"edge_39_40_1742037176216\",\n      \"from\": 39,\n      \"to\": 40,\n      \"label\": \"exposes\"\n    },\n    {\n      \"id\": \"edge_41_1_1742037176217\",\n      \"from\": 41,\n      \"to\": 1,\n      \"label\": \"resolves\"\n    },\n    {\n      \"id\": \"edge_41_42_1742037176217\",\n      \"from\": 41,\n      \"to\": 42,\n      \"label\": \"exposes\"\n    },\n    {\n      \"id\": \"edge_43_1_1742037176217\",\n      \"from\": 43,\n      \"to\": 1,\n      \"label\": \"resolves\"\n    },\n    {\n      \"id\": \"edge_43_44_1742037176218\",\n      \"from\": 43,\n      \"to\": 44,\n      \"label\": \"exposes\"\n    },\n    {\n      \"id\": \"edge_45_1_1742037176218\",\n      \"from\": 45,\n      \"to\": 1,\n      \"label\": \"resolves\"\n    },\n    {\n      \"id\": \"edge_45_46_1742037176218\",\n      \"from\": 45,\n      \"to\": 46,\n      \"label\": \"exposes\"\n    },\n    {\n      \"id\": \"edge_47_1_1742037176219\",\n      \"from\": 47,\n      \"to\": 1,\n      \"label\": \"resolves\"\n    },\n    {\n      \"id\": \"edge_47_48_1742037176219\",\n      \"from\": 47,\n      \"to\": 48,\n      \"label\": \"exposes\"\n    }\n  ]\n}"
  },
  {
    "path": "network_graph-8.json",
    "content": "{\n  \"nodes\": [\n    {\n      \"id\": 1,\n      \"type\": \"ip\",\n      \"ip\": \"137.184.28.58\"\n    },\n    {\n      \"id\": 2,\n      \"type\": \"ip\",\n      \"ip\": \"143.244.184.81\"\n    },\n    {\n      \"id\": 3,\n      \"type\": \"ip\",\n      \"ip\": \"37.120.203.182\"\n    },\n    {\n      \"id\": 4,\n      \"type\": \"ip\",\n      \"ip\": \"165.232.84.226\"\n    },\n    {\n      \"id\": 5,\n      \"type\": \"ip\",\n      \"ip\": \"182.99.234.208\"\n    },\n    {\n      \"id\": 6,\n      \"type\": \"ip\",\n      \"ip\": \"178.176.203.190\"\n    },\n    {\n      \"id\": 7,\n      \"type\": \"ip\",\n      \"ip\": \"5.254.101.169\"\n    },\n    {\n      \"id\": 8,\n      \"type\": \"ip\",\n      \"ip\": \"182.99.246.192\"\n    },\n    {\n      \"id\": 9,\n      \"type\": \"ip\",\n      \"ip\": \"163.172.60.213\"\n    },\n    {\n      \"id\": 10,\n      \"type\": \"ip\",\n      \"ip\": \"117.189.24.153\"\n    },\n    {\n      \"id\": 11,\n      \"type\": \"ip\",\n      \"ip\": \"115.151.228.92\"\n    },\n    {\n      \"id\": 12,\n      \"type\": \"ip\",\n      \"ip\": \"180.149.231.197\"\n    },\n    {\n      \"id\": 13,\n      \"type\": \"ip\",\n      \"ip\": \"160.238.38.196\"\n    },\n    {\n      \"id\": 14,\n      \"type\": \"ip\",\n      \"ip\": \"103.107.198.108\"\n    },\n    {\n      \"id\": 15,\n      \"type\": \"ip\",\n      \"ip\": \"118.27.36.56\"\n    },\n    {\n      \"id\": 16,\n      \"type\": \"ip\",\n      \"ip\": \"68.183.207.73\"\n    },\n    {\n      \"id\": 17,\n      \"type\": \"ip\",\n      \"ip\": \"115.151.229.27\"\n    },\n    {\n      \"id\": 18,\n      \"type\": \"ip\",\n      \"ip\": \"47.74.155.101\"\n    },\n    {\n      \"id\": 19,\n      \"type\": \"ip\",\n      \"ip\": \"188.166.76.204\"\n    },\n    {\n      \"id\": 20,\n      \"type\": \"ip\",\n      \"ip\": \"185.173.92.243\"\n    },\n    {\n      \"id\": 21,\n      \"type\": \"ip\",\n      \"ip\": \"139.177.178.126\"\n    },\n    {\n      \"id\": 22,\n      \"type\": \"ip\",\n      \"ip\": \"159.203.187.141\"\n    },\n    {\n      \"id\": 23,\n      \"type\": \"ip\",\n      \"ip\": \"165.232.80.22\"\n    },\n    {\n      \"id\": 24,\n      \"type\": \"ip\",\n      \"ip\": \"217.22.18.17\"\n    },\n    {\n      \"id\": 25,\n      \"type\": \"ip\",\n      \"ip\": \"178.62.23.146\"\n    },\n    {\n      \"id\": 26,\n      \"type\": \"ip\",\n      \"ip\": \"1.209.249.188\"\n    },\n    {\n      \"id\": 27,\n      \"type\": \"ip\",\n      \"ip\": \"51.89.237.81\"\n    },\n    {\n      \"id\": 28,\n      \"type\": \"ip\",\n      \"ip\": \"121.36.213.142\"\n    },\n    {\n      \"id\": 29,\n      \"type\": \"ip\",\n      \"ip\": \"172.104.179.8\"\n    },\n    {\n      \"id\": 30,\n      \"type\": \"ip\",\n      \"ip\": \"193.239.86.7\"\n    },\n    {\n      \"id\": 31,\n      \"type\": \"ip\",\n      \"ip\": \"178.79.144.101\"\n    },\n    {\n      \"id\": 32,\n      \"type\": \"ip\",\n      \"ip\": \"172.104.161.134\"\n    },\n    {\n      \"id\": 33,\n      \"type\": \"ip\",\n      \"ip\": \"212.102.50.103\"\n    },\n    {\n      \"id\": 34,\n      \"type\": \"ip\",\n      \"ip\": \"141.226.14.14\"\n    },\n    {\n      \"id\": 35,\n      \"type\": \"ip\",\n      \"ip\": \"203.27.106.142\"\n    },\n    {\n      \"id\": 36,\n      \"type\": \"ip\",\n      \"ip\": \"211.218.126.140\"\n    },\n    {\n      \"id\": 37,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.131\"\n    },\n    {\n      \"id\": 38,\n      \"type\": \"ip\",\n      \"ip\": \"185.7.33.36\"\n    },\n    {\n      \"id\": 39,\n      \"type\": \"ip\",\n      \"ip\": \"139.28.218.134\"\n    },\n    {\n      \"id\": 40,\n      \"type\": \"ip\",\n      \"ip\": \"115.151.228.146\"\n    },\n    {\n      \"id\": 41,\n      \"type\": \"ip\",\n      \"ip\": \"37.19.213.150\"\n    },\n    {\n      \"id\": 42,\n      \"type\": \"ip\",\n      \"ip\": \"172.241.167.37\"\n    },\n    {\n      \"id\": 43,\n      \"type\": \"ip\",\n      \"ip\": \"182.99.247.75\"\n    },\n    {\n      \"id\": 44,\n      \"type\": \"ip\",\n      \"ip\": \"45.77.33.141\"\n    },\n    {\n      \"id\": 45,\n      \"type\": \"ip\",\n      \"ip\": \"115.151.228.83\"\n    },\n    {\n      \"id\": 46,\n      \"type\": \"ip\",\n      \"ip\": \"165.232.80.166\"\n    },\n    {\n      \"id\": 47,\n      \"type\": \"ip\",\n      \"ip\": \"59.110.17.191\"\n    },\n    {\n      \"id\": 48,\n      \"type\": \"ip\",\n      \"ip\": \"101.71.38.231\"\n    },\n    {\n      \"id\": 49,\n      \"type\": \"ip\",\n      \"ip\": \"172.105.49.45\"\n    },\n    {\n      \"id\": 50,\n      \"type\": \"ip\",\n      \"ip\": \"172.105.98.92\"\n    },\n    {\n      \"id\": 51,\n      \"type\": \"ip\",\n      \"ip\": \"141.105.65.94\"\n    },\n    {\n      \"id\": 52,\n      \"type\": \"ip\",\n      \"ip\": \"180.149.231.196\"\n    },\n    {\n      \"id\": 53,\n      \"type\": \"ip\",\n      \"ip\": \"45.56.87.129\"\n    },\n    {\n      \"id\": 54,\n      \"type\": \"ip\",\n      \"ip\": \"176.119.195.5\"\n    },\n    {\n      \"id\": 55,\n      \"type\": \"ip\",\n      \"ip\": \"167.172.249.78\"\n    },\n    {\n      \"id\": 56,\n      \"type\": \"ip\",\n      \"ip\": \"106.192.82.193\"\n    },\n    {\n      \"id\": 57,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.67.235\"\n    },\n    {\n      \"id\": 58,\n      \"type\": \"ip\",\n      \"ip\": \"139.162.161.61\"\n    },\n    {\n      \"id\": 59,\n      \"type\": \"ip\",\n      \"ip\": \"192.46.237.121\"\n    },\n    {\n      \"id\": 60,\n      \"type\": \"ip\",\n      \"ip\": \"159.203.56.4\"\n    },\n    {\n      \"id\": 61,\n      \"type\": \"ip\",\n      \"ip\": \"36.231.115.58\"\n    },\n    {\n      \"id\": 62,\n      \"type\": \"ip\",\n      \"ip\": \"114.37.223.180\"\n    },\n    {\n      \"id\": 63,\n      \"type\": \"ip\",\n      \"ip\": \"189.188.33.125\"\n    },\n    {\n      \"id\": 64,\n      \"type\": \"ip\",\n      \"ip\": \"185.202.220.27\"\n    },\n    {\n      \"id\": 65,\n      \"type\": \"ip\",\n      \"ip\": \"192.46.237.123\"\n    },\n    {\n      \"id\": 66,\n      \"type\": \"ip\",\n      \"ip\": \"82.102.18.110\"\n    },\n    {\n      \"id\": 67,\n      \"type\": \"ip\",\n      \"ip\": \"203.175.12.88\"\n    },\n    {\n      \"id\": 68,\n      \"type\": \"ip\",\n      \"ip\": \"139.177.178.140\"\n    },\n    {\n      \"id\": 69,\n      \"type\": \"ip\",\n      \"ip\": \"45.137.21.9\"\n    },\n    {\n      \"id\": 70,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.97.211\"\n    },\n    {\n      \"id\": 71,\n      \"type\": \"ip\",\n      \"ip\": \"139.177.178.144\"\n    },\n    {\n      \"id\": 72,\n      \"type\": \"ip\",\n      \"ip\": \"106.192.210.1\"\n    },\n    {\n      \"id\": 73,\n      \"type\": \"ip\",\n      \"ip\": \"138.68.247.241\"\n    },\n    {\n      \"id\": 74,\n      \"type\": \"ip\",\n      \"ip\": \"139.177.180.114\"\n    },\n    {\n      \"id\": 75,\n      \"type\": \"ip\",\n      \"ip\": \"146.59.45.142\"\n    },\n    {\n      \"id\": 76,\n      \"type\": \"ip\",\n      \"ip\": \"172.104.236.129\"\n    },\n    {\n      \"id\": 77,\n      \"type\": \"ip\",\n      \"ip\": \"89.38.69.99\"\n    },\n    {\n      \"id\": 78,\n      \"type\": \"ip\",\n      \"ip\": \"195.80.151.30\"\n    },\n    {\n      \"id\": 79,\n      \"type\": \"ip\",\n      \"ip\": \"191.101.132.152\"\n    },\n    {\n      \"id\": 80,\n      \"type\": \"ip\",\n      \"ip\": \"5.181.235.45\"\n    },\n    {\n      \"id\": 81,\n      \"type\": \"ip\",\n      \"ip\": \"209.141.41.62\"\n    },\n    {\n      \"id\": 82,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.102.23\"\n    },\n    {\n      \"id\": 83,\n      \"type\": \"ip\",\n      \"ip\": \"68.183.203.184\"\n    },\n    {\n      \"id\": 84,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.97.119\"\n    },\n    {\n      \"id\": 85,\n      \"type\": \"ip\",\n      \"ip\": \"138.68.242.144\"\n    },\n    {\n      \"id\": 86,\n      \"type\": \"ip\",\n      \"ip\": \"163.172.160.209\"\n    },\n    {\n      \"id\": 87,\n      \"type\": \"ip\",\n      \"ip\": \"64.227.188.224\"\n    },\n    {\n      \"id\": 88,\n      \"type\": \"ip\",\n      \"ip\": \"51.83.138.78\"\n    },\n    {\n      \"id\": 89,\n      \"type\": \"ip\",\n      \"ip\": \"198.58.104.209\"\n    },\n    {\n      \"id\": 90,\n      \"type\": \"ip\",\n      \"ip\": \"138.68.4.129\"\n    },\n    {\n      \"id\": 91,\n      \"type\": \"ip\",\n      \"ip\": \"167.179.65.77\"\n    },\n    {\n      \"id\": 92,\n      \"type\": \"ip\",\n      \"ip\": \"138.197.175.206\"\n    },\n    {\n      \"id\": 93,\n      \"type\": \"ip\",\n      \"ip\": \"68.183.192.74\"\n    },\n    {\n      \"id\": 94,\n      \"type\": \"ip\",\n      \"ip\": \"134.209.24.42\"\n    },\n    {\n      \"id\": 95,\n      \"type\": \"ip\",\n      \"ip\": \"193.189.100.195\"\n    },\n    {\n      \"id\": 96,\n      \"type\": \"ip\",\n      \"ip\": \"167.71.71.229\"\n    },\n    {\n      \"id\": 97,\n      \"type\": \"ip\",\n      \"ip\": \"178.62.32.211\"\n    },\n    {\n      \"id\": 98,\n      \"type\": \"ip\",\n      \"ip\": \"194.233.164.98\"\n    },\n    {\n      \"id\": 99,\n      \"type\": \"ip\",\n      \"ip\": \"139.177.178.164\"\n    },\n    {\n      \"id\": 100,\n      \"type\": \"ip\",\n      \"ip\": \"138.197.167.229\"\n    },\n    {\n      \"id\": 101,\n      \"type\": \"ip\",\n      \"ip\": \"64.227.178.29\"\n    },\n    {\n      \"id\": 102,\n      \"type\": \"ip\",\n      \"ip\": \"37.19.213.148\"\n    },\n    {\n      \"id\": 103,\n      \"type\": \"ip\",\n      \"ip\": \"195.110.6.48\"\n    },\n    {\n      \"id\": 104,\n      \"type\": \"ip\",\n      \"ip\": \"46.194.138.182\"\n    },\n    {\n      \"id\": 105,\n      \"type\": \"ip\",\n      \"ip\": \"35.203.96.122\"\n    },\n    {\n      \"id\": 106,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.5.68\"\n    },\n    {\n      \"id\": 107,\n      \"type\": \"ip\",\n      \"ip\": \"109.70.100.36\"\n    },\n    {\n      \"id\": 108,\n      \"type\": \"ip\",\n      \"ip\": \"109.70.100.26\"\n    },\n    {\n      \"id\": 109,\n      \"type\": \"ip\",\n      \"ip\": \"64.227.188.251\"\n    },\n    {\n      \"id\": 110,\n      \"type\": \"ip\",\n      \"ip\": \"109.70.100.27\"\n    },\n    {\n      \"id\": 111,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.3.246\"\n    },\n    {\n      \"id\": 112,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.76.180\"\n    },\n    {\n      \"id\": 113,\n      \"type\": \"ip\",\n      \"ip\": \"194.233.164.129\"\n    },\n    {\n      \"id\": 114,\n      \"type\": \"ip\",\n      \"ip\": \"195.251.41.139\"\n    },\n    {\n      \"id\": 115,\n      \"type\": \"ip\",\n      \"ip\": \"138.68.231.58\"\n    },\n    {\n      \"id\": 116,\n      \"type\": \"ip\",\n      \"ip\": \"188.166.168.128\"\n    },\n    {\n      \"id\": 117,\n      \"type\": \"ip\",\n      \"ip\": \"138.197.151.200\"\n    },\n    {\n      \"id\": 118,\n      \"type\": \"ip\",\n      \"ip\": \"103.107.198.110\"\n    },\n    {\n      \"id\": 119,\n      \"type\": \"ip\",\n      \"ip\": \"51.255.106.85\"\n    },\n    {\n      \"id\": 120,\n      \"type\": \"ip\",\n      \"ip\": \"162.247.74.201\"\n    },\n    {\n      \"id\": 121,\n      \"type\": \"ip\",\n      \"ip\": \"165.227.31.212\"\n    },\n    {\n      \"id\": 122,\n      \"type\": \"ip\",\n      \"ip\": \"82.146.55.139\"\n    },\n    {\n      \"id\": 123,\n      \"type\": \"ip\",\n      \"ip\": \"198.98.51.222\"\n    },\n    {\n      \"id\": 124,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.14.98\"\n    },\n    {\n      \"id\": 125,\n      \"type\": \"ip\",\n      \"ip\": \"194.110.84.93\"\n    },\n    {\n      \"id\": 126,\n      \"type\": \"ip\",\n      \"ip\": \"61.19.24.122\"\n    },\n    {\n      \"id\": 127,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.72.168\"\n    },\n    {\n      \"id\": 128,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.102.248\"\n    },\n    {\n      \"id\": 129,\n      \"type\": \"ip\",\n      \"ip\": \"31.210.20.110\"\n    },\n    {\n      \"id\": 130,\n      \"type\": \"ip\",\n      \"ip\": \"89.163.252.230\"\n    },\n    {\n      \"id\": 131,\n      \"type\": \"ip\",\n      \"ip\": \"195.206.105.217\"\n    },\n    {\n      \"id\": 132,\n      \"type\": \"ip\",\n      \"ip\": \"109.70.100.35\"\n    },\n    {\n      \"id\": 133,\n      \"type\": \"ip\",\n      \"ip\": \"213.164.204.146\"\n    },\n    {\n      \"id\": 134,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.102.250\"\n    },\n    {\n      \"id\": 135,\n      \"type\": \"ip\",\n      \"ip\": \"213.203.177.219\"\n    },\n    {\n      \"id\": 136,\n      \"type\": \"ip\",\n      \"ip\": \"113.141.64.14\"\n    },\n    {\n      \"id\": 137,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.72.129\"\n    },\n    {\n      \"id\": 138,\n      \"type\": \"ip\",\n      \"ip\": \"45.153.160.133\"\n    },\n    {\n      \"id\": 139,\n      \"type\": \"ip\",\n      \"ip\": \"170.210.45.163\"\n    },\n    {\n      \"id\": 140,\n      \"type\": \"ip\",\n      \"ip\": \"146.56.148.181\"\n    },\n    {\n      \"id\": 141,\n      \"type\": \"ip\",\n      \"ip\": \"45.13.104.179\"\n    },\n    {\n      \"id\": 142,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.73.8\"\n    },\n    {\n      \"id\": 143,\n      \"type\": \"ip\",\n      \"ip\": \"23.154.177.2\"\n    },\n    {\n      \"id\": 144,\n      \"type\": \"ip\",\n      \"ip\": \"68.183.41.150\"\n    },\n    {\n      \"id\": 145,\n      \"type\": \"ip\",\n      \"ip\": \"51.195.166.171\"\n    },\n    {\n      \"id\": 146,\n      \"type\": \"ip\",\n      \"ip\": \"196.196.150.38\"\n    },\n    {\n      \"id\": 147,\n      \"type\": \"ip\",\n      \"ip\": \"146.56.131.161\"\n    },\n    {\n      \"id\": 148,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.75.225\"\n    },\n    {\n      \"id\": 149,\n      \"type\": \"ip\",\n      \"ip\": \"140.246.171.141\"\n    },\n    {\n      \"id\": 150,\n      \"type\": \"ip\",\n      \"ip\": \"107.170.69.93\"\n    },\n    {\n      \"id\": 151,\n      \"type\": \"ip\",\n      \"ip\": \"178.176.202.121\"\n    },\n    {\n      \"id\": 152,\n      \"type\": \"ip\",\n      \"ip\": \"192.42.116.26\"\n    },\n    {\n      \"id\": 153,\n      \"type\": \"ip\",\n      \"ip\": \"185.129.61.4\"\n    },\n    {\n      \"id\": 154,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.29.41\"\n    },\n    {\n      \"id\": 155,\n      \"type\": \"ip\",\n      \"ip\": \"45.144.225.119\"\n    },\n    {\n      \"id\": 156,\n      \"type\": \"ip\",\n      \"ip\": \"89.249.63.3\"\n    },\n    {\n      \"id\": 157,\n      \"type\": \"ip\",\n      \"ip\": \"175.6.210.66\"\n    },\n    {\n      \"id\": 158,\n      \"type\": \"ip\",\n      \"ip\": \"144.217.86.109\"\n    },\n    {\n      \"id\": 159,\n      \"type\": \"ip\",\n      \"ip\": \"81.17.18.61\"\n    },\n    {\n      \"id\": 160,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.102.245\"\n    },\n    {\n      \"id\": 161,\n      \"type\": \"ip\",\n      \"ip\": \"91.250.242.12\"\n    },\n    {\n      \"id\": 162,\n      \"type\": \"ip\",\n      \"ip\": \"185.65.205.10\"\n    },\n    {\n      \"id\": 163,\n      \"type\": \"ip\",\n      \"ip\": \"188.68.62.150\"\n    },\n    {\n      \"id\": 164,\n      \"type\": \"ip\",\n      \"ip\": \"45.153.160.136\"\n    },\n    {\n      \"id\": 165,\n      \"type\": \"ip\",\n      \"ip\": \"150.158.189.96\"\n    },\n    {\n      \"id\": 166,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.129\"\n    },\n    {\n      \"id\": 167,\n      \"type\": \"ip\",\n      \"ip\": \"1.116.59.211\"\n    },\n    {\n      \"id\": 168,\n      \"type\": \"ip\",\n      \"ip\": \"109.70.100.22\"\n    },\n    {\n      \"id\": 169,\n      \"type\": \"ip\",\n      \"ip\": \"192.42.116.23\"\n    },\n    {\n      \"id\": 170,\n      \"type\": \"ip\",\n      \"ip\": \"203.160.52.160\"\n    },\n    {\n      \"id\": 171,\n      \"type\": \"ip\",\n      \"ip\": \"49.7.224.217\"\n    },\n    {\n      \"id\": 172,\n      \"type\": \"ip\",\n      \"ip\": \"1.179.247.182\"\n    },\n    {\n      \"id\": 173,\n      \"type\": \"ip\",\n      \"ip\": \"122.161.50.23\"\n    },\n    {\n      \"id\": 174,\n      \"type\": \"ip\",\n      \"ip\": \"185.232.23.46\"\n    },\n    {\n      \"id\": 175,\n      \"type\": \"ip\",\n      \"ip\": \"20.205.104.227\"\n    },\n    {\n      \"id\": 176,\n      \"type\": \"ip\",\n      \"ip\": \"143.198.183.66\"\n    },\n    {\n      \"id\": 177,\n      \"type\": \"ip\",\n      \"ip\": \"182.99.247.122\"\n    },\n    {\n      \"id\": 178,\n      \"type\": \"ip\",\n      \"ip\": \"188.241.156.221\"\n    },\n    {\n      \"id\": 179,\n      \"type\": \"ip\",\n      \"ip\": \"182.99.247.145\"\n    },\n    {\n      \"id\": 180,\n      \"type\": \"ip\",\n      \"ip\": \"139.59.182.104\"\n    },\n    {\n      \"id\": 181,\n      \"type\": \"ip\",\n      \"ip\": \"138.197.106.234\"\n    },\n    {\n      \"id\": 182,\n      \"type\": \"ip\",\n      \"ip\": \"182.99.246.106\"\n    },\n    {\n      \"id\": 183,\n      \"type\": \"ip\",\n      \"ip\": \"164.92.254.33\"\n    },\n    {\n      \"id\": 184,\n      \"type\": \"ip\",\n      \"ip\": \"42.98.70.127\"\n    },\n    {\n      \"id\": 185,\n      \"type\": \"ip\",\n      \"ip\": \"18.116.198.193\"\n    },\n    {\n      \"id\": 186,\n      \"type\": \"ip\",\n      \"ip\": \"115.151.229.16\"\n    },\n    {\n      \"id\": 187,\n      \"type\": \"ip\",\n      \"ip\": \"188.166.225.104\"\n    },\n    {\n      \"id\": 188,\n      \"type\": \"ip\",\n      \"ip\": \"138.197.72.76\"\n    },\n    {\n      \"id\": 189,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.102.255\"\n    },\n    {\n      \"id\": 190,\n      \"type\": \"ip\",\n      \"ip\": \"209.58.185.232\"\n    },\n    {\n      \"id\": 191,\n      \"type\": \"ip\",\n      \"ip\": \"185.244.214.216\"\n    },\n    {\n      \"id\": 192,\n      \"type\": \"ip\",\n      \"ip\": \"115.60.20.9\"\n    },\n    {\n      \"id\": 193,\n      \"type\": \"ip\",\n      \"ip\": \"138.199.21.10\"\n    },\n    {\n      \"id\": 194,\n      \"type\": \"ip\",\n      \"ip\": \"159.89.131.28\"\n    },\n    {\n      \"id\": 195,\n      \"type\": \"ip\",\n      \"ip\": \"192.243.124.39\"\n    },\n    {\n      \"id\": 196,\n      \"type\": \"ip\",\n      \"ip\": \"23.224.189.53\"\n    },\n    {\n      \"id\": 197,\n      \"type\": \"ip\",\n      \"ip\": \"139.177.177.111\"\n    },\n    {\n      \"id\": 198,\n      \"type\": \"ip\",\n      \"ip\": \"89.238.178.212\"\n    },\n    {\n      \"id\": 199,\n      \"type\": \"ip\",\n      \"ip\": \"199.249.230.172\"\n    },\n    {\n      \"id\": 200,\n      \"type\": \"ip\",\n      \"ip\": \"62.182.80.168\"\n    },\n    {\n      \"id\": 201,\n      \"type\": \"ip\",\n      \"ip\": \"139.177.178.127\"\n    },\n    {\n      \"id\": 202,\n      \"type\": \"ip\",\n      \"ip\": \"87.251.64.10\"\n    },\n    {\n      \"id\": 203,\n      \"type\": \"ip\",\n      \"ip\": \"192.46.237.113\"\n    },\n    {\n      \"id\": 204,\n      \"type\": \"ip\",\n      \"ip\": \"210.6.176.90\"\n    },\n    {\n      \"id\": 205,\n      \"type\": \"ip\",\n      \"ip\": \"139.162.228.124\"\n    },\n    {\n      \"id\": 206,\n      \"type\": \"ip\",\n      \"ip\": \"203.175.12.97\"\n    },\n    {\n      \"id\": 207,\n      \"type\": \"ip\",\n      \"ip\": \"109.69.67.17\"\n    },\n    {\n      \"id\": 208,\n      \"type\": \"ip\",\n      \"ip\": \"89.163.144.156\"\n    },\n    {\n      \"id\": 209,\n      \"type\": \"ip\",\n      \"ip\": \"143.110.216.75\"\n    },\n    {\n      \"id\": 210,\n      \"type\": \"ip\",\n      \"ip\": \"103.232.137.187\"\n    },\n    {\n      \"id\": 211,\n      \"type\": \"ip\",\n      \"ip\": \"146.70.75.21\"\n    },\n    {\n      \"id\": 212,\n      \"type\": \"ip\",\n      \"ip\": \"45.56.87.181\"\n    },\n    {\n      \"id\": 213,\n      \"type\": \"ip\",\n      \"ip\": \"172.105.49.109\"\n    },\n    {\n      \"id\": 214,\n      \"type\": \"ip\",\n      \"ip\": \"5.15.139.129\"\n    },\n    {\n      \"id\": 215,\n      \"type\": \"ip\",\n      \"ip\": \"89.187.161.35\"\n    },\n    {\n      \"id\": 216,\n      \"type\": \"ip\",\n      \"ip\": \"44.192.244.182\"\n    },\n    {\n      \"id\": 217,\n      \"type\": \"ip\",\n      \"ip\": \"167.71.4.81\"\n    },\n    {\n      \"id\": 218,\n      \"type\": \"ip\",\n      \"ip\": \"46.167.72.179\"\n    },\n    {\n      \"id\": 219,\n      \"type\": \"ip\",\n      \"ip\": \"39.102.236.51\"\n    },\n    {\n      \"id\": 220,\n      \"type\": \"ip\",\n      \"ip\": \"23.120.182.121\"\n    },\n    {\n      \"id\": 221,\n      \"type\": \"ip\",\n      \"ip\": \"120.211.140.116\"\n    },\n    {\n      \"id\": 222,\n      \"type\": \"ip\",\n      \"ip\": \"109.73.65.32\"\n    },\n    {\n      \"id\": 223,\n      \"type\": \"ip\",\n      \"ip\": \"182.99.246.179\"\n    },\n    {\n      \"id\": 224,\n      \"type\": \"ip\",\n      \"ip\": \"101.71.37.47\"\n    },\n    {\n      \"id\": 225,\n      \"type\": \"ip\",\n      \"ip\": \"182.99.246.199\"\n    },\n    {\n      \"id\": 226,\n      \"type\": \"ip\",\n      \"ip\": \"114.112.161.155\"\n    },\n    {\n      \"id\": 227,\n      \"type\": \"ip\",\n      \"ip\": \"42.159.91.12\"\n    },\n    {\n      \"id\": 228,\n      \"type\": \"ip\",\n      \"ip\": \"188.166.102.47\"\n    },\n    {\n      \"id\": 229,\n      \"type\": \"ip\",\n      \"ip\": \"167.99.186.227\"\n    },\n    {\n      \"id\": 230,\n      \"type\": \"ip\",\n      \"ip\": \"177.131.174.12\"\n    },\n    {\n      \"id\": 231,\n      \"type\": \"ip\",\n      \"ip\": \"68.183.33.144\"\n    },\n    {\n      \"id\": 232,\n      \"type\": \"ip\",\n      \"ip\": \"45.153.230.20\"\n    },\n    {\n      \"id\": 233,\n      \"type\": \"ip\",\n      \"ip\": \"159.89.133.216\"\n    },\n    {\n      \"id\": 234,\n      \"type\": \"ip\",\n      \"ip\": \"89.187.161.208\"\n    },\n    {\n      \"id\": 235,\n      \"type\": \"ip\",\n      \"ip\": \"66.228.32.204\"\n    },\n    {\n      \"id\": 236,\n      \"type\": \"ip\",\n      \"ip\": \"139.177.178.15\"\n    },\n    {\n      \"id\": 237,\n      \"type\": \"ip\",\n      \"ip\": \"172.105.69.55\"\n    },\n    {\n      \"id\": 238,\n      \"type\": \"ip\",\n      \"ip\": \"172.105.64.5\"\n    },\n    {\n      \"id\": 239,\n      \"type\": \"ip\",\n      \"ip\": \"109.74.192.52\"\n    },\n    {\n      \"id\": 240,\n      \"type\": \"ip\",\n      \"ip\": \"165.227.37.189\"\n    },\n    {\n      \"id\": 241,\n      \"type\": \"ip\",\n      \"ip\": \"139.59.237.99\"\n    },\n    {\n      \"id\": 242,\n      \"type\": \"ip\",\n      \"ip\": \"207.148.64.14\"\n    },\n    {\n      \"id\": 243,\n      \"type\": \"ip\",\n      \"ip\": \"198.211.103.63\"\n    },\n    {\n      \"id\": 244,\n      \"type\": \"ip\",\n      \"ip\": \"209.141.36.177\"\n    },\n    {\n      \"id\": 245,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.97.81\"\n    },\n    {\n      \"id\": 246,\n      \"type\": \"ip\",\n      \"ip\": \"128.90.61.199\"\n    },\n    {\n      \"id\": 247,\n      \"type\": \"ip\",\n      \"ip\": \"172.104.152.7\"\n    },\n    {\n      \"id\": 248,\n      \"type\": \"ip\",\n      \"ip\": \"185.82.126.222\"\n    },\n    {\n      \"id\": 249,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.97.31\"\n    },\n    {\n      \"id\": 250,\n      \"type\": \"ip\",\n      \"ip\": \"62.171.142.3\"\n    },\n    {\n      \"id\": 251,\n      \"type\": \"ip\",\n      \"ip\": \"108.61.210.108\"\n    },\n    {\n      \"id\": 252,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.98.251\"\n    },\n    {\n      \"id\": 253,\n      \"type\": \"ip\",\n      \"ip\": \"194.233.164.81\"\n    },\n    {\n      \"id\": 254,\n      \"type\": \"ip\",\n      \"ip\": \"134.122.47.205\"\n    },\n    {\n      \"id\": 255,\n      \"type\": \"ip\",\n      \"ip\": \"147.135.105.62\"\n    },\n    {\n      \"id\": 256,\n      \"type\": \"ip\",\n      \"ip\": \"138.197.197.97\"\n    },\n    {\n      \"id\": 257,\n      \"type\": \"ip\",\n      \"ip\": \"143.110.208.84\"\n    },\n    {\n      \"id\": 258,\n      \"type\": \"ip\",\n      \"ip\": \"169.55.4.38\"\n    },\n    {\n      \"id\": 259,\n      \"type\": \"ip\",\n      \"ip\": \"194.195.244.207\"\n    },\n    {\n      \"id\": 260,\n      \"type\": \"ip\",\n      \"ip\": \"112.74.185.158\"\n    },\n    {\n      \"id\": 261,\n      \"type\": \"ip\",\n      \"ip\": \"159.89.139.34\"\n    },\n    {\n      \"id\": 262,\n      \"type\": \"ip\",\n      \"ip\": \"178.63.95.120\"\n    },\n    {\n      \"id\": 263,\n      \"type\": \"ip\",\n      \"ip\": \"143.110.208.87\"\n    },\n    {\n      \"id\": 264,\n      \"type\": \"ip\",\n      \"ip\": \"165.22.196.189\"\n    },\n    {\n      \"id\": 265,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.102.226\"\n    },\n    {\n      \"id\": 266,\n      \"type\": \"ip\",\n      \"ip\": \"138.68.22.2\"\n    },\n    {\n      \"id\": 267,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.110.124\"\n    },\n    {\n      \"id\": 268,\n      \"type\": \"ip\",\n      \"ip\": \"165.22.231.66\"\n    },\n    {\n      \"id\": 269,\n      \"type\": \"ip\",\n      \"ip\": \"138.68.249.132\"\n    },\n    {\n      \"id\": 270,\n      \"type\": \"ip\",\n      \"ip\": \"194.233.164.117\"\n    },\n    {\n      \"id\": 271,\n      \"type\": \"ip\",\n      \"ip\": \"157.245.40.77\"\n    },\n    {\n      \"id\": 272,\n      \"type\": \"ip\",\n      \"ip\": \"45.137.10.201\"\n    },\n    {\n      \"id\": 273,\n      \"type\": \"ip\",\n      \"ip\": \"138.197.170.130\"\n    },\n    {\n      \"id\": 274,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.79.52\"\n    },\n    {\n      \"id\": 275,\n      \"type\": \"ip\",\n      \"ip\": \"112.74.52.90\"\n    },\n    {\n      \"id\": 276,\n      \"type\": \"ip\",\n      \"ip\": \"134.122.39.124\"\n    },\n    {\n      \"id\": 277,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.23\"\n    },\n    {\n      \"id\": 278,\n      \"type\": \"ip\",\n      \"ip\": \"134.122.22.252\"\n    },\n    {\n      \"id\": 279,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.78.168\"\n    },\n    {\n      \"id\": 280,\n      \"type\": \"ip\",\n      \"ip\": \"147.182.191.32\"\n    },\n    {\n      \"id\": 281,\n      \"type\": \"ip\",\n      \"ip\": \"143.244.156.104\"\n    },\n    {\n      \"id\": 282,\n      \"type\": \"ip\",\n      \"ip\": \"37.19.212.88\"\n    },\n    {\n      \"id\": 283,\n      \"type\": \"ip\",\n      \"ip\": \"209.141.45.168\"\n    },\n    {\n      \"id\": 284,\n      \"type\": \"ip\",\n      \"ip\": \"197.246.171.41\"\n    },\n    {\n      \"id\": 285,\n      \"type\": \"ip\",\n      \"ip\": \"217.160.174.204\"\n    },\n    {\n      \"id\": 286,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.74.211\"\n    },\n    {\n      \"id\": 287,\n      \"type\": \"ip\",\n      \"ip\": \"198.98.56.60\"\n    },\n    {\n      \"id\": 288,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.73.43\"\n    },\n    {\n      \"id\": 289,\n      \"type\": \"ip\",\n      \"ip\": \"193.189.100.201\"\n    },\n    {\n      \"id\": 290,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.14.182\"\n    },\n    {\n      \"id\": 291,\n      \"type\": \"ip\",\n      \"ip\": \"87.118.110.27\"\n    },\n    {\n      \"id\": 292,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.78.213\"\n    },\n    {\n      \"id\": 293,\n      \"type\": \"ip\",\n      \"ip\": \"159.203.29.42\"\n    },\n    {\n      \"id\": 294,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.110.157\"\n    },\n    {\n      \"id\": 295,\n      \"type\": \"ip\",\n      \"ip\": \"194.233.164.127\"\n    },\n    {\n      \"id\": 296,\n      \"type\": \"ip\",\n      \"ip\": \"139.177.178.165\"\n    },\n    {\n      \"id\": 297,\n      \"type\": \"ip\",\n      \"ip\": \"109.70.100.32\"\n    },\n    {\n      \"id\": 298,\n      \"type\": \"ip\",\n      \"ip\": \"143.110.208.212\"\n    },\n    {\n      \"id\": 299,\n      \"type\": \"ip\",\n      \"ip\": \"192.42.116.25\"\n    },\n    {\n      \"id\": 300,\n      \"type\": \"ip\",\n      \"ip\": \"91.219.236.197\"\n    },\n    {\n      \"id\": 301,\n      \"type\": \"ip\",\n      \"ip\": \"45.61.187.205\"\n    },\n    {\n      \"id\": 302,\n      \"type\": \"ip\",\n      \"ip\": \"5.2.72.73\"\n    },\n    {\n      \"id\": 303,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.103.5\"\n    },\n    {\n      \"id\": 304,\n      \"type\": \"ip\",\n      \"ip\": \"58.18.59.179\"\n    },\n    {\n      \"id\": 305,\n      \"type\": \"ip\",\n      \"ip\": \"193.239.232.102\"\n    },\n    {\n      \"id\": 306,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.102.6\"\n    },\n    {\n      \"id\": 307,\n      \"type\": \"ip\",\n      \"ip\": \"151.115.60.113\"\n    },\n    {\n      \"id\": 308,\n      \"type\": \"ip\",\n      \"ip\": \"216.218.134.12\"\n    },\n    {\n      \"id\": 309,\n      \"type\": \"ip\",\n      \"ip\": \"101.35.154.34\"\n    },\n    {\n      \"id\": 310,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.12.7\"\n    },\n    {\n      \"id\": 311,\n      \"type\": \"ip\",\n      \"ip\": \"185.129.61.5\"\n    },\n    {\n      \"id\": 312,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.187\"\n    },\n    {\n      \"id\": 313,\n      \"type\": \"ip\",\n      \"ip\": \"94.142.241.194\"\n    },\n    {\n      \"id\": 314,\n      \"type\": \"ip\",\n      \"ip\": \"191.232.38.25\"\n    },\n    {\n      \"id\": 315,\n      \"type\": \"ip\",\n      \"ip\": \"194.135.33.152\"\n    },\n    {\n      \"id\": 316,\n      \"type\": \"ip\",\n      \"ip\": \"23.154.177.3\"\n    },\n    {\n      \"id\": 317,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.103.6\"\n    },\n    {\n      \"id\": 318,\n      \"type\": \"ip\",\n      \"ip\": \"218.89.222.71\"\n    },\n    {\n      \"id\": 319,\n      \"type\": \"ip\",\n      \"ip\": \"195.176.3.19\"\n    },\n    {\n      \"id\": 320,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.12.238\"\n    },\n    {\n      \"id\": 321,\n      \"type\": \"ip\",\n      \"ip\": \"46.105.95.220\"\n    },\n    {\n      \"id\": 322,\n      \"type\": \"ip\",\n      \"ip\": \"194.88.143.66\"\n    },\n    {\n      \"id\": 323,\n      \"type\": \"ip\",\n      \"ip\": \"31.42.185.24\"\n    },\n    {\n      \"id\": 324,\n      \"type\": \"ip\",\n      \"ip\": \"86.109.208.194\"\n    },\n    {\n      \"id\": 325,\n      \"type\": \"ip\",\n      \"ip\": \"37.221.66.128\"\n    },\n    {\n      \"id\": 326,\n      \"type\": \"ip\",\n      \"ip\": \"89.163.154.91\"\n    },\n    {\n      \"id\": 327,\n      \"type\": \"ip\",\n      \"ip\": \"67.207.93.79\"\n    },\n    {\n      \"id\": 328,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.4.253\"\n    },\n    {\n      \"id\": 329,\n      \"type\": \"ip\",\n      \"ip\": \"5.199.143.202\"\n    },\n    {\n      \"id\": 330,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.186\"\n    },\n    {\n      \"id\": 331,\n      \"type\": \"ip\",\n      \"ip\": \"192.42.116.20\"\n    },\n    {\n      \"id\": 332,\n      \"type\": \"ip\",\n      \"ip\": \"179.43.187.138\"\n    },\n    {\n      \"id\": 333,\n      \"type\": \"ip\",\n      \"ip\": \"5.2.69.50\"\n    },\n    {\n      \"id\": 334,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.178\"\n    },\n    {\n      \"id\": 335,\n      \"type\": \"ip\",\n      \"ip\": \"195.254.135.76\"\n    },\n    {\n      \"id\": 336,\n      \"type\": \"ip\",\n      \"ip\": \"51.15.76.60\"\n    },\n    {\n      \"id\": 337,\n      \"type\": \"ip\",\n      \"ip\": \"221.226.159.22\"\n    },\n    {\n      \"id\": 338,\n      \"type\": \"ip\",\n      \"ip\": \"171.221.235.43\"\n    },\n    {\n      \"id\": 339,\n      \"type\": \"ip\",\n      \"ip\": \"198.144.121.43\"\n    },\n    {\n      \"id\": 340,\n      \"type\": \"ip\",\n      \"ip\": \"185.165.171.175\"\n    },\n    {\n      \"id\": 341,\n      \"type\": \"ip\",\n      \"ip\": \"147.182.216.21\"\n    },\n    {\n      \"id\": 342,\n      \"type\": \"ip\",\n      \"ip\": \"212.102.50.87\"\n    },\n    {\n      \"id\": 343,\n      \"type\": \"ip\",\n      \"ip\": \"89.238.178.213\"\n    },\n    {\n      \"id\": 344,\n      \"type\": \"ip\",\n      \"ip\": \"197.246.171.83\"\n    },\n    {\n      \"id\": 345,\n      \"type\": \"ip\",\n      \"ip\": \"37.120.199.196\"\n    },\n    {\n      \"id\": 346,\n      \"type\": \"ip\",\n      \"ip\": \"167.71.1.144\"\n    },\n    {\n      \"id\": 347,\n      \"type\": \"ip\",\n      \"ip\": \"142.93.157.150\"\n    },\n    {\n      \"id\": 348,\n      \"type\": \"ip\",\n      \"ip\": \"210.217.18.76\"\n    },\n    {\n      \"id\": 349,\n      \"type\": \"ip\",\n      \"ip\": \"159.89.146.147\"\n    },\n    {\n      \"id\": 350,\n      \"type\": \"ip\",\n      \"ip\": \"115.151.228.18\"\n    },\n    {\n      \"id\": 351,\n      \"type\": \"ip\",\n      \"ip\": \"139.59.99.80\"\n    },\n    {\n      \"id\": 352,\n      \"type\": \"ip\",\n      \"ip\": \"139.59.188.119\"\n    },\n    {\n      \"id\": 353,\n      \"type\": \"ip\",\n      \"ip\": \"167.99.221.217\"\n    },\n    {\n      \"id\": 354,\n      \"type\": \"ip\",\n      \"ip\": \"213.152.188.4\"\n    },\n    {\n      \"id\": 355,\n      \"type\": \"ip\",\n      \"ip\": \"139.59.101.242\"\n    },\n    {\n      \"id\": 356,\n      \"type\": \"ip\",\n      \"ip\": \"167.99.44.32\"\n    },\n    {\n      \"id\": 357,\n      \"type\": \"ip\",\n      \"ip\": \"182.99.247.67\"\n    },\n    {\n      \"id\": 358,\n      \"type\": \"ip\",\n      \"ip\": \"182.99.246.172\"\n    },\n    {\n      \"id\": 359,\n      \"type\": \"ip\",\n      \"ip\": \"212.192.246.207\"\n    },\n    {\n      \"id\": 360,\n      \"type\": \"ip\",\n      \"ip\": \"177.185.117.129\"\n    },\n    {\n      \"id\": 361,\n      \"type\": \"ip\",\n      \"ip\": \"138.197.216.230\"\n    },\n    {\n      \"id\": 362,\n      \"type\": \"ip\",\n      \"ip\": \"49.233.62.251\"\n    },\n    {\n      \"id\": 363,\n      \"type\": \"ip\",\n      \"ip\": \"3.249.34.36\"\n    },\n    {\n      \"id\": 364,\n      \"type\": \"ip\",\n      \"ip\": \"112.74.34.48\"\n    },\n    {\n      \"id\": 365,\n      \"type\": \"ip\",\n      \"ip\": \"101.71.38.179\"\n    },\n    {\n      \"id\": 366,\n      \"type\": \"ip\",\n      \"ip\": \"101.71.37.219\"\n    },\n    {\n      \"id\": 367,\n      \"type\": \"ip\",\n      \"ip\": \"159.89.154.185\"\n    },\n    {\n      \"id\": 368,\n      \"type\": \"ip\",\n      \"ip\": \"111.252.206.184\"\n    },\n    {\n      \"id\": 369,\n      \"type\": \"ip\",\n      \"ip\": \"172.104.235.191\"\n    },\n    {\n      \"id\": 370,\n      \"type\": \"ip\",\n      \"ip\": \"182.99.247.199\"\n    },\n    {\n      \"id\": 371,\n      \"type\": \"ip\",\n      \"ip\": \"117.192.11.154\"\n    },\n    {\n      \"id\": 372,\n      \"type\": \"ip\",\n      \"ip\": \"138.68.250.214\"\n    },\n    {\n      \"id\": 373,\n      \"type\": \"ip\",\n      \"ip\": \"172.105.24.222\"\n    },\n    {\n      \"id\": 374,\n      \"type\": \"ip\",\n      \"ip\": \"176.58.115.154\"\n    },\n    {\n      \"id\": 375,\n      \"type\": \"ip\",\n      \"ip\": \"172.105.61.155\"\n    },\n    {\n      \"id\": 376,\n      \"type\": \"ip\",\n      \"ip\": \"64.227.67.110\"\n    },\n    {\n      \"id\": 377,\n      \"type\": \"ip\",\n      \"ip\": \"178.128.232.114\"\n    },\n    {\n      \"id\": 378,\n      \"type\": \"ip\",\n      \"ip\": \"103.107.198.109\"\n    },\n    {\n      \"id\": 379,\n      \"type\": \"ip\",\n      \"ip\": \"139.177.178.141\"\n    },\n    {\n      \"id\": 380,\n      \"type\": \"ip\",\n      \"ip\": \"115.151.228.95\"\n    },\n    {\n      \"id\": 381,\n      \"type\": \"ip\",\n      \"ip\": \"115.151.229.39\"\n    },\n    {\n      \"id\": 382,\n      \"type\": \"ip\",\n      \"ip\": \"159.89.94.219\"\n    },\n    {\n      \"id\": 383,\n      \"type\": \"ip\",\n      \"ip\": \"103.103.0.141\"\n    },\n    {\n      \"id\": 384,\n      \"type\": \"ip\",\n      \"ip\": \"172.104.229.162\"\n    },\n    {\n      \"id\": 385,\n      \"type\": \"ip\",\n      \"ip\": \"50.116.6.175\"\n    },\n    {\n      \"id\": 386,\n      \"type\": \"ip\",\n      \"ip\": \"172.105.39.123\"\n    },\n    {\n      \"id\": 387,\n      \"type\": \"ip\",\n      \"ip\": \"172.104.14.234\"\n    },\n    {\n      \"id\": 388,\n      \"type\": \"ip\",\n      \"ip\": \"74.207.250.89\"\n    },\n    {\n      \"id\": 389,\n      \"type\": \"ip\",\n      \"ip\": \"172.105.49.57\"\n    },\n    {\n      \"id\": 390,\n      \"type\": \"ip\",\n      \"ip\": \"115.151.228.64\"\n    },\n    {\n      \"id\": 391,\n      \"type\": \"ip\",\n      \"ip\": \"203.27.106.166\"\n    },\n    {\n      \"id\": 392,\n      \"type\": \"ip\",\n      \"ip\": \"138.68.48.89\"\n    },\n    {\n      \"id\": 393,\n      \"type\": \"ip\",\n      \"ip\": \"172.104.236.218\"\n    },\n    {\n      \"id\": 394,\n      \"type\": \"ip\",\n      \"ip\": \"207.148.74.130\"\n    },\n    {\n      \"id\": 395,\n      \"type\": \"ip\",\n      \"ip\": \"36.231.111.109\"\n    },\n    {\n      \"id\": 396,\n      \"type\": \"ip\",\n      \"ip\": \"119.237.159.189\"\n    },\n    {\n      \"id\": 397,\n      \"type\": \"ip\",\n      \"ip\": \"139.177.178.230\"\n    },\n    {\n      \"id\": 398,\n      \"type\": \"ip\",\n      \"ip\": \"167.172.44.255\"\n    },\n    {\n      \"id\": 399,\n      \"type\": \"ip\",\n      \"ip\": \"172.104.143.56\"\n    },\n    {\n      \"id\": 400,\n      \"type\": \"ip\",\n      \"ip\": \"194.195.246.98\"\n    },\n    {\n      \"id\": 401,\n      \"type\": \"ip\",\n      \"ip\": \"172.104.143.179\"\n    },\n    {\n      \"id\": 402,\n      \"type\": \"ip\",\n      \"ip\": \"172.107.194.186\"\n    },\n    {\n      \"id\": 403,\n      \"type\": \"ip\",\n      \"ip\": \"45.152.183.196\"\n    },\n    {\n      \"id\": 404,\n      \"type\": \"ip\",\n      \"ip\": \"114.37.213.110\"\n    },\n    {\n      \"id\": 405,\n      \"type\": \"ip\",\n      \"ip\": \"89.35.30.201\"\n    },\n    {\n      \"id\": 406,\n      \"type\": \"ip\",\n      \"ip\": \"139.177.177.104\"\n    },\n    {\n      \"id\": 407,\n      \"type\": \"ip\",\n      \"ip\": \"212.193.30.142\"\n    },\n    {\n      \"id\": 408,\n      \"type\": \"ip\",\n      \"ip\": \"37.183.170.54\"\n    },\n    {\n      \"id\": 409,\n      \"type\": \"ip\",\n      \"ip\": \"172.104.228.149\"\n    },\n    {\n      \"id\": 410,\n      \"type\": \"ip\",\n      \"ip\": \"64.227.188.168\"\n    },\n    {\n      \"id\": 411,\n      \"type\": \"ip\",\n      \"ip\": \"194.195.244.213\"\n    },\n    {\n      \"id\": 412,\n      \"type\": \"ip\",\n      \"ip\": \"37.120.203.181\"\n    },\n    {\n      \"id\": 413,\n      \"type\": \"ip\",\n      \"ip\": \"165.227.0.252\"\n    },\n    {\n      \"id\": 414,\n      \"type\": \"ip\",\n      \"ip\": \"185.236.200.117\"\n    },\n    {\n      \"id\": 415,\n      \"type\": \"ip\",\n      \"ip\": \"198.98.59.35\"\n    },\n    {\n      \"id\": 416,\n      \"type\": \"ip\",\n      \"ip\": \"157.245.42.92\"\n    },\n    {\n      \"id\": 417,\n      \"type\": \"ip\",\n      \"ip\": \"194.195.244.222\"\n    },\n    {\n      \"id\": 418,\n      \"type\": \"ip\",\n      \"ip\": \"165.227.49.32\"\n    },\n    {\n      \"id\": 419,\n      \"type\": \"ip\",\n      \"ip\": \"46.101.52.226\"\n    },\n    {\n      \"id\": 420,\n      \"type\": \"ip\",\n      \"ip\": \"138.68.40.190\"\n    },\n    {\n      \"id\": 421,\n      \"type\": \"ip\",\n      \"ip\": \"103.158.144.28\"\n    },\n    {\n      \"id\": 422,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.97.137\"\n    },\n    {\n      \"id\": 423,\n      \"type\": \"ip\",\n      \"ip\": \"176.10.80.38\"\n    },\n    {\n      \"id\": 424,\n      \"type\": \"ip\",\n      \"ip\": \"89.40.183.205\"\n    },\n    {\n      \"id\": 425,\n      \"type\": \"ip\",\n      \"ip\": \"143.198.163.225\"\n    },\n    {\n      \"id\": 426,\n      \"type\": \"ip\",\n      \"ip\": \"41.77.137.114\"\n    },\n    {\n      \"id\": 427,\n      \"type\": \"ip\",\n      \"ip\": \"165.22.237.1\"\n    },\n    {\n      \"id\": 428,\n      \"type\": \"ip\",\n      \"ip\": \"83.97.20.151\"\n    },\n    {\n      \"id\": 429,\n      \"type\": \"ip\",\n      \"ip\": \"159.223.91.110\"\n    },\n    {\n      \"id\": 430,\n      \"type\": \"ip\",\n      \"ip\": \"36.155.14.163\"\n    },\n    {\n      \"id\": 431,\n      \"type\": \"ip\",\n      \"ip\": \"95.216.145.1\"\n    },\n    {\n      \"id\": 432,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.6\"\n    },\n    {\n      \"id\": 433,\n      \"type\": \"ip\",\n      \"ip\": \"52.247.219.108\"\n    },\n    {\n      \"id\": 434,\n      \"type\": \"ip\",\n      \"ip\": \"165.232.84.153\"\n    },\n    {\n      \"id\": 435,\n      \"type\": \"ip\",\n      \"ip\": \"194.233.164.210\"\n    },\n    {\n      \"id\": 436,\n      \"type\": \"ip\",\n      \"ip\": \"36.113.34.20\"\n    },\n    {\n      \"id\": 437,\n      \"type\": \"ip\",\n      \"ip\": \"91.203.5.146\"\n    },\n    {\n      \"id\": 438,\n      \"type\": \"ip\",\n      \"ip\": \"159.203.45.181\"\n    },\n    {\n      \"id\": 439,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.12.97\"\n    },\n    {\n      \"id\": 440,\n      \"type\": \"ip\",\n      \"ip\": \"185.185.170.27\"\n    },\n    {\n      \"id\": 441,\n      \"type\": \"ip\",\n      \"ip\": \"45.151.167.11\"\n    },\n    {\n      \"id\": 442,\n      \"type\": \"ip\",\n      \"ip\": \"109.70.100.30\"\n    },\n    {\n      \"id\": 443,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.79.6\"\n    },\n    {\n      \"id\": 444,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.75.74\"\n    },\n    {\n      \"id\": 445,\n      \"type\": \"ip\",\n      \"ip\": \"150.158.91.179\"\n    },\n    {\n      \"id\": 446,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.102.8\"\n    },\n    {\n      \"id\": 447,\n      \"type\": \"ip\",\n      \"ip\": \"5.2.70.223\"\n    },\n    {\n      \"id\": 448,\n      \"type\": \"ip\",\n      \"ip\": \"194.233.164.103\"\n    },\n    {\n      \"id\": 449,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.103.115\"\n    },\n    {\n      \"id\": 450,\n      \"type\": \"ip\",\n      \"ip\": \"64.227.8.178\"\n    },\n    {\n      \"id\": 451,\n      \"type\": \"ip\",\n      \"ip\": \"194.195.244.81\"\n    },\n    {\n      \"id\": 452,\n      \"type\": \"ip\",\n      \"ip\": \"185.191.34.223\"\n    },\n    {\n      \"id\": 453,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.72.141\"\n    },\n    {\n      \"id\": 454,\n      \"type\": \"ip\",\n      \"ip\": \"114.254.20.186\"\n    },\n    {\n      \"id\": 455,\n      \"type\": \"ip\",\n      \"ip\": \"143.110.216.127\"\n    },\n    {\n      \"id\": 456,\n      \"type\": \"ip\",\n      \"ip\": \"64.227.178.55\"\n    },\n    {\n      \"id\": 457,\n      \"type\": \"ip\",\n      \"ip\": \"185.165.168.77\"\n    },\n    {\n      \"id\": 458,\n      \"type\": \"ip\",\n      \"ip\": \"5.2.72.110\"\n    },\n    {\n      \"id\": 459,\n      \"type\": \"ip\",\n      \"ip\": \"147.182.214.81\"\n    },\n    {\n      \"id\": 460,\n      \"type\": \"ip\",\n      \"ip\": \"34.197.176.70\"\n    },\n    {\n      \"id\": 461,\n      \"type\": \"ip\",\n      \"ip\": \"194.233.164.99\"\n    },\n    {\n      \"id\": 462,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.77.176\"\n    },\n    {\n      \"id\": 463,\n      \"type\": \"ip\",\n      \"ip\": \"165.227.32.109\"\n    },\n    {\n      \"id\": 464,\n      \"type\": \"ip\",\n      \"ip\": \"79.172.212.132\"\n    },\n    {\n      \"id\": 465,\n      \"type\": \"ip\",\n      \"ip\": \"143.110.216.17\"\n    },\n    {\n      \"id\": 466,\n      \"type\": \"ip\",\n      \"ip\": \"46.182.21.248\"\n    },\n    {\n      \"id\": 467,\n      \"type\": \"ip\",\n      \"ip\": \"159.223.43.161\"\n    },\n    {\n      \"id\": 468,\n      \"type\": \"ip\",\n      \"ip\": \"47.102.199.233\"\n    },\n    {\n      \"id\": 469,\n      \"type\": \"ip\",\n      \"ip\": \"192.42.116.18\"\n    },\n    {\n      \"id\": 470,\n      \"type\": \"ip\",\n      \"ip\": \"193.189.100.202\"\n    },\n    {\n      \"id\": 471,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.106.94\"\n    },\n    {\n      \"id\": 472,\n      \"type\": \"ip\",\n      \"ip\": \"43.254.54.195\"\n    },\n    {\n      \"id\": 473,\n      \"type\": \"ip\",\n      \"ip\": \"185.56.83.81\"\n    },\n    {\n      \"id\": 474,\n      \"type\": \"ip\",\n      \"ip\": \"37.228.129.5\"\n    },\n    {\n      \"id\": 475,\n      \"type\": \"ip\",\n      \"ip\": \"89.163.249.244\"\n    },\n    {\n      \"id\": 476,\n      \"type\": \"ip\",\n      \"ip\": \"51.159.70.42\"\n    },\n    {\n      \"id\": 477,\n      \"type\": \"ip\",\n      \"ip\": \"89.234.157.254\"\n    },\n    {\n      \"id\": 478,\n      \"type\": \"ip\",\n      \"ip\": \"81.17.18.60\"\n    },\n    {\n      \"id\": 479,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.103.4\"\n    },\n    {\n      \"id\": 480,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.128\"\n    },\n    {\n      \"id\": 481,\n      \"type\": \"ip\",\n      \"ip\": \"185.129.61.1\"\n    },\n    {\n      \"id\": 482,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.102.252\"\n    },\n    {\n      \"id\": 483,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.102.240\"\n    },\n    {\n      \"id\": 484,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.102.246\"\n    },\n    {\n      \"id\": 485,\n      \"type\": \"ip\",\n      \"ip\": \"61.175.202.154\"\n    },\n    {\n      \"id\": 486,\n      \"type\": \"ip\",\n      \"ip\": \"193.189.100.205\"\n    },\n    {\n      \"id\": 487,\n      \"type\": \"ip\",\n      \"ip\": \"51.77.52.216\"\n    },\n    {\n      \"id\": 488,\n      \"type\": \"ip\",\n      \"ip\": \"195.144.21.219\"\n    },\n    {\n      \"id\": 489,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.72.120\"\n    },\n    {\n      \"id\": 490,\n      \"type\": \"ip\",\n      \"ip\": \"143.110.216.190\"\n    },\n    {\n      \"id\": 491,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.10.137\"\n    },\n    {\n      \"id\": 492,\n      \"type\": \"ip\",\n      \"ip\": \"121.4.56.143\"\n    },\n    {\n      \"id\": 493,\n      \"type\": \"ip\",\n      \"ip\": \"80.67.172.162\"\n    },\n    {\n      \"id\": 494,\n      \"type\": \"ip\",\n      \"ip\": \"142.93.18.229\"\n    },\n    {\n      \"id\": 495,\n      \"type\": \"ip\",\n      \"ip\": \"167.86.70.252\"\n    },\n    {\n      \"id\": 496,\n      \"type\": \"ip\",\n      \"ip\": \"209.141.45.227\"\n    },\n    {\n      \"id\": 497,\n      \"type\": \"ip\",\n      \"ip\": \"199.195.253.162\"\n    },\n    {\n      \"id\": 498,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.72.239\"\n    },\n    {\n      \"id\": 499,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.77.101\"\n    },\n    {\n      \"id\": 500,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.102.4\"\n    },\n    {\n      \"id\": 501,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.13.238\"\n    },\n    {\n      \"id\": 502,\n      \"type\": \"ip\",\n      \"ip\": \"131.100.148.7\"\n    },\n    {\n      \"id\": 503,\n      \"type\": \"ip\",\n      \"ip\": \"104.248.144.120\"\n    },\n    {\n      \"id\": 504,\n      \"type\": \"ip\",\n      \"ip\": \"221.199.187.100\"\n    },\n    {\n      \"id\": 505,\n      \"type\": \"ip\",\n      \"ip\": \"45.153.160.135\"\n    },\n    {\n      \"id\": 506,\n      \"type\": \"ip\",\n      \"ip\": \"185.100.87.174\"\n    },\n    {\n      \"id\": 507,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.141\"\n    },\n    {\n      \"id\": 508,\n      \"type\": \"ip\",\n      \"ip\": \"109.70.100.23\"\n    },\n    {\n      \"id\": 509,\n      \"type\": \"ip\",\n      \"ip\": \"5.157.38.50\"\n    },\n    {\n      \"id\": 510,\n      \"type\": \"ip\",\n      \"ip\": \"37.187.196.70\"\n    },\n    {\n      \"id\": 511,\n      \"type\": \"ip\",\n      \"ip\": \"111.28.189.51\"\n    },\n    {\n      \"id\": 512,\n      \"type\": \"ip\",\n      \"ip\": \"67.205.170.85\"\n    },\n    {\n      \"id\": 513,\n      \"type\": \"ip\",\n      \"ip\": \"139.59.103.254\"\n    },\n    {\n      \"id\": 514,\n      \"type\": \"ip\",\n      \"ip\": \"138.197.108.154\"\n    },\n    {\n      \"id\": 515,\n      \"type\": \"ip\",\n      \"ip\": \"104.200.138.39\"\n    },\n    {\n      \"id\": 516,\n      \"type\": \"ip\",\n      \"ip\": \"115.151.229.14\"\n    },\n    {\n      \"id\": 517,\n      \"type\": \"ip\",\n      \"ip\": \"139.59.163.74\"\n    },\n    {\n      \"id\": 518,\n      \"type\": \"ip\",\n      \"ip\": \"37.120.158.20\"\n    },\n    {\n      \"id\": 519,\n      \"type\": \"ip\",\n      \"ip\": \"146.70.75.53\"\n    },\n    {\n      \"id\": 520,\n      \"type\": \"ip\",\n      \"ip\": \"133.18.201.195\"\n    },\n    {\n      \"id\": 521,\n      \"type\": \"ip\",\n      \"ip\": \"123.60.215.208\"\n    },\n    {\n      \"id\": 522,\n      \"type\": \"ip\",\n      \"ip\": \"203.27.106.140\"\n    },\n    {\n      \"id\": 523,\n      \"type\": \"ip\",\n      \"ip\": \"5.254.43.59\"\n    },\n    {\n      \"id\": 524,\n      \"type\": \"ip\",\n      \"ip\": \"159.89.154.102\"\n    },\n    {\n      \"id\": 525,\n      \"type\": \"ip\",\n      \"ip\": \"213.152.162.149\"\n    },\n    {\n      \"id\": 526,\n      \"type\": \"ip\",\n      \"ip\": \"115.151.228.235\"\n    },\n    {\n      \"id\": 527,\n      \"type\": \"ip\",\n      \"ip\": \"213.152.162.10\"\n    },\n    {\n      \"id\": 528,\n      \"type\": \"ip\",\n      \"ip\": \"159.203.58.73\"\n    },\n    {\n      \"id\": 529,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.59.77\"\n    },\n    {\n      \"id\": 530,\n      \"type\": \"ip\",\n      \"ip\": \"189.40.83.32\"\n    },\n    {\n      \"id\": 531,\n      \"type\": \"ip\",\n      \"ip\": \"167.99.204.151\"\n    },\n    {\n      \"id\": 532,\n      \"type\": \"ip\",\n      \"ip\": \"159.89.48.173\"\n    },\n    {\n      \"id\": 533,\n      \"type\": \"ip\",\n      \"ip\": \"68.183.37.10\"\n    },\n    {\n      \"id\": 534,\n      \"type\": \"ip\",\n      \"ip\": \"35.232.163.113\"\n    },\n    {\n      \"id\": 535,\n      \"type\": \"ip\",\n      \"ip\": \"27.255.81.163\"\n    },\n    {\n      \"id\": 536,\n      \"type\": \"ip\",\n      \"ip\": \"47.102.205.237\"\n    },\n    {\n      \"id\": 537,\n      \"type\": \"ip\",\n      \"ip\": \"23.82.194.167\"\n    },\n    {\n      \"id\": 538,\n      \"type\": \"ip\",\n      \"ip\": \"167.99.88.151\"\n    },\n    {\n      \"id\": 539,\n      \"type\": \"ip\",\n      \"ip\": \"61.178.32.114\"\n    },\n    {\n      \"id\": 540,\n      \"type\": \"ip\",\n      \"ip\": \"213.152.161.249\"\n    },\n    {\n      \"id\": 541,\n      \"type\": \"ip\",\n      \"ip\": \"154.23.247.219\"\n    },\n    {\n      \"id\": 542,\n      \"type\": \"ip\",\n      \"ip\": \"182.99.246.190\"\n    },\n    {\n      \"id\": 543,\n      \"type\": \"ip\",\n      \"ip\": \"168.196.89.15\"\n    },\n    {\n      \"id\": 544,\n      \"type\": \"ip\",\n      \"ip\": \"147.182.187.229\"\n    },\n    {\n      \"id\": 545,\n      \"type\": \"ip\",\n      \"ip\": \"116.24.67.213\"\n    },\n    {\n      \"id\": 546,\n      \"type\": \"ip\",\n      \"ip\": \"88.80.20.86\"\n    },\n    {\n      \"id\": 547,\n      \"type\": \"ip\",\n      \"ip\": \"143.198.45.117\"\n    },\n    {\n      \"id\": 548,\n      \"type\": \"ip\",\n      \"ip\": \"143.198.32.72\"\n    },\n    {\n      \"id\": 549,\n      \"type\": \"ip\",\n      \"ip\": \"185.189.161.11\"\n    },\n    {\n      \"id\": 550,\n      \"type\": \"ip\",\n      \"ip\": \"120.24.23.84\"\n    },\n    {\n      \"id\": 551,\n      \"type\": \"ip\",\n      \"ip\": \"5.254.101.167\"\n    },\n    {\n      \"id\": 552,\n      \"type\": \"ip\",\n      \"ip\": \"44.192.244.127\"\n    },\n    {\n      \"id\": 553,\n      \"type\": \"ip\",\n      \"ip\": \"172.105.24.118\"\n    },\n    {\n      \"id\": 554,\n      \"type\": \"ip\",\n      \"ip\": \"139.162.165.222\"\n    },\n    {\n      \"id\": 555,\n      \"type\": \"ip\",\n      \"ip\": \"178.79.155.29\"\n    },\n    {\n      \"id\": 556,\n      \"type\": \"ip\",\n      \"ip\": \"172.105.96.165\"\n    },\n    {\n      \"id\": 557,\n      \"type\": \"ip\",\n      \"ip\": \"103.232.137.64\"\n    },\n    {\n      \"id\": 558,\n      \"type\": \"ip\",\n      \"ip\": \"45.79.92.73\"\n    },\n    {\n      \"id\": 559,\n      \"type\": \"ip\",\n      \"ip\": \"103.4.30.79\"\n    },\n    {\n      \"id\": 560,\n      \"type\": \"ip\",\n      \"ip\": \"172.105.119.252\"\n    },\n    {\n      \"id\": 561,\n      \"type\": \"ip\",\n      \"ip\": \"139.162.171.98\"\n    },\n    {\n      \"id\": 562,\n      \"type\": \"ip\",\n      \"ip\": \"172.105.64.54\"\n    },\n    {\n      \"id\": 563,\n      \"type\": \"ip\",\n      \"ip\": \"172.105.42.140\"\n    },\n    {\n      \"id\": 564,\n      \"type\": \"ip\",\n      \"ip\": \"45.79.125.19\"\n    },\n    {\n      \"id\": 565,\n      \"type\": \"ip\",\n      \"ip\": \"203.175.12.87\"\n    },\n    {\n      \"id\": 566,\n      \"type\": \"ip\",\n      \"ip\": \"139.177.177.82\"\n    },\n    {\n      \"id\": 567,\n      \"type\": \"ip\",\n      \"ip\": \"139.177.177.228\"\n    },\n    {\n      \"id\": 568,\n      \"type\": \"ip\",\n      \"ip\": \"113.219.171.101\"\n    },\n    {\n      \"id\": 569,\n      \"type\": \"ip\",\n      \"ip\": \"192.46.237.62\"\n    },\n    {\n      \"id\": 570,\n      \"type\": \"ip\",\n      \"ip\": \"110.191.179.149\"\n    },\n    {\n      \"id\": 571,\n      \"type\": \"ip\",\n      \"ip\": \"218.172.129.173\"\n    },\n    {\n      \"id\": 572,\n      \"type\": \"ip\",\n      \"ip\": \"139.162.161.41\"\n    },\n    {\n      \"id\": 573,\n      \"type\": \"ip\",\n      \"ip\": \"180.149.231.245\"\n    },\n    {\n      \"id\": 574,\n      \"type\": \"ip\",\n      \"ip\": \"192.46.237.134\"\n    },\n    {\n      \"id\": 575,\n      \"type\": \"ip\",\n      \"ip\": \"192.46.237.59\"\n    },\n    {\n      \"id\": 576,\n      \"type\": \"ip\",\n      \"ip\": \"103.192.80.171\"\n    },\n    {\n      \"id\": 577,\n      \"type\": \"ip\",\n      \"ip\": \"203.175.12.98\"\n    },\n    {\n      \"id\": 578,\n      \"type\": \"ip\",\n      \"ip\": \"185.125.204.196\"\n    },\n    {\n      \"id\": 579,\n      \"type\": \"ip\",\n      \"ip\": \"192.243.124.240\"\n    },\n    {\n      \"id\": 580,\n      \"type\": \"ip\",\n      \"ip\": \"139.177.180.102\"\n    },\n    {\n      \"id\": 581,\n      \"type\": \"ip\",\n      \"ip\": \"192.46.237.70\"\n    },\n    {\n      \"id\": 582,\n      \"type\": \"ip\",\n      \"ip\": \"217.70.162.253\"\n    },\n    {\n      \"id\": 583,\n      \"type\": \"ip\",\n      \"ip\": \"139.177.180.91\"\n    },\n    {\n      \"id\": 584,\n      \"type\": \"ip\",\n      \"ip\": \"124.224.87.11\"\n    },\n    {\n      \"id\": 585,\n      \"type\": \"ip\",\n      \"ip\": \"134.122.47.97\"\n    },\n    {\n      \"id\": 586,\n      \"type\": \"ip\",\n      \"ip\": \"194.195.244.74\"\n    },\n    {\n      \"id\": 587,\n      \"type\": \"ip\",\n      \"ip\": \"172.105.49.127\"\n    },\n    {\n      \"id\": 588,\n      \"type\": \"ip\",\n      \"ip\": \"194.195.246.87\"\n    },\n    {\n      \"id\": 589,\n      \"type\": \"ip\",\n      \"ip\": \"194.233.164.102\"\n    },\n    {\n      \"id\": 590,\n      \"type\": \"ip\",\n      \"ip\": \"138.68.246.18\"\n    },\n    {\n      \"id\": 591,\n      \"type\": \"ip\",\n      \"ip\": \"5.2.70.192\"\n    },\n    {\n      \"id\": 592,\n      \"type\": \"ip\",\n      \"ip\": \"194.233.164.92\"\n    },\n    {\n      \"id\": 593,\n      \"type\": \"ip\",\n      \"ip\": \"165.227.24.81\"\n    },\n    {\n      \"id\": 594,\n      \"type\": \"ip\",\n      \"ip\": \"138.68.57.60\"\n    },\n    {\n      \"id\": 595,\n      \"type\": \"ip\",\n      \"ip\": \"165.227.17.22\"\n    },\n    {\n      \"id\": 596,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.110.107\"\n    },\n    {\n      \"id\": 597,\n      \"type\": \"ip\",\n      \"ip\": \"194.195.244.78\"\n    },\n    {\n      \"id\": 598,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.21\"\n    },\n    {\n      \"id\": 599,\n      \"type\": \"ip\",\n      \"ip\": \"165.227.6.110\"\n    },\n    {\n      \"id\": 600,\n      \"type\": \"ip\",\n      \"ip\": \"23.168.193.26\"\n    },\n    {\n      \"id\": 601,\n      \"type\": \"ip\",\n      \"ip\": \"94.159.128.182\"\n    },\n    {\n      \"id\": 602,\n      \"type\": \"ip\",\n      \"ip\": \"128.199.222.221\"\n    },\n    {\n      \"id\": 603,\n      \"type\": \"ip\",\n      \"ip\": \"45.56.80.11\"\n    },\n    {\n      \"id\": 604,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.67.238\"\n    },\n    {\n      \"id\": 605,\n      \"type\": \"ip\",\n      \"ip\": \"157.245.42.12\"\n    },\n    {\n      \"id\": 606,\n      \"type\": \"ip\",\n      \"ip\": \"194.195.246.88\"\n    },\n    {\n      \"id\": 607,\n      \"type\": \"ip\",\n      \"ip\": \"54.211.219.103\"\n    },\n    {\n      \"id\": 608,\n      \"type\": \"ip\",\n      \"ip\": \"194.195.246.93\"\n    },\n    {\n      \"id\": 609,\n      \"type\": \"ip\",\n      \"ip\": \"68.183.199.248\"\n    },\n    {\n      \"id\": 610,\n      \"type\": \"ip\",\n      \"ip\": \"138.197.221.77\"\n    },\n    {\n      \"id\": 611,\n      \"type\": \"ip\",\n      \"ip\": \"165.227.22.243\"\n    },\n    {\n      \"id\": 612,\n      \"type\": \"ip\",\n      \"ip\": \"67.207.82.195\"\n    },\n    {\n      \"id\": 613,\n      \"type\": \"ip\",\n      \"ip\": \"138.68.13.60\"\n    },\n    {\n      \"id\": 614,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.106.130\"\n    },\n    {\n      \"id\": 615,\n      \"type\": \"ip\",\n      \"ip\": \"143.110.220.95\"\n    },\n    {\n      \"id\": 616,\n      \"type\": \"ip\",\n      \"ip\": \"159.89.159.168\"\n    },\n    {\n      \"id\": 617,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.103.113\"\n    },\n    {\n      \"id\": 618,\n      \"type\": \"ip\",\n      \"ip\": \"137.184.156.166\"\n    },\n    {\n      \"id\": 619,\n      \"type\": \"ip\",\n      \"ip\": \"192.81.219.134\"\n    },\n    {\n      \"id\": 620,\n      \"type\": \"ip\",\n      \"ip\": \"194.233.164.95\"\n    },\n    {\n      \"id\": 621,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.3.110\"\n    },\n    {\n      \"id\": 622,\n      \"type\": \"ip\",\n      \"ip\": \"138.68.241.212\"\n    },\n    {\n      \"id\": 623,\n      \"type\": \"ip\",\n      \"ip\": \"159.223.88.139\"\n    },\n    {\n      \"id\": 624,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.132.36\"\n    },\n    {\n      \"id\": 625,\n      \"type\": \"ip\",\n      \"ip\": \"143.110.216.221\"\n    },\n    {\n      \"id\": 626,\n      \"type\": \"ip\",\n      \"ip\": \"109.70.100.34\"\n    },\n    {\n      \"id\": 627,\n      \"type\": \"ip\",\n      \"ip\": \"194.195.241.242\"\n    },\n    {\n      \"id\": 628,\n      \"type\": \"ip\",\n      \"ip\": \"138.68.7.172\"\n    },\n    {\n      \"id\": 629,\n      \"type\": \"ip\",\n      \"ip\": \"64.227.188.223\"\n    },\n    {\n      \"id\": 630,\n      \"type\": \"ip\",\n      \"ip\": \"185.100.87.139\"\n    },\n    {\n      \"id\": 631,\n      \"type\": \"ip\",\n      \"ip\": \"182.99.247.253\"\n    },\n    {\n      \"id\": 632,\n      \"type\": \"ip\",\n      \"ip\": \"51.195.45.190\"\n    },\n    {\n      \"id\": 633,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.31.241\"\n    },\n    {\n      \"id\": 634,\n      \"type\": \"ip\",\n      \"ip\": \"143.110.216.245\"\n    },\n    {\n      \"id\": 635,\n      \"type\": \"ip\",\n      \"ip\": \"178.20.55.16\"\n    },\n    {\n      \"id\": 636,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.77.235\"\n    },\n    {\n      \"id\": 637,\n      \"type\": \"ip\",\n      \"ip\": \"78.247.62.22\"\n    },\n    {\n      \"id\": 638,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.5\"\n    },\n    {\n      \"id\": 639,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.76.173\"\n    },\n    {\n      \"id\": 640,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.103.116\"\n    },\n    {\n      \"id\": 641,\n      \"type\": \"ip\",\n      \"ip\": \"81.6.43.167\"\n    },\n    {\n      \"id\": 642,\n      \"type\": \"ip\",\n      \"ip\": \"109.70.100.28\"\n    },\n    {\n      \"id\": 643,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.8\"\n    },\n    {\n      \"id\": 644,\n      \"type\": \"ip\",\n      \"ip\": \"209.141.46.81\"\n    },\n    {\n      \"id\": 645,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.11.153\"\n    },\n    {\n      \"id\": 646,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.78.183\"\n    },\n    {\n      \"id\": 647,\n      \"type\": \"ip\",\n      \"ip\": \"205.185.125.45\"\n    },\n    {\n      \"id\": 648,\n      \"type\": \"ip\",\n      \"ip\": \"195.176.3.23\"\n    },\n    {\n      \"id\": 649,\n      \"type\": \"ip\",\n      \"ip\": \"193.189.100.197\"\n    },\n    {\n      \"id\": 650,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.12.135\"\n    },\n    {\n      \"id\": 651,\n      \"type\": \"ip\",\n      \"ip\": \"212.192.246.95\"\n    },\n    {\n      \"id\": 652,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.72.7\"\n    },\n    {\n      \"id\": 653,\n      \"type\": \"ip\",\n      \"ip\": \"5.2.76.221\"\n    },\n    {\n      \"id\": 654,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.29.107\"\n    },\n    {\n      \"id\": 655,\n      \"type\": \"ip\",\n      \"ip\": \"164.52.53.163\"\n    },\n    {\n      \"id\": 656,\n      \"type\": \"ip\",\n      \"ip\": \"5.2.70.140\"\n    },\n    {\n      \"id\": 657,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.12.227\"\n    },\n    {\n      \"id\": 658,\n      \"type\": \"ip\",\n      \"ip\": \"81.30.157.43\"\n    },\n    {\n      \"id\": 659,\n      \"type\": \"ip\",\n      \"ip\": \"217.79.189.13\"\n    },\n    {\n      \"id\": 660,\n      \"type\": \"ip\",\n      \"ip\": \"5.183.209.135\"\n    },\n    {\n      \"id\": 661,\n      \"type\": \"ip\",\n      \"ip\": \"45.128.133.242\"\n    },\n    {\n      \"id\": 662,\n      \"type\": \"ip\",\n      \"ip\": \"91.221.57.179\"\n    },\n    {\n      \"id\": 663,\n      \"type\": \"ip\",\n      \"ip\": \"51.68.190.9\"\n    },\n    {\n      \"id\": 664,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.77.122\"\n    },\n    {\n      \"id\": 665,\n      \"type\": \"ip\",\n      \"ip\": \"81.17.18.58\"\n    },\n    {\n      \"id\": 666,\n      \"type\": \"ip\",\n      \"ip\": \"135.148.43.32\"\n    },\n    {\n      \"id\": 667,\n      \"type\": \"ip\",\n      \"ip\": \"176.10.104.240\"\n    },\n    {\n      \"id\": 668,\n      \"type\": \"ip\",\n      \"ip\": \"103.73.160.211\"\n    },\n    {\n      \"id\": 669,\n      \"type\": \"ip\",\n      \"ip\": \"162.247.74.202\"\n    },\n    {\n      \"id\": 670,\n      \"type\": \"ip\",\n      \"ip\": \"209.141.54.195\"\n    },\n    {\n      \"id\": 671,\n      \"type\": \"ip\",\n      \"ip\": \"188.166.170.135\"\n    },\n    {\n      \"id\": 672,\n      \"type\": \"ip\",\n      \"ip\": \"45.153.160.134\"\n    },\n    {\n      \"id\": 673,\n      \"type\": \"ip\",\n      \"ip\": \"23.154.177.5\"\n    },\n    {\n      \"id\": 674,\n      \"type\": \"ip\",\n      \"ip\": \"45.61.185.125\"\n    },\n    {\n      \"id\": 675,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.74.28\"\n    },\n    {\n      \"id\": 676,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.14.76\"\n    },\n    {\n      \"id\": 677,\n      \"type\": \"ip\",\n      \"ip\": \"209.141.55.26\"\n    },\n    {\n      \"id\": 678,\n      \"type\": \"ip\",\n      \"ip\": \"211.154.194.21\"\n    },\n    {\n      \"id\": 679,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.102.242\"\n    },\n    {\n      \"id\": 680,\n      \"type\": \"ip\",\n      \"ip\": \"199.195.251.182\"\n    },\n    {\n      \"id\": 681,\n      \"type\": \"ip\",\n      \"ip\": \"85.93.218.204\"\n    },\n    {\n      \"id\": 682,\n      \"type\": \"ip\",\n      \"ip\": \"195.123.247.209\"\n    },\n    {\n      \"id\": 683,\n      \"type\": \"ip\",\n      \"ip\": \"45.153.160.138\"\n    },\n    {\n      \"id\": 684,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.8.65\"\n    },\n    {\n      \"id\": 685,\n      \"type\": \"ip\",\n      \"ip\": \"45.64.75.134\"\n    },\n    {\n      \"id\": 686,\n      \"type\": \"ip\",\n      \"ip\": \"13.72.102.159\"\n    },\n    {\n      \"id\": 687,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.102.241\"\n    },\n    {\n      \"id\": 688,\n      \"type\": \"ip\",\n      \"ip\": \"45.153.160.131\"\n    },\n    {\n      \"id\": 689,\n      \"type\": \"ip\",\n      \"ip\": \"60.31.180.149\"\n    },\n    {\n      \"id\": 690,\n      \"type\": \"ip\",\n      \"ip\": \"54.37.16.241\"\n    },\n    {\n      \"id\": 691,\n      \"type\": \"ip\",\n      \"ip\": \"5.255.97.170\"\n    },\n    {\n      \"id\": 692,\n      \"type\": \"ip\",\n      \"ip\": \"101.204.24.28\"\n    },\n    {\n      \"id\": 693,\n      \"type\": \"ip\",\n      \"ip\": \"198.98.60.19\"\n    },\n    {\n      \"id\": 694,\n      \"type\": \"ip\",\n      \"ip\": \"68.79.17.59\"\n    },\n    {\n      \"id\": 695,\n      \"type\": \"ip\",\n      \"ip\": \"147.182.131.229\"\n    },\n    {\n      \"id\": 696,\n      \"type\": \"ip\",\n      \"ip\": \"134.122.34.28\"\n    },\n    {\n      \"id\": 697,\n      \"type\": \"ip\",\n      \"ip\": \"185.4.132.183\"\n    },\n    {\n      \"id\": 698,\n      \"type\": \"ip\",\n      \"ip\": \"182.99.247.188\"\n    },\n    {\n      \"id\": 699,\n      \"type\": \"ip\",\n      \"ip\": \"203.27.106.165\"\n    },\n    {\n      \"id\": 700,\n      \"type\": \"ip\",\n      \"ip\": \"138.197.9.239\"\n    },\n    {\n      \"id\": 701,\n      \"type\": \"ip\",\n      \"ip\": \"172.83.40.103\"\n    },\n    {\n      \"id\": 702,\n      \"type\": \"ip\",\n      \"ip\": \"89.38.69.136\"\n    },\n    {\n      \"id\": 703,\n      \"type\": \"ip\",\n      \"ip\": \"147.182.242.241\"\n    },\n    {\n      \"id\": 704,\n      \"type\": \"ip\",\n      \"ip\": \"160.238.38.207\"\n    },\n    {\n      \"id\": 705,\n      \"type\": \"ip\",\n      \"ip\": \"182.99.246.187\"\n    },\n    {\n      \"id\": 706,\n      \"type\": \"ip\",\n      \"ip\": \"5.135.141.139\"\n    },\n    {\n      \"id\": 707,\n      \"type\": \"ip\",\n      \"ip\": \"46.101.223.115\"\n    },\n    {\n      \"id\": 708,\n      \"type\": \"ip\",\n      \"ip\": \"182.99.246.141\"\n    },\n    {\n      \"id\": 709,\n      \"type\": \"ip\",\n      \"ip\": \"84.17.39.201\"\n    },\n    {\n      \"id\": 710,\n      \"type\": \"ip\",\n      \"ip\": \"122.155.174.180\"\n    },\n    {\n      \"id\": 711,\n      \"type\": \"ip\",\n      \"ip\": \"139.28.218.132\"\n    },\n    {\n      \"id\": 712,\n      \"type\": \"ip\",\n      \"ip\": \"78.110.164.45\"\n    },\n    {\n      \"id\": 713,\n      \"type\": \"ip\",\n      \"ip\": \"52.231.93.116\"\n    },\n    {\n      \"id\": 714,\n      \"type\": \"ip\",\n      \"ip\": \"182.99.247.181\"\n    },\n    {\n      \"id\": 715,\n      \"type\": \"ip\",\n      \"ip\": \"147.182.242.144\"\n    },\n    {\n      \"id\": 716,\n      \"type\": \"ip\",\n      \"ip\": \"182.253.160.196\"\n    },\n    {\n      \"id\": 717,\n      \"type\": \"ip\",\n      \"ip\": \"167.99.221.249\"\n    },\n    {\n      \"id\": 718,\n      \"type\": \"ip\",\n      \"ip\": \"159.89.150.150\"\n    },\n    {\n      \"id\": 719,\n      \"type\": \"ip\",\n      \"ip\": \"188.166.86.206\"\n    },\n    {\n      \"id\": 720,\n      \"type\": \"ip\",\n      \"ip\": \"143.198.180.150\"\n    },\n    {\n      \"id\": 721,\n      \"type\": \"ip\",\n      \"ip\": \"37.120.158.21\"\n    },\n    {\n      \"id\": 722,\n      \"type\": \"ip\",\n      \"ip\": \"178.128.226.212\"\n    },\n    {\n      \"id\": 723,\n      \"type\": \"ip\",\n      \"ip\": \"82.102.31.170\"\n    },\n    {\n      \"id\": 724,\n      \"type\": \"ip\",\n      \"ip\": \"111.252.156.42\"\n    },\n    {\n      \"id\": 725,\n      \"type\": \"ip\",\n      \"ip\": \"159.89.154.64\"\n    },\n    {\n      \"id\": 726,\n      \"type\": \"ip\",\n      \"ip\": \"178.79.157.61\"\n    },\n    {\n      \"id\": 727,\n      \"type\": \"ip\",\n      \"ip\": \"45.61.136.76\"\n    },\n    {\n      \"id\": 728,\n      \"type\": \"ip\",\n      \"ip\": \"122.161.49.40\"\n    },\n    {\n      \"id\": 729,\n      \"type\": \"ip\",\n      \"ip\": \"45.33.50.214\"\n    },\n    {\n      \"id\": 730,\n      \"type\": \"ip\",\n      \"ip\": \"109.70.150.120\"\n    },\n    {\n      \"id\": 731,\n      \"type\": \"ip\",\n      \"ip\": \"172.105.96.225\"\n    },\n    {\n      \"id\": 732,\n      \"type\": \"ip\",\n      \"ip\": \"172.105.106.28\"\n    },\n    {\n      \"id\": 733,\n      \"type\": \"ip\",\n      \"ip\": \"31.6.19.41\"\n    },\n    {\n      \"id\": 734,\n      \"type\": \"ip\",\n      \"ip\": \"42.192.69.45\"\n    },\n    {\n      \"id\": 735,\n      \"type\": \"ip\",\n      \"ip\": \"58.100.164.147\"\n    },\n    {\n      \"id\": 736,\n      \"type\": \"ip\",\n      \"ip\": \"83.149.110.185\"\n    },\n    {\n      \"id\": 737,\n      \"type\": \"ip\",\n      \"ip\": \"159.89.154.77\"\n    },\n    {\n      \"id\": 738,\n      \"type\": \"ip\",\n      \"ip\": \"172.107.194.190\"\n    },\n    {\n      \"id\": 739,\n      \"type\": \"ip\",\n      \"ip\": \"139.177.178.19\"\n    },\n    {\n      \"id\": 740,\n      \"type\": \"ip\",\n      \"ip\": \"20.71.156.146\"\n    },\n    {\n      \"id\": 741,\n      \"type\": \"ip\",\n      \"ip\": \"182.99.246.183\"\n    },\n    {\n      \"id\": 742,\n      \"type\": \"ip\",\n      \"ip\": \"138.197.193.220\"\n    },\n    {\n      \"id\": 743,\n      \"type\": \"ip\",\n      \"ip\": \"18.118.13.24\"\n    },\n    {\n      \"id\": 744,\n      \"type\": \"ip\",\n      \"ip\": \"218.29.217.234\"\n    },\n    {\n      \"id\": 745,\n      \"type\": \"ip\",\n      \"ip\": \"213.152.161.10\"\n    },\n    {\n      \"id\": 746,\n      \"type\": \"ip\",\n      \"ip\": \"54.146.233.218\"\n    },\n    {\n      \"id\": 747,\n      \"type\": \"ip\",\n      \"ip\": \"147.135.6.221\"\n    },\n    {\n      \"id\": 748,\n      \"type\": \"ip\",\n      \"ip\": \"183.13.106.232\"\n    },\n    {\n      \"id\": 749,\n      \"type\": \"ip\",\n      \"ip\": \"68.183.35.171\"\n    },\n    {\n      \"id\": 750,\n      \"type\": \"ip\",\n      \"ip\": \"182.99.234.148\"\n    },\n    {\n      \"id\": 751,\n      \"type\": \"ip\",\n      \"ip\": \"95.25.101.193\"\n    },\n    {\n      \"id\": 752,\n      \"type\": \"ip\",\n      \"ip\": \"109.248.114.78\"\n    },\n    {\n      \"id\": 753,\n      \"type\": \"ip\",\n      \"ip\": \"159.48.55.216\"\n    },\n    {\n      \"id\": 754,\n      \"type\": \"ip\",\n      \"ip\": \"195.201.175.217\"\n    },\n    {\n      \"id\": 755,\n      \"type\": \"ip\",\n      \"ip\": \"152.70.36.95\"\n    },\n    {\n      \"id\": 756,\n      \"type\": \"ip\",\n      \"ip\": \"172.105.49.123\"\n    },\n    {\n      \"id\": 757,\n      \"type\": \"ip\",\n      \"ip\": \"172.104.180.116\"\n    },\n    {\n      \"id\": 758,\n      \"type\": \"ip\",\n      \"ip\": \"172.104.143.131\"\n    },\n    {\n      \"id\": 759,\n      \"type\": \"ip\",\n      \"ip\": \"37.19.212.104\"\n    },\n    {\n      \"id\": 760,\n      \"type\": \"ip\",\n      \"ip\": \"109.70.150.140\"\n    },\n    {\n      \"id\": 761,\n      \"type\": \"ip\",\n      \"ip\": \"139.177.180.94\"\n    },\n    {\n      \"id\": 762,\n      \"type\": \"ip\",\n      \"ip\": \"172.105.249.93\"\n    },\n    {\n      \"id\": 763,\n      \"type\": \"ip\",\n      \"ip\": \"178.79.157.186\"\n    },\n    {\n      \"id\": 764,\n      \"type\": \"ip\",\n      \"ip\": \"172.104.145.200\"\n    },\n    {\n      \"id\": 765,\n      \"type\": \"ip\",\n      \"ip\": \"157.245.129.50\"\n    },\n    {\n      \"id\": 766,\n      \"type\": \"ip\",\n      \"ip\": \"165.227.20.170\"\n    },\n    {\n      \"id\": 767,\n      \"type\": \"ip\",\n      \"ip\": \"116.49.189.128\"\n    },\n    {\n      \"id\": 768,\n      \"type\": \"ip\",\n      \"ip\": \"139.177.178.8\"\n    },\n    {\n      \"id\": 769,\n      \"type\": \"ip\",\n      \"ip\": \"139.177.177.79\"\n    },\n    {\n      \"id\": 770,\n      \"type\": \"ip\",\n      \"ip\": \"139.28.219.110\"\n    },\n    {\n      \"id\": 771,\n      \"type\": \"ip\",\n      \"ip\": \"194.195.246.96\"\n    },\n    {\n      \"id\": 772,\n      \"type\": \"ip\",\n      \"ip\": \"134.122.32.225\"\n    },\n    {\n      \"id\": 773,\n      \"type\": \"ip\",\n      \"ip\": \"192.46.237.71\"\n    },\n    {\n      \"id\": 774,\n      \"type\": \"ip\",\n      \"ip\": \"116.89.189.30\"\n    },\n    {\n      \"id\": 775,\n      \"type\": \"ip\",\n      \"ip\": \"192.46.237.61\"\n    },\n    {\n      \"id\": 776,\n      \"type\": \"ip\",\n      \"ip\": \"116.89.189.19\"\n    },\n    {\n      \"id\": 777,\n      \"type\": \"ip\",\n      \"ip\": \"103.103.128.86\"\n    },\n    {\n      \"id\": 778,\n      \"type\": \"ip\",\n      \"ip\": \"149.129.95.75\"\n    },\n    {\n      \"id\": 779,\n      \"type\": \"ip\",\n      \"ip\": \"139.177.180.120\"\n    },\n    {\n      \"id\": 780,\n      \"type\": \"ip\",\n      \"ip\": \"192.46.235.50\"\n    },\n    {\n      \"id\": 781,\n      \"type\": \"ip\",\n      \"ip\": \"210.3.53.213\"\n    },\n    {\n      \"id\": 782,\n      \"type\": \"ip\",\n      \"ip\": \"143.244.153.32\"\n    },\n    {\n      \"id\": 783,\n      \"type\": \"ip\",\n      \"ip\": \"194.233.164.82\"\n    },\n    {\n      \"id\": 784,\n      \"type\": \"ip\",\n      \"ip\": \"165.227.6.93\"\n    },\n    {\n      \"id\": 785,\n      \"type\": \"ip\",\n      \"ip\": \"51.105.55.17\"\n    },\n    {\n      \"id\": 786,\n      \"type\": \"ip\",\n      \"ip\": \"162.243.172.194\"\n    },\n    {\n      \"id\": 787,\n      \"type\": \"ip\",\n      \"ip\": \"68.183.33.236\"\n    },\n    {\n      \"id\": 788,\n      \"type\": \"ip\",\n      \"ip\": \"199.249.230.185\"\n    },\n    {\n      \"id\": 789,\n      \"type\": \"ip\",\n      \"ip\": \"165.227.4.86\"\n    },\n    {\n      \"id\": 790,\n      \"type\": \"ip\",\n      \"ip\": \"194.195.244.79\"\n    },\n    {\n      \"id\": 791,\n      \"type\": \"ip\",\n      \"ip\": \"159.89.122.19\"\n    },\n    {\n      \"id\": 792,\n      \"type\": \"ip\",\n      \"ip\": \"194.233.164.97\"\n    },\n    {\n      \"id\": 793,\n      \"type\": \"ip\",\n      \"ip\": \"194.195.244.206\"\n    },\n    {\n      \"id\": 794,\n      \"type\": \"ip\",\n      \"ip\": \"165.227.18.8\"\n    },\n    {\n      \"id\": 795,\n      \"type\": \"ip\",\n      \"ip\": \"159.89.158.150\"\n    },\n    {\n      \"id\": 796,\n      \"type\": \"ip\",\n      \"ip\": \"64.227.65.146\"\n    },\n    {\n      \"id\": 797,\n      \"type\": \"ip\",\n      \"ip\": \"68.183.192.239\"\n    },\n    {\n      \"id\": 798,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.48.154\"\n    },\n    {\n      \"id\": 799,\n      \"type\": \"ip\",\n      \"ip\": \"138.197.202.163\"\n    },\n    {\n      \"id\": 800,\n      \"type\": \"ip\",\n      \"ip\": \"159.89.85.91\"\n    },\n    {\n      \"id\": 801,\n      \"type\": \"ip\",\n      \"ip\": \"194.233.164.125\"\n    },\n    {\n      \"id\": 802,\n      \"type\": \"ip\",\n      \"ip\": \"64.227.188.164\"\n    },\n    {\n      \"id\": 803,\n      \"type\": \"ip\",\n      \"ip\": \"165.232.84.228\"\n    },\n    {\n      \"id\": 804,\n      \"type\": \"ip\",\n      \"ip\": \"124.224.87.29\"\n    },\n    {\n      \"id\": 805,\n      \"type\": \"ip\",\n      \"ip\": \"194.195.244.218\"\n    },\n    {\n      \"id\": 806,\n      \"type\": \"ip\",\n      \"ip\": \"138.197.197.108\"\n    },\n    {\n      \"id\": 807,\n      \"type\": \"ip\",\n      \"ip\": \"195.154.119.181\"\n    },\n    {\n      \"id\": 808,\n      \"type\": \"ip\",\n      \"ip\": \"159.89.120.48\"\n    },\n    {\n      \"id\": 809,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.106.65\"\n    },\n    {\n      \"id\": 810,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.110.144\"\n    },\n    {\n      \"id\": 811,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.110.113\"\n    },\n    {\n      \"id\": 812,\n      \"type\": \"ip\",\n      \"ip\": \"110.42.200.96\"\n    },\n    {\n      \"id\": 813,\n      \"type\": \"ip\",\n      \"ip\": \"194.195.244.69\"\n    },\n    {\n      \"id\": 814,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.103.119\"\n    },\n    {\n      \"id\": 815,\n      \"type\": \"ip\",\n      \"ip\": \"157.90.35.190\"\n    },\n    {\n      \"id\": 816,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.222.206\"\n    },\n    {\n      \"id\": 817,\n      \"type\": \"ip\",\n      \"ip\": \"143.110.210.200\"\n    },\n    {\n      \"id\": 818,\n      \"type\": \"ip\",\n      \"ip\": \"51.15.235.211\"\n    },\n    {\n      \"id\": 819,\n      \"type\": \"ip\",\n      \"ip\": \"192.42.116.28\"\n    },\n    {\n      \"id\": 820,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.31.87\"\n    },\n    {\n      \"id\": 821,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.106.213\"\n    },\n    {\n      \"id\": 822,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.28\"\n    },\n    {\n      \"id\": 823,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.106.60\"\n    },\n    {\n      \"id\": 824,\n      \"type\": \"ip\",\n      \"ip\": \"109.70.100.24\"\n    },\n    {\n      \"id\": 825,\n      \"type\": \"ip\",\n      \"ip\": \"5.182.210.155\"\n    },\n    {\n      \"id\": 826,\n      \"type\": \"ip\",\n      \"ip\": \"46.232.251.191\"\n    },\n    {\n      \"id\": 827,\n      \"type\": \"ip\",\n      \"ip\": \"89.163.249.192\"\n    },\n    {\n      \"id\": 828,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.3\"\n    },\n    {\n      \"id\": 829,\n      \"type\": \"ip\",\n      \"ip\": \"120.195.30.152\"\n    },\n    {\n      \"id\": 830,\n      \"type\": \"ip\",\n      \"ip\": \"165.227.239.108\"\n    },\n    {\n      \"id\": 831,\n      \"type\": \"ip\",\n      \"ip\": \"199.195.249.16\"\n    },\n    {\n      \"id\": 832,\n      \"type\": \"ip\",\n      \"ip\": \"49.234.81.169\"\n    },\n    {\n      \"id\": 833,\n      \"type\": \"ip\",\n      \"ip\": \"5.182.210.216\"\n    },\n    {\n      \"id\": 834,\n      \"type\": \"ip\",\n      \"ip\": \"89.236.112.100\"\n    },\n    {\n      \"id\": 835,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.10.150\"\n    },\n    {\n      \"id\": 836,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.142\"\n    },\n    {\n      \"id\": 837,\n      \"type\": \"ip\",\n      \"ip\": \"113.98.224.68\"\n    },\n    {\n      \"id\": 838,\n      \"type\": \"ip\",\n      \"ip\": \"92.246.84.133\"\n    },\n    {\n      \"id\": 839,\n      \"type\": \"ip\",\n      \"ip\": \"158.101.118.198\"\n    },\n    {\n      \"id\": 840,\n      \"type\": \"ip\",\n      \"ip\": \"185.100.87.41\"\n    },\n    {\n      \"id\": 841,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.60\"\n    },\n    {\n      \"id\": 842,\n      \"type\": \"ip\",\n      \"ip\": \"45.61.188.164\"\n    },\n    {\n      \"id\": 843,\n      \"type\": \"ip\",\n      \"ip\": \"89.163.252.30\"\n    },\n    {\n      \"id\": 844,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.17\"\n    },\n    {\n      \"id\": 845,\n      \"type\": \"ip\",\n      \"ip\": \"31.42.186.101\"\n    },\n    {\n      \"id\": 846,\n      \"type\": \"ip\",\n      \"ip\": \"45.153.160.140\"\n    },\n    {\n      \"id\": 847,\n      \"type\": \"ip\",\n      \"ip\": \"221.228.87.37\"\n    },\n    {\n      \"id\": 848,\n      \"type\": \"ip\",\n      \"ip\": \"198.98.59.65\"\n    },\n    {\n      \"id\": 849,\n      \"type\": \"ip\",\n      \"ip\": \"41.203.140.114\"\n    },\n    {\n      \"id\": 850,\n      \"type\": \"ip\",\n      \"ip\": \"185.243.218.50\"\n    },\n    {\n      \"id\": 851,\n      \"type\": \"ip\",\n      \"ip\": \"185.170.114.25\"\n    },\n    {\n      \"id\": 852,\n      \"type\": \"ip\",\n      \"ip\": \"167.71.175.10\"\n    },\n    {\n      \"id\": 853,\n      \"type\": \"ip\",\n      \"ip\": \"109.70.100.21\"\n    },\n    {\n      \"id\": 854,\n      \"type\": \"ip\",\n      \"ip\": \"198.98.56.248\"\n    },\n    {\n      \"id\": 855,\n      \"type\": \"ip\",\n      \"ip\": \"103.103.0.142\"\n    },\n    {\n      \"id\": 856,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.12.169\"\n    },\n    {\n      \"id\": 857,\n      \"type\": \"ip\",\n      \"ip\": \"111.59.85.209\"\n    },\n    {\n      \"id\": 858,\n      \"type\": \"ip\",\n      \"ip\": \"103.214.5.13\"\n    },\n    {\n      \"id\": 859,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.134\"\n    },\n    {\n      \"id\": 860,\n      \"type\": \"ip\",\n      \"ip\": \"195.176.3.24\"\n    },\n    {\n      \"id\": 861,\n      \"type\": \"ip\",\n      \"ip\": \"61.19.25.207\"\n    },\n    {\n      \"id\": 862,\n      \"type\": \"ip\",\n      \"ip\": \"154.65.28.250\"\n    },\n    {\n      \"id\": 863,\n      \"type\": \"ip\",\n      \"ip\": \"194.163.163.20\"\n    },\n    {\n      \"id\": 864,\n      \"type\": \"ip\",\n      \"ip\": \"165.227.10.212\"\n    },\n    {\n      \"id\": 865,\n      \"type\": \"ip\",\n      \"ip\": \"203.175.12.100\"\n    },\n    {\n      \"id\": 866,\n      \"type\": \"ip\",\n      \"ip\": \"109.70.100.31\"\n    },\n    {\n      \"id\": 867,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.154\"\n    },\n    {\n      \"id\": 868,\n      \"type\": \"ip\",\n      \"ip\": \"116.62.20.122\"\n    },\n    {\n      \"id\": 869,\n      \"type\": \"ip\",\n      \"ip\": \"45.155.204.20\"\n    },\n    {\n      \"id\": 870,\n      \"type\": \"ip\",\n      \"ip\": \"66.220.242.222\"\n    },\n    {\n      \"id\": 871,\n      \"type\": \"ip\",\n      \"ip\": \"178.17.170.135\"\n    },\n    {\n      \"id\": 872,\n      \"type\": \"ip\",\n      \"ip\": \"193.218.118.231\"\n    },\n    {\n      \"id\": 873,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.77.53\"\n    },\n    {\n      \"id\": 874,\n      \"type\": \"ip\",\n      \"ip\": \"193.189.100.200\"\n    },\n    {\n      \"id\": 875,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.102.7\"\n    },\n    {\n      \"id\": 876,\n      \"type\": \"ip\",\n      \"ip\": \"109.201.133.100\"\n    },\n    {\n      \"id\": 877,\n      \"type\": \"ip\",\n      \"ip\": \"37.123.163.58\"\n    },\n    {\n      \"id\": 878,\n      \"type\": \"ip\",\n      \"ip\": \"178.17.170.23\"\n    },\n    {\n      \"id\": 879,\n      \"type\": \"ip\",\n      \"ip\": \"159.89.159.225\"\n    },\n    {\n      \"id\": 880,\n      \"type\": \"ip\",\n      \"ip\": \"198.98.51.189\"\n    },\n    {\n      \"id\": 881,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.143\"\n    },\n    {\n      \"id\": 882,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.169\"\n    },\n    {\n      \"id\": 883,\n      \"type\": \"ip\",\n      \"ip\": \"199.195.248.29\"\n    },\n    {\n      \"id\": 884,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.59\"\n    },\n    {\n      \"id\": 885,\n      \"type\": \"ip\",\n      \"ip\": \"198.98.57.207\"\n    },\n    {\n      \"id\": 886,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.138\"\n    },\n    {\n      \"id\": 887,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.170\"\n    },\n    {\n      \"id\": 888,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.76.170\"\n    },\n    {\n      \"id\": 889,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.180\"\n    },\n    {\n      \"id\": 890,\n      \"type\": \"ip\",\n      \"ip\": \"159.89.115.238\"\n    },\n    {\n      \"id\": 891,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.41\"\n    },\n    {\n      \"id\": 892,\n      \"type\": \"ip\",\n      \"ip\": \"176.10.99.200\"\n    },\n    {\n      \"id\": 893,\n      \"type\": \"ip\",\n      \"ip\": \"45.153.160.132\"\n    },\n    {\n      \"id\": 894,\n      \"type\": \"ip\",\n      \"ip\": \"209.141.58.146\"\n    },\n    {\n      \"id\": 895,\n      \"type\": \"ip\",\n      \"ip\": \"209.141.34.232\"\n    },\n    {\n      \"id\": 896,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.47\"\n    },\n    {\n      \"id\": 897,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.140\"\n    },\n    {\n      \"id\": 898,\n      \"type\": \"ip\",\n      \"ip\": \"185.100.87.202\"\n    },\n    {\n      \"id\": 899,\n      \"type\": \"ip\",\n      \"ip\": \"209.141.59.180\"\n    },\n    {\n      \"id\": 900,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.49\"\n    },\n    {\n      \"id\": 901,\n      \"type\": \"ip\",\n      \"ip\": \"64.227.188.161\"\n    },\n    {\n      \"id\": 902,\n      \"type\": \"ip\",\n      \"ip\": \"82.221.131.71\"\n    },\n    {\n      \"id\": 903,\n      \"type\": \"ip\",\n      \"ip\": \"159.89.159.204\"\n    },\n    {\n      \"id\": 904,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.37\"\n    },\n    {\n      \"id\": 905,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.168\"\n    },\n    {\n      \"id\": 906,\n      \"type\": \"ip\",\n      \"ip\": \"165.227.26.113\"\n    },\n    {\n      \"id\": 907,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.150\"\n    },\n    {\n      \"id\": 908,\n      \"type\": \"ip\",\n      \"ip\": \"45.153.160.130\"\n    },\n    {\n      \"id\": 909,\n      \"type\": \"ip\",\n      \"ip\": \"213.202.216.189\"\n    },\n    {\n      \"id\": 910,\n      \"type\": \"ip\",\n      \"ip\": \"162.247.74.74\"\n    },\n    {\n      \"id\": 911,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.63\"\n    },\n    {\n      \"id\": 912,\n      \"type\": \"ip\",\n      \"ip\": \"91.219.237.21\"\n    },\n    {\n      \"id\": 913,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.159\"\n    },\n    {\n      \"id\": 914,\n      \"type\": \"ip\",\n      \"ip\": \"193.31.24.154\"\n    },\n    {\n      \"id\": 915,\n      \"type\": \"ip\",\n      \"ip\": \"193.218.118.183\"\n    },\n    {\n      \"id\": 916,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.62\"\n    },\n    {\n      \"id\": 917,\n      \"type\": \"ip\",\n      \"ip\": \"178.17.171.102\"\n    },\n    {\n      \"id\": 918,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.135\"\n    },\n    {\n      \"id\": 919,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.182\"\n    },\n    {\n      \"id\": 920,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.191\"\n    },\n    {\n      \"id\": 921,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.35\"\n    },\n    {\n      \"id\": 922,\n      \"type\": \"ip\",\n      \"ip\": \"45.192.178.218\"\n    },\n    {\n      \"id\": 923,\n      \"type\": \"ip\",\n      \"ip\": \"94.230.208.147\"\n    },\n    {\n      \"id\": 924,\n      \"type\": \"ip\",\n      \"ip\": \"185.51.76.187\"\n    },\n    {\n      \"id\": 925,\n      \"type\": \"ip\",\n      \"ip\": \"165.232.92.7\"\n    },\n    {\n      \"id\": 926,\n      \"type\": \"ip\",\n      \"ip\": \"185.38.175.131\"\n    },\n    {\n      \"id\": 927,\n      \"type\": \"ip\",\n      \"ip\": \"23.154.177.7\"\n    },\n    {\n      \"id\": 928,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.103.7\"\n    },\n    {\n      \"id\": 929,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.6.166\"\n    },\n    {\n      \"id\": 930,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.132\"\n    },\n    {\n      \"id\": 931,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.148\"\n    },\n    {\n      \"id\": 932,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.54\"\n    },\n    {\n      \"id\": 933,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.102.253\"\n    },\n    {\n      \"id\": 934,\n      \"type\": \"ip\",\n      \"ip\": \"151.80.148.159\"\n    },\n    {\n      \"id\": 935,\n      \"type\": \"ip\",\n      \"ip\": \"51.15.244.188\"\n    },\n    {\n      \"id\": 936,\n      \"type\": \"ip\",\n      \"ip\": \"45.153.160.129\"\n    },\n    {\n      \"id\": 937,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.152\"\n    },\n    {\n      \"id\": 938,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.172\"\n    },\n    {\n      \"id\": 939,\n      \"type\": \"ip\",\n      \"ip\": \"193.189.100.206\"\n    },\n    {\n      \"id\": 940,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.136\"\n    },\n    {\n      \"id\": 941,\n      \"type\": \"ip\",\n      \"ip\": \"193.110.95.34\"\n    },\n    {\n      \"id\": 942,\n      \"type\": \"ip\",\n      \"ip\": \"128.31.0.13\"\n    },\n    {\n      \"id\": 943,\n      \"type\": \"ip\",\n      \"ip\": \"138.68.248.48\"\n    },\n    {\n      \"id\": 944,\n      \"type\": \"ip\",\n      \"ip\": \"181.214.39.2\"\n    },\n    {\n      \"id\": 945,\n      \"type\": \"ip\",\n      \"ip\": \"103.135.250.20\"\n    },\n    {\n      \"id\": 946,\n      \"type\": \"ip\",\n      \"ip\": \"152.70.110.78\"\n    },\n    {\n      \"id\": 947,\n      \"type\": \"ip\",\n      \"ip\": \"72.223.168.73\"\n    },\n    {\n      \"id\": 948,\n      \"type\": \"ip\",\n      \"ip\": \"54.173.99.121\"\n    },\n    {\n      \"id\": 949,\n      \"type\": \"ip\",\n      \"ip\": \"147.182.179.141\"\n    },\n    {\n      \"id\": 950,\n      \"type\": \"ip\",\n      \"ip\": \"23.129.64.142\"\n    },\n    {\n      \"id\": 951,\n      \"type\": \"ip\",\n      \"ip\": \"185.165.169.18\"\n    },\n    {\n      \"id\": 952,\n      \"type\": \"ip\",\n      \"ip\": \"181.215.140.23\"\n    },\n    {\n      \"id\": 953,\n      \"type\": \"ip\",\n      \"ip\": \"45.146.164.160\"\n    },\n    {\n      \"id\": 954,\n      \"type\": \"ip\",\n      \"ip\": \"52.35.255.211\"\n    },\n    {\n      \"id\": 955,\n      \"type\": \"ip\",\n      \"ip\": \"193.189.100.203\"\n    },\n    {\n      \"id\": 956,\n      \"type\": \"ip\",\n      \"ip\": \"128.199.108.180\"\n    },\n    {\n      \"id\": 957,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.106.58\"\n    },\n    {\n      \"id\": 958,\n      \"type\": \"ip\",\n      \"ip\": \"5.2.72.124\"\n    },\n    {\n      \"id\": 959,\n      \"type\": \"ip\",\n      \"ip\": \"101.32.162.161\"\n    },\n    {\n      \"id\": 960,\n      \"type\": \"ip\",\n      \"ip\": \"45.153.160.2\"\n    },\n    {\n      \"id\": 961,\n      \"type\": \"ip\",\n      \"ip\": \"134.122.34.12\"\n    },\n    {\n      \"id\": 962,\n      \"type\": \"ip\",\n      \"ip\": \"64.227.188.216\"\n    },\n    {\n      \"id\": 963,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.102.243\"\n    },\n    {\n      \"id\": 964,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.147\"\n    },\n    {\n      \"id\": 965,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.102.251\"\n    },\n    {\n      \"id\": 966,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.179\"\n    },\n    {\n      \"id\": 967,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.174\"\n    },\n    {\n      \"id\": 968,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.102.241\"\n    },\n    {\n      \"id\": 969,\n      \"type\": \"ip\",\n      \"ip\": \"209.127.17.242\"\n    },\n    {\n      \"id\": 970,\n      \"type\": \"ip\",\n      \"ip\": \"185.130.44.108\"\n    },\n    {\n      \"id\": 971,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.102.247\"\n    },\n    {\n      \"id\": 972,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.100.246\"\n    },\n    {\n      \"id\": 973,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.183\"\n    },\n    {\n      \"id\": 974,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.100.245\"\n    },\n    {\n      \"id\": 975,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.144\"\n    },\n    {\n      \"id\": 976,\n      \"type\": \"ip\",\n      \"ip\": \"209.141.41.103\"\n    },\n    {\n      \"id\": 977,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.102.244\"\n    },\n    {\n      \"id\": 978,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.1.178\"\n    },\n    {\n      \"id\": 979,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.173\"\n    },\n    {\n      \"id\": 980,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.137\"\n    },\n    {\n      \"id\": 981,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.36\"\n    },\n    {\n      \"id\": 982,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.184\"\n    },\n    {\n      \"id\": 983,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.190\"\n    },\n    {\n      \"id\": 984,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.102.254\"\n    },\n    {\n      \"id\": 985,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.146\"\n    },\n    {\n      \"id\": 986,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.76.13\"\n    },\n    {\n      \"id\": 987,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.151\"\n    },\n    {\n      \"id\": 988,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.177\"\n    },\n    {\n      \"id\": 989,\n      \"type\": \"ip\",\n      \"ip\": \"185.202.220.75\"\n    },\n    {\n      \"id\": 990,\n      \"type\": \"ip\",\n      \"ip\": \"94.230.208.148\"\n    },\n    {\n      \"id\": 991,\n      \"type\": \"ip\",\n      \"ip\": \"199.195.253.156\"\n    },\n    {\n      \"id\": 992,\n      \"type\": \"ip\",\n      \"ip\": \"139.59.108.5\"\n    },\n    {\n      \"id\": 993,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.55\"\n    },\n    {\n      \"id\": 994,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.39\"\n    },\n    {\n      \"id\": 995,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.31.195\"\n    },\n    {\n      \"id\": 996,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.165\"\n    },\n    {\n      \"id\": 997,\n      \"type\": \"ip\",\n      \"ip\": \"185.107.47.215\"\n    },\n    {\n      \"id\": 998,\n      \"type\": \"ip\",\n      \"ip\": \"81.17.18.62\"\n    },\n    {\n      \"id\": 999,\n      \"type\": \"ip\",\n      \"ip\": \"45.153.160.139\"\n    },\n    {\n      \"id\": 1000,\n      \"type\": \"ip\",\n      \"ip\": \"178.17.174.14\"\n    },\n    {\n      \"id\": 1001,\n      \"type\": \"ip\",\n      \"ip\": \"185.38.175.130\"\n    },\n    {\n      \"id\": 1002,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.145\"\n    },\n    {\n      \"id\": 1003,\n      \"type\": \"ip\",\n      \"ip\": \"159.65.106.11\"\n    },\n    {\n      \"id\": 1004,\n      \"type\": \"ip\",\n      \"ip\": \"122.117.91.144\"\n    },\n    {\n      \"id\": 1005,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.53\"\n    },\n    {\n      \"id\": 1006,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.73.126\"\n    },\n    {\n      \"id\": 1007,\n      \"type\": \"ip\",\n      \"ip\": \"34.65.121.142\"\n    },\n    {\n      \"id\": 1008,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.167\"\n    },\n    {\n      \"id\": 1009,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.61\"\n    },\n    {\n      \"id\": 1010,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.153\"\n    },\n    {\n      \"id\": 1011,\n      \"type\": \"ip\",\n      \"ip\": \"185.10.68.168\"\n    },\n    {\n      \"id\": 1012,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.100.250\"\n    },\n    {\n      \"id\": 1013,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.51\"\n    },\n    {\n      \"id\": 1014,\n      \"type\": \"ip\",\n      \"ip\": \"37.187.96.183\"\n    },\n    {\n      \"id\": 1015,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.162\"\n    },\n    {\n      \"id\": 1016,\n      \"type\": \"ip\",\n      \"ip\": \"185.112.144.68\"\n    },\n    {\n      \"id\": 1017,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.157\"\n    },\n    {\n      \"id\": 1018,\n      \"type\": \"ip\",\n      \"ip\": \"205.185.117.149\"\n    },\n    {\n      \"id\": 1019,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.161\"\n    },\n    {\n      \"id\": 1020,\n      \"type\": \"ip\",\n      \"ip\": \"194.48.199.78\"\n    },\n    {\n      \"id\": 1021,\n      \"type\": \"ip\",\n      \"ip\": \"47.252.38.12\"\n    },\n    {\n      \"id\": 1022,\n      \"type\": \"ip\",\n      \"ip\": \"37.120.247.125\"\n    },\n    {\n      \"id\": 1023,\n      \"type\": \"ip\",\n      \"ip\": \"47.90.161.18\"\n    },\n    {\n      \"id\": 1024,\n      \"type\": \"ip\",\n      \"ip\": \"47.253.82.78\"\n    },\n    {\n      \"id\": 1025,\n      \"type\": \"ip\",\n      \"ip\": \"109.237.96.124\"\n    },\n    {\n      \"id\": 1026,\n      \"type\": \"ip\",\n      \"ip\": \"23.224.189.52\"\n    },\n    {\n      \"id\": 1027,\n      \"type\": \"ip\",\n      \"ip\": \"212.193.57.225\"\n    },\n    {\n      \"id\": 1028,\n      \"type\": \"ip\",\n      \"ip\": \"195.19.192.26\"\n    },\n    {\n      \"id\": 1029,\n      \"type\": \"ip\",\n      \"ip\": \"62.76.41.46\"\n    },\n    {\n      \"id\": 1030,\n      \"type\": \"ip\",\n      \"ip\": \"23.129.64.141\"\n    },\n    {\n      \"id\": 1031,\n      \"type\": \"ip\",\n      \"ip\": \"23.129.64.132\"\n    },\n    {\n      \"id\": 1032,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.102.249\"\n    },\n    {\n      \"id\": 1033,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.189\"\n    },\n    {\n      \"id\": 1034,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.40\"\n    },\n    {\n      \"id\": 1035,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.42\"\n    },\n    {\n      \"id\": 1036,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.100.242\"\n    },\n    {\n      \"id\": 1037,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.133\"\n    },\n    {\n      \"id\": 1038,\n      \"type\": \"ip\",\n      \"ip\": \"209.127.17.234\"\n    },\n    {\n      \"id\": 1039,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.28.84\"\n    },\n    {\n      \"id\": 1040,\n      \"type\": \"ip\",\n      \"ip\": \"166.70.207.2\"\n    },\n    {\n      \"id\": 1041,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.100.254\"\n    },\n    {\n      \"id\": 1042,\n      \"type\": \"ip\",\n      \"ip\": \"62.102.148.69\"\n    },\n    {\n      \"id\": 1043,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.46\"\n    },\n    {\n      \"id\": 1044,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.48\"\n    },\n    {\n      \"id\": 1045,\n      \"type\": \"ip\",\n      \"ip\": \"199.195.250.77\"\n    },\n    {\n      \"id\": 1046,\n      \"type\": \"ip\",\n      \"ip\": \"23.129.64.130\"\n    },\n    {\n      \"id\": 1047,\n      \"type\": \"ip\",\n      \"ip\": \"23.129.64.146\"\n    },\n    {\n      \"id\": 1048,\n      \"type\": \"ip\",\n      \"ip\": \"193.201.9.212\"\n    },\n    {\n      \"id\": 1049,\n      \"type\": \"ip\",\n      \"ip\": \"172.106.17.218\"\n    },\n    {\n      \"id\": 1050,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.74.57\"\n    },\n    {\n      \"id\": 1051,\n      \"type\": \"ip\",\n      \"ip\": \"46.166.139.111\"\n    },\n    {\n      \"id\": 1052,\n      \"type\": \"ip\",\n      \"ip\": \"107.189.1.160\"\n    },\n    {\n      \"id\": 1053,\n      \"type\": \"ip\",\n      \"ip\": \"62.102.148.68\"\n    },\n    {\n      \"id\": 1054,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.188\"\n    },\n    {\n      \"id\": 1055,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.163\"\n    },\n    {\n      \"id\": 1056,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.33\"\n    },\n    {\n      \"id\": 1057,\n      \"type\": \"ip\",\n      \"ip\": \"45.12.134.108\"\n    },\n    {\n      \"id\": 1058,\n      \"type\": \"ip\",\n      \"ip\": \"163.172.213.212\"\n    },\n    {\n      \"id\": 1059,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.44\"\n    },\n    {\n      \"id\": 1060,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.56\"\n    },\n    {\n      \"id\": 1061,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.45\"\n    },\n    {\n      \"id\": 1062,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.149\"\n    },\n    {\n      \"id\": 1063,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.58\"\n    },\n    {\n      \"id\": 1064,\n      \"type\": \"ip\",\n      \"ip\": \"5.189.162.164\"\n    },\n    {\n      \"id\": 1065,\n      \"type\": \"ip\",\n      \"ip\": \"183.220.31.71\"\n    },\n    {\n      \"id\": 1066,\n      \"type\": \"ip\",\n      \"ip\": \"23.129.64.138\"\n    },\n    {\n      \"id\": 1067,\n      \"type\": \"ip\",\n      \"ip\": \"23.129.64.147\"\n    },\n    {\n      \"id\": 1068,\n      \"type\": \"ip\",\n      \"ip\": \"23.129.64.135\"\n    },\n    {\n      \"id\": 1069,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.38\"\n    },\n    {\n      \"id\": 1070,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.100.243\"\n    },\n    {\n      \"id\": 1071,\n      \"type\": \"ip\",\n      \"ip\": \"171.25.193.25\"\n    },\n    {\n      \"id\": 1072,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.76.44\"\n    },\n    {\n      \"id\": 1073,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.160\"\n    },\n    {\n      \"id\": 1074,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.158\"\n    },\n    {\n      \"id\": 1075,\n      \"type\": \"ip\",\n      \"ip\": \"185.83.214.69\"\n    },\n    {\n      \"id\": 1076,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.100.248\"\n    },\n    {\n      \"id\": 1077,\n      \"type\": \"ip\",\n      \"ip\": \"185.100.86.128\"\n    },\n    {\n      \"id\": 1078,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.175\"\n    },\n    {\n      \"id\": 1079,\n      \"type\": \"ip\",\n      \"ip\": \"23.154.177.4\"\n    },\n    {\n      \"id\": 1080,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.164\"\n    },\n    {\n      \"id\": 1081,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.100.249\"\n    },\n    {\n      \"id\": 1082,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.185\"\n    },\n    {\n      \"id\": 1083,\n      \"type\": \"ip\",\n      \"ip\": \"45.154.255.147\"\n    },\n    {\n      \"id\": 1084,\n      \"type\": \"ip\",\n      \"ip\": \"185.113.128.30\"\n    },\n    {\n      \"id\": 1085,\n      \"type\": \"ip\",\n      \"ip\": \"23.236.146.162\"\n    },\n    {\n      \"id\": 1086,\n      \"type\": \"ip\",\n      \"ip\": \"209.141.45.189\"\n    },\n    {\n      \"id\": 1087,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.43\"\n    },\n    {\n      \"id\": 1088,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.57\"\n    },\n    {\n      \"id\": 1089,\n      \"type\": \"ip\",\n      \"ip\": \"185.107.70.56\"\n    },\n    {\n      \"id\": 1090,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.171\"\n    },\n    {\n      \"id\": 1091,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.156\"\n    },\n    {\n      \"id\": 1092,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.100.247\"\n    },\n    {\n      \"id\": 1093,\n      \"type\": \"ip\",\n      \"ip\": \"185.38.175.132\"\n    },\n    {\n      \"id\": 1094,\n      \"type\": \"ip\",\n      \"ip\": \"188.166.74.97\"\n    },\n    {\n      \"id\": 1095,\n      \"type\": \"ip\",\n      \"ip\": \"194.233.71.145\"\n    },\n    {\n      \"id\": 1096,\n      \"type\": \"ip\",\n      \"ip\": \"195.54.160.149\"\n    },\n    {\n      \"id\": 1097,\n      \"type\": \"ip\",\n      \"ip\": \"45.155.205.233\"\n    },\n    {\n      \"id\": 1098,\n      \"type\": \"ip\",\n      \"ip\": \"23.129.64.145\"\n    },\n    {\n      \"id\": 1099,\n      \"type\": \"ip\",\n      \"ip\": \"23.129.64.149\"\n    },\n    {\n      \"id\": 1100,\n      \"type\": \"ip\",\n      \"ip\": \"23.129.64.133\"\n    },\n    {\n      \"id\": 1101,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.100.244\"\n    },\n    {\n      \"id\": 1102,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.139\"\n    },\n    {\n      \"id\": 1103,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.100.253\"\n    },\n    {\n      \"id\": 1104,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.176\"\n    },\n    {\n      \"id\": 1105,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.100.241\"\n    },\n    {\n      \"id\": 1106,\n      \"type\": \"ip\",\n      \"ip\": \"23.129.64.144\"\n    },\n    {\n      \"id\": 1107,\n      \"type\": \"ip\",\n      \"ip\": \"23.129.64.148\"\n    },\n    {\n      \"id\": 1108,\n      \"type\": \"ip\",\n      \"ip\": \"23.129.64.140\"\n    },\n    {\n      \"id\": 1109,\n      \"type\": \"ip\",\n      \"ip\": \"23.129.64.134\"\n    },\n    {\n      \"id\": 1110,\n      \"type\": \"ip\",\n      \"ip\": \"23.129.64.136\"\n    },\n    {\n      \"id\": 1111,\n      \"type\": \"ip\",\n      \"ip\": \"185.14.97.147\"\n    },\n    {\n      \"id\": 1112,\n      \"type\": \"ip\",\n      \"ip\": \"51.15.43.205\"\n    },\n    {\n      \"id\": 1113,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.155\"\n    },\n    {\n      \"id\": 1114,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.100.251\"\n    },\n    {\n      \"id\": 1115,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.32\"\n    },\n    {\n      \"id\": 1116,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.181\"\n    },\n    {\n      \"id\": 1117,\n      \"type\": \"ip\",\n      \"ip\": \"23.129.64.143\"\n    },\n    {\n      \"id\": 1118,\n      \"type\": \"ip\",\n      \"ip\": \"23.129.64.137\"\n    },\n    {\n      \"id\": 1119,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.166\"\n    },\n    {\n      \"id\": 1120,\n      \"type\": \"ip\",\n      \"ip\": \"104.244.72.115\"\n    },\n    {\n      \"id\": 1121,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.50\"\n    },\n    {\n      \"id\": 1122,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.100.252\"\n    },\n    {\n      \"id\": 1123,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.100.240\"\n    },\n    {\n      \"id\": 1124,\n      \"type\": \"ip\",\n      \"ip\": \"18.27.197.252\"\n    },\n    {\n      \"id\": 1125,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.34\"\n    },\n    {\n      \"id\": 1126,\n      \"type\": \"ip\",\n      \"ip\": \"23.129.64.139\"\n    },\n    {\n      \"id\": 1127,\n      \"type\": \"ip\",\n      \"ip\": \"23.129.64.131\"\n    },\n    {\n      \"id\": 1128,\n      \"type\": \"ip\",\n      \"ip\": \"185.107.47.171\"\n    },\n    {\n      \"id\": 1129,\n      \"type\": \"ip\",\n      \"ip\": \"185.56.80.65\"\n    },\n    {\n      \"id\": 1130,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.101.52\"\n    },\n    {\n      \"id\": 1131,\n      \"type\": \"ip\",\n      \"ip\": \"121.5.226.36\"\n    },\n    {\n      \"id\": 1132,\n      \"type\": \"ip\",\n      \"ip\": \"121.5.219.20\"\n    },\n    {\n      \"id\": 1133,\n      \"type\": \"ip\",\n      \"ip\": \"45.129.56.200\"\n    },\n    {\n      \"id\": 1134,\n      \"type\": \"ip\",\n      \"ip\": \"171.25.193.78\"\n    },\n    {\n      \"id\": 1135,\n      \"type\": \"ip\",\n      \"ip\": \"171.25.193.77\"\n    },\n    {\n      \"id\": 1136,\n      \"type\": \"ip\",\n      \"ip\": \"204.8.156.142\"\n    },\n    {\n      \"id\": 1137,\n      \"type\": \"ip\",\n      \"ip\": \"185.220.100.255\"\n    },\n    {\n      \"id\": 1138,\n      \"type\": \"ip\",\n      \"ip\": \"42.192.17.155\"\n    },\n    {\n      \"id\": 1139,\n      \"type\": \"ip\",\n      \"ip\": \"121.5.109.55\"\n    },\n    {\n      \"id\": 1140,\n      \"type\": \"ip\",\n      \"ip\": \"121.5.113.11\"\n    },\n    {\n      \"id\": 1141,\n      \"type\": \"ip\",\n      \"ip\": \"121.4.181.178\"\n    },\n    {\n      \"id\": 1142,\n      \"type\": \"ip\",\n      \"ip\": \"49.234.43.244\"\n    },\n    {\n      \"id\": 1143,\n      \"type\": \"ip\",\n      \"ip\": \"42.193.16.135\"\n    },\n    {\n      \"id\": 1144,\n      \"type\": \"ip\",\n      \"ip\": \"42.193.8.97\"\n    },\n    {\n      \"id\": 1145,\n      \"type\": \"ip\",\n      \"ip\": \"199.249.230.119\"\n    },\n    {\n      \"id\": 1146,\n      \"type\": \"ip\",\n      \"ip\": \"199.249.230.163\"\n    },\n    {\n      \"id\": 1147,\n      \"type\": \"ip\",\n      \"ip\": \"64.113.32.29\"\n    },\n    {\n      \"id\": 1148,\n      \"type\": \"ip\",\n      \"ip\": \"171.25.193.20\"\n    },\n    {\n      \"id\": 1149,\n      \"type\": \"ip\",\n      \"ip\": \"1.15.175.155\"\n    },\n    {\n      \"id\": 1150,\n      \"type\": \"ip\",\n      \"ip\": \"42.193.23.161\"\n    },\n    {\n      \"id\": 1151,\n      \"type\": \"ip\",\n      \"ip\": \"1.14.17.89\"\n    },\n    {\n      \"id\": 1152,\n      \"type\": \"ip\",\n      \"ip\": \"42.193.23.126\"\n    },\n    {\n      \"id\": 1153,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"22\"\n    },\n    {\n      \"id\": 1154,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"80\"\n    },\n    {\n      \"id\": 1155,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"443\"\n    },\n    {\n      \"id\": 1156,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3306\"\n    },\n    {\n      \"id\": 1157,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3000\"\n    },\n    {\n      \"id\": 1158,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"7946\"\n    },\n    {\n      \"id\": 1159,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8080\"\n    },\n    {\n      \"id\": 1160,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9000\"\n    },\n    {\n      \"id\": 1161,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9443\"\n    },\n    {\n      \"id\": 1162,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1935\"\n    },\n    {\n      \"id\": 1163,\n      \"type\": \"domain\",\n      \"domain\": \"www.cam4.de.com\"\n    },\n    {\n      \"id\": 1164,\n      \"type\": \"domain\",\n      \"domain\": \"cam4.de.com\"\n    },\n    {\n      \"id\": 1165,\n      \"type\": \"domain\",\n      \"domain\": \"cam4.es\"\n    },\n    {\n      \"id\": 1166,\n      \"type\": \"domain\",\n      \"domain\": \"cam4.com\"\n    },\n    {\n      \"id\": 1167,\n      \"type\": \"domain\",\n      \"domain\": \"www.cam4.fr\"\n    },\n    {\n      \"id\": 1168,\n      \"type\": \"domain\",\n      \"domain\": \"www.cam4.nl\"\n    },\n    {\n      \"id\": 1169,\n      \"type\": \"domain\",\n      \"domain\": \"cam4.fr\"\n    },\n    {\n      \"id\": 1170,\n      \"type\": \"domain\",\n      \"domain\": \"cam4.it\"\n    },\n    {\n      \"id\": 1171,\n      \"type\": \"domain\",\n      \"domain\": \"www.cam4.it\"\n    },\n    {\n      \"id\": 1172,\n      \"type\": \"domain\",\n      \"domain\": \"cam4.nl\"\n    },\n    {\n      \"id\": 1173,\n      \"type\": \"domain\",\n      \"domain\": \"www.cam4.es\"\n    },\n    {\n      \"id\": 1174,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"25565\"\n    },\n    {\n      \"id\": 1175,\n      \"type\": \"domain\",\n      \"domain\": \"v118-27-36-56.t2w4.static.cnode.io\"\n    },\n    {\n      \"id\": 1176,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"123\"\n    },\n    {\n      \"id\": 1177,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4443\"\n    },\n    {\n      \"id\": 1178,\n      \"type\": \"domain\",\n      \"domain\": \"snsn.spbmn.com\"\n    },\n    {\n      \"id\": 1179,\n      \"type\": \"domain\",\n      \"domain\": \"testtesttestdomain.nl\"\n    },\n    {\n      \"id\": 1180,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3389\"\n    },\n    {\n      \"id\": 1181,\n      \"type\": \"domain\",\n      \"domain\": \"uptime.revalin.com\"\n    },\n    {\n      \"id\": 1182,\n      \"type\": \"domain\",\n      \"domain\": \"139-177-178-126.ip.linodeusercontent.com\"\n    },\n    {\n      \"id\": 1183,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"21\"\n    },\n    {\n      \"id\": 1184,\n      \"type\": \"domain\",\n      \"domain\": \"www.satelliteteams.com\"\n    },\n    {\n      \"id\": 1185,\n      \"type\": \"domain\",\n      \"domain\": \"satelliteteams.com\"\n    },\n    {\n      \"id\": 1186,\n      \"type\": \"domain\",\n      \"domain\": \"1050322.cloudwaysapps.com\"\n    },\n    {\n      \"id\": 1187,\n      \"type\": \"domain\",\n      \"domain\": \"cloudwaysapps.com\"\n    },\n    {\n      \"id\": 1188,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"135\"\n    },\n    {\n      \"id\": 1189,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"139\"\n    },\n    {\n      \"id\": 1190,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1723\"\n    },\n    {\n      \"id\": 1191,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5985\"\n    },\n    {\n      \"id\": 1192,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8090\"\n    },\n    {\n      \"id\": 1193,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"33060\"\n    },\n    {\n      \"id\": 1194,\n      \"type\": \"domain\",\n      \"domain\": \"ecs-121-36-213-142.compute.hwclouds-dns.com\"\n    },\n    {\n      \"id\": 1195,\n      \"type\": \"domain\",\n      \"domain\": \"bonbon.co.id\"\n    },\n    {\n      \"id\": 1196,\n      \"type\": \"domain\",\n      \"domain\": \"172-104-179-8.ip.linodeusercontent.com\"\n    },\n    {\n      \"id\": 1197,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8081\"\n    },\n    {\n      \"id\": 1198,\n      \"type\": \"domain\",\n      \"domain\": \"172-104-161-134.ip.linodeusercontent.com\"\n    },\n    {\n      \"id\": 1199,\n      \"type\": \"domain\",\n      \"domain\": \"liftyad.xyz\"\n    },\n    {\n      \"id\": 1200,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10134\"\n    },\n    {\n      \"id\": 1201,\n      \"type\": \"domain\",\n      \"domain\": \"tor-exit-131.relayon.org\"\n    },\n    {\n      \"id\": 1202,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"887\"\n    },\n    {\n      \"id\": 1203,\n      \"type\": \"domain\",\n      \"domain\": \"absolutebagels.shop\"\n    },\n    {\n      \"id\": 1204,\n      \"type\": \"domain\",\n      \"domain\": \"www.absolutebagels.shop\"\n    },\n    {\n      \"id\": 1205,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"6789\"\n    },\n    {\n      \"id\": 1206,\n      \"type\": \"domain\",\n      \"domain\": \"45.77.33.141.vultrusercontent.com\"\n    },\n    {\n      \"id\": 1207,\n      \"type\": \"domain\",\n      \"domain\": \"report.climbers-club.co.uk\"\n    },\n    {\n      \"id\": 1208,\n      \"type\": \"domain\",\n      \"domain\": \"climbs.climbers-club.co.uk\"\n    },\n    {\n      \"id\": 1209,\n      \"type\": \"domain\",\n      \"domain\": \"rongcloud.cn\"\n    },\n    {\n      \"id\": 1210,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"34034\"\n    },\n    {\n      \"id\": 1211,\n      \"type\": \"domain\",\n      \"domain\": \"172-105-98-92.ip.linodeusercontent.com\"\n    },\n    {\n      \"id\": 1212,\n      \"type\": \"domain\",\n      \"domain\": \"royal.educativo.gt\"\n    },\n    {\n      \"id\": 1213,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3333\"\n    },\n    {\n      \"id\": 1214,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8050\"\n    },\n    {\n      \"id\": 1215,\n      \"type\": \"domain\",\n      \"domain\": \"139-162-161-61.ip.linodeusercontent.com\"\n    },\n    {\n      \"id\": 1216,\n      \"type\": \"domain\",\n      \"domain\": \"dev.healthimpactnews.com\"\n    },\n    {\n      \"id\": 1217,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"53\"\n    },\n    {\n      \"id\": 1218,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"110\"\n    },\n    {\n      \"id\": 1219,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"111\"\n    },\n    {\n      \"id\": 1220,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"143\"\n    },\n    {\n      \"id\": 1221,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"465\"\n    },\n    {\n      \"id\": 1222,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"587\"\n    },\n    {\n      \"id\": 1223,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"993\"\n    },\n    {\n      \"id\": 1224,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"995\"\n    },\n    {\n      \"id\": 1225,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2079\"\n    },\n    {\n      \"id\": 1226,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2082\"\n    },\n    {\n      \"id\": 1227,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2083\"\n    },\n    {\n      \"id\": 1228,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2086\"\n    },\n    {\n      \"id\": 1229,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2087\"\n    },\n    {\n      \"id\": 1230,\n      \"type\": \"domain\",\n      \"domain\": \"192-46-237-121.ip.linodeusercontent.com\"\n    },\n    {\n      \"id\": 1231,\n      \"type\": \"domain\",\n      \"domain\": \"www.excellent.edu.rs\"\n    },\n    {\n      \"id\": 1232,\n      \"type\": \"domain\",\n      \"domain\": \"excellent.edu.rs\"\n    },\n    {\n      \"id\": 1233,\n      \"type\": \"domain\",\n      \"domain\": \"server.ittrend.rs\"\n    },\n    {\n      \"id\": 1234,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1701\"\n    },\n    {\n      \"id\": 1235,\n      \"type\": \"domain\",\n      \"domain\": \"36-231-115-58.dynamic-ip.hinet.net\"\n    },\n    {\n      \"id\": 1236,\n      \"type\": \"domain\",\n      \"domain\": \"192-46-237-123.ip.linodeusercontent.com\"\n    },\n    {\n      \"id\": 1237,\n      \"type\": \"domain\",\n      \"domain\": \"ics.com.mk\"\n    },\n    {\n      \"id\": 1238,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4000\"\n    },\n    {\n      \"id\": 1239,\n      \"type\": \"domain\",\n      \"domain\": \"atmconsole.com\"\n    },\n    {\n      \"id\": 1240,\n      \"type\": \"domain\",\n      \"domain\": \"139-177-180-114.ip.linodeusercontent.com\"\n    },\n    {\n      \"id\": 1241,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1234\"\n    },\n    {\n      \"id\": 1242,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8181\"\n    },\n    {\n      \"id\": 1243,\n      \"type\": \"domain\",\n      \"domain\": \"vps-2dad26bc.vps.ovh.net\"\n    },\n    {\n      \"id\": 1244,\n      \"type\": \"domain\",\n      \"domain\": \"c7985.cloudnet.cloud\"\n    },\n    {\n      \"id\": 1245,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8888\"\n    },\n    {\n      \"id\": 1246,\n      \"type\": \"domain\",\n      \"domain\": \"99.69.38.89.baremetal.zare.com\"\n    },\n    {\n      \"id\": 1247,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"943\"\n    },\n    {\n      \"id\": 1248,\n      \"type\": \"domain\",\n      \"domain\": \"www.georgiabulldogsjerseys.com\"\n    },\n    {\n      \"id\": 1249,\n      \"type\": \"domain\",\n      \"domain\": \"tor-exit-05.darklab.sh\"\n    },\n    {\n      \"id\": 1250,\n      \"type\": \"domain\",\n      \"domain\": \"georgiabulldogsjerseys.com\"\n    },\n    {\n      \"id\": 1251,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9080\"\n    },\n    {\n      \"id\": 1252,\n      \"type\": \"domain\",\n      \"domain\": \"icra.mx\"\n    },\n    {\n      \"id\": 1253,\n      \"type\": \"domain\",\n      \"domain\": \"www.icra.mx\"\n    },\n    {\n      \"id\": 1254,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"81\"\n    },\n    {\n      \"id\": 1255,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8089\"\n    },\n    {\n      \"id\": 1256,\n      \"type\": \"domain\",\n      \"domain\": \"uisp.com\"\n    },\n    {\n      \"id\": 1257,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8443\"\n    },\n    {\n      \"id\": 1258,\n      \"type\": \"domain\",\n      \"domain\": \"198-58-104-209.ip.linodeusercontent.com\"\n    },\n    {\n      \"id\": 1259,\n      \"type\": \"domain\",\n      \"domain\": \"167.179.65.77.vultrusercontent.com\"\n    },\n    {\n      \"id\": 1260,\n      \"type\": \"domain\",\n      \"domain\": \"ficm-testing.cossette.digital\"\n    },\n    {\n      \"id\": 1261,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"82\"\n    },\n    {\n      \"id\": 1262,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"161\"\n    },\n    {\n      \"id\": 1263,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"444\"\n    },\n    {\n      \"id\": 1264,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5000\"\n    },\n    {\n      \"id\": 1265,\n      \"type\": \"domain\",\n      \"domain\": \"tor-exit-2\"\n    },\n    {\n      \"id\": 1266,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"23\"\n    },\n    {\n      \"id\": 1267,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"24\"\n    },\n    {\n      \"id\": 1268,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"25\"\n    },\n    {\n      \"id\": 1269,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"26\"\n    },\n    {\n      \"id\": 1270,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"79\"\n    },\n    {\n      \"id\": 1271,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"88\"\n    },\n    {\n      \"id\": 1272,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"100\"\n    },\n    {\n      \"id\": 1273,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"102\"\n    },\n    {\n      \"id\": 1274,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"104\"\n    },\n    {\n      \"id\": 1275,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"106\"\n    },\n    {\n      \"id\": 1276,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"113\"\n    },\n    {\n      \"id\": 1277,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"122\"\n    },\n    {\n      \"id\": 1278,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"131\"\n    },\n    {\n      \"id\": 1279,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"221\"\n    },\n    {\n      \"id\": 1280,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"311\"\n    },\n    {\n      \"id\": 1281,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"314\"\n    },\n    {\n      \"id\": 1282,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"340\"\n    },\n    {\n      \"id\": 1283,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"427\"\n    },\n    {\n      \"id\": 1284,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"440\"\n    },\n    {\n      \"id\": 1285,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"441\"\n    },\n    {\n      \"id\": 1286,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"442\"\n    },\n    {\n      \"id\": 1287,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"445\"\n    },\n    {\n      \"id\": 1288,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"502\"\n    },\n    {\n      \"id\": 1289,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"503\"\n    },\n    {\n      \"id\": 1290,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"513\"\n    },\n    {\n      \"id\": 1291,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"515\"\n    },\n    {\n      \"id\": 1292,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"541\"\n    },\n    {\n      \"id\": 1293,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"631\"\n    },\n    {\n      \"id\": 1294,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"636\"\n    },\n    {\n      \"id\": 1295,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"700\"\n    },\n    {\n      \"id\": 1296,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"808\"\n    },\n    {\n      \"id\": 1297,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"833\"\n    },\n    {\n      \"id\": 1298,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"902\"\n    },\n    {\n      \"id\": 1299,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1000\"\n    },\n    {\n      \"id\": 1300,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1002\"\n    },\n    {\n      \"id\": 1301,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1013\"\n    },\n    {\n      \"id\": 1302,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1023\"\n    },\n    {\n      \"id\": 1303,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1024\"\n    },\n    {\n      \"id\": 1304,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1027\"\n    },\n    {\n      \"id\": 1305,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1028\"\n    },\n    {\n      \"id\": 1306,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1029\"\n    },\n    {\n      \"id\": 1307,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1081\"\n    },\n    {\n      \"id\": 1308,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1110\"\n    },\n    {\n      \"id\": 1309,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1111\"\n    },\n    {\n      \"id\": 1310,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1200\"\n    },\n    {\n      \"id\": 1311,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1224\"\n    },\n    {\n      \"id\": 1312,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1245\"\n    },\n    {\n      \"id\": 1313,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1311\"\n    },\n    {\n      \"id\": 1314,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1337\"\n    },\n    {\n      \"id\": 1315,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1343\"\n    },\n    {\n      \"id\": 1316,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1400\"\n    },\n    {\n      \"id\": 1317,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1414\"\n    },\n    {\n      \"id\": 1318,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1433\"\n    },\n    {\n      \"id\": 1319,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1443\"\n    },\n    {\n      \"id\": 1320,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1444\"\n    },\n    {\n      \"id\": 1321,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1515\"\n    },\n    {\n      \"id\": 1322,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1521\"\n    },\n    {\n      \"id\": 1323,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1604\"\n    },\n    {\n      \"id\": 1324,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1741\"\n    },\n    {\n      \"id\": 1325,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1800\"\n    },\n    {\n      \"id\": 1326,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1801\"\n    },\n    {\n      \"id\": 1327,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1833\"\n    },\n    {\n      \"id\": 1328,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1901\"\n    },\n    {\n      \"id\": 1329,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1911\"\n    },\n    {\n      \"id\": 1330,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1925\"\n    },\n    {\n      \"id\": 1331,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1926\"\n    },\n    {\n      \"id\": 1332,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2001\"\n    },\n    {\n      \"id\": 1333,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2002\"\n    },\n    {\n      \"id\": 1334,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2003\"\n    },\n    {\n      \"id\": 1335,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2006\"\n    },\n    {\n      \"id\": 1336,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2008\"\n    },\n    {\n      \"id\": 1337,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2020\"\n    },\n    {\n      \"id\": 1338,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2030\"\n    },\n    {\n      \"id\": 1339,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2101\"\n    },\n    {\n      \"id\": 1340,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2109\"\n    },\n    {\n      \"id\": 1341,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2111\"\n    },\n    {\n      \"id\": 1342,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2121\"\n    },\n    {\n      \"id\": 1343,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2122\"\n    },\n    {\n      \"id\": 1344,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2130\"\n    },\n    {\n      \"id\": 1345,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2222\"\n    },\n    {\n      \"id\": 1346,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2232\"\n    },\n    {\n      \"id\": 1347,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2233\"\n    },\n    {\n      \"id\": 1348,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2323\"\n    },\n    {\n      \"id\": 1349,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2332\"\n    },\n    {\n      \"id\": 1350,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2345\"\n    },\n    {\n      \"id\": 1351,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2404\"\n    },\n    {\n      \"id\": 1352,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2423\"\n    },\n    {\n      \"id\": 1353,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2444\"\n    },\n    {\n      \"id\": 1354,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2506\"\n    },\n    {\n      \"id\": 1355,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2525\"\n    },\n    {\n      \"id\": 1356,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2602\"\n    },\n    {\n      \"id\": 1357,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2628\"\n    },\n    {\n      \"id\": 1358,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2701\"\n    },\n    {\n      \"id\": 1359,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3001\"\n    },\n    {\n      \"id\": 1360,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3003\"\n    },\n    {\n      \"id\": 1361,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3004\"\n    },\n    {\n      \"id\": 1362,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3005\"\n    },\n    {\n      \"id\": 1363,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3007\"\n    },\n    {\n      \"id\": 1364,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3009\"\n    },\n    {\n      \"id\": 1365,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3011\"\n    },\n    {\n      \"id\": 1366,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3016\"\n    },\n    {\n      \"id\": 1367,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3018\"\n    },\n    {\n      \"id\": 1368,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3100\"\n    },\n    {\n      \"id\": 1369,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3101\"\n    },\n    {\n      \"id\": 1370,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3107\"\n    },\n    {\n      \"id\": 1371,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3108\"\n    },\n    {\n      \"id\": 1372,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3111\"\n    },\n    {\n      \"id\": 1373,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3112\"\n    },\n    {\n      \"id\": 1374,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3114\"\n    },\n    {\n      \"id\": 1375,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3116\"\n    },\n    {\n      \"id\": 1376,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3127\"\n    },\n    {\n      \"id\": 1377,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3128\"\n    },\n    {\n      \"id\": 1378,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3129\"\n    },\n    {\n      \"id\": 1379,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3133\"\n    },\n    {\n      \"id\": 1380,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3134\"\n    },\n    {\n      \"id\": 1381,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3138\"\n    },\n    {\n      \"id\": 1382,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3141\"\n    },\n    {\n      \"id\": 1383,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3211\"\n    },\n    {\n      \"id\": 1384,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3221\"\n    },\n    {\n      \"id\": 1385,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3301\"\n    },\n    {\n      \"id\": 1386,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3310\"\n    },\n    {\n      \"id\": 1387,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3311\"\n    },\n    {\n      \"id\": 1388,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3341\"\n    },\n    {\n      \"id\": 1389,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3342\"\n    },\n    {\n      \"id\": 1390,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3402\"\n    },\n    {\n      \"id\": 1391,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3403\"\n    },\n    {\n      \"id\": 1392,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3408\"\n    },\n    {\n      \"id\": 1393,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3410\"\n    },\n    {\n      \"id\": 1394,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3412\"\n    },\n    {\n      \"id\": 1395,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3443\"\n    },\n    {\n      \"id\": 1396,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3510\"\n    },\n    {\n      \"id\": 1397,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3521\"\n    },\n    {\n      \"id\": 1398,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3523\"\n    },\n    {\n      \"id\": 1399,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3531\"\n    },\n    {\n      \"id\": 1400,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3540\"\n    },\n    {\n      \"id\": 1401,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3541\"\n    },\n    {\n      \"id\": 1402,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3542\"\n    },\n    {\n      \"id\": 1403,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3622\"\n    },\n    {\n      \"id\": 1404,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3842\"\n    },\n    {\n      \"id\": 1405,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3910\"\n    },\n    {\n      \"id\": 1406,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3922\"\n    },\n    {\n      \"id\": 1407,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4010\"\n    },\n    {\n      \"id\": 1408,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4022\"\n    },\n    {\n      \"id\": 1409,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4040\"\n    },\n    {\n      \"id\": 1410,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4042\"\n    },\n    {\n      \"id\": 1411,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4103\"\n    },\n    {\n      \"id\": 1412,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4120\"\n    },\n    {\n      \"id\": 1413,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4242\"\n    },\n    {\n      \"id\": 1414,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4244\"\n    },\n    {\n      \"id\": 1415,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4321\"\n    },\n    {\n      \"id\": 1416,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4402\"\n    },\n    {\n      \"id\": 1417,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4430\"\n    },\n    {\n      \"id\": 1418,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4433\"\n    },\n    {\n      \"id\": 1419,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4434\"\n    },\n    {\n      \"id\": 1420,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4435\"\n    },\n    {\n      \"id\": 1421,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4436\"\n    },\n    {\n      \"id\": 1422,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4444\"\n    },\n    {\n      \"id\": 1423,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4505\"\n    },\n    {\n      \"id\": 1424,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4506\"\n    },\n    {\n      \"id\": 1425,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4528\"\n    },\n    {\n      \"id\": 1426,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4543\"\n    },\n    {\n      \"id\": 1427,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4545\"\n    },\n    {\n      \"id\": 1428,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4643\"\n    },\n    {\n      \"id\": 1429,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4734\"\n    },\n    {\n      \"id\": 1430,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4808\"\n    },\n    {\n      \"id\": 1431,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4821\"\n    },\n    {\n      \"id\": 1432,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4840\"\n    },\n    {\n      \"id\": 1433,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4911\"\n    },\n    {\n      \"id\": 1434,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5001\"\n    },\n    {\n      \"id\": 1435,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5002\"\n    },\n    {\n      \"id\": 1436,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5003\"\n    },\n    {\n      \"id\": 1437,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5004\"\n    },\n    {\n      \"id\": 1438,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5005\"\n    },\n    {\n      \"id\": 1439,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5006\"\n    },\n    {\n      \"id\": 1440,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5007\"\n    },\n    {\n      \"id\": 1441,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5009\"\n    },\n    {\n      \"id\": 1442,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5010\"\n    },\n    {\n      \"id\": 1443,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5011\"\n    },\n    {\n      \"id\": 1444,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5025\"\n    },\n    {\n      \"id\": 1445,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5120\"\n    },\n    {\n      \"id\": 1446,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5201\"\n    },\n    {\n      \"id\": 1447,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5209\"\n    },\n    {\n      \"id\": 1448,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5222\"\n    },\n    {\n      \"id\": 1449,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5225\"\n    },\n    {\n      \"id\": 1450,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5229\"\n    },\n    {\n      \"id\": 1451,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5230\"\n    },\n    {\n      \"id\": 1452,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5231\"\n    },\n    {\n      \"id\": 1453,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5234\"\n    },\n    {\n      \"id\": 1454,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5400\"\n    },\n    {\n      \"id\": 1455,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5431\"\n    },\n    {\n      \"id\": 1456,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5432\"\n    },\n    {\n      \"id\": 1457,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5433\"\n    },\n    {\n      \"id\": 1458,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5435\"\n    },\n    {\n      \"id\": 1459,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5444\"\n    },\n    {\n      \"id\": 1460,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5601\"\n    },\n    {\n      \"id\": 1461,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5602\"\n    },\n    {\n      \"id\": 1462,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5620\"\n    },\n    {\n      \"id\": 1463,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5800\"\n    },\n    {\n      \"id\": 1464,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5801\"\n    },\n    {\n      \"id\": 1465,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5822\"\n    },\n    {\n      \"id\": 1466,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5900\"\n    },\n    {\n      \"id\": 1467,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5901\"\n    },\n    {\n      \"id\": 1468,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5902\"\n    },\n    {\n      \"id\": 1469,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5907\"\n    },\n    {\n      \"id\": 1470,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5910\"\n    },\n    {\n      \"id\": 1471,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5914\"\n    },\n    {\n      \"id\": 1472,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5915\"\n    },\n    {\n      \"id\": 1473,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5918\"\n    },\n    {\n      \"id\": 1474,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5920\"\n    },\n    {\n      \"id\": 1475,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5938\"\n    },\n    {\n      \"id\": 1476,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"6000\"\n    },\n    {\n      \"id\": 1477,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"6001\"\n    },\n    {\n      \"id\": 1478,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"6002\"\n    },\n    {\n      \"id\": 1479,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"6005\"\n    },\n    {\n      \"id\": 1480,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"6011\"\n    },\n    {\n      \"id\": 1481,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"6021\"\n    },\n    {\n      \"id\": 1482,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"6102\"\n    },\n    {\n      \"id\": 1483,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"6134\"\n    },\n    {\n      \"id\": 1484,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"6405\"\n    },\n    {\n      \"id\": 1485,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"6433\"\n    },\n    {\n      \"id\": 1486,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"6443\"\n    },\n    {\n      \"id\": 1487,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"6503\"\n    },\n    {\n      \"id\": 1488,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"6510\"\n    },\n    {\n      \"id\": 1489,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"6513\"\n    },\n    {\n      \"id\": 1490,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"6543\"\n    },\n    {\n      \"id\": 1491,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"6600\"\n    },\n    {\n      \"id\": 1492,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"6602\"\n    },\n    {\n      \"id\": 1493,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"6633\"\n    },\n    {\n      \"id\": 1494,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"7000\"\n    },\n    {\n      \"id\": 1495,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"7001\"\n    },\n    {\n      \"id\": 1496,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"7006\"\n    },\n    {\n      \"id\": 1497,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"7012\"\n    },\n    {\n      \"id\": 1498,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"7018\"\n    },\n    {\n      \"id\": 1499,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"7020\"\n    },\n    {\n      \"id\": 1500,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"7218\"\n    },\n    {\n      \"id\": 1501,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"7325\"\n    },\n    {\n      \"id\": 1502,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"7401\"\n    },\n    {\n      \"id\": 1503,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"7403\"\n    },\n    {\n      \"id\": 1504,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"7415\"\n    },\n    {\n      \"id\": 1505,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"7433\"\n    },\n    {\n      \"id\": 1506,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"7434\"\n    },\n    {\n      \"id\": 1507,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"7441\"\n    },\n    {\n      \"id\": 1508,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"7443\"\n    },\n    {\n      \"id\": 1509,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"7500\"\n    },\n    {\n      \"id\": 1510,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"7510\"\n    },\n    {\n      \"id\": 1511,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"7601\"\n    },\n    {\n      \"id\": 1512,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"7634\"\n    },\n    {\n      \"id\": 1513,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8000\"\n    },\n    {\n      \"id\": 1514,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8001\"\n    },\n    {\n      \"id\": 1515,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8004\"\n    },\n    {\n      \"id\": 1516,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8006\"\n    },\n    {\n      \"id\": 1517,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8008\"\n    },\n    {\n      \"id\": 1518,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8009\"\n    },\n    {\n      \"id\": 1519,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8010\"\n    },\n    {\n      \"id\": 1520,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8011\"\n    },\n    {\n      \"id\": 1521,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8012\"\n    },\n    {\n      \"id\": 1522,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8013\"\n    },\n    {\n      \"id\": 1523,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8014\"\n    },\n    {\n      \"id\": 1524,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8021\"\n    },\n    {\n      \"id\": 1525,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8022\"\n    },\n    {\n      \"id\": 1526,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8023\"\n    },\n    {\n      \"id\": 1527,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8026\"\n    },\n    {\n      \"id\": 1528,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8027\"\n    },\n    {\n      \"id\": 1529,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8030\"\n    },\n    {\n      \"id\": 1530,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8035\"\n    },\n    {\n      \"id\": 1531,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8038\"\n    },\n    {\n      \"id\": 1532,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8043\"\n    },\n    {\n      \"id\": 1533,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8103\"\n    },\n    {\n      \"id\": 1534,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8111\"\n    },\n    {\n      \"id\": 1535,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8112\"\n    },\n    {\n      \"id\": 1536,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8115\"\n    },\n    {\n      \"id\": 1537,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8123\"\n    },\n    {\n      \"id\": 1538,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8125\"\n    },\n    {\n      \"id\": 1539,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8126\"\n    },\n    {\n      \"id\": 1540,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8129\"\n    },\n    {\n      \"id\": 1541,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8130\"\n    },\n    {\n      \"id\": 1542,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8135\"\n    },\n    {\n      \"id\": 1543,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8136\"\n    },\n    {\n      \"id\": 1544,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8137\"\n    },\n    {\n      \"id\": 1545,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8138\"\n    },\n    {\n      \"id\": 1546,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8139\"\n    },\n    {\n      \"id\": 1547,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8140\"\n    },\n    {\n      \"id\": 1548,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8141\"\n    },\n    {\n      \"id\": 1549,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8200\"\n    },\n    {\n      \"id\": 1550,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8222\"\n    },\n    {\n      \"id\": 1551,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8237\"\n    },\n    {\n      \"id\": 1552,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8239\"\n    },\n    {\n      \"id\": 1553,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8315\"\n    },\n    {\n      \"id\": 1554,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8316\"\n    },\n    {\n      \"id\": 1555,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8317\"\n    },\n    {\n      \"id\": 1556,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8322\"\n    },\n    {\n      \"id\": 1557,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8333\"\n    },\n    {\n      \"id\": 1558,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8334\"\n    },\n    {\n      \"id\": 1559,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8401\"\n    },\n    {\n      \"id\": 1560,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8402\"\n    },\n    {\n      \"id\": 1561,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8404\"\n    },\n    {\n      \"id\": 1562,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8408\"\n    },\n    {\n      \"id\": 1563,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8411\"\n    },\n    {\n      \"id\": 1564,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8413\"\n    },\n    {\n      \"id\": 1565,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8415\"\n    },\n    {\n      \"id\": 1566,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8416\"\n    },\n    {\n      \"id\": 1567,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8417\"\n    },\n    {\n      \"id\": 1568,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8419\"\n    },\n    {\n      \"id\": 1569,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8420\"\n    },\n    {\n      \"id\": 1570,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8422\"\n    },\n    {\n      \"id\": 1571,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8423\"\n    },\n    {\n      \"id\": 1572,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8424\"\n    },\n    {\n      \"id\": 1573,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8430\"\n    },\n    {\n      \"id\": 1574,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8431\"\n    },\n    {\n      \"id\": 1575,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8434\"\n    },\n    {\n      \"id\": 1576,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8435\"\n    },\n    {\n      \"id\": 1577,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8441\"\n    },\n    {\n      \"id\": 1578,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8442\"\n    },\n    {\n      \"id\": 1579,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8444\"\n    },\n    {\n      \"id\": 1580,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8445\"\n    },\n    {\n      \"id\": 1581,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8505\"\n    },\n    {\n      \"id\": 1582,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8510\"\n    },\n    {\n      \"id\": 1583,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8514\"\n    },\n    {\n      \"id\": 1584,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8521\"\n    },\n    {\n      \"id\": 1585,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8523\"\n    },\n    {\n      \"id\": 1586,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8524\"\n    },\n    {\n      \"id\": 1587,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8525\"\n    },\n    {\n      \"id\": 1588,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8529\"\n    },\n    {\n      \"id\": 1589,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8543\"\n    },\n    {\n      \"id\": 1590,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8544\"\n    },\n    {\n      \"id\": 1591,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8545\"\n    },\n    {\n      \"id\": 1592,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8600\"\n    },\n    {\n      \"id\": 1593,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8622\"\n    },\n    {\n      \"id\": 1594,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8630\"\n    },\n    {\n      \"id\": 1595,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8641\"\n    },\n    {\n      \"id\": 1596,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8643\"\n    },\n    {\n      \"id\": 1597,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8705\"\n    },\n    {\n      \"id\": 1598,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8707\"\n    },\n    {\n      \"id\": 1599,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8733\"\n    },\n    {\n      \"id\": 1600,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8743\"\n    },\n    {\n      \"id\": 1601,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8800\"\n    },\n    {\n      \"id\": 1602,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8802\"\n    },\n    {\n      \"id\": 1603,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8803\"\n    },\n    {\n      \"id\": 1604,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8804\"\n    },\n    {\n      \"id\": 1605,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8805\"\n    },\n    {\n      \"id\": 1606,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8809\"\n    },\n    {\n      \"id\": 1607,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8811\"\n    },\n    {\n      \"id\": 1608,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8822\"\n    },\n    {\n      \"id\": 1609,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8826\"\n    },\n    {\n      \"id\": 1610,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8827\"\n    },\n    {\n      \"id\": 1611,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8829\"\n    },\n    {\n      \"id\": 1612,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8833\"\n    },\n    {\n      \"id\": 1613,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8834\"\n    },\n    {\n      \"id\": 1614,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8836\"\n    },\n    {\n      \"id\": 1615,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8837\"\n    },\n    {\n      \"id\": 1616,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8841\"\n    },\n    {\n      \"id\": 1617,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8842\"\n    },\n    {\n      \"id\": 1618,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8843\"\n    },\n    {\n      \"id\": 1619,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8844\"\n    },\n    {\n      \"id\": 1620,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8900\"\n    },\n    {\n      \"id\": 1621,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8901\"\n    },\n    {\n      \"id\": 1622,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8911\"\n    },\n    {\n      \"id\": 1623,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8915\"\n    },\n    {\n      \"id\": 1624,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8916\"\n    },\n    {\n      \"id\": 1625,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8935\"\n    },\n    {\n      \"id\": 1626,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9002\"\n    },\n    {\n      \"id\": 1627,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9004\"\n    },\n    {\n      \"id\": 1628,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9005\"\n    },\n    {\n      \"id\": 1629,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9006\"\n    },\n    {\n      \"id\": 1630,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9009\"\n    },\n    {\n      \"id\": 1631,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9012\"\n    },\n    {\n      \"id\": 1632,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9020\"\n    },\n    {\n      \"id\": 1633,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9021\"\n    },\n    {\n      \"id\": 1634,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9026\"\n    },\n    {\n      \"id\": 1635,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9030\"\n    },\n    {\n      \"id\": 1636,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9032\"\n    },\n    {\n      \"id\": 1637,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9033\"\n    },\n    {\n      \"id\": 1638,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9035\"\n    },\n    {\n      \"id\": 1639,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9037\"\n    },\n    {\n      \"id\": 1640,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9040\"\n    },\n    {\n      \"id\": 1641,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9041\"\n    },\n    {\n      \"id\": 1642,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9042\"\n    },\n    {\n      \"id\": 1643,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9044\"\n    },\n    {\n      \"id\": 1644,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9100\"\n    },\n    {\n      \"id\": 1645,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9102\"\n    },\n    {\n      \"id\": 1646,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9105\"\n    },\n    {\n      \"id\": 1647,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9106\"\n    },\n    {\n      \"id\": 1648,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9109\"\n    },\n    {\n      \"id\": 1649,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9111\"\n    },\n    {\n      \"id\": 1650,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9112\"\n    },\n    {\n      \"id\": 1651,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9118\"\n    },\n    {\n      \"id\": 1652,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9119\"\n    },\n    {\n      \"id\": 1653,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9123\"\n    },\n    {\n      \"id\": 1654,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9124\"\n    },\n    {\n      \"id\": 1655,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9126\"\n    },\n    {\n      \"id\": 1656,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9128\"\n    },\n    {\n      \"id\": 1657,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9130\"\n    },\n    {\n      \"id\": 1658,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9134\"\n    },\n    {\n      \"id\": 1659,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9140\"\n    },\n    {\n      \"id\": 1660,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9142\"\n    },\n    {\n      \"id\": 1661,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9200\"\n    },\n    {\n      \"id\": 1662,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9208\"\n    },\n    {\n      \"id\": 1663,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9211\"\n    },\n    {\n      \"id\": 1664,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9214\"\n    },\n    {\n      \"id\": 1665,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9219\"\n    },\n    {\n      \"id\": 1666,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9220\"\n    },\n    {\n      \"id\": 1667,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9221\"\n    },\n    {\n      \"id\": 1668,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9236\"\n    },\n    {\n      \"id\": 1669,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9241\"\n    },\n    {\n      \"id\": 1670,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9243\"\n    },\n    {\n      \"id\": 1671,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9300\"\n    },\n    {\n      \"id\": 1672,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9301\"\n    },\n    {\n      \"id\": 1673,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9304\"\n    },\n    {\n      \"id\": 1674,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9306\"\n    },\n    {\n      \"id\": 1675,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9313\"\n    },\n    {\n      \"id\": 1676,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9333\"\n    },\n    {\n      \"id\": 1677,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9418\"\n    },\n    {\n      \"id\": 1678,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9433\"\n    },\n    {\n      \"id\": 1679,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9501\"\n    },\n    {\n      \"id\": 1680,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9515\"\n    },\n    {\n      \"id\": 1681,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9530\"\n    },\n    {\n      \"id\": 1682,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9600\"\n    },\n    {\n      \"id\": 1683,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9606\"\n    },\n    {\n      \"id\": 1684,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9611\"\n    },\n    {\n      \"id\": 1685,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9633\"\n    },\n    {\n      \"id\": 1686,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9700\"\n    },\n    {\n      \"id\": 1687,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9711\"\n    },\n    {\n      \"id\": 1688,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9734\"\n    },\n    {\n      \"id\": 1689,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9800\"\n    },\n    {\n      \"id\": 1690,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9810\"\n    },\n    {\n      \"id\": 1691,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9901\"\n    },\n    {\n      \"id\": 1692,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9922\"\n    },\n    {\n      \"id\": 1693,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9929\"\n    },\n    {\n      \"id\": 1694,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9930\"\n    },\n    {\n      \"id\": 1695,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9939\"\n    },\n    {\n      \"id\": 1696,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9943\"\n    },\n    {\n      \"id\": 1697,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9944\"\n    },\n    {\n      \"id\": 1698,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10000\"\n    },\n    {\n      \"id\": 1699,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10001\"\n    },\n    {\n      \"id\": 1700,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10002\"\n    },\n    {\n      \"id\": 1701,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10007\"\n    },\n    {\n      \"id\": 1702,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10009\"\n    },\n    {\n      \"id\": 1703,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10010\"\n    },\n    {\n      \"id\": 1704,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10011\"\n    },\n    {\n      \"id\": 1705,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10012\"\n    },\n    {\n      \"id\": 1706,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10013\"\n    },\n    {\n      \"id\": 1707,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10015\"\n    },\n    {\n      \"id\": 1708,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10018\"\n    },\n    {\n      \"id\": 1709,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10020\"\n    },\n    {\n      \"id\": 1710,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10022\"\n    },\n    {\n      \"id\": 1711,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10023\"\n    },\n    {\n      \"id\": 1712,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10026\"\n    },\n    {\n      \"id\": 1713,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10027\"\n    },\n    {\n      \"id\": 1714,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10028\"\n    },\n    {\n      \"id\": 1715,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10030\"\n    },\n    {\n      \"id\": 1716,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10033\"\n    },\n    {\n      \"id\": 1717,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10035\"\n    },\n    {\n      \"id\": 1718,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10037\"\n    },\n    {\n      \"id\": 1719,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10042\"\n    },\n    {\n      \"id\": 1720,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10045\"\n    },\n    {\n      \"id\": 1721,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10100\"\n    },\n    {\n      \"id\": 1722,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10201\"\n    },\n    {\n      \"id\": 1723,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10209\"\n    },\n    {\n      \"id\": 1724,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10225\"\n    },\n    {\n      \"id\": 1725,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10243\"\n    },\n    {\n      \"id\": 1726,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10443\"\n    },\n    {\n      \"id\": 1727,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10445\"\n    },\n    {\n      \"id\": 1728,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10810\"\n    },\n    {\n      \"id\": 1729,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10909\"\n    },\n    {\n      \"id\": 1730,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10911\"\n    },\n    {\n      \"id\": 1731,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10933\"\n    },\n    {\n      \"id\": 1732,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10934\"\n    },\n    {\n      \"id\": 1733,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10935\"\n    },\n    {\n      \"id\": 1734,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10943\"\n    },\n    {\n      \"id\": 1735,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"11000\"\n    },\n    {\n      \"id\": 1736,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"11002\"\n    },\n    {\n      \"id\": 1737,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"11112\"\n    },\n    {\n      \"id\": 1738,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"11210\"\n    },\n    {\n      \"id\": 1739,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"11211\"\n    },\n    {\n      \"id\": 1740,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"11300\"\n    },\n    {\n      \"id\": 1741,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"11401\"\n    },\n    {\n      \"id\": 1742,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"11434\"\n    },\n    {\n      \"id\": 1743,\n      \"type\": \"domain\",\n      \"domain\": \"fs1.traveltalk\"\n    },\n    {\n      \"id\": 1744,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10050\"\n    },\n    {\n      \"id\": 1745,\n      \"type\": \"domain\",\n      \"domain\": \"1305306.cloudwaysapps.com\"\n    },\n    {\n      \"id\": 1746,\n      \"type\": \"domain\",\n      \"domain\": \"opac.eef.edu.gr\"\n    },\n    {\n      \"id\": 1747,\n      \"type\": \"domain\",\n      \"domain\": \"digilib.eugenfound.edu.gr\"\n    },\n    {\n      \"id\": 1748,\n      \"type\": \"domain\",\n      \"domain\": \"eticket.eef.edu.gr\"\n    },\n    {\n      \"id\": 1749,\n      \"type\": \"domain\",\n      \"domain\": \"www.eef.edu.gr\"\n    },\n    {\n      \"id\": 1750,\n      \"type\": \"domain\",\n      \"domain\": \"web.eef.edu.gr\"\n    },\n    {\n      \"id\": 1751,\n      \"type\": \"domain\",\n      \"domain\": \"eef.edu.gr\"\n    },\n    {\n      \"id\": 1752,\n      \"type\": \"domain\",\n      \"domain\": \"eugenfound.edu.gr\"\n    },\n    {\n      \"id\": 1753,\n      \"type\": \"domain\",\n      \"domain\": \"www.eugenfound.edu.gr\"\n    },\n    {\n      \"id\": 1754,\n      \"type\": \"domain\",\n      \"domain\": \"project.eef.edu.gr\"\n    },\n    {\n      \"id\": 1755,\n      \"type\": \"domain\",\n      \"domain\": \"www.planetarium.gr\"\n    },\n    {\n      \"id\": 1756,\n      \"type\": \"domain\",\n      \"domain\": \"opac.eugenfound.edu.gr\"\n    },\n    {\n      \"id\": 1757,\n      \"type\": \"domain\",\n      \"domain\": \"admin.primestay.ae\"\n    },\n    {\n      \"id\": 1758,\n      \"type\": \"domain\",\n      \"domain\": \"kunstler.tor-exit.calyxinstitute.org\"\n    },\n    {\n      \"id\": 1759,\n      \"type\": \"domain\",\n      \"domain\": \"badip.fvds.ru\"\n    },\n    {\n      \"id\": 1760,\n      \"type\": \"domain\",\n      \"domain\": \"exitrelay71.medvideos-tor.org\"\n    },\n    {\n      \"id\": 1761,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"7547\"\n    },\n    {\n      \"id\": 1762,\n      \"type\": \"domain\",\n      \"domain\": \"static-host-194-110-84-93.awasr.om\"\n    },\n    {\n      \"id\": 1763,\n      \"type\": \"domain\",\n      \"domain\": \"backup.ghesi.net\"\n    },\n    {\n      \"id\": 1764,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"40422\"\n    },\n    {\n      \"id\": 1765,\n      \"type\": \"domain\",\n      \"domain\": \"ca262.calcit.dedicated.server-hosting.expert\"\n    },\n    {\n      \"id\": 1766,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9143\"\n    },\n    {\n      \"id\": 1767,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"50100\"\n    },\n    {\n      \"id\": 1768,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"50101\"\n    },\n    {\n      \"id\": 1769,\n      \"type\": \"domain\",\n      \"domain\": \"ns1.ing.unlpam.edu.ar\"\n    },\n    {\n      \"id\": 1770,\n      \"type\": \"domain\",\n      \"domain\": \"www.ing.unlpam.edu.ar\"\n    },\n    {\n      \"id\": 1771,\n      \"type\": \"domain\",\n      \"domain\": \"dl.erfonkmli.online\"\n    },\n    {\n      \"id\": 1772,\n      \"type\": \"domain\",\n      \"domain\": \"ip171.ip-51-195-166.eu\"\n    },\n    {\n      \"id\": 1773,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4085\"\n    },\n    {\n      \"id\": 1774,\n      \"type\": \"domain\",\n      \"domain\": \"slice-205185.buyvm.net\"\n    },\n    {\n      \"id\": 1775,\n      \"type\": \"domain\",\n      \"domain\": \"this-is-a-tor-exit-node-hviv126.hviv.nl\"\n    },\n    {\n      \"id\": 1776,\n      \"type\": \"domain\",\n      \"domain\": \"tor-project-exit4.dotsrc.org\"\n    },\n    {\n      \"id\": 1777,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5357\"\n    },\n    {\n      \"id\": 1778,\n      \"type\": \"domain\",\n      \"domain\": \"api.fbj-bw.de\"\n    },\n    {\n      \"id\": 1779,\n      \"type\": \"domain\",\n      \"domain\": \"tor-exit-129.relayon.org\"\n    },\n    {\n      \"id\": 1780,\n      \"type\": \"domain\",\n      \"domain\": \"this-is-a-tor-exit-node-hviv123.hviv.nl\"\n    },\n    {\n      \"id\": 1781,\n      \"type\": \"domain\",\n      \"domain\": \"221.156.241.188.baremetal.zare.com\"\n    },\n    {\n      \"id\": 1782,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"843\"\n    },\n    {\n      \"id\": 1783,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1500\"\n    },\n    {\n      \"id\": 1784,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"1830\"\n    },\n    {\n      \"id\": 1785,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3021\"\n    },\n    {\n      \"id\": 1786,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4502\"\n    },\n    {\n      \"id\": 1787,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4524\"\n    },\n    {\n      \"id\": 1788,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"4531\"\n    },\n    {\n      \"id\": 1789,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5244\"\n    },\n    {\n      \"id\": 1790,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"5905\"\n    },\n    {\n      \"id\": 1791,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"6006\"\n    },\n    {\n      \"id\": 1792,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"6514\"\n    },\n    {\n      \"id\": 1793,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"6601\"\n    },\n    {\n      \"id\": 1794,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"7002\"\n    },\n    {\n      \"id\": 1795,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8019\"\n    },\n    {\n      \"id\": 1796,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8041\"\n    },\n    {\n      \"id\": 1797,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8502\"\n    },\n    {\n      \"id\": 1798,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8607\"\n    },\n    {\n      \"id\": 1799,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8832\"\n    },\n    {\n      \"id\": 1800,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8907\"\n    },\n    {\n      \"id\": 1801,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9014\"\n    },\n    {\n      \"id\": 1802,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9017\"\n    },\n    {\n      \"id\": 1803,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9034\"\n    },\n    {\n      \"id\": 1804,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9103\"\n    },\n    {\n      \"id\": 1805,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9125\"\n    },\n    {\n      \"id\": 1806,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10021\"\n    },\n    {\n      \"id\": 1807,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"10044\"\n    },\n    {\n      \"id\": 1808,\n      \"type\": \"domain\",\n      \"domain\": \"kevwells.com\"\n    },\n    {\n      \"id\": 1809,\n      \"type\": \"domain\",\n      \"domain\": \"lyptix.com\"\n    },\n    {\n      \"id\": 1810,\n      \"type\": \"domain\",\n      \"domain\": \"www.fulllab.com.br\"\n    },\n    {\n      \"id\": 1811,\n      \"type\": \"domain\",\n      \"domain\": \"fulllab.com.br\"\n    },\n    {\n      \"id\": 1812,\n      \"type\": \"domain\",\n      \"domain\": \"life.keks-code.com\"\n    },\n    {\n      \"id\": 1813,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"222\"\n    },\n    {\n      \"id\": 1814,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"500\"\n    },\n    {\n      \"id\": 1815,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"7011\"\n    },\n    {\n      \"id\": 1816,\n      \"type\": \"domain\",\n      \"domain\": \"no-mans-land.m247.com\"\n    },\n    {\n      \"id\": 1817,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"888\"\n    },\n    {\n      \"id\": 1818,\n      \"type\": \"domain\",\n      \"domain\": \"7k77.net\"\n    },\n    {\n      \"id\": 1819,\n      \"type\": \"domain\",\n      \"domain\": \"pool.ningpool.com\"\n    },\n    {\n      \"id\": 1820,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3478\"\n    },\n    {\n      \"id\": 1821,\n      \"type\": \"domain\",\n      \"domain\": \"node1-mail.nforto.com\"\n    },\n    {\n      \"id\": 1822,\n      \"type\": \"domain\",\n      \"domain\": \"tor.plutex.de\"\n    },\n    {\n      \"id\": 1823,\n      \"type\": \"domain\",\n      \"domain\": \"172-105-49-109.ip.linodeusercontent.com\"\n    },\n    {\n      \"id\": 1824,\n      \"type\": \"domain\",\n      \"domain\": \"odoo.gunmounts.com\"\n    },\n    {\n      \"id\": 1825,\n      \"type\": \"domain\",\n      \"domain\": \"725438.cloudwaysapps.com\"\n    },\n    {\n      \"id\": 1826,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"11155\"\n    },\n    {\n      \"id\": 1827,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"22907\"\n    },\n    {\n      \"id\": 1828,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"41825\"\n    },\n    {\n      \"id\": 1829,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"6379\"\n    },\n    {\n      \"id\": 1830,\n      \"type\": \"domain\",\n      \"domain\": \"api.hotydogysite.uz\"\n    },\n    {\n      \"id\": 1831,\n      \"type\": \"domain\",\n      \"domain\": \"devapi.hotydogysite.uz\"\n    },\n    {\n      \"id\": 1832,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"343\"\n    },\n    {\n      \"id\": 1833,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"2226\"\n    },\n    {\n      \"id\": 1834,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3123\"\n    },\n    {\n      \"id\": 1835,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"3144\"\n    },\n    {\n      \"id\": 1836,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"7015\"\n    },\n    {\n      \"id\": 1837,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8906\"\n    },\n    {\n      \"id\": 1838,\n      \"type\": \"domain\",\n      \"domain\": \"prod-meitnerium-uk-lon1-0.do.binaryedge.ninja\"\n    },\n    {\n      \"id\": 1839,\n      \"type\": \"domain\",\n      \"domain\": \"vm680945.stark-industries.solutions\"\n    },\n    {\n      \"id\": 1840,\n      \"type\": \"domain\",\n      \"domain\": \"chat.controlecontabil.net\"\n    },\n    {\n      \"id\": 1841,\n      \"type\": \"domain\",\n      \"domain\": \"dev.tcadp.org\"\n    },\n    {\n      \"id\": 1842,\n      \"type\": \"domain\",\n      \"domain\": \"66-228-32-204.ip.linodeusercontent.com\"\n    },\n    {\n      \"id\": 1843,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8099\"\n    },\n    {\n      \"id\": 1844,\n      \"type\": \"domain\",\n      \"domain\": \"172-105-69-55.ip.linodeusercontent.com\"\n    },\n    {\n      \"id\": 1845,\n      \"type\": \"domain\",\n      \"domain\": \"initiationsbyartists.org\"\n    },\n    {\n      \"id\": 1846,\n      \"type\": \"domain\",\n      \"domain\": \"api.initiationsbyartists.org\"\n    },\n    {\n      \"id\": 1847,\n      \"type\": \"domain\",\n      \"domain\": \"172-105-64-5.ip.linodeusercontent.com\"\n    },\n    {\n      \"id\": 1848,\n      \"type\": \"domain\",\n      \"domain\": \"www.initiationsbyartists.org\"\n    },\n    {\n      \"id\": 1849,\n      \"type\": \"domain\",\n      \"domain\": \"beta.initiationsbyartists.org\"\n    },\n    {\n      \"id\": 1850,\n      \"type\": \"domain\",\n      \"domain\": \"www.artnatar.com\"\n    },\n    {\n      \"id\": 1851,\n      \"type\": \"domain\",\n      \"domain\": \"artnatar.com\"\n    },\n    {\n      \"id\": 1852,\n      \"type\": \"domain\",\n      \"domain\": \"svn.tapg.dev\"\n    },\n    {\n      \"id\": 1853,\n      \"type\": \"domain\",\n      \"domain\": \"li1668-7.members.linode.com\"\n    },\n    {\n      \"id\": 1854,\n      \"type\": \"domain\",\n      \"domain\": \"www.mzurad.com\"\n    },\n    {\n      \"id\": 1855,\n      \"type\": \"domain\",\n      \"domain\": \"mzurad.com\"\n    },\n    {\n      \"id\": 1856,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9001\"\n    },\n    {\n      \"id\": 1857,\n      \"type\": \"domain\",\n      \"domain\": \"tor.terminator.net\"\n    },\n    {\n      \"id\": 1858,\n      \"type\": \"domain\",\n      \"domain\": \"www.hvarcharter.com\"\n    },\n    {\n      \"id\": 1859,\n      \"type\": \"domain\",\n      \"domain\": \"108.61.210.108.vultrusercontent.com\"\n    },\n    {\n      \"id\": 1860,\n      \"type\": \"domain\",\n      \"domain\": \"hvarcharter.com\"\n    },\n    {\n      \"id\": 1861,\n      \"type\": \"domain\",\n      \"domain\": \"787791.cloudwaysapps.com\"\n    },\n    {\n      \"id\": 1862,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"8899\"\n    },\n    {\n      \"id\": 1863,\n      \"type\": \"domain\",\n      \"domain\": \"ns1001926.ip-147-135-105.us\"\n    },\n    {\n      \"id\": 1864,\n      \"type\": \"domain\",\n      \"domain\": \"dnspod.com\"\n    },\n    {\n      \"id\": 1865,\n      \"type\": \"domain\",\n      \"domain\": \"1148742.cloudwaysapps.com\"\n    },\n    {\n      \"id\": 1866,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"9993\"\n    },\n    {\n      \"id\": 1867,\n      \"type\": \"domain\",\n      \"domain\": \"static.120.95.63.178.clients.your-server.de\"\n    },\n    {\n      \"id\": 1868,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"30002\"\n    },\n    {\n      \"id\": 1869,\n      \"type\": \"domain\",\n      \"domain\": \"796420.cloudwaysapps.com\"\n    }\n  ],\n  \"edges\": [\n    {\n      \"id\": \"1-1153-Exposes\",\n      \"from\": 1,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"1-1154-Exposes\",\n      \"from\": 1,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"1-1155-Exposes\",\n      \"from\": 1,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"1-1156-Exposes\",\n      \"from\": 1,\n      \"to\": 1156,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"2-1153-Exposes\",\n      \"from\": 2,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"2-1154-Exposes\",\n      \"from\": 2,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"4-1153-Exposes\",\n      \"from\": 4,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"4-1154-Exposes\",\n      \"from\": 4,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"4-1155-Exposes\",\n      \"from\": 4,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"4-1157-Exposes\",\n      \"from\": 4,\n      \"to\": 1157,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"4-1158-Exposes\",\n      \"from\": 4,\n      \"to\": 1158,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"4-1159-Exposes\",\n      \"from\": 4,\n      \"to\": 1159,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"4-1160-Exposes\",\n      \"from\": 4,\n      \"to\": 1160,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"4-1161-Exposes\",\n      \"from\": 4,\n      \"to\": 1161,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"7-1155-Exposes\",\n      \"from\": 7,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"7-1162-Exposes\",\n      \"from\": 7,\n      \"to\": 1162,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"7-1163-ResolvesTo\",\n      \"from\": 7,\n      \"to\": 1163,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"7-1164-ResolvesTo\",\n      \"from\": 7,\n      \"to\": 1164,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"7-1165-ResolvesTo\",\n      \"from\": 7,\n      \"to\": 1165,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"7-1166-ResolvesTo\",\n      \"from\": 7,\n      \"to\": 1166,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"7-1167-ResolvesTo\",\n      \"from\": 7,\n      \"to\": 1167,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"7-1168-ResolvesTo\",\n      \"from\": 7,\n      \"to\": 1168,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"7-1169-ResolvesTo\",\n      \"from\": 7,\n      \"to\": 1169,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"7-1170-ResolvesTo\",\n      \"from\": 7,\n      \"to\": 1170,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"7-1171-ResolvesTo\",\n      \"from\": 7,\n      \"to\": 1171,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"7-1172-ResolvesTo\",\n      \"from\": 7,\n      \"to\": 1172,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"7-1173-ResolvesTo\",\n      \"from\": 7,\n      \"to\": 1173,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"15-1174-Exposes\",\n      \"from\": 15,\n      \"to\": 1174,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"15-1175-ResolvesTo\",\n      \"from\": 15,\n      \"to\": 1175,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"16-1154-Exposes\",\n      \"from\": 16,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"16-1155-Exposes\",\n      \"from\": 16,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"18-1153-Exposes\",\n      \"from\": 18,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"18-1176-Exposes\",\n      \"from\": 18,\n      \"to\": 1176,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"18-1155-Exposes\",\n      \"from\": 18,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"18-1177-Exposes\",\n      \"from\": 18,\n      \"to\": 1177,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"18-1178-ResolvesTo\",\n      \"from\": 18,\n      \"to\": 1178,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"19-1153-Exposes\",\n      \"from\": 19,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"19-1154-Exposes\",\n      \"from\": 19,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"19-1155-Exposes\",\n      \"from\": 19,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"19-1179-ResolvesTo\",\n      \"from\": 19,\n      \"to\": 1179,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"20-1180-Exposes\",\n      \"from\": 20,\n      \"to\": 1180,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"21-1153-Exposes\",\n      \"from\": 21,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"21-1154-Exposes\",\n      \"from\": 21,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"21-1155-Exposes\",\n      \"from\": 21,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"21-1181-ResolvesTo\",\n      \"from\": 21,\n      \"to\": 1181,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"21-1182-ResolvesTo\",\n      \"from\": 21,\n      \"to\": 1182,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"22-1183-Exposes\",\n      \"from\": 22,\n      \"to\": 1183,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"22-1153-Exposes\",\n      \"from\": 22,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"22-1154-Exposes\",\n      \"from\": 22,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"22-1155-Exposes\",\n      \"from\": 22,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"22-1184-ResolvesTo\",\n      \"from\": 22,\n      \"to\": 1184,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"22-1185-ResolvesTo\",\n      \"from\": 22,\n      \"to\": 1185,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"23-1153-Exposes\",\n      \"from\": 23,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"24-1155-Exposes\",\n      \"from\": 24,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"24-1162-Exposes\",\n      \"from\": 24,\n      \"to\": 1162,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"24-1166-ResolvesTo\",\n      \"from\": 24,\n      \"to\": 1166,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"25-1154-Exposes\",\n      \"from\": 25,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"25-1155-Exposes\",\n      \"from\": 25,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"25-1186-ResolvesTo\",\n      \"from\": 25,\n      \"to\": 1186,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"25-1187-ResolvesTo\",\n      \"from\": 25,\n      \"to\": 1187,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"28-1154-Exposes\",\n      \"from\": 28,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"28-1188-Exposes\",\n      \"from\": 28,\n      \"to\": 1188,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"28-1189-Exposes\",\n      \"from\": 28,\n      \"to\": 1189,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"28-1155-Exposes\",\n      \"from\": 28,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"28-1190-Exposes\",\n      \"from\": 28,\n      \"to\": 1190,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"28-1156-Exposes\",\n      \"from\": 28,\n      \"to\": 1156,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"28-1180-Exposes\",\n      \"from\": 28,\n      \"to\": 1180,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"28-1191-Exposes\",\n      \"from\": 28,\n      \"to\": 1191,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"28-1192-Exposes\",\n      \"from\": 28,\n      \"to\": 1192,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"28-1193-Exposes\",\n      \"from\": 28,\n      \"to\": 1193,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"28-1194-ResolvesTo\",\n      \"from\": 28,\n      \"to\": 1194,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"29-1153-Exposes\",\n      \"from\": 29,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"29-1154-Exposes\",\n      \"from\": 29,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"29-1155-Exposes\",\n      \"from\": 29,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"29-1195-ResolvesTo\",\n      \"from\": 29,\n      \"to\": 1195,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"29-1196-ResolvesTo\",\n      \"from\": 29,\n      \"to\": 1196,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"32-1153-Exposes\",\n      \"from\": 32,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"32-1154-Exposes\",\n      \"from\": 32,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"32-1155-Exposes\",\n      \"from\": 32,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"32-1159-Exposes\",\n      \"from\": 32,\n      \"to\": 1159,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"32-1197-Exposes\",\n      \"from\": 32,\n      \"to\": 1197,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"32-1198-ResolvesTo\",\n      \"from\": 32,\n      \"to\": 1198,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"32-1199-ResolvesTo\",\n      \"from\": 32,\n      \"to\": 1199,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"37-1200-Exposes\",\n      \"from\": 37,\n      \"to\": 1200,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"37-1201-ResolvesTo\",\n      \"from\": 37,\n      \"to\": 1201,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"38-1153-Exposes\",\n      \"from\": 38,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"38-1154-Exposes\",\n      \"from\": 38,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"38-1155-Exposes\",\n      \"from\": 38,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"38-1202-Exposes\",\n      \"from\": 38,\n      \"to\": 1202,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"38-1203-ResolvesTo\",\n      \"from\": 38,\n      \"to\": 1203,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"38-1204-ResolvesTo\",\n      \"from\": 38,\n      \"to\": 1204,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"44-1153-Exposes\",\n      \"from\": 44,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"44-1154-Exposes\",\n      \"from\": 44,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"44-1155-Exposes\",\n      \"from\": 44,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"44-1205-Exposes\",\n      \"from\": 44,\n      \"to\": 1205,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"44-1206-ResolvesTo\",\n      \"from\": 44,\n      \"to\": 1206,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"46-1153-Exposes\",\n      \"from\": 46,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"46-1154-Exposes\",\n      \"from\": 46,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"46-1155-Exposes\",\n      \"from\": 46,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"46-1207-ResolvesTo\",\n      \"from\": 46,\n      \"to\": 1207,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"46-1208-ResolvesTo\",\n      \"from\": 46,\n      \"to\": 1208,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"47-1154-Exposes\",\n      \"from\": 47,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"47-1155-Exposes\",\n      \"from\": 47,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"47-1209-ResolvesTo\",\n      \"from\": 47,\n      \"to\": 1209,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"48-1210-Exposes\",\n      \"from\": 48,\n      \"to\": 1210,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"50-1154-Exposes\",\n      \"from\": 50,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"50-1155-Exposes\",\n      \"from\": 50,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"50-1211-ResolvesTo\",\n      \"from\": 50,\n      \"to\": 1211,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"50-1212-ResolvesTo\",\n      \"from\": 50,\n      \"to\": 1212,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"55-1213-Exposes\",\n      \"from\": 55,\n      \"to\": 1213,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"58-1153-Exposes\",\n      \"from\": 58,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"58-1154-Exposes\",\n      \"from\": 58,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"58-1155-Exposes\",\n      \"from\": 58,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"58-1214-Exposes\",\n      \"from\": 58,\n      \"to\": 1214,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"58-1215-ResolvesTo\",\n      \"from\": 58,\n      \"to\": 1215,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"58-1216-ResolvesTo\",\n      \"from\": 58,\n      \"to\": 1216,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"59-1153-Exposes\",\n      \"from\": 59,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"59-1217-Exposes\",\n      \"from\": 59,\n      \"to\": 1217,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"59-1154-Exposes\",\n      \"from\": 59,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"59-1218-Exposes\",\n      \"from\": 59,\n      \"to\": 1218,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"59-1219-Exposes\",\n      \"from\": 59,\n      \"to\": 1219,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"59-1220-Exposes\",\n      \"from\": 59,\n      \"to\": 1220,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"59-1155-Exposes\",\n      \"from\": 59,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"59-1221-Exposes\",\n      \"from\": 59,\n      \"to\": 1221,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"59-1222-Exposes\",\n      \"from\": 59,\n      \"to\": 1222,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"59-1223-Exposes\",\n      \"from\": 59,\n      \"to\": 1223,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"59-1224-Exposes\",\n      \"from\": 59,\n      \"to\": 1224,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"59-1225-Exposes\",\n      \"from\": 59,\n      \"to\": 1225,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"59-1226-Exposes\",\n      \"from\": 59,\n      \"to\": 1226,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"59-1227-Exposes\",\n      \"from\": 59,\n      \"to\": 1227,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"59-1228-Exposes\",\n      \"from\": 59,\n      \"to\": 1228,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"59-1229-Exposes\",\n      \"from\": 59,\n      \"to\": 1229,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"59-1156-Exposes\",\n      \"from\": 59,\n      \"to\": 1156,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"59-1230-ResolvesTo\",\n      \"from\": 59,\n      \"to\": 1230,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"59-1231-ResolvesTo\",\n      \"from\": 59,\n      \"to\": 1231,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"59-1232-ResolvesTo\",\n      \"from\": 59,\n      \"to\": 1232,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"59-1233-ResolvesTo\",\n      \"from\": 59,\n      \"to\": 1233,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"61-1234-Exposes\",\n      \"from\": 61,\n      \"to\": 1234,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"61-1235-ResolvesTo\",\n      \"from\": 61,\n      \"to\": 1235,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"65-1154-Exposes\",\n      \"from\": 65,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"65-1155-Exposes\",\n      \"from\": 65,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"65-1236-ResolvesTo\",\n      \"from\": 65,\n      \"to\": 1236,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"65-1237-ResolvesTo\",\n      \"from\": 65,\n      \"to\": 1237,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"73-1153-Exposes\",\n      \"from\": 73,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"73-1154-Exposes\",\n      \"from\": 73,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"73-1155-Exposes\",\n      \"from\": 73,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"73-1238-Exposes\",\n      \"from\": 73,\n      \"to\": 1238,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"73-1239-ResolvesTo\",\n      \"from\": 73,\n      \"to\": 1239,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"74-1153-Exposes\",\n      \"from\": 74,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"74-1240-ResolvesTo\",\n      \"from\": 74,\n      \"to\": 1240,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"75-1153-Exposes\",\n      \"from\": 75,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"75-1154-Exposes\",\n      \"from\": 75,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"75-1219-Exposes\",\n      \"from\": 75,\n      \"to\": 1219,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"75-1241-Exposes\",\n      \"from\": 75,\n      \"to\": 1241,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"75-1242-Exposes\",\n      \"from\": 75,\n      \"to\": 1242,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"75-1243-ResolvesTo\",\n      \"from\": 75,\n      \"to\": 1243,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"76-1153-Exposes\",\n      \"from\": 76,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"76-1154-Exposes\",\n      \"from\": 76,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"76-1155-Exposes\",\n      \"from\": 76,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"76-1244-ResolvesTo\",\n      \"from\": 76,\n      \"to\": 1244,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"77-1245-Exposes\",\n      \"from\": 77,\n      \"to\": 1245,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"77-1246-ResolvesTo\",\n      \"from\": 77,\n      \"to\": 1246,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"78-1153-Exposes\",\n      \"from\": 78,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"78-1155-Exposes\",\n      \"from\": 78,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"78-1247-Exposes\",\n      \"from\": 78,\n      \"to\": 1247,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"79-1153-Exposes\",\n      \"from\": 79,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"79-1154-Exposes\",\n      \"from\": 79,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"79-1155-Exposes\",\n      \"from\": 79,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"81-1153-Exposes\",\n      \"from\": 81,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"81-1155-Exposes\",\n      \"from\": 81,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"81-1248-ResolvesTo\",\n      \"from\": 81,\n      \"to\": 1248,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"81-1249-ResolvesTo\",\n      \"from\": 81,\n      \"to\": 1249,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"81-1250-ResolvesTo\",\n      \"from\": 81,\n      \"to\": 1250,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"83-1153-Exposes\",\n      \"from\": 83,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"83-1154-Exposes\",\n      \"from\": 83,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"83-1155-Exposes\",\n      \"from\": 83,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"83-1251-Exposes\",\n      \"from\": 83,\n      \"to\": 1251,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"83-1252-ResolvesTo\",\n      \"from\": 83,\n      \"to\": 1252,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"83-1253-ResolvesTo\",\n      \"from\": 83,\n      \"to\": 1253,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"84-1154-Exposes\",\n      \"from\": 84,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"84-1254-Exposes\",\n      \"from\": 84,\n      \"to\": 1254,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"84-1155-Exposes\",\n      \"from\": 84,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"84-1255-Exposes\",\n      \"from\": 84,\n      \"to\": 1255,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"84-1256-ResolvesTo\",\n      \"from\": 84,\n      \"to\": 1256,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"87-1153-Exposes\",\n      \"from\": 87,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"87-1154-Exposes\",\n      \"from\": 87,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"87-1155-Exposes\",\n      \"from\": 87,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"87-1257-Exposes\",\n      \"from\": 87,\n      \"to\": 1257,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"89-1153-Exposes\",\n      \"from\": 89,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"89-1154-Exposes\",\n      \"from\": 89,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"89-1155-Exposes\",\n      \"from\": 89,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"89-1258-ResolvesTo\",\n      \"from\": 89,\n      \"to\": 1258,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"90-1154-Exposes\",\n      \"from\": 90,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"90-1254-Exposes\",\n      \"from\": 90,\n      \"to\": 1254,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"90-1155-Exposes\",\n      \"from\": 90,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"90-1255-Exposes\",\n      \"from\": 90,\n      \"to\": 1255,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"90-1256-ResolvesTo\",\n      \"from\": 90,\n      \"to\": 1256,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"91-1153-Exposes\",\n      \"from\": 91,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"91-1180-Exposes\",\n      \"from\": 91,\n      \"to\": 1180,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"91-1259-ResolvesTo\",\n      \"from\": 91,\n      \"to\": 1259,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"92-1153-Exposes\",\n      \"from\": 92,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"92-1154-Exposes\",\n      \"from\": 92,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"92-1155-Exposes\",\n      \"from\": 92,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"92-1260-ResolvesTo\",\n      \"from\": 92,\n      \"to\": 1260,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"94-1153-Exposes\",\n      \"from\": 94,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"94-1154-Exposes\",\n      \"from\": 94,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"94-1157-Exposes\",\n      \"from\": 94,\n      \"to\": 1157,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"95-1154-Exposes\",\n      \"from\": 95,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"95-1254-Exposes\",\n      \"from\": 95,\n      \"to\": 1254,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"95-1261-Exposes\",\n      \"from\": 95,\n      \"to\": 1261,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"95-1176-Exposes\",\n      \"from\": 95,\n      \"to\": 1176,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"95-1262-Exposes\",\n      \"from\": 95,\n      \"to\": 1262,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"95-1155-Exposes\",\n      \"from\": 95,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"95-1263-Exposes\",\n      \"from\": 95,\n      \"to\": 1263,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"95-1264-Exposes\",\n      \"from\": 95,\n      \"to\": 1264,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"95-1265-ResolvesTo\",\n      \"from\": 95,\n      \"to\": 1265,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"96-1153-Exposes\",\n      \"from\": 96,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1266-Exposes\",\n      \"from\": 96,\n      \"to\": 1266,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1267-Exposes\",\n      \"from\": 96,\n      \"to\": 1267,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1268-Exposes\",\n      \"from\": 96,\n      \"to\": 1268,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1269-Exposes\",\n      \"from\": 96,\n      \"to\": 1269,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1270-Exposes\",\n      \"from\": 96,\n      \"to\": 1270,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1154-Exposes\",\n      \"from\": 96,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1271-Exposes\",\n      \"from\": 96,\n      \"to\": 1271,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1272-Exposes\",\n      \"from\": 96,\n      \"to\": 1272,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1273-Exposes\",\n      \"from\": 96,\n      \"to\": 1273,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1274-Exposes\",\n      \"from\": 96,\n      \"to\": 1274,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1275-Exposes\",\n      \"from\": 96,\n      \"to\": 1275,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1218-Exposes\",\n      \"from\": 96,\n      \"to\": 1218,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1219-Exposes\",\n      \"from\": 96,\n      \"to\": 1219,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1276-Exposes\",\n      \"from\": 96,\n      \"to\": 1276,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1277-Exposes\",\n      \"from\": 96,\n      \"to\": 1277,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1278-Exposes\",\n      \"from\": 96,\n      \"to\": 1278,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1188-Exposes\",\n      \"from\": 96,\n      \"to\": 1188,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1220-Exposes\",\n      \"from\": 96,\n      \"to\": 1220,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1279-Exposes\",\n      \"from\": 96,\n      \"to\": 1279,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1280-Exposes\",\n      \"from\": 96,\n      \"to\": 1280,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1281-Exposes\",\n      \"from\": 96,\n      \"to\": 1281,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1282-Exposes\",\n      \"from\": 96,\n      \"to\": 1282,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1283-Exposes\",\n      \"from\": 96,\n      \"to\": 1283,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1284-Exposes\",\n      \"from\": 96,\n      \"to\": 1284,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1285-Exposes\",\n      \"from\": 96,\n      \"to\": 1285,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1286-Exposes\",\n      \"from\": 96,\n      \"to\": 1286,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1155-Exposes\",\n      \"from\": 96,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1263-Exposes\",\n      \"from\": 96,\n      \"to\": 1263,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1287-Exposes\",\n      \"from\": 96,\n      \"to\": 1287,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1288-Exposes\",\n      \"from\": 96,\n      \"to\": 1288,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1289-Exposes\",\n      \"from\": 96,\n      \"to\": 1289,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1290-Exposes\",\n      \"from\": 96,\n      \"to\": 1290,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1291-Exposes\",\n      \"from\": 96,\n      \"to\": 1291,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1292-Exposes\",\n      \"from\": 96,\n      \"to\": 1292,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1293-Exposes\",\n      \"from\": 96,\n      \"to\": 1293,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1294-Exposes\",\n      \"from\": 96,\n      \"to\": 1294,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1295-Exposes\",\n      \"from\": 96,\n      \"to\": 1295,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1296-Exposes\",\n      \"from\": 96,\n      \"to\": 1296,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1297-Exposes\",\n      \"from\": 96,\n      \"to\": 1297,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1298-Exposes\",\n      \"from\": 96,\n      \"to\": 1298,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1299-Exposes\",\n      \"from\": 96,\n      \"to\": 1299,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1300-Exposes\",\n      \"from\": 96,\n      \"to\": 1300,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1301-Exposes\",\n      \"from\": 96,\n      \"to\": 1301,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1302-Exposes\",\n      \"from\": 96,\n      \"to\": 1302,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1303-Exposes\",\n      \"from\": 96,\n      \"to\": 1303,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1304-Exposes\",\n      \"from\": 96,\n      \"to\": 1304,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1305-Exposes\",\n      \"from\": 96,\n      \"to\": 1305,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1306-Exposes\",\n      \"from\": 96,\n      \"to\": 1306,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1307-Exposes\",\n      \"from\": 96,\n      \"to\": 1307,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1308-Exposes\",\n      \"from\": 96,\n      \"to\": 1308,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1309-Exposes\",\n      \"from\": 96,\n      \"to\": 1309,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1310-Exposes\",\n      \"from\": 96,\n      \"to\": 1310,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1311-Exposes\",\n      \"from\": 96,\n      \"to\": 1311,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1241-Exposes\",\n      \"from\": 96,\n      \"to\": 1241,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1312-Exposes\",\n      \"from\": 96,\n      \"to\": 1312,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1313-Exposes\",\n      \"from\": 96,\n      \"to\": 1313,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1314-Exposes\",\n      \"from\": 96,\n      \"to\": 1314,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1315-Exposes\",\n      \"from\": 96,\n      \"to\": 1315,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1316-Exposes\",\n      \"from\": 96,\n      \"to\": 1316,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1317-Exposes\",\n      \"from\": 96,\n      \"to\": 1317,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1318-Exposes\",\n      \"from\": 96,\n      \"to\": 1318,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1319-Exposes\",\n      \"from\": 96,\n      \"to\": 1319,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1320-Exposes\",\n      \"from\": 96,\n      \"to\": 1320,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1321-Exposes\",\n      \"from\": 96,\n      \"to\": 1321,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1322-Exposes\",\n      \"from\": 96,\n      \"to\": 1322,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1323-Exposes\",\n      \"from\": 96,\n      \"to\": 1323,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1190-Exposes\",\n      \"from\": 96,\n      \"to\": 1190,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1324-Exposes\",\n      \"from\": 96,\n      \"to\": 1324,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1325-Exposes\",\n      \"from\": 96,\n      \"to\": 1325,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1326-Exposes\",\n      \"from\": 96,\n      \"to\": 1326,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1327-Exposes\",\n      \"from\": 96,\n      \"to\": 1327,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1328-Exposes\",\n      \"from\": 96,\n      \"to\": 1328,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1329-Exposes\",\n      \"from\": 96,\n      \"to\": 1329,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1330-Exposes\",\n      \"from\": 96,\n      \"to\": 1330,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1331-Exposes\",\n      \"from\": 96,\n      \"to\": 1331,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1162-Exposes\",\n      \"from\": 96,\n      \"to\": 1162,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1332-Exposes\",\n      \"from\": 96,\n      \"to\": 1332,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1333-Exposes\",\n      \"from\": 96,\n      \"to\": 1333,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1334-Exposes\",\n      \"from\": 96,\n      \"to\": 1334,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1335-Exposes\",\n      \"from\": 96,\n      \"to\": 1335,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1336-Exposes\",\n      \"from\": 96,\n      \"to\": 1336,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1337-Exposes\",\n      \"from\": 96,\n      \"to\": 1337,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1338-Exposes\",\n      \"from\": 96,\n      \"to\": 1338,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1339-Exposes\",\n      \"from\": 96,\n      \"to\": 1339,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1340-Exposes\",\n      \"from\": 96,\n      \"to\": 1340,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1341-Exposes\",\n      \"from\": 96,\n      \"to\": 1341,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1342-Exposes\",\n      \"from\": 96,\n      \"to\": 1342,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1343-Exposes\",\n      \"from\": 96,\n      \"to\": 1343,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1344-Exposes\",\n      \"from\": 96,\n      \"to\": 1344,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1345-Exposes\",\n      \"from\": 96,\n      \"to\": 1345,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1346-Exposes\",\n      \"from\": 96,\n      \"to\": 1346,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1347-Exposes\",\n      \"from\": 96,\n      \"to\": 1347,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1348-Exposes\",\n      \"from\": 96,\n      \"to\": 1348,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1349-Exposes\",\n      \"from\": 96,\n      \"to\": 1349,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1350-Exposes\",\n      \"from\": 96,\n      \"to\": 1350,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1351-Exposes\",\n      \"from\": 96,\n      \"to\": 1351,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1352-Exposes\",\n      \"from\": 96,\n      \"to\": 1352,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1353-Exposes\",\n      \"from\": 96,\n      \"to\": 1353,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1354-Exposes\",\n      \"from\": 96,\n      \"to\": 1354,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1355-Exposes\",\n      \"from\": 96,\n      \"to\": 1355,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1356-Exposes\",\n      \"from\": 96,\n      \"to\": 1356,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1357-Exposes\",\n      \"from\": 96,\n      \"to\": 1357,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1358-Exposes\",\n      \"from\": 96,\n      \"to\": 1358,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1359-Exposes\",\n      \"from\": 96,\n      \"to\": 1359,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1360-Exposes\",\n      \"from\": 96,\n      \"to\": 1360,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1361-Exposes\",\n      \"from\": 96,\n      \"to\": 1361,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1362-Exposes\",\n      \"from\": 96,\n      \"to\": 1362,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1363-Exposes\",\n      \"from\": 96,\n      \"to\": 1363,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1364-Exposes\",\n      \"from\": 96,\n      \"to\": 1364,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1365-Exposes\",\n      \"from\": 96,\n      \"to\": 1365,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1366-Exposes\",\n      \"from\": 96,\n      \"to\": 1366,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1367-Exposes\",\n      \"from\": 96,\n      \"to\": 1367,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1368-Exposes\",\n      \"from\": 96,\n      \"to\": 1368,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1369-Exposes\",\n      \"from\": 96,\n      \"to\": 1369,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1370-Exposes\",\n      \"from\": 96,\n      \"to\": 1370,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1371-Exposes\",\n      \"from\": 96,\n      \"to\": 1371,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1372-Exposes\",\n      \"from\": 96,\n      \"to\": 1372,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1373-Exposes\",\n      \"from\": 96,\n      \"to\": 1373,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1374-Exposes\",\n      \"from\": 96,\n      \"to\": 1374,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1375-Exposes\",\n      \"from\": 96,\n      \"to\": 1375,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1376-Exposes\",\n      \"from\": 96,\n      \"to\": 1376,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1377-Exposes\",\n      \"from\": 96,\n      \"to\": 1377,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1378-Exposes\",\n      \"from\": 96,\n      \"to\": 1378,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1379-Exposes\",\n      \"from\": 96,\n      \"to\": 1379,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1380-Exposes\",\n      \"from\": 96,\n      \"to\": 1380,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1381-Exposes\",\n      \"from\": 96,\n      \"to\": 1381,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1382-Exposes\",\n      \"from\": 96,\n      \"to\": 1382,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1383-Exposes\",\n      \"from\": 96,\n      \"to\": 1383,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1384-Exposes\",\n      \"from\": 96,\n      \"to\": 1384,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1385-Exposes\",\n      \"from\": 96,\n      \"to\": 1385,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1386-Exposes\",\n      \"from\": 96,\n      \"to\": 1386,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1387-Exposes\",\n      \"from\": 96,\n      \"to\": 1387,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1213-Exposes\",\n      \"from\": 96,\n      \"to\": 1213,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1388-Exposes\",\n      \"from\": 96,\n      \"to\": 1388,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1389-Exposes\",\n      \"from\": 96,\n      \"to\": 1389,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1390-Exposes\",\n      \"from\": 96,\n      \"to\": 1390,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1391-Exposes\",\n      \"from\": 96,\n      \"to\": 1391,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1392-Exposes\",\n      \"from\": 96,\n      \"to\": 1392,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1393-Exposes\",\n      \"from\": 96,\n      \"to\": 1393,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1394-Exposes\",\n      \"from\": 96,\n      \"to\": 1394,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1395-Exposes\",\n      \"from\": 96,\n      \"to\": 1395,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1396-Exposes\",\n      \"from\": 96,\n      \"to\": 1396,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1397-Exposes\",\n      \"from\": 96,\n      \"to\": 1397,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1398-Exposes\",\n      \"from\": 96,\n      \"to\": 1398,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1399-Exposes\",\n      \"from\": 96,\n      \"to\": 1399,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1400-Exposes\",\n      \"from\": 96,\n      \"to\": 1400,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1401-Exposes\",\n      \"from\": 96,\n      \"to\": 1401,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1402-Exposes\",\n      \"from\": 96,\n      \"to\": 1402,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1403-Exposes\",\n      \"from\": 96,\n      \"to\": 1403,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1404-Exposes\",\n      \"from\": 96,\n      \"to\": 1404,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1405-Exposes\",\n      \"from\": 96,\n      \"to\": 1405,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1406-Exposes\",\n      \"from\": 96,\n      \"to\": 1406,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1238-Exposes\",\n      \"from\": 96,\n      \"to\": 1238,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1407-Exposes\",\n      \"from\": 96,\n      \"to\": 1407,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1408-Exposes\",\n      \"from\": 96,\n      \"to\": 1408,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1409-Exposes\",\n      \"from\": 96,\n      \"to\": 1409,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1410-Exposes\",\n      \"from\": 96,\n      \"to\": 1410,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1411-Exposes\",\n      \"from\": 96,\n      \"to\": 1411,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1412-Exposes\",\n      \"from\": 96,\n      \"to\": 1412,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1413-Exposes\",\n      \"from\": 96,\n      \"to\": 1413,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1414-Exposes\",\n      \"from\": 96,\n      \"to\": 1414,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1415-Exposes\",\n      \"from\": 96,\n      \"to\": 1415,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1416-Exposes\",\n      \"from\": 96,\n      \"to\": 1416,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1417-Exposes\",\n      \"from\": 96,\n      \"to\": 1417,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1418-Exposes\",\n      \"from\": 96,\n      \"to\": 1418,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1419-Exposes\",\n      \"from\": 96,\n      \"to\": 1419,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1420-Exposes\",\n      \"from\": 96,\n      \"to\": 1420,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1421-Exposes\",\n      \"from\": 96,\n      \"to\": 1421,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1177-Exposes\",\n      \"from\": 96,\n      \"to\": 1177,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1422-Exposes\",\n      \"from\": 96,\n      \"to\": 1422,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1423-Exposes\",\n      \"from\": 96,\n      \"to\": 1423,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1424-Exposes\",\n      \"from\": 96,\n      \"to\": 1424,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1425-Exposes\",\n      \"from\": 96,\n      \"to\": 1425,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1426-Exposes\",\n      \"from\": 96,\n      \"to\": 1426,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1427-Exposes\",\n      \"from\": 96,\n      \"to\": 1427,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1428-Exposes\",\n      \"from\": 96,\n      \"to\": 1428,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1429-Exposes\",\n      \"from\": 96,\n      \"to\": 1429,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1430-Exposes\",\n      \"from\": 96,\n      \"to\": 1430,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1431-Exposes\",\n      \"from\": 96,\n      \"to\": 1431,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1432-Exposes\",\n      \"from\": 96,\n      \"to\": 1432,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1433-Exposes\",\n      \"from\": 96,\n      \"to\": 1433,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1264-Exposes\",\n      \"from\": 96,\n      \"to\": 1264,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1434-Exposes\",\n      \"from\": 96,\n      \"to\": 1434,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1435-Exposes\",\n      \"from\": 96,\n      \"to\": 1435,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1436-Exposes\",\n      \"from\": 96,\n      \"to\": 1436,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1437-Exposes\",\n      \"from\": 96,\n      \"to\": 1437,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1438-Exposes\",\n      \"from\": 96,\n      \"to\": 1438,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1439-Exposes\",\n      \"from\": 96,\n      \"to\": 1439,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1440-Exposes\",\n      \"from\": 96,\n      \"to\": 1440,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1441-Exposes\",\n      \"from\": 96,\n      \"to\": 1441,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1442-Exposes\",\n      \"from\": 96,\n      \"to\": 1442,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1443-Exposes\",\n      \"from\": 96,\n      \"to\": 1443,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1444-Exposes\",\n      \"from\": 96,\n      \"to\": 1444,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1445-Exposes\",\n      \"from\": 96,\n      \"to\": 1445,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1446-Exposes\",\n      \"from\": 96,\n      \"to\": 1446,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1447-Exposes\",\n      \"from\": 96,\n      \"to\": 1447,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1448-Exposes\",\n      \"from\": 96,\n      \"to\": 1448,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1449-Exposes\",\n      \"from\": 96,\n      \"to\": 1449,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1450-Exposes\",\n      \"from\": 96,\n      \"to\": 1450,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1451-Exposes\",\n      \"from\": 96,\n      \"to\": 1451,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1452-Exposes\",\n      \"from\": 96,\n      \"to\": 1452,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1453-Exposes\",\n      \"from\": 96,\n      \"to\": 1453,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1454-Exposes\",\n      \"from\": 96,\n      \"to\": 1454,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1455-Exposes\",\n      \"from\": 96,\n      \"to\": 1455,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1456-Exposes\",\n      \"from\": 96,\n      \"to\": 1456,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1457-Exposes\",\n      \"from\": 96,\n      \"to\": 1457,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1458-Exposes\",\n      \"from\": 96,\n      \"to\": 1458,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1459-Exposes\",\n      \"from\": 96,\n      \"to\": 1459,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1460-Exposes\",\n      \"from\": 96,\n      \"to\": 1460,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1461-Exposes\",\n      \"from\": 96,\n      \"to\": 1461,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1462-Exposes\",\n      \"from\": 96,\n      \"to\": 1462,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1463-Exposes\",\n      \"from\": 96,\n      \"to\": 1463,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1464-Exposes\",\n      \"from\": 96,\n      \"to\": 1464,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1465-Exposes\",\n      \"from\": 96,\n      \"to\": 1465,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1466-Exposes\",\n      \"from\": 96,\n      \"to\": 1466,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1467-Exposes\",\n      \"from\": 96,\n      \"to\": 1467,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1468-Exposes\",\n      \"from\": 96,\n      \"to\": 1468,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1469-Exposes\",\n      \"from\": 96,\n      \"to\": 1469,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1470-Exposes\",\n      \"from\": 96,\n      \"to\": 1470,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1471-Exposes\",\n      \"from\": 96,\n      \"to\": 1471,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1472-Exposes\",\n      \"from\": 96,\n      \"to\": 1472,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1473-Exposes\",\n      \"from\": 96,\n      \"to\": 1473,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1474-Exposes\",\n      \"from\": 96,\n      \"to\": 1474,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1475-Exposes\",\n      \"from\": 96,\n      \"to\": 1475,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1476-Exposes\",\n      \"from\": 96,\n      \"to\": 1476,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1477-Exposes\",\n      \"from\": 96,\n      \"to\": 1477,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1478-Exposes\",\n      \"from\": 96,\n      \"to\": 1478,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1479-Exposes\",\n      \"from\": 96,\n      \"to\": 1479,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1480-Exposes\",\n      \"from\": 96,\n      \"to\": 1480,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1481-Exposes\",\n      \"from\": 96,\n      \"to\": 1481,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1482-Exposes\",\n      \"from\": 96,\n      \"to\": 1482,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1483-Exposes\",\n      \"from\": 96,\n      \"to\": 1483,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1484-Exposes\",\n      \"from\": 96,\n      \"to\": 1484,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1485-Exposes\",\n      \"from\": 96,\n      \"to\": 1485,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1486-Exposes\",\n      \"from\": 96,\n      \"to\": 1486,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1487-Exposes\",\n      \"from\": 96,\n      \"to\": 1487,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1488-Exposes\",\n      \"from\": 96,\n      \"to\": 1488,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1489-Exposes\",\n      \"from\": 96,\n      \"to\": 1489,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1490-Exposes\",\n      \"from\": 96,\n      \"to\": 1490,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1491-Exposes\",\n      \"from\": 96,\n      \"to\": 1491,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1492-Exposes\",\n      \"from\": 96,\n      \"to\": 1492,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1493-Exposes\",\n      \"from\": 96,\n      \"to\": 1493,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1494-Exposes\",\n      \"from\": 96,\n      \"to\": 1494,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1495-Exposes\",\n      \"from\": 96,\n      \"to\": 1495,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1496-Exposes\",\n      \"from\": 96,\n      \"to\": 1496,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1497-Exposes\",\n      \"from\": 96,\n      \"to\": 1497,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1498-Exposes\",\n      \"from\": 96,\n      \"to\": 1498,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1499-Exposes\",\n      \"from\": 96,\n      \"to\": 1499,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1500-Exposes\",\n      \"from\": 96,\n      \"to\": 1500,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1501-Exposes\",\n      \"from\": 96,\n      \"to\": 1501,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1502-Exposes\",\n      \"from\": 96,\n      \"to\": 1502,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1503-Exposes\",\n      \"from\": 96,\n      \"to\": 1503,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1504-Exposes\",\n      \"from\": 96,\n      \"to\": 1504,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1505-Exposes\",\n      \"from\": 96,\n      \"to\": 1505,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1506-Exposes\",\n      \"from\": 96,\n      \"to\": 1506,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1507-Exposes\",\n      \"from\": 96,\n      \"to\": 1507,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1508-Exposes\",\n      \"from\": 96,\n      \"to\": 1508,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1509-Exposes\",\n      \"from\": 96,\n      \"to\": 1509,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1510-Exposes\",\n      \"from\": 96,\n      \"to\": 1510,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1511-Exposes\",\n      \"from\": 96,\n      \"to\": 1511,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1512-Exposes\",\n      \"from\": 96,\n      \"to\": 1512,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1513-Exposes\",\n      \"from\": 96,\n      \"to\": 1513,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1514-Exposes\",\n      \"from\": 96,\n      \"to\": 1514,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1515-Exposes\",\n      \"from\": 96,\n      \"to\": 1515,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1516-Exposes\",\n      \"from\": 96,\n      \"to\": 1516,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1517-Exposes\",\n      \"from\": 96,\n      \"to\": 1517,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1518-Exposes\",\n      \"from\": 96,\n      \"to\": 1518,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1519-Exposes\",\n      \"from\": 96,\n      \"to\": 1519,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1520-Exposes\",\n      \"from\": 96,\n      \"to\": 1520,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1521-Exposes\",\n      \"from\": 96,\n      \"to\": 1521,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1522-Exposes\",\n      \"from\": 96,\n      \"to\": 1522,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1523-Exposes\",\n      \"from\": 96,\n      \"to\": 1523,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1524-Exposes\",\n      \"from\": 96,\n      \"to\": 1524,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1525-Exposes\",\n      \"from\": 96,\n      \"to\": 1525,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1526-Exposes\",\n      \"from\": 96,\n      \"to\": 1526,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1527-Exposes\",\n      \"from\": 96,\n      \"to\": 1527,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1528-Exposes\",\n      \"from\": 96,\n      \"to\": 1528,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1529-Exposes\",\n      \"from\": 96,\n      \"to\": 1529,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1530-Exposes\",\n      \"from\": 96,\n      \"to\": 1530,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1531-Exposes\",\n      \"from\": 96,\n      \"to\": 1531,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1532-Exposes\",\n      \"from\": 96,\n      \"to\": 1532,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1159-Exposes\",\n      \"from\": 96,\n      \"to\": 1159,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1533-Exposes\",\n      \"from\": 96,\n      \"to\": 1533,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1534-Exposes\",\n      \"from\": 96,\n      \"to\": 1534,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1535-Exposes\",\n      \"from\": 96,\n      \"to\": 1535,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1536-Exposes\",\n      \"from\": 96,\n      \"to\": 1536,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1537-Exposes\",\n      \"from\": 96,\n      \"to\": 1537,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1538-Exposes\",\n      \"from\": 96,\n      \"to\": 1538,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1539-Exposes\",\n      \"from\": 96,\n      \"to\": 1539,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1540-Exposes\",\n      \"from\": 96,\n      \"to\": 1540,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1541-Exposes\",\n      \"from\": 96,\n      \"to\": 1541,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1542-Exposes\",\n      \"from\": 96,\n      \"to\": 1542,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1543-Exposes\",\n      \"from\": 96,\n      \"to\": 1543,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1544-Exposes\",\n      \"from\": 96,\n      \"to\": 1544,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1545-Exposes\",\n      \"from\": 96,\n      \"to\": 1545,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1546-Exposes\",\n      \"from\": 96,\n      \"to\": 1546,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1547-Exposes\",\n      \"from\": 96,\n      \"to\": 1547,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1548-Exposes\",\n      \"from\": 96,\n      \"to\": 1548,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1549-Exposes\",\n      \"from\": 96,\n      \"to\": 1549,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1550-Exposes\",\n      \"from\": 96,\n      \"to\": 1550,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1551-Exposes\",\n      \"from\": 96,\n      \"to\": 1551,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1552-Exposes\",\n      \"from\": 96,\n      \"to\": 1552,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1553-Exposes\",\n      \"from\": 96,\n      \"to\": 1553,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1554-Exposes\",\n      \"from\": 96,\n      \"to\": 1554,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1555-Exposes\",\n      \"from\": 96,\n      \"to\": 1555,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1556-Exposes\",\n      \"from\": 96,\n      \"to\": 1556,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1557-Exposes\",\n      \"from\": 96,\n      \"to\": 1557,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1558-Exposes\",\n      \"from\": 96,\n      \"to\": 1558,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1559-Exposes\",\n      \"from\": 96,\n      \"to\": 1559,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1560-Exposes\",\n      \"from\": 96,\n      \"to\": 1560,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1561-Exposes\",\n      \"from\": 96,\n      \"to\": 1561,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1562-Exposes\",\n      \"from\": 96,\n      \"to\": 1562,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1563-Exposes\",\n      \"from\": 96,\n      \"to\": 1563,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1564-Exposes\",\n      \"from\": 96,\n      \"to\": 1564,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1565-Exposes\",\n      \"from\": 96,\n      \"to\": 1565,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1566-Exposes\",\n      \"from\": 96,\n      \"to\": 1566,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1567-Exposes\",\n      \"from\": 96,\n      \"to\": 1567,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1568-Exposes\",\n      \"from\": 96,\n      \"to\": 1568,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1569-Exposes\",\n      \"from\": 96,\n      \"to\": 1569,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1570-Exposes\",\n      \"from\": 96,\n      \"to\": 1570,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1571-Exposes\",\n      \"from\": 96,\n      \"to\": 1571,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1572-Exposes\",\n      \"from\": 96,\n      \"to\": 1572,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1573-Exposes\",\n      \"from\": 96,\n      \"to\": 1573,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1574-Exposes\",\n      \"from\": 96,\n      \"to\": 1574,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1575-Exposes\",\n      \"from\": 96,\n      \"to\": 1575,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1576-Exposes\",\n      \"from\": 96,\n      \"to\": 1576,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1577-Exposes\",\n      \"from\": 96,\n      \"to\": 1577,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1578-Exposes\",\n      \"from\": 96,\n      \"to\": 1578,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1579-Exposes\",\n      \"from\": 96,\n      \"to\": 1579,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1580-Exposes\",\n      \"from\": 96,\n      \"to\": 1580,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1581-Exposes\",\n      \"from\": 96,\n      \"to\": 1581,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1582-Exposes\",\n      \"from\": 96,\n      \"to\": 1582,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1583-Exposes\",\n      \"from\": 96,\n      \"to\": 1583,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1584-Exposes\",\n      \"from\": 96,\n      \"to\": 1584,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1585-Exposes\",\n      \"from\": 96,\n      \"to\": 1585,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1586-Exposes\",\n      \"from\": 96,\n      \"to\": 1586,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1587-Exposes\",\n      \"from\": 96,\n      \"to\": 1587,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1588-Exposes\",\n      \"from\": 96,\n      \"to\": 1588,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1589-Exposes\",\n      \"from\": 96,\n      \"to\": 1589,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1590-Exposes\",\n      \"from\": 96,\n      \"to\": 1590,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1591-Exposes\",\n      \"from\": 96,\n      \"to\": 1591,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1592-Exposes\",\n      \"from\": 96,\n      \"to\": 1592,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1593-Exposes\",\n      \"from\": 96,\n      \"to\": 1593,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1594-Exposes\",\n      \"from\": 96,\n      \"to\": 1594,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1595-Exposes\",\n      \"from\": 96,\n      \"to\": 1595,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1596-Exposes\",\n      \"from\": 96,\n      \"to\": 1596,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1597-Exposes\",\n      \"from\": 96,\n      \"to\": 1597,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1598-Exposes\",\n      \"from\": 96,\n      \"to\": 1598,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1599-Exposes\",\n      \"from\": 96,\n      \"to\": 1599,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1600-Exposes\",\n      \"from\": 96,\n      \"to\": 1600,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1601-Exposes\",\n      \"from\": 96,\n      \"to\": 1601,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1602-Exposes\",\n      \"from\": 96,\n      \"to\": 1602,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1603-Exposes\",\n      \"from\": 96,\n      \"to\": 1603,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1604-Exposes\",\n      \"from\": 96,\n      \"to\": 1604,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1605-Exposes\",\n      \"from\": 96,\n      \"to\": 1605,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1606-Exposes\",\n      \"from\": 96,\n      \"to\": 1606,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1607-Exposes\",\n      \"from\": 96,\n      \"to\": 1607,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1608-Exposes\",\n      \"from\": 96,\n      \"to\": 1608,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1609-Exposes\",\n      \"from\": 96,\n      \"to\": 1609,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1610-Exposes\",\n      \"from\": 96,\n      \"to\": 1610,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1611-Exposes\",\n      \"from\": 96,\n      \"to\": 1611,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1612-Exposes\",\n      \"from\": 96,\n      \"to\": 1612,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1613-Exposes\",\n      \"from\": 96,\n      \"to\": 1613,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1614-Exposes\",\n      \"from\": 96,\n      \"to\": 1614,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1615-Exposes\",\n      \"from\": 96,\n      \"to\": 1615,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1616-Exposes\",\n      \"from\": 96,\n      \"to\": 1616,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1617-Exposes\",\n      \"from\": 96,\n      \"to\": 1617,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1618-Exposes\",\n      \"from\": 96,\n      \"to\": 1618,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1619-Exposes\",\n      \"from\": 96,\n      \"to\": 1619,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1620-Exposes\",\n      \"from\": 96,\n      \"to\": 1620,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1621-Exposes\",\n      \"from\": 96,\n      \"to\": 1621,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1622-Exposes\",\n      \"from\": 96,\n      \"to\": 1622,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1623-Exposes\",\n      \"from\": 96,\n      \"to\": 1623,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1624-Exposes\",\n      \"from\": 96,\n      \"to\": 1624,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1625-Exposes\",\n      \"from\": 96,\n      \"to\": 1625,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1160-Exposes\",\n      \"from\": 96,\n      \"to\": 1160,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1626-Exposes\",\n      \"from\": 96,\n      \"to\": 1626,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1627-Exposes\",\n      \"from\": 96,\n      \"to\": 1627,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1628-Exposes\",\n      \"from\": 96,\n      \"to\": 1628,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1629-Exposes\",\n      \"from\": 96,\n      \"to\": 1629,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1630-Exposes\",\n      \"from\": 96,\n      \"to\": 1630,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1631-Exposes\",\n      \"from\": 96,\n      \"to\": 1631,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1632-Exposes\",\n      \"from\": 96,\n      \"to\": 1632,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1633-Exposes\",\n      \"from\": 96,\n      \"to\": 1633,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1634-Exposes\",\n      \"from\": 96,\n      \"to\": 1634,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1635-Exposes\",\n      \"from\": 96,\n      \"to\": 1635,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1636-Exposes\",\n      \"from\": 96,\n      \"to\": 1636,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1637-Exposes\",\n      \"from\": 96,\n      \"to\": 1637,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1638-Exposes\",\n      \"from\": 96,\n      \"to\": 1638,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1639-Exposes\",\n      \"from\": 96,\n      \"to\": 1639,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1640-Exposes\",\n      \"from\": 96,\n      \"to\": 1640,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1641-Exposes\",\n      \"from\": 96,\n      \"to\": 1641,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1642-Exposes\",\n      \"from\": 96,\n      \"to\": 1642,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1643-Exposes\",\n      \"from\": 96,\n      \"to\": 1643,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1644-Exposes\",\n      \"from\": 96,\n      \"to\": 1644,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1645-Exposes\",\n      \"from\": 96,\n      \"to\": 1645,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1646-Exposes\",\n      \"from\": 96,\n      \"to\": 1646,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1647-Exposes\",\n      \"from\": 96,\n      \"to\": 1647,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1648-Exposes\",\n      \"from\": 96,\n      \"to\": 1648,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1649-Exposes\",\n      \"from\": 96,\n      \"to\": 1649,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1650-Exposes\",\n      \"from\": 96,\n      \"to\": 1650,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1651-Exposes\",\n      \"from\": 96,\n      \"to\": 1651,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1652-Exposes\",\n      \"from\": 96,\n      \"to\": 1652,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1653-Exposes\",\n      \"from\": 96,\n      \"to\": 1653,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1654-Exposes\",\n      \"from\": 96,\n      \"to\": 1654,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1655-Exposes\",\n      \"from\": 96,\n      \"to\": 1655,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1656-Exposes\",\n      \"from\": 96,\n      \"to\": 1656,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1657-Exposes\",\n      \"from\": 96,\n      \"to\": 1657,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1658-Exposes\",\n      \"from\": 96,\n      \"to\": 1658,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1659-Exposes\",\n      \"from\": 96,\n      \"to\": 1659,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1660-Exposes\",\n      \"from\": 96,\n      \"to\": 1660,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1661-Exposes\",\n      \"from\": 96,\n      \"to\": 1661,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1662-Exposes\",\n      \"from\": 96,\n      \"to\": 1662,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1663-Exposes\",\n      \"from\": 96,\n      \"to\": 1663,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1664-Exposes\",\n      \"from\": 96,\n      \"to\": 1664,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1665-Exposes\",\n      \"from\": 96,\n      \"to\": 1665,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1666-Exposes\",\n      \"from\": 96,\n      \"to\": 1666,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1667-Exposes\",\n      \"from\": 96,\n      \"to\": 1667,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1668-Exposes\",\n      \"from\": 96,\n      \"to\": 1668,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1669-Exposes\",\n      \"from\": 96,\n      \"to\": 1669,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1670-Exposes\",\n      \"from\": 96,\n      \"to\": 1670,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1671-Exposes\",\n      \"from\": 96,\n      \"to\": 1671,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1672-Exposes\",\n      \"from\": 96,\n      \"to\": 1672,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1673-Exposes\",\n      \"from\": 96,\n      \"to\": 1673,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1674-Exposes\",\n      \"from\": 96,\n      \"to\": 1674,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1675-Exposes\",\n      \"from\": 96,\n      \"to\": 1675,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1676-Exposes\",\n      \"from\": 96,\n      \"to\": 1676,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1677-Exposes\",\n      \"from\": 96,\n      \"to\": 1677,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1678-Exposes\",\n      \"from\": 96,\n      \"to\": 1678,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1161-Exposes\",\n      \"from\": 96,\n      \"to\": 1161,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1679-Exposes\",\n      \"from\": 96,\n      \"to\": 1679,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1680-Exposes\",\n      \"from\": 96,\n      \"to\": 1680,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1681-Exposes\",\n      \"from\": 96,\n      \"to\": 1681,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1682-Exposes\",\n      \"from\": 96,\n      \"to\": 1682,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1683-Exposes\",\n      \"from\": 96,\n      \"to\": 1683,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1684-Exposes\",\n      \"from\": 96,\n      \"to\": 1684,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1685-Exposes\",\n      \"from\": 96,\n      \"to\": 1685,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1686-Exposes\",\n      \"from\": 96,\n      \"to\": 1686,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1687-Exposes\",\n      \"from\": 96,\n      \"to\": 1687,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1688-Exposes\",\n      \"from\": 96,\n      \"to\": 1688,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1689-Exposes\",\n      \"from\": 96,\n      \"to\": 1689,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1690-Exposes\",\n      \"from\": 96,\n      \"to\": 1690,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1691-Exposes\",\n      \"from\": 96,\n      \"to\": 1691,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1692-Exposes\",\n      \"from\": 96,\n      \"to\": 1692,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1693-Exposes\",\n      \"from\": 96,\n      \"to\": 1693,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1694-Exposes\",\n      \"from\": 96,\n      \"to\": 1694,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1695-Exposes\",\n      \"from\": 96,\n      \"to\": 1695,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1696-Exposes\",\n      \"from\": 96,\n      \"to\": 1696,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1697-Exposes\",\n      \"from\": 96,\n      \"to\": 1697,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1698-Exposes\",\n      \"from\": 96,\n      \"to\": 1698,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1699-Exposes\",\n      \"from\": 96,\n      \"to\": 1699,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1700-Exposes\",\n      \"from\": 96,\n      \"to\": 1700,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1701-Exposes\",\n      \"from\": 96,\n      \"to\": 1701,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1702-Exposes\",\n      \"from\": 96,\n      \"to\": 1702,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1703-Exposes\",\n      \"from\": 96,\n      \"to\": 1703,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1704-Exposes\",\n      \"from\": 96,\n      \"to\": 1704,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1705-Exposes\",\n      \"from\": 96,\n      \"to\": 1705,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1706-Exposes\",\n      \"from\": 96,\n      \"to\": 1706,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1707-Exposes\",\n      \"from\": 96,\n      \"to\": 1707,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1708-Exposes\",\n      \"from\": 96,\n      \"to\": 1708,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1709-Exposes\",\n      \"from\": 96,\n      \"to\": 1709,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1710-Exposes\",\n      \"from\": 96,\n      \"to\": 1710,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1711-Exposes\",\n      \"from\": 96,\n      \"to\": 1711,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1712-Exposes\",\n      \"from\": 96,\n      \"to\": 1712,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1713-Exposes\",\n      \"from\": 96,\n      \"to\": 1713,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1714-Exposes\",\n      \"from\": 96,\n      \"to\": 1714,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1715-Exposes\",\n      \"from\": 96,\n      \"to\": 1715,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1716-Exposes\",\n      \"from\": 96,\n      \"to\": 1716,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1717-Exposes\",\n      \"from\": 96,\n      \"to\": 1717,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1718-Exposes\",\n      \"from\": 96,\n      \"to\": 1718,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1719-Exposes\",\n      \"from\": 96,\n      \"to\": 1719,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1720-Exposes\",\n      \"from\": 96,\n      \"to\": 1720,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1721-Exposes\",\n      \"from\": 96,\n      \"to\": 1721,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1200-Exposes\",\n      \"from\": 96,\n      \"to\": 1200,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1722-Exposes\",\n      \"from\": 96,\n      \"to\": 1722,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1723-Exposes\",\n      \"from\": 96,\n      \"to\": 1723,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1724-Exposes\",\n      \"from\": 96,\n      \"to\": 1724,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1725-Exposes\",\n      \"from\": 96,\n      \"to\": 1725,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1726-Exposes\",\n      \"from\": 96,\n      \"to\": 1726,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1727-Exposes\",\n      \"from\": 96,\n      \"to\": 1727,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1728-Exposes\",\n      \"from\": 96,\n      \"to\": 1728,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1729-Exposes\",\n      \"from\": 96,\n      \"to\": 1729,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1730-Exposes\",\n      \"from\": 96,\n      \"to\": 1730,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1731-Exposes\",\n      \"from\": 96,\n      \"to\": 1731,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1732-Exposes\",\n      \"from\": 96,\n      \"to\": 1732,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1733-Exposes\",\n      \"from\": 96,\n      \"to\": 1733,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1734-Exposes\",\n      \"from\": 96,\n      \"to\": 1734,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1735-Exposes\",\n      \"from\": 96,\n      \"to\": 1735,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1736-Exposes\",\n      \"from\": 96,\n      \"to\": 1736,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1737-Exposes\",\n      \"from\": 96,\n      \"to\": 1737,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1738-Exposes\",\n      \"from\": 96,\n      \"to\": 1738,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1739-Exposes\",\n      \"from\": 96,\n      \"to\": 1739,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1740-Exposes\",\n      \"from\": 96,\n      \"to\": 1740,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1741-Exposes\",\n      \"from\": 96,\n      \"to\": 1741,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"96-1742-Exposes\",\n      \"from\": 96,\n      \"to\": 1742,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"97-1153-Exposes\",\n      \"from\": 97,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"97-1743-ResolvesTo\",\n      \"from\": 97,\n      \"to\": 1743,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"100-1153-Exposes\",\n      \"from\": 100,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"101-1154-Exposes\",\n      \"from\": 101,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"101-1155-Exposes\",\n      \"from\": 101,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"101-1744-Exposes\",\n      \"from\": 101,\n      \"to\": 1744,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"109-1153-Exposes\",\n      \"from\": 109,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"109-1154-Exposes\",\n      \"from\": 109,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"109-1155-Exposes\",\n      \"from\": 109,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"109-1745-ResolvesTo\",\n      \"from\": 109,\n      \"to\": 1745,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"109-1187-ResolvesTo\",\n      \"from\": 109,\n      \"to\": 1187,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"111-1217-Exposes\",\n      \"from\": 111,\n      \"to\": 1217,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"111-1155-Exposes\",\n      \"from\": 111,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"112-1153-Exposes\",\n      \"from\": 112,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"114-1154-Exposes\",\n      \"from\": 114,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"114-1155-Exposes\",\n      \"from\": 114,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"114-1746-ResolvesTo\",\n      \"from\": 114,\n      \"to\": 1746,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"114-1747-ResolvesTo\",\n      \"from\": 114,\n      \"to\": 1747,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"114-1748-ResolvesTo\",\n      \"from\": 114,\n      \"to\": 1748,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"114-1749-ResolvesTo\",\n      \"from\": 114,\n      \"to\": 1749,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"114-1750-ResolvesTo\",\n      \"from\": 114,\n      \"to\": 1750,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"114-1751-ResolvesTo\",\n      \"from\": 114,\n      \"to\": 1751,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"114-1752-ResolvesTo\",\n      \"from\": 114,\n      \"to\": 1752,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"114-1753-ResolvesTo\",\n      \"from\": 114,\n      \"to\": 1753,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"114-1754-ResolvesTo\",\n      \"from\": 114,\n      \"to\": 1754,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"114-1755-ResolvesTo\",\n      \"from\": 114,\n      \"to\": 1755,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"114-1756-ResolvesTo\",\n      \"from\": 114,\n      \"to\": 1756,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"116-1154-Exposes\",\n      \"from\": 116,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"116-1155-Exposes\",\n      \"from\": 116,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"116-1314-Exposes\",\n      \"from\": 116,\n      \"to\": 1314,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"116-1757-ResolvesTo\",\n      \"from\": 116,\n      \"to\": 1757,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"117-1153-Exposes\",\n      \"from\": 117,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"120-1154-Exposes\",\n      \"from\": 120,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"120-1155-Exposes\",\n      \"from\": 120,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"120-1758-ResolvesTo\",\n      \"from\": 120,\n      \"to\": 1758,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"122-1183-Exposes\",\n      \"from\": 122,\n      \"to\": 1183,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"122-1153-Exposes\",\n      \"from\": 122,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"122-1217-Exposes\",\n      \"from\": 122,\n      \"to\": 1217,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"122-1154-Exposes\",\n      \"from\": 122,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"122-1221-Exposes\",\n      \"from\": 122,\n      \"to\": 1221,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"122-1222-Exposes\",\n      \"from\": 122,\n      \"to\": 1222,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"122-1223-Exposes\",\n      \"from\": 122,\n      \"to\": 1223,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"122-1156-Exposes\",\n      \"from\": 122,\n      \"to\": 1156,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"122-1193-Exposes\",\n      \"from\": 122,\n      \"to\": 1193,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"122-1759-ResolvesTo\",\n      \"from\": 122,\n      \"to\": 1759,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"123-1183-Exposes\",\n      \"from\": 123,\n      \"to\": 1183,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"123-1154-Exposes\",\n      \"from\": 123,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"123-1180-Exposes\",\n      \"from\": 123,\n      \"to\": 1180,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"123-1191-Exposes\",\n      \"from\": 123,\n      \"to\": 1191,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"124-1153-Exposes\",\n      \"from\": 124,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"124-1760-ResolvesTo\",\n      \"from\": 124,\n      \"to\": 1760,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"125-1761-Exposes\",\n      \"from\": 125,\n      \"to\": 1761,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"125-1762-ResolvesTo\",\n      \"from\": 125,\n      \"to\": 1762,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"127-1153-Exposes\",\n      \"from\": 127,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"127-1763-ResolvesTo\",\n      \"from\": 127,\n      \"to\": 1763,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"130-1176-Exposes\",\n      \"from\": 130,\n      \"to\": 1176,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"130-1159-Exposes\",\n      \"from\": 130,\n      \"to\": 1159,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"130-1764-Exposes\",\n      \"from\": 130,\n      \"to\": 1764,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"130-1765-ResolvesTo\",\n      \"from\": 130,\n      \"to\": 1765,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"137-1153-Exposes\",\n      \"from\": 137,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"138-1766-Exposes\",\n      \"from\": 138,\n      \"to\": 1766,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"138-1767-Exposes\",\n      \"from\": 138,\n      \"to\": 1767,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"138-1768-Exposes\",\n      \"from\": 138,\n      \"to\": 1768,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"139-1217-Exposes\",\n      \"from\": 139,\n      \"to\": 1217,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"139-1154-Exposes\",\n      \"from\": 139,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"139-1155-Exposes\",\n      \"from\": 139,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"139-1156-Exposes\",\n      \"from\": 139,\n      \"to\": 1156,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"139-1769-ResolvesTo\",\n      \"from\": 139,\n      \"to\": 1769,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"139-1770-ResolvesTo\",\n      \"from\": 139,\n      \"to\": 1770,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"140-1153-Exposes\",\n      \"from\": 140,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"143-1154-Exposes\",\n      \"from\": 143,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"143-1155-Exposes\",\n      \"from\": 143,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"144-1153-Exposes\",\n      \"from\": 144,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"144-1257-Exposes\",\n      \"from\": 144,\n      \"to\": 1257,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"144-1771-ResolvesTo\",\n      \"from\": 144,\n      \"to\": 1771,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"145-1153-Exposes\",\n      \"from\": 145,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"145-1154-Exposes\",\n      \"from\": 145,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"145-1772-ResolvesTo\",\n      \"from\": 145,\n      \"to\": 1772,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"146-1773-Exposes\",\n      \"from\": 146,\n      \"to\": 1773,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"147-1183-Exposes\",\n      \"from\": 147,\n      \"to\": 1183,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"147-1153-Exposes\",\n      \"from\": 147,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"147-1154-Exposes\",\n      \"from\": 147,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"147-1156-Exposes\",\n      \"from\": 147,\n      \"to\": 1156,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"147-1180-Exposes\",\n      \"from\": 147,\n      \"to\": 1180,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"148-1153-Exposes\",\n      \"from\": 148,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"148-1238-Exposes\",\n      \"from\": 148,\n      \"to\": 1238,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"148-1774-ResolvesTo\",\n      \"from\": 148,\n      \"to\": 1774,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"152-1154-Exposes\",\n      \"from\": 152,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"152-1155-Exposes\",\n      \"from\": 152,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"152-1775-ResolvesTo\",\n      \"from\": 152,\n      \"to\": 1775,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"153-1154-Exposes\",\n      \"from\": 153,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"153-1262-Exposes\",\n      \"from\": 153,\n      \"to\": 1262,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"153-1155-Exposes\",\n      \"from\": 153,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"153-1776-ResolvesTo\",\n      \"from\": 153,\n      \"to\": 1776,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"154-1153-Exposes\",\n      \"from\": 154,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"155-1153-Exposes\",\n      \"from\": 155,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"155-1188-Exposes\",\n      \"from\": 155,\n      \"to\": 1188,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"155-1287-Exposes\",\n      \"from\": 155,\n      \"to\": 1287,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"155-1180-Exposes\",\n      \"from\": 155,\n      \"to\": 1180,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"155-1777-Exposes\",\n      \"from\": 155,\n      \"to\": 1777,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"162-1154-Exposes\",\n      \"from\": 162,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"162-1155-Exposes\",\n      \"from\": 162,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"162-1298-Exposes\",\n      \"from\": 162,\n      \"to\": 1298,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"162-1513-Exposes\",\n      \"from\": 162,\n      \"to\": 1513,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"162-1251-Exposes\",\n      \"from\": 162,\n      \"to\": 1251,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"163-1155-Exposes\",\n      \"from\": 163,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"163-1408-Exposes\",\n      \"from\": 163,\n      \"to\": 1408,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"163-1778-ResolvesTo\",\n      \"from\": 163,\n      \"to\": 1778,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"164-1766-Exposes\",\n      \"from\": 164,\n      \"to\": 1766,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"164-1767-Exposes\",\n      \"from\": 164,\n      \"to\": 1767,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"164-1768-Exposes\",\n      \"from\": 164,\n      \"to\": 1768,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"166-1200-Exposes\",\n      \"from\": 166,\n      \"to\": 1200,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"166-1779-ResolvesTo\",\n      \"from\": 166,\n      \"to\": 1779,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"169-1154-Exposes\",\n      \"from\": 169,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"169-1155-Exposes\",\n      \"from\": 169,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"169-1780-ResolvesTo\",\n      \"from\": 169,\n      \"to\": 1780,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"176-1153-Exposes\",\n      \"from\": 176,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"176-1154-Exposes\",\n      \"from\": 176,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"176-1155-Exposes\",\n      \"from\": 176,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"178-1153-Exposes\",\n      \"from\": 178,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"178-1781-ResolvesTo\",\n      \"from\": 178,\n      \"to\": 1781,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"180-1153-Exposes\",\n      \"from\": 180,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1154-Exposes\",\n      \"from\": 180,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1271-Exposes\",\n      \"from\": 180,\n      \"to\": 1271,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1274-Exposes\",\n      \"from\": 180,\n      \"to\": 1274,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1219-Exposes\",\n      \"from\": 180,\n      \"to\": 1219,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1277-Exposes\",\n      \"from\": 180,\n      \"to\": 1277,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1188-Exposes\",\n      \"from\": 180,\n      \"to\": 1188,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1281-Exposes\",\n      \"from\": 180,\n      \"to\": 1281,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1155-Exposes\",\n      \"from\": 180,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1263-Exposes\",\n      \"from\": 180,\n      \"to\": 1263,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1290-Exposes\",\n      \"from\": 180,\n      \"to\": 1290,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1782-Exposes\",\n      \"from\": 180,\n      \"to\": 1782,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1310-Exposes\",\n      \"from\": 180,\n      \"to\": 1310,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1317-Exposes\",\n      \"from\": 180,\n      \"to\": 1317,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1783-Exposes\",\n      \"from\": 180,\n      \"to\": 1783,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1326-Exposes\",\n      \"from\": 180,\n      \"to\": 1326,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1784-Exposes\",\n      \"from\": 180,\n      \"to\": 1784,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1327-Exposes\",\n      \"from\": 180,\n      \"to\": 1327,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1331-Exposes\",\n      \"from\": 180,\n      \"to\": 1331,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1359-Exposes\",\n      \"from\": 180,\n      \"to\": 1359,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1785-Exposes\",\n      \"from\": 180,\n      \"to\": 1785,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1377-Exposes\",\n      \"from\": 180,\n      \"to\": 1377,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1383-Exposes\",\n      \"from\": 180,\n      \"to\": 1383,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1419-Exposes\",\n      \"from\": 180,\n      \"to\": 1419,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1422-Exposes\",\n      \"from\": 180,\n      \"to\": 1422,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1786-Exposes\",\n      \"from\": 180,\n      \"to\": 1786,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1787-Exposes\",\n      \"from\": 180,\n      \"to\": 1787,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1788-Exposes\",\n      \"from\": 180,\n      \"to\": 1788,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1433-Exposes\",\n      \"from\": 180,\n      \"to\": 1433,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1449-Exposes\",\n      \"from\": 180,\n      \"to\": 1449,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1789-Exposes\",\n      \"from\": 180,\n      \"to\": 1789,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1455-Exposes\",\n      \"from\": 180,\n      \"to\": 1455,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1790-Exposes\",\n      \"from\": 180,\n      \"to\": 1790,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1791-Exposes\",\n      \"from\": 180,\n      \"to\": 1791,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1792-Exposes\",\n      \"from\": 180,\n      \"to\": 1792,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1793-Exposes\",\n      \"from\": 180,\n      \"to\": 1793,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1495-Exposes\",\n      \"from\": 180,\n      \"to\": 1495,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1794-Exposes\",\n      \"from\": 180,\n      \"to\": 1794,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1498-Exposes\",\n      \"from\": 180,\n      \"to\": 1498,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1512-Exposes\",\n      \"from\": 180,\n      \"to\": 1512,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1518-Exposes\",\n      \"from\": 180,\n      \"to\": 1518,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1522-Exposes\",\n      \"from\": 180,\n      \"to\": 1522,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1795-Exposes\",\n      \"from\": 180,\n      \"to\": 1795,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1796-Exposes\",\n      \"from\": 180,\n      \"to\": 1796,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1535-Exposes\",\n      \"from\": 180,\n      \"to\": 1535,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1537-Exposes\",\n      \"from\": 180,\n      \"to\": 1537,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1572-Exposes\",\n      \"from\": 180,\n      \"to\": 1572,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1578-Exposes\",\n      \"from\": 180,\n      \"to\": 1578,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1797-Exposes\",\n      \"from\": 180,\n      \"to\": 1797,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1798-Exposes\",\n      \"from\": 180,\n      \"to\": 1798,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1799-Exposes\",\n      \"from\": 180,\n      \"to\": 1799,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1613-Exposes\",\n      \"from\": 180,\n      \"to\": 1613,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1800-Exposes\",\n      \"from\": 180,\n      \"to\": 1800,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1160-Exposes\",\n      \"from\": 180,\n      \"to\": 1160,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1626-Exposes\",\n      \"from\": 180,\n      \"to\": 1626,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1627-Exposes\",\n      \"from\": 180,\n      \"to\": 1627,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1801-Exposes\",\n      \"from\": 180,\n      \"to\": 1801,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1802-Exposes\",\n      \"from\": 180,\n      \"to\": 1802,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1634-Exposes\",\n      \"from\": 180,\n      \"to\": 1634,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1803-Exposes\",\n      \"from\": 180,\n      \"to\": 1803,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1804-Exposes\",\n      \"from\": 180,\n      \"to\": 1804,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1646-Exposes\",\n      \"from\": 180,\n      \"to\": 1646,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1805-Exposes\",\n      \"from\": 180,\n      \"to\": 1805,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1656-Exposes\",\n      \"from\": 180,\n      \"to\": 1656,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1697-Exposes\",\n      \"from\": 180,\n      \"to\": 1697,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1806-Exposes\",\n      \"from\": 180,\n      \"to\": 1806,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1807-Exposes\",\n      \"from\": 180,\n      \"to\": 1807,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1200-Exposes\",\n      \"from\": 180,\n      \"to\": 1200,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1729-Exposes\",\n      \"from\": 180,\n      \"to\": 1729,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1730-Exposes\",\n      \"from\": 180,\n      \"to\": 1730,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"180-1808-ResolvesTo\",\n      \"from\": 180,\n      \"to\": 1808,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"180-1809-ResolvesTo\",\n      \"from\": 180,\n      \"to\": 1809,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"181-1153-Exposes\",\n      \"from\": 181,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"181-1154-Exposes\",\n      \"from\": 181,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"181-1155-Exposes\",\n      \"from\": 181,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"181-1810-ResolvesTo\",\n      \"from\": 181,\n      \"to\": 1810,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"181-1811-ResolvesTo\",\n      \"from\": 181,\n      \"to\": 1811,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"183-1153-Exposes\",\n      \"from\": 183,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"183-1154-Exposes\",\n      \"from\": 183,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"183-1812-ResolvesTo\",\n      \"from\": 183,\n      \"to\": 1812,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"188-1813-Exposes\",\n      \"from\": 188,\n      \"to\": 1813,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"188-1477-Exposes\",\n      \"from\": 188,\n      \"to\": 1477,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"189-1154-Exposes\",\n      \"from\": 189,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"189-1254-Exposes\",\n      \"from\": 189,\n      \"to\": 1254,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"189-1155-Exposes\",\n      \"from\": 189,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"189-1255-Exposes\",\n      \"from\": 189,\n      \"to\": 1255,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"189-1256-ResolvesTo\",\n      \"from\": 189,\n      \"to\": 1256,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"191-1155-Exposes\",\n      \"from\": 191,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"191-1814-Exposes\",\n      \"from\": 191,\n      \"to\": 1814,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"191-1815-Exposes\",\n      \"from\": 191,\n      \"to\": 1815,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"191-1816-ResolvesTo\",\n      \"from\": 191,\n      \"to\": 1816,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"196-1154-Exposes\",\n      \"from\": 196,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"196-1155-Exposes\",\n      \"from\": 196,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"196-1817-Exposes\",\n      \"from\": 196,\n      \"to\": 1817,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"196-1156-Exposes\",\n      \"from\": 196,\n      \"to\": 1156,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"196-1818-ResolvesTo\",\n      \"from\": 196,\n      \"to\": 1818,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"200-1153-Exposes\",\n      \"from\": 200,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"200-1819-ResolvesTo\",\n      \"from\": 200,\n      \"to\": 1819,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"201-1153-Exposes\",\n      \"from\": 201,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"201-1268-Exposes\",\n      \"from\": 201,\n      \"to\": 1268,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"201-1154-Exposes\",\n      \"from\": 201,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"201-1218-Exposes\",\n      \"from\": 201,\n      \"to\": 1218,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"201-1220-Exposes\",\n      \"from\": 201,\n      \"to\": 1220,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"201-1155-Exposes\",\n      \"from\": 201,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"201-1221-Exposes\",\n      \"from\": 201,\n      \"to\": 1221,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"201-1222-Exposes\",\n      \"from\": 201,\n      \"to\": 1222,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"201-1223-Exposes\",\n      \"from\": 201,\n      \"to\": 1223,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"201-1224-Exposes\",\n      \"from\": 201,\n      \"to\": 1224,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"201-1355-Exposes\",\n      \"from\": 201,\n      \"to\": 1355,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"201-1820-Exposes\",\n      \"from\": 201,\n      \"to\": 1820,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"201-1821-ResolvesTo\",\n      \"from\": 201,\n      \"to\": 1821,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"207-1153-Exposes\",\n      \"from\": 207,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"207-1154-Exposes\",\n      \"from\": 207,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"207-1155-Exposes\",\n      \"from\": 207,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"207-1822-ResolvesTo\",\n      \"from\": 207,\n      \"to\": 1822,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"209-1153-Exposes\",\n      \"from\": 209,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"209-1154-Exposes\",\n      \"from\": 209,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"209-1155-Exposes\",\n      \"from\": 209,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"213-1153-Exposes\",\n      \"from\": 213,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"213-1154-Exposes\",\n      \"from\": 213,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"213-1155-Exposes\",\n      \"from\": 213,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"213-1823-ResolvesTo\",\n      \"from\": 213,\n      \"to\": 1823,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"213-1824-ResolvesTo\",\n      \"from\": 213,\n      \"to\": 1824,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"217-1153-Exposes\",\n      \"from\": 217,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"217-1154-Exposes\",\n      \"from\": 217,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"217-1155-Exposes\",\n      \"from\": 217,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"217-1187-ResolvesTo\",\n      \"from\": 217,\n      \"to\": 1187,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"217-1825-ResolvesTo\",\n      \"from\": 217,\n      \"to\": 1825,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"224-1826-Exposes\",\n      \"from\": 224,\n      \"to\": 1826,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"224-1827-Exposes\",\n      \"from\": 224,\n      \"to\": 1827,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"224-1828-Exposes\",\n      \"from\": 224,\n      \"to\": 1828,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"228-1154-Exposes\",\n      \"from\": 228,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"228-1155-Exposes\",\n      \"from\": 228,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"228-1342-Exposes\",\n      \"from\": 228,\n      \"to\": 1342,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"228-1345-Exposes\",\n      \"from\": 228,\n      \"to\": 1345,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"228-1829-Exposes\",\n      \"from\": 228,\n      \"to\": 1829,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"228-1830-ResolvesTo\",\n      \"from\": 228,\n      \"to\": 1830,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"228-1831-ResolvesTo\",\n      \"from\": 228,\n      \"to\": 1831,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"229-1153-Exposes\",\n      \"from\": 229,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"229-1155-Exposes\",\n      \"from\": 229,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"231-1153-Exposes\",\n      \"from\": 231,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"231-1154-Exposes\",\n      \"from\": 231,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"231-1832-Exposes\",\n      \"from\": 231,\n      \"to\": 1832,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"231-1155-Exposes\",\n      \"from\": 231,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"231-1291-Exposes\",\n      \"from\": 231,\n      \"to\": 1291,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"231-1317-Exposes\",\n      \"from\": 231,\n      \"to\": 1317,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"231-1319-Exposes\",\n      \"from\": 231,\n      \"to\": 1319,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"231-1321-Exposes\",\n      \"from\": 231,\n      \"to\": 1321,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"231-1324-Exposes\",\n      \"from\": 231,\n      \"to\": 1324,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"231-1342-Exposes\",\n      \"from\": 231,\n      \"to\": 1342,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"231-1833-Exposes\",\n      \"from\": 231,\n      \"to\": 1833,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"231-1368-Exposes\",\n      \"from\": 231,\n      \"to\": 1368,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"231-1834-Exposes\",\n      \"from\": 231,\n      \"to\": 1834,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"231-1835-Exposes\",\n      \"from\": 231,\n      \"to\": 1835,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"231-1415-Exposes\",\n      \"from\": 231,\n      \"to\": 1415,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"231-1435-Exposes\",\n      \"from\": 231,\n      \"to\": 1435,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"231-1461-Exposes\",\n      \"from\": 231,\n      \"to\": 1461,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"231-1792-Exposes\",\n      \"from\": 231,\n      \"to\": 1792,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"231-1836-Exposes\",\n      \"from\": 231,\n      \"to\": 1836,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"231-1613-Exposes\",\n      \"from\": 231,\n      \"to\": 1613,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"231-1837-Exposes\",\n      \"from\": 231,\n      \"to\": 1837,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"231-1682-Exposes\",\n      \"from\": 231,\n      \"to\": 1682,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"231-1730-Exposes\",\n      \"from\": 231,\n      \"to\": 1730,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"231-1838-ResolvesTo\",\n      \"from\": 231,\n      \"to\": 1838,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"232-1153-Exposes\",\n      \"from\": 232,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"232-1159-Exposes\",\n      \"from\": 232,\n      \"to\": 1159,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"232-1839-ResolvesTo\",\n      \"from\": 232,\n      \"to\": 1839,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"233-1153-Exposes\",\n      \"from\": 233,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"233-1154-Exposes\",\n      \"from\": 233,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"233-1155-Exposes\",\n      \"from\": 233,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"233-1157-Exposes\",\n      \"from\": 233,\n      \"to\": 1157,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"233-1840-ResolvesTo\",\n      \"from\": 233,\n      \"to\": 1840,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"234-1154-Exposes\",\n      \"from\": 234,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"234-1537-Exposes\",\n      \"from\": 234,\n      \"to\": 1537,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"234-1174-Exposes\",\n      \"from\": 234,\n      \"to\": 1174,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"235-1153-Exposes\",\n      \"from\": 235,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"235-1154-Exposes\",\n      \"from\": 235,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"235-1155-Exposes\",\n      \"from\": 235,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"235-1841-ResolvesTo\",\n      \"from\": 235,\n      \"to\": 1841,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"235-1842-ResolvesTo\",\n      \"from\": 235,\n      \"to\": 1842,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"237-1153-Exposes\",\n      \"from\": 237,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"237-1155-Exposes\",\n      \"from\": 237,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"237-1843-Exposes\",\n      \"from\": 237,\n      \"to\": 1843,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"237-1601-Exposes\",\n      \"from\": 237,\n      \"to\": 1601,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"237-1844-ResolvesTo\",\n      \"from\": 237,\n      \"to\": 1844,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"238-1153-Exposes\",\n      \"from\": 238,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"238-1154-Exposes\",\n      \"from\": 238,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"238-1155-Exposes\",\n      \"from\": 238,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"238-1157-Exposes\",\n      \"from\": 238,\n      \"to\": 1157,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"238-1845-ResolvesTo\",\n      \"from\": 238,\n      \"to\": 1845,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"238-1846-ResolvesTo\",\n      \"from\": 238,\n      \"to\": 1846,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"238-1847-ResolvesTo\",\n      \"from\": 238,\n      \"to\": 1847,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"238-1848-ResolvesTo\",\n      \"from\": 238,\n      \"to\": 1848,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"238-1849-ResolvesTo\",\n      \"from\": 238,\n      \"to\": 1849,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"240-1153-Exposes\",\n      \"from\": 240,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"240-1154-Exposes\",\n      \"from\": 240,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"240-1155-Exposes\",\n      \"from\": 240,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"240-1850-ResolvesTo\",\n      \"from\": 240,\n      \"to\": 1850,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"240-1851-ResolvesTo\",\n      \"from\": 240,\n      \"to\": 1851,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"244-1153-Exposes\",\n      \"from\": 244,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"244-1852-ResolvesTo\",\n      \"from\": 244,\n      \"to\": 1852,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"247-1153-Exposes\",\n      \"from\": 247,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"247-1853-ResolvesTo\",\n      \"from\": 247,\n      \"to\": 1853,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"248-1153-Exposes\",\n      \"from\": 248,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"248-1217-Exposes\",\n      \"from\": 248,\n      \"to\": 1217,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"248-1154-Exposes\",\n      \"from\": 248,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"248-1155-Exposes\",\n      \"from\": 248,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"248-1854-ResolvesTo\",\n      \"from\": 248,\n      \"to\": 1854,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"248-1855-ResolvesTo\",\n      \"from\": 248,\n      \"to\": 1855,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"250-1856-Exposes\",\n      \"from\": 250,\n      \"to\": 1856,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"250-1857-ResolvesTo\",\n      \"from\": 250,\n      \"to\": 1857,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"251-1153-Exposes\",\n      \"from\": 251,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"251-1154-Exposes\",\n      \"from\": 251,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"251-1155-Exposes\",\n      \"from\": 251,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"251-1858-ResolvesTo\",\n      \"from\": 251,\n      \"to\": 1858,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"251-1859-ResolvesTo\",\n      \"from\": 251,\n      \"to\": 1859,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"251-1860-ResolvesTo\",\n      \"from\": 251,\n      \"to\": 1860,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"254-1154-Exposes\",\n      \"from\": 254,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"254-1155-Exposes\",\n      \"from\": 254,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"254-1187-ResolvesTo\",\n      \"from\": 254,\n      \"to\": 1187,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"254-1861-ResolvesTo\",\n      \"from\": 254,\n      \"to\": 1861,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"255-1153-Exposes\",\n      \"from\": 255,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"255-1154-Exposes\",\n      \"from\": 255,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"255-1155-Exposes\",\n      \"from\": 255,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"255-1862-Exposes\",\n      \"from\": 255,\n      \"to\": 1862,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"255-1863-ResolvesTo\",\n      \"from\": 255,\n      \"to\": 1863,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"255-1864-ResolvesTo\",\n      \"from\": 255,\n      \"to\": 1864,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"257-1155-Exposes\",\n      \"from\": 257,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"257-1187-ResolvesTo\",\n      \"from\": 257,\n      \"to\": 1187,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"257-1865-ResolvesTo\",\n      \"from\": 257,\n      \"to\": 1865,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"260-1153-Exposes\",\n      \"from\": 260,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"260-1154-Exposes\",\n      \"from\": 260,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"260-1155-Exposes\",\n      \"from\": 260,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"260-1866-Exposes\",\n      \"from\": 260,\n      \"to\": 1866,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"262-1153-Exposes\",\n      \"from\": 262,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"262-1867-ResolvesTo\",\n      \"from\": 262,\n      \"to\": 1867,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"263-1153-Exposes\",\n      \"from\": 263,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"263-1868-Exposes\",\n      \"from\": 263,\n      \"to\": 1868,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"264-1153-Exposes\",\n      \"from\": 264,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"266-1154-Exposes\",\n      \"from\": 266,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"266-1254-Exposes\",\n      \"from\": 266,\n      \"to\": 1254,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"266-1155-Exposes\",\n      \"from\": 266,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"266-1255-Exposes\",\n      \"from\": 266,\n      \"to\": 1255,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"266-1256-ResolvesTo\",\n      \"from\": 266,\n      \"to\": 1256,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"268-1153-Exposes\",\n      \"from\": 268,\n      \"to\": 1153,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"268-1154-Exposes\",\n      \"from\": 268,\n      \"to\": 1154,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"268-1155-Exposes\",\n      \"from\": 268,\n      \"to\": 1155,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"268-1869-ResolvesTo\",\n      \"from\": 268,\n      \"to\": 1869,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"268-1187-ResolvesTo\",\n      \"from\": 268,\n      \"to\": 1187,\n      \"label\": \"Resolves to\"\n    }\n  ]\n}"
  },
  {
    "path": "network_graph-demo-ncsc.json",
    "content": "{\n  \"nodes\": [\n    {\n      \"id\": 1,\n      \"type\": \"domain\",\n      \"domain\": \"ncsc.gov.uk\"\n    },\n    {\n      \"id\": 2,\n      \"type\": \"ip\",\n      \"ip\": \"13.224.222.50\"\n    },\n    {\n      \"id\": 3,\n      \"type\": \"ip\",\n      \"ip\": \"13.224.222.76\"\n    },\n    {\n      \"id\": 4,\n      \"type\": \"ip\",\n      \"ip\": \"13.224.222.59\"\n    },\n    {\n      \"id\": 5,\n      \"type\": \"ip\",\n      \"ip\": \"13.224.222.104\"\n    },\n    {\n      \"id\": 6,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"80\"\n    },\n    {\n      \"id\": 7,\n      \"type\": \"port\",\n      \"portType\": \"TCP\",\n      \"portNumber\": \"443\"\n    },\n    {\n      \"id\": 8,\n      \"type\": \"asn\",\n      \"asn\": \"AS16509\"\n    },\n    {\n      \"id\": 9,\n      \"type\": \"city\",\n      \"city\": \"London\"\n    },\n    {\n      \"id\": 10,\n      \"type\": \"organization\",\n      \"organization\": \"Amazon.com, Inc.\"\n    },\n    {\n      \"id\": 11,\n      \"type\": \"country\",\n      \"country\": \"GB\"\n    },\n    {\n      \"id\": 12,\n      \"type\": \"hosting\"\n    }\n  ],\n  \"edges\": [\n    {\n      \"id\": \"1-2-ResolvesTo\",\n      \"from\": 1,\n      \"to\": 2,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"1-3-ResolvesTo\",\n      \"from\": 1,\n      \"to\": 3,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"1-4-ResolvesTo\",\n      \"from\": 1,\n      \"to\": 4,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"1-5-ResolvesTo\",\n      \"from\": 1,\n      \"to\": 5,\n      \"label\": \"Resolves to\"\n    },\n    {\n      \"id\": \"2-6-Exposes\",\n      \"from\": 2,\n      \"to\": 6,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"2-7-Exposes\",\n      \"from\": 2,\n      \"to\": 7,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"3-6-Exposes\",\n      \"from\": 3,\n      \"to\": 6,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"3-7-Exposes\",\n      \"from\": 3,\n      \"to\": 7,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"4-6-Exposes\",\n      \"from\": 4,\n      \"to\": 6,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"4-7-Exposes\",\n      \"from\": 4,\n      \"to\": 7,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"5-6-Exposes\",\n      \"from\": 5,\n      \"to\": 6,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"5-7-Exposes\",\n      \"from\": 5,\n      \"to\": 7,\n      \"label\": \"Exposes\"\n    },\n    {\n      \"id\": \"2-8-Assigned to\",\n      \"from\": 2,\n      \"to\": 8,\n      \"label\": \"Assigned to\"\n    },\n    {\n      \"id\": \"2-9-Located in\",\n      \"from\": 2,\n      \"to\": 9,\n      \"label\": \"Located in\"\n    },\n    {\n      \"id\": \"2-10-Belongs to\",\n      \"from\": 2,\n      \"to\": 10,\n      \"label\": \"Belongs to\"\n    },\n    {\n      \"id\": \"2-11-Located in\",\n      \"from\": 2,\n      \"to\": 11,\n      \"label\": \"Located in\"\n    },\n    {\n      \"id\": \"2-12-Uses\",\n      \"from\": 2,\n      \"to\": 12,\n      \"label\": \"Uses\"\n    },\n    {\n      \"id\": \"3-8-Assigned to\",\n      \"from\": 3,\n      \"to\": 8,\n      \"label\": \"Assigned to\"\n    },\n    {\n      \"id\": \"3-9-Located in\",\n      \"from\": 3,\n      \"to\": 9,\n      \"label\": \"Located in\"\n    },\n    {\n      \"id\": \"3-10-Belongs to\",\n      \"from\": 3,\n      \"to\": 10,\n      \"label\": \"Belongs to\"\n    },\n    {\n      \"id\": \"3-11-Located in\",\n      \"from\": 3,\n      \"to\": 11,\n      \"label\": \"Located in\"\n    },\n    {\n      \"id\": \"3-12-Uses\",\n      \"from\": 3,\n      \"to\": 12,\n      \"label\": \"Uses\"\n    },\n    {\n      \"id\": \"4-8-Assigned to\",\n      \"from\": 4,\n      \"to\": 8,\n      \"label\": \"Assigned to\"\n    },\n    {\n      \"id\": \"4-9-Located in\",\n      \"from\": 4,\n      \"to\": 9,\n      \"label\": \"Located in\"\n    },\n    {\n      \"id\": \"4-10-Belongs to\",\n      \"from\": 4,\n      \"to\": 10,\n      \"label\": \"Belongs to\"\n    },\n    {\n      \"id\": \"4-11-Located in\",\n      \"from\": 4,\n      \"to\": 11,\n      \"label\": \"Located in\"\n    },\n    {\n      \"id\": \"4-12-Uses\",\n      \"from\": 4,\n      \"to\": 12,\n      \"label\": \"Uses\"\n    },\n    {\n      \"id\": \"5-8-Assigned to\",\n      \"from\": 5,\n      \"to\": 8,\n      \"label\": \"Assigned to\"\n    },\n    {\n      \"id\": \"5-9-Located in\",\n      \"from\": 5,\n      \"to\": 9,\n      \"label\": \"Located in\"\n    },\n    {\n      \"id\": \"5-10-Belongs to\",\n      \"from\": 5,\n      \"to\": 10,\n      \"label\": \"Belongs to\"\n    },\n    {\n      \"id\": \"5-11-Located in\",\n      \"from\": 5,\n      \"to\": 11,\n      \"label\": \"Located in\"\n    },\n    {\n      \"id\": \"5-12-Uses\",\n      \"from\": 5,\n      \"to\": 12,\n      \"label\": \"Uses\"\n    }\n  ]\n}"
  },
  {
    "path": "nrich.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\" />\n  <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n  <title>IP Enricher (Shodan InternetDB)</title>\n  <style>\n    :root { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }\n    * { box-sizing: border-box; }\n\n    body { margin: 0; padding: 16px; background: #0b0f14; color: #e7eef7; }\n    h1 { margin: 0 0 12px; font-size: 18px; }\n\n    .row { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }\n    .card { background: #0f1620; border: 1px solid #243244; border-radius: 14px; padding: 12px; }\n    .muted { color: #a9b7c9; font-size: 12px; }\n    .small { font-size: 12px; }\n    .spacer { flex: 1; }\n\n    textarea {\n      width: 100%;\n      box-sizing: border-box;\n      min-height: 170px;\n      max-height: 340px;\n      resize: vertical;\n      background: #0f1620;\n      color: #e7eef7;\n      border: 1px solid #243244;\n      border-radius: 10px;\n      padding: 12px;\n      line-height: 1.4;\n      display: block;\n    }\n\n    button, select, input[type=\"number\"], input[type=\"text\"] {\n      background: #132033;\n      color: #e7eef7;\n      border: 1px solid #243244;\n      border-radius: 10px;\n      padding: 10px 12px;\n      cursor: pointer;\n    }\n    button:disabled { opacity: .5; cursor: not-allowed; }\n\n    progress { width: 100%; height: 14px; }\n\n    table { width: 100%; border-collapse: collapse; }\n    th, td { border-bottom: 1px solid #223044; padding: 10px 8px; text-align: left; vertical-align: top; }\n    th { position: sticky; top: 0; background: #0f1620; z-index: 1; }\n\n    .pill { display: inline-block; padding: 2px 8px; border: 1px solid #2b3b53; border-radius: 999px; margin: 2px 4px 2px 0; font-size: 12px; color: #cfe1ff; }\n\n    .grid { display: grid; grid-template-columns: 1fr; gap: 12px; }\n    @media (min-width: 1100px) { .grid { grid-template-columns: 1.5fr .5fr; } }\n\n    .tableWrap { max-height: 62vh; overflow: auto; border-radius: 14px; border: 1px solid #243244; }\n\n    .warn { color: #ffd48a; }\n    .err { color: #ff9aa7; }\n    .ok { color: #9affc2; }\n\n    /* Keep pagination footer visible within results card */\n    .resultsFooter {\n      position: sticky;\n      bottom: 0;\n      background: #0f1620;\n      padding-top: 10px;\n      margin-top: 10px;\n      border-top: 1px solid #223044;\n    }\n  </style>\n</head>\n<body>\n  <h1>IP Enricher (Shodan InternetDB)</h1>\n\n  <div class=\"grid\">\n    <div class=\"card\">\n      <div class=\"muted\">\n        Enrichment endpoint used by <code>nrich</code>: <code>https://internetdb.shodan.io/{IP}</code>.\n      </div>\n\n      <div style=\"margin-top:10px;\">\n        <textarea id=\"input\" placeholder=\"Paste text that contains IPs (logs, alerts, etc) ...\"></textarea>\n      </div>\n\n      <div class=\"row\" style=\"margin-top:10px;\">\n        <button id=\"extractBtn\">Extract IPs</button>\n        <button id=\"processBtn\" disabled>Process</button>\n        <button id=\"stopBtn\" disabled>Stop</button>\n\n        <span class=\"spacer\"></span>\n\n        <label class=\"small muted\">Concurrency:\n          <input id=\"concurrency\" type=\"number\" value=\"8\" min=\"1\" max=\"50\" style=\"width:80px;\">\n        </label>\n\n        <label class=\"small muted\">Mode:\n          <select id=\"mode\">\n            <option value=\"ip\">Pivot by IP</option>\n            <option value=\"port\">Pivot by Port</option>\n          </select>\n        </label>\n      </div>\n\n      <div style=\"margin-top:10px;\">\n        <progress id=\"progress\" value=\"0\" max=\"100\"></progress>\n        <div class=\"row\" style=\"margin-top:6px;\">\n          <div id=\"status\" class=\"muted\">Paste text, then click “Extract IPs”.</div>\n          <div class=\"spacer\"></div>\n          <div class=\"muted\" id=\"counts\"></div>\n        </div>\n      </div>\n\n      <div class=\"row\" style=\"margin-top:10px;\">\n        <button id=\"exportJsonBtn\" disabled>Export JSON</button>\n        <button id=\"exportCsvBtn\" disabled>Export CSV</button>\n\n        <span class=\"spacer\"></span>\n\n        <label class=\"small muted\">Search:\n          <input id=\"search\" type=\"text\" placeholder=\"filter table...\" style=\"width:240px;\">\n        </label>\n      </div>\n\n      <div style=\"margin-top:10px;\" class=\"muted small\">\n        <div><span class=\"pill\">Tip</span> “Pivot by Port” repeats IP metadata per port for easy spreadsheet pivoting.</div>\n        <div style=\"margin-top:6px;\"><span class=\"pill warn\">Note</span> If requests fail with CORS, use the proxy snippet at the bottom.</div>\n      </div>\n    </div>\n\n    <div class=\"card\">\n      <div class=\"muted small\">\n        <div><span class=\"pill\">Workflow</span> Extract → Process → Pivot → Export</div>\n        <div style=\"margin-top:6px;\"><span class=\"pill\">Pivot</span> Use “Pivot by Port” for one row per IP+Port.</div>\n        <div style=\"margin-top:6px;\"><span class=\"pill warn\">CORS</span> If blocked, run local proxy and set <code>API_BASE</code>.</div>\n      </div>\n    </div>\n  </div>\n\n  <!-- Results (table + pagination) -->\n  <div class=\"card\" style=\"margin-top:12px;\">\n    <div class=\"tableWrap\">\n      <table id=\"table\">\n        <thead><tr id=\"theadRow\"></tr></thead>\n        <tbody id=\"tbody\"></tbody>\n      </table>\n    </div>\n\n    <div class=\"row resultsFooter\">\n      <button id=\"prevBtn\" disabled>◀ Prev</button>\n      <button id=\"nextBtn\" disabled>Next ▶</button>\n\n      <span class=\"muted small\" id=\"pageInfo\" style=\"margin-left:10px;\"></span>\n\n      <span class=\"spacer\"></span>\n\n      <label class=\"small muted\">\n        Page size:\n        <select id=\"pageSize\">\n          <option>10</option>\n          <option selected>25</option>\n          <option>50</option>\n          <option>100</option>\n          <option>250</option>\n        </select>\n      </label>\n    </div>\n  </div>\n\n<script>\n(() => {\n  // --- Config ---\n  // If you need a proxy, set API_BASE to something like: \"http://localhost:8787/internetdb/\"\n  // and have it forward to https://internetdb.shodan.io/\n  const API_BASE = \"https://internetdb.shodan.io/\"; // endpoint used by nrich\n\n  // IPv4 regex (strict-ish), IPv6 regex (practical, not full RFC edge-cases).\n  const IPV4_RE = /\\b(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\b/g;\n  const IPV6_RE = /\\b(?:[A-F0-9]{1,4}:){2,7}[A-F0-9]{1,4}\\b|\\b(?:[A-F0-9]{1,4}:){1,7}:\\b|\\b:(?::[A-F0-9]{1,4}){1,7}\\b/gi;\n\n  // --- State ---\n  let extracted = [];                 // unique IPs to enrich\n  let resultsByIp = new Map();        // ip -> response or {_error}\n  let flatRows = [];                  // rows for current pivot mode\n  let currentPage = 1;\n  let stopRequested = false;\n\n  // --- Elements ---\n  const elInput = document.getElementById(\"input\");\n  const elExtractBtn = document.getElementById(\"extractBtn\");\n  const elProcessBtn = document.getElementById(\"processBtn\");\n  const elStopBtn = document.getElementById(\"stopBtn\");\n  const elProgress = document.getElementById(\"progress\");\n  const elStatus = document.getElementById(\"status\");\n  const elCounts = document.getElementById(\"counts\");\n  const elMode = document.getElementById(\"mode\");\n  const elConcurrency = document.getElementById(\"concurrency\");\n\n  const elExportJsonBtn = document.getElementById(\"exportJsonBtn\");\n  const elExportCsvBtn = document.getElementById(\"exportCsvBtn\");\n\n  const elSearch = document.getElementById(\"search\");\n\n  const elPageSize = document.getElementById(\"pageSize\");\n  const elPrevBtn = document.getElementById(\"prevBtn\");\n  const elNextBtn = document.getElementById(\"nextBtn\");\n  const elPageInfo = document.getElementById(\"pageInfo\");\n\n  const elTheadRow = document.getElementById(\"theadRow\");\n  const elTbody = document.getElementById(\"tbody\");\n\n  function setStatus(msg, cls=\"muted\") {\n    elStatus.className = cls;\n    elStatus.textContent = msg;\n  }\n\n  function extractIps(text) {\n    const v4 = text.match(IPV4_RE) || [];\n    const v6 = text.match(IPV6_RE) || [];\n    const all = [...v4, ...v6.map(x => x.toLowerCase())];\n    return Array.from(new Set(all));\n  }\n\n  function downloadBlob(filename, contentType, data) {\n    const blob = new Blob([data], { type: contentType });\n    const url = URL.createObjectURL(blob);\n    const a = document.createElement(\"a\");\n    a.href = url;\n    a.download = filename;\n    document.body.appendChild(a);\n    a.click();\n    a.remove();\n    URL.revokeObjectURL(url);\n  }\n\n  function safeJoin(arr) {\n    if (!Array.isArray(arr) || arr.length === 0) return \"\";\n    return arr.join(\", \");\n  }\n\n  function buildRows(pivotMode) {\n    const rows = [];\n    for (const [ip, obj] of resultsByIp.entries()) {\n      if (!obj || obj._error) {\n        rows.push({\n          ip,\n          status: \"error\",\n          error: obj?._error || \"Unknown error\",\n          ports: \"\",\n          hostnames: \"\",\n          tags: \"\",\n          cpes: \"\",\n          vulns: \"\"\n        });\n        continue;\n      }\n\n      const base = {\n        ip,\n        status: \"ok\",\n        hostnames: safeJoin(obj.hostnames),\n        tags: safeJoin(obj.tags),\n        cpes: safeJoin(obj.cpes),\n        vulns: safeJoin(obj.vulns),\n        error: \"\"\n      };\n\n      const ports = Array.isArray(obj.ports) ? obj.ports : [];\n\n      if (pivotMode === \"ip\") {\n        rows.push({ ...base, ports: safeJoin(ports.map(String)) });\n      } else {\n        if (ports.length === 0) {\n          rows.push({ ...base, port: \"\" });\n        } else {\n          for (const p of ports) rows.push({ ...base, port: String(p) });\n        }\n      }\n    }\n    return rows;\n  }\n\n  function applyFilter(rows) {\n    const q = (elSearch.value || \"\").trim().toLowerCase();\n    if (!q) return rows;\n    return rows.filter(r => JSON.stringify(r).toLowerCase().includes(q));\n  }\n\n  function renderTable() {\n    const pivotMode = elMode.value;\n    const pageSize = parseInt(elPageSize.value, 10);\n\n    flatRows = buildRows(pivotMode);\n    const filtered = applyFilter(flatRows);\n\n    const total = filtered.length;\n    const totalPages = Math.max(1, Math.ceil(total / pageSize));\n    currentPage = Math.min(currentPage, totalPages);\n\n    const start = (currentPage - 1) * pageSize;\n    const pageRows = filtered.slice(start, start + pageSize);\n\n    const columns = pivotMode === \"ip\"\n      ? [\"ip\",\"status\",\"ports\",\"hostnames\",\"tags\",\"cpes\",\"vulns\",\"error\"]\n      : [\"ip\",\"status\",\"port\",\"hostnames\",\"tags\",\"cpes\",\"vulns\",\"error\"];\n\n    elTheadRow.innerHTML = \"\";\n    for (const c of columns) {\n      const th = document.createElement(\"th\");\n      th.textContent = c.toUpperCase();\n      elTheadRow.appendChild(th);\n    }\n\n    elTbody.innerHTML = \"\";\n    for (const row of pageRows) {\n      const tr = document.createElement(\"tr\");\n      for (const c of columns) {\n        const td = document.createElement(\"td\");\n        const v = row[c] ?? \"\";\n        if (c === \"status\") {\n          td.innerHTML = v === \"ok\" ? '<span class=\"ok\">ok</span>' : '<span class=\"err\">error</span>';\n        } else {\n          td.textContent = String(v);\n        }\n        tr.appendChild(td);\n      }\n      elTbody.appendChild(tr);\n    }\n\n    elPrevBtn.disabled = currentPage <= 1;\n    elNextBtn.disabled = currentPage >= totalPages;\n    elPageInfo.textContent = `Page ${currentPage} / ${totalPages} • ${total} row(s)`;\n\n    const hasAny = resultsByIp.size > 0;\n    elExportJsonBtn.disabled = !hasAny;\n    elExportCsvBtn.disabled = !hasAny;\n  }\n\n  function toCSV(rows) {\n    if (!rows.length) return \"\";\n    const cols = Object.keys(rows[0]);\n    const esc = (s) => {\n      const str = String(s ?? \"\");\n      if (/[\",\\n]/.test(str)) return '\"' + str.replace(/\"/g, '\"\"') + '\"';\n      return str;\n    };\n    const head = cols.map(esc).join(\",\");\n    const body = rows.map(r => cols.map(c => esc(r[c])).join(\",\")).join(\"\\n\");\n    return head + \"\\n\" + body;\n  }\n\n  async function fetchInternetDB(ip) {\n    const url = API_BASE.endsWith(\"/\") ? (API_BASE + encodeURIComponent(ip)) : (API_BASE + \"/\" + encodeURIComponent(ip));\n    const res = await fetch(url, { method: \"GET\" });\n    if (!res.ok) throw new Error(`HTTP ${res.status}`);\n    return await res.json();\n  }\n\n  async function runWithConcurrency(items, workerFn, concurrency) {\n    let idx = 0;\n    let done = 0;\n    const total = items.length;\n\n    async function worker() {\n      while (true) {\n        if (stopRequested) return;\n        const myIdx = idx++;\n        if (myIdx >= total) return;\n\n        const item = items[myIdx];\n        try {\n          await workerFn(item, myIdx);\n        } finally {\n          done++;\n          const pct = total ? Math.round((done / total) * 100) : 100;\n          elProgress.value = pct;\n          elCounts.textContent = `${done}/${total}`;\n          setStatus(`Processing… ${done}/${total}`, \"muted\");\n          renderTable();\n        }\n      }\n    }\n\n    const workers = Array.from({ length: Math.max(1, concurrency) }, () => worker());\n    await Promise.all(workers);\n  }\n\n  // --- Events ---\n  elExtractBtn.addEventListener(\"click\", () => {\n    extracted = extractIps(elInput.value || \"\");\n    resultsByIp = new Map();\n    currentPage = 1;\n    elProgress.value = 0;\n\n    if (extracted.length === 0) {\n      setStatus(\"No IPs found. Paste content containing IPv4/IPv6, then try again.\", \"warn\");\n      elProcessBtn.disabled = true;\n      elCounts.textContent = \"\";\n      renderTable();\n      return;\n    }\n\n    setStatus(`Found ${extracted.length} unique IP(s). Click “Process” to enrich.`, \"muted\");\n    elCounts.textContent = `0/${extracted.length}`;\n    elProcessBtn.disabled = false;\n    renderTable();\n  });\n\n  elProcessBtn.addEventListener(\"click\", async () => {\n    if (!extracted.length) return;\n\n    stopRequested = false;\n    elStopBtn.disabled = false;\n    elProcessBtn.disabled = true;\n    elExtractBtn.disabled = true;\n\n    setStatus(\"Starting enrichment…\", \"muted\");\n    elProgress.value = 0;\n\n    const conc = Math.max(1, Math.min(50, parseInt(elConcurrency.value, 10) || 8));\n\n    await runWithConcurrency(\n      extracted,\n      async (ip) => {\n        if (stopRequested) return;\n        try {\n          const data = await fetchInternetDB(ip);\n          resultsByIp.set(ip, data);\n        } catch (e) {\n          resultsByIp.set(ip, { _error: String(e?.message || e) });\n        }\n      },\n      conc\n    );\n\n    elStopBtn.disabled = true;\n    elExtractBtn.disabled = false;\n    elProcessBtn.disabled = false;\n\n    if (stopRequested) {\n      setStatus(\"Stopped. Partial results shown.\", \"warn\");\n    } else {\n      setStatus(\"Done. Results are ready.\", \"ok\");\n      elProgress.value = 100;\n    }\n\n    renderTable();\n  });\n\n  elStopBtn.addEventListener(\"click\", () => {\n    stopRequested = true;\n    elStopBtn.disabled = true;\n    setStatus(\"Stopping… (finishing in-flight requests)\", \"warn\");\n  });\n\n  elMode.addEventListener(\"change\", () => { currentPage = 1; renderTable(); });\n  elSearch.addEventListener(\"input\", () => { currentPage = 1; renderTable(); });\n  elPageSize.addEventListener(\"change\", () => { currentPage = 1; renderTable(); });\n\n  elPrevBtn.addEventListener(\"click\", () => { currentPage = Math.max(1, currentPage - 1); renderTable(); });\n  elNextBtn.addEventListener(\"click\", () => { currentPage = currentPage + 1; renderTable(); });\n\n  elExportJsonBtn.addEventListener(\"click\", () => {\n    const exportObj = {\n      pivotMode: elMode.value,\n      extractedCount: extracted.length,\n      resultsByIp: Object.fromEntries(resultsByIp.entries()),\n      rows: buildRows(elMode.value)\n    };\n    downloadBlob(\"internetdb_results.json\", \"application/json;charset=utf-8\", JSON.stringify(exportObj, null, 2));\n  });\n\n  elExportCsvBtn.addEventListener(\"click\", () => {\n    const rows = buildRows(elMode.value);\n    downloadBlob(\"internetdb_results.csv\", \"text/csv;charset=utf-8\", toCSV(rows));\n  });\n\n  // Initial render\n  renderTable();\n})();\n</script>\n\n<!--\nIf CORS blocks fetch() to https://internetdb.shodan.io, use a tiny proxy and set API_BASE accordingly.\n\nExample Node/Express proxy (proxy.js):\n\nimport express from \"express\";\nconst app = express();\napp.get(\"/internetdb/:ip\", async (req, res) => {\n  const upstream = await fetch(`https://internetdb.shodan.io/${encodeURIComponent(req.params.ip)}`);\n  const body = await upstream.text();\n  res.set(\"Access-Control-Allow-Origin\", \"*\");\n  res.status(upstream.status).type(upstream.headers.get(\"content-type\") || \"application/json\").send(body);\n});\napp.listen(8787);\n-->\n</body>\n</html>\n"
  }
]