Full Code of mr-r3b00t/crime-mapper for AI

main 8be3002d0469 cached
17 files
1.3 MB
341.9k tokens
2 symbols
1 requests
Download .txt
Showing preview only (1,317K chars total). Download the full file or copy to clipboard to get everything.
Repository: mr-r3b00t/crime-mapper
Branch: main
Commit: 8be3002d0469
Files: 17
Total size: 1.3 MB

Directory structure:
gitextract_78spr4xa/

├── README.md
├── analysis.md
├── bugs.md
├── compare_strings.html
├── cors_proxy_server.js
├── crimemapper.html
├── email_time_delta.html
├── example-pwndefend.json
├── experimental_mapper.html
├── functions.md
├── header_analysis.html
├── ipinfo_to_csv.html
├── macos_cors_proxy_install.sh
├── network_graph-4.json
├── network_graph-8.json
├── network_graph-demo-ncsc.json
└── nrich.html

================================================
FILE CONTENTS
================================================

================================================
FILE: README.md
================================================
# Enabling cyber investigations from a browser
**A prototype tool for mapping cyber crime**  

Created by **@UK_Daniel_Card**  

The 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!)

IF 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!)

🔗 [Experimental Version](https://mr-r3b00t.github.io/crime-mapper/experimental_mapper.html)

🔗 [IPInfo Enrichment to CSV](https://mr-r3b00t.github.io/crime-mapper/ipinfo_to_csv.html) 

🔗 [Shodan Nrich to CSV](https://mr-r3b00t.github.io/crime-mapper/nrich.html) 

✉️ [Email Analysis Tool - Experimental](https://mr-r3b00t.github.io/crime-mapper/header_analysis.html)

✉️ [Email Time Detla Analysis Tool - Experimental](https://mr-r3b00t.github.io/crime-mapper/email_time_delta.html)

🔗 [Live Demo](https://mr-r3b00t.github.io/crime-mapper/crimemapper.html)  
 

### Features  
- Import and Export to JSON
- FREE Enrichments with GOOGLE, SHODAN INTERNETDB, HUDSON ROCK (more to come!)
- Runs locally or via GitHub Pages
- Includes a POC local CORS proxy - you need to run this using node and run the html file locally as well!
- Powered by an external JS library and third-party API keys (*BYOK - Bring Your Own Keys*)
- 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.

### Built With  
- GROK3  
- ChatGPT  

⚠️ **Status:** Very Alpha - use at your own risk!  

### Support the Project  
☕ [Buy Me a Coffee](https://buymeacoffee.com/mrr3b00t)  

© Xservus Limited  

---

## Explore More Tools  
- [Gephi](https://gephi.org/features/) - Advanced graph visualization  
- [Graphviz](https://graphviz.org) - Graph visualization software  


================================================
FILE: analysis.md
================================================
# Analysis of experimental_mapper.html

## Overview

This 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.

## Assessment

### 1. Structure and Organization
- **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.
- **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.
- **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.

### 2. Maintainability
- **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.
- **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.
- **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.

### 3. Performance
- **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.
- **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.
- **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.

### 4. Scalability
- **Current State**: As a single file, `experimental_mapper.html` handles all aspects of the application, from UI to data processing.
- **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.
- **Assessment**: The current state is not scalable. Adding new features or integrating with additional APIs would further complicate the file, making it unwieldy.

### 5. Testability
- **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.
- **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.
- **Assessment**: Testability is very low. Without separation, it's nearly impossible to write automated tests for individual components or functions.

### 6. Adherence to Standards
- **Current State**: The file uses HTML, CSS, and JavaScript but does not follow modern standards like semantic HTML or modular JavaScript (ES6 modules).
- **Good Practices**: Use semantic HTML5 for better accessibility and SEO, CSS preprocessors or methodologies (like BEM) for styling, and ES6+ features for JavaScript.
- **Assessment**: The adherence to modern web standards is minimal. Updating to use semantic elements and modular JavaScript would improve the codebase significantly.

### 7. Error Handling and Debugging
- **Current State**: There is no centralized error handling or logging mechanism apparent in the description of the file.
- **Good Practices**: Implement centralized error handling and logging to track issues and provide meaningful feedback to developers and users.
- **Assessment**: Error handling and debugging capabilities are likely inadequate, making it harder to diagnose issues in production or development.

## Recommendations

1. **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.
2. **Modularization**: Use ES6 modules to split JavaScript logic into manageable pieces. Create components for UI elements to improve reusability and maintainability.
3. **Performance Optimization**: Implement batch updates for DOM manipulations, use external stylesheets, and consider debouncing for event handlers to reduce performance bottlenecks.
4. **Testing Framework**: Introduce a testing framework like QUnit to enable unit and integration testing. Structure code to allow mocking of dependencies.
5. **Modern Standards**: Update HTML to use semantic elements, organize CSS with a methodology like BEM, and leverage modern JavaScript features.
6. **Error Handling**: Add a centralized error handling mechanism to log errors and provide user feedback, improving debugging and user experience.
7. **Documentation**: Document the codebase structure, functionality, and usage to aid future development and onboarding.

## Conclusion

The 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. 

================================================
FILE: bugs.md
================================================
# Bug Tracking for Experimental Mapper Application

## Overview

This 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.

## Bugs

### Bug 1: UI Not Visible on Loading index.html
- **Date Reported**: [Current Date]
- **Status**: Open
- **Description**: When loading `index.html`, the UI frame is visible, but there are no contents displayed within the frame.
- **Steps to Reproduce**:
  1. Open `index.html` in a browser (preferably through a local web server to handle CORS issues).
  2. Observe that the basic structure or frame of the application loads, but no UI components (like controls, network visualization, etc.) are visible.
- **Possible Causes**:
  - JavaScript modules might not be loading due to CORS restrictions if not served through a local server.
  - Initialization of UI components in `main.js` or related scripts might be failing.
  - Missing or incorrect DOM elements required by the application components.
- **Root Cause Analysis**:
  - After reviewing the console output, all initialization steps in `main.js` are completing successfully, including importing modules, initializing error handler, configuration, model, and controller.
  - UI components are being initialized as 'placeholders' according to logs (e.g., 'TopBarComponent initialized'), but no content is rendered into the DOM.
  - 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`.
  - 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.
- **Recommended Actions**:
  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.
  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.
  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.
  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.
  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`).
- **Notes**:
  - 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.
  - Check browser console for any JavaScript errors that might indicate issues with module loading or initialization.
  - Console output confirms initialization steps complete, but UI rendering does not occur, pointing to incomplete component rendering logic.
- **Assigned To**: [To be assigned]
- **Priority**: High 

================================================
FILE: compare_strings.html
================================================
<script type="text/javascript">
        var gk_isXlsx = false;
        var gk_xlsxFileLookup = {};
        var gk_fileData = {};
        function loadFileData(filename) {
        if (gk_isXlsx && gk_xlsxFileLookup[filename]) {
            try {
                var workbook = XLSX.read(gk_fileData[filename], { type: 'base64' });
                var firstSheetName = workbook.SheetNames[0];
                var worksheet = workbook.Sheets[firstSheetName];

                // Convert sheet to JSON to filter blank rows
                var jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, blankrows: false, defval: '' });
                // Filter out blank rows (rows where all cells are empty, null, or undefined)
                var filteredData = jsonData.filter(row =>
                    row.some(cell => cell !== '' && cell !== null && cell !== undefined)
                );

                // Convert filtered JSON back to CSV
                var csv = XLSX.utils.aoa_to_sheet(filteredData); // Create a new sheet from filtered array of arrays
                csv = XLSX.utils.sheet_to_csv(csv, { header: 1 });
                return csv;
            } catch (e) {
                console.error(e);
                return "";
            }
        }
        return gk_fileData[filename] || "";
        }
        </script><!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Input Comparison Tool</title>
    <style>
        body {
            font-family: 'Inter', sans-serif;
            background-color: #f3f4f6;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            margin: 0;
            color: #1f2937;
        }
        .container {
            background: white;
            padding: 2rem;
            border-radius: 0.5rem;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
            width: 100%;
            max-width: 400px;
        }
        h1 {
            font-size: 1.5rem;
            font-weight: 600;
            margin-bottom: 1.5rem;
            text-align: center;
        }
        .input-group {
            margin-bottom: 1rem;
        }
        label {
            display: block;
            font-size: 0.875rem;
            font-weight: 500;
            margin-bottom: 0.5rem;
        }
        input {
            width: 100%;
            padding: 0.75rem;
            border: 1px solid #d1d5db;
            border-radius: 0.375rem;
            font-size: 1rem;
            transition: border-color 0.2s;
        }
        input:focus {
            outline: none;
            border-color: #3b82f6;
            box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
        }
        button {
            width: 100%;
            padding: 0.75rem;
            background-color: #3b82f6;
            color: white;
            border: none;
            border-radius: 0.375rem;
            font-size: 1rem;
            font-weight: 500;
            cursor: pointer;
            transition: background-color 0.2s;
        }
        button:hover {
            background-color: #2563eb;
        }
        #result {
            margin-top: 1.5rem;
            text-align: center;
            font-size: 1rem;
            font-weight: 500;
        }
        .same {
            color: #16a34a;
        }
        .different {
            color: #dc2626;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Compare Inputs</h1>
        <div class="input-group">
            <label for="input1">Input 1</label>
            <input type="text" id="input1" placeholder="Enter first input">
        </div>
        <div class="input-group">
            <label for="input2">Input 2</label>
            <input type="text" id="input2" placeholder="Enter second input">
        </div>
        <button onclick="compareInputs()">Compare</button>
        <div id="result"></div>
    </div>

    <script>
        function compareInputs() {
            const input1 = document.getElementById('input1').value;
            const input2 = document.getElementById('input2').value;
            const resultDiv = document.getElementById('result');

            if (input1 === input2) {
                resultDiv.textContent = 'Inputs are the same!';
                resultDiv.className = 'same';
            } else {
                resultDiv.textContent = 'Inputs are different!';
                resultDiv.className = 'different';
            }
        }
    </script>
</body>
</html>


================================================
FILE: cors_proxy_server.js
================================================
const express = require('express');
const axios = require('axios');
const https = require('https');
const url = require('url');
const cors = require('cors');
const querystring = require('querystring');

const app = express();
const port = 3000;

const allowList = [
    'api.shodan.io',
    'ipinfo.io',
    'safebrowsing.googleapis.com',
    'dns.google.com',
    'api.hudsonrock.com',
    'cavalier.hudsonrock.com',
    'internetdb.shodan.io',
    'api.greynoise.io',
    'urlscan.io',
    'api.securitytrails.com',
    'urlhaus-api.abuse.ch',
    'api.any.run',
    'dns.google'
];

app.use(express.urlencoded({ extended: true }));
app.use(express.json());

// Use default query parser but log for debugging
app.set('query parser', (str) => {
    const parsed = querystring.parse(str);
    console.log(`[Query Parser] Input: ${str}, Parsed: ${JSON.stringify(parsed)}`);
    return parsed;
});

app.use(cors({
    origin: (origin, callback) => {
        const allowed = !origin || origin === 'null' || origin === 'http://localhost:3000';
        console.log(`[CORS] Origin: ${origin} - ${allowed ? 'Allowed' : 'Rejected'}`);
        if (allowed) {
            callback(null, true);
        } else {
            callback(new Error('Origin not allowed by CORS policy'));
        }
    },
    optionsSuccessStatus: 200
}));

// Status endpoint
app.get('/status', (req, res) => {
    console.log('[Status] Request received');
    res.json({
        status: 'running',
        version: 'fixed-2025-04-12-v6',
        port: port,
        allowList: allowList,
        timestamp: new Date().toISOString()
    });
});

function validateTargetUrl(req, res, next) {
    console.log('[Validate] Full Request Details:', {
        method: req.method,
        url: req.originalUrl,
        headers: req.headers,
        query: req.query,
        body: req.body,
        timestamp: new Date().toISOString()
    });

    // Accurate raw query string
    const rawQuery = req.originalUrl.includes('?') ? req.originalUrl.split('?')[1] : 'none';
    console.log(`[Validate] Raw query string: ${rawQuery}`);

    // Reconstruct full URL from req.query
    let targetUrl = req.query.url;
    if (!targetUrl) {
        console.log('[Validate] Rejected: Missing url parameter');
        return res.status(400).json({ error: 'Missing url parameter' });
    }

    // Append additional query parameters (e.g., type=MX)
    const additionalParams = Object.keys(req.query)
        .filter(key => key !== 'url')
        .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(req.query[key])}`)
        .join('&');
    if (additionalParams) {
        targetUrl += (targetUrl.includes('?') ? '&' : '?') + additionalParams;
    }

    console.log(`[Validate] Reconstructed target URL: ${targetUrl}`);

    let parsedUrl;
    try {
        parsedUrl = new url.URL(targetUrl);
    } catch (error) {
        console.log(`[Validate] Rejected: Invalid URL format - ${targetUrl} - Error: ${error.message}`);
        return res.status(400).json({ error: 'Invalid URL format' });
    }

    const hostname = parsedUrl.hostname;
    if (!allowList.includes(hostname)) {
        console.log(`[Validate] Rejected: Domain not allowed - ${hostname}`);
        return res.status(403).json({ error: 'Target domain not in allow list' });
    }

    req.targetUrl = targetUrl;
    console.log(`[Validate] Validated: ${targetUrl} (${req.method})`);
    next();
}

async function proxyRequest(req, res) {
    console.log(`[Proxy] Processing request for: ${req.targetUrl}`);
    try {
        const axiosConfig = {
            method: req.method.toLowerCase(),
            url: req.targetUrl,
            headers: {
                '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',
                'Content-Type': req.headers['content-type'] || 'application/x-www-form-urlencoded',
                'Accept': req.headers['accept'] || 'application/json'
            },
            httpsAgent: new https.Agent({
                rejectUnauthorized: false
            }),
            responseType: 'stream'
        };

        if (req.method === 'POST' && req.body) {
            axiosConfig.data = req.body;
            console.log(`[Proxy] POST data: ${JSON.stringify(req.body)}`);
        }

        ['api-key', 'key', 'apikey', 'Auth-Key'].forEach(header => {
            if (req.headers[header.toLowerCase()]) {
                axiosConfig.headers[header] = req.headers[header.toLowerCase()];
                console.log(`[Proxy] Forwarding header: ${header}`);
            }
        });

        console.log(`[Proxy] Forwarding request to: ${req.targetUrl} with config:`, {
            method: axiosConfig.method,
            url: axiosConfig.url,
            headers: axiosConfig.headers
        });

        const response = await axios(axiosConfig);

        Object.keys(response.headers).forEach(key => {
            res.setHeader(key, response.headers[key]);
        });

        response.data.pipe(res);

        console.log(`[Proxy] Response from ${req.targetUrl}:`, {
            status: response.status,
            statusText: response.statusText,
            headers: response.headers,
            timestamp: new Date().toISOString()
        });
    } catch (error) {
        console.error(`[Proxy] Error for ${req.targetUrl}:`, error);
        if (error.response) {
            console.log(`[Proxy] Error Response from ${req.targetUrl}:`, {
                status: error.response.status,
                statusText: error.response.statusText,
                headers: error.response.headers,
                timestamp: new Date().toISOString()
            });
            error.response.data.pipe(res);
        } else {
            console.error(`[Proxy] Non-HTTP error: ${error.message}`);
            res.status(500).json({
                error: 'Proxy error',
                message: error.message,
                stack: error.stack,
                request: {
                    url: req.targetUrl,
                    method: req.method
                }
            });
        }
    }
}

app.all('/proxy', validateTargetUrl, proxyRequest);

app.use((err, req, res, next) => {
    console.error(`[Server] Error:`, {
        message: err.message,
        stack: err.stack,
        request: {
            url: req.originalUrl,
            method: req.method,
            headers: req.headers,
            body: req.body
        },
        timestamp: new Date().toISOString()
    });
    res.status(500).json({
        error: 'Internal server error',
        message: err.message,
        stack: err.stack,
        request: {
            url: req.originalUrl,
            method: req.method,
            headers: req.headers,
            body: req.body
        }
    });
});

app.listen(port, () => {
    console.log(`[Server] CORS Proxy Server running on http://localhost:${port}`);
    console.log('[Server] Usage:');
    console.log('[Server]   GET:  http://localhost:3000/proxy?url=<target-url>');
    console.log('[Server] Allowed domains:', allowList.join(', '));
});


================================================
FILE: crimemapper.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Baddie Mapper - Experimental</title>
    <style>

/* Target mx-node class for vis.js nodes */
.vis-network .vis-node.mx-node {
    background-color: #60a5fa; /* Blue */
    border-color: #1e88e5; /* Slightly darker blue border for contrast */
}

.modal {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.5);
    z-index: 2000;
    overflow: auto;
}

.modal-content {
    position: relative;
    margin: 50px auto;
    padding: 20px;
    width: 80%;
    max-width: 800px;
    max-height: 60vh; /* Shorter height: 60% of viewport */
    overflow-y: auto; /* Vertical scroll for content */
    border-radius: 8px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
    background: var(--modal-bg, #fff);
    color: var(--modal-color, #1f2a44);
    border: 1px solid var(--modal-border, #d1d5db);
    display: flex;
    flex-direction: column; /* Stack content vertically */
}

.dark .modal-content {
    background: #1f2a44;
    color: #e2e8f0;
    border-color: #4b5563;
}

.close-modal {
    position: absolute;
    top: 10px;
    right: 15px;
    font-size: 24px;
    cursor: pointer;
    color: inherit;
}

.close-modal:hover {
    color: #ef4444;
}

#riskTableContainer {
    overflow-x: auto; /* Horizontal scroll for wide tables */
    flex-grow: 1; /* Allow table to take available space */
}

table {
    width: 100%;
    border-collapse: collapse;
    margin-bottom: 20px;
}

th, td {
    padding: 10px;
    text-align: left;
    border-bottom: 1px solid var(--table-border, #d1d5db);
    word-wrap: break-word;
    max-width: 250px;
}

.dark th, .dark td {
    border-bottom-color: #4b5563;
}

th {
    background: var(--th-bg, #f1f5f9);
}

.dark th {
    background: #2d3748;
}

.print-button {
    padding: 10px 20px;
    background: #22c55e;
    color: #fff;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    margin-top: 10px; /* Space above button */
    align-self: center; /* Center button horizontally */
}

.print-button:hover {
    background: #16a34a;
}

/* CSS Variables */
:root {
    --transition: 0.3s;
    --shadow-light: 0 2px 10px rgba(0, 0, 0, 0.1);
    --shadow-dark: 0 2px 10px rgba(0, 0, 0, 0.3);
    --border-light: #d1d5db;
    --border-dark: #4b5563;
    --top-bar-height: 40px;
}

/* Base Styles */
body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    margin: 0;
    padding: 0;
    display: flex;
    height: calc(100vh - var(--top-bar-height));
    font-size: 12px;
    transition: background-color var(--transition), color var(--transition);
    margin-top: var(--top-bar-height);
}

body.light-mode {
    background-color: #f0f2f5;
    color: #1f2a44;
}

body.dark-mode {
    background-color: #1e293b;
    color: #e2e8f0;
}

/* Top Bar */
#top-bar {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: var(--top-bar-height);
    background-color: #2d3748;
    color: #e2e8f0;
    display: flex;
    justify-content: space-between; /* Explicitly separate version and links */
    align-items: center;
    padding: 0 20px;
    z-index: 1000;
    box-shadow: var(--shadow-dark);
    box-sizing: border-box;
}

.light-mode #top-bar {
    background-color: #f0f2f5;
    color: #1f2a44;
    box-shadow: var(--shadow-light);
}

#version {
    font-size: 14px;
    font-weight: 500;
    flex-shrink: 0; /* Prevent shrinking */
    margin-right: 20px; /* Add space to separate from links */
}

#top-bar-links {
    display: flex;
    align-items: center;
    gap: 30px; /* Increased spacing between links */
    /* Fallback absolute positioning */
    position: absolute;
    right: 20px;
    top: 50%;
    transform: translateY(-50%);
    /* Diagnostic border to confirm application */
    border: 1px solid red !important;
}

#cyberchef-link,
#github-link {
    font-size: 14px;
    color: #60a5fa;
    text-decoration: none;
    transition: color var(--transition);
}

#cyberchef-link:hover,
#github-link:hover {
    color: #3b82f6;
}

.light-mode #cyberchef-link,
.light-mode #github-link {
    color: #3b82f6;
}

.light-mode #cyberchef-link:hover,
.light-mode #github-link:hover {
    color: #2563eb;
}
/* Controls Panel */
#controls {
    position: fixed;
    top: var(--top-bar-height);
    left: 0;
    height: calc(100vh - var(--top-bar-height));
    z-index: 1000;
    display: flex;
    flex-direction: column;
    overflow-y: auto;
    transition: width var(--transition);
    background-color: #fff;
}

.light-mode #controls {
    background-color: #fff;
    box-shadow: var(--shadow-light);
}

.dark-mode #controls {
    background-color: #2d3748;
    box-shadow: var(--shadow-dark);
    color: #e2e8f0;
}

#controls.collapsed {
    width: 50px;
}

#controls:not(.collapsed) {
    width: 350px;
}

#controls-header {
    padding: 10px;
    display: flex;
    flex-direction: column;
    gap: 10px;
}

#controls-footer {
    text-align: center;
    padding: 10px;
    font-size: 10px;
    margin-top: auto;
    transition: color var(--transition);
}

.light-mode #controls-footer {
    color: #6b7280;
}

.dark-mode #controls-footer {
    color: #94a3b8;
}

/* Menu Toggle Button */
#menu-toggle {
    padding: 6px 12px;
    background-color: #3b82f6;
    color: white;
    border: none;
    border-radius: 4px;
    font-size: 12px;
    cursor: pointer;
    transition: background-color var(--transition);
    width: 100%;
    box-sizing: border-box;
}

#controls.collapsed #menu-toggle {
    width: 100%;
    display: block;
}

.light-mode #menu-toggle {
    background-color: #3b82f6;
}

