Skip to Content

Gap Analysis: Adding Nodes to Canvas

Gap Analysis: Adding Nodes to Canvas

Date: 2025-09-30
Status: Ready for Implementation
Current Phase: Phase 3 Complete (Menu Working) → Need Phase 4 (Canvas Integration)


Executive Summary

Your overlay_manager.js is fully functional and successfully generates n8n-compatible JSON when a user selects a node operation. However, there's a critical gap: the JSON is not being added to the canvas. Currently, it just shows an alert with the JSON.

The Good News: You're 90% there. The menu system works perfectly, and you're already generating the correct JSON format that n8n uses.

The Gap: You need a NodeManager class to bridge between your OverlayManager (menu) and CanvasManager (viewport) to actually create visual nodes on the canvas.


Current State Analysis

✅ What You Have Working

1. Overlay Manager (Menu System) - overlay_manager.js

Status: ✅ COMPLETE

What it does:
- Opens n8n node selection popup
- Loads nodes from database (n8n.simple.node model)
- Filters by supplier, category, type, and platform
- Drills down through services → operations → actions
- Generates n8n-compatible JSON when user selects an operation

Key Method: selectOperation() (Line 3373)

selectOperation(operation) {
    console.log(`✅ Operation selected:`, operation);

    // Generate the canvas-ready JSON structure
    const canvasNodeJSON = {
        nodes: [
            {
                parameters: {
                    resource: operation.resource || '',
                    operation: operation.value || 'execute'
                },
                type: operation.nodeType,
                typeVersion: 1,
                position: [250, 250], // TODO: Smart positioning
                id: this.generateUUID(),
                name: operation.name
            }
        ],
        connections: {},
        pinData: {},
        meta: {
            instanceId: this.generateUUID()
        }
    };

    console.log('📋 Generated canvas JSON:', canvasNodeJSON);

    // ⚠️ CURRENT STATE: Just shows alert
    alert(`Canvas JSON Generated!\n\n${JSON.stringify(canvasNodeJSON, null, 2)}\n\nNext: Add to canvas!`);

    // Close overlay
    this.closeN8nOverlay();
}

Analysis:
- ✅ JSON structure matches n8n format perfectly
- ✅ Generates unique UUIDs
- ✅ Includes all required fields: parameters, type, typeVersion, position, id, name
- ❌ No canvas integration - JSON is generated but not used

2. Canvas Manager (Viewport System) - canvas_manager.js

Status: ✅ COMPLETE (for pan/zoom)

What it does:
- Pan canvas (drag to move)
- Zoom canvas (zoom in/out)
- Grid background
- Viewport transformation

What it DOESN'T do:
- ❌ No node rendering
- ❌ No node creation
- ❌ No node storage
- ❌ No node interaction (click, drag, connect)

Responsibility: Canvas operations only (pan, zoom) - NOT nodes

3. Database Layer

Status: ✅ COMPLETE

What you have:
- n8n.simple.node model with full node metadata
- RPC endpoint: /web/dataset/call_kw with method get_node_operations()
- Node extraction wizard to populate data
- Full node hierarchy: supplier → service → operations → actions


The Gap: Missing Node Manager

❌ What's Missing

You need a NodeManager class to:

  1. Receive JSON from OverlayManager
  2. Store nodes in memory (workflow state)
  3. Render nodes visually on canvas
  4. Handle node interactions (click, drag, delete, connect)
  5. Sync with backend (save/load workflow)

Current Flow (Broken)

User clicks operation in menu
        ↓
OverlayManager.selectOperation()
        ↓
Generates JSON
        ↓
❌ alert() - Dead end!

Target Flow (What We Need)

User clicks operation in menu
        ↓
OverlayManager.selectOperation()
        ↓
Generates JSON
        ↓
✅ NodeManager.addNode(nodeData)
        ↓
NodeManager creates INodeUi object
        ↓
NodeManager.renderNode(node) - Create visual element
        ↓
CanvasViewport.appendChild(nodeElement)
        ↓
User sees node on canvas!

Detailed Gap Analysis

Gap 1: No Node State Management

What n8n Does:

// From: packages/frontend/editor-ui/src/stores/workflows.store.ts
const workflow = {
    nodes: [],  // Array of INodeUi objects
    connections: {},
    pinData: {},
    settings: {},
};

function addNode(nodeData: INodeUi): void {
    workflow.nodes.push(nodeData);
    workflowObject.setNodes(workflow.nodes);
}

What You Need:

class NodeManager {
    constructor() {
        this.nodes = [];  // Store all nodes
        this.connections = {};
        this.selectedNode = null;
        this.nodeCounter = 0;
    }

    addNode(nodeData) {
        // Convert JSON to INodeUi format
        const node = this.createNodeInstance(nodeData);

        // Add to state
        this.nodes.push(node);

        // Render on canvas
        this.renderNode(node);

        // Save to backend
        this.syncWorkflow();

        return node;
    }
}

Status: ❌ MISSING - You have no node storage


Gap 2: No Visual Node Rendering

What n8n Does:

// From: packages/frontend/editor-ui/src/components/canvas/Canvas.vue
<template>
    <div class="canvas-container">
        <NodeElement
            v-for="node in nodes"
            :key="node.id"
            :node="node"
            :position="node.position"
            @click="onNodeClick"
            @drag="onNodeDrag"
        />
    </div>
</template>

What You Need:

class NodeManager {
    renderNode(node) {
        // Create visual node element
        const nodeElement = document.createElement('div');
        nodeElement.className = 'canvas-node';
        nodeElement.id = `node-${node.id}`;
        nodeElement.style.position = 'absolute';
        nodeElement.style.left = `${node.position[0]}px`;
        nodeElement.style.top = `${node.position[1]}px`;

        // Add node content
        nodeElement.innerHTML = `
            <div class="node-header">
                <span class="node-icon">${this.getNodeIcon(node.type)}</span>
                <span class="node-name">${node.name}</span>
            </div>
            <div class="node-body">
                ${this.renderNodeParameters(node.parameters)}
            </div>
        `;

        // Add event listeners
        this.attachNodeEventListeners(nodeElement, node);

        // Add to canvas
        const canvas = window.canvasManager.canvasViewport;
        canvas.appendChild(nodeElement);

        return nodeElement;
    }
}

Status: ❌ MISSING - Nodes are not rendered visually


Gap 3: No Node Interaction Handlers

What n8n Does:

// From: packages/frontend/editor-ui/src/composables/useCanvasOperations.ts
function onNodeClick(node: INodeUi) {
    selectNode(node);
    openNodeSettings(node);
}

function onNodeDrag(node: INodeUi, position: [number, number]) {
    updateNodePosition(node.id, position);
}

function onNodeDelete(node: INodeUi) {
    removeNode(node.id);
}

What You Need:

class NodeManager {
    attachNodeEventListeners(nodeElement, node) {
        // Click to select
        nodeElement.addEventListener('click', (e) => {
            this.selectNode(node);
        });

        // Drag to move
        let isDragging = false;
        let dragStart = { x: 0, y: 0 };

        nodeElement.addEventListener('mousedown', (e) => {
            isDragging = true;
            dragStart = { x: e.clientX, y: e.clientY };
        });

        document.addEventListener('mousemove', (e) => {
            if (isDragging) {
                const dx = e.clientX - dragStart.x;
                const dy = e.clientY - dragStart.y;
                this.moveNode(node, dx, dy);
                dragStart = { x: e.clientX, y: e.clientY };
            }
        });

        document.addEventListener('mouseup', () => {
            isDragging = false;
        });

        // Right-click for context menu
        nodeElement.addEventListener('contextmenu', (e) => {
            e.preventDefault();
            this.showNodeContextMenu(node, e.clientX, e.clientY);
        });
    }
}

Status: ❌ MISSING - Nodes can't be clicked, dragged, or deleted


Gap 4: No Backend Synchronization

What n8n Does:

// From: packages/frontend/editor-ui/src/composables/useWorkflowHelpers.ts
async function saveWorkflow() {
    const workflowData = await getWorkflowDataToSave();

    const response = await fetch('/api/workflow/save', {
        method: 'POST',
        body: JSON.stringify(workflowData),
    });

    return response.json();
}

What You Need:

