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 = `
- ${visibleBuckets.map((b) => renderQuotaCell(b, unavailableReason)).join('')} + ${visibleBuckets.map((b) => renderQuotaCell(b, unavailableReason, isLoading)).join('')}
`; } 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, ( + "состояние загрузки не подавляется при известной причине отказа" + )