# Active Context — Strava Frontend ## Task State - task_id: TASK-PROFILE-ULID-BUG - status: success - summary: **PATCH /profiles/{id} возвращает 500 (смена имени не персистится). Root cause: `default=str(ULID())` в `profiles.py:32` и `users.py:20` — ULID считается один раз при импорте; `DBProfileRepository.update` включает `id` в SET → `IntegrityError`. Фикс: `default_factory=lambda: str(ULID())` + defensive `del new_dict["id"]` + новый API-тест (PATCH → 200 + персистентность имени).** - next task: — ## 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 + фиксы карты тренировки.