class NodeManager {
    async syncWorkflow() {
        const workflowData = {
            nodes: this.nodes.map(n => this.serializeNode(n)),
            connections: this.connections,
        };

        const response = await fetch('/canvas/workflow/save', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(workflowData),
        });

        return response.json();
    }

    serializeNode(node) {
        return {
            id: node.id,
            name: node.name,
            type: node.type,
            typeVersion: node.typeVersion,
            position: node.position,
            parameters: node.parameters,
        };
    }
}

Status: ❌ MISSING - No save/load functionality


Gap 5: No Connection System

What n8n Does:

// From: packages/frontend/editor-ui/src/composables/useCanvasOperations.ts
function connectNodes(
    sourceNode: INodeUi,
    targetNode: INodeUi,
    sourceOutput: number = 0,
    targetInput: number = 0
) {
    const connection = {
        node: targetNode.name,
        type: 'main',
        index: targetInput,
    };

    if (!connections[sourceNode.name]) {
        connections[sourceNode.name] = { main: [[]] };
    }

    connections[sourceNode.name].main[sourceOutput].push(connection);
}

What You Need:

class NodeManager {
    connectNodes(sourceNodeId, targetNodeId, outputIndex = 0, inputIndex = 0) {
        const sourceNode = this.getNodeById(sourceNodeId);
        const targetNode = this.getNodeById(targetNodeId);

        if (!sourceNode || !targetNode) {
            console.error('Invalid nodes for connection');
            return;
        }

        // Add connection to state
        if (!this.connections[sourceNode.name]) {
            this.connections[sourceNode.name] = { main: [[]] };
        }

        this.connections[sourceNode.name].main[outputIndex].push({
            node: targetNode.name,
            type: 'main',
            index: inputIndex,
        });

        // Render connection line
        this.renderConnection(sourceNode, targetNode, outputIndex, inputIndex);

        // Save
        this.syncWorkflow();
    }

    renderConnection(sourceNode, targetNode, outputIndex, inputIndex) {
        // Create SVG line between nodes
        const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
        line.setAttribute('x1', sourceNode.position[0] + 100);
        line.setAttribute('y1', sourceNode.position[1] + 30);
        line.setAttribute('x2', targetNode.position[0]);
        line.setAttribute('y2', targetNode.position[1] + 30);
        line.setAttribute('stroke', '#999');
        line.setAttribute('stroke-width', '2');

        // Add to canvas SVG layer
        this.connectionLayer.appendChild(line);
    }
}

Status: ❌ MISSING - No connection system at all


Implementation Roadmap

Phase 4: Node Manager Implementation (NEXT STEP)

Step 1: Create NodeManager Class

File: static/src/n8n/nodes/node_manager.js

Core Structure:

class NodeManager {
    constructor() {
        this.initialized = false;
        this.nodes = [];
        this.connections = {};
        this.selectedNode = null;
        this.nodeCounter = 0;
    }

    init() {
        console.log('Initializing Node Manager...');
        this.setupNodeInfrastructure();
        this.setupNodeEventHandlers();
        this.initialized = true;
        return this;
    }

    // Node CRUD operations
    addNode(nodeData) { /* ... */ }
    removeNode(nodeId) { /* ... */ }
    updateNode(nodeId, updates) { /* ... */ }
    getNodeById(nodeId) { /* ... */ }

    // Node rendering
    renderNode(node) { /* ... */ }
    updateNodeVisual(nodeId) { /* ... */ }
    removeNodeVisual(nodeId) { /* ... */ }

    // Node interactions
    selectNode(node) { /* ... */ }
    moveNode(node, dx, dy) { /* ... */ }
    attachNodeEventListeners(element, node) { /* ... */ }

    // Connections
    connectNodes(sourceId, targetId) { /* ... */ }
    renderConnection(source, target) { /* ... */ }

    // Backend sync
    async syncWorkflow() { /* ... */ }
    async loadWorkflow(workflowId) { /* ... */ }
    serializeNode(node) { /* ... */ }
}

Step 2: Integrate with OverlayManager

Modify: overlay_manager.js → selectOperation() method (Line 3373)

Change from:

// Current (Line 3401)
alert(`Canvas JSON Generated!\n\n${JSON.stringify(canvasNodeJSON, null, 2)}\n\nNext: Add to canvas!`);

