Wire Hysteria 2 UI with per-server SSL defaults and renew.

Expose domain/email on servers, warn that TCP 80/443 must be free on install, and allow Let's Encrypt re-issue from Hysteria settings.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
orohi
2026-07-26 09:46:20 +03:00
co-authored by Cursor
parent ed5c54cf96
commit 183071f588
17 changed files with 653 additions and 33 deletions
+28
View File
@@ -19,6 +19,8 @@
<div class="card card-hover server-card" id="server-{{ loop.index0 }}" data-idx="{{ loop.index0 }}"
data-name="{{ server.name }}" data-host="{{ server.host }}" data-port="{{ server.ssh_port }}"
data-username="{{ server.username }}" data-auth="{{ 'key' if server.private_key else 'password' }}"
data-ssl-domain="{{ (server.server_info or {}).get('ssl_domain', '') }}"
data-ssl-email="{{ (server.server_info or {}).get('ssl_email', '') }}"
draggable="true">
<div class="server-meta">
<div class="server-icon">{{ icon('server') }}</div>
@@ -137,6 +139,16 @@
</div>
</div>
<div class="form-group" style="margin-top: var(--space-md)">
<label class="form-label">{{ _('server_ssl_domain') }}</label>
<input class="form-input" type="text" id="editServerSslDomain" placeholder="vpn.example.com">
<div class="form-hint">{{ _('server_ssl_hint') }}</div>
</div>
<div class="form-group">
<label class="form-label">{{ _('server_ssl_email') }}</label>
<input class="form-input" type="email" id="editServerSslEmail" placeholder="admin@example.com">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeModal('editServerModal')">{{ _('cancel')
}}</button>
@@ -202,6 +214,16 @@
</div>
</div>
<div class="form-group" style="margin-top: var(--space-md)">
<label class="form-label">{{ _('server_ssl_domain') }}</label>
<input class="form-input" type="text" id="serverSslDomain" placeholder="vpn.example.com">
<div class="form-hint">{{ _('server_ssl_hint') }}</div>
</div>
<div class="form-group">
<label class="form-label">{{ _('server_ssl_email') }}</label>
<input class="form-input" type="email" id="serverSslEmail" placeholder="admin@example.com">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeModal('addServerModal')">{{ _('cancel')
}}</button>
@@ -244,6 +266,8 @@
username: document.getElementById('serverUser').value,
password: document.getElementById('serverPassword').value,
private_key: document.getElementById('serverKey').value,
ssl_domain: document.getElementById('serverSslDomain').value.trim(),
ssl_email: document.getElementById('serverSslEmail').value.trim(),
};
const result = await apiCall('/api/servers/add', 'POST', data);
@@ -292,6 +316,8 @@
document.getElementById('editServerUser').value = card.dataset.username || '';
document.getElementById('editServerPassword').value = '';
document.getElementById('editServerKey').value = '';
document.getElementById('editServerSslDomain').value = card.dataset.sslDomain || '';
document.getElementById('editServerSslEmail').value = card.dataset.sslEmail || '';
// Pre-select the tab matching the server's current auth method.
const authIsKey = card.dataset.auth === 'key';
@@ -321,6 +347,8 @@
// Empty -> null means "leave unchanged" on the backend.
password: document.getElementById('editServerPassword').value || null,
private_key: document.getElementById('editServerKey').value || null,
ssl_domain: document.getElementById('editServerSslDomain').value.trim(),
ssl_email: document.getElementById('editServerSslEmail').value.trim(),
};
await apiCall(`/api/servers/${idx}/edit`, 'POST', body);
showToast(_('server_saved'), 'success');
+2 -1
View File
@@ -196,7 +196,7 @@
const xuiConfigured = {{ 'true' if xui_configured else 'false' }};
const xuiDefaultInbound = {{ xui_default_inbound | int }};
const xuiDefaultPanelId = {{ (xui_default_panel_id or '') | tojson }};
const VPN_PROTO_ORDER = ['awg2', 'awg', 'awg_legacy', 'wireguard', 'xray', 'telemt'];
const VPN_PROTO_ORDER = ['awg2', 'awg', 'awg_legacy', 'wireguard', 'xray', 'telemt', 'hysteria'];
const PROTO_TITLES = {
awg2: 'AmneziaWG 2.0',
awg: 'AmneziaWG',
@@ -204,6 +204,7 @@
wireguard: 'WireGuard',
xray: 'Xray (VLESS-Reality)',
telemt: 'Telemt',
hysteria: 'Hysteria 2',
xui: '3x-ui VLESS',
};
+2 -2
View File
@@ -118,10 +118,10 @@
document.getElementById('configText').textContent = result.config;
document.getElementById('vpnLinkText').textContent = currentVpnLink;
// Telemt uses share links, not vpn:// Amnezia format
// Telemt / Hysteria use share links, not vpn:// Amnezia format
const vpnTab = document.querySelectorAll('.config-tab')[1];
const vpnPanel = document.getElementById('panel-vpn');
if (proto === 'telemt') {
if (proto === 'telemt' || String(proto || '').split('__')[0] === 'hysteria') {
vpnTab.style.display = 'none';
} else {
vpnTab.style.display = '';
+273 -8
View File
@@ -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')} &amp; ${_('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&nbsp;&nbsp;${domain}&nbsp;&nbsp;${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&nbsp;&nbsp;${domain}&nbsp;&nbsp;${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
+2 -1
View File
@@ -1321,7 +1321,7 @@
}
const guestServersData = {{ servers | default([]) | tojson }};
const VPN_PROTO_ORDER = ['awg2', 'awg', 'awg_legacy', 'wireguard', 'xray', 'telemt'];
const VPN_PROTO_ORDER = ['awg2', 'awg', 'awg_legacy', 'wireguard', 'xray', 'telemt', 'hysteria'];
const PROTO_TITLES = {
awg2: 'AmneziaWG 2.0',
awg: 'AmneziaWG',
@@ -1329,6 +1329,7 @@
wireguard: 'WireGuard',
xray: 'Xray (VLESS-Reality)',
telemt: 'Telemt',
hysteria: 'Hysteria 2',
xui: '3x-ui VLESS',
};
function protoTitle(key) {
+2 -1
View File
@@ -631,7 +631,7 @@
const select = document.getElementById(selectId);
const group = groupId ? document.getElementById(groupId) : null;
select.innerHTML = '';
const vpnOrder = ['awg2', 'awg', 'awg_legacy', 'wireguard', 'xray', 'telemt'];
const vpnOrder = ['awg2', 'awg', 'awg_legacy', 'wireguard', 'xray', 'telemt', 'hysteria'];
const titles = {
awg2: 'AmneziaWG 2.0',
awg: 'AmneziaWG',
@@ -639,6 +639,7 @@
wireguard: 'WireGuard',
xray: 'Xray (VLESS-Reality)',
telemt: 'Telemt',
hysteria: 'Hysteria 2',
xui: '3x-ui VLESS',
};
const protoTitle = (key) => {