feat: redesign home page — Material glassmorphism, staggered animations, custom typography

This commit is contained in:
SashegDev
2026-05-20 18:26:27 +00:00
parent a918f669e5
commit 6bb8cdbe18
+173 -53
View File
@@ -1045,31 +1045,37 @@ async def home_page():
statuses = await fetch_servers_status() statuses = await fetch_servers_status()
servers_html = "" servers_html = ""
for s in statuses: for i, s in enumerate(statuses):
chk = s.get("checks", {}) chk = s.get("checks", {})
cpu = chk.get("CPU", {}).get("value") cpu = chk.get("CPU", {}).get("value")
ram = chk.get("RAM", {}).get("value") ram = chk.get("RAM", {}).get("value")
disk = chk.get("Disk /", {}).get("value") disk = chk.get("Disk /", {}).get("value")
net_raw = chk.get("Net ↓↑", {}).get("value", "") net_raw = chk.get("Net ↓↑", {}).get("value", "")
server_name = s.get("server_name", s["name"].upper()) server_name = s.get("server_name", s["name"].upper())
delay = 0.3 + i * 0.15
service_icons = "" svc_tags = ""
for key in ("Caddy", "Minecraft", "Bio site", "Main site", "3x-UI"): for key in ("Caddy", "Minecraft", "Bio site", "Main site", "3x-UI"):
val = chk.get(key, {}).get("value", "") val = chk.get(key, {}).get("value", "")
if val: if val:
dot = "🟢" if "🟢" in val else ("🔴" if "🔴" in val or "🔴" in str(val) else ("🟡" if "🟡" in val else "")) if "🟢" in val or "RUN" in val or "OK" in val or "200" in val:
service_icons += f'<span title="{key}: {val}" style="font-size:14px;margin-right:4px">{dot or ""}</span>' cl, st = "ok", "🟢"
elif "🔴" in val or "DOWN" in val or "502" in val or "503" in val or "error" in val.lower():
cl, st = "err", "🔴"
else:
cl, st = "warn", "🟡"
svc_tags += f'<span class="svc {cl}" title="{key}: {val}">{st} {key}</span>'
servers_html += f''' servers_html += f'''
<div class="server"> <div class="s-card" style="--d: {delay}s">
<div class="srv-head"><span class="flag">{get_flag_emoji(s.get("country",""))}</span> {server_name}</div> <div class="s-head"><span class="flag">{get_flag_emoji(s.get("country",""))}</span> {server_name}</div>
<div class="srv-body"> <div class="s-metrics">
<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="m"><span class="m-l">CPU</span><div class="m-t"><div class="m-f" style="--w: {cpu if cpu is not None else 0}%"></div></div><span class="m-v">{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="m"><span class="m-l">RAM</span><div class="m-t"><div class="m-f" style="--w: {ram if ram is not None else 0}%"></div></div><span class="m-v">{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="m"><span class="m-l">DSK</span><div class="m-t"><div class="m-f" style="--w: {disk if disk is not None else 0}%"></div></div><span class="m-v">{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 class="m"><span class="m-l">NET</span><div class="m-t" style="background:none;align-items:center"><span class="m-net">{net_raw}</span></div></div>
</div> </div>
{f'<div class="srv-svc">{service_icons}</div>' if service_icons else ''} {f'<div class="s-svc">{svc_tags}</div>' if svc_tags else ''}
</div>''' </div>'''
return HTMLResponse(content=f'''<!DOCTYPE html> return HTMLResponse(content=f'''<!DOCTYPE html>
@@ -1077,67 +1083,181 @@ async def home_page():
<head> <head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>{title}</title> <title>{title}</title>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style> <style>
:root {{
--bg: #080c18;
--surface: rgba(255,255,255,0.03);
--border: rgba(255,255,255,0.06);
--border-hover: rgba(255,255,255,0.12);
--text: #fff;
--text-sec: rgba(255,255,255,0.5);
--text-ter: rgba(255,255,255,0.3);
--accent: #6C63FF;
--green: #10b981;
--amber: #f59e0b;
--rose: #f43f5e;
--card-rad: 16px;
}}
* {{ 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}} html {{ scroll-behavior:smooth }}
.container{{max-width:800px;margin:0 auto;padding:30px 16px}} body {{
.header{{text-align:center;margin-bottom:24px}} font-family:'Plus Jakarta Sans',sans-serif;
.header h1{{font-size:26px;font-weight:500;margin-bottom:6px}} background:var(--bg);
.header p{{color:#888;font-size:14px}} color:var(--text);
.stats{{display:flex;gap:12px;justify-content:center;margin-bottom:28px;flex-wrap:wrap}} min-height:100vh;
.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)}} overflow-x:hidden;
.stat .n{{font-size:26px;font-weight:500}} }}
.stat .l{{font-size:12px;color:#888;margin-top:2px}} body::before {{
.servers{{display:grid;gap:12px}} content:'';
.server{{background:rgba(255,255,255,0.04);border-radius:14px;padding:14px 16px;border:1px solid rgba(255,255,255,0.06)}} position:fixed;inset:0;
.srv-head{{font-size:14px;font-weight:500;margin-bottom:10px}} background:
.srv-head .flag{{font-size:18px;margin-right:6px}} radial-gradient(ellipse 80% 50% at 10% 0%, rgba(108,99,255,0.08) 0%, transparent 70%),
.srv-body{{display:grid;gap:7px}} radial-gradient(ellipse 60% 40% at 90% 100%, rgba(16,185,129,0.06) 0%, transparent 70%);
.metric{{display:grid;grid-template-columns:42px 1fr 52px;align-items:center;gap:10px}} pointer-events:none;
.ml{{font-size:12px;color:#888;font-weight:500}} z-index:0;
.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}} .container {{ position:relative; z-index:1; max-width:720px; margin:0 auto; padding:48px 20px 32px }}
.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)}} /* Header */
.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)}} .header {{ text-align:center; margin-bottom:36px; animation:fadeUp .7s ease-out forwards }}
.info h3{{font-size:14px;font-weight:500;margin-bottom:8px}} .header h1 {{
.info p{{font-size:12px;color:#888;line-height:1.7}} font-size:32px; font-weight:800; letter-spacing:-.03em;
.info p span{{color:#aaa}} background:linear-gradient(135deg,#fff 30%,rgba(255,255,255,.6));
.btns{{text-align:center;margin-top:24px}} -webkit-background-clip:text; -webkit-text-fill-color:transparent;
.btn{{display:inline-block;padding:11px 24px;background:#4CAF50;color:#fff;text-decoration:none;border-radius:10px;font-size:13px;margin:4px;transition:.3s}} background-clip:text;
.btn:hover{{background:#45a049}} }}
.btn.da{{background:#E91E63}} .header p {{ color:var(--text-sec); font-size:15px; margin-top:6px; font-weight:400 }}
.btn.da:hover{{background:#c2185b}}
.footer{{text-align:center;margin-top:28px;color:#444;font-size:12px}} /* Stats row */
.stats {{ display:flex; gap:12px; justify-content:center; margin-bottom:36px; animation:fadeUp .7s .1s ease-out both }}
.stat {{
flex:1; max-width:180px;
background:var(--surface); border:1px solid var(--border);
border-radius:var(--card-rad); padding:18px 16px; text-align:center;
backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px);
transition:border-color .3s, transform .3s, box-shadow .3s;
}}
.stat:hover {{ border-color:var(--border-hover); transform:translateY(-2px); box-shadow:0 8px 40px rgba(108,99,255,0.08) }}
.stat .n {{ font-size:28px; font-weight:700; letter-spacing:-.02em }}
.stat .l {{ font-size:12px; color:var(--text-sec); margin-top:4px; font-weight:500 }}
/* Server cards */
.servers {{ display:grid; gap:14px }}
.s-card {{
background:var(--surface); border:1px solid var(--border);
border-radius:var(--card-rad); padding:18px 20px;
backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px);
transition:border-color .4s, transform .4s, box-shadow .4s;
animation:fadeUp .6s ease-out var(--d,0s) both;
}}
.s-card:hover {{ border-color:var(--border-hover); transform:translateY(-3px); box-shadow:0 12px 48px rgba(108,99,255,0.06) }}
.s-head {{ font-size:15px; font-weight:600; margin-bottom:14px; display:flex; align-items:center; gap:8px }}
.s-head .flag {{ font-size:20px; line-height:1 }}
.s-metrics {{ display:grid; gap:10px }}
.m {{ display:grid; grid-template-columns:40px 1fr 50px; align-items:center; gap:10px }}
.m-l {{ font-size:11px; font-weight:600; color:var(--text-sec); letter-spacing:.04em }}
.m-t {{ height:6px; background:rgba(255,255,255,0.06); border-radius:3px; overflow:hidden; display:flex }}
.m-f {{
width:0; height:100%; border-radius:3px;
background:linear-gradient(90deg,var(--accent),var(--green));
animation:fillBar .9s cubic-bezier(.4,0,.2,1) .4s forwards;
}}
.m-v {{ font-family:'JetBrains Mono',monospace; font-size:11px; color:var(--text-sec); text-align:right }}
.m-net {{ font-family:'JetBrains Mono',monospace; font-size:11px; color:var(--text-ter); white-space:nowrap }}
.s-svc {{ margin-top:12px; padding-top:12px; border-top:1px solid var(--border); display:flex; gap:8px; flex-wrap:wrap }}
.svc {{ font-size:11px; padding:3px 8px; border-radius:6px; background:rgba(255,255,255,0.04); font-weight:500 }}
.svc.ok {{ color:var(--green) }}
.svc.err {{ color:var(--rose) }}
.svc.warn {{ color:var(--amber) }}
/* Tech info */
.info {{
margin-top:28px;
background:var(--surface); border:1px solid var(--border);
border-radius:var(--card-rad); padding:20px;
backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px);
animation:fadeUp .6s .5s ease-out both;
transition:border-color .3s;
}}
.info:hover {{ border-color:var(--border-hover) }}
.info h3 {{ font-size:14px; font-weight:600; margin-bottom:12px; color:var(--text) }}
.info-grid {{ display:grid; grid-template-columns:1fr 1fr; gap:8px 16px }}
.info-grid span {{ font-size:12px; color:var(--text-sec); line-height:1.8; display:flex; align-items:center; gap:6px }}
.info-grid span::before {{ content:''; display:inline-block; width:4px; height:4px; border-radius:2px; background:var(--accent); flex-shrink:0 }}
/* Buttons */
.btns {{ display:flex; justify-content:center; gap:10px; margin-top:28px; animation:fadeUp .6s .6s ease-out both }}
.btn {{
display:inline-flex; align-items:center; gap:6px;
padding:12px 28px; border-radius:12px;
font-size:14px; font-weight:600; text-decoration:none;
transition:transform .3s, box-shadow .3s;
cursor:pointer;
}}
.btn:hover {{ transform:translateY(-2px); box-shadow:0 8px 32px rgba(108,99,255,0.2) }}
.btn.da {{ background:linear-gradient(135deg,#f43f5e,#e11d48); color:#fff }}
/* Footer */
.footer {{ text-align:center; margin-top:36px; color:var(--text-ter); font-size:12px; animation:fadeUp .6s .7s ease-out both }}
/* Keyframes */
@keyframes fadeUp {{
from {{ opacity:0; transform:translateY(16px) }}
to {{ opacity:1; transform:translateY(0) }}
}}
@keyframes fillBar {{
from {{ width:0% }}
to {{ width:var(--w) }}
}}
/* Responsive */
@media(max-width:520px) {{
.container {{ padding:32px 14px }}
.header h1 {{ font-size:24px }}
.stat {{ max-width:none; padding:14px 12px }}
.stat .n {{ font-size:22px }}
.s-card {{ padding:14px }}
.m {{ grid-template-columns:34px 1fr 44px; gap:8px }}
.info-grid {{ grid-template-columns:1fr }}
.btns {{ flex-direction:column; align-items:center }}
}}
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<div class="header"> <div class="header">
<h1>⚡ {title}</h1> <h1>⚡ {title}</h1>
<p>Быстрый и надёжный VPN-сервис</p> <p>Быстрый и надёжный VPN-сервис</p>
</div> </div>
<div class="stats"> <div class="stats">
<div class="stat"><div class="n">{total}</div><div class="l">👥 Клиентов</div></div> <div class="stat"><div class="n">{total}</div><div class="l">Пользователей</div></div>
<div class="stat"><div class="n">{len(servers)}</div><div class="l">🌍 Локаций</div></div> <div class="stat"><div class="n">{len(servers)}</div><div class="l">Локаций</div></div>
</div> </div>
<div class="servers">{servers_html}</div> <div class="servers">{servers_html}</div>
<div class="info"> <div class="info">
<h3>🔧 Технические детали</h3> <h3>🔧 Технические детали</h3>
<p> <div class="info-grid">
<span>🔒</span> Шифрование: VLESS + XTLS Vision / Reality<br> <span>VLESS + XTLS Vision / Reality</span>
<span>⚡</span> Протоколы: TCP, WebSocket, gRPC<br> <span>TCP, WebSocket, gRPC</span>
<span>🛡️</span> DDoS-защита на всех серверах<br> <span>DDoS-защита на всех серверах</span>
<span>📡</span> Каналы: 1-10 Gbit/s<br> <span>Каналы 110 Gbit/s</span>
<span>🔐</span> Zero-log политика — трафик не логируется<br> <span>Zero-log политика</span>
<span>🌐</span> Поддержка Happ (iOS/Android/Desktop) <span>Поддержка Happ (iOS/Android)</span>
</p>
</div> </div>
</div>
<div class="btns"> <div class="btns">
<a href="{da_url}" class="btn da">❤️ Поддержать проект</a> <a href="{da_url}" class="btn da">❤️ Поддержать проект</a>
</div> </div>
<div class="footer">© 2026 {title}</div> <div class="footer">© 2026 {title}</div>
</div> </div>
</body> </body>
</html>''') </html>''')