20 KiB
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())+ defensivedel new_dict["id"]+ новый API-тест (PATCH → 200 + персистентность имени). - next task: —
TASK-PROFILE-ULID-BUG: план делегирования
Scope: ../backend/ (3 файла фикса + 1 новый тест).
Изменения:
../backend/app/modules/users/domains/profiles.py:32—default=str(ULID())→default_factory=lambda: str(ULID())(обязательный фикс).../backend/app/modules/users/domains/users.py:20— та же замена (оборона в глубине).../backend/app/modules/users/repositories/profile.py(DBProfileRepository.update, ~106-108) — послеdel new_dict["created_at"]добавитьnew_dict.pop("id", None)(PK не должен участвовать в UPDATE).- Новый тест
../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 на процесс»).
- signup → получить
Acceptance criteria:
./venv/bin/python -m pytest tests/test_api_profile.py -v→ passed../venv/bin/python -m pytest tests/ -v→ green (зафиксировать diff, если есть пре-экзистинг failing)../venv/bin/python -m mypy app/modules/users/ --explicit-package-bases --no-error-summary→ 0 errors.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 + GETfirst_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-setuptest_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/workoutsscoped по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 buildexit 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— проверяет PATCHhide_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 buildexit 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/signupinbeforeAll - 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()insidepage.evaluate() - Base URLs:
FRONTEND(5173),BACKEND(8000) freshPage()helper: goto → clear localStorage → goto again
Тесты в spec:
- Signup:
POST /api/v0/signup→ returns token, user, profile; navigate to/auth/signup→ fill form → submit → redirect to/explore - Login success: pre-seed user via API → UI login → token in localStorage → redirect to
/explore - Login failure: wrong password → error toast appears, stays on
/auth/login - Logout: logged in → open profile dropdown → «Выход» → redirect to login, token cleared
Acceptance Criteria:
- Spec file created at
../integration/tests/auth-flows.spec.ts - All tests self-contained (self-seed data)
- Follows existing
sidebar-auth-switch.spec.tspatterns npx playwright test tests/auth-flows.spec.tspasses against integration stack
TASK-MCP-SETUP: Playwright MCP configuration
Что сделано:
- MCP-конфиг Zoo Code (
mcp_settings.json): serverweb-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):
yarn dev— запустить dev server (порт 5173)browser_navigate→http://localhost:5173/explore— проверить публичную страницуbrowser_evaluate→() => { localStorage.setItem('token', 'test'); localStorage.setItem('user', JSON.stringify({id:1,name:'Test'})); location.reload(); }— сымитировать authbrowser_navigate→ проверить auth-страницы (/workouts,/workouts/:id)browser_snapshot— проверить DOM-структуруbrowser_take_screenshot— визуальная проверка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).- Убедиться, что
itemscomputed (зависит отroute.matchedиfindRouteName) остаётся реактивным — т.к.findRouteNameчитаетglobalStore.isAuthenticatedвнутриitemscomputed, реactivity сохранится.
Acceptance Criteria (TASK-AUTH-NAV-2)
yarn lint→ 0 ошибок.yarn build→ 0 ошибок (vue-tsc --noEmit).NavigationRoutes.ts:authRoutesиpublicRoutesэкспортируются; статическогоroutesс localStorage-тернарником больше нет.AppSidebar.vue: routes = computed отglobalStore.isAuthenticated;setActiveExpandиспользуетroutes.value.AppLayoutNavigation.vue:traverseиспользует выбор поglobalStore.isAuthenticated.- Ручной сценарий (ключевой): без 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(вызывается вitemscomputed → реactivity сохранена).- Verified:
yarn lintexit 0,yarn buildexit 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 build0. (Пред. active: TASK-TEST-PUBLIC → в очереди TASK-TEST-PROTECTED.) -
TASK-AUTH-NAV-1: success —
isAuthenticated+setAuthenticatedвuseGlobalStore; LoginsetAuthenticated(true), LogoutsetAuthenticated(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 + фиксы карты тренировки.