feat: enhance node registration and management with support for multiple nodes and improved UI elements. Fixes #220

This commit is contained in:
Will Miao
2025-06-26 23:00:55 +08:00
parent eb57e04e95
commit 64dd2ed141
10 changed files with 514 additions and 166 deletions

View File

@@ -1,32 +1,20 @@
import logging
import os
import sys
import threading
import asyncio
from server import PromptServer # type: ignore
from aiohttp import web
from ..services.settings_manager import settings
from ..utils.usage_stats import UsageStats
from ..utils.lora_metadata import extract_trained_words
from ..config import config
from ..utils.constants import SUPPORTED_MEDIA_EXTENSIONS
from ..utils.constants import SUPPORTED_MEDIA_EXTENSIONS, NODE_TYPES, DEFAULT_NODE_COLOR
import re
logger = logging.getLogger(__name__)
# Download status tracking
download_task = None
is_downloading = False
download_progress = {
'total': 0,
'completed': 0,
'current_model': '',
'status': 'idle', # idle, running, paused, completed, error
'errors': [],
'last_error': None,
'start_time': None,
'end_time': None,
'processed_models': set(), # Track models that have been processed
'refreshed_models': set() # Track models that had metadata refreshed
}
standalone_mode = 'nodes' not in sys.modules
# Node registry for tracking active workflow nodes
class NodeRegistry:
@@ -34,33 +22,45 @@ class NodeRegistry:
def __init__(self):
self._lock = threading.RLock()
self._current_graph_id = None
self._nodes = {} # node_id -> node_info
self._registry_updated = threading.Event()
def register_node(self, node_id, bgcolor, title, graph_id):
"""Register a node for the current workflow"""
def register_nodes(self, nodes):
"""Register multiple nodes at once, replacing existing registry"""
with self._lock:
# If graph_id changed, clear existing registry for new workflow
if self._current_graph_id != graph_id:
self._current_graph_id = graph_id
self._nodes.clear()
logger.info(f"Workflow changed to {graph_id}, cleared node registry")
# Clear existing registry
self._nodes.clear()
# Register the node
self._nodes[node_id] = {
'id': node_id,
'bgcolor': bgcolor,
'title': title,
'graph_id': graph_id
}
# Register all new nodes
for node in nodes:
node_id = node['node_id']
node_type = node.get('type', '')
# Convert node type name to integer
type_id = NODE_TYPES.get(node_type, 0) # 0 for unknown types
# Handle null bgcolor with default color
bgcolor = node.get('bgcolor')
if bgcolor is None:
bgcolor = DEFAULT_NODE_COLOR
self._nodes[node_id] = {
'id': node_id,
'bgcolor': bgcolor,
'title': node.get('title'),
'type': type_id,
'type_name': node_type
}
logger.info(f"Registered node {node_id} ({title}) for workflow {graph_id} with bgcolor {bgcolor}")
logger.debug(f"Registered {len(nodes)} nodes in registry")
# Signal that registry has been updated
self._registry_updated.set()
def get_registry(self):
"""Get current registry information"""
with self._lock:
return {
'current_graph_id': self._current_graph_id,
'nodes': dict(self._nodes), # Return a copy
'node_count': len(self._nodes)
}
@@ -68,9 +68,13 @@ class NodeRegistry:
def clear_registry(self):
"""Clear the entire registry"""
with self._lock:
self._current_graph_id = None
self._nodes.clear()
logger.info("Node registry cleared")
def wait_for_update(self, timeout=1.0):
"""Wait for registry update with timeout"""
self._registry_updated.clear()
return self._registry_updated.wait(timeout)
# Global registry instance
node_registry = NodeRegistry()
@@ -100,7 +104,7 @@ class MiscRoutes:
app.router.add_get('/api/model-example-files', MiscRoutes.get_model_example_files)
# Node registry endpoints
app.router.add_post('/api/register-node', MiscRoutes.register_node)
app.router.add_post('/api/register-nodes', MiscRoutes.register_nodes)
app.router.add_get('/api/get-registry', MiscRoutes.get_registry)
@staticmethod
@@ -135,10 +139,6 @@ class MiscRoutes:
'error': f"Failed to delete {filename}: {str(e)}"
}, status=500)
# If we want to completely remove the cache folder too (optional,
# but we'll keep the folder structure in place here)
# shutil.rmtree(cache_folder)
return web.json_response({
'success': True,
'message': f"Successfully cleared {len(deleted_files)} cache files",
@@ -457,70 +457,68 @@ class MiscRoutes:
}, status=500)
@staticmethod
async def register_node(request):
async def register_nodes(request):
"""
Register a Lora node for the current workflow
Register multiple Lora nodes at once
Expects a JSON body with:
{
"node_id": 123,
"bgcolor": "#535",
"title": "Lora Loader (LoraManager)",
"graph_id": "151410b3-7845-4561-aac4-8968574e9ba2"
"nodes": [
{
"node_id": 123,
"bgcolor": "#535",
"title": "Lora Loader (LoraManager)"
},
...
]
}
"""
try:
data = await request.json()
# Validate required fields
node_id = data.get('node_id')
bgcolor = data.get('bgcolor')
title = data.get('title')
graph_id = data.get('graph_id')
nodes = data.get('nodes', [])
if node_id is None:
if not isinstance(nodes, list):
return web.json_response({
'success': False,
'error': 'Missing node_id parameter'
'error': 'nodes must be a list'
}, status=400)
if not bgcolor:
return web.json_response({
'success': False,
'error': 'Missing bgcolor parameter'
}, status=400)
# Validate each node
for i, node in enumerate(nodes):
if not isinstance(node, dict):
return web.json_response({
'success': False,
'error': f'Node {i} must be an object'
}, status=400)
node_id = node.get('node_id')
if node_id is None:
return web.json_response({
'success': False,
'error': f'Node {i} missing node_id parameter'
}, status=400)
# Validate node_id is an integer
try:
node['node_id'] = int(node_id)
except (ValueError, TypeError):
return web.json_response({
'success': False,
'error': f'Node {i} node_id must be an integer'
}, status=400)
if not title:
return web.json_response({
'success': False,
'error': 'Missing title parameter'
}, status=400)
if not graph_id:
return web.json_response({
'success': False,
'error': 'Missing graph_id parameter'
}, status=400)
# Validate node_id is an integer
try:
node_id = int(node_id)
except (ValueError, TypeError):
return web.json_response({
'success': False,
'error': 'node_id must be an integer'
}, status=400)
# Register the node
node_registry.register_node(node_id, bgcolor, title, graph_id)
# Register all nodes
node_registry.register_nodes(nodes)
return web.json_response({
'success': True,
'message': f'Node {node_id} registered successfully'
'message': f'{len(nodes)} nodes registered successfully'
})
except Exception as e:
logger.error(f"Failed to register node: {e}", exc_info=True)
logger.error(f"Failed to register nodes: {e}", exc_info=True)
return web.json_response({
'success': False,
'error': str(e)
@@ -528,8 +526,46 @@ class MiscRoutes:
@staticmethod
async def get_registry(request):
"""Get current node registry information"""
"""Get current node registry information by refreshing from frontend"""
try:
# Check if running in standalone mode
if standalone_mode:
logger.warning("Registry refresh not available in standalone mode")
return web.json_response({
'success': False,
'error': 'Standalone Mode Active',
'message': 'Cannot interact with ComfyUI in standalone mode.'
}, status=503)
# Send message to frontend to refresh registry
try:
PromptServer.instance.send_sync("lora_registry_refresh", {})
logger.debug("Sent registry refresh request to frontend")
except Exception as e:
logger.error(f"Failed to send registry refresh message: {e}")
return web.json_response({
'success': False,
'error': 'Communication Error',
'message': f'Failed to communicate with ComfyUI frontend: {str(e)}'
}, status=500)
# Wait for registry update with timeout
def wait_for_registry():
return node_registry.wait_for_update(timeout=1.0)
# Run the wait in a thread to avoid blocking the event loop
loop = asyncio.get_event_loop()
registry_updated = await loop.run_in_executor(None, wait_for_registry)
if not registry_updated:
logger.warning("Registry refresh timeout after 1 second")
return web.json_response({
'success': False,
'error': 'Timeout Error',
'message': 'Registry refresh timeout - ComfyUI frontend may not be responsive'
}, status=408)
# Get updated registry
registry_info = node_registry.get_registry()
return web.json_response({
@@ -541,5 +577,6 @@ class MiscRoutes:
logger.error(f"Failed to get registry: {e}", exc_info=True)
return web.json_response({
'success': False,
'error': str(e)
'error': 'Internal Error',
'message': str(e)
}, status=500)

View File

@@ -7,6 +7,15 @@ NSFW_LEVELS = {
"Blocked": 32, # Probably not actually visible through the API without being logged in on model owner account?
}
# Node type constants
NODE_TYPES = {
"Lora Loader (LoraManager)": 1,
"Lora Stacker (LoraManager)": 2
}
# Default ComfyUI node color when bgcolor is null
DEFAULT_NODE_COLOR = "#353535"
# preview extensions
PREVIEW_EXTENSIONS = [
'.webp',

View File

@@ -116,4 +116,83 @@
background: var(--lora-accent);
color: white;
border-color: var(--lora-accent);
}
/* Node Selector */
.node-selector {
position: fixed;
background: var(--lora-surface);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-xs);
padding: 4px 0;
min-width: 200px;
max-width: 350px;
max-height: 400px;
overflow-y: auto;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 1000;
display: none;
backdrop-filter: blur(10px);
}
.node-item {
padding: 10px 15px;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
color: var(--text-color);
background: var(--lora-surface);
transition: background-color 0.2s;
border-bottom: 1px solid var(--border-color);
}
.node-item:last-child {
border-bottom: none;
}
.node-item:hover {
background-color: var(--lora-accent);
color: var(--lora-text);
}
.node-icon-indicator {
width: 24px;
height: 24px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.node-icon-indicator i {
color: white;
font-size: 12px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.node-icon-indicator.all-nodes {
background: linear-gradient(45deg, #4a90e2, #357abd);
}
/* Remove old node-color-indicator styles */
.node-color-indicator {
display: none;
}
.send-all-item {
border-top: 1px solid var(--border-color);
font-weight: 500;
background: var(--card-bg);
}
.send-all-item:hover {
background-color: var(--lora-accent);
color: var(--lora-text);
}
.send-all-item i {
width: 16px;
text-align: center;
}

View File

@@ -101,4 +101,25 @@ export const NSFW_LEVELS = {
X: 8,
XXX: 16,
BLOCKED: 32
};
};
// Node type constants
export const NODE_TYPES = {
LORA_LOADER: 1,
LORA_STACKER: 2
};
// Node type names to IDs mapping
export const NODE_TYPE_NAMES = {
"Lora Loader (LoraManager)": NODE_TYPES.LORA_LOADER,
"Lora Stacker (LoraManager)": NODE_TYPES.LORA_STACKER
};
// Node type icons
export const NODE_TYPE_ICONS = {
[NODE_TYPES.LORA_LOADER]: "fas fa-l",
[NODE_TYPES.LORA_STACKER]: "fas fa-s"
};
// Default ComfyUI node color when bgcolor is null
export const DEFAULT_NODE_COLOR = "#353535";

View File

@@ -1,7 +1,7 @@
import { state } from '../state/index.js';
import { resetAndReload } from '../api/loraApi.js';
import { getStorageItem, setStorageItem } from './storageHelpers.js';
import { NSFW_LEVELS } from './constants.js';
import { NODE_TYPES, NODE_TYPE_ICONS, DEFAULT_NODE_COLOR } from './constants.js';
/**
* Utility function to copy text to clipboard with fallback for older browsers
@@ -263,56 +263,6 @@ export function updatePanelPositions() {
}
}
}
}
// Update the toggleFolderTags function
export function toggleFolderTags() {
const folderTags = document.querySelector('.folder-tags');
const toggleBtn = document.querySelector('.toggle-folders-btn i');
if (folderTags) {
folderTags.classList.toggle('collapsed');
if (folderTags.classList.contains('collapsed')) {
// Change icon to indicate folders are hidden
toggleBtn.className = 'fas fa-folder-plus';
toggleBtn.parentElement.title = 'Show folder tags';
setStorageItem('folderTagsCollapsed', 'true');
} else {
// Change icon to indicate folders are visible
toggleBtn.className = 'fas fa-folder-minus';
toggleBtn.parentElement.title = 'Hide folder tags';
setStorageItem('folderTagsCollapsed', 'false');
}
// Update panel positions after toggling
// Use a small delay to ensure the DOM has updated
setTimeout(() => {
updatePanelPositions();
}, 50);
}
}
// Add this to your existing initialization code
export function initFolderTagsVisibility() {
const isCollapsed = getStorageItem('folderTagsCollapsed');
if (isCollapsed) {
const folderTags = document.querySelector('.folder-tags');
const toggleBtn = document.querySelector('.toggle-folders-btn i');
if (folderTags) {
folderTags.classList.add('collapsed');
}
if (toggleBtn) {
toggleBtn.className = 'fas fa-folder-plus';
toggleBtn.parentElement.title = 'Show folder tags';
}
} else {
const toggleBtn = document.querySelector('.toggle-folders-btn i');
if (toggleBtn) {
toggleBtn.className = 'fas fa-folder-minus';
toggleBtn.parentElement.title = 'Hide folder tags';
}
}
}
export function initBackToTop() {
@@ -367,33 +317,53 @@ export function getNSFWLevelName(level) {
*/
export async function sendLoraToWorkflow(loraSyntax, replaceMode = false, syntaxType = 'lora') {
try {
let loraNodes = [];
let isDesktopMode = false;
// Get registry information from the new endpoint
const registryResponse = await fetch('/api/get-registry');
const registryData = await registryResponse.json();
// Get the current workflow from localStorage
const workflowData = localStorage.getItem('workflow');
if (workflowData) {
// Web browser mode - extract node IDs from workflow
const workflow = JSON.parse(workflowData);
// Find all Lora Loader (LoraManager) nodes
if (workflow.nodes && Array.isArray(workflow.nodes)) {
for (const node of workflow.nodes) {
if (node.type === "Lora Loader (LoraManager)") {
loraNodes.push(node.id);
}
}
}
if (loraNodes.length === 0) {
showToast('No Lora Loader nodes found in the workflow', 'warning');
if (!registryData.success) {
// Handle specific error cases
if (registryData.error === 'Standalone Mode Active') {
// Standalone mode - show warning with specific message
showToast(registryData.message || 'Cannot interact with ComfyUI in standalone mode', 'warning');
return false;
} else {
// Other errors - show error toast
showToast(registryData.message || registryData.error || 'Failed to get workflow information', 'error');
return false;
}
} else {
// ComfyUI Desktop mode - don't specify node IDs and let backend handle it
isDesktopMode = true;
}
// Success case - check node count
if (registryData.data.node_count === 0) {
// No nodes found - show warning
showToast('No Lora Loader or Lora Stacker nodes found in workflow', 'warning');
return false;
} else if (registryData.data.node_count > 1) {
// Multiple nodes - show selector
showNodeSelector(registryData.data.nodes, loraSyntax, replaceMode, syntaxType);
return true;
} else {
// Single node - send directly
const nodeId = Object.keys(registryData.data.nodes)[0];
return await sendToSpecificNode([nodeId], loraSyntax, replaceMode, syntaxType);
}
} catch (error) {
console.error('Failed to get registry:', error);
showToast('Failed to communicate with ComfyUI', 'error');
return false;
}
}
/**
* Send LoRA to specific nodes
* @param {Array|undefined} nodeIds - Array of node IDs or undefined for desktop mode
* @param {string} loraSyntax - The LoRA syntax to send
* @param {boolean} replaceMode - Whether to replace existing LoRAs
* @param {string} syntaxType - The type of syntax ('lora' or 'recipe')
*/
async function sendToSpecificNode(nodeIds, loraSyntax, replaceMode, syntaxType) {
try {
// Call the backend API to update the lora code
const response = await fetch('/api/update-lora-code', {
method: 'POST',
@@ -401,7 +371,7 @@ export async function sendLoraToWorkflow(loraSyntax, replaceMode = false, syntax
'Content-Type': 'application/json'
},
body: JSON.stringify({
node_ids: isDesktopMode ? undefined : loraNodes,
node_ids: nodeIds,
lora_code: loraSyntax,
mode: replaceMode ? 'replace' : 'append'
})
@@ -428,6 +398,188 @@ export async function sendLoraToWorkflow(loraSyntax, replaceMode = false, syntax
}
}
// Global variable to track active node selector state
let nodeSelectorState = {
isActive: false,
clickHandler: null,
selectorClickHandler: null
};
/**
* Show node selector popup near mouse position
* @param {Object} nodes - Registry nodes data
* @param {string} loraSyntax - The LoRA syntax to send
* @param {boolean} replaceMode - Whether to replace existing LoRAs
* @param {string} syntaxType - The type of syntax ('lora' or 'recipe')
*/
function showNodeSelector(nodes, loraSyntax, replaceMode, syntaxType) {
const selector = document.getElementById('nodeSelector');
if (!selector) return;
// Clean up any existing state
hideNodeSelector();
// Generate node list HTML with icons and proper colors
const nodeItems = Object.values(nodes).map(node => {
const iconClass = NODE_TYPE_ICONS[node.type] || 'fas fa-question-circle';
const bgColor = node.bgcolor || DEFAULT_NODE_COLOR;
return `
<div class="node-item" data-node-id="${node.id}">
<div class="node-icon-indicator" style="background-color: ${bgColor}">
<i class="${iconClass}"></i>
</div>
<span>#${node.id} ${node.title}</span>
</div>
`;
}).join('');
selector.innerHTML = `
${nodeItems}
<div class="node-item send-all-item" data-action="send-all">
<div class="node-icon-indicator all-nodes">
<i class="fas fa-broadcast-tower"></i>
</div>
<span>Send to All</span>
</div>
`;
// Position near mouse
positionNearMouse(selector);
// Show selector
selector.style.display = 'block';
nodeSelectorState.isActive = true;
// Setup event listeners with proper cleanup
setupNodeSelectorEvents(selector, nodes, loraSyntax, replaceMode, syntaxType);
}
/**
* Setup event listeners for node selector
* @param {HTMLElement} selector - The selector element
* @param {Object} nodes - Registry nodes data
* @param {string} loraSyntax - The LoRA syntax to send
* @param {boolean} replaceMode - Whether to replace existing LoRAs
* @param {string} syntaxType - The type of syntax ('lora' or 'recipe')
*/
function setupNodeSelectorEvents(selector, nodes, loraSyntax, replaceMode, syntaxType) {
// Clean up any existing event listeners
cleanupNodeSelectorEvents();
// Handle clicks outside to close
nodeSelectorState.clickHandler = (e) => {
if (!selector.contains(e.target)) {
hideNodeSelector();
}
};
// Handle node selection
nodeSelectorState.selectorClickHandler = async (e) => {
const nodeItem = e.target.closest('.node-item');
if (!nodeItem) return;
e.stopPropagation();
const action = nodeItem.dataset.action;
const nodeId = nodeItem.dataset.nodeId;
if (action === 'send-all') {
// Send to all nodes
const allNodeIds = Object.keys(nodes);
await sendToSpecificNode(allNodeIds, loraSyntax, replaceMode, syntaxType);
} else if (nodeId) {
// Send to specific node
await sendToSpecificNode([nodeId], loraSyntax, replaceMode, syntaxType);
}
hideNodeSelector();
};
// Add event listeners with a small delay to prevent immediate triggering
setTimeout(() => {
if (nodeSelectorState.isActive) {
document.addEventListener('click', nodeSelectorState.clickHandler);
selector.addEventListener('click', nodeSelectorState.selectorClickHandler);
}
}, 100);
}
/**
* Clean up node selector event listeners
*/
function cleanupNodeSelectorEvents() {
if (nodeSelectorState.clickHandler) {
document.removeEventListener('click', nodeSelectorState.clickHandler);
nodeSelectorState.clickHandler = null;
}
if (nodeSelectorState.selectorClickHandler) {
const selector = document.getElementById('nodeSelector');
if (selector) {
selector.removeEventListener('click', nodeSelectorState.selectorClickHandler);
}
nodeSelectorState.selectorClickHandler = null;
}
}
/**
* Hide node selector
*/
function hideNodeSelector() {
const selector = document.getElementById('nodeSelector');
if (selector) {
selector.style.display = 'none';
selector.innerHTML = ''; // Clear content to prevent memory leaks
}
// Clean up event listeners
cleanupNodeSelectorEvents();
nodeSelectorState.isActive = false;
}
/**
* Position element near mouse cursor
* @param {HTMLElement} element - Element to position
*/
function positionNearMouse(element) {
// Get current mouse position from last mouse event or use default
const mouseX = window.lastMouseX || window.innerWidth / 2;
const mouseY = window.lastMouseY || window.innerHeight / 2;
// Show element temporarily to get dimensions
element.style.visibility = 'hidden';
element.style.display = 'block';
const rect = element.getBoundingClientRect();
const viewportWidth = document.documentElement.clientWidth;
const viewportHeight = document.documentElement.clientHeight;
// Calculate position with offset from mouse
let x = mouseX + 10;
let y = mouseY + 10;
// Ensure element doesn't go offscreen
if (x + rect.width > viewportWidth) {
x = mouseX - rect.width - 10;
}
if (y + rect.height > viewportHeight) {
y = mouseY - rect.height - 10;
}
// Apply position
element.style.left = `${x}px`;
element.style.top = `${y}px`;
element.style.visibility = 'visible';
}
// Track mouse position for node selector positioning
document.addEventListener('mousemove', (e) => {
window.lastMouseX = e.clientX;
window.lastMouseY = e.clientY;
});
/**
* Opens the example images folder for a specific model
* @param {string} modelHash - The SHA256 hash of the model

View File

@@ -56,4 +56,8 @@
<button class="nsfw-level-btn" data-level="16">XXX</button>
</div>
</div>
</div>
<div id="nodeSelector" class="node-selector">
<!-- Dynamic node list will be populated here -->
</div>

View File

@@ -76,7 +76,7 @@ app.registerExtension({
// Standard mode - update a specific node
const node = app.graph.getNodeById(+id);
if (!node || node.comfyClass !== "Lora Loader (LoraManager)") {
if (!node || (node.comfyClass !== "Lora Loader (LoraManager)" && node.comfyClass !== "Lora Stacker (LoraManager)")) {
console.warn("Node not found or not a LoraLoader:", id);
return;
}
@@ -218,9 +218,9 @@ app.registerExtension({
// Ensure the node is registered after creation
// Call registration
setTimeout(() => {
this.registerNode();
}, 0);
// setTimeout(() => {
// this.registerNode();
// }, 0);
});
}
},

View File

@@ -147,9 +147,9 @@ app.registerExtension({
};
// Call registration
setTimeout(() => {
this.registerNode();
}, 0);
// setTimeout(() => {
// this.registerNode();
// }, 0);
});
}
},

View File

@@ -601,7 +601,7 @@ export function addLorasWidget(node, name, opts, callback) {
});
// Calculate height based on number of loras and fixed sizes
const calculatedHeight = CONTAINER_PADDING + HEADER_HEIGHT + (Math.min(totalVisibleEntries, 8) * LORA_ENTRY_HEIGHT);
const calculatedHeight = CONTAINER_PADDING + HEADER_HEIGHT + (Math.min(totalVisibleEntries, 10) * LORA_ENTRY_HEIGHT);
updateWidgetHeight(container, calculatedHeight, defaultHeight, node);
};

View File

@@ -13,6 +13,11 @@ app.registerExtension({
this.updateUsageStats(detail.prompt_id);
}
});
// Listen for registry refresh requests
api.addEventListener("lora_registry_refresh", () => {
this.refreshRegistry();
});
},
async updateUsageStats(promptId) {
@@ -32,5 +37,46 @@ app.registerExtension({
} catch (error) {
console.error("Error updating usage statistics:", error);
}
},
async refreshRegistry() {
try {
// Get current workflow nodes
const prompt = await app.graphToPrompt();
const workflow = prompt.workflow;
if (!workflow || !workflow.nodes) {
console.warn("No workflow nodes found for registry refresh");
return;
}
// Find all Lora nodes
const loraNodes = [];
for (const node of workflow.nodes.values()) {
if (node.type === "Lora Loader (LoraManager)" || node.type === "Lora Stacker (LoraManager)") {
loraNodes.push({
node_id: node.id,
bgcolor: node.bgcolor || null,
title: node.title || node.type,
type: node.type
});
}
}
const response = await fetch('/api/register-nodes', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ nodes: loraNodes }),
});
if (!response.ok) {
console.warn("Failed to register Lora nodes:", response.statusText);
} else {
console.log(`Successfully registered ${loraNodes.length} Lora nodes`);
}
} catch (error) {
console.error("Error refreshing registry:", error);
}
}
});