Skip to content

ЧИТИРИ#3

Merged
Lukramancer merged 92 commits into
mainfrom
feat/mvp-rewrite
May 4, 2026
Merged

ЧИТИРИ#3
Lukramancer merged 92 commits into
mainfrom
feat/mvp-rewrite

Conversation

@MrGoldSky
Copy link
Copy Markdown
Member

No description provided.

- Switch to branch feat/mvp-rewrite (D-16)
- Remove web-map/src (Leaflet code) — old code remains on main
- .env confirmed gitignored, untracked, no leak in git history
- VITE_YMAP_KEY already present (D-04 corrected: no rename needed)
- Remove leaflet, react-leaflet, @types/leaflet
- Add MVP runtime stack: @tanstack/react-query, zustand, nuqs, msw, vaul,
  use-debounce, date-fns, react-router, react-hook-form, zod,
  @hookform/resolvers, lucide-react, clsx, tailwind-merge, react-error-boundary,
  @yandex/ymaps3-default-ui-theme
- Add dev tooling: @yandex/ymaps3-types, vitest, @vitest/ui, happy-dom,
  @testing-library/{react,jest-dom,user-event}, @playwright/test, prettier,
  prettier-plugin-tailwindcss, eslint-config-prettier
- Initialize husky + lint-staged
- Used --legacy-peer-deps because react-query-devtools 5.100.2 lists peer
  react-query@^5.100.2 but only 5.100.1 is published
- 4 tests for env config: valid parse, empty key rejection,
  default for VITE_AUTH_MODE, default for VITE_API_BASE_URL
- Forward-declare tests/setup.ts (will be expanded in Plan 02)
- Wire @ alias and vitest config in vite.config.ts
- Add baseUrl + paths + types to tsconfig.app.json
- src/{app,pages/map,widgets,features,entities,shared} layers
- shared/config: zod-typed env (VITE_YMAP_KEY required, AUTH_MODE/API_BASE_URL with defaults)
- shared/config/constants.ts: ITMO_CENTER (lon,lat), DEFAULT_ZOOM,
  VIEWPORT_DEBOUNCE_MS, BBOX_ROUND_DECIMALS
- shared/ui/Spinner.tsx: a11y atom (role=status, sr-only label)
- .env.example: documented env template
- src/main.tsx: placeholder bootstrap rendering Spinner
  (Plan 02 will replace with router + providers)
- eslint.config.js: no-restricted-imports patterns ban
  features↔features and entities↔entities imports (FSD Rules 3-4)
- eslint-config-prettier appended last to disable formatting rules
- .prettierrc: 100 cols, single quotes, trailing comma all,
  prettier-plugin-tailwindcss for class sorting
- .prettierignore: dist, node_modules, coverage, MSW worker, lockfile
- package.json: scripts (format, format:check, test, test:watch,
  test:e2e), lint-staged config
- .husky/pre-commit: runs lint-staged
- vite.config.ts: drop unused proxy callback args (lint fix)
- Pre-existing files (yml/html/md/json) reformatted by Prettier
- FSD self-test: temp @/features/other/model/x import → ESLint
  error reported correctly; deleted → lint clean
