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
|
||||
|
||||
* 🚫 **Blocked User Check**
|
||||
* 📊 **Dashboard Redesign**
|
||||
|
||||
* Subscription endpoint now validates blocked users and prevents access
|
||||
* 🌐 **External Configs in Subscriptions**
|
||||
* Modernized UI with detailed server stats
|
||||
* 🖥️ **Server API Enhancements**
|
||||
|
||||
* NormalSub links now include external proxy configs
|
||||
* ⚙️ **Settings Page**
|
||||
* Added uptime and traffic-since-reboot metrics
|
||||
* ⚡ **System Monitor Optimization**
|
||||
|
||||
* Added management UI for extra subscription configs
|
||||
* 🖥️ **Webpanel API**
|
||||
* Improved performance with async I/O
|
||||
* Accurate traffic tracking since reboot
|
||||
|
||||
* New API endpoints for managing extra configs
|
||||
* 🛠️ **CLI & Scripts**
|
||||
#### 🐛 Fixes
|
||||
|
||||
* CLI support for extra subscription configs
|
||||
* New `extra_config.py` script to manage additional proxy configs:
|
||||
|
||||
* `vmess`, `vless`, `ss`, `trojan`
|
||||
* 🔧 Correctly count **actual device connections** instead of unique users
|
||||
* 🔥 Fixed subscription blocked page to display the right user data
|
||||
|
||||
@ -2,12 +2,17 @@
|
||||
|
||||
import sys
|
||||
import json
|
||||
from hysteria2_api import Hysteria2Client
|
||||
import asyncio
|
||||
import aiofiles
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from functools import lru_cache
|
||||
from hysteria2_api import Hysteria2Client
|
||||
from init_paths import *
|
||||
from paths import *
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_secret() -> str:
|
||||
if not CONFIG_FILE.exists():
|
||||
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:
|
||||
units = [("TB", 1 << 40), ("GB", 1 << 30), ("MB", 1 << 20), ("KB", 1 << 10)]
|
||||
for unit, factor in units:
|
||||
if bytes_val >= factor:
|
||||
return f"{bytes_val / factor:.2f} {unit}"
|
||||
if bytes_val >= (1 << 40):
|
||||
return f"{bytes_val / (1 << 40):.2f} TB"
|
||||
elif bytes_val >= (1 << 30):
|
||||
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"
|
||||
|
||||
|
||||
def get_cpu_usage(interval: float = 0.1) -> float:
|
||||
def read_cpu_times():
|
||||
with open("/proc/stat") as f:
|
||||
line = f.readline()
|
||||
def convert_speed(bytes_per_second: int) -> str:
|
||||
if bytes_per_second >= (1 << 40):
|
||||
return f"{bytes_per_second / (1 << 40):.2f} TB/s"
|
||||
elif bytes_per_second >= (1 << 30):
|
||||
return f"{bytes_per_second / (1 << 30):.2f} GB/s"
|
||||
elif bytes_per_second >= (1 << 20):
|
||||
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"
|
||||
|
||||
|
||||
async def read_file_async(filepath: str) -> str:
|
||||
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
|
||||
|
||||
idle1, total1 = read_cpu_times()
|
||||
time.sleep(interval)
|
||||
idle2, total2 = read_cpu_times()
|
||||
|
||||
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
|
||||
total_delta = total2 - total1
|
||||
@ -50,23 +105,18 @@ def get_cpu_usage(interval: float = 0.1) -> float:
|
||||
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 = {}
|
||||
try:
|
||||
with open("/proc/meminfo", "r") as f:
|
||||
for line in f:
|
||||
for line in content.split('\n'):
|
||||
if ':' in line:
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
key = parts[0].rstrip(':')
|
||||
if parts[1].isdigit():
|
||||
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_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
|
||||
|
||||
if used_kb < 0:
|
||||
used_kb = mem_total_kb - mem_info.get("MemAvailable", mem_total_kb)
|
||||
used_kb = max(0, used_kb)
|
||||
return mem_total_kb // 1024, used_kb // 1024
|
||||
|
||||
|
||||
total_mb = mem_total_kb // 1024
|
||||
used_mb = used_kb // 1024
|
||||
|
||||
return total_mb, used_mb
|
||||
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
|
||||
|
||||
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:
|
||||
client = Hysteria2Client(
|
||||
base_url=API_BASE_URL,
|
||||
@ -101,47 +201,83 @@ def get_online_user_count(secret: str) -> int:
|
||||
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():
|
||||
return 0, 0
|
||||
|
||||
try:
|
||||
with USERS_FILE.open() as f:
|
||||
users = json.load(f)
|
||||
|
||||
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
|
||||
async with aiofiles.open(USERS_FILE, 'r') as f:
|
||||
content = await f.read()
|
||||
return parse_total_traffic(content)
|
||||
except Exception as e:
|
||||
print(f"Error parsing traffic data: {e}", file=sys.stderr)
|
||||
return 0, 0
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
async def main():
|
||||
secret = get_secret()
|
||||
|
||||
cpu_usage = get_cpu_usage()
|
||||
mem_total, mem_used = get_memory_usage()
|
||||
online_users = get_online_user_count(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()
|
||||
]
|
||||
|
||||
print(f"📈 CPU Usage: {cpu_usage}")
|
||||
print(f"📋 Total RAM: {mem_total}MB")
|
||||
print(f"💻 Used RAM: {mem_used}MB")
|
||||
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]
|
||||
|
||||
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()
|
||||
|
||||
total_upload, total_download = get_total_traffic()
|
||||
|
||||
print(f"🔼 Uploaded Traffic: {convert_bytes(total_upload)}")
|
||||
print(f"🔽 Downloaded Traffic: {convert_bytes(total_download)}")
|
||||
print(f"📊 Total Traffic: {convert_bytes(total_upload + total_download)}")
|
||||
print(f"🔼 Upload Speed: {convert_speed(upload_speed)}")
|
||||
print(f"🔽 Download Speed: {convert_speed(download_speed)}")
|
||||
print(f"📡 TCP Connections: {tcp_connections}")
|
||||
print(f"📡 UDP Connections: {udp_connections}")
|
||||
print()
|
||||
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__":
|
||||
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
|
||||
# Because of this problem we use str type instead of int as type
|
||||
class ServerStatusResponse(BaseModel):
|
||||
# disk_usage: int
|
||||
# System Info
|
||||
uptime: str
|
||||
boot_time: str
|
||||
cpu_usage: str
|
||||
total_ram: str
|
||||
ram_usage: str
|
||||
total_ram: str
|
||||
online_users: int
|
||||
|
||||
uploaded_traffic: str
|
||||
downloaded_traffic: str
|
||||
total_traffic: str
|
||||
# Real-time Network
|
||||
upload_speed: 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):
|
||||
hysteria_server: bool
|
||||
hysteria_webpanel: bool
|
||||
hysteria_iplimit: bool
|
||||
# hysteria_singbox: bool
|
||||
hysteria_normal_sub: bool
|
||||
hysteria_telegram_bot: bool
|
||||
hysteria_warp: bool
|
||||
|
||||
@ -11,7 +11,7 @@ async def server_status_api():
|
||||
Retrieve the 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:
|
||||
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:
|
||||
# Initial data with default values
|
||||
"""
|
||||
Parse the server information provided by cli_api.server_info()
|
||||
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.
|
||||
"""
|
||||
data = {
|
||||
'uptime': 'N/A',
|
||||
'boot_time': 'N/A',
|
||||
'cpu_usage': '0%',
|
||||
'total_ram': '0MB',
|
||||
'ram_usage': '0MB',
|
||||
'online_users': 0,
|
||||
'uploaded_traffic': '0KB',
|
||||
'downloaded_traffic': '0KB',
|
||||
'total_traffic': '0KB'
|
||||
'upload_speed': '0 B/s',
|
||||
'download_speed': '0 B/s',
|
||||
'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():
|
||||
# 📈 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
|
||||
current_section = 'general'
|
||||
|
||||
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 = key.strip().lower()
|
||||
value = value.strip()
|
||||
|
||||
if not key or not value:
|
||||
continue # Skip empty or malformed lines
|
||||
continue
|
||||
|
||||
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
|
||||
elif 'total ram' in key:
|
||||
data['total_ram'] = value
|
||||
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:
|
||||
data['online_users'] = int(value)
|
||||
elif 'uploaded traffic' in key:
|
||||
value = value.replace(' ', '')
|
||||
data['uploaded_traffic'] = value
|
||||
elif "downloaded traffic" in key:
|
||||
value = value.replace(' ', '')
|
||||
data['downloaded_traffic'] = value
|
||||
elif 'total traffic' in key:
|
||||
value = value.replace(' ', '')
|
||||
data["total_traffic"] = value
|
||||
except ValueError as e:
|
||||
elif 'upload speed' in key:
|
||||
data['upload_speed'] = value
|
||||
elif 'download speed' in key:
|
||||
data['download_speed'] = value
|
||||
elif 'tcp connections' in key:
|
||||
data['tcp_connections'] = int(value)
|
||||
elif 'udp connections' in key:
|
||||
data['udp_connections'] = int(value)
|
||||
elif 'total uploaded' in key or 'uploaded traffic' in key:
|
||||
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}')
|
||||
|
||||
# Validate required fields
|
||||
try:
|
||||
return ServerStatusResponse(**data) # type: ignore
|
||||
return ServerStatusResponse(**data)
|
||||
except Exception as 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
|
||||
elif 'hysteria-normal-sub' in service:
|
||||
parsed_services_status['hysteria_normal_sub'] = status
|
||||
# elif 'hysteria-singbox' in service:
|
||||
# parsed_services_status['hysteria_singbox'] = status
|
||||
elif 'wg-quick' in service:
|
||||
parsed_services_status['hysteria_warp'] = status
|
||||
return ServerServicesStatusResponse(**parsed_services_status)
|
||||
|
||||
@ -15,63 +15,119 @@
|
||||
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
<!-- Core System & Network Stats -->
|
||||
<div class="row">
|
||||
<div class="col-lg-3 col-6">
|
||||
<!-- small box -->
|
||||
<div class="small-box bg-info">
|
||||
<div class="inner">
|
||||
<h3 id="cpu-usage">--<sup style="font-size: 20px">%</sup></h3>
|
||||
<p>CPU Usage</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="fas fa-microchip"></i>
|
||||
<div class="col-md-3 col-sm-6 col-12">
|
||||
<div class="info-box shadow-sm">
|
||||
<span class="info-box-icon bg-info"><i class="fas fa-microchip"></i></span>
|
||||
<div class="info-box-content">
|
||||
<span class="info-box-text">CPU Usage</span>
|
||||
<span class="info-box-number" id="cpu-usage">--</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-6">
|
||||
<!-- small box -->
|
||||
<div class="small-box bg-warning">
|
||||
<div class="inner">
|
||||
<h3 id="ram-usage">--</h3>
|
||||
<p>RAM Usage</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="fas fa-memory"></i>
|
||||
<div class="col-md-3 col-sm-6 col-12">
|
||||
<div class="info-box shadow-sm">
|
||||
<span class="info-box-icon bg-warning"><i class="fas fa-memory"></i></span>
|
||||
<div class="info-box-content">
|
||||
<span class="info-box-text">RAM Usage</span>
|
||||
<span class="info-box-number" id="ram-usage">-- / --</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-6">
|
||||
<!-- small box -->
|
||||
<div class="small-box bg-secondary">
|
||||
<div class="inner">
|
||||
<h3 id="total-traffic">--</h3>
|
||||
<p>Total Traffic</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="fas fa-network-wired"></i>
|
||||
<div class="col-md-3 col-sm-6 col-12">
|
||||
<div class="info-box shadow-sm">
|
||||
<span class="info-box-icon bg-success"><i class="fas fa-users"></i></span>
|
||||
<div class="info-box-content">
|
||||
<span class="info-box-text">Online Users</span>
|
||||
<span class="info-box-number" id="online-users">--</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-6">
|
||||
<!-- small box -->
|
||||
<div class="small-box bg-success">
|
||||
<div class="inner">
|
||||
<h3 id="online-users">--</h3>
|
||||
<p>Online Users</p>
|
||||
<div class="col-md-3 col-sm-6 col-12">
|
||||
<div class="info-box shadow-sm">
|
||||
<span class="info-box-icon bg-secondary"><i class="fas fa-clock"></i></span>
|
||||
<div class="info-box-content">
|
||||
<span class="info-box-text">Uptime</span>
|
||||
<span class="info-box-number" id="uptime">--</span>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="fas fa-users"></i>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- 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="col-lg-3 col-6">
|
||||
<div class="small-box" id="hysteria2-status-box">
|
||||
<div class="inner">
|
||||
<h3 id="hysteria2-status">--</h3>
|
||||
<p>Hysteria2</p>
|
||||
<p>Hysteria2 Core</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="fas fa-bolt"></i>
|
||||
@ -93,10 +149,10 @@
|
||||
<div class="small-box" id="iplimit-status-box">
|
||||
<div class="inner">
|
||||
<h3 id="iplimit-status">--</h3>
|
||||
<p>IP Limit</p>
|
||||
<p>IP Limit Service</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="fas fa-box"></i>
|
||||
<i class="fas fa-ban"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -104,7 +160,7 @@
|
||||
<div class="small-box" id="normalsub-status-box">
|
||||
<div class="inner">
|
||||
<h3 id="normalsub-status">--</h3>
|
||||
<p>Normal Subscription</p>
|
||||
<p>Subscription Service</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="fas fa-rss"></i>
|
||||
@ -122,21 +178,34 @@
|
||||
fetch('{{ url_for("server_status_api") }}')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Core Stats
|
||||
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('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));
|
||||
}
|
||||
|
||||
|
||||
function updateServiceStatuses() {
|
||||
// Add services api in fetch
|
||||
fetch('{{ url_for("server_services_status_api") }}')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
|
||||
updateServiceBox('hysteria2', data.hysteria_server);
|
||||
updateServiceBox('telegrambot', data.hysteria_telegram_bot);
|
||||
updateServiceBox('iplimit', data.hysteria_iplimit);
|
||||
@ -152,20 +221,19 @@
|
||||
if (status === true) {
|
||||
statusElement.textContent = 'Active';
|
||||
statusBox.classList.remove('bg-danger');
|
||||
statusBox.classList.add('bg-success'); // Add green
|
||||
statusBox.classList.add('bg-success');
|
||||
} else {
|
||||
statusElement.textContent = 'Inactive';
|
||||
statusBox.classList.remove('bg-success');
|
||||
statusBox.classList.add('bg-danger'); // Add red
|
||||
statusBox.classList.add('bg-danger');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
updateServerInfo();
|
||||
updateServiceStatuses();
|
||||
|
||||
setInterval(updateServerInfo, 5000);
|
||||
setInterval(updateServerInfo, 2000);
|
||||
setInterval(updateServiceStatuses, 10000);
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@ -22,6 +22,7 @@ urllib3==2.5.0
|
||||
yarl==1.20.1
|
||||
hysteria2-api==0.1.3
|
||||
schedule==1.2.2
|
||||
aiofiles==24.1.0
|
||||
|
||||
# webpanel
|
||||
annotated-types==0.7.0
|
||||
|
||||
Reference in New Issue
Block a user