sdlc-hub/docs/superpowers/plans/2026-10-22-web-feed.md
Andrey Epifancev 47a156733f Спека и план: лента изменений в веб-приложении (#77)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 11:17:08 +04:00

88 lines
12 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.

# Лента изменений в веб-приложении — 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 <noreply@anthropic.com>`. Зелёный — `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<ChangesResponse>`, ключ `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 })` — `<ul>` строк «время · автор · текст».
- Группа: `IssueLink` с ключом, тема, `Badge` статуса.
- [ ] **Step 1: Tests** — `FeedPage.test.tsx` (через `App` и `mockFetch`, как `Sidebar.test.tsx`): сценарии спеки «Что закрылось за два часа» (адрес и параметры запроса), «Карточка из ленты» (клик по ключу → диалог карточки, закрытие → лента с тем же `period`), «Пустой период», `truncated`, ошибка 500 с «Повторить», все флажки сняты → подсказка и нет запроса; Review Focus 3 (`period=foo&kinds=status,foo`) и 4 (смена проекта). `Sidebar.test.tsx` — раздел «Лента» ведёт на `/feed?project=…` и отмечается `aria-current`. `e2e/feed.spec.ts` — засеять журнал агентским `POST /agent/history` (элементы с `created` = сейчас, ключи из фикстуры `hub-basic`), открыть «Лента», увидеть событие, открыть карточку; на 390 px нет горизонтальной прокрутки.
- [ ] **Step 2: Run** `cd web && npx vitest run src/feed src/shell` — FAIL
- [ ] **Step 3: Implement**
- [ ] **Step 4: Run** `make check` и `cd web && npm run e2e` — зелёные
- [ ] **Step 5: Commit** — `Страница «Лента» и раздел в панели (#77)`
### Task 3: «История» в карточке, документация
**Files:**
- Modify: `web/src/issues/IssueCard.tsx` (блок `History` после `Comments`), `web/src/issues/IssueCard.test.tsx`, `ARCHITECTURE.md` (раздел `web/`: страница «Лента», `feed/`), `CLAUDE.md` репозитория (адрес `/app/feed` в «Команды»)
- [ ] **Step 1: Tests** — сценарий спеки «Карточка с историей» (12 событий → 10 + «Показать все (12)» → 12), `changes: []` → «изменений нет», без поля `changes` → нет заголовка «История» (Review Focus 1); строки — с датой (`alwaysDate`).
- [ ] **Step 2: Run** `cd web && npx vitest run src/issues/IssueCard.test.tsx` — FAIL
- [ ] **Step 3: Implement**, документация
- [ ] **Step 4: Run** `make check` и `cd web && npm run e2e` — зелёные
- [ ] **Step 5: Commit** — `История изменений в карточке задачи (#77)`