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

20 KiB
Raw Permalink Blame History

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

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:

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
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
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):

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
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
git add web
git commit -m "Доска: закрытые задачи и их связи приглушены (#57)"