- axios instance с adapter:'fetch' для MSW 2 + 401-перехватчик (parktrack:unauthorized)
- AuthAdapter контракт: AuthStatus + User + интерфейс
- mock-adapter через TanStack Query на /auth/me (staleTime/gcTime: Infinity)
- shared-adapter — Phase-5 stub
- useAuth — переключение по env.VITE_AUTH_MODE
- AuthReady — Suspense-like гейт против race-condition (Pitfall #7, FOUND-09)
- entities/user — useUserProfile() обёртка над GET /users/me
…trap

- QueryProvider: staleTime 30s, retry 1, refetchOnWindowFocus=false; Devtools только в DEV
- queryClient вынесен в отдельный модуль (react-refresh/only-export-components)
- RootErrorBoundary через react-error-boundary с локализованным fallback
- AppProviders: RootErrorBoundary > NuqsAdapter > QueryProvider > AuthReady > children
- main.tsx: enableMocking() async-bootstrap MSW в DEV или AUTH_MODE='mock'
- BrowserRouter + Routes (/ и /map) с MapPage placeholder
- src/index.css: tailwindcss v4 entry
…раторы

- public/mockServiceWorker.js (msw init)
- mocks/browser.ts + mocks/node.ts (setupWorker + setupServer)
- generators/users.ts: AuthMe + UserProfile (mock-форма из docs api/)
- generators/zones.ts: 200 зон вокруг ИТМО, Mulberry32 PRNG seed=42
- generators/occupancy.ts: timeseries с baseline по часу/дню недели
- generators/forecasts.ts: будущие точки с расширяющимся std и затухающим confidence
- handlers.ts: /auth/me (delay 500ms в DEV), /users/me, /zones, /zones/:id,
  /occupancy, /forecasts, /routing/search, /routing/new (8 шт.)
- tests/setup.ts: jest-dom matchers + MSW node server (server.listen 'error') + ymaps3 vi.mock (Pitfall #19 forward-declared для Plan 03)
- tests/unit/env.spec.ts: 4 теста для EnvSchema (FOUND-10 acceptance)
- playwright.config.ts: chromium-проект, webServer 'npm run dev', port 5173
- tests/e2e/smoke.spec.ts: smoke-скелет (полноценный E2E в Plan 03)
- vite.config: exclude tests/e2e из vitest, чтобы Playwright и Vitest не конфликтовали
…3 touch-point)

- index.html: <script src=api-maps.yandex.ru/v3?apikey=%VITE_YMAP_KEY%&lang=ru_RU>
- src/shared/lib/ymaps/index.ts: единственный модуль, обращающийся к window.ymaps3 (Anti-Pattern #5)
  reactify.bindTo(React, ReactDOM); экспортирует YMap*, YMapZoom/GeolocationControl, useDefault
- src/shared/lib/ymaps/types.ts: re-export LngLat/DrawingStyle/YMapLocationRequest для потребителей
- Pitfall #1 + MAP-07 задокументированы в шапке index.ts
- bbox.spec.ts: roundBbox5 (3 теста) — округление до 5 знаков, стабильность, bboxFromBounds
- zone-style.spec.ts: computeZoneStyle (2 теста) — мемоизация по 5-частному ключу

Тесты сейчас падают (модули создаются на GREEN-этапе).
…le stub

- shared/lib/geo: Bbox/MapBounds типы, roundBbox5 (MAP-06), bboxFromBounds, bbox<->string
- shared/lib/url/parsers: parseAsBbox — кастомный nuqs createParser
- entities/zone: ZoneMapItem (PolygonGeometry inline) + TimeMode + fetchZones (signal) + useZonesQuery
  queryKey ['zones', mode, round5(bbox)] + keepPreviousData + staleTime 30s (MAP-05/06/08)
- widgets/map-canvas/model/zone-style.ts: computeZoneStyle с 5-частным cache-key (Phase 1 stub colors)

Deviation [Rule 3 — Blocking]: @types/geojson не установлен в Plan 01/02 — определил
PolygonGeometry inline в zone.types.ts вместо добавления нового пакета.

Все 5 unit-тестов проходят (3 bbox + 2 zone-style).
…tZones (read)

- widgets/map-canvas/model/useBboxTracking: useDebouncedCallback(400мс) → round5 → setBbox
  skip-write если round5(next) === bbox (jitter-free, MAP-04/06, Pitfall #2)
- features/viewport-driven-zones/model/useViewportZones: чтение bbox из URL → useZonesQuery
  feature НЕ импортирует из @/widgets (FSD: useQueryState дублируется)
- mode='now' захардкожен; Phase 3 заменит на timeMode-адаптер
…ndary

- widgets/map-canvas/ui/MapCanvas: единственный владелец YMap-ref (MAP-01/02)
  YMapDefaultSchemeLayer + YMapDefaultFeaturesLayer (MAP-03 — встроенный парковочный слой)
  YMapListener.onUpdate → useBboxTracking.writeBbox; YMapZoomControl справа
- widgets/map-canvas/ui/MapSkeleton: animate-pulse контейнер, role=status (UX-01)
- widgets/map-canvas/ui/ZoneLayer: debug-overlay с количеством зон (Phase 2 заменит на полигоны)
  + MAP-09 inline note: clusterer threshold ~500 — измерить spike'ом в Phase 2
- app/errors/MapErrorBoundary: react-error-boundary вокруг MapCanvas; fallback с
  кнопкой «Перезагрузить карту» при CDN-fail (MAP-07)
- pages/map/MapPage: React.lazy(MapCanvas) + Suspense(MapSkeleton) + MapErrorBoundary

window.ymaps3 — единственный файл src/shared/lib/ymaps/index.ts (Anti-Pattern #5 enforced).
13/13 unit-тестов зелёные, lint/tsc чистые.
- map.spec.ts:
  * 'карта монтируется ...' — ждёт зональный overlay через getByTestId('zone-count')
  * 'MAP-05: непрерывный пан 5с → не более 3 запросов /zones' — считает реальные
    /zones-запросы во время drag-loop
- npx playwright test --list ↦ 3 теста (smoke + 2 map.spec)

Manual-verify steps задокументированы в SUMMARY (CDN block, deeplink, AuthReady race).
…я Playwright

- .gitignore: разделил .env / playwright-report (regexp слил их в одну строку);
  добавил test-results
- .prettierignore: playwright-report + test-results
- prettier --write нормализовал index.html (CDN-script на одну строку),
  src/index.css, tests/setup.ts (минорные пробелы/EOL)

Final gates на feat/mvp-rewrite (Node v24.11.0):
- npm run lint                exits 0
- npm run format:check        exits 0
- npx tsc -b --noEmit         exits 0
- npm run test                4 файла / 13 тестов — green
- npm run build               успешен (385 kB JS gzip 123 kB, 9.36 kB CSS)
- npx playwright test --list  3 теста дискаверятся

Phase 1 код полностью на feat/mvp-rewrite.
…t fix + playwright 127.0.0.1

- Dockerfile: node:18-alpine -> node:20-alpine (R-1 from 01-01-SUMMARY)
- npm ci с --legacy-peer-deps (R-2 from 01-01-SUMMARY)
- MapPage: lazy import через subpath (web-map/ui/...), чтобы Vite не втянул
  @/shared/lib/ymaps в главный chunk → top-level await не падает ДО монтажа
  React → MapErrorBoundary реально работает
- Playwright: 127.0.0.1 вместо localhost (Windows IPv6 quirk)
- package-lock.json: регенерирован после --legacy-peer-deps install
Failing tests за parallel-geometry.spec.ts (3 кейса) и centroid.spec.ts (2 кейса).
Модули @/shared/lib/geo/parallel и @/shared/lib/geo/centroid пока не существуют —
добавятся в GREEN-коммите Task 1.
- shared/config/zone-palette.ts: 5-цветная D-01 палитра + CONFIDENCE_THRESHOLD
- shared/config/constants.ts: +ZONE_BADGE_MIN_ZOOM=14 (D-02), +FILTER_STORAGE_PREFIX (D-11)
- shared/lib/geo/parallel.ts: polygonToParallelLine — D-04 геометрия
- shared/lib/geo/centroid.ts: zoneCentroid для бейджей и центрирования
- entities/zone/model/zone.types.ts: ZoneMapItem приведён к форме MSW
  (zone_id: number вместо id: string; +location_type, +is_private, +is_accessible,
  +occupancy_updated_at, +confidence_level, +occupied) + добавлен Zone (full model
  для CARD-01)
- barrels: ре-экспорт parallel + centroid + zone-palette + расширенные типы

Тесты parallel-geometry.spec.ts (3) и centroid.spec.ts (2) — все зелёные.
Полный rewrite zone-style.spec.ts: 8 кейсов покрывают 5 цветовых правил D-01,
selected → strokeWidth 3 (D-08), memoization, selected/unselected как разные
cache entries.

Старые Phase 1 кейсы (нейтрально-серый STUB) удалены — Phase 2 заменяет
поведение целиком.

7/8 тестов падают на старом stub'е (1 проходит — memoization сохраняется),
плюс tsc -b ругается на zoneId: number vs string. GREEN-коммит Task 2 закроет.
- StyleKey: zoneId: string -> number; +selected: boolean (D-08)
- pickPalette: 5 правил D-01 (inactive | full | one | freeLow | freeHigh)
- computeZoneStyle: strokeWidth 3 при selected, 1 иначе
- Импорт через named tokens из @/shared/config/zone-palette → Phase 5 swap
  на UI-kit Миши без рефакторинга consumers

Все 8 тестов zone-style.spec.ts зелёные.
- ZoneLayer: REWRITE Phase 1 debug-overlay → реальный рендер standard-зон через
  YMapFeatureDataSource + N <YMapFeature> children (Pattern 1 reactify diff)
- ParallelZoneLayer: NEW — D-04 LineString между midpoint'ами коротких сторон,
  отдельный datasource zIndex 1901
- ZoneBadgesLayer: NEW — ZONE-06 redundant encoding, free_count pill через
  YMapMarker + zoneCentroid; скрыт при zoom < ZONE_BADGE_MIN_ZOOM (=14)
- MapCanvas: zoom трекается локально (setState), 3 новых zone-layer'а композированы
- zone-style.ts: +toDrawingStyle() конвертер ZoneStyle → ymaps3 DrawingStyle
  (граничный, чтобы внутренний формат остался plain { fill, stroke, strokeWidth }
  для тестов и Phase 5 UI-kit swap)
- map-canvas/index.ts: re-export ZoneLayer/ParallelZoneLayer/ZoneBadgesLayer
- e2e/map.spec.ts: data-testid 'zone-count' (Phase 1 debug-overlay) → 'zone-badge'
  (новый сигнал «зоны загрузились»)

selectedZoneId захардкожен false — Plan 02 заменит на useSelectedZone() hook
(?sel= via nuqs). onClick — stub с console.debug; setSelectedZone wiring в Plan 02.

Все 24 unit-теста зелёные; tsc и lint clean; FSD-граница не нарушена.
…ement deferred to HUMAN-UAT)

Auto-mode не позволяет реально замерить fps — для этого нужен живой браузер +
DevTools Performance panel. Зафиксирован inline-комментарий в ZoneLayer.tsx с:
- educated guess: ~50 fps при 200 zones + badges (на основе PITFALLS #2 +
  reactify-diff pattern)
- threshold MVP: 45 fps
- ссылка на .planning/.../02-HUMAN-UAT.md item «MAP-09 fps» — пользователь
  должен выполнить тест и обновить число с реальным measured

Дев-сервер успешно стартует (Vite ready ~640мс с 200 фейковыми зонами),
tsc/lint/тесты зелёные. До получения реального замера MAP-09 формально
считается «закрыт по educated guess», follow-up в Phase 2.x если measured < 45.
- 8 plural кейсов: 0/1/2/5/11/21/22/1.5 — включая критический n=11→many
- 3 relative-time кейса: past/future/часы с vi.useFakeTimers
- pluralizeRu/formatRelativeRu — пока stub'ы (return '')
- pluralizeRu через Intl.PluralRules('ru') с lazy init.
  CLDR 'other' (нецелые числа) маппится на 'few' — речевая норма:
  «1,5 места», «2,7 литра» (родительный падеж единственного).
- formatRelativeRu через date-fns formatDistanceToNow + ru locale
  (date-fns ^4.1.0 каноничный путь импорта).
- Все 11 тестов зелёные (8 plural включая n=1.5 + 3 relative-time).
- useSelectedZone (features/select-zone) — single source of truth для ?sel=:
  setSelectedZone (pushState — Back закрывает карточку, URL-07);
  closeCard (replaceState — без spam history entries, D-14).
- fetchZoneById(id, signal) + useZoneByIdQuery в entities/zone — для CARD-01.
- ZoneLayer + ParallelZoneLayer wired:
  onClick → setSelectedZone(z.zone_id) (вместо console.debug stub Plan 01),
  selected: z.zone_id === selectedZoneId → strokeWidth=3 (D-08 highlight).
- Parallel-вариант: stroke-width 6 → 8 при selected.
- FSD граница соблюдена: features/select-zone не импортит widgets.
- 8 RTL кейсов на ZoneCardContent (CARD-01..07):
  loading/Spinner, '5 мест', '1 место', Бесплатно, 200₽/час,
  Частная (CARD-03), aria-label='Закрыть карточку' (A11Y-02),
  кнопка 'Построить маршрут' (CARD-05).
- ZoneCard/MobileZoneCard — пока stub'ы (return null).
- chore: добавлен dev-dep @testing-library/dom (peer для @testing-library/react v16).
…RD-07 mobile pan (GREEN)

- ZoneCard (D-05): desktop right-side panel 400px overlay; aside hidden lg:block;
  карта НЕ ужимается (D-05). D-08a: key={selectedZoneId} → smooth re-render.
- ZoneCardContent: header + Spinner/error/data branches; через useZoneByIdQuery.
- ZoneCardBody: free_count + ru-плюрализация (CARD-06), capacity, confidence%,
  formatRelativeRu (CARD-02), Бесплатно/₽/час (CARD-04), маркеры zone_type/
  location_type/Частная/Для инвалидов (CARD-03/ZONE-04), кнопка маршрута (CARD-05).
- MobileZoneCard (D-06): vaul snap [0.4, 0.85], lg:hidden Portal/Overlay/Content.
  CARD-07 mobile pan: useEffect → mapRefHolder.current.setLocation({center, duration:300}).
- MapRefContext вынесен в widgets/map-canvas/model/map-ref-context.ts
  (react-refresh/only-export-components rule). Re-exported из barrel.
- MapCanvas: useRef<YMapInstance> + ref={mapRef} к <YMap>; Provider wraps children.
- 8 RTL-тестов GREEN (Spinner loading, '5 мест', '1 место', Бесплатно, 200₽/час,
  Частная, aria-label='Закрыть карточку', кнопка 'Построить маршрут').
- MapPage композирует MapCanvas + ZoneCard + MobileZoneCard.
  ZoneCard: hidden lg:block внутри (desktop overlay).
  MobileZoneCard: lg:hidden внутри Portal/Content классов (mobile vaul).
  Оба слушают один useSelectedZone() — синхронизированы через URL ?sel=.
- Контейнер: relative + h-screen + w-screen + overflow-hidden — для
  position:absolute right:0 ZoneCard'а.
- Контракт Plan 03 (wave 3): когда введёт DesktopLayout/MobileLayout split,
  ОБЯЗАН сохранить ZoneCard в DesktopLayout и MobileZoneCard в MobileLayout.
- chore: prettier отформатировал zone-card.spec.tsx (polygon coordinates).
- useRoutingSearchBody composes URL ?from/?dest + filters + timeMode → RoutingSearchBody|null
- D-14 hardcoded limit:20, provider:'yandex', max_distance_to_destination_meters:500
- D-15 mode dispatch: from && !dest → find_parking; from && dest → route_to_destination
- D-41 use_forecast = (timeMode.kind !== 'now')
- useAutoSelectBestVariant: write ?sel ONLY when ?sel === null (research Q3 / sticky URL)
- 7 vitest specs all green
…tate + scroll sync

- ResultItem layout per D-20 verbatim: badge «Лучший вариант» (rank=1), zone_id,
  free_count/capacity, pay (or «Бесплатно»), forecast row, distance, ETA, confidence
- ResultsList — @tanstack/react-virtual useVirtualizer (estimateSize=140, overscan=4)
- EmptyResultsState (D-44) с verbatim текстом + reset/close buttons
- useResultsScrollSync (D-22) — virtualizer.scrollToIndex когда ?sel ∈ candidates
- W-4: zoneCentroid impl. accepts minimal-shape geometry без cast hack
- 13 vitest specs all green
…rrel

- DesktopResultsPanel: 400px left-side overlay (D-18); CO-03 open=!!from only
- MobileResultsSheet: vaul Drawer single-snap [0.92] (CO-02 / B-3 fix)
- Mutual-exclusion с MobileZoneCard через open=!!from && selectedZoneId===null
  (sequential focus, no nested vaul / focus-trap conflict — Pitfall 11)
- Auto-select best variant (D-21 / WTP-06) подключён в обоих widget'ах
- Empty/Error/Spinner states (D-44, D-45, UX-02)
- tsc --noEmit clean
…earch'] (D-42)

- Aggregate fetchingCount = fetchingZones + fetchingRouting
- Atomic-mode-switch coverage для ResultsPanel (overlay висит пока routing-search не settled)
- Context-aware text: «Поиск парковок…» когда routing fetching > 0;
  «Загрузка данных за выбранное время…» когда zones fetching;
  fallback «Загрузка…»
- Existing 4 tests passing (mocked useIsFetching → returns same value for both subscriptions)
- tsc --noEmit clean
…Mobile Layouts

- DesktopLayout: <DesktopResultsPanel/> рядом с <ZoneCard/> (z-20 left overlay,
  z-30 right card). Map центрируется между ними.
- MobileLayout: <MobileResultsSheet/> перед <MobileZoneCard/> (mutual-exclusion
  via selectedZoneId logic; CO-02 single-snap [0.92] sequential focus).
- Полный suite 272/272 tests passing; tsc --noEmit + ESLint clean
…sk 1)

- D-28: useRouteId hook (?route=<int> URL state, history='replace')
- CO-05/W-2: useRouteSelSync reverse-sync (?route → ?sel) для reload-recovery
- ROUTE-03/D-29: RoutePreviewLayer — LineString polyline + origin/dest markers
  - W-4 fix: zoneCentroid принимает minimal-shape без cast
  - key={routeId} для clean reconciliation; viewport не сбрасывается
- Wire RoutePreviewLayer как последний child YMap в MapCanvas

4 vitest tests green; tsc --noEmit clean.
…leDeeplinkSheet (Task 2)

- D-32: 3 опции menu (Яндекс Навигатор autoFocus / Яндекс Карты web / Google Maps)
- D-33 timer-fallback: visibilitychange listener + setTimeout DEEPLINK_FALLBACK_MS=2500ms
  → если page всё ещё visible после 2500ms, открываем yandex.ru/maps в новом окне
- D-34 coordinate validation: isValidCoords ПЕРЕД сборкой URL; invalid → ptk:deeplink-invalid event
- DesktopDeeplinkPopover: radix Popover, [В путь →] trigger disabled при !coordsValid
- MobileDeeplinkSheet: vaul Drawer, 3 опции + [Отмена]; min-h-[44px] tap targets

5 vitest tests green; tsc --noEmit clean.
- D-31 RouteSummaryCard: «Маршрут построен» heading + ETA + Intl distance + arrival
  - Intl.NumberFormat ru-RU unit:meter для distance
  - Intl.DateTimeFormat timeZone:'Europe/Moscow' для arrival_time → «Прибытие в HH:MM МСК»
  - Embeds DesktopDeeplinkPopover (lg:block) и MobileDeeplinkSheet (lg:hidden)
  - coordsValid := isValidCoords(from) && isValidCoords([zoneLat, zoneLon])
- D-30 FitToRouteButton: user-initiated bbox fit, bottom-right (z-25)
  - aria-label «Показать весь маршрут»
  - bbox охватывает origin + zone_centroid → map.setLocation({bounds, duration:400})
- W-4 zoneCentroid принимает minimal-shape (no cast hack)
- Barrel exports complete: useRouteId/useRouteSelSync/RouteSummaryCard/FitToRouteButton

4 RouteSummaryCard tests green; tsc --noEmit clean.
…s ?route+?sel (Task 4)

- D-27 / ROUTE-01: BuildRouteSection в ZoneCardBody
  - useRoutingSearchBody (?from + ?dest + filters + timeMode) + selected_zone_id
  - useCreateRouteMutation.mutateAsync → setRouteId(route.route_id)
  - On success → routeId !== null → render RouteSummaryCard inline (replaces button)
  - canBuildRoute := body !== null; без ?from prompt-text «укажите стартовую точку»
  - D-46 errorMsg «Не удалось построить маршрут» + [Повторить]
- D-28 close handler: ZoneCard + MobileZoneCard оба зовут handleClose = clearRouteId + closeCard
  → atomic clear ?route + ?sel при X / outside / Esc click

285/285 vitest tests passing (272 baseline + 13 Phase 4 new); tsc --noEmit clean.
…e (Task 5)

- DesktopLayout / MobileLayout: добавлен <FitToRouteButton/> child (gates сам себя по ?route)
- tests/e2e/phase4-smoke.spec.ts:
  - Test 1: search → results → build route → deeplink menu (full sales-funnel)
  - Test 2: reload с invalid ?route не crashит
  - Test 3: ?dest reload renders ok
  - test.skip с reason на ymaps3 CDN failure (Phase 3 blocker per STATE.md)
- ROUTE-08 явно помечен deferred-to-Phase-5 в spec header

285/285 vitest tests; tsc + ESLint clean. Playwright execution может skip из-за
ymaps3 CDN headless issue — spec остаётся как code asset для Phase 5 polish.
@nawinds
Copy link
Copy Markdown
Member

nawinds commented Apr 30, 2026

These changes are now in development branch, which will be deployed separately. This PR will be merged after it is tested in staging

MrGoldSky added 16 commits May 3, 2026 20:36
- Create shared/lib/dom/useVisualViewportHeight hook (Pitfall 1 fix:
  iOS Safari does NOT update 100dvh on keyboard; visualViewport.height does).
  Hook returns dynamic height + sets --keyboard-aware-height CSS var on :root.
- Add 4 unit tests (vv available / sets CSS var / fallback / cleanup) — pass.
- Replace h-screen → h-dvh in DesktopLayout + MobileLayout (D-02).
- Integrate useVisualViewportHeight() side-effect call into 5 mobile components
  (MobileFiltersDrawer, MobileTimeSelectorSheet, MobileResultsSheet,
  MobileZoneCard, MobileSearchBar).
- Add maxHeight: 'calc(var(--keyboard-aware-height, 100dvh) - 80px)' inline
  style to the 4 vaul Drawer.Content surfaces; wrap MobileSearchBar overlay
  height in same CSS var.
- Preserve CO-02 single-snap [0.92] for MobileResultsSheet (D-06).
…ard (RESP-06,07)

- src/index.css: add .map-controls-shifted-container [class*=ymaps3-controls]
  rule reading var(--bottom-sheet-offset, 20px) with 200ms ease transition.
  YMapControls does NOT accept className prop (typed reactify wrapper from
  @yandex/ymaps3-types), so parent-div selector-fallback is used.
- MapCanvas.tsx: wrap inner div with class map-controls-shifted-container.
- MobileLayout.tsx: useEffect sets --bottom-sheet-offset to calc(92vh + 20px)
  when ANY of filtersOpen/timeSheetOpen/resultsSheetOpen/selectedZoneId is
  active; default 20px otherwise.
- eslint.config.js: add no-restricted-syntax rules blocking h-screen /
  min-h-screen / max-h-screen in className= AND any 100vh literal (D-07
  regression guard). Verified rule fires on temp test file.
- tests/e2e/tap-targets.spec.ts: Playwright runtime test asserting all
  buttons/links on /map iPhone 13 viewport have computed bbox >=44x44
  (WCAG 2.5.5). Skips on ymaps3 CDN failure (Phase 3 known blocker)
  consistent with phase4-smoke.spec.ts pattern.

Note: eslint-plugin-tailwindcss is NOT used — research finding: package
does NOT support Tailwind 4 (issue #325 open). Playwright runtime test is
the sole tap-target enforcement mechanism.
- Install sonner@^2.0.7 as runtime dependency (D-19) via --legacy-peer-deps
- Extend env.ts Zod schema: VITE_SHARED_SHELL_URL (D-09) + VITE_API_MODE (D-15)
- Create brand-tokens.ts: single source of truth (D-12) — green/amber/neutral/semantic
- Append @theme directive to index.css (Tailwind 4 native, after Plan 05-01 .map-controls-shifted)
- Update shared/config barrel to re-export brand
- Document all 5 env vars in .env.example incl. localhost limitation warning (Pitfall 4)
…1..03)

- Replace throwing stub with TanStack Query + apiClient.get('/auth/me') (D-08, D-09)
- Map AuthMeResponse -> User (display_name = full_name ?? email)
- Localhost guard: console.warn when VITE_AUTH_MODE=shared on localhost (Pitfall 4)
- 4 vitest tests: 200 happy path, full_name=null fallback, 401 unauthenticated,
  W-1 fix static source-content assertion via Vite ?raw import (replaces placebo)
- Test 4 uses ?raw instead of node:fs/__dirname (app tsconfig has no node types)
…06; UX-05,06)

- AuthListener: 401 CustomEvent listener (D-10) — mock=invalidate+warning toast,
  shared=error toast + 2s redirect to /login?return=...
- AppProviders: mount Toaster (zIndex 100, Pitfall 2 — above vaul z-50) inside
  AuthListener inside QueryProvider; AuthReady wraps children unchanged
- Toast.tsx: thin sonner re-export (D-13) — vendor-swap = single-file change
- Banner.tsx: in-drawer error primitive (D-13) — 4 variants, 44x44 dismiss button
- StubHeader.tsx: returns null in shared mode (D-14, INTEG-06)
- Update barrels: providers/index.ts re-exports AuthListener, ui/index.ts adds 4
- main.tsx: gate MSW worker registration on VITE_API_MODE (independent
  from VITE_AUTH_MODE per D-15) — enables 4-combo testing of API/auth
  modes; default mock when env unset
- playwright.real-api.config.ts: dedicated config (NOT in default CI),
  testMatch pinned to real-api.spec.ts, HTML report to phase-05-uat/
- tests/e2e/real-api.spec.ts: 7 shape-only smoke tests covering all 6
  endpoints (GET /zones, /zones/<id>, /occupancy, /forecasts; POST
  /routing/search, /routing/new) + D-17 combined-filter probe
- package.json: test:e2e:real-api script via cross-env (Windows-portable)
- cross-env@^7.0.3 added as devDep (B-2 ownership: Plan 05-04 must
  grep-guard before re-installing)
- ambient process declaration in spec avoids polluting app tsconfig with
  node types (mirrors Plan 05-02 W-1 approach)
…D-17/D-18)

