diff --git a/.gitignore b/.gitignore
index 83f4683..6dff8e9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@ data/
# Local roadmap and task workspace
/workpack/
+/design-qa.md
# Node/Vite
node_modules/
diff --git a/src/shared/versions.ts b/src/shared/versions.ts
index 67876ea..37aab49 100644
--- a/src/shared/versions.ts
+++ b/src/shared/versions.ts
@@ -1,6 +1,6 @@
export const HARBOR_VERSIONS = Object.freeze({
- macClient: '0.35.0',
- gatewayClient: '0.37.0',
+ macClient: '0.35.1',
+ gatewayClient: '0.37.1',
gatewayBackend: '0.37.0',
});
diff --git a/src/web/features/failover/FailoverFeature.tsx b/src/web/features/failover/FailoverFeature.tsx
index 07e23c0..c75205b 100644
--- a/src/web/features/failover/FailoverFeature.tsx
+++ b/src/web/features/failover/FailoverFeature.tsx
@@ -482,6 +482,9 @@ export function FailoverPanel({
const selectedId = channelOptions.find(({ target }) => (
target.profileId === draft[channel].profileId && target.serverId === draft[channel].serverId
))?.id || '';
+ const failureProgress = !checking && channel === 'primary' && snapshot.reason === 'failure-window'
+ ? `${seconds(Math.max(0, Date.now() - Date.parse(snapshot.primary.stateSince || new Date().toISOString())))} / ${seconds(draft.failureWindowMs)} с`
+ : '';
const healthLabel = missing
? 'Сервер не найден'
: !draft.enabled
@@ -490,15 +493,20 @@ export function FailoverPanel({
? 'VPN выключен'
: snapshot.activation === 'pending'
? 'Проверим после запуска VPN'
- : channel === 'primary' && snapshot.reason === 'failure-window'
- ? `Проверяем основной канал · ${seconds(Math.max(0, Date.now() - Date.parse(snapshot.primary.stateSince || new Date().toISOString())))} из ${seconds(draft.failureWindowMs)} с`
+ : checking
+ ? `Проверяем ${channel === 'primary' ? 'основной' : 'резервный'} канал`
+ : failureProgress
+ ? 'Проверяем основной канал'
: channel === 'primary' && snapshot.currentRole === 'reserve' && health === 'healthy' && snapshot.reason === 'recovery-hold'
? 'Проверяем восстановление'
: health === 'healthy' ? 'Работает' : health === 'unhealthy' ? 'Недоступен' : health === 'not-monitoring' ? 'Проверка выключена' : 'Ждёт проверки';
return
{channel === 'primary' ? 'Основной' : 'Резервный'}
- {healthLabel}
+
+
+ {healthLabel}
+ {failureProgress}
setOpenPicker(open ? channel : '')}
onChange={(optionId) => updateTarget(channel, optionId)}
/>
-
- Проверяем {channel === 'primary' ? 'основной' : 'резервный'} канал
-
{channel === 'primary' &&