feat: backup current state of bare metal page before customization fix
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
|
||||
<!doctype html>
|
||||
<html dir="ltr" lang="en-US" prefix="og: https://ogp.me/ns#">
|
||||
<head>
|
||||
@@ -3295,7 +3296,7 @@ var wpcf7 = {
|
||||
ram: 'Base: 128GB',
|
||||
ramSpeed: '3200 MT/s',
|
||||
ramChannels: '8-channel',
|
||||
basePrice: 350,
|
||||
basePrice: 490,
|
||||
currency: '€',
|
||||
configUrl: 'https://portal.dedicatednodes.io/index.php?rp=/store/bare-metal-servers/amd-epyc-7443p-1',
|
||||
setupTime: 'Instant',
|
||||
@@ -3311,6 +3312,12 @@ var wpcf7 = {
|
||||
{ "id": "113", "text": "New York €35,00 EUR", "price": 35 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "2", "label": "Network", "type": "network",
|
||||
"values": [
|
||||
{ "id": "37", "text": "1Gbps Unmetered", "price": 0 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "48", "label": "RAM", "type": "ram",
|
||||
"values": [
|
||||
@@ -3510,7 +3517,9 @@ var wpcf7 = {
|
||||
if (doc.querySelector('input[name^="configoption"]') || doc.querySelector('select[name^="configoption"]')) {
|
||||
console.log("Direct config page detected");
|
||||
const options = this.parseConfigPage(doc, configUrl);
|
||||
if (options.length > 0) return { options, url: configUrl };
|
||||
if (options.length > 0) {
|
||||
return { options: this.verifyScrapedData(options, selectedServer), url: configUrl };
|
||||
}
|
||||
}
|
||||
|
||||
const configureBtn = doc.querySelector('a[href*="cart.php?a=add"]');
|
||||
@@ -3560,11 +3569,14 @@ var wpcf7 = {
|
||||
const isGen4Slot = slotIndex < gen4Count;
|
||||
|
||||
if (isGen5Slot) {
|
||||
// Filter to ONLY keep Gen5/Enterprise drives
|
||||
opt.values = opt.values.filter(v => {
|
||||
const t = v.text.toLowerCase();
|
||||
return t.includes('gen5') || t.includes('enterprise') || t.includes('solidigm') || t.includes('cm7');
|
||||
});
|
||||
// Filter to ONLY keep Gen5/Enterprise drives (Only for Custom builds)
|
||||
// For Instant servers, we keep everything to ensure we can match the specific plan
|
||||
if (!selectedServer || !selectedServer.id.startsWith('inst')) {
|
||||
opt.values = opt.values.filter(v => {
|
||||
const t = v.text.toLowerCase();
|
||||
return t.includes('gen5') || t.includes('enterprise') || t.includes('solidigm') || t.includes('cm7');
|
||||
});
|
||||
}
|
||||
// Ensure we have at least "None" or a default
|
||||
if (opt.values.length === 0) {
|
||||
opt.values.push({ id: "9999", text: "-", price: 0 });
|
||||
@@ -3579,16 +3591,12 @@ var wpcf7 = {
|
||||
const splitB = selectedServer.fallbackSplit[1];
|
||||
|
||||
if (slotIndex < splitA) {
|
||||
// Group A: Keep as is (or use Template A)
|
||||
// Just ensure they have consistent values
|
||||
// Group A: Keep as is
|
||||
} else {
|
||||
// Group B: Force a slight variation so they don't merge with Group A
|
||||
// We can simulate this by filtering differently OR appending a hidden marker
|
||||
opt.values = opt.values.filter(v => {
|
||||
// Maybe remove the cheapest option to make it distinct?
|
||||
// Or just keep "None" and "Samsung"
|
||||
return true;
|
||||
});
|
||||
// Group B: Force a slight variation
|
||||
if (!selectedServer || !selectedServer.id.startsWith('inst')) {
|
||||
opt.values = opt.values.filter(v => true); // No-op but consistent structure
|
||||
}
|
||||
// HACK: Modify price of first item +0.01 to force signature mismatch from Group A
|
||||
if(opt.values.length > 1) {
|
||||
opt.values[1].price += 0.01;
|
||||
@@ -3596,11 +3604,13 @@ var wpcf7 = {
|
||||
}
|
||||
}
|
||||
|
||||
// Filter to ONLY keep Gen4/Consumer drives (optional, but cleaner)
|
||||
opt.values = opt.values.filter(v => {
|
||||
const t = v.text.toLowerCase();
|
||||
return t.includes('gen4') || t.includes('crucial') || t.includes('samsung') || t.includes('-') || t.includes('none');
|
||||
});
|
||||
// Filter to ONLY keep Gen4/Consumer drives (Only for Custom builds)
|
||||
if (!selectedServer || !selectedServer.id.startsWith('inst')) {
|
||||
opt.values = opt.values.filter(v => {
|
||||
const t = v.text.toLowerCase();
|
||||
return t.includes('gen4') || t.includes('crucial') || t.includes('samsung') || t.includes('-') || t.includes('none');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3787,6 +3797,36 @@ var wpcf7 = {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
verifyScrapedData(options, server) {
|
||||
if (!server) return options;
|
||||
|
||||
// Rule 1: AMD EPYC 7443P (custom-4) must not have 10Gbps
|
||||
// User explicitly stated 7443P is 1Gbps only.
|
||||
if (server.id === 'custom-4') {
|
||||
const netOpt = options.find(o => o.type === 'network');
|
||||
if (netOpt) {
|
||||
// Check if we have 10Gbps options that shouldn't be there
|
||||
const has10G = netOpt.values.some(v => v.text.includes('10Gbps'));
|
||||
if (has10G) {
|
||||
console.warn("Verification: Detected 10Gbps on 7443P. Enforcing 1Gbps limit.");
|
||||
// Force correct 1Gbps option
|
||||
// We use a generic ID if we don't know the real one, or try to find a 1Gbps option in the list
|
||||
const oneGig = netOpt.values.find(v => v.text.includes('1Gbps'));
|
||||
|
||||
if (oneGig) {
|
||||
netOpt.values = [oneGig];
|
||||
} else {
|
||||
// Inject default 1Gbps if not found
|
||||
netOpt.values = [
|
||||
{ "id": "37", "text": "1Gbps Unmetered", "price": 0 }
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
const scraper = new WHMCSScraper();
|
||||
@@ -3898,6 +3938,121 @@ var wpcf7 = {
|
||||
processQueue();
|
||||
}
|
||||
|
||||
// Fetch Instant Prices from Solana Nodes Page
|
||||
async function fetchInstantPrices() {
|
||||
const url = 'https://www.dedicatednodes.io/solana-nodes/';
|
||||
const proxies = [
|
||||
'https://corsproxy.io/?',
|
||||
'https://api.allorigins.win/raw?url='
|
||||
];
|
||||
|
||||
console.log("Fetching instant prices from:", url);
|
||||
|
||||
for (const proxy of proxies) {
|
||||
try {
|
||||
const response = await fetch(proxy + encodeURIComponent(url));
|
||||
if (!response.ok) continue;
|
||||
|
||||
const html = await response.text();
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
|
||||
// Strategy: Find all pricing cards/rows
|
||||
// We look for elements that contain a price symbol and CPU name
|
||||
// This is a generic scraper that tries to map content to our instantServers
|
||||
|
||||
const potentialMatches = [];
|
||||
|
||||
// 1. Try to find common card structures
|
||||
const cards = doc.querySelectorAll('.pricing-table, .price-card, .package, .elementor-widget-price-list, .wp-block-column');
|
||||
|
||||
cards.forEach(card => {
|
||||
const text = card.innerText;
|
||||
const priceMatch = text.match(/([€$£])\s?([0-9,]+)/);
|
||||
if (priceMatch) {
|
||||
potentialMatches.push({
|
||||
element: card,
|
||||
text: text.toLowerCase(),
|
||||
price: parseFloat(priceMatch[2].replace(/,/g, '')),
|
||||
currency: priceMatch[1]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// If no specific cards found, try searching body text blocks?
|
||||
// Better to stick to structural elements if possible.
|
||||
// If potentialMatches is empty, fall back to a more aggressive search
|
||||
if (potentialMatches.length === 0) {
|
||||
// ... implementation detail if needed
|
||||
}
|
||||
|
||||
let updates = 0;
|
||||
|
||||
instantServers.forEach(server => {
|
||||
// Scoring System
|
||||
let bestMatch = null;
|
||||
let highestScore = 0;
|
||||
|
||||
const cpuKeywords = server.cpu.toLowerCase().split(' ').filter(w => w.length > 3);
|
||||
const ramKeywords = server.ram.toLowerCase().match(/(\d+)gb/);
|
||||
const ramVal = ramKeywords ? ramKeywords[1] : null;
|
||||
|
||||
potentialMatches.forEach(match => {
|
||||
let score = 0;
|
||||
|
||||
// CPU Match
|
||||
if (match.text.includes(server.cpu.toLowerCase())) score += 20;
|
||||
else {
|
||||
// Partial CPU match
|
||||
const hitCount = cpuKeywords.filter(k => match.text.includes(k)).length;
|
||||
if (hitCount >= 2) score += hitCount * 3;
|
||||
}
|
||||
|
||||
// RAM Match
|
||||
if (ramVal && match.text.includes(ramVal)) score += 10;
|
||||
|
||||
// Storage Match (Loose)
|
||||
// Check for "NVMe" and maybe capacity
|
||||
if (server.storage) {
|
||||
const storageParts = server.storage.split('+');
|
||||
if (storageParts.length > 0) {
|
||||
const firstPart = storageParts[0].toLowerCase().replace('x', '').trim(); // e.g. "crucial t705"
|
||||
if (match.text.includes(firstPart)) score += 5;
|
||||
}
|
||||
}
|
||||
|
||||
if (score > highestScore && score > 15) { // Threshold
|
||||
highestScore = score;
|
||||
bestMatch = match;
|
||||
}
|
||||
});
|
||||
|
||||
if (bestMatch) {
|
||||
// console.log(`Updated price for ${server.id}: ${server.price} -> ${bestMatch.price}`);
|
||||
server.price = bestMatch.price;
|
||||
updates++;
|
||||
|
||||
// Update UI if this server is currently rendered
|
||||
const priceEl = document.querySelector(`.server-option.instant[data-id="${server.id}"] .server-price-display`);
|
||||
if (priceEl) {
|
||||
priceEl.innerHTML = `${server.currency}${server.price}<small>/mo</small>`;
|
||||
}
|
||||
|
||||
// If selected, update summary
|
||||
if (selectedServer && selectedServer.id === server.id) {
|
||||
updateSummary();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Updated prices for ${updates} instant servers.`);
|
||||
return; // Success
|
||||
|
||||
} catch (e) {
|
||||
console.warn("Failed to fetch instant prices via proxy:", proxy, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to update just the OOS badge on a specific card
|
||||
function updateServerCardOOS(server) {
|
||||
const card = document.querySelector(`.server-option.instant[data-id="${server.id}"]`);
|
||||
@@ -3992,24 +4147,144 @@ var wpcf7 = {
|
||||
|
||||
try {
|
||||
let scrapeUrl = selectedServer.orderUrl;
|
||||
// Parse URL params to find pre-selected IDs
|
||||
const urlObj = new URL(selectedServer.orderUrl);
|
||||
const preselected = {};
|
||||
urlObj.searchParams.forEach((val, key) => {
|
||||
if (key.startsWith('configoption[')) {
|
||||
const idMatch = key.match(/\[(\d+)\]/);
|
||||
if (idMatch) preselected[idMatch[1]] = val;
|
||||
}
|
||||
});
|
||||
|
||||
if (scrapeUrl.includes('&configoption')) {
|
||||
scrapeUrl = scrapeUrl.split('&configoption')[0];
|
||||
}
|
||||
|
||||
const preselected = {};
|
||||
const urlParams = new URLSearchParams(selectedServer.orderUrl);
|
||||
for (const [key, value] of urlParams.entries()) {
|
||||
if (key.startsWith('configoption[')) {
|
||||
const idMatch = key.match(/\[(\d+)\]/);
|
||||
if (idMatch) {
|
||||
preselected[idMatch[1]] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get Options
|
||||
const optionsData = await scraper.getProductConfig(scrapeUrl, selectedServer);
|
||||
|
||||
if (optionsData && optionsData.options.length > 0) {
|
||||
renderDynamicOptions(optionsData.options, true, preselected);
|
||||
// DEEP CLONE options to avoid modifying the static reference
|
||||
const options = JSON.parse(JSON.stringify(optionsData.options));
|
||||
|
||||
// 1. Build ID->Text Map from all known sources
|
||||
const idToTextMap = {};
|
||||
if (typeof FALLBACK_CONFIG_OPTIONS !== 'undefined') {
|
||||
FALLBACK_CONFIG_OPTIONS.forEach(opt => opt.values.forEach(v => idToTextMap[v.id] = v.text));
|
||||
}
|
||||
if (typeof customServers !== 'undefined') {
|
||||
customServers.forEach(cs => {
|
||||
if(cs.specificFallback) {
|
||||
cs.specificFallback.forEach(opt => opt.values.forEach(v => idToTextMap[v.id] = v.text));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Parse Storage Requirements
|
||||
let storageRequirements = {};
|
||||
if (selectedServer.storage) {
|
||||
const parts = selectedServer.storage.split('+');
|
||||
parts.forEach(part => {
|
||||
const match = part.trim().match(/^(\d+)x\s+(.+)$/);
|
||||
if (match) {
|
||||
storageRequirements[match[2].trim().toLowerCase()] = parseInt(match[1]);
|
||||
} else if (part.trim().length > 0) {
|
||||
storageRequirements[part.trim().toLowerCase()] = 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ADJUST PRICES relative to the Included Options
|
||||
options.forEach(opt => {
|
||||
let selectedValId = null;
|
||||
let urlId = preselected[opt.id]; // Original URL ID
|
||||
|
||||
// 1. PRIORITY: Plan Requirements (Text Description)
|
||||
// We try to fulfill the written plan specs first.
|
||||
if (opt.type === 'storage') {
|
||||
for (const [reqName, reqQty] of Object.entries(storageRequirements)) {
|
||||
if (reqQty > 0) {
|
||||
const match = opt.values.find(v => {
|
||||
const vText = v.text.toLowerCase();
|
||||
// Simple Robust Fuzzy Match
|
||||
// Clean requirements (remove "nvme", "gen4" etc to focus on Brand + Capacity)
|
||||
const cleanReq = reqName.toLowerCase().replace(/nvme/g, '').replace(/gen[45]/g, '').replace(/\s+/g, ' ').trim();
|
||||
const cleanVal = vText.replace(/\s?\(.*?\)/, '').replace(/[€$£].*/, '').trim();
|
||||
|
||||
// Check if Value contains Requirement (e.g. "Crucial 1TB" contains "crucial 1tb")
|
||||
// OR if Requirement contains Value (e.g. "2x Crucial 1TB" contains "Crucial 1TB")
|
||||
return cleanVal.includes(cleanReq) || cleanReq.includes(cleanVal);
|
||||
});
|
||||
|
||||
if (match) {
|
||||
selectedValId = match.id;
|
||||
storageRequirements[reqName]--;
|
||||
// console.log(`Slot ${opt.label} matched requirement: ${reqName}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. FALLBACK: URL ID (if valid)
|
||||
// If we didn't match a requirement (or ran out), use the URL's choice.
|
||||
if (!selectedValId && urlId) {
|
||||
if (opt.values.find(v => v.id === urlId)) {
|
||||
selectedValId = urlId;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. FALLBACK: Stale ID Map
|
||||
if (!selectedValId && urlId) {
|
||||
const originalText = idToTextMap[urlId];
|
||||
if (originalText) {
|
||||
const cleanTarget = originalText.replace(/\s?[€$£].*/, '').trim().toLowerCase();
|
||||
const match = opt.values.find(v => v.text.toLowerCase().includes(cleanTarget));
|
||||
if (match) selectedValId = match.id;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. FALLBACK: Generic Text Match (Non-Storage)
|
||||
if (!selectedValId && opt.type !== 'storage') {
|
||||
if (opt.type === 'ram') {
|
||||
const ramMatch = selectedServer.ram.match(/(\d+)\s*GB/i);
|
||||
const ramVal = ramMatch ? ramMatch[1] : selectedServer.ram.split(' ')[0];
|
||||
const match = opt.values.find(v => v.text.toUpperCase().includes(ramVal.toUpperCase()));
|
||||
if(match) selectedValId = match.id;
|
||||
} else if (opt.type === 'network') {
|
||||
const match = opt.values.find(v => v.text.toLowerCase().includes(selectedServer.network.toLowerCase()));
|
||||
if(match) selectedValId = match.id;
|
||||
} else if (opt.type === 'location') {
|
||||
const match = opt.values.find(v => v.text.includes(selectedServer.location));
|
||||
if(match) selectedValId = match.id;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. FALLBACK: Default (Prefer None)
|
||||
if (!selectedValId && opt.values.length > 0) {
|
||||
if (opt.type === 'storage') {
|
||||
const noneOpt = opt.values.find(v => v.text.trim() === '-' || v.text.toLowerCase().includes('none') || v.text.toLowerCase().includes('no hard drive'));
|
||||
selectedValId = noneOpt ? noneOpt.id : opt.values[0].id;
|
||||
} else {
|
||||
selectedValId = opt.values[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedValId) {
|
||||
preselected[opt.id] = selectedValId;
|
||||
const selectedVal = opt.values.find(v => v.id === selectedValId);
|
||||
if (selectedVal && selectedVal.price > 0) {
|
||||
const offset = selectedVal.price;
|
||||
opt.values.forEach(v => {
|
||||
v.price = parseFloat((v.price - offset).toFixed(2));
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
renderDynamicOptions(options, true, preselected);
|
||||
|
||||
} else {
|
||||
renderFallbackOptions(container);
|
||||
const warning = document.createElement('div');
|
||||
@@ -4018,6 +4293,7 @@ var wpcf7 = {
|
||||
container.insertBefore(warning, container.firstChild);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Instant Customization Error:", err);
|
||||
renderFallbackOptions(container);
|
||||
} finally {
|
||||
loader.style.display = 'none';
|
||||
@@ -4340,23 +4616,24 @@ var wpcf7 = {
|
||||
|
||||
window.calculateStorageTotal = function() {
|
||||
let total = 0;
|
||||
let summaryParts = [];
|
||||
let summaryHtml = '';
|
||||
|
||||
Object.values(storageSelection).forEach(item => {
|
||||
if(item.qty > 0) {
|
||||
total += item.qty * item.price;
|
||||
summaryParts.push(`${item.qty}x ${item.name.replace('NVMe Gen4', '').replace('NVMe Gen5 Ent', '').trim()}`);
|
||||
const cleanName = item.name.replace('NVMe Gen4', '').replace('NVMe Gen5 Ent', '').trim();
|
||||
summaryHtml += `<span class="spec-pill" style="margin-right:4px; margin-bottom:4px; display:inline-block;">${item.qty}x ${cleanName}</span>`;
|
||||
}
|
||||
});
|
||||
|
||||
configState['Storage Configuration'] = total;
|
||||
|
||||
const summaryText = summaryParts.length > 0 ? summaryParts.join(', ') : 'None selected';
|
||||
document.getElementById('summaryStorage').innerText = summaryText;
|
||||
if (!summaryHtml) summaryHtml = 'None selected';
|
||||
document.getElementById('summaryStorage').innerHTML = summaryHtml;
|
||||
|
||||
// Also update specs table if visible
|
||||
const specStorage = document.getElementById('spec-storage-config-hero');
|
||||
if(specStorage) specStorage.innerText = summaryParts.length > 0 ? summaryParts.join(' + ') : 'Configurable';
|
||||
if(specStorage) specStorage.innerHTML = summaryHtml;
|
||||
|
||||
updateSummary();
|
||||
};
|
||||
@@ -4501,25 +4778,6 @@ var wpcf7 = {
|
||||
const poolKey = (isInstant ? 'instant' : 'custom') + '-g' + groupIndex;
|
||||
|
||||
// Map slots to pre-selected values if any
|
||||
// IMPROVEMENT: If isInstant, also try to parse the server.storage string to find "Deals"
|
||||
// This fixes the issue where Fallback IDs don't match URL IDs, causing "None" to be selected.
|
||||
|
||||
let storageStringCounts = {};
|
||||
if (isInstant && selectedServer && selectedServer.storage) {
|
||||
// Parse "2x Crucial T705 1TB NVMe + 4x Kioxia..."
|
||||
const parts = selectedServer.storage.split('+');
|
||||
parts.forEach(part => {
|
||||
const match = part.trim().match(/^(\d+)x\s+(.+)$/);
|
||||
if (match) {
|
||||
const qty = parseInt(match[1]);
|
||||
const name = match[2].trim().toLowerCase();
|
||||
// We need to match 'name' against option values loosely
|
||||
storageStringCounts[name] = qty;
|
||||
}
|
||||
});
|
||||
console.log("Parsed Storage Deal:", storageStringCounts);
|
||||
}
|
||||
|
||||
const initializedSlots = storageOptions.map(opt => {
|
||||
const preId = preselected[opt.id];
|
||||
let initialVal = opt.values[0]; // Default
|
||||
@@ -4530,38 +4788,6 @@ var wpcf7 = {
|
||||
if (found) initialVal = found;
|
||||
}
|
||||
|
||||
// 2. If ID match failed (or resulted in Default/None) AND we have string counts, try to claim a slot
|
||||
// Only override if we are currently at Default/None
|
||||
const isDefault = initialVal === opt.values[0] || initialVal.text.toLowerCase().includes('none');
|
||||
|
||||
if (isDefault && Object.keys(storageStringCounts).length > 0) {
|
||||
// Try to find a matching value for one of our required counts
|
||||
for (const [reqName, reqQty] of Object.entries(storageStringCounts)) {
|
||||
if (reqQty > 0) {
|
||||
// Find value that contains this name
|
||||
// We strip "NVMe" "Gen4" etc to improve matching chances?
|
||||
// Or just contains.
|
||||
// reqName: "crucial t705 1tb nvme"
|
||||
// val.text: "Crucial T705 1TB"
|
||||
// val.text is usually shorter or similar.
|
||||
|
||||
const match = opt.values.find(v => {
|
||||
const vText = v.text.toLowerCase().trim();
|
||||
const rName = reqName.toLowerCase().trim();
|
||||
// Check if reqName contains vText OR vText contains reqName
|
||||
// "crucial t705 1tb nvme".includes("crucial t705 1tb") -> true
|
||||
return rName.includes(vText) || vText.includes(rName.replace(' nvme', '').trim());
|
||||
});
|
||||
|
||||
if (match) {
|
||||
initialVal = match;
|
||||
storageStringCounts[reqName]--; // Decrement needed count
|
||||
break; // Taken this slot
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: opt.id,
|
||||
label: opt.label,
|
||||
@@ -4665,8 +4891,8 @@ var wpcf7 = {
|
||||
const qtyMap = {};
|
||||
|
||||
pool.slots.forEach((slot, index) => {
|
||||
// Assume index 0 is "None"
|
||||
const isFilled = slot.currentVal !== slot.values[0];
|
||||
// Fix: Don't assume index 0 is "None". Check text content.
|
||||
const isFilled = !slot.currentVal.text.match(/^(none|-|select|no\s)/i);
|
||||
|
||||
if (slots[index]) {
|
||||
if (isFilled) {
|
||||
@@ -4713,13 +4939,24 @@ var wpcf7 = {
|
||||
|
||||
// Update Summary Text
|
||||
const storageNames = pool.slots
|
||||
.filter(s => s.currentVal !== s.values[0])
|
||||
.filter(s => !s.currentVal.text.match(/^(none|-|select|no\s)/i))
|
||||
.map(s => s.currentVal.text.replace(/\s?\(.*?\)/, ''));
|
||||
|
||||
const storageText = storageNames.length > 0 ? storageNames.join(' + ') : 'None';
|
||||
document.getElementById('summaryStorage').textContent = storageText;
|
||||
// Aggregate counts for pills
|
||||
const counts = {};
|
||||
storageNames.forEach(name => {
|
||||
counts[name] = (counts[name] || 0) + 1;
|
||||
});
|
||||
|
||||
const pillsHtml = Object.keys(counts).length > 0
|
||||
? Object.entries(counts).map(([name, count]) =>
|
||||
`<span class="spec-pill" style="margin-right:4px; margin-bottom:4px; display:inline-block;">${count}x ${name}</span>`
|
||||
).join('')
|
||||
: 'None';
|
||||
|
||||
document.getElementById('summaryStorage').innerHTML = pillsHtml;
|
||||
const specStorage = document.getElementById('spec-storage-config-hero');
|
||||
if(specStorage) specStorage.innerText = storageText;
|
||||
if(specStorage) specStorage.innerHTML = pillsHtml;
|
||||
};
|
||||
|
||||
function createOptionGrid(opt, isWide, storageIndex = -1, isInstant = false, preselected = {}) {
|
||||
@@ -4737,7 +4974,7 @@ var wpcf7 = {
|
||||
|
||||
if (isActive) {
|
||||
card.classList.add('active');
|
||||
// Set initial state
|
||||
// Set initial state - use the relative price (already converted from absolute)
|
||||
configState[opt.label] = val.price;
|
||||
configIds[opt.id] = val.id;
|
||||
|
||||
@@ -4867,19 +5104,38 @@ var wpcf7 = {
|
||||
function buildInstantOrderUrl() {
|
||||
if (!selectedServer || !selectedServer.orderUrl) return null;
|
||||
try {
|
||||
const base = selectedServer.orderUrl.split('&configoption')[0];
|
||||
const u = new URL(base);
|
||||
const p = new URLSearchParams(u.search);
|
||||
// Parse the original URL completely to preserve ALL existing params (pid, billingcycle, etc.)
|
||||
const urlObj = new URL(selectedServer.orderUrl);
|
||||
const params = urlObj.searchParams;
|
||||
|
||||
// Update only the configuration options that are tracked in configIds
|
||||
Object.entries(configIds).forEach(([id, val]) => {
|
||||
if (id && val) p.set(`configoption[${id}]`, val);
|
||||
if (id && val) {
|
||||
// WHMCS format: configoption[123]
|
||||
params.set(`configoption[${id}]`, val);
|
||||
}
|
||||
});
|
||||
p.set('billingcycle', 'monthly');
|
||||
return u.origin + u.pathname + '?' + p.toString();
|
||||
|
||||
// Ensure billing cycle is enforced
|
||||
params.set('billingcycle', 'monthly');
|
||||
|
||||
return urlObj.toString();
|
||||
} catch (e) {
|
||||
console.error("Error building instant URL:", e);
|
||||
return selectedServer.orderUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Debug Logger
|
||||
function sendDebugLog(label, data) {
|
||||
const payload = JSON.stringify(data);
|
||||
// Use a simple GET request so it appears in the server access logs
|
||||
// Truncate if too long to avoid URL limits, though usually fine for local
|
||||
const safePayload = encodeURIComponent(payload).substring(0, 2000);
|
||||
fetch(`/__debug_log__?label=${encodeURIComponent(label)}&data=${safePayload}`)
|
||||
.catch(e => console.error("Log failed", e));
|
||||
}
|
||||
|
||||
// Update Summary
|
||||
function updateSummary() {
|
||||
const pill = document.getElementById('summaryHeaderPill');
|
||||
@@ -4922,9 +5178,17 @@ var wpcf7 = {
|
||||
pill.style.color = '#7e22ce';
|
||||
subtext.textContent = 'Setup time: Up to 48 business hours';
|
||||
|
||||
// ALWAYS use the Instant Deal Price as the Base
|
||||
// All options in configState are relative deltas (offsets) from this base.
|
||||
let basePrice = selectedServer.price;
|
||||
|
||||
let addonPrice = 0;
|
||||
Object.values(configState).forEach(p => addonPrice += p);
|
||||
const totalPrice = selectedServer.price + addonPrice;
|
||||
const totalPrice = basePrice + addonPrice;
|
||||
|
||||
console.log("DEBUG: Base", basePrice, "Addons", addonPrice, "ConfigState", configState);
|
||||
sendDebugLog("INSTANT_CUSTOM", { base: basePrice, addons: addonPrice, state: configState });
|
||||
|
||||
document.getElementById('totalPrice').textContent = selectedServer.currency + totalPrice.toFixed(2);
|
||||
|
||||
const customUrl = buildInstantOrderUrl();
|
||||
@@ -4955,7 +5219,22 @@ var wpcf7 = {
|
||||
document.getElementById('summaryCpu').textContent = selectedServer.cpu;
|
||||
if (!isInstantCustomized) {
|
||||
document.getElementById('summaryRam').textContent = selectedServer.ram;
|
||||
document.getElementById('summaryStorage').textContent = selectedServer.storage;
|
||||
|
||||
// Format Storage as Pills (Bubbles)
|
||||
if (selectedServer.storage) {
|
||||
const parts = selectedServer.storage.includes('+')
|
||||
? selectedServer.storage.split('+').map(p => p.trim())
|
||||
: [selectedServer.storage];
|
||||
|
||||
const storageHtml = parts.map(p =>
|
||||
`<span class="spec-pill" style="margin-right:4px; margin-bottom:4px; display:inline-block;">${p}</span>`
|
||||
).join('');
|
||||
|
||||
document.getElementById('summaryStorage').innerHTML = storageHtml;
|
||||
} else {
|
||||
document.getElementById('summaryStorage').textContent = '-';
|
||||
}
|
||||
|
||||
document.getElementById('summaryNetwork').textContent = selectedServer.network;
|
||||
document.getElementById('summaryLocation').textContent = selectedServer.location;
|
||||
}
|
||||
@@ -4974,6 +5253,9 @@ var wpcf7 = {
|
||||
Object.values(configState).forEach(p => addonPrice += p);
|
||||
const totalPrice = selectedServer.basePrice + addonPrice;
|
||||
|
||||
console.log("DEBUG CUSTOM: Base", selectedServer.basePrice, "Addons", addonPrice, "ConfigState", configState);
|
||||
sendDebugLog("CUSTOM_BUILD", { base: selectedServer.basePrice, addons: addonPrice, state: configState });
|
||||
|
||||
document.getElementById('totalPrice').textContent = selectedServer.currency + totalPrice.toFixed(2);
|
||||
document.getElementById('summaryCpu').textContent = selectedServer.cpu;
|
||||
|
||||
@@ -5038,6 +5320,7 @@ var wpcf7 = {
|
||||
|
||||
// Trigger initial OOS check for all instant servers on page load
|
||||
prefetchStockData();
|
||||
fetchInstantPrices();
|
||||
|
||||
// Pre-select the first instant server so the UI is active immediately
|
||||
if(instantServers.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user