Merge pull request #251 from ReturnFI/beta

Dashboard redesign & server stats upgrade
This commit is contained in:
Whispering Wind
2025-08-19 23:52:37 +03:30
committed by GitHub
6 changed files with 417 additions and 175 deletions

View File

@ -1,22 +1,19 @@
# [1.15.0] - 2025-08-17 # [1.16.0] - 2025-08-19
#### ✨ New Features #### ✨ New Features
* 🚫 **Blocked User Check** * 📊 **Dashboard Redesign**
* Subscription endpoint now validates blocked users and prevents access * Modernized UI with detailed server stats
* 🌐 **External Configs in Subscriptions** * 🖥️ **Server API Enhancements**
* NormalSub links now include external proxy configs * Added uptime and traffic-since-reboot metrics
* ⚙️ **Settings Page** * **System Monitor Optimization**
* Added management UI for extra subscription configs * Improved performance with async I/O
* 🖥️ **Webpanel API** * Accurate traffic tracking since reboot
* New API endpoints for managing extra configs #### 🐛 Fixes
* 🛠️ **CLI & Scripts**
* CLI support for extra subscription configs * 🔧 Correctly count **actual device connections** instead of unique users
* New `extra_config.py` script to manage additional proxy configs: * 🔥 Fixed subscription blocked page to display the right user data
* `vmess`, `vless`, `ss`, `trojan`

View File

