tradein: бэкенд не отдаёт фактический радиус поиска — карта рисует круг по выбору пользователя, а не по тому, что реально искали #2632

Closed
opened 2026-08-02 13:09:58 +00:00 by lekss361 · 4 comments
Owner

Часть находки M1 аудита #2583, которую нельзя закрыть на фронте. Выяснено при работе над PR #2630.

Проблема

Оценщик умеет расширять радиус поиска, когда аналогов не хватает: base_radius_mfallback_radius_m (1000 → 2000 м, estimator.py:87-88). Пользователю про это честно пишут в confidence_explanation: «расширили радиус до 2 км из-за нехватки данных».

Но фактический радиус наружу не отдаётся: base_radius_m и fallback_radius_m — внутренние переменные estimator.py, в схему AggregatedEstimateOut не сериализуются.

Поэтому круг на карте (ParamsPanel.tsx) рисуется по значению из выпадающего списка, который выбрал пользователь. Если он выбрал километр, а поиск фактически шёл по двум — карта показывает километр.

Получается активно неверная визуальная подсказка: текст говорит одно, картинка другое. Причём картинка убедительнее текста.

Почему это заметно именно сейчас

Расширение радиуса чаще всего срабатывает там, где данных мало, — то есть в городах области. Это ровно та аудитория, ради которой шло расширение на область. По Первоуральску расхождение воспроизводится.

Что сделать

  1. Добавить в схему ответа фактический радиус поиска (например search_radius_m) — тот, по которому реально отбирались аналоги, с учётом расширения.
  2. Фронт рисует круг по нему, а не по выбору пользователя. Если значение не пришло (старый ответ) — падать обратно на выбор пользователя, чтобы не сломать существующее.
  3. Если радиус отличается от выбранного — показать это явно, а не молча: пользователь выбрал километр, а ищем по двум, и он должен понимать почему.

Что уже сделано на фронте

PR #2630 вывел confidence_explanation в интерфейс v2 (раньше не читался вовсе) — текстовая часть оговорки теперь видна. Осталась визуальная.

Прочие поля из M1, которые интерфейс v2 по-прежнему не читает

Отмечены при работе, но в scope PR #2630 не входили: analog_tier, address_precision, manual_review_recommended. Бэкенд их отдаёт. Стоит решить, что из этого показывать — по крайней мере manual_review_recommended выглядит существенным для доверия к цифре.

Связано: #2583 (M1), PR #2630, PR #2629 (фолбэк на сделки, из-за которого расширение радиуса стало заметнее).

Часть находки M1 аудита #2583, которую нельзя закрыть на фронте. Выяснено при работе над PR #2630. ## Проблема Оценщик умеет расширять радиус поиска, когда аналогов не хватает: `base_radius_m` → `fallback_radius_m` (1000 → 2000 м, `estimator.py:87-88`). Пользователю про это честно пишут в `confidence_explanation`: «расширили радиус до 2 км из-за нехватки данных». Но **фактический радиус наружу не отдаётся**: `base_radius_m` и `fallback_radius_m` — внутренние переменные `estimator.py`, в схему `AggregatedEstimateOut` не сериализуются. Поэтому круг на карте (`ParamsPanel.tsx`) рисуется по значению из выпадающего списка, который выбрал пользователь. Если он выбрал километр, а поиск фактически шёл по двум — карта показывает километр. Получается активно неверная визуальная подсказка: текст говорит одно, картинка другое. Причём картинка убедительнее текста. ## Почему это заметно именно сейчас Расширение радиуса чаще всего срабатывает там, где данных мало, — то есть в городах области. Это ровно та аудитория, ради которой шло расширение на область. По Первоуральску расхождение воспроизводится. ## Что сделать 1. Добавить в схему ответа фактический радиус поиска (например `search_radius_m`) — тот, по которому реально отбирались аналоги, с учётом расширения. 2. Фронт рисует круг по нему, а не по выбору пользователя. Если значение не пришло (старый ответ) — падать обратно на выбор пользователя, чтобы не сломать существующее. 3. Если радиус отличается от выбранного — показать это явно, а не молча: пользователь выбрал километр, а ищем по двум, и он должен понимать почему. ## Что уже сделано на фронте PR #2630 вывел `confidence_explanation` в интерфейс v2 (раньше не читался вовсе) — текстовая часть оговорки теперь видна. Осталась визуальная. ## Прочие поля из M1, которые интерфейс v2 по-прежнему не читает Отмечены при работе, но в scope PR #2630 не входили: `analog_tier`, `address_precision`, `manual_review_recommended`. Бэкенд их отдаёт. Стоит решить, что из этого показывать — по крайней мере `manual_review_recommended` выглядит существенным для доверия к цифре. Связано: #2583 (M1), PR #2630, PR #2629 (фолбэк на сделки, из-за которого расширение радиуса стало заметнее).
Collaborator

Пункты 1–3 сделаны в PR #2643 (merged + деплой верифицирован): search_radius_m в AggregatedEstimate (флип base→fallback ровно в ветках fallback_used — текст и число структурно не расходятся; ДКП/anchor не влияют; не персистится по конвенции analog_tier), круг в v2 по фактическому радиусу с фолбэком на выбор пользователя, disclosure «Аналогов в радиусе 1 км не хватило — поиск расширен до 2 км».

Live-проверка API (изнутри контейнера, ЕКБ Малышева 84): search_radius_m=1000, confidence high, 17 аналогов — поле отдаётся. Ветка расширения (2000) покрыта тестами; на проде проявится на данных-бедных адресах (Первоуральск сейчас не догеокодился — #2585, ключ Яндекс-геокодера мёртв, это отдельный блокер).

Осталось до закрытия: визуальная проверка круга+disclosure на живом расширенном примере (скриншот-конвенция) и решение по «прочим полям M1» (analog_tier / address_precision / manual_review_recommended) — их интерфейс v2 по-прежнему не читает.

Пункты 1–3 сделаны в PR #2643 (merged + деплой верифицирован): `search_radius_m` в `AggregatedEstimate` (флип base→fallback ровно в ветках `fallback_used` — текст и число структурно не расходятся; ДКП/anchor не влияют; не персистится по конвенции analog_tier), круг в v2 по фактическому радиусу с фолбэком на выбор пользователя, disclosure «Аналогов в радиусе 1 км не хватило — поиск расширен до 2 км». Live-проверка API (изнутри контейнера, ЕКБ Малышева 84): `search_radius_m=1000`, confidence high, 17 аналогов — поле отдаётся. Ветка расширения (2000) покрыта тестами; на проде проявится на данных-бедных адресах (Первоуральск сейчас не догеокодился — #2585, ключ Яндекс-геокодера мёртв, это отдельный блокер). Осталось до закрытия: визуальная проверка круга+disclosure на живом расширенном примере (скриншот-конвенция) и решение по «прочим полям M1» (`analog_tier` / `address_precision` / `manual_review_recommended`) — их интерфейс v2 по-прежнему не читает.
Collaborator

Постановка подтвердилась ЧАСТИЧНО — и не там, где написано в теле задачи.

Опровергнуто: «бэкенд не отдаёт фактический радиус» на POST — отдаёт с #2643, и каскад #oblast-F (#2823) корректно двигает search_radius_m до 3/5 км. Фронт v2 (ParamsPanel) уже рисует круг по этому полю и пишет «Аналогов в радиусе 1 км не хватило — поиск расширен до N».

Подтвердилось: на СОХРАНЁННОЙ оценке поле было пустым — колонки нет, GET /estimate/{id} его не восстанавливал, фронт падал на превью-радиус 1 км. То есть исходный дефект жил ровно там, где по оценке ходят чаще всего: shared-link, ?id=, перезагрузка. Живой прод 2026-08-11, оценка 4c20518f (Советская 51): search_radius_m: null, relaxations: ["радиус расширен до 5000 м"], 10 из 10 показанных аналогов вне круга, самый дальний 4381 м. Плюс третье число там же: confidence_explanation утверждал «расширили радиус до 2 км» — константа старого 1км→2км fallback'а, метка называла не результат (ср. #2811).

Замер: 388 оценок с расстояниями у аналогов; у 62 (16.0%) есть аналог дальше нарисованного километра. Медиана максимального расстояния 787 м, p90 1572 м, максимум 4381 м. За период после каскада (с 10.08): 6 из 19 оценок с расширением — 4× 2000 м, 2× 5000 м.

Сделано (PR #2835, merged, deploy verified): GET восстанавливает фактический радиус из persisted-подписи каскада (точное значение, идентично POST), иначе — из размаха сохранённых аналогов, но не ниже DEFAULT_RADIUS_M; объяснение называет фактический радиус; ответ несёт обе величины — requested_radius_m рядом с search_radius_m. Миграция не понадобилась.

Живая проверка на проде после деплоя (тот же адрес, изнутри контейнера):

POST → requested_radius_m=1000  search_radius_m=5000
        "...(расширили радиус до 5 км из-за нехватки данных)"
        relaxations=["радиус расширен до 5000 м"], самый дальний аналог 4381 м
GET  → search_radius_m=5000 (совпадает с POST), requested_radius_m=null

Остаётся открытым:

  1. Пользователь ЯВНО выбрал радиус → на реоткрытой оценке его не восстановить (radius_m не персистится). Расширения там нет по контракту #2044, но круг рисуется по превью 1 км, если аналоги ближе. Нужна колонка — задача для database-expert.
  2. v1 MapCard.tsx:139 рисует круг с жёстко зашитым radius: 2000 — однострочная правка на estimate.search_radius_m ?? 2000 (frontend-engineer).
  3. Визуальная проверка круга+disclosure скриншотом на живом расширенном примере (см. предыдущий комментарий) и решение по «прочим полям M1» (analog_tier/address_precision/manual_review_recommended).
Постановка подтвердилась ЧАСТИЧНО — и не там, где написано в теле задачи. **Опровергнуто:** «бэкенд не отдаёт фактический радиус» на POST — отдаёт с #2643, и каскад #oblast-F (#2823) корректно двигает `search_radius_m` до 3/5 км. Фронт v2 (`ParamsPanel`) уже рисует круг по этому полю и пишет «Аналогов в радиусе 1 км не хватило — поиск расширен до N». **Подтвердилось:** на СОХРАНЁННОЙ оценке поле было пустым — колонки нет, `GET /estimate/{id}` его не восстанавливал, фронт падал на превью-радиус 1 км. То есть исходный дефект жил ровно там, где по оценке ходят чаще всего: shared-link, `?id=`, перезагрузка. Живой прод 2026-08-11, оценка `4c20518f` (Советская 51): `search_radius_m: null`, `relaxations: ["радиус расширен до 5000 м"]`, 10 из 10 показанных аналогов вне круга, самый дальний 4381 м. Плюс третье число там же: `confidence_explanation` утверждал «расширили радиус до 2 км» — константа старого 1км→2км fallback'а, метка называла не результат (ср. #2811). **Замер:** 388 оценок с расстояниями у аналогов; у 62 (16.0%) есть аналог дальше нарисованного километра. Медиана максимального расстояния 787 м, p90 1572 м, максимум 4381 м. За период после каскада (с 10.08): 6 из 19 оценок с расширением — 4× 2000 м, 2× 5000 м. **Сделано** (PR #2835, merged, deploy verified): GET восстанавливает фактический радиус из persisted-подписи каскада (точное значение, идентично POST), иначе — из размаха сохранённых аналогов, но не ниже `DEFAULT_RADIUS_M`; объяснение называет фактический радиус; ответ несёт обе величины — `requested_radius_m` рядом с `search_radius_m`. Миграция не понадобилась. Живая проверка на проде после деплоя (тот же адрес, изнутри контейнера): ``` POST → requested_radius_m=1000 search_radius_m=5000 "...(расширили радиус до 5 км из-за нехватки данных)" relaxations=["радиус расширен до 5000 м"], самый дальний аналог 4381 м GET → search_radius_m=5000 (совпадает с POST), requested_radius_m=null ``` **Остаётся открытым:** 1. Пользователь ЯВНО выбрал радиус → на реоткрытой оценке его не восстановить (`radius_m` не персистится). Расширения там нет по контракту #2044, но круг рисуется по превью 1 км, если аналоги ближе. Нужна колонка — задача для database-expert. 2. v1 `MapCard.tsx:139` рисует круг с жёстко зашитым `radius: 2000` — однострочная правка на `estimate.search_radius_m ?? 2000` (frontend-engineer). 3. Визуальная проверка круга+disclosure скриншотом на живом расширенном примере (см. предыдущий комментарий) и решение по «прочим полям M1» (`analog_tier`/`address_precision`/`manual_review_recommended`).
Collaborator

Постановка неверна, дефект реален и хуже — проверил

Три утверждения из шапки опровергнуты: фактический радиус бэкенд отдаёт (с #2643), карта v2 рисует именно его, расширение не убрано.

Но дефект был, и в месте, куда ходят чаще всего — на сохранённой оценке. Живой прод до правки:

search_radius_m: null
relaxations:     ["радиус расширен до 5000 м"]
самый дальний из 10 показанных аналогов: 4381 м
текст:           "...(расширили радиус до 2 км из-за нехватки данных)"

Три разных числа об одном радиусе на одном экране: круг 1 км, подпись 5 км, текст 2 км. Ломается на shared-link, на ?id=, на перезагрузке — то есть каждый раз, когда оценку открывают повторно.

Второй дефект, в задаче не заявленный: текст об расширении был прибит константой старого запаса 1→2 км, а каскад уходит до 5. Метка называла вход, а не результат — тот же класс, что чинили сегодня в #2811.

Замер, оправдывающий работу

оценок с расстояниями у аналогов 388
с аналогом дальше нарисованного круга 62 (16.0%)
медиана максимального расстояния 787 м
90-й перцентиль 1572 м
хвост 4381 м — вчетверо шире круга

Проверка в живом контейнере

Функции восстановления радиуса — 6 вхождений в эстиматоре, GET их использует, requested_radius_m в схеме. Единственное оставшееся «до 2 км» — в комментарии, объясняющем прежний дефект; сам текст берёт фактический радиус.

Ответ несёт обе величины, подмены нет: на повторном открытии requested_radius_m остаётся пустым, потому что выбор пользователя не сохраняется — подставить туда умолчание значило бы выдать догадку за его выбор.

Моя ошибка проверки, для протокола

Первый мой запрос к сохранённой оценке вернул все поля пустыми, и я почти написал «правка не работает». Посмотрел код ответа — 401: защита #2213 требует заголовок, который добавляет Caddy, и мой прямой запрос изнутри сети её честно не прошёл.

То есть нули были артефактом моей неудачной авторизации. Механизм сработал ровно как задуман — от подделки заголовка мимо периметра. Ещё один случай, когда «пусто» означало «спросил не тем путём».

Что осталось за границей, названо числом

  1. Пользователь явно зафиксировал радиус → на повторном открытии не восстановить: выбор не персистится. Расширения там нет по контракту #2044, но круг рисуется по превью. Нужна колонка — это миграция, не в этот PR.
  2. Карта v1 рисует круг с зашитой константой 2000 — однострочно, но это фронт, отдельная граница.
  3. Ветка восстановления «по размаху аналогов» на живых строках почти не работает (срок жизни оценки сутки, подпись пишется с 10.08) — она для сохранённых в CRM и для случая явного радиуса.

Скриншот

Сделать не могу: вход в Меру — форма с паролем, а вводить пароль в форму мне запрещено, и обходить это я не буду. Проверено запросами и кодом в контейнере.

Для проверки глазами: оценка 2a70d8b2-c234-4733-85ff-814b4f901575 жива до 13.08 13:00 UTC — на карте должен быть круг 5 км и строка «Аналогов в радиусе 1 км не хватило — поиск расширен до 5 км».

## Постановка неверна, дефект реален и хуже — проверил Три утверждения из шапки **опровергнуты**: фактический радиус бэкенд отдаёт (с #2643), карта v2 рисует именно его, расширение не убрано. Но дефект был, и в месте, куда ходят чаще всего — на **сохранённой** оценке. Живой прод до правки: ``` search_radius_m: null relaxations: ["радиус расширен до 5000 м"] самый дальний из 10 показанных аналогов: 4381 м текст: "...(расширили радиус до 2 км из-за нехватки данных)" ``` **Три разных числа об одном радиусе на одном экране:** круг 1 км, подпись 5 км, текст 2 км. Ломается на shared-link, на `?id=`, на перезагрузке — то есть каждый раз, когда оценку открывают повторно. Второй дефект, в задаче не заявленный: текст об расширении был **прибит константой** старого запаса 1→2 км, а каскад уходит до 5. Метка называла вход, а не результат — тот же класс, что чинили сегодня в #2811. ## Замер, оправдывающий работу | | | |---|---:| | оценок с расстояниями у аналогов | 388 | | **с аналогом дальше нарисованного круга** | **62 (16.0%)** | | медиана максимального расстояния | 787 м | | 90-й перцентиль | 1572 м | | хвост | **4381 м** — вчетверо шире круга | ## Проверка в живом контейнере Функции восстановления радиуса — 6 вхождений в эстиматоре, GET их использует, `requested_radius_m` в схеме. Единственное оставшееся «до 2 км» — **в комментарии, объясняющем прежний дефект**; сам текст берёт фактический радиус. Ответ несёт **обе** величины, подмены нет: на повторном открытии `requested_radius_m` остаётся пустым, потому что выбор пользователя не сохраняется — подставить туда умолчание значило бы выдать догадку за его выбор. ## Моя ошибка проверки, для протокола Первый мой запрос к сохранённой оценке вернул все поля пустыми, и я почти написал «правка не работает». Посмотрел код ответа — **401**: защита #2213 требует заголовок, который добавляет Caddy, и мой прямой запрос изнутри сети её честно не прошёл. То есть нули были артефактом **моей** неудачной авторизации. Механизм сработал ровно как задуман — от подделки заголовка мимо периметра. Ещё один случай, когда «пусто» означало «спросил не тем путём». ## Что осталось за границей, названо числом 1. Пользователь **явно** зафиксировал радиус → на повторном открытии не восстановить: выбор не персистится. Расширения там нет по контракту #2044, но круг рисуется по превью. Нужна колонка — это миграция, не в этот PR. 2. Карта v1 рисует круг с зашитой константой 2000 — однострочно, но это фронт, отдельная граница. 3. Ветка восстановления «по размаху аналогов» на живых строках почти не работает (срок жизни оценки сутки, подпись пишется с 10.08) — она для сохранённых в CRM и для случая явного радиуса. ## Скриншот Сделать не могу: вход в Меру — форма с паролем, а вводить пароль в форму мне запрещено, и обходить это я не буду. Проверено запросами и кодом в контейнере. Для проверки глазами: оценка `2a70d8b2-c234-4733-85ff-814b4f901575` жива до 13.08 13:00 UTC — на карте должен быть круг 5 км и строка «Аналогов в радиусе 1 км не хватило — поиск расширен до 5 км».
lekss361 added the
bug
scope/backend
scope/frontend
tradein
ux
labels 2026-08-16 10:25:10 +00:00
Author
Owner

Закрываю по итогам разбора трекера 16.08.2026

Вердикт: сделано кодом.

Доказательство: PR #2643 (смержен 05.08.2026): search_radius_m отдаётся в trade_in.py:297-309, карта рисует по факту — ParamsPanel.tsx:570-580, с запасным вариантом на выбор пользователя, если поле пустое.

Независимая проверка. Вердикт проверялся отдельным проходом, задачей которого было именно опровергнуть закрытие, а не подтвердить его — опровергнуть не удалось.

Если что-то из перечисленного всё же живо — переоткройте задачу, разбор мог упустить частный случай.

## Закрываю по итогам разбора трекера 16.08.2026 **Вердикт:** сделано кодом. **Доказательство:** PR #2643 (смержен 05.08.2026): `search_radius_m` отдаётся в `trade_in.py:297-309`, карта рисует по факту — `ParamsPanel.tsx:570-580`, с запасным вариантом на выбор пользователя, если поле пустое. **Независимая проверка.** Вердикт проверялся отдельным проходом, задачей которого было именно опровергнуть закрытие, а не подтвердить его — опровергнуть не удалось. Если что-то из перечисленного всё же живо — переоткройте задачу, разбор мог упустить частный случай.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2632
No description provided.