IMPORTANT FOR AI ASSISTANTS: This document defines the plugin architecture for T2AutoTron. ALL new nodes should be created as plugins in
backend/plugins/. Do NOT create nodes infrontend/src/nodes/- that folder is deprecated.
NEW as of January 2026: Pure calculation functions are now shared between frontend and backend!
v3_migration/shared/logic/ ← SHARED CALCULATION FUNCTIONS
├── index.js ← Aggregates all for backend require()
├── LogicGateLogic.js ← calculateAnd/Or/Not/Xor + smartCompare
├── ColorLogic.js ← hsvToRgb, rgbToHsv, mixColors
├── TimeRangeLogic.js ← calculateTimeRange
├── DelayLogic.js ← toMilliseconds, UNIT_MULTIPLIERS
├── UtilityLogic.js ← processCounter, performMath, etc.
└── DeviceLogic.js ← normalizeHSVInput, buildHAPayload
// Get from window global (loaded by 00_SharedLogicLoader.js)
const T2SharedLogic = window.T2SharedLogic || {};
const { smartCompare, hsvToRgb } = T2SharedLogic;
// Use with optional fallback for safety
const result = T2SharedLogic.smartCompare?.(a, op, b) ?? legacyFallback(a, op, b);// Require directly from shared folder
const { smartCompare, hsvToRgb } = require('../../../../shared/logic');
// Use directly - always available
const result = smartCompare(a, op, b);- ✅ Single source of truth for calculations (no more dual maintenance!)
- ✅ Add logic once, both frontend and backend get it
- ✅ Pure functions with no UI dependencies
- ✅ Easy to test in isolation
T2AutoTron now has two execution layers:
| Layer | Location | Purpose | Runs When |
|---|---|---|---|
| Frontend Plugins | backend/plugins/*.js |
Visual UI in browser | Browser open |
| Backend Engine Nodes | backend/src/engine/nodes/*.js |
24/7 automation logic | Always (server-side) |
| Shared Logic | shared/logic/*.js |
Pure calculations | Both layers |
- Frontend plugins render the visual node editor and handle user interaction
- Backend engine nodes execute the same logic on the server, even when the browser is closed
- Shared logic contains pure calculation functions used by both
- This enables 24/7 automations - your lights keep changing colors while you sleep!
| Creating... | Frontend Plugin | Backend Node | Shared Logic |
|---|---|---|---|
| New automation node | ✅ Yes | ✅ Yes | Extract pure calcs to shared |
| Visual-only node | ✅ Yes | ❌ No | If has reusable calcs |
| Server-only logic | ❌ No | ✅ Yes | If has reusable calcs |
| New calculation function | ❌ No | ❌ No | ✅ Yes - add to shared! |
Most new nodes need BOTH - a frontend plugin for the visual editor AND a backend node for 24/7 execution. Extract pure calculations to shared/logic/ when possible!
T2AutoTron uses a plugin-based architecture where all visual nodes are loaded dynamically at runtime from the backend/plugins/ directory. This design allows:
- Protected Core: The compiled React/Vite bundle contains only the editor framework
- User-Extensible: Users can create/modify nodes without rebuilding
- Hot-Reloadable: Node changes take effect on page refresh (no rebuild required)
v3_migration/
├── shared/
│ └── logic/ ← SHARED CALCULATION FUNCTIONS (38 functions)
│ ├── index.js ← Aggregates all exports
│ ├── LogicGateLogic.js ← Logic gate calculations
│ ├── ColorLogic.js ← Color conversion
│ └── ...
├── backend/
│ ├── plugins/ ← ALL NODES GO HERE
│ │ ├── 00_ColorUtilsPlugin.js ← Shared utilities (loads first)
│ │ ├── AllInOneColorNode.js
│ │ ├── BackdropNode.js
│ │ ├── ColorGradientNode.js
│ │ ├── ConditionalSwitchNode.js
│ │ ├── DisplayNode.js
│ │ ├── HAGenericDeviceNode.js
│ │ ├── HSVControlNode.js
│ │ ├── HSVModifierNode.js
│ │ ├── IntegerSelectorNode.js
│ │ └── ... (other nodes)
│ ├── src/
│ │ └── server.js ← Serves plugins via /api/plugins
│ └── frontend/ ← Compiled frontend (from Vite build)
│
└── frontend/
├── src/
│ ├── Editor.jsx ← Core editor (PROTECTED)
│ ├── registries/
│ │ ├── NodeRegistry.js ← Node registration system (PROTECTED)
│ │ └── PluginLoader.js ← Loads plugins at runtime (PROTECTED)
│ ├── sockets.js ← Socket definitions (PROTECTED)
│ ├── utils/
│ │ └── ColorUtils.js ← For React-side usage (if any remain)
│ └── nodes/ ← DEPRECATED - DO NOT USE
│ └── registerNodes.js ← Should be empty or minimal
└── dist/ ← Vite build output
- Browser loads compiled React bundle
- Bundle exposes globals:
window.Rete,window.React,window.nodeRegistry,window.sockets - PluginLoader fetches
/api/plugins→ returns list of plugin JS files - Plugins are loaded via
<script>tags in alphabetical order - Each plugin registers itself with
window.nodeRegistry.register()
Plugins load alphabetically by filename. Use numeric prefixes to control order:
00_ColorUtilsPlugin.js- Loads first (shared utilities)AllInOneColorNode.js- Loads after shared utilitiesZzLastPlugin.js- Would load last
Every plugin follows this pattern:
(function() {
console.log("[NodeName] Loading plugin...");
// Check dependencies
if (!window.Rete || !window.React || !window.RefComponent || !window.sockets) {
console.error("[NodeName] Missing dependencies");
return;
}
// Get dependencies from window globals
const { ClassicPreset } = window.Rete;
const React = window.React;
const { useState, useEffect, useRef, useCallback } = React;
const RefComponent = window.RefComponent;
const sockets = window.sockets;
// -------------------------------------------------------------------------
// CSS INJECTION (inline styles for this node)
// -------------------------------------------------------------------------
const styleId = 'node-name-css';
if (!document.getElementById(styleId)) {
const style = document.createElement('style');
style.id = styleId;
style.innerHTML = `
.my-node { /* styles */ }
`;
document.head.appendChild(style);
}
// -------------------------------------------------------------------------
// SHARED UTILITIES (optional - use window.ColorUtils for color nodes)
// -------------------------------------------------------------------------
if (!window.ColorUtils) {
console.error("[NodeName] window.ColorUtils not found!");
}
const ColorUtils = window.ColorUtils;
// -------------------------------------------------------------------------
// NODE CLASS (Rete.js node definition)
// -------------------------------------------------------------------------
class MyNode extends ClassicPreset.Node {
constructor(changeCallback) {
super("My Node");
this.width = 300;
this.changeCallback = changeCallback;
// Define inputs
this.addInput("input1", new ClassicPreset.Input(
sockets.number, "Input Label"
));
// Define outputs
this.addOutput("output1", new ClassicPreset.Output(
sockets.number, "Output Label"
));
// Node properties (saved/restored)
this.properties = {
value: 0,
enabled: true
};
}
// Process data flow
data(inputs) {
const inputValue = inputs.input1?.[0] ?? 0;
// Process and return outputs
return {
output1: inputValue * 2
};
}
// Restore from saved graph
restore(state) {
if (state.properties) {
Object.assign(this.properties, state.properties);
}
}
}
// -------------------------------------------------------------------------
// REACT COMPONENT (UI for the node)
// -------------------------------------------------------------------------
function MyNodeComponent({ data, emit }) {
const [value, setValue] = useState(data.properties.value);
useEffect(() => {
data.changeCallback = () => {
setValue(data.properties.value);
};
return () => { data.changeCallback = null; };
}, [data]);
const handleChange = (e) => {
const val = Number(e.target.value);
setValue(val);
data.properties.value = val;
if (data.changeCallback) data.changeCallback();
};
// Render inputs/outputs with RefComponent
const inputs = Object.entries(data.inputs);
const outputs = Object.entries(data.outputs);
return React.createElement('div', { className: 'my-node' }, [
React.createElement('div', { key: 'header', className: 'header' }, data.label),
// Inputs
React.createElement('div', { key: 'inputs' },
inputs.map(([key, input]) =>
React.createElement('div', { key, className: 'io-row' }, [
React.createElement(RefComponent, {
key: 'socket',
init: ref => emit({
type: "render",
data: {
type: "socket",
element: ref,
payload: input.socket,
nodeId: data.id,
side: "input",
key
}
}),
unmount: ref => emit({ type: "unmount", data: { element: ref } })
}),
React.createElement('span', { key: 'label' }, input.label)
])
)
),
// Controls
React.createElement('div', {
key: 'controls',
onPointerDown: (e) => e.stopPropagation()
}, [
React.createElement('input', {
key: 'slider',
type: 'range',
value: value,
onChange: handleChange
})
]),
// Outputs
React.createElement('div', { key: 'outputs' },
outputs.map(([key, output]) =>
React.createElement('div', { key, className: 'io-row output' }, [
React.createElement('span', { key: 'label' }, output.label),
React.createElement(RefComponent, {
key: 'socket',
init: ref => emit({
type: "render",
data: {
type: "socket",
element: ref,
payload: output.socket,
nodeId: data.id,
side: "output",
key
}
}),
unmount: ref => emit({ type: "unmount", data: { element: ref } })
})
])
)
)
]);
}
// -------------------------------------------------------------------------
// REGISTER WITH NODE REGISTRY
// -------------------------------------------------------------------------
window.nodeRegistry.register('MyNode', {
label: "My Node", // Display name in add menu
category: "My Category", // Category in add menu
nodeClass: MyNode, // The node class
component: MyNodeComponent, // React component
factory: (cb) => new MyNode(cb) // Factory function
});
console.log("[MyNode] Registered");
})();Some nodes support an editable title in the node header (double-click to edit). Prefer these conventions:
- If the node already has a “Name” field, reuse
properties.customNameand displaycustomName || data.labelin the header. - If the node needs a title separate from other naming fields, use
properties.customTitle.
Implementation checklist:
- Swap the header title text to an
<input>ononDoubleClick. - Add
onPointerDown={(e) => e.stopPropagation()}on the<input>so editing doesn’t drag the node. - Commit on blur/Enter; cancel on Escape.
- Include the chosen property in
serialize()andrestore()so it persists in saved graphs.
Access via window.sockets:
const sockets = window.sockets;
sockets.number // Numeric values
sockets.boolean // True/False
sockets.string // Text values
sockets.any // Accepts any type
sockets.object // Object/HSV dataYou can also create custom sockets:
const mySocket = new ClassicPreset.Socket("my_custom_type");For color-related nodes:
const ColorUtils = window.ColorUtils;
ColorUtils.hsvToRgb(h, s, v) // h,s,v in 0-1 → [r,g,b] 0-255
ColorUtils.rgbToHsv(r, g, b) // r,g,b 0-255 → {hue, sat, val} 0-1
ColorUtils.hsvToRgbDegrees(h, s, v) // h: 0-360, s,v: 0-100 → {r,g,b}
ColorUtils.kelvinToRGB(k) // Color temp → {r,g,b}
ColorUtils.kelvinToHSV(k) // Color temp → {hue, saturation, brightness}
ColorUtils.hexToRgb(hex) // "#RRGGBB" → {r,g,b}
ColorUtils.rgbToHex(r, g, b) // → "#RRGGBB"
ColorUtils.interpolate(v, min, max, start, end)
ColorUtils.clamp(value, min, max)Date/time library for time-based nodes:
const { DateTime } = window.luxon;
const now = DateTime.now();For real-time communication with backend:
window.socket.emit('event', data);
window.socket.on('response', callback);Standard categories for organizing nodes:
"Home Assistant"- HA device integrations"Plugs"- Smart plug controls"Timer/Event"- Time and trigger nodes"Inputs"- User input nodes (buttons, sliders)"Logic"- Conditional/logic operations"CC_Control_Nodes"- Color control nodes"Color"- Color utilities"Utility"- Backdrops, display, helpers"Other"- Miscellaneous
- Create file:
backend/plugins/MyNewNode.js - Follow the template above
- Refresh browser - node appears in add menu
For grouping/organizing other nodes:
window.nodeRegistry.register('MyBackdrop', {
label: "My Backdrop",
category: "Utility",
nodeClass: MyBackdropClass,
component: MyBackdropComponent,
factory: (cb) => new MyBackdropClass(cb),
isBackdrop: true // ← Special flag for backdrop handling
});For nodes that need special update handling (like color sliders):
window.nodeRegistry.register('MyColorNode', {
label: "Color Control",
category: "CC_Control_Nodes",
nodeClass: MyColorClass,
component: MyColorComponent,
factory: (cb) => new MyColorClass(cb),
updateStrategy: 'dataflow' // ← Special handling for performance
});Prevent node dragging when interacting with controls:
React.createElement('div', {
onPointerDown: (e) => e.stopPropagation()
}, /* controls */)For sliders/frequent updates:
const lastUpdateRef = useRef(0);
const timeoutRef = useRef(null);
const updateState = (updates) => {
Object.assign(data.properties, updates);
const now = Date.now();
if (now - lastUpdateRef.current >= 50) {
if (data.changeCallback) data.changeCallback();
lastUpdateRef.current = now;
} else {
clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => {
if (data.changeCallback) data.changeCallback();
lastUpdateRef.current = Date.now();
}, 50);
}
};const [isCollapsed, setIsCollapsed] = useState(false);
// Toggle button
React.createElement('div', {
className: 'collapse-toggle',
onPointerDown: (e) => { e.stopPropagation(); setIsCollapsed(!isCollapsed); }
}, isCollapsed ? "▶" : "▼")
// Conditionally render controls
!isCollapsed && React.createElement('div', { className: 'controls' }, /* ... */)If you find a node in frontend/src/nodes/ that needs to be converted:
- Read the
.jsxfile and.cssfile - Create new plugin in
backend/plugins/ - Convert JSX to
React.createElement()calls - Inject CSS as inline styles
- Replace ES6 imports with window globals
- Delete the old React files
// JSX (React nodes)
<div className="my-class" onClick={handleClick}>
<span>Hello</span>
</div>
// createElement (Plugins)
React.createElement('div', { className: 'my-class', onClick: handleClick }, [
React.createElement('span', { key: 'text' }, 'Hello')
])- Save your plugin file
- Refresh the browser (F5)
- Check console for registration message
- Open Add Node menu and verify node appears
- Add node to canvas and test functionality
The plugin loaded before the main bundle. Check alphabetical ordering.
- Check for JavaScript errors in console
- Verify
window.nodeRegistry.register()is called - Check category spelling matches expected categories
Ensure onPointerDown: (e) => e.stopPropagation() is on interactive elements.
Verify socket types match between nodes. Use sockets.any for flexible connections.
When asked to create a new node:
- ✅ Create frontend plugin in
backend/plugins/NodeName.js - ✅ Create backend node in
backend/src/engine/nodes/(for 24/7 execution) - ✅ Use the IIFE pattern for frontend plugins
- ✅ Use
React.createElement()not JSX in frontend plugins - ✅ Inject CSS inline in frontend plugins
- ✅ Register frontend with
window.nodeRegistry - ✅ Register backend with
require('../BackendNodeRegistry').register() - ❌ Do NOT create in
frontend/src/nodes/ - ❌ Do NOT use ES6 imports in frontend plugins
This section covers the server-side node implementations that run even when the browser is closed.
backend/src/engine/
├── BackendEngine.js # Main engine - 100ms tick loop, graph processing
├── BackendNodeRegistry.js # Node type registry with create() factory
├── index.js # Exports engine singleton + registry
└── nodes/ # Backend node implementations
├── TimeNodes.js # CurrentTime, TimeRange, DayOfWeek, TimeOfDay
├── LogicNodes.js # AND, OR, NOT, Compare, Switch, Threshold, Latch
├── DelayNode.js # Delay, Debounce, Retriggerable modes
├── HADeviceNodes.js # HALight, HASwitch, HASensor, HAGenericDevice
├── HueLightNodes.js # HueLight, HueGroup (direct bridge API)
├── KasaLightNodes.js # KasaLight, KasaPlug (direct local API)
├── ColorNodes.js # SplineTimelineColor, HSVToRGB, RGBToHSV
├── BufferNodes.js # BufferReader, BufferWriter (cross-node state)
├── UtilityNodes.js # Counter, Random, Display, Sender, Receiver
└── WeatherNodes.js # Weather data nodes
Backend nodes are pure JavaScript classes without React/browser dependencies:
/**
* MyNode.js - Backend implementation
*/
const registry = require('../BackendNodeRegistry');
class MyNode {
constructor() {
this.id = null; // Set by engine when loading graph
this.label = 'My Node'; // Display name
this.properties = { // Node state (loaded from saved graph)
value: 0,
enabled: true
};
}
/**
* Restore properties from saved graph
* Called when engine loads a graph
*/
restore(data) {
if (data.properties) {
Object.assign(this.properties, data.properties);
}
}
/**
* Process inputs and return outputs
* Called every engine tick (~100ms)
*
* @param {Object} inputs - Input values keyed by socket name
* e.g., { trigger: [true], value: [42] }
* @returns {Object} - Output values keyed by socket name
* e.g., { output: 84, triggered: true }
*/
data(inputs) {
// Inputs are arrays (can have multiple connections)
const inputValue = inputs.value?.[0] ?? 0;
const trigger = inputs.trigger?.[0] ?? false;
// Process logic
const result = inputValue * 2;
// Return outputs (used by connected nodes)
return {
output: result,
triggered: trigger
};
}
/**
* Optional: Cleanup when node is removed
*/
destroy() {
// Clear any timers, close connections, etc.
}
}
// Register with backend registry
registry.register('MyNode', MyNode);
module.exports = { MyNode };| Aspect | Frontend Plugin | Backend Node |
|---|---|---|
| File location | backend/plugins/ |
backend/src/engine/nodes/ |
| Module format | IIFE (self-executing) | CommonJS (require/module.exports) |
| UI rendering | React.createElement() | None - pure logic |
| Dependencies | window.* globals | require() imports |
| Execution | Browser (when open) | Server (24/7) |
| Registry | window.nodeRegistry.register() |
registry.register() |
Backend nodes can control devices directly:
// Home Assistant
const haManager = require('../../devices/managers/homeAssistantManager');
await haManager.controlDevice('light.living_room', { on: true, brightness: 255 });
// Philips Hue
const hueManager = require('../../devices/managers/hueManager');
await hueManager.setLightState('1', { on: true, hue: 10000, sat: 254 });
// TP-Link Kasa
const kasaManager = require('../../devices/managers/kasaManager');
await kasaManager.setPlugState('192.168.1.100', true);For cross-node state sharing (Sender/Receiver pattern):
const bufferManager = require('../../devices/managers/bufferManager');
// Write to buffer
bufferManager.set('[HSV] My Color', { hue: 0.5, saturation: 1, brightness: 254 });
// Read from buffer
const value = bufferManager.get('[HSV] My Color');
// List all buffers
const keys = bufferManager.keys();The backend registry maps frontend display labels to backend class names. Add your node to the mapping in BackendNodeRegistry.js:
// In getByLabel() method
const labelMappings = {
'My Node': 'MyNode', // ← Add your mapping
'Time of Day': 'TimeOfDayNode',
'Timeline Color': 'SplineTimelineColorNode',
// ... etc
};# Run backend tests
cd v3_migration/backend && npm test
# Test specific node
npm test -- --grep "MyNode"const registry = require('../BackendNodeRegistry');
class DoubleValueNode {
constructor() {
this.id = null;
this.label = 'Double Value';
this.properties = { multiplier: 2 };
}
restore(data) {
if (data.properties) Object.assign(this.properties, data.properties);
}
data(inputs) {
const value = inputs.value?.[0] ?? 0;
return { result: value * this.properties.multiplier };
}
}
registry.register('DoubleValueNode', DoubleValueNode);
module.exports = { DoubleValueNode };(function() {
if (!window.Rete || !window.React || !window.nodeRegistry) return;
const { ClassicPreset } = window.Rete;
const React = window.React;
const sockets = window.sockets;
class DoubleValueNode extends ClassicPreset.Node {
constructor(changeCallback) {
super("Double Value");
this.changeCallback = changeCallback;
this.addInput("value", new ClassicPreset.Input(sockets.number, "Value"));
this.addOutput("result", new ClassicPreset.Output(sockets.number, "Result"));
this.properties = { multiplier: 2 };
}
data(inputs) {
const value = inputs.value?.[0] ?? 0;
return { result: value * this.properties.multiplier };
}
restore(state) {
if (state.properties) Object.assign(this.properties, state.properties);
}
}
function DoubleValueComponent({ data, emit }) {
// Render node UI with sockets
return React.createElement('div', { className: 'node-content' },
React.createElement('div', { className: 'node-header' }, 'Double Value')
// ... socket rendering
);
}
window.nodeRegistry.register('DoubleValueNode', {
label: "Double Value",
category: "Utility",
nodeClass: DoubleValueNode,
component: DoubleValueComponent,
factory: (cb) => new DoubleValueNode(cb)
});
})();const labelMappings = {
'Double Value': 'DoubleValueNode', // ← Add this line
// ... other mappings
};Now your node works in both the visual editor AND runs 24/7 on the server!