Reposition Gateway traffic summary below power controls
Build and Deploy Gateway / build-and-push (push) Successful in 14s
Build and Deploy Gateway / deploy (push) Successful in 7s

This commit is contained in:
2026-08-08 01:13:48 +03:00
parent 3cdb0c735e
commit c26d4cb43b
5 changed files with 47 additions and 26 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
export const HARBOR_VERSIONS = Object.freeze({
macClient: '0.18.1',
gatewayClient: '0.19.1',
macClient: '0.18.2',
gatewayClient: '0.19.2',
gatewayBackend: '0.19.0',
});
+19 -19
View File
@@ -1426,25 +1426,6 @@ export function ClientOverviewPage({
</div>
</div>
{isGateway && <section className="client-gateway-summary" aria-label="Общий трафик Harbor">
<div className="client-gateway-traffic-heading">
<span>Учтено Harbor</span>
<strong>{formatByteString(globalTraffic?.totalBytes || '0')}</strong>
</div>
<div className="client-gateway-traffic-chart">
<TrafficChart
samples={globalTraffic?.history || []}
capacity={deviceSnapshot?.trafficHistoryCapacity || 120}
routeLabel="Gateway"
/>
</div>
<div className={`client-gateway-traffic-freshness${trafficSourceError ? ' is-stale' : ''}`} role="status" aria-live="polite">
{trafficSourceError
? `Трафик не обновляется · последние данные ${trafficFreshness}`
: globalTraffic?.observedAt ? `Обновлено ${trafficFreshness}` : 'Ожидаем первые данные трафика'}
</div>
</section>}
<section className={`client-proxies${isGateway ? ' is-gateway' : ''}`} aria-label={isGateway ? 'Gateway и Gateway Proxy' : 'Локальный прокси'}>
<div className="client-access-point">
{!isGateway && (
@@ -1486,6 +1467,25 @@ export function ClientOverviewPage({
</section>
)}
{isGateway && <section className="client-gateway-summary" aria-label="Общий трафик Harbor">
<div className="client-gateway-traffic-heading">
<span>Учтено Harbor</span>
<strong>{formatByteString(globalTraffic?.totalBytes || '0')}</strong>
</div>
<div className="client-gateway-traffic-chart">
<TrafficChart
samples={globalTraffic?.history || []}
capacity={deviceSnapshot?.trafficHistoryCapacity || 120}
routeLabel="Gateway"
/>
</div>
<div className={`client-gateway-traffic-freshness${trafficSourceError ? ' is-stale' : ''}`} role="status" aria-live="polite">
{trafficSourceError
? `Трафик не обновляется · последние данные ${trafficFreshness}`
: globalTraffic?.observedAt ? `Обновлено ${trafficFreshness}` : 'Ожидаем первые данные трафика'}
</div>
</section>}
<div
ref={isGateway ? subscriptionPanelRef : undefined}
id={isGateway ? 'client-subscription-drawer' : undefined}
+14 -1
View File
@@ -2615,8 +2615,16 @@ p {
justify-self: center;
}
.client-panel.is-gateway-home {
grid-template-rows: minmax(0, 1fr) auto;
row-gap: 28px;
}
.client-gateway-summary {
width: min(300px, 100%);
grid-column: 1 / -1;
grid-row: 2;
justify-self: center;
width: min(480px, calc(100% - 24px));
display: grid;
gap: 6px;
text-align: center;
@@ -4553,6 +4561,11 @@ p {
order: 1;
}
.client-gateway-summary {
grid-row: auto;
order: 2;
}
.client-form {
height: auto;
max-height: none;
+8 -1
View File
@@ -166,12 +166,19 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
});
test('Gateway Home reuses the canonical device snapshot for applied route and global traffic', () => {
const powerStart = overview.indexOf('<section className="client-power-section"');
const trafficStart = overview.indexOf('<section className="client-gateway-summary"');
const powerPrefix = overview.slice(powerStart, trafficStart);
assert.match(overview, /const appliedServerId = state\?\.selection\?\.appliedServerId \|\| ''/);
assert.match(overview, /appliedServer\?\.label \|\| 'VPN-сервер не используется'/);
assert.match(overview, /selectedServerId !== appliedServerId[\s\S]*Переключаем на \{desiredServer\.label\}/);
assert.match(overview, /formatByteString\(globalTraffic\?\.totalBytes \|\| '0'\)/);
assert.match(overview, /samples=\{globalTraffic\?\.history \|\| \[\]\}[\s\S]*capacity=\{deviceSnapshot\?\.trafficHistoryCapacity \|\| 120\}[\s\S]*routeLabel="Gateway"/);
assert.match(overview, /<section className="client-power-section"[\s\S]*client-connection-title[\s\S]*client-gateway-route-summary[\s\S]*client-state-detail[\s\S]*<section className="client-gateway-summary"[\s\S]*client-proxies/);
assert.match(overview, /<section className="client-power-section"[\s\S]*client-connection-title[\s\S]*client-gateway-route-summary[\s\S]*client-state-detail[\s\S]*client-proxies[\s\S]*<section className="client-gateway-summary"/);
assert.ok(powerStart >= 0 && trafficStart > powerStart, 'traffic summary follows the power section');
assert.equal((powerPrefix.match(/<section\b/g) || []).length, (powerPrefix.match(/<\/section>/g) || []).length, 'power section is closed before traffic summary');
assert.equal((overview.match(/className="client-gateway-summary"/g) || []).length, 1);
assert.doesNotMatch(overview, /<TrafficChart[\s\S]{0,240}scale=/);
assert.match(overview, /deviceStatus === 'error' \? deviceError : null/);
assert.match(overview, /if \(!isGateway && \(!connected \|\| !state\?\.singboxStartedAt\)\) return undefined/);
+4 -3
View File
@@ -30,8 +30,9 @@ test('desktop layout keeps the power control on a symmetric center axis', () =>
assert.match(styles, /\.client-panel\.has-subscription \{[\s\S]*transform:\s*translateY\(-9vh\)/);
assert.match(styles, /\.client-panel\.has-subscription \.client-power-section \{[\s\S]*height:\s*var\(--client-work-height\);[\s\S]*padding-top:\s*var\(--client-power-top\)/);
assert.match(styles, /\.client-panel\.has-subscription \.client-form-content \{[\s\S]*padding-top:\s*var\(--client-power-top\)/);
assert.match(rule('.client-gateway-summary'), /width:\s*min\(300px, 100%\)/);
assert.doesNotMatch(rule('.client-gateway-summary'), /grid-column|justify-self|padding-right/);
assert.match(rule('.client-panel.is-gateway-home'), /grid-template-rows:\s*minmax\(0, 1fr\) auto[\s\S]*row-gap:\s*28px/);
assert.match(rule('.client-gateway-summary'), /grid-column:\s*1 \/ -1[\s\S]*grid-row:\s*2[\s\S]*justify-self:\s*center[\s\S]*width:\s*min\(480px, calc\(100% - 24px\)\)/);
assert.doesNotMatch(rule('.client-gateway-summary'), /background|border|box-shadow/);
assert.match(rule('.client-gateway-route-slot'), /min-height:\s*16px[\s\S]*overflow:\s*hidden[\s\S]*text-overflow:\s*ellipsis[\s\S]*white-space:\s*nowrap/);
assert.match(component, /client-panel\$\{showPower \? '' : ' is-setup'\}[\s\S]*is-gateway-home/);
assert.match(component, /const showPower = isGateway \|\| \(hasSubscription && Boolean\(selectedServerId\)\)/);
@@ -74,7 +75,7 @@ test('tablet and mobile regions use normal flow with viewport-safe widths', () =
assert.match(responsive, /grid-template-columns:\s*minmax\(0, 1fr\)/);
assert.match(responsive, /\.client-power-section,[\s\S]*\.client-form,[\s\S]*grid-column:\s*1/);
assert.match(responsive, /\.client-power-section \{[\s\S]*order:\s*1/);
assert.doesNotMatch(responsive, /\.client-gateway-summary/);
assert.match(responsive, /\.client-gateway-summary \{[\s\S]*grid-row:\s*auto;[\s\S]*order:\s*2/);
assert.match(responsive, /width:\s*min\(100%, 380px\)/);
assert.match(responsive, /max-height:\s*none/);
assert.match(responsive, /overflow-y:\s*visible/);