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:
orohimaru2
2026-08-09 18:11:24 +03:00
co-authored by Cursor
parent 01196c066a
commit abbd160dd8
12 changed files with 587 additions and 52 deletions
+116 -26
View File
@@ -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 %}