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

37 KiB
Raw Blame History

Active Context — Strava Frontend

Task State

  • task_id: TASK-SSR-EXPLORE-CARDS
  • status: success
  • summary: SSR /explore теперь рендерит карточки 1:1 с клиентом: <section class="news-feed"> → <article class="news-item"> (.news-item-header→<h3><a>, ul.metadata 9 условных <li> в порядке клиента, .image-container-<a> с background-image). Хэндлер + distInt в server/index.ts; добавлен отсутствовавший tsconfig.server.json (yarn server:build теперь работает).
  • next task: —
  • plan: plans/TASK-SSR-EXPLORE-CARDS.md

✅ CODER SUCCESS REPORT — TASK-INTEG-SECTION (+TASK-SECTION-BUGS)

  • ../integration/tests/section-duration.spec.ts — 3 теста (duration row, zoom persist across tabs, reset on all tabs), self-contained (stable owner itest-section-owner, API-seed, Chart.js via canvas.__vueParentComponent.exposed.chart).
  • Bug 1 фикс (WorkoutItem.vue setChartRef ~1155): ref пушится сразу (без guard r.chart), финализация (collectCharts + applySelectedRangeZoom) через retry nextTick (до 20 попыток) — дождётся асинхронной инициализации Chart.js. nextTick добавлен в импорт из "vue" (~строка 440).
  • Bug 2 фикс (ChartGroup.ts resetAll ~125): после resetZoom(chart) явно x.min=undefined; x.max=undefined; chart.update("none") (broadcastZoom писал min/max напрямую, минуя плагин). Дополнительно: на время цикла resetAll установлена this.broadcasting=true (try/finally) — resetZoom триггерит onZoomComplete, который иначе повторно разносит ещё-зумированный диапазон на остальные чарты; guard isBroadcasting() отбрасывает эти колбэки.
  • npx playwright test tests/section-duration.spec.ts → 3 passed (56s) на docker-стече.
  • yarn build (vue-tsc --noEmit + vite) → exit 0.
  • Known: диагностический tests/chartprobe.spec.ts не удалён (rm отклонён) — удалить вручную, если мешает.

TASK-INTEG-SECTION: план делегирования

Scope: 1 новый файл — ../integration/tests/section-duration.spec.ts + запуск на стеке. Не трогать src/.

Что тестируется (3 UX-требования, компонент src/pages/workouts/components/WorkoutItem.vue, страница workout_item):

  1. Клик по графику (выделение участка) → в блоке .area-data появляется строка «Продолжительность».
  2. Клик по вкладке «Дистанция» → тот же участок остаётся в zoom (не весь путь).
  3. Клик по reset → участок сброшен; после смена вкладки → весь путь (reset на всех вкладках).

Контракты (проверено Архитектором):

  • Детальная страница (приватная, с графиками + вкладками): маршрут workout_item → URL {FRONTEND}/workouts/workouts/{id} (родитель workouts + вложенный workouts/:id). Навигация waitUntil: "domcontentloaded" (Yandex map держит соединения).
  • Seed через API (чисто, без UI-dedup): POST {BACKEND}/api/v0/signup (200) ИЛИ POST /api/v0/signin (fallback на повторном прогоне) → token; POST /api/v0/attachment/upload (FormData поле file, Bearer) → {id}; POST /api/v0/workouts {attachment_id, name} → {id} (400 ObjectExists → взять id из GET /api/v0/workouts[0]).
    • Файл: ../../backend/tests/fitness_tracker_data_parsing-main/20240507053735.fit (тот же, что в workout-upload-flow.spec.ts).
  • Детальный ответ GET /api/v0/workouts/{id} → { results: [{ timestamp, longitude, latitude, speed, heart_rate, power, elevation, ... }], workout: {...} } — из него взять полный диапазон (первая/последняя timestamp, длина) для порогов zoom.
  • Chart.js: canvas графиков — #workout-charts .workout-chart-canvas canvas (несколько; брать [0]). Чертит только если есть метрика (у fixture есть speed). Zoom-состояние: window.Chart.getChart(canvas).isZoomedOrPanned() + chart.options.scales.x.min/max (broadcastZoom пишет сюда) + chart.scales.x.min/max.
  • Клик по графику (выбор участка) — через Chart.js onClick опцию: mouse.down → mouse.move → mouse.up в точке внутри chartArea (не drag, чтобы не было drag-zoom). Координаты взять через chart.chartArea (left/right/top/bottom) в page.evaluate.
  • Кнопки вкладок: по тексту «Время» и «Дистанция» (VaButton). Reset: #zoom-botton. Блок данных участка: .area-data (заголовок «Данные участка»), строка «Продолжительность».

