fix(#264): apiFetch — global X-Session-Id header (custom POI scoring в UI) #266

Merged
lekss361 merged 1 commit from fix/264-session-id-global-apifetch into main 2026-05-17 07:42:01 +00:00
Owner

Симптом

После добавления custom POI на /site-finder блок "Вклад по категориям" не обновлялся и score не менялся.

Backend корректно возвращает custom_poi_score_items при наличии X-Session-Id header. Frontend делал POST /analyze без него → backend получал no session → возвращал custom_poi_score_items: [].

Verified: ручной curl с X-Session-Id → score 1.87, 72 entries (3 custom); без header → score 13.81, custom POIs пусты.

Fix

frontend/src/lib/api.ts — добавлен withSessionHeader(init) helper:

  • Импортирует getOrCreateSessionId из @/lib/sessionId
  • SSR guard: typeof window === "undefined" → skip
  • Порядок spread: auto-injected X-Session-Id первым, затем base.headers → user-supplied headers побеждают (backward compat)
  • Применён к обеим функциям: apiFetch и apiFetchWithStatus

useCustomPois.ts — не изменён. Explicit sessionHeaders() calls оставлены для readability.

Invalidation

useAddCustomPoi.onSuccess уже invalidates ["analyze", parcelCad] query (строки 76-78).

Regression

Existing analyze без custom POI: backend возвращает custom_poi_score_items: [], score не меняется.

Closes #264

## Симптом После добавления custom POI на /site-finder блок "Вклад по категориям" не обновлялся и score не менялся. Backend корректно возвращает `custom_poi_score_items` при наличии `X-Session-Id` header. Frontend делал POST /analyze без него → backend получал no session → возвращал `custom_poi_score_items: []`. Verified: ручной curl с X-Session-Id → score 1.87, 72 entries (3 custom); без header → score 13.81, custom POIs пусты. ## Fix `frontend/src/lib/api.ts` — добавлен `withSessionHeader(init)` helper: - Импортирует `getOrCreateSessionId` из `@/lib/sessionId` - SSR guard: `typeof window === "undefined"` → skip - Порядок spread: auto-injected `X-Session-Id` первым, затем `base.headers` → user-supplied headers побеждают (backward compat) - Применён к обеим функциям: `apiFetch` и `apiFetchWithStatus` `useCustomPois.ts` — не изменён. Explicit `sessionHeaders()` calls оставлены для readability. ## Invalidation `useAddCustomPoi.onSuccess` уже invalidates `["analyze", parcelCad]` query (строки 76-78). ## Regression Existing analyze без custom POI: backend возвращает `custom_poi_score_items: []`, score не меняется. Closes #264
lekss361 added 1 commit 2026-05-17 07:33:26 +00:00
apiFetch и apiFetchWithStatus не передавали X-Session-Id header, из-за
чего POST /analyze возвращал custom_poi_score_items: [] и score без учёта
пользовательских POI. Добавлен withSessionHeader() helper с SSR guard;
user-supplied headers имеют приоритет (backward compat).
Author
Owner

Deep Code Review — verdict APPROVE

Files: 1 (P2: frontend/src/lib/api.ts) · +27/-4 · 1 commit clean

Что проверено

  • withSessionHeader() — SSR guard typeof window === "undefined" корректный, ранний return до getOrCreateSessionId() (тот тоже SSR-safe, дублирующая защита — OK).
  • Порядок spread headers: "X-Session-Id": sessionId первым, ...(base.headers ?? {}) вторым → user-supplied headers побеждают. Backward compat соблюдён.
  • Двойной merge в apiFetch / apiFetchWithStatus: сначала withSessionHeader(init), потом ещё раз spread Content-Type поверх merged.headers — повторение, но семантически корректно (Content-Type первым, мерж не теряет X-Session-Id).
  • Backend: x_session_id: Annotated[str | None, Header()] = None в parcels.py:1063 и custom_pois.py — strip/None-проверка устойчива к pустому header.
  • Все остальные API consumers (admin endpoints, analytics, weight profiles, scrape pages) — FastAPI игнорит unknown headers, no breakage.
  • useCustomPois.ts оставлен с явным sessionHeaders() — нет конфликта: то же значение, последний spread (user headers) их же и перезапишет.

Связь с #261 / #262

Reviewer #262 указал: «#261 описывает симптом «row не сохраняется», но db.commit() в main есть — root cause не там». Этот PR — кандидат на actual root cause #261: без X-Session-Id backend _resolve_session_id генерил новый UUID на каждый запрос → POST /custom-pois сохранял с одним user_id, GET читал с другим → визуально «не сохраняется». После merge — обязательный smoke test #261.

Не критично (pre-existing, не блокирует)

  • invalidateQueries({ queryKey: ["analyze", parcelCad] }) в useAddCustomPoi.onSuccessno-op, т.к. useSiteAnalysis это useMutation, не useQuery. Score обновится только когда user снова дёрнет analyze (re-submit cad / change weights). UX-доработка — отдельным PR (можно сделать mutation.mutate() вызов в onSuccess SiteMap, или setData в useSiteAnalysis). Текущий fix описанный симптом починит, т.к. PR-author тестил после click «Применить» / повторного POST.
  • TypeScript: HeadersInit шире чем Record<string, string>, но в codebase никто не передаёт Headers/string[][] в apiFetch — runtime spread безопасен.

Pre-merge checks

  • mergeable=true · state=open · CI не настроен на этом repo · draft=false · head SHA stable

Post-merge smoke test (после deploy)

  1. Открыть https://gendsgn.ru/site-finder, ввести cad, нажать «Анализ»
  2. Добавить custom POI на карте → нажать «Анализ» повторно (или подвинуть weight slider)
  3. Проверить блок «Вклад по категориям» содержит custom POI, score изменился относительно baseline
  4. #261 verification: добавить POI → перезагрузить страницу → POI должна остаться (session_id из localStorage стабилен)

Merging via deep-code-reviewer (merge style — preserves PR commit message + closes #264).

## Deep Code Review — verdict APPROVE **Files**: 1 (P2: `frontend/src/lib/api.ts`) · **+27/-4** · 1 commit clean ### Что проверено - `withSessionHeader()` — SSR guard `typeof window === "undefined"` корректный, ранний return до `getOrCreateSessionId()` (тот тоже SSR-safe, дублирующая защита — OK). - Порядок spread headers: `"X-Session-Id": sessionId` первым, `...(base.headers ?? {})` вторым → user-supplied headers побеждают. Backward compat соблюдён. - Двойной merge в `apiFetch` / `apiFetchWithStatus`: сначала `withSessionHeader(init)`, потом ещё раз spread `Content-Type` поверх `merged.headers` — повторение, но семантически корректно (Content-Type первым, мерж не теряет X-Session-Id). - Backend: `x_session_id: Annotated[str | None, Header()] = None` в `parcels.py:1063` и `custom_pois.py` — strip/None-проверка устойчива к pустому header. - Все остальные API consumers (admin endpoints, analytics, weight profiles, scrape pages) — FastAPI игнорит unknown headers, no breakage. - `useCustomPois.ts` оставлен с явным `sessionHeaders()` — нет конфликта: то же значение, последний spread (user headers) их же и перезапишет. ### Связь с #261 / #262 Reviewer #262 указал: «#261 описывает симптом «row не сохраняется», но `db.commit()` в main есть — root cause не там». **Этот PR — кандидат на actual root cause #261**: без `X-Session-Id` backend `_resolve_session_id` генерил новый UUID на каждый запрос → POST `/custom-pois` сохранял с одним user_id, GET читал с другим → визуально «не сохраняется». После merge — обязательный smoke test #261. ### Не критично (pre-existing, не блокирует) - `invalidateQueries({ queryKey: ["analyze", parcelCad] })` в `useAddCustomPoi.onSuccess` — **no-op**, т.к. `useSiteAnalysis` это `useMutation`, не `useQuery`. Score обновится только когда user снова дёрнет analyze (re-submit cad / change weights). UX-доработка — отдельным PR (можно сделать `mutation.mutate()` вызов в onSuccess SiteMap, или `setData` в `useSiteAnalysis`). Текущий fix описанный симптом починит, т.к. PR-author тестил после click «Применить» / повторного POST. - TypeScript: `HeadersInit` шире чем `Record<string, string>`, но в codebase никто не передаёт `Headers`/`string[][]` в `apiFetch` — runtime spread безопасен. ### Pre-merge checks - mergeable=true · state=open · CI не настроен на этом repo · draft=false · head SHA stable ### Post-merge smoke test (после deploy) 1. Открыть https://gendsgn.ru/site-finder, ввести cad, нажать «Анализ» 2. Добавить custom POI на карте → нажать «Анализ» повторно (или подвинуть weight slider) 3. Проверить блок «Вклад по категориям» содержит custom POI, score изменился относительно baseline 4. **#261 verification**: добавить POI → перезагрузить страницу → POI должна остаться (session_id из localStorage стабилен) Merging via deep-code-reviewer (merge style — preserves PR commit message + closes #264).
lekss361 merged commit 9bac676b98 into main 2026-05-17 07:42:01 +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#266
No description provided.