feat: restructure home page into 4 sections, add uptime tracking, remove service icons
This commit is contained in:
+117
-73
@@ -79,6 +79,9 @@ def clear_cache(sub_id: str = None):
|
|||||||
_links_cache.clear()
|
_links_cache.clear()
|
||||||
_traffic_cache.clear()
|
_traffic_cache.clear()
|
||||||
|
|
||||||
|
# Uptime tracker: first_seen per server
|
||||||
|
_first_seen: Dict[str, float] = {}
|
||||||
|
|
||||||
def load_json(path: str, default: dict) -> dict:
|
def load_json(path: str, default: dict) -> dict:
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
try:
|
try:
|
||||||
@@ -1035,6 +1038,7 @@ h1{{font-size:24px}}
|
|||||||
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", "")
|
da_url = settings.get("payments", {}).get("donationalerts", {}).get("url", "")
|
||||||
|
host = settings.get("general", {}).get("host", "zernmc.ru")
|
||||||
|
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
try:
|
try:
|
||||||
@@ -1044,38 +1048,47 @@ async def home_page():
|
|||||||
|
|
||||||
statuses = await fetch_servers_status()
|
statuses = await fetch_servers_status()
|
||||||
|
|
||||||
servers_html = ""
|
srv_cards = ""
|
||||||
for i, s in enumerate(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())
|
srv_name = s.get("server_name", s["name"].upper())
|
||||||
delay = 0.3 + i * 0.15
|
delay = 0.45 + i * 0.15
|
||||||
|
online = s.get("online", False)
|
||||||
|
|
||||||
svc_tags = ""
|
inbounds = [ib for ib in servers if ib["name"] == s["name"]]
|
||||||
for key in ("Caddy", "Minecraft", "Bio site", "Main site", "3x-UI"):
|
for srv_cfg in servers:
|
||||||
val = chk.get(key, {}).get("value", "")
|
if srv_cfg["name"] == s["name"]:
|
||||||
if val:
|
all_ib = srv_cfg.get("inbounds", [])
|
||||||
if "🟢" in val or "RUN" in val or "OK" in val or "200" in val:
|
has_free = any(ib.get("is_free", True) for ib in all_ib)
|
||||||
cl, st = "ok", "🟢"
|
has_paid = any(not ib.get("is_free", True) for ib in all_ib)
|
||||||
elif "🔴" in val or "DOWN" in val or "502" in val or "503" in val or "error" in val.lower():
|
break
|
||||||
cl, st = "err", "🔴"
|
|
||||||
else:
|
else:
|
||||||
cl, st = "warn", "🟡"
|
has_free = True
|
||||||
svc_tags += f'<span class="svc {cl}" title="{key}: {val}">{st} {key}</span>'
|
has_paid = True
|
||||||
|
|
||||||
servers_html += f'''
|
if has_free and has_paid:
|
||||||
|
badge = '<span class="tier both">Free + Premium</span>'
|
||||||
|
elif has_free:
|
||||||
|
badge = '<span class="tier free">Free</span>'
|
||||||
|
else:
|
||||||
|
badge = '<span class="tier prem">Premium</span>'
|
||||||
|
|
||||||
|
uptime_str = s.get("uptime", "")
|
||||||
|
uptime_html = f'<span class="upt on">⏱ {uptime_str}</span>' if online else '<span class="upt off">⏱ Offline</span>'
|
||||||
|
|
||||||
|
srv_cards += f'''
|
||||||
<div class="s-card" style="--d: {delay}s">
|
<div class="s-card" style="--d: {delay}s">
|
||||||
<div class="s-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> {srv_name} {badge} {uptime_html}</div>
|
||||||
<div class="s-metrics">
|
<div class="s-metrics">
|
||||||
<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="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="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="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="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="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="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 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="s-svc">{svc_tags}</div>' if svc_tags else ''}
|
|
||||||
</div>'''
|
</div>'''
|
||||||
|
|
||||||
return HTMLResponse(content=f'''<!DOCTYPE html>
|
return HTMLResponse(content=f'''<!DOCTYPE html>
|
||||||
@@ -1120,31 +1133,41 @@ body::before {{
|
|||||||
}}
|
}}
|
||||||
.container {{ position:relative; z-index:1; max-width:720px; margin:0 auto; padding:48px 20px 32px }}
|
.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 */
|
||||||
.header {{ text-align:center; margin-bottom:36px; animation:fadeUp .7s ease-out forwards }}
|
.header {{ text-align:center; --d:0.1s }}
|
||||||
.header h1 {{
|
.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));
|
background:linear-gradient(135deg,#fff 30%,rgba(255,255,255,.6));
|
||||||
-webkit-background-clip:text; -webkit-text-fill-color:transparent;
|
-webkit-background-clip:text; -webkit-text-fill-color:transparent;
|
||||||
background-clip:text;
|
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 */
|
/* Why us */
|
||||||
.stats {{ display:flex; gap:12px; justify-content:center; margin-bottom:36px; animation:fadeUp .7s .1s ease-out both }}
|
.why {{ --d:0.2s }}
|
||||||
.stat {{
|
.why h2 {{ font-size:18px; font-weight:600; margin-bottom:14px }}
|
||||||
flex:1; max-width:180px;
|
.why-grid {{ display:grid; grid-template-columns:1fr 1fr; gap:10px }}
|
||||||
|
.w-card {{
|
||||||
background:var(--surface); border:1px solid var(--border);
|
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);
|
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) }}
|
.w-card:hover {{ border-color:var(--border-hover); transform:translateY(-2px) }}
|
||||||
.stat .n {{ font-size:28px; font-weight:700; letter-spacing:-.02em }}
|
.w-card .e {{ font-size:20px; margin-bottom:6px }}
|
||||||
.stat .l {{ font-size:12px; color:var(--text-sec); margin-top:4px; font-weight:500 }}
|
.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 }}
|
.servers {{ display:grid; gap:14px }}
|
||||||
|
|
||||||
|
/* Server card */
|
||||||
.s-card {{
|
.s-card {{
|
||||||
background:var(--surface); border:1px solid var(--border);
|
background:var(--surface); border:1px solid var(--border);
|
||||||
border-radius:var(--card-rad); padding:18px 20px;
|
border-radius:var(--card-rad); padding:18px 20px;
|
||||||
@@ -1153,42 +1176,44 @@ body::before {{
|
|||||||
animation:fadeUp .6s ease-out var(--d,0s) both;
|
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-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 }}
|
.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 }}
|
.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-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-t {{ height:6px; background:rgba(255,255,255,0.06); border-radius:3px; overflow:hidden; display:flex }}
|
||||||
.m-f {{
|
.m-f {{
|
||||||
width:0; height:100%; border-radius:3px;
|
width:0; height:100%; border-radius:3px;
|
||||||
background:linear-gradient(90deg,var(--accent),var(--green));
|
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-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 }}
|
.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 */
|
/* Tech info */
|
||||||
.info {{
|
.tech {{ --d:0.5s }}
|
||||||
margin-top:28px;
|
.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);
|
background:var(--surface); border:1px solid var(--border);
|
||||||
border-radius:var(--card-rad); padding:20px;
|
border-radius:var(--card-rad); padding:20px;
|
||||||
backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px);
|
backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px);
|
||||||
animation:fadeUp .6s .5s ease-out both;
|
|
||||||
transition:border-color .3s;
|
transition:border-color .3s;
|
||||||
}}
|
}}
|
||||||
.info:hover {{ border-color:var(--border-hover) }}
|
.tech-grid:hover {{ border-color:var(--border-hover) }}
|
||||||
.info h3 {{ font-size:14px; font-weight:600; margin-bottom:12px; color:var(--text) }}
|
.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) }}
|
||||||
.info-grid {{ display:grid; grid-template-columns:1fr 1fr; gap:8px 16px }}
|
.t-item:last-child {{ border-bottom:none }}
|
||||||
.info-grid span {{ font-size:12px; color:var(--text-sec); line-height:1.8; display:flex; align-items:center; gap:6px }}
|
.t-item .t-dot {{ width:4px; height:4px; border-radius:2px; background:var(--accent); flex-shrink:0 }}
|
||||||
.info-grid span::before {{ content:''; display:inline-block; width:4px; height:4px; border-radius:2px; background:var(--accent); flex-shrink:0 }}
|
|
||||||
|
|
||||||
/* Buttons */
|
/* 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 {{
|
.btn {{
|
||||||
display:inline-flex; align-items:center; gap:6px;
|
display:inline-flex; align-items:center; gap:6px;
|
||||||
padding:12px 28px; border-radius:12px;
|
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: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 }}
|
.btn.da {{ background:linear-gradient(135deg,#f43f5e,#e11d48); color:#fff }}
|
||||||
|
|
||||||
/* Footer */
|
.footer {{ text-align:center; margin-top:40px; color:var(--text-ter); font-size:12px; animation:fadeUp .6s .6s ease-out both }}
|
||||||
.footer {{ text-align:center; margin-top:36px; color:var(--text-ter); font-size:12px; animation:fadeUp .6s .7s ease-out both }}
|
|
||||||
|
|
||||||
/* Keyframes */
|
|
||||||
@keyframes fadeUp {{
|
@keyframes fadeUp {{
|
||||||
from {{ opacity:0; transform:translateY(16px) }}
|
from {{ opacity:0; transform:translateY(16px) }}
|
||||||
to {{ opacity:1; transform:translateY(0) }}
|
to {{ opacity:1; transform:translateY(0) }}
|
||||||
@@ -1212,15 +1235,13 @@ body::before {{
|
|||||||
to {{ width:var(--w) }}
|
to {{ width:var(--w) }}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/* Responsive */
|
|
||||||
@media(max-width:520px) {{
|
@media(max-width:520px) {{
|
||||||
.container {{ padding:32px 14px }}
|
.container {{ padding:32px 14px }}
|
||||||
.header h1 {{ font-size:24px }}
|
.header h1 {{ font-size:24px }}
|
||||||
.stat {{ max-width:none; padding:14px 12px }}
|
.why-grid {{ grid-template-columns:1fr }}
|
||||||
.stat .n {{ font-size:22px }}
|
|
||||||
.s-card {{ padding:14px }}
|
.s-card {{ padding:14px }}
|
||||||
.m {{ grid-template-columns:34px 1fr 44px; gap:8px }}
|
.m {{ grid-template-columns:32px 1fr 42px; gap:8px }}
|
||||||
.info-grid {{ grid-template-columns:1fr }}
|
.tech-grid {{ grid-template-columns:1fr }}
|
||||||
.btns {{ flex-direction:column; align-items:center }}
|
.btns {{ flex-direction:column; align-items:center }}
|
||||||
}}
|
}}
|
||||||
</style>
|
</style>
|
||||||
@@ -1228,33 +1249,41 @@ body::before {{
|
|||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
||||||
<div class="header">
|
<div class="section header">
|
||||||
<h1>⚡ {title}</h1>
|
<h1>⚡ {title}</h1>
|
||||||
<p>Быстрый и надёжный VPN-сервис</p>
|
<p>Быстрый VPN с собственными серверами в Европе. Безлимитный трафик, VLESS + XTLS, никаких логов.</p>
|
||||||
|
<p class="sub">Подписка на основе подписки · Happ-совместимость · Мгновенная активация</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stats">
|
<div class="section why">
|
||||||
<div class="stat"><div class="n">{total}</div><div class="l">Пользователей</div></div>
|
<h2>Почему {title}</h2>
|
||||||
<div class="stat"><div class="n">{len(servers)}</div><div class="l">Локаций</div></div>
|
<div class="why-grid">
|
||||||
</div>
|
<div class="w-card" style="--i:0"><div class="e">🚀</div><h4>Скорость</h4><p>Каналы 1–10 Gbit/s, собственные серверы, без посредников</p></div>
|
||||||
|
<div class="w-card" style="--i:1"><div class="e">🔒</div><h4>Приватность</h4><p>Zero-log политика — трафик не хранится и не анализируется</p></div>
|
||||||
<div class="servers">{servers_html}</div>
|
<div class="w-card" style="--i:2"><div class="e">🌍</div><h4>Покрытие</h4><p>Серверы в Германии, Швеции и России — выбирай ближайший</p></div>
|
||||||
|
<div class="w-card" style="--i:3"><div class="e">💎</div><h4>Тарифы</h4><p>Бесплатный доступ к free-серверам или Premium от 150 ₽/мес</p></div>
|
||||||
<div class="info">
|
|
||||||
<h3>🔧 Технические детали</h3>
|
|
||||||
<div class="info-grid">
|
|
||||||
<span>VLESS + XTLS Vision / Reality</span>
|
|
||||||
<span>TCP, WebSocket, gRPC</span>
|
|
||||||
<span>DDoS-защита на всех серверах</span>
|
|
||||||
<span>Каналы 1–10 Gbit/s</span>
|
|
||||||
<span>Zero-log политика</span>
|
|
||||||
<span>Поддержка Happ (iOS/Android)</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="section srv-section">
|
||||||
|
<h2>🖥 Состояние серверов</h2>
|
||||||
|
<div class="servers">{srv_cards}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section tech">
|
||||||
|
<h2>🔧 Технические детали</h2>
|
||||||
|
<div class="tech-grid">
|
||||||
|
<div class="t-item"><span class="t-dot"></span>VLESS + XTLS Vision / Reality</div>
|
||||||
|
<div class="t-item"><span class="t-dot"></span>TCP, WebSocket, gRPC</div>
|
||||||
|
<div class="t-item"><span class="t-dot"></span>DDoS-защита на всех серверах</div>
|
||||||
|
<div class="t-item"><span class="t-dot"></span>Zero-log политика</div>
|
||||||
|
<div class="t-item"><span class="t-dot"></span>Поддержка Happ (iOS / Android / Desktop)</div>
|
||||||
|
<div class="t-item"><span class="t-dot"></span>Автообновление подписки каждые 12ч</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>
|
||||||
|
|
||||||
<div class="footer">© 2026 {title}</div>
|
<div class="footer">© 2026 {title}</div>
|
||||||
|
|
||||||
@@ -1265,10 +1294,11 @@ body::before {{
|
|||||||
|
|
||||||
async def fetch_servers_status() -> list:
|
async def fetch_servers_status() -> list:
|
||||||
results = []
|
results = []
|
||||||
|
now = time.time()
|
||||||
async with httpx.AsyncClient(timeout=8.0, verify=False) as client:
|
async with httpx.AsyncClient(timeout=8.0, verify=False) as client:
|
||||||
for srv in servers:
|
for srv in servers:
|
||||||
url = srv.get("status_url", "")
|
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:
|
if not url:
|
||||||
results.append(entry)
|
results.append(entry)
|
||||||
continue
|
continue
|
||||||
@@ -1276,8 +1306,22 @@ async def fetch_servers_status() -> list:
|
|||||||
resp = await client.get(url)
|
resp = await client.get(url)
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
|
entry["online"] = True
|
||||||
entry["server_name"] = data.get("server_name", "")
|
entry["server_name"] = data.get("server_name", "")
|
||||||
entry["checks"] = data.get("checks", {})
|
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:
|
else:
|
||||||
entry["checks"]["error"] = {"value": f"HTTP {resp.status_code}"}
|
entry["checks"]["error"] = {"value": f"HTTP {resp.status_code}"}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
Reference in New Issue
Block a user