Витрина лендинга: пустой пересчёт больше не считается успешным, а итог игры «Проверьте себя» говорит, что квартиры отобраны #3550

Merged
bot-backend merged 2 commits from fix/landing-showcase-and-game into main 2026-09-17 09:21:09 +00:00
Collaborator

Две правки в витрине публичного лендинга МЕРЫ, по коммиту на issue.

#3511: пустой прогон витрины сделок больше не считается успешным

Что было. _job_landing_showcase_deals (app/services/product_handlers.py) всегда ставил прогону done. У счётчиков витрины (considered/eligible/written) нет ни одного ключа из _RESULT_COUNTER_KEYS кита, поэтому run_brought_data возвращает status == "done", то есть судит только по статусу. Прогон с written = 0 (задача уже сделала DELETE + пустой INSERT и закоммитила) сбрасывал часы свежести так же, как удачный. Блок на странице пропадал, а сводка просроченных молчала.

Почему это важно сейчас. С #3469 задача стоит в суточном расписании: прод 17.09, next_run_at 2026-09-18 06:22 UTC. Последние 5 прогонов (13–17.09) все done, written = 20, eligible 158–160. Пока всё в порядке, но любая правка оценщика, которая сдвинет расхождения за полосу, опустошит витрину без тревоги.

Что сделано. Если written равен 0, прогон помечается mark_failed с причиной «витрина пуста: прогон не записал ни одной строки (written=0)». В лог пишется logger.error с considered/eligible. Непустой прогон, как и раньше, завершается mark_done.

Тест по значению (tests/test_3469_showcase_schedule.py). Проверка идёт тем же путём, что и сводка: обработчик ставит статус, затем freshness_rows и stale_sources. ctx.runs здесь не мок с проверкой вызова: он записывает финал прогона строкой _STALE_SOURCES_SQL.

  • последний непустой прогон был 4 суток назад, вчерашний пустой → landing_showcase_deals в списке просроченных;
  • тот же сценарий, но вчерашний прогон непустой → список пуст (контроль, зелёный до и после правки).

Чего здесь нет. Витрина по-прежнему пустеет, и на странице вместе с таблицей пропадает дата пересчёта. В issue вариант «показывать прежний набор» назван решением продукта, в приёмку он не входит. После правки пустоту заметит сводка через 3 такта от последнего непустого прогона.

#2903: итог игры «Проверьте себя» говорит, что квартиры отобраны

Что было. Всё, что просили в issue, уже сделано раньше (#3229, b72dbc5d): раунды из реальных ДКП, прогноз через спайн бэктеста, заранее посчитанный набор в landing_showcase_deals, без адреса. Но с 24679432 (12.09) витрина, а значит и раунды игры (pickVariedDeals(showcase.deals)), содержит только сделки с расхождением от −5 до +20 %. На проде 17.09 err_pct у 20 строк лежит от −4,55 до 17,93, медиана |err| 6,5 %. Лента и таблица «Точность» полосу называют. Игра не называла: итоговая плитка «ошибка МЕРЫ на этих же квартирах» выходила без оговорки и читалась как точность расчёта.

Что сделано.

  • GuessGameV3 получил обязательный проп bandSelected. Под плитками итога теперь две фразы. Первая, только если bandSelected: «Квартиры для игры взяты из отобранной полосы, где МЕРА разошлась с ценой ДКП от −5 % до +20 %: это не вся сверка». Вторая, всегда: «Медиана расхождения МЕРЫ по всей сверке — X (N сделок)».
  • Числа в текст не вписаны руками: границы берутся из BAND_MIN_PCT/BAND_MAX_PCT, медиана — из BACKTEST.priceError (landing-facts), как в подписи таблицы. Гейт чисел зелёный.
  • page.tsx передаёт bandSelected={allWithinBand(deals)}. Та же самопроверка стоит у ленты и таблицы: если на странице лежат строки старого прогона (вне полосы), про полосу ничего не утверждается.
  • В CSS добавлен класс .gameFinalNote (composes accFootnote).

Тест по значению (__tests__/landing-v3-render.test.tsx). Игра проходится кликами до итога.

  • bandSelected → в итоге есть «отобранной полосы … от -5 % до +20 % … не вся сверка», медиана и размер выборки из BACKTEST.priceError;
  • bandSelected={false} → про полосу ни слова, медиана сверки на месте.

Прогоны

  • МЕРА backend, весь сьют: DATABASE_URL=… uv run python -m pytest tests/ -q -p no:cacheprovider → 6227 passed, 42 skipped, rc=0 (после rebase на origin/main 34642e1d; новых коммитов в main не было)
  • tests/test_3469_showcase_schedule.py → 12 passed, 1 skipped (живой Postgres-тест само-скипается без базы), rc=0
  • uv run ruff check app tests → All checks passed, rc=0; ruff format --check на изменённых файлах → already formatted
  • МЕРА frontend: npx tsc --noEmit rc=0; npx vitest run → 32 files, 297 passed, rc=0; eslint по изменённым файлам rc=0; npm run check:mera-public-isolation rc=0

Фальсификация

Исходник копировался в scratchpad, правился руками, после прогона восстанавливался, diff -q чистый.

#3511, условие if not counters.get("written") заменено на if False (старое поведение, всегда mark_done):

E       AssertionError: пустой прогон витрины засчитан свежестью: статусы ['done', 'done']
E       assert [] == ['landing_showcase_deals']
1 failed, 11 passed, 1 skipped

#2903, (а) фраза про полосу выключена (false ? …):

FAIL  … > строки витрины в полосе → итог говорит, что квартиры из отобранной полосы, и печатает медиану сверки
AssertionError: итог сравнивает игрока с МЕРОЙ на отобранных по её ошибке квартирах и молчит об отборе: expected 'Медиана расхождения МЕРЫ по всей свер…' to match /отобранной полосы.*от -5 % до \+20 %…/u
Tests  1 failed | 16 passed (17)

(б) самопроверка выключена (true ? …):

FAIL  … > строки витрины вне полосы → про полосу молчит, медиана сверки остаётся
AssertionError: expected 'Квартиры для игры взяты из отобранной…' not to match /полос/u
Tests  1 failed | 16 passed (17)

Деплой

Пересоздаются tradein-backend/tradein-scraper (обработчик) и tradein-frontend. Миграций нет. Перед деплоем проверить SELECT source FROM scrape_runs WHERE status='running': 17.09 в 07:2x UTC там шли domclick_city_sweep_moskva, avito_city_sweep, avito_city_sweep_verkhnyaya_pyshma.

Приёмка на проде (после деплоя)

  • #3511, проверить по коду в контейнере (не по статусу деплоя): docker exec tradein-scraper grep -n "written=0" /app/app/services/product_handlers.py находит новую ветку. Прогон 18.09 около 06:22 UTC с непустой витриной → scrape_runs.status='done', written=20. Пустой прогон, если случится → status='failed', error начинается с «витрина пуста», а сводка назовёт источник через 3 суток от последнего непустого. Проверить до 19.09.
  • #2903: на публичном лендинге пройти игру до итога (3 раунда). Под плитками должны стоять «отобранной полосы … от -5 % до +20 % … не вся сверка» и медиана по всей сверке. Проверить скриншотом после выката фронта, до 19.09.

Closes #3511
Closes #2903

🤖 Generated with Claude Code

Две правки в витрине публичного лендинга МЕРЫ, по коммиту на issue. ## #3511: пустой прогон витрины сделок больше не считается успешным **Что было.** `_job_landing_showcase_deals` (`app/services/product_handlers.py`) всегда ставил прогону `done`. У счётчиков витрины (`considered`/`eligible`/`written`) нет ни одного ключа из `_RESULT_COUNTER_KEYS` кита, поэтому `run_brought_data` возвращает `status == "done"`, то есть судит только по статусу. Прогон с `written = 0` (задача уже сделала `DELETE` + пустой `INSERT` и закоммитила) сбрасывал часы свежести так же, как удачный. Блок на странице пропадал, а сводка просроченных молчала. **Почему это важно сейчас.** С #3469 задача стоит в суточном расписании: прод 17.09, `next_run_at` 2026-09-18 06:22 UTC. Последние 5 прогонов (13–17.09) все `done`, `written = 20`, `eligible` 158–160. Пока всё в порядке, но любая правка оценщика, которая сдвинет расхождения за полосу, опустошит витрину без тревоги. **Что сделано.** Если `written` равен 0, прогон помечается `mark_failed` с причиной «витрина пуста: прогон не записал ни одной строки (written=0)». В лог пишется `logger.error` с `considered`/`eligible`. Непустой прогон, как и раньше, завершается `mark_done`. **Тест по значению** (`tests/test_3469_showcase_schedule.py`). Проверка идёт тем же путём, что и сводка: обработчик ставит статус, затем `freshness_rows` и `stale_sources`. `ctx.runs` здесь не мок с проверкой вызова: он записывает финал прогона строкой `_STALE_SOURCES_SQL`. - последний непустой прогон был 4 суток назад, вчерашний пустой → `landing_showcase_deals` в списке просроченных; - тот же сценарий, но вчерашний прогон непустой → список пуст (контроль, зелёный до и после правки). **Чего здесь нет.** Витрина по-прежнему пустеет, и на странице вместе с таблицей пропадает дата пересчёта. В issue вариант «показывать прежний набор» назван решением продукта, в приёмку он не входит. После правки пустоту заметит сводка через 3 такта от последнего непустого прогона. ## #2903: итог игры «Проверьте себя» говорит, что квартиры отобраны **Что было.** Всё, что просили в issue, уже сделано раньше (#3229, b72dbc5d): раунды из реальных ДКП, прогноз через спайн бэктеста, заранее посчитанный набор в `landing_showcase_deals`, без адреса. Но с 24679432 (12.09) витрина, а значит и раунды игры (`pickVariedDeals(showcase.deals)`), содержит только сделки с расхождением от −5 до +20 %. На проде 17.09 `err_pct` у 20 строк лежит от −4,55 до 17,93, медиана |err| 6,5 %. Лента и таблица «Точность» полосу называют. Игра не называла: итоговая плитка «ошибка МЕРЫ на этих же квартирах» выходила без оговорки и читалась как точность расчёта. **Что сделано.** - `GuessGameV3` получил обязательный проп `bandSelected`. Под плитками итога теперь две фразы. Первая, только если `bandSelected`: «Квартиры для игры взяты из отобранной полосы, где МЕРА разошлась с ценой ДКП от −5 % до +20 %: это не вся сверка». Вторая, всегда: «Медиана расхождения МЕРЫ по всей сверке — X (N сделок)». - Числа в текст не вписаны руками: границы берутся из `BAND_MIN_PCT`/`BAND_MAX_PCT`, медиана — из `BACKTEST.priceError` (`landing-facts`), как в подписи таблицы. Гейт чисел зелёный. - `page.tsx` передаёт `bandSelected={allWithinBand(deals)}`. Та же самопроверка стоит у ленты и таблицы: если на странице лежат строки старого прогона (вне полосы), про полосу ничего не утверждается. - В CSS добавлен класс `.gameFinalNote` (composes `accFootnote`). **Тест по значению** (`__tests__/landing-v3-render.test.tsx`). Игра проходится кликами до итога. - `bandSelected` → в итоге есть «отобранной полосы … от -5 % до +20 % … не вся сверка», медиана и размер выборки из `BACKTEST.priceError`; - `bandSelected={false}` → про полосу ни слова, медиана сверки на месте. ## Прогоны - МЕРА backend, весь сьют: `DATABASE_URL=… uv run python -m pytest tests/ -q -p no:cacheprovider` → 6227 passed, 42 skipped, rc=0 (после rebase на origin/main 34642e1d; новых коммитов в main не было) - `tests/test_3469_showcase_schedule.py` → 12 passed, 1 skipped (живой Postgres-тест само-скипается без базы), rc=0 - `uv run ruff check app tests` → All checks passed, rc=0; `ruff format --check` на изменённых файлах → already formatted - МЕРА frontend: `npx tsc --noEmit` rc=0; `npx vitest run` → 32 files, 297 passed, rc=0; `eslint` по изменённым файлам rc=0; `npm run check:mera-public-isolation` rc=0 ## Фальсификация Исходник копировался в scratchpad, правился руками, после прогона восстанавливался, `diff -q` чистый. **#3511**, условие `if not counters.get("written")` заменено на `if False` (старое поведение, всегда `mark_done`): ``` E AssertionError: пустой прогон витрины засчитан свежестью: статусы ['done', 'done'] E assert [] == ['landing_showcase_deals'] 1 failed, 11 passed, 1 skipped ``` **#2903**, (а) фраза про полосу выключена (`false ? …`): ``` FAIL … > строки витрины в полосе → итог говорит, что квартиры из отобранной полосы, и печатает медиану сверки AssertionError: итог сравнивает игрока с МЕРОЙ на отобранных по её ошибке квартирах и молчит об отборе: expected 'Медиана расхождения МЕРЫ по всей свер…' to match /отобранной полосы.*от -5 % до \+20 %…/u Tests 1 failed | 16 passed (17) ``` (б) самопроверка выключена (`true ? …`): ``` FAIL … > строки витрины вне полосы → про полосу молчит, медиана сверки остаётся AssertionError: expected 'Квартиры для игры взяты из отобранной…' not to match /полос/u Tests 1 failed | 16 passed (17) ``` ## Деплой Пересоздаются `tradein-backend`/`tradein-scraper` (обработчик) и `tradein-frontend`. Миграций нет. Перед деплоем проверить `SELECT source FROM scrape_runs WHERE status='running'`: 17.09 в 07:2x UTC там шли `domclick_city_sweep_moskva`, `avito_city_sweep`, `avito_city_sweep_verkhnyaya_pyshma`. ## Приёмка на проде (после деплоя) - **#3511**, проверить по коду в контейнере (не по статусу деплоя): `docker exec tradein-scraper grep -n "written=0" /app/app/services/product_handlers.py` находит новую ветку. Прогон 18.09 около 06:22 UTC с непустой витриной → `scrape_runs.status='done'`, `written=20`. Пустой прогон, если случится → `status='failed'`, `error` начинается с «витрина пуста», а сводка назовёт источник через 3 суток от последнего непустого. Проверить до 19.09. - **#2903**: на публичном лендинге пройти игру до итога (3 раунда). Под плитками должны стоять «отобранной полосы … от -5 % до +20 % … не вся сверка» и медиана по всей сверке. Проверить скриншотом после выката фронта, до 19.09. Closes #3511 Closes #2903 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bot-backend added 2 commits 2026-09-17 07:30:18 +00:00
Обработчик landing_showcase_deals безусловно ставил прогону done. У счётчиков
витрины (considered/eligible/written) нет результатного ключа кита, поэтому
сводка просроченных судит её только по статусу: прогон с written=0 обнулял
часы свежести так же, как удачный, а страница тем временем теряла таблицу.

Теперь written=0 — mark_failed с причиной и logger.error. Тест идёт путём
сводки: обработчик -> freshness_rows -> stale_sources; пустой последний
прогон при старом непустом даёт витрину в тревоге, непустой — нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(mera/лендинг): итог игры «Проверьте себя» говорит, что квартиры взяты из отобранной полосы (#2903)
All checks were successful
CI Trade-In / frontend-checks (pull_request) Successful in 2m32s
CI Trade-In / backend-tests (pull_request) Successful in 7m1s
CI Trade-In / changes (pull_request) Successful in 14s
CI / changes (pull_request) Successful in 17s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
4dffb2d1fb
Раунды игры — строки витрины, а витрина с 12.09 отобрана по ошибке МЕРЫ
(полоса -5..+20 %). Плитка «ошибка МЕРЫ на этих же квартирах» выходила без
оговорки и читалась как точность расчёта: на проде 17.09 медиана |ошибки|
двадцати строк 6,5 %. Лента и таблица «Точность» полосу называли, игра — нет.

Теперь итог печатает медиану по всей сверке (landing-facts) и, если все
строки витрины лежат в полосе (allWithinBand, как у ленты и таблицы),
говорит, что квартиры из отобранной полосы, а не из всей сверки.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bot-backend merged commit 57b03092ea into main 2026-09-17 09:21:09 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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#3550
No description provided.