12 KiB
Лента изменений в веб-приложении — 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
- Ответ хаба без
changesу карточки (старый хаб) и пустойchanges: []— карточка не падает: нет блока / «изменений нет» (тесты в Task 3). - Событие с
at = null,author = null,from/to= null — строка без «null»/«Invalid Date» (тест в Task 1). - Адрес с мусором
period=foo,kinds=status,foo— страница не падает: период по умолчанию, неизвестные виды отброшены (тест в Task 1). - Смена проекта в панели на открытой ленте — запрос по новому проекту, период и виды из адреса сохраняются (тест в Task 2).
- Длинный однословный текст комментария/значения — не ломает ширину (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_value2026-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)