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:
Claude
2025-12-06 00:44:55 +04:00
Unverified
parent 428c8426c5
commit 6e42bab97b

View File

@@ -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) {