Merge pull request 'fix(mera/ui): карточка прогресса перестала выдумывать стадии (#3081, вариант A)' (#3101) from fix/3081-honest-progress into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / deploy (push) Successful in 1m18s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / build-frontend (push) Successful in 2m29s
Deploy Trade-In / test (push) Successful in 3m51s
Deploy Trade-In / build-backend (push) Successful in 35s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s

This commit is contained in:
bot-backend 2026-08-26 08:01:56 +00:00
commit 2116c3774e
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;