Files
ahrommag/resources/views/livewire/server.blade.php
2025-11-16 12:43:07 +03:30

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>