Change to:

// NEW: Use NodeManager to add node to canvas
if (window.nodeManager && window.nodeManager.initialized) {
    const nodeData = canvasNodeJSON.nodes[0];  // Extract first node
    window.nodeManager.addNode(nodeData);
    console.log('✅ Node added to canvas via NodeManager');
} else {
    console.error('❌ NodeManager not initialized');
    alert('Error: NodeManager not ready');
}

Step 3: Add Node Styles

File: static/src/n8n/nodes/node_styles.css (or inline in node_manager.js)

/* Node visual styles */
.canvas-node {
    position: absolute;
    width: 200px;
    background: white;
    border: 2px solid #007bff;
    border-radius: 8px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.15);
    cursor: pointer;
    transition: all 0.2s ease;
}

.canvas-node:hover {
    box-shadow: 0 4px 16px rgba(0,0,0,0.25);
    transform: translateY(-2px);
}

.canvas-node.selected {
    border-color: #28a745;
    box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.2);
}

.node-header {
    padding: 8px 12px;
    background: #f8f9fa;
    border-bottom: 1px solid #dee2e6;
    display: flex;
    align-items: center;
    gap: 8px;
    border-radius: 6px 6px 0 0;
}

.node-icon {
    font-size: 20px;
}

.node-name {
    font-size: 14px;
    font-weight: 600;
    color: #333;
    flex: 1;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.node-body {
    padding: 8px 12px;
}

.node-parameter {
    font-size: 12px;
    color: #666;
    margin-bottom: 4px;
}

.node-connection-point {
    position: absolute;
    width: 12px;
    height: 12px;
    background: #007bff;
    border: 2px solid white;
    border-radius: 50%;
    cursor: crosshair;
}

.node-connection-point.input {
    left: -6px;
    top: 50%;
    transform: translateY(-50%);
}

.node-connection-point.output {
    right: -6px;
    top: 50%;
    transform: translateY(-50%);
}

Step 4: Initialize NodeManager

Modify: Main initialization file (wherever overlayManager and canvasManager are initialized)

Add:

// Initialize Node Manager
const nodeManager = new NodeManager();
nodeManager.init();
window.nodeManager = nodeManager;
console.log('✅ NodeManager initialized and exposed globally');

// Listen for node creation events from OverlayManager
document.addEventListener('n8n-node:created', (e) => {
    console.log('📢 Received n8n-node:created event:', e.detail);
    if (e.detail.nodeData) {
        nodeManager.addNode(e.detail.nodeData);
    }
});

Suggested Implementation Order

Priority 1: Minimal Viable Product (MVP)

Goal: Get a single node to appear on canvas when selected from menu

Tasks:
1. ✅ Create NodeManager class with basic structure
2. ✅ Implement addNode() method
3. ✅ Implement renderNode() method (simple visual)
4. ✅ Modify overlay_manager.js → selectOperation() to call nodeManager.addNode()
5. ✅ Test: Select node from menu → See it appear on canvas

Time Estimate: 2-4 hours

Code Example (Minimal):

// node_manager.js (MVP version)
class NodeManager {
    constructor() {
        this.nodes = [];
    }

    addNode(nodeData) {
        // Store node
        const node = {
            id: nodeData.id,
            name: nodeData.name,
            type: nodeData.type,
            position: nodeData.position,
            parameters: nodeData.parameters,
        };
        this.nodes.push(node);

        // Render node
        this.renderNode(node);

        return node;
    }

    renderNode(node) {
        const canvas = document.querySelector('.canvas-viewport');
        if (!canvas) {
            console.error('Canvas viewport not found');
            return;
        }

        const nodeElement = document.createElement('div');
        nodeElement.className = 'canvas-node';
        nodeElement.id = `node-${node.id}`;
        nodeElement.style.position = 'absolute';
        nodeElement.style.left = `${node.position[0]}px`;
        nodeElement.style.top = `${node.position[1]}px`;
        nodeElement.innerHTML = `
            <div class="node-header">
                <span class="node-name">${node.name}</span>
            </div>
        `;

        canvas.appendChild(nodeElement);
        console.log(`✅ Rendered node: ${node.name}`);
    }
}

// Initialize
const nodeManager = new NodeManager();
window.nodeManager = nodeManager;

Priority 2: Node Interactions

Goal: Click to select, drag to move

Tasks:
1. ✅ Implement selectNode() method
2. ✅ Implement moveNode() method
3. ✅ Implement attachNodeEventListeners() for click and drag
4. ✅ Add visual feedback (highlight on select)

Time Estimate: 3-5 hours

Priority 3: Backend Persistence

Goal: Save and load workflows

Tasks:
1. ✅ Implement syncWorkflow() method
2. ✅ Implement loadWorkflow() method
3. ✅ Create Python controller endpoint /canvas/workflow/save
4. ✅ Create Python controller endpoint /canvas/workflow/load
5. ✅ Create ai.workflow model in Odoo (if not exists)

Time Estimate: 4-6 hours

Priority 4: Connections

Goal: Connect nodes visually

Tasks:
1. ✅ Implement connectNodes() method
2. ✅ Implement renderConnection() method using SVG
3. ✅ Add connection points to nodes
4. ✅ Add drag-to-connect interaction

Time Estimate: 6-8 hours

Priority 5: Advanced Features

Goal: Context menus, copy/paste, undo/redo

Tasks:
1. ✅ Context menu on right-click
2. ✅ Copy/paste nodes
3. ✅ Undo/redo system
4. ✅ Multi-select
5. ✅ Node grouping

Time Estimate: 8-12 hours


Code Templates Ready for Use

Template 1: Complete NodeManager (MVP + Interactions)

/**
 * Node Manager - Node operations and rendering
 * Responsibility: Create, render, update, delete nodes
 */

class NodeManager {
    constructor() {
        this.initialized = false;
        this.nodes = [];
        this.connections = {};
        this.selectedNode = null;
        this.nodeElements = new Map();  // Map<nodeId, HTMLElement>
        this.isDragging = false;
        this.dragNode = null;
        this.dragStart = { x: 0, y: 0 };
    }

    init() {
        console.log('Initializing Node Manager...');
        this.setupNodeEventHandlers();
        this.initialized = true;
        console.log('✅ Node Manager initialized');
        return this;
    }

    setupNodeEventHandlers() {
        // Global mouse handlers for dragging
        document.addEventListener('mousemove', (e) => {
            if (this.isDragging && this.dragNode) {
                const dx = e.clientX - this.dragStart.x;
                const dy = e.clientY - this.dragStart.y;
                this.moveNode(this.dragNode, dx, dy);
                this.dragStart = { x: e.clientX, y: e.clientY };
            }
        });

        document.addEventListener('mouseup', () => {
            if (this.isDragging) {
                this.isDragging = false;
                this.dragNode = null;
                console.log('✅ Node drag ended');
            }
        });

        // Delete key to remove selected node
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Delete' && this.selectedNode) {
                this.removeNode(this.selectedNode.id);
            }
        });
    }

    // ========================================================================
    // NODE CRUD OPERATIONS
    // ========================================================================

    addNode(nodeData) {
        console.log('➕ Adding node:', nodeData);

        // Create node instance
        const node = {
            id: nodeData.id || this.generateUUID(),
            name: nodeData.name || 'Untitled Node',
            type: nodeData.type,
            typeVersion: nodeData.typeVersion || 1,
            position: nodeData.position || [100, 100],
            parameters: nodeData.parameters || {},
            disabled: nodeData.disabled || false,
        };

        // Add to state
        this.nodes.push(node);

        // Render on canvas
        this.renderNode(node);

        console.log(`✅ Node added: ${node.name} (ID: ${node.id})`);

        return node;
    }

    removeNode(nodeId) {
        console.log('🗑️ Removing node:', nodeId);

        // Remove from state
        const index = this.nodes.findIndex(n => n.id === nodeId);
        if (index === -1) {
            console.error('Node not found:', nodeId);
            return;
        }

        const node = this.nodes[index];
        this.nodes.splice(index, 1);

        // Remove visual element
        this.removeNodeVisual(nodeId);

        // Clear selection if this was selected
        if (this.selectedNode && this.selectedNode.id === nodeId) {
            this.selectedNode = null;
        }

        console.log(`✅ Node removed: ${node.name}`);
    }

    updateNode(nodeId, updates) {
        const node = this.getNodeById(nodeId);
        if (!node) {
            console.error('Node not found:', nodeId);
            return;
        }

        Object.assign(node, updates);
        this.updateNodeVisual(nodeId);

        console.log(`✅ Node updated: ${node.name}`);
    }

    getNodeById(nodeId) {
        return this.nodes.find(n => n.id === nodeId);
    }

    // ========================================================================
    // NODE RENDERING
    // ========================================================================

    renderNode(node) {
        const canvas = document.querySelector('.canvas-viewport');
        if (!canvas) {
            console.error('❌ Canvas viewport not found');
            return;
        }

        // Create node element
        const nodeElement = document.createElement('div');
        nodeElement.className = 'canvas-node';
        nodeElement.id = `node-${node.id}`;
        nodeElement.style.position = 'absolute';
        nodeElement.style.left = `${node.position[0]}px`;
        nodeElement.style.top = `${node.position[1]}px`;

        // Build node HTML
        nodeElement.innerHTML = `
            <div class="node-header">
                <span class="node-icon">${this.getNodeIcon(node.type)}</span>
                <span class="node-name">${node.name}</span>
            </div>
            <div class="node-body">
                ${this.renderNodeParameters(node.parameters)}
            </div>
            <div class="node-connection-point input"></div>
            <div class="node-connection-point output"></div>
        `;

        // Add event listeners
        this.attachNodeEventListeners(nodeElement, node);

        // Add to DOM
        canvas.appendChild(nodeElement);

        // Store reference
        this.nodeElements.set(node.id, nodeElement);

        console.log(`✅ Rendered node: ${node.name} at [${node.position[0]}, ${node.position[1]}]`);
    }

    updateNodeVisual(nodeId) {
        const node = this.getNodeById(nodeId);
        const element = this.nodeElements.get(nodeId);

        if (!node || !element) return;

        // Update position
        element.style.left = `${node.position[0]}px`;
        element.style.top = `${node.position[1]}px`;

        // Update content
        const nameElement = element.querySelector('.node-name');
        if (nameElement) {
            nameElement.textContent = node.name;
        }

        const bodyElement = element.querySelector('.node-body');
        if (bodyElement) {
            bodyElement.innerHTML = this.renderNodeParameters(node.parameters);
        }
    }

    removeNodeVisual(nodeId) {
        const element = this.nodeElements.get(nodeId);
        if (element && element.parentNode) {
            element.parentNode.removeChild(element);
        }
        this.nodeElements.delete(nodeId);
    }

    renderNodeParameters(parameters) {
        if (!parameters || Object.keys(parameters).length === 0) {
            return '<div class="node-parameter">No parameters</div>';
        }

        return Object.entries(parameters)
            .map(([key, value]) => {
                const displayValue = typeof value === 'object'
                    ? JSON.stringify(value).substring(0, 30) + '...'
                    : String(value).substring(0, 30);
                return `<div class="node-parameter"><strong>${key}:</strong> ${displayValue}</div>`;
            })
            .join('');
    }

    getNodeIcon(nodeType) {
        // Map node types to icons
        const iconMap = {
            'n8n-nodes-base.gmail': '📧',
            'n8n-nodes-base.googleDrive': '📁',
            'n8n-nodes-base.slack': '💬',
            'n8n-nodes-base.httpRequest': '🌐',
        };

        return iconMap[nodeType] || '⚙️';
    }

    // ========================================================================
    // NODE INTERACTIONS
    // ========================================================================

    attachNodeEventListeners(nodeElement, node) {
        // Click to select
        nodeElement.addEventListener('click', (e) => {
            e.stopPropagation();
            this.selectNode(node);
        });

        // Mousedown to start drag
        const header = nodeElement.querySelector('.node-header');
        header.addEventListener('mousedown', (e) => {
            e.stopPropagation();
            this.isDragging = true;
            this.dragNode = node;
            this.dragStart = { x: e.clientX, y: e.clientY };
            console.log(`🖱️ Started dragging: ${node.name}`);
        });

        // Right-click for context menu
        nodeElement.addEventListener('contextmenu', (e) => {
            e.preventDefault();
            this.showNodeContextMenu(node, e.clientX, e.clientY);
        });
    }

    selectNode(node) {
        console.log(`👆 Selected node: ${node.name}`);

        // Deselect previous
        if (this.selectedNode) {
            const prevElement = this.nodeElements.get(this.selectedNode.id);
            if (prevElement) {
                prevElement.classList.remove('selected');
            }
        }

        // Select new
        this.selectedNode = node;
        const element = this.nodeElements.get(node.id);
        if (element) {
            element.classList.add('selected');
        }

        // Trigger event
        document.dispatchEvent(new CustomEvent('node:selected', { detail: { node } }));
    }

    moveNode(node, dx, dy) {
        node.position[0] += dx;
        node.position[1] += dy;

        const element = this.nodeElements.get(node.id);
        if (element) {
            element.style.left = `${node.position[0]}px`;
            element.style.top = `${node.position[1]}px`;
        }
    }

    showNodeContextMenu(node, x, y) {
        // TODO: Show context menu with options:
        // - Edit
        // - Duplicate
        // - Delete
        // - Copy
        console.log(`Context menu for: ${node.name} at [${x}, ${y}]`);
    }

    // ========================================================================
    // UTILITY
    // ========================================================================

    generateUUID() {
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
            const r = Math.random() * 16 | 0;
            const v = c === 'x' ? r : (r & 0x3 | 0x8);
            return v.toString(16);
        });
    }

    // ========================================================================
    // DEBUG
    // ========================================================================

    getState() {
        return {
            nodes: this.nodes,
            connections: this.connections,
            selectedNode: this.selectedNode,
        };
    }

    logState() {
        console.log('📊 NodeManager State:', this.getState());
    }
}

