Compare commits
11 Commits
e855c30285
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d1f018dba1 | |||
| 1f3308fbb8 | |||
| 04828f0e0d | |||
| 09f1b7b261 | |||
| dc6d1212d6 | |||
| 9a893d5e6d | |||
| dc92b75387 | |||
| 4640262a14 | |||
| deee704e01 | |||
| 6bb8cdbe18 | |||
| a918f669e5 |
@@ -1,44 +1,70 @@
|
||||
# Py-3XUI-multiserver
|
||||
# ZernProxy Manager (Py-3XUI-multiserver)
|
||||
|
||||
Объединение нескольких серверов 3x-UI в единую VPN-подписку. Soft-tariff система с веб-панелью, донатной оплатой и Happ-совместимостью.
|
||||
Объединение нескольких серверов 3x-UI в единую VPN-подписку. Soft-tariff система с веб-панелью, донатной оплатой, Happ и WDTT совместимостью.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
servers.conf ──→ 3x-UI сервера (API + sub URL)
|
||||
settings.conf ──→ тарифы, платежи, админка
|
||||
servers.conf ──→ 3x-UI сервера (API + sub URL + status URL)
|
||||
settings.conf ──→ тарифы, платежи, админка, auto-propagate
|
||||
users.db ───────→ пользователи (SQLite)
|
||||
↓
|
||||
aggregator.py ──→ FastAPI сервер
|
||||
↓
|
||||
/sub/{id} ────→ подписка для клиента (VLESS links)
|
||||
/sub/{id} ────→ подписка для клиента (VLESS / Trojan / VMess / Shadowsocks)
|
||||
/admin/* ─────→ веб-панель управления
|
||||
/admin/api/propagate/{server} → синхронизация пользователей между серверами
|
||||
/ ────────────→ статус-пейдж с health-check серверов
|
||||
```
|
||||
|
||||
**Soft-tariff**: пользователь создаётся на всех inbounds один раз, tier (free/test/paid) меняется локально в БД, без дёргания 3x-UI API.
|
||||
|
||||
## Features
|
||||
|
||||
- Multi-server: объединение любого количества 3x-UI серверов
|
||||
- Multi-inbound: поддержка нескольких inbound на сервере
|
||||
- Soft-tariff: free / test / paid tier без пересоздания клиентов
|
||||
- DonationAlerts: приём платежей (50₽ → test 7д, 150₽ → paid 30д, 990₽ → paid 365д)
|
||||
- Веб-панель: дашборд, CRUD пользователей, просмотр трафика, QR-коды
|
||||
- Happ.su совместимость: hide-settings, sub-expire, announce через HTTP-хедеры
|
||||
- In-memory кеш: ссылки кешируются на 7 дней, трафик — на 2 минуты
|
||||
- ShortID ротация: автоматическая смена shortId на серверах каждые N часов
|
||||
- MOTD: объявления из motd.txt или settings
|
||||
- **Multi-server**: объединение любого количества 3x-UI серверов
|
||||
- **Multi-inbound**: поддержка нескольких inbound на сервере (разные протоколы)
|
||||
- **Soft-tariff**: free / test / paid tier без пересоздания клиентов
|
||||
- **Propagate**: синхронизация пользователей между серверами по strict majority (>50%)
|
||||
- **Auto-propagate**: фоновая синхронизация по расписанию (по умолчанию 30 мин)
|
||||
- **DonationAlerts**: приём платежей с поиском донатера по ID/username
|
||||
- **Веб-панель**: дашборд со статистикой, CRUD пользователей, просмотр трафика, QR-коды
|
||||
- **Статус-пейдж**: публичная страница с health-check серверов (CPU/RAM/Disk/Net/I/O)
|
||||
- **Happ.su / WDTT совместимость**: заголовки profile-title, sub-expire, announce, hide-settings
|
||||
- **Абсолютный expire**: `tariff_end_at` (Unix timestamp) — дни считаются динамически, автопонижение tier при истечении
|
||||
- **In-memory кеш**: ссылки кешируются на 7 дней, трафик — на 2 минуты
|
||||
- **ShortID ротация**: автоматическая смена shortId на серверах каждые N часов
|
||||
- **MOTD**: объявления из motd.txt или settings
|
||||
- **Health scoring**: взвешенная оценка здоровья сервера (CPU 30%, RAM 25%, Disk 20%, Net 15%, I/O 10%)
|
||||
- **Trojan support**: создание клиентов на Trojan инбаундах (password вместо uuid)
|
||||
|
||||
## wdtt (Zern-BlackOut) Compatibility
|
||||
|
||||
Проект полностью совместим с клиентом **Zern-BlackOut** (форк wdtt), доступным по адресу:
|
||||
[https://git.swe.zernmc.ru/sasheg/Zern-BlackOut](https://git.swe.zernmc.ru/sasheg/Zern-BlackOut)
|
||||
|
||||
> **Важно:** Клиент wdtt был модифицирован для совместимости с данным агрегатором.
|
||||
> Использование оригинального wdtt без модификаций может привести к некорректной работе.
|
||||
> Репозиторий Zern-BlackOut находится в активной разработке для полной интеграции.
|
||||
|
||||
Поддерживаемые возможности:
|
||||
- VLESS + XTLS Vision / Reality
|
||||
- Trojan + HTTP + Reality
|
||||
- Автообновление подписки
|
||||
- Отображение expire-даты и лимитов трафика
|
||||
- Hide-settings, announce, support-url через subscription-заголовки
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
pip install fastapi uvicorn httpx py3xui qrcode[pil] Pillow
|
||||
git clone https://github.com/SashegDev/Py-3XUI-multiserver.git
|
||||
git clone ssh://git@git.swe.zernmc.ru:2222/sasheg/Py-3XUI-multiserver.git
|
||||
cd Py-3XUI-multiserver
|
||||
# настроить servers.conf, settings.conf (см. ниже)
|
||||
python3 aggregator.py
|
||||
```
|
||||
|
||||
Для production: `screen -S aggregator python3 aggregator.py`
|
||||
|
||||
## Configuration
|
||||
|
||||
### servers.conf
|
||||
@@ -48,17 +74,27 @@ python3 aggregator.py
|
||||
"servers": [
|
||||
{
|
||||
"name": "ru-1",
|
||||
"subscription_url": "https://panel.domain.com:2096",
|
||||
"api_base_url": "https://panel.domain.com:65431/PATH",
|
||||
"country": "RU",
|
||||
"subscription_url": "http://panel.domain.com:2096",
|
||||
"sub_path": "/sub/{sub_id}",
|
||||
"country": "RU",
|
||||
"status_url": "http://domain.com:18765/status?secret=...",
|
||||
"inbounds": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Reality",
|
||||
"api_host": "https://panel.domain.com:65431/PATH",
|
||||
"api_host": "http://panel.domain.com:22881/PATH",
|
||||
"api_user": "admin",
|
||||
"api_pass": "password"
|
||||
"api_pass": "password",
|
||||
"is_free": true
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Trojan-Inbound",
|
||||
"protocol": "trojan",
|
||||
"api_host": "http://panel.domain.com:22881/PATH",
|
||||
"api_user": "admin",
|
||||
"api_pass": "password",
|
||||
"is_free": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -70,14 +106,16 @@ python3 aggregator.py
|
||||
|-------|------|-------------|
|
||||
| `name` | string | Unique server name |
|
||||
| `subscription_url` | string | Base URL for subscription links |
|
||||
| `api_base_url` | string | API URL for traffic stats |
|
||||
| `country` | string | 2-letter country code (RU, DE, NL...) |
|
||||
| `sub_path` | string | Sub endpoint path on the server (`/sub/{sub_id}`) |
|
||||
| `sub_path` | string | Sub endpoint path (`/sub/{sub_id}`) |
|
||||
| `country` | string | 2-letter country code (RU, DE, PL, SE...) |
|
||||
| `status_url` | string | URL for server health data (3x-UI status endpoint) |
|
||||
| `inbounds[].id` | int | Inbound ID in 3x-UI |
|
||||
| `inbounds[].name` | string | Display name |
|
||||
| `inbounds[].protocol` | string | *(optional)* protocol type (`trojan`) |
|
||||
| `inbounds[].api_host` | string | 3x-UI API URL for this inbound |
|
||||
| `inbounds[].api_user` | string | API login |
|
||||
| `inbounds[].api_pass` | string | API password |
|
||||
| `inbounds[].is_free` | bool | Доступен ли inbound для free-тарифа |
|
||||
|
||||
### settings.conf
|
||||
|
||||
@@ -93,32 +131,46 @@ python3 aggregator.py
|
||||
"tiers": {
|
||||
"free": {
|
||||
"name": "Free",
|
||||
"servers": ["ru-1"],
|
||||
"traffic_limit_gb": 0
|
||||
},
|
||||
"test": {
|
||||
"name": "Test 7 days",
|
||||
"servers": ["ru-1", "nl-1"],
|
||||
"name": "Test",
|
||||
"traffic_limit_gb": 5,
|
||||
"price": 50,
|
||||
"duration_days": 7
|
||||
"prices": {
|
||||
"test": 50
|
||||
},
|
||||
"days": {
|
||||
"test": 7
|
||||
}
|
||||
},
|
||||
"paid": {
|
||||
"name": "Premium",
|
||||
"servers": ["ru-1", "nl-1", "de-1"],
|
||||
"traffic_limit_gb": 0
|
||||
"traffic_limit_gb": 0,
|
||||
"prices": {
|
||||
"monthly": 150,
|
||||
"yearly": 990
|
||||
},
|
||||
"days": {
|
||||
"monthly": 30,
|
||||
"yearly": 365
|
||||
}
|
||||
}
|
||||
},
|
||||
"payments": {
|
||||
"donationalerts": {
|
||||
"enabled": true,
|
||||
"token": "your_donationalerts_token",
|
||||
"api_token": "...",
|
||||
"webhook_secret": "...",
|
||||
"url": "https://www.donationalerts.com/r/you"
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
"username": "admin",
|
||||
"password": "secure_password"
|
||||
},
|
||||
"auto_propagate": {
|
||||
"enabled": true,
|
||||
"interval_minutes": 30
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -127,46 +179,48 @@ python3 aggregator.py
|
||||
|
||||
| Tier | Servers | Traffic | Price | Duration |
|
||||
|------|---------|---------|-------|----------|
|
||||
| free | subset marked `is_free: true` in config | unlimited (0 = ∞) | free | forever |
|
||||
| test | all servers | 5 GB | 50₽ | 7 days |
|
||||
| paid | all servers | unlimited | 150₽/30d, 990₽/365d | configurable |
|
||||
| free | inbounds with `is_free: true` | unlimited (0 = ∞) | free | forever |
|
||||
| test | all inbounds | 5 GB | 50₽ | 7 days |
|
||||
| paid | all inbounds | unlimited | 150₽/30d, 990₽/365d | configurable |
|
||||
|
||||
Free servers are defined in `servers.conf` per-server via `is_free: true`.
|
||||
### Propagate
|
||||
|
||||
### MOTD
|
||||
При добавлении нового сервера или инбаунда пользователи НЕ создаются автоматически на нём.
|
||||
Для синхронизации используется **strict majority**: пользователь добавляется на целевой сервер только если он существует на >50% других активных серверов.
|
||||
|
||||
Two ways:
|
||||
1. `motd.txt` — plain text file in project root
|
||||
2. `settings.conf → announcement` — string (plain or base64)
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/admin/api/propagate/{server_name} -b "admin_token=..."
|
||||
```
|
||||
|
||||
В админ-панели кнопка 🔄 на каждом сервере. Auto-propagate запускается фоном каждые N минут.
|
||||
|
||||
## Endpoints
|
||||
|
||||
| Path | Method | Description |
|
||||
|------|--------|-------------|
|
||||
| `/sub/{id}` | GET | Subscription (format: base64/json/raw) |
|
||||
| `/sub/{id}` | GET (HTML accept) | Web page with QR and info |
|
||||
| `/admin/login` | POST | Admin auth |
|
||||
| `/sub/{id}` | GET (HTML) | Web page with QR and info |
|
||||
| `/admin/login` | GET/POST | Admin auth |
|
||||
| `/admin/users` | GET | User management panel |
|
||||
| `/admin/dashboard` | GET | Stats dashboard |
|
||||
| `/admin/dashboard` | GET | Stats dashboard with server health |
|
||||
| `/admin/api/users` | POST | Create user |
|
||||
| `/admin/api/users/update` | POST | Update user |
|
||||
| `/admin/api/users/delete` | POST | Delete user |
|
||||
| `/admin/api/users/delete` | POST | Delete user (with 3x-UI) |
|
||||
| `/admin/api/reload` | POST | Reload configs + clear cache |
|
||||
| `/admin/api/propagate/{server}` | POST | Sync users to server |
|
||||
| `/admin/api/rotate-shortids` | GET | Rotate short IDs on all servers |
|
||||
| `/` | GET | Landing page |
|
||||
| `/` | GET | Landing page with server status |
|
||||
|
||||
## DonationAlerts
|
||||
|
||||
Polling-based (every ~2 seconds). Amount mapping:
|
||||
- 50₽ → test tier (7 days)
|
||||
- 150₽ → paid tier (30 days)
|
||||
- 990₽ → paid tier (365 days)
|
||||
Polling-based (every ~2 seconds). Amount mapping from `settings.conf → tiers → prices`:
|
||||
- Any configured amount → corresponding tier + days
|
||||
- Donor identified by `id` or `username` from donation message
|
||||
- `tariff_end_at` extends dynamically (MAX of current end, now + days)
|
||||
|
||||
Other amounts are ignored. Donor identified by `id` (priority) or `username` from message parts.
|
||||
## Happ.su / WDTT Support
|
||||
|
||||
## Happ.su Support
|
||||
|
||||
Subscription response includes Happ-compatible headers:
|
||||
Subscription response includes compatible headers:
|
||||
- `Profile-Title`, `Profile-Update-Interval`, `Profile-Web-Page-Url`
|
||||
- `Subscription-Userinfo` (upload/download/total/expire)
|
||||
- `Announce`, `Support-Url`, `Hide-Settings`
|
||||
@@ -179,3 +233,5 @@ Subscription response includes Happ-compatible headers:
|
||||
- **py3xui** — 3x-UI API client
|
||||
- **httpx** — async HTTP for sub links
|
||||
- **qrcode** — QR generation
|
||||
- **Plus Jakarta Sans / JetBrains Mono** — UI typography
|
||||
- **Glassmorphism** — design system
|
||||
|
||||
+593
-418
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Дашборд · {%title%}</title>
|
||||
<link rel="stylesheet" href="/web/css/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-bg"></div>
|
||||
<div class="admin-wrap">
|
||||
<header class="admin-header">
|
||||
<div class="header-left">
|
||||
<h1>📊 Дашборд</h1>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<a href="/admin/users" class="nav-link">👥 Пользователи</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="stat-grid">
|
||||
<div class="stat-card"><div class="stat-lbl">Всего</div><div class="stat-val" style="color:var(--accent)">{%total_users%}</div></div>
|
||||
<div class="stat-card"><div class="stat-lbl">Free</div><div class="stat-val" style="color:var(--green)">{%free_users%}</div></div>
|
||||
<div class="stat-card"><div class="stat-lbl">Test</div><div class="stat-val" style="color:var(--amber)">{%test_users%}</div></div>
|
||||
<div class="stat-card"><div class="stat-lbl">Premium</div><div class="stat-val" style="color:var(--green)">{%paid_users%}</div></div>
|
||||
<div class="stat-card"><div class="stat-lbl">Онлайн</div><div class="stat-val" style="color:var(--accent)">{%online_count%}</div><div class="stat-sub">на всех серверах</div></div>
|
||||
<div class="stat-card"><div class="stat-lbl">Выручка</div><div class="stat-val" style="color:var(--green)">{%total_revenue%}₽</div><div class="stat-sub">всего оплат</div></div>
|
||||
<div class="stat-card"><div class="stat-lbl">Серверов</div><div class="stat-val" style="color:var(--amber)">{%server_count%}</div></div>
|
||||
<div class="stat-card"><div class="stat-lbl">Инбаундов</div><div class="stat-val" style="color:var(--rose)">{%inbound_count%}</div></div>
|
||||
</div>
|
||||
|
||||
<div class="srv-section">
|
||||
<h2>🖥 Серверы</h2>
|
||||
{%servers_rows%}
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function syncServer(name) {
|
||||
if (!confirm('Синхронизировать пользователей на ' + name + '?\nБудут добавлены только те, кто есть на большинстве других серверов.')) return;
|
||||
var btn = event.target;
|
||||
var orig = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '...';
|
||||
fetch('/admin/api/propagate/' + encodeURIComponent(name), {method:'POST'})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
var msg = 'Сервер: ' + d.server + '\n Всего: ' + d.total + '\nУже были: ' + d.already_on_server + '\nДобавлено: ' + d.added + '\nПропущено (мало серверов): ' + d.skipped + '\nОшибок: ' + d.failed;
|
||||
alert(msg);
|
||||
location.reload();
|
||||
})['catch'](function(e) { alert('Ошибка: ' + e); })
|
||||
['finally'](function() { btn.disabled = false; btn.textContent = orig; });
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>{%title%}</title>
|
||||
<link rel="stylesheet" href="/web/css/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-wrap">
|
||||
<div class="login-card">
|
||||
<div class="login-icon">🔒</div>
|
||||
<h1>{%title%}</h1>
|
||||
<form method="post" action="/admin/login">
|
||||
<div class="fg">
|
||||
<label for="u">Пользователь</label>
|
||||
<input id="u" type="text" name="username" required autocomplete="off">
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label for="p">Пароль</label>
|
||||
<input id="p" type="password" name="password" required>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-full" type="submit">Войти</button>
|
||||
</form>
|
||||
{%error_msg%}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,86 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Управление · {%title%}</title>
|
||||
<link rel="stylesheet" href="/web/css/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-bg"></div>
|
||||
<div class="admin-wrap">
|
||||
<header class="admin-header">
|
||||
<div class="header-left">
|
||||
<h1>👥 Пользователи</h1>
|
||||
<span class="header-count">{%user_count%}</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<a href="/admin/dashboard" class="nav-link">📊 Дашборд</a>
|
||||
<button class="btn btn-primary" onclick="openModal()">+ Добавить</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="tbl">
|
||||
<thead>
|
||||
<tr><th>ID</th><th>Username</th><th>Sub ID</th><th>Тариф</th><th>Дней</th><th>Оплачено</th><th>Трафик</th><th>Статус</th><th>Создан</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>{%users_rows%}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="userModal">
|
||||
<div class="modal-overlay" onclick="closeModal()"></div>
|
||||
<div class="modal-card">
|
||||
<div class="modal-h">
|
||||
<h2>Новый пользователь</h2>
|
||||
<button class="modal-x" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<form id="userForm">
|
||||
<div class="fg">
|
||||
<label for="nu">Username</label>
|
||||
<input id="nu" type="text" name="username" required autocomplete="off">
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label for="nt">Лимит трафика (GB, 0 = ∞)</label>
|
||||
<input id="nt" type="number" name="traffic_limit_gb" value="0" min="0">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-full">Создать</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="editModal">
|
||||
<div class="modal-overlay" onclick="closeEditModal()"></div>
|
||||
<div class="modal-card">
|
||||
<div class="modal-h">
|
||||
<h2>Редактировать</h2>
|
||||
<button class="modal-x" onclick="closeEditModal()">×</button>
|
||||
</div>
|
||||
<form id="editForm">
|
||||
<input type="hidden" name="id" id="editId">
|
||||
<div class="fg">
|
||||
<label for="et">Тариф</label>
|
||||
<select id="et" name="tier">
|
||||
{%tier_options%}
|
||||
</select>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label for="ed">Дней осталось</label>
|
||||
<input id="ed" type="number" name="tariff_days_remaining">
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label for="etr">Лимит трафика (GB, 0 = ∞)</label>
|
||||
<input id="etr" type="number" name="traffic_limit_gb">
|
||||
</div>
|
||||
<div class="fg-check">
|
||||
<label><input type="checkbox" name="is_active" id="editActive"> Активен</label>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary btn-full" onclick="submitEditForm()">Сохранить</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/web/js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,240 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||
|
||||
:root {
|
||||
--bg: #080c18;
|
||||
--surface: rgba(255,255,255,0.03);
|
||||
--surface-hover: rgba(255,255,255,0.06);
|
||||
--border: rgba(255,255,255,0.06);
|
||||
--border-hover: rgba(255,255,255,0.12);
|
||||
--text: #fff;
|
||||
--text-sec: rgba(255,255,255,0.5);
|
||||
--text-ter: rgba(255,255,255,0.3);
|
||||
--accent: #6C63FF;
|
||||
--green: #10b981;
|
||||
--amber: #f59e0b;
|
||||
--rose: #f43f5e;
|
||||
--card-rad: 16px;
|
||||
}
|
||||
|
||||
* { margin:0; padding:0; box-sizing:border-box }
|
||||
body {
|
||||
font-family:'Plus Jakarta Sans',sans-serif;
|
||||
background:var(--bg);
|
||||
color:var(--text);
|
||||
min-height:100vh;
|
||||
overflow-x:hidden;
|
||||
}
|
||||
a { color:var(--accent); text-decoration:none }
|
||||
a:hover { opacity:.85 }
|
||||
|
||||
.admin-bg {
|
||||
position:fixed;inset:0;
|
||||
background:
|
||||
radial-gradient(ellipse 70% 50% at 0% 30%, rgba(108,99,255,0.07) 0%, transparent 70%),
|
||||
radial-gradient(ellipse 50% 40% at 100% 70%, rgba(16,185,129,0.05) 0%, transparent 70%);
|
||||
pointer-events:none;
|
||||
z-index:0;
|
||||
}
|
||||
|
||||
.admin-wrap {
|
||||
position:relative;z-index:1;
|
||||
max-width:1100px;margin:0 auto;padding:40px 24px;
|
||||
animation:fadeUp .5s ease-out both;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.admin-header {
|
||||
display:flex;justify-content:space-between;align-items:center;
|
||||
margin-bottom:28px;gap:12px;flex-wrap:wrap;
|
||||
}
|
||||
.header-left { display:flex;align-items:center;gap:10px }
|
||||
.admin-header h1 { font-size:22px; font-weight:700; letter-spacing:-.02em }
|
||||
.header-count {
|
||||
font-size:13px; font-weight:600; font-family:'JetBrains Mono',monospace;
|
||||
padding:3px 10px; border-radius:20px;
|
||||
background:rgba(108,99,255,0.12); color:var(--accent);
|
||||
}
|
||||
.header-right { display:flex;align-items:center;gap:10px }
|
||||
.nav-link {
|
||||
font-size:13px; font-weight:600; padding:8px 14px;
|
||||
border-radius:10px; transition:background .2s;
|
||||
}
|
||||
.nav-link:hover { background:var(--surface) }
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display:inline-flex;align-items:center;gap:6px;
|
||||
padding:10px 20px; border:none; border-radius:10px;
|
||||
font-size:13px; font-weight:600; font-family:'Plus Jakarta Sans',sans-serif;
|
||||
cursor:pointer; transition:transform .25s, box-shadow .25s;
|
||||
text-decoration:none; line-height:1;
|
||||
}
|
||||
.btn:hover { transform:translateY(-2px) }
|
||||
.btn-primary { background:var(--accent); color:#fff }
|
||||
.btn-primary:hover { box-shadow:0 6px 24px rgba(108,99,255,0.25) }
|
||||
.btn-danger { background:var(--rose); color:#fff }
|
||||
.btn-danger:hover { box-shadow:0 6px 24px rgba(244,63,94,0.25) }
|
||||
.btn-sm {
|
||||
padding:6px 10px; border:none; border-radius:8px;
|
||||
font-size:12px; cursor:pointer; background:var(--surface);
|
||||
color:var(--text-sec); transition:background .2s, color .2s;
|
||||
}
|
||||
.btn-sm:hover { background:var(--surface-hover); color:var(--text) }
|
||||
.btn-sm.danger { color:var(--rose) }
|
||||
.btn-sm.danger:hover { background:rgba(244,63,94,0.12) }
|
||||
.btn-full { width:100%; justify-content:center; padding:12px }
|
||||
|
||||
/* Table */
|
||||
.table-wrap {
|
||||
background:var(--surface); border:1px solid var(--border);
|
||||
border-radius:var(--card-rad);
|
||||
backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px);
|
||||
overflow-x:auto;
|
||||
animation:fadeUp .5s .1s ease-out both;
|
||||
}
|
||||
.tbl { width:100%; border-collapse:collapse; font-size:13px }
|
||||
.tbl th {
|
||||
text-align:left; padding:14px 16px;
|
||||
color:var(--text-ter); font-weight:600; font-size:11px;
|
||||
text-transform:uppercase; letter-spacing:.04em;
|
||||
border-bottom:1px solid var(--border);
|
||||
white-space:nowrap;
|
||||
}
|
||||
.tbl td {
|
||||
padding:12px 16px;
|
||||
border-bottom:1px solid var(--border);
|
||||
color:var(--text-sec);
|
||||
white-space:nowrap;
|
||||
}
|
||||
.tbl tr:last-child td { border-bottom:none }
|
||||
.tbl tr:hover td { background:rgba(255,255,255,0.02) }
|
||||
.tbl code { font-family:'JetBrains Mono',monospace; font-size:10px; color:var(--text-ter) }
|
||||
.tbl .actions { display:flex; gap:4px }
|
||||
|
||||
/* Badge */
|
||||
.badge {
|
||||
display:inline-block; padding:3px 10px; border-radius:20px;
|
||||
font-size:11px; font-weight:600; letter-spacing:.02em;
|
||||
}
|
||||
.badge-free { background:rgba(16,185,129,0.12); color:var(--green) }
|
||||
.badge-paid { background:rgba(245,158,11,0.12); color:var(--amber) }
|
||||
.badge-test { background:rgba(108,99,255,0.12); color:var(--accent) }
|
||||
|
||||
/* Stat grid */
|
||||
.stat-grid {
|
||||
display:grid; grid-template-columns:repeat(auto-fill,minmax(140px,1fr));
|
||||
gap:10px; margin-bottom:28px;
|
||||
animation:fadeUp .5s .05s ease-out both;
|
||||
}
|
||||
.stat-card {
|
||||
background:var(--surface); border:1px solid var(--border);
|
||||
border-radius:var(--card-rad); padding:18px 20px;
|
||||
backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px);
|
||||
transition:border-color .3s;
|
||||
}
|
||||
.stat-card:hover { border-color:var(--border-hover) }
|
||||
.stat-lbl { font-size:12px; color:var(--text-ter); font-weight:500; margin-bottom:6px; text-transform:uppercase; letter-spacing:.03em }
|
||||
.stat-val { font-size:30px; font-weight:800; letter-spacing:-.03em }
|
||||
.stat-sub { font-size:11px; color:var(--text-ter); margin-top:4px }
|
||||
|
||||
/* Server section */
|
||||
.srv-section { animation:fadeUp .5s .15s ease-out both }
|
||||
.srv-section h2 { font-size:16px; font-weight:600; margin-bottom:14px }
|
||||
.srv-row {
|
||||
display:flex; justify-content:space-between; align-items:center;
|
||||
background:var(--surface); border:1px solid var(--border);
|
||||
border-radius:12px; padding:14px 18px; margin-bottom:8px;
|
||||
backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px);
|
||||
transition:border-color .3s;
|
||||
}
|
||||
.srv-row:hover { border-color:var(--border-hover) }
|
||||
.srv-row .srv-name { font-size:14px; font-weight:600; display:flex; align-items:center; gap:6px }
|
||||
.srv-row .srv-ibs { font-size:12px; color:var(--text-ter); font-family:'JetBrains Mono',monospace }
|
||||
|
||||
/* Login */
|
||||
.login-wrap {
|
||||
position:fixed;inset:0;display:flex;
|
||||
align-items:center;justify-content:center;
|
||||
padding:20px;
|
||||
}
|
||||
.login-card {
|
||||
background:var(--surface); border:1px solid var(--border);
|
||||
border-radius:var(--card-rad); padding:40px 36px;
|
||||
max-width:380px; width:100%;
|
||||
backdrop-filter:blur(20px); -webkit-backdrop-filter:blur(20px);
|
||||
text-align:center;
|
||||
animation:fadeUp .5s ease-out both;
|
||||
}
|
||||
.login-icon { font-size:36px; margin-bottom:8px }
|
||||
.login-card h1 { font-size:20px; font-weight:700; margin-bottom:24px; letter-spacing:-.02em }
|
||||
.login-err {
|
||||
margin-top:16px; padding:10px 14px;
|
||||
background:rgba(244,63,94,0.08);
|
||||
border:1px solid rgba(244,63,94,0.2);
|
||||
border-radius:10px; font-size:13px; color:var(--rose);
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.fg { margin-bottom:16px; text-align:left }
|
||||
.fg label { display:block; margin-bottom:6px; font-size:12px; font-weight:600; color:var(--text-sec) }
|
||||
.fg input, .fg select {
|
||||
width:100%; padding:12px 14px;
|
||||
background:rgba(255,255,255,0.04);
|
||||
border:1px solid var(--border);
|
||||
border-radius:10px; color:var(--text);
|
||||
font-size:14px; font-family:'Plus Jakarta Sans',sans-serif;
|
||||
outline:none; transition:border-color .2s;
|
||||
}
|
||||
.fg input:focus, .fg select:focus { border-color:var(--accent) }
|
||||
.fg input::placeholder { color:var(--text-ter) }
|
||||
.fg select option { background:var(--bg) }
|
||||
.fg-check { margin-bottom:16px }
|
||||
.fg-check label { font-size:13px; color:var(--text-sec); display:flex; align-items:center; gap:8px; cursor:pointer }
|
||||
.fg-check input[type="checkbox"] { width:16px; height:16px; accent-color:var(--accent) }
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
display:none; position:fixed; inset:0;
|
||||
z-index:1000; align-items:center; justify-content:center;
|
||||
padding:20px;
|
||||
}
|
||||
.modal.active { display:flex }
|
||||
.modal-overlay {
|
||||
position:absolute;inset:0;
|
||||
background:rgba(0,0,0,0.6);
|
||||
backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px);
|
||||
}
|
||||
.modal-card {
|
||||
position:relative;
|
||||
background:var(--bg); border:1px solid var(--border);
|
||||
border-radius:var(--card-rad); padding:28px;
|
||||
max-width:460px; width:100%;
|
||||
animation:fadeUp .3s ease-out both;
|
||||
}
|
||||
.modal-h { display:flex; justify-content:space-between; align-items:center; margin-bottom:20px }
|
||||
.modal-h h2 { font-size:18px; font-weight:700 }
|
||||
.modal-x {
|
||||
background:none; border:none; color:var(--text-ter);
|
||||
font-size:24px; cursor:pointer; padding:4px; line-height:1;
|
||||
transition:color .2s;
|
||||
}
|
||||
.modal-x:hover { color:var(--text) }
|
||||
|
||||
@keyframes fadeUp {
|
||||
from { opacity:0; transform:translateY(16px) }
|
||||
to { opacity:1; transform:translateY(0) }
|
||||
}
|
||||
|
||||
@media(max-width:700px) {
|
||||
.admin-wrap { padding:20px 12px }
|
||||
.admin-header { flex-direction:column; align-items:stretch }
|
||||
.header-right { justify-content:space-between }
|
||||
.tbl { font-size:12px }
|
||||
.tbl th, .tbl td { padding:10px 12px }
|
||||
.stat-card { padding:14px 14px }
|
||||
.stat-val { font-size:22px }
|
||||
}
|
||||
@media(max-width:480px) {
|
||||
.login-card { padding:28px 20px }
|
||||
.modal-card { padding:20px }
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||
|
||||
:root {
|
||||
--bg: #080c18;
|
||||
--surface: rgba(255,255,255,0.03);
|
||||
--border: rgba(255,255,255,0.06);
|
||||
--border-hover: rgba(255,255,255,0.12);
|
||||
--text: #fff;
|
||||
--text-sec: rgba(255,255,255,0.5);
|
||||
--text-ter: rgba(255,255,255,0.3);
|
||||
--accent: #6C63FF;
|
||||
--green: #10b981;
|
||||
--amber: #f59e0b;
|
||||
--rose: #f43f5e;
|
||||
--card-rad: 16px;
|
||||
}
|
||||
|
||||
* { margin:0; padding:0; box-sizing:border-box }
|
||||
html { scroll-behavior:smooth }
|
||||
body {
|
||||
font-family:'Plus Jakarta Sans',sans-serif;
|
||||
background:var(--bg);
|
||||
color:var(--text);
|
||||
min-height:100vh;
|
||||
overflow-x:hidden;
|
||||
}
|
||||
body::before {
|
||||
content:'';
|
||||
position:fixed;inset:0;
|
||||
background:
|
||||
radial-gradient(ellipse 80% 50% at 10% 0%, rgba(108,99,255,0.08) 0%, transparent 70%),
|
||||
radial-gradient(ellipse 60% 40% at 90% 100%, rgba(16,185,129,0.06) 0%, transparent 70%);
|
||||
pointer-events:none;
|
||||
z-index:0;
|
||||
}
|
||||
.container { position:relative; z-index:1; max-width:720px; margin:0 auto; padding:48px 20px 32px }
|
||||
|
||||
.section { animation:fadeUp .6s ease-out var(--d,0s) both; margin-bottom:40px }
|
||||
|
||||
/* Header */
|
||||
.header { text-align:center; --d:0.1s }
|
||||
.header h1 {
|
||||
font-size:34px; font-weight:800; letter-spacing:-.03em;
|
||||
background:linear-gradient(135deg,#fff 30%,rgba(255,255,255,.6));
|
||||
-webkit-background-clip:text; -webkit-text-fill-color:transparent;
|
||||
background-clip:text;
|
||||
}
|
||||
.header p { color:var(--text-sec); font-size:15px; margin-top:8px; line-height:1.6 }
|
||||
.header .sub { color:var(--text-ter); font-size:13px; margin-top:4px }
|
||||
|
||||
/* Why us */
|
||||
.why { --d:0.2s }
|
||||
.why h2 { font-size:18px; font-weight:600; margin-bottom:14px }
|
||||
.why-grid { display:grid; grid-template-columns:1fr 1fr; gap:10px }
|
||||
.w-card {
|
||||
background:var(--surface); border:1px solid var(--border);
|
||||
border-radius:12px; padding:14px;
|
||||
backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px);
|
||||
transition:border-color .3s, transform .3s;
|
||||
animation:fadeUp .5s ease-out calc(var(--d,0s) + var(--i,0)*0.06s) both;
|
||||
}
|
||||
.w-card:hover { border-color:var(--border-hover); transform:translateY(-2px) }
|
||||
.w-card .e { font-size:20px; margin-bottom:6px }
|
||||
.w-card h4 { font-size:13px; font-weight:600; margin-bottom:3px }
|
||||
.w-card p { font-size:11px; color:var(--text-sec); line-height:1.5 }
|
||||
|
||||
/* Servers */
|
||||
.srv-section { --d:0.35s }
|
||||
.srv-section h2 { font-size:18px; font-weight:600; margin-bottom:14px }
|
||||
.servers { display:grid; gap:14px }
|
||||
|
||||
/* Server card */
|
||||
.s-card {
|
||||
background:var(--surface); border:1px solid var(--border);
|
||||
border-left:3px solid var(--hl, transparent);
|
||||
border-radius:var(--card-rad); padding:16px 18px;
|
||||
backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px);
|
||||
transition:border-color .4s, transform .4s, box-shadow .4s;
|
||||
animation:fadeUp .6s ease-out var(--d,0s) both;
|
||||
}
|
||||
.s-card:hover { border-color:var(--border-hover); transform:translateY(-3px); box-shadow:0 12px 48px rgba(108,99,255,0.06) }
|
||||
.s-head { font-size:14px; font-weight:600; display:flex; align-items:center; gap:6px; flex-wrap:wrap }
|
||||
.s-head .flag { font-size:20px; line-height:1 }
|
||||
.tier { font-size:10px; font-weight:600; padding:2px 8px; border-radius:20px; letter-spacing:.02em }
|
||||
.tier.both { background:rgba(108,99,255,0.15); color:var(--accent) }
|
||||
.tier.free { background:rgba(16,185,129,0.12); color:var(--green) }
|
||||
.tier.prem { background:rgba(245,158,11,0.12); color:var(--amber) }
|
||||
.h-dot { width:8px; height:8px; border-radius:50%; display:inline-block; flex-shrink:0 }
|
||||
.h-lbl { font-size:11px; font-weight:600; font-family:'JetBrains Mono',monospace; white-space:nowrap }
|
||||
.s-stats { font-size:11px; color:var(--text-ter); font-family:'JetBrains Mono',monospace; margin-top:8px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis }
|
||||
.s-stats.off { color:var(--rose) }
|
||||
|
||||
/* Inbounds inside server card */
|
||||
.ib-list { margin-top:10px; display:flex; flex-wrap:wrap; gap:6px }
|
||||
.ib-card {
|
||||
display:inline-flex; align-items:center; gap:5px;
|
||||
background:rgba(255,255,255,0.04);
|
||||
border:1px solid rgba(255,255,255,0.06);
|
||||
border-radius:8px; padding:5px 10px;
|
||||
font-size:11px; font-family:'JetBrains Mono',monospace;
|
||||
color:var(--text-sec);
|
||||
transition:border-color .2s;
|
||||
}
|
||||
.ib-card:hover { border-color:var(--border-hover) }
|
||||
.ib-dot { width:5px; height:5px; border-radius:50%; background:var(--green); flex-shrink:0 }
|
||||
.ib-name { font-weight:500 }
|
||||
.ib-type { color:var(--text-ter); font-size:10px }
|
||||
|
||||
/* RU server special note */
|
||||
.ru-note {
|
||||
margin-top:10px; padding:8px 12px;
|
||||
background:rgba(245,158,11,0.08);
|
||||
border:1px solid rgba(245,158,11,0.15);
|
||||
border-radius:8px;
|
||||
font-size:11px; color:var(--amber);
|
||||
display:flex; align-items:center; gap:6px;
|
||||
}
|
||||
|
||||
/* Tech info */
|
||||
.tech { --d:0.5s }
|
||||
.tech h2 { font-size:18px; font-weight:600; margin-bottom:14px }
|
||||
.tech-grid {
|
||||
display:grid; grid-template-columns:1fr 1fr;
|
||||
background:var(--surface); border:1px solid var(--border);
|
||||
border-radius:var(--card-rad); padding:20px;
|
||||
backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px);
|
||||
transition:border-color .3s;
|
||||
}
|
||||
.tech-grid:hover { border-color:var(--border-hover) }
|
||||
.t-item { font-size:12px; color:var(--text-sec); padding:8px 0; display:flex; align-items:center; gap:8px; border-bottom:1px solid var(--border) }
|
||||
.t-item:last-child { border-bottom:none }
|
||||
.t-item .t-dot { width:4px; height:4px; border-radius:2px; background:var(--accent); flex-shrink:0 }
|
||||
|
||||
/* Buttons */
|
||||
.btns { display:flex; justify-content:center; gap:10px; margin-top:12px; animation:fadeUp .6s .55s ease-out both }
|
||||
.btn {
|
||||
display:inline-flex; align-items:center; gap:6px;
|
||||
padding:12px 28px; border-radius:12px;
|
||||
font-size:14px; font-weight:600; text-decoration:none;
|
||||
transition:transform .3s, box-shadow .3s;
|
||||
cursor:pointer;
|
||||
}
|
||||
.btn:hover { transform:translateY(-2px); box-shadow:0 8px 32px rgba(108,99,255,0.2) }
|
||||
.btn.da { background:linear-gradient(135deg,#f43f5e,#e11d48); color:#fff }
|
||||
|
||||
.footer { text-align:center; margin-top:40px; color:var(--text-ter); font-size:12px; animation:fadeUp .6s .6s ease-out both }
|
||||
|
||||
@keyframes fadeUp {
|
||||
from { opacity:0; transform:translateY(16px) }
|
||||
to { opacity:1; transform:translateY(0) }
|
||||
}
|
||||
|
||||
@media(max-width:520px) {
|
||||
.container { padding:24px 12px }
|
||||
.header h1 { font-size:24px }
|
||||
.why-grid { grid-template-columns:1fr }
|
||||
.s-card { padding:12px 14px }
|
||||
.s-head { font-size:13px; gap:4px }
|
||||
.h-lbl { font-size:10px }
|
||||
.s-stats { font-size:10px }
|
||||
.ib-card { font-size:10px; padding:4px 8px }
|
||||
.ru-note { font-size:10px; padding:6px 10px }
|
||||
.tech-grid { grid-template-columns:1fr }
|
||||
.btns { flex-direction:column; align-items:center }
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||
|
||||
:root {
|
||||
--bg: #080c18;
|
||||
--surface: rgba(255,255,255,0.03);
|
||||
--border: rgba(255,255,255,0.06);
|
||||
--border-hover: rgba(255,255,255,0.12);
|
||||
--text: #fff;
|
||||
--text-sec: rgba(255,255,255,0.5);
|
||||
--text-ter: rgba(255,255,255,0.3);
|
||||
--accent: #6C63FF;
|
||||
--green: #10b981;
|
||||
--amber: #f59e0b;
|
||||
--rose: #f43f5e;
|
||||
--card-rad: 16px;
|
||||
}
|
||||
|
||||
* { margin:0; padding:0; box-sizing:border-box }
|
||||
body {
|
||||
font-family:'Plus Jakarta Sans',sans-serif;
|
||||
background:var(--bg);
|
||||
color:var(--text);
|
||||
min-height:100vh;
|
||||
overflow-x:hidden;
|
||||
}
|
||||
|
||||
.sub-bg {
|
||||
position:fixed;inset:0;
|
||||
background:
|
||||
radial-gradient(ellipse 70% 50% at 30% 0%, rgba(108,99,255,0.08) 0%, transparent 70%),
|
||||
radial-gradient(ellipse 50% 40% at 70% 100%, rgba(16,185,129,0.06) 0%, transparent 70%);
|
||||
pointer-events:none;
|
||||
z-index:0;
|
||||
}
|
||||
|
||||
.sub-wrap {
|
||||
position:relative;z-index:1;
|
||||
max-width:600px;margin:0 auto;padding:40px 20px;
|
||||
}
|
||||
|
||||
.sub-card {
|
||||
background:var(--surface); border:1px solid var(--border);
|
||||
border-radius:var(--card-rad); padding:28px 24px;
|
||||
margin-bottom:16px;
|
||||
backdrop-filter:blur(16px); -webkit-backdrop-filter:blur(16px);
|
||||
animation:fadeUp .5s ease-out var(--d,0s) both;
|
||||
}
|
||||
|
||||
.sub-top { text-align:center; --d:0s }
|
||||
.sub-top h1 {
|
||||
font-size:24px; font-weight:800; letter-spacing:-.03em;
|
||||
background:linear-gradient(135deg,#fff 30%,rgba(255,255,255,.6));
|
||||
-webkit-background-clip:text; -webkit-text-fill-color:transparent;
|
||||
background-clip:text;
|
||||
margin:12px 0 8px;
|
||||
}
|
||||
|
||||
.logo-img { width:72px; height:72px; border-radius:14px }
|
||||
.logo-emoji { font-size:48px }
|
||||
|
||||
.tier-badge {
|
||||
display:inline-block;
|
||||
padding:4px 14px; border-radius:20px;
|
||||
font-size:12px; font-weight:600; letter-spacing:.02em;
|
||||
text-transform:uppercase;
|
||||
}
|
||||
|
||||
.announcement {
|
||||
background:rgba(108,99,255,0.08);
|
||||
border:1px solid rgba(108,99,255,0.15);
|
||||
border-left:3px solid var(--accent);
|
||||
border-radius:12px; padding:14px 16px;
|
||||
margin-bottom:16px; font-size:14px;
|
||||
animation:fadeUp .5s .05s ease-out both;
|
||||
}
|
||||
|
||||
.info-block { font-size:14px; line-height:1.8; margin-bottom:16px }
|
||||
.info-block p { display:flex; align-items:center; gap:8px; color:var(--text-sec) }
|
||||
.traffic-server { font-size:12px; margin-top:8px; padding-top:8px; border-top:1px solid var(--border) }
|
||||
.traffic-server .server-name { color:var(--text-ter); font-weight:500 }
|
||||
.traffic-server .traffic-values { color:var(--green); font-family:'JetBrains Mono',monospace }
|
||||
|
||||
.srv-tags { display:flex; flex-wrap:wrap; gap:8px }
|
||||
.srv-tag {
|
||||
display:inline-flex; align-items:center; gap:4px;
|
||||
background:rgba(255,255,255,0.04);
|
||||
border:1px solid var(--border);
|
||||
border-radius:20px; padding:5px 12px;
|
||||
font-size:12px; font-weight:500; color:var(--text-sec);
|
||||
}
|
||||
|
||||
.sub-qr { text-align:center }
|
||||
.qr-box {
|
||||
display:inline-block;
|
||||
background:rgba(255,255,255,0.04);
|
||||
border-radius:14px; padding:8px; margin-bottom:14px;
|
||||
}
|
||||
.qr-box img {
|
||||
display:block;
|
||||
border-radius:10px; width:200px; height:200px;
|
||||
}
|
||||
.qr-url {
|
||||
background:rgba(255,255,255,0.03);
|
||||
border:1px solid var(--border);
|
||||
border-radius:10px; padding:10px 14px;
|
||||
word-break:break-all; font-size:11px; color:var(--text-ter);
|
||||
font-family:'JetBrains Mono',monospace; margin-bottom:16px;
|
||||
}
|
||||
.qr-actions { display:flex; flex-direction:column; gap:10px; align-items:center }
|
||||
|
||||
.btn {
|
||||
display:inline-flex; align-items:center; gap:6px;
|
||||
padding:12px 28px; border:none; border-radius:12px;
|
||||
font-size:14px; font-weight:600; font-family:'Plus Jakarta Sans',sans-serif;
|
||||
cursor:pointer; text-decoration:none;
|
||||
transition:transform .3s, box-shadow .3s;
|
||||
}
|
||||
.btn:hover { transform:translateY(-2px) }
|
||||
.btn-primary { background:var(--accent); color:#fff }
|
||||
.btn-primary:hover { box-shadow:0 8px 32px rgba(108,99,255,0.25) }
|
||||
.btn-support { background:var(--rose); color:#fff }
|
||||
.btn-support:hover { box-shadow:0 8px 32px rgba(244,63,94,0.25) }
|
||||
|
||||
.sub-footer { text-align:center; padding:20px; color:var(--text-ter); font-size:12px; --d:0.25s }
|
||||
|
||||
@keyframes fadeUp {
|
||||
from { opacity:0; transform:translateY(16px) }
|
||||
to { opacity:1; transform:translateY(0) }
|
||||
}
|
||||
|
||||
@media(max-width:520px) {
|
||||
.sub-wrap { padding:20px 12px }
|
||||
.sub-card { padding:20px 16px }
|
||||
.sub-top h1 { font-size:20px }
|
||||
.qr-box img { width:160px; height:160px }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>{%title%}</title>
|
||||
<link rel="stylesheet" href="/web/css/home.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
||||
<div class="section header">
|
||||
<h1>⚡ {%title%}</h1>
|
||||
<p>Быстрый VPN с собственными серверами в Европе. Безлимитный трафик, VLESS + XTLS, никаких логов.</p>
|
||||
<p class="sub">Подписка на основе подписки · Happ-совместимость · Мгновенная активация</p>
|
||||
</div>
|
||||
|
||||
<div class="section why">
|
||||
<h2>Почему {%title%}</h2>
|
||||
<div class="why-grid">
|
||||
<div class="w-card" style="--i:0"><div class="e">🚀</div><h4>Скорость</h4><p>Каналы 1–10 Gbit/s, собственные серверы, без посредников</p></div>
|
||||
<div class="w-card" style="--i:1"><div class="e">🔒</div><h4>Приватность</h4><p>Zero-log политика — трафик не хранится и не анализируется</p></div>
|
||||
<div class="w-card" style="--i:2"><div class="e">🌎</div><h4>Покрытие</h4><p>Серверы в Германии, Швеции и России — выбирай ближайший</p></div>
|
||||
<div class="w-card" style="--i:3"><div class="e">💎</div><h4>Тарифы</h4><p>Бесплатный доступ к free-серверам или Premium от 150 ₽/мес</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section srv-section">
|
||||
<h2>🖥 Состояние серверов</h2>
|
||||
<div class="servers">{%srv_cards%}</div>
|
||||
</div>
|
||||
|
||||
<div class="section tech">
|
||||
<h2>🔧 Технические детали</h2>
|
||||
<div class="tech-grid">
|
||||
<div class="t-item"><span class="t-dot"></span>VLESS + XTLS Vision / Reality</div>
|
||||
<div class="t-item"><span class="t-dot"></span>TCP, WebSocket, gRPC</div>
|
||||
<div class="t-item"><span class="t-dot"></span>DDoS-защита на всех серверах</div>
|
||||
<div class="t-item"><span class="t-dot"></span>Zero-log политика</div>
|
||||
<div class="t-item"><span class="t-dot"></span>Поддержка Happ (iOS / Android / Desktop)</div>
|
||||
<div class="t-item"><span class="t-dot"></span>Автообновление подписки каждые 12ч</div>
|
||||
</div>
|
||||
<div class="btns">
|
||||
<a href="{%da_url%}" class="btn da">❤️ Поддержать проект</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">© 2026 {%title%}</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
function openModal() { document.getElementById('userModal').classList.add('active'); }
|
||||
function closeModal() { document.getElementById('userModal').classList.remove('active'); }
|
||||
|
||||
function toast(msg, ok) {
|
||||
var t = document.createElement('div');
|
||||
t.textContent = msg;
|
||||
Object.assign(t.style, {
|
||||
position:'fixed', bottom:'24px', left:'50%', transform:'translateX(-50%)',
|
||||
padding:'12px 24px', borderRadius:'12px', fontSize:'13px', fontWeight:'600',
|
||||
background: ok ? 'rgba(16,185,129,0.15)' : 'rgba(244,63,94,0.15)',
|
||||
border: ok ? '1px solid rgba(16,185,129,0.3)' : '1px solid rgba(244,63,94,0.3)',
|
||||
color: ok ? 'var(--green)' : 'var(--rose)',
|
||||
backdropFilter:'blur(12px)', zIndex:'9999',
|
||||
animation:'fadeUp .3s ease-out both', fontFamily:'"Plus Jakarta Sans",sans-serif'
|
||||
});
|
||||
document.body.appendChild(t);
|
||||
setTimeout(function() { t.style.opacity='0'; t.style.transition='opacity .4s'; setTimeout(function(){t.remove()},400) }, 3000);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var form = document.getElementById('userForm');
|
||||
if (form) {
|
||||
form.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
var formData = new FormData(e.target);
|
||||
var username = formData.get('username');
|
||||
var btn = e.target.querySelector('button[type="submit"]');
|
||||
var orig = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '...';
|
||||
|
||||
var response = await fetch('/admin/api/users', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
traffic_limit_gb: parseInt(formData.get('traffic_limit_gb')) || 0
|
||||
})
|
||||
});
|
||||
|
||||
var result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
var msg = 'Пользователь ' + username + ' создан!';
|
||||
toast(msg, true);
|
||||
setTimeout(function(){ location.reload() }, 1000);
|
||||
} else {
|
||||
toast('Ошибка: ' + (result.error || 'unknown'), false);
|
||||
btn.disabled = false;
|
||||
btn.textContent = orig;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function editUser(id, tier, days, traffic, active) {
|
||||
document.getElementById('editId').value = id;
|
||||
document.getElementById('et').value = tier;
|
||||
document.getElementById('ed').value = days;
|
||||
document.getElementById('etr').value = traffic;
|
||||
document.getElementById('editActive').checked = active == 1;
|
||||
document.getElementById('editModal').classList.add('active');
|
||||
}
|
||||
|
||||
function closeEditModal() { document.getElementById('editModal').classList.remove('active'); }
|
||||
|
||||
async function submitEditForm() {
|
||||
var form = document.getElementById('editForm');
|
||||
var formData = new FormData(form);
|
||||
var data = {
|
||||
id: formData.get('id'),
|
||||
tier: formData.get('tier'),
|
||||
tariff_days_remaining: parseInt(formData.get('tariff_days_remaining')) || 0,
|
||||
traffic_limit_gb: formData.get('traffic_limit_gb') === '' ? 0 : parseInt(formData.get('traffic_limit_gb')),
|
||||
is_active: formData.get('is_active') === 'on'
|
||||
};
|
||||
var btn = form.querySelector('button');
|
||||
var orig = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '...';
|
||||
|
||||
var response = await fetch('/admin/api/users/update', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
toast('Сохранено!', true);
|
||||
setTimeout(function(){ location.reload() }, 1000);
|
||||
} else {
|
||||
var result = await response.json();
|
||||
toast('Ошибка: ' + (result.error || 'unknown'), false);
|
||||
btn.disabled = false;
|
||||
btn.textContent = orig;
|
||||
}
|
||||
}
|
||||
|
||||
function deleteUser(id, username) {
|
||||
if (confirm('Удалить пользователя ' + username + '? Это удалит его со всех серверов!')) {
|
||||
fetch('/admin/api/users/delete', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({id: id, username: username})
|
||||
}).then(function(r) {
|
||||
if (r.ok) {
|
||||
toast('Удалён!', true);
|
||||
setTimeout(function(){ location.reload() }, 1000);
|
||||
} else {
|
||||
toast('Ошибка удаления', false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Подписка · {%title%}</title>
|
||||
<link rel="stylesheet" href="/web/css/sub.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="sub-bg"></div>
|
||||
<div class="sub-wrap">
|
||||
<div class="sub-card sub-top">
|
||||
{%logo_html%}
|
||||
<h1>{%title%}</h1>
|
||||
{%tier_badge%}
|
||||
</div>
|
||||
|
||||
{%announcement_html%}
|
||||
|
||||
<div class="sub-card">
|
||||
{%info_html%}
|
||||
<div class="srv-tags">{%servers_html%}</div>
|
||||
</div>
|
||||
|
||||
<div class="sub-card sub-qr">
|
||||
<div class="qr-box">
|
||||
<img src="data:image/png;base64,{%qr_base64%}" alt="QR">
|
||||
</div>
|
||||
<div class="qr-url">{%sub_url%}</div>
|
||||
<div class="qr-actions">
|
||||
<a href="{%sub_url%}" class="btn btn-primary">
|
||||
⬇️ Скачать подписку
|
||||
</a>
|
||||
{%support_btn%}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sub-footer">© 2026 {%title%}</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user