Track proxy traffic separately in device inventory
Build and Deploy Gateway / build-and-push (push) Successful in 15s
Build and Deploy Gateway / deploy (push) Successful in 13s

This commit is contained in:
2026-08-07 17:08:24 +03:00
parent 9f31eaf396
commit 53e6cf2146
13 changed files with 714 additions and 146 deletions
+26 -4
View File
@@ -1,6 +1,7 @@
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { api } from '../api.js';
import {
byteString,
formatByteString,
formatLastSeen,
sortDevicesByTraffic,
@@ -210,6 +211,7 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
<h2 id="client-devices-title">Устройства</h2>
<div className="client-instructions-intro">
<p>Устройства, которые Gateway видит в локальной таблице соседей.</p>
<p>Учитывается только трафик, который прошёл через Harbor.</p>
</div>
</header>
@@ -223,6 +225,11 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
Трафик временно не обновляется. Показаны последние сохранённые значения.
</p>
)}
{snapshot?.source?.traffic?.proxy?.error && (
<p className="client-devices-source" role="status">
Прокси-трафик временно не обновляется. Показаны последние сохранённые значения.
</p>
)}
{snapshot?.source?.policy?.error && (
<p className="client-devices-source" role="status">
Маршруты устройств временно не обновляются. Показано последнее подтверждённое состояние.
@@ -248,6 +255,16 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
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';
@@ -314,11 +331,16 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
</div>
)}
<span className="client-device-traffic-slot">
{device.trafficObservedAt && <span
className="client-device-traffic"
aria-label={`Получено ${download}, отдано ${upload}`}
{(gatewayTotal > 0n || proxyTotal > 0n) && <span
className="client-device-traffic client-tooltip-anchor"
tabIndex="0"
aria-label={trafficLabel}
>
<span aria-hidden="true"> {download} · {upload}</span>
<span className="client-device-traffic-sources" aria-hidden="true">
{gatewayTotal > 0n && <span>Gateway {gatewayTraffic}</span>}
{proxyTotal > 0n && <span className="is-proxy">Прокси {proxyTraffic}</span>}
</span>
<Tooltip>{trafficLabel}</Tooltip>
</span>}
</span>
<span className="client-device-pin-wrap client-tooltip-anchor">
+42
View File
@@ -1071,8 +1071,12 @@ p {
}
.client-device-traffic {
display: block;
max-width: min(240px, 42vw);
overflow: hidden;
color: var(--client-text);
font-variant-numeric: tabular-nums;
text-overflow: ellipsis;
white-space: nowrap;
}
@@ -1081,6 +1085,44 @@ p {
font-size: 9px;
}
.client-device-traffic-sources {
display: flex;
gap: 5px;
overflow: hidden;
}
.client-device-traffic-sources span {
overflow: hidden;
text-overflow: ellipsis;
}
.client-device-traffic-sources span + span::before {
margin-right: 5px;
color: var(--client-muted);
content: '·';
}
.client-device-traffic-sources .is-proxy {
color: var(--client-accent);
}
.client-device-traffic:focus-visible {
border-radius: 3px;
outline: 2px solid var(--client-accent);
outline-offset: 3px;
}
.client-device-traffic.client-tooltip-anchor > .client-tooltip {
right: 0;
left: auto;
transform: translate(0, 2px);
}
.client-device-traffic.client-tooltip-anchor:hover > .client-tooltip,
.client-device-traffic.client-tooltip-anchor:focus-visible > .client-tooltip {
transform: translate(0, 0);
}
.client-device-policy-wrap {
width: 72px;
position: relative;
+4 -2
View File
@@ -36,8 +36,10 @@ export function sortDevicesByTraffic(devices, direction = 'desc') {
return (Array.isArray(devices) ? devices : [])
.map((device, index) => ({ device, index }))
.sort((left, right) => {
const leftTotal = byteString(left.device.uploadBytes) + byteString(left.device.downloadBytes);
const rightTotal = byteString(right.device.uploadBytes) + byteString(right.device.downloadBytes);
const leftTotal = byteString(left.device.uploadBytes) + byteString(left.device.downloadBytes)
+ byteString(left.device.proxyUploadBytes) + byteString(left.device.proxyDownloadBytes);
const rightTotal = byteString(right.device.uploadBytes) + byteString(right.device.downloadBytes)
+ byteString(right.device.proxyUploadBytes) + byteString(right.device.proxyDownloadBytes);
if (leftTotal === rightTotal) return left.index - right.index;
return (leftTotal < rightTotal ? -1 : 1) * factor;
})