// ============================================================================
// INITIALIZATION
// ============================================================================

// Create and initialize
const nodeManager = new NodeManager();
nodeManager.init();
window.nodeManager = nodeManager;
console.log('✅ NodeManager exposed globally as window.nodeManager');

// Listen for node creation events from OverlayManager
document.addEventListener('n8n-node:created', (e) => {
    console.log('📢 Received n8n-node:created event:', e.detail);
    if (e.detail.nodeData) {
        nodeManager.addNode(e.detail.nodeData);
    }
});

console.log('✅ NodeManager ready. Use window.nodeManager to interact.');

Template 2: Modify overlay_manager.js

Find: Line 3401 in overlay_manager.js

Replace:

// OLD CODE (Line 3401):
alert(`Canvas JSON Generated!\n\n${JSON.stringify(canvasNodeJSON, null, 2)}\n\nNext: Add to canvas!`);

// NEW CODE:
// Add node to canvas via NodeManager
if (window.nodeManager && window.nodeManager.initialized) {
    const nodeData = canvasNodeJSON.nodes[0];  // Extract first node

    // Smart positioning: Place near last node or at center
    if (window.nodeManager.nodes.length > 0) {
        const lastNode = window.nodeManager.nodes[window.nodeManager.nodes.length - 1];
        nodeData.position = [lastNode.position[0] + 250, lastNode.position[1]];
    }

    window.nodeManager.addNode(nodeData);
    console.log('✅ Node added to canvas via NodeManager');
} else {
    console.error('❌ NodeManager not initialized');
    alert('Error: NodeManager not ready. Check console.');
}

