import React, { useEffect, useState } from 'react';
import { formatRelative, formatBytes } from '../utils/format.js';
import { flagFor } from '../utils/country.js';
import { api } from '../api.js';
function StatusHero({ state, status }) {
const text = {
running: { title: '🟢 VPN-шлюз работает', kind: 'success' },
applying: { title: '🟠 Применяем изменения…', kind: 'warning' },
error: { title: '🔴 Ошибка', kind: 'danger' },
stopped: { title: '⚫ Шлюз остановлен', kind: 'neutral' },
no_config: { title: '⚪ Шлюз не настроен', kind: 'neutral' },
}[status];
const userInfo = state?.userInfo;
const traffic = userInfo
? `${formatBytes((userInfo.upload || 0) + (userInfo.download || 0))} / ${userInfo.total ? formatBytes(userInfo.total) : 'без лимита'}`
: 'нет данных';
return (
{text.title}
{state?.appliedAt ? `Последнее применение: ${formatRelative(state.appliedAt)}` : 'Конфиг ещё не применялся'}
{state?.singboxRunning ? 'sing-box online' : 'sing-box offline'}
Активный сервер
{state?.selectedTag ? (
<>
{flagFor({ tag: state.selectedTag })} {state.selectedTag}
>
) : Не выбран}
Правил маршрутизации
{(state?.customRules || []).filter(r => r.enabled).length} активных
);
}
function QuickActions({ state, busy, onRestart, onStop, onShowConfig, onNav, onBypassToggle }) {
return (
Быстрые действия
);
}
function RecentEvents({ onNav }) {
const [entries, setEntries] = useState([]);
useEffect(() => {
let cancelled = false;
fetch('/api/logs')
.then((r) => r.json())
.then((data) => {
if (cancelled) return;
const list = (data.logs || []).slice(-15).reverse();
setEntries(list);
})
.catch(() => {});
return () => { cancelled = true; };
}, []);
return (
Последние события
{entries.length === 0 ? (
Пока ничего нет.
) : (
{entries.slice(0, 8).map((e, i) => {
const dot = e.level === 'error' ? 'danger'
: e.level === 'warning' ? 'warning'
: 'success';
const time = new Date(e.ts).toLocaleTimeString('ru-RU', { hour12: false });
return (
{time}
{e.line}
);
})}
)}
);
}
function RoutingSummary({ state, onNav, onFlushDirectCache }) {
const rules = state?.customRules || [];
const enabled = rules.filter((r) => r.enabled).length;
const cacheCount = state?.directBypassCount || 0;
const cacheAvailable = state?.directBypassAvailable && state?.directBypassEnabled;
const transparentDefault = state?.devicesConfig?.defaultTransparentMode || 'direct';
const proxyDefault = state?.devicesConfig?.proxyDefaultMode || 'vpn';
return (
Маршрутизация
Private IP→ direct
{state?.routingRuDirect && (
RU (geoip/geosite)→ direct
)}
Global custom правил{enabled} из {rules.length}
Transparent fallback→ {transparentDefault}
Proxy fallback→ {proxyDefault}
{cacheAvailable && (
Direct bypass cache
{cacheCount} IP
)}
);
}
export function OverviewPage({ state, status, busy, onRestart, onStop, onShowConfig, onNav, onBypassToggle, onFlushDirectCache }) {
return (
{state?.bypassMode && (
⚠ Режим обхода правил активен
— весь трафик идёт напрямую, VPN-правила не применяются.
)}
);
}