From ef9abfa8bdd5530fa107b5c3eaf26354787f44ff Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 25 Oct 2026 20:08:15 +0400 Subject: [PATCH] =?UTF-8?q?docs:=20=D0=BF=D0=BB=D0=B0=D0=BD=20=D0=B4=D0=BE?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BE=D0=BA=20=D1=8D=D0=BA=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=B4=D0=BE=D1=81=D0=BA=D0=B8=20(#1?= =?UTF-8?q?12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../plans/2026-10-25-agile-web-polish.md | 323 ++++++++++++++++++ openspec/changes/agile-web-polish/design.md | 5 + 2 files changed, 328 insertions(+) create mode 100644 docs/superpowers/plans/2026-10-25-agile-web-polish.md diff --git a/docs/superpowers/plans/2026-10-25-agile-web-polish.md b/docs/superpowers/plans/2026-10-25-agile-web-polish.md new file mode 100644 index 0000000..53ff738 --- /dev/null +++ b/docs/superpowers/plans/2026-10-25-agile-web-polish.md @@ -0,0 +1,323 @@ +# Доработки «Бэклога» и «Спринта» (#112) — план реализации + +> **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:** фильтры, свёртка, панель эпиков, выбор спринтов, скрытие колонок, значки типов и доска во всю высоту окна на +страницах «Бэклог», «Спринт», «Kanban» веб-приложения sdlc-hub — только просмотр. + +**Architecture:** только фронт (`web/src/modules/jira/agile/`), данные — прежний `GET /agile`. Фильтры — чистая +модель `filters.ts` + запоминание `agilePref.ts` + хук `useAgileView`; страницы и компоненты только рисуют. Общие +части — панель фильтров `AgileToolbar`, меню `ColumnsMenu`, `BoardColumns`. + +**Tech Stack:** React 19 + TypeScript + Vite, TanStack Query, Vitest + Testing Library, Playwright; UI-кит +`web/src/ui`, токены дизайн-системы AURA. + +**Spec:** `openspec/changes/agile-web-polish/` (требование «Спринты и бэклог» в `specs/web-app/spec.md`, решения — +`design.md`, рулинги дизайна R-1…R-13; рулинги контроллера — с R-14). + +## Global Constraints + +- Веб только смотрит: MUST NOT появляться действий, меняющих Jira; ни одна кнопка не ставит команд. +- Сервер, API, схема, агент, MCP — не меняются (никаких правок вне `web/` и `openspec/`/`docs/`, кроме сида + `web/scripts/seed-hub.py`). +- Ключ хранилища — ровно `sdlc-hub-agile`; JSON `{ "<метка>": { "backlog"|"sprint"|"kanban": AgileView } }`; поиск + (`q`) и выбор спринтов не хранятся; любая ошибка хранилища — значения по умолчанию, без исключения наружу. +- Тексты интерфейса — дословно из спеки: «Ключ или тема», «Исполнитель», «все», «Без исполнителя», «Скрыть + выполненные», «Сбросить фильтры», «Эпики», «Все задачи», «Без эпика», «Эпик», «Колонки», «Спринты», «показано K», + «Нет задач», «Нет задач по фильтрам», «Нет активного спринта», «Без колонки». +- Стили — только токены дизайн-системы (как в `agile.css`), без новых цветов-литералов. +- Работа только в worktree `/Users/andrey.epifancev/projects/jira-bridge/sdlc-hub/.wt/agile-polish`: никаких записей + в основной checkout и в другие репозитории (O-6 #108); никаких команд, ждущих stdin (`cat > файл` без heredoc и т.п.). +- Зелёный = `make check` целиком (pytest + `npm run lint` + `npm test`) и `cd web && npm run e2e` целиком. + +## Review Focus + +1. **Задача в двух выбранных спринтах** (перенос из активного в будущий) — на доске одна карточка, не две → тест в Task 2 + (`sprintIssues`) и Task 6. +2. **Сохранённое значение, которого больше нет** (исполнитель ушёл, эпик закрыт и пропал, колонку переименовали в + Jira) — значение остаётся видимым и снимаемым, страница не пустеет молча → тесты в Task 2 (`assigneeOptions`), Task 5, + Task 7. +3. **Испорченное хранилище** (`sdlc-hub-agile` = `"oops"`, `{"AWPCA": 5}`, поле `collapsed: "x"`, `setItem` бросает) + — страница открывается со значениями по умолчанию → тесты в Task 2. +4. **Проект сменён на открытой странице** — фильтры другого проекта не протекают (страница пересоздаётся по `key` + проекта, состояние читается по метке) → тест в Task 4. +5. **Длинная колонка и узкое окно** — 900 px: страница без горизонтальной прокрутки, доска прокручивается внутри, + полоса прокрутки доски в окне; 2560 px — колонки используют ширину окна → e2e в Task 8. + +--- + +### Task 0: Окружение worktree (без коммита) + +- [ ] `cd /Users/andrey.epifancev/projects/jira-bridge/sdlc-hub/.wt/agile-polish && uv sync && (cd web && npm ci)` +- [ ] `make check` — зелёный (база до правок; запомнить число тестов в ledger). + +### Task 1: Формат SP и значки типов + +**Files:** +- Modify: `web/src/modules/jira/agile/model.ts` (`fmtSp`, новое `typeKind`) +- Modify: `web/src/modules/jira/agile/TypeIcon.tsx`, `web/src/modules/jira/agile/agile.css` +- Modify: `web/src/ui/icons.tsx` (новые символы) +- Test: `web/src/modules/jira/agile/model.test.ts`, новый `web/src/modules/jira/agile/TypeIcon.test.tsx` + +**Interfaces:** +- Produces: `fmtSp(n: number | null): string`; `type TypeKind = "epic" | "story" | "task" | "bug" | "subtask" | "other"`; + `typeKind(type: string | null): TypeKind`; `isEpic(i: { issue_type: string | null }): boolean` — всё в `model.ts`. + Значки: `IconBookmark`, `IconCircle`, `IconSquares`, `IconDot` в `web/src/ui/icons.tsx`. + +- [ ] **Step 1: тесты.** `model.test.ts`: `fmtSp(null) === "—"`, `fmtSp(3) === "3"`, `fmtSp(0.1 + 0.2) === "0.3"`, + `fmtSp(2.5) === "2.5"`, `fmtSp(1/3) === "0.33"`, `fmtSp(0) === "0"`. `typeKind`: «Эпик», «epic», «Фича» → `epic`; «История», «Story» → `story`; «Задача», «Task» → + `task`; «Ошибка», «BUG» → `bug`; «Подзадача», «Sub-task», «Subtask» → `subtask`; `null`, `""`, «Риск» → `other`. + `TypeIcon.test.tsx`: для каждого вида — `role="img"` с `aria-label` = исходное имя типа (для `null` — «Задача» не + подставлять: `aria-label="Тип не указан"`), класс `agile-type--`. +- [ ] **Step 2:** `cd web && npx vitest run src/modules/jira/agile/model.test.ts src/modules/jira/agile/TypeIcon.test.tsx` — FAIL. +- [ ] **Step 3: реализация.** `fmtSp`: `Number(n.toFixed(2))` → `String` (M-6). `typeKind` — сравнение без учёта + регистра со списками имён из спеки. Символы в `icons.tsx` тем же `make(...)`: закладка, круг, два квадрата, точка; + эпик — существующий `IconCritical` (молния), задача — `IconCheck`. `TypeIcon`: `` — квадрат 16 px с `border-radius:var(--radius-xs)`, фон — токен вида (эпик `--k-ui`, история + `--color-status-success`, задача и подзадача `--color-action-primary`, ошибка `--color-status-error`, прочее — + `--color-text-secondary`), символ 12 px цвета `--color-text-on-primary`. Контраст символа проверить в светлой и + тёмной теме; не хватает — рулинг с выбранным токеном, не литерал. +- [ ] **Step 4:** тот же vitest — PASS; `npm run lint` — PASS. +- [ ] **Step 5:** коммит `feat(web): значки типов по образцу Jira и формат SP (#112, M-6)`. + +### Task 2: Модель фильтров и запоминание + +**Files:** +- Create: `web/src/modules/jira/agile/filters.ts`, `web/src/modules/jira/agile/agilePref.ts` +- Test: `web/src/modules/jira/agile/filters.test.ts`, `web/src/modules/jira/agile/agilePref.test.ts` + +**Interfaces:** +- Consumes: `isEpic`, `typeKind` (Task 1); типы `AgileIssue`, `AgileSprint`, `AgileResponse` (`../api/types.ts`). +- Produces (`filters.ts`): + - `type AgilePage = "backlog" | "sprint" | "kanban"` + - `interface AgileView { assignee: string; epic: string; hideDone: boolean; collapsed: string[]; hiddenColumns: string[]; epicsPanel: boolean }` + - `const DEFAULT_VIEW: AgileView` = `{ assignee: "", epic: "", hideDone: false, collapsed: [], hiddenColumns: [], epicsPanel: true }` + - `const NONE = "-"` — значение «Без исполнителя» / «Без эпика» + - `interface IssueFilter { q: string; assignee: string; epic: string; hideDone: boolean }` + - `matches(i: AgileIssue, f: IssueFilter): boolean` — И всех условий; `q` — подстрока без учёта регистра в `key` или `summary` (пробелы по краям `q` отбрасываются) + - `hasFilters(f: IssueFilter): boolean` + - `type Opt = { value: string; label: string }` + - `assigneeOptions(issues: AgileIssue[], current: string): Opt[]` — `["", "все"]`, `[NONE, "Без исполнителя"]`, затем уникальные исполнители по `localeCompare("ru")`; `current`, которого нет, добавляется в конец с подписью = значению + - `interface EpicEntry { key: string; title: string; count: number; done: boolean }` + - `epicEntries(d: AgileResponse): EpicEntry[]` — по спеке: строки-эпики из всех спринтов и бэклога по `rank` (без повторов), затем ключи из `epic_key`, строк которых нет, по ключу (`title` = ключ, `done` = false); `count` — задачи-не-эпики с этим `epic_key` в активных, будущих спринтах и бэклоге (без повторов по ключу) + - `byRank(a: AgileIssue, b: AgileIssue): number` — `rank` строкой, `null` в конце, затем по `key` + - `sprintIssues(sprints: AgileSprint[]): AgileIssue[]` — объединение без повторов по `key`, сортировка `byRank` + - `selectableSprints(sprints: AgileSprint[]): AgileSprint[]` — активные (в порядке входа), затем будущие по `start_date` (через `jiraDate`), без даты — в конце + - `parseSprintIds(param: string | null, sprints: AgileSprint[]): number[]` — id из `"101,102"`, только среди `selectableSprints`, порядок `selectableSprints`; пусто → id всех активных +- Produces (`agilePref.ts`): `const AGILE_KEY = "sdlc-hub-agile"`; `readView(label: string, page: AgilePage): AgileView`; + `saveView(label: string, page: AgilePage, v: AgileView): void` (не трогает другие страницы и проекты в JSON). + +- [ ] **Step 1: тесты `filters.test.ts`** (фабрики `iss`/`sprint` — как в `BacklogPage.test.tsx`): `matches` по каждому + условию и их И (`q: " aurora-1 "` находит `AURORA-1` и `AURORA-10`; `q: "тема"` — по теме; `assignee: NONE` — только + `null`; `epic: NONE` — только `epic_key === null`; `hideDone`); `hasFilters` для пустого и каждого поля; + `assigneeOptions` — порядок, «Без исполнителя», ушедший `current` в конце; `epicEntries` на данных сценария «Фильтр + по эпику» спеки (`AURORA-10`: 3 в бэклоге + 2 в активном → `count` 5), эпик, на который ссылаются, но строки нет, + — в конце с `title` = ключ, задачи прошлых спринтов не считаются; `sprintIssues` — задача в двух спринтах одна, + `rank: null` в конце; `selectableSprints` — закрытые отброшены, будущий без даты последним; `parseSprintIds` — + `"102,999"` → `[102]`, `"abc"` → все активные, `null` → все активные, будущий `"103"` → `[103]`. +- [ ] **Step 2: тесты `agilePref.test.ts`:** нет ключа → `DEFAULT_VIEW`; запись `AWPCA/backlog` не меняет + `AWPCA/sprint` и `STOCKS/backlog`; мусор: `"oops"`, `"[1]"`, `{"AWPCA": 5}`, `{"AWPCA":{"backlog":{"collapsed":"x","hideDone":"yes"}}}` + → по полю значения по умолчанию (`collapsed: []`, `hideDone: false`), годные поля сохраняются; `localStorage.setItem` + бросает (`vi.spyOn(Storage.prototype, "setItem")`) — `saveView` не бросает; `getItem` бросает — `readView` → + `DEFAULT_VIEW`. +- [ ] **Step 3:** `npx vitest run src/modules/jira/agile/filters.test.ts src/modules/jira/agile/agilePref.test.ts` — FAIL. +- [ ] **Step 4: реализация** по Interfaces; разбор хранилища — поле за полем с проверкой типа (строка, булево, массив строк). +- [ ] **Step 5:** тот же vitest — PASS; `npm run lint` — PASS. +- [ ] **Step 6:** коммит `feat(web): модель фильтров экранов доски и их запоминание (#112)`. + +### Task 3: Хук состояния и панель фильтров + +**Files:** +- Create: `web/src/modules/jira/agile/useAgileView.ts`, `web/src/modules/jira/agile/AgileToolbar.tsx` +- Modify: `web/src/modules/jira/agile/agile.css` +- Test: `web/src/modules/jira/agile/AgileToolbar.test.tsx` + +**Interfaces:** +- Consumes: Task 2 целиком. +- Produces: + - `useAgileView(label: string, page: AgilePage): { view: AgileView; set(patch: Partial): void; q: string; setQ(v: string): void; filter: IssueFilter; reset(): void }` + — `set` пишет в хранилище (`saveView`) вне функции-обновителя (как T8.1 #69); `reset` очищает `q`, `assignee`, + `epic`, `hideDone` и сохраняет остальное. + - `AgileToolbar({ filter, setQ, set, onReset, assignees, showHideDone, children }: { filter: IssueFilter; setQ(v: string): void; set(patch: Partial): void; onReset(): void; assignees: Opt[]; showHideDone: boolean; children?: ReactNode })` + — `Panel` с `SearchInput label="Поиск по ключу или теме" placeholder="Ключ или тема"`, `Select aria-label="Исполнитель"`, + при `showHideDone` — `Checkbox label="Скрыть выполненные"`, слот `children` (кнопка «Эпики», выбор спринтов, меню + «Колонки»), кнопка «Сбросить фильтры» только при `hasFilters(filter)`. Без кнопок, ставящих команды. + +- [ ] **Step 1: тесты `AgileToolbar.test.tsx`** (рендер с хуком в обёртке): ввод в поиск меняет `filter.q`; выбор + исполнителя сохраняется в `localStorage["sdlc-hub-agile"]` по метке и странице, `q` — нет; «Сбросить фильтры» + появляется после ввода и снимает всё; `showHideDone={false}` — флажка нет; повторный монтаж с той же меткой и + страницей — исполнитель и флажок восстановлены. +- [ ] **Step 2:** `npx vitest run src/modules/jira/agile/AgileToolbar.test.tsx` — FAIL. +- [ ] **Step 3: реализация**; стили `.agile-toolbar` (flex-wrap, gap `--space-3`, поиск растягивается) в `agile.css`. +- [ ] **Step 4:** тот же vitest — PASS; `npm run lint` — PASS. +- [ ] **Step 5:** коммит `feat(web): панель фильтров экранов доски (#112)`. + +### Task 4: «Бэклог»: фильтры, свёртка секций, без строк эпиков + +**Files:** +- Modify: `web/src/modules/jira/agile/BacklogPage.tsx`, `web/src/modules/jira/agile/agile.css` +- Test: `web/src/modules/jira/agile/BacklogPage.test.tsx` + +**Interfaces:** +- Consumes: `useAgileView(label, "backlog")`, `AgileToolbar`, `matches`, `isEpic`, `assigneeOptions` (Tasks 1–3). +- Produces: `SprintSection`/секция «Бэклог» принимают `collapsed: boolean; onToggle(): void; shown: AgileIssue[]` — + Task 5 добавляет фильтр эпика через тот же `filter`. + +- [ ] **Step 1: тесты** (дописать в `BacklogPage.test.tsx`, существующие оставить зелёными): + - заголовок секции — кнопка с `aria-expanded="true"`; щелчок — `false`, задач секции нет, итоги видны; после + повторного монтажа (`unmount` + `open`) секция свёрнута (сценарий «Свёрнутый активный спринт»); свернуть можно и + будущий, и «Бэклог»; + - эпик (`issue_type: "Фича"`) в бэклоге и в спринте — строкой не показан; «N задач» секции «Бэклог» его не считает; + - «Скрыть выполненные» на спринте из 10 задач (4 выполнены, `count: 10`): 6 строк, «10 задач» и «показано 6»; + прошлый спринт раскрыт — все его задачи (сценарий «Скрыть выполненные»); + - поиск «AURORA-50» — в «Бэклоге» одна строка, в секции спринта «Нет задач по фильтрам»; + - смена проекта (Review Focus 4): исполнитель выбран у `AWPCA`, открыт тот же адрес с другим scrum-проектом — + исполнитель «все». + - «Бэклог только смотрит»: на странице нет элементов с `draggable`, все кнопки — свёртка, панель фильтров или + раскрытие (перечислить по `getAllByRole("button")` и их именам). +- [ ] **Step 2:** `npx vitest run src/modules/jira/agile/BacklogPage.test.tsx` — новые FAIL. +- [ ] **Step 3: реализация.** Видимые задачи секции = `issues.filter(i => !isEpic(i) && matches(i, f))`; для прошлых + спринтов `hideDone` не применяется (`{...f, hideDone: false}`). «показано K» — когда `shown.length` меньше числа + задач-не-эпиков секции. Заголовок — `