Files
smolkik-code 7c7c88621d Initial commit: VPN Control Panel
- 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
2026-07-05 17:50:11 +07:00

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>