{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 пока не видит устройств.
)}
{devices.map((device) => {
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 download = formatByteString(device.downloadBytes);
const upload = formatByteString(device.uploadBytes);
const proxyDownload = formatByteString(device.proxyDownloadBytes);
const proxyUpload = formatByteString(device.proxyUploadBytes);
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 trafficLabel = [
gatewayTotal > 0n ? `Gateway: получено ${download}, отдано ${upload}` : '',
proxyTotal > 0n ? `Прокси: получено ${proxyDownload}, отдано ${proxyUpload}` : '',
].filter(Boolean).join('. ');
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.pinned || device.confidence === 'ambiguous');
const policyTooltip = policyBusy
? `Применяем: ${device.desiredPolicy === 'direct' ? 'полностью напрямую' : 'через правила Gateway'}`
: policyFailed
? `${device.policyError || 'Маршрут не применён'}. Сейчас: ${device.appliedPolicy === 'direct' ? 'напрямую' : 'через Gateway'}. Нажмите, чтобы оставить текущий маршрут`
: policyPending
? 'Gateway должен однозначно распознать устройство. Нажмите, чтобы отменить ожидание'
: !device.pinned
? 'Закрепите устройство, чтобы изменить маршрут'
: 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}`}
key={device.id}
>
{editing ? (
) : (
{title}
Изменить название
)}
{(gatewayTotal > 0n || proxyTotal > 0n) &&
{gatewayTotal > 0n && Gateway {gatewayTraffic}}
{proxyTotal > 0n && Прокси {proxyTraffic}}
{trafficLabel}
}
{device.desiredPolicy === 'direct' || device.appliedPolicy === 'direct'
? 'Сначала верните маршрут через Gateway'
: device.pinned ? 'Открепить' : 'Закрепить'}
{title !== device.ip && device.ip && {device.ip}}
{device.manufacturer && {device.manufacturer}}
{policyTooltip}
;
})}