#menu-toggle:hover {
    background-color: #2563eb;
}

.dark-mode #menu-toggle:hover {
    background-color: #3b82f6;
}

/* Top Buttons */
#mode-toggle,
#pause-toggle,
#reset-layout,
#summary-button {
    padding: 6px 12px;
    background-color: #6b7280;
    color: white;
    border: none;
    border-radius: 4px;
    font-size: 12px;
    cursor: pointer;
    transition: background-color var(--transition);
    width: 100%;
    box-sizing: border-box;
}

#mode-toggle:hover,
#pause-toggle:hover,
#reset-layout:hover,
#summary-button:hover {
    background-color: #4b5563;
}

.dark-mode #mode-toggle,
.dark-mode #pause-toggle,
.dark-mode #reset-layout,
.dark-mode #summary-button {
    background-color: #9ca3af;
}

#pause-toggle.paused {
    background-color: #ef4444;
}

#controls.collapsed #mode-toggle,
#controls.collapsed #pause-toggle,
#controls.collapsed #reset-layout,
#controls.collapsed #summary-button,
#controls.collapsed .tab-buttons,
#controls.collapsed .tab-content,
#controls.collapsed #controls-footer {
    display: none;
}

/* Tab Navigation */
.tab-buttons {
    display: flex;
    flex-wrap: wrap;
    border-bottom: 1px solid var(--border-light);
    transition: border-color var(--transition);
    padding: 10px 0;
}

.dark-mode .tab-buttons {
    border-bottom: 1px solid var(--border-dark);
}

.tab-button {
    flex: 1 0 14.28%;
    padding: 10px;
    text-align: center;
    border: none;
    cursor: pointer;
    transition: background-color var(--transition), color var(--transition);
    font-size: 10px;
}

.light-mode .tab-button {
    background-color: #f9fafb;
    color: #1f2a44;
}

.dark-mode .tab-button {
    background-color: #374151;
    color: #e2e8f0;
}

.tab-button.active {
    font-weight: bold;
}

.light-mode .tab-button.active {
    background-color: #fff;
    border-bottom: 2px solid #3b82f6;
}

.dark-mode .tab-button.active {
    background-color: #2d3748;
    border-bottom: 2px solid #60a5fa;
}

.light-mode .tab-button:hover:not(.active) {
    background-color: #e5e7eb;
}

.dark-mode .tab-button:hover:not(.active) {
    background-color: #4b5563;
}

/* Tab Content */
.tab-content {
    padding: 15px;
    display: none;
    flex-grow: 1;
}

.tab-content.active {
    display: block;
}

.input-group {
    margin: 15px 0;
    padding: 10px;
    border-radius: 6px;
    transition: background-color var(--transition);
}

.light-mode .input-group {
    background-color: #f9fafb;
}

.dark-mode .input-group {
    background-color: #374151;
}

.input-group h3 {
    margin: 0 0 8px 0;
    font-size: 14px;
}

.light-mode .input-group h3 {
    color: #1f2a44;
}

.dark-mode .input-group h3 {
    color: #e2e8f0;
}

/* Inputs and Buttons */
input,
select,
textarea {
    margin: 4px 0;
    padding: 6px 10px;
    border-radius: 4px;
    font-size: 12px;
    transition: border-color var(--transition), background-color var(--transition), color var(--transition);
    width: 100%;
    box-sizing: border-box;
}

.light-mode input,
.light-mode select,
.light-mode textarea {
    border: 1px solid var(--border-light);
    background-color: #fff;
    color: #1f2a44;
}

.dark-mode input,
.dark-mode select,
.dark-mode textarea {
    border: 1px solid var(--border-dark);
    background-color: #4b5563;
    color: #e2e8f0;
}

textarea {
    height: 100px;
    resize: vertical;
}

button {
    padding: 6px 12px;
    border: none;
    border-radius: 4px;
    font-size: 12px;
    cursor: pointer;
    transition: background-color var(--transition), color var(--transition);
    width: 100%;
    margin: 4px 0;
}

.light-mode button {
    background-color: #3b82f6;
    color: white;
}

.dark-mode button {
    background-color: #60a5fa;
    color: #1e293b;
}

/* Network Container */
#myNetwork {
    flex-grow: 1;
    height: calc(100vh - var(--top-bar-height));
    border-radius: 0 8px 8px 0;
    box-shadow: var(--shadow-light);
    transition: margin-left var(--transition), margin-right var(--transition);
    position: relative;
}

.light-mode #myNetwork {
    background-color: #fff;
}

.dark-mode #myNetwork {
    background-color: #334155;
    box-shadow: var(--shadow-dark);
}

#controls:not(.collapsed) ~ #myNetwork {
    margin-left: 300px;
}

#controls.collapsed ~ #myNetwork {
    margin-left: 50px;
}

/* Properties Panel */
#properties-panel {
    position: fixed;
    top: var(--top-bar-height);
    right: -350px; /* Ensure it's fully off-screen */
    width: 350px;
    height: calc(100vh - var(--top-bar-height));
    background-color: #fff;
    box-shadow: -2px 0 10px rgba(0, 0, 0, 0.2);
    z-index: 2000;
    padding: 20px;
    overflow-y: auto;
    transition: right var(--transition);
    display: none; /* Hidden by default */
    box-sizing: border-box;
}

#properties-panel.active {
    right: 0;
    display: block; /* Show when active */
}

.dark-mode #properties-panel {
    background-color: #2d3748;
    box-shadow: -2px 0 10px rgba(0, 0, 0, 0.4);
    color: #e2e8f0;
}


#properties-panel .close-button {
    position: absolute;
    top: 10px;
    right: 10px;
    background: none;
    border: none;
    font-size: 18px;
    cursor: pointer;
    color: #1f2a44;
    transition: color var(--transition);
}

.dark-mode #properties-panel .close-button {
    color: #e2e8f0;
}

#properties-panel.active ~ #myNetwork {
    margin-right: 300px;
}

#properties-panel.active ~ #controls:not(.collapsed) ~ #myNetwork {
    margin-left: 300px;
    margin-right: 300px;
}

#properties-panel.active ~ #controls.collapsed ~ #myNetwork {
    margin-left: 50px;
    margin-right: 300px;
}

/* Notes Modal */
#notes-modal {
    display: none;
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background-color: #fff;
    padding: 20px;
    border-radius: 8px;
    box-shadow: var(--shadow-light);
    z-index: 2000;
    width: 500px;
    max-width: 90vw;
    max-height: 80vh;
    overflow-y: auto;
    box-sizing: border-box;
}

.dark-mode #notes-modal {
    background-color: #2d3748;
    box-shadow: var(--shadow-dark);
    color: #e2e8f0;
}

#modal-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vh;
    background: rgba(0, 0, 0, 0.5);
    z-index: 1999;
}

#notes-modal h3 {
    margin: 0 0 15px 0;
    font-size: 16px;
}

#notes-textarea {
    width: 100%;
    height: 300px;
    padding: 10px;
    border-radius: 4px;
    border: 1px solid var(--border-light);
    background-color: #fff;
    color: #1f2a44;
    font-size: 12px;
    resize: vertical;
    box-sizing: border-box;
    overflow-y: auto;
}

.dark-mode #notes-textarea {
    border: 1px solid var(--border-dark);
    background-color: #4b5563;
    color: #e2e8f0;
}

#notes-modal .button-container {
    margin-top: 15px;
    display: flex;
    justify-content: flex-end;
    gap: 10px;
}

#notes-modal button {
    padding: 6px 12px;
    border-radius: 4px;
    border: none;
    cursor: pointer;
    font-size: 12px;
    transition: background-color var(--transition);
}

#notes-save {
    background-color: #3b82f6;
    color: white;
}

#notes-cancel {
    background-color: #6b7280;
    color: white;
}

#notes-save:hover {
    background-color: #2563eb;
}

#notes-cancel:hover {
    background-color: #4b5563;
}

.dark-mode #notes-save {
    background-color: #60a5fa;
    color: #1e293b;
}

.dark-mode #notes-cancel {
    background-color: #9ca3af;
}

/* Summary Modal */
#summary-modal {
    display: none;
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background-color: #fff;
    padding: 20px;
    border-radius: 8px;
    box-shadow: var(--shadow-light);
    z-index: 2000;
    max-width: 500px;
    width: 90%;
    max-height: 80vh;
    overflow-y: auto;
}

.toastify {
    top: calc(var(--top-bar-height) + 10px) !important;
    z-index: 3000 !important;
}

.dark-mode #summary-modal {
    background-color: #2d3748;
    box-shadow: var(--shadow-dark);
    color: #e2e8f0;
}

#summary-modal table {
    width: 100%;
    border-collapse: collapse;
    margin-top: 10px;
}

#summary-modal th,
#summary-modal td {
    padding: 8px;
    text-align: left;
    border-bottom: 1px solid var(--border-light);
}

.dark-mode #summary-modal th,
.dark-mode #summary-modal td {
    border-bottom: 1px solid var(--border-dark);
}

#summary-modal th {
    background-color: #f9fafb;
}

.dark-mode #summary-modal th {
    background-color: #374151;
}

#summary-modal .close-button {
    float: right;
    background: none;
    border: none;
    font-size: 16px;
    cursor: pointer;
    color: #1f2a44;
}

.dark-mode #summary-modal .close-button {
    color: #e2e8f0;
}

/* Progress Bar */
#progress-bar {
    position: fixed;
    top: var(--top-bar-height);
    left: 0;
    width: 100%;
    padding: 10px;
    text-align: center;
    color: white;
    font-size: 14px;
    font-weight: bold;
    z-index: 2000;
    transition: opacity 0.5s ease-in-out;
}

.progress-active {
    background-color: #dc2626;
}

.progress-complete {
    background-color: #22c55e;
}

.progress-hidden {
    opacity: 0;
    pointer-events: none;
}

/* Context Menus */
#contextMenu,
#edgeContextMenu {
    position: absolute;
    background-color: #fff;
    border: 1px solid #ccc;
    box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
    z-index: 2000;
    padding: 5px 0;
}

#contextMenu button,
#edgeContextMenu button {
    display: block;
    width: 100%;
    text-align: left;
    padding: 5px 10px;
    background: none;
    border: none;
    cursor: pointer;
    color: #1f2a44;
}

#contextMenu button:hover,
#edgeContextMenu button:hover {
    background-color: #f0f0f0;
}

.dark-mode #contextMenu,
.dark-mode #edgeContextMenu {
    background-color: #2d3748;
    border: 1px solid var(--border-dark);
}

.dark-mode #contextMenu button,
.dark-mode #edgeContextMenu button {
    color: #e2e8f0;
}

.dark-mode #contextMenu button:hover,
.dark-mode #edgeContextMenu button:hover {
    background-color: #4b5563;
}

/* Network Visualization */
#myNetwork .vis-network canvas {
    overflow: visible; /* Change from hidden to visible */
}

.vis-network .vis-label {
    display: flex;
    justify-content: center;
    align-items: center;
    text-align: center;
    padding: 0;
    margin: 0;
    overflow: hidden;
    white-space: normal;
    max-width: 100%;
}

.vis-network .vis-node {
    min-width: 40px;
    min-height: 40px;
    display: flex;
    justify-content: center;
    align-items: center;
}

/* Password Toggle */
.password-container {
    display: flex;
    flex-direction: column; /* Stack children vertically */
    width: 100%;
    margin-bottom: 8px;
    gap: 4px; /* Add space between input and button */
}

/* Inputs - Remove padding-right */
.password-container input {
    padding: 6px 10px; /* Adjusted from padding-right: 60px to standard padding */
}

/* Password Toggle */
.toggle-password {
    /* Remove absolute positioning */
    padding: 2px 8px;
    background-color: #6b7280;
    color: white;
    border: none;
    border-radius: 4px;
    font-size: 10px;
    cursor: pointer;
    width: auto; /* Let it size naturally */
    align-self: flex-start; /* Align to the left */
}

.light-mode .toggle-password {
    background-color: #6b7280;
}

.dark-mode .toggle-password {
    background-color: #9ca3af;
}

.toggle-password:hover {
    background-color: #4b5563;
}
/* Stop Task Button */
#stop-task {
    padding: 6px 12px;
    background-color: #dc2626;
    color: white;
    border: none;
    border-radius: 4px;
    font-size: 12px;
    cursor: pointer;
    transition: background-color var(--transition);
    width: auto;
    margin: 0 auto;
    display: inline-block;
}

#stop-task:hover {
    background-color: #b91c1c;
}

#stop-task:disabled {
    background-color: #9ca3af;
    cursor: not-allowed;
}

/* Search Input */
#search-input {
    padding: 6px 10px;
    border-radius: 4px;
    border: 1px solid var(--border-light);
    background-color: #fff;
    color: #1f2a44;
    transition: border-color var(--transition);
}

.dark-mode #search-input {
    border: 1px solid var(--border-dark);
    background-color: #4b5563;
    color: #e2e8f0;
}

#search-input:focus {
    outline: none;
    border-color: #3b82f6;
}

/* Checkbox Labels */
.checkbox-label {
    display: flex;
    align-items: center;
    margin: 4px 0;
    font-size: 12px;
}

.light-mode .checkbox-label {
    color: #1f2a44;
}

.dark-mode .checkbox-label {
    color: #e2e8f0;
}

input[type="checkbox"] {
    margin-right: 8px;
    width: auto;
}

/* Media Queries */
@media (max-width: 768px) {
    #controls:not(.collapsed) {
        width: 100%;
    }
    
    #controls:not(.collapsed) ~ #myNetwork {
        margin-left: 0;
        display: none;
    }
    
    #controls.collapsed ~ #myNetwork {
        margin-left: 50px;
        display: block;
    }
    
    #properties-panel.active ~ #myNetwork {
        margin-right: 300px;
    }
}

