Skip to content

Commit e56a74a

Browse files
authored
Merge pull request #34 from ZeroHost-Code/beta
v1.1.1
2 parents 5233c88 + 88347cf commit e56a74a

5 files changed

Lines changed: 340 additions & 11 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "zerohost-dashboard",
3-
"version": "1.1.0",
3+
"version": "1.1.1",
44
"private": true,
55
"type": "module",
66
"scripts": {

public/css/style.css

Lines changed: 158 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,9 +1069,6 @@ cap-widget {
10691069
margin-bottom: 1px;
10701070
position: relative;
10711071
z-index: 2;
1072-
white-space: nowrap;
1073-
overflow: hidden;
1074-
text-overflow: ellipsis;
10751072
}
10761073

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

1083+
.nav-sub-server-info {
1084+
display: flex;
1085+
flex-direction: column;
1086+
min-width: 0;
1087+
}
1088+
1089+
.nav-sub-server-name {
1090+
overflow: hidden;
1091+
text-overflow: ellipsis;
1092+
white-space: nowrap;
1093+
display: flex;
1094+
align-items: center;
1095+
gap: 6px;
1096+
}
1097+
1098+
.nav-sub-egg-name {
1099+
font-size: 0.7rem;
1100+
color: var(--text-muted);
1101+
overflow: hidden;
1102+
text-overflow: ellipsis;
1103+
white-space: nowrap;
1104+
}
1105+
1106+
.nav-sub-nest-icon {
1107+
flex-shrink: 0;
1108+
display: flex;
1109+
align-items: center;
1110+
justify-content: center;
1111+
width: 28px;
1112+
height: 28px;
1113+
border-radius: 6px;
1114+
background: var(--bg-tertiary);
1115+
}
1116+
10861117
.nav-sub-item .nav-sub-dot {
10871118
width: 6px;
10881119
height: 6px;
10891120
border-radius: 50%;
10901121
flex-shrink: 0;
1122+
display: inline-block;
10911123
}
10921124

10931125
.nav-sub-dot.dot-active {
@@ -4607,3 +4639,126 @@ tbody tr:hover {
46074639
border-radius: 4px;
46084640
}
46094641

4642+
/* ===== VERIFICATION OVERLAY ===== */
4643+
.verification-overlay {
4644+
position: fixed;
4645+
inset: 0;
4646+
z-index: 9999;
4647+
display: flex;
4648+
align-items: center;
4649+
justify-content: center;
4650+
background: rgba(0, 0, 0, 0.85);
4651+
backdrop-filter: blur(8px);
4652+
opacity: 0;
4653+
transition: opacity 0.3s ease;
4654+
pointer-events: none;
4655+
}
4656+
4657+
.verification-overlay.open {
4658+
opacity: 1;
4659+
pointer-events: all;
4660+
}
4661+
4662+
.verification-card {
4663+
background: var(--bg-card);
4664+
border: 1px solid var(--border);
4665+
border-radius: var(--radius-lg);
4666+
padding: 48px 40px;
4667+
max-width: 420px;
4668+
width: 90%;
4669+
text-align: center;
4670+
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5);
4671+
}
4672+
4673+
.verification-spinner {
4674+
width: 48px;
4675+
height: 48px;
4676+
border: 3px solid rgba(255, 255, 255, 0.1);
4677+
border-top-color: var(--accent-1);
4678+
border-radius: 50%;
4679+
animation: spin 0.8s linear infinite;
4680+
margin: 0 auto 24px;
4681+
}
4682+
4683+
.verification-title {
4684+
font-size: 1.25rem;
4685+
font-weight: 600;
4686+
margin-bottom: 6px;
4687+
color: var(--text-primary);
4688+
}
4689+
4690+
.verification-subtitle {
4691+
font-size: 0.875rem;
4692+
color: var(--text-secondary);
4693+
margin-bottom: 28px;
4694+
}
4695+
4696+
.verification-steps {
4697+
display: flex;
4698+
flex-direction: column;
4699+
gap: 14px;
4700+
text-align: left;
4701+
}
4702+
4703+
.verification-step {
4704+
display: flex;
4705+
align-items: center;
4706+
gap: 12px;
4707+
padding: 10px 14px;
4708+
border-radius: var(--radius-sm);
4709+
background: var(--bg-tertiary);
4710+
font-size: 0.85rem;
4711+
color: var(--text-muted);
4712+
transition: all var(--transition);
4713+
}
4714+
4715+
.verification-step.active {
4716+
color: var(--text-primary);
4717+
background: rgba(238, 129, 50, 0.08);
4718+
border: 1px solid rgba(238, 129, 50, 0.2);
4719+
}
4720+
4721+
.verification-step.done {
4722+
color: var(--accent-green);
4723+
}
4724+
4725+
.verification-step.failed {
4726+
color: var(--accent-red);
4727+
}
4728+
4729+
.verification-step-icon {
4730+
width: 16px;
4731+
height: 16px;
4732+
flex-shrink: 0;
4733+
display: flex;
4734+
align-items: center;
4735+
justify-content: center;
4736+
}
4737+
4738+
.vstep-spinner {
4739+
display: inline-block;
4740+
width: 14px;
4741+
height: 14px;
4742+
border: 2px solid rgba(238, 129, 50, 0.25);
4743+
border-top-color: var(--accent-1);
4744+
border-radius: 50%;
4745+
animation: spin 0.7s linear infinite;
4746+
}
4747+
4748+
.vstep-pending {
4749+
display: inline-block;
4750+
width: 14px;
4751+
height: 14px;
4752+
border: 2px solid var(--text-muted);
4753+
border-radius: 50%;
4754+
opacity: 0.4;
4755+
}
4756+
4757+
.vstep-check, .vstep-cross {
4758+
flex-shrink: 0;
4759+
}
4760+
4761+
.verification-success-icon, .verification-error-icon {
4762+
margin-bottom: 16px;
4763+
}
4764+

public/js/app.js

Lines changed: 138 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1511,7 +1511,7 @@ async function renderDashboard() {
15111511
<div style="padding:8px 12px 0;display:flex;gap:16px;justify-content:center;flex-wrap:wrap">
15121512
15131513
</div>
1514-
<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>
1514+
<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>
15151515
</div>
15161516
<div class="sidebar-resizer" id="sidebar-resizer"></div>
15171517
</aside>
@@ -1672,6 +1672,13 @@ function initSidebarTooltip() {
16721672
});
16731673
}
16741674

