feat: new home page with server status bars, client count, tech info
This commit is contained in:
+112
-23
@@ -1034,6 +1034,44 @@ h1{{font-size:24px}}
|
|||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def home_page():
|
async def home_page():
|
||||||
title = settings.get("general", {}).get("title", "ZernProxy")
|
title = settings.get("general", {}).get("title", "ZernProxy")
|
||||||
|
da_url = settings.get("payments", {}).get("donationalerts", {}).get("url", "")
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
try:
|
||||||
|
total = conn.execute("SELECT COUNT(*) as c FROM users").fetchone()["c"]
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
statuses = await fetch_servers_status()
|
||||||
|
|
||||||
|
servers_html = ""
|
||||||
|
for s in statuses:
|
||||||
|
chk = s.get("checks", {})
|
||||||
|
cpu = chk.get("CPU", {}).get("value")
|
||||||
|
ram = chk.get("RAM", {}).get("value")
|
||||||
|
disk = chk.get("Disk /", {}).get("value")
|
||||||
|
net_raw = chk.get("Net ↓↑", {}).get("value", "")
|
||||||
|
server_name = s.get("server_name", s["name"].upper())
|
||||||
|
|
||||||
|
service_icons = ""
|
||||||
|
for key in ("Caddy", "Minecraft", "Bio site", "Main site", "3x-UI"):
|
||||||
|
val = chk.get(key, {}).get("value", "")
|
||||||
|
if val:
|
||||||
|
dot = "🟢" if "🟢" in val else ("🔴" if "🔴" in val or "🔴" in str(val) else ("🟡" if "🟡" in val else ""))
|
||||||
|
service_icons += f'<span title="{key}: {val}" style="font-size:14px;margin-right:4px">{dot or "●"}</span>'
|
||||||
|
|
||||||
|
servers_html += f'''
|
||||||
|
<div class="server">
|
||||||
|
<div class="srv-head"><span class="flag">{get_flag_emoji(s.get("country",""))}</span> {server_name}</div>
|
||||||
|
<div class="srv-body">
|
||||||
|
<div class="metric"><span class="ml">CPU</span><div class="mb"><div class="mf" style="width:{cpu if cpu is not None else 0}%"></div></div><span class="mv">{f"{cpu:.1f}%" if cpu is not None else "—"}</span></div>
|
||||||
|
<div class="metric"><span class="ml">RAM</span><div class="mb"><div class="mf" style="width:{ram if ram is not None else 0}%"></div></div><span class="mv">{f"{ram:.1f}%" if ram is not None else "—"}</span></div>
|
||||||
|
<div class="metric"><span class="ml">DISK</span><div class="mb"><div class="mf" style="width:{disk if disk is not None else 0}%"></div></div><span class="mv">{f"{disk:.1f}%" if disk is not None else "—"}</span></div>
|
||||||
|
<div class="metric"><span class="ml">NET</span><div class="mb" style="background:none;padding:0"><span style="font-size:12px;color:#aaa;font-family:monospace">{net_raw}</span></div></div>
|
||||||
|
</div>
|
||||||
|
{f'<div class="srv-svc">{service_icons}</div>' if service_icons else ''}
|
||||||
|
</div>'''
|
||||||
|
|
||||||
return HTMLResponse(content=f'''<!DOCTYPE html>
|
return HTMLResponse(content=f'''<!DOCTYPE html>
|
||||||
<html lang="ru">
|
<html lang="ru">
|
||||||
<head>
|
<head>
|
||||||
@@ -1043,39 +1081,90 @@ async def home_page():
|
|||||||
<style>
|
<style>
|
||||||
*{{margin:0;padding:0;box-sizing:border-box}}
|
*{{margin:0;padding:0;box-sizing:border-box}}
|
||||||
body{{font-family:Roboto,sans-serif;background:linear-gradient(135deg,#1a1a2e,#16213e);min-height:100vh;color:#fff}}
|
body{{font-family:Roboto,sans-serif;background:linear-gradient(135deg,#1a1a2e,#16213e);min-height:100vh;color:#fff}}
|
||||||
.container{{max-width:700px;margin:0 auto;padding:40px 20px;text-align:center}}
|
.container{{max-width:800px;margin:0 auto;padding:30px 16px}}
|
||||||
.logo{{font-size:64px;margin-bottom:20px}}
|
.header{{text-align:center;margin-bottom:24px}}
|
||||||
h1{{font-size:32px;font-weight:500;margin-bottom:16px}}
|
.header h1{{font-size:26px;font-weight:500;margin-bottom:6px}}
|
||||||
p{{color:#aaa;font-size:16px;line-height:1.6;margin-bottom:32px}}
|
.header p{{color:#888;font-size:14px}}
|
||||||
.btn{{display:inline-block;padding:14px 32px;background:#4CAF50;color:#fff;text-decoration:none;border-radius:12px;font-size:16px;font-weight:500;margin:8px;transition:.3s}}
|
.stats{{display:flex;gap:12px;justify-content:center;margin-bottom:28px;flex-wrap:wrap}}
|
||||||
.btn:hover{{background:#45a049;transform:translateY(-2px)}}
|
.stat{{background:rgba(255,255,255,0.04);border-radius:12px;padding:14px 22px;text-align:center;border:1px solid rgba(255,255,255,0.06)}}
|
||||||
.cards{{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-top:40px}}
|
.stat .n{{font-size:26px;font-weight:500}}
|
||||||
.card{{background:rgba(255,255,255,0.05);border-radius:16px;padding:24px;border:1px solid rgba(255,255,255,0.1)}}
|
.stat .l{{font-size:12px;color:#888;margin-top:2px}}
|
||||||
.card .icon{{font-size:32px;margin-bottom:12px}}
|
.servers{{display:grid;gap:12px}}
|
||||||
.card h3{{font-size:16px;margin-bottom:8px}}
|
.server{{background:rgba(255,255,255,0.04);border-radius:14px;padding:14px 16px;border:1px solid rgba(255,255,255,0.06)}}
|
||||||
.card p{{font-size:13px;color:#888}}
|
.srv-head{{font-size:14px;font-weight:500;margin-bottom:10px}}
|
||||||
.footer{{margin-top:60px;color:#555;font-size:13px}}
|
.srv-head .flag{{font-size:18px;margin-right:6px}}
|
||||||
|
.srv-body{{display:grid;gap:7px}}
|
||||||
|
.metric{{display:grid;grid-template-columns:42px 1fr 52px;align-items:center;gap:10px}}
|
||||||
|
.ml{{font-size:12px;color:#888;font-weight:500}}
|
||||||
|
.mb{{height:8px;background:rgba(255,255,255,0.08);border-radius:4px;overflow:hidden}}
|
||||||
|
.mf{{height:100%;border-radius:4px;background:linear-gradient(90deg,#4CAF50,#8BC34A);transition:width .5s}}
|
||||||
|
.mv{{font-size:11px;color:#888;text-align:right;font-family:monospace}}
|
||||||
|
.srv-svc{{margin-top:8px;padding-top:8px;border-top:1px solid rgba(255,255,255,0.05)}}
|
||||||
|
.info{{margin-top:28px;background:rgba(255,255,255,0.03);border-radius:14px;padding:16px;border:1px solid rgba(255,255,255,0.06)}}
|
||||||
|
.info h3{{font-size:14px;font-weight:500;margin-bottom:8px}}
|
||||||
|
.info p{{font-size:12px;color:#888;line-height:1.7}}
|
||||||
|
.info p span{{color:#aaa}}
|
||||||
|
.btns{{text-align:center;margin-top:24px}}
|
||||||
|
.btn{{display:inline-block;padding:11px 24px;background:#4CAF50;color:#fff;text-decoration:none;border-radius:10px;font-size:13px;margin:4px;transition:.3s}}
|
||||||
|
.btn:hover{{background:#45a049}}
|
||||||
|
.btn.da{{background:#E91E63}}
|
||||||
|
.btn.da:hover{{background:#c2185b}}
|
||||||
|
.footer{{text-align:center;margin-top:28px;color:#444;font-size:12px}}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="logo">⚡</div>
|
<div class="header">
|
||||||
<h1>{title}</h1>
|
<h1>⚡ {title}</h1>
|
||||||
<p>Быстрый и надёжный VPN. Подписка на основе подписки. Безлимитный трафик на всех тарифах.</p>
|
<p>Быстрый и надёжный VPN-сервис</p>
|
||||||
<div class="cards">
|
|
||||||
<div class="card"><div class="icon">🆓</div><h3>Free</h3><p>Базовый доступ к серверам. Безлимитный трафик.</p></div>
|
|
||||||
<div class="card"><div class="icon">🧪</div><h3>Test 7 дней</h3><p>Пробный период за 50₽. 5GB трафика.</p></div>
|
|
||||||
<div class="card"><div class="icon">⭐</div><h3>Premium</h3><p>Полный доступ, приоритетные серверы. От 150₽/мес.</p></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top:32px">
|
<div class="stats">
|
||||||
<a href="/admin/users" class="btn">👤 Панель управления</a>
|
<div class="stat"><div class="n">{total}</div><div class="l">👥 Клиентов</div></div>
|
||||||
<a href="{settings.get("payments",{}).get("donationalerts",{}).get("url","#")}" class="btn" style="background:#E91E63">❤️ Поддержать проект</a>
|
<div class="stat"><div class="n">{len(servers)}</div><div class="l">🌍 Локаций</div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">© 2026 {title} · <a href="/admin/users" style="color:#555">admin</a></div>
|
<div class="servers">{servers_html}</div>
|
||||||
|
<div class="info">
|
||||||
|
<h3>🔧 Технические детали</h3>
|
||||||
|
<p>
|
||||||
|
<span>🔒</span> Шифрование: VLESS + XTLS Vision / Reality<br>
|
||||||
|
<span>⚡</span> Протоколы: TCP, WebSocket, gRPC<br>
|
||||||
|
<span>🛡️</span> DDoS-защита на всех серверах<br>
|
||||||
|
<span>📡</span> Каналы: 1-10 Gbit/s<br>
|
||||||
|
<span>🔐</span> Zero-log политика — трафик не логируется<br>
|
||||||
|
<span>🌐</span> Поддержка Happ (iOS/Android/Desktop)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="btns">
|
||||||
|
<a href="{da_url}" class="btn da">❤️ Поддержать проект</a>
|
||||||
|
</div>
|
||||||
|
<div class="footer">© 2026 {title}</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>''')
|
</html>''')
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_servers_status() -> list:
|
||||||
|
results = []
|
||||||
|
async with httpx.AsyncClient(timeout=8.0, verify=False) as client:
|
||||||
|
for srv in servers:
|
||||||
|
url = srv.get("status_url", "")
|
||||||
|
entry = {"name": srv["name"], "country": srv.get("country",""), "checks": {}}
|
||||||
|
if not url:
|
||||||
|
results.append(entry)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
resp = await client.get(url)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
data = resp.json()
|
||||||
|
entry["server_name"] = data.get("server_name", "")
|
||||||
|
entry["checks"] = data.get("checks", {})
|
||||||
|
else:
|
||||||
|
entry["checks"]["error"] = {"value": f"HTTP {resp.status_code}"}
|
||||||
|
except Exception as e:
|
||||||
|
entry["checks"]["error"] = {"value": str(e)[:50]}
|
||||||
|
results.append(entry)
|
||||||
|
return results
|
||||||
|
|
||||||
@app.get("/webhook/tg")
|
@app.get("/webhook/tg")
|
||||||
async def tg_webhook():
|
async def tg_webhook():
|
||||||
bot_cfg = settings.get("bot", {})
|
bot_cfg = settings.get("bot", {})
|
||||||
|
|||||||
Reference in New Issue
Block a user