strava-frontend/.roo/memory-bank/activeContext.md

321 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Active Context — Strava Frontend
## Task State
- task_id: TASK-SSR-EXPLORE-CARDS
- status: success
- summary: **SSR `/explore` теперь рендерит карточки 1:1 с клиентом: `<section class="news-feed">` → `<article class="news-item">` (`.news-item-header`→`<h3><a>`, `ul.metadata` 9 условных `<li>` в порядке клиента, `.image-container`-`<a>` с background-image). Хэндлер + `distInt` в `server/index.ts`; добавлен отсутствовавший `tsconfig.server.json` (`yarn server:build` теперь работает).**
- next task: —
- plan: `plans/TASK-SSR-EXPLORE-CARDS.md`
## ✅ CODER SUCCESS REPORT — TASK-INTEG-SECTION (+TASK-SECTION-BUGS)
- `../integration/tests/section-duration.spec.ts` — 3 теста (duration row, zoom persist across tabs, reset on all tabs), self-contained (stable owner `itest-section-owner`, API-seed, Chart.js via `canvas.__vueParentComponent.exposed.chart`).
- **Bug 1 фикс** (`WorkoutItem.vue` `setChartRef` ~1155): ref пушится сразу (без guard `r.chart`), финализация (`collectCharts` + `applySelectedRangeZoom`) через retry `nextTick` (до 20 попыток) — дождётся асинхронной инициализации Chart.js. `nextTick` добавлен в импорт из "vue" (~строка 440).
- **Bug 2 фикс** (`ChartGroup.ts` `resetAll` ~125): после `resetZoom(chart)` явно `x.min=undefined; x.max=undefined; chart.update("none")` (broadcastZoom писал min/max напрямую, минуя плагин). Дополнительно: на время цикла `resetAll` установлена `this.broadcasting=true` (try/finally) — `resetZoom` триггерит `onZoomComplete`, который иначе повторно разносит ещё-зумированный диапазон на остальные чарты; guard `isBroadcasting()` отбрасывает эти колбэки.
- `npx playwright test tests/section-duration.spec.ts` → **3 passed** (56s) на docker-стече.
- `yarn build` (vue-tsc --noEmit + vite) → exit 0.
- Known: диагностический `tests/chartprobe.spec.ts` не удалён (rm отклонён) — удалить вручную, если мешает.
## TASK-INTEG-SECTION: план делегирования
**Scope:** 1 новый файл — `../integration/tests/section-duration.spec.ts` + запуск на стеке. Не трогать `src/`.
**Что тестируется (3 UX-требования, компонент `src/pages/workouts/components/WorkoutItem.vue`, страница `workout_item`):**
1. Клик по графику (выделение участка) → в блоке `.area-data` появляется строка «Продолжительность».
2. Клик по вкладке «Дистанция» → тот же участок остаётся в zoom (не весь путь).
3. Клик по reset → участок сброшен; после смена вкладки → весь путь (reset на всех вкладках).
**Контракты (проверено Архитектором):**
- Детальная страница (приватная, с графиками + вкладками): маршрут `workout_item` → URL `{FRONTEND}/workouts/workouts/{id}` (родитель `workouts` + вложенный `workouts/:id`). Навигация `waitUntil: "domcontentloaded"` (Yandex map держит соединения).
- Seed через API (чисто, без UI-dedup): `POST {BACKEND}/api/v0/signup` (200) ИЛИ `POST /api/v0/signin` (fallback на повторном прогоне) → token; `POST /api/v0/attachment/upload` (FormData поле `file`, Bearer) → `{id}`; `POST /api/v0/workouts` `{attachment_id, name}` → `{id}` (400 `ObjectExists` → взять id из `GET /api/v0/workouts`[0]).
- Файл: `../../backend/tests/fitness_tracker_data_parsing-main/20240507053735.fit` (тот же, что в `workout-upload-flow.spec.ts`).
- Детальный ответ `GET /api/v0/workouts/{id}` → `{ results: [{ timestamp, longitude, latitude, speed, heart_rate, power, elevation, ... }], workout: {...} }` — из него взять полный диапазон (первая/последняя `timestamp`, длина) для порогов zoom.
- Chart.js: canvas графиков — `#workout-charts .workout-chart-canvas canvas` (несколько; брать `[0]`). Чертит только если есть метрика (у fixture есть speed). Zoom-состояние: `window.Chart.getChart(canvas).isZoomedOrPanned()` + `chart.options.scales.x.min/max` (broadcastZoom пишет сюда) + `chart.scales.x.min/max`.
- Клик по графику (выбор участка) — через Chart.js `onClick` опцию: `mouse.down` → `mouse.move` → `mouse.up` в точке внутри chartArea (не drag, чтобы не было drag-zoom). Координаты взять через `chart.chartArea` (left/right/top/bottom) в `page.evaluate`.
- Кнопки вкладок: по тексту «Время» и «Дистанция» (VaButton). Reset: `#zoom-botton`. Блок данных участка: `.area-data` (заголовок «Данные участка»), строка «Продолжительность».
**Структура спе (self-contained, repeatable):**
- `beforeAll`: проверить стек (frontend `<500`, backend `/api/v0/html_test/login`==200), проверить наличие fixture-файла; seed stable owner (`itest-section-owner@example.com`, idempotent signup/signin) + его workout (upload attachment + create; на 400 — id из списка).
- 3 независимых `test`'а (каждый: `authedPage(token)` → `openWorkoutDetail(id)` → `expect` canvas visible):
1. «duration row on section select»: canvas-click → `expect .area-data` visible → `expect` текст «Продолжительность» в `.area-data`.
2. «section zoom persists across tab switch»: canvas-click (zoom) → assert zoomed (x.min<full_min) → click «Дистанция» (кнопка active/primary) → **assert всё ещё zoomed** (x.min≈, не полный диапазон) → click «Время» → assert zoomed (симметрия).
3. «reset clears section on all tabs»: canvas-click (zoom) → assert zoomed → click `#zoom-botton` (reset) → assert NOT zoomed → click «Дистанция» → assert NOT zoomed (весь путь).
**Замечания:**
- Не `expect` внутри `page.evaluate` — только `return` данных наружу.
- `authedPage`: goto → set `localStorage.token` → goto (паттерн из `workout-upload-flow.spec.ts`).
- Node ≥20: запуск через nvm `export PATH="$HOME/.nvm/versions/node/v22.14.0/bin:$PATH"`.
- `yarn up` (`docker compose up -d --build`) → ждать backend: `until curl -sf http://localhost:8000/api/v0/html_test/login >/dev/null; do sleep 2; done` → `npx playwright test tests/section-duration.spec.ts`.
**Acceptance criteria:**
1. `../integration/tests/section-duration.spec.ts` создан.
2. `npx playwright test tests/section-duration.spec.ts` → **3 passed** на запущенном стеке.
3. Не сломать существующие спе (опционально: `npx playwright test --list`).
## TASK-SECTION-DURATION-ZOOM: план делегирования
**Scope:** 1 файл — `src/pages/workouts/components/WorkoutItem.vue` (компонент, НЕ страница `src/pages/workouts/WorkoutItem.vue`).
**Ключевые якоря (текущее состояние кода):**
- `times` (~строка 1061) — сырые ISO-метки времени всех точек (общие для всех метрик).
- `distances` (prop) — накопительные метры на точку.
- `activeSection = ref<number | null>(null)` (~строка 661) — номер секции, **зависит от `xAxisMode`** (потому что `sectionBounds` пересчитывается под режим).
- `computeAreaData(start, end)` (~строка 590) — считает средние по метрикам в блок «Данные участка» (`areaAvgData`).
- `switchXAxis(mode)` (~строка 1135) — сбрасывает `activeSection = null` (баг п.2).
- `onZoomComplete` (~строка 873) — `activeSection = null` + `computeAreaData(start,end)`.
- `onClick` (~строка 930) — выбор/тоггл секции.
- `resetChartZoom()` (~строка 1449) — reset.
- `setChartRef` (~строка 1126) — после сбора всех refs делает `group.charts = collectCharts()` (сигнал для re-zoom после remount).
- `secondsToDuration` — экспортируется из `../Definitions.vue` (не импортирован сейчас — добавить в импорт).
**Изменения:**
1. Заменить `activeSection: number|null` на `selectedRange: { startIdx: number; endIdx: number } | null` (единственный source of truth, **не зависит от режима** — индексы точек).
2. В `computeAreaData(start,end)`: добавить `avgData["Продолжительность"] = secondsToDuration(durSec)`, где `durSec = max(0, round((Number(new Date(times[end-1])) - Number(new Date(times[start])))/1000))`. Импортировать `secondsToDuration` из `../Definitions.vue`.
3. `onClick`: выбрать `selectedRange = {startIdx, endIdx}` по `sectionBounds`; тоггл-off когда `selectedRange` совпадает (startIdx== и endIdx==); выбрать иначе → `selectedRange`, `computeAreaData`, `broadcastZoom`.
4. `onZoomComplete`: `selectedRange = null` (ручной zoom снимает выбор), `computeAreaData(start,end)` сохранить.
5. `switchXAxis`: **НЕ** сбрасывать `selectedRange`; сохранить `areaAvgData`/`markedCoordinats` (они режим-независимы). Добавить `applySelectedRangeZoom()`: `labels = xAxisMode==='time' ? times : distances`; `toValue = time? Number(new Date(l)) : Number(l)`; `min=toValue(labels[startIdx])`, `max=toValue(labels[endIdx-1])`; `group.broadcastZoom(min,max)`. Вызвать `applySelectedRangeZoom()` в `setChartRef` после `group.charts = collectCharts()` (re-apply zoom в новом режиме после remount).
6. `resetChartZoom()`: `selectedRange = null` (чтобы после reset на другой вкладке тоже был весь путь), остальное как есть (`resetAll`, `areaAvgData={}`, `markedCoordinats=[]`).
**Почему работает «на всех вкладках»: ** reset обнуляет `selectedRange` → при смене вкладки zoom re-apply вызывается только если `selectedRange` set → после reset всегда весь путь.
**Acceptance criteria:**
1. `yarn build` (vue-tsc --noEmit) → exit 0.
2. `yarn lint` → 0 ошибок (или зафиксировать пре-экзистинг системный `tsutils.iterateComments`).
3. Ручной сценарий (MCP browser): выделить участок → в «Данные участка» есть «Продолжительность» → перейти на вкладку «Дистанция» → та же секция остаётся в zoom (не весь путь) → нажать reset → участок сброшен на всех вкладках.
## ✅ CODER SUCCESS REPORT — TASK-SECTION-DURATION-ZOOM
- `src/pages/workouts/components/WorkoutItem.vue`:
- импорт `secondsToDuration` добавлен в импорт из `../Definitions.vue` (рядом с `formatTime`).
- `activeSection: number|null` → `selectedRange: { startIdx: number; endIdx: number } | null` (единственный source of truth; индексы точек, не зависят от `xAxisMode`).
- `computeAreaData(start,end)`: в начале цикла добавлена строка `avgData["Продолжительность"] = secondsToDuration(durSec)`, где `durSec = max(0, round((Number(new Date(times[end-1])) - Number(new Date(times[start])))/1000))`.
- `onClick`: тоггл-off по совпадению `selectedRange.startIdx===startIdx && selectedRange.endIdx===endIdx`; иначе `selectedRange={startIdx,endIdx}` + `computeAreaData` + `broadcastZoom`.
- `onZoomComplete`: `selectedRange=null` (ручной zoom снимает выбор).
- `switchXAxis`: УБРАЛ сброс `activeSection` (выбор сохраняется); `areaAvgData`/`markedCoordinats` не трогаю (режим-независимы). Добавлена `applySelectedRangeZoom()` (labels = time? times : distances; broadcastZoom(min,max)).
- `setChartRef`: внутри `if (chartRefs.length === chartConfigs.value.length)` после `group.charts = collectCharts()` вызывается `applySelectedRangeZoom()` (re-apply zoom в новом режиме после remount).
- `resetChartZoom`: `selectedRange=null` (после reset на любой вкладке — весь путь).
- Verified: `yarn build` (vue-tsc --noEmit) → ✓ built in 7.14s, Done (0 type errors).
- Verified: `grep activeSection` → 0 совпадений; `grep selectedRange` → 9 совпадений; `grep secondsToDuration` → импорт + использование (2 совпадения).
- Note: `yarn lint` сломан системно — `tsutils.iterateComments is not a function` на всех 73 файлах (пре-экзистинг, дефект env eslint/typescript-estree, не от этих изменений).
- Browser verification (MCP ad-hoc): НЕ выполнена — dev server `:5173` и интеграционный стек `:8000` не запущены (не поднимал docker-стек ради ad-hoc проверки). Результат по критериям 1–4.
- ✅ Architect review: прочитаны все изменённые блоки (импорт 481, `computeAreaData` 599-606, `selectedRange` 670, `onClick` 997-1008, `onZoomComplete` 921, `switchXAxis` 1164-1173, `applySelectedRangeZoom` 1142-1153 + вызов в `setChartRef` 1161, `resetChartZoom` 1479). Корректность подтверждена: `times`/`distances`/`lineCoordinates` одной длины; re-apply zoom после remount через `setChartRef`; после reset `selectedRange=null` → на любой вкладке весь путь.
## TASK-PROFILE-ULID-BUG: план делегирования
**Scope:** `../backend/` (3 файла фикса + 1 новый тест).
**Изменения:**
1. `../backend/app/modules/users/domains/profiles.py:32` — `default=str(ULID())` → `default_factory=lambda: str(ULID())` (обязательный фикс).
2. `../backend/app/modules/users/domains/users.py:20` — та же замена (оборона в глубине).
3. `../backend/app/modules/users/repositories/profile.py` (`DBProfileRepository.update`, ~106-108) — после `del new_dict["created_at"]` добавить `new_dict.pop("id", None)` (PK не должен участвовать в UPDATE).
4. Новый тест `../backend/tests/test_api_profile.py` (паттерн `test_api_general.py`, фикстуры `client` + `signin_data` из `conftest.py`):
- signup → получить `profile_id` (из `data["profile"]["id"]`) + token;
- `PATCH /api/v0/profiles/{profile_id}` с `{"profile": {"first_name": "New", "surname": "Family"}}` → 200;
- `GET /api/v0/profiles/{profile_id}` → `profile.first_name == "New"` и `surname == "Family"`;
- второй PATCH (ещё раз) → 200 + персистится (ловит регрессию «один ULID на процесс»).
**Acceptance criteria:**
1. `./venv/bin/python -m pytest tests/test_api_profile.py -v` → passed.
2. `./venv/bin/python -m pytest tests/ -v` → green (зафиксировать diff, если есть пре-экзистинг failing).
3. `./venv/bin/python -m mypy app/modules/users/ --explicit-package-bases --no-error-summary` → 0 errors.
4. `ruff check` (по `ruff.toml`) → 0 errors.
**Не в scope (follow-up):** обновление `../integration/tests/workout-upload-flow.spec.ts` (добавить assert персистентного имени, убрать comment KNOWN BACKEND BUG) и `../integration/README.md` — отдельная мини-задача после success.
## ✅ CODER SUCCESS REPORT — TASK-PROFILE-ULID-BUG
- `../backend/app/modules/users/domains/profiles.py:32` — `default=str(ULID())` → `default_factory=lambda: str(ULID())` (ULID теперь вычисляется на каждый `Profile()`, а не один раз при импорте).
- `../backend/app/modules/users/domains/users.py:20` — та же замена (оборона в глубине).
- `../backend/app/modules/users/repositories/profile.py` (`DBProfileRepository.update`) — добавлено `new_dict.pop("id", None)` после `del new_dict["created_at"]` (PK не участвует в UPDATE — defensive).
- `../backend/tests/test_api_profile.py` — 2 API-теста: (1) PATCH с `{"profile": {"first_name": "New", "surname": "Family"}}` → 200 + GET → персистентность имени/фамилии; (2) повторные PATCH → 200 + GET `first_name == "Second"` (ловит регрессию «один ULID на процесс»).
- Verified: `./venv/bin/python -m pytest tests/test_api_profile.py -v` → **2 passed**.
- Verified: `./venv/bin/python -m pytest tests/ -v` → **56 passed, 10 errors** — все 10 errors **пре-экзистинг** (проверено через `git stash`: `NotImplementedError: No support for ALTER of constraints in SQLite dialect` в fixture-setup `test_db_profile.py`/`test_db_workout.py`/`test_db_workout_photos.py` — миграции на SQLite, не связаны с фиксом; до/после идентично).
- Verified: `./venv/bin/python -m mypy app/modules/users/ --explicit-package-bases --no-error-summary` → **0 errors**.
- Verified: `./venv/bin/python -m ruff check app/modules/users/ tests/test_api_profile.py` → **All checks passed** (полный `ruff check` по репо: единственная находка F401 в `migration/versions/2025-09-25-0000-...-hide-start-end.py:8` — пре-экзистинг, коммит `ff5f468`, вне scope).
- Follow-up (отдельная задача): update `../integration/tests/workout-upload-flow.spec.ts` (assert персистентного имени вместо KNOWN BACKEND BUG comment) + `../integration/README.md`.
## ✅ CODER SUCCESS REPORT — TASK-TEST-WORKOUT-UPLOAD
- `../integration/tests/workout-upload-flow.spec.ts` создан: 5 последовательных тестов (signup UI+профиль, upload .fit + resolve id, make public→GET public=200, hide_start_end→authed GET body true, add photo→img в `.workout-photos-grid`).
- Ключевое решение: **стабильный owner** (`itest-workout-owner@example.com`, idempotent signup/signin) для Части 2 — dedup по content-hash ГЛОБАЛЕН, а `GET /api/v0/workouts` scoped по `created_by`, поэтому уникальный юзер на повторном прогоне всегда падал бы в dedup-ветку с пустым своим списком. Part 1 (профиль) — уникальный юзер через UI (genuine signup).
- Детальный URL `/workouts/workouts/:id` (сверено с `router/index.ts`: вложенный `workouts` → `workouts/:id`); навигация `domcontentloaded` (Yandex map держит соединения, `networkidle` не стабилизируется).
- `../integration/README.md` обновлён (секция + notes про dedup и про backend-баг имени).
- Verified: `npx playwright test tests/workout-upload-flow.spec.ts` → **5 passed**, стабильно ×3 (dedup-путь) + прогон на свежей DB (redirect-путь). `yarn build` (vue-tsc --noEmit) exit 0.
- Cleanup: диагностический `../integration/tests/_diag.spec.ts` удалён; `npx playwright test --list` → 16 tests в 5 файлах, `_diag` отсутствует.
## ⚠️ Discovered (NOT fixed, out-of-scope test-only)
- **Backend-баг сменy имени:** `PATCH /api/v0/profiles/{id}` (ветка `profile`) возвращает 500. Причина: `../backend/app/modules/users/domains/profiles.py:32` — `id: str = field(default=str(ULID()), ...)` вычисляет ULID ОДИН раз при импорте (не `default_factory`), ORM-UPDATE переписывает PK → `IntegrityError: duplicate key (profiles_pkey)`. SPA показывает toast синхронно (до чтения ответа), поэтому имя НЕ персистится. Spec асертит SPA-toast (реальное UI-поведение), персистентное имя задокументировано, но не asserted.
- **Стек должен быть свежим:** колонка `hide_start_end` появляется только после `alembic upgrade head`. Перед прогоном спе — свежий стек / `docker compose up -d --force-recreate backend`.
- **`yarn lint` сломан системно** — `tsutils.iterateComments is not a function` (пре-экзистинг, падает и на нетронутые `.ts`, дефект env eslint/typescript-estree).
## ✅ CODER SUCCESS REPORT — TASK-HIDE-TRACK-3-FIX
- `src/pages/workouts/components/WorkoutItem.vue`:
- импорт `import { useI18n } from "vue-i18n"` + `const { t } = useI18n();` (рядом с `useToast`).
- template: чекбокс `hide_start_end` (v-model + `v-on:change="changeHideStartEnd(...)"`) после блока `is_public`.
- script: `changeHideStartEnd(value: boolean)` → `axiosAuth.patch('/api/v0/workouts/${workoutItem.id}', { hide_start_end: value })` (с guard `!workoutItem` и catch-тостом, как у `changePublic`).
- i18n: ключ `workout.hide_start_end` добавлен во ВСЕ 6 локали (`br, cn, es, gb, ir, ru`) — ранее отсутствовал (секция `workout` была создана заново).
- Verified: `yarn build` exit 0 (vue-tsc --noEmit 0 ошибок, vite ✓).
- Verified: `grep hide_start_end` → 4 совпадения; `grep useI18n` → 2 совпадения.
- Note: `yarn lint` сломан системно — `tsutils.iterateComments is not a function` на всех .vue файлах (существующий дефект окружения, не от этих изменений).
## ✅ CODER SUCCESS REPORT — TASK-HIDE-TRACK-T1
- `../backend/tests/test_api_workout.py`: добавлен `test_workout_hide_start_end` — проверяет PATCH `hide_start_end` (False→True→False) с GET-верификацией на каждом шаге.
- Verified: `./venv/bin/python -m pytest tests/test_api_workout.py::test_workout_hide_start_end -v` → **PASSED (1.92s)**
- Verified: `./venv/bin/python -m pytest tests/test_api_workout.py -v` → **4 passed (5.13s)**
## ✅ CODER SUCCESS REPORT — TASK-HIDE-TRACK-B3
- `../backend/app/web/v0/workout.py`: `WorkoutEditReq` + поле `hide_start_end: bool | None = Field(None)`; endpoint передаёт `hide_start_end=req.hide_start_end` в `workout_service.workout_edit(...)`.
- `../backend/app/modules/charts/services/workout.py`: `workout_edit` + параметр `hide_start_end: bool | None = None`; тело: `if hide_start_end is not None: params["hide_start_end"] = hide_start_end`.
- Verified: `./venv/bin/python -c "from app.web.v0.workout import workout_update; print('OK')"` → OK.
- Verified: `./venv/bin/python -m mypy app/web/v0/workout.py app/modules/charts/services/workout.py --explicit-package-bases --no-error-summary` → 0 errors.
## ✅ CODER SUCCESS REPORT — TASK-HIDE-TRACK-B2
- `../backend/migration/versions/2025-09-25-0000-a1b2c3d4e5f6-hide-start-end.py` создан: `revision='a1b2c3d4e5f6'`, `down_revision='b1c2d3e4f5a6'`; `upgrade()` → `op.add_column('workouts', sa.Column('hide_start_end', sa.Boolean(), server_default=sa.text('FALSE'), nullable=False))`; `downgrade()` → `op.drop_column(...)`. Без `IS_TEST`-guard (колонка нужна и в тестах).
- Verified: `python3 -c "import ast; ast.parse(...)"` → OK.
## ✅ CODER SUCCESS REPORT — TASK-HIDE-TRACK-4
- `src/pages/workouts/WorkoutPublicItem.vue`: импорт `trimTrack`; в `.then()` при `d.workoutItem.hide_start_end === true` — обрезка `lineCoordinates`/`distances`/`data` через `trimTrack(..., totalDist)` (totalDist = последний элемент `distances`). `mapCenter` остаётся неизменным.
- `src/pages/workouts/Definitions.vue`: добавлено поле `hide_start_end: boolean` в тип `WorkoutItem` (восстановлено — было потеряно при git stash).
- Приватный вид (`WorkoutItem.vue` страница) НЕ затронут.
- Verified: `yarn build` exit 0 (vue-tsc --noEmit 0 ошибок, vite ✓ 6.86s).
- Note: `yarn lint` → системная ошибка `tsutils.iterateComments is not a function` (касается ВСЕХ .vue файлов, не связано с изменениями — существующий дефект окружения lint-депенденций).
## ✅ CODER SUCCESS REPORT — TASK-TEST-AUTH
- `../integration/tests/auth-flows.spec.ts` создан: 4 теста (login success, login failure, signup+auto-login, logout)
- Self-seed via `POST /api/v0/signup`, Vuestic selectors (`input[type=email]`, `input[type=password]`)
- Verified: `npx playwright test tests/auth-flows.spec.ts` → **4 passed (19.2s)**
## TASK-TEST-PUBLIC: public-pages.spec.ts
**Цель:** Playwright spec для auth-сценариев (signup, login success/error, logout, validation).
**Файл для создания:** `../integration/tests/auth-flows.spec.ts`
**Конвенции (см. SKILL.md):**
- Self-seed user via `POST /api/v0/signup` in `beforeAll`
- Selectors: `input[type=email]`, `input[type=password]`, `getByRole('button', { name: 'Вход' })`
- Vuestic floating labels → select by input type, NOT placeholder
- Auth mock via `page.evaluate(() => localStorage.setItem(...))`
- Do NOT call `expect()` inside `page.evaluate()`
- Base URLs: `FRONTEND` (5173), `BACKEND` (8000)
- `freshPage()` helper: goto → clear localStorage → goto again
**Тесты в spec:**
1. **Signup**: `POST /api/v0/signup` → returns token, user, profile; navigate to `/auth/signup` → fill form → submit → redirect to `/explore`
2. **Login success**: pre-seed user via API → UI login → token in localStorage → redirect to `/explore`
3. **Login failure**: wrong password → error toast appears, stays on `/auth/login`
4. **Logout**: logged in → open profile dropdown → «Выход» → redirect to login, token cleared
**Acceptance Criteria:**
1. Spec file created at `../integration/tests/auth-flows.spec.ts`
2. All tests self-contained (self-seed data)
3. Follows existing `sidebar-auth-switch.spec.ts` patterns
4. `npx playwright test tests/auth-flows.spec.ts` passes against integration stack
## TASK-MCP-SETUP: Playwright MCP configuration
**Что сделано:**
- MCP-конфиг Zoo Code (`mcp_settings.json`): server `web-browser` → `@playwright/mcp@latest`, Node 22 via nvm absolute path, `--headless`
- Chromium `1234` уже установлен в `~/.cache/ms-playwright/`
- `.mcp.json` создан в проекте (project-level, для совместимости)
- `techContext.md` обновлён: секция "UI Verification (MCP Browser)"
**Как использовать (workflow):**
1. `yarn dev` — запустить dev server (порт 5173)
2. `browser_navigate` → `http://localhost:5173/explore` — проверить публичную страницу
3. `browser_evaluate` → `() => { localStorage.setItem('token', 'test'); localStorage.setItem('user', JSON.stringify({id:1,name:'Test'})); location.reload(); }` — сымитировать auth
4. `browser_navigate` → проверить auth-страницы (`/workouts`, `/workouts/:id`)
5. `browser_snapshot` — проверить DOM-структуру
6. `browser_take_screenshot` — визуальная проверка
7. `browser_network_requests` — проверить API-запросы
### Баг (контекст)
Сайдбар показывает `publicRoutes` (Лента + Маршрут) после логина до F5. Корень: [`NavigationRoutes.ts:70`](src/components/sidebar/NavigationRoutes.ts:70) — `routes` вычисляется один раз при module evaluation и кэшируется. TASK-AUTH-NAV-1 уже добавил реактивный `isAuthenticated` в `useGlobalStore` (инициализация из localStorage + `setAuthenticated` в Login/Logout). Эта задача переключает UI на computed.
### TASK-AUTH-NAV-2: реактивные маршруты (3 файла)
**1. `src/components/sidebar/NavigationRoutes.ts`**
- Сделать named exports: `export const authRoutes: INavigationRoute[]` и `export const publicRoutes: INavigationRoute[]` (сейчас это локальные `const`, не экспортированные).
- Убрать статическое поле `routes` из default export (или оставить default без `routes`, т.к. оба потребителя перейдут на computed). `INavigationRoute` интерфейс — не трогать.
- Важно: массивы `authRoutes`/`publicRoutes` остаются теми же по структуре/содержимому — только экспортируются.
**2. `src/components/sidebar/AppSidebar.vue`** (Options API, `setup()`)
- `import { useGlobalStore } from "../../stores/global-store";` + `import { authRoutes, publicRoutes } from "./NavigationRoutes";` (импорт `navigationRoutes` по default можно оставить/убрать — но все обращения `navigationRoutes.routes` заменить).
- В `setup()`: `const globalStore = useGlobalStore();`
- computed: `const routes = computed(() => globalStore.isAuthenticated ? authRoutes : publicRoutes);`
- В `return` заменить `navigationRoutes` на `routes` (тепловой шаблон уже использует `navigationRoutes.routes` → поправить на `routes.value`? НЕТ — в Options API computed из setup доступно в шаблоне как `routes` без `.value`, т.е. в шаблоне `navigationRoutes.routes` → `routes`).
- `setActiveExpand`: `navigationRoutes.routes.map(...)` → `routes.value.map(...)` (в setup `.value` нужен).
- Шаблоны: `v-for="(route, index) in navigationRoutes.routes"` → `in routes`.
**3. `src/components/app-layout-navigation/AppLayoutNavigation.vue`** (script setup)
- `import { authRoutes, publicRoutes } from "../sidebar/NavigationRoutes";` + `import { useGlobalStore } from "../../stores/global-store";`
- `const globalStore = useGlobalStore();`
- `findRouteName` (строка ~64): `traverse(NavigationRoutes.routes)` → `traverse(globalStore.isAuthenticated ? authRoutes : publicRoutes)`.
- Убедиться, что `items` computed (зависит от `route.matched` и `findRouteName`) остаётся реактивным — т.к. `findRouteName` читает `globalStore.isAuthenticated` внутри `items` computed, реactivity сохранится.
### Acceptance Criteria (TASK-AUTH-NAV-2)
1. `yarn lint` → 0 ошибок.
2. `yarn build` → 0 ошибок (vue-tsc --noEmit).
3. `NavigationRoutes.ts`: `authRoutes` и `publicRoutes` экспортируются; статического `routes` с localStorage-тернарником больше нет.
4. `AppSidebar.vue`: routes = computed от `globalStore.isAuthenticated`; `setActiveExpand` использует `routes.value`.
5. `AppLayoutNavigation.vue`: `traverse` использует выбор по `globalStore.isAuthenticated`.
6. **Ручной сценарий (ключевой)**: без F5 — выход из профиля → `/explore` → логин → сайдбар **сразу** показывает «Мои тренировки» (authRoutes). Аут → сайдбар сразу `publicRoutes`.
## ✅ CODER SUCCESS REPORT — TASK-AUTH-NAV-2
- `src/components/sidebar/NavigationRoutes.ts`: `export const authRoutes` / `export const publicRoutes`; удалён статический `routes: localStorage.getItem("token") ? ...` из default export (оставлен только `root`).
- `src/components/sidebar/AppSidebar.vue`: импорт `authRoutes, publicRoutes` + `useGlobalStore`; `computed(() => globalStore.isAuthenticated ? authRoutes : publicRoutes)`; `setActiveExpand` использует `routes.value.map`; шаблон `v-for="(route, index) in routes"`.
- `src/components/app-layout-navigation/AppLayoutNavigation.vue`: `const globalStore = useGlobalStore()`; `traverse(globalStore.isAuthenticated ? authRoutes : publicRoutes)` внутри `findRouteName` (вызывается в `items` computed → реactivity сохранена).
- Verified: `yarn lint` exit 0, `yarn build` exit 0 (vue-tsc --noEmit 0 ошибок, vite ✓ 7.07s).
## ✅ CODER SUCCESS REPORT — TASK-SSR-EXPLORE-CARDS
- `server/index.ts`: хэндлер `GET /explore` (~строки 135–228) переписан — `content` теперь `<section class="news-feed">` с `<article class="news-item">` на каждый workout: `.news-item-header` → `<h3><a href="/public/workouts/{id}">`, `ul.metadata` с 9 условными `<li>` в порядке клиента (Дата, Фото, Скорость, Пульс, Расстояние, Продолжительность, Каденс, Мощность, Ссылки), `.image-container` — `<a>` c `background-image` (только если `w.attachment?.url`). Имя/ссылки экранированы `escapeHtml`. SEO-мета (`title`/`description`/`canonical`/`jsonLd`) и `catch → 500` сохранены без изменений.
- `server/index.ts`: новый форматтер `distInt(m) = Math.round(m/1000)` (~строка 52, после `formatSpeed`) — расстояние в ленте целыми км (совпадение с клиентским `distConvert`); `formatDistance` (toFixed(1)) не тронут — используется в `/` и detail.
- `server/index.ts`: импорт `WorkoutItem` из `./api` добавлен.
- **`tsconfig.server.json` создан (был отсутствует)** — `yarn server:build` (`tsc -p tsconfig.server.json`) до этого падал «config file missing»; теперь `commonjs`/`outDir: dist-server`/`strict`, как ожидал `server:start` (`node dist-server/index.js`).
- `.gitignore`: добавлен `dist-server` (артефакт server:build).
- Verified: `yarn server:build` → Done (0 ошибок); `yarn build` (vue-tsc --noEmit + vite) → ✓ built in 6.58s (0 ошибок).
- Verified HTML (SSR dev, реальный production-backend): `curl http://localhost:3001/explore` — 3× `news-item`, 3× `metadata`, 3× `image-container`, 1× `news-feed`, `<a href="/public/workouts/{id}"`, метки Дата/Фото/Скорость/Пульс/Расстояние/Продолжительность/Каденс/Мощность. «Ссылки» корректно отсутствует: у всех 3 workout'ов ленты `external_links.values` пустые (v-if — 1:1 с клиентом).
- Verified browser (MCP): `browser_navigate http://localhost:3001/explore` → snapshot: 3 `article` c `<h3>`-заголовками-ссылками на `/public/workouts/{id}` + `list` c «Дата: …»; title «Публичные велотренировки — Cycle Rider». Консоль: только 3 pre-existing 404 на статические ассеты (в dev SSR `dist/` не сервится — в prod это делает nginx; app-ошибок нет).
- Known (вне scope): `yarn lint` (только `./src/**/*`) — 73 pre-existing errors, src не трогался.
- Note: в процессе проверка `git stash`/`pop` столкнулась с конкурентным обновлением `activeContext.md` (TASK-SECTION-DIST-FIX); состояние восстановлено, конфликтных потерь нет.
## История (закрытые задачи)
- **TASK-HIDE-TRACK-4**: success — `trimTrack` в `WorkoutPublicItem.vue` при `hide_start_end`; `hide_start_end` в типе `WorkoutItem` (Definitions.vue). Verified: `yarn build` 0. (Пред. active: TASK-TEST-PUBLIC → в очереди TASK-TEST-PROTECTED.)
- **TASK-AUTH-NAV-1**: success — `isAuthenticated` + `setAuthenticated` в `useGlobalStore`; Login `setAuthenticated(true)`, Logout `setAuthenticated(false)`. Verified lint/build 0.
- TASK-SSR-DETAIL: success — SSR /public/workouts/:id под DOM WorkoutItem.vue.
- TASK-404: success — HTTP 404 для неизвестных страниц.
- TASK-PUBLIC-ROUTE-FIX: success — workout_public_item в top-level router.
- TASK-F11/F12/F13/F14: success — SEO + фиксы карты тренировки.