From 7d6caed5fb9c19b9345b64d8258f19f99c660dae Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sun, 30 Aug 2026 14:54:52 +0500 Subject: [PATCH] =?UTF-8?q?=D0=9C=D0=95=D0=A0=D0=90:=20=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=20=D0=BD=D0=B0=D0=B4=20=D0=BF=D0=B5=D1=80=D0=B2?= =?UTF-8?q?=D1=8B=D0=BC=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=BE=D0=BC=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B0=D0=B5=D1=82=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D1=82=D0=B5=D0=BA=D1=81=D1=82=20=D1=80=D0=B0=D0=B7=D0=B1?= =?UTF-8?q?=D1=80=D0=BE=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Над лентой стояло «медианное расхождение с ценой ДКП — 14,5 % по 327 сделкам», а в самой ленте 5 строк из 20 расходились больше чем на 30 % (75,7 · 63,6 · 47,4 · 41,4 · 38,5). Первое, что видел посетитель страницы про точность, — крупный промах без единой цифры контекста. Промахи не прячутся: строки витрины отобраны по ПОЛНОТЕ и СВЕЖЕСТИ (_sort_key в app/tasks/landing_showcase_deals.py), отбор по величине ошибки был дефектом и снят. Лечится контекстом — под лентой печатается тот же разброс ПОКАЗАННЫХ строк, что уже стоит под таблицей сверок: медиана модуля и худшая, обе из shownSpread() (deal-view.ts), второго расчёта не заводится. Вывода вида «зато обычно точно» в подписи нет: он протух бы на первом пересчёте витрины, а два числа рядом — нет. Разметка: бегущая часть выделена в .tickerStrip — position/overflow нужны только ей, иначе абсолютный бейдж растянулся бы и на подпись. Проверка (landing-v3-render.test.tsx): те же строки с худшей и без неё дают РАЗНЫЕ числа в подписи — она сосчитана по показанному, а не вписана. Co-Authored-By: Claude Opus 5 --- .../__tests__/landing-v3-render.test.tsx | 29 +++++++++ .../_components/v3/DealsTickerV3.tsx | 64 ++++++++++++------- .../src/app/mera-public/landing-v3.module.css | 15 ++++- 3 files changed, 85 insertions(+), 23 deletions(-) diff --git a/tradein-mvp/frontend/src/app/mera-public/__tests__/landing-v3-render.test.tsx b/tradein-mvp/frontend/src/app/mera-public/__tests__/landing-v3-render.test.tsx index 2fdbbac7..4a3c7aca 100644 --- a/tradein-mvp/frontend/src/app/mera-public/__tests__/landing-v3-render.test.tsx +++ b/tradein-mvp/frontend/src/app/mera-public/__tests__/landing-v3-render.test.tsx @@ -10,6 +10,7 @@ import { render, screen } from "@testing-library/react"; import { describe, expect, it } from "vitest"; import { AccuracyV3 } from "../_components/v3/AccuracyV3"; +import { DealsTickerV3 } from "../_components/v3/DealsTickerV3"; import { CostOfErrorV3 } from "../_components/v3/CostOfErrorV3"; import { HeroV3 } from "../_components/v3/HeroV3"; import type { LandingStat, ShowcaseResponse } from "../public-api"; @@ -136,3 +137,31 @@ describe("витрина лэндинга v3 без данных", () => { expect(screen.queryByText(/По всем расчётам МЕРЫ/)).toBeNull(); }); }); + +/** + * Лента висит НАД первым экраном, и её строки отобраны по полноте и свежести, + * а не по величине ошибки, — крупный промах в ней штатен. Проверяется, что + * рядом с промахом стоит его контекст и что оба числа сосчитаны ПО ПОКАЗАННЫМ + * строкам: подпись с вписанными руками величинами тут же разошлась бы с лентой. + */ +describe("лента сделок: контекст разброса", () => { + const tickerDeal = (err_pct: number) => ({ + ...SHOWCASE.deals[0], + err_pct, + predicted_rub: Math.round(SHOWCASE.deals[0].fact_rub * (1 + err_pct / 100)), + }); + + it("подпись печатает медиану и худшую ровно тех строк, что показаны", () => { + render(); + const note = screen.getByText(/Медиана расхождения показанных строк/u); + expect(note.textContent).toContain("11,5 %"); + expect(note.textContent).toContain("75,7 %"); + }); + + it("та же лента без худшей строки печатает ДРУГИЕ числа — они не константы", () => { + render(); + const note = screen.getByText(/Медиана расхождения показанных строк/u); + expect(note.textContent).toContain("7,8 %"); + expect(note.textContent).not.toContain("75,7 %"); + }); +}); diff --git a/tradein-mvp/frontend/src/app/mera-public/_components/v3/DealsTickerV3.tsx b/tradein-mvp/frontend/src/app/mera-public/_components/v3/DealsTickerV3.tsx index 7f56ef50..4e0a957b 100644 --- a/tradein-mvp/frontend/src/app/mera-public/_components/v3/DealsTickerV3.tsx +++ b/tradein-mvp/frontend/src/app/mera-public/_components/v3/DealsTickerV3.tsx @@ -12,6 +12,15 @@ * не из чего. Адреса тоже нет: улица и дом известны у 2.7% сделок, поэтому * объект описан тем, что есть — комнаты, площадь, район, этаж. * + * ПОД лентой — подпись с разбросом ПОКАЗАННЫХ строк (`shownSpread`, та же + * функция, что и под таблицей сверок): медиана модуля и худшая. Без неё первое, + * что видит посетитель страницы про точность, — промах в семьдесят процентов + * без единой цифры контекста, опровергающий её же заголовок. Прятать промахи + * нельзя: строки отобраны по ПОЛНОТЕ и СВЕЖЕСТИ (`_sort_key` в + * app/tasks/landing_showcase_deals.py), поэтому лечится контекстом, а не + * отбором. Вывода («зато обычно точно») в подписи нет намеренно: он протух бы + * на первом же пересчёте витрины, а два числа рядом не протухают. + * * Список дублируется дважды подряд — стандартный приём бесшовного CSS-marquee * (анимация уводит ровно на −50%). * @@ -24,34 +33,45 @@ import type { ShowcaseDeal } from "../../public-api"; import styles from "../../landing-v3.module.css"; -import { dealTitle, errPct, rub } from "./deal-view"; +import { absPct, dealTitle, errPct, rub, shownSpread } from "./deal-view"; export function DealsTickerV3({ deals }: { deals: readonly ShowcaseDeal[] }) { const items = [...deals, ...deals]; + // Разброс считается по `deals`, а не по `items`: `items` — та же двадцатка, + // склеенная сама с собой ради бесшовного marquee. Медиана и худшая на ней + // совпали бы, а `n` — нет, и подпись начала бы врать при первой же правке. + const spread = shownSpread(deals); return (
-
-
- ); } diff --git a/tradein-mvp/frontend/src/app/mera-public/landing-v3.module.css b/tradein-mvp/frontend/src/app/mera-public/landing-v3.module.css index c3705e01..5baa8c3f 100644 --- a/tradein-mvp/frontend/src/app/mera-public/landing-v3.module.css +++ b/tradein-mvp/frontend/src/app/mera-public/landing-v3.module.css @@ -1397,9 +1397,22 @@ /* --- Лента «ПРОГНОЗ → ФАКТ» ---------------------------------------------- */ .tickerRoot { + background: var(--b2c-surface-dark); +} +/* Бегущая часть отделена от подписи: `overflow: hidden` и точка отсчёта для + абсолютного бейджа нужны ТОЛЬКО ей, иначе бейдж растянулся бы на всю высоту + вместе с подписью. */ +.tickerStrip { position: relative; overflow: hidden; - background: var(--b2c-surface-dark); +} +.tickerNote { + margin: 0; + padding: 0 clamp(20px, 5vw, 56px) 10px; + font-family: var(--b2c-font-mono); + font-size: 10px; + letter-spacing: 0.06em; + color: var(--b2c-muted); } .tickerBadge { position: absolute;