// Dashboard ResourceProvider JavaScript // Handles resource_provider dashboard functionality including automatic slice management, grid integration, and node management if (window.resourceProviderDashboardInitialized) { console.debug('ResourceProvider dashboard already initialized; skipping'); } else { window.resourceProviderDashboardInitialized = true; document.addEventListener('DOMContentLoaded', function() { console.log('🚜 ResourceProvider Dashboard JavaScript loaded - Automatic Slice System'); // Initialize dashboard initializeResourceProviderDashboard(); initializeAutomaticSliceManagement(); // NEW: Initialize automatic slice management initializeNodeManagement(); initializeStakingManagement(); initializeCharts(); // Format location displays formatLocationDisplays(); // Ensure delete node button event listener is attached const confirmDeleteNodeBtn = document.getElementById('confirmDeleteNodeBtn'); if (confirmDeleteNodeBtn && !confirmDeleteNodeBtn.dataset.listenerAttached) { confirmDeleteNodeBtn.addEventListener('click', confirmNodeDeletion); confirmDeleteNodeBtn.dataset.listenerAttached = 'true'; console.log('πŸ—‘οΈ Node delete button event listener attached in DOMContentLoaded'); } }); // Ensure charts initializer exists function initializeCharts() { if (typeof Chart === 'undefined') { return; // Chart.js not loaded; skip } try { Chart.defaults.font.family = "'Poppins', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"; Chart.defaults.font.size = 12; } catch (e) { console.error('Failed to initialize charts:', e); } } /** * Initialize automatic slice management functionality */ function initializeAutomaticSliceManagement() { console.log('🍰 Initializing automatic slice management system'); // Add global functions for slice management window.refreshSliceCalculations = refreshSliceCalculations; window.syncWithGrid = syncWithGrid; window.viewNodeSlices = viewNodeSlices; window.validateAndAddNodes = validateAndAddNodes; // Add global functions for node management actions window.viewNodeDetails = viewNodeDetails; window.deleteNodeConfiguration = deleteNodeConfiguration; // Load initial slice data loadSliceStatistics(); // Initialize add node form handlers initializeAddNodeForm(); } /** * Initialize add node form with automatic slice management */ function initializeAddNodeForm() { console.log('πŸ“ Initializing add node form'); // Handle validate nodes button (bind once, new grid flow) const validateNodesBtn = document.getElementById('validateNodesBtn'); if (validateNodesBtn && !validateNodesBtn.dataset.listenerAttached) { validateNodesBtn.addEventListener('click', validateGridNodes); validateNodesBtn.dataset.listenerAttached = 'true'; } // Handle add nodes button (bind once, new grid flow) const addNodesBtn = document.getElementById('addNodesBtn'); if (addNodesBtn && !addNodesBtn.dataset.listenerAttached) { addNodesBtn.addEventListener('click', addGridNodes); addNodesBtn.dataset.listenerAttached = 'true'; } // Handle node input mode changes const nodeInputModeRadios = document.querySelectorAll('input[name="nodeInputMode"]'); nodeInputModeRadios.forEach(radio => { radio.addEventListener('change', handleNodeInputModeChange); }); } // Handle base slice price validation const baseSlicePriceInput = document.getElementById('baseSlicePrice'); if (baseSlicePriceInput) { baseSlicePriceInput.addEventListener('input', validateSlicePrice); } } /** * Handle node input mode changes (single vs multiple) */ function handleNodeInputModeChange(event) { const singleNodeInput = document.getElementById('singleNodeInput'); const multipleNodeInput = document.getElementById('multipleNodeInput'); if (event.target.value === 'single') { singleNodeInput.style.display = 'block'; multipleNodeInput.style.display = 'none'; } else { singleNodeInput.style.display = 'none'; multipleNodeInput.style.display = 'block'; } } /** * Validate slice price is within allowed range */ function validateSlicePrice(event) { const price = parseFloat(event.target.value); const min = 0.10; const max = 2.00; if (price < min || price > max) { event.target.setCustomValidity(`Price must be between ${min} and ${max} TFP/hour`); event.target.classList.add('is-invalid'); } else { event.target.setCustomValidity(''); event.target.classList.remove('is-invalid'); } } /** * Validate nodes by fetching data from Mycelium Grid */ function validateNodes() { console.log('πŸ” Validating nodes from Mycelium Grid'); // Prevent validation if nodes are being added const addBtn = document.getElementById('addNodesBtn'); if (addBtn && addBtn.disabled) { showNotification('Please wait for current operation to complete', 'warning'); return; } const validateBtn = document.getElementById('validateNodesBtn'); if (!validateBtn) { console.error('Missing validate nodes button'); return; } if (validateBtn.disabled) { console.log('⚠️ Validation already in progress, ignoring duplicate call'); return; } // Additional check: prevent validation if we're in the middle of a page reload if (document.hidden || document.visibilityState === 'hidden') { console.log('⚠️ Page is hidden/reloading, skipping validation'); return; } const checkedMode = document.querySelector('input[name="nodeInputMode"]:checked'); if (!checkedMode) { showNotification('Please select node input mode', 'error'); return; } const nodeInputMode = checkedMode.value; let nodeIds = []; if (nodeInputMode === 'single') { const singleNodeInputEl = document.getElementById('gridNodeId'); if (!singleNodeInputEl) { console.error('Missing single node input element'); return; } const singleNodeId = singleNodeInputEl.value.trim(); if (!singleNodeId) { showNotification('Please enter a node ID', 'error'); return; } nodeIds = [parseInt(singleNodeId)]; } else { const multipleNodeInputEl = document.getElementById('gridNodeIds'); if (!multipleNodeInputEl) { console.error('Missing multiple node input element'); return; } const multipleNodeIds = multipleNodeInputEl.value.trim(); if (!multipleNodeIds) { showNotification('Please enter node IDs', 'error'); return; } // Parse comma or newline separated IDs nodeIds = multipleNodeIds.split(/[,\n]/) .map(id => parseInt(id.trim())) .filter(id => !isNaN(id)); } if (nodeIds.length === 0) { showNotification('Please enter valid node IDs', 'error'); return; } // Show loading state const originalText = validateBtn.textContent; validateBtn.disabled = true; validateBtn.innerHTML = 'Fetching...'; // Fetch node data from grid window.apiJson('/api/dashboard/validate-grid-nodes-automatic', { method: 'POST', body: { node_ids: nodeIds } }) .then(data => { // apiJson returns unwrapped data const nodes = Array.isArray(data?.nodes) ? data.nodes : []; const errors = Array.isArray(data?.errors) ? data.errors : []; const totalRequested = (typeof data?.total_requested === 'number') ? data.total_requested : (nodes.length + errors.length); if (nodes.length > 0) { displayNodePreview(nodes); showAddNodesButton(); if (errors.length > 0 || data?.partial_success) { showNotification( `Validated ${nodes.length} of ${totalRequested} node(s). ${errors.length} failed validation.`, 'warning' ); if (errors.length) console.warn('Node validation errors:', errors); } else { showNotification(`Successfully validated ${nodes.length} node(s)`, 'success'); } } else { // No validated nodes if (errors.length > 0) { showNotification( `Failed to validate any nodes. Errors: ${errors.map(e => `Node ${e.node_id}: ${e.error}`).join('; ')}`, 'error' ); console.error('Validation errors:', errors); } else { showNotification('Failed to validate nodes', 'error'); } } }) .catch(err => { console.error('Node validation request failed:', err); showNotification('Failed to validate nodes. Please try again.', 'error'); }) .finally(() => { // Restore button state validateBtn.disabled = false; validateBtn.textContent = originalText; }); } /** * Display node preview with automatic slice calculations */ function displayNodePreview(nodes) { const nodePreview = document.getElementById('nodePreview'); const nodePreviewContent = document.getElementById('nodePreviewContent'); if (!nodePreview || !nodePreviewContent) return; let previewHtml = ''; nodes.forEach(raw => { // Normalize shapes from different endpoints const data = raw?.data || raw; // validate_grid_nodes wraps details under data const gridNodeId = raw?.node_id || raw?.grid_node_id || data?.grid_node_id || raw?.id || 'Unknown'; const cap = raw?.capacity || raw?.total_resources || data?.total_resources || null; const status = raw?.status || 'online'; const city = raw?.city || data?.city; const country = raw?.country || data?.country; const locationStr = raw?.location || (city && country ? `${city}, ${country}` : 'Unknown Location'); const totalBaseSlices = typeof raw?.total_base_slices === 'number' ? raw.total_base_slices : (cap ? Math.max(1, Math.floor(Math.min( (cap.cpu_cores || 0) / 1, (cap.memory_gb || cap.ram_gb || 0) / 4, ((cap.storage_gb || 0)) / 200 ))) : 'N/A'); const availableCombinations = Array.isArray(raw?.available_combinations) ? raw.available_combinations : []; previewHtml += `
Node ${gridNodeId} - ${locationStr}
Node Specifications
  • CPU: ${cap ? cap.cpu_cores : 'Unknown'} cores
  • Memory: ${cap ? (cap.memory_gb ?? cap.ram_gb ?? 'Unknown') : 'Unknown'} GB
  • Storage: ${cap ? (cap.storage_gb ?? ((cap.ssd_storage_gb||0) + (cap.hdd_storage_gb||0))) : 'Unknown'} GB
  • Status: ${status}
Automatic Slice Calculation
  • Total Base Slices: ${totalBaseSlices}
  • Available Combinations: ${availableCombinations.length}
  • Max Slice Multiplier: ${totalBaseSlices}${typeof totalBaseSlices === 'number' ? 'x' : ''}