#controls.collapsed #manual-save,
#controls.collapsed #buy-me-a-coffee-container,
#controls.collapsed #stop-task-container {
    display: none;
}
</style>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastify-js/1.12.0/toastify.min.css">
    <script src="https://cdn.jsdelivr.net/npm/vis-network@9.1.9/dist/vis-network.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/toastify-js/1.12.0/toastify.min.js"></script>  
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.3/jspdf.plugin.autotable.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
</head>
<body class="dark-mode">
    <div id="top-bar">
        <span id="version">Baddie Mapper Experimental</span>
        <a href="https://github.com/mr-r3b00t/crime-mapper" target="_blank" id="github-link">GitHub Repo</a>
        <a href="https://gchq.github.io/CyberChef" target="_blank" id="cyberchef-link">GCHQ CyberChef</a>
    </div>
    <div id="notes-modal">
        <h3>Edit Node Notes</h3>
        <textarea id="notes-textarea" placeholder="Enter notes here (max 1000 characters)"></textarea>
        <div class="button-container">
            <button id="notes-save" onclick="saveNodeNotes()">Save</button>
            <button id="notes-cancel" onclick="hideNotesModal()">Cancel</button>
        </div>
    </div>
        <div id="progress-bar" class="progress-hidden">Task in progress...</div>
        <!-- Rest of your HTML -->

      

    <div id="controls">
        <button id="menu-toggle" onclick="toggleMenu()" title="Toggle Menu">></button>
        <button id="mode-toggle" onclick="toggleMode()">Switch to Light Mode</button>
        <button id="pause-toggle" onclick="togglePhysics()">Pause Physics</button>
        <button id="reset-layout" onclick="resetLayout()">Reset Layout</button>
        <div class="tab-buttons">
            <button class="tab-button" onclick="showTab('object-management')">Object Management</button>
            <button class="tab-button" onclick="showTab('link-management')">Link Management</button>
            <button class="tab-button" onclick="showTab('import-export')">Import/Export</button>
            <button class="tab-button" onclick="showTab('api-keys')">Config</button>
            <button class="tab-button" onclick="showTab('enrichment')">Enrichment</button>
            <button class="tab-button active" onclick="showTab('import-iocs')">Import IOCs</button>
            <button class="tab-button" onclick="showTab('layouts')">Layouts</button>
            <button class="tab-button" onclick="showTab('search')">Search</button>
        </div>
        <div id="object-management" class="tab-content">
            <div class="input-group">
                <h3>Add Entity</h3>
                <select id="addEntityType">
                    <option value="contact">Contact</option>
                    <option value="ip">IP Address</option>
                    <option value="domain">Domain</option>
                    <option value="organization">Organization</option>
                    <option value="port">Port</option>
                    <option value="wallet">Wallet</option>
                    <option value="bank">Bank Account</option>
                    <option value="technology">Technology</option>
                    <option value="device">Device</option>
                    <option value="malware">Malware</option>
                    <option value="vulnerability">Vulnerability</option>
                    <option value="subnet">Subnet</option> <!-- New option -->
                </select>
                <input type="text" id="addVulnNameInput" placeholder="Vulnerability Name" style="display: none;">
                <input type="text" id="addVulnCVEInput" placeholder="CVE (optional)" style="display: none;">
                <input type="text" id="addVulnUrlInput" placeholder="URL (optional)" style="display: none;">
                <input type="text" id="addNameInput" placeholder="Name">
                <input type="email" id="addEmailInput" placeholder="Email (optional)">
                <input type="text" id="addIpInput" placeholder="IP Address" style="display: none;">
                <input type="text" id="addDomainInput" placeholder="Domain" style="display: none;">
                <input type="text" id="addOrgInput" placeholder="Organization Name" style="display: none;">
                <input type="text" id="addSubnetInput" placeholder="Subnet (e.g., 192.168.1.0/24)" style="display: none;">
                <input type="text" id="addPortNumInput" placeholder="Port Number" style="display: none;">
                <select id="addPortType" style="display: none;">
                    <option value="TCP">TCP</option>
                    <option value="UDP">UDP</option>
                </select>
                <input type="text" id="addWalletAddressInput" placeholder="Wallet Address" style="display: none;">
                <input type="text" id="addAccountNumberInput" placeholder="Account Number" style="display: none;">
                <input type="text" id="addSortCodeInput" placeholder="Sort Code" style="display: none;">
                <input type="text" id="addTechNameInput" placeholder="Technology Name" style="display: none;">
                <input type="text" id="addTechVersionInput" placeholder="Version" style="display: none;">
                <select id="addDeviceCategory" style="display: none;">
                    <option value="Server">Server</option>
                    <option value="PC">PC</option>
                    <option value="Laptop">Laptop</option>
                    <option value="MAC">MAC</option>
                    <option value="SmartPhone">SmartPhone</option>
                    <option value="IOT">IOT</option>
                    <option value="Router">Router</option>
                    <option value="Switch">Switch</option>
                    <option value="Wireless Access Point">Wireless Access Point</option>
                    <option value="Other">Other</option>
                </select>
                <input type="text" id="addDeviceNameInput" placeholder="Device Name" style="display: none;">
                <input type="text" id="addMalwareNameInput" placeholder="Malware Name" style="display: none;">
                <select id="addMalwareType" style="display: none;">
                    <option value="Wiper">Wiper</option>
                    <option value="RAT">RAT</option>
                    <option value="Encryptor">Encryptor</option>
                    <option value="Stealer">Stealer</option>
                    <option value="Other">Other</option>
                </select>
                <button onclick="addNode()">Add Entity</button>
            </div>
            <div class="input-group">
                <h3>Edit Entity</h3>
                <select id="editNodeSelect" onchange="loadNodeForEdit()"></select>
                <select id="editEntityType" disabled>
                    <option value="contact">Contact</option>
                    <option value="ip">IP Address</option>
                    <option value="domain">Domain</option>
                    <option value="organization">Organization</option>
                    <option value="port">Port</option>
                    <option value="wallet">Wallet</option>
                    <option value="bank">Bank Account</option>
                    <option value="technology">Technology</option>
                    <option value="device">Device</option>
                    <option value="malware">Malware</option>
                    <option value="vulnerability">Vulnerability</option>
                </select>
                <select id="editEntityType" disabled>
                    <!-- Existing options -->
                    <option value="subnet">Subnet</option>
                </select>
                <input type="text" id="editSubnetInput" placeholder="Subnet (e.g., 192.168.1.0/24)" style="display: none;">
                <input type="text" id="editVulnNameInput" placeholder="Vulnerability Name" style="display: none;">
                <input type="text" id="editVulnCVEInput" placeholder="CVE (optional)" style="display: none;">
                <input type="text" id="editVulnUrlInput" placeholder="URL (optional)" style="display: none;">
                <input type="text" id="editNameInput" placeholder="Name">
                <input type="email" id="editEmailInput" placeholder="Email (optional)">
                <input type="text" id="editIpInput" placeholder="IP Address" style="display: none;">
                <input type="text" id="editDomainInput" placeholder="Domain" style="display: none;">
                <input type="text" id="editOrgInput" placeholder="Organization Name" style="display: none;">
                <input type="text" id="editPortNumInput" placeholder="Port Number" style="display: none;">
                <select id="editPortType" style="display: none;">
                    <option value="TCP">TCP</option>
                    <option value="UDP">UDP</option>
                </select>
                <input type="text" id="editWalletAddressInput" placeholder="Wallet Address" style="display: none;">
                <input type="text" id="editAccountNumberInput" placeholder="Account Number" style="display: none;">
                <input type="text" id="editSortCodeInput" placeholder="Sort Code" style="display: none;">
                <input type="text" id="editTechNameInput" placeholder="Technology Name" style="display: none;">
                <input type="text" id="editTechVersionInput" placeholder="Version" style="display: none;">
                <select id="editDeviceCategory" style="display: none;">
                    <option value="Server">Server</option>
                    <option value="PC">PC</option>
                    <option value="Laptop">Laptop</option>
                    <option value="MAC">MAC</option>
                    <option value="SmartPhone">SmartPhone</option>
                    <option value="IOT">IOT</option>
                    <option value="Router">Router</option>
                    <option value="Switch">Switch</option>
                    <option value="Wireless Access Point">Wireless Access Point</option>
                    <option value="Other">Other</option>
                </select>
                <input type="text" id="editDeviceNameInput" placeholder="Device Name" style="display: none;">
                <input type="text" id="editMalwareNameInput" placeholder="Malware Name" style="display: none;">
                <select id="editMalwareType" style="display: none;">
                    <option value="Wiper">Wiper</option>
                    <option value="RAT">RAT</option>
                    <option value="Encryptor">Encryptor</option>
                    <option value="Stealer">Stealer</option>
                    <option value="Other">Other</option>
                </select>
                <button onclick="editNode()">Save Changes</button>
            </div>
            <div class="input-group">
                <h3>Remove Entity</h3>
                <select id="removeNode"></select>
                <button onclick="removeNode()">Remove Entity</button>
            </div>
        </div>
        <div id="link-management" class="tab-content">
            <div class="input-group">
                <h3>Create Link</h3>
                <select id="fromNode"></select>
                <select id="toNode"></select>
                <input type="text" id="edgeLabel" placeholder="Link Label">
                <button onclick="addEdge()">Add Link</button>
            </div>
            <div class="input-group">
                <h3>Remove Link</h3>
                <select id="removeEdge"></select>
                <button onclick="removeEdge()">Remove Link</button>
            </div>
        </div>
        <div id="import-export" class="tab-content">
            <div class="input-group">
                <h3>Export/Import</h3>
                <button onclick="exportGraph()">Export to JSON</button>
                <button onclick="exportVisibleGraph()">Export Visible to JSON</button>
                <!-- Removed: <input type="file" id="importFile" accept=".json"> -->
                <button onclick="importGraph()">Import from JSON</button>
                <button onclick="clearGraph()">Clear Graph</button>
                <button id="summary-button" onclick="showGraphSummary()">Graph Summary</button>
                <button onclick="exportToPNG()">Export to PNG</button>
                <button onclick="exportToPDF()">Export to PDF</button>
                <button onclick="exportConfigBackup()">Backup Config</button> <!-- New Button -->
                <button onclick="importConfig()">Import Config</button> <!-- New Button -->
                <button onclick="importNMAP()">Import NMAP</button> <!-- New Button -->
            </div>
        </div>
        <div id="api-keys" class="tab-content">
            <div class="input-group">
                <h3>IPINFO API Key</h3>
                <div class="password-container">
                    <input type="password" id="ipinfoApiKey" placeholder="Enter IPinfo API Key">
                    <button type="button" class="toggle-password" data-target="ipinfoApiKey">Show</button>
                </div>
                <label class="checkbox-label">
                    <input type="checkbox" id="storeIpinfoKey"> Store in local storage
                </label>
                <button onclick="saveIpinfoApiKey()">Save IPinfo API Key</button>
            </div>
            <div class="input-group">
                <h3>Shodan API Key</h3>
                <div class="password-container">
                    <input type="password" id="shodanApiKey" placeholder="Enter Shodan API Key">
                    <button type="button" class="toggle-password" data-target="shodanApiKey">Show</button>
                </div>
                <label class="checkbox-label">
                    <input type="checkbox" id="storeShodanKey"> Store in local storage
                </label>
                <button onclick="saveShodanApiKey()">Save Shodan API Key</button>
            </div>
            <div class="input-group">
                <h3>GreyNoise API Key</h3>
                <div class="password-container">
                    <input type="password" id="greynoiseApiKey" placeholder="Enter GreyNoise API Key">
                    <button type="button" class="toggle-password" data-target="greynoiseApiKey">Show</button>
                </div>
                <label class="checkbox-label">
                    <input type="checkbox" id="storeGreynoiseKey"> Store in local storage
                </label>
                <button onclick="saveGreynoiseApiKey()">Save GreyNoise API Key</button>
            </div>
            <div class="input-group">
                <h3>URLscan.io API Key</h3>
                <div class="password-container">
                    <input type="password" id="urlscanApiKey" placeholder="Enter URLscan.io API Key">
                    <button type="button" class="toggle-password" data-target="urlscanApiKey">Show</button>
                </div>
                <label class="checkbox-label">
                    <input type="checkbox" id="storeUrlscanKey"> Store in local storage
                </label>
                <button onclick="saveUrlscanApiKey()">Save URLscan.io API Key</button>
            </div>
            <div class="input-group">
                <h3>SecurityTrails API Key</h3>
                <div class="password-container">
                    <input type="password" id="securitytrailsApiKey" placeholder="Enter SecurityTrails API Key">
                    <button type="button" class="toggle-password" data-target="securitytrailsApiKey">Show</button>
                </div>
                <label class="checkbox-label">
                    <input type="checkbox" id="storeSecuritytrailsKey"> Store in local storage
                </label>
                <button onclick="saveSecuritytrailsApiKey()">Save SecurityTrails API Key</button>
                </div>
                <div class="input-group">
                    <h3>URLhaus API Key</h3>
                    <div class="password-container">
                        <input type="password" id="urlhausApiKey" placeholder="Enter URLhaus API Key">
                        <button type="button" class="toggle-password" data-target="urlhausApiKey">Show</button>
                    </div>
                    <label class="checkbox-label">
                        <input type="checkbox" id="storeUrlhausKey"> Store in local storage
                    </label>
                    <button onclick="saveUrlhausApiKey()">Save URLhaus API Key</button>
                </div>
            <div class="input-group">
                <h3>CORS Proxy URL</h3>
                <div class="password-container">
                    <input type="password" id="corsProxyUrl" placeholder="Enter CORS Proxy URL" value="http://localhost:3000/proxy?url=">
                    <button type="button" class="toggle-password" data-target="corsProxyUrl">Show</button>
                </div>
                <label class="checkbox-label">
                    <input type="checkbox" id="storeCorsProxy" checked> Store in local storage
                </label>
                <label class="checkbox-label">
                    <input type="checkbox" id="routeViaProxy"> Route all traffic via proxy
                </label>
                <label class="checkbox-label">
                    <input type="checkbox" id="ignoreApiKeysViaProxy"> Ignore API keys when using proxy
                </label>
                <button onclick="saveCorsProxyUrl()">Save CORS Proxy URL</button>
            </div>
            <div class="input-group">
                <h3>Test Functions</h3>
                <button onclick="runAllTests()">Run Test Functions</button>
            </div>
        </div>
        <div id="enrichment" class="tab-content">
            <div class="input-group">
                <h3>Bulk Enrichment</h3>
                <button onclick="enrichAllIpinfo()">Enrich All IPs with IPinfo</button>
                <button onclick="enrichAllShodan()">Enrich All IPs with Shodan</button>
                <button onclick="enrichAllInternetDB()">Enrich All IPs with InternetDB</button>
                <button onclick="enrichAllGoogleDNS()">Enrich All Domains with Google DNS</button>
                <button onclick="enrichAllGoogleDNSMX()">Enrich All Domains with Google DNS (MX)</button>
                <button onclick="enrichAllGoogleDNSTXT()">Enrich All Domains with Google DNS (TXT)</button>
                <button onclick="enrichAllHudsonRockEmails()">Enrich All Emails with Hudson Rock</button>
                <button onclick="enrichAllHudsonRockDomains()">Enrich All Domains with Hudson Rock</button>
                <button onclick="enrichAllGreyNoise()">Enrich All IPs with GreyNoise</button>
                <button onclick="enrichAllURLscan()">Enrich All URLs with URLscan.io</button>
                <button onclick="enrichAllSecurityTrails()">Enrich All Domains with SecurityTrails Subdomains</button>
                <button onclick="enrichAllURLhaus()">Enrich All URLs with URLhaus</button>
            </div>
        </div>
        <div id="import-iocs" class="tab-content active">
            <div class="input-group">
                <h3>Import IOCs</h3>
                <textarea id="iocText" placeholder="Paste IOC text here (IPs, domains, emails)"></textarea>
                <button onclick="importIOCsFromText()">Import from Text</button>
                <input type="file" id="iocFile" accept=".txt">
                <button onclick="importIOCsFromFile()">Import from File</button>
            </div>
        </div>
        <div id="search" class="tab-content">
            <div class="input-group" style="margin: 10px 0;">
                <input type="text" id="search-input" placeholder="Search graph..." style="width: 100%; margin-bottom: 5px;">
                <button onclick="searchGraph()" style="background-color: #10b981;">Search</button>
            </div>
            <button onclick="riskAnalysis()">Risk Analysis</button> <!-- New Button -->
        </div>
        <div id="riskModal" class="modal">
            <div class="modal-content">
                <span class="close-modal">&times;</span>
                <h2>Risk Analysis</h2>
                <div id="riskTableContainer"></div>
            </div>
        </div>
        <div id="layouts" class="tab-content">
            <div class="input-group">
                <h3>Graph Layouts</h3>
                <!-- Existing layout buttons remain here -->
                <button onclick="setOrganicLayout()">Organic</button>
                <button onclick="setCircularLayout()">Circular</button>
                <button onclick="setOrthogonalLayout()">Orthogonal</button>
                <button onclick="setTreeLayout()">Tree</button>
                <button onclick="setHierarchicalLayout()">Hierarchical</button>
            </div>
            <!-- New section for label visibility -->
            <div class="input-group">
                <h3>Label Visibility</h3>
                <label class="checkbox-label">
                    <input type="checkbox" id="showNodeLabels" checked onchange="toggleNodeLabels()">
                    Show Node Labels
                </label>
                <label class="checkbox-label">
                    <input type="checkbox" id="showEdgeLabels" checked onchange="toggleEdgeLabels()">
                    Show Edge Labels
                </label>
                <label class="checkbox-label">
                    <input type="checkbox" id="hideIsolatedNodes" onchange="toggleIsolatedNodes()">
                    Hide Nodes Without Links
                </label>
            </div>
            <div class="input-group">
                <h3>Node Size Layouts</h3>
                <button onclick="setNodeSizeLayout('incoming')">Size by Incoming Links</button>
                <button onclick="setNodeSizeLayout('outgoing')">Size by Outgoing Links</button>
                <button onclick="setNodeSizeLayout('both')">Size by All Links</button>
            </div>
            <div class="input-group">
                <h3>Filter Display</h3>
                <button onclick="filterIpAndDomains()">Show Only IPs & Domains</button>
                <button onclick="showAllNodes()">Show All Nodes</button>
            </div>
        </div>
         <!-- Buy Me a Coffee Button -->
         <div id="buy-me-a-coffee-container" style="text-align: center; padding: 10px;">
            <script type="text/javascript" src="https://cdnjs.buymeacoffee.com/1.0.0/button.prod.min.js" 
                data-name="bmc-button" 
                data-slug="mrr3b00t" 
                data-color="#FFDD00" 
                data-emoji=""  
                data-font="Cookie" 
                data-text="Buy me a coffee" 
                data-outline-color="#000000" 
                data-font-color="#000000" 
                data-coffee-color="#ffffff">
            </script>
        </div>

        <button id="manual-save" onclick="saveStateAfterOperation()">Save Now</button>

        <div id="stop-task-container" style="text-align: center; padding: 10px;">
            <button id="stop-task" onclick="stopActiveTask()">Stop Active Task</button>
        </div>

        <!-- Suggested -->
        <footer id="controls-footer"> 
    <p>Created by mrr3b00t (@UK_Daniel_Card)</p>
    <p>© Xservus Limited - v0.289 demo</p>
</footer>
    </div>
    <div id="myNetwork"></div>
    <div id="summary-modal">
        <button class="close-button" onclick="hideGraphSummary()">×</button>
        <h3>Graph Summary</h3>
        <table id="summary-table">
            <thead>
                <tr>
                    <th>Type</th>
                    <th>Count</th>
                </tr>
            </thead>
            <tbody></tbody>
        </table>
    </div>
    <div id="properties-panel">
        <button class="close-button" onclick="hidePropertiesPanel()">×</button>
        <h3>Node Properties</h3>
        <table id="properties-table">
            <thead>
                <tr>
                    <th>Property</th>
                    <th>Value</th>
                </tr>
            </thead>
            <tbody></tbody>
        </table>
    </div>
    <div id="contextMenu" style="display: none;"></div>
    <div id="edgeContextMenu" style="display: none;"></div>

<script>
        let nodes = new vis.DataSet([]);
        let edges = new vis.DataSet([]);
        let nextId = 1;
        let isDarkMode = true;
        let ipinfoApiKey;
        let shodanApiKey;
        let urlhausApiKey = localStorage.getItem('urlhausApiKey') || '';
        let securitytrailsApiKey = localStorage.getItem('securitytrailsApiKey') || ''; //security tails api key variable
        let greynoiseApiKey = localStorage.getItem('greynoiseApiKey') || '';
        let urlscanApiKey = localStorage.getItem('urlscanApiKey') || '';
        let corsProxyUrl;
        let routeViaProxy;
        let ignoreApiKeysViaProxy;
        let isPhysicsPaused = false;
        let lastRequestTime = 0;
        let activeTaskController = null;
        let nodeLabelsVisible = true;
        let edgeLabelsVisible = true;
        let currentEditingNodeId = null;
        const RATE_LIMIT_MS = 500;
        const SHODAN_RATE_LIMIT_MS = 1000; // Specific 1-second delay for Shodan
        const SECURITYTRAILS_RATE_LIMIT_MS = 2000; // 1 second delay for SecurityTrails API
        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}$/ };
        const domainRegex = /^(?:[a-zA-Z0-9-_]+\.)*[a-zA-Z0-9-_]+\.[a-zA-Z]{2,}$/;
        const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;

        let selectedNodes = new Set(); // To track multiple selected nodes
    
        initializeApiKeys();



        // Define updateTheme first
function updateTheme() {
    if (isDarkMode) {
        document.body.classList.remove('light-mode');
        document.body.classList.add('dark-mode');
        options.nodes.font = {
            size: nodeLabelsVisible ? 12 : 0,
            color: '#e2e8f0',  // Light color for dark mode
            multi: true,
            align: 'center',
            vadjust: 0,
            strokeWidth: 0
        };
        options.edges.font = {
            size: edgeLabelsVisible ? 12 : 0,
            color: '#e2e8f0',  // Light color for dark mode
            strokeWidth: 0,
            strokeColor: 'transparent',
            align: 'middle',
            multi: true
        };
    } else {
        document.body.classList.remove('dark-mode');
        document.body.classList.add('light-mode');
        options.nodes.font = {
            size: nodeLabelsVisible ? 12 : 0,
            color: '#1f2a44',  // Dark color for light mode
            multi: true,
            align: 'center',
            vadjust: 0,
            strokeWidth: 0
        };
        options.edges.font = {
            size: edgeLabelsVisible ? 12 : 0,
            color: '#1f2a44',  // Dark color for light mode
            strokeWidth: 0,
            strokeColor: 'transparent',
            align: 'middle',
            multi: true
        };
    }

    // Update all existing nodes and edges with the new font settings
    nodes.forEach(node => {
        nodes.update({
            id: node.id,
            font: options.nodes.font
        });
    });
    
    edges.forEach(edge => {
        edges.update({
            id: edge.id,
            font: options.edges.font
        });
    });

    // Apply the options to the network and force a redraw
    network.setOptions(options);
    updateLabelVisibility();
    //ensureInteractionSettings();
    network.setData({ nodes: nodes, edges: edges });  // Force data refresh
    network.redraw();
}

function searchGraph() {
        const searchTerm = document.getElementById('search-input').value.trim().toLowerCase();
        if (!searchTerm) {
            showToast('Please enter a search term', 'error');
            resetNodeHighlights();
            return;
        }

    // Reset previous highlights
    resetNodeHighlights();

    // Find matching nodes
    const matchingNodes = nodes.get().filter(node => {
        // Search in label, and type-specific fields
        const searchableText = [
            node.label || '',
            node.ip || '',
            node.domain || '',
            node.email || '',
            node.name || '',
            node.organization || '',
            node.portNumber || '',
            node.address || '',
            node.accountNumber || '',
            node.techName || '',
            node.deviceName || '',
            node.malwareName || '',
            node.vulnName || '',
            node.cve || '',
            node.hash || '',
            node.asn || '',
            node.city || '',
            node.country || '',
            node.os || '',
            node.product || ''
        ].join(' ').toLowerCase();

        return searchableText.includes(searchTerm);
    });

    if (matchingNodes.length === 0) {
        showToast('No matches found', 'info');
        return;
    }

    // Highlight matching nodes
    matchingNodes.forEach(node => {
        nodes.update({
            id: node.id,
            color: {
                background: '#ffeb3b', // Bright yellow for highlight
                border: '#f44336',     // Red border for visibility
                highlight: {
                    background: '#ffeb3b',
                    border: '#f44336'
                }
            },
            font: {
                size: 14,  // Slightly larger font for visibility
                color: '#000000'  // Black text for contrast
            }
        });
    });

    // Zoom to the first matching node
    const firstMatchId = matchingNodes[0].id;
    network.focus(firstMatchId, {
        scale: 1.5,  // Zoom in
        animation: {
            duration: 1000,
            easingFunction: 'easeInOutQuad'
        }
    });
    ensureInteractionSettings(); // Ensure panning is enabled after focus
    showToast(`Found ${matchingNodes.length} matching nodes`, 'success');
}

function resetNodeHighlights() {
    nodes.forEach(node => {
        // Restore original colors based on node type
        const originalColor = getNodeColorByType(node.type);
        nodes.update({
            id: node.id,
            color: {
                background: originalColor.background,
                border: isDarkMode ? '#94a3b8' : '#6b7280',
                highlight: {
                    background: originalColor.background,
                    border: '#60a5fa'
                }
            },
            font: {
                size: nodeLabelsVisible ? 12 : 0,
                color: isDarkMode ? '#e2e8f0' : '#1f2a44'
            }
        });
    });
    network.fit({
        animation: {
            duration: 500,
            easingFunction: 'easeInOutQuad'
        }
    });
}

function exportToPNG() {
    network.setOptions({ physics: { enabled: false } });
    network.stabilize(100);
    network.fit(); // Zoom to fit all nodes
    
    setTimeout(() => {
        try {
            const canvas = document.querySelector('#myNetwork .vis-network canvas');
            if (!canvas) throw new Error('Canvas not found');
            
            const dataURL = canvas.toDataURL('image/png');
            const link = document.createElement('a');
            link.href = dataURL;
            link.download = `network_graph_${new Date().toISOString().replace(/[:.]/g, '-')}.png`;
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            
            showToast('Graph exported as PNG', 'success');
        } catch (error) {
            console.error('Error exporting to PNG:', error);
            showToast('Failed to export graph as PNG: ' + error.message, 'error');
        } finally {
            network.setOptions({ physics: { enabled: !isPhysicsPaused } });
        }
    }, 500);
}


function filterIpAndDomains() {
    // Disable physics during filtering
    network.setOptions({ physics: { enabled: false } });
    
    // Update each node's hidden property based on type
    nodes.forEach(node => {
        const shouldHide = node.type !== 'ip' && node.type !== 'domain';
        nodes.update({
            id: node.id,
            hidden: shouldHide
        });
    });
    
    // Hide edges connected to hidden nodes
    edges.forEach(edge => {
        const fromNode = nodes.get(edge.from);
        const toNode = nodes.get(edge.to);
        const shouldHide = fromNode.hidden || toNode.hidden;
        edges.update({
            id: edge.id,
            hidden: shouldHide
        });
    });
    
    // Stabilize and fit the network
    stabilizeNetwork().then(() => {
        network.fit({
            animation: {
                duration: 300,
                easingFunction: 'easeInOutQuad'
            }
        });
        showToast('Showing only IP addresses and domains', 'success');
        saveStateAfterOperation();
    });
}

function showAllNodes() {
    // Disable physics during filtering
    network.setOptions({ physics: { enabled: false } });
    
    // Show all nodes and edges
    nodes.forEach(node => {
        nodes.update({
            id: node.id,
            hidden: false
        });
    });
    
    edges.forEach(edge => {
        edges.update({
            id: edge.id,
            hidden: false
        });
    });
    
    // Stabilize and fit the network
    stabilizeNetwork().then(() => {
        network.fit({
            animation: {
                duration: 300,
                easingFunction: 'easeInOutQuad'
            }
        });
        showToast('Showing all nodes and edges', 'success');
        saveStateAfterOperation();
    });
}

// Helper function to get original colors by node type
function getNodeColorByType(type) {
    const colorMap = {
        'ip': '#f87171',
        'domain': '#60a5fa',
        'contact': '#4ade80',
        'organization': '#facc15',
        'port': '#a78bfa',
        'wallet': '#fb923c',
        'bank': '#10b981',
        'technology': '#ec4899',
        'device': '#14b8a6',
        'malware': '#ef4444',
        'vulnerability': '#dc2626',
        'favicon': '#22d3ee',
        'http_hash': '#f97316',
        'html_hash': '#f59e0b',
        'ssl_hash': '#8b5cf6',
        'asn': '#a3e635',
        'city': '#f97316',
        'country': '#34d399',
        'os': '#10b981',
        'product': '#ec4899',
        'http_title': '#3b82f6',
        'vpn': '#9333ea',
        'proxy': '#f43f5e',
        'tor': '#64748b',
        'relay': '#eab308',
        'hosting': '#14b8a6',
        'tag': '#6d28d9',
        'cpe': '#0d9488',
        'mx': '#34d399', // Green for MX
        'txt': '#f59e0b'  // Orange for TXT
    };
    return { background: colorMap[type] || '#ffffff' };
}

