# Active Context — Strava Frontend ## Task State - task_id: TASK-SECTION-DIST-FIX - status: success - summary: **Добавлена строка «Дистанция» в «Данные участка» + drag-zoom теперь персистится при смене вкладки (selectedRange вместо null в onZoomComplete). 3 passed.** - next task: — ## ✅ 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/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(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). ## История (закрытые задачи) - **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 + фиксы карты тренировки.