- New section «Phase 5 D-17 verification protocol» appended (Phase 2
  baseline preserved unchanged)
- 7-row verification status table (one row per filter), all marked
  unverified — Plan 05-05 UAT fills statuses from real-api smoke
- 5-value status legend (unverified/accepted/degraded/rejected/
  client-only-fallback) clarifies action for each outcome
- Per-filter individual curl probe commands documented for offending-
  param identification when combined GET fails
- D-18 conditional normalizer note: normalizers.ts created ONLY if
  smoke shows shape divergence (avoid speculative dead code)
- phase-05-uat/real-api-smoke.log artefact structure specified
…staleTime (D-29 NFR-01 + D-32 NFR-04)

- Install audit devDeps: @axe-core/playwright, rollup-plugin-visualizer^6.0.4 (resolved ^6.0.11), size-limit^11, @size-limit/preset-app, ts-morph; cross-env already present (B-2 guard verified)
- Enable noUncheckedIndexedAccess + exactOptionalPropertyTypes + noImplicitOverride + noImplicitReturns in tsconfig.app.json AFTER dry-run (107 errors → 0; fixed in batch across 14 files: array-access non-null assertions, conditional spread for optional props, defensive guards in zoneCentroid)
- ESLint: @typescript-eslint/no-explicit-any: error blocks any in new code (existing code clean)
- Mode-aware staleTime in zone.queries.ts (D-32 NFR-04 — I-1 fix moved here from 05-03):
  /zones (now)         → 30s
  /occupancy (past)    → 300s (5min, history immutable)
  /forecasts (future)  → 60s (decay)
  /zones/<id> (now)    → 60s
  /occupancy view=card → 300s
  /forecasts view=card → 60s
