@@ -295,6 +295,30 @@
< / div >
< / div >
<!-- Hysteria Card -->
< div class = "card card-hover protocol-card protocol-hysteria" id = "proto-hysteria" >
< div style = "display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);" >
< div class = "protocol-icon" > {{ icon('refresh') }}< / div >
< div class = "flex gap-sm" id = "hysteria-ctrl" style = "display:none!important;" > < / div >
< / div >
< div class = "protocol-name" > Hysteria 2< / div >
< div class = "protocol-desc" >
{{ _('hysteria_desc') }}
< / div >
< div class = "protocol-status" id = "hysteria-status" >
< span class = "badge badge-warn" > < span class = "badge-dot" > < / span > {{ _('not_checked') }}< / span >
< / div >
< div id = "hysteria-info" class = "hidden" >
< div class = "protocol-info" id = "hysteria-info-grid" > < / div >
< / div >
< div class = "flex gap-sm" id = "hysteria-actions" >
< button class = "btn btn-primary btn-sm" onclick = "openInstallModal('hysteria')" id = "hysteria-install-btn"
style="flex:1">
{{ _('install') }}
< / button >
< / div >
< / div >
<!-- WireGuard Card -->
< div class = "card card-hover protocol-card protocol-wireguard" id = "proto-wireguard" >
< div style = "display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);" >
@@ -657,6 +681,28 @@
< div class = "form-hint" > {{ _('nginx_install_hint') }}< / div >
< / div >
< div id = "hysteriaOptions"
style="display:none; padding: var(--space-md); background: rgba(0,0,0,0.03); border-radius: var(--radius-md); margin-bottom: var(--space-md);">
< div class = "form-hint" style = "margin-bottom: var(--space-md); padding: var(--space-sm) var(--space-md); border-radius: var(--radius-sm); background: rgba(234,179,8,0.12); border: 1px solid rgba(234,179,8,0.35); color: var(--text);" >
{{ _('hysteria_ports_warning') }}
< / div >
< div class = "form-group" >
< label class = "form-label" > {{ _('port') }} (UDP) *< / label >
< input class = "form-input" type = "number" id = "installHysteriaPort" value = "8998" min = "1" max = "65535" >
< div class = "form-hint" > {{ _('hysteria_port_hint') }}< / div >
< / div >
< div class = "form-group" >
< label class = "form-label" > {{ _('hysteria_domain') }}< / label >
< input class = "form-input" type = "text" id = "installHysteriaDomain" placeholder = "vpn.example.com" oninput = "updateHysteriaDnsHint()" >
< div class = "form-hint" id = "hysteriaDnsHint" > < / div >
< / div >
< div class = "form-group" >
< label class = "form-label" > {{ _('hysteria_email') }}< / label >
< input class = "form-input" type = "email" id = "installHysteriaEmail" placeholder = "admin@example.com" >
< / div >
< div class = "form-hint" > {{ _('hysteria_install_hint') }}< / div >
< / div >
< div class = "modal-footer" >
< button class = "btn btn-secondary" onclick = "closeModal('installModal')" > {{ _('cancel') }}< / button >
< button class = "btn btn-primary" onclick = "installProtocol()" id = "installBtn" >
@@ -711,6 +757,45 @@
< / div >
< / div >
<!-- ===== Hysteria Settings Modal ===== -->
< div class = "modal-backdrop" id = "hysteriaSettingsModal" >
< div class = "modal" >
< div class = "modal-header" >
< h2 class = "modal-title" id = "hysteriaSettingsTitle" > {{ _('hysteria_settings_title') }}< / h2 >
< button class = "modal-close" onclick = "closeModal('hysteriaSettingsModal')" > × < / button >
< / div >
< input type = "hidden" id = "hysteriaSetProto" value = "hysteria" / >
< div class = "form-hint" style = "margin-bottom: var(--space-md); padding: var(--space-sm) var(--space-md); border-radius: var(--radius-sm); background: rgba(234,179,8,0.12); border: 1px solid rgba(234,179,8,0.35);" >
{{ _('hysteria_ports_warning') }}
< / div >
< div class = "form-group" >
< label class = "form-label" > {{ _('hysteria_domain') }}< / label >
< input class = "form-input" type = "text" id = "hysteriaSetDomain" placeholder = "vpn.example.com" >
< div class = "form-hint" > {{ _('hysteria_ssl_change_hint') }}< / div >
< / div >
< div class = "form-group" >
< label class = "form-label" > {{ _('hysteria_email') }}< / label >
< input class = "form-input" type = "email" id = "hysteriaSetEmail" placeholder = "admin@example.com" >
< / div >
< div class = "form-group" >
< label class = "form-label" > {{ _('port') }} (UDP)< / label >
< input class = "form-input" type = "number" id = "hysteriaSetPort" min = "1" max = "65535" value = "8998" >
< div class = "form-hint" > {{ _('hysteria_port_change_hint') }}< / div >
< / div >
< div class = "modal-footer" style = "flex-wrap: wrap; gap: var(--space-sm);" >
< button class = "btn btn-secondary" onclick = "closeModal('hysteriaSettingsModal')" > {{ _('cancel') }}< / button >
< button class = "btn btn-secondary" onclick = "renewHysteriaSsl()" id = "hysteriaRenewBtn" >
< span id = "hysteriaRenewBtnText" > {{ _('hysteria_renew_ssl') }}< / span >
< div class = "spinner hidden" id = "hysteriaRenewSpinner" > < / div >
< / button >
< button class = "btn btn-primary" onclick = "saveHysteriaSettings()" id = "hysteriaSaveBtn" >
< span id = "hysteriaSaveBtnText" > {{ _('save') }}< / span >
< div class = "spinner hidden" id = "hysteriaSaveSpinner" > < / div >
< / button >
< / div >
< / div >
< / div >
<!-- ===== NGINX Site Modal ===== -->
< div class = "modal-backdrop" id = "siteConfigModal" >
< div class = "modal" style = "max-width:680px;" >
@@ -961,12 +1046,15 @@
< script >
const SERVER_ID = {{ server_id }};
const SERVER_HOST = "{{ server.host }}";
const SERVER_SSL_DOMAIN = {{ ((server.server_info or {}).get('ssl_domain') or '') | tojson }};
const SERVER_SSL_EMAIL = {{ ((server.server_info or {}).get('ssl_email') or '') | tojson }};
const MARKETPLACE_APPS = [
{ proto: 'awg2', category: 'protocols', icon: 'sparkles', title: 'AmneziaWG 2.0', descKey: 'awg_desc', badge: 'NEW' },
{ proto: 'awg', category: 'protocols', icon: 'shield', title: 'AmneziaWG', descKey: 'awg_desc' },
{ proto: 'awg_legacy', category: 'protocols', icon: 'radio', title: 'AmneziaWG Legacy', descKey: 'awg_legacy_desc' },
{ proto: 'xray', category: 'protocols', icon: 'zap', title: 'Xray (VLESS-Reality)', descKey: 'xray_desc' },
{ proto: 'telemt', category: 'protocols', icon: 'plane', title: 'Telemt (Telegram Proxy)', descKey: 'telemt_desc' },
{ proto: 'hysteria', category: 'protocols', icon: 'refresh', title: 'Hysteria 2', descKey: 'hysteria_desc' },
{ proto: 'wireguard', category: 'protocols', icon: 'lock', title: 'WireGuard', descKey: 'wireguard_desc' },
{ proto: 'dns', category: 'services', icon: 'search', title: 'AmneziaDNS', descKey: 'dns_desc' },
{ proto: 'adguard', category: 'services', icon: 'shield-check', title: 'AdGuard Home', descKey: 'adguard_desc' },
@@ -1002,7 +1090,7 @@
}
function isMultiInstanceBase(proto) {
return ['awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'socks5'].includes(protoBase(proto));
return ['awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'hysteria', 'socks5'].includes(protoBase(proto));
}
function nextSuggestedPort(base, fallback) {
@@ -1207,6 +1295,7 @@
case 'awg_legacy': title = 'AmneziaWG Legacy'; break;
case 'xray': title = 'Xray'; break;
case 'telemt': title = 'Telemt'; break;
case 'hysteria': title = 'Hysteria 2'; break;
case 'wireguard': title = 'WireGuard'; break;
case 'dns': title = 'AmneziaDNS'; break;
case 'socks5': title = 'SOCKS5 Proxy'; break;
@@ -1266,7 +1355,7 @@
for (const [proto, info] of orderedProtocolEntries(data.protocols)) {
updateProtocolCard(proto, info);
const isVPN = ['awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'wireguard'].includes(protoBase(proto));
const isVPN = ['awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'hysteria', 'wireguard'].includes(protoBase(proto));
if (info.container_running & & isVPN) {
const opt = document.createElement('option');
opt.value = proto;
@@ -1495,6 +1584,9 @@
grid = buildServiceInfoGrid(proto, info);
} else if (info.port) {
grid += `< div class = "protocol-info-item" > < span class = "protocol-info-label" > ${_('port')}< / span > < span class = "protocol-info-value" > ${info.port}/${(['xray', 'telemt'].includes(protoBase(proto))) ? 'TCP' : 'UDP'}< / span > < / div > `;
if (protoBase(proto) === 'hysteria' & & info.domain) {
grid += `< div class = "protocol-info-item" > < span class = "protocol-info-label" > ${_('hysteria_domain')}< / span > < span class = "protocol-info-value" > ${info.domain}< / span > < / div > `;
}
}
if (!isService & & info.clients_count !== undefined) {
grid += `< div class = "protocol-info-item" > < span class = "protocol-info-label" > ${_('connections')}< / span > < span class = "protocol-info-value" > ${info.clients_count}< / span > < / div > `;
@@ -1529,6 +1621,14 @@
< button class = "btn btn-secondary btn-sm" onclick = "showProtocolBackups('${proto}')" style = "flex:1" > ${_('backup')}< / button >
< button class = "btn btn-danger btn-sm" onclick = "uninstallProtocol('${proto}')" > ${_('uninstall')}< / button >
`;
} else if (protoBase(proto) === 'hysteria') {
actionsEl.innerHTML = `
< button class = "btn btn-secondary btn-sm" onclick = "selectProtocolForConns('${proto}')" style = "flex:1" > ${_('connections')}< / button >
< button class = "btn btn-secondary btn-sm" onclick = "openHysteriaSettings('${proto}')" > ${_('hysteria_change_port')}< / button >
< button class = "btn btn-secondary btn-sm" onclick = "showProtocolBackups('${proto}')" > ${_('backup')}< / button >
< button class = "btn btn-danger btn-sm" onclick = "uninstallProtocol('${proto}')" > ${_('uninstall')}< / button >
`;
showConnectionsSection();
} else {
actionsEl.innerHTML = `
< button class = "btn btn-secondary btn-sm" onclick = "selectProtocolForConns('${proto}')" style = "flex:1" > ${_('connections')}< / button >
@@ -1565,6 +1665,7 @@
} else if (info.container_exists) {
installedProtocols[proto] = true;
const errText = String(info.error || '').trim();
const portBusy = !!info.port_busy || /address already in use|UDP port \d+ already in use|already in use/i.test(errText);
statusEl.innerHTML = errText
? `< span class = "badge badge-danger" style = "max-width:100%; white-space:normal; text-align:left; line-height:1.35;" > < span class = "badge-dot" > < / span > ${_('stop')}: ${escapeHtml(errText.slice(0, 120))}< / span > `
: `< span class = "badge badge-danger" > < span class = "badge-dot" > < / span > ${_('stop')}< / span > `;
@@ -1583,6 +1684,25 @@
if (errText) {
grid += `< div class = "protocol-info-item" style = "grid-column:1/-1;" > < span class = "protocol-info-label" > ${_('error')}< / span > < span class = "protocol-info-value" style = "color:var(--danger); word-break:break-word;" > ${escapeHtml(errText)}< / span > < / div > `;
}
if (protoBase(proto) === 'hysteria') {
const curPort = parseInt(info.port, 10) || 443;
const suggest = (curPort === 443 || portBusy) ? 8998 : curPort;
grid += `
< div class = "protocol-info-item" style = "grid-column:1/-1; display:block; padding-top:8px;" >
< span class = "protocol-info-label" style = "display:block; margin-bottom:6px;" > ${_('hysteria_change_port')} (UDP)< / span >
< div style = "display:flex; gap:8px; flex-wrap:wrap; align-items:center;" >
< input class = "form-input" type = "number" id = "hysteriaInlinePort-${protoDomKey(proto)}"
value="${suggest}" min="1" max="65535" style="width:120px; flex:0 0 auto;">
< button class = "btn btn-primary btn-sm" type = "button"
onclick="applyHysteriaPort('${proto}', document.getElementById('hysteriaInlinePort-${protoDomKey(proto)}').value)">
${_('save')} & ${_('start_btn')}
< / button >
< button class = "btn btn-secondary btn-sm" type = "button" onclick = "openHysteriaSettings('${proto}')" > ${_('hysteria_settings_title')}< / button >
< / div >
< div class = "form-hint" style = "margin-top:6px;" > ${_('hysteria_port_change_hint')}< / div >
< / div >
`;
}
infoGrid.innerHTML = grid;
}
if (isService) {
@@ -1592,6 +1712,13 @@
< button class = "btn btn-secondary btn-sm" onclick = "showProtocolBackups('${proto}')" > ${_('backup')}< / button >
< button class = "btn btn-danger btn-sm" onclick = "uninstallProtocol('${proto}')" > ${_('uninstall')}< / button >
`;
} else if (protoBase(proto) === 'hysteria') {
actionsEl.innerHTML = `
< button class = "btn btn-primary btn-sm" onclick = "openHysteriaSettings('${proto}')" style = "flex:1" > ${_('hysteria_change_port')}< / button >
< button class = "btn btn-secondary btn-sm" onclick = "showContainerLogs('${proto}')" > ${_('logs_btn')}< / button >
< button class = "btn btn-secondary btn-sm" onclick = "openInstallModal('${proto}')" > ${_('reinstall')}< / button >
< button class = "btn btn-danger btn-sm" onclick = "uninstallProtocol('${proto}')" > ${_('uninstall')}< / button >
`;
} else {
actionsEl.innerHTML = `
< button class = "btn btn-primary btn-sm" onclick = "openInstallModal('${proto}')" style = "flex:1" > ${_('reinstall')}< / button >
@@ -1872,6 +1999,14 @@
hint.innerHTML = `${_('nginx_dns_hint')} < code > A ${domain} ${SERVER_HOST}< / code > `;
}
function updateHysteriaDnsHint() {
const input = document.getElementById('installHysteriaDomain');
const hint = document.getElementById('hysteriaDnsHint');
if (!input || !hint) return;
const domain = (input.value || '').trim() || 'vpn.example.com';
hint.innerHTML = `${_('hysteria_dns_hint')} < code > A ${domain} ${SERVER_HOST}< / code > `;
}
function openInstallModal(proto, installAnother = false) {
const base = protoBase(proto);
currentInstallProto = installAnother ? base : proto;
@@ -1887,11 +2022,13 @@
const socks5Opts = document.getElementById('socks5Options');
const adguardOpts = document.getElementById('adguardOptions');
const nginxOpts = document.getElementById('nginxOptions');
const hysteriaOpts = document.getElementById('hysteriaOptions');
telemtOpts.style.display = 'none';
socks5Opts.style.display = 'none';
adguardOpts.style.display = 'none';
nginxOpts.style.display = 'none';
hysteriaOpts.style.display = 'none';
if (portGroup) portGroup.style.display = '';
if (base === 'dns') {
@@ -1928,7 +2065,27 @@
portInput.disabled = false;
portHint.textContent = _('nginx_port_hint');
nginxOpts.style.display = 'block';
const nxDomain = document.getElementById('installNginxDomain');
const nxEmail = document.getElementById('installNginxEmail');
if (nxDomain & & !nxDomain.value & & SERVER_SSL_DOMAIN) nxDomain.value = SERVER_SSL_DOMAIN;
if (nxEmail & & !nxEmail.value & & SERVER_SSL_EMAIL) nxEmail.value = SERVER_SSL_EMAIL;
updateNginxDnsHint();
} else if (base === 'hysteria') {
if (portGroup) portGroup.style.display = 'none';
const suggested = currentInstallAnother ? nextSuggestedPort(currentInstallProto, 8998) : '8998';
portInput.value = suggested;
portInput.disabled = false;
hysteriaOpts.style.display = 'block';
const hyPort = document.getElementById('installHysteriaPort');
if (hyPort) {
hyPort.value = suggested;
hyPort.oninput = () => { portInput.value = hyPort.value; };
}
const hyDomain = document.getElementById('installHysteriaDomain');
const hyEmail = document.getElementById('installHysteriaEmail');
if (hyDomain & & !hyDomain.value & & SERVER_SSL_DOMAIN) hyDomain.value = SERVER_SSL_DOMAIN;
if (hyEmail & & !hyEmail.value & & SERVER_SSL_EMAIL) hyEmail.value = SERVER_SSL_EMAIL;
updateHysteriaDnsHint();
} else {
portLabel.textContent = _('port') + ' (UDP)';
portInput.value = currentInstallAnother ? nextSuggestedPort(currentInstallProto, 55424) : '55424';
@@ -1995,6 +2152,13 @@
} else if (protoBase(currentInstallProto) === 'nginx') {
params.nginx_domain = document.getElementById('installNginxDomain').value.trim();
params.nginx_email = document.getElementById('installNginxEmail').value.trim();
} else if (protoBase(currentInstallProto) === 'hysteria') {
const hyPortEl = document.getElementById('installHysteriaPort');
if (hyPortEl & & hyPortEl.value) {
params.port = hyPortEl.value;
}
params.hysteria_domain = document.getElementById('installHysteriaDomain').value.trim();
params.hysteria_email = document.getElementById('installHysteriaEmail').value.trim();
}
const result = await apiCall(`/api/servers/${SERVER_ID}/install`, 'POST', params);
clearInterval(progressInterval);
@@ -2210,6 +2374,107 @@
}
}
// ========== Hysteria Settings (port) ==========
async function applyHysteriaPort(proto, portValue) {
const port = parseInt(portValue, 10);
if (!port || port < 1 | | port > 65535) {
showToast(_('error') + ': invalid port', 'error');
return;
}
if (port === 80) {
showToast(_('error') + ': port 80 reserved', 'error');
return;
}
try {
showToast(_('saving') || 'Saving...', 'info');
const res = await apiCall(`/api/servers/${SERVER_ID}/hysteria/settings`, 'POST', {
protocol: proto || 'hysteria', port,
});
showToast(res.message || _('hysteria_settings_saved'), 'success');
setTimeout(() => checkServer(), 800);
} catch (err) {
showToast(_('error') + ': ' + err.message, 'error');
openHysteriaSettings(proto);
}
}
async function openHysteriaSettings(proto = 'hysteria') {
const live = currentProtocolStatus[proto] || {};
const livePort = live.port || 443;
const suggest = (parseInt(livePort, 10) === 443) ? 8998 : (livePort || 8998);
document.getElementById('hysteriaSetProto').value = proto;
document.getElementById('hysteriaSettingsTitle').textContent =
`${_('hysteria_settings_title')} — ${getProtoTitle(proto)}`;
document.getElementById('hysteriaSetDomain').value = live.domain || SERVER_SSL_DOMAIN || '';
document.getElementById('hysteriaSetEmail').value = live.email || SERVER_SSL_EMAIL || '';
document.getElementById('hysteriaSetPort').value = suggest;
openModal('hysteriaSettingsModal');
try {
const data = await apiCall(`/api/servers/${SERVER_ID}/hysteria/settings?protocol=${encodeURIComponent(proto)}`);
if (data.domain) document.getElementById('hysteriaSetDomain').value = data.domain;
if (data.email) document.getElementById('hysteriaSetEmail').value = data.email;
const p = parseInt(data.port, 10);
if (p & & p !== 443) document.getElementById('hysteriaSetPort').value = p;
else if (p === 443) document.getElementById('hysteriaSetPort').value = 8998;
} catch (_) { /* modal already open with local values */ }
}
async function saveHysteriaSettings(opts = {}) {
const btn = document.getElementById('hysteriaSaveBtn');
const text = document.getElementById('hysteriaSaveBtnText');
const spinner = document.getElementById('hysteriaSaveSpinner');
const renewBtn = document.getElementById('hysteriaRenewBtn');
const renewText = document.getElementById('hysteriaRenewBtnText');
const renewSpinner = document.getElementById('hysteriaRenewSpinner');
const proto = document.getElementById('hysteriaSetProto').value || 'hysteria';
const port = parseInt(document.getElementById('hysteriaSetPort').value, 10);
const domain = document.getElementById('hysteriaSetDomain').value.trim();
const email = document.getElementById('hysteriaSetEmail').value.trim();
const renewSsl = !!opts.renew_ssl;
if (!port || port < 1 | | port > 65535) {
showToast(_('error') + ': invalid port', 'error');
return;
}
if (port === 80) {
showToast(_('error') + ': port 80 reserved', 'error');
return;
}
if ((renewSsl || domain) & & (!domain || !email)) {
showToast(_('error') + ': ' + _('hysteria_ssl_required'), 'error');
return;
}
btn.disabled = true;
if (renewBtn) renewBtn.disabled = true;
if (renewSsl) {
renewText.textContent = _('saving') || 'Saving...';
renewSpinner.classList.remove('hidden');
} else {
text.textContent = _('saving') || 'Saving...';
spinner.classList.remove('hidden');
}
try {
const res = await apiCall(`/api/servers/${SERVER_ID}/hysteria/settings`, 'POST', {
protocol: proto, port, domain, email, renew_ssl: renewSsl,
});
showToast(res.message || _('hysteria_settings_saved'), 'success');
closeModal('hysteriaSettingsModal');
setTimeout(() => checkServer(), 800);
} catch (err) {
showToast(_('error') + ': ' + err.message, 'error');
} finally {
btn.disabled = false;
if (renewBtn) renewBtn.disabled = false;
text.textContent = _('save');
spinner.classList.add('hidden');
if (renewText) renewText.textContent = _('hysteria_renew_ssl');
if (renewSpinner) renewSpinner.classList.add('hidden');
}
}
async function renewHysteriaSsl() {
return saveHysteriaSettings({ renew_ssl: true });
}
// ========== SOCKS5 Settings ==========
async function openSocks5Settings(proto = 'socks5') {
currentSocks5Proto = proto || 'socks5';
@@ -2294,7 +2559,7 @@
const sent = userData.dataSent || '';
const initial = name.charAt(0).toUpperCase();
const enabled = (client.enabled !== undefined) ? client.enabled : (userData.enabled !== false);
const hasPrivKey = !!userData.clientPrivateKey || proto === 'xray' || proto === 'telemt';
const hasPrivKey = !!userData.clientPrivateKey || proto === 'xray' || proto === 'telemt' || protoBase(proto) === 'hysteria' ;
const assignedUser = client.assigned_user || '';
let metaHtml = '';
@@ -2332,7 +2597,7 @@
< / div >
< / div >
< div class = "client-actions" >
< button class = "btn btn-secondary btn-sm btn-icon" onclick = "showConnectionConfig('${escapeJs(client.clientId)}', '${escapeJs(name)}', ${!!userData.clientPrivateKey || proto === 'xray' || proto === 'telemt'})" title = "${_('config')}" > ${uiIcon('file')}< / button >
< button class = "btn btn-secondary btn-sm btn-icon" onclick = "showConnectionConfig('${escapeJs(client.clientId)}', '${escapeJs(name)}', ${!!userData.clientPrivateKey || proto === 'xray' || proto === 'telemt' || protoBase(proto) === 'hysteria' })" title = "${_('config')}" > ${uiIcon('file')}< / button >
${proto === 'telemt' ? `< button class = "btn btn-secondary btn-sm btn-icon" onclick = "editConnection('${escapeJs(client.clientId)}')" title = "${_('edit')}" > ${uiIcon('pencil')}< / button > ` : ''}
< button class = "btn btn-secondary btn-sm btn-icon" onclick = "toggleConnection('${escapeJs(client.clientId)}', ${!enabled})" title = "${toggleTitle}" > ${toggleIcon}< / button >
< button class = "btn btn-danger btn-sm btn-icon" onclick = "removeConnection('${escapeJs(client.clientId)}')" title = "${_('delete')}" > ${uiIcon('trash')}< / button >
@@ -2384,8 +2649,8 @@
document.getElementById('configModalTitle').textContent = `${_('config')} — ${connName}`;
document.getElementById('configText').textContent = result.config;
document.getElementById('vpnLinkText').textContent = currentVpnLink;
document.getElementById('panel-vpn').style.display = (proto === 'telemt' ? 'none' : '');
document.querySelectorAll('.config-tab')[1].style.display = (proto === 'telemt' ? 'none' : '');
document.getElementById('panel-vpn').style.display = (proto === 'telemt' || protoBase(proto) === 'hysteria' ? 'none' : '');
document.querySelectorAll('.config-tab')[1].style.display = (proto === 'telemt' || protoBase(proto) === 'hysteria' ? 'none' : '');
switchConfigTab('conf');
openModal('configModal');
generateQR(result.config);
@@ -2463,9 +2728,9 @@
const proto = document.getElementById('connProtoSelect').value;
// Restore tabs visibility first
document.getElementById('panel-vpn').style.display = (proto === 'telemt' ? 'none' : '');
document.getElementById('panel-vpn').style.display = (proto === 'telemt' || protoBase(proto) === 'hysteria' ? 'none' : '');
document.getElementById('panel-qr').style.display = '';
document.querySelectorAll('.config-tab')[1].style.display = (proto === 'telemt' ? 'none' : '');
document.querySelectorAll('.config-tab')[1].style.display = (proto === 'telemt' || protoBase(proto) === 'hysteria' ? 'none' : '');
document.querySelectorAll('.config-tab')[2].style.display = '';
// Client was created via native Amnezia app — private key is not stored server-side