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
+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>