Add admin link IDs, tabs for extend/traffic, and pagination

This commit is contained in:
orohi
2026-07-25 01:15:54 +03:00
parent 663e9b1d99
commit 7842ddf1b8
4 changed files with 194 additions and 18 deletions
+74 -8
View File
@@ -64,20 +64,62 @@ async def admin_dashboard(request: Request):
return RedirectResponse("/admin/login", status_code=303)
settings = get_settings()
tab = (request.query_params.get("type") or "extend").strip().lower()
if tab not in ("extend", "traffic", "all"):
tab = "extend"
try:
page = max(1, int(request.query_params.get("page") or 1))
except ValueError:
page = 1
per_page = 10
offset = (page - 1) * per_page
db = await get_db()
try:
cursor = await db.execute(
"""
if tab == "all":
count_sql = "SELECT COUNT(*) AS c FROM guest_links"
list_sql = """
SELECT l.*,
(SELECT COUNT(*) FROM guest_claims c WHERE c.link_id = l.id) AS claims
FROM guest_links l
ORDER BY l.id DESC
LIMIT ? OFFSET ?
"""
)
count_args: tuple = ()
list_args: tuple = (per_page, offset)
else:
count_sql = "SELECT COUNT(*) AS c FROM guest_links WHERE link_type = ?"
list_sql = """
SELECT l.*,
(SELECT COUNT(*) FROM guest_claims c WHERE c.link_id = l.id) AS claims
FROM guest_links l
WHERE l.link_type = ?
ORDER BY l.id DESC
LIMIT ? OFFSET ?
"""
count_args = (tab,)
list_args = (tab, per_page, offset)
cursor = await db.execute(count_sql, count_args)
total = int((await cursor.fetchone())["c"])
cursor = await db.execute(list_sql, list_args)
links = [dict(row) for row in await cursor.fetchall()]
cursor = await db.execute(
"SELECT link_type, COUNT(*) AS c FROM guest_links GROUP BY link_type"
)
counts = {"extend": 0, "traffic": 0, "all": 0}
for row in await cursor.fetchall():
counts[row["link_type"]] = int(row["c"])
counts["all"] += int(row["c"])
finally:
await db.close()
total_pages = max(1, (total + per_page - 1) // per_page)
if page > total_pages:
page = total_pages
site_base = public_base_url(request)
return templates.TemplateResponse(
"admin/dashboard.html",
@@ -87,6 +129,12 @@ async def admin_dashboard(request: Request):
"links": links,
"site_base": site_base,
"flash": request.query_params.get("flash"),
"tab": tab,
"page": page,
"per_page": per_page,
"total": total,
"total_pages": total_pages,
"counts": counts,
},
)
@@ -159,11 +207,19 @@ async def create_link(
finally:
await db.close()
return RedirectResponse("/admin?flash=created", status_code=303)
return RedirectResponse(
f"/admin?type={link_type}&flash=created", status_code=303
)
@router.post("/links/{link_id}/toggle")
async def toggle_link(link_id: int, _admin: str = Depends(get_current_admin)):
async def toggle_link(
request: Request,
link_id: int,
_admin: str = Depends(get_current_admin),
):
tab = (request.query_params.get("type") or "extend").strip()
page = request.query_params.get("page") or "1"
db = await get_db()
try:
await db.execute(
@@ -173,18 +229,28 @@ async def toggle_link(link_id: int, _admin: str = Depends(get_current_admin)):
await db.commit()
finally:
await db.close()
return RedirectResponse("/admin?flash=toggled", status_code=303)
return RedirectResponse(
f"/admin?type={tab}&page={page}&flash=toggled", status_code=303
)
@router.post("/links/{link_id}/delete")
async def delete_link(link_id: int, _admin: str = Depends(get_current_admin)):
async def delete_link(
request: Request,
link_id: int,
_admin: str = Depends(get_current_admin),
):
tab = (request.query_params.get("type") or "extend").strip()
page = request.query_params.get("page") or "1"
db = await get_db()
try:
await db.execute("DELETE FROM guest_links WHERE id = ?", (link_id,))
await db.commit()
finally:
await db.close()
return RedirectResponse("/admin?flash=deleted", status_code=303)
return RedirectResponse(
f"/admin?type={tab}&page={page}&flash=deleted", status_code=303
)
@router.get("/links/{link_id}", response_class=HTMLResponse)
+72
View File
@@ -530,6 +530,78 @@ th {
max-width: min(520px, 100%);
}
.admin-tabs {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 0 0 1rem;
border-bottom: 1px solid var(--line);
padding-bottom: 0.75rem;
}
.admin-tab {
display: inline-flex;
align-items: center;
gap: 0.45rem;
padding: 0.55rem 0.9rem;
border: 1px solid var(--line);
color: var(--mist);
font-size: 0.9rem;
}
.admin-tab:hover {
color: var(--fog);
border-color: rgba(212, 255, 63, 0.35);
}
.admin-tab.is-active {
color: var(--ink);
background: var(--signal);
border-color: var(--signal);
}
.tab-count {
font-size: 0.75rem;
opacity: 0.85;
}
.id-pill {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-weight: 600;
color: var(--signal);
}
.list-meta {
margin: 0 0 0.85rem;
font-size: 0.85rem;
}
.pager {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.4rem;
margin-top: 1.25rem;
}
.pager .btn {
padding: 0.4rem 0.7rem;
font-size: 0.8rem;
}
.pager-current {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2rem;
padding: 0.4rem 0.7rem;
background: rgba(212, 255, 63, 0.15);
color: var(--signal);
font-size: 0.85rem;
font-weight: 600;
}
.login-shell {
min-height: 100vh;
display: grid;
+44 -6
View File
@@ -37,8 +37,8 @@
<label>
Направление
<select name="link_type" required>
<option value="extend">Продление VPN (дни)</option>
<option value="traffic">Докупка трафика (ГБ)</option>
<option value="extend" {% if tab == "extend" %}selected{% endif %}>Продление VPN (дни)</option>
<option value="traffic" {% if tab == "traffic" %}selected{% endif %}>Докупка трафика (ГБ)</option>
</select>
</label>
<label>
@@ -49,7 +49,7 @@
<label>
Трафик тарифа (ГБ)
<input type="number" name="plan_traffic_gb" min="0" value="100" />
<span class="muted" style="font-size:0.75rem;">Только продление: лимит станет ровно столько (и сбросится счётчик). Пример: тариф 49₽ → 100</span>
<span class="muted" style="font-size:0.75rem;">Только продление: лимит станет ровно столько (и сбросится счётчик)</span>
</label>
<label>
Срок бонус-трафика (дней)
@@ -77,10 +77,27 @@
</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>
@@ -93,6 +110,7 @@
<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>
@@ -127,10 +145,10 @@
<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">
<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" onsubmit="return confirm('Удалить ссылку?')">
<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>
@@ -138,12 +156,32 @@
</tr>
{% else %}
<tr>
<td colspan="7" class="muted">Пока нет ссылок — создайте первую выше.</td>
<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>
+1 -1
View File
@@ -13,7 +13,7 @@
<div class="panel-wrap">
<header class="panel-header">
<div>
<h1>{{ link.title }}</h1>
<h1>#{{ link.id }} · {{ link.title }}</h1>
<div class="link-copy-row">
<button
type="button"