- Separate storage options handling from individual component matching - Pass storage requirements to pooled grid for proper pre-filling - Improve brand and capacity matching logic for storage drives - Reset slots to None before filling with instant server configuration - Add console warnings for debugging unmatched drives This ensures that when customizing an instant server, the storage configuration is correctly preserved (e.g., 4x Crucial T705 4TB + 2x Kioxia CM7-V 3.2TB) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
5471 lines
255 KiB
HTML
5471 lines
255 KiB
HTML
|
||
<!doctype html>
|
||
<html dir="ltr" lang="en-US" prefix="og: https://ogp.me/ns#">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<link rel="profile" href="https://gmpg.org/xfn/11">
|
||
<title>Bare metal server details - DedicatedNodes</title>
|
||
|
||
<!-- All in One SEO 4.8.7.2 - aioseo.com -->
|
||
<meta name="robots" content="max-image-preview:large" />
|
||
<link rel="canonical" href="https://www.dedicatednodes.io/bare-metal-server-details/" />
|
||
<meta name="generator" content="All in One SEO (AIOSEO) 4.8.7.2" />
|
||
<meta property="og:locale" content="en_US" />
|
||
<meta property="og:site_name" content="DedicatedNodes - High performance bare metal servers for Solana" />
|
||
<meta property="og:type" content="article" />
|
||
<meta property="og:title" content="Bare metal server details - DedicatedNodes" />
|
||
<meta property="og:url" content="https://www.dedicatednodes.io/bare-metal-server-details/" />
|
||
<meta property="og:image" content="https://www.dedicatednodes.io/wp-content/uploads/2024/05/cropped-dedicatednodes_badge.png" />
|
||
<meta property="og:image:secure_url" content="https://www.dedicatednodes.io/wp-content/uploads/2024/05/cropped-dedicatednodes_badge.png" />
|
||
<meta property="og:image:width" content="512" />
|
||
<meta property="og:image:height" content="512" />
|
||
<meta property="article:published_time" content="2025-11-21T14:30:43+00:00" />
|
||
<meta property="article:modified_time" content="2025-11-21T14:30:43+00:00" />
|
||
<meta name="twitter:card" content="summary_large_image" />
|
||
<meta name="twitter:site" content="@dedicatednodes" />
|
||
<meta name="twitter:title" content="Bare metal server details - DedicatedNodes" />
|
||
<meta name="twitter:creator" content="@dedicatednodes" />
|
||
<meta name="twitter:image" content="https://www.dedicatednodes.io/wp-content/uploads/2024/05/cropped-dedicatednodes_badge.png" />
|
||
<script type="application/ld+json" class="aioseo-schema">
|
||
{"@context":"https:\/\/schema.org","@graph":[{"@type":"BreadcrumbList","@id":"https:\/\/www.dedicatednodes.io\/bare-metal-server-details\/#breadcrumblist","itemListElement":[{"@type":"ListItem","@id":"https:\/\/www.dedicatednodes.io#listItem","position":1,"name":"Home","item":"https:\/\/www.dedicatednodes.io","nextItem":{"@type":"ListItem","@id":"https:\/\/www.dedicatednodes.io\/bare-metal-server-details\/#listItem","name":"Bare metal server details"}},{"@type":"ListItem","@id":"https:\/\/www.dedicatednodes.io\/bare-metal-server-details\/#listItem","position":2,"name":"Bare metal server details","previousItem":{"@type":"ListItem","@id":"https:\/\/www.dedicatednodes.io#listItem","name":"Home"}}]},{"@type":"Organization","@id":"https:\/\/www.dedicatednodes.io\/#organization","name":"DedicatedNodes","description":"High performance bare metal servers for Solana","url":"https:\/\/www.dedicatednodes.io\/","logo":{"@type":"ImageObject","url":"https:\/\/www.dedicatednodes.io\/wp-content\/uploads\/2024\/05\/cropped-dedicatednodes_badge.png","@id":"https:\/\/www.dedicatednodes.io\/bare-metal-server-details\/#organizationLogo","width":512,"height":512},"image":{"@id":"https:\/\/www.dedicatednodes.io\/bare-metal-server-details\/#organizationLogo"},"sameAs":["https:\/\/www.x.com\/dedicatednodes"]},{"@type":"WebPage","@id":"https:\/\/www.dedicatednodes.io\/bare-metal-server-details\/#webpage","url":"https:\/\/www.dedicatednodes.io\/bare-metal-server-details\/","name":"Bare metal server details - DedicatedNodes","inLanguage":"en-US","isPartOf":{"@id":"https:\/\/www.dedicatednodes.io\/#website"},"breadcrumb":{"@id":"https:\/\/www.dedicatednodes.io\/bare-metal-server-details\/#breadcrumblist"},"datePublished":"2025-11-21T14:30:43+00:00","dateModified":"2025-11-21T14:30:43+00:00"},{"@type":"WebSite","@id":"https:\/\/www.dedicatednodes.io\/#website","url":"https:\/\/www.dedicatednodes.io\/","name":"DedicatedNodes","description":"High performance bare metal servers for Solana","inLanguage":"en-US","publisher":{"@id":"https:\/\/www.dedicatednodes.io\/#organization"}}]}
|
||
</script>
|
||
<!-- All in One SEO -->
|
||
|
||
<link rel='dns-prefetch' href='//www.dedicatednodes.io' />
|
||
<link rel='dns-prefetch' href='//cdn.jsdelivr.net' />
|
||
<link rel="alternate" type="application/rss+xml" title="DedicatedNodes » Feed" href="https://www.dedicatednodes.io/feed/" />
|
||
<link rel="alternate" type="application/rss+xml" title="DedicatedNodes » Comments Feed" href="https://www.dedicatednodes.io/comments/feed/" />
|
||
<link rel="alternate" title="oEmbed (JSON)" type="application/json+oembed" href="https://www.dedicatednodes.io/wp-json/oembed/1.0/embed?url=https%3A%2F%2Fwww.dedicatednodes.io%2Fbare-metal-server-details%2F" />
|
||
<link rel="alternate" title="oEmbed (XML)" type="text/xml+oembed" href="https://www.dedicatednodes.io/wp-json/oembed/1.0/embed?url=https%3A%2F%2Fwww.dedicatednodes.io%2Fbare-metal-server-details%2F&format=xml" />
|
||
<!-- This site uses the Google Analytics by MonsterInsights plugin v9.10.0 - Using Analytics tracking - https://www.monsterinsights.com/ -->
|
||
<script src="//www.googletagmanager.com/gtag/js?id=G-7S9GL1MN5L" data-cfasync="false" data-wpfc-render="false" async></script>
|
||
<script data-cfasync="false" data-wpfc-render="false">
|
||
var mi_version = '9.10.0';
|
||
var mi_track_user = true;
|
||
var mi_no_track_reason = '';
|
||
var MonsterInsightsDefaultLocations = {"page_location":"https:\/\/www.dedicatednodes.io\/bare-metal-server-details\/"};
|
||
if ( typeof MonsterInsightsPrivacyGuardFilter === 'function' ) {
|
||
var MonsterInsightsLocations = (typeof MonsterInsightsExcludeQuery === 'object') ? MonsterInsightsPrivacyGuardFilter( MonsterInsightsExcludeQuery ) : MonsterInsightsPrivacyGuardFilter( MonsterInsightsDefaultLocations );
|
||
} else {
|
||
var MonsterInsightsLocations = (typeof MonsterInsightsExcludeQuery === 'object') ? MonsterInsightsExcludeQuery : MonsterInsightsDefaultLocations;
|
||
}
|
||
|
||
var disableStrs = [
|
||
'ga-disable-G-7S9GL1MN5L',
|
||
];
|
||
|
||
/* Function to detect opted out users */
|
||
function __gtagTrackerIsOptedOut() {
|
||
for (var index = 0; index < disableStrs.length; index++) {
|
||
if (document.cookie.indexOf(disableStrs[index] + '=true') > -1) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/* Disable tracking if the opt-out cookie exists. */
|
||
if (__gtagTrackerIsOptedOut()) {
|
||
for (var index = 0; index < disableStrs.length; index++) {
|
||
window[disableStrs[index]] = true;
|
||
}
|
||
}
|
||
|
||
/* Opt-out function */
|
||
function __gtagTrackerOptout() {
|
||
for (var index = 0; index < disableStrs.length; index++) {
|
||
document.cookie = disableStrs[index] + '=true; expires=Thu, 31 Dec 2099 23:59:59 UTC; path=/';
|
||
window[disableStrs[index]] = true;
|
||
}
|
||
}
|
||
|
||
if ('undefined' === typeof gaOptout) {
|
||
function gaOptout() {
|
||
__gtagTrackerOptout();
|
||
}
|
||
}
|
||
window.dataLayer = window.dataLayer || [];
|
||
|
||
window.MonsterInsightsDualTracker = {
|
||
helpers: {},
|
||
trackers: {},
|
||
};
|
||
if (mi_track_user) {
|
||
function __gtagDataLayer() {
|
||
dataLayer.push(arguments);
|
||
}
|
||
|
||
function __gtagTracker(type, name, parameters) {
|
||
if (!parameters) {
|
||
parameters = {};
|
||
}
|
||
|
||
if (parameters.send_to) {
|
||
__gtagDataLayer.apply(null, arguments);
|
||
return;
|
||
}
|
||
|
||
if (type === 'event') {
|
||
parameters.send_to = monsterinsights_frontend.v4_id;
|
||
var hookName = name;
|
||
if (typeof parameters['event_category'] !== 'undefined') {
|
||
hookName = parameters['event_category'] + ':' + name;
|
||
}
|
||
|
||
if (typeof MonsterInsightsDualTracker.trackers[hookName] !== 'undefined') {
|
||
MonsterInsightsDualTracker.trackers[hookName](parameters);
|
||
} else {
|
||
__gtagDataLayer('event', name, parameters);
|
||
}
|
||
|
||
} else {
|
||
__gtagDataLayer.apply(null, arguments);
|
||
}
|
||
}
|
||
|
||
__gtagTracker('js', new Date());
|
||
__gtagTracker('set', {
|
||
'developer_id.dZGIzZG': true,
|
||
});
|
||
if ( MonsterInsightsLocations.page_location ) {
|
||
__gtagTracker('set', MonsterInsightsLocations);
|
||
}
|
||
__gtagTracker('config', 'G-7S9GL1MN5L', {"forceSSL":"true","link_attribution":"true","page_path":location.pathname + location.search + location.hash} );
|
||
window.gtag = __gtagTracker; (function () {
|
||
/* https://developers.google.com/analytics/devguides/collection/analyticsjs/ */
|
||
/* ga and __gaTracker compatibility shim. */
|
||
var noopfn = function () {
|
||
return null;
|
||
};
|
||
var newtracker = function () {
|
||
return new Tracker();
|
||
};
|
||
var Tracker = function () {
|
||
return null;
|
||
};
|
||
var p = Tracker.prototype;
|
||
p.get = noopfn;
|
||
p.set = noopfn;
|
||
p.send = function () {
|
||
var args = Array.prototype.slice.call(arguments);
|
||
args.unshift('send');
|
||
__gaTracker.apply(null, args);
|
||
};
|
||
var __gaTracker = function () {
|
||
var len = arguments.length;
|
||
if (len === 0) {
|
||
return;
|
||
}
|
||
var f = arguments[len - 1];
|
||
if (typeof f !== 'object' || f === null || typeof f.hitCallback !== 'function') {
|
||
if ('send' === arguments[0]) {
|
||
var hitConverted, hitObject = false, action;
|
||
if ('event' === arguments[1]) {
|
||
if ('undefined' !== typeof arguments[3]) {
|
||
hitObject = {
|
||
'eventAction': arguments[3],
|
||
'eventCategory': arguments[2],
|
||
'eventLabel': arguments[4],
|
||
'value': arguments[5] ? arguments[5] : 1,
|
||
}
|
||
}
|
||
}
|
||
if ('pageview' === arguments[1]) {
|
||
if ('undefined' !== typeof arguments[2]) {
|
||
hitObject = {
|
||
'eventAction': 'page_view',
|
||
'page_path': arguments[2],
|
||
}
|
||
}
|
||
}
|
||
if (typeof arguments[2] === 'object') {
|
||
hitObject = arguments[2];
|
||
}
|
||
if (typeof arguments[5] === 'object') {
|
||
Object.assign(hitObject, arguments[5]);
|
||
}
|
||
if ('undefined' !== typeof arguments[1].hitType) {
|
||
hitObject = arguments[1];
|
||
if ('pageview' === hitObject.hitType) {
|
||
hitObject.eventAction = 'page_view';
|
||
}
|
||
}
|
||
if (hitObject) {
|
||
action = 'timing' === arguments[1].hitType ? 'timing_complete' : hitObject.eventAction;
|
||
hitConverted = mapArgs(hitObject);
|
||
__gtagTracker('event', action, hitConverted);
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
function mapArgs(args) {
|
||
var arg, hit = {};
|
||
var gaMap = {
|
||
'eventCategory': 'event_category',
|
||
'eventAction': 'event_action',
|
||
'eventLabel': 'event_label',
|
||
'eventValue': 'event_value',
|
||
'nonInteraction': 'non_interaction',
|
||
'timingCategory': 'event_category',
|
||
'timingVar': 'name',
|
||
'timingValue': 'value',
|
||
'timingLabel': 'event_label',
|
||
'page': 'page_path',
|
||
'location': 'page_location',
|
||
'title': 'page_title',
|
||
'referrer' : 'page_referrer',
|
||
};
|
||
for (arg in args) {
|
||
if (!(!args.hasOwnProperty(arg) || !gaMap.hasOwnProperty(arg))) {
|
||
hit[gaMap[arg]] = args[arg];
|
||
} else {
|
||
hit[arg] = args[arg];
|
||
}
|
||
}
|
||
return hit;
|
||
}
|
||
|
||
try {
|
||
f.hitCallback();
|
||
} catch (ex) {
|
||
}
|
||
};
|
||
__gaTracker.create = newtracker;
|
||
__gaTracker.getByName = newtracker;
|
||
__gaTracker.getAll = function () {
|
||
return [];
|
||
};
|
||
__gaTracker.remove = noopfn;
|
||
__gaTracker.loaded = true;
|
||
window['__gaTracker'] = __gaTracker;
|
||
})();
|
||
} else {
|
||
console.log("");
|
||
(function () {
|
||
function __gtagTracker() {
|
||
return null;
|
||
}
|
||
|
||
window['__gtagTracker'] = __gtagTracker;
|
||
window['gtag'] = __gtagTracker;
|
||
})();
|
||
}
|
||
</script>
|
||
<!-- / Google Analytics by MonsterInsights -->
|
||
<style id='wp-img-auto-sizes-contain-inline-css'>
|
||
img:is([sizes=auto i],[sizes^="auto," i]){contain-intrinsic-size:3000px 1500px}
|
||
/*# sourceURL=wp-img-auto-sizes-contain-inline-css */
|
||
</style>
|
||
<style id='wp-emoji-styles-inline-css'>
|
||
|
||
img.wp-smiley, img.emoji {
|
||
display: inline !important;
|
||
border: none !important;
|
||
box-shadow: none !important;
|
||
height: 1em !important;
|
||
width: 1em !important;
|
||
margin: 0 0.07em !important;
|
||
vertical-align: -0.1em !important;
|
||
background: none !important;
|
||
padding: 0 !important;
|
||
}
|
||
/*# sourceURL=wp-emoji-styles-inline-css */
|
||
</style>
|
||
<style id='wp-block-library-inline-css'>
|
||
:root{--wp-block-synced-color:#7a00df;--wp-block-synced-color--rgb:122,0,223;--wp-bound-block-color:var(--wp-block-synced-color);--wp-editor-canvas-background:#ddd;--wp-admin-theme-color:#007cba;--wp-admin-theme-color--rgb:0,124,186;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-10--rgb:0,107,160.5;--wp-admin-theme-color-darker-20:#005a87;--wp-admin-theme-color-darker-20--rgb:0,90,135;--wp-admin-border-width-focus:2px}@media (min-resolution:192dpi){:root{--wp-admin-border-width-focus:1.5px}}.wp-element-button{cursor:pointer}:root .has-very-light-gray-background-color{background-color:#eee}:root .has-very-dark-gray-background-color{background-color:#313131}:root .has-very-light-gray-color{color:#eee}:root .has-very-dark-gray-color{color:#313131}:root .has-vivid-green-cyan-to-vivid-cyan-blue-gradient-background{background:linear-gradient(135deg,#00d084,#0693e3)}:root .has-purple-crush-gradient-background{background:linear-gradient(135deg,#34e2e4,#4721fb 50%,#ab1dfe)}:root .has-hazy-dawn-gradient-background{background:linear-gradient(135deg,#faaca8,#dad0ec)}:root .has-subdued-olive-gradient-background{background:linear-gradient(135deg,#fafae1,#67a671)}:root .has-atomic-cream-gradient-background{background:linear-gradient(135deg,#fdd79a,#004a59)}:root .has-nightshade-gradient-background{background:linear-gradient(135deg,#330968,#31cdcf)}:root .has-midnight-gradient-background{background:linear-gradient(135deg,#020381,#2874fc)}:root{--wp--preset--font-size--normal:16px;--wp--preset--font-size--huge:42px}.has-regular-font-size{font-size:1em}.has-larger-font-size{font-size:2.625em}.has-normal-font-size{font-size:var(--wp--preset--font-size--normal)}.has-huge-font-size{font-size:var(--wp--preset--font-size--huge)}.has-text-align-center{text-align:center}.has-text-align-left{text-align:left}.has-text-align-right{text-align:right}.has-fit-text{white-space:nowrap!important}#end-resizable-editor-section{display:none}.aligncenter{clear:both}.items-justified-left{justify-content:flex-start}.items-justified-center{justify-content:center}.items-justified-right{justify-content:flex-end}.items-justified-space-between{justify-content:space-between}.screen-reader-text{clip:rect(1px,1px,1px,1px);word-wrap:normal!important;border:0;-webkit-clip-path:inset(50%);clip-path:inset(50%);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.screen-reader-text:focus{clip:auto!important;background-color:#ddd;-webkit-clip-path:none;clip-path:none;color:#444;display:block;font-size:1em;height:auto;left:5px;line-height:normal;padding:15px 23px 14px;text-decoration:none;top:5px;width:auto;z-index:100000}html :where(.has-border-color){border-style:solid}html :where([style*=border-top-color]){border-top-style:solid}html :where([style*=border-right-color]){border-right-style:solid}html :where([style*=border-bottom-color]){border-bottom-style:solid}html :where([style*=border-left-color]){border-left-style:solid}html :where([style*=border-width]){border-style:solid}html :where([style*=border-top-width]){border-top-style:solid}html :where([style*=border-right-width]){border-right-style:solid}html :where([style*=border-bottom-width]){border-bottom-style:solid}html :where([style*=border-left-width]){border-left-style:solid}html :where(img[class*=wp-image-]){height:auto;max-width:100%}
|
||
|
||
/*# sourceURL=wp-block-library-inline-css */
|
||
</style><style id='global-styles-inline-css'>
|
||
:root{--wp--preset--aspect-ratio--square: 1;--wp--preset--aspect-ratio--4-3: 4/3;--wp--preset--aspect-ratio--3-4: 3/4;--wp--preset--aspect-ratio--3-2: 3/2;--wp--preset--aspect-ratio--2-3: 2/3;--wp--preset--aspect-ratio--16-9: 16/9;--wp--preset--aspect-ratio--9-16: 9/16;--wp--preset--color--black: #000000;--wp--preset--color--cyan-bluish-gray: #abb8c3;--wp--preset--color--white: #ffffff;--wp--preset--color--pale-pink: #f78da7;--wp--preset--color--vivid-red: #cf2e2e;--wp--preset--color--luminous-vivid-orange: #ff6900;--wp--preset--color--luminous-vivid-amber: #fcb900;--wp--preset--color--light-green-cyan: #7bdcb5;--wp--preset--color--vivid-green-cyan: #00d084;--wp--preset--color--pale-cyan-blue: #8ed1fc;--wp--preset--color--vivid-cyan-blue: #0693e3;--wp--preset--color--vivid-purple: #9b51e0;--wp--preset--gradient--vivid-cyan-blue-to-vivid-purple: linear-gradient(135deg,rgb(6,147,227) 0%,rgb(155,81,224) 100%);--wp--preset--gradient--light-green-cyan-to-vivid-green-cyan: linear-gradient(135deg,rgb(122,220,180) 0%,rgb(0,208,130) 100%);--wp--preset--gradient--luminous-vivid-amber-to-luminous-vivid-orange: linear-gradient(135deg,rgb(252,185,0) 0%,rgb(255,105,0) 100%);--wp--preset--gradient--luminous-vivid-orange-to-vivid-red: linear-gradient(135deg,rgb(255,105,0) 0%,rgb(207,46,46) 100%);--wp--preset--gradient--very-light-gray-to-cyan-bluish-gray: linear-gradient(135deg,rgb(238,238,238) 0%,rgb(169,184,195) 100%);--wp--preset--gradient--cool-to-warm-spectrum: linear-gradient(135deg,rgb(74,234,220) 0%,rgb(151,120,209) 20%,rgb(207,42,186) 40%,rgb(238,44,130) 60%,rgb(251,105,98) 80%,rgb(254,248,76) 100%);--wp--preset--gradient--blush-light-purple: linear-gradient(135deg,rgb(255,206,236) 0%,rgb(152,150,240) 100%);--wp--preset--gradient--blush-bordeaux: linear-gradient(135deg,rgb(254,205,165) 0%,rgb(254,45,45) 50%,rgb(107,0,62) 100%);--wp--preset--gradient--luminous-dusk: linear-gradient(135deg,rgb(255,203,112) 0%,rgb(199,81,192) 50%,rgb(65,88,208) 100%);--wp--preset--gradient--pale-ocean: linear-gradient(135deg,rgb(255,245,203) 0%,rgb(182,227,212) 50%,rgb(51,167,181) 100%);--wp--preset--gradient--electric-grass: linear-gradient(135deg,rgb(202,248,128) 0%,rgb(113,206,126) 100%);--wp--preset--gradient--midnight: linear-gradient(135deg,rgb(2,3,129) 0%,rgb(40,116,252) 100%);--wp--preset--font-size--small: 13px;--wp--preset--font-size--medium: 20px;--wp--preset--font-size--large: 36px;--wp--preset--font-size--x-large: 42px;--wp--preset--spacing--20: 0.44rem;--wp--preset--spacing--30: 0.67rem;--wp--preset--spacing--40: 1rem;--wp--preset--spacing--50: 1.5rem;--wp--preset--spacing--60: 2.25rem;--wp--preset--spacing--70: 3.38rem;--wp--preset--spacing--80: 5.06rem;--wp--preset--shadow--natural: 6px 6px 9px rgba(0, 0, 0, 0.2);--wp--preset--shadow--deep: 12px 12px 50px rgba(0, 0, 0, 0.4);--wp--preset--shadow--sharp: 6px 6px 0px rgba(0, 0, 0, 0.2);--wp--preset--shadow--outlined: 6px 6px 0px -3px rgba(255, 255, 255, 1), 6px 6px rgba(0, 0, 0, 1);--wp--preset--shadow--crisp: 6px 6px 0px rgba(0, 0, 0, 1);}:where(.is-layout-flex){gap: 0.5em;}:where(.is-layout-grid){gap: 0.5em;}body .is-layout-flex{display: flex;}body .is-layout-flex{flex-wrap: wrap;align-items: center;}body .is-layout-flex > *{margin: 0;}body .is-layout-grid{display: grid;}body .is-layout-grid > *{margin: 0;}:where(.wp-block-columns.is-layout-flex){gap: 2em;}:where(.wp-block-columns.is-layout-grid){gap: 2em;}:where(.wp-block-post-template.is-layout-flex){gap: 1.25em;}:where(.wp-block-post-template.is-layout-grid){gap: 1.25em;}.has-black-color{color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-color{color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-color{color: var(--wp--preset--color--white) !important;}.has-pale-pink-color{color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-color{color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-color{color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-color{color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-color{color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-color{color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-color{color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-color{color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-color{color: var(--wp--preset--color--vivid-purple) !important;}.has-black-background-color{background-color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-background-color{background-color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-background-color{background-color: var(--wp--preset--color--white) !important;}.has-pale-pink-background-color{background-color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-background-color{background-color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-background-color{background-color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-background-color{background-color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-background-color{background-color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-background-color{background-color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-background-color{background-color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-background-color{background-color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-background-color{background-color: var(--wp--preset--color--vivid-purple) !important;}.has-black-border-color{border-color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-border-color{border-color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-border-color{border-color: var(--wp--preset--color--white) !important;}.has-pale-pink-border-color{border-color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-border-color{border-color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-border-color{border-color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-border-color{border-color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-border-color{border-color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-border-color{border-color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-border-color{border-color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-border-color{border-color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-border-color{border-color: var(--wp--preset--color--vivid-purple) !important;}.has-vivid-cyan-blue-to-vivid-purple-gradient-background{background: var(--wp--preset--gradient--vivid-cyan-blue-to-vivid-purple) !important;}.has-light-green-cyan-to-vivid-green-cyan-gradient-background{background: var(--wp--preset--gradient--light-green-cyan-to-vivid-green-cyan) !important;}.has-luminous-vivid-amber-to-luminous-vivid-orange-gradient-background{background: var(--wp--preset--gradient--luminous-vivid-amber-to-luminous-vivid-orange) !important;}.has-luminous-vivid-orange-to-vivid-red-gradient-background{background: var(--wp--preset--gradient--luminous-vivid-orange-to-vivid-red) !important;}.has-very-light-gray-to-cyan-bluish-gray-gradient-background{background: var(--wp--preset--gradient--very-light-gray-to-cyan-bluish-gray) !important;}.has-cool-to-warm-spectrum-gradient-background{background: var(--wp--preset--gradient--cool-to-warm-spectrum) !important;}.has-blush-light-purple-gradient-background{background: var(--wp--preset--gradient--blush-light-purple) !important;}.has-blush-bordeaux-gradient-background{background: var(--wp--preset--gradient--blush-bordeaux) !important;}.has-luminous-dusk-gradient-background{background: var(--wp--preset--gradient--luminous-dusk) !important;}.has-pale-ocean-gradient-background{background: var(--wp--preset--gradient--pale-ocean) !important;}.has-electric-grass-gradient-background{background: var(--wp--preset--gradient--electric-grass) !important;}.has-midnight-gradient-background{background: var(--wp--preset--gradient--midnight) !important;}.has-small-font-size{font-size: var(--wp--preset--font-size--small) !important;}.has-medium-font-size{font-size: var(--wp--preset--font-size--medium) !important;}.has-large-font-size{font-size: var(--wp--preset--font-size--large) !important;}.has-x-large-font-size{font-size: var(--wp--preset--font-size--x-large) !important;}
|
||
/*# sourceURL=global-styles-inline-css */
|
||
</style>
|
||
|
||
<style id='classic-theme-styles-inline-css'>
|
||
/*! This file is auto-generated */
|
||
.wp-block-button__link{color:#fff;background-color:#32373c;border-radius:9999px;box-shadow:none;text-decoration:none;padding:calc(.667em + 2px) calc(1.333em + 2px);font-size:1.125em}.wp-block-file__button{background:#32373c;color:#fff;text-decoration:none}
|
||
/*# sourceURL=/wp-includes/css/classic-themes.min.css */
|
||
</style>
|
||
<link rel='stylesheet' id='contact-form-7-css' href='https://www.dedicatednodes.io/wp-content/plugins/contact-form-7/includes/css/styles.css?ver=6.1.2' media='all' />
|
||
<link rel='stylesheet' id='wpddn-bootstrap-css' href='https://www.dedicatednodes.io/wp-content/themes/dedicatednodes/assets/css/bootstrap.min.css?ver=1764837370' media='all' />
|
||
<link rel='stylesheet' id='wpddn-aos-css' href='https://www.dedicatednodes.io/wp-content/themes/dedicatednodes/assets/css/aos.css?ver=1764837370' media='all' />
|
||
<link rel='stylesheet' id='wpddn-public-css' href='https://www.dedicatednodes.io/wp-content/themes/dedicatednodes/assets/css/wpddn-public.css?ver=1764837370' media='all' />
|
||
<link rel='stylesheet' id='dedicatednodes-style-css' href='https://www.dedicatednodes.io/wp-content/themes/dedicatednodes/style.css?ver=1764837370' media='all' />
|
||
<link rel='stylesheet' id='wpddn-font-awesome-css' href='https://www.dedicatednodes.io/wp-content/themes/dedicatednodes/assets/css/font-awesome.min.css?ver=1764837370' media='all' />
|
||
<link rel='stylesheet' id='wpddn-responsive-css' href='https://www.dedicatednodes.io/wp-content/themes/dedicatednodes/assets/css/responsive.css?ver=1764837370' media='all' />
|
||
<link rel='stylesheet' id='intl-tel-input-css' href='https://cdn.jsdelivr.net/npm/intl-tel-input@22.0.2/build/css/intlTelInput.css?ver=1764837370' media='all' />
|
||
<style id='imh-6310-head-css-inline-css'>
|
||
.imh-6310-point-icons{display: none}
|
||
/*# sourceURL=imh-6310-head-css-inline-css */
|
||
</style>
|
||
<script src="https://www.dedicatednodes.io/wp-content/plugins/google-analytics-for-wordpress/assets/js/frontend-gtag.min.js?ver=9.10.0" id="monsterinsights-frontend-script-js" async data-wp-strategy="async"></script>
|
||
<script data-cfasync="false" data-wpfc-render="false" id='monsterinsights-frontend-script-js-extra'>var monsterinsights_frontend = {"js_events_tracking":"true","download_extensions":"doc,pdf,ppt,zip,xls,docx,pptx,xlsx","inbound_paths":"[]","home_url":"https:\/\/www.dedicatednodes.io","hash_tracking":"true","v4_id":"G-7S9GL1MN5L"};</script>
|
||
<script src="https://www.dedicatednodes.io/wp-includes/js/jquery/jquery.min.js?ver=3.7.1" id="jquery-core-js"></script>
|
||
<script src="https://www.dedicatednodes.io/wp-includes/js/jquery/jquery-migrate.min.js?ver=3.4.1" id="jquery-migrate-js"></script>
|
||
<link rel="https://api.w.org/" href="https://www.dedicatednodes.io/wp-json/" /><link rel="alternate" title="JSON" type="application/json" href="https://www.dedicatednodes.io/wp-json/wp/v2/pages/2877" /><link rel="EditURI" type="application/rsd+xml" title="RSD" href="https://www.dedicatednodes.io/xmlrpc.php?rsd" />
|
||
<meta name="generator" content="WordPress 6.9" />
|
||
<link rel='shortlink' href='https://www.dedicatednodes.io/?p=2877' />
|
||
<style type="text/css">
|
||
.site-title,
|
||
.site-description {
|
||
position: absolute;
|
||
clip: rect(1px, 1px, 1px, 1px);
|
||
}
|
||
</style>
|
||
<link rel="icon" href="https://www.dedicatednodes.io/wp-content/uploads/2024/05/cropped-dedicatednodes_badge-32x32.png" sizes="32x32" />
|
||
<link rel="icon" href="https://www.dedicatednodes.io/wp-content/uploads/2024/05/cropped-dedicatednodes_badge-192x192.png" sizes="192x192" />
|
||
<link rel="apple-touch-icon" href="https://www.dedicatednodes.io/wp-content/uploads/2024/05/cropped-dedicatednodes_badge-180x180.png" />
|
||
<meta name="msapplication-TileImage" content="https://www.dedicatednodes.io/wp-content/uploads/2024/05/cropped-dedicatednodes_badge-270x270.png" />
|
||
<link rel="stylesheet"href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" />
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||
</head>
|
||
<style>
|
||
:root {
|
||
--brand-primary: #2C79FF;
|
||
/* Updated Blue */
|
||
--brand-cyan: #00D4FF;
|
||
--brand-dark: #0A1628;
|
||
--text-primary: #1a1a2e;
|
||
--text-secondary: #6b7280;
|
||
--bg-light: #f8fafc;
|
||
--border-color: #e5e7eb;
|
||
--success: #22c55e;
|
||
--warning: #f59e0b;
|
||
--instant-color: #10b981;
|
||
--custom-color: #8b5cf6;
|
||
--lavender-badge: #d8b4fe;
|
||
--font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||
}
|
||
|
||
body {
|
||
font-family: var(--font-family-base) !important;
|
||
-webkit-font-smoothing: antialiased;
|
||
-moz-osx-font-smoothing: grayscale;
|
||
padding-top: 80px; /* Account for fixed header */
|
||
}
|
||
|
||
/* --- UNIFIED FONT & BUTTON ECOSYSTEM --- */
|
||
/* Force Inter font on all interactive elements to fix "weird fonts" */
|
||
body, button, input, select, textarea, .btn, .navbar-brand, .nav-link, .h1, .h2, .h3, .h4, .h5, .h6 {
|
||
font-family: 'Inter', sans-serif !important;
|
||
}
|
||
|
||
/* Standardize all buttons */
|
||
.btn, .btn-primary, .btn-success, .btn-secondary, .btn-order, .btn-cta-white, .btn-cta-outline {
|
||
border-radius: 8px !important;
|
||
font-weight: 600 !important;
|
||
letter-spacing: 0.01em;
|
||
text-transform: none !important;
|
||
transition: all 0.2s ease-in-out;
|
||
padding: 0.75rem 1.5rem;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
/* Primary Action Buttons (Blue) */
|
||
.btn-primary, .btn-success, .btn-order {
|
||
background-color: var(--brand-primary) !important;
|
||
border: 1px solid var(--brand-primary) !important;
|
||
color: white !important;
|
||
}
|
||
|
||
.btn-primary:hover, .btn-success:hover, .btn-order:hover {
|
||
background-color: #1b66e6 !important;
|
||
border-color: #1b66e6 !important;
|
||
color: white !important;
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(44, 121, 255, 0.25);
|
||
}
|
||
|
||
/* Secondary Buttons (White/Light) */
|
||
.btn-secondary {
|
||
background-color: white !important;
|
||
border: 1px solid var(--border-color) !important;
|
||
color: var(--brand-dark) !important;
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background-color: #f8fafc !important;
|
||
border-color: var(--brand-primary) !important;
|
||
color: var(--brand-primary) !important;
|
||
}
|
||
|
||
/* Outline Buttons */
|
||
.btn-outline, .btn-cta-outline {
|
||
background: transparent !important;
|
||
border: 1px solid currentColor !important;
|
||
}
|
||
|
||
h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
|
||
font-family: var(--font-family-base) !important;
|
||
}
|
||
|
||
/* Header Styles - Overrides for Bootstrap Navbar */
|
||
#navbar_main {
|
||
background-color: white;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||
}
|
||
|
||
/* Hero Section - Light Mode */
|
||
.server-hero {
|
||
background-color: #f0f9ff;
|
||
background: linear-gradient(180deg, #f0f9ff 0%, #e0f2fe 100%);
|
||
color: var(--brand-dark);
|
||
padding: 8rem 0 6rem; /* Account for fixed header */
|
||
position: relative;
|
||
margin-top: 0;
|
||
}
|
||
|
||
.hero-breadcrumb {
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
.breadcrumb-list {
|
||
display: flex;
|
||
list-style: none;
|
||
padding: 0;
|
||
margin: 0;
|
||
gap: 0.5rem;
|
||
font-size: 0.85rem;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.breadcrumb-list a {
|
||
color: var(--text-secondary);
|
||
text-decoration: none;
|
||
}
|
||
|
||
.breadcrumb-list li::after {
|
||
content: "/";
|
||
margin-left: 0.5rem;
|
||
opacity: 0.5;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.breadcrumb-list li:last-child::after {
|
||
content: "";
|
||
}
|
||
|
||
.breadcrumb-list li.current {
|
||
color: var(--brand-dark);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.hero-content {
|
||
max-width: 800px;
|
||
}
|
||
|
||
.hero-badge {
|
||
display: inline-block;
|
||
padding: 0.35rem 1rem;
|
||
background: rgba(44, 121, 255, 0.1);
|
||
color: var(--brand-primary);
|
||
border-radius: 50px;
|
||
font-size: 0.85rem;
|
||
font-weight: 600;
|
||
margin-bottom: 1.5rem;
|
||
border: 1px solid rgba(44, 121, 255, 0.2);
|
||
}
|
||
|
||
.hero-title {
|
||
font-size: 3rem;
|
||
font-weight: 800;
|
||
line-height: 1.1;
|
||
margin-bottom: 1rem;
|
||
color: var(--brand-dark);
|
||
}
|
||
|
||
.hero-subtitle {
|
||
font-size: 1.25rem;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
.hero-actions {
|
||
display: flex;
|
||
gap: 1rem;
|
||
margin-bottom: 3rem;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: white;
|
||
color: var(--brand-dark);
|
||
padding: 0.75rem 1.5rem;
|
||
border-radius: 6px;
|
||
font-weight: 600;
|
||
text-decoration: none;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
border: 1px solid var(--border-color);
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: #f8fafc;
|
||
border-color: var(--brand-primary);
|
||
color: var(--brand-primary);
|
||
}
|
||
|
||
.btn-primary svg, .btn-secondary svg {
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.hero-stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 2rem;
|
||
border-top: 1px solid rgba(0,0,0,0.1);
|
||
padding-top: 2rem;
|
||
}
|
||
|
||
.stat-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 1.5rem;
|
||
font-weight: 700;
|
||
color: var(--brand-primary);
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 0.85rem;
|
||
color: var(--text-secondary);
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* Main Specs Styling */
|
||
.main-specs {
|
||
padding: 4rem 0;
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.specs-header {
|
||
text-align: center;
|
||
max-width: 700px;
|
||
margin: 0 auto 3rem;
|
||
}
|
||
|
||
.specs-title {
|
||
font-size: 2rem;
|
||
font-weight: 800;
|
||
color: var(--brand-dark);
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.specs-subtitle {
|
||
color: var(--text-secondary);
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
.specs-layout {
|
||
display: grid;
|
||
grid-template-columns: 1fr 320px;
|
||
gap: 2rem;
|
||
}
|
||
|
||
.specs-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 1.5rem;
|
||
}
|
||
|
||
.spec-card {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 1.5rem;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.03);
|
||
border: 1px solid var(--border-color);
|
||
}
|
||
|
||
.spec-card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
margin-bottom: 1.25rem;
|
||
padding-bottom: 1rem;
|
||
border-bottom: 1px solid #f3f4f6;
|
||
}
|
||
|
||
.spec-icon {
|
||
font-size: 1.5rem;
|
||
}
|
||
|
||
.spec-card-title {
|
||
font-size: 1.1rem;
|
||
font-weight: 700;
|
||
color: var(--brand-dark);
|
||
margin: 0;
|
||
}
|
||
|
||
.spec-detail {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-size: 0.9rem;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.spec-detail-label { color: var(--text-secondary); }
|
||
.spec-detail-value { font-weight: 600; color: var(--text-primary); }
|
||
.spec-detail-value.highlight { color: var(--brand-primary); }
|
||
|
||
.spec-options {
|
||
margin-top: 1rem;
|
||
padding-top: 1rem;
|
||
border-top: 1px dashed #e5e7eb;
|
||
}
|
||
|
||
.spec-option {
|
||
font-size: 0.85rem;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 0.25rem;
|
||
padding-left: 1rem;
|
||
position: relative;
|
||
}
|
||
|
||
.spec-option::before {
|
||
content: "•";
|
||
position: absolute;
|
||
left: 0;
|
||
color: var(--brand-primary);
|
||
}
|
||
|
||
/* Sidebar Cards */
|
||
.pricing-card {
|
||
background: var(--brand-dark);
|
||
color: white;
|
||
border-radius: 12px;
|
||
padding: 2rem;
|
||
text-align: center;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.pricing-label {
|
||
color: rgba(255,255,255,0.6);
|
||
font-size: 0.9rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.pricing-amount {
|
||
font-size: 2.5rem;
|
||
font-weight: 800;
|
||
color: white;
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
|
||
.pricing-period {
|
||
color: rgba(255,255,255,0.6);
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.btn-order {
|
||
background: var(--brand-primary);
|
||
color: white;
|
||
display: block;
|
||
width: 100%;
|
||
padding: 1rem;
|
||
border-radius: 6px;
|
||
font-weight: 700;
|
||
text-decoration: none;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.btn-order:hover {
|
||
background: #2260c4;
|
||
color: white;
|
||
}
|
||
|
||
.sidebar-card {
|
||
background: white;
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 12px;
|
||
padding: 1.5rem;
|
||
}
|
||
|
||
.sidebar-card-title {
|
||
font-size: 1rem;
|
||
font-weight: 700;
|
||
margin-bottom: 1rem;
|
||
color: var(--brand-dark);
|
||
}
|
||
|
||
.quick-feature {
|
||
display: flex;
|
||
gap: 0.75rem;
|
||
margin-bottom: 0.75rem;
|
||
font-size: 0.9rem;
|
||
color: var(--text-secondary);
|
||
align-items: center;
|
||
}
|
||
|
||
/* Features Section */
|
||
.features-section {
|
||
padding: 4rem 0;
|
||
background: white;
|
||
}
|
||
|
||
.features-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 2rem;
|
||
}
|
||
|
||
.feature-card {
|
||
text-align: center;
|
||
padding: 1.5rem;
|
||
border-radius: 12px;
|
||
background: #f8fafc;
|
||
transition: transform 0.2s;
|
||
}
|
||
|
||
.feature-card:hover {
|
||
transform: translateY(-5px);
|
||
}
|
||
|
||
.feature-emoji {
|
||
font-size: 2.5rem;
|
||
margin-bottom: 1rem;
|
||
display: block;
|
||
}
|
||
|
||
.feature-title {
|
||
font-size: 1.1rem;
|
||
font-weight: 700;
|
||
margin-bottom: 0.5rem;
|
||
color: var(--brand-dark);
|
||
}
|
||
|
||
.feature-description {
|
||
font-size: 0.9rem;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
/* Stats Bar */
|
||
.stats-bar {
|
||
background: var(--brand-primary);
|
||
color: white;
|
||
padding: 2rem 0;
|
||
}
|
||
|
||
.stats-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 2rem;
|
||
text-align: center;
|
||
}
|
||
|
||
.stat .stat-value {
|
||
font-size: 2rem;
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
|
||
.stat .stat-label {
|
||
color: rgba(255,255,255,0.8);
|
||
}
|
||
|
||
/* Map Container */
|
||
.map-container {
|
||
height: 500px;
|
||
background: #e5e7eb;
|
||
position: relative;
|
||
margin-bottom: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
#world-map-canvas {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
/* Locations Section Styles */
|
||
.locations {
|
||
padding: 4rem 0 0;
|
||
background: white;
|
||
}
|
||
|
||
.locations-header {
|
||
text-align: center;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.locations-title {
|
||
font-size: 2rem;
|
||
font-weight: 800;
|
||
color: var(--brand-dark);
|
||
}
|
||
|
||
.locations-intro {
|
||
text-align: center;
|
||
max-width: 800px;
|
||
margin: 0 auto 2rem;
|
||
}
|
||
|
||
.locations-description {
|
||
color: var(--text-secondary);
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
.location-badges {
|
||
display: flex;
|
||
justify-content: center;
|
||
flex-wrap: wrap;
|
||
gap: 1rem;
|
||
margin-bottom: 3rem;
|
||
}
|
||
|
||
.loc-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0.5rem 1rem;
|
||
background: #f3f4f6;
|
||
border-radius: 50px;
|
||
font-size: 0.9rem;
|
||
font-weight: 600;
|
||
color: var(--brand-dark);
|
||
border: 1px solid var(--border-color);
|
||
}
|
||
|
||
.loc-badge .flag-icon {
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
.loc-badge-future {
|
||
background: rgba(245, 158, 11, 0.1);
|
||
color: #d97706;
|
||
border-color: rgba(245, 158, 11, 0.2);
|
||
}
|
||
|
||
/* Map Controls / Overlay */
|
||
.map-controls {
|
||
position: absolute;
|
||
bottom: 2rem;
|
||
left: 2rem;
|
||
background: rgba(255, 255, 255, 0.95);
|
||
backdrop-filter: blur(10px);
|
||
padding: 1.5rem;
|
||
border-radius: 12px;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
||
width: 300px;
|
||
z-index: 10;
|
||
}
|
||
|
||
.controls-title {
|
||
font-size: 0.9rem;
|
||
font-weight: 700;
|
||
color: var(--brand-dark);
|
||
margin-bottom: 1rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.pulse {
|
||
width: 8px;
|
||
height: 8px;
|
||
background: #22c55e;
|
||
border-radius: 50%;
|
||
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7);
|
||
animation: pulse-green 2s infinite;
|
||
}
|
||
|
||
@keyframes pulse-green {
|
||
0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7); }
|
||
70% { transform: scale(1); box-shadow: 0 0 0 10px rgba(34, 197, 94, 0); }
|
||
100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); }
|
||
}
|
||
|
||
.activity-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.activity-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-size: 0.8rem;
|
||
padding-bottom: 0.5rem;
|
||
border-bottom: 1px solid #f3f4f6;
|
||
}
|
||
|
||
.activity-row:last-child {
|
||
border-bottom: none;
|
||
padding-bottom: 0;
|
||
}
|
||
|
||
.activity-route { color: var(--text-secondary); }
|
||
.activity-latency { font-weight: 600; color: var(--brand-primary); }
|
||
|
||
|
||
/* Final CTA */
|
||
.final-cta {
|
||
background: var(--brand-dark);
|
||
padding: 5rem 0;
|
||
text-align: center;
|
||
color: white;
|
||
}
|
||
|
||
.cta-title {
|
||
font-size: 2.5rem;
|
||
font-weight: 800;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.cta-subtitle {
|
||
font-size: 1.2rem;
|
||
color: rgba(255,255,255,0.7);
|
||
margin-bottom: 2.5rem;
|
||
}
|
||
|
||
.cta-actions {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.btn-cta-white {
|
||
background: white;
|
||
color: var(--brand-dark);
|
||
padding: 1rem 2rem;
|
||
border-radius: 6px;
|
||
font-weight: 700;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.btn-cta-outline {
|
||
border: 1px solid rgba(255,255,255,0.3);
|
||
color: white;
|
||
padding: 1rem 2rem;
|
||
border-radius: 6px;
|
||
font-weight: 700;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.btn-cta-white:hover {
|
||
background: #f3f4f6;
|
||
}
|
||
|
||
.btn-cta-outline:hover {
|
||
background: rgba(255,255,255,0.1);
|
||
}
|
||
|
||
|
||
/* Tags */
|
||
.tag {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.25rem;
|
||
padding: 0.2rem 0.5rem;
|
||
border-radius: 4px;
|
||
font-size: 0.65rem;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
}
|
||
|
||
.tag-instant {
|
||
background: rgba(16, 185, 129, 0.15);
|
||
color: #059669;
|
||
}
|
||
|
||
.tag-custom {
|
||
background: #E9D5FF;
|
||
/* Lavender */
|
||
color: #7e22ce;
|
||
}
|
||
|
||
/* Calculator Section */
|
||
.calculator-section {
|
||
padding: 2rem 0 4rem;
|
||
background-color: #fff;
|
||
}
|
||
|
||
.calculator-wrapper {
|
||
background: white;
|
||
border-radius: 12px;
|
||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.03);
|
||
overflow: hidden;
|
||
border: 1px solid var(--border-color);
|
||
}
|
||
|
||
.calculator-header {
|
||
display: none; /* Hidden in new design */
|
||
}
|
||
|
||
/* Custom Header bar for wrapper */
|
||
.calc-top-bar {
|
||
background: transparent;
|
||
color: var(--text-secondary);
|
||
padding: 0.75rem 0;
|
||
font-size: 0.85rem;
|
||
font-weight: 500;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.calculator-body {
|
||
display: grid;
|
||
grid-template-columns: 1fr 320px;
|
||
gap: 0;
|
||
}
|
||
|
||
.config-panel {
|
||
padding: 1.5rem;
|
||
border-right: 1px solid var(--border-color);
|
||
}
|
||
|
||
.summary-panel {
|
||
padding: 1.5rem;
|
||
background: white;
|
||
}
|
||
|
||
/* Tabs */
|
||
.config-tabs {
|
||
display: flex;
|
||
gap: 1rem;
|
||
margin-bottom: 2rem;
|
||
justify-content: center;
|
||
background: transparent;
|
||
padding: 0;
|
||
}
|
||
|
||
.config-tab {
|
||
padding: 0.75rem 2rem;
|
||
border: 1px solid var(--border-color);
|
||
background: white;
|
||
border-radius: 50px;
|
||
font-size: 0.95rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
color: var(--text-secondary);
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||
flex: initial;
|
||
}
|
||
|
||
.config-tab:hover {
|
||
background: #f9fafb;
|
||
}
|
||
|
||
.config-tab.active {
|
||
border-color: transparent;
|
||
color: var(--brand-primary);
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||
}
|
||
|
||
.config-tab.active.instant {
|
||
color: var(--brand-primary);
|
||
}
|
||
|
||
.config-tab .badge-count {
|
||
background: #f3f4f6;
|
||
padding: 0.1rem 0.5rem;
|
||
border-radius: 10px;
|
||
font-size: 0.75rem;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
/* Server Cards Grid */
|
||
.servers-section {
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.section-label {
|
||
font-size: 0.8rem;
|
||
font-weight: 700;
|
||
color: var(--text-secondary);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
margin-bottom: 1rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.section-pill {
|
||
padding: 0.25rem 0.75rem;
|
||
border-radius: 20px;
|
||
font-size: 0.75rem;
|
||
font-weight: 700;
|
||
color: white;
|
||
}
|
||
|
||
.section-pill.instant { background: var(--instant-color); }
|
||
.section-pill.custom { background: var(--custom-color); }
|
||
|
||
.servers-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 1rem;
|
||
}
|
||
|
||
.server-option {
|
||
background: white;
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 12px;
|
||
padding: 1.25rem;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
position: relative;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.03);
|
||
}
|
||
|
||
.server-option:hover {
|
||
border-color: var(--brand-primary);
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
.server-option.active {
|
||
border-color: var(--brand-primary);
|
||
box-shadow: 0 0 0 1px var(--brand-primary);
|
||
}
|
||
|
||
/* Internal Card Layout */
|
||
.server-top-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.server-badge {
|
||
font-size: 0.65rem;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
padding: 0.2rem 0.6rem;
|
||
border-radius: 4px;
|
||
display: inline-block;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.server-badge.instant { background: rgba(16, 185, 129, 0.15); color: #059669; }
|
||
.server-badge.custom { background: #E9D5FF; color: #7e22ce; }
|
||
|
||
.server-title {
|
||
font-size: 0.95rem;
|
||
font-weight: 800;
|
||
color: var(--text-primary);
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.server-specs-row {
|
||
display: flex;
|
||
gap: 0.75rem;
|
||
font-size: 0.75rem;
|
||
color: var(--text-secondary);
|
||
flex-wrap: wrap;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.spec-pill {
|
||
background: #f3f4f6;
|
||
padding: 0.15rem 0.5rem;
|
||
border-radius: 4px;
|
||
border: none;
|
||
}
|
||
|
||
.server-meta-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-size: 0.75rem;
|
||
color: var(--text-secondary);
|
||
border-top: 1px solid #f3f4f6;
|
||
padding-top: 0.75rem;
|
||
margin-top: 0.5rem;
|
||
}
|
||
|
||
.server-price-display {
|
||
font-size: 1rem;
|
||
font-weight: 700;
|
||
color: var(--brand-primary);
|
||
}
|
||
|
||
.server-price-display small {
|
||
font-size: 0.7rem;
|
||
font-weight: 400;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
/* --- Config Options --- */
|
||
.config-section {
|
||
margin-top: 2rem;
|
||
animation: fadeIn 0.3s ease;
|
||
}
|
||
|
||
.config-header-bar {
|
||
background: #f8fafc;
|
||
padding: 0.75rem 1rem;
|
||
border-radius: 8px;
|
||
margin-bottom: 1.5rem;
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.config-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||
gap: 1rem;
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
.config-grid.wide {
|
||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||
}
|
||
|
||
.config-card {
|
||
background: white;
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 8px;
|
||
padding: 1rem;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
text-align: center;
|
||
position: relative;
|
||
min-height: 80px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.config-card:hover {
|
||
border-color: var(--brand-primary);
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
||
}
|
||
|
||
.config-card.active {
|
||
border-color: var(--brand-primary);
|
||
box-shadow: 0 0 0 1px var(--brand-primary);
|
||
}
|
||
|
||
.config-card .option-name {
|
||
font-size: 0.85rem;
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
|
||
.config-card .option-price {
|
||
font-size: 0.8rem;
|
||
color: var(--brand-primary);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.config-card .option-price.included {
|
||
color: var(--brand-primary);
|
||
font-size: 0.75rem;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* Storage Slots Visualizer */
|
||
.storage-slots-container {
|
||
margin-top: 0.5rem;
|
||
margin-bottom: 1.5rem;
|
||
padding: 1rem;
|
||
background: #f8fafc;
|
||
border-radius: 8px;
|
||
border: 1px solid var(--border-color);
|
||
}
|
||
|
||
.slots-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-size: 0.85rem;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 0.75rem;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.slots-visual {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.drive-slot {
|
||
width: 40px;
|
||
height: 12px;
|
||
background: #e2e8f0;
|
||
border-radius: 2px;
|
||
position: relative;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.drive-slot.filled {
|
||
background: var(--brand-primary);
|
||
box-shadow: 0 0 8px rgba(44, 121, 255, 0.4);
|
||
}
|
||
|
||
.drive-slot.filled.gen5 {
|
||
background: var(--custom-color);
|
||
box-shadow: 0 0 8px rgba(139, 92, 246, 0.4);
|
||
}
|
||
|
||
.drive-slot.filled::after {
|
||
content: "";
|
||
position: absolute;
|
||
right: 2px;
|
||
top: 2px;
|
||
bottom: 2px;
|
||
width: 2px;
|
||
background: rgba(255,255,255,0.5);
|
||
border-radius: 1px;
|
||
animation: blink 2s infinite;
|
||
}
|
||
|
||
@keyframes blink {
|
||
0%, 100% { opacity: 0.3; }
|
||
50% { opacity: 1; }
|
||
}
|
||
|
||
/* Quantity Stepper */
|
||
.qty-stepper {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 0.5rem;
|
||
margin-top: 0.5rem;
|
||
background: #f3f4f6;
|
||
border-radius: 6px;
|
||
padding: 0.25rem;
|
||
width: 100%;
|
||
}
|
||
|
||
.btn-qty {
|
||
width: 24px;
|
||
height: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: none;
|
||
background: white;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-weight: 700;
|
||
color: var(--brand-primary);
|
||
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||
transition: all 0.1s;
|
||
}
|
||
|
||
.btn-qty:hover {
|
||
background: var(--brand-primary);
|
||
color: white;
|
||
}
|
||
|
||
.qty-val {
|
||
font-size: 0.9rem;
|
||
font-weight: 700;
|
||
color: var(--brand-dark);
|
||
min-width: 20px;
|
||
text-align: center;
|
||
}
|
||
|
||
.config-card.has-stepper {
|
||
cursor: default; /* Don't click the whole card */
|
||
padding-bottom: 0.75rem;
|
||
}
|
||
|
||
.config-card.has-stepper.active {
|
||
border-color: var(--brand-primary);
|
||
background: #f0f9ff;
|
||
box-shadow: 0 0 0 1px var(--brand-primary);
|
||
}
|
||
|
||
/* Summary Panel */
|
||
.summary-header {
|
||
font-size: 0.7rem;
|
||
font-weight: 700;
|
||
color: var(--text-secondary);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.summary-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-size: 0.8rem;
|
||
margin-bottom: 0.75rem;
|
||
padding-bottom: 0.75rem;
|
||
border-bottom: 1px solid #f3f4f6;
|
||
}
|
||
|
||
.summary-row .label { color: var(--text-secondary); }
|
||
.summary-row .val { font-weight: 500; text-align: right; max-width: 60%; }
|
||
|
||
.total-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-top: 1rem;
|
||
margin-bottom: 1.5rem;
|
||
padding-top: 0.5rem;
|
||
}
|
||
|
||
.total-row .label { font-size: 0.9rem; color: var(--text-secondary); }
|
||
.total-row .amount { font-size: 1.75rem; font-weight: 800; color: var(--brand-primary); }
|
||
|
||
.btn-success {
|
||
background: var(--brand-primary);
|
||
color: white;
|
||
border: none;
|
||
width: 100%;
|
||
justify-content: center;
|
||
padding: 0.75rem;
|
||
}
|
||
|
||
.btn-success:hover {
|
||
background: #2260c4;
|
||
}
|
||
|
||
/* Smart Action Buttons */
|
||
.action-buttons {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.action-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.75rem;
|
||
width: 100%;
|
||
}
|
||
|
||
.btn-smart {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0.8rem 1rem;
|
||
border-radius: 8px;
|
||
text-decoration: none;
|
||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
position: relative;
|
||
overflow: hidden;
|
||
width: 100%;
|
||
}
|
||
|
||
.btn-smart:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.btn-smart.primary {
|
||
background: var(--brand-primary);
|
||
color: white;
|
||
border: 1px solid var(--brand-primary);
|
||
}
|
||
|
||
.btn-smart.primary:hover {
|
||
background: #2260c4;
|
||
border-color: #2260c4;
|
||
}
|
||
|
||
.btn-smart.secondary {
|
||
background: #f8fafc;
|
||
color: var(--brand-primary);
|
||
border: 1px solid #e2e8f0;
|
||
}
|
||
|
||
.btn-smart.secondary:hover {
|
||
background: #f1f5f9;
|
||
border-color: #cbd5e1;
|
||
color: #1e40af;
|
||
}
|
||
|
||
.btn-smart.full-width {
|
||
width: 100%;
|
||
}
|
||
|
||
.btn-smart .btn-icon {
|
||
font-size: 1.1rem;
|
||
margin-right: 0.5rem;
|
||
}
|
||
|
||
.btn-smart .btn-text {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
line-height: 1.1;
|
||
}
|
||
|
||
.btn-smart .main-label {
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.btn-smart .sub-label {
|
||
font-size: 0.65rem;
|
||
font-weight: 400;
|
||
opacity: 0.8;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
/* Collapsed Secondary Links */
|
||
.secondary-links {
|
||
text-align: center;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.link-muted {
|
||
color: var(--text-secondary);
|
||
text-decoration: none;
|
||
border-bottom: 1px dotted #cbd5e1;
|
||
transition: color 0.2s;
|
||
}
|
||
|
||
.link-muted:hover {
|
||
color: var(--brand-primary);
|
||
border-bottom-color: var(--brand-primary);
|
||
}
|
||
|
||
/* Tab Content */
|
||
.tab-content {
|
||
display: none;
|
||
}
|
||
|
||
.tab-content.active {
|
||
display: block;
|
||
}
|
||
|
||
/* Loading Spinner */
|
||
.loading-spinner {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
padding: 2rem;
|
||
color: var(--brand-primary);
|
||
font-weight: 600;
|
||
gap: 0.5rem;
|
||
}
|
||
.loading-spinner::after {
|
||
content: "";
|
||
width: 20px;
|
||
height: 20px;
|
||
border: 2px solid var(--brand-primary);
|
||
border-top-color: transparent;
|
||
border-radius: 50%;
|
||
animation: spin 0.8s linear infinite;
|
||
}
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
/* Smart Specs Grid - Google Material Design Inspired */
|
||
.smart-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||
gap: 1.5rem;
|
||
}
|
||
|
||
.smart-spec-card {
|
||
background: #ffffff;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 16px;
|
||
padding: 1.5rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
transition: transform 0.2s, box-shadow 0.2s;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.smart-spec-card:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05);
|
||
border-color: var(--brand-primary);
|
||
}
|
||
|
||
.card-overline {
|
||
font-size: 0.7rem;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.08em;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 0.5rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.card-overline svg {
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.card-hero {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 0.25rem;
|
||
margin-bottom: 0.25rem;
|
||
color: var(--brand-dark);
|
||
}
|
||
|
||
.hero-val {
|
||
font-size: 2.5rem;
|
||
font-weight: 800;
|
||
line-height: 1;
|
||
letter-spacing: -0.02em;
|
||
background: linear-gradient(135deg, var(--brand-dark) 0%, #4b5563 100%);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
}
|
||
|
||
.hero-unit {
|
||
font-size: 1rem;
|
||
font-weight: 600;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.card-subhero {
|
||
font-size: 0.9rem;
|
||
font-weight: 600;
|
||
color: var(--brand-primary);
|
||
margin-bottom: 1.25rem;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
background: #f0f9ff;
|
||
display: inline-block;
|
||
padding: 0.2rem 0.6rem;
|
||
border-radius: 6px;
|
||
align-self: flex-start;
|
||
}
|
||
|
||
.spec-divider {
|
||
height: 1px;
|
||
background: #f1f5f9;
|
||
margin-bottom: 1rem;
|
||
width: 100%;
|
||
}
|
||
|
||
.mini-specs-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 0.75rem 1rem;
|
||
}
|
||
|
||
.mini-spec {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.mini-spec .label {
|
||
font-size: 0.7rem;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 0.15rem;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.mini-spec .val {
|
||
font-size: 0.85rem;
|
||
font-weight: 600;
|
||
color: var(--brand-dark);
|
||
}
|
||
|
||
/* ============ RESPONSIVE STYLES ============ */
|
||
@media (max-width: 1024px) {
|
||
.calculator-body {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.config-panel {
|
||
border-right: none;
|
||
border-bottom: 1px solid var(--border-color);
|
||
}
|
||
|
||
.servers-grid {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.container {
|
||
padding: 0 0.75rem;
|
||
}
|
||
|
||
.nav-links {
|
||
display: none;
|
||
}
|
||
|
||
.mobile-menu-btn {
|
||
display: block;
|
||
}
|
||
|
||
.hero h1 {
|
||
font-size: 1.75rem;
|
||
}
|
||
|
||
.calculator-header {
|
||
padding: 1rem;
|
||
}
|
||
|
||
.calculator-title h2 {
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.config-panel,
|
||
.summary-panel {
|
||
padding: 1rem;
|
||
}
|
||
|
||
.servers-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.config-tabs {
|
||
flex-direction: column;
|
||
}
|
||
}
|
||
</style>
|
||
<body class="wp-singular page-template page-template-bare-metal-server-details page-template-bare-metal-server-details-php page page-id-2877 wp-custom-logo wp-theme-dedicatednodes">
|
||
|
||
<span class="screen-darken"></span>
|
||
|
||
<!-- Header -->
|
||
<nav class="navbar navbar-expand-lg navbar-light bg-white fixed-top" id="navbar_main" style="padding: 1rem 0; border-bottom: 1px solid #e5e7eb; z-index: 9999;">
|
||
<div class="container">
|
||
<div class="offcanvas-header d-lg-none">
|
||
<button class="btn-close float-end"></button>
|
||
</div>
|
||
<a class="navbar-brand" href="https://www.dedicatednodes.io/">
|
||
<img src="https://www.dedicatednodes.io/wp-content/uploads/2024/04/LOGO.svg" alt="DedicatedNodes" style="height: 36px;">
|
||
</a>
|
||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||
<span class="navbar-toggler-icon"></span>
|
||
</button>
|
||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||
<ul class="navbar-nav ms-lg-4 mb-2 mb-lg-0">
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="https://www.dedicatednodes.io/solana-nodes/" style="color: var(--brand-dark); font-weight: 600;">Solana dedicated nodes</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="https://www.dedicatednodes.io/performance-vps/" style="color: var(--brand-dark); font-weight: 600;">Performance VPS</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="https://www.dedicatednodes.io/about-us/" style="color: var(--brand-dark); font-weight: 600;">About us</a>
|
||
</li>
|
||
</ul>
|
||
<ul class="navbar-nav ms-auto right d-flex flex-row align-items-center gap-3">
|
||
<li class="nav-item">
|
||
<a class="nav-link outline btn-outline" href="https://portal.dedicatednodes.io/contact.php" style="border: 1px solid var(--brand-dark); border-radius: 6px; padding: 0.5rem 1.5rem; font-weight: 600; color: var(--brand-dark);">Contact us</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link fill btn-primary" href="https://portal.dedicatednodes.io/login" style="background: var(--brand-primary); color: white; border-radius: 6px; padding: 0.5rem 1.5rem; font-weight: 600;">Control panel</a>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<main class="bare-metal-details">
|
||
|
||
<!-- Hero Section -->
|
||
<section class="server-hero">
|
||
<div class="container">
|
||
<nav class="hero-breadcrumb">
|
||
<ol class="breadcrumb-list">
|
||
<li><a href="https://www.dedicatednodes.io">Home</a></li>
|
||
<li><a href="https://www.dedicatednodes.io/bare-metal-servers">Bare Metal Servers</a></li>
|
||
<li class="current">AMD Ryzen Threadripper PRO 9975WX</li>
|
||
</ol>
|
||
</nav>
|
||
|
||
<div class="hero-content">
|
||
<span class="hero-badge">
|
||
⚡ Ultimate Performance Beast </span>
|
||
|
||
<h1 class="hero-title">AMD Ryzen Threadripper PRO 9975WX</h1>
|
||
<p class="hero-subtitle">
|
||
32 cores •
|
||
5.4 GHz boost •
|
||
384GB+ memory
|
||
</p>
|
||
|
||
<div class="hero-actions">
|
||
<a href="#calculator" class="btn-primary">
|
||
Configure server
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||
<polyline points="12 5 19 12 12 19"></polyline>
|
||
</svg>
|
||
</a>
|
||
<a href="#specs" class="btn-secondary">
|
||
View specifications
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<polyline points="7 13 12 18 17 13"></polyline>
|
||
<polyline points="7 6 12 11 17 6"></polyline>
|
||
</svg>
|
||
</a>
|
||
</div>
|
||
|
||
<div class="hero-stats">
|
||
<div class="stat-item">
|
||
<span class="stat-value">32</span>
|
||
<span class="stat-label">CPU Cores</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span class="stat-value">384GB+</span>
|
||
<span class="stat-label">Memory</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span class="stat-value">2x 10Gbps</span>
|
||
<span class="stat-label">Network</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span class="stat-value">Instant</span>
|
||
<span class="stat-label">Setup</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Calculator Section (New Builder) -->
|
||
<div class="calc-top-bar">
|
||
<div class="container">
|
||
Select a server configuration below
|
||
</div>
|
||
</div>
|
||
|
||
<section class="calculator-section" id="calculator">
|
||
<div class="container">
|
||
|
||
<!-- Tabs -->
|
||
<div class="config-tabs">
|
||
<button class="config-tab active instant" data-tab="instant">
|
||
⚡ Instant Deploy <span class="badge-count">9</span>
|
||
</button>
|
||
<button class="config-tab custom" data-tab="custom">
|
||
⚙️ Custom Build <span class="badge-count">4</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="calculator-wrapper">
|
||
<div class="calculator-body">
|
||
|
||
<!-- Left Panel -->
|
||
<div class="config-panel">
|
||
|
||
<!-- Instant Servers Tab -->
|
||
<div class="tab-content active" id="instant-content">
|
||
<div class="section-label">
|
||
<span class="section-pill instant">INSTANT</span>
|
||
<span style="color:var(--text-secondary); font-weight:600;">READY IN 15 MINUTES</span>
|
||
</div>
|
||
<div class="servers-grid" id="instantServersGrid">
|
||
<!-- Populated by JS -->
|
||
</div>
|
||
|
||
<!-- Instant Customization Area -->
|
||
<div class="config-section" id="instantOptions" style="display:none;">
|
||
<div class="config-header-bar" style="display: flex; align-items: center; gap: 1rem;">
|
||
<button class="btn-back" onclick="closeInstantCustomization()" style="background:none; border:none; font-size:1.2rem; cursor:pointer; padding:0;">←</button>
|
||
<div>
|
||
<div style="font-weight:700;">Configure Your Server</div>
|
||
<div style="font-size:0.8rem; color:var(--text-secondary); font-weight:400;">Customize specs for this instant server</div>
|
||
</div>
|
||
<span style="font-size: 0.75rem; font-weight: normal; margin-left: auto; color: #059669; background: #f0fdf4; padding: 0.1rem 0.5rem; border-radius: 4px; border: 1px solid #bbf7d0;">Delivery: Up to 48h</span>
|
||
</div>
|
||
|
||
<div id="instantConfigLoader" style="display:none;" class="loading-spinner">
|
||
Loading upgrade options...
|
||
</div>
|
||
|
||
<div id="instantDynamicConfigContainer">
|
||
<!-- Dynamic Options injected here -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Custom Build Tab -->
|
||
<div class="tab-content" id="custom-content">
|
||
<div class="section-label">
|
||
<span class="section-pill custom">CUSTOM</span>
|
||
<span style="color:var(--text-secondary); font-weight:600;">CHOOSE YOUR BASE CPU</span>
|
||
</div>
|
||
<div class="servers-grid" id="customServersGrid">
|
||
<!-- Populated by JS -->
|
||
</div>
|
||
|
||
<!-- Custom Options Area -->
|
||
<div class="config-section" id="customOptions" style="display:none;">
|
||
<div class="config-header-bar">
|
||
⚙️ Configure Your Server
|
||
</div>
|
||
|
||
<div id="configLoader" style="display:none;" class="loading-spinner">
|
||
Loading real-time options...
|
||
</div>
|
||
|
||
<div id="dynamicConfigContainer">
|
||
<!-- Dynamic Options will be injected here -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right Summary Panel -->
|
||
<div class="summary-panel">
|
||
<!-- Dynamic Header for Summary -->
|
||
<div id="summaryHeaderPill" class="section-pill instant" style="display:inline-block; margin-bottom:0.5rem; font-size:0.7rem;">
|
||
Instant Deploy
|
||
</div>
|
||
<div id="summarySubtext" style="font-size:0.75rem; color:var(--text-secondary); margin-bottom:1.5rem;">
|
||
Ready in 15 minutes
|
||
</div>
|
||
|
||
<!-- Instant Customization Toggle Removed -->
|
||
<div id="instantToggleContainer" style="display:none;"></div>
|
||
|
||
<div class="summary-header">CONFIGURATION SUMMARY</div>
|
||
|
||
<div class="summary-row">
|
||
<span class="label">CPU</span>
|
||
<span class="val" id="summaryCpu">-</span>
|
||
</div>
|
||
<div class="summary-row">
|
||
<span class="label">RAM</span>
|
||
<span class="val" id="summaryRam">-</span>
|
||
</div>
|
||
<div class="summary-row">
|
||
<span class="label">Storage</span>
|
||
<span class="val" id="summaryStorage">-</span>
|
||
</div>
|
||
<div class="summary-row">
|
||
<span class="label">Network</span>
|
||
<span class="val" id="summaryNetwork">-</span>
|
||
</div>
|
||
<div class="summary-row">
|
||
<span class="label">Location</span>
|
||
<span class="val" id="summaryLocation">-</span>
|
||
</div>
|
||
|
||
<div class="total-row">
|
||
<span class="label">Est. Monthly</span>
|
||
<span class="amount" id="totalPrice">€0</span>
|
||
</div>
|
||
|
||
<div class="action-buttons smart-actions">
|
||
<!-- Instant Buttons -->
|
||
<div id="instantButtons" class="action-group" style="display:none;">
|
||
<a href="#" class="btn-smart primary" id="btnDeployInstant">
|
||
<span class="btn-icon">⚡</span>
|
||
<div class="btn-text">
|
||
<span class="main-label">Deploy Now</span>
|
||
<span class="sub-label">Default Config</span>
|
||
</div>
|
||
</a>
|
||
<button class="btn-smart secondary" id="btnCustomizeInstant" title="Customize Configuration">
|
||
<span class="btn-icon">⚙️</span>
|
||
<span class="btn-label">Customize</span>
|
||
</button>
|
||
</div>
|
||
|
||
<a href="#" class="btn-smart primary full-width" id="configureBtn" style="display:none;">
|
||
<span class="btn-icon">🔧</span>
|
||
Start Custom Build
|
||
</a>
|
||
|
||
<div class="secondary-links">
|
||
<a href="https://portal.dedicatednodes.io/contact.php" class="link-muted">Talk to an expert</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Main Specifications -->
|
||
<section class="main-specs" id="specs">
|
||
<div class="container">
|
||
<div class="specs-header">
|
||
<h2 class="specs-title">Technical specifications</h2>
|
||
<p class="specs-subtitle">Enterprise-grade hardware configured for maximum performance</p>
|
||
</div>
|
||
|
||
<div class="specs-layout">
|
||
<div class="specs-grid smart-grid">
|
||
<!-- Processor -->
|
||
<div class="smart-spec-card">
|
||
<div class="card-overline">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect><rect x="9" y="9" width="6" height="6"></rect><line x1="9" y1="1" x2="9" y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line x1="9" y1="20" x2="9" y2="23"></line><line x1="15" y1="20" x2="15" y2="23"></line><line x1="20" y1="9" x2="23" y2="9"></line><line x1="20" y1="14" x2="23" y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line x1="1" y1="14" x2="4" y2="14"></line></svg>
|
||
Processor
|
||
</div>
|
||
<div class="card-hero">
|
||
<span class="hero-val" id="spec-cpu-cores-count">32</span>
|
||
<span class="hero-unit">Cores</span>
|
||
</div>
|
||
<div class="card-subhero" id="spec-cpu-model-short">Threadripper PRO 9975WX</div>
|
||
|
||
<div class="spec-divider"></div>
|
||
|
||
<div class="mini-specs-grid">
|
||
<div class="mini-spec">
|
||
<span class="label">Boost Clock</span>
|
||
<span class="val" id="spec-cpu-boost">5.4 GHz</span>
|
||
</div>
|
||
<div class="mini-spec">
|
||
<span class="label">Base Clock</span>
|
||
<span class="val" id="spec-cpu-base">3.5 GHz</span>
|
||
</div>
|
||
<div class="mini-spec">
|
||
<span class="label">Cache</span>
|
||
<span class="val" id="spec-cpu-cache">128MB L3</span>
|
||
</div>
|
||
<div class="mini-spec">
|
||
<span class="label">Architecture</span>
|
||
<span class="val" id="spec-cpu-arch">Zen 4</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Memory -->
|
||
<div class="smart-spec-card">
|
||
<div class="card-overline">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12h20"></path><path d="M2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6"></path><path d="M22 12v-6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v6"></path><path d="M6 12v-6"></path><path d="M10 12v-6"></path><path d="M14 12v-6"></path><path d="M18 12v-6"></path></svg>
|
||
Memory
|
||
</div>
|
||
<div class="card-hero">
|
||
<span class="hero-val" id="spec-ram-capacity-hero">384</span>
|
||
<span class="hero-unit">GB DDR5</span>
|
||
</div>
|
||
<div class="card-subhero">ECC Registered</div>
|
||
|
||
<div class="spec-divider"></div>
|
||
|
||
<div class="mini-specs-grid">
|
||
<div class="mini-spec">
|
||
<span class="label">Speed</span>
|
||
<span class="val" id="spec-ram-speed">6400 MT/s</span>
|
||
</div>
|
||
<div class="mini-spec">
|
||
<span class="label">Channels</span>
|
||
<span class="val" id="spec-ram-channels">8-channel</span>
|
||
</div>
|
||
<div class="mini-spec">
|
||
<span class="label">Type</span>
|
||
<span class="val">RDIMM</span>
|
||
</div>
|
||
<div class="mini-spec">
|
||
<span class="label">Upgrade</span>
|
||
<span class="val">Available</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Storage -->
|
||
<div class="smart-spec-card">
|
||
<div class="card-overline">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3"></ellipse><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path></svg>
|
||
Storage
|
||
</div>
|
||
<div class="card-hero">
|
||
<span class="hero-val">Gen5</span>
|
||
<span class="hero-unit">NVMe</span>
|
||
</div>
|
||
<div class="card-subhero" id="spec-storage-config-hero">Up to 12x Drives</div>
|
||
|
||
<div class="spec-divider"></div>
|
||
|
||
<div class="mini-specs-grid">
|
||
<div class="mini-spec">
|
||
<span class="label">Read Speed</span>
|
||
<span class="val">14 GB/s</span>
|
||
</div>
|
||
<div class="mini-spec">
|
||
<span class="label">Write Speed</span>
|
||
<span class="val">12 GB/s</span>
|
||
</div>
|
||
<div class="mini-spec">
|
||
<span class="label">Grade</span>
|
||
<span class="val">Enterprise</span>
|
||
</div>
|
||
<div class="mini-spec">
|
||
<span class="label">Flexibility</span>
|
||
<span class="val">High</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Network -->
|
||
<div class="smart-spec-card">
|
||
<div class="card-overline">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>
|
||
Network
|
||
</div>
|
||
<div class="card-hero">
|
||
<span class="hero-val" id="spec-network-speed-hero">10</span>
|
||
<span class="hero-unit">Gbps</span>
|
||
</div>
|
||
<div class="card-subhero" id="spec-network-bandwidth-hero">Unmetered</div>
|
||
|
||
<div class="spec-divider"></div>
|
||
|
||
<div class="mini-specs-grid">
|
||
<div class="mini-spec">
|
||
<span class="label">IPv4</span>
|
||
<span class="val">/29 Included</span>
|
||
</div>
|
||
<div class="mini-spec">
|
||
<span class="label">IPv6</span>
|
||
<span class="val">/64 Block</span>
|
||
</div>
|
||
<div class="mini-spec">
|
||
<span class="label">Uplink</span>
|
||
<span class="val">Redundant</span>
|
||
</div>
|
||
<div class="mini-spec">
|
||
<span class="label">DDoS Prot.</span>
|
||
<span class="val">Standard</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sidebar -->
|
||
<div class="specs-sidebar">
|
||
<!-- Pricing Card Removed as per request -->
|
||
|
||
<!-- Quick Features -->
|
||
<div class="sidebar-card">
|
||
<h3 class="sidebar-card-title">Key features</h3>
|
||
<div class="quick-features">
|
||
<div class="quick-feature">
|
||
<span class="quick-feature-icon">🚀</span>
|
||
<span>Instant deployment</span>
|
||
</div>
|
||
<div class="quick-feature">
|
||
<span class="quick-feature-icon">🔧</span>
|
||
<span>IPMI 2.0 with KVM</span>
|
||
</div>
|
||
<div class="quick-feature">
|
||
<span class="quick-feature-icon">📊</span>
|
||
<span>100% uptime in 2025</span>
|
||
</div>
|
||
<div class="quick-feature">
|
||
<span class="quick-feature-icon">💬</span>
|
||
<span>24/7 expert support</span>
|
||
</div>
|
||
<div class="quick-feature">
|
||
<span class="quick-feature-icon">🔐</span>
|
||
<span>Full root access</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Features Section -->
|
||
<section class="features-section">
|
||
<div class="container">
|
||
<div class="specs-header">
|
||
<h2 class="specs-title">Everything you need</h2>
|
||
<p class="specs-subtitle">Complete infrastructure solution with enterprise support</p>
|
||
</div>
|
||
|
||
<div class="features-grid">
|
||
<div class="feature-card">
|
||
<span class="feature-emoji">💿</span>
|
||
<h3 class="feature-title">Operating system</h3>
|
||
<p class="feature-description">Linux, Windows, custom images</p>
|
||
</div>
|
||
|
||
<div class="feature-card">
|
||
<span class="feature-emoji">💾</span>
|
||
<h3 class="feature-title">Backup storage</h3>
|
||
<p class="feature-description">Optional managed backups available</p>
|
||
</div>
|
||
|
||
<div class="feature-card">
|
||
<span class="feature-emoji">👥</span>
|
||
<h3 class="feature-title">24/7 support</h3>
|
||
<p class="feature-description">Expert human support, no chatbots</p>
|
||
</div>
|
||
|
||
<div class="feature-card">
|
||
<span class="feature-emoji">✅</span>
|
||
<h3 class="feature-title">SLA guarantee</h3>
|
||
<p class="feature-description">100% uptime in 2025</p>
|
||
</div>
|
||
|
||
<div class="feature-card">
|
||
<span class="feature-emoji">🔐</span>
|
||
<h3 class="feature-title">Secure infrastructure</h3>
|
||
<p class="feature-description">Enterprise-grade security</p>
|
||
</div>
|
||
|
||
<div class="feature-card">
|
||
<span class="feature-emoji">🔧</span>
|
||
<h3 class="feature-title">IPMI access</h3>
|
||
<p class="feature-description">Full remote management</p>
|
||
</div>
|
||
|
||
<div class="feature-card">
|
||
<span class="feature-emoji">⚡</span>
|
||
<h3 class="feature-title">Instant setup</h3>
|
||
<p class="feature-description">Deploy in minutes</p>
|
||
</div>
|
||
|
||
<div class="feature-card">
|
||
<span class="feature-emoji">🌍</span>
|
||
<h3 class="feature-title">Global network</h3>
|
||
<p class="feature-description">Multiple locations worldwide</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Locations -->
|
||
|
||
|
||
<!-- Strategic Locations Section with Network Map -->
|
||
<section class="locations" id="locations">
|
||
<div class="container">
|
||
<header class="locations-header">
|
||
<h2 class="locations-title">Strategic locations</h2>
|
||
</header>
|
||
<div class="locations-intro">
|
||
<p class="locations-description">
|
||
Optimized global presence with ultra-low latency connections directly to Jito and major blockchain networks. We operate our own network backbone to ensure consistent performance and reliability under AS214783.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="location-badges">
|
||
<span class="loc-badge loc-badge-future" data-tooltip="Opening December 2025">
|
||
<span class="flag-icon">🇳🇱</span>
|
||
Amsterdam
|
||
<svg class="future-icon" width="14" height="14" viewBox="0 0 16 16" fill="currentColor" style="opacity: 0.5; margin-left: 4px; vertical-align: middle;">
|
||
<path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z"/>
|
||
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"/>
|
||
</svg>
|
||
</span>
|
||
<span class="loc-badge">
|
||
<span class="flag-icon">🇳🇱</span>
|
||
Rotterdam
|
||
</span>
|
||
<span class="loc-badge">
|
||
<span class="flag-icon">🇩🇪</span>
|
||
Frankfurt
|
||
</span>
|
||
<span class="loc-badge">
|
||
<span class="flag-icon">🇬🇧</span>
|
||
London
|
||
</span>
|
||
<span class="loc-badge">
|
||
<span class="flag-icon">🇺🇸</span>
|
||
New York
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="map-container">
|
||
<div id="world-map-canvas" role="img" aria-label="Interactive world map showing our datacenter locations"></div>
|
||
|
||
<div class="map-controls">
|
||
<h3 class="controls-title">
|
||
<span class="pulse"></span>
|
||
Live latency
|
||
</h3>
|
||
<div class="activity-list">
|
||
<div class="activity-row activity-row-future" data-tooltip="Opening December 2025">
|
||
<span class="activity-route">Amsterdam → Jito</span>
|
||
<span class="activity-latency">0.1ms</span>
|
||
</div>
|
||
<div class="activity-row">
|
||
<span class="activity-route">Rotterdam → Jito</span>
|
||
<span class="activity-latency">1.6ms</span>
|
||
</div>
|
||
<div class="activity-row">
|
||
<span class="activity-route">Frankfurt → Jito</span>
|
||
<span class="activity-latency">0.6ms</span>
|
||
</div>
|
||
<div class="activity-row">
|
||
<span class="activity-route">London → Jito</span>
|
||
<span class="activity-latency">0.2ms</span>
|
||
</div>
|
||
<div class="activity-row">
|
||
<span class="activity-route">New York → Jito</span>
|
||
<span class="activity-latency">0.1ms</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stats-bar">
|
||
<div class="container">
|
||
<div class="stats-grid">
|
||
<div class="stat">
|
||
<div class="stat-value">5</div>
|
||
<div class="stat-label">Global Locations</div>
|
||
</div>
|
||
<div class="stat">
|
||
<div class="stat-value">0.1ms</div>
|
||
<div class="stat-label">Lowest Latency</div>
|
||
</div>
|
||
<div class="stat">
|
||
<div class="stat-value">100%</div>
|
||
<div class="stat-label">Uptime in 2025</div>
|
||
</div>
|
||
<div class="stat">
|
||
<div class="stat-value">24/7</div>
|
||
<div class="stat-label">Support</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- MapLibre Script -->
|
||
<script src='https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js'></script>
|
||
<link href='https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.css' rel='stylesheet' />
|
||
<script src="/wp-content/themes/dedicatednodes/assets/js/locations.js"></script>
|
||
|
||
|
||
<!-- Final CTA -->
|
||
<section class="final-cta">
|
||
<div class="container">
|
||
<div class="cta-content">
|
||
<h2 class="cta-title">Ready to deploy?</h2>
|
||
<p class="cta-subtitle">Get your AMD Ryzen Threadripper PRO 9975WX server running in minutes</p>
|
||
|
||
<div class="cta-actions">
|
||
<a href="https://portal.dedicatednodes.io/order/bareserver" class="btn-cta-white">
|
||
Start configuration
|
||
</a>
|
||
<a href="https://portal.dedicatednodes.io/contact" class="btn-cta-outline">
|
||
Talk to sales
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
</main>
|
||
|
||
<footer id="colophon" class="site-footer">
|
||
<div class="container pb-5">
|
||
<div class="row">
|
||
<div class="col-lg-4">
|
||
<a href="https://www.dedicatednodes.io">
|
||
<img src="https://www.dedicatednodes.io/wp-content/uploads/2024/04/LOGO-white.png">
|
||
</a>
|
||
<p>Engineer innovative solutions that go above and beyond the ordinary. </p>
|
||
<div class="d-flex gap-3">
|
||
<a href="https://www.x.com/dedicatednodes">
|
||
<img src="https://www.dedicatednodes.io/wp-content/uploads/2024/04/x.png">
|
||
</a>
|
||
<a href="https://discord.gg/tH8mhfpT8G">
|
||
<img src="https://www.dedicatednodes.io/wp-content/uploads/2024/08/8725815_discord_icon-1-1.png">
|
||
</a>
|
||
<a href="https://www.trustpilot.com/review/dedicatednodes.io">
|
||
<img src="https://www.dedicatednodes.io/wp-content/uploads/2025/11/trustpilot.png">
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-lg-8">
|
||
<div class="d-flex justify-content-between mt-lg-0 mt-5">
|
||
<div>
|
||
<div class="d-flex justify-content-lg-end justify-content-center">
|
||
<div class="footer-title">Technical support</div>
|
||
</div>
|
||
<div class="d-flex justify-content-lg-end justify-content-center">
|
||
<a href="https://portal.dedicatednodes.io/contact.php" class="btn outline">Support</a>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<div class="d-flex justify-content-lg-end justify-content-center">
|
||
<div class="footer-title">We are here to help you</div>
|
||
</div>
|
||
<div class="d-flex justify-content-lg-end justify-content-center">
|
||
<a href="https://portal.dedicatednodes.io/contact.php" class="btn fill">Contact us</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<hr>
|
||
<div class="container">
|
||
<div class="footer-bottom">
|
||
<div class="d-flex justify-content-between flex-column flex-lg-row text-center">
|
||
<div>@2025 all right reserved</div>
|
||
<div class="site-info">
|
||
<a href="/privacy-policy/" target="">
|
||
Privacy Policy </a><span class="sep"> | </span>
|
||
<a href="/terms-of-condition/" target="">
|
||
Terms of condition </a><span class="sep"> | </span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</footer>
|
||
<script type="speculationrules">
|
||
{"prefetch":[{"source":"document","where":{"and":[{"href_matches":"/*"},{"not":{"href_matches":["/wp-*.php","/wp-admin/*","/wp-content/uploads/*","/wp-content/*","/wp-content/plugins/*","/wp-content/themes/dedicatednodes/*","/*\\?(.+)"]}},{"not":{"selector_matches":"a[rel~=\"nofollow\"]"}},{"not":{"selector_matches":".no-prefetch, .no-prefetch a"}}]},"eagerness":"conservative"}]}
|
||
</script>
|
||
<script src="https://www.dedicatednodes.io/wp-includes/js/dist/hooks.min.js?ver=dd5603f07f9220ed27f1" id="wp-hooks-js"></script>
|
||
<script src="https://www.dedicatednodes.io/wp-includes/js/dist/i18n.min.js?ver=c26c3dc7bed366793375" id="wp-i18n-js"></script>
|
||
<script id="wp-i18n-js-after">
|
||
wp.i18n.setLocaleData( { 'text direction\u0004ltr': [ 'ltr' ] } );
|
||
//# sourceURL=wp-i18n-js-after
|
||
</script>
|
||
<script src="https://www.dedicatednodes.io/wp-content/plugins/contact-form-7/includes/swv/js/index.js?ver=6.1.2" id="swv-js"></script>
|
||
<script id="contact-form-7-js-before">
|
||
var wpcf7 = {
|
||
"api": {
|
||
"root": "https:\/\/www.dedicatednodes.io\/wp-json\/",
|
||
"namespace": "contact-form-7\/v1"
|
||
}
|
||
};
|
||
//# sourceURL=contact-form-7-js-before
|
||
</script>
|
||
<script src="https://www.dedicatednodes.io/wp-content/plugins/contact-form-7/includes/js/index.js?ver=6.1.2" id="contact-form-7-js"></script>
|
||
<script src="https://www.dedicatednodes.io/wp-content/themes/dedicatednodes/assets/js/bootstrap.min.js?ver=1764837370" id="wpddn-bootstrap-js"></script>
|
||
<script src="https://www.dedicatednodes.io/wp-content/themes/dedicatednodes/assets/js/bootstrap.bundle.min.js?ver=1764837370" id="wpddn-bootstrap-bundle-js"></script>
|
||
<script src="https://www.dedicatednodes.io/wp-content/themes/dedicatednodes/assets/js/slick.js?ver=1764837370" id="wpddn-slick-js"></script>
|
||
<script src="https://www.dedicatednodes.io/wp-content/themes/dedicatednodes/assets/js/aos.js?ver=1764837370" id="wpddn-aos-js"></script>
|
||
<script src="https://www.dedicatednodes.io/wp-content/themes/dedicatednodes/assets/js/tippy.min.js?ver=1764837370" id="wpddn-tippy-min-js"></script>
|
||
<script src="https://www.dedicatednodes.io/wp-content/themes/dedicatednodes/assets/js/navigation.js?ver=1764837370" id="dedicatednodes-navigation-js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/intl-tel-input@22.0.2/build/js/intlTelInput.min.js?ver=1764837370" id="intl-tel-input-js"></script>
|
||
<script src="https://www.dedicatednodes.io/wp-content/themes/dedicatednodes/assets/js/phone-form-utils.js?ver=1764837370" id="phone-form-utils-js"></script>
|
||
<script src="https://www.dedicatednodes.io/wp-content/themes/dedicatednodes/assets/js/order-server.js?ver=1764837370" id="order-server-js"></script>
|
||
<script src="https://www.dedicatednodes.io/wp-content/themes/dedicatednodes/assets/js/wpddn-public.js?ver=1764837370" id="wpddn-public-js"></script>
|
||
<script id="wp-emoji-settings" type="application/json">
|
||
{"baseUrl":"https://s.w.org/images/core/emoji/17.0.2/72x72/","ext":".png","svgUrl":"https://s.w.org/images/core/emoji/17.0.2/svg/","svgExt":".svg","source":{"concatemoji":"https://www.dedicatednodes.io/wp-includes/js/wp-emoji-release.min.js?ver=6.9"}}
|
||
</script>
|
||
<script>
|
||
// Server Data - Instant Servers (Synced with live website)
|
||
const instantServers = [
|
||
{
|
||
"id": "inst-1",
|
||
"cpu": "AMD Ryzen Threadripper PRO 7965WX",
|
||
"cores": 24,
|
||
"ghz": "5.3",
|
||
"baseClock": "4.2 GHz",
|
||
"boostClock": "5.3",
|
||
"cache": "128MB L3",
|
||
"arch": "Zen 4",
|
||
"ram": "768GB DDR5",
|
||
"ramSpeed": "4800 MT/s",
|
||
"ramChannels": "8-channel",
|
||
"storage": "2x Crucial T705 1TB NVMe + 4x Kioxia CM7-V 1.6TB NVMe + 2x Kioxia CM7-V 3.2TB NVMe",
|
||
"network": "10Gbps",
|
||
"bandwidth": "Unmetered",
|
||
"location": "Rotterdam",
|
||
"price": 1576,
|
||
"currency": "€",
|
||
"limits": { "gen4": 3, "gen5": 8 },
|
||
"orderUrl": "https://portal.dedicatednodes.io/cart.php?a=add&pid=50&configoption[34]=167&configoption[35]=253&configoption[36]=261&configoption[37]=270&configoption[38]=280&configoption[39]=288&configoption[40]=292&configoption[41]=300&configoption[69]=493&configoption[2]=37&configoption[22]=551&billingcycle=monthly"
|
||
},
|
||
{
|
||
"id": "inst-2",
|
||
"cpu": "AMD Ryzen Threadripper PRO 7965WX",
|
||
"cores": 24,
|
||
"ghz": "5.3",
|
||
"baseClock": "4.2 GHz",
|
||
"boostClock": "5.3",
|
||
"cache": "128MB L3",
|
||
"arch": "Zen 4",
|
||
"ram": "768GB DDR5",
|
||
"ramSpeed": "4800 MT/s",
|
||
"ramChannels": "8-channel",
|
||
"storage": "4x Crucial T705 4TB NVMe + 2x Kioxia CM7-V 3.2TB NVMe",
|
||
"network": "10Gbps",
|
||
"bandwidth": "Unmetered",
|
||
"location": "Rotterdam",
|
||
"price": 1520,
|
||
"currency": "€",
|
||
"limits": { "gen4": 3, "gen5": 8 },
|
||
"orderUrl": "https://portal.dedicatednodes.io/cart.php?a=add&pid=50&configoption[34]=168&configoption[35]=254&configoption[36]=260&configoption[37]=269&configoption[38]=278&configoption[39]=286&configoption[69]=493&configoption[2]=37&configoption[22]=551&billingcycle=monthly"
|
||
},
|
||
{
|
||
"id": "inst-3",
|
||
"cpu": "AMD Ryzen Threadripper PRO 7965WX",
|
||
"cores": 24,
|
||
"ghz": "5.3",
|
||
"baseClock": "4.2 GHz",
|
||
"boostClock": "5.3",
|
||
"cache": "128MB L3",
|
||
"arch": "Zen 4",
|
||
"ram": "768GB DDR5",
|
||
"ramSpeed": "4800 MT/s",
|
||
"ramChannels": "8-channel",
|
||
"storage": "1x Crucial T705 1TB NVMe + 2x Kioxia CM7-V 1.6TB NVMe + 2x Kioxia CM7-V 3.2TB NVMe",
|
||
"network": "10Gbps",
|
||
"bandwidth": "Unmetered",
|
||
"location": "Frankfurt",
|
||
"price": 1408,
|
||
"currency": "€",
|
||
"limits": { "gen4": 3, "gen5": 8 },
|
||
"orderUrl": "https://portal.dedicatednodes.io/cart.php?a=add&pid=50&configoption[34]=167&configoption[35]=253&configoption[36]=262&configoption[37]=271&configoption[38]=276&configoption[69]=493&configoption[2]=37&configoption[22]=111&billingcycle=monthly"
|
||
},
|
||
{
|
||
"id": "inst-4",
|
||
"cpu": "AMD Ryzen Threadripper PRO 7965WX",
|
||
"cores": 24,
|
||
"ghz": "5.3",
|
||
"baseClock": "4.2 GHz",
|
||
"boostClock": "5.3",
|
||
"cache": "128MB L3",
|
||
"arch": "Zen 4",
|
||
"ram": "768GB DDR5",
|
||
"ramSpeed": "4800 MT/s",
|
||
"ramChannels": "8-channel",
|
||
"storage": "1x Solidigm D7-PS1030 1.6TB NVMe + 3x Solidigm D7-PS1030 3.2TB NVMe",
|
||
"network": "10Gbps",
|
||
"bandwidth": "Unmetered",
|
||
"location": "Rotterdam",
|
||
"price": 1395,
|
||
"currency": "€",
|
||
"limits": { "gen4": 3, "gen5": 8 },
|
||
"orderUrl": "https://portal.dedicatednodes.io/cart.php?a=add&pid=50&configoption[34]=221&configoption[35]=256&configoption[36]=264&configoption[37]=273&configoption[69]=493&configoption[2]=37&configoption[22]=551&billingcycle=monthly"
|
||
},
|
||
{
|
||
"id": "inst-5",
|
||
"cpu": "AMD Ryzen Threadripper PRO 7965WX",
|
||
"cores": 24,
|
||
"ghz": "5.3",
|
||
"baseClock": "4.2 GHz",
|
||
"boostClock": "5.3",
|
||
"cache": "128MB L3",
|
||
"arch": "Zen 4",
|
||
"ram": "768GB DDR5",
|
||
"ramSpeed": "4800 MT/s",
|
||
"ramChannels": "8-channel",
|
||
"storage": "4x Crucial T705 2TB NVMe",
|
||
"network": "10Gbps",
|
||
"bandwidth": "Unmetered",
|
||
"location": "Rotterdam",
|
||
"price": 1204,
|
||
"currency": "€",
|
||
"limits": { "gen4": 3, "gen5": 8 },
|
||
"orderUrl": "https://portal.dedicatednodes.io/cart.php?a=add&pid=50&configoption[34]=165&configoption[35]=251&configoption[36]=259&configoption[37]=268&configoption[69]=493&configoption[2]=37&configoption[22]=551&billingcycle=monthly"
|
||
},
|
||
{
|
||
"id": "inst-6",
|
||
"cpu": "AMD Ryzen Threadripper PRO 7965WX",
|
||
"cores": 24,
|
||
"ghz": "5.3",
|
||
"baseClock": "4.2 GHz",
|
||
"boostClock": "5.3",
|
||
"cache": "128MB L3",
|
||
"arch": "Zen 4",
|
||
"ram": "768GB DDR5",
|
||
"ramSpeed": "4800 MT/s",
|
||
"ramChannels": "8-channel",
|
||
"storage": "1x Crucial T705 1TB NVMe + 3x Crucial T705 2TB NVMe",
|
||
"network": "10Gbps",
|
||
"bandwidth": "Unmetered",
|
||
"location": "Rotterdam",
|
||
"price": 1194,
|
||
"currency": "€",
|
||
"limits": { "gen4": 3, "gen5": 8 },
|
||
"orderUrl": "https://portal.dedicatednodes.io/cart.php?a=add&pid=50&configoption[34]=164&configoption[35]=251&configoption[36]=259&configoption[37]=268&configoption[69]=493&configoption[2]=37&configoption[22]=551&billingcycle=monthly"
|
||
},
|
||
{
|
||
"id": "inst-7",
|
||
"cpu": "AMD Ryzen Threadripper PRO 7965WX",
|
||
"cores": 24,
|
||
"ghz": "5.3",
|
||
"baseClock": "4.2 GHz",
|
||
"boostClock": "5.3",
|
||
"cache": "128MB L3",
|
||
"arch": "Zen 4",
|
||
"ram": "768GB DDR5",
|
||
"ramSpeed": "4800 MT/s",
|
||
"ramChannels": "8-channel",
|
||
"storage": "1x Crucial T705 1TB NVMe",
|
||
"network": "10Gbps",
|
||
"bandwidth": "Unmetered",
|
||
"location": "Rotterdam",
|
||
"price": 1038,
|
||
"currency": "€",
|
||
"limits": { "gen4": 3, "gen5": 8 },
|
||
"orderUrl": "https://portal.dedicatednodes.io/cart.php?a=add&pid=50&configoption[34]=164&configoption[69]=493&configoption[2]=37&configoption[22]=551&billingcycle=monthly"
|
||
},
|
||
{
|
||
"id": "inst-8",
|
||
"cpu": "AMD Ryzen Threadripper PRO 7965WX",
|
||
"cores": 24,
|
||
"ghz": "5.3",
|
||
"baseClock": "4.2 GHz",
|
||
"boostClock": "5.3",
|
||
"cache": "128MB L3",
|
||
"arch": "Zen 4",
|
||
"ram": "576GB DDR5",
|
||
"ramSpeed": "4800 MT/s",
|
||
"ramChannels": "8-channel",
|
||
"storage": "1x Crucial T705 1TB NVMe + 3x Crucial T705 2TB NVMe",
|
||
"network": "10Gbps",
|
||
"bandwidth": "Unmetered",
|
||
"location": "Rotterdam",
|
||
"price": 1044,
|
||
"currency": "€",
|
||
"limits": { "gen4": 3, "gen5": 8 },
|
||
"orderUrl": "https://portal.dedicatednodes.io/cart.php?a=add&pid=50&configoption[34]=164&configoption[35]=251&configoption[36]=259&configoption[37]=268&configoption[69]=492&configoption[2]=37&configoption[22]=551&billingcycle=monthly"
|
||
},
|
||
{
|
||
"id": "inst-11",
|
||
"cpu": "AMD Ryzen Threadripper PRO 7965WX",
|
||
"cores": 24,
|
||
"ghz": "5.3",
|
||
"baseClock": "4.2 GHz",
|
||
"boostClock": "5.3",
|
||
"cache": "128MB L3",
|
||
"arch": "Zen 4",
|
||
"ram": "576GB DDR5",
|
||
"ramSpeed": "4800 MT/s",
|
||
"ramChannels": "8-channel",
|
||
"storage": "1x Solidigm D7-PS1030 1.6TB NVMe + 2x Solidigm D7-PS1030 3.2TB NVMe",
|
||
"network": "10Gbps",
|
||
"bandwidth": "Unmetered",
|
||
"location": "London",
|
||
"price": 1230,
|
||
"currency": "€",
|
||
"limits": { "gen4": 3, "gen5": 8 },
|
||
"orderUrl": "https://portal.dedicatednodes.io/cart.php?a=add&pid=50&configoption[34]=221&configoption[35]=256&configoption[36]=264&configoption[69]=492&configoption[2]=37&configoption[22]=112&billingcycle=monthly"
|
||
},
|
||
{
|
||
"id": "inst-12",
|
||
"cpu": "AMD Ryzen Threadripper PRO 7965WX",
|
||
"cores": 24,
|
||
"ghz": "5.3",
|
||
"baseClock": "4.2 GHz",
|
||
"boostClock": "5.3",
|
||
"cache": "128MB L3",
|
||
"arch": "Zen 4",
|
||
"ram": "576GB DDR5",
|
||
"ramSpeed": "4800 MT/s",
|
||
"ramChannels": "8-channel",
|
||
"storage": "1x Kioxia CM7-V 1.6TB NVMe + 2x Kioxia CM7-V 3.2TB NVMe",
|
||
"network": "10Gbps",
|
||
"bandwidth": "Unmetered",
|
||
"location": "London",
|
||
"price": 1230,
|
||
"currency": "€",
|
||
"limits": { "gen4": 3, "gen5": 8 },
|
||
"orderUrl": "https://portal.dedicatednodes.io/cart.php?a=add&pid=50&configoption[34]=167&configoption[35]=254&configoption[36]=262&configoption[69]=492&configoption[2]=37&configoption[22]=112&billingcycle=monthly"
|
||
},
|
||
{
|
||
"id": "inst-14",
|
||
"cpu": "AMD Ryzen Threadripper PRO 7965WX",
|
||
"cores": 24,
|
||
"ghz": "5.3",
|
||
"baseClock": "4.2 GHz",
|
||
"boostClock": "5.3",
|
||
"cache": "128MB L3",
|
||
"arch": "Zen 4",
|
||
"ram": "576GB DDR5",
|
||
"ramSpeed": "4800 MT/s",
|
||
"ramChannels": "8-channel",
|
||
"storage": "1x Crucial T705 1TB NVMe + 1x Crucial T705 2TB NVMe",
|
||
"network": "10Gbps",
|
||
"bandwidth": "Unmetered",
|
||
"location": "Rotterdam",
|
||
"price": 940,
|
||
"currency": "€",
|
||
"limits": { "gen4": 3, "gen5": 8 },
|
||
"orderUrl": "https://portal.dedicatednodes.io/cart.php?a=add&pid=50&configoption[34]=164&configoption[35]=251&configoption[69]=492&configoption[2]=37&configoption[22]=551&billingcycle=monthly"
|
||
},
|
||
{
|
||
"id": "inst-15",
|
||
"cpu": "AMD Ryzen Threadripper PRO 7965WX",
|
||
"cores": 24,
|
||
"ghz": "5.3",
|
||
"baseClock": "4.2 GHz",
|
||
"boostClock": "5.3",
|
||
"cache": "128MB L3",
|
||
"arch": "Zen 4",
|
||
"ram": "576GB DDR5",
|
||
"ramSpeed": "4800 MT/s",
|
||
"ramChannels": "8-channel",
|
||
"storage": "1x Crucial T705 1TB NVMe",
|
||
"network": "10Gbps",
|
||
"bandwidth": "Unmetered",
|
||
"location": "Rotterdam",
|
||
"price": 888,
|
||
"currency": "€",
|
||
"limits": { "gen4": 3, "gen5": 8 },
|
||
"orderUrl": "https://portal.dedicatednodes.io/cart.php?a=add&pid=50&configoption[34]=164&configoption[69]=492&configoption[2]=37&configoption[22]=551&billingcycle=monthly"
|
||
},
|
||
{
|
||
"id": "inst-17",
|
||
"cpu": "AMD EPYC 9275F",
|
||
"cores": 24,
|
||
"ghz": "4.1",
|
||
"baseClock": "4.05 GHz",
|
||
"boostClock": "4.3",
|
||
"cache": "256MB L3",
|
||
"arch": "Zen 4",
|
||
"ram": "1152GB DDR5",
|
||
"ramSpeed": "4800 MT/s",
|
||
"ramChannels": "12-channel",
|
||
"storage": "1x Kioxia CM7-V 1.6TB NVMe + 2x Kioxia CM7-V 3.2TB NVMe",
|
||
"network": "10Gbps",
|
||
"bandwidth": "Unmetered",
|
||
"location": "Rotterdam",
|
||
"price": 1415,
|
||
"currency": "€",
|
||
"limits": { "gen4": 3, "gen5": 8 },
|
||
"orderUrl": "https://portal.dedicatednodes.io/cart.php?a=add&pid=37&configoption[34]=167&configoption[35]=254&configoption[36]=262&configoption[46]=229&configoption[2]=37&configoption[22]=551&billingcycle=monthly"
|
||
},
|
||
{
|
||
"id": "inst-22",
|
||
"cpu": "AMD EPYC 9275F",
|
||
"cores": 24,
|
||
"ghz": "4.1",
|
||
"baseClock": "4.05 GHz",
|
||
"boostClock": "4.3",
|
||
"cache": "256MB L3",
|
||
"arch": "Zen 4",
|
||
"ram": "384GB DDR5",
|
||
"ramSpeed": "4800 MT/s",
|
||
"ramChannels": "12-channel",
|
||
"storage": "1x Kioxia CM7-V 1.6TB NVMe + 2x Kioxia CM7-V 3.2TB NVMe",
|
||
"network": "10Gbps",
|
||
"bandwidth": "Unmetered",
|
||
"location": "Rotterdam",
|
||
"price": 815,
|
||
"currency": "€",
|
||
"limits": { "gen4": 3, "gen5": 8 },
|
||
"orderUrl": "https://portal.dedicatednodes.io/cart.php?a=add&pid=37&configoption[34]=167&configoption[35]=254&configoption[36]=262&configoption[46]=225&configoption[2]=37&configoption[22]=551&billingcycle=monthly"
|
||
},
|
||
{
|
||
"id": "inst-23",
|
||
"cpu": "AMD EPYC 9274F",
|
||
"cores": 24,
|
||
"ghz": "4.1",
|
||
"baseClock": "4.05 GHz",
|
||
"boostClock": "4.3",
|
||
"cache": "256MB L3",
|
||
"arch": "Zen 4",
|
||
"ram": "1152GB DDR5",
|
||
"ramSpeed": "4800 MT/s",
|
||
"ramChannels": "12-channel",
|
||
"storage": "1x Kioxia CM7-V 1.6TB NVMe + 2x Kioxia CM7-V 3.2TB NVMe",
|
||
"network": "10Gbps",
|
||
"bandwidth": "Unmetered",
|
||
"location": "Rotterdam",
|
||
"price": 1099,
|
||
"currency": "€",
|
||
"limits": { "gen4": 3, "gen5": 8 },
|
||
"orderUrl": "https://portal.dedicatednodes.io/cart.php?a=add&pid=25&configoption[34]=167&configoption[35]=254&configoption[36]=262&configoption[46]=229&configoption[2]=37&configoption[22]=551&billingcycle=monthly"
|
||
},
|
||
{
|
||
"id": "inst-24",
|
||
"cpu": "AMD EPYC 7543P",
|
||
"cores": 32,
|
||
"ghz": "2.8",
|
||
"baseClock": "2.8 GHz",
|
||
"boostClock": "3.7",
|
||
"cache": "256MB L3",
|
||
"arch": "Zen 3",
|
||
"ram": "1024GB DDR4",
|
||
"ramSpeed": "3200 MT/s",
|
||
"ramChannels": "8-channel",
|
||
"storage": "1x Samsung 990 PRO 1TB NVMe + 2x Samsung 990 PRO 2TB NVMe",
|
||
"network": "10Gbps",
|
||
"bandwidth": "Unmetered",
|
||
"location": "Rotterdam",
|
||
"price": 650,
|
||
"currency": "€",
|
||
"limits": { "gen4": 8, "gen5": 0 },
|
||
"orderUrl": "https://portal.dedicatednodes.io/cart.php?a=add&pid=39&configoption[49]=357&configoption[50]=447&configoption[51]=452&configoption[48]=345&configoption[61]=462&configoption[22]=551&billingcycle=monthly"
|
||
},
|
||
{
|
||
"id": "inst-25",
|
||
"cpu": "AMD EPYC 7443P",
|
||
"cores": 24,
|
||
"ghz": "2.85",
|
||
"baseClock": "2.85 GHz",
|
||
"boostClock": "4.0",
|
||
"cache": "128MB L3",
|
||
"arch": "Zen 3",
|
||
"ram": "1024GB DDR4",
|
||
"ramSpeed": "3200 MT/s",
|
||
"ramChannels": "8-channel",
|
||
"storage": "3x Samsung PM9A3 2TB NVMe",
|
||
"network": "10Gbps",
|
||
"bandwidth": "Unmetered",
|
||
"location": "Rotterdam",
|
||
"price": 610,
|
||
"currency": "€",
|
||
"limits": { "gen4": 4, "gen5": 0 },
|
||
"fallbackSplit": [2, 2],
|
||
"orderUrl": "https://portal.dedicatednodes.io/cart.php?a=add&pid=48&configoption[49]=356&configoption[50]=449&configoption[51]=454&configoption[48]=345&configoption[47]=266&configoption[22]=551&billingcycle=monthly"
|
||
}
|
||
];
|
||
|
||
// Custom Servers (Matched to Image 3)
|
||
const customServers = [
|
||
{ id: 'custom-1', cpu: 'AMD Threadripper PRO 9975WX', cores: 32, ghz: '5.4', baseClock: '3.6 GHz', boostClock: '5.3', cache: '128MB L3', arch: 'Zen 4', ram: 'Base: 256GB', ramSpeed: '4800 MT/s', ramChannels: '8-channel', basePrice: 850, currency: '€', configUrl: 'https://portal.dedicatednodes.io/index.php?rp=/store/bare-metal-servers/amd-ryzen-threadripper-pro-9975wx', setupTime: 'Instant', limits: { gen4: 3, gen5: 8 },
|
||
specificFallback: [
|
||
{
|
||
"id": "22",
|
||
"label": "Location",
|
||
"type": "location",
|
||
"values": [
|
||
{ "id": "551", "text": "Rotterdam", "price": 0 },
|
||
{ "id": "111", "text": "Frankfurt €10,00 EUR", "price": 10.0 },
|
||
{ "id": "112", "text": "London €85,00 EUR", "price": 85.0 },
|
||
{ "id": "113", "text": "New York €35,00 EUR", "price": 35.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "69",
|
||
"label": "Memory",
|
||
"type": "ram",
|
||
"values": [
|
||
{ "id": "491", "text": "384GB", "price": 0 },
|
||
{ "id": "492", "text": "576GB €150,00 EUR", "price": 150.0 },
|
||
{ "id": "493", "text": "768GB €300,00 EUR", "price": 300.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "34",
|
||
"label": "NVMe 1",
|
||
"type": "storage",
|
||
"values": [
|
||
{ "id": "220", "text": "-", "price": 0 },
|
||
{ "id": "221", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "222", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "167", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "168", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "164", "text": "Crucial T705 1TB €18,00 EUR", "price": 18.0 },
|
||
{ "id": "165", "text": "Crucial T705 2TB €28,00 EUR", "price": 28.0 },
|
||
{ "id": "166", "text": "Crucial T705 4TB €52,00 EUR", "price": 52.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "35",
|
||
"label": "NVMe 2",
|
||
"type": "storage",
|
||
"values": [
|
||
{ "id": "257", "text": "-", "price": 0 },
|
||
{ "id": "255", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "256", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "253", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "254", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "250", "text": "Crucial T705 1TB €28,00 EUR", "price": 28.0 },
|
||
{ "id": "251", "text": "Crucial T705 2TB €52,00 EUR", "price": 52.0 },
|
||
{ "id": "252", "text": "Crucial T705 4TB €75,00 EUR", "price": 75.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "36",
|
||
"label": "NVMe 3",
|
||
"type": "storage",
|
||
"values": [
|
||
{ "id": "265", "text": "-", "price": 0 },
|
||
{ "id": "263", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "264", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "261", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "262", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "258", "text": "Crucial T705 1TB €28,00 EUR", "price": 28.0 },
|
||
{ "id": "259", "text": "Crucial T705 2TB €52,00 EUR", "price": 52.0 },
|
||
{ "id": "260", "text": "Crucial T705 4TB €75,00 EUR", "price": 75.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "37",
|
||
"label": "NVMe 4",
|
||
"type": "storage",
|
||
"values": [
|
||
{ "id": "274", "text": "-", "price": 0 },
|
||
{ "id": "272", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "273", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "270", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "271", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "267", "text": "Crucial T705 1TB €28,00 EUR", "price": 28.0 },
|
||
{ "id": "268", "text": "Crucial T705 2TB €52,00 EUR", "price": 52.0 },
|
||
{ "id": "269", "text": "Crucial T705 4TB €75,00 EUR", "price": 75.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "38",
|
||
"label": "NVMe 5",
|
||
"type": "storage",
|
||
"values": [
|
||
{ "id": "275", "text": "-", "price": 0 },
|
||
{ "id": "281", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "282", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "279", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "280", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "39",
|
||
"label": "NVMe 6",
|
||
"type": "storage",
|
||
"values": [
|
||
{ "id": "283", "text": "-", "price": 0 },
|
||
{ "id": "289", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "290", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "287", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "288", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "40",
|
||
"label": "NVMe 7",
|
||
"type": "storage",
|
||
"values": [
|
||
{ "id": "291", "text": "-", "price": 0 },
|
||
{ "id": "297", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "298", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "295", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "296", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "41",
|
||
"label": "NVMe 8",
|
||
"type": "storage",
|
||
"values": [
|
||
{ "id": "299", "text": "-", "price": 0 },
|
||
{ "id": "305", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "306", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "303", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "304", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "42",
|
||
"label": "NVMe 9",
|
||
"type": "storage",
|
||
"values": [
|
||
{ "id": "307", "text": "-", "price": 0 },
|
||
{ "id": "313", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "314", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "311", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "312", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "43",
|
||
"label": "NVMe 10",
|
||
"type": "storage",
|
||
"values": [
|
||
{ "id": "315", "text": "-", "price": 0 },
|
||
{ "id": "321", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "322", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "319", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "320", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "44",
|
||
"label": "NVMe 11",
|
||
"type": "storage",
|
||
"values": [
|
||
{ "id": "323", "text": "-", "price": 0 },
|
||
{ "id": "329", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "330", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "327", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "328", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "45",
|
||
"label": "NVMe 12",
|
||
"type": "storage",
|
||
"values": [
|
||
{ "id": "331", "text": "-", "price": 0 },
|
||
{ "id": "337", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "338", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "335", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "336", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "2",
|
||
"label": "Network",
|
||
"type": "network",
|
||
"values": [
|
||
{ "id": "37", "text": "10Gbps", "price": 0 },
|
||
{ "id": "475", "text": "2x 10Gbps (bond) €25,00 EUR", "price": 25.0 }
|
||
]
|
||
}
|
||
]
|
||
},
|
||
{ id: 'custom-2', cpu: 'AMD Threadripper PRO 7965WX', cores: 24, ghz: '5.3', baseClock: '4.2 GHz', boostClock: '5.3', cache: '128MB L3', arch: 'Zen 4', ram: 'Base: 256GB', ramSpeed: '4800 MT/s', ramChannels: '8-channel', basePrice: 650, currency: '€', configUrl: 'https://portal.dedicatednodes.io/index.php?rp=/store/bare-metal-servers/amd-ryzen-threadripper-pro-7965wx', setupTime: '1 Day', limits: { gen4: 3, gen5: 8 },
|
||
specificFallback: [
|
||
{
|
||
"id": "22",
|
||
"label": "Location",
|
||
"type": "location",
|
||
"values": [
|
||
{ "id": "551", "text": "Rotterdam", "price": 0 },
|
||
{ "id": "111", "text": "Frankfurt €10,00 EUR", "price": 10.0 },
|
||
{ "id": "112", "text": "London €85,00 EUR", "price": 85.0 },
|
||
{ "id": "113", "text": "New York €35,00 EUR", "price": 35.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "69",
|
||
"label": "Memory",
|
||
"type": "ram",
|
||
"values": [
|
||
{ "id": "491", "text": "384GB", "price": 0 },
|
||
{ "id": "492", "text": "576GB €150,00 EUR", "price": 150.0 },
|
||
{ "id": "493", "text": "768GB €300,00 EUR", "price": 300.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "34",
|
||
"label": "NVMe 1",
|
||
"type": "storage",
|
||
"values": [
|
||
{ "id": "220", "text": "-", "price": 0 },
|
||
{ "id": "221", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "222", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "167", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "168", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "164", "text": "Crucial T705 1TB €18,00 EUR", "price": 18.0 },
|
||
{ "id": "165", "text": "Crucial T705 2TB €28,00 EUR", "price": 28.0 },
|
||
{ "id": "166", "text": "Crucial T705 4TB €52,00 EUR", "price": 52.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "35",
|
||
"label": "NVMe 2",
|
||
"type": "storage",
|
||
"values": [
|
||
{ "id": "257", "text": "-", "price": 0 },
|
||
{ "id": "255", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "256", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "253", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "254", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "250", "text": "Crucial T705 1TB €28,00 EUR", "price": 28.0 },
|
||
{ "id": "251", "text": "Crucial T705 2TB €52,00 EUR", "price": 52.0 },
|
||
{ "id": "252", "text": "Crucial T705 4TB €75,00 EUR", "price": 75.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "36",
|
||
"label": "NVMe 3",
|
||
"type": "storage",
|
||
"values": [
|
||
{ "id": "265", "text": "-", "price": 0 },
|
||
{ "id": "263", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "264", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "261", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "262", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "258", "text": "Crucial T705 1TB €28,00 EUR", "price": 28.0 },
|
||
{ "id": "259", "text": "Crucial T705 2TB €52,00 EUR", "price": 52.0 },
|
||
{ "id": "260", "text": "Crucial T705 4TB €75,00 EUR", "price": 75.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "37",
|
||
"label": "NVMe 4",
|
||
"type": "storage",
|
||
"values": [
|
||
{ "id": "274", "text": "-", "price": 0 },
|
||
{ "id": "272", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "273", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "270", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "271", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "267", "text": "Crucial T705 1TB €28,00 EUR", "price": 28.0 },
|
||
{ "id": "268", "text": "Crucial T705 2TB €52,00 EUR", "price": 52.0 },
|
||
{ "id": "269", "text": "Crucial T705 4TB €75,00 EUR", "price": 75.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "38",
|
||
"label": "NVMe 5",
|
||
"type": "storage",
|
||
"values": [
|
||
{ "id": "275", "text": "-", "price": 0 },
|
||
{ "id": "281", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "282", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "279", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "280", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "39",
|
||
"label": "NVMe 6",
|
||
"type": "storage",
|
||
"values": [
|
||
{ "id": "283", "text": "-", "price": 0 },
|
||
{ "id": "289", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "290", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "287", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "288", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "40",
|
||
"label": "NVMe 7",
|
||
"type": "storage",
|
||
"values": [
|
||
{ "id": "291", "text": "-", "price": 0 },
|
||
{ "id": "297", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "298", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "295", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "296", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "41",
|
||
"label": "NVMe 8",
|
||
"type": "storage",
|
||
"values": [
|
||
{ "id": "299", "text": "-", "price": 0 },
|
||
{ "id": "305", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "306", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "303", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "304", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "42",
|
||
"label": "NVMe 9",
|
||
"type": "storage",
|
||
"values": [
|
||
{ "id": "307", "text": "-", "price": 0 },
|
||
{ "id": "313", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "314", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "311", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "312", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "43",
|
||
"label": "NVMe 10",
|
||
"type": "storage",
|
||
"values": [
|
||
{ "id": "315", "text": "-", "price": 0 },
|
||
{ "id": "321", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "322", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "319", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "320", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "44",
|
||
"label": "NVMe 11",
|
||
"type": "storage",
|
||
"values": [
|
||
{ "id": "323", "text": "-", "price": 0 },
|
||
{ "id": "329", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "330", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "327", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "328", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "45",
|
||
"label": "NVMe 12",
|
||
"type": "storage",
|
||
"values": [
|
||
{ "id": "331", "text": "-", "price": 0 },
|
||
{ "id": "337", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "338", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100.0 },
|
||
{ "id": "335", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75.0 },
|
||
{ "id": "336", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100.0 }
|
||
]
|
||
},
|
||
{
|
||
"id": "2",
|
||
"label": "Network",
|
||
"type": "network",
|
||
"values": [
|
||
{ "id": "37", "text": "10Gbps", "price": 0 },
|
||
{ "id": "475", "text": "2x 10Gbps (bond) €25,00 EUR", "price": 25.0 }
|
||
]
|
||
}
|
||
]
|
||
},
|
||
{ id: 'custom-3', cpu: 'AMD EPYC 9274F', cores: 24, ghz: '4.1', baseClock: '4.05 GHz', boostClock: '4.3', cache: '256MB L3', arch: 'Zen 4', ram: 'Base: 256GB', ramSpeed: '4800 MT/s', ramChannels: '12-channel', basePrice: 450, currency: '€', configUrl: 'https://portal.dedicatednodes.io/index.php?rp=/store/bare-metal-servers/amd-epyc-9274f', setupTime: '1 Day', limits: { gen4: 3, gen5: 8 },
|
||
specificFallback: [
|
||
{
|
||
"id": "22", "label": "Location", "type": "location",
|
||
"values": [
|
||
{ "id": "551", "text": "Rotterdam", "price": 0 },
|
||
{ "id": "111", "text": "Frankfurt €10,00 EUR", "price": 10 },
|
||
{ "id": "112", "text": "London €85,00 EUR", "price": 85 },
|
||
{ "id": "113", "text": "New York €35,00 EUR", "price": 35 }
|
||
]
|
||
},
|
||
{
|
||
"id": "46", "label": "Memory", "type": "ram",
|
||
"values": [
|
||
{ "id": "225", "text": "384GB", "price": 0 },
|
||
{ "id": "226", "text": "576GB €150,00 EUR", "price": 150 },
|
||
{ "id": "227", "text": "768GB €300,00 EUR", "price": 300 },
|
||
{ "id": "228", "text": "960GB €450,00 EUR", "price": 450 },
|
||
{ "id": "229", "text": "1152GB €600,00 EUR", "price": 600 },
|
||
{ "id": "230", "text": "1344GB €750,00 EUR", "price": 750 },
|
||
{ "id": "231", "text": "1536GB €900,00 EUR", "price": 900 },
|
||
{ "id": "232", "text": "1728GB €1.150,00 EUR", "price": 1150 },
|
||
{ "id": "233", "text": "1920GB €1.300,00 EUR", "price": 1300 },
|
||
{ "id": "234", "text": "2112GB €1.450,00 EUR", "price": 1450 },
|
||
{ "id": "235", "text": "2304GB €1.600,00 EUR", "price": 1600 }
|
||
]
|
||
},
|
||
// Group A (Gen4)
|
||
{
|
||
"id": "34", "label": "NVMe 1 (Gen4)", "type": "storage",
|
||
"values": [
|
||
{ "id": "220", "text": "-", "price": 0 },
|
||
{ "id": "164", "text": "Crucial T705 1TB €18,00 EUR", "price": 18 },
|
||
{ "id": "165", "text": "Crucial T705 2TB €28,00 EUR", "price": 28 },
|
||
{ "id": "166", "text": "Crucial T705 4TB €52,00 EUR", "price": 52 }
|
||
]
|
||
},
|
||
{
|
||
"id": "35", "label": "NVMe 2 (Gen4)", "type": "storage",
|
||
"values": [
|
||
{ "id": "257", "text": "-", "price": 0 },
|
||
{ "id": "250", "text": "Crucial T705 1TB €28,00 EUR", "price": 28 },
|
||
{ "id": "251", "text": "Crucial T705 2TB €52,00 EUR", "price": 52 },
|
||
{ "id": "252", "text": "Crucial T705 4TB €75,00 EUR", "price": 75 }
|
||
]
|
||
},
|
||
{
|
||
"id": "36", "label": "NVMe 3 (Gen4)", "type": "storage",
|
||
"values": [
|
||
{ "id": "265", "text": "-", "price": 0 },
|
||
{ "id": "258", "text": "Crucial T705 1TB €28,00 EUR", "price": 28 },
|
||
{ "id": "259", "text": "Crucial T705 2TB €52,00 EUR", "price": 52 },
|
||
{ "id": "260", "text": "Crucial T705 4TB €75,00 EUR", "price": 75 }
|
||
]
|
||
},
|
||
// Group B (Gen5)
|
||
{
|
||
"id": "37", "label": "NVMe 4 (Gen5)", "type": "storage",
|
||
"values": [
|
||
{ "id": "274", "text": "-", "price": 0 },
|
||
{ "id": "272", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75 },
|
||
{ "id": "273", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100 },
|
||
{ "id": "270", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75 },
|
||
{ "id": "271", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100 }
|
||
]
|
||
},
|
||
{
|
||
"id": "38", "label": "NVMe 5 (Gen5)", "type": "storage",
|
||
"values": [
|
||
{ "id": "275", "text": "-", "price": 0 },
|
||
{ "id": "281", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75 },
|
||
{ "id": "282", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100 },
|
||
{ "id": "279", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75 },
|
||
{ "id": "280", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100 }
|
||
]
|
||
}
|
||
// Additional slots 39-45 follow same pattern, will rely on auto-replication if needed or can be added explicitly
|
||
]
|
||
},
|
||
{
|
||
id: 'custom-4',
|
||
cpu: 'AMD EPYC 7443P',
|
||
cores: 24,
|
||
ghz: '4.0',
|
||
baseClock: '2.85 GHz',
|
||
boostClock: '4.0',
|
||
cache: '128MB L3',
|
||
arch: 'Zen 3',
|
||
ram: 'Base: 128GB',
|
||
ramSpeed: '3200 MT/s',
|
||
ramChannels: '8-channel',
|
||
basePrice: 490,
|
||
currency: '€',
|
||
configUrl: 'https://portal.dedicatednodes.io/index.php?rp=/store/bare-metal-servers/amd-epyc-7443p-1',
|
||
setupTime: 'Instant',
|
||
limits: { gen4: 4, gen5: 0 },
|
||
fallbackSplit: [2, 2],
|
||
specificFallback: [
|
||
{
|
||
"id": "22", "label": "Location", "type": "location",
|
||
"values": [
|
||
{ "id": "551", "text": "Rotterdam", "price": 0 },
|
||
{ "id": "111", "text": "Frankfurt €10,00 EUR", "price": 10 },
|
||
{ "id": "112", "text": "London €85,00 EUR", "price": 85 },
|
||
{ "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": [
|
||
{ "id": "345", "text": "256GB", "price": 0 },
|
||
{ "id": "346", "text": "384GB €10,00 EUR", "price": 10 },
|
||
{ "id": "347", "text": "512GB €20,00 EUR", "price": 20 },
|
||
{ "id": "348", "text": "640GB €30,00 EUR", "price": 30 },
|
||
{ "id": "349", "text": "768GB €40,00 EUR", "price": 40 },
|
||
{ "id": "350", "text": "896GB €50,00 EUR", "price": 50 },
|
||
{ "id": "351", "text": "1024GB €60,00 EUR", "price": 60 }
|
||
]
|
||
},
|
||
// Group A (49, 50) - 6 Items
|
||
{
|
||
"id": "49", "label": "NVMe 1", "type": "storage",
|
||
"values": [
|
||
{ "id": "357", "text": "-", "price": 0 },
|
||
{ "id": "356", "text": "Samsung PM9A3 2TB €20,00 EUR", "price": 20 },
|
||
{ "id": "358", "text": "Samsung 990 PRO 1TB €10,00 EUR", "price": 10 },
|
||
{ "id": "359", "text": "Samsung 990 PRO 2TB €17,50 EUR", "price": 17.5 },
|
||
{ "id": "360", "text": "Samsung 990 PRO 4TB €35,00 EUR", "price": 35 },
|
||
{ "id": "361", "text": "WD BLACK SN850X 4TB €35,00 EUR", "price": 35 }
|
||
]
|
||
},
|
||
{
|
||
"id": "50", "label": "NVMe 2", "type": "storage",
|
||
"values": [
|
||
{ "id": "448", "text": "-", "price": 0 },
|
||
{ "id": "449", "text": "Samsung PM9A3 2TB €20,00 EUR", "price": 20 },
|
||
{ "id": "447", "text": "Samsung 990 PRO 1TB €10,00 EUR", "price": 10 },
|
||
{ "id": "450", "text": "Samsung 990 PRO 2TB €17,50 EUR", "price": 17.5 },
|
||
{ "id": "451", "text": "Samsung 990 PRO 4TB €35,00 EUR", "price": 35 },
|
||
{ "id": "452", "text": "WD BLACK SN850X 4TB €35,00 EUR", "price": 35 }
|
||
]
|
||
},
|
||
// Group B (51, 52) - 5 Items (No WD Black)
|
||
{
|
||
"id": "51", "label": "NVMe 3", "type": "storage",
|
||
"values": [
|
||
{ "id": "453", "text": "-", "price": 0 },
|
||
{ "id": "454", "text": "Samsung PM9A3 2TB €20,00 EUR", "price": 20 },
|
||
{ "id": "455", "text": "Samsung 990 PRO 1TB €10,00 EUR", "price": 10 },
|
||
{ "id": "456", "text": "Samsung 990 PRO 2TB €17,50 EUR", "price": 17.5 },
|
||
{ "id": "457", "text": "Samsung 990 PRO 4TB €35,00 EUR", "price": 35 }
|
||
]
|
||
},
|
||
{
|
||
"id": "52", "label": "NVMe 4", "type": "storage",
|
||
"values": [
|
||
{ "id": "458", "text": "-", "price": 0 },
|
||
{ "id": "459", "text": "Samsung PM9A3 2TB €20,00 EUR", "price": 20 },
|
||
{ "id": "460", "text": "Samsung 990 PRO 1TB €10,00 EUR", "price": 10 },
|
||
{ "id": "461", "text": "Samsung 990 PRO 2TB €17,50 EUR", "price": 17.5 },
|
||
{ "id": "462", "text": "Samsung 990 PRO 4TB €35,00 EUR", "price": 35 }
|
||
]
|
||
}
|
||
]
|
||
},
|
||
];
|
||
|
||
// State
|
||
let selectedServer = null;
|
||
let currentTab = 'instant';
|
||
let configState = {};
|
||
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 = [
|
||
{
|
||
"id": "22",
|
||
"label": "Location",
|
||
"type": "location",
|
||
"values": [
|
||
{ "id": "551", "text": "Rotterdam", "price": 0 },
|
||
{ "id": "111", "text": "Frankfurt €10,00 EUR", "price": 10 },
|
||
{ "id": "112", "text": "London €85,00 EUR", "price": 85 },
|
||
{ "id": "113", "text": "New York €35,00 EUR", "price": 35 }
|
||
]
|
||
},
|
||
{
|
||
"id": "69",
|
||
"label": "Memory",
|
||
"type": "ram",
|
||
"values": [
|
||
{ "id": "491", "text": "384GB", "price": 0 },
|
||
{ "id": "492", "text": "576GB €150,00 EUR", "price": 150 },
|
||
{ "id": "493", "text": "768GB €300,00 EUR", "price": 300 }
|
||
]
|
||
},
|
||
{
|
||
"id": "34", "label": "NVMe 1", "type": "storage",
|
||
"values": [
|
||
{ "id": "220", "text": "-", "price": 0 },
|
||
{ "id": "219", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75 },
|
||
{ "id": "222", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100 },
|
||
{ "id": "167", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75 },
|
||
{ "id": "168", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100 },
|
||
{ "id": "164", "text": "Crucial T705 1TB €18,00 EUR", "price": 18 },
|
||
{ "id": "165", "text": "Crucial T705 2TB €28,00 EUR", "price": 28 },
|
||
{ "id": "166", "text": "Crucial T705 4TB €52,00 EUR", "price": 52 }
|
||
]
|
||
},
|
||
{
|
||
"id": "35", "label": "NVMe 2", "type": "storage",
|
||
"values": [
|
||
{ "id": "257", "text": "-", "price": 0 },
|
||
{ "id": "255", "text": "Solidigm D7-PS1030 1.6TB €75,00 EUR", "price": 75 },
|
||
{ "id": "256", "text": "Solidigm D7-PS1030 3.2TB €100,00 EUR", "price": 100 },
|
||
{ "id": "253", "text": "Kioxia CM7-V 1.6TB €75,00 EUR", "price": 75 },
|
||
{ "id": "254", "text": "Kioxia CM7-V 3.2TB €100,00 EUR", "price": 100 },
|
||
{ "id": "250", "text": "Crucial T705 1TB €28,00 EUR", "price": 28 },
|
||
{ "id": "251", "text": "Crucial T705 2TB €52,00 EUR", "price": 52 },
|
||
{ "id": "252", "text": "Crucial T705 4TB €75,00 EUR", "price": 75 }
|
||
]
|
||
},
|
||
// Repeated for NVMe 3-12 (Simplified for brevity, using same options as 2)
|
||
// We will dynamically replicate these for the fallback if needed, or just include key ones
|
||
{
|
||
"id": "2", "label": "Network interface", "type": "network",
|
||
"values": [
|
||
{ "id": "37", "text": "10Gbps", "price": 0 },
|
||
{ "id": "475", "text": "2x 10Gbps (bond) €25,00 EUR", "price": 25 }
|
||
]
|
||
},
|
||
{
|
||
"id": "1", "label": "Operating System", "type": "other",
|
||
"values": [
|
||
{ "id": "546", "text": "Debian 13", "price": 0 },
|
||
{ "id": "26", "text": "Debian 12", "price": 0 },
|
||
{ "id": "34", "text": "Ubuntu 24.04", "price": 0 },
|
||
{ "id": "20", "text": "Ubuntu 22.04", "price": 0 },
|
||
{ "id": "33", "text": "CentOS 9", "price": 0 },
|
||
{ "id": "18", "text": "Rocky Linux 9", "price": 0 },
|
||
{ "id": "17", "text": "AlmaLinux 9", "price": 0 },
|
||
{ "id": "39", "text": "Windows Server 2022 Standard", "price": 0 },
|
||
{ "id": "30", "text": "Proxmox VE 8", "price": 0 }
|
||
]
|
||
}
|
||
];
|
||
|
||
// Helper to replicate storage options for slots 3-12 if missing from manual list
|
||
// We do this dynamically inside renderFallbackOptions or similar, but here we need it statically for the scraper to return
|
||
// To ensure grouping works (Gen4 vs Gen5), we need to differentiate the options if possible.
|
||
// But here we don't know the server yet.
|
||
// We will handle the differentiation in the "Smart Fallback" logic inside getProductConfig or renderDynamicOptions.
|
||
for(let i=36; i<=45; i++) {
|
||
if(!FALLBACK_CONFIG_OPTIONS.find(o => o.id == i)) {
|
||
const template = FALLBACK_CONFIG_OPTIONS.find(o => o.id == "35"); // Use NVMe 2 as template
|
||
if(template) {
|
||
FALLBACK_CONFIG_OPTIONS.push({
|
||
...template,
|
||
id: i.toString(),
|
||
label: `NVMe ${i-32}`
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- WHMCS Scraper Class ---
|
||
class WHMCSScraper {
|
||
constructor() {
|
||
this.proxies = [
|
||
'https://corsproxy.io/?',
|
||
'https://api.allorigins.win/raw?url='
|
||
];
|
||
}
|
||
|
||
async getProductConfig(configUrl, selectedServer) {
|
||
// Use static fallback for Threadripper/Instant servers if proxy is unreliable
|
||
// This is a "Smart Fallback" - it ensures the user sees options even if the proxy fails
|
||
const useFallback = true;
|
||
|
||
let lastError = null;
|
||
|
||
for (const proxy of this.proxies) {
|
||
try {
|
||
console.log(`Fetching ${configUrl} via ${proxy}...`);
|
||
|
||
const controller = new AbortController();
|
||
const timeoutId = setTimeout(() => controller.abort(), 5000); // Short 5s timeout for faster fallback
|
||
|
||
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');
|
||
|
||
let cartUrl = configUrl;
|
||
// Check if we are already on a config page (inputs present)
|
||
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: this.verifyScrapedData(options, selectedServer), url: configUrl };
|
||
}
|
||
}
|
||
|
||
const configureBtn = doc.querySelector('a[href*="cart.php?a=add"]');
|
||
if (configureBtn) {
|
||
// Follow link logic...
|
||
// But if we fail here, we return null and trigger fallback
|
||
throw new Error("Redirect required but risky");
|
||
}
|
||
|
||
throw new Error("Could not find configuration form");
|
||
} catch (error) {
|
||
console.warn(`Proxy ${proxy} failed:`, error);
|
||
lastError = error;
|
||
}
|
||
}
|
||
|
||
console.error("All proxies failed or timed out. Using cached fallback data.", lastError);
|
||
|
||
// SMART FALLBACK GENERATION
|
||
// Customize fallback options based on server limits to ensure correct grouping
|
||
|
||
// 1. Check for SERVER-SPECIFIC fallback (e.g., 7443P)
|
||
if (selectedServer && selectedServer.specificFallback) {
|
||
console.log("Using Server-Specific Fallback Data");
|
||
return { options: selectedServer.specificFallback, url: configUrl };
|
||
}
|
||
|
||
let fallbackOptions = JSON.parse(JSON.stringify(FALLBACK_CONFIG_OPTIONS));
|
||
|
||
if (selectedServer && selectedServer.limits) {
|
||
const limits = selectedServer.limits;
|
||
// We need to map fallback slots (34, 35, 36...) to Gen4/Gen5 groups
|
||
// Assume sequential mapping:
|
||
// Slot 0 (ID 34) -> 1st
|
||
// Slot 1 (ID 35) -> 2nd
|
||
// ...
|
||
|
||
const gen4Count = limits.gen4 || 0;
|
||
const gen5Count = limits.gen5 || 0;
|
||
|
||
fallbackOptions.forEach(opt => {
|
||
if (opt.type === 'storage') {
|
||
const slotIndex = parseInt(opt.id) - 34; // 34 is base
|
||
if (slotIndex >= 0) {
|
||
// Determine if this slot should be Gen4 or Gen5
|
||
const isGen5Slot = slotIndex >= gen4Count && slotIndex < (gen4Count + gen5Count);
|
||
const isGen4Slot = slotIndex < gen4Count;
|
||
|
||
if (isGen5Slot) {
|
||
// 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 });
|
||
}
|
||
// Rename label to clarify
|
||
opt.label = opt.label.replace('NVMe', 'Gen5 NVMe');
|
||
} else if (isGen4Slot) {
|
||
// If we have a custom fallbackSplit, handle sub-groups
|
||
// Example: 7443P [2, 2] -> First 2 are one group, Next 2 are another
|
||
if (selectedServer.fallbackSplit) {
|
||
const splitA = selectedServer.fallbackSplit[0];
|
||
const splitB = selectedServer.fallbackSplit[1];
|
||
|
||
if (slotIndex < splitA) {
|
||
// Group A: Keep as is
|
||
} else {
|
||
// 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;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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');
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
return { options: fallbackOptions, url: configUrl };
|
||
}
|
||
|
||
parseConfigPage(doc, sourceUrl) {
|
||
const options = [];
|
||
|
||
// 1. Parse Select Configurable Options
|
||
const selects = doc.querySelectorAll('select[name^="configoption"]');
|
||
selects.forEach(select => {
|
||
const name = select.getAttribute('name');
|
||
const idMatch = name.match(/\[(\d+)\]/);
|
||
const id = idMatch ? idMatch[1] : name;
|
||
|
||
// Robust Label Finding Strategy
|
||
let label = "Option";
|
||
|
||
// Strategy 1: Associated Label (explicit 'for')
|
||
const explicitLabel = doc.querySelector(`label[for="${select.id}"]`);
|
||
if (explicitLabel) label = explicitLabel.innerText;
|
||
|
||
// Strategy 2: Container Label (Bootstrap style)
|
||
else if (select.parentElement.querySelector('label')) {
|
||
label = select.parentElement.querySelector('label').innerText;
|
||
}
|
||
|
||
// Strategy 3: Previous Sibling (standard HTML)
|
||
else if (select.previousElementSibling?.tagName === 'LABEL') {
|
||
label = select.previousElementSibling.innerText;
|
||
}
|
||
|
||
// Strategy 4: Table Structure (WHMCS Standard: <tr><td class="fieldlabel">Label</td><td>Input</td></tr>)
|
||
else {
|
||
const tr = select.closest('tr');
|
||
if (tr) {
|
||
const fieldLabel = tr.querySelector('.fieldlabel');
|
||
if (fieldLabel) label = fieldLabel.innerText;
|
||
else {
|
||
// Try first cell if no class
|
||
const firstCell = tr.querySelector('td');
|
||
if (firstCell && firstCell !== select.parentElement) label = firstCell.innerText;
|
||
}
|
||
}
|
||
|
||
// Strategy 5: Previous text node (messy HTML)
|
||
if (label === "Option") {
|
||
let prev = select.previousSibling;
|
||
while(prev && (prev.nodeType !== 3 || prev.textContent.trim() === '')) {
|
||
prev = prev.previousSibling;
|
||
}
|
||
if(prev && prev.nodeType === 3) label = prev.textContent;
|
||
}
|
||
}
|
||
|
||
// Clean label
|
||
label = label.trim().replace(/:$/, '').trim();
|
||
|
||
const values = [];
|
||
select.querySelectorAll('option').forEach(opt => {
|
||
const valId = opt.value;
|
||
const text = opt.textContent;
|
||
|
||
let price = 0;
|
||
const priceMatch = text.match(/([+-]?)[€$£]?\s?([0-9.,]+)/);
|
||
if (text.includes('+') && priceMatch) {
|
||
price = parseFloat(priceMatch[2].replace(',', ''));
|
||
}
|
||
|
||
values.push({ id: valId, text: text.trim(), price: price, raw: text });
|
||
});
|
||
|
||
if (values.length > 0) {
|
||
options.push(this.formatOption(id, label, values, name));
|
||
}
|
||
});
|
||
|
||
// 2. Parse Radio Configurable Options
|
||
const radioGroups = {};
|
||
const radios = doc.querySelectorAll('input[type="radio"][name^="configoption"]');
|
||
|
||
radios.forEach(radio => {
|
||
const name = radio.getAttribute('name');
|
||
const idMatch = name.match(/\[(\d+)\]/);
|
||
const id = idMatch ? idMatch[1] : name;
|
||
|
||
if (!radioGroups[id]) {
|
||
let label = "Option";
|
||
// Strategy: Look for a field label in the container or row
|
||
const tr = radio.closest('tr');
|
||
const formGroup = radio.closest('.form-group');
|
||
const container = formGroup || tr || radio.parentElement.parentElement;
|
||
|
||
if (container) {
|
||
const header = container.querySelector('.fieldlabel') ||
|
||
container.querySelector('label.field-label') ||
|
||
container.querySelector('.field-title');
|
||
|
||
if (header) label = header.innerText;
|
||
else {
|
||
// Look for label in previous element (often a header row or div)
|
||
const prev = container.previousElementSibling;
|
||
if(prev && (prev.tagName === 'LABEL' || prev.classList.contains('field-label'))) {
|
||
label = prev.innerText;
|
||
}
|
||
}
|
||
}
|
||
|
||
radioGroups[id] = { id, label: label.trim().replace(/:$/, ''), values: [], formName: name };
|
||
}
|
||
|
||
// Extract Value Text
|
||
let text = "";
|
||
// 1. Wrapped in label: <label><input> Text</label>
|
||
if (radio.parentElement.tagName === 'LABEL') {
|
||
// Clone to get text without input's value if needed, but innerText usually works
|
||
// Remove the input itself from text extraction if possible?
|
||
// Simplest: innerText usually contains the text.
|
||
text = radio.parentElement.innerText.trim();
|
||
}
|
||
// 2. Label 'for' this radio
|
||
else if (radio.id && doc.querySelector(`label[for="${radio.id}"]`)) {
|
||
text = doc.querySelector(`label[for="${radio.id}"]`).innerText.trim();
|
||
}
|
||
// 3. Next Sibling Text
|
||
else if (radio.nextSibling && radio.nextSibling.nodeType === 3) {
|
||
text = radio.nextSibling.textContent.trim();
|
||
}
|
||
|
||
let price = 0;
|
||
const priceMatch = text.match(/([+-]?)[€$£]?\s?([0-9.,]+)/);
|
||
if (text.includes('+') && priceMatch) {
|
||
price = parseFloat(priceMatch[2].replace(',', ''));
|
||
}
|
||
|
||
radioGroups[id].values.push({ id: radio.value, text: text, price: price, raw: text });
|
||
});
|
||
|
||
Object.values(radioGroups).forEach(group => {
|
||
if (group.values.length > 0) {
|
||
options.push(this.formatOption(group.id, group.label, group.values, group.formName));
|
||
}
|
||
});
|
||
|
||
console.log("Parsed options:", options); // Debug log
|
||
return options;
|
||
}
|
||
|
||
formatOption(id, label, values, formName) {
|
||
let type = 'other';
|
||
const lowerLabel = label.toLowerCase();
|
||
if (lowerLabel.includes('memory') || lowerLabel.includes('ram')) type = 'ram';
|
||
else if (lowerLabel.includes('nvme') || lowerLabel.includes('disk') || lowerLabel.includes('storage') || lowerLabel.includes('drive')) type = 'storage';
|
||
else if (lowerLabel.includes('location') || lowerLabel.includes('region') || lowerLabel.includes('datacenter')) type = 'location';
|
||
else if (lowerLabel.includes('network') || lowerLabel.includes('bandwidth') || lowerLabel.includes('uplink')) type = 'network';
|
||
else if (lowerLabel.includes('operating system') || lowerLabel.includes('os')) type = 'os'; // Explicit OS type
|
||
|
||
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;
|
||
}
|
||
|
||
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();
|
||
|
||
// 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) => {
|
||
const el = document.getElementById(id);
|
||
if(el) el.innerText = val;
|
||
};
|
||
|
||
// Processor
|
||
set('spec-cpu-cores-count', server.cores);
|
||
set('spec-cpu-model-short', server.cpu.replace('AMD Ryzen Threadripper PRO ', '').replace('AMD EPYC ', ''));
|
||
set('spec-cpu-base', server.baseClock || '-');
|
||
set('spec-cpu-boost', server.boostClock ? `${server.boostClock} GHz` : '-');
|
||
set('spec-cpu-cache', server.cache || '-');
|
||
set('spec-cpu-arch', server.arch || '-');
|
||
|
||
// Memory
|
||
// Extract number from "384GB" -> "384"
|
||
const ramMatch = server.ram.match(/(\d+)/);
|
||
set('spec-ram-capacity-hero', ramMatch ? ramMatch[1] : server.ram);
|
||
set('spec-ram-speed', server.ramSpeed || '4800 MT/s');
|
||
set('spec-ram-channels', server.ramChannels || '8-channel');
|
||
|
||
// Storage
|
||
set('spec-storage-config-hero', server.storage || 'Configurable');
|
||
|
||
// Network
|
||
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');
|
||
}
|
||
|
||
// Render Instant Servers
|
||
function renderInstantServers() {
|
||
const grid = document.getElementById('instantServersGrid');
|
||
grid.innerHTML = instantServers.map(server => `
|
||
<div class="server-option instant ${selectedServer?.id === server.id ? 'active' : ''}" data-id="${server.id}" onclick="selectInstantServer('${server.id}')">
|
||
<div class="server-badge instant">⚡ INSTANT</div>
|
||
${server.oos ? '<div class="server-badge" style="background:#ef4444;color:#fff;margin-left:8px;">OOS</div>' : ''}
|
||
<div class="server-title">${server.cpu}</div>
|
||
<div class="server-specs-row">
|
||
<span class="spec-pill">${server.cores} cores</span>
|
||
<span class="spec-pill">${server.ghz} GHz</span>
|
||
<span class="spec-pill">${server.ram}</span>
|
||
<span class="spec-pill">${server.storage}</span>
|
||
</div>
|
||
<div class="server-meta-row">
|
||
<div class="server-location">📍 ${server.location}</div>
|
||
<div class="server-price-display">${server.currency}${server.price}<small>/mo</small></div>
|
||
</div>
|
||
</div>
|
||
`).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();
|
||
}
|
||
|
||
// 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}"]`);
|
||
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
|
||
function renderCustomServers() {
|
||
const grid = document.getElementById('customServersGrid');
|
||
grid.innerHTML = customServers.map(server => `
|
||
<div class="server-option custom ${selectedServer?.id === server.id ? 'active' : ''}" data-id="${server.id}" onclick="selectCustomServer('${server.id}')">
|
||
<div class="server-top-row">
|
||
<div class="server-badge custom">BUILD</div>
|
||
<div class="server-price-display" style="font-size:0.9rem;">From ${server.currency}${server.basePrice}<small>/mo</small></div>
|
||
</div>
|
||
<div class="server-title">${server.cpu}</div>
|
||
<div class="server-specs-row">
|
||
<span class="spec-pill">${server.cores} cores</span>
|
||
<span class="spec-pill">${server.ghz} GHz</span>
|
||
<span class="spec-pill">${server.ram}</span>
|
||
</div>
|
||
<div class="server-meta-row">
|
||
<div>⏱️ Setup: ${server.setupTime}</div>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
// Select Instant Server
|
||
function selectInstantServer(id) {
|
||
selectedServer = instantServers.find(s => s.id === id);
|
||
renderInstantServers();
|
||
|
||
// 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();
|
||
updateSpecs(selectedServer);
|
||
}
|
||
|
||
// Close Instant Customization
|
||
function closeInstantCustomization() {
|
||
isInstantCustomized = false;
|
||
document.getElementById('instantOptions').style.display = 'none';
|
||
document.getElementById('instantServersGrid').style.display = 'grid';
|
||
updateSummary();
|
||
}
|
||
|
||
// Customize Instant Server Button Logic
|
||
document.getElementById('btnCustomizeInstant').addEventListener('click', async function(e) {
|
||
e.preventDefault();
|
||
isInstantCustomized = true;
|
||
|
||
const instantOptions = document.getElementById('instantOptions');
|
||
const serversGrid = document.getElementById('instantServersGrid');
|
||
const loader = document.getElementById('instantConfigLoader');
|
||
const container = document.getElementById('instantDynamicConfigContainer');
|
||
|
||
// Hide Grid, Show Options (View Swap)
|
||
serversGrid.style.display = 'none';
|
||
instantOptions.style.display = 'block';
|
||
|
||
loader.style.display = 'flex';
|
||
container.innerHTML = '';
|
||
|
||
configState = {};
|
||
storageSelection = {};
|
||
|
||
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];
|
||
}
|
||
|
||
// Get Options
|
||
const optionsData = await scraper.getProductConfig(scrapeUrl, selectedServer);
|
||
|
||
if (optionsData && optionsData.options.length > 0) {
|
||
// 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));
|
||
}
|
||
});
|
||
}
|
||
|
||
// Parse Storage Requirements - More robust parsing
|
||
let storageRequirements = [];
|
||
if (selectedServer.storage) {
|
||
const parts = selectedServer.storage.split('+');
|
||
parts.forEach(part => {
|
||
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.push({
|
||
qty: parseInt(match[1]),
|
||
spec: match[2].trim().toLowerCase()
|
||
});
|
||
} else {
|
||
storageRequirements.push({
|
||
qty: 1,
|
||
spec: trimmed.toLowerCase()
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
// For storage options, we'll handle them in the pooled grid
|
||
// So we skip individual storage matching here
|
||
const storageOptions = options.filter(opt => opt.type === 'storage');
|
||
const nonStorageOptions = options.filter(opt => opt.type !== 'storage');
|
||
|
||
// Enhanced matching for non-storage instant server specs
|
||
nonStorageOptions.forEach(opt => {
|
||
let selectedValId = null;
|
||
let urlId = preselected[opt.id];
|
||
|
||
// Enhanced RAM matching
|
||
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;
|
||
}
|
||
}
|
||
|
||
// Fallback to URL ID if available and valid
|
||
if (!selectedValId && urlId && opt.values.find(v => v.id === urlId)) {
|
||
selectedValId = urlId;
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
|
||
// Last resort - pick first option (avoid "None" if possible)
|
||
if (!selectedValId && opt.values.length > 0) {
|
||
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);
|
||
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');
|
||
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("Instant Customization Error:", err);
|
||
renderFallbackOptions(container);
|
||
} finally {
|
||
loader.style.display = 'none';
|
||
}
|
||
|
||
updateSummary();
|
||
});
|
||
|
||
// Select Custom Server
|
||
async function selectCustomServer(id) {
|
||
selectedServer = customServers.find(s => s.id === id);
|
||
renderCustomServers();
|
||
|
||
document.getElementById('instantToggleContainer').style.display = 'none';
|
||
|
||
const optionsContainer = document.getElementById('customOptions');
|
||
optionsContainer.style.display = 'block';
|
||
|
||
const loader = document.getElementById('configLoader');
|
||
const dynamicContainer = document.getElementById('dynamicConfigContainer');
|
||
|
||
// Show loader, hide container
|
||
loader.style.display = 'flex';
|
||
dynamicContainer.innerHTML = '';
|
||
|
||
optionsContainer.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||
|
||
// Reset selections
|
||
configState = {};
|
||
storageSelection = {};
|
||
|
||
// Fetch real-time options
|
||
const optionsData = await scraper.getProductConfig(selectedServer.configUrl, selectedServer);
|
||
|
||
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
|
||
console.warn("Scrape failed, using fallback");
|
||
renderFallbackOptions(dynamicContainer);
|
||
}
|
||
|
||
updateSummary();
|
||
}
|
||
|
||
function renderFallbackOptions(container) {
|
||
let html = '';
|
||
|
||
// 1. RAM Configuration
|
||
html += '<div class="section-label" style="margin-top:1rem;">💾 RAM Configuration</div>';
|
||
html += '<div class="config-grid wide">';
|
||
const ramOptions = [
|
||
{ name: '128 GB ECC', price: 0 },
|
||
{ name: '256 GB ECC', price: 100 },
|
||
{ name: '384 GB ECC', price: 200 },
|
||
{ name: '512 GB ECC', price: 400 },
|
||
{ name: '768 GB ECC', price: 600 },
|
||
{ name: '1.5 TB ECC', price: 1300 }
|
||
];
|
||
|
||
ramOptions.forEach((opt, idx) => {
|
||
const priceText = opt.price === 0 ? 'Included' : `+€${opt.price}`;
|
||
const priceClass = opt.price === 0 ? 'included' : '';
|
||
const activeClass = idx === 1 ? 'active' : '';
|
||
|
||
html += `
|
||
<div class="config-card ${activeClass}" onclick="selectFallbackOption(this, 'ram', '${opt.name}', ${opt.price})">
|
||
<div class="option-name">${opt.name}</div>
|
||
<div class="option-price ${priceClass}">${priceText}</div>
|
||
</div>`;
|
||
|
||
if(idx === 1) {
|
||
configState['RAM Configuration'] = opt.price;
|
||
setTimeout(() => { document.getElementById('summaryRam').textContent = opt.name; updateSummary(); }, 0);
|
||
}
|
||
});
|
||
html += '</div>';
|
||
|
||
// 2. Storage Configuration (Split by Gen4 / Gen5)
|
||
const limits = selectedServer.limits || { gen4: 3, gen5: 8 };
|
||
|
||
// --- Gen4 Section ---
|
||
if (limits.gen4 > 0) {
|
||
html += '<div class="section-label" style="margin-top:1rem;">💿 Gen4 NVMe Storage (Consumer)</div>';
|
||
html += `
|
||
<div class="storage-slots-container">
|
||
<div class="slots-header">
|
||
<span>Available Slots: <strong id="slotsCount-gen4">0/${limits.gen4}</strong></span>
|
||
<span id="slotsStatus-gen4" style="color:var(--success); font-size:0.75rem;">Select drives</span>
|
||
</div>
|
||
<div class="slots-visual" id="slotsVisual-gen4">
|
||
${Array(limits.gen4).fill('<div class="drive-slot"></div>').join('')}
|
||
</div>
|
||
</div>`;
|
||
|
||
html += '<div class="config-grid">';
|
||
const gen4Drives = [
|
||
{ id: 'nvme-1tb-g4', name: '1 TB NVMe Gen4', price: 40 },
|
||
{ id: 'nvme-2tb-g4', name: '2 TB NVMe Gen4', price: 75 },
|
||
{ id: 'nvme-4tb-g4', name: '4 TB NVMe Gen4', price: 140 }
|
||
];
|
||
|
||
gen4Drives.forEach((drive) => {
|
||
html += `
|
||
<div class="config-card has-stepper" id="card-${drive.id}">
|
||
<div class="option-name">${drive.name}</div>
|
||
<div class="option-price">+€${drive.price}</div>
|
||
<div class="qty-stepper">
|
||
<button class="btn-qty" onclick="event.stopPropagation(); updateStorageQty('${drive.id}', -1, ${drive.price}, '${drive.name}', 'gen4', ${limits.gen4})">-</button>
|
||
<span class="qty-val" id="qty-${drive.id}">0</span>
|
||
<button class="btn-qty" onclick="event.stopPropagation(); updateStorageQty('${drive.id}', 1, ${drive.price}, '${drive.name}', 'gen4', ${limits.gen4})">+</button>
|
||
</div>
|
||
</div>`;
|
||
});
|
||
html += '</div>';
|
||
}
|
||
|
||
// --- Gen5 Section ---
|
||
if (limits.gen5 > 0) {
|
||
html += '<div class="section-label" style="margin-top:1rem;">🚀 Gen5 NVMe Storage (Enterprise)</div>';
|
||
html += `
|
||
<div class="storage-slots-container">
|
||
<div class="slots-header">
|
||
<span>Available Slots: <strong id="slotsCount-gen5">0/${limits.gen5}</strong></span>
|
||
<span id="slotsStatus-gen5" style="color:var(--success); font-size:0.75rem;">Select drives</span>
|
||
</div>
|
||
<div class="slots-visual" id="slotsVisual-gen5">
|
||
${Array(limits.gen5).fill('<div class="drive-slot"></div>').join('')}
|
||
</div>
|
||
</div>`;
|
||
|
||
html += '<div class="config-grid">';
|
||
const gen5Drives = [
|
||
{ id: 'nvme-2tb-g5', name: '2 TB NVMe Gen5 Ent', price: 180 },
|
||
{ id: 'nvme-4tb-g5', name: '4 TB NVMe Gen5 Ent', price: 350 },
|
||
{ id: 'nvme-8tb-g5', name: '8 TB NVMe Gen5 Ent', price: 650 }
|
||
];
|
||
|
||
gen5Drives.forEach((drive) => {
|
||
html += `
|
||
<div class="config-card has-stepper" id="card-${drive.id}">
|
||
<div class="option-name">${drive.name}</div>
|
||
<div class="option-price">+€${drive.price}</div>
|
||
<div class="qty-stepper">
|
||
<button class="btn-qty" onclick="event.stopPropagation(); updateStorageQty('${drive.id}', -1, ${drive.price}, '${drive.name}', 'gen5', ${limits.gen5})">-</button>
|
||
<span class="qty-val" id="qty-${drive.id}">0</span>
|
||
<button class="btn-qty" onclick="event.stopPropagation(); updateStorageQty('${drive.id}', 1, ${drive.price}, '${drive.name}', 'gen5', ${limits.gen5})">+</button>
|
||
</div>
|
||
</div>`;
|
||
});
|
||
html += '</div>';
|
||
}
|
||
|
||
// 3. Location
|
||
html += '<div class="section-label" style="margin-top:1rem;">📍 Data Center Location</div>';
|
||
html += '<div class="config-grid">';
|
||
const locOptions = [
|
||
{ name: 'Rotterdam, NL', price: 0 },
|
||
{ name: 'Frankfurt, DE', price: 50 },
|
||
{ name: 'London, UK', price: 50 },
|
||
{ name: 'New York, US', price: 100 },
|
||
{ name: 'Amsterdam, NL', price: 20 }
|
||
];
|
||
|
||
locOptions.forEach((opt, idx) => {
|
||
const priceText = opt.price === 0 ? 'Included' : `+€${opt.price}`;
|
||
const priceClass = opt.price === 0 ? 'included' : '';
|
||
const activeClass = idx === 0 ? 'active' : '';
|
||
|
||
html += `
|
||
<div class="config-card ${activeClass}" onclick="selectFallbackOption(this, 'location', '${opt.name}', ${opt.price})">
|
||
<div class="option-name">${opt.name}</div>
|
||
<div class="option-price ${priceClass}">${priceText}</div>
|
||
</div>`;
|
||
|
||
if(idx === 0) {
|
||
configState['Location'] = opt.price;
|
||
setTimeout(() => { document.getElementById('summaryLocation').textContent = opt.name; updateSummary(); }, 0);
|
||
}
|
||
});
|
||
html += '</div>';
|
||
|
||
// 4. Network
|
||
html += '<div class="section-label" style="margin-top:1rem;">🌐 Network Uplink</div>';
|
||
html += '<div class="config-grid wide">';
|
||
const netOptions = [
|
||
{ name: '1 Gbps Unmetered', price: 0 },
|
||
{ name: '10 Gbps Dedicated', price: 50 },
|
||
{ name: '2× 10 Gbps LACP', price: 100 }
|
||
];
|
||
|
||
netOptions.forEach((opt, idx) => {
|
||
const priceText = opt.price === 0 ? 'Included' : `+€${opt.price}`;
|
||
const priceClass = opt.price === 0 ? 'included' : '';
|
||
const activeClass = idx === 0 ? 'active' : '';
|
||
|
||
html += `
|
||
<div class="config-card ${activeClass}" onclick="selectFallbackOption(this, 'network', '${opt.name}', ${opt.price})">
|
||
<div class="option-name">${opt.name}</div>
|
||
<div class="option-price ${priceClass}">${priceText}</div>
|
||
</div>`;
|
||
|
||
if(idx === 0) {
|
||
configState['Network'] = opt.price;
|
||
setTimeout(() => { document.getElementById('summaryNetwork').textContent = opt.name; updateSummary(); }, 0);
|
||
}
|
||
});
|
||
html += '</div>';
|
||
|
||
container.innerHTML = html;
|
||
|
||
// Initialize storage summary
|
||
calculateStorageTotal();
|
||
}
|
||
|
||
// Helper for fallback clicks
|
||
window.selectFallbackOption = function(card, type, name, price) {
|
||
const grid = card.parentElement;
|
||
grid.querySelectorAll('.config-card').forEach(c => c.classList.remove('active'));
|
||
card.classList.add('active');
|
||
|
||
if(type === 'ram') {
|
||
configState['RAM Configuration'] = price;
|
||
document.getElementById('summaryRam').textContent = name;
|
||
const specRam = document.getElementById('spec-ram-capacity-hero');
|
||
if(specRam) specRam.innerText = name;
|
||
} else if(type === 'location') {
|
||
configState['Location'] = price;
|
||
document.getElementById('summaryLocation').textContent = name;
|
||
} else if(type === 'network') {
|
||
configState['Network'] = price;
|
||
document.getElementById('summaryNetwork').textContent = name;
|
||
const specNetwork = document.getElementById('spec-network-speed-hero');
|
||
if(specNetwork) specNetwork.innerText = name;
|
||
}
|
||
|
||
updateSummary();
|
||
};
|
||
|
||
// Storage Logic (Updated for Categories)
|
||
window.updateStorageQty = function(id, change, price, name, category, maxLimit) {
|
||
if(!storageSelection[id]) storageSelection[id] = { qty: 0, price, name, category };
|
||
|
||
let newQty = storageSelection[id].qty + change;
|
||
if(newQty < 0) newQty = 0;
|
||
|
||
// Calculate current total for this category
|
||
const currentCategoryTotal = Object.values(storageSelection)
|
||
.filter(item => item.category === category)
|
||
.reduce((acc, curr) => acc + curr.qty, 0);
|
||
|
||
// Check if adding would exceed limit (only if increasing)
|
||
if(change > 0 && (currentCategoryTotal + change) > maxLimit) {
|
||
// Optionally show a small toast or just shake the UI
|
||
return;
|
||
}
|
||
|
||
storageSelection[id].qty = newQty;
|
||
|
||
// Update UI
|
||
document.getElementById(`qty-${id}`).innerText = newQty;
|
||
const card = document.getElementById(`card-${id}`);
|
||
if(newQty > 0) card.classList.add('active');
|
||
else card.classList.remove('active');
|
||
|
||
calculateStorageTotal();
|
||
updateSlotsVisual(category, maxLimit);
|
||
};
|
||
|
||
window.updateSlotsVisual = function(category, maxSlots) {
|
||
const currentTotal = Object.values(storageSelection)
|
||
.filter(item => item.category === category)
|
||
.reduce((acc, curr) => acc + curr.qty, 0);
|
||
|
||
const visualContainer = document.getElementById(`slotsVisual-${category}`);
|
||
const countLabel = document.getElementById(`slotsCount-${category}`);
|
||
const statusLabel = document.getElementById(`slotsStatus-${category}`);
|
||
|
||
if(!visualContainer) return;
|
||
|
||
// Update Count
|
||
countLabel.innerText = `${currentTotal}/${maxSlots}`;
|
||
|
||
// Update Visuals
|
||
const slots = visualContainer.getElementsByClassName('drive-slot');
|
||
for(let i = 0; i < maxSlots; i++) {
|
||
if(i < currentTotal) {
|
||
slots[i].classList.add('filled');
|
||
if(category === 'gen5') slots[i].classList.add('gen5');
|
||
} else {
|
||
slots[i].classList.remove('filled');
|
||
slots[i].classList.remove('gen5');
|
||
}
|
||
}
|
||
|
||
// Update Status Text
|
||
if(currentTotal === 0) {
|
||
statusLabel.innerText = "Select drives below";
|
||
statusLabel.style.color = "var(--text-secondary)";
|
||
} else if (currentTotal >= maxSlots) {
|
||
statusLabel.innerText = "Max capacity reached";
|
||
statusLabel.style.color = "var(--warning)";
|
||
} else {
|
||
statusLabel.innerText = `${maxSlots - currentTotal} slots remaining`;
|
||
statusLabel.style.color = "var(--success)";
|
||
}
|
||
};
|
||
|
||
window.calculateStorageTotal = function() {
|
||
let total = 0;
|
||
let summaryHtml = '';
|
||
|
||
Object.values(storageSelection).forEach(item => {
|
||
if(item.qty > 0) {
|
||
total += item.qty * item.price;
|
||
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;
|
||
|
||
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.innerHTML = summaryHtml;
|
||
|
||
updateSummary();
|
||
};
|
||
|
||
function renderDynamicOptions(options, isInstant = false, preselected = {}) {
|
||
const container = isInstant ? document.getElementById('instantDynamicConfigContainer') : document.getElementById('dynamicConfigContainer');
|
||
container.innerHTML = '';
|
||
|
||
const groups = { core: [], storage: [], network: [], other: [] };
|
||
|
||
options.forEach(opt => {
|
||
if (opt.type === 'ram' || opt.type === 'location') groups.core.push(opt);
|
||
else if (opt.type === 'storage') groups.storage.push(opt);
|
||
else if (opt.type === 'network') groups.network.push(opt);
|
||
else groups.other.push(opt);
|
||
});
|
||
|
||
// 1. Core (RAM, Location)
|
||
groups.core.forEach(opt => {
|
||
const label = document.createElement('div');
|
||
label.className = 'section-label';
|
||
label.style.marginTop = '1.5rem';
|
||
let icon = '⚙️';
|
||
if(opt.type === 'ram') icon = '💾 RAM CONFIGURATION';
|
||
else if(opt.type === 'location') icon = '📍 DATA CENTER LOCATION';
|
||
label.textContent = icon;
|
||
container.appendChild(label);
|
||
container.appendChild(createOptionGrid(opt, true, -1, isInstant, preselected));
|
||
});
|
||
|
||
// 2. Storage
|
||
if (groups.storage.length > 0) {
|
||
// APPLY LIMITS if available (fixes "too many drives" issue)
|
||
if (selectedServer && selectedServer.limits) {
|
||
const limitTotal = (selectedServer.limits.gen4 || 0) + (selectedServer.limits.gen5 || 0);
|
||
if (limitTotal > 0 && groups.storage.length > limitTotal) {
|
||
console.log(`Limiting storage options from ${groups.storage.length} to ${limitTotal} based on server specs.`);
|
||
groups.storage = groups.storage.slice(0, limitTotal);
|
||
}
|
||
}
|
||
|
||
// Group storage options by signature (content similarity)
|
||
// This allows us to separate "Consumer Gen4 Slots" from "Enterprise Gen5 Slots" automatically
|
||
const storageGroups = {};
|
||
const groupOrder = []; // Preserve order
|
||
|
||
groups.storage.forEach(opt => {
|
||
// Create a signature based on available values.
|
||
// We use a simplified signature: text + price of all options
|
||
// We trim and lowercase to be robust against minor scraping diffs
|
||
const signature = opt.values.map(v => v.text.trim().toLowerCase() + '|' + v.price).join('||');
|
||
|
||
if (!storageGroups[signature]) {
|
||
storageGroups[signature] = [];
|
||
groupOrder.push(signature);
|
||
}
|
||
storageGroups[signature].push(opt);
|
||
});
|
||
|
||
// Render each group
|
||
groupOrder.forEach((sig, groupIndex) => {
|
||
const groupOpts = storageGroups[sig];
|
||
const firstOpt = groupOpts[0];
|
||
|
||
const label = document.createElement('div');
|
||
label.className = 'section-label';
|
||
label.style.marginTop = '1.5rem';
|
||
|
||
// Detect Title based on content
|
||
const firstLabel = firstOpt.label.toLowerCase();
|
||
// Check values for keywords
|
||
const hasGen5 = firstOpt.values.some(v => v.text.toLowerCase().includes('gen5'));
|
||
const hasGen4 = firstOpt.values.some(v => v.text.toLowerCase().includes('gen4'));
|
||
const hasEnt = firstOpt.values.some(v => v.text.toLowerCase().includes('enterprise') || v.text.toLowerCase().includes('ent'));
|
||
|
||
let title = '💿 STORAGE CONFIGURATION';
|
||
if (hasGen5) title = '🚀 GEN5 NVME STORAGE (ENTERPRISE)';
|
||
else if (hasGen4) title = '💿 GEN4 NVME STORAGE (CONSUMER)';
|
||
else if (hasEnt) title = '💾 ENTERPRISE STORAGE';
|
||
|
||
// Append Group Index if multiple groups exist to differentiate
|
||
if (groupOrder.length > 1) {
|
||
title += ` (Group ${groupIndex + 1})`;
|
||
}
|
||
|
||
label.textContent = title;
|
||
container.appendChild(label);
|
||
|
||
// Visualizer for THIS group
|
||
const maxSlots = groupOpts.length;
|
||
const visualContainer = document.createElement('div');
|
||
visualContainer.className = 'storage-slots-container';
|
||
const uniqueSuffix = (isInstant ? '-instant' : '') + '-g' + groupIndex;
|
||
|
||
visualContainer.innerHTML = `
|
||
<div class="slots-header">
|
||
<span>Available Slots: <strong id="dynamic-slots-count${uniqueSuffix}">0/${maxSlots}</strong></span>
|
||
<span id="dynamic-slots-status${uniqueSuffix}" style="color:var(--success); font-size:0.75rem;">Select drives</span>
|
||
</div>
|
||
<div class="slots-visual" id="dynamic-slots-visual${uniqueSuffix}">
|
||
${Array(maxSlots).fill('<div class="drive-slot"></div>').join('')}
|
||
</div>
|
||
`;
|
||
container.appendChild(visualContainer);
|
||
|
||
// Always use pooled grid for these groups as they are by definition identical
|
||
container.appendChild(createPooledStorageGrid(groupOpts, isInstant, preselected, groupIndex, storageRequirements));
|
||
});
|
||
}
|
||
|
||
// 3. Network & Other
|
||
const remaining = [...groups.network, ...groups.other];
|
||
if (remaining.length > 0) {
|
||
const label = document.createElement('div');
|
||
label.className = 'section-label';
|
||
label.style.marginTop = '1.5rem';
|
||
label.textContent = '🌐 NETWORK & EXTRAS';
|
||
container.appendChild(label);
|
||
remaining.forEach(opt => {
|
||
const sub = document.createElement('div');
|
||
sub.style.marginBottom = '0.5rem';
|
||
sub.style.fontWeight = '600';
|
||
sub.style.fontSize = '0.9rem';
|
||
sub.textContent = opt.label;
|
||
container.appendChild(sub);
|
||
container.appendChild(createOptionGrid(opt, true, -1, isInstant, preselected));
|
||
});
|
||
}
|
||
|
||
updateSummary();
|
||
}
|
||
|
||
// New Pooled Storage Grid
|
||
function createPooledStorageGrid(storageOptions, isInstant, preselected = {}, groupIndex = 0, storageRequirements = []) {
|
||
const grid = document.createElement('div');
|
||
grid.className = 'config-grid';
|
||
|
||
const templateValues = storageOptions[0].values;
|
||
|
||
if (!window.pooledState) window.pooledState = {};
|
||
// Use a unique key for each group to prevent conflicts
|
||
const poolKey = (isInstant ? 'instant' : 'custom') + '-g' + groupIndex;
|
||
|
||
// Storage requirements are now passed as a parameter for instant customization
|
||
|
||
// 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,
|
||
label: opt.label,
|
||
currentVal: initialVal,
|
||
values: opt.values
|
||
};
|
||
});
|
||
|
||
window.pooledState[poolKey] = { slots: initializedSlots };
|
||
|
||
// For instant customization, pre-fill with the server's storage configuration
|
||
if (isInstant && storageRequirements.length > 0) {
|
||
// First, reset all slots to None
|
||
initializedSlots.forEach(slot => {
|
||
const noneOption = slot.values.find(v =>
|
||
v.text.trim() === '-' ||
|
||
v.text.toLowerCase().includes('none') ||
|
||
v.text.toLowerCase().includes('no hard drive')
|
||
);
|
||
if (noneOption) {
|
||
slot.currentVal = noneOption;
|
||
configState[slot.label] = noneOption.price;
|
||
configIds[slot.id] = noneOption.id;
|
||
}
|
||
});
|
||
|
||
// 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 exact matching drive type
|
||
const match = emptySlot.values.find(v => {
|
||
const vText = v.text.toLowerCase();
|
||
|
||
// Parse both to normalized form for comparison
|
||
const normalizeSpec = (spec) => {
|
||
return spec.toLowerCase()
|
||
.replace(/\s+/g, ' ')
|
||
.replace(/nvme/g, '')
|
||
.replace(/ssd/g, '')
|
||
.replace(/\s+/g, ' ')
|
||
.trim();
|
||
};
|
||
|
||
// Extract capacity from value
|
||
const valCapacityMatch = vText.match(/(\d+(?:\.\d+)?)(?:\s*tb)?/i);
|
||
const valCapacity = valCapacityMatch ? parseFloat(valCapacityMatch[1]) : 0;
|
||
|
||
// Extract capacity from requirement
|
||
const reqCapacityMatch = req.spec.match(/(\d+(?:\.\d+)?)(?:\s*tb)?/i);
|
||
const reqCapacity = reqCapacityMatch ? parseFloat(reqCapacityMatch[1]) : 0;
|
||
|
||
// Match capacity exactly
|
||
if (Math.abs(valCapacity - reqCapacity) < 0.1) {
|
||
// Check for exact brand match first
|
||
if (vText.includes('crucial') && req.spec.includes('crucial')) return true;
|
||
if (vText.includes('kioxia') && req.spec.includes('kioxia')) return true;
|
||
if (vText.includes('samsung') && req.spec.includes('samsung')) return true;
|
||
if (vText.includes('solidigm') && req.spec.includes('solidigm')) return true;
|
||
if (vText.includes('wd') && req.spec.includes('wd')) return true;
|
||
if (vText.includes('seagate') && req.spec.includes('seagate')) return true;
|
||
if (vText.includes('kingston') && req.spec.includes('kingston')) return true;
|
||
|
||
// If no brand match, check if it's just generic NVMe
|
||
if (!req.spec.includes('crucial') && !req.spec.includes('kioxia') &&
|
||
!req.spec.includes('samsung') && !req.spec.includes('solidigm')) {
|
||
return vText.includes('nvme');
|
||
}
|
||
}
|
||
|
||
return false;
|
||
});
|
||
|
||
if (match) {
|
||
emptySlot.currentVal = match;
|
||
configState[emptySlot.label] = match.price;
|
||
configIds[emptySlot.id] = match.id;
|
||
} else {
|
||
console.warn(`Could not find matching drive for requirement: ${req.spec}`);
|
||
}
|
||
} else {
|
||
console.warn(`No empty slots available for requirement: ${req.spec}`);
|
||
}
|
||
}
|
||
});
|
||
} 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, '')}`;
|
||
|
||
let priceText = val.price === 0 ? 'Included' : `+€${val.price}`;
|
||
let displayName = val.text.replace(/\s?\(.*?\)/, '');
|
||
|
||
card.innerHTML = `
|
||
<div class="option-name">${displayName}</div>
|
||
<div class="option-price">${priceText}</div>
|
||
<div class="qty-stepper">
|
||
<button class="btn-qty" onclick="updatePooledQty('${poolKey}', '${val.text.replace(/'/g, "\\'")}', -1, ${isInstant})">-</button>
|
||
<span class="qty-val" id="pool-qty-${poolKey}-${val.text.replace(/\s/g, '')}">0</span>
|
||
<button class="btn-qty" onclick="updatePooledQty('${poolKey}', '${val.text.replace(/'/g, "\\'")}', 1, ${isInstant})">+</button>
|
||
</div>
|
||
`;
|
||
grid.appendChild(card);
|
||
});
|
||
|
||
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;
|
||
}
|
||
|
||
window.updatePooledQty = function(poolKey, valText, change, isInstant) {
|
||
const pool = window.pooledState[poolKey];
|
||
if (!pool) return;
|
||
|
||
if (change > 0) {
|
||
// Find first slot that is "None" or default (assuming index 0 is None/Default)
|
||
// We need to identify what is "Empty". We assume the first value in options list is "Empty" or "None".
|
||
const targetSlot = pool.slots.find(s => s.currentVal === s.values[0]);
|
||
|
||
if (targetSlot) {
|
||
// Find the matching value object in this slot's values
|
||
const newVal = targetSlot.values.find(v => v.text === valText);
|
||
if (newVal) {
|
||
targetSlot.currentVal = newVal;
|
||
|
||
// Update Global Config State
|
||
configState[targetSlot.label] = newVal.price;
|
||
configIds[targetSlot.id] = newVal.id;
|
||
}
|
||
}
|
||
} else {
|
||
// Decrement: Find last slot that matches this value
|
||
// We search array in reverse to remove from end
|
||
for (let i = pool.slots.length - 1; i >= 0; i--) {
|
||
if (pool.slots[i].currentVal.text === valText) {
|
||
// Reset to default (index 0)
|
||
const defaultVal = pool.slots[i].values[0];
|
||
pool.slots[i].currentVal = defaultVal;
|
||
|
||
// Update Global Config State
|
||
configState[pool.slots[i].label] = defaultVal.price;
|
||
configIds[pool.slots[i].id] = defaultVal.id;
|
||
break; // Only remove one
|
||
}
|
||
}
|
||
}
|
||
|
||
updatePooledVisuals(poolKey, isInstant);
|
||
updateSummary();
|
||
};
|
||
|
||
window.updatePooledVisuals = function(poolKey, isInstant) {
|
||
const pool = window.pooledState[poolKey];
|
||
if (!pool) return;
|
||
|
||
// Extract group index from poolKey (e.g., "instant-g0" -> "0")
|
||
const parts = poolKey.split('-g');
|
||
const groupIndex = parts.length > 1 ? parts[1] : '0';
|
||
|
||
const uniqueSuffix = (isInstant ? '-instant' : '') + '-g' + groupIndex;
|
||
const slots = document.querySelectorAll(`#dynamic-slots-visual${uniqueSuffix} .drive-slot`);
|
||
const countEl = document.getElementById(`dynamic-slots-count${uniqueSuffix}`);
|
||
const statusEl = document.getElementById(`dynamic-slots-status${uniqueSuffix}`);
|
||
|
||
let filledCount = 0;
|
||
|
||
// Reset counts map for UI
|
||
const qtyMap = {};
|
||
|
||
pool.slots.forEach((slot, index) => {
|
||
// 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) {
|
||
slots[index].classList.add('filled');
|
||
if (slot.currentVal.text.toLowerCase().includes('gen5')) slots[index].classList.add('gen5');
|
||
else slots[index].classList.remove('gen5');
|
||
filledCount++;
|
||
|
||
// Track quantity for stepper numbers
|
||
const key = slot.currentVal.text.replace(/\s/g, '');
|
||
qtyMap[key] = (qtyMap[key] || 0) + 1;
|
||
} else {
|
||
slots[index].classList.remove('filled');
|
||
slots[index].classList.remove('gen5');
|
||
}
|
||
}
|
||
});
|
||
|
||
// Update Stepper UI Numbers
|
||
document.querySelectorAll(`[id^="pool-qty-${poolKey}-"]`).forEach(el => el.textContent = '0');
|
||
document.querySelectorAll(`[id^="pool-card-"]`).forEach(el => el.classList.remove('active'));
|
||
|
||
Object.entries(qtyMap).forEach(([key, qty]) => {
|
||
const qtyEl = document.getElementById(`pool-qty-${poolKey}-${key}`);
|
||
const cardEl = document.getElementById(`pool-card-${key}`);
|
||
if (qtyEl) qtyEl.textContent = qty;
|
||
if (cardEl) cardEl.classList.add('active');
|
||
});
|
||
|
||
if (countEl) countEl.textContent = `${filledCount}/${pool.slots.length}`;
|
||
|
||
if (statusEl) {
|
||
if (filledCount === 0) {
|
||
statusEl.textContent = "Select drives below";
|
||
statusEl.style.color = "var(--text-secondary)";
|
||
} else if (filledCount >= pool.slots.length) {
|
||
statusEl.textContent = "Max capacity reached";
|
||
statusEl.style.color = "var(--brand-primary)";
|
||
} else {
|
||
statusEl.textContent = `${pool.slots.length - filledCount} slots remaining`;
|
||
statusEl.style.color = "var(--success)";
|
||
}
|
||
}
|
||
|
||
// Update Summary Text
|
||
const storageNames = pool.slots
|
||
.filter(s => !s.currentVal.text.match(/^(none|-|select|no\s)/i))
|
||
.map(s => s.currentVal.text.replace(/\s?\(.*?\)/, ''));
|
||
|
||
// 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.innerHTML = pillsHtml;
|
||
};
|
||
|
||
function createOptionGrid(opt, isWide, storageIndex = -1, isInstant = false, preselected = {}) {
|
||
const grid = document.createElement('div');
|
||
grid.className = isWide ? 'config-grid wide' : 'config-grid';
|
||
|
||
opt.values.forEach((val, index) => {
|
||
const card = document.createElement('div');
|
||
card.className = 'config-card';
|
||
|
||
// Check if this value is the preselected one
|
||
const isPreselected = preselected[opt.id] && preselected[opt.id] === val.id;
|
||
// Default behavior: Select if preselected, OR if no preselection exists and it's the first item
|
||
const isActive = isPreselected || (!preselected[opt.id] && index === 0);
|
||
|
||
if (isActive) {
|
||
card.classList.add('active');
|
||
// Set initial state - use the relative price (already converted from absolute)
|
||
configState[opt.label] = val.price;
|
||
configIds[opt.id] = val.id;
|
||
|
||
// Update Summary Texts & Tech Specs
|
||
if (opt.type === 'ram') {
|
||
const cleanName = val.text.replace(/\s?\(.*?\)/, '');
|
||
document.getElementById('summaryRam').textContent = cleanName;
|
||
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-hero');
|
||
if(specNet) specNet.innerText = val.text;
|
||
}
|
||
|
||
// For storage, we need to aggregate
|
||
if (opt.type === 'storage') {
|
||
// Store selection for this "bay" (opt.id)
|
||
storageSelection[opt.id] = { name: val.text, price: val.price };
|
||
updateDynamicStorageVisual(storageIndex, val.text, isInstant);
|
||
}
|
||
}
|
||
|
||
let priceText = val.price === 0 ? 'Included' : `+€${val.price}`;
|
||
let priceClass = val.price === 0 ? 'included' : '';
|
||
let displayName = val.text.replace(/\s?\(.*?\)/, ''); // Clean text
|
||
|
||
card.innerHTML = `
|
||
<div class="option-name">${displayName}</div>
|
||
<div class="option-price ${priceClass}">${priceText}</div>
|
||
`;
|
||
|
||
card.addEventListener('click', () => {
|
||
grid.querySelectorAll('.config-card').forEach(c => c.classList.remove('active'));
|
||
card.classList.add('active');
|
||
|
||
configState[opt.label] = val.price;
|
||
configIds[opt.id] = val.id;
|
||
|
||
if (opt.type === 'ram') {
|
||
document.getElementById('summaryRam').textContent = displayName;
|
||
const specRam = document.getElementById('spec-ram-capacity');
|
||
if(specRam) specRam.innerText = displayName;
|
||
}
|
||
if (opt.type === 'location') document.getElementById('summaryLocation').textContent = displayName;
|
||
if (opt.type === 'network') {
|
||
document.getElementById('summaryNetwork').textContent = displayName;
|
||
const specNet = document.getElementById('spec-network-speed');
|
||
if(specNet) specNet.innerText = displayName;
|
||
}
|
||
|
||
if (opt.type === 'storage') {
|
||
storageSelection[opt.id] = { name: displayName, price: val.price };
|
||
// Recalculate total storage price
|
||
let totalStoragePrice = 0;
|
||
let storageNames = [];
|
||
Object.values(storageSelection).forEach(s => {
|
||
totalStoragePrice += s.price;
|
||
if(!s.name.toLowerCase().includes('none')) storageNames.push(s.name);
|
||
});
|
||
|
||
// Update visualizer
|
||
updateDynamicStorageVisual(storageIndex, displayName, isInstant);
|
||
|
||
// Update summary text
|
||
const storageText = storageNames.length > 0 ? storageNames.join(' + ') : 'None';
|
||
document.getElementById('summaryStorage').textContent = storageText;
|
||
|
||
// Update Tech Specs
|
||
const specStorage = document.getElementById('spec-storage-config-hero');
|
||
if(specStorage) specStorage.innerText = storageText;
|
||
}
|
||
|
||
updateSummary();
|
||
});
|
||
|
||
grid.appendChild(card);
|
||
});
|
||
|
||
// Initial trigger for storage summary if this is a storage grid
|
||
if (opt.type === 'storage') {
|
||
// Debounce initial summary update
|
||
setTimeout(() => {
|
||
let totalStoragePrice = 0;
|
||
let storageNames = [];
|
||
Object.values(storageSelection).forEach(s => {
|
||
totalStoragePrice += s.price;
|
||
if(!s.name.toLowerCase().includes('none')) storageNames.push(s.name);
|
||
});
|
||
document.getElementById('summaryStorage').textContent = storageNames.length > 0 ? storageNames.join(' + ') : 'None';
|
||
updateSummary();
|
||
}, 100);
|
||
}
|
||
|
||
return grid;
|
||
}
|
||
|
||
function updateDynamicStorageVisual(index, selectedName, isInstant = false) {
|
||
if(index === -1) return;
|
||
|
||
const suffix = isInstant ? '-instant' : '';
|
||
const slots = document.querySelectorAll(`#dynamic-slots-visual${suffix} .drive-slot`);
|
||
const countEl = document.getElementById(`dynamic-slots-count${suffix}`);
|
||
|
||
if(slots[index]) {
|
||
if(selectedName.toLowerCase().includes('none')) {
|
||
slots[index].classList.remove('filled');
|
||
slots[index].classList.remove('gen5');
|
||
} else {
|
||
slots[index].classList.add('filled');
|
||
// Try to detect Gen5
|
||
if(selectedName.toLowerCase().includes('gen5')) {
|
||
slots[index].classList.add('gen5');
|
||
} else {
|
||
slots[index].classList.remove('gen5');
|
||
}
|
||
}
|
||
}
|
||
|
||
// Count filled
|
||
const filled = document.querySelectorAll(`#dynamic-slots-visual${suffix} .filled`).length;
|
||
if(countEl) countEl.textContent = `${filled}/${slots.length}`;
|
||
}
|
||
|
||
function buildInstantOrderUrl() {
|
||
if (!selectedServer || !selectedServer.orderUrl) return null;
|
||
try {
|
||
// 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) {
|
||
// WHMCS format: configoption[123]
|
||
params.set(`configoption[${id}]`, val);
|
||
}
|
||
});
|
||
|
||
// 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');
|
||
const subtext = document.getElementById('summarySubtext');
|
||
const instantButtons = document.getElementById('instantButtons');
|
||
const configureBtn = document.getElementById('configureBtn');
|
||
const totalPriceEl = document.getElementById('totalPrice');
|
||
|
||
if (!selectedServer) {
|
||
// No server selected
|
||
pill.style.display = 'none';
|
||
subtext.textContent = 'Select a server to configure';
|
||
instantButtons.style.display = 'none';
|
||
configureBtn.style.display = 'none';
|
||
totalPriceEl.textContent = '-';
|
||
|
||
['Cpu', 'Ram', 'Storage', 'Network', 'Location'].forEach(k => {
|
||
const el = document.getElementById(`summary${k}`);
|
||
if(el) el.textContent = '-';
|
||
});
|
||
return;
|
||
}
|
||
pill.style.display = 'inline-block';
|
||
|
||
const isInstant = instantServers.some(s => s.id === selectedServer.id);
|
||
|
||
// Buttons
|
||
const btnDeployInstant = document.getElementById('btnDeployInstant');
|
||
|
||
if (isInstant) {
|
||
// Show Instant Buttons, Hide Custom Button
|
||
instantButtons.style.display = 'flex';
|
||
configureBtn.style.display = 'none';
|
||
|
||
pill.className = 'section-pill instant';
|
||
|
||
if (isInstantCustomized) {
|
||
pill.textContent = 'Customized Instant';
|
||
pill.style.background = '#E9D5FF';
|
||
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 = 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();
|
||
btnDeployInstant.href = customUrl || selectedServer.orderUrl;
|
||
|
||
// Update Button Labels for Customized State
|
||
const mainLabel = btnDeployInstant.querySelector('.main-label');
|
||
const subLabel = btnDeployInstant.querySelector('.sub-label');
|
||
if(mainLabel) mainLabel.textContent = 'Deploy Config';
|
||
if(subLabel) subLabel.textContent = 'Customized';
|
||
|
||
} else {
|
||
pill.textContent = 'Instant Deploy';
|
||
pill.style.background = 'var(--instant-color)';
|
||
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');
|
||
if(mainLabel) mainLabel.textContent = 'Deploy Now';
|
||
if(subLabel) subLabel.textContent = 'Default Config';
|
||
}
|
||
|
||
document.getElementById('summaryCpu').textContent = selectedServer.cpu;
|
||
if (!isInstantCustomized) {
|
||
document.getElementById('summaryRam').textContent = selectedServer.ram;
|
||
|
||
// 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;
|
||
}
|
||
|
||
} else {
|
||
// Custom Mode
|
||
instantButtons.style.display = 'none';
|
||
configureBtn.style.display = 'flex'; // Changed from block to flex for smart button
|
||
// configureBtn.textContent = 'Request Custom Build →'; // Removed to preserve icon structure
|
||
|
||
pill.className = 'section-pill custom';
|
||
pill.textContent = 'Custom Build';
|
||
subtext.textContent = `Setup time: ${selectedServer.setupTime}`;
|
||
|
||
let addonPrice = 0;
|
||
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;
|
||
|
||
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', () => {
|
||
document.querySelectorAll('.config-tab').forEach(t => t.classList.remove('active'));
|
||
tab.classList.add('active');
|
||
|
||
currentTab = tab.dataset.tab;
|
||
document.getElementById('instant-content').classList.toggle('active', currentTab === 'instant');
|
||
document.getElementById('custom-content').classList.toggle('active', currentTab === 'custom');
|
||
|
||
selectedServer = null;
|
||
document.getElementById('customOptions').style.display = 'none';
|
||
updateSummary();
|
||
});
|
||
});
|
||
|
||
// Initialize
|
||
renderInstantServers();
|
||
renderCustomServers();
|
||
|
||
// 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) {
|
||
selectInstantServer(instantServers[0].id);
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|