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

20 KiB
Raw Blame History

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 — 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 + фиксы карты тренировки.