feat: Add server availability check API and integrate it into the Dashboard and ServerManager components for enhanced server monitoring and user experience
Publish Fast Tabler Docker image / build-and-push-fast (push) Has been cancelled

This commit is contained in:
2025-08-11 00:00:07 +07:00
parent d25e8fc810
commit cc8d11cfb1
3 changed files with 118 additions and 5 deletions
+50
View File
@@ -5,6 +5,7 @@ const cors = require('cors');
const path = require('path');
const compression = require('compression');
const Ajv = require('ajv');
const net = require('net');
const app = express();
const port = 3001;
@@ -1395,6 +1396,55 @@ app.get('/api/auto-urls', async (req, res) => {
}
});
// --- Servers availability check ---
// Simple TCP connect check to 80 then 443 with short timeouts
function tcpCheck(host, port, timeoutMs) {
return new Promise((resolve) => {
const socket = new net.Socket();
let settled = false;
const onOk = () => { if (!settled) { settled = true; try{ socket.destroy(); }catch{} resolve(true); } };
const onFail = () => { if (!settled) { settled = true; try{ socket.destroy(); }catch{} resolve(false); } };
socket.setTimeout(timeoutMs);
socket.once('error', onFail);
socket.once('timeout', onFail);
socket.connect(port, host, onOk);
});
}
async function checkOneServer(srv, timeoutMs = 1500) {
const hostCandidates = [];
if (srv.ip) hostCandidates.push(srv.ip);
if (srv.dns) hostCandidates.push(srv.dns);
for (const host of hostCandidates) {
// try 80 then 443
const ok80 = await tcpCheck(host, 80, timeoutMs);
if (ok80) return true;
const ok443 = await tcpCheck(host, 443, timeoutMs);
if (ok443) return true;
}
return false;
}
app.get('/api/servers/availability', async (req, res) => {
try {
const data = await s3.getObject({ Bucket: BUCKET_NAME, Key: 'servers.json' }).promise();
let servers = [];
try {
servers = JSON.parse(data.Body.toString('utf-8'));
if (!Array.isArray(servers)) servers = [];
} catch {
servers = [];
}
const checks = await Promise.allSettled(servers.map(s => checkOneServer(s)));
const statuses = servers.map((s, i) => ({ ip: s.ip, dns: s.dns, online: checks[i].status === 'fulfilled' ? Boolean(checks[i].value) : false }));
const online = statuses.filter(x => x.online).length;
res.json({ online, total: servers.length, statuses });
} catch (e) {
console.error('availability error', e);
res.status(500).json({ online: 0, total: 0, statuses: [] });
}
});
// Update auto URLs in S3
app.post('/api/auto-urls', async (req, res) => {
const { urls } = req.body;
+4 -2
View File
@@ -84,6 +84,7 @@ function Dashboard() {
axios.get('/api/ip-ranges'),
axios.get('/api/asns'),
axios.get('/api/servers'),
axios.get('/api/servers/availability'),
axios.get('/api/s3/last-modified')
]);
@@ -92,7 +93,8 @@ function Dashboard() {
const ipRangesRes = results[1];
const asnsRes = results[2];
const serversRes = results[3];
const s3Res = results[4];
const availabilityRes = results[4];
const s3Res = results[5];
// Получаем данные серверов для подсчета дополнительной статистики
const domains = domainsRes.status === 'fulfilled' ? domainsRes.value.data : [];
@@ -102,7 +104,7 @@ function Dashboard() {
setRaw({ domains, ipRanges, asns, servers });
const countries = new Set(servers.map(server => server.country).filter(Boolean));
const providers = new Set(servers.map(server => server.provider).filter(Boolean));
const onlineServers = servers.filter(server => server.status === 'Онлайн').length;
const onlineServers = availabilityRes.status === 'fulfilled' ? (availabilityRes.value.data?.online || 0) : (servers.filter(server => server.status === 'Онлайн').length);
// Получаем дату последнего обновления
// Новый формат: объект с ключами { domainsNew, asns, servers, filters, ipRanges }
+64 -3
View File
@@ -15,7 +15,9 @@ import {
IconAlertTriangle,
IconServer,
IconChevronDown,
IconLink
IconLink,
IconFilter,
IconMapPin
} from '@tabler/icons-react';
import GraphView from './GraphView';
@@ -66,6 +68,11 @@ function ServerManager() {
type: 'routes',
version: 'v4.rsc'
});
const presets = {
default: { baseUrl: 'https://functions.yandexcloud.net/d4eno3im0qgsr4tj5hdo', cloudflare_gateway: 'SWE-IHOR', bunny_gateway: 'SWE-IHOR', fastly_gateway: 'SWE-IHOR', telegram_gateway: '94.142.140.1', hetzner_gateway: '94.142.140.1', type: 'routes', version: 'v4.rsc' },
cloudflareSWE: { baseUrl: 'https://cf.example/api', cloudflare_gateway: 'SWE-IHOR', bunny_gateway: '', fastly_gateway: '', telegram_gateway: '', hetzner_gateway: '', type: 'routes', version: 'v4.rsc' },
hetznerDE: { baseUrl: 'https://hetzner.example/api', cloudflare_gateway: '', bunny_gateway: '', fastly_gateway: '', telegram_gateway: '', hetzner_gateway: '94.142.140.1', type: 'routes', version: 'v4.rsc' }
};
// Состояние для мониторинга сети
const [networkStatus, setNetworkStatus] = useState({});
@@ -81,6 +88,11 @@ function ServerManager() {
]);
const [newConnection, setNewConnection] = useState({ from: '', to: '', tunnelType: 'GRE', ipA: '', ipB: '' });
// Фильтры графа
const [graphTunnelFilters, setGraphTunnelFilters] = useState({ GRE: true, IPSec: true, WireGuard: true, OpenVPN: true });
const [graphCountryFilter, setGraphCountryFilter] = useState('');
const [graphProviderFilter, setGraphProviderFilter] = useState('');
useEffect(() => {
fetchServers();
}, []);
@@ -207,6 +219,15 @@ function ServerManager() {
return `${urlSettings.baseUrl}?${params.toString()}`;
};
const applyPreset = (name) => {
const p = presets[name];
if (!p) return;
setUrlSettings(prev => ({ ...prev, ...p }));
localStorage.setItem('urlSettings', JSON.stringify({ ...urlSettings, ...p }));
setSuccess('Профиль применён');
setTimeout(() => setSuccess(''), 2000);
};
// Функция резервного копирования для http
const fallbackCopyToClipboard = (text) => {
const textArea = document.createElement('textarea');
@@ -244,9 +265,13 @@ function ServerManager() {
};
const exportAllLinks = () => {
const filtered = servers
.filter(s => (!filterTunnel || s.tunnel === filterTunnel))
.filter(s => (!filterCountry || s.country === filterCountry))
.filter(s => (!filterProvider || s.provider === filterProvider));
const csvContent = [
['IP', 'DNS', 'Country', 'Provider', 'Tunnel', 'Generated URL'],
...servers.map(server => [
...filtered.map(server => [
server.ip,
server.dns,
server.country,
@@ -456,6 +481,17 @@ function ServerManager() {
<span className="me-2 d-flex align-items-center"><IconPlus className="icon" /></span>
<span className="fw-bold">Добавить</span>
</button>
{/* Пресеты генератора ссылок */}
<div className="dropdown">
<button className="btn btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown">
Профили ссылок
</button>
<div className="dropdown-menu">
{Object.keys(presets).map(key => (
<button key={key} className="dropdown-item" type="button" onClick={() => applyPreset(key)}>{key}</button>
))}
</div>
</div>
<button
type="button"
className="btn btn-outline-primary d-flex align-items-center"
@@ -705,7 +741,32 @@ function ServerManager() {
<div className="text-muted small">Поддержка: GRE, IPSec, WireGuard, OpenVPN</div>
</div>
<div className="card-body">
<GraphView servers={servers} connections={connections} />
<div className="mb-3 d-flex flex-wrap gap-2 align-items-center">
<span className="text-muted small d-flex align-items-center"><IconFilter size={16} className="me-1" />Фильтры графа:</span>
<div className="btn-group btn-group-sm" role="group" aria-label="Туннели">
{['GRE','IPSec','WireGuard','OpenVPN'].map(t => (
<button key={t} className={`btn ${graphTunnelFilters[t] ? 'btn-primary' : 'btn-outline-primary'}`} onClick={() => setGraphTunnelFilters(prev => ({...prev, [t]: !prev[t]}))}>{t}</button>
))}
</div>
<div className="d-flex align-items-center ms-2">
<IconMapPin size={16} className="me-1 text-muted" />
<select className="form-select form-select-sm" value={graphCountryFilter} onChange={e => setGraphCountryFilter(e.target.value)}>
<option value="">Все страны</option>
{Array.from(new Set(servers.map(s => s.country).filter(Boolean))).map(c => (<option key={c} value={c}>{c}</option>))}
</select>
</div>
<div className="d-flex align-items-center">
<IconServer size={16} className="me-1 text-muted" />
<select className="form-select form-select-sm" value={graphProviderFilter} onChange={e => setGraphProviderFilter(e.target.value)}>
<option value="">Все провайдеры</option>
{Array.from(new Set(servers.map(s => s.provider).filter(Boolean))).map(p => (<option key={p} value={p}>{p}</option>))}
</select>
</div>
</div>
<GraphView
servers={servers.filter(s => (!graphCountryFilter || s.country === graphCountryFilter) && (!graphProviderFilter || s.provider === graphProviderFilter))}
connections={connections.filter(c => graphTunnelFilters[c.tunnelType])}
/>
<div className="mt-3">
<div className="d-flex align-items-center gap-3 flex-wrap">
<div className="d-flex align-items-center gap-2"><span style={{display:'inline-block',width:24,height:4,background:'#206bc4'}}></span><span className="text-muted small">GRE</span></div>