fix(mera/ui): карточка прогресса перестала выдумывать стадии (#3081, вариант A)
All checks were successful
CI / changes (pull_request) Successful in 11s
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 59s

Запрос оценки один и блокирующий (POST /estimate, mutation.isPending) —
промежуточных событий по источникам не существует, а карточка рисовала
пофайловые «сбор...» с полосками 60%, счётчик 0/5 и подпись про Celery
group с таймаутом. Пользователь не мог отличить «думает» от «завис», а
серверная механика текла в UI-текст.

Вариант A из задачи (только фронт):
- на pending строки нейтральны («ожидает ответ»), раскраска — только по
  факту estimate.sources_used после ответа;
- общая полоса на pending — честная неопределённая анимация вместо
  выдуманного процента (Math.min(95, ...) убран);
- счётчик N/M на pending заменён на «опрашиваем…» (0/5 при идущей работе
  читался как отказ);
- подпись без Celery/таймаута, тон legacy-заглушки «Считаем оценку…»;
- недостижимые ветки error/loading у строк удалены (их ничто не выставляло).

Вариант B (реальный статус с бэкенда) отклонён сознательно: /estimate
остаётся синхронным по решениям #3082/#3083, городить async+task_id ради
прогресс-бара — против них.

Preview-страница /ui-preview/estimate теперь рендерит ОБА состояния
карточки. Проверено скриншотом на dev (pending + done).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
bot-backend 2026-08-26 12:54:32 +05:00
parent 729e9acc52
commit a4604dd9db
3 changed files with 35 additions and 34 deletions

View file

@ -152,7 +152,8 @@ function PreviewContent() {
Оценка стоимости квартиры
</h1>
<section className="result-col">
{/* 1. SourcesProgress */}
{/* 1. SourcesProgress — оба состояния: pending (#3081) и done */}
<SourcesProgress estimate={null} isPending={true} />
<SourcesProgress estimate={FIXTURE_ESTIMATE} isPending={false} />
{/* 2. WhatIfPanel */}

View file

@ -20,7 +20,7 @@ interface SourceRow {
key: string;
label: string;
dotClass: string;
status: "done" | "loading" | "error" | "idle";
status: "done" | "idle";
count?: number;
}
@ -40,11 +40,15 @@ export function SourcesProgress({ estimate, isPending }: Props) {
}
// Листинговые источники — из реестра.
// #3081: пофайловых статусов во время запроса НЕТ — запрос один и блокирующий
// (POST /estimate, mutation.isPending), промежуточных событий по источникам не
// существует. Строки нейтральны до ответа; раскраска — только по факту
// estimate.sources_used.
const listingRows: SourceRow[] = LIVE_LISTING_SOURCES.map((s) => ({
key: s.id,
label: s.label,
dotClass: s.dot,
status: used.has(s.id) ? "done" : isPending ? "loading" : "idle",
status: used.has(s.id) ? "done" : "idle",
count: countBySource[s.id],
}));
@ -54,19 +58,16 @@ export function SourcesProgress({ estimate, isPending }: Props) {
key: "rosreestr",
label: "Росреестр (внутр.)",
dotClass: "rosreestr",
status:
isDone && (estimate?.actual_deals.length ?? 0) > 0
? "done"
: isPending
? "loading"
: "idle",
status: isDone && (estimate?.actual_deals.length ?? 0) > 0 ? "done" : "idle",
count: estimate?.actual_deals.length,
},
];
const doneCount = rows.filter((r) => r.status === "done").length;
const totalCount = rows.length;
const overallPct = isDone ? 100 : Math.min(95, (doneCount / totalCount) * 100 + (isPending ? 10 : 0));
// #3081: процент не выдумывается — реального прогресса у блокирующего запроса
// нет. На pending полоса неопределённая (CSS-анимация), после ответа — 100.
const overallPct = isDone ? 100 : 0;
return (
<article className="card progress-card">
@ -77,8 +78,14 @@ export function SourcesProgress({ estimate, isPending }: Props) {
</div>
<div className="card-meta">
<div className="num-done">
<span data-tnum>{doneCount}</span>
<span className="total"> / {totalCount}</span> источников
{isPending ? (
<span className="total">опрашиваем</span>
) : (
<>
<span data-tnum>{doneCount}</span>
<span className="total"> / {totalCount}</span> источников
</>
)}
</div>
</div>
</div>
@ -87,13 +94,13 @@ export function SourcesProgress({ estimate, isPending }: Props) {
<div className="progress-summary">
<span className="lead">
{isPending
? "Идёт параллельный запрос — Celery group, timeout 30 сек."
? "Считаем оценку — обычно занимает несколько секунд…"
: isDone
? "Готово. Частичные результаты доступны при недоступных источниках."
: "Введите параметры квартиры и нажмите «Оценить»."}
</span>
<div className="progress-overall">
<div className="bar" style={{ width: `${overallPct}%` }} />
<div className={`progress-overall${isPending ? " is-indeterminate" : ""}`}>
<div className="bar" style={isPending ? undefined : { width: `${overallPct}%` }} />
</div>
</div>
@ -106,17 +113,6 @@ export function SourcesProgress({ estimate, isPending }: Props) {
<polyline points="20 6 9 17 4 12" />
</svg>
)}
{r.status === "loading" && (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
)}
{r.status === "error" && (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
)}
{r.status === "idle" && (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="9" />
@ -128,25 +124,20 @@ export function SourcesProgress({ estimate, isPending }: Props) {
<i
style={{
// @ts-expect-error CSS custom prop
"--p": r.status === "done" ? "100%" : r.status === "loading" ? "60%" : "0%",
"--p": r.status === "done" ? "100%" : "0%",
}}
/>
</span>
<span
className="src-value"
style={{ color: r.status === "error" ? "var(--danger)" : undefined }}
>
<span className="src-value">
{r.status === "done" && r.count !== undefined && (
<>
<b>{r.count} лотов</b>
</>
)}
{r.status === "done" && r.count === undefined && <b>готово</b>}
{r.status === "loading" && "сбор..."}
{r.status === "error" && "timeout — нет ответа"}
{r.status === "idle" && (
<span style={{ color: "var(--muted-2)" }}>
{isDone ? "нет данных" : "ожидает запрос"}
{isDone ? "нет данных" : isPending ? "ожидает ответ" : "ожидает запрос"}
</span>
)}
</span>

View file

@ -497,6 +497,15 @@
background: linear-gradient(90deg, var(--accent) 0%, var(--viz-3) 100%);
transition: width .3s ease;
}
/* #3081: во время запроса реального процента нет — полоса неопределённая */
.progress-overall.is-indeterminate .bar {
width: 32%;
animation: indeterminate-slide 1.4s ease-in-out infinite;
}
@keyframes indeterminate-slide {
0% { transform: translateX(-110%); }
100% { transform: translateX(340%); }
}
.progress-eta {
font-family: var(--font-mono);
font-size: 11px;