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

12 KiB
Raw Permalink Blame History

Лента изменений в веб-приложении — 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)