From de2728ccb91a9ba46935f9fbcebd499405b87b8e Mon Sep 17 00:00:00 2001 From: artem Date: Sun, 27 Sep 2026 06:52:55 +0300 Subject: [PATCH] fix html --- .gitignore | 1 + .roo/memory-bank/activeContext.md | 24 ++++++++- .roo/memory-bank/progress.md | 22 ++++++++ server/index.ts | 88 +++++++++++++++++++++++++------ 4 files changed, 118 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index 6a7d6d8..75cbdf5 100644 --- a/.gitignore +++ b/.gitignore @@ -90,6 +90,7 @@ out # Nuxt.js build / generate output .nuxt dist +dist-server # Gatsby files .cache/ diff --git a/.roo/memory-bank/activeContext.md b/.roo/memory-bank/activeContext.md index 1dd60c8..7c6ddff 100644 --- a/.roo/memory-bank/activeContext.md +++ b/.roo/memory-bank/activeContext.md @@ -2,12 +2,14 @@ ## Task State -- task_id: TASK-SECTION-DIST-FIX +- task_id: TASK-SSR-EXPLORE-CARDS - status: success -- summary: **Добавлена строка «Дистанция» в «Данные участка» + drag-zoom теперь персистится при смене вкладки (selectedRange вместо null в onZoomComplete). 3 passed.** +- summary: **SSR `/explore` теперь рендерит карточки 1:1 с клиентом: `
` → `
` (`.news-item-header`→`

`, `ul.metadata` 9 условных `
  • ` в порядке клиента, `.image-container`-`` с 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()` отбрасывает эти колбэки. @@ -20,11 +22,13 @@ **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`). @@ -34,6 +38,7 @@ - Кнопки вкладок: по тексту «Время» и «Дистанция» (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`. @@ -41,12 +46,14 @@ 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`). @@ -289,6 +296,19 @@ - `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` теперь `
    ` с `
    ` на каждый workout: `.news-item-header` → `

    `, `ul.metadata` с 9 условными `
  • ` в порядке клиента (Дата, Фото, Скорость, Пульс, Расстояние, Продолжительность, Каденс, Мощность, Ссылки), `.image-container` — `` 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`, ``-заголовками-ссылками на `/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.) diff --git a/.roo/memory-bank/progress.md b/.roo/memory-bank/progress.md index 628867d..5c96436 100644 --- a/.roo/memory-bank/progress.md +++ b/.roo/memory-bank/progress.md @@ -1,5 +1,17 @@ # Progress — Strava Frontend +## 2026-09-26 — TASK-SECTION-REZOOM-LOOP: фикс бесконечного цикла re-render (CPU-шум) + +- `src/pages/workouts/components/WorkoutItem.vue`: + - Новый флаг `let pendingRezoom = false;` (рядом с `chartRefs`, ~строка 526). + - `switchXAxis`: после `chartRefs.length = 0` добавлено `pendingRezoom = true`. + - `setChartRef` / `tryFinalize`: `applySelectedRangeZoom()` вызывается + только если `pendingRezoom === true` (затем флаг сбрасывается в false). + Устранён цикл: клик → areaAvgData → re-render → setChartRef → + applySelectedRangeZoom → broadcastZoom → update("none") → + reactivity → re-render → setChartRef → ... (бесконечно). +- Verified: `yarn build` exit 0; `npx playwright test tests/section-duration.spec.ts` → 3 passed (13.7s). + ## 2026-09-26 — TASK-SECTION-DIST-FIX: строка «Дистанция» + drag-zoom персистится при смене вкладки - `src/pages/workouts/components/WorkoutItem.vue`: @@ -332,3 +344,13 @@ - `../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)** + +## TASK-SSR-EXPLORE-CARDS (2026-09-27) + +- `server/index.ts`: хэндлер `GET /explore` — `content` = `
    ` c `
    ` (`.news-item-header`→`

    `, `ul.metadata` 9 условных `
  • ` в порядке клиента, `.image-container`-`` c `background-image`); SEO-мета и catch→500 без изменений; импорт `WorkoutItem` из `./api`. +- `server/index.ts`: новый форматтер `distInt(m)=Math.round(m/1000)` (после `formatSpeed`, ~строка 52); `formatDistance` не тронут. +- `tsconfig.server.json` (новый файл): `module commonjs`, `outDir dist-server`, `strict`, `rootDir server`, `include server/**/*.ts` — скрипт `server:build` ссылался на него, файла не было. +- `.gitignore`: добавлена строка `dist-server`. +- Verified: `yarn server:build` → Done (0 ошибок); `yarn build` → ✓ built in 6.58s (0 ошибок, pre-existing >500kB chunk warning). +- Verified HTML: `curl http://localhost:3001/explore` (SSR dev, production backend) — `news-feed`×1, `news-item`×3, `metadata`×3, `image-container`×3, ``-ссылкой + `list` «Дата: …»), title SSR-заголовка; console — только 3 pre-existing 404 на статические ассеты (dev SSR не сервит `dist/`; в prod — nginx), app-ошибок нет. diff --git a/server/index.ts b/server/index.ts index 6c74b31..925a64e 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,5 +1,5 @@ import express, { Request, Response } from "express"; -import { getPublicWorkouts, getPublicWorkout } from "./api"; +import { getPublicWorkouts, getPublicWorkout, WorkoutItem } from "./api"; import { renderTemplate, getAssetTags, SeoMeta } from "./template"; const app = express(); @@ -49,6 +49,10 @@ function formatSpeed(ms: number): string { return Math.round(ms * 3.6).toString(); } +function distInt(m: number): string { + return Math.round(m / 1000).toString(); +} + function getToken(req: Request): string | null { const cookieHeader = req.headers.cookie; if (!cookieHeader) return null; @@ -132,24 +136,78 @@ app.get("/explore", async (_req: Request, res: Response) => { try { const workouts = await getPublicWorkouts(); const list = workouts - .map( - (w) => - `
  • \n ${escapeHtml( - w.name, - )}\n — ${formatDate(w.workouted_at)}, ${formatDistance( - w.distantion, - )} км, ${formatSpeed(w.speed)} км/ч, ${ - w.heart_rate - } уд/мин\n
  • `, - ) + .map((w: WorkoutItem) => { + const meta: string[] = []; + if (w.workouted_at) { + meta.push( + `
  • Дата: ${formatDate(w.workouted_at)}
  • `, + ); + } + if (w.photos && w.photos.length > 0) { + meta.push(`
  • Фото: ${w.photos.length}
  • `); + } + if (w.speed) { + meta.push( + `
  • Скорость: ${formatSpeed( + w.speed, + )} км/ч
  • `, + ); + } + if (w.heart_rate) { + meta.push( + `
  • Пульс: ${w.heart_rate} уд /мин
  • `, + ); + } + if (w.distantion) { + meta.push( + `
  • Расстояние: ${distInt( + w.distantion, + )} км
  • `, + ); + } + if (w.duraion_sec) { + meta.push( + `
  • Продолжительность: ${formatDuration( + w.duraion_sec, + )}
  • `, + ); + } + if (w.cadence) { + meta.push(`
  • Каденс: ${w.cadence} об/мин
  • `); + } + if (w.power) { + meta.push( + `
  • Мощность: ${Math.round(w.power)} Вт
  • `, + ); + } + if (w.external_links && w.external_links.values.length > 0) { + meta.push( + `
  • Ссылки: Дзен
  • `, + ); + } + const imageUrl = w.attachment?.url; + const image = imageUrl + ? `\n ` + : ""; + return ` `; + }) .join("\n"); const content = ` -

    Публичные тренировки

    -

    Лента велотренировок от сообщества.

    -
      +
      ${list} -
    `; +
    `; const meta: SeoMeta = { title: "Публичные велотренировки — Cycle Rider",