Base slice: 1 vCPU + 4GB RAM + 200GB storage
`; }); nodePreviewContent.innerHTML = previewHtml; nodePreview.style.display = 'block'; } /** * Show the add nodes button after successful validation */ function showAddNodesButton() { const addNodesBtn = document.getElementById('addNodesBtn'); if (addNodesBtn) { addNodesBtn.style.display = 'inline-block'; } } /** * Add validated nodes with automatic slice configuration */ function addValidatedNodes() { console.log('βž• Adding validated nodes with automatic slice management'); // Prevent multiple simultaneous calls const addBtn = document.getElementById('addNodesBtn'); if (addBtn.disabled) { console.log('⚠️ Node addition already in progress, ignoring duplicate call'); return; } // Collect form data const formData = { node_ids: getValidatedNodeIds(), node_group: document.getElementById('nodeGroup').value, base_slice_price: parseFloat(document.getElementById('baseSlicePrice').value) || 0.50, node_uptime_sla: parseFloat(document.getElementById('nodeUptimeSLA').value) || 99.8, node_bandwidth_sla: parseInt(document.getElementById('nodeBandwidthSLA').value) || 100, enable_staking: document.getElementById('enableStaking').checked }; // Validate required fields if (!formData.node_ids || formData.node_ids.length === 0) { showNotification('Please validate nodes first', 'error'); return; } if (formData.base_slice_price < 0.10 || formData.base_slice_price > 2.00) { showNotification('Base slice price must be between 0.10 and 2.00 TFP/hour', 'error'); return; } // Show loading state and prevent duplicate calls const originalText = addBtn.textContent; addBtn.disabled = true; addBtn.innerHTML = 'Adding Nodes...'; // Also disable the validate button to prevent conflicts const validateBtn = document.getElementById('validateNodesBtn'); if (validateBtn) { validateBtn.disabled = true; } // Use new grid-nodes flow (delegates to addGridNodes which calls /api/dashboard/grid-nodes/add) addGridNodes(); return; } /** * Get validated node IDs from the preview */ function getValidatedNodeIds() { const nodeInputMode = document.querySelector('input[name="nodeInputMode"]:checked').value; let nodeIds = []; if (nodeInputMode === 'single') { const singleNodeId = document.getElementById('gridNodeId').value.trim(); if (singleNodeId) { nodeIds = [parseInt(singleNodeId)]; } } else { const multipleNodeIds = document.getElementById('gridNodeIds').value.trim(); if (multipleNodeIds) { nodeIds = multipleNodeIds.split(/[,\n]/) .map(id => parseInt(id.trim())) .filter(id => !isNaN(id)); } } return nodeIds; } /** * Update the nodes table with newly added nodes (optimistic UI update) */ function updateNodesTableWithNewNodes(newNodes) { console.log('πŸ”„ Updating nodes table with new nodes:', newNodes); if (!newNodes || newNodes.length === 0) { console.log('⚠️ No new nodes to add to table'); return; } const nodesTableBody = document.querySelector('#nodesTable tbody'); if (!nodesTableBody) { console.warn('⚠️ Nodes table body not found, skipping optimistic update'); return; } // Add each new node to the table newNodes.forEach(node => { const newRow = createNodeTableRow(node); if (newRow) { nodesTableBody.appendChild(newRow); console.log(`βœ… Added node ${node.id} to table optimistically`); } }); // Update any summary statistics if they exist updateNodeCountDisplay(); } /** * Create a table row for a new node (optimistic UI update) */ function createNodeTableRow(node) { try { const row = document.createElement('tr'); row.id = `node-row-${node.id}`; // Get the status badge class const statusBadgeClass = getStatusBadgeClass(node.status); // Format the location const location = node.location || 'Unknown Location'; // Format the capacity const capacity = `${node.capacity.cpu_cores}c/${node.capacity.memory_gb}GB/${node.capacity.storage_gb}GB`; // Format earnings const earnings = node.earnings_today || '0.00'; // Create the row HTML row.innerHTML = `
${node.name}
${node.id}
${location} ${node.status} ${capacity} Uptime: ${node.uptime_percentage}% ${earnings} TFP
`; return row; } catch (error) { console.error('❌ Error creating node table row:', error); return null; } } /** * Update node count display in the dashboard */ function updateNodeCountDisplay() { const nodesTable = document.querySelector('#nodesTable tbody'); if (!nodesTable) return; const nodeCount = nodesTable.children.length; // Update any node count displays const nodeCountElements = document.querySelectorAll('[data-node-count]'); nodeCountElements.forEach(element => { element.textContent = nodeCount; }); console.log(`πŸ“Š Updated node count display: ${nodeCount} nodes`); } /** * Combined function for validating and adding nodes (legacy compatibility) */ function validateAndAddNodes() { console.log('πŸ”„ Legacy validateAndAddNodes called - redirecting to new flow'); // First validate nodes validateNodes(); // Note: addValidatedNodes will be called separately after validation // This function exists for backward compatibility } /** * Refresh slice calculations for all nodes */ function refreshSliceCalculations() { console.log('πŸ”„ Refreshing slice calculations'); window.apiJson('/api/dashboard/refresh-slice-calculations', { method: 'POST' }) .then(() => { // Treat resolved response as success; apiJson throws on non-OK showNotification('Slice calculations refreshed successfully', 'success'); setTimeout(() => { window.location.reload(); }, 1000); }) .catch(error => { console.error('Error refreshing slice calculations:', error); showNotification('Error refreshing slice calculations', 'error'); }); } /** * Sync node data with Mycelium Grid */ function syncWithGrid() { console.log('☁️ Syncing with Mycelium Grid'); // Immediate UX feedback while the backend sync runs try { showNotification('Syncing with Mycelium Grid...', 'info'); } catch (_) {} window.apiJson('/api/dashboard/sync-with-grid', { method: 'POST' }) .then(() => { // Treat resolved response as success; apiJson throws on non-OK showNotification('Successfully synced with Mycelium Grid', 'success'); setTimeout(() => { window.location.reload(); }, 1000); }) .catch(error => { console.error('Error syncing with grid:', error); showNotification('Error syncing with Mycelium Grid', 'error'); }); } /** * View detailed slice information for a specific node */ function viewNodeSlices(nodeId) { console.log('πŸ‘οΈ Viewing slices for node:', nodeId); window.apiJson(`/api/dashboard/node-slices/${nodeId}`) .then(data => { // apiJson returns unwrapped data; support either {node_slices: {...}} or direct object const details = (data && data.node_slices) ? data.node_slices : data; displayNodeSliceDetails(details); }) .catch(error => { console.error('Error loading node slice details:', error); showNotification('Error loading node slice details', 'error'); }); } /** * Display node slice details in a modal */ function displayNodeSliceDetails(nodeSlices) { // Create and show modal with slice details const modalHtml = ` `; // Remove existing modal if present const existingModal = document.getElementById('nodeSliceDetailsModal'); if (existingModal) { existingModal.remove(); } // Add modal to page and show document.body.insertAdjacentHTML('beforeend', modalHtml); const modal = new bootstrap.Modal(document.getElementById('nodeSliceDetailsModal')); modal.show(); } /** * Show node details in a Bootstrap modal */ function showNodeDetailsModal(node) { try { const safe = (v, fallback = 'N/A') => (v === undefined || v === null || v === '' ? fallback : v); const cap = (node && node.capacity) || {}; const sla = (node && node.marketplace_sla) || null; const slicePricing = (node && node.slice_pricing) || {}; const basePricePerHour = slicePricing.base_price_per_hour ?? slicePricing["base_price_per_hour"]; const title = safe(node && (node.name || (node.grid_node_id && `Node ${node.grid_node_id}`) || 'Node Details'), 'Node Details'); const rawJson = (() => { try { return JSON.stringify(node, null, 2); } catch (_) { return ''; } })(); const modalHtml = ` `; // Remove existing modal if present const existing = document.getElementById('nodeDetailsModal'); if (existing) existing.remove(); // Append and show document.body.insertAdjacentHTML('beforeend', modalHtml); const modal = new bootstrap.Modal(document.getElementById('nodeDetailsModal')); modal.show(); } catch (e) { console.error('Failed to render node details modal:', e); showNotification('Failed to render node details', 'error'); } } /** * Load slice statistics for dashboard */ function loadSliceStatistics() { console.log('πŸ“Š Loading slice statistics'); window.apiJson('/api/dashboard/resource_provider/slice-statistics') .then(data => { // apiJson returns unwrapped data; support either {statistics: {...}} or {...} const stats = (data && data.statistics) ? data.statistics : (data || {}); updateSliceStatistics(stats); }) .catch(error => { console.debug('Slice statistics endpoint not available (this is expected):', error.message); // Gracefully handle missing endpoint - show default/empty state updateSliceStatistics({ total_slices: 0, active_slices: 0, revenue_today: 0, revenue_month: 0 }); }); } /** * Update slice statistics in the UI */ function updateSliceStatistics(stats) { // Update slice overview cards if they exist const totalSlicesEl = document.getElementById('total-base-slices'); const allocatedSlicesEl = document.getElementById('allocated-base-slices'); const availableSlicesEl = document.getElementById('available-base-slices'); const utilizationEl = document.getElementById('slice-utilization'); if (totalSlicesEl) totalSlicesEl.textContent = stats.total_base_slices || 0; if (allocatedSlicesEl) allocatedSlicesEl.textContent = stats.allocated_base_slices || 0; if (availableSlicesEl) availableSlicesEl.textContent = stats.available_base_slices || 0; if (utilizationEl) utilizationEl.textContent = (stats.slice_utilization_percentage || 0).toFixed(1) + '%'; } /** * Show notification to user */ function showNotification(message, type = 'info') { // Create notification element const notification = document.createElement('div'); notification.className = `alert alert-${type === 'error' ? 'danger' : type} alert-dismissible fade show position-fixed`; notification.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;'; notification.innerHTML = ` ${message} `; document.body.appendChild(notification); // Auto-remove after 5 seconds setTimeout(() => { if (notification.parentNode) { notification.remove(); } }, 5000); } /** * Simple setup for individual pricing listener */ function setupIndividualPricingListener() { console.log('🏒 Setting up individual pricing listener'); // Check if listener already set up if (document.body.dataset.individualPricingListenerSetup === 'true') { console.log('🏒 Individual pricing listener already set up, skipping'); return; } // Use event delegation to handle dynamically created elements document.addEventListener('change', function(event) { if (event.target && event.target.id === 'individualPricing') { console.log('🏒 Individual pricing radio button clicked!'); if (event.target.checked) { console.log('🏒 Individual pricing selected - creating forms'); // Clean up first to prevent duplicates cleanupIndividualPricingSections(); setTimeout(() => { createSimpleIndividualPricingForms(); }, 100); } else { console.log('🏒 Individual pricing deselected - cleaning up'); cleanupIndividualPricingSections(); } } if (event.target && event.target.id === 'sameForAllNodes') { console.log('🏒 Same for all nodes selected - cleaning up individual forms'); cleanupIndividualPricingSections(); } }); // Mark as set up document.body.dataset.individualPricingListenerSetup = 'true'; console.log('🏒 Individual pricing listener set up with event delegation'); } /** * Simple version of creating individual pricing table rows */ function createSimpleIndividualPricingForms() { console.log('🏒 Creating individual pricing table rows'); // Check if individual pricing table already exists const existingContainer = document.getElementById('individualPricingContainer'); if (existingContainer) { console.log('🏒 Individual pricing table already exists, skipping creation'); return; } // Find the existing pricing inputs const hourlyInput = document.getElementById('fullNodePriceHour') || document.getElementById('fullNodePriceHourNew'); const dailyInput = document.getElementById('fullNodePriceDay') || document.getElementById('fullNodePriceDayNew'); const monthlyInput = document.getElementById('fullNodePriceMonth') || document.getElementById('fullNodePriceMonthNew'); const yearlyInput = document.getElementById('fullNodePriceYear') || document.getElementById('fullNodePriceYearNew'); if (!hourlyInput || !dailyInput || !monthlyInput || !yearlyInput) { console.warn('🏒 Could not find existing pricing inputs'); return; } // Get the validated nodes from the preview const nodePreviewContent = document.getElementById('nodePreviewContent'); let nodeCount = 0; let nodeNames = []; let nodeIds = []; if (nodePreviewContent) { const nodeCards = nodePreviewContent.querySelectorAll('.card'); if (nodeCards.length > 0) { nodeCount = nodeCards.length; nodeCards.forEach((card, index) => { // Extract the actual Grid Node ID from the card content const nodeIdMatch = card.textContent.match(/Grid Node (\d+)/); if (nodeIdMatch) { const actualNodeId = nodeIdMatch[1]; nodeNames.push(`Node ${actualNodeId}`); nodeIds.push(`grid_${actualNodeId}`); } else { // Fallback if we can't find the ID nodeNames.push(`Node ${index + 1}`); nodeIds.push(`node_${index}`); } }); } } // If no nodes found in preview, check if we can get them from input fields if (nodeCount === 0) { // Try to get node IDs from the multiple node input field const multipleNodeInput = document.getElementById('gridNodeIds'); if (multipleNodeInput && multipleNodeInput.value.trim()) { const inputNodeIds = multipleNodeInput.value.split(/[,\n]/) .map(id => id.trim()) .filter(id => id && !isNaN(id)); if (inputNodeIds.length > 0) { nodeCount = inputNodeIds.length; inputNodeIds.forEach(nodeId => { nodeNames.push(`Node ${nodeId}`); nodeIds.push(`grid_${nodeId}`); }); } } // Try single node input as fallback if (nodeCount === 0) { const singleNodeInput = document.getElementById('gridNodeId'); if (singleNodeInput && singleNodeInput.value.trim()) { const nodeId = singleNodeInput.value.trim(); if (!isNaN(nodeId)) { nodeCount = 1; nodeNames.push(`Node ${nodeId}`); nodeIds.push(`grid_${nodeId}`); } } } } // Final fallback for testing if (nodeCount === 0) { console.warn('🏒 No nodes found, using test data'); nodeCount = 2; nodeNames = ['Node 10', 'Node 11']; // Test with realistic node IDs nodeIds = ['grid_10', 'grid_11']; } console.log(`🏒 Creating pricing rows for ${nodeCount} nodes:`, nodeNames); console.log(`🏒 Node IDs:`, nodeIds); // Find the parent container of the pricing inputs const pricingContainer = hourlyInput.closest('.row') || hourlyInput.closest('.card-body'); if (!pricingContainer) { console.warn('🏒 Could not find pricing container'); return; } // Hide the original single-row pricing inputs const originalRow = hourlyInput.closest('.row'); if (originalRow) { originalRow.style.display = 'none'; } // Hide only the redundant rate inputs (not the discount controls) if (dailyInput) { // Hide just the rate input, not the entire column (which contains discount controls) dailyInput.style.display = 'none'; const dailyLabel = dailyInput.previousElementSibling; if (dailyLabel && dailyLabel.tagName === 'LABEL') { dailyLabel.style.display = 'none'; } } if (monthlyInput) { monthlyInput.style.display = 'none'; const monthlyLabel = monthlyInput.previousElementSibling; if (monthlyLabel && monthlyLabel.tagName === 'LABEL') { monthlyLabel.style.display = 'none'; } } if (yearlyInput) { yearlyInput.style.display = 'none'; const yearlyLabel = yearlyInput.previousElementSibling; if (yearlyLabel && yearlyLabel.tagName === 'LABEL') { yearlyLabel.style.display = 'none'; } } // Find and show discount controls with updated labeling for individual pricing const dailyDiscountEl = document.getElementById('fullNodeDailyDiscount'); const monthlyDiscountEl = document.getElementById('fullNodeMonthlyDiscount'); const yearlyDiscountEl = document.getElementById('fullNodeYearlyDiscount'); if (dailyDiscountEl) { const discountContainer = dailyDiscountEl.closest('.col-md-4'); if (discountContainer) { discountContainer.style.display = ''; // Update label to indicate it applies to individual nodes const label = discountContainer.querySelector('label[for="fullNodeDailyDiscount"]'); if (label) { label.innerHTML = 'Daily Discount % (applies to all nodes)'; } } } if (monthlyDiscountEl) { const discountContainer = monthlyDiscountEl.closest('.col-md-4'); if (discountContainer) { discountContainer.style.display = ''; const label = discountContainer.querySelector('label[for="fullNodeMonthlyDiscount"]'); if (label) { label.innerHTML = 'Monthly Discount % (applies to all nodes)'; } } } if (yearlyDiscountEl) { const discountContainer = yearlyDiscountEl.closest('.col-md-4'); if (discountContainer) { discountContainer.style.display = ''; const label = discountContainer.querySelector('label[for="fullNodeYearlyDiscount"]'); if (label) { label.innerHTML = 'Yearly Discount % (applies to all nodes)'; } } } // Create individual pricing table const tableContainer = document.createElement('div'); tableContainer.id = 'individualPricingContainer'; tableContainer.className = 'mt-3'; let tableHTML = `
Individual Node Pricing: Set different rates for each node.
`; // Add a row for each node using actual node IDs nodeNames.forEach((nodeName, index) => { const nodeId = nodeIds[index]; // Use the actual node ID tableHTML += ` `; }); tableHTML += `
Node Base Hourly Rate (TFP) Daily Rate (TFP) Monthly Rate (TFP) Yearly Rate (TFP)
${nodeName}
`; tableContainer.innerHTML = tableHTML; // Insert the table after the original pricing row if (originalRow && originalRow.parentNode) { originalRow.parentNode.insertBefore(tableContainer, originalRow.nextSibling); } else { pricingContainer.appendChild(tableContainer); } // Add auto-calculation for each row using actual node IDs nodeIds.forEach((nodeId, index) => { const hourlyInput = document.getElementById(`hourly_${nodeId}`); const dailyInput = document.getElementById(`daily_${nodeId}`); const monthlyInput = document.getElementById(`monthly_${nodeId}`); const yearlyInput = document.getElementById(`yearly_${nodeId}`); if (hourlyInput) { hourlyInput.addEventListener('input', function() { const hourlyRate = parseFloat(this.value) || 0; if (hourlyRate > 0) { // Get discount values from the original discount controls const dailyDiscountInput = document.getElementById('fullNodeDailyDiscount'); const monthlyDiscountInput = document.getElementById('fullNodeMonthlyDiscount'); const yearlyDiscountInput = document.getElementById('fullNodeYearlyDiscount'); const dailyDiscount = dailyDiscountInput ? parseFloat(dailyDiscountInput.value) || 0 : 0; const monthlyDiscount = monthlyDiscountInput ? parseFloat(monthlyDiscountInput.value) || 0 : 0; const yearlyDiscount = yearlyDiscountInput ? parseFloat(yearlyDiscountInput.value) || 0 : 0; // Apply discount function const applyDiscount = (baseRate, discountPercent) => { return baseRate * (1 - discountPercent / 100); }; // Calculate rates with discounts applied const baseDailyRate = hourlyRate * 24; const baseMonthlyRate = hourlyRate * 24 * 30; const baseYearlyRate = hourlyRate * 24 * 365; const dailyRate = applyDiscount(baseDailyRate, dailyDiscount); const monthlyRate = applyDiscount(baseMonthlyRate, monthlyDiscount); const yearlyRate = applyDiscount(baseYearlyRate, yearlyDiscount); if (dailyInput) dailyInput.value = dailyRate.toFixed(2); if (monthlyInput) monthlyInput.value = monthlyRate.toFixed(0); if (yearlyInput) yearlyInput.value = yearlyRate.toFixed(0); } else { if (dailyInput) dailyInput.value = ''; if (monthlyInput) monthlyInput.value = ''; if (yearlyInput) yearlyInput.value = ''; } }); } }); // Add event listeners to discount controls to update all individual node rates when changed const updateAllNodeRates = () => { nodeIds.forEach((nodeId) => { const hourlyInput = document.getElementById(`hourly_${nodeId}`); if (hourlyInput && hourlyInput.value) { // Trigger the input event to recalculate rates with new discounts hourlyInput.dispatchEvent(new Event('input')); } }); }; if (dailyDiscountEl) { dailyDiscountEl.addEventListener('input', updateAllNodeRates); } if (monthlyDiscountEl) { monthlyDiscountEl.addEventListener('input', updateAllNodeRates); } if (yearlyDiscountEl) { yearlyDiscountEl.addEventListener('input', updateAllNodeRates); } console.log('🏒 Individual pricing table created successfully!'); } /** * Initialize resource_provider dashboard functionality */ function initializeResourceProviderDashboard() { console.log('🚜 Initializing resource_provider dashboard'); // Load resource_provider data loadResourceProviderData(); // Load slice templates loadSliceTemplates(); // Load node groups loadNodeGroups(); // Set up periodic data refresh setInterval(loadResourceProviderData, 30000); // Refresh every 30 seconds setInterval(loadSliceTemplates, 60000); // Refresh slice templates every minute setInterval(loadNodeGroups, 60000); // Refresh node groups every minute } /** * Initialize node management functionality */ function initializeNodeManagement() { console.log('🌐 Initializing node management'); // Node input mode toggle const singleNodeMode = document.getElementById('singleNodeMode'); const multipleNodeMode = document.getElementById('multipleNodeMode'); const singleNodeInput = document.getElementById('singleNodeInput'); const multipleNodeInput = document.getElementById('multipleNodeInput'); if (singleNodeMode && multipleNodeMode) { singleNodeMode.addEventListener('change', function() { if (this.checked) { singleNodeInput.style.display = 'block'; multipleNodeInput.style.display = 'none'; clearNodePreview(); } }); multipleNodeMode.addEventListener('change', function() { if (this.checked) { singleNodeInput.style.display = 'none'; multipleNodeInput.style.display = 'block'; clearNodePreview(); } }); } // Add direct event listeners for individual pricing radio buttons as fallback // This will be called multiple times but that's okay due to our duplicate prevention setTimeout(() => { const sameForAllNodes = document.getElementById('sameForAllNodes'); const individualPricing = document.getElementById('individualPricing'); if (sameForAllNodes && individualPricing) { console.log('🏒 Adding direct event listeners to pricing radio buttons'); sameForAllNodes.addEventListener('change', function() { console.log('🏒 Direct event: Same for all nodes selected'); if (this.checked) { cleanupIndividualPricingSections(); } }); individualPricing.addEventListener('change', function() { console.log('🏒 Direct event: Individual pricing selected'); if (this.checked) { createIndividualNodePricingForms(); } }); } }, 1000); // Wait a bit longer to ensure all elements are loaded // Group configuration toggle const createGroupToggle = document.getElementById('createGroupToggle'); const groupConfigSection = document.getElementById('groupConfigSection'); const existingGroupSection = document.getElementById('existingGroupSection'); if (createGroupToggle) { createGroupToggle.addEventListener('change', function() { if (this.checked) { groupConfigSection.style.display = 'block'; existingGroupSection.style.display = 'block'; loadExistingGroups(); } else { groupConfigSection.style.display = 'none'; existingGroupSection.style.display = 'none'; } }); } // Validate nodes button const validateNodesBtn = document.getElementById('validateNodesBtn'); if (validateNodesBtn && !validateNodesBtn.dataset.listenerAttached) { validateNodesBtn.addEventListener('click', validateGridNodes); validateNodesBtn.dataset.listenerAttached = 'true'; } // Add nodes button const addNodesBtn = document.getElementById('addNodesBtn'); if (addNodesBtn && !addNodesBtn.dataset.listenerAttached) { addNodesBtn.addEventListener('click', addGridNodes); addNodesBtn.dataset.listenerAttached = 'true'; } // Handle form submission for node configuration with full node rental const nodeEditModal = document.getElementById('nodeEditModal'); if (nodeEditModal) { nodeEditModal.addEventListener('shown.bs.modal', function() { // Only initialize if not already done for this modal session const enableFullNodeRental = document.getElementById('editEnableFullNodeRental'); if (!enableFullNodeRental || enableFullNodeRental.dataset.initialized !== 'true') { console.log('🏒 Edit modal shown - initializing full node rental pricing'); initializeFullNodeRentalPricing(); } }); nodeEditModal.addEventListener('hidden.bs.modal', function() { // Reset full node pricing section when modal closes const editFullNodePricingSection = document.getElementById('editFullNodePricingSection'); if (editFullNodePricingSection) { editFullNodePricingSection.style.display = 'none'; } // Remove any dynamic sections const dynamicSections = document.querySelectorAll('.dynamic-full-node-pricing'); dynamicSections.forEach(section => section.remove()); // Reset initialization flag and checkbox state for Edit modal const enableFullNodeRental = document.getElementById('editEnableFullNodeRental'); if (enableFullNodeRental) { enableFullNodeRental.dataset.initialized = 'false'; enableFullNodeRental.checked = false; } // Reset staking section const enableStaking = document.getElementById('editEnableStaking'); const stakingConfigSection = document.getElementById('editStakingConfigSection'); if (enableStaking && stakingConfigSection) { enableStaking.checked = false; stakingConfigSection.style.display = 'none'; } }); } // Initialize slice configuration modal initializeSliceConfiguration(); // Handle Add Node modal events const addNodeModal = document.getElementById('addNodeModal'); if (addNodeModal) { // When modal is shown addNodeModal.addEventListener('shown.bs.modal', function() { // Load default slice formats and node groups when modal opens loadDefaultSliceFormats(); loadNodeGroupsForSelection(); // Only initialize if not already done for this modal session const enableFullNodeRental = document.getElementById('enableFullNodeRental'); if (!enableFullNodeRental || enableFullNodeRental.dataset.initialized !== 'true') { console.log('🏒 Add modal shown - initializing full node rental pricing'); initializeFullNodeRentalPricing(); } // Ensure sections are properly hidden based on checkbox states initializeAddModalSectionVisibility(); }); // When modal is closed addNodeModal.addEventListener('hidden.bs.modal', function() { // Reset the initialization flag so it can be re-initialized next time const enableFullNodeRental = document.getElementById('enableFullNodeRental'); if (enableFullNodeRental) { enableFullNodeRental.dataset.initialized = 'false'; enableFullNodeRental.checked = false; console.log('🏒 Reset full node rental initialization flag'); } // Reset staking section for Add modal const enableStaking = document.getElementById('enableStaking'); const stakingConfigSection = document.getElementById('stakingConfigSection'); if (enableStaking && stakingConfigSection) { enableStaking.checked = false; stakingConfigSection.style.display = 'none'; } }); } } /** * Load node groups for selection in dropdowns */ async function loadNodeGroupsForSelection() { try { const groupsResult = await window.apiJson('/api/dashboard/node-groups'); const groups = (groupsResult && Array.isArray(groupsResult.groups)) ? groupsResult.groups : (Array.isArray(groupsResult) ? groupsResult : []); console.log('πŸ“¦ Loaded node groups for selection:', groups); // Update all group selection dropdowns const groupSelects = document.querySelectorAll('.node-group-select'); groupSelects.forEach(select => { const currentValue = select.value; select.innerHTML = ''; groups.forEach(group => { const option = document.createElement('option'); option.value = group.id; option.textContent = group.name; if (currentValue === group.id) { option.selected = true; } select.appendChild(option); }); }); } catch (error) { console.error('πŸ“¦ Error loading node groups for selection:', error); } } /** * Load default slice formats for node creation */ async function loadDefaultSliceFormats() { try { // Load both default formats and custom slice products const defaultFormats = await window.apiJson('/api/dashboard/default-slice-formats'); const customSliceProducts = await window.apiJson('/api/dashboard/slice-products'); console.log('🍰 Loaded slice formats for Add Nodes:', { defaultFormats, customSliceProducts }); // Update slice format selection in add node modal const sliceFormatContainer = document.getElementById('sliceFormatSelection'); if (sliceFormatContainer) { sliceFormatContainer.innerHTML = ''; // Add default formats defaultFormats.forEach(format => { const formatCard = createSliceFormatCard(format, 'default'); sliceFormatContainer.appendChild(formatCard); }); // Add custom slice products customSliceProducts.forEach(product => { const sliceConfig = product.attributes?.slice_configuration?.value; if (sliceConfig) { const formatCard = createSliceFormatCard({ id: product.id, name: product.name, cpu_cores: sliceConfig.cpu_cores, memory_gb: sliceConfig.memory_gb, storage_gb: sliceConfig.storage_gb, bandwidth_mbps: sliceConfig.bandwidth_mbps, price: product.base_price, currency: product.base_currency, pricing: sliceConfig.pricing }, 'custom'); sliceFormatContainer.appendChild(formatCard); } }); } } catch (error) { console.error('🍰 Error loading slice formats:', error); } } /** * Initialize slice configuration functionality */ function initializeSliceConfiguration() { console.log('🍰 Initializing slice configuration'); // Handle slice configuration form submission const configureSliceModal = document.getElementById('configureSliceModal'); if (configureSliceModal) { configureSliceModal.addEventListener('hidden.bs.modal', function() { resetSliceConfigurationForm(); }); } // Add event listener for slice creation form const createSliceBtn = document.getElementById('saveSliceConfigBtn'); if (createSliceBtn) { createSliceBtn.addEventListener('click', createSliceTemplate); } // Full node rental checkbox event listener is now handled in initializeFullNodeRentalPricing() // CRITICAL FIX: Add event listener for slice delete confirmation button const confirmDeleteSliceBtn = document.getElementById('confirmDeleteSliceBtn'); if (confirmDeleteSliceBtn) { confirmDeleteSliceBtn.addEventListener('click', confirmSliceDeletion); console.log('🍰 Slice delete button event listener attached'); } else { console.warn('🍰 confirmDeleteSliceBtn element not found'); } // Add event listener for node delete confirmation button const confirmDeleteNodeBtn = document.getElementById('confirmDeleteNodeBtn'); if (confirmDeleteNodeBtn) { confirmDeleteNodeBtn.addEventListener('click', confirmNodeDeletion); console.log('πŸ—‘οΈ Node delete button event listener attached'); } // Add auto-calculation for pricing fields initializePricingCalculation(); } /** * Initialize pricing auto-calculation with transparent discount controls */ function initializePricingCalculation() { const priceHour = document.getElementById('priceHour'); const priceDay = document.getElementById('priceDay'); const priceMonth = document.getElementById('priceMonth'); const priceYear = document.getElementById('priceYear'); const autoPricingToggle = document.getElementById('autoPricingToggle'); // Discount controls const dailyDiscount = document.getElementById('dailyDiscount'); const monthlyDiscount = document.getElementById('monthlyDiscount'); const yearlyDiscount = document.getElementById('yearlyDiscount'); if (!priceHour || !priceDay || !priceMonth || !priceYear || !autoPricingToggle) return; if (!dailyDiscount || !monthlyDiscount || !yearlyDiscount) return; // Helper function to check if auto-calculation is enabled const isAutoCalculationEnabled = () => autoPricingToggle.checked; // Helper function to calculate rate with discount const applyDiscount = (baseRate, discountPercent) => { const discount = parseFloat(discountPercent) || 0; return baseRate * (1 - discount / 100); }; // Calculate all rates from hourly base rate const calculateFromHourly = () => { if (!isAutoCalculationEnabled()) return; const hourlyRate = parseFloat(priceHour.value) || 0; if (hourlyRate <= 0) { priceDay.value = ''; priceMonth.value = ''; priceYear.value = ''; return; } // Calculate base rates (no discount) const baseDailyRate = hourlyRate * 24; const baseMonthlyRate = hourlyRate * 24 * 30; const baseYearlyRate = hourlyRate * 24 * 365; // Apply discounts const finalDailyRate = applyDiscount(baseDailyRate, dailyDiscount.value); const finalMonthlyRate = applyDiscount(baseMonthlyRate, monthlyDiscount.value); const finalYearlyRate = applyDiscount(baseYearlyRate, yearlyDiscount.value); // Update fields priceDay.value = finalDailyRate.toFixed(2); priceMonth.value = finalMonthlyRate.toFixed(0); priceYear.value = finalYearlyRate.toFixed(0); }; // Auto-calculate when hourly rate changes priceHour.addEventListener('input', calculateFromHourly); // Auto-calculate when discount percentages change dailyDiscount.addEventListener('input', calculateFromHourly); monthlyDiscount.addEventListener('input', calculateFromHourly); yearlyDiscount.addEventListener('input', calculateFromHourly); // Handle manual input in other fields (when auto-calculation is disabled) priceDay.addEventListener('input', function() { if (isAutoCalculationEnabled()) return; // Only allow manual input when auto-calc is off }); priceMonth.addEventListener('input', function() { if (isAutoCalculationEnabled()) return; // Only allow manual input when auto-calc is off }); priceYear.addEventListener('input', function() { if (isAutoCalculationEnabled()) return; // Only allow manual input when auto-calc is off }); // Add visual feedback when toggle is changed autoPricingToggle.addEventListener('change', function() { const pricingFields = [priceDay, priceMonth, priceYear]; const discountFields = [dailyDiscount, monthlyDiscount, yearlyDiscount]; if (this.checked) { // Enable auto-calculation pricingFields.forEach(field => { field.style.borderColor = ''; field.readOnly = true; field.placeholder = 'Auto-calculated'; field.title = 'Auto-calculated from hourly rate and discount percentage'; }); discountFields.forEach(field => { field.disabled = false; field.style.opacity = '1'; }); // Recalculate immediately calculateFromHourly(); showNotification('Auto-pricing enabled - set hourly rate and discount percentages', 'info'); } else { // Disable auto-calculation pricingFields.forEach(field => { field.style.borderColor = '#ffc107'; field.readOnly = false; field.placeholder = 'Set custom rate'; field.title = 'Set custom rate for special deals'; }); discountFields.forEach(field => { field.disabled = true; field.style.opacity = '0.5'; }); showNotification('Auto-pricing disabled - set completely custom rates', 'warning'); } }); // Initialize state without triggering notification if (autoPricingToggle.checked) { // Set initial state silently const pricingFields = [priceDay, priceMonth, priceYear]; const discountFields = [dailyDiscount, monthlyDiscount, yearlyDiscount]; pricingFields.forEach(field => { field.style.borderColor = ''; field.readOnly = true; field.placeholder = 'Auto-calculated'; field.title = 'Auto-calculated from hourly rate and discount percentage'; }); discountFields.forEach(field => { field.disabled = false; field.style.opacity = '1'; }); // Calculate initial values without notification calculateFromHourly(); } } /** * Initialize full node rental pricing functionality */ function initializeFullNodeRentalPricing() { console.log('🏒 Initializing full node rental pricing'); // Determine which modal is active const addNodeModal = document.getElementById('addNodeModal'); const editNodeModal = document.getElementById('nodeEditModal'); const isAddModalActive = addNodeModal && addNodeModal.classList.contains('show'); const isEditModalActive = editNodeModal && editNodeModal.classList.contains('show'); // Get the appropriate checkbox based on active modal const enableFullNodeRental = isEditModalActive ? document.getElementById('editEnableFullNodeRental') : document.getElementById('enableFullNodeRental'); if (!enableFullNodeRental) { console.log('🏒 Full node rental checkbox not found, skipping initialization'); return; } // Check if already initialized if (enableFullNodeRental.dataset.initialized === 'true') { console.log('🏒 Full node rental pricing already initialized, skipping'); return; } // Also prevent multiple dynamic sections from being created const existingDynamicSections = document.querySelectorAll('.dynamic-full-node-pricing'); if (existingDynamicSections.length > 0) { console.log('🏒 Found existing dynamic pricing sections, cleaning up before initialization'); existingDynamicSections.forEach(section => section.remove()); } // Full node rental checkbox toggle const fullNodePricingSection = document.getElementById('fullNodePricingSection'); // Remove any existing event listeners to prevent duplicates const existingListeners = enableFullNodeRental.cloneNode(true); enableFullNodeRental.parentNode.replaceChild(existingListeners, enableFullNodeRental); const freshElement = isEditModalActive ? document.getElementById('editEnableFullNodeRental') : document.getElementById('enableFullNodeRental'); // Track if notification was already shown to prevent duplicates let notificationShown = false; freshElement.addEventListener('change', function() { console.log('🏒 Full node rental checkbox changed:', this.checked); // Re-check modal states const isAddModalActive = addNodeModal && addNodeModal.classList.contains('show'); const isEditModalActive = editNodeModal && editNodeModal.classList.contains('show'); console.log('🏒 Modal states:', { isAddModalActive, isEditModalActive }); if (this.checked) { if (isEditModalActive) { // Edit modal - show existing section const editFullNodePricingSection = document.getElementById('editFullNodePricingSection'); if (editFullNodePricingSection) { editFullNodePricingSection.style.display = 'block'; cleanupFullNodePricingSection(editFullNodePricingSection); setTimeout(() => { initializeFullNodePricingCalculation(); }, 100); } } else if (isAddModalActive) { // Add modal - create dynamic section createDynamicFullNodePricingSection(this); } // Check if we need to show multi-node options and initialize individual pricing handlers setTimeout(() => { handleNodeInputModeChange(); }, 150); // Only show notification once per modal session if (!notificationShown) { showNotification('Full node rental enabled - configure pricing below', 'info'); notificationShown = true; } } else { if (isEditModalActive) { // Hide existing section in edit modal const editFullNodePricingSection = document.getElementById('editFullNodePricingSection'); if (editFullNodePricingSection) { editFullNodePricingSection.style.display = 'none'; } } if (isAddModalActive) { // Remove dynamic sections in add modal const dynamicSections = document.querySelectorAll('.dynamic-full-node-pricing'); dynamicSections.forEach(section => section.remove()); } // Hide multi-node options when full node rental is disabled const multiNodeOptions = document.getElementById('multiNodePricingOptions'); if (multiNodeOptions) { multiNodeOptions.style.display = 'none'; } cleanupIndividualPricingSections(); // Only show notification once per modal session if (!notificationShown) { showNotification('Full node rental disabled', 'warning'); notificationShown = true; } } }); // Mark as initialized freshElement.dataset.initialized = 'true'; console.log('🏒 Full node rental pricing initialized successfully'); // Initialize staking functionality for the appropriate modal const isCurrentlyEditModal = editNodeModal && editNodeModal.classList.contains('show'); if (isCurrentlyEditModal) { initializeEditStakingFunctionality(); } else { initializeAddStakingFunctionality(); } } /** * Initialize section visibility for Edit modal based on checkbox states */ function initializeEditModalSectionVisibility() { console.log('πŸ”§ Initializing Edit modal section visibility'); // Handle staking section visibility const enableStaking = document.getElementById('editEnableStaking'); const stakingConfigSection = document.getElementById('editStakingConfigSection'); if (enableStaking && stakingConfigSection) { // Hide staking section if checkbox is unchecked if (!enableStaking.checked) { stakingConfigSection.style.display = 'none'; } } // Handle full node rental section visibility const enableFullNodeRental = document.getElementById('editEnableFullNodeRental'); const editFullNodePricingSection = document.getElementById('editFullNodePricingSection'); if (enableFullNodeRental && editFullNodePricingSection) { // Hide full node pricing section if checkbox is unchecked if (!enableFullNodeRental.checked) { editFullNodePricingSection.style.display = 'none'; } } console.log('πŸ”§ Edit modal section visibility initialized'); } /** * Initialize section visibility for Add modal based on checkbox states */ function initializeAddModalSectionVisibility() { console.log('🏒 Initializing Add modal section visibility'); // Handle staking section visibility const enableStaking = document.getElementById('enableStaking'); const stakingConfigSection = document.getElementById('stakingConfigSection'); if (enableStaking && stakingConfigSection) { // Hide staking section if checkbox is unchecked if (!enableStaking.checked) { stakingConfigSection.style.display = 'none'; } } console.log('🏒 Add modal section visibility initialized'); } /** * Initialize staking functionality for Add Node modal */ function initializeAddStakingFunctionality() { const enableStaking = document.getElementById('enableStaking'); const stakingConfigSection = document.getElementById('stakingConfigSection'); if (enableStaking && stakingConfigSection) { // Ensure staking section is hidden by default stakingConfigSection.style.display = 'none'; // Remove any existing event listeners to prevent duplicates const existingListeners = enableStaking.cloneNode(true); enableStaking.parentNode.replaceChild(existingListeners, enableStaking); const freshElement = document.getElementById('enableStaking'); freshElement.addEventListener('change', function() { stakingConfigSection.style.display = this.checked ? 'block' : 'none'; }); } } /** * Initialize staking functionality for Edit Node modal */ function initializeEditStakingFunctionality() { const enableStaking = document.getElementById('editEnableStaking'); const stakingConfigSection = document.getElementById('editStakingConfigSection'); if (enableStaking && stakingConfigSection) { // Ensure staking section is hidden by default stakingConfigSection.style.display = 'none'; // Remove any existing event listeners to prevent duplicates enableStaking.removeEventListener('change', handleEditStakingToggle); // Add the proper event listener enableStaking.addEventListener('change', handleEditStakingToggle); console.log('πŸ›‘οΈ Edit staking functionality initialized'); } } /** * Clean up the existing full node pricing section styling */ function cleanupFullNodePricingSection(section) { // Remove test colors and apply clean styling section.style.backgroundColor = ''; section.style.border = ''; section.style.minHeight = ''; section.style.padding = ''; section.style.margin = ''; section.style.position = ''; section.style.zIndex = ''; // Clean up card elements const cardElements = section.querySelectorAll('.card, .card-body, .card-header'); cardElements.forEach(el => { el.style.backgroundColor = ''; el.style.border = ''; el.style.minHeight = ''; }); // Apply proper Bootstrap classes const mainCard = section.querySelector('.card'); if (mainCard) { mainCard.className = 'card'; const cardHeader = mainCard.querySelector('.card-header'); if (cardHeader) { cardHeader.className = 'card-header'; } const cardBody = mainCard.querySelector('.card-body'); if (cardBody) { cardBody.className = 'card-body'; } } } /** * Create dynamic full node pricing section for Add Nodes modal */ function createDynamicFullNodePricingSection(checkbox) { // First, remove any existing dynamic pricing sections to prevent duplicates const existingDynamicSections = document.querySelectorAll('.dynamic-full-node-pricing'); existingDynamicSections.forEach(section => { console.log('🏒 Removing existing dynamic pricing section to prevent duplication'); section.remove(); }); const pricingSection = document.createElement('div'); pricingSection.className = 'dynamic-full-node-pricing mt-3'; pricingSection.innerHTML = `
Full Node Rental Pricing (TFP)
When enabled, entering one rate automatically calculates others. Disable to set completely custom rates.
Set discount percentages to offer better rates for longer commitments. 0% = no discount.
days
Minimum time customers must rent the full node
`; // Insert after the checkbox's parent container const checkboxContainer = checkbox.closest('.mb-3'); if (checkboxContainer) { checkboxContainer.insertAdjacentElement('afterend', pricingSection); console.log('🏒 Created dynamic full node pricing section'); // Initialize pricing calculation for the new section initializeFullNodePricingCalculation(); // Handle multi-node options based on input mode handleNodeInputModeChange(); } } /** * Handle node input mode changes for multi-node pricing */ function handleNodeInputModeChange() { console.log('πŸ”„ handleNodeInputModeChange called'); const nodeInputMode = document.querySelector('input[name="nodeInputMode"]:checked')?.value; const multiNodeOptions = document.getElementById('multiNodePricingOptions'); const enableFullNodeRental = document.getElementById('enableFullNodeRental'); console.log('πŸ”„ Node input mode state:', { nodeInputMode, multiNodeOptions: !!multiNodeOptions, enableFullNodeRental: !!enableFullNodeRental, fullNodeRentalChecked: enableFullNodeRental?.checked }); if (multiNodeOptions && enableFullNodeRental?.checked) { if (nodeInputMode === 'multiple') { console.log('πŸ”„ Showing multi-node options'); multiNodeOptions.style.display = 'block'; // Initialize individual pricing handlers with a small delay to ensure elements are rendered setTimeout(() => { initializeIndividualPricingHandlers(); }, 100); } else { console.log('πŸ”„ Hiding multi-node options'); multiNodeOptions.style.display = 'none'; // Clean up individual pricing sections cleanupIndividualPricingSections(); } } else { console.log('πŸ”„ Conditions not met for showing multi-node options'); } } /** * Initialize handlers for individual pricing radio buttons */ function initializeIndividualPricingHandlers() { console.log('🏒 Attempting to initialize individual pricing handlers'); const sameForAllNodes = document.getElementById('sameForAllNodes'); const individualPricing = document.getElementById('individualPricing'); console.log('🏒 Found elements:', { sameForAllNodes: !!sameForAllNodes, individualPricing: !!individualPricing }); if (sameForAllNodes && individualPricing) { // Remove existing listeners to prevent duplicates sameForAllNodes.removeEventListener('change', handlePricingModeChange); individualPricing.removeEventListener('change', handlePricingModeChange); // Add new listeners sameForAllNodes.addEventListener('change', handlePricingModeChange); individualPricing.addEventListener('change', handlePricingModeChange); console.log('🏒 Individual pricing handlers initialized successfully'); // Test if the individual pricing radio button is already selected if (individualPricing.checked) { console.log('🏒 Individual pricing is already selected, triggering handler'); handlePricingModeChange(); } } else { console.warn('🏒 Could not find pricing radio button elements'); } } /** * Handle pricing mode changes (same for all vs individual) */ function handlePricingModeChange() { console.log('🏒 handlePricingModeChange called'); const individualPricing = document.getElementById('individualPricing'); const sameForAllNodes = document.getElementById('sameForAllNodes'); console.log('🏒 Pricing mode elements:', { individualPricing: !!individualPricing, individualChecked: individualPricing?.checked, sameForAllNodes: !!sameForAllNodes, sameChecked: sameForAllNodes?.checked }); if (individualPricing && individualPricing.checked) { console.log('🏒 Individual pricing selected - creating individual node forms'); createIndividualNodePricingForms(); } else { console.log('🏒 Same pricing selected - cleaning up individual forms'); cleanupIndividualPricingSections(); } } /** * Create individual pricing forms for each node */ function createIndividualNodePricingForms() { console.log('🏒 createIndividualNodePricingForms called'); // Get the validated nodes from the preview const nodePreviewContent = document.getElementById('nodePreviewContent'); console.log('🏒 nodePreviewContent found:', !!nodePreviewContent); if (!nodePreviewContent) { console.warn('🏒 No node preview content found'); return; } // Extract node information from the preview const nodeCards = nodePreviewContent.querySelectorAll('.node-preview-card'); console.log('🏒 Found node cards:', nodeCards.length); if (nodeCards.length === 0) { console.warn('🏒 No node cards found in preview'); // Let's also check for any cards without the specific class const allCards = nodePreviewContent.querySelectorAll('.card'); console.log('🏒 Found general cards:', allCards.length); if (allCards.length > 0) { console.log('🏒 Found cards but they don\'t have .node-preview-card class'); // Let's try to work with general cards return createIndividualNodePricingFormsFromGeneralCards(allCards); } return; } // Clean up any existing individual pricing sections cleanupIndividualPricingSections(); // Create container for individual pricing forms const multiNodePricingOptions = document.getElementById('multiNodePricingOptions'); console.log('🏒 multiNodePricingOptions found:', !!multiNodePricingOptions); if (!multiNodePricingOptions) { console.warn('🏒 Multi-node pricing options container not found'); return; } // Create individual pricing container const individualPricingContainer = document.createElement('div'); individualPricingContainer.id = 'individualPricingContainer'; individualPricingContainer.className = 'mt-4'; const headerHtml = `
Individual Node Pricing Configuration
Individual Pricing: Set custom pricing for each node based on their specifications and location.
`; individualPricingContainer.innerHTML = headerHtml; // Create pricing form for each node nodeCards.forEach((nodeCard, index) => { const nodeId = nodeCard.dataset.nodeId || `node_${index}`; const nodeName = nodeCard.querySelector('.node-name')?.textContent || `Node ${index + 1}`; const nodeSpecs = nodeCard.querySelector('.node-specs')?.textContent || 'Unknown specs'; const nodeLocation = nodeCard.querySelector('.node-location')?.textContent || 'Unknown location'; console.log(`🏒 Creating form for node ${index}:`, { nodeId, nodeName, nodeSpecs, nodeLocation }); const nodeForm = createIndividualNodePricingForm(nodeId, nodeName, nodeSpecs, nodeLocation, index); individualPricingContainer.appendChild(nodeForm); }); multiNodePricingOptions.appendChild(individualPricingContainer); console.log(`🏒 Created individual pricing forms for ${nodeCards.length} nodes`); } /** * Fallback function to create pricing forms from general cards */ function createIndividualNodePricingFormsFromGeneralCards(cards) { console.log('🏒 Creating individual pricing forms from general cards'); // Clean up any existing individual pricing sections cleanupIndividualPricingSections(); // Create container for individual pricing forms const multiNodePricingOptions = document.getElementById('multiNodePricingOptions'); if (!multiNodePricingOptions) { console.warn('🏒 Multi-node pricing options container not found'); return; } // Create individual pricing container const individualPricingContainer = document.createElement('div'); individualPricingContainer.id = 'individualPricingContainer'; individualPricingContainer.className = 'mt-4'; const headerHtml = `
Individual Node Pricing Configuration
Individual Pricing: Set custom pricing for each node based on their specifications and location.
`; individualPricingContainer.innerHTML = headerHtml; // Create pricing form for each card cards.forEach((card, index) => { // Try to extract node information from the card const nodeIdMatch = card.textContent.match(/Grid Node (\d+)/); const nodeId = nodeIdMatch ? `grid_${nodeIdMatch[1]}` : `node_${index}`; const nodeName = card.querySelector('h6')?.textContent || `Node ${index + 1}`; // Try to find location and specs const cardBody = card.querySelector('.card-body'); const locationMatch = cardBody?.textContent.match(/Location:\s*([^Resources]+)/); const nodeLocation = locationMatch ? locationMatch[1].trim() : 'Unknown location'; const resourcesMatch = cardBody?.textContent.match(/Resources:\s*([^Certification]+)/); const nodeSpecs = resourcesMatch ? resourcesMatch[1].trim() : 'Unknown specs'; console.log(`🏒 Creating form for general card ${index}:`, { nodeId, nodeName, nodeSpecs, nodeLocation }); const nodeForm = createIndividualNodePricingForm(nodeId, nodeName, nodeSpecs, nodeLocation, index); individualPricingContainer.appendChild(nodeForm); }); multiNodePricingOptions.appendChild(individualPricingContainer); console.log(`🏒 Created individual pricing forms for ${cards.length} general cards`); } /** * Create individual pricing form for a single node */ function createIndividualNodePricingForm(nodeId, nodeName, nodeSpecs, nodeLocation, index) { const formContainer = document.createElement('div'); formContainer.className = 'card mb-3 individual-node-pricing-form'; formContainer.dataset.nodeId = nodeId; const formHtml = `
${nodeName} ${nodeLocation}
${nodeSpecs}
TFP/hour
TFP/day
TFP/month
TFP/year
`; formContainer.innerHTML = formHtml; // Initialize pricing calculation for this node setTimeout(() => { initializeNodePricingCalculation(nodeId); }, 100); return formContainer; } /** * Initialize pricing calculation for a specific node */ function initializeNodePricingCalculation(nodeId) { const hourlyInput = document.getElementById(`nodeHourlyRate_${nodeId}`); const dailyInput = document.getElementById(`nodeDailyRate_${nodeId}`); const monthlyInput = document.getElementById(`nodeMonthlyRate_${nodeId}`); const yearlyInput = document.getElementById(`nodeYearlyRate_${nodeId}`); const autoToggle = document.getElementById(`nodeAutoPricing_${nodeId}`); const dailyDiscountInput = document.getElementById(`nodeDailyDiscount_${nodeId}`); const monthlyDiscountInput = document.getElementById(`nodeMonthlyDiscount_${nodeId}`); const yearlyDiscountInput = document.getElementById(`nodeYearlyDiscount_${nodeId}`); if (!hourlyInput || !dailyInput || !monthlyInput || !yearlyInput || !autoToggle) { console.warn(`🏒 Missing pricing inputs for node ${nodeId}`); return; } // Helper function to apply discount const applyDiscount = (baseRate, discountPercent) => { const discount = parseFloat(discountPercent) || 0; return baseRate * (1 - discount / 100); }; // Calculate rates from hourly const calculateFromHourly = () => { if (!autoToggle.checked) return; const hourlyRate = parseFloat(hourlyInput.value) || 0; if (hourlyRate <= 0) { dailyInput.value = ''; monthlyInput.value = ''; yearlyInput.value = ''; return; } // Calculate base rates const baseDailyRate = hourlyRate * 24; const baseMonthlyRate = hourlyRate * 24 * 30; const baseYearlyRate = hourlyRate * 24 * 365; // Apply discounts const finalDailyRate = applyDiscount(baseDailyRate, dailyDiscountInput.value); const finalMonthlyRate = applyDiscount(baseMonthlyRate, monthlyDiscountInput.value); const finalYearlyRate = applyDiscount(baseYearlyRate, yearlyDiscountInput.value); // Update fields dailyInput.value = finalDailyRate.toFixed(2); monthlyInput.value = finalMonthlyRate.toFixed(0); yearlyInput.value = finalYearlyRate.toFixed(0); }; // Add event listeners hourlyInput.addEventListener('input', calculateFromHourly); dailyDiscountInput.addEventListener('input', calculateFromHourly); monthlyDiscountInput.addEventListener('input', calculateFromHourly); yearlyDiscountInput.addEventListener('input', calculateFromHourly); // Handle auto-calculation toggle autoToggle.addEventListener('change', function() { const pricingFields = [dailyInput, monthlyInput, yearlyInput]; const discountFields = [dailyDiscountInput, monthlyDiscountInput, yearlyDiscountInput]; if (this.checked) { // Enable auto-calculation pricingFields.forEach(field => { field.readOnly = true; field.placeholder = 'Auto-calculated'; field.style.borderColor = ''; }); discountFields.forEach(field => { field.disabled = false; field.style.opacity = '1'; }); calculateFromHourly(); } else { // Disable auto-calculation pricingFields.forEach(field => { field.readOnly = false; field.placeholder = 'Set custom rate'; field.style.borderColor = '#ffc107'; }); discountFields.forEach(field => { field.disabled = true; field.style.opacity = '0.5'; }); } }); // Initialize state if (autoToggle.checked) { calculateFromHourly(); } console.log(`🏒 Initialized pricing calculation for node ${nodeId}`); } /** * Clean up individual pricing sections */ function cleanupIndividualPricingSections() { const existingContainer = document.getElementById('individualPricingContainer'); if (existingContainer) { existingContainer.remove(); console.log('🏒 Cleaned up individual pricing sections'); } // Show the original single-row pricing inputs again const hourlyInput = document.getElementById('fullNodePriceHour') || document.getElementById('fullNodePriceHourNew'); if (hourlyInput) { const originalRow = hourlyInput.closest('.row'); if (originalRow) { originalRow.style.display = ''; console.log('🏒 Restored original pricing row'); } } // Restore the daily/monthly/yearly rate inputs and their labels that were hidden const dailyInput = document.getElementById('fullNodePriceDay') || document.getElementById('fullNodePriceDayNew'); const monthlyInput = document.getElementById('fullNodePriceMonth') || document.getElementById('fullNodePriceMonthNew'); const yearlyInput = document.getElementById('fullNodePriceYear') || document.getElementById('fullNodePriceYearNew'); if (dailyInput) { dailyInput.style.display = ''; const dailyLabel = dailyInput.previousElementSibling; if (dailyLabel && dailyLabel.tagName === 'LABEL') { dailyLabel.style.display = ''; } } if (monthlyInput) { monthlyInput.style.display = ''; const monthlyLabel = monthlyInput.previousElementSibling; if (monthlyLabel && monthlyLabel.tagName === 'LABEL') { monthlyLabel.style.display = ''; } } if (yearlyInput) { yearlyInput.style.display = ''; const yearlyLabel = yearlyInput.previousElementSibling; if (yearlyLabel && yearlyLabel.tagName === 'LABEL') { yearlyLabel.style.display = ''; } } // Restore original discount labels const dailyDiscountLabel = document.querySelector('label[for="fullNodeDailyDiscount"]'); const monthlyDiscountLabel = document.querySelector('label[for="fullNodeMonthlyDiscount"]'); const yearlyDiscountLabel = document.querySelector('label[for="fullNodeYearlyDiscount"]'); if (dailyDiscountLabel) { dailyDiscountLabel.innerHTML = 'Daily Discount %'; } if (monthlyDiscountLabel) { monthlyDiscountLabel.innerHTML = 'Monthly Discount %'; } if (yearlyDiscountLabel) { yearlyDiscountLabel.innerHTML = 'Yearly Discount %'; } console.log('🏒 Restored daily/monthly/yearly rate controls and discount labels'); } /** * Initialize full node pricing auto-calculation */ function initializeFullNodePricingCalculation() { const priceHour = document.getElementById('fullNodePriceHour'); const priceDay = document.getElementById('fullNodePriceDay'); const priceMonth = document.getElementById('fullNodePriceMonth'); const priceYear = document.getElementById('fullNodePriceYear'); const autoPricingToggle = document.getElementById('fullNodeAutoPricingToggle'); // Discount controls const dailyDiscount = document.getElementById('fullNodeDailyDiscount'); const monthlyDiscount = document.getElementById('fullNodeMonthlyDiscount'); const yearlyDiscount = document.getElementById('fullNodeYearlyDiscount'); console.log('🏒 Initializing full node pricing calculation'); console.log('🏒 Elements found:', { priceHour: !!priceHour, priceDay: !!priceDay, priceMonth: !!priceMonth, priceYear: !!priceYear, autoPricingToggle: !!autoPricingToggle, dailyDiscount: !!dailyDiscount, monthlyDiscount: !!monthlyDiscount, yearlyDiscount: !!yearlyDiscount }); if (!priceHour || !priceDay || !priceMonth || !priceYear) { console.warn('🏒 Missing required pricing elements, skipping initialization'); return; } if (!autoPricingToggle) { console.warn('🏒 Missing auto pricing toggle, skipping initialization'); return; } // Helper function to check if auto-calculation is enabled const isAutoCalculationEnabled = () => autoPricingToggle.checked; // Helper function to calculate rate with discount const applyDiscount = (baseRate, discountPercent) => { const discount = parseFloat(discountPercent) || 0; return baseRate * (1 - discount / 100); }; // Calculate all rates from hourly base rate const calculateFromHourly = () => { if (!isAutoCalculationEnabled()) return; const hourlyRate = parseFloat(priceHour.value) || 0; if (hourlyRate <= 0) { priceDay.value = ''; priceMonth.value = ''; priceYear.value = ''; return; } // Calculate base rates (no discount) const baseDailyRate = hourlyRate * 24; const baseMonthlyRate = hourlyRate * 24 * 30; const baseYearlyRate = hourlyRate * 24 * 365; // Apply discounts const finalDailyRate = applyDiscount(baseDailyRate, dailyDiscount.value); const finalMonthlyRate = applyDiscount(baseMonthlyRate, monthlyDiscount.value); const finalYearlyRate = applyDiscount(baseYearlyRate, yearlyDiscount.value); // Update fields priceDay.value = finalDailyRate.toFixed(2); priceMonth.value = finalMonthlyRate.toFixed(0); priceYear.value = finalYearlyRate.toFixed(0); }; // Auto-calculate when hourly rate changes priceHour.addEventListener('input', calculateFromHourly); // Auto-calculate when discount percentages change if (dailyDiscount) { dailyDiscount.addEventListener('input', calculateFromHourly); } if (monthlyDiscount) { monthlyDiscount.addEventListener('input', calculateFromHourly); } if (yearlyDiscount) { yearlyDiscount.addEventListener('input', calculateFromHourly); } // Handle manual input in other fields (when auto-calculation is disabled) priceDay.addEventListener('input', function() { if (isAutoCalculationEnabled()) return; // Only allow manual input when auto-calc is off }); priceMonth.addEventListener('input', function() { if (isAutoCalculationEnabled()) return; // Only allow manual input when auto-calc is off }); priceYear.addEventListener('input', function() { if (isAutoCalculationEnabled()) return; // Only allow manual input when auto-calc is off }); // Add visual feedback when toggle is changed autoPricingToggle.addEventListener('change', function() { const pricingFields = [priceDay, priceMonth, priceYear]; const discountFields = [dailyDiscount, monthlyDiscount, yearlyDiscount]; if (this.checked) { // Enable auto-calculation pricingFields.forEach(field => { field.style.borderColor = ''; field.readOnly = true; field.placeholder = 'Auto-calculated'; field.title = 'Auto-calculated from hourly rate and discount percentage'; }); discountFields.forEach(field => { field.disabled = false; field.style.opacity = '1'; }); // Recalculate immediately calculateFromHourly(); showNotification('Full node auto-pricing enabled - set hourly rate and discount percentages', 'info'); } else { // Disable auto-calculation pricingFields.forEach(field => { field.style.borderColor = '#ffc107'; field.readOnly = false; field.placeholder = 'Set custom rate'; field.title = 'Set custom rate for special deals'; }); discountFields.forEach(field => { field.disabled = true; field.style.opacity = '0.5'; }); showNotification('Full node auto-pricing disabled - set completely custom rates', 'warning'); } }); // Initialize state without triggering notification if (autoPricingToggle.checked) { // Set initial state silently const pricingFields = [priceDay, priceMonth, priceYear]; const discountFields = [dailyDiscount, monthlyDiscount, yearlyDiscount]; pricingFields.forEach(field => { field.style.borderColor = ''; field.readOnly = true; field.placeholder = 'Auto-calculated'; field.title = 'Auto-calculated from hourly rate and discount percentage'; }); discountFields.forEach(field => { field.disabled = false; field.style.opacity = '1'; }); // Calculate initial values without notification calculateFromHourly(); } } /** * Collect full node pricing configuration from form */ function collectFullNodePricingData() { const enableFullNodeRental = document.getElementById('enableFullNodeRental'); if (!enableFullNodeRental || !enableFullNodeRental.checked) { return { full_node_rental_enabled: false }; } // Check if individual pricing is selected const individualPricing = document.getElementById('individualPricing'); if (individualPricing && individualPricing.checked) { console.log('🏒 Collecting individual node pricing data'); return collectIndividualNodePricingData(); } // Default: collect standard pricing data const hourly = parseFloat( document.getElementById('fullNodePriceHour')?.value || document.getElementById('fullNodePriceHourNew')?.value ) || 0; const daily = parseFloat( document.getElementById('fullNodePriceDay')?.value || document.getElementById('fullNodePriceDayNew')?.value ) || 0; const monthly = parseFloat( document.getElementById('fullNodePriceMonth')?.value || document.getElementById('fullNodePriceMonthNew')?.value ) || 0; const yearly = parseFloat( document.getElementById('fullNodePriceYear')?.value || document.getElementById('fullNodePriceYearNew')?.value ) || 0; const autoCalculate = document.getElementById('fullNodeAutoPricingToggle')?.checked || true; const dailyDiscount = parseFloat(document.getElementById('fullNodeDailyDiscount')?.value) || 0; const monthlyDiscount = parseFloat(document.getElementById('fullNodeMonthlyDiscount')?.value) || 0; const yearlyDiscount = parseFloat(document.getElementById('fullNodeYearlyDiscount')?.value) || 0; const minRentalDuration = parseInt(document.getElementById('fullNodeMinRentalDuration')?.value) || 30; console.log('🏒 Collecting standard full node pricing data'); return { full_node_rental_enabled: true, pricing_mode: 'same_for_all', full_node_pricing: { hourly: hourly, daily: daily, monthly: monthly, yearly: yearly, auto_calculate: autoCalculate, daily_discount_percent: dailyDiscount, monthly_discount_percent: monthlyDiscount, yearly_discount_percent: yearlyDiscount }, minimum_rental_days: minRentalDuration }; } /** * Collect individual node pricing data */ function collectIndividualNodePricingData() { const individualPricingContainer = document.getElementById('individualPricingContainer'); if (!individualPricingContainer) { console.warn('🏒 Individual pricing container not found'); return { full_node_rental_enabled: false }; } const nodePricingForms = individualPricingContainer.querySelectorAll('.individual-node-pricing-form'); if (nodePricingForms.length === 0) { console.warn('🏒 No individual node pricing forms found'); return { full_node_rental_enabled: false }; } const individualPricingData = {}; nodePricingForms.forEach(form => { const nodeId = form.dataset.nodeId; if (!nodeId) { console.warn('🏒 Node form missing nodeId'); return; } const hourlyInput = form.querySelector(`#nodeHourlyRate_${nodeId}`); const dailyInput = form.querySelector(`#nodeDailyRate_${nodeId}`); const monthlyInput = form.querySelector(`#nodeMonthlyRate_${nodeId}`); const yearlyInput = form.querySelector(`#nodeYearlyRate_${nodeId}`); const autoToggle = form.querySelector(`#nodeAutoPricing_${nodeId}`); const dailyDiscountInput = form.querySelector(`#nodeDailyDiscount_${nodeId}`); const monthlyDiscountInput = form.querySelector(`#nodeMonthlyDiscount_${nodeId}`); const yearlyDiscountInput = form.querySelector(`#nodeYearlyDiscount_${nodeId}`); if (!hourlyInput || !dailyInput || !monthlyInput || !yearlyInput) { console.warn(`🏒 Missing pricing inputs for node ${nodeId}`); return; } individualPricingData[nodeId] = { hourly: parseFloat(hourlyInput.value) || 0, daily: parseFloat(dailyInput.value) || 0, monthly: parseFloat(monthlyInput.value) || 0, yearly: parseFloat(yearlyInput.value) || 0, auto_calculate: autoToggle?.checked || false, daily_discount_percent: parseFloat(dailyDiscountInput?.value) || 0, monthly_discount_percent: parseFloat(monthlyDiscountInput?.value) || 0, yearly_discount_percent: parseFloat(yearlyDiscountInput?.value) || 0, }; console.log(`🏒 Collected pricing data for node ${nodeId}:`, individualPricingData[nodeId]); }); return { full_node_rental_enabled: true, pricing_mode: 'individual', individual_node_pricing: individualPricingData, minimum_rental_days: 30 // Default value for individual pricing }; } /** * Get multi-node pricing configuration */ function getMultiNodePricingConfig() { const multiNodeOptions = document.getElementById('multiNodePricingOptions'); if (!multiNodeOptions || multiNodeOptions.style.display === 'none') { return 'single'; // Single node or not applicable } const selectedOption = document.querySelector('input[name="multiNodePricing"]:checked'); return selectedOption ? selectedOption.value : 'same'; } /** * Create slice template */ async function createSliceTemplate() { console.log('🍰 Creating slice template'); // Get form data const sliceName = document.getElementById('sliceName')?.value?.trim(); const sliceCPU = parseInt(document.getElementById('sliceCPU')?.value) || 0; const sliceRAM = parseInt(document.getElementById('sliceRAM')?.value) || 0; const sliceStorage = parseInt(document.getElementById('sliceStorage')?.value) || 0; const sliceBandwidth = parseInt(document.getElementById('sliceBandwidth')?.value) || 0; const sliceUptime = parseFloat(document.getElementById('sliceUptime')?.value) || 99.0; const sliceIPsValue = document.getElementById('sliceIPs')?.value || 'none'; const sliceIPs = sliceIPsValue === 'none' ? 0 : (sliceIPsValue === 'included' ? 1 : 0); // Get pricing data const priceHour = parseFloat(document.getElementById('priceHour')?.value) || 0; const priceDay = parseFloat(document.getElementById('priceDay')?.value) || 0; const priceMonth = parseFloat(document.getElementById('priceMonth')?.value) || 0; const priceYear = parseFloat(document.getElementById('priceYear')?.value) || 0; // Validate form data if (!sliceName || sliceCPU <= 0 || sliceRAM <= 0 || sliceStorage <= 0) { showNotification('Please fill in all required fields with valid values', 'warning'); return; } // Validate pricing if (priceHour <= 0) { showNotification('Please enter a valid hourly price', 'warning'); return; } const createBtn = document.getElementById('saveSliceConfigBtn'); if (createBtn) { createBtn.disabled = true; createBtn.innerHTML = 'Creating...'; } try { const sliceData = { name: sliceName, cpu: sliceCPU, ram: sliceRAM, storage: sliceStorage, bandwidth: sliceBandwidth, uptime: sliceUptime, public_ips: sliceIPs, price_hour: priceHour, price_day: priceDay, price_month: priceMonth, price_year: priceYear }; const result = await window.apiJson('/api/dashboard/slice-products', { method: 'POST', body: sliceData }); console.log('🍰 Slice template created:', result); showNotification('Slice template created successfully', 'success'); // Close modal const modal = bootstrap.Modal.getInstance(document.getElementById('configureSliceModal')); if (modal) { modal.hide(); } // Reload slice templates loadSliceTemplates(); } catch (error) { console.error('🍰 Error creating slice template:', error); showNotification('Failed to create slice template', 'error'); } finally { // Reset button state if (createBtn) { createBtn.disabled = false; createBtn.innerHTML = 'Save Configuration'; } } } /** * Reset slice configuration form */ function resetSliceConfigurationForm() { const form = document.querySelector('#configureSliceModal form'); if (form) { form.reset(); } } /** * Load resource_provider data from API */ async function loadResourceProviderData() { try { const data = await window.apiJson('/api/dashboard/resource_provider-data'); console.log('🚜 Loaded resource_provider data:', data); // Load node groups as well const groupsResult = await window.apiJson('/api/dashboard/node-groups'); const nodeGroups = (groupsResult && Array.isArray(groupsResult.groups)) ? groupsResult.groups : (Array.isArray(groupsResult) ? groupsResult : []); data.nodeGroups = nodeGroups; console.log('🚜 Node groups loaded:', nodeGroups); // Store resource_provider data globally for slice status checking window.resourceProviderData = data; // Update dashboard stats updateDashboardStats(data); // Update nodes table updateNodesTable(data.nodes || []); // Update node groups updateNodeGroups(data.nodeGroups || []); // Reload slice templates to update status based on new node data loadSliceTemplates(); } catch (error) { console.error('🚜 Error loading resource_provider data:', error); showNotification('Failed to load resource_provider data', 'error'); } } /** * Load slice templates (default formats + custom slice products) */ async function loadSliceTemplates() { try { console.log('🍰 Loading slice templates'); // Load default slice formats const defaultFormats = await window.apiJson('/api/dashboard/default-slice-formats'); // Load user's custom slice products const sliceProducts = await window.apiJson('/api/dashboard/slice-products'); console.log('🍰 Loaded slice templates:', { defaultFormats, sliceProducts }); // Update slice templates table updateSliceTemplatesTable(defaultFormats, sliceProducts); } catch (error) { console.error('🍰 Error loading slice templates:', error); showSliceTemplatesError(); } } /** * Update slice templates table */ function updateSliceTemplatesTable(defaultFormats, sliceProducts) { const tbody = document.getElementById('slice-templates-table'); if (!tbody) return; tbody.innerHTML = ''; // Add default slice formats defaultFormats.forEach(format => { // Determine status based on whether any nodes are using this slice format const status = getSliceFormatStatus(format.id); const statusBadge = status === 'Active' ? 'Active' : 'Available'; const row = document.createElement('tr'); row.innerHTML = ` ${format.name}
Default Format ${format.cpu_cores} vCPU, ${format.memory_gb}GB RAM
${format.storage_gb}GB Storage, ${format.bandwidth_mbps}Mbps 99.0% ${format.bandwidth_mbps}Mbps 0 ${format.price_per_hour} TFP
per hour ${statusBadge}
`; tbody.appendChild(row); }); // Add custom slice products sliceProducts.forEach(product => { const sliceConfig = product.attributes?.slice_configuration?.value; if (!sliceConfig) return; // Extract real pricing data from slice pricing structure const slicePricing = product.attributes?.slice_pricing?.value; let pricingDisplay = `${product.base_price} ${product.base_currency}
per hour`; if (slicePricing) { // Display comprehensive pricing if available const hourly = slicePricing.hourly || product.base_price; const daily = slicePricing.daily; const monthly = slicePricing.monthly; const yearly = slicePricing.yearly; pricingDisplay = `
${hourly} ${product.base_currency}/hr
${daily ? `${daily} ${product.base_currency}/day
` : ''} ${monthly ? `${monthly} ${product.base_currency}/month
` : ''} ${yearly ? `${yearly} ${product.base_currency}/year` : ''}
`; } // Extract real resource specifications const cpuCores = sliceConfig.cpu_cores || 0; const memoryGb = sliceConfig.memory_gb || 0; const storageGb = sliceConfig.storage_gb || 0; const bandwidthMbps = sliceConfig.bandwidth_mbps || 0; const publicIps = sliceConfig.public_ips || 0; const minUptimeSla = sliceConfig.min_uptime_sla || 99.0; // Determine status based on whether any nodes are using this slice format const status = getSliceFormatStatus(product.id); const statusBadge = status === 'Active' ? 'Active' : 'Available'; const row = document.createElement('tr'); row.innerHTML = ` ${product.name}
Custom Slice ${cpuCores} vCPU, ${memoryGb}GB RAM
${storageGb}GB Storage, ${bandwidthMbps}Mbps ${minUptimeSla}% ${bandwidthMbps}Mbps ${publicIps} ${pricingDisplay} ${statusBadge}
`; tbody.appendChild(row); }); // If no templates, show empty state if (defaultFormats.length === 0 && sliceProducts.length === 0) { tbody.innerHTML = `

