# Закрытые задачи: скрыть в списке, приглушить на доске — 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)" ```