- WTPCTAButton.test.tsx fixed (deferred from 05-01): mock navigator.permissions + findByText for async handleClick
…o (D-22/23/24/31/33 NFR-03/05/06)

- vite.config.ts: rollup-plugin-visualizer (BUILD_ANALYZE=1) + manualChunks splitting:
  vendor-react (react+react-dom+scheduler+react-error-boundary co-located per Pitfall 10 TDZ),
  vendor-tanstack, vendor-state, vendor-ui (vaul+radix), vendor-icons (lucide), vendor-misc
- .size-limit.json: 5 hard-fail budgets (initial<250KB, vendor-react<100KB, etc.); all pass:
  initial=21.65KB, vendor-react=60.89KB, vendor-tanstack=15.94KB, vendor-ui=17.69KB, vendor-icons=2.55KB
- package.json: build:analyze + size scripts (cross-env from 05-03 reused per B-2)
- nginx.conf: Content-Security-Policy verbatim from Yandex docs (script/connect/style/img/worker/frame-ancestors)
  + X-Content-Type-Options, X-Frame-Options, Referrer-Policy
- index.html: csp=202512 migration param appended to Yandex CDN URL (Pitfall 12)
- 4 widgets wrapped in React.memo (D-31 / I-3 fix incl. ParallelZoneLayer):
  ZoneLayer, RoutePreviewLayer, ParallelZoneLayer, DesktopResultsPanel
