feat: rebuild bare metal experience
This commit is contained in:
645
new-baremetal.html
Normal file
645
new-baremetal.html
Normal file
@@ -0,0 +1,645 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>DedicatedNodes | Bare Metal Server Details</title>
|
||||
<meta name="description"
|
||||
content="Instant inventory, custom builds, and Jito latency data for high-performance DedicatedNodes bare metal servers.">
|
||||
<link rel="icon" href="https://www.dedicatednodes.io/wp-content/uploads/2024/04/favicon.png">
|
||||
<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@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--brand-primary: #2892da;
|
||||
--brand-dark: #0a1628;
|
||||
--brand-cyan: #00d4ff;
|
||||
--text-primary: #0f172a;
|
||||
--text-muted: #5e6c84;
|
||||
--bg-surface: #f8fafc;
|
||||
--border: rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
color: var(--text-primary);
|
||||
background: #fff;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
header {
|
||||
background: var(--brand-dark);
|
||||
color: #fff;
|
||||
padding: 1rem 1.5rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 40;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header .logo {
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
header nav a {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-left: 1rem;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1.25rem 4rem;
|
||||
}
|
||||
|
||||
.hero {
|
||||
border-radius: 1.5rem;
|
||||
background: linear-gradient(135deg, var(--brand-dark), #031326);
|
||||
color: #fff;
|
||||
padding: 3rem 2rem;
|
||||
box-shadow: 0 25px 60px rgba(3, 16, 32, 0.45);
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 2.8rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
max-width: 650px;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.hero .actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: none;
|
||||
padding: 0.85rem 1.5rem;
|
||||
border-radius: 0.75rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--brand-cyan);
|
||||
color: #0a1628;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.section-heading h2 {
|
||||
font-size: 2.1rem;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.section-heading p {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.instant-section {
|
||||
background: var(--bg-surface);
|
||||
border-radius: 1.5rem;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 20px 40px rgba(15, 23, 42, 0.1);
|
||||
}
|
||||
|
||||
.instant-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.instant-table thead tr {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.instant-table th,
|
||||
.instant-table td {
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.instant-table tbody tr {
|
||||
border-bottom: 1px solid rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
.instant-table tbody tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.deploy-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--brand-primary);
|
||||
color: #fff;
|
||||
border-radius: 0.65rem;
|
||||
padding: 0.45rem 0.85rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.configurator {
|
||||
background: #fff;
|
||||
border-radius: 1.5rem;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 20px 45px rgba(3, 16, 32, 0.1);
|
||||
}
|
||||
|
||||
.config-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, 1fr) minmax(320px, 1fr);
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.offer-card {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 1rem;
|
||||
padding: 1rem 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
cursor: pointer;
|
||||
transition: border 0.2s ease, transform 0.2s ease;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.offer-card.active {
|
||||
border-color: var(--brand-primary);
|
||||
box-shadow: 0 10px 30px rgba(40, 146, 218, 0.15);
|
||||
}
|
||||
|
||||
.option-panel {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 1rem;
|
||||
padding: 1.25rem;
|
||||
background: var(--bg-surface);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.option-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.option-row label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.option-row select {
|
||||
padding: 0.6rem 0.75rem;
|
||||
border-radius: 0.65rem;
|
||||
border: 1px solid rgba(15, 23, 42, 0.3);
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.summary-panel {
|
||||
border-radius: 0.9rem;
|
||||
border: 1px solid var(--border);
|
||||
padding: 1rem;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.summary-panel div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.summary-panel strong {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.map-section {
|
||||
background: var(--brand-dark);
|
||||
border-radius: 1.5rem;
|
||||
padding: 2rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.map-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 1fr) 320px;
|
||||
gap: 1.5rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.map-graphic {
|
||||
position: relative;
|
||||
background: radial-gradient(circle at 50% 30%, rgba(255, 255, 255, 0.25), transparent 60%), #0f1e39;
|
||||
border-radius: 1rem;
|
||||
min-height: 280px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.map-graphic .marker {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--brand-cyan);
|
||||
box-shadow: 0 0 12px rgba(0, 212, 255, 0.75);
|
||||
}
|
||||
|
||||
.map-graphic .marker::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -8px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(0, 212, 255, 0.4);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.map-graphic .marker:nth-child(1) {
|
||||
top: 28%;
|
||||
left: 55%;
|
||||
}
|
||||
|
||||
.map-graphic .marker:nth-child(2) {
|
||||
top: 35%;
|
||||
left: 48%;
|
||||
}
|
||||
|
||||
.map-graphic .marker:nth-child(3) {
|
||||
top: 39%;
|
||||
left: 56%;
|
||||
}
|
||||
|
||||
.map-graphic .marker:nth-child(4) {
|
||||
top: 34%;
|
||||
left: 44%;
|
||||
}
|
||||
|
||||
.map-graphic .marker:nth-child(5) {
|
||||
top: 36%;
|
||||
left: 19%;
|
||||
}
|
||||
|
||||
.latency-panel {
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 0.9rem;
|
||||
padding: 1.25rem;
|
||||
background: rgba(3, 16, 32, 0.4);
|
||||
}
|
||||
|
||||
.latency-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.6rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.latency-value {
|
||||
color: var(--brand-cyan);
|
||||
}
|
||||
|
||||
.real-time {
|
||||
font-size: 0.85rem;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
padding: 1.5rem 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.config-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.map-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
header nav {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<div class="logo">DedicatedNodes</div>
|
||||
<nav>
|
||||
<a href="#instant-deals">Instant inventory</a>
|
||||
<a href="#custom-config">Custom builds</a>
|
||||
<a href="#map-section">Jito latency</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="hero">
|
||||
<h1>Dedicated bare metal servers for Solana</h1>
|
||||
<p>Pricing, configurable addons, and latency data mirror the live DedicatedNodes experience while keeping everything on this offline-friendly page.</p>
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" onclick="document.getElementById('custom-config').scrollIntoView({behavior:'smooth'})">Configure now</button>
|
||||
<button class="btn btn-outline" onclick="document.getElementById('instant-deals').scrollIntoView({behavior:'smooth'})">Browse instant servers</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="instant-deals" class="instant-section">
|
||||
<div class="section-heading">
|
||||
<h2>Instant inventory</h2>
|
||||
<p>Available from stock with immediate deployment. Data scraped live from DedicatedNodes.solana-nodes.</p>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table class="instant-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>CPU</th>
|
||||
<th>Location</th>
|
||||
<th>Cores</th>
|
||||
<th>Memory</th>
|
||||
<th>Storage</th>
|
||||
<th>Network</th>
|
||||
<th>Bandwidth</th>
|
||||
<th>Price</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="instantDealsBody">
|
||||
<tr>
|
||||
<td colspan="9" style="text-align:center; padding:2rem 0;">Loading inventory...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="custom-config" class="configurator">
|
||||
<div class="section-heading">
|
||||
<h2>Customizable builds</h2>
|
||||
<p>Pick a portal “Configure now” base server and interact with the exact add-on dropdowns and prices.</p>
|
||||
</div>
|
||||
<div class="config-grid">
|
||||
<div id="customOffersList">
|
||||
<p style="color: var(--text-muted);">Loading custom offers...</p>
|
||||
</div>
|
||||
<div class="option-panel">
|
||||
<h3>Customization options</h3>
|
||||
<div class="options-wrapper" id="customOptionsContainer">
|
||||
<p style="color: var(--text-muted);">Select a base server to reveal the dropdowns.</p>
|
||||
</div>
|
||||
<div class="summary-panel">
|
||||
<div><span>Base price</span><strong id="customBasePrice">—</strong></div>
|
||||
<div><span>Add-ons</span><strong id="customAddonPrice">—</strong></div>
|
||||
<div><span>Total</span><strong id="customTotalPrice">—</strong></div>
|
||||
</div>
|
||||
<div class="custom-addons-summary" id="custom-addons-summary">Add-ons: base configuration</div>
|
||||
<button class="btn btn-primary" id="customPreviewBtn" disabled>Open configure page</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="map-section" class="map-section">
|
||||
<div class="section-heading">
|
||||
<h2>Jito latency map</h2>
|
||||
<p>Live simulated latency to Jito endpoints, refreshed every few seconds for realism.</p>
|
||||
</div>
|
||||
<div class="map-grid">
|
||||
<div class="map-graphic" role="presentation">
|
||||
<div class="marker"></div>
|
||||
<div class="marker"></div>
|
||||
<div class="marker"></div>
|
||||
<div class="marker"></div>
|
||||
<div class="marker"></div>
|
||||
</div>
|
||||
<div class="latency-panel">
|
||||
<h3>Live latency</h3>
|
||||
<div class="latency-row"><span>Amsterdam → Jito</span><span class="latency-value" id="latency-ams">0.10ms</span></div>
|
||||
<div class="latency-row"><span>Rotterdam → Jito</span><span class="latency-value" id="latency-rtm">1.60ms</span></div>
|
||||
<div class="latency-row"><span>Frankfurt → Jito</span><span class="latency-value" id="latency-fra">0.60ms</span></div>
|
||||
<div class="latency-row"><span>London → Jito</span><span class="latency-value" id="latency-lon">0.20ms</span></div>
|
||||
<div class="latency-row"><span>New York → Jito</span><span class="latency-value" id="latency-nyc">0.90ms</span></div>
|
||||
<div class="real-time" id="latency-updated">Updated just now</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>© DedicatedNodes. All rights reserved.</p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const instantDealsBody = document.getElementById('instantDealsBody');
|
||||
const customOffersList = document.getElementById('customOffersList');
|
||||
const customOptionsContainer = document.getElementById('customOptionsContainer');
|
||||
const basePriceEl = document.getElementById('customBasePrice');
|
||||
const addonPriceEl = document.getElementById('customAddonPrice');
|
||||
const totalPriceEl = document.getElementById('customTotalPrice');
|
||||
const addonsSummaryEl = document.getElementById('custom-addons-summary');
|
||||
const previewBtn = document.getElementById('customPreviewBtn');
|
||||
const latencyUpdated = document.getElementById('latency-updated');
|
||||
let offers = [];
|
||||
let activeOffer = null;
|
||||
|
||||
const formatCurrency = (value, symbol = '€') => {
|
||||
if (!Number.isFinite(value)) {
|
||||
return '—';
|
||||
}
|
||||
return `${symbol}${value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 })}`;
|
||||
};
|
||||
|
||||
const renderInstantDeals = (deals) => {
|
||||
if (!deals.length) {
|
||||
instantDealsBody.innerHTML = '<tr><td colspan="9" style="text-align:center; padding:2rem 0;">No inventory available.</td></tr>';
|
||||
return;
|
||||
}
|
||||
instantDealsBody.innerHTML = deals.map((deal) => `
|
||||
<tr>
|
||||
<td>${deal.cpu}</td>
|
||||
<td>${deal.location}</td>
|
||||
<td>${deal.cores}</td>
|
||||
<td>${deal.memory}</td>
|
||||
<td>${deal.storage}</td>
|
||||
<td>${deal.network}</td>
|
||||
<td>${deal.bandwidth}</td>
|
||||
<td class="price">${formatCurrency(deal.price, deal.currencySymbol)}</td>
|
||||
<td><a class="deploy-btn" href="${deal.orderUrl}" target="_blank" rel="noreferrer">Deploy now</a></td>
|
||||
</tr>
|
||||
`).join('');
|
||||
};
|
||||
|
||||
const loadInstantDeals = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/instant-offers', { cache: 'no-cache' });
|
||||
const payload = await res.json();
|
||||
renderInstantDeals(payload.offers || []);
|
||||
} catch (error) {
|
||||
console.error('Instant offers fetch failed', error);
|
||||
instantDealsBody.innerHTML = '<tr><td colspan="9" style="text-align:center;">Unable to load instant inventory.</td></tr>';
|
||||
}
|
||||
};
|
||||
|
||||
const renderCustomOffers = () => {
|
||||
if (!offers.length) {
|
||||
customOffersList.innerHTML = '<p style="color: var(--text-muted);">No custom offers available right now.</p>';
|
||||
return;
|
||||
}
|
||||
customOffersList.innerHTML = offers.map((offer) => `
|
||||
<div class="offer-card" data-offer-id="${offer.id}">
|
||||
<h3>${offer.title}</h3>
|
||||
<div class="offer-price">${formatCurrency(offer.basePrice, offer.currencySymbol)}</div>
|
||||
<div class="offer-meta">${offer.stock || offer.setupTime || 'Custom order'}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
customOffersList.querySelectorAll('.offer-card').forEach((card) => {
|
||||
card.addEventListener('click', () => {
|
||||
const id = card.dataset.offerId;
|
||||
const offer = offers.find((o) => o.id === id);
|
||||
if (!offer) return;
|
||||
activeOffer = offer;
|
||||
customOffersList.querySelectorAll('.offer-card').forEach((c) => c.classList.remove('active'));
|
||||
card.classList.add('active');
|
||||
renderCustomOptions(offer);
|
||||
});
|
||||
});
|
||||
const firstCard = customOffersList.querySelector('.offer-card');
|
||||
if (firstCard) {
|
||||
firstCard.click();
|
||||
}
|
||||
};
|
||||
|
||||
const renderCustomOptions = (offer) => {
|
||||
if (!offer.options?.length) {
|
||||
customOptionsContainer.innerHTML = '<p style="color: var(--text-muted);">Dropdown data unavailable.</p>';
|
||||
return;
|
||||
}
|
||||
const optionsHtml = offer.options.map((option, idx) => {
|
||||
const selectId = `option-${offer.id}-${idx}`;
|
||||
const selects = option.choices.map((choice) => {
|
||||
const label = choice.label || choice.rawLabel || 'Option';
|
||||
const delta = Number(choice.priceDelta) || 0;
|
||||
const symbol = choice.priceSymbol || offer.currencySymbol || '€';
|
||||
const deltaLabel = delta ? ` (${delta > 0 ? '+' : ''}${formatCurrency(delta, symbol)})` : '';
|
||||
return `<option value="${delta}" data-option-label="${label}">${label}${deltaLabel}</option>`;
|
||||
}).join('');
|
||||
return `
|
||||
<div class="option-row">
|
||||
<label for="${selectId}">${option.label}</label>
|
||||
<select id="${selectId}" data-option-name="${option.name}" data-option-label="${option.label}">
|
||||
${selects}
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
customOptionsContainer.innerHTML = `<div class="options-wrapper">${optionsHtml}</div>`;
|
||||
customOptionsContainer.querySelectorAll('select').forEach((select) => {
|
||||
select.addEventListener('change', () => updateCustomTotals(offer));
|
||||
});
|
||||
updateCustomTotals(offer);
|
||||
};
|
||||
|
||||
const updateCustomTotals = (offer) => {
|
||||
if (!offer) return;
|
||||
const selects = customOptionsContainer.querySelectorAll('select');
|
||||
let addonTotal = 0;
|
||||
const detailLines = [];
|
||||
selects.forEach((select) => {
|
||||
const value = Number(select.value) || 0;
|
||||
addonTotal += value;
|
||||
const label = select.dataset.optionLabel || select.getAttribute('data-option-name');
|
||||
if (value) {
|
||||
const symbol = offer.currencySymbol || '€';
|
||||
detailLines.push(`${label} (${value > 0 ? '+' : ''}${formatCurrency(value, symbol)})`);
|
||||
}
|
||||
});
|
||||
const base = Number(offer.basePrice) || 0;
|
||||
const total = base + addonTotal;
|
||||
const currency = offer.currencySymbol || '€';
|
||||
basePriceEl.textContent = formatCurrency(base, currency);
|
||||
addonPriceEl.textContent = formatCurrency(addonTotal, currency);
|
||||
totalPriceEl.textContent = formatCurrency(total, currency);
|
||||
addonsSummaryEl.textContent = detailLines.length ? `Add-ons: ${detailLines.join(', ')}` : 'Add-ons: base configuration';
|
||||
previewBtn.disabled = false;
|
||||
previewBtn.onclick = () => {
|
||||
if (offer.configUrl) {
|
||||
window.open(offer.configUrl, '_blank');
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const loadCustomOffers = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/custom-offers', { cache: 'no-cache' });
|
||||
const payload = await res.json();
|
||||
offers = (payload.offers || []).map((offer, idx) => ({
|
||||
...offer,
|
||||
id: `custom-${idx}`,
|
||||
basePrice: offer.basePrice || offer.price || 0,
|
||||
currencySymbol: offer.currencySymbol || '€',
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Custom offers load failed', error);
|
||||
offers = [];
|
||||
}
|
||||
renderCustomOffers();
|
||||
};
|
||||
|
||||
const updateLatencies = () => {
|
||||
const list = [
|
||||
{ id: 'latency-ams', base: 0.1 },
|
||||
{ id: 'latency-rtm', base: 1.6 },
|
||||
{ id: 'latency-fra', base: 0.6 },
|
||||
{ id: 'latency-lon', base: 0.2 },
|
||||
{ id: 'latency-nyc', base: 0.9 },
|
||||
];
|
||||
list.forEach((item) => {
|
||||
const el = document.getElementById(item.id);
|
||||
if (el) {
|
||||
const jitter = (item.base + (Math.random() - 0.5) * (item.base * 0.3 + 0.05)).toFixed(2);
|
||||
el.textContent = `${jitter}ms`;
|
||||
}
|
||||
});
|
||||
if (latencyUpdated) {
|
||||
latencyUpdated.textContent = `Updated just now • ${new Date().toLocaleTimeString()}`;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadInstantDeals();
|
||||
loadCustomOffers();
|
||||
updateLatencies();
|
||||
setInterval(updateLatencies, 4200);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
342
serve10000.py
Normal file
342
serve10000.py
Normal file
@@ -0,0 +1,342 @@
|
||||
from flask import Flask, jsonify, send_from_directory
|
||||
from pathlib import Path
|
||||
import time
|
||||
import json
|
||||
from urllib.parse import urljoin
|
||||
import re
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError: # pragma: no cover
|
||||
requests = None
|
||||
|
||||
try:
|
||||
from bs4 import BeautifulSoup
|
||||
except ImportError: # pragma: no cover
|
||||
BeautifulSoup = None
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
CUSTOM_OFFERS_PATH = BASE_DIR / "custom_offers.json"
|
||||
REMOTE_URL = "https://www.dedicatednodes.io/solana-nodes/"
|
||||
CACHE_TTL = 5 * 60
|
||||
OPTION_CACHE_TTL = 5 * 60
|
||||
HEADERS = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||||
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
}
|
||||
|
||||
_cache = {"ts": 0, "data": None}
|
||||
_remote_cache = {"ts": 0, "soup": None}
|
||||
_instant_cache = {"ts": 0, "data": None}
|
||||
_option_cache = {}
|
||||
PRICE_PATTERN = re.compile(r"([+-])?\s*([€$£]|[A-Za-z]{1,3})?\s*([0-9.,]+)")
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
def normalize_text(value: str) -> str:
|
||||
if not value:
|
||||
return ""
|
||||
return " ".join(value.replace("\xa0", " ").split())
|
||||
|
||||
|
||||
def text_from_element(el):
|
||||
if not el:
|
||||
return ""
|
||||
return normalize_text(el.get_text(separator=" ", strip=True))
|
||||
|
||||
|
||||
def find_card_root(node):
|
||||
current = node
|
||||
depth = 0
|
||||
while current and depth < 6:
|
||||
current = current.parent
|
||||
depth += 1
|
||||
if not current:
|
||||
break
|
||||
classes = current.get("class") or []
|
||||
if isinstance(classes, str):
|
||||
classes = classes.split()
|
||||
if "card" in classes:
|
||||
return current
|
||||
return node.parent
|
||||
|
||||
|
||||
def collect_features(card):
|
||||
features = []
|
||||
for li in card.select("li"):
|
||||
component = li.select_one(".component")
|
||||
if component:
|
||||
label = normalize_text(component.get_text())
|
||||
value = text_from_element(li.select_one(".component-value"))
|
||||
description = text_from_element(li.select_one(".component-description"))
|
||||
parts = [part for part in (value, description) if part]
|
||||
if label and parts:
|
||||
features.append("|".join([label] + parts))
|
||||
continue
|
||||
price_band = li.select_one(".price-band")
|
||||
if price_band:
|
||||
label_text = normalize_text(price_band.select_one(".label").get_text()) if price_band.select_one(".label") else "Starting from"
|
||||
amount_text = normalize_text(price_band.select_one(".amount").get_text()) if price_band.select_one(".amount") else ""
|
||||
per_text = normalize_text(price_band.select_one(".per").get_text()) if price_band.select_one(".per") else ""
|
||||
parts = [part for part in (amount_text, per_text) if part]
|
||||
if label_text and parts:
|
||||
features.append("|".join([label_text] + parts))
|
||||
return features
|
||||
|
||||
|
||||
def parse_custom_offers(soup):
|
||||
if not BeautifulSoup or not soup:
|
||||
return []
|
||||
offers = []
|
||||
seen_links = set()
|
||||
for anchor in soup.find_all("a", string=lambda text: text and "configure now" in text.lower()):
|
||||
href = anchor.get("href")
|
||||
if not href or href in seen_links:
|
||||
continue
|
||||
seen_links.add(href)
|
||||
card = find_card_root(anchor)
|
||||
if not card:
|
||||
continue
|
||||
title_el = card.find(class_="card-title") or card.find(["h2", "h3", "h4"])
|
||||
title = normalize_text(title_el.get_text()) if title_el else "Custom Server"
|
||||
features = collect_features(card)
|
||||
if not features:
|
||||
continue
|
||||
link = urljoin(REMOTE_URL, href)
|
||||
offer = {
|
||||
"title": title,
|
||||
"link": link,
|
||||
"features": features,
|
||||
}
|
||||
options = parse_product_options(link)
|
||||
if options:
|
||||
offer["options"] = options
|
||||
offers.append(offer)
|
||||
return offers
|
||||
|
||||
|
||||
def fetch_remote_html():
|
||||
if not requests:
|
||||
return None
|
||||
try:
|
||||
response = requests.get(REMOTE_URL, headers=HEADERS, timeout=10)
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
except Exception as exc: # pragma: no cover
|
||||
print(f"[serve10000] Unable to reach {REMOTE_URL}: {exc}")
|
||||
return None
|
||||
|
||||
|
||||
def load_fallback_offers():
|
||||
if not CUSTOM_OFFERS_PATH.exists():
|
||||
return []
|
||||
try:
|
||||
data = json.loads(CUSTOM_OFFERS_PATH.read_text(encoding="utf-8"))
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
except json.JSONDecodeError:
|
||||
print("[serve10000] Invalid fallback JSON, returning empty list")
|
||||
return []
|
||||
|
||||
|
||||
def fetch_remote_soup():
|
||||
if not BeautifulSoup:
|
||||
return None
|
||||
now = time.time()
|
||||
cached = _remote_cache.get("soup")
|
||||
if cached and now - _remote_cache["ts"] < CACHE_TTL:
|
||||
return cached
|
||||
html = fetch_remote_html()
|
||||
if not html:
|
||||
return None
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
_remote_cache["soup"] = soup
|
||||
_remote_cache["ts"] = now
|
||||
return soup
|
||||
|
||||
|
||||
def parse_instant_offers(soup):
|
||||
results = []
|
||||
if not soup:
|
||||
return results
|
||||
table = soup.select_one("#instant-servers-table tbody")
|
||||
if not table:
|
||||
return results
|
||||
for row in table.select("tr"):
|
||||
cells = row.select("td")
|
||||
if len(cells) < 8:
|
||||
continue
|
||||
cpu = normalize_text(cells[0].get_text())
|
||||
location = normalize_text(cells[1].get_text())
|
||||
cores = normalize_text(cells[2].get_text())
|
||||
memory = normalize_text(cells[3].get_text())
|
||||
storage = normalize_text(cells[4].get_text())
|
||||
network = normalize_text(cells[5].get_text())
|
||||
bandwidth = normalize_text(cells[6].get_text())
|
||||
price_text = normalize_text(cells[7].get_text())
|
||||
price_delta, currency_symbol = parse_price_delta(price_text)
|
||||
order_link = ""
|
||||
action = row.select_one(".action-cell a")
|
||||
if action:
|
||||
order_link = urljoin(REMOTE_URL, action.get("href", ""))
|
||||
results.append(
|
||||
{
|
||||
"cpu": cpu,
|
||||
"location": location,
|
||||
"cores": cores,
|
||||
"memory": memory,
|
||||
"storage": storage,
|
||||
"network": network,
|
||||
"bandwidth": bandwidth,
|
||||
"priceText": price_text,
|
||||
"price": price_delta,
|
||||
"currencySymbol": currency_symbol or "€",
|
||||
"orderUrl": order_link,
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def get_instant_offers():
|
||||
now = time.time()
|
||||
if _instant_cache["data"] and now - _instant_cache["ts"] < CACHE_TTL:
|
||||
return _instant_cache["data"]
|
||||
soup = fetch_remote_soup()
|
||||
offers = parse_instant_offers(soup)
|
||||
_instant_cache["data"] = offers
|
||||
_instant_cache["ts"] = now
|
||||
return offers
|
||||
|
||||
|
||||
def format_price_delta(delta: float, symbol: str) -> str:
|
||||
if not delta:
|
||||
return ""
|
||||
sign = "+" if delta > 0 else "-"
|
||||
abs_value = abs(delta)
|
||||
formatted = f"{abs_value:,.2f}".replace(",", "_").replace(".", ",").replace("_", ".")
|
||||
return f"{sign} {symbol}{formatted}".strip()
|
||||
|
||||
|
||||
def strip_price_from_label(text: str) -> str:
|
||||
cleaned = re.sub(
|
||||
r"\s*[+-]?\s*([€$£]|[A-Za-z]{1,3}|ƒ,ª)?\s*[0-9.,]+\s*(EUR|USD|GBP)?",
|
||||
"",
|
||||
text,
|
||||
flags=re.IGNORECASE,
|
||||
).strip()
|
||||
return cleaned or text
|
||||
|
||||
|
||||
def parse_price_delta(text: str) -> tuple[float, str]:
|
||||
match = PRICE_PATTERN.search(text)
|
||||
if not match:
|
||||
return 0.0, ""
|
||||
sign = match.group(1)
|
||||
symbol = match.group(2) or ""
|
||||
raw_amount = match.group(3).replace(" ", "")
|
||||
if raw_amount.count(",") and raw_amount.count("."):
|
||||
normalized = raw_amount.replace(".", "").replace(",", ".")
|
||||
elif "," in raw_amount:
|
||||
normalized = raw_amount.replace(",", ".")
|
||||
else:
|
||||
normalized = raw_amount
|
||||
try:
|
||||
amount = float(normalized)
|
||||
except ValueError:
|
||||
amount = 0.0
|
||||
if sign == "-":
|
||||
amount = -amount
|
||||
return amount, symbol
|
||||
|
||||
|
||||
def parse_product_options(url: str):
|
||||
now = time.time()
|
||||
cached = _option_cache.get(url)
|
||||
if cached and now - cached["ts"] < OPTION_CACHE_TTL:
|
||||
return cached["data"]
|
||||
if not requests or not BeautifulSoup:
|
||||
return cached["data"] if cached else []
|
||||
try:
|
||||
response = requests.get(url, headers=HEADERS, timeout=10)
|
||||
response.raise_for_status()
|
||||
except Exception as exc:
|
||||
print(f"[serve10000] Failed to fetch options from {url}: {exc}")
|
||||
return cached["data"] if cached else []
|
||||
soup = BeautifulSoup(response.text, "html.parser")
|
||||
selects = []
|
||||
for select in soup.select('select[name^="configoption"]'):
|
||||
label_text = ""
|
||||
select_id = select.get("id")
|
||||
if select_id:
|
||||
label_el = soup.find("label", attrs={"for": select_id})
|
||||
if label_el:
|
||||
label_text = normalize_text(label_el.get_text())
|
||||
if not label_text:
|
||||
label_text = normalize_text(
|
||||
select.get("data-option-name")
|
||||
or select.get("data-custom-option-name")
|
||||
or select.get("name")
|
||||
)
|
||||
choices = []
|
||||
for opt in select.select("option"):
|
||||
option_text = normalize_text(opt.get_text())
|
||||
delta, currency = parse_price_delta(option_text)
|
||||
choices.append(
|
||||
{
|
||||
"value": opt.get("value"),
|
||||
"label": strip_price_from_label(option_text),
|
||||
"rawLabel": option_text,
|
||||
"priceDelta": delta,
|
||||
"priceSymbol": currency,
|
||||
"priceDisplay": format_price_delta(delta, currency or ""),
|
||||
}
|
||||
)
|
||||
selects.append(
|
||||
{
|
||||
"name": select.get("name"),
|
||||
"label": label_text,
|
||||
"choices": choices,
|
||||
}
|
||||
)
|
||||
_option_cache[url] = {"ts": now, "data": selects}
|
||||
return selects
|
||||
|
||||
|
||||
def get_custom_offers():
|
||||
now = time.time()
|
||||
if _cache["data"] and now - _cache["ts"] < CACHE_TTL:
|
||||
return _cache["data"]
|
||||
soup = fetch_remote_soup()
|
||||
offers = parse_custom_offers(soup) if soup else []
|
||||
if not offers:
|
||||
offers = load_fallback_offers()
|
||||
_cache["data"] = offers
|
||||
_cache["ts"] = now
|
||||
return offers
|
||||
|
||||
|
||||
@app.route("/")
|
||||
@app.route("/new-baremetal.html")
|
||||
def serve_baremetal():
|
||||
return send_from_directory(BASE_DIR, "new-baremetal.html")
|
||||
|
||||
|
||||
@app.route("/custom_offers.json")
|
||||
def custom_offers_file():
|
||||
return send_from_directory(BASE_DIR, "custom_offers.json")
|
||||
|
||||
|
||||
@app.route("/api/custom-offers")
|
||||
def custom_offers_api():
|
||||
return jsonify({"offers": get_custom_offers()})
|
||||
|
||||
|
||||
@app.route("/api/instant-offers")
|
||||
def instant_offers_api():
|
||||
return jsonify({"offers": get_instant_offers()})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Starting DedicatedNodes mock site at http://127.0.0.1:10000/new-baremetal.html")
|
||||
app.run(host="127.0.0.1", port=10000)
|
||||
Reference in New Issue
Block a user