Files

1088 lines
52 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru" data-theme="dark" data-bs-theme="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Админка — VPN Service</title>
<script>
(function () {
try {
var t = localStorage.getItem("ef_theme");
if (t !== "light" && t !== "dark") {
t = window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
}
document.documentElement.setAttribute("data-theme", t);
document.documentElement.setAttribute("data-bs-theme", t);
} catch (e) {
document.documentElement.setAttribute("data-theme", "dark");
document.documentElement.setAttribute("data-bs-theme", "dark");
}
})();
</script>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="/static/theme-bs.css" />
<link rel="stylesheet" href="/static/styles.css" />
<link rel="stylesheet" href="/static/admin.css" />
</head>
<body class="admin-body">
<div class="admin-bg" aria-hidden="true"></div>
<div id="admin-app" class="admin-app">
<header class="admin-topbar">
<a href="/" class="admin-brand">
<span class="admin-brand-mark" aria-hidden="true">🦊</span>
<span>
<span class="admin-brand-eyebrow">VPN Service</span>
<span class="admin-brand-title">Админ-панель</span>
</span>
</a>
<div class="admin-topbar-actions">
<button type="button" class="theme-toggle" id="btn-theme-toggle" title="Тема" aria-label="Переключить тему">
<i class="fa-solid fa-moon icon-moon"></i>
<i class="fa-solid fa-sun icon-sun"></i>
</button>
<a href="/" class="btn ghost small-btn admin-link-cabinet"><i class="fa-solid fa-arrow-left"></i> Кабинет</a>
<button type="button" class="btn ghost small-btn hidden" id="btn-admin-logout"><i class="fa-solid fa-right-from-bracket"></i> Выйти</button>
</div>
</header>
<section id="admin-login" class="admin-login-wrap">
<div class="admin-login-card">
<div class="admin-login-glow" aria-hidden="true"></div>
<div class="admin-login-icon" aria-hidden="true"><i class="fa-solid fa-lock"></i></div>
<p class="admin-login-kicker">Только для администраторов</p>
<h1>С возвращением</h1>
<p class="admin-login-lead">
Отдельный вход для <strong>/admin</strong>.
</p>
<form id="form-admin-login" class="auth-form admin-login-form">
<label>
Логин
<input name="login" type="text" required autocomplete="username" placeholder="admin" />
</label>
<label>
Пароль
<input name="password" type="password" required autocomplete="current-password" placeholder="••••••••" />
</label>
<button class="btn primary full btn-lg admin-login-btn" type="submit">Войти в панель</button>
</form>
<p id="admin-login-error" class="admin-alert hidden" role="alert"></p>
</div>
</section>
<section id="admin-panel" class="admin-dashboard hidden">
<aside class="admin-sidebar" aria-label="Меню админки">
<div class="admin-sidebar-brand">
<span class="admin-sidebar-kicker">Разделы</span>
<strong class="admin-sidebar-title">Управление</strong>
</div>
<nav class="admin-nav" id="admin-nav">
<button type="button" class="admin-nav-btn active" data-jump="stats">
<span class="admin-nav-ico" aria-hidden="true"><i class="fa-solid fa-chart-column"></i></span>
Обзор
</button>
<button type="button" class="admin-nav-btn" data-jump="xui">
<span class="admin-nav-ico" aria-hidden="true"><i class="fa-solid fa-shield-halved"></i></span>
WireGuard
</button>
<button type="button" class="admin-nav-btn" data-jump="awg">
<span class="admin-nav-ico" aria-hidden="true"><i class="fa-solid fa-user-secret"></i></span>
AWG 2.0
</button>
<button type="button" class="admin-nav-btn" data-jump="tariffs">
<span class="admin-nav-ico" aria-hidden="true"><i class="fa-solid fa-tags"></i></span>
Тарифы
</button>
<button type="button" class="admin-nav-btn" data-jump="payments">
<span class="admin-nav-ico" aria-hidden="true"><i class="fa-solid fa-credit-card"></i></span>
Платежи
</button>
<button type="button" class="admin-nav-btn" data-jump="smtp">
<span class="admin-nav-ico" aria-hidden="true"><i class="fa-solid fa-envelope"></i></span>
SMTP
</button>
<button type="button" class="admin-nav-btn" data-jump="codes">
<span class="admin-nav-ico" aria-hidden="true"><i class="fa-solid fa-ticket"></i></span>
Коды
</button>
<button type="button" class="admin-nav-btn" data-jump="users">
<span class="admin-nav-ico" aria-hidden="true"><i class="fa-solid fa-users"></i></span>
Пользователи
</button>
<button type="button" class="admin-nav-btn" data-jump="growth">
<span class="admin-nav-ico" aria-hidden="true"><i class="fa-solid fa-arrow-trend-up"></i></span>
Рост
</button>
<button type="button" class="admin-nav-btn" data-jump="db">
<span class="admin-nav-ico" aria-hidden="true"><i class="fa-solid fa-database"></i></span>
База
</button>
<button type="button" class="admin-nav-btn" data-jump="tickets">
<span class="admin-nav-ico" aria-hidden="true"><i class="fa-solid fa-headset"></i></span>
Тикеты
</button>
<button type="button" class="admin-nav-btn" data-jump="ai">
<span class="admin-nav-ico" aria-hidden="true"><i class="fa-solid fa-robot"></i></span>
ИИ
</button>
</nav>
</aside>
<div class="admin-main">
<div class="admin-hello">
<div>
<p class="admin-hello-title" id="admin-hello-title">Панель управления</p>
<p class="muted small" id="admin-hello-sub">Выберите раздел слева</p>
</div>
<div class="admin-hello-hint muted small" id="admin-section-hint">
Обзор статистики и быстрый статус сервиса
</div>
</div>
<div class="admin-views">
<div class="admin-section is-active" id="section-stats" data-section="stats">
<div class="admin-stats-grid" id="admin-stats">
<div class="admin-stat-skeleton">Загрузка статистики…</div>
</div>
</div>
<article class="card admin-card admin-card-xui admin-section" id="section-xui" data-section="xui">
<div class="admin-list-head">
<div class="admin-card-head compact">
<span class="admin-card-icon admin-card-icon--svg" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3 5 6v5c0 5 3.2 8.4 7 9.5 3.8-1.1 7-4.5 7-9.5V6l-7-3z"/></svg>
</span>
<div>
<h2>3x-ui · WireGuard</h2>
<p class="muted small">Панель 3.5.0 · API URL и URL подписки задаются вручную</p>
</div>
</div>
<div class="admin-toolbar">
<button type="button" class="btn ghost small-btn" id="btn-refresh-xui" title="Обновить"></button>
<button type="button" class="btn primary small-btn" id="btn-xui-new">+ Сервер</button>
</div>
</div>
<div id="xui-servers-list" class="xui-servers-list">
<p class="muted small">Загрузка…</p>
</div>
<form id="form-xui" class="auth-form admin-form-grid xui-form hidden">
<input type="hidden" name="id" id="xui-id" value="" />
<label>
Название
<input name="name" id="xui-name" type="text" required maxlength="80" placeholder="NL-WG-1" />
</label>
<label>
API URL панели
<input
name="api_url"
id="xui-api-url"
type="url"
required
placeholder="https://panel.example.com:2053/secret"
/>
</label>
<label class="full-width">
API Token
<input
name="api_key"
id="xui-api-key"
type="password"
autocomplete="off"
placeholder="Bearer token из Settings → Security → API Token"
/>
<span class="muted small" id="xui-api-key-hint">Рекомендуется для 3.5.0 (логин не нужен)</span>
</label>
<label>
Логин <span class="muted small">(если без токена)</span>
<input name="username" id="xui-username" type="text" autocomplete="off" />
</label>
<label>
Пароль
<input
name="password"
id="xui-password"
type="password"
autocomplete="new-password"
placeholder="••••••••"
/>
<span class="muted small" id="xui-password-hint">Только если нет API Token</span>
</label>
<label class="full-width">
URL подписки (base, вручную)
<input
name="subscription_url"
id="xui-subscription-url"
type="url"
placeholder="https://sub.example.com:2096/sub"
/>
<span class="muted small" id="xui-sub-preview">Итоговая ссылка: base/{subId}</span>
</label>
<label class="full-width">
WireGuard inbound
<div class="admin-key-row">
<select name="inbound_id" id="xui-inbound-id" class="admin-select full-select">
<option value="">— сначала проверьте панель —</option>
</select>
<button type="button" class="btn ghost small-btn" id="btn-xui-load-inbounds">Загрузить WG</button>
</div>
<input type="hidden" name="inbound_remark" id="xui-inbound-remark" value="" />
</label>
<label class="admin-check">
<input type="checkbox" name="enabled" id="xui-enabled" checked />
Сервер включён
</label>
<label class="admin-check">
<input type="checkbox" name="verify_ssl" id="xui-verify-ssl" checked />
Проверять SSL
</label>
<div class="full-width admin-ai-actions">
<button type="button" class="btn secondary" id="btn-xui-test">Проверить API</button>
<button type="submit" class="btn primary">Сохранить</button>
<button type="button" class="btn ghost" id="btn-xui-cancel">Отмена</button>
</div>
<pre id="xui-test-out" class="ai-test-out hidden"></pre>
</form>
<div class="xui-wg-settings">
<div class="admin-card-head compact">
<span class="admin-card-icon admin-card-icon--svg" aria-hidden="true">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.7 13.4a2 2 0 0 0 2 1.6h9.7a2 2 0 0 0 2-1.6L23 6H6"/></svg>
</span>
<div>
<h3 class="admin-subhead">Покупка WG-конфигов</h3>
<p class="muted small">При активной подписке — до 10 конфигов. Ползунок включает продажи.</p>
</div>
</div>
<div class="admin-sale-toggle" id="wg-sale-bar">
<div class="admin-sale-toggle-main">
<label class="admin-switch">
<input type="checkbox" id="wg-purchase-enabled" />
<span class="admin-switch-slider" aria-hidden="true"></span>
<span class="admin-switch-text">
<strong>Покупка WG</strong>
<small id="wg-sale-status">Выключена — пользователи не видят блок</small>
</span>
</label>
</div>
<div class="admin-sale-toggle-price">
<label>
Цена, ₽
<input name="price" id="wg-config-price" type="number" min="0" step="1" value="0" />
</label>
<button type="button" class="btn primary small-btn" id="btn-wg-settings-save">Сохранить</button>
</div>
</div>
</div>
</article>
<article class="card admin-card admin-card-awg admin-section" id="section-awg" data-section="awg">
<div class="admin-list-head">
<div class="admin-card-head compact">
<span class="admin-card-icon admin-card-icon--svg" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2 4 6v6c0 5.5 3.4 9.4 8 10.5 4.6-1.1 8-5 8-10.5V6l-8-4z"/><path d="M9 12l2 2 4-4"/></svg>
</span>
<div>
<h2>AmneziaWG 2.0</h2>
<p class="muted small">Панель AmneziaWG · API Token · протокол <code>awg2</code></p>
</div>
</div>
<div class="admin-toolbar">
<button type="button" class="btn ghost small-btn" id="btn-refresh-awg" title="Обновить"></button>
<button type="button" class="btn primary small-btn" id="btn-awg-new">+ Сервер</button>
</div>
</div>
<div class="admin-sale-toggle" id="awg-sale-bar">
<div class="admin-sale-toggle-main">
<label class="admin-switch">
<input type="checkbox" id="awg-purchase-enabled" />
<span class="admin-switch-slider" aria-hidden="true"></span>
<span class="admin-switch-text">
<strong>Покупка AWG</strong>
<small id="awg-sale-status">Выключена — пользователи не видят блок</small>
</span>
</label>
</div>
<div class="admin-sale-toggle-price">
<label>
Цена, ₽
<input name="price" id="awg-config-price" type="number" min="0" step="1" value="0" />
</label>
<button type="button" class="btn primary small-btn" id="btn-awg-settings-save">Сохранить</button>
</div>
</div>
<div id="awg-servers-list" class="xui-servers-list">
<p class="muted small">Загрузка…</p>
</div>
<form id="form-awg" class="auth-form admin-form-grid xui-form hidden">
<input type="hidden" name="id" id="awg-id" value="" />
<label>
Название
<input name="name" id="awg-name" type="text" required maxlength="80" placeholder="NL-AWG-1" />
</label>
<label>
Panel server ID
<input name="panel_server_id" id="awg-panel-server-id" type="number" min="0" step="1" value="0" required />
<span class="muted small">Индекс ноды в панели (0, 1, 2…)</span>
</label>
<label class="full-width">
API URL панели
<input
name="api_url"
id="awg-api-url"
type="url"
required
placeholder="https://panel.example.com"
/>
</label>
<label class="full-width">
API Token
<input
name="api_token"
id="awg-api-token"
type="password"
autocomplete="off"
placeholder="awp_… из Settings → API Tokens"
/>
<span class="muted small" id="awg-api-token-hint">Создайте токен в панели Amnezia Web Panel</span>
</label>
<label>
Протокол
<select name="protocol" id="awg-protocol" class="admin-select full-select">
<option value="awg2">awg2 · AmneziaWG 2.0</option>
<option value="awg">awg · AmneziaWG</option>
<option value="awg_legacy">awg_legacy · Legacy</option>
</select>
</label>
<label class="admin-check">
<input type="checkbox" name="enabled" id="awg-enabled" checked />
Сервер включён
</label>
<label class="admin-check">
<input type="checkbox" name="verify_ssl" id="awg-verify-ssl" checked />
Проверять SSL
</label>
<div class="full-width admin-ai-actions">
<button type="button" class="btn secondary" id="btn-awg-discover">Загрузить ноды</button>
<button type="button" class="btn secondary" id="btn-awg-test">Проверить</button>
<button type="submit" class="btn primary">Сохранить</button>
<button type="button" class="btn ghost" id="btn-awg-cancel">Отмена</button>
</div>
<div id="awg-discover-box" class="full-width hidden"></div>
<pre id="awg-test-out" class="ai-test-out hidden"></pre>
</form>
</article>
<article class="card admin-card admin-section" id="section-tariffs" data-section="tariffs">
<div class="admin-list-head">
<div class="admin-card-head compact">
<span class="admin-card-icon admin-card-icon--svg" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
</span>
<div>
<h2>Тарифы</h2>
<p class="muted small">Создание и удаление · пишется в <code>tariffs.json</code></p>
</div>
</div>
<div class="admin-toolbar">
<button type="button" class="btn ghost small-btn" id="btn-refresh-tariffs" title="Обновить"></button>
<button type="button" class="btn primary small-btn" id="btn-tariff-new">+ Тариф</button>
</div>
</div>
<div id="tariffs-admin-list" class="xui-servers-list">
<p class="muted small">Загрузка…</p>
</div>
<form id="form-tariff" class="auth-form admin-form-grid xui-form hidden">
<input type="hidden" id="tariff-edit-id" value="" />
<label>
ID
<input id="tariff-id" type="text" required minlength="2" maxlength="40" pattern="[A-Za-z0-9_]{2,40}" placeholder="month_1" />
</label>
<label>
Название
<input id="tariff-title" type="text" required maxlength="80" placeholder="1 месяц" />
</label>
<label>
Цена, ₽
<input id="tariff-price" type="number" min="0" step="1" required value="199" />
</label>
<label>
Дней
<input id="tariff-days" type="number" min="1" max="3650" required value="30" />
</label>
<label>
Трафик, ГБ
<input id="tariff-traffic" type="number" min="0" step="1" value="1000" />
</label>
<label>
Устройства
<input id="tariff-devices" type="number" min="1" max="100" value="10" />
</label>
<label class="full-width">
Squad UUID <span class="muted small">(через запятую)</span>
<input id="tariff-squads" type="text" required placeholder="03a5cc01-80b0-426e-9356-d2aa6ec20988" />
</label>
<label class="full-width">
Описание
<input id="tariff-description" type="text" maxlength="300" placeholder="Все серверы · 10 устройств · 1000 ГБ" />
</label>
<div class="full-width admin-ai-actions">
<button type="submit" class="btn primary">Сохранить тариф</button>
<button type="button" class="btn ghost" id="btn-tariff-cancel">Отмена</button>
</div>
</form>
</article>
<article class="card admin-card admin-section" id="section-payments" data-section="payments">
<div class="admin-card-head">
<span class="admin-card-icon admin-card-icon--svg" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="5" width="20" height="14" rx="2"/><path d="M2 10h20"/></svg>
</span>
<div>
<h2>Платёжные системы</h2>
<p class="muted small">Ключи пишутся в <code>.env</code> · применяются сразу без рестарта</p>
</div>
</div>
<p id="payments-env-hint" class="muted small admin-users-summary"></p>
<form id="form-payments" class="auth-form admin-form-grid admin-ai-form">
<h3 class="admin-subhead full-width">Crypto Pay</h3>
<label class="full-width">
CRYPTO_BOT_TOKEN
<div class="admin-key-row">
<input id="pay-crypto-token" type="password" autocomplete="off" placeholder="токен приложения" />
<button type="button" class="btn ghost small-btn" data-clear-secret="pay-crypto-token">Сброс</button>
</div>
<span class="muted small" id="pay-crypto-hint">не задан</span>
</label>
<div class="admin-sale-toggle full-width" id="pay-crypto-testnet-bar">
<div class="admin-sale-toggle-main">
<label class="admin-switch">
<input type="checkbox" id="pay-crypto-testnet" />
<span class="admin-switch-slider" aria-hidden="true"></span>
<span class="admin-switch-text">
<strong>Testnet</strong>
<small>Crypto Pay в тестовом режиме</small>
</span>
</label>
</div>
</div>
<h3 class="admin-subhead full-width">Heleket</h3>
<label>
MERCHANT_ID
<input id="pay-heleket-merchant" type="text" autocomplete="off" />
</label>
<label>
CURRENCY
<input id="pay-heleket-currency" type="text" value="RUB" />
</label>
<label class="full-width">
API_KEY
<div class="admin-key-row">
<input id="pay-heleket-key" type="password" autocomplete="off" />
<button type="button" class="btn ghost small-btn" data-clear-secret="pay-heleket-key">Сброс</button>
</div>
<span class="muted small" id="pay-heleket-hint">не задан</span>
</label>
<label class="full-width">
CALLBACK_URL
<input id="pay-heleket-callback" type="url" placeholder="https://…/api/payments/heleket" />
</label>
<h3 class="admin-subhead full-width">Lava.ru</h3>
<label>
SHOP_ID
<input id="pay-lava-shop" type="text" autocomplete="off" />
</label>
<label>
EXPIRE_MINUTES
<input id="pay-lava-expire" type="number" min="5" max="1440" value="60" />
</label>
<label class="full-width">
SECRET_KEY
<div class="admin-key-row">
<input id="pay-lava-secret" type="password" autocomplete="off" />
<button type="button" class="btn ghost small-btn" data-clear-secret="pay-lava-secret">Сброс</button>
</div>
<span class="muted small" id="pay-lava-secret-hint">не задан</span>
</label>
<label class="full-width">
ADDITIONAL_KEY
<div class="admin-key-row">
<input id="pay-lava-additional" type="password" autocomplete="off" />
<button type="button" class="btn ghost small-btn" data-clear-secret="pay-lava-additional">Сброс</button>
</div>
<span class="muted small" id="pay-lava-additional-hint">не задан</span>
</label>
<label class="full-width">
HOOK_URL
<input id="pay-lava-hook" type="url" placeholder="https://…/api/payments/lava" />
</label>
<label>
SUCCESS_URL
<input id="pay-lava-success" type="url" />
</label>
<label>
FAIL_URL
<input id="pay-lava-fail" type="url" />
</label>
<h3 class="admin-subhead full-width">AnyPay</h3>
<p class="muted small full-width">
В кабинете AnyPay укажите URL оповещения:
<code id="pay-anypay-notify">https://…/api/payments/anypay</code>
</p>
<label>
API_ID
<input id="pay-anypay-api-id" type="text" autocomplete="off" />
</label>
<label>
PROJECT_ID
<input id="pay-anypay-project" type="text" autocomplete="off" />
</label>
<label class="full-width">
API_KEY
<div class="admin-key-row">
<input id="pay-anypay-key" type="password" autocomplete="off" />
<button type="button" class="btn ghost small-btn" data-clear-secret="pay-anypay-key">Сброс</button>
</div>
<span class="muted small" id="pay-anypay-hint">не задан</span>
</label>
<label>
CURRENCY
<input id="pay-anypay-currency" type="text" value="RUB" />
</label>
<label>
METHOD <span class="muted small">(пусто = выбор на странице)</span>
<input id="pay-anypay-method" type="text" placeholder="card / sbp / …" />
</label>
<label>
SUCCESS_URL
<input id="pay-anypay-success" type="url" />
</label>
<label>
FAIL_URL
<input id="pay-anypay-fail" type="url" />
</label>
<label class="full-width">
EMAIL по умолчанию
<input id="pay-anypay-email" type="email" placeholder="pay@example.com" />
</label>
<div class="admin-sale-toggle full-width" id="pay-anypay-ip-bar">
<div class="admin-sale-toggle-main">
<label class="admin-switch">
<input type="checkbox" id="pay-anypay-check-ip" />
<span class="admin-switch-slider" aria-hidden="true"></span>
<span class="admin-switch-text">
<strong>Проверять IP AnyPay</strong>
<small>185.162.128.38 / .39 / .88</small>
</span>
</label>
</div>
</div>
<h3 class="admin-subhead full-width">Digiseller</h3>
<label>
SELLER_ID
<input id="pay-digi-seller" type="text" autocomplete="off" />
</label>
<label>
CREDIT_FROM
<select id="pay-digi-credit" class="admin-select full-select">
<option value="cnt_goods">cnt_goods</option>
<option value="amount">amount</option>
</select>
</label>
<label class="full-width">
API_KEY
<div class="admin-key-row">
<input id="pay-digi-key" type="password" autocomplete="off" />
<button type="button" class="btn ghost small-btn" data-clear-secret="pay-digi-key">Сброс</button>
</div>
<span class="muted small" id="pay-digi-hint">не задан</span>
</label>
<label>
PRODUCT_IDS
<input id="pay-digi-products" type="text" placeholder="123,456" />
</label>
<label>
PAY_URL
<input id="pay-digi-payurl" type="url" />
</label>
<label class="full-width">
CALLBACK_URL
<input id="pay-digi-callback" type="url" placeholder="https://…/api/payments/digiseller" />
</label>
<h3 class="admin-subhead full-width">Stars и коды</h3>
<div class="admin-sale-toggle full-width" id="pay-stars-bar">
<div class="admin-sale-toggle-main">
<label class="admin-switch">
<input type="checkbox" id="pay-stars-enabled" checked />
<span class="admin-switch-slider" aria-hidden="true"></span>
<span class="admin-switch-text">
<strong>Telegram Stars</strong>
<small>Пополнение баланса звёздами</small>
</span>
</label>
</div>
<div class="admin-sale-toggle-price">
<label>
₽ за 1⭐
<input id="pay-stars-rate" type="number" min="0.01" step="0.01" value="1.5" />
</label>
</div>
</div>
<label class="full-width">
TOPUP_CODE_PAY_URL
<input id="pay-topup-url" type="url" placeholder="Ссылка на покупку кода пополнения" />
</label>
<div class="full-width admin-ai-actions">
<button type="submit" class="btn primary">Сохранить в .env</button>
</div>
</form>
</article>
<article class="card admin-card admin-section" id="section-smtp" data-section="smtp">
<div class="admin-card-head">
<span class="admin-card-icon admin-card-icon--svg" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16v12H4z"/><path d="m4 7 8 6 8-6"/></svg>
</span>
<div>
<h2>SMTP · важные письма</h2>
<p class="muted small">Автопродление, напоминания, пополнения — на email пользователя</p>
</div>
</div>
<p id="smtp-env-hint" class="muted small admin-users-summary"></p>
<form id="form-smtp" class="auth-form admin-form-grid admin-ai-form">
<label>
SMTP_HOST
<input id="smtp-host" type="text" placeholder="smtp.example.com" autocomplete="off" />
</label>
<label>
SMTP_PORT
<input id="smtp-port" type="number" min="1" max="65535" value="587" />
</label>
<label>
SMTP_USER
<input id="smtp-user" type="text" autocomplete="off" />
</label>
<label>
SMTP_PASSWORD
<div class="admin-key-row">
<input id="smtp-password" type="password" autocomplete="off" />
<button type="button" class="btn ghost small-btn" data-clear-secret="smtp-password">Сброс</button>
</div>
<span class="muted small" id="smtp-password-hint">не задан</span>
</label>
<label class="full-width">
SMTP_FROM
<input id="smtp-from" type="email" placeholder="noreply@example.com" />
</label>
<div class="admin-sale-toggle full-width" id="smtp-tls-bar">
<div class="admin-sale-toggle-main">
<label class="admin-switch">
<input type="checkbox" id="smtp-tls" checked />
<span class="admin-switch-slider" aria-hidden="true"></span>
<span class="admin-switch-text">
<strong>STARTTLS</strong>
<small>Обычно порт 587</small>
</span>
</label>
</div>
</div>
<div class="admin-sale-toggle full-width" id="smtp-ssl-bar">
<div class="admin-sale-toggle-main">
<label class="admin-switch">
<input type="checkbox" id="smtp-ssl" />
<span class="admin-switch-slider" aria-hidden="true"></span>
<span class="admin-switch-text">
<strong>SSL</strong>
<small>Обычно порт 465 (вместо STARTTLS)</small>
</span>
</label>
</div>
</div>
<label class="full-width">
Тестовый email
<input id="smtp-test-to" type="email" placeholder="куда отправить тест" />
</label>
<div class="full-width admin-ai-actions">
<button type="submit" class="btn primary">Сохранить в .env</button>
<button type="button" class="btn secondary" id="btn-smtp-test">Отправить тест</button>
</div>
</form>
</article>
<div class="admin-grid admin-section" id="section-codes" data-section="codes">
<article class="card admin-card admin-card-generate">
<div class="admin-card-head">
<span class="admin-card-icon admin-card-icon--svg" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>
</span>
<div>
<h2>Создать коды</h2>
<p class="muted small">Одноразовые коды для пополнения баланса</p>
</div>
</div>
<form id="form-generate" class="auth-form admin-form-grid">
<label>
Сумма, ₽
<input name="amount" type="number" min="1" step="1" required value="100" />
</label>
<label>
Количество
<input name="count" type="number" min="1" max="100" required value="5" />
</label>
<label class="full-width">
Комментарий
<input name="note" type="text" maxlength="120" placeholder="web_admin" />
</label>
<button class="btn primary full-width" type="submit">Сгенерировать</button>
</form>
<div id="generated-wrap" class="generated-wrap hidden">
<div class="generated-head">
<span class="muted small">Новые коды — готовы к выдаче</span>
<button type="button" class="btn ghost small-btn" id="btn-copy-codes">Копировать все</button>
</div>
<div id="generated-codes" class="code-output"></div>
</div>
</article>
<article class="card admin-card admin-card-table">
<div class="admin-list-head">
<div class="admin-card-head compact">
<span class="admin-card-icon admin-card-icon--svg" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></svg>
</span>
<div>
<h2>Список кодов</h2>
<p class="muted small">Нажмите на код, чтобы скопировать</p>
</div>
</div>
<div class="admin-toolbar">
<select id="filter-status" class="admin-select" aria-label="Фильтр">
<option value="unused">Свободные</option>
<option value="used">Использованные</option>
<option value="all">Все</option>
</select>
<button type="button" class="btn ghost small-btn" id="btn-refresh-codes">↻ Обновить</button>
</div>
</div>
<div id="codes-table-wrap" class="codes-table-wrap">
<table class="codes-table">
<thead>
<tr>
<th>Код</th>
<th>Сумма</th>
<th>Создан</th>
<th>Статус</th>
</tr>
</thead>
<tbody id="codes-tbody"></tbody>
</table>
</div>
</article>
</div>
<article class="card admin-card admin-card-users admin-section" id="section-users" data-section="users">
<div class="admin-list-head">
<div class="admin-card-head compact">
<span class="admin-card-icon admin-card-icon--svg" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="3"/><path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a3 3 0 0 1 0 5.74"/></svg>
</span>
<div>
<h2>Пользователи</h2>
<p class="muted small">Поиск · баланс · бан · удаление</p>
</div>
</div>
<div class="admin-toolbar admin-toolbar--grow">
<input
type="search"
id="users-search"
class="admin-search"
placeholder="Найти пользователя…"
autocomplete="off"
/>
<button type="button" class="btn ghost small-btn" id="btn-refresh-users" title="Обновить"></button>
</div>
</div>
<p id="users-summary" class="muted small admin-users-summary"></p>
<div class="codes-table-wrap">
<table class="codes-table users-table">
<thead>
<tr>
<th>Пользователь</th>
<th>ID</th>
<th>Баланс</th>
<th></th>
</tr>
</thead>
<tbody id="users-tbody"></tbody>
</table>
</div>
<div class="admin-pagination">
<button type="button" class="btn ghost small-btn" id="btn-users-prev" disabled>← Назад</button>
<span id="users-page-label" class="muted small"></span>
<button type="button" class="btn ghost small-btn" id="btn-users-next" disabled>Далее →</button>
</div>
</article>
<article class="card admin-card admin-section" id="section-growth" data-section="growth">
<div class="admin-card-head">
<div>
<h2>Рост · промо</h2>
<p class="muted small">Рефералка, trial, промокоды, журнал платежей, Postgres dump</p>
</div>
</div>
<form id="form-growth" class="auth-form admin-form-grid">
<label>Бонус рефереру, ₽<input id="growth-ref-referrer" type="number" step="1" min="0" value="50" /></label>
<label>Бонус новичку, ₽<input id="growth-ref-referee" type="number" step="1" min="0" value="50" /></label>
<label>Trial дней<input id="growth-trial-days" type="number" min="0" max="30" value="3" /></label>
<label>Trial tariff id<input id="growth-trial-tariff" type="text" placeholder="опционально" /></label>
<div class="full-width"><button type="submit" class="btn primary">Сохранить</button></div>
</form>
<h3 class="admin-subhead">Промокоды</h3>
<form id="form-promo" class="auth-form admin-form-grid">
<label>Код<input name="code" required maxlength="40" /></label>
<label>Тип
<select name="kind">
<option value="percent">percent %</option>
<option value="fixed">fixed ₽</option>
<option value="days">days +дни</option>
</select>
</label>
<label>Значение<input name="value" type="number" step="0.01" required /></label>
<label>Макс. использований<input name="max_uses" type="number" min="1" placeholder="∞" /></label>
<div class="full-width"><button type="submit" class="btn secondary">Создать промо</button></div>
</form>
<div id="promos-list" class="muted small"></div>
<h3 class="admin-subhead">Журнал платежей</h3>
<button type="button" class="btn ghost small-btn" id="btn-refresh-journal">Обновить</button>
<div id="payments-journal" class="muted small"></div>
<p class="muted small" style="margin-top:12px">
Экспорт / импорт Postgres — в разделе <strong>База</strong> слева.
</p>
</article>
<article class="card admin-card admin-section" id="section-db" data-section="db">
<div class="admin-card-head">
<div>
<h2><i class="fa-solid fa-database"></i> База данных</h2>
<p class="muted small">Экспорт и импорт Postgres · бэкап перед restore делается автоматически</p>
</div>
</div>
<div class="admin-toolbar" style="margin-bottom:12px;gap:8px;flex-wrap:wrap">
<button type="button" class="btn primary small-btn" id="btn-backup-run">
<i class="fa-solid fa-file-export"></i> Создать бэкап
</button>
<button type="button" class="btn ghost small-btn" id="btn-backup-refresh">
<i class="fa-solid fa-rotate"></i> Обновить список
</button>
</div>
<div id="backups-list" class="backup-list muted small"></div>
<h3 class="admin-subhead">Импорт / восстановление</h3>
<p class="muted small">
Восстановление <strong>заменит текущие данные</strong>. Перед импортом создаётся страховочный бэкап.
Принимаются файлы <code>.sql</code> или <code>.sql.gz</code> (дамп pg_dump).
</p>
<form id="form-backup-restore" class="auth-form admin-form-grid" style="margin-top:8px">
<label class="full-width">
Файл дампа
<input type="file" id="backup-file" accept=".sql,.gz,.sql.gz,application/gzip,application/sql" required />
</label>
<label class="admin-check full-width">
<input type="checkbox" id="backup-confirm" required />
<span>Понимаю: данные будут перезаписаны из файла</span>
</label>
<div class="full-width">
<button type="submit" class="btn secondary" id="btn-backup-restore">
<i class="fa-solid fa-file-import"></i> Восстановить из файла
</button>
</div>
</form>
<p id="backup-status" class="muted small" style="margin-top:8px"></p>
</article>
<article class="card admin-card admin-card-tickets admin-section" id="section-tickets" data-section="tickets">
<div class="admin-list-head">
<div class="admin-card-head compact">
<span class="admin-card-icon admin-card-icon--svg" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v3a2 2 0 1 0 0 4v3a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-3a2 2 0 1 0 0-4V7z"/></svg>
</span>
<div>
<h2>Тикеты</h2>
<p class="muted small" id="tickets-stats-line">Поддержка пользователей</p>
</div>
</div>
<div class="admin-toolbar">
<select id="filter-tickets" class="admin-select" aria-label="Фильтр тикетов">
<option value="open">Открытые</option>
<option value="answered">С ответом</option>
<option value="closed">Закрытые</option>
<option value="all">Все</option>
</select>
<button type="button" class="btn ghost small-btn" id="btn-refresh-tickets" title="Обновить"></button>
</div>
</div>
<div class="admin-tickets-layout">
<div id="admin-tickets-list" class="admin-tickets-list"></div>
<div id="admin-ticket-detail" class="admin-ticket-detail hidden">
<div class="admin-ticket-detail-head">
<div>
<h3 id="admin-ticket-title">Тикет</h3>
<p id="admin-ticket-meta" class="muted small"></p>
</div>
<div class="admin-ticket-actions">
<button type="button" class="btn ghost small-btn" id="btn-admin-ticket-close">Закрыть</button>
<button type="button" class="btn ghost small-btn hidden" id="btn-admin-ticket-reopen">Открыть</button>
</div>
</div>
<div id="admin-ticket-messages" class="admin-ticket-messages"></div>
<form id="form-admin-ticket-reply" class="admin-ticket-reply">
<textarea name="body" rows="3" maxlength="2000" required placeholder="Ответ пользователю…"></textarea>
<button type="submit" class="btn primary full">Отправить ответ</button>
</form>
</div>
<div id="admin-ticket-empty" class="admin-ticket-empty">
<p class="empty-title">Выберите тикет</p>
<p class="muted small">Список слева — переписка и ответ появятся здесь</p>
</div>
</div>
</article>
<article class="card admin-card admin-card-ai admin-section" id="section-ai" data-section="ai">
<div class="admin-card-head">
<span class="admin-card-icon admin-card-icon--svg" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
</span>
<div>
<h2>ИИ · OpenRouter</h2>
<p class="muted small">Автоответы в тикетах. Ключ и модели настраиваются здесь.</p>
</div>
</div>
<form id="form-ai" class="auth-form admin-form-grid admin-ai-form">
<div class="admin-sale-toggle full-width" id="ai-enabled-bar">
<div class="admin-sale-toggle-main">
<label class="admin-switch">
<input type="checkbox" name="enabled" id="ai-enabled" />
<span class="admin-switch-slider" aria-hidden="true"></span>
<span class="admin-switch-text">
<strong>ИИ-ответы</strong>
<small id="ai-enabled-status">Выключены</small>
</span>
</label>
</div>
</div>
<div class="admin-sale-toggle full-width" id="ai-auto-bar">
<div class="admin-sale-toggle-main">
<label class="admin-switch">
<input type="checkbox" name="auto_reply" id="ai-auto-reply" checked />
<span class="admin-switch-slider" aria-hidden="true"></span>
<span class="admin-switch-text">
<strong>Автоответ</strong>
<small>На новые сообщения в тикетах</small>
</span>
</label>
</div>
</div>
<label class="full-width">
API-ключ OpenRouter
<div class="admin-key-row">
<input
name="api_key"
id="ai-api-key"
type="password"
autocomplete="off"
placeholder="sk-or-v1-…"
/>
<button type="button" class="btn ghost small-btn" id="btn-ai-clear-key">Сброс</button>
</div>
<span class="muted small" id="ai-key-hint">Ключ не задан</span>
</label>
<label class="full-width">
Активная модель
<select name="model" id="ai-model" class="admin-select full-select"></select>
</label>
<div class="full-width">
<div class="admin-list-head compact-head">
<span class="muted small">Модели для ответов (fallback по порядку)</span>
<div class="admin-toolbar">
<input
type="search"
id="ai-model-search"
class="admin-search"
placeholder="Поиск модели…"
autocomplete="off"
/>
<button type="button" class="btn ghost small-btn" id="btn-ai-load-models">Каталог</button>
</div>
</div>
<div class="admin-key-row" style="margin-bottom:8px">
<input type="text" id="ai-model-custom" placeholder="или id модели: openai/gpt-4o-mini" />
<button type="button" class="btn ghost small-btn" id="btn-ai-add-model">+</button>
</div>
<div id="ai-models-pick" class="ai-models-pick"></div>
<p class="muted small">Отметьте модели — первая в списке и активная используются с fallback.</p>
</div>
<label class="full-width">
System prompt
<textarea name="system_prompt" id="ai-system-prompt" rows="6" maxlength="8000"></textarea>
</label>
<div class="full-width admin-ai-actions">
<button type="submit" class="btn primary">Сохранить</button>
<button type="button" class="btn secondary" id="btn-ai-test">Тест</button>
</div>
<pre id="ai-test-out" class="ai-test-out hidden"></pre>
</form>
</article>
</div>
</div>
<dialog id="dialog-credit" class="admin-dialog">
<form id="form-credit" method="dialog" class="admin-dialog-inner">
<h3>Пополнить баланс</h3>
<p id="credit-user-label" class="muted small"></p>
<input type="hidden" name="telegram_id" id="credit-telegram-id" />
<label>
Сумма, ₽
<input name="amount" type="number" min="1" step="1" required placeholder="100" />
</label>
<label>
Комментарий для пользователя
<textarea
name="comment"
required
maxlength="200"
rows="3"
placeholder="Например: компенсация за простой"
></textarea>
</label>
<p class="muted small">Комментарий виден в истории операций в боте и кабинете.</p>
<div class="admin-dialog-actions">
<button type="button" class="btn ghost" id="btn-credit-cancel">Отмена</button>
<button type="submit" class="btn primary">Зачислить</button>
</div>
</form>
</dialog>
</section>
<div id="admin-toast" class="toast admin-toast hidden" role="status"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="/static/admin.js"></script>
</body>
</html>