{snapshot?.source?.error && (
Список временно не обновляется. Показаны последние сохранённые данные.
)}
{snapshot?.source?.traffic?.error && (
Трафик временно не обновляется. Показаны последние сохранённые значения.
)}
{snapshot?.source?.traffic?.proxy?.error && (
Прокси-трафик временно не обновляется. Показаны последние сохранённые значения.
)}
{snapshot?.source?.policy?.error && (
Маршруты устройств временно не обновляются. Показано последнее подтверждённое состояние.
)}
{error && (
{error.message}
)}
{status === 'loading' &&
Ищем устройства…
}
{status !== 'loading' && !devices.length && !error && (
Gateway пока не видит устройств.
)}
{copyFeedback ? copyFeedback.failed ? 'Не удалось скопировать IP' : 'IP скопирован' : ''}
{devices.map((device) => {
const hasName = Boolean(device.alias || device.hostname);
const title = device.alias || device.hostname || device.ip || 'Неизвестное устройство';
const editing = editingId === device.id;
const saving = savingId === device.id;
const seen = formatLastSeen(device.lastSeenAt);
const online = device.status === 'online';
const gatewayTotal = byteString(device.downloadBytes) + byteString(device.uploadBytes);
const proxyTotal = byteString(device.proxyDownloadBytes) + byteString(device.proxyUploadBytes);
const gatewayTraffic = formatByteString(gatewayTotal.toString());
const proxyTraffic = formatByteString(proxyTotal.toString());
const totalTraffic = formatByteString((gatewayTotal + proxyTotal).toString());
const hasProxyTraffic = proxyTotal > 0n;
const trafficDelta = trafficDeltas[device.id] || {};
const copied = copyFeedback?.id === device.id;
const policyBusy = device.policyStatus === 'applying';
const policyFailed = device.policyStatus === 'failed';
const policyPending = device.policyStatus === 'pending';
const displayPolicy = device.appliedPolicy;
const policyTarget = device.policyStatus === 'applied'
? device.appliedPolicy === 'direct' ? 'vpn' : 'direct'
: device.appliedPolicy;
const cannotEnableDirect = device.policyStatus === 'applied'
&& device.appliedPolicy !== 'direct'
&& device.confidence === 'ambiguous';
const policyTooltip = policyBusy
? `Применяем: ${device.desiredPolicy === 'direct' ? 'полностью напрямую' : 'через правила Gateway'}`
: policyFailed
? `${device.policyError || 'Маршрут не применён'}. Сейчас: ${device.appliedPolicy === 'direct' ? 'напрямую' : 'через Gateway'}. Нажмите, чтобы оставить текущий маршрут`
: policyPending
? 'Gateway должен однозначно распознать устройство. Нажмите, чтобы отменить ожидание'
: device.confidence === 'ambiguous' && device.desiredPolicy !== 'direct'
? 'Маршрут недоступен, пока Gateway видит несколько сетевых адресов одного устройства'
: displayPolicy === 'direct'
? 'Полностью обходит sing-box. Нажмите, чтобы вернуть обработку Gateway'
: 'Проходит через sing-box и правила Gateway. Нажмите, чтобы пустить полностью напрямую';
return
{
if (node) deviceNodes.current.set(device.id, node);
else deviceNodes.current.delete(device.id);
}}
className={`client-device is-${device.status}${device.pinned ? ' is-pinned' : ''}`}
key={device.id}
>
{device.pinned ? 'Открепить' : 'Закрепить'}
Всего
Gateway
{hasProxyTraffic && Прокси}
{policyTooltip}
;
})}