- FastAPI REST API with JWT auth - aiogram 3 Telegram bot with admin middleware - APScheduler daily tasks (expiry, reminders, revoke, sync) - SQLAlchemy 2 async ORM with Alembic migrations - Jinja2 admin panel (Dashboard, Users, Payments, Servers, Tariffs) - VPN provider abstraction with MockProvider - Stats service with revenue/subscription analytics - Docker Compose (PostgreSQL + Redis + app) - Healthcheck endpoint
70 lines
3.3 KiB
HTML
70 lines
3.3 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{% block title %}VPN Control Panel{% endblock %}</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
display: flex; min-height: 100vh; background: #f5f7fa; color: #1a1a2e;
|
|
}
|
|
.sidebar {
|
|
width: 220px; background: #1a1a2e; color: #fff; padding: 24px 0;
|
|
flex-shrink: 0; min-height: 100vh;
|
|
}
|
|
.sidebar h1 {
|
|
font-size: 16px; padding: 0 20px 20px; border-bottom: 1px solid #2a2a4e;
|
|
margin-bottom: 12px; letter-spacing: 0.3px;
|
|
}
|
|
.sidebar a {
|
|
display: block; padding: 10px 20px; color: #a0a0c0; text-decoration: none;
|
|
font-size: 14px; transition: background .15s, color .15s;
|
|
}
|
|
.sidebar a:hover, .sidebar a.active { background: #2a2a4e; color: #fff; }
|
|
.content { flex: 1; padding: 32px; max-width: 1200px; }
|
|
h2 { font-size: 22px; margin-bottom: 20px; font-weight: 600; }
|
|
table {
|
|
width: 100%; border-collapse: collapse; background: #fff;
|
|
border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,.08);
|
|
}
|
|
th, td { padding: 10px 14px; text-align: left; font-size: 13px; }
|
|
th { background: #f0f2f5; font-weight: 600; color: #555; text-transform: uppercase; font-size: 11px; letter-spacing: .5px; }
|
|
td { border-top: 1px solid #eee; }
|
|
tr:hover td { background: #fafbfc; }
|
|
.badge {
|
|
display: inline-block; padding: 2px 8px; border-radius: 10px;
|
|
font-size: 11px; font-weight: 600;
|
|
}
|
|
.badge-green { background: #e6f7ee; color: #1a8a4a; }
|
|
.badge-red { background: #fde8e8; color: #c53030; }
|
|
.badge-yellow { background: #fef3cd; color: #856404; }
|
|
.badge-blue { background: #e0f0ff; color: #2563eb; }
|
|
.cards {
|
|
display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
gap: 16px; margin-bottom: 28px;
|
|
}
|
|
.card {
|
|
background: #fff; border-radius: 8px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,.08);
|
|
}
|
|
.card .value { font-size: 28px; font-weight: 700; margin-bottom: 4px; }
|
|
.card .label { font-size: 12px; color: #888; text-transform: uppercase; letter-spacing: .5px; }
|
|
.count { font-weight: 600; color: #555; font-size: 13px; margin-bottom: 12px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<nav class="sidebar">
|
|
<h1>VPN Control Panel</h1>
|
|
<a href="/admin/" class="{% if request.url.path == '/admin/' %}active{% endif %}">Dashboard</a>
|
|
<a href="/admin/users" class="{% if request.url.path == '/admin/users' %}active{% endif %}">Users</a>
|
|
<a href="/admin/payments" class="{% if request.url.path == '/admin/payments' %}active{% endif %}">Payments</a>
|
|
<a href="/admin/servers" class="{% if request.url.path == '/admin/servers' %}active{% endif %}">Servers</a>
|
|
<a href="/admin/tariffs" class="{% if request.url.path == '/admin/tariffs' %}active{% endif %}">Tariffs</a>
|
|
</nav>
|
|
<main class="content">
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
</body>
|
|
</html>
|