feat(site-finder): VelocityBlock в MarketTab (#34 sub-PR 2/2 FINAL) #147

Merged
lekss361 merged 2 commits from feat/34-velocity-block-frontend into main 2026-05-14 22:44:16 +00:00
lekss361 commented 2026-05-14 22:31:29 +00:00 (Migrated from github.com)

Summary

#34 D2 Velocity-score — sub-PR 2/2 FINAL. Backend service merged #146; этот PR — UI render.

Visual elements

  • Score gauge (горизонт. bar, цвета per 0.66/0.33 thresholds)
  • Period + ЖК count meta
  • Confidence chip
  • Top 5 competitors таблица

TS types

Velocity, VelocityCompetitor, VelocityPeriod. ParcelAnalysis.velocity? optional.

Integration

MarketTab.tsx между market_trend и competitors. Guard 'velocity' in data.

Checks

  • tsc: 0 errors
  • ESLint: 0 warnings

Vault

code/modules/Module_Velocity_Block.md NEW

Closes #34
Refs: #144 (backend), audit #127

## Summary **#34 D2 Velocity-score — sub-PR 2/2 FINAL**. Backend service merged #146; этот PR — UI render. ## Visual elements - Score gauge (горизонт. bar, цвета per 0.66/0.33 thresholds) - Period + ЖК count meta - Confidence chip - Top 5 competitors таблица ## TS types Velocity, VelocityCompetitor, VelocityPeriod. ParcelAnalysis.velocity? optional. ## Integration MarketTab.tsx между market_trend и competitors. Guard 'velocity' in data. ## Checks - tsc: 0 errors - ESLint: 0 warnings ## Vault code/modules/Module_Velocity_Block.md NEW Closes #34 Refs: #144 (backend), audit #127
lekss361 commented 2026-05-14 22:34:54 +00:00 (Migrated from github.com)

Bot review (SHA 33b6148) — НЕ мержу, нашёл cross-stack contract mismatch

CI зелёный, но tsc проверяет только consistency внутри frontend, не контракт с backend. Я сравнил TS типы с тем что реально отдаёт velocity.py (merged в PR #146) — два поля разъезжаются.

Bug: TS types vs backend sample_competitors

В velocity.py:compute_velocity() строки строятся так (PR #146):

sample = sorted(
    [
        {
            "obj_id": oid,
            **competitor_meta[oid],   # name, dev_name, obj_class, district_name, distance_m
            "total_sqm_period": round(sales_by_id.get(oid, 0.0), 0),
        }
        for oid in obj_ids ...
    ], ...
)

То есть payload содержит: obj_id, name, dev_name, obj_class, district_name, distance_m, total_sqm_period.

TS VelocityCompetitor в этом PR:

export interface VelocityCompetitor {
  obj_id: number;
  name: string | null;
  class: string | null;       // ← backend отдаёт obj_class
  distance_m: number;
  total_sqm: number;          // ← backend отдаёт total_sqm_period
}

И VelocityBlock.tsx обращается к этим именам:

  • Строка ~155: {c.class && <span>{c.class}</span>}undefined && ... всегда false → класс ЖК никогда не показывается
  • Строка ~170: {Math.round(c.total_sqm).toLocaleString("ru")}Math.round(undefined) = NaNNaN.toLocaleString("ru") = «не число» в колонке «Продано м²» для всех 5 строк top-sellers таблицы

Как поправить

Cleanest вариант — frontend conforms к backend (backend уже задеплоен через PR #146):

1. TS type:

 export interface VelocityCompetitor {
   obj_id: number;
   name: string | null;
-  class: string | null;
-  distance_m: number;
-  total_sqm: number;
+  obj_class: string | null;
+  distance_m: number;
+  total_sqm_period: number;
+  dev_name?: string | null;       // bonus поле, есть в backend
+  district_name?: string | null;  // bonus поле, есть в backend
 }

2. VelocityBlock.tsx:

-  {c.class && (
+  {c.obj_class && (
     <span style={{...}}>
-      {c.class}
+      {c.obj_class}
     </span>
   )}
   ...
-  {Math.round(c.total_sqm).toLocaleString("ru")}
+  {Math.round(c.total_sqm_period).toLocaleString("ru")}

После push'а нового SHA — на следующем тике cron перепроверю и смерджу если корректно.

Альтернатива (если предпочтительнее backend-side rename)

Можно переименовать в velocity.py:

  • obj_classclass (но class — reserved word в Python, не критично здесь т.к. dict key, не attribute, но в JS/TS это reserved word и valid identifier только в object literal, не как class field name TS auto-completes... — лучше избегать)
  • total_sqm_periodtotal_sqm

Не рекомендую — class в TS object фактически работает, но семантически зарезервирован у читателя в голове.

Bonus замечания (не блокеры)

  1. MarketTab guard "velocity" in data — корректно: проверяет наличие ключа, не truthiness. Если backend пришлёт velocity: null (sub-PR #146 returns velocity_data: dict[str, Any] | None = None), "velocity" in data = true, и VelocityBlock получит null. VelocityBlock корректно обрабатывает: if (!velocity) return <EmptyState>. ✓

  2. Цветовая schema gauge (0.66/0.33 thresholds) — visual decision, не блокер.

  3. Confidence chip + Russian labels (Высокая / Средняя / Низкая) — match backend literal types ✓

## Bot review (SHA `33b6148`) — НЕ мержу, нашёл cross-stack contract mismatch CI зелёный, но tsc проверяет только consistency **внутри** frontend, не контракт с backend. Я сравнил TS типы с тем что реально отдаёт `velocity.py` (merged в PR #146) — два поля разъезжаются. ### Bug: TS types vs backend `sample_competitors` В `velocity.py:compute_velocity()` строки строятся так (PR #146): ```python sample = sorted( [ { "obj_id": oid, **competitor_meta[oid], # name, dev_name, obj_class, district_name, distance_m "total_sqm_period": round(sales_by_id.get(oid, 0.0), 0), } for oid in obj_ids ... ], ... ) ``` То есть payload содержит: `obj_id`, `name`, `dev_name`, `obj_class`, `district_name`, `distance_m`, **`total_sqm_period`**. TS `VelocityCompetitor` в этом PR: ```ts export interface VelocityCompetitor { obj_id: number; name: string | null; class: string | null; // ← backend отдаёт obj_class distance_m: number; total_sqm: number; // ← backend отдаёт total_sqm_period } ``` И `VelocityBlock.tsx` обращается к этим именам: - Строка ~155: `{c.class && <span>{c.class}</span>}` — `undefined && ...` всегда false → класс ЖК **никогда не показывается** - Строка ~170: `{Math.round(c.total_sqm).toLocaleString("ru")}` — `Math.round(undefined)` = `NaN` → `NaN.toLocaleString("ru")` = **«не число»** в колонке «Продано м²» для всех 5 строк top-sellers таблицы ### Как поправить Cleanest вариант — frontend conforms к backend (backend уже задеплоен через PR #146): **1. TS type:** ```diff export interface VelocityCompetitor { obj_id: number; name: string | null; - class: string | null; - distance_m: number; - total_sqm: number; + obj_class: string | null; + distance_m: number; + total_sqm_period: number; + dev_name?: string | null; // bonus поле, есть в backend + district_name?: string | null; // bonus поле, есть в backend } ``` **2. VelocityBlock.tsx:** ```diff - {c.class && ( + {c.obj_class && ( <span style={{...}}> - {c.class} + {c.obj_class} </span> )} ... - {Math.round(c.total_sqm).toLocaleString("ru")} + {Math.round(c.total_sqm_period).toLocaleString("ru")} ``` После push'а нового SHA — на следующем тике cron перепроверю и смерджу если корректно. ### Альтернатива (если предпочтительнее backend-side rename) Можно переименовать в `velocity.py`: - `obj_class` → `class` (но `class` — reserved word в Python, не критично здесь т.к. dict key, не attribute, но в JS/TS это **reserved word и valid identifier** только в object literal, не как class field name TS auto-completes... — лучше избегать) - `total_sqm_period` → `total_sqm` Не рекомендую — `class` в TS object фактически работает, но семантически зарезервирован у читателя в голове. ### Bonus замечания (не блокеры) 1. **MarketTab guard `"velocity" in data`** — корректно: проверяет наличие ключа, не truthiness. Если backend пришлёт `velocity: null` (sub-PR #146 returns `velocity_data: dict[str, Any] | None = None`), `"velocity" in data` = true, и VelocityBlock получит `null`. VelocityBlock корректно обрабатывает: `if (!velocity) return <EmptyState>`. ✓ 2. **Цветовая schema gauge** (0.66/0.33 thresholds) — visual decision, не блокер. 3. **Confidence chip** + Russian labels (`Высокая` / `Средняя` / `Низкая`) — match backend literal types ✓
lekss361 commented 2026-05-14 22:43:41 +00:00 (Migrated from github.com)

Final review (SHA c6cabc1, CI ) — closes #34

Field naming bug исправлен корректно:

TS VelocityCompetitor: obj_class + total_sqm_period + bonus optional dev_name? / district_name? — точное соответствие backend payload
VelocityBlock.tsx: c.obj_class в conditional render, c.total_sqm_period в м²-колонке
Backend SUCCESS + frontend SUCCESS — оба чека прошли

approve merge

## Final review (SHA `c6cabc1`, CI ✅) — closes #34 Field naming bug исправлен корректно: ✅ TS `VelocityCompetitor`: `obj_class` + `total_sqm_period` + bonus optional `dev_name?` / `district_name?` — точное соответствие backend payload ✅ VelocityBlock.tsx: `c.obj_class` в conditional render, `c.total_sqm_period` в м²-колонке ✅ Backend SUCCESS + frontend SUCCESS — оба чека прошли approve merge
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#147
No description provided.