// Add this with your other event listeners
document.getElementById('search-input').addEventListener('keypress', function(event) {
    if (event.key === 'Enter') {
        event.preventDefault();
        searchGraph();
    }
});


        async function enrichAllIpinfo() {
    if (!ipinfoApiKey && !ignoreApiKeysViaProxy) { 
        showToast('Please set your IPinfo API key in the "API Keys" tab first.', 'error'); 
        return; 
    }
    
    showProgressBar();
    network.setOptions({ physics: { enabled: false } });
    const ipNodes = nodes.get({ filter: n => n.type === 'ip' && n.ip });
    const totalIPs = ipNodes.length;
    let successfulEnrichments = 0;
    
    const batchSize = 50;
    const delayBetweenBatches = 200;
    const totalBatches = Math.ceil(totalIPs / batchSize);
    const assumedRequestTimeMs = 100;
    const timePerBatchMs = assumedRequestTimeMs;
    const totalBatchDelays = (totalBatches - 1) * delayBetweenBatches;
    const estimatedTimeMs = (timePerBatchMs * totalBatches) + totalBatchDelays + 1000;
    
    const estimatedSeconds = Math.ceil(estimatedTimeMs / 1000);
    const estimatedMinutes = Math.floor(estimatedSeconds / 60);
    const remainingSeconds = estimatedSeconds % 60;
    const timeEstimateStr = estimatedMinutes > 0 
        ? `${estimatedMinutes}m ${remainingSeconds}s` 
        : `${estimatedSeconds}s`;
    
    showToast(`Estimated time for IPinfo enrichment: ~${timeEstimateStr}`, 'info');
    document.getElementById('progress-bar').textContent = `IPinfo Enrichment: 0/${totalIPs} IPs (0%) - Est. ${timeEstimateStr}`;
    
    const newNodes = [];
    const newEdges = [];
    let existingAsns = new Map(nodes.get({ filter: n => n.type === 'asn' }).map(n => [n.asn, n.id]));
    let existingCities = new Map(nodes.get({ filter: n => n.type === 'city' }).map(n => [n.city, n.id]));
    let existingOrgs = new Map(nodes.get({ filter: n => n.type === 'organization' }).map(n => [n.organization, n.id]));
    let existingCountries = new Map(nodes.get({ filter: n => n.type === 'country' }).map(n => [n.country, n.id]));
    let existingPrivacyTypes = new Map([
        ['vpn', null], ['proxy', null], ['tor', null], ['relay', null], ['hosting', null]
    ].map(([type]) => {
        const existing = nodes.get({ filter: n => n.type === type })[0];
        return [type, existing ? existing.id : null];
    }));

    const privacyTypes = [
        { key: 'vpn', label: 'VPN', color: '#9333ea' },
        { key: 'proxy', label: 'Proxy', color: '#f43f5e' },
        { key: 'tor', label: 'Tor', color: '#64748b' },
        { key: 'relay', label: 'Relay', color: '#eab308' },
        { key: 'hosting', label: 'Hosting', color: '#14b8a6' }
    ];

    async function processBatch(batch) {
        const promises = batch.map(node => {
            if (activeTaskController && activeTaskController.signal.aborted) {
                return Promise.resolve(null);
            }
            const baseUrl = ignoreApiKeysViaProxy ? 
                `https://ipinfo.io/${node.ip}/json` : 
                `https://ipinfo.io/${node.ip}/json?token=${ipinfoApiKey}`;
            const url = constructUrl(baseUrl, !ignoreApiKeysViaProxy);
            return fetch(url)
                .then(response => {
                    if (!response.ok) throw new Error('Failed to fetch IPinfo data');
                    return response.json();
                })
                .then(data => {
                    const ipNodeId = node.id;
                    const asn = data.asn?.asn || 'Unknown ASN';
                    const city = data.city || 'Unknown City';
                    const companyName = data.company?.name || 'Unknown Company';
                    const country = data.country || 'Unknown Country';
                    const privacy = data.privacy || { vpn: false, proxy: false, tor: false, relay: false, hosting: false };

                    // ASN
                    let asnId = existingAsns.get(asn);
                    if (!asnId) {
                        asnId = nextId++;
                        newNodes.push({ 
                            id: asnId, 
                            type: 'asn', 
                            label: `ASN: ${asn}`, 
                            title: `ASN: ${asn}`, 
                            color: { background: '#a3e635' }, 
                            asn 
                        });
                        existingAsns.set(asn, asnId);
                    }
                    const asnEdgeId = `${ipNodeId}-${asnId}-AssignedTo`;
                    if (!edges.get(asnEdgeId) && !newEdges.some(e => e.id === asnEdgeId)) {
                        newEdges.push({ id: asnEdgeId, from: ipNodeId, to: asnId, label: 'Assigned to' });
                    }

                    // City
                    let cityId = existingCities.get(city);
                    if (!cityId) {
                        cityId = nextId++;
                        newNodes.push({ 
                            id: cityId, 
                            type: 'city', 
                            label: `City: ${city}`, 
                            title: `City: ${city}`, 
                            color: { background: '#f97316' }, 
                            city 
                        });
                        existingCities.set(city, cityId);
                    }
                    const cityEdgeId = `${ipNodeId}-${cityId}-LocatedIn`;
                    if (!edges.get(cityEdgeId) && !newEdges.some(e => e.id === cityEdgeId)) {
                        newEdges.push({ id: cityEdgeId, from: ipNodeId, to: cityId, label: 'Located in' });
                    }

                    // Organization
                    let orgId = existingOrgs.get(companyName);
                    if (!orgId) {
                        orgId = nextId++;
                        newNodes.push({ 
                            id: orgId, 
                            type: 'organization', 
                            label: `Organization: ${companyName}`, 
                            title: `Company: ${companyName}`, 
                            color: { background: '#facc15' }, 
                            organization: companyName 
                        });
                        existingOrgs.set(companyName, orgId);
                    }
                    const orgEdgeId = `${ipNodeId}-${orgId}-BelongsTo`;
                    if (!edges.get(orgEdgeId) && !newEdges.some(e => e.id === orgEdgeId)) {
                        newEdges.push({ id: orgEdgeId, from: ipNodeId, to: orgId, label: 'Belongs to' });
                    }

                    // Country
                    let countryId = existingCountries.get(country);
                    if (!countryId) {
                        countryId = nextId++;
                        newNodes.push({ 
                            id: countryId, 
                            type: 'country', 
                            label: `Country: ${country}`, 
                            title: `Country: ${country}`, 
                            color: { background: '#34d399' }, 
                            country 
                        });
                        existingCountries.set(country, countryId);
                    }
                    const countryEdgeId = `${ipNodeId}-${countryId}-LocatedIn`;
                    if (!edges.get(countryEdgeId) && !newEdges.some(e => e.id === countryEdgeId)) {
                        newEdges.push({ id: countryEdgeId, from: ipNodeId, to: countryId, label: 'Located in' });
                    }

                    // Privacy Types
                    privacyTypes.forEach(privacyType => {
                        if (privacy[privacyType.key]) {
                            let privacyNodeId = existingPrivacyTypes.get(privacyType.key);
                            if (!privacyNodeId) {
                                privacyNodeId = nextId++;
                                newNodes.push({ 
                                    id: privacyNodeId, 
                                    type: privacyType.key, 
                                    label: privacyType.label, 
                                    title: privacyType.label, 
                                    color: { background: privacyType.color }
                                });
                                existingPrivacyTypes.set(privacyType.key, privacyNodeId);
                            }
                            const privacyEdgeId = `${ipNodeId}-${privacyNodeId}-Uses`;
                            if (!edges.get(privacyEdgeId) && !newEdges.some(e => e.id === privacyEdgeId)) {
                                newEdges.push({ id: privacyEdgeId, from: ipNodeId, to: privacyNodeId, label: 'Uses' });
                            }
                        }
                    });

                    successfulEnrichments++;
                })
                .catch(error => {
                    console.error(`Failed to enrich IP ${node.ip}: ${error.message}`);
                    showToast(`Failed to enrich IP ${node.ip}: ${error.message}`, 'error');
                    return null;
                });
        });
        await Promise.all(promises);
    }
    
    let lastProgressUpdate = 0;
    const progressUpdateInterval = 1000;
    const startTime = Date.now();
    
    for (let i = 0; i < totalIPs; i += batchSize) {
        if (activeTaskController && activeTaskController.signal.aborted) {
            showToast('IPinfo enrichment stopped', 'info');
            break;
        }
        
        const batch = ipNodes.slice(i, Math.min(i + batchSize, totalIPs));
        await processBatch(batch);
        
        if (newNodes.length > 0) {
            nodes.add(newNodes);
            newNodes.length = 0;
        }
        if (newEdges.length > 0) {
            edges.add(newEdges);
            newEdges.length = 0;
        }
        
        const currentTime = Date.now();
        if (currentTime - lastProgressUpdate >= progressUpdateInterval) {
            const processedIPs = Math.min(i + batchSize, totalIPs);
            const progress = ((processedIPs / totalIPs) * 100).toFixed(1);
            const remainingIPs = totalIPs - processedIPs;
            const remainingTimeMs = Math.max(0, remainingIPs * assumedRequestTimeMs);
            const remainingSeconds = Math.ceil(remainingTimeMs / 1000);
            const remainingMinutes = Math.floor(remainingSeconds / 60);
            const remainingSecondsPart = remainingSeconds % 60;
            const remainingTimeStr = remainingMinutes > 0 
                ? `${remainingMinutes}m ${remainingSecondsPart}s` 
                : `${remainingSeconds}s`;
            
            document.getElementById('progress-bar').textContent = 
                `IPinfo Enrichment: ${successfulEnrichments}/${totalIPs} IPs (${progress}%) - Est. ${remainingTimeStr} remaining`;
            lastProgressUpdate = currentTime;
            updateSelectOptions();
        }
        
        await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));
    }
    
    if (newNodes.length > 0) nodes.add(newNodes);
    if (newEdges.length > 0) edges.add(newEdges);
    updateNodeSizes();
    updateSelectOptions();
    await stabilizeNetwork();
    //ensureInteractionSettings();
    completeProgressBar();
    showToast(`IPinfo enrichment completed: ${successfulEnrichments}/${totalIPs} IPs enriched`, 'success');
    
    if (window.innerWidth <= 768) {
        const controls = document.getElementById('controls');
        controls.classList.add('collapsed');
        document.getElementById('myNetwork').style.display = 'block';
        network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });
    }
}


//Export to PDF


function exportToPDF() {
    network.setOptions({ physics: { enabled: false } });
    network.stabilize(100);
    network.fit();
    
    setTimeout(() => {
        try {
            const canvas = document.querySelector('#myNetwork .vis-network canvas');
            if (!canvas) throw new Error('Canvas not found');

            const { jsPDF } = window.jspdf;
            const pdf = new jsPDF({
                orientation: 'landscape',
                unit: 'px',
                format: [canvas.width, canvas.height]
            });

            // Add title
            pdf.setFontSize(16);
            pdf.text('Network Graph Export', 40, 20);
            
            const imgData = canvas.toDataURL('image/png');
            pdf.addImage(imgData, 'PNG', 0, 40, canvas.width, canvas.height - 40); // Offset for title
            pdf.save(`network_graph_${new Date().toISOString().replace(/[:.]/g, '-')}.pdf`);
            
            showToast('Graph exported as PDF', 'success');
        } catch (error) {
            console.error('Error exporting to PDF:', error);
            showToast('Failed to export graph as PDF: ' + error.message, 'error');
        } finally {
            network.setOptions({ physics: { enabled: !isPhysicsPaused } });
        }
    }, 500);
}

// Function to trigger save after key operations
function saveStateAfterOperation() {
    saveState();
    showToast('Progress saved', 'success');
}

window.onload = function() {
    const stateLoaded = loadState();
    if (!stateLoaded) {
        nodes = new vis.DataSet([]);
        edges = new vis.DataSet([]);
        nextId = 1;
    }

    document.getElementById('ipinfoApiKey').value = ipinfoApiKey;
    document.getElementById('shodanApiKey').value = shodanApiKey;
    document.getElementById('corsProxyUrl').value = corsProxyUrl;
    document.getElementById('routeViaProxy').checked = routeViaProxy;
    document.getElementById('ignoreApiKeysViaProxy').checked = ignoreApiKeysViaProxy;
    document.getElementById('storeIpinfoKey').checked = !!localStorage.getItem('ipinfoApiKey');
    document.getElementById('storeShodanKey').checked = !!localStorage.getItem('shodanApiKey');
    document.getElementById('storeCorsProxy').checked = !!localStorage.getItem('corsProxyUrl');

    updateSelectOptions();
    updateTheme();
    
    // Ensure interaction settings with dragView
    network.setOptions({
        interaction: {
            ...baseInteractionOptions,
            dragView: true,  // Explicitly enable dragging
            zoomView: true,
            zoomSpeed: 0.5
        }
    });

    document.getElementById('menu-toggle').textContent = '<';  // Set to "<" since menu starts expanded
    ensureInteractionSettings();
    
    container.removeEventListener('mousedown', handleMouseDown);
    container.removeEventListener('mousemove', handleMouseMove);
    container.removeEventListener('mouseup', handleMouseUp);
    container.addEventListener('wheel', handleWheel, { passive: false });

    if (window.innerWidth <= 768) {
        document.getElementById('controls').classList.add('collapsed');
    }

    stabilizeNetwork().then(() => {
        network.fit({ animation: { duration: 300 } });
    });

    let hasChanges = false;
    nodes.on('*', () => hasChanges = true);
    edges.on('*', () => hasChanges = true);
    setInterval(() => {
        if (hasChanges) {
            saveState();
            hasChanges = false;
            showToast('Auto-saved', 'info');
        }
    }, 60 * 1000);
};


function loadState() {
    const savedState = localStorage.getItem('networkGraphState');
    if (!savedState) {
        console.log('No saved state found in localStorage');
        return false;
    }

    try {
        const state = JSON.parse(savedState);
        if (!state.nodes || !state.edges) return false;

        nodes.clear();
        edges.clear();
        
        state.nodes.forEach(node => nodes.add(node));
        state.edges.forEach(edge => edges.add(edge));
        
        nextId = state.nextId || 1;
        isDarkMode = state.isDarkMode !== undefined ? state.isDarkMode : true;
        isPhysicsPaused = state.isPhysicsPaused || false;
        nodeLabelsVisible = state.nodeLabelsVisible !== undefined ? state.nodeLabelsVisible : true;
        edgeLabelsVisible = state.edgeLabelsVisible !== undefined ? state.edgeLabelsVisible : true;
        
        document.getElementById('showNodeLabels').checked = nodeLabelsVisible;
        document.getElementById('showEdgeLabels').checked = edgeLabelsVisible;
        
        updateTheme();
        const pauseButton = document.getElementById('pause-toggle');
        pauseButton.textContent = isPhysicsPaused ? 'Resume Physics' : 'Pause Physics';
        pauseButton.classList.toggle('paused', isPhysicsPaused);
        
         // Ensure interaction settings with dragView
    network.setOptions({
        interaction: {
            ...baseInteractionOptions,
            dragView: true,  // Explicitly enable dragging
            zoomView: true,
            zoomSpeed: 0.5
        }
    });

        
        // Re-apply wheel event listener (from previous zoom fix)
        container.removeEventListener('wheel', handleWheel);
        container.addEventListener('wheel', handleWheel, { passive: false });
        
        console.log('Loaded state with dragView enabled');
        return true;
    } catch (e) {
        console.error('Error loading state:', e);
        showToast('Failed to load saved state: ' + e.message, 'error');
        localStorage.removeItem('networkGraphState');
        return false;
    }
}


// Separate wheel handler function for reusability //removed this as it broke pan
function handleWheel(event) {
    // event.preventDefault();
    // const scale = event.deltaY > 0 ? 0.9 : 1.1;
    // const currentScale = network.getScale();
    // network.moveTo({
    //     scale: currentScale * scale,
    //     animation: { duration: 100 }
    // });
}

function throttleRequest(fn) {
            return async function(...args) {
                const now = Date.now();
                const timeSinceLast = now - lastRequestTime;
                if (timeSinceLast < RATE_LIMIT_MS) await new Promise(resolve => setTimeout(resolve, RATE_LIMIT_MS - timeSinceLast));
                lastRequestTime = now;
                return fn(...args);
            };
        }


const baseInteractionOptions = {
    dragNodes: true,
    dragView: true,    // Explicitly enable panning
    zoomView: true,    // Enable zooming
    selectable: true,
    multiselect: true,
    hover: true,
    zoomSpeed: 0.5
};

        let container = document.getElementById('myNetwork');
        let data = { nodes: nodes, edges: edges };

        let options = {
    nodes: {
        shape: 'dot',
        size: 10,
        font: { 
            size: nodeLabelsVisible ? 12 : 0,
            color: '#e2e8f0',
            multi: true,
            align: 'center',
            vadjust: 0
        },
        scaling: {
            min: 10,
            max: 125,
            label: { enabled: false }
        },
        fixed: {
            x: false,
            y: false
        },
        shapeProperties: {
            useBorderWithImage: false
        },
        chosen: {
            label: function(values, id, selected, hovering) {
                values.size = nodeLabelsVisible ? 12 : 0;
            }
        },
        color: {
            hover: {
                border: '#60a5fa',
                background: '#4b5563'
            },
            highlight: {
                border: '#60a5fa',
                background: '#4b5563'
            }
        }
    },
    edges: { 
        arrows: { to: { enabled: true, scaleFactor: 0.5 } },
        font: { 
            size: edgeLabelsVisible ? 12 : 0,
            color: '#e2e8f0',
            strokeWidth: 0,
            strokeColor: 'transparent'
        },
        smooth: true,
        width: 1,
        chosen: {
            label: function(values, id, selected, hovering) {
                values.size = edgeLabelsVisible ? 12 : 0;
            }
        },
        color: { 
            color: '#94a3b8',
            highlight: '#60a5fa'
        }
    },
    physics: { 
        enabled: true,
        stabilization: {
            enabled: true,
            iterations: 100,
            updateInterval: 25
        },
        barnesHut: { 
            gravitationalConstant: -8000,
            centralGravity: 0.1,
            springLength: 200,
            springConstant: 0.04,
            damping: 0.9,
            avoidOverlap: 0.5
        },
        maxVelocity: 25,
        minVelocity: 0.1,
        solver: 'barnesHut'
    },
    interaction: {
        dragNodes: true,
        dragView: true,    // Enable view dragging (panning)
        zoomView: true,    // Enable zooming
        hover: true,
        multiselect: true,
        selectable: true,
        zoomSpeed: 0.5
    },
    layout: { improvedLayout: true }
};

        let network = new vis.Network(container, data, options);

network.on('selectNode', function(params) {
    selectedNodes = new Set(params.nodes); // Sync with vis.js selection
    console.log('Nodes selected:', [...selectedNodes]);
});

network.on('deselectNode', function(params) {
    selectedNodes = new Set(params.nodes); // Sync with vis.js selection
    console.log('Nodes deselected, remaining:', [...selectedNodes]);
});

function ensureInteractionSettings() {
    network.setOptions({
        interaction: { 
            dragNodes: true,
            dragView: true,    // Explicitly enable panning
            zoomView: true,
            selectable: true,
            multiselect: true,
            hover: true,
            zoomSpeed: 0.5
        },
        physics: {
            enabled: !isPhysicsPaused,
            stabilization: { enabled: false },
            barnesHut: {
                gravitationalConstant: -8000,
                centralGravity: 0.1,
                springLength: 200,
                springConstant: 0.04,
                damping: 0.9,
                avoidOverlap: 0.5
            },
            maxVelocity: 25,
            minVelocity: 0.1
        },
        nodes: {
            fixed: { x: false, y: false } // Ensure nodes remain movable unless explicitly fixed
        }
    });
    console.log('Interaction settings reapplied with dragView enabled');
}


network.on('dragStart', function(params) {
    if (params.nodes.length) {
        // Dragging a node
        console.log('Dragging node:', params.nodes);
    } else {
        // Dragging background (panning)
        console.log('Starting pan');
        network.setOptions({
            interaction: { dragView: true }
        });
    }
});

network.on('dragEnd', function(params) {
    if (params.nodes.length > 0) {
        // Node dragging ended
        params.nodes.forEach(nodeId => {
            nodes.update({
                id: nodeId,
                fixed: { x: false, y: false }
            });
        });
        console.log('Node drag ended, position updated');
        //saveState(); // Save silently without toast
    } else {
        // View panning ended
        console.log('View pan ended, no save needed');
    }
    ensureInteractionSettings(); // Reapply interaction settings regardless
});

function stabilizeNetwork(skipFit = false) {
    return new Promise(resolve => {
        network.setOptions({
            physics: {
                enabled: true,
                stabilization: { enabled: true, iterations: 200, updateInterval: 50 }
            }
        });
        network.stabilize(200);
        let resolved = false;
        network.once('stabilizationIterationsDone', () => {
            if (!resolved) {
                resolved = true;
                finishStabilization(resolve, skipFit);
            }
        });
        setTimeout(() => {
            if (!resolved) {
                resolved = true;
                finishStabilization(resolve, skipFit);
            }
        }, 5000);
    });
}

function finishStabilization(resolve, skipFit) {
    network.setOptions({ 
        physics: { enabled: !isPhysicsPaused, stabilization: { enabled: false } }
    });
    ensureInteractionSettings(); // Reapply interaction settings after stabilization
    if (!skipFit) network.fit({ animation: { duration: 500, easingFunction: 'easeInOutQuad' } });
    resolve();
}
   

network.on('init', function() {
            container.addEventListener('wheel', function(event) {
                event.preventDefault();
                const scale = event.deltaY > 0 ? 0.9 : 1.1;
                const currentScale = network.getScale();
                network.moveTo({
                    scale: currentScale * scale,
                    animation: { duration: 100 }
                });
            }, { passive: false });
}

);

network.on('oncontext', function(params) {
    params.event.preventDefault();
    const nodeId = this.getNodeAt(params.pointer.DOM);
    const edgeId = this.getEdgeAt(params.pointer.DOM);

    // Don't modify selectedNodes here unless it's a deliberate action
    if (selectedNodes.size > 0) {
        const firstNode = nodes.get([...selectedNodes][0]);
        let value;
        switch (firstNode.type) {
            case 'ip': value = firstNode.ip; break;
            case 'domain': value = firstNode.domain; break;
            default: value = firstNode.label;
        }
        showContextMenu(params.pointer.DOM.x, params.pointer.DOM.y, value, [...selectedNodes], firstNode.type);
    } else if (nodeId) {
        // Fallback for single node if no prior selection
        const node = nodes.get(nodeId);
        let value;
        switch (node.type) {
            case 'ip': value = node.ip; break;
            case 'domain': value = node.domain; break;
            default: value = node.label;
        }
        showContextMenu(params.pointer.DOM.x, params.pointer.DOM.y, value, [nodeId], node.type);
    } else if (edgeId) {
        showEdgeContextMenu(params.pointer.DOM.x, params.pointer.DOM.y, edgeId);
    }
});

