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

335 lines
40 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Progress — Strava Frontend
## 2026-09-26 — TASK-SECTION-DIST-FIX: строка «Дистанция» + drag-zoom персистится при смене вкладки
- `src/pages/workouts/components/WorkoutItem.vue`:
- `computeAreaData`: добавлена строка `avgData["Дистанция"]` =
`distances[end-1] - distances[start]` (формат: >=1000 → "X.X км",
иначе "X м"). Расположена после «Продолжительность», перед циклом
по `chartConfigs`.
- `onZoomComplete`: `selectedRange.value = null` →
`selectedRange.value = { startIdx: start, endIdx: end }` — drag-zoom
теперь сохраняет диапазон индексов, и `applySelectedRangeZoom()`
(из `setChartRef` на remount) повторно применяет zoom после смены
вкладки.
- Verified: `yarn build` exit 0; `npx playwright test tests/section-duration.spec.ts` → 3 passed.
## 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) «Добавить фото» → `<img>` в `.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` — `<title>` set to «Cycle Rider — платформа для анализа велотренировок: мощность, пульс, скорость, каденс»; added `<meta name="description">` in `<head>` as fallback; moved Yandex.Metrika counter block from `<head>` to top of `<body>` (Vite 5 parse5 rejects `<div>/<img>` inside `<noscript>` within `<head>` → `disallowed-content-in-noscript-in-head`).
- Verified: `npm run build` exit 0 (vue-tsc 0 errors, vite build ✓ 6.65s); `npm run lint` exit 0.
- Tech note: `@unhead/vue` v3 API — `createUnhead` is in `unhead` core (`createHead` from `unhead/client` adds DOM renderer); the `Unhead` type is exported from `unhead/types`. `@unhead/vue` itself only re-exports `useHead`/`useSeoMeta`/`injectHead`/`headSymbol` — no `createHead` from it.
## 2026-09-19 — TASK-PUBLIC-ROUTE-FIX: workout_public_item moved to top-level (no auth)
- `src/router/index.ts`: removed nested `workout_public_item` from `workouts` children; added top-level route `/public/workouts/:id` before catch-all, outside AppLayout.
- Result: `/public/workouts/:id` accessible without auth, aligns with SSR endpoint + nginx proxy.
- Verified: `yarn lint` exit 0, `yarn build` exit 0.
## 2026-09-18 — TASK-SSR-3: Sitemap, robots.txt, nginx, Dockerfile, guards
- Modified: `server/index.ts` (+49 lines: `GET /sitemap.xml` — XML with 3 static URLs + workout URLs from API, `Content-Type: application/xml`; `GET /robots.txt` — Disallow `/auth/ /workouts/ /preferences/ /admin/`, Sitemap line, `Content-Type: text/plain`).
- Modified: `nginx.conf` — full rewrite: SSR proxy locations for `/explore|/routes|/sitemap.xml|/robots.txt` and `/public/workouts/`; `location = /` with `hascookie token` (no cookie → proxy, cookie → static); `/assets/` with 30d immutable cache; SPA fallback `try_files $uri /index.html`.
- Modified: `Dockerfile` — multi-stage (node:20-alpine): build stage (yarn install + yarn build), runtime stage (nginx + node, copies dist/, server/, node_modules/, package.json; CMD `npx tsx server/index.ts & nginx -g 'daemon off;'`).
- Modified: `run.sh` — `cd /app && npx tsx server/index.ts & nginx -g 'daemon off;'`.
- Modified: `package.json` — `tsx` moved from devDependencies to dependencies.
- Modified: `src/stores/user-store.ts` — `typeof window === "undefined"` guard in `state()` (returns empty state on SSR).
- Modified: `src/main.ts` — `typeof window !== "undefined" &&` guard before `localStorage.getItem("token")` (line 110).
- Verified: `yarn lint` exit 0, `yarn build` exit 0 (vue-tsc 0 errors, vite 6.68s). Curl: `/robots.txt` → 200 + correct text; `/sitemap.xml` → 200 + valid XML (3 static + 2 workout URLs); `/health` → ok.
## 2026-09-18 — TASK-SSR-2: SSR route handlers
- Modified: `server/index.ts` — 4 route handlers: `GET /` (landing, 302→/explore if `token` cookie), `GET /explore` (public feed via `getPublicWorkouts()`), `GET /public/workouts/:id` (detail via `getPublicWorkout()`, 404 on API 404, 500 on other errors), `GET /routes` (static SEO). `app.disable("x-powered-by")`. Helpers: `getAssetTagsSafe()` (falls back to dev script tag if no `dist/index.html`), `escapeHtml`, `formatDate` (DD.MM.YYYY), `formatDuration` (X ч. Y мин.), `formatDistance` (m→km 1 decimal), `formatSpeed` (m/s→km/h rounded). Canonical base: `SSR_BASE_URL` || `https://cycle-rider.ru`.
- Verified: `yarn lint` exit 0, `yarn build` exit 0 (vue-tsc 0 errors). Curl: `/` → `<h1>Cycle Rider — анализ велотренировок</h1>`; `/routes` → `<h1>Конструктор маршрутов</h1>`; `/public/workouts/test` → 500 (API unreachable/404→500 acceptable locally); `/explore` → 200; `/` with `token` cookie → 302 → `/explore`; `/health` → ok.
## 2026-09-18 — TASK-SSR-1: SSR server skeleton
- Created: `server/index.ts`, `server/template.ts`, `server/api.ts`.
- Modified: `package.json` (+express, +@types/express, +tsx, +3 scripts).
- `tsconfig.json` unchanged (server/ already excluded via `include` glob).
- Verified: `yarn lint` exit 0, `yarn build` exit 0, `npx tsx server/index.ts` + curl /health OK.
## What works (verified at Memory Bank init)
- `yarn dev` / `yarn build` / `yarn lint` scripts defined; build runs `vue-tsc --noEmit` type-check.
- App boots: axios providers, Pinia, router, i18n, Vuestic, Yandex Maps, optional GTM (`src/main.ts`).
- Auth flow: JWT Bearer interceptor, 401 -> logout redirect, startup `/api/v0/auth/check`.
- Pages: workout feed (dashboard), workout list/detail/public detail, upload, routes, preferences, auth pages, 404.
- Charts (Chart.js) and Yandex Maps route rendering for workout data.
## Known issues / tech debt (observed, not yet fixed)
- `HOST`, Yandex Maps API key and GTM keys hardcoded in `src/main.ts` (no env-driven base URL).
- `GetWorkout.ts` uses mutable module-level `let` variables shared across calls (state leakage risk) — refactor to pure functions returning state if touched.
- `user-store.ts` embeds a huge base64 avatar blob in source.
- No test suite; no global router auth guard (relies on 401 interceptor).
- Mixed callback (`.then/.catch`) and imperative styles in pages.
- ~~**BLOCKER (2026-09-04, TASK-DEPS-UPDATE-W1)**~~ — **RESOLVED by W0a (build) + W0b (lint)**: both `yarn build` and `yarn lint` are green; W1/W2/W3 unblocked.
- `vue/no-mutating-props` in `workouts/components/WorkoutItem.vue` (checkbox/name edits mutate the prop object in place) — suppressed file-targeted in `eslint.config.mjs`; proper fix = local state extraction, needs its own task.
- `src/main.ts` axios interceptors loosely typed (`any`) — suppressed file-targeted; pre-existing documented debt.
- ~~**`typescript` capped at 5.4.5** (TASK-DEPS-UPDATE-W1)~~ — **RESOLVED by W2b**: `typescript` now 5.8.3 (`"5.8"` pinned no-caret in package.json), `vue-tsc` 2.2.12 (`^2`). vue-tsc 2.x officially supports TS up to 5.8 — do NOT go to TS 5.9.
- **`sass` left at 1.69.5** (TASK-DEPS-UPDATE-W1): latest 1.x (1.104.0) requires node ≥20.19.0. Docker is now node:22, so a sass bump is viable — verify the local dev node version first; small follow-up task.
- **Stale `package-lock.json`** in repo (npm workflow abandoned for yarn in W2c-FIX) — remove as a small cleanup task.
- **yarn v1 nested-stale dirs**: after major bumps, `node_modules` can keep leftover nested packages (observed: `@typescript-eslint/*@7` under `typescript-eslint/`) causing transient CJS resolution glitches — fix with a clean `rm -rf node_modules && yarn install`; not reproducible from the lockfile.
- Installed yarn 1.22.22 binary lacks the `up` alias — use `yarn upgrade` (verified equivalent).
## Milestones
### 2026-09-19 — SSR PHASE COMPLETE (TASK-SSR-0 → SSR-3 + PUBLIC-ROUTE-FIX)
- **SSR-0**: Renamed `/dashboard` → `/explore` across router, nav, auth pages, layout guard. Legacy redirect `/dashboard` → `explore`.
- **SSR-1**: Created `server/` (Express on :3001): `index.ts` (app + /health), `template.ts` (SEO HTML shell with JSON-LD, OG, canonical), `api.ts` (getPublicWorkouts/getPublicWorkout).
- **SSR-2**: 4 SSR route handlers: `GET /` (landing/302), `GET /explore`, `GET /public/workouts/:id`, `GET /routes`. Helpers for formatting.
- **SSR-3**: `GET /sitemap.xml`, `GET /robots.txt`. Nginx rewrite (SSR proxy + SPA fallback). Dockerfile multi-stage (node:20-alpine + nginx). tsx → dependencies. SSR guards in user-store/main.ts.
- **PUBLIC-ROUTE-FIX**: `workout_public_item` moved to top-level router (outside AppLayout) — accessible without auth, aligns with SSR + nginx.
- **Final state**: SSR renders `/`, `/explore`, `/routes`, `/public/workouts/:id`, `/sitemap.xml`, `/robots.txt` with full SEO meta + JSON-LD. Nginx proxies these to :3001; all else → SPA. Docker: multi-stage, `npx tsx server/index.ts & nginx`.
- Verified: lint 0, build 0, docker build 0, curl all SSR endpoints 200.
### 2026-09-05 — TASK-DEPS-UPDATE-W0a: baseline `yarn build` green (uncommitted)
- Replaced `// @ts-ignore` with non-null assertions (`data!.labels!`) in `LineWithLineChart.ts` `draw()` — eliminates the latent TS2532 and the `ban-ts-comment` lint error for that file.
- Verified: `vue-tsc --noEmit` 0 errors, `vite build` OK (1309 modules).
- The two SFC "parsing errors" were confirmed eslint-only (vite/vue-tsc parse both files fine) — deferred to W0b along with remaining lint errors.
- Note: on the clean tree the build was already green (TS2532 was suppressed by the `@ts-ignore`); the fix is still required because stricter TS/vue-tsc in W2 would re-surface it without the suppression.
### 2026-09-05 — TASK-DEPS-UPDATE-W0b: `yarn lint` green (uncommitted)
- Rewrote `eslint.config.mjs` (flat config):
- TS parser wired into `.vue` files (`languageOptions.parserOptions.parser = tsParser` from `typescript-eslint` CJS default import) — this was the root cause of all ~20 SFC parse errors (vue flat preset leaves espree as inner parser).
- `@typescript-eslint/no-unused-vars` global `argsIgnorePattern: "^_"`.
- File-targeted overrides only (no inline eslint-disable anywhere): `src/main.ts` (any + unused, documented exception), `src/pages/**/*.vue` (multi-word names), `Logout.vue` (valid-template-root), `components/WorkoutItem.vue` (no-mutating-props).
- Code fixes (21 files, type-only / dead-code / unused-cleanup): unused import in `router/index.ts`; `any`→`unknown`/`Event`/structural types across `services/utils.ts`, auth pages, workouts pages, `AppLayoutNavigation.vue`; `@ts-ignore`→`@ts-expect-error` in `LineWithLineChart.ts`; removed dead code (`VuesticLogo` computed, `PreferencesHeader.readFile`, `Login.HOST`, `AppNavbarActions.t`, `Logout.push`); removed redundant `v-if` on `v-for` in `Feed.vue`/`WorkoutList.vue`; null-safe error handling in `CheckTheEmail.vue` (fixes TS18048/TS18046 surfaced by the new strict typing); `AppSidebar` name → `AppSidebar`; added `:key` to `v-for` marker in `components/WorkoutItem.vue`.
- Prettier auto-reformatted ~30 `src/` files via `prelint` — formatting changes are part of the intended clean diff.
- Verified: `yarn lint` 0 errors (exit 0), `yarn build` green (`vue-tsc --noEmit` 0 errors, vite build OK).
### 2026-09-05 — TASK-DEPS-UPDATE-W1: patch/minor dependency refresh via `yarn upgrade` (uncommitted)
- Ran `yarn upgrade <all direct deps except sass>` (yarn 1.22.22 binary has no `up` alias). Baseline was green before the run (lint 0 errors, build green).
- Result: `package.json` unchanged; `yarn.lock` refreshed — 61 direct deps verified, none moved to a new major. Notable bumps: typescript 5.2.2→5.4.5 (capped), postcss 8.4.31→8.5.28, axios 1.7.7→1.20.0, chart.js 4.4.4→4.5.1, eslint 8.57.0→8.57.1, typescript-eslint 7.6.0→7.18.0, @typescript-eslint/\* 6.11.0→6.21.0, prettier 3.1.0→3.9.6, tailwindcss 3.4.1→3.4.19, vite 4.5.5→4.5.14, vue-i18n 9.6.5→9.14.5, vue-router 4.2.5→4.6.4, vue-yandex-maps 2.1.4→2.3.3, vuestic-ui 1.9.0→1.10.3, storybook suite →7.6.24, pinia 2.1.7→2.3.1.
- Two caps applied (rule 5): `typescript` pinned to 5.4.5 in the lock (vue-tsc 1.8.27 incompatible with TS ≥5.5; `yarn add -D typescript@5.4.5` then `git checkout -- package.json` + 1-line lock key rename `typescript@5.4.5`→`typescript@^5.2.2`); `sass` excluded from the upgrade (engine node ≥20.19.0 vs baseline 18.19.1).
- Verified after upgrade: `yarn install --frozen-lockfile` clean; `yarn lint` 0 errors; `yarn build` green (vue-tsc 0 errors, vite build OK ~7.3s).
- No `src/` changes in W1; no HOST/Yandex/GTM changes.
### 2026-09-05 — TASK-DEPS-UPDATE-W2a: vue 3.3.9 → 3.5.42 + pinia 2 → 3.0.4 (uncommitted)
- `yarn add vue@^3.5 pinia@^3`: `package.json` changed exactly two lines (`vue: 3.3.9 → ^3.5`, `pinia: ^2.1.7 → ^3`); lockfile resolved `vue@3.5.42`, `pinia@3.0.4`. No other direct dep moved to a new major (new lock entries are only the vue 3.5 / pinia 3 subtrees).
- Vue 3.5 template type-check surfaced 6× TS2322 (`number[]` → ymaps `LngLat` tuple) in the `vue-yandex-maps` `:settings` props of `src/pages/routes/Route.vue` and `src/pages/workouts/components/WorkoutItem.vue`. Fixed type-only: `ref<LngLat>` in Route.vue; `import type { LngLat }` + 4 template `as LngLat`/`as LngLat[]` casts + `clickCoordinates` typed `ref<LngLat[]>` in components/WorkoutItem.vue. No runtime/prop-signature changes.
- Verified: `yarn lint` 0 errors; `yarn build` green (vue-tsc 0 errors, vite build ~7.5s); `yarn dev` smoke-check OK (boot + key modules serve 200, no console/compile errors).
### 2026-09-05 — TASK-DEPS-UPDATE-W2b: typescript 5.4.5 → 5.8.3 + vue-tsc 1.8.27 → 2.2.12 (uncommitted)
- `yarn add -D typescript@5.8 vue-tsc@^2`: `package.json` changed exactly the two lines (`typescript: ^5.2.2 → "5.8"`, `vue-tsc: ^1.8.22 → ^2`); lock resolved `typescript@5.8.3`, `vue-tsc@2.2.12`. No other direct dep moved to a new major (new lock entries are only the vue-tsc 2.x subtree: `@vue/language-core@2.2.12`, `@volar/*@2.4.15`, `muggle-string`, `alien-signals`, `vscode-uri`, `@vue/compiler-vue2`).
- vue-tsc 2 + TS 5.8 surfaced 4 type errors in 2 file-upload pages (TS 5.8 `Blob` gained required `bytes` property + definite-assignment TS2454 on the uninitialized `let file`). Fixed type-only: legacy 14-line inline structural `file` type → `let file: File | undefined = undefined` in both files; `formData.append("file", file!)` non-null assertion in `PreferencesHeader.vue` (`WorkoutUpload.vue` already guards `if (file == undefined) return`). No runtime/prop-signature changes.
- Verified: baseline green before update; after update `yarn lint` 0 errors, `yarn build` green (vue-tsc 2.2.12 --noEmit 0 errors, vite build ~7.3s).
### 2026-09-05 — TASK-DEPS-UPDATE-W2c: vite 4.5.14 → 5.4.21 + @vitejs/plugin-vue 4.6.2 → 5.2.4 (uncommitted)
- `yarn add -D vite@^5 @vitejs/plugin-vue@^5`: `package.json` changed exactly the two lines (`vite: ^4.4.6 → ^5`, `@vitejs/plugin-vue: ^4.2.3 → ^5`); lock resolved `vite@5.4.21`, `@vitejs/plugin-vue@5.2.4`. No other direct dep moved to a new major — new lock entries are only the vite 5 subtree (`rollup@4.63.1` + `@rollup/*` platform binaries, `esbuild@0.21.5` + `@esbuild/*` platform binaries, `@napi-rs/lzma-linux-x64-gnu`); storybook pin `@vitejs/plugin-vue@^4.0.0` still resolves 4.6.2 (separate lock entry); `esbuild@0.18.20`/`rollup@3.30.0` retained for storybook.
- No `src/` or `vite.config.ts` changes — config API-compatible between vite 4 and 5.
- Warnings: CJS Node API deprecation (vite 5, expected, documented, not fixed); NO sass legacy-API warnings appeared (sass 1.69.5 untouched, per task rule).
- Verified: baseline green before update; after update `yarn lint` 0 errors, `yarn build` green (vue-tsc 0 errors, vite 5.4.21 build ~6.5s, 1396 modules); `yarn dev` smoke-check OK (VITE v5.4.21 ready 372 ms; `GET /`, `/src/main.ts`, `/src/App.vue`, `/src/pages/workouts/Feed.vue` all 200; server stopped after check).
### 2026-09-05 — TASK-DEPS-UPDATE-W2c-FIX: Dockerfile switched to yarn (uncommitted)
- `Dockerfile` only (2 lines): `COPY package.json package-lock.json ./` → `COPY package.json yarn.lock ./`; `RUN npm install` → `RUN yarn install --frozen-lockfile`. Root cause fixed: the Docker build ran `npm install` against a stale `package-lock.json` while dev uses yarn; npm's strict peer validation failed on `@storybook/vue3-vite@7.6.20` → `@vitejs/plugin-vue@^4` vs root `^5`. yarn 1 resolves this via a nested plugin-vue 4.6.2 for storybook (existing separate lock entry).
- `node:18` base image ships yarn 1.22.22 out of the box (verified via `docker run --rm node:18 yarn --version`) — no `npm i -g yarn@1` line added.
- `package-lock.json` intentionally left in the repo (removal = separate decision, out of scope).
- Verified: `yarn install --frozen-lockfile` clean locally (lockfiles untouched per `git status`); `yarn lint` 0 errors; `yarn build` green (vue-tsc 0 errors, vite 5.4.21 ~6.4s); **full `docker build` green** (image `strava-frontend-w2cfix` built: yarn install + npm run build + nginx steps all OK).
### 2026-09-05 — TASK-DEPS-UPDATE-W2d: dead Storybook deps removed (uncommitted)
- `yarn remove` of 9 packages: `storybook`, `@storybook/addon-essentials`, `@storybook/addon-interactions`, `@storybook/addon-links`, `@storybook/blocks`, `@storybook/testing-library`, `@storybook/vue3`, `@storybook/vue3-vite`, `eslint-plugin-storybook` + manual removal of the 2 scripts (`storybook`, `build-storybook`) from `package.json`.
- `eslint.config.mjs` verified: zero storybook references — untouched.
- `yarn.lock` pruned: `grep -c storybook` → 0; nested `@vitejs/plugin-vue@4.6.2` (storybook peer-conflict workaround) gone — only `@vitejs/plugin-vue@^5` → 5.2.4 remains.
- Baseline green before removal; after: `yarn install` clean, `yarn lint` 0 errors, `yarn build` green (vue-tsc 0 errors, vite 5.4.21 ~6.3s, 1396 modules).
- No `src/` changes; `package-lock.json` left stale (separate decision).
### 2026-09-05 — TASK-DEPS-UPDATE-W3 (FINAL): eslint 8→9 + typescript-eslint 7→8 (uncommitted)
- `package.json` devDeps: `eslint` ^8.57.0→**^9** (resolved 9.39.5), `typescript-eslint` ^7.6.0→**^8** (resolved 8.69.0); REMOVED `@typescript-eslint/eslint-plugin@^6.11.0` + `@typescript-eslint/parser@^6.11.0` (not imported by `eslint.config.mjs` — verified). No other package moved to a new major (lock checked: vue 3.5.42, pinia 3, TS 5.8.3, vue-tsc 2.2.12, vite 5.4.21, sass 1.69.5).
- `eslint.config.mjs`: **unchanged** — flat config (CJS default-import of the `typescript-eslint` meta-package, `configs.recommended` spread, `tseslintPkg.parser`) works as-is on eslint 9 / typescript-eslint 8.
- `Dockerfile`: base image `node:18` → `node:22` (transitive `brace-expansion@5.0.9` / `eslint-visitor-keys@5` engines require node ≥20; node 18 EOL). Local dev verified on node 22.14.0.
- Verified: baseline green at wave start; after — `yarn lint` 0 errors, `yarn build` green (vue-tsc 0 errors, vite 5.4.21 ~5.3s), `docker build --pull --no-cache` green (image `strava-frontend-w3`, `yarn install --frozen-lockfile` clean inside image).
- No `src/` changes; `vite.config.ts`, i18n, HOST/keys untouched. W3 = FINAL wave → TASK-DEPS-UPDATE ready to close.
### 2026-09-05 — TASK-DEPS-UPDATE CLOSED (parent)
- All waves W0a → W3 completed and Architect-verified. Final stack: vue 3.5.42, pinia 3.0.4, TS 5.8.3, vue-tsc 2.2.12, vite 5.4.21, @vitejs/plugin-vue 5.2.4, eslint 9.39.5, typescript-eslint 8.69.0, axios 1.20.0, tailwind 3.4.19, vuestic 1.10.3, node:22 (Docker). Storybook fully removed.
- End state: `yarn lint` 0 errors, `yarn build` green, `docker build` green. Runtime behavior unchanged (type-level + config + lockfile only).
- Lessons captured in `systemPatterns.md`: TS parser for `.vue` in flat eslint config; `LngLat` typing contract for vue-yandex-maps; vue-tsc 2 ↔ TS ≤5.8 ceiling; yarn-only workflow; `ban-ts-comment` enforcement.
## Left to build (small follow-ups, no urgency)
- Remove stale `package-lock.json` from the repo (npm workflow abandoned in W2c-FIX).
- Bump `sass` to latest 1.x — Docker (node:22) is ready; check local dev node ≥20.19 first.
- Consider vite 6/7 after the ecosystem settles (own breaking changes).
- `GetWorkout.ts` module-level `let` refactor to pure functions returning state.
- `components/WorkoutItem.vue` prop mutation → local state extraction (unblock the `no-mutating-props` suppression).
### 2026-09-12 — TASK-F1: workout photos gallery + upload/delete + map markers (uncommitted)
- `src/pages/workouts/Definitions.vue`: `WorkoutPhoto` type (`{id, url, size, latitude: number|null, longitude: number|null}`) + `photos?: Array<WorkoutPhoto>` in `WorkoutItem`. No fetch changes (backend already sends `workout.photos` via `GET /workouts/{id}` / public / list).
- `src/pages/workouts/components/WorkoutItem.vue`: local `photos` ref initialized from `workoutItem?.photos`; `photosWithCoords` computed (non-null lat/lon); photo markers as `<yandex-map-default-marker>` with `coordinates: [lon, lat] as LngLat` (same order as `clickCoordinates`), `color: 'blue'`, `onClick` → `window.open(photo.url, "_blank")`.
- Same file: `#workout-photos` section under the map/short-data container — grid of `<img :src="photo.url">` (auto-fill 160px, 120px cover), «Добавить фото» (`v-if="isPrivate"`, hidden `input[type=file] accept="image/*" multiple`, `axiosAuth.post` multipart field `file`, `Promise.allSettled`, success → push to `photos` + toast, partial failure → error toast, VaButton `:loading`), per-photo «Удалить» (`v-if="isPrivate"`, `axiosAuth.delete` → filter from `photos`, error toast). `isPrivate` added to `defineProps` destructure. Styles: `.workout-photos-*` block.
- `WorkoutListItem.vue` untouched (per scope).
- Verified: `npm run build` exit 0 (vue-tsc --noEmit 0 errors, vite `✓ built in 11.71s`; pre-existing >500kB chunk warning), `npm run lint` exit 0 (prettier reformatted the component, eslint 0 errors).
### 2026-09-17 — TASK-LINK1: «Ссылки на описание» скрыта на публичной странице без ссылки (uncommitted)
- `src/pages/workouts/components/WorkoutItem.vue` (единственный файл, 1 строка): на внешний `div.workout-item-params` секции «Ссылки на описание» добавлен `v-if="dzenLink || isPrivate"`. Поведение: публичная без ссылки → секция скрыта; публичная со ссылкой → только «Дзен»; приватная без ссылки → «Добавить» (модалка доступна); приватная со ссылкой → «Дзен» + иконка edit. Внутренняя логика секции не тронута.
- Verified: `yarn lint` exit 0, `yarn build` exit 0 (vue-tsc --noEmit 0 ошибок, vite `✓ built in 5.41s`; pre-existing >500kB chunk warning).
### 2026-09-18 — TASK-SSR-0: rename /dashboard → /explore + legacy redirect (uncommitted)
- `src/router/index.ts`: маршрут `name: "explore"`, `path: "explore"` (Feed.vue); catch-all и admin redirect → `{ name: "explore" }`; новый legacy redirect `{ path: "/dashboard", redirect: { name: "explore" } }` (до catch-all).
- `src/components/sidebar/NavigationRoutes.ts`: `name: "dashboard"` → `"explore"` в `authRoutes` и `publicRoutes` (i18n-ключ `menu.dashboard` сохранён).
- `src/components/app-layout-navigation/AppLayoutNavigation.vue`: breadcrumb `:to="{ name: 'explore' }"`.
- `src/layouts/AppLayout.vue`: auth-guard `useRoute().path != "/explore"`.
- `src/pages/auth/Login.vue` (2×), `src/pages/auth/Signup.vue` (2×), `src/pages/auth/CheckTheEmail.vue` (1×): `push({ name: "explore" })`.
- `src/pages/workouts/Feed.vue`: изменений не потребовалось (ссылок на dashboard нет).
- Verified: `yarn lint` exit 0, `yarn build` exit 0 (vue-tsc --noEmit 0 ошибок, vite `✓ built in 6.99s`; pre-existing >500kB chunk warning).
## TASK-HIDE-TRACK-T1 (2026-09-25)
- `../backend/tests/test_api_workout.py`: добавлен `test_workout_hide_start_end` (PATCH hide_start_end False→True→False + GET-верификация)
- Результат: `python -m pytest tests/test_api_workout.py -v` → **4 passed (5.13s)**