2026-10-15 00:55:25 +04:00

146 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## Context
`main` d38fac8: веб-приложение хаба (`web/`, React 19 + TS + Vite, TanStack Query, React Router) под `/app/`, спека
`openspec/specs/web-app/`, хаб раздаёт сборку (`h_app` в `api.py`). Отложенные находки change `web-app` собраны в
jira-hub#29 (46 пунктов, маркеры сохраняются из PR #27). Устройство, на которое опираются правки:
- `api/client.ts` — единственный HTTP-клиент: `call()` берёт токен через `getToken()`, 401 → `AuthError`, сеть и 5xx →
`HubDownError`, прочие 4xx → `ApiError {status, error, detail}`. Токен очищается `clearToken("bad")` (из
`useBoardSource.report` и глобального обработчика ошибок `queryClient`), `AuthGate` показывает `LoginScreen`.
- `board/useBoardSource.ts` — источник доски: `source` (hub | file), `event` (одна строка состояния), `active`
(опрашиваемый пакет); подхват незавершённого пакета `viewer` по каждому `board.dataUpdatedAt` через `GET /batches`.
Состояние локально для `BoardPage` и пропадает при уходе со страницы; снимок хаба — в кэше TanStack Query,
раскладка и фильтры — в `localStorage`.
- `GET /batches` отдаёт пакеты без команд (`queue.list_batches`); `GET /batches/{id}` — с `commands[]` (`type`).
- `issues/IssuesPage.tsx` — фильтры в query-строке, варианты — из `filterOptions(board, settings)`.
- `index.html` подключает IBM Plex Sans/Mono с fonts.googleapis.com; `npm audit` — 1 high в `source-map-js@1.2.1`
(транзитивная dev-зависимость), исправление — 1.2.2 (2026-09-30).
## Goals / Non-Goals
**Goals:** закрыть jira-hub#29 целиком: каждый пункт — `fixed <commit>` или `rejected <причина>`; поведение доски на тех
же данных совпадает с текущим, кроме пунктов группы A; `make check` и e2e зелёные, e2e устойчив к повторам.
**Non-Goals:** см. proposal.md.
## Decisions
1. **Прокрутка доски (M-3).** При размонтировании `BoardPage` запоминает `window.scrollX/scrollY` (и `scrollLeft`
горизонтальной обёртки холста, если она прокручивается сама) в переменной модуля; при монтировании, после первой
отрисовки холста с данными, восстанавливает. Перезагрузка страницы открывает доску сверху, как сейчас. Состояние
источника не поднимается выше страницы — rejected часть M-3 (решение владельца).
2. **Строка состояния при идущем пакете (T10.3).** `reload()` и `loadFile()` не сбрасывают событие `batch`, пока
`active !== null`; сообщения о файле (`file-error`, `json-applied`) по-прежнему заменяют его (одно событие, M-5 #20), а
следующий ответ опроса возвращает строку пакета.
3. **Ошибка хаба при снимке из файла (T10.4).** Событие `hub-down` получает признак `fromFile`; текст — «хаб не отвечает —
на доске снимок из файла». Он ставится, когда снимок хаба не загрузился при показанном файле (перечитывание после
завершения пакета) и когда JSON-снимок загружен без данных хаба; `hasData` в последнем случае — есть ли в модели задачи.
«Перечитать» по-прежнему возвращает источник «хаб».
4. **Подхват пакета (T10.6, T10.1).** Кандидат из `GET /batches` (автор `viewer`, `queued`/`running`) проверяется запросом
`GET /batches/{id}`: подхватывается, только если первая команда — `refresh`. Подхват не запускается по обновлению
снимка, которое вызвал сам `finish()` (флаг «снимок после своего пакета»), — без лишнего `GET /batches` (T10.1).
5. **Токен в ошибке (T3.5).** `call()` кладёт в `AuthError` токен, с которым ушёл запрос; `clearToken` вызывается только
если этот токен равен текущему. Запоздавший 401 от прежнего токена игнорируется.
6. **Вход и 4xx (T3.2).** `LoginScreen`: `ApiError` → «хаб отверг запрос: <detail или error>»; `HubDownError` — прежний
текст «хаб не отвечает».
7. **Фильтр вне вариантов (T13.1).** Если значение фильтра из адреса не найдено в `opts[name]`, оно добавляется первым
вариантом с подписью «<значение> (нет на доске)» — как текущее значение «вида работы» в заметке.
8. **Карточка задачи (O-10, O-11).** Ссылки страниц задач — `color: var(--accent)` из темы (как в списке); пустой
`commands` — строка «команд по задаче не было» вместо пустой таблицы.
9. **NUL в пути (T1.1).** `h_app` отвечает 404 `not_found`, если в декодированном пути есть `\x00` или разрешение пути
падает с `ValueError`/`OSError`; остальное поведение `h_app` не меняется.
10. **Шрифты (T2.2).** `@fontsource/ibm-plex-sans` (400, 500, 600) и `@fontsource/ibm-plex-mono` (500), импорт CSS в
`main.tsx`; ссылка на Google Fonts из `index.html` удаляется. Файлы `woff2` попадают в `assets/`, хаб уже отдаёт
`font/woff2`. Версии — точные, `npm install --before=2026-10-01 --save-exact`.
11. **source-map-js (O-1).** `overrides: {"source-map-js": "1.2.2"}` в `package.json`, проверка `npm audit` (0 high)
и сборка; если 1.2.2 не закрывает уязвимость — O-1 rejected с текстом аудита.
12. **Эталоны (T5.1).** `layout.clamp` — число строк заголовка при ручной высоте карточки; «unset» — у карточек с
автоматической высотой. В `scenarios.json` добавляется сценарий `saved-short` с сохранённой раскладкой четырёх карточек
разной высоты (120, 136, 200, 320), эталоны перегенерируются `npm run golden` из `links.html@3931a88`; прежние
сценарии и фикстура `hub-basic.board.json` не меняются.
13. **e2e (T15.4, T15.6).** Порт тестового хаба — одна константа в `web/e2e/hub.ts` и `playwright.config.ts` берёт её же;
`scripts/test-hub.sh` перед стартом проверяет, что порт свободен, и выходит с понятной ошибкой. Готовность — e2e
проходит 10 раз подряд.
14. **Состав плана.** 8 задач: хаб; клиент и вход; источник доски; холст и страница доски; задачи и карточка; модель,
хранилище, эталоны; окружение и e2e; документация. Исполнение — SDD-контроллер (sonnet), задачи с готовым кодом —
haiku, интеграция — sonnet, финальное ревью ветки — opus.
## Пункты #29 и их судьба
| Маркер | Что делаем | Задача плана |
|---|---|---|
| T1.1 | 404 на NUL в пути (решение 9) | хаб |
| T1.4 | тест Content-Type для svg/png/ico/woff2/map | хаб |
| T16.1 | убрать дубль `import urllib.*` | хаб |
| T3.2 | 4xx на входе — текст ошибки хаба (решение 6) | клиент и вход |
| T3.3 | содержательный тест 401 и тесты `queryClient`/`queries` | клиент и вход |
| T3.4 | `token.test` проверяет `removeItem` при закрытом хранилище | клиент и вход |
| T3.5 | токен в `AuthError` (решение 5) | клиент и вход |
| T3.6 | интеграционный тест AuthError → LoginScreen | клиент и вход |
| T10.1 | без лишнего `GET /batches` (решение 4) | источник доски |
| T10.3 | строка пакета не стирается (решение 2) | источник доски |
| T10.4 | «на доске снимок из файла» (решение 3) | источник доски |
| T10.5 | тест «поздний ответ» проверяет, что ответ хаба пришёл после файла | источник доски |
| T10.6 | подхват только `refresh` (решение 4) | источник доски |
| M-3 | прокрутка (решение 1); хранение источника — rejected | холст и страница доски |
| T11.1 | стабильные пропсы `Card`, `memo` срабатывает | холст и страница доски |
| T11.2 | отмена `requestAnimationFrame` в `DatesBar` | холст и страница доски |
| T11.3 | тест перетаскивания дорожки | холст и страница доски |
| T11.4 | точные проверки в тестах I-2/I-3 | холст и страница доски |
| T12.1 | `onOpen()` вне updater `setOpen` | холст и страница доски |
| T12.2 | `writeFlag`/`writeHidden` вне updater | холст и страница доски |
| T12.3 | зависимости `useLayoutEffect`, сброс `--head-h` | холст и страница доски |
| T12.4 | MultiSelect: `role="group"` с флажками вместо `listbox` без `option` | холст и страница доски |
| T13.1 | значение фильтра вне вариантов (решение 7) | задачи и карточка |
| O-10 | ссылки цветом темы (решение 8) | задачи и карточка |
| O-11 | «команд по задаче не было» (решение 8) | задачи и карточка |
| T14.2 | ожидания через `waitFor`/`expect.poll` вместо пауз 20 мс | задачи и карточка |
| T14.3 | уникальный id `datalist` (`useId`) | задачи и карточка |
| T2.1 | `canon.ts` — сравнение по кодам символов | модель, хранилище, эталоны |
| T5.1 | сценарий `saved-short` для `layout.clamp` (решение 12) | модель, хранилище, эталоны |
| T6.3 | прямые тесты `util.ts` | модель, хранилище, эталоны |
| T7.1 | `initialWho` считает `whoItems` один раз | модель, хранилище, эталоны |
| T9.1 | `colWidths()` пропускает только конечные числа | модель, хранилище, эталоны |
| T9.3 | тесты `toggleLane`/`moveLane`/`resetLayout`/`exportFileName`/`mode=all`, закрытое хранилище | модель, хранилище, эталоны |
| T9.4 | порядок импортов `fromJson.ts` | модель, хранилище, эталоны |
| T2.2 | шрифты из `@fontsource` (решение 10) | окружение и e2e |
| O-1 | `overrides` source-map-js (решение 11) | окружение и e2e |
| T4.1 | тест «Задачи и обратно» с обратным переходом | окружение и e2e |
| T4.2 | тест ссылок страницы «не найдено» | окружение и e2e |
| T4.3 | убрать `App.Layout`, тест берёт `Layout` | окружение и e2e |
| T4.4 | один импорт из `@testing-library/react` | окружение и e2e |
| T4.5 | очистка атрибутов в `theme.test` | окружение и e2e |
| T15.1 | высота карточек в geometry сверяется с эталоном, не с формулой | окружение и e2e |
| T15.2 | растяжка через `expect.poll`, без литерала 240 | окружение и e2e |
| T15.4 | порт — одна константа (решение 13) | окружение и e2e |
| T15.6 | проверка свободного порта (решение 13) | окружение и e2e |
| M-5 | rejected: хаб запускается из checkout, wheel не используется | — |
## Risks / Trade-offs
- Перегенерация эталонов (T5.1) может показать расхождение порта с `links.html` в `layout.clamp` → это дефект порта,
чинится в коде, эталон не подгоняется.
- Стабилизация пропсов `Card` (T11.1) затрагивает перетаскивание → e2e board/geometry обязательны в задаче.
- Восстановление прокрутки до отрисовки высот даст прыжок → восстанавливать после первой отрисовки с данными.
## Decisions made without discussion
Нумерация R-n — заново в пределах этой фичи.
- **R-1 (решение 1), 15.10.2026.** Прокрутка хранится в переменной модуля, не в `sessionStorage`: переживает переходы
внутри вкладки, но не перезагрузку. Обоснование: владелец согласился на «помнить, пока открыта вкладка» в смысле
переходов; перезагрузка — новая сессия доски. Цена ошибки: если нужна и после F5 — заменить на `sessionStorage`, одна строка.
- **R-2 (решение 4), 15.10.2026.** Тип пакета определяется дополнительным `GET /batches/{id}` по кандидату, а не
изменением `GET /batches` в хабе. Обоснование: API хаба в этом change не меняется (Non-Goals web-app). Цена: один лишний
запрос при найденном кандидате.
- **R-3 (решение 5), 15.10.2026.** Запоздавший 401 со старым токеном молча игнорируется. Обоснование: новый токен уже
проверен `GET /settings`. Цена: если новый токен тоже станет неверным, экран входа появится на следующем запросе.
- **R-4 (решение 7), 15.10.2026.** Значение фильтра вне вариантов подписывается «(нет на доске)». Цена — текст подписи.
- **R-5 (решение 11), 15.10.2026.** O-1 закрывается `overrides`, а не обновлением прямых зависимостей. Обоснование:
уязвимость только в транзитивной dev-зависимости; обновление vite/vitest шире задачи. Цена: `overrides` надо снять, когда
прямые зависимости подтянут 1.2.2 сами.
- **R-6 (решение 12), 15.10.2026.** T5.1 закрывается новым сценарием `saved-short` с ручными высотами, а не новыми
задачами в фикстуре: обрезку заголовка в генераторе определяет только ручная высота (заглушка DOM меряет все карточки
одинаково). Цена: длинные темы на реальной вёрстке проверяет e2e, не эталон.
- **R-7 (M-5), 15.10.2026.** M-5 rejected — показано владельцу в сводке понимания до утверждения разделов, возражений не было.