From 4c2594bcf6bd59e65521934c57451d0e32c342d7 Mon Sep 17 00:00:00 2001 From: Hermes Team Date: Tue, 25 Aug 2026 17:17:29 +0700 Subject: [PATCH] =?UTF-8?q?fix(web):=20=C2=AB=D1=82=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=BA=D0=BE=20=D0=BF=D0=BE=D0=B4=D0=BA=D0=BB=D1=8E=D1=87=D1=91?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=B5=C2=BB=20=D0=BF=D1=80=D0=BE=D0=BF=D1=83?= =?UTF-8?q?=D1=81=D0=BA=D0=B0=D0=BB=D0=BE=20=D1=85=D0=BE=D0=BB=D0=BE=D0=B4?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D1=80=D0=B5=D0=B7=D0=B5=D1=80=D0=B2=20?= =?UTF-8?q?=D0=B8=20=D0=BF=D1=83=D1=81=D1=82=D1=8B=D0=B5=20=D1=81=D0=BB?= =?UTF-8?q?=D0=BE=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проверка A26 исполнением. Признак подключённости был записан как p.authenticated === true || (p.health_state && p.health_state !== 'not_configured') и содержит два дефекта. Поля authenticated в ProfileViewModel нет вовсе — первая половина условия мертва. Вторая пропускает всё, кроме not_configured, то есть холодный резерв (health_state "disabled") и непроверенные пустые слоты. Измерено на живом снапшоте: из 24 профилей фильтр пропускал 5, при том что по-настоящему подключён 1. При нуле настоящих аккаунтов страница показывала три карточки «Холодный резерв» вместо пустого состояния — ровно тот мусор, который владелец просил убрать. Тот же предикат используется для выбора аккаунта на «Обзоре», поэтому там предлагалось назначать роли на пустые слоты: мышление слотами, ради отмены которого задание и делалось. Authoritative признак — auth_state: у подключённого AUTHENTICATED, у пустого слота и у холодного резерва NOT_CONFIGURED. AUTH_REQUIRED и AUTH_EXPIRED означают подключённый аккаунт, которому нужен повторный вход, — показываем. Заодно в выборе аккаунта показывается почта, а не имя профиля: жалоба из A24 про «Кодер 1 — назначенный аккаунт Кодер 2» иначе возвращалась. Проверено в браузере: было 5 «подключённых» из 24, стало 2 — оба с auth_state AUTHENTICATED; холодный резерв из выбора на «Обзоре» исчез. 442 passed, ruff чисто. Co-Authored-By: Claude Opus 5 --- .../router/web/static/app.js | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/antigravity_provider/router/web/static/app.js b/src/antigravity_provider/router/web/static/app.js index 831b13f..aa9028f 100644 --- a/src/antigravity_provider/router/web/static/app.js +++ b/src/antigravity_provider/router/web/static/app.js @@ -297,6 +297,28 @@ function startPolling() { } } +// Подключён ли профиль на самом деле. +// +// A26 определял это как «health_state не равен not_configured», а поле +// authenticated в модели вообще отсутствует, поэтому первая половина условия +// была мертва. Через фильтр проходили холодный резерв (health_state +// "disabled") и непроверенные пустые слоты: при нуле настоящих аккаунтов +// страница показывала три карточки «Холодный резерв», а «Обзор» предлагал +// назначать роли на пустые слоты — то самое мышление слотами, ради отмены +// которого задание и делалось. +// +// Authoritative признак — auth_state: у подключённого AUTHENTICATED, у +// пустого слота и у холодного резерва NOT_CONFIGURED. Состояния +// AUTH_REQUIRED и AUTH_EXPIRED означают подключённый аккаунт, которому нужен +// повторный вход, — их показываем. +function isConnectedProfile(p) { + if (!p) return false; + const st = String(p.auth_state || '').toUpperCase(); + if (st) return st !== 'NOT_CONFIGURED'; + // Запасной путь, если поле не пришло: судим по наличию опознанного аккаунта. + return Boolean(p.email); +} + // ── КОПИРОВАНИЕ В БУФЕР ── // // navigator.clipboard существует только в защищённом контексте: HTTPS или @@ -472,7 +494,7 @@ function updateGlobalHeader() { const readiness = currentSnapshot.readiness || {}; const allProfiles = Object.values(currentSnapshot.all_profiles || {}); const connectedAccounts = readiness.accounts_connected_count ?? allProfiles.filter( - (p) => p.authenticated === true || (p.health_state && p.health_state !== 'not_configured') + (p) => isConnectedProfile(p) ).length; if (elements.navAccountsCount) elements.navAccountsCount.textContent = connectedAccounts; @@ -544,7 +566,7 @@ function renderAccountsView() { const allProfiles = Object.values(currentSnapshot.all_profiles || {}); const totalConnectedInSystem = allProfiles.filter( - (p) => p.authenticated === true || (p.health_state && p.health_state !== 'not_configured') + (p) => isConnectedProfile(p) ).length; if (totalConnectedInSystem === 0) { @@ -588,7 +610,7 @@ function renderAccountsView() { if (providerFilter !== 'all' && providerFilter !== providerId) continue; const filtered = profiles.filter((p) => { - const isConnected = p.authenticated === true || (p.health_state && p.health_state !== 'not_configured'); + const isConnected = isConnectedProfile(p); if (!isConnected) return false; totalProfiles++; @@ -825,7 +847,7 @@ function renderOverviewView() { if (diagramBox) { const roles = currentSnapshot.routing || {}; const allConnectedProfiles = Object.values(currentSnapshot.all_profiles || {}).filter( - (p) => p.authenticated === true || (p.health_state && p.health_state !== 'not_configured') + (p) => isConnectedProfile(p) ); let diagramHtml = ''; @@ -847,7 +869,7 @@ function renderOverviewView() { if (allConnectedProfiles.length > 0) { accountControlHtml = ` `;