diff --git a/docs/superpowers/plans/2026-10-17-hide-closed.md b/docs/superpowers/plans/2026-10-17-hide-closed.md new file mode 100644 index 0000000..9debd4f --- /dev/null +++ b/docs/superpowers/plans/2026-10-17-hide-closed.md @@ -0,0 +1,305 @@ +# Закрытые задачи: скрыть в списке, приглушить на доске — 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 `. +- Зелёным считается только весь набор: `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` — строки из `s.closed_statuses`, если это массив; иначе + пустой набор. + - `isClosed(status: string | null | undefined, closed: Set): boolean` — `!!status && closed.has(status)`. + - `useClosedStatuses(): Set` — `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"`; флажок `` рядом с «только соседи», снятие ставит `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, 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`, `className="closed"` у ``) +- Modify: `web/src/board/components/BoardCanvas.tsx` (`BoardCanvasProps.closedStatuses?: Set`) +- 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"]` — нет; +- `` связи в `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)" +```