No slice templates configured

Create your first slice template to get started

`; } } /** * Show error in slice templates table */ function showSliceTemplatesError() { const tbody = document.getElementById('slice-templates-table'); if (!tbody) return; tbody.innerHTML = `

Failed to load slice templates

`; } /** * Get slice format status based on node usage * Returns 'Active' if at least one node is using this slice format, 'Available' otherwise */ function getSliceFormatStatus(sliceFormatId) { // Check if we have resource_provider data loaded if (!window.resourceProviderData || !window.resourceProviderData.nodes) { return 'Available'; // Default to Available if no data } // Check if any node has this slice format in their slice_formats array const isUsedByAnyNode = window.resourceProviderData.nodes.some(node => { return node.slice_formats && Array.isArray(node.slice_formats) && node.slice_formats.includes(sliceFormatId); }); return isUsedByAnyNode ? 'Active' : 'Available'; } /** * Use a default slice format */ function useSliceFormat(formatId) { console.log('🍰 Using slice format:', formatId); showNotification(`Using ${formatId} slice format`, 'info'); // This would open the slice configuration modal with the format pre-selected } /** * View slice configuration details */ async function viewSliceDetails(sliceId) { console.log('🍰 Viewing slice details:', sliceId); try { const sliceData = await window.apiJson(`/api/dashboard/slice-details/${sliceId}`); console.log('🍰 Loaded slice details:', sliceData); // Show slice details modal showSliceDetailsModal(sliceData); } catch (error) { console.error('🍰 Error loading slice details:', error); showNotification('Failed to load slice details', 'error'); } } /** * View default slice details */ async function viewDefaultSliceDetails(formatId) { console.log('🍰 Viewing default slice details for:', formatId); try { const formatData = await window.apiJson(`/api/dashboard/default-slice-details/${formatId}`); console.log('🍰 Loaded default slice details:', formatData); // Convert default slice format to slice data format for reuse const sliceData = { id: formatData.id, name: formatData.name, description: formatData.description, provider_name: "Mycelium (Default)", base_price: formatData.price_per_hour, slice_configuration: { cpu_cores: formatData.cpu_cores, memory_gb: formatData.memory_gb, storage_gb: formatData.storage_gb, bandwidth_mbps: formatData.bandwidth_mbps, public_ips: 0, min_uptime_sla: 99.0, pricing: { hourly: formatData.price_per_hour, daily: formatData.price_per_hour * 24, monthly: formatData.price_per_hour * 24 * 30, yearly: formatData.price_per_hour * 24 * 365 } }, isDefault: true }; showSliceDetailsModal(sliceData); } catch (error) { console.error('🍰 Error loading default slice details:', error); showNotification('Failed to load default slice details', 'error'); } } /** * Edit default slice configuration */ async function editDefaultSliceConfiguration(formatId) { console.log('🍰 Editing default slice configuration:', formatId); try { const formatData = await window.apiJson(`/api/dashboard/default-slice-details/${formatId}`); console.log('🍰 Loaded default slice for editing:', formatData); // Convert default slice format to slice data format for reuse const sliceData = { id: formatData.id, name: formatData.name, description: formatData.description, provider_name: "Mycelium (Default)", base_price: formatData.price_per_hour, attributes: { slice_configuration: { value: { cpu_cores: formatData.cpu_cores, memory_gb: formatData.memory_gb, storage_gb: formatData.storage_gb, bandwidth_mbps: formatData.bandwidth_mbps, public_ips: 0, min_uptime_sla: 99.0, pricing: { hourly: formatData.price_per_hour, daily: formatData.price_per_hour * 24, monthly: formatData.price_per_hour * 24 * 30, yearly: formatData.price_per_hour * 24 * 365 } } } }, isDefault: true }; showSliceEditModal(sliceData); } catch (error) { console.error('🍰 Error loading default slice for editing:', error); showNotification('Failed to load default slice configuration', 'error'); } } /** * Edit slice configuration */ async function editSliceConfiguration(sliceId) { console.log('🍰 Editing slice configuration:', sliceId); try { const sliceData = await window.apiJson(`/api/dashboard/slice-details/${sliceId}`); console.log('🍰 Loaded slice for editing:', sliceData); // Show slice edit modal showSliceEditModal(sliceData); } catch (error) { console.error('🍰 Error loading slice for editing:', error); showNotification('Failed to load slice configuration', 'error'); } } /** * Delete slice configuration */ async function deleteSliceConfiguration(sliceId) { console.log('🍰 Deleting slice configuration:', sliceId); // Store slice ID for confirmation window.pendingDeleteSliceId = sliceId; // Get slice name for display try { const sliceData = await window.apiJson(`/api/dashboard/slice-details/${sliceId}`); document.getElementById('deleteSliceName').textContent = sliceData.name || 'Unknown Slice'; } catch (error) { document.getElementById('deleteSliceName').textContent = 'Unknown Slice'; } // Show custom confirmation modal const deleteModal = new bootstrap.Modal(document.getElementById('deleteSliceModal')); deleteModal.show(); } /** * Confirm slice deletion (called from modal) */ async function confirmSliceDeletion() { const sliceId = window.pendingDeleteSliceId; console.log('🍰 confirmSliceDeletion called with sliceId:', sliceId); if (!sliceId) { console.error('🍰 No pending delete slice ID found'); showNotification('Error: No slice selected for deletion', 'error'); return; } const confirmBtn = document.getElementById('confirmDeleteSliceBtn'); if (confirmBtn) { confirmBtn.disabled = true; confirmBtn.innerHTML = 'Deleting...'; } try { console.log('🍰 Sending DELETE request to:', `/api/dashboard/slice-products/${sliceId}`); const result = await window.apiJson(`/api/dashboard/slice-products/${sliceId}`, { method: 'DELETE' }); console.log('🍰 Slice configuration deleted successfully:', result); showNotification('Slice configuration deleted successfully', 'success'); // Hide modal const deleteModal = bootstrap.Modal.getInstance(document.getElementById('deleteSliceModal')); if (deleteModal) { deleteModal.hide(); } // Reload slice templates with small delay for backend processing setTimeout(() => { loadSliceTemplates(); }, 100); } catch (error) { console.error('🍰 Error deleting slice configuration:', error); showNotification(`Failed to delete slice configuration: ${error.message}`, 'error'); } finally { // Reset button state if (confirmBtn) { confirmBtn.disabled = false; confirmBtn.innerHTML = 'Delete Slice Configuration'; } // Clear pending ID window.pendingDeleteSliceId = null; } } /** * Edit slice product (legacy function for compatibility) */ function editSliceProduct(productId) { editSliceConfiguration(productId); } /** * Delete slice product (legacy function for compatibility) */ async function deleteSliceProduct(productId) { await deleteSliceConfiguration(productId); } /** * Update dashboard statistics */ function updateDashboardStats(data) { // Update active nodes count const activeNodesCount = document.getElementById('active-nodes-count'); if (activeNodesCount && data.nodes) { const activeNodes = data.nodes.filter(node => node.status === 'Online').length; activeNodesCount.textContent = activeNodes; } // Update active slices count const activeSlicesCount = document.getElementById('active-slices-count'); if (activeSlicesCount && data.slice_stats) { activeSlicesCount.textContent = data.slice_stats.active_slices || 0; } // Update uptime percentage const uptimePercentage = document.getElementById('uptime-percentage'); if (uptimePercentage && data.uptime_stats) { uptimePercentage.textContent = `${data.uptime_stats.average_uptime || 99.0}%`; } // Update monthly earnings const monthlyEarnings = document.getElementById('monthly-earnings'); if (monthlyEarnings && data.earnings_stats) { monthlyEarnings.textContent = data.earnings_stats.monthly_earnings || 0; } } /** * Format node specifications for display */ function formatNodeSpecifications(resources) { // Add debugging and null checks console.log('πŸ” formatNodeSpecifications called with:', resources); if (!resources) { console.error('❌ Resources is null or undefined'); return 'No resource data available'; } const vcores = resources.cpu_cores || 0; const memoryGB = resources.memory_gb || 0; const ssdTB = ((resources.ssd_storage_gb || 0) / 1000).toFixed(1); const hddTB = ((resources.hdd_storage_gb || 0) / 1000).toFixed(1); console.log('πŸ” Parsed values:', { vcores, memoryGB, ssdTB, hddTB }); let specs = `${vcores} vCores, ${memoryGB} GB RAM`; if (resources.ssd_storage_gb > 0) { specs += `, ${ssdTB} TB SSD`; } if (resources.hdd_storage_gb > 0) { specs += `, ${hddTB} TB HDD`; } console.log('πŸ” Final specs:', specs); return specs; } /** * Format legacy node specifications for backward compatibility */ function formatLegacyNodeSpecifications(node) { const vcores = node.capacity?.cpu_cores || node.cpu_cores || 0; const memoryGB = node.capacity?.memory_gb || node.ram_gb || node.memory_gb || 0; const storageGB = node.capacity?.storage_gb || node.storage_gb || 0; const storageTB = (storageGB / 1000).toFixed(1); return `${vcores} vCores, ${memoryGB} GB RAM, ${storageTB} TB Storage`; } /** * Format GridProxy raw resources for preview display */ function formatGridProxyResources(resources) { const vcores = resources.cru; const memoryGB = Math.round(resources.mru / (1024 * 1024 * 1024)); const ssdTB = (resources.sru / (1024 * 1024 * 1024 * 1024)).toFixed(1); const hddTB = (resources.hru / (1024 * 1024 * 1024 * 1024)).toFixed(1); let specs = `${vcores} vCores, ${memoryGB} GB RAM`; if (resources.sru > 0) { specs += `, ${ssdTB} TB SSD`; } if (resources.hru > 0) { specs += `, ${hddTB} TB HDD`; } return specs; } /** * Update nodes table */ function updateNodesTable(nodes) { const tbody = document.getElementById('nodes-table-tbody'); if (!tbody) return; tbody.innerHTML = ''; if (nodes.length === 0) { tbody.innerHTML = `

