Release v2.6.3: support page with SBP/card/crypto donate placeholders and admin settings.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
orohi
2026-07-28 13:32:09 +03:00
co-authored by Cursor
parent 73d0ee277d
commit 32a0e90e19
10 changed files with 244 additions and 2 deletions
+6
View File
@@ -277,6 +277,12 @@ GitHub Actions workflows in `.github/workflows/`:
## 📋 Fix / changelog (this fork)
### v2.6.3
* **Client connection domain (AWG / WireGuard)** — set a DNS name per server (or per AWG/WG protocol) instead of IP in client configs (`Endpoint = domain:port`). Survives server IP changes — update the A-record only. UI on server list, server page, and when adding a server.
* **Legacy `data.json` → PostgreSQL** — import normalizes old backups (UUIDs, duplicate usernames/tokens, string `server_info`); failed auto-import no longer bricks panel startup; JSON restore returns clear errors.
* **Dokploy / Traefik 404** — `traefik.http.services.amnezia_panel.loadbalancer.server.port=5000` in compose (Traefik must route to container port **5000**).
* **Support the project** — `/support` page for all users (SBP / card / crypto placeholders); admin configures requisites in **Settings** (no popups).
### v2.6.2
* **Panel backup → PostgreSQL dump** — Settings backup downloads `.sql` via `pg_dump`; restore accepts `.sql` or legacy `data.json`. JSON export kept as secondary link.
+31 -1
View File
@@ -103,7 +103,7 @@ else:
application_path = os.path.dirname(__file__)
DATA_FILE = os.path.join(application_path, 'data.json') # legacy JSON; used only for one-shot import / export
CURRENT_VERSION = "v2.6.5"
CURRENT_VERSION = "v2.6.3"
RELEASES_REPO_URL = repo_url()
RELEASES_API_LATEST = api_latest_url()
BIN_DIR = os.environ.get('TUNNEL_BIN_DIR', os.path.join(application_path, 'bin'))
@@ -241,12 +241,14 @@ def get_current_user(request: Request, data: Optional[dict] = None):
def tpl(request, template, **kwargs):
data = load_data()
lang = request.cookies.get('lang', 'en')
donate_cfg = (data.get('settings') or {}).get('donate') or {}
ctx = {
'request': request,
'current_user': get_current_user(request, data),
'site_settings': data.get('settings', {}).get('appearance', {}),
'captcha_settings': data.get('settings', {}).get('captcha', {}),
'telegram_settings': data.get('settings', {}).get('telegram', {}),
'donate_enabled': bool(donate_cfg.get('enabled', True)),
'bot_running': tg_bot.is_running(),
'lang': lang,
'_': lambda text_id: _t(text_id, lang),
@@ -2359,6 +2361,20 @@ class GuestSettings(BaseModel):
create_xui_panel_id: str = ''
class DonateMethodSettings(BaseModel):
enabled: bool = True
title: str = ''
details: str = ''
class DonateSettings(BaseModel):
enabled: bool = True
intro: str = ''
sbp: DonateMethodSettings = DonateMethodSettings()
card: DonateMethodSettings = DonateMethodSettings()
crypto: DonateMethodSettings = DonateMethodSettings()
class UpdateUserRequest(BaseModel):
telegramId: Optional[str] = None
email: Optional[str] = None
@@ -2379,6 +2395,7 @@ class SaveSettingsRequest(BaseModel):
telegram: TelegramSettings
ssl: SSLSettings
guest: GuestSettings = GuestSettings()
donate: DonateSettings = DonateSettings()
class ToggleUserRequest(BaseModel):
@@ -6072,6 +6089,18 @@ async def api_my_connection_config(request: Request, connection_id: str):
return JSONResponse({'error': str(e)}, status_code=500)
@app.get('/support', response_class=HTMLResponse, tags=["System Templates"])
async def support_page(request: Request):
user = get_current_user(request)
if not user:
return RedirectResponse('/login', status_code=302)
data = load_data()
donate = dict((data.get('settings') or {}).get('donate') or {})
if not donate.get('enabled', True):
return RedirectResponse('/my', status_code=302)
return tpl(request, 'support.html', donate=donate)
@app.get('/settings', response_class=HTMLResponse, tags=["System Templates"])
async def settings_page(request: Request):
user = _check_admin(request)
@@ -6381,6 +6410,7 @@ async def save_settings(request: Request, payload: SaveSettingsRequest):
else:
guest['password_hash'] = existing_guest.get('password_hash')
data['settings']['guest'] = guest
data['settings']['donate'] = payload.donate.dict()
save_data(data)
logger.info("Settings saved (including captcha and telegram)")
+7
View File
@@ -54,6 +54,13 @@ DEFAULT_SETTINGS = {
'create_protocol': 'awg',
'create_server_id': 0,
},
'donate': {
'enabled': True,
'intro': '',
'sbp': {'enabled': True, 'title': '', 'details': ''},
'card': {'enabled': True, 'title': '', 'details': ''},
'crypto': {'enabled': True, 'title': '', 'details': ''},
},
}
+9
View File
@@ -2207,6 +2207,15 @@ a:hover {
font-weight: 600;
}
.nav-link-support {
opacity: 0.85;
}
.nav-link-support:hover {
opacity: 1;
color: #f472b6;
}
.nav-user {
display: flex;
align-items: center;
+3
View File
@@ -108,4 +108,7 @@
<symbol id="layers" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/>
</symbol>
<symbol id="heart" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.51 4.04 3 5.5l7 7Z"/>
</symbol>
</svg>
+3
View File
@@ -52,6 +52,9 @@
<a href="/settings" class="nav-link">{{ icon('settings') }} {{ _('nav_settings') }}</a>
{% endif %}
<a href="/my" class="nav-link">{{ icon('layers') }} {{ _('nav_connections') }}</a>
{% if donate_enabled %}
<a href="/support" class="nav-link nav-link-support">{{ icon('heart') }} {{ _('nav_support') }}</a>
{% endif %}
</nav>
<div class="nav-user">
+49 -1
View File
@@ -650,6 +650,41 @@
</div>
</div>
<!-- BLOCK: Support / Donate (admin) -->
{% set donate_cfg = settings.get('donate') or {} %}
<div class="card" style="margin-top: var(--space-lg);">
<h3 class="card-title" style="margin-bottom: var(--space-lg);">{{ icon('heart') }} {{ _('donate_settings_title') }}</h3>
<p class="form-hint" style="margin-top: 0; margin-bottom: var(--space-md);">{{ _('donate_settings_hint') }}</p>
<div class="form-group">
<label class="form-label" style="display:flex; align-items:center; gap:8px; cursor:pointer;">
<input type="checkbox" id="donate_enabled" {% if donate_cfg.get('enabled', True) %}checked{% endif %}>
{{ _('donate_enabled') }}
</label>
</div>
<div class="form-group">
<label class="form-label">{{ _('donate_intro') }}</label>
<textarea class="form-textarea" id="donate_intro" rows="2" placeholder="{{ _('support_intro') }}">{{ donate_cfg.get('intro') or '' }}</textarea>
</div>
{% for key, label_key in [('sbp', 'donate_sbp'), ('card', 'donate_card'), ('crypto', 'donate_crypto')] %}
{% set method = donate_cfg.get(key) or {} %}
<div style="border:1px solid var(--border-color); border-radius:var(--radius-md); padding:var(--space-md); margin-bottom:var(--space-md);">
<label class="form-label" style="display:flex; align-items:center; gap:8px; cursor:pointer; margin-bottom:var(--space-sm);">
<input type="checkbox" id="donate_{{ key }}_enabled" {% if method.get('enabled', True) %}checked{% endif %}>
{{ _(label_key) }}
</label>
<div class="form-group" style="margin-bottom:var(--space-sm);">
<label class="form-label">{{ _('donate_method_title') }}</label>
<input type="text" class="form-input" id="donate_{{ key }}_title" value="{{ method.get('title') or '' }}" placeholder="{{ _(label_key) }}">
</div>
<div class="form-group" style="margin-bottom:0;">
<label class="form-label">{{ _('donate_method_details') }}</label>
<textarea class="form-textarea" id="donate_{{ key }}_details" rows="3" placeholder="{{ _('donate_details_placeholder') }}">{{ method.get('details') or '' }}</textarea>
</div>
</div>
{% endfor %}
<p class="form-hint">{{ _('donate_preview_hint') }} <a href="/support" target="_blank" rel="noopener">/support</a></p>
</div>
<!-- BLOCK: About & Updates -->
<div class="card" style="margin-top: var(--space-lg);">
<h3 class="card-title" style="margin-bottom: var(--space-lg);">️ {{ _('about_title') }}</h3>
@@ -1478,11 +1513,24 @@
create_xui_panel_id: document.getElementById('guest_create_xui_panel')?.value || '',
};
const donateMethod = (key) => ({
enabled: document.getElementById(`donate_${key}_enabled`).checked,
title: document.getElementById(`donate_${key}_title`).value.trim(),
details: document.getElementById(`donate_${key}_details`).value.trim(),
});
const donate = {
enabled: document.getElementById('donate_enabled').checked,
intro: document.getElementById('donate_intro').value.trim(),
sbp: donateMethod('sbp'),
card: donateMethod('card'),
crypto: donateMethod('crypto'),
};
try {
const res = await fetch('/api/settings/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ appearance, sync, captcha, telegram, ssl, guest })
body: JSON.stringify({ appearance, sync, captcha, telegram, ssl, guest, donate })
});
if (!res.ok) throw new Error(_('error'));
showToast(_('settings_saved'), 'success');
+104
View File
@@ -0,0 +1,104 @@
{% extends "base.html" %}
{% from "macros/icons.html" import icon %}
{% block title_extra %} — {{ _('nav_support') }}{% endblock %}
{% block head_extra %}
<style>
.support-hero {
text-align: center;
padding: var(--space-xl) var(--space-md);
margin-bottom: var(--space-xl);
}
.support-hero-icon {
font-size: 2.5rem;
margin-bottom: var(--space-md);
color: var(--accent);
}
.support-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: var(--space-lg);
max-width: 960px;
margin: 0 auto;
}
.support-card {
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: var(--space-lg);
background: var(--bg-secondary);
display: flex;
flex-direction: column;
gap: var(--space-md);
}
.support-card-title {
font-weight: 700;
font-size: 1.05rem;
display: flex;
align-items: center;
gap: var(--space-sm);
}
.support-details {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.85rem;
white-space: pre-wrap;
word-break: break-word;
padding: var(--space-sm);
border-radius: var(--radius-md);
background: var(--bg-primary);
border: 1px dashed var(--border-color);
min-height: 4rem;
color: var(--text-muted);
}
.support-details.has-content {
color: var(--text-primary);
border-style: solid;
}
</style>
{% endblock %}
{% block content %}
<section class="support-hero">
<div class="support-hero-icon">{{ icon('heart') }}</div>
<h1 class="section-title" style="justify-content: center; margin-bottom: var(--space-sm);">
{{ _('support_title') }}
</h1>
<p class="text-muted" style="max-width: 560px; margin: 0 auto; line-height: 1.5;">
{% if donate.get('intro') %}
{{ donate.intro }}
{% else %}
{{ _('support_intro') }}
{% endif %}
</p>
</section>
<div class="support-grid">
{% set methods = [
('sbp', _('donate_sbp'), '💳'),
('card', _('donate_card'), '🏦'),
('crypto', _('donate_crypto'), '₿'),
] %}
{% for key, default_title, emoji in methods %}
{% set method = donate.get(key) or {} %}
{% if method.get('enabled', True) %}
<div class="card support-card">
<div class="support-card-title">
<span aria-hidden="true">{{ emoji }}</span>
<span>{{ method.get('title') or default_title }}</span>
</div>
{% set details = (method.get('details') or '').strip() %}
<div class="support-details{% if details %} has-content{% endif %}" id="donate-{{ key }}-details">{{ details or _('donate_details_soon') }}</div>
{% if details %}
<button type="button" class="btn btn-secondary btn-sm" onclick="copyToClipboard(document.getElementById('donate-{{ key }}-details').textContent)">
{{ icon('copy') }} {{ _('copy') }}
</button>
{% endif %}
</div>
{% endif %}
{% endfor %}
</div>
<p class="text-muted text-sm" style="text-align: center; margin-top: var(--space-xl); max-width: 520px; margin-left: auto; margin-right: auto;">
{{ _('support_footer') }}
</p>
{% endblock %}
+16
View File
@@ -4,6 +4,7 @@
"nav_invites": "Invite links",
"nav_settings": "Settings",
"nav_connections": "Connections",
"nav_support": "Support",
"nav_logout": "Logout",
"theme_dark": "Dark",
"theme_light": "Light",
@@ -405,6 +406,21 @@
"server_connect_domain_ssh": "SSH",
"server_connect_domain_saved": "Connection domain saved",
"server_endpoint_label": "Endpoint",
"support_title": "Support the project",
"support_intro": "If this panel helps you, you can support development. Choose a convenient method below.",
"support_footer": "Thank you for using Amnezia Web Panel.",
"donate_sbp": "SBP (Fast Payments)",
"donate_card": "Bank card",
"donate_crypto": "Cryptocurrency",
"donate_details_soon": "Payment details will be added soon.",
"donate_settings_title": "Support / donations",
"donate_settings_hint": "Shown on the /support page for all users. No popups — only when they open the page.",
"donate_enabled": "Show support page in navigation",
"donate_intro": "Page intro text (optional)",
"donate_method_title": "Button title (optional)",
"donate_method_details": "Payment details",
"donate_details_placeholder": "Phone, card number, wallet address…",
"donate_preview_hint": "Preview:",
"container_logs": "Container logs",
"logs_btn": "📋 Logs",
"logs_live": "Live",
+16
View File
@@ -4,6 +4,7 @@
"nav_invites": "Ссылки",
"nav_settings": "Настройки",
"nav_connections": "Подключения",
"nav_support": "Поддержать",
"nav_logout": "Выход",
"theme_dark": "Темная",
"theme_light": "Светлая",
@@ -405,6 +406,21 @@
"server_connect_domain_ssh": "SSH",
"server_connect_domain_saved": "Домен подключения сохранён",
"server_endpoint_label": "Endpoint",
"support_title": "Поддержать проект",
"support_intro": "Если панель вам помогает, можно поддержать разработку. Выберите удобный способ ниже.",
"support_footer": "Спасибо, что пользуетесь Amnezia Web Panel.",
"donate_sbp": "СБП",
"donate_card": "Банковская карта",
"donate_crypto": "Криптовалюта",
"donate_details_soon": "Реквизиты будут добавлены позже.",
"donate_settings_title": "Поддержка / донат",
"donate_settings_hint": "Страница /support для всех пользователей. Без всплывающих окон — только по ссылке в меню.",
"donate_enabled": "Показывать страницу поддержки в меню",
"donate_intro": "Текст в начале страницы (необязательно)",
"donate_method_title": "Название кнопки (необязательно)",
"donate_method_details": "Реквизиты",
"donate_details_placeholder": "Телефон, номер карты, адрес кошелька…",
"donate_preview_hint": "Предпросмотр:",
"container_logs": "Логи контейнера",
"logs_btn": "📋 Логи",
"logs_live": "В реальном времени",