Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "zerohost-dashboard",
"version": "1.1.0",
"version": "1.1.1",
"private": true,
"type": "module",
"scripts": {
Expand Down
161 changes: 158 additions & 3 deletions public/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -1069,9 +1069,6 @@ cap-widget {
margin-bottom: 1px;
position: relative;
z-index: 2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.nav-sub-item:hover {
Expand All @@ -1083,11 +1080,46 @@ cap-widget {
color: var(--accent-1);
}

.nav-sub-server-info {
display: flex;
flex-direction: column;
min-width: 0;
}

.nav-sub-server-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: flex;
align-items: center;
gap: 6px;
}

.nav-sub-egg-name {
font-size: 0.7rem;
color: var(--text-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.nav-sub-nest-icon {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 6px;
background: var(--bg-tertiary);
}

.nav-sub-item .nav-sub-dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
display: inline-block;
}

.nav-sub-dot.dot-active {
Expand Down Expand Up @@ -4607,3 +4639,126 @@ tbody tr:hover {
border-radius: 4px;
}

/* ===== VERIFICATION OVERLAY ===== */
.verification-overlay {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(8px);
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}

.verification-overlay.open {
opacity: 1;
pointer-events: all;
}

.verification-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 48px 40px;
max-width: 420px;
width: 90%;
text-align: center;
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5);
}

.verification-spinner {
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.1);
border-top-color: var(--accent-1);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 0 auto 24px;
}

.verification-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 6px;
color: var(--text-primary);
}

.verification-subtitle {
font-size: 0.875rem;
color: var(--text-secondary);
margin-bottom: 28px;
}

.verification-steps {
display: flex;
flex-direction: column;
gap: 14px;
text-align: left;
}

.verification-step {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
border-radius: var(--radius-sm);
background: var(--bg-tertiary);
font-size: 0.85rem;
color: var(--text-muted);
transition: all var(--transition);
}

.verification-step.active {
color: var(--text-primary);
background: rgba(238, 129, 50, 0.08);
border: 1px solid rgba(238, 129, 50, 0.2);
}

.verification-step.done {
color: var(--accent-green);
}

.verification-step.failed {
color: var(--accent-red);
}

.verification-step-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}

.vstep-spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid rgba(238, 129, 50, 0.25);
border-top-color: var(--accent-1);
border-radius: 50%;
animation: spin 0.7s linear infinite;
}

.vstep-pending {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid var(--text-muted);
border-radius: 50%;
opacity: 0.4;
}

.vstep-check, .vstep-cross {
flex-shrink: 0;
}

.verification-success-icon, .verification-error-icon {
margin-bottom: 16px;
}

144 changes: 138 additions & 6 deletions public/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1511,7 +1511,7 @@ async function renderDashboard() {
<div style="padding:8px 12px 0;display:flex;gap:16px;justify-content:center;flex-wrap:wrap">

</div>
<div style="padding:4px 0 8px;text-align:center;font-size:0.7rem;color:var(--text-muted);letter-spacing:0.05em">v1.1.0</div>
<div style="padding:4px 0 8px;text-align:center;font-size:0.7rem;color:var(--text-muted);letter-spacing:0.05em">v1.1.1</div>
</div>
<div class="sidebar-resizer" id="sidebar-resizer"></div>
</aside>
Expand Down Expand Up @@ -1672,6 +1672,13 @@ function initSidebarTooltip() {
});
}

function renderNestLogoSmall(logo) {
if (!logo) return html`<i data-lucide="box" style="width:16px;height:16px;color:var(--text-secondary)"></i>`;
if (logo.startsWith('si:')) return html`<img src="${siUrl(logo.slice(3))}" alt="" style="width:16px;height:16px" />`;
if (logo.startsWith('lucide:')) return html`<i data-lucide="${logo.slice(7)}" style="width:16px;height:16px"></i>`;
return html`<img src="${logo}" alt="" style="width:16px;height:16px;border-radius:3px" />`;
}

function buildServerSubList() {
if (state.sidebarServersLoading) return html`<div class="nav-sub-empty"><span class="spinner"></span> Loading...</div>`;
if (state.servers.length === 0) return html`<div class="nav-sub-empty">No servers</div>`;
Expand All @@ -1680,10 +1687,14 @@ function buildServerSubList() {
const isSuspended = s.status === 'suspended';
const dotClass = isSuspended ? 'dot-suspended' : (isInstalling ? 'dot-installing' : 'dot-active');
const isActive = state.currentPage === 'server' && state.serverId === s.id;
const eggName = s.eggDetails?.name || '';
return html`
<a class="nav-sub-item ${isActive ? 'active' : ''}" data-server-nav="${s.id}" href="/server/${s.id}">
<span class="nav-sub-dot ${dotClass}"></span>
${escapeHtml(s.name)}
<span class="nav-sub-nest-icon">${renderNestLogoSmall(s.nestLogo)}</span>
<span class="nav-sub-server-info">
<span class="nav-sub-server-name">${escapeHtml(s.name)}<span class="nav-sub-dot ${dotClass}"></span></span>
${eggName ? html`<span class="nav-sub-egg-name">${escapeHtml(eggName)}</span>` : ''}
</span>
</a>
`;
}).join('');
Expand Down Expand Up @@ -2917,6 +2928,122 @@ function isValidServerName(name) {
return name && name.length >= 1 && name.length <= 255 && /^[a-zA-Z0-9 _.-]+$/.test(name);
}

