- Gateway · Direct ↔ VPN
+ {isGateway ? 'Gateway' : 'Connect'} · Direct ↔ VPN
Маршруты
diff --git a/src/web/components/DevicesPanel.jsx b/src/web/components/DevicesPanel.jsx
index a78882e..1424e24 100644
--- a/src/web/components/DevicesPanel.jsx
+++ b/src/web/components/DevicesPanel.jsx
@@ -589,7 +589,7 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
onClick={() => copyDeviceIp(device)}
>{device.ip} : !hasName && Неизвестное устройство}
-
+ {!hasName &&
Изменить название
-
+ }
>
)}
diff --git a/src/web/styles.css b/src/web/styles.css
index 5d47170..d908e73 100644
--- a/src/web/styles.css
+++ b/src/web/styles.css
@@ -946,7 +946,11 @@ p {
display: flex;
align-items: flex-end;
gap: 2px;
- padding: 11px 27px 0 0;
+ padding: 11px 0 0;
+}
+
+.client-device-main:has(.client-device-edit-wrap) {
+ padding-right: 27px;
}
.client-device-main > h3 {
@@ -1396,7 +1400,10 @@ p {
.client-device-traffic-plot {
grid-column: 1 / -1;
grid-row: 1;
+ height: 100%;
+ min-height: 0;
min-width: 0;
+ overflow: hidden;
cursor: crosshair;
}
@@ -1408,7 +1415,7 @@ p {
width: 100%;
height: 100%;
display: block;
- overflow: visible;
+ overflow: hidden;
}
.client-device-traffic-grid line {
diff --git a/test/server/connectivity-diagnostics.test.js b/test/server/connectivity-diagnostics.test.js
index 48a8f83..c784135 100644
--- a/test/server/connectivity-diagnostics.test.js
+++ b/test/server/connectivity-diagnostics.test.js
@@ -1,10 +1,14 @@
import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import path from 'node:path';
import test from 'node:test';
import {
createConnectivityDiagnosticsService,
CURL_META_MARKER,
} from '../../src/server/services/connectivityDiagnosticsService.js';
+const server = fs.readFileSync(path.resolve(import.meta.dirname, '../../src/server/index.js'), 'utf8');
+
function response(body = '', overrides = {}) {
return {
exitCode: 0,
@@ -48,6 +52,11 @@ test('connectivity diagnostics force separate direct and VPN paths', async () =>
assert.ok(calls.some((args) => args.includes('--proxy') && args.includes('http://127.0.0.1:18080')));
});
+test('connectivity diagnostics endpoint is available in Connect and Gateway', () => {
+ assert.match(server, /const localConnectivityDiagnostics = !remoteDataplane/);
+ assert.doesNotMatch(server, /settings\.appMode !== 'gateway'[\s\S]{0,120}ENDPOINT_NOT_FOUND/);
+});
+
test('connectivity diagnostics reports a likely direct restriction without claiming its owner', async () => {
const attempts = new Map();
const execute = async (args) => {
diff --git a/test/server/singbox-client-mode.test.js b/test/server/singbox-client-mode.test.js
index d27e3ff..3b0bce0 100644
--- a/test/server/singbox-client-mode.test.js
+++ b/test/server/singbox-client-mode.test.js
@@ -30,9 +30,14 @@ test('client exposes one local proxy and routes local exceptions before the sele
],
});
- assert.deepEqual(config.inbounds.map((inbound) => inbound.tag), ['mixed-in']);
+ assert.deepEqual(config.inbounds.map((inbound) => inbound.tag), [
+ 'mixed-in',
+ 'diagnostics-vpn-in',
+ ]);
assert.equal(config.inbounds[0].listen_port, 8082);
+ assert.equal(config.inbounds[1].listen_port, 18080);
assert.deepEqual(config.route.rules, [
+ { inbound: ['diagnostics-vpn-in'], outbound: 'test-vpn' },
{ domain_suffix: ['ru'], outbound: 'direct' },
{ domain: ['example.com'], outbound: 'direct' },
{ inbound: ['mixed-in'], outbound: 'test-vpn' },
@@ -48,9 +53,10 @@ test('client keeps its local proxy but routes directly when Harbor Gateway is ah
});
assert.deepEqual(config.route.rules, [
+ { inbound: ['diagnostics-vpn-in'], outbound: 'test-vpn' },
{ domain_suffix: ['ru'], outbound: 'direct' },
{ inbound: ['mixed-in'], outbound: 'direct' },
]);
assert.equal(config.route.final, 'direct');
- assert.deepEqual(config.outbounds.map((outbound) => outbound.tag), ['direct', 'block']);
+ assert.deepEqual(config.outbounds.map((outbound) => outbound.tag), ['test-vpn', 'direct', 'block']);
});
diff --git a/test/server/state-contract.test.js b/test/server/state-contract.test.js
index 67cccb4..06be5be 100644
--- a/test/server/state-contract.test.js
+++ b/test/server/state-contract.test.js
@@ -433,9 +433,9 @@ setInterval(() => {}, 60_000);
assert.equal(routed.state.route.localRulesPendingRestart, false);
assert.deepEqual(routed.state.route.activeLocalRules, routed.state.route.localRules);
assert.deepEqual(JSON.parse(fs.readFileSync(path.join(dir, 'sing-box-config.json'))).route.rules.slice(0, 3), [
+ { inbound: ['diagnostics-vpn-in'], outbound: testServerId },
{ domain: ['example.com'], outbound: 'direct' },
{ domain_suffix: ['example.org'], outbound: 'direct' },
- { inbound: ['mixed-in'], outbound: testServerId },
]);
await mutation('/api/singbox/stop');
diff --git a/test/web/device-inventory-contract.test.js b/test/web/device-inventory-contract.test.js
index 6fbf985..a16f19c 100644
--- a/test/web/device-inventory-contract.test.js
+++ b/test/web/device-inventory-contract.test.js
@@ -42,8 +42,7 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
assert.match(panel, /const hasName = Boolean\(device\.alias \|\| device\.hostname\)/);
assert.match(panel, /client-device-alias-trigger[\s\S]*client-device-name-separator[\s\S]*client-device-ip/);
assert.match(panel, /onClick=\{\(\) => startEditing\(device\)\}/);
- assert.match(panel, //);
- assert.doesNotMatch(panel, /\{!hasName && /);
assert.match(panel, /pencilAnimationId === device\.id \? ' is-writing'/);
assert.match(panel, /onAnimationEnd=\{\(\) => setPencilAnimationId/);
assert.match(panel, /COPY_FEEDBACK_MS = 800/);
@@ -117,7 +116,8 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
assert.match(styles, /\.client-device-traffic strong \{[\s\S]*font-size: 10px/);
assert.match(styles, /\.client-device-name-separator \{[\s\S]*color: var\(--client-muted\)/);
assert.match(styles, /\.client-device-ip\.is-copied \{[\s\S]*client-device-ip-copy 800ms/);
- assert.match(styles, /\.client-device-main \{[^}]*height: 34px[\s\S]*align-items: flex-end[\s\S]*padding: 11px 27px 0 0/);
+ assert.match(styles, /\.client-device-main \{[^}]*height: 34px[\s\S]*align-items: flex-end[\s\S]*padding: 11px 0 0/);
+ assert.match(styles, /\.client-device-main:has\(\.client-device-edit-wrap\) \{[\s\S]*padding-right: 27px/);
assert.match(styles, /\.client-device-last-seen \{[^}]*height: 10px[\s\S]*align-items: center/);
assert.match(styles, /\.client-device-traffic-value\.has-delta > \.is-total[\s\S]*translateY\(-0\.18em\)/);
assert.match(styles, /\.client-device-last-seen\.is-online \{[\s\S]*color: var\(--client-accent\)/);
@@ -129,6 +129,8 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
assert.match(styles, /\.client-device-edit-wrap \{[\s\S]*position: absolute;[\s\S]*right: 0;[\s\S]*bottom: 0/);
assert.match(styles, /\.client-device-edit\.is-writing svg \{[\s\S]*client-device-pencil-write 620ms/);
assert.match(styles, /@keyframes client-device-pencil-write[\s\S]*0%, 100%[\s\S]*translate\(1px, -1px\) rotate\(-5deg\)/);
+ assert.match(styles, /\.client-device-traffic-plot \{[\s\S]*height: 100%;[\s\S]*min-height: 0;[\s\S]*overflow: hidden/);
+ assert.match(styles, /\.client-device-traffic-plot svg \{[\s\S]*height: 100%;[\s\S]*overflow: hidden/);
assert.match(styles, /\.client-devices-sort:hover \.client-devices-sort-icon[\s\S]*rotate\(360deg\)/);
assert.match(styles, /\.client-devices-refresh-ring circle[\s\S]*client-devices-refresh-progress 15s/);
assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.client-device-policy svg[\s\S]*\.client-device-alias-trigger[\s\S]*\.client-device-ip[\s\S]*\.client-device-traffic-value > span[\s\S]*\.client-device-traffic-breakdown[\s\S]*\.client-device-traffic-lines[\s\S]*\.client-text-morph-value/);
diff --git a/test/web/responsive-layout-contract.test.js b/test/web/responsive-layout-contract.test.js
index 286bbfe..a8daeee 100644
--- a/test/web/responsive-layout-contract.test.js
+++ b/test/web/responsive-layout-contract.test.js
@@ -91,6 +91,8 @@ test('secondary menus share one right rail and both drawers open from the right'
assert.match(component, /client-instructions-toggle[\s\S]*client-local-rules-toggle/);
assert.match(component, /client-diagnostics-toggle/);
assert.match(component, /