Testing Checklist

Phase 4 Testing (Once NodeManager is implemented)

Test 1: Basic Node Creation
- [ ] Click "N8N Node" button → Menu opens
- [ ] Select a platform (e.g., Gmail)
- [ ] Select an operation (e.g., Send Email)
- [ ] Expected: Node appears on canvas
- [ ] Expected: Node shows correct name and icon

Test 2: Node Selection
- [ ] Click on a node
- [ ] Expected: Node gets highlighted/selected
- [ ] Expected: Console shows "Selected node: [name]"

Test 3: Node Dragging
- [ ] Click and hold node header
- [ ] Move mouse
- [ ] Expected: Node follows mouse
- [ ] Release mouse
- [ ] Expected: Node stays at new position

Test 4: Multiple Nodes
- [ ] Add 3 different nodes from menu
- [ ] Expected: All 3 nodes appear on canvas
- [ ] Expected: Each node is at a different position
- [ ] Expected: Can select and drag each independently

Test 5: Node Deletion
- [ ] Select a node
- [ ] Press Delete key
- [ ] Expected: Node disappears from canvas
- [ ] Expected: Node removed from nodeManager.nodes array

Test 6: State Inspection
- [ ] Add 2-3 nodes
- [ ] Open browser console
- [ ] Run: window.nodeManager.logState()
- [ ] Expected: Console shows all nodes with correct data


