20 KiB
Закрытые задачи: скрыть в списке, приглушить на доске — 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
- Список, когда настройки ещё не загружены, а в адресе
status=Готово: таблица не должна навсегда остаться пустой — после загрузки настроек запрос уходит безhide_closed(тест в Task 2). - Карточка-сосед, она же закрытая (
out+closed): должна быть прозрачнее соседа, а не.7— порядок правил CSS (тест класса в Task 4, правило.card.closedниже.card.outвboard.css). - Сохранение заметки у задачи с непустым
fact_status:PUTне должен отправитьfact_status: null(тест в Task 3). PUT /settings {closed_statuses: "Готово"}(строка вместо списка) — 422, а не поломка фильтра (тест в Task 1).- Пустой
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)"