Initial commit
This commit is contained in:
888
resources/views/livewire/server.blade.php
Normal file
888
resources/views/livewire/server.blade.php
Normal file
@@ -0,0 +1,888 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user