1675+
function renderNestLogoSmall(logo) {
1676+
if (!logo) return html`<i data-lucide="box" style="width:16px;height:16px;color:var(--text-secondary)"></i>`;
1677+
if (logo.startsWith('si:')) return html`<img src="${siUrl(logo.slice(3))}" alt="" style="width:16px;height:16px" />`;
1678+
if (logo.startsWith('lucide:')) return html`<i data-lucide="${logo.slice(7)}" style="width:16px;height:16px"></i>`;
1679+
return html`<img src="${logo}" alt="" style="width:16px;height:16px;border-radius:3px" />`;
1680+
}
1681+
16751682
function buildServerSubList() {
16761683
if (state.sidebarServersLoading) return html`<div class="nav-sub-empty"><span class="spinner"></span> Loading...</div>`;
16771684
if (state.servers.length === 0) return html`<div class="nav-sub-empty">No servers</div>`;
@@ -1680,10 +1687,14 @@ function buildServerSubList() {
16801687
const isSuspended = s.status === 'suspended';
16811688
const dotClass = isSuspended ? 'dot-suspended' : (isInstalling ? 'dot-installing' : 'dot-active');
16821689
const isActive = state.currentPage === 'server' && state.serverId === s.id;
1690+
const eggName = s.eggDetails?.name || '';
16831691
return html`
16841692
<a class="nav-sub-item ${isActive ? 'active' : ''}" data-server-nav="${s.id}" href="/server/${s.id}">
1685-
<span class="nav-sub-dot ${dotClass}"></span>
1686-
${escapeHtml(s.name)}
1693+
<span class="nav-sub-nest-icon">${renderNestLogoSmall(s.nestLogo)}</span>
1694+
<span class="nav-sub-server-info">
1695+
<span class="nav-sub-server-name">${escapeHtml(s.name)}<span class="nav-sub-dot ${dotClass}"></span></span>
1696+
${eggName ? html`<span class="nav-sub-egg-name">${escapeHtml(eggName)}</span>` : ''}
1697+
</span>
16871698
</a>
16881699
`;
16891700
}).join('');
@@ -2917,6 +2928,122 @@ function isValidServerName(name) {
29172928
return name && name.length >= 1 && name.length <= 255 && /^[a-zA-Z0-9 _.-]+$/.test(name);
29182929
}
29192930