No nodes added yet

Click "Add Nodes" to get started

`; return; } nodes.forEach(node => { const row = document.createElement('tr'); // Enhanced status handling with real grid data let nodeStatus = node.status || 'Unknown'; let statusBadge = getStatusBadge(nodeStatus); // If we have grid data, use the grid status if (node.grid_data && node.grid_data.status) { nodeStatus = node.grid_data.status; statusBadge = getStatusBadge(nodeStatus); } // Group info with enhanced display - get actual group name from loaded groups let groupInfo = 'Single'; if (node.node_group_id && window.resourceProviderData && window.resourceProviderData.nodeGroups) { const group = window.resourceProviderData.nodeGroups.find(g => g.id === node.node_group_id); if (group) { groupInfo = `${group.name}`; } } // Enhanced specifications with real grid proxy data let specs = 'No data available'; if (node.grid_data && node.grid_data.total_resources) { // Use real grid proxy data specs = formatNodeSpecifications(node.grid_data.total_resources); } else if (node.capacity) { // Use stored capacity data specs = formatLegacyNodeSpecifications(node); } else { // Fallback to basic node properties const vcores = node.cpu_cores || 0; const memoryGB = node.memory_gb || node.ram_gb || 0; const storageGB = node.storage_gb || 0; specs = `${vcores} vCores, ${memoryGB} GB RAM, ${(storageGB/1000).toFixed(1)} TB Storage`; } // Enhanced farm and location with real grid data let farmLocation = 'Unknown Location'; // Debug logging to understand the node structure console.log('πŸ” Node data for location formatting:', { node_id: node.id, grid_data: node.grid_data, location: node.location, country: node.country }); if (node.grid_data) { const farmName = node.grid_data.farm_name || 'Unknown Farm'; const city = node.grid_data.city || node.grid_data.location?.city || 'Unknown'; const country = node.grid_data.country || node.grid_data.location?.country || 'Unknown'; // Use the correct logic: if city is "Unknown", show only country const locationText = (city === 'Unknown') ? country : `${city}, ${country}`; farmLocation = `${farmName}
${locationText}`; console.log('πŸ“ Using grid_data:', { farmName, city, country, locationText, raw_grid_data: { farm_name: node.grid_data.farm_name, city: node.grid_data.city, country: node.grid_data.country, location: node.grid_data.location } }); } else if (node.location || node.country) { // Parse the location string to extract city and country let city = 'Unknown'; let country = node.country || 'Unknown Country'; let farmName = 'Unknown Farm'; if (node.location && node.location.includes(',')) { const parts = node.location.split(',').map(part => part.trim()); if (parts.length >= 2) { city = parts[0]; country = parts[1]; } const locationText = (city === 'Unknown') ? country : `${city}, ${country}`; farmLocation = `${farmName}
${locationText}`; console.log('πŸ“ Parsed location with comma:', { city, country, locationText }); } else if (node.location) { // If location doesn't contain comma, it might be the farm name or full location farmName = node.location; const locationText = (city === 'Unknown') ? country : `${city}, ${country}`; farmLocation = `${farmName}
${locationText}`; console.log('πŸ“ Using location as farm name:', { farmName, city, country, locationText }); } else { const locationText = (city === 'Unknown') ? country : `${city}, ${country}`; farmLocation = `${farmName}
${locationText}`; console.log('πŸ“ Using defaults:', { farmName, city, country, locationText }); } } // Enhanced certification with real grid data let certificationBadge = 'Unknown'; if (node.grid_data && node.grid_data.certification_type) { const certType = node.grid_data.certification_type; const badgeClass = certType === 'Certified' ? 'bg-success' : 'bg-warning'; certificationBadge = `${certType}`; } else if (node.certification_type) { const badgeClass = node.certification_type === 'Certified' ? 'bg-success' : 'bg-warning'; certificationBadge = `${node.certification_type}`; } // Enhanced utilization calculation let utilization = 0; if (node.grid_data && node.grid_data.used_resources && node.grid_data.total_resources) { // Calculate real utilization from grid data const used = node.grid_data.used_resources; const total = node.grid_data.total_resources; const cpuUtil = total.cru > 0 ? (used.cru / total.cru) * 100 : 0; const memUtil = total.mru > 0 ? (used.mru / total.mru) * 100 : 0; utilization = Math.round((cpuUtil + memUtil) / 2); } else if (node.utilization !== undefined) { utilization = node.utilization; } // Enhanced earnings display with real data let earningsDisplay = 'No data'; if (node.monthly_earnings && node.monthly_earnings > 0) { earningsDisplay = `${node.monthly_earnings} TFP
This month`; } else if (node.total_earnings && node.total_earnings > 0) { earningsDisplay = `${node.total_earnings} TFP
Total earned`; } // Grid Node ID display with enhanced formatting const nodeIdDisplay = node.grid_node_id ? `${node.grid_node_id}
Grid Node` : `${node.id}
Local Node`; // Staking information let stakingInfo = 'Not Staked'; if (node.staking_options && node.staking_options.staking_enabled) { const stakedAmount = node.staking_options.staked_amount || 0; stakingInfo = `${stakedAmount} TFP
Staked for slashing protection`; } // SLA & Pricing information - should always be available from saved data let slaInfo = 'No data'; if (node.marketplace_sla) { const sla = node.marketplace_sla; // Format uptime to 1 decimal place const formattedUptime = parseFloat(sla.uptime_guarantee_percentage).toFixed(1); slaInfo = `
${sla.base_slice_price} TFP/hr
${formattedUptime}% uptime
${sla.bandwidth_guarantee_mbps} Mbps
`; } else if (node.slice_pricing && node.slice_pricing.base_price_per_hour) { // Fallback to slice_pricing if marketplace_sla is missing const formattedUptime = parseFloat(node.uptime_percentage || 99.8).toFixed(1); const bandwidth = node.capacity ? node.capacity.bandwidth_mbps : 100; slaInfo = `
${node.slice_pricing.base_price_per_hour} TFP/hr
${formattedUptime}% uptime
${bandwidth} Mbps
`; } else { // Final fallback - this should not happen if data is properly saved console.warn('🚨 Node missing both marketplace_sla and slice_pricing:', node.id); const formattedUptime = parseFloat(node.uptime_percentage || 99.8).toFixed(1); const bandwidth = node.capacity ? node.capacity.bandwidth_mbps : 100; slaInfo = `
0.5 TFP/hr
${formattedUptime}% uptime
${bandwidth} Mbps
`; } row.innerHTML = ` ${nodeIdDisplay} ${farmLocation} ${specs} ${slaInfo} ${certificationBadge} ${statusBadge} ${groupInfo} ${stakingInfo}
`; tbody.appendChild(row); }); } /** * Get staking action buttons for a node */ function getStakingActionButtons(node) { if (node.staking_options && node.staking_options.staking_enabled) { // Node is staked - show update and unstake buttons return ` `; } else { // Node is not staked - show stake button return ` `; } } /** * Get status badge HTML */ function getStatusBadge(status) { const statusMap = { 'Online': 'bg-success', 'Offline': 'bg-danger', 'Maintenance': 'bg-warning', 'Standby': 'bg-secondary' }; const badgeClass = statusMap[status] || 'bg-secondary'; return `${status}`; } /** * Update node groups display */ function updateNodeGroups(groups) { console.log('🌐 Updating node groups:', groups); // This would update the node groups section // Implementation depends on the specific UI requirements } /** * Validate grid nodes */ async function validateGridNodes() { console.log('🌐 Validating grid nodes'); const validateBtn = document.getElementById('validateNodesBtn'); const addBtn = document.getElementById('addNodesBtn'); const nodePreview = document.getElementById('nodePreview'); const nodePreviewContent = document.getElementById('nodePreviewContent'); // Get node IDs const nodeIds = getNodeIdsFromInput(); if (nodeIds.length === 0) { showNotification('Please enter at least one node ID', 'warning'); return; } // Guard duplicate submits and show loading state if (!validateBtn) { console.error('Missing validateNodesBtn element'); return; } if (validateBtn.dataset.inFlight === 'true') { console.log('⚠️ Validation already in progress, ignoring duplicate call'); return; } validateBtn.dataset.inFlight = 'true'; validateBtn.disabled = true; validateBtn.innerHTML = 'Fetching...'; try { const result = await window.apiJson('/api/dashboard/grid-nodes/validate', { method: 'POST', body: { node_ids: nodeIds } }); console.log('🌐 Validation result:', result); // Handle errors first (including duplicates) if (result.errors.length > 0) { const duplicateErrors = result.errors.filter(e => e.error.includes('already registered')); const otherErrors = result.errors.filter(e => !e.error.includes('already registered')); if (duplicateErrors.length > 0) { const duplicateMsg = duplicateErrors.map(e => `Node ${e.node_id}: ${e.error}`).join('\n'); showNotification(`Duplicate nodes detected:\n${duplicateMsg}`, 'error'); } if (otherErrors.length > 0) { const errorMsg = otherErrors.map(e => `Node ${e.node_id}: ${e.error}`).join('\n'); showNotification(`Some nodes failed validation:\n${errorMsg}`, 'warning'); } } if (result.validated_nodes.length > 0) { // Show node preview displayNodePreview(result.validated_nodes); nodePreview.style.display = 'block'; // Show add button addBtn.style.display = 'inline-block'; showNotification(`Successfully validated ${result.validated_nodes.length} nodes`, 'success'); } else if (result.validated_nodes.length === 0 && result.errors.length === 0) { showNotification('No valid nodes found', 'error'); nodePreview.style.display = 'none'; addBtn.style.display = 'none'; } else if (result.validated_nodes.length === 0) { // All nodes had errors (duplicates or validation failures) nodePreview.style.display = 'none'; addBtn.style.display = 'none'; } } catch (error) { console.error('🌐 Error validating nodes:', error); showNotification('Failed to validate nodes', 'error'); nodePreview.style.display = 'none'; addBtn.style.display = 'none'; } finally { // Reset button state delete validateBtn.dataset.inFlight; validateBtn.disabled = false; validateBtn.innerHTML = 'Fetch Nodes'; } } /** * Get node IDs from input fields */ function getNodeIdsFromInput() { const singleNodeMode = document.getElementById('singleNodeMode'); const nodeIds = []; if (singleNodeMode && singleNodeMode.checked) { // Single node mode const nodeIdInput = document.getElementById('gridNodeId'); if (nodeIdInput && nodeIdInput.value.trim()) { const nodeId = parseInt(nodeIdInput.value.trim()); if (!isNaN(nodeId) && nodeId > 0) { nodeIds.push(nodeId); } } } else { // Multiple nodes mode const nodeIdsInput = document.getElementById('gridNodeIds'); if (nodeIdsInput && nodeIdsInput.value.trim()) { const input = nodeIdsInput.value.trim(); // Split by commas or newlines and parse const rawIds = input.split(/[\,\n\r]+/); for (const rawId of rawIds) { const nodeId = parseInt(rawId.trim()); if (!isNaN(nodeId) && nodeId > 0) { nodeIds.push(nodeId); } } } } return [...new Set(nodeIds)]; // Remove duplicates } /** * Add grid nodes */ async function addGridNodes() { console.log('🌐 Adding grid nodes'); const addBtn = document.getElementById('addNodesBtn'); const modal = document.getElementById('addNodeModal'); // Get node IDs const nodeIds = getNodeIdsFromInput(); if (nodeIds.length === 0) { showNotification('Please validate nodes first', 'warning'); return; } // Get group configuration const groupConfig = getGroupConfiguration(); // Get slice configurations for each node const sliceConfigs = {}; nodeIds.forEach(nodeId => { const sliceSelect = document.getElementById(`sliceFormat_${nodeId}`); if (sliceSelect) { sliceConfigs[nodeId] = sliceSelect.value; } }); // Guard duplicate submits and show loading state if (!addBtn) { console.error('Missing addNodesBtn element'); return; } if (addBtn.dataset.inFlight === 'true') { console.log('⚠️ Node addition already in progress, ignoring duplicate call'); return; } addBtn.dataset.inFlight = 'true'; addBtn.disabled = true; addBtn.innerHTML = 'Adding Nodes...'; try { // Get slice configuration from new selection system const sliceConfiguration = getSelectedSliceConfiguration(); // Get selected node group from dropdown const nodeGroupSelect = document.getElementById('nodeGroup'); const selectedGroupId = nodeGroupSelect ? nodeGroupSelect.value : ''; // Collect full node pricing configuration const fullNodePricingData = collectFullNodePricingData(); const multiNodePricingConfig = getMultiNodePricingConfig(); // Collect staking configuration const stakingConfiguration = getStakingConfiguration(); const requestData = { node_ids: nodeIds, slice_configuration: sliceConfiguration, full_node_rental_config: fullNodePricingData, multi_node_pricing: multiNodePricingConfig, staking_configuration: stakingConfiguration }; // Add group assignment if a group is selected if (selectedGroupId) { requestData.node_group_id = selectedGroupId; console.log('🌐 Assigning nodes to group:', selectedGroupId); } // Add group configuration if provided (for creating new groups) if (groupConfig) { requestData.group_config = groupConfig; } console.log('🏒 Full node rental config:', fullNodePricingData); console.log('🏒 Multi-node pricing config:', multiNodePricingConfig); const result = await window.apiJson('/api/dashboard/grid-nodes/add', { method: 'POST', body: requestData }); console.log('🌐 Add nodes result:', result); { const addedCount = Array.isArray(result?.added_nodes) ? result.added_nodes.length : (parseInt(result?.added, 10) || 0); showNotification(addedCount > 0 ? `Successfully added ${addedCount} nodes` : 'Nodes added successfully', 'success'); // Close modal const modalInstance = bootstrap.Modal.getInstance(modal); if (modalInstance) { modalInstance.hide(); } // Reset form resetAddNodeForm(); // Reload resource_provider data and node groups with small delay for backend processing setTimeout(async () => { await loadResourceProviderData(); await loadNodeGroups(); // FARMER FIX: Refresh node groups table after adding new nodes await loadWalletBalance(); // Refresh wallet balance after staking await updateStakingDisplay(); // Refresh staking statistics after staking }, 100); } } catch (error) { console.error('🌐 Error adding nodes:', error); showNotification('Failed to add nodes', 'error'); } finally { // Reset button state delete addBtn.dataset.inFlight; addBtn.disabled = false; addBtn.innerHTML = 'Add Nodes'; } } /** * Reset Add Nodes form and UI state */ function resetAddNodeForm() { try { // Clear node ID inputs const nodeIdInput = document.getElementById('gridNodeId'); const nodeIdsInput = document.getElementById('gridNodeIds'); if (nodeIdInput) nodeIdInput.value = ''; if (nodeIdsInput) nodeIdsInput.value = ''; // Hide preview and clear content const nodePreview = document.getElementById('nodePreview'); const nodePreviewContent = document.getElementById('nodePreviewContent'); if (nodePreview) nodePreview.style.display = 'none'; if (nodePreviewContent) nodePreviewContent.innerHTML = ''; // Hide Add Nodes button const addBtn = document.getElementById('addNodesBtn'); if (addBtn) addBtn.style.display = 'none'; // Reset validation button const validateBtn = document.getElementById('validateNodesBtn'); if (validateBtn) { delete validateBtn.dataset.inFlight; validateBtn.disabled = false; validateBtn.innerHTML = 'Fetch Nodes'; } // Reset group creation toggle and fields const createGroupToggle = document.getElementById('createGroupToggle'); if (createGroupToggle) createGroupToggle.checked = false; const groupName = document.getElementById('groupName'); const groupDescription = document.getElementById('groupDescription'); const groupSliceFormat = document.getElementById('groupSliceFormat'); const groupSlicePrice = document.getElementById('groupSlicePrice'); if (groupName) groupName.value = ''; if (groupDescription) groupDescription.value = ''; if (groupSliceFormat) groupSliceFormat.selectedIndex = 0; if (groupSlicePrice) groupSlicePrice.value = ''; // Reset group dropdowns const nodeGroup = document.getElementById('nodeGroup'); const existingGroup = document.getElementById('existingGroup'); if (nodeGroup) nodeGroup.selectedIndex = 0; if (existingGroup) existingGroup.selectedIndex = 0; } catch (e) { console.warn('resetAddNodeForm encountered a non-fatal error:', e); } } function getGroupConfiguration() { const createGroupToggle = document.getElementById('createGroupToggle'); if (!createGroupToggle || !createGroupToggle.checked) { return null; } const groupName = document.getElementById('groupName'); const groupDescription = document.getElementById('groupDescription'); const groupSliceFormat = document.getElementById('groupSliceFormat'); const groupSlicePrice = document.getElementById('groupSlicePrice'); if (!groupName || !groupName.value.trim()) { return null; } return { name: groupName.value.trim(), description: groupDescription ? groupDescription.value.trim() : '', slice_format: groupSliceFormat ? groupSliceFormat.value : 'performance', slice_price: groupSlicePrice ? groupSlicePrice.value : '100' }; } /** * Load existing groups for selection */ async function loadExistingGroups() { try { // Use detailed API for consistent shape and stats const result = await window.apiJson('/api/dashboard/node-groups/api', { cache: 'no-store' }); const entries = (result && Array.isArray(result.groups)) ? result.groups : (Array.isArray(result) ? result : []); const existingGroupSelect = document.getElementById('existingGroup'); if (existingGroupSelect) { existingGroupSelect.innerHTML = ''; const { defaults, customs } = normalizeGroups(entries); const append = ({ group, stats }) => { const total = stats && typeof stats.total_nodes === 'number' ? stats.total_nodes : (group.nodes ? group.nodes.length : undefined); const isCustom = group.group_type && ((typeof group.group_type === 'string' && group.group_type === 'Custom') || (typeof group.group_type === 'object' && group.group_type.Custom)); const option = document.createElement('option'); option.value = group.id; option.textContent = `${group.name}${isCustom ? ' (Custom)' : ''}${typeof total === 'number' ? ` (${total} nodes)` : ''}`; existingGroupSelect.appendChild(option); }; defaults.forEach(append); customs.forEach(append); } } catch (error) { console.error('🌐 Error loading existing groups:', error); } } // Stale-guard for node details fetches // Guard against redeclaration - use window scope to persist across reloads window.__nodeDetailsSeq = window.__nodeDetailsSeq || 0; let __nodeDetailsSeq = window.__nodeDetailsSeq; /** * View node details */ async function viewNodeDetails(nodeId) { console.log('πŸ‘οΈ Viewing node details:', nodeId); try { // Abort any in-flight request to prevent stale data try { if (window.__nodeDetailsController) { window.__nodeDetailsController.abort(); } } catch (_) {} window.__nodeDetailsController = (typeof AbortController !== 'undefined') ? new AbortController() : null; // Sequence guard to avoid race conditions const requestId = ++window.__nodeDetailsSeq; const nodeData = await window.apiJson(`/api/dashboard/farm-nodes/${nodeId}`, { cache: 'no-store', signal: window.__nodeDetailsController ? window.__nodeDetailsController.signal : undefined }); // If a newer request started, ignore this response if (requestId !== window.__nodeDetailsSeq) return; console.log('πŸ‘οΈ Loaded node details:', nodeData); showNodeDetailsModal(nodeData); } catch (error) { console.error('πŸ‘οΈ Error loading node details:', error); showNotification('Failed to load node details', 'error'); } } /** * Delete node configuration (show confirmation modal) */ async function deleteNodeConfiguration(nodeId) { console.log('πŸ—‘οΈ Initiating node deletion for nodeId:', nodeId); if (!nodeId) { console.error('πŸ—‘οΈ No node ID provided for deletion'); showNotification('Error: No node ID provided', 'error'); return; } try { // Fetch node data to get the node name for confirmation console.log('πŸ—‘οΈ Fetching node data for deletion confirmation:', nodeId); const nodeData = await window.apiJson(`/api/dashboard/farm-nodes/${nodeId}`, { cache: 'no-store' }); console.log('πŸ—‘οΈ Loaded node data for deletion:', nodeData); // Set the pending delete node ID window.pendingDeleteNodeId = nodeId; // Update the modal with node information const deleteNodeNameElement = document.getElementById('deleteNodeName'); if (deleteNodeNameElement) { const nodeName = nodeData.name || `Node ${nodeId}`; deleteNodeNameElement.textContent = nodeName; } // Show the delete confirmation modal const deleteModal = new bootstrap.Modal(document.getElementById('deleteNodeModal')); deleteModal.show(); } catch (error) { console.error('πŸ—‘οΈ Error preparing node deletion:', error); showNotification(`Failed to prepare node deletion: ${error.message}`, 'error'); } } /** * Confirm node deletion (called from modal) */ async function confirmNodeDeletion() { const nodeId = window.pendingDeleteNodeId; console.log('πŸ—‘οΈ confirmNodeDeletion called with nodeId:', nodeId); if (!nodeId) { console.error('πŸ—‘οΈ No pending delete node ID found'); showNotification('Error: No node selected for deletion', 'error'); return; } const confirmBtn = document.getElementById('confirmDeleteNodeBtn'); if (confirmBtn) { confirmBtn.disabled = true; confirmBtn.innerHTML = 'Removing...'; } try { console.log('πŸ—‘οΈ Sending DELETE request to:', `/api/dashboard/farm-nodes/${nodeId}`); await window.apiJson(`/api/dashboard/farm-nodes/${nodeId}`, { method: 'DELETE' }); console.log('πŸ—‘οΈ Node deleted successfully'); showNotification('Node removed successfully', 'success'); // Hide modal const deleteModal = bootstrap.Modal.getInstance(document.getElementById('deleteNodeModal')); if (deleteModal) { deleteModal.hide(); } // Reload resource_provider data and node groups with small delay for backend processing setTimeout(() => { loadResourceProviderData(); loadNodeGroups(); // FARMER FIX: Refresh node groups table after node deletion }, 100); } catch (error) { console.error('πŸ—‘οΈ Error deleting node:', error); showNotification(`Failed to remove node: ${error.message}`, 'error'); } finally { // Reset button state if (confirmBtn) { confirmBtn.disabled = false; confirmBtn.innerHTML = 'Remove Node'; } window.pendingDeleteNodeId = null; } } /** * Load node groups for edit modal */ async function loadNodeGroupsForEdit() { try { const result = await window.apiJson('/api/dashboard/node-groups/api'); const groupsArray = (result && Array.isArray(result.groups)) ? result.groups : (Array.isArray(result) ? result : []); const groupSelect = document.getElementById('nodeGroupSelect'); if (groupSelect) { // Clear existing options and use "Single" to match the table display groupSelect.innerHTML = ''; groupsArray.forEach(groupData => { const group = groupData.group || groupData; const option = document.createElement('option'); option.value = group.id; option.textContent = group.name; groupSelect.appendChild(option); }); console.log('πŸ”§ Loaded groups for edit modal:', groupsArray.length); } } catch (error) { console.error('Error loading node groups for edit:', error); } } /** * Populate node edit form with current values * * @param {object} nodeData - Node data to populate the form with */ function populateNodeEditForm(nodeData) { console.log('πŸ“ Populating node edit form with data:', nodeData); console.log('πŸ“ Node slice formats:', nodeData.slice_formats); console.log('πŸ“ Node rental options:', nodeData.rental_options); // Set group assignment toggle and selection const assignToGroupToggle = document.getElementById('assignToGroupToggle'); const groupSelectionSection = document.getElementById('groupSelectionSection'); const groupSelect = document.getElementById('nodeGroupSelect'); if (nodeData.node_group_id) { // Node is in a group if (assignToGroupToggle) assignToGroupToggle.checked = true; if (groupSelectionSection) groupSelectionSection.style.display = 'block'; if (groupSelect) groupSelect.value = nodeData.node_group_id; console.log('πŸ“ Set node group assignment:', nodeData.node_group_id); } else { // Node is individual (Single) if (assignToGroupToggle) assignToGroupToggle.checked = false; if (groupSelectionSection) groupSelectionSection.style.display = 'none'; if (groupSelect) groupSelect.value = ''; console.log('πŸ“ Set node as individual (no group)'); } // Set slice rental enabled state const sliceRentalCheckbox = document.getElementById('enableSliceRental'); if (sliceRentalCheckbox && nodeData.rental_options?.slice_rental_enabled !== undefined) { sliceRentalCheckbox.checked = nodeData.rental_options.slice_rental_enabled; console.log('πŸ“ Set slice rental enabled:', nodeData.rental_options.slice_rental_enabled); } // Set slice format selections using the new card format if (nodeData.slice_formats && Array.isArray(nodeData.slice_formats)) { console.log('πŸ“ Setting slice format selections for:', nodeData.slice_formats); // Simple retry mechanism for slice format population const populateSliceFormats = (attempt = 1) => { console.log(`πŸ“ Attempt ${attempt} to populate slice formats`); // Debug: List all available checkboxes first const allCheckboxes = document.querySelectorAll('.slice-format-checkbox'); console.log('πŸ“ Available checkboxes:', Array.from(allCheckboxes).map(cb => cb.id)); console.log('πŸ“ Looking for:', nodeData.slice_formats.map(f => `slice_${f}`)); let successCount = 0; nodeData.slice_formats.forEach(formatId => { // Look for the new checkbox format from createSliceFormatCard const checkbox = document.getElementById(`slice_${formatId}`); console.log(`πŸ“ Looking for checkbox: slice_${formatId}, found:`, !!checkbox); if (checkbox) { checkbox.checked = true; checkbox.dispatchEvent(new Event('change')); // Also add the selected class to the card const card = checkbox.closest('.slice-format-card'); if (card) { card.classList.add('selected'); console.log(`πŸ“ Added selected class to card for ${formatId}`); } successCount++; console.log(`πŸ“ βœ… Successfully set checkbox for ${formatId}`); } else { console.warn(`πŸ“ ❌ Checkbox not found for format: ${formatId}`); } }); console.log(`πŸ“ Attempt ${attempt}: Successfully set ${successCount}/${nodeData.slice_formats.length} slice format selections`); // If we didn't get all formats and this is our first attempt, try again after a short delay if (successCount < nodeData.slice_formats.length && attempt === 1) { console.log('πŸ“ Retrying slice format population in 100ms...'); setTimeout(() => populateSliceFormats(2), 100); } else if (successCount === 0) { console.error('πŸ“ ❌ Failed to set any slice format selections - elements may not be available'); } }; populateSliceFormats(); // Try to populate slice pricing if available if (nodeData.slice_prices || nodeData.group_slice_price) { console.log('πŸ“ Found slice pricing data:', { slice_prices: nodeData.slice_prices, group_slice_price: nodeData.group_slice_price }); // If there's a group slice price, we could populate that if (nodeData.group_slice_price) { console.log('πŸ“ Node has group slice price:', nodeData.group_slice_price); // This would be used for group-level pricing } // If there are individual slice prices, populate those if (nodeData.slice_prices && typeof nodeData.slice_prices === 'object') { console.log('πŸ“ Node has individual slice prices:', nodeData.slice_prices); // This could be used to populate slice-specific pricing fields if they exist Object.entries(nodeData.slice_prices).forEach(([sliceFormat, price]) => { console.log(`πŸ“ Slice ${sliceFormat} price: ${price} TFP`); // Future: populate slice-specific pricing fields when they're added to the UI }); } } } else { console.log('πŸ“ No slice formats to set or invalid format'); } // Set full node rental settings and populate pricing data const fullNodeRentalCheckbox = document.getElementById('enableFullNodeRental'); const fullNodePricingSection = document.getElementById('fullNodePricingSection'); console.log('πŸ“ Full node rental data:', nodeData.rental_options); console.log('πŸ“ Full node rental enabled:', nodeData.rental_options?.full_node_rental_enabled); if (nodeData.rental_options?.full_node_rental_enabled) { console.log('πŸ“ Setting full node rental settings:', nodeData.rental_options); if (fullNodeRentalCheckbox) { console.log('πŸ“ Setting full node rental checkbox to checked'); fullNodeRentalCheckbox.checked = true; // Manually show the pricing section since the checkbox is checked if (fullNodePricingSection) { console.log('πŸ“ Showing full node pricing section'); fullNodePricingSection.style.display = 'block'; } // Trigger the change event to ensure any other handlers are called fullNodeRentalCheckbox.dispatchEvent(new Event('change', { bubbles: true })); } // Populate pricing fields if available if (nodeData.rental_options.full_node_pricing) { const pricing = nodeData.rental_options.full_node_pricing; console.log('πŸ“ Populating pricing fields:', pricing); // Populate pricing fields immediately (no timeout needed) const populatePricingFields = () => { // Populate hourly rate const hourlyInput = document.getElementById('fullNodePriceHour') || document.getElementById('fullNodePriceHourNew'); if (hourlyInput && pricing.hourly !== undefined) { hourlyInput.value = pricing.hourly; console.log('πŸ“ Set hourly rate:', pricing.hourly); } // Populate daily rate const dailyInput = document.getElementById('fullNodePriceDay') || document.getElementById('fullNodePriceDayNew'); if (dailyInput && pricing.daily !== undefined) { dailyInput.value = pricing.daily; console.log('πŸ“ Set daily rate:', pricing.daily); } // Populate monthly rate const monthlyInput = document.getElementById('fullNodePriceMonth') || document.getElementById('fullNodePriceMonthNew'); if (monthlyInput && pricing.monthly !== undefined) { monthlyInput.value = pricing.monthly; console.log('πŸ“ Set monthly rate:', pricing.monthly); } // Populate yearly rate const yearlyInput = document.getElementById('fullNodePriceYear') || document.getElementById('fullNodePriceYearNew'); if (yearlyInput && pricing.yearly !== undefined) { yearlyInput.value = pricing.yearly; console.log('πŸ“ Set yearly rate:', pricing.yearly); } // Populate discount percentages const dailyDiscountInput = document.getElementById('fullNodeDailyDiscount'); if (dailyDiscountInput && pricing.daily_discount_percent !== undefined) { dailyDiscountInput.value = pricing.daily_discount_percent; console.log('πŸ“ Set daily discount:', pricing.daily_discount_percent); } const monthlyDiscountInput = document.getElementById('fullNodeMonthlyDiscount'); if (monthlyDiscountInput && pricing.monthly_discount_percent !== undefined) { monthlyDiscountInput.value = pricing.monthly_discount_percent; console.log('πŸ“ Set monthly discount:', pricing.monthly_discount_percent); } const yearlyDiscountInput = document.getElementById('fullNodeYearlyDiscount'); if (yearlyDiscountInput && pricing.yearly_discount_percent !== undefined) { yearlyDiscountInput.value = pricing.yearly_discount_percent; console.log('πŸ“ Set yearly discount:', pricing.yearly_discount_percent); } }; populatePricingFields(); } // Populate minimum rental duration const minRentalDurationInput = document.getElementById('minRentalDuration'); if (minRentalDurationInput && nodeData.rental_options.minimum_rental_days) { minRentalDurationInput.value = nodeData.rental_options.minimum_rental_days; console.log('πŸ“ Set minimum rental days:', nodeData.rental_options.minimum_rental_days); } // Set auto-renewal if available const autoRenewalInput = document.getElementById('autoRenewalEnabled'); if (autoRenewalInput && nodeData.rental_options.auto_renewal_enabled !== undefined) { autoRenewalInput.checked = nodeData.rental_options.auto_renewal_enabled; console.log('πŸ“ Set auto renewal:', nodeData.rental_options.auto_renewal_enabled); } } else { console.log('πŸ“ Full node rental is disabled for this node'); if (fullNodeRentalCheckbox) { fullNodeRentalCheckbox.checked = false; } if (fullNodePricingSection) { fullNodePricingSection.style.display = 'none'; } } // Set staking configuration const editEnableStaking = document.getElementById('editEnableStaking'); const editStakingConfigSection = document.getElementById('editStakingConfigSection'); console.log('πŸ“ Processing staking configuration for node:', nodeData.id); console.log('πŸ“ Node staking options:', nodeData.staking_options); if (nodeData.staking_options && nodeData.staking_options.staking_enabled) { console.log('πŸ“ Setting staking configuration:', nodeData.staking_options); if (editEnableStaking) { editEnableStaking.checked = true; console.log('πŸ“ Staking checkbox enabled'); // Show staking configuration section if (editStakingConfigSection) { editStakingConfigSection.style.display = 'block'; console.log('πŸ“ Staking configuration section shown'); } } // Populate staking fields const editStakingAmount = document.getElementById('editStakingAmount'); if (editStakingAmount && nodeData.staking_options.staked_amount) { editStakingAmount.value = nodeData.staking_options.staked_amount; console.log('πŸ“ Set staking amount:', nodeData.staking_options.staked_amount); } const editStakingPeriod = document.getElementById('editStakingPeriod'); if (editStakingPeriod && nodeData.staking_options.staking_period_months) { editStakingPeriod.value = nodeData.staking_options.staking_period_months; console.log('πŸ“ Set staking period:', nodeData.staking_options.staking_period_months); } const editEarlyWithdrawalAllowed = document.getElementById('editEarlyWithdrawalAllowed'); if (editEarlyWithdrawalAllowed && nodeData.staking_options.early_withdrawal_allowed !== undefined) { editEarlyWithdrawalAllowed.checked = nodeData.staking_options.early_withdrawal_allowed; console.log('πŸ“ Set early withdrawal allowed:', nodeData.staking_options.early_withdrawal_allowed); } // Show staking status in UI showNotification(`Node has ${nodeData.staking_options.staked_amount} TFP staked`, 'info'); } else { console.log('πŸ“ No staking configuration for this node - setting defaults'); if (editEnableStaking) { editEnableStaking.checked = false; console.log('πŸ“ Staking checkbox disabled'); } if (editStakingConfigSection) { editStakingConfigSection.style.display = 'none'; console.log('πŸ“ Staking configuration section hidden'); } // Clear staking form fields const editStakingAmount = document.getElementById('editStakingAmount'); if (editStakingAmount) { editStakingAmount.value = ''; } const editStakingPeriod = document.getElementById('editStakingPeriod'); if (editStakingPeriod) { editStakingPeriod.value = '12'; // Default to 12 months } const editEarlyWithdrawalAllowed = document.getElementById('editEarlyWithdrawalAllowed'); if (editEarlyWithdrawalAllowed) { editEarlyWithdrawalAllowed.checked = true; // Default to allowed } } // Set availability status const availabilitySelect = document.getElementById('nodeAvailabilityStatus'); if (availabilitySelect && nodeData.availability_status) { availabilitySelect.value = nodeData.availability_status; } } /** * Set up event listeners for node edit modal */ function setupNodeEditEventListeners() { console.log('πŸ”§ setupNodeEditEventListeners called'); // Group assignment toggle const assignToGroupToggle = document.getElementById('assignToGroupToggle'); const groupSelectionSection = document.getElementById('groupSelectionSection'); console.log('πŸ”§ Group toggle found:', !!assignToGroupToggle); if (assignToGroupToggle) { assignToGroupToggle.removeEventListener('change', handleGroupAssignmentToggle); assignToGroupToggle.addEventListener('change', handleGroupAssignmentToggle); } // Full node rental toggle const fullNodeRentalCheckbox = document.getElementById('enableFullNodeRental'); if (fullNodeRentalCheckbox && !fullNodeRentalCheckbox.dataset.editListenerAttached) { // Only attach if not already attached to prevent duplicates fullNodeRentalCheckbox.addEventListener('change', handleFullNodeRentalToggle); fullNodeRentalCheckbox.dataset.editListenerAttached = 'true'; console.log('🏒 Edit modal: Full node rental listener attached'); } // Edit staking toggle const editEnableStaking = document.getElementById('editEnableStaking'); if (editEnableStaking) { editEnableStaking.removeEventListener('change', handleEditStakingToggle); editEnableStaking.addEventListener('change', handleEditStakingToggle); } // Add validation to staking amount input const editStakingAmount = document.getElementById('editStakingAmount'); if (editStakingAmount) { editStakingAmount.removeEventListener('input', handleStakingAmountInput); editStakingAmount.addEventListener('input', handleStakingAmountInput); editStakingAmount.removeEventListener('blur', handleStakingAmountBlur); editStakingAmount.addEventListener('blur', handleStakingAmountBlur); } // Save button const saveBtn = document.getElementById('saveNodeConfigBtn'); if (saveBtn) { // Remove any existing listeners saveBtn.removeEventListener('click', saveNodeConfiguration); // Add the event listener saveBtn.addEventListener('click', function(event) { console.log('πŸ’Ύ Save button clicked! Event:', event); event.preventDefault(); event.stopPropagation(); saveNodeConfiguration(); }); console.log('πŸ’Ύ Save button event listener attached successfully'); } else { console.error('πŸ’Ύ Save button not found!'); } // Add event listener for delete confirmation button const confirmDeleteNodeBtn = document.getElementById('confirmDeleteNodeBtn'); if (confirmDeleteNodeBtn) { // Remove existing listener first confirmDeleteNodeBtn.removeEventListener('click', confirmNodeDeletion); // Add new listener confirmDeleteNodeBtn.addEventListener('click', confirmNodeDeletion); console.log('πŸ—‘οΈ Delete button event listener attached'); } else { console.error('πŸ—‘οΈ confirmDeleteNodeBtn not found!'); } } /** * Handle group assignment toggle */ function handleGroupAssignmentToggle() { const groupSelectionSection = document.getElementById('groupSelectionSection'); if (groupSelectionSection) { groupSelectionSection.style.display = this.checked ? 'block' : 'none'; } } /** * Handle full node rental toggle */ function handleFullNodeRentalToggle() { console.log('🏒 Edit modal: Full node rental toggle changed:', this.checked); const fullNodePricingSection = document.getElementById('fullNodePricingSection'); if (fullNodePricingSection) { if (this.checked) { console.log('🏒 Edit modal: Showing full node pricing section'); fullNodePricingSection.style.display = 'block'; // Initialize pricing calculation for the edit modal setTimeout(() => { console.log('🏒 Edit modal: Initializing full node pricing calculation'); initializeFullNodePricingCalculation(); }, 100); // Don't show notification here - it's already shown by the main event listener } else { console.log('🏒 Edit modal: Hiding full node pricing section'); fullNodePricingSection.style.display = 'none'; // Don't show notification here - it's already shown by the main event listener } } } /** * Handle edit staking toggle */ function handleEditStakingToggle() { const editStakingConfigSection = document.getElementById('editStakingConfigSection'); const editStakingAmount = document.getElementById('editStakingAmount'); console.log('πŸ›‘οΈ Edit staking toggle changed:', this.checked); if (editStakingConfigSection) { editStakingConfigSection.style.display = this.checked ? 'block' : 'none'; } // Load wallet balance and clear previous values when staking is enabled if (this.checked) { console.log('πŸ›‘οΈ Staking enabled - loading wallet balance'); loadWalletBalance(); // Clear previous staking amount to force user to enter new value if (editStakingAmount) { editStakingAmount.value = ''; } } else { console.log('πŸ›‘οΈ Staking disabled'); // Clear staking amount when disabled if (editStakingAmount) { editStakingAmount.value = ''; } } } /** * Handle staking amount input changes (real-time validation) */ function handleStakingAmountInput() { // Clear validation classes on input to provide immediate feedback this.classList.remove('is-invalid', 'is-valid'); clearValidationError(this); } /** * Handle staking amount blur (full validation) */ function handleStakingAmountBlur() { if (this.value.trim() !== '') { validateStakingAmount(this); } } /** * Show confirmation dialog for staking changes */ function showStakingConfirmationDialog(title, message) { return new Promise((resolve) => { console.log('πŸ›‘οΈ Showing staking confirmation dialog:', title); // Create modal HTML const modalHtml = ` `; // Remove existing modal if any const existingModal = document.getElementById('stakingConfirmModal'); if (existingModal) { existingModal.remove(); } // Add modal to body document.body.insertAdjacentHTML('beforeend', modalHtml); // Get modal elements const modal = new bootstrap.Modal(document.getElementById('stakingConfirmModal')); const proceedBtn = document.getElementById('stakingConfirmProceed'); const cancelBtn = document.getElementById('stakingConfirmCancel'); // Add timeout to auto-resolve if modal doesn't work const timeout = setTimeout(() => { console.warn('πŸ›‘οΈ Staking confirmation dialog timeout - auto-confirming'); resolve(true); }, 30000); // 30 second timeout // Handle proceed proceedBtn.addEventListener('click', () => { console.log('πŸ›‘οΈ User confirmed staking change'); clearTimeout(timeout); modal.hide(); resolve(true); }); // Handle cancel cancelBtn.addEventListener('click', () => { console.log('πŸ›‘οΈ User cancelled staking change'); clearTimeout(timeout); modal.hide(); resolve(false); }); // Handle modal close (X button or ESC) document.getElementById('stakingConfirmModal').addEventListener('hidden.bs.modal', () => { console.log('πŸ›‘οΈ Staking confirmation modal closed'); clearTimeout(timeout); document.getElementById('stakingConfirmModal').remove(); }); // Handle close button document.querySelector('#stakingConfirmModal .btn-close').addEventListener('click', () => { console.log('πŸ›‘οΈ User closed staking confirmation dialog'); clearTimeout(timeout); modal.hide(); resolve(false); }); // Show modal modal.show(); console.log('πŸ›‘οΈ Staking confirmation dialog shown'); }); } /** * Save node configuration */ async function saveNodeConfiguration() { console.log('πŸ’Ύ Save node configuration called'); const nodeData = window.currentEditingNode; if (!nodeData) { console.error('πŸ’Ύ No node data found for editing'); showNotification('Error: No node selected for editing', 'error'); return; } console.log('πŸ’Ύ Saving configuration for node:', nodeData.id); const saveBtn = document.getElementById('saveNodeConfigBtn'); if (saveBtn) { saveBtn.disabled = true; saveBtn.innerHTML = 'Saving...'; console.log('πŸ’Ύ Save button disabled and loading state set'); } try { // Collect form data - only include fields that are available/changed const formData = {}; // Handle group assignment based on toggle state const assignToGroupToggle = document.getElementById('assignToGroupToggle'); const groupSelect = document.getElementById('nodeGroupSelect'); if (assignToGroupToggle && groupSelect) { if (assignToGroupToggle.checked && groupSelect.value) { formData.node_group_id = groupSelect.value; } else { formData.node_group_id = null; // Set to Single/Individual } } // Collect slice pricing settings (new automatic slice system) const slicePriceInput = document.getElementById('editSlicePrice'); if (slicePriceInput) { formData.slice_pricing = { base_price_per_hour: parseFloat(slicePriceInput.value) || 0.50 }; console.log('πŸ’Ύ Collected slice pricing:', formData.slice_pricing); } // Note: Slice formats and rental options are now automatically managed // Collect auto-renewal setting const autoRenewalInput = document.getElementById('autoRenewalEnabled'); if (autoRenewalInput) { formData.auto_renewal_enabled = autoRenewalInput.checked; } // Collect staking configuration (include in main configuration) const editEnableStakingCheckbox = document.getElementById('editEnableStaking'); console.log('πŸ›‘οΈ Checking staking configuration...'); console.log('πŸ›‘οΈ Staking checkbox found:', !!editEnableStakingCheckbox); console.log('πŸ›‘οΈ Staking checkbox checked:', editEnableStakingCheckbox?.checked); if (editEnableStakingCheckbox) { formData.staking_enabled = editEnableStakingCheckbox.checked; if (editEnableStakingCheckbox.checked) { const editStakingAmountInput = document.getElementById('editStakingAmount'); const editStakingPeriodSelect = document.getElementById('editStakingPeriod'); const editEarlyWithdrawalInput = document.getElementById('editEarlyWithdrawalAllowed'); console.log('πŸ›‘οΈ Staking enabled, collecting data...'); console.log('πŸ›‘οΈ Amount input found:', !!editStakingAmountInput); console.log('πŸ›‘οΈ Amount input value:', editStakingAmountInput?.value); console.log('πŸ›‘οΈ Period select found:', !!editStakingPeriodSelect); console.log('πŸ›‘οΈ Period select value:', editStakingPeriodSelect?.value); console.log('πŸ›‘οΈ Early withdrawal found:', !!editEarlyWithdrawalInput); console.log('πŸ›‘οΈ Early withdrawal checked:', editEarlyWithdrawalInput?.checked); if (editStakingAmountInput && editStakingAmountInput.value) { console.log('πŸ›‘οΈ Validating staking amount:', editStakingAmountInput.value); // Validate staking amount const isValid = validateStakingAmount(editStakingAmountInput); console.log('πŸ›‘οΈ Validation result:', isValid); if (!isValid) { console.error('πŸ›‘οΈ Staking amount validation failed'); throw new Error('Invalid staking amount'); } const newStakingAmount = parseFloat(editStakingAmountInput.value); const currentStakingAmount = nodeData.staking_options?.staked_amount || 0; // Check if this is a significant staking change that affects wallet balance const stakingChange = newStakingAmount - currentStakingAmount; const walletBalance = window.userWalletBalance || 0; // For now, skip confirmation dialogs to test basic functionality // TODO: Re-enable confirmations after basic save is working console.log('πŸ›‘οΈ Staking change detected:', { newAmount: newStakingAmount, currentAmount: currentStakingAmount, change: stakingChange, walletBalance: walletBalance }); // Basic validation without confirmation if (stakingChange > 0 && stakingChange > walletBalance) { throw new Error(`Insufficient wallet balance. Need ${stakingChange} TFP, have ${walletBalance} TFP`); } formData.staked_amount = newStakingAmount; formData.staking_period_months = editStakingPeriodSelect ? parseInt(editStakingPeriodSelect.value) : 12; formData.early_withdrawal_allowed = editEarlyWithdrawalInput ? editEarlyWithdrawalInput.checked : true; formData.early_withdrawal_penalty_percent = 25.0; // Default penalty console.log('πŸ›‘οΈ Collected staking data for main configuration:', { staked_amount: formData.staked_amount, staking_period_months: formData.staking_period_months, early_withdrawal_allowed: formData.early_withdrawal_allowed }); } else { console.error('πŸ›‘οΈ Staking enabled but no amount input found or no value provided'); console.error('πŸ›‘οΈ Amount input element:', editStakingAmountInput); console.error('πŸ›‘οΈ Amount input value:', editStakingAmountInput?.value); throw new Error('Staking amount is required when staking is enabled'); } } else { // Disabling staking - log the action const currentStakingAmount = nodeData.staking_options?.staked_amount || 0; console.log('πŸ›‘οΈ Disabling staking. Current staked amount:', currentStakingAmount); if (currentStakingAmount > 0) { const penalty = currentStakingAmount * 0.25; // 25% penalty const netReturn = currentStakingAmount - penalty; console.log('πŸ›‘οΈ Staking disable will return:', netReturn, 'TFP (after', penalty, 'TFP penalty)'); } console.log('πŸ›‘οΈ Staking disabled'); } } else { console.log('πŸ›‘οΈ Staking checkbox not found'); } // Always include availability status if control exists const availabilitySelect = document.getElementById('nodeAvailabilityStatus'); if (availabilitySelect) { formData.availability_status = availabilitySelect.value; } // Note: Slice validation removed - slices are now automatically managed console.log('πŸ’Ύ Saving node configuration for node:', nodeData.id); console.log('πŸ’Ύ Form data being saved:', formData); // Save the complete node configuration (including staking) const configResult = await window.apiJson(`/api/dashboard/farm-nodes/${nodeData.id}/configuration`, { method: 'PUT', body: formData }); console.log('πŸ’Ύ Node configuration saved:', configResult); // Show success message if (formData.staking_enabled) { showNotification('Node configuration and staking updated successfully', 'success'); } else { showNotification('Node configuration updated successfully', 'success'); } // Hide modal const modal = bootstrap.Modal.getInstance(document.getElementById('nodeEditModal')); if (modal) { modal.hide(); } // Reload resource_provider data and node groups with small delay for backend processing setTimeout(async () => { await loadResourceProviderData(); await loadNodeGroups(); // FARMER FIX: Refresh node groups table after node configuration changes await loadWalletBalance(); // Refresh wallet balance after staking changes await updateStakingDisplay(); // Refresh staking statistics after staking changes }, 100); } catch (error) { console.error('πŸ’Ύ Error saving node configuration:', error); console.error('πŸ’Ύ Error details:', error.message, error.stack); showNotification(`Failed to save node configuration: ${error.message}`, 'error'); } finally { // Reset button state if (saveBtn) { saveBtn.disabled = false; saveBtn.innerHTML = 'Save Configuration'; console.log('πŸ’Ύ Save button reset to enabled state'); } } } /** * Test save function - can be called from browser console */ window.testSaveNodeConfiguration = function() { console.log('πŸ§ͺ Testing save node configuration...'); const saveBtn = document.getElementById('saveNodeConfigBtn'); console.log('πŸ§ͺ Save button found:', !!saveBtn); if (saveBtn) { console.log('πŸ§ͺ Save button disabled:', saveBtn.disabled); console.log('πŸ§ͺ Save button innerHTML:', saveBtn.innerHTML); console.log('πŸ§ͺ Save button onclick:', saveBtn.onclick); console.log('πŸ§ͺ Save button style.display:', saveBtn.style.display); console.log('πŸ§ͺ Save button offsetParent:', saveBtn.offsetParent); console.log('πŸ§ͺ Save button getBoundingClientRect:', saveBtn.getBoundingClientRect()); // Test if button is actually clickable console.log('πŸ§ͺ Attempting to click button programmatically...'); saveBtn.click(); // Test direct handler call if (window.debugSaveHandler) { console.log('πŸ§ͺ Calling save handler directly...'); window.debugSaveHandler({ preventDefault: () => {}, stopPropagation: () => {} }); } } const nodeData = window.currentEditingNode; console.log('πŸ§ͺ Current editing node:', nodeData); if (nodeData) { console.log('πŸ§ͺ Calling saveNodeConfiguration directly...'); saveNodeConfiguration(); } else { console.log('πŸ§ͺ No node data available for editing'); } }; /** * Test button click simulation */ window.testButtonClick = function() { console.log('πŸ§ͺ Testing button click simulation...'); const saveBtn = window.debugSaveBtn || document.getElementById('saveNodeConfigBtn'); if (saveBtn) { console.log('πŸ§ͺ Button found, simulating click...'); // Try different click simulation methods console.log('πŸ§ͺ Method 1: Direct click()'); saveBtn.click(); console.log('πŸ§ͺ Method 2: Mouse events'); saveBtn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); saveBtn.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); saveBtn.dispatchEvent(new MouseEvent('click', { bubbles: true })); console.log('πŸ§ͺ Method 3: Direct handler call'); if (window.debugSaveHandler) { window.debugSaveHandler({ preventDefault: () => console.log('preventDefault called'), stopPropagation: () => console.log('stopPropagation called'), target: saveBtn, currentTarget: saveBtn }); } } else { console.log('πŸ§ͺ Save button not found'); } }; /** * Simple test to check if staking can be enabled */ window.testStakingEnable = function() { console.log('πŸ§ͺ Testing staking enable...'); const stakingCheckbox = document.getElementById('editEnableStaking'); const stakingAmount = document.getElementById('editStakingAmount'); if (stakingCheckbox && stakingAmount) { stakingCheckbox.checked = true; stakingAmount.value = '10'; // Trigger the toggle event stakingCheckbox.dispatchEvent(new Event('change')); console.log('πŸ§ͺ Staking enabled with 10 TFP'); console.log('πŸ§ͺ Now try clicking save or call testSaveNodeConfiguration()'); } else { console.log('πŸ§ͺ Staking elements not found'); } }; /** * Get status badge class for node status */ function getStatusBadgeClass(status) { const statusMap = { 'Online': 'bg-success', 'Offline': 'bg-danger', 'Maintenance': 'bg-warning', 'Error': 'bg-danger', 'Standby': 'bg-secondary' }; return statusMap[status] || 'bg-secondary'; } /** * Show slice details modal */ function showSliceDetailsModal(sliceData) { // Create modal HTML const modalHtml = ` `; // Remove existing modal if present const existingModal = document.getElementById('sliceDetailsModal'); if (existingModal) { existingModal.remove(); } // Add modal to DOM document.body.insertAdjacentHTML('beforeend', modalHtml); // Show modal const modal = new bootstrap.Modal(document.getElementById('sliceDetailsModal')); modal.show(); // Clean up when modal is hidden document.getElementById('sliceDetailsModal').addEventListener('hidden.bs.modal', function() { this.remove(); }); } /** * Show slice edit modal */ function showSliceEditModal(sliceData) { const isDefault = sliceData.isDefault || false; // Create modal HTML with enhanced pricing controls const modalHtml = ` `; // Remove existing modal if present const existingModal = document.getElementById('sliceEditModal'); if (existingModal) { existingModal.remove(); } // Add modal to DOM document.body.insertAdjacentHTML('beforeend', modalHtml); // Show modal const modal = new bootstrap.Modal(document.getElementById('sliceEditModal')); modal.show(); // Initialize pricing calculation for edit modal setTimeout(() => initializeEditPricingCalculation(), 100); // Clean up when modal is hidden document.getElementById('sliceEditModal').addEventListener('hidden.bs.modal', function() { this.remove(); }); } /** * Initialize pricing calculation for edit modal */ function initializeEditPricingCalculation() { const priceHour = document.getElementById('editPriceHour'); const priceDay = document.getElementById('editPriceDay'); const priceMonth = document.getElementById('editPriceMonth'); const priceYear = document.getElementById('editPriceYear'); const autoPricingToggle = document.getElementById('editAutoPricingToggle'); // Discount controls const dailyDiscount = document.getElementById('editDailyDiscount'); const monthlyDiscount = document.getElementById('editMonthlyDiscount'); const yearlyDiscount = document.getElementById('editYearlyDiscount'); if (!priceHour || !priceDay || !priceMonth || !priceYear || !autoPricingToggle) return; if (!dailyDiscount || !monthlyDiscount || !yearlyDiscount) return; // Helper function to check if auto-calculation is enabled const isAutoCalculationEnabled = () => autoPricingToggle.checked; // Helper function to calculate rate with discount const applyDiscount = (baseRate, discountPercent) => { const discount = parseFloat(discountPercent) || 0; return baseRate * (1 - discount / 100); }; // Calculate all rates from hourly base rate const calculateFromHourly = () => { if (!isAutoCalculationEnabled()) return; const hourlyRate = parseFloat(priceHour.value) || 0; if (hourlyRate <= 0) { priceDay.value = ''; priceMonth.value = ''; priceYear.value = ''; return; } // Calculate base rates (no discount) const baseDailyRate = hourlyRate * 24; const baseMonthlyRate = hourlyRate * 24 * 30; const baseYearlyRate = hourlyRate * 24 * 365; // Apply discounts const finalDailyRate = applyDiscount(baseDailyRate, dailyDiscount.value); const finalMonthlyRate = applyDiscount(baseMonthlyRate, monthlyDiscount.value); const finalYearlyRate = applyDiscount(baseYearlyRate, yearlyDiscount.value); // Update fields priceDay.value = finalDailyRate.toFixed(2); priceMonth.value = finalMonthlyRate.toFixed(0); priceYear.value = finalYearlyRate.toFixed(0); }; // Auto-calculate when hourly rate changes priceHour.addEventListener('input', calculateFromHourly); // Auto-calculate when discount percentages change dailyDiscount.addEventListener('input', calculateFromHourly); monthlyDiscount.addEventListener('input', calculateFromHourly); yearlyDiscount.addEventListener('input', calculateFromHourly); // Handle toggle changes autoPricingToggle.addEventListener('change', function() { const pricingFields = [priceDay, priceMonth, priceYear]; const discountFields = [dailyDiscount, monthlyDiscount, yearlyDiscount]; if (this.checked) { // Enable auto-calculation pricingFields.forEach(field => { field.readOnly = true; field.placeholder = 'Auto-calculated'; field.title = 'Auto-calculated from hourly rate and discount percentage'; }); discountFields.forEach(field => { field.disabled = false; field.style.opacity = '1'; }); // Recalculate immediately calculateFromHourly(); } else { // Disable auto-calculation pricingFields.forEach(field => { field.readOnly = false; field.placeholder = 'Set custom rate'; field.title = 'Set custom rate for special deals'; }); discountFields.forEach(field => { field.disabled = true; field.style.opacity = '0.5'; }); } }); // Initialize state autoPricingToggle.dispatchEvent(new Event('change')); } /** * Save slice configuration changes */ async function saveSliceConfiguration(sliceId) { console.log('🍰 Saving slice configuration:', sliceId); // Check if this is a default slice const isDefault = sliceId === 'basic' || sliceId === 'standard' || sliceId === 'performance'; // Get form data const name = document.getElementById('editSliceName').value.trim(); const description = document.getElementById('editSliceDescription').value.trim(); const cpuCores = parseInt(document.getElementById('editSliceCPU').value) || 2; const memoryGb = parseInt(document.getElementById('editSliceRAM').value) || 4; const storageGb = parseInt(document.getElementById('editSliceStorage').value) || 100; const bandwidthMbps = parseInt(document.getElementById('editSliceBandwidth').value) || 100; const minUptimeSla = parseFloat(document.getElementById('editSliceUptime').value) || 99.0; const publicIps = parseInt(document.getElementById('editSliceIPs').value) || 0; const hourlyPrice = parseFloat(document.getElementById('editPriceHour').value) || 0; const dailyPrice = parseFloat(document.getElementById('editPriceDay').value) || 0; const monthlyPrice = parseFloat(document.getElementById('editPriceMonth').value) || 0; const yearlyPrice = parseFloat(document.getElementById('editPriceYear').value) || 0; // Validate form data if (!name || cpuCores <= 0 || memoryGb <= 0 || storageGb <= 0 || hourlyPrice <= 0) { showNotification('Please fill in all required fields with valid values', 'warning'); return; } const updateData = { name: name, description: description, pricing: { hourly: hourlyPrice, daily: dailyPrice, monthly: monthlyPrice, yearly: yearlyPrice }, slice_configuration: { cpu_cores: cpuCores, memory_gb: memoryGb, storage_gb: storageGb, bandwidth_mbps: bandwidthMbps, min_uptime_sla: minUptimeSla, public_ips: publicIps, pricing: { hourly: hourlyPrice, daily: dailyPrice, monthly: monthlyPrice, yearly: yearlyPrice } } }; try { if (isDefault) { // Save default slice customizations const customizationData = { name: name, description: description, cpu_cores: cpuCores, memory_gb: memoryGb, storage_gb: storageGb, bandwidth_mbps: bandwidthMbps, price_per_hour: hourlyPrice }; const result = await window.apiJson(`/api/dashboard/default-slice-customization/${sliceId}`, { method: 'PUT', body: customizationData }); console.log('🍰 Default slice customization saved:', result); showNotification(`Default slice "${name}" updated successfully`, 'success'); } else { // Handle custom slice updates const result = await window.apiJson(`/api/dashboard/slice-configuration/${sliceId}`, { method: 'PUT', body: updateData }); console.log('🍰 Slice configuration updated:', result); showNotification('Slice configuration updated successfully', 'success'); } // Close modal const modal = bootstrap.Modal.getInstance(document.getElementById('sliceEditModal')); if (modal) { modal.hide(); } // Reload slice templates loadSliceTemplates(); } catch (error) { console.error('🍰 Error updating slice configuration:', error); showNotification('Failed to update slice configuration', 'error'); } } /** * Load slice formats for node addition modal */ async function loadSliceFormatsForNodeAddition() { console.log('🍰 Loading slice formats for node addition'); const sliceFormatSelection = document.getElementById('sliceFormatSelection'); if (!sliceFormatSelection) return; try { // Load default slice formats const defaultFormats = await window.apiJson('/api/dashboard/default-slice-formats'); // Load user's custom slice products const sliceProducts = await window.apiJson('/api/dashboard/slice-products'); console.log('🍰 Loaded formats for node addition:', { defaultFormats, sliceProducts }); // Clear loading state sliceFormatSelection.innerHTML = ''; // Add default formats defaultFormats.forEach(format => { const formatCard = createSliceFormatCard(format, 'default'); sliceFormatSelection.appendChild(formatCard); }); // Add custom slice products sliceProducts.forEach(product => { const sliceConfig = product.attributes?.slice_configuration?.value; if (sliceConfig) { const formatCard = createSliceFormatCard({ id: product.id, name: product.name, cpu_cores: sliceConfig.cpu_cores, memory_gb: sliceConfig.memory_gb, storage_gb: sliceConfig.storage_gb, bandwidth_mbps: sliceConfig.bandwidth_mbps, price: product.base_price, currency: product.base_currency, pricing: sliceConfig.pricing }, 'custom'); sliceFormatSelection.appendChild(formatCard); } }); // If no formats available, show message if (defaultFormats.length === 0 && sliceProducts.length === 0) { sliceFormatSelection.innerHTML = `

