206 lines
20 KiB
Markdown
206 lines
20 KiB
Markdown
# 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 + фиксы карты тренировки.
|