Template
Ship AIVPN smart protocol picker and bump to v3.0.0.
Heuristic selection (stealth/balanced/speed) for invites, guest create, and user connections. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -197,7 +197,7 @@
|
||||
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', 'hysteria', 'naiveproxy', 'mieru'];
|
||||
const PROTO_TITLES = {
|
||||
const PROTO_TITLES = {
|
||||
awg2: 'AmneziaWG 2.0',
|
||||
awg: 'AmneziaWG',
|
||||
awg_legacy: 'AWG Legacy',
|
||||
@@ -207,6 +207,7 @@
|
||||
hysteria: 'Hysteria 2',
|
||||
naiveproxy: 'NaiveProxy',
|
||||
mieru: 'Mieru',
|
||||
aivpn: 'AIVPN (auto)',
|
||||
xui: '3x-ui VLESS',
|
||||
};
|
||||
|
||||
@@ -264,6 +265,12 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const aivpnOpt = document.createElement('option');
|
||||
aivpnOpt.value = 'aivpn';
|
||||
aivpnOpt.textContent = _('aivpn_protocol_option') || 'AIVPN (auto)';
|
||||
if (preferProtocol === 'aivpn') aivpnOpt.selected = true;
|
||||
sel.appendChild(aivpnOpt);
|
||||
|
||||
installed.forEach(key => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = key;
|
||||
|
||||
+116
-26
@@ -412,24 +412,44 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== AIVPN teaser (locked, full grid width) ===== -->
|
||||
<div class="promo-block promo-aivpn" aria-disabled="true" role="article">
|
||||
<!-- ===== AIVPN (smart protocol picker) ===== -->
|
||||
<div class="promo-block promo-aivpn" id="aivpnCard" role="article">
|
||||
<div class="promo-orbs" aria-hidden="true">
|
||||
<span class="promo-orb"></span>
|
||||
<span class="promo-orb"></span>
|
||||
<span class="promo-orb"></span>
|
||||
</div>
|
||||
<span class="promo-lock-badge">{{ icon('lock') }} {{ _('coming_soon') }}</span>
|
||||
<div class="promo-content">
|
||||
<span class="promo-lock-badge" id="aivpnBadge">{{ icon('brain') }} AIVPN</span>
|
||||
<div class="promo-content" style="flex-wrap: wrap;">
|
||||
<div class="promo-icon" aria-hidden="true">{{ icon('brain') }}</div>
|
||||
<div class="promo-text">
|
||||
<div class="promo-text" style="flex: 1; min-width: 200px;">
|
||||
<div class="promo-title">AIVPN</div>
|
||||
<div class="promo-subtitle">{{ _('aivpn_subtitle') }}</div>
|
||||
</div>
|
||||
<a class="promo-cta" href="https://github.com/PRVTPRO/Amnezia-Web-Panel" target="_blank" rel="noopener">
|
||||
<a class="promo-cta" href="{{ releases_repo_url }}" target="_blank" rel="noopener">
|
||||
⭐ <span>{{ _('promo_star_cta') }}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="aivpn-panel" style="position:relative; z-index:1; margin-top: var(--space-md); display:flex; flex-direction:column; gap: var(--space-sm);">
|
||||
<div style="display:flex; flex-wrap:wrap; gap: var(--space-sm); align-items:center;">
|
||||
<select class="form-input" id="aivpnStrategy" style="width:auto; min-width:140px; background:rgba(0,0,0,0.25); color:#fff; border-color:rgba(255,255,255,0.25);">
|
||||
<option value="balanced">{{ _('aivpn_strategy_balanced') }}</option>
|
||||
<option value="stealth">{{ _('aivpn_strategy_stealth') }}</option>
|
||||
<option value="speed">{{ _('aivpn_strategy_speed') }}</option>
|
||||
</select>
|
||||
<label style="display:flex; align-items:center; gap:6px; color:#fff; font-size:0.85rem;">
|
||||
<input type="checkbox" id="aivpnEnabled"> {{ _('aivpn_enable') }}
|
||||
</label>
|
||||
<label style="display:flex; align-items:center; gap:6px; color:#fff; font-size:0.85rem;">
|
||||
<input type="checkbox" id="aivpnProbe" checked> {{ _('aivpn_probe') }}
|
||||
</label>
|
||||
</div>
|
||||
<div style="display:flex; flex-wrap:wrap; gap: var(--space-sm);">
|
||||
<button type="button" class="btn btn-secondary btn-sm" onclick="saveAivpnSettings()" id="aivpnSaveBtn">{{ _('save') }}</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="runAivpnPick()" id="aivpnPickBtn">{{ _('aivpn_pick_now') }}</button>
|
||||
</div>
|
||||
<div class="text-sm" id="aivpnResult" style="color: rgba(255,255,255,0.9);"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DNS Card -->
|
||||
@@ -551,25 +571,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="promo-block promo-aivpn" aria-disabled="true" role="article">
|
||||
<div class="promo-orbs" aria-hidden="true">
|
||||
<span class="promo-orb"></span>
|
||||
<span class="promo-orb"></span>
|
||||
<span class="promo-orb"></span>
|
||||
</div>
|
||||
<span class="promo-lock-badge">{{ icon('lock') }} Coming soon</span>
|
||||
<div class="promo-content">
|
||||
<div class="promo-icon" aria-hidden="true">{{ icon('brain') }}</div>
|
||||
<div class="promo-text">
|
||||
<div class="promo-title">AIVPN</div>
|
||||
<div class="promo-subtitle">AI-driven protocol selection that picks the right tunnel for the moment. Land it sooner — drop a star.</div>
|
||||
</div>
|
||||
<a class="promo-cta" href="https://github.com/PRVTPRO/Amnezia-Web-Panel" target="_blank" rel="noopener">
|
||||
⭐ <span>Star us on GitHub</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Connections Section (renamed from Clients) -->
|
||||
<div class="clients-section" id="connectionsSection" style="display:none;">
|
||||
@@ -1227,6 +1228,7 @@
|
||||
const SERVER_CONNECT_DOMAIN = {{ ((server.server_info or {}).get('connect_domain') or '') | tojson }};
|
||||
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 }};
|
||||
let aivpnEnabled = {{ (((server.server_info or {}).get('aivpn') or {}).get('enabled') or false) | 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' },
|
||||
@@ -1647,6 +1649,7 @@
|
||||
case 'naiveproxy': title = 'NaiveProxy'; break;
|
||||
case 'mieru': title = 'Mieru'; break;
|
||||
case 'wireguard': title = 'WireGuard'; break;
|
||||
case 'aivpn': title = 'AIVPN'; break;
|
||||
case 'dns': title = 'AmneziaDNS'; break;
|
||||
case 'socks5': title = 'SOCKS5 Proxy'; break;
|
||||
case 'adguard': title = 'AdGuard Home'; break;
|
||||
@@ -3051,7 +3054,7 @@
|
||||
selectedConnectionIds.clear();
|
||||
updateMoveSelection();
|
||||
try {
|
||||
const data = await apiCall(`/api/servers/${SERVER_ID}/connections?protocol=${proto}`);
|
||||
const data = await apiCall(`/api/servers/${SERVER_ID}/connections?protocol=${encodeURIComponent(proto)}`);
|
||||
loading.style.display = 'none';
|
||||
if (!data.clients || data.clients.length === 0) {
|
||||
emptyEl.classList.remove('hidden');
|
||||
@@ -3399,9 +3402,96 @@
|
||||
|
||||
|
||||
|
||||
// ========== AIVPN ==========
|
||||
function formatAivpnRecommendation(rec) {
|
||||
if (!rec || !rec.protocol) {
|
||||
return _('aivpn_no_candidates');
|
||||
}
|
||||
const rtt = (rec.rtt_ms != null) ? ` · ${rec.rtt_ms} ms` : '';
|
||||
const alts = (rec.alternatives || []).slice(0, 3)
|
||||
.map(a => getProtoTitle(a.protocol))
|
||||
.filter(Boolean);
|
||||
const altTxt = alts.length ? ` · ${_('aivpn_alternatives')}: ${alts.join(', ')}` : '';
|
||||
return `${_('aivpn_picked')}: <strong>${getProtoTitle(rec.protocol)}</strong> (score ${Math.round(rec.score || 0)}${rtt})${altTxt}`;
|
||||
}
|
||||
|
||||
function applyAivpnUi(data) {
|
||||
const settings = (data && data.settings) || {};
|
||||
aivpnEnabled = !!settings.enabled;
|
||||
const strat = document.getElementById('aivpnStrategy');
|
||||
const en = document.getElementById('aivpnEnabled');
|
||||
const probe = document.getElementById('aivpnProbe');
|
||||
if (strat && settings.strategy) strat.value = settings.strategy;
|
||||
if (en) en.checked = aivpnEnabled;
|
||||
if (probe) probe.checked = settings.probe !== false;
|
||||
const badge = document.getElementById('aivpnBadge');
|
||||
if (badge) {
|
||||
badge.innerHTML = aivpnEnabled
|
||||
? `${uiIcon('brain')} AIVPN · ${_('aivpn_on')}`
|
||||
: `${uiIcon('brain')} AIVPN`;
|
||||
}
|
||||
const resultEl = document.getElementById('aivpnResult');
|
||||
if (resultEl) resultEl.innerHTML = formatAivpnRecommendation(data && data.recommendation);
|
||||
}
|
||||
|
||||
async function loadAivpnSettings(doProbe) {
|
||||
try {
|
||||
const q = doProbe ? '?probe=1' : '?probe=0';
|
||||
const data = await apiCall(`/api/servers/${SERVER_ID}/aivpn${q}`);
|
||||
applyAivpnUi(data);
|
||||
} catch (err) {
|
||||
console.warn('AIVPN load failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAivpnSettings() {
|
||||
const btn = document.getElementById('aivpnSaveBtn');
|
||||
if (btn) btn.disabled = true;
|
||||
try {
|
||||
const data = await apiCall(`/api/servers/${SERVER_ID}/aivpn`, 'POST', {
|
||||
enabled: document.getElementById('aivpnEnabled').checked,
|
||||
strategy: document.getElementById('aivpnStrategy').value,
|
||||
probe: document.getElementById('aivpnProbe').checked,
|
||||
});
|
||||
applyAivpnUi(data);
|
||||
showToast(_('settings_saved'), 'success');
|
||||
} catch (err) {
|
||||
showToast(_('error') + ': ' + err.message, 'error');
|
||||
} finally {
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function runAivpnPick() {
|
||||
const btn = document.getElementById('aivpnPickBtn');
|
||||
if (btn) btn.disabled = true;
|
||||
const resultEl = document.getElementById('aivpnResult');
|
||||
if (resultEl) resultEl.textContent = _('aivpn_picking');
|
||||
try {
|
||||
const data = await apiCall(`/api/servers/${SERVER_ID}/aivpn?probe=1`);
|
||||
applyAivpnUi(data);
|
||||
const picked = data.recommendation && data.recommendation.protocol;
|
||||
if (!picked) {
|
||||
showToast(_('aivpn_no_candidates'), 'warning');
|
||||
} else {
|
||||
const sel = document.getElementById('connProtoSelect');
|
||||
if (sel && [...sel.options].some(o => o.value === picked)) {
|
||||
sel.value = picked;
|
||||
loadConnections();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (resultEl) resultEl.textContent = '';
|
||||
showToast(_('error') + ': ' + err.message, 'error');
|
||||
} finally {
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Init ==========
|
||||
applyInstalledAppsVisibility();
|
||||
checkServer();
|
||||
loadServerStats();
|
||||
loadAivpnSettings(false);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1560,6 +1560,7 @@
|
||||
hysteria: 'Hysteria 2',
|
||||
naiveproxy: 'NaiveProxy',
|
||||
mieru: 'Mieru',
|
||||
aivpn: 'AIVPN (auto)',
|
||||
xui: '3x-ui VLESS',
|
||||
};
|
||||
function protoTitle(key) {
|
||||
@@ -1602,6 +1603,12 @@
|
||||
opt.textContent = _('no_protocols');
|
||||
protoSel.appendChild(opt);
|
||||
} else {
|
||||
const aivpnOpt = document.createElement('option');
|
||||
aivpnOpt.value = 'aivpn';
|
||||
aivpnOpt.textContent = _('aivpn_protocol_option') || 'AIVPN (auto)';
|
||||
if (prefer === 'aivpn') aivpnOpt.selected = true;
|
||||
protoSel.appendChild(aivpnOpt);
|
||||
|
||||
installed.forEach(key => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = key;
|
||||
|
||||
@@ -685,6 +685,11 @@
|
||||
});
|
||||
|
||||
if (count > 0) {
|
||||
const aivpnOpt = document.createElement('option');
|
||||
aivpnOpt.value = 'aivpn';
|
||||
aivpnOpt.textContent = _('aivpn_protocol_option') || 'AIVPN (auto)';
|
||||
select.insertBefore(aivpnOpt, select.firstChild);
|
||||
count++;
|
||||
if (group) group.style.display = '';
|
||||
} else {
|
||||
const opt = document.createElement('option');
|
||||
@@ -766,6 +771,13 @@
|
||||
});
|
||||
|
||||
document.getElementById('ucProtocol').addEventListener('change', (e) => {
|
||||
if (e.target.value === 'aivpn') {
|
||||
const mode = document.getElementById('ucMode');
|
||||
if (mode && mode.value === 'existing') {
|
||||
mode.value = 'new';
|
||||
toggleUCMode();
|
||||
}
|
||||
}
|
||||
if (document.getElementById('ucMode').value === 'existing') {
|
||||
fetchExistingClients();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user