From ce4f9ad81b0f68a002fcf07b28fb3c7018201be4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 6 Dec 2025 00:59:33 +0400 Subject: [PATCH] fix: retain instant server specifications when customizing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Enhanced component matching algorithm with scoring system - Improved storage matching with capacity, type, and brand detection - Pre-fill pooled storage grid with instant server configuration - Display correct quantities for preselected storage drives - Better RAM, network, and location matching When user clicks Customize on an instant server, the system now: 1. Analyzes the instant server specs (CPU, RAM, Storage, Network) 2. Intelligently matches them with available configuration options 3. Pre-selects the matching components in the configuration tool 4. Allows user to modify the pre-configured setup 5. Maintains all specifications in the WHMCS order URL 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../new-baremetal.html | 352 +++++++++++++----- 1 file changed, 268 insertions(+), 84 deletions(-) diff --git a/Documents/Vibe Coding Projects/dedicatednodes-redesign/dedicatednodes-bare-metal/new-baremetal.html b/Documents/Vibe Coding Projects/dedicatednodes-redesign/dedicatednodes-bare-metal/new-baremetal.html index 7577400..df2f6c1 100644 --- a/Documents/Vibe Coding Projects/dedicatednodes-redesign/dedicatednodes-bare-metal/new-baremetal.html +++ b/Documents/Vibe Coding Projects/dedicatednodes-redesign/dedicatednodes-bare-metal/new-baremetal.html @@ -4181,96 +4181,190 @@ var wpcf7 = { }); } - // 2. Parse Storage Requirements - let storageRequirements = {}; + // Parse Storage Requirements - More robust parsing + let storageRequirements = []; if (selectedServer.storage) { const parts = selectedServer.storage.split('+'); parts.forEach(part => { - const match = part.trim().match(/^(\d+)x\s+(.+)$/); + const trimmed = part.trim(); + if (!trimmed) return; + + // Match patterns like "2x 1TB NVMe" or "1TB NVMe" + const match = trimmed.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; + storageRequirements.push({ + qty: parseInt(match[1]), + spec: match[2].trim().toLowerCase() + }); + } else { + storageRequirements.push({ + qty: 1, + spec: trimmed.toLowerCase() + }); } }); } - - // ADJUST PRICES relative to the Included Options + + // Enhanced matching for instant server specs options.forEach(opt => { let selectedValId = null; - let urlId = preselected[opt.id]; // Original URL ID + let urlId = preselected[opt.id]; - // 1. PRIORITY: Plan Requirements (Text Description) - // We try to fulfill the written plan specs first. + // Enhanced storage matching 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; - } - } - } - } + // Create a score for each option based on how well it matches the requirements + const scores = opt.values.map(val => { + let score = 0; + const valText = val.text.toLowerCase(); - // 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; + // Check against each storage requirement + for (const req of storageRequirements) { + // Extract capacity from value (e.g., "1tb", "2tb") + const valCapacityMatch = valText.match(/(\d+)\s*tb/i); + const valCapacity = valCapacityMatch ? parseInt(valCapacityMatch[1]) : 0; + + // Extract capacity from requirement + const reqCapacityMatch = req.spec.match(/(\d+)\s*tb/i); + const reqCapacity = reqCapacityMatch ? parseInt(reqCapacityMatch[1]) : 0; + + // Score based on capacity match + if (valCapacity === reqCapacity && reqCapacity > 0) { + score += 100; + } + + // Score based on type keywords + const keywords = ['nvme', 'ssd', 'gen4', 'gen5', 'enterprise', 'consumer']; + keywords.forEach(keyword => { + if (valText.includes(keyword) && req.spec.includes(keyword)) { + score += 20; + } + }); + + // Score based on brand + const brands = ['samsung', 'crucial', 'wd', 'seagate', 'kingston']; + brands.forEach(brand => { + if (valText.includes(brand) && req.spec.includes(brand)) { + score += 30; + } + }); + + // Bonus for exact text match + if (valText.includes(req.spec) || req.spec.includes(valText.replace(/\s?\(.*?\)/, '').trim())) { + score += 50; + } + } + + return { id: val.id, score }; + }); + + // Select the highest scoring option + scores.sort((a, b) => b.score - a.score); + if (scores[0] && scores[0].score > 0) { + selectedValId = scores[0].id; + } + + // If no good match, try to use "None" option + if (!selectedValId) { + 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 : null; + } + } + // Enhanced RAM matching + else if (opt.type === 'ram') { + const ramMatch = selectedServer.ram.match(/(\d+)\s*GB/i); + const ramAmount = ramMatch ? parseInt(ramMatch[1]) : 0; + + // Find the closest RAM option + let bestMatch = null; + let smallestDiff = Infinity; + + opt.values.forEach(val => { + const valMatch = val.text.match(/(\d+)\s*GB/i); + const valAmount = valMatch ? parseInt(valMatch[1]) : 0; + + if (valAmount === ramAmount) { + bestMatch = val.id; + smallestDiff = 0; + } else if (Math.abs(valAmount - ramAmount) < smallestDiff) { + bestMatch = val.id; + smallestDiff = Math.abs(valAmount - ramAmount); + } + }); + + selectedValId = bestMatch; + } + // Enhanced network matching + else if (opt.type === 'network') { + const networkSpec = selectedServer.network.toLowerCase(); + const match = opt.values.find(v => { + const vText = v.text.toLowerCase(); + // Exact match first + if (vText === networkSpec) return true; + // Match bandwidth (e.g., "1gbit", "10gbit") + const vSpeed = vText.match(/(\d+)\s*[gm]bit/); + const sSpeed = networkSpec.match(/(\d+)\s*[gm]bit/); + if (vSpeed && sSpeed && vSpeed[1] === sSpeed[1]) return true; + return false; + }); + selectedValId = match ? match.id : null; + } + // Location matching + else if (opt.type === 'location') { + const match = opt.values.find(v => + v.text.toLowerCase().includes(selectedServer.location.toLowerCase()) || + selectedServer.location.toLowerCase().includes(v.text.toLowerCase()) + ); + selectedValId = match ? match.id : null; + } + // Other component types + else { + // Try to find best match based on text similarity + const serverSpec = selectedServer.cpu ? selectedServer.cpu.toLowerCase() : ''; + if (serverSpec) { + const match = opt.values.find(v => { + const vText = v.text.toLowerCase(); + // Look for key matching terms + const serverTerms = serverSpec.split(/\s+/); + const matchingTerms = serverTerms.filter(term => + term.length > 2 && vText.includes(term) + ); + return matchingTerms.length > 0; + }); + selectedValId = match ? match.id : null; } } - // 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; - } + // Fallback to URL ID if available and valid + if (!selectedValId && urlId && opt.values.find(v => v.id === urlId)) { + selectedValId = urlId; } - // 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; - } + // Final fallback - try stale ID map + if (!selectedValId && urlId && idToTextMap[urlId]) { + const originalText = idToTextMap[urlId]; + const cleanTarget = originalText.replace(/\s?[€$£].*/, '').trim().toLowerCase(); + const match = opt.values.find(v => + v.text.toLowerCase().includes(cleanTarget) || + cleanTarget.includes(v.text.toLowerCase()) + ); + if (match) selectedValId = match.id; } - // 5. FALLBACK: Default (Prefer None) + // Last resort - pick first option (avoid "None" if possible) 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; - } + const nonNoneOptions = opt.values.filter(v => + v.text.trim() !== '-' && + !v.text.toLowerCase().includes('none') && + !v.text.toLowerCase().includes('no hard drive') + ); + selectedValId = nonNoneOptions.length > 0 ? nonNoneOptions[0].id : opt.values[0].id; } + // Apply selection and price adjustment if (selectedValId) { preselected[opt.id] = selectedValId; const selectedVal = opt.values.find(v => v.id === selectedValId); @@ -4777,40 +4871,113 @@ var wpcf7 = { // Use a unique key for each group to prevent conflicts const poolKey = (isInstant ? 'instant' : 'custom') + '-g' + groupIndex; - // Map slots to pre-selected values if any + // For instant customization, try to match storage requirements from server specs + let storageRequirements = []; + if (isInstant && selectedServer && selectedServer.storage) { + const parts = selectedServer.storage.split('+'); + parts.forEach(part => { + const trimmed = part.trim(); + if (!trimmed) return; + const match = trimmed.match(/^(\d+)x\s+(.+)$/); + if (match) { + storageRequirements.push({ + qty: parseInt(match[1]), + spec: match[2].trim().toLowerCase() + }); + } else { + storageRequirements.push({ + qty: 1, + spec: trimmed.toLowerCase() + }); + } + }); + } + + // Initialize slots with preselected values or defaults const initializedSlots = storageOptions.map(opt => { const preId = preselected[opt.id]; let initialVal = opt.values[0]; // Default - + // 1. Try ID Match (Standard) if (preId) { const found = opt.values.find(v => v.id === preId); if (found) initialVal = found; } - - return { - id: opt.id, + + return { + id: opt.id, label: opt.label, currentVal: initialVal, - values: opt.values + values: opt.values }; }); window.pooledState[poolKey] = { slots: initializedSlots }; - - // Pre-set configState - initializedSlots.forEach(slot => { - configState[slot.label] = slot.currentVal.price; - configIds[slot.id] = slot.currentVal.id; - }); + + // For instant customization, pre-fill with the server's storage configuration + if (isInstant && storageRequirements.length > 0) { + // Fill slots based on storage requirements + storageRequirements.forEach(req => { + for (let i = 0; i < req.qty; i++) { + // Find an empty slot + const emptySlot = initializedSlots.find(s => + s.currentVal.text === '-' || + s.currentVal.text.toLowerCase().includes('none') || + s.currentVal.text.toLowerCase().includes('no hard drive') + ); + + if (emptySlot) { + // Find matching drive type + const match = emptySlot.values.find(v => { + const vText = v.text.toLowerCase(); + // Extract capacity from value + const valCapacityMatch = vText.match(/(\d+)\s*tb/i); + const valCapacity = valCapacityMatch ? parseInt(valCapacityMatch[1]) : 0; + // Extract capacity from requirement + const reqCapacityMatch = req.spec.match(/(\d+)\s*tb/i); + const reqCapacity = reqCapacityMatch ? parseInt(reqCapacityMatch[1]) : 0; + + // Match capacity and keywords + if (valCapacity === reqCapacity && reqCapacity > 0) { + // Check for type keywords + const keywords = ['nvme', 'ssd', 'gen4', 'gen5', 'enterprise', 'consumer']; + const hasKeyword = keywords.some(k => + vText.includes(k) && req.spec.includes(k) + ); + // Check for brand + const brands = ['samsung', 'crucial', 'wd', 'seagate', 'kingston']; + const hasBrand = brands.some(b => + vText.includes(b) && req.spec.includes(b) + ); + + return hasKeyword || hasBrand || vText.includes(req.spec); + } + return false; + }); + + if (match) { + emptySlot.currentVal = match; + configState[emptySlot.label] = match.price; + configIds[emptySlot.id] = match.id; + } + } + } + }); + } else { + // Pre-set configState for non-instant or fallback + initializedSlots.forEach(slot => { + configState[slot.label] = slot.currentVal.price; + configIds[slot.id] = slot.currentVal.id; + }); + } templateValues.forEach(val => { if (val.text.toLowerCase().includes('none') || val.text.toLowerCase().includes('no hard drive')) return; const card = document.createElement('div'); card.className = 'config-card has-stepper'; - card.id = `pool-card-${val.text.replace(/\s/g, '')}`; - + card.id = `pool-card-${val.text.replace(/\s/g, '')}`; + let priceText = val.price === 0 ? 'Included' : `+€${val.price}`; let displayName = val.text.replace(/\s?\(.*?\)/, ''); @@ -4825,8 +4992,25 @@ var wpcf7 = { `; grid.appendChild(card); }); - - setTimeout(() => updatePooledVisuals(poolKey, isInstant), 100); + + setTimeout(() => { + // Update quantity displays based on current selection + initializedSlots.forEach(slot => { + if (slot.currentVal && slot.currentVal.text !== '-' && + !slot.currentVal.text.toLowerCase().includes('none') && + !slot.currentVal.text.toLowerCase().includes('no hard drive')) { + + // Increment counter for this value type + const qtyEl = document.getElementById(`pool-qty-${poolKey}-${slot.currentVal.text.replace(/\s/g, '')}`); + if (qtyEl) { + const currentQty = parseInt(qtyEl.textContent) || 0; + qtyEl.textContent = currentQty + 1; + } + } + }); + + updatePooledVisuals(poolKey, isInstant); + }, 100); return grid; }