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
Publish Fast Tabler Docker image / build-and-push-fast (push) Has been cancelled
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user