# Доработки «Бэклога» и «Спринта» (#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. **Ветка:** `feat/112-agile-web-polish` (worktree `.wt/agile-polish`). **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` меньше числа задач-не-эпиков секции. Заголовок — `