@ -2,12 +2,17 @@
import sys import sys
import json import json
from hysteria2_api import Hysteria2Client import asyncio
import aiofiles
import time import time
from concurrent.futures import ThreadPoolExecutor
from functools import lru_cache
from hysteria2_api import Hysteria2Client
from init_paths import * from init_paths import *
from paths import * from paths import *
@lru_cache(maxsize=1)
def get_secret() -> str: def get_secret() -> str:
if not CONFIG_FILE.exists(): if not CONFIG_FILE.exists():
print("Error: config.json file not found!", file=sys.stderr) print("Error: config.json file not found!", file=sys.stderr)
@ -25,24 +30,74 @@ def get_secret() -> str:
def convert_bytes(bytes_val: int) -> str: def convert_bytes(bytes_val: int) -> str:
units = [("TB", 1 << 40), ("GB", 1 << 30), ("MB", 1 << 20), ("KB", 1 << 10)] if bytes_val >= (1 << 40):
for unit, factor in units: return f"{bytes_val / (1 << 40):.2f} TB"
if bytes_val >= factor: elif bytes_val >= (1 << 30):
return f"{bytes_val / factor:.2f} {unit}" return f"{bytes_val / (1 << 30):.2f} GB"
elif bytes_val >= (1 << 20):
return f"{bytes_val / (1 << 20):.2f} MB"
elif bytes_val >= (1 << 10):
return f"{bytes_val / (1 << 10):.2f} KB"
return f"{bytes_val} B" return f"{bytes_val} B"
def get_cpu_usage(interval: float = 0.1) -> float: def convert_speed(bytes_per_second: int) -> str:
def read_cpu_times(): if bytes_per_second >= (1 << 40):
with open("/proc/stat") as f: return f"{bytes_per_second / (1 << 40):.2f} TB/s"
line = f.readline() elif bytes_per_second >= (1 << 30):
fields = list(map(int, line.strip().split()[1:])) return f"{bytes_per_second / (1 << 30):.2f} GB/s"
idle, total = fields[3], sum(fields) elif bytes_per_second >= (1 << 20):
return idle, total return f"{bytes_per_second / (1 << 20):.2f} MB/s"
elif bytes_per_second >= (1 << 10):
return f"{bytes_per_second / (1 << 10):.2f} KB/s"
return f"{int(bytes_per_second)} B/s"
idle1, total1 = read_cpu_times()
time.sleep(interval) async def read_file_async(filepath: str) -> str:
idle2, total2 = read_cpu_times() try:
async with aiofiles.open(filepath, 'r') as f:
return await f.read()
except FileNotFoundError:
return ""
def format_uptime(seconds: float) -> str:
seconds = int(seconds)
days, remainder = divmod(seconds, 86400)
hours, remainder = divmod(remainder, 3600)
minutes, _ = divmod(remainder, 60)
return f"{days}d {hours}h {minutes}m"
async def get_uptime_and_boottime() -> tuple[str, str]:
try:
content = await read_file_async("/proc/uptime")
uptime_seconds = float(content.split()[0])
boot_time_epoch = time.time() - uptime_seconds
boot_time_str = time.strftime("%Y-%m-%d %H:%M", time.localtime(boot_time_epoch))
uptime_str = format_uptime(uptime_seconds)
return uptime_str, boot_time_str
except (FileNotFoundError, IndexError, ValueError):
return "N/A", "N/A"
def parse_cpu_stats(content: str) -> tuple[int, int]:
if not content:
return 0, 0
line = content.split('\n')[0]
fields = list(map(int, line.strip().split()[1:]))
idle, total = fields[3], sum(fields)
return idle, total
async def get_cpu_usage(interval: float = 0.1) -> float:
content1 = await read_file_async("/proc/stat")
idle1, total1 = parse_cpu_stats(content1)
await asyncio.sleep(interval)
content2 = await read_file_async("/proc/stat")
idle2, total2 = parse_cpu_stats(content2)
idle_delta = idle2 - idle1 idle_delta = idle2 - idle1
total_delta = total2 - total1 total_delta = total2 - total1
@ -50,23 +105,18 @@ def get_cpu_usage(interval: float = 0.1) -> float:
return round(cpu_usage, 1) return round(cpu_usage, 1)
def parse_meminfo(content: str) -> tuple[int, int]:
if not content:
return 0, 0
def get_memory_usage() -> tuple[int, int]:
mem_info = {} mem_info = {}
try: for line in content.split('\n'):
with open("/proc/meminfo", "r") as f: if ':' in line:
for line in f: parts = line.split()
parts = line.split() if len(parts) >= 2:
if len(parts) >= 2: key = parts[0].rstrip(':')
key = parts[0].rstrip(':') if parts[1].isdigit():
if parts[1].isdigit(): mem_info[key] = int(parts[1])
mem_info[key] = int(parts[1])
except FileNotFoundError:
print("Error: /proc/meminfo not found.", file=sys.stderr)
return 0, 0
except Exception as e:
print(f"Error reading /proc/meminfo: {e}", file=sys.stderr)
return 0, 0
mem_total_kb = mem_info.get("MemTotal", 0) mem_total_kb = mem_info.get("MemTotal", 0)
mem_free_kb = mem_info.get("MemFree", 0) mem_free_kb = mem_info.get("MemFree", 0)
@ -76,19 +126,69 @@ def get_memory_usage() -> tuple[int, int]:
used_kb = mem_total_kb - mem_free_kb - buffers_kb - cached_kb - sreclaimable_kb used_kb = mem_total_kb - mem_free_kb - buffers_kb - cached_kb - sreclaimable_kb
if used_kb < 0: used_kb = max(0, used_kb)
used_kb = mem_total_kb - mem_info.get("MemAvailable", mem_total_kb) return mem_total_kb // 1024, used_kb // 1024
used_kb = max(0, used_kb)
total_mb = mem_total_kb // 1024 async def get_memory_usage() -> tuple[int, int]:
used_mb = used_kb // 1024 content = await read_file_async("/proc/meminfo")
return parse_meminfo(content)
return total_mb, used_mb
def parse_network_stats(content: str) -> tuple[int, int]:
if not content:
return 0, 0
def get_online_user_count(secret: str) -> int: rx_bytes, tx_bytes = 0, 0
lines = content.split('\n')
for line in lines[2:]:
if not line.strip():
continue
parts = line.split()
if len(parts) < 10:
continue
iface = parts[0].strip().replace(':', '')
if iface == 'lo':
continue
try:
rx_bytes += int(parts[1])
tx_bytes += int(parts[9])
except (IndexError, ValueError):
continue
return rx_bytes, tx_bytes
async def get_network_stats() -> tuple[int, int]:
content = await read_file_async('/proc/net/dev')
return parse_network_stats(content)
async def get_network_speed(interval: float = 0.5) -> tuple[int, int]:
rx1, tx1 = await get_network_stats()
await asyncio.sleep(interval)
rx2, tx2 = await get_network_stats()
rx_speed = (rx2 - rx1) / interval
tx_speed = (tx2 - tx1) / interval
return int(rx_speed), int(tx_speed)
def parse_connection_counts(tcp_content: str, udp_content: str) -> tuple[int, int]:
tcp_count = len(tcp_content.split('\n')) - 2 if tcp_content else 0
udp_count = len(udp_content.split('\n')) - 2 if udp_content else 0
return max(0, tcp_count), max(0, udp_count)
async def get_connection_counts() -> tuple[int, int]:
tcp_task = read_file_async('/proc/net/tcp')
udp_task = read_file_async('/proc/net/udp')
tcp_content, udp_content = await asyncio.gather(tcp_task, udp_task)
return parse_connection_counts(tcp_content, udp_content)
def get_online_user_count_sync(secret: str) -> int:
try: try:
client = Hysteria2Client( client = Hysteria2Client(
base_url=API_BASE_URL, base_url=API_BASE_URL,
@ -101,47 +201,83 @@ def get_online_user_count(secret: str) -> int:
return 0 return 0
def get_total_traffic() -> tuple[int, int]: async def get_online_user_count(secret: str) -> int:
loop = asyncio.get_event_loop()
with ThreadPoolExecutor() as executor:
return await loop.run_in_executor(executor, get_online_user_count_sync, secret)
def parse_total_traffic(content: str) -> tuple[int, int]:
if not content:
return 0, 0
try:
users = json.loads(content)
total_upload = sum(int(user_data.get("upload_bytes", 0) or 0) for user_data in users.values())
total_download = sum(int(user_data.get("download_bytes", 0) or 0) for user_data in users.values())
return total_upload, total_download
except (json.JSONDecodeError, ValueError, AttributeError):
return 0, 0
async def get_user_traffic() -> tuple[int, int]:
if not USERS_FILE.exists(): if not USERS_FILE.exists():
return 0, 0 return 0, 0
try: try:
with USERS_FILE.open() as f: async with aiofiles.open(USERS_FILE, 'r') as f:
users = json.load(f) content = await f.read()
return parse_total_traffic(content)
total_upload = 0
total_download = 0
for user_data in users.values():
total_upload += int(user_data.get("upload_bytes", 0) or 0)
total_download += int(user_data.get("download_bytes", 0) or 0)
return total_upload, total_download
except Exception as e: except Exception as e:
print(f"Error parsing traffic data: {e}", file=sys.stderr) print(f"Error parsing traffic data: {e}", file=sys.stderr)
return 0, 0 return 0, 0
async def main():
def main():
secret = get_secret() secret = get_secret()
cpu_usage = get_cpu_usage() tasks = [
mem_total, mem_used = get_memory_usage() get_uptime_and_boottime(),
online_users = get_online_user_count(secret) get_memory_usage(),
get_connection_counts(),
get_online_user_count(secret),
get_user_traffic(),
get_cpu_usage(0.1),
get_network_speed(0.3),
get_network_stats()
]
print(f"📈 CPU Usage: {cpu_usage}") results = await asyncio.gather(*tasks)
print(f"📋 Total RAM: {mem_total}MB")
print(f"💻 Used RAM: {mem_used}MB") uptime_str, boot_time_str = results[0]
mem_total, mem_used = results[1]
tcp_connections, udp_connections = results[2]
online_users = results[3]
user_upload, user_download = results[4]
cpu_usage = results[5]
download_speed, upload_speed = results[6]
reboot_rx, reboot_tx = results[7]
print(f"🕒 Uptime: {uptime_str} (since {boot_time_str})")
print(f"📈 CPU Usage: {cpu_usage}%")
print(f"💻 Used RAM: {mem_used}MB / {mem_total}MB")
print(f"👥 Online Users: {online_users}") print(f"👥 Online Users: {online_users}")
print() print()
print(f"🔼 Upload Speed: {convert_speed(upload_speed)}")
total_upload, total_download = get_total_traffic() print(f"🔽 Download Speed: {convert_speed(download_speed)}")
print(f"📡 TCP Connections: {tcp_connections}")
print(f"🔼 Uploaded Traffic: {convert_bytes(total_upload)}") print(f"📡 UDP Connections: {udp_connections}")
print(f"🔽 Downloaded Traffic: {convert_bytes(total_download)}") print()
print(f"📊 Total Traffic: {convert_bytes(total_upload + total_download)}") print("📊 Traffic Since Last Reboot:")
print(f" 🔼 Total Uploaded: {convert_bytes(reboot_tx)}")
print(f" 🔽 Total Downloaded: {convert_bytes(reboot_rx)}")
print(f" 📈 Combined Traffic: {convert_bytes(reboot_tx + reboot_rx)}")
print()
print("📊 User Traffic (All Time):")
print(f" 🔼 Uploaded Traffic: {convert_bytes(user_upload)}")
print(f" 🔽 Downloaded Traffic: {convert_bytes(user_download)}")
print(f" 📈 Total Traffic: {convert_bytes(user_upload + user_download)}")
if __name__ == "__main__": if __name__ == "__main__":
main() asyncio.run(main())

View File

@ -5,22 +5,35 @@ from pydantic import BaseModel
# It's better to chnage the underlying script to return bytes instead of changing it here # It's better to chnage the underlying script to return bytes instead of changing it here
# Because of this problem we use str type instead of int as type # Because of this problem we use str type instead of int as type
class ServerStatusResponse(BaseModel): class ServerStatusResponse(BaseModel):
# disk_usage: int # System Info
uptime: str
boot_time: str
cpu_usage: str cpu_usage: str
total_ram: str
ram_usage: str ram_usage: str
total_ram: str
online_users: int online_users: int
uploaded_traffic: str # Real-time Network
downloaded_traffic: str upload_speed: str
total_traffic: str download_speed: str
tcp_connections: int
udp_connections: int
# Traffic Since Reboot
reboot_uploaded_traffic: str
reboot_downloaded_traffic: str
reboot_total_traffic: str
# User Traffic (All Time)
user_uploaded_traffic: str
user_downloaded_traffic: str
user_total_traffic: str
class ServerServicesStatusResponse(BaseModel): class ServerServicesStatusResponse(BaseModel):
hysteria_server: bool hysteria_server: bool
hysteria_webpanel: bool hysteria_webpanel: bool
hysteria_iplimit: bool hysteria_iplimit: bool
# hysteria_singbox: bool
hysteria_normal_sub: bool hysteria_normal_sub: bool
hysteria_telegram_bot: bool hysteria_telegram_bot: bool
hysteria_warp: bool hysteria_warp: bool

View File

@ -11,7 +11,7 @@ async def server_status_api():
Retrieve the server status. Retrieve the server status.
This endpoint provides information about the current server status, This endpoint provides information about the current server status,
including CPU usage, RAM usage, online users, and traffic statistics. including uptime, CPU usage, RAM usage, online users, and traffic statistics.
Returns: Returns:
ServerStatusResponse: A response model containing server status details. ServerStatusResponse: A response model containing server status details.
@ -30,7 +30,6 @@ async def server_status_api():
def __parse_server_status(server_info: str) -> ServerStatusResponse: def __parse_server_status(server_info: str) -> ServerStatusResponse:
# Initial data with default values
""" """
Parse the server information provided by cli_api.server_info() Parse the server information provided by cli_api.server_info()
and return a ServerStatusResponse instance. and return a ServerStatusResponse instance.
@ -45,57 +44,87 @@ def __parse_server_status(server_info: str) -> ServerStatusResponse:
ValueError: If the server information is invalid or incomplete. ValueError: If the server information is invalid or incomplete.
""" """
data = { data = {
'uptime': 'N/A',
'boot_time': 'N/A',
'cpu_usage': '0%', 'cpu_usage': '0%',
'total_ram': '0MB', 'total_ram': '0MB',
'ram_usage': '0MB', 'ram_usage': '0MB',
'online_users': 0, 'online_users': 0,
'uploaded_traffic': '0KB', 'upload_speed': '0 B/s',
'downloaded_traffic': '0KB', 'download_speed': '0 B/s',
'total_traffic': '0KB' 'tcp_connections': 0,
'udp_connections': 0,
'reboot_uploaded_traffic': '0 B',
'reboot_downloaded_traffic': '0 B',
'reboot_total_traffic': '0 B',
'user_uploaded_traffic': '0 B',
'user_downloaded_traffic': '0 B',
'user_total_traffic': '0 B'
} }
# Example output(server_info) from cli_api.server_info(): current_section = 'general'
# 📈 CPU Usage: 9.4%
# 📋 Total RAM: 3815MB
# 💻 Used RAM: 2007MB
# 👥 Online Users: 0
#
# 🔼 Uploaded Traffic: 0 KB
# 🔽 Downloaded Traffic: 0 KB
# 📊 Total Traffic: 0 KB
for line in server_info.splitlines(): for line in server_info.splitlines():
line = line.strip()
if not line:
continue
if 'Traffic Since Last Reboot' in line:
current_section = 'reboot'
continue
elif 'User Traffic (All Time)' in line:
current_section = 'user'
continue
key, _, value = line.partition(":") key, _, value = line.partition(":")
key = key.strip().lower() key = key.strip().lower()
value = value.strip() value = value.strip()
if not key or not value: if not key or not value:
continue # Skip empty or malformed lines continue
try: try:
if 'cpu usage' in key: if 'uptime' in key:
uptime_part, _, boottime_part = value.partition('(')
data['uptime'] = uptime_part.strip()
data['boot_time'] = boottime_part.replace('since ', '').replace(')', '').strip()
elif 'cpu usage' in key:
data['cpu_usage'] = value data['cpu_usage'] = value
elif 'total ram' in key:
data['total_ram'] = value
elif 'used ram' in key: elif 'used ram' in key:
data['ram_usage'] = value parts = value.split('/')
if len(parts) == 2:
data['ram_usage'] = parts[0].strip()
data['total_ram'] = parts[1].strip()
elif 'online users' in key: elif 'online users' in key:
data['online_users'] = int(value) data['online_users'] = int(value)
elif 'uploaded traffic' in key: elif 'upload speed' in key:
value = value.replace(' ', '') data['upload_speed'] = value
data['uploaded_traffic'] = value elif 'download speed' in key:
elif "downloaded traffic" in key: data['download_speed'] = value
value = value.replace(' ', '') elif 'tcp connections' in key:
data['downloaded_traffic'] = value data['tcp_connections'] = int(value)
elif 'total traffic' in key: elif 'udp connections' in key:
value = value.replace(' ', '') data['udp_connections'] = int(value)
data["total_traffic"] = value elif 'total uploaded' in key or 'uploaded traffic' in key:
except ValueError as e: if current_section == 'reboot':
data['reboot_uploaded_traffic'] = value
elif current_section == 'user':
data['user_uploaded_traffic'] = value
elif 'total downloaded' in key or 'downloaded traffic' in key:
if current_section == 'reboot':
data['reboot_downloaded_traffic'] = value
elif current_section == 'user':
data['user_downloaded_traffic'] = value
elif 'combined traffic' in key or 'total traffic' in key:
if current_section == 'reboot':
data['reboot_total_traffic'] = value
elif current_section == 'user':
data['user_total_traffic'] = value
except (ValueError, IndexError) as e:
raise ValueError(f'Error parsing line \'{line}\': {e}') raise ValueError(f'Error parsing line \'{line}\': {e}')
# Validate required fields
try: try:
return ServerStatusResponse(**data) # type: ignore return ServerStatusResponse(**data)
except Exception as e: except Exception as e:
raise ValueError(f'Invalid or incomplete server info: {e}') raise ValueError(f'Invalid or incomplete server info: {e}')
@ -141,8 +170,6 @@ def __parse_services_status(services_status: dict[str, bool]) -> ServerServicesS
parsed_services_status['hysteria_telegram_bot'] = status parsed_services_status['hysteria_telegram_bot'] = status
elif 'hysteria-normal-sub' in service: elif 'hysteria-normal-sub' in service:
parsed_services_status['hysteria_normal_sub'] = status parsed_services_status['hysteria_normal_sub'] = status
# elif 'hysteria-singbox' in service:
# parsed_services_status['hysteria_singbox'] = status
elif 'wg-quick' in service: elif 'wg-quick' in service:
parsed_services_status['hysteria_warp'] = status parsed_services_status['hysteria_warp'] = status
return ServerServicesStatusResponse(**parsed_services_status) return ServerServicesStatusResponse(**parsed_services_status)

View File

@ -15,63 +15,119 @@
<section class="content"> <section class="content">
<div class="container-fluid"> <div class="container-fluid">
<!-- Core System & Network Stats -->
<div class="row"> <div class="row">
<div class="col-lg-3 col-6"> <div class="col-md-3 col-sm-6 col-12">
<!-- small box --> <div class="info-box shadow-sm">
<div class="small-box bg-info"> <span class="info-box-icon bg-info"><i class="fas fa-microchip"></i></span>
<div class="inner"> <div class="info-box-content">
<h3 id="cpu-usage">--<sup style="font-size: 20px">%</sup></h3> <span class="info-box-text">CPU Usage</span>
<p>CPU Usage</p> <span class="info-box-number" id="cpu-usage">--</span>
</div>
<div class="icon">
<i class="fas fa-microchip"></i>
</div> </div>
</div> </div>
</div> </div>
<div class="col-lg-3 col-6"> <div class="col-md-3 col-sm-6 col-12">
<!-- small box --> <div class="info-box shadow-sm">
<div class="small-box bg-warning"> <span class="info-box-icon bg-warning"><i class="fas fa-memory"></i></span>
<div class="inner"> <div class="info-box-content">
<h3 id="ram-usage">--</h3> <span class="info-box-text">RAM Usage</span>
<p>RAM Usage</p> <span class="info-box-number" id="ram-usage">-- / --</span>
</div>
<div class="icon">
<i class="fas fa-memory"></i>
</div> </div>
</div> </div>
</div> </div>
<div class="col-lg-3 col-6"> <div class="col-md-3 col-sm-6 col-12">
<!-- small box --> <div class="info-box shadow-sm">
<div class="small-box bg-secondary"> <span class="info-box-icon bg-success"><i class="fas fa-users"></i></span>
<div class="inner"> <div class="info-box-content">
<h3 id="total-traffic">--</h3> <span class="info-box-text">Online Users</span>
<p>Total Traffic</p> <span class="info-box-number" id="online-users">--</span>
</div>
<div class="icon">
<i class="fas fa-network-wired"></i>
</div> </div>
</div> </div>
</div> </div>
<div class="col-lg-3 col-6"> <div class="col-md-3 col-sm-6 col-12">
<!-- small box --> <div class="info-box shadow-sm">
<div class="small-box bg-success"> <span class="info-box-icon bg-secondary"><i class="fas fa-clock"></i></span>
<div class="inner"> <div class="info-box-content">
<h3 id="online-users">--</h3> <span class="info-box-text">Uptime</span>
<p>Online Users</p> <span class="info-box-number" id="uptime">--</span>
</div> </div>
<div class="icon"> </div>
<i class="fas fa-users"></i> </div>
</div>
<div class="row">
<div class="col-md-6 col-12">
<div class="info-box shadow-sm">
<span class="info-box-icon bg-primary"><i class="fas fa-exchange-alt"></i></span>
<div class="info-box-content">
<span class="info-box-text">Download / Upload Speed</span>
<span class="info-box-number" id="network-speed">-- / --</span>
</div>
</div>
</div>
<div class="col-md-6 col-12">
<div class="info-box shadow-sm">
<span class="info-box-icon bg-dark"><i class="fas fa-project-diagram"></i></span>
<div class="info-box-content">
<span class="info-box-text">TCP / UDP Connections</span>
<span class="info-box-number" id="network-connections">-- / --</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Traffic Monitoring -->
<div class="row">
<div class="col-lg-6">
<div class="card card-outline card-danger">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-power-off mr-2"></i>Traffic Since Last Reboot</h3>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center border-bottom mb-3 pb-2">
<p class="text-lg text-success"><i class="fas fa-arrow-up mr-2"></i>Uploaded</p>
<p class="text-lg" id="reboot-uploaded-traffic">--</p>
</div>
<div class="d-flex justify-content-between align-items-center border-bottom mb-3 pb-2">
<p class="text-lg text-primary"><i class="fas fa-arrow-down mr-2"></i>Downloaded</p>
<p class="text-lg" id="reboot-downloaded-traffic">--</p>
</div>
<div class="d-flex justify-content-between align-items-center">
<p class="text-lg text-info"><i class="fas fa-chart-line mr-2"></i>Combined</p>
<p class="text-lg" id="reboot-total-traffic">--</p>
</div>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card card-outline card-info">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-history mr-2"></i>User Traffic (All Time)</h3>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center border-bottom mb-3 pb-2">
<p class="text-lg text-success"><i class="fas fa-arrow-up mr-2"></i>Uploaded</p>
<p class="text-lg" id="user-uploaded-traffic">--</p>
</div>
<div class="d-flex justify-content-between align-items-center border-bottom mb-3 pb-2">
<p class="text-lg text-primary"><i class="fas fa-arrow-down mr-2"></i>Downloaded</p>
<p class="text-lg" id="user-downloaded-traffic">--</p>
</div>
<div class="d-flex justify-content-between align-items-center">
<p class="text-lg text-info"><i class="fas fa-chart-line mr-2"></i>Combined</p>
<p class="text-lg" id="user-total-traffic">--</p>
</div>
</div>
</div>
</div>
</div>
<!-- Service Statuses -->
<div class="row"> <div class="row">
<div class="col-lg-3 col-6"> <div class="col-lg-3 col-6">
<div class="small-box" id="hysteria2-status-box"> <div class="small-box" id="hysteria2-status-box">
<div class="inner"> <div class="inner">
<h3 id="hysteria2-status">--</h3> <h3 id="hysteria2-status">--</h3>
<p>Hysteria2</p> <p>Hysteria2 Core</p>
</div> </div>
<div class="icon"> <div class="icon">
<i class="fas fa-bolt"></i> <i class="fas fa-bolt"></i>
@ -93,10 +149,10 @@
<div class="small-box" id="iplimit-status-box"> <div class="small-box" id="iplimit-status-box">
<div class="inner"> <div class="inner">
<h3 id="iplimit-status">--</h3> <h3 id="iplimit-status">--</h3>
<p>IP Limit</p> <p>IP Limit Service</p>
</div> </div>
<div class="icon"> <div class="icon">
<i class="fas fa-box"></i> <i class="fas fa-ban"></i>
</div> </div>
</div> </div>
</div> </div>
@ -104,7 +160,7 @@
<div class="small-box" id="normalsub-status-box"> <div class="small-box" id="normalsub-status-box">
<div class="inner"> <div class="inner">
<h3 id="normalsub-status">--</h3> <h3 id="normalsub-status">--</h3>
<p>Normal Subscription</p> <p>Subscription Service</p>
</div> </div>
<div class="icon"> <div class="icon">
<i class="fas fa-rss"></i> <i class="fas fa-rss"></i>
@ -122,21 +178,34 @@
fetch('{{ url_for("server_status_api") }}') fetch('{{ url_for("server_status_api") }}')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
// Core Stats
document.getElementById('cpu-usage').textContent = data.cpu_usage; document.getElementById('cpu-usage').textContent = data.cpu_usage;
document.getElementById('ram-usage').textContent = data.ram_usage; document.getElementById('ram-usage').textContent = `${data.ram_usage} / ${data.total_ram}`;
document.getElementById('online-users').textContent = data.online_users; document.getElementById('online-users').textContent = data.online_users;
document.getElementById('total-traffic').textContent = data.total_traffic; document.getElementById('uptime').textContent = data.uptime;
// Network Stats
document.getElementById('network-speed').innerHTML = `🔽 ${data.download_speed} / 🔼 ${data.upload_speed}`;
document.getElementById('network-connections').textContent = `${data.tcp_connections} / ${data.udp_connections}`;
// Traffic Since Reboot
document.getElementById('reboot-uploaded-traffic').textContent = data.reboot_uploaded_traffic;
document.getElementById('reboot-downloaded-traffic').textContent = data.reboot_downloaded_traffic;
document.getElementById('reboot-total-traffic').textContent = data.reboot_total_traffic;
// User Traffic (All Time)
document.getElementById('user-uploaded-traffic').textContent = data.user_uploaded_traffic;
document.getElementById('user-downloaded-traffic').textContent = data.user_downloaded_traffic;
document.getElementById('user-total-traffic').textContent = data.user_total_traffic;
}) })
.catch(error => console.error('Error fetching server info:', error)); .catch(error => console.error('Error fetching server info:', error));
} }
function updateServiceStatuses() { function updateServiceStatuses() {
// Add services api in fetch
fetch('{{ url_for("server_services_status_api") }}') fetch('{{ url_for("server_services_status_api") }}')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
updateServiceBox('hysteria2', data.hysteria_server); updateServiceBox('hysteria2', data.hysteria_server);
updateServiceBox('telegrambot', data.hysteria_telegram_bot); updateServiceBox('telegrambot', data.hysteria_telegram_bot);
updateServiceBox('iplimit', data.hysteria_iplimit); updateServiceBox('iplimit', data.hysteria_iplimit);
@ -152,20 +221,19 @@
if (status === true) { if (status === true) {
statusElement.textContent = 'Active'; statusElement.textContent = 'Active';
statusBox.classList.remove('bg-danger'); statusBox.classList.remove('bg-danger');
statusBox.classList.add('bg-success'); // Add green statusBox.classList.add('bg-success');
} else { } else {
statusElement.textContent = 'Inactive'; statusElement.textContent = 'Inactive';
statusBox.classList.remove('bg-success'); statusBox.classList.remove('bg-success');
statusBox.classList.add('bg-danger'); // Add red statusBox.classList.add('bg-danger');
} }
} }
document.addEventListener('DOMContentLoaded', function () {
updateServerInfo();
updateServerInfo(); updateServiceStatuses();
updateServiceStatuses(); setInterval(updateServerInfo, 2000);
setInterval(updateServiceStatuses, 10000);
setInterval(updateServerInfo, 5000); });
</script> </script>
{% endblock %} {% endblock %}

View File

@ -22,6 +22,7 @@ urllib3==2.5.0
yarl==1.20.1 yarl==1.20.1
hysteria2-api==0.1.3 hysteria2-api==0.1.3
schedule==1.2.2 schedule==1.2.2
aiofiles==24.1.0
# webpanel # webpanel
annotated-types==0.7.0 annotated-types==0.7.0