Структура спе (self-contained, repeatable):

  • beforeAll: проверить стек (frontend <500, backend /api/v0/html_test/login==200), проверить наличие fixture-файла; seed stable owner (itest-section-owner@example.com, idempotent signup/signin) + его workout (upload attachment + create; на 400 — id из списка).
  • 3 независимых test'а (каждый: authedPage(token) → openWorkoutDetail(id) → expect canvas visible):
    1. «duration row on section select»: canvas-click → expect .area-data visible → expect текст «Продолжительность» в .area-data.
    2. «section zoom persists across tab switch»: canvas-click (zoom) → assert zoomed (x.min<full_min) → click «Дистанция» (кнопка active/primary) → assert всё ещё zoomed (x.min≈, не полный диапазон) → click «Время» → assert zoomed (симметрия).
    3. «reset clears section on all tabs»: canvas-click (zoom) → assert zoomed → click #zoom-botton (reset) → assert NOT zoomed → click «Дистанция» → assert NOT zoomed (весь путь).

Замечания:

  • Не expect внутри page.evaluate — только return данных наружу.
  • authedPage: goto → set localStorage.token → goto (паттерн из workout-upload-flow.spec.ts).
  • Node ≥20: запуск через nvm export PATH="$HOME/.nvm/versions/node/v22.14.0/bin:$PATH".
  • yarn up (docker compose up -d --build) → ждать backend: until curl -sf http://localhost:8000/api/v0/html_test/login >/dev/null; do sleep 2; done → npx playwright test tests/section-duration.spec.ts.

Acceptance criteria:

  1. ../integration/tests/section-duration.spec.ts создан.
  2. npx playwright test tests/section-duration.spec.ts → 3 passed на запущенном стеке.
  3. Не сломать существующие спе (опционально: npx playwright test --list).

TASK-SECTION-DURATION-ZOOM: план делегирования

Scope: 1 файл — src/pages/workouts/components/WorkoutItem.vue (компонент, НЕ страница src/pages/workouts/WorkoutItem.vue).

Ключевые якоря (текущее состояние кода):

  • times (~строка 1061) — сырые ISO-метки времени всех точек (общие для всех метрик).
  • distances (prop) — накопительные метры на точку.
  • activeSection = ref<number | null>(null) (~строка 661) — номер секции, зависит от xAxisMode (потому что sectionBounds пересчитывается под режим).
  • computeAreaData(start, end) (~строка 590) — считает средние по метрикам в блок «Данные участка» (areaAvgData).
  • switchXAxis(mode) (~строка 1135) — сбрасывает activeSection = null (баг п.2).
  • onZoomComplete (~строка 873) — activeSection = null + computeAreaData(start,end).
  • onClick (~строка 930) — выбор/тоггл секции.
  • resetChartZoom() (~строка 1449) — reset.
  • setChartRef (~строка 1126) — после сбора всех refs делает group.charts = collectCharts() (сигнал для re-zoom после remount).
  • secondsToDuration — экспортируется из ../Definitions.vue (не импортирован сейчас — добавить в импорт).

Изменения:

  1. Заменить activeSection: number|null на selectedRange: { startIdx: number; endIdx: number } | null (единственный source of truth, не зависит от режима — индексы точек).
  2. В computeAreaData(start,end): добавить avgData["Продолжительность"] = secondsToDuration(durSec), где durSec = max(0, round((Number(new Date(times[end-1])) - Number(new Date(times[start])))/1000)). Импортировать secondsToDuration из ../Definitions.vue.
  3. onClick: выбрать selectedRange = {startIdx, endIdx} по sectionBounds; тоггл-off когда selectedRange совпадает (startIdx== и endIdx==); выбрать иначе → selectedRange, computeAreaData, broadcastZoom.
  4. onZoomComplete: selectedRange = null (ручной zoom снимает выбор), computeAreaData(start,end) сохранить.
  5. switchXAxis: НЕ сбрасывать selectedRange; сохранить areaAvgData/markedCoordinats (они режим-независимы). Добавить applySelectedRangeZoom(): labels = xAxisMode==='time' ? times : distances; toValue = time? Number(new Date(l)) : Number(l); min=toValue(labels[startIdx]), max=toValue(labels[endIdx-1]); group.broadcastZoom(min,max). Вызвать applySelectedRangeZoom() в setChartRef после group.charts = collectCharts() (re-apply zoom в новом режиме после remount).
  6. resetChartZoom(): selectedRange = null (чтобы после reset на другой вкладке тоже был весь путь), остальное как есть (resetAll, areaAvgData={}, markedCoordinats=[]).

**Почему работает «на всех вкладках»: ** reset обнуляет selectedRange → при смене вкладки zoom re-apply вызывается только если selectedRange set → после reset всегда весь путь.

Acceptance criteria:

  1. yarn build (vue-tsc --noEmit) → exit 0.
  2. yarn lint → 0 ошибок (или зафиксировать пре-экзистинг системный tsutils.iterateComments).
  3. Ручной сценарий (MCP browser): выделить участок → в «Данные участка» есть «Продолжительность» → перейти на вкладку «Дистанция» → та же секция остаётся в zoom (не весь путь) → нажать reset → участок сброшен на всех вкладках.