Architecture Diagram

┌─────────────────────────────────────────────────────────────────┐
│                        USER INTERFACE                            │
│  ┌────────────────┐  ┌──────────────────┐  ┌─────────────────┐ │
│  │  N8N Node Btn  │  │  Canvas Viewport │  │  Node Elements  │ │
│  └────────┬───────┘  └──────────────────┘  └─────────────────┘ │
└───────────┼──────────────────────────────────────────────────────┘
            │
            ▼
┌─────────────────────────────────────────────────────────────────┐
│                     OVERLAY MANAGER (Menu)                       │
│  ✅ Handles node selection popup                                 │
│  ✅ Loads node data from database                                │
│  ✅ Generates n8n-compatible JSON                                │
│  ➡️  Calls NodeManager.addNode()                                 │
└─────────────────────────────┬───────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                    NODE MANAGER (Bridge) ❌ MISSING              │
│  ❌ Receives node JSON from OverlayManager                       │
│  ❌ Stores nodes in memory (workflow state)                      │
│  ❌ Renders nodes visually on canvas                             │
│  ❌ Handles node interactions (click, drag, delete)              │
│  ❌ Manages connections between nodes                            │
│  ❌ Syncs with backend (save/load)                               │
└─────────────────┬───────────────────────────┬───────────────────┘
                  │                           │
                  ▼                           ▼
    ┌─────────────────────────┐   ┌────────────────────────────┐
    │  CANVAS MANAGER (View)  │   │  BACKEND (Persistence)     │
    │  ✅ Pan/zoom viewport    │   │  ❌ Save workflow          │
    │  ✅ Grid background      │   │  ❌ Load workflow          │
    │  ❌ Node rendering       │   │  ✅ Node data (n8n.simple) │
    └─────────────────────────┘   └────────────────────────────┘