network.on('click', function(params) {

    if (params.nodes.length === 0) {
        // Empty space clicked, prevent default zooming
        params.event.preventDefault();
        console.log('Empty space clicked, no action taken');
        return;
    }
    // Only proceed if exactly one node is selected and not dragging
    if (params.nodes.length !== 1 || params.event.type === 'drag') {
        console.log('Click event ignored:', params);
        return;
    }

    const nodeId = params.nodes[0];
    const node = nodes.get(nodeId);

    // Verify node exists
    if (!node) {
        console.error('Node not found for ID:', nodeId);
        showToast('Node not found', 'error');
        return;
    }

    // Log the node for debugging
    console.log('Clicked node:', JSON.stringify(node, null, 2));

    // Determine the value to copy based on node type
    let valueToCopy;
    switch (node.type) {
        case 'ip':
            valueToCopy = node.ip;
            break;
        case 'domain':
            valueToCopy = node.domain;
            break;
        case 'url':
            valueToCopy = node.url;
            break;
        case 'contact':
            valueToCopy = node.email || node.name;
            break;
        case 'organization':
            valueToCopy = node.organization;
            break;
        case 'port':
            valueToCopy = `${node.portType}/${node.portNumber}`;
            break;
        case 'wallet':
            valueToCopy = node.address;
            break;
        case 'bank':
            valueToCopy = node.accountNumber;
            break;
        case 'technology':
            valueToCopy = node.techName;
            break;
        case 'device':
            valueToCopy = node.deviceName;
            break;
        case 'malware':
            valueToCopy = node.malwareName;
            break;
        case 'vulnerability':
            valueToCopy = node.cve || node.vulnName;
            break;
        case 'favicon':
        case 'http_hash':
        case 'html_hash':
        case 'ssl_hash':
        case 'hash': // New case for file hashes
            valueToCopy = node.hash; // Use full hash value
            break;
        case 'asn':
            valueToCopy = node.asn;
            break;
        case 'city':
            valueToCopy = node.city;
            break;
        case 'country':
            valueToCopy = node.country;
            break;
        case 'os':
            valueToCopy = node.os;
            break;
        case 'product':
            valueToCopy = node.product;
            break;
        case 'http_title':
            valueToCopy = node.title;
            break;
        case 'vpn':
        case 'proxy':
        case 'tor':
        case 'relay':
        case 'hosting':
            valueToCopy = node.value;
            break;
        case 'hash': // Add this case for file hashes
            if (!node.hash) return;
            nodeData.hash = node.hash;
            nodeData.hashType = node.hashType || 'Unknown';
            nodeData.label = `${node.hashType}: ${node.hash.substring(0, 8)}...`;
            nodeData.title = `File Hash\nType: ${node.hashType}\nValue: ${node.hash}${node.notes ? '\nNotes: ' + node.notes : ''}`;
            nodeData.color.background = node.color?.background || '#f97316';
            break;
        case 'txt': // New case for TXT records
            valueToCopy = node.text;
            break;
        default:
            valueToCopy = node.label.split('\n')[0].split(': ')[1] || node.label.split('\n')[0];
            console.warn(`Unhandled node type "${node.type}", using fallback value:`, valueToCopy);
    }

    // Check if a valid value was found
    if (!valueToCopy) {
        console.warn('No valid value to copy for node:', node);
        showToast(`No value available to copy for ${node.type}`, 'warning');
        return;
    }

    // Log the value being copied for debugging
    console.log('Value to copy:', valueToCopy);

    // Attempt to copy to clipboard
    navigator.clipboard.writeText(valueToCopy)
        .then(() => {
            showToast(`Copied "${valueToCopy}" to clipboard`, 'success');
        })
        .catch(err => {
            console.error('Failed to copy to clipboard:', err);
            showToast(`Failed to copy: ${err.message}`, 'error');
        });
});

function showEdgeContextMenu(x, y, edgeId) {
    const menu = document.getElementById('edgeContextMenu');
    const edge = edges.get(edgeId);
    if (!edge) return;
    
    const menuHtml = `
        <button onclick="editEdgeLabel('${edgeId}')">Edit Label</button>
        <button onclick="removeEdgeDirect('${edgeId}')">Delete Edge</button>
    `;
    
    menu.innerHTML = menuHtml;
    const canvasOffset = container.getBoundingClientRect();
    menu.style.left = `${x + canvasOffset.left}px`;
    menu.style.top = `${y + canvasOffset.top}px`;
    menu.style.display = 'block';
    document.addEventListener('click', hideEdgeContextMenu);
}

function hideEdgeContextMenu() {
    document.getElementById('edgeContextMenu').style.display = 'none';
    document.removeEventListener('click', hideEdgeContextMenu);
}

function editEdgeLabel(edgeId) {
    const edge = edges.get(edgeId);
    if (!edge) {
        showToast('Edge not found', 'error');
        return;
    }
    
    const currentLabel = edge.label || '';
    const newLabel = prompt('Enter new edge label (leave empty to remove):', currentLabel);
    
    if (newLabel !== null) { // null means user cancelled
        edges.update({
            id: edgeId,
            label: newLabel.trim() === '' ? undefined : newLabel.trim()
            
        });
        updateEdgeSelectOptions();
        stabilizeNetwork();
        saveStateAfterOperation();
        showToast('Edge label updated', 'success');
    }
    
    hideEdgeContextMenu();
}

function removeEdgeDirect(edgeId) {
    const edge = edges.get(edgeId);
    if (!edge) {
        showToast('Edge not found', 'error');
        return;
    }
    
    edges.remove({ id: edgeId });
    updateNodeSizes();
    updateEdgeSelectOptions();
    stabilizeNetwork();
    saveStateAfterOperation();
    showToast('Edge removed', 'success');
    hideEdgeContextMenu();
}

// show context menu on right click
function showContextMenu(x, y, value, nodeIds, type) {
    const nodesArray = Array.isArray(nodeIds) ? nodeIds : [nodeIds];
    const menu = document.getElementById('contextMenu');
    const isMultiple = nodesArray.length > 1;
    
    let menuHtml = `
        <button onclick="deleteNodes([${nodesArray.join(',')}])">Delete ${isMultiple ? 'Selected Nodes' : 'Node'}</button>
        ${isMultiple ? '' : `<button onclick="startLinkCreation(${nodesArray[0]})">Create Link From Here</button>`}
        ${isMultiple ? '' : `<button onclick="showPropertiesPanel(${nodesArray[0]}); hideContextMenu();">View Node Properties</button>`}
        ${isMultiple ? '' : `<button onclick="editNodeNotes(${nodesArray[0]}); hideContextMenu();">Add/Edit Notes</button>`}
    `;
    
    // Check if all selected nodes are of the same type
    const allSameType = nodesArray.every(id => {
        const node = nodes.get(id);
        return node && node.type === type;
    });
    
    if (allSameType) {
        if (type === 'ip') {
            menuHtml += `
                <button onclick="throttledEnrichIPMultiple([${nodesArray.map(id => `'${nodes.get(id).ip}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])">Enrich via IPINFO</button>
                <button onclick="throttledEnrichShodanMultiple([${nodesArray.map(id => `'${nodes.get(id).ip}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])">Enrich via Shodan</button>
                <button onclick="throttledEnrichInternetDBMultiple([${nodesArray.map(id => `'${nodes.get(id).ip}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])">Enrich via InternetDB</button>
                <button onclick="throttledEnrichGreyNoiseMultiple([${nodesArray.map(id => `'${nodes.get(id).ip}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])">Enrich via GreyNoise</button>
                <button onclick="throttledSendHttpsRequestMultiple([${nodesArray.map(id => `'${nodes.get(id).ip}'`).join(',')}], 'ip', 'https')">Send HTTPS Request</button>
                <button onclick="throttledSendHttpsRequestMultiple([${nodesArray.map(id => `'${nodes.get(id).ip}'`).join(',')}], 'ip', 'http')">Send HTTP Request</button>
                <button onclick="throttledEnrichURLscan('${nodes.get(nodesArray[0]).ip}', ${nodesArray[0]})">Enrich via URLscan.io</button>
            `;
        } else if (type === 'domain') {
            menuHtml += `
                <button onclick="throttledEnrichGoogleDNSMultiple([${nodesArray.map(id => `'${nodes.get(id).domain}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])">Enrich via Google DNS</button>
                <button onclick="throttledEnrichGoogleDNSMXMultiple([${nodesArray.map(id => `'${nodes.get(id).domain}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])">Enrich via Google DNS (MX)</button>
                <button onclick="throttledEnrichGoogleDNSTXTMultiple([${nodesArray.map(id => `'${nodes.get(id).domain}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])">Enrich via Google DNS (TXT)</button>
                <button onclick="throttledEnrichShodanMultiple([${nodesArray.map(id => `'${nodes.get(id).domain}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])">Enrich via Shodan</button>
                <button onclick="throttledEnrichHudsonRockDomainMultiple([${nodesArray.map(id => `'${nodes.get(id).domain}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])">Enrich via Hudson Rock</button>
                <button onclick="throttledSendHttpsRequestMultiple([${nodesArray.map(id => `'${nodes.get(id).domain}'`).join(',')}], 'domain', 'https')">Send HTTPS Request</button>
                <button onclick="throttledSendHttpsRequestMultiple([${nodesArray.map(id => `'${nodes.get(id).domain}'`).join(',')}], 'domain', 'http')">Send HTTP Request</button>
                <button onclick="throttledEnrichURLscan('https://${nodes.get(nodesArray[0]).domain}', ${nodesArray[0]})">Enrich via URLscan.io</button>
                <button onclick="throttledEnrichSecurityTrailsDomainMultiple([${nodesArray.map(id => `'${nodes.get(id).domain}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])">Enrich via SecurityTrails</button>
            `;
        } else if (type === 'html_hash') {
            menuHtml += `
                <button onclick="throttledSearchShodanHtmlHash('${nodesArray[0]}', '${nodes.get(nodesArray[0]).hash}')">Search Shodan for IPs with this HTML Hash</button>
            `;
        } else if (type === 'contact') {
            menuHtml += `
                <button onclick="throttledEnrichHudsonRockMultiple([${nodesArray.map(id => `'${nodes.get(id).email}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])">Enrich via Hudson Rock</button>
            `;
        }
    } else if (type === 'url') {
            menuHtml += `
                <button onclick="throttledEnrichURLhausMultiple([${nodesArray.map(id => `'${nodes.get(id).url}'`).join(',')}], [${nodesArray.map(id => `'${id}'`).join(',')}])">Enrich via URLhaus</button>
            `;
        
    }

    menu.innerHTML = menuHtml;
    const canvasOffset = container.getBoundingClientRect();
    menu.style.left = `${x + canvasOffset.left}px`;
    menu.style.top = `${y + canvasOffset.top}px`;
    menu.style.display = 'block';
    document.addEventListener('click', hideContextMenu);
}


// why is this here?
let linkFromNode = null;
// set variable above - move to top

function saveState() {
    try {
        const state = {
            nodes: nodes.get().map(node => {
                // Base properties common to all nodes
                const nodeData = {
                    id: node.id,
                    type: node.type,
                    label: node.label,
                    title: node.title,
                    color: node.color,
                    x: node.x,
                    y: node.y,
                    size: node.size,
                    originalLabel: node.originalLabel,
                    notes: node.notes // Include notes for all types
                };
                if (node.type === 'subnet') {
                nodeData.subnet = node.subnet;
                nodeData.isPrivate = node.isPrivate;
            }
                // Add type-specific properties
                switch (node.type) {
                    case 'contact':
                        nodeData.name = node.name;
                        nodeData.email = node.email;
                        break;
                    case 'ip':
                        nodeData.ip = node.ip;
                        break;
                    case 'domain':
                        nodeData.domain = node.domain;
                        break;
                    case 'organization':
                        nodeData.organization = node.organization;
                        break;
                    case 'port':
                        nodeData.portType = node.portType;
                        nodeData.portNumber = node.portNumber;
                        break;
                    case 'wallet':
                        nodeData.address = node.address;
                        break;
                    case 'bank':
                        nodeData.accountNumber = node.accountNumber;
                        nodeData.sortCode = node.sortCode;
                        break;
                    case 'technology':
                        nodeData.techName = node.techName;
                        nodeData.techVersion = node.techVersion;
                        break;
                    case 'device':
                        nodeData.deviceCategory = node.deviceCategory;
                        nodeData.deviceName = node.deviceName;
                        break;
                    case 'malware':
                        nodeData.malwareName = node.malwareName;
                        nodeData.malwareType = node.malwareType;
                        break;
                    case 'vulnerability':
                        nodeData.vulnName = node.vulnName;
                        nodeData.cve = node.cve;
                        nodeData.url = node.url;
                        break;
                    case 'favicon':
                        nodeData.hash = node.hash;
                        nodeData.location = node.location;
                        break;
                    case 'http_hash':
                    case 'html_hash':
                    case 'ssl_hash':
                        nodeData.hash = node.hash;
                        break;
                    case 'asn':
                        nodeData.asn = node.asn;
                        break;
                    case 'city':
                        nodeData.city = node.city;
                        break;
                    case 'country':
                        nodeData.country = node.country;
                        break;
                    case 'os':
                        nodeData.os = node.os;
                        break;
                    case 'product':
                        nodeData.product = node.product;
                        break;
                    case 'http_title':
                        nodeData.title = node.title; // Already in base properties, but explicit for clarity
                        break;
                    case 'vpn':
                    case 'proxy':
                    case 'tor':
                    case 'relay':
                    case 'hosting':
                        nodeData.value = node.value;
                        break;
                    case 'tag':
                        nodeData.tag = node.tag;
                        break;
                    case 'cpe':
                        nodeData.cpe = node.cpe;
                        break;
                    case 'service':
                        nodeData.name = node.name;
                        break;
                    case 'timestamp':
                        nodeData.timestamp = node.timestamp;
                        break;
                    case 'url':
                        nodeData.url = node.url;
                        break;
                    case 'port_title':
                        nodeData.portType = node.portType;
                        nodeData.portNumber = node.portNumber;
                        nodeData.title = node.title;
                        break;
                    case 'hash': // Add this case for file hashes
                        nodeData.hash = node.hash;
                        nodeData.hashType = node.hashType;
                        break;
                    case 'txt':
                        nodeData.text = node.text;
                        break;
                    default:
                        console.warn(`Unhandled node type in saveState: ${node.type}`);
                }

                return nodeData;
            }),
            edges: edges.get().map(edge => ({
                id: edge.id,
                from: edge.from,
                to: edge.to,
                label: edge.label,
                originalLabel: edge.originalLabel
            })),
            nextId: nextId,
            isDarkMode: isDarkMode,
            isPhysicsPaused: isPhysicsPaused,
            nodeLabelsVisible: nodeLabelsVisible,
            edgeLabelsVisible: edgeLabelsVisible
        };

        localStorage.setItem('networkGraphState', JSON.stringify(state));
        console.log('State saved successfully');
    } catch (e) {
        console.error('Failed to save state:', e);
        showToast('Failed to save state: ' + e.message, 'error');
    }
}



const throttledSearchShodanHtmlHash = throttleRequest(async function searchShodanHtmlHash(htmlHashNodeId, htmlHash, signal) {
    if (!shodanApiKey) {
        showToast('Please set your Shodan API key in the "API Keys" tab first.', 'error');
        return;
    }

    // Validate htmlHash
    const hashNum = parseInt(htmlHash);
    if (!Number.isInteger(hashNum)) {
        showToast(`Invalid HTML Hash: ${htmlHash}. Must be an integer.`, 'error');
        return;
    }

    network.setOptions({ physics: { enabled: false } });
    showProgressBar();
    document.getElementById('progress-bar').textContent = `Searching Shodan for IPs with HTML Hash ${htmlHash.substring(0, 8)}...`;

    try {
        // Construct the URL to match the Bash script exactly
        const query = `http.html_hash:${hashNum}`; // No encoding here yet
        const baseUrl = `https://api.shodan.io/shodan/host/search?key=${shodanApiKey}&query=${query}`;
        const url = routeViaProxy ? `${corsProxyUrl}/${baseUrl}` : baseUrl;

        console.log('Search Shodan Base URL (before proxy):', baseUrl);
        console.log('Search Shodan Final URL:', url);
        console.log('routeViaProxy:', routeViaProxy, 'corsProxyUrl:', corsProxyUrl);

        const response = await fetch(url, {
            method: 'GET',
            signal: signal,
            headers: {
                'Accept': 'application/json' // Ensure JSON response
            }
        });

        if (!response.ok) {
            const errorText = await response.text();
            console.log('Search Shodan Status:', response.status, response.statusText);
            console.log('Search Shodan Response Body:', errorText);
            console.log('Request Headers:', Object.fromEntries(response.headers.entries()));
            throw new Error(`Failed to fetch Shodan data: ${response.statusText} - ${errorText}`);
        }

        const data = await response.json();
        console.log('Shodan Response:', data);

        if (!data.matches || data.matches.length === 0) {
            showToast(`No IPs found on Shodan with HTML Hash ${htmlHash}`, 'info');
            completeProgressBar();
            await stabilizeNetwork();
            return;
        }

        // Process the matches
        const newNodes = [];
        const newEdges = [];
        let successfulAdditions = 0;
        const totalMatches = data.matches.length;

        const existingIPs = new Map(nodes.get({ filter: n => n.type === 'ip' }).map(n => [n.ip, n.id]));

        for (const match of data.matches) {
            if (activeTaskController && activeTaskController.signal.aborted) {
                showToast('Shodan HTML Hash search stopped', 'info');
                break;
            }

            const ip = match.ip_str;
            let ipId = existingIPs.get(ip);

            if (!ipId) {
                ipId = nextId++;
                newNodes.push({
                    id: ipId,
                    type: 'ip',
                    label: `IP: ${ip}`,
                    title: `IP Address: ${ip}\nFrom Shodan HTML Hash Search`,
                    color: { background: '#f87171' },
                    ip: ip,
                    size: 20
                });
                existingIPs.set(ip, ipId);
            }

            const edgeId = `${ipId}-${htmlHashNodeId}-SharesHash`;
            if (!edges.get(edgeId) && !newEdges.some(e => e.id === edgeId)) {
                newEdges.push({
                    id: edgeId,
                    from: ipId,
                    to: htmlHashNodeId,
                    label: 'Shares HTML Hash'
                });
            }

            successfulAdditions++;
            const progress = ((successfulAdditions / totalMatches) * 100).toFixed(1);
            document.getElementById('progress-bar').textContent = `Shodan HTML Hash Search: ${successfulAdditions}/${totalMatches} IPs (${progress}%)`;
        }

        if (newNodes.length > 0) nodes.add(newNodes);
        if (newEdges.length > 0) edges.add(newEdges);

        updateNodeSizes();
        updateSelectOptions();
        await stabilizeNetwork();
        //ensureInteractionSettings();

        completeProgressBar();
        showToast(`Found and added ${successfulAdditions} IPs with HTML Hash ${htmlHash.substring(0, 8)}...`, 'success');
    } catch (error) {
        if (error.name === 'AbortError') {
            showToast('Shodan HTML Hash search was cancelled', 'info');
        } else {
            console.error(`Error searching Shodan for HTML Hash ${htmlHash}: ${error.message}`);
            showToast(`Error searching Shodan: ${error.message}. Check CORS proxy in Config tab.`, 'error');
        }
        completeProgressBar();
        await stabilizeNetwork();
    }
}, SHODAN_RATE_LIMIT_MS);



function cancelLinkCreation() {
    if (linkFromNode) {
        // Reset visual feedback
        const originalNode = nodes.get(linkFromNode);
        nodes.update({
            id: linkFromNode,
            color: { border: isDarkMode ? '#94a3b8' : '#6b7280', background: originalNode.color.background }
        });
        
        showToast('Link creation cancelled', 'info');
        linkFromNode = null;
        
        // Remove temporary listeners
        network.off('oncontext', handleLinkDestination);
        network.off('click', cancelLinkCreation);
    }
}


        function hideContextMenu() {
            document.getElementById('contextMenu').style.display = 'none';
            document.removeEventListener('click', hideContextMenu);
        }

        const throttledSendHttpsRequest = throttleRequest(async function sendHttpsRequest(target, type) {
    network.setOptions({ physics: { enabled: false } });
    let url = type === 'ip' ? 
        `https://${target}` : 
        `https://${target}`;
    url = constructUrl(url);
    let message;

    try {
        const response = await fetch(url, {
            method: 'GET',
            headers: {
                'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
                'Origin': window.location.origin
            },
            mode: 'cors',
            credentials: 'omit'
        });

        const status = response.status;
        const statusText = response.statusText;
        const headers = {};
        response.headers.forEach((value, key) => {
            headers[key] = value;
        });

        let body = '';
        const contentType = headers['content-type'] || '';
        if (contentType.includes('text') || contentType.includes('html') || contentType.includes('json')) {
            body = await response.text();
            if (body.length > 500) {
                body = body.substring(0, 500) + '... (truncated)';
            }
        } else {
            body = '(Binary or unsupported content type)';
        }

        message = `
            HTTPS Request to https://${target}
            Status: ${status} ${statusText}
            Headers: ${JSON.stringify(headers, null, 2)}
            Body: ${body}
        `.trim();
        showToast(message, 'success');
    } catch (error) {
        message = `
            HTTPS Request to https://${target}
            Error: ${error.message}
            Note: Ensure the CORS proxy (${corsProxyUrl}) is active and correctly configured
        `.trim();
        showToast(message, 'error');
    } finally {
        await stabilizeNetwork();
    }
});

        function saveCorsProxyUrl() {
    corsProxyUrl = document.getElementById('corsProxyUrl').value.trim();
    const storeProxy = document.getElementById('storeCorsProxy').checked;
    routeViaProxy = document.getElementById('routeViaProxy').checked;
    ignoreApiKeysViaProxy = document.getElementById('ignoreApiKeysViaProxy').checked;

    if (corsProxyUrl) {
        if (storeProxy) {
            localStorage.setItem('corsProxyUrl', corsProxyUrl);
            localStorage.setItem('routeViaProxy', routeViaProxy);
            localStorage.setItem('ignoreApiKeysViaProxy', ignoreApiKeysViaProxy);
            showToast('CORS Proxy settings saved successfully!', 'success');
        } else {
            localStorage.removeItem('corsProxyUrl');
            localStorage.removeItem('routeViaProxy');
            localStorage.removeItem('ignoreApiKeysViaProxy');
            showToast('CORS Proxy settings set for this session only', 'success');
        }
    } else {
        corsProxyUrl = 'http://localhost:3000/proxy?url=';
        routeViaProxy = false;
        ignoreApiKeysViaProxy = false;
        localStorage.removeItem('corsProxyUrl');
        localStorage.removeItem('routeViaProxy');
        localStorage.removeItem('ignoreApiKeysViaProxy');
        document.getElementById('corsProxyUrl').value = corsProxyUrl;
        document.getElementById('routeViaProxy').checked = false;
        document.getElementById('ignoreApiKeysViaProxy').checked = false;
        showToast('CORS Proxy settings reset to default', 'info');
    }
}