…s docs (D-30/34 NFR-02/07)

- OfflineBanner via @tanstack/react-query onlineManager.subscribe (NOT navigator.onLine direct read per Pitfall 8 — Chrome bug)
- Toast feedback through @/shared/ui wrapper (Plan 05-02 ingress): error 'Нет соединения' (id='offline', duration:Infinity) + success 'Соединение восстановлено' on reconnect
- Mounted in AppProviders sibling to Toaster, inside AuthListener
- Exported from app/providers/index.ts barrel
- docs/fsd-exceptions.md documents 2 known cross-layer imports (ZoneCard→MapCanvas, useFilteredZones→filter-zones) — NFR-02 reviewer trail
- Security grep audit (D-33): no token leakage in console.*, no token in localStorage, no dangerouslySetInnerHTML usage
…11y docs (D-21/25/27/28/35)

- tests/e2e/a11y.spec.ts: axe-core scan over 4 flows (/map, /map?sel=42, /map?from=&dest=, /map?route=1), critical=0 blocks merge per D-26, serious console.warn for backlog (W-2 fix: no fs imports/writes)
- tests/e2e/atomic-state.spec.ts: NFR-08 verification — 2 tests
  1. parallel filter+time+zone change → no runtime errors
  2. rapid filter toggle → AbortController cascade verifies completedRequests ≤ 2 (I-2 fix: tightened heuristic with rationale comment)
