From 428c8426c596a294d8fa317179ce38da1a57faec Mon Sep 17 00:00:00 2001 From: Gemini AI Date: Fri, 5 Dec 2025 12:57:17 +0400 Subject: [PATCH] instant has configurable options, custom too --- .../new-baremetal.html | 279 ++++++++++++++---- 1 file changed, 216 insertions(+), 63 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 146cbf6..85ef124 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 @@ -3378,6 +3378,7 @@ var wpcf7 = { let configIds = {}; let storageSelection = {}; // Track individual drive quantities let isInstantCustomized = false; // Track customization state for instant servers + let verifiedDefaults = {}; // --- Static Fallback Data (Generated from Live Site) --- const FALLBACK_CONFIG_OPTIONS = [ @@ -3763,10 +3764,37 @@ var wpcf7 = { return { id, label, type, values, formName }; } + + async checkStock(configUrl) { + let lastError = null; + for (const proxy of this.proxies) { + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 4000); + const response = await fetch(proxy + encodeURIComponent(configUrl), { signal: controller.signal }); + clearTimeout(timeoutId); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const html = await response.text(); + const doc = new DOMParser().parseFromString(html, 'text/html'); + const bodyText = (doc.body?.innerText || '').toLowerCase(); + const dangerBox = doc.querySelector('.alert-danger'); + const dangerText = dangerBox ? dangerBox.innerText.toLowerCase() : ''; + if (bodyText.includes('out of stock') || dangerText.includes('out of stock')) return true; + return false; + } catch (err) { + lastError = err; + } + } + return false; + } } const scraper = new WHMCSScraper(); + // Cache for OOS states to avoid repeated checks + const oosCache = new Map(); + let oosCheckInProgress = new Set(); + // Update Technical Specs Section (New Smart Card Layout) function updateSpecs(server) { const set = (id, val) => { @@ -3793,8 +3821,10 @@ var wpcf7 = { set('spec-storage-config-hero', server.storage || 'Configurable'); // Network - const netSpeed = (server.network || '10Gbps').replace('Gbps', ''); - set('spec-network-speed-hero', netSpeed); + const netSrc = verifiedDefaults.network || server.network || ''; + const netSpeedMatch = netSrc ? netSrc.match(/(\d+)\s*Gbps/i) : null; + const netSpeed = netSpeedMatch ? netSpeedMatch[1] : (netSrc ? netSrc.replace('Gbps','') : ''); + set('spec-network-speed-hero', netSpeed || '-'); set('spec-network-bandwidth-hero', server.bandwidth || 'Unmetered'); } @@ -3804,6 +3834,7 @@ var wpcf7 = { grid.innerHTML = instantServers.map(server => `
⚡ INSTANT
+ ${server.oos ? '
OOS
' : ''}
${server.cpu}
${server.cores} cores @@ -3817,6 +3848,80 @@ var wpcf7 = {
`).join(''); + + // Pre-fetch stock data in background for all instant servers + prefetchStockData(); + } + + // Pre-fetch stock data for all instant servers + async function prefetchStockData() { + const batchSize = 3; // Limit concurrent requests + const queue = [...instantServers]; + + async function processQueue() { + while (queue.length > 0) { + const batch = queue.splice(0, batchSize); + await Promise.all(batch.map(async (server) => { + const url = server.orderUrl; + + // Skip if already cached or currently checking + if (oosCache.has(url) || oosCheckInProgress.has(url)) { + return; + } + + oosCheckInProgress.add(url); + + try { + const isOutOfStock = await scraper.checkStock(url); + if (isOutOfStock !== server.oos) { // Only update if changed + server.oos = isOutOfStock; + oosCache.set(url, isOutOfStock); + updateServerCardOOS(server); // Update specific card + if (selectedServer?.id === server.id) { + updateSummary(); + } + } else { + oosCache.set(url, isOutOfStock); + } + } catch (err) { + console.error(`Failed to check stock for ${server.id}:`, err); + oosCache.set(url, false); // Assume in stock on error + } finally { + oosCheckInProgress.delete(url); + } + })); + // Small delay between batches to be nice to proxies + await new Promise(resolve => setTimeout(resolve, 500)); + } + } + + processQueue(); + } + + // 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}"]`); + if (!card) return; + + const existingBadge = card.querySelector('.server-badge[style*="background:#ef4444"]'); + + if (server.oos) { + if (!existingBadge) { + // Insert badge after the INSTANT badge + const instantBadge = card.querySelector('.server-badge.instant'); + if (instantBadge) { + const oosBadge = document.createElement('div'); + oosBadge.className = 'server-badge'; + oosBadge.style.cssText = 'background:#ef4444;color:#fff;margin-left:8px;'; + oosBadge.textContent = 'OOS'; + instantBadge.after(oosBadge); + } + } + } else { + if (existingBadge) { + existingBadge.remove(); + } + } } // Render Custom Servers @@ -3849,6 +3954,8 @@ var wpcf7 = { // Reset Customization State isInstantCustomized = false; document.getElementById('instantOptions').style.display = 'none'; + const instantContainer = document.getElementById('instantDynamicConfigContainer'); + if (instantContainer) instantContainer.innerHTML = ''; document.getElementById('instantServersGrid').style.display = 'grid'; // Ensure grid is visible updateSummary(); @@ -3877,59 +3984,43 @@ var wpcf7 = { serversGrid.style.display = 'none'; instantOptions.style.display = 'block'; - // Fetch options if container is empty OR if we want to refresh ensures all options are loaded - // Checking if container is empty is usually enough unless server changed. - // However, to be safe and ensure "all customizable options" are shown as requested: - if (container.innerHTML.trim() === '') { - loader.style.display = 'flex'; + loader.style.display = 'flex'; + container.innerHTML = ''; + + configState = {}; + storageSelection = {}; + + try { + let scrapeUrl = selectedServer.orderUrl; + if (scrapeUrl.includes('&configoption')) { + scrapeUrl = scrapeUrl.split('&configoption')[0]; + } - // Reset selection state - configState = {}; - storageSelection = {}; - - try { - // 1. CLEAN THE URL: Remove specific config options to get the full configuration page - let scrapeUrl = selectedServer.orderUrl; - if (scrapeUrl.includes('&configoption')) { - scrapeUrl = scrapeUrl.split('&configoption')[0]; - } - - // 2. Parse Pre-selected Options from the Original URL - const preselected = {}; - const urlParams = new URLSearchParams(selectedServer.orderUrl); - for (const [key, value] of urlParams.entries()) { - if (key.startsWith('configoption[')) { - // Extract ID: configoption[34] -> 34 - const idMatch = key.match(/\[(\d+)\]/); - if (idMatch) { - preselected[idMatch[1]] = value; - } + 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; } } - console.log("Pre-selected options for instant:", preselected); - - // Use the scraper with the Clean URL - const optionsData = await scraper.getProductConfig(scrapeUrl, selectedServer); - - if (optionsData && optionsData.options.length > 0) { - renderDynamicOptions(optionsData.options, true, preselected); - } else { - // Fallback if optionsData is null or empty - console.warn("No live options found, using fallback."); - renderFallbackOptions(container); - - // Add a visual warning - const warning = document.createElement('div'); - warning.style.cssText = "background: #fff3cd; color: #856404; padding: 10px; margin-bottom: 15px; border-radius: 4px; font-size: 0.9rem; text-align: center;"; - warning.textContent = "Could not load live customization options. Standard configuration shown."; - container.insertBefore(warning, container.firstChild); - } - } catch (err) { - console.error("Error in customize click:", err); - renderFallbackOptions(container); - } finally { - loader.style.display = 'none'; } + + const optionsData = await scraper.getProductConfig(scrapeUrl, selectedServer); + if (optionsData && optionsData.options.length > 0) { + renderDynamicOptions(optionsData.options, true, preselected); + } else { + renderFallbackOptions(container); + const warning = document.createElement('div'); + warning.style.cssText = "background: #fff3cd; color: #856404; padding: 10px; margin-bottom: 15px; border-radius: 4px; font-size: 0.9rem; text-align: center;"; + warning.textContent = "Could not load live customization options. Standard configuration shown."; + container.insertBefore(warning, container.firstChild); + } + } catch (err) { + renderFallbackOptions(container); + } finally { + loader.style.display = 'none'; } updateSummary(); @@ -3939,7 +4030,6 @@ var wpcf7 = { async function selectCustomServer(id) { selectedServer = customServers.find(s => s.id === id); renderCustomServers(); - updateSpecs(selectedServer); document.getElementById('instantToggleContainer').style.display = 'none'; @@ -3965,6 +4055,16 @@ var wpcf7 = { loader.style.display = 'none'; if (optionsData && optionsData.options.length > 0) { + const netOpt = optionsData.options.find(o => o.type === 'network' || (o.label && o.label.toLowerCase().includes('network')) || (o.label && o.label.toLowerCase().includes('uplink'))); + if (netOpt && netOpt.values && netOpt.values.length > 0) { + const defVal = netOpt.values[0]; + verifiedDefaults.network = defVal.text; + configState[netOpt.label] = defVal.price || 0; + configIds[netOpt.id] = defVal.id; + } else { + verifiedDefaults.network = null; + } + updateSpecs(selectedServer); renderDynamicOptions(optionsData.options); } else { // Fallback if scrape fails @@ -4154,7 +4254,7 @@ var wpcf7 = { if(type === 'ram') { configState['RAM Configuration'] = price; document.getElementById('summaryRam').textContent = name; - const specRam = document.getElementById('spec-ram-capacity'); + const specRam = document.getElementById('spec-ram-capacity-hero'); if(specRam) specRam.innerText = name; } else if(type === 'location') { configState['Location'] = price; @@ -4162,7 +4262,7 @@ var wpcf7 = { } else if(type === 'network') { configState['Network'] = price; document.getElementById('summaryNetwork').textContent = name; - const specNetwork = document.getElementById('spec-network-speed'); + const specNetwork = document.getElementById('spec-network-speed-hero'); if(specNetwork) specNetwork.innerText = name; } @@ -4255,7 +4355,7 @@ var wpcf7 = { document.getElementById('summaryStorage').innerText = summaryText; // Also update specs table if visible - const specStorage = document.getElementById('spec-storage-config'); + const specStorage = document.getElementById('spec-storage-config-hero'); if(specStorage) specStorage.innerText = summaryParts.length > 0 ? summaryParts.join(' + ') : 'Configurable'; updateSummary(); @@ -4618,7 +4718,7 @@ var wpcf7 = { const storageText = storageNames.length > 0 ? storageNames.join(' + ') : 'None'; document.getElementById('summaryStorage').textContent = storageText; - const specStorage = document.getElementById('spec-storage-config'); + const specStorage = document.getElementById('spec-storage-config-hero'); if(specStorage) specStorage.innerText = storageText; }; @@ -4645,13 +4745,13 @@ var wpcf7 = { if (opt.type === 'ram') { const cleanName = val.text.replace(/\s?\(.*?\)/, ''); document.getElementById('summaryRam').textContent = cleanName; - const specRam = document.getElementById('spec-ram-capacity'); + const specRam = document.getElementById('spec-ram-capacity-hero'); if(specRam) specRam.innerText = cleanName; } if (opt.type === 'location') document.getElementById('summaryLocation').textContent = val.text; if (opt.type === 'network') { document.getElementById('summaryNetwork').textContent = val.text; - const specNet = document.getElementById('spec-network-speed'); + const specNet = document.getElementById('spec-network-speed-hero'); if(specNet) specNet.innerText = val.text; } @@ -4709,7 +4809,7 @@ var wpcf7 = { document.getElementById('summaryStorage').textContent = storageText; // Update Tech Specs - const specStorage = document.getElementById('spec-storage-config'); + const specStorage = document.getElementById('spec-storage-config-hero'); if(specStorage) specStorage.innerText = storageText; } @@ -4764,6 +4864,22 @@ var wpcf7 = { if(countEl) countEl.textContent = `${filled}/${slots.length}`; } + 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); + Object.entries(configIds).forEach(([id, val]) => { + if (id && val) p.set(`configoption[${id}]`, val); + }); + p.set('billingcycle', 'monthly'); + return u.origin + u.pathname + '?' + p.toString(); + } catch (e) { + return selectedServer.orderUrl; + } + } + // Update Summary function updateSummary() { const pill = document.getElementById('summaryHeaderPill'); @@ -4811,7 +4927,8 @@ var wpcf7 = { const totalPrice = selectedServer.price + addonPrice; document.getElementById('totalPrice').textContent = selectedServer.currency + totalPrice.toFixed(2); - btnDeployInstant.href = selectedServer.orderUrl; + const customUrl = buildInstantOrderUrl(); + btnDeployInstant.href = customUrl || selectedServer.orderUrl; // Update Button Labels for Customized State const mainLabel = btnDeployInstant.querySelector('.main-label'); @@ -4825,9 +4942,9 @@ var wpcf7 = { pill.style.color = 'white'; subtext.textContent = 'Ready in 15 minutes'; document.getElementById('totalPrice').textContent = selectedServer.currency + selectedServer.price; - + btnDeployInstant.href = selectedServer.orderUrl; - + // Update Button Labels for Default State const mainLabel = btnDeployInstant.querySelector('.main-label'); const subLabel = btnDeployInstant.querySelector('.sub-label'); @@ -4863,9 +4980,42 @@ var wpcf7 = { if (selectedServer.configUrl) { configureBtn.href = selectedServer.configUrl; } + + const netEl = document.getElementById('summaryNetwork'); + if (netEl && (netEl.textContent === '-' || netEl.textContent.trim() === '')) { + netEl.textContent = verifiedDefaults.network || netEl.textContent; + } } } + document.getElementById('btnDeployInstant').addEventListener('click', async (e) => { + if (!selectedServer) return; + e.preventDefault(); + const url = isInstantCustomized ? (buildInstantOrderUrl() || selectedServer.orderUrl) : selectedServer.orderUrl; + + // Check cache first for instant OOS response + let outOfStock = oosCache.get(url); + + // If not cached, check stock (this should rarely happen now due to pre-fetch) + if (outOfStock === undefined) { + try { + outOfStock = await scraper.checkStock(url); + oosCache.set(url, outOfStock); + } catch (err) { + outOfStock = false; + oosCache.set(url, false); + } + } + + if (outOfStock) { + selectedServer.oos = true; + renderInstantServers(); + updateSummary(); + } else { + window.location.href = url; + } + }); + // Tab switching document.querySelectorAll('.config-tab').forEach(tab => { tab.addEventListener('click', () => { @@ -4886,10 +5036,13 @@ var wpcf7 = { renderInstantServers(); renderCustomServers(); + // Trigger initial OOS check for all instant servers on page load + prefetchStockData(); + // Pre-select the first instant server so the UI is active immediately if(instantServers.length > 0) { selectInstantServer(instantServers[0].id); } - \ No newline at end of file +