From 04e5d0dcf592cb6bcf6a0b30d1e25c25b398baf9 Mon Sep 17 00:00:00 2001 From: Hermes Team Date: Sun, 23 Aug 2026 21:38:31 +0700 Subject: [PATCH] =?UTF-8?q?fix(web):=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83?= =?UTF-8?q?=D0=B7=D0=BA=D0=B0=20=D0=BA=D0=B2=D0=BE=D1=82=20=D0=B1=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D1=88=D0=B5=20=D0=BD=D0=B5=20=D0=B2=D1=8B=D0=B3?= =?UTF-8?q?=D0=BB=D1=8F=D0=B4=D0=B8=D1=82=20=D0=BA=D0=B0=D0=BA=20=D0=BE?= =?UTF-8?q?=D1=82=D1=81=D1=83=D1=82=D1=81=D1=82=D0=B2=D0=B8=D0=B5=20=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D1=8B=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Владелец запустил веб, увидел «Н/Д» у всех аккаунтов и сообщил, что лимиты не подтягиваются. Через пятнадцать секунд всё появилось: опрос провайдера просто ещё шёл. Признак is_loading сервер отдавал (баз�овый снапшот выставляет его при незавершённом опросе), но клиент его игнорировал и рисовал «Н/Д» — тот же текст, что у подключённого аккаунта без лимитов. Два разных состояния выглядели одинаково, и различить их было нельзя. Теперь во время опроса ячейка показывает «Загрузка…» и «Опрашиваем провайдера…» вместо прочерка. Причина отказа важнее флага: если провайдер уже ответил «лимитов не даю», состояние загрузки подавляется — иначе opencode-go и grok показывали бы «Загрузка…» бесконечно. Закреплено тестом в test_web_client_contract.py, включая проверку этого подавления. Тесты: 328 passed, ruff чисто. Co-Authored-By: Claude Opus 5 --- .../router/web/static/app.js | 20 +++++++++++++----- tests/test_web_client_contract.py | 21 +++++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/antigravity_provider/router/web/static/app.js b/src/antigravity_provider/router/web/static/app.js index e945fb4..26782b9 100644 --- a/src/antigravity_provider/router/web/static/app.js +++ b/src/antigravity_provider/router/web/static/app.js @@ -413,6 +413,12 @@ function renderAccountCard(profile) { const quotaSnap = profile.quota_snapshot || (currentSnapshot.quotas || {})[profile.profile_id]; const buckets = (quotaSnap && quotaSnap.buckets) ? quotaSnap.buckets : []; const unavailableReason = quotaSnap ? quotaSnap.unavailable_reason : null; + // Опрос провайдера идёт в фоне и занимает секунды. Пока он не завершился, + // корзины пусты — но это НЕ «данных нет». Показывать в этот момент «Н/Д» + // значит выдавать загрузку за отсутствие данных: владелец видел ровно это + // и решил, что лимиты не подтягиваются. Причина отказа важнее флага: если + // провайдер уже ответил «лимитов не даю», это не загрузка. + const isLoading = Boolean(quotaSnap && quotaSnap.is_loading) && !unavailableReason; let quotaGridHtml = ''; @@ -420,11 +426,11 @@ function renderAccountCard(profile) { const visibleBuckets = buckets.slice(0, 4); quotaGridHtml = ` `; } else { - const reasonText = unavailableReason || ( + const reasonText = (isLoading ? 'Опрашиваем провайдера…' : null) || unavailableReason || ( profile.health_state === 'not_configured' || profile.health_state === 'auth_required' ? 'Аккаунт не подключён' : 'Провайдер не отдаёт лимиты' @@ -434,7 +440,7 @@ function renderAccountCard(profile) {
Квота - Н/Д + ${isLoading ? 'Загрузка…' : 'Н/Д'}
@@ -469,9 +475,9 @@ function renderAccountCard(profile) { `; } -function renderQuotaCell(bucket, unavailableReason) { +function renderQuotaCell(bucket, unavailableReason, isLoading) { const remaining = bucket.remaining_percent; - let formattedValue = 'Н/Д'; + let formattedValue = isLoading ? 'Загрузка…' : 'Н/Д'; let barWidth = 0; let colorClass = 'var(--status-disabled)'; @@ -489,6 +495,10 @@ function renderQuotaCell(bucket, unavailableReason) { ? `Сброс: ${formatIsoDate(bucket.reset_at)}` : (bucket.period ? `Период: ${bucket.period}` : (unavailableReason || 'Период провайдера')); + if (isLoading && typeof remaining !== 'number') { + resetText = 'Опрашиваем провайдера…'; + } + return `
diff --git a/tests/test_web_client_contract.py b/tests/test_web_client_contract.py index 67fb75c..f03aebb 100644 --- a/tests/test_web_client_contract.py +++ b/tests/test_web_client_contract.py @@ -103,3 +103,24 @@ def test_actions_contract_handling(): assert "/api/action" in app_js assert "executeAction" in app_js assert "modal-feedback-area" in app_js + + +def test_client_distinguishes_loading_from_missing_data(): + """Загрузка не должна выглядеть как отсутствие данных. + + Опрос провайдера идёт в фоне и занимает секунды. Пока он не завершился, + корзины квот пусты. Клиент показывал в этот момент «Н/Д» — то же самое, + что при подключённом аккаунте без лимитов, — и владелец решил, что + лимиты не подтягиваются вовсе. Сервер отдаёт признак is_loading; + клиент обязан его учитывать. + """ + app_js = (STATIC_DIR / "app.js").read_text(encoding="utf-8") + + assert "is_loading" in app_js, "клиент игнорирует признак загрузки из снапшота" + assert "Загрузка" in app_js, "нет отдельного текста для состояния загрузки" + + # Причина отказа важнее флага: если провайдер уже ответил «лимитов не + # даю», это не загрузка, и показывать «Загрузка…» бесконечно нельзя. + assert "!unavailableReason" in app_js or "! unavailableReason" in app_js, ( + "состояние загрузки не подавляется при известной причине отказа" + )