- tests/unit/no-silent-failures.spec.ts: ts-morph AST audit asserts every useQuery/useMutation has onError/throwOnError; allowlist for queries that propagate error to caller (auth adapters, address suggest/resolve, zone/routing queries, user profile)
- ambient declare process per Plan 05-02/03 minimal-surface pattern (no @types/node pollution)
- 4 docs: a11y-backlog.md (placeholder for v1.x), a11y-keyboard-walkthrough.md (10-step manual scenario per D-27), a11y-colorblind-audit.md (5 vision-mode test matrix per D-28)
- web-map/docs/uat-flows-checklist.md: 12 manual flow steps (10 + VK/TG D-37/D-38)
- web-map/docs/uat-matrix.md: required + optional device list with status table
- Templates per D-36 — owner = Илья Р. (real-device tester); Claude prepares
- Pass criteria: all 10 flows on iPhone iOS17+ Safari, Android 14+ Chrome, VK/TG webview
- Phase 5 deliverables: RESP-01..07, INTEG-01..06, A11Y-06, UX-05/06, NFR-01..08
- Documents ALL 24 Phase 5 requirements with implementation note
- Lists known limitations + v1.x deferrals (Misha-shell, UI-kit, Lighthouse perf)
- Notes default Playwright headless ymaps3 CDN limitation; UAT delegated
- Phase 5 verification artifacts archived in .planning/phases/05-.../uat/
Mock /routing/search возвращал inactive (is_active=false) zones в кандидатах
ranking → tap на парковку из ResultsPanel → ZoneCard показывала empty-state
'Зона неактивна в этот период' вместо контента.

