fix(web): загрузка квот больше не выглядит как отсутствие данных

Владелец запустил веб, увидел «Н/Д» у всех аккаунтов и сообщил, что
лимиты не подтягиваются. Через пятнадцать секунд всё появилось: опрос
провайдера просто ещё шёл.

Признак is_loading сервер отдавал (баз�овый снапшот выставляет его при
незавершённом опросе), но клиент его игнорировал и рисовал «Н/Д» — тот
же текст, что у подключённого аккаунта без лимитов. Два разных состояния
выглядели одинаково, и различить их было нельзя.

Теперь во время опроса ячейка показывает «Загрузка…» и «Опрашиваем
провайдера…» вместо прочерка.

Причина отказа важнее флага: если провайдер уже ответил «лимитов не
даю», состояние загрузки подавляется — иначе opencode-go и grok
показывали бы «Загрузка…» бесконечно.

Закреплено тестом в test_web_client_contract.py, включая проверку этого
подавления.

Тесты: 328 passed, ruff чисто.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hermes Team 2026-08-23 21:38:31 +07:00
parent c75b4e42ac
commit 04e5d0dcf5
2 changed files with 36 additions and 5 deletions

View file

@ -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 = `
<div class="account-quota-grid ${visibleBuckets.length === 1 ? 'single-cell' : ''}">
${visibleBuckets.map((b) => renderQuotaCell(b, unavailableReason)).join('')}
${visibleBuckets.map((b) => renderQuotaCell(b, unavailableReason, isLoading)).join('')}
</div>
`;
} 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) {
<div class="quota-cell">
<div class="quota-cell-top">
<span class="quota-cell-title">Квота</span>
<span class="quota-cell-value text-muted">Н/Д</span>
<span class="quota-cell-value text-muted">${isLoading ? 'Загрузка…' : 'Н/Д'}</span>
</div>
<div class="quota-bar-track">
<div class="quota-bar-fill" style="width: 0%; background-color: var(--status-disabled);"></div>
@ -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 `
<div class="quota-cell">
<div class="quota-cell-top">

View file

@ -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, (
"состояние загрузки не подавляется при известной причине отказа"
)