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