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:
parent
c75b4e42ac
commit
04e5d0dcf5
2 changed files with 36 additions and 5 deletions
|
|
@ -413,6 +413,12 @@ function renderAccountCard(profile) {
|
||||||
const quotaSnap = profile.quota_snapshot || (currentSnapshot.quotas || {})[profile.profile_id];
|
const quotaSnap = profile.quota_snapshot || (currentSnapshot.quotas || {})[profile.profile_id];
|
||||||
const buckets = (quotaSnap && quotaSnap.buckets) ? quotaSnap.buckets : [];
|
const buckets = (quotaSnap && quotaSnap.buckets) ? quotaSnap.buckets : [];
|
||||||
const unavailableReason = quotaSnap ? quotaSnap.unavailable_reason : null;
|
const unavailableReason = quotaSnap ? quotaSnap.unavailable_reason : null;
|
||||||
|
// Опрос провайдера идёт в фоне и занимает секунды. Пока он не завершился,
|
||||||
|
// корзины пусты — но это НЕ «данных нет». Показывать в этот момент «Н/Д»
|
||||||
|
// значит выдавать загрузку за отсутствие данных: владелец видел ровно это
|
||||||
|
// и решил, что лимиты не подтягиваются. Причина отказа важнее флага: если
|
||||||
|
// провайдер уже ответил «лимитов не даю», это не загрузка.
|
||||||
|
const isLoading = Boolean(quotaSnap && quotaSnap.is_loading) && !unavailableReason;
|
||||||
|
|
||||||
let quotaGridHtml = '';
|
let quotaGridHtml = '';
|
||||||
|
|
||||||
|
|
@ -420,11 +426,11 @@ function renderAccountCard(profile) {
|
||||||
const visibleBuckets = buckets.slice(0, 4);
|
const visibleBuckets = buckets.slice(0, 4);
|
||||||
quotaGridHtml = `
|
quotaGridHtml = `
|
||||||
<div class="account-quota-grid ${visibleBuckets.length === 1 ? 'single-cell' : ''}">
|
<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>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
const reasonText = unavailableReason || (
|
const reasonText = (isLoading ? 'Опрашиваем провайдера…' : null) || unavailableReason || (
|
||||||
profile.health_state === 'not_configured' || profile.health_state === 'auth_required'
|
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">
|
||||||
<div class="quota-cell-top">
|
<div class="quota-cell-top">
|
||||||
<span class="quota-cell-title">Квота</span>
|
<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>
|
||||||
<div class="quota-bar-track">
|
<div class="quota-bar-track">
|
||||||
<div class="quota-bar-fill" style="width: 0%; background-color: var(--status-disabled);"></div>
|
<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;
|
const remaining = bucket.remaining_percent;
|
||||||
let formattedValue = 'Н/Д';
|
let formattedValue = isLoading ? 'Загрузка…' : 'Н/Д';
|
||||||
let barWidth = 0;
|
let barWidth = 0;
|
||||||
let colorClass = 'var(--status-disabled)';
|
let colorClass = 'var(--status-disabled)';
|
||||||
|
|
||||||
|
|
@ -489,6 +495,10 @@ function renderQuotaCell(bucket, unavailableReason) {
|
||||||
? `Сброс: ${formatIsoDate(bucket.reset_at)}`
|
? `Сброс: ${formatIsoDate(bucket.reset_at)}`
|
||||||
: (bucket.period ? `Период: ${bucket.period}` : (unavailableReason || 'Период провайдера'));
|
: (bucket.period ? `Период: ${bucket.period}` : (unavailableReason || 'Период провайдера'));
|
||||||
|
|
||||||
|
if (isLoading && typeof remaining !== 'number') {
|
||||||
|
resetText = 'Опрашиваем провайдера…';
|
||||||
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="quota-cell">
|
<div class="quota-cell">
|
||||||
<div class="quota-cell-top">
|
<div class="quota-cell-top">
|
||||||
|
|
|
||||||
|
|
@ -103,3 +103,24 @@ def test_actions_contract_handling():
|
||||||
assert "/api/action" in app_js
|
assert "/api/action" in app_js
|
||||||
assert "executeAction" in app_js
|
assert "executeAction" in app_js
|
||||||
assert "modal-feedback-area" 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, (
|
||||||
|
"состояние загрузки не подавляется при известной причине отказа"
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue