1088 lines
52 KiB
HTML
1088 lines
52 KiB
HTML
<!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>
|