No slice formats available

Create slice configurations first

`; } } catch (error) { console.error('🍰 Error loading slice formats for node addition:', error); sliceFormatSelection.innerHTML = `

Failed to load slice formats

`; } } /** * Create a slice format selection card */ function createSliceFormatCard(format, type) { const col = document.createElement('div'); col.className = 'col-md-6 col-lg-4 mb-3'; const isCustom = type === 'custom'; const currency = format.currency || 'TFP'; // Create pricing display using persistent data let pricingDisplay = ''; if (isCustom) { // For custom slices, the actual price is in base_price field // The pricing object in slice_configuration is often empty/zero let hourlyRate = 0; // Priority order: base_price (main price) > pricing.hourly > price > fallback if (format.base_price && format.base_price > 0) { hourlyRate = format.base_price; } else if (format.pricing && format.pricing.hourly && format.pricing.hourly > 0) { hourlyRate = format.pricing.hourly; } else if (format.price && format.price > 0) { hourlyRate = format.price; } else { hourlyRate = 0; // Show 0 if no valid price found } console.log('🍰 Custom slice pricing debug:', { formatId: format.id, formatName: format.name, base_price: format.base_price, pricing: format.pricing, price: format.price, calculatedHourlyRate: hourlyRate }); pricingDisplay = `
${hourlyRate} ${currency}/hour
`; } else { // Use persistent data from resource_provider settings (price_per_hour) instead of hardcoded fallback const hourlyPrice = format.price_per_hour || format.price || 10; // Default to 10 if no price found pricingDisplay = `${hourlyPrice} ${currency}/hour`; } col.innerHTML = `
${getSliceFormatDisplayName(format.id, format.name)}
${format.cpu_cores} vCPU β€’ ${format.memory_gb}GB RAM
${format.storage_gb}GB Storage β€’ ${format.bandwidth_mbps}Mbps
${pricingDisplay} ${isCustom ? 'Custom' : 'Default'}
`; // Add click handler for card selection const card = col.querySelector('.slice-format-card'); const checkbox = col.querySelector('.slice-format-checkbox'); card.addEventListener('click', function(e) { if (e.target.type !== 'checkbox') { checkbox.checked = !checkbox.checked; checkbox.dispatchEvent(new Event('change')); } }); checkbox.addEventListener('change', function() { if (this.checked) { card.classList.add('selected'); } else { card.classList.remove('selected'); } updateSelectedSliceFormats(); }); return col; } /** * Get proper display name for slice format */ function getSliceFormatDisplayName(formatId, formatName) { // Handle special slice format IDs that should show proper names const sliceFormatNames = { 'slice_d4e3d66d': 'Excellence HA', 'slice_d4e3d66dbasic': 'Excellence HA Basic', 'slice_d4e3d66dstandard': 'Excellence HA Standard', 'slice_d4e3d66dperformance': 'Excellence HA Performance', 'basic': 'Basic', 'standard': 'Standard', 'performance': 'Performance', 'small': 'Small', 'medium': 'Medium', 'large': 'Large', 'xlarge': 'Extra Large' }; // First check hardcoded mappings for default formats if (sliceFormatNames[formatId]) { return sliceFormatNames[formatId]; } // If formatName is provided, use it if (formatName) { return formatName; } // Look up custom slice products from loaded resource_provider data if (window.resourceProviderData && window.resourceProviderData.slice_products) { const sliceProduct = window.resourceProviderData.slice_products.find(product => product.id === formatId); if (sliceProduct && sliceProduct.name) { return sliceProduct.name; } } // Look up from globally loaded slice products if resource_provider data not available if (window.loadedSliceProducts) { const sliceProduct = window.loadedSliceProducts.find(product => product.id === formatId); if (sliceProduct && sliceProduct.name) { return sliceProduct.name; } } // Fallback to the formatId itself return formatId; } /** * Update selected slice formats display */ function updateSelectedSliceFormats() { const selectedFormats = document.querySelectorAll('.slice-format-checkbox:checked'); console.log('🍰 Selected slice formats:', selectedFormats.length); // Enable/disable add nodes button based on selection const addNodesBtn = document.getElementById('addNodesBtn'); if (addNodesBtn) { // At least one slice format must be selected (or full node rental enabled) const fullNodeEnabled = document.getElementById('enableFullNodeRental')?.checked; const hasSliceSelection = selectedFormats.length > 0; if (hasSliceSelection || fullNodeEnabled) { addNodesBtn.disabled = false; } else { addNodesBtn.disabled = true; } } } /** * Get selected slice configuration for node addition */ function getSelectedSliceConfiguration() { const selectedFormats = document.querySelectorAll('.slice-format-checkbox:checked'); const sliceFormats = Array.from(selectedFormats).map(checkbox => checkbox.value); const fullNodeRental = document.getElementById('enableFullNodeRental')?.checked || false; return { slice_formats: sliceFormats, full_node_rental_enabled: fullNodeRental, slice_rental_enabled: sliceFormats.length > 0 }; } /** * Sync node data from Mycelium Grid */ /** * Show default slice details modal */ function showDefaultSliceDetailsModal(formatData) { console.log('🍰 Showing default slice details modal:', formatData); const modalHtml = ` `; // Remove existing modal if present const existingModal = document.getElementById('defaultSliceDetailsModal'); if (existingModal) { existingModal.remove(); } // Add modal to DOM document.body.insertAdjacentHTML('beforeend', modalHtml); // Show modal const modal = new bootstrap.Modal(document.getElementById('defaultSliceDetailsModal')); modal.show(); // Clean up when modal is hidden document.getElementById('defaultSliceDetailsModal').addEventListener('hidden.bs.modal', function() { this.remove(); }); } /** * Show default slice edit modal */ function showDefaultSliceEditModal(formatData) { console.log('🍰 Showing default slice edit modal:', formatData); const modalHtml = ` `; // Remove existing modal if present const existingModal = document.getElementById('defaultSliceEditModal'); if (existingModal) { existingModal.remove(); } // Add modal to DOM document.body.insertAdjacentHTML('beforeend', modalHtml); // Show modal const modal = new bootstrap.Modal(document.getElementById('defaultSliceEditModal')); modal.show(); // Add real-time calculation updates const priceHourInput = document.getElementById('defaultPriceHour'); priceHourInput.addEventListener('input', function() { const hourlyRate = parseFloat(this.value) || 0; document.getElementById('defaultCalcDaily').textContent = (hourlyRate * 24).toFixed(2) + ' TFP'; document.getElementById('defaultCalcMonthly').textContent = (hourlyRate * 24 * 30).toFixed(0) + ' TFP'; document.getElementById('defaultCalcYearly').textContent = (hourlyRate * 24 * 365).toFixed(0) + ' TFP'; }); // Clean up when modal is hidden document.getElementById('defaultSliceEditModal').addEventListener('hidden.bs.modal', function() { this.remove(); }); } /** * Save default slice configuration */ async function saveDefaultSliceConfiguration(formatId) { console.log('🍰 Saving default slice configuration:', formatId); const priceHour = parseFloat(document.getElementById('defaultPriceHour').value) || 0; if (priceHour <= 0) { showNotification('Please enter a valid hourly rate', 'warning'); return; } const saveBtn = document.querySelector('#defaultSliceEditModal .btn-primary'); if (saveBtn) { saveBtn.disabled = true; saveBtn.innerHTML = 'Saving...'; } try { // For now, just show success message since we're not persisting default slice customizations // In a real implementation, this would save to user preferences showNotification(`Default slice pricing updated: ${priceHour} TFP/hour`, 'success'); // Close modal const modal = bootstrap.Modal.getInstance(document.getElementById('defaultSliceEditModal')); if (modal) { modal.hide(); } // Reload slice templates to show updated pricing loadSliceTemplates(); } catch (error) { console.error('🍰 Error saving default slice configuration:', error); showNotification('Failed to save configuration', 'error'); } finally { // Reset button state if (saveBtn) { saveBtn.disabled = false; saveBtn.innerHTML = 'Save Changes'; } } }/** * Show default slice details modal */ function showDefaultSliceDetailsModal(formatData) { console.log('🍰 Showing default slice details modal:', formatData); const modalHtml = ` `; // Remove existing modal if present const existingModal = document.getElementById('defaultSliceDetailsModal'); if (existingModal) { existingModal.remove(); } // Add modal to DOM document.body.insertAdjacentHTML('beforeend', modalHtml); // Show modal const modal = new bootstrap.Modal(document.getElementById('defaultSliceDetailsModal')); modal.show(); // Clean up when modal is hidden document.getElementById('defaultSliceDetailsModal').addEventListener('hidden.bs.modal', function() { this.remove(); }); } /** * Show default slice edit modal */ function showDefaultSliceEditModal(formatData) { console.log('🍰 Showing default slice edit modal:', formatData); const modalHtml = ` `; // Remove existing modal if present const existingModal = document.getElementById('defaultSliceEditModal'); if (existingModal) { existingModal.remove(); } // Add modal to DOM document.body.insertAdjacentHTML('beforeend', modalHtml); // Show modal const modal = new bootstrap.Modal(document.getElementById('defaultSliceEditModal')); modal.show(); // Add real-time calculation updates const priceHourInput = document.getElementById('defaultPriceHour'); priceHourInput.addEventListener('input', function() { const hourlyRate = parseFloat(this.value) || 0; document.getElementById('defaultCalcDaily').textContent = (hourlyRate * 24).toFixed(2) + ' TFP'; document.getElementById('defaultCalcMonthly').textContent = (hourlyRate * 24 * 30).toFixed(0) + ' TFP'; document.getElementById('defaultCalcYearly').textContent = (hourlyRate * 24 * 365).toFixed(0) + ' TFP'; }); // Clean up when modal is hidden document.getElementById('defaultSliceEditModal').addEventListener('hidden.bs.modal', function() { this.remove(); }); } /** * Save default slice configuration */ async function saveDefaultSliceConfiguration(formatId) { console.log('🍰 Saving default slice configuration:', formatId); const priceHour = parseFloat(document.getElementById('defaultPriceHour').value) || 0; if (priceHour <= 0) { showNotification('Please enter a valid hourly rate', 'warning'); return; } const saveBtn = document.querySelector('#defaultSliceEditModal .btn-primary'); if (saveBtn) { saveBtn.disabled = true; saveBtn.innerHTML = 'Saving...'; } try { // For now, just show success message since we're not persisting default slice customizations // In a real implementation, this would save to user preferences showNotification(`Default slice pricing updated: ${priceHour} TFP/hour`, 'success'); // Close modal const modal = bootstrap.Modal.getInstance(document.getElementById('defaultSliceEditModal')); if (modal) { modal.hide(); } // Reload slice templates to show updated pricing loadSliceTemplates(); } catch (error) { console.error('🍰 Error saving default slice configuration:', error); showNotification('Failed to save configuration', 'error'); } finally { // Reset button state if (saveBtn) { saveBtn.disabled = false; saveBtn.innerHTML = 'Save Changes'; } } } // ============================================================================= // NODE GROUPS MANAGEMENT // ============================================================================= /** * Load node groups data */ async function loadNodeGroups() { console.log('πŸ“¦ Loading node groups'); // Show loading indicator const nodeGroupsTable = document.getElementById('node-groups-table'); if (nodeGroupsTable) { nodeGroupsTable.innerHTML = `
Loading...
Refreshing node groups... `; } try { const result = await window.apiJson('/api/dashboard/node-groups/api'); console.log('πŸ“¦ Node groups loaded:', result); // apiJson returns unwrapped data; support both array and { groups: [...] } const groups = Array.isArray(result?.groups) ? result.groups : (Array.isArray(result) ? result : []); updateNodeGroupsTable(groups); updateNodeGroupSelects(groups); } catch (error) { console.error('πŸ“¦ Error loading node groups:', error); showNodeGroupsError(); } } /** * Update node groups table */ function updateNodeGroupsTable(groups) { const tbody = document.getElementById('node-groups-table'); if (!tbody) return; tbody.innerHTML = ''; // Defensive coding: check if groups is defined and is an array if (!groups || !Array.isArray(groups) || groups.length === 0) { tbody.innerHTML = `

