Merge pull request #251 from ReturnFI/beta
Dashboard redesign & server stats upgrade
This commit is contained in:
25
changelog
25
changelog
@ -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`
|
|
||||||
|
|||||||
@ -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]:
|
||||||
def get_memory_usage() -> tuple[int, int]:
|
if not content:
|
||||||
|
return 0, 0
|
||||||
|
|
||||||
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)
|
||||||
@ -75,20 +125,70 @@ def get_memory_usage() -> tuple[int, int]:
|
|||||||
sreclaimable_kb = mem_info.get("SReclaimable", 0)
|
sreclaimable_kb = mem_info.get("SReclaimable", 0)
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
used_kb = max(0, used_kb)
|
||||||
|
return mem_total_kb // 1024, used_kb // 1024
|
||||||
|
|
||||||
|
|
||||||
|
async def get_memory_usage() -> tuple[int, int]:
|
||||||
|
content = await read_file_async("/proc/meminfo")
|
||||||
|
return parse_meminfo(content)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_network_stats(content: str) -> tuple[int, int]:
|
||||||
|
if not content:
|
||||||
|
return 0, 0
|
||||||
|
|
||||||
if used_kb < 0:
|
rx_bytes, tx_bytes = 0, 0
|
||||||
used_kb = mem_total_kb - mem_info.get("MemAvailable", mem_total_kb)
|
lines = content.split('\n')
|
||||||
used_kb = max(0, used_kb)
|
|
||||||
|
|
||||||
|
|
||||||
total_mb = mem_total_kb // 1024
|
|
||||||
used_mb = used_kb // 1024
|
|
||||||
|
|
||||||
return total_mb, used_mb
|
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)
|
||||||
|
|
||||||
def get_online_user_count(secret: str) -> int:
|
|
||||||
|
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:
|
||||||
if not USERS_FILE.exists():
|
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
|
return 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_traffic() -> tuple[int, int]:
|
||||||
|
if not USERS_FILE.exists():
|
||||||
|
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()
|
||||||
|
|
||||||
|
tasks = [
|
||||||
|
get_uptime_and_boottime(),
|
||||||
|
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()
|
||||||
|
]
|
||||||
|
|
||||||
|
results = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
cpu_usage = get_cpu_usage()
|
print(f"🕒 Uptime: {uptime_str} (since {boot_time_str})")
|
||||||
mem_total, mem_used = get_memory_usage()
|
print(f"📈 CPU Usage: {cpu_usage}%")
|
||||||
online_users = get_online_user_count(secret)
|
print(f"💻 Used RAM: {mem_used}MB / {mem_total}MB")
|
||||||
|
|
||||||
print(f"📈 CPU Usage: {cpu_usage}")
|
|
||||||
print(f"📋 Total RAM: {mem_total}MB")
|
|
||||||
print(f"💻 Used RAM: {mem_used}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())
|
||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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 %}
|
||||||
@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user