✅ CODER SUCCESS REPORT — TASK-SECTION-DURATION-ZOOM

  • src/pages/workouts/components/WorkoutItem.vue:
    • импорт secondsToDuration добавлен в импорт из ../Definitions.vue (рядом с formatTime).
    • activeSection: number|null → selectedRange: { startIdx: number; endIdx: number } | null (единственный source of truth; индексы точек, не зависят от xAxisMode).
    • computeAreaData(start,end): в начале цикла добавлена строка avgData["Продолжительность"] = secondsToDuration(durSec), где durSec = max(0, round((Number(new Date(times[end-1])) - Number(new Date(times[start])))/1000)).
    • onClick: тоггл-off по совпадению selectedRange.startIdx===startIdx && selectedRange.endIdx===endIdx; иначе selectedRange={startIdx,endIdx} + computeAreaData + broadcastZoom.
    • onZoomComplete: selectedRange=null (ручной zoom снимает выбор).
    • switchXAxis: УБРАЛ сброс activeSection (выбор сохраняется); areaAvgData/markedCoordinats не трогаю (режим-независимы). Добавлена applySelectedRangeZoom() (labels = time? times : distances; broadcastZoom(min,max)).
    • setChartRef: внутри if (chartRefs.length === chartConfigs.value.length) после group.charts = collectCharts() вызывается applySelectedRangeZoom() (re-apply zoom в новом режиме после remount).
    • resetChartZoom: selectedRange=null (после reset на любой вкладке — весь путь).
  • Verified: yarn build (vue-tsc --noEmit) → ✓ built in 7.14s, Done (0 type errors).
  • Verified: grep activeSection → 0 совпадений; grep selectedRange → 9 совпадений; grep secondsToDuration → импорт + использование (2 совпадения).
  • Note: yarn lint сломан системно — tsutils.iterateComments is not a function на всех 73 файлах (пре-экзистинг, дефект env eslint/typescript-estree, не от этих изменений).
  • Browser verification (MCP ad-hoc): НЕ выполнена — dev server :5173 и интеграционный стек :8000 не запущены (не поднимал docker-стек ради ad-hoc проверки). Результат по критериям 1–4.
  • ✅ Architect review: прочитаны все изменённые блоки (импорт 481, computeAreaData 599-606, selectedRange 670, onClick 997-1008, onZoomComplete 921, switchXAxis 1164-1173, applySelectedRangeZoom 1142-1153 + вызов в setChartRef 1161, resetChartZoom 1479). Корректность подтверждена: times/distances/lineCoordinates одной длины; re-apply zoom после remount через setChartRef; после reset selectedRange=null → на любой вкладке весь путь.

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).

✅ CODER SUCCESS REPORT — TASK-SSR-EXPLORE-CARDS

  • server/index.ts: хэндлер GET /explore (~строки 135–228) переписан — content теперь <section class="news-feed"> с <article class="news-item"> на каждый workout: .news-item-header → <h3><a href="/public/workouts/{id}">, ul.metadata с 9 условными <li> в порядке клиента (Дата, Фото, Скорость, Пульс, Расстояние, Продолжительность, Каденс, Мощность, Ссылки), .image-container — <a> c background-image (только если w.attachment?.url). Имя/ссылки экранированы escapeHtml. SEO-мета (title/description/canonical/jsonLd) и catch → 500 сохранены без изменений.
  • server/index.ts: новый форматтер distInt(m) = Math.round(m/1000) (~строка 52, после formatSpeed) — расстояние в ленте целыми км (совпадение с клиентским distConvert); formatDistance (toFixed(1)) не тронут — используется в / и detail.
  • server/index.ts: импорт WorkoutItem из ./api добавлен.
  • tsconfig.server.json создан (был отсутствует) — yarn server:build (tsc -p tsconfig.server.json) до этого падал «config file missing»; теперь commonjs/outDir: dist-server/strict, как ожидал server:start (node dist-server/index.js).
  • .gitignore: добавлен dist-server (артефакт server:build).
  • Verified: yarn server:build → Done (0 ошибок); yarn build (vue-tsc --noEmit + vite) → ✓ built in 6.58s (0 ошибок).
  • Verified HTML (SSR dev, реальный production-backend): curl http://localhost:3001/explore — 3× news-item, 3× metadata, 3× image-container, 1× news-feed, <a href="/public/workouts/{id}", метки Дата/Фото/Скорость/Пульс/Расстояние/Продолжительность/Каденс/Мощность. «Ссылки» корректно отсутствует: у всех 3 workout'ов ленты external_links.values пустые (v-if — 1:1 с клиентом).
  • Verified browser (MCP): browser_navigate http://localhost:3001/explore → snapshot: 3 article c <h3>-заголовками-ссылками на /public/workouts/{id} + list c «Дата: …»; title «Публичные велотренировки — Cycle Rider». Консоль: только 3 pre-existing 404 на статические ассеты (в dev SSR dist/ не сервится — в prod это делает nginx; app-ошибок нет).
  • Known (вне scope): yarn lint (только ./src/**/*) — 73 pre-existing errors, src не трогался.
  • Note: в процессе проверка git stash/pop столкнулась с конкурентным обновлением activeContext.md (TASK-SECTION-DIST-FIX); состояние восстановлено, конфликтных потерь нет.

История (закрытые задачи)

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