Server design assumption (applyClientCandidateFilters comment): RouteCandidate
не имеет is_active поля — server должен ВСЕГДА возвращать только active+public
parkings. Mock теперь mirror это поведение через ALWAYS-ON фильтр в rankCandidates.

Affected: ResultsPanel ranked list, MobileResultsSheet, MobileZoneCard handoff
flow. Frontend изменений не требует — applyClientCandidateFilters уже учитывает
этот контракт.
Root cause: Phase 2 D-06 specified snapPoints=[0.4, 0.85], но vaul snap math
требует drawer height >= largestSnap × viewport (≥792px на iPhone 14 Pro Max).
Реальный content (header+tags+button ~408px) намного меньше — vaul применяет
transform translateY(559px) который пушит drawer ENTIRELY off-screen. Карточка
рендерится в DOM (verified: data-state=open, content visible), но визуально
не видна.

Тот же bug был в Phase 4 MobileResultsSheet — решился single-snap [0.92] (CO-02).
Применяем тот же pattern: drawer открывается на 92% экрана, drag-down dismiss.
Preview-режим [0.4] deferred to v1.x design pass per CO-02 protocol.

Affected mobile flow: tap parking → ResultsSheet close → MobileZoneCard open.
Все 294/294 tests pass.
User feedback: drawer открывался на 92dvh с большим пустым пространством под
кнопкой «Построить маршрут»; drag handle не работал из-за vaul snap math.

Решение: убрать snapPoints/activeSnapPoint полностью. vaul без snap-points
auto-fit'нет drawer на natural content height — drawer открывается ровно до
кнопки + 15px (pb-[15px]) внизу. Drag-down dismiss работает нативно через
vaul handle. max-height ограничивает экстремальные случаи viewport-safe-area.

Снимает: hot-fix 56a7fa3 ([0.92] snap который тоже не fit'ил content корректно).
Ничего не ломает existing tests (294/294 pass).
@Lukramancer Lukramancer merged commit abd8ae8 into main May 4, 2026
1 check passed
@Lukramancer Lukramancer mentioned this pull request May 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants