diff --git a/aggregator.py b/aggregator.py index f58ea2e..3472e97 100644 --- a/aggregator.py +++ b/aggregator.py @@ -79,6 +79,9 @@ def clear_cache(sub_id: str = None): _links_cache.clear() _traffic_cache.clear() +# Uptime tracker: first_seen per server +_first_seen: Dict[str, float] = {} + def load_json(path: str, default: dict) -> dict: if os.path.exists(path): try: @@ -1035,6 +1038,7 @@ h1{{font-size:24px}} async def home_page(): title = settings.get("general", {}).get("title", "ZernProxy") da_url = settings.get("payments", {}).get("donationalerts", {}).get("url", "") + host = settings.get("general", {}).get("host", "zernmc.ru") conn = get_db() try: @@ -1044,38 +1048,47 @@ async def home_page(): statuses = await fetch_servers_status() - servers_html = "" + srv_cards = "" for i, s in enumerate(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()) - delay = 0.3 + i * 0.15 + srv_name = s.get("server_name", s["name"].upper()) + delay = 0.45 + i * 0.15 + online = s.get("online", False) - svc_tags = "" - for key in ("Caddy", "Minecraft", "Bio site", "Main site", "3x-UI"): - val = chk.get(key, {}).get("value", "") - if val: - if "🟢" in val or "RUN" in val or "OK" in val or "200" in val: - 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'{st} {key}' + inbounds = [ib for ib in servers if ib["name"] == s["name"]] + for srv_cfg in servers: + if srv_cfg["name"] == s["name"]: + all_ib = srv_cfg.get("inbounds", []) + has_free = any(ib.get("is_free", True) for ib in all_ib) + has_paid = any(not ib.get("is_free", True) for ib in all_ib) + break + else: + has_free = True + has_paid = True - servers_html += f''' + if has_free and has_paid: + badge = 'Free + Premium' + elif has_free: + badge = 'Free' + else: + badge = 'Premium' + + uptime_str = s.get("uptime", "") + uptime_html = f'⏱ {uptime_str}' if online else '⏱ Offline' + + srv_cards += f'''
-
{get_flag_emoji(s.get("country",""))} {server_name}
+
{get_flag_emoji(s.get("country",""))} {srv_name} {badge} {uptime_html}
CPU
{f"{cpu:.1f}%" if cpu is not None else "—"}
RAM
{f"{ram:.1f}%" if ram is not None else "—"}
DSK
{f"{disk:.1f}%" if disk is not None else "—"}
NET
{net_raw}
- {f'
{svc_tags}
' if svc_tags else ''}
''' return HTMLResponse(content=f''' @@ -1120,31 +1133,41 @@ body::before {{ }} .container {{ position:relative; z-index:1; max-width:720px; margin:0 auto; padding:48px 20px 32px }} +.section {{ animation:fadeUp .6s ease-out var(--d,0s) both; margin-bottom:40px }} + /* Header */ -.header {{ text-align:center; margin-bottom:36px; animation:fadeUp .7s ease-out forwards }} +.header {{ text-align:center; --d:0.1s }} .header h1 {{ - font-size:32px; font-weight:800; letter-spacing:-.03em; + font-size:34px; font-weight:800; letter-spacing:-.03em; background:linear-gradient(135deg,#fff 30%,rgba(255,255,255,.6)); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }} -.header p {{ color:var(--text-sec); font-size:15px; margin-top:6px; font-weight:400 }} +.header p {{ color:var(--text-sec); font-size:15px; margin-top:8px; line-height:1.6 }} +.header .sub {{ color:var(--text-ter); font-size:13px; margin-top:4px }} -/* 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; +/* Why us */ +.why {{ --d:0.2s }} +.why h2 {{ font-size:18px; font-weight:600; margin-bottom:14px }} +.why-grid {{ display:grid; grid-template-columns:1fr 1fr; gap:10px }} +.w-card {{ background:var(--surface); border:1px solid var(--border); - border-radius:var(--card-rad); padding:18px 16px; text-align:center; + border-radius:12px; padding:14px; backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px); - transition:border-color .3s, transform .3s, box-shadow .3s; + transition:border-color .3s, transform .3s; + animation:fadeUp .5s ease-out calc(var(--d,0s) + var(--i,0)*0.06s) both; }} -.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 }} +.w-card:hover {{ border-color:var(--border-hover); transform:translateY(-2px) }} +.w-card .e {{ font-size:20px; margin-bottom:6px }} +.w-card h4 {{ font-size:13px; font-weight:600; margin-bottom:3px }} +.w-card p {{ font-size:11px; color:var(--text-sec); line-height:1.5 }} -/* Server cards */ +/* Servers */ +.srv-section {{ --d:0.35s }} +.srv-section h2 {{ font-size:18px; font-weight:600; margin-bottom:14px }} .servers {{ display:grid; gap:14px }} + +/* Server card */ .s-card {{ background:var(--surface); border:1px solid var(--border); border-radius:var(--card-rad); padding:18px 20px; @@ -1153,42 +1176,44 @@ body::before {{ 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 {{ font-size:14px; font-weight:600; margin-bottom:14px; display:flex; align-items:center; gap:6px; flex-wrap:wrap }} .s-head .flag {{ font-size:20px; line-height:1 }} +.tier {{ font-size:10px; font-weight:600; padding:2px 8px; border-radius:20px; letter-spacing:.02em }} +.tier.both {{ background:rgba(108,99,255,0.15); color:var(--accent) }} +.tier.free {{ background:rgba(16,185,129,0.12); color:var(--green) }} +.tier.prem {{ background:rgba(245,158,11,0.12); color:var(--amber) }} +.upt {{ font-size:11px; font-weight:500; font-family:'JetBrains Mono',monospace }} +.upt.on {{ color:var(--green) }} +.upt.off {{ color:var(--rose) }} .s-metrics {{ display:grid; gap:10px }} -.m {{ display:grid; grid-template-columns:40px 1fr 50px; align-items:center; gap:10px }} +.m {{ display:grid; grid-template-columns:38px 1fr 48px; 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; + animation:fillBar .9s cubic-bezier(.4,0,.2,1) .5s 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; +.tech {{ --d:0.5s }} +.tech h2 {{ font-size:18px; font-weight:600; margin-bottom:14px }} +.tech-grid {{ + display:grid; grid-template-columns:1fr 1fr; 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 }} +.tech-grid:hover {{ border-color:var(--border-hover) }} +.t-item {{ font-size:12px; color:var(--text-sec); padding:8px 0; display:flex; align-items:center; gap:8px; border-bottom:1px solid var(--border) }} +.t-item:last-child {{ border-bottom:none }} +.t-item .t-dot {{ 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 }} +.btns {{ display:flex; justify-content:center; gap:10px; margin-top:12px; animation:fadeUp .6s .55s ease-out both }} .btn {{ display:inline-flex; align-items:center; gap:6px; padding:12px 28px; border-radius:12px; @@ -1199,10 +1224,8 @@ body::before {{ .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 }} +.footer {{ text-align:center; margin-top:40px; color:var(--text-ter); font-size:12px; animation:fadeUp .6s .6s ease-out both }} -/* Keyframes */ @keyframes fadeUp {{ from {{ opacity:0; transform:translateY(16px) }} to {{ opacity:1; transform:translateY(0) }} @@ -1212,15 +1235,13 @@ body::before {{ 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 }} + .why-grid {{ grid-template-columns:1fr }} .s-card {{ padding:14px }} - .m {{ grid-template-columns:34px 1fr 44px; gap:8px }} - .info-grid {{ grid-template-columns:1fr }} + .m {{ grid-template-columns:32px 1fr 42px; gap:8px }} + .tech-grid {{ grid-template-columns:1fr }} .btns {{ flex-direction:column; align-items:center }} }} @@ -1228,33 +1249,41 @@ body::before {{
-
+

⚡ {title}

-

Быстрый и надёжный VPN-сервис

+

Быстрый VPN с собственными серверами в Европе. Безлимитный трафик, VLESS + XTLS, никаких логов.

+

Подписка на основе подписки · Happ-совместимость · Мгновенная активация

-
-
{total}
Пользователей
-
{len(servers)}
Локаций
-
- -
{servers_html}
- -
-

🔧 Технические детали

-
-VLESS + XTLS Vision / Reality -TCP, WebSocket, gRPC -DDoS-защита на всех серверах -Каналы 1–10 Gbit/s -Zero-log политика -Поддержка Happ (iOS/Android) +
+

Почему {title}

+
+
🚀

Скорость

Каналы 1–10 Gbit/s, собственные серверы, без посредников

+
🔒

Приватность

Zero-log политика — трафик не хранится и не анализируется

+
🌍

Покрытие

Серверы в Германии, Швеции и России — выбирай ближайший

+
💎

Тарифы

Бесплатный доступ к free-серверам или Premium от 150 ₽/мес

+
+

🖥 Состояние серверов

+
{srv_cards}
+
+ +
+

🔧 Технические детали

+
+
VLESS + XTLS Vision / Reality
+
TCP, WebSocket, gRPC
+
DDoS-защита на всех серверах
+
Zero-log политика
+
Поддержка Happ (iOS / Android / Desktop)
+
Автообновление подписки каждые 12ч
+
+
@@ -1265,10 +1294,11 @@ body::before {{ async def fetch_servers_status() -> list: results = [] + now = time.time() 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": {}} + entry = {"name": srv["name"], "country": srv.get("country",""), "checks": {}, "online": False, "uptime": ""} if not url: results.append(entry) continue @@ -1276,8 +1306,22 @@ async def fetch_servers_status() -> list: resp = await client.get(url) if resp.status_code == 200: data = resp.json() + entry["online"] = True entry["server_name"] = data.get("server_name", "") entry["checks"] = data.get("checks", {}) + + name = srv["name"] + if name not in _first_seen: + _first_seen[name] = now + uptime_secs = now - _first_seen[name] + d = int(uptime_secs // 86400) + h = int((uptime_secs % 86400) // 3600) + m = int((uptime_secs % 3600) // 60) + parts = [] + if d > 0: parts.append(f"{d}д") + parts.append(f"{h}ч") + parts.append(f"{m}м") + entry["uptime"] = " ".join(parts) else: entry["checks"]["error"] = {"value": f"HTTP {resp.status_code}"} except Exception as e: