# Лента изменений в веб-приложении — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** страница «Лента» (`/app/feed`) и блок «История» в карточке задачи поверх готовых `GET /changes` и `GET /issues/{key}.changes`. **Architecture:** модуль `web/src/feed/`: чистые функции `events.ts` (период, виды из адреса, текст события, группировка), компонент `EventList.tsx` (общий для ленты и карточки), страница `FeedPage.tsx`. Маршрут — `shell/pages.tsx`, раздел — `shell/Sidebar.tsx`, запрос — `api/queries.ts` (`useChanges`, ключ `["changes", …]`), перечитывание после «Обновить из Jira» — `shell/RefreshProvider.tsx`. **Tech Stack:** React 19 + TypeScript, TanStack Query, react-router, Vitest + Testing Library, Playwright. **Spec:** `openspec/changes/web-feed/` (proposal, design 1–7, `specs/web-app/spec.md`: MODIFIED «Оболочка и адреса», ADDED «Лента изменений», «История в карточке задачи»). Контракт хаба — `openspec/specs/hub-api/spec.md` «Лента изменений», «История задачи в карточке». **Worktree:** `/Users/andrey.epifancev/projects/jira-bridge/jira-hub/.wt/feed`, ветка `feat/77-feed` (aep/jira-hub#77). ## Global Constraints - Бэкенд (`src/jira_hub/`) не меняется. - Тексты событий — ровно по спеке: «статус «было» → «стало»», «описание изменено», «срок ДД.ММ → ДД.ММ» / «плановое начало …» / «плановое окончание …», «комментарий: <300 символов…>», «<подпись>: «было» → «стало»» (значения до 80 символов с «…», пустое — «—»). - Подписи прочих полей: `assignee` исполнитель, `Sprint` спринт, `labels` метки, `priority` приоритет, `summary` тема, `epic_link` эпик, `Link` связь, `Attachment` вложение, `resolution` резолюция; иначе имя поля как есть. - Период: `2h` | `today` (по умолчанию, в адрес не пишется) | `yesterday` | `7d`; границы по местному времени, в запрос — `toISOString()`. `limit=2000`. - Виды: `status`, `description`, `dates`, `comment`, `other`; по умолчанию первые четыре (в адрес не пишутся); подписи флажков «Статусы», «Описание», «Сроки», «Комментарии», «Прочие поля». - Цвета и отступы — только токены дизайн-системы (`styles/tokens.css`), базовые компоненты из `web/src/ui/`; `dangerouslySetInnerHTML` не появляется; новых зависимостей нет. - Коммиты — с `(#77)` и строкой `Co-Authored-By: Claude Opus 5.5 `. Зелёный — `make check` и `cd web && npm run e2e`. - Решения вне спеки и плана — в `openspec/changes/web-feed/design.md`, раздел «Decisions made without discussion», маркер `R-n`. ## Review Focus 1. Ответ хаба без `changes` у карточки (старый хаб) и пустой `changes: []` — карточка не падает: нет блока / «изменений нет» (тесты в Task 3). 2. Событие с `at = null`, `author = null`, `from`/`to` = null — строка без «null»/«Invalid Date» (тест в Task 1). 3. Адрес с мусором `period=foo`, `kinds=status,foo` — страница не падает: период по умолчанию, неизвестные виды отброшены (тест в Task 1). 4. Смена проекта в панели на открытой ленте — запрос по новому проекту, период и виды из адреса сохраняются (тест в Task 2). 5. Длинный однословный текст комментария/значения — не ломает ширину (CSS `overflow-wrap: anywhere`) и нет горизонтальной прокрутки на 390 px (e2e в Task 2). --- ### Task 1: `feed/events.ts`, тип события, запрос `useChanges`, перечитывание **Files:** - Create: `web/src/feed/events.ts`, `web/src/feed/events.test.ts` - Modify: `web/src/api/types.ts` (`ChangeEvent`, `ChangesResponse`, `IssueDetail.changes?: ChangeEvent[]`), `web/src/api/queries.ts` (`qk.changes`, `useChanges`), `web/src/shell/RefreshProvider.tsx` (инвалидация `["changes"]`), `web/src/shell/refresh.test.tsx` (ожидание) **Interfaces (produces):** - `type ChangeKind = "status" | "description" | "dates" | "comment" | "other"`; `interface ChangeEvent { at: string | null; key?: string; summary?: string | null; status?: string | null; kind: ChangeKind; field: string; author: string | null; from: string | null; to: string | null; from_value: string | null; to_value: string | null }`; `interface ChangesResponse { since: string; until: string; truncated: boolean; changes: ChangeEvent[] }`. - `type Period = "2h" | "today" | "yesterday" | "7d"`; `PERIODS: { value: Period; label: string }[]` (2 часа, Сегодня, Вчера, 7 дней); `parsePeriod(v: string | null): Period`; `periodRange(p: Period, now: Date): { since: Date; until: Date }`. - `KINDS: { value: ChangeKind; label: string }[]`, `DEFAULT_KINDS`; `parseKinds(v: string | null): ChangeKind[]` (неизвестные отброшены, `null`/пусто → по умолчанию); `kindsParam(k: ChangeKind[]): string | null` (`null` для набора по умолчанию). - `eventTime(at: string | null, now: Date, alwaysDate = false): string` (`ЧЧ:ММ` / `ДД.ММ ЧЧ:ММ` / «—»); `eventText(e: ChangeEvent): string`; `groupByIssue(events: ChangeEvent[]): { key: string; summary: string | null; status: string | null; events: ChangeEvent[] }[]`. - `useChanges(q: { project: string; since: string; until: string; kinds: string }, enabled: boolean)` → `useQuery`, ключ `qk.changes(q) = ["changes", q]`, `limit: 2000`. - [ ] **Step 1: Tests** — `events.test.ts`: `periodRange` для всех четырёх при `now = new Date(2026, 9, 22, 14, 5)` (today → 22.10 00:00–14:05; yesterday → 21.10 00:00–22.10 00:00; 2h → 12:05; 7d → 15.10 14:05); `parsePeriod("foo") === "today"`; `parseKinds("status,foo") → ["status"]`, `parseKinds(null)` → по умолчанию; `kindsParam(DEFAULT_KINDS) === null`; `eventText` на каждом виде (статус, описание, срок по `from_value` `2026-10-23` → «срок 23.10 → —», плановое окончание со строкой без ISO-значения, комментарий 350 символов → 300 + «…», прочее `assignee` → «исполнитель: «—» → «ivanov»», незнакомое поле — как есть); `eventTime(null, …) === "—"`, сегодня — `ЧЧ:ММ`, вчера — `21.10 ЧЧ:ММ`, `alwaysDate` — с датой; `groupByIssue` — порядок групп по первому появлению. `refresh.test.tsx` — после пакета инвалидируется и `["changes"]`. - [ ] **Step 2: Run** `cd web && npx vitest run src/feed src/shell/refresh.test.tsx` — FAIL - [ ] **Step 3: Implement** - [ ] **Step 4: Run** `make check` — зелёный - [ ] **Step 5: Commit** — `Лента: события, период и запрос /changes (#77)` ### Task 2: Страница «Лента», раздел в панели **Files:** - Create: `web/src/feed/FeedPage.tsx`, `web/src/feed/EventList.tsx`, `web/src/feed/feed.css`, `web/src/feed/FeedPage.test.tsx`, `web/e2e/feed.spec.ts` - Modify: `web/src/shell/pages.tsx` (`/feed` под `ProjectGate`), `web/src/shell/Sidebar.tsx` (`Section` + «Лента», значок `IconHistory` в `ui/icons.tsx`), `web/src/shell/Layout.tsx` (если там список разделов для `section`), `web/src/shell/Sidebar.test.tsx` **Interfaces:** - `FeedPage({ project }: { project: string })` — `PageHeader` «Лента» с подзаголовком «N событий · ДД.ММ ЧЧ:ММ — ДД.ММ ЧЧ:ММ», `Segmented` периода (кнопки, запись в адрес через `setSearchParams`), флажки видов (`Checkbox`), `EventList` по группам; пустые/ошибка/truncated — `Notice` с текстами спеки. - `EventList({ events, now, alwaysDate }: { events: ChangeEvent[]; now: Date; alwaysDate?: boolean })` — `