Expose outbound traffic totals and simplify Direct chart breakdown
Build and Deploy Gateway / build-and-push (push) Successful in 20s
Build and Deploy Gateway / deploy (push) Successful in 13s

This commit is contained in:
2026-08-12 22:43:40 +03:00
parent 9d43e74d97
commit 72c085a5b8
10 changed files with 173 additions and 69 deletions
+31 -2
View File
@@ -548,13 +548,32 @@ test('device outbound history keeps sing-box routes and kernel Direct on separat
}),
});
assert.deepEqual((await service.refresh()).devices[0].outboundTrafficHistory, []);
let snapshot = await service.refresh();
assert.deepEqual(snapshot.devices[0].outboundTraffic, {
observedAt,
singboxObservedAt: observedAt,
directIpv4ObservedAt: observedAt,
vpnBytes: '300',
directTrackedBytes: '30',
directIpv4Bytes: '100',
unknownBytes: '3',
});
assert.deepEqual(snapshot.devices[0].outboundTrafficHistory, []);
observedAt = '2026-08-12T12:00:15.000Z';
vpn = ['150', '250'];
trackedDirect = ['30', '50'];
unknown = ['4', '8'];
directIpv4 = ['70', '90'];
let snapshot = await service.refresh();
snapshot = await service.refresh();
assert.deepEqual(snapshot.devices[0].outboundTraffic, {
observedAt,
singboxObservedAt: observedAt,
directIpv4ObservedAt: observedAt,
vpnBytes: '400',
directTrackedBytes: '80',
directIpv4Bytes: '160',
unknownBytes: '12',
});
assert.deepEqual(snapshot.devices[0].outboundTrafficHistory, [{
observedAt,
vpnBytes: '100',
@@ -570,6 +589,15 @@ test('device outbound history keeps sing-box routes and kernel Direct on separat
unknown = ['0', '1'];
directIpv4 = ['5', '6'];
snapshot = await service.refresh();
assert.deepEqual(snapshot.devices[0].outboundTraffic, {
observedAt,
singboxObservedAt: observedAt,
directIpv4ObservedAt: observedAt,
vpnBytes: '7',
directTrackedBytes: '3',
directIpv4Bytes: '11',
unknownBytes: '1',
});
assert.deepEqual(snapshot.devices[0].outboundTrafficHistory.at(-1), {
observedAt,
vpnBytes: '0',
@@ -578,6 +606,7 @@ test('device outbound history keeps sing-box routes and kernel Direct on separat
unknownBytes: '0',
});
assert.doesNotMatch(fs.readFileSync(filePath, 'utf8'), /outboundTrafficHistory/);
assert.doesNotMatch(fs.readFileSync(filePath, 'utf8'), /outboundTraffic/);
});
test('global traffic stays monotonic when a device expires and returns in the same epoch', async (t) => {
+12 -8
View File
@@ -82,9 +82,11 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
assert.match(panel, /device\.confidence === 'ambiguous'/);
assert.match(panel, /stabilizeDevicesByTraffic\(snapshot\?\.devices, sortDirection, previousIds\)/);
assert.match(panel, /Трафик временно не обновляется/);
assert.match(panel, /Учитывается только трафик, который прошёл через Harbor/);
assert.match(panel, /client-device-traffic-total[\s\S]*<b>Всего<\/b><TrafficValue value=\{totalTraffic\} delta=\{trafficDelta\.total\}/);
assert.match(panel, /client-device-traffic-breakdown[\s\S]*<b>Gateway<\/b><TrafficValue value=\{gatewayTraffic\} delta=\{trafficDelta\.gateway\}/);
assert.match(panel, /Вход — накопленные Gateway\/Proxy\. Выход — оценка VPN\/Direct с запуска текущего учёта\./);
assert.match(panel, /const displayedTotal = trafficView === 'outbound'[\s\S]*`≈ \$\{formatByteString\(outboundTotal\.toString\(\)\)\}`[\s\S]*const trafficLabel = trafficView === 'outbound' \? 'Выход' : 'Вход'/);
assert.match(panel, /client-device-traffic-total[\s\S]*<b>\{trafficLabel\}<\/b><TrafficValue value=\{displayedTotal\}/);
assert.match(panel, /client-device-traffic-breakdown[\s\S]*<b>VPN<\/b>[\s\S]*<b>Direct ≈<\/b>[\s\S]*<b>Неизв\.<\/b>/);
assert.match(panel, /<b>Gateway<\/b><TrafficValue value=\{gatewayTraffic\} delta=\{trafficDelta\.gateway\}/);
assert.match(panel, /const hasProxyTraffic = proxyTotal > 0n/);
assert.match(panel, /client-device-traffic-breakdown[\s\S]*\{hasProxyTraffic && <span className="is-proxy"><b>Прокси<\/b><TrafficValue value=\{proxyTraffic\} delta=\{trafficDelta\.proxy\}/);
assert.match(panel, /TrafficChart[\s\S]*samples=\{trafficView === 'outbound' \? device\.outboundTrafficHistory \|\| \[\] : device\.trafficHistory \|\| \[\]\}[\s\S]*scale=\{trafficScale\}[\s\S]*capacity=\{snapshot\?\.trafficHistoryCapacity[\s\S]*series=\{trafficView\}/);
@@ -94,19 +96,20 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
assert.match(chart, /function trafficPathAnimationSource[\s\S]*previousByTime[\s\S]*gatewayY: 100[\s\S]*attributeName="d"[\s\S]*dur="520ms"/);
assert.match(chart, /function smoothTrafficPath[\s\S]*const midX = \(previous\.x \+ point\.x\) \/ 2[\s\S]* C /);
assert.match(chart, /TRAFFIC_CHART_HEADROOM = 10[\s\S]*trafficChartY = \(ratio: number\) => 100 - ratio \* \(100 - TRAFFIC_CHART_HEADROOM\)/);
assert.match(chart, /gatewayY: trafficChartY\(trafficScaleRatio\(gateway, max, scale\)\)[\s\S]*proxyY: trafficChartY\(trafficScaleRatio\(proxy, max, scale\)\)[\s\S]*directIpv4Y: trafficChartY\(trafficScaleRatio\(directIpv4, max, scale\)\)[\s\S]*unknownY: trafficChartY\(trafficScaleRatio\(unknown, max, scale\)\)/);
assert.match(chart, /proxy: byteString\(outbound\.directTrackedBytes\) \+ byteString\(outbound\.directIpv4Bytes\)/);
assert.match(chart, /gatewayY: trafficChartY\(trafficScaleRatio\(gateway, max, scale\)\)[\s\S]*proxyY: trafficChartY\(trafficScaleRatio\(proxy, max, scale\)\)[\s\S]*unknownY: trafficChartY\(trafficScaleRatio\(unknown, max, scale\)\)/);
assert.match(chart, /client-device-traffic-grid[\s\S]*y1=\{TRAFFIC_CHART_HEADROOM\}[\s\S]*y1=\{\(100 \+ TRAFFIC_CHART_HEADROOM\) \/ 2\}/);
assert.match(chart, /client-device-traffic-cursor[\s\S]*y1=\{TRAFFIC_CHART_HEADROOM\} y2="100"/);
assert.doesNotMatch(panel, /key=\{latest\}/);
assert.match(chart, /createPortal\([\s\S]*client-device-traffic-point-tooltip[\s\S]*document\.body/);
assert.match(chart, /onPointerMove=\{trackPointer\}/);
assert.match(chart, /pinned && max > 0n && <span className="client-device-traffic-axis"[\s\S]*is-max[\s\S]*is-mid[\s\S]*is-zero/);
assert.match(chart, /series === 'outbound'[\s\S]*'is-vpn'[\s\S]*'is-direct-tracked'[\s\S]*'is-direct-ipv4'[\s\S]*'is-unknown'/);
assert.match(chart, /series === 'outbound'[\s\S]*'is-vpn'[\s\S]*'is-direct-total'[\s\S]*'is-unknown'/);
assert.match(chart, /client-device-traffic-lines[\s\S]*visibleLines\.map[\s\S]*smoothTrafficPath\(previous, line\.yKey\)[\s\S]*client-device-traffic-cursor[\s\S]*visibleLines\.map/);
assert.match(panel, /routeLabel=\{device\.appliedPolicy === 'direct' \? 'Напрямую' : 'Gateway'\}/);
assert.match(chart, /\{hovered\.proxy > 0n && <span className="is-proxy">Proxy \{formatByteString\(hovered\.proxy\)\}<\/span>\}/);
assert.match(chart, /VPN · sing-box[\s\S]*Direct · sing-box[\s\S]*Direct · IPv4[\s\S]*Неизвестно · sing-box[\s\S]*Разные уровни учёта не складываются/);
assert.match(chart, /<time dateTime=\{visibleSamples\[0\]\.observedAt\}>[\s\S]*client-device-traffic-legend[\s\S]*VPN[\s\S]*Direct · sing-box[\s\S]*Direct · IPv4[\s\S]*series === 'speed' \? '↓ \/ ↑' : 'Вход'/);
assert.match(chart, /Учтено ≈[\s\S]*VPN[\s\S]*Direct ≈[\s\S]*через sing-box[\s\S]*мимо sing-box · IPv4[\s\S]*Неизвестно · sing-box[\s\S]*Оценка за 15-секундный интервал/);
assert.match(chart, /<time dateTime=\{visibleSamples\[0\]\.observedAt\}>[\s\S]*client-device-traffic-legend[\s\S]*VPN[\s\S]*Direct [\s\S]*series === 'speed' \? '↓ \/ ↑' : 'Вход'/);
assert.match(panel, /positiveByteDelta\(previous\.gateway, gateway\)[\s\S]*positiveByteDelta\(previous\.proxy, proxy\)/);
assert.match(panel, /setTimeout\(\(\) => setTrafficDeltas\(\{\}\), TRAFFIC_DELTA_MS\)/);
assert.doesNotMatch(panel, /client-device-traffic client-tooltip-anchor|<Tooltip>\{trafficLabel\}<\/Tooltip>/);
@@ -157,7 +160,8 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
assert.match(styles, /\.client-device-traffic-point-tooltip \{[\s\S]*position: fixed;[\s\S]*z-index: 1002/);
assert.doesNotMatch(styles, /client-device-traffic-shift|client-device-traffic-line-draw/);
assert.match(styles, /\.client-device-traffic-lines \.is-upload \{[\s\S]*stroke-dasharray: 5 4/);
assert.match(styles, /\.client-device-traffic-lines \.is-vpn[\s\S]*\.is-direct-tracked[\s\S]*\.is-direct-ipv4[\s\S]*\.is-unknown/);
assert.match(styles, /\.client-device-traffic-lines \.is-vpn[\s\S]*\.is-direct-total[\s\S]*\.is-unknown/);
assert.match(styles, /\.client-device-traffic-breakdown \.is-vpn[\s\S]*\.is-direct-total[\s\S]*\.is-unknown/);
assert.match(styles, /@keyframes client-device-traffic-plot-expand[\s\S]*scaleY\(0\.3448275862\)[\s\S]*scaleY\(1\)/);
assert.match(styles, /@keyframes client-device-traffic-plot-collapse[\s\S]*scaleY\(2\.9\)[\s\S]*scaleY\(1\)/);
assert.match(styles, /@keyframes client-device-traffic-detail-in[\s\S]*opacity: 0[\s\S]*opacity: 1/);
+11
View File
@@ -62,6 +62,15 @@ test('all unknown inventory results pass one identity-preserving runtime parser'
appliedPolicy: 'vpn',
confidence: 'high',
trafficHistory: [{ observedAt, gatewayBytes: '42', proxyBytes: '0' }],
outboundTraffic: {
observedAt,
singboxObservedAt: observedAt,
directIpv4ObservedAt: observedAt,
vpnBytes: '30',
directTrackedBytes: '8',
directIpv4Bytes: '4',
unknownBytes: '0',
},
outboundTrafficHistory: [{
observedAt,
vpnBytes: '30',
@@ -116,6 +125,8 @@ test('all unknown inventory results pass one identity-preserving runtime parser'
{ ...valid, devices: [{ ...valid.devices[0], confidence: 'unknown' }] },
{ ...valid, devices: [{ ...valid.devices[0], lastSeenAt: 'not-a-date' }] },
{ ...valid, devices: [{ ...valid.devices[0], trafficHistory: [{ observedAt, gatewayBytes: '1' }] }] },
{ ...valid, devices: [{ ...valid.devices[0], outboundTraffic: { observedAt, vpnBytes: '1' } }] },
{ ...valid, devices: [{ ...valid.devices[0], outboundTraffic: { ...valid.devices[0].outboundTraffic, directIpv4ObservedAt: 'not-a-date' } }] },
{ ...valid, devices: [{ ...valid.devices[0], outboundTrafficHistory: [{ observedAt, vpnBytes: '1' }] }] },
{ ...valid, traffic: { ...valid.traffic, totalBytes: -4 } },
{ ...valid, traffic: { ...valid.traffic, observedAt: 'not-a-date' } },
+13 -13
View File
@@ -37,26 +37,26 @@ const expectedImports = [
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
const acceptedLedger = {
counts: {
cascadeEdges: 829,
cascadeEdges: 813,
customProperties: 103,
declarations: 3268,
declarations: 3270,
important: 0,
keyframes: 56,
media: 13,
rules: 939,
rules: 941,
variableReferences: 812,
},
hashes: {
cascadeEdges: '31e13ff6817e157b5ab4bb6a7caeb9f869da114e7ddb72a44d6972f3594f027a',
cascadeEdges: '38c9f42cdc5839efbfc06fd12e21d943742fd71a7170b239059c9041e675697d',
customProperties: 'ef70fb1d9b61b177f1939f811babe1116bee631de26f5eac0aa0d0009ca5a1fb',
declarations: '9eee80b8494eab2d9015b797ad0ea7487bcc5b01c84ec90f00884ddf39396cd2',
declarations: '8ed706131420b5e15efcaaffa64282d4d44e6bcf8473938c16ec55f375013661',
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
duplicateSelectors: '257eff4727dab9ce25f4e1f9320e89bf2270eb29feae921b96601f103ad7f036',
keyframes: 'd4378751f36eccc87923c12540315462055032a5d34978a0f45ecada0a5cc1ce',
ruleDeclarationSequences: '7fcacaa4386895d6eeb43678d151ec882959bbbd6815843a626f7cea7886a5f6',
selectors: 'edf95ecfdb232b500674687f577a03884c399ec33a590ac73fc5145ec280ca38',
variableReferences: '2035aa280c6096e2581bb9e06ef687fd80e4ce4ee5d8844ed16c96b3fa735ff9',
witnesses: 'cf5d27b5fac96afae3c8f5c68f5787815679ef00c825d06d76ea3b27e4d6ead0',
ruleDeclarationSequences: 'f054d14a153e93f07aebc28ada717e004ac8618b9f766d4701cc748a271fad9a',
selectors: '4e8f1d8af4830eb6ca40a5b94d3b1b234817f22eb7073c643b4773ee36f8249c',
variableReferences: 'd3a740a583156df0b42ce0f52687617a5b5507b394251070c63d355560921f03',
witnesses: '834f4fcda87e58c8cd49011d6d8f3e88393c09a88b1d255f3e8d43ccb00ea3e6',
},
};
@@ -209,7 +209,7 @@ test('client typography uses the shared semantic scale outside the token owner',
test('accepted stylesheet has pinned declaration, selector, keyframe, variable, and cascade ledgers', () => {
const witnesses = readStyleWitnesses(root);
assert.equal(witnesses.length, 770);
assert.equal(witnesses.length, 787);
assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0);
const ledger = createStyleLedger(readStyleSource(root), { witnesses });
assert.deepEqual(ledger.counts, acceptedLedger.counts);
@@ -405,8 +405,8 @@ test('main owns one public stylesheet and the regrouped production CSS is determ
assert.equal((main.match(/import ['"][^'"]+\.css['"]/g) || []).length, 1);
const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css'));
assert.deepEqual(assets, ['index-DxMz3Vnp.css']);
assert.deepEqual(assets, ['index-BT8lRf1r.css']);
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
assert.equal(built.byteLength, 126501);
assert.equal(sha256(built), '99f66c86e8a2bb8d0351b46cad8d16393ae92c1246b9027312e7b79756355a7d');
assert.equal(built.byteLength, 126715);
assert.equal(sha256(built), '0d1d0c03b2d89265a373ebd8e80aa5f5b2a37bb1dd4d5cb350802dcb7ddf673a');
});