feat(#254,#114): custom POI UI (Leaflet click-to-add) + score breakdown stacked-bar #258
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
вторичка
ИРД
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#258
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/254-custom-pois-ui"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
ScoreBreakdownStackedBar— stacked bar по категориям с переключением «По категории» / «По фактору», custom POI выделены оранжевымChanged files (frontend only)
New:
frontend/src/lib/sessionId.ts—getOrCreateSessionId()→X-Session-Idheaderfrontend/src/types/customPoi.ts—CustomPoi,CustomPoiCreate,CustomPoiUpdatefrontend/src/hooks/useCustomPois.ts—useCustomPois/useAddCustomPoi/useUpdateCustomPoi/useDeleteCustomPoifrontend/src/components/site-finder/CustomPoiLayer.tsx— Leaflet LayerGroup маркеры (green/red/gray по весу)frontend/src/components/site-finder/CustomPoiToggleButton.tsx— кнопка crosshair-режимаfrontend/src/components/site-finder/CustomPoiAddModal.tsx— форма добавленияfrontend/src/components/site-finder/CustomPoiEditModal.tsx— форма редактированияfrontend/src/components/site-finder/ScoreBreakdownStackedBar.tsx— stacked bar для #114Modified:
SiteMap.tsx—MapClickHandler, интеграция custom POI компонентовpage.tsx—useCustomPois(data?.cad_num)→ SiteMapOverviewTab.tsx— добавленScoreBreakdownStackedBarDependency
Custom POI CRUD требует backend PR —
/api/v1/custom-poisэндпоинты должны быть задеплоены. Score stacked bar (#114) работает независимо от backend.Test plan
/site-finder, submit cad66:41:0603016:194ScoreBreakdownStackedBarотображается в OverviewTabCustomPoiAddModalоткрывается с координатамиTest POI, weight=+2, submit → маркер (зелёный) появляетсяDeep review — verdict: APPROVE
Files: 11 (10 frontend new/modified + 0 backend). +1552/−2. mergeable=true.
Соответствие backend #257 (
8cc94472)customPoi.ts↔CustomPoiOut— поля совпадают:id: number(BIGSERIAL),weight: number(REAL, диапазон −5..+5),lon/lat, nullable consistency (parcel_cad,category,notes).apiFetchкорректно проставляетContent-Type: application/jsonплюсX-Session-Idчерез spread ({ ...sessionHeaders() })./api/v1/custom-pois(POST/GET/PATCH/DELETE) — пути совпадают с backend router.enabled: typeof window !== "undefined"+getOrCreateSessionId()возвращает""на сервере —useCustomPoisне падает при гидрации.React-Leaflet
MapClickHandler— sub-component внутри<MapContainer>,useMapEventsобязателен в этом контексте. OK.window.addEventListener('keydown')сaddModeRef.current— race-safe, без stale closure.CircleMarker+LayerGroup+ popup — рендерится поверхConnectionPointsLayer(последний JSX в<MapContainer>). Кликабельность маркеров не мешает CP, так как они на одном уровне z-index react-leaflet, но pendingCoords модал перехватит фокус. Сосуществование с PR #230 не сломано.TanStack Query
["custom-pois", parcelCad ?? null],["custom-pois", null]. После CRUD инвалидируется и parcel-specific, и global — корректно для случая «глобальная POI» (она видна на всех участках, поэтому глобальная инвалидация нужна).invalidateQueries(["analyze", parcelCad])— пересчитает score после CRUD. Совпадает с ключом изuseSiteAnalysis? Минорное замечание ниже.staleTime: 30_000— разумный баланс.TS strict
any/as unknown as.customPoiFactors?: string[]опционально.as constдляCATEGORY_OPTIONS— но потомuseState<string>(...). Type narrowing потерян, но не критично.Findings
MEDIUM
useCustomPois.ts:59,72,93) — мутации имеют толькоonSuccess. НетonError→ при 4xx/5xx сетевойapiFetchброситError, но user видит только «Сохранение…» которое сбрасывается черезisPending=falseи форма остаётся открытой без сообщения. Toast / inline error отсутствует. Не блокер — fail-safe (state не корраптится), но UX слабее.SiteMap.tsx:184-189) — глобальный ESC-listener срабатывает только когдаaddModeRef.current === true. Но при открытииCustomPoiAddModalстрокаsetAddMode(false)вhandleMapClickубирает флаг, и ESC перестаёт что-либо делать → закрыть модалку можно только кликом по бэкдропу или «Отмена». Аналогично дляCustomPoiEditModal. A11y слабое:role="dialog", focus trap, ESC-inside-modal — отсутствуют.ScoreBreakdownStackedBar) — пропсcustomPoiFactorsопциональный иOverviewTabего не передаёт. Все ветки оранжевого outlining (custom POI highlight) — dead-code на текущий момент. Это OK как задел, но в issue #114 эта подсветка указана как фича — желательно прокинуть массив factor-имён изdata.score_breakdown_detailed(гдеcategory === 'custom'?) или добавить TODO-комментарий, чтобы не забыть.useAddCustomPoi.ts:46) —invalidateQueries({ queryKey: ["analyze", parcelCad] }), ноuseSiteAnalysisиспользуетuseMutation(POST с body), а неuseQuery— invalidate не сработает. Backend интеграция кастомных POI в analyze response потребует либо re-mutation, либо отдельныйuseQueryдля analyze. Эффект: после добавления POI пользователь должен повторно нажать «Анализ» вручную, чтобы увидеть обновлённый score. Если такой flow задуман — OK, иначе нужен явный triggering (например,analyzeMutation.mutate()вonSuccess).LOW / nits
CustomPoiAddModal.tsx:115+CustomPoiEditModal.tsx) — категории в Add/Edit дублируются (26 строк дважды). Можно вынести вlib/customPoiCategories.ts.SiteMap.tsx:374-380) — плюрализация «точка/точки/точек» не учитывает 11..14 (требуется «точек»:11 пользовательских точек, не «точки»). Безобидно для UI-числа.useCustomPois.ts:53) — DELETE отправляется сContent-Type: application/json(черезapiFetchdefaults), хотя body нет. FastAPI это съест, но семантически излишне.frontend.md: «inline styles только для динамических значений». Здесь стили статичные. Не блокер, но style-drift с остальным кодом.Vault cross-check
domains/ui/components/Component_SiteFinder_Page.mdуже обновлён до v4.4 — описывает все 8 новых файлов, изменения SiteMap/page/OverviewTab, API contract, UX flow. Rule #6 (vault-with-code) соблюдён.Verdict — APPROVE
Критичных проблем нет, backend contract выдержан, SSR-guard есть, react-leaflet pattern корректный. Перечисленные M1–M4 / L1–L4 — желательные доработки, но не блокирующие.
Merge via
mergestyle.Smoke test после deploy
/site-finder→ submit66:41:0603016:194session_id(UUID) после первой загрузкиname=Test,weight=+3→ DevTools Network → POST/api/v1/custom-pois→ request headerX-Session-Id== localStorage value, response 201addMode=true(до клика на карте) — режим отключается.ScoreBreakdownStackedBarрендерится, toggle «По категории» / «По фактору» работает.Merged via deep-code-reviewer — verdict APPROVE. Merge commit
e3eb8c8.Post-deploy smoke (когда Caddy/Docker подтянет образ):
Recommended follow-ups (не блокеры, можно отдельным PR):
useCustomPoismutations (onError → notify)CustomPoiAddModal/EditModal+role="dialog"+ focus trap