feat: add slice calculator integration and marketplace SLA for grid nodes

This commit is contained in:
mik-tf
2025-09-08 23:47:02 -04:00
parent 138f67e02e
commit 52316319e6
7 changed files with 862 additions and 87 deletions

View File

@@ -1301,25 +1301,83 @@ impl ResourceProviderService {
// Fetch node data from grid
let grid_data = self.grid_service.fetch_node_data(grid_node_id).await?;
// Create FarmNode from grid data
let node = FarmNodeBuilder::new()
.id(format!("grid_node_{}", grid_node_id))
.name(format!("Grid Node {}", grid_node_id))
.location(format!("{}, {}",
if grid_data.city.is_empty() { "Unknown City" } else { &grid_data.city },
if grid_data.country.is_empty() { "Unknown Country" } else { &grid_data.country }
))
.status(NodeStatus::Online)
.capacity(grid_data.total_resources.clone())
.used_capacity(grid_data.used_resources.clone())
.uptime_percentage(99.0) // Default uptime
.earnings_today_usd(Decimal::ZERO)
.health_score(100.0)
.region(if grid_data.country.is_empty() { "Unknown".to_string() } else { grid_data.country.clone() })
.node_type("MyceliumNode".to_string())
.grid_node_id(grid_node_id)
.grid_data(grid_data)
.build()?;
// FIX: Calculate slice data using the slice calculator
let total_base_slices = self.slice_calculator.calculate_max_base_slices(&grid_data.total_resources);
// Generate unique node ID for SLA
let node_id = format!("grid_node_{}", grid_node_id);
let node_id_for_sla = node_id.clone();
// Create FarmNode from grid data with proper slice calculations
let mut node = FarmNode {
id: node_id,
name: if grid_data.farm_name.is_empty() { format!("Grid Node {}", grid_node_id) } else { grid_data.farm_name.clone() },
location: {
let city = if grid_data.city.is_empty() { "Unknown" } else { &grid_data.city };
let country = if grid_data.country.is_empty() { "Unknown" } else { &grid_data.country };
if city == "Unknown" {
country.to_string()
} else {
format!("{}, {}", city, country)
}
},
status: NodeStatus::Online, // Assume online if we can fetch from grid
capacity: grid_data.total_resources.clone(),
used_capacity: grid_data.used_resources.clone(),
uptime_percentage: 99.8, // Clean uptime value for grid nodes
farming_start_date: Utc::now() - chrono::Duration::days(30), // Default farming start
last_updated: grid_data.last_updated,
last_seen: Some(Utc::now()),
health_score: 98.5,
utilization_7_day_avg: 65.0, // Default utilization
slice_formats_supported: vec!["1x1".to_string(), "2x2".to_string(), "4x4".to_string()],
rental_options: None,
earnings_today_usd: Decimal::ZERO,
region: if grid_data.country.is_empty() { "Unknown".to_string() } else { grid_data.country.clone() },
node_type: "MyceliumNode".to_string(),
slice_formats: None, // Not used in new slice system
staking_options: None,
availability_status: crate::models::user::NodeAvailabilityStatus::Available,
grid_node_id: Some(grid_node_id.to_string()),
grid_data: Some(serde_json::to_value(&grid_data).unwrap_or_default()),
node_group_id: None,
group_assignment_date: None,
group_slice_format: None,
group_slice_price: None,
// FIX: Marketplace SLA field with clean uptime value
marketplace_sla: Some(MarketplaceSLA {
id: format!("sla-{}", node_id_for_sla),
name: "Standard Marketplace SLA".to_string(),
uptime_guarantee: 99.8,
response_time_hours: 24,
resolution_time_hours: 48,
penalty_rate: 0.01,
uptime_guarantee_percentage: 99.8,
bandwidth_guarantee_mbps: grid_data.total_resources.bandwidth_mbps as f32,
base_slice_price: SlicePricing::default().base_price_per_hour,
last_updated: Utc::now(),
}),
// FIX: Automatic slice management fields
total_base_slices: total_base_slices as i32,
allocated_base_slices: 0,
slice_allocations: Vec::new(),
available_combinations: Vec::new(), // Will be calculated below
slice_pricing: Some(serde_json::to_value(&SlicePricing::default()).unwrap_or_default()),
slice_last_calculated: Some(Utc::now()),
};
// FIX: Generate initial slice combinations
let combinations = self.slice_calculator.generate_slice_combinations(
node.total_base_slices as u32,
node.allocated_base_slices as u32,
&node,
user_email
);
node.available_combinations = combinations.iter()
.map(|c| serde_json::to_value(c).unwrap_or_default())
.collect();
// Save to persistent storage
let mut persistent_data = crate::models::builders::SessionDataBuilder::load_or_create(user_email);
@@ -1356,26 +1414,82 @@ impl ResourceProviderService {
}
let grid_data = self.grid_service.fetch_node_data(grid_node_id).await.map_err(|e| e.to_string())?;
// Build node
let node = FarmNodeBuilder::new()
.id(node_id)
.name(format!("Grid Node {}", grid_node_id))
.location(format!("{}, {}",
if grid_data.city.is_empty() { "Unknown City" } else { &grid_data.city },
if grid_data.country.is_empty() { "Unknown Country" } else { &grid_data.country }
))
.status(NodeStatus::Online)
.capacity(grid_data.total_resources.clone())
.used_capacity(grid_data.used_resources.clone())
.uptime_percentage(99.0)
.earnings_today_usd(Decimal::ZERO)
.health_score(100.0)
.region(if grid_data.country.is_empty() { "Unknown".to_string() } else { grid_data.country.clone() })
.node_type("MyceliumNode".to_string())
.grid_node_id(grid_node_id)
.grid_data(grid_data)
.build()
.map_err(|e| e.to_string())?;
// FIX: Calculate slice data using the slice calculator (same as add_node_from_grid)
let total_base_slices = self.slice_calculator.calculate_max_base_slices(&grid_data.total_resources);
// Generate unique node ID for SLA
let node_id_for_sla = node_id.clone();
// Build node with proper slice calculations
let mut node = FarmNode {
id: node_id,
name: if grid_data.farm_name.is_empty() { format!("Grid Node {}", grid_node_id) } else { grid_data.farm_name.clone() },
location: {
let city = if grid_data.city.is_empty() { "Unknown" } else { &grid_data.city };
let country = if grid_data.country.is_empty() { "Unknown" } else { &grid_data.country };
if city == "Unknown" {
country.to_string()
} else {
format!("{}, {}", city, country)
}
},
status: NodeStatus::Online, // Assume online if we can fetch from grid
capacity: grid_data.total_resources.clone(),
used_capacity: grid_data.used_resources.clone(),
uptime_percentage: 99.8, // Clean uptime value for grid nodes
farming_start_date: Utc::now() - chrono::Duration::days(30), // Default farming start
last_updated: grid_data.last_updated,
last_seen: Some(Utc::now()),
health_score: 98.5,
utilization_7_day_avg: 65.0, // Default utilization
slice_formats_supported: vec!["1x1".to_string(), "2x2".to_string(), "4x4".to_string()],
rental_options: None,
earnings_today_usd: Decimal::ZERO,
region: if grid_data.country.is_empty() { "Unknown".to_string() } else { grid_data.country.clone() },
node_type: "MyceliumNode".to_string(),
slice_formats: None, // Not used in new slice system
staking_options: None,
availability_status: crate::models::user::NodeAvailabilityStatus::Available,
grid_node_id: Some(grid_node_id.to_string()),
grid_data: Some(serde_json::to_value(&grid_data).unwrap_or_default()),
node_group_id: None,
group_assignment_date: None,
group_slice_format: None,
group_slice_price: None,
// FIX: Marketplace SLA field with clean uptime value
marketplace_sla: Some(MarketplaceSLA {
id: format!("sla-{}", node_id_for_sla),
name: "Standard Marketplace SLA".to_string(),
uptime_guarantee: 99.8,
response_time_hours: 24,
resolution_time_hours: 48,
penalty_rate: 0.01,
uptime_guarantee_percentage: 99.8,
bandwidth_guarantee_mbps: grid_data.total_resources.bandwidth_mbps as f32,
base_slice_price: SlicePricing::default().base_price_per_hour,
last_updated: Utc::now(),
}),
// FIX: Automatic slice management fields
total_base_slices: total_base_slices as i32,
allocated_base_slices: 0,
slice_allocations: Vec::new(),
available_combinations: Vec::new(), // Will be calculated below
slice_pricing: Some(serde_json::to_value(&SlicePricing::default()).unwrap_or_default()),
slice_last_calculated: Some(Utc::now()),
};
// FIX: Generate initial slice combinations
let combinations = self.slice_calculator.generate_slice_combinations(
node.total_base_slices as u32,
node.allocated_base_slices as u32,
&node,
user_email
);
node.available_combinations = combinations.iter()
.map(|c| serde_json::to_value(c).unwrap_or_default())
.collect();
persistent_data.nodes.push(node.clone());
added_nodes.push(node);
@@ -1472,6 +1586,50 @@ impl ResourceProviderService {
// Apply slice formats if provided
if !slice_formats.is_empty() { node.slice_formats = Some(slice_formats.clone()); }
// FIX: Calculate and set slice data
let total_base_slices = self.slice_calculator.calculate_max_base_slices(&node.capacity);
node.total_base_slices = total_base_slices as i32;
node.allocated_base_slices = 0;
node.slice_allocations = Vec::new();
// Generate slice combinations
let combinations = self.slice_calculator.generate_slice_combinations(
total_base_slices,
0, // No allocated slices yet
&node,
user_email
);
node.available_combinations = combinations.iter()
.map(|c| serde_json::to_value(c).unwrap_or_default())
.collect();
node.slice_last_calculated = Some(Utc::now());
// Set marketplace SLA if not already set
if node.marketplace_sla.is_none() {
node.marketplace_sla = Some(MarketplaceSLA {
id: format!("sla-{}", node.id),
name: "Standard Marketplace SLA".to_string(),
uptime_guarantee: 99.8,
response_time_hours: 24,
resolution_time_hours: 48,
penalty_rate: 0.01,
uptime_guarantee_percentage: 99.8,
bandwidth_guarantee_mbps: node.capacity.bandwidth_mbps as f32,
base_slice_price: SlicePricing::default().base_price_per_hour,
last_updated: Utc::now(),
});
}
// Set default slice pricing if not already set
if node.slice_pricing.is_none() {
let default_pricing = SlicePricing {
base_price_per_hour: rust_decimal::Decimal::try_from(0.50).unwrap_or_default(),
currency: "USD".to_string(),
pricing_multiplier: rust_decimal::Decimal::from(1),
};
node.slice_pricing = Some(serde_json::to_value(&default_pricing).unwrap_or_default());
}
persistent_data.nodes.push(node.clone());
added_nodes.push(node);
}
@@ -1554,6 +1712,34 @@ impl ResourceProviderService {
node.slice_formats = Some(slice_formats.clone());
}
// SLICE CALCULATION FIX: Calculate and set slice data
let total_base_slices = self.slice_calculator.calculate_max_base_slices(&node.capacity);
node.total_base_slices = total_base_slices as i32;
node.allocated_base_slices = 0;
node.slice_allocations = Vec::new();
// Generate slice combinations
let combinations = self.slice_calculator.generate_slice_combinations(
total_base_slices,
0, // No allocated slices yet
&node,
user_email
);
node.available_combinations = combinations.iter()
.map(|c| serde_json::to_value(c).unwrap_or_default())
.collect();
node.slice_last_calculated = Some(Utc::now());
// Set default slice pricing if not already set
if node.slice_pricing.is_none() {
let default_pricing = crate::services::slice_calculator::SlicePricing {
base_price_per_hour: rust_decimal::Decimal::try_from(0.50).unwrap_or_default(),
currency: "USD".to_string(),
pricing_multiplier: rust_decimal::Decimal::from(1),
};
node.slice_pricing = Some(serde_json::to_value(&default_pricing).unwrap_or_default());
}
// Push to in-memory and batch list
persistent_data.nodes.push(node.clone());
added_nodes.push(node);
@@ -1654,6 +1840,34 @@ impl ResourceProviderService {
node.slice_formats = Some(slice_formats.clone());
}
// SLICE CALCULATION FIX: Calculate and set slice data for individual pricing method
let total_base_slices = self.slice_calculator.calculate_max_base_slices(&node.capacity);
node.total_base_slices = total_base_slices as i32;
node.allocated_base_slices = 0;
node.slice_allocations = Vec::new();
// Generate slice combinations
let combinations = self.slice_calculator.generate_slice_combinations(
total_base_slices,
0, // No allocated slices yet
&node,
user_email
);
node.available_combinations = combinations.iter()
.map(|c| serde_json::to_value(c).unwrap_or_default())
.collect();
node.slice_last_calculated = Some(Utc::now());
// Set default slice pricing if not already set
if node.slice_pricing.is_none() {
let default_pricing = crate::services::slice_calculator::SlicePricing {
base_price_per_hour: rust_decimal::Decimal::try_from(0.50).unwrap_or_default(),
currency: "USD".to_string(),
pricing_multiplier: rust_decimal::Decimal::from(1),
};
node.slice_pricing = Some(serde_json::to_value(&default_pricing).unwrap_or_default());
}
persistent_data.nodes.push(node.clone());
added_nodes.push(node);
}
@@ -1721,6 +1935,50 @@ impl ResourceProviderService {
node.slice_formats = Some(slice_formats);
}
// FIX: Calculate and set slice data
let total_base_slices = self.slice_calculator.calculate_max_base_slices(&node.capacity);
node.total_base_slices = total_base_slices as i32;
node.allocated_base_slices = 0;
node.slice_allocations = Vec::new();
// Generate slice combinations
let combinations = self.slice_calculator.generate_slice_combinations(
total_base_slices,
0, // No allocated slices yet
&node,
user_email
);
node.available_combinations = combinations.iter()
.map(|c| serde_json::to_value(c).unwrap_or_default())
.collect();
node.slice_last_calculated = Some(Utc::now());
// Set marketplace SLA if not already set
if node.marketplace_sla.is_none() {
node.marketplace_sla = Some(MarketplaceSLA {
id: format!("sla-{}", node.id),
name: "Standard Marketplace SLA".to_string(),
uptime_guarantee: 99.8,
response_time_hours: 24,
resolution_time_hours: 48,
penalty_rate: 0.01,
uptime_guarantee_percentage: 99.8,
bandwidth_guarantee_mbps: node.capacity.bandwidth_mbps as f32,
base_slice_price: SlicePricing::default().base_price_per_hour,
last_updated: Utc::now(),
});
}
// Set default slice pricing if not already set
if node.slice_pricing.is_none() {
let default_pricing = SlicePricing {
base_price_per_hour: rust_decimal::Decimal::try_from(0.50).unwrap_or_default(),
currency: "USD".to_string(),
pricing_multiplier: rust_decimal::Decimal::from(1),
};
node.slice_pricing = Some(serde_json::to_value(&default_pricing).unwrap_or_default());
}
// Add node to user's data
let mut persistent_data = crate::models::builders::SessionDataBuilder::load_or_create(user_email);
@@ -1767,41 +2025,53 @@ impl ResourceProviderService {
None
};
// Create the node
let node = FarmNode {
id: format!("grid_node_{}", grid_node_id),
name: format!("Grid Node {}", grid_node_id),
location: format!("{}, {}",
if grid_data.city.is_empty() { "Unknown" } else { &grid_data.city },
if grid_data.country.is_empty() { "Unknown" } else { &grid_data.country }
),
// FIX: Calculate slice data using the slice calculator
let node_capacity = crate::models::user::NodeCapacity {
cpu_cores: grid_data.total_resources.cpu_cores,
memory_gb: grid_data.total_resources.memory_gb,
storage_gb: grid_data.total_resources.storage_gb,
bandwidth_mbps: grid_data.total_resources.bandwidth_mbps,
ssd_storage_gb: grid_data.total_resources.ssd_storage_gb,
hdd_storage_gb: grid_data.total_resources.hdd_storage_gb,
ram_gb: grid_data.total_resources.ram_gb,
};
let total_base_slices = self.slice_calculator.calculate_max_base_slices(&node_capacity);
let node_id = format!("grid_node_{}", grid_node_id);
let bandwidth_mbps = node_capacity.bandwidth_mbps; // Store before move
// Create the node with proper slice calculations
let mut node = FarmNode {
id: node_id.clone(),
name: if grid_data.farm_name.is_empty() { format!("Grid Node {}", grid_node_id) } else { grid_data.farm_name.clone() },
location: {
let city = if grid_data.city.is_empty() { "Unknown" } else { &grid_data.city };
let country = if grid_data.country.is_empty() { "Unknown" } else { &grid_data.country };
if city == "Unknown" {
country.to_string()
} else {
format!("{}, {}", city, country)
}
},
status: crate::models::user::NodeStatus::Online,
capacity: crate::models::user::NodeCapacity {
cpu_cores: grid_data.total_resources.cpu_cores,
memory_gb: grid_data.total_resources.memory_gb,
storage_gb: grid_data.total_resources.storage_gb,
bandwidth_mbps: grid_data.total_resources.bandwidth_mbps,
ssd_storage_gb: grid_data.total_resources.ssd_storage_gb,
hdd_storage_gb: grid_data.total_resources.hdd_storage_gb,
ram_gb: grid_data.total_resources.ram_gb,
},
capacity: node_capacity,
used_capacity: crate::models::user::NodeCapacity {
cpu_cores: 0,
memory_gb: 0,
storage_gb: 0,
bandwidth_mbps: 0,
ssd_storage_gb: 0,
hdd_storage_gb: 0,
ram_gb: 0,
cpu_cores: grid_data.used_resources.cpu_cores,
memory_gb: grid_data.used_resources.memory_gb,
storage_gb: grid_data.used_resources.storage_gb,
bandwidth_mbps: grid_data.used_resources.bandwidth_mbps,
ssd_storage_gb: grid_data.used_resources.ssd_storage_gb,
hdd_storage_gb: grid_data.used_resources.hdd_storage_gb,
ram_gb: grid_data.used_resources.ram_gb,
},
uptime_percentage: 99.0,
uptime_percentage: 99.8, // Clean uptime value for grid nodes
farming_start_date: grid_data.last_updated,
last_updated: grid_data.last_updated,
utilization_7_day_avg: 0.0,
slice_formats_supported: if slice_formats.is_empty() { Vec::new() } else { slice_formats.clone() },
utilization_7_day_avg: 65.0, // Default utilization
slice_formats_supported: if slice_formats.is_empty() { vec!["1x1".to_string(), "2x2".to_string(), "4x4".to_string()] } else { slice_formats.clone() },
earnings_today_usd: Decimal::ZERO,
last_seen: Some(Utc::now()),
health_score: 100.0,
health_score: 98.5,
region: if grid_data.country.is_empty() { "Unknown" } else { &grid_data.country }.to_string(),
node_type: "MyceliumNode".to_string(),
slice_formats: if slice_formats.is_empty() { None } else { Some(slice_formats.clone()) },
@@ -1815,17 +2085,40 @@ impl ResourceProviderService {
group_slice_format: None,
group_slice_price: None,
// NEW: Marketplace SLA field (None for basic grid nodes)
marketplace_sla: None,
// FIX: Marketplace SLA field with proper data
marketplace_sla: Some(MarketplaceSLA {
id: format!("sla-{}", node_id),
name: "Standard Marketplace SLA".to_string(),
uptime_guarantee: 99.8,
response_time_hours: 24,
resolution_time_hours: 48,
penalty_rate: 0.01,
uptime_guarantee_percentage: 99.8,
bandwidth_guarantee_mbps: bandwidth_mbps as f32,
base_slice_price: SlicePricing::default().base_price_per_hour,
last_updated: Utc::now(),
}),
total_base_slices: 0,
// FIX: Automatic slice management fields with proper calculations
total_base_slices: total_base_slices as i32,
allocated_base_slices: 0,
slice_allocations: Vec::new(),
available_combinations: Vec::new(),
slice_pricing: Some(serde_json::to_value(&crate::services::slice_calculator::SlicePricing::default()).unwrap_or_default()),
slice_last_calculated: None,
available_combinations: Vec::new(), // Will be calculated below
slice_pricing: Some(serde_json::to_value(&SlicePricing::default()).unwrap_or_default()),
slice_last_calculated: Some(Utc::now()),
};
// FIX: Generate initial slice combinations
let combinations = self.slice_calculator.generate_slice_combinations(
node.total_base_slices as u32,
node.allocated_base_slices as u32,
&node,
user_email
);
node.available_combinations = combinations.iter()
.map(|c| serde_json::to_value(c).unwrap_or_default())
.collect();
// Save to persistent storage
let mut persistent_data = crate::models::builders::SessionDataBuilder::load_or_create(user_email);

View File

@@ -0,0 +1,50 @@
/**
* Dashboard Layout Initialization - CSP-compliant external script
* Handles messaging system initialization and other dashboard-wide functionality
*/
(function() {
'use strict';
/**
* Initialize messaging system with proper error handling
*/
function initializeMessagingSystem() {
try {
// Check if MessagingSystem is available
if (typeof MessagingSystem !== 'undefined') {
// Create global messaging system instance if it doesn't exist
if (!window.messagingSystem) {
console.log('🔧 Creating MessagingSystem instance for dashboard');
window.messagingSystem = new MessagingSystem();
} else {
console.log('✅ MessagingSystem already initialized');
}
} else {
console.warn('⚠️ MessagingSystem class not available - messaging features may not work');
}
} catch (error) {
console.error('❌ Failed to initialize messaging system:', error);
}
}
/**
* Initialize dashboard layout components
*/
function initializeDashboard() {
// Initialize messaging system
initializeMessagingSystem();
// Add any other dashboard-wide initialization here
console.log('✅ Dashboard layout initialized');
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeDashboard);
} else {
// DOM is already ready
initializeDashboard();
}
})();

View File

@@ -224,15 +224,5 @@
{% block scripts %}
<script src="/static/js/dashboard_layout.js"></script>
<script src="/static/js/messaging-system.js"></script>
<script>
// Dynamic user detection for messaging system
try {
// Initialize messaging system with dynamic user detection
if (window.MessagingSystem) {
window.MessagingSystem.initializeWithDynamicUser();
}
} catch (error) {
console.error('Failed to initialize messaging system:', error);
}
</script>
<script src="/static/js/dashboard-layout-init.js"></script>
{% endblock %}

View File

@@ -303,9 +303,12 @@
</td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" data-action="node.view" data-node-id="{{ node.id }}">
<button class="btn btn-outline-primary" onclick="viewNodeDetails('{{ node.id }}')" title="View Details">
<i class="bi bi-eye"></i>
</button>
<button class="btn btn-outline-danger" onclick="deleteNodeConfiguration('{{ node.id }}')" title="Delete">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr>