# Progress — Strava Frontend
## 2026-09-26 — TASK-INTEG-SECTION + TASK-SECTION-BUGS: интеграционный тест section-duration + фикс 2 багов
- `../integration/tests/section-duration.spec.ts` (new): 3 Playwright-теста
(duration row on section select; zoom persist across Время↔Дистанция;
reset clears on all tabs). Self-contained: stable owner
`itest-section-owner@example.com` (idempotent signup/signin), API-seed
workout (upload + create, dedup-fallback → id из `GET /workouts`[0]),
Chart.js access через `canvas.__vueParentComponent.exposed.chart`
(глобального `window.Chart` в SPA нет); zoom-детект по
`options.scales.x.min/max` vs полный диапазон (`isZoomedOrPanned()`
всегда false — zoom применяется прямой записью, минуя плагин).
- **Bug 1** (`src/pages/workouts/components/WorkoutItem.vue`,
`setChartRef` ~строка 1155): ref пушится сразу (guard `r.chart` убран —
Chart.js создаёт инстанс асинхронно); финализация
(`group.charts = collectCharts()` + `applySelectedRangeZoom()`) через
`tryFinalize(retries)` — retry через `nextTick` (до 20 попыток) до тех
пор, пока все рефы не получат `chart`. `nextTick` добавлен в импорт из
"vue". Корень бага: на remount после `switchXAxis` `setChartRef`
срабатывал до создания chart → `chartRefs` не пополнялся →
`applySelectedRangeZoom()` не вызывался → zoom терялся.
- **Bug 2** (`src/pages/workouts/components/ChartGroup.ts`, `resetAll`
~строка 125): после `resetZoom(chart)` явный сброс
`options.scales.x.min/max = undefined` + `chart.update("none")`
(broadcastZoom писал min/max напрямую, минуя внутреннее состояние
плагина, поэтому `resetZoom` один ничего не сбрасывал). Дополнительная
корневая причина: `resetZoom` триггерит `onZoomComplete`, который
повторно разносит ещё-зумированный диапазон на остальные чарты
(перетирая сброшенный) — в `resetAll` на время цикла
`this.broadcasting = true` (try/finally), guard `isBroadcasting()`
в `onZoomComplete` отбрасывает эти колбэки.
- Verified: `npx playwright test tests/section-duration.spec.ts` →
**3 passed** (56s, docker-стек); `yarn build` (vue-tsc --noEmit +
vite) → exit 0.
- Known: диагностический `../integration/tests/chartprobe.spec.ts` не
удалён (rm-команда отклонена окружением) — удалить вручную.
## 2026-09-26 — TASK-SECTION-DURATION-ZOOM: «Продолжительность» + сохранение выбора участка при смене вкладки
- `src/pages/workouts/components/WorkoutItem.vue`:
- импорт `secondsToDuration` из `../Definitions.vue`.
- `activeSection: number|null` → `selectedRange: { startIdx, endIdx } | null` (режим-независимый source of truth, индексы точек).
- `computeAreaData`: строка `avgData["Продолжительность"] = secondsToDuration(durSec)` по `times[start]`/`times[end-1]`.
- `onClick`: тоггл-off по совпадению индексов (`startIdx` и `endIdx`); иначе `selectedRange={startIdx,endIdx}` + `computeAreaData` + `broadcastZoom`.
- `onZoomComplete`: `selectedRange = null` (ручной zoom снимает выбор).
- `switchXAxis`: убран сброс выбора; добавлена `applySelectedRangeZoom()`; `areaAvgData`/`markedCoordinats` не сбрасываются.
- `setChartRef`: `applySelectedRangeZoom()` после `group.charts = collectCharts()`.
- `resetChartZoom`: `selectedRange = null` (после reset — весь путь на всех вкладках).
- Verified: `yarn build` → ✓ built in 7.14s (vue-tsc --noEmit 0 ошибок).
- Verified: `grep activeSection` → 0 совпадений; `grep selectedRange` → 9; `grep secondsToDuration` → 2 (импорт + использование).
- Known issue: `yarn lint` сломан системно — `tsutils.iterateComments is not a function` на всех 73 файлах (дефект окружения eslint/typescript-estree).
- Browser (MCP ad-hoc): не выполнена — dev server `:5173` и интеграционный стек `:8000` не запущены (не поднимал docker-стек ради ad-hoc проверки). Результат по критериям 1–4.
## 2026-09-25 — TASK-PROFILE-ULID-BUG: фикс PATCH /profiles 500 (смена имени не персистится)
- `../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`: `new_dict.pop("id", None)` (PK вне UPDATE).
- `../backend/tests/test_api_profile.py` — 2 теста (персистентность имени + повторные PATCH).
- Verified: `pytest tests/test_api_profile.py -v` → 2 passed; `pytest tests/ -v` → 56 passed, 10 errors (все 10 пре-экзистинг: SQLite alembic ALTER-constraints в db-fixture setup, подтверждено git stash); `mypy app/modules/users/ --explicit-package-bases --no-error-summary` → 0; `ruff check app/modules/users/ tests/test_api_profile.py` → 0.
- Фикс закрывает known issue из TASK-TEST-WORKOUT-UPLOAD (backend-баг имени).
## 2026-09-25 — TASK-TEST-WORKOUT-UPLOAD: интеграционный тест загрузки тренировки (Playwright)
- `../integration/tests/workout-upload-flow.spec.ts` — 5 последовательных тестов полного user-journey: (1) signup через UI + профиль (аватар-загрузка toast «Фото успешно загружено!», смена имени toast «Вы успешно изменили имя!»), (2) загрузка `.fit` на `/workouts/upload` + resolve id (redirect ИЛИ dedup-фолбэк через owner's `GET /api/v0/workouts`), (3) «Сделать публичной» → `GET /api/v0/public/workouts/{id}`=200, (4) «Скрыть начало и конец трека» → authed detail `workout.hide_start_end===true`, (5) «Добавить фото» → `` в `.workout-photos-grid`.
- Ключевое: **стабильный owner**-юзер для Части 2 (глобальный content-hash dedup vs scoped `GET /workouts`); unique-юзер через UI для Части 1. Detail URL `/workouts/workouts/:id`, nav `domcontentloaded`.
- `../integration/README.md` — секция про спек + notes (workout-dedup, backend-баг имени).
- Cleanup: `../integration/tests/_diag.spec.ts` (диагностический) удалён.
- Verified: `npx playwright test tests/workout-upload-flow.spec.ts` → **5 passed** (×3 dedup-путь + 1 свежая DB redirect-путь); `yarn build` exit 0; `npx playwright test --list` → 16 tests / 5 files (без `_diag`).
- Known issues (не фикс, out-of-scope): backend `PATCH /profiles` (имя) → 500 из-за `Profile.id = field(default=str(ULID()))` (не `default_factory`) → имя не персистится, SPA toast ложный; `yarn lint` системно сломан (`tsutils.iterateComments`); перед прогоном спе нужен свежий стек / force-recreate backend (миграция `hide_start_end`).
## 2026-09-25 — TASK-HIDE-TRACK-3-FIX: восстановление чекбокса hide_start_end в WorkoutItem.vue
- `src/pages/workouts/components/WorkoutItem.vue` — восстановлены (потеряны в git stash при TASK-HIDE-TRACK-4):
- `import { useI18n } from "vue-i18n"` + `const { t } = useI18n();`
- template: чекбокс `hide_start_end` после блока `is_public`
- `changeHideStartEnd(value: boolean)` → PATCH `hide_start_end` (guard + catch-тост, паттерн `changePublic`)
- i18n: секция `workout` + ключ `hide_start_end` добавлены во все 6 локалей (`br, cn, es, gb, ir, ru`) — секция отсутствовала.
- Verified: `yarn build` exit 0 (vue-tsc 0 errors, vite ✓ 7.09s).
- Known issue: `yarn lint` сломан системно — `tsutils.iterateComments is not a function` (дефект окружения).
## 2026-09-25 — TASK-HIDE-TRACK-B3: hide_start_end в PATCH endpoint + service (backend)
- `../backend/app/web/v0/workout.py` — `WorkoutEditReq` + `hide_start_end: bool | None = Field(None)`; PATCH `/workouts/{workout_id}` передаёт `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` перед `repository.update`.
- 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.
## 2026-09-25 — TASK-HIDE-TRACK-B2: Alembic migration hide_start_end (backend)
- `../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('workouts', 'hide_start_end')`. Без `IS_TEST`-guard (это колонка, не index/FK — нужна в тестовой БД тоже).
- Verified: `python3 -c "import ast; ast.parse(open(...).read())"` → OK (exit 0).
## 2026-09-25 — TASK-HIDE-TRACK-4: trimTrack в публичном виде тренировки
- `src/pages/workouts/WorkoutPublicItem.vue` — импорт `trimTrack` (`./components/TrimTrack`); в `.then()` `initWorkout`: при `d.workoutItem.hide_start_end` → `trimTrack(d.lineCoordinates, d.distances, d.data, totalDist)`, где `totalDist = d.distances[d.distances.length - 1] ?? 0`; иначе значения присваиваются как есть. `mapCenter` не меняется. Приватный вид не затронут.
- `src/pages/workouts/Definitions.vue` — `WorkoutItem` + поле `hide_start_end: boolean` (восстановлено после потери в git stash).
- Verified: `yarn build` exit 0 (vue-tsc 0 errors, vite ✓ 6.86s).
- Known issue: `yarn lint` сломан системно — `tsutils.iterateComments is not a function` на всех .vue файлах (дефект окружения, не от этих изменений).
## 2026-09-24 — UI-testing skill + mandatory rule
- `.roo/skills/ui-testing/SKILL.md` — how to test UI via (A) MCP browser ad-hoc and (B) the Playwright integration stand: run commands, spec conventions, selectors, gotchas.
- `.roo/rules/ui-testing.md` — MANDATORY rule: after any `src/` change affecting a visible route/interaction the task is not done until `yarn lint` + `yarn build` + browser verification (MCP ad-hoc for quick/visual, Playwright spec for must-not-regress) + green `npx playwright test`. Includes run cheat-sheet and required reporting format.
## 2026-09-24 — Integration UI-test stand (Playwright + docker-compose)
- Created `../integration/` (sibling of `frontend/`, `backend/`):
- `docker-compose.yml` — 3 services: `db` (postgres:15, host:5433, schema `strava` via `db-init/01-schema.sql`), `backend` (python:3.12, `pip install -r requirements.txt` + `alembic upgrade head` + `python -m app.web.__main__`, port 8000, `DB_URI=postgresql+asyncpg://svcuser:svcpass@db:5432/svc`), `frontend` (node:20-alpine, `yarn install --frozen-lockfile --ignore-scripts` + `yarn dev --host 0.0.0.0 --port 5173`, env `VITE_HOST=http://localhost:8000`, `HUSKY=0`).
- `package.json` + `playwright.config.ts` (`@playwright/test` ^1.54, baseURL `FRONTEND_URL||localhost:5173`, `API_URL`).
- `tests/sidebar-auth-switch.spec.ts` — TASK-AUTH-NAV regression: publicRoutes (no «Тренировки») → UI login (`/auth/login`, `input[type=email]` + `input[type=password]` + button «Вход») → authRoutes («Тренировки» appears, no reload) → UI logout (profile dropdown → «Выход») → publicRoutes. Self-seeds a unique user via `POST /api/v0/signup` in `beforeAll`.
- `README.md` — run instructions (`yarn up`, wait for backend, `yarn test`).
- Frontend change: `src/main.ts` `HOST = import.meta.env.VITE_HOST || "https://cycle-rider.ru"` (production unaffected — env unset → falls back to prod host).
- Verified: full stack up (frontend 200, backend 200 on `/api/v0/html_test/login`, db healthy); `npx playwright test tests/sidebar-auth-switch.spec.ts` → **1 passed (9.9s)** on Node 22 (nvm `~/.nvm/versions/node/v22.14.0/bin`).
- Gotchas: (1) npm install in frontend container fails on `@unhead/vue` peer-dep (wants vite>=6, project vite 5) → must use **yarn** (`yarn.lock`), and `node:20-alpine` already ships yarn (do NOT `npm i -g yarn` → EEXIST); (2) backend health path is `/api/v0/html_test/login` (router_public prefix `/api/v0`, no `/auth`); (3) Vuestic `VaInput` uses floating labels → no usable placeholder, select login fields by `input[type=email]` / `input[type=password]`; (4) Playwright/MCP need Node ≥20 (system node is 18) → run via nvm Node 22.
## 2026-09-24 — TASK-AUTH-NAV-2: реактивные маршруты в сайдбаре и хлебных крошках
- `NavigationRoutes.ts`: named exports `authRoutes`/`publicRoutes`, удалён module-level `localStorage.getItem("token")` тернарник.
- `AppSidebar.vue`: computed `routes` от `globalStore.isAuthenticated`; `AppLayoutNavigation.vue`: `traverse` по `globalStore.isAuthenticated ? authRoutes : publicRoutes`.
- Verified: `yarn lint` exit 0, `yarn build` exit 0 (vue-tsc 0 ошибок).
## 2026-09-24 — TASK-AUTH-NAV-1: auth-состояние в Pinia (реактивный isAuthenticated)
- `src/stores/global-store.ts` — state: `isAuthenticated` (SSR-guard `typeof window !== "undefined"`), action `setAuthenticated(v: boolean)`.
- `src/pages/auth/Login.vue` — `import { useGlobalStore }`; в `.then` после `localStorage.setItem(...)` и до `push({ name: "explore" })`: `globalStore.setAuthenticated(true)`.
- `src/pages/auth/Logout.vue` — `import { useGlobalStore }`; после `localStorage.clear()` и до `push({ name: "login" })`: `globalStore.setAuthenticated(false)`.
- Verified: `yarn lint` exit 0, `yarn build` exit 0 (vue-tsc 0 errors, vite ✓ 7.17s).
## 2026-09-19 — TASK-404: HTTP 404 для несуществующих страниц (SEO)
- `server/index.ts` — catch-all 404 handler (`app.use` без path, перед `app.listen`): GET → `renderTemplate` (SEO-мета, canonical `/404`), non-GET → `res.status(404).send("Not Found")`.
- `nginx.conf` — SPA whitelist location `~ ^/(workouts|auth|preferences|404)(/|$)` → `try_files $uri /index.html`; catch-all `location /` → `proxy_pass http://127.0.0.1:3001` (SSR 404 для неизвестных URL).
- `src/router/index.ts` — catch-all redirect: `explore` → `404`.
- `src/router/seo.ts` — `SEO_MAP["404"]` добавлен.
- Verified: `yarn lint` exit 0, `yarn build` exit 0 (vue-tsc 0, vite ✓ 7.02s).
## 2026-09-19 — TASK-F11: SEO @unhead/vue dynamic title + meta description per route
- `package.json` — `@unhead/vue` ^3.4.1 added to dependencies (`npm install @unhead/vue --legacy-peer-deps`; vite transitively bumped 4.5.5→5.4.21).
- Created `src/router/seo.ts` — `setupSeo(router, head: Unhead)`: `SEO_MAP` keyed by route `name` (+ `""` for `/`), `DEFAULT_SEO` fallback, `head.push({ title, meta: [{ name: "description", content }] })` + `router.afterEach(() => update())`. Covers: `""`, `explore`, `routes`, `list_workouts`, `upload_workouts`, `workout_item`, `workout_public_item`, `preferences`, `login`, `signup`, `recover-password`.
- `src/main.ts` — `import { createHead } from "unhead/client"`, `import { headSymbol } from "@unhead/vue"`; `const head = createHead(); app.provide(headSymbol, head); setupSeo(router, head);` right after `app.use(router)`. (NOT `app.use(head)` — `ClientUnhead` is not a Vue plugin; `@unhead/vue` v3 installs via `app.provide(headSymbol, head)`.)
- `index.html` — `
` set to «Cycle Rider — платформа для анализа велотренировок: мощность, пульс, скорость, каденс»; added `` in `` as fallback; moved Yandex.Metrika counter block from `` to top of `` (Vite 5 parse5 rejects `