888 lines
41 KiB
PHP
888 lines
41 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="fa">
|
|
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Ahrom Server Status</title>
|
|
<script defer src="{{ asset('server_status/alpins.js') }}"></script>
|
|
<script src="{{ asset('server_status/tailwind.js') }}"></script>
|
|
<script src="{{ asset('server_status/index.js') }}"></script>
|
|
<script src="{{ asset('server_status/map.js') }}"></script>
|
|
<script src="{{ asset('server_status/worldLow.js') }}"></script>
|
|
<style>
|
|
/* Gauge Wrapper */
|
|
.gauge {
|
|
width: 90%;
|
|
height: 100%;
|
|
position: relative;
|
|
}
|
|
|
|
/* Gauge Arc */
|
|
.gauge-arc {
|
|
width: 100%;
|
|
height: 200%;
|
|
/* semicircle */
|
|
border-radius: 50%;
|
|
clip-path: inset(0 0 50% 0);
|
|
--color: limegreen;
|
|
--deg: 0deg;
|
|
background-image: radial-gradient(closest-side,
|
|
#1e1e1e 0%,
|
|
#1e1e1e calc(100% - 1rem),
|
|
transparent calc(100% - 1rem)),
|
|
conic-gradient(from -90deg,
|
|
var(--color) 0deg var(--deg),
|
|
#121212 var(--deg) 180deg,
|
|
transparent 180deg);
|
|
transition: --color 0.6s ease, --deg 0.4s ease;
|
|
}
|
|
|
|
/* Gauge Info */
|
|
.gauge-info {
|
|
position: absolute;
|
|
bottom: 0px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 3px;
|
|
color: #e2e2e2;
|
|
text-align: center;
|
|
}
|
|
|
|
.percent-number {
|
|
font-size: clamp(1.3rem, 6vw, 0.6rem);
|
|
font-weight: bold;
|
|
color: #e2e2e2;
|
|
}
|
|
|
|
.percent-sign {
|
|
font-size: clamp(1rem, 3vw, 0.5rem);
|
|
font-weight: bold;
|
|
margin-left: 4px;
|
|
transition: color 0.4s ease;
|
|
}
|
|
|
|
.usage {
|
|
/* font-size: clamp(0.7rem, 1.2vw, 0.4rem); */
|
|
opacity: 0.85;
|
|
text-shadow: 0 0 6px #000;
|
|
}
|
|
|
|
/* Neon Badge */
|
|
/* .neon-badge {
|
|
font-size: 0.9rem;
|
|
font-weight: bold;
|
|
padding: 6px 14px;
|
|
border-radius: 9999px;
|
|
background: rgba(0, 0, 0, 0.4);
|
|
color: var(--badge-color, limegreen);
|
|
text-shadow: 0 0 2px #000, 0 0 3px var(--badge-color),
|
|
0 0 6px var(--badge-color), 0 0 12px var(--badge-color);
|
|
box-shadow: 0 0 4px var(--badge-color),
|
|
0 0 8px var(--badge-color),
|
|
inset 0 0 2px rgba(255, 255, 255, 0.1);
|
|
transition: all 0.3s ease;
|
|
} */
|
|
|
|
.grid-cols-7>div {
|
|
padding: 4px;
|
|
background-color: #1e1e1e;
|
|
}
|
|
|
|
.col-span-5>div {
|
|
padding: 4px;
|
|
background-color: #1e1e1e;
|
|
}
|
|
|
|
.pulse-connected {
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0% {
|
|
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
|
|
}
|
|
|
|
70% {
|
|
box-shadow: 0 0 0 10px rgba(16, 185, 129, 0);
|
|
}
|
|
|
|
100% {
|
|
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
|
|
}
|
|
}
|
|
|
|
.fade-in {
|
|
animation: fadeIn 0.5s ease-in;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from {
|
|
opacity: 0;
|
|
}
|
|
|
|
to {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body class="bg-[#121212] h-screen overflow-hidden">
|
|
<div x-data="fullscreenHandler()" x-init="init()"
|
|
class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 w-[40%]">
|
|
<button x-show="!isFullscreen" @click="enterFullscreen()"
|
|
class="px-5 py-32 bg-amber-500 hover:bg-amber-400 rounded-lg w-full text-6xl font-bold">
|
|
Fullscreen mode
|
|
</button>
|
|
</div>
|
|
|
|
<div x-data="serverMetrics()" x-init="init()" class="h-full">
|
|
<!-- Grid row at 25% of viewport height -->
|
|
<div class="grid grid-cols-7 h-[25vh] max-h-[25vh] m-3 mb-0 items-stretch rounded-2xl gap-2">
|
|
<!-- Cell 1: Gauge -->
|
|
<div x-data="{
|
|
get percent() { return data?.cpu?.usage || 0 },
|
|
get color() {
|
|
if (this.percent <= 50) return 'limegreen';
|
|
if (this.percent <= 70) return 'yellow';
|
|
return 'red';
|
|
}
|
|
}" class="flex flex-col items-center justify-center h-full w-full overflow-hidden rounded-2xl"
|
|
x-effect="
|
|
$el.querySelector('.gauge-arc').style.setProperty('--color', color);
|
|
$el.querySelector('.gauge-arc').style.setProperty('--deg', (percent * 1.8) + 'deg');
|
|
">
|
|
<div class="gauge">
|
|
<div class="gauge-arc"></div>
|
|
<div class="gauge-info">
|
|
<div class="flex flex-col items-center justify-center gap-2">
|
|
<div class="flex items-center justify-center">
|
|
<span class="percent-number leading-none" x-text="percent"></span>
|
|
<span class="percent-sign">%</span>
|
|
</div>
|
|
<!-- <div class="usage text-[#e2e2e2]" x-text="used + ' MB / ' + Math.round(total/1024) + ' GB'"></div> -->
|
|
<div class="neon-badge text-sm font-semibold">CPU Usage</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div x-data="{
|
|
get percent() { return data?.memory?.usagePercent || 0 },
|
|
get color() {
|
|
if (this.percent <= 50) return 'limegreen';
|
|
if (this.percent <= 70) return 'yellow';
|
|
return 'red';
|
|
}
|
|
}" class="flex flex-col items-center justify-center h-full w-full overflow-hidden rounded-2xl"
|
|
x-effect="
|
|
$el.querySelector('.gauge-arc').style.setProperty('--color', color);
|
|
$el.querySelector('.gauge-arc').style.setProperty('--deg', (percent * 1.8) + 'deg');
|
|
">
|
|
<div class="gauge">
|
|
<div class="gauge-arc"></div>
|
|
<div class="gauge-info">
|
|
<div class="flex items-center justify-center">
|
|
<span class="percent-number leading-none" x-text="percent"></span>
|
|
<span class="percent-sign">%</span>
|
|
</div>
|
|
<div class="usage text-[#e2e2e2] text-xs"
|
|
x-text="data?.memory ? `${formatBytes(data.memory.used)} / ${formatBytes(data.memory.total)}` : '--'">
|
|
</div>
|
|
<div class="neon-badge text-sm font-semibold">Memory Usage</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<section class="grid grid-cols-2 col-span-5 items-stretch rounded-2xl gap-2">
|
|
<div class="flex flex-col items-center justify-center h-full w-full text-[#e2e2e2] gap-3 rounded-2xl">
|
|
<dt class="text-2xl font-bold text-[#e2e2e2] truncate">Uptime</dt>
|
|
<dd class="flex items-center">
|
|
<div class="text-xl font-semibold text-[#e2e2e2]" style="word-spacing: 190px;"
|
|
x-text="data?.uptime ? `${data.uptime.days}d ${data.uptime.hours}h ${data.uptime.minutes}m` : '--'">
|
|
</div>
|
|
</dd>
|
|
</div>
|
|
<div class="flex flex-col items-center justify-center h-full w-full text-[#e2e2e2] gap-2 rounded-2xl">
|
|
<dt class="text-2xl font-bold text-[#e2e2e2]">System Load</dt>
|
|
<div class="grid grid-cols-3 gap-2 w-full">
|
|
<div class="text-center flex flex-col gap-2">
|
|
<div class="text-md font-semibold text-[#e2e2e2]">1 min</div>
|
|
<div class="text-xl font-semibold text-[#e2e2e2]" x-text="data?.load?.one || '--'"></div>
|
|
</div>
|
|
<div class="text-center flex flex-col gap-2">
|
|
<div class="text-md font-semibold text-[#e2e2e2]">5 min</div>
|
|
<div class="text-xl font-semibold text-[#e2e2e2]" x-text="data?.load?.five || '--'"></div>
|
|
</div>
|
|
<div class="text-center flex flex-col gap-2">
|
|
<div class="text-md font-semibold text-[#e2e2e2]">15 min</div>
|
|
<div class="text-xl font-semibold text-[#e2e2e2]" x-text="data?.load?.fifteen || '--'">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Cell 1: Gauge -->
|
|
<div x-data="{
|
|
get percent() { return (data?.disk?.percent || '0').replace('%', '') },
|
|
get color() {
|
|
if (this.percent <= 50) return 'limegreen';
|
|
if (this.percent <= 70) return 'yellow';
|
|
return 'red';
|
|
}
|
|
}" class="flex flex-col items-center justify-center h-full w-full overflow-hidden rounded-2xl"
|
|
x-effect="
|
|
$el.querySelector('.gauge-arc').style.setProperty('--color', color);
|
|
$el.querySelector('.gauge-arc').style.setProperty('--deg', (percent * 1.8) + 'deg');
|
|
">
|
|
<div class="gauge">
|
|
<div class="gauge-arc"></div>
|
|
<div class="gauge-info">
|
|
<div class="flex items-center justify-center">
|
|
<span class="percent-number leading-none" x-text="percent"></span>
|
|
<span class="percent-sign">%</span>
|
|
</div>
|
|
<div class="usage text-[#e2e2e2] text-xs"
|
|
x-text="data?.disk ? `${data.disk.used} / ${data.disk.size}` : '--'"></div>
|
|
<div class="neon-badge text-sm font-semibold">Disk Usage</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div x-data="{
|
|
get percent() { return parseFloat(((data?.swap?.used || 0) / 8000) * 100).toFixed(1) },
|
|
get color() {
|
|
if (this.percent <= 50) return 'limegreen';
|
|
if (this.percent <= 70) return 'yellow';
|
|
return 'red';
|
|
}
|
|
}" class="flex flex-col items-center justify-center h-full w-full overflow-hidden rounded-2xl"
|
|
x-effect="
|
|
$el.querySelector('.gauge-arc').style.setProperty('--color', color);
|
|
$el.querySelector('.gauge-arc').style.setProperty('--deg', (percent * 1.8) + 'deg');
|
|
">
|
|
<div class="gauge">
|
|
<div class="gauge-arc"></div>
|
|
<div class="gauge-info">
|
|
<div class="flex items-center justify-center">
|
|
<span class="percent-number leading-none" x-text="percent"></span>
|
|
<span class="percent-sign">%</span>
|
|
</div>
|
|
<div class="usage text-[#e2e2e2] text-xs"
|
|
x-text="data?.swap ? `${data?.swap?.used} MB / ${8000} GB` : '--'"></div>
|
|
<div class="neon-badge text-sm font-semibold">Swap Usage</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<section class="grid grid-cols-2 col-span-5 items-stretch rounded-2xl gap-2">
|
|
<div class="flex flex-col items-center justify-center h-full w-full text-[#e2e2e2] gap-2 rounded-2xl">
|
|
<dt class="text-2xl font-bold text-[#e2e2e2]">System Info</dt>
|
|
<div class="grid grid-cols-3 gap-2 w-full">
|
|
<div class="text-center flex flex-col gap-2">
|
|
<div class="text-md font-medium text-[#e2e2e2]">Processes</div>
|
|
<div class="text-xl font-semibold text-[#e2e2e2]" x-text="data?.processCount || '--'"></div>
|
|
</div>
|
|
<div class="text-center flex flex-col gap-2">
|
|
<div class="text-md font-medium text-[#e2e2e2]">Pending Updates</div>
|
|
<div class="text-xl font-semibold text-[#e2e2e2]" x-text="data?.updates?.total || '--'">
|
|
</div>
|
|
</div>
|
|
<div class="text-center flex flex-col gap-2">
|
|
<div class="text-md font-medium text-[#e2e2e2]">Needs Reboot</div>
|
|
<div :class="`text-xl font-semibold ${(data?.needsReboot !== undefined && data?.needsReboot) ? 'text-red-600' : 'text-[#e2e2e2]'}`"
|
|
x-text="data?.needsReboot !== undefined ? (data.needsReboot ? 'Yes' : 'No') : '--'">
|
|
</div>
|
|
</div>
|
|
<!-- <div class="text-center flex flex-col gap-2">
|
|
<div class="text-sm font-medium text-[#e2e2e2]">Disk Space</div>
|
|
<div class="text-md font-semibold text-[#e2e2e2]"
|
|
x-text="data?.disk ? `${data.disk.used} / ${data.disk.size}` : '--'">
|
|
</div>
|
|
</div> -->
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-col items-center justify-center h-full w-full text-[#e2e2e2] gap-2 rounded-2xl">
|
|
<dt class="text-2xl font-bold text-[#e2e2e2]">Network Usage</dt>
|
|
<div class="grid grid-cols-2 gap-2 w-full">
|
|
<div class="text-center flex flex-col gap-2">
|
|
<div class="text-md font-medium text-[#e2e2e2]">Today</div>
|
|
<div class="text-xl font-semibold text-[#e2e2e2]"
|
|
x-text="data?.network?.today ? formatMB(data.network.today.totalMB) : '--'">
|
|
</div>
|
|
</div>
|
|
<div class="text-center flex flex-col gap-2">
|
|
<div class="text-md font-medium text-[#e2e2e2]">This Month</div>
|
|
<div class="text-xl font-semibold text-[#e2e2e2]"
|
|
x-text="data?.network?.month ? formatMB(data.network.month.totalMB) : '--'">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<!-- <div class="flex items-center justify-center h-full w-full text-[#e2e2e2] rounded-2xl">
|
|
7
|
|
</div> -->
|
|
</div>
|
|
<div class="w-full max-h-[71vh] flex justify-between px-3 mt-3">
|
|
<div class="mr-3 w-[20vw] h-[71vh] max-h-[71vh] overflow-hidden bg-[#1e1e1e] rounded-xl relative"
|
|
x-data="ipRequests()" x-init="init()">
|
|
<div class="px-2 py-2 sm:px-6 border-b border-[#e2e2e2] flex justify-between items-center">
|
|
<h2 class="text-lg font-medium text-[#e2e2e2]">IP Requests</h2>
|
|
<div class="flex items-center space-x-2">
|
|
<span class="h-3 w-3 rounded-full"
|
|
:class="connected ? 'bg-green-500 pulse-connected' : 'bg-red-500'"></span>
|
|
<span x-text="connected ? 'Connected' : 'Disconnected'" class="text-sm text-[#e2e2e2]"></span>
|
|
</div>
|
|
</div>
|
|
<div class="px-2 py-2 sm:p-3">
|
|
<div x-show="loading" class="text-center py-8">
|
|
<div
|
|
class="inline-block animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500">
|
|
</div>
|
|
<p class="mt-2 text-[#e2e2e2]">Loading IP requests...</p>
|
|
</div>
|
|
<div x-show="!loading && data?.length" class="fade-in">
|
|
<table class="min-w-full">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col"
|
|
class="px-2 py-2 text-center text-xs font-medium text-[#e2e2e2] uppercase tracking-wider border-b border-[#a1a1a1]">
|
|
IP Address</th>
|
|
<th scope="col"
|
|
class="px-2 py-2 text-center text-xs font-medium text-[#e2e2e2] uppercase tracking-wider border-b border-[#a1a1a1]">
|
|
Requests</th>
|
|
<th scope="col"
|
|
class="px-2 py-2 text-center text-xs font-medium text-[#e2e2e2] uppercase tracking-wider border-b border-[#a1a1a1]">
|
|
Last Activity</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-[#121212]">
|
|
<template x-for="(item, index) in data" :key="index">
|
|
<tr
|
|
:class="item.count > 15000 ? 'bg-rose-500' : ((index % 2 == 0) ? 'bg-[#121212]' : 'bg-[#1e1e1e]')">
|
|
<td class="px-3 py-2 whitespace-nowrap text-sm font-medium text-center text-[#e2e2e2] border-r border-[#a1a1a1]"
|
|
x-text="item.ip"></td>
|
|
<td class="px-3 py-2 whitespace-nowrap text-sm text-center text-[#e2e2e2] border-r border-[#a1a1a1]"
|
|
x-text="item.count">
|
|
</td>
|
|
<td class="px-3 py-2 whitespace-nowrap text-sm text-center text-[#e2e2e2]"
|
|
x-text="item?.last?.replace('.000', '')">
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div x-show="!loading && !data?.length" class="text-center py-8 text-[#e2e2e2]">
|
|
No IP request data available
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div x-data="ipCountrySettings()" x-init="init()"
|
|
class=" w-[71vw] grow h-[71vh] max-h-[71vh] overflow-hidden bg-[#1e1e1e] rounded-xl relative">
|
|
<template x-if="loading">
|
|
<div class="text-center text-[#e2e2e2] mb-4">Loading map data...</div>
|
|
</template>
|
|
|
|
<div id="chartdiv" x-show="!loading" class="shadow-lg rounded-md w-full h-full"></div>
|
|
</div>
|
|
</div>
|
|
<!-- <div class="m-3 max-h-[50%] overflow-hidden bg-[#1e1e1e] rounded-xl relative border border-[#e2e2e2]"
|
|
x-data="ipRequests()" x-init="init()">
|
|
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 flex justify-between items-center">
|
|
<h2 class="text-lg font-medium text-gray-900">IP Requests</h2>
|
|
<div class="flex items-center space-x-2">
|
|
<span class="h-3 w-3 rounded-full"
|
|
:class="connected ? 'bg-green-500 pulse-connected' : 'bg-red-500'"></span>
|
|
<span x-text="connected ? 'Connected' : 'Disconnected'" class="text-sm text-gray-500"></span>
|
|
</div>
|
|
</div>
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<div x-show="loading" class="text-center py-8">
|
|
<div class="inline-block animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500">
|
|
</div>
|
|
<p class="mt-2 text-gray-500">Loading IP requests...</p>
|
|
</div>
|
|
<div x-show="!loading && data?.length" class="fade-in overflow-x-auto">
|
|
<div class="flex flex-wrap gap-4">
|
|
<template x-for="(chunk, cIndex) in chunkedData()" :key="cIndex">
|
|
<table
|
|
class="bg-[#76ABAE] divide-y divide-gray-200 w-[20%] min-w-[200px] text-sm rounded shadow">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">IP
|
|
Address</th>
|
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">
|
|
Requests</th>
|
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Last
|
|
Activity</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-200">
|
|
<template x-for="(item, index) in chunk" :key="index">
|
|
<tr>
|
|
<td class="px-4 py-2 whitespace-nowrap text-gray-900" x-text="item.ip"></td>
|
|
<td class="px-4 py-2 whitespace-nowrap text-gray-500" x-text="item.count">
|
|
</td>
|
|
<td class="px-4 py-2 whitespace-nowrap text-gray-500" x-text="item.last">
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
<div x-show="!loading && !data?.length" class="text-center py-8 text-gray-500">
|
|
No IP request data available
|
|
</div>
|
|
</div>
|
|
</div> -->
|
|
<!-- <div class="m-3 max-h-[50%] overflow-none bg-[#1e1e1e] rounded-xl relative border border-[#e2e2e2]"
|
|
x-data="ipCountrySettings()" x-init="init()">
|
|
<div
|
|
class="px-4 py-5 sm:px-6 border-b border-gray-200 bg-[#1e1e1e] flex justify-between items-center sticky top-0">
|
|
<h2 class="text-md font-medium text-[#e2e2e2]">IP Country Settings</h2>
|
|
<div class="flex items-center space-x-2">
|
|
<span class="h-3 w-3 rounded-full"
|
|
:class="statusAllConnected ? 'bg-green-500 pulse-connected' : 'bg-red-500'"></span>
|
|
<span x-text="statusAllConnected ? 'Connected' : 'Disconnected'"
|
|
class="text-sm text-[#e2e2e2]"></span>
|
|
</div>
|
|
</div>
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<div x-show="loading" class="text-center py-8">
|
|
<div class="inline-block animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500">
|
|
</div>
|
|
<p class="text-lg mt-2 text-[#e2e2e2]">Loading country settings...</p>
|
|
</div>
|
|
<div x-show="!loading" class="fade-in">
|
|
<div class="flex flex-wrap gap-2">
|
|
<template x-for="country in data" :key="country.country">
|
|
<div class="flex items-center justify-center basis-[2%]">
|
|
<span x-text="country.country.toUpperCase()"
|
|
class="inline-flex items-center justify-center w-[36px] px-1 py-1 rounded-full text-xs font-bold"
|
|
:class="country.accepted ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'"></span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div> -->
|
|
</div>
|
|
|
|
<script>
|
|
function serverMetrics() {
|
|
return {
|
|
connected: false,
|
|
data: null,
|
|
loading: true,
|
|
socket: null,
|
|
reconnectInterval: null,
|
|
heartbeatInterval: null,
|
|
heartbeatTimeout: null,
|
|
|
|
usageColor(usage) {
|
|
usage = parseFloat(usage || 0);
|
|
if (usage <= 25) return 'bg-[#10a900] drop-shadow-2xl drop-shadow-[#10a900]/50';
|
|
if (usage <= 50) return 'bg-[#ffe117] drop-shadow-2xl drop-shadow-[#ffe117]/50';
|
|
if (usage <= 75) return 'bg-[#e30000] drop-shadow-2xl drop-shadow-[#e30000]/50';
|
|
return 'bg-[#4f21d5] drop-shadow-2xl drop-shadow-[#4f21d5]/50';
|
|
},
|
|
|
|
usageGradient(usage) {
|
|
if (usage == 0) {
|
|
return false
|
|
}
|
|
usage = parseFloat(usage || 0);
|
|
if (usage <= 25) return 'drop-shadow-2xl drop-shadow-[#10a900]/50 bg-gradient-to-b from-[#10a900]/100 to-transparent';
|
|
if (usage <= 50) return 'drop-shadow-2xl drop-shadow-[#ffe117]/50 bg-gradient-to-b from-[#ffe117]/100 to-transparent';
|
|
if (usage <= 75) return 'drop-shadow-2xl drop-shadow-[#e30000]/50 bg-gradient-to-b from-[#e30000]/100 to-transparent';
|
|
return 'drop-shadow-2xl drop-shadow-[#4f21d5]/50 bg-gradient-to-b from-[#4f21d5]/100 to-transparent';
|
|
},
|
|
|
|
init() {
|
|
this.connectWebSocket();
|
|
this.startHeartbeat();
|
|
|
|
this.reconnectInterval = setInterval(() => {
|
|
if (!this.connected) {
|
|
console.log('تلاش برای اتصال دوباره به server metrics...');
|
|
this.connectWebSocket();
|
|
}
|
|
}, 60000);
|
|
},
|
|
|
|
startHeartbeat() {
|
|
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
|
|
if (this.heartbeatTimeout) clearTimeout(this.heartbeatTimeout);
|
|
|
|
this.heartbeatInterval = setInterval(() => {
|
|
if (this.connected && this.socket.readyState === WebSocket.OPEN) {
|
|
this.socket.send(JSON.stringify({
|
|
type: 'ping'
|
|
}));
|
|
|
|
this.heartbeatTimeout = setTimeout(() => {
|
|
console.warn('No pong received, closing socket...');
|
|
this.socket.close();
|
|
}, 10000);
|
|
}
|
|
}, 30000);
|
|
},
|
|
|
|
connectWebSocket() {
|
|
if (this.socket) {
|
|
this.socket.close();
|
|
}
|
|
|
|
this.socket = new WebSocket('wss://stream.ahrm.ir/security/usage');
|
|
|
|
this.socket.addEventListener('open', (event) => {
|
|
this.connected = true;
|
|
console.log('Server metrics WebSocket connection opened:', event);
|
|
});
|
|
|
|
this.socket.addEventListener('message', (event) => {
|
|
try {
|
|
const message = JSON.parse(event.data);
|
|
|
|
if (message.type === 'pong') {
|
|
clearTimeout(this.heartbeatTimeout);
|
|
return;
|
|
}
|
|
|
|
this.data = message;
|
|
console.log(this.data);
|
|
this.loading = false;
|
|
} catch (e) {
|
|
console.error('Error parsing server metrics message:', e);
|
|
}
|
|
});
|
|
|
|
this.socket.addEventListener('close', (event) => {
|
|
this.connected = false;
|
|
clearInterval(this.heartbeatInterval);
|
|
clearTimeout(this.heartbeatTimeout);
|
|
console.log('Server metrics WebSocket connection closed:', event);
|
|
});
|
|
|
|
this.socket.addEventListener('error', (event) => {
|
|
this.connected = false;
|
|
clearInterval(this.heartbeatInterval);
|
|
clearTimeout(this.heartbeatTimeout);
|
|
console.error('Server metrics WebSocket error:', event);
|
|
});
|
|
},
|
|
|
|
formatBytes(bytes) {
|
|
if (bytes === 0) return '0 Bytes';
|
|
const k = 1024;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
},
|
|
|
|
formatMB(mb) {
|
|
if (mb < 1024) return mb.toFixed(2) + ' MB';
|
|
return (mb / 1024).toFixed(2) + ' GB';
|
|
}
|
|
}
|
|
}
|
|
|
|
function ipRequests() {
|
|
return {
|
|
connected: false,
|
|
data: null,
|
|
loading: true,
|
|
socket: null,
|
|
reconnectInterval: null,
|
|
heartbeatInterval: null,
|
|
heartbeatTimeout: null,
|
|
|
|
init() {
|
|
this.connectWebSocket();
|
|
this.startHeartbeat();
|
|
|
|
this.reconnectInterval = setInterval(() => {
|
|
if (!this.connected) {
|
|
console.log('تلاش برای اتصال دوباره ipRequests...');
|
|
this.connectWebSocket();
|
|
}
|
|
}, 60000);
|
|
},
|
|
|
|
startHeartbeat() {
|
|
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
|
|
if (this.heartbeatTimeout) clearTimeout(this.heartbeatTimeout);
|
|
|
|
this.heartbeatInterval = setInterval(() => {
|
|
if (this.connected && this.socket.readyState === WebSocket.OPEN) {
|
|
this.socket.send(JSON.stringify({
|
|
type: 'ping'
|
|
}));
|
|
|
|
this.heartbeatTimeout = setTimeout(() => {
|
|
console.warn('No pong received, closing socket...');
|
|
this.socket.close();
|
|
}, 10000);
|
|
}
|
|
}, 30000);
|
|
},
|
|
|
|
chunkedData() {
|
|
if (!this.data) return [];
|
|
|
|
const chunks = [];
|
|
const itemsPerChunk = 6;
|
|
const maxChunks = 10;
|
|
|
|
for (let i = 0; i < Math.min(this.data.length, maxChunks * itemsPerChunk); i += itemsPerChunk) {
|
|
chunks.push(this.data.slice(i, i + itemsPerChunk));
|
|
}
|
|
|
|
return chunks;
|
|
},
|
|
|
|
connectWebSocket() {
|
|
if (this.socket) {
|
|
this.socket.close();
|
|
}
|
|
|
|
this.socket = new WebSocket('wss://stream.ahrm.ir/security/ip-request/');
|
|
|
|
this.socket.addEventListener('open', (event) => {
|
|
this.connected = true;
|
|
console.log('IP requests WebSocket connection opened:', event);
|
|
});
|
|
|
|
this.socket.addEventListener('message', (event) => {
|
|
try {
|
|
const message = JSON.parse(event.data);
|
|
|
|
if (message.type === 'pong') {
|
|
clearTimeout(this.heartbeatTimeout);
|
|
return;
|
|
}
|
|
|
|
this.data = message;
|
|
this.loading = false;
|
|
} catch (e) {
|
|
console.error('Error parsing IP requests message:', e);
|
|
}
|
|
});
|
|
|
|
this.socket.addEventListener('close', (event) => {
|
|
this.connected = false;
|
|
clearInterval(this.heartbeatInterval);
|
|
clearTimeout(this.heartbeatTimeout);
|
|
console.log('IP requests WebSocket connection closed:', event);
|
|
});
|
|
|
|
this.socket.addEventListener('error', (event) => {
|
|
this.connected = false;
|
|
clearInterval(this.heartbeatInterval);
|
|
clearTimeout(this.heartbeatTimeout);
|
|
console.error('IP requests WebSocket error:', event);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function ipCountrySettings() {
|
|
return {
|
|
connected: false,
|
|
data: null,
|
|
loading: true,
|
|
socket: null,
|
|
reconnectInterval: null,
|
|
heartbeatInterval: null,
|
|
heartbeatTimeout: null,
|
|
statusAllConnected: true,
|
|
root: null,
|
|
chart: null,
|
|
countryMap: {},
|
|
|
|
init() {
|
|
if (!this.root) {
|
|
this.renderMap(); // Only once!
|
|
}
|
|
this.connectWebSocket();
|
|
this.startHeartbeat();
|
|
|
|
this.reconnectInterval = setInterval(() => {
|
|
if (!this.connected) {
|
|
console.log('تلاش برای اتصال دوباره به IP country settings...');
|
|
this.connectWebSocket();
|
|
}
|
|
}, 60000);
|
|
},
|
|
|
|
startHeartbeat() {
|
|
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
|
|
if (this.heartbeatTimeout) clearTimeout(this.heartbeatTimeout);
|
|
|
|
this.heartbeatInterval = setInterval(() => {
|
|
if (this.connected && this.socket.readyState === WebSocket.OPEN) {
|
|
this.socket.send(JSON.stringify({
|
|
type: 'ping'
|
|
}));
|
|
|
|
this.heartbeatTimeout = setTimeout(() => {
|
|
console.warn('No pong received, closing socket...');
|
|
this.socket.close();
|
|
}, 10000);
|
|
}
|
|
}, 30000);
|
|
},
|
|
|
|
updateConnectionStatus() {
|
|
this.statusAllConnected = !this.data.some(country => !country.accepted);
|
|
},
|
|
destroy() {
|
|
if (this.root) {
|
|
this.root.dispose();
|
|
this.root = null;
|
|
}
|
|
},
|
|
|
|
connectWebSocket() {
|
|
if (this.socket) {
|
|
this.socket.close();
|
|
}
|
|
|
|
this.socket = new WebSocket('wss://stream.ahrm.ir/security/ipset');
|
|
|
|
this.socket.addEventListener('open', (event) => {
|
|
this.connected = true;
|
|
console.log('IP country settings WebSocket connection opened:', event);
|
|
});
|
|
|
|
this.socket.addEventListener('message', (event) => {
|
|
try {
|
|
const message = JSON.parse(event.data);
|
|
|
|
if (message.type === 'pong') {
|
|
clearTimeout(this.heartbeatTimeout);
|
|
return;
|
|
}
|
|
|
|
this.data = message?.ipCountry || [];
|
|
|
|
// ✅ Step 1: Create a fast lookup map for countries
|
|
this.countryMap = {};
|
|
this.data.forEach(c => {
|
|
if (c.country) {
|
|
this.countryMap[c.country.toLowerCase()] = c;
|
|
}
|
|
});
|
|
|
|
this.updateConnectionStatus();
|
|
this.loading = false;
|
|
|
|
// ✅ Step 3: Refresh the map by re-setting the geoJSON
|
|
if (this.polygonSeries) {
|
|
const currentGeoJSON = this.polygonSeries.get("geoJSON");
|
|
|
|
// Force a redraw to trigger fill adapters
|
|
this.polygonSeries.set("geoJSON", null);
|
|
this.polygonSeries.set("geoJSON", currentGeoJSON);
|
|
}
|
|
} catch (e) {
|
|
console.error('Error parsing IP country settings message:', e);
|
|
}
|
|
});
|
|
|
|
this.socket.addEventListener('close', (event) => {
|
|
this.connected = false;
|
|
clearInterval(this.heartbeatInterval);
|
|
clearTimeout(this.heartbeatTimeout);
|
|
console.log('IP country settings WebSocket connection closed:', event);
|
|
});
|
|
|
|
this.socket.addEventListener('error', (event) => {
|
|
this.connected = false;
|
|
clearInterval(this.heartbeatInterval);
|
|
clearTimeout(this.heartbeatTimeout);
|
|
console.error('IP country settings WebSocket error:', event);
|
|
});
|
|
},
|
|
renderMap() {
|
|
if (this.chart) {
|
|
this.chart.dispose();
|
|
}
|
|
if (this.root) {
|
|
this.root.dispose();
|
|
}
|
|
|
|
am5.ready(() => {
|
|
let root = am5.Root.new("chartdiv");
|
|
|
|
root.setThemes([am5.Theme.new(root)]);
|
|
|
|
let chart = root.container.children.push(
|
|
am5map.MapChart.new(root, {
|
|
panX: "rotateX",
|
|
panY: "rotateY",
|
|
projection: am5map.geoMercator(),
|
|
wheelY: "zoom"
|
|
})
|
|
);
|
|
|
|
let polygonSeries = chart.series.push(
|
|
am5map.MapPolygonSeries.new(root, {
|
|
geoJSON: am5geodata_worldLow,
|
|
exclude: ["AQ"] // exclude Antarctica if you want
|
|
})
|
|
);
|
|
|
|
// Add adapter here:
|
|
polygonSeries.mapPolygons.template.adapters.add("fill", (fill, target) => {
|
|
const countryId = target.dataItem.get("id").toLowerCase();
|
|
|
|
const countryData = this.countryMap?.[countryId];
|
|
|
|
if (countryData) {
|
|
return countryData.accepted ? am5.color(0x22c55e) : am5.color(0xef4444);
|
|
}
|
|
|
|
return am5.color(0xcbd5e1); // default color
|
|
});
|
|
|
|
polygonSeries.mapPolygons.template.setAll({
|
|
tooltipText: "{name}",
|
|
interactive: true
|
|
});
|
|
|
|
this.chart = chart;
|
|
this.polygonSeries = polygonSeries;
|
|
this.root = root;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function fullscreenHandler() {
|
|
return {
|
|
isFullscreen: false,
|
|
|
|
init() {
|
|
// Watch for fullscreen change event
|
|
document.addEventListener('fullscreenchange', () => {
|
|
this.isFullscreen = !!document.fullscreenElement;
|
|
});
|
|
},
|
|
|
|
enterFullscreen() {
|
|
document.documentElement.requestFullscreen();
|
|
}
|
|
};
|
|
}
|
|
</script>
|
|
</body>
|
|
|
|
</html> |