Remove initial server health check and bump Harbor versions
All checks were successful
Build and Deploy Gateway / build-and-push (push) Successful in 15s
Build and Deploy Gateway / deploy (push) Successful in 12s

This commit is contained in:
2026-07-13 10:31:01 +03:00
parent 90447de0aa
commit 8c19f2cba9
17 changed files with 398 additions and 53 deletions

View File

@@ -17,6 +17,7 @@ import { instructionBlocks } from '../instructions.js';
import { operationBlocked } from '../state/operations.js';
import { ConfirmationPopup } from './ConfirmationPopup.jsx';
import { ServerPicker } from './ServerPicker.jsx';
import { ERROR_DEFINITIONS } from '../../shared/errors.js';
import { canAppendRouteRule } from '../../shared/routingRules.js';
import {
HARBOR_VERSIONS,
@@ -134,10 +135,10 @@ function InlineError({ error, context }) {
return (
<div className={`client-inline-error is-${context}`} role="alert">
<span>{error.message}</span>
{error.retry && <button type="button" onClick={error.retry}>Повторить</button>}
{error.correlationId && (
<small title={error.correlationId}>Код: {error.correlationId.slice(0, 8)}</small>
)}
{error.retry && <button type="button" onClick={error.retry}>Повторить</button>}
</div>
);
}
@@ -234,10 +235,11 @@ const localRuleValues = (rules) => rules
const localRulesSignature = (rules) => JSON.stringify(localRuleValues(rules));
const localRuleKey = ({ type, value, enabled }) => `${type}:${String(value || '').trim().toLowerCase()}:${enabled}`;
function localRuleStatus(rule, savedRules, activeRules) {
function localRuleStatus(rule, savedRules, activeRules, runtimeActive) {
const key = localRuleKey(rule);
if (!savedRules.some((saved) => localRuleKey(saved) === key)) return ['unsaved', 'Не сохранено'];
if (!rule.enabled) return ['disabled', 'Выключено'];
if (!runtimeActive) return ['saved', 'Сохранено'];
if (activeRules.some((active) => localRuleKey(active) === key)) return ['active', 'Активно'];
return ['pending', 'Ждёт перезапуска'];
}
@@ -340,6 +342,7 @@ function LocalRulesPanel({
rules,
savedRules,
activeRules,
runtimeActive,
blocked,
dirty,
restartPending,
@@ -399,7 +402,12 @@ function LocalRulesPanel({
<span id="local-rules-list-title">Правила</span>
<div className="client-local-rules-list">
{rules.map((rule, index) => {
const [status, statusLabel] = localRuleStatus(rule, savedRules, activeRules);
const [status, statusLabel] = localRuleStatus(
rule,
savedRules,
activeRules,
runtimeActive,
);
return (
<div
className={`client-local-rule client-deletable-row is-${status}${rule.enabled ? '' : ' is-disabled'}${rule.removing ? ' is-removing' : ''}`}
@@ -626,6 +634,12 @@ export function ClientOverviewPage({
const [subscriptionContentReady, setSubscriptionContentReady] = useState(hasSubscription);
const [accessTab, setAccessTab] = useState('gateway');
const [copyFeedback, setCopyFeedback] = useState(null);
const [subscriptionValidation, setSubscriptionValidation] = useState({
url: '',
status: 'idle',
error: null,
});
const [subscriptionValidationAttempt, setSubscriptionValidationAttempt] = useState(0);
const [refreshingInfo, setRefreshingInfo] = useState(false);
const [usageUpdated, setUsageUpdated] = useState(false);
const [serverRevealVersion, setServerRevealVersion] = useState(0);
@@ -670,9 +684,21 @@ export function ClientOverviewPage({
? [openInstruction, ...instructionGuides.filter((block) => block.id !== openInstructionId)]
: instructionGuides;
const normalizedSubscriptionUrl = subscriptionUrl.trim();
const currentSubscriptionValidation = subscriptionValidation.url === normalizedSubscriptionUrl
? subscriptionValidation
: null;
const localSubscriptionError = normalizedSubscriptionUrl
&& !isSubscriptionUrlValid(normalizedSubscriptionUrl)
? { context: 'subscription', message: ERROR_DEFINITIONS.SUBSCRIPTION_INVALID.message }
: null;
const subscriptionError = currentSubscriptionValidation?.error
|| localSubscriptionError
|| (error?.context === 'subscription' ? error : null);
const subscriptionValidationStatus = !normalizedSubscriptionUrl
? 'idle'
: isSubscriptionUrlValid(normalizedSubscriptionUrl) ? 'valid' : 'invalid';
: subscriptionError || !isSubscriptionUrlValid(normalizedSubscriptionUrl)
? 'invalid'
: currentSubscriptionValidation?.status || 'checking';
const subscriptionWaiting = hasSubscription && !subscriptionContentReady;
const connectionBlocked = operationBlocked(operations, 'connection');
const serverApplyBlocked = operationBlocked(operations, 'serverApply');
@@ -682,9 +708,12 @@ export function ClientOverviewPage({
const gatewayAutoBlocked = operationBlocked(operations, 'gatewayAuto');
const routeRulesBlocked = operationBlocked(operations, 'routeRules');
const localRulesDirty = localRulesSignature(localRulesDraft) !== localRulesBaselineRef.current;
const pendingLocalRulesCount = (state?.route?.localRules || []).filter((rule) => (
rule.enabled && !(state?.route?.activeLocalRules || []).some((active) => localRuleKey(active) === localRuleKey(rule))
)).length;
const localRulesPendingRestart = connected && state?.route?.localRulesPendingRestart === true;
const pendingLocalRulesCount = localRulesPendingRestart
? (state?.route?.localRules || []).filter((rule) => (
rule.enabled && !(state?.route?.activeLocalRules || []).some((active) => localRuleKey(active) === localRuleKey(rule))
)).length
: 0;
useEffect(() => {
setNow(Date.now());
@@ -698,6 +727,36 @@ export function ClientOverviewPage({
if (editingSubscription) subscriptionInputRef.current?.focus();
}, [editingSubscription]);
useEffect(() => {
if (!normalizedSubscriptionUrl || !isSubscriptionUrlValid(normalizedSubscriptionUrl)) return undefined;
const controller = new AbortController();
setSubscriptionValidation({ url: normalizedSubscriptionUrl, status: 'checking', error: null });
const timer = setTimeout(async () => {
try {
await api.subscription.validate(normalizedSubscriptionUrl, { signal: controller.signal });
setSubscriptionValidation({ url: normalizedSubscriptionUrl, status: 'valid', error: null });
} catch (requestError) {
if (requestError?.name === 'AbortError') return;
setSubscriptionValidation({
url: normalizedSubscriptionUrl,
status: 'invalid',
error: {
context: 'subscription',
message: requestError.message,
correlationId: requestError.correlationId,
retry: requestError.retryable
? () => setSubscriptionValidationAttempt((attempt) => attempt + 1)
: null,
},
});
}
}, 300);
return () => {
clearTimeout(timer);
controller.abort();
};
}, [normalizedSubscriptionUrl, subscriptionValidationAttempt]);
useEffect(() => {
if (!showIntro) return undefined;
const timer = setTimeout(() => setShowIntro(false), 1200);
@@ -956,7 +1015,7 @@ export function ClientOverviewPage({
localRulesBaselineRef.current = JSON.stringify(rules);
setLocalRulesRevision(result.state.route.localRulesRevision);
setConfirmingLocalRulesClose(false);
if (!result.state.route.localRulesPendingRestart) setLocalRulesOpen(false);
if (!connected || !result.state.route.localRulesPendingRestart) setLocalRulesOpen(false);
}
function requestCloseLocalRules() {
@@ -1030,7 +1089,7 @@ export function ClientOverviewPage({
</button>}
{hasSubscription && subscriptionContentReady && <button
ref={localRulesToggleRef}
className={`client-local-rules-toggle${localRulesOpen ? ' is-open' : ''}${state?.route?.localRulesPendingRestart ? ' has-pending' : ''}`}
className={`client-local-rules-toggle${localRulesOpen ? ' is-open' : ''}${localRulesPendingRestart ? ' has-pending' : ''}`}
type="button"
disabled={gatewayDirect}
aria-expanded={localRulesOpen}
@@ -1047,7 +1106,7 @@ export function ClientOverviewPage({
</svg>
<span>{gatewayDirect
? 'Сейчас работают правила Gateway'
: state?.route?.localRulesPendingRestart ? 'Правила ждут перезапуска' : 'Локальные правила'}</span>
: localRulesPendingRestart ? 'Правила ждут перезапуска' : 'Локальные правила'}</span>
</button>}
<main className={`client-panel${showPower ? '' : ' is-setup'}${hasSubscription ? ' has-subscription' : ''}`}>
{showPower && (
@@ -1287,7 +1346,11 @@ export function ClientOverviewPage({
placeholder="Вставьте ссылку подписки"
className={subscriptionUrl ? 'has-value' : ''}
value={subscriptionUrl}
onChange={(event) => setSubscriptionUrl(event.target.value)}
onChange={(event) => {
if (error?.context === 'subscription') onDismissError();
setSubscriptionValidation({ url: '', status: 'idle', error: null });
setSubscriptionUrl(event.target.value);
}}
onKeyDown={(event) => {
if (event.key === 'Escape' && state?.hasSubscription) {
setSubscriptionUrl('');
@@ -1304,18 +1367,21 @@ export function ClientOverviewPage({
<button
className="client-subscription-submit"
type="submit"
aria-live="polite"
aria-label={subscriptionValidationStatus === 'valid'
? 'Сохранить подписку'
: 'Ссылка подписки не распознана'}
: subscriptionValidationStatus === 'checking'
? 'Проверяем подписку'
: subscriptionError?.message || 'Ссылка подписки не распознана'}
disabled={subscriptionImportBlocked || subscriptionValidationStatus !== 'valid'}
>
{subscriptionValidationStatus === 'valid'
? '✓'
: '×'}
: subscriptionValidationStatus === 'checking' ? '…' : '×'}
</button>
)}
</form>
<InlineError error={error} context="subscription" />
<InlineError error={subscriptionError || error} context="subscription" />
<InlineProgress operations={operations} context="subscription" />
</div>
@@ -1406,9 +1472,10 @@ export function ClientOverviewPage({
rules={localRulesDraft}
savedRules={state?.route?.localRules || []}
activeRules={state?.route?.activeLocalRules || []}
runtimeActive={connected}
blocked={routeRulesBlocked || localRulesDraft.some((rule) => rule.removing)}
dirty={localRulesDirty}
restartPending={state?.route?.localRulesPendingRestart === true}
restartPending={localRulesPendingRestart}
error={error}
operations={operations}
panelRef={localRulesPanelRef}

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { api } from '../api.js';
import {
autoServer,
@@ -115,7 +115,6 @@ export function ServerPicker({
const [collapsed, setCollapsed] = useState([]);
const [pings, setPings] = useState({});
const [checking, setChecking] = useState(false);
const initialCheckStarted = useRef(false);
const serverKey = servers.map(({ id }) => id).join('|');
useEffect(() => {
@@ -189,12 +188,6 @@ export function ServerPicker({
}
}
useEffect(() => {
if (initialCheckStarted.current || !servers.length) return;
initialCheckStarted.current = true;
checkVisible();
}, [serverKey]);
if (servers.length === 1) {
return <section className="client-servers" aria-label="Выберите сервер">
{prompt && <span className="client-server-prompt">Выберите сервер</span>}