From 47a156733fb5b04dd0d5d1ad67c31424a13352c8 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 22 Oct 2026 11:17:08 +0400 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=BF=D0=B5=D0=BA=D0=B0=20=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=BB=D0=B0=D0=BD:=20=D0=BB=D0=B5=D0=BD=D1=82=D0=B0=20?= =?UTF-8?q?=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=B2?= =?UTF-8?q?=20=D0=B2=D0=B5=D0=B1-=D0=BF=D1=80=D0=B8=D0=BB=D0=BE=D0=B6?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B8=20(#77)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/superpowers/plans/2026-10-22-web-feed.md | 87 +++++++++++++++++ openspec/changes/web-feed/.openspec.yaml | 2 + openspec/changes/web-feed/design.md | 27 ++++++ openspec/changes/web-feed/proposal.md | 25 +++++ .../changes/web-feed/specs/web-app/spec.md | 93 +++++++++++++++++++ 5 files changed, 234 insertions(+) create mode 100644 docs/superpowers/plans/2026-10-22-web-feed.md create mode 100644 openspec/changes/web-feed/.openspec.yaml create mode 100644 openspec/changes/web-feed/design.md create mode 100644 openspec/changes/web-feed/proposal.md create mode 100644 openspec/changes/web-feed/specs/web-app/spec.md diff --git a/docs/superpowers/plans/2026-10-22-web-feed.md b/docs/superpowers/plans/2026-10-22-web-feed.md new file mode 100644 index 0000000..1bb30a2 --- /dev/null +++ b/docs/superpowers/plans/2026-10-22-web-feed.md @@ -0,0 +1,87 @@ +# Лента изменений в веб-приложении — 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 })` — `
    ` строк «время · автор · текст». +- Группа: `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)` diff --git a/openspec/changes/web-feed/.openspec.yaml b/openspec/changes/web-feed/.openspec.yaml new file mode 100644 index 0000000..7cc41bb --- /dev/null +++ b/openspec/changes/web-feed/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-22 diff --git a/openspec/changes/web-feed/design.md b/openspec/changes/web-feed/design.md new file mode 100644 index 0000000..eae81da --- /dev/null +++ b/openspec/changes/web-feed/design.md @@ -0,0 +1,27 @@ +## Context + +`main` 78a0d78. Веб-приложение — React 19 + TanStack Query, страницы рисует `shell/Layout.tsx` по `shell/pages.tsx`, +разделы — `shell/Sidebar.tsx` (`SECTIONS`), проект — `ProjectGate`, карточка — окно `IssueModal` поверх страницы +(`IssueLink`/`useOpenIssue` с `openCardState`). Запросы — `api/queries.ts` (`qk`), перечитывание после «Обновить из +Jira» — `shell/RefreshProvider.tsx`. Хаб: `GET /changes` (`since`/`until` — ISO со смещением, иначе 422; `kinds`, +`project`, `limit` ≤ 2000, ответ `{since, until, truncated, changes}`), `GET /issues/{key}.changes`. + +## Decisions + +1. **Модуль `web/src/feed/`**: `events.ts` — чистые функции (границы периода, разбор `kinds` из адреса, текст события, + группировка по задаче), `EventList.tsx` — список событий (лента и карточка), `FeedPage.tsx` — страница, `feed.css`. +2. **Период — переключатель из четырёх, а не календарь**: покрывает «за пару часов» и «за сегодня» владельца; «Вчера» и + «7 дней» — утренний разбор и неделя. Границы считаются по местному времени браузера, в запрос уходят + `toISOString()` (UTC с `Z`) — хаб принимает `Z`, пояс не теряется. +3. **`limit` = 2000** (максимум хаба) — неделя активного проекта помещается; иначе `truncated` с подсказкой. +4. **Подписи прочих полей** — словарь в `events.ts`; незнакомые — имя поля Jira. +5. **Комментарий в ленте — простой текст** (300 символов одной строкой), без вики-разметки: лента — обзор, полный текст + — в карточке. +6. **История в карточке — 10 последних с «Показать все»**; блок в основной колонке после комментариев. +7. **Перечитывание**: ключ запроса `["changes", …]`; `RefreshProvider` после пакета инвалидирует `["changes"]`; TanStack + Query перечитывает при возврате на вкладку (`refetchOnWindowFocus` по умолчанию). + +## Decisions made without discussion + +1. 2026-10-22 — **все решения выше (1–7) — мои**: владелец сказал «начинай» без обсуждения интерфейса. Цена ошибки: + переделка вёрстки страницы; бэкенд не затронут. diff --git a/openspec/changes/web-feed/proposal.md b/openspec/changes/web-feed/proposal.md new file mode 100644 index 0000000..5313873 --- /dev/null +++ b/openspec/changes/web-feed/proposal.md @@ -0,0 +1,25 @@ +## Why + +Хаб уже хранит журнал изменений задач (change-history, #70): лента `GET /changes` и история задачи в +`GET /issues/{key}`, в MCP — инструмент `changes`. Владелец хочет видеть то же в веб-приложении: что закрылось и +поменялось за последние часы или за день, и как менялась задача — в её карточке. Решение владельца 22.10.2026: делать +веб-часть после редизайна (#69), теперь он закрыт; детали интерфейса владелец отдал на усмотрение («начинай»). + +## What Changes + +- Новый раздел «Лента» в боковой панели и страница `/app/feed`: период (2 часа / Сегодня / Вчера / 7 дней), флажки видов + событий, события сгруппированы по задаче, ключ открывает карточку окном. +- Блок «История» в карточке задачи. +- Бэкенд не меняется. + +## Non-Goals + +- Свой интервал дат (календарь) — переключателя периодов достаточно; при нужде — позже. +- Лента по всем проектам сразу и по соседям проекта. +- Уведомления о новых событиях, автообновление по таймеру. +- Отображение текста правки описания (было/стало) — только факт правки. + +## Anti-Goals + +- Лента молча показывает не тот период (часовой пояс). +- Страница или карточка ломается на старом хабе без `changes`. diff --git a/openspec/changes/web-feed/specs/web-app/spec.md b/openspec/changes/web-feed/specs/web-app/spec.md new file mode 100644 index 0000000..d05cde1 --- /dev/null +++ b/openspec/changes/web-feed/specs/web-app/spec.md @@ -0,0 +1,93 @@ +## MODIFIED Requirements + +### Requirement: Оболочка и адреса +Приложение SHALL открываться под префиксом `/app/` и иметь страницы `/app/board` (доска связей), `/app/issues` (список +задач), `/app/feed` (лента изменений, требование «Лента изменений») и `/app/issues/{key}` (карточка задачи — окно поверх страницы, требование «Карточка задачи»); `/app` и `/app/` SHALL вести на `/app/board`, неизвестный путь — +на страницу «не найдено» со ссылками на разделы. Боковая панель SHALL быть закреплена у левого края окна на всю его +высоту и MUST NOT прокручиваться вместе со страницей; страница занимает остальную ширину. Сверху вниз панель SHALL +содержать: название «Хаб Jira» и кнопку «Свернуть панель»; выпадающий список проектов («<метка> — <название>», текущий +отмечен, в конце «+ Новый проект…»; требование «Проект в приложении»); разделы «Доска связей», «Задачи» и «Лента» — вертикальным +списком ссылок со значками (текущий отмечен `aria-current="page"`); внизу — блок данных Jira с действием «Обновить из +Jira» (требование «Обновление из Jira»), переключатель темы (требование «Тема») и меню с действием «Сменить токен». +Свёрнутая панель SHALL быть узкой полосой с теми же действиями значками: «Развернуть панель», метка текущего проекта, +разделы, «Обновить из Jira» с точкой состояния агента, тема, меню; названия — в подсказках и `aria-label`. Свёртка +SHALL храниться в `localStorage` под ключом `jira-hub-sidebar` (`collapsed`; без записи — развёрнута). На экранах уже +640 px панели в потоке нет: сверху SHALL быть закреплена строка с кнопкой «Меню», названием и кнопкой «Обновить из +Jira»; «Меню» открывает развёрнутую панель поверх страницы, панель закрывается выбором раздела, клавишей Escape или +щелчком мимо неё. Горизонтальной прокрутки страницы на любой ширине нет. +Неожиданная ошибка отрисовки страницы SHALL показываться сообщением с кнопкой «Перезагрузить», не пустым экраном. + +#### Scenario: Переход между разделами +- **WHEN** на доске в боковой панели выбран раздел «Задачи» +- **THEN** адрес — `/app/issues` с текущим проектом, показан список задач; «назад» возвращает на доску + +#### Scenario: Раздел «Лента» +- **WHEN** на доске в боковой панели выбран раздел «Лента» +- **THEN** адрес — `/app/feed` с текущим проектом, показана лента изменений за сегодня; раздел отмечен в панели + +#### Scenario: Сменить токен +- **WHEN** в меню боковой панели выбрано «Сменить токен» +- **THEN** токен очищен, показан экран входа + +#### Scenario: Панель на месте при прокрутке +- **WHEN** доска прокручена до самого низа +- **THEN** боковая панель видна целиком на прежнем месте: разделы и «Обновить из Jira» доступны без прокрутки вверх + +#### Scenario: Свёрнутая панель +- **WHEN** нажата «Свернуть панель», страница перезагружена +- **THEN** панель свёрнута в полосу значков, доска шире; значок «Задачи» открывает список задач; «Развернуть панель» + возвращает полную панель + +#### Scenario: Узкий экран +- **WHEN** ширина окна 390 px, нажата «Меню», в панели выбран раздел «Задачи» +- **THEN** панель открылась поверх страницы, после выбора закрыта, показан список задач; горизонтальной прокрутки нет + +## ADDED Requirements + +### Requirement: Лента изменений +Страница `/app/feed?project=<метка>` SHALL показывать события журнала изменений своих задач текущего проекта за период +(`GET /changes` с `project`, `since`, `until`, `kinds`). Период SHALL выбираться переключателем «2 часа» / «Сегодня» / +«Вчера» / «7 дней» (по умолчанию «Сегодня»; границы — по местному времени браузера: «Сегодня» — с начала текущих суток +до сейчас, «Вчера» — с начала прошлых суток до начала текущих) и храниться в адресе (`period=2h|today|yesterday|7d`; +«Сегодня» в адрес не пишется). Виды событий SHALL выбираться флажками «Статусы», «Описание», «Сроки», «Комментарии», +«Прочие поля» (по умолчанию все, кроме «Прочие поля») и храниться в адресе (`kinds=` через запятую; набор по умолчанию в +адрес не пишется); снятые все флажки — сообщение «выберите хотя бы один вид», запроса нет. + +События SHALL быть сгруппированы по задаче; задачи — по времени последнего события, новые сверху. У группы: ключ — +ссылка на карточку задачи (окно поверх ленты, требование «Карточка задачи»), тема и текущий статус; под ней события по +времени, новые сверху, каждое — время (`ЧЧ:ММ`, с датой `ДД.ММ`, если событие не в текущих сутках), автор и текст: +статус — «статус «было» → «стало»»; описание — «описание изменено»; срок, плановое начало и окончание — «срок ДД.ММ → +ДД.ММ» (дата по значению `from_value`/`to_value`, иначе строка журнала; пустое — «—»); комментарий — «комментарий:» и +первые 300 символов текста одной строкой с «…»; прочие поля — «<название поля>: «было» → «стало»» (значения до 80 +символов с «…»), где название — подпись из словаря приложения (исполнитель, спринт, метки, приоритет, тема, эпик, связь, +вложение, резолюция), иначе имя поля Jira. Подзаголовок страницы — число событий и границы периода. Событий нет — +сообщение «за период изменений нет»; `truncated` — сообщение «показаны последние N событий — выберите период короче». +Ошибка хаба — сообщение с кнопкой «Повторить». Ленту SHALL перечитывать завершение «Обновить из Jira» (вместе со списками +задач) и возврат на вкладку браузера. + +#### Scenario: Что закрылось за два часа +- **WHEN** открыта лента проекта AWPCA, выбран период «2 часа», сняты все флажки, кроме «Статусы» +- **THEN** адрес — `/app/feed?project=AWPCA&period=2h&kinds=status`; запрос `GET /changes?project=AWPCA&since=<сейчас − 2 ч>&kinds=status`; + задачи сгруппированы, у каждой — события «ЧЧ:ММ автор: статус «В работе» → «Готово»» + +#### Scenario: Карточка из ленты +- **WHEN** в ленте нажат ключ AURORA-9002 +- **THEN** поверх ленты открыто окно карточки AURORA-9002; закрытие окна возвращает ленту с тем же периодом + +#### Scenario: Пустой период +- **WHEN** за выбранный период событий нет +- **THEN** показано «за период изменений нет» + +#### Scenario: Вчера +- **WHEN** выбран период «Вчера», сейчас 22.10 14:05 местного времени +- **THEN** запрос с `since` = 21.10 00:00 и `until` = 22.10 00:00 местного времени + +### Requirement: История в карточке задачи +Карточка задачи SHALL показывать в основной колонке после комментариев блок «История (N)» — события `changes` из +`GET /issues/{key}` (все виды, новые сверху) в том же виде, что в ленте, всегда с датой. Видны 10 последних событий, +остальные — по кнопке «Показать все (N)»; без событий — подсказка «изменений нет»; ответ хаба без поля `changes` — +блока нет. + +#### Scenario: Карточка с историей +- **WHEN** открыта карточка задачи с 12 событиями журнала +- **THEN** блок «История (12)» показывает 10 последних событий и кнопку «Показать все (12)»; после нажатия — все 12