sdlc-hub/docs/superpowers/plans/2026-10-17-hide-closed.md
Andrey Epifancev 43fba6954a План: hide-closed (#57)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:24:04 +04:00

306 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Закрытые задачи: скрыть в списке, приглушить на доске — 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:** закрытые задачи (статус Jira из настройки `closed_statuses`, по умолчанию «Готово», «Снято») скрыты в списке
по умолчанию и приглушены на доске; «статуса по факту» в веб-приложении нет.
**Architecture:** хаб получает настройку `closed_statuses` и фильтр `GET /issues?hide_closed=1` (фильтр на сервере —
список порциями по 50). Приложение берёт набор из `GET /settings` одним помощником `web/src/api/closed.ts`; список
шлёт `hide_closed`, доска вешает классы при отрисовке (модель и golden-эталоны не меняются), карточка считает
«просрочено» по статусу Jira. Поле `fact_status` уходит из кода приложения, в API и базе остаётся.
**Tech Stack:** Python 3.13 (stdlib http, SQLite), pytest; React 19 + TypeScript + Vite, TanStack Query, Vitest,
Testing Library, Playwright.
**Spec:** `openspec/changes/hide-closed/` (proposal.md, design.md, specs/web-app, specs/hub-api, specs/data-model)
## Global Constraints
- Закрытые статусы по умолчанию — ровно `["Готово", "Снято"]`; сравнение — точное совпадение строки статуса.
- `closed_statuses` не входит в `AGENT_KEYS`.
- Задача без статуса (`NULL` / `""`) — не закрыта.
- Без `closed_statuses` в настройках (или не список) — ничего не закрыто, сообщения «нет словаря» нет.
- Адрес списка: по умолчанию параметра нет (закрытые скрыты); снятый флажок — `closed=1`; `closed` в `GET /issues` не
уходит; `fact_status` из адреса игнорируется.
- Выбранный в «Статус Jira» статус из `closed_statuses` — `hide_closed` не отправляется.
- Доска: модель, видимая выборка, критический путь, проблемы, раскладка, экспорт JSON от приглушения не зависят —
golden-эталоны `web/src/board/__fixtures__/*.expected.json` не перегенерировать.
- `dangerouslySetInnerHTML` не появляется; новых зависимостей нет.
- Язык комментариев, коммитов, подписей — русский; коммиты заканчиваются строкой
`Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`.
- Зелёным считается только весь набор: `make check` (pytest + lint и тесты фронта) и `cd web && npm run e2e`.
## Review Focus
1. Список, когда настройки ещё не загружены, а в адресе `status=Готово`: таблица не должна навсегда остаться пустой —
после загрузки настроек запрос уходит без `hide_closed` (тест в Task 2).
2. Карточка-сосед, она же закрытая (`out` + `closed`): должна быть прозрачнее соседа, а не `.7` — порядок правил CSS
(тест класса в Task 4, правило `.card.closed` ниже `.card.out` в `board.css`).
3. Сохранение заметки у задачи с непустым `fact_status`: `PUT` не должен отправить `fact_status: null` (тест в Task 3).
4. `PUT /settings {closed_statuses: "Готово"}` (строка вместо списка) — 422, а не поломка фильтра (тест в Task 1).
5. Пустой `closed_statuses = []` — `hide_closed=1` ничего не скрывает и не падает на `NOT IN ()` (тест в Task 1).
---
### Task 1: Хаб — настройка `closed_statuses` и фильтр `hide_closed`
**Files:**
- Modify: `src/jira_hub/settings.py` (`DEFAULTS`)
- Modify: `src/jira_hub/mirror.py` (`list_issues`)
- Modify: `src/jira_hub/api.py` (`ISSUE_FILTERS`, `h_issues`)
- Modify: `README.md:153` (строка `GET /issues` — добавить `hide_closed`), `ARCHITECTURE.md:50` (словари из
`GET /settings` — добавить `closed_statuses`, убрать `status_labels`: приложение его больше не читает)
- Test: `tests/test_settings.py`, `tests/test_mirror.py`, `tests/test_api.py`
**Interfaces:**
- Produces: настройка `closed_statuses: list[str]` в `GET /settings`; `GET /issues?hide_closed=1`;
`mirror.list_issues(conn, filters, limit, offset, closed_statuses=())` — новый необязательный параметр.
- [ ] **Step 1: Write the failing tests**
`tests/test_settings.py`:
```python
def test_closed_statuses_default(conn):
assert settings.get(conn, "closed_statuses") == ["Готово", "Снято"]
assert "closed_statuses" not in settings.AGENT_KEYS
assert "closed_statuses" not in settings.for_agent(conn)
def test_closed_statuses_must_be_list():
with pytest.raises(ValueError):
settings.validate("closed_statuses", "Готово")
settings.validate("closed_statuses", ["Готово"])
```
(`conn` — фикстура модуля, уже вызывает `ensure_defaults`; если нет — вызвать.)
`tests/test_mirror.py`:
```python
def test_list_issues_hide_closed(conn):
mirror.apply_state(conn, [issue("A-1", status="В работе"), issue("A-2", status="Готово"),
issue("A-3", status="Снято"), issue("A-4", status=None)], [])
closed = ["Готово", "Снято"]
keys = lambda f, c=closed: sorted(i["key"] for i in mirror.list_issues(conn, f, closed_statuses=c))
assert keys({"hide_closed": "1"}) == ["A-1", "A-4"]
assert keys({"hide_closed": "true"}) == ["A-1", "A-4"]
assert keys({}) == ["A-1", "A-2", "A-3", "A-4"]
assert keys({"hide_closed": "0"}) == ["A-1", "A-2", "A-3", "A-4"]
assert keys({"hide_closed": "1"}, []) == ["A-1", "A-2", "A-3", "A-4"] # пустой набор — ничего не скрыто
assert keys({"hide_closed": "1", "status": "Готово"}) == [] # сервер не отменяет — это делает приложение
```
(если помощник `issue()` не принимает `status` — передать его так же, как `sprint` в соседнем тесте.)
`tests/test_api.py` — сценарий «Без закрытых» из `specs/hub-api`: четыре задачи как выше, `GET /issues?hide_closed=1`
→ ключи `A-1`, `A-4`; без параметра — все четыре; после `PUT /settings {closed_statuses: ["Готово"]}` —
`hide_closed=1` отдаёт `A-1`, `A-3`, `A-4`. И `PUT /settings {closed_statuses: "Готово"}` → 422, значение не
изменилось (добавить случай в параметризацию `test_settings_put_invalid_value_is_422`, если она по ключу/значению).
- [ ] **Step 2: Run tests to verify they fail**
Run: `uv run pytest tests/test_settings.py tests/test_mirror.py tests/test_api.py -k "closed" -v`
Expected: FAIL (`closed_statuses` нет в настройках, `list_issues` не знает `closed_statuses`).
- [ ] **Step 3: Implement**
- `settings.DEFAULTS["closed_statuses"] = ["Готово", "Снято"]` — тип проверит существующая ветка `validate` по
значению по умолчанию (список).
- `mirror.list_issues(conn, filters=None, limit=50, offset=0, closed_statuses=())`: при `filters["hide_closed"]` из
`(True, 1, "1", "true")` и непустом `closed_statuses` — условие
`(i.status IS NULL OR i.status NOT IN (?, …))` с плейсхолдерами по числу статусов.
- `api.h_issues`: `hide_closed` в разобранные фильтры (добавить в `ISSUE_FILTERS`), `closed_statuses` — из
`settings.get(conn, "closed_statuses")`, если это список строк, иначе `()`.
- README и ARCHITECTURE — по строкам из **Files**.
- [ ] **Step 4: Run tests to verify they pass**
Run: `uv run pytest` — весь набор зелёный.
- [ ] **Step 5: Commit**
```bash
git add src/jira_hub tests README.md ARCHITECTURE.md
git commit -m "Хаб: настройка closed_statuses и фильтр GET /issues?hide_closed (#57)"
```
---
### Task 2: Приложение — помощник закрытых статусов и список «Задачи»
**Files:**
- Create: `web/src/api/closed.ts`, `web/src/api/closed.test.ts`
- Modify: `web/src/api/types.ts` (`HubSettings`: `closed_statuses?: string[]`; убрать `status_labels` и комментарий о нём)
- Modify: `web/src/api/queries.ts` (`ISSUE_FILTER_NAMES` без `fact_status`; `IssueFilters` допускает `hide_closed`)
- Modify: `web/src/issues/IssuesPage.tsx`, `web/src/issues/options.ts`
- Modify: `web/src/board/__fixtures__/settings.json` (добавить `"closed_statuses": ["Готово", "Снято"]`)
- Test: `web/src/issues/IssuesPage.test.tsx`, `web/e2e/issues.spec.ts`
**Interfaces:**
- Produces (`web/src/api/closed.ts`):
- `closedStatuses(s: HubSettings | undefined): Set<string>` — строки из `s.closed_statuses`, если это массив; иначе
пустой набор.
- `isClosed(status: string | null | undefined, closed: Set<string>): boolean` — `!!status && closed.has(status)`.
- `useClosedStatuses(): Set<string>` — `closedStatuses(useSettings().data)`, мемоизировано по `settings.data`.
- Consumes: `GET /issues?hide_closed=1` (Task 1).
- [ ] **Step 1: Write the failing tests**
`closed.test.ts`: `closedStatuses({closed_statuses: ["Готово", "Снято"]})` — набор из двух; `closedStatuses({})`,
`closedStatuses(undefined)`, `closedStatuses({closed_statuses: "Готово"})` — пустой; `isClosed("Снято", set)` — true,
`isClosed("", set)`, `isClosed(null, set)`, `isClosed("В работе", set)` — false.
`IssuesPage.test.tsx` (хаб из `hub()` уже отдаёт `settings.json` с `closed_statuses`):
- «закрытые скрыты по умолчанию»: открыт `/issues` → флажок «скрыть закрытые» (`getByRole("checkbox", {name:
"скрыть закрытые"})`) отмечен, последний вызов `/issues` содержит `hide_closed=1`, не содержит `closed`; снятие
флажка → адрес содержит `closed=1`, последний вызов `/issues` без `hide_closed`.
- «адрес closed=1»: открыт `/issues?closed=1` → флажок снят, запрос без `hide_closed`.
- «выбран закрытый статус»: открыт `/issues?status=Готово` → запрос `status=Готово` без `hide_closed`; открыт
`/issues?status=В работе` → с `hide_closed=1`.
- «настройки позже» (Review Focus 1): ответ `/settings` задержан (`deferred`), адрес `/issues?status=Готово`; после
выдачи настроек последний вызов `/issues` — без `hide_closed`.
- «нет статуса по факту»: нет `combobox` «Статус по факту», нет заголовка колонки «Статус по факту»; открыт
`/issues?fact_status=В работе` → в вызове `/issues` нет `fact_status`.
- Существующие тесты, которые сравнивают точный набор параметров `/issues`, поправить на `hide_closed=1`.
`web/e2e/issues.spec.ts`: в тесте «окно поверх списка» ожидание `toHaveCount(10)` → `9` (AURORA-9006 «Готово»
скрыта); новый тест: `/app/issues` — AURORA-9006 нет в таблице; снят флажок «скрыть закрытые» — `toHaveURL(/closed=1/)`,
AURORA-9006 есть, строк 10.
- [ ] **Step 2: Run tests to verify they fail**
Run: `cd web && npx vitest run src/api/closed.test.ts src/issues/IssuesPage.test.tsx`
Expected: FAIL (нет модуля `closed.ts`, нет флажка).
- [ ] **Step 3: Implement**
- `closed.ts` — по Interfaces.
- `IssuesPage`: из `SELECTS` убрать `fact_status`, из таблицы — колонку «Статус по факту»; `showClosed =
params.get("closed") === "1"`; флажок `<label className="check"><input type="checkbox" checked={!showClosed} …
/>скрыть закрытые</label>` рядом с «только соседи», снятие ставит `closed=1`, отметка — удаляет `closed`;
в `useIssues` уходят `filters` плюс `hide_closed: "1"`, если `!showClosed && !isClosed(filters.status, closed)`.
- `options.ts`: убрать `fact_status` из `filterOptions`.
- [ ] **Step 4: Run tests to verify they pass**
Run: `make check`, затем `cd web && npm run e2e`
Expected: всё зелёное.
- [ ] **Step 5: Commit**
```bash
git add web
git commit -m "Список задач: «скрыть закрытые» по умолчанию, без статуса по факту (#57)"
```
---
### Task 3: Карточка задачи и заметки без статуса по факту
**Files:**
- Modify: `web/src/issues/card.ts` (`isOverdue`; удалить `factClass`, `CLOSED_FACT`)
- Modify: `web/src/issues/IssueCard.tsx` (шапка без бейджа факта; `isOverdue` по статусу Jira)
- Modify: `web/src/issues/Notes.tsx` (`ROWS` без `fact_status`), `web/src/issues/NoteForm.tsx` (поле, `datalist`,
`facts`, `useId` если больше не нужен)
- Modify: `web/src/api/types.ts` (`NOTE_FIELDS` без `fact_status`; тип `Note` — `fact_status?: string | null` остаётся
необязательным полем ответа)
- Modify: `web/src/issues/issues.css` (удалить `.badge.fact*`)
- Modify: `web/src/test/issueFixtures.ts` только если типы требуют
- Test: `web/src/issues/card.test.ts`, `web/src/issues/IssueCard.test.tsx`
**Interfaces:**
- Consumes: `useClosedStatuses()`, `closedStatuses`, `isClosed` (Task 2).
- Produces: `isOverdue(due: string, status: string | null | undefined, closed: Set<string>, today: string): boolean`.
- [ ] **Step 1: Write the failing tests**
`card.test.ts` (заменить тесты `factClass` и R-2):
```ts
const closed = new Set(["Готово", "Снято"]);
it("срок прошёл, статус не закрытый — просрочено", () => {
expect(isOverdue("2026-10-01", "В работе", closed, "2026-10-17")).toBe(true);
expect(isOverdue("2026-10-01", null, closed, "2026-10-17")).toBe(true);
});
it("закрытая задача или срок не прошёл — не просрочено", () => {
expect(isOverdue("2026-10-01", "Готово", closed, "2026-10-17")).toBe(false);
expect(isOverdue("2026-10-01", "Снято", closed, "2026-10-17")).toBe(false);
expect(isOverdue("2026-10-17", "В работе", closed, "2026-10-17")).toBe(false);
expect(isOverdue("", "В работе", closed, "2026-10-17")).toBe(false);
});
```
`IssueCard.test.tsx`:
- шапка: нет элемента `.badge.fact` и текста «факт:»; строка фактов как прежде с «просрочено» (у фикстуры статус не
закрытый); новый тест — та же задача со `status: "Готово"` → «просрочено» нет (сценарий «Просрочка закрытой задачи»).
- заметки в чтении: нет `dt` «Статус по факту»; в форме нет поля с подписью «Статус по факту» (заменить тест
со строки ~390 про `datalist`).
- Review Focus 3: у заметки `fact_status: "В работе"`, в форме изменено только «Осталось», «Сохранить» → тело `PUT` —
ровно `{rest_text: "…", updated_by: "web"}`, ключа `fact_status` нет.
- [ ] **Step 2: Run tests to verify they fail**
Run: `cd web && npx vitest run src/issues/card.test.ts src/issues/IssueCard.test.tsx`
Expected: FAIL.
- [ ] **Step 3: Implement** — по Files и Interfaces; в `IssueCard` набор — `useClosedStatuses()`, вызов
`isOverdue(due, i.status, closed, todayISO())`.
- [ ] **Step 4: Run tests to verify they pass**
Run: `make check`; `grep -rn "fact_status\|status_labels\|factClass" web/src --exclude-dir=__fixtures__` — остаются
только необязательные поля типов ответа API (`BoardIssue`, `IssueRow`, `Note`) и тестовые фикстуры.
- [ ] **Step 5: Commit**
```bash
git add web
git commit -m "Карточка задачи: без статуса по факту, «просрочено» по статусу Jira (#57)"
```
---
### Task 4: Доска — приглушение закрытых карточек и стрелок
**Files:**
- Modify: `web/src/board/components/Card.tsx` (`CardProps.closed?: boolean`, класс `closed`, `cardPropsEqual`)
- Modify: `web/src/board/components/Edges.tsx` (`closedKeys: Set<string>`, `className="closed"` у `<path>`)
- Modify: `web/src/board/components/BoardCanvas.tsx` (`BoardCanvasProps.closedStatuses?: Set<string>`)
- Modify: `web/src/board/BoardPage.tsx` (передать `useClosedStatuses()`)
- Modify: `web/src/board/board.css`
- Test: `web/src/board/components/BoardCanvas.test.tsx`, `web/src/board/components/Card.test.ts`
**Interfaces:**
- Consumes: `useClosedStatuses()`, `isClosed` (Task 2).
- Produces: `BoardCanvas` принимает `closedStatuses` (по умолчанию пустой набор); `Card` — `closed`; `Edges` —
`closedKeys` (ключи закрытых задач среди `vis.nodes`, считаются в `BoardCanvas` через `useMemo`).
- [ ] **Step 1: Write the failing tests**
`BoardCanvas.test.tsx` — через `baseProps`/`draw` с `vis`, где AURORA-1620 «В работе», AURORA-1621 «Готово»,
AURORA-1622 «Снято», связь AURORA-1620 → AURORA-1621, `closedStatuses: new Set(["Готово", "Снято"])`:
- `[data-key="AURORA-1621"]` и `[data-key="AURORA-1622"]` имеют класс `closed`, `[data-key="AURORA-1620"]` — нет;
- `<path>` связи в `svg.edges` имеет класс `closed`;
- без `closedStatuses` — ни у одной карточки и стрелки класса `closed` нет;
- закрытая карточка-сосед (`dim: true`, «Готово») — классы `out` и `closed` оба.
`Card.test.ts`: `cardPropsEqual` возвращает `false`, если отличается только `closed`.
CSS проверяется правилом в `board.css` (jsdom стили не считает): `.card.closed{opacity:.4}` стоит после `.card.out`.
- [ ] **Step 2: Run tests to verify they fail**
Run: `cd web && npx vitest run src/board/components`
Expected: FAIL.
- [ ] **Step 3: Implement**
- CSS (после правила `.card.out`): `.card.closed{opacity:.4}`,
`.card.closed:hover,.card.closed:focus-within{opacity:1}`, `.edges path.closed{opacity:.4}`.
- Карточки слоя измерения (`BoardCanvas.tsx:204`) класс `closed` не получают — на размер он не влияет.
- [ ] **Step 4: Run tests to verify they pass**
Run: `make check`, `cd web && npm run e2e` (golden-тесты доски — без перегенерации эталонов).
Expected: всё зелёное.
- [ ] **Step 5: Commit**
```bash
git add web
git commit -m "Доска: закрытые задачи и их связи приглушены (#57)"
```