Files
Claude 01b4c09e73 fix: remove extra closing brace causing plans to not display
Fixed JavaScript syntax error where there was an extra closing brace
causing the page to break and plans not to show.

🤖 Generated with [Claude Code](https://claude.com/claude.com)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-06 02:28:02 +04:00

5696 lines
264 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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 &raquo; Feed" href="https://www.dedicatednodes.io/feed/" />
<link rel="alternate" type="application/rss+xml" title="DedicatedNodes &raquo; 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&#038;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;
}
/* Enhanced Storage Pills for Summary */
.storage-pill {
display: inline-flex;
align-items: center;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 0.4rem 0.75rem;
margin: 0.25rem 0.5rem 0.25rem 0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
}
.storage-pill:hover {
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
.storage-icon {
font-size: 1.2rem;
margin-right: 0.5rem;
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.1));
}
.storage-info {
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.storage-count {
font-size: 0.7rem;
font-weight: 600;
color: var(--brand-primary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.storage-name {
font-size: 0.85rem;
font-weight: 500;
color: var(--text-primary);
}
.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%;
}
/* Special handling for storage summary to allow multiple pills */
#summaryStorage {
max-width: 65%;
line-height: 1.2;
}
#summaryStorage .storage-pill {
margin-bottom: 0.5rem;
}
.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';
// Clear the customization container to avoid conflicts
const instantContainer = document.getElementById('instantDynamicConfigContainer');
if (instantContainer) instantContainer.innerHTML = '';
// Reset config state to clean values
configState = {};
configIds = {};
storageSelection = {};
// Clear any original drives tracking
if (window.originalDrives) {
window.originalDrives = [];
}
// Re-render the instant servers grid to ensure proper display
renderInstantServers();
// Update summary and specs for the selected instant server
updateSummary();
updateSpecs(selectedServer);
}
// 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));
}
});
}
// For instant customization, we need to map the actual storage requirements to available options
// Parse storage requirements from description and match them to available options
console.log("Processing instant server with storage:", selectedServer.storage);
// Parse storage requirements from description
const storageReqs = [];
const parts = selectedServer.storage.split('+');
parts.forEach(part => {
const trimmed = part.trim();
if (!trimmed) return;
const match = trimmed.match(/^(\d+)x\s+(.+)$/);
if (match) {
storageReqs.push({
qty: parseInt(match[1]),
spec: match[2].trim()
});
}
});
console.log("Parsed storage requirements:", storageReqs);
// Map requirements to available storage options
let reqIndex = 0;
options.forEach(opt => {
let selectedValId = null;
if (opt.type === 'storage' && reqIndex < storageReqs.length) {
const req = storageReqs[reqIndex];
let bestMatch = null;
let bestScore = 0;
// Find the best matching option for this requirement
opt.values.forEach(val => {
let score = 0;
const valText = val.text.toLowerCase();
const reqSpec = req.spec.toLowerCase();
// Check capacity match (exact match required)
const valCap = valText.match(/(\d+(?:\.\d+)?)\s*tb/i);
const reqCap = reqSpec.match(/(\d+(?:\.\d+)?)\s*tb/i);
if (valCap && reqCap && valCap[1] === reqCap[1]) {
score += 100;
}
// Check brand match
if (valText.includes('crucial') && reqSpec.includes('crucial')) score += 50;
if (valText.includes('kioxia') && reqSpec.includes('kioxia')) score += 50;
if (valText.includes('samsung') && reqSpec.includes('samsung')) score += 50;
// Check model match
if (valText.includes('t705') && reqSpec.includes('t705')) score += 50;
if (valText.includes('cm7') && reqSpec.includes('cm7')) score += 50;
if (score > bestScore) {
bestScore = score;
bestMatch = val;
}
});
if (bestMatch && bestScore >= 100) {
selectedValId = bestMatch.id;
preselected[opt.id] = selectedValId;
reqIndex++;
} else {
// If no match found, try to use URL preselected value
if (preselected[opt.id]) {
const urlMatch = opt.values.find(v => v.id === preselected[opt.id]);
if (urlMatch) {
selectedValId = preselected[opt.id];
}
}
}
} else if (opt.type !== 'storage' && preselected[opt.id]) {
// Use preselected IDs for non-storage options
selectedValId = preselected[opt.id];
}
// Apply price adjustment for selected non-storage options
if (opt.type !== 'storage' && 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));
});
}
}
});
// Render options with the exact WHMCS preselected values
// Also pass the storage requirements for proper pre-filling
renderDynamicOptions(options, true, preselected, storageReqs);
} 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 = {}, storageRequirements = []) {
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);
}
}
// For both instant and custom, group storage options by signature
// This creates a pooled grid where we can prefill with the correct drives
const storageGroups = {};
const groupOrder = []; // Preserve order
groups.storage.forEach(opt => {
// Create a signature based on available values
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;
// Initialize slots with preselected values from WHMCS or defaults
const initializedSlots = storageOptions.map(opt => {
const preId = preselected[opt.id];
let initialVal = opt.values[0]; // Default (usually None)
// Use the preselected value from WHMCS if available
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, track original drives and pre-fill with requirements
if (isInstant) {
window.originalDrives = [];
// 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
if (storageRequirements && storageRequirements.length > 0) {
// Clear storageSelection to start fresh
storageSelection = {};
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')
);
if (emptySlot) {
console.log(`Filling slot ${emptySlot.id} with ${req.spec}`);
// Find matching drive type
const match = emptySlot.values.find(v => {
const vText = v.text.toLowerCase();
const reqSpec = req.spec.toLowerCase();
// Extract capacity
const valCap = vText.match(/(\d+(?:\.\d+)?)\s*tb/i);
const reqCap = reqSpec.match(/(\d+(?:\.\d+)?)\s*tb/i);
if (valCap && reqCap && valCap[1] === reqCap[1]) {
// Check brand match
if ((vText.includes('crucial') && reqSpec.includes('crucial')) ||
(vText.includes('kioxia') && reqSpec.includes('kioxia')) ||
(vText.includes('t705') && reqSpec.includes('t705')) ||
(vText.includes('cm7') && reqSpec.includes('cm7'))) {
return true;
}
}
return false;
});
if (match) {
emptySlot.currentVal = match;
configState[emptySlot.label] = 0; // Included in base price
configIds[emptySlot.id] = match.id;
// Store in storageSelection for summary
const displayName = match.text.replace(/\s?\(.*?\)/, '');
storageSelection[emptySlot.id] = {
name: displayName,
price: 0 // Free for included drives
};
// Track as original drive
window.originalDrives.push({
slotId: emptySlot.id,
driveId: match.id,
text: match.text,
price: match.price
});
console.log(`✓ Filled with: ${match.text}`);
} else {
console.error(`✗ Could not find matching drive for: ${req.spec}`);
}
} else {
console.error(`✗ No empty slots available for: ${req.spec}`);
}
}
});
} else {
// Pre-set configState for custom servers
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);
// For instant customization, ensure storage summary is updated
if (isInstant) {
console.log("Updating storage summary for instant customization...");
updateStorageSummary();
updateSummary(); // Also call the main updateSummary
}
}, 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
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;
// For instant customization, calculate price difference
if (isInstant && window.originalDrives) {
// Check if this slot had an original drive
const originalDrive = window.originalDrives.find(d => d.slotId === targetSlot.id);
if (originalDrive) {
// Calculate price difference
const priceDiff = newVal.price - originalDrive.price;
configState[targetSlot.label] = priceDiff;
// Update original drives tracking
originalDrive.driveId = newVal.id;
originalDrive.text = newVal.text;
originalDrive.price = newVal.price;
} else {
// This is a new drive, charge full price
configState[targetSlot.label] = newVal.price;
}
} else {
// Custom server or no tracking, charge full price
configState[targetSlot.label] = newVal.price;
}
configIds[targetSlot.id] = newVal.id;
}
}
} else {
// Decrement: Find last slot that matches this value
for (let i = pool.slots.length - 1; i >= 0; i--) {
if (pool.slots[i].currentVal.text === valText) {
// Reset to default
const defaultVal = pool.slots[i].values[0];
pool.slots[i].currentVal = defaultVal;
// For instant customization, refund the price if it was an upgrade
if (isInstant && window.originalDrives) {
const originalDrive = window.originalDrives.find(d => d.slotId === pool.slots[i].id);
if (originalDrive && pool.slots[i].currentVal.id !== originalDrive.driveId) {
// We're removing an upgrade, refund the difference
const currentVal = pool.slots[i].values.find(v => v.id === configIds[pool.slots[i].id]);
if (currentVal) {
configState[pool.slots[i].label] = -currentVal.price;
}
} else {
configState[pool.slots[i].label] = defaultVal.price;
}
} else {
configState[pool.slots[i].label] = defaultVal.price;
}
configIds[pool.slots[i].id] = defaultVal.id;
break;
}
}
}
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 to update storage summary with enhanced styling
function updateStorageSummary() {
// Collect all selected drives
const storageCount = {};
let storageHtml = '';
Object.values(storageSelection).forEach(drive => {
if (drive.name && !drive.name.toLowerCase().includes('none')) {
storageCount[drive.name] = (storageCount[drive.name] || 0) + 1;
}
});
if (Object.keys(storageCount).length === 0) {
document.getElementById('summaryStorage').innerHTML = '<span style="color: var(--text-secondary); font-style: italic;">No drives selected</span>';
} else {
// Create styled drive pills
Object.entries(storageCount).forEach(([name, count], index) => {
// Determine drive type icon
let icon = '💿';
if (name.toLowerCase().includes('nvme')) {
icon = '⚡';
if (name.toLowerCase().includes('gen5')) icon = '🚀';
if (name.toLowerCase().includes('t705')) icon = '🔥';
}
if (name.toLowerCase().includes('crucial')) icon = '🔴';
if (name.toLowerCase().includes('kioxia')) icon = '🔵';
if (name.toLowerCase().includes('samsung')) icon = '🔶';
// Extract clean name
const cleanName = name.replace(/\s?[€$£].*/, '').trim();
storageHtml += `
<div class="storage-pill">
<div class="storage-icon">${icon}</div>
<div class="storage-info">
<span class="storage-count">${count}x</span>
<span class="storage-name">${cleanName}</span>
</div>
</div>
`;
});
document.getElementById('summaryStorage').innerHTML = storageHtml;
}
// Update Tech Specs with simple text version
const specStorage = document.getElementById('spec-storage-config-hero');
if (specStorage) {
const storageText = Object.entries(storageCount)
.map(([name, count]) => `${count}x ${name}`)
.join(' + ') || 'None selected';
specStorage.textContent = storageText;
}
}
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');
// For instant customization storage, preselected drives should have 0 cost
if (isInstant && opt.type === 'storage' && isPreselected) {
configState[opt.label] = 0; // Included in base price
} else {
// 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') {
const displayName = val.text.replace(/\s?\(.*?\)/, ''); // Clean text
// Store selection for this "bay" (opt.id)
storageSelection[opt.id] = { name: displayName, price: val.price };
// For instant customization with individual slots, update the slot visual
if (isInstant && storageIndex >= 0) {
updateDynamicStorageVisual(storageIndex, displayName, isInstant);
}
// Always update the storage summary
updateStorageSummary();
}
}
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');
// For instant customization storage, handle price differences
if (isInstant && opt.type === 'storage') {
// Find the original drive for this slot
const originalDrive = window.originalDrives ?
window.originalDrives.find(d => d.slotId === opt.id) : null;
if (originalDrive && originalDrive.driveId !== val.id) {
// User is changing from original drive, charge the difference
configState[opt.label] = val.price - originalDrive.price;
} else if (originalDrive && originalDrive.driveId === val.id) {
// User is reverting to original drive, no charge
configState[opt.label] = 0;
} else {
// No original drive found, charge full price
configState[opt.label] = val.price;
}
// Update original drives tracking
if (window.originalDrives) {
const drive = window.originalDrives.find(d => d.slotId === opt.id);
if (drive) {
drive.driveId = val.id;
drive.text = val.text;
drive.price = val.price;
}
}
} else {
// Non-instant or non-storage, use full price
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') {
// For instant customization, build storage summary differently
if (isInstant) {
// Collect all selected drives from individual slots
const allStorageOptions = document.querySelectorAll('.config-card.active');
const storageCount = {};
let storageSummary = [];
allStorageOptions.forEach(card => {
const name = card.querySelector('.option-name').textContent;
const price = card.querySelector('.option-price').textContent;
// Count each drive type
if (!name.toLowerCase().includes('none')) {
storageCount[name] = (storageCount[name] || 0) + 1;
}
});
// Build summary text with counts
Object.entries(storageCount).forEach(([name, count]) => {
storageSummary.push(`${count}x ${name}`);
});
const storageText = storageSummary.length > 0 ? storageSummary.join(' + ') : 'None';
document.getElementById('summaryStorage').textContent = storageText;
// Update Tech Specs
const specStorage = document.getElementById('spec-storage-config-hero');
if(specStorage) specStorage.innerText = storageText;
} else {
// Custom server storage handling (original logic)
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');
// Reset customization state when switching
isInstantCustomized = false;
document.getElementById('instantOptions').style.display = 'none';
document.getElementById('instantServersGrid').style.display = 'grid';
document.getElementById('customOptions').style.display = 'none';
// Clear containers
const instantContainer = document.getElementById('instantDynamicConfigContainer');
if (instantContainer) instantContainer.innerHTML = '';
// Reset config state
configState = {};
configIds = {};
storageSelection = {};
// If switching back to instant tab, re-render instant servers
if (currentTab === 'instant') {
renderInstantServers();
// Auto-select first instant server if none selected
if (!selectedServer && instantServers.length > 0) {
selectInstantServer(instantServers[0].id);
}
} else {
selectedServer = null;
}
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>