17 KiB
17 KiB
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
- Прокрутка доски (M-3). При размонтировании
BoardPageзапоминаетwindow.scrollX/scrollY(иscrollLeftгоризонтальной обёртки холста, если она прокручивается сама) в переменной модуля; при монтировании, после первой отрисовки холста с данными, восстанавливает. Перезагрузка страницы открывает доску сверху, как сейчас. Состояние источника не поднимается выше страницы — rejected часть M-3 (решение владельца). - Строка состояния при идущем пакете (T10.3).
reload()иloadFile()не сбрасывают событиеbatch, покаactive !== null; сообщения о файле (file-error,json-applied) по-прежнему заменяют его (одно событие, M-5 #20), а следующий ответ опроса возвращает строку пакета. - Ошибка хаба при снимке из файла (T10.4). Событие
hub-downполучает признакfromFile; текст — «хаб не отвечает — на доске снимок из файла». Он ставится, когда снимок хаба не загрузился при показанном файле (перечитывание после завершения пакета) и когда JSON-снимок загружен без данных хаба;hasDataв последнем случае — есть ли в модели задачи. «Перечитать» по-прежнему возвращает источник «хаб». - Подхват пакета (T10.6, T10.1). Кандидат из
GET /batches(авторviewer,queued/running) проверяется запросомGET /batches/{id}: подхватывается, только если первая команда —refresh. Подхват не запускается по обновлению снимка, которое вызвал самfinish()(флаг «снимок после своего пакета»), — без лишнегоGET /batches(T10.1). - Токен в ошибке (T3.5).
call()кладёт вAuthErrorтокен, с которым ушёл запрос;clearTokenвызывается только если этот токен равен текущему. Запоздавший 401 от прежнего токена игнорируется. - Вход и 4xx (T3.2).
LoginScreen:ApiError→ «хаб отверг запрос: <detail или error>»;HubDownError— прежний текст «хаб не отвечает». - Фильтр вне вариантов (T13.1). Если значение фильтра из адреса не найдено в
opts[name], оно добавляется первым вариантом с подписью «<значение> (нет на доске)» — как текущее значение «вида работы» в заметке. - Карточка задачи (O-10, O-11). Ссылки страниц задач —
color: var(--accent)из темы (как в списке); пустойcommands— строка «команд по задаче не было» вместо пустой таблицы. - NUL в пути (T1.1).
h_appотвечает 404not_found, если в декодированном пути есть\x00или разрешение пути падает сValueError/OSError; остальное поведениеh_appне меняется. - Шрифты (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. - source-map-js (O-1).
overrides: {"source-map-js": "1.2.2"}вpackage.json, проверкаnpm audit(0 high) и сборка; если 1.2.2 не закрывает уязвимость — O-1 rejected с текстом аудита. - Эталоны (T5.1).
layout.clamp— число строк заголовка при ручной высоте карточки; «unset» — у карточек с автоматической высотой. Вscenarios.jsonдобавляется сценарийsaved-shortс сохранённой раскладкой четырёх карточек разной высоты (120, 136, 200, 320), эталоны перегенерируютсяnpm run goldenизlinks.html@3931a88; прежние сценарии и фикстураhub-basic.board.jsonне меняются. - e2e (T15.4, T15.6). Порт тестового хаба — одна константа в
web/e2e/hub.tsиplaywright.config.tsберёт её же;scripts/test-hub.shперед стартом проверяет, что порт свободен, и выходит с понятной ошибкой. Готовность — e2e проходит 10 раз подряд. - Состав плана. 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 — показано владельцу в сводке понимания до утверждения разделов, возражений не было.