function constructUrl(baseUrl, useApiKey = true) {
    if (routeViaProxy) {
        // Ensure corsProxyUrl doesn't end with a slash and baseUrl doesn't start with one
        const cleanProxyUrl = corsProxyUrl.replace(/\/+$/, ''); // Remove trailing slashes
        const cleanBaseUrl = baseUrl.replace(/^\/+/, '');      // Remove leading slashes
        return `${cleanProxyUrl}${cleanBaseUrl}`;
    }
    return useApiKey ? baseUrl : baseUrl.split('?')[0]; // Remove query params if ignoring API keys
}

 function showToast(message, type = 'info') {
    console.log('Showing toast:', message, type);
    const topBarHeight = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--top-bar-height')) || 40;
    const buffer = 20;
    const toastOptions = {
        text: message,
        duration: 6500,
        position: "center",
        style: {
            background: isDarkMode ? '#2d3748' : '#fff',
            color: isDarkMode ? '#e2e8f0' : '#1f2a44',
            border: `1px solid ${isDarkMode ? '#4b5563' : '#d1d5db'}`,
            boxShadow: isDarkMode ? '0 2px 10px rgba(0, 0, 0, 0.3)' : '0 2px 10px rgba(0, 0, 0, 0.1)',
            position: 'fixed',
            top: `${topBarHeight + buffer}px`,
            left: '50%',
            transform: 'translateX(-50%)',
            width: 'auto',
            maxWidth: '80%',
            zIndex: 3000
        }
    };
    switch (type) {
        case 'success': toastOptions.style.background = isDarkMode ? '#166534' : '#22c55e'; toastOptions.style.color = '#fff'; break;
        case 'error': toastOptions.style.background = isDarkMode ? '#991b1b' : '#ef4444'; toastOptions.style.color = '#fff'; break;
    }
    const toast = Toastify(toastOptions);
    console.log('Toast instance created:', toast);
    setTimeout(() => {
        toast.showToast();
        console.log('Toast should now be visible');
    }, 100); // Small delay to ensure rendering
}

async function stabilizeNetwork(skipFit = false) {
    return new Promise(resolve => {
        network.setOptions({
            physics: {
                enabled: true,
                stabilization: {
                    enabled: true,
                    iterations: 200,
                    updateInterval: 50
                },
                barnesHut: {
                    gravitationalConstant: -8000,
                    centralGravity: 0.3,
                    springLength: 150,
                    avoidOverlap: 1.0,
                    damping: 0.9
                }
            },
            // Explicitly preserve interaction settings
            interaction: { 
                ...baseInteractionOptions,
                zoomView: true,  // Ensure zooming is enabled
                //dragView: true,
                zoomSpeed: 0.5
            }
        });

        network.stabilize(200);
        network.once('stabilizationIterationsDone', () => {
            network.setOptions({
                physics: {
                    enabled: !isPhysicsPaused,
                    stabilization: { enabled: false }
                },
                interaction: { 
                    ...baseInteractionOptions,
                    zoomView: true,
                    //dragView: true,
                    zoomSpeed: 0.5
                }
            });

            if (!skipFit) {
                network.fit({
                    animation: {
                        duration: 500,
                        easingFunction: 'easeInOutQuad'
                    }
                });
            }

            setTimeout(() => {
                const boundingBox = network.getBoundingBox();
                if (boundingBox && nodes.length > 0) {
                    const margin = 50;
                    nodes.forEach(node => {
                        const { x, y } = network.getPositions([node.id])[node.id] || { x: 0, y: 0 };
                        nodes.update({
                            id: node.id,
                            x: Math.max(boundingBox.left + margin, Math.min(boundingBox.right - margin, x)),
                            y: Math.max(boundingBox.top + margin, Math.min(boundingBox.bottom - margin, y))
                        });
                    });
                }
                resolve();
                
            }, skipFit ? 0 : 550);
            ensureInteractionSettings(); // Ensure panning is enabled
        });
    });
}



const throttledEnrichInternetDB = throttleRequest(async function enrichInternetDB(ip, ipNodeId, isBulk = false) {
    network.setOptions({ physics: { enabled: false } });
    try {
        const url = constructUrl(`https://internetdb.shodan.io/${ip}`);
        const response = await fetch(url);
        if (!response.ok) throw new Error('Failed to fetch InternetDB data');
        const data = await response.json();

        // Deduplication maps
        const existingPorts = new Map(nodes.get({ filter: n => n.type === 'port' }).map(n => [`${n.portType}/${n.portNumber}`, n.id]));
        const existingDomains = new Map(nodes.get({ filter: n => n.type === 'domain' }).map(n => [n.domain, n.id]));
        const existingVulns = new Map(nodes.get({ filter: n => n.type === 'vulnerability' }).map(n => [n.cve, n.id]));
        const existingTags = new Map(nodes.get({ filter: n => n.type === 'tag' }).map(n => [n.tag, n.id]));
        const existingCPEs = new Map(nodes.get({ filter: n => n.type === 'cpe' }).map(n => [n.cpe, n.id]));

        const newNodes = [];
        const newEdges = [];

        // Ports (existing logic with deduplication)
        if (data.ports && data.ports.length > 0) {
            data.ports.forEach(port => {
                const portKey = `TCP/${port}`;
                let portId = existingPorts.get(portKey);
                if (!portId) {
                    portId = nextId++;
                    newNodes.push({ 
                        id: portId, 
                        type: 'port', 
                        label: `TCP/${port}`, 
                        title: `Port\nType: TCP\nNumber: ${port}`, 
                        color: { background: '#a78bfa' }, 
                        portType: 'TCP', 
                        portNumber: port.toString() 
                    });
                    existingPorts.set(portKey, portId);
                }
                const edgeId = `${ipNodeId}-${portId}-Exposes`;
                if (!edges.get(edgeId) && !newEdges.some(e => e.id === edgeId)) {
                    newEdges.push({ id: edgeId, from: ipNodeId, to: portId, label: 'Exposes' });
                }
            });
        }

        // Hostnames (existing logic with deduplication)
        if (data.hostnames && data.hostnames.length > 0) {
            data.hostnames.forEach(hostname => {
                let domainId = existingDomains.get(hostname);
                if (!domainId) {
                    domainId = nextId++;
                    newNodes.push({ 
                        id: domainId, 
                        type: 'domain', 
                        label: hostname, 
                        title: `Domain: ${hostname}`, 
                        color: { background: '#60a5fa' }, 
                        domain: hostname 
                    });
                    existingDomains.set(hostname, domainId);
                }
                const edgeId = `${ipNodeId}-${domainId}-ResolvesTo`;
                if (!edges.get(edgeId) && !newEdges.some(e => e.id === edgeId)) {
                    newEdges.push({ id: edgeId, from: ipNodeId, to: domainId, label: 'Resolves to' });
                }
            });
        }

        // Vulnerabilities (existing logic with deduplication)
        if (data.cves && data.cves.length > 0) {
            data.cves.forEach(cve => {
                let cveId = existingVulns.get(cve);
                if (!cveId) {
                    cveId = nextId++;
                    newNodes.push({ 
                        id: cveId, 
                        type: 'vulnerability', 
                        label: `Vulnerability: ${cve}`, 
                        title: `Vulnerability\nName: ${cve}\nCVE: ${cve}`, 
                        color: { background: '#dc2626' }, 
                        vulnName: cve, 
                        cve: cve,
                        url: `https://nvd.nist.gov/vuln/detail/${cve}`
                    });
                    existingVulns.set(cve, cveId);
                }
                const edgeId = `${ipNodeId}-${cveId}-Has`;
                if (!edges.get(edgeId) && !newEdges.some(e => e.id === edgeId)) {
                    newEdges.push({ id: edgeId, from: ipNodeId, to: cveId, label: 'Has' });
                }
            });
        }

        // Tags (new)
        if (data.tags && data.tags.length > 0) {
            data.tags.forEach(tag => {
                let tagId = existingTags.get(tag);
                if (!tagId) {
                    tagId = nextId++;
                    newNodes.push({
                        id: tagId,
                        type: 'tag',
                        label: `Tag: ${tag}`,
                        title: `Tag: ${tag}`,
                        color: { background: '#6d28d9' },
                        tag: tag
                    });
                    existingTags.set(tag, tagId);
                }
                const edgeId = `${ipNodeId}-${tagId}-Tagged`;
                if (!edges.get(edgeId) && !newEdges.some(e => e.id === edgeId)) {
                    newEdges.push({ id: edgeId, from: ipNodeId, to: tagId, label: 'Tagged' });
                }
            });
        }

        // CPEs (new)
        if (data.cpes && data.cpes.length > 0) {
            data.cpes.forEach(cpe => {
                let cpeId = existingCPEs.get(cpe);
                if (!cpeId) {
                    cpeId = nextId++;
                    newNodes.push({
                        id: cpeId,
                        type: 'cpe',
                        label: `CPE: ${cpe.split(':')[3] || cpe}`,
                        title: `CPE: ${cpe}`,
                        color: { background: '#0d9488' },
                        cpe: cpe
                    });
                    existingCPEs.set(cpe, cpeId);
                }
                const edgeId = `${ipNodeId}-${cpeId}-Runs`;
                if (!edges.get(edgeId) && !newEdges.some(e => e.id === edgeId)) {
                    newEdges.push({ id: edgeId, from: ipNodeId, to: cpeId, label: 'Runs' });
                }
            });
        }

        // Batch update
        if (newNodes.length > 0) nodes.add(newNodes);
        if (newEdges.length > 0) edges.add(newEdges);

        updateNodeSizes();
        updateSelectOptions();
        await stabilizeNetwork();
        //ensureInteractionSettings();
        if (!isBulk) showToast(`IP ${ip} enrichment completed using InternetDB`, 'success');
    } catch (error) {
        console.error(`Error enriching IP ${ip} with InternetDB: ${error.message}`);
        showToast(`Error enriching IP ${ip} with InternetDB: ${error.message}`, 'error');
        await stabilizeNetwork();
    }
});

 
async function enrichAllShodan() {
    if (!shodanApiKey && !ignoreApiKeysViaProxy) { 
        showToast('Please set your Shodan API key in the "API Keys" tab first.', 'error'); 
        return; 
    }
    
    showProgressBar();
    const progressBar = document.getElementById('progress-bar');
    network.setOptions({ physics: { enabled: false } });
    const ipNodes = nodes.get({ filter: n => n.type === 'ip' && n.ip });
    const totalIPs = ipNodes.length;
    let successfulEnrichments = 0;
    
    if (totalIPs === 0) {
        showToast('No IP nodes found to enrich', 'info');
        completeProgressBar();
        return;
    }
    
    console.log(`Found ${totalIPs} IP nodes to enrich with Shodan`);
    showToast(`Starting Shodan enrichment for ${totalIPs} IPs`, 'info');
    
    const batchSize = 5; // Smaller batch size to respect Shodan rate limits
    const delayBetweenBatches = 100; // Small delay between batches
    const shodanDelayMs = SHODAN_RATE_LIMIT_MS; // 1-second delay per request
    const totalBatches = Math.ceil(totalIPs / batchSize);
    const timePerBatchMs = shodanDelayMs * batchSize;
    const totalBatchDelays = (totalBatches - 1) * delayBetweenBatches;
    const estimatedTimeMs = (timePerBatchMs * totalBatches) + totalBatchDelays + 1000;
    
    const estimatedSeconds = Math.ceil(estimatedTimeMs / 1000);
    const estimatedMinutes = Math.floor(estimatedSeconds / 60);
    const remainingSeconds = estimatedSeconds % 60;
    const timeEstimateStr = estimatedMinutes > 0 
        ? `${estimatedMinutes}m ${remainingSeconds}s` 
        : `${estimatedSeconds}s`;
    
    showToast(`Estimated time for Shodan enrichment: ~${timeEstimateStr}`, 'info');
    progressBar.textContent = `Shodan Enrichment: 0/${totalIPs} IPs (0%) - Est. ${timeEstimateStr}`;
    
    async function processBatch(batch) {
        for (const node of batch) {
            if (activeTaskController && activeTaskController.signal.aborted) {
                return false;
            }
            try {
                const baseUrl = ignoreApiKeysViaProxy ?
                    `https://api.shodan.io/shodan/host/${node.ip}` :
                    `https://api.shodan.io/shodan/host/${node.ip}?key=${shodanApiKey}`;
                const url = constructUrl(baseUrl, !ignoreApiKeysViaProxy);
                const response = await fetch(url, { signal: activeTaskController?.signal });
                if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
                const data = await response.json();
                console.log(`Shodan response for ${node.ip}:`, JSON.stringify(data, null, 2));

                // Use the same processing logic as right-click enrichment
                await processShodanData(node.id, data);
                
                successfulEnrichments++;
            } catch (error) {
                if (error.name === 'AbortError') {
                    return false;
                }
                console.error(`Failed to enrich IP ${node.ip}: ${error.message}`);
                showToast(`Failed to enrich IP ${node.ip}: ${error.message}`, 'error');
            }
            await new Promise(resolve => setTimeout(resolve, shodanDelayMs)); // Respect Shodan rate limit
        }
        return true;
    }
    
    let lastProgressUpdate = 0;
    const progressUpdateInterval = 500;
    const startTime = Date.now();

    try {
        for (let i = 0; i < totalIPs; i += batchSize) {
            if (activeTaskController && activeTaskController.signal.aborted) {
                showToast('Shodan enrichment stopped', 'info');
                progressBar.textContent = `Shodan Enrichment: Stopped at ${successfulEnrichments}/${totalIPs} IPs`;
                break;
            }
            
            const batch = ipNodes.slice(i, Math.min(i + batchSize, totalIPs));
            console.log(`Processing batch ${Math.floor(i / batchSize) + 1} of ${totalBatches}, IPs ${i} to ${Math.min(i + batchSize - 1, totalIPs - 1)}`);
            
            const batchSuccess = await processBatch(batch);
            
            if (batchSuccess) {
                updateNodeSizes();
                updateSelectOptions();
            }
            
            const currentTime = Date.now();
            const processedIPs = Math.min(i + batchSize, totalIPs);
            const progress = ((processedIPs / totalIPs) * 100).toFixed(1);
            
            if (currentTime - lastProgressUpdate >= progressUpdateInterval || processedIPs === totalIPs) {
                const elapsedTimeMs = currentTime - startTime;
                const timePerIp = successfulEnrichments > 0 ? elapsedTimeMs / successfulEnrichments : shodanDelayMs;
                const remainingIPs = totalIPs - successfulEnrichments;
                const remainingTimeMs = remainingIPs * timePerIp;
                const remainingSeconds = Math.ceil(remainingTimeMs / 1000);
                const remainingMinutes = Math.floor(remainingSeconds / 60);
                const remainingSecondsPart = remainingSeconds % 60;
                const remainingTimeStr = remainingMinutes > 0 
                    ? `${remainingMinutes}m ${remainingSecondsPart}s` 
                    : `${remainingSeconds}s`;
                
                progressBar.textContent = `Shodan Enrichment: ${successfulEnrichments}/${totalIPs} IPs (${progress}%) - Est. ${remainingTimeStr} remaining`;
                lastProgressUpdate = currentTime;
            }
            
            await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));
        }
    } finally {
        updateNodeSizes();
        updateSelectOptions();
        updateTheme();
        
        await stabilizeNetwork();
        //ensureInteractionSettings();

        if (!(activeTaskController && activeTaskController.signal.aborted)) {
            completeProgressBar();
            showToast(`Shodan enrichment completed: ${successfulEnrichments}/${totalIPs} IPs enriched`, 'success');
        } else {
            showToast(`Shodan enrichment stopped: ${successfulEnrichments}/${totalIPs} IPs processed`, 'info');
        }
        
        if (window.innerWidth <= 768) {
            const controls = document.getElementById('controls');
            controls.classList.add('collapsed');
            document.getElementById('myNetwork').style.display = 'block';
            network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });
        }
    }
}

async function enrichAllInternetDB() {
    showProgressBar();
    network.setOptions({ physics: { enabled: false } });
    const ipNodes = nodes.get({ filter: n => n.type === 'ip' && n.ip });
    const totalIPs = ipNodes.length;
    let successfulEnrichments = 0;
    
    console.log(`Found ${totalIPs} IP nodes to enrich with InternetDB`);
    showToast(`Starting InternetDB enrichment for ${totalIPs} IPs`, 'info');
    
    const batchSize = 50;
    const delayBetweenBatches = 200;
    const totalBatches = Math.ceil(totalIPs / batchSize);
    const assumedRequestTimeMs = 100;
    const timePerBatchMs = assumedRequestTimeMs;
    const totalBatchDelays = (totalBatches - 1) * delayBetweenBatches;
    const estimatedTimeMs = (timePerBatchMs * totalBatches) + totalBatchDelays + 1000;
    
    const estimatedSeconds = Math.ceil(estimatedTimeMs / 1000);
    const estimatedMinutes = Math.floor(estimatedSeconds / 60);
    const remainingSeconds = estimatedSeconds % 60;
    const timeEstimateStr = estimatedMinutes > 0 
        ? `${estimatedMinutes}m ${remainingSeconds}s` 
        : `${estimatedSeconds}s`;
    
    showToast(`Estimated time for InternetDB enrichment: ~${timeEstimateStr}`, 'info');
    document.getElementById('progress-bar').textContent = `InternetDB Enrichment: 0/${totalIPs} IPs (0%) - Est. ${timeEstimateStr}`;
    
    // Deduplication maps
    const existingPorts = new Map(nodes.get({ filter: n => n.type === 'port' }).map(n => [`${n.portType}/${n.portNumber}`, n.id]));
    const existingDomains = new Map(nodes.get({ filter: n => n.type === 'domain' }).map(n => [n.domain, n.id]));
    const existingVulns = new Map(nodes.get({ filter: n => n.type === 'vulnerability' }).map(n => [n.cve, n.id]));
    const existingTags = new Map(nodes.get({ filter: n => n.type === 'tag' }).map(n => [n.tag, n.id]));
    const existingCPEs = new Map(nodes.get({ filter: n => n.type === 'cpe' }).map(n => [n.cpe, n.id]));
    
    const newNodes = [];
    const newEdges = [];
    
    async function processBatch(batch) {
        const promises = batch.map(node => {
            if (activeTaskController && activeTaskController.signal.aborted) {
                return Promise.resolve(null);
            }
            const url = constructUrl(`https://internetdb.shodan.io/${node.ip}`);
            return fetch(url)
                .then(response => {
                    if (!response.ok) throw new Error('Failed to fetch InternetDB data');
                    return response.json();
                })
                .then(data => {
                    // Ports
                    if (data.ports && data.ports.length > 0) {
                        data.ports.forEach(port => {
                            const portKey = `TCP/${port}`;
                            let portId = existingPorts.get(portKey);
                            if (!portId) {
                                portId = nextId++;
                                newNodes.push({
                                    id: portId,
                                    type: 'port',
                                    label: `TCP/${port}`,
                                    title: `Port\nType: TCP\nNumber: ${port}`,
                                    color: { background: '#a78bfa' },
                                    portType: 'TCP',
                                    portNumber: port.toString()
                                });
                                existingPorts.set(portKey, portId);
                            }
                            const edgeId = `${node.id}-${portId}-Exposes`;
                            if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {
                                newEdges.push({ id: edgeId, from: node.id, to: portId, label: 'Exposes' });
                            }
                        });
                    }

                    // Hostnames
                    if (data.hostnames && data.hostnames.length > 0) {
                        data.hostnames.forEach(hostname => {
                            let domainId = existingDomains.get(hostname);
                            if (!domainId) {
                                domainId = nextId++;
                                newNodes.push({
                                    id: domainId,
                                    type: 'domain',
                                    label: `Domain: ${hostname}`,
                                    title: `Domain: ${hostname}`,
                                    color: { background: '#60a5fa' },
                                    domain: hostname
                                });
                                existingDomains.set(hostname, domainId);
                            }
                            const edgeId = `${node.id}-${domainId}-ResolvesTo`;
                            if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {
                                newEdges.push({ id: edgeId, from: node.id, to: domainId, label: 'Resolves to' });
                            }
                        });
                    }

                    // Vulnerabilities
                    if (data.cves && data.cves.length > 0) {
                        data.cves.forEach(cve => {
                            let cveId = existingVulns.get(cve);
                            if (!cveId) {
                                cveId = nextId++;
                                newNodes.push({
                                    id: cveId,
                                    type: 'vulnerability',
                                    label: `Vulnerability: ${cve}`,
                                    title: `Vulnerability\nName: ${cve}\nCVE: ${cve}`,
                                    color: { background: '#dc2626' },
                                    vulnName: cve,
                                    cve: cve,
                                    url: `https://nvd.nist.gov/vuln/detail/${cve}`
                                });
                                existingVulns.set(cve, cveId);
                            }
                            const edgeId = `${node.id}-${cveId}-Has`;
                            if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {
                                newEdges.push({ id: edgeId, from: node.id, to: cveId, label: 'Has' });
                            }
                        });
                    }

                    // Tags
                    if (data.tags && data.tags.length > 0) {
                        data.tags.forEach(tag => {
                            let tagId = existingTags.get(tag);
                            if (!tagId) {
                                tagId = nextId++;
                                newNodes.push({
                                    id: tagId,
                                    type: 'tag',
                                    label: `Tag: ${tag}`,
                                    title: `Tag: ${tag}`,
                                    color: { background: '#6d28d9' },
                                    tag: tag
                                });
                                existingTags.set(tag, tagId);
                            }
                            const edgeId = `${node.id}-${tagId}-Tagged`;
                            if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {
                                newEdges.push({ id: edgeId, from: node.id, to: tagId, label: 'Tagged' });
                            }
                        });
                    }

                    // CPEs
                    if (data.cpes && data.cpes.length > 0) {
                        data.cpes.forEach(cpe => {
                            let cpeId = existingCPEs.get(cpe);
                            if (!cpeId) {
                                cpeId = nextId++;
                                newNodes.push({
                                    id: cpeId,
                                    type: 'cpe',
                                    label: `CPE: ${cpe.split(':')[3] || cpe}`,
                                    title: `CPE: ${cpe}`,
                                    color: { background: '#0d9488' },
                                    cpe: cpe
                                });
                                existingCPEs.set(cpe, cpeId);
                            }
                            const edgeId = `${node.id}-${cpeId}-Runs`;
                            if (!newEdges.some(e => e.id === edgeId) && !edges.get(edgeId)) {
                                newEdges.push({ id: edgeId, from: node.id, to: cpeId, label: 'Runs' });
                            }
                        });
                    }

                    successfulEnrichments++;
                })
                .catch(error => {
                    console.error(`Failed to enrich IP ${node.ip}: ${error.message}`);
                    showToast(`Failed to enrich IP ${node.ip}: ${error.message}`, 'error');
                    return null;
                });
        });
        await Promise.all(promises);
    }
    
    let lastProgressUpdate = 0;
    const progressUpdateInterval = 1000;
    const startTime = Date.now();
    
    for (let i = 0; i < totalIPs; i += batchSize) {
        if (activeTaskController && activeTaskController.signal.aborted) {
            showToast('InternetDB enrichment stopped', 'info');
            break;
        }
        
        const batch = ipNodes.slice(i, Math.min(i + batchSize, totalIPs));
        console.log(`Processing batch ${Math.floor(i / batchSize) + 1} of ${totalBatches}, IPs ${i} to ${Math.min(i + batchSize - 1, totalIPs - 1)}`);
        
        await processBatch(batch);
        
        if (newNodes.length > 0) {
            nodes.add(newNodes);
            newNodes.length = 0;
        }
        if (newEdges.length > 0) {
            edges.add(newEdges);
            newEdges.length = 0;
        }
        
        const currentTime = Date.now();
        if (currentTime - lastProgressUpdate >= progressUpdateInterval) {
            const processedIPs = Math.min(i + batchSize, totalIPs);
            const progress = ((processedIPs / totalIPs) * 100).toFixed(1);
            const remainingIPs = totalIPs - processedIPs;
            const remainingTimeMs = Math.max(0, remainingIPs * assumedRequestTimeMs);
            const remainingSeconds = Math.ceil(remainingTimeMs / 1000);
            const remainingMinutes = Math.floor(remainingSeconds / 60);
            const remainingSecondsPart = remainingSeconds % 60;
            const remainingTimeStr = remainingMinutes > 0 
                ? `${remainingMinutes}m ${remainingSecondsPart}s` 
                : `${remainingSeconds}s`;
            
            document.getElementById('progress-bar').textContent = 
                `InternetDB Enrichment: ${successfulEnrichments}/${totalIPs} IPs (${progress}%) - Est. ${remainingTimeStr} remaining`;
            lastProgressUpdate = currentTime;
            updateSelectOptions();
        }
        
        await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));
    }
    
    if (newNodes.length > 0) nodes.add(newNodes);
    if (newEdges.length > 0) edges.add(newEdges);
    updateNodeSizes();
    updateSelectOptions();
    await stabilizeNetwork();
    //ensureInteractionSettings();
    completeProgressBar();
    showToast(`InternetDB enrichment completed: ${successfulEnrichments}/${totalIPs} IPs enriched`, 'success');
    
    if (window.innerWidth <= 768) {
        const controls = document.getElementById('controls');
        controls.classList.add('collapsed');
        document.getElementById('myNetwork').style.display = 'block';
        network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });
    }
}

