212 lines
9.0 KiB
HTML
212 lines
9.0 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Админка — {{ brand }}</title>
|
|
<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=Bricolage+Grotesque:opsz,wght@12..96,500..800&family=Sora:wght@400;500;600&display=swap" rel="stylesheet" />
|
|
<link rel="stylesheet" href="/static/css/style.css" />
|
|
</head>
|
|
<body class="panel-body">
|
|
<div class="panel-wrap">
|
|
<header class="panel-header">
|
|
<div>
|
|
<h1>Гостевые ссылки</h1>
|
|
<p class="muted">Продление фиксирует трафик тарифа; докупка даёт временные ГБ</p>
|
|
</div>
|
|
<div class="row-actions">
|
|
<a class="btn btn-ghost" href="/">На сайт</a>
|
|
<form method="post" action="/admin/logout">
|
|
<button class="btn btn-danger" type="submit">Выйти</button>
|
|
</form>
|
|
</div>
|
|
</header>
|
|
|
|
{% if flash == "created" %}
|
|
<div class="flash">Ссылка создана</div>
|
|
{% elif flash == "deleted" %}
|
|
<div class="flash">Ссылка удалена</div>
|
|
{% elif flash == "toggled" %}
|
|
<div class="flash">Статус ссылки обновлён</div>
|
|
{% elif flash == "imported" %}
|
|
<div class="flash">База восстановлена из бэкапа</div>
|
|
{% endif %}
|
|
{% if flash_error %}
|
|
<div class="alert-error">Импорт не удался: {{ flash_error }}</div>
|
|
{% endif %}
|
|
|
|
<section class="backup-box">
|
|
<h2 style="font-family:var(--font-display);font-size:1.15rem;margin:0 0 0.5rem;">Бэкап базы</h2>
|
|
<p class="muted" style="margin:0 0 1rem;font-size:0.9rem;">
|
|
Экспорт / импорт всех ссылок, активаций и временного трафика (JSON).
|
|
Импорт <strong>полностью заменяет</strong> текущие данные.
|
|
</p>
|
|
<div class="row-actions">
|
|
<a class="btn btn-signal" href="/admin/backup/export">Скачать бэкап</a>
|
|
</div>
|
|
<form class="backup-import" method="post" action="/admin/backup/import" enctype="multipart/form-data" onsubmit="return confirm('Импорт заменит ВСЕ текущие ссылки и историю. Продолжить?')">
|
|
<label>
|
|
Восстановить из файла
|
|
<input type="file" name="file" accept=".json,application/json" required />
|
|
</label>
|
|
<button class="btn btn-ghost" type="submit">Импортировать</button>
|
|
</form>
|
|
</section>
|
|
|
|
<h2 style="font-family:var(--font-display);font-size:1.25rem;margin:0 0 1rem;">Создать ссылку</h2>
|
|
<form class="form-grid" method="post" action="/admin/links">
|
|
<label>
|
|
Направление
|
|
<select name="link_type" required>
|
|
<option value="extend" {% if tab == "extend" %}selected{% endif %}>Продление VPN (дни)</option>
|
|
<option value="traffic" {% if tab == "traffic" %}selected{% endif %}>Докупка трафика (ГБ)</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Значение
|
|
<input type="number" name="value" min="1" value="30" required />
|
|
<span class="muted" style="font-size:0.75rem;">Продление = дни · Трафик = ГБ бонуса</span>
|
|
</label>
|
|
<label>
|
|
Трафик тарифа (ГБ)
|
|
<input type="number" name="plan_traffic_gb" min="0" value="100" />
|
|
<span class="muted" style="font-size:0.75rem;">Только продление: лимит станет ровно столько (и сбросится счётчик)</span>
|
|
</label>
|
|
<label>
|
|
Срок бонус-трафика (дней)
|
|
<input type="number" name="duration_days" min="0" value="7" />
|
|
<span class="muted" style="font-size:0.75rem;">Только докупка: через N дней бонус-ГБ снимутся</span>
|
|
</label>
|
|
<label>
|
|
Заголовок
|
|
<input type="text" name="title" placeholder="Напр. +50 ГБ на 7 дней" maxlength="120" />
|
|
</label>
|
|
<label>
|
|
Макс. активаций
|
|
<input type="number" name="max_uses" min="1" value="1" required />
|
|
</label>
|
|
<label>
|
|
Срок жизни ссылки (опционально)
|
|
<input type="datetime-local" name="expires_at" />
|
|
</label>
|
|
<label class="full">
|
|
Заметка
|
|
<textarea name="note" rows="2" placeholder="Для себя"></textarea>
|
|
</label>
|
|
<div class="full">
|
|
<button class="btn btn-signal" type="submit">Создать гостевую ссылку</button>
|
|
</div>
|
|
</form>
|
|
|
|
<div class="admin-tabs">
|
|
<a class="admin-tab {% if tab == 'extend' %}is-active{% endif %}" href="/admin?type=extend">
|
|
Продление <span class="tab-count">{{ counts.extend }}</span>
|
|
</a>
|
|
<a class="admin-tab {% if tab == 'traffic' %}is-active{% endif %}" href="/admin?type=traffic">
|
|
Трафик <span class="tab-count">{{ counts.traffic }}</span>
|
|
</a>
|
|
<a class="admin-tab {% if tab == 'all' %}is-active{% endif %}" href="/admin?type=all">
|
|
Все <span class="tab-count">{{ counts.all }}</span>
|
|
</a>
|
|
</div>
|
|
|
|
<p class="muted list-meta">
|
|
Показано {{ links|length }} из {{ total }} · страница {{ page }} / {{ total_pages }}
|
|
</p>
|
|
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Тип</th>
|
|
<th>Заголовок</th>
|
|
<th>Значение</th>
|
|
<th>Использовано</th>
|
|
<th>Ссылка</th>
|
|
<th>Статус</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for link in links %}
|
|
<tr>
|
|
<td><span class="id-pill">#{{ link.id }}</span></td>
|
|
<td>
|
|
{% if link.link_type == "extend" %}
|
|
<span class="badge badge-extend">Продление</span>
|
|
{% else %}
|
|
<span class="badge badge-traffic">Трафик</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ link.title }}</td>
|
|
<td>
|
|
{% if link.link_type == "extend" %}
|
|
+{{ link.value }} дн. / {{ link.plan_traffic_gb or "?" }} ГБ
|
|
{% else %}
|
|
+{{ link.value }} ГБ / {{ link.duration_days or "?" }} дн.
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ link.used_count }} / {{ link.max_uses }}</td>
|
|
<td>
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost btn-copy"
|
|
data-copy="{{ site_base }}/g/{{ link.token }}"
|
|
title="{{ site_base }}/g/{{ link.token }}"
|
|
>Копировать</button>
|
|
</td>
|
|
<td>
|
|
{% if link.is_active %}
|
|
активна
|
|
{% else %}
|
|
<span class="badge badge-off">выкл</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div class="row-actions">
|
|
<a class="btn btn-ghost" href="/admin/links/{{ link.id }}">История</a>
|
|
<form method="post" action="/admin/links/{{ link.id }}/toggle?type={{ tab }}&page={{ page }}">
|
|
<button class="btn btn-ghost" type="submit">{% if link.is_active %}Выкл{% else %}Вкл{% endif %}</button>
|
|
</form>
|
|
<form method="post" action="/admin/links/{{ link.id }}/delete?type={{ tab }}&page={{ page }}" onsubmit="return confirm('Удалить ссылку #{{ link.id }}?')">
|
|
<button class="btn btn-danger" type="submit">Удалить</button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr>
|
|
<td colspan="8" class="muted">В этом разделе пока нет ссылок.</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{% if total_pages > 1 %}
|
|
<nav class="pager">
|
|
{% if page > 1 %}
|
|
<a class="btn btn-ghost" href="/admin?type={{ tab }}&page={{ page - 1 }}">← Назад</a>
|
|
{% endif %}
|
|
{% for p in range(1, total_pages + 1) %}
|
|
{% if p == page %}
|
|
<span class="pager-current">{{ p }}</span>
|
|
{% elif p == 1 or p == total_pages or (p >= page - 2 and p <= page + 2) %}
|
|
<a class="btn btn-ghost" href="/admin?type={{ tab }}&page={{ p }}">{{ p }}</a>
|
|
{% elif p == page - 3 or p == page + 3 %}
|
|
<span class="muted">…</span>
|
|
{% endif %}
|
|
{% endfor %}
|
|
{% if page < total_pages %}
|
|
<a class="btn btn-ghost" href="/admin?type={{ tab }}&page={{ page + 1 }}">Вперёд →</a>
|
|
{% endif %}
|
|
</nav>
|
|
{% endif %}
|
|
</div>
|
|
<script src="/static/js/admin.js"></script>
|
|
</body>
|
|
</html>
|