No node groups found

Default groups will be created automatically

`; return; } groups.forEach(groupData => { const group = groupData.group; const stats = groupData.stats; const row = document.createElement('tr'); // Group type badge let typeBadge = ''; if (group.group_type && group.group_type.Default) { const defaultType = group.group_type.Default; typeBadge = `Default - ${getDefaultGroupDisplayName(defaultType)}`; } else { typeBadge = `Custom`; } // Status badge let statusBadge = ''; if (stats) { if (stats.online_nodes === stats.total_nodes && stats.total_nodes > 0) { statusBadge = `All Online`; } else if (stats.online_nodes > 0) { statusBadge = `Partial`; } else if (stats.total_nodes > 0) { statusBadge = `All Offline`; } else { statusBadge = `Empty`; } } else { statusBadge = `Unknown`; } // Resources summary let resourcesSummary = 'No nodes'; if (stats && stats.total_nodes > 0) { resourcesSummary = `${stats.total_capacity.cpu_cores} cores, ${stats.total_capacity.memory_gb}GB RAM, ${Math.round(stats.total_capacity.storage_gb/1000)}TB storage`; } // Actions let actions = ''; const gt = group.group_type; const isCustom = (typeof gt === 'string' && gt === 'Custom') || (gt && typeof gt === 'object' && gt.Custom); if (isCustom) { actions = ` `; } else { actions = ` `; } row.innerHTML = ` ${group.name} ${group.description ? `
${group.description}` : ''} ${typeBadge} ${stats ? stats.total_nodes : 0} nodes ${stats && stats.online_nodes !== stats.total_nodes ? `
${stats.online_nodes} online` : ''} ${resourcesSummary} ${statusBadge} ${stats ? `${stats.average_uptime.toFixed(1)}%` : 'N/A'} ${actions} `; tbody.appendChild(row); }); } /** * Normalize groups from API entries into defaults and customs with stats */ function normalizeGroups(entries) { const defaults = []; const customs = []; (entries || []).forEach(entry => { const group = entry && (entry.group || entry); if (!group) return; const gt = group.group_type; const isDefault = (gt && typeof gt === 'object' && gt.Default) || (typeof gt === 'string' && ['Compute', 'Storage', 'AiGpu', 'AI/GPU', 'Default'].includes(gt)); const isCustom = (gt && typeof gt === 'object' && gt.Custom) || (typeof gt === 'string' && gt === 'Custom'); const bucket = isDefault ? defaults : (isCustom ? customs : null); if (bucket) bucket.push({ group, stats: entry.stats }); }); // Sort alphabetically within sections const byName = (a, b) => String(a.group?.name || '').localeCompare(String(b.group?.name || '')); defaults.sort(byName); customs.sort(byName); return { defaults, customs }; } /** * Update node group select dropdowns */ function updateNodeGroupSelects(groups) { // Exclude #existingGroup here; it is populated by loadExistingGroups() const selects = document.querySelectorAll('.node-group-select, select[name="node_group"], #nodeGroup, #nodeGroupSelect'); const { defaults, customs } = normalizeGroups(groups); selects.forEach(select => { const currentValue = select.value; select.innerHTML = ''; const append = ({ group, stats }) => { const total = stats && typeof stats.total_nodes === 'number' ? stats.total_nodes : (group.nodes ? group.nodes.length : undefined); const isCustom = group.group_type && ((typeof group.group_type === 'string' && group.group_type === 'Custom') || (typeof group.group_type === 'object' && group.group_type.Custom)); const option = document.createElement('option'); option.value = group.id; option.textContent = `${group.name}${isCustom ? ' (Custom)' : ''}${typeof total === 'number' ? ` (${total} nodes)` : ''}`; select.appendChild(option); }; defaults.forEach(append); customs.forEach(append); // Restore previous value if it still exists if (currentValue) { select.value = currentValue; } }); } /** * Show node groups loading error */ function showNodeGroupsError() { const tbody = document.getElementById('node-groups-table'); if (!tbody) return; tbody.innerHTML = `

