Replace emoji UI icons with a shared SVG icon system.

Add an icons sprite and helpers so nav, actions, and protocol cards use consistent stroke icons.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
orohi
2026-07-26 04:58:06 +03:00
co-authored by Cursor
parent 30ae4d476c
commit 16d14a7256
12 changed files with 398 additions and 149 deletions
+55 -54
View File
@@ -1,4 +1,5 @@
{% extends "base.html" %}
{% from "macros/icons.html" import icon %}
{% block title_extra %} — {{ server.name }}{% endblock %}
@@ -125,7 +126,7 @@
<div class="card" style="margin-bottom: var(--space-xl);">
<div class="flex items-center justify-between" style="flex-wrap: wrap; gap: var(--space-md);">
<div class="flex items-center gap-md">
<div class="server-icon" style="width:56px; height:56px; font-size:1.6rem;">🖥</div>
<div class="server-icon" style="width:56px; height:56px; font-size:1.6rem;">{{ icon('server') }}</div>
<div>
<h1 style="font-size:1.4rem; font-weight:700;">{{ server.name }}</h1>
<div class="font-mono text-muted text-sm">{{ server.host }}:{{ server.ssh_port }} • {{ server.username
@@ -146,10 +147,10 @@
<div class="flex gap-md" style="flex-wrap:wrap" id="statusBadges"></div>
<div class="server-status-actions">
<button class="btn btn-primary btn-sm" onclick="openMarketplaceModal()" id="marketplaceBtn">
<span>🛒</span> {{ _('marketplace') }}
<span>{{ icon('cart') }}</span> {{ _('marketplace') }}
</button>
<button class="btn btn-secondary btn-sm" onclick="openManagementModal()" id="managementBtn">
<span>⚙️</span> {{ _('management') }}
<span>{{ icon('settings') }}</span> {{ _('management') }}
</button>
</div>
</div>
@@ -163,7 +164,7 @@
<span>{{ _('checking_server') }}</span>
</div>
<div class="empty-state hidden" id="installedAppsEmpty" style="padding: var(--space-xl) 0; margin-bottom: var(--space-xl);">
<div class="empty-icon">🛒</div>
<div class="empty-icon">{{ icon('cart') }}</div>
<div class="empty-title">{{ _('no_installed_apps') }}</div>
<div class="empty-desc">{{ _('no_installed_apps_desc') }}</div>
<button class="btn btn-primary btn-sm" onclick="openMarketplaceModal()" style="margin-top: var(--space-md);">
@@ -175,7 +176,7 @@
<!-- AWG2 Card (first - new version) -->
<div class="card card-hover protocol-card protocol-awg" id="proto-awg2">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
<div class="protocol-icon"></div>
<div class="protocol-icon">{{ icon('sparkles') }}</div>
<div class="flex gap-sm" id="awg2-ctrl" style="display:none!important;"></div>
</div>
<div class="protocol-name">AmneziaWG 2.0 <span
@@ -201,7 +202,7 @@
<!-- AWG Card -->
<div class="card card-hover protocol-card protocol-awg" id="proto-awg">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
<div class="protocol-icon">🔮</div>
<div class="protocol-icon">{{ icon('shield') }}</div>
<div class="flex gap-sm" id="awg-ctrl" style="display:none!important;"></div>
</div>
<div class="protocol-name">AmneziaWG</div>
@@ -225,7 +226,7 @@
<!-- AWG Legacy Card -->
<div class="card card-hover protocol-card protocol-legacy" id="proto-awg-legacy">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
<div class="protocol-icon">📡</div>
<div class="protocol-icon">{{ icon('radio') }}</div>
<div class="flex gap-sm" id="awg-legacy-ctrl" style="display:none!important;"></div>
</div>
<div class="protocol-name">AmneziaWG Legacy</div>
@@ -249,7 +250,7 @@
<!-- Xray Card -->
<div class="card card-hover protocol-card protocol-xray" id="proto-xray">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
<div class="protocol-icon"></div>
<div class="protocol-icon">{{ icon('zap') }}</div>
<div class="flex gap-sm" id="xray-ctrl" style="display:none!important;"></div>
</div>
<div class="protocol-name">Xray (VLESS-Reality)</div>
@@ -273,7 +274,7 @@
<!-- Telemt Card -->
<div class="card card-hover protocol-card protocol-telemt" id="proto-telemt">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
<div class="protocol-icon"></div>
<div class="protocol-icon">{{ icon('plane') }}</div>
<div class="flex gap-sm" id="telemt-ctrl" style="display:none!important;"></div>
</div>
<div class="protocol-name">Telemt (Telegram Proxy)</div>
@@ -297,7 +298,7 @@
<!-- 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);">
<div class="protocol-icon">🔒</div>
<div class="protocol-icon">{{ icon('lock') }}</div>
<div class="flex gap-sm" id="wireguard-ctrl" style="display:none!important;"></div>
</div>
<div class="protocol-name">WireGuard</div>
@@ -325,9 +326,9 @@
<span class="promo-orb"></span>
<span class="promo-orb"></span>
</div>
<span class="promo-lock-badge">🔒 {{ _('coming_soon') }}</span>
<span class="promo-lock-badge">{{ icon('lock') }} {{ _('coming_soon') }}</span>
<div class="promo-content">
<div class="promo-icon" aria-hidden="true">🧠</div>
<div class="promo-icon" aria-hidden="true">{{ icon('brain') }}</div>
<div class="promo-text">
<div class="promo-title">AIVPN</div>
<div class="promo-subtitle">{{ _('aivpn_subtitle') }}</div>
@@ -341,7 +342,7 @@
<!-- DNS Card -->
<div class="card card-hover protocol-card protocol-dns" id="proto-dns">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
<div class="protocol-icon">🔍</div>
<div class="protocol-icon">{{ icon('search') }}</div>
<div class="flex gap-sm" id="dns-ctrl" style="display:none!important;"></div>
</div>
<div class="protocol-name">AmneziaDNS</div>
@@ -365,7 +366,7 @@
<!-- AdGuard Home Card -->
<div class="card card-hover protocol-card protocol-adguard" id="proto-adguard">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
<div class="protocol-icon">🛡️</div>
<div class="protocol-icon">{{ icon('shield-check') }}</div>
<div class="flex gap-sm" id="adguard-ctrl" style="display:none!important;"></div>
</div>
<div class="protocol-name">AdGuard Home</div>
@@ -389,7 +390,7 @@
<!-- SOCKS5 Card -->
<div class="card card-hover protocol-card protocol-socks5" id="proto-socks5">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
<div class="protocol-icon">🧦</div>
<div class="protocol-icon">{{ icon('network') }}</div>
<div class="flex gap-sm" id="socks5-ctrl" style="display:none!important;"></div>
</div>
<div class="protocol-name">SOCKS5 Proxy</div>
@@ -414,7 +415,7 @@
<!-- NGINX Card -->
<div class="card card-hover protocol-card protocol-nginx" id="proto-nginx">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
<div class="protocol-icon">🌐</div>
<div class="protocol-icon">{{ icon('globe') }}</div>
<div class="flex gap-sm" id="nginx-ctrl" style="display:none!important;"></div>
</div>
<div class="protocol-name">NGINX</div>
@@ -443,9 +444,9 @@
<span class="promo-orb"></span>
<span class="promo-orb"></span>
</div>
<span class="promo-lock-badge">🔒 {{ _('coming_soon') }}</span>
<span class="promo-lock-badge">{{ icon('lock') }} {{ _('coming_soon') }}</span>
<div class="promo-content">
<div class="promo-icon" aria-hidden="true">🛡️</div>
<div class="promo-icon" aria-hidden="true">{{ icon('shield-check') }}</div>
<div class="promo-text">
<div class="promo-title">{{ _('revproxy_title') }}</div>
<div class="promo-subtitle">{{ _('revproxy_subtitle') }}</div>
@@ -463,9 +464,9 @@
<span class="promo-orb"></span>
<span class="promo-orb"></span>
</div>
<span class="promo-lock-badge">🔒 Coming soon</span>
<span class="promo-lock-badge">{{ icon('lock') }} Coming soon</span>
<div class="promo-content">
<div class="promo-icon" aria-hidden="true">🧠</div>
<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>
@@ -480,7 +481,7 @@
<div class="clients-section" id="connectionsSection" style="display:none;">
<div class="clients-header">
<h2 class="section-title" style="margin-bottom:0;">
<span class="icon">🔗</span>
<span class="icon">{{ icon('link') }}</span>
{{ _('connections') }}
</h2>
<div class="flex gap-sm items-center">
@@ -494,7 +495,7 @@
<option value="wireguard">WireGuard</option>
</select>
<button class="btn btn-primary btn-sm" onclick="openAddConnectionModal()">
<span></span> {{ _('add') }}
<span>{{ icon('plus') }}</span> {{ _('add') }}
</button>
</div>
</div>
@@ -505,7 +506,7 @@
<span>{{ _('loading_connections') }}</span>
</div>
<div id="connectionsEmpty" class="empty-state hidden" style="padding: var(--space-xl) 0;">
<div class="empty-icon">🔗</div>
<div class="empty-icon">{{ icon('link') }}</div>
<div class="empty-title">{{ _('no_connections') }}</div>
<div class="empty-desc">{{ _('no_connections_desc') }}</div>
</div>
@@ -518,7 +519,7 @@
<div class="modal" style="max-width: 980px;">
<div class="modal-header">
<div>
<h2 class="modal-title">🛒 {{ _('marketplace') }}</h2>
<h2 class="modal-title">{{ icon('cart') }} {{ _('marketplace') }}</h2>
<div class="text-muted text-sm">{{ _('marketplace_desc') }}</div>
</div>
<button class="modal-close" onclick="closeModal('marketplaceModal')">×</button>
@@ -532,7 +533,7 @@
<div class="modal" style="max-width: 520px;">
<div class="modal-header">
<div>
<h2 class="modal-title">⚙️ {{ _('server_management') }}</h2>
<h2 class="modal-title">{{ icon('settings') }} {{ _('server_management') }}</h2>
<div class="text-muted text-sm">{{ _('server_management_desc') }}</div>
</div>
<button class="modal-close" onclick="closeModal('managementModal')">×</button>
@@ -758,7 +759,7 @@
</div>
<div id="backupLoading" class="form-hint hidden">{{ _('loading') }}</div>
<div id="backupEmpty" class="empty-state hidden" style="padding:var(--space-xl);">
<div class="empty-icon">📦</div>
<div class="empty-icon">{{ icon('package') }}</div>
<div class="empty-title">{{ _('no_backups') }}</div>
<div class="empty-desc">{{ _('no_backups_desc') }}</div>
</div>
@@ -952,21 +953,21 @@
const SERVER_ID = {{ server_id }};
const SERVER_HOST = "{{ server.host }}";
const MARKETPLACE_APPS = [
{ proto: 'awg2', category: 'protocols', icon: '', title: 'AmneziaWG 2.0', descKey: 'awg_desc', badge: 'NEW' },
{ proto: 'awg', category: 'protocols', icon: '🔮', title: 'AmneziaWG', descKey: 'awg_desc' },
{ proto: 'awg_legacy', category: 'protocols', icon: '📡', title: 'AmneziaWG Legacy', descKey: 'awg_legacy_desc' },
{ proto: 'xray', category: 'protocols', icon: '', title: 'Xray (VLESS-Reality)', descKey: 'xray_desc' },
{ proto: 'telemt', category: 'protocols', icon: '', title: 'Telemt (Telegram Proxy)', descKey: 'telemt_desc' },
{ proto: 'wireguard', category: 'protocols', icon: '🔒', title: 'WireGuard', descKey: 'wireguard_desc' },
{ proto: 'dns', category: 'services', icon: '🔍', title: 'AmneziaDNS', descKey: 'dns_desc' },
{ proto: 'adguard', category: 'services', icon: '🛡️', title: 'AdGuard Home', descKey: 'adguard_desc' },
{ proto: 'socks5', category: 'services', icon: '🧦', title: 'SOCKS5 Proxy', descKey: 'socks5_desc' },
{ proto: 'nginx', category: 'web_servers', icon: '🌐', title: 'NGINX', descKey: 'nginx_desc' },
{ 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: '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' },
{ proto: 'socks5', category: 'services', icon: 'network', title: 'SOCKS5 Proxy', descKey: 'socks5_desc' },
{ proto: 'nginx', category: 'web_servers', icon: 'globe', title: 'NGINX', descKey: 'nginx_desc' },
];
const MARKETPLACE_CATEGORIES = [
{ id: 'protocols', icon: '🔐', titleKey: 'protocols' },
{ id: 'services', icon: '🛠', titleKey: 'services' },
{ id: 'web_servers', icon: '🌐', titleKey: 'web_servers' },
{ id: 'protocols', icon: 'lock', titleKey: 'protocols' },
{ id: 'services', icon: 'wrench', titleKey: 'services' },
{ id: 'web_servers', icon: 'globe', titleKey: 'web_servers' },
];
let currentInstallProto = 'awg';
let currentInstallAnother = false;
@@ -1041,16 +1042,16 @@
el.innerHTML = `
<div class="flex" style="flex-direction: column; gap: var(--space-sm);">
<button class="btn btn-secondary" onclick="closeModal('managementModal'); checkServer();" style="justify-content: flex-start; padding: 12px 20px;">
<span style="font-size: 1.2rem; margin-right: 8px;">🔄</span> ${_('check_server_services')}
<span style="font-size: 1.2rem; margin-right: 8px; display:inline-flex;">${uiIcon('refresh')}</span> ${_('check_server_services')}
</button>
<button class="btn btn-warning" onclick="closeModal('managementModal'); rebootServer();" style="justify-content: flex-start; padding: 12px 20px;">
<span style="font-size: 1.2rem; margin-right: 8px;">🔁</span> ${_('reboot_server')}
<span style="font-size: 1.2rem; margin-right: 8px; display:inline-flex;">${uiIcon('refresh')}</span> ${_('reboot_server')}
</button>
<button class="btn btn-danger" onclick="closeModal('managementModal'); clearServer();" style="justify-content: flex-start; padding: 12px 20px;">
<span style="font-size: 1.2rem; margin-right: 8px;">🧹</span> ${_('clear_server')}
<span style="font-size: 1.2rem; margin-right: 8px; display:inline-flex;">${uiIcon('trash')}</span> ${_('clear_server')}
</button>
<button class="btn btn-danger" onclick="closeModal('managementModal'); deleteServer();" style="justify-content: flex-start; padding: 12px 20px;">
<span style="font-size: 1.2rem; margin-right: 8px;">🗑️</span> ${_('remove_server')}
<span style="font-size: 1.2rem; margin-right: 8px; display:inline-flex;">${uiIcon('trash')}</span> ${_('remove_server')}
</button>
</div>
`;
@@ -1080,7 +1081,7 @@
const apps = MARKETPLACE_APPS.filter(app => app.category === cat.id);
return `
<div class="marketplace-category">
<div class="marketplace-category-title"><span>${cat.icon}</span> ${_(cat.titleKey)}</div>
<div class="marketplace-category-title"><span>${uiIcon(cat.icon)}</span> ${_(cat.titleKey)}</div>
<div class="marketplace-grid">
${apps.map(app => {
const installed = baseInstalled(app.proto);
@@ -1089,7 +1090,7 @@
<div class="marketplace-app-card">
<div class="marketplace-app-top">
<div class="flex items-center gap-sm">
<div class="protocol-icon" style="width:38px;height:38px;font-size:1.2rem;">${app.icon}</div>
<div class="protocol-icon" style="width:38px;height:38px;font-size:1.2rem;">${uiIcon(app.icon)}</div>
<div>
<div class="marketplace-app-title">${app.title} ${app.badge ? `<span class="badge badge-info" style="font-size:0.65rem;">${app.badge}</span>` : ''}</div>
${marketplaceAppStatus(app.proto)}
@@ -1214,10 +1215,10 @@
const el = document.getElementById('serverStats');
el.style.display = '';
el.innerHTML = `
<span class="stat-pill" title="CPU"> ${stats.cpu || 0}%</span>
<span class="stat-pill" title="RAM: ${formatBytes(stats.ram_used)} / ${formatBytes(stats.ram_total)}">🧠 ${stats.ram_percent || 0}%</span>
<span class="stat-pill" title="Disk: ${formatBytes(stats.disk_used)} / ${formatBytes(stats.disk_total)}">💾 ${stats.disk_percent || 0}%</span>
<span class="stat-pill" title="Network">🌐${formatBytes(stats.net_rx)}${formatBytes(stats.net_tx)}</span>
<span class="stat-pill" title="CPU">${uiIcon('zap')} ${stats.cpu || 0}%</span>
<span class="stat-pill" title="RAM: ${formatBytes(stats.ram_used)} / ${formatBytes(stats.ram_total)}">${uiIcon('brain')} ${stats.ram_percent || 0}%</span>
<span class="stat-pill" title="Disk: ${formatBytes(stats.disk_used)} / ${formatBytes(stats.disk_total)}">${uiIcon('package')} ${stats.disk_percent || 0}%</span>
<span class="stat-pill" title="Network">${uiIcon('globe')}${formatBytes(stats.net_rx)}${formatBytes(stats.net_tx)}</span>
`;
} catch (err) {
console.warn('Stats load failed:', err);
@@ -2220,11 +2221,11 @@
if (!enabled) metaHtml += `<span class="badge badge-danger" style="font-size:0.65rem">${_('stop')}</span>`;
const disabledStyle = enabled ? '' : 'opacity:0.5;';
const toggleIcon = enabled ? '🔵' : '⚫';
const toggleIcon = enabled ? uiIcon('pause') : uiIcon('play');
const toggleTitle = enabled ? _('stop') : _('starting');
// Show config button for all WireGuard-based clients.
// If clientPrivateKey is missing (created via native app), we show a warning instead.
const canShowConfig = proto === 'xray' || true; // always show 📄 button
const canShowConfig = proto === 'xray' || true; // always show config button
listEl.innerHTML += `
<div class="client-item" style="${disabledStyle}">
@@ -2236,10 +2237,10 @@
</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')}">📄</button>
${proto === 'telemt' ? `<button class="btn btn-secondary btn-sm btn-icon" onclick="editConnection('${escapeJs(client.clientId)}')" title="${_('edit')}">✏️</button>` : ''}
<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>
${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')}">🗑</button>
<button class="btn btn-danger btn-sm btn-icon" onclick="removeConnection('${escapeJs(client.clientId)}')" title="${_('delete')}">${uiIcon('trash')}</button>
</div>
</div>
`;