2931+
function showVerificationPage(name) {
2932+
const overlay = document.createElement('div');
2933+
overlay.id = 'verification-overlay';
2934+
overlay.className = 'verification-overlay';
2935+
overlay.innerHTML = html`
2936+
<div class="verification-card">
2937+
<div class="verification-spinner"></div>
2938+
<h2 class="verification-title">Creating "${escapeHtml(name)}"</h2>
2939+
<p class="verification-subtitle">Performing security checks...</p>
2940+
<div class="verification-steps" id="verification-steps">
2941+
<div class="verification-step active" id="vstep-vpn">
2942+
<div class="verification-step-icon"><span class="vstep-spinner"></span></div>
2943+
<span>Checking connection security</span>
2944+
</div>
2945+
<div class="verification-step" id="vstep-ua">
2946+
<div class="verification-step-icon"><span class="vstep-pending"></span></div>
2947+
<span>Verifying browser</span>
2948+
</div>
2949+
<div class="verification-step" id="vstep-email">
2950+
<div class="verification-step-icon"><span class="vstep-pending"></span></div>
2951+
<span>Checking email verification</span>
2952+
</div>
2953+
<div class="verification-step" id="vstep-country">
2954+
<div class="verification-step-icon"><span class="vstep-pending"></span></div>
2955+
<span>Verifying region</span>
2956+
</div>
2957+
<div class="verification-step" id="vstep-create">
2958+
<div class="verification-step-icon"><span class="vstep-pending"></span></div>
2959+
<span>Creating server</span>
2960+
</div>
2961+
</div>
2962+
</div>
2963+
`;
2964+
document.body.appendChild(overlay);
2965+
requestAnimationFrame(() => overlay.classList.add('open'));
2966+
2967+
let stepIndex = 0;
2968+
const steps = ['vstep-vpn', 'vstep-ua', 'vstep-email', 'vstep-country', 'vstep-create'];
2969+
const interval = setInterval(() => {
2970+
if (stepIndex < steps.length) {
2971+
const el = document.getElementById(steps[stepIndex]);
2972+
if (el) {
2973+
el.classList.remove('active');
2974+
el.classList.add('done');
2975+
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>';
2976+
}
2977+
stepIndex++;
2978+
if (stepIndex < steps.length) {
2979+
const next = document.getElementById(steps[stepIndex]);
2980+
if (next) {
2981+
next.classList.add('active');
2982+
next.querySelector('.vstep-pending').outerHTML = '<span class="vstep-spinner"></span>';
2983+
}
2984+
}
2985+
} else {
2986+
clearInterval(interval);
2987+
}
2988+
}, 600);
2989+
2990+
overlay._interval = interval;
2991+
}
2992+
2993+
function showVerificationSuccess(name) {
2994+
const overlay = document.getElementById('verification-overlay');
2995+
if (!overlay) return;
2996+
clearInterval(overlay._interval);
2997+
2998+
const steps = overlay.querySelectorAll('.verification-step');
2999+
steps.forEach(s => {
3000+
s.classList.remove('active');
3001+
s.classList.add('done');
3002+
const icon = s.querySelector('.vstep-spinner, .vstep-pending');
3003+
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>';
3004+
});
3005+
3006+
overlay.querySelector('.verification-card').innerHTML = html`
3007+
<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>
3008+
<h2 class="verification-title">Server Created!</h2>
3009+
<p class="verification-subtitle">"${escapeHtml(name)}" is now being set up.</p>
3010+
`;
3011+
}
3012+
3013+
function showVerificationError(message) {
3014+
const overlay = document.getElementById('verification-overlay');
3015+
if (!overlay) return;
3016+
clearInterval(overlay._interval);
3017+
3018+
const steps = overlay.querySelectorAll('.verification-step.active');
3019+
steps.forEach(s => {
3020+
s.classList.remove('active');
3021+
s.classList.add('failed');
3022+
const icon = s.querySelector('.vstep-spinner');
3023+
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>';
3024+
});
3025+
3026+
const card = overlay.querySelector('.verification-card');
3027+
card.innerHTML = html`
3028+
<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>
3029+
<h2 class="verification-title" style="color:var(--accent-red)">Verification Failed</h2>
3030+
<p class="verification-subtitle">${escapeHtml(message)}</p>
3031+
<button class="btn btn-primary" id="verification-retry-btn" style="margin-top:16px">Go Back</button>
3032+
`;
3033+
3034+
document.getElementById('verification-retry-btn').addEventListener('click', () => {
3035+
overlay.remove();
3036+
});
3037+
}
3038+
3039+
function removeVerificationOverlay() {
3040+
const overlay = document.getElementById('verification-overlay');
3041+
if (overlay) {
3042+
clearInterval(overlay._interval);
3043+
overlay.remove();
3044+
}
3045+
}
3046+
29203047
async function handleWizardCreate() {
29213048
const btn = $('#wizard-create-btn');
29223049
btn.disabled = true;
@@ -2935,16 +3062,21 @@ async function handleWizardCreate() {
29353062
const environment = {};
29363063
const dockerImage = createState.selectedDockerImage || '';
29373064

3065+
showVerificationPage(name);
3066+
29383067
try {
29393068
const capToken = document.querySelector('[name="cap-token"]')?.value || '';
29403069
await api('/servers/create', {
29413070
method: 'POST',
29423071
body: JSON.stringify({ name, nestId: nest.pteroNestId, eggId: egg.eggId, environment, capToken, dockerImage }),
29433072
});
2944-
showToast(`Server "${name}" created successfully!`, 'success');
2945-
navigateTo('servers');
3073+
showVerificationSuccess(name);
3074+
setTimeout(() => {
3075+
removeVerificationOverlay();
3076+
navigateTo('servers');
3077+
}, 2000);
29463078
} catch (err) {
2947-
showToast(err.message, 'error');
3079+
showVerificationError(err.message);
29483080
btn.disabled = false;
29493081
btn.innerHTML = '<i data-lucide="plus" style="width:16px;height:16px"></i> Create Server';
29503082
initIcons();

routes/auth.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,6 @@ router.get('/export-data', authenticateToken, sensitiveLimiter, async (req, res)
911911
}
912912
});
913913

914-
export { getClientIp };
914+
export { getClientIp, fetchWithTimeout, normalizeClientIp, isPrivateIp };
915915

916916
export default router;

0 commit comments

Comments
 (0)