const throttledEnrichIP = throttleRequest(async function enrichIP(ip, ipNodeId, isBulk = false, signal) {
    if (!ipinfoApiKey && !ignoreApiKeysViaProxy) { 
        showToast('Please set your IPinfo API key in the "API Keys" tab first.', 'error'); 
        return; 
    }
    network.setOptions({ physics: { enabled: false } });
    try {
        const baseUrl = ignoreApiKeysViaProxy ? 
            `https://ipinfo.io/${ip}/json` : 
            `https://ipinfo.io/${ip}/json?token=${ipinfoApiKey}`;
        const url = constructUrl(baseUrl, !ignoreApiKeysViaProxy);
        const response = await fetch(url, { signal });
        if (!response.ok) throw new Error('Failed to fetch IP info');
        const data = await response.json();
        
        const asn = data.asn?.asn || 'Unknown ASN';
        const city = data.city || 'Unknown City';
        const companyName = data.company?.name || 'Unknown Company';
        const country = data.country || 'Unknown Country';
        const privacy = data.privacy || { vpn: false, proxy: false, tor: false, relay: false, hosting: false };

        const newNodes = [];
        const newEdges = [];
        
        // Pre-collect existing nodes for deduplication
        let existingAsns = new Map(nodes.get({ filter: n => n.type === 'asn' }).map(n => [n.asn, n.id]));
        let existingCities = new Map(nodes.get({ filter: n => n.type === 'city' }).map(n => [n.city, n.id]));
        let existingOrgs = new Map(nodes.get({ filter: n => n.type === 'organization' }).map(n => [n.organization, n.id]));
        let existingCountries = new Map(nodes.get({ filter: n => n.type === 'country' }).map(n => [n.country, n.id]));
        let existingPrivacyTypes = new Map([
            ['vpn', null], ['proxy', null], ['tor', null], ['relay', null], ['hosting', null]
        ].map(([type]) => {
            const existing = nodes.get({ filter: n => n.type === type })[0];
            return [type, existing ? existing.id : null];
        }));

        const privacyTypes = [
            { key: 'vpn', label: 'VPN', color: '#9333ea' },
            { key: 'proxy', label: 'Proxy', color: '#f43f5e' },
            { key: 'tor', label: 'Tor', color: '#64748b' },
            { key: 'relay', label: 'Relay', color: '#eab308' },
            { key: 'hosting', label: 'Hosting', color: '#14b8a6' }
        ];

        // Helper function to add node and edge with unique ID
        const addNodeAndEdge = (type, key, value, labelPrefix, title, color, edgeLabel) => {
            let targetId = existingAsns.get(value) || existingCities.get(value) || existingOrgs.get(value) || existingCountries.get(value);
            if (!targetId) {
                targetId = nextId++;
                newNodes.push({ 
                    id: targetId, 
                    type: type, 
                    label: `${labelPrefix}: ${value}`, 
                    title: title, 
                    color: { background: color }, 
                    [key]: value 
                });
                if (type === 'asn') existingAsns.set(value, targetId);
                else if (type === 'city') existingCities.set(value, targetId);
                else if (type === 'organization') existingOrgs.set(value, targetId);
                else if (type === 'country') existingCountries.set(value, targetId);
            }
            const edgeId = `${ipNodeId}-${targetId}-${edgeLabel}`;
            if (!edges.get(edgeId) && !newEdges.some(e => e.id === edgeId)) {
                newEdges.push({ id: edgeId, from: ipNodeId, to: targetId, label: edgeLabel });
            }
        };

        // Add nodes and edges
        addNodeAndEdge('asn', 'asn', asn, 'ASN', `ASN: ${asn}`, '#a3e635', 'Assigned to');
        addNodeAndEdge('city', 'city', city, 'City', `City: ${city}`, '#f97316', 'Located in');
        addNodeAndEdge('organization', 'organization', companyName, 'Organization', `Company: ${companyName}`, '#facc15', 'Belongs to');
        addNodeAndEdge('country', 'country', country, 'Country', `Country: ${country}`, '#34d399', 'Located in');

        // Privacy Types
        privacyTypes.forEach(privacyType => {
            if (privacy[privacyType.key]) {
                let privacyNodeId = existingPrivacyTypes.get(privacyType.key);
                if (!privacyNodeId) {
                    privacyNodeId = nextId++;
                    newNodes.push({ 
                        id: privacyNodeId, 
                        type: privacyType.key, 
                        label: privacyType.label, 
                        title: privacyType.label, 
                        color: { background: privacyType.color }
                    });
                    existingPrivacyTypes.set(privacyType.key, privacyNodeId);
                }
                const edgeId = `${ipNodeId}-${privacyNodeId}-Uses`;
                if (!edges.get(edgeId) && !newEdges.some(e => e.id === edgeId)) {
                    newEdges.push({ id: edgeId, from: ipNodeId, to: privacyNodeId, label: 'Uses' });
                }
            }
        });

        // Batch update
        if (newNodes.length > 0) nodes.add(newNodes);
        if (newEdges.length > 0) edges.add(newEdges);

        updateNodeSizes();
        updateSelectOptions();
        await stabilizeNetwork();
        if (!isBulk) showToast(`IP ${ip} enrichment completed using IPinfo`, 'success');
    } catch (error) {
        if (error.name === 'AbortError') {
            showToast(`Enrichment of IP ${ip} was cancelled`, 'info');
            return;
        }
        console.error(`Error enriching IP ${ip}: ${error.message}`);
        showToast(`Error enriching IP ${ip}: ${error.message}`, 'error');
        await stabilizeNetwork();
    }
}, RATE_LIMIT_MS);


const throttledEnrichShodan = throttleRequest(async function enrichShodan(ip, ipNodeId, isBulk = false, signal) {
    if (!shodanApiKey && !ignoreApiKeysViaProxy) { 
        showToast('Please set your Shodan API key in the "API Keys" tab first.', 'error'); 
        return; 
    }
    
    if (!isBulk) network.setOptions({ physics: { enabled: false } });
    
    try {
        const baseUrl = ignoreApiKeysViaProxy ? 
            `https://api.shodan.io/shodan/host/${ip}` : 
            `https://api.shodan.io/shodan/host/${ip}?key=${shodanApiKey}`;
        const url = constructUrl(baseUrl, !ignoreApiKeysViaProxy);
        const response = await fetch(url, { signal });
        if (!response.ok) throw new Error(`Failed to fetch Shodan data: ${response.statusText}`);
        const data = await response.json();

        // Process Shodan data using the shared helper
        await processShodanData(ipNodeId, data);

        updateNodeSizes();
        updateSelectOptions();
        if (!isBulk) {
            await stabilizeNetwork();
            updateTheme();
            showToast(`IP ${ip} enrichment completed using Shodan`, 'success');
        }
    } catch (error) {
        if (error.name === 'AbortError') {
            showToast(`Enrichment of IP ${ip} was cancelled`, 'info');
            return;
        }
        console.error(`Error enriching IP ${ip} with Shodan: ${error.message}`);
        showToast(`Error enriching IP ${ip} with Shodan: ${error.message}`, 'error');
        if (!isBulk) await stabilizeNetwork();
    }
}, SHODAN_RATE_LIMIT_MS);



async function importIOCsFromText() {
    let text = document.getElementById('iocText').value.trim();
    if (!text) { 
        showToast('Please enter some text containing IOCs', 'error'); 
        return; 
    }
    // Apply comprehensive refanging
    text = refangText(text);
    await processIOCs(text);
    document.getElementById('iocText').value = '';
    saveStateAfterOperation();
    showToast('IOCs (including emails) import completed', 'success');
}

async function importIOCsFromFile() {
    const fileInput = document.getElementById('iocFile');
    const file = fileInput.files[0];
    if (!file) { 
        showToast('Please select a text file containing IOCs', 'error'); 
        return; 
    }
    const reader = new FileReader();
    reader.onload = async function(event) { 
        // Apply comprehensive refanging
        let text = refangText(event.target.result);
        await processIOCs(text); 
        fileInput.value = ''; 
        saveStateAfterOperation();
        showToast('IOCs (including emails) import completed', 'success');
    };
    reader.readAsText(file);
}




function toggleMode() {
    isDarkMode = !isDarkMode;
    updateTheme();
    document.getElementById('mode-toggle').textContent = isDarkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode';
}

        function togglePhysics() {
    isPhysicsPaused = !isPhysicsPaused;
    const pauseButton = document.getElementById('pause-toggle');
    network.setOptions({ 
        physics: { 
            enabled: !isPhysicsPaused,
            barnesHut: {
                // Ensure consistent physics settings
                gravitationalConstant: -8000,
                centralGravity: 0.1,
                springLength: 200,
                springConstant: 0.04,
                damping: 0.9,
                avoidOverlap: 0.5
            },
            maxVelocity: 25,
            minVelocity: 0.1
        },
        interaction: { ...baseInteractionOptions }
    });
    pauseButton.textContent = isPhysicsPaused ? 'Resume Physics' : 'Pause Physics';
    pauseButton.classList.toggle('paused', isPhysicsPaused);
    if (!isPhysicsPaused) {
        // Trigger stabilization when resuming physics
        stabilizeNetwork();
    }
    //ensureInteractionSettings();
    }

        function resetLayout() {
            nodes.forEach(node => nodes.update({ id: node.id, x: undefined, y: undefined }));
            stabilizeNetwork(false);
        }

        function setOrganicLayout() {
    network.setOptions({
        physics: {
            enabled: true,
            stabilization: {
                enabled: true,
                iterations: 200
            },
            barnesHut: {
                gravitationalConstant: -8000,
                centralGravity: 0.3,
                springLength: 150,
                springConstant: 0.05,
                damping: 0.9,
                avoidOverlap: 1.0
            },
            maxVelocity: 50,
            minVelocity: 0.1
        },
        layout: { 
            hierarchical: false,
            improvedLayout: false
        },
        interaction: { ...baseInteractionOptions }
    });

    // Reset node positions
    nodes.forEach(node => {
        nodes.update({ 
            id: node.id, 
            x: undefined, 
            y: undefined,
            fixed: { x: false, y: false }
        });
    });

    stabilizeNetwork().then(() => {
        isPhysicsPaused = true;
        network.setOptions({ physics: { enabled: false } });
        const pauseButton = document.getElementById('pause-toggle');
        pauseButton.textContent = 'Resume Physics';
        pauseButton.classList.add('paused');
        //ensureInteractionSettings();
    });
}

function setCircularLayout() {
    network.setOptions({
        physics: { enabled: false },
        layout: { hierarchical: false },
        interaction: { ...baseInteractionOptions }
    });

    const containerRect = container.getBoundingClientRect();
    const radius = Math.min(containerRect.width, containerRect.height) / 2 - 100;
    const nodeCount = nodes.length;
    const angleStep = (2 * Math.PI) / nodeCount;
    const centerX = containerRect.width / 2;
    const centerY = containerRect.height / 2;

    nodes.forEach((node, i) => {
        const x = centerX + radius * Math.cos(angleStep * i);
        const y = centerY + radius * Math.sin(angleStep * i);
        nodes.update({
            id: node.id,
            x: x,
            y: y,
            fixed: { x: true, y: true }
        });
    });

    network.fit({
        animation: {
            duration: 500,
            easingFunction: 'easeInOutQuad'
        }
    });
}

function setOrthogonalLayout() {
    network.setOptions({
        physics: { enabled: false },
        layout: { hierarchical: false },
        interaction: { ...baseInteractionOptions }
    });

    const containerRect = container.getBoundingClientRect();
    const gridSize = Math.ceil(Math.sqrt(nodes.length));
    const stepX = containerRect.width / (gridSize + 1);
    const stepY = containerRect.height / (gridSize + 1);
    let i = 0;

    nodes.forEach(node => {
        const x = (i % gridSize + 0.5) * stepX;
        const y = (Math.floor(i / gridSize) + 0.5) * stepY;
        nodes.update({
            id: node.id,
            x: x,
            y: y,
            fixed: { x: true, y: true }
        });
        i++;
    });

    network.fit({
        animation: {
            duration: 500,
            easingFunction: 'easeInOutQuad'
        }
    });
}

function setTreeLayout() {
    network.setOptions({
        physics: { enabled: false },
        layout: {
            hierarchical: {
                enabled: true,
                levelSeparation: 200,
                nodeSpacing: 150,
                treeSpacing: 200,
                direction: 'UD',
                sortMethod: 'hubsize',
                shakeTowards: 'leaves'
            }
        },
        edges: {
            smooth: {
                enabled: true,
                type: 'cubicBezier'
            }
        },
        interaction: { ...baseInteractionOptions }
    });

    // Reset positions before applying layout
    nodes.forEach(node => {
        nodes.update({
            id: node.id,
            x: undefined,
            y: undefined,
            fixed: { x: false, y: false }
        });
    });

    network.fit({
        animation: {
            duration: 500,
            easingFunction: 'easeInOutQuad'
        }
    });
}