Current State:
- ✅ Green boxes = Working
- ❌ Red items = Missing/Not working

The Gap:
- OverlayManager generates JSON ✅
- NodeManager receives JSON ❌ MISSING
- Nodes appear on canvas ❌ MISSING


Summary

What You Have (90% Complete)

  1. ✅ Functional menu system (overlay_manager.js)
  2. ✅ Node data from database (n8n.simple.node)
  3. ✅ Correct JSON generation (n8n format)
  4. ✅ Canvas viewport with pan/zoom (canvas_manager.js)

What You Need (The 10% Gap)

  1. ❌ NodeManager class - The bridge between menu and canvas
  2. ❌ Node rendering - Visual elements on canvas
  3. ❌ Node interactions - Click, drag, delete
  4. ❌ Backend sync - Save/load workflows
  5. ❌ Connection system - Link nodes together

Next Action

IMMEDIATE NEXT STEP:

  1. Create static/src/n8n/nodes/node_manager.js with the template provided
  2. Modify overlay_manager.js line 3401 to call nodeManager.addNode()
  3. Initialize NodeManager in your main initialization file
  4. Test: Select node from menu → See it on canvas!

This will give you a working MVP in 2-4 hours.

Resources Created for You

  1. ✅ Complete n8n research: how_n8n_creates_nodes.md
  2. ✅ This gap analysis: Current document
  3. ✅ Ready-to-use code templates: NodeManager MVP (above)
  4. ✅ Testing checklist: Verify each feature works

Questions to Answer Before Starting

  1. Where should node_manager.js be created?
  2. Suggested: static/src/n8n/nodes/node_manager.js

  3. Where is your main initialization file?

  4. Need to add nodeManager.init() there

  5. Do you have a Python controller for /canvas/workflow/save yet?

  6. If not, backend sync will be Phase 5

  7. What visual style do you want for nodes?

  8. Use template CSS provided or customize?

Let me know when you're ready to implement, and I'll guide you through each step!

Was this helpful?