function showVerificationPage(name) {
const overlay = document.createElement('div');
overlay.id = 'verification-overlay';
overlay.className = 'verification-overlay';
overlay.innerHTML = html`
<div class="verification-card">
<div class="verification-spinner"></div>
<h2 class="verification-title">Creating "${escapeHtml(name)}"</h2>
<p class="verification-subtitle">Performing security checks...</p>
<div class="verification-steps" id="verification-steps">
<div class="verification-step active" id="vstep-vpn">
<div class="verification-step-icon"><span class="vstep-spinner"></span></div>
<span>Checking connection security</span>
</div>
<div class="verification-step" id="vstep-ua">
<div class="verification-step-icon"><span class="vstep-pending"></span></div>
<span>Verifying browser</span>
</div>
<div class="verification-step" id="vstep-email">
<div class="verification-step-icon"><span class="vstep-pending"></span></div>
<span>Checking email verification</span>
</div>
<div class="verification-step" id="vstep-country">
<div class="verification-step-icon"><span class="vstep-pending"></span></div>
<span>Verifying region</span>
</div>
<div class="verification-step" id="vstep-create">
<div class="verification-step-icon"><span class="vstep-pending"></span></div>
<span>Creating server</span>
</div>
</div>
</div>
`;
document.body.appendChild(overlay);
requestAnimationFrame(() => overlay.classList.add('open'));

let stepIndex = 0;
const steps = ['vstep-vpn', 'vstep-ua', 'vstep-email', 'vstep-country', 'vstep-create'];
const interval = setInterval(() => {
if (stepIndex < steps.length) {
const el = document.getElementById(steps[stepIndex]);
if (el) {
el.classList.remove('active');
el.classList.add('done');
el.querySelector('.vstep-spinner, .vstep-pending').outerHTML = '<svg class="vstep-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--accent-green)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
}
stepIndex++;
if (stepIndex < steps.length) {
const next = document.getElementById(steps[stepIndex]);
if (next) {
next.classList.add('active');
next.querySelector('.vstep-pending').outerHTML = '<span class="vstep-spinner"></span>';
}
}
} else {
clearInterval(interval);
}
}, 600);

overlay._interval = interval;
}

function showVerificationSuccess(name) {
const overlay = document.getElementById('verification-overlay');
if (!overlay) return;
clearInterval(overlay._interval);

const steps = overlay.querySelectorAll('.verification-step');
steps.forEach(s => {
s.classList.remove('active');
s.classList.add('done');
const icon = s.querySelector('.vstep-spinner, .vstep-pending');
if (icon) icon.outerHTML = '<svg class="vstep-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--accent-green)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
});

overlay.querySelector('.verification-card').innerHTML = html`
<svg class="verification-success-icon" width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="var(--accent-green)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
<h2 class="verification-title">Server Created!</h2>
<p class="verification-subtitle">"${escapeHtml(name)}" is now being set up.</p>
`;
}

function showVerificationError(message) {
const overlay = document.getElementById('verification-overlay');
if (!overlay) return;
clearInterval(overlay._interval);

const steps = overlay.querySelectorAll('.verification-step.active');
steps.forEach(s => {
s.classList.remove('active');
s.classList.add('failed');
const icon = s.querySelector('.vstep-spinner');
if (icon) icon.outerHTML = '<svg class="vstep-cross" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--accent-red)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
});

const card = overlay.querySelector('.verification-card');
card.innerHTML = html`
<svg class="verification-error-icon" width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="var(--accent-red)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
<h2 class="verification-title" style="color:var(--accent-red)">Verification Failed</h2>
<p class="verification-subtitle">${escapeHtml(message)}</p>
<button class="btn btn-primary" id="verification-retry-btn" style="margin-top:16px">Go Back</button>
`;

document.getElementById('verification-retry-btn').addEventListener('click', () => {
overlay.remove();
});
}

function removeVerificationOverlay() {
const overlay = document.getElementById('verification-overlay');
if (overlay) {
clearInterval(overlay._interval);
overlay.remove();
}
}

async function handleWizardCreate() {
const btn = $('#wizard-create-btn');
btn.disabled = true;
Expand All @@ -2935,16 +3062,21 @@ async function handleWizardCreate() {
const environment = {};
const dockerImage = createState.selectedDockerImage || '';

showVerificationPage(name);

try {
const capToken = document.querySelector('[name="cap-token"]')?.value || '';
await api('/servers/create', {
method: 'POST',
body: JSON.stringify({ name, nestId: nest.pteroNestId, eggId: egg.eggId, environment, capToken, dockerImage }),
});
showToast(`Server "${name}" created successfully!`, 'success');
navigateTo('servers');
showVerificationSuccess(name);
setTimeout(() => {
removeVerificationOverlay();
navigateTo('servers');
}, 2000);
} catch (err) {
showToast(err.message, 'error');
showVerificationError(err.message);
btn.disabled = false;
btn.innerHTML = '<i data-lucide="plus" style="width:16px;height:16px"></i> Create Server';
initIcons();
Expand Down
2 changes: 1 addition & 1 deletion routes/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,6 @@ router.get('/export-data', authenticateToken, sensitiveLimiter, async (req, res)
}
});

export { getClientIp };
export { getClientIp, fetchWithTimeout, normalizeClientIp, isPrivateIp };

export default router;
Loading
Loading