Update Harbor client implementation
Build and Deploy Gateway / build-and-push (push) Successful in 34s
Build and Deploy Gateway / deploy (push) Successful in 13s

This commit is contained in:
2026-09-10 21:54:18 +03:00
parent 1ae23d848b
commit 74c5b66482
11 changed files with 868 additions and 806 deletions
+79 -21
View File
@@ -2,6 +2,12 @@ import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { TrafficGroupRow } from '../../.test-dist/src/web/features/traffic/TrafficFeature.js';
import { HistoryRows } from '../../.test-dist/src/web/features/traffic/TrafficHistoryPanel.js';
import { emptyTrafficHistory, parseTrafficHistoryQuery } from '../../.test-dist/src/shared/trafficHistory.js';
import { assertLiveTrafficSnapshot } from '../../.test-dist/src/shared/liveTraffic.js';
import {
@@ -23,6 +29,8 @@ const rowModel = source('src/web/features/traffic/trafficRows.ts');
const boundary = source('src/web/features/traffic/index.ts');
const styles = source('src/web/styles/features/traffic.css');
const primitives = source('src/web/styles/primitives.css');
const history = source('src/web/features/traffic/TrafficHistoryPanel.tsx');
const controls = source('src/web/features/traffic/TrafficControls.tsx');
const validSnapshot = {
apiVersion: 1,
@@ -175,7 +183,6 @@ test('traffic drawer exposes the requested truthful states and accessible contro
'Только трафик через Harbor Connect. Приложения macOS недоступны внутри Docker.',
'Только соединения, прошедшие через sing-box Gateway. Трафик, обходящий sing-box напрямую, здесь не виден.',
]) assert.match(feature, new RegExp(copy.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
assert.match(feature, /feature\.view === 'live' \? `\$\{snapshot\?\.summary\.active \|\| 0\} АКТИВНЫХ` : 'ИСТОРИЯ'/);
assert.match(feature, /feature\.isGateway[\s\S]*Инспектор трафика выключен в настройках Harbor Gateway\.[\s\S]*Инспектор трафика выключен в настройках Harbor Connect\./);
assert.match(feature, /feature\.isGateway[\s\S]*Только соединения, прошедшие через sing-box Gateway\. Трафик, обходящий sing-box напрямую, здесь не виден\.[\s\S]*Только трафик через Harbor Connect\. Приложения macOS недоступны внутри Docker\./);
assert.match(feature, /aria-label="Найти устройство"/);
@@ -205,7 +212,6 @@ test('traffic retention and grouping use canonical server settings and the froze
assert.match(feature, /groupTrafficConnections\(selectedConnections, grouping\)/);
assert.match(feature, /trafficGroupMatches\(group, query, 'all', 'all'\)/);
assert.match(feature, /group\.connections\.length === 1[\s\S]*`Завершено · \$\{group\.protocol\}`/);
assert.match(feature, /Соединения сгруппированы по назначению, протоколу и маршруту\./);
});
test('traffic groups combine compatible UUIDs with exact byte sums and whole-group search', () => {
@@ -384,7 +390,7 @@ test('traffic groups stay mounted and inert through exit while the same group ca
assert.match(rowModel, /const desiredIds = new Set\(desired\.map\(\(group\) => group\.id\)\)/);
assert.match(feature, /groupTrafficConnections\(selectedConnections, grouping\)/);
assert.match(feature, /trafficGroupMatches\(group, query, 'all', 'all'\)/);
assert.match(feature, /reconcileTrafficGroups\(current, groups, immediate\)/);
assert.match(feature, /reconcileTrafficGroups\(current, groups, immediate, !reorder\)/);
assert.match(feature, /setExpandedId\(\(current\) => desiredIds\.has\(current\) \? current : ''\)/);
assert.match(feature, /inert=\{exiting \|\| undefined\}/);
assert.match(feature, /event\.target === event\.currentTarget[\s\S]*event\.animationName === 'client-traffic-connection-out'/);
@@ -392,31 +398,83 @@ test('traffic groups stay mounted and inert through exit while the same group ca
assert.match(feature, /matchMedia\('\(prefers-reduced-motion: reduce\)'\)[\s\S]*media\.addEventListener\('change', update\)[\s\S]*media\.removeEventListener\('change', update\)/);
assert.match(feature, /const immediate = reducedMotion[\s\S]*\['disabled', 'incompatible', 'stopped'\]\.includes/);
assert.match(feature, /aria-label="Группы активных и недавно завершённых соединений"/);
assert.match(feature, /group\.connections\.length > 1[\s\S]*×\$\{group\.connections\.length\}/);
assert.match(feature, /group\.activeCount > 0 && <strong>/);
assert.match(feature, /displayedGroups\.map\(\(row\) => <TrafficGroupRow[\s\S]*key=\{row\.group\.id\}/);
assert.match(feature, /<b>Активных распознано<\/b>/);
assert.match(feature, /<b>Активных требует внимания<\/b>/);
});
test('traffic styling preserves the shared drawer geometry and minimal motion', () => {
test('polling updates values without reshuffling readable rows, while explicit sorting can reorder', () => {
const groups = groupTrafficConnections([
trafficConnection('a', { destination: { domain: 'a.test' } }),
trafficConnection('b', { destination: { domain: 'b.test' } }),
trafficConnection('c', { destination: { domain: 'c.test' } }),
]);
const [a, b, c] = groups;
const updated = { ...b, traffic: { ...b.traffic, downloadBytes: '9007199254740993' } };
for (const immediate of [false, true]) {
let rows = reconcileTrafficGroups([], [a, b], immediate, true);
rows = reconcileTrafficGroups(rows, [updated, c, a], immediate, true);
assert.deepEqual(rows.map(({ group }) => group.label), ['a.test', 'b.test', 'c.test']);
assert.equal(rows[1].group.traffic.downloadBytes, '9007199254740993');
const sorted = reconcileTrafficGroups(rows, [c, updated, a], immediate, false);
assert.deepEqual(sorted.map(({ group }) => group.label), ['c.test', 'b.test', 'a.test']);
rows = reconcileTrafficGroups(rows, [c, a], immediate, true);
assert.equal(rows.some((row) => row.group.id === b.id), !immediate);
rows = reconcileTrafficGroups(rows, [a, updated, c], immediate, true);
assert.equal(rows.find((row) => row.group.id === b.id).exiting, false);
}
});
test('minimal traffic rows distinguish current rates from historical totals and completed connections', () => {
const [group] = groupTrafficConnections([trafficConnection('current', { traffic: {
downloadBytes: '9007199254740993', uploadBytes: '1048576',
downloadBytesPerSecond: '2097152', uploadBytesPerSecond: '1024',
} })]);
const render = (group, expanded = false) => renderToStaticMarkup(createElement(TrafficGroupRow, {
group, expanded, exiting: false, onExited() {}, onToggle() {},
}));
const current = render(group);
assert.match(current, /aria-label="Скачивание: 2,0 МБ\/с"/);
assert.match(current, /aria-label="Отправка: 1,0 КБ\/с"/);
assert.doesNotMatch(current, /<svg|<img|<dl|8,0 ПБ|tls/);
assert.match(render(group, true), /<dt>Скачано<\/dt><dd>8,0 ПБ<\/dd>/);
const completed = render({ ...group, activeCount: 0, recentCount: 1 });
assert.match(completed, /aria-label="Скачивание: соединение завершено"/);
assert.doesNotMatch(completed, /МБ\/с/);
const snapshot = emptyTrafficHistory(parseTrafficHistoryQuery(new URLSearchParams()));
snapshot.rows = [{ key: 'example.com', label: 'example.com', downloadBytes: '9007199254740993', uploadBytes: '1048576', route: 'vpn' }];
const historical = renderToStaticMarkup(createElement(HistoryRows, {
snapshot, active: true, load() { throw new Error('Collapsed history must not load details'); },
}));
assert.match(historical, /aria-label="Скачивание: 8,0 ПБ"/);
assert.match(historical, /aria-label="Отправка: 1,0 МБ"/);
assert.doesNotMatch(historical, /<svg|<img|\/с|VPN|Загружаем/);
});
test('traffic modes retain mounted state and stop hidden polling; menu Escape stays inside the drawer', () => {
assert.doesNotMatch(feature, /view === 'history' && <TrafficHistoryPanel/);
assert.match(feature, /<TrafficHistoryPanel active=\{feature.isOpen && !feature.paused && feature.view === 'history'\}/);
assert.match(feature, /aria-hidden=\{feature.view !== 'history'\} inert=\{feature.view !== 'history' \|\| undefined\}/);
assert.match(history, /if \(!active\) return/);
assert.match(history, /controller.abort\(\); clearTimeout\(timer\)/);
assert.match(history, /until: snapshot.query.until/);
assert.match(controls, /event.stopPropagation\(\);[\s\S]*trigger.current\?\.focus\(\)/);
assert.match(controls, /aria-hidden=\{!open\} inert=\{!open \|\| undefined\}/);
assert.match(controls, /getComputedStyle\(node\).opacity;[\s\S]*animation.cancel\(\)/);
assert.match(controls, /media.addEventListener\('change', reduce\)/);
assert.match(controls, /media.removeEventListener\('change', reduce\)/);
});
test('traffic styling reserves readable columns and one list scroll owner with calm motion', () => {
assert.match(styles, /\.client-traffic-toggle svg \{[\s\S]*width: 24px;[\s\S]*height: 24px/);
assert.match(primitives, /\.client-drawer \{[\s\S]*width: min\(580px, 100vw\)/);
assert.match(styles, /@media \(max-width: 768px\) \{[\s\S]*\.client-traffic \{[\s\S]*width: 100vw/);
assert.doesNotMatch(styles, /overflow-y:\s*(?:auto|scroll)/);
assert.match(styles, /\.client-traffic-meta button \{[\s\S]*width: 96px/);
assert.match(styles, /\.client-traffic-details \{[\s\S]*animation: client-traffic-details-in 180ms/);
assert.match(styles, /\.client-traffic-connection \{[\s\S]*animation: client-traffic-connection-in 420ms/);
assert.match(styles, /\.client-traffic-connection\.is-exiting \{[\s\S]*animation: client-traffic-connection-out 240ms/);
const detailMotion = /@keyframes client-traffic-details-in \{([\s\S]*?)\n\}/.exec(styles)?.[1] || '';
assert.match(detailMotion, /opacity:/);
assert.match(detailMotion, /translateY/);
assert.doesNotMatch(detailMotion, /height|width|margin|padding|scale|filter/);
assert.match(styles, /width: min\(800px, 100vw\)/);
assert.match(styles, /@media \(max-width: 480px\)/);
assert.match(styles, /\.client-traffic-scroll \{[\s\S]*min-height: 0;[\s\S]*overflow-y: auto/);
assert.match(styles, /\.client-traffic-pause \{[\s\S]*width: 112px/);
assert.match(styles, /\.client-traffic-columns,[\s\S]*grid-template-columns: minmax\(0, 1fr\) var\(--traffic-value-width\) var\(--traffic-value-width\)/);
for (const name of ['client-traffic-connection-in', 'client-traffic-connection-out']) {
const rowMotion = new RegExp(`@keyframes ${name} \\{([\\s\\S]*?)\\n\\}`).exec(styles)?.[1] || '';
assert.match(rowMotion, /opacity:/);
assert.match(rowMotion, /translateY/);
assert.doesNotMatch(rowMotion, /height|width|margin|padding|scale|filter/);
assert.doesNotMatch(rowMotion, /transform|height|width|margin|padding|scale|filter/);
}
assert.match(styles, /@media \(prefers-reduced-motion: reduce\) \{[\s\S]*\.client-traffic-connection,[\s\S]*\.client-traffic-details,[\s\S]*\.client-traffic-device-list \{[\s\S]*animation: none/);
assert.match(styles, /@media \(prefers-reduced-motion: reduce\) \{[\s\S]*animation: none;[\s\S]*transition: none/);
});
+21 -19
View File
@@ -40,26 +40,26 @@ const expectedImports = [
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
const acceptedLedger = {
counts: {
cascadeEdges: 1190,
customProperties: 115,
declarations: 4872,
cascadeEdges: 1210,
customProperties: 128,
declarations: 4842,
important: 0,
keyframes: 55,
media: 23,
rules: 1332,
variableReferences: 1274,
keyframes: 54,
media: 24,
rules: 1321,
variableReferences: 1244,
},
hashes: {
cascadeEdges: '07be381535d1cd8f78990a5b2eae27903471ec2c6cbe1589e36e8f1797f1e610',
customProperties: '81d1e70737ae5a8e4b20d4c1be1b24685975a404863444da4a8821be1f0d5097',
declarations: 'bd186c9317ee7ff540b4eeddfc33d2c4ab64a1b2a9362f95480d180d4f644605',
cascadeEdges: '224ac4ef12ba44e54b737ce00812315090f9f401cc8836e3eba483284b71da25',
customProperties: '35cecb86835ecd0f51e703424bb4440ddf291138d97eeb070b880706a8efc6a1',
declarations: 'c1b1061f8cf328f5f266da23f8f72ed9878f9f3db057fa586ee7d69781b62db1',
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
duplicateSelectors: '8982145dba05b33bf1c93b2d54cfbe76305b276ff3c657aa020144016ada5848',
keyframes: 'b9b0bf3fad92e69b93ec0c24f01da15e78bbe89a095597d64ce4f7ef5e272fdd',
ruleDeclarationSequences: 'b066b7e20fd1195c67c23c38cbaf7b58edcd6356e62bd173371234206c74817e',
selectors: '2e38ff14b0a7d581b090b520c92b6bff60c84082db4dc20a49c454ef468fbeb5',
variableReferences: '5fd2c93d2467be16976c102b595a5fb15c5692e2d98645022d7a9acf2f42d829',
witnesses: 'e9ae2a8416aa44a75a01452bc17884dcb133cccb939880cdecc761ea1043721c',
keyframes: '4178d589addd5be04ff4d89a5fd93527427795f62d583527e063e0a541f70bd0',
ruleDeclarationSequences: '7659444a0a4871d15b7702f886c3730f02ea8fdfa54dc5659465ecc4cd1743a6',
selectors: 'e15851f9c14afb747e8c7d5969586d1c2193305dbd9ddc809020eae87d2514af',
variableReferences: '3f5340864200e324948e4387a4bb534e82732ed91bc4e3958081890cf7bb854e',
witnesses: '27f96ddab7b128a8924c7a733244523fe007a34ba91d192f63ba19a50c38d4a5',
},
};
@@ -212,7 +212,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, 1486);
assert.equal(witnesses.length, 1463);
assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0);
const ledger = createStyleLedger(readStyleSource(root), { witnesses });
assert.deepEqual(ledger.counts, acceptedLedger.counts);
@@ -328,6 +328,8 @@ test('selector proof uses the observed level-four grammar and exact specificity'
['.client-instructions-toggle:not(.client-devices-toggle):not(.client-diagnostics-toggle)', [{ a: 0, b: 3, c: 0 }]],
[".client-power[aria-checked='true']::before", [{ a: 0, b: 2, c: 1 }]],
['.client-failover-number-setting input::-webkit-inner-spin-button', [{ a: 0, b: 1, c: 2 }]],
['.client-traffic-search input::-webkit-search-cancel-button', [{ a: 0, b: 1, c: 2 }]],
['.client-traffic button:enabled:hover', [{ a: 0, b: 3, c: 1 }]],
['.client-instruction-block:nth-child(n)', [{ a: 0, b: 2, c: 0 }]],
['.client-confirmation-actions button:hover:not(:disabled), #root', [
{ a: 0, b: 3, c: 1 },
@@ -411,8 +413,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-D6ACNk74.css']);
assert.deepEqual(assets, ['index-DVxr9dv9.css']);
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
assert.equal(built.byteLength, 185399);
assert.equal(sha256(built), '3327b4873e7dba63fd44c21d34c4fe19f78167ef6c5badcfd8dfe08eb2705c5d');
assert.equal(built.byteLength, 183862);
assert.equal(sha256(built), 'b717ce3c169f63cd28d08fd708758e2d4fe5e85587a649dae15d1cf30ce778a0');
});
+4 -4
View File
@@ -479,7 +479,7 @@ export function readStyleWitnesses(root) {
source: fs.readFileSync(file, 'utf8'),
}));
// History has exactly service/domain/hostname/IP levels, not arbitrary JSX recursion.
return createStyleWitnesses(files, { recursionLimits: { HistoryRows: 4, HistoryBranch: 3 } });
return createStyleWitnesses(files, { recursionLimits: { HistoryRows: 4, HistoryBranch: 3, TrafficReveal: 3 } });
}
const OBSERVED_PROPERTIES = new Set(`
@@ -499,7 +499,7 @@ margin margin-bottom margin-inline margin-left margin-right margin-top max-heigh
min-height min-width mix-blend-mode opacity order outline outline-offset overflow
overflow-wrap overflow-x overflow-y overscroll-behavior padding padding-block
padding-bottom padding-inline padding-left padding-right padding-top place-content
place-items pointer-events position right row-gap scrollbar-width stroke stroke-dasharray
place-items pointer-events position right row-gap scrollbar-width scrollbar-gutter stroke stroke-dasharray
stroke-dashoffset stroke-linecap stroke-linejoin stroke-width table-layout text-align
text-decoration text-overflow text-shadow text-transform text-underline-offset top
touch-action transform transform-box transform-origin transition transition-delay user-select
@@ -525,9 +525,9 @@ const PROPERTY_FAMILIES = new Map([
const SUPPORTED_SELECTOR_NODES = new Set(['attribute', 'class', 'combinator', 'id', 'pseudo', 'selector', 'tag', 'universal']);
const SUPPORTED_COMBINATORS = new Set([' ', '+', '>']);
const SUPPORTED_PSEUDOS = new Set([
':-webkit-autofill', '::-webkit-inner-spin-button', '::-webkit-outer-spin-button', '::-webkit-scrollbar',
':-webkit-autofill', '::-webkit-inner-spin-button', '::-webkit-outer-spin-button', '::-webkit-scrollbar', '::-webkit-search-cancel-button',
'::after', '::before', '::marker', '::placeholder',
'::view-transition-group', '::view-transition-new', '::view-transition-old', ':active', ':disabled',
'::view-transition-group', '::view-transition-new', '::view-transition-old', ':active', ':disabled', ':enabled',
':first-child', ':focus', ':focus-visible', ':focus-within', ':has', ':hover', ':last-child', ':not',
':nth-child', ':root',
]);