function setHierarchicalLayout() {
    network.setOptions({
        physics: { enabled: false },
        layout: {
            hierarchical: {
                enabled: true,
                levelSeparation: 200,
                nodeSpacing: 150,
                treeSpacing: 200,
                direction: 'UD',
                sortMethod: 'directed',
                shakeTowards: 'roots'
            }
        },
        edges: {
            smooth: {
                enabled: true,
                type: 'cubicBezier'
            }
        },
        interaction: { ...baseInteractionOptions }
    });

    // Reset positions before applying layout
    nodes.forEach(node => {
        nodes.update({
            id: node.id,
            x: undefined,
            y: undefined,
            fixed: { x: false, y: false }
        });
    });

    network.fit({
        animation: {
            duration: 500,
            easingFunction: 'easeInOutQuad'
        }
    });
}

        function showTab(tabId) {
            document.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active'));
            document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
            document.getElementById(tabId).classList.add('active');
            document.querySelector(`.tab-button[onclick="showTab('${tabId}')"]`).classList.add('active');
        }

        document.getElementById('addEntityType').addEventListener('change', function() {
            let type = this.value;
            document.getElementById('addVulnNameInput').style.display = type === 'vulnerability' ? 'block' : 'none';
            document.getElementById('addVulnCVEInput').style.display = type === 'vulnerability' ? 'block' : 'none';
            document.getElementById('addVulnUrlInput').style.display = type === 'vulnerability' ? 'block' : 'none';
            document.getElementById('addNameInput').style.display = type === 'contact' ? 'block' : 'none';
            document.getElementById('addEmailInput').style.display = type === 'contact' ? 'block' : 'none';
            document.getElementById('addIpInput').style.display = type === 'ip' ? 'block' : 'none';
            document.getElementById('addDomainInput').style.display = type === 'domain' ? 'block' : 'none';
            document.getElementById('addOrgInput').style.display = type === 'organization' ? 'block' : 'none';
            document.getElementById('addPortNumInput').style.display = type === 'port' ? 'block' : 'none';
            document.getElementById('addPortType').style.display = type === 'port' ? 'block' : 'none';
            document.getElementById('addWalletAddressInput').style.display = type === 'wallet' ? 'block' : 'none';
            document.getElementById('addAccountNumberInput').style.display = type === 'bank' ? 'block' : 'none';
            document.getElementById('addSortCodeInput').style.display = type === 'bank' ? 'block' : 'none';
            document.getElementById('addTechNameInput').style.display = type === 'technology' ? 'block' : 'none';
            document.getElementById('addTechVersionInput').style.display = type === 'technology' ? 'block' : 'none';
            document.getElementById('addDeviceCategory').style.display = type === 'device' ? 'block' : 'none';
            document.getElementById('addDeviceNameInput').style.display = type === 'device' ? 'block' : 'none';
            document.getElementById('addMalwareNameInput').style.display = type === 'malware' ? 'block' : 'none';
            document.getElementById('addMalwareType').style.display = type === 'malware' ? 'block' : 'none';
            document.getElementById('addSubnetInput').style.display = type === 'subnet' ? 'block' : 'none';
        });

        function createNodeData(type, values) {
    const nodeData = { 
        id: nextId++, 
        size: 10,
        type,
        widthConstraint: false,
        heightConstraint: false
    };
    
    // Define configs for all node types
    const configs = {
        vulnerability: { 
            fields: ['vulnName'], 
            optionalFields: ['cve', 'url', 'notes'], 
            color: '#dc2626',
            label: v => `Vulnerability: ${v.vulnName}${v.cve ? '\nCVE: ' + v.cve : ''}${v.url ? '\nURL: ' + v.url : ''}`,
            title: v => `Vulnerability\nName: ${v.vulnName}${v.cve ? '\nCVE: ' + v.cve : ''}${v.url ? '\nURL: ' + v.url : ''}${v.notes ? '\nNotes: ' + v.notes : ''}`
        },
        contact: { 
            fields: ['name'], 
            optionalFields: ['email', 'notes'], 
            color: '#4ade80', 
            label: v => `Contact: ${v.name}${v.email ? '\n' + v.email : ''}`, 
            title: v => `Contact\nName: ${v.name}${v.email ? '\nEmail: ' + v.email : ''}${v.notes ? '\nNotes: ' + v.notes : ''}`
        },
        ip: { 
            fields: ['ip'], 
            optionalFields: ['notes'],
            color: '#f87171', 
            label: v => `IP: ${v.ip}`, 
            title: v => `IP Address: ${v.ip}${v.notes ? '\nNotes: ' + v.notes : ''}`,
            validate: v => {
                const ipRegex = {
                    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]?)$/,
                    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
                };
                return ipRegex.ipv4.test(v.ip) || ipRegex.ipv6.test(v.ip);
            }
        },
        domain: { 
            fields: ['domain'], 
            optionalFields: ['notes'],
            color: '#60a5fa', 
            label: v => `Domain: ${v.domain}`, 
            title: v => `Domain: ${v.domain}${v.notes ? '\nNotes: ' + v.notes : ''}` 
        },
        organization: { 
            fields: ['organization'], 
            optionalFields: ['notes'],
            color: '#facc15', 
            label: v => `Organization: ${v.organization}`, 
            title: v => `Organization: ${v.organization}${v.notes ? '\nNotes: ' + v.notes : ''}` 
        },
        port: { 
            fields: ['portNumber', 'portType'], 
            optionalFields: ['notes'],
            color: '#a78bfa', 
            label: v => `Port: ${v.portType}/${v.portNumber}`, 
            title: v => `Port\nType: ${v.portType}\nNumber: ${v.portNumber}${v.notes ? '\nNotes: ' + v.notes : ''}` 
        },
        wallet: { 
            fields: ['address'], 
            optionalFields: ['notes'],
            color: '#fb923c', 
            label: v => `Wallet: ${v.address}`, 
            title: v => `Wallet\nAddress: ${v.address}${v.notes ? '\nNotes: ' + v.notes : ''}` 
        },
        bank: { 
            fields: ['accountNumber', 'sortCode'], 
            optionalFields: ['notes'],
            color: '#10b981', 
            label: v => `Bank: ${v.accountNumber}\nSort Code: ${v.sortCode}`, 
            title: v => `Bank Account\nAccount Number: ${v.accountNumber}\nSort Code: ${v.sortCode}${v.notes ? '\nNotes: ' + v.notes : ''}` 
        },
        technology: { 
            fields: ['techName'], 
            optionalFields: ['techVersion', 'notes'], 
            color: '#ec4899', 
            label: v => `Technology: ${v.techName}${v.techVersion ? '\nVersion: ' + v.techVersion : ''}`, 
            title: v => `Technology\nName: ${v.techName}${v.techVersion ? '\nVersion: ' + v.techVersion : ''}${v.notes ? '\nNotes: ' + v.notes : ''}` 
        },
        device: { 
            fields: ['deviceCategory', 'deviceName'], 
            optionalFields: ['notes'],
            color: '#14b8a6', 
            label: v => `Device: ${v.deviceName}\nCategory: ${v.deviceCategory}`, 
            title: v => `Device\nName: ${v.deviceName}\nCategory: ${v.deviceCategory}${v.notes ? '\nNotes: ' + v.notes : ''}` 
        },
        malware: { 
            fields: ['malwareName', 'malwareType'], 
            optionalFields: ['notes'],
            color: '#ef4444', 
            label: v => `Malware: ${v.malwareName}\nType: ${v.malwareType}`, 
            title: v => `Malware\nName: ${v.malwareName}\nType: ${v.malwareType}${v.notes ? '\nNotes: ' + v.notes : ''}` 
        },
        favicon: { 
            fields: ['hash'], 
            optionalFields: ['location', 'notes'], 
            color: '#22d3ee',
            label: v => `Favicon: ${v.hash.substring(0, 8)}...`, 
            title: v => `Favicon\nHash: ${v.hash}${v.location ? '\nPath: ' + v.location : ''}${v.notes ? '\nNotes: ' + v.notes : ''}` 
        },
        subnet: { 
            fields: ['subnet'], 
            optionalFields: ['notes'],
            color: '#9333ea',
            validate: v => {
                const cidrRegex = /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(\d{1,2})$/;
                if (!cidrRegex.test(v.subnet)) return false;
                const [ip, mask] = v.subnet.split('/');
                const octets = ip.split('.').map(Number);
                const maskNum = Number(mask);
                return octets.every(o => o >= 0 && o <= 255) && maskNum >= 0 && maskNum <= 32;
            }
        },
        mx: {
            fields: ['hostname'],
            optionalFields: ['notes'],
            color: '#34d399', // Green for MX records
            label: v => `MX: ${v.hostname.length > 30 ? v.hostname.substring(0, 27) + '...' : v.hostname}`,
            title: v => `Mail Exchanger\nHostname: ${v.hostname}${v.notes ? '\nNotes: ' + v.notes : ''}`
        },
        txt: {
            fields: ['text'],
            optionalFields: ['notes'],
            color: '#f59e0b', // Orange for TXT records
            label: v => `TXT: ${v.text.length > 30 ? v.text.substring(0, 27) + '...' : v.text}`,
            title: v => `TXT Record\nValue: ${v.text}${v.notes ? '\nNotes: ' + v.notes : ''}`
        }
    };

    const config = configs[type];
    if (!config || config.fields.some(f => !values[f])) { 
        showToast(`Please enter all required fields for ${type}`, 'error'); 
        return null; 
    }
    if (config.validate && !config.validate(values)) { 
        showToast(`Invalid ${type} format`, 'error'); 
        return null; 
    }
    
    const allValues = { ...values };
    if (config.optionalFields) config.optionalFields.forEach(f => { if (values[f]) allValues[f] = values[f]; });
    
    // Handle subnet type separately to set label and title directly
    if (type === 'subnet') {
        const [ip] = values.subnet.split('/');
        const isPrivate = isPrivateIP(ip);
        allValues.isPrivate = isPrivate;
        nodeData.isPrivate = isPrivate;
        nodeData.label = `Subnet: ${values.subnet} (${isPrivate ? 'Private' : 'Public'})`;
        nodeData.title = `Subnet: ${values.subnet}\nType: ${isPrivate ? 'Private' : 'Public'}${values.notes ? '\nNotes: ' + values.notes : ''}`;
        console.log(`Subnet ${values.subnet} classified as ${isPrivate ? 'Private' : 'Public'}`);
        console.log(`Label set to: ${nodeData.label}`);
        console.log(`Title set to: ${nodeData.title}`);
    } else {
        // For other types, use the config's label and title functions
        nodeData.label = config.label ? config.label(allValues) : undefined;
        nodeData.title = config.title ? config.title(allValues) : undefined;
    }

    nodeData.color = { background: config.color };
    Object.assign(nodeData, allValues);

    console.log(`Final nodeData: ${JSON.stringify(nodeData)}`);
    return nodeData;
}

// Define isPrivateIP if not already defined elsewhere
function isPrivateIP(ip) {
    console.log(`isPrivateIP called with: ${ip}`);
    const octets = ip.split('.').map(Number);
    console.log(`Parsed octets: ${octets}`);
    
    if (octets.length !== 4 || octets.some(o => o < 0 || o > 255)) {
        console.log(`Invalid IP format: ${ip}`);
        return false;
    }

    const isPrivate = (
        (octets[0] === 10) || // 10.0.0.0/8
        (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) || // 172.16.0.0/12
        (octets[0] === 192 && octets[1] === 168) // 192.168.0.0/16
    );
    
    console.log(`IP ${ip} is ${isPrivate ? 'private' : 'public'}`);
    return isPrivate;
}


function addNode() {
    const type = document.getElementById('addEntityType').value;
    
    // Collect inputs based on entity type
    const inputs = {
        vulnerability: { 
            vulnName: document.getElementById('addVulnNameInput').value.trim(),
            cve: document.getElementById('addVulnCVEInput').value.trim(),
            url: document.getElementById('addVulnUrlInput').value.trim()
        },
        contact: { 
            name: document.getElementById('addNameInput').value.trim(),
            email: document.getElementById('addEmailInput').value.trim()
        },
        ip: { 
            ip: document.getElementById('addIpInput').value.trim()
        },
        subnet: { 
            subnet: document.getElementById('addSubnetInput').value.trim()
        },
        domain: { 
            domain: document.getElementById('addDomainInput').value.trim()
        },
        organization: { 
            organization: document.getElementById('addOrgInput').value.trim()
        },
        port: { 
            portNumber: document.getElementById('addPortNumInput').value.trim(),
            portType: document.getElementById('addPortType').value
        },
        wallet: { 
            address: document.getElementById('addWalletAddressInput').value.trim()
        },
        bank: { 
            accountNumber: document.getElementById('addAccountNumberInput').value.trim(),
            sortCode: document.getElementById('addSortCodeInput').value.trim()
        },
        technology: { 
            techName: document.getElementById('addTechNameInput').value.trim(),
            techVersion: document.getElementById('addTechVersionInput').value.trim()
        },
        device: { 
            deviceCategory: document.getElementById('addDeviceCategory').value,
            deviceName: document.getElementById('addDeviceNameInput').value.trim()
        },
        malware: { 
            malwareName: document.getElementById('addMalwareNameInput').value.trim(),
            malwareType: document.getElementById('addMalwareType').value
        }
    };

    const nodeData = createNodeData(type, inputs[type]);
    if (!nodeData) {
        return;
    }

    // Check for duplicates based on key fields with corrected syntax
    const existingNodes = nodes.get({
        filter: n => n.type === type && (
            (type === 'contact' && n.name === inputs[type].name && (!inputs[type].email || n.email === inputs[type].email)) ||
            (type === 'ip' && n.ip === inputs[type].ip) ||
            (type === 'domain' && n.domain === inputs[type].domain) ||
            (type === 'organization' && n.organization === inputs[type].organization) ||
            (type === 'port' && n.portNumber === inputs[type].portNumber && n.portType === inputs[type].portType) ||
            (type === 'wallet' && n.address === inputs[type].address) ||
            (type === 'bank' && n.accountNumber === inputs[type].accountNumber && n.sortCode === inputs[type].sortCode) ||
            (type === 'technology' && n.techName === inputs[type].techName && n.techVersion === inputs[type].techVersion) ||
            (type === 'device' && n.deviceCategory === inputs[type].deviceCategory && n.deviceName === inputs[type].deviceName) ||
            (type === 'malware' && n.malwareName === inputs[type].malwareName && n.malwareType === inputs[type].malwareType) ||
            (type === 'vulnerability' && n.vulnName === inputs[type].vulnName && 
             (!inputs[type].cve || n.cve === inputs[type].cve) && 
             (!inputs[type].url || n.url === inputs[type].url))
        )
    });

    if (existingNodes.length > 0) {
        showToast(`${type} already exists`, 'error');
        return;
    }

    nodes.add({
        ...nodeData,
        size: 10,
        widthConstraint: false,
        heightConstraint: false
    });

    updateSelectOptions();
    clearAddInputs();
    updateNodeSizes();
    stabilizeNetwork();
    saveStateAfterOperation();
    showToast(`${type} added successfully`, 'success');
}


        function editNode() {
            const nodeId = document.getElementById('editNodeSelect').value;
            if (!nodeId) { showToast('Please select a node to edit', 'error'); return; }
            const node = nodes.get(parseInt(nodeId));
            if (!node) { showToast('Selected node not found', 'error'); return; }

            const type = node.type;
            const inputs = {
                contact: { name: document.getElementById('editNameInput').value, email: document.getElementById('editEmailInput').value },
                ip: { ip: document.getElementById('editIpInput').value.trim() },
                domain: { domain: document.getElementById('editDomainInput').value },
                organization: { organization: document.getElementById('editOrgInput').value },
                port: { portNumber: document.getElementById('editPortNumInput').value, portType: document.getElementById('editPortType').value },
                wallet: { address: document.getElementById('editWalletAddressInput').value },
                bank: { accountNumber: document.getElementById('editAccountNumberInput').value, sortCode: document.getElementById('editSortCodeInput').value },
                technology: { techName: document.getElementById('editTechNameInput').value, techVersion: document.getElementById('editTechVersionInput').value },
                device: { deviceCategory: document.getElementById('editDeviceCategory').value, deviceName: document.getElementById('editDeviceNameInput').value },
                malware: { malwareName: document.getElementById('editMalwareNameInput').value, malwareType: document.getElementById('editMalwareType').value },
                vulnerability: {vulnName: document.getElementById('editVulnNameInput').value.trim(),
            cve: document.getElementById('editVulnCVEInput').value.trim(),
            subnet: { subnet: document.getElementById('editSubnetInput').value.trim() },
            url: document.getElementById('editVulnUrlInput').value.trim()
            }
            };
            const updatedNodeData = createNodeData(type, inputs[type]);
            if (updatedNodeData) {
                const existingNode = nodes.get({ filter: n => n.id !== parseInt(nodeId) && n.type === type && Object.keys(inputs[type]).every(key => n[key] === inputs[type][key]) });
                if (existingNode) { showToast(`Another ${type} with these values already exists`, 'error'); return; }
                updatedNodeData.id = node.id;
                nodes.update(updatedNodeData);
                updateSelectOptions();
                clearEditInputs();
                stabilizeNetwork();
                saveStateAfterOperation(); 
            }
        }

        function loadNodeForEdit() {
            const nodeId = document.getElementById('editNodeSelect').value;
            clearEditInputs();
            if (!nodeId) return;

            const node = nodes.get(parseInt(nodeId));
            if (!node) return;

            document.getElementById('editEntityType').value = node.type;
            switch (node.type) {
                case 'subnet':
            document.getElementById('editSubnetInput').value = node.subnet || '';
                    break;
                case 'contact':
                    document.getElementById('editNameInput').value = node.name || '';
                    document.getElementById('editEmailInput').value = node.email || '';
                    break;
                case 'ip':
                    document.getElementById('editIpInput').value = node.ip || '';
                    break;
                case 'domain':
                    document.getElementById('editDomainInput').value = node.domain || '';
                    break;
                case 'organization':
                    document.getElementById('editOrgInput').value = node.organization || '';
                    break;
                case 'port':
                    document.getElementById('editPortNumInput').value = node.portNumber || '';
                    document.getElementById('editPortType').value = node.portType || 'TCP';
                    break;
                case 'wallet':
                    document.getElementById('editWalletAddressInput').value = node.address || '';
                    break;
                case 'bank':
                    document.getElementById('editAccountNumberInput').value = node.accountNumber || '';
                    document.getElementById('editSortCodeInput').value = node.sortCode || '';
                    break;
                case 'technology':
                    document.getElementById('editTechNameInput').value = node.techName || '';
                    document.getElementById('editTechVersionInput').value = node.techVersion || '';
                    break;
                case 'device':
                    document.getElementById('editDeviceCategory').value = node.deviceCategory || 'Server';
                    document.getElementById('editDeviceNameInput').value = node.deviceName || '';
                    break;
                case 'malware':
                    document.getElementById('editMalwareNameInput').value = node.malwareName || '';
                    document.getElementById('editMalwareType').value = node.malwareType || 'Wiper';
                    break;
                case 'vulnerability':
                    document.getElementById('editVulnNameInput').value = node.vulnName || '';
                    document.getElementById('editVulnCVEInput').value = node.cve || '';
                    document.getElementById('editVulnUrlInput').value = node.url || '';
                    break;
            }
            document.getElementById('editNameInput').style.display = node.type === 'contact' ? 'block' : 'none';
            document.getElementById('editEmailInput').style.display = node.type === 'contact' ? 'block' : 'none';
            document.getElementById('editIpInput').style.display = node.type === 'ip' ? 'block' : 'none';
            document.getElementById('editDomainInput').style.display = node.type === 'domain' ? 'block' : 'none';
            document.getElementById('editOrgInput').style.display = node.type === 'organization' ? 'block' : 'none';
            document.getElementById('editPortNumInput').style.display = node.type === 'port' ? 'block' : 'none';
            document.getElementById('editPortType').style.display = node.type === 'port' ? 'block' : 'none';
            document.getElementById('editWalletAddressInput').style.display = node.type === 'wallet' ? 'block' : 'none';
            document.getElementById('editAccountNumberInput').style.display = node.type === 'bank' ? 'block' : 'none';
            document.getElementById('editSortCodeInput').style.display = node.type === 'bank' ? 'block' : 'none';
            document.getElementById('editTechNameInput').style.display = node.type === 'technology' ? 'block' : 'none';
            document.getElementById('editTechVersionInput').style.display = node.type === 'technology' ? 'block' : 'none';
            document.getElementById('editDeviceCategory').style.display = node.type === 'device' ? 'block' : 'none';
            document.getElementById('editDeviceNameInput').style.display = node.type === 'device' ? 'block' : 'none';
            document.getElementById('editMalwareNameInput').style.display = node.type === 'malware' ? 'block' : 'none';
            document.getElementById('editMalwareType').style.display = node.type === 'malware' ? 'block' : 'none';
            document.getElementById('editVulnNameInput').style.display = node.type === 'vulnerability' ? 'block' : 'none';
            document.getElementById('editVulnCVEInput').style.display = node.type === 'vulnerability' ? 'block' : 'none';
            document.getElementById('editVulnUrlInput').style.display = node.type === 'vulnerability' ? 'block' : 'none';
            document.getElementById('editSubnetInput').style.display = node.type === 'subnet' ? 'block' : 'none';
        }

        function clearAddInputs() {
            document.querySelectorAll('#object-management .input-group:first-child input').forEach(input => input.value = '');
            document.getElementById('addDeviceCategory').value = 'Server';
            document.getElementById('addMalwareType').value = 'Wiper';
            document.getElementById('addPortType').value = 'TCP';
            document.getElementById('addVulnNameInput').value = '';
            document.getElementById('addVulnCVEInput').value = '';
            document.getElementById('addVulnUrlInput').value = '';
        }

        function clearEditInputs() {
            document.querySelectorAll('#object-management .input-group:nth-child(2) input').forEach(input => input.value = '');
            document.getElementById('editDeviceCategory').value = 'Server';
            document.getElementById('editMalwareType').value = 'Wiper';
            document.getElementById('editPortType').value = 'TCP';
            document.getElementById('editVulnNameInput').value = '';
            document.getElementById('editVulnCVEInput').value = '';
            document.getElementById('editVulnUrlInput').value = '';
  
Download .txt
gitextract_78spr4xa/

├── README.md
├── analysis.md
├── bugs.md
├── compare_strings.html
├── cors_proxy_server.js
├── crimemapper.html
├── email_time_delta.html
├── example-pwndefend.json
├── experimental_mapper.html
├── functions.md
├── header_analysis.html
├── ipinfo_to_csv.html
├── macos_cors_proxy_install.sh
├── network_graph-4.json
├── network_graph-8.json
├── network_graph-demo-ncsc.json
└── nrich.html
Download .txt
SYMBOL INDEX (2 symbols across 1 files)

FILE: cors_proxy_server.js
  function validateTargetUrl (line 62) | function validateTargetUrl(req, res, next) {
  function proxyRequest (line 113) | async function proxyRequest(req, res) {
Condensed preview — 17 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,398K chars).
[
  {
    "path": "README.md",
    "chars": 2123,
    "preview": "# Enabling cyber investigations from a browser\n**A prototype tool for mapping cyber crime**  \n\nCreated by **@UK_Daniel_C"
  },
  {
    "path": "analysis.md",
    "chars": 6056,
    "preview": "# Analysis of experimental_mapper.html\n\n## Overview\n\nThis document provides an assessment of the current state of `exper"
  },
  {
    "path": "bugs.md",
    "chars": 3603,
    "preview": "# Bug Tracking for Experimental Mapper Application\n\n## Overview\n\nThis document is used to track bugs and issues encounte"
  },
  {
    "path": "compare_strings.html",
    "chars": 4615,
    "preview": "<script type=\"text/javascript\">\n        var gk_isXlsx = false;\n        var gk_xlsxFileLookup = {};\n        var gk_fileDa"
  },
  {
    "path": "cors_proxy_server.js",
    "chars": 7167,
    "preview": "const express = require('express');\nconst axios = require('axios');\nconst https = require('https');\nconst url = require("
  },
  {
    "path": "crimemapper.html",
    "chars": 412668,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width"
  },
  {
    "path": "email_time_delta.html",
    "chars": 34683,
    "preview": "<script type=\"text/javascript\">\n        var gk_isXlsx = false;\n        var gk_xlsxFileLookup = {};\n        var gk_fileDa"
  },
  {
    "path": "example-pwndefend.json",
    "chars": 5116,
    "preview": "{\n  \"nodes\": [\n    {\n      \"id\": 1,\n      \"type\": \"domain\",\n      \"domain\": \"pwndefend.com\"\n    },\n    {\n      \"id\": 2,\n"
  },
  {
    "path": "experimental_mapper.html",
    "chars": 412415,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width"
  },
  {
    "path": "functions.md",
    "chars": 16635,
    "preview": "# Functions in experimental_mapper.html\n\nThis document lists the JavaScript functions found in `experimental_mapper.html"
  },
  {
    "path": "header_analysis.html",
    "chars": 87357,
    "preview": "<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale="
  },
  {
    "path": "ipinfo_to_csv.html",
    "chars": 7754,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, "
  },
  {
    "path": "macos_cors_proxy_install.sh",
    "chars": 3907,
    "preview": "#!/bin/bash\n\n# Script to install the CORS proxy server from mr-r3b00t/crime-mapper on macOS\n# Ensures Homebrew and npm d"
  },
  {
    "path": "network_graph-4.json",
    "chars": 9185,
    "preview": "{\n  \"nodes\": [\n    {\n      \"id\": 1,\n      \"type\": \"domain\",\n      \"domain\": \"example.com\"\n    },\n    {\n      \"id\": 2,\n  "
  },
  {
    "path": "network_graph-8.json",
    "chars": 280018,
    "preview": "{\n  \"nodes\": [\n    {\n      \"id\": 1,\n      \"type\": \"ip\",\n      \"ip\": \"137.184.28.58\"\n    },\n    {\n      \"id\": 2,\n      \"t"
  },
  {
    "path": "network_graph-demo-ncsc.json",
    "chars": 4234,
    "preview": "{\n  \"nodes\": [\n    {\n      \"id\": 1,\n      \"type\": \"domain\",\n      \"domain\": \"ncsc.gov.uk\"\n    },\n    {\n      \"id\": 2,\n  "
  },
  {
    "path": "nrich.html",
    "chars": 16310,
    "preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\" />\n  <meta name=\"viewport\" content=\"width=device-width,i"
  }
]

About this extraction

This page contains the full source code of the mr-r3b00t/crime-mapper GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 17 files (1.3 MB), approximately 341.9k tokens, and a symbol index with 2 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!