146 lines
17 KiB
Markdown
146 lines
17 KiB
Markdown
## 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 — показано владельцу в сводке понимания до утверждения разделов, возражений не было.
|