Failed to load node groups

`; } /** * Create custom node group */ async function createCustomNodeGroup() { console.log('πŸ“¦ Creating custom node group'); const form = document.getElementById('createCustomGroupForm'); const formData = new FormData(form); const name = document.getElementById('customGroupName').value.trim(); const description = document.getElementById('customGroupDescription').value.trim(); const resourceOptimization = document.getElementById('resourceOptimization').value; const autoScaling = document.getElementById('autoScaling').checked; // Get selected slice formats const preferredSliceFormats = []; document.querySelectorAll('input[type="checkbox"][value]').forEach(checkbox => { if (checkbox.checked && ['basic', 'standard', 'performance'].includes(checkbox.value)) { preferredSliceFormats.push(checkbox.value); } }); if (!name) { showNotification('Group name is required', 'error'); return; } const createBtn = document.getElementById('createCustomGroupBtn'); createBtn.disabled = true; createBtn.innerHTML = 'Creating...'; try { const result = await window.apiJson('/api/dashboard/node-groups/custom', { method: 'POST', body: { name, description: description || null, resource_optimization: resourceOptimization, auto_scaling: autoScaling, preferred_slice_formats: preferredSliceFormats } }); console.log('πŸ“¦ Custom group created:', result); // apiJson resolved -> treat as success showNotification('Custom node group created successfully', 'success'); // Hide modal const modal = bootstrap.Modal.getInstance(document.getElementById('createCustomNodeGroupModal')); if (modal) { modal.hide(); } // Reset form form.reset(); // Reload node groups and refresh existing-group select loadNodeGroups(); try { loadExistingGroups(); } catch (_) {} } catch (error) { console.error('πŸ“¦ Error creating custom group:', error); showNotification('Failed to create custom group', 'error'); } finally { createBtn.disabled = false; createBtn.innerHTML = 'Create Custom Group'; } } /** * Delete custom node group */ async function deleteCustomNodeGroup(groupId) { console.log('πŸ“¦ Deleting custom node group:', groupId); // Remove existing modal if present const existing = document.getElementById('deleteGroupConfirmModal'); if (existing) existing.remove(); // Try to resolve group name for nicer messaging let groupName = 'this custom node group'; try { const result = await window.apiJson('/api/dashboard/node-groups/api', { cache: 'no-store' }); const entries = (result && Array.isArray(result.groups)) ? result.groups : (Array.isArray(result) ? result : []); const entry = entries.find(e => String((e && e.group && e.group.id) ? e.group.id : e && e.id) === String(groupId)); const group = entry && (entry.group || entry); if (group && group.name) groupName = `"${group.name}"`; } catch (_) {} const modalHtml = ` `; document.body.insertAdjacentHTML('beforeend', modalHtml); const modalEl = document.getElementById('deleteGroupConfirmModal'); const modal = bootstrap.Modal.getOrCreateInstance(modalEl); const confirmBtn = modalEl.querySelector('#confirmDeleteGroupButton'); const onHidden = () => { modalEl.removeEventListener('hidden.bs.modal', onHidden); modalEl.remove(); }; modalEl.addEventListener('hidden.bs.modal', onHidden); confirmBtn.addEventListener('click', async () => { confirmBtn.disabled = true; confirmBtn.innerHTML = 'Deleting...'; try { const result = await window.apiJson(`/api/dashboard/node-groups/${groupId}`, { method: 'DELETE' }); console.log('πŸ“¦ Custom group deleted:', result); showNotification('Custom node group deleted successfully', 'success'); modal.hide(); setTimeout(() => { loadNodeGroups(); try { loadExistingGroups(); } catch (_) {} loadResourceProviderData(); }, 100); } catch (error) { console.error('πŸ“¦ Error deleting custom group:', error); showNotification('Failed to delete custom group', 'error'); confirmBtn.disabled = false; confirmBtn.innerHTML = ' Delete Group'; } }, { once: true }); modal.show(); } /** * Manage node group (placeholder) */ async function manageNodeGroup(groupId) { console.log('πŸ“¦ Managing node group:', groupId); try { // Load group details from detailed API and find by id const result = await window.apiJson('/api/dashboard/node-groups/api', { cache: 'no-store' }); const entries = (result && Array.isArray(result.groups)) ? result.groups : (Array.isArray(result) ? result : []); const entry = entries.find(e => String((e && e.group && e.group.id) ? e.group.id : e && e.id) === String(groupId)); if (!entry) { showNotification('Failed to load group details', 'error'); return; } // Normalize shapes const group = entry.group || entry; const stats = entry.stats || {}; const nodes = Array.isArray(entry.nodes) ? entry.nodes : null; const nodeIds = Array.isArray(group.node_ids) ? group.node_ids : (nodes ? nodes.map(n => n.id) : []); // Build a detailed, read-only details modal dynamically const existing = document.getElementById('manageNodeGroupModal'); if (existing) existing.remove(); const modalHtml = ` `; document.body.insertAdjacentHTML('beforeend', modalHtml); const modalEl = document.getElementById('manageNodeGroupModal'); const modal = bootstrap.Modal.getOrCreateInstance(modalEl); modal.show(); } catch (error) { console.error('πŸ“¦ Error opening manage group modal:', error); showNotification('Failed to open manage group modal', 'error'); } } /** * Assign node to group */ async function assignNodeToGroup(nodeId, groupId) { console.log('πŸ“¦ Assigning node to group:', nodeId, groupId); try { const result = await window.apiJson('/api/dashboard/nodes/assign-group', { method: 'POST', body: { node_id: nodeId, group_id: groupId || null } }); console.log('πŸ“¦ Node assignment updated:', result); // apiJson resolved -> treat as success showNotification('Node assignment updated successfully', 'success'); // FARMER FIX: Add small delay to ensure backend processing is complete setTimeout(() => { loadResourceProviderData(); // Reload to show updated assignments loadNodeGroups(); // Reload to update group statistics }, 100); } catch (error) { console.error('πŸ“¦ Error updating node assignment:', error); showNotification('Failed to update node assignment', 'error'); } } /** * Get display name for default group types */ function getDefaultGroupDisplayName(defaultType) { switch (defaultType) { case 'Compute': return 'Compute'; case 'Storage': return 'Storage'; case 'AiGpu': return 'AI/GPU'; default: return defaultType; } } /** * View node group details */ async function viewNodeGroupDetails(groupId) { console.log('πŸ“¦ Viewing node group details:', groupId); try { // Load from list API and select the group client-side const result = await window.apiJson('/api/dashboard/node-groups', { cache: 'no-store' }); const groups = (result && Array.isArray(result.groups)) ? result.groups : (Array.isArray(result) ? result : []); const group = groups.find(g => String(g.id) === String(groupId)); if (!group) { showNotification('Failed to load group details', 'error'); return; } console.log('πŸ“¦ Group details loaded:', group); // Show group details (simple toast for now) const total = (group.stats && typeof group.stats.total_nodes === 'number') ? group.stats.total_nodes : (Array.isArray(group.nodes) ? group.nodes.length : 0); showNotification(`Group: ${group.name} - ${total} nodes`, 'info'); } catch (error) { console.error('πŸ“¦ Error loading group details:', error); showNotification('Failed to load group details', 'error'); } } /** * Edit node group */ async function editNodeGroup(groupId) { console.log('πŸ“¦ Editing node group:', groupId); // For now, just show a notification - you can implement a full edit modal showNotification('Group editing functionality coming soon', 'info'); } /** * Delete node group */ async function deleteNodeGroup(groupId) { console.log('πŸ“¦ Deleting node group:', groupId); if (!confirm('Are you sure you want to delete this custom node group? This action cannot be undone.')) { return; } try { await window.apiJson(`/api/dashboard/node-groups/${groupId}`, { method: 'DELETE' }); showNotification('Node group deleted successfully', 'success'); // Reload node groups loadNodeGroups(); } catch (error) { console.error('πŸ“¦ Error deleting node group:', error); showNotification('Failed to delete node group', 'error'); } } // Initialize custom group modal event listeners document.addEventListener('DOMContentLoaded', function() { const createCustomGroupBtn = document.getElementById('createCustomGroupBtn'); if (createCustomGroupBtn) { createCustomGroupBtn.addEventListener('click', createCustomNodeGroup); } // Global event delegation for dashboard actions document.addEventListener('click', function(event) { // Refresh slice calculations buttons if (event.target.matches('[data-action="slice.refresh"]') || event.target.closest('[data-action="slice.refresh"]')) { event.preventDefault(); try { refreshSliceCalculations(); } catch (e) { console.error(e); } return; } // Sync with Grid buttons if (event.target.matches('[data-action="grid.sync"]') || event.target.closest('[data-action="grid.sync"]')) { event.preventDefault(); try { syncWithGrid(); } catch (e) { console.error(e); } return; } // Node view details buttons if (event.target.matches('[data-action="node.view"]') || event.target.closest('[data-action="node.view"]')) { event.preventDefault(); const button = event.target.matches('[data-action="node.view"]') ? event.target : event.target.closest('[data-action="node.view"]'); const nodeId = button && button.getAttribute('data-node-id'); if (nodeId) { try { viewNodeDetails(nodeId); } catch (e) { console.error(e); } } return; } // Create Custom Group modal: programmatic fallback show to avoid darken-only issue if (event.target.matches('[data-bs-target="#createCustomNodeGroupModal"]') || event.target.closest('[data-bs-target="#createCustomNodeGroupModal"]')) { // Stop Bootstrap's own delegated handler to avoid double show/backdrop try { event.preventDefault(); event.stopPropagation(); if (event.stopImmediatePropagation) event.stopImmediatePropagation(); } catch (_) {} const modalEl = document.getElementById('createCustomNodeGroupModal'); if (modalEl && window.bootstrap && window.bootstrap.Modal) { try { // Ensure modal is attached directly to to avoid stacking context/overflow issues if (modalEl.parentElement !== document.body) { document.body.appendChild(modalEl); } const modal = bootstrap.Modal.getOrCreateInstance(modalEl); modal.show(); } catch (e) { console.error('Error showing Create Custom Group modal:', e); showNotification('Failed to open Create Custom Group modal', 'error'); } } else { console.error('Create Custom Group modal element or Bootstrap missing'); showNotification('Failed to open Create Custom Group modal', 'error'); } return; } }); }); // ============================================================================= // STAKING MANAGEMENT FUNCTIONS // ============================================================================= /** * Initialize staking management functionality */ function initializeStakingManagement() { console.log('πŸ›‘οΈ Initializing staking management'); // Initialize staking toggles initializeStakingToggles(); // Initialize staking validation initializeStakingValidation(); // Load user wallet balance loadWalletBalance(); // Update staking display updateStakingDisplay(); // Initialize staking API functions initializeStakingAPI(); } /** * Initialize staking API functions */ function initializeStakingAPI() { console.log('πŸ›‘οΈ Initializing staking API functions'); // Add event listeners for staking actions document.addEventListener('click', function(event) { // Handle stake button clicks if (event.target.matches('[data-action="stake-node"]') || event.target.closest('[data-action="stake-node"]')) { const button = event.target.matches('[data-action="stake-node"]') ? event.target : event.target.closest('[data-action="stake-node"]'); const nodeId = button.getAttribute('data-node-id'); if (nodeId) { showStakeNodeModal(nodeId); } } // Handle unstake button clicks if (event.target.matches('[data-action="unstake-node"]') || event.target.closest('[data-action="unstake-node"]')) { const button = event.target.matches('[data-action="unstake-node"]') ? event.target : event.target.closest('[data-action="unstake-node"]'); const nodeId = button.getAttribute('data-node-id'); if (nodeId) { unstakeFromNode(nodeId); } } // Handle update stake button clicks if (event.target.matches('[data-action="update-stake"]') || event.target.closest('[data-action="update-stake"]')) { const button = event.target.matches('[data-action="update-stake"]') ? event.target : event.target.closest('[data-action="update-stake"]'); const nodeId = button.getAttribute('data-node-id'); if (nodeId) { showUpdateStakeModal(nodeId); } } }); } /** * Initialize staking toggle functionality */ function initializeStakingToggles() { // Add node modal staking toggle const enableStaking = document.getElementById('enableStaking'); const stakingConfigSection = document.getElementById('stakingConfigSection'); const multiNodeStakingOptions = document.getElementById('multiNodeStakingOptions'); if (enableStaking && stakingConfigSection) { enableStaking.addEventListener('change', function() { if (this.checked) { stakingConfigSection.style.display = 'block'; loadWalletBalance(); // Show multi-node options if multiple nodes detected const nodePreviewContent = document.getElementById('nodePreviewContent'); if (nodePreviewContent) { const nodeCards = nodePreviewContent.querySelectorAll('.card'); if (nodeCards.length > 1 && multiNodeStakingOptions) { multiNodeStakingOptions.style.display = 'block'; } } } else { stakingConfigSection.style.display = 'none'; if (multiNodeStakingOptions) { multiNodeStakingOptions.style.display = 'none'; } } }); console.log('πŸ›‘οΈ Add Node modal staking toggle initialized'); } else { console.warn('πŸ›‘οΈ Add Node modal staking elements not found:', { enableStaking: !!enableStaking, stakingConfigSection: !!stakingConfigSection }); } // Edit node modal staking toggle const editStakingEnabled = document.getElementById('editStakingEnabled'); const editStakingConfigSection = document.getElementById('editStakingConfigSection'); if (editStakingEnabled && editStakingConfigSection) { editStakingEnabled.addEventListener('change', function() { if (this.checked) { editStakingConfigSection.style.display = 'block'; loadWalletBalance(); } else { editStakingConfigSection.style.display = 'none'; } }); } // Multi-node staking radio buttons const sameStakingForAllNodes = document.getElementById('sameStakingForAllNodes'); const individualStaking = document.getElementById('individualStaking'); if (sameStakingForAllNodes && individualStaking) { sameStakingForAllNodes.addEventListener('change', function() { if (this.checked) { cleanupIndividualStakingSections(); } }); individualStaking.addEventListener('change', function() { if (this.checked) { createIndividualStakingForms(); } }); } } /** * Initialize staking validation */ function initializeStakingValidation() { // Staking amount validation const stakingAmount = document.getElementById('stakingAmount'); if (stakingAmount) { stakingAmount.addEventListener('input', function() { validateStakingAmount(this); }); } // Edit staking amount validation const editStakingAmount = document.getElementById('editStakingAmount'); if (editStakingAmount) { editStakingAmount.addEventListener('input', function() { validateStakingAmount(this); }); } // Staking period change handler const stakingPeriod = document.getElementById('stakingPeriod'); if (stakingPeriod) { stakingPeriod.addEventListener('change', function() { updateStakingDiscountDisplay(this.value); }); } // Edit staking period change handler const editStakingPeriod = document.getElementById('editStakingPeriod'); if (editStakingPeriod) { editStakingPeriod.addEventListener('change', function() { updateStakingPeriodDisplay(this.value); }); } } /** * Validate staking amount against wallet balance */ function validateStakingAmount(input) { const amount = parseFloat(input.value) || 0; const availableBalance = parseFloat(window.userWalletBalance) || 0; // Remove existing validation classes input.classList.remove('is-valid', 'is-invalid'); // Find or create feedback element let feedback = input.parentNode.querySelector('.invalid-feedback'); if (!feedback) { feedback = document.createElement('div'); feedback.className = 'invalid-feedback'; input.parentNode.appendChild(feedback); } if (amount <= 0) { input.classList.add('is-invalid'); feedback.textContent = 'Staking amount must be greater than 0'; return false; } else if (amount > availableBalance) { input.classList.add('is-invalid'); feedback.textContent = `Insufficient balance. Available: ${availableBalance} TFP`; return false; } else { input.classList.add('is-valid'); feedback.textContent = ''; return true; } } /** * Update staking period display (removed discount functionality) */ function updateStakingPeriodDisplay(period) { // Staking is now only for slashing protection, no discounts console.log(`πŸ›‘οΈ Staking period set to ${period} months for slashing protection`); return period; } /** * Load user wallet balance */ async function loadWalletBalance() { try { const data = await window.apiJson('/api/dashboard/user-data'); // Fix: Access wallet balance from the correct nested structure const walletBalance = data.wallet_summary?.balance || data.wallet_balance || 0; // Store globally for validation window.userWalletBalance = walletBalance; // Update balance displays in staking modals const availableBalanceElements = document.querySelectorAll('#availableBalance, #editAvailableBalance'); availableBalanceElements.forEach(el => { el.textContent = walletBalance; }); // Update main dashboard wallet balance const walletBalanceElement = document.getElementById('wallet-balance'); if (walletBalanceElement) { walletBalanceElement.textContent = walletBalance; } console.log('πŸ’° Wallet balance loaded:', walletBalance, 'TFP'); } catch (error) { console.error('❌ Error loading wallet balance:', error); } } /** * Validate staking amount input */ function validateStakingAmount(input) { const amount = parseFloat(input.value); const walletBalance = window.userWalletBalance || 0; const nodeData = window.currentEditingNode; const currentStaked = nodeData?.staking_options?.staked_amount || 0; // Clear previous validation input.classList.remove('is-invalid', 'is-valid'); // Check if amount is valid number if (isNaN(amount) || amount <= 0) { input.classList.add('is-invalid'); showValidationError(input, 'Please enter a valid staking amount greater than 0'); return false; } // Check minimum staking amount (1 TFP) if (amount < 1) { input.classList.add('is-invalid'); showValidationError(input, 'Minimum staking amount is 1 TFP'); return false; } // Check if user has sufficient balance for additional staking const additionalStakeNeeded = amount - currentStaked; if (additionalStakeNeeded > 0 && additionalStakeNeeded > walletBalance) { input.classList.add('is-invalid'); showValidationError(input, `Insufficient balance. Available: ${walletBalance} TFP, Required: ${additionalStakeNeeded} TFP`); return false; } // Validation passed input.classList.add('is-valid'); clearValidationError(input); return true; } /** * Show validation error for input field */ function showValidationError(input, message) { // Remove existing error message clearValidationError(input); // Create error message element const errorDiv = document.createElement('div'); errorDiv.className = 'invalid-feedback'; errorDiv.textContent = message; errorDiv.id = input.id + '_error'; // Insert after input input.parentNode.insertBefore(errorDiv, input.nextSibling); } /** * Clear validation error for input field */ function clearValidationError(input) { const errorElement = document.getElementById(input.id + '_error'); if (errorElement) { errorElement.remove(); } } /** * Update staking display in dashboard */ async function updateStakingDisplay() { try { const data = await window.apiJson('/api/dashboard/resource_provider-data'); const nodes = data.nodes || []; // Calculate staking statistics let totalStaked = 0; let stakedNodes = 0; let nextUnlockDate = null; let averageDiscount = 0; // Initialize averageDiscount to prevent undefined error nodes.forEach(node => { if (node.staking_options && node.staking_options.staking_enabled) { totalStaked += parseFloat(node.staking_options.staked_amount) || 0; stakedNodes++; // Calculate discount if available if (node.staking_options.discount_percentage) { averageDiscount += parseFloat(node.staking_options.discount_percentage) || 0; } // Find earliest unlock date if (node.staking_options.staking_start_date) { const endDate = new Date(node.staking_options.staking_start_date); endDate.setMonth(endDate.getMonth() + (node.staking_options.staking_period_months || 12)); if (!nextUnlockDate || endDate < nextUnlockDate) { nextUnlockDate = endDate; } } } }); // Calculate average discount if (stakedNodes > 0) { averageDiscount = averageDiscount / stakedNodes; // Update staked nodes count (replaces discount display) const stakedNodesElement = document.getElementById('staked-nodes-count'); if (stakedNodesElement) { stakedNodesElement.textContent = stakedNodes; } // Legacy elements for backward compatibility const legacyTotalStakedElement = document.getElementById('total-staked-amount'); if (legacyTotalStakedElement) { legacyTotalStakedElement.textContent = `${totalStaked} TFP`; } const stakingDiscountElement = document.getElementById('staking-discount'); if (stakingDiscountElement) { stakingDiscountElement.textContent = `Staking Discount: ${averageDiscount.toFixed(1)}%`; } const stakingNodesElement = document.getElementById('staking-nodes'); if (stakingNodesElement) { stakingNodesElement.textContent = `Staked Nodes: ${stakedNodes}`; } const nextUnlockElement = document.getElementById('next-unlock'); if (nextUnlockElement) { if (nextUnlockDate) { nextUnlockElement.textContent = `Next Unlock: ${nextUnlockDate.toLocaleDateString()}`; } else { nextUnlockElement.textContent = 'Next Unlock: -'; } } console.log('πŸ›‘οΈ Staking display updated:', { totalStaked, stakedNodes, averageDiscount: averageDiscount.toFixed(1) + '%' }); } } catch (error) { console.error('❌ Error updating staking display:', error); } } /** * Create individual staking forms for multiple nodes */ function createIndividualStakingForms() { console.log('πŸ›‘οΈ Creating individual staking forms'); // Clean up existing forms first cleanupIndividualStakingSections(); // Get nodes from preview const nodePreviewContent = document.getElementById('nodePreviewContent'); if (!nodePreviewContent) { console.warn('πŸ›‘οΈ No node preview content found'); return; } const nodeCards = nodePreviewContent.querySelectorAll('.card'); if (nodeCards.length === 0) { console.warn('πŸ›‘οΈ No nodes found in preview'); return; } // Find the staking config section const stakingConfigSection = document.getElementById('stakingConfigSection'); if (!stakingConfigSection) { console.warn('πŸ›‘οΈ Staking config section not found'); return; } // Hide the general staking inputs const stakingAmount = document.getElementById('stakingAmount'); const stakingPeriod = document.getElementById('stakingPeriod'); if (stakingAmount && stakingPeriod) { stakingAmount.closest('.row').style.display = 'none'; } // Create individual staking table const tableContainer = document.createElement('div'); tableContainer.id = 'individualStakingContainer'; tableContainer.className = 'mt-3'; let tableHTML = `
Individual Node Staking: Set different staking amounts for each node.
`; // Add a row for each node nodeCards.forEach((card, index) => { // Extract node ID from card content const nodeIdMatch = card.textContent.match(/Grid Node (\d+)/); const nodeId = nodeIdMatch ? `grid_${nodeIdMatch[1]}` : `node_${index}`; const nodeName = nodeIdMatch ? `Node ${nodeIdMatch[1]}` : `Node ${index + 1}`; tableHTML += ` `; }); tableHTML += `
Node Staking Amount (TFP) Staking Period Discount
${nodeName} 10%
`; tableContainer.innerHTML = tableHTML; // Insert the table into the staking config section stakingConfigSection.querySelector('.card-body').appendChild(tableContainer); // Add event listeners for validation and discount updates nodeCards.forEach((card, index) => { const nodeIdMatch = card.textContent.match(/Grid Node (\d+)/); const nodeId = nodeIdMatch ? `grid_${nodeIdMatch[1]}` : `node_${index}`; const amountInput = document.getElementById(`staking_amount_${nodeId}`); const periodSelect = document.getElementById(`staking_period_${nodeId}`); const discountBadge = document.getElementById(`staking_discount_${nodeId}`); if (amountInput) { amountInput.addEventListener('input', function() { validateStakingAmount(this); }); } if (periodSelect && discountBadge) { periodSelect.addEventListener('change', function() { const discountMap = { '3': '5%', '6': '8%', '12': '10%', '24': '15%' }; discountBadge.textContent = discountMap[this.value] || '10%'; }); } }); console.log('πŸ›‘οΈ Individual staking forms created successfully'); } /** * Clean up individual staking sections */ function cleanupIndividualStakingSections() { const existingContainer = document.getElementById('individualStakingContainer'); if (existingContainer) { existingContainer.remove(); } // Show the general staking inputs again const stakingAmount = document.getElementById('stakingAmount'); if (stakingAmount) { const row = stakingAmount.closest('.row'); if (row) { row.style.display = ''; } } } /** * Get staking configuration data for form submission */ function getStakingConfiguration() { const enableStaking = document.getElementById('enableStaking'); if (!enableStaking || !enableStaking.checked) { return null; } // Check if individual staking is enabled const individualStaking = document.getElementById('individualStaking'); if (individualStaking && individualStaking.checked) { return getIndividualStakingConfiguration(); } else { return getGeneralStakingConfiguration(); } } /** * Get general staking configuration */ function getGeneralStakingConfiguration() { const stakingAmount = document.getElementById('stakingAmount'); const stakingPeriod = document.getElementById('stakingPeriod'); const earlyWithdrawalAllowed = document.getElementById('earlyWithdrawalAllowed'); if (!stakingAmount || !stakingPeriod) { return null; } const amount = parseFloat(stakingAmount.value) || 0; const period = parseInt(stakingPeriod.value) || 12; const earlyWithdrawal = earlyWithdrawalAllowed ? earlyWithdrawalAllowed.checked : true; if (amount <= 0) { return null; } return { staking_enabled: true, staked_amount: amount, staking_period_months: period, early_withdrawal_allowed: earlyWithdrawal, early_withdrawal_penalty_percent: 25.0 }; } /** * Get individual staking configuration */ function getIndividualStakingConfiguration() { const individualAmounts = document.querySelectorAll('.individual-staking-amount'); const stakingConfig = {}; individualAmounts.forEach(input => { const nodeId = input.dataset.nodeId; const amount = parseFloat(input.value) || 0; if (amount > 0) { const periodSelect = document.getElementById(`staking_period_${nodeId}`); const period = periodSelect ? parseInt(periodSelect.value) || 12 : 12; stakingConfig[nodeId] = { staking_enabled: true, staked_amount: amount, staking_period_months: period, early_withdrawal_allowed: true, early_withdrawal_penalty_percent: 25.0 }; } }); return Object.keys(stakingConfig).length > 0 ? stakingConfig : null; } /** * Handle manage staking button click */ function handleManageStaking() { console.log('πŸ›‘οΈ Manage staking clicked'); // This could open a dedicated staking management modal // For now, we'll just refresh the staking display updateStakingDisplay(); } // Add event listener for manage staking button document.addEventListener('DOMContentLoaded', function() { const manageStakingBtn = document.getElementById('manageStakingBtn'); if (manageStakingBtn) { manageStakingBtn.addEventListener('click', handleManageStaking); } }); // ============================================================================= // STAKING API FUNCTIONS // ============================================================================= /** * Show stake node modal */ function showStakeNodeModal(nodeId) { console.log('πŸ›‘οΈ Showing stake modal for node:', nodeId); // Find the node data const node = window.resourceProviderData?.nodes?.find(n => n.id === nodeId); if (!node) { showNotification('Node not found', 'error'); return; } // Check if node is already staked if (node.staking_options && node.staking_options.staking_enabled) { showNotification('Node is already staked. Use "Update Stake" to modify.', 'warning'); return; } // Create modal HTML const modalHTML = ` `; // Remove existing modal if any const existingModal = document.getElementById('stakeNodeModal'); if (existingModal) { existingModal.remove(); } // Add modal to DOM document.body.insertAdjacentHTML('beforeend', modalHTML); // Load wallet balance loadWalletBalanceForStaking(); // Show modal const modal = new bootstrap.Modal(document.getElementById('stakeNodeModal')); modal.show(); // Add event listener for confirm button document.getElementById('confirmStakeBtn').addEventListener('click', () => { stakeOnNode(nodeId, modal); }); } /** * Stake TFP on a node */ async function stakeOnNode(nodeId, modal) { console.log('πŸ›‘οΈ Staking TFP on node:', nodeId); const stakeAmount = document.getElementById('stakeAmount').value; const stakePeriod = document.getElementById('stakePeriod').value; const allowEarlyWithdrawal = document.getElementById('allowEarlyWithdrawal').checked; if (!stakeAmount || stakeAmount <= 0) { showNotification('Please enter a valid staking amount', 'error'); return; } // Disable button during request const confirmBtn = document.getElementById('confirmStakeBtn'); const originalText = confirmBtn.innerHTML; confirmBtn.disabled = true; confirmBtn.innerHTML = 'Staking...'; try { const result = await window.apiJson(`/api/dashboard/farm-nodes/${nodeId}/stake`, { method: 'POST', body: { staked_amount: stakeAmount, staking_period_months: parseInt(stakePeriod), early_withdrawal_allowed: allowEarlyWithdrawal } }); // apiJson resolved -> treat as success showNotification(`Successfully staked ${stakeAmount} TFP on node`, 'success'); modal.hide(); // Refresh resource_provider data to show updated staking await loadResourceProviderData(); // Update wallet balance display await loadWalletBalance(); // Update staking statistics display await updateStakingDisplay(); } catch (error) { console.error('πŸ›‘οΈ Error staking TFP:', error); showNotification(`Failed to stake TFP: ${error.message}`, 'error'); } finally { // Re-enable button confirmBtn.disabled = false; confirmBtn.innerHTML = originalText; } } /** * Unstake TFP from a node */ async function unstakeFromNode(nodeId) { console.log('πŸ›‘οΈ Unstaking TFP from node:', nodeId); // Find the node data const node = window.resourceProviderData?.nodes?.find(n => n.id === nodeId); if (!node || !node.staking_options || !node.staking_options.staking_enabled) { showNotification('Node is not currently staked', 'error'); return; } const stakedAmount = node.staking_options.staked_amount; const earlyWithdrawal = !node.staking_options.is_staking_period_ended; let confirmMessage = `Are you sure you want to unstake ${stakedAmount} TFP from this node?`; if (earlyWithdrawal) { confirmMessage += '\n\nNote: Early withdrawal penalty may apply.'; } if (!confirm(confirmMessage)) { return; } try { const result = await window.apiJson(`/api/dashboard/farm-nodes/${nodeId}/staking`, { method: 'PUT', body: { action: 'unstake' } }); // apiJson resolved -> treat as success const returnedAmount = result?.returned_amount || stakedAmount; showNotification(`Successfully unstaked ${returnedAmount} TFP from node`, 'success'); // Refresh resource_provider data to show updated staking await loadResourceProviderData(); // Update wallet balance display await loadWalletBalance(); } catch (error) { console.error('πŸ›‘οΈ Error unstaking TFP:', error); showNotification(`Failed to unstake TFP: ${error.message}`, 'error'); } } /** * Show update stake modal */ function showUpdateStakeModal(nodeId) { console.log('πŸ›‘οΈ Showing update stake modal for node:', nodeId); // Find the node data const node = window.resourceProviderData?.nodes?.find(n => n.id === nodeId); if (!node || !node.staking_options || !node.staking_options.staking_enabled) { showNotification('Node is not currently staked', 'error'); return; } const currentStake = node.staking_options.staked_amount; const currentPeriod = node.staking_options.staking_period_months; // Create modal HTML const modalHTML = ` `; // Remove existing modal if any const existingModal = document.getElementById('updateStakeModal'); if (existingModal) { existingModal.remove(); } // Add modal to DOM document.body.insertAdjacentHTML('beforeend', modalHTML); // Load wallet balance loadWalletBalanceForUpdate(); // Show modal const modal = new bootstrap.Modal(document.getElementById('updateStakeModal')); modal.show(); // Add event listener for confirm button document.getElementById('confirmUpdateStakeBtn').addEventListener('click', () => { updateNodeStaking(nodeId, modal); }); } /** * Update node staking */ async function updateNodeStaking(nodeId, modal) { console.log('πŸ›‘οΈ Updating staking for node:', nodeId); const newStakeAmount = document.getElementById('newStakeAmount').value; const newStakePeriod = document.getElementById('newStakePeriod').value; if (!newStakeAmount || newStakeAmount < 0) { showNotification('Please enter a valid staking amount', 'error'); return; } // Disable button during request const confirmBtn = document.getElementById('confirmUpdateStakeBtn'); const originalText = confirmBtn.innerHTML; confirmBtn.disabled = true; confirmBtn.innerHTML = 'Updating...'; try { const result = await window.apiJson(`/api/dashboard/farm-nodes/${nodeId}/staking`, { method: 'PUT', body: { action: 'update', staked_amount: newStakeAmount, staking_period_months: parseInt(newStakePeriod), early_withdrawal_allowed: true } }); // apiJson resolved -> treat as success showNotification(`Successfully updated staking to ${newStakeAmount} TFP`, 'success'); modal.hide(); // Refresh resource_provider data to show updated staking await loadResourceProviderData(); // Update wallet balance display await loadWalletBalance(); } catch (error) { console.error('πŸ›‘οΈ Error updating staking:', error); showNotification(`Failed to update staking: ${error.message}`, 'error'); } finally { // Re-enable button confirmBtn.disabled = false; confirmBtn.innerHTML = originalText; } } /** * Load wallet balance for staking modal */ async function loadWalletBalanceForStaking() { try { const data = await window.apiJson('/api/dashboard/user-data'); if (data && data.wallet_balance !== undefined) { const balanceElement = document.getElementById('availableBalanceStake'); if (balanceElement) { balanceElement.textContent = data.wallet_balance; } } } catch (error) { console.error('πŸ›‘οΈ Error loading wallet balance for staking:', error); const balanceElement = document.getElementById('availableBalanceStake'); if (balanceElement) { balanceElement.textContent = 'Error loading'; } } } /** * Load wallet balance for update modal */ async function loadWalletBalanceForUpdate() { try { const data = await window.apiJson('/api/dashboard/user-data'); if (data && data.wallet_balance !== undefined) { const balanceElement = document.getElementById('availableBalanceUpdate'); if (balanceElement) { balanceElement.textContent = data.wallet_balance; } } } catch (error) { console.error('πŸ›‘οΈ Error loading wallet balance for update:', error); const balanceElement = document.getElementById('availableBalanceUpdate'); if (balanceElement) { balanceElement.textContent = 'Error loading'; } } } /** * Format node location - show only country if city is "Unknown" */ function formatNodeLocation(nodeData) { if (!nodeData.grid_data) { return nodeData.location || 'Unknown'; } const city = nodeData.grid_data.city; const country = nodeData.grid_data.country; if (!city || city === 'Unknown') { return country || 'Unknown'; } return `${city}, ${country}`; } /** * Format location display elements on page load */ function formatLocationDisplays() { console.log('πŸ—ΊοΈ Formatting location displays'); // Format location spans in both My Nodes and Slice Management tables const locationElements = document.querySelectorAll('.node-location'); locationElements.forEach(element => { const city = element.getAttribute('data-city'); const country = element.getAttribute('data-country'); if (!city || city === 'Unknown') { element.textContent = country || 'Unknown'; } else { element.textContent = `${city}, ${country}`; } }); } /** * Format node group - show actual group name instead of "In Group" */ function formatNodeGroup(nodeGroupId) { if (!nodeGroupId) { return 'Individual'; } // Map group IDs to readable names const groupNames = { 'group_1': 'Compute', 'group_2': 'Storage', 'group_3': 'Network', 'single': 'Single', 'compute': 'Compute', 'storage': 'Storage', 'network': 'Network' }; return groupNames[nodeGroupId] || nodeGroupId; } /** * Get node slice pricing from available combinations or fallback */ function getNodeSlicePricing(nodeData) { // First try to get from available_combinations (most accurate) if (nodeData.available_combinations && nodeData.available_combinations.length > 0) { const baseSlice = nodeData.available_combinations.find(combo => combo.multiplier === 1); if (baseSlice) { return baseSlice.price_per_hour; } } // Fallback to slice_pricing if available if (nodeData.slice_pricing && nodeData.slice_pricing.base_price_per_hour) { return nodeData.slice_pricing.base_price_per_hour; } // Default fallback return '1.00'; } /** * Format staking information section */ function formatStakingInfo(nodeData) { if (!nodeData.staking_options || !nodeData.staking_options.staking_enabled) { return ''; } const staking = nodeData.staking_options; const stakedAmount = staking.staked_amount || 0; const stakingPeriod = staking.staking_period_months || 0; const startDate = staking.staking_start_date ? new Date(staking.staking_start_date).toLocaleDateString() : 'Unknown'; return `
Staking Information
Staked Amount:${stakedAmount} TFP
Staking Period:${stakingPeriod} months
Start Date:${startDate}
Early Withdrawal:${staking.early_withdrawal_allowed ? 'Allowed' : 'Not Allowed'}
${staking.early_withdrawal_allowed ? `
Early withdrawal penalty: ${staking.early_withdrawal_penalty_percent || 25}%
` : ''}
`; }