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>
|
<!doctype html>
|
||||||
<html dir="ltr" lang="en-US" prefix="og: https://ogp.me/ns#">
|
<html dir="ltr" lang="en-US" prefix="og: https://ogp.me/ns#">
|
||||||
<head>
|
<head>
|
||||||
@@ -3295,7 +3296,7 @@ var wpcf7 = {
|
|||||||
ram: 'Base: 128GB',
|
ram: 'Base: 128GB',
|
||||||
ramSpeed: '3200 MT/s',
|
ramSpeed: '3200 MT/s',
|
||||||
ramChannels: '8-channel',
|
ramChannels: '8-channel',
|
||||||
basePrice: 350,
|
basePrice: 490,
|
||||||
currency: '€',
|
currency: '€',
|
||||||
configUrl: 'https://portal.dedicatednodes.io/index.php?rp=/store/bare-metal-servers/amd-epyc-7443p-1',
|
configUrl: 'https://portal.dedicatednodes.io/index.php?rp=/store/bare-metal-servers/amd-epyc-7443p-1',
|
||||||
setupTime: 'Instant',
|
setupTime: 'Instant',
|
||||||
@@ -3311,6 +3312,12 @@ var wpcf7 = {
|
|||||||
{ "id": "113", "text": "New York €35,00 EUR", "price": 35 }
|
{ "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",
|
"id": "48", "label": "RAM", "type": "ram",
|
||||||
"values": [
|
"values": [
|
||||||
@@ -3510,7 +3517,9 @@ var wpcf7 = {
|
|||||||
if (doc.querySelector('input[name^="configoption"]') || doc.querySelector('select[name^="configoption"]')) {
|
if (doc.querySelector('input[name^="configoption"]') || doc.querySelector('select[name^="configoption"]')) {
|
||||||
console.log("Direct config page detected");
|
console.log("Direct config page detected");
|
||||||
const options = this.parseConfigPage(doc, configUrl);
|
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"]');
|
const configureBtn = doc.querySelector('a[href*="cart.php?a=add"]');
|
||||||
@@ -3560,11 +3569,14 @@ var wpcf7 = {
|
|||||||
const isGen4Slot = slotIndex < gen4Count;
|
const isGen4Slot = slotIndex < gen4Count;
|
||||||
|
|
||||||
if (isGen5Slot) {
|
if (isGen5Slot) {
|
||||||
// Filter to ONLY keep Gen5/Enterprise drives
|
// 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 => {
|
opt.values = opt.values.filter(v => {
|
||||||
const t = v.text.toLowerCase();
|
const t = v.text.toLowerCase();
|
||||||
return t.includes('gen5') || t.includes('enterprise') || t.includes('solidigm') || t.includes('cm7');
|
return t.includes('gen5') || t.includes('enterprise') || t.includes('solidigm') || t.includes('cm7');
|
||||||
});
|
});
|
||||||
|
}
|
||||||
// Ensure we have at least "None" or a default
|
// Ensure we have at least "None" or a default
|
||||||
if (opt.values.length === 0) {
|
if (opt.values.length === 0) {
|
||||||
opt.values.push({ id: "9999", text: "-", price: 0 });
|
opt.values.push({ id: "9999", text: "-", price: 0 });
|
||||||
@@ -3579,16 +3591,12 @@ var wpcf7 = {
|
|||||||
const splitB = selectedServer.fallbackSplit[1];
|
const splitB = selectedServer.fallbackSplit[1];
|
||||||
|
|
||||||
if (slotIndex < splitA) {
|
if (slotIndex < splitA) {
|
||||||
// Group A: Keep as is (or use Template A)
|
// Group A: Keep as is
|
||||||
// Just ensure they have consistent values
|
|
||||||
} else {
|
} else {
|
||||||
// Group B: Force a slight variation so they don't merge with Group A
|
// Group B: Force a slight variation
|
||||||
// We can simulate this by filtering differently OR appending a hidden marker
|
if (!selectedServer || !selectedServer.id.startsWith('inst')) {
|
||||||
opt.values = opt.values.filter(v => {
|
opt.values = opt.values.filter(v => true); // No-op but consistent structure
|
||||||
// Maybe remove the cheapest option to make it distinct?
|
}
|
||||||
// Or just keep "None" and "Samsung"
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
// HACK: Modify price of first item +0.01 to force signature mismatch from Group A
|
// HACK: Modify price of first item +0.01 to force signature mismatch from Group A
|
||||||
if(opt.values.length > 1) {
|
if(opt.values.length > 1) {
|
||||||
opt.values[1].price += 0.01;
|
opt.values[1].price += 0.01;
|
||||||
@@ -3596,7 +3604,8 @@ var wpcf7 = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter to ONLY keep Gen4/Consumer drives (optional, but cleaner)
|
// Filter to ONLY keep Gen4/Consumer drives (Only for Custom builds)
|
||||||
|
if (!selectedServer || !selectedServer.id.startsWith('inst')) {
|
||||||
opt.values = opt.values.filter(v => {
|
opt.values = opt.values.filter(v => {
|
||||||
const t = v.text.toLowerCase();
|
const t = v.text.toLowerCase();
|
||||||
return t.includes('gen4') || t.includes('crucial') || t.includes('samsung') || t.includes('-') || t.includes('none');
|
return t.includes('gen4') || t.includes('crucial') || t.includes('samsung') || t.includes('-') || t.includes('none');
|
||||||
@@ -3604,6 +3613,7 @@ var wpcf7 = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3787,6 +3797,36 @@ var wpcf7 = {
|
|||||||
}
|
}
|
||||||
return false;
|
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();
|
const scraper = new WHMCSScraper();
|
||||||
@@ -3898,6 +3938,121 @@ var wpcf7 = {
|
|||||||
processQueue();
|
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
|
// Helper to update just the OOS badge on a specific card
|
||||||
function updateServerCardOOS(server) {
|
function updateServerCardOOS(server) {
|
||||||
const card = document.querySelector(`.server-option.instant[data-id="${server.id}"]`);
|
const card = document.querySelector(`.server-option.instant[data-id="${server.id}"]`);
|
||||||
@@ -3992,24 +4147,144 @@ var wpcf7 = {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
let scrapeUrl = selectedServer.orderUrl;
|
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')) {
|
if (scrapeUrl.includes('&configoption')) {
|
||||||
scrapeUrl = scrapeUrl.split('&configoption')[0];
|
scrapeUrl = scrapeUrl.split('&configoption')[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const preselected = {};
|
// Get Options
|
||||||
const urlParams = new URLSearchParams(selectedServer.orderUrl);
|
const optionsData = await scraper.getProductConfig(scrapeUrl, selectedServer);
|
||||||
for (const [key, value] of urlParams.entries()) {
|
|
||||||
if (key.startsWith('configoption[')) {
|
if (optionsData && optionsData.options.length > 0) {
|
||||||
const idMatch = key.match(/\[(\d+)\]/);
|
// DEEP CLONE options to avoid modifying the static reference
|
||||||
if (idMatch) {
|
const options = JSON.parse(JSON.stringify(optionsData.options));
|
||||||
preselected[idMatch[1]] = value;
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const optionsData = await scraper.getProductConfig(scrapeUrl, selectedServer);
|
// 2. FALLBACK: URL ID (if valid)
|
||||||
if (optionsData && optionsData.options.length > 0) {
|
// If we didn't match a requirement (or ran out), use the URL's choice.
|
||||||
renderDynamicOptions(optionsData.options, true, preselected);
|
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 {
|
} else {
|
||||||
renderFallbackOptions(container);
|
renderFallbackOptions(container);
|
||||||
const warning = document.createElement('div');
|
const warning = document.createElement('div');
|
||||||
@@ -4018,6 +4293,7 @@ var wpcf7 = {
|
|||||||
container.insertBefore(warning, container.firstChild);
|
container.insertBefore(warning, container.firstChild);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error("Instant Customization Error:", err);
|
||||||
renderFallbackOptions(container);
|
renderFallbackOptions(container);
|
||||||
} finally {
|
} finally {
|
||||||
loader.style.display = 'none';
|
loader.style.display = 'none';
|
||||||
@@ -4340,23 +4616,24 @@ var wpcf7 = {
|
|||||||
|
|
||||||
window.calculateStorageTotal = function() {
|
window.calculateStorageTotal = function() {
|
||||||
let total = 0;
|
let total = 0;
|
||||||
let summaryParts = [];
|
let summaryHtml = '';
|
||||||
|
|
||||||
Object.values(storageSelection).forEach(item => {
|
Object.values(storageSelection).forEach(item => {
|
||||||
if(item.qty > 0) {
|
if(item.qty > 0) {
|
||||||
total += item.qty * item.price;
|
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;
|
configState['Storage Configuration'] = total;
|
||||||
|
|
||||||
const summaryText = summaryParts.length > 0 ? summaryParts.join(', ') : 'None selected';
|
if (!summaryHtml) summaryHtml = 'None selected';
|
||||||
document.getElementById('summaryStorage').innerText = summaryText;
|
document.getElementById('summaryStorage').innerHTML = summaryHtml;
|
||||||
|
|
||||||
// Also update specs table if visible
|
// Also update specs table if visible
|
||||||
const specStorage = document.getElementById('spec-storage-config-hero');
|
const specStorage = document.getElementById('spec-storage-config-hero');
|
||||||
if(specStorage) specStorage.innerText = summaryParts.length > 0 ? summaryParts.join(' + ') : 'Configurable';
|
if(specStorage) specStorage.innerHTML = summaryHtml;
|
||||||
|
|
||||||
updateSummary();
|
updateSummary();
|
||||||
};
|
};
|
||||||
@@ -4501,25 +4778,6 @@ var wpcf7 = {
|
|||||||
const poolKey = (isInstant ? 'instant' : 'custom') + '-g' + groupIndex;
|
const poolKey = (isInstant ? 'instant' : 'custom') + '-g' + groupIndex;
|
||||||
|
|
||||||
// Map slots to pre-selected values if any
|
// 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 initializedSlots = storageOptions.map(opt => {
|
||||||
const preId = preselected[opt.id];
|
const preId = preselected[opt.id];
|
||||||
let initialVal = opt.values[0]; // Default
|
let initialVal = opt.values[0]; // Default
|
||||||
@@ -4530,38 +4788,6 @@ var wpcf7 = {
|
|||||||
if (found) initialVal = found;
|
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 {
|
return {
|
||||||
id: opt.id,
|
id: opt.id,
|
||||||
label: opt.label,
|
label: opt.label,
|
||||||
@@ -4665,8 +4891,8 @@ var wpcf7 = {
|
|||||||
const qtyMap = {};
|
const qtyMap = {};
|
||||||
|
|
||||||
pool.slots.forEach((slot, index) => {
|
pool.slots.forEach((slot, index) => {
|
||||||
// Assume index 0 is "None"
|
// Fix: Don't assume index 0 is "None". Check text content.
|
||||||
const isFilled = slot.currentVal !== slot.values[0];
|
const isFilled = !slot.currentVal.text.match(/^(none|-|select|no\s)/i);
|
||||||
|
|
||||||
if (slots[index]) {
|
if (slots[index]) {
|
||||||
if (isFilled) {
|
if (isFilled) {
|
||||||
@@ -4713,13 +4939,24 @@ var wpcf7 = {
|
|||||||
|
|
||||||
// Update Summary Text
|
// Update Summary Text
|
||||||
const storageNames = pool.slots
|
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?\(.*?\)/, ''));
|
.map(s => s.currentVal.text.replace(/\s?\(.*?\)/, ''));
|
||||||
|
|
||||||
const storageText = storageNames.length > 0 ? storageNames.join(' + ') : 'None';
|
// Aggregate counts for pills
|
||||||
document.getElementById('summaryStorage').textContent = storageText;
|
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');
|
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 = {}) {
|
function createOptionGrid(opt, isWide, storageIndex = -1, isInstant = false, preselected = {}) {
|
||||||
@@ -4737,7 +4974,7 @@ var wpcf7 = {
|
|||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
card.classList.add('active');
|
card.classList.add('active');
|
||||||
// Set initial state
|
// Set initial state - use the relative price (already converted from absolute)
|
||||||
configState[opt.label] = val.price;
|
configState[opt.label] = val.price;
|
||||||
configIds[opt.id] = val.id;
|
configIds[opt.id] = val.id;
|
||||||
|
|
||||||
@@ -4867,19 +5104,38 @@ var wpcf7 = {
|
|||||||
function buildInstantOrderUrl() {
|
function buildInstantOrderUrl() {
|
||||||
if (!selectedServer || !selectedServer.orderUrl) return null;
|
if (!selectedServer || !selectedServer.orderUrl) return null;
|
||||||
try {
|
try {
|
||||||
const base = selectedServer.orderUrl.split('&configoption')[0];
|
// Parse the original URL completely to preserve ALL existing params (pid, billingcycle, etc.)
|
||||||
const u = new URL(base);
|
const urlObj = new URL(selectedServer.orderUrl);
|
||||||
const p = new URLSearchParams(u.search);
|
const params = urlObj.searchParams;
|
||||||
|
|
||||||
|
// Update only the configuration options that are tracked in configIds
|
||||||
Object.entries(configIds).forEach(([id, val]) => {
|
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) {
|
} catch (e) {
|
||||||
|
console.error("Error building instant URL:", e);
|
||||||
return selectedServer.orderUrl;
|
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
|
// Update Summary
|
||||||
function updateSummary() {
|
function updateSummary() {
|
||||||
const pill = document.getElementById('summaryHeaderPill');
|
const pill = document.getElementById('summaryHeaderPill');
|
||||||
@@ -4922,9 +5178,17 @@ var wpcf7 = {
|
|||||||
pill.style.color = '#7e22ce';
|
pill.style.color = '#7e22ce';
|
||||||
subtext.textContent = 'Setup time: Up to 48 business hours';
|
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;
|
let addonPrice = 0;
|
||||||
Object.values(configState).forEach(p => addonPrice += p);
|
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);
|
document.getElementById('totalPrice').textContent = selectedServer.currency + totalPrice.toFixed(2);
|
||||||
|
|
||||||
const customUrl = buildInstantOrderUrl();
|
const customUrl = buildInstantOrderUrl();
|
||||||
@@ -4955,7 +5219,22 @@ var wpcf7 = {
|
|||||||
document.getElementById('summaryCpu').textContent = selectedServer.cpu;
|
document.getElementById('summaryCpu').textContent = selectedServer.cpu;
|
||||||
if (!isInstantCustomized) {
|
if (!isInstantCustomized) {
|
||||||
document.getElementById('summaryRam').textContent = selectedServer.ram;
|
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('summaryNetwork').textContent = selectedServer.network;
|
||||||
document.getElementById('summaryLocation').textContent = selectedServer.location;
|
document.getElementById('summaryLocation').textContent = selectedServer.location;
|
||||||
}
|
}
|
||||||
@@ -4974,6 +5253,9 @@ var wpcf7 = {
|
|||||||
Object.values(configState).forEach(p => addonPrice += p);
|
Object.values(configState).forEach(p => addonPrice += p);
|
||||||
const totalPrice = selectedServer.basePrice + addonPrice;
|
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('totalPrice').textContent = selectedServer.currency + totalPrice.toFixed(2);
|
||||||
document.getElementById('summaryCpu').textContent = selectedServer.cpu;
|
document.getElementById('summaryCpu').textContent = selectedServer.cpu;
|
||||||
|
|
||||||
@@ -5038,6 +5320,7 @@ var wpcf7 = {
|
|||||||
|
|
||||||
// Trigger initial OOS check for all instant servers on page load
|
// Trigger initial OOS check for all instant servers on page load
|
||||||
prefetchStockData();
|
prefetchStockData();
|
||||||
|
fetchInstantPrices();
|
||||||
|
|
||||||
// Pre-select the first instant server so the UI is active immediately
|
// Pre-select the first instant server so the UI is active immediately
|
||||||
if(instantServers.length > 0) {
|
if(instantServers.length > 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user