`;
// 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.
Node
Base Hourly Rate (TFP)
Daily Rate (TFP)
Monthly Rate (TFP)
Yearly Rate (TFP)
`;
// Add a row for each node using actual node IDs
nodeNames.forEach((nodeName, index) => {
const nodeId = nodeIds[index]; // Use the actual node ID
tableHTML += `
${nodeName}
`;
});
tableHTML += `
`;
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
Multi-Node Pricing Configuration
Multiple Nodes Detected: Choose how to apply full node rental pricing across your nodes.
`;
// 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 = `
`;
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 = `
`;
} 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 = `
`;
}
}
/**
* 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 = `